Saltar a contenido

1.4 · NumPy

Objetivos. Al terminar este capítulo podrás: crear e inspeccionar arrays; indexarlos y filtrarlos con máscaras booleanas; calcular estadísticas por fila o columna; generar números aleatorios reproducibles; distinguir copias de vistas; transponer y multiplicar matrices; y usar broadcasting para operar arrays de distinto tamaño.

Evidencia de logro. Transformarás y resumirás una tabla de ventas de TechShop usando arrays, máscaras y broadcasting, justificando cada operación.

Contexto y motivación

Los datos tabulares son, por debajo, números en bloque. NumPy es la capa fundamental que da a Python una estructura rápida y cómoda para trabajar con esos bloques: el array. Pandas (cap. 1.5) y scikit-learn (unidades 3–7) se construyen sobre él: un DataFrame de Pandas guarda sus columnas como arrays, y scikit-learn espera los datos en arrays.

Aprender NumPy no es un trámite: es entender cómo se representan los datos por debajo. Casi todos los errores de rendimiento y de lógica que verás más adelante (una operación lenta, una matriz mal alineada, un cambio que "se propaga solo") se explican con los conceptos de este capítulo.

Vocabulario

Término Significado
Array Estructura de NumPy: colección de elementos del mismo tipo, en 1D, 2D, etc.
dtype Tipo de datos de un array (int64, float64…)
Máscara Array de booleanos que selecciona elementos
Eje (axis) Eje por el que se agrega la operación: axis=0 atraviesa las filas y devuelve una entrada por columna; axis=1 atraviesa las columnas y devuelve una entrada por fila
Vista Array que comparte memoria con el original
Copia Array independiente, con su propia memoria
Broadcasting Regla que permite operar arrays de formas distintas

Prerrequisitos

Haber leído los capítulos 1.1 a 1.3 y tener el entorno preparado (Entorno de trabajo).

Ruta de estudio

El núcleo obligatorio para la práctica es crear arrays, inspeccionar su forma y tipo, indexar y filtrar con máscaras, calcular estadísticas, generar datos reproducibles, distinguir vistas de copias y usar broadcasting. La indexación avanzada, el producto matricial, np.where, las operaciones de conjuntos y la combinación de arrays se trabajan como ampliación guiada o demostración cuando el ritmo del grupo lo permita.

Orden de los ejemplos

Los bloques de esta página están pensados para ejecutarse de arriba abajo. Algunos reutilizan imports y arrays definidos en el bloque anterior; si copias uno por separado, incluye también su preparación. En un cuaderno marimo, cada celda debe mostrar esas dependencias.

1. Listas de Python vs. arrays de NumPy

Una lista de Python guarda referencias a objetos (cada elemento puede ser de un tipo distinto y vive en un lugar distinto de la memoria). Un array de NumPy agrupa elementos del mismo tipo y permite almacenarlos de forma compacta; muchos arrays son contiguos en memoria. Esa diferencia tiene dos consecuencias prácticas: operar es más simple y más rápido.

import numpy as np

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

print("lista * 2:", [x * 2 for x in lista])   # hay que hacer un bucle
print("array * 2:", arr * 2)                  # se aplica a todo el array

Salida esperada:

lista * 2: [2, 4, 6, 8, 10]
array * 2: [ 2  4  6  8 10]

En NumPy arr * 2 multiplica cada elemento sin escribir ningún bucle: es una operación vectorizada. Tú escribes la operación una vez y NumPy la aplica a todos los elementos a la vez.

¿Por qué es más rápido? Porque el bucle for x in lista se ejecuta en Python, instrucción a instrucción, con toda su sobrecarga. La operación vectorizada se ejecuta en C, un lenguaje mucho más cercano a la máquina, y además sobre datos contiguos en memoria (que la CPU puede leer en bloque). Es la misma razón por la que sumar a mano 1000 números es más lento que usar una calculadora.

import time

n = 1_000_000
numeros = list(range(n))

t0 = time.perf_counter()
suma_lista = sum(x ** 2 for x in numeros)      # bucle Python
t_lista = time.perf_counter() - t0

t0 = time.perf_counter()
suma_numpy = (np.arange(n) ** 2).sum()          # vectorizado
t_numpy = time.perf_counter() - t0

print("sumas iguales:", suma_lista == suma_numpy)
print(f"tiempos medidos: lista={t_lista:.4f} s, numpy={t_numpy:.4f} s")

Qué observar (la salida cambia en cada equipo): el programa muestra sumas iguales: True y dos tiempos medidos. Los valores concretos dependen del equipo y de la carga del sistema; no se evalúa que coincidan con una cifra concreta, sino la proporción entre ambos. time.perf_counter() funciona como un cronómetro: restar dos lecturas da los segundos transcurridos. En general, NumPy suele ser más rápido: el bucle en Python es el cuello de botella y NumPy ejecuta la operación vectorizada en C por debajo.

Regla práctica

En NumPy (y en Pandas) evita los bucles siempre que exista una operación vectorizada. Piensa en "qué quiero hacerle a todo el array", no en "qué le hago a cada elemento uno a uno".

2. Crear arrays

import numpy as np

print(np.zeros(3))          # inicializar con ceros
print(np.ones(4))           # inicializar con unos
print(np.arange(0, 10, 2))  # secuencia con paso (como range)
print(np.linspace(0, 1, 5)) # 5 valores equiespaciados entre 0 y 1

Salida esperada:

[0. 0. 0.]
[1. 1. 1. 1.]
[0 2 4 6 8]
[0.   0.25 0.5  0.75 1.  ]
  • zeros y ones sirven para inicializar (crear un contenedor que luego rellenarás).
  • arange(0, 10, 2) genera 0, 2, 4, 6, 8 (el 10 no se incluye, igual que range).
  • linspace(0, 1, 5) genera 5 valores equiespaciados entre 0 y 1, incluyendo ambos extremos. Es lo que usas cuando te da igual el paso y te importa el número de puntos.

2.1 Arrays 2D (matrices)

matriz = np.array([[1, 2, 3],
                   [4, 5, 6]])

print(matriz)
print("shape:", matriz.shape, "· ndim:", matriz.ndim,
      "· size:", matriz.size, "· dtype:", matriz.dtype)

Salida esperada:

[[1 2 3]
 [4 5 6]]
shape: (2, 3) · ndim: 2 · size: 6 · dtype: int64

Interpreta los cuatro atributos, porque los usarás constantemente:

  • shape = (filas, columnas) → (2, 3): dos filas y tres columnas.
  • ndim = número de dimensiones (2: es una tabla).
  • size = número total de elementos (6 = 2 × 3).
  • dtype = tipo de datos (int64).

2.2 Tipos de datos

print(np.array([1, 2, 3], dtype=float))  # forzar float64
print(np.array([1, 2, 3]).astype(float)) # convertir después

Salida esperada:

[1. 2. 3.]
[1. 2. 3.]

Elegir bien el tipo

Un array de enteros no admite decimales. NumPy promueve automáticamente a float en operaciones como np.array([1, 2, 3]) / 2 → [0.5 1. 1.5], pero si guardas un 2.5 en un array int, se trunca a 2 sin avisar. Cuando trabajes con importes, precios o medias, usa float64 desde el principio. El tipo correcto evita resultados silenciosamente equivocados.

3. Indexación y slicing

arr = np.array([1, 2, 3, 4, 5])

print(arr[0], arr[-1], arr[1:4])   # primer, último y rango [1,4)
print(arr[::-1])                   # invertido

Salida esperada:

1 5 [2 3 4]
[5 4 3 2 1]

Recuerda la regla de Python: arr[1:4] va del índice 1 al 3 (el 4 no se incluye). Los índices negativos cuentan desde el final (-1 es el último). El truco [::-1] recorre el array hacia atrás, es decir, lo invierte.

En 2D usamos [fila, columna]:

matriz = np.array([[1, 2, 3], [4, 5, 6]])

print(matriz[0, 1])     # fila 0, columna 1
print(matriz[:, 1])     # toda la columna 1
print(matriz[1, :2])    # fila 1, columnas 0 y 1

Salida esperada:

2
[2 5]
[4 5]

: solo significa "todo ese eje": matriz[:, 1] es "todas las filas, columna 1". Memoriza este patrón porque en Pandas y en ML lo usarás a diario: fila, columna, y : para "todo".

4. Operaciones y máscaras booleanas

Las operaciones aritméticas son elemento a elemento:

precios = np.array([12.5, 8.0, 25.0, 5.5, 30.0])

print(precios + 1)
print(np.round(precios * 0.21, 2))   # IVA 21 %

Salida esperada:

[13.5  9.  26.   6.5 31. ]
[2.62 1.68 5.25 1.16 6.3 ]

precios + 1 suma 1 a cada precio y precios * 0.21 calcula el IVA de cada precio a la vez. No hay bucle: la operación se "reparte" por todo el array.

Comparar produce una máscara booleana, y esa máscara selecciona elementos:

print(precios > 10)                 # la máscara
print(precios[precios > 10])        # los precios que la cumplen
print(precios[(precios >= 8) & (precios <= 20)])  # entre 8 y 20
print(precios[(precios < 8) | (precios > 20)])    # fuera de 8-20
print(precios[~(precios < 10)])                   # negación

Salida esperada:

[ True False  True False  True]
[12.5 25.  30. ]
[12.5  8. ]
[25.   5.5 30. ]
[12.5 25.  30. ]

Una máscara es un array de True/False con la misma forma que el original. Al usarla dentro de los corchetes (precios[mascara]), NumPy se queda con los elementos donde la máscara vale True. Es la forma idiomática de "filtrar" en NumPy, y el patrón tabla[condición] se repite idéntico en Pandas.

Paréntesis obligatorios

Para combinar condiciones no uses and/or (fallan con arrays). Usa & (y), | (o), ~ (no) y rodea cada comparación con paréntesis: (precios >= 8) & (precios <= 20). Sin paréntesis, Python evalúa en otro orden y obtienes un error o un resultado incorrecto.

5. Estadísticas y ejes

tabla = np.array([[10, 20, 30],
                  [40, 50, 60]])

print(tabla.mean())          # media de todo
print(tabla.mean(axis=0))    # media por columna (reduce filas)
print(tabla.mean(axis=1))    # media por fila (reduce columnas)
print(tabla.sum(axis=0))     # suma por columna
print(round(tabla.std(), 2), round(tabla.var(), 2))  # desviación y varianza

Salida esperada:

35.0
[25. 35. 45.]
[20. 50.]
[50 70 90]
17.08 291.67

Aquí está la parte que más cuesta al principio: qué significa axis.

  • axis=0 significa "recorre las filas": el resultado tiene una entrada por columna (media de la columna 0 = (10+40)/2 = 25, etc.).
  • axis=1 significa "recorre las columnas": una entrada por fila (media de la fila 0 = (10+20+30)/3 = 20).

Truco para recordarlo: axis=0 "aplasta" hacia abajo (por columnas), axis=1 "aplasta" hacia la derecha (por filas). Si dudas, dibuja la flecha sobre la tabla.

La varianza (291,67) es la media de las distancias al cuadrado a la media; la desviación típica (17,08) es su raíz cuadrada, y se mide en las mismas unidades que los datos. Cuanto mayores, más dispersos están los valores.

std() de NumPy y de pandas no coinciden por defecto

NumPy calcula por defecto la desviación poblacional (ddof=0, divide entre n), mientras que pandas usa la muestral (ddof=1, divide entre n - 1), como viste en 1.2. Con los mismos datos obtendrás números ligeramente distintos. Si el valor importa, escríbelo explícitamente: tabla.std(ddof=1).

6. Números aleatorios reproducibles

rng = np.random.default_rng(42)   # generador moderno con semilla 42

print(rng.integers(0, 10, size=5))     # enteros en [0, 10)
print(np.round(rng.random(5), 3))      # decimales en [0, 1)
print(np.round(rng.normal(0, 1, 5), 3))  # normal media 0, desv. 1
print(np.round(rng.uniform(1, 10, 5), 3))  # uniforme en [1, 10)

Salida esperada:

[0 7 6 4 4]
[0.697 0.094 0.976 0.761 0.786]
[-0.017 -0.853  0.879  0.778  0.066]
[8.405 4.991 3.045 5.991 1.574]

Los números aleatorios sirven para simular datos (generar ventas de prueba, repartir clientes en grupos, etc.). Las distribuciones más usadas:

  • integers(0, 10): enteros en [0, 10) — el 10 no entra.
  • random(5): decimales en [0, 1).
  • normal(media, desv): campana de Gauss (la mayoría de valores cerca de la media).
  • uniform(a, b): todos los valores entre a y b igual de probables.

La semilla (42) fija el generador: con la misma semilla obtienes los mismos números siempre. Es como elegir "la partida guardada" del azar.

otro = np.random.default_rng(42)
print(otro.integers(0, 10, size=5))   # idéntico a antes

Salida esperada:

[0 7 6 4 4]

default_rng, no el método antiguo

Usa np.random.default_rng(seed). El método antiguo np.random.seed(...) sigue existiendo, pero está desaconsejado: es global (afecta a todo el programa) y menos robusto. Con default_rng creas tu propio generador, local e independiente.

7. Guardar y cargar arrays

np.save("ventas.npy", precios)     # guarda en formato binario .npy
cargado = np.load("ventas.npy")    # lo recupera
print(cargado)

Salida esperada:

[12.5  8.  25.   5.5 30. ]

Este bloque crea el archivo ventas.npy en tu carpeta de trabajo: esa es su comprobación visible (además de la línea impresa). .npy guarda el array en binario, con su dtype y su forma, de modo que al cargarlo vuelve exacto (sin pérdidas de precisión como podría ocurrir al pasar por texto). Para varios arrays a la vez usa np.savez.

8. Copias vs. vistas (y el atributo .base)

Esta sección explica uno de los comportamientos más sorprendentes de NumPy, y es la causa de bugs muy difíciles de cazar si no lo conoces.

Un slicing devuelve una vista: comparte memoria con el original (es una "ventana" al mismo dato). Una máscara booleana o un fancy indexing devuelven una copia: un array nuevo e independiente.

a = np.array([1, 2, 3, 4, 5])

vista = a[1:4]
vista[0] = 99
print(a)                    # ¡el original cambió!
print("¿vista comparte memoria?", vista.base is a)

Salida esperada:

[ 1 99  3  4  5]
¿vista comparte memoria? True

Al hacer vista = a[1:4] no creamos datos nuevos: vista es una ventana a los elementos 1, 2 y 3 de a. Por eso, modificar vista[0] modifica también a. Es útil (evita copiar datos grandes) pero hay que saberlo.

b = np.array([1, 2, 3, 4, 5])

seleccion = b[b > 2]        # máscara → copia
seleccion[0] = 100
print(b)                    # el original NO cambia
print("¿seleccion comparte memoria?", seleccion.base is b)

Salida esperada:

[1 2 3 4 5]
¿seleccion comparte memoria? False

La selección con máscara crea un array nuevo (no hay forma de "ventanear" elementos no contiguos). Por eso cambiarla no afecta a b.

Cómo saberlo en el código

arr.base devuelve el array del que depende: si es None, arr es dueño de su memoria (copia); si apunta a otro array, es una vista. Úsalo cuando dudes de por qué un cambio "se propaga" o no. Regla práctica: el slicing es vista; la selección con máscara o con índices es copia.

9. Fancy indexing

Podemos seleccionar con arrays de enteros:

mat = np.array([[10, 11, 12],
                [20, 21, 22],
                [30, 31, 32]])

print(mat[[0, 2]])            # filas 0 y 2
print(mat[[0, 2], [1, 0]])    # pares (fila,columna): (0,1) y (2,0)

Salida esperada:

[[10 11 12]
 [30 31 32]]
[11 30]

"Fancy indexing" es seleccionar con listas/arrays de índices en vez de un solo índice o un rango. mat[[0, 2]] toma las filas 0 y 2 completas; mat[[0, 2], [1, 0]] toma los pares (fila 0, columna 1) y (fila 2, columna 0). Recuerda de la sección anterior: el resultado es una copia, no una vista.

10. Transposición y producto matricial

A = np.array([[1, 2],
              [3, 4],
              [5, 6]])

print(A.T)              # transpuesta
print(A * 2)            # producto ELEMENTO a ELEMENTO
print(A.T @ A)          # producto MATRICIAL (con @)

Salida esperada:

[[1 3 5]
 [2 4 6]]
[[ 2  4]
 [ 6  8]
 [10 12]]
[[35 44]
 [44 56]]
  • Transponer (A.T) intercambia filas por columnas: una matriz de 3×2 pasa a 2×3.
  • A * 2 es el producto elemento a elemento.
  • A.T @ A es el producto matricial: cada celda (i, j) es la suma de los productos de la fila i por la columna j. El resultado es 2×2.

No confundas * con @

  • A * B multiplica elemento a elemento y admite broadcasting si las formas son compatibles; no exige siempre la misma forma.
  • A @ B es el producto matricial (filas × columnas; las dimensiones internas deben coincidir).
  • A.T @ A (matriz de Gram, cuadrada y simétrica) aparece muchísimo en ML: resume cómo se relacionan las columnas de A entre sí, y es la base de métodos como la regresión lineal que verás en la Unidad 4.

11. np.where: localizar y condición vectorizada

np.where hace dos cosas según cuántos argumentos le pases. La más habitual es localizar posiciones; la menos habitual es usarlo como condición vectorizada (un if/else aplicado a todo el array).

11.1 Localizar con np.where (lo más habitual)

Si le pasas solo la condición, devuelve las posiciones que la cumplen. Es la forma vectorizada de preguntar "¿dónde están los elementos que cumplen esto?".

ventas = np.array([120, 45, 300, 80, 250])
indices = np.where(ventas >= 100)   # localizar las posiciones
print(indices)
print(ventas[indices])              # y recuperar sus valores

Salida esperada:

(array([0, 2, 4]),)
[120 300 250]

np.where(condición) devuelve una tupla de arrays de índices: en 1D es una tupla con un único array ([0, 2, 4]), que son las posiciones donde la condición es True. Con esos índices puedes recuperar los valores (ventas[indices]), contarlos (len(indices[0]) → 3) o marcar filas en otra estructura.

En un array 2D devuelve dos arrays: uno con las filas y otro con las columnas. Si prefieres directamente los pares (fila, columna), usa np.argwhere(condición).

11.2 Condición vectorizada con np.where (menos habitual)

Con tres argumentos (condición, valor_si_verdad, valor_si_falso), np.where devuelve un array nuevo en el que cada posición toma un valor u otro según se cumpla la condición. Es como un if/else aplicado de una sola vez a todos los elementos, útil para clasificar o sustituir valores sin escribir un bucle:

nivel = np.where(ventas >= 100, "alta", "baja")
print(nivel)

Salida esperada:

['alta' 'baja' 'alta' 'baja' 'alta']

np.where(condición, valor_si_verdad, valor_si_falso) es el "si/entonces" aplicado a todo el array a la vez, sin bucle: cada posición mira su condición y elige uno de los dos valores. Es la versión vectorizada del if/else que usarías en un bucle.

12. Operaciones de conjuntos

hoy = np.array([101, 102, 103, 104])   # productos vistos hoy
ayer = np.array([102, 104, 105])       # productos vistos ayer

print(np.intersect1d(hoy, ayer))       # en ambos días
print(np.union1d(hoy, ayer))           # en alguno de los dos
print(np.setdiff1d(hoy, ayer))         # solo hoy
print(np.isin(hoy, [101, 105]))        # ¿está en la lista?
print(np.setxor1d(hoy, ayer))          # en uno u otro, no en ambos
print(np.unique(np.array([101, 101, 102])))  # sin repetidos

Salida esperada:

[102 104]
[101 102 103 104 105]
[101 103]
[ True False False False]
[101 103 105]
[101 102]

Estas funciones responden a preguntas típicas de negocio: ¿qué productos se compraron ambos días (intersect1d)? ¿cuáles solo hoy (setdiff1d)? ¿está este id en la lista (isin)? Te ahorran escribir bucles de comparación, que son lentos y propensos a errores.

isin, no in1d

Usa np.isin. La función antigua np.in1d está desaconsejada en las versiones modernas de NumPy.

13. Manipular la forma

m = np.arange(12)
print(m.reshape(3, 4))         # 12 elementos → matriz 3x4
print(m.reshape(3, -1).shape)  # -1 = "calcúlalo tú"

Salida esperada:

[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
(3, 4)

reshape reorganiza los mismos datos con otra forma (el número de elementos debe coincidir: 12 = 3×4). El -1 es un comodín: "calcula esta dimensión para que cuadre". Es muy útil cuando sabes cuántas filas (o columnas) quieres y no quieres hacer la cuenta a mano.

ravel() intenta devolver una vista cuando el orden de los datos lo permite; si no puede, crea una copia. flatten() crea siempre una copia:

m2 = np.arange(12).reshape(3, 4)
aplanado = m2.ravel()
aplanado[0] = 999
print(m2[0, 0])   # 999: ravel compartía memoria

m3 = np.arange(12).reshape(3, 4)
copia = m3.flatten()
copia[0] = 999
print(m3[0, 0])   # 0: flatten creó una copia

m4 = np.arange(12).reshape(3, 4)[:, ::2]  # vista no contigua
aplanado_no_contiguo = m4.ravel()
print(np.shares_memory(m4, aplanado_no_contiguo))  # False: ravel tuvo que copiar

Salida esperada:

999
0
False

ravel() comparte memoria cuando devuelve una vista, por lo que modificar su resultado puede modificar el array original. En otras situaciones, como la vista no contigua del ejemplo, devuelve una copia. Si necesitas garantizar un resultado independiente, usa flatten() o ravel().copy().

14. Combinar y dividir

x = np.array([1, 2, 3])
y = np.array([4, 5, 6])

print(np.concatenate([x, y]))   # uno detrás de otro
print(np.vstack([x, y]))        # apilados como filas
print(np.hstack([x, y]))        # apilados como columnas
print(np.split(np.arange(9), 3))  # dividir en 3 trozos iguales

Salida esperada:

[1 2 3 4 5 6]
[[1 2 3]
 [4 5 6]]
[1 2 3 4 5 6]
[array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8])]
  • concatenate pega arrays uno tras otro (misma dimensión).
  • vstack apila verticalmente (cada array se convierte en una fila).
  • hstack apila horizontalmente (columnas).
  • split divide en trozos iguales (el tamaño debe ser divisible).

15. Broadcasting

NumPy permite operar arrays de formas distintas cuando una de las dimensiones es 1 (o coincide). Se "estira" la dimensión pequeña:

mat = np.array([[1, 2, 3],
                [4, 5, 6]])

print(mat + 10)                    # escalar → se aplica a todos
print(mat + np.array([[10], [20]]))  # columna → se suma por filas
print(mat * np.array([1, 10, 100]))  # fila → se multiplica por columnas

Salida esperada:

[[11 12 13]
 [14 15 16]]
[[11 12 13]
 [24 25 26]]
[[  1  20 300]
 [  4  50 600]]

Qué está pasando en cada caso:

  • mat + 10: el escalar 10 se "estira" a la forma de mat y se suma a cada celda.
  • mat + columna (2×1): la columna [10, 20] se repite por las 3 columnas; así, la primera fila suma 10 y la segunda suma 20.
  • mat * fila (3,): la fila [1, 10, 100] se repite por las 2 filas y multiplica cada columna por su factor (columna 0 ×1, columna 1 ×10, columna 2 ×100).

Broadcasting es lo que hace que "sumar una columna a toda la tabla" se escriba en una línea, sin bucles.

La regla

Dos formas son compatibles si, comparadas de derecha a izquierda, cada dimensión es igual o una de las dos es 1. Un escalar es compatible con cualquier forma. Ej.: (2, 3) con (1, 3) ✓; (2, 3) con (3,) se alinea como (1, 3) ✓; (2, 3) con (2,) ✗; (2, 3) con (2, 2) ✗. Si no cuadran, NumPy lanza un error de broadcasting y te toca ajustar las formas.

Errores frecuentes

  • Usar and/or con máscaras en lugar de &/| (y sin paréntesis).
  • Creer que arr[1:4] incluye el índice 4.
  • Confundir * (elemento a elemento) con @ (matricial).
  • Modificar una "copia" pensando que era vista (o al revés).
  • Usar np.random.seed global en lugar de default_rng.
  • Esperar que np.split divida cuando el tamaño no es múltiplo exacto del número de trozos.
  • Olvidar que los dtype enteros truncan los decimales.

Práctica de transferencia

TechShop guarda las ventas de una semana así (filas = días, columnas = [unidades, importe]):

import numpy as np

rng = np.random.default_rng(7)
unidades = rng.integers(10, 100, size=7)
importe = unidades * rng.uniform(20, 80, size=7)
ventas = np.column_stack([unidades, np.round(importe, 2)])
print(ventas)
  1. Calcula el importe medio y el número total de unidades vendidas.
  2. Crea una máscara que seleccione los días con más de 50 unidades y muestra esos días.
  3. Añade una columna nueva con el "precio medio por unidad" de cada día (importe / unidades) usando broadcasting o división vectorizada.
  4. Calcula la media de cada columna con axis=0 y explica qué representa cada resultado.
  5. Comprueba con .base si ventas[:, 0] es vista o copia, y explica la diferencia práctica.

Qué debes comprobar: ventas.shape es (7, 2) y, tras añadir la columna del punto 3, (7, 3); el precio medio por unidad de cada día queda entre 20 y 80, porque así se generaron los datos. Si sale fuera de ese rango, revisa qué columna divides entre cuál.

Producto evaluable

Cuaderno 02_numpy.py con:

  1. Los ejercicios 1–5 anteriores, cada uno con su celda de código y una celda Markdown que interprete el resultado.
  2. Una celda final que genere, con default_rng y una semilla fija, 1000 valores simulados de "importe de pedido" y muestre su media, mediana y desviación típica.

Formato de entrega: cuaderno marimo ejecutado, con conclusiones al final.

Resumen y referencia rápida

Tarea Función/patrón
Crear np.array, zeros, ones, arange, linspace
Inspeccionar shape, ndim, size, dtype
Indexar arr[i], arr[1:4], mat[fila, col]
Filtrar arr[mascara], combinando con &, |, ~
Estadísticas mean, median, std, var, sum, min, max
Aleatorios default_rng(seed), integers, random, normal, uniform
Guardar/cargar np.save / np.load
Copia vs. vista .base (vista → apunta al original; copia → None)
Transponer/multiplicar .T, @ (matricial), * (elemento a elemento)
Localizar / si-entonces vectorizado np.where(cond) / np.where(cond, a, b)
Conjuntos unique, intersect1d, union1d, setdiff1d, isin, setxor1d
Forma reshape, ravel (vista si puede), flatten (copia)
Combinar/dividir concatenate, vstack, hstack, split
Broadcasting operar formas compatibles (dimensión 1 o igual)

Siguiente: 1.5 · Pandas.