kr4phy's picture
Sync from GitHub
cff6ac7
Raw
History Blame Contribute Delete
9.92 kB
"""
ํ•™๊ต ์ง€์† ๊ฐ€๋Šฅ์„ฑ ์ ์ˆ˜(Sustainability Score) ์‚ฐ์ถœ ๋ชจ๋“ˆ.
KEDI(ํ•œ๊ตญ๊ต์œก๊ฐœ๋ฐœ์›) ๊ณตํ†ต์ง€ํ‘œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ 4๊ฐœ ์˜์—ญ์˜ ๊ฐ€์ค‘ ํ•ฉ์‚ฐ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
๊ฐ€์ค‘์น˜:
๊ต์œก๊ณผ์ • ์šด์˜ (curriculum) : 45%
์ธ๋ ฅ ๊ตฌ์„ฑ (personnel) : 25%
์‹œ์„ค ํ™˜๊ฒฝ (facility) : 20%
์ง€์—ญ ์—ฐ๊ณ„ (community) : 10%
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Any
import numpy as np
logger = logging.getLogger(__name__)
# ์˜์—ญ๋ณ„ ๊ฐ€์ค‘์น˜
WEIGHTS = {
"curriculum": 0.45,
"personnel": 0.25,
"facility": 0.20,
"community": 0.10,
}
def _clamp(value: float, lo: float = 0.0, hi: float = 100.0) -> float:
"""๊ฐ’์„ [lo, hi] ๋ฒ”์œ„๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค."""
return float(np.clip(value, lo, hi))
@dataclass
class SustainabilityScore:
"""์ง€์† ๊ฐ€๋Šฅ์„ฑ ์ ์ˆ˜ ๊ฒฐ๊ณผ ์ปจํ…Œ์ด๋„ˆ."""
schul_code: str
total_score: float # 0~100
curriculum_score: float # ๊ต์œก๊ณผ์ • ์šด์˜
personnel_score: float # ์ธ๋ ฅ ๊ตฌ์„ฑ
facility_score: float # ์‹œ์„ค ํ™˜๊ฒฝ
community_score: float # ์ง€์—ญ ์—ฐ๊ณ„
sub_scores: dict[str, float] = field(default_factory=dict)
warnings: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"schul_code": self.schul_code,
"total_score": round(self.total_score, 2),
"curriculum_score": round(self.curriculum_score, 2),
"personnel_score": round(self.personnel_score, 2),
"facility_score": round(self.facility_score, 2),
"community_score": round(self.community_score, 2),
"sub_scores": {k: round(v, 2) for k, v in self.sub_scores.items()},
"warnings": self.warnings,
}
class SustainabilityScorer:
"""
ํ•™๊ต ์ง€ํ‘œ๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ์ง€์† ๊ฐ€๋Šฅ์„ฑ ์ ์ˆ˜๋ฅผ ์‚ฐ์ถœํ•˜๋Š” ํด๋ž˜์Šค.
๊ฐ ํ•˜์œ„ ์ ์ˆ˜๋Š” 0~100 ๋ฒ”์œ„๋กœ ์ •๊ทœํ™”๋ฉ๋‹ˆ๋‹ค.
์‚ฌ์šฉ ์˜ˆ::
scorer = SustainabilityScorer()
score = scorer.compute(
schul_code="7431234",
school_data={
"student_count": 45,
"class_count": 6,
"teacher_count": 5,
"temp_teacher_count": 1,
"established_year": 1980,
"transfer_net_avg": -5,
"population_risk_index": 0.6,
}
)
"""
# ๊ต์œก๊ณผ์ • ์šด์˜ ๊ธฐ์ค€๊ฐ’ (ํ•™๊ธ‰๋‹น ํ•™์ƒ ์ˆ˜)
_STUDENTS_PER_CLASS_IDEAL = 20.0
_STUDENTS_PER_CLASS_MIN = 1.0
# ์ธ๋ ฅ ๊ตฌ์„ฑ ๊ธฐ์ค€๊ฐ’
_TEACHER_STUDENT_RATIO_IDEAL = 15.0 # ๊ต์‚ฌ 1์ธ๋‹น ํ•™์ƒ 15๋ช…์ด ์ด์ƒ์ 
_TEMP_TEACHER_RATIO_SAFE = 0.2 # ๊ธฐ๊ฐ„์ œ ๋น„์œจ 20% ์ดํ•˜ ์–‘ํ˜ธ
# ์‹œ์„ค ๋…ธํ›„๋„ ๊ธฐ์ค€๊ฐ’ (์—ฐ)
_FACILITY_AGE_NEW = 10
_FACILITY_AGE_OLD = 40
# ์ง€์—ญ ์—ฐ๊ณ„ ๊ธฐ์ค€๊ฐ’ (์†Œ๋ฉธ์œ„ํ—˜์ง€์ˆ˜)
_EXTINCTION_RISK_SAFE = 1.5 # 1.5 ์ด์ƒ: ์•ˆ์ „
_EXTINCTION_RISK_DANGER = 0.5 # 0.5 ์ดํ•˜: ์†Œ๋ฉธ ์œ„ํ—˜
def _curriculum_score(self, data: dict[str, Any]) -> tuple[float, dict[str, float]]:
"""๊ต์œก๊ณผ์ • ์šด์˜ ์ ์ˆ˜ (0~100)."""
sub: dict[str, float] = {}
student = float(data.get("student_count", 0) or 0)
class_count = float(data.get("class_count", 0) or 0)
# ํ•™๊ธ‰๋‹น ํ•™์ƒ ์ˆ˜ ์ ์ˆ˜ (๋„ˆ๋ฌด ์ ๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ๊ฐ์ )
if class_count > 0:
ratio = student / class_count
# ์ด์ƒ์ ์ธ ํ•™๊ธ‰๋‹น ํ•™์ƒ์ˆ˜(20๋ช…) ๊ธฐ์ค€ ์ •๊ทœํ™”, ์ตœ๋Œ€ 25๋ช…
sub["students_per_class"] = _clamp(
(ratio / self._STUDENTS_PER_CLASS_IDEAL) * 100
if ratio <= self._STUDENTS_PER_CLASS_IDEAL
else max(0, 100 - (ratio - self._STUDENTS_PER_CLASS_IDEAL) * 5),
)
else:
sub["students_per_class"] = max(0.0, min(100.0, student * 2))
# ์ „์ฒด ํ•™์ƒ ์ˆ˜ ๊ธฐ๋ฐ˜ ์šด์˜ ๊ฐ€๋Šฅ์„ฑ ์ ์ˆ˜
if student >= 60:
sub["operation_viability"] = 100.0
elif student >= 30:
sub["operation_viability"] = 60.0 + (student - 30) * (40 / 30)
elif student >= 10:
sub["operation_viability"] = 20.0 + (student - 10) * (40 / 20)
else:
sub["operation_viability"] = max(0.0, student * 2)
score = float(np.mean(list(sub.values())))
return _clamp(score), sub
def _personnel_score(self, data: dict[str, Any]) -> tuple[float, dict[str, float]]:
"""์ธ๋ ฅ ๊ตฌ์„ฑ ์ ์ˆ˜ (0~100)."""
sub: dict[str, float] = {}
student = float(data.get("student_count", 1) or 1)
teacher = float(data.get("teacher_count", 1) or 1)
temp = float(data.get("temp_teacher_count", 0) or 0)
# ๊ต์‚ฌ 1์ธ๋‹น ํ•™์ƒ ์ˆ˜ ์ ์ˆ˜ (์ด์ƒ์ : 15๋ช…, ๋งŽ์œผ๋ฉด ๊ฐ์ )
ts_ratio = student / max(teacher, 1)
if ts_ratio <= self._TEACHER_STUDENT_RATIO_IDEAL:
sub["teacher_ratio"] = 100.0
else:
sub["teacher_ratio"] = _clamp(
100.0 - (ts_ratio - self._TEACHER_STUDENT_RATIO_IDEAL) * 3
)
# ๊ธฐ๊ฐ„์ œ ๊ต์› ๋น„์œจ ์ ์ˆ˜ (๋‚ฎ์„์ˆ˜๋ก ์–‘ํ˜ธ)
temp_ratio = temp / max(teacher, 1)
if temp_ratio <= self._TEMP_TEACHER_RATIO_SAFE:
sub["temp_teacher_ratio"] = 100.0
else:
sub["temp_teacher_ratio"] = _clamp(
100.0 - (temp_ratio - self._TEMP_TEACHER_RATIO_SAFE) * 100
)
score = float(np.mean(list(sub.values())))
return _clamp(score), sub
def _facility_score(self, data: dict[str, Any]) -> tuple[float, dict[str, float]]:
"""์‹œ์„ค ํ™˜๊ฒฝ ์ ์ˆ˜ (0~100)."""
sub: dict[str, float] = {}
import pandas as pd
established = int(data.get("established_year", 0) or 0)
current_year = pd.Timestamp.now().year
if established > 0:
age = current_year - established
if age <= self._FACILITY_AGE_NEW:
sub["facility_age"] = 100.0
elif age >= self._FACILITY_AGE_OLD:
sub["facility_age"] = 20.0
else:
sub["facility_age"] = 100.0 - (age - self._FACILITY_AGE_NEW) * (
80.0 / (self._FACILITY_AGE_OLD - self._FACILITY_AGE_NEW)
)
else:
sub["facility_age"] = 50.0 # ์ •๋ณด ์—†์Œ โ†’ ์ค‘๊ฐ„๊ฐ’
score = float(np.mean(list(sub.values())))
return _clamp(score), sub
def _community_score(self, data: dict[str, Any]) -> tuple[float, dict[str, float]]:
"""์ง€์—ญ ์—ฐ๊ณ„ ์ ์ˆ˜ (0~100)."""
sub: dict[str, float] = {}
# ์†Œ๋ฉธ์œ„ํ—˜์ง€์ˆ˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜
risk_index = float(data.get("population_risk_index", 1.0) or 1.0)
if risk_index >= self._EXTINCTION_RISK_SAFE:
sub["extinction_risk"] = 100.0
elif risk_index <= self._EXTINCTION_RISK_DANGER:
sub["extinction_risk"] = 0.0
else:
sub["extinction_risk"] = (
(risk_index - self._EXTINCTION_RISK_DANGER)
/ (self._EXTINCTION_RISK_SAFE - self._EXTINCTION_RISK_DANGER)
) * 100
# ์ „์ถœ์ž… ์ˆœ ๋ณ€ํ™” ์ ์ˆ˜ (์ˆœ ์ฆ๊ฐ€ โ†’ ์ข‹์Œ)
transfer_net = float(data.get("transfer_net_avg", 0) or 0)
if transfer_net >= 0:
sub["transfer_net"] = min(100.0, 70.0 + transfer_net * 2)
else:
sub["transfer_net"] = _clamp(70.0 + transfer_net * 5)
score = float(np.mean(list(sub.values())))
return _clamp(score), sub
def compute(
self,
schul_code: str,
school_data: dict[str, Any],
) -> SustainabilityScore:
"""
ํ•™๊ต ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ง€์† ๊ฐ€๋Šฅ์„ฑ ์ ์ˆ˜๋ฅผ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค.
Parameters
----------
schul_code:
๋Œ€์ƒ ํ•™๊ต SD_SCHUL_CODE.
school_data:
ํ•™๊ต ์ง€ํ‘œ ๋”•์…”๋„ˆ๋ฆฌ. ์ฃผ์š” ํ‚ค:
student_count, class_count, teacher_count,
temp_teacher_count, established_year,
transfer_net_avg, population_risk_index.
Returns
-------
SustainabilityScore
"""
warnings: list[str] = []
c_score, c_sub = self._curriculum_score(school_data)
p_score, p_sub = self._personnel_score(school_data)
f_score, f_sub = self._facility_score(school_data)
com_score, com_sub = self._community_score(school_data)
total = (
c_score * WEIGHTS["curriculum"]
+ p_score * WEIGHTS["personnel"]
+ f_score * WEIGHTS["facility"]
+ com_score * WEIGHTS["community"]
)
# ๊ฒฝ๊ณ  ์ƒ์„ฑ
if school_data.get("student_count", 100) < 10:
warnings.append("ํ•™์ƒ ์ˆ˜๊ฐ€ 10๋ช… ๋ฏธ๋งŒ์œผ๋กœ ๋งค์šฐ ์œ„ํ—˜ํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.")
if school_data.get("data_quality_score", 1.0) < 0.5:
warnings.append("๋ฐ์ดํ„ฐ ์‹ ๋ขฐ๋„๊ฐ€ ๋‚ฎ์•„ ์ ์ˆ˜ ์ •ํ™•๋„๊ฐ€ ์ œํ•œ๋ฉ๋‹ˆ๋‹ค.")
all_sub = {
**{f"curriculum_{k}": v for k, v in c_sub.items()},
**{f"personnel_{k}": v for k, v in p_sub.items()},
**{f"facility_{k}": v for k, v in f_sub.items()},
**{f"community_{k}": v for k, v in com_sub.items()},
}
logger.info(
"์ง€์† ๊ฐ€๋Šฅ์„ฑ ์ ์ˆ˜ ์‚ฐ์ถœ: schul_code=%s total=%.1f",
schul_code,
total,
)
return SustainabilityScore(
schul_code=schul_code,
total_score=_clamp(total),
curriculum_score=c_score,
personnel_score=p_score,
facility_score=f_score,
community_score=com_score,
sub_scores=all_sub,
warnings=warnings,
)