Back to the lesson·Lesson 7 of 8·Reshaping and repeating
Undoing a flattened repeat group
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.
What this lesson covers
- What a form platform does to a repeat group
pivot_longer()with.value- Check the row count against what you expect
pivot_wider()and the warning that means something- Separating and uniting columns
- Nesting, briefly
- Where this fits
- What comes next
Speaker notes
pivot_longer() with .value, the one call that turns a CommCare or Kobo export back into one row per person — plus the pivot_wider() warning that means your key is not a key.What a form platform does to a repeat group — Example
hh member_1_age member_1_sex member_2_age member_2_sex H1 34 f 8 mSpeaker notes
A household questionnaire asks about each member. In CommCare, KoboToolbox or ODK the answers come back flattened — one row per household, with the repeat group spread across numbered columns: Nothing about that shape is analysable. You cannot compute a mean age, filter to children, or join to anything, because a person is not a row.pivot_longer()with.value— In Rlibrary(tidyr) long <- wide |> pivot_longer( starts_with("member_"), names_to = c("member", ".value"), names_pattern = "member_(\\d+)_(.*)" )pivot_longer()with.value— Example# A tibble: 2 × 4 hh member age sex <chr> <chr> <dbl> <chr> 1 H1 1 34 f 2 H1 2 8 mpivot_longer()with.value- The pattern
Speaker notes
One row per person, andageandsexare separate columns with their own types. That is the shape everything downstream expects. The.valuesentinel is the piece worth understanding. Innames_to, an ordinary name —"member"— becomes a new column holding that part of the old column name.".value"is different: it says this part of the name is the name of the output column. Somember_1_agesplits intomember = "1"and a value belonging to a column calledage. Without.valueyou would get a singlevaluecolumn mixing numbers and text, and anamecolumn you then have to split and pivot again.names_patternis a regular expression with one capture group per entry innames_to, in order."member_(\\d+)_(.*)"captures the number and then everything after the second underscore. Two alternatives when the names are simpler:pivot_longer()with.value— In R# Fixed separator, no regex needed. pivot_longer(wide, starts_with("member_"), names_to = c("member", ".value"), names_sep = "_(?=[a-z]+$)") # One value column, name kept whole. pivot_longer(wide, starts_with("crop_"), names_to = "slot", names_prefix = "crop_", values_to = "crop", values_drop_na = TRUE)Speaker notes
values_drop_na = TRUEon that last one matters: a household with three crop slots and two crops has an empty third, and aNArow per unused slot is noise that changes every count.Check the row count against what you expect — In R
stopifnot(nrow(long) == sum(!is.na(wide$member_1_age)) + sum(!is.na(wide$member_2_age)))Speaker notes
A pivot that produces the wrong number of rows is the easiest error to make and the hardest to see, because the result looks correct.Check the row count against what you expect — In R
long |> count(hh, name = "members_found") |> left_join(households |> select(hh, hh_size), by = "hh") |> filter(members_found != hh_size)Speaker notes
More practically, count people per household and compare against the household size the form recorded: An empty result is the check passing. A non-empty one is a list of households where the roster and the reported size disagree — which is a data-quality finding before it is a pivot problem.pivot_wider()and the warning that means something — In Rby_commune_month |> pivot_wider(names_from = month, values_from = cases, values_fill = 0)Speaker notes
The reverse operation builds the wide table a report wants:pivot_wider()and the warning that means something — ExampleWarning: Values from `v` are not uniquely identified; output will contain list-cols.Speaker notes
If more than one row shares the same key, tidyr does not error:pivot_wider()and the warning that means something- That warning means your key is not a key — The cell now holds a list of the several values that collided, and every…
Speaker notes
That warning means your key is not a key. The cell now holds a list of the several values that collided, and every downstream operation on it either fails strangely or silently operates on a list. Do not addvalues_fn = sumto make the warning go away without first finding out why there were duplicates. It is usually one of three things: a re-submitted form, a facility reporting twice in a month, or a grouping variable you forgot to include. Only the first is safe to sum.pivot_wider()and the warning that means something — In Rby_commune_month |> count(commune, month) |> filter(n > 1)pivot_wider()and the warning that means somethingvalues_fill
Speaker notes
That is the diagnostic, and it should come before the pivot rather than after the warning.pivot_wider()and the warning that means something — In Rpivot_wider(..., values_fill = 0)Speaker notes
Safe for a count, where an absent combination genuinely had zero. Wrong for a rate, where the absence means "not computed" rather than "zero percent" — a commune with no screening in March has an undefined GAM rate, not a GAM rate of 0%, and filling it with zero drags every average down.Separating and uniting columns — In R
tibble(x = c("2024-01", "2024-02")) |> separate_wider_delim(x, delim = "-", names = c("year", "month"))Separating and uniting columns — Example
# A tibble: 2 × 2 year month <chr> <chr> 1 2024 01 2 2024 02Separating and uniting columns — In R
separate_wider_delim(x, "-", names = c("year", "month"), too_few = "align_start")Speaker notes
separate_wider_delim()replaced the olderseparate(), and the improvement is that it is strict: a row with the wrong number of pieces is an error rather than a silent truncation. Where ragged input is expected, say so:Separating and uniting columns — In R
unite(df, "key", district, community, sep = "-", remove = FALSE)Speaker notes
The union isunite(), useful for building a composite key before a join:remove = FALSEkeeps the source columns, which you almost always still want.Nesting, briefly — In R
by_commune <- muac |> tidyr::nest(.by = commune) by_commune$data[[1]] # the full tibble for the first communeSpeaker notes
A repeat group can also stay nested rather than being flattened: That is the shape for running the same model or the same summary per commune without a loop. It is worth knowing it exists; for most programme reporting,.byinsummarise()gets you there with less machinery.Where this fits
- The Python course covers the same reshaping under
meltandpivot.
Speaker notes
The Python course covers the same reshaping undermeltandpivot. The vocabulary differs, the failure modes do not: a repeat group that must become rows, a key that turns out not to be unique, and a fill value that is right for a count and wrong for a rate.- The Python course covers the same reshaping under
What comes next
- The data has the right shape.
Speaker notes
The data has the right shape. The last lesson turns everything so far into a function another officer can run on next quarter's export without editing anything but its arguments.