¿Por qué mi código es tan lento? Una guía para crear perfiles de Py-Spy Python

Los problemas frustrantes que hay que depurar en el código de ciencia de datos no son errores de sintaxis ni errores lógicos. Más bien, provienen de un código que hace exactamente lo que se supone que debe hacer, pero se toma su tiempo para hacerlo.

El código funcional pero ineficiente puede ser un enorme cuello de botella en el flujo de trabajo de la ciencia de datos. En este artículo, proporcionaré una breve introducción y un recorrido por py-spy, una poderosa herramienta diseñada para perfilar su código Python. Puede identificar exactamente dónde dedica más tiempo su programa para poder identificar y corregir las ineficiencias.

Problema de ejemplo

Configuremos una pregunta de investigación simple para escribir código:

"Para todos los vuelos entre estados y territorios de EE. UU., ¿qué aeropuerto de salida tiene los vuelos más largos en promedio?"

A continuación se muestra un script de Python simple para responder esta pregunta de investigación, utilizando datos recuperados de la Oficina de Estadísticas de Transporte (BTS). El conjunto de datos consta de datos de cada vuelo dentro de los estados y territorios de EE. UU. entre enero y junio de 2025 con información sobre los aeropuertos de origen y destino. Son aproximadamente 3,5 millones de filas.

Calcula la distancia Haversine (la distancia más corta entre dos puntos en una esfera) para cada vuelo. Luego, agrupa los resultados por aeropuerto de salida para encontrar la distancia promedio e informa los cinco primeros.

importar pandas como pd importar matemáticas importar tiempo def haversine(lat_1, lon_1, lat_2, lon_2): """Calcular la distancia de Haversine entre dos puntos de latitud y longitud""" lat_1_rad = math.radians(lat_1) lon_1_rad = math.radians(lon_1) lat_2_rad = math.radians(lat_2) lon_2_rad = math.radians(lon_2) delta_lat = lat_2_rad – lat_1_rad delta_lon = lon_2_rad – lon_1_rad R = 6371 # Radio de la Tierra en km return 2*R*math.asin(math.sqrt(math.sin(delta_lat/2)**2 + math.cos(lat_1_rad)*math.cos(lat_2_rad)*(math.sin(delta_lon/2))**2)) if __name__ == '__main__': # Cargar datos de vuelo en un marco de datos vuelo_data_file = r"./data/2025_flight_data.csv" vuelos_df = pd.read_csv(flight_data_file) # Iniciar el cronómetro para ver cuánto tiempo lleva el análisis start = time.time() # Calcular la distancia haversine entre el aeropuerto de inicio y fin de cada vuelo haversine_dists =[]para i, fila en vuelos_df.iterrows(): haversine_dists.append(haversine(lat_1=fila["LATITUDE_ORIGIN"], lon_1=fila["LONGITUDE_ORIGIN"], lat_2=fila["LATITUDE_DEST"], lon_2=fila["LONGITUDE_DEST"])) vuelos_df["Distancia"] = haversine_dists # Obtenga el resultado agrupando por aeropuerto de origen, tomando la distancia promedio de vuelo e imprimiendo los 5 primeros resultados = ( vuelos_df .groupby('DISPLAY_AIRPORT_NAME_ORIGIN').agg(avg_dist=('Distance', 'mean')) .sort_values('avg_dist', ascending=False) ) print(result.head(5)) # Finalizar el cronómetro e imprimir el análisis fin del tiempo = tiempo.tiempo() print(f"Tomó {final – inicio} s")

Al ejecutar este código se obtiene el siguiente resultado:

avg_dist DISPLAY_AIRPORT_NAME_ORIGIN Pago Pago International 4202.493567 Guam International 3142.363005 Luis Munoz Marin International 2386.141780 Ted Stevens Anchorage International 2246.530036 Daniel K Inouye International 2211.857407 Took 169.8935534954071s

Estos resultados tienen sentido, ya que los aeropuertos enumerados se encuentran en Samoa Americana, Guam, Puerto Rico, Alaska y Hawái, respectivamente. Todos estos son lugares fuera de los Estados Unidos contiguos donde uno esperaría distancias de vuelo promedio largas.

El problema aquí no son los resultados, que son válidos, sino el tiempo de ejecución: ¡casi tres minutos! Si bien tres minutos pueden ser tolerables para una ejecución única, se convierte en un factor que acaba con la productividad durante el desarrollo. Imagínese esto como parte de un proceso de datos más largo. Cada vez que se modifica un parámetro, se corrige un error o se vuelve a ejecutar una celda, se ve obligado a permanecer inactivo mientras se ejecuta el programa. Esa fricción interrumpe tu flujo y convierte un análisis rápido en un asunto que dura toda la tarde.

Ahora veamos cómo py-spy puede ayudarnos a diagnosticar exactamente qué líneas tardan tanto.

