Objetivo do curso: Capacitar participantes a extrair, validar, tratar e analisar microdados do SUS com R, integrando fontes de custos/preços e técnicas aplicadas à avaliação econômica.
fread() ainda é útil?Objetivo da aula: adaptar a ETL da Parte 1 para volumes que não devem ser mantidos integralmente na memória do R.
Execute este bloco uma vez para instalar os pacotes usados nesta parte do módulo.
Quando read.dbc() lê um arquivo, o resultado é
materializado como um data.frame. O tamanho depende de:
O número de linhas isoladamente não determina o consumo. Cem milhões de linhas com três inteiros são diferentes de cem milhões de linhas com dezenas de campos de texto.
Com R base:
Para remover objetos que não serão mais utilizados:
# Remove partes intermediárias e solicita coleta de memória.
rm(partes_pa, partes_am, pa_exemplo)
gc()gc() não torna um objeto grande menor. Ele apenas
permite recuperar memória de objetos que deixaram de ser
referenciados.
Um tibble local é um objeto em memória. Portanto, dplyr
não garante que 100 milhões de linhas caibam na RAM. O resultado depende
do número e dos tipos das colunas, da memória disponível e das cópias
temporárias exigidas pela operação.
O tidyverse continua útil em grandes volumes quando a sintaxe
dplyr é executada sobre um backend preguiçoso, como:
dbplyr;DBI e
dbplyr.Nesses casos, filter(), select(),
group_by() e summarise() são traduzidos ou
executados pelo backend. Somente o resultado necessário deve ser trazido
para a RAM com collect().
| Estratégia | Dados principais ficam na RAM? | Uso recomendado |
|---|---|---|
data.frame ou tibble local |
Sim | recortes que cabem confortavelmente na memória |
data.table local |
Sim | leitura e transformação rápida de arquivos delimitados |
| Arrow Dataset | Não integralmente | coleção de arquivos Parquet e consultas por partição |
| DuckDB | Não integralmente | SQL analítico sobre arquivos ou banco local |
| PostgreSQL | Não integralmente | dados persistentes, multiusuário e rotinas institucionais |
A interface
dplyrnão define onde os dados são armazenados. Um pipeline pode operar sobre um tibble em RAM, um conjunto Arrow ou uma tabela PostgreSQL.
fread() ainda é utilizado?Sim. data.table::fread() continua sendo uma opção
relevante para arquivos regulares e delimitados, como CSV e TSV. Entre
seus recursos estão:
colClasses;nrows = 0;keepLeadingZeros = TRUE.fread() não lê diretamente o formato DBC. Ele se aplica
depois que os dados foram convertidos para um arquivo delimitado ou
quando a fonte já é CSV/TSV.
read.dbc() materializa um DBC inteiro. Para reduzir o
pico de memória, leia um arquivo mensal, selecione as colunas
necessárias, grave o resultado e libere o objeto antes do próximo
arquivo.
arquivos_pa <- list.files(
"dados_modulo_2",
pattern = "^PA.*\\.dbc$",
full.names = TRUE,
ignore.case = TRUE
)
arquivos_pa## [1] "dados_modulo_2/PAAC2409.dbc" "dados_modulo_2/PAAC2410.dbc"
## [3] "dados_modulo_2/PAAC2411.dbc" "dados_modulo_2/PAAC2501.dbc"
O formato Parquet armazena colunas de forma compacta e permite que mecanismos analíticos leiam apenas colunas e grupos de linhas necessários.
library(arrow)
library(dplyr)
library(read.dbc)
diretorio_parquet <- "dados_modulo_2/parquet_pa"
dir.create(
diretorio_parquet,
showWarnings = FALSE,
recursive = TRUE
)
for (arquivo in arquivos_pa) {
# Mantém somente as colunas da análise.
pa_mes <- read.dbc(arquivo) |>
transmute(
PA_CODUNI = as.character(PA_CODUNI),
PA_CMP = as.character(PA_CMP),
PA_PROC_ID = as.character(PA_PROC_ID),
PA_AUTORIZ = as.character(PA_AUTORIZ),
PA_QTDAPR = as.numeric(PA_QTDAPR),
PA_VALAPR = as.numeric(PA_VALAPR),
arquivo_origem = basename(arquivo)
)
saida <- file.path(
diretorio_parquet,
sub("\\.dbc$", ".parquet", basename(arquivo), ignore.case = TRUE)
)
write_parquet(
pa_mes,
saida
)
# Libera o mês antes da próxima leitura.
rm(pa_mes)
gc()
}Essa estratégia não elimina o custo de memória necessário para abrir um DBC, mas evita manter várias competências simultaneamente na RAM.
library(arrow)
library(dplyr)
pa_dataset <- open_dataset(
"dados_modulo_2/parquet_pa",
format = "parquet"
)
pa_datasetopen_dataset() cria uma representação da coleção de
arquivos. Os registros não são convertidos imediatamente em um tibble
local.
library(arrow)
library(dplyr)
resumo_arrow <- pa_dataset |>
filter(
PA_CMP >= "202409",
PA_CMP <= "202411"
) |>
group_by(PA_CMP) |>
summarise(
quantidade = sum(PA_QTDAPR, na.rm = TRUE),
valor = sum(PA_VALAPR, na.rm = TRUE)
) |>
collect()
resumo_arrowO collect() aparece somente depois da agregação. Assim,
a RAM recebe três linhas resumidas, e não toda a coleção.
DuckDB executa SQL analítico local e pode consultar arquivos Parquet sem importá-los integralmente para um data frame do R.
dplyrlibrary(DBI)
library(duckdb)
library(dplyr)
pa_lazy <- tbl(
con,
"read_parquet('dados_modulo_2/parquet_pa/*.parquet')"
)
resumo_duckdb <- pa_lazy |>
filter(
PA_CMP >= "202409",
PA_CMP <= "202411"
) |>
group_by(PA_CMP) |>
summarise(
quantidade = sum(PA_QTDAPR, na.rm = TRUE),
valor = sum(PA_VALAPR, na.rm = TRUE)
) |>
collect()
resumo_duckdbO Data Viewer virtualiza a exibição das linhas, mas o objeto local continua ocupando memória. Além disso, ordenar ou filtrar um data frame muito grande pode exigir uma varredura completa.
Evite:
Visualize uma amostra:
Para exploração inicial, prefira:
Para dados externos à RAM, filtre e agregue no Arrow, DuckDB ou
PostgreSQL; depois use collect() apenas no resultado
destinado à inspeção ou ao gráfico.
| Situação | Estratégia inicial |
|---|---|
| Um ou poucos DBC mensais | read.dbc() + dplyr |
| Vários DBC que ainda cabem na RAM | lista + bind_rows() |
| CSV grande | fread(select = ..., colClasses = ...) |
| Muitos DBC mensais | converter um por vez para Parquet |
| Coleção Parquet | Arrow Dataset ou DuckDB |
| Rotina institucional persistente | PostgreSQL, DuckDB persistente ou lakehouse |
| Resultado para gráfico ou tabela | agregar antes de collect() |
View() como método de validação de bases
gigantes;fread() continua adequado para arquivos delimitados, mas
não substitui read.dbc() na leitura do DBC. Um tibble local
com 100 milhões de linhas pode exceder a RAM, principalmente quando
contém muitas colunas de texto ou quando as transformações criam
cópias.
Para grandes volumes, mantenha a sintaxe familiar do
dplyr, mas altere o backend:
DBC mensal
→ seleção de colunas
→ Parquet
→ Arrow ou DuckDB
→ filtro e agregação preguiçosos
→ collect() somente do resultado
Para realizar avaliações econômicas voltadas a linhas de cuidado específicas, precisamos saber quais códigos CID-10 buscar nas bases de produção. O Ministério da Saúde, por meio da Conitec, publica os Protocolos Clínicos e Diretrizes Terapêuticas (PCDT).
Utilizamos uma tabela de metadados padronizada para mapear a doença aos seus respectivos CIDs.
O chunk abaixo lê o arquivo diretamente do repositório GitHub e exibe suas 20 primeiras linhas.
library(readr)
library(dplyr)
library(knitr)
# URL da tabela de metadados declarada como texto simples
url_tabela <- "https://raw.githubusercontent.com/LabXiabr/bd_geral/refs/heads/main/td_diretriz_cuidado.csv"
# Leitura do CSV diretamente da web
tabela_diretrizes <- read_csv(url_tabela, show_col_types = FALSE)
# Exibicao das 20 primeiras linhas
tabela_diretrizes |>
head(20) |>
kable(caption = "Primeiras 20 linhas da Tabela de Diretrizes de Cuidado")| co_diretriz_cuidado…1 | co_diretriz_cuidado…2 | co_cid | sg_pcdt | no_abreviado | no_diretriz_extenso | no_diretriz50 | sg_tipo | st_rara |
|---|---|---|---|---|---|---|---|---|
| 1 | Acidente Vascular Cerebral Isquêmico Agudo | I630,I631,I632,I633,I634,I635,I636,I638,I639,I650,I651,I652,I653,I658,I659,I660,I661,I662,I663,I664,I668,I669 | AVC | avc_isquem | Trombólise no Acidente Vascular Cerebral (AVC) Isquêmico Agudo | Acidente Vascular Cerebral Isquêmico Agudo | pcdt | 0 |
| 2 | Acne grave | L700,L701,L708 | ACN | acne | Isotretinoína no tratamento da acne grave | Acne grave | Protocolos | 0 |
| 3 | Acromegalia | E220 | ACRO | acromegali | Acromegalia | Acromegalia | pcdt | 1 |
| 4 | Adenocarcinoma de Próstata | C61,D75 | PROS | c_prostata | Adenocarcinoma de Próstata | Adenocarcinoma de Próstata | ddt | 0 |
| 5 | Anemia Hemolítica Autoimune | D590,D591,D460,D461,D467,D600,D608,D610,D611,D612,D613,D618,D70,Z948 | AHA | anemia_hem | Anemia Hemolítica Autoimune | Anemia Hemolítica Autoimune | pcdt | 1 |
| 6 | Anemia por Deficiência de Ferro | D500,D508 | FER | anemia_fer | Anemia por Deficiência de Ferro | Anemia por Deficiência de Ferro | pcdt | 0 |
| 7 | Angioedema | D841 | ANGI | angiodema | Angioedema associado à deficiência de C1 esterase | Angioedema | pcdt | 1 |
| 8 | Aorta Torácica Descendente | I712 | ATD | aorta_tora | Diretrizes Brasileiras para Utilização de Endoprótese em Aorta Torácica Descendente | Aorta Torácica Descendente | Diretrizes | 0 |
| 9 | Artrite Idiopática Juvenil | M080,M081,M082,M083,M084,M088,M089 | AIJ | artrite_ju | Artrite Idiopática Juvenil (AIJ) | Artrite Idiopática Juvenil | pcdt | 1 |
| 10 | Artrite Psoríaca | M070,M072,M073 | APSO | artrite_ps | Artrite Psoríaca | Artrite Psoríaca | pcdt | 0 |
| 11 | Artrite Reativa | M021,M023,M032,M036 | AREA | artrite_rea | Artrite Reativa | Artrite Reativa | pcdt | 0 |
| 12 | Artrite Reumatoide | M050,M051,M052,M053,M058,M060,M068 | AREU | artrite_reu | Artrite Reumatoide | NA | NA | 0 |
| 13 | Asma | J450,J451,J458,J45 | ASMA | asma | Asma | Asma | pcdt;csap | 0 |
| 14 | Atrofia Muscular Espinhal | G120,G121 | AME | atrofia | Atrofia Muscular Espinhal 5q Tipo 1 | Atrofia Muscular Espinhal | pcdt | 1 |
| 15 | COVID-19 | B342,U04,U049,U071,U072,U089,U099,U109,U129 | COV | covid_19 | Diretrizes Brasileiras para Tratamento Hospitalar do Paciente com COVID-19 | COVID-19 | Diretrizes | 0 |
| 16 | Carcinoma Diferenciado de Tireoide | C73 | CTIR | c_tireoide | Carcinoma Diferenciado de Tireoide | NA | NA | 0 |
| 17 | Carcinoma de Células Renais | C64 | CREN | c_renal | Carcinoma de Células Renais | Carcinoma de Células Renais | pcdt | 0 |
| 18 | Carcinoma de Mama | C500,C501,C502,C503,C504,C505,C506,C508,C509 | CMAM | c_mama | Carcinoma de Mama | Carcinoma de Mama | ddt | 0 |
| 19 | Carcinoma de estômago e esôfago | C150,C151,C152,C153,C154,C155,C158,C159,C160,C161,C162,C163,C164,C165,C166,C168,C169,C170,C171,C172,C173,C178,C179,C18,C180,C181,C182,C183,C184,C185,C186,C187,C188,C189,C19,C20,C268,C474,C481,C493 | CEES | c_estomago | Carcinoma de estômago e esôfago | NA | NA | 0 |
| 20 | Carcinoma hepatocelular | C22 | CHEP | c_hepatocel | Carcinoma hepatocelular | NA | NA | 0 |
Para tratar volumes massivos como os do estado de São Paulo sem estourar a memória RAM, aplicamos uma rotina iterativa de ETL (mês a mês):
/media/ferre/ferre500gb/Downloads/dbc/, o parâmetro
booleano manter_dbc (para apagar os arquivos brutos e
economizar espaço em disco) e a lista de CIDs para Artrite Reumatoide
(M050, M051, M052,
M053, M058, M060,
M068).AP_CNSPCN (CNS do paciente)
do arquivo AM no arquivo PA.PA_CIDPRI
ou PA_CIDSEC, descartando os registros irrelevantes.
Removemos os arquivos .dbc do disco (caso
manter_dbc = FALSE) e liberamos a RAM com
gc()..csv.gz.library(curl)
library(read.dbc)
library(dplyr)
library(readr)
# Baixa apenas se o arquivo ainda nao estiver no disco
baixar_se_ausente <- function(url, destino) {
if (file.exists(destino) && file.size(destino) > 0) return(TRUE)
ok <- try(curl_download(url, destino, quiet = TRUE), silent = TRUE)
if (inherits(ok, "try-error")) {
if (file.exists(destino)) file.remove(destino) # remove download parcial
return(FALSE)
}
TRUE
}
# 1. Definicao de Parametros
dir_dbc <- "/media/ferre/ferre500gb/Downloads/dbc/"
manter_dbc <- TRUE # Define se os arquivos .dbc baixados permanecem no disco
url_ftp <- "ftp://ftp.datasus.gov.br/dissemin/publicos/SIASUS/200801_/Dados/"
uf_alvo <- "SP"
anos_alvo <- c(23, 24)
meses_alvo <- 1:12
cids_artrite <- c("M050", "M051", "M052", "M053", "M058", "M060", "M068")
# Criar o diretorio local caso nao exista
dir.create(dir_dbc, showWarnings = FALSE, recursive = TRUE)
# Objeto acumulador em memoria para o recorte filtrado
dados_artrite_sp <- data.frame()
# 2. Execucao da ETL Iterativa
for (ano in anos_alvo) {
for (mes in meses_alvo) {
arq_pa <- sprintf("PA%s%02d%02d.dbc", uf_alvo, ano, mes)
arq_am <- sprintf("AM%s%02d%02d.dbc", uf_alvo, ano, mes)
dest_pa <- file.path(dir_dbc, arq_pa)
dest_am <- file.path(dir_dbc, arq_am)
# Downloads protegidos contra falhas de conexao
down_pa <- baixar_se_ausente(paste0(url_ftp, arq_pa), dest_pa)
down_am <- baixar_se_ausente(paste0(url_ftp, arq_am), dest_am)
if (!inherits(down_pa, "try-error") && file.exists(dest_pa)) {
# Leitura e tipificacao do PA
df_pa <- read.dbc(dest_pa) |>
mutate(
PA_CMP = as.character(PA_CMP),
PA_AUTORIZ = as.character(PA_AUTORIZ),
PA_CIDPRI = as.character(PA_CIDPRI),
PA_CIDSEC = as.character(PA_CIDSEC)
)
# Leitura e cruzamento com o AM (se disponivel no mes)
if (!inherits(down_am, "try-error") && file.exists(dest_am)) {
df_am <- read.dbc(dest_am) |>
transmute(
PA_CMP = as.character(AP_CMP),
PA_AUTORIZ = as.character(AP_AUTORIZ),
AP_CNSPCN = as.character(AP_CNSPCN)
) |>
distinct()
df_pa <- left_join(df_pa, df_am, by = c("PA_CMP", "PA_AUTORIZ"))
} else {
df_pa$AP_CNSPCN <- NA_character_
}
# Filtragem agressiva por CID principal ou secundario
df_pa_filtrado <- df_pa |>
filter(
substr(PA_CIDPRI, 1, 4) %in% cids_artrite |
substr(PA_CIDSEC, 1, 4) %in% cids_artrite
)
# Acumula o recorte filtrado
if (nrow(df_pa_filtrado) > 0) {
dados_artrite_sp <- bind_rows(dados_artrite_sp, df_pa_filtrado)
}
# Limpeza de objetos da memoria RAM
rm(df_pa, df_pa_filtrado)
if (exists("df_am")) rm(df_am)
gc()
}
# Exclusao dos arquivos DBC do disco para economizar espaco
if (!manter_dbc) {
if (file.exists(dest_pa)) file.remove(dest_pa)
if (file.exists(dest_am)) file.remove(dest_am)
}
}
}
# Exportacao consolidada e comprimida
arquivo_saida_sp <- file.path(dir_dbc, "artrite_reumatoide_sp_23_24.csv.gz")
write_csv(dados_artrite_sp, arquivo_saida_sp)
cat("Processamento concluído. Total de registros obtidos:", nrow(dados_artrite_sp), "\n")## Processamento concluído. Total de registros obtidos: 0
Após gerar o arquivo .csv.gz, podemos inspecioná-lo
utilizando o pacote arrow, que acessa os dados diretamente
do disco sem carregá-los integralmente na RAM.
library(arrow)
library(dplyr)
arquivo_saida_sp <- file.path("/media/ferre/ferre500gb/Downloads/dbc/", "artrite_reumatoide_sp_23_24.csv.gz")
if (file.exists(arquivo_saida_sp)) {
# Abertura preguiçosa (lazy execution)
dataset_artrite <- open_dataset(arquivo_saida_sp, format = "csv")
# Selecao e coleta de apenas 20 linhas para exibicao
amostra_sp <- dataset_artrite |>
select(PA_CMP, PA_AUTORIZ, AP_CNSPCN, PA_CIDPRI, PA_PROC_ID, PA_QTDAPR, PA_VALAPR) |>
head(20) |>
collect()
print(amostra_sp)
}O pipeline consolidado nesta aula resolve o desafio de extrair dados do SUS em escala nacional utilizando computadores normais:
manter_dbc) e diretórios locais
estruturados (/media/ferre/...) para controlar a retenção
de arquivos brutos.UF -> Ano -> Mês, limitando o
pico de consumo de RAM ao tamanho de um único arquivo mensal.rm(), gc()) e exclusão de arquivos no disco
(file.remove()).arrow::open_dataset), permitindo consultas exploratórias
ágeis sem reidratar gigabytes na RAM.