Buscar entradas que satisfagan una condición específica es un proceso doloroso, especialmente si lo está buscando en un conjunto de datos grande que tiene cientos o miles de entradas. Si conoce las consultas SQL fundamentales, debe conocer la cláusula 'WHERE' que se usa con la instrucción SELECT para obtener dichas entradas de una base de datos relacional que satisfaga ciertas condiciones.

NumPy ofrece una funcionalidad similar para encontrar dichos elementos en una matriz NumPy que satisfacen una condición booleana dada a través de su función ' where () ', excepto que se usa de una manera ligeramente diferente a la instrucción SQL SELECT con la cláusula WHERE.

En este tutorial, veremos las diversas formas en que la función NumPy where puede usarse para una variedad de casos de uso. Vámonos.

Un uso muy simple de NumPy donde

Comencemos con una aplicación simple de ' np.where () ' en una matriz NumPy unidimensional de enteros.
Usaremos la función 'np.where' para encontrar posiciones con valores menores que 5.

Primero crearemos una matriz unidimensional de 10 valores enteros elegidos al azar entre 0 y 9.

1
2
3
4
5
6
7
import numpy as np
 
np.random.seed(42)
 
a = np.random.randint()
 
print("a = {}".format(a))

Salida:

Ahora llamaremos a 'np.where' con la condición 'a <5', es decir, le pediremos a 'np.where' que nos diga en qué lugar de la matriz a están los valores menores que 5.
Nos devolverá una matriz de índices donde se cumple la condición especificada.

1
2
3
result = np.where(a < 5)
 
print(result)

Salida:

Obtenemos los índices 1, 3, 6, 9 como salida y se puede verificar a partir de la matriz que los valores en estas posiciones son de hecho menores que 5.
Tenga en cuenta que el valor devuelto es una tupla de 1 elemento. Esta tupla tiene una serie de índices.
Entenderemos la razón por la que el resultado se devuelve como una tupla cuando analicemos np.where en matrices 2D.

¿Cómo funciona NumPy where?

Para comprender lo que sucede dentro de la expresión compleja que involucra la función 'np.where', es importante comprender el primer parámetro de 'np.where', es decir, la condición.

Cuando llamamos a una expresión booleana que involucra una matriz NumPy como 'a> 2' o 'a% 2 == 0', en realidad devuelve una matriz NumPy de valores booleanos.

Esta matriz tiene el valor  Verdadero  en las posiciones donde la condición se evalúa como Verdadera y tiene el valor  Falso en  otros lugares. Esto sirve como una ' máscara ' para NumPy donde funciona.

Aquí hay un ejemplo de código.

1
2
3
4
5
a = np.array([1, 10, 13, 8, 7, 9, 6, 3, 0])
 
print ("a > 5:")
 
print(a > 5)

Salida:

Entonces, lo que efectivamente hacemos es pasar una matriz de valores booleanos a la función 'np.where' que luego devuelve los índices donde la matriz tenía el valor  Verdadero .

Esto se puede verificar pasando una matriz constante de valores booleanos en lugar de especificar la condición en la matriz que solemos hacer.

1
2
3
bool_array = np.array([True, True, True, False, False, False, False, False, False])
 
print(np.where(bool_array))

Salida:

Observe cómo, en lugar de pasar una condición en una matriz de valores reales, pasamos una matriz booleana y la función 'np.where' nos devolvió los índices donde los valores eran verdaderos.

Matrices 2D

Ahora que lo hemos visto en matrices NumPy unidimensionales, entendamos cómo se comportaría 'np.where' en matrices 2D.

La idea sigue siendo la misma. Llamamos a la función 'np.where' y pasamos una condición en una matriz 2D. La diferencia está en la forma en que devuelve los índices de resultados.
Anteriormente, np.where devolvía una matriz unidimensional de índices (almacenados dentro de una tupla) para una matriz 1-D, especificando las posiciones donde los valores satisfacen una condición determinada.

