healthbR or microdatasus? The two R packages for DATASUS, side by side on the same download
SIM, SINASC and SIH read by both packages: what each returns, how long it takes, where codes become labels — and one trap on each side
Published 5 September 2026. Every number here was measured that day, in one R session, with healthbR 0.3.1 (development version), microdatasus 3.0.0, arrow 25.0.0, dplyr 1.2.1 and R 4.6.1 on Windows, over a residential connection in Brazil. The code of each measurement is pasted here and in the package’s vignette; nothing is illustrative.
If you read Brazilian public-health microdata in R — deaths from SIM, live births from SINASC, hospital admissions from SIH, all published by DATASUS, the Ministry of Health’s informatics department — you have two packages to choose from. microdatasus, by Raphael Saldanha, described in Saldanha et al. (2019), is the usual answer to “how do I get DATASUS into R”. healthbR is mine. They overlap on six DATASUS systems and differ in almost everything they do with the bytes after the download.
This post puts the two side by side on the same download, three times: admissions in Roraima for billing competence 2023-01, deaths in Acre in 2022, births in Acre in 2022. I show what each returns, how long it takes, and where the choice changes what a script computes. The point is not to win a comparison; it is that whoever reaches DATASUS through R knows what they are choosing.
Coverage
| healthbR | microdatasus 3.0.0 | |
|---|---|---|
| SIM (mortality) |
sim_data(): DO, 1996–2024 |
DO, DOFET, DOEXT, DOINF, DOMAT |
| SINASC (live births) |
sinasc_data(): 1996–2024 |
SINASC |
| SIH (admissions) |
sih_data(): RD (reduced AIH), 2008–2026 |
RD, RJ, SP, ER |
| SIA (outpatient) | 13 file types, 2008–2026 | 12 file types |
| CNES (facilities) | 13 file types, 2005–2026 | 13 file types |
| SINAN (notifiable diseases) | 31 diseases, 2007–2026 | 8 diseases |
| SI-PNI (vaccination) | 1994–2026 | — |
| SISAB (primary care) | yes | — |
| Surveys | VIGITEL, PNS, PNAD Contínua, POF, Censo denominators | — |
| Agencies | ANS, ANVISA | — |
| Auxiliary tables |
*_dictionary(), *_variables() per module |
SIGTAP, CADGER, tabMun, tabCBO, tabOcupacao, tabNaturalidade
|
microdatasus goes deeper into SIM and SIH (the fetal, external-cause, infant and maternal SIM extracts; the RJ, SP and ER SIH files) and ships the SIGTAP procedure table. healthbR goes wider: more SINAN diseases, SI-PNI, SISAB, the household surveys and the agencies, under one *_years() / *_data() / *_dictionary() API.
Same reader, two philosophies
Both decompress the Ministry’s .dbc files (DBF compressed with PKWare DCL) with the same C code: microdatasus 3.0.0 dropped its read.dbc dependency and adopted healthbR’s vendored implementation — it is in its NEWS, with thanks. The bytes are identical; the difference starts right after.
healthbR keeps the codes and types the columns. SEXO stays 1/3, MORTE stays 0/1, DT_INTER becomes a Date, VAL_TOT a double, IDADE an integer. Labels live in sih_dictionary(), sim_dictionary(), sinasc_dictionary(), joined when you want them.
microdatasus separates fetching from processing. fetch_datasus() returns every column as character, as the DBF has it. process_sih(), process_sim(), process_sinasc() replace codes with labels (SEXO becomes "Masculino"/"Feminino", MORTE becomes "Não"/"Sim"), convert dates, split the SIM age field into one column per unit and, with municipality_data = TRUE, join municipality names and coordinates. The result is a tibble of character columns.
Neither is wrong. Labels are what you want in a report table; codes are what you want to join, compare across years, or model. The trap is mixing them: after process_sih(), MORTE == 1 is FALSE on every row.
One download in both: SIH, Roraima, competence 2023-01
Roraima is the smallest SIH file; January 2023 has 4,734 admissions. Both reads start from an empty cache.
library(healthbR)
library(microdatasus)
library(dplyr)
rr_h <- sih_data(year = 2023, month = 1, uf = "RR") # healthbR
#> ℹ Reading SIH data from R2: 2023/01 (1 UF(s))...
dim(rr_h)
#> [1] 4734 116
table(vapply(rr_h, function(x) class(x)[1], ""))
#> character Date integer numeric
#> 101 3 7 5
rr_m <- fetch_datasus(year_start = 2023, month_start = 1, # microdatasus
year_end = 2023, month_end = 1,
uf = "RR", information_system = "SIH-RD")
dim(rr_m)
#> [1] 4734 113
rr_mp <- process_sih(rr_m)
dim(rr_mp)
#> [1] 4734 121Same 4,734 rows, identical sets of N_AIH. healthbR adds year, month and uf_source (the competence and state of the file, which the AIH record does not carry); process_sih() adds eight municipality columns.
The same five records, three ways:
cols <- c("N_AIH", "SEXO", "COD_IDADE", "IDADE", "MORTE", "DT_INTER",
"DIAG_PRINC", "MUNIC_RES", "VAL_TOT")
as.data.frame(head(rr_h[cols], 5)) # healthbR
#> N_AIH SEXO COD_IDADE IDADE MORTE DT_INTER DIAG_PRINC MUNIC_RES VAL_TOT
#> 1 1423100411392 3 4 18 0 2022-12-13 O809 140017 568.80
#> 2 1423100911661 1 4 83 0 2022-12-26 J189 140010 994.59
#> 3 1423100911672 3 4 65 0 2022-12-03 I219 140010 620.12
#> 4 1423100911683 1 4 58 0 2022-12-26 I248 140020 333.08
#> 5 1423100911694 1 4 35 0 2022-11-01 A499 140010 11202.59
as.data.frame(head(rr_m[cols], 5)) # microdatasus, raw
#> N_AIH SEXO COD_IDADE IDADE MORTE DT_INTER DIAG_PRINC MUNIC_RES VAL_TOT
#> 1 1423100411392 3 4 18 0 20221213 O809 140017 568.8
#> 2 1423100911661 1 4 83 0 20221226 J189 140010 994.59
#> 3 1423100911672 3 4 65 0 20221203 I219 140010 620.12
#> 4 1423100911683 1 4 58 0 20221226 I248 140020 333.08
#> 5 1423100911694 1 4 35 0 20221101 A499 140010 11202.59
as.data.frame(head(rr_mp[c(cols, "munResNome")], 5)) # microdatasus, processed
#> N_AIH SEXO COD_IDADE IDADE MORTE DT_INTER DIAG_PRINC MUNIC_RES VAL_TOT munResNome
#> 1 1423100411392 Feminino Anos 18 Não 2022-12-13 O809 140017 568.8 Cantá
#> 2 1423100911661 Masculino Anos 83 Não 2022-12-26 J189 140010 994.59 Boa Vista
#> 3 1423100911672 Feminino Anos 65 Não 2022-12-03 I219 140010 620.12 Boa Vista
#> 4 1423100911683 Masculino Anos 58 Não 2022-12-26 I248 140020 333.08 Caracaraí
#> 5 1423100911694 Masculino Anos 35 Não 2022-11-01 A499 140010 11202.59 Boa VistaNote DT_INTER: admissions billed in competence 2023-01 happened in November and December 2022. In both packages the year and month select the billing competence, not the admission date; the admissions of a calendar year keep arriving for a few competences after it (measured on the whole mirror: the four following competences close 99.7–99.9% of a year).
The trap, one line each:
sum(rr_h$MORTE == 1) # healthbR: integer code
#> [1] 113
sum(rr_m$MORTE == "1") # microdatasus raw: character code
#> [1] 113
sum(rr_mp$MORTE == 1) # microdatasus processed: the code is gone
#> [1] 0
table(rr_mp$MORTE)
#> Não Sim
#> 4621 113Same 113 deaths; the third line silently returns zero in a script written against codes. I learned it the hard way: an aggregate cube behind my own SIH MCP server sat for months with sex “unknown” on every row and zero deaths, because the build script compared codes after a process_sih(). No test complained — the label is exactly what prevents suspicion.
Timing
| Step | healthbR | microdatasus |
|---|---|---|
| Empty cache, mirror (manifest of 11,157 partitions + one Parquet) | 11.4 s | — |
Empty cache, DATASUS FTP .dbc (source = "datasus") |
1.1 s | 2.7 s |
| Same call again in the session | 0.5 s (local Parquet cache) | downloads again (no cache) |
Labels (process_sih()) |
— | 0.8 s |
| In-memory size | 4.8 MB | 5.4 MB raw, 5.7 MB processed |
On a 349 KB file the FTP wins the first read: the mirror’s price of entry is its 10 MB manifest, downloaded once and revalidated by ETag afterwards (sih_status() reads it in 0.4 s from then on). The mirror pays back on repeated reads, on lazy queries over many competences, and in what comes with the data.
A measurement confession: in the first round, healthbR’s “warm” read took 5.5 s — slower than the cold FTP. The cause was the package’s: building the manifest summary (11k partitions) cost 4.6 s and ran on every call. It was fixed before publishing (the summary is now memoised per manifest content) and the numbers above are from the fixed code. Measuring against the neighbour is a good way to find your own slow paths.
What comes with the data: provenance
From the mirror, the tibble carries the source it was served from and one row per DATASUS file behind it — URL, MD5 and size of the .dbc, record count, processing timestamp and pipeline version:
attr(rr_h, "healthbr_source")
#> [1] "r2"
attr(rr_h, "healthbr_provenance") |>
select(year, month, uf, records, source_hash_md5, source_size_bytes)
#> year month uf records source_hash_md5 source_size_bytes
#> 1 2023 1 RR 4734 12e74d4b059589ceb47e4136e3b2ce5f 349244
st <- sih_status()
nrow(st); attr(st, "last_updated"); range(st$year)
#> [1] 11157
#> [1] "2026-08-18T12:33:31"
#> [1] 1992 2026microdatasus 3.0.0 has track_source = TRUE, which appends the name of the .dbc each row came from; it records no hash or timestamp, and the FTP offers none. If a derived product must say exactly which files it was built from, this is the difference that matters.
Lazy: a year without downloading it
sih_data(year = 2023, uf = "RR", lazy = TRUE) |>
filter(MORTE == 1) |>
count(month) |>
collect() |>
arrange(month)
#> month n
#> 1 1 113
#> 2 2 120
#> 3 3 175
#> 4 4 166
#> 5 5 118
#> 6 6 78
#> 7 7 97
#> 8 8 144
#> 9 9 105
#> 10 10 104
#> 11 11 128
#> 12 12 10219.4 s for twelve competences over the mirror, no local copy. With microdatasus the equivalent downloads twelve .dbc files into memory and filters afterwards. For one small state it makes no difference; for the whole country (a year of SIH is about 2 GB of .dbc) it decides whether the query fits on a laptop.
SIM: deaths, Acre 2022
ac_h <- sim_data(year = 2022, uf = "AC") # 1.5 s
ac_m <- fetch_datasus(2022, year_end = 2022, uf = "AC",
information_system = "SIM-DO") # 2.1 s
ac_mp <- process_sim(ac_m) # 0.7 s
dim(ac_h); dim(ac_m); dim(ac_mp)
#> [1] 4159 90
#> [1] 4159 87
#> [1] 4159 100Same 4,159 deaths. The interesting column is age: SIM stores it in three characters, unit first (0 minutes, 1 hours, 2 days, 3 months, 4 years, 5 years over 100), then the value. The packages decode it differently:
# six infant deaths, the same rows in both
#> IDADE healthbR_age_years md_IDADEanos md_IDADEmeses md_IDADEdias md_IDADEhoras
#> 1 021 3.992699e-05 <NA> <NA> <NA> <NA>
#> 2 101 1.140771e-04 <NA> <NA> <NA> 1
#> 3 201 2.737851e-03 <NA> <NA> 1 <NA>
#> 4 302 1.666667e-01 <NA> 2 <NA> <NA>
#> 5 102 2.281542e-04 <NA> <NA> <NA> 2
#> 6 108 9.126169e-04 <NA> <NA> <NA> 8healthbR adds one continuous age_years (21 minutes is 0.00004 years, 2 months is 0.167), so age bands and rates come from one column. microdatasus splits the field into IDADEminutos, IDADEhoras, IDADEdias, IDADEmeses, IDADEanos, each NA outside its unit, closer to how the death certificate reads. For adult mortality they agree to the year; for infant mortality you reach for different columns.
SINASC: births, Acre 2022
ac_n <- sinasc_data(year = 2022, uf = "AC") # 3.1 s
class(ac_n$PESO); sum(is.na(ac_n$PESO)); median(ac_n$PESO, na.rm = TRUE)
#> [1] "integer"
#> [1] 94
#> [1] 3230
ac_nm <- fetch_datasus(2022, year_end = 2022, uf = "AC",
information_system = "SINASC") # 3.3 s
ac_nmp <- process_sinasc(ac_nm) # 3.5 s
sum(is.na(ac_nm$PESO)); sum(is.na(ac_nmp$PESO))
#> [1] 94
#> [1] 14483Same 14,483 births. One thing to know before relying on process_sinasc() in the versions measured here: it returned PESO (birth weight) as NA on every row, while the raw column had 94 missing out of 14,483. It reproduces offline with the package’s own sample:
sum(is.na(microdatasus::sinasc_sample$PESO))
#> [1] 2
sum(is.na(process_sinasc(microdatasus::sinasc_sample)$PESO))
#> [1] 100The sentinel recode ("0" and "9999" to NA) is followed by an as.numeric(), and with dplyr 1.2.1 the whole column comes back missing. It is the kind of thing a maintainer fixes in an afternoon once reported; check the package’s news and issues for your versions and, meanwhile, take PESO from the raw tibble. healthbR does not rewrite the column: PESO is parsed to integer and the sentinels are left for you to treat.
Where the bytes come from
| healthbR | microdatasus | |
|---|---|---|
| SIM, SINASC, SIA, SINAN, CNES | DATASUS FTP, .dbc, decompressed locally |
DATASUS FTP, .dbc, decompressed locally |
| SIH, SI-PNI | healthbr-data mirror: partitioned Parquet on Cloudflare R2, one partition per DATASUS file, byte-identical values, provenance per file; FTP as fallback | DATASUS FTP |
| Local cache | partitioned Parquet per module; a second call never touches the network | none |
| Re-issued files |
sih_status(): MD5 and size per partition, mirror last_updated
|
— |
| Lazy queries |
lazy = TRUE, arrow or duckdb, over the cache or the mirror |
— |
| Parallel downloads |
future::plan(multisession) + furrr |
sequential |
Other routes
Not measured here: PySUS, the Python package over the same FTP, and Base dos Dados, treated SIM and SINASC tables on BigQuery with an R client.
Which one
- Labels in the tibble, a quick table: microdatasus.
- Joins, comparisons across years, models: healthbR. Codes stay codes, types are parsed, dictionaries are functions, nothing is rewritten under you.
- SIH at scale, or repeatedly: healthbR. Local Parquet cache, lazy queries over the mirror, provenance you can cite.
- SIM fetal/infant/maternal extracts, RJ/SP/ER SIH files, SIGTAP: microdatasus, which has them and healthbR (yet) does not.
- SI-PNI, SISAB, surveys, ANS, ANVISA: healthbR.
They compose: fetch_datasus() and sih_data(parse = FALSE) return the same character columns for the same file, so you can read with one and label with the other:
sih_data(year = 2023, month = 1, uf = "RR", parse = FALSE) |>
process_sih()Reproducing
install.packages(c("microdatasus", "arrow"))
# install.packages("pak"); pak::pak("SidneyBissoli/healthbR") # dev version
sih_clear_cache(); sim_clear_cache(); sinasc_clear_cache()
system.time(sih_data(year = 2023, month = 1, uf = "RR"))
system.time(fetch_datasus(2023, 1, 2023, 1, uf = "RR",
information_system = "SIH-RD"))The same comparison, as a package vignette: healthbR vs microdatasus. healthbR is on CRAN (0.2.0) and GitHub (development version, with the SIH mirror). Thanks to Raphael Saldanha: microdatasus opened the road healthbR travels, and its adoption of healthbR’s .dbc reader in 3.0.0 is the kind of exchange that makes both better.