Saltar al contenido

Indice / NumPy / 3.2

Vectorización

La idea entera de NumPy en una línea: celsius * 9 / 5 + 32 convierte los seis valores, o los 240, sin que escribas un bucle. Y no es solo velocidad - es que la expresión dice qué se calcula en vez de cómo se recorre.

Para qué sirve

Es el hábito que hay que adquirir antes de pandas, porque pandas está construido sobre él: df["importe"] * 1.21 es exactamente esto. Un alumno que sigue escribiendo for i in range(len(df)) va a pelear con la biblioteca en cada ejercicio.

ELI5Explicado facil

Un bucle es hacer las fotocopias de a una: poner la hoja, apretar el botón, sacarla, repetir.

Una operación vectorizada es poner las cien hojas en el alimentador y apretar una vez. El mismo trabajo, una sola instrucción, y sobre todo: la instrucción dice «copiar todo esto», no «poner, apretar, sacar».

Cómo se ve

Paso a pasoEl bucle paga por elemento; la operacion vectorizada, una vez
  1. Punto de partidaSeis lecturas del sensor S01, en grados Celsius. Hay que pasarlas a Fahrenheit: f = c * 9/5 + 32.
    celsius = lecturas[0, :6]
  2. Vuelta 1 del buclePython toma el elemento 0, hace 20.8 * 9/5 + 32 y guarda 69.4. Despues pasa al siguiente.
    for c in celsius:
        fahrenheit.append(c * 9 / 5 + 32)
  3. Vuelta 2 del buclePython toma el elemento 1, hace 19.8 * 9/5 + 32 y guarda 67.6. Despues pasa al siguiente.
    for c in celsius:
        fahrenheit.append(c * 9 / 5 + 32)
  4. Vuelta 3 del buclePython toma el elemento 2, hace 19.2 * 9/5 + 32 y guarda 66.6. Despues pasa al siguiente.
    for c in celsius:
        fahrenheit.append(c * 9 / 5 + 32)
  5. Vuelta 4 del buclePython toma el elemento 3, hace 19.0 * 9/5 + 32 y guarda 66.2. Despues pasa al siguiente.
    for c in celsius:
        fahrenheit.append(c * 9 / 5 + 32)
  6. Vuelta 5 del buclePython toma el elemento 4, hace 19.6 * 9/5 + 32 y guarda 67.3. Despues pasa al siguiente.
    for c in celsius:
        fahrenheit.append(c * 9 / 5 + 32)
  7. Vuelta 6 del buclePython toma el elemento 5, hace 20.3 * 9/5 + 32 y guarda 68.5. Despues pasa al siguiente.
    for c in celsius:
        fahrenheit.append(c * 9 / 5 + 32)
  8. Lo mismo, vectorizadoUna sola expresion, y NumPy la aplica a los seis valores a la vez. No hay bucle en Python: el recorrido ocurre adentro de NumPy, compilado.
    fahrenheit = celsius * 9 / 5 + 32
  9. Y con 240 lecturas es igualLa misma linea sirve para la grilla entera de 10 sensores por 24 horas. El bucle de Python pagaria 240 vueltas; la expresion vectorizada, una.
    fahrenheit = lecturas * 9 / 5 + 32

Elemento a elemento

EjemploUna expresion, todos los elementos
import numpy as np

lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
                         skip_header=1, usecols=range(2, 26))
print("lecturas:", lecturas.shape, lecturas.dtype)

celsius = lecturas[0, :6]
print()
print("las 6 primeras horas del sensor S01, en Celsius:")
print("  ", celsius)

print()
print("A Fahrenheit, sin escribir un solo bucle:")
print("   celsius * 9 / 5 + 32 ->", np.round(celsius * 9 / 5 + 32, 1))

print()
print("Todos los operadores aritmeticos funcionan igual, elemento a elemento:")
print("   + 10  ->", np.round(celsius + 10, 1))
print("   - 20  ->", np.round(celsius - 20, 1))
print("   * 2   ->", np.round(celsius * 2, 1))
print("   / 2   ->", np.round(celsius / 2, 1))
print("   ** 2  ->", np.round(celsius ** 2, 1))
print("   // 5  ->", celsius // 5)
print("   % 5   ->", np.round(celsius % 5, 1))

print()
print("Y entre DOS arrays del mismo tamano, tambien elemento a elemento:")
manana = lecturas[0, 6:12]
tarde = lecturas[0, 12:18]
print("   manana (h06-h11):", manana)
print("   tarde  (h12-h17):", tarde)
print("   tarde - manana  :", np.round(tarde - manana, 1))
print("   Cada resta usa la lectura de la misma posicion. No hay que")
print("   emparejar nada a mano.")

print()
print("Las funciones de NumPy (las ufunc) hacen lo mismo:")
print("   np.sqrt   ->", np.round(np.sqrt(celsius), 2))
print("   np.abs    ->", np.abs(np.array([-3.0, 2.0, -1.5])))
print("   np.round  ->", np.round(celsius))
print("   np.log    ->", np.round(np.log(celsius), 3))
print("   np.minimum(celsius, 20) ->", np.minimum(celsius, 20))

print()
print("Las comparaciones tambien: devuelven un array de booleanos.")
print("   celsius > 20 ->", celsius > 20)
print("   Ese array booleano es la mascara de la leccion 3.4, y es")
print("   exactamente lo mismo que hace df[df.nota > 7] en pandas.")

print()
print("La regla: si escribiste un for para recorrer un array, casi siempre")
print("hay una expresion que lo reemplaza. Y no es solo velocidad: la")
print("expresion dice QUE se calcula, el bucle dice COMO se recorre.")
lecturas: (10, 24) float64

las 6 primeras horas del sensor S01, en Celsius:
   [20.8 19.8 19.2 19.  19.6 20.3]

A Fahrenheit, sin escribir un solo bucle:
   celsius * 9 / 5 + 32 -> [69.4 67.6 66.6 66.2 67.3 68.5]

Todos los operadores aritmeticos funcionan igual, elemento a elemento:
   + 10  -> [30.8 29.8 29.2 29.  29.6 30.3]
   - 20  -> [ 0.8 -0.2 -0.8 -1.  -0.4  0.3]
   * 2   -> [41.6 39.6 38.4 38.  39.2 40.6]
   / 2   -> [10.4  9.9  9.6  9.5  9.8 10.2]
   ** 2  -> [432.6 392.  368.6 361.  384.2 412.1]
   // 5  -> [4. 3. 3. 3. 3. 4.]
   % 5   -> [0.8 4.8 4.2 4.  4.6 0.3]

Y entre DOS arrays del mismo tamano, tambien elemento a elemento:
   manana (h06-h11): [20.  21.  21.1 21.7 21.9 22.9]
   tarde  (h12-h17): [22.6 23.7 23.9 24.3 24.4 23.3]
   tarde - manana  : [2.6 2.7 2.8 2.6 2.5 0.4]
   Cada resta usa la lectura de la misma posicion. No hay que
   emparejar nada a mano.

Las funciones de NumPy (las ufunc) hacen lo mismo:
   np.sqrt   -> [4.56 4.45 4.38 4.36 4.43 4.51]
   np.abs    -> [3.  2.  1.5]
   np.round  -> [21. 20. 19. 19. 20. 20.]
   np.log    -> [3.035 2.986 2.955 2.944 2.976 3.011]
   np.minimum(celsius, 20) -> [20.  19.8 19.2 19.  19.6 20. ]

Las comparaciones tambien: devuelven un array de booleanos.
   celsius > 20 -> [ True False False False False  True]
   Ese array booleano es la mascara de la leccion 3.4, y es
   exactamente lo mismo que hace df[df.nota > 7] en pandas.

La regla: si escribiste un for para recorrer un array, casi siempre
hay una expresion que lo reemplaza. Y no es solo velocidad: la
expresion dice QUE se calcula, el bucle dice COMO se recorre.
Todo lo que es elemento a elemento
FormaEjemploDevuelve
Array y escalarcelsius * 9 / 5 + 32Un array del mismo tamaño
Array y arraytarde - mananaEmpareja por posición, sin que vos emparejes nada
Funciones (ufunc)np.sqrt, np.abs, np.log, np.minimumUn array del mismo tamaño
Comparacionescelsius > 20Un array de bool - la máscara de la lección 3.4

La regla: si escribiste un for para recorrer un array, casi siempre hay una expresión que lo reemplaza. Y el motivo no es el cronómetro: la expresión dice qué se calcula y el bucle dice cómo se recorre, que es un detalle que a nadie le importa.

reshape

Ejemploreshape: la misma memoria, otra forma
import numpy as np

lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
                         skip_header=1, usecols=range(2, 26))
