Advancing standard WASH metrics with experiential indicators in Mozambique

Author

John Brogan

Published

January 26, 2026

1 Introduction

Progress on improving drinking water services is predominantly tracked using directly observable measures such as water source type and time to collect water. (Jepson et al. 2017) In the transition from Millennium Development Goals to the Sustainable Development Goals in 2015, the WHO/UNICEF Joint Monitoring Programme for Water Supply, Sanitation and Hygiene (JMP) introduced a distance threshold for monitoring access to drinking water service levels. This addition has been essential for monitoring and evaluating progress on improving water services across countries and regions.(Cassivi et al. 2018) While such measures are essential, they do not do not tell us if people have insufficient water for basic daily needs – that is, whether they are water insecure.(Joshua D. Miller, Indira Bose, and Sera L. Young 2024)

Water insecurity is the inability to reliably access and use water to meet basic domestic needs. It can be caused by problems with water availability, accessibility, acceptability, safety, or reliability. (Jepson et al. 2017) The WISE Scales include 12 questions about how frequently problems with water are experienced. Items focus on experiences with water for consumption (e.g., drinking, cooking) and hygiene (e.g., handwashing), and consider psychological manifestations of water insecurity (e.g., worry, anger). (Joshua D. Miller, Indira Bose, and Sera L. Young 2024)

Since its inception in 2018, the KALAI Project has supported local actors to build or rehabilitate 568 water points in Nampula and Cabo Delgado provinces of Mozambique. These are primarily borehole wells with hand pumps according to the government guidelines that serve rural communities. Annual project cycles are implemented by Helvetas and local partners with funding from charity: water and have reached over 200’000 people to date. KALAI’s 2024-2025 grant supported communities in Larde, Mecuburi, Memba and Moma districts of Nampula Province.

This paper presents a comparative analysis of pre- and post-intervention data using the JMP indicators and service ladder approach, alongside experiential indicators of service reliability measured with the Household Water Insecurity Experience (HWISE) scales. (Sera Young, n.d.)

2 Methods

Helvetas staff administered baseline and endline household surveys in 40 randomly selected villages across the four districts covered by the project. Enumerators from the localities were accompanied by local leaders and district technicians. Baseline data were collected at 402 households from 13–25 November 2024, and endline data at 406 households from 14–27 June 2025. Enumerators received two days of training prior to each survey, covering survey instruments, identification of water sources and household sampling procedures.

The surveys combined the JMP core questions to classify household water service levels (basic, limited, unimproved, or surface water) and HWISE. The 12 HWISE items invite respondents to reflect on their own experiences—or those of anyone in their household—about water-related adversities over the preceding four weeks. Each of the 12 items is scored according to the reported frequency in a Likert-type ordinal scale from 0 to 36 using the following response categories: Never (0 times), Rarely (1–2 times), Sometimes (3–10 times), Often (11–20 times), and Always (more than 20 times). As summarized in Table 1, each item contributes up to 3 points, with higher scores indicating more frequent experiences of household water insecurity. Surveys missing responses to any of the 12 items were excluded from the HWISE score analysis.

Code
library(gt)
library(tibble)

# Create and render the table
gt(
  tibble(
    Responses = c("Never", "Rarely", "Sometimes", "Often", "Always"),
    Frequency = c("0 times", "1–2 times", "3–10 times", "11–20 times", "> 20 times"),
    Score = c("0", "1", "2", "3", "3")
  )
) |>
  tab_caption(
    "HWISE Response Options and Score per Frequency of Experience"
  )
Table 1: HWISE Response Options and Score per Frequency of Experience
Responses Frequency Score
Never 0 times 0
Rarely 1–2 times 1
Sometimes 3–10 times 2
Often 11–20 times 3
Always > 20 times 3

Households (~10 per village) were selected using either simple random sampling from community lists or a semi-random method of spinning a bottle to determine direction and sampling households at fixed intervals. Surveys were conducted preferentially with female heads of household following informed consent.

Of the 40 villages, 13 were surveyed in both rounds: four in Erati, seven in Larde, two in Memba and one in Moma, covering 132 households at baseline and 143 at endline. This paper focuses on a subset of data covering these 13 communities. Artificial intelligence was used to assist with code development. No AI system was involved in data interpretation or decision-making.

Code
library(dplyr)
library(readr)
library(tidyr)

