Ir al contenido

Para desarrolladores

Todo lo que hace el comando urg-forecast son unas pocas funciones sobre DataFrames. Esta página va de lo más simple a lo más detallado: cada función del paquete urgencias_core, desde instalarlo hasta comparar modelos, con su explicación a la izquierda y un ejemplo con su salida a la derecha; al final, las recetas para extenderlo con tu propio modelo o una variable externa. El mapa del paquete está en Arquitectura y hay scripts completos en Ejemplos.

Primeros pasos

Todo el paquete trabaja con DataFrames de pandas. Los ejemplos de esta página se ejecutan tal cual, en orden, con el snapshot del DEIS que trae el paquete (offline=True: Hospital de Puerto Montt y Hospital de Frutillar), así que puedes copiarlos y obtener lo mismo. Sin offline=True leen la copia semanal del repositorio, con cualquier establecimiento.

Instalar

Python 3.11 o 3.12. Instala el paquete desde PyPI; trae el comando urg-forecast y la biblioteca urgencias_core. Las funciones de uso común se importan directo desde urgencias_core.

# pip install urg-forecast-core
import urgencias_core
print(urgencias_core.__version__)
Salida
0.4.0

La serie semanal

Casi todo recibe la misma entrada: un DataFrame con timestamp (el lunes que cierra cada semana DEIS: los 7 días que terminan ese lunes) y count (atenciones). Solo semanas completas; 2020 y 2021 quedan fuera por la pandemia. Si traes tus propios datos, arma ese mismo DataFrame y todo lo demás funciona igual.

from urgencias_core import load_deis, weekly_series
df = load_deis(["24-105"], offline=True)
semanal = weekly_series(df, "24-105")
print(semanal.tail(3))
Salida
timestamp count
247 2026-10-05 1966
248 2026-10-12 1986
249 2026-10-19 2068

Datos

De dónde vienen las series: el DEIS (copia semanal del repositorio, archivos completos o snapshot incluido) y el listado de establecimientos.

load_deis

load_deis(
    codes: list[str],
    start_year: int = 2022,
    offline: bool = False,
    fallback_to_snapshot: bool = False,
    cache_dir: Path | str = Path("data/external/deis_cache"),
    source: str = "repo",
)

urgencias_core.data.series · ver el código

Filas DEIS (diarias, por causa y grupo de edad) de uno o más establecimientos. Acepta el código en cualquiera de sus formas (24-105 o 124105). La fuente usada queda en df.attrs['source'].

codes
lista de códigos DEIS
start_year
primer año (por defecto 2022; 2020-2021 siempre se excluyen)
offline
usar el snapshot incluido, sin red
source
"repo" (copia semanal, liviana) o "deis" (archivos completos)
cache_dir
dónde guardar las descargas

Devuelve: DataFrame con date, facility_code, facility_name, cause_group, age_group, count. Lanza NoDataError si no hay datos.

df = load_deis(["24-105"], offline=True)
print(df.attrs["source"])
print(len(df), "filas", list(df.columns))
Salida
snapshot DEIS incluido en el paquete
70760 filas ['year', 'date', 'facility_code', 'facility_name', 'cause_id', 'cause_group', 'age_group', 'count']

weekly_series

weekly_series(df: pd.DataFrame, code: str)

urgencias_core.data.series · ver el código

Las atenciones totales de un establecimiento por semana DEIS. Recorta las semanas incompletas de los extremos: la última semana suele llegar con días faltantes y parecería una caída.

df
salida de load_deis
code
código DEIS

Devuelve: DataFrame timestamp, count.

semanal = weekly_series(df, "24-105")
print(len(semanal), "semanas")
Salida
250 semanas

daily_series

daily_series(df: pd.DataFrame, code: str)

urgencias_core.data.series · ver el código

Lo mismo por día. Sirve para el perfil por día de la semana.

df
salida de load_deis
code
código DEIS

Devuelve: DataFrame timestamp, count.

from urgencias_core.data.series import daily_series
diaria = daily_series(df, "24-105")
print(diaria.tail(2))
Salida
timestamp count
1759 2026-10-28 296
1760 2026-10-29 297

facility_code_variants

facility_code_variants(code: str)

urgencias_core.data.deis · ver el código

Las formas de un mismo código. El DEIS publica los establecimientos más antiguos con el código SS-NNN (los dos primeros dígitos indicaban el Servicio de Salud) y también como 1SSNNN; los creados después tienen solo un código de 6 dígitos. Todas las funciones aceptan cualquiera de las formas.

code
código DEIS

Devuelve: conjunto de formas equivalentes.

from urgencias_core.data.deis import facility_code_variants
print(sorted(facility_code_variants("24-105")))
Salida
['124105', '24-105']

mirror_facilities

mirror_facilities(
    cache_dir: Path | str = Path("data/external/deis_cache"),
    base_url: str = "https://raw.githubusercontent.com/nicoveraz/urg-forecast-core/datos/",
    max_age_hours: float = 12,
)

urgencias_core.data.mirror · ver el código

El listado de establecimientos de la copia semanal: código, nombre, tipo (Hospital, SAPU, SAR, SUR…), región, comuna, servicio de salud y fechas con datos. Es lo que usa urg-forecast buscar. Necesita red.

Devuelve: DataFrame con una fila por establecimiento.

from urgencias_core.data.mirror import mirror_facilities
est = mirror_facilities()
print(est[est["comuna"] == "Puerto Montt"][["codigo", "nombre", "tipo"]])

Este ejemplo necesita red, así que no se ejecuta al generar la página.

Análisis

Cómo ha sido la demanda, antes de pronosticar. Funciones puras: reciben la serie semanal y devuelven un DataFrame. Es lo que hace urg-forecast analizar.

yearly_summary

yearly_summary(weekly: pd.DataFrame)

urgencias_core.analysis · ver el código

Una fila por año. El cambio compara las mismas semanas del año anterior, así un año incompleto (enero a marzo) se compara con enero a marzo y no con un año completo.

weekly
serie semanal

Devuelve: semanas, complete, total, promedio semanal, change_pct, semana más alta.

from urgencias_core.analysis import yearly_summary
print(yearly_summary(semanal)[["weeks", "weekly_mean", "change_pct"]])
Salida
weeks weekly_mean change_pct
year
2022 51 1308.1 NaN
2023 52 1557.1 19.6
2024 53 1601.1 3.0
2025 52 1640.1 2.2
2026 42 1907.8 17.0

seasonal_profile

seasonal_profile(
    weekly: pd.DataFrame,
    years: int | None = None,
)

urgencias_core.analysis · ver el código

El año típico: para cada semana del año, el promedio y el rango entre años, y index: cuánto se aleja esa semana del nivel de su propio año (1,15 = 15 % sobre el nivel). Así el crecimiento de un año a otro no se confunde con la estación.

weekly
serie semanal
years
usar solo los últimos años

Devuelve: una fila por semana: mean, low, high, years, index.

from urgencias_core.analysis import seasonal_profile
perfil = seasonal_profile(semanal)
altas = perfil.sort_values("index", ascending=False).head(3)
print(altas.to_string(float_format="%.2f"))
Salida
mean low high years index
week
17 1840.20 1379.00 2191.00 5 1.19
16 1829.00 1441.00 2318.00 5 1.17
21 1795.20 1473.00 2180.00 5 1.12

level y year_over_year

level(weekly: pd.DataFrame, window: int = 52)

urgencias_core.analysis · ver el código

level es el promedio de las últimas 52 semanas: al promediar un año completo desaparece la estación y queda la tendencia. year_over_year compara cada semana con la misma de un año antes (sumas de 4 semanas).

weekly
serie semanal
window
semanas del promedio (52)

Devuelve: timestamp, count, level, change_pct.

from urgencias_core.analysis import level, year_over_year
print(level(semanal).tail(2))
print(year_over_year(semanal)[["timestamp", "change_pct"]].tail(2))
Salida
timestamp count level change_pct
248 2026-10-12 1986.0 1856.9 15.4
249 2026-10-19 2068.0 1863.6 15.8
timestamp change_pct
248 2026-10-12 13.1
249 2026-10-19 13.9

write_analysis

write_analysis(
    weekly: pd.DataFrame,
    out: Path,
    name: str,
    code: str,
    source: str = "",
    daily: pd.DataFrame | None = None,
)

urgencias_core.analysis.report · ver el código

El informe completo: texto en español, una figura y las tablas en CSV, en una carpeta. analysis_text devuelve solo el texto.

weekly
serie semanal
out
carpeta de salida
name, code
para los títulos
daily
serie diaria, para el perfil por día de la semana

Devuelve: lista de archivos escritos.

from pathlib import Path
from urgencias_core.analysis.report import write_analysis
archivos = write_analysis(
semanal, Path("salida"), "Hospital de Puerto Montt", "24-105"
)
print([a.name for a in archivos])
Salida
['analisis.txt', 'analisis.png', 'analisis_anual.csv', 'analisis_ano_tipico.csv', 'analisis_nivel_interanual.csv']

Pronóstico

Medir modelos en el pasado y pronosticar. Es lo que hace urg-forecast pronosticar.

run_forecast

run_forecast(
    history: pd.DataFrame,
    horizon_weeks: int,
    code: str = "",
    name: str = "",
    models: dict[str, Callable[[], Forecaster]] | None = None,
    source: str = "DEIS MINSAL (descarga actualizada)",
    origins: int = 3,
    step: int = 8,
    calibrate: bool = False,
    select_by: Criterion | None = None,
    quantiles: tuple[float, ...] | None = None,
)

urgencias_core.forecast · ver el código

Cada modelo pronostica semanas pasadas que ya se conocen (ventanas tan largas como el horizonte, hasta 26 semanas) y se pronostica con Ensamble, la mediana de AutoARIMA, Armónico y MSTL, reajustado con toda la historia: en 40 hospitales rindió más que elegir el mejor modelo de cada uno. Los demás quedan en backtest como referencia. select_by elige en cambio el mejor según un criterio, y con calibrate=True las bandas se ensanchan según el error que tuvo en esas ventanas.

history
serie semanal
horizon_weeks
semanas a pronosticar
models
nombre → fábrica; por defecto default_models(): Ensamble y sus referencias. Sin Ensamble entre ellos, gana el de menor pérdida P80
origins, step
número de ventanas (3) y semanas entre ellas (8); al elegir, más ventanas dan una elección más estable, no más acertada
calibrate
ensanchar los intervalos según el backtest (False por defecto: en 40 hospitales empeoró la pérdida P80 en la mayoría)
select_by
None (por defecto): Ensamble. Para elegir, una columna: "qloss_80", "qloss_50", "qloss_90", "qloss_95", "qloss_mean", "wis", "mae", o una función que reciba la tabla del backtest y devuelva un modelo
quantiles
cuantiles del backtest y del pronóstico; por defecto P50 a P95, y de P5 a P95 con select_by="wis"

Devuelve: ForecastResult: best (el modelo que pronostica), backtest (error por modelo), forecast (timestamp, q50, q80, q90, q95), coverage80, interval_scale (vacío si no se calibró).

from urgencias_core import run_forecast
nombre = "Hospital de Puerto Montt"
res = run_forecast(semanal, 12, code="24-105", name=nombre)
print(res.best)
print(res.backtest[["mae", "qloss_80"]])
print(res.forecast[["timestamp", "q50", "q80"]].head(3))
Salida
Ensamble
mae qloss_80
model
Armónico 164.0 68.3
AutoARIMA 168.4 75.5
Ensamble 148.2 66.0
MSTL 130.9 53.2
SeasonalNaive 436.7 280.6
timestamp q50 q80
0 2026-10-26 1951.5 2014.4
1 2026-11-02 1913.8 1987.0
2 2026-11-09 2011.3 2117.9

Cómo leer los cuantiles

q50 es el valor central: la mitad de las veces el real queda bajo él. q80 y q95 son los valores que, con 80 % y 95 % de probabilidad, no se superan; sirven para planificar con margen. La pérdida P80 (pinball) mide el error de q80 castigando más quedarse corto que pasarse; menor es mejor. coverage80 cuenta en cuántas semanas del backtest el real quedó bajo q80 (lo esperable es cerca de 80 %).

