"""E8 · datos de las maquetas (sin red; solo agregados: nunca un contrato suelto ni una empresa). Lee las salidas selladas y escribe /p/maquetas/datos/precios.json: curvas oficiales mensuales, precio pagado por trimestre (n ≥ 3), razón por año con IC (bootstrap 2.000, semilla 20261027; n ≥ 5), cobertura, y los hechos por órgano (N ≥ 10, mercados que pasan).""" import csv, json, random, statistics as st, collections, os P = '/p' PROV = {'01': 'Araba/Álava', '02': 'Albacete', '03': 'Alicante', '04': 'Almería', '05': 'Ávila', '06': 'Badajoz', '07': 'Illes Balears', '08': 'Barcelona', '09': 'Burgos', '10': 'Cáceres', '11': 'Cádiz', '12': 'Castellón', '13': 'Ciudad Real', '14': 'Córdoba', '15': 'A Coruña', '16': 'Cuenca', '17': 'Girona', '18': 'Granada', '19': 'Guadalajara', '20': 'Gipuzkoa', '21': 'Huelva', '22': 'Huesca', '23': 'Jaén', '24': 'León', '25': 'Lleida', '26': 'La Rioja', '27': 'Lugo', '28': 'Madrid', '29': 'Málaga', '30': 'Murcia', '31': 'Navarra', '32': 'Ourense', '33': 'Asturias', '34': 'Palencia', '36': 'Pontevedra', '37': 'Salamanca', '39': 'Cantabria', '40': 'Segovia', '41': 'Sevilla', '42': 'Soria', '43': 'Tarragona', '44': 'Teruel', '45': 'Toledo', '46': 'Valencia', '47': 'Valladolid', '48': 'Bizkaia', '49': 'Zamora', '50': 'Zaragoza'} R = json.load(open(f'{P}/salidas/e8_resumen_impl1.json')); V = json.load(open(f'{P}/salidas/e8_veredicto.json')) C = [r for r in csv.DictReader(open(f'{P}/priv/e8_contratos_impl1.csv')) if r['estado'] == 'valido' and r['grupo_referencia'] != 'True'] rnd = random.Random(20261027) def ic(xs, B=2000): n = len(xs); meds = sorted(st.median([xs[rnd.randrange(n)] for _ in range(n)]) for _ in range(B)) return [round(meds[int(0.025 * B)], 4), round(meds[int(0.975 * B) - 1], 4)] def trimestre(mes): return f"{mes[:4]}-T{(int(mes[5:7]) - 1) // 3 + 1}" def bloque(filas, clave_prod): """Agregados por producto × provincia ('00' = todas): trimestres con n ≥ 3 y años con n ≥ 5.""" out = {} grupos = collections.defaultdict(list) for r in filas: p = clave_prod(r) for pv in ('00', r['prov'] or 'sd'): # '00' = todas; 'sd' = provincia desconocida (solo cuenta en '00') grupos[(p, pv)].append(r) for (p, pv), rs in grupos.items(): if pv == 'sd': continue tri = collections.defaultdict(list); ani = collections.defaultdict(list) for r in rs: tri[trimestre(r['mes'])].append(r); ani[r['mes'][:4]].append(float(r['razon'])) xs = [float(r['razon']) for r in rs] out.setdefault(p, {})[pv] = dict( n=len(rs), mediana=round(st.median(xs), 6), ic=ic(xs) if len(xs) >= 5 else None, pct_encima=round(100 * sum(x > 1.15 for x in xs) / len(xs), 1), trimestres=[[t, len(v), round(st.median(float(x['precio_unitario']) for x in v), 4), round(st.median(float(x['razon']) for x in v), 4)] for t, v in sorted(tri.items()) if len(v) >= 3], anios=[[a, len(v), round(st.median(v), 4), ic(v), round(100 * sum(x > 1.15 for x in v) / len(v), 1)] for a, v in sorted(ani.items()) if len(v) >= 5]) return out carb = [r for r in C if r['mercado'] == 'carburantes'] pap = [r for r in C if r['mercado'] == 'papel'] ref = collections.defaultdict(lambda: collections.defaultdict(list)) for r in csv.DictReader(open(f'{P}/salidas/ref_carb_mensual.csv')): ref[r['producto']][r['prov_ine']].append([r['mes'], round(float(r['ref_sin_iva_eur_l']), 4)]) am = [dict(am=r['am'], lote=int(r['lote']), fibra=r['fibra'], gramaje=int(r['gramaje']), desde=r['vigencia_desde'], hasta=r['vigencia_hasta'], mediana=float(r['mediana_eur_caja']), minimo=float(r['minimo_eur_caja']), n=int(r['n_adjudicatarios'])) for r in csv.DictReader(open(f'{P}/salidas/ref_papel_am.csv'))] lote = lambda r: 'lote2' if r['producto'] == 'reciclado' else 'lote1' org = [dict(mercado=o['mercado'], producto=o['producto'], organo=o['organo_nombre'], tipo=o['tipo_organo'], n=int(o['n']), k=int(o['k_por_encima']), p0=round(float(o['p0_resto']), 4), p=float(o['p_valor']), mediana=round(float(o['mediana']), 4), ic=[round(float(o['ic95_lo']), 4), round(float(o['ic95_hi']), 4)], hecho=o['hecho']) for o in csv.DictReader(open(f'{P}/salidas/e8_organos_impl1.csv'))] # años de las compras de cada unidad (para la base de la frase) anios_org = collections.defaultdict(list) for r in C: anios_org[(r['mercado'], r['organo_nombre'], r['producto'] if r['mercado'] == 'carburantes' else 'todos')].append(int(r['mes'][:4])) razones = collections.defaultdict(list) for r in C: razones[(r['mercado'], r['producto'] if r['mercado'] == 'carburantes' else 'todos')].append((r['organo_nombre'], float(r['razon']))) for o in org: a = anios_org[(o['mercado'], o['organo'], o['producto'])]; o['anios'] = [min(a), max(a)] if a else None resto = [x for nm, x in razones[(o['mercado'], o['producto'])] if nm != o['organo']] o['resto_n'] = len(resto); o['resto_mediana'] = round(st.median(resto), 4) if resto else None o['unidades_evaluables'] = sum(1 for u in org if u['mercado'] == o['mercado']) cob = {m: dict(universo=R[m]['universo'], validos=R[m]['validos'], pct=round(R[m]['cobertura_pct'], 2), pct_importe=round(R[m]['cobertura_importe_pct'], 2), universo_meur=round(R[m]['universo_importe'] / 1e6, 1), validos_meur=round(R[m]['validos_importe'] / 1e6, 2), veredicto=V[m]['veredicto'], mediana=round(R[m]['todos']['mediana'], 6), ic=[round(x, 6) for x in R[m]['todos']['ic95']], pct_encima=round(R[m]['todos']['pct_por_encima'], 1), ic_pct=[round(x, 1) for x in R[m]['todos']['ic95_pct_por_encima']], p10=round(R[m]['todos']['p10'], 3), p90=round(R[m]['todos']['p90'], 3), dentro15=round(R[m]['todos']['pct_dentro_15'], 1)) for m in R} cob['papel']['grupo_referencia'] = {k: (round(v, 4) if isinstance(v, float) else v) for k, v in R['papel']['grupo_referencia'].items() if k in ('n', 'mediana', 'dentro_085_110_pct')} out = dict(fecha_datos='2026-10-25', construccion='unified_20261025', md5_prerregistro=open(f'{P}/DEFINICIONES_PREVIAS_E8.md5').read().split()[0], provincias=PROV, cobertura=cob, carburantes=dict(referencia={p: dict(v) for p, v in ref.items()}, pagado=bloque(carb, lambda r: r['producto'])), papel=dict(referencia=am, pagado=bloque(pap, lote)), organos=[o for o in org if V[o['mercado']]['veredicto'] == 'PASA']) os.makedirs(f'{P}/maquetas/datos', exist_ok=True) json.dump(out, open(f'{P}/maquetas/datos/precios.json', 'w'), ensure_ascii=False, separators=(',', ':')) print('bytes', os.path.getsize(f'{P}/maquetas/datos/precios.json'), 'carb productos', list(out['carburantes']['pagado']), 'provincias con datos', {p: len(v) for p, v in out['carburantes']['pagado'].items()}, 'papel', {p: len(v) for p, v in out['papel']['pagado'].items()})