---
name: run_sales_forecast
kind: pipeline
lang: py
domain: pipelines
purity: impure
version: "1.1.0"
signature: "def run_sales_forecast(as_of: str = '', horizon: int = 7, model: str = 'baseline_v1', author: str = 'egutierrez', dry_run: bool = False) -> dict"
description: "Forecast diario de ventas Aurgi (dia x centro x subcategoria CGQ) escrito en BigQuery autingo-159109.sales_forecast.predictions, en una sola llamada. Compone funciones del registry: bq_auth(drop_quota_project=True) para el cliente sin quota project ajeno, bq_query para leer la historia agregada del mart bi_ventas_mart.base_margenes_aa (18 semanas, venta_n saneado) y ejecutar el DELETE de idempotencia, forecast_seasonal_median (modelo PURO mediana estacional + tendencia acotada) para generar todas las predicciones, y bq_load_from_file para cargar el JSONL a la tabla de predicciones. Historia utilizable hasta as_of-1 (el dia as_of esta parcial cuando corre el cron a las 21:00); predice as_of+1..as_of+horizon; run_date=as_of. Solo predice series activas (venta>0 en las ultimas 8 semanas). Idempotente por (run_date, model, author). --dry-run no escribe."
tags: [forecast, bigquery, sales, aurgi, pipeline, launcher]
uses_functions:
  - forecast_seasonal_median_py_datascience
  - bq_auth_py_infra
  - bq_query_py_infra
  - bq_load_from_file_py_infra
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: [google-cloud-bigquery]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/pipelines/run_sales_forecast.py"
params:
  - name: as_of
    desc: "fecha de corte 'YYYY-MM-DD' (dia de la corrida). Vacio (DEFAULT) = hoy. La historia utilizable llega hasta as_of-1 dia (el dia as_of esta parcial en el cron 21:00); se predice as_of+1..as_of+horizon; run_date=as_of"
  - name: horizon
    desc: "numero de dias futuros a predecir a partir de as_of+1. Default 7"
  - name: model
    desc: "etiqueta del modelo escrita en la columna model de cada fila. Default 'baseline_v1'. Forma parte de la clave de idempotencia"
  - name: author
    desc: "autor de la corrida (columna author). Default 'egutierrez'. Forma parte de la clave de idempotencia"
  - name: dry_run
    desc: "si True no escribe en BigQuery (ni DELETE ni load): devuelve el resumen + una muestra de 5 filas. Default False"
output: "dict dict-no-throw. En exito {status:'ok', run_date, series:N (series activas), rows:N (filas predichas), model, author, rows_loaded, job_id}; con dry_run=True incluye sample:[5 filas] y omite rows_loaded/job_id. En error {status:'error', error, stage}. Por stdout imprime el JSON del resumen; exit 0 si ok, 1 si error"
---

## Ejemplo

```bash
# Corrida real (cron 21:00): predice los 7 dias siguientes a hoy y carga a BigQuery.
./fn run run_sales_forecast

# Fecha de corte y horizonte explicitos, sin escribir (revisar la muestra):
./fn run run_sales_forecast --as-of 2026-07-01 --horizon 7 --dry-run

# Modelo alternativo (clave de idempotencia distinta: no pisa baseline_v1):
./fn run run_sales_forecast --model baseline_v2 --author egutierrez
```

```python
# Uso programatico (venv del proyecto, PYTHONPATH=python/functions):
from pipelines.run_sales_forecast import run_sales_forecast

r = run_sales_forecast(as_of="2026-07-01", horizon=7, dry_run=True)
print(r["series"], "series activas,", r["rows"], "filas")
for row in r["sample"]:
    print(row["forecast_date"], row["center_id"], row["subcat_cgq"], row["y_pred"])
```

## Cuando usarla

Cuando quieras (re)generar el forecast diario de ventas Aurgi por centro y
subcategoria CGQ y dejarlo en `autingo-159109.sales_forecast.predictions` en una
sola llamada. Es el pipeline que dispara el cron nocturno (21:00): lee la historia
del mart, aplica el baseline estacional, y carga las predicciones de forma
idempotente. Usa `--dry-run` para inspeccionar la muestra antes de escribir, o
para probar tras un cambio en el mart o en el modelo. Cambia `--model` para probar
una variante sin pisar las predicciones del modelo actual (la clave de
idempotencia es run_date + model + author).

## Gotchas

- Impura: requiere ADC de BigQuery configurado (`gcloud auth application-default
  login`) con acceso a `autingo-159109`. Usa `bq_auth(drop_quota_project=True)`
  para descartar el quota project del ADC del usuario `egutierrez` y evitar el
  `403 USER_PROJECT_DENIED` (gotcha conocido del repo).
- Escribe en produccion: en modo real hace `DELETE` de las predicciones previas de
  `(run_date, model, author)` y luego carga (WRITE_APPEND). Es idempotente para esa
  combinacion: re-ejecutar la misma corrida no duplica. Cambiar `model` o `author`
  crea un conjunto de predicciones paralelo. Usa `--dry-run` si solo quieres mirar.
- La tabla `sales_forecast.predictions` debe existir con schema fijo y con las
  columnas exactas que emite el pipeline: `run_ts` (TIMESTAMP), `run_date` (DATE),
  `forecast_date` (DATE), `lag_days` (INT64), `center_id` (STRING), `center_name`
  (STRING), `ambito` (STRING), `subcat_cgq` (STRING), `model` (STRING), `author`
  (STRING), `y_pred` (FLOAT64). El load usa `autodetect=False`: los nombres del
  JSONL deben coincidir con los de la tabla o el load falla.
- `center_id` se emite como STRING (str(idCentro)); `subcat_cgq` toma el valor de
  la columna `subcat_cqq` del mart (el nombre difiere entre origen y destino a
  proposito). center_name/ambito son los ultimos conocidos por serie (fecha maxima).
- Historia hasta as_of-1: el dia `as_of` NO entra en la historia (esta parcial en
  el cron de las 21:00). Si necesitas incluir el dia en curso, pasa `--as-of` con
  el dia siguiente.
- Solo predice series con venta > 0 en las ultimas 8 semanas: las series muertas se
  omiten (no aparecen en la tabla). `series` en la salida cuenta las activas.
- Guarda 18 semanas de historia del mart: cubre la ventana estacional (8 semanas
  del mismo dia) mas la tendencia (4+4 semanas) con margen. venta_n se filtra
  `ABS < 1e9` para descartar las filas veneno del mart.

## Capability growth log

- v1.1.0 (2026-07-02) — añade paso 9: refresh de `sales_forecast.actuals_daily` (tabla física de venta real, ventana móvil de 10 días) tras cargar las predicciones; `forecast_eval` y el dashboard de competición comparan contra ella.