cubiertas, total = res.coverage80
print(f"bajo P80 en {cubiertas} de {total} semanas")
# con calibrate=True, cuánto se ensanchó cada banda (1 = nada)
cal = run_forecast(semanal, 12, calibrate=True)
print({k: round(v, 2) for k, v in cal.interval_scale.items()})
Salida
bajo P80 en 20 de 36 semanas
{'q80': 2.08, 'q90': 2.27, 'q95': 1.97}

summary_text y write_outputs

write_outputs(
    result: ForecastResult,
    base: Path,
    today: date | None = None,
)

urgencias_core.report · ver el código

El mismo resumen que imprime el comando (summary_text) y los CSV y figuras que guarda (write_outputs).

result
un ForecastResult
base
carpeta de salida

Devuelve: lista de archivos escritos.

from urgencias_core.report import summary_text, write_outputs
print(summary_text(res).splitlines()[0])
print([a.name for a in write_outputs(res, Path("salida"))])
Salida
Hospital de Puerto Montt (24-105)
['resumen.txt', 'pronostico.csv', 'backtest.csv', 'historia_semanal.csv', 'pronostico.png', 'backtest.png']

Modelos

Un modelo es cualquier clase con fit y predict. Los incluidos se piden por nombre; uno propio se pasa como fábrica.

Forecaster (contrato)

class Forecaster(Protocol)

urgencias_core.models.protocol · ver el código

fit(history, target_col) aprende de la serie semanal. predict(horizon) devuelve horizon.length filas con timestamp y una columna por cuantil de horizon.quantiles (q50, q80, q90, q95; con el criterio WIS también q5, q10, q20), ordenadas. El constructor no debe exigir argumentos (o se registra una función que lo construya). Las fechas futuras salen de future_index.

import numpy as np
from urgencias_core.models.protocol import HorizonSpec, future_index
class UltimoAnio:
"""Cada semana = la misma semana del año pasado, ± su variación."""
def fit(self, history, target_col):
self.s = history.set_index("timestamp")[target_col]
def predict(self, horizon):
fechas = future_index(self.s.index[-1], horizon)
hace_un_anio = fechas - pd.Timedelta(weeks=52)
centro = self.s.reindex(hace_un_anio).ffill().bfill().to_numpy()
d = self.s.diff(52).dropna()
out = pd.DataFrame({"timestamp": fechas})
for q, col in zip(horizon.quantiles, horizon.quantile_columns):
out[col] = centro + max(np.quantile(d, q), 0) * (q > 0.5)
return out
m = UltimoAnio()
m.fit(semanal, "count")
print(m.predict(HorizonSpec(grain="W-MON", length=2)))
Salida
timestamp q50 q80 q90 q95
0 2026-10-26 1729.0 2027.2 2114.6 2173.8
1 2026-11-02 1695.0 1993.2 2080.6 2139.8

default_models, available_models, resolve_models

resolve_models(names: list[str] | None)

urgencias_core.models.registry · ver el código

Los modelos por nombre. default_models() son los que se miden por defecto: Ensamble, que pronostica, y SeasonalNaive, AutoARIMA, Armónico y MSTL como referencia; available_models() suma los opcionales; resolve_models acepta nombres sin tildes ni mayúsculas, o 'archivo:Clase' para uno propio.

names
lista de nombres; None = los de default_models()

Devuelve: dict nombre → fábrica (una instancia nueva por ajuste).

from urgencias_core import available_models, default_models
from urgencias_core.models.registry import resolve_models
print(list(default_models()))
print(list(resolve_models(["armonicoferiados", "mstl"])))
modelos = default_models() | {"UltimoAnio": UltimoAnio}
Salida
['SeasonalNaive', 'AutoARIMA', 'Armónico', 'MSTL', 'Ensamble']
['ArmónicoFeriados', 'MSTL']

Evaluación

Qué tan bien funciona un modelo, y si un cambio ayuda de verdad o es ruido. Todo usa el mismo motor de backtest: ninguna ventana ve el futuro de otra.

backtest