print("original :", lecturas.shape, "=", lecturas.size, "lecturas")

plano = lecturas.reshape(-1)
print("aplanado :", plano.shape, " <- reshape(-1) siempre da 1 dimension")
print("   primeros 6:", plano[:6])
print("   son las 6 primeras horas de S01: reshape recorre FILA POR FILA.")

print()
por_horas = lecturas.reshape(24, 10)
print("reshape(24, 10):", por_horas.shape)
print("   OJO: esto NO es 'las 24 horas por los 10 sensores'.")
print("   Solo reagrupo los mismos 240 numeros de a 10, en el mismo orden.")
print("   primera fila:", np.round(por_horas[0], 1))
print("   ...que son las 10 primeras horas de S01, no la hora 0 de todos.")

print()
print("Para eso esta la transpuesta, que SI intercambia los ejes:")
transpuesta = lecturas.T
print("   lecturas.T:", transpuesta.shape)
print("   primera fila:", np.round(transpuesta[0], 1))
print("   ahora si: la hora h00 de los 10 sensores.")
print("   Se comprueba contra la columna 0 del original:",
      np.array_equal(transpuesta[0], lecturas[:, 0], equal_nan=True))

print()
print("El -1 significa 'calcula vos esta dimension':")
print("   reshape(-1)      ->", lecturas.reshape(-1).shape)
print("   reshape(5, -1)   ->", lecturas.reshape(5, -1).shape)
print("   reshape(-1, 4)   ->", lecturas.reshape(-1, 4).shape)
print("   Solo puede haber UN -1, y el total tiene que dar justo:")
try:
    lecturas.reshape(7, -1)
except ValueError as error:
    print("   reshape(7, -1) ->", type(error).__name__ + ":", error)

print()
print("Y el detalle que sorprende: reshape devuelve una VISTA, no una copia.")
copia_o_no = lecturas.reshape(24, 10)
copia_o_no[0, 0] = -999
print("   toque copia_o_no[0, 0] = -999")
print("   lecturas[0, 0] ahora vale:", lecturas[0, 0])
print("   Comparten la memoria. Si hace falta independencia: .copy()")
lecturas[0, 0] = 20.8       # dejar el array como estaba

print()
print("Un caso real donde reshape resuelve el problema: pasar la grilla a")
print("formato largo, una fila por (sensor, hora).")
sensores = np.repeat(np.arange(1, 11), 24)
horas = np.tile(np.arange(24), 10)
largo = np.column_stack([sensores, horas, lecturas.reshape(-1)])
print("   shape:", largo.shape)
print("   primeras 3 filas:")
print(largo[:3])
print("   ultimas 2 filas:")
print(largo[-2:])
print("   Es el melt() de la leccion 6.5, en tres lineas de NumPy.")
original : (10, 24) = 240 lecturas
aplanado : (240,)  <- reshape(-1) siempre da 1 dimension
   primeros 6: [20.8 19.8 19.2 19.  19.6 20.3]
   son las 6 primeras horas de S01: reshape recorre FILA POR FILA.

reshape(24, 10): (24, 10)
   OJO: esto NO es 'las 24 horas por los 10 sensores'.
   Solo reagrupo los mismos 240 numeros de a 10, en el mismo orden.
   primera fila: [20.8 19.8 19.2 19.  19.6 20.3 20.  21.  21.1 21.7]
   ...que son las 10 primeras horas de S01, no la hora 0 de todos.

Para eso esta la transpuesta, que SI intercambia los ejes:
   lecturas.T: (24, 10)
   primera fila: [20.8 18.5 17.8 21.2 19.9 19.9 23.3 19.1 21.4 16.1]
   ahora si: la hora h00 de los 10 sensores.
   Se comprueba contra la columna 0 del original: True

El -1 significa 'calcula vos esta dimension':
   reshape(-1)      -> (240,)
   reshape(5, -1)   -> (5, 48)
   reshape(-1, 4)   -> (60, 4)
   Solo puede haber UN -1, y el total tiene que dar justo:
   reshape(7, -1) -> ValueError: cannot reshape array of size 240 into shape (7,newaxis)

Y el detalle que sorprende: reshape devuelve una VISTA, no una copia.
   toque copia_o_no[0, 0] = -999
   lecturas[0, 0] ahora vale: -999.0
   Comparten la memoria. Si hace falta independencia: .copy()

Un caso real donde reshape resuelve el problema: pasar la grilla a
formato largo, una fila por (sensor, hora).
   shape: (240, 3)
   primeras 3 filas:
[[ 1.   0.  20.8]
 [ 1.   1.  19.8]
 [ 1.   2.  19.2]]
   ultimas 2 filas:
[[10.  22.  17. ]
 [10.  23.  16.7]]
   Es el melt() de la leccion 6.5, en tres lineas de NumPy.
reshape(24, 10) no es «las 24 horas por los 10 sensores»

reshape reagrupa los mismos números en el mismo orden, recorriendo fila por fila. La primera fila del resultado son las 10 primeras horas de S01, no la hora h00 de los 10 sensores.

Para intercambiar los ejes de verdad está .T, la transpuesta. Se comprueba en una línea: lecturas.T[0] tiene que ser igual a lecturas[:, 0].

Dos cosas más de reshape

-1 significa «calculá vos esta dimensión», y solo puede haber uno. reshape(5, -1) sobre 240 elementos da (5, 48); reshape(7, -1) es ValueError, porque 240 no es múltiplo de 7.

Devuelve una vista, no una copia. Tocar el resultado modifica el original: si hace falta independencia, .copy(). Es el mismo tema que la lección 3.3, y la causa del SettingWithCopy de pandas.

axis=: el eje que desaparece

Ejemploaxis=: el eje que DESAPARECE
import numpy as np

lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
                         skip_header=1, usecols=range(2, 26))
print("lecturas:", lecturas.shape, " (10 sensores, 24 horas)")

print()
print("Sin axis, la reduccion aplasta TODO a un solo numero:")
print("   np.nanmean(lecturas) ->", round(float(np.nanmean(lecturas)), 2))
print("   shape del resultado  :", np.nanmean(lecturas).shape, "(un escalar)")

print()
print("Con axis, el eje que nombras es el que DESAPARECE:")
por_sensor = np.nanmean(lecturas, axis=1)
por_hora = np.nanmean(lecturas, axis=0)
print("   axis=1 -> desaparecen las 24 horas ->", por_sensor.shape,
      "un promedio POR SENSOR")
print("   axis=0 -> desaparecen los 10 sensores ->", por_hora.shape,
      "un promedio POR HORA")

print()
print("Esa es la regla, y es la unica que hay que recordar:")
print("   (10, 24) con axis=0 -> (24,)")
print("   (10, 24) con axis=1 -> (10,)")
print("   El numero del axis es la POSICION en shape que se elimina.")

print()
print("promedio por sensor (axis=1):")
for i, valor in enumerate(por_sensor, start=1):
    print(f"   S{i:02d}  {valor:6.2f}")

print()
print("promedio por hora (axis=0), las primeras 8:")
print("  ", np.round(por_hora[:8], 2))

print()
print("La forma de saber cual pediste: el resultado tiene que tener tantos")
print("elementos como la dimension que QUEDA.")
print("   10 sensores -> 10 promedios (axis=1)")
print("   24 horas    -> 24 promedios (axis=0)")

print()
print("Y hay una senal barata: el 999.9 imposible esta en S05.")
print("   S05 con axis=1 ->", round(float(por_sensor[4]), 2),
      "  <- salta a la vista")
print("   los otros nueve estan entre",
      round(float(np.delete(por_sensor, 4).min()), 2), "y",
      round(float(np.delete(por_sensor, 4).max()), 2))
print("   Un promedio por eje es tambien una herramienta de diagnostico.")

print()
print("Las reducciones que mas se usan, todas aceptan axis:")
for nombre, funcion in (("sum", np.nansum), ("mean", np.nanmean),
                        ("min", np.nanmin), ("max", np.nanmax),
                        ("std", np.nanstd)):
    resultado = funcion(lecturas, axis=1)
    print(f"   np.nan{nombre:<5}(a, axis=1) -> shape {resultado.shape}, "
          f"primer valor {resultado[0]:.2f}")
lecturas: (10, 24)  (10 sensores, 24 horas)

Sin axis, la reduccion aplasta TODO a un solo numero:
   np.nanmean(lecturas) -> 24.91
   shape del resultado  : () (un escalar)

Con axis, el eje que nombras es el que DESAPARECE:
   axis=1 -> desaparecen las 24 horas -> (10,) un promedio POR SENSOR
   axis=0 -> desaparecen los 10 sensores -> (24,) un promedio POR HORA

Esa es la regla, y es la unica que hay que recordar:
   (10, 24) con axis=0 -> (24,)
   (10, 24) con axis=1 -> (10,)
   El numero del axis es la POSICION en shape que se elimina.

promedio por sensor (axis=1):
   S01   21.73
   S02   19.62
   S03   18.75
   S04   21.63
   S05   60.65
   S06   21.08
   S07   24.55
   S08   20.04
   S09   22.78
   S10   17.53

promedio por hora (axis=0), las primeras 8:
   [19.8  18.83 18.29 17.93 18.27 18.89 19.17 20.12]

La forma de saber cual pediste: el resultado tiene que tener tantos
elementos como la dimension que QUEDA.
   10 sensores -> 10 promedios (axis=1)
   24 horas    -> 24 promedios (axis=0)

Y hay una senal barata: el 999.9 imposible esta en S05.
   S05 con axis=1 -> 60.65   <- salta a la vista
   los otros nueve estan entre 17.53 y 24.55
   Un promedio por eje es tambien una herramienta de diagnostico.

Las reducciones que mas se usan, todas aceptan axis:
   np.nansum  (a, axis=1) -> shape (10,), primer valor 521.60
   np.nanmean (a, axis=1) -> shape (10,), primer valor 21.73
   np.nanmin  (a, axis=1) -> shape (10,), primer valor 19.00
   np.nanmax  (a, axis=1) -> shape (10,), primer valor 24.40
   np.nanstd  (a, axis=1) -> shape (10,), primer valor 1.57

Es la fuente número uno de confusión de NumPy y se resuelve con una sola regla:

Sobre una grilla (10, 24) de 10 sensores × 24 horas
ExpresiónShape del resultadoQué calculó
np.nanmean(a)()Un solo número: el promedio de las 240
np.nanmean(a, axis=0)(24,)Un promedio por hora: desaparecieron los sensores
np.nanmean(a, axis=1)(10,)Un promedio por sensor: desaparecieron las horas

El número del axis es la posición de shape que se elimina. Y la forma de verificar que pediste lo que querías: el resultado tiene tantos elementos como la dimensión que queda.

Un promedio por eje es también un diagnóstico

Los promedios por sensor dan entre 17,53 y 24,55 - salvo S05, que da 60,65. Ahí está el 999.9 imposible, repartido entre sus 24 horas.

Antes de escribir un validador, una reducción por eje ya señala dónde mirar.

Cuándo el bucle sigue haciendo falta

EjemploEl mismo calculo, con bucle y sin bucle
import numpy as np

lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
                         skip_header=1, usecols=range(2, 26))

# TAREA: para cada sensor, cuantas horas estuvo por encima de 21 grados,
# ignorando las lecturas ausentes y la imposible.

VALIDO_MAX = 50.0
UMBRAL = 21.0

# --- Version con bucles ------------------------------------------------
con_bucle = []
for fila in range(lecturas.shape[0]):
    cuantas = 0
    for columna in range(lecturas.shape[1]):
        valor = lecturas[fila, columna]
        if np.isnan(valor):
            continue
        if valor > VALIDO_MAX:
            continue
        if valor > UMBRAL:
            cuantas += 1
    con_bucle.append(cuantas)
con_bucle = np.array(con_bucle)

# --- Version vectorizada ------------------------------------------------
usable = ~np.isnan(lecturas) & (lecturas <= VALIDO_MAX)
sin_bucle = ((lecturas > UMBRAL) & usable).sum(axis=1)

print("con bucle  :", con_bucle)
print("sin bucle  :", sin_bucle)
print("¿iguales?  :", np.array_equal(con_bucle, sin_bucle))

print()
print("12 lineas contra 2, y el mismo resultado. Pero lo que importa no es")
print("la cantidad de lineas: es que la version de abajo se LEE.")
print("   usable      = no es NaN Y no supera el maximo posible")
print("   sin_bucle   = de las usables, cuantas superan el umbral, por fila")

print()
print("El detalle que hace que ande: sumar booleanos.")
mascara = (lecturas > UMBRAL) & usable
print("   dtype de la mascara:", mascara.dtype)
print("   mascara.sum(axis=1) cuenta los True, porque True vale 1.")
print("   int(True) ->", int(True), "| int(False) ->", int(False))

print()
print("Y NO siempre se puede vectorizar. El caso tipico es cuando cada")
print("elemento depende del ANTERIOR:")
serie = np.array([3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0])
maximo_hasta_aca = []
tope = -np.inf
for valor in serie:
    tope = max(tope, valor)
    maximo_hasta_aca.append(tope)
print("   serie              :", serie)
print("   maximo acumulado   :", np.array(maximo_hasta_aca))
print("   ...pero NumPy tambien tiene eso, y se llama acumulador:")
print("   np.maximum.accumulate ->", np.maximum.accumulate(serie))
print("   np.cumsum             ->", np.cumsum(serie))
print("   Antes de escribir el bucle, conviene buscar si ya existe.")
con bucle  : [16  8  1 14  1 12 24  7 17  0]
sin bucle  : [16  8  1 14  1 12 24  7 17  0]
¿iguales?  : True

12 lineas contra 2, y el mismo resultado. Pero lo que importa no es
la cantidad de lineas: es que la version de abajo se LEE.
   usable      = no es NaN Y no supera el maximo posible
   sin_bucle   = de las usables, cuantas superan el umbral, por fila

El detalle que hace que ande: sumar booleanos.
   dtype de la mascara: bool
   mascara.sum(axis=1) cuenta los True, porque True vale 1.
   int(True) -> 1 | int(False) -> 0

Y NO siempre se puede vectorizar. El caso tipico es cuando cada
elemento depende del ANTERIOR:
   serie              : [3. 1. 4. 1. 5. 9. 2.]
   maximo acumulado   : [3. 3. 4. 4. 5. 9. 9.]
   ...pero NumPy tambien tiene eso, y se llama acumulador:
   np.maximum.accumulate -> [3. 3. 4. 4. 5. 9. 9.]
   np.cumsum             -> [ 3.  4.  8.  9. 14. 23. 25.]
   Antes de escribir el bucle, conviene buscar si ya existe.

Las dos versiones dan [16, 8, 1, 14, 1, 12, 24, 7, 17, 0]: doce líneas contra dos. Y el truco que hace andar a la corta es que sumar booleanos cuenta los True, porque int(True) es 1.

Vectorizar no siempre se puede: cuando cada elemento depende del anterior, hay una dependencia real. Pero antes de escribir ese bucle conviene buscar, porque NumPy ya trae los casos frecuentes:

