---
title: "What Kills Kenya — Mortality by Sex, Age, and County"
output:
flexdashboard::flex_dashboard:
orientation: rows
vertical_layout: fill
theme: flatly
source_code: embed
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(flexdashboard)
library(dplyr)
library(tidyr)
library(ggplot2)
library(plotly)
library(leaflet)
library(sf)
library(scales)
library(DT)
# raster/terra (pulled in by sf/leaflet) are known to silently mask
# common dplyr verbs -- pin these explicitly so the right version
# always wins, regardless of package load order.
filter <- dplyr::filter
select <- dplyr::select
recode <- dplyr::recode
rename <- dplyr::rename
source("data_prep.R") # loads and tidies all data frames used below
# House style for the static ggplot layer before it's handed to ggplotly()
theme_story <- function() {
theme_minimal(base_size = 12) +
theme(
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 13),
plot.subtitle = element_text(color = "grey40", size = 10)
)
}
pal_sex <- c("Male" = "#2b6cb0", "Female" = "#c05621")
```
Overview
=====================================
Row {data-height=120}
-------------------------------------
### Registered deaths, 2024
```{r}
valueBox(comma(kenya_national_completeness$reg_total),
caption = "Registered deaths (KNBS, 2024)",
icon = "fa-file-text", color = "#2b6cb0")
```
### Sex ratio at death
```{r}
sex_ratio_national <- round(sum(age_sex_deaths$male) / sum(age_sex_deaths$female) * 100)
valueBox(sex_ratio_national,
caption = "Male deaths per 100 female deaths",
icon = "fa-venus-mars", color = "#c05621")
```
### Registration completeness
```{r}
valueBox(paste0(kenya_national_completeness$comp_total, "%"),
caption = "Share of expected deaths actually registered",
icon = "fa-exclamation-triangle", color = "#e53e3e")
```
Row {data-height=440}
-------------------------------------
### The undercount, in one bar
```{r}
gap_df <- kenya_national_completeness |>
transmute(
Registered = reg_total,
Unregistered = exp_total - reg_total
) |>
pivot_longer(everything(), names_to = "status", values_to = "n") |>
mutate(status = factor(status, levels = c("Registered", "Unregistered")))
p <- ggplot(gap_df, aes(x = "2024", y = n, fill = status,
text = paste0(status, ": ", comma(n)))) +
geom_col(width = 0.5) +
scale_fill_manual(values = c("Registered" = "#2b6cb0", "Unregistered" = "#cbd5e0")) +
scale_y_continuous(labels = comma) +
coord_flip() +
labs(title = "Fewer than half of Kenya's expected deaths get registered",
subtitle = "460,585 deaths were expected in 2024; 206,417 were registered",
x = NULL, y = "Deaths") +
theme_story() +
theme(legend.title = element_blank())
ggplotly(p, tooltip = "text") |> layout(hovermode = "closest")
```
### National completeness trend, 2020-2024
```{r}
trend_national <- county_completeness_trend |> filter(county == "Kenya")
p <- ggplot(trend_national, aes(x = year, y = completeness_pct,
text = paste0(year, ": ", completeness_pct, "%"))) +
geom_line(color = "#2b6cb0", linewidth = 1) +
geom_point(color = "#2b6cb0", size = 2) +
scale_y_continuous(limits = c(0, 60), labels = function(x) paste0(x, "%")) +
labs(title = "Completeness is improving, unevenly",
subtitle = "Death registration completeness, Kenya, 2020-2024",
x = NULL, y = "Completeness (%)") +
theme_story()
ggplotly(p, tooltip = "text")
```
Age & Sex
=====================================
Row {data-height=500}
-------------------------------------
### Where the gap between men and women widens: sex ratio at death by age
```{r}
p <- ggplot(age_sex_deaths, aes(x = age_group, y = sex_ratio, group = 1,
text = paste0(age_group, ": ", sex_ratio,
" male deaths per 100 female deaths"))) +
geom_hline(yintercept = 100, linetype = "dashed", color = "grey60") +
geom_line(color = "#c05621", linewidth = 1) +
geom_point(color = "#c05621", size = 2) +
labs(title = "More men die at almost every age",
subtitle = "Sex ratio at death by age group, Kenya, 2024 (100 = parity)",
x = "Age group", y = "Male deaths per 100 female deaths") +
theme_story() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
ggplotly(p, tooltip = "text")
```
Row {data-height=500}
-------------------------------------
### Same causes, different weight: leading causes of death by sex, 2024
```{r fig.height=6}
dumbbell_df <- causes_dumbbell |>
filter(!is.na(Male) | !is.na(Female)) |>
mutate(cause = reorder(cause, gap))
p <- ggplot(dumbbell_df) +
geom_segment(aes(x = cause, xend = cause, y = Female, yend = Male),
color = "grey70", linewidth = 1) +
geom_point(aes(x = cause, y = Male, text = paste0(cause, " (male): ", comma(Male))),
color = pal_sex["Male"], size = 3) +
geom_point(aes(x = cause, y = Female, text = paste0(cause, " (female): ", comma(Female))),
color = pal_sex["Female"], size = 3) +
coord_flip() +
scale_y_continuous(labels = comma) +
labs(title = "Leading causes of registered health-facility deaths, by sex",
subtitle = "Blue = male deaths, orange = female deaths. Sorted by size of the gap.",
x = NULL, y = "Registered deaths, 2024") +
theme_story()
ggplotly(p, tooltip = "text")
```
Causes by Age
=====================================
Row {data-height=650}
-------------------------------------
### What kills you depends on how old you are: leading causes by age band, 2024
```{r fig.height=8}
top5_by_age <- causes_by_age_band |>
group_by(age_band) |>
slice_min(rank, n = 5) |>
ungroup() |>
mutate(cause = reorder(cause, deaths))
p <- ggplot(top5_by_age, aes(x = cause, y = deaths, fill = age_band,
text = paste0(cause, ": ", comma(deaths)))) +
geom_col(show.legend = FALSE) +
coord_flip() +
facet_wrap(~ age_band, scales = "free", ncol = 4) +
scale_y_continuous(labels = comma) +
labs(title = NULL, x = NULL, y = "Registered deaths, 2024") +
theme_story() +
theme(strip.text = element_text(face = "bold", size = 9),
axis.text.y = element_text(size = 8))
ggplotly(p, tooltip = "text", height = 750) |>
layout(margin = list(l = 40))
```
Geography
=====================================
Row {data-height=600}
-------------------------------------
### Where registration is most and least complete
```{r}
# --------------------------------------------------------------
# This map needs a Kenya county boundary file, which is NOT part
# of the KNBS release and must be fetched separately (one-time).
# Two easy options:
#
# 1) geoBoundaries (simplest, no auth):
# download.file(
# "https://www.geoboundaries.org/api/current/gbOpen/KEN/ADM1/",
# destfile = "data/ken_adm1.json"
# )
# # then follow the returned "simplifiedGeometryGeoJSON" URL to
# # get the actual GeoJSON, and save it as data/kenya_counties.geojson
#
# 2) rKenyaCensus package (bundles county shapefiles directly):
# install.packages("rKenyaCensus")
# counties_sf <- rKenyaCensus::KenyaCounties_SHP |> sf::st_as_sf()
#
# Once you have counties_sf with a county-name column, run the
# join below. County-name spelling will very likely need manual
# reconciliation -- see the notes at the bottom of data_prep.R.
# --------------------------------------------------------------
geojson_path <- "data/kenya_counties.geojson"
if (file.exists(geojson_path)) {
counties_sf <- st_read(geojson_path, quiet = TRUE)
# Adjust "shapeName" to whatever the name column is actually called
# in your shapefile (inspect with names(counties_sf)).
counties_sf <- counties_sf |>
mutate(county_join = shapeName |>
stringr::str_replace("Murang'a", "Muranga") |>
stringr::str_replace("Nairobi City", "Nairobi") |>
stringr::str_replace("Taita/Taveta", "Taita Taveta") |>
stringr::str_replace("Elgeyo/Marakwet", "Elgeyo Marakwet"))
map_data <- county_completeness |>
mutate(county_join = county_clean |>
stringr::str_replace("Murang'a", "Muranga") |>
stringr::str_replace("Nairobi City", "Nairobi") |>
stringr::str_replace("Taita/Taveta", "Taita Taveta") |>
stringr::str_replace("Elgeyo/Marakwet", "Elgeyo Marakwet"))
counties_sf <- counties_sf |> left_join(map_data, by = "county_join")
pal <- colorNumeric("YlOrRd", domain = counties_sf$comp_total, reverse = TRUE)
leaflet(counties_sf) |>
addProviderTiles(providers$CartoDB.Positron) |>
addPolygons(
fillColor = ~pal(comp_total),
fillOpacity = 0.8,
color = "white",
weight = 1,
label = ~paste0(county_clean, ": ", comp_total, "% completeness"),
highlightOptions = highlightOptions(weight = 2, color = "#333", bringToFront = TRUE)
) |>
addLegend(pal = pal, values = ~comp_total, title = "Completeness (%)",
position = "bottomright")
} else {
# Fallback so the dashboard still renders before you've added the
# shapefile: a sorted bar chart of the same data.
bar_df <- county_completeness |>
arrange(comp_total) |>
mutate(county_clean = factor(county_clean, levels = county_clean))
p <- ggplot(bar_df, aes(x = county_clean, y = comp_total,
text = paste0(county_clean, ": ", comp_total, "%"))) +
geom_col(fill = "#c05621") +
coord_flip() +
labs(title = "Death registration completeness by county, 2024",
subtitle = "Map view needs data/kenya_counties.geojson -- see code comments",
x = NULL, y = "Completeness (%)") +
theme_story() +
theme(axis.text.y = element_text(size = 7))
ggplotly(p, tooltip = "text", height = 900)
}
```
Row {data-height=400}
-------------------------------------
### Full county table
```{r}
county_completeness |>
select(County = county_clean,
`Registered` = reg_total,
`Expected` = exp_total,
`Completeness (%)` = comp_total) |>
arrange(`Completeness (%)`) |>
datatable(options = list(pageLength = 10, order = list(list(3, "asc"))),
rownames = FALSE)
```