Pero en el caso de una matriz 2D, se especifica una sola posición utilizando 2 valores: el índice de fila y el índice de columna.
Entonces, en este caso, np.where devolverá 2 matrices, la primera con los índices de fila y la segunda con los índices de columna correspondientes.

Tanto estas matrices de índice de filas como de columnas se almacenan dentro de una tupla (ahora sabe por qué obtuvimos una tupla como respuesta incluso en el caso de una matriz 1-D).

Veamos esto en acción para entenderlo mejor. Escribiremos un código para encontrar dónde en una matriz de 3 × 3 están las entradas divisibles por 2.

1
2
3
4
5
6
7
8
9
np.random.seed(42)
 
a = np.random.randint(0,10, size=(3,3))
 
print("a =\n{}\n".format(a))
 
result = np.where(a % 2 == 0)
 
print("result: {}".format(result))

Salida:

La tupla devuelta tiene 2 matrices, cada una con los índices de fila y columna de las posiciones en la matriz donde los valores son divisibles por 2.

La selección ordenada por pares de valores de las dos matrices nos da una posición para cada uno.
La longitud de cada una de las dos matrices es 5, lo que indica que hay 5 de tales posiciones que satisfacen la condición dada.

Si miramos el tercer par - (1,1), el valor en (1,1) en la matriz es 6, que es divisible por 2.
Asimismo, puede verificar y verificar con otros pares de índices.

Matriz multidimensional

Así como vimos el funcionamiento de 'np.where' en una matriz 2-D, obtendremos resultados similares cuando aplicamos np.where en una matriz NumPy multidimensional.

La longitud de la tupla devuelta será igual al número de dimensiones de la matriz de entrada.
Cada matriz en la posición k en la tupla devuelta representará los índices en la k-ésima dimensión de los elementos que satisfacen la condición especificada.

Veamos rápidamente un ejemplo.

01
02
03
04
05
06
07
08
09
10
11
np.random.seed(42)
 
a = np.random.randint(0,10, size=(3,3,3,3)) #4-dimensional array
 
print("a =\n{}\n".format(a))
 
result = np.where(a == 5) #checking which values are equal to 5
 
print("len(result)= {}".format(len(result)))
 
print("len(result[0]= {})".format(len(result[0])))

Salida:

len (resultado) = 4 indica que la matriz de entrada es de 4 dimensiones.

La longitud de una de las matrices en la tupla de resultados es 6, lo que significa que hay seis posiciones en la matriz 3x3x3x3 dada donde se cumple la condición dada (es decir, que contiene el valor 5).

Usando el resultado como índice

Hasta ahora hemos visto cómo obtenemos la tupla de índices, en cada dimensión, de los valores que satisfacen la condición dada.

La mayoría de las veces, estaríamos interesados ​​en obtener los valores reales que satisfacen la condición dada en lugar de sus índices.

Para lograr esto, podemos usar la tupla devuelta como índice en la matriz dada. Esto devolverá solo aquellos valores cuyos índices están almacenados en la tupla.

Comprobemos esto para el ejemplo de la matriz 2-D.

01
02
03
04
05
06
07
08
09
10
11
np.random.seed(42)
 
a = np.random.randint(0,10, size=(3,3))
 
print("a =\n{}\n".format(a))
 
result_indices = np.where(a % 2 == 0)
 
result = a[result_indices]
 
print("result: {}".format(result))

Salida:

Como se discutió anteriormente, obtenemos todos esos valores (no sus índices) que satisfacen la condición dada que, en nuestro caso, era divisibilidad por 2, es decir, números pares.

Parámetros 'x' e 'y'

En lugar de obtener los índices como resultado de llamar a la función 'np.where', también podemos proporcionar como parámetros, dos matrices opcionales xey de la misma forma (o forma broadcastable) como matriz de entrada, cuyos valores se devolverán cuando la condición especificada en los valores correspondientes en la matriz de entrada es Verdadero o Falso respectivamente.

