La lección anterior terminó señalando al culpable: las listas de Python no saben de matemáticas. [12.50, 9.95] * 2 no duplica los precios — repite la lista. Para sumar el IVA a 487 importes necesitas un bucle o una comprensión, y para un millón de importes ese bucle se nota. NumPy (Numerical Python) resuelve ambas cosas con una sola estructura, el ndarray: un bloque compacto de números del mismo tipo sobre el que las operaciones se escriben sin bucle y se ejecutan en C. Es la pieza más abajo del ecosistema PyData — pandas, Matplotlib y scikit-learn están construidos sobre ella — así que entender NumPy es entender qué hacen los demás por dentro.
Contenido
- Por qué las listas no bastan
- El
ndarray:np.array,dtype,shape,ndim - Vectorización: operar sin bucles
- Crear arrays:
zeros,ones,arange,linspace - Indexación, slicing y la idea estrella: boolean masking
- Agregaciones:
sum,mean,max,argmax,std - Arrays 2D y el parámetro
axis - Broadcasting básico
- Lista vs array: cuándo cada uno
Por qué las listas no bastan
Instala y prueba (dentro del venv, como siempre):
precios = [12.50, 9.95, 15.90, 21.00] # Odisea, Hamlet, Quijote, Fausto
print(precios * 2) # ¿duplicar precios? No: repetir la lista
# [12.5, 9.95, 15.9, 21.0, 12.5, 9.95, 15.9, 21.0]
# print(precios * 1.04) # TypeError: can't multiply sequence by non-intLa lista es un contenedor genérico: puede mezclar floats, cadenas y dataclasses Libro, y por eso cada elemento es un objeto Python completo con su sobrecoste. Dos consecuencias:
- Expresividad: las operaciones matemáticas elemento a elemento no existen; hay que escribir el bucle (o la comprensión de M2) cada vez.
- Rendimiento: el bucle de Python interpreta cada iteración; con muchos datos, la diferencia frente a código compilado es de uno a dos órdenes de magnitud.
El ndarray
import numpy as np # el alias 'np' es convención universal
precios = np.array([12.50, 9.95, 15.90, 21.00])
print(precios) # [12.5 9.95 15.9 21. ]
print(type(precios)) # <class 'numpy.ndarray'>
print(precios.dtype) # float64 -> TODOS los elementos son de este tipo
print(precios.shape) # (4,) -> tupla con el tamaño de cada dimensión
print(precios.ndim) # 1 -> número de dimensionesTres atributos que consultarás constantemente:
| Atributo | Qué dice | Ejemplo |
|---|---|---|
dtype |
Tipo único de todos los elementos (float64, int64, bool...) |
float64 |
shape |
Tamaño por dimensión, como tupla | (4,) vector, (4, 6) matriz |
ndim |
Número de dimensiones | 1 vector, 2 tabla |
El precio de esa homogeneidad: si mezclas tipos, NumPy convierte todo al más general (np.array([1, 2.5]) → float64; si hay una cadena, todo acaba siendo texto y adiós matemáticas). Esa restricción es justo lo que le permite ser compacto y rápido.
Vectorización: operar sin bucles
La operación se aplica elemento a elemento, sin que escribas el bucle. Compara las tres formas de añadir el IVA del 4% a los precios:
precios_lista = [12.50, 9.95, 15.90, 21.00]
IVA_LIBROS = 0.04
# 1) Bucle clásico (M2)
con_iva = []
for p in precios_lista:
con_iva.append(round(p * (1 + IVA_LIBROS), 2))
# 2) Comprensión (M2)
con_iva = [round(p * (1 + IVA_LIBROS), 2) for p in precios_lista]
# 3) NumPy: la operación ES la expresión
precios = np.array(precios_lista)
con_iva = np.round(precios * (1 + IVA_LIBROS), 2)
print(con_iva) # [13. 10.35 16.54 21.84]Las tres dan lo mismo; la tercera no solo es más corta, es más rápida (el bucle ocurre en C) y se lee como matemáticas: "precios por 1.04". Lo mismo con dos arrays:
unidades = np.array([154, 121, 182, 63]) # vendidas ene-jun (11-01)
ingresos = precios * unidades # elemento a elemento
print(ingresos) # [1925. 1203.95 2893.8 1323. ]
print(ingresos.sum()) # 7345.75 -> facturación total a PVPY la tarifa de socio del curso entero (IVA del 4 % y descuento del 5 %, la fórmula de M3), ahora en una línea:
IVA_LIBROS, DESCUENTO_SOCIO = 0.04, 0.05
precios_socio = np.round(precios * (1 + IVA_LIBROS) * (1 - DESCUENTO_SOCIO), 2)
print(precios_socio) # [12.35 9.83 15.71 20.75] <- los de M3, clavadosCrear arrays
| Función | Qué crea | Ejemplo |
|---|---|---|
np.zeros(n) |
n ceros (acumuladores) | np.zeros(7) → contador por día de semana |
np.ones(n) |
n unos | np.ones(4) |
np.arange(a, b, paso) |
Como range, pero array (admite floats) |
np.arange(1, 7) → meses [1 2 3 4 5 6] |
np.linspace(a, b, n) |
n puntos equiespaciados incluyendo ambos extremos | np.linspace(0, 1, 5) → [0. 0.25 0.5 0.75 1.] |
meses = np.arange(1, 7)
print(meses) # [1 2 3 4 5 6]
print(np.zeros(3)) # [0. 0. 0.]
print(np.linspace(9.95, 21.0, 3)) # [ 9.95 15.475 21. ]Indexación, slicing y boolean masking
Índices y rebanadas funcionan como en las listas (M4):
importes = np.array([12.50, 19.90, 15.71, 21.00, 31.80, 47.70, 25.00])
print(importes[0]) # 12.5
print(importes[-1]) # 25.0
print(importes[1:4]) # [19.9 15.71 21. ]Y ahora la idea estrella del ecosistema entero, la que reaparecerá idéntica en pandas: comparar un array produce un array de booleanos, y ese array sirve de filtro.
grandes = importes > 15.00
print(grandes) # [False True True True True True True]
print(importes[grandes]) # [19.9 15.71 21. 31.8 47.7 25. ]
# En una línea, como se escribe en la práctica:
print(importes[importes > 15.00])
# Combinando condiciones: & (y), | (o), ~ (no) — con paréntesis obligatorios
print(importes[(importes > 15) & (importes < 30)]) # [19.9 15.71 21. 25. ]Léelo despacio: importes > 15.00 no es un if, es una operación vectorizada que devuelve (7,) booleanos; usarlo entre corchetes selecciona los elementos donde hay True. Es el filter/comprensión-con-if de M2 convertido en álgebra. Un pariente útil: np.where(condición, si_true, si_false) aplica un "if vectorizado" (np.where(es_socio, precio * 0.95, precio)).
Agregaciones
print(importes.sum()) # 173.61
print(importes.mean()) # 24.80142857142857
print(importes.max()) # 47.7
print(importes.argmax()) # 5 -> el ÍNDICE del máximo, no el valor
print(importes.std()) # 11.29... desviación típica: cuánto se dispersanargmax merece atención: devuelve dónde está el máximo, lo que permite responder "¿qué título factura más?" cruzando arrays paralelos:
titulos = np.array(["La Odisea", "Hamlet", "El Quijote", "Fausto"])
print(titulos[ingresos.argmax()]) # El Quijote (2893.80 €)Y combinando máscara + agregación, preguntas enteras en una línea: importes[importes > 15].mean() — "importe medio de las ventas grandes".
Arrays 2D y el parámetro axis
Las ventas por título y por mes forman una tabla: 4 filas (títulos) × 6 columnas (enero-junio). Conceptualmente, las unidades de ventas_2026.csv reorganizadas:
# filas: Odisea, Hamlet, Quijote, Fausto | columnas: ene..jun
unidades_2d = np.array([
[18, 17, 21, 50, 25, 23], # La Odisea
[15, 13, 17, 39, 19, 18], # Hamlet
[22, 20, 25, 59, 29, 27], # El Quijote
[ 7, 8, 8, 20, 11, 9], # Fausto
])
print(unidades_2d.shape) # (4, 6)
print(unidades_2d.ndim) # 2
print(unidades_2d[2, 3]) # 59 -> El Quijote en abril (fila 2, columna 3)
print(unidades_2d[:, 3]) # [50 39 59 20] -> TODOS los títulos en abrilaxis indica a lo largo de qué dimensión se agrega — la dimensión que desaparece:
print(unidades_2d.sum()) # 520 -> total del semestre
print(unidades_2d.sum(axis=0)) # [ 62 58 71 168 84 77] -> por MES (colapsa filas)
print(unidades_2d.sum(axis=1)) # [154 121 182 63] -> por TÍTULO (colapsa columnas)Ahí está, en números, el gancho de M10: abril (168) más que duplica a cualquier otro mes. Sant Jordi existe y los arrays lo ven. La regla mnemotécnica: axis=0 agrega "hacia abajo" (resultado por columna), axis=1 "hacia la derecha" (resultado por fila).
Broadcasting básico
Broadcasting es la regla por la que NumPy opera arrays de formas distintas "estirando" el pequeño. Dos casos cubren el 90% del uso:
# 1) Escalar contra array: el escalar se aplica a todo (ya lo usaste: precios * 1.04)
print(unidades_2d * 2)
# 2) Vector contra matriz: el vector (6,) se aplica a CADA fila de la (4, 6)
peso_mes = np.array([1.0, 1.0, 1.0, 0.5, 1.0, 1.0]) # abril "descontado" al comparar
print((unidades_2d * peso_mes).sum(axis=1))
# [129. 101.5 152.5 53. ] -> ventas "desestacionalizadas" por títuloLa regla formal (las formas se comparan de derecha a izquierda y deben coincidir o ser 1) da para mucho más, pero con escalar-contra-array y vector-contra-matriz tienes lo necesario para este curso.
Lista vs array: cuándo cada uno
| Criterio | list |
np.ndarray |
|---|---|---|
Tipos mezclados (Libro, cadenas, floats) |
Sí — su gran virtud | No: un solo dtype |
Crecer elemento a elemento (append) |
Barata y natural | Cara (recrea el array): construye la lista y convierte al final |
| Matemáticas elemento a elemento | Bucle/comprensión manual | Una expresión, ejecutada en C |
| Filtrar por condición | Comprensión con if |
Máscara booleana |
| Miles/millones de números | Lenta y pesada en memoria | Su hábitat natural |
| El catálogo de Papyrus (objetos ricos) | list[Libro] de M5, correcto |
No aplica |
La honestidad prometida: en el día a día de Papyrus no escribirás casi NumPy puro — usarás pandas, que le pone nombres a filas y columnas y lee CSV solo. Pero pandas es NumPy por debajo: cada columna de un DataFrame es un array, las máscaras booleanas de esta lección se escriben idénticas allí, y axis, dtype y las agregaciones reaparecen con los mismos nombres. Lo aprendido aquí no se tira: se hereda.
Errores Comunes y Consejos
and/oren máscaras.(a > 1) and (a < 5)lanzaValueError: The truth value of an array is ambiguous. Con arrays se usa&,|,~— y siempre con paréntesis, porque&tiene más precedencia que>.- Olvidar que el slice es una vista.
b = a[1:4]; b[0] = 99modifica tambiéna(a diferencia de las listas de M4, cuyo slicing copia). Si necesitas independencia:a[1:4].copy(). dtypesilenciosamente entero.np.array([12, 9, 15])esint64; una división posterior funciona (produce floats), pero asignara[0] = 12.5truncaría a12. Si trabajas con precios, parte de floats:np.array([12.0, ...]).- Confundir
maxyargmax. Uno devuelve el valor, el otro el índice. Para "¿qué título vende más?" necesitasargmax+ el array de títulos. - Hacer
appenden bucle a un array.np.appendrecrea el array entero cada vez. Acumula en unalisty haznp.array(lista)al final — o mejor, deja quepandas.read_csvlo haga todo en 11-03.
Ejercicios
- Con
precios = np.array([12.50, 9.95, 15.90, 21.00])yunidades = np.array([154, 121, 182, 63]): calcula en una expresión cada uno los ingresos por título a tarifa de socio (IVA 4 % + descuento 5 %, redondeo a 2 decimales del precio antes de multiplicar) y el título con menos ingresos a PVP. - Con
importes = np.array([12.50, 19.90, 15.71, 21.00, 31.80, 47.70, 25.00, 9.95, 12.35]): (a) ¿cuántas ventas superan la media? (b) ¿qué porcentaje del importe total aportan? Pistas: una máscara puede sumarse (Truevale 1) y usarse dos veces. - Con la matriz
unidades_2dde la lección: obtén la media mensual de cada título excluyendo abril (para no dejar que Sant Jordi infle el "mes normal"), usando slicing o una máscara sobre las columnas. ¿Qué título tiene el mes normal más alto?
Soluciones
-
Cuidado con el matiz del enunciado: se redondea la tarifa de socio (12.35, 9.83...) y luego se multiplica — como cobra Papyrus — no al revés.ingresos_socio = np.round(precios * 1.04 * 0.95, 2) * unidades print(ingresos_socio) # [1901.9 1189.43 2859.22 1307.25] titulos = np.array(["La Odisea", "Hamlet", "El Quijote", "Fausto"]) print(titulos[(precios * unidades).argmin()]) # Hamlet (1203.95 € a PVP) -
Tres ventas de nueve aportan más de la mitad del importe: otra vez la asimetría de 11-01.media = importes.mean() # 21.77... mascara = importes > media print(mascara.sum()) # 3 ventas (31.80, 47.70, 25.00) print(importes[mascara].sum() / importes.sum() * 100) # 53.2...%mascara.sum()cuentaTrues porque los booleanos son 0/1 — un idioma NumPy que verás constantemente. -
sin_abril = np.delete(unidades_2d, 3, axis=1) # o unidades_2d[:, [0,1,2,4,5]] medias = sin_abril.mean(axis=1) print(np.round(medias, 1)) # [20.8 16.4 24.6 8.6] titulos = np.array(["La Odisea", "Hamlet", "El Quijote", "Fausto"]) print(titulos[medias.argmax()]) # El Quijoteaxis=1porque quieres colapsar los meses (columnas) y quedarte con un valor por título (fila). El Quijote lidera también el mes "normal": su liderazgo no es solo cosa de Sant Jordi.
Conclusión
NumPy te ha dado el motor: el ndarray homogéneo y compacto, la vectorización que convierte bucles en expresiones (precios * 1.04), las máscaras booleanas que convierten filtros en álgebra (importes[importes > 15]), las agregaciones con axis que hicieron visible el pico de Sant Jordi (abril: 168 unidades), y el broadcasting que estira escalares y vectores sin copiarlos. También te ha enseñado su límite para Papyrus: los arrays son números anónimos — la fila 2 "es" El Quijote solo porque tú lo recuerdas, y meter las 487 filas del CSV en arrays paralelos a mano es trabajo de picapedrero. La próxima lección presenta pandas, que envuelve estos arrays en tablas con nombres: columnas etiquetadas, read_csv que carga ventas_2026.csv en una línea (el csv.DictReader de M6 con esteroides, como prometimos), y groupby para responder por fin, con datos y de una vez, las preguntas de Ana.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
