NumPy básico: arrays y operaciones numéricas en Python
- ¿Qué es NumPy y por qué existe?
- El ndarray: la estructura central
- Crear arrays: las funciones esenciales
- Operaciones vectorizadas: adiós a los bucles
- Indexing y slicing en arrays multidimensionales
- Reshape, flatten y combinación de arrays
- Estadística y reducción por ejes
- Broadcasting: la magia de las formas compatibles
- Preguntas frecuentes
Si Python es el idioma, NumPy es la gramática matemática que lo hace potente. Antes de NumPy, hacer cálculos numéricos serios en Python era lento y verboso. Después de NumPy, operaciones que en C requerían decenas de líneas se expresan en una sola, y se ejecutan a velocidad compilada. Sin entender NumPy no hay Pandas, no hay scikit-learn, no hay TensorFlow. Todo el ecosistema científico de Python está construido sobre esta librería.
¿Qué es NumPy y por qué existe?
NumPy (Numerical Python) es la librería fundamental para el cómputo numérico en Python. Apareció en 2005 para resolver un problema concreto: Python es un lenguaje interpretado y dinámico, lo que lo hace flexible y legible pero lento para operaciones numéricas masivas. NumPy resuelve esto introduciendo el ndarray, un array multidimensional homogéneo implementado en C que permite operar sobre millones de datos sin un solo bucle Python.
El impacto de NumPy en el ecosistema Python es difícil de exagerar. Pandas usa ndarrays internamente para sus Series y DataFrames. scikit-learn espera ndarrays como entrada y salida. TensorFlow y PyTorch tienen interfaces compatibles con el formato NumPy. Matplotlib acepta arrays NumPy directamente. Aprender NumPy no es aprender una librería más: es aprender el formato universal del dato numérico en Python.
pip install numpy
import numpy as np # la convención universal: siempre np
print(np.__version__) # 1.26.x o 2.x
El ndarray: la estructura central
El ndarray (N-dimensional array) es la piedra angular de NumPy. A diferencia de las listas Python, un ndarray almacena todos sus elementos en un bloque contiguo de memoria, todos del mismo tipo de dato. Esto elimina el overhead de la gestión dinámica de Python y permite que las operaciones se ejecuten en C o Fortran optimizados.
import numpy as np
# Crear un array
arr = np.array([10, 20, 30, 40, 50])
# Atributos fundamentales
print(arr.shape) # (5,) → forma: 5 elementos en 1 dimensión
print(arr.ndim) # 1 → número de dimensiones
print(arr.dtype) # int64 → tipo de cada elemento
print(arr.size) # 5 → número total de elementos
print(arr.nbytes) # 40 → bytes en memoria (5 × 8 bytes por int64)
# Array 2D (matriz)
matriz = np.array([[1, 2, 3],
[4, 5, 6]])
print(matriz.shape) # (2, 3) → 2 filas, 3 columnas
print(matriz.ndim) # 2
Tipos de dato (dtype)
# NumPy infiere el dtype automáticamente
np.array([1, 2, 3]) # int64 (enteros)
np.array([1.0, 2.5, 3.7]) # float64 (flotantes)
np.array([True, False, True]) # bool
# También puedes forzarlo
np.array([1, 2, 3], dtype=np.float32) # 4 bytes por elemento (ahorra RAM)
np.array([1, 2, 3], dtype=np.int32) # 4 bytes por elemento
# Convertir el tipo de un array existente
arr = np.array([1, 2, 3])
arr_float = arr.astype(float) # crea nuevo array float64
float32 en lugar de float64 puede reducir el uso de RAM a la mitad sin pérdida perceptible de precisión en la mayoría de casos. Para entrenamiento de redes neuronales, float32 es el estándar.
Crear arrays: las funciones esenciales
import numpy as np
# ── Desde datos ──────────────────────────────────────────────────
np.array([1, 2, 3, 4]) # desde lista Python
np.array([[1, 2], [3, 4]]) # 2D desde lista de listas
# ── Arrays predefinidos ──────────────────────────────────────────
np.zeros((3, 4)) # matriz 3×4 de ceros (float64)
np.ones((2, 3)) # matriz 2×3 de unos
np.full((3, 3), 7) # matriz 3×3 con valor 7
np.eye(4) # matriz identidad 4×4
np.empty((2, 2)) # sin inicializar (rápido, valores aleatorios)
# ── Rangos y secuencias ──────────────────────────────────────────
np.arange(0, 10, 2) # [0, 2, 4, 6, 8] (inicio, fin_excl, paso)
np.arange(5) # [0, 1, 2, 3, 4]
np.linspace(0, 1, 5) # [0.0, 0.25, 0.5, 0.75, 1.0] (N puntos equiespaciados)
np.linspace(0, 2*np.pi, 100) # 100 puntos para una función trigonométrica
# ── Números aleatorios ───────────────────────────────────────────
np.random.seed(42) # fijar semilla para reproducibilidad
np.random.rand(3, 3) # uniformes en [0, 1)
np.random.randn(3, 3) # normales con media 0 y desviación 1
np.random.randint(0, 10, (4,)) # enteros aleatorios en [0, 10)
np.random.choice([10, 20, 30, 40], size=5) # muestreo con reemplazo
La distinción entre arange y linspace importa más de lo que parece: arange especifica el paso y no garantiza el número de puntos; linspace especifica el número de puntos y garantiza que incluye el extremo final. Para generar valores de función continua (graficar un seno, calcular una integral), usa siempre linspace.
Operaciones vectorizadas: adiós a los bucles
La diferencia más importante entre NumPy y las listas Python es la vectorización: cuando escribes arr * 2, NumPy no itera elemento a elemento en Python. En cambio, delega la operación completa a una función en C que procesa el bloque de memoria de una vez. El resultado puede ser 10, 100 o 1000 veces más rápido, según el tamaño del array.
a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])
# Operaciones elemento a elemento (no hace falta bucle)
a + b # [11, 22, 33, 44, 55]
a * b # [10, 40, 90, 160, 250]
a ** 2 # [1, 4, 9, 16, 25]
a / b # [0.1, 0.1, 0.1, 0.1, 0.1]
a % 2 # [1, 0, 1, 0, 1]
# Con escalares (broadcasting implícito)
a * 2 # [2, 4, 6, 8, 10]
a + 100 # [101, 102, 103, 104, 105]
a > 3 # [False, False, False, True, True] ← array booleano
Funciones universales (ufuncs)
arr = np.array([1.0, 4.0, 9.0, 16.0])
np.sqrt(arr) # [1.0, 2.0, 3.0, 4.0]
np.log(arr) # logaritmo natural
np.exp(arr) # e^x
np.abs(arr) # valor absoluto
np.round(arr, 2) # redondear a 2 decimales
# Trigonometría
angulos = np.linspace(0, 2 * np.pi, 5)
np.sin(angulos)
np.cos(angulos)
# Comparar rendimiento (timeit mental):
# Lista Python: [x**2 for x in range(1_000_000)] → ~200ms
# NumPy: np.arange(1_000_000)**2 → ~2ms
@ o np.dot(). La multiplicación con * en NumPy es siempre elemento a elemento: A @ B es el producto matricial estándar.
tablero[4, 4] (índice base 0). Cada pieza tiene un dtype: puede ser un entero que codifica el tipo, un string, un objeto. NumPy te da las herramientas para representar, indexar y transformar cualquier estructura matricial con precisión quirúrgica. Fuente: Pexels (licencia libre).Indexing y slicing en arrays multidimensionales
El sistema de indexing de NumPy es más potente que el de las listas Python. Para arrays 1D funciona igual; para 2D y más, la sintaxis arr[fila, columna] es más expresiva y eficiente que la doble indexación de listas.
m = np.array([[10, 20, 30, 40],
[50, 60, 70, 80],
[90,100,110,120]])
# ── Elemento individual ──────────────────────────────────────────
m[0, 1] # 20 (fila 0, columna 1)
m[1, 2] # 70
m[-1, -1] # 120 (última fila, última columna)
# ── Fila / columna completa ──────────────────────────────────────
m[1, :] # [50, 60, 70, 80] → fila 1 entera
m[:, 2] # [30, 70, 110] → columna 2 entera
# ── Submatriz (slicing) ──────────────────────────────────────────
m[0:2, 1:3] # [[20, 30], [60, 70]] → filas 0-1, cols 1-2
m[::2, :] # [[10,20,30,40],[90,100,110,120]] → filas pares
# ── Indexado booleano (filtrado) ─────────────────────────────────
m[m > 50] # [60, 70, 80, 90, 100, 110, 120]
m[m % 20 == 0] # elementos divisibles por 20
# ── Fancy indexing ───────────────────────────────────────────────
indices = [0, 2]
m[indices, :] # filas 0 y 2 → [[10,20,30,40],[90,100,110,120]]
# ── Asignar con indexado ─────────────────────────────────────────
m[m < 50] = 0 # poner a 0 todos los elementos menores de 50
sub = m[0:2, :] devuelve una vista: modificar sub modifica m. Si necesitas independencia, usa m[0:2, :].copy(). El fancy indexing (m[[0,2], :]) siempre devuelve copia.
Reshape, flatten y combinación de arrays
reshape cambia la forma de un array sin cambiar sus datos. Es una operación que aparece constantemente en Machine Learning, donde los modelos esperan entradas con formas muy específicas.
arr = np.arange(12) # [0, 1, 2, ..., 11]
# Cambiar forma
arr.reshape(3, 4) # 3 filas × 4 columnas
arr.reshape(2, 6) # 2 filas × 6 columnas
arr.reshape(4, 3) # 4 filas × 3 columnas
arr.reshape(2, 2, 3) # 3D: 2 bloques de 2×3
# Usar -1 para dejar que NumPy calcule una dimensión
arr.reshape(3, -1) # 3 filas, columnas = 12/3 = 4
arr.reshape(-1, 4) # columnas=4, filas = 12/4 = 3
arr.reshape(-1) # equivalente a flatten
# De 2D a 1D
m = np.array([[1,2,3],[4,5,6]])
m.flatten() # [1,2,3,4,5,6] → siempre copia
m.ravel() # [1,2,3,4,5,6] → vista cuando sea posible (más eficiente)
# Transponer
m.T # [[1,4],[2,5],[3,6]] → intercambia ejes
# Combinar arrays
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.concatenate([a, b]) # [1,2,3,4,5,6]
np.vstack([m, m]) # apilar verticalmente (más filas)
np.hstack([m, m]) # apilar horizontalmente (más columnas)
# Dividir
np.split(a, 3) # [array([1]), array([2]), array([3])]
np.hsplit(m, 3) # dividir m en 3 columnas
Estadística y reducción por ejes
Las funciones estadísticas de NumPy pueden operar sobre el array completo (devuelven un escalar) o a lo largo de un eje específico (devuelven un array de menor dimensión). El concepto de eje es esencial: axis=0 opera sobre las filas (reduce a una sola fila), axis=1 opera sobre las columnas (reduce a una sola columna).
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# ── Reducción total (escalar) ────────────────────────────────────
np.sum(m) # 45
np.mean(m) # 5.0
np.std(m) # 2.58
np.min(m) # 1
np.max(m) # 9
np.median(m) # 5.0
# ── Reducción por eje ────────────────────────────────────────────
np.sum(m, axis=0) # [12, 15, 18] → suma de cada columna (colapsa filas)
np.sum(m, axis=1) # [6, 15, 24] → suma de cada fila (colapsa columnas)
np.mean(m, axis=0) # [4.0, 5.0, 6.0] → media de cada columna
# ── Índices ──────────────────────────────────────────────────────
np.argmax(m) # 8 → índice del máximo en el array aplanado
np.argmin(m, axis=1) # [0, 0, 0] → índice del mínimo en cada fila
np.argsort(m, axis=1) # índices que ordenarían cada fila
# ── Acumuladas ───────────────────────────────────────────────────
arr = np.array([1, 2, 3, 4])
np.cumsum(arr) # [1, 3, 6, 10]
np.diff(arr) # [1, 1, 1] → diferencias entre consecutivos
np.cumprod(arr) # [1, 2, 6, 24]
Broadcasting: la magia de las formas compatibles
El broadcasting es el mecanismo que permite a NumPy realizar operaciones entre arrays de formas distintas, siempre que esas formas sean compatibles según un conjunto de reglas. En la práctica, evita tener que duplicar datos solo para que las dimensiones coincidan.
# Caso 1: array + escalar (broadcasting trivial)
arr = np.array([1, 2, 3])
arr + 10 # [11, 12, 13] → el 10 se "expande" a [10, 10, 10]
# Caso 2: matriz + vector fila
m = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
v = np.array([10, 20, 30]) # shape (3,) → se trata como (1,3)
m + v
# [[11, 22, 33],
# [14, 25, 36]]
# Caso 3: columna + fila → tabla de combinaciones
col = np.array([[1], [2], [3]]) # shape (3, 1)
fil = np.array([10, 20, 30]) # shape (3,) → (1, 3)
col + fil
# [[11, 21, 31],
# [12, 22, 32],
# [13, 23, 33]]
# Error típico: formas incompatibles
a = np.array([1, 2, 3]) # (3,)
b = np.array([1, 2]) # (2,)
# a + b → ValueError: operands could not be broadcast
La regla de broadcasting de NumPy: compara las formas de derecha a izquierda. Dos dimensiones son compatibles si son iguales, o si una de ellas es 1. Si tienen distinto número de dimensiones, se completa con 1s por la izquierda. Si no hay ningún 1 para expandir, se lanza un ValueError.
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre NumPy básico: arrays y operaciones numéricas en Python
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre NumPy básico: arrays y operaciones numéricas en Python? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!