Overview
The SI-PNI (Sistema de Informacao do Programa Nacional de Imunizacoes) is Brazil’s national immunization information system, managed by the Ministry of Health. It tracks vaccination doses applied and coverage rates across the country.
SI-PNI data come in two eras:
| Era | Years | Data type | Granularity |
|---|---|---|---|
| Aggregated | 1994–2019 | Dose counts (DPNI) and coverage (CPNI) | Annual per UF |
| Microdata | 2020+ | Individual-level (one row per dose) | Monthly |
sipni_data() automatically routes to the correct era
based on the requested year.
Data sources: the R2 mirror and DATASUS
By default, healthbR reads SI-PNI data from the healthbr-data
mirror: hive-partitioned Parquet on Cloudflare R2 (free egress), with
values byte-identical to the Ministry’s files and full
provenance metadata (source URL, hash, download date, pipeline version)
embedded in every file. If the mirror is unreachable,
sipni_data() falls back automatically to the official
DATASUS/OpenDataSUS sources.
# default: R2 mirror with automatic DATASUS fallback
sipni_data(year = 2024, uf = "AC", month = 1)
# pin a single source (no fallback)
sipni_data(year = 2019, uf = "AC", source = "datasus")
# invert the priority (DATASUS first, R2 as fallback)
sipni_data(year = 2019, uf = "AC", source = c("datasus", "r2"))Why the mirror is the default:
- The Ministry decommissioned the old OpenDataSUS host in 2026 and removed the 2020–2025 microdata files from the new one; only the mirror holds the complete 2020+ series.
- Only the requested UF/month partitions are transferred (no ~1.4 GB national CSV download to extract one UF).
- Aggregates read in seconds (the DATASUS FTP is slow and unstable for large UFs).
The result records where the data actually came from:
data <- sipni_data(year = 2024, uf = "AC", month = 1)
attr(data, "healthbr_source")
#> microdata
#> "r2"
attr(data, "healthbr_provenance")
#> # A tibble: 1 x 7 (partition, processing timestamp, Ministry source URL...)Checking availability with sipni_status()
The mirror publishes a manifest.json per dataset
recording, for every partition, the Ministry source file, its hash, the
processing timestamp and record counts. sipni_status()
reads it:
# everything the mirror holds
sipni_status()
# which 2026 microdata months are published so far?
sipni_status("microdados") |>
filter(year == 2026)Check available years
sipni_years()
#> [1] 1994 1995 ... 2025 2026Aggregated data: doses applied (DPNI)
The default type returns aggregated dose counts (1994–2019):
# doses applied in Acre, 2019
ac_doses <- sipni_data(year = 2019, uf = "AC")
ac_dosesKey variables (DPNI)
| Variable | Description |
|---|---|
| ANO | Reference year |
| UF | UF code (IBGE 2 digits) |
| MUNIC | Municipality code (IBGE 6 digits) |
| IMUNO | Immunobiological code |
| DOSE | Dose type (1st, 2nd, booster, etc.) |
| QT_DOSE | Number of doses applied |
| FX_ETARIA | Age group (coded) |
Using the dictionary
By default the dictionary is read from the mirror — the full versions converted from the Ministry’s original .cnv/.dbf files.
Important — decoding data requires
lookup = TRUE. In the .cnv files,
code is a sequential category code, and the value(s)
actually found in the data columns live in source_codes
(possibly several per label, reflecting code changes over the years —
e.g. data codes 08 and 82
both mean Hepatite B). Joining data against code silently
produces wrong labels. lookup = TRUE expands the dictionary
into one row per data code, ready to join:
# published form (code = .cnv category, source_codes = data codes)
sipni_dictionary("IMUNO")
# join-ready lookup: one row per data code
sipni_dictionary("IMUNO", lookup = TRUE)
# dose types and age groups
sipni_dictionary("DOSE", lookup = TRUE)
sipni_dictionary("FX_ETARIA", lookup = TRUE)
# the built-in offline copy (already in data-code form)
sipni_dictionary("IMUNO", source = "datasus")When more than one .cnv category claims the same data code, the more
specific claim wins: explicitly listed codes take precedence over codes
that only fall inside a range. This matters for residual catch-all
categories: FX_ETARIA’s “Idade ignorada” spans codes 00-99,
but it only labels codes that no specific age group claimed
explicitly.
Aggregated data: vaccination coverage (CPNI)
The CPNI type provides coverage rates per municipality:
# vaccination coverage in Acre, 2019
ac_coverage <- sipni_data(year = 2019, type = "CPNI", uf = "AC")
ac_coverageMicrodata (2020+)
For years 2020 and later, SI-PNI provides individual-level microdata
(one row per vaccination dose). The type parameter is
ignored for these years:
# microdata for Acre, January 2024
ac_micro <- sipni_data(year = 2024, uf = "AC", month = 1)
ac_microColumn names differ by source. The mirror publishes the Ministry’s JSON exports (56 fields, no CSV serialization artifacts); the OpenDataSUS CSVs use different names (~47 fields). Each source returns its columns exactly as published — healthbR does not rename or remap them:
| R2 mirror (default) | DATASUS CSV |
|---|---|
dt_vacina |
data_vacina |
ds_vacina |
descricao_vacina |
tp_sexo_paciente |
tipo_sexo_paciente |
nu_idade_paciente |
numero_idade_paciente |
sg_uf_estabelecimento |
sigla_uf_estabelecimento |
Key variables (R2 microdata)
| Variable | Description |
|---|---|
| sg_uf_estabelecimento | UF of the health facility |
| co_municipio_estabelecimento | Municipality (IBGE) |
| tp_sexo_paciente | Sex (M/F) |
| nu_idade_paciente | Patient age |
| no_raca_cor_paciente | Race/color (descriptive) |
| ds_vacina | Vaccine name |
| ds_dose_vacina | Dose description |
| dt_vacina | Vaccination date |
Exploring variables
# DPNI variables
sipni_variables()
# CPNI variables
sipni_variables(type = "CPNI")
# microdata variables, R2 mirror (default; 56 fields)
sipni_variables(type = "API")
# microdata variables, OpenDataSUS CSV (~47 fields)
sipni_variables(type = "API", source = "datasus")
# search
sipni_variables(search = "dose")Month parameter for microdata
Each month is a separate partition. Use month to select
specific months:
# single month
jan <- sipni_data(year = 2024, uf = "AC", month = 1)
# first quarter
q1 <- sipni_data(year = 2024, uf = "AC", month = 1:3)
# all 12 months (default)
full_year <- sipni_data(year = 2024, uf = "AC")For aggregated data (1994–2019), the month parameter is
ignored because the files are annual.
Example: vaccine doses by immunobiological
ac_2019 <- sipni_data(year = 2019, uf = "AC")
# decode immunobiological names: lookup = TRUE gives data-code rows
imuno_labels <- sipni_dictionary("IMUNO", lookup = TRUE) |>
select(code, label)
doses_by_vaccine <- ac_2019 |>
group_by(IMUNO) |>
summarize(total_doses = sum(as.integer(QT_DOSE), na.rm = TRUE),
.groups = "drop") |>
left_join(imuno_labels, by = c("IMUNO" = "code")) |>
arrange(desc(total_doses))
doses_by_vaccineNote: the 2019 aggregated data are drastically incomplete at the source (the Ministry was migrating to the new SI-PNI that year); totals are a fraction of 2018’s. This is faithful to what DATASUS publishes.
Example: coverage trends over time
# coverage data for Sao Paulo, 2015-2019
sp_cov <- sipni_data(
year = 2015:2019,
type = "CPNI",
uf = "SP"
)
# average coverage by year
sp_cov |>
group_by(year) |>
summarize(
mean_coverage = mean(as.numeric(COBERT), na.rm = TRUE),
.groups = "drop"
)Example: individual-level analysis (2020+)
# vaccinations in Acre, January 2024
ac_jan <- sipni_data(year = 2024, uf = "AC", month = 1)
# vaccines administered
ac_jan |>
count(ds_vacina, sort = TRUE)
# doses by sex
ac_jan |>
count(tp_sexo_paciente)
# age distribution
ac_jan |>
mutate(age = as.integer(nu_idade_paciente)) |>
filter(!is.na(age)) |>
mutate(age_group = cut(age,
breaks = c(0, 5, 12, 18, 30, 60, Inf),
right = FALSE)) |>
count(age_group)Mixed year requests
When requesting years that span both eras (e.g., 2019 and 2024),
sipni_data() fetches each era and combines the results.
Note that column names and structure differ between eras:
# aggregated (2019) + microdata (2024)
mixed <- sipni_data(year = c(2019, 2024), uf = "AC", month = 1)
# aggregated (UPPERCASE) and microdata columns are combined
# with NAs where columns don't overlap
names(mixed)Lazy evaluation over the mirror
With lazy = TRUE and the default source,
sipni_data() returns the remote arrow
dataset — dplyr verbs are pushed down to R2 and only the partitions your
query touches are transferred. In this mode the partition columns keep
the bucket layout names (ano, mes,
uf, as strings):
ds <- sipni_data(year = 2019, uf = "AC", lazy = TRUE)
ds |>
filter(IMUNO == "02") |> # data code 02 = BCG (see sipni_dictionary)
select(MUNIC, DOSE, QT_DOSE) |>
collect()Download tips
- Aggregates (1994–2019) read from the mirror in seconds.
- Microdata months for large UFs (SP, MG) hold millions of rows —
start with a single month and UF, or use
lazy = TRUEand aggregate beforecollect(). - Results are cached locally per partition: a second request for the same year/month/UF is served from disk.
- If you pin
source = "datasus"for 2020+, only the current year’s CSVs exist at the source, and each month is a ~1.4 GB national download.
Smart type parsing
# parsed types (default)
ac <- sipni_data(year = 2019, uf = "AC")
class(ac$QT_DOSE) # integer
# raw character columns, exactly as published
ac_raw <- sipni_data(year = 2019, uf = "AC", parse = FALSE)Cache management
Downloaded data is cached locally for faster future access:
# check cache status
sipni_cache_status()
# clear cache if needed
sipni_clear_cache()Additional resources
- healthbr-data — the mirror’s pipelines, reproducibility policy and dataset documentation
- OpenDataSUS (
dadosabertos.saude.gov.br) - Census vignette for population denominators