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.

Ecossistema de dados do SUS com R

Ementa

  • Como estimar o uso de memória dos objetos no R?
  • fread() ainda é útil?
  • Um tibble com 100 milhões de registros cabe na RAM?
  • Como processar vários DBC sem manter todos simultaneamente na memória?
  • Como converter uma coleção mensal para Parquet?
  • Como consultar arquivos com Arrow ou DuckDB sem coletar tudo?
  • Como visualizar amostras no RStudio?

Objetivo da aula: adaptar a ETL da Parte 1 para volumes que não devem ser mantidos integralmente na memória do R.


Preparar os pacotes

Execute este bloco uma vez para instalar os pacotes usados nesta parte do módulo.

install.packages(c(
  "arrow", "data.table", "DBI", "dplyr",
  "duckdb", "lobstr", "remotes"
))

# O read.dbc pode não estar disponível no CRAN para sua versão do R
remotes::install_github("danicat/read.dbc", upgrade = "never")

Memória e volume

O que ocupa memória

Quando read.dbc() lê um arquivo, o resultado é materializado como um data.frame. O tamanho depende de:

  • quantidade de linhas;
  • quantidade de colunas;
  • tipos das colunas;
  • comprimento e repetição dos textos;
  • objetos intermediários produzidos pelas transformações;
  • cópias temporárias criadas durante seleções, junções e ordenações.

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.

Medir um objeto

library(lobstr)

# pa foi produzido na Parte 1.
obj_size(pa)

Com R base:

# Exibe uma estimativa do tamanho do objeto.
format(
  object.size(pa),
  units = "GB"
)

Observar a memória da sessão

# Mostra o uso do gerenciador de memória.
gc()

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.


O tidyverse e 100 milhões de registros

Resposta curta

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:

  • Arrow;
  • DuckDB;
  • PostgreSQL por meio de dbplyr;
  • outros bancos compatíveis com 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().

Comparação das estratégias

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 dplyr nã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:

  • leitura multithread;
  • detecção automática de separador e tipos;
  • seleção de colunas durante a leitura;
  • definição de tipos com colClasses;
  • inspeção da estrutura com nrows = 0;
  • preservação de zeros à esquerda com 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.

Inspecionar sem carregar todas as linhas

library(data.table)

# Lê apenas nomes e tipos inferidos.
estrutura <- fread(
  "pa_2024.csv",
  nrows = 0
)

estrutura

Ler somente as colunas necessárias

library(data.table)

pa_dt <- fread(
  "pa_2024.csv",
  select = c(
    "PA_CODUNI",
    "PA_CMP",
    "PA_PROC_ID",
    "PA_QTDAPR",
    "PA_VALAPR"
  ),
  colClasses = list(
    character = c(
      "PA_CODUNI",
      "PA_CMP",
      "PA_PROC_ID"
    )
  ),
  keepLeadingZeros = TRUE
)

Atualizar por referência

Uma diferença importante do data.table é a atualização por referência com :=, que reduz cópias desnecessárias.

library(data.table)

# Cria a métrica dentro do mesmo objeto.
pa_dt[
  PA_QTDAPR > 0,
  valor_por_unidade := PA_VALAPR / PA_QTDAPR
]

Processar os DBC um por vez

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.

Localizar os PA já baixados

arquivos_pa <- list.files(
  "dados_modulo_2",
  pattern = "^PA.*\\.dbc$",
  full.names = TRUE,
  ignore.case = TRUE
)

arquivos_pa
Mostrar resultado
## [1] "dados_modulo_2/PAAC2409.dbc" "dados_modulo_2/PAAC2410.dbc"
## [3] "dados_modulo_2/PAAC2411.dbc" "dados_modulo_2/PAAC2501.dbc"

Converter cada PA para Parquet

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.


Consultar com Arrow

Abrir a coleção sem coletar

library(arrow)
library(dplyr)

pa_dataset <- open_dataset(
  "dados_modulo_2/parquet_pa",
  format = "parquet"
)

pa_dataset

open_dataset() cria uma representação da coleção de arquivos. Os registros não são convertidos imediatamente em um tibble local.

Filtrar, selecionar e resumir

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_arrow