¿Qué es Py-Spy?

Para comprender qué hace py-spy y los beneficios de usarlo, es útil comparar py-spy con el generador de perfiles integrado de Python cProfile.

cProfile: este es un generador de perfiles de seguimiento que funciona de manera similar a un cronómetro en cada llamada de función. El tiempo entre cada llamada de función y el retorno se mide y se informa. Si bien es muy preciso, esto añade una sobrecarga significativa, ya que el generador de perfiles tiene que pausar y registrar datos constantemente, lo que puede ralentizar significativamente el script. py-spy: este es un generador de perfiles de muestreo que funciona de manera similar a una cámara de alta velocidad que observa todo el programa a la vez. py-spy se encuentra completamente fuera del script Python en ejecución y toma instantáneas de alta frecuencia del estado del programa. Examina toda la "pila de llamadas" para ver exactamente qué línea de código se está ejecutando y qué función la llamó, hasta el nivel superior.

Ejecutando Py-spy

Para ejecutar py-spy en un script de Python, la biblioteca py-spy debe estar instalada en el entorno de Python.

instalación de pip py-spy

Una vez instalada la biblioteca py-spy, se puede crear un perfil de nuestro script ejecutando el siguiente comando en la terminal:

registro py-spy -o perfil.svg -r 100 –python main.py

Esto es lo que realmente hace cada parte de este comando:

py-spy: llama a la herramienta. grabar: esto le dice a py-spy que use su modo "grabar", que monitoreará continuamente el programa mientras se ejecuta y guarda los datos. -o perfil.svg: esto especifica el nombre del archivo de salida y el formato, indicándole que genere los resultados como un archivo SVG llamado perfil.svg. -r 100: Esto especifica la frecuencia de muestreo, configurándola en 100 veces por segundo. Esto significa que py-spy comprobará lo que hace el programa 100 veces por segundo. –: Esto separa el comando py-spy del comando del script Python. Le dice a py-spy que todo lo que sigue a este indicador es el comando a ejecutar, no argumentos para py-spy en sí. python main.py: este es el comando para ejecutar el script de Python que se va a perfilar con py-spy, en este caso ejecutando main.py.

Nota: Si se ejecuta en Linux, los privilegios sudo suelen ser un requisito para ejecutar py-spy, por razones de seguridad.

Una vez que este comando termine de ejecutarse, aparecerá un archivo de salida perfil.svg que nos permitirá profundizar en qué partes del código están tardando más.

Salida de py-spy

Salida de Icicle Graph de py-spy

Al abrir el perfil de salida.svg se revela la visualización que py-spy ha creado de cuánto tiempo pasó nuestro programa en diferentes partes del código. Esto se conoce como gráfico de carámbano (o, a veces, gráfico de llama si el eje y está invertido) y se interpreta de la siguiente manera:

Barras: Cada barra de color representa una función particular que fue llamada durante la ejecución del programa. Eje X (Población): El eje horizontal representa la recolección de todas las muestras tomadas durante la elaboración del perfil. Están agrupados de modo que el ancho de una barra particular represente la proporción del total de muestras que el programa estaba en la función representada por esa barra. Nota: Esta no es una línea de tiempo; el orden no representa cuándo se llamó la función, solo el volumen total de tiempo invertido. Eje Y (profundidad de la pila): el eje vertical representa la pila de llamadas. La barra superior denominada "todos" representa el programa completo y las barras debajo representan funciones llamadas desde "todos". Esto continúa recursivamente con cada barra dividida en las funciones que se llamaron durante su ejecución. La barra inferior muestra la función que realmente se estaba ejecutando en la CPU cuando se tomó la muestra.

Interactuando con el gráfico

Si bien la imagen de arriba es estática, el archivo .svg real generado por py-spy es completamente interactivo. Cuando lo abre en un navegador web, puede:

Buscar (Ctrl+F): resalte funciones específicas para ver dónde aparecen en la pila. Zoom: haga clic en cualquier barra para ampliar esa función específica y sus elementos secundarios, lo que le permitirá aislar partes complejas de la pila de llamadas. Colocar el cursor sobre cualquier barra: muestra el nombre de la función específica, la ruta del archivo, el número de línea y el porcentaje exacto de tiempo que consumió.

La regla más crítica para leer el gráfico de carámbanos es simple: cuanto más ancha sea la barra, más frecuente será la función. Si una barra de funciones abarca el 50% del ancho del gráfico, significa que el programa estuvo trabajando en la ejecución de esa función durante el 50% del tiempo de ejecución total.

Diagnóstico

En el gráfico de carámbanos de arriba, podemos ver que la barra que representa la función iterrows() de Pandas es notablemente ancha. Al pasar el cursor sobre esa barra al visualizar el archivo perfil.svg, se revela que la proporción real para esta función fue del 68,36%. Entonces, más de 2/3 del tiempo de ejecución se dedicaron a la función iterrows(). Intuitivamente, este cuello de botella tiene sentido, ya que iterrows() crea un objeto Pandas Series para cada fila del bucle, lo que provoca una sobrecarga masiva. Esto revela un objetivo claro para intentar optimizar el tiempo de ejecución del script.

Optimizando el guión

El camino más claro para optimizar este script basándose en lo aprendido de py-spy es dejar de usar iterrows() para recorrer cada fila y calcular la distancia haversine. En su lugar, debería reemplazarse con un cálculo vectorizado usando NumPy que realizará el cálculo para cada fila con una sola llamada a la función. Entonces los cambios a realizar son:

Reescriba la función haversine() para utilizar operaciones NumPy de nivel C vectorizadas y eficientes que permitan pasar matrices completas en lugar de un conjunto de coordenadas a la vez. Reemplace el bucle iterrows() con una sola llamada a esta función haversine() recién vectorizada. importar pandas como pd importar numpy como np importar tiempo def haversine(lat_1, lon_1, lat_2, lon_2): """Calcular la distancia de Haversine entre dos puntos de latitud y longitud""" lat_1_rad = np.radians(lat_1) lon_1_rad = np.radians(lon_1) lat_2_rad = np.radians(lat_2) lon_2_rad = np.radians(lon_2) delta_lat = lat_2_rad – lat_1_rad delta_lon = lon_2_rad – lon_1_rad R = 6371 # Radio de la Tierra en km return 2*R*np.asin(np.sqrt(np.sin(delta_lat/2)**2 + np.cos(lat_1_rad)*np.cos(lat_2_rad)*(np.sin(delta_lon/2))**2)) if __name__ == '__main__': # Cargar datos de vuelo en un marco de datos vuelo_data_file = r"./data/2025_flight_data.csv" vuelos_df = pd.read_csv(flight_data_file) # Iniciar temporizador para ver cuánto tiempo lleva el análisis start = time.time() # Calcular la distancia haversine entre el inicio y el final de cada vuelo en el aeropuerto. lon_2=flights_df["LONGITUDE_DEST"]) # Obtenga el resultado agrupando por aeropuerto de origen, tomando la distancia promedio de vuelo e imprimiendo los 5 primeros resultados = ( vuelos_df .groupby('DISPLAY_AIRPORT_NAME_ORIGIN').agg(avg_dist=('Distance', 'mean')) .sort_values('avg_dist', ascending=False) ) print(result.head(5)) # Finalizar el cronómetro y tiempo de análisis de impresión end = time.time() print(f"Tomó {end – start} s")

Al ejecutar este código se obtiene el siguiente resultado:

avg_dist DISPLAY_AIRPORT_NAME_ORIGIN Pago Pago International 4202.493567 Guam International 3142.363005 Luis Munoz Marin International 2386.141780 Ted Stevens Anchorage International 2246.530036 Daniel K Inouye International 2211.857407 Took 0.5649983882904053 s

Estos resultados son idénticos a los resultados anteriores a la optimización del código, pero en lugar de tardar casi tres minutos en procesarse, ¡tomó poco más de medio segundo!

Mirando hacia el futuro

Si está leyendo esto desde el futuro (finales de 2026 o más allá), verifique si está ejecutando Python 3.15 o posterior. Se espera que Python 3.15 introduzca un generador de perfiles de muestreo nativo en la biblioteca estándar, que ofrezca una funcionalidad similar a py-spy sin requerir instalación externa. Para cualquiera que utilice Python 3.14 o versiones anteriores, py-spy sigue siendo el estándar de oro.

Este artículo exploró una herramienta para abordar una frustración común en la ciencia de datos: un script que funciona según lo previsto, pero está escrito de manera ineficiente y tarda mucho tiempo en ejecutarse. Se proporcionó un script de ejemplo para saber qué aeropuertos de salida de EE. UU. tienen la distancia de vuelo promedio más larga según la distancia de Haversine. Este script funcionó como se esperaba, pero tardó casi tres minutos en ejecutarse.

Usando el generador de perfiles Python py-spy, pudimos aprender que la causa de la ineficiencia era el uso de la función iterrows(). Al reemplazar iterrows() con un cálculo vectorizado más eficiente de la distancia de Haversine, el tiempo de ejecución se optimizó de tres minutos a poco más de medio segundo.

Consulte mi repositorio de GitHub para ver el código de este artículo, incluido el preprocesamiento de los datos sin procesar de BTS.

¡Gracias por leer!

Fuentes de datos

Los datos de la Oficina de Estadísticas de Transporte (BTS) son un trabajo del gobierno federal de EE. UU. y son de dominio público según 17 USC § 105. Es de uso gratuito, compartido y adaptado sin restricciones de derechos de autor.