Quant500 Academy

5. Descargar el S&P 500 completo: web scraping y limpieza de datos

Nivel 3: Datos de Mercado y APIs

Hasta ahora has trabajado con un único ticker. Pero comparar estrategias, construir un screener o simplemente investigar el mercado en conjunto requiere trabajar con cientos de activos a la vez — y nadie va a escribir a mano la lista de los 500 tickers del S&P 500.

La lista actualizada de componentes del S&P 500 está publicada en Wikipedia, y se puede extraer automáticamente con web scraping: descargar el HTML de la página y buscar dentro de él la tabla que contiene los tickers, usando la librería BeautifulSoup.

import requests
import bs4 as bs

respuesta = requests.get("http://en.wikipedia.org/wiki/List_of_S%26P_500_companies")
sopa = bs.BeautifulSoup(respuesta.text, "lxml")
tabla = sopa.find("table", {"class": "wikitable sortable"})

tickers = []
for fila in tabla.findAll("tr")[1:]:
    ticker = fila.findAll("td")[0].text.replace("\n", "")
    tickers.append(ticker)

[1:] en findAll('tr')[1:] descarta la primera fila de la tabla, que es la cabecera con los nombres de columna, no un ticker real. Con la lista completa de tickers, descargar todos los precios a la vez es tan simple como pasarle la lista entera a yfinance en vez de un único string:

import yfinance as yf
import datetime as dt

inicio = dt.datetime(2018, 11, 1)
fin = dt.datetime(2020, 1, 31)

datos = yf.download(tickers, start=inicio, end=fin)["Adj Close"]

El resultado es un único DataFrame con una columna por cada empresa. Y aquí llega el problema casi inevitable de trabajar con cientos de activos reales: datos faltantes. Empresas que salieron a bolsa después de tu fecha de inicio, que fueron excluidas del índice antes de tu fecha final, o simples errores de la fuente de datos, dejan huecos (NaN) en la tabla.

pandas ofrece dropna() para limpiar esos huecos, pero con matices importantes según qué argumentos le pases:

# elimina columnas (empresas) que no tienen NINGUN dato en todo el rango
datos_limpios = datos.dropna(axis="columns", how="all")

# elimina filas (dias) que no tienen NINGUN dato para ninguna empresa
datos_limpios = datos_limpios.dropna(axis="index", how="all")

# elimina columnas que tengan AL MENOS UN dato faltante, en cualquier fila
datos_limpios = datos_limpios.dropna(axis="columns", how="any")

El orden importa, y también el criterio how. how='all' es conservador: solo borra si TODOS los valores de esa fila o columna faltan. how='any' es agresivo: borra en cuanto falta UN SOLO valor. Aplicar how='any' como primer paso sobre 500 empresas normalmente te dejaría con muy pocas columnas, porque basta que una empresa tenga un solo día sin dato para perderla entera — por eso se aplica al final, después de ya haber descartado lo claramente vacío.

Una vez limpios, puedes seleccionar subconjuntos concretos de la tabla con iloc, que indexa por posición numérica en vez de por nombre:

datos.iloc[0]          # la primera fila (el primer dia)
datos.iloc[:, 0]        # la primera columna (la primera empresa)
datos.iloc[0:5, 0:2]    # las primeras 5 filas y las primeras 2 columnas

Con la tabla limpia, calcular los retornos logarítmicos de las 500 empresas a la vez es, de nuevo, una operación vectorizada de una sola línea gracias a NumPy y pandas trabajando juntos:

import numpy as np

retornos = np.log(datos_limpios).diff().dropna()

Para ampliar esta lección

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.