openstef-models 4.2.2__tar.gz → 4.3.0__tar.gz

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.
Files changed (164) hide show
  1. {openstef_models-4.2.2 → openstef_models-4.3.0}/PKG-INFO +2 -2
  2. {openstef_models-4.2.2 → openstef_models-4.3.0}/pyproject.toml +1 -1
  3. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/mlflow/mlflow_storage.py +44 -3
  4. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/median_forecaster.py +4 -1
  5. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/presets/forecasting_workflow.py +16 -1
  6. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/postprocessing/__init__.py +7 -1
  7. openstef_models-4.3.0/src/openstef_models/transforms/postprocessing/conformalized_quantile_calibrator.py +115 -0
  8. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/utils/data_split.py +1 -1
  9. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/mlflow/test_mlflow_storage.py +20 -1
  10. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/conftest.py +1 -1
  11. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_median_forecaster.py +6 -1
  12. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_dimensionality_reducer.py +1 -1
  13. openstef_models-4.3.0/tests/unit/transforms/postprocessing/test_conformalized_quantile_calibrator.py +141 -0
  14. {openstef_models-4.2.2 → openstef_models-4.3.0}/.gitignore +0 -0
  15. {openstef_models-4.2.2 → openstef_models-4.3.0}/README.md +0 -0
  16. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/__init__.py +0 -0
  17. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/explainability/__init__.py +0 -0
  18. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/explainability/mixins.py +0 -0
  19. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/explainability/plotters/__init__.py +0 -0
  20. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/explainability/plotters/contributions_plotter.py +0 -0
  21. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/explainability/plotters/feature_importance_plotter.py +0 -0
  22. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/__init__.py +0 -0
  23. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/joblib/__init__.py +0 -0
  24. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/joblib/joblib_model_serializer.py +0 -0
  25. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/mlflow/__init__.py +0 -0
  26. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/mlflow/mlflow_storage_callback.py +0 -0
  27. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/optuna/__init__.py +0 -0
  28. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/integrations/optuna/tuner.py +0 -0
  29. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/mixins/__init__.py +0 -0
  30. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/mixins/callbacks.py +0 -0
  31. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/mixins/model_serializer.py +0 -0
  32. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/__init__.py +0 -0
  33. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/component_splitting/__init__.py +0 -0
  34. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/component_splitting/component_splitter.py +0 -0
  35. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/component_splitting/constant_component_splitter.py +0 -0
  36. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/component_splitting/linear_component_splitter.py +0 -0
  37. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/component_splitting/linear_component_splitter_model/linear_component_splitter_model.z +0 -0
  38. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/component_splitting/linear_component_splitter_model/linear_component_splitter_model.z.license +0 -0
  39. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/component_splitting_model.py +0 -0
  40. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/__init__.py +0 -0
  41. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/base_case_forecaster.py +0 -0
  42. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/constant_quantile_forecaster.py +0 -0
  43. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/flatliner_forecaster.py +0 -0
  44. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/forecaster.py +0 -0
  45. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/gblinear_forecaster.py +0 -0
  46. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/lgbm_forecaster.py +0 -0
  47. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/lgbmlinear_forecaster.py +0 -0
  48. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting/xgboost_forecaster.py +0 -0
  49. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/models/forecasting_model.py +0 -0
  50. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/presets/__init__.py +0 -0
  51. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/testing.py +0 -0
  52. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/__init__.py +0 -0
  53. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/energy_domain/__init__.py +0 -0
  54. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/energy_domain/wind_power_feature_adder.py +0 -0
  55. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/__init__.py +0 -0
  56. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/dimensionality_reducer.py +0 -0
  57. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/empty_feature_remover.py +0 -0
  58. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/flagger.py +0 -0
  59. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/imputer.py +0 -0
  60. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/nan_dropper.py +0 -0
  61. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/outlier_handler.py +0 -0
  62. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/sample_weighter.py +0 -0
  63. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/scaler.py +0 -0
  64. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/selector.py +0 -0
  65. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/general/shifter.py +0 -0
  66. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/postprocessing/confidence_interval_applicator.py +0 -0
  67. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/postprocessing/isotonic_quantile_calibrator.py +0 -0
  68. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/postprocessing/quantile_sorter.py +0 -0
  69. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/time_domain/__init__.py +0 -0
  70. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/time_domain/cyclic_features_adder.py +0 -0
  71. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/time_domain/datetime_features_adder.py +0 -0
  72. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/time_domain/holiday_features_adder.py +0 -0
  73. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/time_domain/lags_adder.py +0 -0
  74. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/time_domain/rolling_aggregates_adder.py +0 -0
  75. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/time_domain/versioned_lags_adder.py +0 -0
  76. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/validation/__init__.py +0 -0
  77. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/validation/completeness_checker.py +0 -0
  78. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/validation/flatline_checker.py +0 -0
  79. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/validation/input_consistency_checker.py +0 -0
  80. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/weather_domain/__init__.py +0 -0
  81. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/weather_domain/atmosphere_derived_features_adder.py +0 -0
  82. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/weather_domain/daylight_feature_adder.py +0 -0
  83. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/transforms/weather_domain/radiation_derived_features_adder.py +0 -0
  84. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/utils/__init__.py +0 -0
  85. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/utils/evaluation_functions.py +0 -0
  86. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/utils/feature_selection.py +0 -0
  87. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/utils/loss_functions.py +0 -0
  88. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/utils/multi_quantile_regressor.py +0 -0
  89. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/utils/xgboost.py +0 -0
  90. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/workflows/__init__.py +0 -0
  91. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/workflows/callbacks/__init__.py +0 -0
  92. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/workflows/callbacks/data_save.py +0 -0
  93. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/workflows/callbacks/model_performance_callback.py +0 -0
  94. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/workflows/custom_component_split_workflow.py +0 -0
  95. {openstef_models-4.2.2 → openstef_models-4.3.0}/src/openstef_models/workflows/custom_forecasting_workflow.py +0 -0
  96. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/__init__.py +0 -0
  97. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/integration/__init__.py +0 -0
  98. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/integration/test_integration.py +0 -0
  99. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/__init__.py +0 -0
  100. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/explainability/__init__.py +0 -0
  101. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/explainability/plotters/__init__.py +0 -0
  102. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/explainability/plotters/test_contributions_plotter.py +0 -0
  103. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/__init__.py +0 -0
  104. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/joblib/__init__.py +0 -0
  105. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/joblib/test_joblib_model_serializer.py +0 -0
  106. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/mlflow/__init__.py +0 -0
  107. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/mlflow/test_mlflow_storage_callback.py +0 -0
  108. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/optuna/__init__.py +0 -0
  109. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/integrations/optuna/test_tuner.py +0 -0
  110. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/__init__.py +0 -0
  111. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/component_splitting/__init__.py +0 -0
  112. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/component_splitting/test_constant_component_splitter.py +0 -0
  113. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/component_splitting/test_linear_component_splitter.py +0 -0
  114. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/__init__.py +0 -0
  115. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_base_case_forecaster.py +0 -0
  116. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_constant_quantile_forecaster.py +0 -0
  117. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_flatliner_forecaster.py +0 -0
  118. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_gblinear_forecaster.py +0 -0
  119. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_lgbm_forecaster.py +0 -0
  120. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_lgbmlinear_forecaster.py +0 -0
  121. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/forecasting/test_xgboost_forecaster.py +0 -0
  122. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/models/test_forecasting_model.py +0 -0
  123. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/test_example.py +0 -0
  124. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/__init__.py +0 -0
  125. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/energy_domain/__init__.py +0 -0
  126. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/energy_domain/test_wind_power_feature_adder.py +0 -0
  127. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/__init__.py +0 -0
  128. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_empty_feature_remover.py +0 -0
  129. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_flagger.py +0 -0
  130. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_imputer.py +0 -0
  131. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_nan_dropper.py +0 -0
  132. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_outlier_handler.py +0 -0
  133. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_sample_weighter.py +0 -0
  134. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_scaler.py +0 -0
  135. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_selector.py +0 -0
  136. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/general/test_shifter.py +0 -0
  137. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/postprocessing/__init__.py +0 -0
  138. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/postprocessing/test_confidence_interval_applicator.py +0 -0
  139. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/postprocessing/test_isotonic_quantile_calibrator.py +0 -0
  140. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/postprocessing/test_quantile_sorter.py +0 -0
  141. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/time_domain/__init__.py +0 -0
  142. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/time_domain/test_cyclic_features_adder.py +0 -0
  143. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/time_domain/test_datetime_features_adder.py +0 -0
  144. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/time_domain/test_holiday_features_adder.py +0 -0
  145. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/time_domain/test_lags_adder.py +0 -0
  146. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/time_domain/test_rolling_aggregates_adder.py +0 -0
  147. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/time_domain/test_versioned_lags_adder.py +0 -0
  148. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/validation/__init__.py +0 -0
  149. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/validation/test_completeness_checker.py +0 -0
  150. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/validation/test_flatline_checker.py +0 -0
  151. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/validation/test_input_consistency_checker.py +0 -0
  152. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/weather_domain/__init__.py +0 -0
  153. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/weather_domain/test_atmosphere_derived_features_adder.py +0 -0
  154. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/weather_domain/test_daylight_feature_adder.py +0 -0
  155. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/transforms/weather_domain/test_radiation_derived_featuers_adder.py +0 -0
  156. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/utils/__init__.py +0 -0
  157. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/utils/test_data_split.py +0 -0
  158. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/utils/test_feature_selection.py +0 -0
  159. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/utils/test_loss_functions.py +0 -0
  160. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/utils/test_multi_quantile_regressor.py +0 -0
  161. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/workflows/__init__.py +0 -0
  162. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/workflows/callbacks/__init__.py +0 -0
  163. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/workflows/callbacks/test_model_performance_callback.py +0 -0
  164. {openstef_models-4.2.2 → openstef_models-4.3.0}/tests/unit/workflows/test_custom_forecasting_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: openstef-models
