"""Nó local do demonstrador: roda DENTRO de cada hospital simulado. Recebe um pedido (JSON pela entrada padrão), confere se o protocolo foi aprovado por este hospital, executa só operações permitidas sobre variáveis aprovadas, aplica controles de saída e devolve apenas agregados (JSON pela saída padrão). Toda decisão fica registrada na auditoria local do hospital. Os registros individuais nunca saem daqui. Uso (feito pelo coordenador): python no.py hospitais/A < pedido.json """ import csv import datetime as dt import hashlib import json import sys from pathlib import Path import numpy as np CODIGO_VERSAO = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()[:12] def hash_protocolo(p): return hashlib.sha256(json.dumps(p, sort_keys=True, ensure_ascii=False).encode()).hexdigest() def ler_pacientes(pasta): with open(pasta / "pacientes_ficticios.csv") as f: linhas = [l for l in f if not l.startswith("#")] return list(csv.DictReader(linhas)) FAIXAS = {"idade": (18, 100), "albumina_g_dl": (1.0, 6.5), "hemoglobina_g_dl": (4.0, 20.0), "rnl": (0.05, 100.0), "perda_peso_pct": (0.0, 60.0)} CATEGORIAS = {"sexo": {"M", "F"}, "ct": {"T1-2", "T3-4"}, "cn": {"N0", "N+"}, "localizacao": {"superior", "medio", "inferior"}, "ecog": {"0", "1+"}} def num(v, campo=None): """Número válido ou ausente: vazio, texto inválido ou valor fora da faixa plausível viram ausência.""" try: x = float(v) except (TypeError, ValueError): return np.nan if not np.isfinite(x): return np.nan if campo in FAIXAS and not FAIXAS[campo][0] <= x <= FAIXAS[campo][1]: return np.nan return x def cat(campo, positivo): """Indicadora de uma categoria; campo vazio ou fora das categorias válidas vira ausência (nunca a referência).""" return lambda r: float(r[campo] == positivo) if r[campo] in CATEGORIAS[campo] else np.nan TRANSFORMACOES = { # implementação fixa das transformações nomeadas no protocolo "idade_10anos": lambda r: (num(r["idade"], "idade") - 60) / 10, "sexo_feminino": cat("sexo", "F"), "ct3_4": cat("ct", "T3-4"), "cn_positivo": cat("cn", "N+"), "terco_inferior": cat("localizacao", "inferior"), "ecog_1_ou_mais": cat("ecog", "1+"), "albumina_g_dl": lambda r: num(r["albumina_g_dl"], "albumina_g_dl") - 3.9, "hemoglobina_g_dl": lambda r: num(r["hemoglobina_g_dl"], "hemoglobina_g_dl") - 12.8, "log_rnl": lambda r: np.log(num(r["rnl"], "rnl")) - np.log(2.6), "perda_peso_pct": lambda r: num(r["perda_peso_pct"], "perda_peso_pct") - 8, } def valido(r, v): """O valor de uma variável aprovada está presente e é válido?""" if v in CATEGORIAS: return r[v] in CATEGORIAS[v] if v in FAIXAS: return not np.isnan(num(r[v], v)) if v == "pcr": return r[v] in ("0", "1") return r[v] in ("sim", "nao") def matriz(pacientes, termos, casos_de=None): """Casos completos nas variáveis do modelo; com casos_de, usa os casos completos de outro conjunto de termos (para comparar modelos exatamente nos mesmos pacientes).""" X = np.array([[1.0] + [TRANSFORMACOES[t](r) for t in termos] for r in pacientes]).reshape(-1, len(termos) + 1) y = np.array([num(r["pcr"]) if r["pcr"] in ("0", "1") else np.nan for r in pacientes]) filtro = np.array([[TRANSFORMACOES[t](r) for t in (casos_de or termos)] for r in pacientes]).reshape(-1, len(casos_de or termos)) ok = np.isfinite(X).all(axis=1) & np.isfinite(filtro).all(axis=1) & np.isfinite(y) return X[ok], y[ok] def suprimir(contagens, k, complementar): """Suprime células entre 1 e k-1; se só uma célula foi suprimida, suprime também a menor restante, para que ela não possa ser recuperada pela diferença com o total.""" out = {c: (v if v == 0 or v >= k else None) for c, v in contagens.items()} sup = [c for c, v in out.items() if v is None] if complementar and len(sup) == 1: resto = sorted((v, c) for c, v in out.items() if v not in (None, 0)) if resto: out[resto[0][1]] = None return {c: (f"<{k}" if v is None and contagens[c] < k else "suprimido" if v is None else v) for c, v in out.items()} def margem(n_total, faltantes, k): """Faltantes e observados de uma variável são uma única tabela com total conhecido: os dois rótulos são decididos juntos, em todas as operações, para que uma consulta não desfaça a supressão de outra.""" obs = n_total - faltantes if 0 < faltantes < k: return f"<{k}", f"{n_total - k + 1}–{n_total - 1}" if 0 < obs < k: return f"{n_total - k + 1}–{n_total - 1}", f"<{k}" return faltantes, obs def auc(y, p): pos, neg = p[y == 1], p[y == 0] if len(pos) == 0 or len(neg) == 0: return np.nan ordem = np.argsort(np.concatenate([pos, neg]), kind="mergesort") ranks = np.empty(len(ordem)) todos = np.concatenate([pos, neg])[ordem] i = 0 while i < len(todos): # postos médios em empates j = i while j + 1 < len(todos) and todos[j + 1] == todos[i]: j += 1 ranks[ordem[i:j + 1]] = (i + j) / 2 + 1 i = j + 1 return (ranks[:len(pos)].sum() - len(pos) * (len(pos) + 1) / 2) / (len(pos) * len(neg)) def inclinacao_calibracao(y, p): z = np.log(p / (1 - p)) b = np.zeros(2) Z = np.column_stack([np.ones_like(z), z]) for _ in range(50): q = 1 / (1 + np.exp(-Z @ b)) H = Z.T @ (Z * (q * (1 - q))[:, None]) passo = np.linalg.solve(H, Z.T @ (y - q)) b += passo if np.abs(passo).max() < 1e-10: break return float(b[1]) def executar(pedido, pasta): protocolo, op, par = pedido["protocolo"], pedido["operacao"], pedido.get("parametros", {}) ctrl = protocolo["controles_de_saida"] k, nmin = ctrl["celula_minima"], ctrl["n_minimo_para_modelo"] h = hash_protocolo(protocolo) aprov = json.loads((pasta / "aprovacoes.json").read_text()) if h not in aprov["protocolos_aprovados"]: return {"recusado": "protocolo não aprovado por este hospital (versão diferente da aprovada)"} codigo = aprov["codigo_hospital"] desenvolvimento = codigo in protocolo["desenvolvimento"] externo = codigo in protocolo["teste_externo"] if desenvolvimento == externo: return {"recusado": "hospital sem papel único no protocolo aprovado"} if op not in protocolo["operacoes_permitidas"]: return {"recusado": f"operação '{op}' não está prevista no protocolo"} aprovadas = protocolo["variaveis_aprovadas"] pedidas = par.get("variaveis", []) fora = [v for v in pedidas if v not in aprovadas] if fora: return {"recusado": f"variáveis fora do protocolo: {', '.join(fora)}"} pac = ler_pacientes(pasta) if op == "qualidade": faltam = {v: margem(len(pac), sum(1 for r in pac if not valido(r, v)), k)[0] for v in aprovadas} return {"n_total": len(pac), "faltantes": faltam} if op == "descritivo": out = {} for v in pedidas: if aprovadas[v] == "contínua": x = np.array([num(r[v], v) for r in pac]) x = x[~np.isnan(x)] n_rot = margem(len(pac), len(pac) - len(x), k)[1] out[v] = {"n": n_rot, "media": round(float(x.mean()), 2), "dp": round(float(x.std(ddof=1)), 2)} \ if len(x) >= k else {"n": n_rot} else: rot = lambda r: r[v] if valido(r, v) else "(não registrado)" niveis = sorted({rot(r) for r in pac}) cont = {nv: sum(1 for r in pac if rot(r) == nv) for nv in niveis} out[v] = suprimir(cont, k, ctrl["supressao_complementar"]) return {"n_total": len(pac), "variaveis": out} if op == "logistica_passo" and externo: return {"recusado": "este hospital está reservado para teste externo e não participa do desenvolvimento"} if op == "avaliar" and externo: mc = par.get("modelo_congelado") if not mc: return {"recusado": "o teste externo só avalia um modelo congelado e registrado neste hospital"} hm = hash_protocolo(mc) if hm not in aprov.get("modelos_congelados_aprovados", []): return {"recusado": "modelo congelado não registrado neste hospital"} if mc["protocolo"] != h: return {"recusado": "modelo congelado pertence a outra versão do protocolo"} if mc.get("termos") != ["intercepto"] + protocolo["modelos"].get(mc.get("modelo"), []): return {"recusado": "termos do modelo congelado não correspondem ao protocolo"} if mc.get("avaliacao") != protocolo["avaliacao"]: return {"recusado": "avaliação do modelo congelado não corresponde ao protocolo"} ja = [l for l in (pasta / "auditoria_local.jsonl").read_text().splitlines() if l and json.loads(l).get("modelo") == hm[:12] and json.loads(l)["decisao"] == "executado"] if ja: return {"recusado": "este modelo congelado já foi avaliado aqui; o teste externo é feito uma única vez"} par = dict(par, modelo=mc["modelo"], beta=mc["beta"]) if op == "avaliar": for campo, valor in protocolo["avaliacao"].items(): if campo in par and par[campo] != valor: return {"recusado": "parâmetros de avaliação diferentes dos aprovados no protocolo"} par = dict(par, **protocolo["avaliacao"]) termos = protocolo["modelos"][par["modelo"]] X, y = matriz(pac, termos, protocolo["modelos"].get(par.get("mesmos_casos_que", ""), None)) if len(y) < nmin: return {"recusado": f"menos de {nmin} casos completos; o modelo não é executado neste hospital"} if min(int(y.sum()), int(len(y) - y.sum())) < k: return {"recusado": "grupo de resposta ou não resposta menor que o mínimo de divulgação"} b = np.array(par["beta"], dtype=float) if b.shape != (len(termos) + 1,) or not np.isfinite(b).all(): return {"recusado": "coeficientes inválidos ou incompatíveis com o modelo"} if op == "logistica_passo": usados = sum(1 for l in (pasta / "auditoria_local.jsonl").read_text().splitlines() if l and json.loads(l)["operacao"] == "logistica_passo" and json.loads(l)["decisao"] == "executado" and json.loads(l)["protocolo"] == h[:12]) if usados >= ctrl["maximo_de_rodadas_de_modelo_por_hospital"]: return {"recusado": "limite de rodadas de modelo atingido para este protocolo"} p = 1 / (1 + np.exp(-X @ b)) return {"n": int(len(y)), "eventos": int(y.sum()), "gradiente": (X.T @ (y - p)).round(10).tolist(), "hessiana": (X.T @ (X * (p * (1 - p))[:, None])).round(10).tolist(), "logverossimilhanca": float(np.sum(y * np.log(p) + (1 - y) * np.log(1 - p)))} if op == "avaliar": p = 1 / (1 + np.exp(-X @ b)) rng = np.random.default_rng(par.get("semente", 1)) boots = [] for _ in range(par.get("reamostragens", 1000)): i = rng.integers(0, len(y), len(y)) boots.append(auc(y[i], p[i])) boots = np.array(boots) boots = boots[~np.isnan(boots)] return {"n": int(len(y)), "eventos": int(y.sum()), "auc": round(float(auc(y, p)), 4), "auc_ic95": [round(float(np.quantile(boots, 0.025)), 4), round(float(np.quantile(boots, 0.975)), 4)], "brier": round(float(np.mean((p - y) ** 2)), 4), "observado_sobre_esperado": round(float(y.sum() / p.sum()), 3), "inclinacao_calibracao": round(inclinacao_calibracao(y, p), 3)} return {"recusado": f"operação '{op}' não implementada"} def verificar_saida(saida, nmin): """Última barreira: bloqueia qualquer resposta que contenha listas longas (possíveis linhas individuais).""" def longa(o): if isinstance(o, list): return len(o) >= nmin or any(longa(x) for x in o) if isinstance(o, dict): return any(longa(x) for x in o.values()) return False return {"recusado": "saída bloqueada: parecia conter dados em nível individual"} if longa(saida) else saida def main(): pasta = Path(sys.argv[1]) pedido = json.loads(sys.stdin.read()) try: saida = executar(pedido, pasta) saida = verificar_saida(saida, pedido["protocolo"]["controles_de_saida"]["n_minimo_para_modelo"]) except Exception as e: # o hospital não devolve detalhes internos saida = {"recusado": f"erro local ({type(e).__name__})"} registro = {"quando": dt.datetime.now().isoformat(timespec="seconds"), "operacao": pedido.get("operacao"), "protocolo": hash_protocolo(pedido["protocolo"])[:12], "codigo": CODIGO_VERSAO, "decisao": "recusado" if "recusado" in saida else "executado", "motivo": saida.get("recusado", "")} if pedido.get("parametros", {}).get("modelo_congelado"): registro["modelo"] = hash_protocolo(pedido["parametros"]["modelo_congelado"])[:12] with open(pasta / "auditoria_local.jsonl", "a") as f: f.write(json.dumps(registro, ensure_ascii=False) + "\n") saida["_codigo"] = CODIGO_VERSAO print(json.dumps(saida, ensure_ascii=False)) if __name__ == "__main__": main()