Lo que querías hacer con un bucleYa existe
Suma acumuladanp.cumsum(a)
Producto acumuladonp.cumprod(a)
Máximo hasta acánp.maximum.accumulate(a)
Diferencia con el anteriornp.diff(a)
Correr los valores un lugarnp.roll(a, 1)
Contar cuántos cumplen(a > x).sum()

Dónde se rompe

ValueError: cannot reshape array of size 240 into shape (7,newaxis)

Causa: el total no es divisible por la dimensión que fijaste.

Arreglo: a.size tiene que ser múltiplo. Si no lo es, el problema es de los datos, no del reshape.

ValueError: operands could not be broadcast together with shapes (10,24) (10,)

Causa: dos arrays de formas incompatibles en una operación elemento a elemento.

Arreglo: es el tema de la lección 3.5. La solución casi siempre es reshape(-1, 1) en el más chico.

(sin error) el promedio da nan

Causa: hay al menos un NaN en el eje que se está reduciendo.

Arreglo: np.nanmean en vez de np.mean. Es la lección 3.6.

(sin error) modificar el reshape cambió el original

Causa: reshape devuelve una vista: los dos arrays comparten la memoria.

Arreglo: .copy() si necesitás independencia. Y a.base is None dice si un array es dueño de sus datos.

(sin error) el resultado tiene la cantidad de elementos equivocada

Causa: confundiste axis=0 con axis=1.

Arreglo: mirá el shape del resultado antes de seguir. Si esperabas 10 promedios y tenés 24, invertí el eje.

Recetario

Vectorización
ExpresiónQué haceCuándo
a * 2 + 1Elemento a elementoSiempre, en vez de un bucle
a - bEmpareja por posiciónDos arrays del mismo shape
np.sqrt(a), np.abs(a), np.log(a)Funciones vectorizadasEn vez de math.sqrt en un bucle
np.minimum(a, 20)El menor entre cada valor y 20Recortar por arriba
a.reshape(f, c)Otra forma, misma memoriaReorganizar
a.reshape(-1)Aplanar a una dimensiónAntes de un column_stack
a.TIntercambia los ejesCuando reshape no es lo que querés
a.sum(axis=0)Elimina las filasUn total por columna
a.mean(axis=1)Elimina las columnasUn promedio por fila
(a > x).sum(axis=1)Cuenta por filaSumar booleanos es contar
np.cumsum(a), np.diff(a)Acumular, diferenciarAntes de escribir un bucle secuencial
np.column_stack([a, b, c])Pega columnasArmar una tabla desde vectores
np.repeat / np.tileRepite valores / repite el patrónConstruir las claves de una tabla larga

Ponete a prueba

1. a tiene shape (10, 24). ¿Qué shape tiene a.mean(axis=1)?Nivel 1 · Recordar
2. ¿Qué devuelve celsius > 20 si celsius es un array de 6 floats?Nivel 1 · Recordar
3. Sobre una grilla (10, 24) de 10 sensores × 24 horas, ¿qué hace a.reshape(24, 10)?Nivel 2 · Predecir la salida
4. ¿Por qué (a > 21).sum(axis=1) cuenta cuántas horas superó cada sensor?Nivel 2 · Predecir la salida
5. Hay que contar, por sensor, las horas por encima de 21 °C ignorando NaN y la lectura imposible de 999.9. Las cuatro corren: ¿cuál conviene?Nivel 3 · Elegir entre dos casi correctas
6. Los promedios por sensor dan entre 17,5 y 24,5, salvo uno que da 60,65. ¿Qué conviene concluir?Nivel 3 · Elegir entre dos casi correctas
7. Este código normaliza cada fila restándole su promedio, corre sin error, y el resultado no tiene promedio cero por fila. ¿Por qué?
a = np.genfromtxt("sensores.csv", delimiter=",", skip_header=1,
                  usecols=range(2, 26))       # shape (10, 24)
centrado = a - a.mean(axis=1, keepdims=True)
Nivel 4 · Encontrar el defecto

Prácticas de esta subunidad