From d82d6fa14edee92fbb7bafa53fadd168d7dd289d Mon Sep 17 00:00:00 2001 From: Amar Akram <263758252+amarakramali@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:35:11 +0200 Subject: [PATCH] Use a named Python import package --- .github/workflows/ci.yml | 14 +++++ README.md | 51 ++++++++++--------- pyproject.toml | 16 +++--- src/{ => aep_load_forecasting}/__init__.py | 0 .../baseline_eval.py | 4 +- src/{ => aep_load_forecasting}/demo_data.py | 0 .../demo_pipeline.py | 22 +++++--- src/{ => aep_load_forecasting}/evaluation.py | 0 .../forecast_24h.py | 7 ++- src/{ => aep_load_forecasting}/forecasting.py | 0 .../make_features.py | 0 src/{ => aep_load_forecasting}/plot_load.py | 0 src/{ => aep_load_forecasting}/reporting.py | 0 src/{ => aep_load_forecasting}/sample_data.py | 0 src/{ => aep_load_forecasting}/xgb_eval.py | 13 +++-- streamlit_app.py | 2 +- tests/test_baseline_eval.py | 2 +- tests/test_demo_data.py | 2 +- tests/test_demo_pipeline.py | 4 +- tests/test_evaluation.py | 5 +- tests/test_forecast_24h.py | 4 +- tests/test_forecasting.py | 2 +- tests/test_make_features.py | 6 ++- tests/test_reporting.py | 2 +- tests/test_sample_data.py | 7 ++- tests/test_xgb_eval.py | 4 +- 26 files changed, 105 insertions(+), 62 deletions(-) rename src/{ => aep_load_forecasting}/__init__.py (100%) rename src/{ => aep_load_forecasting}/baseline_eval.py (98%) rename src/{ => aep_load_forecasting}/demo_data.py (100%) rename src/{ => aep_load_forecasting}/demo_pipeline.py (94%) rename src/{ => aep_load_forecasting}/evaluation.py (100%) rename src/{ => aep_load_forecasting}/forecast_24h.py (97%) rename src/{ => aep_load_forecasting}/forecasting.py (100%) rename src/{ => aep_load_forecasting}/make_features.py (100%) rename src/{ => aep_load_forecasting}/plot_load.py (100%) rename src/{ => aep_load_forecasting}/reporting.py (100%) rename src/{ => aep_load_forecasting}/sample_data.py (100%) rename src/{ => aep_load_forecasting}/xgb_eval.py (97%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc27e00..11a520f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,20 @@ jobs: - name: Build wheel and source distribution run: python -m build + - name: Verify wheel package layout + run: | + python - <<'PY' + import glob + import zipfile + + wheel, = glob.glob("dist/*.whl") + with zipfile.ZipFile(wheel) as archive: + names = set(archive.namelist()) + + assert "aep_load_forecasting/__init__.py" in names + assert "src/__init__.py" not in names + PY + - name: Smoke-test installed console commands run: | aep-demo --help diff --git a/README.md b/README.md index efd9451..4909a68 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,16 @@ out-of-time test set: ## Approach - **Target:** hourly load `AEP_MW`. -- **Features** (`src/make_features.py`): +- **Features** (`src/aep_load_forecasting/make_features.py`): - Calendar: `hour`, `dayofweek`, `month`, `is_weekend` - Lags: `lag_1` (1 h), `lag_24` (1 day), `lag_168` (1 week) - Rolling means (shifted, past-only): `roll_24_mean`, `roll_168_mean` -- **Split** (`src/xgb_eval.py`): disjoint, time-based partitions — train +- **Split** (`src/aep_load_forecasting/xgb_eval.py`): disjoint, time-based + partitions — train (everything before the final 60 days), validation (720 h), and test (720 h). - **Model:** `XGBRegressor(n_estimators=800, learning_rate=0.05, max_depth=6, subsample=0.8, colsample_bytree=0.8)`. -- **24-hour forecast** (`src/forecasting.py`, `src/forecast_24h.py`): the +- **24-hour forecast** (`src/aep_load_forecasting/forecasting.py`, + `src/aep_load_forecasting/forecast_24h.py`): the final model is fit on all data, then steps hour-by-hour, feeding each prediction back in as the next `lag_1`. The reusable forecast core validates the hourly history and model feature contract before prediction. @@ -65,16 +67,17 @@ out-of-time test set: ```text load-forecasting-xgboost/ ├── src/ -│ ├── plot_load.py # EDA: plot the last 14 days of load -│ ├── make_features.py # build calendar + lag + rolling features -│ ├── sample_data.py # deterministic dataset for a no-download demo -│ ├── evaluation.py # leakage-safe chronological split utilities -│ ├── reporting.py # reusable metrics and CSV report persistence -│ ├── forecasting.py # validated recursive forecast utilities -│ ├── demo_data.py # forecast CSV validation for the demo -│ ├── baseline_eval.py # naive baselines (yesterday / last week / blend) -│ ├── xgb_eval.py # train + evaluate XGBoost vs. baseline (last 30 days) -│ └── forecast_24h.py # final model + recursive next-24h forecast export +│ └── aep_load_forecasting/ +│ ├── plot_load.py # EDA: plot the last 14 days of load +│ ├── make_features.py # build calendar + lag + rolling features +│ ├── sample_data.py # deterministic no-download dataset +│ ├── evaluation.py # chronological split utilities +│ ├── reporting.py # metrics and CSV report persistence +│ ├── forecasting.py # validated recursive forecast utilities +│ ├── demo_data.py # forecast CSV validation for the demo +│ ├── baseline_eval.py # yesterday / last-week / blend baselines +│ ├── xgb_eval.py # train and evaluate XGBoost +│ └── forecast_24h.py # export a recursive future forecast ├── streamlit_app.py # interactive demo (reads assets/forecast_next24h.csv) ├── assets/ # plots + a sample forecast CSV ├── requirements.txt @@ -102,8 +105,8 @@ This single command creates the sample data, features, baseline and XGBoost metrics, evaluation plots, trained model, and next-24h forecast under the existing ignored `data/`, `reports/`, and `models/` directories. Use `--output-dir` to keep every artifact below a different directory. The module -form, `python -m src.demo_pipeline`, remains available when working directly -from a source checkout. +form, `python -m aep_load_forecasting.demo_pipeline`, remains available after +installing the project. Each successful run also writes `reports/sample_run_manifest.json`. The manifest records the effective pipeline parameters, Python and dependency @@ -160,30 +163,30 @@ mkdir data # -> place AEP_hourly.csv in .\data\ # 2) build the validated feature table -python -m src.make_features +aep-make-features # 3) evaluate baselines and XGBoost (test = last 30 days) -python -m src.baseline_eval -python -m src.xgb_eval +aep-baseline-eval +aep-xgb-eval # 4) train the final model and export the next-24h forecast -python -m src.forecast_24h +aep-forecast ``` The evaluation commands create reproducible artifacts automatically: | Command | Metrics | Plot | |---|---|---| -| `python -m src.baseline_eval` | `reports/baseline_metrics.csv` | `reports/figures/baseline_evaluation.png` | -| `python -m src.xgb_eval` | `reports/xgb_evaluation_metrics.csv` | `reports/figures/xgb_evaluation.png` | -| `python -m src.forecast_24h` | `reports/forecast_next24h.csv` | `reports/figures/forecast_next24h.png` | +| `aep-baseline-eval` | `reports/baseline_metrics.csv` | `reports/figures/baseline_evaluation.png` | +| `aep-xgb-eval` | `reports/xgb_evaluation_metrics.csv` | `reports/figures/xgb_evaluation.png` | +| `aep-forecast` | `reports/forecast_next24h.csv` | `reports/figures/forecast_next24h.png` | The commands create their output directories automatically. The forecast horizon, evaluation windows, boosting rounds, and every input or output path can also be configured from the command line: ```powershell -python -m src.forecast_24h ` +aep-forecast ` --horizon 48 ` --estimators 400 ` --output reports\forecast_next48h.csv ` @@ -215,7 +218,7 @@ before rendering them. Custom paths and target columns can be supplied without editing the source: ```powershell -python -m src.make_features ` +aep-make-features ` --input data\AEP_hourly.csv ` --output data\features_aep.csv ` --target AEP_MW diff --git a/pyproject.toml b/pyproject.toml index f696abf..dd57cf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,19 +40,19 @@ dev = [ ] [project.scripts] -aep-demo = "src.demo_pipeline:main" -aep-sample-data = "src.sample_data:main" -aep-make-features = "src.make_features:main" -aep-baseline-eval = "src.baseline_eval:main" -aep-xgb-eval = "src.xgb_eval:main" -aep-forecast = "src.forecast_24h:main" +aep-demo = "aep_load_forecasting.demo_pipeline:main" +aep-sample-data = "aep_load_forecasting.sample_data:main" +aep-make-features = "aep_load_forecasting.make_features:main" +aep-baseline-eval = "aep_load_forecasting.baseline_eval:main" +aep-xgb-eval = "aep_load_forecasting.xgb_eval:main" +aep-forecast = "aep_load_forecasting.forecast_24h:main" [project.urls] Homepage = "https://github.com/amarakramali/load-forecasting-xgboost" Issues = "https://github.com/amarakramali/load-forecasting-xgboost/issues" -[tool.setuptools] -packages = ["src"] +[tool.setuptools.packages.find] +where = ["src"] [tool.ruff] target-version = "py310" diff --git a/src/__init__.py b/src/aep_load_forecasting/__init__.py similarity index 100% rename from src/__init__.py rename to src/aep_load_forecasting/__init__.py diff --git a/src/baseline_eval.py b/src/aep_load_forecasting/baseline_eval.py similarity index 98% rename from src/baseline_eval.py rename to src/aep_load_forecasting/baseline_eval.py index 0beb441..59181ea 100644 --- a/src/baseline_eval.py +++ b/src/aep_load_forecasting/baseline_eval.py @@ -9,8 +9,8 @@ import pandas as pd from matplotlib.figure import Figure -from src.evaluation import HOURS_PER_DAY, trailing_window -from src.reporting import ( +from aep_load_forecasting.evaluation import HOURS_PER_DAY, trailing_window +from aep_load_forecasting.reporting import ( EvaluationResult, evaluate_predictions, format_result, diff --git a/src/demo_data.py b/src/aep_load_forecasting/demo_data.py similarity index 100% rename from src/demo_data.py rename to src/aep_load_forecasting/demo_data.py diff --git a/src/demo_pipeline.py b/src/aep_load_forecasting/demo_pipeline.py similarity index 94% rename from src/demo_pipeline.py rename to src/aep_load_forecasting/demo_pipeline.py index 70660f6..b683f22 100644 --- a/src/demo_pipeline.py +++ b/src/aep_load_forecasting/demo_pipeline.py @@ -12,29 +12,35 @@ import joblib -from src.baseline_eval import ( +from aep_load_forecasting.baseline_eval import ( DEFAULT_EVALUATION_DAYS, DEFAULT_PLOT_DAYS, evaluate_baselines, save_baseline_plot, ) -from src.evaluation import HOURS_PER_DAY -from src.forecast_24h import ( +from aep_load_forecasting.evaluation import HOURS_PER_DAY +from aep_load_forecasting.forecast_24h import ( DEFAULT_ESTIMATORS, save_forecast_plot, train_final_model, ) -from src.forecasting import recursive_forecast -from src.make_features import load_hourly_series, make_feature_table -from src.reporting import save_results -from src.sample_data import ( +from aep_load_forecasting.forecasting import recursive_forecast +from aep_load_forecasting.make_features import ( + load_hourly_series, + make_feature_table, +) +from aep_load_forecasting.reporting import save_results +from aep_load_forecasting.sample_data import ( DEFAULT_DAYS, DEFAULT_SEED, DEFAULT_START, generate_hourly_load, save_sample_data, ) -from src.xgb_eval import evaluate_xgboost, save_evaluation_plot +from aep_load_forecasting.xgb_eval import ( + evaluate_xgboost, + save_evaluation_plot, +) DEFAULT_OUTPUT_DIR = Path(".") DEFAULT_HORIZON = 24 diff --git a/src/evaluation.py b/src/aep_load_forecasting/evaluation.py similarity index 100% rename from src/evaluation.py rename to src/aep_load_forecasting/evaluation.py diff --git a/src/forecast_24h.py b/src/aep_load_forecasting/forecast_24h.py similarity index 97% rename from src/forecast_24h.py rename to src/aep_load_forecasting/forecast_24h.py index 2d0de39..1f7c9ac 100644 --- a/src/forecast_24h.py +++ b/src/aep_load_forecasting/forecast_24h.py @@ -10,12 +10,15 @@ from matplotlib.figure import Figure from xgboost import XGBRegressor -from src.forecasting import ( +from aep_load_forecasting.forecasting import ( FORECAST_FEATURES, recursive_forecast, validate_feature_columns, ) -from src.make_features import DEFAULT_INPUT, load_hourly_series +from aep_load_forecasting.make_features import ( + DEFAULT_INPUT, + load_hourly_series, +) DEFAULT_FEATURES = Path("data") / "features_aep.csv" DEFAULT_FORECAST = Path("reports") / "forecast_next24h.csv" diff --git a/src/forecasting.py b/src/aep_load_forecasting/forecasting.py similarity index 100% rename from src/forecasting.py rename to src/aep_load_forecasting/forecasting.py diff --git a/src/make_features.py b/src/aep_load_forecasting/make_features.py similarity index 100% rename from src/make_features.py rename to src/aep_load_forecasting/make_features.py diff --git a/src/plot_load.py b/src/aep_load_forecasting/plot_load.py similarity index 100% rename from src/plot_load.py rename to src/aep_load_forecasting/plot_load.py diff --git a/src/reporting.py b/src/aep_load_forecasting/reporting.py similarity index 100% rename from src/reporting.py rename to src/aep_load_forecasting/reporting.py diff --git a/src/sample_data.py b/src/aep_load_forecasting/sample_data.py similarity index 100% rename from src/sample_data.py rename to src/aep_load_forecasting/sample_data.py diff --git a/src/xgb_eval.py b/src/aep_load_forecasting/xgb_eval.py similarity index 97% rename from src/xgb_eval.py rename to src/aep_load_forecasting/xgb_eval.py index 4c7800c..9d8ac1a 100644 --- a/src/xgb_eval.py +++ b/src/aep_load_forecasting/xgb_eval.py @@ -12,9 +12,16 @@ from matplotlib.figure import Figure from xgboost import XGBRegressor -from src.evaluation import HOURS_PER_DAY, chronological_split, trailing_window -from src.forecasting import FORECAST_FEATURES, validate_feature_columns -from src.reporting import ( +from aep_load_forecasting.evaluation import ( + HOURS_PER_DAY, + chronological_split, + trailing_window, +) +from aep_load_forecasting.forecasting import ( + FORECAST_FEATURES, + validate_feature_columns, +) +from aep_load_forecasting.reporting import ( EvaluationResult, evaluate_predictions, format_result, diff --git a/streamlit_app.py b/streamlit_app.py index be8e16e..6d6a366 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -4,7 +4,7 @@ import streamlit as st -from src.demo_data import ( +from aep_load_forecasting.demo_data import ( FORECAST_COLUMN, ForecastDataError, forecast_plot_columns, diff --git a/tests/test_baseline_eval.py b/tests/test_baseline_eval.py index 7a53ed2..6a15470 100644 --- a/tests/test_baseline_eval.py +++ b/tests/test_baseline_eval.py @@ -4,7 +4,7 @@ import pandas as pd import pytest -from src.baseline_eval import ( +from aep_load_forecasting.baseline_eval import ( evaluate_baselines, load_baseline_features, main, diff --git a/tests/test_demo_data.py b/tests/test_demo_data.py index 1b66817..70196b5 100644 --- a/tests/test_demo_data.py +++ b/tests/test_demo_data.py @@ -5,7 +5,7 @@ import pandas as pd import pytest -from src.demo_data import ( +from aep_load_forecasting.demo_data import ( BASELINE_COLUMN, FORECAST_COLUMN, ForecastDataError, diff --git a/tests/test_demo_pipeline.py b/tests/test_demo_pipeline.py index d45bc11..c855a40 100644 --- a/tests/test_demo_pipeline.py +++ b/tests/test_demo_pipeline.py @@ -7,8 +7,8 @@ import pandas as pd import pytest -from src.demo_pipeline import main, run_demo_pipeline -from src.forecasting import FORECAST_FEATURES +from aep_load_forecasting.demo_pipeline import main, run_demo_pipeline +from aep_load_forecasting.forecasting import FORECAST_FEATURES def test_command_writes_complete_reproducible_demo(tmp_path) -> None: diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py index 45e1a1b..8294c9c 100644 --- a/tests/test_evaluation.py +++ b/tests/test_evaluation.py @@ -3,7 +3,10 @@ import pandas as pd import pytest -from src.evaluation import chronological_split, trailing_window +from aep_load_forecasting.evaluation import ( + chronological_split, + trailing_window, +) def hourly_frame(periods: int = 2_000) -> pd.DataFrame: diff --git a/tests/test_forecast_24h.py b/tests/test_forecast_24h.py index bffae48..b13196a 100644 --- a/tests/test_forecast_24h.py +++ b/tests/test_forecast_24h.py @@ -4,8 +4,8 @@ import pandas as pd -import src.forecast_24h as command -from src.forecasting import FORECAST_FEATURES +import aep_load_forecasting.forecast_24h as command +from aep_load_forecasting.forecasting import FORECAST_FEATURES def test_command_forwards_estimator_count_and_writes_outputs( diff --git a/tests/test_forecasting.py b/tests/test_forecasting.py index 5ce4146..90cf1bc 100644 --- a/tests/test_forecasting.py +++ b/tests/test_forecasting.py @@ -4,7 +4,7 @@ import pandas as pd import pytest -from src.forecasting import ( +from aep_load_forecasting.forecasting import ( FORECAST_FEATURES, make_forecast_row, recursive_forecast, diff --git a/tests/test_make_features.py b/tests/test_make_features.py index 9032776..4d197ca 100644 --- a/tests/test_make_features.py +++ b/tests/test_make_features.py @@ -3,7 +3,11 @@ import pandas as pd import pytest -from src.make_features import build_features, load_hourly_series, make_feature_table +from aep_load_forecasting.make_features import ( + build_features, + load_hourly_series, + make_feature_table, +) def synthetic_load(periods: int = 200) -> pd.Series: diff --git a/tests/test_reporting.py b/tests/test_reporting.py index 7344849..c3e8e2b 100644 --- a/tests/test_reporting.py +++ b/tests/test_reporting.py @@ -4,7 +4,7 @@ import pandas as pd import pytest -from src.reporting import ( +from aep_load_forecasting.reporting import ( EvaluationResult, evaluate_predictions, format_result, diff --git a/tests/test_sample_data.py b/tests/test_sample_data.py index 5ab6a6e..3749be3 100644 --- a/tests/test_sample_data.py +++ b/tests/test_sample_data.py @@ -3,8 +3,11 @@ import pandas as pd import pytest -from src.make_features import build_features, load_hourly_series -from src.sample_data import generate_hourly_load, main +from aep_load_forecasting.make_features import ( + build_features, + load_hourly_series, +) +from aep_load_forecasting.sample_data import generate_hourly_load, main def test_generate_hourly_load_is_deterministic_and_hourly() -> None: diff --git a/tests/test_xgb_eval.py b/tests/test_xgb_eval.py index 019d8dc..63f6de2 100644 --- a/tests/test_xgb_eval.py +++ b/tests/test_xgb_eval.py @@ -4,8 +4,8 @@ import pandas as pd import pytest -from src.forecasting import FORECAST_FEATURES -from src.xgb_eval import ( +from aep_load_forecasting.forecasting import FORECAST_FEATURES +from aep_load_forecasting.xgb_eval import ( evaluate_xgboost, load_evaluation_features, main,