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.
Busca en todas las páginas de la documentación
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.
NaNs cuando los índices no coinciden, y ni NumPy ni pandas eager planifican con antelación como lo hace un motor lazy.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.
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 almacenadasEl 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 |
NaNs que una operación NumPy cruda sobre los mismos números nunca haría.collect() todavía tiene que ejecutarse eventualmente..loc resuelve etiquetas, .iloc resuelve 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 nativo para flotantes, lo que obligaba a las columnas de enteros con datos faltantes a hacer un upcasting a float64; 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.a + b en dos arrays NumPy es dramáticamente más rápido que un bucle for de Python haciendo las mismas sumas.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.
NaN en el resultado en lugar de lanzar un error.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.
.loc selecciona por etiqueta (lo que está en el Índice), .iloc selecciona por posición entera (basada en 0, ignorando las etiquetas).RangeIndex predeterminado, estos a menudo coinciden, lo que oculta la distinción hasta que el Índice se reordena, se filtra o no es entero..sort_values() o .reset_index() es una fuente frecuente de errores de "desfase de una fila".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.
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.
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.
pl.DataFrame) ejecuta cada operación inmediatamente, como pandas.pl.LazyFrame) construye un plan de consulta no ejecutado y espera a .collect().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.
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.
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.
.apply()/bucle de Python (interpretado, lento) antes de escribirla.Versiones de Stack: Esta página fue escrita para Python 3.14 (estable) / 3.13 (mantenimiento), pandas 2.2+, y Polars 1.x.
Revisado por Chris St. John·Última actualización: 15 jul 2026