"""LT1 · 20 · estudio de eventos pre-registrado (DEFINICIONES_PREVIAS §2-§5, §8). DuckDB (lectura) + numpy. Lee interno/cel_men.parquet, cel_nm.parquet, lotes_sara.parquet, org.parquet. Escribe salidas/eventos.csv y salidas/20_eventos.json.""" import duckdb, json, math, csv, sys, time import numpy as np T0 = time.time() def log(*a): print(f"[{time.time()-T0:6.1f}s]", *a, flush=True) SEM, B = 20261022, 1000 OFF = 18 # meses a cada lado del evento que se cargan c = duckdb.connect(); c.execute("SET threads=4; SET memory_limit='6GB'; SET temp_directory='/w/tmp/duck'; SET max_temp_directory_size='5GB'") x = c.execute x("CREATE TABLE cm AS SELECT *, (year(mes)-2012)*12 + month(mes)-1 mi FROM '/w/interno/cel_men.parquet'") x("CREATE TABLE cn AS SELECT *, (year(mes)-2012)*12 + month(mes)-1 mi FROM '/w/interno/cel_nm.parquet'") x("CREATE TABLE ls AS SELECT *, (year(mes_pub)-2012)*12 + month(mes_pub)-1 mi FROM '/w/interno/lotes_sara.parquet'") x("CREATE TABLE org AS SELECT * FROM '/w/interno/org.parquet'") def mi(a, m): return (a - 2012) * 12 + m - 1 def mname(i): return f"{2012 + i // 12}-{i % 12 + 1:02d}" MAXMI = mi(2026, 9) # ---------- catálogo de fechas (para «normas vecinas») NORMAS = {'L02a': mi(2014, 12), 'L02b': mi(2015, 12), 'L03': mi(2016, 6), 'L05': mi(2018, 3), 'L06': mi(2018, 1), 'L07': mi(2018, 7), 'L08': mi(2019, 2), 'L09': mi(2019, 3), 'L10': mi(2020, 1), 'L11': mi(2020, 2), 'L12': mi(2020, 3), 'L14': mi(2021, 1), 'L15': mi(2022, 1), 'L18': mi(2023, 3), 'L19': mi(2024, 1), 'L20': mi(2026, 1), 'L01': mi(2011, 12), 'L04': mi(2016, 6), 'L13a': mi(2020, 4), 'L13b': mi(2020, 9), 'L16': mi(2022, 1), 'L23': mi(2022, 4), 'L24': mi(2019, 1)} def vecinas(cod, d): return ' '.join(sorted(k for k, v in NORMAS.items() if k != cod and abs(v - d) <= OFF and not (k[:3] == cod[:3]))) # ---------- series # tabla, num, den (None = recuento), reloj (solo nm) SER = {'M_N': ('m', 'n', None), 'M_EUR': ('m', 'eur', 'n_imp*'), 'M_FRANJA': ('m', 'fr_num', 'fr_den'), 'M_ALTO': ('m', 'alto_num', 'alto_den'), 'M_FRANJA50': ('m', 'f50_num', 'f50_den'), 'M_N2': ('m', 'n2_num', 'n2_den'), 'M_REP': ('m', 'rep_num', 'rep_den'), 'M_1183': ('m', 't_num', 't_den'), 'M_1183r': ('m', 't2_num', 't2_den'), 'NM_N': ('n', 'n', None, 'adj'), 'NM_OU': ('n', 'ou_num', 'ou_den', 'pub'), 'NM_URG': ('n', 'urg_num', 'urg_den', 'pub'), 'NM_EMER': ('n', 'emer_num', 'emer_den', 'adj'), 'NM_NSP': ('n', 'nsp_num', 'nsp_den', 'adj'), 'NM_SIMPL': ('n', 'simpl_num', 'simpl_den', 'pub'), 'NM_MOD': ('n', 'mod_num', 'mod_den', 'adj'), 'NM_DOUE': ('b', 'ted', 'lotes', 'pub'), 'NM_SARA': ('b', 'sara', 'sara_no_nulo', 'pub')} def es_recuento(s): return SER[s][2] is None or SER[s][2].endswith('*') def where_org(f): """filtro de órganos: dict con claves secti/salud/age/cat (True/False) -> SQL sobre org""" if not f: return "TRUE" return ' AND '.join(f"o.{k} = {'TRUE' if v else 'FALSE'}" for k, v in f.items()) def arm_men(s, d, grupo, f=None, lo=None, hi=None): """menores: elige fuente por (órgano, grupo) en [d-18, d+18] (o [lo, hi]); devuelve dict org -> (37,2) [num, den]""" lo = d - OFF if lo is None else lo; hi = d + OFF if hi is None else hi num = SER[s][1]; den = SER[s][2] den_sql = '0' if den is None else den.rstrip('*') q = f"""WITH c AS (SELECT cm.* FROM cm JOIN org o USING (org) WHERE grupo='{grupo}' AND mi BETWEEN {lo} AND {hi} AND {where_org(f)}), el AS (SELECT org, arg_min(source, (-n, CASE WHEN source='PLACSP' THEN 0 ELSE 1 END, source)) fuente FROM (SELECT org, source, sum(n) n FROM c GROUP BY ALL) GROUP BY 1) SELECT c.org, c.mi - {lo} k, sum({num})::DOUBLE, sum({den_sql})::DOUBLE FROM c JOIN el ON el.org=c.org AND el.fuente=c.source GROUP BY ALL""" return to_arr(x(q).fetchall(), hi - lo + 1) def arm_nm(s, d, grupo, f=None, ue=None, reloj=None, lo=None, hi=None): lo = d - OFF if lo is None else lo; hi = d + OFF if hi is None else hi num = SER[s][1]; den = SER[s][2]; rel = reloj or SER[s][3] g = "grupo IN ('SS','SSI')" if grupo == 'SS+SSI' else f"grupo='{grupo}'" u = '' if ue is None else f" AND ue = {'TRUE' if ue else 'FALSE'}" q = f"""SELECT cn.org, cn.mi - {lo} k, sum({num})::DOUBLE, sum({'0' if den is None else den})::DOUBLE FROM cn JOIN org o USING (org) WHERE {g} AND reloj='{rel}' AND mi BETWEEN {lo} AND {hi} AND {where_org(f)}{u} GROUP BY ALL""" return to_arr(x(q).fetchall(), hi - lo + 1) def arm_banda(d, ua, ub, siempre, sara=False): """lotes SS de la banda que cambia (siempre=False) o siempre SARA; num = TED (o sara), den = lotes (o sara no nulo)""" lo, hi = d - OFF, d + OFF ma, Ma = min(ua), max(ua); mb, Mb = min(ub), max(ub) if siempre: ca, cb = f"ve >= {Ma} AND ve < {1.5*Ma}", f"ve >= {Mb} AND ve < {1.5*Mb}" else: ca, cb = f"ve >= {ma} AND ve < {Ma}", f"ve >= {mb} AND ve < {Mb}" if sara: nd = "count(*) FILTER (WHERE sara)::DOUBLE, count(sara)::DOUBLE" else: nd = "count(*) FILTER (WHERE ted)::DOUBLE, count(*)::DOUBLE" q = f"""SELECT org, mi - {lo} k, {nd} FROM ls WHERE mi BETWEEN {lo} AND {hi} AND ((age AND {ca}) OR (NOT age AND {cb})) GROUP BY ALL""" return to_arr(x(q).fetchall(), hi - lo + 1) def to_arr(rows, L): out = {} for o, k, a, b in rows: if o not in out: out[o] = np.zeros((L, 2)) out[o][int(k), 0] += a; out[o][int(k), 1] += b return out def panel(arm, recuento, semestres): """celdas con >= 1 unidad del denominador (o del recuento) en cada semestre (lista de (a, b) índices inclusive)""" keep = {} j = 0 if recuento else 1 for o, A in arm.items(): if all(A[a:b + 1, j].sum() > 0 for a, b in semestres): keep[o] = A return keep SEMS = [(0, 5), (6, 11), (12, 17), (19, 24), (25, 30), (31, 36)] # índices relativos a d-18 (d = 18 fuera) def ventanas(cen): """índices (antes, después) para centro cen (relativo a d): antes [cen-12, cen-1], después [cen+1, cen+12]""" b = list(range(OFF + cen - 12, OFF + cen)); a = list(range(OFF + cen + 1, OFF + cen + 13)); return b, a def stats_from(S, recuento, wins): """S: (Bn, L, 2) sumas ponderadas. wins: lista de (b, a). Devuelve lista de (Rb, Ra, Δ) arrays (Bn,)""" out = [] for b, a in wins: nb, db = S[:, b, 0].sum(1), S[:, b, 1].sum(1) na, da = S[:, a, 0].sum(1), S[:, a, 1].sum(1) with np.errstate(divide='ignore', invalid='ignore'): Rb = nb if recuento else nb / db Ra = na if recuento else na / da D = np.log(Ra) - np.log(Rb) D[~np.isfinite(D)] = np.nan out.append((Rb, Ra, D, db if db.sum() > 0 else nb, da if da.sum() > 0 else na)) return out def boot(arms, same, recuento_list, wins): """arms: lista de dict org->(L,2). same: un solo estrato (mismos órganos) o un estrato por brazo. Devuelve por brazo: (punto, bootstrap) listas de stats por ventana.""" orgs = sorted(set().union(*[a.keys() for a in arms])) idx = {o: i for i, o in enumerate(orgs)}; n = len(orgs) L = next(iter(arms[0].values())).shape[0] if arms[0] else 37 mats = [] for a in arms: M = np.zeros((n, L, 2)) for o, A in a.items(): M[idx[o]] = A mats.append(M.reshape(n, -1)) if same: estr = np.zeros(n, int) else: estr = np.full(n, -1) for k, a in enumerate(arms): for o in a: estr[idx[o]] = k rng = np.random.default_rng(SEM) W = np.zeros((B, n)) for s in sorted(set(estr.tolist())): ix = np.where(estr == s)[0] W[:, ix] = rng.multinomial(len(ix), np.full(len(ix), 1 / len(ix)), size=B) res = [] for M, rec in zip(mats, recuento_list): P = M.sum(0).reshape(1, L, 2); S = (W @ M).reshape(B, L, 2) res.append((stats_from(P, rec, wins), stats_from(S, rec, wins))) return res def ic(v): v = np.asarray(v, float); v = v[np.isfinite(v)] return [float(np.percentile(v, 2.5)), float(np.percentile(v, 97.5))] if len(v) > 50 else [float('nan')] * 2 LN115, LN125 = math.log(1.15), math.log(1.25) VOLCAR = {'L05|M_1183|SS', 'L05|M_REP|SS', 'L11|M_REP|SS vs SSI sin salud', 'L12|NM_EMER|SS', 'L12|NM_EMER|O'}; VOL = {} FILAS = [] def veredicto(D, Dic, K, Kic, medible, especial=None): if not medible: return 'no medible' if especial is not None: return especial if any(map(math.isnan, Dic + Kic)): return 'no concluyente' if (Dic[0] > 0 or Dic[1] < 0) and (Kic[0] > 0 or Kic[1] < 0) and np.sign(Dic[0]) == np.sign(Kic[0]) and np.sign(Dic[0]) == np.sign(Dic[1]): return 'efecto claro' if abs(D) >= LN125 else 'efecto' if Kic[0] <= 0 <= Kic[1] and Dic[0] > -LN115 and Dic[1] < LN115: return 'sin efecto' return 'no concluyente' def direccion(D, hip): if hip in ('?', None) or not np.isfinite(D): return '' if hip == '≈': return '' s = '↑' if D > 0 else '↓' return 'esperada' if (hip.startswith(s)) else 'contraria' def evento(cod, d, serie, grupo, hip, armT, armC=None, same=False, primaria=False, nota='', sin_panel=False, reloj=''): """aplica §4-§5 y añade una fila""" rec = es_recuento(serie) if not sin_panel: armT = panel(armT, rec, SEMS) if armC is not None: armC = panel(armC, rec, SEMS) elif armC is not None and nota.startswith('T sin equilibrar'): armC = panel(armC, rec, SEMS); armT = {o: A for o, A in armT.items() if o in armC} wins = [ventanas(0), ventanas(-6), ventanas(6)] arms = [armT] + ([armC] if armC is not None else []) nT = len(armT); nC = len(armC) if armC is not None else None if nT == 0 or (armC is not None and nC == 0): FILAS.append(dict(norma=cod, d=mname(d), serie=serie, grupo=grupo, diseno='DiD' if armC is not None else 'Δ', hipotesis=hip, n_org_T=nT, n_org_C=nC, veredicto='no medible', primaria=primaria, vecinas=vecinas(cod, d), nota=nota + ' panel vacío', reloj=reloj)); return res = boot(arms, same, [rec] * len(arms), wins) clave = f'{cod}|{serie}|{grupo}' if clave in VOLCAR: VOL[clave] = {k: {o: [[float(A[w[0]].sum(0)[j]) for w in wins for j in (0, 1)], [float(A[w[1]].sum(0)[j]) for w in wins for j in (0, 1)]] for o, A in arm.items()} for k, arm in zip(('T', 'C'), arms)} (pT, bT) = res[0] medible = nT >= 20 and min(pT[0][3][0], pT[0][4][0]) >= 200 row = dict(norma=cod, d=mname(d), serie=serie, grupo=grupo, diseno='DiD' if armC is not None else 'Δ', hipotesis=hip, n_org_T=nT, n_org_C=nC, den_antes_T=float(pT[0][3][0]), den_despues_T=float(pT[0][4][0]), R_antes_T=float(pT[0][0][0]), R_despues_T=float(pT[0][1][0])) DT = [p[2][0] for p in pT]; bDT = [b_[2] for b_ in bT] row.update(delta_T=DT[0], delta_T_ic=ic(bDT[0]), placebo_menos6_T=DT[1], placebo_mas6_T=DT[2]) if armC is not None: (pC, bC) = res[1] medible = medible and nC >= 20 and min(pC[0][3][0], pC[0][4][0]) >= 200 DC = [p[2][0] for p in pC]; bDC = [b_[2] for b_ in bC] row.update(den_antes_C=float(pC[0][3][0]), den_despues_C=float(pC[0][4][0]), R_antes_C=float(pC[0][0][0]), R_despues_C=float(pC[0][1][0]), delta_C=DC[0], delta_C_ic=ic(bDC[0]), placebo_menos6_C=DC[1], placebo_mas6_C=DC[2]) Ds = [DT[i] - DC[i] for i in range(3)]; bDs = [bDT[i] - bDC[i] for i in range(3)] else: Ds = DT; bDs = bDT D = Ds[0]; K = Ds[0] - 0.5 * (Ds[1] + Ds[2]); bK = bDs[0] - 0.5 * (bDs[1] + bDs[2]) Dic = ic(bDs[0]); Kic = ic(bK) row.update(delta=D, cambio_pct=100 * (math.exp(D) - 1) if np.isfinite(D) else float('nan'), delta_ic=Dic, cambio_pct_ic=[100 * (math.exp(v) - 1) for v in Dic], delta_placebo_menos6=Ds[1], delta_placebo_mas6=Ds[2], K=K, K_ic=Kic, replicas_descartadas=int(np.sum(~np.isfinite(bDs[0])))) row['veredicto'] = veredicto(D, Dic, K, Kic, medible) row['direccion'] = direccion(D, hip) if row['veredicto'].startswith('efecto') else '' row.update(primaria=primaria, vecinas=vecinas(cod, d), nota=nota, reloj=reloj) FILAS.append(row) log(cod, serie, grupo, row['diseno'], f"nT={nT} nC={nC} Δ={D:+.3f} {Dic} K={K:+.3f} {Kic} -> {row['veredicto']} {row['direccion']}") def evento_L07(): """§5.2: ventanas especiales, DiD SSI - SS, placebo de calendario +12""" for serie, prim in (('M_ALTO', True), ('M_FRANJA50', False)): out = {} for nombre, sh in (('real', 0), ('placebo+12', 12)): b = list(range(mi(2018, 4) + sh, mi(2018, 6) + sh + 1)); a = list(range(mi(2018, 8) + sh, mi(2019, 1) + sh + 1)) lo, hi = b[0], a[-1] arms = [] for g in ('SSI', 'SS'): A = arm_men(serie, None, g, lo=lo, hi=hi) bb = [i - lo for i in b]; aa = [i - lo for i in a] A = {o: M for o, M in A.items() if M[bb, 1].sum() > 0 and M[aa, 1].sum() > 0} arms.append(A) bb = [i - lo for i in b]; aa = [i - lo for i in a] res = boot(arms, False, [False, False], [(bb, aa)]) (pT, bT), (pC, bC) = res D = pT[0][2][0] - pC[0][2][0]; bD = bT[0][2] - bC[0][2] out[nombre] = dict(D=D, ic=ic(bD), nT=len(arms[0]), nC=len(arms[1]), denT=(pT[0][3][0], pT[0][4][0]), denC=(pC[0][3][0], pC[0][4][0]), RT=(pT[0][0][0], pT[0][1][0]), RC=(pC[0][0][0], pC[0][1][0]), DT=pT[0][2][0], DC=pC[0][2][0], DTic=ic(bT[0][2]), DCic=ic(bC[0][2])) r, p = out['real'], out['placebo+12'] medible = r['nT'] >= 20 and r['nC'] >= 20 and min(*r['denT'], *r['denC']) >= 200 if not medible: v = 'no medible' elif (r['ic'][0] > 0 or r['ic'][1] < 0) and ((p['ic'][0] <= 0 <= p['ic'][1]) or abs(p['D']) < abs(r['D']) / 2): v = 'efecto claro' if abs(r['D']) >= LN125 else 'efecto' elif r['ic'][0] <= 0 <= r['ic'][1] and r['ic'][0] > -LN115 and r['ic'][1] < LN115: v = 'sin efecto' else: v = 'no concluyente' FILAS.append(dict(norma='L07', d='2018-07', serie=serie, grupo='SSI vs SS', diseno='DiD (ventanas §5.2)', hipotesis='↑', n_org_T=r['nT'], n_org_C=r['nC'], den_antes_T=r['denT'][0], den_despues_T=r['denT'][1], R_antes_T=r['RT'][0], R_despues_T=r['RT'][1], delta_T=r['DT'], delta_T_ic=r['DTic'], den_antes_C=r['denC'][0], den_despues_C=r['denC'][1], R_antes_C=r['RC'][0], R_despues_C=r['RC'][1], delta_C=r['DC'], delta_C_ic=r['DCic'], delta=r['D'], cambio_pct=100 * (math.exp(r['D']) - 1), delta_ic=r['ic'], cambio_pct_ic=[100 * (math.exp(v_) - 1) for v_ in r['ic']], delta_placebo_mas12=p['D'], delta_placebo_mas12_ic=p['ic'], veredicto=v, direccion=direccion(r['D'], '↑') if v.startswith('efecto') else '', primaria=prim, vecinas='L05 L08', nota='antes abr-jun 2018, después ago-2018 a ene-2019; placebo +12 meses; sin K')) log('L07', serie, out) # =================== EVENTOS (§2, §5.3) =================== d = mi(2014, 12) for g in ('SS', 'O'): evento('L02a', d, 'M_N', g, '↑', arm_men('M_N', d, g)) d = mi(2015, 12) for g in ('SS', 'O'): evento('L02b', d, 'M_N', g, '↑', arm_men('M_N', d, g), primaria=(g == 'SS')) d = mi(2016, 6) for g in ('SS', 'O'): evento('L03', d, 'NM_NSP', g, '↓', arm_nm('NM_NSP', d, g, {'cat': True}), arm_nm('NM_NSP', d, g, {'cat': False}), primaria=(g == 'SS'), nota='T Cataluña, C resto', reloj='adj') d = mi(2018, 3) H05 = {'M_N': '↑', 'M_EUR': '?', 'M_FRANJA': '≈', 'M_REP': '↓', 'M_1183': '↓', 'M_N2': '↓', 'NM_N': '↑', 'NM_OU': '↑', 'NM_URG': '≈', 'NM_SIMPL': '↑'} for g in ('SS', 'O'): for s in ('M_N', 'M_EUR', 'M_FRANJA', 'M_REP', 'M_1183', 'M_N2'): evento('L05', d, s, g, H05[s], arm_men(s, d, g), primaria=(s == 'M_1183' and g == 'SS')) for s in ('NM_N', 'NM_OU', 'NM_URG', 'NM_SIMPL'): evento('L05', d, s, g, H05[s], arm_nm(s, d, g), reloj=SER[s][3]) evento('L05', d, 'NM_NSP', g, '↓', arm_nm('NM_NSP', d, g, {'cat': False}), arm_nm('NM_NSP', d, g, {'cat': True}), nota='T resto, C Cataluña', reloj='adj') # SARA for cod, dd, ua, ub, hip, prim in (('L06', mi(2018, 1), (135000, 144000), (209000, 221000), '↓', True), ('L10', mi(2020, 1), (144000, 139000), (221000, 214000), '↑', True), ('L19', mi(2024, 1), (140000, 143000), (215000, 221000), '↓', True)): evento(cod, dd, 'NM_DOUE', 'SS+SSI', hip, arm_banda(dd, ua, ub, False), arm_banda(dd, ua, ub, True), same=True, primaria=prim, sin_panel=True, nota='T banda que cambia, C banda siempre SARA; todos los órganos', reloj='pub') if cod == 'L19': evento(cod, dd, 'NM_SARA', 'SS+SSI', hip, arm_banda(dd, ua, ub, False, sara=True), arm_banda(dd, ua, ub, True, sara=True), same=True, sin_panel=True, nota='marca sara de la PLACSP; T banda que cambia, C siempre SARA', reloj='pub') evento_L07() d = mi(2019, 2) for s in ('M_REP', 'M_1183'): evento('L08', d, s, 'SSI vs SS', '↑', arm_men(s, d, 'SSI'), arm_men(s, d, 'SS'), primaria=(s == 'M_REP'), nota='T SSI, C SS') d = mi(2019, 3) for g in ('SS', 'O'): for s in ('M_REP', 'M_1183'): evento('L09', d, s, g, '↑', arm_men(s, d, g, {'secti': False}), primaria=(s == 'M_REP' and g == 'SS'), nota='sin SECTI') d = mi(2020, 2) for s in ('M_REP', 'M_1183'): evento('L11', d, s, 'SS vs SSI sin salud', '↑', arm_men(s, d, 'SS'), arm_men(s, d, 'SSI', {'salud': False}), primaria=(s == 'M_REP'), nota='T SS, C SSI sin salud') evento('L11', d, s, 'SS vs SSI', '↑', arm_men(s, d, 'SS'), arm_men(s, d, 'SSI'), nota='sensibilidad: C SSI entero') evento('L11', d, s, 'O', '↑', arm_men(s, d, 'O', {'secti': False}), nota='obras sin SECTI, sin contraste') d = mi(2020, 3) H12 = {'NM_EMER': '↑', 'NM_N': '↓', 'NM_URG': '↑', 'NM_OU': '↑', 'M_N': '↓', 'M_EUR': '↓'} for g in ('SS', 'O'): for s in ('NM_EMER', 'NM_N', 'NM_URG', 'NM_OU'): evento('L12', d, s, g, H12[s], arm_nm(s, d, g), primaria=(s == 'NM_EMER' and g == 'SS'), reloj=SER[s][3]) for s in ('M_N', 'M_EUR'): evento('L12', d, s, g, H12[s], arm_men(s, d, g)) evento('L12', d, 'NM_EMER', 'SS AGE vs no AGE', '↑', arm_nm('NM_EMER', d, 'SS', {'age': True}), arm_nm('NM_EMER', d, 'SS', {'age': False}), nota='secundario: T AGE, C no AGE', reloj='adj') d = mi(2021, 1) for g in ('SS', 'O'): for s in ('NM_URG', 'NM_SIMPL', 'NM_OU'): evento('L14', d, s, g, '↑', arm_nm(s, d, g, ue=True), arm_nm(s, d, g, ue=False), same=True, sin_panel=True, primaria=(s == 'NM_URG' and g == 'SS'), nota='T sin equilibrar: financiación UE; C sin UE (equilibrado), mismos órganos', reloj=SER[s][3]) d = mi(2023, 3) for g in ('SS', 'O'): for s in ('NM_OU', 'NM_N', 'NM_MOD'): evento('L18', d, s, g, '≈', arm_nm(s, d, g), primaria=(s == 'NM_OU' and g == 'SS'), nota='control negativo', reloj=SER[s][3]) for s in ('M_N', 'M_REP'): evento('L18', d, s, g, '≈', arm_men(s, d, g), nota='control negativo') # =================== POSTERIOR (enmienda 3): exploratorio, no cambia ninguna primaria =================== d = mi(2018, 3) for g in ('SS', 'O'): evento('L05', d, 'M_1183r', g, '↓', arm_men('M_1183r', d, g), nota='POSTERIOR: solo menores bajo el tope (sin la parte mecánica del umbral viejo)') evento('L05', d, 'NM_NSP', g, '↓', arm_nm('NM_NSP', d, g), nota='POSTERIOR: sin contraste (el catalán no es medible)', reloj='adj') d = mi(2019, 3) for g in ('SS', 'O'): evento('L09', d, 'M_1183r', g, '↑', arm_men('M_1183r', d, g, {'secti': False}), nota='POSTERIOR: solo menores bajo el tope; sin SECTI') d = mi(2020, 2) evento('L11', d, 'M_1183r', 'SS vs SSI sin salud', '↑', arm_men('M_1183r', d, 'SS'), arm_men('M_1183r', d, 'SSI', {'salud': False}), nota='POSTERIOR: solo menores bajo el tope') for s in ('M_REP', 'M_1183', 'M_1183r'): evento('L11', d, s, 'SS', '↑', arm_men(s, d, 'SS'), nota='POSTERIOR: tratado sin contraste (Δ y K)') d = mi(2016, 6) for g in ('SS', 'O'): evento('L03', d, 'NM_NSP', g, '↓', arm_nm('NM_NSP', d, g, {'cat': True}), nota='POSTERIOR: Cataluña sin contraste', reloj='adj') # =================== salida =================== keys = [] for r in FILAS: for k in r: if k not in keys: keys.append(k) def fmt(v): if isinstance(v, float): return '' if math.isnan(v) else f"{v:.6g}" if isinstance(v, list): return '[' + ';'.join(fmt(z) for z in v) + ']' return v with open('/w/salidas/eventos.csv', 'w', newline='') as f: w = csv.DictWriter(f, fieldnames=keys); w.writeheader() for r in FILAS: w.writerow({k: fmt(r.get(k, '')) for k in keys}) json.dump(FILAS, open('/w/salidas/20_eventos.json', 'w'), ensure_ascii=False, indent=1, default=float) json.dump(VOL, open('/w/interno/impl1_celdas.json', 'w')) log('filas', len(FILAS))