Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

332
Vistas
¿Los bucles for en pandas son realmente malos? ¿Cuándo debería importarme?

¿Los bucles for son realmente "malos"? Si no, ¿en qué situación(es) sería mejor que usar un enfoque "vectorizado" más convencional? 1

Estoy familiarizado con el concepto de "vectorización" y cómo pandas emplea técnicas vectorizadas para acelerar el cálculo. Las funciones vectorizadas transmiten operaciones sobre toda la serie o DataFrame para lograr aceleraciones mucho mayores que la iteración convencional sobre los datos.

Sin embargo, estoy bastante sorprendido de ver una gran cantidad de código (incluidas las respuestas en Stack Overflow) que ofrece soluciones a problemas que implican recorrer datos mediante bucles for y listas de comprensión. La documentación y la API dicen que los bucles son "malos" y que uno "nunca" debe iterar sobre matrices, series o tramas de datos. Entonces, ¿cómo es que a veces veo usuarios que sugieren soluciones basadas en bucles?


1 - Si bien es cierto que la pregunta suena un tanto amplia, lo cierto es que hay situaciones muy concretas en las que los bucles for suelen ser mejores que iterar datos de forma convencional. Esta publicación tiene como objetivo capturar esto para la posteridad.

over 4 years ago · Santiago Trujillo
2 Respuestas
Responde la pregunta

0

TLDR; No, los bucles for no son "malos" en general, al menos, no siempre. Probablemente sea más exacto decir que algunas operaciones vectorizadas son más lentas que iterar , en lugar de decir que la iteración es más rápida que algunas operaciones vectorizadas. Saber cuándo y por qué es clave para obtener el máximo rendimiento de su código. En pocas palabras, estas son las situaciones en las que vale la pena considerar una alternativa a las funciones de pandas vectorizadas:

  1. Cuando sus datos son pequeños (... dependiendo de lo que esté haciendo),
  2. Cuando se trata de object /tipos mixtos
  3. Al usar las funciones de acceso str /regex

Examinemos estas situaciones individualmente.


Iteración v/s Vectorización en Small Data

Pandas sigue un enfoque de "convención sobre configuración" en su diseño de API. Esto significa que la misma API se ha ajustado para atender una amplia gama de datos y casos de uso.

Cuando se llama a una función pandas, las siguientes cosas (entre otras) deben ser manejadas internamente por la función, para garantizar el funcionamiento

  1. Alineación de índice/eje
  2. Manejo de tipos de datos mixtos
  3. Manejo de datos faltantes

Casi todas las funciones tendrán que lidiar con estos en mayor o menor medida, y esto presenta una sobrecarga . La sobrecarga es menor para las funciones numéricas (por ejemplo, Series.add ), mientras que es más pronunciada para las funciones de cadena (por ejemplo, Series.str.replace ).

Los buclesfor , por otro lado, son más rápidos de lo que piensas. Lo que es aún mejor es que las listas por comprensión (que crean listas mediante bucles for ) son aún más rápidas, ya que son mecanismos iterativos optimizados para la creación de listas.

Las comprensiones de listas siguen el patrón

 [f(x) for x in seq]

Donde seq es una serie de pandas o una columna DataFrame. O, cuando se opera en múltiples columnas,

 [f(x, y) for x, y in zip(seq1, seq2)]

Donde seq1 y seq2 son columnas.

Comparación numérica
Considere una operación de indexación booleana simple. El método de comprensión de listas se ha sincronizado con Series.ne ( != ) y query . Aquí están las funciones:

 # Boolean indexing with Numeric value comparison. df[df.A != df.B] # vectorized != df.query('A != B') # query (numexpr) df[[x != y for x, y in zip(df.A, df.B)]] # list comp

Para simplificar, he usado el paquete perfplot para ejecutar todas las pruebas de timeit en esta publicación. Los tiempos para las operaciones anteriores son los siguientes:

ingrese la descripción de la imagen aquí

La comprensión de listas supera a la query para N de tamaño moderado, e incluso supera a la comparación vectorial no igual a N pequeña. Desafortunadamente, la comprensión de listas se escala linealmente, por lo que no ofrece mucha ganancia de rendimiento para N más grandes.

Nota
Vale la pena mencionar que gran parte del beneficio de la comprensión de la lista proviene de no tener que preocuparse por la alineación del índice, pero esto significa que si su código depende de la alineación de la indexación, esto fallará. En algunos casos, se puede considerar que las operaciones vectorizadas sobre las matrices NumPy subyacentes aportan "lo mejor de ambos mundos", lo que permite la vectorización sin toda la sobrecarga innecesaria de las funciones pandas. Esto significa que puede reescribir la operación anterior como

 df[df.A.values != df.B.values]

