"""Gera o relatório da Fase 1b (RHC, varredura espacial e tendências) em PT e EN. Saídas: ../fase1b/index.html (PT) e ../fase1b/en/index.html (EN), no mesmo modelo do relatório técnico. Uso: python gerar_fase1b.py (depois de rhc.py, rhc.py com-sp, varredura.py e tendencias.py) """ import json import math import os import pandas as pd from config import BRUTOS, GRUPOS, RAIZ, RESULTADOS VERSAO = "1.7" DATAS = {"pt": "13 de novembro de 2026", "en": "November 13, 2026"} BASE = "https://cledon.org" ORCID = "0009-0008-5280-1869" DOI = os.environ.get("CLEDON_DOI", "").strip() PLANO_COMMIT = "84533e1" class F: def __init__(self, lang): self.pt = lang == "pt" def n(self, x, casas=2): s = f"{x:,.{casas}f}" return s.replace(",", "X").replace(".", ",").replace("X", ".") if self.pt else s def i(self, x): s = f"{int(round(x)):,}" return s.replace(",", ".") if self.pt else s def p(self, p): if p < 0.001: return "< 0,001" if self.pt else "< 0.001" return self.n(p, 3) def ic(self, a, b): return f"{self.n(a)} – {self.n(b)}" def t(self, pt, en): return pt if self.pt else en def tabela(cab, linhas, classe="num"): th = "".join(f"{c}" for c in cab) trs = "".join("" + "".join(f"{v}" for v in l) + "" for l in linhas) return f'
{th}{trs}
' # ---------------- figuras ---------------- LON0, LON1, LAT0, LAT1 = -74.2, -34.6, -34.0, 5.4 K = math.cos(math.radians(-15)) LARG = 620 ESC = LARG / ((LON1 - LON0) * K) ALT = round((LAT1 - LAT0) * ESC) def xy(lon, lat): return round((lon - LON0) * K * ESC, 1), round((LAT1 - lat) * ESC, 1) def aneis(geom): return geom["coordinates"] if geom["type"] == "MultiPolygon" else [geom["coordinates"]] def caminho(geom, passo=1): out = [] for poly in aneis(geom): anel = poly[0][::passo] if len(anel) > 2: out.append("M" + "L".join("{},{}".format(*xy(*p)) for p in anel) + "Z") return " ".join(out) def mapa(cods_agrup, cods_sinais, rotulo): ufs = json.loads((BRUTOS / "malha_ufs.geojson").read_text()) muns = json.loads((BRUTOS / "malha_municipios.geojson").read_text()) areas, cent = [], {} for f in muns["features"]: cod = int(f["properties"]["codarea"]) pts = [p for poly in aneis(f["geometry"]) for p in poly[0]] cent[cod] = ((min(p[0] for p in pts) + max(p[0] for p in pts)) / 2, (min(p[1] for p in pts) + max(p[1] for p in pts)) / 2) if cod in cods_agrup: areas.append(caminho(f["geometry"])) s = [f'', '', f''] for cod in cods_sinais: x, y = xy(*cent[cod]) s.append(f'') s.append("") return "".join(s) def grafico(an, f): series = [("16 sinais — Minas Gerais", "Minas Gerais (9)", "var(--s1)"), ("16 sinais — Sul (PR, SC, RS)", f.t("Sul: PR, SC, RS (5)", "South: PR, SC, RS (5)"), "var(--s2)"), ("16 sinais — Bahia", "Bahia (2)", "var(--s3)")] w, h, ml, mr, mt, mb = 660, 310, 44, 170, 26, 34 ymax = 6 X = lambda a: ml + (a - 2015) / 9 * (w - ml - mr) Y = lambda v: mt + (1 - v / ymax) * (h - mt - mb) s = [f'', f'', f'' + f.t("período usado para escolher os municípios", "period used to select the municipalities") + ""] for v in range(ymax + 1): s.append(f'' f'{v}') s.append(f'') for a in range(2015, 2025): s.append(f'{a}') finais = [] for chave, rot, cor in series: d = an[an["unidade"] == chave].sort_values("ano") s.append(f'') for a, v in zip(d["ano"], d["rmp"]): s.append(f'') finais.append([Y(d["rmp"].iloc[-1]), rot, cor]) finais.append([Y(1), f.t("esperado (= 1)", "expected (= 1)"), None]) finais.sort() for i in range(1, len(finais)): finais[i][0] = max(finais[i][0], finais[i - 1][0] + 16) for y, rot, cor in finais: if cor: s.append(f'') s.append(f'{rot}') s.append(f'' + f.t("RMP, em relação ao estado no mesmo ano", "SMR, relative to the state in the same year") + "") return "".join(s) ESTILO = """""" def carregar(): d = {} for nome in ["rhc_esofago_16_municipios", "rhc_esofago_regioes", "rhc_concordancia_sim", "rhc_esofago_descritivos", "rhc_esofago_agrupamentos_nao_preespecificado", "varredura_agrupamentos", "varredura_municipios", "varredura_validacao_esofago", "tendencias_anuais", "tendencias_inclinacao"]: d[nome] = pd.read_csv(RESULTADOS / f"{nome}.csv") for nome in ["rhc_esofago_16_municipios", "rhc_esofago_regioes", "rhc_concordancia_sim", "rhc_esofago_agrupamentos_nao_preespecificado"]: d[nome + "_sp"] = pd.read_csv(RESULTADOS / f"{nome}_com_sp.csv") d["descartes"] = pd.read_csv(RESULTADOS / "rhc_descartes.csv", index_col=0) sim = pd.read_csv(RESULTADOS / "municipios_rmp.csv") d["sinais"] = sim[(sim["grupo"] == "esofago") & sim["sinal"] & (sim["contexto"] == "prioritário")] r = d["rhc_esofago_16_municipios"] d["r16"] = r[r["periodo"] == "principal_2015_2022"] d["r16s"] = r[r["periodo"] == "sensibilidade_2015_2023"] rs = d["rhc_esofago_16_municipios_sp"] d["r16sp"] = rs[rs["periodo"] == "principal_2015_2022"] ag = d["varredura_agrupamentos"] sp = d["rhc_esofago_agrupamentos_nao_preespecificado_sp"][["ordem", "esof_obs", "rip", "ic95_inf", "ic95_sup", "p", "corrobora"]] sp = sp.rename(columns={c: c + "_sp" for c in sp.columns if c != "ordem"}) d["ae"] = (ag[ag["grupo"] == "esofago"] .merge(d["varredura_validacao_esofago"][["ordem", "rmp_2015_19", "replica"]], on="ordem") .merge(sp, on="ordem")) mb = d["varredura_municipios"] d["me"] = mb[mb["grupo"] == "esofago"] incl = d["tendencias_inclinacao"].set_index("unidade") d["incl"] = incl d["ag_incl"] = incl[incl.index.str.startswith("agrupamento ")] return d def corpo(d, f): t = f.t r16, r16s, r16sp, ae, me, incl, ag_incl = d["r16"], d["r16s"], d["r16sp"], d["ae"], d["me"], d["incl"], d["ag_incl"] regs, regs_sp = d["rhc_esofago_regioes"], d["rhc_esofago_regioes_sp"] t16 = regs[regs["regiao"] == "Os 16 juntos"].iloc[0] t16sp = regs_sp[regs_sp["regiao"] == "Os 16 juntos"].iloc[0] corr, corr_sp = d["rhc_concordancia_sim"], d["rhc_concordancia_sim_sp"] desc = d["rhc_esofago_descritivos"] an = d["tendencias_anuais"] i16, iag = incl.loc["16 sinais — todos"], incl.loc["agrupamentos — todos"] n_corr, n_corr_s, n_corr_sp = int(r16["corrobora"].sum()), int(r16s["corrobora"].sum()), int(r16sp["corrobora"].sum()) dentro = int(d["sinais"]["cod_ibge"].isin(me["cod_ibge"]).sum()) novos = int((ae["sinais_prioritarios_dentro"] == 0).sum()) rep = int(ae["replica"].sum()) corr_ag_sp = int(ae["corrobora_sp"].sum()) linhas_arq = int(d["descartes"].loc["registros_no_arquivo"].sum()) a16 = an[an["unidade"] == "16 sinais — todos"] nc = {r.centro: r for r in ae[~ae["corrobora_sp"]].itertuples()} reg_nome = {"Minas Gerais": "Minas Gerais", "Sul (PR, SC, RS)": t("Sul (PR, SC, RS)", "South (PR, SC, RS)"), "Bahia": "Bahia", "Os 16 juntos": t("Os 16 juntos", "All 16")} sim_nao = t("sim", "yes"), t("não", "no") doi = f' DOI: {DOI}.' if DOI else "" t1 = tabela([t("Município", "Municipality"), "UF", t("RMP (SIM 2020–24)", "SMR (SIM 2020–24)"), t("Casos de câncer no RHC", "Cancer cases in RHC"), t("Esôfago obs.", "Esophagus obs."), t("Esôfago esp.", "Esophagus exp."), t("RIP", "PIR"), t("IC 95%", "95% CI"), "p", t("Corrobora", "Corroborates"), t("Casos RHC ÷ óbitos SIM", "RHC cases ÷ SIM deaths")], [[r.municipio, r.uf_sigla, f.n(r.rmp_uf), f.i(r.casos_cancer), f.i(r.esof_obs), f.n(r.esof_esp, 1), f.n(r.rip), f.ic(r.ic95_inf, r.ic95_sup), f.p(r.p), sim_nao[0] if r.corrobora else sim_nao[1], f.n(r.casos_rhc_por_obito_sim)] for r in r16.sort_values(["uf_sigla", "municipio"]).itertuples()]) t2 = tabela([t("Região", "Region"), t("Municípios", "Municipalities"), t("Esôfago obs.", "Esophagus obs."), t("Esôfago esp.", "Esophagus exp."), t("RIP agrupada", "Pooled PIR"), t("IC 95%", "95% CI"), "p"], [[reg_nome[r.regiao], r.municipios, f.i(r.esof_obs), f.n(r.esof_esp, 1), f.n(r.rip), f.ic(r.ic95_inf, r.ic95_sup), f.p(r.p)] for r in regs.itertuples()]) rot_conj = lambda c: (t("Os 16 municípios", "The 16 municipalities") if c.startswith("os 16") else t("Estado: ", "State: ") + c.split()[-1]) t3 = tabela([t("Conjunto", "Set"), t("Casos de esôfago", "Esophageal cases"), t("Epidermoide (%)", "Squamous (%)"), "Adenocarcinoma (%)", t("Álcool informado (%)", "Alcohol recorded (%)"), t("Álcool: sim ou ex (%)*", "Alcohol: current or former (%)*"), t("Tabaco informado (%)", "Tobacco recorded (%)"), t("Tabaco: sim ou ex (%)*", "Tobacco: current or former (%)*")], [[rot_conj(r.conjunto), f.i(r.casos_esofago), f.n(r.pct_epidermoide, 1), f.n(r.pct_adenocarcinoma, 1), f.n(r.pct_alcool_informado, 1), f.n(r.pct_alcool_sim_ou_ex_entre_informados, 1), f.n(r.pct_tabaco_informado, 1), f.n(r.pct_tabaco_sim_ou_ex_entre_informados, 1)] for r in desc.itertuples()]) rip_ag = lambda r: (f"{f.n(r.rip_sp)} ({f.n(r.ic95_inf_sp)}–{f.n(r.ic95_sup_sp)})" if r.esof_obs_sp >= 10 else t("poucos casos", "few cases")) t4 = tabela(["#", t("Centro", "Centre"), "UF", t("Raio (km)", "Radius (km)"), t("Municípios", "Municipalities"), t("Sinais dentro", "Signals inside"), t("Óbitos obs.", "Deaths obs."), t("Esp.", "Exp."), t("RMP 2020–24", "SMR 2020–24"), "p", t("RMP 2015–19", "SMR 2015–19"), t("Replica", "Replicates"), t("RIP no RHC*", "PIR in RHC*"), t("Variação anual", "Annual change")], [[r.ordem, r.centro, r.uf_centro, f.n(r.raio_km, 0), r.municipios, r.sinais_prioritarios_dentro, f.i(r.observados), f.n(r.esperados, 0), f.n(r.rmp), f.p(r.p_monte_carlo), f.n(r.rmp_2015_19), sim_nao[0] if r.replica else sim_nao[1], rip_ag(r), f.n(ag_incl.iloc[i]["variacao_anual_pct"], 1) + "%"] for i, r in enumerate(ae.itertuples())]) ag = d["varredura_agrupamentos"] outros = ag[ag["grupo"] != "esofago"].groupby("grupo").agg( n=("ordem", "size"), cap=("inclui_capital", "sum"), um=("municipios", lambda s: int((s == 1).sum()))) nome_g = lambda g: GRUPOS[g]["nome"] if f.pt else GRUPOS[g]["en"] val = lambda g, c: int(outros.loc[g, c]) if g in outros.index else 0 t5 = tabela([t("Grupo", "Group"), t("Agrupamentos (p < 0,05)", "Clusters (p < 0.05)"), t("Incluem capital", "Include a capital"), t("De um só município", "Single municipality")], [[nome_g(g), val(g, "n"), val(g, "cap"), val(g, "um")] for g in sorted((g for g in GRUPOS if g != "esofago"), key=lambda g: -val(g, "n"))]) rot_mapa = t(f"Mapa do Brasil com os {len(ae)} agrupamentos de câncer de esôfago e os 16 municípios com sinal", f"Map of Brazil with the {len(ae)} esophageal cancer clusters and the 16 municipalities with a signal") fig_mapa = mapa(set(me["cod_ibge"]), list(d["sinais"]["cod_ibge"]), rot_mapa) fig_tend = grafico(an, f) acima10 = int((ag_incl.anos_com_rmp_acima_de_1 == 10).sum()) ind = incl[incl.index.str.startswith("município — ")]["anos_com_rmp_acima_de_1"] ind10 = int((ind == 10).sum()) anos_mun = lambda nome: int(ind[[k for k in ind.index if nome in k][0]]) rip_acima = int((r16["rip"] > 1).sum()) alta = int((ag_incl.ic95_inf_pct > 0).sum()) queda = int((ag_incl.ic95_sup_pct < 0).sum()) if f.pt: return f"""{ESTILO}

