Skip to content

Add observation-based scoring for filters - #296

Open
mattlevine22 wants to merge 9 commits into
mainfrom
ml-filter-scoring-2
Open

Add observation-based scoring for filters#296
mattlevine22 wants to merge 9 commits into
mainfrom
ml-filter-scoring-2

Conversation

@mattlevine22

@mattlevine22 mattlevine22 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Replaces #259 (codex 1 shot re-implementation of #259 using more recent main as starting point).

Trying to get this up in time for @baptistar's summer school lectures at Duke!

Summary

  • Adds scoring of one-step-ahead predictive observation distributions for continuous-time KF, EKF, UKF, and EnKF.
  • Includes Gaussian log probability, Dawid–Sebastiani, observation-wise CRPS, and energy scores.
  • Structures the implementation into:
    • reusable score definitions under dynestyx.evaluation;
    • filter-specific scoring configuration;
    • a canonical predicted-observation representation that translates CD-Dynamax outputs;
    • Filter/ConditionedResult integration and optional NumPyro sites.
  • Adds API documentation and Tutorial 12, which compares EKF, UKF, and EnKF using per-time scores and their temporal mean/SD.
  • Moves plotting utilities from dynestyx.diagnostics to dynestyx.evaluation.

Reflections

  • It would be useful to include a high-particle PF reference, but neither Dynestyx nor CD-Dynamax currently exposes the required pre-update predictive particles and weights. Post-update particles have already incorporated the observation being scored and therefore are not a valid substitute.
  • Can we really justify ObservationScoringConfig over directly passing around a Scoring object? Would make hard-coding a scorer easier...but maybe we need a lot of this metadata, so it is better to do the Config style approach?

@mattlevine22
mattlevine22 marked this pull request as ready for review August 5, 2026 19:24
@mattlevine22
mattlevine22 requested a review from DanWaxman August 5, 2026 19:24
@DanWaxman

Copy link
Copy Markdown
Collaborator

I find attaching the config to the filter this way a little clunky -- do you have thoughts on something like this? It's what I had settled on when thinking about some diagonistics.

diagnostic_config = DiagnosticConfig(
    n_particles=1_000,
    rng_seed=1337,
    compute_M_diagnostic=True,
    compute_U_diagnostic=True,
    compute_F_diagnostic=True,
    record_log_weights=True,
    record_target_particles=True,
)
with ComputeDiagnostics(diagnostic_config):
    with Filter(filter_config=filter_config):
        dsx.sample("f", dynamics, obs_times=obs_times, obs_values=obs_values)

One reason I find it a bit clunky is that we would have to duplicate some code across different inference context where we want to compute diagnostics -- filtering, smoothing, and LatentPathBuilder.

Another reason to prefer this is then the same object can be configured to automatically do things like "score at lead time" or something.

@mattlevine22

Copy link
Copy Markdown
Collaborator Author

Mostly agree, but we might want Filter to return different things depending on the diagnostic request.

@DanWaxman

DanWaxman commented Aug 6, 2026 via email

Copy link
Copy Markdown
Collaborator

@mattlevine22

mattlevine22 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Sounds good to me. something like:

with ComputeDiagnostics(observation_scoring_config=..., filter_scoring_config=...)

given that your F/U/M thing is different...and generally, we will have different diagnostics to run (including one just on parameter posterior sampling stuff for example)

@DanWaxman

DanWaxman commented Aug 6, 2026 via email

Copy link
Copy Markdown
Collaborator

@mattlevine22

Copy link
Copy Markdown
Collaborator Author

oh hmmm interesting, it is true that this is doing eval of the 1-step-ahead forecast.

I'm not really clear on the difference between "metrics" and "diagnostics", and I'm not convinced that it will be clear to a user either, so I think they probably ought to be packaged together (or we attach diagnostics to existing "runs", like Filter, automatically).

  • I get that a diagnostic is more like ESS in sampling land and a metric would be more like a suite of posterior-predictive checks (e.g., CRPS of forecast at lead time tau)

  • reminds me that eventually we should also start thinking about having held-out data too (automatic splits and metrics/diagnostics computed therein as well)

@DanWaxman

Copy link
Copy Markdown
Collaborator

I'm not really clear on the difference between "metrics" and "diagnostics", and I'm not convinced that it will be clear to a user either, so I think they probably ought to be packaged together (or we attach diagnostics to existing "runs", like Filter, automatically).

I think, generally speaking, diagnostics are indications that computation/approximation were okay. E.g., low ESS or spread-error ratio would indicate a poor filter performance, but say very little about actual performance (other than you might expect it to be bad). Metrics are a more direct evaluation of performance, but are less interpretable usually.

I would prefer to keep them separate, since they're somewhat semantically different. This is also in line with something like arviz, which keeps things like MCMC diagnostics separate from things like loo metrics. I would also prefer to not attach them to the filter, since you probably wouldn't want to compute these during expensive inference like MCMC, and computation of many are embarrassingly parallel. I view this as a post-processing step that the filter is not particularly aware of.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants