chap-core 2.0.0__tar.gz → 2.2.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 (259) hide show
  1. {chap_core-2.0.0 → chap_core-2.2.0}/PKG-INFO +7 -8
  2. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/api_types.py +29 -2
  3. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/backtest_plots/__init__.py +0 -1
  4. chap_core-2.2.0/chap_core/assessment/backtest_plots/db_dimensions.py +167 -0
  5. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/backtest_plots/evaluation_plot.py +3 -2
  6. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/backtest_plots/horizon_location_grid.py +109 -42
  7. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/backtest_plots/predicted_vs_actual_linear_plot.py +11 -11
  8. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/backtest_plots/predicted_vs_actual_plot.py +2 -1
  9. chap_core-2.2.0/chap_core/assessment/backtest_plots/sample_bias_plot.py +91 -0
  10. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/dataset_splitting.py +11 -11
  11. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/evaluation.py +80 -19
  12. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/flat_representations.py +7 -0
  13. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/forecast.py +12 -6
  14. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/horizon_sum.py +4 -3
  15. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/time_period_mean.py +2 -2
  16. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/time_period_sum.py +4 -3
  17. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/__init__.py +20 -0
  18. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/above_truth.py +1 -0
  19. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/base.py +14 -7
  20. chap_core-2.2.0/chap_core/assessment/metrics/crps.py +112 -0
  21. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/crps_norm.py +2 -0
  22. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/example_metric.py +2 -0
  23. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/mae.py +2 -0
  24. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/mape.py +2 -0
  25. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/outbreak_detection.py +9 -4
  26. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/peak_diff.py +2 -0
  27. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/percentile_coverage.py +2 -0
  28. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/rmse.py +2 -0
  29. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/test_metrics.py +2 -0
  30. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metrics/winkler_score.py +5 -0
  31. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/prediction_evaluator.py +79 -20
  32. chap_core-2.2.0/chap_core/assessment/thresholds/seasonal.py +84 -0
  33. chap_core-2.2.0/chap_core/assessment/weather_providers/__init__.py +152 -0
  34. chap_core-2.2.0/chap_core/assessment/weather_providers/base.py +55 -0
  35. chap_core-2.2.0/chap_core/assessment/weather_providers/climatology.py +20 -0
  36. chap_core-2.2.0/chap_core/assessment/weather_providers/damped_persistence.py +82 -0
  37. chap_core-2.2.0/chap_core/assessment/weather_providers/mstl_arima.py +194 -0
  38. chap_core-2.2.0/chap_core/assessment/weather_providers/observed.py +24 -0
  39. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli.py +2 -0
  40. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/_common.py +48 -28
  41. chap_core-2.2.0/chap_core/cli_endpoints/ensemble.py +416 -0
  42. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/evaluate.py +44 -19
  43. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/explain.py +2 -1
  44. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/generated_plot_ids.py +2 -2
  45. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/utils.py +1 -1
  46. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/climate_predictor.py +21 -12
  47. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/data/datasets.py +1 -1
  48. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/database.py +151 -46
  49. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/model_spec_tables.py +5 -0
  50. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/model_template_seed.py +65 -25
  51. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/model_templates_and_config_tables.py +100 -12
  52. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/tables.py +11 -2
  53. chap_core-2.2.0/chap_core/ensemble/__init__.py +1 -0
  54. chap_core-2.2.0/chap_core/ensemble/_meta_models.py +131 -0
  55. chap_core-2.2.0/chap_core/ensemble/_predictor.py +110 -0
  56. chap_core-2.2.0/chap_core/ensemble/_sample_extractor.py +123 -0
  57. chap_core-2.2.0/chap_core/ensemble/ensemble_model.py +317 -0
  58. chap_core-2.2.0/chap_core/ensemble/wrappers.py +59 -0
  59. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/documentation.md +1 -1
  60. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/lime.py +41 -11
  61. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/segment.py +4 -4
  62. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/userguide.md +1 -1
  63. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/ExtendedPredictor.py +3 -3
  64. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/github.py +23 -0
  65. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/file_io/example_data_set.py +1 -1
  66. chap_core-2.2.0/chap_core/hpo/hpoModel.py +134 -0
  67. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/hpoModelInterface.py +2 -1
  68. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/objective.py +22 -21
  69. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/searcher.py +35 -63
  70. chap_core-2.2.0/chap_core/hpo/types.py +11 -0
  71. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/chapkit_rest_api_wrapper.py +78 -15
  72. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/chapkit_service_manager.py +53 -4
  73. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/external_chapkit_model.py +20 -9
  74. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/local_configuration.py +1 -0
  75. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/utils.py +1 -1
  76. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/backtest_plot.py +4 -2
  77. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/dataset_plot.py +3 -1
  78. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/evaluation_plot.py +4 -1
  79. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/yaml_plot.py +3 -1
  80. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/app.py +11 -1
  81. chap_core-2.2.0/chap_core/rest_api/auth.py +140 -0
  82. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/celery_tasks.py +1 -1
  83. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/common_routes.py +9 -0
  84. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/data_models.py +65 -8
  85. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/db_worker_functions.py +55 -21
  86. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/services/orchestrator.py +60 -33
  87. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/services/schemas.py +4 -0
  88. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v1/routers/analytics.py +97 -4
  89. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v1/routers/crud.py +76 -83
  90. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v1/routers/visualization.py +76 -19
  91. chap_core-2.2.0/chap_core/rest_api/v2/dependencies.py +57 -0
  92. chap_core-2.2.0/chap_core/rest_api/v2/rest_api.py +7 -0
  93. chap_core-2.2.0/chap_core/rest_api/v2/routers/proxy.py +161 -0
  94. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v2/routers/services.py +1 -1
  95. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/command_line_runner.py +16 -34
  96. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/simulation/naive_simulator.py +9 -1
  97. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/util.py +15 -1
  98. {chap_core-2.0.0 → chap_core-2.2.0}/pyproject.toml +18 -12
  99. chap_core-2.0.0/chap_core/assessment/backtest_plots/metrics_dashboard.py +0 -101
  100. chap_core-2.0.0/chap_core/assessment/backtest_plots/sample_bias_plot.py +0 -138
  101. chap_core-2.0.0/chap_core/assessment/metrics/crps.py +0 -71
  102. chap_core-2.0.0/chap_core/assessment/thresholds/seasonal.py +0 -56
  103. chap_core-2.0.0/chap_core/hpo/hpoModel.py +0 -123
  104. chap_core-2.0.0/chap_core/rest_api/v2/dependencies.py +0 -53
  105. chap_core-2.0.0/chap_core/rest_api/v2/rest_api.py +0 -6
  106. {chap_core-2.0.0 → chap_core-2.2.0}/README.md +0 -0
  107. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/__init__.py +0 -0
  108. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/adaptors/__init__.py +0 -0
  109. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/adaptors/command_line_interface.py +0 -0
  110. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/adaptors/gluonts.py +0 -0
  111. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/adaptors/rest_api.py +0 -0
  112. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/alarms.py +0 -0
  113. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/api.py +0 -0
  114. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/__init__.py +0 -0
  115. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/causal_plot.py +0 -0
  116. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/data_representation_transforming.py +0 -0
  117. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/eval_tracking.py +0 -0
  118. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/evaluator.py +0 -0
  119. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/evaluator_suites.py +0 -0
  120. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/__init__.py +0 -0
  121. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/horizon_location_mean.py +0 -0
  122. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/horizon_mean.py +0 -0
  123. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/metric_map.py +0 -0
  124. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/regional_distribution.py +0 -0
  125. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_plots/time_period_location_mean.py +0 -0
  126. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/metric_table.py +0 -0
  127. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/representations.py +0 -0
  128. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/thresholds/__init__.py +0 -0
  129. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/assessment/thresholds/base.py +0 -0
  130. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/__init__.py +0 -0
  131. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/_args.py +0 -0
  132. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/aggregate_eval.py +0 -0
  133. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/build_counterfactual.py +0 -0
  134. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/causal.py +0 -0
  135. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/convert.py +0 -0
  136. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/forecast.py +0 -0
  137. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/generate_modelcard.py +0 -0
  138. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/model.py +0 -0
  139. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/preference_learn.py +0 -0
  140. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/report.py +0 -0
  141. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/cli_endpoints/validate.py +0 -0
  142. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/climate_data/__init__.py +0 -0
  143. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/climate_data/seasonal_forecasts.py +0 -0
  144. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/data/__init__.py +0 -0
  145. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/data/adaptors.py +0 -0
  146. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/data/gluonts_adaptor/__init__.py +0 -0
  147. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/data/gluonts_adaptor/dataset.py +0 -0
  148. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/data/open_dengue.py +0 -0
  149. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/__init__.py +0 -0
  150. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/base_tables.py +0 -0
  151. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/dataset_manager.py +0 -0
  152. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/dataset_tables.py +0 -0
  153. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/datasets_seed.py +0 -0
  154. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/feature_tables.py +0 -0
  155. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/database/manager.py +0 -0
  156. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/datatypes.py +0 -0
  157. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/docker_helper_functions.py +0 -0
  158. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/exceptions.py +0 -0
  159. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/distance.py +0 -0
  160. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/perturb.py +0 -0
  161. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/plot.py +0 -0
  162. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/surrogate.py +0 -0
  163. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/testing/__init__.py +0 -0
  164. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/explainability/testing/metrics.py +0 -0
  165. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/__init__.py +0 -0
  166. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/external_model.py +0 -0
  167. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/mlflow_wrappers.py +0 -0
  168. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/model_configuration.py +0 -0
  169. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/python_model.py +0 -0
  170. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/r_description.py +0 -0
  171. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/r_model.py +0 -0
  172. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/r_models.py +0 -0
  173. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/external/uv_wrapper.py +0 -0
  174. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/feature_generators/__init__.py +0 -0
  175. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/feature_generators/seasonality_cluster.py +0 -0
  176. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/fetch/__init__.py +0 -0
  177. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/file_io/__init__.py +0 -0
  178. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/file_io/cleaners.py +0 -0
  179. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/file_io/external_file.py +0 -0
  180. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/file_io/file_paths.py +0 -0
  181. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/file_io/load.py +0 -0
  182. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/geo_coding/__init__.py +0 -0
  183. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/geojson.py +0 -0
  184. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/geometry.py +0 -0
  185. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/geoutils.py +0 -0
  186. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/README.md +0 -0
  187. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/__init__.py +0 -0
  188. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/base.py +0 -0
  189. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/config1.yaml +0 -0
  190. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/config2.yaml +0 -0
  191. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/hpo/config3.yaml +0 -0
  192. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/log_config.py +0 -0
  193. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/model_spec.py +0 -0
  194. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/__init__.py +0 -0
  195. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/configured_model.py +0 -0
  196. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/external_model.py +0 -0
  197. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/external_web_model.py +0 -0
  198. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/model_template.py +0 -0
  199. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/models/model_template_interface.py +0 -0
  200. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/pandas_adaptors.py +0 -0
  201. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/__init__.py +0 -0
  202. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/backtest_dashboard_spec.json +0 -0
  203. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/conftest.py +0 -0
  204. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/prediction_plot.py +0 -0
  205. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/produce_plots_from_yaml.py +0 -0
  206. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/plotting/season_plot.py +0 -0
  207. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/predictor/__init__.py +0 -0
  208. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/predictor/feature_spec.py +0 -0
  209. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/predictor/model_registry.py +0 -0
  210. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/predictor/naive_estimator.py +0 -0
  211. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/predictor/naive_predictor.py +0 -0
  212. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/predictor/poisson.py +0 -0
  213. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/predictor/protocol.py +0 -0
  214. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/preference_learning/__init__.py +0 -0
  215. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/preference_learning/decision_maker.py +0 -0
  216. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/preference_learning/preference_learner.py +0 -0
  217. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rbased_docker.py +0 -0
  218. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/__init__.py +0 -0
  219. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/experimental.py +0 -0
  220. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/services/__init__.py +0 -0
  221. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v1/__init__.py +0 -0
  222. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v1/jobs.py +0 -0
  223. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v1/rest_api.py +0 -0
  224. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v1/routers/dependencies.py +0 -0
  225. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v2/__init__.py +0 -0
  226. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/v2/routers/__init__.py +0 -0
  227. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/rest_api/worker_functions.py +0 -0
  228. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/__init__.py +0 -0
  229. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/conda_runner.py +0 -0
  230. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/docker_runner.py +0 -0
  231. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/helper_functions.py +0 -0
  232. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/mlflow_runner.py +0 -0
  233. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/renv_runner.py +0 -0
  234. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/runner.py +0 -0
  235. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/runners/uv_runner.py +0 -0
  236. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/services/dataset_validation.py +0 -0
  237. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/services/prediction_setup_service.py +0 -0
  238. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/simulation/__init__.py +0 -0
  239. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/simulation/simulator.py +0 -0
  240. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/spatio_temporal_data/__init__.py +0 -0
  241. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/spatio_temporal_data/converters.py +0 -0
  242. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/spatio_temporal_data/multi_country_dataset.py +0 -0
  243. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/spatio_temporal_data/temporal_dataclass.py +0 -0
  244. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/testing/__init__.py +0 -0
  245. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/testing/estimators.py +0 -0
  246. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/testing/external_model.py +0 -0
  247. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/testing/mocks.py +0 -0
  248. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/testing/testing.py +0 -0
  249. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/time_period/__init__.py +0 -0
  250. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/time_period/date_util_wrapper.py +0 -0
  251. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/time_period/multi_resolution.py +0 -0
  252. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/time_period/period_assignment.py +0 -0
  253. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/time_period/relationships.py +0 -0
  254. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/transformations/__init__.py +0 -0
  255. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/transformations/covid_mask.py +0 -0
  256. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/validators.py +0 -0
  257. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/worker/__init__.py +0 -0
  258. {chap_core-2.0.0 → chap_core-2.2.0}/chap_core/worker/interface.py +0 -0
  259. {chap_core-2.0.0 → chap_core-2.2.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.0.0
3
+ Version: 2.2.0
4
4
  Summary: Climate Health Analysis Platform (CHAP)
5
5
  Keywords: chap_core
6
6
  Author: Chap Team
@@ -32,7 +32,8 @@ Requires-Dist: pyyaml>=6.0.3
32
32
  Requires-Dist: requests>=2.34.2
33
33
  Requires-Dist: urllib3>=2.7.0
34
34
  Requires-Dist: rq>=2.9.0
35
- Requires-Dist: scikit-learn>=1.8.0
35
+ Requires-Dist: scikit-learn>=1.9.0
36
+ Requires-Dist: statsmodels>=0.14.6
36
37
  Requires-Dist: topojson>=1.10
37
38
  Requires-Dist: unidecode>=1.4.0
38
39
  Requires-Dist: uvicorn>=0.48.0
@@ -45,21 +46,19 @@ Requires-Dist: altair>=5.5.0
45
46
  Requires-Dist: virtualenv>=20.34.0
46
47
  Requires-Dist: geojson-pydantic>=2.1.1
47
48
  Requires-Dist: pandera>=0.31.1
48
- Requires-Dist: vegafusion>=2.0.3
49
49
  Requires-Dist: vl-convert-python>=1.9.0.post1
50
- Requires-Dist: pyarrow>=23.0.1
51
- Requires-Dist: numpy>=2.1.3
50
+ Requires-Dist: numpy>=2.3.2
52
51
  Requires-Dist: gunicorn>=25.3.0
53
- Requires-Dist: optuna>=4.8.0
52
+ Requires-Dist: optuna>=4.9.0
54
53
  Requires-Dist: httpx>=0.28.1
55
54
  Requires-Dist: alembic>=1.18.4
56
- Requires-Dist: chapkit>=0.26.0
55
+ Requires-Dist: chapkit>=1.0.0,<2
57
56
  Requires-Dist: cryptography>=46.0.7
58
57
  Requires-Dist: croniter>=6.2.2
59
58
  Requires-Dist: stumpy>=1.14.1
60
59
  Requires-Dist: pyts>=0.13.0
61
60
  Requires-Dist: fastdtw>=0.3.4
62
- Requires-Python: >=3.13, <3.14
61
+ Requires-Python: >=3.13, <3.15
63
62
  Project-URL: Homepage, https://github.com/dhis2/chap-core
64
63
  Description-Content-Type: text/markdown
65
64
 
@@ -1,4 +1,5 @@
1
1
  from enum import StrEnum
2
+ from pathlib import Path
2
3
  from typing import Any, Literal
3
4
 
4
5
  import numpy as np
@@ -16,8 +17,9 @@ from geojson_pydantic import (
16
17
  Point,
17
18
  Polygon,
18
19
  )
19
- from pydantic import BaseModel, Field, model_validator
20
+ from pydantic import BaseModel, Field, field_validator, model_validator
20
21
 
22
+ from chap_core.assessment.weather_providers import DEFAULT_WEATHER_PROVIDER_ID, resolve_weather_provider
21
23
  from chap_core.database.base_tables import DBModel
22
24
 
23
25
 
@@ -124,6 +126,18 @@ class BacktestParams(DBModel):
124
126
  gt=0,
125
127
  description="Number of times the model is retrained, evenly spaced across the splits. 1 means train once.",
126
128
  )
