3 Harmonising MICS6 reading outcomes
Cross-country work on foundational reading needs comparable outcomes: word scores, comprehension, and skills indicators pooled across MICS6 surveys. Official country files use different names and layouts, so they do not pool cleanly on their own.
IPUMS MICS solves much of that problem. It harmonises variable names and codes and releases integrated extracts that serve most cross-country analyses well.
It has one decisive limit for reading. Where a survey administered several passages in different languages, the IPUMS extract keeps only the first passage. In multilingual countries a child’s strongest or language-matched attempt may sit on a later passage, so those extracts are a poor base for reading-achievement analysis.
This chapter builds the reading outcomes from the official per-survey
fs.sav files prepared in Download the MICS Data. It
harmonises names, retains every passage (and folds B/C where the scripts
require), derives passage length from the data, builds scores and skills on
a common footing, and writes data/mics6_reading_harmonized.dta with merge
keys so you can still link background variables from an IPUMS extract.
One complete v1.1 R script accompanies the chapter. It lives in data/
next to MICS_Datasets/:
| Script | Language |
|---|---|
data/harmonize-mics-reading-v1.1.R |
R, using the tidyverse |
Download the file from AFLEARN’s GitHub page, set the working directory to the
data/ folder, and run:
It writes data/mics6_reading_harmonized.dta. The sections below explain what
each stage does and why, so you can adapt the logic rather than treat it as a
black box.
3.1 Before you start
You need data/MICS_Datasets/<SURVEY>/fs.sav in place from
Download the MICS Data (prepare-mics-fs.R). The
fs.sav file holds the Foundational Learning Skills questionnaire,
administered to children aged 5–17, with the reading assessment itself
targeted at ages 7–14. The guide workspace looks like this:
mics-guide/
└── data/
├── MICS_Datasets/
│ └── BEN_2021_MICS6_v01_M/fs.sav
└── harmonize-mics-reading-v1.1.R
Currently, 18 country codes are supported:
BEN CAF COD COM GHA GMB GNB LSO MDG MWI NGA SLE STP SWZ TCD TGO TUN ZWE
Any other survey folder in data/MICS_Datasets/ is skipped with a message.
3.2 The harmonisation pipeline at a glance
for each survey folder
1. identify the survey from its folder name
2. fix questionnaire numbering quirks
3. harmonise variable names
4. repair known data problems (SLE swap; early refusal cleaning)
5. assign passage language
6. derive passage length from the data
7. Move Malawi's Chichewa-only attempts into the main slots
8. build reading, comprehension, and practice scores
9. build accuracy and foundational skills
10. classify the assessment outcome (codes 0–9)
11. add merge keys and keep the analysis variables
append all surveys, then label and save
3.3 Identify the survey
Folder names carry an ISO3 country code and a year, as in
BEN_2021_MICS6_v01_M. A regular expression pulls both out. Anything that
does not match, is not in the supported list, or has no fs.sav, is skipped.
for (folder in list.dirs(root, recursive = FALSE, full.names = FALSE)) {
m <- regmatches(folder, regexec("^([A-Za-z]{3})_([0-9]{4})_", folder))[[1]]
if (length(m) == 0) next
iso <- toupper(m[2])
year <- m[3]
if (!iso %in% supported) next
d <- read_sav(file.path(root, folder, "fs.sav")) %>% zap_labels()
}zap_labels() strips the SPSS value labels but keeps the
underlying numeric codes. Fresh labels are applied at the very end.
3.4 Fix questionnaire numbering
Two surveys need repairs before renaming. Zimbabwe stores five comprehension
items in FL22B through FL22F. Comoros shifts FL21BE/FL21BF by one
position (it does not rename the whole FL21B* series onto FL22*).
if (iso == "ZWE") {
d <- d %>% rename(FL22D = "FL22E", FL22E = "FL22F")
}
if (iso == "COM") {
d <- d %>% rename(FL21BD = "FL21BE", FL21BE = "FL21BF")
}These renames are deliberately not wrapped in capture. If the data ever
changes shape, you want a loud error here.
3.5 Harmonise variable names
A slim set of renames maps questionnaire codes onto readable names used
downstream. Each rename is conditional: a survey that never asked a question
keeps its own variables and the script carries on. A B or C suffix marks
the second or third reading passage.
d <- cap_rename(d, c(
age = "CB3", consent = "FL1", child_consent = "FL3",
likestory = "FL10", words_att = "FL20A", words_incorrect = "FL20B"
# ... full map in data/harmonize-mics-reading-v1.1.R
))3.5.1 Chad school language
For Chad only, FL9A can supply lang_school, and non-missing FL9B values
are copied in afterwards:
3.5.2 Country-specific comprehension renames
Most surveys store passage-1 comprehension in FL22A–FL22E. Four surveys
store passage 1 in FL21BA–FL21BE and passage 2 in FL22A–FL22E:
| Surveys | Passage 1 | Passage 2 |
|---|---|---|
| Default (everyone else) | FL22A–FL22E → read_comp_1–5 |
other codes (FL122*, FLB22*, …) |
| SWZ, NGA, ZWE, COM | FL21BA–FL21BE → read_comp_1–5 |
FL22A–FL22E → read_compB_1–5 |
COM is excluded from the default FL22 rename so those items remain available
for passage B.
After renaming, the script guarantees that the variables the pipeline needs
exist, creating them as missing where a survey omitted them (ensure).
3.6 Repair known data problems
3.6.1 Sierra Leone word swap
Some Sierra Leone records have words-attempted and words-incorrect stored the wrong way round. The fix swaps them where attempted is smaller than incorrect, and only where the incorrect count is actually recorded (not missing). A careless “less than or equal to missing” style check would create false zeros.
3.6.2 Early refusal cleaning
In São Tomé and Príncipe, Madagascar, and the Central African Republic
(CAF, not CAR), children who refused the story or failed practice can
still have non-missing word counts. Those counts are cleared before
scores are built:
if (iso %in% c("STP", "MDG", "CAF")) {
drop1 <- !eq(d$likestory, 1) | ne_obs(d$practice_correct, 1)
d <- d %>% mutate(
words_att = if_else(drop1, NA_real_, words_att),
words_incorrect = if_else(drop1, NA_real_, words_incorrect)
)
}Madagascar applies the same idea to passages B and C using FL110 / FL210
and the practice-B/C items.
3.7 Passage language
Language is assigned next, before length. Languages for the single-language surveys are English (Ghana, Sierra Leone, Gambia), French (Benin, Central African Republic, Chad, Comoros, DR Congo, Togo), Arabic (Tunisia) and Portuguese (São Tomé, Guinea-Bissau). For these countries, the language is assigned to the passage language variable.
| Group | Code | Surveys |
|---|---|---|
| English | 1010 | GHA, SLE, GMB |
| French | 1020 | BEN, CAF, TCD, COM, COD, TGO |
| Portuguese | 1040 | STP, GNB |
| Arabic | 2010 | TUN |
For Nigeria, Eswatini, Lesotho, Madagascar, Malawi, and Zimbabwe, the variables used to identify the passage language differ across surveys. The harmonisation process takes this into account (see the scripts). For instance, Zimbabwe is the most involved, because the passage language has to be inferred from the language of instruction, then from the language spoken at home, then from the child’s own choice of story
if (iso == "ZWE") {
d <- d %>% ensure(c("FL10C", "FL21D")) %>% mutate(
passage_language = case_when(
eq(lang_school, 1) ~ 1010,
eq(lang_school, 2) ~ 3140,
eq(lang_school, 3) ~ 3150,
present(lang_school) & lang_school >= 7 & lang_school <= 9 ~ NA_real_,
TRUE ~ as.numeric(lang_school)
),
passage_language = if_else(is.na(words_att), NA_real_, passage_language),
passage_language = case_when(
eq(lang_home, 1) & is.na(passage_language) & present(words_att) ~ 1010,
eq(lang_home, 2) & is.na(passage_language) & present(words_att) ~ 3140,
eq(lang_home, 3) & is.na(passage_language) & present(words_att) ~ 3150,
TRUE ~ passage_language
),
passage_language = case_when(
eq(FL10C, 1) & present(words_att) ~ 1010,
eq(FL10C, 2) & present(words_att) ~ 3140,
eq(FL10C, 3) & present(words_att) ~ 3150,
TRUE ~ passage_language
),
passage_length = if_else(present(words_att), 72, NA_real_),
passageB_length = if_else(present(wordsB_att), 62, NA_real_)
)
}3.8 Passage length from the data
For each language, passage length is the maximum words attempted among children who read that language:
\[ \text{passage_length} = \max(\text{words_att} \mid \text{passage_language}) \]
d$passage_length <- passage_len_by_lang(d$words_att, d$passage_language)
# same idea for B/C when those passages existThis puts different languages on a common accuracy scale without maintaining a country-by-country word-count table. It assumes that, within a language, at least one child reached (or nearly reached) the end of the passage. If every child stopped early, the estimated length would be too short and accuracy could exceed 1.
3.9 Chichewa-only attempts
In Malawi, English is passage A and Chichewa is passage B. Some children attempt only Chichewa. Those records are moved into the main slots so the pooled file treats them as the child’s primary attempt, and the B slots are cleared:
if (iso == "MWI") {
tmp <- is.na(d$words_att) & present(d$wordsB_att)
d <- d %>% mutate(
words_att = if_else(tmp, wordsB_att, words_att),
words_incorrect = if_else(tmp, wordsB_incorrect, words_incorrect),
passage_language = if_else(tmp, passageB_language, passage_language),
passage_length = if_else(tmp, passageB_length, passage_length)
# ... comprehension items, then blank B
)
}3.10 Reading, comprehension, and practice scores
With cleaned word counts in place:
\[ \text{reading_score} = \text{words_att} - \text{words_incorrect} \]
Comprehension scores count how many of the five items are coded 1 (correct),
and are missing when the first item was never asked. Practice is passed only
if the practice sentence and both practice questions are correct.
fail_practice is set if any passage’s practice was failed.
3.11 Accuracy and foundational reading skills
3.11.1 Accuracy
Accuracy is the share of the passage the child read correctly:
\[ \text{reading_accuracy} = \frac{\text{reading_score}}{\text{passage_length}} \]
Dividing by passage_length puts different languages on a common scale. The
same formula is used for a second passage as readingB_accuracy when that
passage exists.
3.11.2 Foundational reading skills
Foundational reading skills is 1 only when both hold (among children with a reading score):
- All five comprehension questions are correct: \(\text{read_comp_score} = 5\).
- At least 90% of the passage words are correct:
\[ \text{cutoff} = \lfloor 0.9 \times \text{passage_length} \rfloor \]
\[ \text{reading_skills} = \begin{cases} 1 & \text{if } \text{read_comp_score} = 5 \text{ and } \text{reading_score} \ge \text{cutoff} \\ 0 & \text{otherwise (when } \text{reading_score} \text{ is non-missing)} \end{cases} \]
3.12 Classify the assessment outcome
reading_status records why a child does or does not have a reading score.
Codes 2–4 only fill gaps; codes 5–8 overwrite; code 9 catches anyone still
unclassified (inconsistent data, for example comprehension without word
counts):
| Code | Meaning |
|---|---|
| 0 | Not interviewed |
| 1 | Age under 7 or over 14 |
| 2 | Caregiver consent not given |
| 3 | Child consent not given |
| 4 | Assessment language does not match the child’s language |
| 5 | Child does not want to read the story |
| 6 | Failed the practice sentence and questions |
| 7 | Attempted a reading passage |
| 8 | Read 90% of words and answered all comprehension questions correctly |
| 9 | Not classified — data inconsistent |
d <- d %>% mutate(
reading_status = if_else(present(interview_result) & interview_result > 1, 0, NA_real_),
reading_status = if_else(present(age) & (age < 7 | age > 14), 1, reading_status),
reading_status = if_else(ne_obs(consent, 1) & is.na(reading_status), 2, reading_status),
reading_status = if_else(ne_obs(child_consent, 1) & is.na(reading_status), 3, reading_status),
reading_status = if_else(eq(lang_mismatch, 1) & is.na(reading_status), 4, reading_status),
reading_status = if_else(eq(child_refuses_read, 1), 5, reading_status),
reading_status = if_else(eq(fail_practice, 1), 6, reading_status),
reading_status = if_else(present(max_reading_score), 7, reading_status),
reading_status = if_else(eq(max_reading_skills, 1), 8, reading_status),
reading_status = if_else(is.na(reading_status), 9, reading_status)
)3.13 Merge keys and variable selection
Each survey is uniquely identified by cluster, household and line number.
Those are copied to the IPUMS-MICS names cluster, hhno, and linech.
Country and year are added, then the file is cut to a slim analysis set:
3.14 What the harmonized dataset contains
One row per child in the Foundational Learning Skills module, across all supported surveys.
| Variable | Meaning |
|---|---|
country_iso3, year |
Survey identification |
cluster, hhno, linech |
Merge keys for IPUMS-MICS (copies of HH1, HH2, LN) |
reading_status |
Outcome of the reading assessment, coded 0–9 |
words_att, words_incorrect |
Raw word counts for passage 1 |
reading_score |
Words read correctly, passage 1 |
reading_accuracy |
reading_score / passage_length |
reading_skills |
90% of words plus all comprehension correct |
read_comp_1–read_comp_5, read_comp_score |
Comprehension items and total |
practice_*, practice_outcome |
Practice sentence and questions |
passage_language, passage_length |
Passage characteristics (length from data) |
*B_* equivalents |
The same measures for the second passage |
max_reading_score, max_reading_accuracy, max_reading_skills |
Best result across passages |
3.15 Known limitations
- Data-driven passage length uses the maximum words attempted within each language. If nobody in that language reached the end of the passage, the estimated length is too short and accuracy can exceed 1.
- Year differences within a country are not modelled. Tunisia appears twice (2018 and 2023) and both use the same language code.
3.16 Checks after running
library(tidyverse)
library(haven)
d <- read_dta("data/mics6_reading_harmonized.dta")
count(d, country_iso3, year)
count(d, reading_status)
summary(d$reading_accuracy)
d %>% filter(reading_status == 8) %>% nrow()Accuracy should sit between 0 and 1. Values above 1 point to an underestimated passage length for that language.
3.17 Next steps
With data/mics6_reading_harmonized.dta in place:
- Follow R Tutorial to analyse it in R.
- Follow Stata Tutorial to analyse it in Stata.
- Build non-reading FS themes (schooling, labour, discipline, numeracy setup,
and more) with Harmonising MICS6 FS background themes
and merge on
country_iso3,year, andHH1/HH2/LN.
Because the file carries cluster, hhno and linech, it can also be merged
onto an IPUMS-MICS extract to combine these reading outcomes with harmonised
household and background variables.