raw <- read_csv("../data/raw/MZ_WISE_baseline-endline2.csv")

cols_to_remove <- c(
  "Container_size", "total_liters", "liters_person",    
  "25liter", "20liter", "15liter", "10liter", "5liter",
  "Code", "survey_date", "mgmt_turnoff", "mgmt_comm",
  "notsatisfied_why", "places_adults_poo", "handwash_demo"
)
clean <- raw |>
  select(-any_of(cols_to_remove)) |>
  rename_with(~ gsub("hwise:drinking", "hwise_drinking", .x))

# --- Factor and numeric conversions ---
clean <- clean |>
  mutate(
    survey_type = factor(survey_type, levels = c("Baseline", "Endline")),
    District = factor(District, levels = c("Larde", "Memba", "Moma", "Mecuburi")),
    insecurity_level = factor(insecurity_level,
                              levels = c("High", "Moderate", "Low", "No-to-marginal" )),
    whether_improved = factor(whether_improved, levels = c("Unimproved", "Improved")),
    satisfied = case_when(
      trimws(satisfied) == "Satistfied" ~ "Satisfied",
      TRUE ~ satisfied
    ),
    satisfied = factor(satisfied, levels = c("Not Satisfied", "Satisfied")),
    hwise_score = as.numeric(hwise_score),
    total_collect_time = as.numeric(total_collect_time),
    water_basic = as.numeric(water_basic),
    water_limited = as.numeric(water_limited),
    water_unimproved = as.numeric(water_unimproved),
    water_surface = as.numeric(water_surface)
  )

hwise_cols <- c(
  "hwise_worry", "hwise_interrupt", "hwise_clothes",
  "hwise_change_plans", "hwise_change_meal", "hwise_nohandwash",
  "hwise_no_bodywash", "hwise_drinking", "hwise_angry",
  "hwise_sleepthirsty", "hwise_nowater", "hwise_shame"
)
# --- Filter out invalid responses ---
clean <- clean |>
  filter(!if_any(all_of(hwise_cols), ~ . %in% c("", "DNK", "N/A")))

clean <- clean |>
  rename(
    Felt_Worried = hwise_worry,
    Service_Interrupted = hwise_interrupt,
    Too_Little_for_Clothes = hwise_clothes,
    Changed_Routine = hwise_change_plans,
    Too_Little_for_Cooking = hwise_change_meal,
    Too_Little_for_Hands = hwise_nohandwash,
    Too_Little_to_Bathe = hwise_no_bodywash,
    Too_Little_to_Drink = hwise_drinking,
    Felt_Angry = hwise_angry,
    Slept_Thirsty = hwise_sleepthirsty,
    No_Water_at_All = hwise_nowater,
    Felt_Shame = hwise_shame
  )

hwise_items <- c(
  "Felt_Worried",
  "Service_Interrupted",
  "Too_Little_for_Clothes",
  "Changed_Routine",
  "Too_Little_for_Cooking",
  "Too_Little_for_Hands",
  "Too_Little_to_Bathe",
  "Too_Little_to_Drink",
  "Felt_Angry",
  "Slept_Thirsty",
  "No_Water_at_All",
  "Felt_Shame"
)
hwise_labels <- gsub("_", " ", hwise_items)

# --- Recode text responses into 4 levels ---
clean <- clean |>
  mutate(across(
    all_of(hwise_items),
    ~ case_when(
      . %in% c("Never (0 times)")                 ~ "Never",
      . %in% c("Rarely (1-2 times)")             ~ "Rarely",
      . %in% c("Sometimes (3-10 times)")         ~ "Sometimes",
      . %in% c("Often (11-20 times)", "Always (more than 20 times)") ~ "Often/Always",
      TRUE ~ NA_character_
    )
  ))

# --- Prepare long dataset for plotting with correct percent calculation ---
water_insecurity_items <- clean |>
  select(survey_type, all_of(hwise_items)) |>
  pivot_longer(
    cols = all_of(hwise_items),
    names_to = "item",
    values_to = "frequency"
  ) |>
  filter(!is.na(frequency)) |>
  mutate(
    frequency = factor(frequency, levels = c("Never", "Rarely", "Sometimes", "Often/Always")),
    item = factor(item, levels = rev(hwise_items))
  ) |>
  group_by(survey_type, item, frequency) |>
  summarise(n = n(), .groups = "drop") |>
  group_by(survey_type, item) |>        # <-- crucial for correct percentages
  mutate(percent = n / sum(n) * 100) |>
  ungroup()

