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.
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 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...)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:
The default type returns aggregated dose counts (1994–2019):
| 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) |
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.
The CPNI type provides coverage rates per municipality:
# vaccination coverage in Acre, 2019
ac_coverage <- sipni_data(year = 2019, type = "CPNI", uf = "AC")
ac_coverage| Variable | Description |
|---|---|
| ANO | Reference year |
| UF | UF code (IBGE 2 digits) |
| MUNIC | Municipality code (IBGE 6 digits) |
| IMUNO | Immunobiological code |
| QT_DOSE | Number of doses applied |
| POP | Target population |
| COBERT | Vaccination coverage (%) |
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 |
| 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 |
# 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")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.
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.
# 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)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:
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):
lazy = TRUE and
aggregate before collect().source = "datasus" for 2020+, only the
current year’s CSVs exist at the source, and each month is a ~1.4 GB
national download.Downloaded data is cached locally for faster future access:
dadosabertos.saude.gov.br)
Need a high-speed mirror for your open-source project?
Contact our mirror admin team at info@clientvps.com.
This archive is provided as a free public service to the community.
Proudly supported by infrastructure from VPSPulse , RxServers , BuyNumber , UnitVPS , OffshoreName and secure payment technology by ArionPay.