129
+ future_weather_provider: str = Field(
130
+ default=DEFAULT_WEATHER_PROVIDER_ID,
131
+ description="Id of the registered future-weather provider supplying climate covariates for each "
132
+ "forecast window. Use the same provider here and on the prediction so backtest scores reflect "
133
+ "what the model will see in production. See GET /v1/analytics/weather-providers.",
134
+ )
135
+
136
+ @field_validator("future_weather_provider")
137
+ @classmethod
138
+ def _check_weather_provider(cls, value: str) -> str:
139
+ resolve_weather_provider(value)
140
+ return value
127
141
 
128
142
  @model_validator(mode="after")
129
143
  def _check_n_retrain(self) -> "BacktestParams":
@@ -209,11 +223,24 @@ class EstimatorOptions(BaseModel):
209
223
  default=EstimatorMode.NORMAL,
210
224
  description="Estimator mode: 'normal' = normal run, 'hpo' = hyperparameter optimization, 'ensemble' = ensemble learning.",
211
225
  )
226
+ search_space: Path | None = Field(
227
+ default=None,
228
+ description="YAML defining the HPO search space. If omitted, hpo_search_space from model's MLProject is used.",
229
+ )
212
230
  metric: str | None = Field(
213
231
  default=None,
214
232
  description="Metric used for HPO or ensemble. Default will be used if none provided. Ignored in normal mode.",
215
233
  )
