Web scraping para Machine Learning
Tienda de coches
Imagen de Betty Longbottom / Archbold Carshop — Church Street / CC BY-SA 2.0

Hay una regla universal para comprar cosas, “pagar menos dinero es mejor que pagar más dinero”. Por razones personales necesitaba comprar un coche usado pero no tenía prisa, tuve tiempo para pensarlo y encontrar la mejor oferta.

Al comprobar las tiendas de coches de segunda mano de la zona, me mostraron coches de entre 7.000 y 10.000 euros. Esto es mucho dinero, así que pensé que debería usar mis habilidades de análisis de datos para encontrar la mejor oferta.

Recopilación de datos

Lo primero que necesitamos para un proyecto de machine learning son los datos. Como podrás imaginar, no hay ningún dataset disponible con datos de coches de segunda mano en el mercado español, e incluso si los hubiera, quedarían desactualizados mes a mes ya que los precios cambian continuamente.

Obtuve datos de las páginas de anuncios clasificados más populares en España para coches usados. Mantendré en secreto los nombres de las páginas que he utilizado por razones de privacidad. Las restricciones utilizadas para las búsquedas fueron las siguientes:

  • Distancia de mi: menos de 100 km
  • Edad del coche: menos de 10 años
  • Precio: menos de 10.000 euros

Revisemos las herramientas utilizadas para obtener los datos de las web:

  • urllib.request: Utilicé este módulo en la biblioteca estándar de Python para descargar el código html de los sitios web.
  • BeautifulSoup4: Utilicé este módulo para analizar el código HTML y encontrar la información relevante.

Necesitamos descargar dos cosas, una página que contenga una lista de coches disponibles con nuestras restricciones y una página para el coche con toda su información.

Para la lista de coches algunos ejemplos de URL podrían ser:

https://www.webdeanuncios/cars-search/?location=Barcelona&uptoprice=10000&yearfrom=2010

Para un cierto coche, la URL podría ser algo así:

https://www.webdeanuncios/cars/?carid=141238

Usando urllib descargamos y guardamos estas páginas:

from urllib.request import urlopendef download_and_save_page(url, file):
    html_code = urlopen(url).read()#.decode('utf-8')
    f = open(file, 'wb')
    f.write(html_code)
    f.close()
    return html_code

En primer lugar, scrapeamos la página con la lista de coches para encontrar todos los enlaces a los coches disponibles y descargarlos también. Esto es fácil de hacer con el método find_all de BeautifulSoup

links = soup.find_all('a', href=True, class_='car-link')
for link in links:
    print(link['href'])

Ten en cuenta que las páginas de anuncios clasificados suelen seguir un sistema de paginación, por lo que en cada página obtendremos una cierta cantidad de coches listados (por ejemplo 20), para poder tener todos los coches disponibles necesitamos entender cómo funciona el sistema de paginación. Normalmente sólo tenemos que añadir un número de página a la URL, por ejemplo:

https://www.webdeanuncios/cars-search/?location=Barcelona&uptoprice=10000&yearfrom=2010&page=7

Usando este código podemos crear una base de datos de enlaces a coches y alimentarlos a la función de descarga para poner en marcha nuestro web-scraper y descargar páginas con información sobre todos los coches disponibles.

El siguiente paso es procesar todas las páginas descargadas y extraer la información relevante sobre los coches. Toda la información se guarda en un archivo CSS. Los datos que decidí guardar fueron:

  • URL del anuncio (para poder comprobarlo más tarde manualmente cuando encuentre algo interesante)
  • Marca del coche
  • Modelo del coche
  • Tipo de vendedor (Particular/ profesional)
  • Precio
  • Descripción
  • Año
  • Kms
  • Fecha de publicación
  • Lista de URLs con imágenes

Terminé no usando algunos de los campos (descripciones e imágenes) ya que agregaban mucha complejidad, sin embargo pueden contener información relevante, puedes ver la condición del auto mirando las imágenes, y las descripciones muchas veces hablan de los extras que tiene el coche y pueden dar información relevante (partes dañadas del motor)


La extracción de los datos se hizo principalmente con los métodos find y find_all de BeautifulSoup, buscando los campos relevantes en la página.

soup.find('div', class_='car-price').get_text()

Es importante encontrar los nombres de las clases correctos, lo hice con las herramientas de desarrollo de Firefox (abrir el inspector con Ctrl+Mayús+C).

Ejemplo de web con el inspector de Firefox
Seleccionar un campo de un sitio web con el inspector de Firefox

Con el inspector seleccionamos todos los campos relevantes de la página y tomamos nota de su información.

  • ¿Son elementos <div>, <p> o qué tipo de elementos?
  • Cómo identificarlos a partir de todos los elementos del mismo tipo (id único o nombre de clase único)

Uniendo todos estos elementos podemos crear un archivo CSV con todos los datos de los coches. En mi caso el fichero CSV era de 5 MB e incluía los datos de 3487 coches. Estos datos son sólo para los coches que se vendían en diciembre de 2019 y dentro de los criterios de búsqueda descritos anteriormente. Sería interesante obtener datos más antiguos, pero no estaban disponibles en línea.