backtest(
    history: pd.DataFrame,
    models: dict[str, Callable[[], Forecaster]],
    length: int,
    origins: int,
    step: int,
    min_train_weeks: int = 104,
    quantiles: tuple[float, ...] = (0.5, 0.8, 0.9, 0.95),
    target_col: str = "count",
    stop_failed: bool = False,
)

urgencias_core.eval.backtest · ver el código

Ajusta cada modelo desde cero en ventanas que van hacia atrás desde el final y guarda lo que predijo. scores() da una fila por ventana y modelo.

history
serie semanal
models
nombre → fábrica
length
semanas de cada ventana
origins, step
número de ventanas y semanas entre ellas

Devuelve: Backtest: windows, scores(), failed, pairs(modelo). scores() trae mae, la pérdida de cada cuantil (qloss_80, …), sus dos partes (qloss_80_short: semanas sobre el P80, a 0,8 por atención; qloss_80_excess: bajo él, a 0,2), qloss_mean (promedio de P50 a P95), wis (si se pidieron P5, P10 y P20), el ancho width_80 (P80 menos P50) y covered de n semanas bajo el P80. compare_scores hace la comparación de compare_models sobre una corrida ya hecha.

from urgencias_core import backtest
dos = {"Armónico": modelos["Armónico"], "UltimoAnio": UltimoAnio}
corrida = backtest(semanal, dos, length=4, origins=6, step=8)
errores = corrida.scores()
cols = ["mae", "width_80", "qloss_80_short", "qloss_80_excess", "qloss_80"]
print(errores.groupby("model")[cols].mean().round(1))
Salida
mae width_80 qloss_80_short qloss_80_excess qloss_80
model
Armónico 135.5 90.3 36.5 11.7 48.2
UltimoAnio 298.7 278.2 72.1 14.5 86.6

compare_models

compare_models(
    history: pd.DataFrame,
    base: tuple[str, Callable[[], Forecaster]],
    candidate: tuple[str, Callable[[], Forecaster]],
    horizon_weeks: int = 4,
    origins: int = 36,
    step: int = 4,
    min_train_weeks: int = 104,
    alpha: float = 0.05,
    metric: str = "qloss_80",
)

urgencias_core.eval.compare · ver el código

¿El candidato le gana a la base de forma consistente? Ajusta ambos en las mismas ventanas (36 de 4 semanas por defecto), cuenta en cuántas gana, aplica una prueba de signos y separa invierno del resto. Es urg-forecast comparar.

history
serie semanal
base, candidate
pares (nombre, fábrica)
horizon_weeks, origins, step
ventanas (4, 36, 4)
metric
qué se compara en cada ventana (por defecto "qloss_80")

Devuelve: Comparison: verdict (mejora, empeora, sin diferencia clara), text(), summary(), windows.

from urgencias_core import compare_models
from urgencias_core.models.harmonic import HarmonicRegression
cmp = compare_models(semanal, ("Armónico", HarmonicRegression),
("UltimoAnio", UltimoAnio), origins=8)
print(cmp.text())
Salida
UltimoAnio frente a Armónico, 8 ventanas de 4 semanas: gana 3, pierde 5 (p = 0.73); cambio mediano de la pérdida P80 +22.7 %. Veredicto: sin diferencia clara.

Regresores

Variables externas al DEIS (feriados, virus, clima) con las reglas de su fuente. Explicación y resultados en la página Regresores.

Regressor

Regressor(
    name: str,
    load: Callable[[], pd.DataFrame],
    columns: tuple[str, ...],
    report_delay_weeks: int = 0,
    known_future: bool = False,
    description: str = "",
)

urgencias_core.regressors · ver el código

Una tabla semanal (timestamp + una columna por variable) y las reglas de su fuente: cuántas semanas tarda en publicarse y si su futuro se conoce. Vienen calendar() (feriados, interferiados, vacaciones escolares; futuro conocido) y flunet() (positividad de influenza y VRS; 2 semanas de atraso).

