---
name: forecast_seasonal_median
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: pure
signature: "def forecast_seasonal_median(history: list[dict], horizon_dates: list[str], as_of: str, dow_weeks: int = 8, trend_recent_weeks: int = 4, trend_clip: tuple = (0.5, 2.0)) -> list[dict]"
description: "Forecast diario por mediana estacional (mismo dia de semana) mas factor de tendencia acotado, para una o varias series temporales. Base estacional = mediana del valor en las ultimas dow_weeks fechas con el mismo dia de semana que la fecha objetivo (dias ausentes = 0, para series intermitentes). Factor de tendencia por serie = razon de la suma de las ultimas trend_recent_weeks semanas frente a las trend_recent_weeks anteriores, clipped a trend_clip. y_pred = max(0, base * factor). Funcion pura y determinista (solo stdlib, sin I/O ni datetime.now). Nucleo del forecast de ventas diarias Aurgi (dia x centro x subcategoria CGQ)."
tags: [forecast, bigquery, timeseries, seasonal, median, baseline, sales, aurgi, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
  - name: history
    desc: "lista de observaciones {series_id: str, date: 'YYYY-MM-DD', value: float}. Filas duplicadas (misma serie+fecha) se suman. Los dias sin fila dentro de las ventanas cuentan como valor 0 (series intermitentes: sin fila = sin venta)"
  - name: horizon_dates
    desc: "fechas futuras a predecir, strings ISO 'YYYY-MM-DD'. Tipicamente as_of+1..as_of+horizon"
  - name: as_of
    desc: "fecha de corte 'YYYY-MM-DD': ultimo dia de historia utilizable, inclusive. Todas las ventanas se calculan hacia atras desde aqui"
  - name: dow_weeks
    desc: "numero de fechas del mismo dia de semana que la objetivo a promediar (mediana) para la base estacional. Default 8 (8 semanas)"
  - name: trend_recent_weeks
    desc: "tamano en semanas de cada una de las dos ventanas de tendencia (reciente y anterior). Default 4: compara 4 semanas recientes vs las 4 previas"
  - name: trend_clip
    desc: "tupla (min, max) al que se acota el factor de tendencia. Default (0.5, 2.0): la prediccion no puede caer a menos de la mitad ni superar el doble por tendencia"
output: "list[dict]: una fila {series_id: str, date: str, y_pred: float} por cada serie presente en history y cada fecha de horizon_dates. Ordenada por series_id (asc) y luego por el orden de horizon_dates. y_pred siempre >= 0.0"
tested: true
tests:
  - "serie regular con patron semanal claro da la mediana correcta"
  - "serie intermitente: los dias ausentes cuentan como 0 en la mediana"
  - "serie con tendencia creciente aplica factor >1 acotado a trend_clip"
  - "sin datos en la ventana anterior, el factor de tendencia es 1.0"
  - "horizon de 7 dias produce una fila por serie y fecha, ordenadas"
test_file_path: "python/functions/datascience/forecast_seasonal_median_test.py"
file_path: "python/functions/datascience/forecast_seasonal_median.py"
---

## Ejemplo

```python
from datascience import forecast_seasonal_median

# Historia diaria por serie (centro|subcategoria). Sin fila = sin venta = 0.
history = [
    {"series_id": "12|NEUMATICOS", "date": "2026-06-23", "value": 1450.0},
    {"series_id": "12|NEUMATICOS", "date": "2026-06-16", "value": 1380.0},
    {"series_id": "12|NEUMATICOS", "date": "2026-06-09", "value": 1500.0},
    # ... mas historia (idealmente >= 8 semanas para la base estacional) ...
]

# as_of = ultimo dia cerrado; predice los 7 dias siguientes.
horizon = ["2026-06-30", "2026-07-01", "2026-07-02", "2026-07-03",
           "2026-07-04", "2026-07-05", "2026-07-06"]

preds = forecast_seasonal_median(history, horizon, as_of="2026-06-29")
for p in preds:
    print(p["series_id"], p["date"], round(p["y_pred"], 2))
```

## Cuando usarla

Cuando necesites un baseline de forecast diario robusto y explicable para series
con estacionalidad semanal fuerte (ventas por dia de la semana) y posibles huecos
(dias sin venta). Es el nucleo puro del pipeline `run_sales_forecast`: se llama una
vez con toda la historia agregada y devuelve todas las predicciones de golpe.
Usala como punto de partida antes de modelos mas pesados (Prophet, ARIMA, gradient
boosting): captura el patron dia-de-semana + una correccion de tendencia acotada
sin dependencias externas ni entrenamiento. Ideal para muchas series a la vez
(miles de pares centro x subcategoria) donde entrenar un modelo por serie no
compensa.

## Notas

- Funcion pura y determinista: no hace I/O, no llama `datetime.now()`; el corte
  temporal siempre es el argumento `as_of` explicito. Solo stdlib (datetime,
  statistics), sin numpy ni pandas.
- La base estacional toma las fechas EXACTAS del calendario: la mas reciente
  <= as_of con el mismo dia de semana que la objetivo, y de ahi 7 dias hacia atras
  por punto (hasta `dow_weeks` puntos). Una fecha ausente en `history` cuenta como
  0, por lo que la mediana refleja bien las series intermitentes.
- El factor de tendencia se calcula UNA vez por serie (no depende de la fecha
  objetivo) como razon de sumas de dos ventanas contiguas de `trend_recent_weeks`
  semanas. Denominador 0 => factor 1.0 (evita division por cero y no infla series
  que arrancan). El clip a `trend_clip` evita que un pico reciente dispare la
  prediccion.
- `y_pred = max(0.0, base * factor)`: nunca negativo. No modela festivos ni eventos
  puntuales; para eso se necesitaria una capa de calendario adicional.
- Para que la base estacional sea fiable conviene aportar >= `dow_weeks` semanas de
  historia. Con menos historia, los puntos ausentes (=0) empujan la mediana hacia
  abajo.