write_csv(clean, "../data/processed/moz_clean_file.csv")

3 Results

Prior to the intervention, no households had a basic drinking water service (hereafter JMP basic). Only 14% obtained water from an improved source (protected from outside contamination) requiring more than 30 minutes (round-trip). Seventy-seven per cent used water from an unprotected source and 9% relied on surface water. Over 96% were experiencing moderate or severe water insecurity per the HWISE Scale.

Following the intervention, more than 98% of households collected water from improved sources, with 32% accessing water within 30 minutes (JMP basic). No households reported surface water as their main drinking source. Water insecurity declined sharply to 3%.

Code
library(tidyverse)
library(cowplot)

data_processed <- read_csv("../data/processed/moz_clean_file.csv")
Code
drinking_water <- data_processed |>
  select(survey_type, water_basic, water_limited, water_unimproved, water_surface) |>
  pivot_longer(
    cols = starts_with("water_"),
    names_to = "service_level",
    values_to = "value"
  )

drinking_water_percent <- drinking_water |>
  group_by(survey_type, service_level) |>
  summarise(percent = mean(value) * 100, .groups = "drop") |>
  mutate(
    service_level = factor(
      service_level,
      levels = c("water_basic", "water_limited", "water_unimproved", "water_surface"),
      labels = c("Basic", "Limited", "Unimproved", "Surface water")
    )
  ) |>
  pivot_wider(
    names_from = survey_type,
    values_from = percent,
    values_fill = 0
  ) |>
  arrange(service_level) |>
  mutate(
    baseline_cum = cumsum(Baseline) - Baseline,
    endline_cum  = cumsum(Endline) - Endline,
    baseline_mid = baseline_cum + Baseline / 2,
    endline_mid  = endline_cum + Endline / 2
  )
Code
x_center_jmp <- 2
bar_width <- 0.9

drinking_water_polygons <- drinking_water_percent |>
  rowwise() |>
  mutate(polygon = list(tibble(
    x = c(x_center_jmp - bar_width/2, x_center_jmp - bar_width/2,
          x_center_jmp + bar_width/2, x_center_jmp + bar_width/2),
    y = c(baseline_cum, baseline_cum + Baseline,
          endline_cum + Endline, endline_cum)
  ))) |>
  unnest(polygon) |>
  group_by(service_level) |>
  mutate(group_id = cur_group_id()) |>
  ungroup()
Code
baseline_n <- nrow(data_processed |> filter(survey_type == "Baseline"))
endline_n  <- nrow(data_processed |> filter(survey_type == "Endline"))

p_jmp <- ggplot() +
  geom_polygon(data = drinking_water_polygons,
               aes(x = x, y = y, group = group_id, fill = service_level),
               color = "black") +
  geom_text(
    data = drinking_water_percent |> filter(Baseline > 0),
    aes(x = x_center_jmp - bar_width/2 - 0.02, y = baseline_mid,
        label = paste0(round(Baseline, 0), "%")),
    hjust = 1, size = 3
  ) +
  geom_text(
    data = drinking_water_percent |> filter(Endline > 0),
    aes(x = x_center_jmp + bar_width/2 + 0.02, y = endline_mid,
        label = paste0(round(Endline, 0), "%")),
    hjust = 0, size = 3
  ) +
  geom_text(aes(x = x_center_jmp, y = 105,
                label = "Drinking Water"), size = 3.5) +
  geom_text(aes(x = x_center_jmp - bar_width/2, y = -3,
                label = "Baseline"), size = 3.2) +
  geom_text(aes(x = x_center_jmp + bar_width/2, y = -3,
                label = "Endline"), size = 3.2) +
  geom_text(aes(x = x_center_jmp - bar_width/2, y = -8,
                label = paste0("n=", baseline_n)), size = 3) +
  geom_text(aes(x = x_center_jmp + bar_width/2, y = -8,
                label = paste0("n=", endline_n)), size = 3) +
  scale_fill_manual(values = c(
    "Basic"="#00B8EC","Limited"="#FFF176",
    "Unimproved"="#FFDA46","Surface water"="#FEBC11"
  )) +
  scale_x_continuous(limits = c(1, 3), breaks = NULL) +
  scale_y_continuous(limits = c(-10, 110), breaks = c(40, 80)) +
  theme_minimal() +
  theme(
    legend.position = "right",
    legend.text = element_text(size = 7),
    legend.title = element_text(size = 8.5),
    legend.key.size = unit(0.5, "cm")
  ) +
  labs(y = "Households (%)", x = NULL)

legend_jmp <- cowplot::get_legend(
  p_jmp + guides(fill = guide_legend(
    title = "JMP   ", title.position = "top", title.hjust = 0.5
  ))
)

p_jmp <- p_jmp + theme(legend.position = "none")
Code
#HWISE Bar
hwise_levels <- c("No-to-marginal","Low","Moderate","High")

water_insecurity <- data_processed |>
   mutate(insecurity_level = factor(insecurity_level, levels = hwise_levels)) |>
  select(survey_type, insecurity_level) |>
  group_by(survey_type, insecurity_level) |>
  summarise(
    percent = n() / nrow(data_processed[data_processed$survey_type == survey_type, ]) * 100,
    .groups = "drop"
  ) |>
  filter(percent > 0) |>
  pivot_wider(
    names_from = survey_type,
    values_from = percent,
    values_fill = 0
  ) |>
  arrange(insecurity_level) |>
  mutate(
    baseline_cum = cumsum(Baseline) - Baseline,
    endline_cum  = cumsum(Endline) - Endline,
    baseline_mid = baseline_cum + Baseline/2,
    endline_mid  = endline_cum + Endline/2
  )
Code
x_center_hwise <- 0.9

water_insecurity_polygons <- water_insecurity |>
  rowwise() |>
  mutate(polygon = list(tibble(
    x = c(x_center_hwise - bar_width/2, x_center_hwise - bar_width/2,
          x_center_hwise + bar_width/2, x_center_hwise + bar_width/2),
    y = c(baseline_cum, baseline_cum + Baseline,
          endline_cum + Endline, endline_cum)
  ))) |>
  unnest(polygon) |>
  group_by(insecurity_level) |>
  mutate(group_id = cur_group_id()) |>
  ungroup()

p_hwise <- ggplot() +
  geom_polygon(data = water_insecurity_polygons,
               aes(x = x, y = y, group = group_id, fill = insecurity_level),
               color = "black") +
  geom_text(
    data = water_insecurity |> filter(Baseline > 0),
    aes(x = x_center_hwise - bar_width/2 - 0.02, y = baseline_mid,
        label = paste0(round(Baseline, 0), "%")),
    hjust = 1, size = 3
  ) +
  geom_text(
    data = water_insecurity |> filter(Endline > 0),
    aes(x = x_center_hwise + bar_width/2 + 0.02, y = endline_mid,
        label = paste0(round(Endline, 0), "%")),
    hjust = 0, size = 3
  ) +
  geom_text(aes(x = x_center_hwise, y = 105, label = "Water Insecurity"), size = 3.2) +
  geom_text(aes(x = x_center_hwise - bar_width/2, y = -3, label = "Baseline"), size = 3.5) +
  geom_text(aes(x = x_center_hwise + bar_width/2, y = -3, label = "Endline"), size = 3.2) +
  geom_text(aes(x = x_center_hwise - bar_width/2, y = -8, label = paste0("n=", baseline_n)), size = 3) +
  geom_text(aes(x = x_center_hwise + bar_width/2, y = -8, label = paste0("n=", endline_n)), size = 3) +
  scale_fill_manual(values = c(
    "No-to-marginal"="#0080C6","Low"="#00B8EC",
    "Moderate"="#FEBC11","High"="#EF414A"
  )) +
  scale_x_continuous(limits = c(0,2), breaks = NULL) +
  scale_y_continuous(limits = c(-10,110)) +
  theme_minimal() +
  theme(
    legend.position = "right",
    legend.text = element_text(size = 7),
    legend.title = element_text(size = 8.5),
    legend.key.size = unit(0.5, "cm")
  ) +
  labs(x = NULL, y = NULL)

legend_hwise <- cowplot::get_legend(
  p_hwise + guides(fill = guide_legend(
    title = "HWISE", title.position = "top", title.hjust = 0.5
  ))
)

p_hwise <- p_hwise + theme(legend.position = "none")

#Combined Bars
legend_jmp_lower <- ggdraw() +
  draw_grob(legend_jmp, x = 1.0, y = 0.15, vjust = 0.5)

legend_hwise_lower <- ggdraw() +
  draw_grob(legend_hwise, x = -0.21, y = 0.15, vjust = 0.5)
Code
combined <- plot_grid(
  legend_jmp_lower,
  ggdraw(p_jmp),
  ggdraw(p_hwise),
  legend_hwise_lower,
  nrow = 1,
  rel_widths = c(0.2, 1.05, 0.95, 0.2),
  align = "h"
)
final_plot <- ggdraw(combined) +
  draw_label(
    "Figure 1. Changes in drinking water service and water insecurity experience",
    fontface = "bold",
    x = 0.5, y = 0.97, hjust = 0.5)

final_plot

As shown in Table 2 below, across all four districts, water insecurity—as measured by the HWISE scale—declined markedly from baseline to endline, as did round-trip water collection time. However, variability in median collection time among the population remained high. The prevalence of households using an improved water source and user satisfaction with drinking water services increased substantially over the same period. Despite these gains, the median round-trip collection time remained well above 30 minutes for two-thirds of the population, and 7.5% of households in Larde continued to experience moderate water insecurity. Notably, one village in Larde—Macuire—accounted for the majority of water-related adversity.

Table 2. Changes in Water Services Before and After Intervention, by District

Code
library(dplyr)
library(tidyr)
library(flextable)

district_cols <- c(
   "Larde_Baseline", "Larde_Endline",
   "Mecuburi_Baseline", "Mecuburi_Endline",
   "Memba_Baseline", "Memba_Endline",
   "Moma_Baseline", "Moma_Endline"
)
percent_table <- function(data, var) {
  data |> 
    group_by(District, survey_type, !!sym(var)) |> 
    summarise(n = n(), .groups = "drop") |> 
    group_by(District, survey_type) |> 
    mutate(pct = round(n / sum(n) * 100, 1)) |> 
    select(District, survey_type, !!sym(var), pct)
}
row_labels <- c(
  "", 
  "Surveys per district",       
  "HWISE score, \nmean ± SD",
  "Insecurity level, %",
  "High", "Moderate", "Low", "No-to-Marginal",
  "Improved water source, %",
  "No", "Yes",
  "Collect (min.), median (IQR)",
  "Satisfied with service, %",
  "No", "Yes"
)
results_general <- data.frame(Row = row_labels, stringsAsFactors = FALSE)

row_surveys    <- which(results_general$Row == "Surveys per district")
row_hwise      <- which(results_general$Row == "HWISE score, \nmean ± SD")
rows_insecurity <- which(results_general$Row %in% 
                           c("High","Moderate","Low","No-to-Marginal"))
rows_improved  <- which(results_general$Row %in% c("No","Yes"))[1:2]
row_collect    <- which(results_general$Row == "Collect (min.), median (IQR)")
rows_satisfied <- which(results_general$Row %in% c("No","Yes"))[3:4]

surveys_per_district <- clean |>
  group_by(District, survey_type) |>
  summarise(n = n(), .groups = "drop") |>
  pivot_wider(names_from = c(District, survey_type),
              values_from = n,
              names_sep = "_")
results_general[row_surveys, 2:9] <- surveys_per_district |> select(all_of(district_cols)) |> as.matrix()

hwise_table <- clean |>
  group_by(District, survey_type) |>
  summarise(
    mean_sd = paste0(round(mean(hwise_score, na.rm = TRUE), 1), " \n± ", round(sd(hwise_score, na.rm = TRUE), 1)),
    .groups = "drop"
  ) |>
  pivot_wider(names_from = c(District, survey_type),
              values_from = mean_sd,
              names_sep = "_")
results_general[row_hwise, 2:9] <- hwise_table |> select(all_of(district_cols)) |> as.matrix()

insecurity_table <- percent_table(clean, "insecurity_level") |>
  pivot_wider(names_from = c(District, survey_type),
              values_from = pct,
              names_sep = "_")
results_general[rows_insecurity, 2:9] <- insecurity_table |> select(-insecurity_level) |> select(all_of(district_cols)) |> as.matrix()

improved_table <- percent_table(clean, "whether_improved") |>
  pivot_wider(names_from = c(District, survey_type),
              values_from = pct,
              names_sep = "_")
results_general[rows_improved, 2:9] <- improved_table |> select(-whether_improved) |> select(all_of(district_cols)) |> as.matrix()

collect_time_table <- clean |>
  group_by(District, survey_type) |>
  summarise(
    median = median(total_collect_time, na.rm = TRUE),
    IQR = paste0("[", quantile(total_collect_time, 0.25, na.rm = TRUE), "-", quantile(total_collect_time, 0.75, na.rm = TRUE), "]"),
    .groups = "drop"
  ) |>
  mutate(summary = paste0(median, "\n", IQR)) |>
  select(District, survey_type, summary) |>
  pivot_wider(names_from = c(District, survey_type),
              values_from = summary,
              names_sep = "_")
results_general[row_collect, 2:9] <- collect_time_table |> select(all_of(district_cols)) |> as.matrix()

satisfied_table <- percent_table(clean, "satisfied") |>
  pivot_wider(names_from = c(District, survey_type),
              values_from = pct,
              names_sep = "_")
results_general[rows_satisfied, 2:9] <- satisfied_table |> select(-satisfied) |> select(all_of(district_cols)) |> as.matrix()

col_keys <- c("row", district_cols)
names(results_general) <- col_keys


ft <- flextable(results_general) |>
   set_caption(caption = "Changes in Water Services Before and After Intervention, by District") |>

  set_header_labels(
    Mecuburi_Baseline = "Baseline",
    Mecuburi_Endline  = "Endline",
    Larde_Baseline = "Baseline",
    Larde_Endline  = "Endline",
    Memba_Baseline = "Baseline",
    Memba_Endline  = "Endline",
    Moma_Baseline  = "Baseline",
    Moma_Endline   = "Endline"
  ) |>
  add_header_lines( "" ) |>
  add_header_row(
    values = c("", "Larde", "Mecuburi", "Memba",
               "Moma"),
    colwidths = c(1, 2, 2, 2, 2)
  ) |>
  merge_h(part = "header") |>
  width(j = 1, width = 4) |>
  width(j = 2:9, width = 0.9) |>
  align(align = "center", part = "all") |>
  colformat_num(j = 2:9, digits = 1) |>
  fontsize(size = 11, part = "body") |>
  bg(i = c(2,4,5,6,7,8,12), j = 1:9, bg = "lightblue") |>
  fontsize(i = 1, size = 16, part = "header") |>
  bold(i = 1, part = "header") |>
  fontsize(i = 2, size = 14, part = "header") |>
  bold(i = 2, part = "header") |>
  fontsize(i = 3, size = 10, part = "header") |>
  bold(i = 3, part = "header") |>
  bold(j = 1, part = "body")|>
  color(j = c(3, 5, 7, 9), color = "blue", part = "body") |>
  color(j = c(3, 5, 7, 9), color = "blue", part = "header") 

ft

Larde

Mecuburi

Memba

Moma

row

Baseline

Endline

Baseline

Endline

Baseline

Endline

Baseline

Endline

Surveys per district

31

40

10

10

71

73

20

20

HWISE score,
mean ± SD

23
± 5.9

1.6
± 4.3

22.6
± 5.1

0
± 0

22
± 6.7

0.3
± 1.5

24.4
± 5.3

0
± 0

Insecurity level, %

High

51.6

60

39.4

60

Moderate

45.2

7.5

40

54.9

1.4

40

Low

3.2

7.5

5.6

2.7

No-to-Marginal

85

100

95.9

100

Improved water source, %

No

100

7.5

100

76.1

90

Yes

92.5

100

23.9

100

10

100

Collect (min.), median (IQR)

200
[145-345]

37.5
[27.5-50]

90
[64.5-160]

58
[37-64.25]

180
[110-260]

46
[30-58]

305
[180-375]

28
[20-40]

Satisfied with service, %

No

100

17.5

100

100

1.4

100

5

Yes

82.5

100

98.6

95

As shown in Figure 2 below, more than half of all households reported experiencing Sometimes or Often/Always for each of the 12 HWISE items examined at baseline, with the exception of Slept Thirsty (45% of households). The most frequently experienced water service adversities were Worry (88%) and not having any water at all at home (90%). The frequency of water insecurity experiences declined sharply at endline where at least 98% of respondents reported Never or Low frequency across all 12 survey items.

Code
library(ggplot2)
library(dplyr)
library(cowplot)  # for aligning n labels

freq_colors <- c(
  "Never" = "#0080C6",
  "Rarely" = "#00B8EC",
  "Sometimes" = "#FEBC11",
  "Often/Always" = "#EF414A"
)