load
función que devuelve la tabla
columns
columnas que usa el modelo
report_delay_weeks
atraso de publicación
known_future
si los valores futuros se conocen
from urgencias_core.regressors import calendar
cal = calendar()
print(cal.columns, cal.known_future)
cols = ["timestamp", "holidays", "bridge_days", "school_holiday"]
tabla = cal.table()[cols]
print(tabla.query("timestamp >= '2026-09-14'").head(3))
Salida
('holidays', 'bridge_days', 'school_holiday') True
timestamp holidays bridge_days school_holiday
975 2026-09-14 0.0 0.0 0.0
976 2026-09-21 1.0 1.0 0.6
977 2026-09-28 0.0 0.0 0.0

HarmonicExogenous.from_regressor

HarmonicExogenous.from_regressor(
    regressor: Regressor,
    columns: Sequence[str] | None = None,
    lag_weeks: int = 0,
    future: str = "climatology",
    **kwargs,
)

urgencias_core.models.exogenous · ver el código

El Armónico más un regresor. En cada ventana solo usa lo que estaba publicado en esa fecha; lo que falta lo rellena con el promedio histórico ("climatology") o con una proyección de la serie ("forecast"). regressor_path muestra qué supuso para las semanas pronosticadas.

regressor
un Regressor
columns
subconjunto de columnas (por defecto todas)
lag_weeks
la semana t usa el valor de t − rezago
future
"climatology" o "forecast"

Devuelve: un modelo que cumple el contrato Forecaster.

from urgencias_core.models.exogenous import HarmonicExogenous
feriados = lambda: HarmonicExogenous.from_regressor(cal, ["holidays"])
m = feriados()
m.fit(semanal, "count")
print(m.regressor_path(HorizonSpec(grain="W-MON", length=3)))
cmp = compare_models(semanal, ("Armónico", HarmonicRegression),
("Feriados", feriados), origins=8)
print(cmp.verdict)
Salida
timestamp holidays
0 2026-10-26 0.0
1 2026-11-02 0.0
2 2026-11-09 0.0
sin diferencia clara

Extender el paquete

Recetas paso a paso para sumar algo propio y medir si sirve. Dónde va cada pieza está en Arquitectura.

Agregar tu propio modelo

Un modelo es una clase con dos métodos:

  • fit(history, target_col): recibe la historia semanal (un DataFrame con timestamp y la columna target_col) y aprende lo que necesite.
  • predict(horizon): devuelve un DataFrame con una fila por semana futura y las columnas timestamp y una por cada cuantil de horizon.quantiles: q50, q80, q90 y q95, más q5, q10 y q20 cuando se elige con --criterio wis. horizon.length es la cantidad de semanas.

El constructor no debe exigir argumentos.

1. Escribe el modelo. Este ejemplo pronostica cada semana como el promedio de esa misma semana en los últimos tres años, y arma los intervalos con los errores que esa regla tuvo en el pasado. Guárdalo como mi_modelo.py en la carpeta donde vas a correr el comando:

mi_modelo.py
import numpy as np
import pandas as pd
from urgencias_core.models.protocol import future_index
class PromedioAnual:
"""Cada semana futura = promedio de esa misma semana en los últimos años."""
def __init__(self, años=3):
self.años = años
def fit(self, history, target_col):
s = history.set_index("timestamp")[target_col].astype(float)
self.ultima_semana = s.index[-1]
# Promedio de cada semana del año (1 a 53), solo con los últimos años.
recientes = s[s.index > s.index[-1] - pd.DateOffset(years=self.años)]
self.promedio = recientes.groupby(recientes.index.isocalendar().week).mean()
# Errores históricos de esa regla, para armar los intervalos.
estimado = pd.Series(s.index.isocalendar().week.to_numpy()).map(self.promedio)
self.errores = (s.to_numpy() - estimado.to_numpy())[estimado.notna().to_numpy()]
def predict(self, horizon):
fechas = future_index(self.ultima_semana, horizon)
centro = fechas.isocalendar().week.map(self.promedio).to_numpy(dtype=float)
salida = pd.DataFrame({"timestamp": fechas})
for q, columna in zip(horizon.quantiles, horizon.quantile_columns):
# q50 es el promedio; los demás le suman el error de ese cuantil
# (negativo bajo la mediana, para q5, q10 y q20 cuando se piden).
salida[columna] = centro + (np.quantile(self.errores, q) if q != 0.5 else 0.0)
return salida