Estudo exploratório — não são conclusões. Estudos ecológicos e descritivos, ainda não revisados por especialistas independentes. Não indicam causa, não permitem estimar o risco de uma pessoa específica e não mudam nenhuma recomendação médica. Versão {VERSAO} · {DATAS['pt']}.{doi}

Resumo

A Fase 1 encontrou 16 municípios, fora de capitais, do entorno de capitais e de polos de tratamento, com mortalidade por câncer de esôfago acima da esperada para o próprio estado em 2020–2024, e mostrou que o excesso já existia em 2015–2019. Esta Fase 1b faz três verificações adicionais, com plano de análise registrado localmente no git (commit {PLANO_COMMIT}) antes de qualquer resultado. O que não estava no plano está marcado como não pré-especificado.

1. Cruzamento com o Registro Hospitalar de Câncer (RHC/INCA)

Por quê. A mortalidade vem das declarações de óbito. O RHC registra os casos atendidos em hospitais, com o diagnóstico e, em geral, o tipo histológico do tumor. Se o excesso também aparecer ali, fica menos provável que seja só um problema de preenchimento das declarações de óbito.

Como. Bases nacionais do Integrador RHC (INCA), por ano da primeira consulta, de 2015 a 2022 (2023 como sensibilidade, porque está incompleto); casos analíticos; município de residência do paciente; localização C00–C97, exceto C44. Como o RHC não cobre todos os hospitais, não comparamos com a população. Usamos a razão de incidência proporcional (RIP): casos de esôfago observados ÷ esperados, com o esperado dado pela fração de esôfago entre os cânceres do próprio estado, por sexo e faixa etária. "Corrobora" = RIP > 1 e p < 0,05 (Poisson unilateral).