O collect() aparece somente depois da agregação. Assim, a RAM recebe três linhas resumidas, e não toda a coleção.

Obter uma amostra pequena

library(arrow)
library(dplyr)

amostra_pa <- pa_dataset |>
  select(
    PA_CMP,
    PA_CODUNI,
    PA_PROC_ID,
    PA_QTDAPR,
    PA_VALAPR
  ) |>
  head(1000) |>
  collect()

Consultar com DuckDB

DuckDB executa SQL analítico local e pode consultar arquivos Parquet sem importá-los integralmente para um data frame do R.

Criar uma conexão local

library(DBI)
library(duckdb)

con <- dbConnect(
  duckdb(),
  dbdir = "dados_modulo_2/sus.duckdb"
)

# Limita a memória usada pelo DuckDB.
dbExecute(
  con,
  "SET memory_limit = '4GB'"
)

Consultar Parquet com dplyr

library(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_duckdb

Encerrar a conexão

library(DBI)
library(duckdb)

dbDisconnect(
  con,
  shutdown = TRUE
)

Visualizar no RStudio

O 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:

# Evite abrir o objeto integral.
View(pa)

Visualize uma amostra:

# Abre somente mil linhas já coletadas.
View(amostra_pa)

Para exploração inicial, prefira:

library(dplyr)

glimpse(amostra_pa)
head(amostra_pa, 20)
summary(amostra_pa)

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.


Estratégia recomendada por escala

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()

Regras práticas

  • selecione somente as colunas necessárias;
  • preserve identificadores como texto;
  • processe uma partição por vez;
  • remova objetos intermediários;
  • evite ordenar uma base inteira sem necessidade;
  • agregue antes de trazer os resultados para a RAM;
  • não use View() como método de validação de bases gigantes;
  • mantenha DBC como fonte bruta e Parquet ou banco como camada analítica;
  • registre arquivo, competência, layout e transformação aplicada.

Síntese

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

Tabela de Diretrizes de Cuidado (PCDT)

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.

Consultando a tabela de diretrizes

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")
Mostrar resultado
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

Estudo de Caso 1: Artrite Reumatoide em São Paulo (2023-2024)

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):

  1. Definição de Parâmetros: Configuramos o diretório /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).
  2. Download e Leitura: Baixamos individualmente os arquivos PA (Produção) e AM (Medicamentos) do mês.
  3. Enriquecimento: Cruzamos o número da autorização (APAC) para integrar a coluna AP_CNSPCN (CNS do paciente) do arquivo AM no arquivo PA.
  4. Filtragem e Limpeza: Filtramos apenas os registros que apresentam os CIDs de Artrite Reumatoide em 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().
  5. Exportação: Salvamos a base consolidada e reduzida em formato .csv.gz.

Script ETL para Artrite Reumatoide (SP)

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")
Mostrar resultado
## Processamento concluído. Total de registros obtidos: 0

Leitura analítica do arquivo compactado sem sobrecarga de RAM

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)
}

Síntese do Pipeline de Extração e Validação

O pipeline consolidado nesta aula resolve o desafio de extrair dados do SUS em escala nacional utilizando computadores normais:

  1. Mapeamento Prévio de Metadados: Identificação dos CIDs e procedimentos no PCDT antes do download para evitar o carregamento de dados desnecessários.
  2. Gerenciamento Transparente de Armazenamento: Uso de parâmetros booleanos (manter_dbc) e diretórios locais estruturados (/media/ferre/...) para controlar a retenção de arquivos brutos.
  3. Fracionamento Estruturado (Staging Iterativo): Processamento em laços UF -> Ano -> Mês, limitando o pico de consumo de RAM ao tamanho de um único arquivo mensal.
  4. Enriquecimento On-the-fly: Junção entre subsistemas (SIA/PA com SIA/AM) realizada individualmente por competência antes da filtragem.
  5. Redução Agressiva e Limpeza: Filtro imedioto por diagnósticos de interesse seguido da remoção de objetos da RAM (rm(), gc()) e exclusão de arquivos no disco (file.remove()).
  6. Persistência Analítica Eficiente: Salvamento em formatos compactados e leitura preguiçosa (arrow::open_dataset), permitindo consultas exploratórias ágeis sem reidratar gigabytes na RAM.