¿Que és pandas?
Pandas es una biblioteca de código abierto escrita en Python que proporciona estructuras de datos de alto rendimiento y fáciles de utilizar, así como herramientas de análisis de datos. Se suele utilizar para manipulación y análisis de datos. Utiliza(ba) NumPy por debajo (ahora verás el porqué de ese "(ba)").
Pandas proporciona herramientas para leer y escribir datos en diferentes formatos (como CSV, Excel o SQL), y ofrece una amplia gama de funciones para el análisis de datos, como la agregación, la agrupación, la fusión o la transformación de datos. Con pandas, los usuarios pueden realizar tareas como la limpieza, la manipulación y transformación, la agregación y la visualización de datos de manera eficiente y sencilla.
Pandas proporciona dos estructuras de datos principales: Series
y DataFrame
:
- Un
Series
es un objeto similar a un array de una sola dimensión que puede contener cualquier tipo de datos, como números, cadenas o booleanos.
- Un
DataFrame
es una estructura de datos tabular bidimensional que puede contener varias columnas de diferentes tipos de datos: números, cadenas, booleanos... o incluso otros objetos Series
o DataFrames
.
Pandas es, hoy por hoy, una biblioteca esencial para cualquier persona que trabaje con datos en Python.
pandas 2.0
Pandas 2.0 por fin está aquí. Se trata de una versión que lleva mucho tiempo en desarrollo y que era muy esperada por la comunidad.
Esta versión viene con nuevas funciones, correcciones de errores y, sobre todo, con cambios internos muy importantes para ofrecer un rendimiento muy mejorado (pero tienes que leer las salvedades más abajo).
IMPORTANTE: Se recomienda que actualices tu código a pandas 1.5.3 antes de dar el salto directo a pandas 2.0. De este modo, te aseguras de que va a funcionar, pero al mismo tiempo te generará mensajes de advertencia sobre funciones descatalogadas (deprecated) o problemas que te puede dar la versión 2.0. Ten en cuenta que hay más de 150 características "deprecadas" en esta versión que se han ido anunciando en las sucesivas 1.x en los últimos años.
Vamos a ver las novedades más importantes de esta versión...
¿Adiós NumPy, hola Apache Arrow?
La actualización principal de esta versión tiene que ver con el uso de Apache Arrow (PyArrow) en lugar de NumPy como "backend" para pandas. Este cambio puede parecer poco más que una curiosidad interna, sobre todo porque se ha tenido mucho cuidado con romper la compatibilidad lo menos posible, pero sus implicaciones son muy importantes.
Apache Arrow está orientado hacia el rendimiento y la eficiencia en el manejo de datos en memoria, tanto tabulares como jerárquicos, además de sacar el máximo partido a las modernas GPUs.
MUY IMPORTANTE: aunque verás en todas partes reseñado que ahora Apache Arrow ha sustituido a NumPy, no es cierto. NumPy sigue estando ahí y sigue siendo el "backend" por defecto. Existe un nuevo "backend" basado en Arrow (pyarrow) que puedes utilizar y que es del que te estoy hablando en este apartado, pero no soporta aún todas las operaciones del anterior y por eso no es todavía el que se utiliza por defecto. Aun así, es por donde va el futuro y, por lo tanto, deberías prestarle mucha atención.
Solo por el cambio de NumPy a Apache Arrow tenemos multitud de ventajas, entre las que cabe destacar (fuente: Marc García, Datapythonista - pandas 2.0 and the Arrow revolution (part I)):
- Velocidad: como decíamos antes, Arrow está centrado en el rendimiento y supera por mucho a NumPy, especialmente con algunos tipos de datos, como por ejemplo las cadenas de caracteres:
Operación |
NumPy |
Arrow |
Ganancia de velocidad |
read parquet (50Mb) |
141 ms |
87 ms |
1.6x |
mean (int64) |
2.03 ms |
1.11 ms |
1.8x |
mean (float64) |
3.56 ms |
1.73 ms |
2.1x |
endswith (string) |
471 ms |
14.9 ms |
31.6x |
- Tipos de datos: a pesar de que NumPy proporciona un gran soporte para valores enteros, flotantes, booleanos y fecha y hora, se queda corto para otros tipos de datos. Arrow tiene un mejor soporte para fechas y horas, utiliza un solo bit para tipos booleanos (en lugar de los 8 bits de NumPy, usando 8 veces menos memoria), y tiene soporte nativo para otros tipos como campos de tipo bit, valores decimales y otros tipos complejos.
- Categorías: Arrow presenta otra gran ventaja para una operación muy común: las categorías o etiquetas. Arrow tiene un tipo específico para ello que, en lugar de almacenar cadenas de texto para cada elemento categorizado que tengamos, internamente usa un índice numérico para cada posible valor, con las cadenas relacionadas guardadas en otra parte y solo una vez. Esto parece algo evidente, pero no se podía hacer con NumPy. Gracias a ello se puede ahorrar muchísima memoria en conjuntos de datos grandes categorizados o con muchas cadenas que se repiten, algo con lo que es muy habitual encontrarse.
- Valores perdidos: representar de forma eficiente los valores que faltan en los datos de partida para el análisis puede ser muy complicado, y es un problema muy frecuente. En Python se puede utilizar
None
como comodín para indicar que un objeto falta. Sin embargo, en las representaciones internas de datos hay que tratar de aprovechar todos los bits de memoria y, en muchas ocasiones, la única opción es forzar la conversión de datos numéricos (incluso enteros) a coma flotante para usar NaN
, un valor especial que se puede almacenar en memoria junto al resto de números. Para evitar estas conversiones, recientemente pandas comenzó a permitir el uso de una matriz auxiliar para almacenar las posiciones de los valores perdidos. Este problema viene solucionado de serie con Arrow, que ya incluye en sus representaciones internas formas de indicar valores perdidos, por lo que no son necesarias conversiones ni matrices adicionales.
- Interoperabilidad: en este ámbito los beneficios de Arrow vienen por duplicado. En primer lugar, porque es más fácil y más estándar compartir datos entre diferentes programas con esta biblioteca. Y en segundo, porque permite compartir datos directamente en memoria, de modo que dos programas que necesiten los mismos datos no tienen que duplicarlos, sino que usan los mismos 😲. Vale, esto no es muy habitual, pero cuando lo necesites, la diferencia de rendimiento será abismal.
Y todo ello, además, procurando que se rompa la compatibilidad hacia atrás lo menos posible, de forma que impacte poco en nuestros desarrollos y sea relativamente fácil migrar a la versión 2.0.
En el artículo de Marc García referenciado al principio de este epígrafe, del equipo "core" de pandas, puedes encontrar un montón de ejemplos y código sobre todo esto.
Índices con tipos arbitrarios dtype
En versiones anteriores, un índice solo admitía los tipos dtype
de NumPy: int64
, float64
y uint64
. Esto generaba los tipos Index
específicos: Int64Index
, Float64Index
o UInt64Index
. Estas clases ahora han desaparecido (se habían declarado como obsoletas en pandas 1.4), y todos los índices numéricos se representan como la clase única Index
, pero con un dtype
asociado, por ejemplo:
In [1]: pd.Index([1, 2, 3], dtype="int64")
Out[1]: Index([1, 2, 3], dtype='int64')
In [2]: pd.Index([1, 2, 3], dtype="int32")
Out[2]: Index([1, 2, 3], dtype='int32')
Esto tiene la ventaja adicional de que muchas operaciones que antes implicaban la creación de índices de 64 bit ahora pueden crear índices de orden inferior, por ejemplo de 32 bits (consumiendo la mitad de memoria), consiguiendo mayor eficiencia y rendimiento.
Cambio de comportamiento en numeric_only
para funciones de agregación
En versiones anteriores, podíamos llamar a funciones de agregación en un DataFrame
con tipos heterogéneos (varios tipos mezclados en los datos) y obtener diferentes resultados de esta operación. Por ejemplo, a veces esa agregación funcionaba y simplemente dejaba fuera a los dtype
no numéricos. En otras ocasiones simplemente se generaba un error y punto.
El argumento numeric_only
ahora es consistente y funcionará al aplicarse en un DataFrame
con tipos de datos no numéricos. Para obtener el mismo comportamiento que antes puedes establecer numeric_only
a True
o hacer que tu DataFrame
solo contenga columnas numéricas y así evitarás eliminar por error columnas relevantes de dicho DataFrame
al hacer la agregación.
Esto es lo que pasaba en versiones anteriores al calcular la media de los valores de un DataFrame
que no tenía solo números:
In[2] df = DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
In[3] df.mean()
Out[3]:
a 2.0
dtype: float64
Como ves, en este caso funcionaba, pero no utilizaba los valores no numéricos que había. En pandas 2.0 esta operación ahora genera un error para evitar descartar columnas relevantes en estas agregaciones:
TypeError: Could not convert ['xyz'] to numeric
Otras mejoras
- Mejor soporte para
dtypes
anulables y matrices de extensión: internamente, muchas operaciones ahora usan semántica anulable en lugar de conversión a objeto (boxing) cuando se usan dtypes
anulables como Int64
, boolean
o Float64
. El manejo interno de los arrays de extensión ha ido mejorando constantemente ya en las versiones 1.x hasta llegar a los actuales de la 2.0. Estas mejoras se traducen un montón de ganancias de rendimiento.
DataFrames
respaldados por Pyarrow: ya en la versión 1.5.0 se incluyó una nueva matriz de extensión que permitía a los usuarios crear DataFrames
respaldados por matrices PyArrow
. Estas matrices de extensión proporcionan una gran mejora al operar en columnas con cadenas de texto, ya que la representación de objetos NumPy para esto no es muy eficiente, como se comentó antes. La API que salió con 1.5.0 era bastante experimental y seguía usando NumPy para casi todo. Con pandas 2.0 y PyArrow 7.0 ahora es posible utilizar en muchos más métodos las funciones de cálculo de PyArrow correspondientes, mejorando significativamente el rendimiento y eliminando muchas advertencias de rendimiento.
- Resolución de no nanosegundos en marcas de tiempo: un problema en versiones anteriores era que las marcas de tiempo (
TimeStamps
) siempre se representaban con una resolución de nanosegundos. Como consecuencia, no había forma de representar fechas anteriores al 1 de enero de 1970 o posteriores al 11 de abril de 2264. Esto traía problemas en ciertas comunidades de investigadores que analizan datos de series temporales que abarcan periodos mucho mayores. La versión 2.0 introduce compatibilidad con otras resoluciones, y añade soporte para resolución de segundos, milisegundos y microsegundos. Esto permite rangos de tiempo de hasta +/- 2.9e11 años y, por lo tanto, debería cubrir los casos de uso más habituales.
- Mejoras en Copy-on-Write (CoW): cualquier
DataFrame
o Serie
derivada de otra en cualquier modo siempre se comporta como una copia. Debido a ello, sólo se pueden cambiar los valores de un objeto modificando el objeto en sí, no sobre una copia. CoW no permite actualizar un DataFrame
o una Serie
que comparte datos con otro objeto DataFrame
o Serie
. En pandas 2.0 casi todos los métodos utilizan un mecanismo de copia diferida para retrasar el copiado de los datos subyacentes el mayor tiempo posible. Sin CoW habilitado, la mayoría de los métodos realizan copias defensivas para evitar efectos secundarios cuando un objeto se modifica más adelante. Esto da como resultado un alto uso de memoria y un tiempo de ejecución relativamente alto. Copy-on-Write permite eliminar todas esas copias defensivas y diferir las copias reales hasta que se modifiquen de verdad los datos de un objeto. El resultado es un mejor rendimiento en general. Más información en: Copy-on-Write (CoW) en la documentación oficial de pandas.
- Mejoras en el manejo de fechas y horas: ahora no solo son mucho mas eficientes y rápidas (hasta 10 veces más rápidas) sino que el manejo de datos de tipo
DateTime
en pandas es mucho más consistente.
En las notas de la versión puedes encontrar muchos más detalles de todo esto.