3
- Version: 4.2.2
3
+ Version: 4.3.0
4
4
  Summary: Core models for OpenSTEF
5
5
  Project-URL: Documentation, https://openstef.github.io/openstef/index.html
6
6
  Project-URL: Homepage, https://lfenergy.org/projects/openstef/
@@ -7,7 +7,7 @@ requires = [ "hatchling" ]
7
7
 
8
8
  [project]
9
9
  name = "openstef-models"
10
- version = "4.2.2"
10
+ version = "4.3.0"
11
11
  description = "Core models for OpenSTEF"
12
12
  readme = "README.md"
13
13
  keywords = [ "energy", "forecasting", "machinelearning" ]
@@ -22,7 +22,7 @@ from urllib.parse import urlparse
22
22
  from mlflow import MlflowClient
23
23
  from mlflow.entities import Metric, Param, Run
24
24
  from mlflow.exceptions import MlflowException
25
- from pydantic import Field, PrivateAttr
25
+ from pydantic import ConfigDict, Field, PrivateAttr
26
26
 
27
27
  from openstef_core.base_model import BaseConfig
28
28
  from openstef_core.exceptions import ModelNotFoundError
@@ -31,7 +31,7 @@ from openstef_models.integrations.joblib import JoblibModelSerializer
31
31
  from openstef_models.mixins import ModelIdentifier, ModelSerializer
32
32
 
33
33
 
34
- def normalize_tracking_uri(uri: str) -> str:
34
+ def normalize_tracking_uri(uri: str, exceptions: Sequence[str] = []) -> str:
35
35
  r"""Normalize a tracking URI to a file:/// URI when it refers to a local path.
36
36
 
37
37
  MLflow's model registry rejects bare Windows paths (e.g. ``D:\mlflow``) because
@@ -44,10 +44,13 @@ def normalize_tracking_uri(uri: str) -> str:
44
44
 
45
45
  Args:
46
46
  uri: Raw tracking URI string, may be a path or a proper URI.
47
+ exceptions: List of URI to treat as urls even if they have no scheme
47
48
 
48
49
  Returns:
49
50
  A ``file:///`` URI for local paths, or the original URI for remote schemes.
50
51
  """
52
+ if uri in exceptions:
53
+ return uri
51
54
  scheme = urlparse(uri).scheme
52
55
  # Empty scheme → relative/absolute POSIX path.
53
56
  # Single-letter scheme → Windows drive letter (e.g. "D" from "D:\\...").
@@ -63,9 +66,33 @@ class MLFlowStorage(BaseConfig):
63
66
  Handles creation, storage, and retrieval of MLflow runs including models,
64
67
  training data, metrics, and hyperparameters. Organizes artifacts locally
65
68
  before uploading to MLflow tracking server.
