Install mongolstats, and mongolmaps for the maps, from GitHub:
This walk-through uses the infant mortality rate (IMR), a key indicator of population health and health system performance. It covers the full workflow: find a table, check its structure, fetch data, calculate rates, and map them.
library(mongolstats)
library(mongolmaps)
library(dplyr)
library(ggplot2)
# Set language to English for readable output
nso_options(mongolstats.lang = "en")
# Set global ggplot2 theme with proper margins to prevent text cutoff
theme_set(
theme_minimal(base_size = 11) +
theme(
plot.margin = margin(10, 10, 10, 10),
plot.title = element_text(size = 13, face = "bold"),
plot.subtitle = element_text(size = 10, color = "grey40"),
legend.text = element_text(size = 9),
legend.title = element_text(size = 10)
)
)Search the catalogue by keyword:
NSO publishes both rates (per 1,000 live births) and
the counts behind them (infant deaths). We start with
the monthly rate table, DT_NSO_2100_015V1, and later use
the counts to compare aimags.
Before fetching data, check which dimensions the table has:
# View table structure
meta <- nso_table_meta("DT_NSO_2100_015V1")
meta
# Periods oldest first, with their dates. Month labels are "YYYY-MM"; the
# codes count back from the latest month ("0"), so select periods by label.
months <- nso_table_periods("DT_NSO_2100_015V1")
head(months)
range(months$label)Get the national rate for every month NSO publishes:
imr_national <- nso_data(
tbl_id = "DT_NSO_2100_015V1",
selections = list(
Region = "0", # national total
Month = months$label
),
labels = "en" # add English label columns
)
# The Month column holds labels such as "2024-03", which keep their meaning
# when NSO publishes a new month
imr_national |>
arrange(Month) |>
head(10)NSO rounds these monthly rates to whole numbers, which is coarse for a measure that sits around 10 to 20 per 1,000.
imr_national |>
mutate(date = nso_period_date(Month)) |> # "YYYY-MM" to Date
ggplot(aes(x = date, y = value)) +
geom_line(color = "#2c3e50", linewidth = 1, alpha = 0.3) + # dim raw data so trend stands out
geom_point(color = "#e74c3c", size = 2, shape = 21, fill = "white", stroke = 1, alpha = 0.6) +
# LOESS reveals the trend through month-to-month noise
geom_smooth(
method = "loess", formula = y ~ x, se = TRUE,
color = "#3498db", fill = "#3498db", alpha = 0.2, linewidth = 1.5
) +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(limits = c(0, NA)) +
labs(
title = "Infant Mortality Rate in Mongolia (Monthly)",
subtitle = "Deaths under age 1 per 1,000 live births, national",
x = NULL,
y = "IMR (per 1,000)",
caption = "Source: NSO Mongolia, DT_NSO_2100_015V1"
) +
theme(
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank() # vertical gridlines clutter time series
)To compare aimags over a year, do not average the 12 monthly rates. A mean of monthly rates gives a month with few births the same weight as a busy month, and NSO’s rounding makes small aimags’ monthly rates jump between values. Instead, add up the infant deaths and the live births for the year, then divide:
\[\text{IMR} = \frac{\text{infant deaths in the year}}{\text{live births in the year}} \times 1{,}000\]
The counts come from two monthly tables: infant deaths
(DT_NSO_2100_014V1) and live births
(DT_NSO_2100_018V5).
# Sum a monthly count table over one calendar year, for every region code
annual_counts <- function(tbl, year) {
months <- nso_dim_values(tbl, "Month", labels = "en")
nso_data(
tbl_id = tbl,
selections = list(
Region = nso_dim_values(tbl, "Region")$code,
Month = months$code[startsWith(months$label_en, year)]
),
labels = "en"
) |>
summarise(count = sum(value), n_months = n(), .by = c(Region, Region_en))
}
# Latest year with all 12 months published
birth_months <- nso_dim_values("DT_NSO_2100_018V5", "Month", labels = "en")$label_en
latest_year <- names(which(table(substr(birth_months, 1, 4)) == 12)) |> max()
infant_deaths <- annual_counts("DT_NSO_2100_014V1", latest_year)
live_births <- annual_counts("DT_NSO_2100_018V5", latest_year)
imr_regional <- infant_deaths |>
select(Region, Region_en, deaths = count) |>
inner_join(select(live_births, Region, births = count), by = join_by(Region)) |>
mutate(
Region_en = trimws(Region_en),
level = case_when(
Region == "0" ~ "National",
Region %in% c("1", "2", "3", "4") ~ "Region",
# Ulaanbaatar's figures sit under code "5"; code "511" is an empty duplicate
Region == "5" ~ "Capital",
Region == "511" ~ "Duplicate",
.default = "Aimag"
),
imr = deaths / births * 1000,
# Exact Poisson 95% confidence interval for the rate
lower = qchisq(0.025, 2 * deaths) / 2 / births * 1000,
upper = qchisq(0.975, 2 * (deaths + 1)) / 2 / births * 1000
)
national_imr <- imr_regional$imr[imr_regional$level == "National"]
imr_units <- imr_regional |>
filter(level %in% c("Aimag", "Capital"))
# Highest rates
imr_units |>
arrange(desc(imr)) |>
select(Region_en, deaths, births, imr, lower, upper) |>
head(10)In the latest year, the national IMR was - per 1,000 live births. Many aimags record only a few dozen infant deaths a year, so their rates come with wide confidence intervals.
imr_units |>
mutate(Region_en = forcats::fct_reorder(Region_en, imr)) |> # order by rate, not alphabet
ggplot(aes(x = imr, y = Region_en)) +
geom_vline(xintercept = national_imr, linetype = "dashed", color = "grey50") +
geom_errorbar(aes(xmin = lower, xmax = upper), width = 0.3, color = "grey60", orientation = "y") +
geom_point(aes(color = level), size = 2.5) +
scale_color_manual(values = c(Aimag = "#2a78d6", Capital = "#eb6834"), name = NULL) +
labs(
title = paste0("Infant Mortality by Aimag, ", latest_year),
subtitle = "Per 1,000 live births, with exact 95% CIs; dashed line = national rate",
x = "Deaths per 1,000 live births",
y = NULL,
caption = "Source: NSO Mongolia, DT_NSO_2100_014V1 and DT_NSO_2100_018V5"
) +
theme(
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
legend.position = "top"
)Where an interval crosses the dashed line, that aimag’s rate is consistent with the national rate. Infant deaths and births are counted where they are recorded, so referrals of high-risk births to hospitals in Ulaanbaatar can raise the capital’s rate and lower the rate of the aimag the mother came from.
mongolmaps
provides boundaries that carry NSO codes, so the Region
column joins without matching names.
# mn_join() takes Ulaanbaatar's figures from region code "5" when the
# capital's own code "511" is empty, as it is in this table
imr_map <- imr_units |>
mn_join(by = "Region", level = "aimag")
mn_map(
imr_map,
fill = imr,
title = paste0("Infant Mortality Rate, ", latest_year)
) +
labs(subtitle = "Infant deaths per 1,000 live births. Data: NSO Mongolia", fill = "IMR\n(per 1,000)")The map shows point estimates only. Read it with the confidence intervals above: in small aimags, a few more or fewer infant deaths can move the colour a long way.
| Function | Purpose | Example |
|---|---|---|
nso_search() |
Find tables by keyword | nso_search("mortality") |
nso_table_meta() |
Get table dimensions | nso_table_meta("DT_NSO_...") |
nso_dim_values() |
List dimension values | nso_dim_values(tbl, "Region") |
nso_table_periods() |
Check time coverage | nso_table_periods(tbl) |
nso_latest_periods() |
Latest periods, for selections | list(Year = nso_latest_periods(tbl, n = 5)) |
nso_data() |
Fetch data | nso_data(tbl, selections, labels) |
nso_period_date() |
Period labels to dates | nso_period_date(data$Month) |
mongolmaps::mn_join() |
Join NSO data to boundaries by code | mn_join(data, by = "Region", level = "aimag") |
mongolmaps::mn_map() |
Quick map of joined data | mn_map(joined, fill = value) |
labels = "en" adds readable label columns
alongside the codes. Periods are the exception: NSO numbers them from
the latest ("0"), so mongolstats always returns them as
labels, and you should select them by label or with
nso_latest_periods().nso_table_meta() and nso_dim_values() to see
dimensions and codes before fetching."0"), regions
("1"–"4"), the capital ("5"), and
aimags. Filter to one level before summing or ranking, or you will count
deaths twice.mongolmaps::mn_join() on the Region code
column rather than matching names.mn_join() by
Region code → mn_map()