This report analyses 3,056 subnational estimates covering 27 indicators from 9 DHS survey(s) in Kenya (1989–2022). The most recent survey (2022) reports results for 54 regions.
The analysis answers four questions:
top_id <- focus_ids[1]
d_top <- dhs |> filter(indicator_id == top_id, year == latest_year)
hi <- d_top |> slice_max(value, n = 1, with_ties = FALSE)
lo <- d_top |> slice_min(value, n = 1, with_ties = FALSE)
cat("::: {style=\"background:#F4F7F9;border-left:5px solid #2A9D8F;padding:12px 18px;border-radius:6px;\"}\n\n")Headline findings (auto-generated)
cat(sprintf("- On *%s* (%s), values range from **%.1f** in **%s** to **%.1f** in **%s** — a gap of **%.1f points**.\n",
lab[[top_id]], latest_year, lo$value, lo$region, hi$value, hi$region, hi$value - lo$value))cat(sprintf("- The report profiles **%d headline indicators** and groups regions into **%d development clusters**.\n",
length(focus_ids), k_clusters_value))The data come from the Demographic and Health Surveys (DHS) Program, nationally representative household surveys that provide subnational estimates of population, health and nutrition indicators. Each row is one indicator estimate for one region in one survey.
tibble::tribble(
~Field, ~Meaning,
"Indicator / IndicatorId", "Name and code of the measure (e.g. a percentage or rate)",
"Location", "Subnational region the estimate refers to",
"SurveyYear / SurveyId", "Survey the estimate comes from",
"Value", "Point estimate",
"CILow / CIHigh", "95% confidence interval bounds",
"DenominatorWeighted / Unweighted", "Number of people/cases the estimate is based on",
"IndicatorType", "Type of measure (I = percentage, R = rate, V = value, etc.)",
"IsPreferred", "Flags the preferred estimate when duplicates exist"
) |> knitr::kable()| Field | Meaning |
|---|---|
| Indicator / IndicatorId | Name and code of the measure (e.g. a percentage or rate) |
| Location | Subnational region the estimate refers to |
| SurveyYear / SurveyId | Survey the estimate comes from |
| Value | Point estimate |
| CILow / CIHigh | 95% confidence interval bounds |
| DenominatorWeighted / Unweighted | Number of people/cases the estimate is based on |
| IndicatorType | Type of measure (I = percentage, R = rate, V = value, etc.) |
| IsPreferred | Flags the preferred estimate when duplicates exist |
IsPreferred.survey_geo |>
select(Survey = survey_id, Year = year, Regions = n_regions,
Indicators = n_indicators, `Overlap with latest` = overlap) |>
mutate(`Overlap with latest` = percent(`Overlap with latest`, accuracy = 1)) |>
DT::datatable(rownames = FALSE, options = list(dom = "t", pageLength = 20))The table demonstrates that geographical coverage is a critical consideration in analysing Kenya’s subnational health and development indicators. Although the historical surveys provide valuable context, the 2014 and 2022 DHS rounds offer the strongest starting point for regional comparisons because both cover 54 regions and report complete overlap with the latest framework. A robust analysis should therefore prioritise these two rounds while treating earlier surveys and the MIS rounds as supplementary evidence where comparable data are available.
Comparable baseline survey selected: 2014.
cov_plot <- dhs |>
filter(indicator_id %in% pool$indicator_id) |>
count(indicator_id, year, name = "n_regions") |>
mutate(ind_short = lab[indicator_id])
top_cov <- cov_tbl |> filter(indicator_id %in% pool$indicator_id) |>
arrange(desc(n_surveys), desc(n_obs)) |> slice_head(n = 25) |> pull(indicator_id)
cov_plot |>
filter(indicator_id %in% top_cov) |>
mutate(ind_short = fct_reorder(ind_short, n_regions, sum),
year = factor(year)) |>
ggplot(aes(year, ind_short, fill = n_regions)) +
geom_tile(colour = "white", linewidth = .6) +
geom_text(aes(label = n_regions), colour = "white", size = 3, fontface = "bold") +
scale_fill_gradient(low = pal$grey, high = pal$navy, name = "Regions reported") +
labs(title = "Indicator coverage across surveys",
subtitle = "Number of regions with an estimate (top 25 indicators by coverage)",
x = NULL, y = NULL, caption = src_caption)The heatmap illustrates substantial changes in geographical and indicator coverage across Kenya’s DHS and MIS surveys between 1989 and 2022. The surveys conducted between 1989 and 1998 generally reported estimates across seven regions, increasing to eight regions in 2003 and 2008. In contrast, the 2014 and 2022 DHS surveys reported estimates across as many as 54 regions for several indicators, providing broader subnational coverage and greater opportunities for analysing regional differences in health and development outcomes.
Several indicators, including total fertility rate, contraceptive use, child mortality, childhood vaccination, and child nutritional status, appear across multiple DHS rounds. Their repeated availability provides a potential basis for examining changes in health and demographic outcomes over time. However, indicator coverage is not consistent across all surveys. Some measures are available only in selected years, while the 2015 and 2020 MIS surveys report estimates across five regions for the indicators shown. Consequently, comparisons across the entire survey period should focus on indicators with consistent definitions, measurement methods, and sufficiently comparable geographical coverage.
Overall, the findings suggest that the 2014 and 2022 DHS surveys are promising starting points for subnational comparisons because of their extensive regional coverage. Earlier DHS rounds can provide historical context, while the MIS surveys may offer complementary evidence for specific indicators. Nevertheless, differences in the number of regions reporting estimates should not automatically be interpreted as changes in administrative boundaries or health outcomes. Before conducting trend analysis, it is important to verify indicator definitions, survey methodologies, and regional identifiers to ensure that observed differences reflect meaningful changes rather than variations in data coverage or reporting practices.
Subnational estimates rest on smaller samples than national ones, so uncertainty matters.
prec <- dhs |>
filter(indicator_id %in% focus_ids, !is.na(ci_low), !is.na(ci_high)) |>
mutate(ci_width = ci_high - ci_low)
p1 <- prec |>
filter(!is.na(denom_u), denom_u > 0) |>
ggplot(aes(denom_u, ci_width)) +
geom_point(alpha = .35, colour = pal$teal) +
geom_smooth(method = "loess", se = FALSE, colour = pal$navy, formula = y ~ x) +
scale_x_log10(labels = comma) +
labs(title = "Bigger samples, tighter intervals",
subtitle = "95% CI width vs unweighted sample size (log scale)",
x = "Unweighted denominator", y = "CI width")
p2 <- prec |>
ggplot(aes(ci_width)) +
geom_histogram(fill = pal$navy, colour = "white", bins = 30) +
labs(title = "Distribution of CI widths", x = "CI width", y = "Estimates")
p1 + p2 + plot_annotation(caption = src_caption)The figure examines the relationship between sample size and the precision of estimates using 95% confidence interval (CI) widths. The left panel is intended to show how confidence interval width varies with the unweighted sample size, while the right panel displays the distribution of CI widths across estimates. In general, larger sample sizes tend to produce narrower confidence intervals, indicating greater precision, whereas smaller samples often result in wider intervals and greater uncertainty. However, the relationship may also depend on the indicator being measured and the survey design.
The histogram on the right shows that most confidence interval widths are concentrated approximately between 20 and 40, with the highest frequencies occurring around 25–35. This suggests that many estimates have relatively similar levels of uncertainty, although a smaller number have considerably wider intervals, including values above 60. These wider intervals indicate less precise estimates and may warrant caution when interpreting or comparing the corresponding indicators. The distribution also suggests that precision varies across the estimates rather than being uniform throughout the dataset.
Overall, the figure highlights the importance of considering statistical uncertainty when interpreting Kenya’s subnational health and development indicators. Estimates based on limited samples or with wide confidence intervals should be interpreted cautiously, particularly when comparing regions or identifying differences over time. However, the chart alone does not establish that sample size explains all observed variation in confidence interval widths. Furthermore, the left panel appears to be missing its plotted points in the supplied image, so the relationship between sample size and CI width cannot be directly assessed from the displayed figure.
cov_tbl |>
select(Indicator = indicator, Type = ind_type, Surveys = n_surveys,
Regions = n_regions, Observations = n_obs, First = first_year, Last = last_year) |>
DT::datatable(filter = "top", rownames = FALSE, extensions = "Buttons",
options = list(pageLength = 8, scrollX = TRUE, dom = "Bfrtip",
buttons = c("csv", "excel")))The analysis focuses on the 6 best-covered indicators that are available in the latest survey:
Each panel ranks regions in the 2022 survey. Horizontal bars show the 95% confidence interval; the dashed line is the population-weighted average across regions.
plot_rank <- function(id) {
d <- dhs |> filter(indicator_id == id, year == latest_year) |>
mutate(region = fct_reorder(region, value))
avg <- wmean(d$value, d$denom_w)
d <- d |> mutate(side = if_else(value >= avg, "Above average", "Below average"))
ggplot(d, aes(value, region)) +
geom_linerange(aes(xmin = ci_low, xmax = ci_high), colour = pal$grey,
linewidth = .9, na.rm = TRUE) +
geom_point(aes(colour = side), size = 3) +
geom_vline(xintercept = avg, linetype = 2, colour = pal$navy) +
scale_colour_manual(values = c("Above average" = pal$teal, "Below average" = pal$orange),
name = NULL) +
labs(title = str_wrap(lab[[id]], 70),
subtitle = sprintf("Weighted regional average: %.1f | Survey: %s", avg, latest_year),
x = "Estimate (with 95% CI)", y = NULL, caption = src_caption)
}for (id in focus_ids) {
cat("\n\n### ", str_trunc(lab[[id]], 38), "\n\n", sep = "")
print(plot_rank(id))
cat("\n\n")
}trend <- dhs |>
filter(indicator_id %in% focus_ids) |>
group_by(indicator_id, ind_short, year) |>
summarise(national = wmean(value, denom_w), lo = min(value), hi = max(value),
n_regions = n(), .groups = "drop")p <- trend |>
mutate(ind_short = str_wrap(ind_short, 38)) |>
ggplot(aes(year, national)) +
geom_ribbon(aes(ymin = lo, ymax = hi), fill = pal$teal, alpha = .18) +
geom_line(colour = pal$navy, linewidth = 1) +
geom_point(aes(text = paste0("Year: ", year, "<br>Weighted avg: ", round(national, 1),
"<br>Range: ", round(lo, 1), " – ", round(hi, 1),
"<br>Regions: ", n_regions)),
colour = pal$navy, size = 2.5) +
facet_wrap(~ind_short, scales = "free_y") +
labs(title = "Trends in the weighted regional average",
subtitle = "Shaded band = lowest to highest region",
x = NULL, y = NULL)
ggplotly(p, tooltip = "text") |> layout(margin = list(t = 70))Reading note. Because the set of reporting regions changes between surveys, the line reflects a weighted average of available regions and is best read as a directional guide, not an official national series.
The chart tracks the weighted regional averages of five key development and health indicators in Kenya from roughly 1990 to 2023, highlighting both national progress and persistent disparities. The data shows a clear, positive trajectory for access to essential services and education: the share of fully vaccinated children has risen from around 45% to over 80%, while households with electricity have seen a dramatic, steady increase from near zero to roughly 55%. Similarly, the percentage of women with secondary or higher education has steadily climbed from approximately 55% to nearly 60%. These upward trends indicate broad, sustained improvements in healthcare access, infrastructure, and female education across Kenya’s regions over the past three decades.
Conversely, the chart reveals significant declines in fertility and child mortality, which are strong indicators of improved public health and family planning. The total fertility rate has dropped sharply, from over 6 children per woman in the early 1990s to around 3.5, reflecting a major demographic shift. Likewise, both infant and under-five mortality rates have fallen substantially, with under-five mortality plummeting from roughly 100 to under 50 deaths per 1,000 live births. A critical feature of the chart, however, is the shaded band representing the range between the best- and worst-performing regions. While the averages have improved, the persistent width of these bands—particularly for vaccination, electricity, and mortality—demonstrates that substantial regional inequalities remain, meaning national progress does not yet benefit all areas equally.
How wide is the spread between regions, and is it narrowing?
ineq <- dhs |>
filter(indicator_id %in% focus_ids) |>
group_by(indicator_id, ind_short, year) |>
filter(n() >= 3) |>
summarise(
n_regions = n(), mean = mean(value), min = min(value), max = max(value),
range = max - min, ratio = if_else(min > 0, max / min, NA_real_),
cv = sd(value) / mean * 100, .groups = "drop"
)
ineq |>
mutate(across(c(mean, min, max, range, ratio, cv), ~ round(.x, 2))) |>
select(Indicator = ind_short, Year = year, Regions = n_regions, Mean = mean,
Min = min, Max = max, Range = range, `Max/Min` = ratio, `CV %` = cv) |>
DT::datatable(rownames = FALSE, filter = "top", options = list(pageLength = 8, scrollX = TRUE))ineq |>
mutate(ind_short = str_wrap(ind_short, 38)) |>
ggplot(aes(year, range)) +
geom_col(fill = pal$navy, width = 1.2, alpha = .9) +
geom_text(aes(label = round(range, 1)), vjust = -.4, size = 3, colour = pal$navy) +
facet_wrap(~ind_short, scales = "free_y") +
scale_y_continuous(expand = expansion(mult = c(0, .15))) +
labs(title = "The gap between best- and worst-performing regions",
subtitle = "Range (max − min) across regions in each survey",
x = NULL, y = "Range", caption = src_caption)This chart illustrates the absolute gap between the best- and worst-performing regions across Kenya for five key health and development indicators over the last three decades. The data reveals a highly concerning trend: despite national averages showing improvement in previous charts, regional inequalities have widened significantly. For example, the gap in full child vaccination has more than doubled, growing from a range of about 26 points in the 1990s to 73 points by 2022. Similarly, the disparity in access to electricity has surged from a 47-point gap in the early 1990s to 78 points in recent years. Alarmingly, the regional gap in women receiving secondary education has also consistently increased, reaching a staggering 63.8 points, suggesting that the benefits of educational progress are highly concentrated in specific areas rather than being distributed evenly.
The mortality and fertility indicators present a more nuanced but still challenging picture. While the absolute gap in under-five mortality has generally narrowed from its peak of 166 deaths per 1,000 live births around 1998 down to 58 recently, infant mortality has fluctuated with no clear, sustained reduction in inequality (ending at a 46-point gap). The total fertility rate shows extreme volatility, dropping to a gap of just 1.2 in 2020 before spiking back up to 5.1, indicating that family planning and demographic transitions are occurring at vastly different paces across regions. Ultimately, this chart underscores that while Kenya as a whole is making developmental strides, these gains are highly uneven, leaving certain regions far behind and highlighting a critical need for targeted, equity-focused policy interventions.
b <- dhs |> filter(year == baseline_year) |>
transmute(indicator_id, ind_short, region, v0 = value,
se0 = (ci_high - ci_low) / (2 * 1.96))
l <- dhs |> filter(year == latest_year) |>
transmute(indicator_id, region, v1 = value,
se1 = (ci_high - ci_low) / (2 * 1.96))
chg <- inner_join(b, l, by = c("indicator_id", "region")) |>
mutate(
diff = v1 - v0,
z = diff / sqrt(se0^2 + se1^2),
sig = replace_na(abs(z) > 1.96, FALSE),
direction = case_when(
sig & diff > 0 ~ "Significant increase",
sig & diff < 0 ~ "Significant decrease",
TRUE ~ "No significant change"
)
)cat(sprintf("Comparing **%s** with **%s** across `%d` region–indicator pairs. A change is flagged *significant* when a two-sided z-test on the difference (using standard errors derived from the confidence intervals, assuming independent surveys) exceeds 1.96.\n",
baseline_year, latest_year, nrow(chg)))Comparing 2014 with 2022 across
974 region–indicator pairs. A change is flagged
significant when a two-sided z-test on the difference (using
standard errors derived from the confidence intervals, assuming
independent surveys) exceeds 1.96.
chg |>
filter(indicator_id %in% focus_ids) |>
count(ind_short, direction) |>
mutate(ind_short = str_wrap(ind_short, 45)) |>
ggplot(aes(n, ind_short, fill = direction)) +
geom_col() +
scale_fill_manual(values = c("Significant increase" = pal$teal,
"No significant change" = pal$grey,
"Significant decrease" = pal$red), name = NULL) +
labs(title = "How many regions changed significantly?",
subtitle = sprintf("%s to %s", baseline_year, latest_year),
x = "Number of regions", y = NULL, caption = src_caption)This chart tracks the statistical significance of changes across Kenyan regions between 2014 and 2022, revealing a striking pattern of developmental stagnation. For three of the six indicators—women with secondary education, total fertility rate, and full child vaccination—not a single region experienced a statistically significant change. Access to electricity saw the most movement, yet still more than half of the regions (roughly 30 out of 47) remained stagnant. This overwhelming gray dominance across the chart suggests that while national averages may have shown gradual improvements over the past decade, the pace of change at the local level has been too slow or too uneven to register as statistically meaningful in most areas.
The few significant changes that did occur are largely negative, particularly concerning child health. The under-five mortality rate experienced significant decreases in roughly 15 regions (indicated in orange), meaning the situation worsened, with only one region showing a significant improvement (in teal). Infant mortality similarly saw a significant increase in several regions without any significant decreases. This indicates a worrying backward slide in child survival in specific areas of the country, countering the broader narrative of national improvement. Ultimately, this chart highlights a critical policy challenge: the “tyranny of the average” is masking severe stagnation, and in some cases regression, at the subnational level, emphasizing the urgent need for targeted interventions rather than blanket national strategies.
d <- chg |> filter(indicator_id == spot_id) |>
mutate(region = fct_reorder(region, v1))
ggplot(d, aes(y = region)) +
geom_segment(aes(x = v0, xend = v1, yend = region, colour = direction), linewidth = 1.2) +
geom_point(aes(x = v0), colour = pal$grey, size = 3) +
geom_point(aes(x = v1, colour = direction), size = 3.5) +
scale_colour_manual(values = c("Significant increase" = pal$teal,
"No significant change" = pal$grey,
"Significant decrease" = pal$red), name = NULL) +
labs(title = str_wrap(lab[[spot_id]], 70),
subtitle = sprintf("Grey dot = %s, coloured dot = %s", baseline_year, latest_year),
x = "Estimate", y = NULL, caption = src_caption)This dumbbell chart visualizes the Total Fertility Rate (TFR) across Kenyan regions in 2014 compared to 2022, highlighting a persistent and stark geographical divide in population dynamics. The regions are sorted by their 2022 estimates, revealing a clear continuum from the lowest fertility areas at the bottom (Nairobi, Kiambu, and Machakos, hovering around 2 to 3 children per woman) to the highest at the top (Mandera, Wajir, and Turkana, which remain near 7 to 8 children per woman). Although the overwhelming presence of a single color—grey—for every connector line confirms that there were no statistically significant changes within individual regions over this eight-year period, the general trend of the dots leans slightly leftward. This suggests that fertility is slowly declining across the board, but the relative ranking of the regions remains firmly entrenched.
The most striking takeaway from this visual is the sheer magnitude of the gap between the two extremes. The fertility rate in Mandera is nearly four times higher than in Nairobi, representing vastly different realities in terms of women’s empowerment, access to education, family planning, and healthcare infrastructure. Furthermore, the chart highlights the conservative nature of statistical significance when dealing with survey data; even though many regions experienced a noticeable drop in average fertility between 2014 and 2022, the wide confidence intervals associated with subnational sampling mean these shifts cannot be flagged as definitive in a single region. Ultimately, this visual reinforces the report’s broader narrative: while the national demographic transition is slowly progressing, it is happening at wildly different speeds, leaving arid and semi-arid northern regions significantly behind the more urbanized and central areas of the country.
chg |>
filter(indicator_id %in% focus_ids) |>
group_by(indicator_id) |>
mutate(rel = diff / sd(diff, na.rm = TRUE)) |>
ungroup() |>
mutate(ind_short = str_wrap(ind_short, 22),
region = fct_reorder(region, diff, .fun = mean)) |>
ggplot(aes(ind_short, region, fill = rel)) +
geom_tile(colour = "white") +
geom_text(aes(label = if_else(sig, "*", "")), size = 5, colour = "black") +
scale_fill_gradient2(low = pal$red, mid = "white", high = pal$teal, midpoint = 0,
name = "Standardised\nchange") +
labs(title = "Where did things move, and by how much?",
subtitle = "Change in each indicator scaled by its own variability; * = statistically significant",
x = NULL, y = NULL, caption = src_caption) +
theme(axis.text.x = element_text(size = 8))This heatmap provides a comprehensive, standardized view of how six key indicators shifted across Kenya’s regions between 2014 and 2022, revealing a stark contrast between steady educational progress and volatile health outcomes. The most visually dominant feature is the rightmost column: nearly every region shows a solid teal block for “Women with secondary or higher education,” indicating widespread, standardized increases in female education. Conversely, access to electricity (second column) shows a generally positive but much weaker pattern, while full vaccination (first column) is largely stagnant or slightly negative across the board.
The health indicators paint a highly localized picture of distress, particularly in child survival. Infant and under-five mortality rates are predominantly shaded in salmon (indicating a standardized decrease in health, since higher mortality is bad), with under-five mortality showing severe, statistically significant (*) worsening concentrated in the country’s coastal and eastern regions (like Kilifi, Kwale, Tana River, and Garissa). For total fertility rate, the picture is mixed, with declines (teal) clustered in the central and western regions (like Kiambu, Nyeri, and Kakamega), while arid northern regions show stagnation or slight increases. Ultimately, the chart demonstrates that while national education drives progress almost everywhere, health interventions and infrastructure are failing to keep pace and are significantly deteriorating in specific, already vulnerable geographic pockets.
cat("*Not enough complete indicators and regions in the latest survey for multivariate analysis.*\n")cm <- cor(mat, use = "pairwise.complete.obs")
colnames(cm) <- rownames(cm) <- mat_labels
ord <- hclust(as.dist(1 - cm))$order
as_tibble(cm, rownames = "a") |>
pivot_longer(-a, names_to = "b", values_to = "r") |>
mutate(a = factor(a, levels = rownames(cm)[ord]),
b = factor(b, levels = rownames(cm)[ord])) |>
ggplot(aes(a, b, fill = r)) +
geom_tile(colour = "white") +
geom_text(aes(label = sprintf("%.2f", r)), size = 2.6) +
scale_fill_gradient2(low = pal$red, mid = "white", high = pal$navy,
limits = c(-1, 1), name = "Pearson r") +
labs(title = "Correlation between indicators across regions",
x = NULL, y = NULL, caption = src_caption) +
theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7),
axis.text.y = element_text(size = 7))This correlation matrix reveals profound structural relationships between health, education, and demographic indicators across Kenya’s regions, with two distinct “clusters” of variables emerging. The strongest negative relationship in the entire dataset is between the Total Fertility Rate (TFR) and modern contraceptive use (r = -0.89). This is part of a larger pattern: high fertility rates are strongly linked to high rates of stunting and underweight children (r = 0.77 and 0.64, respectively), creating a vicious cycle of rapid population growth and poor child nutrition in certain areas. Conversely, regions with high contraceptive use and higher education for women have significantly lower fertility rates and better-nourished children, illustrating how female empowerment and family planning are foundational to broader health outcomes.
Furthermore, the matrix highlights a clear “urban advantage” tied to infrastructure and education. Access to electricity, women’s secondary education, and contraceptive use are all strongly correlated with one another (ranging from r = 0.61 to 0.99). Regions where women marry later and are more educated also have better vaccination coverage and lower fertility. However, the data also shows that infant and under-five mortality rates are highly correlated with each other (r = 0.86) but are surprisingly disconnected from most other variables—showing only weak negative correlations with electricity and education. This suggests that while education and infrastructure drive down fertility and improve nutrition, they do not automatically guarantee child survival, which likely depends on more localized factors like healthcare access and disease environments. Ultimately, the matrix confirms that development is holistic: education, family planning, infrastructure, and nutrition move together, while high fertility and child malnutrition remain deeply intertwined.
pairs <- as_tibble(cm, rownames = "a") |>
pivot_longer(-a, names_to = "b", values_to = "r") |>
filter(a < b) |>
arrange(desc(abs(r)))
top_pair <- pairs[1, ]
sc <- tibble(region = rownames(mat),
x = mat[, which(mat_labels == top_pair$a)],
y = mat[, which(mat_labels == top_pair$b)])
ggplot(sc, aes(x, y)) +
geom_smooth(method = "lm", formula = y ~ x, colour = pal$navy,
fill = pal$teal, alpha = .15) +
geom_point(colour = pal$teal, size = 3) +
ggrepel::geom_text_repel(aes(label = region), size = 3, max.overlaps = 15,
colour = "grey25") +
labs(title = "The strongest regional relationship",
subtitle = sprintf("r = %.2f", top_pair$r),
x = str_wrap(top_pair$a, 50), y = str_wrap(top_pair$b, 50), caption = src_caption)This scatterplot illustrates an exceptionally strong, near-perfect linear relationship (r = 0.99) between two measures of contraceptive use across Kenya’s regions: the percentage of married women using any method versus those using any modern method. The tight clustering of regions along the regression line indicates that traditional or folk methods of family planning make up only a tiny, consistent fraction of overall contraceptive use. In other words, almost everywhere in Kenya, if a woman is using contraception, she is using a modern method.
The chart also acts as a clear map of Kenya’s demographic divide, with regions arrayed along the diagonal based on their overall development levels. The top-right quadrant is densely clustered with high-performing, largely urban or central regions like Embu, Nyeri, Kiambu, and Nairobi, where contraceptive use exceeds 60%. In stark contrast, the bottom-left corner is anchored by arid northern counties like Mandera, Wajir, and Marsabit, where contraceptive use is practically non-existent (hovering near zero). Notably, there are a few outliers below the trendline—such as Turkana and Samburu—which have higher rates of “any method” use but noticeably lower rates of “modern method” use, suggesting these specific areas rely more heavily on traditional family planning practices than the rest of the country.
pca <- prcomp(mat, scale. = TRUE)
ve <- pca$sdev^2 / sum(pca$sdev^2)
scree <- tibble(pc = seq_along(ve), ve = ve) |>
ggplot(aes(pc, ve)) +
geom_col(fill = pal$navy) +
geom_line(aes(y = cumsum(ve)), colour = pal$orange) +
geom_point(aes(y = cumsum(ve)), colour = pal$orange) +
scale_y_continuous(labels = percent) +
labs(title = "Variance explained", subtitle = "Bars = per component, line = cumulative",
x = "Principal component", y = NULL)
load <- as_tibble(pca$rotation[, 1:2], rownames = "id") |>
mutate(label = str_wrap(mat_labels, 22))
sco <- as_tibble(pca$x[, 1:2], rownames = "region")
k_scale <- 0.8 * max(abs(sco$PC1), abs(sco$PC2))
biplot <- ggplot() +
geom_point(data = sco, aes(PC1, PC2), colour = pal$teal, size = 2.5, alpha = .8) +
ggrepel::geom_text_repel(data = sco, aes(PC1, PC2, label = region),
size = 2.8, max.overlaps = 12, colour = "grey30") +
geom_segment(data = load, aes(0, 0, xend = PC1 * k_scale, yend = PC2 * k_scale),
arrow = arrow(length = unit(.15, "cm")), colour = pal$red, alpha = .7) +
geom_text(data = load, aes(PC1 * k_scale * 1.08, PC2 * k_scale * 1.08, label = label),
size = 2.4, colour = pal$red) +
labs(title = "Regions in indicator space",
subtitle = sprintf("PC1 %s | PC2 %s", percent(ve[1], .1), percent(ve[2], .1)),
x = "PC1", y = "PC2")
scree + biplot + plot_layout(widths = c(1, 2)) + plot_annotation(caption = src_caption)This composite chart uses Principal Component Analysis (PCA) to reduce the complex, multi-dimensional health and development data into a visual map, revealing the underlying structure of regional inequality in Kenya. The scree plot on the left shows that the first two principal components capture nearly 70% of the total variance across all indicators (PC1 explains 50.3% and PC2 explains 17.9%), meaning this 2D map is a highly accurate representation of the data.
The biplot on the right makes the stark divide in Kenya unmistakable, primarily driven by PC1 (the horizontal axis). Regions on the far left—like Nairobi, Kiambu, Nyeri, and Central—are clustered tightly with vectors pointing toward them for “Women with secondary or higher education,” “Households with electricity,” and high contraceptive use. Conversely, moving far to the right along PC1, we see a cluster of arid and semi-arid northern regions like Mandera, Wajir, Marsabit, and Turkana. These regions are strongly associated with the red vectors pointing rightward, representing high “Total fertility rate,” “Children stunted,” and “Children underweight.” PC2 (the vertical axis) captures a distinct secondary dimension primarily related to child mortality; regions at the bottom, such as Migori and Homa Bay, struggle with high infant and under-five mortality, independent of their education or fertility levels. Overall, the map confirms that Kenya’s development landscape is not just unequal, but polarized into two drastically different realities: a high-education, low-fertility, modernized center, and a low-education, high-fertility, high-malnutrition periphery.
dm <- dist(scale(mat))
hc <- hclust(dm, "ward.D2")
k_max <- min(8, nrow(mat) - 1)
sil <- tibble(k = 2:k_max,
width = map_dbl(2:k_max, ~ mean(silhouette(cutree(hc, .x), dm)[, "sil_width"])))
ggplot(sil, aes(k, width)) +
geom_line(colour = pal$navy) +
geom_point(size = 3, colour = pal$navy) +
geom_point(data = filter(sil, k == k_clusters_value), size = 5, colour = pal$orange) +
scale_x_continuous(breaks = 2:k_max) +
labs(title = "Choosing the number of clusters",
subtitle = sprintf("Average silhouette width; orange = k = %d used in this report", k_clusters_value),
x = "Number of clusters (k)", y = "Silhouette width", caption = src_caption)This chart uses the silhouette method to determine the optimal number of regional clusters (k) for the dataset, plotting the average silhouette width (a measure of how well regions fit within their assigned cluster versus neighboring ones) against different values of k. The highest score occurs at k = 2 (roughly 0.40), which statistically suggests that Kenya’s regions are best divided into just two broad groups—likely a primary split between high-development and low-development areas, as seen in the previous PCA biplot. However, the chart shows that the silhouette score drops sharply at k = 3 and then plateaus at a lower level (around 0.20) for k = 4 through k = 8.
Despite the mathematical preference for two clusters, the authors have chosen k = 4 for this report, highlighted by the orange dot. This choice reflects a conscious trade-off between statistical purity and practical policy relevance. While two clusters tell a simple story of “advantaged” versus “disadvantaged” regions, splitting the data into four clusters provides a more nuanced typology of the country. This approach allows policymakers and researchers to identify distinct sub-groups—such as regions that share high fertility but differ in mortality, or those with rising education but lagging infrastructure—enabling more tailored, targeted interventions than a broad two-tier system would allow.
k <- k_clusters_value
cl <- tibble(region = rownames(mat), cluster = factor(cutree(hc, k)))
sco2 <- as_tibble(pca$x[, 1:2], rownames = "region") |> left_join(cl, by = "region")
ggplot(sco2, aes(PC1, PC2, colour = cluster, fill = cluster)) +
stat_ellipse(geom = "polygon", alpha = .1, level = .8, colour = NA,
type = "norm", na.rm = TRUE) +
geom_point(size = 3.5) +
ggrepel::geom_text_repel(aes(label = region), size = 3, max.overlaps = 20,
show.legend = FALSE) +
scale_colour_manual(values = unname(unlist(pal[c("navy", "teal", "orange", "red", "gold", "grey")]))) +
scale_fill_manual(values = unname(unlist(pal[c("navy", "teal", "orange", "red", "gold", "grey")]))) +
labs(title = "Region clusters", subtitle = "Ward hierarchical clustering on standardised indicators",
caption = src_caption)This chart visualizes the results of a Ward hierarchical clustering algorithm applied to the standardized health and development indicators, mapping the four distinct regional profiles identified in the report onto the PCA space. The primary division (along the horizontal PC1 axis) is starkly evident: Cluster 4 (Salmon/Red), occupying the far right, isolates the vast majority of Kenya’s arid and semi-arid lands (ASALs) like Mandera, Wajir, Marsabit, and Turkana. These regions are characterized by extreme fertility, high child malnutrition (stunting and underweight), and very low education and contraceptive use. Conversely, the regions on the left side of the chart represent the more developed, higher-resource areas of the country.
The left side is further subdivided into three distinct profiles. Cluster 3 (Orange), tightly clustered on the far left, represents the highly urbanized and prosperous central heartland (Nairobi, Kiambu, Nyeri, Kirinyaga) with high education and infrastructure but lower fertility. Cluster 2 (Teal), situated in the upper-middle, groups regions that share strong educational outcomes and moderate development but are geographically spread out (from Tharaka-Nithi to Kilifi). Finally, Cluster 1 (Navy Blue), located at the bottom center, forms a unique epidemiological block. While these regions (like Migori, Siaya, Homa Bay, and Kisumu) share decent education and fertility levels with Cluster 2, their defining characteristic is their extreme vertical position (PC2), which, as seen in the previous charts, correlates with disproportionately high infant and under-five mortality rates. This clustering map successfully translates complex data into a clear geographical typology, proving that Kenya’s development challenges are highly localized and require differentiated regional strategies.
prof <- as_tibble(scale(mat), rownames = "region") |>
left_join(cl, by = "region") |>
group_by(cluster) |>
summarise(across(all_of(colnames(mat)), mean), .groups = "drop") |>
pivot_longer(-cluster, names_to = "id", values_to = "z") |>
mutate(indicator = str_wrap(mat_labels[match(id, colnames(mat))], 26))
ggplot(prof, aes(indicator, cluster, fill = z)) +
geom_tile(colour = "white") +
geom_text(aes(label = sprintf("%.1f", z)), size = 3) +
scale_fill_gradient2(low = pal$orange, mid = "white", high = pal$navy, midpoint = 0, name = "Mean z") +
labs(title = "What defines each cluster?", x = NULL, y = "Cluster", caption = src_caption) +
theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 8))This heatmap profiles the four regional clusters identified in the previous chart by displaying their mean standardized scores (z-scores) across twelve key indicators. The colors immediately delineate the structural divide: dark blue indicates significantly above-average performance (or high values), while dark orange indicates below-average performance.
Cluster 4 (the arid/semi-arid northern regions) stands out for its intense orange profile on the left side, showing severe deficits in children’s nutrition (stunting and underweight) and alarmingly low contraceptive use. However, it registers a massive dark blue block for “Total fertility rate” and “Under-five mortality rate,” indicating these are the defining—and most troubling—characteristics of these regions. In stark contrast, Cluster 3 (the central/urban regions) is dominated by dark blue across the right side of the matrix, reflecting exceptionally high rates of electricity, education, and modern contraceptive use, paired with below-average fertility and mortality.
The chart also highlights the critical nuances between Clusters 1 and 2. Cluster 1 (the western/lake regions) is defined by a single, overwhelming dark blue square for “Under-five mortality rate,” confirming that high child mortality is its primary distinguishing burden, even though its other indicators are fairly average. Cluster 2 represents a more balanced “middle” group; it lacks extreme scores (no dark orange or dark blue blocks) but shows mild above-average performance in education and vaccination. By translating complex data into relative strengths and weaknesses, this heatmap provides a targeted diagnostic tool for policymakers, revealing that while Cluster 4 needs broad, systemic development interventions, Cluster 1 requires a highly focused child-survival strategy.
cl |> group_by(Cluster = cluster) |>
summarise(Regions = n(), Members = paste(sort(region), collapse = ", ")) |>
DT::datatable(rownames = FALSE, options = list(dom = "t", pageLength = 10))Cluster 4 represents the extreme periphery, comprising almost entirely the Arid and Semi-Arid Lands (ASALs) of northern and eastern Kenya (e.g., Mandera, Wajir, Turkana, Garissa). These regions face the most acute structural challenges, combining severe poverty, low education, high fertility, and significant food insecurity. Cluster 3 is the extreme opposite: the high-potential, highly urbanized, and prosperous central heartland, anchored by Nairobi, Kiambu, and Nyeri. Cluster 1 is the western/lake region (Nyanza and Western provinces), distinct not for poverty, but for its specific epidemiological crisis—extremely high child and infant mortality rates.
The most fascinating group is Cluster 2, the “middle” cluster, which contains 24 members. This group is massive and geographically diverse, blending historically wealthier Rift Valley counties (Nakuru, Uasin Gishu) with coastal areas (Mombasa, Kilifi) and eastern regions (Machakos, Meru). Its large size and “average” profile suggest that the majority of Kenya is transitioning; they are not facing the extreme crises of the North, nor do they enjoy the full advantages of the Central hub. This highlights a crucial policy insight: while Cluster 4 needs immediate humanitarian and infrastructural intervention, the vast Cluster 2 requires sustained investment to prevent them from slipping backward.
Replace this section with your own interpretation — it is what turns a technical report into a portfolio piece. Suggested prompts:
This subnational analysis of Kenya’s Demographic and Health Survey data tells a story that is, at once, encouraging and deeply troubling. Nationally, Kenya has made remarkable developmental progress over the past three decades: child vaccination rates have risen dramatically, electricity access has expanded, female education has become nearly universal in reaching secondary levels, and both fertility and child mortality have declined substantially. Yet, as the data has consistently shown across every analytical lens in this report—league tables, inequality ranges, change tests, correlations, PCA, and clustering—these national averages conceal a country increasingly fractured along geographic, historical, and structural lines.
The persistence of inequality is the defining finding. While virtually every headline indicator has improved on average, the gap between Kenya’s best- and worst-performing regions has, in many cases, widened rather than narrowed. Regional disparities in vaccination, electricity access, and women’s education grew substantially over the study period. This is not a story of uniform progress; it is a story of progress concentrating in already-advantaged areas, leaving the periphery further behind in relative—and sometimes absolute—terms. The “tyranny of the average” highlighted in the change analysis is the report’s central warning: national statistics that look reassuring may mask severe stagnation or regression at the subnational level, particularly for child health outcomes where several regions saw statistically significant deterioration.
Kenya is not one development narrative but several. The clustering analysis distilled the country into four distinct profiles, each demanding a different policy response:
Cluster 4 (the ASAL periphery) — comprising Mandera, Wajir, Turkana, Marsabit, Garissa, and neighboring arid counties — represents the most acute structural challenge. These regions combine the world’s highest fertility levels, severe child malnutrition, near-zero contraceptive use, and the lowest education rates. They require broad, systemic, and sustained humanitarian and infrastructural intervention, not incremental adjustment.
Cluster 1 (the lake/western region) — including Migori, Siaya, Homa Bay, and Kisumu — is not defined by poverty but by a specific epidemiological crisis: disproportionately high infant and under-five mortality. This region demands a focused child-survival strategy addressing localized healthcare access, disease burdens, and maternal health systems.
Cluster 3 (the central heartland) — Nairobi, Kiambu, Nyeri, and neighboring counties — already enjoys the full “urban advantage” of high education, infrastructure, and family planning. It is the engine of national progress and can be a source of policy learning and resource mobilization for the rest.
Cluster 2 (the vast middle) — 24 regions spanning the Rift Valley, Coast, and Eastern areas — is the country’s largest and most geographically diverse group. Its relative “averageness” should not be mistaken for stability; it represents a transition zone where continued investment determines whether regions consolidate gains or slide backward.
What moves together matters. The correlation analysis revealed that education, family planning, electricity, and child nutrition are tightly bound into a single developmental syndrome, while high fertility, stunting, and underweight children cluster together on the opposite pole. This confirms that development is holistic: piecemeal interventions in one domain will be constrained by deficits in others. Yet the data also revealed a crucial exception: child mortality is surprisingly weakly correlated with most socioeconomic indicators, suggesting that child survival depends on distinct, health-system-specific factors rather than general development alone. This nuance is essential—it means that even relatively prosperous regions cannot assume that rising education or electricity will automatically translate into fewer child deaths.
The path forward requires disaggregation. The DHS data tells us, with increasing precision, who is falling behind, where they are, and how much is at stake. The policy implication is unambiguous: Kenya’s next chapter of development cannot be written in national averages. It must be written region by region, guided by the recognition that the same intervention may be transformational in one cluster and irrelevant in another. The most urgent priorities are (1) child survival in the western/lake region, (2) foundational development—education, family planning, and nutrition—across the arid north, and (3) sustained investment in the vast middle to prevent regression and consolidate gains.
Kenya has demonstrated that national progress is possible. The challenge now is to ensure that progress is shared—and that no region is left on the wrong side of the gap that this report has measured so clearly.
dhs |>
select(Survey = survey_id, Year = year, Region = region, Indicator = indicator,
Value = value, `CI low` = ci_low, `CI high` = ci_high, `Denominator (w)` = denom_w) |>
DT::datatable(filter = "top", rownames = FALSE, extensions = "Buttons",
options = list(pageLength = 10, scrollX = TRUE, dom = "Bfrtip",
buttons = c("csv", "excel")))## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_Kenya.utf8 LC_CTYPE=English_Kenya.utf8
## [3] LC_MONETARY=English_Kenya.utf8 LC_NUMERIC=C
## [5] LC_TIME=English_Kenya.utf8
##
## time zone: Africa/Nairobi
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] cluster_2.1.8.2 ggrepel_0.9.8 patchwork_1.3.2 plotly_4.12.1
## [5] DT_0.34.0 scales_1.4.0 janitor_2.2.1 lubridate_1.9.5
## [9] forcats_1.0.1 stringr_1.6.0 dplyr_1.2.1 purrr_1.2.2
## [13] readr_2.2.0 tidyr_1.3.2 tibble_3.3.1 ggplot2_4.0.3
## [17] tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 xfun_0.60 bslib_0.12.0 htmlwidgets_1.6.4
## [5] lattice_0.22-9 tzdb_0.5.0 vctrs_0.7.3 tools_4.6.1
## [9] crosstalk_1.2.2 generics_0.1.4 parallel_4.6.1 pkgconfig_2.0.3
## [13] Matrix_1.7-5 data.table_1.18.4 RColorBrewer_1.1-3 S7_0.2.2
## [17] lifecycle_1.0.5 compiler_4.6.1 farver_2.1.2 snakecase_0.11.1
## [21] htmltools_0.5.9 sass_0.4.10 yaml_2.3.12 pillar_1.11.1
## [25] crayon_1.5.3 jquerylib_0.1.4 cachem_1.1.0 nlme_3.1-169
## [29] tidyselect_1.2.1 digest_0.6.39 stringi_1.8.7 labeling_0.4.3
## [33] splines_4.6.1 fastmap_1.2.0 grid_4.6.1 cli_3.6.6
## [37] magrittr_2.0.5 withr_3.0.3 bit64_4.8.2 timechange_0.4.0
## [41] rmarkdown_2.31 httr_1.4.8 bit_4.6.0 otel_0.2.0
## [45] hms_1.1.4 evaluate_1.0.5 knitr_1.51 viridisLite_0.4.3
## [49] mgcv_1.9-4 rlang_1.3.0 Rcpp_1.1.2 glue_1.8.1
## [53] rstudioapi_0.19.0 vroom_1.7.1 jsonlite_2.0.0 R6_2.6.1
## [57] fs_2.1.0