69
+
70
+ ``tracking_uri`` configures the Tracking backend that stores experiments and
71
+ runs. ``registry_uri`` optionally configures a separate Model Registry
72
+ backend; if it is omitted, MLflow uses the registry associated with the
73
+ tracking backend. ``artifact_location`` configures the artifact root for
74
+ experiments created by this instance.
75
+
76
+ Typical configurations:
77
+
78
+ tracking_uri="sqlite:///mlflow.db", # Local SQLite tracking backend with separate local artifacts
79
+ tracking_uri="https://mlflow.example.com", # Remote MLflow Tracking Server with S3 artifacts
80
+ tracking_uri="databricks", # Databricks MLflow Tracking Server with DBFS artifacts
81
+
82
+ A caller may provide ``client`` to use an already configured
83
+ :class:`mlflow.MlflowClient`. When supplied, it takes precedence over
84
+ ``tracking_uri`` and ``registry_uri`` and is excluded from serialized model
85
+ configuration.
66
86
  """
67
87
 
88
+ model_config = ConfigDict(arbitrary_types_allowed=True)
89
+
68
90
  tracking_uri: str = Field(default="./mlflow", description="MLflow tracking server URI.")
91
+ registry_uri: str | None = Field(default=None, description="MLflow tracking server URI.")
92
+
93
+ # Excluded from model_dump(), JSON serialization, and config persistence.
94
+ client: MlflowClient | None = Field(default=None, exclude=True, repr=False)
95
+
69
96
  local_artifacts_path: Path = Field(
70
97
  default=Path("./mlflow_artifacts_local"), description="Local path for storing MLflow artifacts before upload."
71
98
  )
@@ -86,6 +113,11 @@ class MLFlowStorage(BaseConfig):
86
113
 
87
114
  model_serializer: ModelSerializer = Field(default_factory=JoblibModelSerializer)
88
115
 
116
+ uri_exceptions: Sequence[str] = Field(
117
+ default=["databricks"],
118
+ description="List of URI treat as urls even if they have no scheme",
119
+ )
120
+
89
121
  _client: MlflowClient = PrivateAttr()
90
122
  _logger: logging.Logger = PrivateAttr(default=logging.getLogger(__name__))
91
123
 
@@ -95,8 +127,17 @@ class MLFlowStorage(BaseConfig):
95
127
  # Suppress MLflow's stdout messages (emoji URLs)
96
128
  os.environ.setdefault("MLFLOW_SUPPRESS_PRINTING_URL_TO_STDOUT", "true")
97
129
  os.environ.setdefault("MLFLOW_ENABLE_ARTIFACTS_PROGRESS_BAR", "false")
130
+
131
+ if self.client is not None:
132
+ self._client = self.client
133
+ return
134
+
98
135
  self.tracking_uri = normalize_tracking_uri(self.tracking_uri)
99
- self._client = MlflowClient(tracking_uri=self.tracking_uri)
136
+ self.registry_uri = normalize_tracking_uri(self.registry_uri) if self.registry_uri else None
137
+ self._client = MlflowClient(
138
+ tracking_uri=self.tracking_uri,
139
+ registry_uri=self.registry_uri,
140
+ )
100
141
 
101
142
  def create_run(
102
143
  self,
@@ -200,7 +200,10 @@ class MedianForecaster(Forecaster, ExplainableForecaster, ContributionsMixin):
200
200
  lag_df = input_data.reindex(new_index, fill_value=np.nan)[self._feature_names]
201
201
 
202
202
  # Convert the lag DataFrame to NumPy arrays for faster processing.
203
- lag_array = lag_df.to_numpy()
203
+ # Copy explicitly: pandas Copy-on-Write may hand back a read-only view of the
204
+ # DataFrame's underlying block, but the autoregressive loop below fills the
205
+ # array in place (via _fill_diagonal_with_median), so it must own a writable copy.
206
+ lag_array = lag_df.to_numpy(copy=True)
204
207
  # Initialize the prediction array with NaNs.
205
208
  prediction = np.full(lag_array.shape[0], np.nan)
206
209
 
@@ -233,6 +233,14 @@ class ForecastingWorkflowConfig(BaseConfig): # PredictionJob
233
233
  description="Feature selection for which features to replace out-of-range values with NaN. "
234
234
  "Defaults to no features (disabled).",
235
235
  )
236
+ outlier_strictness_n_std: float = Field(
237
+ default=2.0,
238
+ description=(
239
+ "Number of standard deviations for outlier detection (lower = stricter bounds). "
240
+ "Only used when nan_on_outlier_features is enabled."
241
+ ),
242
+ gt=0,
243
+ )
236
244
  max_day_lags: int = Field(
237
245
  default=14,
238
246
  description="Maximum number of days to look back for day-based lags. "
@@ -412,7 +420,14 @@ def create_forecasting_workflow(
412
420
  if config.model == "xgboost":
413
421
  nan_outlier_handlers = [
414
422
  *(
415
- [OutlierHandler(mode="standard", selection=config.nan_on_outlier_features, outlier_action="nan")]
423
+ [
424
+ OutlierHandler(
425
+ mode="standard",
426
+ selection=config.nan_on_outlier_features,
427
+ outlier_action="nan",
428
+ n_std=config.outlier_strictness_n_std,
429
+ )
430
+ ]
416
431
  if config.nan_on_outlier_features != FeatureSelection.NONE
417
432
  else []
418
433
  ),
@@ -10,7 +10,13 @@ operate on ForecastDataset objects after the core prediction step.
10
10
  """
