"""Fase 1b, parte A: cruzamento dos sinais de câncer de esôfago com o Registro Hospitalar de Câncer. Plano registrado em PLANO-fase1b.md (antes de rodar). Resumo: - bases nacionais do Integrador RHC (INCA), ano da primeira consulta 2015–2022 (principal) e 2015–2023 (sensibilidade); município de residência = PROCEDEN; - casos analíticos (TPCASO = 1), sexo e idade informados, LOCTUDET C00–C97 exceto C44; - razão de incidência proporcional (RIP): casos de esôfago (C15) observados ÷ esperados, com o esperado calculado pela fração de esôfago entre os casos de câncer do próprio estado, por sexo e faixa etária; - "corrobora" se RIP > 1 e p < 0,05 (Poisson unilateral); RIP agrupada por região; - contraste: correlação de Spearman entre a RMP de mortalidade (SIM 2020–2024) e a RIP do RHC; - descritivos: carcinoma epidermoide, casos no RHC ÷ óbitos no SIM, álcool e tabaco. O RHC não é de base populacional (só hospitais que enviam dados; o mesmo paciente pode estar em mais de um hospital). A RIP não usa a população e é robusta a diferenças de cobertura entre municípios, desde que a cobertura não dependa da localização do tumor. Achado ao processar (não previsto no plano): na base nacional sem SP, toda linha idêntica aparece um número par de vezes (2, 4, 6...), o que indica o arquivo inteiro duplicado; dividimos cada multiplicidade por 2. Em 2019, metade das linhas (259.093) coincide com as linhas de hospitais fora de SP na base com SP, que não vem duplicada. A conferência das multiplicidades fica em resultados/rhc_descartes.csv. Saídas: dados/processados/rhc_agregado.csv (contagens) e resultados/rhc_*.csv Uso: python rhc.py (depois de baixar.py rhc, analisar.py, contar_anual.py e varredura.py) """ import shutil import sys import tempfile import zipfile from collections import Counter from concurrent.futures import ProcessPoolExecutor import dbfread import numpy as np import pandas as pd from scipy import stats from config import BRUTOS, COD_UF, FAIXAS, PROCESSADOS, RESULTADOS COD_UFS = set(COD_UF.values()) ANOS_PRINCIPAL = list(range(2015, 2023)) ANOS_SENSIBILIDADE = list(range(2015, 2024)) ALFA = 0.05 CAMPOS = ["TPCASO", "SEXO", "IDADE", "PROCEDEN", "LOCTUDET", "TIPOHIST", "ALCOOLIS", "TABAGISM"] # Base usada: "nacional" (registrada no plano; sem os hospitais de SP, com cada registro duplicado no # arquivo) ou "com-sp" (sensibilidade não pré-especificada: base nacional com os hospitais de SP, sem # duplicação). Uso: python rhc.py [com-sp] BASES = {"nacional": ("rhc", True), "com-sp": ("rhc_sp", False)} BASE = "com-sp" if sys.argv[1:] == ["com-sp"] else "nacional" SUFIXO = "" if BASE == "nacional" else "_com_sp" REGIOES = {"Minas Gerais": ["MG"], "Sul (PR, SC, RS)": ["PR", "SC", "RS"], "Bahia": ["BA"]} def histologia(cod): """Classe histológica (CID-O-3) dos casos de esôfago.""" try: m = int((cod or "").split("/")[0]) except ValueError: return "sem_info" if 8050 <= m <= 8089: return "epidermoide" if 8140 <= m <= 8389: return "adenocarcinoma" return "outra" def ler_ano(args): """Conta os casos de um ano. Devolve contagens agregadas (nenhum dado individual sai daqui).""" ano, prefixo, duplicada = args zpath = BRUTOS / "rhc" / f"{prefixo}_{ano}.zip" tmp = tempfile.mkdtemp() cont, esof, desc = Counter(), Counter(), Counter() try: with zipfile.ZipFile(zpath) as z: nome = [n for n in z.namelist() if n.lower().endswith(".dbf")][0] z.extract(nome, tmp) # Na base nacional sem SP, toda linha idêntica aparece um número par de vezes (2, 4, 6...; inclusive # hospital e datas). Contamos as linhas distintas com a multiplicidade dividida por 2 e conferimos # que todas as multiplicidades são pares (caso contrário, o ano é sinalizado em "descartes"). linhas = Counter(tuple((r.get(k) or "").strip() for k in CAMPOS) for r in dbfread.DBF(f"{tmp}/{nome}", encoding="latin-1", char_decode_errors="replace")) desc["registros_no_arquivo"] = sum(linhas.values()) desc["multiplicidade_impar"] = sum(1 for v in linhas.values() if v % 2) for chave, mult in linhas.items(): r = dict(zip(CAMPOS, chave)) peso = (mult // 2 if mult % 2 == 0 else mult) if duplicada else mult if r["TPCASO"] != "1": desc["nao_analitico"] += peso continue loc = r["LOCTUDET"].upper()[:3] if not ("C00" <= loc <= "C97") or len(loc) != 3: desc["localizacao_fora_C00_C97"] += peso continue if loc == "C44": desc["pele_nao_melanoma_C44"] += peso continue if r["SEXO"] not in ("1", "2"): desc["sexo_ignorado"] += peso continue if not r["IDADE"].isdigit() or int(r["IDADE"]) > 120: desc["idade_ignorada"] += peso continue mun = r["PROCEDEN"] if len(mun) != 7 or not mun.isdigit() or mun.endswith("00000") or int(mun[:2]) not in COD_UFS: desc["municipio_ignorado"] += peso continue cod6, sexo = int(mun) // 10, int(r["SEXO"]) faixa = FAIXAS[min(int(r["IDADE"]) // 5, len(FAIXAS) - 1)] sitio = "esofago" if loc == "C15" else "outros" cont[(ano, cod6, sexo, faixa, sitio)] += peso if sitio == "esofago": esof[(ano, cod6, "hist", histologia(r["TIPOHIST"]))] += peso esof[(ano, cod6, "alcool", r["ALCOOLIS"] or "vazio")] += peso esof[(ano, cod6, "tabaco", r["TABAGISM"] or "vazio")] += peso finally: shutil.rmtree(tmp, ignore_errors=True) return ano, cont, esof, desc def agregar(): prefixo, duplicada = BASES[BASE] cont, esof, desc = Counter(), Counter(), Counter() with ProcessPoolExecutor(max_workers=3) as ex: for ano, c, e, d in ex.map(ler_ano, [(a, prefixo, duplicada) for a in ANOS_SENSIBILIDADE]): cont.update(c) esof.update(e) for k, v in d.items(): desc[(ano, k)] += v print(f" RHC {ano}: {sum(c.values()):,} casos incluídos", flush=True) ag = pd.DataFrame([(*k, v) for k, v in cont.items()], columns=["ano", "cod6", "sexo", "faixa", "sitio", "casos"]) es = pd.DataFrame([(*k, v) for k, v in esof.items()], columns=["ano", "cod6", "variavel", "valor", "casos"]) de = pd.DataFrame([(*k, v) for k, v in desc.items()], columns=["ano", "motivo", "registros"]) ag.to_csv(PROCESSADOS / f"rhc_agregado{SUFIXO}.csv", index=False) es.to_csv(PROCESSADOS / f"rhc_esofago_descritivo{SUFIXO}.csv", index=False) de.to_csv(PROCESSADOS / f"rhc_descartes{SUFIXO}.csv", index=False) return ag, es, de def ic(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 return inf, sup def rip(ag, anos): """Esôfago observado e esperado (pela fração do estado no estrato) por município.""" d = ag[ag["ano"].isin(anos)] t = d.pivot_table(index=["cod6", "sexo", "faixa"], columns="sitio", values="casos", aggfunc="sum", fill_value=0).reset_index() t["todos"] = t["esofago"] + t["outros"] t["uf"] = t["cod6"] // 10000 frac = t.groupby(["uf", "sexo", "faixa"])[["esofago", "todos"]].sum() frac = (frac["esofago"] / frac["todos"]).rename("frac_uf") t = t.merge(frac, left_on=["uf", "sexo", "faixa"], right_index=True) t["esperado"] = t["todos"] * t["frac_uf"] m = t.groupby("cod6").agg(casos_cancer=("todos", "sum"), esof_obs=("esofago", "sum"), esof_esp=("esperado", "sum")) m["rip"] = m["esof_obs"] / m["esof_esp"] m["p"] = stats.poisson.sf(m["esof_obs"] - 1, m["esof_esp"]) return m def main(): ag, es, de = agregar() sim = pd.read_csv(RESULTADOS / "municipios_rmp.csv") sim_e = sim[sim["grupo"] == "esofago"].set_index("cod6") sinais = sim_e[sim_e["sinal"] & (sim_e["contexto"] == "prioritário")] anual = pd.read_csv(PROCESSADOS / "obitos_anuais.csv") obitos_esof = anual[(anual["grupo"] == "esofago") & anual["ano"].isin(ANOS_PRINCIPAL)].groupby("cod6")["obitos"].sum() saidas = {} for rotulo, anos in (("principal_2015_2022", ANOS_PRINCIPAL), ("sensibilidade_2015_2023", ANOS_SENSIBILIDADE)): m = rip(ag, anos) s = sinais[["municipio", "uf_sigla", "observados", "rmp_uf"]].join(m, how="left") s[["casos_cancer", "esof_obs"]] = s[["casos_cancer", "esof_obs"]].fillna(0) ics = [ic(o, e) if e > 0 else (np.nan, np.nan) for o, e in zip(s["esof_obs"], s["esof_esp"].fillna(0))] s["ic95_inf"], s["ic95_sup"] = zip(*ics) s["corrobora"] = (s["rip"] > 1) & (s["p"] < ALFA) s["obitos_sim_mesmos_anos"] = obitos_esof.reindex(s.index).fillna(0).astype(int) s["casos_rhc_por_obito_sim"] = s["esof_obs"] / s["obitos_sim_mesmos_anos"].replace(0, np.nan) s.insert(0, "periodo", rotulo) saidas[rotulo] = (m, s) m, s = saidas["principal_2015_2022"] regs = [] for reg, ufs in REGIOES.items(): d = s[s["uf_sigla"].isin(ufs)] o, e = d["esof_obs"].sum(), d["esof_esp"].sum() inf, sup = ic(o, e) regs.append({"regiao": reg, "municipios": len(d), "casos_cancer": int(d["casos_cancer"].sum()), "esof_obs": int(o), "esof_esp": e, "rip": o / e, "ic95_inf": inf, "ic95_sup": sup, "p": stats.poisson.sf(o - 1, e)}) o, e = s["esof_obs"].sum(), s["esof_esp"].sum() inf, sup = ic(o, e) regs.append({"regiao": "Os 16 juntos", "municipios": len(s), "casos_cancer": int(s["casos_cancer"].sum()), "esof_obs": int(o), "esof_esp": e, "rip": o / e, "ic95_inf": inf, "ic95_sup": sup, "p": stats.poisson.sf(o - 1, e)}) regs = pd.DataFrame(regs) # Contraste: concordância geral entre mortalidade (SIM) e casos (RHC) cmp = sim_e[["uf_sigla", "esperados_uf", "rmp_uf", "sinal", "contexto"]].join(m[["esof_esp", "rip"]], how="inner") cmp = cmp[(cmp["esperados_uf"] >= 5) & (cmp["esof_esp"] > 0)] ufs_sinais = sorted({u for v in REGIOES.values() for u in v}) corr = [] for nome, d in (("estados com sinais (MG, PR, SC, RS, BA)", cmp[cmp["uf_sigla"].isin(ufs_sinais)]), ("Brasil", cmp)): r, p = stats.spearmanr(d["rmp_uf"], d["rip"]) corr.append({"conjunto": nome, "municipios": len(d), "spearman": r, "p": p}) corr = pd.DataFrame(corr) # Descritivos: histologia, álcool e tabaco (esôfago, 2015–2022), nos 16 e nos estados e = es[es["ano"].isin(ANOS_PRINCIPAL)].copy() e["uf"] = e["cod6"] // 10000 desc = [] for nome, filtro in [("os 16 municípios", e["cod6"].isin(s.index))] + [ (f"estado {u}", e["uf"] == c) for u, c in (("MG", 31), ("PR", 41), ("SC", 42), ("RS", 43), ("BA", 29))]: d = e[filtro] h = d[d["variavel"] == "hist"].groupby("valor")["casos"].sum() a = d[d["variavel"] == "alcool"].groupby("valor")["casos"].sum() t = d[d["variavel"] == "tabaco"].groupby("valor")["casos"].sum() inf_h = h.drop("sem_info", errors="ignore").sum() inf_a = a.reindex(["1", "2", "3"]).fillna(0).sum() inf_t = t.reindex(["1", "2", "3"]).fillna(0).sum() desc.append({"conjunto": nome, "casos_esofago": int(h.sum()), "pct_epidermoide": 100 * h.get("epidermoide", 0) / inf_h if inf_h else np.nan, "pct_adenocarcinoma": 100 * h.get("adenocarcinoma", 0) / inf_h if inf_h else np.nan, "pct_alcool_informado": 100 * inf_a / a.sum() if a.sum() else np.nan, "pct_alcool_sim_ou_ex_entre_informados": 100 * a.reindex(["2", "3"]).fillna(0).sum() / inf_a if inf_a else np.nan, "pct_tabaco_informado": 100 * inf_t / t.sum() if t.sum() else np.nan, "pct_tabaco_sim_ou_ex_entre_informados": 100 * t.reindex(["2", "3"]).fillna(0).sum() / inf_t if inf_t else np.nan}) desc = pd.DataFrame(desc) # Não pré-especificado: a mesma RIP para as áreas dos agrupamentos de esôfago de varredura.py ag_v = pd.read_csv(RESULTADOS / "varredura_agrupamentos.csv") mb_v = pd.read_csv(RESULTADOS / "varredura_municipios.csv") agr = [] for _, a in ag_v[ag_v["grupo"] == "esofago"].iterrows(): cods = mb_v[(mb_v["grupo"] == "esofago") & (mb_v["ordem"] == a["ordem"])]["cod_ibge"] // 10 d = m.reindex(cods) o, e = d["esof_obs"].fillna(0).sum(), d["esof_esp"].fillna(0).sum() inf, sup = ic(o, e) agr.append({"ordem": a["ordem"], "centro": a["centro"], "uf_centro": a["uf_centro"], "municipios": a["municipios"], "sinais_prioritarios_dentro": a["sinais_prioritarios_dentro"], "rmp_sim_2020_24": a["rmp"], "casos_cancer_rhc": int(d["casos_cancer"].fillna(0).sum()), "esof_obs": int(o), "esof_esp": e, "rip": o / e if e else np.nan, "ic95_inf": inf, "ic95_sup": sup, "p": stats.poisson.sf(o - 1, e), "corrobora": bool(e and o / e > 1 and stats.poisson.sf(o - 1, e) < ALFA)}) agr = pd.DataFrame(agr) agr.to_csv(RESULTADOS / f"rhc_esofago_agrupamentos_nao_preespecificado{SUFIXO}.csv", index=False, float_format="%.4g") print(agr.round(3).to_string(index=False)) pd.concat([saidas[k][1] for k in saidas]).reset_index().to_csv( RESULTADOS / f"rhc_esofago_16_municipios{SUFIXO}.csv", index=False, float_format="%.4g") regs.to_csv(RESULTADOS / f"rhc_esofago_regioes{SUFIXO}.csv", index=False, float_format="%.4g") corr.to_csv(RESULTADOS / f"rhc_concordancia_sim{SUFIXO}.csv", index=False, float_format="%.4g") desc.to_csv(RESULTADOS / f"rhc_esofago_descritivos{SUFIXO}.csv", index=False, float_format="%.4g") de.pivot_table(index="motivo", columns="ano", values="registros", aggfunc="sum").to_csv( RESULTADOS / f"rhc_descartes{SUFIXO}.csv") pd.set_option("display.width", 250, "display.max_columns", 30) print("\nCasos incluídos por ano:", ag.groupby("ano")["casos"].sum().to_dict()) print(de.pivot_table(index="motivo", columns="ano", values="registros", aggfunc="sum").to_string()) for k in saidas: print(f"\n{k}") print(saidas[k][1][["municipio", "uf_sigla", "rmp_uf", "casos_cancer", "esof_obs", "esof_esp", "rip", "ic95_inf", "ic95_sup", "p", "corrobora", "obitos_sim_mesmos_anos", "casos_rhc_por_obito_sim"]].round(3).to_string()) print() print(regs.round(4).to_string(index=False)) print() print(corr.round(4).to_string(index=False)) print() print(desc.round(1).to_string(index=False)) if __name__ == "__main__": main()