Um problema encontrado e corrigido. Na base nacional baixada sem os hospitais de São Paulo ({f.i(linhas_arq)} linhas nos 9 anos), toda linha aparece um número par de vezes: em cada ano, todas as linhas idênticas (todos os campos, inclusive hospital e datas) repetem-se 2, 4, 6 ou mais vezes, e nenhuma aparece um número ímpar de vezes. Em 2019, por exemplo, foram 257.655 linhas repetidas 2 vezes, 585 repetidas 4 vezes e 82 repetidas 6 ou mais vezes. Interpretamos isso como o arquivo inteiro duplicado; as linhas repetidas 4, 6 ou mais vezes seriam registros que já se repetiam na origem. Por isso, dividimos por dois o número de vezes que cada linha aparece. Uma conferência independente apoia essa leitura: em 2019, metade das linhas dessa base (259.093) é exatamente o número de linhas de hospitais fora de São Paulo na base nacional que inclui SP, que não vem duplicada. A correção não muda as razões, mas evita intervalos de confiança indevidamente estreitos.

Tabela 1. Os 16 municípios no RHC, 2015–2022.

{t1}

Tabela 2. RIP agrupada por região, 2015–2022.

{t2}

Sensibilidade. Com 2023 incluído: {n_corr_s} de 16. Com a base nacional que inclui os hospitais de São Paulo (baixada separadamente, sem duplicação; não pré-especificado): {n_corr_sp} de 16, RIP agrupada {f.n(t16sp.rip)} (IC 95% {f.ic(t16sp.ic95_inf, t16sp.ic95_sup)}).

