chap-core 2.2.0__tar.gz → 2.3.0__tar.gz

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