| """ |
| ์ข
ํฉ ์ง๋จ ์์ง ๋ชจ๋. |
| |
| ์์ธก(GranitePredictor), XAI(ShapExplainer), ์ ์(SustainabilityScorer) ๋ฅผ |
| ํตํฉํ์ฌ ๋จ์ผ API ํธ์ถ๋ก ํ๊ต ์ ์ฒด ์ง๋จ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค. |
| |
| ๋ํ ์ ์ฑ
์๋ฎฌ๋ ์ด์
๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค: |
| - ๊ต์ฌ 1์ธ๋น ํ์ ์ ๊ฐ์ ์๋๋ฆฌ์ค |
| - ๋ณตํฉ์์ค ๋์
์๋๋ฆฌ์ค |
| - ๊ธฐ๊ฐ์ ๊ต์ ์ ๊ท์ง ์ ํ ์๋๋ฆฌ์ค |
| """ |
|
|
| from __future__ import annotations |
|
|
| import logging |
| from dataclasses import dataclass, field |
| from typing import Any |
|
|
| import pandas as pd |
|
|
| from src.analytics.predictor import ForecastResult, GranitePredictor |
| from src.analytics.scorer import SustainabilityScore, SustainabilityScorer |
| from src.analytics.xai import ShapExplainer, ShapResult |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| SIMULATION_SCENARIOS: dict[str, dict[str, Any]] = { |
| "improve_teacher_ratio": { |
| "label": "๊ต์ฌ 1์ธ๋น ํ์ ์ ๊ฐ์ ", |
| "description": "๊ธฐ๊ฐ์ ๊ต์์ ์ ๊ท์ง์ผ๋ก ์ ํํ๊ณ ๊ต์์ 1๋ช
์ถ๊ฐ ๋ฐฐ์นํฉ๋๋ค.", |
| "delta": {"teacher_count": 1, "temp_teacher_count": -1}, |
| }, |
| "introduce_complex_facility": { |
| "label": "๋ณตํฉ์์ค ๋์
", |
| "description": "๋์๊ดยท์ฒด์ก๊ด ๋ฑ ๋ณตํฉ์์ค์ ๋์
ํ์ฌ ์ง์ญ ์ ์
ํจ๊ณผ๋ฅผ ๋ฐ์ํฉ๋๋ค.", |
| "delta": {"established_year_delta": 20, "transfer_net_avg": 3}, |
| }, |
| "community_revitalization": { |
| "label": "์ง์ญ ์ฐ๊ณ ๊ฐํ", |
| "description": "์ง์ญ ์ฌ์ ๊ฑฐ์ ์ผ๋ก ์ ํํ์ฌ ์๋ฉธ์ํ์ง์๋ฅผ ๊ฐ์ ํฉ๋๋ค.", |
| "delta": {"population_risk_index": 0.3}, |
| }, |
| } |
|
|
|
|
| @dataclass |
| class DiagnosticsResult: |
| """์ข
ํฉ ์ง๋จ ๊ฒฐ๊ณผ ์ปจํ
์ด๋.""" |
|
|
| schul_code: str |
| school_name: str |
| status_label: str |
| status_code: int |
| sustainability_score: float |
| forecast: ForecastResult |
| shap_result: ShapResult |
| score_detail: SustainabilityScore |
| simulations: dict[str, dict[str, Any]] = field(default_factory=dict) |
| metadata: dict[str, Any] = field(default_factory=dict) |
|
|
| def to_dict(self) -> dict[str, Any]: |
| return { |
| "schul_code": self.schul_code, |
| "school_name": self.school_name, |
| "status_label": self.status_label, |
| "status_code": self.status_code, |
| "sustainability_score": round(self.sustainability_score, 2), |
| "forecast": self.forecast.to_dict(), |
| "shap_result": self.shap_result.to_dict(), |
| "score_detail": self.score_detail.to_dict(), |
| "simulations": self.simulations, |
| "metadata": self.metadata, |
| } |
|
|
|
|
| class DiagnosticsEngine: |
| """ |
| ํ๊ต ์ข
ํฉ ์ง๋จ ์์ง. |
| |
| ์ฌ์ฉ ์:: |
| |
| engine = DiagnosticsEngine() |
| result = engine.diagnose( |
| schul_code="7431234", |
| school_data={...}, |
| timeseries=pd.Series({2018: 120, ...}), |
| ) |
| """ |
|
|
| def __init__( |
| self, |
| predictor: GranitePredictor | None = None, |
| shap_explainer: ShapExplainer | None = None, |
| scorer: SustainabilityScorer | None = None, |
| ) -> None: |
| self._predictor = predictor or GranitePredictor() |
| self._shap = shap_explainer or ShapExplainer() |
| self._scorer = scorer or SustainabilityScorer() |
|
|
| def _run_simulation( |
| self, |
| scenario_id: str, |
| base_data: dict[str, Any], |
| ) -> dict[str, Any]: |
| """ |
| ๋จ์ผ ์ ์ฑ
์๋๋ฆฌ์ค๋ฅผ ์ ์ฉํ ์ง์ ๊ฐ๋ฅ์ฑ ์ ์๋ฅผ ๋ฐํํฉ๋๋ค. |
| |
| Parameters |
| ---------- |
| scenario_id: |
| SIMULATION_SCENARIOS ์ ํค. |
| base_data: |
| ์๋ณธ ํ๊ต ๋ฐ์ดํฐ ๋์
๋๋ฆฌ. |
| |
| Returns |
| ------- |
| dict |
| ์๋๋ฆฌ์ค ๋ ์ด๋ธ, ์ค๋ช
, ๋ณ๊ฒฝ ์ /ํ ์ ์ ๋น๊ต. |
| """ |
| scenario = SIMULATION_SCENARIOS[scenario_id] |
| delta = scenario["delta"] |
|
|
| modified = dict(base_data) |
| for key, change in delta.items(): |
| if key == "established_year_delta": |
| |
| orig = int(modified.get("established_year", 0) or 0) |
| if orig > 0: |
| modified["established_year"] = orig + int(change) |
| elif key in modified: |
| modified[key] = (float(modified[key] or 0)) + float(change) |
| else: |
| modified[key] = float(change) |
|
|
| base_score = self._scorer.compute(base_data.get("sd_schul_code", ""), base_data) |
| sim_score = self._scorer.compute(base_data.get("sd_schul_code", ""), modified) |
|
|
| return { |
| "label": scenario["label"], |
| "description": scenario["description"], |
| "base_total_score": round(base_score.total_score, 2), |
| "sim_total_score": round(sim_score.total_score, 2), |
| "delta_score": round(sim_score.total_score - base_score.total_score, 2), |
| "sim_curriculum_score": round(sim_score.curriculum_score, 2), |
| "sim_personnel_score": round(sim_score.personnel_score, 2), |
| "sim_facility_score": round(sim_score.facility_score, 2), |
| "sim_community_score": round(sim_score.community_score, 2), |
| } |
|
|
| def diagnose( |
| self, |
| schul_code: str, |
| school_data: dict[str, Any], |
| timeseries: pd.Series, |
| horizon_years: int = 5, |
| run_simulations: bool = True, |
| ) -> DiagnosticsResult: |
| """ |
| ํ๊ต ์ข
ํฉ ์ง๋จ์ ์ํํฉ๋๋ค. |
| |
| Parameters |
| ---------- |
| schul_code: |
| ๋์ ํ๊ต SD_SCHUL_CODE. |
| school_data: |
| ํ๊ต ์งํ ๋์
๋๋ฆฌ (SustainabilityScorer, ShapExplainer ๊ณตํต ์
๋ ฅ). |
| timeseries: |
| ์ฐ๋(int) ์ธ๋ฑ์ค์ ํ์ ์ ์๊ณ์ด Series. |
| horizon_years: |
| ์์ธก ๊ธฐ๊ฐ (๋
). |
| run_simulations: |
| ์ ์ฑ
์๋ฎฌ๋ ์ด์
์คํ ์ฌ๋ถ. |
| |
| Returns |
| ------- |
| DiagnosticsResult |
| """ |
| school_name = str(school_data.get("school_name", schul_code)) |
| logger.info("์ง๋จ ์์: schul_code=%s school_name=%s", schul_code, school_name) |
|
|
| |
| try: |
| forecast = self._predictor.predict( |
| schul_code=schul_code, |
| timeseries=timeseries, |
| horizon_years=horizon_years, |
| target_col="student_count", |
| ) |
| except Exception as exc: |
| logger.error("์์ธก ์คํจ: schul_code=%s error=%s", schul_code, exc) |
| raise |
|
|
| |
| school_data_enriched = dict(school_data) |
| school_data_enriched["student_count_history"] = forecast.context_values |
| school_data_enriched["sd_schul_code"] = schul_code |
|
|
| |
| try: |
| shap_result = self._shap.explain(schul_code=schul_code, school_data=school_data_enriched) |
| except Exception as exc: |
| logger.error("SHAP ๋ถ์ ์คํจ: schul_code=%s error=%s", schul_code, exc) |
| raise |
|
|
| |
| try: |
| score_detail = self._scorer.compute(schul_code=schul_code, school_data=school_data_enriched) |
| except Exception as exc: |
| logger.error("์ ์ ์ฐ์ถ ์คํจ: schul_code=%s error=%s", schul_code, exc) |
| raise |
|
|
| |
| simulations: dict[str, dict[str, Any]] = {} |
| if run_simulations: |
| for sid in SIMULATION_SCENARIOS: |
| try: |
| simulations[sid] = self._run_simulation(sid, school_data_enriched) |
| except Exception as exc: |
| logger.warning("์๋ฎฌ๋ ์ด์
์คํจ: scenario=%s error=%s", sid, exc) |
| simulations[sid] = {"error": str(exc)} |
|
|
| result = DiagnosticsResult( |
| schul_code=schul_code, |
| school_name=school_name, |
| status_label=shap_result.status_label, |
| status_code=shap_result.status_code, |
| sustainability_score=score_detail.total_score, |
| forecast=forecast, |
| shap_result=shap_result, |
| score_detail=score_detail, |
| simulations=simulations, |
| metadata={ |
| "diagnosed_at": pd.Timestamp.now().isoformat(), |
| "model_version": forecast.model_version, |
| "data_quality": school_data_enriched.get("data_quality_score", 1.0), |
| }, |
| ) |
|
|
| logger.info( |
| "์ง๋จ ์๋ฃ: schul_code=%s status=%s score=%.1f", |
| schul_code, |
| shap_result.status_label, |
| score_detail.total_score, |
| ) |
| return result |
|
|