Ciencia de datos · Machine learning

51,000 anuncios, 190 filas: revisando un modelo de visitas para Kijiji.

En 2023, mi equipo construyó un modelo para predecir cuántas visitas recibe un anuncio de autos en Kijiji. Los gráficos se veían bien y el informe decía que las predicciones seguían a los valores reales. Tres años después volví a leer el código y encontré que el modelo se había entrenado con unas 190 filas en lugar de 51,000 anuncios, y que la forma de elegir sus predicciones miraba la respuesta. Esto es lo que salió mal, cómo lo reconstruí y lo que los datos dicen de verdad sobre las visitas.

En resumen

El pipeline de 2023 agrupaba los anuncios en totales por ciudad y mes, eliminaba cada conteo de visitas que marcara cualquiera de seis detectores de atípicos y elegía, para cada fila, el modelo que más se acercaba al valor real. La reconstrucción predice las visitas de cada anuncio, mantiene todo el preprocesamiento dentro de la validación cruzada y evalúa un conjunto de prueba una sola vez. Gradient boosting reduce el error absoluto medio en cerca de un 41% frente a una línea base de la mediana (158 contra 266 visitas). Lo que más explica las visitas no es el auto: es cuánto tiempo lleva publicado el anuncio. Código: GitHub.

El problema

Kijiji es uno de los sitios de clasificados en línea más grandes de Canadá, y los autos son una de sus categorías más activas. Las visitas son la parte alta del embudo de ventas: un anuncio que nadie abre no se vende. Un modelo que predice visitas a partir del contenido del anuncio puede decirle a un vendedor que su anuncio rinde menos que otros parecidos, ayudar a ordenar los listados y dar a los concesionarios una referencia para su inventario.

51,343anuncios de autos extraídos de Kijiji, con 28 columnas cada uno
1 díatodos se recolectaron el 27 de agosto de 2022
75visitas por anuncio en la mediana; el máximo es 54,064

Los datos son un dataset público de Kaggle (kijiji-car-sales-ads, CC BY-NC-SA 4.0) que cubre doce regiones de Kijiji en Columbia Británica, Alberta, Ontario y Quebec. Trabajamos en él como Grupo 6: Camilo Florez, Yasar Mansuri, Syed Zumair y yo.

Lo que construimos en 2023

Empezamos en un notebook de Jupyter y después pasamos el proyecto a un programa de línea de comandos en Python. Limpiaba los datos, generaba una docena de gráficos exploratorios, eliminaba atípicos y entrenaba tres regresores (regresión lineal, random forest y gradient boosting) por separado para cada ciudad, con una división 60/40. El informe comparaba el promedio de visitas reales con el promedio de visitas predichas por año y concluía que las predicciones tenían "un comportamiento similar a las reales". Random forest se "eligió" para cerca de la mitad de las filas, y una ejecución completa tardaba unos 30 minutos.

Viéndolo hoy, dos cosas del informe debieron preocuparnos. El README describía el proyecto como predicción del precio, aunque el objetivo eran las visitas. Y leía los gráficos como estacionalidad: julio y agosto eran "los grandes meses" y la mayoría de anuncios se publicó en agosto, algo "relacionado con el verano".

Al volver: cinco problemas

Al leer el código en 2026 encontré problemas que impedían confiar en los resultados de 2023:

  • El modelo miraba la respuesta. Para cada fila, el código calculaba el error de cada modelo contra las visitas reales y se quedaba con la predicción de menor error. Eso es escoger la respuesta más cercana a la verdad después de ver la verdad, así que la precisión reportada no era algo que el modelo pudiera lograr con un anuncio nuevo.
  • 51,000 anuncios se convirtieron en unas 190 filas. Justo antes de entrenar, los datos se agrupaban en el total de visitas por ciudad, mes y año. Las únicas variables que quedaban eran el mes y el año. Eso deja entre 2 y 22 filas por ciudad para entrenar, y luego se predecía sobre todas las filas, incluidas las de entrenamiento.
  • Atípicos eliminados por la unión de seis detectores. Se borraba cualquier conteo de visitas marcado por Z score, Z score robusto, IQR, recorte por percentiles, DBSCAN o Isolation Forest. Las funciones de DBSCAN e Isolation Forest tenían errores de índices, y los anuncios muy populares que eliminaban eran reales, no errores.
  • Valores escritos en las filas equivocadas. Los valores imputados se asignaban de vuelta con índices desalineados, y se imputaba el propio objetivo en lugar de descartar los anuncios sin conteo de visitas.
  • Un "no" que significaba sí. La opción --storage_model no se guardaba como un miembro de enum, que Python evalúa como verdadero, así que los modelos se guardaban siempre.

