Skip to content

Commit e250307

Browse files
committed
feat: evaluation tests
1 parent 6dec263 commit e250307

1 file changed

Lines changed: 215 additions & 0 deletions

File tree

tests/test_evaluation.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import pytest
22
import pandas as pd
33
import numpy as np
4+
from unittest.mock import MagicMock, patch
5+
import tempfile
6+
import os
47

58
from epftoolbox2.evaluators import MAEEvaluator
69
from epftoolbox2.results.report import EvaluationReport
10+
from epftoolbox2.pipelines.model_pipeline import ModelPipeline
11+
from epftoolbox2.exporters.terminal import TerminalExporter
12+
from epftoolbox2.exporters.excel import ExcelExporter
713

814

915
class TestMAEEvaluator:
@@ -83,3 +89,212 @@ def test_by_year(self, sample_results):
8389

8490
assert "year" in by_year.columns
8591
assert len(by_year) == 2 # 2 models × 1 year
92+
93+
94+
class TestModelPipeline:
95+
@pytest.fixture
96+
def sample_data(self):
97+
dates = pd.date_range("2024-01-01", periods=48 * 10, freq="h")
98+
return pd.DataFrame({"price": np.random.randn(len(dates))}, index=dates)
99+
100+
@pytest.fixture
101+
def mock_model(self):
102+
model = MagicMock()
103+
model.name = "MockModel"
104+
model.run.return_value = [
105+
{"prediction": 10, "actual": 12, "hour": 0, "horizon": 1, "target_date": "2024-01-01"},
106+
{"prediction": 20, "actual": 18, "hour": 1, "horizon": 1, "target_date": "2024-01-01"},
107+
]
108+
return model
109+
110+
def test_init(self):
111+
pipeline = ModelPipeline()
112+
assert pipeline.models == []
113+
assert pipeline.evaluators == []
114+
assert pipeline.exporters == []
115+
116+
def test_add_model_returns_self(self, mock_model):
117+
pipeline = ModelPipeline()
118+
result = pipeline.add_model(mock_model)
119+
assert result is pipeline
120+
assert len(pipeline.models) == 1
121+
122+
def test_add_evaluator_returns_self(self):
123+
pipeline = ModelPipeline()
124+
evaluator = MAEEvaluator()
125+
result = pipeline.add_evaluator(evaluator)
126+
assert result is pipeline
127+
assert len(pipeline.evaluators) == 1
128+
129+
def test_add_exporter_returns_self(self):
130+
pipeline = ModelPipeline()
131+
exporter = MagicMock()
132+
result = pipeline.add_exporter(exporter)
133+
assert result is pipeline
134+
assert len(pipeline.exporters) == 1
135+
136+
def test_builder_pattern_chaining(self, mock_model):
137+
pipeline = ModelPipeline().add_model(mock_model).add_evaluator(MAEEvaluator()).add_exporter(MagicMock())
138+
assert len(pipeline.models) == 1
139+
assert len(pipeline.evaluators) == 1
140+
assert len(pipeline.exporters) == 1
141+
142+
def test_run_without_models_raises_error(self, sample_data):
143+
pipeline = ModelPipeline()
144+
with pytest.raises(ValueError, match="At least one model is required"):
145+
pipeline.run(sample_data, "2024-01-05", "2024-01-10")
146+
147+
def test_run_calls_model_run(self, sample_data, mock_model):
148+
pipeline = ModelPipeline().add_model(mock_model).add_evaluator(MAEEvaluator())
149+
pipeline.run(sample_data, "2024-01-05", "2024-01-10")
150+
mock_model.run.assert_called_once()
151+
152+
def test_run_returns_evaluation_report(self, sample_data, mock_model):
153+
pipeline = ModelPipeline().add_model(mock_model).add_evaluator(MAEEvaluator())
154+
report = pipeline.run(sample_data, "2024-01-05", "2024-01-10")
155+
assert isinstance(report, EvaluationReport)
156+
157+
def test_run_calls_exporters(self, sample_data, mock_model):
158+
exporter = MagicMock()
159+
pipeline = ModelPipeline().add_model(mock_model).add_evaluator(MAEEvaluator()).add_exporter(exporter)
160+
report = pipeline.run(sample_data, "2024-01-05", "2024-01-10")
161+
exporter.export.assert_called_once_with(report)
162+
163+
def test_run_multiple_models(self, sample_data, mock_model):
164+
model2 = MagicMock()
165+
model2.name = "MockModel2"
166+
model2.run.return_value = [
167+
{"prediction": 11, "actual": 12, "hour": 0, "horizon": 1, "target_date": "2024-01-01"},
168+
]
169+
pipeline = ModelPipeline().add_model(mock_model).add_model(model2).add_evaluator(MAEEvaluator())
170+
report = pipeline.run(sample_data, "2024-01-05", "2024-01-10")
171+
assert "MockModel" in report.results
172+
assert "MockModel2" in report.results
173+
174+
175+
class TestTerminalExporter:
176+
@pytest.fixture
177+
def sample_report(self):
178+
results = {
179+
"model_a": [
180+
{"prediction": 10, "actual": 12, "hour": 0, "horizon": 1, "target_date": "2024-01-01"},
181+
{"prediction": 20, "actual": 18, "hour": 1, "horizon": 1, "target_date": "2024-01-01"},
182+
],
183+
}
184+
return EvaluationReport(results, [MAEEvaluator()])
185+
186+
def test_init_default_show(self):
187+
exporter = TerminalExporter()
188+
assert exporter.show == ["summary", "horizon"]
189+
190+
def test_init_custom_show(self):
191+
exporter = TerminalExporter(show=["summary", "hour", "year"])
192+
assert exporter.show == ["summary", "hour", "year"]
193+
194+
@patch("epftoolbox2.exporters.terminal.Console")
195+
def test_export_summary(self, mock_console_class, sample_report):
196+
mock_console = MagicMock()
197+
mock_console_class.return_value = mock_console
198+
exporter = TerminalExporter(show=["summary"])
199+
exporter.export(sample_report)
200+
assert mock_console.print.called
201+
202+
@patch("epftoolbox2.exporters.terminal.Console")
203+
def test_export_all_sections(self, mock_console_class, sample_report):
204+
mock_console = MagicMock()
205+
mock_console_class.return_value = mock_console
206+
exporter = TerminalExporter(show=["summary", "hour", "horizon", "hour_horizon", "year", "year_horizon"])
207+
exporter.export(sample_report)
208+
# Should have multiple print calls for headers and tables
209+
assert mock_console.print.call_count >= 6
210+
211+
def test_df_to_table(self, sample_report):
212+
exporter = TerminalExporter()
213+
df = sample_report.summary()
214+
table = exporter._df_to_table(df)
215+
assert table is not None
216+
217+
def test_format_float(self):
218+
exporter = TerminalExporter()
219+
assert exporter._format(1.23456789) == "1.2346"
220+
221+
def test_format_string(self):
222+
exporter = TerminalExporter()
223+
assert exporter._format("test") == "test"
224+
225+
def test_format_int(self):
226+
exporter = TerminalExporter()
227+
assert exporter._format(42) == "42"
228+
229+
230+
class TestExcelExporter:
231+
@pytest.fixture
232+
def sample_report(self):
233+
results = {
234+
"model_a": [
235+
{"prediction": 10, "actual": 12, "hour": 0, "horizon": 1, "target_date": "2024-01-01"},
236+
{"prediction": 20, "actual": 18, "hour": 1, "horizon": 1, "target_date": "2024-01-01"},
237+
{"prediction": 30, "actual": 33, "hour": 0, "horizon": 2, "target_date": "2024-01-02"},
238+
{"prediction": 40, "actual": 38, "hour": 1, "horizon": 2, "target_date": "2024-01-02"},
239+
],
240+
"model_b": [
241+
{"prediction": 11, "actual": 12, "hour": 0, "horizon": 1, "target_date": "2024-01-01"},
242+
{"prediction": 19, "actual": 18, "hour": 1, "horizon": 1, "target_date": "2024-01-01"},
243+
{"prediction": 32, "actual": 33, "hour": 0, "horizon": 2, "target_date": "2024-01-02"},
244+
{"prediction": 39, "actual": 38, "hour": 1, "horizon": 2, "target_date": "2024-01-02"},
245+
],
246+
}
247+
return EvaluationReport(results, [MAEEvaluator()])
248+
249+
def test_init_default_sheets(self):
250+
with tempfile.TemporaryDirectory() as tmpdir:
251+
path = os.path.join(tmpdir, "test.xlsx")
252+
exporter = ExcelExporter(path)
253+
assert exporter.sheets == ["summary", "hour", "horizon", "hour_horizon", "year", "year_horizon"]
254+
255+
def test_init_custom_sheets(self):
256+
with tempfile.TemporaryDirectory() as tmpdir:
257+
path = os.path.join(tmpdir, "test.xlsx")
258+
exporter = ExcelExporter(path, sheets=["summary", "horizon"])
259+
assert exporter.sheets == ["summary", "horizon"]
260+
261+
def test_export_creates_file(self, sample_report):
262+
with tempfile.TemporaryDirectory() as tmpdir:
263+
path = os.path.join(tmpdir, "test.xlsx")
264+
exporter = ExcelExporter(path)
265+
exporter.export(sample_report)
266+
assert os.path.exists(path)
267+
268+
def test_export_creates_parent_directory(self, sample_report):
269+
with tempfile.TemporaryDirectory() as tmpdir:
270+
path = os.path.join(tmpdir, "subdir", "test.xlsx")
271+
exporter = ExcelExporter(path)
272+
exporter.export(sample_report)
273+
assert os.path.exists(path)
274+
275+
def test_export_summary_sheet(self, sample_report):
276+
with tempfile.TemporaryDirectory() as tmpdir:
277+
path = os.path.join(tmpdir, "test.xlsx")
278+
exporter = ExcelExporter(path, sheets=["summary"])
279+
exporter.export(sample_report)
280+
df = pd.read_excel(path, sheet_name="Summary")
281+
assert "model" in df.columns
282+
assert len(df) == 2
283+
284+
def test_export_horizon_sheet(self, sample_report):
285+
with tempfile.TemporaryDirectory() as tmpdir:
286+
path = os.path.join(tmpdir, "test.xlsx")
287+
exporter = ExcelExporter(path, sheets=["horizon"])
288+
exporter.export(sample_report)
289+
df = pd.read_excel(path, sheet_name="horizon_MAE")
290+
assert "horizon_1" in df.columns or "horizon_2" in df.columns
291+
292+
def test_export_all_sheets(self, sample_report):
293+
with tempfile.TemporaryDirectory() as tmpdir:
294+
path = os.path.join(tmpdir, "test.xlsx")
295+
exporter = ExcelExporter(path)
296+
exporter.export(sample_report)
297+
with pd.ExcelFile(path) as xl:
298+
expected_sheets = ["Summary", "hour_MAE", "horizon_MAE", "HourHorizon_MAE", "year_MAE", "YearHorizon_MAE"]
299+
for sheet in expected_sheets:
300+
assert sheet in xl.sheet_names

0 commit comments

Comments
 (0)