Saltar al contenido

Indice / NumPy / 3.4

Máscaras booleanas

La lección bisagra del curso. df[df.nota_1 >= 7] de la unidad 4 es esto, con otro nombre para las filas: una comparación que produce un array de True y False, y ese array usado como índice.

Para qué sirve

Filtrar es la operación más frecuente de todo el análisis de datos, y en NumPy y en pandas se hace de la misma manera. Los tres errores que produce - and en vez de &, los paréntesis que faltan, y el NaN que se descarta solo - también son los mismos.

ELI5Explicado facil

Es un sello de «pasa» o «no pasa» puesto en cada fila, y después la orden de dejar entrar solo a las selladas.

Lo importante es que son dos cosas separadas: primero se sella todo, después se filtra. El sello se puede mirar, contar y guardar sin filtrar nada - y eso es justamente lo que lo hace tan útil.

Los tres pasos

EjemploDe una comparacion a un filtro
import numpy as np

lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
                         skip_header=1, usecols=range(2, 26))
s01 = lecturas[0]
print("las 24 lecturas de S01:")
print(np.round(s01, 1))

print()
print("PASO 1 - la comparacion devuelve un array de booleanos del MISMO largo:")
calido = s01 > 22
print("   s01 > 22 ->")
print(calido)
print("   dtype:", calido.dtype, "| shape:", calido.shape)

print()
print("PASO 2 - ese array booleano se usa como indice:")
print("   s01[s01 > 22] ->", s01[calido])
print("   Se quedan solo las posiciones donde la mascara dice True.")
print("   shape:", s01[calido].shape, " <- ya NO son 24")

print()
print("PASO 3 - y sumarla cuenta, porque True vale 1:")
print("   calido.sum()          ->", calido.sum())
print("   np.count_nonzero()    ->", np.count_nonzero(calido))
print("   calido.mean()         ->", round(float(calido.mean()), 3),
      " <- la PROPORCION")
print("   Ese ultimo es el truco mas util: el promedio de una mascara es")
print("   el porcentaje de filas que cumplen.")

print()
print("Sobre la grilla entera funciona igual, y la mascara es 2D:")
mascara = lecturas > 22
print("   (lecturas > 22).shape ->", mascara.shape)
print("   total de lecturas > 22:", mascara.sum())
print("   por sensor (axis=1)   :", mascara.sum(axis=1))
print("   por hora   (axis=0)   :", mascara.sum(axis=0))

print()
print("Y el filtrado sobre una grilla APLANA el resultado:")
print("   lecturas[lecturas > 22].shape ->", lecturas[mascara].shape)
print("   Tiene sentido: las que cumplen no forman un rectangulo.")

print()
print("Un detalle que salva horas: NaN no cumple NINGUNA comparacion.")
print("   np.nan > 0   ->", np.nan > 0)
print("   np.nan < 0   ->", np.nan < 0)
print("   np.nan == np.nan ->", np.nan == np.nan)
print("   Asi que una mascara descarta los ausentes sin que lo pidas...")
print("   ...y eso puede ser lo que queres, o puede ser un dato perdido")
print("   en silencio. Para preguntarlo explicitamente: np.isnan(a).")
print("   NaN en la grilla:", int(np.isnan(lecturas).sum()))
las 24 lecturas de S01:
[20.8 19.8 19.2 19.  19.6 20.3 20.  21.  21.1 21.7 21.9 22.9 22.6 23.7
 23.9 24.3 24.4 23.3 23.2 22.3 22.4 21.9 21.2 21.1]

PASO 1 - la comparacion devuelve un array de booleanos del MISMO largo:
   s01 > 22 ->
[False False False False False False False False False False False  True
  True  True  True  True  True  True  True  True  True False False False]
   dtype: bool | shape: (24,)

PASO 2 - ese array booleano se usa como indice:
   s01[s01 > 22] -> [22.9 22.6 23.7 23.9 24.3 24.4 23.3 23.2 22.3 22.4]
   Se quedan solo las posiciones donde la mascara dice True.
   shape: (10,)  <- ya NO son 24

PASO 3 - y sumarla cuenta, porque True vale 1:
   calido.sum()          -> 10
   np.count_nonzero()    -> 10
   calido.mean()         -> 0.417  <- la PROPORCION
   Ese ultimo es el truco mas util: el promedio de una mascara es
   el porcentaje de filas que cumplen.

