How to use this report
This document unifies the thirty instructional code modules into one
executable R Markdown workflow. Edit the kr_file parameter
in the YAML header so it points to the Ethiopia DHS 2024-25 Kid Recode
(.dta) file, then select Knit > Knit to
PDF in RStudio. The report installs missing R packages,
reconstructs and validates the exclusive breastfeeding indicator,
performs the survey-weighted analyses, prints all major tables, creates
all analytical figures, generates the epidemiologic DAG and programmatic
conceptual framework, and exports reusable CSV and PNG files.
The DHS data file is not distributed with this report. Access must
remain governed by the DHS Program data-use agreement.
Module 1: Package management and reproducibility
required_packages <- c(
"haven", "dplyr", "tidyr", "purrr", "stringr", "forcats",
"survey", "broom", "emmeans", "ggplot2", "knitr", "scales"
)
missing_packages <- required_packages[
!required_packages %in% rownames(installed.packages())
]
if (length(missing_packages) > 0L) {
install.packages(missing_packages, dependencies = TRUE)
}
invisible(lapply(required_packages, library, character.only = TRUE))
set.seed(2026)
output_dir <- normalizePath(
params$output_dir,
winslash = "/",
mustWork = FALSE
)
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
dir.create(file.path(output_dir, "tables"), recursive = TRUE, showWarnings = FALSE)
dir.create(file.path(output_dir, "figures"), recursive = TRUE, showWarnings = FALSE)
session_start <- Sys.time()
Module 2: Read the DHS Kid Recode file
kr_file <- params$kr_file
if (!file.exists(kr_file)) {
stop(
"The KR data file was not found. Edit params$kr_file in the YAML header.\nCurrent path: ",
kr_file
)
}
kr2024 <- haven::read_dta(kr_file)
file_summary <- data.frame(
Item = c("Data file", "Records", "Variables"),
Value = c(kr_file, format(nrow(kr2024), big.mark = ","), ncol(kr2024))
)
knitr::kable(file_summary, caption = "Input-data summary", booktabs = TRUE)
Input-data summary
| Data file |
C:/Users/aynal/OneDrive/Documents/ETH-DHS-2024-25/2024DATA/ETKR8ADT/ETKR8AFL.dta |
| Records |
13,565 |
| Variables |
1301 |
Module 3: Confirm required variables
essential_variables <- c(
"caseid", "v005", "v021", "v022", "v024", "v025", "v106", "v190",
"v012", "v201", "b5", "b9", "b19", "bidx", "b4", "m4", "v409"
)
missing_essential <- setdiff(essential_variables, names(kr2024))
if (length(missing_essential) > 0L) {
stop(
"Required variables are missing from the KR file: ",
paste(missing_essential, collapse = ", ")
)
}
variable_audit <- data.frame(
Variable = essential_variables,
Present = essential_variables %in% names(kr2024)
)
knitr::kable(variable_audit, caption = "Required-variable audit", booktabs = TRUE)
Required-variable audit
| caseid |
TRUE |
| v005 |
TRUE |
| v021 |
TRUE |
| v022 |
TRUE |
| v024 |
TRUE |
| v025 |
TRUE |
| v106 |
TRUE |
| v190 |
TRUE |
| v012 |
TRUE |
| v201 |
TRUE |
| b5 |
TRUE |
| b9 |
TRUE |
| b19 |
TRUE |
| bidx |
TRUE |
| b4 |
TRUE |
| m4 |
TRUE |
| v409 |
TRUE |
Module 4: Identify DHS-8 liquid and food variables
v413_variables <- grep("^v413", names(kr2024), value = TRUE)
v414_variables <- grep("^v414", names(kr2024), value = TRUE)
feeding_variable_inventory <- data.frame(
Series = c(rep("v413 liquid series", length(v413_variables)),
rep("v414 food series", length(v414_variables))),
Variable = c(v413_variables, v414_variables)
)
knitr::kable(
feeding_variable_inventory,
caption = "DHS-8 liquid and food variables detected in the KR file",
booktabs = TRUE,
longtable = TRUE
)
DHS-8 liquid and food variables detected in the KR
file
| v413 liquid series |
v413 |
| v413 liquid series |
v413s |
| v413 liquid series |
v413a |
| v413 liquid series |
v413as |
| v413 liquid series |
v413b |
| v413 liquid series |
v413bs |
| v413 liquid series |
v413c |
| v413 liquid series |
v413d |
| v413 liquid series |
v413e |
| v413 liquid series |
v413f |
| v413 liquid series |
v413g |
| v413 liquid series |
v413h |
| v413 liquid series |
v413i |
| v414 food series |
v414a |
| v414 food series |
v414b |
| v414 food series |
v414c |
| v414 food series |
v414d |
| v414 food series |
v414e |
| v414 food series |
v414f |
| v414 food series |
v414g |
| v414 food series |
v414h |
| v414 food series |
v414i |
| v414 food series |
v414j |
| v414 food series |
v414k |
| v414 food series |
v414l |
| v414 food series |
v414m |
| v414 food series |
v414n |
| v414 food series |
v414o |
| v414 food series |
v414p |
| v414 food series |
v414q |
| v414 food series |
v414r |
| v414 food series |
v414s |
| v414 food series |
v414t |
| v414 food series |
v414u |
| v414 food series |
v414v |
| v414 food series |
v414w |
| v414 food series |
v414wa |
| v414 food series |
v414wb |
| v414 food series |
v414wc |
| v414 food series |
v414wd |
| v414 food series |
v414we |
Module 5: Define helper functions
consumed_1_to_7 <- function(x) {
as.integer(!is.na(x) & x >= 1 & x <= 7)
}
any_consumed <- function(data, variables) {
variables <- intersect(variables, names(data))
if (length(variables) == 0L) return(rep(0L, nrow(data)))
consumption_matrix <- vapply(
data[variables], consumed_1_to_7, integer(nrow(data))
)
if (is.null(dim(consumption_matrix))) {
consumption_matrix <- matrix(consumption_matrix, ncol = 1L)
}
as.integer(rowSums(consumption_matrix, na.rm = TRUE) > 0)
}
format_p <- function(p) {
ifelse(is.na(p), NA_character_, ifelse(p < 0.001, "<0.001", sprintf("%.3f", p)))
}
report_table <- function(x, caption, digits = 2) {
numeric_columns <- vapply(x, is.numeric, logical(1))
x[numeric_columns] <- lapply(x[numeric_columns], round, digits = digits)
knitr::kable(x, caption = caption, booktabs = TRUE, longtable = TRUE, row.names = FALSE)
}
Module 6: Define and document the analytical sample
sample_flow <- data.frame(
Step = c(
"All KR records",
"Living children",
"Living infants aged 0-5 completed months",
"Most recent birth",
"Infant resides with interviewed mother"
),
Unweighted_N = c(
nrow(kr2024),
sum(kr2024$b5 == 1, na.rm = TRUE),
sum(kr2024$b5 == 1 & kr2024$b19 >= 0 & kr2024$b19 < 6, na.rm = TRUE),
sum(kr2024$b5 == 1 & kr2024$b19 >= 0 & kr2024$b19 < 6 & kr2024$bidx == 1, na.rm = TRUE),
sum(kr2024$b5 == 1 & kr2024$b19 >= 0 & kr2024$b19 < 6 & kr2024$bidx == 1 & kr2024$b9 == 0, na.rm = TRUE)
)
)
report_table(sample_flow, "Analytical sample flow")
Analytical sample flow
| All KR records |
13565 |
| Living children |
12921 |
| Living infants aged 0-5 completed months |
1364 |
| Most recent birth |
1342 |
| Infant resides with interviewed mother |
1325 |
write.csv(sample_flow, file.path(output_dir, "tables", "Table_Sample_Flow.csv"), row.names = FALSE)
ebf_data <- kr2024 %>%
filter(
b5 == 1,
b19 >= 0,
b19 < 6,
bidx == 1,
b9 == 0
) %>%
mutate(weight = v005 / 1000000)
age_sample_distribution <- ebf_data %>%
count(b19, name = "Unweighted_N") %>%
rename(Infant_Age_Completed_Months = b19)
report_table(age_sample_distribution, "Eligible sample by completed month of infant age")
Eligible sample by completed month of infant age
| 0 |
207 |
| 1 |
233 |
| 2 |
237 |
| 3 |
213 |
| 4 |
224 |
| 5 |
211 |
Module 7: Reconstruct current breastfeeding
ebf_data <- ebf_data %>%
mutate(currently_breastfeeding = as.integer(m4 == 95))
current_bf_check <- ebf_data %>%
count(currently_breastfeeding, name = "Unweighted_N") %>%
mutate(Status = recode(as.character(currently_breastfeeding),
`0` = "Not currently breastfeeding",
`1` = "Currently breastfeeding")) %>%
select(Status, Unweighted_N)
report_table(current_bf_check, "Current breastfeeding reconstructed from m4 = 95")
Current breastfeeding reconstructed from m4 = 95
| Not currently breastfeeding |
119 |
| Currently breastfeeding |
1206 |
Module 8: Reconstruct water, liquids, milk, and solid-food
indicators
base_liquid_variables <- c("v409a", "v410", "v410a", "v412c")
all_liquid_variables <- unique(c(base_liquid_variables, v413_variables))
milk_variables <- c("v411", "v411a")
solid_variables <- unique(c(v414_variables, "v412a", "v412b", "m39a"))
ebf_data <- ebf_data %>%
mutate(
water = consumed_1_to_7(v409),
liquids = any_consumed(cur_data(), all_liquid_variables),
milk = any_consumed(cur_data(), milk_variables),
solids = any_consumed(cur_data(), solid_variables)
)
feeding_component_check <- bind_rows(
ebf_data %>% count(water) %>% mutate(Component = "Plain water", Response = water),
ebf_data %>% count(liquids) %>% mutate(Component = "Other nonmilk liquids", Response = liquids),
ebf_data %>% count(milk) %>% mutate(Component = "Other milk or formula", Response = milk),
ebf_data %>% count(solids) %>% mutate(Component = "Solid, semisolid, or soft food", Response = solids)
) %>%
transmute(Component, Response = ifelse(Response == 1, "Yes", "No"), Unweighted_N = n)
report_table(feeding_component_check, "Feeding-component diagnostics")
Feeding-component diagnostics
| Plain water |
No |
973 |
| Plain water |
Yes |
352 |
| Other nonmilk liquids |
No |
1230 |
| Other nonmilk liquids |
Yes |
95 |
| Other milk or formula |
No |
1138 |
| Other milk or formula |
Yes |
187 |
| Solid, semisolid, or soft food |
No |
1143 |
| Solid, semisolid, or soft food |
Yes |
182 |
Module 9: Construct the DHS breastfeeding-status hierarchy and EBF
outcome
ebf_data <- ebf_data %>%
mutate(
breastfeeding_status = 1L,
breastfeeding_status = ifelse(water == 1, 2L, breastfeeding_status),
breastfeeding_status = ifelse(liquids == 1, 3L, breastfeeding_status),
breastfeeding_status = ifelse(milk == 1, 4L, breastfeeding_status),
breastfeeding_status = ifelse(solids == 1, 5L, breastfeeding_status),
breastfeeding_status = ifelse(currently_breastfeeding == 0, 0L, breastfeeding_status),
ebf = as.integer(breastfeeding_status == 1L)
)
status_labels <- c(
`0` = "Not breastfeeding",
`1` = "Exclusively breastfeeding",
`2` = "Breastfeeding plus plain water",
`3` = "Breastfeeding plus nonmilk liquids",
`4` = "Breastfeeding plus other milk/formula",
`5` = "Breastfeeding plus complementary foods"
)
feeding_status_table <- ebf_data %>%
count(breastfeeding_status, name = "Unweighted_N") %>%
mutate(Status = unname(status_labels[as.character(breastfeeding_status)])) %>%
select(Status, Unweighted_N)
report_table(feeding_status_table, "DHS breastfeeding-status hierarchy")
DHS breastfeeding-status hierarchy
| Not breastfeeding |
119 |
| Exclusively breastfeeding |
733 |
| Breastfeeding plus plain water |
161 |
| Breastfeeding plus nonmilk liquids |
30 |
| Breastfeeding plus other milk/formula |
125 |
| Breastfeeding plus complementary foods |
157 |
Module 10: Validate the reconstructed EBF indicator
unweighted_ebf <- ebf_data %>%
summarise(
Unweighted_N = n(),
EBF_N = sum(ebf == 1, na.rm = TRUE),
Unweighted_EBF_Percent = 100 * mean(ebf, na.rm = TRUE)
)
report_table(unweighted_ebf, "Unweighted EBF diagnostic")
Unweighted EBF diagnostic
| 1325 |
733 |
55.32 |
Module 11: Create the final analysis variables
analysis_data <- ebf_data %>%
mutate(
ebf_factor = factor(ebf, levels = c(0, 1),
labels = c("Not exclusively breastfed", "Exclusively breastfed")),
infant_age_month = factor(b19, levels = 0:5, labels = paste0(0:5, " months")),
infant_sex = factor(b4, levels = c(1, 2), labels = c("Male", "Female")),
residence = factor(v025, levels = c(2, 1), labels = c("Rural", "Urban")),
maternal_education = factor(v106, levels = c(0, 1, 2, 3),
labels = c("No education", "Primary", "Secondary", "Higher")),
wealth_quintile = factor(v190, levels = 1:5,
labels = c("Poorest", "Poorer", "Middle", "Richer", "Richest")),
wealth_score = ifelse(v190 %in% 1:5, as.numeric(v190), NA_real_),
maternal_age_group = cut(v012, breaks = c(14, 24, 34, 49),
labels = c("15-24", "25-34", "35-49"), right = TRUE),
maternal_age_years = ifelse(v012 >= 15 & v012 <= 49, as.numeric(v012), NA_real_),
parity_group = case_when(
v201 == 1 ~ "1",
v201 %in% 2:3 ~ "2-3",
v201 %in% 4:5 ~ "4-5",
v201 >= 6 & v201 < 90 ~ "6+",
TRUE ~ NA_character_
),
parity_group = factor(parity_group, levels = c("1", "2-3", "4-5", "6+")),
region = case_when(
as.numeric(v024) == 1 ~ "Tigray",
as.numeric(v024) == 2 ~ "Afar",
as.numeric(v024) == 3 ~ "Amhara",
as.numeric(v024) == 4 ~ "Oromia",
as.numeric(v024) == 5 ~ "Somali",
as.numeric(v024) == 6 ~ "Benishangul-Gumuz",
as.numeric(v024) == 7 ~ "Central Ethiopia",
as.numeric(v024) == 8 ~ "Sidama",
as.numeric(v024) == 9 ~ "Southwest Ethiopia",
as.numeric(v024) == 10 ~ "South Ethiopia",
as.numeric(v024) == 12 ~ "Gambella",
as.numeric(v024) == 13 ~ "Harari",
as.numeric(v024) == 14 ~ "Addis Ababa",
as.numeric(v024) == 15 ~ "Dire Dawa",
TRUE ~ NA_character_
),
region = factor(region, levels = c(
"Oromia", "Tigray", "Afar", "Amhara", "Somali", "Benishangul-Gumuz",
"Central Ethiopia", "Sidama", "Southwest Ethiopia", "South Ethiopia",
"Gambella", "Harari", "Addis Ababa", "Dire Dawa"
)),
harmonized_region = case_when(
as.numeric(v024) == 1 ~ "Tigray",
as.numeric(v024) == 2 ~ "Afar",
as.numeric(v024) == 3 ~ "Amhara",
as.numeric(v024) == 4 ~ "Oromia",
as.numeric(v024) == 5 ~ "Somali",
as.numeric(v024) == 6 ~ "Benishangul-Gumuz",
as.numeric(v024) %in% c(7, 8, 9, 10) ~ "Former SNNPR",
as.numeric(v024) == 12 ~ "Gambella",
as.numeric(v024) == 13 ~ "Harari",
as.numeric(v024) == 14 ~ "Addis Ababa",
as.numeric(v024) == 15 ~ "Dire Dawa",
TRUE ~ NA_character_
),
harmonized_region = factor(harmonized_region, levels = c(
"Oromia", "Tigray", "Afar", "Amhara", "Somali", "Benishangul-Gumuz",
"Former SNNPR", "Gambella", "Harari", "Addis Ababa", "Dire Dawa"
))
)
Module 12: Audit constructed variables
variables_to_check <- c(
"ebf_factor", "infant_age_month", "infant_sex", "residence",
"maternal_education", "wealth_quintile", "maternal_age_group",
"parity_group", "region", "harmonized_region"
)
constructed_variable_audit <- map_dfr(variables_to_check, function(variable_name) {
analysis_data %>%
count(.data[[variable_name]], name = "Unweighted_N", .drop = FALSE) %>%
transmute(
Variable = variable_name,
Category = as.character(.data[[variable_name]]),
Unweighted_N
)
})
report_table(constructed_variable_audit, "Constructed-variable audit")
Constructed-variable audit
| ebf_factor |
Not exclusively breastfed |
592 |
| ebf_factor |
Exclusively breastfed |
733 |
| infant_age_month |
0 months |
207 |
| infant_age_month |
1 months |
233 |
| infant_age_month |
2 months |
237 |
| infant_age_month |
3 months |
213 |
| infant_age_month |
4 months |
224 |
| infant_age_month |
5 months |
211 |
| infant_sex |
Male |
659 |
| infant_sex |
Female |
666 |
| residence |
Rural |
933 |
| residence |
Urban |
392 |
| maternal_education |
No education |
496 |
| maternal_education |
Primary |
496 |
| maternal_education |
Secondary |
201 |
| maternal_education |
Higher |
132 |
| wealth_quintile |
Poorest |
467 |
| wealth_quintile |
Poorer |
228 |
| wealth_quintile |
Middle |
196 |
| wealth_quintile |
Richer |
177 |
| wealth_quintile |
Richest |
257 |
| maternal_age_group |
15-24 |
410 |
| maternal_age_group |
25-34 |
672 |
| maternal_age_group |
35-49 |
243 |
| parity_group |
1 |
281 |
| parity_group |
2-3 |
516 |
| parity_group |
4-5 |
253 |
| parity_group |
6+ |
275 |
| region |
Oromia |
131 |
| region |
Tigray |
123 |
| region |
Afar |
115 |
| region |
Amhara |
106 |
| region |
Somali |
123 |
| region |
Benishangul-Gumuz |
82 |
| region |
Central Ethiopia |
86 |
| region |
Sidama |
76 |
| region |
Southwest Ethiopia |
76 |
| region |
South Ethiopia |
147 |
| region |
Gambella |
69 |
| region |
Harari |
72 |
| region |
Addis Ababa |
48 |
| region |
Dire Dawa |
71 |
| harmonized_region |
Oromia |
131 |
| harmonized_region |
Tigray |
123 |
| harmonized_region |
Afar |
115 |
| harmonized_region |
Amhara |
106 |
| harmonized_region |
Somali |
123 |
| harmonized_region |
Benishangul-Gumuz |
82 |
| harmonized_region |
Former SNNPR |
385 |
| harmonized_region |
Gambella |
69 |
| harmonized_region |
Harari |
72 |
| harmonized_region |
Addis Ababa |
48 |
| harmonized_region |
Dire Dawa |
71 |
write.csv(constructed_variable_audit,
file.path(output_dir, "tables", "Table_Constructed_Variable_Audit.csv"),
row.names = FALSE)
Module 13: DAG-guided covariate specification
The epidemiologic model treats infant age and region as primary
explanatory dimensions and retains infant sex, residence, maternal
education, household wealth, maternal age, and parity as prespecified
covariates. Selection is based on causal and substantive reasoning
rather than automated significance screening.
Module 14: Build the complex survey design
design <- svydesign(
ids = ~v021,
strata = ~v022,
weights = ~weight,
data = analysis_data,
nest = TRUE
)
survey_design_summary <- data.frame(
Measure = c("Eligible infants", "Sampled PSUs", "Sampling strata", "Sum of normalized weights"),
Value = c(
nrow(analysis_data),
dplyr::n_distinct(analysis_data$v021),
dplyr::n_distinct(analysis_data$v022),
round(sum(analysis_data$weight, na.rm = TRUE), 2)
)
)
report_table(survey_design_summary, "Complex survey-design summary")
Complex survey-design summary
| Eligible infants |
1325.00 |
| Sampled PSUs |
619.00 |
| Sampling strata |
27.00 |
| Sum of normalized weights |
1318.45 |
Module 15: Define the reusable weighted-prevalence function
weighted_ebf_table <- function(design_object, data_object, grouping_variable, grouping_label) {
grouping_formula <- as.formula(paste0("~", grouping_variable))
estimate <- svyby(
formula = ~ebf,
by = grouping_formula,
design = design_object,
FUN = svymean,
na.rm = TRUE,
vartype = c("se", "ci"),
level = 0.95,
keep.names = FALSE,
drop.empty.groups = FALSE
) %>% as.data.frame()
names(estimate)[1] <- grouping_variable
unweighted <- data_object %>%
filter(!is.na(.data[[grouping_variable]]), !is.na(ebf)) %>%
count(.data[[grouping_variable]], name = "Unweighted_N", .drop = FALSE)
names(unweighted)[1] <- grouping_variable
estimate %>%
left_join(unweighted, by = grouping_variable) %>%
transmute(
Characteristic = grouping_label,
Category = as.character(.data[[grouping_variable]]),
Unweighted_N,
EBF_Percent = 100 * ebf,
Standard_Error_Percentage_Points = 100 * se,
Lower_95_CI = 100 * ci_l,
Upper_95_CI = 100 * ci_u
)
}
Module 16: Estimate and validate national EBF prevalence
national_object <- svymean(~ebf, design, na.rm = TRUE)
national_ci <- confint(national_object, level = 0.95)
national_results <- data.frame(
Characteristic = "National",
Category = "Ethiopia",
Unweighted_N = nrow(analysis_data),
EBF_Percent = 100 * coef(national_object)[1],
Standard_Error_Percentage_Points = 100 * SE(national_object)[1],
Lower_95_CI = 100 * national_ci[1, 1],
Upper_95_CI = 100 * national_ci[1, 2]
)
published_comparison <- national_results %>%
transmute(
Published_Percent = params$published_ebf_percent,
Reconstructed_Percent = EBF_Percent,
Absolute_Difference_Percentage_Points = abs(EBF_Percent - params$published_ebf_percent),
Tolerance_Percentage_Points = params$validation_tolerance_points,
Validation = ifelse(
Absolute_Difference_Percentage_Points <= params$validation_tolerance_points,
"Pass: close agreement",
"Review required"
)
)
report_table(national_results, "National survey-weighted exclusive breastfeeding prevalence")
National survey-weighted exclusive breastfeeding
prevalence
| National |
Ethiopia |
1325 |
58.07 |
2.51 |
53.15 |
63 |
report_table(published_comparison, "Validation against the published EDHS estimate")
Validation against the published EDHS estimate
| 57.3 |
58.07 |
0.77 |
1 |
Pass: close agreement |
Module 17: Estimate the age-specific EBF pattern
age_results <- weighted_ebf_table(
design, analysis_data, "infant_age_month", "Infant age"
) %>%
mutate(Infant_Age_Months = as.integer(stringr::str_extract(Category, "^[0-5]")))
report_table(age_results, "Exclusive breastfeeding by completed month of infant age")
Exclusive breastfeeding by completed month of infant
age
| Infant age |
0 months |
207 |
85.22 |
4.75 |
75.90 |
94.53 |
0 |
| Infant age |
1 months |
233 |
69.46 |
5.19 |
59.28 |
79.64 |
1 |
| Infant age |
2 months |
237 |
63.40 |
5.58 |
52.46 |
74.34 |
2 |
| Infant age |
3 months |
213 |
71.26 |
4.95 |
61.56 |
80.97 |
3 |
| Infant age |
4 months |
224 |
42.08 |
5.21 |
31.88 |
52.29 |
4 |
| Infant age |
5 months |
211 |
26.19 |
5.84 |
14.74 |
37.64 |
5 |
Module 18: Estimate EBF by infant sex
sex_results <- weighted_ebf_table(
design, analysis_data, "infant_sex", "Infant sex"
)
report_table(sex_results, "Exclusive breastfeeding by infant sex")
Exclusive breastfeeding by infant sex
| Infant sex |
Male |
659 |
60.15 |
3.35 |
53.58 |
66.71 |
| Infant sex |
Female |
666 |
56.09 |
3.36 |
49.50 |
62.69 |
Module 19: Estimate EBF by residence
residence_results <- weighted_ebf_table(
design, analysis_data, "residence", "Residence"
)
report_table(residence_results, "Exclusive breastfeeding by urban-rural residence")
Exclusive breastfeeding by urban-rural residence
| Residence |
Rural |
933 |
59.79 |
3.23 |
53.45 |
66.12 |
| Residence |
Urban |
392 |
54.43 |
3.75 |
47.07 |
61.78 |
Module 20: Estimate EBF by maternal education
education_results <- weighted_ebf_table(
design, analysis_data, "maternal_education", "Maternal education"
)
report_table(education_results, "Exclusive breastfeeding by maternal education")
Exclusive breastfeeding by maternal education
| Maternal education |
No education |
496 |
55.76 |
3.91 |
48.09 |
63.44 |
| Maternal education |
Primary |
496 |
58.99 |
3.49 |
52.15 |
65.83 |
| Maternal education |
Secondary |
201 |
58.51 |
6.03 |
46.70 |
70.33 |
| Maternal education |
Higher |
132 |
61.42 |
7.11 |
47.48 |
75.36 |
Module 21: Estimate EBF by household wealth
wealth_results <- weighted_ebf_table(
design, analysis_data, "wealth_quintile", "Household wealth"
)
report_table(wealth_results, "Exclusive breastfeeding by household wealth quintile")
Exclusive breastfeeding by household wealth quintile
| Household wealth |
Poorest |
467 |
55.28 |
4.01 |
47.42 |
63.14 |
| Household wealth |
Poorer |
228 |
54.72 |
5.03 |
44.86 |
64.59 |
| Household wealth |
Middle |
196 |
57.16 |
5.63 |
46.13 |
68.19 |
| Household wealth |
Richer |
177 |
64.96 |
6.14 |
52.93 |
77.00 |
| Household wealth |
Richest |
257 |
57.46 |
4.79 |
48.08 |
66.84 |
Module 22: Estimate EBF by maternal age
maternal_age_results <- weighted_ebf_table(
design, analysis_data, "maternal_age_group", "Maternal age"
)
report_table(maternal_age_results, "Exclusive breastfeeding by maternal age")
Exclusive breastfeeding by maternal age
| Maternal age |
15-24 |
410 |
55.97 |
4.20 |
47.75 |
64.20 |
| Maternal age |
25-34 |
672 |
57.63 |
3.48 |
50.82 |
64.44 |
| Maternal age |
35-49 |
243 |
61.95 |
5.10 |
51.95 |
71.95 |
Module 23: Estimate EBF by parity
parity_results <- weighted_ebf_table(
design, analysis_data, "parity_group", "Parity"
)
report_table(parity_results, "Exclusive breastfeeding by parity")
Exclusive breastfeeding by parity
| Parity |
1 |
281 |
65.33 |
4.55 |
56.42 |
74.24 |
| Parity |
2-3 |
516 |
53.80 |
3.64 |
46.66 |
60.94 |
| Parity |
4-5 |
253 |
62.46 |
4.83 |
52.99 |
71.94 |
| Parity |
6+ |
275 |
55.65 |
5.10 |
45.66 |
65.64 |
Module 24: Estimate current and harmonized regional prevalence
region_results <- weighted_ebf_table(
design, analysis_data, "region", "Current region"
)
harmonized_region_results <- weighted_ebf_table(
design, analysis_data, "harmonized_region", "Harmonized region"
)
report_table(region_results, "Exclusive breastfeeding by current region")
Exclusive breastfeeding by current region
| Current region |
Oromia |
131 |
48.73 |
5.06 |
38.80 |
58.65 |
| Current region |
Tigray |
123 |
77.94 |
3.28 |
71.51 |
84.37 |
| Current region |
Afar |
115 |
41.09 |
5.71 |
29.90 |
52.28 |
| Current region |
Amhara |
106 |
72.26 |
5.22 |
62.04 |
82.49 |
| Current region |
Somali |
123 |
28.88 |
5.07 |
18.95 |
38.80 |
| Current region |
Benishangul-Gumuz |
82 |
70.72 |
5.33 |
60.27 |
81.16 |
| Current region |
Central Ethiopia |
86 |
58.09 |
5.94 |
46.46 |
69.73 |
| Current region |
Sidama |
76 |
59.58 |
5.60 |
48.61 |
70.55 |
| Current region |
Southwest Ethiopia |
76 |
65.54 |
6.03 |
53.73 |
77.35 |
| Current region |
South Ethiopia |
147 |
58.38 |
5.06 |
48.47 |
68.29 |
| Current region |
Gambella |
69 |
42.14 |
7.12 |
28.19 |
56.10 |
| Current region |
Harari |
72 |
51.62 |
6.73 |
38.43 |
64.81 |
| Current region |
Addis Ababa |
48 |
47.88 |
7.73 |
32.74 |
63.03 |
| Current region |
Dire Dawa |
71 |
49.95 |
7.62 |
35.01 |
64.89 |
report_table(harmonized_region_results, "Exclusive breastfeeding by harmonized 2016-compatible region")
Exclusive breastfeeding by harmonized 2016-compatible
region
| Harmonized region |
Oromia |
131 |
48.73 |
5.06 |
38.80 |
58.65 |
| Harmonized region |
Tigray |
123 |
77.94 |
3.28 |
71.51 |
84.37 |
| Harmonized region |
Afar |
115 |
41.09 |
5.71 |
29.90 |
52.28 |
| Harmonized region |
Amhara |
106 |
72.26 |
5.22 |
62.04 |
82.49 |
| Harmonized region |
Somali |
123 |
28.88 |
5.07 |
18.95 |
38.80 |
| Harmonized region |
Benishangul-Gumuz |
82 |
70.72 |
5.33 |
60.27 |
81.16 |
| Harmonized region |
Former SNNPR |
385 |
59.37 |
3.12 |
53.25 |
65.49 |
| Harmonized region |
Gambella |
69 |
42.14 |
7.12 |
28.19 |
56.10 |
| Harmonized region |
Harari |
72 |
51.62 |
6.73 |
38.43 |
64.81 |
| Harmonized region |
Addis Ababa |
48 |
47.88 |
7.73 |
32.74 |
63.03 |
| Harmonized region |
Dire Dawa |
71 |
49.95 |
7.62 |
35.01 |
64.89 |
Module 25: Combine all descriptive results
all_prevalence_results <- bind_rows(
national_results,
age_results %>% select(-Infant_Age_Months),
sex_results,
residence_results,
education_results,
wealth_results,
maternal_age_results,
parity_results,
region_results
) %>%
mutate(
Estimate_95_CI = sprintf("%.1f%% (%.1f-%.1f)", EBF_Percent, Lower_95_CI, Upper_95_CI)
)
report_table(all_prevalence_results, "Combined survey-weighted descriptive results")
Combined survey-weighted descriptive results
| National |
Ethiopia |
1325 |
58.07 |
2.51 |
53.15 |
63.00 |
58.1% (53.1-63.0) |
| Infant age |
0 months |
207 |
85.22 |
4.75 |
75.90 |
94.53 |
85.2% (75.9-94.5) |
| Infant age |
1 months |
233 |
69.46 |
5.19 |
59.28 |
79.64 |
69.5% (59.3-79.6) |
| Infant age |
2 months |
237 |
63.40 |
5.58 |
52.46 |
74.34 |
63.4% (52.5-74.3) |
| Infant age |
3 months |
213 |
71.26 |
4.95 |
61.56 |
80.97 |
71.3% (61.6-81.0) |
| Infant age |
4 months |
224 |
42.08 |
5.21 |
31.88 |
52.29 |
42.1% (31.9-52.3) |
| Infant age |
5 months |
211 |
26.19 |
5.84 |
14.74 |
37.64 |
26.2% (14.7-37.6) |
| Infant sex |
Male |
659 |
60.15 |
3.35 |
53.58 |
66.71 |
60.1% (53.6-66.7) |
| Infant sex |
Female |
666 |
56.09 |
3.36 |
49.50 |
62.69 |
56.1% (49.5-62.7) |
| Residence |
Rural |
933 |
59.79 |
3.23 |
53.45 |
66.12 |
59.8% (53.4-66.1) |
| Residence |
Urban |
392 |
54.43 |
3.75 |
47.07 |
61.78 |
54.4% (47.1-61.8) |
| Maternal education |
No education |
496 |
55.76 |
3.91 |
48.09 |
63.44 |
55.8% (48.1-63.4) |
| Maternal education |
Primary |
496 |
58.99 |
3.49 |
52.15 |
65.83 |
59.0% (52.2-65.8) |
| Maternal education |
Secondary |
201 |
58.51 |
6.03 |
46.70 |
70.33 |
58.5% (46.7-70.3) |
| Maternal education |
Higher |
132 |
61.42 |
7.11 |
47.48 |
75.36 |
61.4% (47.5-75.4) |
| Household wealth |
Poorest |
467 |
55.28 |
4.01 |
47.42 |
63.14 |
55.3% (47.4-63.1) |
| Household wealth |
Poorer |
228 |
54.72 |
5.03 |
44.86 |
64.59 |
54.7% (44.9-64.6) |
| Household wealth |
Middle |
196 |
57.16 |
5.63 |
46.13 |
68.19 |
57.2% (46.1-68.2) |
| Household wealth |
Richer |
177 |
64.96 |
6.14 |
52.93 |
77.00 |
65.0% (52.9-77.0) |
| Household wealth |
Richest |
257 |
57.46 |
4.79 |
48.08 |
66.84 |
57.5% (48.1-66.8) |
| Maternal age |
15-24 |
410 |
55.97 |
4.20 |
47.75 |
64.20 |
56.0% (47.7-64.2) |
| Maternal age |
25-34 |
672 |
57.63 |
3.48 |
50.82 |
64.44 |
57.6% (50.8-64.4) |
| Maternal age |
35-49 |
243 |
61.95 |
5.10 |
51.95 |
71.95 |
62.0% (52.0-72.0) |
| Parity |
1 |
281 |
65.33 |
4.55 |
56.42 |
74.24 |
65.3% (56.4-74.2) |
| Parity |
2-3 |
516 |
53.80 |
3.64 |
46.66 |
60.94 |
53.8% (46.7-60.9) |
| Parity |
4-5 |
253 |
62.46 |
4.83 |
52.99 |
71.94 |
62.5% (53.0-71.9) |
| Parity |
6+ |
275 |
55.65 |
5.10 |
45.66 |
65.64 |
55.6% (45.7-65.6) |
| Current region |
Oromia |
131 |
48.73 |
5.06 |
38.80 |
58.65 |
48.7% (38.8-58.6) |
| Current region |
Tigray |
123 |
77.94 |
3.28 |
71.51 |
84.37 |
77.9% (71.5-84.4) |
| Current region |
Afar |
115 |
41.09 |
5.71 |
29.90 |
52.28 |
41.1% (29.9-52.3) |
| Current region |
Amhara |
106 |
72.26 |
5.22 |
62.04 |
82.49 |
72.3% (62.0-82.5) |
| Current region |
Somali |
123 |
28.88 |
5.07 |
18.95 |
38.80 |
28.9% (18.9-38.8) |
| Current region |
Benishangul-Gumuz |
82 |
70.72 |
5.33 |
60.27 |
81.16 |
70.7% (60.3-81.2) |
| Current region |
Central Ethiopia |
86 |
58.09 |
5.94 |
46.46 |
69.73 |
58.1% (46.5-69.7) |
| Current region |
Sidama |
76 |
59.58 |
5.60 |
48.61 |
70.55 |
59.6% (48.6-70.6) |
| Current region |
Southwest Ethiopia |
76 |
65.54 |
6.03 |
53.73 |
77.35 |
65.5% (53.7-77.4) |
| Current region |
South Ethiopia |
147 |
58.38 |
5.06 |
48.47 |
68.29 |
58.4% (48.5-68.3) |
| Current region |
Gambella |
69 |
42.14 |
7.12 |
28.19 |
56.10 |
42.1% (28.2-56.1) |
| Current region |
Harari |
72 |
51.62 |
6.73 |
38.43 |
64.81 |
51.6% (38.4-64.8) |
| Current region |
Addis Ababa |
48 |
47.88 |
7.73 |
32.74 |
63.03 |
47.9% (32.7-63.0) |
| Current region |
Dire Dawa |
71 |
49.95 |
7.62 |
35.01 |
64.89 |
49.9% (35.0-64.9) |
write.csv(all_prevalence_results,
file.path(output_dir, "tables", "Table_All_Weighted_Descriptive_Results.csv"),
row.names = FALSE)
Module 26: Design-adjusted bivariate tests
test_variables <- c(
infant_age_month = "Infant age",
infant_sex = "Infant sex",
residence = "Residence",
maternal_education = "Maternal education",
wealth_quintile = "Household wealth",
maternal_age_group = "Maternal age",
parity_group = "Parity",
region = "Region"
)
rao_scott_results <- imap_dfr(test_variables, function(label, variable) {
test <- svychisq(
as.formula(paste0("~ebf_factor + ", variable)),
design = design,
statistic = "F"
)
data.frame(
Predictor = label,
Design_Adjusted_F = unname(test$statistic),
Numerator_df = unname(test$parameter[1]),
Denominator_df = unname(test$parameter[2]),
P_Value = unname(test$p.value),
P_Value_Formatted = format_p(unname(test$p.value))
)
})
report_table(rao_scott_results, "Rao-Scott design-adjusted tests of association")
Rao-Scott design-adjusted tests of association
| Infant age |
13.73 |
4.53 |
2683.60 |
0.00 |
<0.001 |
| Infant sex |
0.82 |
1.00 |
592.00 |
0.37 |
0.365 |
| Residence |
1.17 |
1.00 |
592.00 |
0.28 |
0.279 |
| Maternal education |
0.23 |
2.97 |
1756.38 |
0.88 |
0.877 |
| Household wealth |
0.66 |
3.58 |
2118.16 |
0.60 |
0.601 |
| Maternal age |
0.43 |
1.96 |
1158.41 |
0.64 |
0.644 |
| Parity |
1.74 |
2.90 |
1718.61 |
0.16 |
0.159 |
| Region |
6.72 |
4.76 |
2818.22 |
0.00 |
<0.001 |
write.csv(rao_scott_results,
file.path(output_dir, "tables", "Table_Rao_Scott_Tests.csv"),
row.names = FALSE)
Module 27: Survey-weighted logistic regression
extract_or_table <- function(fitted_model, model_name) {
broom::tidy(fitted_model, conf.int = TRUE, exponentiate = TRUE) %>%
filter(term != "(Intercept)") %>%
transmute(
Model = model_name,
Term = term,
Odds_Ratio = estimate,
Lower_95_CI = conf.low,
Upper_95_CI = conf.high,
P_Value = p.value,
P_Value_Formatted = format_p(p.value)
)
}
unadjusted_predictors <- c(
"infant_age_month", "infant_sex", "residence", "maternal_education",
"wealth_quintile", "maternal_age_group", "parity_group", "region"
)
unadjusted_models <- setNames(
lapply(unadjusted_predictors, function(variable) {
svyglm(as.formula(paste0("ebf ~ ", variable)),
design = design, family = quasibinomial())
}),
unadjusted_predictors
)
unadjusted_or_table <- imap_dfr(
unadjusted_models,
~extract_or_table(.x, paste("Unadjusted:", .y))
)
unadjusted_global_tests <- imap_dfr(unadjusted_models, function(model, variable) {
test <- regTermTest(model, as.formula(paste0("~", variable)))
data.frame(
Predictor = variable,
Wald_F = unname(test$Ftest),
Numerator_df = unname(test$df),
Denominator_df = unname(test$ddf),
P_Value = unname(test$p),
P_Value_Formatted = format_p(unname(test$p))
)
})
adjusted_model <- svyglm(
ebf ~ infant_age_month + infant_sex + residence + maternal_education +
wealth_quintile + maternal_age_group + parity_group + region,
design = design,
family = quasibinomial()
)
adjusted_or_table <- extract_or_table(adjusted_model, "Fully adjusted model")
adjusted_terms <- unadjusted_predictors
adjusted_global_tests <- map_dfr(adjusted_terms, function(variable) {
test <- regTermTest(adjusted_model, as.formula(paste0("~", variable)))
data.frame(
Predictor = variable,
Wald_F = unname(test$Ftest),
Numerator_df = unname(test$df),
Denominator_df = unname(test$ddf),
P_Value = unname(test$p),
P_Value_Formatted = format_p(unname(test$p))
)
})
report_table(unadjusted_or_table, "Unadjusted survey-weighted logistic-regression estimates")
Unadjusted survey-weighted logistic-regression
estimates
| Unadjusted: infant_age_month |
infant_age_month1 months |
0.39 |
0.16 |
0.94 |
0.04 |
0.037 |
| Unadjusted: infant_age_month |
infant_age_month2 months |
0.30 |
0.12 |
0.74 |
0.01 |
0.009 |
| Unadjusted: infant_age_month |
infant_age_month3 months |
0.43 |
0.22 |
0.86 |
0.02 |
0.016 |
| Unadjusted: infant_age_month |
infant_age_month4 months |
0.13 |
0.05 |
0.30 |
0.00 |
<0.001 |
| Unadjusted: infant_age_month |
infant_age_month5 months |
0.06 |
0.02 |
0.16 |
0.00 |
<0.001 |
| Unadjusted: infant_sex |
infant_sexFemale |
0.85 |
0.59 |
1.21 |
0.37 |
0.365 |
| Unadjusted: residence |
residenceUrban |
0.80 |
0.54 |
1.20 |
0.28 |
0.280 |
| Unadjusted: maternal_education |
maternal_educationPrimary |
1.14 |
0.77 |
1.69 |
0.51 |
0.511 |
| Unadjusted: maternal_education |
maternal_educationSecondary |
1.12 |
0.64 |
1.97 |
0.70 |
0.696 |
| Unadjusted: maternal_education |
maternal_educationHigher |
1.26 |
0.65 |
2.46 |
0.49 |
0.493 |
| Unadjusted: wealth_quintile |
wealth_quintilePoorer |
0.98 |
0.62 |
1.54 |
0.92 |
0.922 |
| Unadjusted: wealth_quintile |
wealth_quintileMiddle |
1.08 |
0.63 |
1.83 |
0.78 |
0.778 |
| Unadjusted: wealth_quintile |
wealth_quintileRicher |
1.50 |
0.80 |
2.82 |
0.21 |
0.207 |
| Unadjusted: wealth_quintile |
wealth_quintileRichest |
1.09 |
0.66 |
1.80 |
0.73 |
0.728 |
| Unadjusted: maternal_age_group |
maternal_age_group25-34 |
1.07 |
0.70 |
1.63 |
0.75 |
0.753 |
| Unadjusted: maternal_age_group |
maternal_age_group35-49 |
1.28 |
0.77 |
2.12 |
0.33 |
0.335 |
| Unadjusted: parity_group |
parity_group2-3 |
0.62 |
0.39 |
0.98 |
0.04 |
0.041 |
| Unadjusted: parity_group |
parity_group4-5 |
0.88 |
0.50 |
1.56 |
0.67 |
0.669 |
| Unadjusted: parity_group |
parity_group6+ |
0.67 |
0.39 |
1.15 |
0.14 |
0.142 |
| Unadjusted: region |
regionTigray |
3.72 |
2.15 |
6.42 |
0.00 |
<0.001 |
| Unadjusted: region |
regionAfar |
0.73 |
0.40 |
1.35 |
0.32 |
0.320 |
| Unadjusted: region |
regionAmhara |
2.74 |
1.43 |
5.24 |
0.00 |
0.002 |
| Unadjusted: region |
regionSomali |
0.43 |
0.23 |
0.80 |
0.01 |
0.008 |
| Unadjusted: region |
regionBenishangul-Gumuz |
2.54 |
1.34 |
4.84 |
0.00 |
0.005 |
| Unadjusted: region |
regionCentral Ethiopia |
1.46 |
0.78 |
2.72 |
0.23 |
0.234 |
| Unadjusted: region |
regionSidama |
1.55 |
0.85 |
2.84 |
0.16 |
0.155 |
| Unadjusted: region |
regionSouthwest Ethiopia |
2.00 |
1.04 |
3.87 |
0.04 |
0.039 |
| Unadjusted: region |
regionSouth Ethiopia |
1.48 |
0.83 |
2.61 |
0.18 |
0.181 |
| Unadjusted: region |
regionGambella |
0.77 |
0.38 |
1.54 |
0.45 |
0.455 |
| Unadjusted: region |
regionHarari |
1.12 |
0.58 |
2.18 |
0.73 |
0.731 |
| Unadjusted: region |
regionAddis Ababa |
0.97 |
0.47 |
2.00 |
0.93 |
0.928 |
| Unadjusted: region |
regionDire Dawa |
1.05 |
0.51 |
2.16 |
0.89 |
0.894 |
report_table(unadjusted_global_tests, "Global Wald tests for unadjusted models")
Global Wald tests for unadjusted models
| infant_age_month |
10.46 |
5 |
587 |
0.00 |
<0.001 |
| infant_sex |
0.82 |
1 |
591 |
0.37 |
0.365 |
| residence |
1.17 |
1 |
591 |
0.28 |
0.280 |
| maternal_education |
0.22 |
3 |
589 |
0.88 |
0.879 |
| wealth_quintile |
0.48 |
4 |
588 |
0.75 |
0.753 |
| maternal_age_group |
0.47 |
2 |
590 |
0.63 |
0.625 |
| parity_group |
2.05 |
3 |
589 |
0.11 |
0.106 |
| region |
6.23 |
13 |
579 |
0.00 |
<0.001 |
report_table(adjusted_or_table, "Adjusted survey-weighted logistic-regression estimates")
Adjusted survey-weighted logistic-regression
estimates
| Fully adjusted model |
infant_age_month1 months |
0.36 |
0.15 |
0.90 |
0.03 |
0.028 |
| Fully adjusted model |
infant_age_month2 months |
0.27 |
0.11 |
0.67 |
0.01 |
0.005 |
| Fully adjusted model |
infant_age_month3 months |
0.40 |
0.20 |
0.82 |
0.01 |
0.012 |
| Fully adjusted model |
infant_age_month4 months |
0.09 |
0.04 |
0.20 |
0.00 |
<0.001 |
| Fully adjusted model |
infant_age_month5 months |
0.05 |
0.02 |
0.12 |
0.00 |
<0.001 |
| Fully adjusted model |
infant_sexFemale |
0.96 |
0.63 |
1.47 |
0.86 |
0.857 |
| Fully adjusted model |
residenceUrban |
0.54 |
0.23 |
1.30 |
0.17 |
0.171 |
| Fully adjusted model |
maternal_educationPrimary |
1.19 |
0.73 |
1.95 |
0.49 |
0.490 |
| Fully adjusted model |
maternal_educationSecondary |
0.83 |
0.42 |
1.63 |
0.59 |
0.589 |
| Fully adjusted model |
maternal_educationHigher |
0.79 |
0.35 |
1.78 |
0.57 |
0.574 |
| Fully adjusted model |
wealth_quintilePoorer |
1.04 |
0.57 |
1.87 |
0.90 |
0.903 |
| Fully adjusted model |
wealth_quintileMiddle |
1.28 |
0.63 |
2.60 |
0.50 |
0.498 |
| Fully adjusted model |
wealth_quintileRicher |
1.93 |
0.87 |
4.29 |
0.10 |
0.104 |
| Fully adjusted model |
wealth_quintileRichest |
2.55 |
0.76 |
8.55 |
0.13 |
0.129 |
| Fully adjusted model |
maternal_age_group25-34 |
1.24 |
0.70 |
2.21 |
0.46 |
0.457 |
| Fully adjusted model |
maternal_age_group35-49 |
1.31 |
0.56 |
3.05 |
0.53 |
0.532 |
| Fully adjusted model |
parity_group2-3 |
0.56 |
0.30 |
1.05 |
0.07 |
0.073 |
| Fully adjusted model |
parity_group4-5 |
0.78 |
0.32 |
1.92 |
0.59 |
0.592 |
| Fully adjusted model |
parity_group6+ |
0.56 |
0.23 |
1.34 |
0.19 |
0.192 |
| Fully adjusted model |
regionTigray |
4.79 |
2.42 |
9.50 |
0.00 |
<0.001 |
| Fully adjusted model |
regionAfar |
0.67 |
0.30 |
1.53 |
0.34 |
0.342 |
| Fully adjusted model |
regionAmhara |
2.70 |
1.29 |
5.65 |
0.01 |
0.008 |
| Fully adjusted model |
regionSomali |
0.33 |
0.15 |
0.74 |
0.01 |
0.007 |
| Fully adjusted model |
regionBenishangul-Gumuz |
2.36 |
1.06 |
5.22 |
0.03 |
0.035 |
| Fully adjusted model |
regionCentral Ethiopia |
1.69 |
0.76 |
3.74 |
0.20 |
0.198 |
| Fully adjusted model |
regionSidama |
1.40 |
0.68 |
2.90 |
0.36 |
0.364 |
| Fully adjusted model |
regionSouthwest Ethiopia |
2.33 |
1.10 |
4.91 |
0.03 |
0.027 |
| Fully adjusted model |
regionSouth Ethiopia |
1.59 |
0.87 |
2.91 |
0.13 |
0.133 |
| Fully adjusted model |
regionGambella |
0.72 |
0.30 |
1.71 |
0.45 |
0.450 |
| Fully adjusted model |
regionHarari |
0.85 |
0.43 |
1.66 |
0.63 |
0.628 |
| Fully adjusted model |
regionAddis Ababa |
0.86 |
0.27 |
2.70 |
0.79 |
0.790 |
| Fully adjusted model |
regionDire Dawa |
0.74 |
0.30 |
1.83 |
0.51 |
0.513 |
report_table(adjusted_global_tests, "Global Wald tests for the adjusted model")
Global Wald tests for the adjusted model
| infant_age_month |
13.24 |
5 |
560 |
0.00 |
<0.001 |
| infant_sex |
0.03 |
1 |
560 |
0.86 |
0.857 |
| residence |
1.88 |
1 |
560 |
0.17 |
0.171 |
| maternal_education |
0.79 |
3 |
560 |
0.50 |
0.498 |
| wealth_quintile |
1.08 |
4 |
560 |
0.36 |
0.364 |
| maternal_age_group |
0.29 |
2 |
560 |
0.75 |
0.750 |
| parity_group |
1.63 |
3 |
560 |
0.18 |
0.181 |
| region |
5.17 |
13 |
560 |
0.00 |
<0.001 |
write.csv(unadjusted_or_table, file.path(output_dir, "tables", "Table_Unadjusted_Odds_Ratios.csv"), row.names = FALSE)
write.csv(adjusted_or_table, file.path(output_dir, "tables", "Table_Adjusted_Odds_Ratios.csv"), row.names = FALSE)
write.csv(adjusted_global_tests, file.path(output_dir, "tables", "Table_Adjusted_Global_Wald_Tests.csv"), row.names = FALSE)
Module 28: Adjusted odds ratios and trend models
age_trend_model <- svyglm(
ebf ~ b19 + infant_sex + residence + maternal_education + wealth_quintile +
maternal_age_group + parity_group + region,
design = design,
family = quasibinomial()
)
age_trend_result <- broom::tidy(age_trend_model, conf.int = TRUE, exponentiate = TRUE) %>%
filter(term == "b19") %>%
transmute(
Predictor = "Each additional completed month of infant age",
Adjusted_Odds_Ratio = estimate,
Lower_95_CI = conf.low,
Upper_95_CI = conf.high,
P_Value = p.value,
P_Value_Formatted = format_p(p.value)
)
wealth_trend_model <- svyglm(
ebf ~ infant_age_month + infant_sex + residence + maternal_education + wealth_score +
maternal_age_group + parity_group + region,
design = design,
family = quasibinomial()
)
wealth_trend_result <- broom::tidy(wealth_trend_model, conf.int = TRUE, exponentiate = TRUE) %>%
filter(term == "wealth_score") %>%
transmute(
Predictor = "One-quintile increase in household wealth",
Adjusted_Odds_Ratio = estimate,
Lower_95_CI = conf.low,
Upper_95_CI = conf.high,
P_Value = p.value,
P_Value_Formatted = format_p(p.value)
)
report_table(age_trend_result, "Adjusted linear trend in EBF by infant age")
Adjusted linear trend in EBF by infant age
| Each additional completed month of infant age |
0.59 |
0.51 |
0.68 |
0 |
<0.001 |
report_table(wealth_trend_result, "Adjusted linear trend in EBF by household wealth")
Adjusted linear trend in EBF by household wealth
| One-quintile increase in household wealth |
1.25 |
0.99 |
1.58 |
0.07 |
0.066 |
harmonized_model <- svyglm(
ebf ~ infant_age_month + infant_sex + residence + maternal_education +
wealth_quintile + maternal_age_group + parity_group + harmonized_region,
design = design,
family = quasibinomial()
)
harmonized_model_results <- extract_or_table(
harmonized_model,
"Adjusted model using harmonized regions"
)
model_variables <- c(
"ebf", "infant_age_month", "infant_sex", "residence", "maternal_education",
"wealth_quintile", "maternal_age_group", "parity_group", "region"
)
model_sample_flow <- data.frame(
Stage = c("Eligible analytical sample", "Complete cases for primary adjusted model", "Excluded for missing model covariates"),
Unweighted_N = c(
nrow(analysis_data),
sum(complete.cases(analysis_data[model_variables])),
nrow(analysis_data) - sum(complete.cases(analysis_data[model_variables]))
)
)
report_table(model_sample_flow, "Model sample flow")
Model sample flow
| Eligible analytical sample |
1325 |
| Complete cases for primary adjusted model |
1325 |
| Excluded for missing model covariates |
0 |
report_table(harmonized_model_results, "Sensitivity model using harmonized regions")
Sensitivity model using harmonized regions
| Adjusted model using harmonized regions |
infant_age_month1 months |
0.36 |
0.15 |
0.89 |
0.03 |
0.028 |
| Adjusted model using harmonized regions |
infant_age_month2 months |
0.27 |
0.11 |
0.67 |
0.00 |
0.005 |
| Adjusted model using harmonized regions |
infant_age_month3 months |
0.40 |
0.20 |
0.82 |
0.01 |
0.012 |
| Adjusted model using harmonized regions |
infant_age_month4 months |
0.09 |
0.04 |
0.20 |
0.00 |
<0.001 |
| Adjusted model using harmonized regions |
infant_age_month5 months |
0.05 |
0.02 |
0.12 |
0.00 |
<0.001 |
| Adjusted model using harmonized regions |
infant_sexFemale |
0.96 |
0.63 |
1.46 |
0.84 |
0.841 |
| Adjusted model using harmonized regions |
residenceUrban |
0.55 |
0.23 |
1.31 |
0.18 |
0.177 |
| Adjusted model using harmonized regions |
maternal_educationPrimary |
1.18 |
0.72 |
1.92 |
0.52 |
0.518 |
| Adjusted model using harmonized regions |
maternal_educationSecondary |
0.82 |
0.42 |
1.61 |
0.56 |
0.565 |
| Adjusted model using harmonized regions |
maternal_educationHigher |
0.79 |
0.35 |
1.76 |
0.56 |
0.559 |
| Adjusted model using harmonized regions |
wealth_quintilePoorer |
1.03 |
0.57 |
1.86 |
0.92 |
0.916 |
| Adjusted model using harmonized regions |
wealth_quintileMiddle |
1.27 |
0.63 |
2.58 |
0.50 |
0.500 |
| Adjusted model using harmonized regions |
wealth_quintileRicher |
1.92 |
0.87 |
4.23 |
0.11 |
0.105 |
| Adjusted model using harmonized regions |
wealth_quintileRichest |
2.49 |
0.75 |
8.28 |
0.14 |
0.136 |
| Adjusted model using harmonized regions |
maternal_age_group25-34 |
1.25 |
0.71 |
2.21 |
0.44 |
0.438 |
| Adjusted model using harmonized regions |
maternal_age_group35-49 |
1.33 |
0.57 |
3.09 |
0.50 |
0.504 |
| Adjusted model using harmonized regions |
parity_group2-3 |
0.56 |
0.30 |
1.05 |
0.07 |
0.072 |
| Adjusted model using harmonized regions |
parity_group4-5 |
0.78 |
0.32 |
1.89 |
0.58 |
0.579 |
| Adjusted model using harmonized regions |
parity_group6+ |
0.55 |
0.23 |
1.30 |
0.17 |
0.172 |
| Adjusted model using harmonized regions |
harmonized_regionTigray |
4.79 |
2.42 |
9.49 |
0.00 |
<0.001 |
| Adjusted model using harmonized regions |
harmonized_regionAfar |
0.67 |
0.30 |
1.52 |
0.34 |
0.336 |
| Adjusted model using harmonized regions |
harmonized_regionAmhara |
2.69 |
1.29 |
5.63 |
0.01 |
0.009 |
| Adjusted model using harmonized regions |
harmonized_regionSomali |
0.33 |
0.15 |
0.74 |
0.01 |
0.007 |
| Adjusted model using harmonized regions |
harmonized_regionBenishangul-Gumuz |
2.35 |
1.06 |
5.20 |
0.04 |
0.036 |
| Adjusted model using harmonized regions |
harmonized_regionFormer SNNPR |
1.64 |
0.95 |
2.84 |
0.07 |
0.075 |
| Adjusted model using harmonized regions |
harmonized_regionGambella |
0.72 |
0.30 |
1.71 |
0.45 |
0.451 |
| Adjusted model using harmonized regions |
harmonized_regionHarari |
0.85 |
0.43 |
1.67 |
0.64 |
0.637 |
| Adjusted model using harmonized regions |
harmonized_regionAddis Ababa |
0.86 |
0.27 |
2.71 |
0.80 |
0.797 |
| Adjusted model using harmonized regions |
harmonized_regionDire Dawa |
0.74 |
0.30 |
1.84 |
0.52 |
0.518 |
write.csv(model_sample_flow, file.path(output_dir, "tables", "Table_Model_Sample_Flow.csv"), row.names = FALSE)
write.csv(harmonized_model_results, file.path(output_dir, "tables", "Table_Harmonized_Region_Model.csv"), row.names = FALSE)
Module 29: Adjusted predicted probabilities
extract_emmeans_probability <- function(emm_object, grouping_name, output_name) {
emm_df <- as.data.frame(emm_object)
probability_name <- intersect(c("prob", "response"), names(emm_df))[1]
lower_name <- intersect(c("asymp.LCL", "lower.CL"), names(emm_df))[1]
upper_name <- intersect(c("asymp.UCL", "upper.CL"), names(emm_df))[1]
if (any(is.na(c(probability_name, lower_name, upper_name)))) {
stop("Expected probability or confidence-limit columns were not found in emmeans output.")
}
emm_df %>%
transmute(
Category = as.character(.data[[grouping_name]]),
Adjusted_EBF_Percent = 100 * .data[[probability_name]],
Standard_Error_Percentage_Points = 100 * SE,
Lower_95_CI = 100 * .data[[lower_name]],
Upper_95_CI = 100 * .data[[upper_name]]
) %>%
rename(!!output_name := Category)
}
age_nuisance_factors <- c(
"infant_sex", "residence", "maternal_education", "wealth_quintile",
"maternal_age_group", "parity_group", "region"
)
age_emmeans <- emmeans(
adjusted_model,
specs = ~infant_age_month,
nuisance = age_nuisance_factors,
wt.nuis = "proportional",
weights = "proportional",
type = "response"
)
adjusted_age_probabilities <- extract_emmeans_probability(
age_emmeans, "infant_age_month", "Infant_Age"
) %>%
mutate(Infant_Age_Months = as.integer(str_extract(Infant_Age, "^[0-5]")))
region_nuisance_factors <- c(
"infant_age_month", "infant_sex", "residence", "maternal_education",
"wealth_quintile", "maternal_age_group", "parity_group"
)
region_emmeans <- emmeans(
adjusted_model,
specs = ~region,
nuisance = region_nuisance_factors,
wt.nuis = "proportional",
weights = "proportional",
type = "response"
)
adjusted_region_probabilities <- extract_emmeans_probability(
region_emmeans, "region", "Region"
)
report_table(adjusted_age_probabilities, "Adjusted EBF probabilities by infant age")
Adjusted EBF probabilities by infant age
| 0 months |
84.77 |
4.42 |
74.00 |
91.58 |
0 |
| 1 months |
66.92 |
5.74 |
54.90 |
77.07 |
1 |
| 2 months |
59.91 |
5.78 |
48.25 |
70.55 |
2 |
| 3 months |
69.18 |
4.79 |
59.10 |
77.71 |
3 |
| 4 months |
32.29 |
4.68 |
23.86 |
42.05 |
4 |
| 5 months |
21.14 |
4.93 |
13.05 |
32.38 |
5 |
report_table(adjusted_region_probabilities, "Adjusted EBF probabilities by region")
Adjusted EBF probabilities by region
| Oromia |
50.31 |
6.32 |
38.15 |
62.43 |
| Tigray |
82.92 |
3.42 |
75.16 |
88.62 |
| Afar |
40.51 |
7.66 |
26.75 |
55.95 |
| Amhara |
73.21 |
5.88 |
60.29 |
83.10 |
| Somali |
25.29 |
5.86 |
15.56 |
38.35 |
| Benishangul-Gumuz |
70.46 |
6.81 |
55.66 |
81.92 |
| Central Ethiopia |
63.07 |
7.76 |
47.05 |
76.65 |
| Sidama |
58.65 |
6.70 |
45.22 |
70.90 |
| Southwest Ethiopia |
70.21 |
5.96 |
57.42 |
80.46 |
| South Ethiopia |
61.69 |
4.57 |
52.43 |
70.18 |
| Gambella |
42.01 |
8.60 |
26.62 |
59.13 |
| Harari |
46.15 |
6.61 |
33.73 |
59.08 |
| Addis Ababa |
46.41 |
13.42 |
23.12 |
71.38 |
| Dire Dawa |
42.80 |
9.79 |
25.48 |
62.10 |
write.csv(adjusted_age_probabilities, file.path(output_dir, "tables", "Table_Adjusted_Age_Probabilities.csv"), row.names = FALSE)
write.csv(adjusted_region_probabilities, file.path(output_dir, "tables", "Table_Adjusted_Region_Probabilities.csv"), row.names = FALSE)
Module 30: Publication figures, DAG, conceptual framework, and
export
Figure 1. Epidemiologic DAG
dag_boxes <- data.frame(
label = c("Regional context", "Residence", "Maternal education", "Household wealth",
"Maternal age", "Parity", "Infant sex", "Infant age", "Exclusive breastfeeding"),
x = c(5, 2, 5, 8, 1.5, 3.8, 6.2, 8.5, 5),
y = c(9, 7, 7, 7, 4.7, 4.7, 4.7, 4.7, 1.7),
width = c(4.2, 2.3, 2.5, 2.3, 1.8, 1.8, 1.8, 1.8, 3.4),
height = c(1.0, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 1.1),
group = c("context", rep("socioeconomic", 3), rep("proximal", 4), "outcome")
)
dag_edges <- data.frame(
from = c(1,1,1,2,3,2,2,3,3,4,4,4,5,6,7,8),
to = c(2,3,4,3,4,5,6,5,6,6,7,8,9,9,9,9)
)
get_anchor <- function(index) dag_boxes[index, ]
dag_plot <- ggplot() +
geom_segment(data = dag_edges, aes(
x = dag_boxes$x[from], y = dag_boxes$y[from] - dag_boxes$height[from]/2,
xend = dag_boxes$x[to], yend = dag_boxes$y[to] + dag_boxes$height[to]/2
), arrow = arrow(length = unit(0.16, "inches")), linewidth = 0.45) +
geom_rect(data = dag_boxes, aes(
xmin = x - width/2, xmax = x + width/2,
ymin = y - height/2, ymax = y + height/2,
fill = group
), color = "black", linewidth = 0.45) +
geom_text(data = dag_boxes, aes(x = x, y = y, label = label),
fontface = "bold", size = 3.4, lineheight = 0.95) +
scale_fill_manual(values = c(
context = "#D9EAF7", socioeconomic = "#DDEFD9",
proximal = "#F7E7C6", outcome = "#F5D5DD"
)) +
coord_cartesian(xlim = c(0, 10), ylim = c(0.5, 10), clip = "off") +
labs(title = "Directed Acyclic Graph of Hypothesized Determinants of EBF",
subtitle = "Infants aged 0-5 completed months, Ethiopia DHS 2024-25") +
theme_void(base_size = 11) +
theme(legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold"),
plot.subtitle = element_text(hjust = 0.5))
dag_plot
ggsave(file.path(output_dir, "figures", "Figure_1_EBF_DAG.png"), dag_plot,
width = 8, height = 10, dpi = 300)
Figure 2. Programmatic conceptual framework
cf_boxes <- data.frame(
label = c(
"POLICIES & HEALTH SYSTEM\nIYCF guidance, maternity protection,\nBaby-Friendly services, HEWs",
"HIGH INITIATION\nBreastfeeding begins at or soon after birth",
"CONTINUATION CHALLENGES\nPerceived insufficient milk; family influence;\nworkload; early liquids/foods; limited follow-up",
"ENABLING CONDITIONS\nMaternal confidence; family support;\ncommunity norms; repeated skilled counseling",
"SUSTAINED EBF TO 6 MONTHS\nImproved infant, maternal, and societal outcomes",
"STRATEGIC PRIORITY\nIntensify support during months 2-5\nand tailor implementation to regional context"
),
x = c(1.8, 5, 8.2, 5, 8.2, 5),
y = c(7.2, 7.2, 7.2, 4.6, 4.6, 1.8),
width = c(3.0, 2.7, 3.0, 4.6, 3.0, 5.4),
height = c(1.5, 1.5, 1.8, 1.6, 1.5, 1.5),
type = c("input", "continuum", "barrier", "enabler", "outcome", "priority")
)
cf_edges <- data.frame(from = c(1,2,3,4,4,5), to = c(2,3,4,5,6,6))
cf_plot <- ggplot() +
geom_segment(data = cf_edges, aes(
x = cf_boxes$x[from], y = cf_boxes$y[from],
xend = cf_boxes$x[to], yend = cf_boxes$y[to]
), arrow = arrow(length = unit(0.16, "inches")), linewidth = 0.55) +
geom_rect(data = cf_boxes, aes(
xmin = x - width/2, xmax = x + width/2,
ymin = y - height/2, ymax = y + height/2,
fill = type
), color = "black", linewidth = 0.5) +
geom_text(data = cf_boxes, aes(x = x, y = y, label = label),
size = 3.2, lineheight = 0.95) +
scale_fill_manual(values = c(
input = "#D9EAF7", continuum = "#DDEFD9", barrier = "#F7E7C6",
enabler = "#E8DDF2", outcome = "#D8EEDB", priority = "#DCE7F7"
)) +
coord_cartesian(xlim = c(0, 10), ylim = c(0.5, 8.5), clip = "off") +
labs(title = "From Initiation to Continuation",
subtitle = "A programmatic conceptual framework for exclusive breastfeeding in Ethiopia") +
theme_void(base_size = 11) +
theme(legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold"),
plot.subtitle = element_text(hjust = 0.5))
cf_plot
ggsave(file.path(output_dir, "figures", "Figure_2_EBF_Conceptual_Framework.png"), cf_plot,
width = 10, height = 8, dpi = 300)
Figure 3. Observed and adjusted EBF by infant age
age_plot_data <- age_results %>%
select(Infant_Age_Months, Observed = EBF_Percent, Observed_Lower = Lower_95_CI, Observed_Upper = Upper_95_CI) %>%
left_join(
adjusted_age_probabilities %>%
select(Infant_Age_Months, Adjusted = Adjusted_EBF_Percent,
Adjusted_Lower = Lower_95_CI, Adjusted_Upper = Upper_95_CI),
by = "Infant_Age_Months"
)
age_plot_long <- bind_rows(
age_plot_data %>% transmute(Infant_Age_Months, Series = "Observed prevalence", Estimate = Observed,
Lower = Observed_Lower, Upper = Observed_Upper),
age_plot_data %>% transmute(Infant_Age_Months, Series = "Adjusted probability", Estimate = Adjusted,
Lower = Adjusted_Lower, Upper = Adjusted_Upper)
)
age_figure <- ggplot(age_plot_long, aes(x = Infant_Age_Months, y = Estimate,
group = Series, linetype = Series, shape = Series)) +
geom_line(linewidth = 0.8) +
geom_point(size = 2.4) +
geom_errorbar(aes(ymin = Lower, ymax = Upper), width = 0.08, linewidth = 0.45) +
scale_x_continuous(breaks = 0:5) +
scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 20), labels = scales::label_percent(scale = 1)) +
labs(x = "Infant age in completed months", y = "Exclusive breastfeeding",
title = "Age-Specific Exclusive Breastfeeding in Ethiopia",
linetype = NULL, shape = NULL) +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")
age_figure
ggsave(file.path(output_dir, "figures", "Figure_3_EBF_By_Infant_Age.png"), age_figure,
width = 8, height = 5.5, dpi = 300)
Final exports and reproducibility record
write.csv(region_results, file.path(output_dir, "tables", "Table_Current_Region_Prevalence.csv"), row.names = FALSE)
write.csv(harmonized_region_results, file.path(output_dir, "tables", "Table_Harmonized_Region_Prevalence.csv"), row.names = FALSE)
write.csv(age_results, file.path(output_dir, "tables", "Table_Age_Specific_Prevalence.csv"), row.names = FALSE)
write.csv(national_results, file.path(output_dir, "tables", "Table_National_Prevalence.csv"), row.names = FALSE)
write.csv(published_comparison, file.path(output_dir, "tables", "Table_Official_Estimate_Validation.csv"), row.names = FALSE)
saveRDS(
list(
analysis_data = analysis_data,
design = design,
adjusted_model = adjusted_model,
national_results = national_results,
all_prevalence_results = all_prevalence_results,
adjusted_or_table = adjusted_or_table,
adjusted_age_probabilities = adjusted_age_probabilities,
adjusted_region_probabilities = adjusted_region_probabilities
),
file.path(output_dir, "EBF_Analysis_Objects.rds")
)
capture.output(sessionInfo(), file = file.path(output_dir, "R_Session_Info.txt"))
completion_summary <- data.frame(
Item = c("Analysis completed", "Elapsed minutes", "Output directory", "Final analytical N"),
Value = c(
format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
round(as.numeric(difftime(Sys.time(), session_start, units = "mins")), 2),
output_dir,
nrow(analysis_data)
)
)
report_table(completion_summary, "Analysis completion summary")
Analysis completion summary
| Analysis completed |
2026-07-17 07:37:01 |
| Elapsed minutes |
0.38 |
| Output directory |
C:/Users/aynal/OneDrive/Documents/ETH-DHS-2024-25/EBF_Unified_Output |
| Final analytical N |
1325 |
Interpretation note
The national EBF estimate is a cross-sectional prevalence among
infants aged 0-5 completed months based on feeding during the preceding
24 hours. It is not the proportion of a birth cohort that remained
continuously exclusively breastfed from birth through six months.
Age-specific estimates therefore provide essential information about
breastfeeding continuation.