---
title: "Timeliness against the clinical standard"
subtitle: "Protection referrals, 2024"
author: "Cassion · data-analysis.cassion.dev"
format:
  html:
    toc: true
    code-fold: false
jupyter: python3
---

## The question and why the clock matters

For a GBV survivor seeking health care, 72 hours is not an administrative target.
Post-exposure prophylaxis for HIV must start within 72 hours to work, and
emergency contraception has a similar window. A referral that arrives on day four
is a different event from one that arrives on day two, and an average delay hides
that entirely.

This dataset is synthetic. No real person is described, and it must never be used
as a template for storing real case data.

## Setup

```{python}
import pandas as pd
import numpy as np

URL = (
    "https://data-analysis.cassion.dev/datasets/files/"
    "protection-referrals-2024.v1.csv"
)

cases = pd.read_csv(URL, dtype={"case_id": "string"})
print(cases.shape)
```

## Build the denominator deliberately

Timeliness is measured on referrals that were **accepted** — a referral that
never reached a service has no service date and belongs in the completion
indicator, not this one. Then narrow to the population the standard applies to.

```{python}
accepted = cases[cases["referral_accepted"]].copy()

gbv_health = accepted[
    (accepted["case_category"] == "gbv")
    & (accepted["service_requested"] == "health")
].copy()

print(f"all cases                      : {len(cases)}")
print(f"accepted referrals             : {len(accepted)}")
print(f"accepted GBV health referrals  : {len(gbv_health)}")
print(f"  with a service time recorded : {int(gbv_health['days_to_first_service'].notna().sum())}")
```

Each narrowing is a decision, and each one has to be defensible. The 72-hour
standard is clinical and applies to health care after sexual violence — applying
it to a legal aid referral would be inventing a target.

## The missing dates are the analysis

Forty accepted referrals across the dataset have no service time recorded. **The
timeliness denominator is therefore smaller than the completion denominator**,
and how you treat those forty changes the answer.

```{python}
missing = gbv_health["days_to_first_service"].isna()
print(f"accepted GBV health referrals with no service time: {int(missing.sum())}")
```

Three defensible treatments, three different numbers:

```{python}
WINDOW_DAYS = 3  # 72 hours

within = gbv_health["days_to_first_service"] <= WINDOW_DAYS
recorded = gbv_health["days_to_first_service"].notna()

treatments = pd.DataFrame([
    {
        "treatment": "exclude missing (recorded only)",
        "denominator": int(recorded.sum()),
        "within 72h": int((within & recorded).sum()),
    },
    {
        "treatment": "count missing as outside the window",
        "denominator": len(gbv_health),
        "within 72h": int((within & recorded).sum()),
    },
    {
        "treatment": "count missing as inside the window",
        "denominator": len(gbv_health),
        "within 72h": int((within & recorded).sum() + missing.sum()),
    },
])
treatments["rate"] = (treatments["within 72h"] / treatments["denominator"]).round(3)
treatments
```

**Which is right?** The first is the honest default: it reports what is known and
states the denominator. The second is the conservative option and is defensible
if you have reason to think a missing date means the service was never actually
delivered. The third is not defensible — it assumes the best case for the cases
you know least about.

Report the first, and report the number of records it excluded in the same
sentence. A timeliness figure without its denominator is not a measurement.

## The distribution, not the mean

```{python}
recorded_times = gbv_health.loc[recorded, "days_to_first_service"]

print(f"mean  : {recorded_times.mean():.1f} days")
print(f"median: {recorded_times.median():.0f} days")

recorded_times.value_counts().sort_index().head(12)
```

The mean is close to useless here. The standard is a threshold, so what matters
is the share on the correct side of it and how far past it the rest fall. A
programme with a mean of four days could have every case at four days, or half at
one day and half at seven — and only one of those is a service failure.

```{python}
bands = pd.cut(
    recorded_times,
    bins=[-1, 3, 7, 14, np.inf],
    labels=["within 72h", "4-7 days", "8-14 days", "over 14 days"],
)
(bands.value_counts(normalize=True).sort_index() * 100).round(1)
```

## Where the delay is

```{python}
def timeliness(df, by, window=WINDOW_DAYS):
    known = df[df["days_to_first_service"].notna()]
    out = known.groupby(by).agg(
        referrals=("days_to_first_service", "size"),
        within=("days_to_first_service", lambda s: (s <= window).sum()),
        median_days=("days_to_first_service", "median"),
    )
    out["within 72h"] = (out["within"] / out["referrals"]).round(3)
    return out.sort_values("within 72h")

timeliness(gbv_health, "admin2")
```

Small denominators again — the areas contribute between six and twenty-seven
referrals each, so the ordering between them is weak and the smallest is not
interpretable at all. Report the areas, report the counts, and resist ranking
them.

```{python}
timeliness(accepted[accepted["service_requested"] == "health"], "case_category")
```

## What this cannot tell you

The clock here starts at the referral and stops at the first service. It does not
start at the incident, because this dataset deliberately holds no incident date —
and under GBV information management principles it should not.

That means **a survivor who reached a caseworker on day five and a service on day
six appears here as a one-day referral**, well inside the window, while the
clinical window had already closed. The indicator measures the referral pathway,
not the survivor's total time to care, and a report that conflates the two
overstates what the programme achieved.

Say that in the limitations. It is the difference between an honest pathway
indicator and a claim about clinical outcomes the data cannot support.

## What to report

The share within 72 hours, its denominator, the number of accepted referrals
excluded for want of a service date, the distribution rather than the mean, and
an explicit statement that the clock starts at referral rather than at incident.
