chap-core 2.2.0__tar.gz → 2.3.1__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.1}/PKG-INFO +2 -2
- chap_core-2.3.1/chap_core/assessment/causal_plot.py +246 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/evaluation.py +44 -16
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/__init__.py +0 -6
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/mape.py +4 -4
- chap_core-2.3.1/chap_core/assessment/metrics/outbreak_detection.py +201 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/thresholds/__init__.py +25 -3
- chap_core-2.3.1/chap_core/assessment/thresholds/base.py +76 -0
- chap_core-2.3.1/chap_core/assessment/thresholds/params.py +82 -0
- chap_core-2.3.1/chap_core/assessment/thresholds/percentile.py +113 -0
- chap_core-2.3.1/chap_core/assessment/thresholds/seasonal.py +66 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/_common.py +48 -2
- chap_core-2.3.1/chap_core/cli_endpoints/build_counterfactual.py +393 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/causal.py +120 -14
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/evaluate.py +5 -1
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/generate_modelcard.py +0 -1
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/preference_learn.py +1 -1
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/database.py +41 -4
- chap_core-2.3.1/chap_core/database/migration_helpers.py +35 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/model_template_seed.py +6 -4
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/tables.py +82 -6
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/wrappers.py +1 -1
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/log_config.py +5 -3
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/chapkit_rest_api_wrapper.py +57 -3
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/external_chapkit_model.py +103 -43
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/celery_tasks.py +36 -23
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/data_models.py +6 -2
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/db_worker_functions.py +19 -3
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/services/schemas.py +15 -2
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/jobs.py +3 -2
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/analytics.py +65 -21
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/crud.py +6 -1
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/visualization.py +10 -1
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/services/dataset_validation.py +15 -6
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/simulation/naive_simulator.py +10 -4
- chap_core-2.3.1/chap_core/time_period/vectorized.py +54 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/pyproject.toml +2 -2
- chap_core-2.2.0/chap_core/alarms.py +0 -16
- chap_core-2.2.0/chap_core/assessment/causal_plot.py +0 -52
- chap_core-2.2.0/chap_core/assessment/metrics/example_metric.py +0 -38
- chap_core-2.2.0/chap_core/assessment/metrics/outbreak_detection.py +0 -204
- 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/cli_endpoints/build_counterfactual.py +0 -187
- {chap_core-2.2.0 → chap_core-2.3.1}/README.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/command_line_interface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/gluonts.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/rest_api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/api_types.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/db_dimensions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/evaluation_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/horizon_location_grid.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/predicted_vs_actual_linear_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/predicted_vs_actual_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/sample_bias_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/data_representation_transforming.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/dataset_splitting.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/eval_tracking.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/evaluator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/evaluator_suites.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/flat_representations.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/forecast.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/horizon_location_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/horizon_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/horizon_sum.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/metric_map.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/regional_distribution.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/time_period_location_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/time_period_mean.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/time_period_sum.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_table.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/above_truth.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/base.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/crps.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/crps_norm.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/mae.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/peak_diff.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/percentile_coverage.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/rmse.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/winkler_score.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/prediction_evaluator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/representations.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/base.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/climatology.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/damped_persistence.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/mstl_arima.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/observed.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/_args.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/aggregate_eval.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/convert.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/ensemble.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/explain.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/forecast.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/generated_plot_ids.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/report.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/utils.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/validate.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/climate_data/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/climate_data/seasonal_forecasts.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/climate_predictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/adaptors.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/datasets.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/gluonts_adaptor/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/gluonts_adaptor/dataset.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/open_dengue.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/base_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/dataset_manager.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/dataset_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/datasets_seed.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/feature_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/manager.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/model_spec_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/model_templates_and_config_tables.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/datatypes.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/docker_helper_functions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/_meta_models.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/_predictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/_sample_extractor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/ensemble_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/exceptions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/distance.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/documentation.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/lime.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/perturb.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/segment.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/surrogate.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/testing/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/testing/metrics.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/userguide.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/ExtendedPredictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/external_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/github.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/mlflow_wrappers.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/model_configuration.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/python_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/r_description.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/r_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/r_models.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/uv_wrapper.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/feature_generators/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/feature_generators/seasonality_cluster.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/fetch/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/cleaners.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/example_data_set.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/external_file.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/file_paths.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/load.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geo_coding/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geojson.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geometry.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geoutils.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/README.md +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/base.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/config1.yaml +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/config2.yaml +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/config3.yaml +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/hpoModel.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/hpoModelInterface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/objective.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/searcher.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/types.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/model_spec.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/chapkit_service_manager.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/configured_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/external_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/external_web_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/local_configuration.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/model_template.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/model_template_interface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/utils.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/pandas_adaptors.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/backtest_dashboard_spec.json +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/backtest_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/conftest.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/dataset_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/evaluation_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/prediction_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/produce_plots_from_yaml.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/season_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/yaml_plot.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/feature_spec.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/model_registry.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/naive_estimator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/naive_predictor.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/poisson.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/protocol.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/preference_learning/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/preference_learning/decision_maker.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/preference_learning/preference_learner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rbased_docker.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/app.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/auth.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/common_routes.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/experimental.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/services/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/services/orchestrator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/rest_api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/dependencies.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/dependencies.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/rest_api.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/routers/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/routers/proxy.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/routers/services.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/worker_functions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/command_line_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/conda_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/docker_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/helper_functions.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/mlflow_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/renv_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/uv_runner.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/services/prediction_setup_service.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/simulation/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/simulation/simulator.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/converters.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/multi_country_dataset.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/temporal_dataclass.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/estimators.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/external_model.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/mocks.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/testing.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/date_util_wrapper.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/multi_resolution.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/period_assignment.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/relationships.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/transformations/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/transformations/covid_mask.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/util.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/validators.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/worker/__init__.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/worker/interface.py +0 -0
- {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/worker/rq_worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: chap_core
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Climate Health Analysis Platform (CHAP)
|
|
5
5
|
Keywords: chap_core
|
|
6
6
|
Author: Chap Team
|
|
@@ -52,7 +52,7 @@ Requires-Dist: gunicorn>=25.3.0
|
|
|
52
52
|
Requires-Dist: optuna>=4.9.0
|
|
53
53
|
Requires-Dist: httpx>=0.28.1
|
|
54
54
|
Requires-Dist: alembic>=1.18.4
|
|
55
|
-
Requires-Dist: chapkit>=1.0
|
|
55
|
+
Requires-Dist: chapkit>=2.1.0,<3
|
|
56
56
|
Requires-Dist: cryptography>=46.0.7
|
|
57
57
|
Requires-Dist: croniter>=6.2.2
|
|
58
58
|
Requires-Dist: stumpy>=1.14.1
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Side-by-side and overlaid comparison plots for causal counterfactual analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import altair as alt
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from chap_core.assessment.backtest_plots.evaluation_plot import EvaluationPlot
|
|
12
|
+
from chap_core.plotting.backtest_plot import clean_time
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from chap_core.assessment.backtest_plots import ChartType
|
|
16
|
+
|
|
17
|
+
_QUANTILES = (0.1, 0.25, 0.5, 0.75, 0.9)
|
|
18
|
+
_ORIGINAL_LABEL = "Original"
|
|
19
|
+
_COUNTERFACTUAL_LABEL = "Counterfactual"
|
|
20
|
+
_OBSERVED_LABEL = "Observed (pre-split)"
|
|
21
|
+
_DEFAULT_X_LABEL = "Time period"
|
|
22
|
+
_DEFAULT_Y_LABEL = "Disease cases"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _mark_type(layer) -> str | None:
|
|
26
|
+
mark = getattr(layer, "mark", None)
|
|
27
|
+
return getattr(mark, "type", mark)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _set_axis_titles(chart: ChartType, x_label: str, y_label: str) -> None:
|
|
31
|
+
"""Set axis titles on every layer of a layered chart.
|
|
32
|
+
|
|
33
|
+
Vega-Lite ignores `title` in axis config, so `configure_axisX` / `configure_axisY`
|
|
34
|
+
cannot be used to label the axes. Every layer needs the same explicit title, or
|
|
35
|
+
Vega-Lite merges the per-layer field names into titles like "q_10, q_25".
|
|
36
|
+
"""
|
|
37
|
+
for layer in chart.layer:
|
|
38
|
+
encoding = getattr(layer, "encoding", None)
|
|
39
|
+
for channel, label in (("x", x_label), ("y", y_label)):
|
|
40
|
+
channel_def = getattr(encoding, channel, None)
|
|
41
|
+
if channel_def is not None and channel_def is not alt.Undefined:
|
|
42
|
+
channel_def.title = label
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _chart_for_location(
|
|
46
|
+
flat_evaluation,
|
|
47
|
+
location: str,
|
|
48
|
+
title: str,
|
|
49
|
+
*,
|
|
50
|
+
x_label: str,
|
|
51
|
+
y_label: str,
|
|
52
|
+
show_confidence_intervals: bool = True,
|
|
53
|
+
) -> ChartType:
|
|
54
|
+
obs = pd.DataFrame(flat_evaluation.observations)
|
|
55
|
+
forecasts = pd.DataFrame(flat_evaluation.forecasts)
|
|
56
|
+
historical = None
|
|
57
|
+
if flat_evaluation.historical_observations is not None:
|
|
58
|
+
hist = pd.DataFrame(flat_evaluation.historical_observations)
|
|
59
|
+
loc_hist = hist[hist["location"] == location]
|
|
60
|
+
historical = loc_hist if not loc_hist.empty else None
|
|
61
|
+
chart = (
|
|
62
|
+
EvaluationPlot()
|
|
63
|
+
.plot(
|
|
64
|
+
obs[obs["location"] == location],
|
|
65
|
+
forecasts[forecasts["location"] == location],
|
|
66
|
+
historical,
|
|
67
|
+
)
|
|
68
|
+
.properties(title=title)
|
|
69
|
+
)
|
|
70
|
+
if not show_confidence_intervals:
|
|
71
|
+
chart.layer = [layer for layer in chart.layer if _mark_type(layer) != "errorband"]
|
|
72
|
+
_set_axis_titles(chart, x_label, y_label)
|
|
73
|
+
return chart
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def plot_counterfactual(
|
|
77
|
+
eval_original,
|
|
78
|
+
eval_cf,
|
|
79
|
+
counterfactual_columns: list[str] | None = None,
|
|
80
|
+
*,
|
|
81
|
+
title: str | None = None,
|
|
82
|
+
x_label: str | None = None,
|
|
83
|
+
y_label: str | None = None,
|
|
84
|
+
original_label: str = _ORIGINAL_LABEL,
|
|
85
|
+
counterfactual_label: str = _COUNTERFACTUAL_LABEL,
|
|
86
|
+
show_confidence_intervals: bool = True,
|
|
87
|
+
) -> ChartType:
|
|
88
|
+
"""Return a per-location vconcat of side-by-side Altair charts comparing original vs counterfactual."""
|
|
89
|
+
x_label = x_label if x_label is not None else _DEFAULT_X_LABEL
|
|
90
|
+
y_label = y_label if y_label is not None else _DEFAULT_Y_LABEL
|
|
91
|
+
locations = sorted(pd.DataFrame(eval_original.to_flat().observations)["location"].unique())
|
|
92
|
+
flat_evaluation = eval_original.to_flat()
|
|
93
|
+
flat_evaluation_cf = eval_cf.to_flat()
|
|
94
|
+
|
|
95
|
+
rows = []
|
|
96
|
+
for loc in locations:
|
|
97
|
+
orig_chart = _chart_for_location(
|
|
98
|
+
flat_evaluation,
|
|
99
|
+
loc,
|
|
100
|
+
original_label,
|
|
101
|
+
x_label=x_label,
|
|
102
|
+
y_label=y_label,
|
|
103
|
+
show_confidence_intervals=show_confidence_intervals,
|
|
104
|
+
)
|
|
105
|
+
cf_chart = _chart_for_location(
|
|
106
|
+
flat_evaluation_cf,
|
|
107
|
+
loc,
|
|
108
|
+
counterfactual_label,
|
|
109
|
+
x_label=x_label,
|
|
110
|
+
y_label=y_label,
|
|
111
|
+
show_confidence_intervals=show_confidence_intervals,
|
|
112
|
+
)
|
|
113
|
+
rows.append(alt.hconcat(orig_chart, cf_chart).resolve_scale(y="shared"))
|
|
114
|
+
|
|
115
|
+
return alt.vconcat(*rows).properties(
|
|
116
|
+
title=_compose_title(title, counterfactual_columns, original_label, counterfactual_label)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _compose_title(
|
|
121
|
+
title: str | None,
|
|
122
|
+
counterfactual_columns: list[str] | None,
|
|
123
|
+
original_label: str,
|
|
124
|
+
counterfactual_label: str,
|
|
125
|
+
) -> str:
|
|
126
|
+
if title is not None:
|
|
127
|
+
return title
|
|
128
|
+
subtitle = f" ({', '.join(counterfactual_columns)})" if counterfactual_columns else ""
|
|
129
|
+
return f"Causal Analysis: {original_label} vs {counterfactual_label}{subtitle}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _location_quantiles(forecasts: pd.DataFrame, location: str, series: str) -> pd.DataFrame:
|
|
133
|
+
"""Per-time-period forecast quantiles for one location, tagged with a series label."""
|
|
134
|
+
subset = forecasts[forecasts["location"] == location]
|
|
135
|
+
grouped = subset.groupby("time_period")["forecast"].quantile(np.array(_QUANTILES)).unstack()
|
|
136
|
+
grouped.columns = [f"q_{int(q * 100)}" for q in _QUANTILES]
|
|
137
|
+
grouped = grouped.reset_index()
|
|
138
|
+
grouped["time_period"] = grouped["time_period"].astype(str).apply(clean_time)
|
|
139
|
+
grouped["series"] = series
|
|
140
|
+
return grouped
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _location_observed(flat_evaluation, location: str) -> pd.DataFrame | None:
|
|
144
|
+
"""Observed disease_cases for one location, restricted to the pre-split historical window."""
|
|
145
|
+
if flat_evaluation.historical_observations is None:
|
|
146
|
+
return None
|
|
147
|
+
df = pd.DataFrame(flat_evaluation.historical_observations)
|
|
148
|
+
df = df[df["location"] == location]
|
|
149
|
+
if df.empty:
|
|
150
|
+
return None
|
|
151
|
+
df = df.copy()
|
|
152
|
+
df["time_period"] = df["time_period"].astype(str).apply(clean_time)
|
|
153
|
+
df["series"] = _OBSERVED_LABEL
|
|
154
|
+
return df
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _x(x_label: str) -> alt.X:
|
|
158
|
+
return alt.X("time_period:T", title=x_label)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _y(field: str, y_label: str) -> alt.Y:
|
|
162
|
+
return alt.Y(f"{field}:Q", scale=alt.Scale(zero=False), title=y_label)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _color(color_scale: alt.Scale) -> alt.Color:
|
|
166
|
+
return alt.Color("series:N", scale=color_scale, legend=alt.Legend(title=None))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _overlay_for_location(
|
|
170
|
+
quantiles: pd.DataFrame,
|
|
171
|
+
observed: pd.DataFrame | None,
|
|
172
|
+
location: str,
|
|
173
|
+
x_label: str,
|
|
174
|
+
y_label: str,
|
|
175
|
+
color_scale: alt.Scale,
|
|
176
|
+
show_confidence_intervals: bool = True,
|
|
177
|
+
) -> ChartType:
|
|
178
|
+
median_line = (
|
|
179
|
+
alt.Chart(quantiles).mark_line().encode(x=_x(x_label), y=_y("q_50", y_label), color=_color(color_scale))
|
|
180
|
+
)
|
|
181
|
+
layers = []
|
|
182
|
+
if show_confidence_intervals:
|
|
183
|
+
layers.append(
|
|
184
|
+
alt.Chart(quantiles)
|
|
185
|
+
.mark_errorband(opacity=0.15)
|
|
186
|
+
.encode(x=_x(x_label), y=_y("q_10", y_label), y2="q_90:Q", color=_color(color_scale))
|
|
187
|
+
)
|
|
188
|
+
layers.append(
|
|
189
|
+
alt.Chart(quantiles)
|
|
190
|
+
.mark_errorband(opacity=0.3)
|
|
191
|
+
.encode(x=_x(x_label), y=_y("q_25", y_label), y2="q_75:Q", color=_color(color_scale))
|
|
192
|
+
)
|
|
193
|
+
layers.append(median_line)
|
|
194
|
+
if observed is not None and not observed.empty:
|
|
195
|
+
layers.append(
|
|
196
|
+
alt.Chart(observed)
|
|
197
|
+
.mark_line(strokeDash=[4, 2])
|
|
198
|
+
.encode(x=_x(x_label), y=_y("disease_cases", y_label), color=_color(color_scale))
|
|
199
|
+
)
|
|
200
|
+
return alt.layer(*layers).properties(title=location)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def plot_counterfactual_overlayed(
|
|
204
|
+
eval_original,
|
|
205
|
+
eval_cf,
|
|
206
|
+
counterfactual_columns: list[str] | None = None,
|
|
207
|
+
*,
|
|
208
|
+
title: str | None = None,
|
|
209
|
+
x_label: str | None = None,
|
|
210
|
+
y_label: str | None = None,
|
|
211
|
+
original_label: str = _ORIGINAL_LABEL,
|
|
212
|
+
counterfactual_label: str = _COUNTERFACTUAL_LABEL,
|
|
213
|
+
show_confidence_intervals: bool = True,
|
|
214
|
+
) -> ChartType:
|
|
215
|
+
"""Return a per-location vconcat of single overlaid charts: original vs counterfactual forecasts
|
|
216
|
+
on shared axes, with the observed line drawn only up to the split point."""
|
|
217
|
+
x_label = x_label if x_label is not None else _DEFAULT_X_LABEL
|
|
218
|
+
y_label = y_label if y_label is not None else _DEFAULT_Y_LABEL
|
|
219
|
+
flat_original = eval_original.to_flat()
|
|
220
|
+
flat_cf = eval_cf.to_flat()
|
|
221
|
+
original_forecasts = pd.DataFrame(flat_original.forecasts)
|
|
222
|
+
cf_forecasts = pd.DataFrame(flat_cf.forecasts)
|
|
223
|
+
locations = sorted(original_forecasts["location"].unique())
|
|
224
|
+
|
|
225
|
+
color_scale = alt.Scale(
|
|
226
|
+
domain=[original_label, counterfactual_label, _OBSERVED_LABEL],
|
|
227
|
+
range=["#1f77b4", "#d62728", "#333333"],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
rows = []
|
|
231
|
+
for loc in locations:
|
|
232
|
+
quantiles = pd.concat(
|
|
233
|
+
[
|
|
234
|
+
_location_quantiles(original_forecasts, loc, original_label),
|
|
235
|
+
_location_quantiles(cf_forecasts, loc, counterfactual_label),
|
|
236
|
+
],
|
|
237
|
+
ignore_index=True,
|
|
238
|
+
)
|
|
239
|
+
observed = _location_observed(flat_original, loc)
|
|
240
|
+
rows.append(
|
|
241
|
+
_overlay_for_location(quantiles, observed, loc, x_label, y_label, color_scale, show_confidence_intervals)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return alt.vconcat(*rows).properties(
|
|
245
|
+
title=_compose_title(title, counterfactual_columns, original_label, counterfactual_label)
|
|
246
|
+
)
|
|
@@ -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
|
|
@@ -49,6 +46,15 @@ except ImportError:
|
|
|
49
46
|
CHAP_VERSION = "unknown"
|
|
50
47
|
|
|
51
48
|
|
|
49
|
+
def _recorded_version(version: str | None) -> str | None:
|
|
50
|
+
"""The version to persist: a real release version, or None for the "unknown" a dev checkout reports."""
|
|
51
|
+
return None if version in (None, "unknown") else version
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Version stored on every backtest this process produces.
|
|
55
|
+
RECORDED_CHAP_VERSION = _recorded_version(CHAP_VERSION)
|
|
56
|
+
|
|
57
|
+
|
|
52
58
|
def _flat_data_to_xarray(flat_data: "FlatEvaluationData", model_metadata: dict) -> xr.Dataset:
|
|
53
59
|
"""
|
|
54
60
|
Convert FlatEvaluationData to xarray.Dataset.
|
|
@@ -284,8 +290,8 @@ class Evaluation(EvaluationBase):
|
|
|
284
290
|
def future_weather_provider(self) -> str:
|
|
285
291
|
"""Id of the future-weather provider these results were produced with.
|
|
286
292
|
|
|
287
|
-
The backtest
|
|
288
|
-
and NetCDF exports cannot disagree.
|
|
293
|
+
The backtest's specification is the single source of truth, so database
|
|
294
|
+
persistence and NetCDF exports cannot disagree.
|
|
289
295
|
"""
|
|
290
296
|
return self._backtest.future_weather_provider
|
|
291
297
|
|
|
@@ -311,11 +317,25 @@ class Evaluation(EvaluationBase):
|
|
|
311
317
|
info: BacktestCreate,
|
|
312
318
|
historical_observations: list[Observation] | None = None,
|
|
313
319
|
historical_context_periods: int = 0,
|
|
320
|
+
specification: BacktestSpecification | None = None,
|
|
314
321
|
) -> "Evaluation":
|
|
315
322
|
info.created = datetime.datetime.now()
|
|
323
|
+
# The parameters live on the specification now and are computed fields on
|
|
324
|
+
# Backtest, reading through the link. Excluding them here keeps that explicit;
|
|
325
|
+
# passing them would be silently ignored rather than rejected.
|
|
316
326
|
backtest = Backtest(
|
|
317
|
-
**info.model_dump()
|
|
318
|
-
| {
|
|
327
|
+
**info.model_dump(exclude=set(BacktestParams.model_fields))
|
|
328
|
+
| {
|
|
329
|
+
"model_db_id": configured_model.id,
|
|
330
|
+
"model_template_version": configured_model.model_template.version,
|
|
331
|
+
"chap_version": RECORDED_CHAP_VERSION,
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
# Callers persisting the backtest pass the deduplicated row resolved against the
|
|
335
|
+
# database. The CLI paths build an in-memory evaluation only, so a detached
|
|
336
|
+
# specification carrying the same parameters is enough for them.
|
|
337
|
+
backtest.specification = specification or BacktestSpecification(
|
|
338
|
+
dataset_id=info.dataset_id, **info.model_dump(include=set(BacktestParams.model_fields))
|
|
319
339
|
)
|
|
320
340
|
org_units = set()
|
|
321
341
|
split_points = set()
|
|
@@ -393,7 +413,7 @@ class Evaluation(EvaluationBase):
|
|
|
393
413
|
configured_model: ConfiguredModelDB,
|
|
394
414
|
estimator,
|
|
395
415
|
dataset: _DataSet,
|
|
396
|
-
backtest_params:
|
|
416
|
+
backtest_params: BacktestParams,
|
|
397
417
|
backtest_name: str = "evaluation",
|
|
398
418
|
historical_context_years: int = 6,
|
|
399
419
|
) -> "Evaluation":
|
|
@@ -689,7 +709,15 @@ class Evaluation(EvaluationBase):
|
|
|
689
709
|
split_periods=split_periods,
|
|
690
710
|
forecasts=[],
|
|
691
711
|
dataset_id=0,
|
|
692
|
-
|
|
712
|
+
# The version of the writer, not of the process importing the file.
|
|
713
|
+
chap_version=_recorded_version(ds.attrs.get("chap_version")),
|
|
714
|
+
# The stored file carries no parameters beyond the provider, so this is a
|
|
715
|
+
# detached placeholder that keeps the parameter properties readable.
|
|
716
|
+
specification=BacktestSpecification(
|
|
717
|
+
dataset_id=0,
|
|
718
|
+
org_units=org_units,
|
|
719
|
+
future_weather_provider=str(ds.attrs["future_weather_provider"]),
|
|
720
|
+
),
|
|
693
721
|
)
|
|
694
722
|
|
|
695
723
|
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
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Outbreak detection metrics: sensitivity, specificity, and accuracy.
|
|
3
|
+
|
|
4
|
+
An "outbreak" is observed cases exceeding the seasonal baseline (mean + 2*std)
|
|
5
|
+
for a given location and season bucket — calendar month for monthly data,
|
|
6
|
+
epi-week for weekly. An "alert" is raised when more than
|
|
7
|
+
:data:`ALERT_SAMPLE_FRACTION` of the forecast samples exceed that threshold.
|
|
8
|
+
|
|
9
|
+
All three metrics score the same joined frame, built once by
|
|
10
|
+
:func:`outbreak_and_alert`. They differ only in which rows they keep and what
|
|
11
|
+
they compare, so the threshold and alert rules live in one place.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from chap_core.assessment.metrics import metric
|
|
21
|
+
from chap_core.assessment.metrics.base import (
|
|
22
|
+
AggregationOp,
|
|
23
|
+
Metric,
|
|
24
|
+
MetricSpec,
|
|
25
|
+
OptimizationDirection,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# The outbreak metrics score forecasts against seasonal thresholds, computed by the
|
|
29
|
+
# threshold strategy module.
|
|
30
|
+
from chap_core.assessment.thresholds.seasonal import compute_seasonal_thresholds
|
|
31
|
+
from chap_core.time_period.vectorized import season_column
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
import pandera.pandas as pa
|
|
35
|
+
|
|
36
|
+
from chap_core.assessment.flat_representations import FlatObserved
|
|
37
|
+
|
|
38
|
+
#: Fraction of forecast samples that must exceed the threshold for an alert to be raised.
|
|
39
|
+
ALERT_SAMPLE_FRACTION = 0.5
|
|
40
|
+
|
|
41
|
+
_OUTBREAK_COLUMNS = ["location", "time_period", "horizon_distance", "outbreak", "alert"]
|
|
42
|
+
_METRIC_DIMENSIONS = ["location", "time_period", "horizon_distance"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _season_of(thresholds: pd.DataFrame) -> str:
|
|
46
|
+
"""The season bucket a threshold frame is keyed on."""
|
|
47
|
+
return "week" if "week" in thresholds.columns else "month"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def has_season_buckets(observations: pd.DataFrame) -> bool:
|
|
51
|
+
"""Whether the observations' time periods bucket into calendar months or epi-weeks."""
|
|
52
|
+
if observations.empty:
|
|
53
|
+
return False
|
|
54
|
+
try:
|
|
55
|
+
season_column(observations["time_period"])
|
|
56
|
+
except (ValueError, KeyError, TypeError):
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def outbreak_and_alert(
|
|
62
|
+
historical_observations: pd.DataFrame | None,
|
|
63
|
+
observations: pd.DataFrame,
|
|
64
|
+
forecasts: pd.DataFrame,
|
|
65
|
+
) -> pd.DataFrame:
|
|
66
|
+
"""Label every scored cell as outbreak and alert against the seasonal threshold.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
historical_observations: Observations the threshold is computed from, with
|
|
70
|
+
columns ``[location, time_period, disease_cases]``. ``None`` yields an
|
|
71
|
+
empty frame — the metrics are not applicable without it.
|
|
72
|
+
observations: Observed cases to label, same columns.
|
|
73
|
+
forecasts: Forecast samples, with columns
|
|
74
|
+
``[location, time_period, horizon_distance, sample, forecast]``.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
One row per ``(location, time_period, horizon_distance)`` that has both a
|
|
78
|
+
computable threshold and a forecast, with columns
|
|
79
|
+
``[location, time_period, horizon_distance, outbreak, alert]``. ``outbreak``
|
|
80
|
+
is 1.0 where observed cases exceed the threshold; ``alert`` is 1.0 where more
|
|
81
|
+
than :data:`ALERT_SAMPLE_FRACTION` of the samples exceed it. Cells whose
|
|
82
|
+
threshold cannot be computed (a single historical value gives an undefined
|
|
83
|
+
standard deviation) are dropped.
|
|
84
|
+
"""
|
|
85
|
+
empty = pd.DataFrame(columns=_OUTBREAK_COLUMNS)
|
|
86
|
+
if historical_observations is None or historical_observations.empty:
|
|
87
|
+
return empty
|
|
88
|
+
if observations.empty or forecasts.empty:
|
|
89
|
+
return empty
|
|
90
|
+
|
|
91
|
+
thresholds = compute_seasonal_thresholds(historical_observations)
|
|
92
|
+
if thresholds.empty:
|
|
93
|
+
return empty
|
|
94
|
+
season = _season_of(thresholds)
|
|
95
|
+
|
|
96
|
+
obs = observations[["location", "time_period", "disease_cases"]].copy()
|
|
97
|
+
obs_season, obs_buckets = season_column(obs["time_period"])
|
|
98
|
+
if obs_season != season:
|
|
99
|
+
# Historical and scored periods are at different frequencies; nothing to join on.
|
|
100
|
+
return empty
|
|
101
|
+
obs[season] = obs_buckets
|
|
102
|
+
obs = obs.merge(thresholds, on=["location", season], how="left").dropna(subset=["threshold"])
|
|
103
|
+
if obs.empty:
|
|
104
|
+
return empty
|
|
105
|
+
obs["outbreak"] = (obs["disease_cases"] > obs["threshold"]).astype(float)
|
|
106
|
+
|
|
107
|
+
fc = forecasts.copy()
|
|
108
|
+
fc[season] = season_column(fc["time_period"])[1]
|
|
109
|
+
fc = fc.merge(thresholds, on=["location", season], how="left")
|
|
110
|
+
fc["exceeds"] = (fc["forecast"] > fc["threshold"]).astype(float)
|
|
111
|
+
alert = fc.groupby(_METRIC_DIMENSIONS, as_index=False)["exceeds"].mean()
|
|
112
|
+
alert["alert"] = (alert["exceeds"] > ALERT_SAMPLE_FRACTION).astype(float)
|
|
113
|
+
|
|
114
|
+
merged = obs[["location", "time_period", "outbreak"]].merge(
|
|
115
|
+
alert[[*_METRIC_DIMENSIONS, "alert"]],
|
|
116
|
+
on=["location", "time_period"],
|
|
117
|
+
how="inner",
|
|
118
|
+
)
|
|
119
|
+
return pd.DataFrame(merged[_OUTBREAK_COLUMNS])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _as_metric(rows: pd.DataFrame, values: pd.Series) -> pd.DataFrame:
|
|
123
|
+
"""Reshape a slice of the outbreak frame into the metric output contract."""
|
|
124
|
+
result = rows[_METRIC_DIMENSIONS].copy()
|
|
125
|
+
result["metric"] = values
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _OutbreakMetric(Metric):
|
|
130
|
+
"""Shared applicability rule for metrics scored against a seasonal threshold."""
|
|
131
|
+
|
|
132
|
+
def is_applicable(self, observations: pa.typing.DataFrame[FlatObserved]) -> bool:
|
|
133
|
+
return self.historical_observations is not None and has_season_buckets(observations)
|
|
134
|
+
|
|
135
|
+
def _frame(self, observations: pd.DataFrame, forecasts: pd.DataFrame) -> pd.DataFrame:
|
|
136
|
+
return outbreak_and_alert(self.historical_observations, observations, forecasts)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@metric()
|
|
140
|
+
class SensitivityMetric(_OutbreakMetric):
|
|
141
|
+
"""Sensitivity (true positive rate) for outbreak detection.
|
|
142
|
+
|
|
143
|
+
Measures the proportion of actual outbreaks that were correctly
|
|
144
|
+
predicted (alerted) by the forecast.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
spec = MetricSpec(
|
|
148
|
+
metric_id="sensitivity",
|
|
149
|
+
metric_name="Sensitivity",
|
|
150
|
+
aggregation_op=AggregationOp.MEAN,
|
|
151
|
+
description="True positive rate for outbreak detection alerts",
|
|
152
|
+
optimization_direction=OptimizationDirection.MAXIMIZE,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def compute_detailed(self, observations: pd.DataFrame, forecasts: pd.DataFrame) -> pd.DataFrame:
|
|
156
|
+
frame = self._frame(observations, forecasts)
|
|
157
|
+
outbreaks = frame[frame["outbreak"] == 1.0]
|
|
158
|
+
return _as_metric(outbreaks, outbreaks["alert"])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@metric()
|
|
162
|
+
class SpecificityMetric(_OutbreakMetric):
|
|
163
|
+
"""Specificity (true negative rate) for outbreak detection.
|
|
164
|
+
|
|
165
|
+
Measures the proportion of non-outbreak periods that were correctly
|
|
166
|
+
not alerted by the forecast.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
spec = MetricSpec(
|
|
170
|
+
metric_id="specificity",
|
|
171
|
+
metric_name="Specificity",
|
|
172
|
+
aggregation_op=AggregationOp.MEAN,
|
|
173
|
+
description="True negative rate for outbreak detection alerts",
|
|
174
|
+
optimization_direction=OptimizationDirection.MAXIMIZE,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def compute_detailed(self, observations: pd.DataFrame, forecasts: pd.DataFrame) -> pd.DataFrame:
|
|
178
|
+
frame = self._frame(observations, forecasts)
|
|
179
|
+
quiet = frame[frame["outbreak"] == 0.0]
|
|
180
|
+
return _as_metric(quiet, 1.0 - quiet["alert"])
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@metric()
|
|
184
|
+
class OutbreakAccuracyMetric(_OutbreakMetric):
|
|
185
|
+
"""Accuracy for outbreak detection.
|
|
186
|
+
|
|
187
|
+
Measures the proportion of all periods where the alert status
|
|
188
|
+
correctly matches the outbreak status: (TP + TN) / (TP + TN + FP + FN).
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
spec = MetricSpec(
|
|
192
|
+
metric_id="outbreak_accuracy",
|
|
193
|
+
metric_name="Outbreak Accuracy",
|
|
194
|
+
aggregation_op=AggregationOp.MEAN,
|
|
195
|
+
description="Proportion of correctly classified outbreak/non-outbreak periods",
|
|
196
|
+
optimization_direction=OptimizationDirection.MAXIMIZE,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def compute_detailed(self, observations: pd.DataFrame, forecasts: pd.DataFrame) -> pd.DataFrame:
|
|
200
|
+
frame = self._frame(observations, forecasts)
|
|
201
|
+
return _as_metric(frame, (frame["alert"] == frame["outbreak"]).astype(float))
|