Lo que supera tanto a los pandas como a los equivalentes de comprensión de listas:

La vectorización NumPy está fuera del alcance de esta publicación, pero definitivamente vale la pena considerarla, si el rendimiento es importante.

Valor cuenta
Tomando otro ejemplo, esta vez, con otra construcción de python vainilla que es más rápida que un bucle for: collections.Counter . Un requisito común es calcular los recuentos de valores y devolver el resultado como un diccionario. Esto se hace con value_counts , np.unique y Counter :

 # Value Counts comparison. ser.value_counts(sort=False).to_dict() # value_counts dict(zip(*np.unique(ser, return_counts=True))) # np.unique Counter(ser) # Counter

ingrese la descripción de la imagen aquí

Los resultados son más pronunciados, Counter gana sobre ambos métodos vectorizados para un rango más amplio de N pequeño (~3500).

Nota
Más curiosidades (cortesía @user2357112). El Counter se implementa con un acelerador de C , por lo que, aunque todavía tiene que funcionar con objetos de Python en lugar de los tipos de datos de C subyacentes, sigue siendo más rápido que un bucle for . ¡Poder de pitón!

Por supuesto, lo importante de aquí es que el rendimiento depende de sus datos y caso de uso. El objetivo de estos ejemplos es convencerlo de que no descarte estas soluciones como opciones legítimas. Si estos todavía no le dan el rendimiento que necesita, siempre hay cython y numba . Agreguemos esta prueba a la mezcla.

 from numba import njit, prange @njit(parallel=True) def get_mask(x, y): result = [False] * len(x) for i in prange(len(x)): result[i] = x[i] != y[i] return np.array(result) df[get_mask(df.A.values, df.B.values)] # numba

ingrese la descripción de la imagen aquí

Numba ofrece la compilación JIT de código Python loopy a código vectorizado muy potente. Comprender cómo hacer que numba funcione implica una curva de aprendizaje.


Operaciones con tipos de object /mixtos

Comparación basada en cadenas
Volviendo al ejemplo de filtrado de la primera sección, ¿qué pasa si las columnas que se comparan son cadenas? Considere las mismas 3 funciones anteriores, pero con el DataFrame de entrada convertido en cadena.

 # Boolean indexing with string value comparison. df[df.A != df.B] # vectorized != df.query('A != B') # query (numexpr) df[[x != y for x, y in zip(df.A, df.B)]] # list comp

ingrese la descripción de la imagen aquí

Entonces, ¿qué cambió? Lo que hay que tener en cuenta aquí es que las operaciones con cadenas son intrínsecamente difíciles de vectorizar. Pandas trata las cadenas como objetos, y todas las operaciones en los objetos recurren a una implementación lenta y descabellada.

Ahora, debido a que esta implementación descabellada está rodeada por todos los gastos generales mencionados anteriormente, existe una diferencia de magnitud constante entre estas soluciones, aunque se escalan de la misma manera.

Cuando se trata de operaciones en objetos mutables/complejos, no hay comparación. La comprensión de listas supera todas las operaciones que involucran dictados y listas.

Acceder a los valores del diccionario por clave
Aquí hay tiempos para dos operaciones que extraen un valor de una columna de diccionarios: el map y la comprensión de la lista. La configuración se encuentra en el Apéndice, bajo el título "Fragmentos de código".

 # Dictionary value extraction. ser.map(operator.itemgetter('value')) # map pd.Series([x.get('value') for x in ser]) # list comprehension

ingrese la descripción de la imagen aquí

Indexación de lista posicional
Tiempos para 3 operaciones que extraen el elemento 0 de una lista de columnas (manejo de excepciones), map , método de acceso str.get y la comprensión de la lista:

 # List positional indexing. def get_0th(lst): try: return lst[0] # Handle empty lists and NaNs gracefully. except (IndexError, TypeError): return np.nan
 ser.map(get_0th) # map ser.str[0] # str accessor pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]) # list comp pd.Series([get_0th(x) for x in ser]) # list comp safe

Nota
Si el índice importa, querrías hacer:

 pd.Series([...], index=ser.index)

Al reconstruir la serie.

ingrese la descripción de la imagen aquí

