NumPy: pensar en arreglos

Tema 3 · Fase 1, Programar con datos · Toda la ruta

Los 19 bloques de código de la teoría, en el mismo orden. Cópialos y córrelos en tu Python o en Colab; en la ficha Teoría está la explicación de cada uno.

De la lista al arreglo

import numpy as np

lista = [1, 2, 3, 4]
arreglo = np.array([1, 2, 3, 4])

print(lista * 2)     # la lista repetida dos veces
print(arreglo * 2)   # cada número multiplicado por 2
print(arreglo + 10)
print(arreglo ** 2)
materias = ["Matemática", "Física", "Lenguaje", "Biología"]
# Notas sobre 100: una fila por estudiante, una columna por materia
notas = np.array([
    [72, 65, 80, 58],
    [45, 50, 62, 70],
    [88, 92, 75, 81],
    [60, 48, 55, 66],
    [95, 85, 90, 88],
    [38, 52, 47, 61],
])

print("shape:", notas.shape)   # (filas, columnas)
print("ndim: ", notas.ndim)    # cuántas dimensiones tiene
print("size: ", notas.size)    # cuántos números hay en total
print("dtype:", notas.dtype)   # de qué tipo es cada número
print("En décimos:", (notas / 10).dtype)
print(np.zeros((2, 3)))          # una tabla de ceros de 2 × 3
print(np.ones(4))                # cuatro unos
print(np.arange(0, 10, 2))       # como range, pero da un arreglo
print(np.linspace(0, 1, 5))      # 5 números parejos de 0 a 1

# Números al azar, con semilla fija para que salgan siempre iguales
rng = np.random.default_rng(42)
print(rng.integers(1, 7, size=10))          # diez dados
print(rng.normal(0, 1, size=(2, 3)).round(2))

Indexar, rebanar y filtrar

print(notas[0])          # la fila 0: el primer estudiante
print(notas[0, 2])       # fila 0, columna 2: su nota de Lenguaje
print(notas[-1])         # la última fila
print(notas[:, 0])       # todas las filas, columna 0: Matemática
print(notas[1:3, :2])    # filas 1 y 2, columnas 0 y 1
print(notas[::2, -1])    # una fila sí y otra no, última columna
aprobado = notas >= 51
print(aprobado[:2])                  # las dos primeras filas
print("Notas reprobadas:", notas[notas < 51])
print("Cuántas notas aprobadas:", aprobado.sum())
# Las notas de Matemática de quienes aprobaron Física
print(notas[notas[:, 1] >= 51, 0])

Los ejes

M = np.array([[2, 5, 1, 4],
              [3, 0, 6, 2],
              [1, 4, 2, 7]])

print("Sin eje:  ", M.sum())
print("axis=0:   ", M.sum(axis=0), "forma", M.sum(axis=0).shape)
print("axis=1:   ", M.sum(axis=1), "forma", M.sum(axis=1).shape)
import matplotlib.pyplot as plt

filas, columnas = M.shape
fig, ax = plt.subplots()
for i in range(filas):
    for j in range(columnas):
        # La fila i se dibuja más abajo cuanto mayor es i
        ax.add_patch(plt.Rectangle((j, -i), 1, 1, fill=False,
                                   edgecolor="C0", linewidth=1.5))
        ax.text(j + 0.5, -i + 0.5, M[i, j], ha="center", va="center")
for j, s in enumerate(M.sum(axis=0)):
    ax.text(j + 0.5, -filas + 0.1, s, ha="center", va="center",
            color="C2")
for i, s in enumerate(M.sum(axis=1)):
    ax.text(columnas + 0.6, -i + 0.5, s, ha="center", va="center",
            color="C2")
# Las flechas de los ejes
flecha = dict(arrowstyle="-|>", color="C0", linewidth=2)
ax.annotate("", xy=(-0.4, -filas + 1), xytext=(-0.4, 1),
            arrowprops=flecha)
ax.annotate("", xy=(columnas, 1.4), xytext=(0, 1.4), arrowprops=flecha)
ax.text(-0.7, -0.5, "eje 0", rotation=90, ha="center", va="center")
ax.text(columnas / 2, 1.75, "eje 1", ha="center")
ax.text(columnas / 2, -filas - 0.5, "sum(axis=0): forma (4,)",
        ha="center", color="C2")
ax.text(columnas + 1.2, -filas / 2 + 1, "sum(axis=1): forma (3,)",
        rotation=-90, ha="center", va="center", color="C2")
ax.set_xlim(-1, columnas + 1.5)
ax.set_ylim(-filas - 0.9, 2.1)
ax.set_aspect("equal")
ax.axis("off")
plt.show()
prom_materia = notas.mean(axis=0)     # un promedio por materia
prom_estudiante = notas.mean(axis=1)  # un promedio por estudiante

for materia, p in zip(materias, prom_materia):
    print(f"{materia:<11} {p:5.1f}")
print("Por estudiante:", prom_estudiante.round(1))
print("Mejor estudiante: fila", prom_estudiante.argmax())
print("Nota más alta de cada materia:", notas.max(axis=0))

Cambiar la forma: reshape y transpuesta

a = np.arange(12)
print(a, a.shape)

b = a.reshape(3, 4)
print(b)
print("b.T:", b.T.shape)
print(b.T)
print(a.reshape(2, 2, 3).shape)
print(a.reshape(-1, 6).shape)    # -1: «calcula tú este número»

Broadcasting: operar con formas distintas

print(notas.shape, prom_materia.shape)
diferencia = notas - prom_materia      # (6, 4) - (4,)
print(diferencia[:2].round(1))
prom_col = notas.mean(axis=1, keepdims=True)
print(prom_col.shape)                              # (6, 1)
print((notas - prom_col)[:2].round(1))
# Lo mismo, agregando un eje al vector de forma (6,)
print(prom_estudiante[:, np.newaxis].shape)

Producto punto: medir el parecido

# fútbol, música, lectura, videojuegos
ana = np.array([5, 1, 0, 4])
beto = np.array([4, 0, 1, 5])
caro = np.array([0, 5, 4, 1])

print("Ana · Beto =", ana @ beto)
print("Ana · Caro =", ana @ caro)
print("A mano:    ", np.sum(ana * beto))
def parecido(a, b):
    """Similitud coseno: el producto punto de los vectores normalizados."""
    return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))

print(f"Norma de Ana: {np.linalg.norm(ana):.2f}")
print(f"Ana y Beto: {parecido(ana, beto):.2f}")
print(f"Ana y Caro: {parecido(ana, caro):.2f}")
G = np.array([ana, beto, caro])                  # forma (3, 4)
normas = np.linalg.norm(G, axis=1, keepdims=True)  # forma (3, 1)
G_unit = G / normas                              # cada fila mide 1
print(np.linalg.norm(G_unit, axis=1))

similitudes = G_unit @ G_unit.T                  # (3, 4) @ (4, 3)
print(similitudes.shape)
print(similitudes.round(2))
T = np.array([[1.5, 0.5],
              [0.0, 1.0]])
angulos = np.linspace(0, 2 * np.pi, 100)
# 100 puntos de un círculo, uno por columna: forma (2, 100)
circulo = np.array([np.cos(angulos), np.sin(angulos)])
transformado = T @ circulo                       # (2, 2) @ (2, 100)

v = np.array([0.0, 1.0])
print("T @ v =", T @ v)

plt.plot(circulo[0], circulo[1], color="C2", label="Antes")
plt.plot(transformado[0], transformado[1], color="C0",
         label="Después de T")
plt.annotate("", xy=v, xytext=(0, 0),
             arrowprops=dict(arrowstyle="-|>", color="C2", lw=2))
plt.annotate("", xy=T @ v, xytext=(0, 0),
             arrowprops=dict(arrowstyle="-|>", color="C1", lw=2.5))
plt.text(0.5, 0.35, "T @ v")
plt.gca().set_aspect("equal")
plt.ylim(-1.2, 1.9)
plt.title("Una matriz transforma el plano")
plt.legend(loc="upper center", ncol=2)
plt.show()

Una imagen es un arreglo

from sklearn.datasets import load_sample_image

foto = load_sample_image("china.jpg")
print(type(foto).__name__, foto.shape, foto.dtype)
print("Píxel de arriba a la izquierda (R, G, B):", foto[0, 0])
print("Valor más chico y más grande:", foto.min(), foto.max())
recorte = foto[60:300, 150:470]      # filas 60 a 299, columnas 150 a 469
espejo = foto[:, ::-1]               # las columnas, de atrás para adelante
pesos = np.array([0.299, 0.587, 0.114])
gris = foto @ pesos                  # un número por píxel
print(recorte.shape, espejo.shape, gris.shape)

fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8))
ejes[0, 0].imshow(foto)
ejes[0, 0].set_title("Original")
ejes[0, 1].imshow(recorte)
ejes[0, 1].set_title("Recortada")
ejes[1, 0].imshow(espejo)
ejes[1, 0].set_title("Volteada")
ejes[1, 1].imshow(gris, cmap="gray")
ejes[1, 1].set_title("En gris")
for eje in ejes.flat:
    eje.axis("off")
plt.show()
lote = np.stack([foto, espejo, foto[::-1]])      # tres fotos
print("lote:", lote.shape)                        # (N, alto, ancho, 3)
print("una foto:", lote[1].shape)
print("el píxel (0, 0) de cada foto:", lote[:, 0, 0].shape)
# El color promedio de cada foto: se tachan el alto y el ancho
print(lote.mean(axis=(1, 2)).round(1))

Vectorizar: cambiar bucles por arreglos

import time

grande = rng.random(1_000_000)        # un millón de números entre 0 y 1

inicio = time.perf_counter()
total = 0.0
for valor in grande:
    total += valor * valor
t_bucle = time.perf_counter() - inicio

inicio = time.perf_counter()
total_np = np.sum(grande * grande)
t_numpy = time.perf_counter() - inicio

print("¿Dan lo mismo?", np.isclose(total, total_np))
print("¿NumPy fue más de 10 veces más rápido?", t_bucle > 10 * t_numpy)