Sobre la grilla entera funciona igual, y la mascara es 2D:
   (lecturas > 22).shape -> (10, 24)
   total de lecturas > 22: 66
   por sensor (axis=1)   : [10  3  0  8  1  7 20  3 14  0]
   por hora   (axis=0)   : [1 1 0 0 0 0 1 1 2 2 2 6 5 5 6 7 6 6 5 3 3 1 2 1]

Y el filtrado sobre una grilla APLANA el resultado:
   lecturas[lecturas > 22].shape -> (66,)
   Tiene sentido: las que cumplen no forman un rectangulo.

Un detalle que salva horas: NaN no cumple NINGUNA comparacion.
   np.nan > 0   -> False
   np.nan < 0   -> False
   np.nan == np.nan -> False
   Asi que una mascara descarta los ausentes sin que lo pidas...
   ...y eso puede ser lo que queres, o puede ser un dato perdido
   en silencio. Para preguntarlo explicitamente: np.isnan(a).
   NaN en la grilla: 4
De la comparación al número
PasoExpresiónDevuelve
1. Comparars01 > 22Un array de bool, del mismo largo
2. Filtrars01[s01 > 22]Solo los que cumplen. Ya no son 24
3a. Contar(s01 > 22).sum()Cuántos cumplen, porque True vale 1
3b. Proporción(s01 > 22).mean()Qué fracción cumple: el truco más útil de todos

Sobre una grilla la máscara es 2D y se puede reducir por eje: (lecturas > 22).sum(axis=1) da cuántas horas cálidas tuvo cada sensor. El filtrado, en cambio, aplana - y tiene sentido, porque las celdas que cumplen no forman un rectángulo.

NaN no cumple ninguna comparación

np.nan > 0, np.nan < 0 y hasta np.nan == np.nan son los tres False. Así que una máscara descarta los ausentes sin que se lo pidas.

A veces eso es exactamente lo que querías. A veces es un dato que se pierde en silencio y nadie cuenta. La diferencia entre las dos situaciones es si lo escribiste: np.isnan(a) pregunta explícitamente.

Combinar condiciones

EjemploCombinar condiciones: & | ~ y los parentesis
import numpy as np

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

print("Los tres operadores de mascaras son simbolos, no palabras:")
print("   &  Y     |  O     ~  NO")

templado = (s01 > 20) & (s01 < 22)
print()
print("templado = (s01 > 20) & (s01 < 22)")
print("   cuantas:", templado.sum(), "de", s01.size)
print("   valores:", np.round(s01[templado], 1))

extremo = (s01 <= 20) | (s01 >= 22)
print()
print("extremo = (s01 <= 20) | (s01 >= 22)")
print("   cuantas:", extremo.sum())

print()
print("~ invierte la mascara:")
print("   (~templado).sum() ->", (~templado).sum(), "= 24 -", templado.sum())
print("   ¿~templado es igual a extremo?",
      np.array_equal(~templado, extremo))
print("   Si, y no es casualidad: negar un Y da un O con las dos")
print("   condiciones negadas. NO(a>20 Y a<22) = (a<=20) O (a>=22).")
print("   Se llaman leyes de De Morgan, y sirven para no escribir")
print("   condiciones dobles cuando alcanza con negar una.")

print()
print("=" * 62)
print("LOS DOS ERRORES QUE TODO EL MUNDO COMETE")
print("=" * 62)

print()
print("1. Usar 'and' en vez de '&':")
try:
    s01 > 20 and s01 < 22
except ValueError as error:
    print("   s01 > 20 and s01 < 22 ->", type(error).__name__)
    print("   ", error)
print("   'and' necesita un solo True o False para decidir, y le estas")
print("   dando 24. NumPy se niega a adivinar cual queres.")

print()
print("2. Olvidar los parentesis:")
try:
    s01 > 20 & s01 < 22
except (ValueError, TypeError) as error:
    print("   s01 > 20 & s01 < 22 ->", type(error).__name__)
    print("   ", str(error)[:70])
print("   '&' tiene MAS precedencia que '>' y '<', asi que Python lee")
print("   s01 > (20 & s01) < 22. Los parentesis no son estilo: son")
print("   obligatorios.")

print()
print("La forma correcta, siempre:")
print("   (condicion_a) & (condicion_b)")
print("   (condicion_a) | (condicion_b)")
print("   ~(condicion_a)")

