|
1 | 1 | import pytest |
2 | 2 | import pandas as pd |
3 | 3 | import numpy as np |
| 4 | +from unittest.mock import MagicMock, patch |
| 5 | +import tempfile |
| 6 | +import os |
4 | 7 |
|
5 | 8 | from epftoolbox2.evaluators import MAEEvaluator |
6 | 9 | 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 |
7 | 13 |
|
8 | 14 |
|
9 | 15 | class TestMAEEvaluator: |
@@ -83,3 +89,212 @@ def test_by_year(self, sample_results): |
83 | 89 |
|
84 | 90 | assert "year" in by_year.columns |
85 | 91 | 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