openstef 3.4.10__py3-none-any.whl → 3.4.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openstef/app_settings.py +19 -0
- openstef/data_classes/data_prep.py +1 -1
- openstef/data_classes/prediction_job.py +12 -8
- openstef/enums.py +3 -7
- openstef/exceptions.py +1 -1
- openstef/feature_engineering/apply_features.py +0 -6
- openstef/feature_engineering/data_preparation.py +12 -5
- openstef/feature_engineering/feature_applicator.py +1 -5
- openstef/feature_engineering/general.py +14 -0
- openstef/feature_engineering/missing_values_transformer.py +99 -0
- openstef/feature_engineering/weather_features.py +7 -0
- openstef/metrics/figure.py +3 -0
- openstef/metrics/metrics.py +58 -1
- openstef/metrics/reporter.py +7 -0
- openstef/model/confidence_interval_applicator.py +28 -3
- openstef/model/model_creator.py +36 -27
- openstef/model/objective.py +11 -28
- openstef/model/objective_creator.py +4 -3
- openstef/model/regressors/arima.py +1 -1
- openstef/model/regressors/dazls.py +35 -96
- openstef/model/regressors/flatliner.py +100 -0
- openstef/model/regressors/linear_quantile.py +247 -0
- openstef/model/regressors/xgb_multioutput_quantile.py +261 -0
- openstef/model/regressors/xgb_quantile.py +3 -0
- openstef/model/serializer.py +10 -0
- openstef/model_selection/model_selection.py +3 -0
- openstef/monitoring/performance_meter.py +1 -2
- openstef/monitoring/teams.py +11 -0
- openstef/pipeline/create_basecase_forecast.py +11 -1
- openstef/pipeline/create_component_forecast.py +11 -22
- openstef/pipeline/create_forecast.py +20 -1
- openstef/pipeline/optimize_hyperparameters.py +18 -16
- openstef/pipeline/train_create_forecast_backtest.py +11 -1
- openstef/pipeline/train_model.py +23 -7
- openstef/pipeline/utils.py +3 -0
- openstef/postprocessing/postprocessing.py +29 -0
- openstef/settings.py +15 -0
- openstef/tasks/calculate_kpi.py +20 -17
- openstef/tasks/create_basecase_forecast.py +13 -5
- openstef/tasks/create_components_forecast.py +20 -4
- openstef/tasks/create_forecast.py +5 -2
- openstef/tasks/split_forecast.py +7 -0
- openstef/tasks/train_model.py +7 -5
- openstef/tasks/utils/taskcontext.py +7 -0
- openstef/validation/validation.py +27 -2
- {openstef-3.4.10.dist-info → openstef-3.4.29.dist-info}/METADATA +34 -38
- openstef-3.4.29.dist-info/RECORD +91 -0
- {openstef-3.4.10.dist-info → openstef-3.4.29.dist-info}/WHEEL +1 -1
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_adaptation_model.z +0 -0
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_adaptation_model.z.license +0 -3
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_adaptation_model_features.z +0 -0
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_adaptation_model_features.z.license +0 -3
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_adaptation_model_scaler.z +0 -0
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_adaptation_model_scaler.z.license +0 -3
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_domain_model.z +0 -0
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_domain_model.z.license +0 -3
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_domain_model_features.z +0 -2
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_domain_model_features.z.license +0 -3
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_domain_model_scaler.z +0 -0
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_domain_model_scaler.z.license +0 -3
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_target.z +0 -0
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_target.z.license +0 -3
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_target_scaler.z +0 -6
- openstef/data/dazls_model_3.4.0/dazls_stored_3.4.0_target_scaler.z.license +0 -3
- openstef/feature_engineering/historic_features.py +0 -40
- openstef/model/regressors/proloaf.py +0 -281
- openstef/tasks/run_tracy.py +0 -145
- openstef-3.4.10.dist-info/RECORD +0 -104
- {openstef-3.4.10.dist-info → openstef-3.4.29.dist-info}/LICENSE +0 -0
- {openstef-3.4.10.dist-info → openstef-3.4.29.dist-info}/top_level.txt +0 -0
openstef/pipeline/train_model.py
CHANGED
@@ -23,6 +23,7 @@ from openstef.model.regressors.regressor import OpenstfRegressor
|
|
23
23
|
from openstef.model.serializer import MLflowSerializer
|
24
24
|
from openstef.model.standard_deviation_generator import StandardDeviationGenerator
|
25
25
|
from openstef.model_selection.model_selection import split_data_train_validation_test
|
26
|
+
from openstef.settings import Settings
|
26
27
|
from openstef.validation import validation
|
27
28
|
|
28
29
|
DEFAULT_TRAIN_HORIZONS_HOURS: list[float] = [0.25, 47.0]
|
@@ -31,6 +32,11 @@ MAXIMUM_MODEL_AGE: int = 7
|
|
31
32
|
DEFAULT_EARLY_STOPPING_ROUNDS: int = 10
|
32
33
|
PENALTY_FACTOR_OLD_MODEL: float = 1.2
|
33
34
|
|
35
|
+
structlog.configure(
|
36
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
37
|
+
logging.getLevelName(Settings.log_level)
|
38
|
+
)
|
39
|
+
)
|
34
40
|
logger = structlog.get_logger(__name__)
|
35
41
|
|
36
42
|
|
@@ -60,6 +66,13 @@ def train_model_pipeline(
|
|
60
66
|
- The validation dataset with forecasts
|
61
67
|
- The test dataset with forecasts
|
62
68
|
|
69
|
+
Raises:
|
70
|
+
InputDataInsufficientError: when input data is insufficient.
|
71
|
+
InputDataWrongColumnOrderError: when input data has a invalid column order.
|
72
|
+
'load' column should be first and 'horizon' column last.
|
73
|
+
OldModelHigherScoreError: When old model is better than new model.
|
74
|
+
SkipSaveTrainingForecasts: If old model is better or younger than `MAXIMUM_MODEL_AGE`, the model is not saved.
|
75
|
+
|
63
76
|
"""
|
64
77
|
# Initialize serializer
|
65
78
|
serializer = MLflowSerializer(mlflow_tracking_uri=mlflow_tracking_uri)
|
@@ -164,6 +177,7 @@ def train_model_pipeline_core(
|
|
164
177
|
InputDataInsufficientError: when input data is insufficient.
|
165
178
|
InputDataWrongColumnOrderError: when input data has a invalid column order.
|
166
179
|
OldModelHigherScoreError: When old model is better than new model.
|
180
|
+
InputDataOngoingZeroFlatlinerError: when all recent load measurements are zero.
|
167
181
|
|
168
182
|
Returns:
|
169
183
|
- Fitted_model (OpenstfRegressor)
|
@@ -172,8 +186,6 @@ def train_model_pipeline_core(
|
|
172
186
|
- Datasets (tuple[pd.DataFrmae, pd.DataFrame, pd.Dataframe): The train, validation and test sets
|
173
187
|
|
174
188
|
"""
|
175
|
-
logger = structlog.get_logger(__name__)
|
176
|
-
|
177
189
|
# Call common pipeline
|
178
190
|
(
|
179
191
|
model,
|
@@ -257,6 +269,8 @@ def train_pipeline_common(
|
|
257
269
|
Raises:
|
258
270
|
InputDataInsufficientError: when input data is insufficient.
|
259
271
|
InputDataWrongColumnOrderError: when input data has a invalid column order.
|
272
|
+
'load' column should be first and 'horizon' column last.
|
273
|
+
InputDataOngoingZeroFlatlinerError: when all recent load measurements are zero.
|
260
274
|
|
261
275
|
"""
|
262
276
|
data_with_features = train_pipeline_step_compute_features(
|
@@ -346,12 +360,9 @@ def train_pipeline_step_compute_features(
|
|
346
360
|
InputDataInsufficientError: when input data is insufficient.
|
347
361
|
InputDataWrongColumnOrderError: when input data has a invalid column order.
|
348
362
|
ValueError: when the horizon is a string and the corresponding column in not in the input data
|
363
|
+
InputDataOngoingZeroFlatlinerError: when all recent load measurements are zero.
|
349
364
|
|
350
365
|
"""
|
351
|
-
if pj["model"] == "proloaf":
|
352
|
-
# proloaf is only able to train with one horizon
|
353
|
-
horizons = [horizons[0]]
|
354
|
-
|
355
366
|
if input_data.empty:
|
356
367
|
raise InputDataInsufficientError("Input dataframe is empty")
|
357
368
|
elif "load" not in input_data.columns:
|
@@ -423,6 +434,10 @@ def train_pipeline_step_train_model(
|
|
423
434
|
Returns:
|
424
435
|
The trained model
|
425
436
|
|
437
|
+
Raises:
|
438
|
+
NotImplementedError: When using invalid model type in the prediction job.
|
439
|
+
InputDataWrongColumnOrderError: When 'load' column is not first and 'horizon' column is not last.
|
440
|
+
|
426
441
|
"""
|
427
442
|
# Test if first column is "load" and last column is "horizon"
|
428
443
|
if train_data.columns[0] != "load" or train_data.columns[-1] != "horizon":
|
@@ -435,6 +450,7 @@ def train_pipeline_step_train_model(
|
|
435
450
|
model = ModelCreator.create_model(
|
436
451
|
pj["model"],
|
437
452
|
quantiles=pj["quantiles"],
|
453
|
+
**(pj.model_kwargs or {}),
|
438
454
|
)
|
439
455
|
|
440
456
|
# split x and y data
|
@@ -523,7 +539,7 @@ def train_pipeline_step_split_data(
|
|
523
539
|
if pj.train_split_func is None:
|
524
540
|
split_func = split_data_train_validation_test
|
525
541
|
split_args = {
|
526
|
-
"stratification_min_max":
|
542
|
+
"stratification_min_max": True,
|
527
543
|
"back_test": backtest,
|
528
544
|
}
|
529
545
|
else:
|
openstef/pipeline/utils.py
CHANGED
@@ -27,6 +27,9 @@ def generate_forecast_datetime_range(
|
|
27
27
|
Returns:
|
28
28
|
Start and end datetimes of the forecast range.
|
29
29
|
|
30
|
+
Raises:
|
31
|
+
ValueError: If the target column does not have null values.
|
32
|
+
|
30
33
|
"""
|
31
34
|
# By labeling the True/False values (based on the isnull() statement) as clusters,
|
32
35
|
# we find what True value belongs to what cluster and the amount of True clusters.
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# SPDX-FileCopyrightText: 2017-2023 Contributors to the OpenSTEF project <korte.termijn.prognoses@alliander.com> # noqa E501>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
import logging
|
4
5
|
from enum import Enum
|
5
6
|
|
6
7
|
import numpy as np
|
@@ -10,6 +11,7 @@ import structlog
|
|
10
11
|
from openstef.data_classes.prediction_job import PredictionJobDataClass
|
11
12
|
from openstef.enums import ForecastType
|
12
13
|
from openstef.feature_engineering import weather_features
|
14
|
+
from openstef.settings import Settings
|
13
15
|
|
14
16
|
# this is the default for "Lagerwey100"
|
15
17
|
TURBINE_DATA = {
|
@@ -219,6 +221,11 @@ def add_prediction_job_properties_to_forecast(
|
|
219
221
|
Dataframe with added metadata.
|
220
222
|
|
221
223
|
"""
|
224
|
+
structlog.configure(
|
225
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
226
|
+
logging.getLevelName(Settings.log_level)
|
227
|
+
)
|
228
|
+
)
|
222
229
|
logger = structlog.get_logger(__name__)
|
223
230
|
|
224
231
|
logger.info("Postproces in preparation of storing")
|
@@ -244,3 +251,25 @@ def add_prediction_job_properties_to_forecast(
|
|
244
251
|
forecast["algtype"] = algorithm_type
|
245
252
|
|
246
253
|
return forecast
|
254
|
+
|
255
|
+
|
256
|
+
def sort_quantiles(
|
257
|
+
forecast: pd.DataFrame, quantile_col_start="quantile_P"
|
258
|
+
) -> pd.DataFrame:
|
259
|
+
"""Sort quantile values so quantiles do not cross.
|
260
|
+
|
261
|
+
This function assumes that all quantile columns start with 'quantile_P' For more academic details on why this is
|
262
|
+
mathematically sounds, please refer to Quantile and Probability Curves Without Crossing (Chernozhukov, 2010)
|
263
|
+
|
264
|
+
"""
|
265
|
+
p_columns = [col for col in forecast.columns if col.startswith(quantile_col_start)]
|
266
|
+
|
267
|
+
if len(p_columns) == 0:
|
268
|
+
return forecast
|
269
|
+
|
270
|
+
# sort the columns
|
271
|
+
p_columns = np.sort(p_columns)
|
272
|
+
|
273
|
+
forecast.loc[:, p_columns] = forecast[p_columns].apply(sorted, axis=1).to_list()
|
274
|
+
|
275
|
+
return forecast
|
openstef/settings.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2017-2023 Contributors to the OpenSTEF project <korte.termijn.prognoses@alliander.com> # noqa E501>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from functools import lru_cache
|
6
|
+
|
7
|
+
from openstef.app_settings import AppSettings
|
8
|
+
|
9
|
+
|
10
|
+
@lru_cache
|
11
|
+
def _get_app_settings() -> AppSettings:
|
12
|
+
return AppSettings()
|
13
|
+
|
14
|
+
|
15
|
+
Settings = _get_app_settings()
|
openstef/tasks/calculate_kpi.py
CHANGED
@@ -18,6 +18,8 @@ Example:
|
|
18
18
|
$ python calculate_kpi.py
|
19
19
|
|
20
20
|
"""
|
21
|
+
import logging
|
22
|
+
|
21
23
|
# Import builtins
|
22
24
|
from datetime import datetime, timedelta
|
23
25
|
from pathlib import Path
|
@@ -30,6 +32,7 @@ from openstef.data_classes.prediction_job import PredictionJobDataClass
|
|
30
32
|
from openstef.enums import MLModelType
|
31
33
|
from openstef.exceptions import NoPredictedLoadError, NoRealisedLoadError
|
32
34
|
from openstef.metrics import metrics
|
35
|
+
from openstef.settings import Settings
|
33
36
|
from openstef.tasks.utils.predictionjobloop import PredictionJobLoop
|
34
37
|
from openstef.tasks.utils.taskcontext import TaskContext
|
35
38
|
from openstef.validation import validation
|
@@ -69,6 +72,8 @@ def check_kpi_task(
|
|
69
72
|
context: TaskContext,
|
70
73
|
start_time: datetime,
|
71
74
|
end_time: datetime,
|
75
|
+
threshold_optimizing=THRESHOLD_OPTIMIZING,
|
76
|
+
threshold_retraining=THRESHOLD_RETRAINING,
|
72
77
|
) -> None:
|
73
78
|
# Apply default parameters if none are provided
|
74
79
|
if start_time is None:
|
@@ -99,28 +104,21 @@ def check_kpi_task(
|
|
99
104
|
|
100
105
|
# Add pid to the list of pids that should be retrained or optimized if
|
101
106
|
# performance is insufficient
|
102
|
-
if kpis["47.0h"]["rMAE"] >
|
107
|
+
if kpis["47.0h"]["rMAE"] > threshold_retraining:
|
103
108
|
context.logger.warning(
|
104
109
|
"Need to retrain model, retraining threshold rMAE 47h exceeded",
|
105
110
|
t_ahead="47.0h",
|
106
111
|
rMAE=kpis["47.0h"]["rMAE"],
|
107
|
-
retraining_threshold=
|
112
|
+
retraining_threshold=threshold_retraining,
|
108
113
|
)
|
109
|
-
function_name = "train_model"
|
110
|
-
|
111
|
-
context.logger.info("Adding tracy job", function=function_name)
|
112
|
-
context.database.ktp_api.add_tracy_job(pj["id"], function=function_name)
|
113
114
|
|
114
|
-
if kpis["47.0h"]["rMAE"] >
|
115
|
+
if kpis["47.0h"]["rMAE"] > threshold_optimizing:
|
115
116
|
context.logger.warning(
|
116
117
|
"Need to optimize hyperparameters, optimizing threshold rMAE 47h exceeded",
|
117
118
|
t_ahead="47.0h",
|
118
119
|
rMAE=kpis["47.0h"]["rMAE"],
|
119
|
-
optimizing_threshold=
|
120
|
+
optimizing_threshold=threshold_optimizing,
|
120
121
|
)
|
121
|
-
function_name = "optimize_hyperparameters"
|
122
|
-
context.logger.info("Adding tracy job", function=function_name)
|
123
|
-
context.database.ktp_api.add_tracy_job(pj["id"], function=function_name)
|
124
122
|
|
125
123
|
|
126
124
|
def calc_kpi_for_specific_pid(
|
@@ -160,7 +158,12 @@ def calc_kpi_for_specific_pid(
|
|
160
158
|
COMPLETENESS_REALISED_THRESHOLDS = 0.7
|
161
159
|
COMPLETENESS_PREDICTED_LOAD_THRESHOLD = 0.7
|
162
160
|
|
163
|
-
|
161
|
+
structlog.configure(
|
162
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
163
|
+
logging.getLevelName(Settings.log_level)
|
164
|
+
)
|
165
|
+
)
|
166
|
+
logger = structlog.get_logger(__name__)
|
164
167
|
|
165
168
|
# If predicted is empty
|
166
169
|
if len(predicted_load) == 0:
|
@@ -194,9 +197,9 @@ def calc_kpi_for_specific_pid(
|
|
194
197
|
|
195
198
|
# Raise exception in case of constant load
|
196
199
|
if combined.load.nunique() == 1:
|
197
|
-
|
200
|
+
logger.warning(
|
198
201
|
"The load is constant! KPIs will still be calculated, but relative metrics"
|
199
|
-
" will be nan"
|
202
|
+
" will be nan."
|
200
203
|
)
|
201
204
|
|
202
205
|
# Define output dictonary
|
@@ -213,7 +216,7 @@ def calc_kpi_for_specific_pid(
|
|
213
216
|
date = pd.to_datetime(end_time)
|
214
217
|
|
215
218
|
# Calculate model metrics and add them to the output dictionary
|
216
|
-
|
219
|
+
logger.info("Start calculating kpis")
|
217
220
|
for hor_cols in hor_list:
|
218
221
|
t_ahead_h = hor_cols[0].split("_")[1]
|
219
222
|
fc = combined[hor_cols[0]] # load predictions
|
@@ -272,7 +275,7 @@ def calc_kpi_for_specific_pid(
|
|
272
275
|
)
|
273
276
|
|
274
277
|
if completeness_realised < COMPLETENESS_REALISED_THRESHOLDS:
|
275
|
-
|
278
|
+
logger.warning(
|
276
279
|
"Completeness realised load too low",
|
277
280
|
prediction_id=pid,
|
278
281
|
start_time=start_time,
|
@@ -282,7 +285,7 @@ def calc_kpi_for_specific_pid(
|
|
282
285
|
)
|
283
286
|
set_incomplete_kpi_to_nan(kpis, t_ahead_h)
|
284
287
|
if completeness_predicted_load.any() < COMPLETENESS_PREDICTED_LOAD_THRESHOLD:
|
285
|
-
|
288
|
+
logger.warning(
|
286
289
|
"Completeness predicted load of specific horizon too low",
|
287
290
|
prediction_id=pid,
|
288
291
|
horizon=t_ahead_h,
|
@@ -32,7 +32,10 @@ T_AHEAD_DAYS: int = 14
|
|
32
32
|
|
33
33
|
|
34
34
|
def create_basecase_forecast_task(
|
35
|
-
pj: PredictionJobDataClass,
|
35
|
+
pj: PredictionJobDataClass,
|
36
|
+
context: TaskContext,
|
37
|
+
t_behind_days=T_BEHIND_DAYS,
|
38
|
+
t_ahead_days=T_AHEAD_DAYS,
|
36
39
|
) -> None:
|
37
40
|
"""Top level task that creates a basecase forecast.
|
38
41
|
|
@@ -41,6 +44,8 @@ def create_basecase_forecast_task(
|
|
41
44
|
Args:
|
42
45
|
pj: Prediction job
|
43
46
|
context: Contect object that holds a config manager and a database connection
|
47
|
+
t_behind_days: number of days included as history. This is used to generated lagged features for the to-be-forecasted period
|
48
|
+
t_ahead_days: number of days a basecase forecast is created for
|
44
49
|
|
45
50
|
"""
|
46
51
|
# Check pipeline types
|
@@ -63,8 +68,8 @@ def create_basecase_forecast_task(
|
|
63
68
|
return
|
64
69
|
|
65
70
|
# Define datetime range for input data
|
66
|
-
datetime_start = datetime.utcnow() - timedelta(days=
|
67
|
-
datetime_end = datetime.utcnow() + timedelta(days=
|
71
|
+
datetime_start = datetime.utcnow() - timedelta(days=t_behind_days)
|
72
|
+
datetime_end = datetime.utcnow() + timedelta(days=t_ahead_days)
|
68
73
|
|
69
74
|
# Retrieve input data
|
70
75
|
input_data = context.database.get_model_input(
|
@@ -77,11 +82,14 @@ def create_basecase_forecast_task(
|
|
77
82
|
# Make basecase forecast using the corresponding pipeline
|
78
83
|
basecase_forecast = create_basecase_forecast_pipeline(pj, input_data)
|
79
84
|
|
80
|
-
# Do not store basecase forecasts for moments within
|
85
|
+
# Do not store basecase forecasts for moments within the prediction job's horizon.
|
81
86
|
# Those should be updated by regular forecast process.
|
82
87
|
basecase_forecast = basecase_forecast.loc[
|
83
88
|
basecase_forecast.index
|
84
|
-
> (
|
89
|
+
> (
|
90
|
+
pd.to_datetime(datetime.utcnow(), utc=True)
|
91
|
+
+ timedelta(minutes=pj.horizon_minutes)
|
92
|
+
),
|
85
93
|
:,
|
86
94
|
]
|
87
95
|
|
@@ -21,11 +21,12 @@ Example:
|
|
21
21
|
$ python create_components_forecast.py
|
22
22
|
|
23
23
|
"""
|
24
|
+
import logging
|
24
25
|
from datetime import datetime, timedelta, timezone
|
25
26
|
from pathlib import Path
|
26
27
|
|
27
|
-
import structlog
|
28
28
|
import pandas as pd
|
29
|
+
import structlog
|
29
30
|
|
30
31
|
from openstef.data_classes.prediction_job import PredictionJobDataClass
|
31
32
|
from openstef.enums import MLModelType
|
@@ -33,6 +34,7 @@ from openstef.exceptions import ComponentForecastTooShortHorizonError
|
|
33
34
|
from openstef.pipeline.create_component_forecast import (
|
34
35
|
create_components_forecast_pipeline,
|
35
36
|
)
|
37
|
+
from openstef.settings import Settings
|
36
38
|
from openstef.tasks.utils.predictionjobloop import PredictionJobLoop
|
37
39
|
from openstef.tasks.utils.taskcontext import TaskContext
|
38
40
|
|
@@ -41,7 +43,10 @@ T_AHEAD_DAYS = 3
|
|
41
43
|
|
42
44
|
|
43
45
|
def create_components_forecast_task(
|
44
|
-
pj: PredictionJobDataClass,
|
46
|
+
pj: PredictionJobDataClass,
|
47
|
+
context: TaskContext,
|
48
|
+
t_behind_days: int = T_BEHIND_DAYS,
|
49
|
+
t_ahead_days: int = T_AHEAD_DAYS,
|
45
50
|
) -> None:
|
46
51
|
"""Top level task that creates a components forecast.
|
47
52
|
|
@@ -50,8 +55,19 @@ def create_components_forecast_task(
|
|
50
55
|
Args:
|
51
56
|
pj: Prediction job
|
52
57
|
context: Contect object that holds a config manager and a database connection
|
58
|
+
t_behind_days: number of days in the past that the component forecast is created for
|
59
|
+
t_ahead_days: number of days in the future that the component forecast is created for
|
60
|
+
|
61
|
+
Raises:
|
62
|
+
ComponentForecastTooShortHorizonError: If the forecast horizon is too short
|
63
|
+
(less than 30 minutes in advance)
|
53
64
|
|
54
65
|
"""
|
66
|
+
structlog.configure(
|
67
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
68
|
+
logging.getLevelName(Settings.log_level)
|
69
|
+
)
|
70
|
+
)
|
55
71
|
logger = structlog.get_logger(__name__)
|
56
72
|
if pj["train_components"] == 0:
|
57
73
|
context.logger.info(
|
@@ -60,8 +76,8 @@ def create_components_forecast_task(
|
|
60
76
|
return
|
61
77
|
|
62
78
|
# Define datetime range for input data
|
63
|
-
datetime_start = datetime.utcnow() - timedelta(days=
|
64
|
-
datetime_end = datetime.utcnow() + timedelta(days=
|
79
|
+
datetime_start = datetime.utcnow() - timedelta(days=t_behind_days)
|
80
|
+
datetime_end = datetime.utcnow() + timedelta(days=t_ahead_days)
|
65
81
|
|
66
82
|
logger.info(
|
67
83
|
"Get predicted load", datetime_start=datetime_start, datetime_end=datetime_end
|
@@ -34,7 +34,9 @@ from openstef.validation.validation import detect_ongoing_zero_flatliner
|
|
34
34
|
T_BEHIND_DAYS: int = 14
|
35
35
|
|
36
36
|
|
37
|
-
def create_forecast_task(
|
37
|
+
def create_forecast_task(
|
38
|
+
pj: PredictionJobDataClass, context: TaskContext, t_behind_days: int = T_BEHIND_DAYS
|
39
|
+
) -> None:
|
38
40
|
"""Top level task that creates a forecast.
|
39
41
|
|
40
42
|
On this task level all database and context manager dependencies are resolved.
|
@@ -45,6 +47,7 @@ def create_forecast_task(pj: PredictionJobDataClass, context: TaskContext) -> No
|
|
45
47
|
Args:
|
46
48
|
pj: Prediction job
|
47
49
|
context: Contect object that holds a config manager and a database connection
|
50
|
+
t_behind_days: number of days included as history. This is used to generated lagged features for the to-be-forecasted period
|
48
51
|
|
49
52
|
"""
|
50
53
|
# Check pipeline types
|
@@ -70,7 +73,7 @@ def create_forecast_task(pj: PredictionJobDataClass, context: TaskContext) -> No
|
|
70
73
|
mlflow_tracking_uri = context.config.paths_mlflow_tracking_uri
|
71
74
|
|
72
75
|
# Define datetime range for input data
|
73
|
-
datetime_start = datetime.utcnow() - timedelta(days=
|
76
|
+
datetime_start = datetime.utcnow() - timedelta(days=t_behind_days)
|
74
77
|
datetime_end = datetime.utcnow() + timedelta(seconds=pj.horizon_minutes * 60)
|
75
78
|
|
76
79
|
# Retrieve input data
|
openstef/tasks/split_forecast.py
CHANGED
@@ -22,6 +22,7 @@ Example:
|
|
22
22
|
$ python split_forecast.py
|
23
23
|
|
24
24
|
"""
|
25
|
+
import logging
|
25
26
|
from datetime import datetime
|
26
27
|
from pathlib import Path
|
27
28
|
|
@@ -33,6 +34,7 @@ import structlog
|
|
33
34
|
import openstef.monitoring.teams as monitoring
|
34
35
|
from openstef.data_classes.prediction_job import PredictionJobDataClass
|
35
36
|
from openstef.enums import MLModelType
|
37
|
+
from openstef.settings import Settings
|
36
38
|
from openstef.tasks.utils.predictionjobloop import PredictionJobLoop
|
37
39
|
from openstef.tasks.utils.taskcontext import TaskContext
|
38
40
|
|
@@ -70,6 +72,11 @@ def split_forecast_task(
|
|
70
72
|
Energy splitting coefficients.
|
71
73
|
|
72
74
|
"""
|
75
|
+
structlog.configure(
|
76
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
77
|
+
logging.getLevelName(Settings.log_level)
|
78
|
+
)
|
79
|
+
)
|
73
80
|
logger = structlog.get_logger(__name__)
|
74
81
|
|
75
82
|
logger.info("Start splitting energy", pid=pj["id"])
|
openstef/tasks/train_model.py
CHANGED
@@ -23,22 +23,20 @@ from datetime import datetime, timedelta
|
|
23
23
|
from pathlib import Path
|
24
24
|
|
25
25
|
from openstef.data_classes.prediction_job import PredictionJobDataClass
|
26
|
-
|
27
26
|
from openstef.enums import MLModelType, PipelineType
|
28
27
|
from openstef.exceptions import (
|
29
|
-
SkipSaveTrainingForecasts,
|
30
28
|
InputDataOngoingZeroFlatlinerError,
|
29
|
+
SkipSaveTrainingForecasts,
|
31
30
|
)
|
31
|
+
from openstef.model.serializer import MLflowSerializer
|
32
32
|
from openstef.pipeline.train_model import (
|
33
|
+
MAXIMUM_MODEL_AGE,
|
33
34
|
train_model_pipeline,
|
34
35
|
train_pipeline_step_load_model,
|
35
|
-
MAXIMUM_MODEL_AGE,
|
36
36
|
)
|
37
37
|
from openstef.tasks.utils.predictionjobloop import PredictionJobLoop
|
38
38
|
from openstef.tasks.utils.taskcontext import TaskContext
|
39
39
|
|
40
|
-
from openstef.model.serializer import MLflowSerializer
|
41
|
-
|
42
40
|
TRAINING_PERIOD_DAYS: int = 120
|
43
41
|
DEFAULT_CHECK_MODEL_AGE: bool = True
|
44
42
|
|
@@ -65,6 +63,10 @@ def train_model_task(
|
|
65
63
|
datetime_start: Start
|
66
64
|
datetime_end: End
|
67
65
|
|
66
|
+
Raises:
|
67
|
+
SkipSaveTrainingForecasts: If old model is better or too young, you don't need to save the traing forcast.
|
68
|
+
InputDataOngoingZeroFlatlinerError: If all recent load measurements are zero.
|
69
|
+
|
68
70
|
"""
|
69
71
|
# Check pipeline types
|
70
72
|
if PipelineType.TRAIN not in pj.pipelines_to_run:
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# SPDX-FileCopyrightText: 2017-2023 Contributors to the OpenSTEF project <korte.termijn.prognoses@alliander.com> # noqa E501>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
import logging
|
4
5
|
import traceback
|
5
6
|
from typing import Callable
|
6
7
|
|
@@ -9,6 +10,7 @@ import structlog
|
|
9
10
|
from openstef.exceptions import PredictionJobException
|
10
11
|
from openstef.monitoring.performance_meter import PerformanceMeter
|
11
12
|
from openstef.monitoring.teams import post_teams
|
13
|
+
from openstef.settings import Settings
|
12
14
|
|
13
15
|
|
14
16
|
class TaskContext:
|
@@ -62,6 +64,11 @@ class TaskContext:
|
|
62
64
|
self.database = database
|
63
65
|
|
64
66
|
def __enter__(self):
|
67
|
+
structlog.configure(
|
68
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
69
|
+
logging.getLevelName(Settings.log_level)
|
70
|
+
)
|
71
|
+
)
|
65
72
|
self.logger = structlog.get_logger(__name__).bind(task=self.name)
|
66
73
|
|
67
74
|
self.perf_meter = PerformanceMeter(self.logger)
|
@@ -1,17 +1,19 @@
|
|
1
1
|
# SPDX-FileCopyrightText: 2017-2023 Contributors to the OpenSTEF project <korte.termijn.prognoses@alliander.com> # noqa E501>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
import logging
|
5
|
+
import math
|
4
6
|
from datetime import datetime, timedelta
|
5
7
|
from typing import Union
|
6
8
|
|
7
|
-
import math
|
8
9
|
import numpy as np
|
9
10
|
import pandas as pd
|
10
11
|
import structlog
|
11
12
|
|
12
13
|
from openstef.exceptions import InputDataOngoingZeroFlatlinerError
|
13
|
-
from openstef.preprocessing.preprocessing import replace_repeated_values_with_nan
|
14
14
|
from openstef.model.regressors.regressor import OpenstfRegressor
|
15
|
+
from openstef.preprocessing.preprocessing import replace_repeated_values_with_nan
|
16
|
+
from openstef.settings import Settings
|
15
17
|
|
16
18
|
|
17
19
|
def validate(
|
@@ -37,7 +39,15 @@ def validate(
|
|
37
39
|
Returns:
|
38
40
|
Dataframe where repeated values are set to None
|
39
41
|
|
42
|
+
Raises:
|
43
|
+
InputDataOngoingZeroFlatlinerError: If all recent load measurements are zero.
|
44
|
+
|
40
45
|
"""
|
46
|
+
structlog.configure(
|
47
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
48
|
+
logging.getLevelName(Settings.log_level)
|
49
|
+
)
|
50
|
+
)
|
41
51
|
logger = structlog.get_logger(__name__)
|
42
52
|
|
43
53
|
if not isinstance(data.index, pd.DatetimeIndex):
|
@@ -81,6 +91,11 @@ def validate(
|
|
81
91
|
|
82
92
|
|
83
93
|
def drop_target_na(data: pd.DataFrame) -> pd.DataFrame:
|
94
|
+
structlog.configure(
|
95
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
96
|
+
logging.getLevelName(Settings.log_level)
|
97
|
+
)
|
98
|
+
)
|
84
99
|
logger = structlog.get_logger(__name__)
|
85
100
|
len_original = len(data)
|
86
101
|
# Remove where load is NA, NaN features are preserved
|
@@ -119,6 +134,11 @@ def is_data_sufficient(
|
|
119
134
|
else:
|
120
135
|
weights = model.feature_importance_dataframe
|
121
136
|
|
137
|
+
structlog.configure(
|
138
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
139
|
+
logging.getLevelName(Settings.log_level)
|
140
|
+
)
|
141
|
+
)
|
122
142
|
logger = structlog.get_logger(__name__)
|
123
143
|
# Set output variable
|
124
144
|
is_sufficient = True
|
@@ -251,6 +271,11 @@ def calc_completeness_dataframe(
|
|
251
271
|
Dataframe with fraction of completeness per column
|
252
272
|
|
253
273
|
"""
|
274
|
+
structlog.configure(
|
275
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
276
|
+
logging.getLevelName(Settings.log_level)
|
277
|
+
)
|
278
|
+
)
|
254
279
|
logger = structlog.get_logger(__name__)
|
255
280
|
|
256
281
|
if homogenise and isinstance(df.index, pd.DatetimeIndex) and len(df) > 0:
|