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 columnasCon 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()