Concordância geral. Entre os municípios com pelo menos 5 óbitos esperados por câncer de esôfago, a RMP de mortalidade e a RIP do RHC andam juntas: correlação de Spearman {f.n(corr.iloc[0].spearman)} nos estados com sinais ({f.i(corr.iloc[0].municipios)} municípios) e {f.n(corr_sp.iloc[1].spearman)} no Brasil, com a base que inclui SP.

Tabela 3. Descritivos dos casos de esôfago no RHC, 2015–2022 (sem limiar).

{t3}

* Entre os casos com a informação preenchida. Álcool e tabaco têm muitos campos "sem informação" e não medem quantidade nem tipo de bebida.

Nos 16 municípios, {f.n(desc.iloc[0].pct_epidermoide, 1)}% dos casos de esôfago são carcinoma epidermoide, o tipo associado a bebidas muito quentes, álcool e tabaco, um pouco acima do observado nos estados. As proporções registradas de álcool e tabaco ficam acima das de alguns estados e abaixo das de outros, com preenchimento e composição de pacientes diferentes. Esses dados descritivos não permitem determinar se diferenças de exposição a álcool e tabaco explicam os sinais encontrados.

Ressalvas. O RHC não é de base populacional, e o próprio INCA ressalta que registros hospitalares não medem a incidência na população. Na maioria dos 16 municípios há menos casos no RHC do que óbitos no SIM nos mesmos anos (última coluna da Tabela 1). Essa razão pode sugerir diferenças de cobertura, mas não mede diretamente a completude, porque primeiras consultas e óbitos do mesmo período não são necessariamente as mesmas pessoas. A RIP só é válida se a cobertura não depender do tipo de câncer; por exemplo, se pacientes com câncer de esôfago não forem mais encaminhados a hospitais com RHC do que pacientes com outros cânceres. Os anos do RHC se sobrepõem em parte ao período usado para escolher os municípios (2020–2024), mas a fonte é diferente.

2. Agrupamentos espaciais (varredura de Kulldorff)

Como. Procuramos, em todo o Brasil, círculos de municípios vizinhos, com até 100 km de raio e até 1 milhão de habitantes, com mais óbitos por câncer de esôfago em 2020–2024 do que o esperado para o próprio estado (modelo de Poisson). O valor de p vem de 999 simulações. O método olha regiões, não municípios isolados: pode achar excessos espalhados por vários municípios pequenos que, sozinhos, não chegariam a ser sinais. Agrupamentos secundários sem sobreposição foram mantidos se p < 0,05. Cada área foi testada em 2015–2019 (replica se RMP > 1 e p < 0,05).

{fig_mapa}

municípios dentro dos {len(ae)} agrupamentos de câncer de esôfago (p < 0,05) · os 16 municípios com sinal. Fontes: SIM/DATASUS, Censo 2022 e malhas do IBGE.

Tabela 4. Os {len(ae)} agrupamentos de câncer de esôfago, em ordem de força estatística.

{t4}

* Não pré-especificado: a mesma RIP do RHC, 2015–2022, aplicada à área de cada agrupamento, com a base que inclui os hospitais de São Paulo. Com 999 simulações, o menor valor de p possível é 0,001. Variação anual: ver seção 3.

{rep} dos {len(ae)} agrupamentos também tinham excesso em 2015–2019. No RHC, {corr_ag_sp} dos {len(ae)} têm RIP acima de 1 com p < 0,05. Nos outros quatro, as fontes não concordam: Rio das Flores (RJ) e Arapeí (SP) têm fração de esôfago abaixo da esperada no RHC (RIP {f.n(nc['Rio das Flores'].rip_sp)} e {f.n(nc['Arapeí'].rip_sp)}); Casa Branca (SP) fica pouco acima, sem significância ({f.n(nc['Casa Branca'].rip_sp)}); e São Domingos (GO) tem poucos casos no RHC. Dos 16 municípios com sinal, só Dom Feliciano (RS) ficou fora dos agrupamentos.

Outros grupos de câncer (exploratório). A mesma varredura nos outros 12 grupos, sem correção entre eles. Esses agrupamentos incluem capitais e cidades grandes, onde diferenças de diagnóstico, registro e endereço pesam mais, e não foram examinados em detalhe.

{t5}

3. Tendências anuais, 2015–2024

Como. Óbitos por ano, lidos de novo dos mesmos arquivos do SIM (a soma confere exatamente com as bases da Fase 1). Esperados com as taxas do próprio estado no mesmo ano; população interpolada entre os Censos de 2010 e 2022 até 2021, e a de 2022 depois. A variação anual vem de uma regressão de Poisson com os esperados como offset; os erros-padrão foram corrigidos para sobredispersão (quasi-Poisson), detalhe não previsto no plano.

{fig_tend}

Razão de mortalidade padronizada (RMP) por câncer de esôfago nos 16 municípios, por região. Acima de 1 = mais óbitos que o esperado para o estado, na mesma idade e sexo. A faixa sombreada é o período usado para escolher os municípios.

Considerando os 16 municípios em conjunto, a RMP permaneceu acima de 1 em cada ano de 2015 a 2024 (de {f.n(a16.rmp.min())} a {f.n(a16.rmp.max())}). Individualmente, {ind10} dos 16 ficaram acima de 1 em todos os anos; Dom Feliciano ficou acima em {anos_mun("Dom Feliciano")} dos 10 anos e Riozinho em {anos_mun("Riozinho")}, com poucos óbitos por ano. A alta aparente ({f.n(i16.variacao_anual_pct, 1)}% ao ano; IC 95% {f.n(i16.ic95_inf_pct, 1)} a {f.n(i16.ic95_sup_pct, 1)}) é em boa parte esperada por construção, porque os municípios foram escolhidos por terem excesso em 2020–2024. Nas áreas da varredura, o excesso é persistente ao longo do período, mas não estável: em conjunto, a RMP variou {f.n(iag.variacao_anual_pct, 1)}% ao ano (IC 95% {f.n(iag.ic95_inf_pct, 1)} a {f.n(iag.ic95_sup_pct, 1)}), uma leve alta; {acima10} dos {len(ag_incl)} agrupamentos ficaram acima do esperado em todos os 10 anos; {alta} têm tendência de alta e {queda} de queda. A mesma ressalva vale para eles: os agrupamentos também foram selecionados pelo excesso em 2020–2024, o que tende a produzir inclinações positivas por construção. A Fase 1c estende a série para 1996–2014, com dados que não participaram da seleção: {BASE}/fase1c.

4. O que isto sugere, com cautela

