Skip to content

Add aspirational flag to FlowInputs #140

@zmek

Description

@zmek

Problem

The ed_yta flow can be aspirational or empirical, but once its Poisson lambda is stored in a FlowInputs object that provenance is lost. There is no programmatic way for downstream code to discover whether a given flow was generated under aspirational assumptions.

What "aspirational" means in this codebase

When the yet-to-arrive model is a ParametricIncomingAdmissionPredictor, the Poisson mean for ed_yta is computed as:

μ = Σ_t  λ_t × θ_t

where λ_t is the historical arrival rate per time-slice and θ_t comes from get_y_from_aspirational_curve(time_remaining, x1, y1, x2, y2). The x1/y1/x2/y2 parameters encode target ED performance (e.g. 76% admitted within 4 hours, 99% within 12 hours). This means the predicted admission count reflects demand as if ED performance targets are met, not demand as historically observed.

When the model is an EmpiricalIncomingAdmissionPredictor, θ_t comes from a fitted survival curve instead, and the prediction reflects actual historical admission timing. The two approaches produce different lambdas for the same snapshot, but FlowInputs stores only the resulting float — the distinction is invisible.

Where the distinction matters today

_create_flow_inputs in service.py builds the ed_yta FlowInputs by calling yet_to_arrive_model.predict_mean(...). Whether x1/y1/x2/y2 affect the result depends on the model type, but the FlowInputs object is identical either way:

"ed_yta": FlowInputs(
    flow_id="ed_yta",
    flow_type="poisson",
    distribution=float(
        yet_to_arrive_model.predict_mean(
            prediction_context, x1=x1, y1=y1, x2=x2, y2=y2
        )
        ...
    ),
    display_name="ED yet-to-arrive admissions",
),

The ed_current flow is also partially affected: _prepare_base_probabilities uses the aspirational curve (via calculate_probability) to compute prob_admission_in_window when the YTA model is parametric, and the survival curve (via calculate_admission_probability_from_survival_curve) when it is empirical. This weights each current ED patient's contribution to the ed_current PMF. However, the primary concern is ed_yta, because:

  • ed_current can still be evaluated per-patient (we observe which current patients were admitted)
  • ed_yta predicts aggregate demand from patients not yet present, and the aspirational version predicts a different number than what will actually be observed

Why this blocks evaluation

get_prob_dist_by_service compares the composed prediction against observed admissions via _count_observed_admissions. When ed_yta is aspirational, this comparison is invalid — the predicted distribution expresses demand-under-targets while the observed count reflects actual performance. Notebook 4d calls this out in prose:

"if the emergency flows are aspirational (expressing demand as if ED 4-hour targets are met) they cannot be directly evaluated against observed numbers of admissions without careful thought"

But neither get_prob_dist_by_service nor PredictionBundle can detect this condition. Consumers must hardcode knowledge that ed_yta (when produced by the parametric model) is aspirational, which is fragile and prevents a general-purpose evaluation loop.

Proposed change

1. Add aspirational: bool = False to FlowInputs

@dataclass(frozen=True)
class FlowInputs:
    flow_id: str
    flow_type: str
    distribution: Union[np.ndarray, float]
    display_name: Optional[str] = None
    aspirational: bool = False

2. Set it in _create_flow_inputs

ed_yta should be marked aspirational when the yet-to-arrive model is a ParametricIncomingAdmissionPredictor. The model type is already available via the yet_to_arrive_model parameter:

"ed_yta": FlowInputs(
    flow_id="ed_yta",
    flow_type="poisson",
    distribution=float(...),
    display_name="ED yet-to-arrive admissions",
    aspirational=isinstance(yet_to_arrive_model, ParametricIncomingAdmissionPredictor),
),

All other flows (ed_current, non_ed_yta, elective_yta, transfers, departures) default to False.

Note on ed_current: the admission-in-window weighting for current ED patients also depends on the model type (aspirational curve vs survival curve), but ed_current is a PMF derived from per-patient probabilities, not a Poisson rate. Its evaluation against observed per-patient admissions remains valid regardless. If this is later considered a concern, ed_current could also be flagged, but ed_yta is the clear first priority.

3. Expose is_aspirational on PredictionBundle

DemandPredictor.predict_service has access to the ServicePredictionInputs and the FlowSelection. It can inspect the included inflows to determine whether any aspirational flow contributed to the arrivals distribution:

@property
def is_aspirational(self) -> bool:
    """True if any inflow contributing to this prediction is aspirational."""
    ...

This could be implemented in two ways:

  • (a) DemandPredictor.predict_service computes it and passes it as a new field on PredictionBundle (requires adding the field).
  • (b) PredictionBundle stores the ServicePredictionInputs and derives it on access (keeps prediction logic out of the bundle).

Option (a) is simpler and avoids storing the full inputs on the bundle.

Downstream usage

get_prob_dist_by_service

With PredictionBundle.is_aspirational available, the function can:

  • Warn when evaluating an aspirational bundle against observed counts, making the comparison's invalidity explicit rather than silent.
  • Skip aspirational bundles by default (with an opt-in include_aspirational=True parameter) so that the standard EPUDD / PIT / delta evaluation loop only runs on flows where observed-vs-predicted comparison is valid.
  • Select appropriate evaluation in future: arrival rate comparisons for aspirational flows (as in notebook 3f), EPUDD for non-aspirational.

Notebook 4d

The evaluation notebook currently notes the caveat manually. With the flag, the code can:

bundle = predictor.predict_service(inputs=service_data[svc], flow_selection=fs)
if bundle.is_aspirational:
    # Use arrival-rate comparison instead of EPUDD
    ...
else:
    # Standard observed-vs-predicted evaluation
    ...

Display / reporting

FlowInputs.get_display_name() or ServicePredictionInputs.__repr__ could annotate aspirational flows (e.g. "ED yet-to-arrive admissions (aspirational)"), making the assumption visible when inspecting predictions.

Scope

  • This is a metadata addition — no prediction logic changes.
  • All existing tests and notebooks continue to work unchanged (aspirational defaults to False).
  • The flag enables downstream improvements (evaluation gating, display annotation) but does not implement them; those are separate follow-up items.

Files to change

File Change Risk
src/patientflow/predict/service.py Add aspirational field to FlowInputs; set True for ed_yta when parametric Low — additive, default False
src/patientflow/predict/types.py Add is_aspirational to PredictionBundle Low — additive
src/patientflow/predict/demand.py Pass aspirational info through when building PredictionBundle Low

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions