Lesson 3 of 8
Unit · Coping is a sequence, not a score
A household is classified by its worst strategy
Ten strategies, three severity phases, and the rule is the maximum rather than the sum. 18.2% of these households used an emergency strategy, and one district's answer to "not applicable" makes its prevalence figures wrong while leaving its classification intact.
The module, and why it is not another index
The Livelihood Coping Strategies module asks whether a household used any of ten strategies in the last thirty days. Each strategy is pre-classified into a severity phase before any data is collected, and the household is then classified by the most severe strategy it used.
| Phase | Strategies | What they have in common |
|---|---|---|
| Stress | Sold household assets, spent savings, borrowed money, sold more animals than usual | Reduce the ability to deal with future shocks; reversible |
| Crisis | Sold productive assets, withdrew children from school, cut health spending | Reduce future productivity; hard to reverse |
| Emergency | Sold house or land, begged, sold last female breeding animals | Reduce future productivity and are close to irreversible |
The classification is a maximum, not a sum, and not a count. A household that sold its last breeding animals once is worse off than one that borrowed money four times, and any arithmetic adding the two together says the opposite.
import pandas as pd
coping = pd.read_csv("livelihood-coping-2024.v1.csv")
PHASE = {
"sold_household_assets": "stress",
"spent_savings": "stress",
"borrowed_money": "stress",
"sold_more_animals_than_usual": "stress",
"sold_productive_assets": "crisis",
"withdrew_children_from_school": "crisis",
"reduced_health_expenditure": "crisis",
"sold_house_or_land": "emergency",
"begged": "emergency",
"sold_last_female_animals": "emergency",
}
RANK = {"none": 0, "stress": 1, "crisis": 2, "emergency": 3}
used = coping[list(PHASE)].eq("yes")
severity = pd.Series("none", index=coping.index)
for strategy, phase in PHASE.items():
higher = used[strategy] & (severity.map(RANK) < RANK[phase])
severity[higher] = phase
print((severity.value_counts(normalize=True) * 100).round(1))
library(dplyr)
phase <- c(sold_household_assets = "stress", spent_savings = "stress",
borrowed_money = "stress", sold_more_animals_than_usual = "stress",
sold_productive_assets = "crisis",
withdrew_children_from_school = "crisis",
reduced_health_expenditure = "crisis",
sold_house_or_land = "emergency", begged = "emergency",
sold_last_female_animals = "emergency")
rank <- c(none = 0, stress = 1, crisis = 2, emergency = 3)
| Classification | Households | Share |
|---|---|---|
| None | 339 | 16.0% |
| Stress | 754 | 35.5% |
| Crisis | 642 | 30.3% |
| Emergency | 386 | 18.2% |
48.5% used a crisis or emergency strategy. That single figure is what the IPC evidence table takes from this module, and it is a very different picture from the 7.3% with poor food consumption.
Not applicable is not no
A household with no animals cannot sell its last breeding female. The module
records that as not-applicable, and it is a third answer rather than a flavour
of no.
answers = coping["sold_last_female_animals"].value_counts()
print(answers)
applicable = coping["sold_last_female_animals"].isin(["yes", "no"])
print(f"prevalence among applicable: "
f"{coping.loc[applicable, 'sold_last_female_animals'].eq('yes').mean():.1%}")
print(f"prevalence if n/a counted as no: "
f"{coping['sold_last_female_animals'].eq('yes').mean():.1%}")
coping |> count(sold_last_female_animals)
coping |>
filter(sold_last_female_animals %in% c("yes", "no")) |>
summarise(prevalence = mean(sold_last_female_animals == "yes"), n = n())
| Strategy | Among households it applies to | If n/a counted as no |
|---|---|---|
| Sold more animals than usual | 37.4% | 27.5% |
| Sold productive assets | 19.8% | 17.3% |
| Withdrew children from school | 19.7% | 16.5% |
| Sold last female animals | 8.6% | 5.8% |
Ten points of difference on the first row. The denominator for a per-strategy prevalence is the households the strategy is available to, and counting the rest as non-users understates every one of them.
The household-level classification survives this and the per-strategy
prevalence does not, because a maximum over yes values does not care whether
the non-yes values are no or not-applicable. That asymmetry is worth
knowing: it is why the same file can support a correct classification and a wrong
prevalence table on the same page.
The district where the distinction was lost
by_district = coping.groupby("district")["sold_last_female_animals"].agg(
yes=lambda s: (s == "yes").sum(),
applicable=lambda s: s.isin(["yes", "no"]).sum(),
not_applicable=lambda s: (s == "not-applicable").sum(),
)
by_district["correct"] = by_district["yes"] / by_district["applicable"]
by_district["naive"] = by_district["yes"] / coping.groupby("district").size()
print((by_district[["correct", "naive"]] * 100).round(1))
coping |>
summarise(yes = sum(sold_last_female_animals == "yes"),
applicable = sum(sold_last_female_animals %in% c("yes", "no")),
n = n(), .by = district) |>
mutate(correct = yes / applicable, naive = yes / n)
| District | Correct denominator | Every household |
|---|---|---|
| Nord-Ouest | 12.3% | 6.8% |
| Artibonite | 9.7% | 5.3% |
| Sud | 7.5% | 4.5% |
| Centre | 6.5% | 6.4% |
Centre’s two columns are almost identical, and every other district’s are not.
The reason is in the raw data: Centre has no not-applicable cells at all,
because its enumerators recorded them as no throughout.
print(coping.groupby("district")[list(PHASE)]
.apply(lambda block: (block == "not-applicable").sum().sum()))
coping |> summarise(across(all_of(names(phase)),
~ sum(.x == "not-applicable")), .by = district)
On the correct denominator Nord-Ouest is nearly twice Centre. On the wrong one they are the same. A district comparison built from the naive column would conclude that livestock distress selling is uniform across the response area, and would be reading one team’s data entry habit.
The partial module
Fifty-eight households have the three emergency questions blank.
emergency = [s for s, phase in PHASE.items() if phase == "emergency"]
partial = coping[emergency].eq("").all(axis=1) | coping[emergency].isna().all(axis=1)
print(f"{partial.sum()} households cannot be classified above crisis")
coping |> filter(if_all(c(sold_house_or_land, begged, sold_last_female_animals),
~ is.na(.x))) |> nrow()
A maximum computed over what is present classifies them as crisis at worst. So 18.2% in emergency is a lower bound, and the honest report says so rather than presenting it as the estimate.
The alternative — dropping them — makes the emergency share an unbiased estimate of a slightly different population. Both are defensible; picking silently is not.
Why coping is measured separately from consumption
The instruments do not overlap the way people expect.
survey = pd.read_csv("food-security-survey-2024.v1.csv")
merged = survey.merge(coping, on="household_id", how="left", indicator=True)
print(merged["_merge"].value_counts())
survey |> left_join(coping, by = "household_id") |>
summarise(matched = sum(!is.na(district.y)), n = n())
Households resort to coping strategies before their food consumption falls — that is the point of coping. A household selling assets to keep eating has intact consumption and a collapsing asset base, and an analysis reading only the FCS records it as food secure right up until the assets run out.
That is the argument for measuring both, and the next lesson is what happens when you do.
Two things to check on the join. Nine households appear in the module and not in the survey, because the module was administered from a separate listing — an inner join drops them without a word. And the survey’s twelve duplicate households appear once in the module, so the join is not one-to-one.
What comes next
Four instruments now exist on these households: consumption, hunger, coping behaviour and livelihood strategies. The next lesson asks which of them agree about who is food insecure, and the answer is most of the reason the IPC exists.