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.
Files changed (261) hide show
  1. {chap_core-2.2.0 → chap_core-2.3.1}/PKG-INFO +2 -2
  2. chap_core-2.3.1/chap_core/assessment/causal_plot.py +246 -0
  3. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/evaluation.py +44 -16
  4. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/__init__.py +0 -6
  5. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/mape.py +4 -4
  6. chap_core-2.3.1/chap_core/assessment/metrics/outbreak_detection.py +201 -0
  7. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/thresholds/__init__.py +25 -3
  8. chap_core-2.3.1/chap_core/assessment/thresholds/base.py +76 -0
  9. chap_core-2.3.1/chap_core/assessment/thresholds/params.py +82 -0
  10. chap_core-2.3.1/chap_core/assessment/thresholds/percentile.py +113 -0
  11. chap_core-2.3.1/chap_core/assessment/thresholds/seasonal.py +66 -0
  12. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/_common.py +48 -2
  13. chap_core-2.3.1/chap_core/cli_endpoints/build_counterfactual.py +393 -0
  14. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/causal.py +120 -14
  15. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/evaluate.py +5 -1
  16. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/generate_modelcard.py +0 -1
  17. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/preference_learn.py +1 -1
  18. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/database.py +41 -4
  19. chap_core-2.3.1/chap_core/database/migration_helpers.py +35 -0
  20. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/model_template_seed.py +6 -4
  21. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/tables.py +82 -6
  22. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/wrappers.py +1 -1
  23. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/log_config.py +5 -3
  24. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/chapkit_rest_api_wrapper.py +57 -3
  25. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/external_chapkit_model.py +103 -43
  26. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/celery_tasks.py +36 -23
  27. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/data_models.py +6 -2
  28. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/db_worker_functions.py +19 -3
  29. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/services/schemas.py +15 -2
  30. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/jobs.py +3 -2
  31. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/analytics.py +65 -21
  32. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/crud.py +6 -1
  33. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/visualization.py +10 -1
  34. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/services/dataset_validation.py +15 -6
  35. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/simulation/naive_simulator.py +10 -4
  36. chap_core-2.3.1/chap_core/time_period/vectorized.py +54 -0
  37. {chap_core-2.2.0 → chap_core-2.3.1}/pyproject.toml +2 -2
  38. chap_core-2.2.0/chap_core/alarms.py +0 -16
  39. chap_core-2.2.0/chap_core/assessment/causal_plot.py +0 -52
  40. chap_core-2.2.0/chap_core/assessment/metrics/example_metric.py +0 -38
  41. chap_core-2.2.0/chap_core/assessment/metrics/outbreak_detection.py +0 -204
  42. chap_core-2.2.0/chap_core/assessment/metrics/test_metrics.py +0 -40
  43. chap_core-2.2.0/chap_core/assessment/thresholds/base.py +0 -47
  44. chap_core-2.2.0/chap_core/assessment/thresholds/seasonal.py +0 -84
  45. chap_core-2.2.0/chap_core/cli_endpoints/build_counterfactual.py +0 -187
  46. {chap_core-2.2.0 → chap_core-2.3.1}/README.md +0 -0
  47. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/__init__.py +0 -0
  48. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/__init__.py +0 -0
  49. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/command_line_interface.py +0 -0
  50. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/gluonts.py +0 -0
  51. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/adaptors/rest_api.py +0 -0
  52. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/api.py +0 -0
  53. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/api_types.py +0 -0
  54. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/__init__.py +0 -0
  55. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/__init__.py +0 -0
  56. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/db_dimensions.py +0 -0
  57. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/evaluation_plot.py +0 -0
  58. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/horizon_location_grid.py +0 -0
  59. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/predicted_vs_actual_linear_plot.py +0 -0
  60. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/predicted_vs_actual_plot.py +0 -0
  61. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/backtest_plots/sample_bias_plot.py +0 -0
  62. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/data_representation_transforming.py +0 -0
  63. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/dataset_splitting.py +0 -0
  64. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/eval_tracking.py +0 -0
  65. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/evaluator.py +0 -0
  66. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/evaluator_suites.py +0 -0
  67. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/flat_representations.py +0 -0
  68. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/forecast.py +0 -0
  69. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/__init__.py +0 -0
  70. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/horizon_location_mean.py +0 -0
  71. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/horizon_mean.py +0 -0
  72. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/horizon_sum.py +0 -0
  73. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/metric_map.py +0 -0
  74. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/regional_distribution.py +0 -0
  75. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/time_period_location_mean.py +0 -0
  76. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/time_period_mean.py +0 -0
  77. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_plots/time_period_sum.py +0 -0
  78. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metric_table.py +0 -0
  79. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/above_truth.py +0 -0
  80. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/base.py +0 -0
  81. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/crps.py +0 -0
  82. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/crps_norm.py +0 -0
  83. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/mae.py +0 -0
  84. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/peak_diff.py +0 -0
  85. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/percentile_coverage.py +0 -0
  86. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/rmse.py +0 -0
  87. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/metrics/winkler_score.py +0 -0
  88. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/prediction_evaluator.py +0 -0
  89. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/representations.py +0 -0
  90. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/__init__.py +0 -0
  91. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/base.py +0 -0
  92. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/climatology.py +0 -0
  93. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/damped_persistence.py +0 -0
  94. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/mstl_arima.py +0 -0
  95. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/assessment/weather_providers/observed.py +0 -0
  96. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli.py +0 -0
  97. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/__init__.py +0 -0
  98. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/_args.py +0 -0
  99. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/aggregate_eval.py +0 -0
  100. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/convert.py +0 -0
  101. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/ensemble.py +0 -0
  102. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/explain.py +0 -0
  103. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/forecast.py +0 -0
  104. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/generated_plot_ids.py +0 -0
  105. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/model.py +0 -0
  106. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/report.py +0 -0
  107. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/utils.py +0 -0
  108. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/cli_endpoints/validate.py +0 -0
  109. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/climate_data/__init__.py +0 -0
  110. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/climate_data/seasonal_forecasts.py +0 -0
  111. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/climate_predictor.py +0 -0
  112. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/__init__.py +0 -0
  113. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/adaptors.py +0 -0
  114. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/datasets.py +0 -0
  115. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/gluonts_adaptor/__init__.py +0 -0
  116. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/gluonts_adaptor/dataset.py +0 -0
  117. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/data/open_dengue.py +0 -0
  118. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/__init__.py +0 -0
  119. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/base_tables.py +0 -0
  120. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/dataset_manager.py +0 -0
  121. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/dataset_tables.py +0 -0
  122. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/datasets_seed.py +0 -0
  123. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/feature_tables.py +0 -0
  124. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/manager.py +0 -0
  125. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/model_spec_tables.py +0 -0
  126. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/database/model_templates_and_config_tables.py +0 -0
  127. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/datatypes.py +0 -0
  128. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/docker_helper_functions.py +0 -0
  129. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/__init__.py +0 -0
  130. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/_meta_models.py +0 -0
  131. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/_predictor.py +0 -0
  132. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/_sample_extractor.py +0 -0
  133. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/ensemble/ensemble_model.py +0 -0
  134. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/exceptions.py +0 -0
  135. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/distance.py +0 -0
  136. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/documentation.md +0 -0
  137. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/lime.py +0 -0
  138. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/perturb.py +0 -0
  139. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/plot.py +0 -0
  140. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/segment.py +0 -0
  141. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/surrogate.py +0 -0
  142. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/testing/__init__.py +0 -0
  143. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/testing/metrics.py +0 -0
  144. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/explainability/userguide.md +0 -0
  145. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/ExtendedPredictor.py +0 -0
  146. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/__init__.py +0 -0
  147. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/external_model.py +0 -0
  148. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/github.py +0 -0
  149. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/mlflow_wrappers.py +0 -0
  150. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/model_configuration.py +0 -0
  151. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/python_model.py +0 -0
  152. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/r_description.py +0 -0
  153. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/r_model.py +0 -0
  154. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/r_models.py +0 -0
  155. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/external/uv_wrapper.py +0 -0
  156. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/feature_generators/__init__.py +0 -0
  157. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/feature_generators/seasonality_cluster.py +0 -0
  158. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/fetch/__init__.py +0 -0
  159. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/__init__.py +0 -0
  160. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/cleaners.py +0 -0
  161. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/example_data_set.py +0 -0
  162. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/external_file.py +0 -0
  163. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/file_paths.py +0 -0
  164. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/file_io/load.py +0 -0
  165. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geo_coding/__init__.py +0 -0
  166. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geojson.py +0 -0
  167. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geometry.py +0 -0
  168. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/geoutils.py +0 -0
  169. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/README.md +0 -0
  170. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/__init__.py +0 -0
  171. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/base.py +0 -0
  172. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/config1.yaml +0 -0
  173. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/config2.yaml +0 -0
  174. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/config3.yaml +0 -0
  175. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/hpoModel.py +0 -0
  176. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/hpoModelInterface.py +0 -0
  177. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/objective.py +0 -0
  178. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/searcher.py +0 -0
  179. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/hpo/types.py +0 -0
  180. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/model_spec.py +0 -0
  181. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/__init__.py +0 -0
  182. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/chapkit_service_manager.py +0 -0
  183. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/configured_model.py +0 -0
  184. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/external_model.py +0 -0
  185. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/external_web_model.py +0 -0
  186. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/local_configuration.py +0 -0
  187. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/model_template.py +0 -0
  188. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/model_template_interface.py +0 -0
  189. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/models/utils.py +0 -0
  190. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/pandas_adaptors.py +0 -0
  191. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/__init__.py +0 -0
  192. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/backtest_dashboard_spec.json +0 -0
  193. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/backtest_plot.py +0 -0
  194. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/conftest.py +0 -0
  195. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/dataset_plot.py +0 -0
  196. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/evaluation_plot.py +0 -0
  197. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/prediction_plot.py +0 -0
  198. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/produce_plots_from_yaml.py +0 -0
  199. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/season_plot.py +0 -0
  200. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/plotting/yaml_plot.py +0 -0
  201. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/__init__.py +0 -0
  202. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/feature_spec.py +0 -0
  203. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/model_registry.py +0 -0
  204. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/naive_estimator.py +0 -0
  205. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/naive_predictor.py +0 -0
  206. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/poisson.py +0 -0
  207. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/predictor/protocol.py +0 -0
  208. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/preference_learning/__init__.py +0 -0
  209. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/preference_learning/decision_maker.py +0 -0
  210. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/preference_learning/preference_learner.py +0 -0
  211. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rbased_docker.py +0 -0
  212. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/__init__.py +0 -0
  213. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/app.py +0 -0
  214. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/auth.py +0 -0
  215. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/common_routes.py +0 -0
  216. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/experimental.py +0 -0
  217. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/services/__init__.py +0 -0
  218. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/services/orchestrator.py +0 -0
  219. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/__init__.py +0 -0
  220. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/rest_api.py +0 -0
  221. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v1/routers/dependencies.py +0 -0
  222. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/__init__.py +0 -0
  223. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/dependencies.py +0 -0
  224. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/rest_api.py +0 -0
  225. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/routers/__init__.py +0 -0
  226. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/routers/proxy.py +0 -0
  227. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/v2/routers/services.py +0 -0
  228. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/rest_api/worker_functions.py +0 -0
  229. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/__init__.py +0 -0
  230. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/command_line_runner.py +0 -0
  231. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/conda_runner.py +0 -0
  232. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/docker_runner.py +0 -0
  233. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/helper_functions.py +0 -0
  234. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/mlflow_runner.py +0 -0
  235. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/renv_runner.py +0 -0
  236. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/runner.py +0 -0
  237. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/runners/uv_runner.py +0 -0
  238. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/services/prediction_setup_service.py +0 -0
  239. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/simulation/__init__.py +0 -0
  240. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/simulation/simulator.py +0 -0
  241. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/__init__.py +0 -0
  242. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/converters.py +0 -0
  243. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/multi_country_dataset.py +0 -0
  244. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/spatio_temporal_data/temporal_dataclass.py +0 -0
  245. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/__init__.py +0 -0
  246. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/estimators.py +0 -0
  247. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/external_model.py +0 -0
  248. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/mocks.py +0 -0
  249. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/testing/testing.py +0 -0
  250. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/__init__.py +0 -0
  251. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/date_util_wrapper.py +0 -0
  252. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/multi_resolution.py +0 -0
  253. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/period_assignment.py +0 -0
  254. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/time_period/relationships.py +0 -0
  255. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/transformations/__init__.py +0 -0
  256. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/transformations/covid_mask.py +0 -0
  257. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/util.py +0 -0
  258. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/validators.py +0 -0
  259. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/worker/__init__.py +0 -0
  260. {chap_core-2.2.0 → chap_core-2.3.1}/chap_core/worker/interface.py +0 -0
  261. {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.2.0
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.0,<2
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 TYPE_CHECKING, cast
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.assessment.weather_providers import (
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 row is the single source of truth, so database persistence
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
- | {"model_db_id": configured_model.id, "model_template_version": configured_model.model_template.version}
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: "BacktestParams",
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
- future_weather_provider=str(ds.attrs["future_weather_provider"]),
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 average of the absolute error for each entry. When aggregated using
22
- MEAN, this produces the MAPE (mean of absolute percentage error).
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 - measures average absolute prediction 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))