Aplanamiento de lista
Un ejemplo final es el aplanamiento de listas. Este es otro problema común y demuestra cuán poderoso es Python puro aquí.

 # Nested list flattening. pd.DataFrame(ser.tolist()).stack().reset_index(drop=True) # stack pd.Series(list(chain.from_iterable(ser.tolist()))) # itertools.chain pd.Series([y for x in ser for y in x]) # nested list comp

ingrese la descripción de la imagen aquí

Tanto itertools.chain.from_iterable como la comprensión de listas anidadas son construcciones puras de python y escalan mucho mejor que la solución de stack .

Estos tiempos son una fuerte indicación del hecho de que pandas no está equipado para trabajar con tipos de d mixtos, y que probablemente debería abstenerse de usarlo para hacerlo. Siempre que sea posible, los datos deben estar presentes como valores escalares (ints/floats/strings) en columnas separadas.

Por último, la aplicabilidad de estas soluciones depende en gran medida de sus datos. Por lo tanto, lo mejor que puede hacer es probar estas operaciones en sus datos antes de decidir qué hacer. Observe cómo no he cronometrado la apply de estas soluciones, porque sesgaría el gráfico (sí, es así de lento).


Operaciones .str y métodos de acceso .str

Pandas puede aplicar operaciones de expresiones regulares como str.contains , str.extract y str.extractall , así como otras operaciones de cadenas "vectorizadas" (como str.split , str.find , str.translate , etc.) en cadenas columnas Estas funciones son más lentas que las listas de comprensión y están destinadas a ser más funciones de conveniencia que cualquier otra cosa.

Por lo general, es mucho más rápido compilar previamente un patrón de expresiones regulares e iterar sobre sus datos con re.compile (consulte también ¿Vale la pena usar re.compile de Python? ). La compilación de lista equivalente a str.contains se parece a esto:

 p = re.compile(...) ser2 = pd.Series([x for x in ser if p.search(x)])

O,

 ser2 = ser[[bool(p.search(x)) for x in ser]]

Si necesita manejar NaN, puede hacer algo como

 ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]

La compilación de lista equivalente a str.extract (sin grupos) se verá así:

 df['col2'] = [p.search(x).group(0) for x in df['col']]

Si necesita manejar no coincidencias y NaN, puede usar una función personalizada (¡aún más rápido!):

 def matcher(x): m = p.search(str(x)) if m: return m.group(0) return np.nan df['col2'] = [matcher(x) for x in df['col']]

La función matcher es muy extensible. Se puede ajustar para devolver una lista para cada grupo de captura, según sea necesario. Simplemente extraiga la consulta del atributo de group o groups del objeto comparador.

Para str.extractall , cambie p.search a p.findall .

Extracción de cadenas
Considere una operación de filtrado simple. La idea es extraer 4 dígitos si va precedido de una letra mayúscula.

 # Extracting strings. p = re.compile(r'(?<=[AZ])(\d{4})') def matcher(x): m = p.search(x) if m: return m.group(0) return np.nan ser.str.extract(r'(?<=[AZ])(\d{4})', expand=False) # str.extract pd.Series([matcher(x) for x in ser]) # list comprehension

ingrese la descripción de la imagen aquí

Más ejemplos
Divulgación completa: soy el autor (en parte o en su totalidad) de estas publicaciones que se enumeran a continuación.

  • Eliminación rápida de puntuación con pandas

  • Concatenación de cadenas de dos columnas pandas

  • Eliminar partes no deseadas de cadenas en una columna

  • Reemplace todo menos la última aparición de un carácter en un marco de datos


Conclusión

Como se muestra en los ejemplos anteriores, la iteración brilla cuando se trabaja con pequeñas filas de tramas de datos, tipos de datos mixtos y expresiones regulares.

La aceleración que obtenga depende de sus datos y su problema, por lo que su millaje puede variar. Lo mejor que puede hacer es realizar pruebas cuidadosamente y ver si el pago vale la pena.

Las funciones "vectorizadas" brillan por su simplicidad y legibilidad, por lo que si el rendimiento no es crítico, definitivamente debería preferirlas.

Otra nota al margen, ciertas operaciones de cadena se ocupan de las restricciones que favorecen el uso de NumPy. Aquí hay dos ejemplos en los que la cuidadosa vectorización de NumPy supera a Python:

  • Cree una nueva columna con valores incrementales de una manera más rápida y eficiente - Respuesta de Divakar

  • Eliminación rápida de puntuación con pandas - Respuesta de Paul Panzer

