Jugar voleibol con agentes de IA para predecir la rotación de restaurantes.
Un hackathon de rotación de restaurantes basado en agentes de IA controlados por humanos: auditoría de datos, validación adaptativa, ingeniería de características, ensembling y la brecha con las puntuaciones del ranking.
Imagínese un hackathon en el que el modelo en sí solo representa la mitad del trabajo. La otra mitad consiste en mantener ocupados a unos pocos agentes de IA trabajando en ideas competitivas, sin permitir que ninguno de ellos realice una llamada que corresponda a una persona. Este texto describe uno de esos esfuerzos, proveniente de un hackathon sobre predicción del volumen de ventas en restaurantes organizado por Great Learning en colaboración con la UT Austin y su escuela de negocios McCombs. Se aborda un flujo de trabajo de aprendizaje automático eficiente, una validación estricta y un conjunto de algoritmos que, según los propios organizadores del proyecto, tuvo buen desempeño en la competencia.
La capa de orquestación era un marco llamado DontPanic. Sus flujos de trabajo siguen la lógica de un rally de voleibol: un agente recibe una tarea, planifica el siguiente paso, transfiere el trabajo a roles especializados y finalmente entrega el resultado, pero la “pelota” siempre vuelve a manos de un humano en la línea de toma de decisiones. Los agentes aceleran la ejecución, pero no tienen autoridad para tomar decisiones.
1. La tarea
Los competidores debían predecir el volumen de negocios anual de los restaurantes en toda la India a partir de 3,493 registros de entrenamiento etiquetados y 500 registros de prueba sin etiquetar. Existían aproximadamente 32 predictores brutos que tenían en cuenta la ubicación, la cocina, el formato del restaurante, las comodidades, la popularidad en redes sociales y las calificaciones de experiencia del cliente. Por “volumen de negocios” se entienden las ventas anuales, no la rotación del personal. La métrica utilizada fue el RMSE, que eleva al cuadrado los errores antes de calcular su promedio, por lo que algunos errores muy grandes pueden arruinar la puntuación.
El objetivo no era crear el modelo más sofisticado. Se trataba de un sistema de pronóstico que pudiera defenderse: auditar los datos, comprender la economía del sector objetivo, diseñar señales que tuvieran sentido desde el punto de vista empresarial, validar las propuestas mediante validación cruzada estable, descartar las mejoras que no se reproducían y, finalmente, mantener la verdadera capacidad de generalización del modelo separada de los ajustes específicos para la competencia.
2. Las herramientas
Todo se encontraba en VS Code como un paquete modular de Python dentro de src/, en lugar de un montón de experimentos con cuadernos de notas sin relación entre sí. Esa elección resultó beneficiosa porque la limpieza, la creación de funcionalidades, la validación y la inferencia se realizaban a través del mismo código reutilizable. Por lo tanto, una funcionalidad no podía comportarse de manera diferente durante el entrenamiento y al momento de entregar el resultado.
3. Orquestación agente
DontPanic era el mecanismo que coordinaba todo el proyecto. La autonomía total nunca fue el objetivo. Lo importante era permitir que varios agentes actuaran rápidamente, al tiempo que se definía claramente la estructura de toma de decisiones: quién explora, quién construye, quién plantea dudas, qué resultado es autoritativo y cuándo el trabajo debe ser revisado por una persona para decidir si proceder o no.
Eso es importante porque la experimentación con inteligencia artificial plantea un paso lógico tras otro. Un agente que opera de forma autónoma seguirá optimizando la señal que haya visto más recientemente, y un humano puede perder de vista si la hipótesis, el protocolo de validación o el criterio de promoción han cambiado en el fondo. Considere a DontPanic como el reglamento bajo el cual se llevó a cabo el experimento: los agentes tenían libertad para avanzar en su acción, pero nunca para reescribir las reglas sin que nadie se diera cuenta.
Cada ejecución de un agente estaba descrita por un manifiesto. Un ejemplo, para una ejecución de Claude CLI, detalla los comandos y archivos que se requieren, cómo se verifica la preparación, qué partes son responsabilidad del framework, qué debe hacer el operador y qué acciones están prohibidas:
{
"schema_version": "1.0.0",
"id": "agent-claude-cli",
"title": "Claude CLI agent runtime",
"kind": "agent_cli",
"category": "agent-cli",
"summary": "External agent CLI used by DontPanic as an implementer, auditor, or goal-auditor runtime.",
"setup_required": true,
"setup_doc": "docs/GETTING_STARTED.md#setup-tracks--pick-yours",
"default_in_profiles": ["core"],
"use_cases": ["U1", "U3", "U4", "U5", "U6", "U7"],
"requires": {
"commands": ["claude"],
"env": [],
"files": ["~/.dontpanic/config.json"],
"services": ["Claude account / Claude Code or CLI runtime"],
"auth": ["Claude CLI authenticated by operator"],
"config": [
"roles.implementer",
"roles.auditor",
"roles.goal_auditor"
]
},
"verify": {
"doctor_profiles": ["core"],
"probes": ["cli:claude", "agent_role_config"],
"readiness": "Ready when the claude command is present and the selected DontPanic role points at claude."
},
"owner_boundary": {
"dontpanic_core": [
"agent registry entry",
"dispatch envelope",
"audit/signoff capture",
"quota and breaker handling",
"role configuration"
],
"adapter": [
"subprocess executor for claude",
"stream/output parsing",
"timeout and envelope normalization"
],
"operator": [
"Claude CLI installation",
"account authentication",
"model/quota management",
"local CLI updates"
]
},
"mutation_boundary": {
"writes_dontpanic_state": true,
"external_mutation": "agent_subprocess_execution",
"allowed_path": "DontPanic supervisor invokes claude through the governed executor and captures artifacts through audit/signoff writers.",
"forbidden": [
"Bypassing supervisor gates",
"Untracked agent writes outside declared plan scope",
"Treating Claude account configuration as DontPanic-owned state"
]
},
"notes": [
"Claude is an external runtime, not bundled by DontPanic.",
"Core profile may also support other agent CLIs; this manifest records the Claude-specific path."
],
"setup_steps": [
{
"id": "install_claude_cli",
"what": "Install the Claude CLI so DontPanic can spawn agent runs through the governed executor.",
"automatable": true,
"command_template": "npm install -g @anthropic-ai/claude-cli",
"verify_probe": "cli:claude",
"human_required_reason": null
},
{
"id": "authenticate_claude",
"what": "Authenticate the Claude CLI against the operator's Claude account or API key.",
"automatable": false,
"command_template": "claude auth login --token <YOUR_CLAUDE_API_TOKEN>",
"verify_probe": "agent_role_config",
"human_required_reason": "Operator must approve scopes for their Claude account and treat the API token as a secret; DontPanic does not provision Claude credentials on the operator's behalf."
}
]
}
3.1 El rally en cinco pasos
- Servir: antes de comenzar cualquier experimento, el humano planteó la pregunta empresarial, las restricciones, la métrica, los criterios de aceptación y el modelo actual ganador.
- Recibir: un agente de descubrimiento examinó las pruebas, identificó anomalías y propuso hipótesis verificables en lugar de reescribir el modelo de inmediato.
- Establecer: una vez aprobada una hipótesis, un agente de implementación la redactó como código modular, artefactos deterministas y una definición de experimento que pudiera ejecutarse nuevamente.
- Volea: un agente revisor buscó fugas de información, validación cruzada defectuosa, semillas inestables y un origen de datos poco claro, y preguntó si la ganancia aparente resistía una comparación con el modelo ganador actual.
La verdadera ventaja radicaba en el paralelismo controlado, y no en la velocidad bruta de codificación. Diferentes agentes podían concentrarse en distintas formas de fallar, mientras que el control humano protegía la intención del producto, su viabilidad comercial y la rendición de cuentas. El resultado se asemejaba más a un pequeño equipo de desarrollo que a una simple sesión con un chatbot.
3.2 Entrega ágil de ML
El flujo de trabajo de los agentes también se adaptó a un ciclo de vida de producto ágil, con el objetivo de hacer que los experimentos fueran económicos en la fase de desarrollo, dificultar la falsificación de pruebas en la fase de pruebas de usuario y evitar cambios accidentales en los estados de producción. La división entre DEV, UAT y PROD ganó aún más importancia a medida que aumentaba el número de experimentos. Un nuevo agente podía unirse sin depender de la memoria de las conversaciones, ya que los registros oficiales se conservaban en forma de archivos, manifiestos, predicciones fuera del contexto (OOF), un registro de agentes destacados y decisiones de promoción por escrito. El patrón a mantener es: dejar que los agentes aporten velocidad y que los artefactos y controles garanticen la veracidad.
4. Auditoría de los datos antes de creer en ellos
Antes del modelado, los archivos de entrenamiento y prueba se analizaron con SHA-256 y se revisaron columna por columna. Eso no era burocracia; varias peculiaridades podrían haber alterado el espacio de características en el momento de la inferencia y afectado las predicciones sin generar ningún error visible.
- El conjunto de entrenamiento tenía 3,493 filas y 34 columnas, mientras que el conjunto de prueba tenía 500 filas y 33 columnas, ya que no contaba con variable objetivo.
- Las columnas
AmbienteyNivel del restaurantetenían tipos de datos diferentes en el conjunto de entrenamiento y en el de prueba, por lo que se necesitó una coerción explícita para que los modelos recibieran características idénticas. - Una columna se escribía como
Endorsed Byen el conjunto de entrenamiento y comoEndoresed Byen el de prueba. Sin una correspondencia, el proceso podría haber generado una característica de prueba que faltara por completo.
Número de registro funcionó como un identificador, no como un predictor comercial (Pearson alrededor de -0.017, Spearman alrededor de -0.022). Se excluyó de los modelos y se mantuvo únicamente para asociar las filas al final.5. Exploración de los datos
5.1 Un objetivo con fuerte sesgo a la derecha
El volumen de negocios anual presentaba una larga cola derecha. El percentil 90 se situaba cerca de ₹48M, el 95 cerca de ₹57M, el 99 cerca de ₹100M y el valor máximo en ₹400M, con una asimetría bruta de aproximadamente 6.65. Una transformación log1p hizo que la distribución fuera casi simétrica, y se compararon los valores brutos, los del log1p y los según el método Yeo-Johnson; posteriormente se probó también una variante basada en el área por pie cuadrado. En términos comerciales, la mayoría de los restaurantes se encuentran dentro de un rango de ingresos bastante estrecho, mientras que un pequeño grupo de establecimientos muy grandes genera errores financieros significativos cada vez que se les subestima.
La cola derecha no era solo visualmente inusual; además dominaba las pérdidas. El análisis de errores mostró que un pequeño conjunto de restaurantes cuyas predicciones fueron las peores representaba una gran parte del error cuadrático total, por lo que los diagnósticos relacionados con dicha cola se convirtieron en una parte esencial de la evaluación.
5.2 Los datos faltantes requieren más de una regla
Los valores faltantes no se trataron como un fenómeno único. Algunos reflejaban deficiencias habituales en la calidad de los datos. Otros codificaban la estructura empresarial; por ejemplo, una calificación de entretenimiento en blanco se debía a que el lugar no ofrecía entretenimiento. Aplicar la misma regla de imputación para ambos casos destruiría información útil.
La falta de datos habitual, como la popularidad en redes sociales, la calificación general, el ambiente y la categoría, se gestionó mediante imputación basada en datos de entrenamiento además de indicadores explícitos de falta de datos. Esto permite a un modelo distinguir entre un valor estimado y el hecho de que no se observó nada. La falta de datos por motivos estructurales, relacionada con música en vivo, actuaciones de comedia, ofertas especiales y deportes en vivo, se representó mediante flags de disponibilidad como has_live_music y has_comedy. Una calificación en blanco puede significar que el servicio no existe, lo cual representa un estado empresarial diferente al de una calificación desconocida.
ORDINARY_MISSINGNESS_FLAGS = {
"facebook_popularity_quotient": "facebook_missing",
"instagram_popularity_quotient": "instagram_missing",
"overall_restaurant_rating": "overall_rating_missing",
"ambience": "ambience_missing",
"restaurant_tier": "tier_missing",
}
STRUCTURAL_AVAILABILITY_FLAGS = {
"live_music_rating": "has_live_music",
"comedy_gigs_rating": "has_comedy",
"value_deals_rating": "has_value_deals",
"live_sports_rating": "has_live_sports",
}
def add_missingness_features(df: pd.DataFrame) -> pd.DataFrame:
"""Add ordinary-null and structural-availability indicators before imputation."""
required_columns = set(ORDINARY_MISSINGNESS_FLAGS) | set(
STRUCTURAL_AVAILABILITY_FLAGS
)
missing_columns = sorted(required_columns - set(df.columns))
if missing_columns:
raise KeyError(f"Missing required columns for missingness features: {missing_columns}")
df = df.copy()
for column, flag in ORDINARY_MISSINGNESS_FLAGS.items():
df[flag] = df[column].isna().astype("int8")
for column, flag in STRUCTURAL_AVAILABILITY_FLAGS.items():
df[flag] = df[column].notna().astype("int8")
return df
5.3 Desorden categorial: 297 formas de escribir ciudades
La columna “Ciudad” contenía 297 formas brutas de escribir ciudades, correspondientes a un conjunto mucho más reducido de mercados reales. Variaban la mayúscula y los espacios en blanco, aparecían errores tipográficos y alias, y algunas entradas eran cadenas compuestas que nombraban varias ciudades. Una limpieza reproducible combinó una revisión mediante coincidencias difusas con rapidfuzz, un diccionario de alias cuidadosamente seleccionado y un análisis explícito de las entradas compuestas. Las marcas de ciudades raras y las características de frecuencia de las ciudades se mantuvieron, ya que tanto el tamaño del mercado como el soporte de datos pueden influir en las estimaciones de rotación. El paso de coincidencia solo propone candidatos para revisión humana y nunca modifica los valores por sí mismo:
def generate_city_candidate_matches(
city: pd.Series,
minimum_similarity: float = 80.0,
) -> pd.DataFrame:
"""Return nearest fuzzy matches for review; this never changes city values."""
normalized = normalize_city_values(city, aliases={})
counts = normalized.value_counts()
names = [name for name in counts.index if name != "unknown"]
rows = []
for raw_city in names:
candidates = [candidate for candidate in names if candidate != raw_city]
match = process.extractOne(raw_city, candidates, scorer=fuzz.ratio)
if match is not None and match[1] >= minimum_similarity:
rows.append(
{
"Raw City": raw_city,
"Candidate": match[0],
"Similarity": float(match[1]),
"Count": int(counts[raw_city]),
}
)
return pd.DataFrame(
rows,
columns=["Raw City", "Candidate", "Similarity", "Count"],
).sort_values(["Similarity", "Count", "Raw City"], ascending=[False, False, True])
5.4 Pares de variables
Tanto el tipo de restaurante como el nivel de la ciudad separan de manera significativa el volumen de negocios. El formato del establecimiento influye en el precio promedio de la comida, el volumen de transacciones y el modelo operativo, mientras que el nivel de la ciudad sirve como indicador aproximado de la densidad de demanda, el poder adquisitivo y el tamaño del mercado. Las calificaciones individuales fueron útiles, pero presentaban mucha variabilidad. La comida, la higiene, la relación calidad-precio, el servicio y el ambiente no son factores independientes; interactúan con el formato, la antigüedad del establecimiento, el mercado, la visibilidad social y la posición competitiva, lo que motivó el uso tanto de métodos de aprendizaje no lineales como de características de interacción cuidadosamente controladas.
5.5 La imagen general
Las correlaciones de Pearson y Spearman contaron historias diferentes. Varios predictores presentaban una relación monotónica mucho más fuerte que lineal con la rotación de empleados, por lo que existía una señal útil incluso en aquellos casos donde un ajuste con línea recta era deficiente. Se añadieron pruebas de soporte entre pares para evitar que las correlaciones extremas basadas en muy pocas observaciones superpuestas fueran confundidas con una estructura sólida.
5.6 Características basadas en el tiempo
Las fechas de apertura se dividieron en componentes calendáricos y características relacionadas con la antigüedad. Los restaurantes más antiguos tendían a ganar más, pero esto se consideró como una asociación y no como una afirmación causal, ya que la antigüedad podría ser un indicador del reconocimiento de marca, de los locales que siguen en funcionamiento, de la demanda recurrente o de la escala acumulada. Se utilizó una fecha de referencia fija para garantizar la reproducibilidad de esta característica.
6. Un problema de desequilibrio dentro de una tarea de regresión
La regresión no presenta desequilibrio de clases en el sentido de la clasificación, pero este objetivo tenía un análogo: la cola superior era rara y costosa. Bajo el RMSE, unos pocos residuos grandes pueden dominar el total de errores cuadráticos. En los diagnósticos, las observaciones peores representaban una proporción desproporcionada, por lo que un modelo podía parecer adecuado para la mayoría de los restaurantes y aún así obtener una calificación deficiente al omitir a unos pocos con ingresos elevados.
Por lo tanto, la validación utilizó una validación cruzada de 5 grupos estratificada por objetivo. El objetivo continuo se dividió en 10 intervalos de cuantiles únicamente para asignar los grupos, mientras que los modelos se entrenaban y evaluaban con las cifras brutas de facturación. Las verificaciones finales de promoción se repitieron con semillas independientes (17, 42 y 101). Esto redujo las posibilidades de que un candidato se convirtiera en campeón simplemente porque una división afortunada hubiera colocado la cola extrema en grupos más fáciles.
7. Ingeniería de características
Las características se dividieron en transformaciones sin estado y estadísticas ajustadas. La lógica sin estado funcionaba de manera idéntica en los datos de entrenamiento y de prueba. Todo lo que se ajustaba, ya fueran medias, mapas de frecuencia o codificaciones, se aprendía únicamente a partir de los datos de entrenamiento, se versionaba y se reutilizaba durante la inferencia. Esto permitía que el proceso fuera auditable y evitaba desviaciones sutiles entre los datos de entrenamiento y de prueba causadas por el preprocesamiento.
- Banderas de ausencia y disponibilidad, como se describió anteriormente.
- Características relacionadas con el calendario a partir de la fecha de inicio (año, mes, trimestre, día de la semana), además de la edad en meses y años y una categoría de “época de inicio” dividida en cuatro grupos.
- Un conteo de cocinas, obtenido al analizar la lista de cocinas separadas por comas.
- Una jerarquía de ciudades que va desde la ciudad en bruto hasta la ciudad limpia, luego a la ciudad principal y finalmente al estado, con banderas para entradas compuestas, regiones y alias, junto con indicadores de frecuencia e ciudades raras en tres umbrales.
El pipeline principal con parámetros fijos utilizó 71 predictores. Las familias de características adicionales diseñadas se probaron mediante ablation en lugar de mantenerse automáticamente, y una característica obtuvo su lugar gracias a un mejoramiento incremental en la validación, no por su atractivo intuitivo.
8. Incluir datos de referencia externos
Otra rama de investigación combinó las claves de ciudades y estados ya procesadas con datos geográficos y económicos públicos de referencia. La pregunta era si la capacidad del mercado local, la urbanización, la intensidad turística y el tamaño de la población aportaban información adicional más allá de las características propias del restaurante.
- Por ciudad: población, número y tamaño de los hogares, logaritmo de la población, coordenadas, además de marcadores sí/no para ciudades metropolitanas, regiones capitales nacionales, ciudades costeras, capitales estatales y ciudades con más de un millón de habitantes.
- A nivel estatal: población, población urbana y su proporción, densidad, PIB per cápita, PIB bruto estatal y PIB per cápita, así como visitas de turistas nacionales e internacionales, con un valor derivado de turismo per cápita.
El resultado fue deliberadamente conservador. Varios rasgos externos mostraron señales en pruebas aisladas, pero la familia no mejoró el conjunto final bajo el protocolo completo de sustitución y promoción, por lo que nunca llegó a ser el campeón definitivo. Es una lección útil para el modelado empresarial: una variable puede ser plausible y predictiva por sí sola, pero aporta poco cuando existen señales correlacionadas más fuertes.
9. Modelos y por qué
Los datos combinaban calificaciones continuas, brechas escasas y estructurales, además de categorías de alta cardinalidad; por lo tanto, varias familias de modelos compitieron como hipótesis en lugar de asumir que un algoritmo dominaría. Ridge y ElasticNet resultaron ser inesperadamente competitivos con CatBoost. Eso no fue una derrota para los métodos de boosting, sino que reflejó cuánta estructura había surgido en las características generadas artificialmente y cuán útil es la regularización cuando coexisten muchos predictores relacionados. Los modelos basados en árboles seguían siendo importantes, ya que capturaban interacciones no lineales que la familia lineal no puede expresar directamente.
10. Ajuste
Durante el desarrollo, cada familia tuvo su propio estudio con Optuna, evaluado mediante el RMSE fuera del conjunto de validación a partir de divisiones repetidas y estratificadas según el objetivo. La identificación de candidatos y su promoción se basaron en la validación interna, no en la tabla de clasificación, una distinción importante ya que la fase posterior de optimización para competiciones no debe confundirse con la generalización.
La combinación más efectiva integró CatBoost, Ridge, ElasticNet y una variante de Ridge con raíz cuadrada estabilizada por varianza (WR01) en proporciones de aproximadamente el 30%, 10%, 6% y 54%. Su RMSE fuera del conjunto de validación repetido fue de unos ₹20.10M. El alto peso asignado a WR01 era revelador: el modelo lineal transformado no era el mejor aprendiz independiente, pero su geometría de error diferente lo hacía muy valioso dentro del conjunto integrado.
11. Qué ayudó y qué no
Cada experimento se registró con su familia de características, configuración del modelo, diseño de validación, predicciones fuera de conjunto y decisión de selección. El progreso se logró rechazando las ideas débiles con la misma firmeza con que se promovían las fuertes.
- Primero se estableció un punto de referencia. Los predictores promedio y mediano fijaron un límite mínimo para el RMSE de aproximadamente ₹21.6 a 21.7 millones, lo que facilitó identificar modelos que eran simplemente complicados.
- La regularización fue importante. Ridge y ElasticNet superaron con creces al OLS tradicional, ya que el espacio generado contenía señales correlacionadas e interacciones.
- CatBoost aportó capacidad no lineal y categórica, pero no se suponía que su ajuste ayudaría. Un resultado prometedor de CatBoost mejoró la división de optimización en aproximadamente ₹258 mil, pero falló en la confirmación con semillas independientes y fue descartado por sobreajuste en la selección.
12. Puntuaciones de los diferentes campeones
El proyecto generó dos tipos de campeones: un modelo limpio seleccionado mediante validación cruzada repetida, y un campeón de competición ajustado al entorno de evaluación. Un tercer punto de control, S08, ilustra el efecto de una corrección intencionadamente leve en la distribución de resultados, antes de las fases más intensivas específicas para competiciones. Estas puntuaciones responden a preguntas diferentes y no son medidas intercambiables de generalización.
En el ámbito de la competición, la propuesta LB05 redujo el RMSE público en ₹9,195,461.72 con respecto a S08, lo que representa aproximadamente un 76.1%. Se trata de un resultado en la tabla de clasificación, no de evidencia de que el modelo listo para desplegar logre repentinamente una generalización con un RMSE de ₹2.89M. Aquí está el script que integra los componentes congelados y escribe la primera propuesta combinada:
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
import time
import json
import joblib
import numpy as np
import pandas as pd
from src import config, data, models, submission
from src.pipeline import FeaturePipeline
from main import _prepare_catboost_matrices, _prepare_model_matrices
S01_ID = "S01"
S01_WEIGHTS = {"catboost": 0.50, "ridge": 0.45, "elasticnet": 0.05}
def run(output_dir: Path | None = None) -> tuple[Path, Path]:
output_dir = output_dir or config.SUBMISSIONS_DIR
output_dir.mkdir(parents=True, exist_ok=True)
manifest_path = config.EXPERIMENTS_DIR / "frozen_v1_manifest.json"
if not manifest_path.exists():
raise FileNotFoundError(f"Frozen V1 manifest is required: {manifest_path}")
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if manifest.get("feature_pipeline") != "processed_v1" or manifest.get("predictor_count") != 71:
raise ValueError("Frozen V1 manifest does not describe the processed_v1 71-predictor contract")
train_raw = data.load_train()
test_raw = data.load_test()
pipeline = FeaturePipeline().fit(train_raw)
train = pipeline.transform(train_raw)
test = pipeline.transform(test_raw)
target = config.INTERNAL_TARGET
X_train = train.drop(columns=[target, config.INTERNAL_ID_COL], errors="ignore")
X_test = test.drop(columns=[config.INTERNAL_ID_COL], errors="ignore")
y = train[target].astype(float).to_numpy()
X_numeric, X_test_numeric = _prepare_model_matrices(X_train, X_test)
X_cat, X_test_cat = _prepare_catboost_matrices(X_train, X_test)
predictions = {}
timings = {}
start = time.perf_counter()
catboost = models.get_catboost(**{
key: value for key, value in manifest["catboost_config"].items()
if key != "native_categorical"
})
cat_features = [
index for index, dtype in enumerate(X_cat.dtypes)
if str(dtype) in {"object", "string", "category"}
]
catboost.fit(X_cat, y, cat_features=cat_features, verbose=False)
predictions["catboost"] = catboost.predict(X_test_cat)
timings["catboost_seconds"] = time.perf_counter() - start
start = time.perf_counter()
ridge = models.get_ridge(**manifest["ridge_config"])
ridge.fit(X_numeric, y)
predictions["ridge"] = ridge.predict(X_test_numeric)
timings["ridge_seconds"] = time.perf_counter() - start
start = time.perf_counter()
elasticnet = models.get_elasticnet(**manifest["elasticnet_config"])
elasticnet.fit(X_numeric, y)
predictions["elasticnet"] = elasticnet.predict(X_test_numeric)
timings["elasticnet_seconds"] = time.perf_counter() - start
weights = manifest["ensemble_weights"]
blend_prediction = sum(weights[name] * predictions[name] for name in weights)
submission_df = submission.build_submission(test_raw, blend_prediction)
submission_path = output_dir / "S01_frozen_v1_blend.csv"
submission.write_submission(submission_df, submission_path, expected_rows=len(test_raw))
component_paths = {}
for name, prediction in predictions.items():
component_paths[name] = output_dir / f"S01_{name}_predictions.csv"
pd.DataFrame({config.ID_COL: test_raw[config.ID_COL].to_numpy(), config.TARGET: prediction}).to_csv(
component_paths[name], index=False
)
metadata = {
"submission_id": S01_ID,
"model": "Frozen V1 Blend",
"feature_version": "frozen production",
"oof_rmse": 20097389.205713525,
"catboost_weight": S01_WEIGHTS["catboost"],
"ridge_weight": S01_WEIGHTS["ridge"],
"elasticnet_weight": S01_WEIGHTS["elasticnet"],
"public_leaderboard_rmse": None,
"leaderboard_rank": None,
"timestamp_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"submission_file": str(submission_path),
"component_files": {name: str(path) for name, path in component_paths.items()},
"fit_method": "fit_each_frozen_component_on_all_3493_processed_v1_training_rows",
"catboost_production_iterations_rule": "fixed_manifest_value_no_early_stopping",
"manifest_path": str(manifest_path),
**timings,
}
metadata_path = output_dir / "S01_metadata.json"
metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
return submission_path, metadata_path
if __name__ == "__main__":
for path in run():
print(path)
13. Rendimiento en competiciones versus generalización
Una vez que se congeló el modelo limpio y su protocolo de validación, una etapa separada de optimización para competiciones mejoró las predicciones para el conjunto de evaluación específico no utilizado previamente. Las mecánicas detrás de esto quedan fuera del alcance de esta explicación. Lo importante desde el punto de vista metodológico es que dicha etapa utilizó información del entorno de la competición, y no datos de entrenamiento recién etiquetados.
Por lo tanto, el RMSE final en la tabla de clasificación, de aproximadamente ₹2.893M, debe considerarse específico de la competición y potencialmente sobreajustado a ese conjunto de evaluación. La estimación más fiable de cómo rendiría el modelo subyacente en nuevos restaurantes sigue siendo el resultado del CV repetido, de alrededor de ₹20.10M. Es esencial mantener separados estos dos valores para una interpretación tanto científica como empresarial.
En una implementación real de pronóstico, la capa optimizada para competir se descartaría a menos que estuviera disponible un señal de calibración equivalente de antemano. El sistema desplegable utilizaría el pipeline de características y el conjunto ya definidos, monitorizaría los residuos y la desviación en la distribución, y se volvería a entrenar a medida que llegaran los datos reales de rotación.
14. Suposiciones
- Se supuso que los datos de entrenamiento y prueba provenían de procesos empresariales lo suficientemente similares como para que la validación cruzada tuviera sentido. No se realizó ninguna corrección formal por cambio de covariables más allá de las verificaciones de esquema y distribución.
- El número de registro era estrictamente un identificador y se excluyó del modelo final, ya que no tenía una relación clara y justificable con la rotación.
- Se asumió que las calificaciones faltantes de servicios incluían información estructural cuando dicho servicio estaba ausente, lo que motivó el diseño de la bandera de disponibilidad.
15. Recomendaciones
- Trate la calidad de los datos como un indicador del rendimiento del modelo. Las incoherencias en el esquema, los nombres de ciudades fragmentados y las categorías inconsistentes pueden anular la señal antes de que el algoritmo la detecte.
- Separe los valores “faltantes por ser desconocidos” de aquellos “faltantes porque la capacidad no existe”. Este último caso suele ser una característica, no un problema.
- En el caso de objetivos de ingresos sesgados, haga un seguimiento del RMSE de la cola y de la concentración del error al cuadrado, así como de la cifra total. Unos pocos clientes importantes pueden determinar si una previsión es útil desde el punto de vista financiero.
- Utilice mecanismos explícitos de promoción: descubrimiento de candidatos, confirmación mediante datos independientes y sustitución en conjuntos. Esto evita que una mejora observada en una división sea erróneamente considerada la mejor.
- Trate a la IA agente como un modelo operativo, no como una licencia para realizar promociones de forma autónoma. Deje que los agentes propongan, construyan y cuestionen, mientras que las decisiones importantes relacionadas con modelos y productos permanezcan en manos humanas.
- Almacene los registros de la información relevante en otro lugar distinto a la ventana de chat. Los conjuntos de datos, los manifiestos, las predicciones fuera de rango, los registros de experimentos, los mejores resultados fijados y las decisiones para promoverlos deben conservarse todos, de modo que un nuevo agente pueda basarse en esas pruebas y no en un desfile de mensajes anteriores.
- Al experimentar con inteligencia artificial, se debe separar las entornos DEV, UAT y PROD. La creatividad libre tiene lugar en DEV, mientras que la replicación y las pruebas rigurosas se realizan en UAT; PROD, por su parte, debe ser determinista, estable y reversible.
16. Lecciones aprendidas
La lección principal no es que un algoritmo haya ganado. Un enfoque disciplinado para plantear el problema, la auditoría de datos, la validación teniendo en cuenta casos extremos, la ingeniería de características, la diversidad de residuos y un flujo de trabajo controlado permitieron crear un sistema de pronóstico mejor que uno basado únicamente en la complejidad del modelo. Con DontPanic, los agentes trabajaron como equipo, acelerando el descubrimiento e implementación, mientras que controles humanos explícitos evitaron que la velocidad reemplazara al juicio crítico.
El campeón final de la competencia alcanzó un RMSE de aproximadamente 2.893 millones después de una fase de optimización adaptada al entorno de evaluación. Esa puntuación decidió el resultado de la competencia, pero se presenta por separado porque podría ser un sobreajuste y no debe considerarse como el error esperado en restaurantes futuros.
La lección más importante es práctica: realice pruebas de validación antes de cualquier ajuste, conserve los experimentos fallidos, solo considere los avances después de que se repitan y nunca confunda un modelo que supera a un punto de referencia con uno en el que se pueda confiar en producción. Y si participan varios agentes de IA, hágales jugar voleibol: permítales trabajar de manera agresiva, pero asegúrese de que las decisiones importantes vuelvan a un humano antes de que se anote el punto.