"""Fase 1c: série histórica 1996–2014 do câncer de esôfago (análises A a G de PLANO-fase1c.md). Etapas: 1. Populações de 1991 (SIDRA 205) e 2000 (SIDRA 1518) por município, sexo e faixa etária, por estado. 2. Óbitos por ano, município, sexo, faixa e grupo, 1996–2014, com as regras de preparar.py. Nos arquivos antigos o código do município tem 7 dígitos: usamos os 6 primeiros (a conversão fica neste script, para não mudar os resultados já publicados das outras fases). 3. Esperados com as taxas do próprio estado no mesmo ano; RMP por unidade (16 municípios, 24 áreas) e período. Segurança: microdados públicos sem nome; só contagens agregadas saem deste script. Saídas: dados/processados/obitos_anuais_1996_2014.csv, dados/brutos/fase1c/*.json e resultados/fase1c_*.csv Uso: python historico.py (depois de baixar o SIM 1996–2014 com baixar.baixar_sim) """ import json import time from collections import Counter from concurrent.futures import ProcessPoolExecutor import numpy as np import pandas as pd import requests from scipy import stats from config import BRUTOS, COD_UF, FAIXAS, PROCESSADOS, RESULTADOS, UFS from preparar import registrar ANOS = list(range(1996, 2015)) PASTA = BRUTOS / "fase1c" IDADE_1991 = {1140: "0-4", 1141: "5-9", 1142: "10-14", 1143: "15-19", 1144: "20-24", 1145: "25-29", 1146: "30-34", 1147: "35-39", 1148: "40-44", 1149: "45-49", 1150: "50-54", 1151: "55-59", 1152: "60-64", 1153: "65-69", 1154: "70-74", 1155: "75-79", 2503: "80+"} IDADE_2000 = {**{k: v for k, v in IDADE_1991.items() if k != 2503}, 6802: "80+", 6803: "80+", 92963: "80+", 92964: "80+", 92965: "80+"} CONSULTAS = { 1991: ("205", "c2/4,5/c58/" + ",".join(map(str, IDADE_1991)), {"4": 1, "5": 2}, IDADE_1991), 2000: ("1518", "c1/0/c2/92956,92957/c58/" + ",".join(map(str, IDADE_2000)), {"92956": 1, "92957": 2}, IDADE_2000), } PERIODOS = {"1996-1999": range(1996, 2000), "2000-2004": range(2000, 2005), "2005-2009": range(2005, 2010), "2010-2014": range(2010, 2015)} GRUPOS_FASE = ["esofago", "pulmao", "todos", "todas_causas", "mal_definidas"] ALFA = 0.05 UFS_AREAS = [] METODO_POP = "principal" # "plano" = interpolação do plano original (sensibilidade) # ---------- população ---------- def populacao_censo(ano): """População por município, sexo e faixa num Censo antigo (consulta por estado, cache em JSON).""" tabela, filtro, sexos, idades = CONSULTAS[ano] partes = [] for uf in UFS: destino = PASTA / f"pop_{ano}_{uf}.json" if not destino.exists(): url = f"https://apisidra.ibge.gov.br/values/t/{tabela}/n6/in%20n3%20{COD_UF[uf]}/v/93/p/{ano}/{filtro}" for tentativa in range(5): try: r = requests.get(url, timeout=600) r.raise_for_status() destino.write_text(r.text) break except requests.RequestException: time.sleep(10 * (tentativa + 1)) else: raise RuntimeError(f"população {ano} {uf}") d = pd.DataFrame(json.loads(destino.read_text())[1:]) col_sexo = next(c for c in d.columns if c.endswith("C") and set(d[c]) <= set(sexos)) col_idade = next(c for c in d.columns if c.endswith("C") and set(d[c]) <= {str(k) for k in idades}) partes.append(pd.DataFrame({"cod_ibge": d["D1C"].astype(int), "sexo": d[col_sexo].map(sexos), "faixa": d[col_idade].astype(int).map(idades), "pop": pd.to_numeric(d["V"], errors="coerce").fillna(0)})) p = pd.concat(partes).groupby(["cod_ibge", "sexo", "faixa"], as_index=False)["pop"].sum() print(f" Censo {ano}: {p['cod_ibge'].nunique()} municípios, {p['pop'].sum():,.0f} pessoas") return p CENSOS = {} def populacao_anual(): """Interpolação linear entre Censos; município ausente num Censo usa o Censo seguinte em que aparece.""" c = {1991: populacao_censo(1991), 2000: populacao_censo(2000), 2010: pd.read_csv(BRUTOS / "populacao_censo2010.csv"), 2022: pd.read_csv(BRUTOS / "populacao_censo2022.csv")} CENSOS.update(c) chave = ["cod_ibge", "sexo", "faixa"] base = pd.concat([x[chave] for x in c.values()]).drop_duplicates() tab = base.copy() for ano, x in c.items(): tab = tab.merge(x.rename(columns={"pop": f"p{ano}"}), on=chave, how="left") # preenche para trás: ausente num Censo -> Censo seguinte disponível for a, b in ((2010, 2022), (2000, 2010), (1991, 2000)): tab[f"p{a}"] = tab[f"p{a}"].fillna(tab[f"p{b}"]) criados = set(tab.loc[tab["cod_ibge"].isin(set(c[1991]["cod_ibge"])) == False, "cod_ibge"]) # Correção (não pré-especificada): o preenchimento acima conta duas vezes a área dos municípios criados depois de # 1991 (já incluída no município de origem em 1991), inflando a população de 1996–1999 em ~4%. No método # principal, 1996–1999 usa a divisão municipal de 2000, ajustada pela variação da população de cada estado, por # sexo e faixa, entre 1991 e 2000. O método do plano fica como sensibilidade (METODO_POP = "plano"). tab["uf"] = tab["cod_ibge"] // 100000 est = {a: c[a].assign(uf=c[a]["cod_ibge"] // 100000).groupby(["uf", "sexo", "faixa"])["pop"].sum() for a in (1991, 2000)} razao = tab.merge(pd.DataFrame({"r91": est[1991], "r00": est[2000]}).reset_index(), on=["uf", "sexo", "faixa"], how="left") linhas = [] for ano in ANOS: a, b = (1991, 2000) if ano < 2000 else (2000, 2010) if ano < 2010 else (2010, 2022) f = (ano - a) / (b - a) q = tab[chave].copy() q["ano"] = ano if ano < 2000 and METODO_POP == "principal": fator = (razao["r91"] + f * (razao["r00"] - razao["r91"])) / razao["r00"] q["pop"] = (tab["p2000"] * fator.fillna(1)).clip(lower=0) else: q["pop"] = (tab[f"p{a}"] + f * (tab[f"p{b}"] - tab[f"p{a}"])).clip(lower=0) linhas.append(q) pop = pd.concat(linhas, ignore_index=True) pop["cod6"] = pop["cod_ibge"] // 10 pop["uf"] = pop["cod_ibge"] // 100000 return pop, criados # ---------- óbitos ---------- def _seis(reg): """Arquivos antigos: código do município com 7 dígitos -> 6 primeiros.""" for campo in ("CODMUNRES", "CODMUNOCOR"): v = (reg.get(campo) or "").strip() if len(v) == 7 and v.isdigit(): reg[campo] = v[:6] return reg def contar_arquivo(args): import dbfread, datasus_dbc, os, tempfile caminho, ano = args cont, desc = Counter(), Counter() fd, tmp = tempfile.mkstemp(suffix=".dbf") os.close(fd) try: datasus_dbc.decompress(str(caminho), tmp) for reg in dbfread.DBF(tmp, encoding="latin-1", char_decode_errors="replace"): registrar(_seis(dict(reg)), cont, desc) finally: os.remove(tmp) return ano, caminho.name, cont, desc def obitos_anuais(): destino = PROCESSADOS / "obitos_anuais_1996_2014.csv" if destino.exists(): return pd.read_csv(destino), pd.read_csv(PROCESSADOS / "descartes_1996_2014.csv") arquivos = [(BRUTOS / "sim" / f"DO{uf}{ano}.dbc", ano) for ano in ANOS for uf in UFS] faltam = [a.name for a, _ in arquivos if not a.exists()] if faltam: raise SystemExit(f"Faltam {len(faltam)} arquivos do SIM, ex.: {faltam[:5]}") total, desc = Counter(), Counter() with ProcessPoolExecutor() as ex: for n, (ano, nome, cont, d) in enumerate(ex.map(contar_arquivo, arquivos), 1): for (m, s, f, g), k in cont.items(): if g in GRUPOS_FASE: total[(ano, m, s, f, g)] += k for k, v in d.items(): desc[(ano, k)] += v if n % 27 == 0: print(f" {ano} ok ({n}/{len(arquivos)})", flush=True) df = pd.DataFrame([(*k, v) for k, v in total.items()], columns=["ano", "cod6", "sexo", "faixa", "grupo", "obitos"]) de = pd.DataFrame([(*k, v) for k, v in desc.items()], columns=["ano", "motivo", "registros"]) df.to_csv(destino, index=False) de.to_csv(PROCESSADOS / "descartes_1996_2014.csv", index=False) return df, de # ---------- áreas mínimas comparáveis ---------- # Municípios criados em 2013, fora da tabela de AMC 1991–2010 do IPEA: associados à AMC do município de origem # (Lei de criação; Paraíso das Águas/MS tem mais de uma origem: usamos Costa Rica, a principal). ORIGEM_2013 = {1504752: 1506807, 4212650: 4209409, 4220000: 4207007, 4314548: 4302105, 5006275: 5003256} def amc_por_municipio(): """Código da área mínima comparável 1991–2010 (IPEA/geobr) de cada município atual (cod_ibge de 7 dígitos).""" import sqlite3 con = sqlite3.connect(PASTA / "AMC_1991_2010_simplified.gpkg") a = pd.read_sql("select code_amc, list_code_muni_2010 from AMC_1991_2010_simplified", con) con.close() m = {int(x): int(r.code_amc) for r in a.itertuples() for x in r.list_code_muni_2010.split(",")} for novo, origem in ORIGEM_2013.items(): m[novo] = m[origem] # Uma AMC (11052) junta municípios de MG e do ES: cada parte estadual vira uma unidade própria, para que os # esperados usem as taxas do estado certo (o território de cada parte é constante entre 1991 e 2014). return {k: v * 100 + k // 100000 for k, v in m.items()} def oe_amc(pop_censos, ob, grupo): """Observados e esperados por AMC e ano, com a população interpolada linearmente entre os Censos dentro de cada AMC (território constante, como no plano original).""" m = amc_por_municipio() chave = ["amc", "sexo", "faixa"] tab = None for ano_c, pc in pop_censos.items(): x = pc.assign(amc=pc["cod_ibge"].map(m)).dropna(subset=["amc"]).groupby(chave)["pop"].sum().rename(f"p{ano_c}") tab = x.to_frame() if tab is None else tab.join(x, how="outer") tab = tab.fillna(0).reset_index() linhas = [] for ano in ANOS: a, b = (1991, 2000) if ano < 2000 else (2000, 2010) if ano < 2010 else (2010, 2022) f = (ano - a) / (b - a) q = tab[chave].copy() q["ano"] = ano q["pop"] = (tab[f"p{a}"] + f * (tab[f"p{b}"] - tab[f"p{a}"])).clip(lower=0) linhas.append(q) pop = pd.concat(linhas, ignore_index=True) pop["uf"] = (pop["amc"] % 100).astype(int) # código composto: AMC × 100 + UF o = ob.pivot_table(index=["ano", "cod6", "sexo", "faixa"], columns="grupo", values="obitos", aggfunc="sum", fill_value=0).reset_index() o["outros"] = o["todos"] - o["esofago"] m6 = {k // 10: v for k, v in m.items()} o["amc"] = o["cod6"].map(m6) o["uf"] = o["cod6"] // 10000 o = o.dropna(subset=["amc"]) taxa = (o.groupby(["ano", "uf", "sexo", "faixa"])[grupo].sum() / pop.groupby(["ano", "uf", "sexo", "faixa"])["pop"].sum()).rename("taxa").fillna(0) pop = pop.merge(taxa, left_on=["ano", "uf", "sexo", "faixa"], right_index=True, how="left") pop["esp"] = pop["pop"] * pop["taxa"].fillna(0) e = pop.groupby(["amc", "ano"])["esp"].sum() obs = o.groupby(["amc", "ano"])[grupo].sum().reindex(e.index).fillna(0) return pd.DataFrame({"obs": obs, "esp": e}).reset_index(), m # ---------- RMP ---------- def observados_esperados(pop, ob, grupo, redistribuir=False): """Observados e esperados (taxas do estado no mesmo ano) por município e ano.""" chave = ["ano", "cod6", "sexo", "faixa"] o = ob.pivot_table(index=chave, columns="grupo", values="obitos", aggfunc="sum", fill_value=0).reset_index() for g in GRUPOS_FASE: if g not in o: o[g] = 0 o["outros"] = o["todos"] - o["esofago"] x = o[grupo].astype(float) o["uf"] = o["cod6"] // 10000 if redistribuir: # Redistribuição proporcional das causas mal definidas entre as definidas, no mesmo município, ano, sexo e # faixa. Estratos sem nenhuma causa definida usam a fração do estado no mesmo ano, sexo e faixa (sem isso, # ~10% dos óbitos mal definidos ficariam sem alocação). Resultado descritivo: as contagens deixam de ser inteiras. definidas = o["todas_causas"] - o["mal_definidas"] est = o.assign(defi=definidas).groupby(["ano", "uf", "sexo", "faixa"]).agg(g=(grupo, "sum"), d=("defi", "sum")) frac_est = (est["g"] / est["d"].where(est["d"] > 0)).fillna(0).rename("frac_est") o = o.merge(frac_est, left_on=["ano", "uf", "sexo", "faixa"], right_index=True, how="left") definidas = o["todas_causas"] - o["mal_definidas"] frac = np.where(definidas > 0, o[grupo] / definidas.where(definidas > 0, 1), o["frac_est"].fillna(0)) x = o[grupo] + o["mal_definidas"] * frac o["obs"] = x p = pop.groupby(chave + ["uf"], as_index=False)["pop"].sum() taxa = (o.groupby(["ano", "uf", "sexo", "faixa"])["obs"].sum() / p.groupby(["ano", "uf", "sexo", "faixa"])["pop"].sum()).rename("taxa").fillna(0) p = p.merge(taxa, left_on=["ano", "uf", "sexo", "faixa"], right_index=True, how="left") p["esp"] = p["pop"] * p["taxa"].fillna(0) e = p.groupby(["cod6", "ano"])["esp"].sum() obs = o.groupby(["cod6", "ano"])["obs"].sum().reindex(e.index).fillna(0) return pd.DataFrame({"obs": obs, "esp": e}).reset_index() def rmp(o, e): inf = stats.chi2.ppf(0.025, 2 * o) / 2 / e if o > 0 else 0.0 sup = stats.chi2.ppf(0.975, 2 * (o + 1)) / 2 / e p = stats.gamma.cdf(e, o) if o > 0 else 1.0 # P(Poisson(e) >= o) = P(Gama(o) <= e); vale para o não inteiro return {"obs": o, "esp": e, "rmp": o / e if e else np.nan, "ic95_inf": inf, "ic95_sup": sup, "p": p} def main(): PASTA.mkdir(parents=True, exist_ok=True) pop, criados = populacao_anual() ob, desc = obitos_anuais() print("Óbitos por ano (todas as causas):", ob[ob["grupo"] == "todas_causas"].groupby("ano")["obitos"].sum().to_dict()) rmpd = pd.read_csv(RESULTADOS / "municipios_rmp.csv") s16 = rmpd[(rmpd["grupo"] == "esofago") & rmpd["sinal"] & (rmpd["contexto"] == "prioritário")] mb = pd.read_csv(RESULTADOS / "varredura_municipios.csv") mb = mb[mb["grupo"] == "esofago"] ag = pd.read_csv(RESULTADOS / "varredura_agrupamentos.csv") ag = ag[ag["grupo"] == "esofago"].set_index("ordem") unidades = {"16 municípios": set(s16["cod6"])} for reg, ufs in (("16 — Minas Gerais", ["MG"]), ("16 — Sul (PR, SC, RS)", ["PR", "SC", "RS"]), ("16 — Bahia", ["BA"])): unidades[reg] = set(s16[s16["uf_sigla"].isin(ufs)]["cod6"]) for _, r in s16.iterrows(): unidades[f"município — {r['municipio']} ({r['uf_sigla']})"] = {r["cod6"]} unidades["24 áreas"] = set(mb["cod_ibge"] // 10) for ordem, g in mb.groupby("ordem"): unidades[f"área {ordem:02d} — {ag.loc[ordem, 'centro']} ({ag.loc[ordem, 'uf_centro']})"] = set(g["cod_ibge"] // 10) criados6 = {c // 10 for c in criados} global UFS_AREAS UFS_AREAS = sorted(mb["uf_sigla"].unique()) linhas = [] cenarios = [("principal", "esofago", False, False), ("redistribuicao_mal_definidas", "esofago", True, False), ("sem_criados_apos_1991", "esofago", False, True), ("pulmao", "pulmao", False, False), ("outros_canceres", "outros", False, False)] for cen, grupo, redis, sem_criados in cenarios: oe = observados_esperados(pop, ob, grupo, redis) for nome, cods in unidades.items(): if not (nome.startswith("16") or nome.startswith("24") or cen in ("principal", "redistribuicao_mal_definidas")): continue cs = cods - criados6 if sem_criados else cods d = oe[oe["cod6"].isin(cs)] for per, anos in {**PERIODOS, "2000-2014": range(2000, 2015), "1996-2014": range(1996, 2015)}.items(): x = d[d["ano"].isin(anos)] linhas.append({"cenario": cen, "unidade": nome, "periodo": per, "municipios": len(cs), **rmp(x["obs"].sum(), x["esp"].sum())}) # Sensibilidade: população pelo método do plano original (conta duas vezes a área de municípios criados após 1991) global METODO_POP METODO_POP = "plano" pop_plano, _ = populacao_anual() METODO_POP = "principal" oe = observados_esperados(pop_plano, ob, "esofago") for nome, cods in unidades.items(): if nome.startswith("16") or nome.startswith("24"): d = oe[oe["cod6"].isin(cods)] for per, anos in {**PERIODOS, "2000-2014": range(2000, 2015), "1996-2014": range(1996, 2015)}.items(): x = d[d["ano"].isin(anos)] linhas.append({"cenario": "populacao_metodo_do_plano", "unidade": nome, "periodo": per, "municipios": len(cods), **rmp(x["obs"].sum(), x["esp"].sum())}) # Sensibilidade territorial (não pré-especificada, após revisão): áreas mínimas comparáveis 1991–2010 (IPEA). # Cada unidade passa a ser a união das AMC dos seus municípios; território constante de 1991 a 2014. oe, m = oe_amc(CENSOS, ob, "esofago") composicao = [] for nome, cods in unidades.items(): amcs = {m[c] for c in pd.Series(sorted(cods)).map({k // 10: k for k in m}).dropna().astype(int)} d = oe[oe["amc"].isin(amcs)] membros = sorted(k for k, v in m.items() if v in amcs) composicao.append({"unidade": nome, "amcs": len(amcs), "municipios_atuais_nas_amc": len(membros), "municipios_da_unidade": len(cods)}) for per, anos in {**PERIODOS, "2000-2014": range(2000, 2015), "1996-2014": range(1996, 2015)}.items(): x = d[d["ano"].isin(anos)] linhas.append({"cenario": "areas_minimas_comparaveis", "unidade": nome, "periodo": per, "municipios": len(membros), **rmp(x["obs"].sum(), x["esp"].sum())}) pd.DataFrame(composicao).to_csv(RESULTADOS / "fase1c_amc_composicao.csv", index=False) res = pd.DataFrame(linhas) res["replica"] = (res["rmp"] > 1) & (res["p"] < ALFA) # Com óbitos redistribuídos (não inteiros), IC e p de Poisson não valem: só a RMP é apresentada red = res["cenario"] == "redistribuicao_mal_definidas" res.loc[red, ["ic95_inf", "ic95_sup", "p"]] = np.nan res["replica"] = res["replica"].astype(object) res.loc[red, "replica"] = "" # Qualidade: % de causas mal definidas por período q = [] for per, anos in PERIODOS.items(): o = ob[ob["ano"].isin(anos)] for nome, cods in [(k, unidades[k]) for k in ("16 municípios", "16 — Minas Gerais", "16 — Sul (PR, SC, RS)", "16 — Bahia", "24 áreas")] + [ (f"estado {uf}", None) for uf in UFS_AREAS]: x = o[o["cod6"].isin(cods)] if cods else o[o["cod6"] // 10000 == COD_UF[nome.split()[-1]]] tot = x[x["grupo"] == "todas_causas"]["obitos"].sum() md = x[x["grupo"] == "mal_definidas"]["obitos"].sum() q.append({"periodo": per, "conjunto": nome, "obitos": int(tot), "pct_mal_definidas": 100 * md / tot if tot else np.nan}) q = pd.DataFrame(q) pv = ob.pivot_table(index=["ano", "cod6", "sexo", "faixa"], columns="grupo", values="obitos", aggfunc="sum", fill_value=0) sem_def = pv[(pv["todas_causas"] > 0) & (pv["todas_causas"] == pv["mal_definidas"])] pd.DataFrame([{"estratos_sem_causa_definida": len(sem_def), "obitos_mal_definidos_nesses_estratos": int(sem_def["mal_definidas"].sum()), "obitos_mal_definidos_total": int(pv["mal_definidas"].sum())}]).to_csv(RESULTADOS / "fase1c_redistribuicao_estratos.csv", index=False) # Resumo dos critérios B e C (2000–2014, principal) pr = res[(res["cenario"] == "principal") & (res["periodo"] == "2000-2014")] mun = pr[pr["unidade"].str.startswith("município")] areas = pr[pr["unidade"].str.startswith("área")] resumo = pd.DataFrame([ {"criterio": "A: 16 juntos, 2000–2014", **pr[pr["unidade"] == "16 municípios"].iloc[0][["rmp", "ic95_inf", "ic95_sup", "p", "replica"]].to_dict()}, {"criterio": "B: municípios que replicam (de 16)", "n": int(mun["replica"].sum()), "rmp_acima_de_1": int((mun["rmp"] > 1).sum())}, {"criterio": "C: áreas que replicam (de 24)", "n": int(areas["replica"].sum()), "rmp_acima_de_1": int((areas["rmp"] > 1).sum())}, ]) res.to_csv(RESULTADOS / "fase1c_rmp.csv", index=False, float_format="%.10g") # sem arredondar contagens q.to_csv(RESULTADOS / "fase1c_qualidade.csv", index=False, float_format="%.3g") resumo.to_csv(RESULTADOS / "fase1c_resumo.csv", index=False, float_format="%.10g") desc.pivot_table(index="motivo", columns="ano", values="registros", aggfunc="sum").to_csv(RESULTADOS / "fase1c_descartes.csv") pd.DataFrame({"cod_ibge": sorted(criados)}).to_csv(RESULTADOS / "fase1c_municipios_criados_apos_1991.csv", index=False) pd.set_option("display.width", 250, "display.max_columns", 30) print(resumo.round(4).to_string(index=False)) t = res[res["unidade"].isin(["16 municípios", "16 — Minas Gerais", "16 — Sul (PR, SC, RS)", "16 — Bahia", "24 áreas"])] print(t.pivot_table(index=["unidade", "cenario"], columns="periodo", values="rmp").round(2).to_string()) print(q.pivot(index="conjunto", columns="periodo", values="pct_mal_definidas").round(1).to_string()) print(mun[["unidade", "obs", "esp", "rmp", "ic95_inf", "ic95_sup", "p", "replica"]].round(3).to_string(index=False)) if __name__ == "__main__": main()