kr4phy's picture
Sync from GitHub
cff6ac7
Raw
History Blame Contribute Delete
4.44 kB
"""
GranitePredictor ๋‹จ์œ„ ํ…Œ์ŠคํŠธ.
์‹ค์ œ ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ ์—†์ด statsforecast AutoARIMA fallback ๊ฒฝ๋กœ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from src.analytics.predictor import ForecastResult, GranitePredictor, _monthly_to_yearly
class TestMonthlyToYearly:
def test_last_month_aggregation(self) -> None:
monthly = np.arange(24, dtype=float) # 24๊ฐœ์›” = 2๋…„
yearly = _monthly_to_yearly(monthly, agg="last")
assert len(yearly) == 2
assert yearly[0] == 11.0 # ์ฒซ ํ•ด์˜ ๋งˆ์ง€๋ง‰ ๋‹ฌ
assert yearly[1] == 23.0 # ๋‘ ๋ฒˆ์งธ ํ•ด์˜ ๋งˆ์ง€๋ง‰ ๋‹ฌ
def test_mean_aggregation(self) -> None:
monthly = np.ones(12, dtype=float) * 5.0 # 12๊ฐœ์›”, ๋ชจ๋‘ 5
yearly = _monthly_to_yearly(monthly, agg="mean")
assert len(yearly) == 1
assert yearly[0] == pytest.approx(5.0)
def test_remainder_months_handled(self) -> None:
monthly = np.ones(14, dtype=float) # 14๊ฐœ์›” = 1๋…„ + 2๊ฐœ์›” ๋‚˜๋จธ์ง€
yearly = _monthly_to_yearly(monthly)
assert len(yearly) == 2 # 1๋…„ + ๋‚˜๋จธ์ง€
class TestForecastResult:
def test_to_dict_has_all_keys(self) -> None:
result = ForecastResult(
schul_code="TEST",
target_col="student_count",
forecast_years=[2024, 2025],
point_forecast=[80.0, 70.0],
lower_bound=[60.0, 50.0],
upper_bound=[100.0, 90.0],
model_version="arima-fallback",
context_years=[2020, 2021, 2022, 2023],
context_values=[120.0, 110.0, 100.0, 90.0],
)
d = result.to_dict()
required = {"schul_code", "target_col", "forecast_years", "point_forecast",
"lower_bound", "upper_bound", "model_version"}
assert required.issubset(d.keys())
class TestGranitePredictor:
def _make_timeseries(self, n: int = 5) -> pd.Series:
years = list(range(2019, 2019 + n))
values = [120.0, 110.0, 98.0, 87.0, 75.0][:n]
return pd.Series(dict(zip(years, values)))
def test_insufficient_data_raises(self) -> None:
predictor = GranitePredictor()
ts = pd.Series({2023: 50.0}) # ๋‹จ 1๊ฐœ โ†’ ๋ถ€์กฑ
with pytest.raises(ValueError, match="๋ถ€์กฑ"):
predictor.predict("TEST", ts)
def test_empty_series_raises(self) -> None:
predictor = GranitePredictor()
with pytest.raises(ValueError):
predictor.predict("TEST", pd.Series(dtype=float))
def test_arima_fallback_produces_result(self) -> None:
"""TTM ๋กœ๋“œ ์—†์ด ARIMA fallback ์ด ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆ."""
predictor = GranitePredictor()
# TTM ๋กœ๋“œ๋ฅผ ๊ฐ•์ œ๋กœ ์‹คํŒจ์‹œ์ผœ ARIMA fallback ์œผ๋กœ ์ด๋™
predictor._model_version = predictor._MODEL_VERSION_ARIMA
ts = self._make_timeseries()
try:
result = predictor.predict("TEST", ts, horizon_years=3)
assert isinstance(result, ForecastResult)
assert len(result.forecast_years) == 3
assert len(result.point_forecast) == 3
assert all(v >= 0 for v in result.point_forecast)
assert result.model_version == predictor._MODEL_VERSION_ARIMA
except ImportError:
pytest.skip("statsforecast ๋ฏธ์„ค์น˜, ARIMA fallback ๊ฑด๋„ˆ๋œ€")
def test_forecast_years_sequential(self) -> None:
"""์˜ˆ์ธก ์—ฐ๋„๊ฐ€ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋Š”์ง€ ๊ฒ€์ฆ."""
predictor = GranitePredictor()
predictor._model_version = predictor._MODEL_VERSION_ARIMA
ts = self._make_timeseries()
try:
result = predictor.predict("TEST", ts, horizon_years=3)
years = result.forecast_years
for i in range(1, len(years)):
assert years[i] == years[i - 1] + 1
except ImportError:
pytest.skip("statsforecast ๋ฏธ์„ค์น˜")
def test_lower_bound_le_upper_bound(self) -> None:
predictor = GranitePredictor()
predictor._model_version = predictor._MODEL_VERSION_ARIMA
ts = self._make_timeseries()
try:
result = predictor.predict("TEST", ts, horizon_years=3)
for lo, hi in zip(result.lower_bound, result.upper_bound):
assert lo <= hi
except ImportError:
pytest.skip("statsforecast ๋ฏธ์„ค์น˜")