11
11
 
12
12
  from openstef_models.transforms.postprocessing.confidence_interval_applicator import ConfidenceIntervalApplicator
13
+ from openstef_models.transforms.postprocessing.conformalized_quantile_calibrator import ConformalizedQuantileCalibrator
13
14
  from openstef_models.transforms.postprocessing.isotonic_quantile_calibrator import IsotonicQuantileCalibrator
14
15
  from openstef_models.transforms.postprocessing.quantile_sorter import QuantileSorter
15
16
 
16
- __all__ = ["ConfidenceIntervalApplicator", "IsotonicQuantileCalibrator", "QuantileSorter"]
17
+ __all__ = [
18
+ "ConfidenceIntervalApplicator",
19
+ "ConformalizedQuantileCalibrator",
20
+ "IsotonicQuantileCalibrator",
21
+ "QuantileSorter",
22
+ ]
@@ -0,0 +1,115 @@
1
+ # SPDX-FileCopyrightText: 2026 Contributors to the OpenSTEF project <openstef@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+ """Asymmetric conformal quantile calibration without external dependencies."""
6
+
7
+ import logging
8
+ from typing import override
9
+
10
+ import numpy as np
11
+ from pydantic import BaseModel, Field, PrivateAttr
12
+
13
+ from openstef_core.datasets import ForecastDataset
14
+ from openstef_core.exceptions import NotFittedError
15
+ from openstef_core.mixins import Transform
16
+ from openstef_core.types import Quantile
17
+
18
+ MEDIAN_QUANTILE = Quantile(0.5)
19
+
20
+
21
+ def _conformal_quantile(scores: np.ndarray, level: float) -> float:
22
+ """Return the finite-sample conformal quantile of one-sided scores."""
23
+ n_samples = scores.size
24
+ if n_samples == 0:
25
+ return 0.0
26
+ adjusted_level = min(level * (n_samples + 1) / n_samples, 1.0)
27
+ return float(np.quantile(scores, adjusted_level, method="higher"))
28
+
29
+
30
+ class ConformalizedQuantileCalibrator(BaseModel, Transform[ForecastDataset, ForecastDataset]):
31
+ """Apply asymmetric split-conformal corrections to forecast quantiles.
32
+
33
+ Lower quantiles are corrected using lower-tail scores and upper quantiles
34
+ using upper-tail scores. The median is left unchanged by default.
35
+
36
+ Args:
37
+ quantiles: Quantiles to calibrate. If None, all input quantiles are used.
38
+ conformalize_median: Whether to apply the upper-tail correction to P50.
39
+ min_calibration_samples: Minimum number of valid calibration pairs required
40
+ before fitting a correction for a quantile. Quantiles with fewer valid
41
+ pairs are left unchanged; if all quantiles are skipped, fitting becomes
42
+ a no-op calibrator.
43
+ """
44
+
45
+ quantiles: list[Quantile] | None = Field(default=None)
46
+ conformalize_median: bool = Field(default=False)
47
+ min_calibration_samples: int = Field(default=100, ge=1)
48
+
49
+ _corrections: dict[str, float] = PrivateAttr(default_factory=dict)
50
+ _is_fitted: bool = PrivateAttr(default=False)
51
+ _logger: logging.Logger = PrivateAttr(default=logging.getLogger(__name__))
52
+
53
+ @property
54
+ @override
55
+ def is_fitted(self) -> bool:
56
+ """Return whether calibration corrections have been fitted."""
57
+ return self._is_fitted
58
+
59
+ @override
60
+ def fit(self, data: ForecastDataset) -> None:
61
+ """Estimate one-sided conformal corrections from forecast errors."""
62
+ if data.target_series is None:
63
+ raise ValueError("Input data must contain target series for calibration.")
64
+
65
+ quantiles_to_fit = self.quantiles if self.quantiles is not None else data.quantiles
66
+ if not quantiles_to_fit:
67
+ raise ValueError("No quantiles found to calibrate.")
68
+
69
+ self._is_fitted = False
70
+ actuals = data.target_series.to_numpy()
71
+ self._corrections = {}
72
+
73
+ for quantile in quantiles_to_fit:
74
+ column = quantile.format()
75
+ if column not in data.data.columns:
76
+ continue
77
+ if quantile == MEDIAN_QUANTILE and not self.conformalize_median:
78
+ continue
79
+
80
+ predictions = data.data[column].to_numpy()
81
+ valid = ~(np.isnan(predictions) | np.isnan(actuals))
82
+ predictions_valid = predictions[valid]
83
+ actuals_valid = actuals[valid]
84
+ if predictions_valid.size < self.min_calibration_samples:
85
+ self._logger.warning(
86
+ "Skipping calibration for quantile %s: not enough data points (found %d, require %d).",
87
+ column,
88
+ predictions_valid.size,
89
+ self.min_calibration_samples,
90
+ )
91
+ continue
92
+
93
+ if quantile < MEDIAN_QUANTILE:
94
+ scores = predictions_valid - actuals_valid
95
+ self._corrections[column] = -_conformal_quantile(scores, level=1.0 - quantile)
96
+ else:
97
+ scores = actuals_valid - predictions_valid
98
+ self._corrections[column] = _conformal_quantile(scores, level=quantile)
99
+
100
+ self._is_fitted = True
101
+
102
+ @override
103
+ def transform(self, data: ForecastDataset) -> ForecastDataset:
104
+ """Apply fitted corrections without changing quantile ordering."""
105
+ if not self._is_fitted:
106
+ raise NotFittedError(self.__class__.__name__)
107
+
108
+ predictions = data.data.copy(deep=False)
109
+ for column, correction in self._corrections.items():
110
+ if column in predictions.columns:
111
+ predictions[column] = predictions[column] + correction
112
+ return data._copy_with_data(data=predictions) # noqa: SLF001 - safe transform copy
113
+
114
+
115
+ __all__ = ["ConformalizedQuantileCalibrator"]
@@ -201,7 +201,7 @@ def _get_extreme_days(
201
201
  raise TypeError("target_series must have a DatetimeIndex.")
202
202
 
203
203
  # Compute daily min and max once
204
- daily_agg: pd.DataFrame = target_series.resample("1D").agg(["min", "max"]) # type: ignore
204
+ daily_agg: pd.DataFrame = target_series.resample("1D").agg(["min", "max"])
205
205
  n_days = len(daily_agg)
206
206
  n_extremes = max(int(fraction * n_days), 2)
207
207
 
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
  from typing import TYPE_CHECKING, cast
8
8
 
9
9
  import pytest
10
+ from mlflow import MlflowClient
10
11
 
11
12
  from openstef_core.mixins import HyperParams, Stateful
12
13
  from openstef_models.integrations.mlflow import MLFlowStorage
@@ -49,6 +50,21 @@ def model_id() -> str:
49
50
  return "test_model_123"
50
51
 
51
52
 
53
+ def test_construct_from_mlflow(tmp_path: Path, model_id: str):
54
+ """Test that experiment_name_prefix is prepended to experiment names."""
55
+ # Arrange
56
+ mlflow_client = MlflowClient(tracking_uri=f"sqlite:///{tmp_path / 'mlflow.db'}")
57
+
58
+ # Act
59
+ storage = MLFlowStorage(
60
+ tracking_uri=f"sqlite:///{tmp_path / 'mlflow.db'}",
61
+ client=mlflow_client,
62
+ )
63
+
64
+ # Assert
65
+ assert storage._client is mlflow_client
66
+
67
+
52
68
  def test_create_run(storage: MLFlowStorage, model_id: str):
53
69
  """Test that create_run converts HyperParams to MLflow parameters."""
54
70
  # Arrange
@@ -194,12 +210,15 @@ def test_search_run__returns_matching_run(storage: MLFlowStorage, model_id: str)
194
210
  pytest.param("https://mlflow.example.com:5000", "https://mlflow.example.com:5000", id="https-port"),
195
211
  pytest.param("sqlite:///mlflow.db", "sqlite:///mlflow.db", id="sqlite"),
196
212
  pytest.param("postgresql://user:pass@host/db", "postgresql://user:pass@host/db", id="postgresql"),
213
+ pytest.param("databricks", "databricks", id="databricks"),
197
214
  ],
198
215
  )
199
216
  def test_normalize_tracking_uri(uri: str, expected: str):
200
217
  """Local paths become file:/// URIs; remote/database URIs pass through unchanged."""
201
218
  # Act
202
- result = normalize_tracking_uri(uri)
219
+ result = normalize_tracking_uri(
220
+ uri, exceptions=["databricks"]
221
+ ) # databricks is a special case that should not be normalized
203
222
 
204
223
  # Assert
205
224
  if expected == "file:///":
@@ -30,7 +30,7 @@ def sample_forecast_input_dataset() -> ForecastInputDataset:
30
30
  "feature2": feature_2,
31
31
  "feature3": feature_3,
32
32
  },
33
- index=pd.date_range(start=start_date, periods=num_samples, freq="1d"),
33
+ index=pd.date_range(start=start_date, periods=num_samples, freq="1D"),
34
34
  ),
35
35
  sample_interval=timedelta(days=1),
36
36
  target_column="load",
@@ -158,7 +158,12 @@ def test_median_handles_all_missing_data():
158
158
  {
159
159
  "quantile_P50": [],
160
160
  },
161
- index=pd.DatetimeIndex([], freq="h"),
161
+ # Slice `index` rather than building a bare `DatetimeIndex([], freq="h")` so the
162
+ # (empty) expected index shares its dtype resolution with the one produced by the
163
+ # real prediction pipeline (pandas infers datetime64 resolution from context, and
164
+ # an index with no data to infer from does not necessarily match one derived from
165
+ # actual timestamps).
166
+ index=index[:0],
162
167
  ),
