---
title: "JMP service ladders for water, sanitation and hygiene"
subtitle: "WASH household survey, 2024 · R"
author: "Cassion · data-analysis.cassion.dev"
format:
  html:
    toc: true
    code-fold: false
---

## What this produces

The same three ladder classifications as the Python example, built with dplyr
`case_when`. Python and R are peers here; you will inherit whichever your
predecessor used.

Every dataset on this platform is synthetic. No real household is described.

## Setup

```{r}
#| message: false
library(readr)
library(dplyr)
library(tidyr)

URL <- paste0(
  "https://data-analysis.cassion.dev/datasets/files/",
  "wash-household-survey-2024.v1.csv"
)

wash <- read_csv(URL, col_types = cols(
  household_id = col_character(),
  district     = col_character(),
  community    = col_character(),
  .default     = col_guess()
))

glimpse(wash)
```

## Normalise the district before grouping by it

One team wrote Nord-Ouest four different ways. Six districts instead of three,
and the worst-performing one split into pieces too small to notice.

```{r}
count(wash, district)
```

```{r}
wash <- wash |>
  mutate(district = gsub(" ", "-", tolower(trimws(district)), fixed = TRUE))

count(wash, district)
```

## The three ladders

`case_when` evaluates top to bottom and stops at the first match, which is
exactly the shape of a ladder definition — put the overriding conditions first.

```{r}
IMPROVED <- c(
  "piped-into-dwelling", "piped-into-yard", "public-tap", "borehole",
  "protected-well", "protected-spring", "tanker-truck"
)
IMPROVED_SANITATION <- c(
  "flush-to-sewer", "flush-to-septic", "vip-latrine", "pit-latrine-with-slab"
)

wash <- wash |>
  mutate(
    water_service = case_when(
      water_source == "surface-water"                      ~ "surface water",
      !water_source %in% IMPROVED                          ~ "unimproved",
      is.na(round_trip_minutes)                            ~ "improved, time unknown",
      round_trip_minutes <= 30                             ~ "basic",
      TRUE                                                 ~ "limited"
    ),
    sanitation_service = case_when(
      sanitation_facility == "open-defecation"             ~ "open defecation",
      !sanitation_facility %in% IMPROVED_SANITATION        ~ "unimproved",
      shared_sanitation                                    ~ "limited",
      TRUE                                                 ~ "basic"
    ),
    hygiene_service = case_when(
      handwashing_facility == "no-facility"                ~ "no facility",
      soap_observed                                        ~ "basic",
      TRUE                                                 ~ "limited"
    )
  )

wash |> count(water_service) |> mutate(pct = round(100 * n / sum(n), 1))
```

Note the ordering in the water ladder. An improved source with an unrecorded
collection time cannot be called basic, so that clause sits above the time
comparison — otherwise `NA <= 30` returns `NA`, falls through to `TRUE`, and the
household is silently reported as limited service on no evidence at all.

```{r}
wash |> count(sanitation_service) |> mutate(pct = round(100 * n / sum(n), 1))
wash |> count(hygiene_service) |> mutate(pct = round(100 * n / sum(n), 1))
```

## Coverage by district

```{r}
coverage <- wash |>
  group_by(district) |>
  summarise(
    households        = n(),
    basic_water       = round(100 * mean(water_service == "basic"), 1),
    basic_sanitation  = round(100 * mean(sanitation_service == "basic"), 1),
    basic_hygiene     = round(100 * mean(hygiene_service == "basic"), 1),
    open_defecation   = round(100 * mean(sanitation_service == "open defecation"), 1),
    .groups = "drop"
  ) |>
  arrange(basic_water)

coverage
```

## Quantity is a separate indicator

```{r}
SPHERE_MINIMUM <- 15

wash |>
  summarise(
    below_sphere = round(100 * mean(litres_per_person_day < SPHERE_MINIMUM, na.rm = TRUE), 1),
    over_30_min  = round(100 * mean(round_trip_minutes > 30, na.rm = TRUE), 1)
  )
```

```{r}
wash |>
  count(water_service, below_sphere = litres_per_person_day < SPHERE_MINIMUM) |>
  group_by(water_service) |>
  mutate(pct = round(100 * n / sum(n), 1)) |>
  ungroup()
```

Households on basic service still fall below the Sphere minimum. Access and
quantity answer different questions and neither stands in for the other.

## What to report

The rung, the denominator it rests on, and the quantity indicator separately —
plus the households that could not be classified at all. A household with no
collection time recorded is not basic service, it is unknown, and folding it into
the basic count is how a coverage figure drifts upward with nobody deciding that
it should.
