Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fundamentos, Caracteristicas y Aplicaciones de ...

Fundamentos, Caracteristicas y Aplicaciones de los Modulos NumPy , Matplotlib y Pandas

Avatar for Abraham Zamudio

Abraham Zamudio

August 02, 2026

More Decks by Abraham Zamudio

Other Decks in Education

Transcript

  1. Fundamentos, Caracterı́sticas y Aplicaciones de los Módulos NumPy , Matplotlib

    y Pandas Abraham Zamudio 2026 Resumen El presente documento compila tres informes técnicos exhaustivos dedicados a los pilares fundamentales de la computación cientı́fica y el análisis de datos en Python. La primera sección analiza NumPy, detallando la arquitectura de memoria del ndarray, la vectorización mediante instrucciones SIMD, el mecanismo de broadcasting y el álgebra lineal de alto rendimiento como sustrato computacional irremplazable. La segunda sección examina Matplotlib, explorando su pipeline de renderizado, la jerarquı́a de objetos (Artists), la superioridad de la API orientada a objetos frente a la máquina de estados de pyplot, y su aplicación rigurosa en el diseño de layouts complejos, diagnóstico de modelos y visualización matemática. La tercera sección aborda Pandas, diseccionando su evolución arquitectónica desde el BlockManager hacia el backend de Apache Arrow y la semántica Copy-on-Write (CoW), profundizando en su rol como motor de álgebra relacional en memoria, el manejo avanzado de series temporales y las prácticas de optimización para pipelines de datos a escala. En conjunto, estos reportes enfatizan las buenas prácticas de modularidad, reproducibilidad estadı́stica y eficiencia computacional a nivel de silicio, sirviendo como una referencia técnica sólida y rigurosa para profesionales en ciencia de datos, ingenierı́a de software e inteligencia artificial. Índice 1. Módulo NumPy: Fundamentos, Caracterı́sticas y Aplicaciones 1.1. Introducción a NumPy: Arquitectura, Memoria y el Paradigma de la Computación Cientı́fica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1. La Anatomı́a de la Ineficiencia en Python Nativo . . . . . . . . . . . . 1.1.2. El Modelo de Memoria Contigua y la Homogeneidad de Tipos . . . . . 1 4 4 4 5
  2. 1.2. 1.3. 1.4. 1.5. 1.6. 1.7. 1.1.3. Vectorización, SIMD y

    la Liberación del GIL . . . . . . . . . . . . . . 6 1.1.4. El Ecosistema PyData y la Interoperabilidad . . . . . . . . . . . . . . . 7 Fundamentos y Caracterı́sticas Principales . . . . . . . . . . . . . . . . . . . . 7 1.2.1. El objeto ndarray: Anatomı́a y Rol como Sustrato del Ecosistema PyData 7 1.2.2. Tipos de Datos (dtypes) . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.2.3. Creación de Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Operaciones y Manipulación de Arreglos . . . . . . . . . . . . . . . . . . . . 10 1.3.1. Indexación y Slicing . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.3.2. Vectorización y Broadcasting: Abstracción de Alto Nivel y Optimización de Bajo Nivel . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.3.3. Funciones Universales (ufuncs): Arquitectura en C y Operaciones Algebraicas Avanzadas . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.3.4. Manipulación de la Forma (Reshaping) . . . . . . . . . . . . . . . . . 15 Álgebra Lineal y Operaciones Matemáticas: El Motor Numérico de Alto Rendimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 1.4.1. Producto Punto, Contracción Tensorial y la Convención de Einstein . . 16 1.4.2. Descomposición Espectral: Autovalores y Autovectores . . . . . . . . . 16 1.4.3. Descomposición en Valores Singulares (SVD) y Aproximación de Rango 16 1.4.4. Resolución de Sistemas Lineales y Estabilidad Numérica . . . . . . . . 17 Aplicaciones en Ciencia de Datos e Ingenierı́a . . . . . . . . . . . . . . . . . . 18 1.5.1. Generación de Números Aleatorios: Teorı́a, Arquitectura y Reproducibilidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 1.5.2. Interoperabilidad con el Ecosistema Python: El Protocolo ndarray como Lingua Franca . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 Buenas Prácticas y Optimización de Rendimiento . . . . . . . . . . . . . . . . 25 Conclusión y Perspectivas sobre el Paradigma NumPy . . . . . . . . . . . . . . 25 2. Módulo Matplotlib: Fundamentos, Caracterı́sticas y Aplicaciones 2.1. Introducción a Matplotlib: Filosofı́a de Diseño, Pipeline de Renderizado y el Estándar Cientı́fico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2. Fundamentos y Arquitectura . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1. La Jerarquı́a de Objetos (Artist Hierarchy) y el Grafo de Escena . . . . 2.2.2. Paradigmas de Interacción: La Máquina de Estados de pyplot vs. la Arquitectura Orientada a Objetos . . . . . . . . . . . . . . . . . . . . 2.3. Caracterı́sticas Principales y Personalización . . . . . . . . . . . . . . . . . . . 2.3.1. Diseño de Layouts Complejos con GridSpec . . . . . . . . . . . . . . 2.3.2. Renderizado de Texto y Ecuaciones Matemáticas . . . . . . . . . . . . 2.4. Aplicaciones en Visualización Cientı́fica y Estadı́stica . . . . . . . . . . . . . . 2.4.1. Análisis de Series Temporales . . . . . . . . . . . . . . . . . . . . . . 2.4.2. Diagnósticos de Modelos y Análisis de Residuos . . . . . . . . . . . . 2.4.3. Representación de Funciones Matemáticas Complejas . . . . . . . . . 2.5. Buenas Prácticas de Desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6. Conclusión sobre Matplotlib . . . . . . . . . . . . . . . . . . . . . . . . . . . 3. Módulo Pandas: Fundamentos, Caracterı́sticas y Aplicaciones 3.1. Introducción al Paradigma de Datos Tabulares y Heterogéneos . . . . . . . . . 3.2. Arquitectura Interna: Del BlockManager a la Revolución de PyArrow y Copyon-Write . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abraham Zamudio 2 27 27 28 28 31 33 33 34 34 34 34 35 35 35 36 36 37 Programa de Iniciación Tecnológica
  3. 3.3. El Objeto Index: Inmutabilidad, Hashing y Jerarquı́a . .

    . . . . . . . . . . . . 42 3.4. Semántica de Operaciones: Alineación, Álgebra Relacional y Split-Apply-Combine 42 3.4.1. Alineación Automática y el Manejo de Datos Faltantes . . . . . . . . . 42 3.4.2. Álgebra Relacional: merge, join y concat . . . . . . . . . . . . . . . 42 3.4.3. El Paradigma Split-Apply-Combine (groupby) . . . . . . . . . . . . . 43 3.5. Análisis de Series Temporales y Datos de Alta Frecuencia . . . . . . . . . . . 43 3.6. Aplicaciones en Ingenierı́a de Datos y Buenas Prácticas de Rendimiento . . . . 43 3.7. Conclusión sobre Pandas: Sı́ntesis del Paradigma Analı́tico Moderno . . . . . . 45 Abraham Zamudio 3 Programa de Iniciación Tecnológica
  4. 1 Módulo NumPy: Fundamentos, Caracterı́sticas y Aplicaciones 1.1 Introducción a

    NumPy: Arquitectura, Memoria y el Paradigma de la Computación Cientı́fica NumPy (abreviatura de Numerical Python) no es simplemente una biblioteca de funciones matemáticas; es el sustrato fundamental sobre el cual se erige todo el ecosistema moderno de ciencia de datos, aprendizaje automático y computación cientı́fica en Python. Creada originalmente por Travis Oliphant en 2005, NumPy nació como la solución definitiva a la fragmentación que existı́a en la comunidad cientı́fica de Python a principios de los años 2000. En aquel entonces, existı́an dos bibliotecas rivales para el manejo de arreglos: Numeric, desarrollada por Jim Hugunin, que era rápida pero carecı́a de funcionalidades avanzadas, y Numarray, diseñada especı́ficamente para manejar arreglos de gran tamaño, pero que sacrificaba rendimiento en arreglos pequeños. NumPy unificó lo mejor de ambos mundos, introduciendo el objeto ndarray (N-dimensional array) y estableciendo un estándar de facto que, hasta la fecha, permanece inalterado en su filosofı́a central. Figura 1: El paquete fundamental para la computación cientı́fica con Python La relevancia de NumPy trasciende su API superficial; radica en su capacidad para subvertir las limitaciones estructurales e inherentes al lenguaje Python cuando se enfrenta a cargas de trabajo de computación numérica intensiva. Para comprender la magnitud de esta contribución, es imperativo analizar las diferencias arquitectónicas entre las estructuras de datos nativas de Python y el modelo de memoria de NumPy, ası́ como su interacción con el hardware subyacente y el intérprete de CPython. 1.1.1 La Anatomı́a de la Ineficiencia en Python Nativo Para apreciar el valor de NumPy, primero debemos desentrañar por qué las estructuras de datos nativas de Python, especı́ficamente las listas (list), son intrı́nsecamente ineficientes para el cómputo numérico a gran escala. Python es un lenguaje de tipado dinámico y fuertemente tipado. Esto significa que una lista en Python no es un bloque contiguo de valores primitivos (como un arreglo en C o Java), sino un arreglo dinámico de punteros. Cada elemento dentro de una lista de Python es, en realidad, un puntero a un objeto PyObject disperso en la memoria del heap. Cada uno de estos objetos PyObject contiene, como mı́nimo, Abraham Zamudio 4 Programa de Iniciación Tecnológica
  5. un conteo de referencias (reference counting) para el recolector de

    basura, un puntero a su tipo de dato (para habilitar el despacho dinámico) y el valor en sı́ mismo. Por ejemplo, una lista de Python que contiene un millón de enteros no almacena un millón de enteros en un bloque continuo; almacena un millón de punteros de 8 bytes (en arquitecturas de 64 bits), más la sobrecarga de un millón de objetos enteros individuales, cada uno de los cuales consume aproximadamente 28 bytes. El resultado es un consumo de memoria exorbitante (más de 36 MB para un millón de enteros simples) y una fragmentación severa de la memoria. Además del costo en memoria, esta arquitectura impone una penalización computacional masiva. Cuando Python ejecuta un bucle for para sumar dos listas elemento por elemento, en cada iteración el intérprete debe: (1) desreferenciar el puntero, (2) verificar el tipo del objeto para asegurar que la operación de suma es válida (despacho dinámico), (3) crear un nuevo objeto PyObject para el resultado, y (4) actualizar el conteo de referencias. Este ciclo de verificación y asignación de memoria, conocido como boxing y unboxing, hace que los bucles en Python puro sean órdenes de magnitud más lentos que sus equivalentes en lenguajes compilados. 1.1.2 El Modelo de Memoria Contigua y la Homogeneidad de Tipos NumPy resuelve estos cuellos de botella mediante un cambio de paradigma radical: la homogeneidad de tipos y la asignación de memoria contigua. A diferencia de una lista de Python, un ndarray requiere que todos sus elementos sean exactamente del mismo tipo de dato (e.g., float64, int32). Al eliminar la necesidad de almacenar metadatos de tipo por cada elemento, NumPy almacena los datos puros en un bloque de memoria RAM estrictamente contiguo. Esta contigüidad no es solo una optimización de espacio (una matriz de un millón de float64 consume exactamente 8 MB, sin ninguna sobrecarga adicional), sino que es un requisito fundamental para la eficiencia computacional moderna. La arquitectura de los procesadores modernos (CPU) depende crı́ticamente de la localidad espacial y temporal de los datos. Las CPU no leen la memoria RAM byte por byte; la leen en bloques llamados cache lines (tı́picamente de 64 bytes). Cuando los datos están almacenados de manera contigua, como en un ndarray, el hardware puede predecir y precargar (prefetching) las siguientes lı́neas de caché (L1, L2, L3) de manera automática y altamente eficiente. En contraste, los objetos dispersos de una lista de Python provocan constantes fallos de caché (cache misses), obligando a la CPU a esperar los lentos accesos a la memoria RAM principal. Abraham Zamudio 5 Programa de Iniciación Tecnológica
  6. Definición 1.1 Mapeo Multidimensional y el Mecanismo de Strides Dado

    que la memoria fı́sica de la computadora es estrictamente unidimensional (una secuencia lineal de bytes), NumPy debe mapear matemáticamente la estructura lógica N-dimensional del ndarray a este espacio unidimensional. Esto se logra mediante el mecanismo de strides (pasos). Los strides son una tupla de enteros que indica cuántos bytes debe saltar la memoria para avanzar un paso en cada dimensión. Si tenemos un arreglo 2D de forma (𝑁, 𝑀) con tipo float64 (8 bytes por elemento), los strides en orden C (row-major) serı́an (𝑀 × 8, 8). La dirección de memoria exacta de un elemento en el ı́ndice (𝑖, 𝑗) se calcula mediante la fórmula: Dirección(𝑖, 𝑗) = Puntero Base + (𝑖 × stride0 ) + ( 𝑗 × stride1 ) (1) Este mecanismo permite que operaciones como la transposición (arr.T) o el rebanado (slicing) se realicen en tiempo 𝑂 (1), simplemente modificando la tupla de strides sin necesidad de mover o copiar un solo byte de datos en la memoria fı́sica. 1.1.3 Vectorización, SIMD y la Liberación del GIL La capacidad de NumPy para aplicar operaciones a conjuntos completos de datos sin bucles explı́citos en Python se conoce como vectorización. Desde una perspectiva de ingenierı́a de software, la vectorización es el acto de delegar la iteración desde el intérprete de Python (lento) hacia bucles internos escritos en C altamente optimizados. Al escribir C = A + B, el desarrollador está invocando una función C subyacente que itera sobre los punteros de memoria de A y B de manera nativa, eliminando por completo la sobrecarga del despacho dinámico de Python. Esta vectorización a nivel de C permite, a su vez, la explotación de instrucciones SIMD (Single Instruction, Multiple Data) disponibles en los procesadores modernos (como las extensiones AVX2 o AVX-512 en arquitecturas x86, o NEON en ARM). Las instrucciones SIMD permiten a la CPU realizar la misma operación aritmética (por ejemplo, una suma o multiplicación) sobre múltiples pares de datos simultáneamente en un solo ciclo de reloj. NumPy, a través de su integración con bibliotecas subyacentes y compiladores de C, alinea los datos en memoria para aprovechar estas instrucciones vectoriales, multiplicando el rendimiento por un factor de 4x, 8x o incluso 16x en operaciones aritméticas elementales. Además de la optimización a nivel de instrucción, NumPy aborda inteligentemente una de las mayores limitaciones de CPython: el Global Interpreter Lock (GIL). El GIL es un mecanismo de exclusión mutua que impide que múltiples hilos nativos de Python ejecuten bytecodes simultáneamente, limitando el paralelismo real en Python puro. Sin embargo, las operaciones numéricas pesadas en NumPy (como la multiplicación de matrices, transformadas de Fourier o álgebra lineal) liberan explı́citamente el GIL antes de entrar en sus rutinas de C/Fortran. Esto significa que, aunque Python no pueda ejecutar múltiples hilos de código Python en paralelo, un programa Python puede lanzar múltiples hilos que ejecuten operaciones de NumPy simultáneamente en múltiples núcleos de la CPU, logrando paralelismo real a nivel de hardware. Abraham Zamudio 6 Programa de Iniciación Tecnológica
  7. 1.1.4 El Ecosistema PyData y la Interoperabilidad Finalmente, la introducción

    a NumPy estarı́a incompleta sin reconocer su papel como el protocolo de comunicación universal del ecosistema PyData. NumPy no solo proporciona una estructura de datos eficiente; proporciona un contrato de interoperabilidad. Bibliotecas de manipulación de datos tabulares como Pandas utilizan el ndarray como su motor de almacenamiento subyacente para columnas de datos homogéneos. Bibliotecas de aprendizaje automático como Scikit-Learn exigen que los datos de entrada y salida sean ndarray para garantizar que sus algoritmos de optimización (como el descenso de gradiente o los árboles de decisión) operen con la máxima eficiencia. Más aún, en el ámbito del aprendizaje profundo, frameworks como TensorFlow y PyTorch han diseñado sus estructuras de datos principales (los tensores) para ser conceptual y fı́sicamente compatibles con el ndarray. Gracias a la API de buffer protocol de CPython, la conversión entre un ndarray de NumPy y un tensor de PyTorch puede realizarse mediante una operación de zero-copy (cero copias), compartiendo el mismo bloque de memoria subyacente y evitando transferencias de datos catastróficas para el rendimiento. En resumen, NumPy es la piedra angular que permite a Python competir, e incluso superar, a lenguajes tradicionalmente asociados con la computación de alto rendimiento como C++, Fortran o Julia. Su diseño, que armoniza la expresividad sintáctica de Python con la brutal eficiencia de la memoria contigua, los bucles en C, las instrucciones SIMD y las rutinas de BLAS/LAPACK, lo convierte en una herramienta de dominio obligatorio para cualquier profesional dedicado a la matemática aplicada, la ingenierı́a de datos o la inteligencia artificial. 1.2 Fundamentos y Caracterı́sticas Principales 1.2.1 El objeto ndarray: Anatomı́a y Rol como Sustrato del Ecosistema PyData El núcleo absoluto de NumPy, y por extensión de toda la computación cientı́fica en Python, es la clase numpy.ndarray (N-dimensional array). A diferencia de las listas nativas de Python, que son colecciones heterogéneas de punteros a objetos dispersos, un ndarray es una estructura de datos de alto rendimiento diseñada bajo dos principios inquebrantables: la homogeneidad de tipos y la contigüidad de memoria. Esto significa que todos los elementos dentro de un arreglo deben ser exactamente del mismo tipo de dato primitivo (e.g., float64, int32), y estos deben estar almacenados en un bloque de memoria RAM estrictamente contiguo. Para comprender por qué el ndarray es la piedra angular del ecosistema de datos, primero debemos diseccionar su anatomı́a interna. Un objeto ndarray no es solo un bloque de datos; es un objeto de Python que encapsula un búfer de memoria crudo y una serie de metadatos que dictan cómo interpretar ese búfer. Abraham Zamudio 7 Programa de Iniciación Tecnológica
  8. Definición 1.2 Anatomı́a Interna del ndarray Todo objeto numpy.ndarray está

    compuesto intrı́nsecamente por los siguientes atributos fundamentales: – data: Un búfer de memoria contiguo (o una vista de él) que contiene los valores puros de los elementos. No almacena objetos de Python, sino valores binarios crudos (e.g., 64 bits para un float64). – dtype: El descriptor del tipo de dato. Define cómo deben interpretarse los bytes en el búfer data (tamaño, signo, precisión de punto flotante, etc.). – shape: Una tupla de enteros que define la dimensionalidad lógica del arreglo (e.g., ( 𝑓 𝑖𝑙𝑎𝑠, 𝑐𝑜𝑙𝑢𝑚𝑛𝑎𝑠) para una matriz 2D). – strides: Una tupla de enteros que indica cuántos bytes debe saltar la memoria para avanzar un paso en cada dimensión. Es el mecanismo matemático que permite mapear la memoria 1D a estructuras N-dimensionales. – flags: Metadatos booleanos que indican el estado de la memoria, como si el arreglo es contiguo en estilo C (C CONTIGUOUS) o en estilo Fortran (F CONTIGUOUS), o si es propietario de su memoria (OWNDATA). Esta combinación de un búfer de datos crudo y metadatos de mapeo (shape y strides) convierte al ndarray en una estructura de datos excepcionalmente flexible y eficiente. Sin embargo, su verdadera magnitud radica en que NumPy no diseñó el ndarray para ser una estructura aislada, sino como un protocolo universal de intercambio de datos. El ndarray es el ”lenguaje franca”de la ciencia de datos en Python; casi todas las bibliotecas de análisis, modelado y visualización lo utilizan como su sustrato subyacente. El ndarray como Base de los DataFrames (Pandas) La biblioteca Pandas, el estándar de facto para el manejo de datos tabulares y heterogéneos en Python, está construida casi en su totalidad sobre los cimientos del ndarray. Aunque un DataFrame de Pandas permite tener columnas de diferentes tipos (heterogeneidad a nivel de columna), internamente no almacena los datos en una matriz 2D gigante. En su arquitectura clásica (a través del BlockManager), un DataFrame es esencialmente un diccionario ordenado de arreglos ndarray de 1 dimensión. Cada columna (o bloque de columnas del mismo tipo) es, de hecho, un numpy.ndarray de 1D. La estructura Series de Pandas, que representa una sola columna, es conceptualmente un ndarray de 1D envuelto, al cual se le ha añadido un objeto Index (que a su vez es otro ndarray o una subclase de él) para proporcionar etiquetas personalizadas. Cuando realizas operaciones vectorizadas en un DataFrame (como df[’col a’] + df[’col b’]), Pandas simplemente extrae los ndarray subyacentes, delega la operación a las rutinas de C de NumPy, y envuelve el resultado en una nueva Series. Sin la eficiencia del ndarray, las operaciones tabulares de Pandas serı́an prohibitivamente lentas. El Paradigma Matricial en el Aprendizaje Automático (Scikit-Learn) En el ecosistema del aprendizaje automático tradicional, Scikit-Learn adopta el ndarray como el contrato de entrada y salida para todos sus algoritmos. La convención universal en esta biblioteca es que el conjunto de datos de caracterı́sticas (features) debe ser una matriz 2D de NumPy (denominada convencionalmente como 𝑋, de forma (𝑛 𝑠𝑎𝑚 𝑝𝑙𝑒𝑠, 𝑛 𝑓 𝑒𝑎𝑡𝑢𝑟𝑒𝑠)), y la variable objetivo (target) Abraham Zamudio 8 Programa de Iniciación Tecnológica
  9. debe ser un arreglo 1D (denominado como 𝑦, de forma

    (𝑛 𝑠𝑎𝑚 𝑝𝑙𝑒𝑠, )). Esta dependencia no es arbitraria. Los algoritmos de optimización, desde la regresión lineal mediante mı́nimos cuadrados ordinarios (que requiere inversiones de matrices y productos punto) hasta los árboles de decisión y los modelos de clustering, están implementados en Cython o C y esperan recibir punteros a búferes de memoria contiguos. Al estandarizar el ndarray como la única estructura de entrada válida, Scikit-Learn garantiza que los datos fluyan desde la memoria de Python hacia los algoritmos de C/C++ sin necesidad de costosas conversiones de formato o serialización. La Evolución hacia los Tensores en el Aprendizaje Profundo La influencia del ndarray se extiende hasta las arquitecturas más modernas del aprendizaje profundo. Los frameworks dominantes, como PyTorch y TensorFlow, utilizan estructuras de datos llamadas ”tensores”. Un tensor no es más que una generalización conceptual y práctica del ndarray. Al igual que el ndarray, un tensor es un arreglo multidimensional, homogéneo y contiguo en memoria. La diferencia principal es que los tensores están diseñados para ejecutarse no solo en la CPU, sino también en aceleradores de hardware como GPUs y TPUs, y poseen capacidades nativas para el cálculo de gradientes (autodiferenciación) necesarios para el backpropagation. Sin embargo, la interoperabilidad entre ambos mundos es total. Gracias al Protocolo de Búfer de Python (una API de C que permite a los objetos de Python exponer sus búferes de memoria a otras extensiones de C), es posible convertir un ndarray de NumPy en un tensor de PyTorch mediante una operación de zero-copy (cero copias). Esto significa que ambos objetos apuntan exactamente al mismo bloque de memoria RAM fı́sica; modificar el tensor modifica el arreglo de NumPy instantáneamente, eliminando por completo la sobrecarga de transferencia de datos entre el ecosistema cientı́fico clásico y el de aprendizaje profundo. En conclusión, el numpy.ndarray no debe ser entendido meramente como una ”matriz.o un .arreglo”más. Es el sustrato atómico de la computación de datos en Python. Comprender su naturaleza homogénea, su mapeo mediante strides y su papel como proveedor de memoria para estructuras de mayor nivel como los DataFrames o los tensores, es un requisito indispensable para dominar la arquitectura subyacente de cualquier pipeline moderno de ciencia de datos e inteligencia artificial. 1.2.2 Tipos de Datos (dtypes) NumPy soporta una gama mucho más amplia y precisa de tipos de datos numéricos que Python nativo. Esto es crucial para controlar el uso de memoria y la precisión numérica en cálculos cientı́ficos: – Enteros: int8, int16, int32, int64 (y sus versiones sin signo uint8, etc.). – Punto flotante: float16, float32, float64 (equivalente a float en Python), float128. – Complejos: complex64, complex128. – Booleanos: bool (True/False). 1.2.3 Creación de Arreglos Existen múltiples funciones para instanciar arreglos, dependiendo de la necesidad inicial del modelo o análisis: – np.array(): Convierte una secuencia de Python (lista, tupla) en un ndarray. Abraham Zamudio 9 Programa de Iniciación Tecnológica
  10. – np.zeros() y np.ones(): Crean arreglos llenos de ceros o

    unos, respectivamente, dada una forma especı́fica. – np.empty(): Crea un arreglo sin inicializar sus valores (contiene residuos de memoria), siendo ligeramente más rápido que zeros. – np.arange(start, stop, step): Similar a la función nativa range, pero devuelve un ndarray. – np.linspace(start, stop, num): Genera num muestras espaciadas uniformemente en el intervalo cerrado [𝑠𝑡𝑎𝑟𝑡, 𝑠𝑡𝑜 𝑝]. – np.eye(N): Crea una matriz identidad de 𝑁 × 𝑁. import numpy as np # Desde una lista , especificando el tipo de dato arr1 = np. array ([1 , 2, 3, 4], dtype =np. float32 ) # Matriz de ceros de 2x3 arr2 = np. zeros ((2 , 3)) # Secuencia de 0 a 10 con paso de 2 arr3 = np. arange (0, 11, 2) # 5 puntos equidistantes entre 0 y 1 arr4 = np. linspace (0, 1, 5) Listing 1: Ejemplos de creación de arreglos en NumPy 1.3 Operaciones y Manipulación de Arreglos 1.3.1 Indexación y Slicing La indexación en NumPy es poderosa y flexible, permitiendo acceder a elementos individuales, subconjuntos o máscaras lógicas de manera eficiente. – Indexación básica: arr[i, j] para acceder al elemento en la fila 𝑖 y columna 𝑗. – Slicing (Rebanado): arr[start:stop:step]. El slicing en NumPy devuelve una vista del arreglo original, no una copia. Modificar la vista modifica el arreglo original. – Indexación avanzada (Fancy Indexing): Permite pasar un arreglo de ı́ndices (enteros) para seleccionar elementos arbitrarios no contiguos. – Indexación booleana: Permite filtrar elementos mediante una condición. Por ejemplo, arr[arr >5] devuelve un nuevo arreglo 1D con solo los elementos que cumplen la condición. 1.3.2 Vectorización y Broadcasting: Abstracción de Alto Nivel y Optimización de Bajo Nivel La vectorización y el broadcasting no son simplemente comodidades sintácticas para escribir código más limpio; son los mecanismos fundamentales mediante los cuales NumPy cierra la Abraham Zamudio 10 Programa de Iniciación Tecnológica
  11. brecha de rendimiento entre la expresividad de Python y la

    velocidad del hardware subyacente. Comprender estos conceptos requiere trascender la API superficial y analizar cómo interactúan con el modelo de objetos de CPython y la arquitectura de la memoria. La Anatomı́a de la Vectorización La vectorización es el proceso de eliminar bucles explı́citos en Python (escritos en bytecode) y delegar la iteración a bucles internos escritos en C altamente optimizados. En un bucle for nativo de Python, cada iteración incurre en una sobrecarga masiva: el intérprete debe evaluar el tipo de los operandos (despacho dinámico), crear un nuevo objeto PyObject para el resultado, y actualizar los contadores de referencias para el recolector de basura. Al vectorizar una operación (por ejemplo, C = A + B), NumPy invoca una Función Universal (ufunc). Las ufuncs son bucles en C que iteran directamente sobre los búferes de memoria contiguos de A y B, leyendo los valores binarios crudos, realizando la operación aritmética a nivel de registro de la CPU, y escribiendo el resultado en el búfer de C. Este enfoque no solo elimina la sobrecarga del intérprete, sino que permite al compilador de C y al hardware de la CPU aplicar optimizaciones agresivas: – Localidad de Caché y Prefetching: Al iterar sobre memoria contigua, el controlador de memoria de la CPU puede predecir los siguientes accesos y precargar los datos en la caché L1/L2, minimizando los costosos fallos de caché (cache misses). – Instrucciones SIMD: Las ufuncs de NumPy están compiladas para aprovechar las extensiones SIMD (Single Instruction, Multiple Data) como AVX2 o AVX-512 en arquitecturas x86. Esto permite a la CPU realizar la misma operación aritmética (ej. una suma de float64) sobre múltiples pares de datos simultáneamente en un solo ciclo de reloj, multiplicando el rendimiento por un factor de 4x a 8x. El Broadcasting: La Ilusión de la Replicación de Memoria El broadcasting (difusión) es el conjunto de reglas que permite a NumPy realizar operaciones aritméticas entre arreglos de diferentes formas (shapes) sin necesidad de crear copias explı́citas de los datos. Es la herramienta que permite, por ejemplo, sumar un escalar a una matriz, o restar el vector de medias de cada columna a un DataFrame completo. Teorema 1.1 Reglas Algorı́tmicas de Broadcasting Para determinar si dos arreglos 𝐴 y 𝐵 son compatibles para el broadcasting, NumPy alinea sus formas (shapes) desde la dimensión final (más a la derecha) hacia la izquierda. Dos dimensiones son compatibles si y solo si: 1. Son exactamente iguales, o 2. Una de ellas es igual a 1. Si ninguna de estas condiciones se cumple para alguna dimensión alineada, NumPy lanza una excepción ValueError. Si las condiciones se cumplen, el arreglo con dimensión 1 se estira conceptualmente a lo largo de ese eje para coincidir con la dimensión del otro arreglo. El Secreto de Bajo Nivel: Strides Cero La verdadera genialidad del broadcasting radica en cómo se implementa en memoria. NumPy no replica los datos en memoria fı́sica para hacer que Abraham Zamudio 11 Programa de Iniciación Tecnológica
  12. las formas coincidan (lo cual serı́a catastrófico para el consumo

    de RAM). En su lugar, manipula los strides (pasos de memoria) del arreglo. Cuando un eje de tamaño 1 necesita ser .estirado”para coincidir con un eje de tamaño 𝑁, NumPy simplemente establece el stride de esa dimensión a 0. Matemáticamente, esto significa que al avanzar un paso en esa dimensión, el puntero de memoria no se mueve; el mismo valor en memoria es leı́do 𝑁 veces. Esta operación de zero-copy (cero copias) garantiza que el broadcasting sea extremadamente eficiente tanto en tiempo de CPU como en uso de memoria. Aplicaciones en Ciencia de Datos y Aprendizaje Automático El broadcasting es ubicuo en el preprocesamiento de datos y la arquitectura de redes neuronales. Algunos casos de uso crı́ticos incluyen: – Estandarización de Caracterı́sticas: Restar la media (𝜇) y dividir por la desviación estándar (𝜎) de cada columna en una matriz de caracterı́sticas 𝑋 ∈ R𝑚×𝑛 . El vector de medias 𝜇 ∈ R𝑛 se difunde a lo largo de las 𝑚 filas. – Adición de Bias en Redes Neuronales: En un lote (batch) de predicciones 𝑍 ∈ R𝑏×𝑐 (donde 𝑏 es el tamaño del lote y 𝑐 el número de clases), el vector de sesgo (bias) 𝑏 ∈ R𝑐 se suma a cada fila de 𝑍 mediante broadcasting. – Cálculo de Distancias (Outer Operations): Calcular la distancia entre todos los pares de puntos en dos conjuntos de datos utilizando operaciones de resta y expansión de dimensiones (np.newaxis). import numpy as np # Simulacion de un lote de 1000 imagenes de 32 x32 en escala de grises # Shape: ( batch_size , height , width ) = (1000 , 32, 32) batch_images = np. random .rand (1000 , 32, 32) # 1. VECTORIZACION : Calcular la media y desviacion estandar de todo el lote # En lugar de bucles , usamos ufuncs optimizadas en C mean_val = np.mean( batch_images ) std_val = np.std( batch_images ) # 2. BROADCASTING : Estandarizar el lote completo en una sola linea # El escalar mean_val y std_val se difunden a los 1 ,024 ,000 pixeles normalized_images = ( batch_images - mean_val ) / ( std_val + 1e -8) # 3. BROADCASTING AVANZADO : Restar un vector de 1D a una matriz 2D # Supongamos una matriz de caracteristicas X de (1000 , 50) X = np. random .rand (1000 , 50) # Calculamos la media de cada columna ( resulta en un vector de shape (50 ,)) col_means = np.mean(X, axis =0) # Broadcasting : col_means (50 ,) se alinea con X (1000 , 50) desde la derecha . # NumPy " estira " conceptualmente col_means a lo largo de las 1000 filas. Abraham Zamudio 12 Programa de Iniciación Tecnológica
  13. X_centered = X - col_means # Verificacion de la no

    - copia de memoria en broadcasting # Si creamos una vista con broadcasting , los strides de la dimension 1 son 0 broadcast_view = np. broadcast_to (col_means , (1000 , 50)) print(" Strides de la vista difundida :", broadcast_view . strides ) # Salida esperada : (0, 8) -> El stride de la fila es 0 (zero -copy) Listing 2: Aplicación práctica de Vectorización y Broadcasting en ML En resumen, la vectorización y el broadcasting son los pilares que permiten a NumPy expresar operaciones matemáticas complejas sobre grandes volúmenes de datos con una sintaxis declarativa y elegante, mientras que bajo el capó se ejecutan como rutinas de C de altı́simo rendimiento, aprovechando al máximo la jerarquı́a de memoria y las instrucciones vectoriales del procesador. 1.3.3 Funciones Universales (ufuncs): Arquitectura en C y Operaciones Algebraicas Avanzadas Las Funciones Universales, o ufuncs (del inglés universal functions), no son meras funciones de Python que aceptan arreglos; son la abstracción de más alto nivel sobre los bucles de C optimizados que ejecutan la aritmética elemental en NumPy. Mientras que el módulo estándar math de Python solo opera sobre escalares, las ufuncs están diseñadas para operar element-wise (elemento por elemento) sobre arreglos N-dimensionales, manejando internamente el broadcasting, el almacenamiento en caché y la ejecución vectorizada (SIMD). Definición 1.3 Arquitectura Interna de una ufunc A nivel de la API de C de NumPy, una ufunc es una instancia de la estructura PyUFuncObject. Esta estructura no contiene el código matemático en sı́, sino que actúa como un despachador que encapsula: – El Bucle Interno (Inner Loop): Un puntero a una función de C que realiza la operación primitiva (ej. suma, seno) sobre tipos de datos crudos de C (como double o float). – El Iterador de Arreglos: Un mecanismo que recorre los búferes de memoria de los operandos, resolviendo las direcciones de memoria mediante los strides y aplicando las reglas de broadcasting en tiempo de ejecución. – Resolución de Tipos (Type Resolution): La lógica que determina el tipo de dato de salida (dtype) basándose en las reglas de promoción de tipos de NumPy (ej. operar un int32 con un float32 promueve el resultado a float64 para evitar desbordamientos o pérdida de precisión). Métodos Algebraicos Avanzados: Más allá del Element-wise La verdadera potencia de las ufuncs radica en que no se limitan a operaciones binarias o unarias elemento por elemento. Al ser objetos de primera clase, poseen métodos que implementan patrones de diseño algorı́tmico fundamentales (como el plegado fold y el barrido scan de la programación funcional), permitiendo reducir dimensiones o calcular operaciones dependientes del orden sin escribir una sola lı́nea de bucles en Python. Abraham Zamudio 13 Programa de Iniciación Tecnológica
  14. – Reducción (reduce): Colapsa una dimensión del arreglo aplicando la

    operación binaria de forma acumulativa a lo largo de un eje especı́fico. Matemáticamente, es una operación de plegado (fold). Por ejemplo, np.add.reduce(arr) es equivalente a np.sum(arr), pero np.logical and.reduce(arr, axis=0) es extraordinariamente útil para verificar si todas las filas de una matriz booleana cumplen una condición simultáneamente, reemplazando costosos bucles de validación. – Acumulación (accumulate): A diferencia de reduce, que devuelve un solo valor escalar por eje, accumulate devuelve un arreglo del mismo tamaño que la entrada, donde cada elemento 𝑘 es el resultado de aplicar la operación a todos los elementos desde el ı́ndice 0 hasta 𝑘. Es la implementación del algoritmo de prefix sum (suma de prefijos) o barrido (scan). np.add.accumulate es la base de np.cumsum, pero su uso con otras operaciones (como np.multiply.accumulate para factoriales o np.maximum.accumulate para máximos acumulados) es vital en series temporales y procesamiento de señales. – Producto Exterior (outer): Aplica la operación a todos los pares posibles de elementos de dos arreglos 1D, generando una matriz 2D. Si 𝐴 tiene tamaño 𝑁 y 𝐵 tiene tamaño 𝑀, np.subtract.outer(A, B) genera una matriz 𝑁 × 𝑀 donde el elemento (𝑖, 𝑗) es 𝐴𝑖 − 𝐵 𝑗 . Esto es la base para calcular matrices de distancia de manera vectorizada o crear mallas (meshgrids) para evaluar funciones bivariadas. – Reducción Parcial (reduceat): El método más técnico y menos conocido. Permite realizar reducciones en çhunk.o bloques especı́ficos definidos por un arreglo de ı́ndices. Es fundamental para implementar operaciones de agrupamiento (groupby) o binning de alta velocidad a nivel de C, evitando la sobrecarga de las funciones de agrupamiento de alto nivel cuando se requiere rendimiento extremo. La Trampa de np.vectorize y la Extensibilidad Es crucial distinguir entre las ufuncs nativas de NumPy y la función np.vectorize. Aunque np.vectorize permite aplicar una función escalar de Python a un arreglo, no es una verdadera ufunc ni ofrece mejoras de rendimiento; es simplemente un bucle for de Python disfrazado con sintaxis de arreglo (su documentación oficial la clasifica como una ”función de conveniencia”). Para crear verdaderas ufuncs personalizadas que operen a velocidad de C, los profesionales de la ciencia de datos utilizan compiladores JIT (Just-In-Time) como Numba (mediante el decorador @vectorize) o extensiones de Cython. Esto permite inyectar bucles internos personalizados directamente en el núcleo de NumPy, manteniendo la compatibilidad con el broadcasting y la resolución de tipos nativa. import numpy as np # 1. OUTER : Calculo vectorizado de una matriz de distancias 1D # Dados dos conjuntos de puntos en una linea puntos_A = np. array ([1.0 , 3.5 , 7.2]) puntos_B = np. array ([2.0 , 5.0 , 6.0 , 9.0]) # Matriz de diferencias (A_i - B_j) usando outer # Shape resultante : (3, 4) diff_matrix = np. subtract . outer (puntos_A , puntos_B ) distancias = np.abs( diff_matrix ) # 2. ACCUMULATE : Maximo acumulado ( Prefix Maximum ) Abraham Zamudio 14 Programa de Iniciación Tecnológica
  15. # Util en series temporales para encontrar el " drawdown

    " o caada desde el pico serie_tiempo = np. array ([3 , 1, 4, 1, 5, 9, 2, 6]) # El maximo hasta el paso k maximos_acumulados = np. maximum . accumulate ( serie_tiempo ) # Calculo vectorizado de la caida ( drawdown ) respecto al pico historico drawdown = maximos_acumulados - serie_tiempo # 3. REDUCE : Validacion matricial de restricciones # Supongamos una matriz de 1000 muestras y 50 caracteristicas booleanas # (True si la caracteristica cumple una restriccion fisica ) restricciones = np. random . choice ([ True , False ], size =(1000 , 50) , p =[0.9 , 0.1]) # Queremos saber que muestras ( filas ) cumplen TODAS las restricciones # np. logical_and . reduce colapsa las columnas (axis =1) muestras_validas = np. logical_and . reduce ( restricciones , axis =1) cantidad_validas = np.sum( muestras_validas ) # Cuantas filas son True Listing 3: Aplicación avanzada de métodos de ufuncs en Álgebra Lineal y Series Temporales En sı́ntesis, las ufuncs son el motor aritmético de NumPy. Dominar sus métodos avanzados (reduce, accumulate, outer) permite al ingeniero de datos y al matemático aplicado traducir algoritmos secuenciales y bucles anidados en expresiones matriciales puras, aprovechando al máximo la arquitectura de la CPU y garantizando un código que es simultáneamente más legible, matemáticamente elegante y computacionalmente óptimo. 1.3.4 Manipulación de la Forma (Reshaping) Cambiar la forma de un arreglo es una operación común que, cuando es posible, devuelve una vista sin copiar los datos subyacentes: – arr.reshape(new shape): Cambia la forma del arreglo. El nuevo tamaño debe ser compatible con el tamaño original. – arr.flatten(): Devuelve una copia del arreglo colapsado en una sola dimensión (1D). – arr.ravel(): Devuelve una vista del arreglo colapsado en 1D siempre que sea posible. – arr.T o np.transpose(arr): Transpone el arreglo (invierte sus ejes). 1.4 Álgebra Lineal y Operaciones Matemáticas: El Motor Numérico de Alto Rendimiento El paquete numpy.linalg actúa como el puente de alto nivel entre la API de Python y las bibliotecas de referencia absoluta en computación cientı́fica: BLAS (Basic Linear Algebra Subprograms) y LAPACK (Linear Algebra PACKage). NumPy no reescribe estos algoritmos desde cero; en su lugar, se enlaza dinámicamente contra implementaciones altamente optimizadas de estas bibliotecas, como OpenBLAS, Intel MKL (Math Kernel Library) o Apple Accelerate. Esto garantiza que las operaciones de álgebra lineal no solo sean matemáticamente correctas, sino Abraham Zamudio 15 Programa de Iniciación Tecnológica
  16. que exploten al máximo la arquitectura del hardware mediante multihilos

    nativos, instrucciones SIMD y optimizaciones de caché de nivel L1/L2. A continuación, se desglosan las operaciones fundamentales, sus implicaciones numéricas y sus extensiones tensoriales. 1.4.1 Producto Punto, Contracción Tensorial y la Convención de Einstein La multiplicación de matrices es la operación primitiva más costosa en el aprendizaje automático y la simulación fı́sica. NumPy ofrece múltiples interfaces para esta operación, cada una con semánticas distintas: – El operador @ (np.matmul): Es el estándar moderno para la multiplicación de matrices 2D. A diferencia de np.dot, matmul respeta las reglas de broadcasting para arreglos Ndimensionales, permitiendo la multiplicación por lotes (batched matrix multiplication) de manera nativa. Si 𝐴 tiene forma (𝑘, 𝑚, 𝑛) y 𝐵 tiene forma (𝑘, 𝑛, 𝑝), A @ B realiza 𝑘 multiplicaciones de matrices independientes de forma (𝑚, 𝑝), todo ello ejecutado en un solo bucle de C optimizado. – Contracción Tensorial con np.einsum: La función np.einsum (Einstein summation convention) es la herramienta más poderosa y flexible para el álgebra multilineal. Permite expresar operaciones complejas como trazas, productos externos, transposiciones y contracciones de tensores de orden superior mediante una notación de subı́ndices basada en cadenas de texto. Es fundamental en fı́sica computacional, redes neuronales tensoriales y geometrı́a diferencial. 1.4.2 Descomposición Espectral: Autovalores y Autovectores El análisis espectral es la base del Análisis de Componentes Principales (PCA), la mecánica cuántica y el análisis de sistemas dinámicos. Dada una matriz cuadrada 𝐴, buscamos escalares 𝜆 y vectores no nulos 𝑣 tales que 𝐴𝑣 = 𝜆𝑣. – np.linalg.eig: Calcula los autovalores y autovectores derechos para matrices cuadradas generales. Dado que una matriz arbitraria puede tener raı́ces complejas, esta función devuelve arreglos con dtype complejo (complex128) si es necesario. – np.linalg.eigh: Esta es la función de elección cuando la matriz es simétrica (o Hermitiana en el campo complejo), como es el caso de las matrices de covarianza en estadı́stica. Al explotar la simetrı́a, eigh utiliza algoritmos de divide y vencerás (como el de Pal-BhattacharyyaRajopadhye) que no solo garantizan que todos los autovalores sean estrictamente reales, sino que los autovectores resultantes son ortonormales. Además, es computacionalmente más rápida y numéricamente más estable que eig. 1.4.3 Descomposición en Valores Singulares (SVD) y Aproximación de Rango La SVD es considerada la ”navaja suiza”del álgebra lineal. Factoriza cualquier matriz 𝐴 ∈ R𝑚×𝑛 en el producto 𝐴 = 𝑈Σ𝑉 𝑇 , donde 𝑈 y 𝑉 son matrices ortogonales y Σ es una matriz diagonal con valores singulares no negativos ordenados de mayor a menor. – SVD Completa vs. Económica: np.linalg.svd(A, full matrices=True) devuelve 𝑈 de (𝑚, 𝑚) y 𝑉 𝑇 de (𝑛, 𝑛). En ciencia de datos, casi siempre se prefiere la SVD económica (full matrices=False), que devuelve 𝑈 de (𝑚, 𝑘) y 𝑉 𝑇 de (𝑘, 𝑛), donde 𝑘 = mı́n(𝑚, 𝑛), ahorrando memoria y tiempo de cómputo al descartar bases nulas. Abraham Zamudio 16 Programa de Iniciación Tecnológica
  17. – Aproximación de Mejor Rango: Según el Teorema de Eckart-Young-Mirsky,

    truncar la SVD a los 𝑟 valores singulares más grandes proporciona la mejor aproximación de rango 𝑟 a la matriz original en la norma de Frobenius. Esta es la base matemática de la compresión de imágenes, la eliminación de ruido y la reducción de dimensionalidad (PCA). 1.4.4 Resolución de Sistemas Lineales y Estabilidad Numérica Resolver el sistema 𝐴𝑥 = 𝑏 es una de las operaciones más frecuentes en la ingenierı́a. – La Regla de Oro: Evitar la Inversa: Calcular explı́citamente la matriz inversa 𝐴−1 mediante np.linalg.inv(A) y luego multiplicar 𝐴−1 𝑏 es una práctica desaconsejada. Es computacionalmente más costosa (requiere más operaciones de punto flotante) y, lo que es peor, numéricamente inestable. – np.linalg.solve: Esta función resuelve 𝐴𝑥 = 𝑏 directamente utilizando la descomposición LU con pivoteo parcial (algoritmo de LAPACK dgesv). Transforma el problema en resolver dos sistemas triangulares (𝐿𝑦 = 𝑏 y 𝑈𝑥 = 𝑦), lo cual es drásticamente más rápido y preserva la precisión de punto flotante. – Número de Condición: Antes de resolver un sistema, es vital evaluar su sensibilidad a errores de redondeo mediante el número de condición 𝜅( 𝐴) = ∥ 𝐴∥ · ∥ 𝐴−1 ∥, calculable con np.linalg.cond(A). Si 𝜅( 𝐴) es muy grande, la matriz está mal condicionada (cercana a ser singular), y la solución numérica podrı́a ser errónea incluso con solve. Definición 1.4 Pseudo-inversa y Sistemas Sobredeterminados Cuando un sistema 𝐴𝑥 = 𝑏 no tiene solución exacta (por ejemplo, en problemas de mı́nimos cuadrados donde 𝑚 > 𝑛), o cuando 𝐴 es singular, la inversa no existe. NumPy resuelve esto mediante la pseudo-inversa de Moore-Penrose (np.linalg.pinv), la cual se calcula de manera robusta utilizando la SVD. Los valores singulares menores que un umbral de tolerancia (rcond) se tratan como cero, proporcionando la solución de norma mı́nima para sistemas subdeterminados, o la solución de mı́nimos cuadrados para sistemas sobredeterminados. import numpy as np # 1. EINSUM : Contraccion tensorial avanzada # Producto punto de dos matrices A (3 x4) y B (4 x5) usando notacion de Einstein A = np. random .rand (3, 4) B = np. random .rand (4, 5) # ’ij ,jk ->ik ’ indica sumar sobre el indice j ( columnas de A, filas de B) C_einsum = np. einsum (’ij ,jk ->ik ’, A, B) # Equivalente a A @ B, pero einsum escala a tensores de orden N sin ambiguedad # 2. SVD y Aproximacion de Rango ( Compresion de Matriz ) M = np. random .rand (100 , 80) U, S, Vt = np. linalg .svd(M, full_matrices = False ) Abraham Zamudio 17 Programa de Iniciación Tecnológica
  18. # Reconstruccion de la matriz usando solo los 10 componentes

    principales ( rango 10) k = 10 M_approx = U[:, :k] @ np.diag(S[:k]) @ Vt [:k, :] # El error de Frobenius es minimo para cualquier aproximacion de rango 10 error_frobenius = np. linalg .norm(M - M_approx , ’fro ’) # 3. Resolucion de Sistemas y Analisis de Condicionamiento # Sistema lineal Ax = b A_sys = np. array ([[3 , 1], [1, 2]]) b_sys = np. array ([9 , 8]) # Verificar estabilidad numerica antes de resolver cond_num = np. linalg .cond( A_sys ) print(f" Numero de condicion : { cond_num :.2f}") # Valor bajo = bien condicionado # Resolver usando descomposicion LU (via LAPACK ) x = np. linalg . solve (A_sys , b_sys ) # 4. EIGH: Analisis espectral de una Matriz de Covarianza ( Simetrica ) # Generar datos y calcular matriz de covarianza ( garantizada simetrica y semidefinida positiva ) data = np. random . randn (500 , 4) cov_matrix = np.cov(data , rowvar = False ) # eigh es mas rapido y estable que eig para matrices simetricas eigenvalues , eigenvectors = np. linalg .eigh( cov_matrix ) # Los autovalores estan garantizados ser reales y ordenados ascendentemente Listing 4: Operaciones Avanzadas de Álgebra Lineal y Tensores en NumPy En conclusión, el dominio del álgebra lineal en NumPy exige ir más allá de la sintaxis básica. Requiere comprender cuándo utilizar eigh en lugar de eig, por qué solve es superior a inv, cómo aprovechar la SVD para la extracción de caracterı́sticas de bajo rango, y cómo utilizar einsum para manipular tensores de alto orden con la elegancia y eficiencia del cálculo tensorial clásico. 1.5 Aplicaciones en Ciencia de Datos e Ingenierı́a 1.5.1 Generación de Números Aleatorios: Teorı́a, Arquitectura y Reproducibilidad La generación de números aleatorios es uno de los pilares más crı́ticos y, paradójicamente, más malentendidos de la computación cientı́fica. En un entorno determinista como una computadora digital, la verdadera aleatoriedad es inalcanzable; lo que NumPy proporciona son números pseudoaleatorios: secuencias deterministas de bits que, bajo pruebas estadı́sticas rigurosas, son indistinguibles de secuencias verdaderamente aleatorias. La calidad de estas secuencias no es un detalle trivial: un generador deficiente puede introducir sesgos sistemáticos en simulaciones de Monte Carlo, invalidar intervalos de confianza en inferencia estadı́stica o provocar patrones de convergencia espurios en el entrenamiento de redes neuronales. Abraham Zamudio 18 Programa de Iniciación Tecnológica
  19. Evolución Histórica: Del Estado Global a la Arquitectura Moderna Antes

    de la versión 1.17 de NumPy (lanzada en 2019), la generación de números aleatorios se gestionaba mediante un estado global implı́cito controlado por la función np.random.seed(). Este enfoque, heredado de las primeras versiones de NumPy, presentaba problemas fundamentales de diseño: – Acoplamiento global: Cualquier módulo, biblioteca o función de terceros que llamara a np.random.seed() o generara un número aleatorio alteraba el estado compartido, destruyendo la reproducibilidad del código principal de manera silenciosa e impredecible. – Imposibilidad de paralelismo seguro: En entornos multihilo o multiproceso, múltiples flujos de ejecución compitiendo por el mismo estado global generaban condiciones de carrera (race conditions) y secuencias correlacionadas, invalidando la independencia estadı́stica de las muestras. – Algoritmo obsoleto: El generador subyacente era el Mersenne Twister (MT19937), que aunque posee un periodo astronómico de 219937 − 1, tiene deficiencias conocidas: falla ciertas pruebas estadı́sticas modernas (como las de BigCrush de la suite TestU01), tiene un estado interno de 2.5 KB que tarda en çalentarse”si se inicializa con semillas de baja entropı́a, y su implementación no es criptográficamente segura ni eficiente en hardware moderno. La API Moderna: default rng() y la Arquitectura de Tres Capas A partir de la versión 1.17, NumPy introdujo una arquitectura completamente nueva, orientada a objetos y basada en tres capas claramente separadas: Definición 1.5 Arquitectura del Sistema Aleatorio de NumPy El sistema de generación aleatoria moderno se compone de tres componentes desacoplados: 1. SeedSequence (numpy.random.SeedSequence): Es el mecanismo de inicialización de alta entropı́a. Toma una semilla de usuario (un entero, una lista de enteros o incluso None para aleatoriedad del sistema operativo) y la transforma mediante un algoritmo de hashing criptográfico (basado en MurmurHash3) en un estado inicial de alta calidad. Su propiedad más importante es la capacidad de spawn (generar) sub-secuencias independientes y no correlacionadas para entornos paralelos. 2. BitGenerator: Es el motor de bajo nivel que produce el flujo crudo de bits pseudoaleatorios. El predeterminado es PCG64 (Permuted Congruential Generator de 64 bits), diseñado por Melissa O’Neill. PCG64 ofrece un periodo de 2128 , pasa todas las pruebas de TestU01, tiene un estado interno de solo 16 bytes (frente a los 2.5 KB de MT19937), y es significativamente más rápido gracias a su simplicidad aritmética. 3. Generator (numpy.random.Generator): Es la interfaz de alto nivel que el usuario interactúa directamente. Toma un BitGenerator y expone métodos para muestrear de distribuciones estadı́sticas especı́ficas (normal, uniforme, binomial, etc.), aplicando transformaciones matemáticas (como el método de Box-Muller o la inversión de la CDF) sobre el flujo crudo de bits. Distribuciones Estadı́sticas y Métodos de Muestreo El objeto Generator proporciona una biblioteca exhaustiva de distribuciones de probabilidad, implementadas en C y altamente vectorizadas. Las más relevantes para la ciencia de datos y la ingenierı́a incluyen: Abraham Zamudio 19 Programa de Iniciación Tecnológica
  20. – Distribuciones Continuas: – rng.uniform(low, high, size): Distribución uniforme continua

    U (𝑎, 𝑏). Es la base de todas las demás distribuciones. – rng.normal(loc, scale, size): Distribución gaussiana N (𝜇, 𝜎 2 ). Utiliza internamente el algoritmo de Zigurat, que es más eficiente que Box-Muller para muestreo masivo. – rng.exponential(scale, size): Distribución exponencial, fundamental para modelar tiempos de espera y procesos de Poisson. – rng.beta(a, b, size): Distribución Beta, esencial en inferencia bayesiana como distribución conjugada previa para proporciones. – rng.gamma(shape, scale, size): Distribución Gamma, utilizada en modelado de tiempos de vida y como base para generar variables chi-cuadrado. – Distribuciones Discretas: – rng.integers(low, high, size): Enteros uniformes en [𝑙𝑜𝑤, ℎ𝑖𝑔ℎ). Reemplaza al obsoleto randint. – rng.binomial(n, p, size): Distribución binomial, para modelar conteos de éxitos en ensayos independientes. – rng.poisson(lam, size): Distribución de Poisson, para modelar eventos raros en intervalos de tiempo o espacio. – rng.choice(a, size, replace, p): Muestreo aleatorio de un arreglo 1D, con o sin reemplazo, y con probabilidades personalizadas. Es la base del bootstrapping y el remuestreo. Reproducibilidad y Paralelismo con SeedSequence.spawn La capacidad más poderosa de la API moderna es la generación de flujos aleatorios independientes y reproducibles para computación paralela. En lugar de usar semillas arbitrarias (como seed=42, seed=43, etc., lo cual no garantiza independencia estadı́stica), SeedSequence utiliza un esquema de tree hashing que garantiza que las sub-secuencias generadas no tengan correlación cruzada. import numpy as np # ============================================================ # 1. USO BASICO : Generador reproducible con semilla explicita # ============================================================ rng = np. random . default_rng (seed =42) # Muestreo de una distribucion normal multivariada media = np. array ([0.0 , 5.0]) covarianza = np. array ([[1.0 , 0.8] , [0.8 , 2.0]]) datos_normales = rng. multivariate_normal (mean=media , cov= covarianza , size =1000) # ============================================================ # 2. SIMULACION DE MONTE CARLO : Estimacion de Pi # ============================================================ def estimar_pi_montecarlo (rng , n_puntos =1 _000_000 ): Abraham Zamudio 20 Programa de Iniciación Tecnológica
  21. """ Estima Pi generando puntos uniformes en el cuadrado unitario

    ." "" # Generar coordenadas (x, y) ˜ U(0, 1) de forma vectorizada puntos = rng. random (( n_puntos , 2)) # Calcular distancias al origen : sqrt(xˆ2 + yˆ2) distancias = np. linalg .norm(puntos , axis =1) # Contar puntos dentro del cuarto de circulo unitario dentro_circulo = np.sum( distancias <= 1.0) return 4.0 * dentro_circulo / n_puntos rng_mc = np. random . default_rng (seed =123) pi_estimado = estimar_pi_montecarlo (rng_mc , n_puntos =5 _000_000 ) print(f" Estimacion de Pi: { pi_estimado :.6f}") # aprox . 3.14159 # ============================================================ # 3. PARALELISMO SEGURO : Spawn de generadores independientes # ============================================================ # Escenario : Simulacion en 4 procesos / hilos independientes seed_seq = np. random . SeedSequence (42) # spawn () genera 4 SeedSequence hijas con flujos NO correlacionados child_seeds = seed_seq . spawn (4) # Cada hilo/ proceso recibe su propio generador independiente generadores = [np. random . default_rng (s) for s in child_seeds ] # Verificacion : cada generador produce una secuencia distinta for i, gen in enumerate ( generadores ): print (f" Generador {i}: {gen. random (3)}") # ============================================================ # 4. INICIALIZACION DE PESOS EN REDES NEURONALES ( Xavier /He) # ============================================================ def inicializar_pesos_he (rng , fan_in , fan_out ): """ Inicializacion de He para capas con activacion ReLU.""" # Desviacion estandar = sqrt (2 / fan_in ) std = np.sqrt (2.0 / fan_in ) return rng. normal (loc =0.0 , scale =std , size =( fan_in , fan_out )) rng_nn = np. random . default_rng (seed =0) W1 = inicializar_pesos_he (rng_nn , fan_in =784 , fan_out =256) W2 = inicializar_pesos_he (rng_nn , fan_in =256 , fan_out =10) # ============================================================ # 5. BOOTSTRAPPING : Intervalos de confianza no parametricos # ============================================================ rng_boot = np. random . default_rng (seed =99) muestra_original = rng_boot . exponential ( scale =5.0 , size =200) n_bootstrap = 10 _000 medias_bootstrap = np. empty ( n_bootstrap ) for i in range ( n_bootstrap ): Abraham Zamudio 21 Programa de Iniciación Tecnológica
  22. # Remuestreo con reemplazo usando rng. choice muestra_remuestreada = rng_boot

    . choice ( muestra_original , size =200 , replace =True) medias_bootstrap [i] = np.mean( muestra_remuestreada ) # Intervalo de confianza del 95 % basado en percentiles ic_inferior , ic_superior = np. percentile ( medias_bootstrap , [2.5 , 97.5]) Listing 5: Generación de números aleatorios: API moderna, reproducibilidad y paralelismo Buenas Prácticas y Consideraciones Finales Para garantizar la integridad estadı́stica y la reproducibilidad en proyectos de producción, se deben observar las siguientes directrices: 1. Nunca usar el estado global: Evitar np.random.seed(), np.random.rand() y todas las funciones del módulo np.random que no pasen por un objeto Generator. Estas funciones están oficialmente en modo de mantenimiento y no recibirán mejoras. 2. Pasar el generador como argumento: En el diseño de software modular, las funciones que requieran aleatoriedad deben recibir el objeto rng como parámetro explı́cito, en lugar de crear su propio generador interno. Esto permite que el llamador controle la reproducibilidad de todo el pipeline. 3. Evitar semillas triviales en producción: Usar seed=None (que lee de /dev/urandom en Linux o CryptGenRandom en Windows) para obtener aleatoriedad del sistema operativo cuando no se requiere reproducibilidad exacta. 4. No usar np.random para criptografı́a: Ni PCG64 ni MT19937 son generadores criptográficamente seguros (CSPRNG). Para generación de tokens, contraseñas o claves, se debe utilizar el módulo secrets de la biblioteca estándar de Python. En sı́ntesis, el sistema de generación aleatoria de NumPy ha evolucionado de un mecanismo global frágil a una arquitectura modular, paralelizable y estadı́sticamente robusta. Dominar la API moderna (default rng, SeedSequence.spawn) no es opcional para el profesional de la ciencia de datos; es un requisito fundamental para construir simulaciones de Monte Carlo confiables, pipelines de entrenamiento reproducibles y procedimientos de inferencia estadı́stica válidos. 1.5.2 Interoperabilidad con el Ecosistema Python: El Protocolo ndarray como Lingua Franca NumPy no debe ser concebido meramente como una biblioteca aislada de álgebra lineal; es el protocolo de comunicación universal y el sustrato de memoria sobre el cual se ha construido el ecosistema PyData. La hegemonı́a de NumPy no se debe solo a su velocidad, sino a su diseño centrado en la interoperabilidad. A través de la exposición de metadatos de memoria estandarizados, NumPy permite que bibliotecas de dominios dispares (desde estadı́stica tabular hasta aprendizaje profundo distribuido) compartan, manipulen y transformen datos sin incurrir en las catastróficas penalizaciones de rendimiento asociadas a la serialización o a la copia de memoria. Abraham Zamudio 22 Programa de Iniciación Tecnológica
  23. Definición 1.6 La Interfaz array y el Protocolo de Búfer

    de Python La interoperabilidad de NumPy se fundamenta en dos mecanismos de la API de C de Python: 1. La interfaz array : Un método especial de Python que permite a cualquier objeto personalizado definir cómo debe ser convertido en un numpy.ndarray. Cuando una función de NumPy recibe un objeto desconocido, invoca internamente np.asarray(obj), el cual busca e invoca este método. 2. El Protocolo de Búfer de Python (Py buffer): Una API de C de bajo nivel que permite a los objetos de Python exponer sus búferes de memoria cruda (punteros, strides, shape, dtype) a otras extensiones de C. Gracias a este protocolo, NumPy, Pandas y los frameworks de Deep Learning pueden compartir la misma dirección de memoria fı́sica (RAM) sin copiar los datos subyacentes. Pandas: La Abstracción Tabular sobre Bloques de ndarray Aunque Pandas fue diseñado para manejar datos tabulares heterogéneos (donde cada columna puede tener un tipo de dato distinto), su motor de cómputo numérico es, en esencia, un orquestador de arreglos de NumPy. Internamente, un DataFrame no es una matriz 2D gigante. En su arquitectura clásica (el BlockManager), un DataFrame es un diccionario ordenado de bloques, donde cada bloque es un numpy.ndarray de 2 dimensiones que agrupa columnas contiguas del mismo dtype. La estructura Series, por su parte, es conceptualmente un ndarray de 1D envuelto, al cual se le adjunta un objeto Index (que a su vez está respaldado por un ndarray). Cuando se ejecuta una operación vectorizada en Pandas (por ejemplo, df[’col a’] * 2), Pandas extrae el ndarray subyacente, delega la ejecución a las ufuncs de C de NumPy, y envuelve el búfer resultante en una nueva Series. Sin la contigüidad y velocidad del ndarray, las operaciones de filtrado, agrupamiento y transformación de Pandas serı́an computacionalmente inviables a gran escala. Matplotlib y Seaborn: El Pipeline de Renderizado Estadı́stico En el ámbito de la visualización, Matplotlib y Seaborn dependen crı́ticamente de la estructura de memoria de NumPy para su pipeline de renderizado. – Matplotlib: El motor de dibujo (como el backend Agg) requiere que las coordenadas de los datos (puntos, lı́neas, mallas) estén en arreglos 1D o 2D de tipo float64 estrictamente contiguos. Si se le pasa una lista de Python, Matplotlib debe iterar y convertir, generando una sobrecarga masiva. Al pasar un ndarray, el backend accede directamente al puntero de memoria mediante el protocolo de búfer, logrando un renderizado de alto rendimiento. – Seaborn: Aunque es una biblioteca de visualización de alto nivel, su núcleo es estadı́stico. Funciones como kdeplot (estimación de densidad de kernel), regplot (ajuste de modelos lineales) o bootstrapping para intervalos de confianza, invocan silenciosamente rutinas de numpy.linalg, numpy.random y scipy.stats (que a su vez usa NumPy) para realizar los cálculos matemáticos antes de pasar los resultados a Matplotlib. Aprendizaje Profundo (PyTorch / TensorFlow): Tensores y la Ilusión del Zero-Copy La transición entre la ciencia de datos clásica (NumPy) y el aprendizaje profundo (PyTorch, TensorFlow, JAX) es fluida gracias a la compatibilidad de sus estructuras de datos. El ”tensor”de estos frameworks es una generalización del ndarray que incluye soporte para computación en GPU y diferenciación automática (autodiff). Abraham Zamudio 23 Programa de Iniciación Tecnológica
  24. – Zero-Copy en CPU (Host Memory): Gracias al protocolo de

    búfer, convertir un ndarray en un tensor de PyTorch (torch.from numpy()) o de TensorFlow (tf.convert to tensor()) en la CPU es una operación de cero-copia (zero-copy). Ambos objetos apuntan a la misma dirección de memoria RAM fı́sica. Modificar el tensor modifica el arreglo de NumPy instantáneamente, lo cual es vital para pipelines de preprocesamiento de datos donde la memoria es un recurso escaso. – La Barrera del Hardware (Device Memory): Es crucial comprender que el zero-copy solo aplica a la memoria del host (CPU RAM). Si el tensor reside en la memoria del dispositivo (GPU VRAM), la conversión desde un ndarray de CPU requiere una transferencia fı́sica de datos a través del bus del sistema (PCIe o NVLink). Esta operación es costosa y sı́ realiza una copia de memoria, por lo que las mejores prácticas dictan que los tensores de GPU deben crearse y manipularse directamente en el dispositivo, evitando transferencias innecesarias en el bucle de entrenamiento. – El Estándar DLPack: Para superar las limitaciones de la interoperabilidad bilateral, la industria ha adoptado DLPack, un formato de intercambio de tensores de código abierto. DLPack permite el intercambio zero-copy no solo entre NumPy y PyTorch, sino entre cualquier framework compatible (JAX, CuPy, MXNet), preservando los metadatos de dispositivo, strides y dtype de manera universal. import numpy as np import pandas as pd import torch # ============================================================ # 1. INTEROPERABILIDAD CON PANDAS : Extraccion del sustrato NumPy # ============================================================ df = pd. DataFrame ({ ’A’: np. arange (5, dtype =np. float32 ), ’B’: np. arange (5, dtype =np. float32 ) * 2 }) # Extraer el ndarray subyacente de una Series (zero -copy si es un solo bloque ) serie_A = df[’A’] array_subyacente = serie_A . to_numpy () # Metodo recomendado sobre . values # Verificar que comparten memoria (el id del puntero base es el mismo ) print(" Comparten memoria Pandas y NumPy ?", np. shares_memory (serie_A , array_subyacente )) # ============================================================ # 2. ZERO -COPY ENTRE NUMPY Y PYTORCH (CPU) # ============================================================ # Crear un arreglo de NumPy np_array = np. array ([1.0 , 2.0 , 3.0 , 4.0] , dtype =np. float64 ) # Convertir a tensor de PyTorch SIN COPIAR MEMORIA Abraham Zamudio 24 Programa de Iniciación Tecnológica
  25. pt_tensor = torch . from_numpy ( np_array ) # Modificar

    el tensor de PyTorch pt_tensor [0] = 99.0 # El cambio se refleja instantaneamente en el arreglo de NumPy original print(" Arreglo NumPy despues de modificar el tensor :", np_array ) # Salida : [99. 2. 3. 4.] # ============================================================ # 3. EL LIMITE DEL ZERO -COPY: Transferencia a GPU # ============================================================ if torch.cuda. is_available (): # Esto SI copia los datos a la VRAM de la GPU ( cruza el bus PCIe) gpu_tensor = pt_tensor .to(’cuda ’) # Modificar el tensor en GPU NO afecta al ndarray en CPU gpu_tensor [1] = -1.0 print (" Arreglo NumPy (no afectado por cambio en GPU):", np_array ) Listing 6: Demostración de Interoperabilidad, Extracción de Bloques y Zero-Copy En conclusión, la interoperabilidad de NumPy es el pegamento arquitectónico que mantiene unido el ecosistema cientı́fico de Python. Comprender cómo las bibliotecas de alto nivel descomponen sus estructuras en bloques de ndarray, cómo los motores de renderizado consumen búferes contiguos, y cómo los frameworks de Deep Learning comparten la memoria del host mediante el protocolo de búfer, es esencial para diseñar pipelines de datos que sean no solo funcionalmente correctos, sino óptimos en el uso de la memoria y el ancho de banda del sistema. 1.6 Buenas Prácticas y Optimización de Rendimiento 1. Evitar bucles for en Python: Reemplazar iteraciones explı́citas por operaciones vectorizadas o ufuncs. 2. Preasignar memoria: En lugar de usar np.append dentro de un bucle, preasignar un arreglo con np.zeros del tamaño final conocido. 3. Entender Vistas vs. Copias: Ser consciente de que el slicing devuelve vistas. Si se necesita un objeto independiente, usar explı́citamente .copy(). 4. Uso de tipos de datos adecuados: Usar float32 en lugar de float64 cuando la precisión lo permita, reduciendo el uso de memoria a la mitad. 1.7 Conclusión y Perspectivas sobre el Paradigma NumPy A lo largo de este informe técnico, se ha desglosado la anatomı́a, la arquitectura de memoria y las capacidades computacionales de NumPy, revelando que su verdadera magnitud trasciende la de una simple biblioteca de funciones matemáticas. NumPy se ha consolidado como el sustrato fundamental y el protocolo de comunicación universal sobre el cual se erige todo el ecosistema moderno de ciencia de datos, aprendizaje automático y computación cientı́fica en Python. Abraham Zamudio 25 Programa de Iniciación Tecnológica
  26. Arquitectura de Sistemas y el Puente al Silicio Desde una

    perspectiva de ingenierı́a de software, NumPy logra una hazaña excepcional: armonizar la expresividad sintáctica y el tipado dinámico de Python con la brutal eficiencia de los lenguajes compilados. Al imponer la homogeneidad de tipos y la contigüidad de memoria a través del ndarray, la biblioteca permite que el hardware subyacente (CPU, caché L1/L2, instrucciones SIMD) opere en su máximo rendimiento, eludiendo las penalizaciones inherentes al modelo de objetos de CPython y al Global Interpreter Lock (GIL). Conceptos como los strides, el broadcasting mediante pasos de memoria cero y las ufuncs no son meras abstracciones de API, sino mecanismos de bajo nivel que dictan cómo los datos fluyen desde la memoria RAM hacia los registros del procesador, minimizando la latencia y maximizando el throughput. El Ecosistema PyData y la Lingua Franca de la Memoria En el ámbito de la interoperabilidad, NumPy actúa como el pegamento arquitectónico que mantiene unido el ecosistema cientı́fico. Gracias al Protocolo de Búfer de Python y a estándares emergentes como DLPack, NumPy permite el intercambio de datos de zero-copy entre dominios dispares. Desde la abstracción tabular del BlockManager en Pandas, pasando por los contratos matriciales de Scikit-Learn, hasta la transición fluida hacia los tensores de PyTorch y TensorFlow (y su ejecución en aceleradores GPU/TPU), NumPy proporciona el contrato de memoria que hace viables los pipelines de datos a escala industrial, evitando las catastróficas penalizaciones de la serialización y la copia redundante. Rigor Matemático y Estabilidad Numérica en IA Para el profesional dedicado a la matemática aplicada y la inteligencia artificial, el dominio de NumPy es un requisito ineludible. Comprender las implicaciones de la estabilidad numérica (como la elección entre solve y inv, o el uso de eigh para matrices simétricas), saber explotar la descomposición en valores singulares (SVD) para la extracción de caracterı́sticas de bajo rango, y dominar la contracción tensorial mediante la convención de suma de Einstein (einsum), son las habilidades que separan la implementación ingenua de algoritmos de la ingenierı́a de modelos de alto rendimiento. Asimismo, la adopción de la API moderna de generación aleatoria (default rng, SeedSequence) garantiza la reproducibilidad estadı́stica y el paralelismo seguro, pilares de la investigación cientı́fica rigurosa y las simulaciones de Monte Carlo. Sı́ntesis Final En conclusión, NumPy no es solo una herramienta; es un paradigma de computación. Su diseño elegante y su implacable enfoque en el rendimiento a nivel de silicio lo han convertido en una pieza de infraestructura crı́tica e irremplazable. Dominar los fundamentos del ndarray, la manipulación avanzada de memoria y el álgebra lineal numéricamente estable no es simplemente un paso en el aprendizaje de Python; es la adquisición del lenguaje nativo de la ciencia de datos moderna. Para cualquier ingeniero, investigador o cientı́fico que busque empujar los lı́mites de la analı́tica predictiva y el modelado computacional, NumPy es, y seguirá siendo, el cimiento inamovible sobre el cual se construye el futuro de la inteligencia artificial y la computación de alto rendimiento. Abraham Zamudio 26 Programa de Iniciación Tecnológica
  27. 2 Módulo Matplotlib: Fundamentos, Caracterı́sticas y Aplicaciones 2.1 Introducción a

    Matplotlib: Filosofı́a de Diseño, Pipeline de Renderizado y el Estándar Cientı́fico Matplotlib trasciende su definición convencional como una simple biblioteca de trazado para consolidarse como el motor de renderizado gráfico fundamental y el sustrato de visualización de todo el ecosistema PyData. Iniciada por John D. Hunter en 2003 con el objetivo pragmático de emular la sintaxis de trazado de MATLAB en Python, la biblioteca ha experimentado una evolución arquitectónica profunda. Hoy en dı́a, no es solo una herramienta para dibujar gráficos; es un framework de visualización complejo, altamente modular y orientado a objetos, diseñado para gestionar la complejidad de la representación gráfica de datos multidimensionales en entornos de computación cientı́fica de misión crı́tica. El Pipeline de Renderizado y la Abstracción de Backends La genialidad arquitectónica de Matplotlib radica en su estricta separación de responsabilidades entre la representación lógica de la escena y su materialización fı́sica. A diferencia de bibliotecas que acoplan la lógica del gráfico al formato de salida, Matplotlib construye un grafo de escena abstracto compuesto por objetos geométricos y textuales. Este grafo es luego procesado por los Backends de renderizado, que se dividen en dos categorı́as fundamentales: – Backends Rasterizados: El motor principal es Anti-Grain Geometry (Agg), una biblioteca de C++ de alta calidad que utiliza renderizado por subpı́xeles y suavizado de bordes (antialiasing) para generar imágenes de mapa de bits (PNG, JPEG) de calidad fotográfica. – Backends Vectoriales: Para la publicación académica, Matplotlib traduce el grafo de escena abstracto directamente a instrucciones de dibujo nativas en formatos como PDF, SVG, EPS y PGF/TikZ. Esto garantiza que las lı́neas, los marcadores y el texto permanezcan matemáticamente precisos y escalables a cualquier resolución sin pérdida de calidad (artefactos de pixelación). Esta abstracción permite que el mismo código Python genere simultáneamente un gráfico interactivo para una pantalla (vı́a backends de GUI como Qt o Tkinter) y una figura de alta fidelidad para un paper en LaTeX (vı́a el backend PGF), manteniendo la coherencia tipográfica y geométrica absoluta. El “Lenguaje de Ensamblaje” de la Visualización en Python En el panorama actual de la visualización de datos, coexisten bibliotecas de alto nivel como Seaborn, Plotly, Altair o Bokeh. Sin embargo, es crucial comprender la posición jerárquica de Matplotlib: opera en un nivel de abstracción inferior, actuando como el lenguaje de ensamblaje o la API de nivel C del trazado en Python. Mientras que las bibliotecas declarativas o de alto nivel (como Seaborn o Plotly Express) priorizan la ergonomı́a y la generación rápida de gráficos estadı́sticos complejos con una sola lı́nea de código, lo hacen a costa de ceder el control granular. Eventualmente, cuando un usuario de Seaborn necesita ajustar el espaciado exacto de una etiqueta, modificar la opacidad de un borde especı́fico o integrar una anotación matemática compleja, la biblioteca de alto nivel “delega” o “filtra” la petición hacia Matplotlib. Dominar Matplotlib no es opcional para el Abraham Zamudio 27 Programa de Iniciación Tecnológica
  28. cientı́fico de datos senior; es el requisito para romper las

    barreras de las abstracciones de alto nivel y ejercer un control absoluto e intransigente sobre cada pı́xel de la figura. Rigor Cientı́fico, Tipografı́a y Precisión Matemática La adopción de Matplotlib como el estándar de facto en la fı́sica, la ingenierı́a, la econometrı́a y las ciencias biológicas no se debe a su facilidad de uso, sino a su capacidad para satisfacer los requisitos no negociables de la publicación cientı́fica rigurosa: – Tipografı́a y Matemáticas (Mathtext y LaTeX): Matplotlib posee un motor de renderizado TeX interno (mathtext) que permite incrustar ecuaciones complejas directamente en los gráficos. Para una integración tipográfica perfecta con documentos compilados en LaTeX, soporta la invocación del motor LaTeX nativo del sistema (text.usetex = True), asegurando que las fuentes, los kernings y los sı́mbolos matemáticos del gráfico sean idénticos a los del texto circundante. – Control Algorı́tmico de Ejes (Tickers y Formatters): A diferencia de los ejes de escalado automático genérico, Matplotlib expone los algoritmos de localización y formateo de marcas. Esto permite implementar escalas logarı́tmicas personalizadas, ejes de fechas con localizadores especı́ficos (e.g., mostrar solo el primer dı́a de cada mes en series temporales de alta frecuencia), o escalas simétricas logarı́tmicas (symlog) para datos con valores negativos y positivos cercanos a cero. – Motores de Layout Avanzados: Para el diagnóstico de modelos y la presentación de datos multidimensionales, integra motores de diseño basados en restricciones (constrained layout) y cuadrı́culas flexibles (GridSpec), permitiendo la creación de dashboards estáticos con subtramas de proporciones asimétricas, ejes compartidos y anotaciones globales, resolviendo los complejos problemas de superposición de elementos (overplotting). En sı́ntesis, Matplotlib es la infraestructura de visualización de Python. Su diseño, que sacrifica la inmediatez sintáctica en favor de un control arquitectónico total y una precisión matemática absoluta, lo convierte en la herramienta indispensable para la comunicación rigurosa de resultados cientı́ficos. Aprender Matplotlib es aprender a pensar en términos de grafos de escena, backends de renderizado y geometrı́a computacional, elevando la práctica del trazado de datos de una mera ilustración a una disciplina de ingenierı́a de software. 2.2 Fundamentos y Arquitectura 2.2.1 La Jerarquı́a de Objetos (Artist Hierarchy) y el Grafo de Escena La arquitectura de Matplotlib no se limita a ser un simple generador de imágenes; es un motor de renderizado basado en un grafo de escena (scene graph) orientado a objetos. En el núcleo de este diseño se encuentra la clase base abstracta matplotlib.artist.Artist. En Matplotlib, todo lo que es visible en la salida final es un Artist. Un Artist es una entidad que encapsula dos responsabilidades fundamentales: el estado (propiedades geométricas y estéticas como color, grosor de lı́nea, opacidad, y transformaciones) y el comportamiento (la implementación del método draw(renderer), que instruye al backend de renderizado sobre cómo materializar el objeto en pı́xeles o vectores). Los Artists se dividen en dos categorı́as: los primitivos (lı́neas, texto, parches, imágenes) que representan los elementos visuales básicos, y los contenedores (Figuras, Ejes) que agrupan y gestionan a otros Artists. Abraham Zamudio 28 Programa de Iniciación Tecnológica
  29. Figura 2: Este es el pie de foto o leyenda.

    Abraham Zamudio 29 Programa de Iniciación Tecnológica
  30. Definición 2.1 Anatomı́a de la Jerarquı́a de Contenedores La estructura

    fundamental que organiza el grafo de escena en Matplotlib se articula en tres niveles de contención jerárquica estricta: 1. Figure (matplotlib.figure.Figure): Es el nodo raı́z y el contenedor de nivel superior. Representa la ventana completa o la página fı́sica. No solo agrapa subtramas; gestiona el Canvas (el lienzo que interactúa con el backend de eventos/GUI), el Renderer (el motor que dibuja), la resolución en PPP (DPI), y los motores de layout global (constrained layout). Contiene instancias de Axes, tı́tulos globales (suptitle), leyendas de figura y barras de color globales. 2. Axes (matplotlib.axes.Axes): A pesar de su nombre (que es el plural de eje), este objeto es en realidad el gráfico completo o el área de trazado rectangular. Es el contenedor de nivel medio y la interfaz principal para el usuario (el 95 % de las llamadas a la API ocurren aquı́). Un Axes actúa como un sistema de coordenadas local: mapea los datos a un espacio bidimensional, gestiona los lı́mites de los datos (data limits), y contiene todos los Artists primitivos (lı́neas de datos, textos, histogramas). Un Figure puede contener múltiples Axes, pero un Axes solo puede pertenecer a un único Figure. 3. Axis (matplotlib.axis.Axis): Son los objetos de nivel inferior encargados de la algoritmia de los ejes numéricos. Cada Axes contiene dos (o tres) instancias de Axis (eje X, eje Y, eje Z). El Axis no solo dibuja lı́neas; es un motor computacional que determina dónde van las marcas (ticks) mediante Localizadores (Locators), cómo se formatean sus etiquetas mediante Formateadores (Formatters), y gestiona las lı́neas de contorno (Spines) que enmarcan el área de datos. El Pipeline de Transformaciones de Coordenadas La verdadera potencia de esta jerarquı́a radica en cómo los Artists traducen las coordenadas de los datos a pı́xeles en la pantalla. Matplotlib implementa un sistema de transformaciones afines anidadas que permite un control geométrico absoluto. Cuando se grafica un punto (𝑥, 𝑦), este atraviesa una cadena de transformaciones lineales antes de ser renderizado: 1. Transformación de Datos (transData): Convierte las coordenadas crudas de los datos al sistema de coordenadas del Axes, respetando las escalas (lineal, logarı́tmica) y los lı́mites (xlim, ylim). 2. Transformación del Axes (transAxes): Mapea el sistema de datos al espacio relativo del Axes, donde (0, 0) es la esquina inferior izquierda del área de trazado y (1, 1) es la superior, independientemente del tamaño fı́sico en pulgadas. 3. Transformación de la Figura (transFigure): Mapea el Axes dentro de la Figure completa, considerando los márgenes y el espacio entre subtramas. 4. Transformación de Visualización (transDisplay): La conversión final a coordenadas absolutas de pı́xeles (o puntos vectoriales) en el Canvas. Entender este pipeline es crucial para tareas avanzadas, como anclar anotaciones en coordenadas de datos que deben permanecer estáticas al hacer zoom, o dibujar flechas que conectan elementos a través de diferentes Axes en una misma figura. Abraham Zamudio 30 Programa de Iniciación Tecnológica
  31. La Algoritmia del Axis: Localizadores y Formateadores A diferencia de

    bibliotecas que hardcodean las marcas de los ejes, el objeto Axis en Matplotlib delega su lógica a dos componentes altamente modulares: – Locators (matplotlib.ticker): Determinan qué valores numéricos merecen una marca. Por ejemplo, el MaxNLocator garantiza que no se muestren más de 𝑁 marcas para evitar el solapamiento, mientras que el LogLocator calcula las marcas especı́ficas para escalas logarı́tmicas (1, 10, 100...). – Formatters: Determinan cómo se convierte ese valor numérico en una cadena de texto. Permiten desde formateo cientı́fico (ScalarFormatter) hasta la conversión de marcas de tiempo (DateFormatter) o la inyección de cadenas personalizadas (FuncFormatter). En sı́ntesis, la Artist Hierarchy no es solo una estructura de contención de datos; es un marco de trabajo de geometrı́a computacional. Comprender la distinción entre el Axes (el sistema de coordenadas y contenedor de datos) y el Axis (el algoritmo de mapeo numérico), ası́ como dominar el pipeline de transformaciones, es lo que permite al ingeniero de datos trascender las limitaciones de las funciones de trazado de alto nivel y construir visualizaciones cientı́ficas de precisión quirúrgica. 2.2.2 Paradigmas de Interacción: La Máquina de Estados de pyplot vs. la Arquitectura Orientada a Objetos Matplotlib presenta una dualidad arquitectónica en su API pública, una decisión de diseño histórica que busca equilibrar la accesibilidad para usuarios provenientes de MATLAB con las exigencias de la ingenierı́a de software moderna. Comprender la diferencia fundamental entre estos dos paradigmas no es una mera cuestión de preferencia sintáctica, sino un requisito crı́tico para garantizar la robustez, la testabilidad y la gestión eficiente de la memoria en aplicaciones de producción. La Interfaz pyplot: Una Máquina de Estados Global Implı́cita El módulo matplotlib.pyplot no es el núcleo de la biblioteca, sino una capa de abstracción (un wrapper) que implementa una máquina de estados global. Su propósito original era facilitar la exploración interactiva de datos en entornos REPL (como la consola de IPython o Jupyter Notebooks), permitiendo al usuario graficar sin preocuparse por la instanciación explı́cita de objetos. Bajo el capó, pyplot mantiene un registro global (gestionado internamente por la clase matplotlib. pylab helpers.Gcf) de todas las figuras abiertas. Cuando el usuario invoca una función como plt.plot() o plt.title(), pyplot realiza implı́citamente las siguientes operaciones: 1. Consulta el registro global para obtener la Figura activa actual (gcf() - get current figure). Si no existe, la crea y la registra. 2. Consulta la Figura para obtener el Eje activo actual (gca() - get current axes). Si no existe, lo crea y lo añade a la Figura. 3. Delega la llamada al método correspondiente del objeto Axes subyacente (e.g., current ax.plot()). El Peligro en Entornos de Producción: Aunque ergonómica, esta dependencia del estado global introduce vulnerabilidades severas en scripts modulares o servidores web: Abraham Zamudio 31 Programa de Iniciación Tecnológica
  32. – Efectos Secundarios y Acoplamiento: Si una función auxiliar llama

    a plt.plot(), altera el estado global. El llamador no tiene control explı́cito sobre dónde se está dibujando, lo que rompe la transparencia referencial y hace que el código sea extremadamente difı́cil de depurar y testear unitariamente. – Fugas de Memoria (Memory Leaks): Las figuras creadas vı́a pyplot quedan retenidas en el registro global (Gcf). Incluso si las variables locales de Python que las referencian salen de ámbito y son recolectadas por el Garbage Collector, la figura permanece viva en la memoria RAM hasta que se invoca explı́citamente plt.close(). En un servidor que genera miles de gráficos, esto agota la memoria del sistema rápidamente. – Inseguridad en Multihilo: En aplicaciones concurrentes (e.g., un servidor Flask/Django generando reportes en PDF), múltiples hilos compartiendo el estado global de pyplot provocarán condiciones de carrera (race conditions), donde un hilo podrı́a terminar dibujando datos en la figura de otro hilo. La Interfaz Orientada a Objetos (OO): Control Explı́cito y Encapsulación La API Orientada a Objetos es el acceso directo y sin intermediarios a la Jerarquı́a de Artistas (definida en la subsección anterior). En este paradigma, el desarrollador asume la responsabilidad de instanciar y gestionar explı́citamente los objetos Figure y Axes, y el despacho de métodos se realiza directamente sobre estas instancias (e.g., ax.plot(), fig.savefig()). Este enfoque alinea a Matplotlib con el principio del Zen de Python: .Explı́cito es mejor que implı́cito”. Sus ventajas arquitectónicas son ineludibles para el desarrollo profesional: – Encapsulación y Modularidad: Los objetos Figure y Axes pueden ser creados, pasados como argumentos a funciones, y retornados sin alterar ningún estado global. Esto permite diseñar funciones puras de visualización (e.g., def plot residuals(ax, data):) que son fácilmente testables y reutilizables. – Gestión Determinista de la Memoria: Al no registrar las figuras en el estado global de pyplot, el ciclo de vida del objeto Figure está ligado estrictamente a las referencias de Python. Cuando la variable local sale de ámbito, el Garbage Collector libera la memoria inmediatamente, sin necesidad de llamadas manuales a close(). – Soporte para Layouts Complejos: La construcción de dashboards complejos mediante GridSpec o Figure.add subplots() es exclusiva y nativa de la API OO. pyplot carece de la capacidad de expresar estas topologı́as de manera elegante. La Instanciación Pura (Pure OO) para Entornos Headless Para un control absoluto, especialmente en servidores de renderizado headless (sin interfaz gráfica) o hilos de trabajo, se debe evitar incluso plt.subplots(), ya que este método todavı́a interactúa con el gestor de figuras de pyplot. La práctica de élite consiste en instanciar las clases directamente desde sus módulos raı́z: import matplotlib . pyplot as plt from matplotlib . figure import Figure import numpy as np # ============================================================ # ANTI - PATRON : Interfaz pyplot ( Estado Global Implicito ) # ============================================================ Abraham Zamudio 32 Programa de Iniciación Tecnológica
  33. def mala_practica_plotting ( datos ): # Crea una figura y

    la registra en el estado global de pyplot plt. figure () plt.plot( datos ) plt.title (" Grafico Global ") # Si esta funcion se llama en un hilo , o dentro de otro contexto , # el estado global se corrompe . Ademas , genera fuga de memoria . plt. savefig (" output .png") plt.close () # Obligatorio para evitar memory leaks # ============================================================ # MEJOR PRACTICA : Interfaz OO ( Encapsulacion y Modularidad ) # ============================================================ def buena_practica_plotting (ax , datos ): """ Funcion pura: recibe un Axes explicito , no altera estado global .""" ax.plot(datos , color =’blue ’, linewidth =1.5) ax. set_title (" Grafico Encapsulado ", fontsize =12) ax.grid(True , linestyle =’--’, alpha =0.7) # Instanciacion explicita y gestion determinista de memoria fig = Figure ( figsize =(8 , 6) , dpi =100) # No se registra en pyplot ax = fig. add_subplot (111) # Control explicito del layout datos = np. random . randn (100) . cumsum () buena_practica_plotting (ax , datos ) # Inyeccion de dependencias # Renderizado directo usando el backend Agg from matplotlib . backends . backend_agg import FigureCanvasAgg canvas = FigureCanvasAgg (fig) canvas . print_png (" output_pure_oo .png") # Al terminar la funcion , ’fig ’ y ’ax ’ son recolectados automaticamente . Listing 7: Comparativa de Paradigmas: Estado Global Implı́cito vs. Ingenierı́a Orientada a Objetos En conclusión, mientras que pyplot es una herramienta legı́tima para el análisis exploratorio rápido en la terminal, la Interfaz Orientada a Objetos es el único paradigma aceptable para la ingenierı́a de software de producción. Dominar la API OO implica dejar de pensar en çomandos de dibujo empezar a pensar en la manipulación explı́cita de un grafo de escena, garantizando ası́ código modular, thread-safe, y libre de fugas de memoria. 2 2.3 Caracterı́sticas Principales y Personalización 2.3.1 Diseño de Layouts Complejos con GridSpec Para la visualización cientı́fica avanzada, como el análisis de residuos o la comparación de múltiples distribuciones, la función básica plt.subplots() resulta insuficiente. El módulo matplotlib.gridspec permite definir cuadrı́culas de subtramas con relaciones de tamaño asimétricas y spanning (ocupación de múltiples celdas). Abraham Zamudio 33 Programa de Iniciación Tecnológica
  34. import matplotlib . pyplot as plt import matplotlib . gridspec

    as gridspec # 1. Modularidad : Funcion que retorna Figure y Axes para trazabilidad def crear_layout_diagnostico (): fig = plt. figure ( figsize =(10 , 8)) # Grid de 2x2 , donde la fila superior es mas alta que la inferior gs = gridspec . GridSpec (2, 2, height_ratios =[3 , 1], hspace =0.3 , wspace =0.3) # Asignacion explicita de ejes ax_residuals = fig. add_subplot (gs[0, :]) fila superior ax_qq = fig. add_subplot (gs[1, 0]) izquierda ax_hist = fig. add_subplot (gs[1, 1]) # Ocupa toda la # Inferior # Inferior derecha return fig , ( ax_residuals , ax_qq , ax_hist ) fig , (ax_res , ax_qq , ax_hist ) = crear_layout_diagnostico () # ... (aqui se graficarian los residuos , Q-Q plot e histograma ) plt.show () Listing 8: Diseño de layout complejo para análisis de residuos con GridSpec 2.3.2 Renderizado de Texto y Ecuaciones Matemáticas Matplotlib posee un motor de procesamiento de texto TeX interno (mathtext) que permite renderizar ecuaciones matemáticas complejas utilizando una sintaxis similar a LaTeX, encerrada entre sı́mbolos de dólar ($...$). Para una fidelidad tipográfica absoluta con el documento LaTeX circundante, se puede activar el renderizado con el motor LaTeX del sistema mediante plt.rcParams[’text.usetex’] = True. Esto es fundamental al graficar funciones como los polinomios de Hermite 𝐻𝑛 (𝑥) o funciones de densidad de probabilidad. 2.4 Aplicaciones en Visualización Cientı́fica y Estadı́stica 2.4.1 Análisis de Series Temporales El manejo de datos temporales requiere un control preciso sobre los ejes. Matplotlib integra el módulo matplotlib.dates, que proporciona localizadores (locators) y formateadores (formatters) especı́ficos para manejar objetos datetime. – mdates.DateFormatter(’ %Y- %m’): Para mostrar solo año y mes en el eje X. – fig.autofmt xdate(): Rota automáticamente las etiquetas del eje X para evitar superposiciones, una práctica esencial en series temporales de alta frecuencia. 2.4.2 Diagnósticos de Modelos y Análisis de Residuos En el contexto de la estadı́stica computacional, la validación de modelos exige un examen visual de los residuos. Matplotlib permite replicar los diagnósticos de bibliotecas como Abraham Zamudio 34 Programa de Iniciación Tecnológica
  35. statsmodels con total personalización: – Residuos vs. Ajustados: Un gráfico

    de dispersión (ax.scatter) para detectar heterocedasticidad o patrones no lineales no capturados. – Histograma de Residuos con KDE: Superposición de un histograma (ax.hist) con una estimación de densidad de kernel para evaluar visualmente la desviación de la normalidad. – Gráfico Q-Q (Cuantil-Cuantil): Trazado de los cuantiles muestrales contra los cuantiles teóricos de una distribución normal, utilizando ax.plot con marcadores especı́ficos para identificar colas pesadas o asimetrı́a. 2.4.3 Representación de Funciones Matemáticas Complejas Para la enseñanza y el análisis de funciones especiales (e.g., polinomios ortogonales), Matplotlib permite la superposición de múltiples escalas y ejes. Por ejemplo, utilizando 2 ax.twinx() se puede graficar la función de peso 𝑤(𝑥) = 𝑒 −𝑥 en un eje Y secundario, mientras se visualizan los polinomios de Hermite 𝐻𝑛 (𝑥) en el eje Y primario, facilitando la comprensión de su ortogonalidad en el intervalo (−∞, ∞). 2.5 Buenas Prácticas de Desarrollo Para garantizar la reproducibilidad, modularidad y eficiencia en proyectos de ciencia de datos, se deben observar las siguientes directrices: 1. Preferir la API Orientada a Objetos: Evitar el uso de plt.gca() o plt.gcf() en código de producción. Siempre pasar explı́citamente los objetos ax a las funciones de graficado. 2. Gestión de Estilos y Reproducibilidad: Utilizar plt.style.use(’nombre estilo’) o definir diccionarios de rcParams personalizados al inicio del script para mantener coherencia visual corporativa. 3. Eficiencia en Actualizaciones: En lugar de llamar a ax.clear() y volver a graficar en un bucle, se debe crear los objetos Artist una sola vez y actualizar sus datos mediante el método set data(), seguido de fig.canvas.draw idle(). 4. Limpieza de Memoria: En entornos de ejecución prolongada, es crucial llamar a plt.close(fig) después de guardar la figura, para evitar fugas de memoria (memory leaks). 5. Uso de constrained layout: Activar fig.set constrained layout(True) al crear la figura para evitar que etiquetas de ejes, tı́tulos o anotaciones se superpongan o queden cortadas al guardar el archivo. 2.6 Conclusión sobre Matplotlib Matplotlib trasciende su rol de simple biblioteca de graficado para consolidarse como el estándar de facto para la visualización cientı́fica programática en Python. Su arquitectura basada en la jerarquı́a de Artists y su potente interfaz orientada a objetos ofrecen un nivel de control sin igual, indispensable para la construcción de layouts complejos, el diagnóstico riguroso de modelos estadı́sticos y la representación precisa de funciones matemáticas. Dominar Matplotlib implica comprender cómo estructurar visualmente la información para que sea Abraham Zamudio 35 Programa de Iniciación Tecnológica
  36. matemáticamente precisa, computacionalmente eficiente y estéticamente coherente con los más

    altos estándares de publicación académica y profesional. 3 Módulo Pandas: Fundamentos, Caracterı́sticas y Aplicaciones 3.1 Introducción al Paradigma de Datos Tabulares y Heterogéneos Mientras que NumPy establece el estándar de oro para el cómputo numérico sobre tensores densos y homogéneos, su modelo de datos intrı́nsecamente rı́gido colapsa ante la naturaleza de los datos del mundo real: colecciones intrı́nsecamente heterogéneas, esparsas, relacionales y semánticamente etiquetadas. Pandas (acrónimo de Panel Data) surge en 2008, desarrollado por Wes McKinney en el contexto de la gestión de fondos cuantitativos (AQR Capital Management), para resolver un problema de dominio crı́tico: la necesidad de manipular series temporales desfasadas, fusionar fuentes de datos dispares y manejar valores faltantes de manera determinista, todo ello manteniendo un rendimiento analı́tico de alto nivel. Pandas no es simplemente una capa de conveniencia sobre NumPy; es un motor analı́tico en memoria (in-memory analytical engine) que implementa los principios del modelo relacional de bases de datos, adaptados a la ergonomı́a y el ecosistema de Python. Reducir Pandas a la noción coloquial de ”tablas con etiquetas.es omitir su rigor matemático y computacional. Sus estructuras fundamentales son, en esencia, implementaciones de mapeos discretos optimizados para la alineación automática y la integridad referencial. Definición 3.1 Anatomı́a de las Estructuras de Datos en Pandas El ecosistema de datos de Pandas se cimienta sobre dos abstracciones duales: – La Series: Matemáticamente, una Series es un arreglo 1D de valores homogéneos acoplado a un Index (un arreglo 1D de etiquetas). Más que una simple lista, una Series puede conceptualizarse como una función discreta 𝑓 : 𝐼 → 𝑉, donde 𝐼 es el dominio (el ı́ndice, que puede ser temporal, categórico o jerárquico) y 𝑉 es el rango (los valores, respaldados por un ndarray de NumPy). Esta dualidad permite que las Series se comporten simultáneamente como arreglos (acceso posicional) y como diccionarios ordenados (acceso por clave), con complejidad de búsqueda 𝑂 (1) para el último gracias a las tablas hash subyacentes. – El DataFrame: Es una estructura tabular 2D que consiste en un contenedor ordenado de Series que comparten un ı́ndice común (las filas). En la terminologı́a de bases de datos relacionales, el DataFrame representa una relación o tabla, donde las columnas son atributos (variables aleatorias o caracterı́sticas) y las filas son tuplas (observaciones o instancias). La heterogeneidad de tipos se maneja a nivel de columna, permitiendo que cada atributo mantenga su propio dtype subyacente, mientras que el ı́ndice actúa como la clave primaria que garantiza la integridad y alineación de los datos. Pandas como Motor de Álgebra Relacional y ETL La verdadera potencia de Pandas radica en su implementación del álgebra relacional y su capacidad para orquestar pipelines de ETL (Extract, Transform, Load) y Feature Engineering. A diferencia de las bases de datos relacionales tradicionales que operan sobre disco mediante lenguajes declarativos (SQL), Pandas Abraham Zamudio 36 Programa de Iniciación Tecnológica
  37. ejecuta estas operaciones en memoria RAM mediante una API programática

    y vectorizada. Operaciones fundamentales como la proyección (selección de columnas), la selección (filtrado de filas mediante máscaras booleanas), y las uniones (merge/join basados en claves primarias y foráneas) están optimizadas mediante algoritmos de hash join y merge sort implementados en Cython. Esto permite la integración y limpieza de datasets masivos, la imputación de valores faltantes mediante interpolación estadı́stica, y la generación de nuevas variables derivadas, cerrando la brecha crı́tica entre la ingesta de datos crudos y la alimentación de modelos de aprendizaje automático. El Dominio de las Series Temporales y Datos de Alta Frecuencia Finalmente, el ADN de Pandas está indeleblemente marcado por su origen en las finanzas cuantitativas, lo que lo dota del motor de series temporales más robusto del ecosistema de Python. El manejo nativo de ı́ndices temporales (DatetimeIndex, PeriodIndex), la aritmética de fechas, el re-muestreo (resampling) para cambios de frecuencia, y el cálculo de ventanas móviles (rolling windows) permiten modelar fenómenos estocásticos, calcular rendimientos financieros, y analizar señales de alta frecuencia con una precisión y elegancia sintáctica que serı́an prohibitivas de implementar desde cero utilizando arreglos nativos. En sı́ntesis, Pandas es el puente indispensable que transforma la materia prima de los datos caóticos en información estructurada, lista para el modelado estadı́stico y la inferencia computacional. 3.2 Arquitectura Interna: Del BlockManager a la Revolución de PyArrow y Copy-on-Write Para comprender las limitaciones de rendimiento y las capacidades analı́ticas de Pandas, es imperativo diseccionar su arquitectura de gestión de memoria. El diseño interno de Pandas ha experimentado una transformación radical con el lanzamiento de la versión 2.0, marcando el fin de una era de compromisos heredados de NumPy y el inicio de un nuevo paradigma basado en especificaciones de memoria columnar modernas. Abraham Zamudio 37 Programa de Iniciación Tecnológica
  38. Definición 3.2 El BlockManager Clásico: Fragmentación y la Tragedia de

    la Promoción de Tipos En las versiones clásicas de Pandas (hasta la 1.5.x), un DataFrame no almacenaba sus datos en una matriz 2D única. Internamente, utilizaba el BlockManager, una estructura de datos compleja que agrupaba las columnas lógicas del DataFrame en bloques (blocks) fı́sicos basándose estrictamente en su dtype. Cada Block consistı́a en un ndarray de NumPy de 2 dimensiones y un arreglo de placement (un mapeo de ı́ndices que indicaba qué columnas lógicas correspondı́an a qué columnas fı́sicas del bloque). – Agrupación por Tipo: Todas las columnas float64 se concatenaban en un único bloque 2D; las int64 en otro, y las object (Python nativo) en un tercero. Esto permitı́a aprovechar los bucles vectorizados de C de NumPy para operaciones intra-bloque. – Fragmentación de Memoria y Copias Defensivas: Al insertar una nueva columna de un dtype distinto, se creaba un nuevo bloque. Más grave aún, debido a la semántica de referencias de NumPy, modificar un subconjunto de un bloque multi-columna requerı́a realizar una copia defensiva completa del bloque en memoria para evitar efectos secundarios no deseados en otras vistas del DataFrame. Esto provocaba picos catastróficos de uso de RAM y la infame advertencia SettingWithCopyWarning. – La Tragedia del NaN y la Promoción de Tipos: NumPy no posee un tipo nativo para representar valores nulos (NA) en enteros o booleanos. Para insertar un np.nan en una columna de enteros, Pandas se veı́a forzado a promocionar (convertir) silenciosamente todo el bloque de int64 a float64. Esto no solo duplicaba el consumo de memoria, sino que introducı́a pérdida de precisión para enteros mayores a 253 (debido a la mantisa de 52 bits del estándar IEEE 754 de doble precisión). El Paradigma de Pandas 2.0: Apache Arrow y el Backend Columnar A partir de la versión 2.0, Pandas rompió su acoplamiento exclusivo con NumPy introduciendo un backend nativo basado en Apache Arrow (activado mediante dtype backend=’pyarrow’). Apache Arrow no es solo un formato de archivo en disco; es una especificación de memoria columnar in-memory estandarizada a nivel de la industria. Este cambio arquitectónico resuelve las limitaciones históricas de Pandas a nivel de silicio: – Memoria Estrictamente Columnar y Contigua: A diferencia de los bloques 2D de NumPy (que están en orden C, es decir, contiguos por filas), Arrow almacena cada columna como un arreglo 1D estrictamente contiguo en memoria. Cuando se realiza una agregación o filtrado por columna (la operación más común en DataFrames), la CPU puede aprovechar al máximo la localidad espacial, precargando las lı́neas de caché (L1/L2) de manera óptima y habilitando instrucciones SIMD sobre los datos sin saltos de memoria. – Nulos Nativos mediante Máscaras de Validez (Bitmasks): Arrow elimina la necesidad de la promoción de tipos. Para permitir nulos en cualquier tipo de dato, Arrow añade un búfer de validez (validity bitmap) a cada columna. Es un arreglo de bits donde un 1 indica un valor válido y un 0 indica un nulo. Esto permite tener columnas int64[pyarrow] o bool[pyarrow] con valores faltantes, consumiendo apenas 1 bit adicional por fila, preservando la integridad matemática y el rendimiento de C. – Tipos de Datos de Ancho Variable: Arrow introduce búferes de offsets que permiten manejar Abraham Zamudio 38 Programa de Iniciación Tecnológica
  39. Figura 3: La percepción común sobre la estructura de un

    pandas.DataFrame en la memoria es que contiene una pequeña cantidad de metadatos, y el resto de las columnas se almacenan como numpy.ndarray individuales, como se muestra a continuación. Abraham Zamudio 39 Programa de Iniciación Tecnológica
  40. Figura 4: Sin embargo, la estructura de memoria real de

    un DataFrame es un poco diferente (ver la figura a continuación). Esto se debe a que la estructura de datos no es simplemente un diccionario de arrays. En cambio, un pandas.DataFrame es una combinación de un ı́ndice (que no se muestra en la imagen) y los datos almacenados en bloques, según se gestiona por el BlockManager. Wes McKinney lo introdujo en julio de 2011 como parte de una actualización general sobre el desarrollo de pandas, detallando la motivación y estructura del BlockManager. Para otras visualizaciones explicativas sobre la estructura interna de un DataFrame, consulte la presentación Pandas Under The Hood: Peeking behind the scenes of a high performance data analys de Jeffrey Tratner.. Abraham Zamudio 40 Programa de Iniciación Tecnológica
  41. cadenas de texto (string[pyarrow]) y listas anidadas de manera eficiente

    en memoria, eliminando por completo la dependencia del lento y fragmentado tipo object de Python para el manejo de texto. Copy-on-Write (CoW): La Nueva Semántica de Referencias El otro pilar fundamental de la arquitectura de Pandas 2.0 es la adopción global de la semántica Copy-on-Write (CoW). Bajo este nuevo modelo, todas las operaciones que devuelven un DataFrame o Series (como el slicing, rename, o selección de columnas) se comportan estrictamente como vistas (views) que no copian datos. Sin embargo, si el usuario intenta modificar una de estas vistas, Pandas intercepta la operación a nivel de C, realiza una copia profunda (deep copy) únicamente de los búferes de memoria que están a punto de ser alterados, y aı́sla el estado. Esto elimina por completo las copias defensivas del antiguo BlockManager, reduce el uso de memoria en órdenes de magnitud durante operaciones de filtrado y transformación, y garantiza que el comportamiento del código sea predecible, determinista y libre de efectos secundarios ocultos. Interoperabilidad Zero-Copy y el Ecosistema del DataFrame Al adoptar la especificación de memoria de Arrow, Pandas deja de ser una isla. Gracias a la Interfaz C de Datos de Arrow (Arrow C Data Interface), los búferes de memoria subyacentes pueden exponerse directamente a otros motores analı́ticos. Esto permite el intercambio de DataFrames entre Pandas, Polars, DuckDB, Ibis y motores de bases de datos modernas en tiempo 𝑂 (1) y con cero copias de memoria (zero-copy). Pandas 2.0 trasciende ası́ su rol de biblioteca aislada para convertirse en el nodo de interoperabilidad central de un ecosistema analı́tico unificado. import pandas as pd import numpy as np # 1. ACTIVACION DEL BACKEND PYARROW # Al leer datos , especificamos el backend para usar memoria Arrow df = pd. DataFrame ({ ’id’: [1, 2, None , 4], # Se infiere como int64 [ pyarrow ] con nulo nativo ’score ’: [9.5 , 8.0 , 7.5 , None], # Se infiere como double [ pyarrow ] ’category ’: [’A’, ’B’, ’A’, None] # Se infiere como string [ pyarrow ] }, dtype_backend =’pyarrow ’) print(" Dtypes nativos de Arrow (sin promocion a float64 por NaN):") print(df. dtypes ) # 2. SEMANTICA COPY -ON - WRITE (CoW) # Habilitar CoW (por defecto en Pandas 3.0 , explicito en 2.x) pd. options .mode. copy_on_write = True df_slice = df [[ ’id ’, ’score ’]] # Esto es una VISTA , no hay copia en memoria print(f" Comparten memoria ? {np. shares_memory (df[’id ’], df_slice [’id ’])}") # True Abraham Zamudio 41 Programa de Iniciación Tecnológica
  42. # Si modificamos la vista , CoW intercepta y copia

    SOLO la columna ’id ’ df_slice .loc [:, ’id ’] = df_slice [’id ’]. fillna (0) # El DataFrame original ’df ’ permanece inalterado , garantizando inmutabilidad logica Listing 9: Demostración del Backend PyArrow y Semántica Copy-on-Write 3.3 El Objeto Index: Inmutabilidad, Hashing y Jerarquı́a El componente que distingue a una Series o un DataFrame de un simple arreglo de NumPy es el Index. Lejos de ser una simple lista de etiquetas, el Index es una estructura de datos altamente optimizada y fundamental para el rendimiento de las operaciones. – Inmutabilidad y Seguridad: Los ı́ndices son inmutables. Esto no es una limitación arbitraria, sino un requisito de seguridad: si un ı́ndice pudiera mutar, las referencias hash internas se corromperı́an, rompiendo la alineación de datos. Esta inmutabilidad permite que los ı́ndices sean compartidos de forma segura entre múltiples Series o DataFrames (memory sharing). – Tablas Hash para Búsqueda 𝑂 (1): Internamente, Pandas construye una tabla hash (dict) que mapea las etiquetas del ı́ndice a sus posiciones enteras. Esto permite que operaciones como df.loc[’etiqueta’] o la alineación automática de ı́ndices se ejecuten en tiempo 𝑂 (1), en lugar de requerir búsquedas lineales 𝑂 (𝑁). – MultiIndex (Indexación Jerárquica): Permite representar datos de 𝑁 dimensiones en una estructura 2D. Mediante tuplas de etiquetas, el MultiIndex habilita operaciones de agrupamiento, rebanado y transformación complejas sin necesidad de realizar costosas operaciones de pivot o melt. 3.4 Semántica de Operaciones: Alineación, Álgebra Relacional y SplitApply-Combine 3.4.1 Alineación Automática y el Manejo de Datos Faltantes A diferencia de NumPy, que depende del broadcasting posicional, Pandas alinea los datos por sus etiquetas de ı́ndice antes de realizar cualquier operación binaria. Si se suman dos Series con ı́ndices parcialmente superpuestos, Pandas empareja los valores por sus etiquetas y rellena las diferencias con NaN. Esta caracterı́stica es extraordinariamente poderosa para el análisis de series temporales desfasadas o conjuntos de datos incompletos, eliminando la necesidad de bucles de validación manuales. 3.4.2 Álgebra Relacional: merge, join y concat Pandas implementa las operaciones fundamentales del álgebra relacional de bases de datos. – merge: Equivale al JOIN de SQL. Permite combinar DataFrames basándose en columnas clave o ı́ndices, soportando inner, left, right y outer joins. Internamente, utiliza algoritmos de ordenamiento y mezcla (merge sort) o tablas hash para lograr un rendimiento óptimo. – concat: Realiza la concatenación axial (apilamiento vertical u horizontal). Es la operación subyacente para la unión de múltiples lotes de datos. Abraham Zamudio 42 Programa de Iniciación Tecnológica
  43. 3.4.3 El Paradigma Split-Apply-Combine (groupby) La operación de agrupamiento es

    el núcleo del análisis estadı́stico agregado. Pandas implementa el paradigma Split-Apply-Combine de manera vectorizada y altamente optimizada. Teorema 3.1 El Paradigma Split-Apply-Combine Dado un DataFrame 𝐷 y una clave de agrupamiento 𝐾, la operación groupby ejecuta el siguiente pipeline determinista: 1. Split (Dividir): Particiona 𝐷 en subconjuntos disjuntos 𝐷 𝑖 basándose en los valores únicos de 𝐾. 2. Apply (Aplicar): Ejecuta una función de agregación, transformación o filtrado 𝑓 sobre cada subconjunto 𝐷 𝑖 de manera independiente. 3. Combine (Combinar): Concatena los resultados 𝑓 (𝐷 𝑖 ) en una nueva estructura de salida, preservando o reindexando la jerarquı́a de 𝐾. Este enfoque permite expresar transformaciones complejas (como la estandarización dentro de cada grupo o el cálculo de ventanas móviles por categorı́a) en una sola lı́nea de código declarativa, delegando la iteración a rutinas de Cython/C. 3.5 Análisis de Series Temporales y Datos de Alta Frecuencia Pandas fue concebido en el ámbito de las finanzas cuantitativas, y su motor de series temporales es, posiblemente, el más robusto en el ecosistema de Python. Proporciona herramientas nativas para manejar la complejidad de los datos indexados por tiempo. – DatetimeIndex y PeriodIndex: Permiten indexar datos con resoluciones que van desde nanosegundos hasta años. El DatetimeIndex representa puntos en el tiempo (timestamps), mientras que PeriodIndex representa intervalos o duraciones (e.g., .el tercer trimestre de 2024”). – Re-muestreo (resample): Permite cambiar la frecuencia de los datos temporales (e.g., convertir datos de alta frecuencia de 1 minuto a datos diarios). Soporta operaciones de agregación (suma, media) y de interpolación, manejando automáticamente los huecos temporales y las zonas horarias (timezones). – Ventanas Móviles (rolling, expanding): Facilita el cálculo de estadı́sticas en ventanas deslizantes (e.g., medias móviles de 20 dı́as, volatilidad histórica), operaciones crı́ticas en el procesamiento de señales y el análisis financiero. 3.6 Aplicaciones en Ingenierı́a de Datos y Buenas Prácticas de Rendimiento Para garantizar que los pipelines de datos construidos con Pandas sean escalables y eficientes en memoria, es imperativo seguir prácticas de ingenierı́a de software rigurosas: 1. Uso de Tipos Categorical: Para columnas con baja cardinalidad (e.g., dı́as de la semana, códigos de paı́s, estados booleanos), el tipo category almacena los datos como enteros Abraham Zamudio 43 Programa de Iniciación Tecnológica
  44. subyacentes con un diccionario de mapeo. Esto puede reducir el

    uso de memoria en un 90 % y acelerar drásticamente las operaciones groupby. 2. Evitar el Bucle apply: El método apply es una herramienta de conveniencia que itera fila por fila o columna por columna en Python puro, destruyendo la vectorización. Siempre que sea posible, se debe reemplazar por operaciones vectorizadas nativas, métodos de acceso .str o .dt, o compiladores JIT como Numba. 3. Procesamiento Out-of-Core (Chunking): Cuando el tamaño del dataset excede la memoria RAM disponible, Pandas permite leer archivos en fragmentos (chunksize en read csv). Combinado con el formato de archivo Parquet (que permite lectura selectiva de columnas y particiones), Pandas puede procesar terabytes de datos de manera distribuida o secuencial. 4. Adopción del Backend PyArrow: Migrar a pd.DataFrame(data, dtype backend=’pyarrow’) no solo mejora el rendimiento, sino que elimina los comportamientos erráticos de los NaN flotantes, proporcionando un tipado estricto y predecible. import pandas as pd import numpy as np # 1. LECTURA EFICIENTE Y TIPOS CATEGORICOS # Leer un CSV definiendo tipos para optimizar memoria desde el inicio dtypes = { ’user_id ’: ’int32 ’, ’country_code ’: ’category ’, # Reduce drasticamente la memoria ’transaction_amount ’: ’float32 ’ } df = pd. read_csv (’transactions .csv ’, dtype =dtypes , parse_dates =[ ’ timestamp ’]) # 2. TRANSFORMACION VECTORIAL DE SERIES TEMPORALES # Establecer el indice temporal y ordenar df = df. set_index (’timestamp ’). sort_index () # Calcular la media movil de 7 dias ( ventana movil ) y la volatilidad df[’amount_rolling_mean ’] = df[’transaction_amount ’]. rolling (’7D’). mean () df[’amount_rolling_std ’] = df[’transaction_amount ’]. rolling (’7D’).std () # 3. SPLIT -APPLY - COMBINE AVANZADO ( Transformacion ) # En lugar de agregar , calculamos el porcentaje respecto a la media del grupo # usando . transform () para mantener el shape original del DataFrame group_means = df. groupby (’country_code ’)[’transaction_amount ’]. transform (’mean ’) df[’amount_vs_country_mean ’] = df[’transaction_amount ’] / group_means # 4. RE - MUESTREO TEMPORAL ( Resampling ) # Cambiar la frecuencia de los datos a nivel diario , calculando el volumen total daily_summary = df. resample (’1D’).agg ({ Abraham Zamudio 44 Programa de Iniciación Tecnológica
  45. ’transaction_amount ’: ’sum ’, ’user_id ’: ’nunique ’ # Contar

    usuarios unicos por dia }) # 5. INTEROPERABILIDAD Y EXPORTACION ( Formato Parquet ) # Parquet preserva los tipos categoricos , las fechas y permite compresion Snappy # Ademas , es nativamente compatible con el ecosistema Arrow daily_summary . to_parquet ( ’daily_summary . parquet ’, engine =’pyarrow ’, compression =’snappy ’ ) Listing 10: Pipeline Avanzado en Pandas: PyArrow, Categoricals y Time-Series 3.7 Conclusión sobre Pandas: Sı́ntesis del Paradigma Analı́tico Moderno A lo largo de este informe técnico, se ha desglosado la anatomı́a, la evolución arquitectónica y las capacidades computacionales del módulo Pandas, revelando que su verdadera magnitud trasciende con creces la noción simplista de ser una mera herramienta para manipular ”hojas de cálculo en Python”. Pandas se ha consolidado como el motor analı́tico en memoria (in-memory analytical engine) de facto, actuando como el puente indispensable entre la ingesta de datos crudos, caóticos y heterogéneos, y la alimentación de modelos estadı́sticos y de aprendizaje automático rigurosos. Desde una perspectiva de ingenierı́a de software y arquitectura de sistemas, la evolución de Pandas representa un caso de estudio magistral en la maduración de un ecosistema de código abierto. La transición desde el legado BlockManager —con sus inherentes problemas de fragmentación de memoria, copias defensivas y la catastrófica promoción de tipos para manejar valores nulos— hacia el paradigma moderno basado en Apache Arrow y la semántica Copy-onWrite (CoW), marca un hito fundamental. Este cambio no es simplemente una actualización de sintaxis, sino una reingenierı́a a nivel de silicio que alinea a Pandas con las mejores prácticas de la computación de alto rendimiento: localidad espacial de caché, máscaras de validez (bitmasks) para nulos nativos sin pérdida de precisión, e interoperabilidad zero-copy con el ecosistema moderno de datos (Polars, DuckDB, motores de bases de datos columnares). En el ámbito matemático y algorı́tmico, Pandas impone un rigor que NumPy no puede ofrecer por diseño. La introducción del objeto Index transforma las operaciones de datos de un mapeo posicional ciego a una alineación semántica basada en claves. Esta caracterı́stica, respaldada por tablas hash para búsquedas de complejidad 𝑂 (1), garantiza la integridad referencial durante operaciones binarias, fusiones (joins) y re-muestreos temporales, eliminando silenciosamente la posibilidad de errores de desalineación que son comunes en lenguajes de scripting menos estrictos. Asimismo, la formalización del paradigma Split-Apply-Combine permite expresar transformaciones estadı́sticas agregadas de alta complejidad con una elegancia declarativa, delegando la iteración pesada a rutinas optimizadas en Cython/C. Sin embargo, el poder de Pandas conlleva una responsabilidad técnica ineludible. Como se ha detallado en las buenas prácticas, la facilidad de uso de la biblioteca puede convertirse en una trampa de rendimiento si se ignora su modelo de ejecución. El uso indiscriminado del método apply, la negligencia en la definición de tipos de datos (ignorando las optimizaciones Abraham Zamudio 45 Programa de Iniciación Tecnológica
  46. de category o int32), o la falta de comprensión sobre

    cómo las vistas y las copias interactúan en la memoria, pueden degradar un pipeline de datos de milisegundos a minutos, o provocar fallos por agotamiento de memoria (OOM) en datasets de mediana escala. Por ello, el dominio de Pandas no se mide por la capacidad de encadenar métodos, sino por la comprensión profunda de cómo cada operación afecta la estructura de memoria subyacente y el flujo de datos. En sı́ntesis, Pandas es mucho más que una biblioteca; es un paradigma computacional completo. Su capacidad para abstraer la complejidad del álgebra relacional, el manejo de series temporales de alta frecuencia y la gestión eficiente de la memoria lo convierte en una herramienta insustituible. Para el cientı́fico de datos, el ingeniero de machine learning y el matemático aplicado, dominar Pandas significa poseer la capacidad de domar la entropı́a de los datos del mundo real, transformándolos en estructuras deterministas, escalables y matemáticamente sound, listas para la inferencia computacional de vanguardia. Su continua evolución hacia estándares abiertos como Arrow asegura que seguirá siendo la piedra angular del análisis de datos en Python en la década venidera. Abraham Zamudio 46 Programa de Iniciación Tecnológica