# --- Compute n for each item and survey_type (for side labels) ---
n_labels_right <- water_insecurity_items |>
  group_by(survey_type, item) |>
   summarise(total_percent = sum(percent), n = sum(n), .groups = "drop")

# --- Base plot ---
p_hwise_items <- ggplot(water_insecurity_items, 
                        aes(x = percent, y = item, fill = frequency)) +
  geom_col(position = "stack", width = .85) +
  facet_wrap(~survey_type, ncol = 1, scales = "free_y") +
  scale_fill_manual(values = freq_colors, name = "Frequency") +
    scale_y_discrete(
    limits = rev(hwise_items),   # keeps the order
    labels = rev(hwise_labels)   # replaces underscores with spaces
  ) +
  labs(
    x = "Frequency of experiences",
    y = "HWISE ITEMS",
    caption = "Figure 2. Frequency of experiences by HWISE item"
  ) +
  theme_minimal(base_size = 10) +
  theme(
    strip.text = element_text(face = "bold", size = 11),
    axis.text.y = element_text(face = "bold"),
    legend.position = "right",
    plot.caption = element_text(hjust = 0, face = "bold")
  )
# --- Percentage labels inside stacks ---
p_hwise_items <- p_hwise_items +
  geom_text(
    aes(label = paste0(round(percent, 0), "%")),
    position = position_stack(vjust = 0.5),
    size = 2,
    color = "white",
    fontface = "bold"
  )

# --- Add n labels to right of bars ---
p_hwise_items <- p_hwise_items +
  geom_text(
    data = n_labels_right,
    aes(x = total_percent + 2, y = item, label = paste0("n=", n)),
    inherit.aes = FALSE,
    hjust = 0,    # left-aligned relative to x, so the label sits just right of the bar
    size = 1.5
  )
p_hwise_items <- p_hwise_items +
  geom_text(
    aes(label = ifelse(percent >= 3,
                       paste0(round(percent, 0), "%"),
                       "")),
    position = position_stack(vjust = 0.5),
    size = 2,
    color = "white",
    fontface = "bold"
  )

print(p_hwise_items)

4 Conclusions

This analysis demonstrates the utility of experiential indicators in measuring the potential impact of water service improvements. From baseline to endline, drinking water service levels improved and experiences related to water insecurity decreased. HWISE items captured ongoing concerns about unmet water needs at baseline, and dramatic decreases in experiences of water scarcity and psycosocial-related adversities at endline. HWISE was also useful in identifying a community where water insecurity persisted post intervention.

Although nearly all households reported accessing an improved water source, two-thirds still lack a basic service, indicating that more work is needed to reduce collection time through the installation of additional water points.

The baseline took place at the tail end of the dry season before monsoon conditions were underway and the endline was conducted in peak dry season. Post-intervention monitoring should allow enough time for the intervention to take effect. Helvetas’ monitoring guidance recommends that new or rehabilitated water supply systems be operational for at least three to six months before using HWISE. To assess lasting impact, KALAI project should measure with HWISE again after 1–2 years.

5 Acknowledgements

The author gratefully acknowledges Dercio Obra (Helvetas Mozambique) for overseeing all monitoring and evaluation activities of KALAI VII Project, Josh Miller (University at Buffalo) for providing inspiration in data analysis visualisation for the WISE Scales, and the team at the Open WASH Data Course by ETH Zurich led by Lars Schöbitz for the opportunity to learn about data science tools and produce such reports.

6 References

Cassivi, A., R. Johnston, E. O. D. Waygood, and C. C. Dorea. 2018. “Access to Drinking Water: Time Matters.” Journal of Water and Health 16 (4): 661–66. https://doi.org/10.2166/wh.2018.009.
Jepson, Wendy E., Amber Wutich, Shalean M. Colllins, Godfred O. Boateng, and Sera L. Young. 2017. “Progress in Household Water Insecurity Metrics: A Cross-Disciplinary Approach.” WIREs Water 4 (3). https://doi.org/10.1002/wat2.1214.
Joshua D. Miller, Indira Bose, and Sera L. Young. 2024. “Measuring Human Experiences to Advance Safe Water for All.” https://doi.org/10.21985/N2-XVRR-7693.
Sera Young. n.d. “The Household Water InSecurity Experiences (HWISE) Scale Study Manual.” https://doi.org/10.21985/N2P450.

Reuse