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).
27 min de lectura
16 secciones
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.
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.
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
1234567891011121314
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]].
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.
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.
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.
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
12
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.
🟢 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.
05
3. Operaciones, broadcasting y multiplicación de matrices#
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.
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
123
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
1234
[[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
123456
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).
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].
🟢 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.
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
12345
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.
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
1234
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
1234
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.
🛠️ 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
12
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
123
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.
🟢 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.
import time
numeros =list(range(1_000_000))
arreglo = np.arange(1_000_000)
inicio = time.perf_counter()
dobles_lista =[x *2for x in numeros]print('lista:', time.perf_counter()- inicio)
inicio = time.perf_counter()
dobles_arreglo = arreglo *2print('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é?
Homogeneidad: todos los datos tienen el mismo tipo y están juntos en memoria, sin que Python revise el tipo de cada uno.
Vectorización: el bucle ocurre dentro de código compilado (C), no en Python.
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.
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
1234
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:
💡 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".
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.
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.)
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).
Crea tres banderas de países o departamentos de Colombia con franjas de distintas proporciones (horizontales y verticales).
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].
Amplía el pixel art a 320 × 320 sin perder los bordes nítidos (pista: np.repeat o np.kron).
Genera un degradado de color con np.linspace y broadcasting.
(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.
Extrae 3 columnas numéricas como arreglo: X = df[['col1', 'col2', 'col3']].to_numpy(). Revisa X.shape y X.dtype.
Calcula el promedio y la desviación de cada columna con axis=0.
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,).
Crea una variable categórica con np.where (por ejemplo, "alto"/"bajo" según la mediana).
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.
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?
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.
14
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)
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.