Ninguno de estos errores es raro. Elegir resultados usando la etiqueta, agregar hasta perder la unidad que se quiere predecir y borrar datos incómodos son errores que también he visto en código de producción. Un gráfico de promedio predicho contra promedio real esconde los tres, porque los promedios coinciden aunque las predicciones individuales no.

Leer bien los datos

Antes de modelar otra vez, tenía que entender qué es realmente la columna visit. Dos hechos lo cambiaron todo:

  • Es una foto, no una serie de tiempo. El archivo solo contiene los anuncios que seguían en línea el día de la extracción. Los anuncios más antiguos tuvieron más tiempo para venderse y desaparecer, así que cuanto más atrás el mes de publicación, menos anuncios quedan. Agosto "domina" por la fecha en que se recolectaron los datos, no por la demanda de verano.
  • Las visitas son acumuladas. La columna cuenta todas las visitas desde que se publicó el anuncio, así que un anuncio con dos meses en línea ha tenido muchas más oportunidades de ser visto que uno publicado ayer. La antigüedad del anuncio tiene que ser una variable.

Las visitas además están muy sesgadas: mediana 75, media 294, percentil 99 de unas 3,350. Unos pocos anuncios virales dominarían cualquier modelo entrenado con los conteos brutos, así que los modelos nuevos aprenden log(1 + visitas) y luego convierten de vuelta.

La reconstrucción

El nuevo pipeline predice las visitas de cada anuncio. Limpia lo que está claramente mal (precios de relleno por debajo de 100 dólares, odómetros de más de 1,000,000 km, años de modelo anteriores a 1940, anuncios sin conteo de visitas) y conserva 50,540 anuncios. La imputación, el escalado y el one-hot encoding ocurren dentro de un Pipeline de scikit-learn, así que solo aprenden de los folds de entrenamiento. Los detectores de atípicos siguen ahí, ahora vectorizados y correctos, pero solo para reportar: no se ponen de acuerdo, marcan entre el 0.04% y el 16% de los anuncios, lo cual es una buena razón para no borrar filas basándose en ninguno.

La división es 80/20, estratificada por deciles de visitas. Cuatro candidatos se comparan con validación cruzada de 5 folds sobre el conjunto de entrenamiento, y el ganador se evalúa una sola vez sobre el conjunto de prueba:

Modelo (CV de 5 folds)MAE (visitas)R²R² (escala log)
Gradient boosting164 ± 60.400.82
Random forest177 ± 70.300.80
Regresión ridge194 ± 70.240.75
Línea base: siempre la mediana273 ± 7−0.07−0.01

En los 10,108 anuncios de prueba, que nunca se vieron durante el entrenamiento ni la selección, gradient boosting tiene un error absoluto medio de 158 visitas contra 266 de la línea base, y un error mediano de 32 visitas contra 70. Los resultados de prueba coinciden con la validación cruzada, así que el modelo no está sobreajustado. La ejecución completa tarda unos dos minutos en mi entorno, en lugar de 30.

Las dos columnas de R² cuentan historias distintas, y ambas son ciertas. En escala logarítmica, 0.82 significa que el modelo acierta el orden de magnitud: distingue un anuncio de 20 visitas de uno de 2,000. En visitas brutas, 0.40 significa que no capta el tamaño de los pocos anuncios virales, que tiende a subestimar.

Un gráfico de promedios puede coincidir perfectamente con la realidad mientras cada predicción individual está mal. Evalúa al nivel en el que se toma la decisión.

Qué explica las visitas