Por ejemplo, si llamamos al método en una matriz unidimensional de longitud 10, y proporcionamos dos matrices más xey de la misma longitud.
En este caso, siempre que un valor en la matriz de entrada satisfaga la condición dada, se devolverá el valor correspondiente en la matriz x mientras que, si la condición es falsa en un valor dado, se devolverá el valor correspondiente de la matriz y.

Estos valores de xey en sus respectivas posiciones se devolverán como una matriz de la misma forma que la matriz de entrada.

Comprendamos mejor esto a través del código.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
np.random.seed(42)
 
a = np.random.randint(0,10, size=(10))
 
x = a
 
y = a*10
 
print("a = {}".format(a))
 
print("x = {}".format(x))
 
print("y = {}".format(y))
 
result = np.where(a%2 == 1, x, y) #if number is odd return the same number else return its multiple of 10.
 
print("\nresult = {}".format(result))

Salida:

Este método es útil si desea reemplazar los valores que satisfacen una condición particular por otro conjunto de valores y dejar los que no satisfacen la condición sin cambios.
En ese caso, pasaremos los valores de reemplazo al parámetro x y la matriz original al parámetro y.

Tenga en cuenta que podemos pasar tanto x como y juntos o ninguno de ellos. No podemos pasar uno de ellos y saltarnos el otro.

Aplicar en Pandas DataFrames

La función 'where' de Numpy no tiene por qué aplicarse necesariamente a las matrices NumPy. Se puede usar con cualquier iterable que produzca una lista de valores booleanos.

Veamos cómo podemos aplicar la función 'np.where' en un Pandas DataFrame para ver  si las cadenas de una columna contienen una subcadena en particular .

01
02
03
04
05
06
07
08
09
10
11
12
import pandas as pd
 
import numpy as np
 
df  = pd.DataFrame({"fruit":["apple", "banana", "musk melon",
                             "watermelon", "pineapple", "custard apple"],
                   "color": ["red", "green/yellow", "white",
                            "green", "yellow", "green"]})
 
print("Fruits DataFrame:\n")
 
print(df)

Salida:

Ahora vamos a usar 'np.where' para extraer esas filas del DataFrame 'df' donde la columna 'fruta' tiene la subcadena 'manzana'

1
2
3
apple_df = df.iloc[np.where(df.fruit.str.contains("apple"))]
 
print(apple_df)

Salida:

Probemos con un ejemplo más en el mismo DataFrame donde extraemos filas para las cuales la columna 'color'  no contiene  la subcadena 'grito'.

Nota:  usamos el signo de tilde (~) para invertir los valores booleanos en Pandas DataFrame o una matriz NumPy.

1
2
3
non_yellow_fruits = df.iloc[np.where(~df.color.str.contains("yell"))]
 
print("Non Yellow fruits:\n{}".format(non_yellow_fruits))

Salida:

Varias condiciones

Hasta ahora hemos estado evaluando una sola condición booleana en la función 'np.where'. A veces, es posible que necesitemos combinar varias condiciones booleanas utilizando operadores booleanos como ' Y ' u  'O' .

Es fácil especificar múltiples condiciones y combinarlas usando un operador booleano.
La única advertencia es que para la matriz NumPy de valores booleanos, no podemos usar las palabras clave normales 'y' o 'o' que normalmente usamos para valores individuales.
Necesitamos usar el operador '&' para 'AND' y '|' operador para la operación 'OR' para operaciones de combinación booleana por elementos.

Entendamos esto con un ejemplo.

1
2
3
4
5
np.random.seed(42)
 
a = np.random.randint(0,15, (5,5)) #5x5 matrix with values from 0 to 14
 
print(a)

Salida:

Buscaremos valores que sean menores a 8 y que sean impares. Podemos combinar estas dos condiciones usando el operador AND (&).

1
2
3
4
5
# get indices of odd values less than 8 in a
indices = np.where((a < 8) & (a % 2==1))
 
#print the actual values
print(a[indices])

Salida:

También podemos usar el operador OR (|) para combinar las mismas condiciones.
Esto nos dará valores que son 'menores que 8' O 'valores impares', es decir, se devolverán todos los valores menores que 8 y todos los valores impares mayores que 8.

1
2
3
4
5
# get indices of values less than 8 OR odd values in a
indices = np.where((a < 8) | (a % 2==1))
 
#print the actual values
print(a[indices])

Salida:

Anidado donde (donde dentro de donde)

Repasemos el ejemplo de nuestra tabla de 'frutas'.

01
02
03
04
05
06
07
08
09
10
11
12
import pandas as pd
 
import numpy as np
 
df  = pd.DataFrame({"fruit":["apple", "banana", "musk melon",
                             "watermelon", "pineapple", "custard apple"],
                   "color": ["red", "green/yellow", "white",
                            "green", "yellow", "green"]})
 
print("Fruits DataFrame:\n")
 
print(df)

Salida:

Supongamos ahora que queremos crear una 'bandera' de columna más que tendría el valor 1 si la fruta en esa fila tiene una subcadena 'manzana' o es de color 'amarillo'.

Podemos lograr esto usando llamadas where anidadas, es decir, llamaremos a la función 'np.where' como un parámetro dentro de otra llamada 'np.where'.

1
2
3
4
df['flag'] = np.where(df.fruit.str.contains("apple"), 1, # if fruit == 'apple', set 1
                     np.where(df.color.str.contains("yellow"), 1, 0)) #else if color has 'yellow' set 1, else set 0
 
print(df)

Salida:

La expresión compleja anterior se puede traducir al inglés simple como:

  1. Si la columna 'fruta' tiene la subcadena 'manzana', establezca el valor de 'bandera' en 1
  2. Más:
    1. Si la columna 'color' tiene la subcadena 'amarilla', establezca el valor de la 'bandera' en 1
    2. De lo contrario, establezca el valor de la 'bandera' en 0

Tenga en cuenta  que podemos lograr el mismo resultado utilizando el operador OR ( | ).

1
2
3
4
5
#set flag = 1 if any of the two conditions is true, else set it to 0
df['flag'] = np.where(df.fruit.str.contains("apple") |
                      df.color.str.contains("yellow"), 1, 0)
 
print(df)

Salida:

Por lo tanto, el lugar anidado es particularmente útil para datos tabulares como Pandas DataFrames y es un buen equivalente de la cláusula WHERE anidada utilizada en las consultas SQL.

Encontrar filas de ceros

A veces, en una matriz 2D, algunas o todas las filas tienen todos los valores iguales a cero. Por ejemplo, consulte la siguiente matriz NumPy.

1
2
3
4
5
6
7
8
a = np.array([[1, 2, 0],
             [0, 9, 20],
             [0, 0, 0],
             [3, 3, 12],
             [0, 0, 0]
             [1, 0, 0]])
 
print(a)

Salida:

Como podemos ver, las filas 2 y 4 tienen todos los valores iguales a cero. Pero, ¿cómo encontramos esto usando la función 'np.where'?

Si queremos encontrar tales filas usando la función NumPy where, tendremos que crear  una matriz booleana que indique qué filas tienen todos los valores iguales a cero .

Podemos usar la función ' np.any () ' con 'axis = 1', que devuelve True si al menos uno de los valores en una fila es distinto de cero.

El resultado de np.any () será una matriz booleana de longitud igual al número de filas en nuestra matriz NumPy, en la que las posiciones con el valor Verdadero indican que la fila correspondiente tiene al menos un valor distinto de cero.

¡Pero necesitábamos una matriz booleana que fuera todo lo contrario a esto!

Es decir, necesitábamos una matriz booleana donde el valor 'Verdadero' indicaría que cada elemento en esa fila es igual a cero.

Bueno, esto se puede obtener mediante un simple paso de inversión. El operador NOT o tilde (~) invierte cada uno de los valores booleanos en una matriz NumPy.

La matriz booleana invertida se puede pasar a la función 'np.where'.

Ok, esa fue una explicación larga y agotadora. Veamos esto en acción.

1
2
3
zero_rows = np.where(~np.any(a, axis=1))[0]
 
print(zero_rows)

Salida:

Veamos lo que está sucediendo paso a paso:

  • np.any () devuelve Verdadero si al menos un elemento de la matriz es Verdadero (distinto de cero). axis = 1 le indica que haga esta operación por filas.
  • Devolvería una matriz booleana de longitud igual al número de filas en a, con el valor Verdadero para las filas que tienen valores distintos de cero y Falso para las filas que tienen todos los valores = 0.
    np.any(a, axis=1)
    Salida:
  • El operador tilde (~) invierte la matriz booleana anterior:
    ~np.any(a, axis=1)
    Salida :
  • 'np.where ()' acepta esta matriz booleana y devuelve índices que tienen el valor True.

La indexación [0] se usa porque, como se discutió anteriormente, 'np.where' devuelve una tupla.

Encontrar la última aparición de una condición verdadera

Sabemos que la función 'dónde' de NumPy devuelve múltiples índices o pares de índices (en el caso de una matriz 2D) para los cuales la condición especificada es verdadera.

Pero a veces nos interesa solo la primera aparición o la última aparición del valor para el que se cumple la condición especificada.

Tomemos el ejemplo simple de una matriz unidimensional donde encontraremos la última aparición de un valor divisible por 3.

01
02
03
04
05
06
07
08
09
10
11
np.random.seed(42)
 
a = np.random.randint(0,10, size=(10))
 
print("Array a:", a)
 
indices = np.where(a%3==0)[0]
 
last_occurrence_position = indices[-1]
 
print("last occurrence at", last_occurrence_position)

Salida:

Aquí podríamos usar directamente el índice '-1' en los índices devueltos para obtener el último valor de la matriz.

Pero, ¿cómo extraeríamos la posición de la última aparición en una matriz multidimensional, donde el resultado devuelto es una tupla de matrices y cada matriz almacena los índices en una de las dimensiones?

Podemos usar la función zip que toma múltiples iterables y devuelve una combinación de valores por pares de cada iterable en el orden dado.

Devuelve un objeto iterador, por lo que necesitamos convertir el objeto devuelto en una lista, tupla o cualquier iterable.

Veamos primero cómo funciona zip:

1
2
3
4
5
6
7
a = (1, 2, 3, 4)
 
b = (5, 6, 7, 8)
 
c = list(zip(a,b))
 
print(c)

Salida:

Entonces, el primer elemento de ay el primer elemento de b forman una tupla, luego el segundo elemento de ay el segundo elemento de b forman la segunda tupla en c, y así sucesivamente.

Usaremos la misma técnica para encontrar la posición de la última aparición de una condición que se cumple en una matriz multidimensional.

Usémoslo para una matriz 2D con la misma condición que vimos en el ejemplo anterior.

01
02
03
04
05
06
07
08
09
10
11
np.random.seed(42)
 
a = np.random.randint(0,10, size=(3,3))
 
print("Matrix a:\n", a)
 
indices = np.where(a % 3 == 0)
 
last_occurrence_position = list(zip(*indices))[-1]
 
print("last occurrence at",last_occurrence_position)

Salida:

Podemos ver en la matriz que la última aparición de un múltiplo de 3 está en la posición (2,1), que es el valor 6.

Nota:  El operador * es un operador de desempaquetado que se utiliza para descomprimir una secuencia de valores en argumentos posicionales separados.

Usando datos de DateTime

Hemos estado usando la función 'np.where' para evaluar ciertas condiciones en valores numéricos (mayor que, menor que, igual a, etc.) o datos de cadena (contiene, no contiene, etc.)

También podemos usar la función 'np.where' en datos de fecha y hora.

Por ejemplo, podemos verificar en una lista de valores de fecha y hora, cuáles de las instancias de fecha y hora son anteriores / posteriores a una fecha y hora especificada.

Entendamos esto con un ejemplo.
Nota : Usaremos el  módulo datetime de Python para crear objetos de fecha.

Primero definamos un DataFrame que especifique las fechas de nacimiento de 6 personas.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
import datetime
 
names = ["John", "Smith",