cassionData Analysis

Back to the lessonLesson 5 of 8The caseload

Thirty-three cases each

The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 24

    What this lesson covers

    • Caseload is a stock, not a flow
    • The number the guidance is written about
    • What an overloaded caseload does
    • The establishment error
    • What caseload is not
    • Report it as a supervision table
    • What comes next
    Speaker notes
    Guidance puts an active GBV caseload near 25 cases per worker. One area here averages 33.2 and peaks at 51. It also holds 1.78 case plan reviews per case against 2.45, and loses contact with 38.9% of the cases it closes. Three tables, one cause.
  2. Slide 2 / 24

    Caseload is a stock, not a flow — In Python (cont.)

    import pandas as pd
    
    cases = pd.read_csv("protection-case-management-2024.v1.csv")
    CUTOFF = 12
    
    def months_open(row):
        opened = int(row["opened_month"][5:7])
        closed = int(row["closed_month"][5:7]) if pd.notna(row["closed_month"]) else CUTOFF
        return range(opened, max(closed, opened) + 1)
    
    active = [
        {"caseworker_id": row["caseworker_id"], "admin2": row["admin2"], "month": month}
        for _, row in cases.iterrows()
        for month in months_open(row)
    ]
    active = pd.DataFrame(active)
    Speaker notes
    A case count is a flow: how many cases opened this month. A caseload is a stock: how many are open right now, and it is the number a caseworker experiences. Computing it means expanding each case across the months it was open.
  3. Slide 3 / 24

    Caseload is a stock, not a flow — In Python (cont.)

    
    caseload = active.groupby(["caseworker_id", "month"]).size().rename("open_cases")
    print(caseload.describe().round(1))
  4. Slide 4 / 24

    Caseload is a stock, not a flow — In R

    library(dplyr)
    
    cases |>
      mutate(opened = as.integer(substr(opened_month, 6, 7)),
             closed = coalesce(as.integer(substr(closed_month, 6, 7)), 12L)) |>
      rowwise() |>
      mutate(month = list(seq(opened, max(closed, opened)))) |>
      tidyr::unnest(month) |>
      count(caseworker_id, month, name = "open_cases")
  5. Slide 5 / 24

    Caseload is a stock, not a flow

    • This is the person-time expansion from module 4's first course — applied to a caseworker rather than to a patient
    Speaker notes
    This is the person-time expansion from module 4's first course, applied to a caseworker rather than to a patient. A case open from March to August contributes to six monthly caseloads, and a register read one row at a time never shows it.
  6. Slide 6 / 24

    The number the guidance is written about — In Python

    by_area = caseload.reset_index().merge(
        cases[["caseworker_id", "admin2"]].drop_duplicates("caseworker_id"),
        on="caseworker_id",
    )
    summary = by_area.groupby("admin2")["open_cases"].agg(["mean", "max"]).round(1)
    print(summary.sort_values("mean", ascending=False))
  7. Slide 7 / 24

    The number the guidance is written about — In R

    # Mean and peak monthly caseload per area.
  8. Slide 8 / 24

    The number the guidance is written about

    AreaMean caseloadPeak
    Port-de-Paix33.251
    Gonaives28.241
    Saint-Marc24.743
    Hinche20.744
    Mirebalais18.732
    Saint-Louis-du-Nord14.826
  9. Slide 9 / 24

    The number the guidance is written about

    • GBV case management guidance puts an active caseload at around 25 — Port-de-Paix sits above it all year and peaks at…
    • Report the mean and the peak — A worker at 51 open cases in one month has, that month, roughly four working hours per…
    Speaker notes
    GBV case management guidance puts an active caseload at around 25. Port-de-Paix sits above it all year and peaks at twice it; Saint-Louis-du-Nord sits well below. Report the mean and the peak. A worker at 51 open cases in one month has, that month, roughly four working hours per case including travel and documentation, and an annual mean of 33 conceals it.
  10. Slide 10 / 24

    What an overloaded caseload does — In Python

    quality = cases.groupby("admin2").agg(
        cases=("case_id", "size"),
        reviews_per_case=("case_plan_reviews", "mean"),
    )
    closed = cases[cases["closure_reason"].notna()]
    quality["lost_contact"] = closed.groupby("admin2")["closure_reason"].apply(
        lambda s: (s == "lost-contact").mean()
    )
    print(quality.round(2).sort_values("reviews_per_case"))
    Speaker notes
    The reason caseload has a threshold is that things fail when it is exceeded, and this register lets you watch two of them fail together.
  11. Slide 11 / 24

    What an overloaded caseload does — In R

    cases |>
      summarise(n = n(), reviews = mean(case_plan_reviews),
                lost = mean(closure_reason == "lost-contact", na.rm = TRUE),
                .by = admin2)
  12. Slide 12 / 24

    What an overloaded caseload does

    AreaCaseloadReviews per caseLost contact
    Saint-Louis-du-Nord14.81.7426.0%
    Port-de-Paix33.21.7838.9%
    Mirebalais18.72.1030.8%
    Gonaives28.22.3124.7%
    Saint-Marc24.72.3822.1%
    Hinche20.72.4520.2%
  13. Slide 13 / 24

    What an overloaded caseload does

    • Port-de-Paix carries the highest caseload, holds among the fewest case plan reviews, and loses contact with the largest…
    Speaker notes
    Port-de-Paix carries the highest caseload, holds among the fewest case plan reviews, and loses contact with the largest share of the cases it closes. Three different tables produced by one cause, and the caseload table is the one that names the cause. Note that Saint-Louis-du-Nord also holds few reviews on a low caseload — so reviews per case is not a clean function of caseload, and the honest reading is that Port-de-Paix has a workload problem while Saint-Louis-du-Nord may have a different one. A pattern that fits three areas and not the fourth is still a pattern, and saying which area does not fit is part of reporting it.
  14. Slide 14 / 24

    The establishment error — In Python

    areas_per_worker = cases.groupby("caseworker_id")["admin2"].nunique()
    print(areas_per_worker[areas_per_worker > 1])
  15. Slide 15 / 24

    The establishment error — In R

    cases |> summarise(areas = n_distinct(admin2), .by = caseworker_id) |>
      filter(areas > 1)
  16. Slide 16 / 24

    The establishment error

    • A caseload computed per worker is right. A caseload computed per worker per area is wrong — because it splits one…
    Speaker notes
    One caseworker identifier appears under two areas, because a worker who transferred was re-registered rather than moved. A caseload computed per worker is right. A caseload computed per worker per area is wrong, because it splits one person's real workload across two rows and makes both look manageable. Decide which the identifier means before you group by it, and if the register cannot tell you, that is a question for the office rather than an assumption for the analyst.
  17. Slide 17 / 24

    What caseload is not

    • It is not a productivity measure — A worker with 40 open cases is not working harder than one with 15; they are more…
    • It is not comparable across case types — A child protection case with a case plan involving a school and a guardian is…
    Speaker notes
    It is not a productivity measure. A worker with 40 open cases is not working harder than one with 15; they are more likely to be failing 40 people slowly. Using caseload to rank workers inverts what the indicator is for. It is not comparable across case types. A child protection case with a case plan involving a school and a guardian is not equivalent to a one-off legal referral, and this register's case_category is what lets you weight them.
  18. Slide 18 / 24

    What caseload is not — In Python

    mix = cases.groupby(["admin2", "case_category"]).size().unstack(fill_value=0)
    print((mix.div(mix.sum(axis=1), axis=0) * 100).round(1))
  19. Slide 19 / 24

    What caseload is not — In R

    cases |> count(admin2, case_category) |>
      mutate(share = n / sum(n), .by = admin2)
  20. Slide 20 / 24

    What caseload is not

    • Check the case mix before comparing caseloads — An area with more child protection cases is carrying more work per…
    Speaker notes
    Check the case mix before comparing caseloads. An area with more child protection cases is carrying more work per case, and a raw comparison penalises it.
  21. Slide 21 / 24

    Report it as a supervision table — Example

    Caseload, 2024, 1,108 cases across 17 caseworkers
    
      Area                  Mean   Peak   Reviews/case   Lost contact
      Port-de-Paix          33.2     51           1.78          38.9%
      Gonaives              28.2     41           2.31          24.7%
      Saint-Marc            24.7     43           2.38          22.1%
      Hinche                20.7     44           2.45          20.2%
      Mirebalais            18.7     32           2.10          30.8%
      Saint-Louis-du-Nord   14.8     26           1.74          26.0%
    
      Guidance: active caseload around 25 cases per worker.
      One caseworker identifier appears in two areas after a transfer; caseload
      is computed per worker, not per worker per area.
    
      Port-de-Paix is above the guideline all year and shows the pattern that
      follows: fewest case plan reviews and most cases closed for lost contact.
  22. Slide 22 / 24

    Report it as a supervision table

    • Put the guideline in the table — A caseload of 33 means nothing to a reader who does not know what 25 is, and the whole…
    Speaker notes
    Put the guideline in the table. A caseload of 33 means nothing to a reader who does not know what 25 is, and the whole point of the row is the comparison.
  23. Slide 23 / 24

    What comes next

    • Cases close, and how long they take is the other number a supervisor is judged on.
    Speaker notes
    Cases close, and how long they take is the other number a supervisor is judged on. The next lesson is that measurement, and the 42.6% of cases that have not closed yet and would bias it if they were ignored.
  24. Slide 24 / 24

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson