Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 77: NumPy — cálculo con arreglos de N dimensiones (y fotos hechas de números)

100 Días de Código en Python — Día 77: NumPy — cálculo con arreglos de N dimensiones (y fotos hechas de números)

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 77 (nivel avanzado).

Este tutorial adapta pedagógicamente el Día 77. Las secciones 📝 Contenido original del curso provienen de las lecciones 568 a 573; el cuaderno de inicio y la imagen de ejemplo están en los recursos del curso (Computation with NumPy (start).zip). En varias lecciones parte del código aparece solo en imágenes; aquí se reconstruye y se verifica. Los retos 🟢🟡🟠🔴, las secciones sobre por qué NumPy es rápido y el desbordamiento de uint8, el proyecto extra y la etapa 2 del Laboratorio de Datos Abiertos son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, NumPy 2.5.3, Matplotlib 3.11.2, SciPy 1.18.1 y Pillow 12.3.0, se ejecutó todo el código. Los resultados coinciden con el curso (forma (3, 2, 4) del arreglo misterioso; c12 = 25 y c33 = 28; imagen de 768 × 1024 píxeles), con una diferencia importante: scipy.misc.face() ya no existe (AttributeError: module 'scipy.misc' has no attribute 'face'). La imagen del mapache está ahora en scipy.datasets.face(), que además necesita el paquete pooch y conexión a Internet la primera vez.

🧮 Para el aula: este es, en palabras del curso, "uno de los días más matemáticos". Si en clase de matemáticas aún no han visto matrices, la sección 3 explica lo necesario desde cero. Conviene coordinarlo con el docente de matemáticas.


Tabla de contenidos

  1. Antes de empezar: ¿qué es NumPy?
  2. El ndarray: 1, 2 y N dimensiones
  3. Generar y rebanar arreglos
  4. Operaciones, broadcasting y multiplicación de matrices
  5. Imágenes como arreglos
  6. 📖 ¿Por qué NumPy es tan rápido?
  7. 📖 El desbordamiento de uint8
  8. 🚀 Proyecto del día: editor de fotos con NumPy
  9. 🌟 Proyecto extra: banderas y arte con matrices
  10. 🏗️ Laboratorio de Datos Abiertos — Etapa 2
  11. 🧩 Comprueba lo aprendido
  12. 🤖 Programar con apoyo de IA
  13. 🛠️ Correcciones técnicas al material original
  14. 📋 Matriz de trazabilidad
  15. 📦 Evidencias de aprendizaje del Día 77

0. Antes de empezar: ¿qué es NumPy?

(Fuente: 568-day-77-goals-what-you-will-make-by-the-end-of-the-day.md)

📝 Contenido original del curso

Ningún curso de ciencia de datos está completo sin NumPy (Numerical Python), una biblioteca usada en casi todos los campos de la ciencia y la ingeniería: prácticamente el estándar para trabajar con datos numéricos en Python.

Hasta ahora hemos usado pandas, que está construido sobre NumPy. pandas es una herramienta de alto nivel para manipular datos (series de tiempo, agrupar, unir, combinar, encontrar faltantes); NumPy brilla en tareas de bajo nivel, como cálculos matemáticos serios.

Hoy aprenderás a aprovechar los ndarray, acceder a valores y subconjuntos de un arreglo de N dimensiones, usar el broadcasting, hacer álgebra lineal, generar puntos para graficar y manipular imágenes como arreglos.

Preparación: descarga el ZIP, sube el cuaderno a Google Drive, ábrelo con Colab y agrega la imagen.

Prerequisitos: listas y rebanadas (slicing) de Python (Días 4 y 5), gráficos con Matplotlib (Día 73). En Colab NumPy ya está instalado; en tu computador: pip install numpy matplotlib pillow.


1. El ndarray: 1, 2 y N dimensiones

(Fuente: 569-numpy-s-ndarray-incredible-power-at-your-fingertips.md)

🎯 Objetivo de aprendizaje

Crear arreglos de 1, 2 y 3 dimensiones, describirlos con ndim y shape, y acceder a cualquier valor o subconjunto.

📝 Contenido original del curso

Por convención, NumPy se importa como np. La joya de la corona es el ndarray: un arreglo homogéneo de n dimensiones.

  • Homogéneo: todos los datos son del mismo tipo (por ejemplo, todos decimales). Una lista de Python o un DataFrame pueden mezclar textos, números y objetos.
  • N dimensiones: desde una columna (1 dimensión) hasta una matriz (2) o un montón de matrices apiladas (N).
Python
import numpy as np

my_array = np.array([1.1, 9.2, 8.1, 4.7])   # un "vector"
my_array.shape    # (4,)
my_array[2]       # 8.1
my_array.ndim     # 1

array_2d = np.array([[1, 2, 3, 9],
                     [5, 6, 7, 8]])         # una "matriz"
print(f'array_2d has {array_2d.ndim} dimensions')
print(f'Its shape is {array_2d.shape}')
print(f'It has {array_2d.shape[0]} rows and {array_2d.shape[1]} columns')
array_2d[1, 2]    # 7: tercer valor de la segunda fila
array_2d[0, :]    # toda la primera fila

NumPy llama ejes (axes) a las dimensiones: el primer eje tiene longitud 2 y el segundo, 4. Un arreglo de 3 dimensiones o más suele llamarse tensor (de ahí el nombre de TensorFlow).

Reto: para el arreglo de abajo: ¿cuántas dimensiones?, ¿qué forma?, accede al valor 18, obtén el vector [97, 0, 27, 18] y la matriz (3, 2) [[0, 4], [7, 5], [5, 97]].

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

                          [[7, 86, 6, 98],
                           [5, 1, 0, 4]],

                          [[5, 36, 32, 48],
                           [97, 0, 27, 18]]])

Solución:

Python
print(f'We have {mystery_array.ndim} dimensions')   # 3
print(f'The shape is {mystery_array.shape}')        # (3, 2, 4)
mystery_array[2, 1, 3]    # 18
mystery_array[2, 1, :]    # [97  0 27 18]
mystery_array[:, :, 0]    # [[ 0  4] [ 7  5] [ 5 97]]

Con 3 o más dimensiones es difícil visualizar el arreglo: detente y experimenta seleccionando distintos subconjuntos hasta sentirte cómodo pensando en los distintos ejes.

📖 Explicación conceptual (adaptación)

Una analogía para 3 dimensiones: piensa en un edificio. mystery_array[piso, fila, columna]:

  • Eje 0 — 3 pisos (las 3 "hojas" separadas por línea en blanco).
  • Eje 1 — 2 filas en cada piso.
  • Eje 2 — 4 columnas (apartamentos) en cada fila.

mystery_array[2, 1, 3] = piso 2, fila 1, apartamento 3 → 18. Y mystery_array[:, :, 0] = "el apartamento 0 de todas las filas de todos los pisos".

Cómo leer la forma: cuenta los corchetes de apertura al principio ([[[ → 3 dimensiones) y luego cuántos elementos hay en cada nivel, de afuera hacia adentro: 3 bloques, 2 filas, 4 números → (3, 2, 4).

🛠️ Nota técnica: la solución del curso dice que hay "4 elementos a lo largo del eje #3". Los ejes se numeran desde 0, así que un arreglo de 3 dimensiones tiene los ejes 0, 1 y 2: los 4 elementos están en el eje 2.

⚠️ Errores frecuentes

Error Qué ocurre Solución
array_2d[1][2] en vez de array_2d[1, 2] Funciona, pero crea un arreglo intermedio y no sirve con rebanadas en varios ejes Usa una coma: array_2d[1, 2]
Filas de distinta longitud: np.array([[1, 2], [3]]) ValueError (arreglo "irregular") Todas las filas deben tener la misma longitud
Confundir shape con ndim shape es una tupla con la longitud de cada eje; ndim es cuántos ejes hay ndim == len(shape)
Índice fuera de rango IndexError: index 3 is out of bounds for axis 0 with size 3 El mensaje dice qué eje falló: revísalo

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: obtén de mystery_array el piso del medio completo (forma (2, 4)).
  • 🟡 Reto 2: obtén la última columna de todas las filas de todos los pisos. ¿Qué forma tiene?
  • 🟠 Reto 3: calcula mystery_array.sum(axis=0), sum(axis=1) y sum(axis=2). Predice primero la forma de cada resultado. ¿Qué "aplasta" cada eje?

2. Generar y rebanar arreglos

(Fuente: 570-generating-and-manipulating-ndarrays.md)

🎯 Objetivo de aprendizaje

Crear arreglos con funciones de NumPy y seleccionar partes con rebanadas.

📝 Contenido original del curso

La mejor forma de aprender NumPy es aplicarlo a pequeños problemas. Ocho mini retos:

Reto Enunciado Solución del curso
1 Vector a con los valores de 10 a 29 usando .arange() (se abre en otra pestaña) a = np.arange(10, 30)
2 Los últimos 3 valores; el 4.º, 5.º y 6.º; todos menos los 12 primeros; los números pares (cada segundo número) a[-3:], a[3:6], a[12:], a[::2]
3 Invertir el orden np.flip(a) o a[::-1]
4 Índices de los elementos no nulos de [6, 0, 9, 0, 0, 5, 0] np.nonzero(b) (devuelve una tupla)
5 Arreglo aleatorio de 3 × 3 × 3 np.random.random((3, 3, 3))
6 Vector x de 9 valores igualmente espaciados entre 0 y 100 (incluidos) con .linspace() (se abre en otra pestaña) x = np.linspace(0, 100, num=9)
7 Vector y de 9 valores entre −3 y 3 y un gráfico de líneas de x contra y y = np.linspace(start=-3, stop=3, num=9); plt.plot(x, y)
8 Arreglo noise de 128 × 128 × 3 aleatorio mostrado como imagen con imshow noise = np.random.random((128, 128, 3)); plt.imshow(noise)

En el reto 8, los valores entre 0 y 1 se interpretan como el rojo, verde y azul (RGB) de cada píxel: acabamos de generar una imagen de 128 × 128 píxeles de ruido aleatorio.

📖 Explicación conceptual (verificada)

arange contra linspace:

np.arange(inicio, fin, paso) np.linspace(inicio, fin, num)
Tú eliges El paso La cantidad de valores
¿Incluye el final? No (como range) Sí (por defecto)
Ejemplo np.arange(10, 30) → 10 … 29 np.linspace(0, 100, 9) → 0, 12.5, 25 … 100
Úsalo para Enteros consecutivos Puntos para graficar una función

Rebanadas: funcionan como en las listas: [inicio:fin:paso], con el fin excluido. Resultados verificados: a[-3:] → [27 28 29]; a[3:6] → [13 14 15]; a[::-1] empieza en 29.

🛠️ Nota técnica: a[::2] toma cada segundo elemento, no "los pares". Coinciden aquí solo porque a empieza en 10. Con np.arange(11, 31), [::2] da 11, 13, 15… (impares). Para quedarte con los pares, sin importar dónde empiece, usa un filtro booleano: a[a % 2 == 0].

💡 Números aleatorios hoy: np.random.random sigue funcionando, pero la documentación de NumPy recomienda el generador moderno:

Python
rng = np.random.default_rng(42)     # 42 = semilla: mismos números en cada ejecución
z = rng.random((3, 3, 3))

La semilla hace que tus resultados sean reproducibles: tus compañeros obtendrán los mismos números.

⚠️ Errores frecuentes

  • np.array(1, 2, 3) → TypeError. Los valores van en una lista: np.array([1, 2, 3]).
  • np.random.random(3, 3, 3) → TypeError. La forma va en una tupla: random((3, 3, 3)).
  • Esperar que arange(0, 1, 0.1) incluya el 1: no lo incluye; y con decimales puede sorprender por redondeo. Para decimales, prefiere linspace.
  • Modificar una rebanada: en NumPy, b = a[3:6] es una vista: si cambias b, cambia a. Usa a[3:6].copy() si necesitas independencia.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: con linspace, genera 100 valores de x entre −10 y 10 y grafica y = x² (sin bucles: y = x ** 2).
  • 🟡 Reto 2: crea con rng.integers(1, 7, size=1000) mil lanzamientos de un dado. ¿Cuántas veces salió cada cara? (Pista: np.unique(..., return_counts=True).)
  • 🟠 Reto 3: demuestra con código que una rebanada es una vista: cambia un valor de la rebanada y muestra el arreglo original.

3. Operaciones, broadcasting y multiplicación de matrices

(Fuente: 571-broadcasting-scalars-and-matrix-multiplication.md)

🎯 Objetivo de aprendizaje

Distinguir operaciones elemento a elemento, broadcasting y multiplicación de matrices, y calcular a mano un elemento de un producto de matrices.

📝 Contenido original del curso

NumPy trata vectores y matrices como lo esperaría un matemático:

Python
v1 = np.array([4, 5, 2, 7])
v2 = np.array([2, 1, 3, 3])
v1 + v2    # array([ 6,  6,  5, 10])
v1 * v2    # array([ 8,  5,  6, 21])

list1 = [4, 5, 2, 7]
list2 = [2, 1, 3, 3]
list1 + list2    # [4, 5, 2, 7, 2, 1, 3, 3]  (concatena)
list1 * list2    # ¡error!

Broadcasting: a menudo se opera un arreglo con un solo número (un escalar), por ejemplo, multiplicar todo por 2. NumPy "expande" el escalar para hacerlo compatible con el arreglo más grande; lo mismo vale en 2 o más dimensiones: el escalar opera elemento a elemento.

Multiplicación de matrices: si multiplicamos por otro vector o matriz, se siguen las reglas del álgebra lineal (se abre en otra pestaña).

Python
a1 = np.array([[1, 3],
               [0, 1],
               [6, 2],
               [9, 7]])

b1 = np.array([[4, 1, 3],
               [5, 8, 5]])

Reto: calcula a mano c₁₂ y c₃₃ de a1 × b1 y compruébalo con np.matmul() o el operador @.

Python
c = np.matmul(a1, b1)    # o: a1 @ b1

c₁₂ = 1·1 + 3·8 = 1 + 24 = 25 y c₃₃ = 6·3 + 2·5 = 18 + 10 = 28.

📖 Explicación conceptual (verificada)

Tres operaciones que se escriben parecido y hacen cosas distintas:

Operación Código Qué hace Requisito de formas
Elemento a elemento v1 * v2 Multiplica posición por posición Formas iguales (o compatibles por broadcasting)
Con escalar array_2d * 2 Multiplica cada elemento por 2 Ninguno
Producto de matrices a1 @ b1 Fila × columna, sumando Columnas de la 1.ª = filas de la 2.ª

Cómo se calcula c₁₂ (fila 1 de a1, columna 2 de b1; en el código, c[0, 1] porque se cuenta desde 0):

Texto
fila 1 de a1:     [1, 3]
columna 2 de b1:  [1, 8]
c12 = 1·1 + 3·8 = 25

Regla de las formas: (4, 2) @ (2, 3) → (4, 3). Los números "de adentro" deben coincidir (2 y 2) y los "de afuera" dan la forma del resultado. Resultado verificado:

Texto
[[19 25 18]
 [ 5  8  5]
 [34 22 28]
 [71 65 62]]

Broadcasting más allá de un escalar (adaptación): también funciona entre arreglos si, comparando las formas desde la derecha, cada par de longitudes es igual o una de ellas es 1:

Python
array_2d = np.array([[1, 2, 3, 4],
                     [5, 6, 7, 8]])
array_2d + np.array([10, 20, 30, 40])   # (2, 4) + (4,) -> suma el vector a cada fila
# [[11 22 33 44]
#  [15 26 37 48]]
array_2d + np.array([1, 2, 3])          # (2, 4) + (3,) -> ValueError: could not be broadcast

¿Para qué sirve en la vida real? Para aplicar la misma operación a millones de datos sin bucles: convertir todas las temperaturas de °C a °F (c * 9/5 + 32), aplicar un descuento del 15 % a todos los precios, o convertir una imagen a gris (sección 4).

⚠️ Errores frecuentes

  • Usar * cuando se quería @: con dos matrices cuadradas ambos funcionan y dan resultados distintos, sin ningún error. Verificado con m = [[1, 2], [3, 4]]: m * m = [[1, 4], [9, 16]], pero m @ m = [[7, 10], [15, 22]].
  • Formas incompatibles en @: b1 @ a1 es (2, 3) @ (4, 2) → ValueError. El orden importa: el producto de matrices no es conmutativo.
  • Confundir los índices matemáticos con los de Python: c₁₂ es c[0, 1].

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: calcula a mano c₄₁ y compruébalo con código.
  • 🟡 Reto 2: una tienda escolar vende 3 productos a precios [1500, 2000, 800] pesos. Las ventas de 4 días son una matriz de 4 × 3. Calcula con un solo @ el ingreso de cada día.
  • 🔴 Reto 3 — Pensamiento computacional: tienes las notas de 30 estudiantes en 4 actividades (matriz 30 × 4) y los porcentajes [0.2, 0.2, 0.3, 0.3]. Calcula la nota definitiva de todos con una sola operación y explica por qué funciona.

4. Imágenes como arreglos

(Fuente: 572-manipulating-images-as-ndarrays.md y 573-learning-points-y-summary.md)

🎯 Objetivo de aprendizaje

Representar una imagen como un arreglo de 3 dimensiones y transformarla (gris, volteo, rotación, negativo) con operaciones de NumPy.

📝 Contenido original del curso

Una imagen es una colección de píxeles; cada píxel es un color; y cada color es una combinación de rojo, verde y azul (RGB). SciPy y PIL ayudan a trabajar con imágenes. SciPy trae una imagen de un mapache:

Python
from scipy import misc
from PIL import Image

img = misc.face()
plt.imshow(img)

Reto: ¿de qué tipo es img? ¿Qué forma y cuántas dimensiones tiene? ¿Qué resolución?

Solución: es un ndarray de 3 dimensiones: tres matrices apiladas (rojo, verde y azul), cada una de 768 filas y 1024 columnas, que es la resolución de la imagen.

Reto: convierte la imagen a blanco y negro con la fórmula de luminancia (se abre en otra pestaña). La fórmula necesita valores entre 0 y 1 (formato sRGB), pero img va de 0 a 255:

Python
sRGB_array = img / 255                          # división por un escalar: broadcasting
grey_vals = np.array([0.2126, 0.7152, 0.0722])  # pesos de la fórmula
img_gray = sRGB_array @ grey_vals               # o: np.matmul(sRGB_array, grey_vals)
plt.imshow(img_gray, cmap='gray')

El parámetro cmap es importante: sin él, Matplotlib no sabe que es una imagen en blanco y negro.

Reto: 1) voltea la imagen gris "de cabeza"; 2) rota la imagen a color; 3) invierte (solariza) la imagen a color: el negro (0) pasa a blanco (255).

Python
plt.imshow(np.flip(img_gray), cmap='gray')   # 1) invierte filas y columnas
plt.imshow(np.rot90(img))                    # 2) rota 90°
solar_img = 255 - img                        # 3) broadcasting con el escalar 255
plt.imshow(solar_img)

Usa tus propias imágenes: el ZIP incluye un .jpg. La imagen debe tener 3 canales (RGB); un .png con 4 canales necesita pasos adicionales. Se abre con PIL y se convierte en arreglo:

Python
my_img = Image.open('yummy_macarons.jpg')
img_array = np.array(my_img)
img_array.ndim     # 3
img_array.shape    # la resolución de tu imagen y 3 canales

Resumen del día: crear arreglos con np.array(); generarlos con .arange(), .random() y .linspace(); analizar forma y dimensiones; rebanar; operaciones con escalares y multiplicación de matrices; broadcasting; manipular imágenes como arreglos.

📖 Explicación conceptual (verificada)

🛠️ Nota técnica (verificado): scipy.misc.face() ya no existe en las versiones actuales de SciPy (AttributeError); todo el módulo scipy.misc está obsoleto. La imagen está ahora en scipy.datasets:

Python
from scipy import datasets     # requiere: pip install pooch
img = datasets.face()          # la primera vez la descarga de Internet

Si no tienes Internet o no quieres instalar más paquetes, usa tu propia imagen con PIL (es lo que haremos en el proyecto).

La forma (768, 1024, 3) (verificado, tipo uint8, valores de 0 a 255) se lee: 768 filas × 1024 columnas × 3 valores por píxel. img[:, :, 0] es la matriz del rojo; img[0, 0] es el primer píxel, con sus 3 valores.

¿Por qué @ convierte a gris? Cada píxel es un vector [R, G, B]. sRGB_array @ grey_vals hace en cada píxel: 0,2126·R + 0,7152·G + 0,0722·B. El resultado tiene forma (768, 1024): un número (brillo) por píxel. El verde pesa más porque el ojo humano es más sensible a él.

🛠️ Nota técnica: la fórmula de la Y lineal se aplica, en rigor, a valores RGB lineales; los valores de una imagen (sRGB) están codificados con una "gamma" y habría que linealizarlos primero. Dividir entre 255 no lo hace. Para un efecto visual el resultado es muy bueno, pero es una aproximación.

🛠️ Nota técnica (verificado): np.flip(img_gray) sin eje invierte filas y columnas, es decir, rota 180° (la imagen queda de cabeza y en espejo). Para ponerla solo de cabeza: np.flipud(img_gray) (equivale a img_gray[::-1]); para el espejo horizontal: np.fliplr(img_gray).

🛠️ Nota técnica (verificado): con un PNG de 4 canales (RGBA), imagen / 255 @ grey_vals falla con ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0 ... (size 3 is different from 4). La solución es convertir al abrir: np.array(Image.open('foto.png').convert('RGB')).

🔎 Ejemplo explicado: recortar y aclarar (adaptación)

Python
recorte = img[100:400, 300:700]        # filas 100 a 399, columnas 300 a 699 -> forma (300, 400, 3)
solo_rojo = img.copy()
solo_rojo[:, :, 1:] = 0                # apaga verde y azul en todos los píxeles

Recortar es rebanar (sección 2) y quitar canales es asignar a una rebanada. Todo sin un solo bucle for.

⚠️ Errores frecuentes

Error Síntoma Solución
Olvidar cmap='gray' La imagen gris se ve en verde y morado (mapa de color por defecto) plt.imshow(img_gray, cmap='gray')
Sumar brillo con img + 100 Colores extraños: los valores "dan la vuelta" Sección 6
Pasar a imshow decimales fuera de 0–1 Advertencia y colores recortados Divide entre 255 o convierte a uint8
Abrir un PNG con transparencia ValueError en @ .convert('RGB')

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: muestra por separado los canales rojo, verde y azul de tu imagen, cada uno con cmap='gray'. ¿Cuál se parece más a la versión en gris? ¿Por qué?
  • 🟡 Reto 2: convierte la imagen gris en blanco y negro puro (solo 0 y 255) con un umbral: (img_gray > 0.5) * 255. Prueba varios umbrales.
  • 🟠 Reto 3: reduce la resolución a la mitad tomando uno de cada dos píxeles (img[::2, ::2]). ¿Qué forma tiene? ¿Qué se pierde?
  • 🔴 Reto 4 — Pensamiento computacional: crea un efecto "sepia" investigando su fórmula (una matriz de 3 × 3) y aplicándola con @. Explica por qué la forma de la matriz debe ser 3 × 3 y cómo evitas valores mayores que 255.

5. 📖 ¿Por qué NumPy es tan rápido?

(Sección creada para esta adaptación.)

Compara duplicar un millón de números:

Python
import time

numeros = list(range(1_000_000))
arreglo = np.arange(1_000_000)

inicio = time.perf_counter()
dobles_lista = [x * 2 for x in numeros]
print('lista:', time.perf_counter() - inicio)

inicio = time.perf_counter()
dobles_arreglo = arreglo * 2
print('NumPy:', time.perf_counter() - inicio)

En el computador de la verificación, NumPy fue unas 18 veces más rápido (0,041 s contra 0,002 s). Los tiempos cambian en cada equipo; lo que importa es el orden de magnitud. ¿Por qué?

  1. Homogeneidad: todos los datos tienen el mismo tipo y están juntos en memoria, sin que Python revise el tipo de cada uno.
  2. Vectorización: el bucle ocurre dentro de código compilado (C), no en Python.
  3. Menos código: arreglo * 2 es más corto y claro que un bucle.

Regla práctica: si estás escribiendo un for para recorrer un arreglo de NumPy o una columna de pandas, probablemente existe una operación vectorizada que lo hace mejor.


6. 📖 El desbordamiento de uint8

(Sección creada para esta adaptación.)

Las imágenes usan el tipo uint8: enteros sin signo de 8 bits, de 0 a 255. ¿Qué pasa si te sales del rango? Verificado:

Python
pixel = np.array([250, 10, 128], dtype=np.uint8)
pixel + 10    # [  4  20 138]   <- ¡250 + 10 = 4!
pixel * 2     # [244  20   0]   <- ¡128 * 2 = 0!
255 - pixel   # [  5 245 127]   <- el negativo del curso: siempre dentro del rango

El valor "da la vuelta" como un odómetro o un reloj: después de 255 viene 0. Y NumPy no avisa. Por eso el negativo 255 - img del curso funciona (nunca sale del rango), pero "aclarar" con img + 50 produce manchas de colores.

La solución: calcular en un tipo más grande y recortar al rango válido:

Python
aclarada = np.clip(img.astype(np.int16) + 50, 0, 255).astype(np.uint8)

💡 Este tipo de error (un desbordamiento silencioso) ha causado fallas reales en software. Es un buen ejemplo de por qué hay que probar con valores en los bordes (0 y 255), no solo con valores "normales".


7. 🚀 Proyecto del día: editor de fotos con NumPy

(Integra las lecciones 569 a 573.)

Problema

Las aplicaciones de filtros de fotos parecen magia. Detrás hay operaciones con matrices como las de hoy.

Contexto

El club de fotografía del colegio quiere un programa sencillo que aplique filtros a las fotos de los eventos sin subirlas a Internet.

Objetivo

Un cuaderno o script con funciones de filtro que reciben un arreglo y devuelven otro, y una galería que muestre el antes y el después.

Requisitos funcionales

  1. Cargar una imagen propia con PIL y convertirla a RGB.
  2. Funciones: a_gris(img), voltear(img, modo) (vertical, horizontal o 180°), rotar(img, veces), negativo(img), ajustar_brillo(img, cantidad), recortar(img, fila1, fila2, col1, col2).
  3. Una galería de 2 × 3 con plt.subplots que muestre los filtros.
  4. Guardar cada resultado con Image.fromarray(...).save(...).

Restricciones técnicas

  • Sin bucles for sobre píxeles: solo operaciones de NumPy.
  • ajustar_brillo no debe desbordar (sección 6).
  • Cada función con docstring y sin modificar la imagen original (devuelve un arreglo nuevo).

Planificación

  1. Comprender cómo se guarda una imagen. 2. Entradas: una foto. 3. Procesos: una función por filtro. 4. Salidas: galería y archivos. 5. Dividir: primero cargar, luego cada filtro. 6. Diseñar la firma de cada función. 7. Implementar. 8. Probar con la tabla. 9. Corregir. 10. Agregar filtros propios.

Construcción paso a paso

  • Versión 1: cargar, mostrar forma y tipo.
  • Versión 2: gris, volteo y rotación.
  • Versión 3: negativo y brillo con np.clip.
  • Versión 4: recorte y galería.
  • Versión final: guardado y un filtro de tu invención.

Pruebas

Prueba Resultado esperado
a_gris(img).shape (alto, ancho): 2 dimensiones
rotar(img, 1).shape con una imagen de 768 × 1024 (1024, 768, 3) (verificado)
negativo(negativo(img)) Igual a img (np.array_equal)
ajustar_brillo(img, 300).max() 255, sin desbordar
ajustar_brillo(img, -300).min() 0
img después de aplicar todos los filtros Sin cambios

🧪 Prueba y depuración

Python
def a_gris(img):
    return img @ [0.2126, 0.7152, 0.0722]

def ajustar_brillo(img, cantidad):
    return img + cantidad

def voltear_de_cabeza(img):
    return np.flip(img)

foto = np.array(Image.open('foto.png'))
plt.imshow(a_gris(foto))

Encuentra cuatro problemas: con un PNG de 4 canales @ falla (falta .convert('RGB')); ajustar_brillo desborda uint8; np.flip sin eje en una imagen de 3 dimensiones invierte también los canales (el rojo y el azul se intercambian) además de rotar 180°; y imshow de la imagen gris sin cmap='gray'. (Detalle: a_gris no divide entre 255, pero imshow normaliza automáticamente las imágenes de 2 dimensiones; el problema aparecería al guardarla.)

⭐ Extensión del proyecto

  • Reto 1: un filtro de desenfoque promediando cada píxel con sus vecinos (pista: suma rebanadas desplazadas).
  • Reto 2: un collage que una 4 fotos en una cuadrícula con np.concatenate o np.block.
  • Reto 3: un menú de texto que pregunte qué filtro aplicar (repaso de los Días 1 a 10).

8. 🌟 Proyecto extra: banderas y arte con matrices

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

Para la semana cultural, el colegio quiere imágenes generadas por código: banderas, logotipos sencillos y "pixel art".

Ejemplo de partida (verificado)

Python
bandera = np.zeros((300, 450, 3), dtype=np.uint8)   # imagen negra de 300 x 450
bandera[:150] = [252, 209, 22]      # mitad superior: amarillo
bandera[150:225] = [0, 56, 147]     # un cuarto: azul
bandera[225:] = [206, 17, 38]       # un cuarto: rojo
Image.fromarray(bandera).save('colombia.png')

Observa el broadcasting: [252, 209, 22] (forma (3,)) se asigna a todos los píxeles de la rebanada (150, 450, 3).

Requisitos

  1. Crea tres banderas de países o departamentos de Colombia con franjas de distintas proporciones (horizontales y verticales).
  2. Crea un "pixel art" de al menos 16 × 16 a partir de una matriz de números (0 = fondo, 1 = color A, 2 = color B…) y una paleta, usando indexación: paleta[matriz].
  3. Amplía el pixel art a 320 × 320 sin perder los bordes nítidos (pista: np.repeat o np.kron).
  4. Genera un degradado de color con np.linspace y broadcasting.

Decisiones que debes tomar (y justificar)

  • ¿Cómo representas los colores: tuplas, un diccionario, un arreglo de paleta?
  • Para banderas con figuras (estrellas, escudos), ¿qué puedes lograr con matrices y qué no?

Casos de prueba

  • Las proporciones de cada franja coinciden con las oficiales (compruébalo contando filas).
  • imagen.dtype == np.uint8 y imagen.shape[2] == 3.

9. 🏗️ Laboratorio de Datos Abiertos — Etapa 2

(Proyecto integrador creado para esta adaptación; ver la etapa 1 en el Día 76.)

Etapa 2 (hoy): usa NumPy para preparar las variables de tu conjunto de datos.

  1. Extrae 3 columnas numéricas como arreglo: X = df[['col1', 'col2', 'col3']].to_numpy(). Revisa X.shape y X.dtype.
  2. Calcula el promedio y la desviación de cada columna con axis=0.
  3. Normaliza las columnas a la escala 0–1 con broadcasting, sin bucles: (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0)). Explica en Markdown por qué funciona con las formas (n, 3) y (3,).
  4. Crea una variable categórica con np.where (por ejemplo, "alto"/"bajo" según la mediana).
  5. Calcula la matriz de correlaciones con np.corrcoef(X, rowvar=False) y escribe qué pareja de variables está más relacionada: será la candidata para la regresión del Día 78.

Producto de la etapa: sección 3 de laboratorio.ipynb.


10. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué significa que un ndarray sea homogéneo? ¿Por qué eso lo hace rápido?
  2. ¿Qué diferencia hay entre arange y linspace?
  3. Explica con tus palabras qué es el broadcasting.
  4. ¿Por qué una imagen a color tiene 3 dimensiones y una gris solo 2?
  5. ¿Por qué 255 - img nunca desborda, pero img + 50 sí?

Análisis de código

Python
m = np.arange(12).reshape(3, 4)
print(m[:, 1:3])
print(m.sum(axis=1))

¿Qué imprime cada línea? ¿Qué formas tienen los resultados?

Predicción

¿Qué forma tiene np.ones((5, 1)) + np.ones(3)? ¿Y np.ones((2, 3)) @ np.ones((3, 4))?

Depuración

"Convertí mi foto a gris y se ve verde y morada". ¿Qué falta?

Modificación

Cambia a_gris para que use el promedio simple de los 3 canales (img.mean(axis=2)). Compara visualmente con la fórmula de luminancia. ¿Cuál se ve más natural?

Creación

Escribe tablero(n, tamaño) que devuelva una imagen de ajedrez de n × n casillas usando solo NumPy (pista: (filas + columnas) % 2).


11. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — Código que ya no funciona. Pide a una IA "cargar la imagen del mapache de SciPy". Si propone scipy.misc.face(), ejecútalo, lee el error y busca en la documentación de SciPy (scipy.datasets) la alternativa actual.

Actividad 2 — Desbordamiento. Pide a una IA una función para aumentar el brillo de una imagen con NumPy. Pruébala con un píxel de valor 250 y cantidad 10. Si da 4, explícale a la IA el problema y compara su corrección con np.clip.

Actividad 3 — ¿flip o flipud? Pregunta a una IA cómo poner una imagen "de cabeza" con NumPy. Comprueba con np.array_equal si su respuesta invierte solo las filas o también las columnas, y contrasta con la documentación de numpy.flip.


12. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
572 from scipy import misc; misc.face() No existe en SciPy actual (AttributeError, verificado); ahora scipy.datasets.face(), que requiere pooch e Internet Nota técnica de la sección 4
569 "4 elementos a lo largo del eje #3" Los ejes son 0, 1 y 2: es el eje 2 Nota técnica de la sección 1
570 a[::2] da "los números pares" Da cada segundo elemento; coincide solo porque a empieza en 10 (verificado) Nota técnica de la sección 2
570 np.random.random Funciona; la recomendación actual es np.random.default_rng() Sección 2
572 np.flip(img_gray) voltea "de cabeza" Rota 180° (invierte filas y columnas; verificado); de cabeza es np.flipud Nota técnica de la sección 4
572 Fórmula de Y lineal sobre valores divididos entre 255 Aproximación: los valores sRGB no son lineales Nota técnica de la sección 4
572 Los PNG de 4 canales necesitan "pasos adicionales" Error verificado y solución con .convert('RGB') Nota técnica de la sección 4
571 c₁₂ = 25 y c₃₃ = 28 Correcto (verificado) Sección 3
568 "ndarrays" en varias frases Errata menor: ndarray, nonzero "imprime" índices (devuelve una tupla) —

13. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
NumPy en la ciencia, pandas sobre NumPy, qué se aprende, cuaderno e imagen 568 Sección 0 Preparar el cuaderno — Sección 7
ndarray homogéneo de N dimensiones, shape, ndim, indexación en 1 y 2 dimensiones, tensores, arreglo misterioso 569 Sección 1 Reto del arreglo misterioso Retos 1–3 de la sección 1 Secciones 7 y 8
arange, rebanadas, flip, nonzero, random, linspace, gráfico, ruido con imshow 570 Sección 2 8 mini retos Retos 1–3 de la sección 2 Secciones 7, 8 y 9
Suma y producto de vectores, listas contra arreglos, escalares, broadcasting, matmul y @, c₁₂ y c₃₃ 571 Secciones 3 y 5 Reto de multiplicación de matrices Tienda escolar; notas definitivas Secciones 7 y 9
RGB, mapache de SciPy, forma de la imagen, gris con @, cmap, flip, rot90, negativo, imagen propia con PIL 572 Secciones 4 y 6 4 retos de imágenes Canales, umbral, sepia Secciones 7 y 8
Resumen del día, cuaderno completo 573 Sección 4 — — Sección 7
(No existe en el curso original) — Secciones 5 y 6 — Vectorización y desbordamiento Sección 7
(No existe en el curso original) — — — Banderas y arte con matrices Sección 8
(No existe en el curso original) — — — Laboratorio de Datos Abiertos, etapa 2 Sección 9

14. 📦 Evidencias de aprendizaje del Día 77

  • El cuaderno con los retos del arreglo misterioso, los 8 mini retos y la multiplicación de matrices (con el cálculo de c₁₂ y c₃₃ hecho a mano, en foto o en Markdown).
  • El editor de fotos con su galería y la tabla de pruebas superada.
  • Las banderas y el pixel art del proyecto extra.
  • La etapa 2 del Laboratorio de Datos Abiertos.
  • Cuaderno de errores: scipy.misc.face, forma pasada sin tupla, * en vez de @, formas incompatibles, flip que rota 180°, olvido de cmap, PNG de 4 canales, desbordamiento de uint8, rebanadas que son vistas.