O excesso de mortalidade por câncer de esôfago nesses municípios (1) não está só nas declarações de óbito: aparece também nos casos atendidos em hospitais, com tipo histológico; (2) faz parte de áreas regionais maiores, algumas sem nenhum município que chamasse atenção sozinho; e (3) é persistente ao longo do período de 2015 a 2024, considerando os municípios em conjunto. Isso torna mais justificável investigar essas regiões, mas não diz por quê. Vieses comuns às duas fontes, como acesso a diagnóstico ou endereço informado, continuam possíveis, e os dados não medem exposições individuais. O passo seguinte mais informativo é levar esses achados a epidemiologistas e às secretarias estaduais de saúde, e comparar com registros de câncer de base populacional onde existirem. A Fase 2 ({BASE}/fase2) descreve características municipais associadas a esse excesso.

5. Limitações

6. Reprodução

Código em {BASE}/codigo. Plano: PLANO-fase1b.md (commit {PLANO_COMMIT}). Depois dos passos da Fase 1 (incluindo validar.py): baixar.py rhc rhc-sp; contar_anual.py; varredura.py; rhc.py e rhc.py com-sp (que leem os agrupamentos da varredura); tendencias.py; gerar_fase1b.py. Resultados em CSV na página do mapa ({BASE}/mapa) e no depósito do Zenodo. O SHA-256 das bases do RHC está em hashes_entradas.csv.

Em memória de Francisco Cledio Capistrano (F.C.C.).

""" return f"""{ESTILO}

Exploratory study — not conclusions. Ecological, descriptive studies that have not yet been reviewed by independent specialists. They do not indicate causation, do not allow estimating the risk of any specific person and do not change any medical recommendation. Version {VERSAO} · {DATAS['en']}.{doi} This is a translation of the Portuguese report; in case of discrepancy, the Portuguese version prevails.

Abstract

Phase 1 found 16 municipalities (outside state capitals, their surroundings and treatment hubs) with esophageal cancer mortality above the expectation for their own state in 2020–2024, and showed that the excess was already present in 2015–2019. Phase 1b runs three additional checks, with an analysis plan recorded locally in git (commit {PLANO_COMMIT}) before any result. Anything not in the plan is labelled as not pre-specified.

1. Cross-check with the Hospital Cancer Registry (RHC/INCA)

Why. Mortality comes from death certificates. The RHC records cases treated in hospitals, with the diagnosis and usually the tumour histology. If the excess also appears there, it is less likely to be only a death-certificate completion problem.

How. National bases of the RHC Integrator (INCA), by year of first visit, 2015–2022 (2023 as sensitivity, since it is incomplete); analytic cases; patient's municipality of residence; sites C00–C97 except C44. Because the RHC does not cover all hospitals, we do not compare with the population. We use the proportional incidence ratio (PIR): observed ÷ expected esophageal cases, with the expectation given by the share of esophageal cancer among cancers in the same state, by sex and age group. "Corroborates" = PIR > 1 and p < 0.05 (one-sided Poisson).

A problem found and fixed. In the national base downloaded without São Paulo hospitals ({f.i(linhas_arq)} rows over 9 years), every row appears an even number of times: in each year, all identical rows (all fields, including hospital and dates) occur 2, 4, 6 or more times, and none occurs an odd number of times. In 2019, for example, 257,655 rows occurred twice, 585 four times and 82 six or more times. We interpret this as the whole file being duplicated; rows occurring 4, 6 or more times would be records already repeated at the source. We therefore divide by two the number of times each row appears. An independent check supports this: in 2019, half of the rows in this base (259,093) is exactly the number of rows from hospitals outside São Paulo in the national base that includes SP, which is not duplicated. The correction does not change the ratios, but avoids wrongly narrow confidence intervals.

Table 1. The 16 municipalities in the RHC, 2015–2022.

{t1}

Table 2. Pooled PIR by region, 2015–2022.

{t2}

Sensitivity. Including 2023: {n_corr_s} of 16. With the national base that includes São Paulo hospitals (downloaded separately, without duplication; not pre-specified): {n_corr_sp} of 16, pooled PIR {f.n(t16sp.rip)} (95% CI {f.ic(t16sp.ic95_inf, t16sp.ic95_sup)}).

Overall agreement. Among municipalities with at least 5 expected esophageal cancer deaths, the mortality SMR and the RHC PIR move together: Spearman correlation {f.n(corr.iloc[0].spearman)} in the states with signals ({f.i(corr.iloc[0].municipios)} municipalities) and {f.n(corr_sp.iloc[1].spearman)} across Brazil, with the base that includes SP.

Table 3. Descriptive data on esophageal cases in the RHC, 2015–2022 (no threshold).

{t3}

* Among cases with the field filled in. Alcohol and tobacco have many "no information" entries and do not measure amount or type of drink.