163
168
  sample_interval=training_input_data.sample_interval,
164
169
  forecast_start=training_input_data.forecast_start,
@@ -33,7 +33,7 @@ def sample_forecast_input_dataset() -> TimeSeriesDataset:
33
33
  "feature2": feature_2,
34
34
  "feature3": feature_3,
35
35
  },
36
- index=pd.date_range(start=start_date, periods=num_samples, freq="1d"),
36
+ index=pd.date_range(start=start_date, periods=num_samples, freq="1D"),
37
37
  ),
38
38
  sample_interval=timedelta(days=1),
39
39
  )
@@ -0,0 +1,141 @@
1
+ # SPDX-FileCopyrightText: 2026 Contributors to the OpenSTEF project <openstef@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import pytest
8
+
9
+ from openstef_core.datasets import ForecastDataset
10
+ from openstef_core.types import Quantile
11
+ from openstef_models.transforms.postprocessing import ConformalizedQuantileCalibrator
12
+
13
+
14
+ def _dataset(predictions: np.ndarray, actuals: np.ndarray) -> ForecastDataset:
15
+ """Build a forecast dataset with P10, P50, and P90 columns."""
16
+ return ForecastDataset(
17
+ data=pd.DataFrame(
18
+ {
19
+ "quantile_P10": predictions[:, 0],
20
+ "quantile_P50": predictions[:, 1],
21
+ "quantile_P90": predictions[:, 2],
22
+ "load": actuals,
23
+ },
24
+ index=pd.date_range("2025-01-01", periods=len(actuals), freq="h"),
25
+ )
26
+ )
27
+
28
+
29
+ def test_conformalized_calibrator_applies_reference_tail_corrections() -> None:
30
+ """Test asymmetric lower and upper corrections while preserving the median."""
31
+ predictions = np.column_stack([np.zeros(100), np.full(100, 5.0), np.full(100, 10.0)])
32
+ actuals = np.concatenate([np.full(90, 1.0), np.full(10, 20.0)])
33
+ calibration = _dataset(predictions, actuals)
34
+ forecast = _dataset(predictions[:2], np.full(2, 5.0))
35
+
36
+ calibrator = ConformalizedQuantileCalibrator(quantiles=[Quantile(0.1), Quantile(0.5), Quantile(0.9)])
37
+ calibrator.fit(calibration)
38
+ result = calibrator.transform(forecast)
39
+
40
+ np.testing.assert_allclose(result.data["quantile_P10"], [1.0, 1.0])
41
+ np.testing.assert_allclose(result.data["quantile_P50"], [5.0, 5.0])
42
+ np.testing.assert_allclose(result.data["quantile_P90"], [20.0, 20.0])
43
+
44
+
45
+ def test_conformalized_calibrator_can_calibrate_the_median_when_enabled() -> None:
46
+ """Test that median calibration is explicitly opt-in."""
47
+ predictions = np.column_stack([np.zeros(100), np.zeros(100), np.full(100, 10.0)])
48
+ actuals = np.full(100, 5.0)
49
+ calibration = _dataset(predictions, actuals)
50
+ forecast = _dataset(predictions[:1], np.full(1, 5.0))
51
+
52
+ calibrator = ConformalizedQuantileCalibrator(
53
+ quantiles=[Quantile(0.1), Quantile(0.5), Quantile(0.9)],
54
+ conformalize_median=True,
55
+ )
56
+ calibrator.fit(calibration)
57
+ result = calibrator.transform(forecast)
58
+
59
+ np.testing.assert_allclose(result.data["quantile_P50"], [5.0])
60
+
61
+
62
+ def test_conformalized_calibrator_skips_short_calibration_windows() -> None:
63
+ """Test that too few calibration samples leave forecasts unchanged."""
64
+ predictions = np.column_stack([np.zeros(2), np.full(2, 5.0), np.full(2, 10.0)])
65
+ actuals = np.full(2, 20.0)
66
+ calibration = _dataset(predictions, actuals)
67
+ forecast = _dataset(predictions[:1], np.full(1, 5.0))
68
+
69
+ calibrator = ConformalizedQuantileCalibrator(
70
+ quantiles=[Quantile(0.1), Quantile(0.5), Quantile(0.9)],
71
+ min_calibration_samples=3,
72
+ )
73
+ calibrator.fit(calibration)
74
+ result = calibrator.transform(forecast)
75
+
76
+ np.testing.assert_allclose(result.data["quantile_P10"], [0.0])
77
+ np.testing.assert_allclose(result.data["quantile_P90"], [10.0])
78
+
79
+
80
+ def test_conformalized_calibrator_skips_only_sparse_quantiles(caplog: pytest.LogCaptureFixture) -> None:
81
+ """Test that sparse quantiles are skipped while sufficiently populated ones calibrate."""
82
+ predictions = np.column_stack(
83
+ [
84
+ np.concatenate([np.zeros(100), [np.nan]]),
85
+ np.full(101, 5.0),
86
+ np.full(101, 10.0),
87
+ ]
88
+ )
89
+ actuals = np.full(101, 20.0)
90
+ calibration = _dataset(predictions, actuals)
91
+ forecast = _dataset(np.array([[0.0, 5.0, 10.0]]), np.array([5.0]))
92
+
93
+ calibrator = ConformalizedQuantileCalibrator(
94
+ quantiles=[Quantile(0.1), Quantile(0.5), Quantile(0.9)],
95
+ min_calibration_samples=101,
96
+ )
97
+ calibrator.fit(calibration)
98
+ result = calibrator.transform(forecast)
99
+
100
+ np.testing.assert_allclose(result.data["quantile_P10"], [0.0])
101
+ np.testing.assert_allclose(result.data["quantile_P90"], [20.0])
102
+ assert "Skipping calibration for quantile quantile_P10" in caplog.text
103
+
104
+
105
+ def test_conformalized_calibrator_skips_quantiles_missing_from_data() -> None:
106
+ """Test that configured quantiles absent from the data are skipped."""
107
+ calibration = ForecastDataset(
108
+ data=pd.DataFrame(
109
+ {
110
+ "quantile_P90": np.full(2, 10.0),
111
+ "load": np.full(2, 20.0),
112
+ },
113
+ index=pd.date_range("2025-01-01", periods=2, freq="h"),
114
+ )
115
+ )
116
+ calibrator = ConformalizedQuantileCalibrator(
117
+ quantiles=[Quantile(0.1), Quantile(0.9)],
118
+ min_calibration_samples=1,
119
+ )
120
+
121
+ calibrator.fit(calibration)
122
+ result = calibrator.transform(calibration)
123
+
124
+ np.testing.assert_allclose(result.data["quantile_P90"], [20.0, 20.0])
125
+
126
+
127
+ def test_conformalized_calibrator_does_not_sort_quantiles() -> None:
128
+ """Test that ordering remains the responsibility of a downstream sorter."""
129
+ predictions = np.column_stack([np.full(2, 10.0), np.zeros(2), np.full(2, 5.0)])
130
+ actuals = np.full(2, 5.0)
131
+ calibration = _dataset(predictions, actuals)
132
+ forecast = _dataset(predictions[:1], np.full(1, 5.0))
133
+
134
+ calibrator = ConformalizedQuantileCalibrator(
135
+ quantiles=[Quantile(0.1), Quantile(0.5), Quantile(0.9)],
136
+ min_calibration_samples=1,
137
+ )
138
+ calibrator.fit(calibration)
139
+ result = calibrator.transform(forecast)
140
+
141
+ np.testing.assert_allclose(result.data.filter(like="quantile_").to_numpy(), [[5.0, 0.0, 5.0]])