Indice / NumPy / 3.3
Indexación y rebanado
Sacar un pedazo de un array se parece a sacarlo de una lista, con una diferencia que cambia todo: el pedazo no es una copia, es una ventana al mismo dato.
Para qué sirve
Todo lo que viene después - máscaras, broadcasting, estadística por eje
- empieza por seleccionar un pedazo. Y la distinción vista/copia es la causa
directa del ChainedAssignmentError de pandas, que es el error que más
confunde en la unidad 4.
ELI5Explicado facil
Rebanar una lista de Python es fotocopiar unas páginas: te llevás las copias y el original queda como estaba.
Rebanar un array es poner un marco sobre unas páginas del original. Lo que escribas dentro del marco, queda escrito en el libro.
La sintaxis
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)")
s01 = lecturas[0]
print()
print("EN UNA DIMENSION es igual que una lista de Python:")
print(" s01 :", np.round(s01[:8], 1), "...")
print(" s01[0] :", s01[0])
print(" s01[-1] :", s01[-1], " <- el ultimo")
print(" s01[2:5] :", s01[2:5], " <- desde 2 INCLUIDO hasta 5 EXCLUIDO")
print(" s01[:3] :", s01[:3])
print(" s01[20:] :", s01[20:])
print(" s01[::6] :", s01[::6], " <- de a 6")
print(" s01[::-1] :", np.round(s01[::-1][:4], 1), "... <- al reves")
print()
print("EN DOS DIMENSIONES se indexa con UNA coma, no con dos corchetes:")
print(" lecturas[0, 0] ->", lecturas[0, 0], " (sensor 0, hora 0)")
print(" lecturas[0][0] ->", lecturas[0][0], " (funciona, pero es mas lento:")
print(" arma la fila entera y despues busca)")
print(" lecturas[4, 11] ->", lecturas[4, 11], " <- la lectura imposible")
print()
print("Fila entera, columna entera, y un rectangulo:")
print(" lecturas[2] -> shape", lecturas[2].shape, " (el sensor S03)")
print(" lecturas[:, 0] -> shape", lecturas[:, 0].shape,
" (la hora h00 de los 10)")
print(" lecturas[:3, :4] -> shape", lecturas[:3, :4].shape)
print(np.round(lecturas[:3, :4], 1))
print()
print("Los dos puntos solos significan 'todo este eje':")
print(" lecturas[:, 8:12] -> shape", lecturas[:, 8:12].shape,
" (todas las filas, 4 horas)")
print(" lecturas[3:5, :] -> shape", lecturas[3:5, :].shape,
" (2 filas, todas las horas)")
print()
print("Y la diferencia que cuesta ver: un indice REDUCE la dimension,")
print("un rebanado NO.")
print(" lecturas[0] -> shape", lecturas[0].shape, " (1 dimension)")
print(" lecturas[0:1] -> shape", lecturas[0:1].shape, " (sigue en 2)")
print(" lecturas[:, 0] -> shape", lecturas[:, 0].shape)
print(" lecturas[:, 0:1] -> shape", lecturas[:, 0:1].shape)
print(" Los dos tienen los mismos numeros. La diferencia importa cuando")
print(" despues hay que operar con otro array (leccion 3.5).")
print()
print("Ojo con el limite superior: NumPy lo excluye, como Python.")
print(" s01[0:3] son 3 elementos, no 4:", s01[0:3])
print(" pandas es la excepcion: loc['a':'c'] SI incluye 'c' (leccion 4.4)")
lecturas: (10, 24) (10 sensores, 24 horas)
EN UNA DIMENSION es igual que una lista de Python:
s01 : [20.8 19.8 19.2 19. 19.6 20.3 20. 21. ] ...
s01[0] : 20.8
s01[-1] : 21.1 <- el ultimo
s01[2:5] : [19.2 19. 19.6] <- desde 2 INCLUIDO hasta 5 EXCLUIDO
s01[:3] : [20.8 19.8 19.2]
s01[20:] : [22.4 21.9 21.2 21.1]
s01[::6] : [20.8 20. 22.6 23.2] <- de a 6
s01[::-1] : [21.1 21.2 21.9 22.4] ... <- al reves
EN DOS DIMENSIONES se indexa con UNA coma, no con dos corchetes:
lecturas[0, 0] -> 20.8 (sensor 0, hora 0)
lecturas[0][0] -> 20.8 (funciona, pero es mas lento:
arma la fila entera y despues busca)
lecturas[4, 11] -> 999.9 <- la lectura imposible
Fila entera, columna entera, y un rectangulo:
lecturas[2] -> shape (24,) (el sensor S03)
lecturas[:, 0] -> shape (10,) (la hora h00 de los 10)
lecturas[:3, :4] -> shape (3, 4)
[[20.8 19.8 19.2 19. ]
[18.5 17.1 16.6 16.1]
[17.8 16.6 16.6 15.5]]
Los dos puntos solos significan 'todo este eje':
lecturas[:, 8:12] -> shape (10, 4) (todas las filas, 4 horas)
lecturas[3:5, :] -> shape (2, 24) (2 filas, todas las horas)
Y la diferencia que cuesta ver: un indice REDUCE la dimension,
un rebanado NO.
lecturas[0] -> shape (24,) (1 dimension)
lecturas[0:1] -> shape (1, 24) (sigue en 2)
lecturas[:, 0] -> shape (10,)
lecturas[:, 0:1] -> shape (10, 1)
Los dos tienen los mismos numeros. La diferencia importa cuando
despues hay que operar con otro array (leccion 3.5).
Ojo con el limite superior: NumPy lo excluye, como Python.
s01[0:3] son 3 elementos, no 4: [20.8 19.8 19.2]
pandas es la excepcion: loc['a':'c'] SI incluye 'c' (leccion 4.4)
| Expresión | Shape | Qué es |
|---|---|---|
a[0, 0] | escalar | Una celda. Una coma, no dos corchetes |
a[0] | (24,) | La fila 0 entera |
a[:, 0] | (10,) | La columna 0 entera |
a[:3, :4] | (3, 4) | Un rectángulo |
a[3:5, :] | (2, 24) | Dos filas completas |
a[::-1] | (10, 24) | Las filas al revés |
a[0] tiene shape (24,) y a[0:1] tiene
(1, 24). Los mismos números, distinta forma.
Parece una sutileza y deja de serlo en la lección 3.5: (10, 1) y
(10,) se comportan distinto al combinarse con otro array.
a[0:3] son tres elementos, no cuatro - igual que en las listas de
Python. La excepción del curso es loc de pandas, que sí
incluye el final; es el tema de la lección 4.4 y sorprende a todo el mundo.
Vistas y copias
import numpy as np
original = np.arange(10)
print("original:", original)
rebanada = original[2:6]
print("rebanada = original[2:6] ->", rebanada)
print("¿es una vista?", rebanada.base is not None)
rebanada[0] = 999
print()
print("toque rebanada[0] = 999")
print(" rebanada:", rebanada)
print(" original:", original, " <- CAMBIO TAMBIEN")
print()
print("En una lista de Python esto no pasa: el slice copia.")
lista = list(range(10))
trozo = lista[2:6]
trozo[0] = 999
print(" lista :", lista, " <- intacta")
print(" trozo :", trozo)
print()
print("Es la diferencia mas importante entre las dos, y la que produce los")
print("bugs mas dificiles de encontrar: el cambio aparece en otro lado.")
print()
print("Como saber si un array es dueno de sus datos:")
copia = original[2:6].copy()
for nombre, array in (("original", original), ("rebanada", rebanada),
("copia", copia)):
print(f" {nombre:<9} .base is None -> {array.base is None}"
f" {'(dueno)' if array.base is None else '(vista)'}")
print()
print("QUE DEVUELVE UNA VISTA y que devuelve una copia:")
print(f" {'a[2:6]':<24} vista")
print(f" {'a[:, 0]':<24} vista")
print(f" {'a.reshape(2, 5)':<24} vista")
print(f" {'a.T':<24} vista")
print(f" {'a.ravel()':<24} vista si se puede")
print(f" {'a.flatten()':<24} COPIA, siempre")
print(f" {'a.copy()':<24} COPIA, siempre")
print(f" {'a[[0, 2, 4]]':<24} COPIA (indexado por lista)")
print(f" {'a[a > 3]':<24} COPIA (indexado booleano)")
print(f" {'a.astype(float)':<24} COPIA, siempre")
grilla = np.arange(12).reshape(3, 4)
print()
print("Se comprueba en dos lineas:")
print(" grilla[:, 1].base is None ->", grilla[:, 1].base is None)
print(" grilla[grilla > 5].base is None ->", grilla[grilla > 5].base is None)
print()
print("La regla practica: si vas a MODIFICAR, pedi .copy() explicito.")
print("Cuesta una linea y evita el bug que aparece tres funciones despues.")
print("En pandas la misma idea produce el ChainedAssignmentError de la")
print("leccion 4.7, y por el mismo motivo.")
original: [0 1 2 3 4 5 6 7 8 9]
rebanada = original[2:6] -> [2 3 4 5]
¿es una vista? True
toque rebanada[0] = 999
rebanada: [999 3 4 5]
original: [ 0 1 999 3 4 5 6 7 8 9] <- CAMBIO TAMBIEN
En una lista de Python esto no pasa: el slice copia.
lista : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] <- intacta
trozo : [999, 3, 4, 5]
Es la diferencia mas importante entre las dos, y la que produce los
bugs mas dificiles de encontrar: el cambio aparece en otro lado.
Como saber si un array es dueno de sus datos:
original .base is None -> True (dueno)
rebanada .base is None -> False (vista)
copia .base is None -> True (dueno)
QUE DEVUELVE UNA VISTA y que devuelve una copia:
a[2:6] vista
a[:, 0] vista
a.reshape(2, 5) vista
a.T vista
a.ravel() vista si se puede
a.flatten() COPIA, siempre
a.copy() COPIA, siempre
a[[0, 2, 4]] COPIA (indexado por lista)
a[a > 3] COPIA (indexado booleano)
a.astype(float) COPIA, siempre
Se comprueba en dos lineas:
grilla[:, 1].base is None -> False
grilla[grilla > 5].base is None -> True
La regla practica: si vas a MODIFICAR, pedi .copy() explicito.
Cuesta una linea y evita el bug que aparece tres funciones despues.
En pandas la misma idea produce el ChainedAssignmentError de la
leccion 4.7, y por el mismo motivo.
Es la diferencia con una lista de Python, y produce los bugs más difíciles de encontrar, porque el cambio aparece en otro lado:
| Devuelve una vista (comparte memoria) | Devuelve una copia |
|---|---|
a[2:6] | a.copy() |
a[:, 0] | a.flatten() |
a.reshape(2, 5) | a[[0, 2, 4]] - indexado por lista |
a.T | a[a > 3] - indexado booleano |
a.ravel(), si puede | a.astype(float) |
Se comprueba en una línea: a.base is None es True si el
array es dueño de sus datos, y False si es una vista.
Si vas a modificar, pedí .copy() explícito. Cuesta una
línea y evita el bug que aparece tres funciones después.
Que NumPy no copie es una decisión de rendimiento: rebanar un array de un millón de elementos no debería costar un millón de escrituras. La contrapartida es que hay que saberlo.
Asignar, y el indexado por lista
import numpy as np
lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
skip_header=1, usecols=range(2, 26)).copy()
print("Se puede asignar a un rebanado entero, no solo a una celda:")
prueba = np.zeros((3, 4))
print(prueba)
prueba[1] = 5
print()
print("prueba[1] = 5 ->")
print(prueba)
prueba[:, 0] = [1, 2, 3]
print()
print("prueba[:, 0] = [1, 2, 3] ->")
print(prueba)
print()
print("El lado derecho tiene que ENTRAR en la forma del izquierdo, y si no:")
try:
prueba[:, 0] = [1, 2]
except ValueError as error:
print(" prueba[:, 0] = [1, 2] ->", type(error).__name__ + ":", error)
print()
print("=" * 62)
print("El indexado por LISTA de posiciones (fancy indexing)")
print("=" * 62)
s01 = lecturas[0]
print(" s01[[0, 6, 12, 18]] ->", s01[[0, 6, 12, 18]])
print(" Toma esas cuatro posiciones, en ESE orden, y devuelve una COPIA.")
print(" s01[[18, 0]] ->", s01[[18, 0]], " <- el orden lo elegis vos")
print(" s01[[0, 0, 0]] ->", s01[[0, 0, 0]], " <- se puede repetir")
print()
print("En dos dimensiones toma los PARES, no el rectangulo:")
print(" lecturas[[0, 1], [5, 6]] ->", lecturas[[0, 1], [5, 6]])
print(" que es [lecturas[0, 5], lecturas[1, 6]], dos numeros.")
print(" Para el rectangulo hay que separarlo en dos pasos:")
print(" lecturas[[0, 1]][:, [5, 6]] ->")
print(lecturas[[0, 1]][:, [5, 6]])
print(" O usar np.ix_, que arma la grilla:")
print(" lecturas[np.ix_([0, 1], [5, 6])] ->")
print(lecturas[np.ix_([0, 1], [5, 6])])
print()
print("Como devuelve una copia, asignarle NO modifica el original...")
copia = s01[[0, 1, 2]]
copia[0] = -1
print(" toque copia[0] = -1 -> s01[0] sigue en", s01[0])
print("...pero asignar DIRECTAMENTE sobre el indexado, si:")
s01[[0, 1, 2]] = -1
print(" s01[[0, 1, 2]] = -1 -> s01[:4] =", s01[:4])
print(" La diferencia: en el segundo caso NumPy nunca crea la copia,")
print(" escribe en el original. Es la misma distincion que en pandas")
print(" entre df[mask]['col'] = 0 y df.loc[mask, 'col'] = 0.")
print()
print("Y el ultimo, el mas util de todos: ordenar por otra cosa.")
# Se relee el archivo: arriba le escribimos -1 a las tres primeras horas
# de S01, y ese promedio ya no seria el del archivo.
intactas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
skip_header=1, usecols=range(2, 26))
promedios = np.nanmean(intactas, axis=1)
orden = np.argsort(promedios)
print(" promedios :", np.round(promedios, 1))
print(" np.argsort() :", orden, " <- las POSICIONES ordenadas")
print(" promedios[orden]:", np.round(promedios[orden], 1))
print(" Y con esas posiciones se reordena cualquier otro array:")
nombres = np.array([f"S{i:02d}" for i in range(1, 11)])
print(" nombres[orden]:", nombres[orden])
Se puede asignar a un rebanado entero, no solo a una celda:
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
prueba[1] = 5 ->
[[0. 0. 0. 0.]
[5. 5. 5. 5.]
[0. 0. 0. 0.]]
prueba[:, 0] = [1, 2, 3] ->
[[1. 0. 0. 0.]
[2. 5. 5. 5.]
[3. 0. 0. 0.]]
El lado derecho tiene que ENTRAR en la forma del izquierdo, y si no:
prueba[:, 0] = [1, 2] -> ValueError: could not broadcast input array from shape (2,) into shape (3,)
==============================================================
El indexado por LISTA de posiciones (fancy indexing)
==============================================================
s01[[0, 6, 12, 18]] -> [20.8 20. 22.6 23.2]
Toma esas cuatro posiciones, en ESE orden, y devuelve una COPIA.
s01[[18, 0]] -> [23.2 20.8] <- el orden lo elegis vos
s01[[0, 0, 0]] -> [20.8 20.8 20.8] <- se puede repetir
En dos dimensiones toma los PARES, no el rectangulo:
lecturas[[0, 1], [5, 6]] -> [20.3 18.3]
que es [lecturas[0, 5], lecturas[1, 6]], dos numeros.
Para el rectangulo hay que separarlo en dos pasos:
lecturas[[0, 1]][:, [5, 6]] ->
[[20.3 20. ]
[17. 18.3]]
O usar np.ix_, que arma la grilla:
lecturas[np.ix_([0, 1], [5, 6])] ->
[[20.3 20. ]
[17. 18.3]]
Como devuelve una copia, asignarle NO modifica el original...
toque copia[0] = -1 -> s01[0] sigue en 20.8
...pero asignar DIRECTAMENTE sobre el indexado, si:
s01[[0, 1, 2]] = -1 -> s01[:4] = [-1. -1. -1. 19.]
La diferencia: en el segundo caso NumPy nunca crea la copia,
escribe en el original. Es la misma distincion que en pandas
entre df[mask]['col'] = 0 y df.loc[mask, 'col'] = 0.
Y el ultimo, el mas util de todos: ordenar por otra cosa.
promedios : [21.7 19.6 18.7 21.6 60.6 21.1 24.5 20. 22.8 17.5]
np.argsort() : [9 2 1 7 5 3 0 8 6 4] <- las POSICIONES ordenadas
promedios[orden]: [17.5 18.7 19.6 20. 21.1 21.6 21.7 22.8 24.5 60.6]
Y con esas posiciones se reordena cualquier otro array:
nombres[orden]: ['S10' 'S03' 'S02' 'S08' 'S06' 'S04' 'S01' 'S09' 'S07' 'S05']
Se puede asignar a una fila entera, a una columna entera o a un rectángulo, y el lado derecho tiene que entrar en la forma del izquierdo. El indexado por lista de posiciones agrega tres cosas que un rebanado no puede hacer:
- Elegir posiciones sueltas:
a[[0, 6, 12, 18]]. - Elegir el orden:
a[[18, 0]]devuelve esos dos, en ese orden. - Repetir:
a[[0, 0, 0]]devuelve tres veces el mismo.
a[[0, 1], [5, 6]] son dos números, no un rectángulo
Toma los pares: a[0, 5] y a[1, 6]. Para
el rectángulo hacen falta dos pasos (a[[0, 1]][:, [5, 6]]) o
np.ix_([0, 1], [5, 6]), que arma la grilla.
copia = a[[0, 1, 2]] y después copia[0] = -1
no toca a. Pero a[[0, 1, 2]] = -1
sí, porque ahí NumPy nunca crea la copia intermedia.
Es exactamente la distinción de pandas entre
df[mask]["col"] = 0 (que no hace nada) y
df.loc[mask, "col"] = 0 (que funciona).
argsort: ordenar por otra cosa
Es el uso más productivo del indexado por lista. np.argsort(promedios)
no devuelve los promedios ordenados: devuelve las posiciones en el
orden correcto. Y con esas posiciones se reordena cualquier otro array del mismo
largo.
orden = np.argsort(promedios)
promedios[orden] # los promedios, de menor a mayor
nombres[orden] # los nombres de los sensores, en ESE mismo orden
Sobre la grilla real, nombres[orden] da ['S10', 'S03', 'S02',
…, 'S07', 'S05']: S05 queda último, con su 60,65 producto de la lectura
imposible.
Dónde se rompe
Causa: los índices van de 0 a 23. El mensaje dice el eje y el tamaño.
Arreglo: a[-1] para el último. Un rebanado fuera de
rango no falla: a[100:200] devuelve un array vacío en
silencio.
Causa: escribiste a[0, 0] sobre un array de una
dimensión.
Arreglo: print(a.shape). Suele pasar después de un
a[0] que ya redujo la dimensión.
Causa: el lado derecho de la asignación no entra en la forma del izquierdo.
Arreglo: o le pasás tantos valores como posiciones, o le pasás uno solo y NumPy lo repite.
Causa: las dos son vistas del mismo array.
Arreglo: .base is None para confirmarlo, y
.copy() para separarlas.
Causa: el indexado booleano y el indexado por lista devuelven copias: escribiste en un array temporal.
Arreglo: asignar directamente sobre el indexado
(a[a > 3] = 0), no sobre una variable intermedia.
Recetario
| Expresión | Qué hace | ¿Copia? |
|---|---|---|
a[i, j] | Una celda | - |
a[i] / a[:, j] | Fila / columna | Vista |
a[i0:i1, j0:j1] | Rectángulo | Vista |
a[::paso] | De a saltos | Vista |
a[::-1] | Al revés | Vista |
a[[0, 2, 4]] | Posiciones sueltas, en ese orden | Copia |
a[np.ix_(filas, cols)] | El rectángulo de esas filas y columnas | Copia |
a[i] = valor | Asigna a toda la fila | Escribe en el original |
a.copy() | Copia independiente | Copia |
a.base is None | ¿Es dueño de sus datos? | - |
np.argsort(v) | Las posiciones en orden | - |
otro[np.argsort(v)] | Reordena otro por v | Copia |