chap-core 2.2.0__tar.gz → 2.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.
- {chap_core-2.2.0 → chap_core-2.3.0}/PKG-INFO +1 -1
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/evaluation.py +28 -15
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/__init__.py +0 -6
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/mape.py +4 -4
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/outbreak_detection.py +8 -7
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/thresholds/__init__.py +25 -3
- chap_core-2.3.0/chap_core/assessment/thresholds/base.py +76 -0
- chap_core-2.3.0/chap_core/assessment/thresholds/params.py +82 -0
- chap_core-2.3.0/chap_core/assessment/thresholds/percentile.py +113 -0
- chap_core-2.3.0/chap_core/assessment/thresholds/seasonal.py +66 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/generate_modelcard.py +0 -1
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/database.py +37 -1
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/tables.py +78 -6
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/data_models.py +6 -2
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/db_worker_functions.py +19 -3
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v1/routers/analytics.py +65 -21
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v1/routers/crud.py +3 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v1/routers/visualization.py +10 -1
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/services/dataset_validation.py +15 -6
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/simulation/naive_simulator.py +8 -4
- chap_core-2.3.0/chap_core/time_period/vectorized.py +54 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/pyproject.toml +1 -1
- chap_core-2.2.0/chap_core/assessment/metrics/example_metric.py +0 -38
- chap_core-2.2.0/chap_core/assessment/metrics/test_metrics.py +0 -40
- chap_core-2.2.0/chap_core/assessment/thresholds/base.py +0 -47
- chap_core-2.2.0/chap_core/assessment/thresholds/seasonal.py +0 -84
- {chap_core-2.2.0 → chap_core-2.3.0}/README.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/adaptors/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/adaptors/command_line_interface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/adaptors/gluonts.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/adaptors/rest_api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/alarms.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/api_types.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/backtest_plots/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/backtest_plots/db_dimensions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/backtest_plots/evaluation_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/backtest_plots/horizon_location_grid.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/backtest_plots/predicted_vs_actual_linear_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/backtest_plots/predicted_vs_actual_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/backtest_plots/sample_bias_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/causal_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/data_representation_transforming.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/dataset_splitting.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/eval_tracking.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/evaluator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/evaluator_suites.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/flat_representations.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/forecast.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/horizon_location_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/horizon_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/horizon_sum.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/metric_map.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/regional_distribution.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/time_period_location_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/time_period_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_plots/time_period_sum.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metric_table.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/above_truth.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/base.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/crps.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/crps_norm.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/mae.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/peak_diff.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/percentile_coverage.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/rmse.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/metrics/winkler_score.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/prediction_evaluator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/representations.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/weather_providers/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/weather_providers/base.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/weather_providers/climatology.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/weather_providers/damped_persistence.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/weather_providers/mstl_arima.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/assessment/weather_providers/observed.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/_args.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/_common.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/aggregate_eval.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/build_counterfactual.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/causal.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/convert.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/ensemble.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/evaluate.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/explain.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/forecast.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/generated_plot_ids.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/preference_learn.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/report.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/utils.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/cli_endpoints/validate.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/climate_data/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/climate_data/seasonal_forecasts.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/climate_predictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/data/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/data/adaptors.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/data/datasets.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/data/gluonts_adaptor/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/data/gluonts_adaptor/dataset.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/data/open_dengue.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/base_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/dataset_manager.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/dataset_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/datasets_seed.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/feature_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/manager.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/model_spec_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/model_template_seed.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/database/model_templates_and_config_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/datatypes.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/docker_helper_functions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/ensemble/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/ensemble/_meta_models.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/ensemble/_predictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/ensemble/_sample_extractor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/ensemble/ensemble_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/ensemble/wrappers.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/exceptions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/distance.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/documentation.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/lime.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/perturb.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/segment.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/surrogate.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/testing/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/testing/metrics.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/explainability/userguide.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/ExtendedPredictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/external_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/github.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/mlflow_wrappers.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/model_configuration.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/python_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/r_description.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/r_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/r_models.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/external/uv_wrapper.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/feature_generators/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/feature_generators/seasonality_cluster.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/fetch/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/file_io/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/file_io/cleaners.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/file_io/example_data_set.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/file_io/external_file.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/file_io/file_paths.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/file_io/load.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/geo_coding/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/geojson.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/geometry.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/geoutils.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/README.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/base.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/config1.yaml +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/config2.yaml +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/config3.yaml +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/hpoModel.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/hpoModelInterface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/objective.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/searcher.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/hpo/types.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/log_config.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/model_spec.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/chapkit_rest_api_wrapper.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/chapkit_service_manager.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/configured_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/external_chapkit_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/external_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/external_web_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/local_configuration.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/model_template.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/model_template_interface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/models/utils.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/pandas_adaptors.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/backtest_dashboard_spec.json +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/backtest_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/conftest.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/dataset_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/evaluation_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/prediction_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/produce_plots_from_yaml.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/season_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/plotting/yaml_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/predictor/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/predictor/feature_spec.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/predictor/model_registry.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/predictor/naive_estimator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/predictor/naive_predictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/predictor/poisson.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/predictor/protocol.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/preference_learning/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/preference_learning/decision_maker.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/preference_learning/preference_learner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rbased_docker.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/app.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/auth.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/celery_tasks.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/common_routes.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/experimental.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/services/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/services/orchestrator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/services/schemas.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v1/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v1/jobs.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v1/rest_api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v1/routers/dependencies.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v2/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v2/dependencies.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v2/rest_api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v2/routers/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v2/routers/proxy.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/v2/routers/services.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/rest_api/worker_functions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/command_line_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/conda_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/docker_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/helper_functions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/mlflow_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/renv_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/runners/uv_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/services/prediction_setup_service.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/simulation/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/simulation/simulator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/spatio_temporal_data/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/spatio_temporal_data/converters.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/spatio_temporal_data/multi_country_dataset.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/spatio_temporal_data/temporal_dataclass.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/testing/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/testing/estimators.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/testing/external_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/testing/mocks.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/testing/testing.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/time_period/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/time_period/date_util_wrapper.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/time_period/multi_resolution.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/time_period/period_assignment.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/time_period/relationships.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/transformations/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/transformations/covid_mask.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/util.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/validators.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/worker/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/worker/interface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.0}/chap_core/worker/rq_worker.py +0 -0
|
@@ -11,7 +11,7 @@ from abc import ABC, abstractmethod
|
|
|
11
11
|
from collections.abc import Iterable
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import cast
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
import pandas as pd
|
|
@@ -19,14 +19,7 @@ import pandera.pandas as pa
|
|
|
19
19
|
import xarray as xr
|
|
20
20
|
from packaging.version import Version
|
|
21
21
|
|
|
22
|
-
from chap_core.
|
|
23
|
-
DEFAULT_WEATHER_PROVIDER_ID,
|
|
24
|
-
LEGACY_WEATHER_PROVIDER_ID,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from chap_core.api_types import BacktestParams
|
|
29
|
-
|
|
22
|
+
from chap_core.api_types import BacktestParams
|
|
30
23
|
from chap_core.assessment.flat_representations import (
|
|
31
24
|
FlatForecasts,
|
|
32
25
|
FlatObserved,
|
|
@@ -34,10 +27,14 @@ from chap_core.assessment.flat_representations import (
|
|
|
34
27
|
convert_backtest_to_flat_forecasts,
|
|
35
28
|
max_horizon_distance,
|
|
36
29
|
)
|
|
30
|
+
from chap_core.assessment.weather_providers import (
|
|
31
|
+
DEFAULT_WEATHER_PROVIDER_ID,
|
|
32
|
+
LEGACY_WEATHER_PROVIDER_ID,
|
|
33
|
+
)
|
|
37
34
|
from chap_core.data import DataSet as _DataSet
|
|
38
35
|
from chap_core.database.dataset_tables import DataSet, Observation, ObservationBase
|
|
39
36
|
from chap_core.database.model_templates_and_config_tables import ConfiguredModelDB
|
|
40
|
-
from chap_core.database.tables import Backtest, BacktestForecast
|
|
37
|
+
from chap_core.database.tables import Backtest, BacktestForecast, BacktestSpecification
|
|
41
38
|
from chap_core.datatypes import SamplesWithTruth
|
|
42
39
|
from chap_core.external.model_configuration import ModelTemplateConfigV2
|
|
43
40
|
from chap_core.rest_api.data_models import BacktestCreate
|
|
@@ -284,8 +281,8 @@ class Evaluation(EvaluationBase):
|
|
|
284
281
|
def future_weather_provider(self) -> str:
|
|
285
282
|
"""Id of the future-weather provider these results were produced with.
|
|
286
283
|
|
|
287
|
-
The backtest
|
|
288
|
-
and NetCDF exports cannot disagree.
|
|
284
|
+
The backtest's specification is the single source of truth, so database
|
|
285
|
+
persistence and NetCDF exports cannot disagree.
|
|
289
286
|
"""
|
|
290
287
|
return self._backtest.future_weather_provider
|
|
291
288
|
|
|
@@ -311,12 +308,22 @@ class Evaluation(EvaluationBase):
|
|
|
311
308
|
info: BacktestCreate,
|
|
312
309
|
historical_observations: list[Observation] | None = None,
|
|
313
310
|
historical_context_periods: int = 0,
|
|
311
|
+
specification: BacktestSpecification | None = None,
|
|
314
312
|
) -> "Evaluation":
|
|
315
313
|
info.created = datetime.datetime.now()
|
|
314
|
+
# The parameters live on the specification now and are computed fields on
|
|
315
|
+
# Backtest, reading through the link. Excluding them here keeps that explicit;
|
|
316
|
+
# passing them would be silently ignored rather than rejected.
|
|
316
317
|
backtest = Backtest(
|
|
317
|
-
**info.model_dump()
|
|
318
|
+
**info.model_dump(exclude=set(BacktestParams.model_fields))
|
|
318
319
|
| {"model_db_id": configured_model.id, "model_template_version": configured_model.model_template.version}
|
|
319
320
|
)
|
|
321
|
+
# Callers persisting the backtest pass the deduplicated row resolved against the
|
|
322
|
+
# database. The CLI paths build an in-memory evaluation only, so a detached
|
|
323
|
+
# specification carrying the same parameters is enough for them.
|
|
324
|
+
backtest.specification = specification or BacktestSpecification(
|
|
325
|
+
dataset_id=info.dataset_id, **info.model_dump(include=set(BacktestParams.model_fields))
|
|
326
|
+
)
|
|
320
327
|
org_units = set()
|
|
321
328
|
split_points = set()
|
|
322
329
|
observations = []
|
|
@@ -393,7 +400,7 @@ class Evaluation(EvaluationBase):
|
|
|
393
400
|
configured_model: ConfiguredModelDB,
|
|
394
401
|
estimator,
|
|
395
402
|
dataset: _DataSet,
|
|
396
|
-
backtest_params:
|
|
403
|
+
backtest_params: BacktestParams,
|
|
397
404
|
backtest_name: str = "evaluation",
|
|
398
405
|
historical_context_years: int = 6,
|
|
399
406
|
) -> "Evaluation":
|
|
@@ -689,7 +696,13 @@ class Evaluation(EvaluationBase):
|
|
|
689
696
|
split_periods=split_periods,
|
|
690
697
|
forecasts=[],
|
|
691
698
|
dataset_id=0,
|
|
692
|
-
|
|
699
|
+
# The stored file carries no parameters beyond the provider, so this is a
|
|
700
|
+
# detached placeholder that keeps the parameter properties readable.
|
|
701
|
+
specification=BacktestSpecification(
|
|
702
|
+
dataset_id=0,
|
|
703
|
+
org_units=org_units,
|
|
704
|
+
future_weather_provider=str(ds.attrs["future_weather_provider"]),
|
|
705
|
+
),
|
|
693
706
|
)
|
|
694
707
|
|
|
695
708
|
forecasts_df = pd.DataFrame(cast("pd.DataFrame", flat_data.forecasts))
|
|
@@ -83,13 +83,11 @@ def _discover_metrics():
|
|
|
83
83
|
above_truth,
|
|
84
84
|
crps,
|
|
85
85
|
crps_norm,
|
|
86
|
-
example_metric,
|
|
87
86
|
mae,
|
|
88
87
|
mape,
|
|
89
88
|
outbreak_detection,
|
|
90
89
|
percentile_coverage,
|
|
91
90
|
rmse,
|
|
92
|
-
test_metrics,
|
|
93
91
|
winkler_score,
|
|
94
92
|
)
|
|
95
93
|
|
|
@@ -101,7 +99,6 @@ _discover_metrics()
|
|
|
101
99
|
from chap_core.assessment.metrics.above_truth import RatioAboveTruthMetric
|
|
102
100
|
from chap_core.assessment.metrics.crps import CRPSLog1pMetric, CRPSMetric
|
|
103
101
|
from chap_core.assessment.metrics.crps_norm import CRPSNormMetric
|
|
104
|
-
from chap_core.assessment.metrics.example_metric import ExampleMetric
|
|
105
102
|
from chap_core.assessment.metrics.mae import MAEMetric
|
|
106
103
|
from chap_core.assessment.metrics.mape import MAPEMetric
|
|
107
104
|
from chap_core.assessment.metrics.outbreak_detection import (
|
|
@@ -116,7 +113,6 @@ from chap_core.assessment.metrics.percentile_coverage import (
|
|
|
116
113
|
PercentileCoverageMetric,
|
|
117
114
|
)
|
|
118
115
|
from chap_core.assessment.metrics.rmse import RMSEMetric
|
|
119
|
-
from chap_core.assessment.metrics.test_metrics import SampleCountMetric
|
|
120
116
|
from chap_core.assessment.metrics.winkler_score import (
|
|
121
117
|
WinklerScore10_90Log1pMetric,
|
|
122
118
|
WinklerScore10_90Metric,
|
|
@@ -139,7 +135,6 @@ __all__ = [
|
|
|
139
135
|
"Coverage25_75Metric",
|
|
140
136
|
"DataDimension",
|
|
141
137
|
"DeterministicMetric",
|
|
142
|
-
"ExampleMetric",
|
|
143
138
|
"MAEMetric",
|
|
144
139
|
"MAPEMetric",
|
|
145
140
|
"Metric",
|
|
@@ -151,7 +146,6 @@ __all__ = [
|
|
|
151
146
|
"ProbabilisticMetric",
|
|
152
147
|
"RMSEMetric",
|
|
153
148
|
"RatioAboveTruthMetric",
|
|
154
|
-
"SampleCountMetric",
|
|
155
149
|
"SensitivityMetric",
|
|
156
150
|
"SpecificityMetric",
|
|
157
151
|
"WinklerScore10_90Log1pMetric",
|
|
@@ -18,8 +18,8 @@ class MAPEMetric(DeterministicMetric):
|
|
|
18
18
|
"""
|
|
19
19
|
Mean Absolute Percentage Error metric.
|
|
20
20
|
|
|
21
|
-
Computes the
|
|
22
|
-
MEAN, this produces the MAPE (mean of absolute percentage
|
|
21
|
+
Computes the absolute percentage error at the detailed level. When aggregated
|
|
22
|
+
using MEAN, this produces the MAPE (mean of absolute percentage errors).
|
|
23
23
|
|
|
24
24
|
Usage:
|
|
25
25
|
mape = MAPEMetric()
|
|
@@ -32,12 +32,12 @@ class MAPEMetric(DeterministicMetric):
|
|
|
32
32
|
metric_id="mape",
|
|
33
33
|
metric_name="MAPE",
|
|
34
34
|
aggregation_op=AggregationOp.MEAN,
|
|
35
|
-
description="Mean Absolute Percentage Error -
|
|
35
|
+
description="Mean Absolute Percentage Error - average absolute error as percent of observed",
|
|
36
36
|
optimization_direction=OptimizationDirection.MINIMIZE,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
def compute_point_metric(self, forecast: float, observed: float) -> float:
|
|
40
|
-
"""Compute absolute error for a single forecast/observation pair."""
|
|
40
|
+
"""Compute absolute percentage error for a single forecast/observation pair."""
|
|
41
41
|
if observed == 0.0:
|
|
42
42
|
return np.nan
|
|
43
43
|
return abs(observed - forecast) / abs(observed) * 100.0
|
|
@@ -20,8 +20,9 @@ from chap_core.assessment.metrics.base import (
|
|
|
20
20
|
|
|
21
21
|
# The outbreak metrics score forecasts against seasonal thresholds, computed by the
|
|
22
22
|
# threshold strategy module.
|
|
23
|
-
from chap_core.assessment.thresholds.seasonal import
|
|
23
|
+
from chap_core.assessment.thresholds.seasonal import compute_seasonal_thresholds
|
|
24
24
|
from chap_core.time_period import TimePeriod
|
|
25
|
+
from chap_core.time_period.vectorized import extract_month
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def _get_thresholds(metric_instance: Metric) -> pd.DataFrame:
|
|
@@ -68,7 +69,7 @@ class SensitivityMetric(Metric):
|
|
|
68
69
|
return empty
|
|
69
70
|
|
|
70
71
|
obs = observations[["location", "time_period", "disease_cases"]].copy()
|
|
71
|
-
obs["month"] =
|
|
72
|
+
obs["month"] = extract_month(obs["time_period"])
|
|
72
73
|
obs = obs.merge(thresholds, on=["location", "month"], how="left")
|
|
73
74
|
|
|
74
75
|
# Keep only condition-positive rows (actual outbreaks)
|
|
@@ -78,7 +79,7 @@ class SensitivityMetric(Metric):
|
|
|
78
79
|
|
|
79
80
|
# Merge forecasts with thresholds
|
|
80
81
|
fc = forecasts.copy()
|
|
81
|
-
fc["month"] =
|
|
82
|
+
fc["month"] = extract_month(fc["time_period"])
|
|
82
83
|
fc = fc.merge(thresholds, on=["location", "month"], how="left")
|
|
83
84
|
|
|
84
85
|
# Compute alert per (location, time_period, horizon_distance)
|
|
@@ -121,7 +122,7 @@ class SpecificityMetric(Metric):
|
|
|
121
122
|
return empty
|
|
122
123
|
|
|
123
124
|
obs = observations[["location", "time_period", "disease_cases"]].copy()
|
|
124
|
-
obs["month"] =
|
|
125
|
+
obs["month"] = extract_month(obs["time_period"])
|
|
125
126
|
obs = obs.merge(thresholds, on=["location", "month"], how="left")
|
|
126
127
|
|
|
127
128
|
# Keep only condition-negative rows (no outbreak)
|
|
@@ -131,7 +132,7 @@ class SpecificityMetric(Metric):
|
|
|
131
132
|
|
|
132
133
|
# Merge forecasts with thresholds
|
|
133
134
|
fc = forecasts.copy()
|
|
134
|
-
fc["month"] =
|
|
135
|
+
fc["month"] = extract_month(fc["time_period"])
|
|
135
136
|
fc = fc.merge(thresholds, on=["location", "month"], how="left")
|
|
136
137
|
|
|
137
138
|
# Compute alert per (location, time_period, horizon_distance)
|
|
@@ -174,7 +175,7 @@ class OutbreakAccuracyMetric(Metric):
|
|
|
174
175
|
return empty
|
|
175
176
|
|
|
176
177
|
obs = observations[["location", "time_period", "disease_cases"]].copy()
|
|
177
|
-
obs["month"] =
|
|
178
|
+
obs["month"] = extract_month(obs["time_period"])
|
|
178
179
|
obs = obs.merge(thresholds, on=["location", "month"], how="left")
|
|
179
180
|
obs = obs.dropna(subset=["threshold"])
|
|
180
181
|
if obs.empty:
|
|
@@ -184,7 +185,7 @@ class OutbreakAccuracyMetric(Metric):
|
|
|
184
185
|
|
|
185
186
|
# Merge forecasts with thresholds
|
|
186
187
|
fc = forecasts.copy()
|
|
187
|
-
fc["month"] =
|
|
188
|
+
fc["month"] = extract_month(fc["time_period"])
|
|
188
189
|
fc = fc.merge(thresholds, on=["location", "month"], how="left")
|
|
189
190
|
|
|
190
191
|
# Compute alert per (location, time_period, horizon_distance)
|
|
@@ -2,28 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
Each strategy is a single class registered with the :func:`threshold` decorator,
|
|
4
4
|
mirroring the ``@backtest_plot`` registry. New strategies are added by writing a
|
|
5
|
-
class
|
|
5
|
+
class with a typed params model, importing its module from
|
|
6
|
+
:func:`_discover_strategies`, and adding the params model to
|
|
7
|
+
:data:`~chap_core.assessment.thresholds.params.ThresholdParams` — the endpoint
|
|
6
8
|
code never needs editing.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
10
12
|
|
|
13
|
+
from typing import TYPE_CHECKING, get_args
|
|
14
|
+
|
|
11
15
|
from chap_core.assessment.thresholds.base import ThresholdStrategyBase
|
|
12
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
13
20
|
# Global registry for threshold strategies
|
|
14
21
|
_threshold_strategies_registry: dict[str, type[ThresholdStrategyBase]] = {}
|
|
15
22
|
|
|
16
23
|
|
|
17
|
-
def
|
|
18
|
-
"""
|
|
24
|
+
def params_type_literal(params_model: type[BaseModel]) -> str:
|
|
25
|
+
"""The value of a params model's required ``type: Literal[...]`` discriminator field."""
|
|
26
|
+
(literal,) = get_args(params_model.model_fields["type"].annotation)
|
|
27
|
+
return str(literal)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def threshold(strategy_id: str, name: str, params_model: type[BaseModel], description: str = ""):
|
|
31
|
+
"""Decorator to register a threshold strategy class with its typed params model."""
|
|
19
32
|
|
|
20
33
|
def decorator(cls: type[ThresholdStrategyBase]) -> type[ThresholdStrategyBase]:
|
|
21
34
|
if not issubclass(cls, ThresholdStrategyBase):
|
|
22
35
|
raise TypeError(f"{cls.__name__} must inherit from ThresholdStrategyBase")
|
|
23
36
|
|
|
37
|
+
type_literal = params_type_literal(params_model)
|
|
38
|
+
if type_literal != strategy_id:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"{params_model.__name__}.type literal {type_literal!r} does not match "
|
|
41
|
+
f"registered strategy id {strategy_id!r}"
|
|
42
|
+
)
|
|
43
|
+
|
|
24
44
|
cls.id = strategy_id
|
|
25
45
|
cls.name = name
|
|
26
46
|
cls.description = description
|
|
47
|
+
cls.params_model = params_model
|
|
27
48
|
|
|
28
49
|
_threshold_strategies_registry[strategy_id] = cls
|
|
29
50
|
return cls
|
|
@@ -52,6 +73,7 @@ def list_threshold_strategies() -> list[dict]:
|
|
|
52
73
|
|
|
53
74
|
def _discover_strategies():
|
|
54
75
|
from chap_core.assessment.thresholds import (
|
|
76
|
+
percentile,
|
|
55
77
|
seasonal,
|
|
56
78
|
)
|
|
57
79
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Base class for threshold (endemic channel) calculation strategies.
|
|
2
|
+
|
|
3
|
+
A strategy turns a dataset's historical ``disease_cases`` observations into one
|
|
4
|
+
or more threshold lines per requested ``(period_id, org_unit)``. Subclasses
|
|
5
|
+
implement :meth:`compute`; registration happens via the :func:`threshold`
|
|
6
|
+
decorator, which also binds the strategy's typed params model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import pandas as pd
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ThresholdStrategyBase[ParamsT: BaseModel](ABC):
|
|
20
|
+
"""Base class for threshold strategies.
|
|
21
|
+
|
|
22
|
+
Subclasses implement :meth:`compute`, which receives a flat DataFrame of
|
|
23
|
+
historical observations, the periods to produce thresholds for, and the
|
|
24
|
+
strategy's validated params model, and returns one row per
|
|
25
|
+
``(period_id, location, line)``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
id: str = ""
|
|
29
|
+
name: str = ""
|
|
30
|
+
description: str = ""
|
|
31
|
+
params_model: type[ParamsT]
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def compute(
|
|
35
|
+
self,
|
|
36
|
+
historical_observations: pd.DataFrame,
|
|
37
|
+
period_ids: list[str],
|
|
38
|
+
params: ParamsT,
|
|
39
|
+
) -> pd.DataFrame:
|
|
40
|
+
"""Compute thresholds for the requested periods.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
historical_observations: DataFrame with columns
|
|
44
|
+
``[location, time_period, disease_cases]``.
|
|
45
|
+
period_ids: Periods to produce thresholds for (e.g. ``["2024-01"]``).
|
|
46
|
+
params: Validated instance of :attr:`params_model`.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
DataFrame with columns ``[period_id, location, line, threshold]`` —
|
|
50
|
+
one row per ``(period_id, location, line)``, where ``line`` is the
|
|
51
|
+
zero-based index into ``params.lines``. Combinations the strategy
|
|
52
|
+
cannot compute may be omitted; the endpoint fills them with ``null``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def align_seasonal_to_periods(per_season: pd.DataFrame, period_ids: list[str]) -> pd.DataFrame:
|
|
57
|
+
"""Merge per-season threshold lines onto the requested periods.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
per_season: DataFrame with columns ``[location, month|week, line, threshold]``.
|
|
61
|
+
period_ids: Periods to produce thresholds for.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
DataFrame with columns ``[period_id, location, line, threshold]``.
|
|
65
|
+
"""
|
|
66
|
+
import pandas as pd
|
|
67
|
+
|
|
68
|
+
from chap_core.time_period.vectorized import season_column
|
|
69
|
+
|
|
70
|
+
requested = pd.DataFrame({"period_id": period_ids})
|
|
71
|
+
season, buckets = season_column(requested["period_id"])
|
|
72
|
+
if season not in per_season.columns:
|
|
73
|
+
raise ValueError(f"period_ids are {season}-based but the dataset's time periods have a different frequency")
|
|
74
|
+
requested[season] = buckets
|
|
75
|
+
merged = requested.merge(per_season, on=season)
|
|
76
|
+
return merged[["period_id", "location", "line", "threshold"]]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Typed per-strategy parameter models for the thresholds API.
|
|
2
|
+
|
|
3
|
+
Each strategy declares a pydantic model whose required ``type`` literal is the
|
|
4
|
+
discriminator selecting the strategy, so the OpenAPI schema renders
|
|
5
|
+
:data:`ThresholdParams` as a tagged union that generated clients can build a
|
|
6
|
+
strategy picker and per-strategy params form from. ``type`` has no default on
|
|
7
|
+
purpose: a default would make it optional in the schema, and an optional
|
|
8
|
+
discriminator cannot tell the union members apart in generated code. The
|
|
9
|
+
:func:`~chap_core.assessment.thresholds.threshold` registry decorator asserts
|
|
10
|
+
each strategy's model literal matches its registered id.
|
|
11
|
+
|
|
12
|
+
Line parameters (``std_multiplier``, ``quantile``) accept a scalar or a list;
|
|
13
|
+
each list entry produces one threshold line in the response, computed over the
|
|
14
|
+
same historical window. Every model exposes the normalized list as
|
|
15
|
+
:attr:`ThresholdParamsBase.lines`, which the strategies compute from and the
|
|
16
|
+
endpoint echoes so clients can label lines without knowing the strategy.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Annotated, Literal
|
|
22
|
+
|
|
23
|
+
from pydantic import Field
|
|
24
|
+
|
|
25
|
+
from chap_core.database.base_tables import DBModel
|
|
26
|
+
|
|
27
|
+
Fraction = Annotated[float, Field(ge=0.0, le=1.0)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ThresholdParamsBase(DBModel):
|
|
31
|
+
"""Common interface of the per-strategy params models."""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def lines(self) -> list[float]:
|
|
35
|
+
"""The line parameter as a list, one value per threshold line, in request order."""
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SeasonalParams(ThresholdParamsBase):
|
|
40
|
+
"""Parameters for the seasonal mean + k*std strategy."""
|
|
41
|
+
|
|
42
|
+
type: Literal["seasonal"]
|
|
43
|
+
std_multiplier: float | Annotated[list[float], Field(min_length=1)] = Field(
|
|
44
|
+
2.0,
|
|
45
|
+
description="Number of standard deviations above the seasonal mean. "
|
|
46
|
+
"A list produces one threshold line per entry.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def lines(self) -> list[float]:
|
|
51
|
+
return line_values(self.std_multiplier)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PercentileParams(ThresholdParamsBase):
|
|
55
|
+
"""Parameters for the seasonal percentile (WHO endemic channel) strategy."""
|
|
56
|
+
|
|
57
|
+
type: Literal["percentile"]
|
|
58
|
+
quantile: Fraction | Annotated[list[Fraction], Field(min_length=1)] = Field(
|
|
59
|
+
0.75,
|
|
60
|
+
description="Percentile of historical same-season values, as a fraction in [0, 1]. "
|
|
61
|
+
"A list produces one threshold line per entry, e.g. `[0.25, 0.75]` for the endemic channel band.",
|
|
62
|
+
)
|
|
63
|
+
baseline_years: int | None = Field(
|
|
64
|
+
5,
|
|
65
|
+
ge=1,
|
|
66
|
+
description="Number of the most recent complete years in the dataset to compute the baseline from. "
|
|
67
|
+
"A partial final year is excluded. `null` uses all available history.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def lines(self) -> list[float]:
|
|
72
|
+
return line_values(self.quantile)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
ThresholdParams = Annotated[SeasonalParams | PercentileParams, Field(discriminator="type")]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def line_values(scalar_or_list: float | list[float]) -> list[float]:
|
|
79
|
+
"""Normalize a line parameter to the list of per-line values."""
|
|
80
|
+
if isinstance(scalar_or_list, list):
|
|
81
|
+
return scalar_or_list
|
|
82
|
+
return [scalar_or_list]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Percentile threshold strategy: WHO-style endemic channel from historical same-season values.
|
|
2
|
+
|
|
3
|
+
Malaria surveillance programmes plot channel graphs from a percentile of the previous few
|
|
4
|
+
complete years, reading a value above the upper line as an outbreak. A percentile is used
|
|
5
|
+
rather than mean + k*std because case counts are heavily right-skewed and a single past
|
|
6
|
+
epidemic year inflates the standard deviation far above any plausible endemic level.
|
|
7
|
+
|
|
8
|
+
The channel is static: one dataset defines one threshold per (location, season), computed
|
|
9
|
+
from the most recent complete years in the dataset, and the requested periods only select
|
|
10
|
+
which rows come back. Past and future periods therefore share one line. A consequence is
|
|
11
|
+
that a past period is scored against a baseline that includes it, so a historical epidemic
|
|
12
|
+
partly raises the line it is plotted against. That is inherent to the endemic-channel idiom.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from chap_core.assessment.thresholds import threshold
|
|
20
|
+
from chap_core.assessment.thresholds.base import ThresholdStrategyBase, align_seasonal_to_periods
|
|
21
|
+
from chap_core.assessment.thresholds.params import PercentileParams
|
|
22
|
+
from chap_core.time_period.vectorized import extract_year, season_column
|
|
23
|
+
|
|
24
|
+
_FULL_YEAR_BUCKETS = {"month": 12, "week": 52}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def last_complete_year(historical_observations: pd.DataFrame) -> int:
|
|
28
|
+
"""Return the latest year in the data, or the year before it when the latest year is partial.
|
|
29
|
+
|
|
30
|
+
A year is complete when it has every season bucket (12 months or at least 52 weeks).
|
|
31
|
+
A live dataset's final year is usually in progress; including it would give the
|
|
32
|
+
early-season buckets one more observation than the late-season ones, and would let an
|
|
33
|
+
outbreak in progress raise the threshold it is about to be compared against.
|
|
34
|
+
"""
|
|
35
|
+
years = extract_year(historical_observations["time_period"])
|
|
36
|
+
season, buckets = season_column(historical_observations["time_period"])
|
|
37
|
+
latest = int(years.max())
|
|
38
|
+
latest_buckets = pd.Index(buckets)[years == latest].nunique()
|
|
39
|
+
if latest_buckets < _FULL_YEAR_BUCKETS[season]:
|
|
40
|
+
return latest - 1
|
|
41
|
+
return latest
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def filter_to_baseline(historical_observations: pd.DataFrame, baseline_years: int | None) -> pd.DataFrame:
|
|
45
|
+
"""Restrict observations to the most recent ``baseline_years`` complete years in the dataset.
|
|
46
|
+
|
|
47
|
+
The window is anchored on the dataset, not on the requested periods, so every requested
|
|
48
|
+
period, past or future, is compared against the same line. A partial final year is
|
|
49
|
+
excluded (see :func:`last_complete_year`). Pass ``None`` to use all available history.
|
|
50
|
+
"""
|
|
51
|
+
if baseline_years is None:
|
|
52
|
+
return historical_observations
|
|
53
|
+
|
|
54
|
+
years = extract_year(historical_observations["time_period"])
|
|
55
|
+
last = last_complete_year(historical_observations)
|
|
56
|
+
first = last - baseline_years + 1
|
|
57
|
+
windowed = historical_observations[(years >= first) & (years <= last)]
|
|
58
|
+
|
|
59
|
+
if windowed.empty:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"No complete years in the {baseline_years}-year baseline window {first}-{last}; "
|
|
62
|
+
f"the data covers {int(years.min())}-{int(years.max())}"
|
|
63
|
+
)
|
|
64
|
+
return windowed
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compute_percentile_thresholds(historical_observations: pd.DataFrame, quantiles: list[float]) -> pd.DataFrame:
|
|
68
|
+
"""Compute outbreak threshold lines as per-season percentiles of historical observations.
|
|
69
|
+
|
|
70
|
+
All quantiles are computed from one groupby pass over the same observations.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
historical_observations: DataFrame with columns [location, time_period, disease_cases]
|
|
74
|
+
quantiles: Percentiles to take, as fractions (e.g. ``[0.25, 0.75]``).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
DataFrame with columns [location, month|week, line, threshold], where ``line``
|
|
78
|
+
is the index into ``quantiles``.
|
|
79
|
+
"""
|
|
80
|
+
df = historical_observations.copy()
|
|
81
|
+
season, buckets = season_column(df["time_period"])
|
|
82
|
+
df[season] = buckets
|
|
83
|
+
unique_quantiles = list(dict.fromkeys(quantiles))
|
|
84
|
+
per_quantile = df.groupby(["location", season])["disease_cases"].quantile(pd.Series(unique_quantiles)).unstack()
|
|
85
|
+
lines = []
|
|
86
|
+
for i, quantile in enumerate(quantiles):
|
|
87
|
+
line = per_quantile[quantile].rename("threshold").reset_index()
|
|
88
|
+
line["line"] = i
|
|
89
|
+
lines.append(line)
|
|
90
|
+
return pd.concat(lines, ignore_index=True)[["location", season, "line", "threshold"]]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@threshold(
|
|
94
|
+
"percentile",
|
|
95
|
+
"Seasonal percentile (WHO)",
|
|
96
|
+
PercentileParams,
|
|
97
|
+
"Outbreak threshold as a percentile of historical same-month (or same-week) values, over a "
|
|
98
|
+
"baseline window of the most recent complete years in the dataset. Defaults to the 75th "
|
|
99
|
+
"percentile over 5 years, the WHO "
|
|
100
|
+
"malaria channel practice. Robust to past epidemic years, which inflate mean + k*std.",
|
|
101
|
+
)
|
|
102
|
+
class PercentileThresholdStrategy(ThresholdStrategyBase[PercentileParams]):
|
|
103
|
+
"""Registered strategy computing one line per requested quantile from a shared baseline window."""
|
|
104
|
+
|
|
105
|
+
def compute(
|
|
106
|
+
self,
|
|
107
|
+
historical_observations: pd.DataFrame,
|
|
108
|
+
period_ids: list[str],
|
|
109
|
+
params: PercentileParams,
|
|
110
|
+
) -> pd.DataFrame:
|
|
111
|
+
windowed = filter_to_baseline(historical_observations, params.baseline_years)
|
|
112
|
+
per_season = compute_percentile_thresholds(windowed, params.lines)
|
|
113
|
+
return align_seasonal_to_periods(per_season, period_ids)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Seasonal threshold strategy: mean + k*std over historical same-season (month or week) values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from chap_core.assessment.thresholds import threshold
|
|
8
|
+
from chap_core.assessment.thresholds.base import ThresholdStrategyBase, align_seasonal_to_periods
|
|
9
|
+
from chap_core.assessment.thresholds.params import SeasonalParams
|
|
10
|
+
from chap_core.time_period.vectorized import season_column
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def seasonal_stats(historical_observations: pd.DataFrame) -> tuple[str, pd.DataFrame]:
|
|
14
|
+
"""Per-(location, season) mean and std of disease_cases.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The season column name ("month" or "week") and a DataFrame with columns
|
|
18
|
+
``[location, month|week, mean, std]``.
|
|
19
|
+
"""
|
|
20
|
+
df = historical_observations.copy()
|
|
21
|
+
season, buckets = season_column(df["time_period"])
|
|
22
|
+
df[season] = buckets
|
|
23
|
+
grouped = df.groupby(["location", season])["disease_cases"].agg(["mean", "std"]).reset_index()
|
|
24
|
+
return season, grouped
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compute_seasonal_thresholds(historical_observations: pd.DataFrame, k: float = 2.0) -> pd.DataFrame:
|
|
28
|
+
"""Compute outbreak thresholds from historical observations.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
historical_observations: DataFrame with columns [location, time_period, disease_cases]
|
|
32
|
+
k: Number of standard deviations above the mean (default 2.0).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
DataFrame with columns [location, month, threshold] for monthly data,
|
|
36
|
+
or [location, week, threshold] for weekly data.
|
|
37
|
+
"""
|
|
38
|
+
season, grouped = seasonal_stats(historical_observations)
|
|
39
|
+
grouped["threshold"] = grouped["mean"] + k * grouped["std"]
|
|
40
|
+
return grouped[["location", season, "threshold"]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@threshold(
|
|
44
|
+
"seasonal",
|
|
45
|
+
"Seasonal mean + k*std",
|
|
46
|
+
SeasonalParams,
|
|
47
|
+
"Outbreak threshold as mean + k standard deviations of historical same-month (or same-week) values.",
|
|
48
|
+
)
|
|
49
|
+
class SeasonalThresholdStrategy(ThresholdStrategyBase[SeasonalParams]):
|
|
50
|
+
"""Registered strategy computing one line per requested std multiplier from shared seasonal stats."""
|
|
51
|
+
|
|
52
|
+
def compute(
|
|
53
|
+
self,
|
|
54
|
+
historical_observations: pd.DataFrame,
|
|
55
|
+
period_ids: list[str],
|
|
56
|
+
params: SeasonalParams,
|
|
57
|
+
) -> pd.DataFrame:
|
|
58
|
+
season, stats = seasonal_stats(historical_observations)
|
|
59
|
+
lines = []
|
|
60
|
+
for i, multiplier in enumerate(params.lines):
|
|
61
|
+
line = stats[["location", season]].copy()
|
|
62
|
+
line["line"] = i
|
|
63
|
+
line["threshold"] = stats["mean"] + multiplier * stats["std"]
|
|
64
|
+
lines.append(line)
|
|
65
|
+
per_season = pd.concat(lines, ignore_index=True)
|
|
66
|
+
return align_seasonal_to_periods(per_season, period_ids)
|
|
@@ -325,7 +325,6 @@ def _build_results_summary(backtest: Backtest) -> str:
|
|
|
325
325
|
f"MAE (aggregate): {metrics.get('mae')}\n",
|
|
326
326
|
f"Coverage within 10-90%: {metrics.get('coverage_10_90')}\n",
|
|
327
327
|
f"Coverage within 25-75%: {metrics.get('coverage_25_75')}\n",
|
|
328
|
-
f"Sample count: {metrics.get('sample_count')}\n",
|
|
329
328
|
]
|
|
330
329
|
)
|
|
331
330
|
|