In the 16 municipalities, {f.n(desc.iloc[0].pct_epidermoide, 1)}% of esophageal cases are squamous cell carcinoma, the type associated with very hot drinks, alcohol and tobacco, slightly above the states. The recorded shares of alcohol and tobacco use are above those of some states and below others, with different completeness and patient mix. These descriptive data cannot determine whether differences in alcohol and tobacco exposure explain the signals found.

Caveats. The RHC is not population-based, and INCA itself notes that hospital registries do not measure population incidence. In most of the 16 municipalities there are fewer RHC cases than SIM deaths in the same years (last column of Table 1). This ratio may suggest coverage differences but does not directly measure completeness, because first visits and deaths in the same period are not necessarily the same people. The PIR is valid only if coverage does not depend on cancer type; for example, if esophageal cancer patients are not referred to RHC hospitals more often than patients with other cancers. The RHC years partly overlap the period used to select the municipalities (2020–2024), but the source is different.

2. Spatial clusters (Kulldorff scan)

How. Across Brazil, we searched for circles of neighbouring municipalities, up to 100 km in radius and 1 million inhabitants, with more esophageal cancer deaths in 2020–2024 than expected for their own state (Poisson model). The p-value comes from 999 simulations. The method looks at regions, not isolated municipalities: it can find excesses spread over several small municipalities that would not be signals on their own. Non-overlapping secondary clusters were kept if p < 0.05. Each area was tested in 2015–2019 (replicates if SMR > 1 and p < 0.05).

{fig_mapa}

municipalities inside the {len(ae)} esophageal cancer clusters (p < 0.05) · the 16 municipalities with a signal. Sources: SIM/DATASUS, 2022 Census and IBGE boundaries.

Table 4. The {len(ae)} esophageal cancer clusters, in order of statistical strength.

{t4}

* Not pre-specified: the same RHC PIR, 2015–2022, applied to the area of each cluster, using the base that includes São Paulo hospitals. With 999 simulations, the smallest possible p-value is 0.001. Annual change: see section 3.

{rep} of the {len(ae)} clusters also had an excess in 2015–2019. In the RHC, {corr_ag_sp} of the {len(ae)} have a PIR above 1 with p < 0.05. In the other four the sources disagree: Rio das Flores (RJ) and Arapeí (SP) have a share of esophageal cancer below expectation in the RHC (PIR {f.n(nc['Rio das Flores'].rip_sp)} and {f.n(nc['Arapeí'].rip_sp)}); Casa Branca (SP) is slightly above, not significant ({f.n(nc['Casa Branca'].rip_sp)}); and São Domingos (GO) has few RHC cases. Of the 16 municipalities with a signal, only Dom Feliciano (RS) is outside the clusters.

Other cancer groups (exploratory). The same scan for the other 12 groups, without correction across them. These clusters include state capitals and large cities, where differences in diagnosis, recording and address weigh more, and were not examined in detail.

{t5}

3. Annual trends, 2015–2024

How. Deaths by year, read again from the same SIM files (the totals match the Phase 1 bases exactly). Expected deaths use the state's rates in the same year; population interpolated between the 2010 and 2022 Censuses up to 2021, and the 2022 population afterwards. The annual change comes from a Poisson regression with expected deaths as offset; standard errors were corrected for overdispersion (quasi-Poisson), a detail not in the plan.

{fig_tend}

Standardized mortality ratio (SMR) for esophageal cancer in the 16 municipalities, by region. Above 1 = more deaths than expected for the state, at the same age and sex. The shaded band is the period used to select the municipalities.

Considering the 16 municipalities together, the SMR remained above 1 in every year from 2015 to 2024 (from {f.n(a16.rmp.min())} to {f.n(a16.rmp.max())}). Individually, {ind10} of the 16 were above 1 in all years; Dom Feliciano was above in {anos_mun("Dom Feliciano")} of the 10 years and Riozinho in {anos_mun("Riozinho")}, with few deaths per year. The apparent rise ({f.n(i16.variacao_anual_pct, 1)}% per year; 95% CI {f.n(i16.ic95_inf_pct, 1)} to {f.n(i16.ic95_sup_pct, 1)}) is largely expected by construction, since the municipalities were selected for having an excess in 2020–2024. In the scan areas, the excess persists throughout the period but is not stable: together, the SMR changed {f.n(iag.variacao_anual_pct, 1)}% per year (95% CI {f.n(iag.ic95_inf_pct, 1)} to {f.n(iag.ic95_sup_pct, 1)}), a slight increase; {acima10} of the {len(ag_incl)} clusters were above expectation in all 10 years; {alta} show an upward trend and {queda} a downward one. The same caveat applies: the clusters were also selected for their excess in 2020–2024, which tends to produce positive slopes by construction. Phase 1c extends the series back to 1996–2014, with data that played no part in the selection: {BASE}/fase1c/en.

4. What this suggests, with caution

