Um óbito no terceiro dia de vida e um óbito no oitavo mês entram na mesma estatística de mortalidade infantil — mas contam histórias muito diferentes. Separei as duas em Julia, estado por estado.
A taxa de mortalidade infantil — a proporção de nascidos vivos que morrem antes de completar um ano — é um dos indicadores mais usados para medir a qualidade de um sistema de saúde. Mas o número sozinho esconde histórias muito diferentes: um óbito no terceiro dia de vida costuma apontar para problemas de gestação, parto e cuidado neonatal; um óbito no oitavo mês costuma apontar para infecções, saneamento e acesso continuado a serviços de saúde.
Foi para separar essas duas histórias que fiz o gráfico acima em Julia, usando o pacote MicroSUS.jl para baixar e ler diretamente os microdados do SUS. Neste post, mostro o código completo e como interpretar o resultado.
1) Neonatal e pós-neonatal: por que separar
A literatura de saúde pública divide o primeiro ano de vida em duas janelas:
- Neonatal (0 a 27 dias completos): domina por causas ligadas à gestação e ao parto — prematuridade, malformações congênitas, asfixia, infecções adquiridas no parto. Reflete principalmente a qualidade do pré-natal e da assistência ao parto.
- Pós-neonatal (28 dias a 11 meses): domina por causas ligadas ao ambiente em que a criança vive depois de alta — pneumonia, diarreia, desnutrição, acidentes. Reflete mais saneamento, renda e acesso continuado a serviços de saúde.
Um estado pode ter mortalidade infantil total parecida com outro e, ainda assim, apontar para problemas completamente diferentes conforme essa proporção. É esse contraste que o gráfico tenta deixar visível.
2) As fontes: SIM e SINASC via MicroSUS.jl
Os dados vêm de dois sistemas do Ministério da Saúde:
- SIM (Sistema de Informações sobre Mortalidade): um registro por óbito, com a idade da criança ao morrer.
- SINASC (Sistema de Informações sobre Nascidos Vivos): um registro por nascimento vivo, usado como denominador.
Em vez de baixar manualmente os arquivos .dbc do DATASUS, usei o MicroSUS.jl, que expõe duas funções simples:
using MicroSUS
caminho = baixar(:sim, "SP"; ano = 2023) # baixa (e cacheia) o SIM de SP em 2023
tabela = ler(caminho; colunas = [:IDADE, :CODMUNRES])
O pacote cuida do cache local (por isso os logs [ Info: cache: ...DOAC2023.dbc, um por UF, no meu terminal) e devolve os dados como uma tabela particionada — nada precisa ser materializado inteiro em memória.
3) O detalhe que decide tudo: o campo IDADE do SIM
A parte mais delicada do código não é estatística, é decodificação de formato. No SIM, a idade ao óbito vem em um campo de 3 dígitos: o primeiro dígito é a unidade, os outros dois são o valor.
| 1º dígito | Unidade |
|---|---|
| 0 | minutos |
| 1 | horas |
| 2 | dias |
| 3 | meses |
| 4 | anos |
| 9 | ignorada |
Um óbito com "205" significa “5 dias” (neonatal); "302" significa “2 meses” (pós-neonatal). A fronteira dos 28 dias cai bem no meio da unidade “dias”, então a classificação precisa comparar o valor, não só a unidade:
function faixa_obito_infantil(idade::AbstractString)
t = strip(idade)
(length(t) != 3 || !all(isdigit, t)) && return :fora
u = t[1] - '0'
v = 10 * (t[2] - '0') + (t[3] - '0')
(u == 0 || u == 1) && return :neonatal # minutos, horas
u == 2 && return v < 28 ? :neonatal : :posneonatal
u == 3 && return v == 0 ? :neonatal : :posneonatal
return :fora
end
faixa_obito_infantil(::Missing) = :fora
Um caso ambíguo de propósito: "400" (zero anos completos, sem mais detalhe) é descartado — não dá para saber de que lado dos 28 dias o óbito cai, então incluí-lo enviesaria uma das duas faixas. Fica registrado na nota de rodapé do gráfico.
4) Agregando por UF de residência
Com a função de classificação pronta, o resto é contar. Um ponto importante: uso sempre CODMUNRES (município de residência), não o município de ocorrência do óbito — assim um parto complicado que termina num hospital de referência em outra cidade continua contando para a UF de origem da família.
function coletar(cfg::Config = Config())
neo = Dict{String,Int}(uf => 0 for uf in UFS)
pos = Dict{String,Int}(uf => 0 for uf in UFS)
nvs = Dict{String,Int}(uf => 0 for uf in UFS)
# óbitos infantis (SIM)
for uf in UFS
caminho = baixar(:sim, uf; ano = cfg.ano)
isnothing(caminho) && (@warn "SIM ausente" uf; continue)
tabela = ler(caminho; colunas = [:IDADE, :CODMUNRES],
schema = nothing, tamanho_lote = cfg.tamanho_lote)
for lote in Tables.partitions(tabela)
idades = Tables.getcolumn(lote, :IDADE)
munis = Tables.getcolumn(lote, :CODMUNRES)
for i in eachindex(idades)
(ismissing(idades[i]) || ismissing(munis[i])) && continue
faixa = faixa_obito_infantil(String(idades[i]))
faixa === :fora && continue
sigla = uf_de_codmun(String(munis[i]))
isnothing(sigla) && continue
faixa === :neonatal ? (neo[sigla] += 1) : (pos[sigla] += 1)
end
end
end
# nascidos vivos (SINASC)
for uf in UFS
caminho = baixar(:sinasc, uf; ano = cfg.ano)
isnothing(caminho) && (@warn "SINASC ausente" uf; continue)
tabela = ler(caminho; colunas = [:CODMUNRES],
schema = nothing, tamanho_lote = cfg.tamanho_lote)
for lote in Tables.partitions(tabela)
for m in Tables.getcolumn(lote, :CODMUNRES)
ismissing(m) && continue
sigla = uf_de_codmun(String(m))
isnothing(sigla) && continue
nvs[sigla] += 1
end
end
end
resultado = Dict{String,NamedTuple}()
for uf in UFS
nv = nvs[uf]
nv > cfg.minimo_nascimentos || continue
resultado[uf] = (neo = neo[uf], pos = pos[uf], nascimentos = nv,
taxa_neo = neo[uf] / nv, taxa_pos = pos[uf] / nv)
end
return resultado
end
Repare que peço explicitamente schema = nothing na leitura do SIM: isso faz o IDADE chegar como texto cru ("205", "302"…), em vez de já convertido para anos — a conversão automática do MicroSUS.jl perderia justamente a granularidade fina que a fronteira dos 28 dias exige.
5) A figura: barras empilhadas com CairoMakie
Para a visualização, escolhi barras horizontais empilhadas — uma linha por estado, ordenadas pela taxa total decrescente, com a fatia neonatal (lilás) e pós-neonatal (rosa) uma ao lado da outra em cada barra.
function grafico_mortalidade(dados::AbstractDict, cfg::Config = Config())
ufs = sort(collect(keys(dados));
by = uf -> -(dados[uf].taxa_neo + dados[uf].taxa_pos))
n = length(ufs)
nomes = [UF_NOME[uf] for uf in ufs]
tneo = [dados[uf].taxa_neo for uf in ufs]
tpos = [dados[uf].taxa_pos for uf in ufs]
ys = collect(n:-1:1) # decrescente: o primeiro da lista fica no topo
ax = Axis(fig[3, 1]; yticks = (ys, nomes)) # eixo com só os rótulos das UFs
barplot!(ax, ys, tneo; direction = :x, color = BAR_NEO, width = 0.72)
barplot!(ax, ys, tneo .+ tpos; direction = :x, fillto = tneo,
color = BAR_POS, width = 0.72)
# rótulos, cabeçalhos de coluna e rodapé omitidos por brevidade
end
Alguns detalhes que valeram a pena registrar como comentário no próprio script:
- Ordenação manual do eixo Y. Em vez de confiar em
yreversed, gero as posições já na ordem decrescente (n:-1:1) — mais previsível quando a figura também usalimits!explícito. - Coluna única no layout. Com duas colunas e
Labels emtellwidth = false, as larguras ficam indeterminadas e o texto vaza da figura. Uma coluna só, com quebras de linha manuais, resolve. - Formatação pt-BR sem zeros à direita. Uma função
pctpequena transforma0.0068em"0,68%"e0.0100em"1%", sem o zero decorativo. - Logo com fallback. A logo é baixada uma vez para o
tempdir(); se a rede falhar, a figura não quebra — só cai para uma assinatura em texto.
6) O resultado no terminal
Antes da figura, o script imprime o ranking bruto, útil para conferir os números que vão para o gráfico:
Saída esperada
Roraima neo 1.366% pós 1.023% (nv = 13105)
Amapá neo 1.189% pós 0.904% (nv = 12948)
Sergipe neo 1.193% pós 0.655% (nv = 29005)
Amazonas neo 1.011% pós 0.698% (nv = 70453)
Acre neo 1.002% pós 0.691% (nv = 14473)
Pará neo 1.009% pós 0.495% (nv = 126152)
Piauí neo 0.928% pós 0.567% (nv = 42144)
Maranhão neo 0.976% pós 0.508% (nv = 97205)
Bahia neo 1.056% pós 0.425% (nv = 170314)
Mato Grosso neo 0.924% pós 0.480% (nv = 58553)
Alagoas neo 0.885% pós 0.471% (nv = 46543)
Mato Grosso do Sul neo 0.825% pós 0.529% (nv = 40233)
Rio de Janeiro neo 0.898% pós 0.448% (nv = 176145)
Pernambuco neo 0.844% pós 0.477% (nv = 116175)
Paraíba neo 0.854% pós 0.442% (nv = 51531)
Tocantins neo 0.843% pós 0.428% (nv = 23145)
Goiás neo 0.842% pós 0.404% (nv = 91822)
Rondônia neo 0.727% pós 0.497% (nv = 23921)
Ceará neo 0.837% pós 0.335% (nv = 111091)
Espírito Santo neo 0.776% pós 0.383% (nv = 52187)
São Paulo neo 0.787% pós 0.349% (nv = 503910)
Minas Gerais neo 0.792% pós 0.336% (nv = 233918)
Rio Grande do Norte neo 0.751% pós 0.365% (nv = 39440)
Distrito Federal neo 0.776% pós 0.309% (nv = 35551)
Paraná neo 0.750% pós 0.330% (nv = 139836)
Rio Grande do Sul neo 0.682% pós 0.286% (nv = 120974)
Santa Catarina neo 0.629% pós 0.278% (nv = 96802)
7) Lendo o gráfico
Alguns padrões saltam aos olhos:
- O Norte e o Nordeste ocupam o topo em quase todas as posições. Roraima, Amapá, Sergipe, Amazonas e Acre têm as cinco maiores taxas totais — e são também os cinco únicos estados com pós-neonatal acima de 0,6%, a fatia mais associada a condições de vida e acesso continuado a serviços de saúde.
- A fatia neonatal domina em praticamente todo o país. Mesmo nos estados do Sul, com as menores taxas totais (Rio Grande do Sul e Santa Catarina fecham a lista), a parte neonatal segue maior que a pós-neonatal — sinal de que os ganhos dos últimos anos vieram mais de reduzir mortes evitáveis depois do primeiro mês do que de resolver a raiz perinatal.
- A proporção entre as duas fatias varia mais do que o total. Bahia (neo 1,06% / pós 0,43%) e Rio de Janeiro (neo 0,90% / pós 0,45%) têm pós-neonatal quase idêntico — a diferença na taxa total entre os dois vem quase inteiramente da fatia neonatal, sugerindo que o gargalo da Bahia está mais concentrado no início da vida do que no restante do primeiro ano.
- Distrito Federal quebra o padrão regional, com taxa total baixa (comparável a Paraná e Rio Grande do Norte) apesar de estar fora do Sul/Sudeste — reflexo, provavelmente, de renda e estrutura de saúde concentradas na capital federal.
8) Limitações
- Atribuição por UF de residência, não de ocorrência — o objetivo é medir o resultado para a família, não a carga do hospital.
- Óbitos com idade registrada só como
"0 anos"(sem indicar dias/meses) foram descartados — não é possível saber de que lado da fronteira dos 28 dias eles caem, e incluí-los arbitrariamente distorceria a comparação entre as duas fatias. - Um único ano (2023). Taxas de mortalidade infantil sofrem flutuação amostral, especialmente em UFs menores (Roraima, Amapá, Acre têm poucos nascimentos): comparações estado a estado ficam mais confiáveis olhando para uma série de anos, não um único corte.
9) Código-fonte
O script completo — configuração, cores, dicionários de UF, classificação de idade, agregação e montagem da figura — está reproduzido nas seções acima. Ele depende de:
using Pkg
Pkg.add(["MicroSUS", "CairoMakie", "Tables"])
Referências
Pacotes Julia:
- MicroSUS.jl — leitura de microdados do DATASUS direto em Julia.
- CairoMakie.jl — biblioteca de visualização.
Fontes dos dados:
- Ministério da Saúde — SIM (Sistema de Informações sobre Mortalidade) e SINASC (Sistema de Informações sobre Nascidos Vivos), ano de referência 2023.
Nota Importante sobre os Dados e a Análise
Todos os dados utilizados nesta análise são de domínio público, extraídos dos sistemas SIM e SINASC do Ministério da Saúde do Brasil, via MicroSUS.jl.
Esta análise foi realizada de forma independente, com propósitos puramente educacionais e de demonstração tecnológica. Os resultados, visualizações e conclusões aqui apresentados não representam um comunicado oficial de qualquer órgão governamental, seja municipal, estadual ou federal. O autor não possui vínculo nem recebeu financiamento de nenhuma entidade pública para a realização deste trabalho.
O objetivo deste artigo é estritamente didático: demonstrar o processo de coleta, classificação e visualização de dados de saúde pública utilizando a linguagem Julia. As informações não devem ser utilizadas como base para a tomada de decisões de políticas públicas, alocação de recursos ou avaliações de gestão. O autor não se responsabiliza por quaisquer interpretações, usos ou consequências derivadas da leitura deste material.
Comentários