2. Compáralo con los modelos incluidos. Pásalo con -m archivo:Clase y repite -m para cada modelo contra el que quieras compararlo:

Ventana de terminal
urg-forecast pronosticar 24-105 -H 12 -m mi_modelo:PromedioAnual -m AutoARIMA -m MSTL

3. Lee la tabla de backtest. Tu modelo aparece junto a los demás, evaluado en las mismas ventanas, y gana solo si tiene la menor pérdida P80:

+---------------+-------+----------+---------------+
| Modelo | MAE | MAPE % | Pérdida P80 |
|---------------+-------+----------+---------------|
| MSTL * | 122 | 7.4 | 50.3 |
| AutoARIMA | 118 | 7.1 | 51.0 |
| PromedioAnual | 182 | 10.5 | 99.5 |
+---------------+-------+----------+---------------+

Acá la regla simple pierde: no sigue los cambios de nivel recientes. Ese es el punto de comparar antes de confiar en un modelo. Si el tuyo gana de forma consistente en tu establecimiento, úsalo solo con -m mi_modelo:PromedioAnual.

Desde Python es lo mismo: agrega tu clase a los modelos por defecto. Su backtest aparece junto al de los demás; el pronóstico sigue siendo de Ensamble, salvo que pidas elegir con un criterio.

from urgencias_core import default_models
from mi_modelo import PromedioAnual
modelos = default_models() | {"PromedioAnual": PromedioAnual}
resultado = run_forecast(semanal, 12, models=modelos)
print(resultado.backtest)
elegido = run_forecast(semanal, 12, models=modelos, select_by="qloss_80") # gana el mejor

Más ejemplos en src/urgencias_core/models/: harmonic.py (regresión con términos de Fourier y feriados) y ensemble.py (combina varios modelos).

Agregar un regresor externo

Un regresor es una serie semanal que no viene del DEIS: feriados, circulación viral, temperatura, contaminación, vacunación. La forma estándar de sumarlo tiene dos pasos: agregarlo sin hacer trampa y medir si sirve. Qué son, cuáles vienen y qué mostraron con datos reales está en docs/regresores.md. En la demo puedes agregarlos al pronóstico con un botón.

1. Agregarlo. Un Regressor es una tabla con timestamp (el lunes de cierre de cada semana DEIS) y una columna por variable, más las reglas de su fuente. Vienen dos: flunet() (positividad viral, 2 semanas de atraso) y calendar() (feriados, interferiados y vacaciones escolares, que se conocen de antemano). Uno propio se declara igual, y HarmonicExogenous.from_regressor lo suma al Armónico:

import pandas as pd
from urgencias_core.models.exogenous import HarmonicExogenous
from urgencias_core.regressors import Regressor
temperatura = Regressor(
name="temperatura",
load=lambda: pd.read_csv("temperatura_semanal.csv", parse_dates=["timestamp"]),
columns=("temp_min",),
report_delay_weeks=1, # cuántas semanas tarda en publicarse cada dato
)
con_temp = lambda: HarmonicExogenous.from_regressor(
temperatura,
lag_weeks=0, # la semana t usa el valor de la semana t - lag
future="forecast", # qué hacer con lo no publicado: "forecast" o "climatology"
)

La regla que lo mantiene honesto: en cada ventana del backtest el modelo solo ve los datos que ya estaban publicados en esa fecha. Lo que falta se rellena con el promedio histórico de esa semana del año ("climatology") o con una proyección de la propia serie ("forecast": Armónico sin tendencia y ARIMA, que arrastra si la temporada viene adelantada o atrasada). 2020 y 2021 quedan fuera de ambos. Las variables con futuro conocido (known_future=True, como el calendario) se usan tal cual, también para las semanas que se pronostican.

2. Medir si sirve. Tres ventanas alcanzan para elegir modelo, no para saber si un regresor ayuda: con pocas, la diferencia es casi siempre ruido. compare_models ajusta ambos modelos en las mismas 36 ventanas de 4 semanas (unos tres inviernos), cuenta en cuántas gana el candidato, aplica una prueba de signos y separa invierno del resto:

from urgencias_core.eval.compare import compare_models
from urgencias_core.models.harmonic import HarmonicRegression
cmp = compare_models(semanal, ("Armónico", HarmonicRegression), ("ConTemp", con_temp))
print(cmp.text()) # gana, pierde, p, cambio mediano y veredicto
cmp.summary() # todas, invierno (mayo a septiembre) y resto
cmp.windows # una fila por ventana

Desde la terminal, con un modelo incluido o uno propio (archivo:Clase):

Ventana de terminal
urg-forecast comparar 24-105 -m ArmonicoFluNetProy

El veredicto es mejora, empeora o sin diferencia clara, por establecimiento. Si pruebas muchos, cerca de uno de cada veinte saldrá distinto de “sin diferencia” por azar: un regresor útil mejora en muchos, no en unos pocos. El ejemplo completo, con FluNet y el calendario, está en examples/04_regresor_externo.py.

Pedirle a una IA que lo extienda

Si usas un asistente de IA (ChatGPT, Claude, Copilot u otro), copia esta instrucción, reemplaza lo que está entre corchetes y pégala. Le da el contexto y las reglas que necesita para que el modelo funcione con urg-forecast:

Estoy usando urg-forecast-core, un paquete de Python para pronosticar las
atenciones semanales de un servicio de urgencia en Chile con datos públicos del
DEIS MINSAL (https://github.com/nicoveraz/urg-forecast-core).
Antes de responder, lee:
- https://nicoveraz.github.io/urg-forecast-core/desarrolladores/
- https://nicoveraz.github.io/urg-forecast-core/modelos/
Lo que quiero: [describe el modelo o el cambio, por ejemplo: "un modelo que use
las vacaciones de invierno como variable" o "probar Prophet"].
Reglas:
1. Escribe una sola clase en un archivo mi_modelo.py, con fit(history,
target_col) y predict(horizon). El constructor no debe exigir argumentos.
2. history es un DataFrame con una fila por semana (semanas que terminan en
lunes) y las columnas timestamp y target_col.
3. predict debe devolver un DataFrame con horizon.length filas y las columnas
timestamp y una por cada cuantil de horizon.quantiles (q50, q80, q90 y q95;
también q5, q10 y q20 si se piden), ordenadas de menor a mayor. Usa
urgencias_core.models.protocol.future_index(ultima_semana, horizon) para
las fechas.
4. Si necesitas una biblioteca adicional, dime cómo instalarla.
5. No uses datos de pacientes ni envíes datos a servicios externos.
6. Termina con el comando para compararlo con los modelos incluidos:
urg-forecast pronosticar [código DEIS] -H 12 -m mi_modelo:[Clase] -m AutoARIMA -m MSTL

Revisa lo que te entregue igual que cualquier modelo: lo que importa es la tabla de backtest, no la explicación.

Otras ideas

Otras series. Con load_deis(..., source="deis") (los archivos completos del DEIS) tienes conteos diarios por causa y grupo de edad, así que puedes analizar y pronosticar las causas respiratorias o la urgencia pediátrica en vez del total. La copia semanal del repositorio trae solo el total.

Fuentes externas. Con la receta de Agregar un regresor externo se puede probar clima, vacunación o vigilancia regional con el mismo rigor. Los dos ejemplos incluidos dan resultados opuestos (detalle en docs/modelos.md): los feriados mejoran el pronóstico en la mayoría de los 40 hospitales con más atenciones (ArmónicoFeriados), mientras que la circulación viral de FluNet no mejora el total de atenciones en ninguno de forma consistente. Las dos cosas son resultados.

Calendario chileno. urgencias_core.features.calendar_features (por día) y weekly_calendar (por semana DEIS) construyen variables de feriados, interferiados, calendario escolar y eventos regionales; regressors.calendar() las deja listas para HarmonicExogenous.

Revisa docs/roadmap.md para ver brechas conocidas que sirven como primeras contribuciones.

Generada con scripts/construir_api.py y urg-forecast-core 0.4.0; las recetas vienen de la sección "Extender" del README. La CI ejecuta todos los ejemplos y falla si una firma cambia sin regenerar la página.