The excess esophageal cancer mortality in these municipalities (1) is not only in death certificates: it also appears in cases treated in hospitals, with histology; (2) is part of larger regional areas, some without any municipality that stood out on its own; and (3) persists throughout 2015–2024, considering the municipalities together. This makes investigating these regions more justified, but does not say why. Biases shared by both sources, such as access to diagnosis or the address recorded, remain possible, and the data do not measure individual exposures. The most informative next step is to bring these findings to epidemiologists and state health departments, and to compare them with population-based cancer registries where they exist. Phase 2 ({BASE}/fase2/en) describes municipal characteristics associated with this excess.

5. Limitations

6. Reproduction

Code at {BASE}/codigo. Plan: PLANO-fase1b.md (commit {PLANO_COMMIT}). Steps: after the Phase 1 steps (including validar.py): baixar.py rhc rhc-sp; contar_anual.py; varredura.py; rhc.py and rhc.py com-sp (which read the scan clusters); tendencias.py; gerar_fase1b.py. CSV results on the map page ({BASE}/mapa) and in the Zenodo deposit. SHA-256 of the RHC bases in hashes_entradas.csv.

In memory of Francisco Cledio Capistrano (F.C.C.).

""" SECOES = ["resumo", "rhc", "varredura", "tendencias", "interpretacao", "limitacoes", "reproducao"] AUTOR = {"pt": f'Autor: Clériston Almeida Capistrano (ORCID {ORCID}) · ' f'pesquisador independente, sem formação na área da saúde · cledon.org', "en": f'Author: Clériston Almeida Capistrano (ORCID {ORCID}) · ' f'independent researcher with no formal training in health sciences · cledon.org'} CASCA = { "pt": {"HTML_LANG": "pt-BR", "TITLE": "Fase 1b · Câncer de esôfago — Cledon", "DESCRIPTION": "Câncer de esôfago no Brasil: cruzamento com o registro hospitalar do INCA, agrupamentos " "espaciais e tendências anuais. Estudo exploratório.", "NAV_LABEL": "Navegação principal", "NAV": f'O mapaRelatório técnico' f'CódigoEnglish', "EYEBROW": "Relatório · Fase 1b", "H1": "Câncer de esôfago: registro hospitalar, agrupamentos espaciais e tendências", "LEAD": f'Três verificações adicionais sobre os sinais de câncer de esôfago da Fase 1 ' f'(relatório técnico). English version.', "AUTHOR": AUTOR["pt"], "TOC_LABEL": "Seções", "TOC": ["Resumo", "Registro hospitalar", "Agrupamentos", "Tendências", "Interpretação", "Limitações", "Reprodução"], "FOOTER": "Cledon · Find Cancer Clues · Iniciativa independente de pesquisa. Este estudo não oferece diagnóstico, " "tratamento ou orientação médica. Correções e revisões são bem-vindas: " 'contato@cledon.org.', "PAGE": "Página", "OF": "de", "RUNNING": f"Cledon · Fase 1b · v{VERSAO}"}, "en": {"HTML_LANG": "en", "TITLE": "Phase 1b · Esophageal cancer — Cledon", "DESCRIPTION": "Esophageal cancer in Brazil: cross-check with the INCA hospital cancer registry, spatial " "clusters and annual trends. Exploratory study.", "NAV_LABEL": "Main navigation", "NAV": f'The mapTechnical report' f'CodePortuguês', "EYEBROW": "Report · Phase 1b", "H1": "Esophageal cancer: hospital registry, spatial clusters and trends", "LEAD": f'Three additional checks on the Phase 1 esophageal cancer signals ' f'(technical report). Versão em português.', "AUTHOR": AUTOR["en"], "TOC_LABEL": "Sections", "TOC": ["Abstract", "Hospital registry", "Clusters", "Trends", "Interpretation", "Limitations", "Reproduction"], "FOOTER": "Cledon · Find Cancer Clues · Independent research initiative. This study does not provide diagnosis, " "treatment or medical advice. Corrections and reviews are welcome: " 'contato@cledon.org.', "PAGE": "Page", "OF": "of", "RUNNING": f"Cledon · Phase 1b · v{VERSAO}"}, } def main(): d = carregar() modelo = (RAIZ / "modelo_relatorio.html").read_text() for lang, saida in (("pt", RAIZ.parent / "fase1b" / "index.html"), ("en", RAIZ.parent / "fase1b" / "en" / "index.html")): casca = dict(CASCA[lang]) casca["TOC"] = "".join(f'{n}' for s, n in zip(SECOES, casca["TOC"])) pagina = modelo.replace("{{CORPO}}", corpo(d, F(lang))) for chave, valor in casca.items(): pagina = pagina.replace("{{" + chave + "}}", valor) assert "{{" not in pagina, f"marcador sem valor em {lang}" saida.parent.mkdir(parents=True, exist_ok=True) saida.write_text(pagina) print(f"Fase 1b ({lang}): {saida} ({saida.stat().st_size / 1e3:.0f} KB)") if __name__ == "__main__": main()