Skip to content

Commit 02ffb16

Browse files
committed
feat: Add LightGBM model with enhanced feature engineering (Issue #30)
This commit introduces a new LightGBM-based solar power predictor that uses enhanced feature engineering to improve forecast accuracy. New files: - feature_engineering.py: Solar position, cyclical time, derived weather features - v3_lightgbm.py: LightGBM predictor with physics-based fallback - test_v3_lightgbm.py: Comprehensive tests for the new model Modified files: - forecast.py: Added 'lgbm' model option - __init__.py: Export new classes The model is experimental and uses a physics-based fallback until training data and hyperparameters are finalized. Closes #30
1 parent 0d788ff commit 02ffb16

5 files changed

Lines changed: 1005 additions & 4 deletions

File tree

quartz_solar_forecast/forecast.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from quartz_solar_forecast.data import get_nwp, make_pv_data
77
from quartz_solar_forecast.forecasts import (
88
TryolabsSolarPowerPredictor,
9+
LightGBMSolarPredictor,
910
forecast_v1_tilt_orientation,
1011
)
1112
from quartz_solar_forecast.pydantic_models import PVSite
@@ -120,6 +121,64 @@ def predict_tryolabs(site: PVSite, ts: datetime | str = None):
120121
return predictions
121122

122123

124+
def predict_lightgbm(site: PVSite, ts: datetime | str = None):
125+
"""
126+
Run the forecast with the LightGBM model.
127+
128+
This model uses enhanced feature engineering including solar position,
129+
cyclical time features, and derived weather features.
130+
131+
:param site: the PV site
132+
:param ts: the timestamp of the site. If None, defaults to the current
133+
timestamp rounded down to 15 minutes.
134+
:return: The PV forecast of the site for time (ts) for 48 hours
135+
"""
136+
137+
# instantiate class to make predictions
138+
solar_power_predictor = LightGBMSolarPredictor()
139+
140+
# set start and end time, if no time is given use current time
141+
if ts is None:
142+
start_date = pd.Timestamp.now().strftime("%Y-%m-%d")
143+
start_time = pd.Timestamp.now().round(freq="h")
144+
else:
145+
start_date = pd.Timestamp(ts).strftime("%Y-%m-%d")
146+
start_time = pd.Timestamp(ts).round(freq="h")
147+
148+
end_time = start_time + pd.Timedelta(hours=48)
149+
start_date_datetime = datetime.strptime(start_date, "%Y-%m-%d")
150+
151+
# Check if the start date is more than 3 months ago
152+
three_months_ago = datetime.today() - timedelta(days=3 * 30)
153+
154+
if start_date_datetime < three_months_ago:
155+
print(
156+
f"Start date ({start_date}) is more than 3 months ago, no",
157+
"forecast data available.",
158+
)
159+
return None
160+
else:
161+
# load model (will use physics-based fallback if not trained yet)
162+
solar_power_predictor.load_model()
163+
# make predictions
164+
predictions = solar_power_predictor.predict_power_output(
165+
latitude=site.latitude,
166+
longitude=site.longitude,
167+
start_date=start_date,
168+
kwp=site.capacity_kwp,
169+
orientation=site.orientation,
170+
tilt=site.tilt,
171+
)
172+
173+
# postprocessing of the dataframe
174+
predictions = predictions[
175+
(predictions["date"] >= start_time) & (predictions["date"] < end_time)
176+
]
177+
predictions = predictions.reset_index(drop=True)
178+
predictions.set_index("date", inplace=True)
179+
print("Predictions finished.")
180+
return predictions
181+
123182
def run_forecast(
124183
site: PVSite,
125184
model: str = "gb",
@@ -131,8 +190,10 @@ def run_forecast(
131190
Predict solar power output for a given site using a specified model.
132191
133192
:param site: the PV site
134-
:param model: the model to use for prediction, choose between "ocf" and "tryolabs",
135-
by default "ocf" is used
193+
:param model: the model to use for prediction. Options:
194+
- "gb": Gradient Boosting (default, OCF model)
195+
- "xgb": XGBoost (Tryolabs model)
196+
- "lgbm": LightGBM with enhanced features (experimental)
136197
:param ts: the timestamp of the site. If None, defaults to the current
137198
timestamp rounded down to 15 minutes.
138199
:param nwp_source: the nwp data source. Either "gfs", "icon" or "ukmo". Defaults to "icon"
@@ -159,5 +220,11 @@ def run_forecast(
159220
"Ignoring live_generation input.")
160221
return predict_tryolabs(site, ts)
161222

223+
elif model == "lgbm":
224+
if live_generation is not None:
225+
log.warning("Live generation data is currently not supported with the lgbm model. " \
226+
"Ignoring live_generation input.")
227+
return predict_lightgbm(site, ts)
228+
162229
else:
163-
raise ValueError(f"Unsupported model: {model}. Choose between 'xgb' and 'gb'")
230+
raise ValueError(f"Unsupported model: {model}. Choose between 'gb', 'xgb', or 'lgbm'")

quartz_solar_forecast/forecasts/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@
55
Different models are put in different files like:
66
- v1.py contains the v1 model
77
- v2.py contains the v2 model which was developed by Tryolabs
8+
- v3_lightgbm.py contains the v3 LightGBM model with enhanced features
89
"""
910

1011
from .v1 import forecast_v1
1112
from .v1_tilt_orientation import forecast_v1_tilt_orientation
1213
from .v2 import TryolabsSolarPowerPredictor
14+
from .v3_lightgbm import LightGBMSolarPredictor
15+
from .feature_engineering import FeatureEngineer
16+
17+
__all__ = [
18+
"forecast_v1",
19+
"forecast_v1_tilt_orientation",
20+
"TryolabsSolarPowerPredictor",
21+
"LightGBMSolarPredictor",
22+
"FeatureEngineer",
23+
]
1324

14-
__all__ = ["forecast_v1", "forecast_v1_tilt_orientation", "TryolabsSolarPowerPredictor"]

0 commit comments

Comments
 (0)