print()
print("Y la variante que evita el problema cuando hay muchas condiciones:")
condiciones = [s01 > 20, s01 < 22, ~np.isnan(s01)]
todas = np.logical_and.reduce(condiciones)
print("   np.logical_and.reduce([a, b, c]) ->", todas.sum(), "cumplen las 3")
print("   np.logical_and(a, b)  es lo mismo que  a & b")
print("   np.logical_or(a, b)   es lo mismo que  a | b")
print("   np.logical_not(a)     es lo mismo que  ~a")

print()
print("Sobre la grilla, combinar es igual de barato:")
usable = ~np.isnan(lecturas) & (lecturas <= 50)
print("   usable = ~np.isnan(lecturas) & (lecturas <= 50)")
print("   lecturas usables:", usable.sum(), "de", lecturas.size)
print("   descartadas     :", lecturas.size - usable.sum(),
      "= 4 ausentes + 1 imposible")
Los tres operadores de mascaras son simbolos, no palabras:
   &  Y     |  O     ~  NO

templado = (s01 > 20) & (s01 < 22)
   cuantas: 9 de 24
   valores: [20.8 20.3 21.  21.1 21.7 21.9 21.9 21.2 21.1]

extremo = (s01 <= 20) | (s01 >= 22)
   cuantas: 15

~ invierte la mascara:
   (~templado).sum() -> 15 = 24 - 9
   ¿~templado es igual a extremo? True
   Si, y no es casualidad: negar un Y da un O con las dos
   condiciones negadas. NO(a>20 Y a<22) = (a<=20) O (a>=22).
   Se llaman leyes de De Morgan, y sirven para no escribir
   condiciones dobles cuando alcanza con negar una.

==============================================================
LOS DOS ERRORES QUE TODO EL MUNDO COMETE
==============================================================

1. Usar 'and' en vez de '&':
   s01 > 20 and s01 < 22 -> ValueError
    The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
   'and' necesita un solo True o False para decidir, y le estas
   dando 24. NumPy se niega a adivinar cual queres.

2. Olvidar los parentesis:
   s01 > 20 & s01 < 22 -> TypeError
    ufunc 'bitwise_and' not supported for the input types, and the inputs 
   '&' tiene MAS precedencia que '>' y '<', asi que Python lee
   s01 > (20 & s01) < 22. Los parentesis no son estilo: son
   obligatorios.

La forma correcta, siempre:
   (condicion_a) & (condicion_b)
   (condicion_a) | (condicion_b)
   ~(condicion_a)

Y la variante que evita el problema cuando hay muchas condiciones:
   np.logical_and.reduce([a, b, c]) -> 9 cumplen las 3
   np.logical_and(a, b)  es lo mismo que  a & b
   np.logical_or(a, b)   es lo mismo que  a | b
   np.logical_not(a)     es lo mismo que  ~a

Sobre la grilla, combinar es igual de barato:
   usable = ~np.isnan(lecturas) & (lecturas <= 50)
   lecturas usables: 235 de 240
   descartadas     : 5 = 4 ausentes + 1 imposible
Los dos errores que todo el mundo comete

1. and en vez de &. ValueError: The truth value of an array with more than one element is ambiguous. and necesita un solo True o False para decidir, y le estás dando 24: NumPy se niega a adivinar.

2. Los paréntesis. & tiene más precedencia que > y <, así que a > 20 & a < 22 se lee a > (20 & a) < 22 y da TypeError. No son estilo: son obligatorios.

Se escribeSignificaAlternativa
(a) & (b)Ynp.logical_and(a, b)
(a) | (b)Onp.logical_or(a, b)
~(a)NOnp.logical_not(a)
np.logical_and.reduce([a, b, c]) para muchas condiciones a la vez
De Morgan, que ahorra paréntesis

Negar un Y da un O con las dos condiciones negadas: ~((a>20) & (a<22)) es lo mismo que (a<=20) | (a>=22).

Sirve sobre todo para lo contrario: cuando una condición doble se pone difícil de leer, muchas veces alcanza con negar la simple.

Decidir sin filtrar

Ejemplonp.where, any, all: decidir sin filtrar
import numpy as np

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

print("np.where(condicion, si, no) elige valor por valor:")
etiquetas = np.where(s01 > 22, "calido", "templado")
print("   np.where(s01 > 22, 'calido', 'templado'), primeros 8:")
print("  ", etiquetas[:8])
print("   Devuelve un array del MISMO largo: no filtra, reemplaza.")

print()
print("Y es la forma de limpiar sin perder filas:")
limpias = np.where(lecturas > 50, np.nan, lecturas)
print("   np.where(lecturas > 50, np.nan, lecturas)")
print("   shape:", limpias.shape, " <- las 240 siguen ahi")
print("   NaN antes :", int(np.isnan(lecturas).sum()))
print("   NaN despues:", int(np.isnan(limpias).sum()),
      " <- el 999.9 paso a ser un ausente declarado")

print()
print("Se pueden encadenar, aunque anidados se leen mal:")
tres = np.where(s01 < 20, "frio", np.where(s01 < 22, "templado", "calido"))
print("   primeros 8:", tres[:8])

print()
print("=" * 62)
print("any y all: contestan SI o NO sobre todo el array")
print("=" * 62)
print("   np.isnan(lecturas).any() ->", np.isnan(lecturas).any(),
      "  ¿hay ALGUN ausente?")
print("   np.isnan(lecturas).all() ->", np.isnan(lecturas).all(),
      " ¿son TODOS ausentes?")
print("   (lecturas > 0).all()     ->", (lecturas > 0).all(),
      " ¿son TODAS positivas? (los NaN dicen que no)")
print("   (~np.isnan(lecturas)).all() ->", (~np.isnan(lecturas)).all())

print()
print("Con axis contestan por fila o por columna:")
print("   np.isnan(lecturas).any(axis=1) ->", np.isnan(lecturas).any(axis=1))
print("   ...los sensores con al menos un hueco: S03, S06 y S09")
con_hueco = np.nonzero(np.isnan(lecturas).any(axis=1))[0]
print("   posiciones:", con_hueco, "-> S03, S06, S09")

print()
print("Son la herramienta de los assert de invariantes:")
print("   assert (salida['edad'] >= 18).all()")
print("   assert not salida.isna().any().any()")
print("   Un any/all que falla dice que una regla se rompio, y eso es")
print("   mucho mejor que descubrirlo en el informe.")

print()
print("Y las tres que dicen DONDE:")
print("   np.nonzero(s01 > 23)  ->", np.nonzero(s01 > 23)[0],
      " (las posiciones)")
print("   np.argmax(s01)        ->", np.argmax(s01),
      " (la posicion del maximo)")
print("   np.nanargmax(lecturas) ->", np.nanargmax(lecturas),
      " (en el array aplanado)")
print("   np.unravel_index(np.nanargmax(lecturas), lecturas.shape) ->",
      np.unravel_index(np.nanargmax(lecturas), lecturas.shape),
      " <- fila 4, hora 11: el 999.9")
np.where(condicion, si, no) elige valor por valor:
   np.where(s01 > 22, 'calido', 'templado'), primeros 8:
   ['templado' 'templado' 'templado' 'templado' 'templado' 'templado'
 'templado' 'templado']
   Devuelve un array del MISMO largo: no filtra, reemplaza.

Y es la forma de limpiar sin perder filas:
   np.where(lecturas > 50, np.nan, lecturas)
   shape: (10, 24)  <- las 240 siguen ahi
   NaN antes : 4
   NaN despues: 5  <- el 999.9 paso a ser un ausente declarado

Se pueden encadenar, aunque anidados se leen mal:
   primeros 8: ['templado' 'frio' 'frio' 'frio' 'frio' 'templado' 'templado' 'templado']

==============================================================
any y all: contestan SI o NO sobre todo el array
==============================================================
   np.isnan(lecturas).any() -> True   ¿hay ALGUN ausente?
   np.isnan(lecturas).all() -> False  ¿son TODOS ausentes?
   (lecturas > 0).all()     -> False  ¿son TODAS positivas? (los NaN dicen que no)
   (~np.isnan(lecturas)).all() -> False

Con axis contestan por fila o por columna:
   np.isnan(lecturas).any(axis=1) -> [False False  True False False  True False False  True False]
   ...los sensores con al menos un hueco: S03, S06 y S09
   posiciones: [2 5 8] -> S03, S06, S09

Son la herramienta de los assert de invariantes:
   assert (salida['edad'] >= 18).all()
   assert not salida.isna().any().any()
   Un any/all que falla dice que una regla se rompio, y eso es
   mucho mejor que descubrirlo en el informe.

Y las tres que dicen DONDE:
   np.nonzero(s01 > 23)  -> [13 14 15 16 17 18]  (las posiciones)
   np.argmax(s01)        -> 16  (la posicion del maximo)
   np.nanargmax(lecturas) -> 107  (en el array aplanado)
   np.unravel_index(np.nanargmax(lecturas), lecturas.shape) -> (np.int64(4), np.int64(11))  <- fila 4, hora 11: el 999.9
Qué hacer con una máscara, además de filtrar
ExpresiónDevuelvePara qué
np.where(cond, si, no)Un array del mismo largoReemplazar sin perder filas
mask.any()Un solo bool¿Hay alguno?
mask.all()Un solo bool¿Cumplen todos? - los assert de invariantes
mask.any(axis=1)Un bool por filaQué filas tienen algún problema
np.nonzero(mask)Las posicionesSaber dónde
np.unravel_index(np.nanargmax(a), a.shape)La coordenada del máximoEncontrar el atípico: (4, 11)

np.where es la herramienta para limpiar sin descartar: np.where(lecturas > 50, np.nan, lecturas) convierte la lectura imposible en un ausente declarado y deja las 240 celdas en su lugar. De 4 NaN pasa a 5, y ahora el defecto está marcado en vez de escondido.

Y esto ya es pandas

EjemploLa misma mascara, en NumPy y en pandas
import numpy as np
import pandas as pd

# TAREA: las lecturas usables (no ausentes, no imposibles) que superan
# los 22 grados, contadas por sensor.

VALIDO_MAX = 50.0
UMBRAL = 22.0

# ---------------------------------------------------------------- NumPy
grilla = np.genfromtxt("numpy_sensores.csv", delimiter=",",
                       skip_header=1, usecols=range(2, 26))
usable_np = ~np.isnan(grilla) & (grilla <= VALIDO_MAX)
calidas_np = ((grilla > UMBRAL) & usable_np).sum(axis=1)

# --------------------------------------------------------------- pandas
tabla = pd.read_csv("numpy_sensores.csv")
horas = [c for c in tabla.columns if c.startswith("h")]
valores = tabla[horas]
usable_pd = valores.notna() & (valores <= VALIDO_MAX)
calidas_pd = ((valores > UMBRAL) & usable_pd).sum(axis=1)

print("NumPy :", calidas_np)
print("pandas:", calidas_pd.to_numpy())
print("¿iguales?", np.array_equal(calidas_np, calidas_pd.to_numpy()))

print()
print("Las dos lineas que importan, una al lado de la otra:")
print("   NumPy : ~np.isnan(g) & (g <= 50)")
print("   pandas: valores.notna() & (valores <= 50)")
print()
print("   NumPy : ((g > 22) & usable).sum(axis=1)")
print("   pandas: ((valores > 22) & usable).sum(axis=1)")
print()
print("Es la MISMA idea con otro nombre para 'no es ausente'. Todo lo")
print("demas (el &, los parentesis, sumar booleanos, el axis) es igual,")
print("porque pandas usa NumPy por debajo.")

print()
print("Y la version con nombres, que es lo que agrega pandas:")
resumen = pd.DataFrame({
    "sensor": tabla["sensor_id"],
    "ubicacion": tabla["ubicacion"],
    "horas_calidas": calidas_pd,
    "horas_usables": usable_pd.sum(axis=1),
})
print(resumen.to_string(index=False))

print()
print("Las tres filas con menos horas usables son S03 (2 huecos), S06 y")
print("S09 (uno cada uno), mas S05, que pierde su lectura imposible.")
print()
print("El paso de NumPy a pandas no es aprender otra cosa: es que las")
print("filas y columnas pasan a tener NOMBRE. La mascara es la misma.")
NumPy : [10  3  0  8  0  7 20  3 14  0]
pandas: [10  3  0  8  0  7 20  3 14  0]
¿iguales? True

Las dos lineas que importan, una al lado de la otra:
   NumPy : ~np.isnan(g) & (g <= 50)
   pandas: valores.notna() & (valores <= 50)

   NumPy : ((g > 22) & usable).sum(axis=1)
   pandas: ((valores > 22) & usable).sum(axis=1)

Es la MISMA idea con otro nombre para 'no es ausente'. Todo lo
demas (el &, los parentesis, sumar booleanos, el axis) es igual,
porque pandas usa NumPy por debajo.

Y la version con nombres, que es lo que agrega pandas:
sensor       ubicacion  horas_calidas  horas_usables
   S01        deposito             10             24
   S02 sala_servidores              3             24
   S03         oficina              0             22
   S04     planta_baja              8             24
   S05     laboratorio              0             23
   S06        deposito              7             23
   S07 sala_servidores             20             24
   S08         oficina              3             24
   S09     planta_baja             14             23
   S10     laboratorio              0             24

Las tres filas con menos horas usables son S03 (2 huecos), S06 y
S09 (uno cada uno), mas S05, que pierde su lectura imposible.

El paso de NumPy a pandas no es aprender otra cosa: es que las
filas y columnas pasan a tener NOMBRE. La mascara es la misma.
La misma tarea, las mismas ideas
NumPypandas
No es ausente~np.isnan(g)valores.notna()
Combinar&, con paréntesisIgual
Contar por fila.sum(axis=1)Igual
Filtrara[mask]df[mask]
Lo que agrega-Que las filas y columnas tengan nombre

El paso de NumPy a pandas no es aprender otra cosa. La máscara es la misma, porque pandas usa NumPy por debajo: lo que cambia es que lecturas[4, 11] pasa a llamarse tabla.loc["S05", "h11"].

Dónde se rompe

ValueError: The truth value of an array with more than one element is ambiguous

Causa: usaste and, or, not, o pusiste una máscara en un if.

Arreglo: &, |, ~ para combinar máscaras; .any() o .all() si lo que querías era una sola respuesta.

TypeError: ufunc 'bitwise_and' not supported for the input types

Causa: faltan los paréntesis y & se aplicó a los números en vez de a las máscaras.

Arreglo: (a > 20) & (a < 22).

IndexError: boolean index did not match indexed array

Causa: la máscara tiene otro largo que el array - casi siempre porque se construyó antes de un filtrado.

Arreglo: las máscaras se construyen sobre el array que se va a indexar, y no sobreviven a un filtrado anterior.

(sin error) el filtro descartó más filas de las esperadas

Causa: había NaN, y NaN no cumple ninguna comparación.

Arreglo: contá primero: np.isnan(a).sum(). Después decidí si los ausentes se descartan o se cuentan aparte.

(sin error) asigné sobre el filtrado y no cambió nada

Causa: a[mask] devuelve una copia.

Arreglo: a[mask] = valor en una sola expresión, no sub = a[mask] y después sub[:] = valor.

Recetario

Máscaras
ExpresiónQué hace
a > 22La máscara, del mismo largo que a
a[a > 22]Los valores que cumplen (una copia)
a[a > 22] = 0Los reemplaza en el original
(a > 22).sum()Cuántos cumplen
(a > 22).mean()Qué proporción cumple
(a > 22).sum(axis=1)Cuántos por fila
(x > 1) & (x < 5)Y - con paréntesis
~maskLa máscara invertida
np.isnan(a)Dónde falta el dato
np.where(cond, si, no)Reemplaza sin filtrar
mask.any() / mask.all()¿Alguno? / ¿Todos?
np.nonzero(mask)Las posiciones que cumplen
np.isin(a, [1, 2, 3])¿Está en esta lista? - el isin de pandas

Ponete a prueba

1. s tiene 24 valores. ¿Qué devuelve s > 22?Nivel 1 · Recordar
2. ¿Qué devuelve (s > 22).mean()?Nivel 1 · Recordar
3. a > 20 and a < 22 levanta ValueError: The truth value of an array … is ambiguous. ¿Por qué?Nivel 2 · Predecir la salida
4. Un array de 240 lecturas tiene 4 NaN. ¿Cuántos elementos devuelve a[a > 0], si todas las lecturas presentes son positivas?Nivel 2 · Predecir la salida
5. Hay que convertir en NaN las lecturas imposibles (mayores a 50) sin perder ninguna fila. Las cuatro corren: ¿cuál conviene?Nivel 3 · Elegir entre dos casi correctas
6. Querés saber qué sensores tienen al menos un hueco. ¿Cuál lo contesta?Nivel 3 · Elegir entre dos casi correctas
7. Este código cuenta las lecturas cálidas por sensor. Corre sin error y le atribuye 1 hora cálida al sensor S05, que en realidad no tuvo ninguna. ¿Por qué?
calidas = (lecturas > 22).sum(axis=1)
Nivel 4 · Encontrar el defecto

Prácticas de esta subunidad