18/02/2024
En el corazón de la ciencia de datos y la computación numérica en Python, reside una biblioteca fundamental: NumPy. Conocida por su capacidad para manejar arrays multidimensionales de manera eficiente, NumPy no solo simplifica tareas complejas, sino que las acelera drásticamente, superando las limitaciones de las estructuras de datos nativas de Python. Si alguna vez te has preguntado cómo las operaciones matemáticas y lógicas sobre grandes conjuntos de datos pueden ser tan rápidas, la respuesta a menudo se encuentra en las optimizaciones de bajo nivel que ofrece NumPy, particularmente a través de la vectorización.

Este artículo te guiará a través de las funciones esenciales de NumPy, explorando técnicas para optimizar el uso de la memoria, manejar datos jerárquicos y, lo más importante, cómo aprovechar la vectorización para escribir código más limpio y eficiente. Prepárate para descubrir cómo NumPy puede transformar tu enfoque en el análisis y procesamiento de datos.
- Fundamentos de NumPy: Más Allá de los Bucles Tradicionales
- Optimizando el Rendimiento: Minimizando Copias de Arrays
- Arrays Estructurados: Datos con Significado
- Análisis y Visualización de Datos Jerárquicos
- Creando Funciones Optimizadas: Vectorización Personalizada
- Preguntas Frecuentes (FAQ) sobre NumPy
- Conclusión
Fundamentos de NumPy: Más Allá de los Bucles Tradicionales
NumPy es el estándar de facto para trabajar con arrays numéricos en Python. Su objeto principal, el ndarray (array N-dimensional), es una estructura de datos altamente optimizada que almacena todos sus elementos en un área contigua de memoria. Esta característica es crucial porque permite un acceso a los datos extremadamente rápido, lo que a su vez acelera las operaciones.
Una de las mayores ventajas de NumPy es la vectorización. En pocas palabras, la vectorización es la capacidad de realizar operaciones en arrays completos sin la necesidad de escribir bucles explícitos en Python. En lugar de iterar elemento por elemento (lo que en Python puede ser lento), NumPy delega estas operaciones a código C subyacente, que es mucho más rápido. Por ejemplo, la multiplicación de matrices, una operación común en álgebra lineal, se realiza de forma vectorizada en NumPy, lo que resulta en un rendimiento superior.
¿Por qué la vectorización es tan importante?
- Velocidad: Reduce significativamente el tiempo de ejecución para grandes conjuntos de datos.
- Claridad del código: Elimina la necesidad de bucles
forcomplejos, haciendo el código más legible y conciso. - Menos errores: Al depender de implementaciones optimizadas y probadas, se reduce la probabilidad de errores manuales en los bucles.
Optimizando el Rendimiento: Minimizando Copias de Arrays
Aunque NumPy es eficiente, ciertas operaciones pueden generar copias innecesarias de arrays, lo que consume memoria y tiempo de procesamiento. Esto es particularmente relevante cuando se trabaja con arrays que se expanden o se construyen a partir de múltiples fuentes de datos.
El problema del proceso de copia
Cuando necesitas expandir un array NumPy existente, por ejemplo, al importar nuevos datos de un archivo, una práctica común es concatenar el nuevo contenido. Sin embargo, np.concatenate() u operaciones similares a menudo crean una copia completa del array original y luego la expanden. Esta copia es necesaria para asegurar que el array resultante siga siendo contiguo en memoria. Para datasets grandes, copiar arrays constantemente puede ralentizar el procesamiento y agotar la memoria del sistema.
Técnicas para minimizar los efectos de la copia:
- Pre-asignación: Determina el tamaño final de tu array antes de poblarlo y créalo con
np.zeros()(o una función similar) de antemano. Incluso sobrestimar el tamaño puede ser beneficioso. - Rellenado directo: Una vez que el array está pre-asignado, rellénalo con los datos de origen. Esto evita la necesidad de expandir el array y, por lo tanto, las copias.
Ejemplo: Creación de arrays multidimensionales a partir de archivos
Consideremos la creación de un array tridimensional a partir de múltiples archivos CSV. Supongamos que tenemos file1.csv, file2.csv y file3.csv, cada uno con 2 filas y 3 columnas.
# file1.csv: # 1.1, 1.2, 1.3 # 1.4, 1.5, 1.6 # file2.csv: # 2.1, 2.2, 2.3 # 2.4, 2.5, 2.6 # file3.csv: # 3.1, 3.2, 3.3 # 3.4, 3.5, 3.6 El array resultante tendrá una forma de (3, 2, 3). Aquí se muestra cómo poblarlo eficientemente:
from pathlib import Path import numpy as np # Pre-asignar el array array = np.zeros((3, 2, 3)) print(f"ID del array inicial: {id(array)}") # Rellenar el array sin copiarlo for file_count, csv_file in enumerate(Path.cwd().glob("file?.csv")): array[file_count] = np.loadtxt(csv_file.name, delimiter=",") print(f"ID del array final: {id(array)}") print(f"Forma del array: {array.shape}") print(array) Observa cómo el id() del array permanece constante, lo que demuestra que no se crearon copias innecesarias. Esto es clave para la eficiencia de la memoria.
Manejo de tamaños de datos diferentes
¿Qué sucede si los archivos de entrada no tienen las mismas dimensiones? NumPy ofrece flexibilidad:
- Archivos más cortos: Si un archivo tiene menos datos de los esperados para una sub-array, puedes insertarlo en una posición específica. Por ejemplo, si
short_file.csvtiene solo una fila (4.1, 4.2, 4.3), y tu array pre-asignado espera 2 filas, puedes insertarlo enarray[3, 0](asumiendo que es el cuarto bloque y en la primera fila). El resto se llenará con ceros. - Archivos más largos: Si un archivo contiene más datos de los que tu array puede acomodar en una de sus dimensiones, necesitarás usar
np.insert(). Sin embargo, ten en cuenta quenp.insert()sí crea una copia del array para acomodar la nueva dimensión. Esto subraya la importancia de la pre-asignación inicial.
Asegurando el orden correcto de los archivos
Cuando usas funciones como Path.cwd().glob("file*.csv"), los archivos pueden no ser devueltos en el orden numérico esperado (por ejemplo, file1.csv, file10.csv, file11.csv, file2.csv...). Esto se debe a la clasificación alfanumérica. Para obtener un orden natural, especialmente con números de dos dígitos o más, es recomendable usar la librería natsort:
from natsort import natsorted # ... (código previo para configurar el array) for file_count, csv_file in enumerate(natsorted(Path.cwd().glob("file*.csv"))): array[file_count] = np.loadtxt(csv_file.name, delimiter=",") Esto garantiza que file1.csv, file2.csv, file3.csv, file10.csv, file11.csv se procesen en el orden lógico.
Arrays Estructurados: Datos con Significado
Una de las características más potentes de NumPy, a menudo subestimada, son los arrays estructurados. Permiten asignar nombres significativos a las columnas (campos) de tu array, en lugar de depender únicamente de índices numéricos. Esto mejora enormemente la legibilidad y mantenibilidad del código, haciendo que las operaciones sobre los datos sean más intuitivas.
Creando un array estructurado
Un array estructurado se define con un tipo de dato compuesto por una lista de tuplas, donde cada tupla contiene el nombre del campo y su tipo de dato (usando códigos de protocolo de interfaz de array, como 'U12' para cadenas de 12 caracteres, 'f4' para flotantes de 4 bytes, o 'i4' para enteros de 4 bytes).
import numpy as np race_results = np.array( [ ("At The Back", 1.2, 3), ("Fast Eddie", 1.3, 1), ("Almost There", 1.1, 2), ], dtype=[ ("horse_name", "U12"), ("price", "f4"), ("position", "i4"), ], ) print(race_results["horse_name"]) print(np.sort(race_results, order="position")[["horse_name", "price"]]) print(race_results[race_results["position"] == 1]["horse_name"]) Como puedes ver, acceder a race_results["horse_name"] es mucho más claro que race_results[:, 0]. Además, puedes ordenar y filtrar fácilmente usando los nombres de los campos.
Conciliando datos utilizando arrays estructurados (Joins)
Los arrays estructurados son ideales para combinar datos de diferentes fuentes, similar a cómo se realizan las uniones (JOINs) en bases de datos relacionales. NumPy proporciona funciones auxiliares para esto, como numpy.lib.recfunctions.rec_join().
Imagina que tienes dos archivos CSV: issued_checks.csv (cheques emitidos) y cashed_checks.csv (cheques cobrados). Quieres ver los detalles completos de los cheques cobrados, incluyendo el beneficiario y la fecha de emisión, que solo están en el primer archivo.
import numpy.lib.recfunctions as rfn from pathlib import Path issued_dtypes = [ ("id", "i8"), ("payee", "U10"), ("amount", "f8"), ("date_issued", "U10"), ] cashed_dtypes = [ ("id", "i8"), ("amount", "f8"), ("date_cashed", "U10"), ] issued_checks = np.loadtxt( Path("issued_checks.csv"), delimiter=",", dtype=issued_dtypes, skiprows=1 ) cashed_checks = np.loadtxt( Path("cashed_checks.csv"), delimiter=",", dtype=cashed_dtypes, skiprows=1 ) # Realizar una unión interna por el campo 'id' cashed_check_details = rfn.rec_join( "id", issued_checks, cashed_checks, jointype="inner" ) print(cashed_check_details[["payee", "date_issued", "date_cashed"]]) La función rec_join() toma el campo clave ("id"), los arrays a unir, y el tipo de unión (jointype="inner" para solo registros coincidentes). Es importante tener en cuenta que si ambos arrays tienen campos con el mismo nombre (como "amount"), rec_join() los renombrará automáticamente (ej. "amount1", "amount2") para evitar conflictos. Puedes verificar los nuevos nombres con cashed_check_details.dtype.
Manejo de valores clave duplicados
Antes de realizar uniones, es crucial asegurarse de que no haya valores clave duplicados en los arrays, ya que pueden conducir a resultados inesperados o relaciones uno-a-muchos. NumPy ofrece rfn.find_duplicates() para identificar y np.unique() para eliminar duplicados:
# Para encontrar duplicados (necesita un array enmascarado) print(rfn.find_duplicates(np.ma.asarray(issued_checks))) # Para eliminar duplicados (crea un nuevo array) issued_checks = np.unique(issued_checks, axis=0) print(issued_checks) np.unique(..., axis=0) eliminará filas duplicadas completas, dejando solo una instancia. Esto es vital para la integridad de tus uniones.
Análisis y Visualización de Datos Jerárquicos
Los datos jerárquicos, donde los elementos se organizan en niveles (como una estructura padre-hijo), son comunes en muchos dominios. Los arrays estructurados de NumPy son una excelente herramienta para consolidar y analizar este tipo de datos, ya que permiten acceder a ellos a través de etiquetas significativas.
Consideremos un ejemplo de una cartera de acciones. Tenemos un archivo portfolio.csv con información de la compañía y su sector, y varios archivos share_prices-n.csv con los precios diarios de las acciones.
# portfolio.csv: # Company,Sector # Company_A,technology # ... # share_prices-1.csv: # Company,mon # Company_A,100.5 # ... Creando y poblando el array
Primero, definimos la estructura de nuestro array portfolio, que incluirá el nombre de la compañía, el sector y los precios diarios (lunes a viernes). La clave aquí es la concatenación de tipos de datos para construir un dtype complejo.
import numpy as np from pathlib import Path days = ["mon", "tue", "wed", "thu", "fri"] days_dtype = [(day, "f8") for day in days] company_dtype = [("company", "U20"), ("sector", "U20")] portfolio_dtype = np.dtype(company_dtype + days_dtype) # Combinar dtypes portfolio = np.zeros((6,), dtype=portfolio_dtype) # Pre-asignar print(portfolio) # Poblar con datos de la compañía companies = np.loadtxt( Path("portfolio.csv"), delimiter=",", dtype=company_dtype, skiprows=1 ).reshape((6,)) portfolio[["company", "sector"]] = companies print(portfolio) # Poblar con precios diarios for day, csv_file in zip(days, sorted(Path.cwd().glob("share_prices-?.csv"))): portfolio[day] = np.loadtxt( csv_file.name, delimiter=",", dtype=[("company", "U20"), ("temp", "f8")], skiprows=1 )["temp"] # Solo tomar la columna de temperatura print(portfolio) Una vez que el array portfolio está completo, puedes realizar análisis potentes y específicos:
- Extracción por campo:
portfolio[portfolio["company"] == "Company_C"]para obtener los datos de una empresa específica. - Filtrado combinado:
portfolio[portfolio["sector"] == "technology"]["fri"]para obtener los precios del viernes de las empresas de tecnología. - Cálculos agregados: Si posees 250 acciones de cada empresa de tecnología, puedes calcular su valor total al final de la semana:
sum(portfolio[portfolio["sector"] == "technology"]["fri"] * 250 * 0.01).
Trazando los datos con Matplotlib
La integración con librerías de visualización como Matplotlib es fluida. Puedes usar los nombres de los campos de tu array estructurado para plotear directamente los datos, haciendo el código más legible y menos propenso a errores de indexación.
import matplotlib.pyplot as plt tech_mask = portfolio["sector"] == "technology" tech_sector_names = portfolio[tech_mask]["company"] tech_valuation = portfolio[tech_mask]["fri"] * 250 * 0.01 plt.bar(x=tech_sector_names, height=tech_valuation, color='g') plt.xlabel("Empresas de Tecnología") plt.ylabel("Valoración del Viernes ($)") plt.title("Valoración de Acciones Tecnológicas ($)") plt.show() Este código generaría un gráfico de barras mostrando la valoración de las empresas tecnológicas de tu portfolio, utilizando directamente los nombres de los campos para la claridad.
Creando Funciones Optimizadas: Vectorización Personalizada
La vectorización no se limita a las funciones incorporadas de NumPy. Puedes aplicar la vectorización a tus propias funciones de Python, lo que es extremadamente útil cuando necesitas realizar operaciones complejas que NumPy no cubre directamente, pero aún deseas el rendimiento de las operaciones vectorizadas.
El problema de las funciones escalares
Si defines una función de Python que espera valores escalares (números individuales) y le pasas arrays de NumPy, obtendrás un ValueError, ya que Python no sabe cómo aplicar lógicas condicionales (como if/else) a un array completo.
def profit_with_bonus(first_day, last_day): if last_day >= first_day * 1.01: return (last_day - first_day) * 1.1 else: return last_day - first_day # Esto fallaría: # profit_with_bonus(portfolio["mon"], portfolio["fri"]) Añadiendo funcionalidad de vectorización con np.vectorize()
La función np.vectorize() toma una función escalar (que opera en un solo par de valores) y devuelve una nueva función que puede operar sobre arrays de NumPy, aplicando la lógica original elemento por elemento, pero con el beneficio de la optimización de NumPy.
vectorized_profit_with_bonus = np.vectorize(profit_with_bonus) profits = vectorized_profit_with_bonus(portfolio["mon"], portfolio["fri"]) print(profits) Esta aproximación te permite mantener tu función original para uso escalar si lo deseas, y tener una versión vectorizada para arrays.
Usando el decorador @np.vectorize
Como alternativa más concisa, puedes usar np.vectorize como un decorador directamente sobre tu función. Esto transformará la función original en una versión vectorizada, sobrescribiendo su comportamiento:
@np.vectorize def profit_with_bonus(first_day, last_day): if last_day >= first_day * 1.01: return (last_day - first_day) * 1.1 else: return last_day - first_day profits = profit_with_bonus(portfolio["mon"], portfolio["fri"]) print(profits) Con el decorador, la llamada a profit_with_bonus() ahora espera arrays y devuelve un array. Si se le pasan escalares, también devolverá un array.
Uso de la funcionalidad de vectorización existente con np.where()
Antes de crear tus propias funciones vectorizadas, siempre es una buena práctica verificar si NumPy ya tiene una función incorporada que se ajuste a tus necesidades. Muchas de las funciones de NumPy ya están vectorizadas nativamente.
Por ejemplo, para la lógica de beneficio con bonificación, np.where() es una excelente alternativa. Esta función es similar a una expresión condicional ternaria (if-else) aplicada elemento a elemento en arrays:
profits_where = np.where( portfolio["fri"] > portfolio["mon"] * 1.01, (portfolio["fri"] - portfolio["mon"]) * 1.1, # Valor si la condición es True portfolio["fri"] - portfolio["mon"], # Valor si la condición es False ) print(profits_where) np.where() evalúa la condición para cada elemento y elige el valor correspondiente de la segunda o tercera matriz. Es una forma muy eficiente y legible de aplicar lógica condicional vectorizada.
Preguntas Frecuentes (FAQ) sobre NumPy
Aquí respondemos algunas de las preguntas más comunes que surgen al trabajar con NumPy:
| Pregunta | Respuesta |
|---|---|
| ¿Cuál es la diferencia principal entre una lista de Python y un array NumPy? | Los arrays NumPy son homogéneos (todos los elementos del mismo tipo), lo que permite operaciones matemáticas vectorizadas y un uso de memoria más eficiente. Las listas de Python pueden contener elementos de diferentes tipos y son menos eficientes para cálculos numéricos grandes. |
| ¿Cómo puedo cambiar la forma (reshape) de un array NumPy? | Usa el método .reshape() del array. Por ejemplo, my_array.reshape((rows, columns)). Ten en cuenta que el número total de elementos debe ser el mismo. |
| ¿Cómo manejo los valores faltantes (NaN) en NumPy? | NumPy representa los valores faltantes con np.nan. Puedes detectarlos con np.isnan() y reemplazarlos o eliminarlos según tu análisis. Pandas ofrece herramientas más robustas para el manejo de datos faltantes. |
| ¿Es NumPy solo para números? | Aunque su enfoque principal es la computación numérica, los arrays NumPy pueden almacenar otros tipos de datos (cadenas, booleanos) si se especifica el dtype adecuado, especialmente en arrays estructurados. Sin embargo, su mayor optimización es para datos numéricos. |
| ¿Cuándo debería usar Pandas en lugar de NumPy? | Pandas se construye sobre NumPy y es ideal para datos tabulares y heterogéneos (DataFrames). Si tus datos tienen etiquetas de filas/columnas, tipos de datos mixtos o necesitas funcionalidades de manipulación de datos más complejas (agrupación, uniones avanzadas, series de tiempo), Pandas suele ser la mejor opción. Para operaciones numéricas puras y de alto rendimiento en arrays, NumPy es el rey. |
Conclusión
NumPy es una piedra angular en el ecosistema de la computación científica en Python. Su capacidad para manejar arrays multidimensionales de manera eficiente, optimizar el uso de la memoria mediante la pre-asignación y su potente funcionalidad de vectorización, tanto nativa como personalizada, lo convierten en una herramienta indispensable. Al dominar estas técnicas, no solo escribirás un código más rápido y limpio, sino que también podrás abordar problemas de análisis de datos a gran escala con una confianza renovada. Sigue explorando y experimentando con NumPy; las posibilidades son vastas.
Si quieres conocer otros artículos parecidos a NumPy: Potencia tu Análisis de Datos con Eficiencia puedes visitar la categoría Librerías.
