Skip to content
DHIS2.org Community GitHub

Modeling internals: where the modelling actually happens

chap_core is not where models are implemented. Models are developed and maintained independently, outside this repository; chap_core is the tooling around them — running them, evaluating them, and storing and serving the results.

If you are looking for the code that learns disease dynamics from data, it is not in this package. This page explains the boundary, and then — because the question keeps coming up — accounts for the ML-looking code you will nonetheless find in the tree.

The external boundary

Everything with real modeling weight lives outside chap_core. The production forecasting models (for example the DeepAR-based autoregressive models, and R/INLA models) are maintained in their own repositories and executed through a TrainPredictRunner implementation (Docker, UV, Conda, Renv, command-line, MLflow) or against a remote chapkit HTTP service (see Architecture diagrams). chap_core discovers them, feeds them data, runs them, scores what comes back, and persists it.

The plumbing that loads and runs them is documented in Code overview, and the reference/example models are catalogued in Running models in Chap. The two entry points into this shared core — the chap CLI and the REST API — are compared in CLI vs REST API.

Why there is still ML-looking code in the tree

A few things inside chap_core do use ML libraries. None of them are the forecasting models the platform exists to run: they are support machinery (covariate preparation, hyperparameter search, explainability) plus a handful of trivial baselines used in tests and as fast stand-ins.

The ML that does live inside chap_core is classical/statistical only. There is no deep learning in the package. Confirmed absent from chap_core: torch, tensorflow, jax, statsmodels, xgboost, lightgbm, pymc, prophet, numpyro, pyro, gpflow.

The direct ML-relevant dependencies chap_core actually uses:

Library Role in chap_core
scikit-learn climate predictor, Poisson baselines, KMeans clustering, LIME surrogate
optuna hyperparameter search (HPO)
gluonts evaluation (Evaluator) and data adaptors
scipy explainability (signal / distance helpers)
numpy / pandas all hand-written scoring metrics

Three categories

It helps to split the in-tree code into three buckets, because much of what looks like "ML in chap_core" is actually tooling around models, or evaluation statistics.

Category What it is Examples
A Genuinely ML — learns from data to predict climate predictor, Poisson/naive baselines, KMeans
B ML tooling — uses ML, but is not itself a forecaster HPO (optuna), LIME explainability
C Not ML — evaluation statistics or heuristics CRPS/Winkler/MAE metrics, preference learning

A — Genuinely ML (learns to predict)

  • chap_core/climate_predictor.py — fits one scikit-learn LinearRegression per (location, climate field) on a one-hot month/week feature matrix. This is the one in-tree model that is truly load-bearing: it fabricates the future climate covariates that a disease model needs before it can predict (the disease model never receives ground-truth future weather). It runs in both the real-prediction path (via forecast_with_predicted_weather) and the backtesting path (via QuickForecastFetcher).
  • chap_core/predictor/poisson.py, chap_core/predictor/naive_predictor.py — scikit-learn PoissonRegressor baselines (including a per-location, lagged-cases + one-hot-season variant). Mostly used in tests and example models.
  • chap_core/predictor/naive_estimator.py — NaiveEstimator: the database special-cases the configured model named naive_model and returns it as a fast, R-free stand-in. It predicts the per-location mean and draws Poisson samples — barely ML (essentially a statistic).
  • chap_core/models/builtin/ — built-in models: Python models that run in-process. A model subclasses BuiltinModel, sets a BuiltinModelSpec (whose version must change with any change to the model's output or spec) and is registered with @builtin_model(). BuiltinModelTemplate gives it the same interface as an MLproject or chapkit template, under the source URL builtin:<name>@<version> (stored templates run only if the version matches the code), so it runs through chap eval and the REST backtest and prediction paths alike (chap eval --model-name builtin:global_median ...). Every registered model is seeded at startup with its spec's role, and tests/test_builtin_models.py runs each one through all of these paths. The baselines, all with role baseline and all per location:
  • global_median: all past observations as forecast samples for every period.
  • seasonal_median: all past observations of the same month or week of the year (week 53 pooled with week 52). Fails if a forecast period of the year was never observed.
  • persistence: the last observed value, scaled by the changes over the same horizon and time of year in earlier years, so the spread widens with horizon.

All three use deterministic quantile samples whose two middle values are both the median, so the sample median is exactly the point forecast.

The REST API runs every live baseline once on each backtest specification (chap_core/rest_api/baseline_runs.py). A backtest queued through the API asks for them as soon as its specification is resolved, and reading a specification (GET /v1/crud/backtest-specifications/{id}) asks for any that are still missing, such as a new baseline version. Each run is a background job with a SpecificationBaseline row recording pending, done, not_applicable or failed. A baseline that raises BaselineNotApplicable for a specification's data is not applicable there and is not retried. A failed run, or one still pending 24 hours after it was queued, is queued again by the next trigger after those 24 hours.

The same read returns the baselines with their status and aggregate metrics, and compares each backtest's aggregate metrics with every baseline that is done, using the metric's comparison operator. With includeComparisonModels, the newest backtest of each comparison model on the specification is listed and compared with the same way. The response also lists every metric with its comparison operator and that operator's one-line description, registered with @comparison_op(name, description), so a client needs no second call. - chap_core/feature_generators/seasonality_cluster.py and chap_core/plotting/season_plot.py — scikit-learn KMeans seasonality clustering (unsupervised).

B — ML tooling (uses ML, but is not a forecaster)

  • chap_core/hpo/ — hyperparameter optimization built on optuna's TPE sampler (plus hand-rolled grid and random searchers). It tunes an external model template's hyperparameters against a backtest metric (default RMSE). It is CLI-only and not exposed in the REST API:
chap eval --estimator-options.mode=hpo ...
  • chap_core/explainability/ (lime.py, surrogate.py, distance.py) — post-hoc LIME explanations. It fits a scikit-learn Ridge local surrogate around an already-trained model to report feature importance. Note the nuance: LIME uses ML mechanics (it fits a regression) but is not a predictive model — it is interpretability tooling about a model, so under a strict "learns-to-predict" definition it does not count as an ML model.

C — Not ML (evaluation statistics / heuristics)

  • chap_core/assessment/metrics/ — the scoring metrics, all hand-implemented on numpy: a from-scratch order-statistic CRPS estimator, Winkler interval scores, MAE / RMSE / MAPE, percentile-coverage calibration, outbreak-detection sensitivity/specificity. This is substantive statistical code and the busiest ML-adjacent surface (the REST worker, the REST API router, the CLI, and HPO all route through it) — but it is scoring, not learning. See Evaluation pipeline and Creating custom metrics.
  • chap_core/preference_learning/ — despite a docstring mentioning Bayesian / bandit strategies, the only implementation is a plain tournament bracket that A/B-compares model configurations. No ML library; one CLI command. See Preference learning.
  • gluonts Evaluator usage in chap_core/assessment/prediction_evaluator.py — quantile-based evaluation plumbing, not a model.

One-line answer

The models are external. The only strictly-ML (learns-to-predict) code in chap_core is the scikit-learn climate-covariate predictor and the Poisson/naive baselines, plus KMeans clustering — support machinery and baselines, not the platform's forecasting models. HPO and LIME are ML tooling; the metrics and preference learning are statistics and heuristics.