216
234
  searcher: SearcherType | None = Field(
217
235
  default=None,
218
- description="Searcher used for HPO. If not provided, a default RandomSearcher will be used. Ignored in normal and ensemble modes.",
236
+ description="Searcher used for HPO. If not provided, a default TPESearcher will be used. Ignored in normal and ensemble modes.",
237
+ )
238
+ max_trials: int | None = Field(
239
+ default=None,
240
+ gt=0,
241
+ description="Maximum HPO trials. If omitted, grid search is exhaustive while random/TPE use the default HPO trial count.",
242
+ )
243
+ seed: int | None = Field(
244
+ default=None,
245
+ description="Random seed used by stochastic search strategies.",
219
246
  )
@@ -291,7 +291,6 @@ def _discover_plots():
291
291
  from chap_core.assessment.backtest_plots import (
292
292
  evaluation_plot,
293
293
  horizon_location_grid,
294
- metrics_dashboard,
295
294
  predicted_vs_actual_linear_plot,
296
295
  predicted_vs_actual_plot,
297
296
  sample_bias_plot,
@@ -0,0 +1,167 @@
1
+ """Database-backed facet dimensions for backtest plots.
2
+
3
+ These dimensions know how to (a) enumerate their available coordinate values with
4
+ cheap database queries and (b) translate a requested coordinate into SQL filters,
5
+ so a single subplot can be served by loading only the matching forecast rows
6
+ instead of flattening the entire backtest in memory.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from sqlalchemy import false, tuple_
14
+ from sqlmodel import select
15
+
16
+ from chap_core.assessment.backtest_plots import FacetDimension
17
+ from chap_core.assessment.flat_representations import (
18
+ FlatForecasts,
19
+ FlatObserved,
20
+ convert_backtest_observations_to_flat_observations,
21
+ convert_backtest_to_flat_forecasts,
22
+ horizon_diff,
23
+ )
24
+ from chap_core.database.dataset_manager import DataSetManager
25
+ from chap_core.database.tables import BacktestForecast
26
+ from chap_core.plotting.backtest_plot import clean_time
27
+ from chap_core.time_period import TimePeriod
28
+
29
+ if TYPE_CHECKING:
30
+ from sqlalchemy.sql.elements import ColumnElement
31
+ from sqlmodel import Session
32
+
33
+ from chap_core.assessment.evaluation import FlatEvaluationData
34
+ from chap_core.database.tables import Backtest
35
+
36
+
37
+ class DBFacetDimension(FacetDimension):
38
+ """A facet dimension whose coordinates and filters can be served from the database."""
39
+
40
+ def distinct_values(self, session: Session, backtest: Backtest) -> list[Any]:
41
+ """Coordinate values available for this dimension, in display form."""
42
+ raise NotImplementedError
43
+
44
+ def forecast_filters(self, value: Any, session: Session, backtest: Backtest) -> list[ColumnElement]:
45
+ """SQL clauses narrowing `BacktestForecast` to the rows for `value`."""
46
+ return []
47
+
48
+
49
+ class LocationDimension(DBFacetDimension):
50
+ """Facet by org unit (`location`), matching `BacktestForecast.org_unit` directly."""
51
+
52
+ def distinct_values(self, session: Session, backtest: Backtest) -> list[Any]:
53
+ if backtest.org_units:
54
+ return sorted(backtest.org_units)
55
+ rows = session.exec(
56
+ select(BacktestForecast.org_unit).where(BacktestForecast.backtest_id == backtest.id).distinct()
57
+ ).all()
58
+ return sorted(rows)
59
+
60
+ def forecast_filters(self, value: Any, session: Session, backtest: Backtest) -> list[ColumnElement]:
61
+ return [BacktestForecast.org_unit == value]
62
+
63
+
64
+ def _split_display(last_seen_period: str) -> str:
65
+ """The split-period coordinate a forecast with this `last_seen_period` falls under.
66
+
67
+ Mirrors the plot's `clean_time(time_period - horizon * delta)`, which reduces to
68
+ `clean_time(last_seen_period - delta)`.
69
+ """
70
+ tp = TimePeriod.parse(last_seen_period)
71
+ return str(clean_time((tp - tp.time_delta).to_string()))
72
+
73
+
74
+ def _distinct_last_seen(session: Session, backtest: Backtest) -> list[str]:
75
+ return list(
76
+ session.exec(
77
+ select(BacktestForecast.last_seen_period).where(BacktestForecast.backtest_id == backtest.id).distinct()
78
+ ).all()
79
+ )
80
+
81
+
82
+ class SplitPeriodDimension(DBFacetDimension):
83
+ """Facet by split period, derived from `BacktestForecast.last_seen_period`."""
84
+
85
+ def distinct_values(self, session: Session, backtest: Backtest) -> list[Any]:
86
+ return sorted({_split_display(ls) for ls in _distinct_last_seen(session, backtest)})
87
+
88
+ def forecast_filters(self, value: Any, session: Session, backtest: Backtest) -> list[ColumnElement]:
89
+ matching = [ls for ls in _distinct_last_seen(session, backtest) if _split_display(ls) == value]
90
+ if not matching:
91
+ return [false()]
92
+ return [BacktestForecast.last_seen_period.in_(matching)] # type: ignore[attr-defined]
93
+
94
+
95
+ def _distinct_period_pairs(session: Session, backtest: Backtest) -> list[tuple[str, str]]:
96
+ rows = session.exec(
97
+ select(BacktestForecast.period, BacktestForecast.last_seen_period)
98
+ .where(BacktestForecast.backtest_id == backtest.id)
99
+ .distinct()
100
+ ).all()
101
+ return [(str(period), str(last_seen)) for period, last_seen in rows]
102
+
103
+
104
+ class HorizonDistanceDimension(DBFacetDimension):
105
+ """Facet by 1-based horizon distance.
106
+
107
+ Coordinates run `1..max_horizon_distance` when the backtest records it, falling
108
+ back to deriving distinct horizons from the forecasts for legacy rows.
109
+ """
110
+
111
+ def distinct_values(self, session: Session, backtest: Backtest) -> list[Any]:
112
+ if backtest.max_horizon_distance is not None:
113
+ return list(range(1, backtest.max_horizon_distance + 1))
114
+ horizons = {horizon_diff(period, last_seen) for period, last_seen in _distinct_period_pairs(session, backtest)}
115
+ return sorted(horizons)
116
+
117
+ def forecast_filters(self, value: Any, session: Session, backtest: Backtest) -> list[ColumnElement]:
118
+ pairs = [
119
+ (period, last_seen)
120
+ for period, last_seen in _distinct_period_pairs(session, backtest)
121
+ if horizon_diff(period, last_seen) == int(value)
122
+ ]
123
+ if not pairs:
124
+ return [false()]
125
+ return [tuple_(BacktestForecast.period, BacktestForecast.last_seen_period).in_(pairs)] # type: ignore[arg-type]
126
+
127
+
128
+ def load_filtered_flat_data(
129
+ session: Session,
130
+ backtest: Backtest,
131
+ coords: dict[str, Any],
132
+ dimensions: list[FacetDimension],
133
+ ) -> FlatEvaluationData:
134
+ """Load only the forecast/observation rows matching `coords`, as flat frames.
135
+
136
+ Coordinates whose dimension is a `DBFacetDimension` are pushed down to SQL; any
137
+ other coordinate is ignored here and left to the plot's in-memory filter.
138
+ """
139
+ from chap_core.assessment.evaluation import FlatEvaluationData
140
+
141
+ dim_by_name = {d.clean_name: d for d in dimensions if isinstance(d, DBFacetDimension)}
142
+
143
+ forecast_clauses: list[Any] = [BacktestForecast.backtest_id == backtest.id]
144
+ for name, value in coords.items():
145
+ dim = dim_by_name.get(name)
146
+ if dim is None:
147
+ continue
148
+ forecast_clauses += dim.forecast_filters(value, session, backtest)
149
+
150
+ forecasts = session.exec(select(BacktestForecast).where(*forecast_clauses)).all()
151
+
152
+ # Observations narrow only by org unit (any location coordinate); other dimensions
153
+ # filter the forecasts and rely on the plot's in-memory filter for observations.
154
+ org_units = [value for name, value in coords.items() if isinstance(dim_by_name.get(name), LocationDimension)]
155
+ observations = DataSetManager(session).observations(
156
+ backtest.dataset_id,
157
+ org_units=org_units or None,
158
+ feature_names=["disease_cases"],
159
+ )
160
+
161
+ forecasts_df = convert_backtest_to_flat_forecasts(list(forecasts))
162
+ observations_df = convert_backtest_observations_to_flat_observations(list(observations))
163
+
164
+ return FlatEvaluationData(
165
+ forecasts=FlatForecasts.validate(forecasts_df),
166
+ observations=FlatObserved.validate(observations_df),
167
+ )
@@ -10,6 +10,7 @@ import numpy as np
10
10
  import pandas as pd
11
11
 
12
12
  from chap_core.assessment.backtest_plots import ChartType, FacetDimension, FacetedBacktestPlot, backtest_plot
13
+ from chap_core.assessment.backtest_plots.db_dimensions import LocationDimension, SplitPeriodDimension
13
14
  from chap_core.plotting.backtest_plot import clean_time
14
15
  from chap_core.time_period import TimePeriod
15
16
 
@@ -57,8 +58,8 @@ class EvaluationPlot(FacetedBacktestPlot):
57
58
  """Shows forecasts with uncertainty bands and observed values with historical context."""
58
59
 
59
60
  facet_dimensions: list[FacetDimension] = [
60
- FacetDimension(field_name="split_period:O", display_name="Split Period"),
61
- FacetDimension(field_name="location:N", display_name="Location"),
61
+ SplitPeriodDimension(field_name="split_period:O", display_name="Split Period"),
62
+ LocationDimension(field_name="location:N", display_name="Location"),
62
63
  ]
63
64
 
64
65
  def _preprocess(
@@ -9,7 +9,8 @@ line charts stacked below.
9
9
  import altair as alt
10
10
  import pandas as pd
11
11
 
12
- from chap_core.assessment.backtest_plots import BacktestPlotBase, ChartType, backtest_plot
12
+ from chap_core.assessment.backtest_plots import ChartType, FacetDimension, FacetedBacktestPlot, backtest_plot
13
+ from chap_core.assessment.backtest_plots.db_dimensions import HorizonDistanceDimension, LocationDimension
13
14
  from chap_core.assessment.backtest_plots.evaluation_plot import _compute_quantiles_from_forecasts
14
15
  from chap_core.assessment.flat_representations import FlatForecasts, FlatObserved
15
16
  from chap_core.assessment.metrics.crps import CRPSLog1pMetric, CRPSMetric
@@ -22,13 +23,13 @@ CELL_HEIGHT = 150
22
23
  METRIC_HEIGHT = 60
23
24
 
24
25
 
25
- def _empty_placeholder(width: int, height: int, title: str = "") -> ChartType:
26
- """Return an empty chart placeholder that won't trip vegafusion's type checks."""
26
+ def _empty_placeholder(width: int, height: int) -> ChartType:
27
+ """Return an empty chart placeholder that won't trip Altair/Vega type checks."""
27
28
  return ( # type: ignore[no-any-return]
28
29
  alt.Chart(pd.DataFrame({"x": [0]}))
29
30
  .mark_point(opacity=0)
30
31
  .encode(x=alt.value(0), y=alt.value(0))
31
- .properties(width=width, height=height, title=title)
32
+ .properties(width=width, height=height)
32
33
  )
33
34
 
34
35
 
@@ -48,7 +49,7 @@ def _build_forecast_cell(
48
49
  obs = obs[obs["time_period"].isin(fq["time_period"])]
49
50
 
50
51
  if fq.empty:
51
- return _empty_placeholder(CELL_WIDTH, CELL_HEIGHT, f"{location} | horizon {horizon}")
52
+ return _empty_placeholder(CELL_WIDTH, CELL_HEIGHT)
52
53
 
53
54
  base_forecast = alt.Chart(fq)
54
55
 
@@ -70,7 +71,7 @@ def _build_forecast_cell(
70
71
  layers = error_10_90 + error_25_75 + median_line
71
72
 
72
73
  # Only add observations layer if there is data; an empty DataFrame with
73
- # object-typed columns causes vegafusion to fail on temporal encoding.
74
+ # object-typed columns causes Altair/Vega to fail on temporal encoding.
74
75
  if not obs.empty:
75
76
  obs_line = (
76
77
  alt.Chart(obs)
@@ -82,9 +83,7 @@ def _build_forecast_cell(
82
83
  )
83
84
  layers = layers + obs_line
84
85
 
85
- return layers.properties( # type: ignore[no-any-return]
86
- width=CELL_WIDTH, height=CELL_HEIGHT, title=f"{location} | horizon {horizon}"
87
- )
86
+ return layers.properties(width=CELL_WIDTH, height=CELL_HEIGHT) # type: ignore[no-any-return]
88
87
 
89
88
 
90
89
  def _build_metric_chart(
@@ -152,68 +151,136 @@ def _build_summary_table(
152
151
  )
153
152
 
154
153
 
154
+ def _metric_instances(historical_observations: pd.DataFrame | None) -> list:
155
+ """The metrics shown in each grid cell, in display order."""
156
+ return [
157
+ CRPSMetric(historical_observations=historical_observations),
158
+ CRPSLog1pMetric(historical_observations=historical_observations),
159
+ WinklerScore10_90Metric(historical_observations=historical_observations),
160
+ WinklerScore10_90Log1pMetric(historical_observations=historical_observations),
161
+ OutbreakAccuracyMetric(historical_observations=historical_observations),
162
+ ]
163
+
164
+
165
+ def _metric_dfs_from_preprocessed(df: pd.DataFrame) -> list[tuple[str, pd.DataFrame]]:
166
+ """Recover the per-metric frames from the packed preprocessed frame, in order."""
167
+ metric_rows = df[df["role"] == "metric"]
168
+ return [(str(name), group) for name, group in metric_rows.groupby("metric_name", sort=False)]
169
+
170
+
155
171
  @backtest_plot(
156
172
  plot_id="horizon_location_grid",
157
173
  name="Forecast Grid (Locations x Horizons)",
158
174
  description="Grid of forecast intervals and metrics across locations and forecast horizons.",
159
175
  needs_historical=True,
160
176
  )
161
- class HorizonLocationGridPlot(BacktestPlotBase):
162
- """Grid plot with locations as rows and forecast horizons as columns."""
177
+ class HorizonLocationGridPlot(FacetedBacktestPlot):
178
+ """Grid plot with locations as rows and forecast horizons as columns.
179
+
180
+ Each cell is a composite (forecast band + observed + per-metric line charts),
181
+ so the all-cells view (`get_full_plot`) keeps the hand-built grid, while
182
+ `facet_coords` / `get_subplot` expose a single location x horizon cell for the
183
+ database-backed faceting workflow.
184
+ """
163
185
 
164
- def plot(
186
+ facet_dimensions: list[FacetDimension] = [
187
+ HorizonDistanceDimension(field_name="horizon_distance:O", display_name="Horizon Distance"),
188
+ LocationDimension(field_name="location:N", display_name="Location"),
189
+ ]
190
+
191
+ def _preprocess(
165
192
  self,
166
193
  observations: pd.DataFrame,
167
194
  forecasts: pd.DataFrame,
168
195
  historical_observations: pd.DataFrame | None = None,
169
- ) -> ChartType:
170
- flat_obs = FlatObserved(observations)
171
- flat_fc = FlatForecasts(forecasts)
196
+ ) -> pd.DataFrame:
197
+ """Pack forecast quantiles, observed values and metric series into one frame.
198
+
199
+ Each row carries `location` + `horizon_distance` (observations replicated
200
+ across horizons) so the generic coordinate filter can slice to one cell, plus
201
+ a `role` discriminator the cell renderer splits back out.
202
+ """
203
+ # No forecasts for this coordinate (e.g. a location x horizon cell with no
204
+ # data): skip metric computation and let the cell render as a placeholder.
205
+ if forecasts.empty:
206
+ return pd.DataFrame(columns=["location", "time_period", "horizon_distance", "role"])
207
+
208
+ flat_obs = FlatObserved.validate(observations)
209
+ flat_fc = FlatForecasts.validate(forecasts)
210
+ horizons = sorted(forecasts["horizon_distance"].unique())
172
211
 
173
- # Compute quantiles for forecast cells
174
212
  forecast_quantiles = _compute_quantiles_from_forecasts(forecasts)
175
213
  forecast_quantiles["time_period"] = forecast_quantiles["time_period"].apply(clean_time)
214
+ forecast_quantiles["role"] = "forecast"
176
215
 
177
216
  obs_df = observations.copy()
178
217
  obs_df["time_period"] = obs_df["time_period"].apply(clean_time)
218
+ # Observations have no horizon; replicate across horizons so the cell filter keeps them.
219
+ obs_df = obs_df.merge(pd.DataFrame({"horizon_distance": horizons}), how="cross")
220
+ obs_df["role"] = "observed"
179
221
 
180
- locations = sorted(forecasts["location"].unique())
181
- horizons = sorted(forecasts["horizon_distance"].unique())
182
-
183
- # Compute metrics
184
- metric_instances = [
185
- CRPSMetric(historical_observations=historical_observations),
186
- CRPSLog1pMetric(historical_observations=historical_observations),
187
- WinklerScore10_90Metric(historical_observations=historical_observations),
188
- WinklerScore10_90Log1pMetric(historical_observations=historical_observations),
189
- OutbreakAccuracyMetric(historical_observations=historical_observations),
190
- ]
191
-
192
- metric_dfs: list[tuple[str, pd.DataFrame]] = []
193
- for m in metric_instances:
222
+ frames: list[pd.DataFrame] = [forecast_quantiles, obs_df]
223
+ for m in _metric_instances(historical_observations):
194
224
  if not m.is_applicable(flat_obs):
195
225
  continue
196
226
  detailed = m.get_detailed_metric(flat_obs, flat_fc)
197
227
  detailed["time_period"] = detailed["time_period"].apply(clean_time)
198
- metric_dfs.append((m.get_name(), detailed))
228
+ detailed["role"] = "metric"
229
+ detailed["metric_name"] = m.get_name()
230
+ frames.append(detailed)
231
+
232
+ return pd.concat(frames, ignore_index=True)
233
+
234
+ def _plot(self, df: pd.DataFrame) -> ChartType:
235
+ """Render one location x horizon cell from the packed frame."""
236
+ if df.empty:
237
+ return _empty_placeholder(CELL_WIDTH, CELL_HEIGHT)
238
+
239
+ forecast_rows = df[df["role"] == "forecast"]
240
+ observed_rows = df[df["role"] == "observed"]
241
+ key_src = forecast_rows if not forecast_rows.empty else df
242
+ location = key_src["location"].iloc[0]
243
+ horizon = int(key_src["horizon_distance"].iloc[0])
244
+
245
+ cell_parts: list[ChartType] = [_build_forecast_cell(forecast_rows, observed_rows, location, horizon)]
246
+ for metric_name, mdf in _metric_dfs_from_preprocessed(df):
247
+ cell_parts.append(_build_metric_chart(mdf, location, horizon, metric_name))
248
+
249
+ return alt.vconcat(*cell_parts).properties(spacing=2) # type: ignore[no-any-return]
250
+
251
+ def get_full_plot(
252
+ self,
253
+ observations: pd.DataFrame,
254
+ forecasts: pd.DataFrame,
255
+ historical_observations: pd.DataFrame | None = None,
256
+ ) -> ChartType:
257
+ """The full grid: composite cells (locations as rows, horizons as columns) plus a summary table.
258
+
259
+ Overrides the base native-faceting implementation, which cannot tile the
260
+ composite (vconcat) cells this plot renders.
261
+ """
262
+ df = self._preprocess(observations, forecasts, historical_observations)
263
+ locations = sorted(forecasts["location"].unique())
264
+ horizons = sorted(forecasts["horizon_distance"].unique())
199
265
 
200
- # Build grid: rows = locations, columns = horizons
201
266
  location_rows: list[ChartType] = []
202
267
  for loc in locations:
203
- horizon_cells = []
204
- for hz in horizons:
205
- cell_parts: list[ChartType] = [_build_forecast_cell(forecast_quantiles, obs_df, loc, hz)]
206
- for metric_name, mdf in metric_dfs:
207
- cell_parts.append(_build_metric_chart(mdf, loc, hz, metric_name))
208
- horizon_cells.append(alt.vconcat(*cell_parts).properties(spacing=2))
268
+ # The grid layout is the only thing identifying a cell, so label each
269
+ # one here; the single-cell subplot view leaves identification to the
270
+ # frontend's facet selection.
271
+ horizon_cells = [
272
+ self._plot(df[(df["location"] == loc) & (df["horizon_distance"] == hz)]).properties(
273
+ title=f"{loc} | horizon {hz}"
274
+ )
275
+ for hz in horizons
276
+ ]
209
277
  location_rows.append(alt.hconcat(*horizon_cells).properties(spacing=10))
210
278
 
211
- # Aggregated summary tables
279
+ metric_dfs = _metric_dfs_from_preprocessed(df)
212
280
  if metric_dfs:
213
- summary = _build_summary_table(metric_dfs, horizons)
214
- location_rows.append(summary)
281
+ location_rows.append(_build_summary_table(metric_dfs, horizons))
215
282
 
216
- return alt.vconcat(*location_rows).properties( # type: ignore[return-value]
283
+ return alt.vconcat(*location_rows).properties( # type: ignore[no-any-return]
217
284
  spacing=15,
218
285
  title="Forecast Grid: Locations x Horizons",
219
286
  )
@@ -8,10 +8,10 @@ over- and under-prediction at high case counts immediately visible.
8
8
 
9
9
  import pandas as pd
10
10
 
11
- from chap_core.assessment.backtest_plots import BacktestPlotBase, ChartType, backtest_plot
11
+ from chap_core.assessment.backtest_plots import ChartType, backtest_plot
12
12
  from chap_core.assessment.backtest_plots.predicted_vs_actual_plot import (
13
+ PredictedVsActualPlot,
13
14
  build_predicted_vs_actual_chart,
14
- median_forecasts_joined_with_observations,
15
15
  )
16
16
 
17
17
 
@@ -20,16 +20,16 @@ from chap_core.assessment.backtest_plots.predicted_vs_actual_plot import (
20
20
  name="Predicted vs Actual (linear)",
21
21
  description="Scatter plots of predicted (median) vs actual values on linear axes, faceted by horizon and colored by location.",
22
22
  )
23
- class PredictedVsActualLinearPlot(BacktestPlotBase):
24
- def plot(
25
- self,
26
- observations: pd.DataFrame,
27
- forecasts: pd.DataFrame,
28
- historical_observations: pd.DataFrame | None = None,
29
- ) -> ChartType:
30
- merged = median_forecasts_joined_with_observations(forecasts, observations)
23
+ class PredictedVsActualLinearPlot(PredictedVsActualPlot):
24
+ """Linear-axes variant of :class:`PredictedVsActualPlot`.
25
+
26
+ Inherits the horizon faceting and the median-vs-observed preprocessing; only
27
+ the per-facet chart differs (raw values instead of log1p-transformed axes).
28
+ """
29
+
30
+ def _plot(self, data: pd.DataFrame) -> ChartType:
31
31
  return build_predicted_vs_actual_chart(
32
- merged,
32
+ data,
33
33
  x_field="median_forecast",
34
34
  y_field="disease_cases",
35
35
  title="Predicted vs Actual (linear scale)",
@@ -10,6 +10,7 @@ import numpy as np
10
10
  import pandas as pd
11
11
 
12
12
  from chap_core.assessment.backtest_plots import ChartType, FacetDimension, FacetedBacktestPlot, backtest_plot
13
+ from chap_core.assessment.backtest_plots.db_dimensions import HorizonDistanceDimension
13
14
  from chap_core.plotting.backtest_plot import clean_time
14
15
 
15
16
 
@@ -93,7 +94,7 @@ def build_predicted_vs_actual_chart(
93
94
  )
94
95
  class PredictedVsActualPlot(FacetedBacktestPlot):
95
96
  facet_dimensions: list[FacetDimension] = [
96
- FacetDimension(field_name="horizon_distance:O", display_name="Horizon Distance"),
97
+ HorizonDistanceDimension(field_name="horizon_distance:O", display_name="Horizon Distance"),
97
98
  ]
98
99
 
99
100
  resolve_scale_x = "shared"
@@ -0,0 +1,91 @@
1
+ """
2
+ Sample bias plots for backtests.
3
+
4
+ This module provides backtest plots that show the ratio of forecast samples
5
+ above truth, one by horizon distance and one by time period.
6
+ """
7
+
8
+ import altair as alt
9
+ import pandas as pd
10
+
11
+ from chap_core.assessment.backtest_plots import ChartType, FacetDimension, FacetedBacktestPlot, backtest_plot
12
+ from chap_core.assessment.flat_representations import FlatForecasts, FlatObserved
13
+ from chap_core.assessment.metrics import RatioAboveTruthMetric
14
+
15
+ _BIAS_DESCRIPTION = (
16
+ "Shows biases in the samples returned by the model, specifically whether the samples "
17
+ "generally are over or under the true observation. This can be used to assess model "
18
+ "calibration and tendency to over- or under-predict."
19
+ )
20
+
21
+
22
+ class SampleBiasPlotBase(FacetedBacktestPlot):
23
+ """
24
+ Shared preprocessing for the sample bias plots.
25
+
26
+ Computes the ratio of forecast samples that are above the true observation
27
+ value per forecast row. The plots are aggregates with no per-coordinate
28
+ decomposition, so they declare no facet dimensions; they conform to the
29
+ faceting interface only so every registered plot shares the same shape.
30
+ """
31
+
32
+ facet_dimensions: list[FacetDimension] = []
33
+
34
+ def _preprocess(
35
+ self,
36
+ observations: pd.DataFrame,
37
+ forecasts: pd.DataFrame,
38
+ historical_observations: pd.DataFrame | None = None,
39
+ ) -> pd.DataFrame:
40
+ """Compute the ratio-of-samples-above-truth metric per forecast row."""
41
+ flat_observations = FlatObserved.validate(observations)
42
+ flat_forecasts = FlatForecasts.validate(forecasts)
43
+ metric = RatioAboveTruthMetric()
44
+ return metric.get_detailed_metric(flat_observations, flat_forecasts)
45
+
46
+
47
+ @backtest_plot(
48
+ plot_id="sample_bias_by_horizon",
49
+ name="Sample Bias by Horizon",
50
+ description=f"Ratio of forecast samples above truth per forecast horizon. {_BIAS_DESCRIPTION}",
51
+ )
52
+ class SampleBiasByHorizonPlot(SampleBiasPlotBase):
53
+ """Bar chart of the mean ratio of samples above truth per forecast horizon."""
54
+
55
+ def _plot(self, metric_df: pd.DataFrame) -> ChartType:
56
+ horizon_df = metric_df.groupby(["horizon_distance"]).agg({"metric": "mean"}).reset_index()
57
+ return ( # type: ignore[no-any-return]
58
+ alt.Chart(horizon_df)
59
+ .mark_bar()
60
+ .encode(
61
+ x=alt.X("horizon_distance:O", title="Horizon (periods ahead)"),
62
+ y=alt.Y("metric:Q", title="Ratio of samples above truth"),
63
+ tooltip=["horizon_distance", "metric"],
64
+ )
65
+ .properties(height=300)
66
+ )
67
+
68
+
69
+ @backtest_plot(
70
+ plot_id="sample_bias_by_time_period",
71
+ name="Sample Bias by Time Period",
72
+ description=(
73
+ f"Ratio of forecast samples above truth over time periods, one line per location. {_BIAS_DESCRIPTION}"
74
+ ),
75
+ )
76
+ class SampleBiasByTimePeriodPlot(SampleBiasPlotBase):
77
+ """Line chart of the mean ratio of samples above truth per time period and location."""
78
+
79
+ def _plot(self, metric_df: pd.DataFrame) -> ChartType:
80
+ time_df = metric_df.groupby(["time_period", "location"]).agg({"metric": "mean"}).reset_index()
81
+ return ( # type: ignore[no-any-return]
82
+ alt.Chart(time_df)
83
+ .mark_line(point=True)
84
+ .encode(
85
+ x=alt.X("time_period:O", title="Time period"),
86
+ y=alt.Y("metric:Q", title="Ratio of samples above truth"),
87
+ color=alt.Color("location:N", title="Location"),
88
+ tooltip=["time_period", "location", "metric"],
89
+ )
90
+ .properties(height=300)
91
+ )