Solución:
MultiIndex / Indexación avanzada
Nota
Esta publicación estará estructurada de la siguiente manera:
- Las preguntas planteadas en el PO se abordarán una a una.
- Para cada pregunta, se demostrarán uno o más métodos aplicables para resolver este problema y obtener el resultado esperado.
NotaSe incluirán s (muy parecidos a este) para los lectores interesados en aprender sobre funcionalidades adicionales, detalles de implementación y otra información superficial sobre el tema en cuestión. Estas notas se han compilado a través de la búsqueda de documentos y el descubrimiento de varias características oscuras, y de mi propia experiencia (ciertamente limitada).
Todos los ejemplos de código se han creado y probado en pandas v0.23.4, python3.7. Si algo no está claro o es incorrecto en cuanto a los hechos, o si no encontró una solución aplicable a su caso de uso, no dude en sugerir una edición, solicitar una aclaración en los comentarios o abrir una nueva pregunta, … según corresponda .
Aquí hay una introducción a algunos modismos comunes (en adelante, los Cuatro Modismos) que volveremos a visitar con frecuencia.
-
DataFrame.loc
– Una solución general para la selección por etiqueta (+pd.IndexSlice
para aplicaciones más complejas que involucran cortes) -
DataFrame.xs
– Extraiga una sección transversal particular de un Series / DataFrame. -
DataFrame.query
– Especificar operaciones de corte y / o filtrado dinámicamente (es decir, como una expresión que se evalúa dinámicamente. Es más aplicable a algunos escenarios que a otros. Consulte también esta sección de los documentos para realizar consultas sobre índices múltiples). -
Indexación booleana con una máscara generada usando
MultiIndex.get_level_values
(a menudo junto conIndex.isin
, especialmente cuando se filtra con varios valores). Esto también es muy útil en algunas circunstancias.
Será beneficioso observar los diversos problemas de corte y filtrado en términos de los Cuatro Modismos para comprender mejor lo que se puede aplicar a una situación determinada. Es muy importante comprender que no todos los modismos funcionarán igual de bien (si es que funcionan) en todas las circunstancias. Si un idioma no se ha incluido como una posible solución a un problema a continuación, eso significa que el idioma no se puede aplicar a ese problema de manera efectiva.
Pregunta 1
¿Cómo selecciono filas que tienen “a” en el nivel “uno”?
col one two a t 0 u 1 v 2 w 3
Puedes usar loc
, como una solución de propósito general aplicable a la mayoría de situaciones:
df.loc[['a']]
En este punto, si obtiene
TypeError: Expected tuple, got str
Eso significa que estás usando una versión anterior de pandas. ¡Considere actualizar! De lo contrario, use df.loc[('a', slice(None)), :]
.
Alternativamente, puede usar xs
aquí, ya que estamos extrayendo una sola sección transversal. Nota la levels
y axis
argumentos (aquí se pueden asumir valores predeterminados razonables).
df.xs('a', level=0, axis=0, drop_level=False)
# df.xs('a', drop_level=False)
Aquí el drop_level=False
Se necesita un argumento para prevenir xs
de bajar el nivel “uno” en el resultado (el nivel en el que cortamos).
Otra opción más aquí es usar query
:
df.query("one == 'a'")
Si el índice no tiene un nombre, deberá cambiar su cadena de consulta para que sea "ilevel_0 == 'a'"
.
Finalmente, usando get_level_values
:
df[df.index.get_level_values('one') == 'a']
# If your levels are unnamed, or if you need to select by position (not label),
# df[df.index.get_level_values(0) == 'a']
Además, ¿cómo podría bajar el nivel “uno” en la salida?
col two t 0 u 1 v 2 w 3
Esto puede ser fácilmente hecho usando
df.loc['a'] # Notice the single string argument instead the list.
O,
df.xs('a', level=0, axis=0, drop_level=True)
# df.xs('a')
Observe que podemos omitir el drop_level
argumento (se supone que es True
por defecto).
Nota
Puede notar que un DataFrame filtrado todavía puede tener todos los niveles, incluso si no se muestran al imprimir el DataFrame. Por ejemplo,
v = df.loc[['a']] print(v) col one two a t 0 u 1 v 2 w 3 print(v.index) MultiIndex(levels=[['a', 'b', 'c', 'd'], ['t', 'u', 'v', 'w']], labels=[[0, 0, 0, 0], [0, 1, 2, 3]], names=['one', 'two'])
Puede deshacerse de estos niveles usando
MultiIndex.remove_unused_levels
:v.index = v.index.remove_unused_levels()
print(v.index) MultiIndex(levels=[['a'], ['t', 'u', 'v', 'w']], labels=[[0, 0, 0, 0], [0, 1, 2, 3]], names=['one', 'two'])
Pregunta 1b
¿Cómo corte todas las filas con valor “t” en el nivel “dos”?
col one two a t 0 b t 4 t 8 d t 12
Intuitivamente, querrías algo que involucrara slice()
:
df.loc[(slice(None), 't'), :]
¡Simplemente funciona! ™ Pero es torpe. Podemos facilitar una sintaxis de corte más natural usando el pd.IndexSlice
API aquí.
idx = pd.IndexSlice
df.loc[idx[:, 't'], :]
Esto es mucho, mucho más limpio.
Nota
¿Por qué el segmento final
:
a través de las columnas requeridas? Esto es porque,loc
se puede utilizar para seleccionar y cortar a lo largo de ambos ejes (axis=0
o
axis=1
). Sin aclarar explícitamente en qué eje se va a realizar el corte, la operación se vuelve ambigua. Vea el gran recuadro rojo en la documentación sobre rebanar.Si desea eliminar cualquier sombra de ambigüedad,
loc
acepta unaxis
parámetro:df.loc(axis=0)[pd.IndexSlice[:, 't']]
Sin el
axis
parámetro (es decir, simplemente haciendodf.loc[pd.IndexSlice[:, 't']]
), se supone que el corte está en las columnas, y unKeyError
será criado en esta circunstancia.Esto está documentado en segmentaciones. Sin embargo, para el propósito de esta publicación, especificaremos explícitamente todos los ejes.
Con xs
, está
df.xs('t', axis=0, level=1, drop_level=False)
Con query
, está
df.query("two == 't'")
# Or, if the first level has no name,
# df.query("ilevel_1 == 't'")
Y finalmente, con get_level_values
, puedes hacer
df[df.index.get_level_values('two') == 't']
# Or, to perform selection by position/integer,
# df[df.index.get_level_values(1) == 't']
Todo con el mismo efecto.
Pregunta 2
¿Cómo puedo seleccionar filas correspondientes a los elementos “b” y “d” en el nivel “uno”?
col one two b t 4 u 5 v 6 w 7 t 8 d w 11 t 12 u 13 v 14 w 15
Usando loc, esto se hace de manera similar especificando una lista.
df.loc[['b', 'd']]
Para resolver el problema anterior de seleccionar “b” y “d”, también puede utilizar query
:
items = ['b', 'd']
df.query("one in @items")
# df.query("one == @items", parser='pandas')
# df.query("one in ['b', 'd']")
# df.query("one == ['b', 'd']", parser='pandas')
Nota
Sí, el analizador predeterminado es
'pandas'
, pero es importante resaltar que esta sintaxis no es Python convencional. El analizador de Pandas genera un árbol de análisis ligeramente diferente al de la expresión. Esto se hace para que algunas operaciones sean más intuitivas de especificar. Para obtener más información, lea mi publicación sobre Evaluación de expresiones dinámicas en pandas usando pd.eval ().
Y con get_level_values
+ Index.isin
:
df[df.index.get_level_values("one").isin(['b', 'd'])]
Pregunta 2b
¿Cómo obtendría todos los valores correspondientes a “t” y “w” en el nivel “dos”?
col one two a t 0 w 3 b t 4 w 7 t 8 d w 11 t 12 w 15
Con loc
, Esto es posible solamente en conjunción con pd.IndexSlice
.
df.loc[pd.IndexSlice[:, ['t', 'w']], :]
El primer colon :
en pd.IndexSlice[:, ['t', 'w']]
significa cortar a través del primer nivel. A medida que aumenta la profundidad del nivel que se consulta, deberá especificar más sectores, uno por nivel. No necesitarás especificar más niveles más allá de el que está siendo cortado, sin embargo.
Con query
, este es
items = ['t', 'w']
df.query("two in @items")
# df.query("two == @items", parser='pandas')
# df.query("two in ['t', 'w']")
# df.query("two == ['t', 'w']", parser='pandas')
Con get_level_values
y Index.isin
(similar al anterior):
df[df.index.get_level_values('two').isin(['t', 'w'])]
Pregunta 3
¿Cómo recupero una sección transversal, es decir, una sola fila que tiene valores específicos para el índice de
df
? Específicamente, ¿cómo recupero la sección transversal de('c', 'u')
, dada porcol one two c u 9
Usar loc
especificando una tupla de claves:
df.loc[('c', 'u'), :]
O,
df.loc[pd.IndexSlice[('c', 'u')]]
Nota
En este punto, puede encontrarse con un
PerformanceWarning
que se ve así:PerformanceWarning: indexing past lexsort depth may impact performance.
Esto solo significa que su índice no está ordenado. pandas depende del índice que se esté ordenando (en este caso, lexicográficamente, ya que estamos tratando con valores de cadena) para una búsqueda y recuperación óptimas. Una solución rápida sería ordenar su DataFrame por adelantado usando
DataFrame.sort_index
. Esto es especialmente deseable desde el punto de vista del rendimiento si planea realizar varias consultas de este tipo en conjunto:df_sort = df.sort_index() df_sort.loc[('c', 'u')]
También puedes usar
MultiIndex.is_lexsorted()
para comprobar si el índice está ordenado o no. Esta función devuelveTrue
oFalse
respectivamente. Puede llamar a esta función para determinar si se requiere un paso de clasificación adicional o no.
Con xs
, esto nuevamente es simplemente pasar una sola tupla como primer argumento, con todos los demás argumentos configurados en sus valores predeterminados apropiados:
df.xs(('c', 'u'))
Con query
, las cosas se vuelven un poco torpes:
df.query("one == 'c' and two == 'u'")
Ahora puede ver que esto va a ser relativamente difícil de generalizar. Pero todavía está bien para este problema en particular.
Con accesos que abarcan varios niveles, get_level_values
todavía se puede utilizar, pero no se recomienda:
m1 = (df.index.get_level_values('one') == 'c')
m2 = (df.index.get_level_values('two') == 'u')
df[m1 & m2]
Pregunta 4
¿Cómo selecciono las dos filas correspondientes a
('c', 'u')
, y('a', 'w')
?col one two c u 9 a w 3
Con loc
, esto sigue siendo tan simple como:
df.loc[[('c', 'u'), ('a', 'w')]]
# df.loc[pd.IndexSlice[[('c', 'u'), ('a', 'w')]]]
Con query
, deberá generar dinámicamente una cadena de consulta iterando sobre sus secciones transversales y niveles:
cses = [('c', 'u'), ('a', 'w')]
levels = ['one', 'two']
# This is a useful check to make in advance.
assert all(len(levels) == len(cs) for cs in cses)
query = '(' + ') or ('.join([
' and '.join([f"(l == repr(c))" for l, c in zip(levels, cs)])
for cs in cses
]) + ')'
print(query)
# ((one == 'c') and (two == 'u')) or ((one == 'a') and (two == 'w'))
df.query(query)
¡100% NO RECOMIENDO! Pero es posible.
¿Y si tengo varios niveles?
Una opción en este escenario sería utilizar droplevel
para eliminar los niveles que no estás comprobando, luego usa isin
para probar la membresía, y luego un índice booleano en el resultado final.
df[df.index.droplevel(unused_level).isin([('c', 'u'), ('a', 'w')])]
Pregunta 5
¿Cómo puedo recuperar todas las filas correspondientes a “a” en el nivel “uno” o “t” en el nivel “dos”?
col one two a t 0 u 1 v 2 w 3 b t 4 t 8 d t 12
En realidad, esto es muy difícil de hacer con loc
asegurando la corrección y aún manteniendo la claridad del código. df.loc[pd.IndexSlice['a', 't']]
es incorrecta, se interpreta como df.loc[pd.IndexSlice[('a', 't')]]
(es decir, seleccionar una sección transversal). Puede pensar en una solución con pd.concat
para manipular cada etiqueta por separado:
pd.concat([
df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:]
])
col
one two
a t 0
u 1
v 2
w 3
t 0 # Does this look right to you? No, it isn't!
b t 4
t 8
d t 12
Pero notará que una de las filas está duplicada. Esto se debe a que esa fila cumplió ambas condiciones de corte y, por lo tanto, apareció dos veces. En su lugar, necesitarás hacer
v = pd.concat([
df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:]
])
v[~v.index.duplicated()]
Pero si su DataFrame contiene inherentemente índices duplicados (que desea), esto no los retendrá. Úselo con extrema precaución.
Con query
, esto es estúpidamente simple:
df.query("one == 'a' or two == 't'")
Con get_level_values
, esto sigue siendo simple, pero no tan elegante:
m1 = (df.index.get_level_values('one') == 'a')
m2 = (df.index.get_level_values('two') == 't')
df[m1 | m2]
Pregunta 6
¿Cómo puedo cortar secciones transversales específicas? Para “a” y “b”, me gustaría seleccionar todas las filas con subniveles “u” y “v”, y para “d”, me gustaría seleccionar filas con subnivel “w”.
col one two a u 1 v 2 b u 5 v 6 d w 11 w 15
Este es un caso especial que agregué para ayudar a comprender la aplicabilidad de los cuatro modismos; este es un caso en el que ninguno de ellos funcionará de manera efectiva, ya que el corte es muy específico, y no sigue ningún patrón real.
Por lo general, cortar problemas como este requerirá pasar explícitamente una lista de claves a loc
. Una forma de hacer esto es con:
keys = [('a', 'u'), ('a', 'v'), ('b', 'u'), ('b', 'v'), ('d', 'w')]
df.loc[keys, :]
Si desea guardar algo de escritura, reconocerá que hay un patrón para cortar “a”, “b” y sus subniveles, por lo que podemos separar la tarea de corte en dos porciones y concat
el resultado:
pd.concat([
df.loc[(('a', 'b'), ('u', 'v')), :],
df.loc[('d', 'w'), :]
], axis=0)
La especificación de corte para “a” y “b” es un poco más limpia (('a', 'b'), ('u', 'v'))
porque los mismos subniveles que se indexan son los mismos para cada nivel.
Pregunta 7
¿Cómo obtengo todas las filas donde los valores en el nivel “dos” son mayores que 5?
col one two b 7 4 9 5 c 7 10 d 6 11 8 12 8 13 6 15
Esto se puede hacer usando query
,
df2.query("two > 5")
Y get_level_values
.
df2[df2.index.get_level_values('two') > 5]
Nota
De manera similar a este ejemplo, podemos filtrar en función de cualquier condición arbitraria utilizando estas construcciones. En general, es útil recordar que
loc
yxs
son específicamente para la indexación basada en etiquetas, mientras quequery
y
get_level_values
son útiles para crear máscaras condicionales generales para el filtrado.
Pregunta extra
¿Qué pasa si necesito cortar un
MultiIndex
columna?
En realidad, la mayoría de las soluciones aquí también son aplicables a las columnas, con cambios menores. Considerar:
np.random.seed(0)
mux3 = pd.MultiIndex.from_product([
list('ABCD'), list('efgh')
], names=['one','two'])
df3 = pd.DataFrame(np.random.choice(10, (3, len(mux))), columns=mux3)
print(df3)
one A B C D
two e f g h e f g h e f g h e f g h
0 5 0 3 3 7 9 3 5 2 4 7 6 8 8 1 6
1 7 7 8 1 5 9 8 9 4 3 0 3 5 0 2 3
2 8 1 3 3 3 7 0 1 9 9 0 4 7 3 2 7
Estos son los siguientes cambios que deberá realizar en los cuatro modismos para que funcionen con columnas.
-
Para cortar con
loc
, usardf3.loc[:, ....] # Notice how we slice across the index with `:`.
o,
df3.loc[:, pd.IndexSlice[...]]
-
Usar
xs
según corresponda, simplemente pase un argumentoaxis=1
. -
Puede acceder a los valores de nivel de columna directamente usando
df.columns.get_level_values
. Entonces necesitarás hacer algo comodf.loc[:, condition]
Dónde
condition
representa alguna condición construida usandocolumns.get_level_values
. -
Usar
query
, su única opción es transponer, consultar el índice y transponer nuevamente:df3.T.query(...).T
No recomendado, use una de las otras 3 opciones.
Recientemente, me encontré con un caso de uso en el que tenía un marco de datos de índices múltiples de más de 3 niveles en el que no podía hacer que ninguna de las soluciones anteriores produjera los resultados que estaba buscando. Es muy posible que las soluciones anteriores funcionen, por supuesto, para mi caso de uso, y probé varias, sin embargo, no pude hacer que funcionaran con el tiempo que tenía disponible.
Estoy lejos de ser un experto, pero me encontré con una solución que no figuraba en las respuestas completas anteriores. No ofrezco ninguna garantía de que las soluciones sean óptimas de alguna manera.
Esta es una forma diferente de obtener un resultado ligeramente diferente al de la Pregunta 6 anterior. (y probablemente otras preguntas también)
Específicamente estaba buscando:
- Una forma de elegir dos valores + de un nivel del índice y un valor único de otro nivel del índice, y
- Una forma de dejar los valores de índice de la operación anterior en la salida del marco de datos.
Como una llave inglesa en los engranajes (aunque totalmente reparable):
- Los índices no tenían nombre.
En el marco de datos del juguete a continuación:
index = pd.MultiIndex.from_product([['a','b'],
['stock1','stock2','stock3'],
['price','volume','velocity']])
df = pd.DataFrame([1,2,3,4,5,6,7,8,9,
10,11,12,13,14,15,16,17,18],
index)
0
a stock1 price 1
volume 2
velocity 3
stock2 price 4
volume 5
velocity 6
stock3 price 7
volume 8
velocity 9
b stock1 price 10
volume 11
velocity 12
stock2 price 13
volume 14
velocity 15
stock3 price 16
volume 17
velocity 18
El uso de los siguientes trabajos, por supuesto:
df.xs(('stock1', 'velocity'), level=(1,2))
0
a 3
b 12
Pero quería un resultado diferente, por lo que mi método para obtener ese resultado fue:
df.iloc[df.index.isin(['stock1'], level=1) &
df.index.isin(['velocity'], level=2)]
0
a stock1 velocity 3
b stock1 velocity 12
Y si quisiera dos valores + de un nivel y un valor único (o 2+) de otro nivel:
df.iloc[df.index.isin(['stock1','stock3'], level=1) &
df.index.isin(['velocity'], level=2)]
0
a stock1 velocity 3
stock3 velocity 9
b stock1 velocity 12
stock3 velocity 18
El método anterior es probablemente un poco torpe, sin embargo, encontré que satisfacía mis necesidades y, como beneficio adicional, era más fácil de entender y leer.
Nos puedes corroborar nuestro ensayo exponiendo un comentario y dejando una valoración te lo agradecemos.