Además, a veces, solo operar en las matrices subyacentes a través de .values en lugar de Series o DataFrames puede ofrecer una aceleración lo suficientemente saludable para la mayoría de los escenarios habituales (consulte la Nota en la sección Comparación numérica anterior). Entonces, por ejemplo, df[df.A.values != df.B.values] mostraría aumentos de rendimiento instantáneos sobre df[df.A != df.B] . Usar .values puede no ser apropiado en todas las situaciones, pero es un truco útil saberlo.

Como se mencionó anteriormente, depende de usted decidir si vale la pena implementar estas soluciones.


Apéndice: fragmentos de código

 import perfplot import operator import pandas as pd import numpy as np import re from collections import Counter from itertools import chain

<!- ->

 # Boolean indexing with Numeric value comparison. perfplot.show( setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B']), kernels=[ lambda df: df[df.A != df.B], lambda df: df.query('A != B'), lambda df: df[[x != y for x, y in zip(df.A, df.B)]], lambda df: df[get_mask(df.A.values, df.B.values)] ], labels=['vectorized !=', 'query (numexpr)', 'list comp', 'numba'], n_range=[2**k for k in range(0, 15)], xlabel='N' )

<!- ->

 # Value Counts comparison. perfplot.show( setup=lambda n: pd.Series(np.random.choice(1000, n)), kernels=[ lambda ser: ser.value_counts(sort=False).to_dict(), lambda ser: dict(zip(*np.unique(ser, return_counts=True))), lambda ser: Counter(ser), ], labels=['value_counts', 'np.unique', 'Counter'], n_range=[2**k for k in range(0, 15)], xlabel='N', equality_check=lambda x, y: dict(x) == dict(y) )

<!- ->

 # Boolean indexing with string value comparison. perfplot.show( setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B'], dtype=str), kernels=[ lambda df: df[df.A != df.B], lambda df: df.query('A != B'), lambda df: df[[x != y for x, y in zip(df.A, df.B)]], ], labels=['vectorized !=', 'query (numexpr)', 'list comp'], n_range=[2**k for k in range(0, 15)], xlabel='N', equality_check=None )

<!- ->

 # Dictionary value extraction. ser1 = pd.Series([{'key': 'abc', 'value': 123}, {'key': 'xyz', 'value': 456}]) perfplot.show( setup=lambda n: pd.concat([ser1] * n, ignore_index=True), kernels=[ lambda ser: ser.map(operator.itemgetter('value')), lambda ser: pd.Series([x.get('value') for x in ser]), ], labels=['map', 'list comprehension'], n_range=[2**k for k in range(0, 15)], xlabel='N', equality_check=None )

<!- ->

 # List positional indexing. ser2 = pd.Series([['a', 'b', 'c'], [1, 2], []]) perfplot.show( setup=lambda n: pd.concat([ser2] * n, ignore_index=True), kernels=[ lambda ser: ser.map(get_0th), lambda ser: ser.str[0], lambda ser: pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]), lambda ser: pd.Series([get_0th(x) for x in ser]), ], labels=['map', 'str accessor', 'list comprehension', 'list comp safe'], n_range=[2**k for k in range(0, 15)], xlabel='N', equality_check=None )

<!- ->

 # Nested list flattening. ser3 = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']]) perfplot.show( setup=lambda n: pd.concat([ser2] * n, ignore_index=True), kernels=[ lambda ser: pd.DataFrame(ser.tolist()).stack().reset_index(drop=True), lambda ser: pd.Series(list(chain.from_iterable(ser.tolist()))), lambda ser: pd.Series([y for x in ser for y in x]), ], labels=['stack', 'itertools.chain', 'nested list comp'], n_range=[2**k for k in range(0, 15)], xlabel='N', equality_check=None )

<!- _>

 # Extracting strings. ser4 = pd.Series(['foo xyz', 'test A1234', 'D3345 xtz']) perfplot.show( setup=lambda n: pd.concat([ser4] * n, ignore_index=True), kernels=[ lambda ser: ser.str.extract(r'(?<=[AZ])(\d{4})', expand=False), lambda ser: pd.Series([matcher(x) for x in ser]) ], labels=['str.extract', 'list comprehension'], n_range=[2**k for k in range(0, 15)], xlabel='N', equality_check=None )
over 4 years ago · Santiago Trujillo Denunciar

0

En breve

  • for loop + iterrows es extremadamente lento. La sobrecarga no es significativa en ~1k filas, pero se nota en más de 10k filas.
  • for loop + itertuples es mucho más rápido que iterrows o apply .
  • la vectorización suele ser mucho más rápida que itertuples

Punto de referencia ingrese la descripción de la imagen aquí

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda