El modelo mental de NumPy y pandas
NumPy y pandas suelen enseñarse como dos bibliotecas separadas con dos API separadas, pero debajo de la sintaxis comparten un modelo.
Los datos numéricos viven en arrays contiguos y homogéneamente tipados, y casi todas las operaciones que escribes son una solicitud para ejecutar un bucle en código compilado en lugar de en el intérprete de Python.
Esta página trata sobre ese modelo compartido: qué hacen realmente la vectorización y la difusión bajo la sintaxis, cómo las Series, DataFrames e Índices de pandas construyen una estructura etiquetada sobre los arrays crudos de NumPy, y dónde el motor perezoso de Polars rompe por completo con este modelo.
Conceptos básicos de análisis de datos repasa la API concreta para un primer análisis; esta página trata sobre la máquina que hay debajo.
Resumen
- NumPy almacena datos numéricos homogéneos en memoria contigua y traslada los bucles a código compilado (vectorización); pandas añade filas y columnas etiquetadas (Series, DataFrame, Index) sobre ese mismo modelo de array, y la alineación se realiza por etiqueta en lugar de por posición.
- Por qué importa: El código que "parece igual" puede comportarse de manera muy diferente dependiendo de si activa una ufunc vectorizada, una difusión, una unión alineada por índice o una materialización completa; leer mal cuál de ellas está ocurriendo es la mayor fuente de código lento y errores silenciosos en el análisis de datos.
- Conceptos Clave: vectorización, difusión (broadcasting), dtype, Index y alineación, ufunc, evaluación perezosa (lazy evaluation).
- Cuándo usarlo: Cualquier transformación numérica sobre una columna o array, uniones/combinaciones basadas en etiquetas, agregaciones que necesiten respetar la identidad de la fila, y cualquier punto donde decidas si el modelo eager de pandas o el modelo lazy de Polars se ajustan al tamaño de tus datos.
- Limitaciones / Compensaciones: La vectorización y la evaluación eager todavía materializan cada resultado intermedio en memoria, la alineación puede introducir silenciosamente
NaNs cuando los índices no coinciden, y ni NumPy ni pandas eager planifican con antelación como lo hace un motor lazy. - Temas Relacionados: indexación y remodelación de arrays, agregación groupby, optimización de rendimiento y memoria, modelo de ejecución lazy de Polars.
Fundamentos
Un ndarray de NumPy es un bloque de memoria contigua que contiene valores de un único dtype (tipo de dato): todo int64, todo float64, o todo de otro tipo de ancho fijo.
Esa homogeneidad es lo que hace posible la vectorización: como cada elemento tiene el mismo tipo y tamaño, NumPy puede entregar todo el array a un bucle compilado (una ufunc, abreviatura de función universal) en lugar de pedir al intérprete de Python que visite cada elemento uno a la vez.
Un bucle for de Python sobre una lista incurre en sobrecarga del intérprete (comprobaciones de tipo, despacho dinámico, empaquetado de objetos) en cada iteración; una llamada vectorizada de NumPy paga esa sobrecarga una vez y luego ejecuta un bucle C ajustado sobre bytes crudos.
La difusión (broadcasting) es la regla que permite que arrays de diferentes formas se combinen sin que escribas un bucle explícito o una copia: cuando las formas no coinciden, NumPy las compara desde la dimensión final hacia adentro, y cualquier dimensión de tamaño 1 se estira para que coincida con su contraparte, conceptualmente, no físicamente, ya que no se duplica memoria alguna.
Una analogía simple: la difusión es como entregar el mismo folleto a cada asiento de un estadio sin imprimir uno por asiento: el contenido del folleto se reutiliza, solo cambia la "posición".
pandas toma este modelo de array y añade dos cosas que NumPy no tiene: etiquetas y heterogeneidad entre columnas.
Una Series es un array NumPy (o un array basado en Arrow) más un Index (índice): un eje etiquetado que da a cada elemento una identidad más allá de su posición.
Un DataFrame se entiende mejor como un diccionario de Series que comparten el mismo Índice de filas, que es por lo que cada columna puede contener su propio dtype, aunque cada valor dentro de una columna todavía tenga que compartir uno.
Este es el detalle que confunde a los recién llegados de NumPy: la aritmética y las uniones de pandas operan primero sobre la alineación por etiqueta, y la posición es casi incidental.
Mecánicas e Interacciones
La vectorización y la difusión explican por qué las matemáticas de NumPy son rápidas, pero las mecánicas interesantes ocurren en el límite entre "vectorizado" y "no vectorizado".
Cada llamada a ufunc de NumPy tiene que decidir, antes de ejecutarse, si las formas de las entradas son compatibles para la difusión; si no lo son, obtienes un ValueError, no una salida incorrecta silenciosa, que es una de las mejores propiedades de seguridad del modelo.
pandas superpone la alineación sobre eso: sumar dos Series primero reindexa ambas a la unión de sus índices, y cualquier etiqueta presente en una pero no en la otra produce NaN en lugar de lanzar un error, una elección de diseño que intercambia seguridad por conveniencia, y que fabrica silenciosamente datos faltantes si no esperabas una desalineación.
Ese mismo paso de alineación es también por lo que una operación de DataFrame puede ser mucho más lenta que la operación equivalente de array NumPy crudo sobre los mismos datos: las etiquetas tienen que ser reconciliadas cada vez, no solo los números.
La disposición de la memoria también importa: los arrays NumPy son contiguos y pueden ser de orden de fila (orden C) o de orden de columna (orden Fortran), y una operación que recorre en contra de la veta de esa disposición incurre en una penalización de localidad de caché, incluso si el bucle vectorizado todavía "funciona".
El gestor de bloques interno de pandas históricamente almacenaba columnas del mismo dtype juntas por esta misma razón, y el pandas moderno (2.2+) cada vez más utiliza arrays Arrow como base para las columnas, lo que cambia la historia de los valores faltantes: los dtypes anulables basados en Arrow llevan un mapa de bits de validez explícito en lugar de depender de NaN, por lo que las columnas de enteros y cadenas pueden contener valores faltantes reales sin necesidad de hacer un upcasting a float64 como lo hacía el pandas clásico basado en NumPy.
La indexación es donde los bordes afilados del modelo se muestran más: .loc opera en el espacio de etiquetas, .iloc en el espacio posicional, y la indexación encadenada (df[df.x > 0]['y'] = 1) puede escribir silenciosamente en una copia temporal en lugar del frame original, que es el mecanismo detrás de SettingWithCopyWarning.
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)
# b se difunde a través de cada fila: las formas (2,3) y (3,) se alinean en la
# dimensión final, por lo que b se repite conceptualmente para cada fila
result = a + b # shape (2, 3), sin bucle explícito, sin copias de b almacenadasConsideraciones y Aplicaciones Avanzadas
El modelo de array y alineación escala bien hasta que los datos dejan de caber cómodamente en memoria, y es exactamente ahí donde el diseño de Polars se aparta de él.
Polars mantiene la misma idea de memoria columnar basada en Arrow, pero su API LazyFrame no ejecuta nada cuando escribes una transformación: construye un plan de consulta, y solo collect() desencadena la ejecución.
Ese modelo diferido permite al optimizador de consultas de Polars empujar filtros y selecciones de columnas hacia abajo antes de que ocurra cualquier I/O, por lo que escanear un archivo Parquet con scan_parquet() y un .filter() puede omitir la lectura de grupos de filas o columnas enteras que el plan demuestra que son innecesarios.
pandas no tiene una etapa de planificación equivalente: cada llamada a .assign(), .merge(), o .groupby() se ejecuta inmediatamente y materializa su resultado completo, lo cual es simple de razonar pero significa que todavía se construye un DataFrame intermedio innecesario, incluso si un paso posterior descarta la mayor parte.
La API eager de Polars también existe y se comporta más como pandas: ejecuta inmediatamente, pero la API lazy es donde la diferencia arquitectónica realmente da sus frutos, particularmente en datos demasiado grandes para contener cómodamente múltiples copias intermedias.
Nada de esto hace que NumPy o pandas eager sean obsoletos: para datos que ya caben en memoria con unos pocos pasos de transformación, un optimizador tiene poco que optimizar, y la simplicidad de la ejecución inmediata a menudo vale más que la sobrecarga de la planificación diferida.
| Enfoque | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
ndarray de NumPy | Matemáticas numéricas densas más rápidas posibles, mínima sobrecarga | Sin etiquetas, sin columnas heterogéneas, incómodo para datos tabulares/unidos | Arrays puramente numéricos: imágenes, matrices, datos de señales |
| pandas (eager) | Rica API etiquetada, enorme ecosistema, I/O maduro | Ejecuta cada paso inmediatamente - sin optimización entre pasos; limitado por memoria | Análisis exploratorio, datos tabulares pequeños a medianos |
| Polars (eager) | Más rápido que pandas en muchas operaciones a través de multihilo + Arrow | Todavía materializa cada resultado intermedio | Datos medianos donde quieres velocidad sin cambiar el estilo de la API |
| Polars (lazy) | El optimizador fusiona/empuja filtros y proyecciones antes de la ejecución | La indirección del plan de consulta añade un paso mental; algunos modismos de pandas no se mapean 1:1 | Archivos/conjuntos de datos grandes, pipelines con varias transformaciones encadenadas |
Conceptos Erróneos Comunes
- "pandas es solo NumPy con etiquetas añadidas." - Las etiquetas cambian la semántica, no solo la presentación: la aritmética entre dos Series se alinea primero en el Índice, lo que puede introducir silenciosamente
NaNs que una operación NumPy cruda sobre los mismos números nunca haría. - "El código vectorizado siempre es barato en memoria." - La vectorización evita la sobrecarga del intérprete, no el costo de memoria; la difusión de un array grande contra otro todavía asigna un array de resultado completo, y las operaciones pandas encadenadas pueden cada una asignar un nuevo DataFrame intermedio.
- "La API lazy de Polars es siempre más rápida que pandas." - El optimizador se gana su sueldo en escaneos grandes con filtros/proyecciones para empujar hacia abajo; en datos pequeños en memoria con un par de operaciones, la sobrecarga de planificación puede anular cualquier ventaja, y
collect()todavía tiene que ejecutarse eventualmente. - ".loc e .iloc son básicamente intercambiables." -
.locresuelve etiquetas,.ilocresuelve posiciones enteras, y en un DataFrame con un Índice no predeterminado o reordenado, estos dos pueden devolver filas completamente diferentes para el argumento numérico "igual". - "NaN es cómo pandas representa cualquier valor faltante." - El pandas clásico basado en NumPy solo tenía un
NaNnativo para flotantes, lo que obligaba a las columnas de enteros con datos faltantes a hacer un upcasting afloat64; los dtypes anulables basados en Arrow en pandas 2.2+ finalmente dan a los enteros y cadenas un marcador de falta real sin ese upcasting.
Preguntas Frecuentes
¿Qué significa realmente "vectorización" en NumPy?
- Vectorización significa entregar un array completo a un bucle compilado (una ufunc) en lugar de iterar elemento por elemento en Python.
- La sobrecarga del intérprete (comprobaciones de tipo, despacho dinámico) se paga una vez por llamada en lugar de una vez por elemento.
- Es por eso que
a + ben dos arrays NumPy es dramáticamente más rápido que un bucleforde Python haciendo las mismas sumas.
¿Qué es la difusión (broadcasting), en una frase?
La difusión es la regla que permite a NumPy combinar arrays de formas diferentes pero compatibles estirando conceptualmente (no físicamente) cualquier dimensión de tamaño 1 para que coincida con su contraparte, comparando las formas desde la dimensión final hacia adentro.
¿Por qué sumar dos Series de pandas con índices diferentes no genera un error?
- pandas se alinea por etiqueta antes de realizar la aritmética, reindexando ambas Series a la unión de sus índices.
- Las etiquetas presentes en una sola Serie producen
NaNen el resultado en lugar de lanzar un error. - Esta es una compensación de conveniencia deliberada: evita forzar la reindexación manual, a costa de fabricar silenciosamente valores faltantes cuando no se esperaba una desalineación de índices.
¿Es un DataFrame solo un array NumPy 2D?
No, un DataFrame se parece más a un diccionario de Series que comparten un Índice de filas, que es por lo que cada columna puede contener su propio dtype (cadena, entero, fecha y hora) aunque un único array NumPy solo pueda contener un dtype para todo el bloque.
¿Por qué `.loc` versus `.iloc` es una fuente tan común de errores?
.locselecciona por etiqueta (lo que está en el Índice),.ilocselecciona por posición entera (basada en 0, ignorando las etiquetas).- En un
RangeIndexpredeterminado, estos a menudo coinciden, lo que oculta la distinción hasta que el Índice se reordena, se filtra o no es entero. - Mezclarlos después de un
.sort_values()o.reset_index()es una fuente frecuente de errores de "desfase de una fila".
¿Qué causa `SettingWithCopyWarning` y por qué importa?
Señala que una operación encadenada (como df[mask]['col'] = value) puede haber escrito en un objeto intermedio temporal en lugar del DataFrame original, porque pandas no siempre puede garantizar si una selección intermedia devolvió una vista o una copia; la solución es seleccionar y asignar en una única llamada .loc[mask, 'col'] = value.
¿Por qué los datos faltantes obligan a que una columna de enteros se convierta en flotante en algunos códigos de pandas?
Los arrays de enteros clásicos basados en NumPy no tienen un patrón de bits reservado para "faltante", por lo que pandas históricamente tuvo que hacer un upcasting de una columna de enteros a float64 en el momento en que aparecía un NaN, ya que solo los flotantes tienen una representación nativa de valor faltante; los dtypes de enteros anulables basados en Arrow (pandas 2.2+) evitan esto al llevar una máscara de validez separada.
¿Cómo se diferencia el modelo de memoria de Polars del de pandas clásico?
Polars se basa en el formato de memoria columnar de Apache Arrow desde el principio, lo que permite multihilo eficiente e interoperabilidad más fácil de copia cero con otras herramientas basadas en Arrow; pandas ha estado migrando hacia dtypes opcionales basados en Arrow desde la versión 2.2, pero su gestor de bloques predeterminado todavía tiene raíces de la era NumPy.
¿Cuál es la diferencia real entre el modo eager y lazy de Polars?
- El modo eager (
pl.DataFrame) ejecuta cada operación inmediatamente, como pandas. - El modo lazy (
pl.LazyFrame) construye un plan de consulta no ejecutado y espera a.collect(). - La ruta lazy permite al optimizador de Polars reordenar, fusionar y empujar filtros/proyecciones antes de tocar los datos, algo que la ejecución eager no tiene oportunidad de hacer.
¿Cuándo debería elegir Polars en lugar de pandas?
Elige Polars cuando los datos sean lo suficientemente grandes como para que la optimización de filtros/proyecciones en tiempo de escaneo o la ejecución multihilo sean significativamente importantes, o cuando un pipeline encadene muchas transformaciones sobre archivos más grandes que caben cómodamente en memoria varias veces; para trabajos pequeños, exploratorios o con mucho ecosistema (bibliotecas de visualización, herramientas antiguas), la madurez de pandas a menudo gana.
¿Usar Polars significa abandonar pandas por completo?
No, la mayoría de los adoptantes de Polars usan ambos: pandas para trabajo exploratorio, integraciones heredadas y bibliotecas que esperan un DataFrame de pandas, y Polars para las etapas específicas grandes o sensibles al rendimiento de un pipeline, convirtiendo entre los dos con to_pandas()/from_pandas() cuando es necesario.
¿Por qué dos líneas de código pandas que "parecen iguales" pueden tener un rendimiento muy diferente?
Porque la operación subyacente podría ser una única llamada a ufunc vectorizada en un caso y una llamada a función por fila en el nivel de Python con .apply() en el otro; ambos pueden parecer df['x'].something(...), pero solo la ruta vectorizada evita la sobrecarga del intérprete por elemento.
¿Cuál es el hábito más importante para razonar correctamente sobre este modelo?
- Saber si una operación está vectorizada (bucle compilado, rápido) o es un
.apply()/bucle de Python (interpretado, lento) antes de escribirla. - Saber si una operación se alinea por etiqueta (pandas) o ignora las etiquetas (NumPy crudo) antes de combinar dos objetos.
- Saber si estás en un contexto eager (ejecuta ahora) o lazy (construye un plan) antes de asumir que una operación "ya se ejecutó".
Relacionados
- Arrays NumPy - creación, dtypes y las mecánicas de difusión en las que se basa esta página.
- Series y DataFrames de pandas - la construcción concreta y la API de selección para el modelo Series/DataFrame/Index.
- Indexación y Remodelación de NumPy - vistas vs copias y manipulación de formas en la práctica.
- Polars - el motor lazy basado en Arrow al que se hace referencia en Consideraciones Avanzadas.
- Rendimiento y Memoria - optimización concreta una vez que sepas qué parte del modelo te está costando.
- GroupBy y Agregación - dividir-aplicar-combinar como una extensión del modelo de alineación.
Versiones de Stack: Esta página fue escrita para Python 3.14 (estable) / 3.13 (mantenimiento), pandas 2.2+, y Polars 1.x.