Análisis de datos

Ahora a la parte divertida, abrimos nuestro Jupyter notebook y empezamos a analizar los datos.

#Load data
df = pd.read_csv('data/car_data.csv')

Os presento a Pandas Profiling, un módulo de Python que genera informes desde un DataFrame. El informe nos proporciona una rápida visualización de las estadísticas de todas las variables de nuestro dataset.

from pandas_profiling import ProfileReport
prof = ProfileReport(df)
prof.to_file('output.html')
información estadística para la variable kms
Información estadística para la variable kms
Histograma de la variable kms
Histograma de la variable kms
Pestaña valores extremos de pandas profiling
En la pestaña de valores extremos podemos ver valores que son error de escritura (coches con más de un millón de kms)

Algunas estadísticas interesantes son las marcas y modelos más comunes que se venden en España:

marcas de coche más vendidas en España
Modelos de coche más vendidos de España

Después de esta rápida visualización, he comprobado las marcas menos comunes en el dataset y me he dado cuenta de que algunas marcas de coches sólo tienen un par de entradas. Las he borrado porque los pocos datos que tenía harían que el algoritmo hiciera overfit a estas marcas en particular.

Entonces apliqué one-hot encoding para las marcas y modelos y acabé con 371 variables.

Utilicé una división de train/test para comprobar la precisión de los modelos de ML y RMSE para evaluar su rendimiento. La variable objetivo a predecir era el precio del coche.


Los modelos de los que quiero hablar son XGBoost, con el que obtuve el mejor valor para RMSE y KNN, que me parece un modelo interesante para esta aplicación ya que lo que estamos haciendo de forma intuitiva a la hora de buscar el precio de un coche, es compararlo con otros coches similares en venta.

XGBoost:

import xgboost as xgb
model=xgb.XGBRegressor(random_state=1, n_estimators=5000, learning_rate=0.01)
model.fit(X_train, y_train)sqrt(mean_squared_error(y_val, model.predict(X_val)))

Obtuve un RMSE de 1.288,39. Nótese que esta magnitud es comparable a euros de error. Es un valor alto, ten en cuenta que solo estamos mirando coches más baratos de 10.000 euros y con un precio medio de 7.226 euros.

KNN:

from sklearn.neighbors import KNeighborsRegressor
knn = KNeighborsRegressor(n_neighbors=5)
knn.fit(X_train, y_train)sqrt(mean_squared_error(y_val, model.predict(X_val)))

Aunque esperaba buenos resultados del enfoque intuitivo de este modelo, fracasó miserablemente al dar una RMSE de 7.242.

Conclusiones

Mi principal logro con este proyecto es que pude encontrar coches tan buenos como los que vi en las tiendas cercanas a mí por un mejor precio, pudiendo encontrar coches en el rango de 4.000 a 5.000 euros, las mejores ofertas disponibles en el momento.

Finalmente encontré una gran oferta para un Citroën C4 con el que estoy muy contento.

Citroen C4
El coche que compré

Algunas cosas que me gustaría mencionar, los datos que se extraen de una página de anuncios clasificados no significan que los coches publicados allí a un determinado precio terminen vendiéndose por ese precio. La mayoría de las veces los vendedores bajan su precio después de hablar con los compradores y esto no se refleja en el sitio. Otros vendedores pueden poner un precio ridículamente alto y no recibir llamadas ya que nadie quiere comprar su coche a ese precio. Todo esto le da un sesgo al algoritmo para dar precios más altos que los reales.

Había muchos anuncios con información faltante, a veces faltaba la edad del coche, a veces el número de kms, etc… Otras veces los datos estaban mal formateados, los vendedores no introducían los datos en el campo apropiado y terminaban con un anuncio en el que faltaba la mayoría de las características, pero sin embargo introducían todos estos datos con su propio formato en la descripción, algo que el algoritmo no puede interpretar. Todo esto afecta a la calidad de los datos y por lo tanto a nuestros resultados.

Otras formas de mejorar este análisis incluyen el uso de NLP para añadir información de la descripción, los vendedores a veces hablan de los extras incluidos en el coche, a veces hablan del motor o de la condición del coche, a veces dicen que hay un problema mecánico. Creo que se podría extraer mucha información interesante de esa descripción. Otra forma es usar computer vision para obtener información extra de las fotos de los coches publicados con los anuncios, a veces se puede ver que el auto tuvo algún accidente, a veces se ve el interior del coche, a veces no hay fotos lo cual es una mala señal.

Estos son sólo algunos de los muchos caminos disponibles para seguir mejorando los resultados de este proyecto, sin embargo, después de comprar el coche, no siento el impulso de seguir trabajando en este proyecto.

Albert Sanchez Lafuente

Estudié Ingenieria Industrial en la UPC. Continué mi formación en inteligencia artificial junto a la asociación Saturdays.AI de la que he formado parte del equipo de Barcelona desde sus inicios.