La importancia por permutación, cuánto crece el error de prueba al desordenar una variable, pone la antigüedad del anuncio muy por delante de todo lo demás, seguida de la edad del auto, el precio, la ciudad y el tipo de vendedor. El equipamiento, el color y el día de la semana de publicación casi no aportan. El análisis exploratorio apunta en la misma dirección:

  • Los vendedores particulares reciben unas tres veces más visitas que los concesionarios en autos usados (mediana 226 contra 67). Los autos nuevos de concesionarios reciben una mediana de solo 8.
  • Los autos más viejos, baratos y con más kilómetros atraen más visitas. Correlación de Spearman con las visitas: edad del auto +0.55, kilómetros +0.47, antigüedad del anuncio +0.35, precio −0.35.
  • La región importa mucho. Calgary (mediana 115) y la ciudad de Toronto (112) reciben muchas más visitas por anuncio que Vancouver (11) o North Shore (8).

Son asociaciones en una sola foto, no causas. El sedán usado de un particular probablemente atrae a un público distinto que la camioneta nueva de un concesionario, y el modelo no puede separar ambas cosas.

Lo que aprendí

  • Nunca elijas una predicción usando la etiqueta. Si el valor real participa en seleccionar el resultado, la métrica mide la selección, no el modelo.
  • Modela al nivel de la decisión. La pregunta era sobre un anuncio. Agregar a totales por ciudad y mes descartó los datos y, con ellos, la pregunta.
  • Entiende cómo se recolectaron los datos. Una sola fecha de extracción explica tanto la "estacionalidad" como por qué la antigüedad del anuncio pesa tanto.
  • Reporta los atípicos antes de borrarlos. Cuando seis detectores difieren por un factor de 400, ninguno te está diciendo la verdad sobre tus datos.
  • Empieza con una línea base. "Predecir siempre la mediana" le da sentido a todos los demás números.
  • Prueba la plomería. El nuevo repositorio tiene pruebas con pytest sobre datos sintéticos y las ejecuta junto con ruff en GitHub Actions en cada push. Una prueba habría detectado el error del "no que significa sí".

Dónde encajan los asistentes de IA

Igual que al revisar un clasificador de marketing bancario, usé un asistente de programación con IA para leer el código antiguo, rastrear dónde se transformaban los datos y escribir el nuevo pipeline y sus pruebas. Detectó rápido que la selección del modelo usaba la etiqueta y escribió rápido el código repetitivo de validación cruzada. Decidir qué significaba realmente el objetivo, qué enfoque usaría el negocio y qué resultados valía la pena reportar siguió siendo mi trabajo.

Próximos pasos

  • Corregir el sesgo de supervivencia. Los anuncios que se vendieron rápido, a menudo los más atractivos, faltan en una sola foto. Varias extracciones permitirían un mejor objetivo, como las visitas en los primeros siete días.
  • Agregar las señales que faltan. Las fotos, el texto de la descripción y si el anuncio está promocionado no están en los datos, y probablemente explican buena parte de lo que el modelo no capta en los anuncios virales.
  • Ajustar. Una búsqueda aleatoria de hiperparámetros para gradient boosting y target encoding para columnas con muchas categorías, como modelo y versión.

El proyecto y el código

Datos
Kijiji car sales ads en Kaggle, extraídos el 27 de agosto de 2022, CC BY-NC-SA 4.0
Equipo (2023)
Grupo 6: Erwin Mongui, Camilo Florez, Yasar Mansuri, Syed Zumair
Modelo (2026)
HistGradientBoostingRegressor sobre log(1 + visitas), elegido con validación cruzada de 5 folds
Resultado de prueba
MAE de 158 visitas contra 266 de la línea base de la mediana; error mediano de 32 visitas; R² de 0.82 en escala logarítmica
Tecnologías
Python, pandas, scikit-learn, matplotlib, pytest, ruff, GitHub Actions
Código
training-ml-python-kijiji en GitHub: el pipeline reconstruido, los gráficos, las pruebas y el informe completo (en inglés)

Erwin MonguiDirector de Tecnología en Cedar Planters e ingeniero certificado en Inteligencia Artificial (Lambton College, 2024), en Toronto.

Recibe mis próximos casos y artículos en la Búsqueda de Google.