Seleccionar filas en pandas MultiIndex DataFrame

Objetivo y Motivación.

La API MultiIndex ha ido ganando popularidad a lo largo de los años, sin embargo, no todo se entiende completamente en términos de estructura, trabajo y operaciones asociadas.

Una operación importante es el filtrado . El filtrado es un requisito común, pero los casos de uso son diversos. En consecuencia, ciertos métodos y funciones serán más aplicables a algunos casos de uso que a otros.

En resumen, el objective de este post es abordar algunos problemas comunes de filtrado y casos de uso, demostrar varios métodos diferentes para resolver estos problemas y discutir su aplicabilidad. Algunas de las preguntas de alto nivel que esta publicación pretende abordar son:

  • Rebanado basado en un solo valor / etiqueta
  • Rebanado basado en múltiples tags de uno o más niveles
  • Filtrado en condiciones booleanas y expresiones
  • ¿Qué métodos son aplicables en qué circunstancias?

Estos problemas se han dividido en 6 preguntas concretas, enumeradas a continuación. Para simplificar, los DataFrames de ejemplo en la configuración a continuación solo tienen dos niveles y no tienen claves de índice duplicadas. La mayoría de las soluciones presentadas a los problemas pueden generalizarse a N niveles.

Esta publicación no explicará cómo crear MultiIndexes, cómo realizar operaciones de asignación en ellos o cualquier discusión relacionada con el rendimiento (estos son temas separados para otro momento).


Preguntas

La pregunta 1-6 se hará en contexto a la configuración a continuación.

 mux = pd.MultiIndex.from_arrays([ list('aaaabbbbbccddddd'), list('tuvwtuvwtuvwtuvw') ], names=['one', 'two']) df = pd.DataFrame({'col': np.arange(len(mux))}, mux) col one two at 0 u 1 v 2 w 3 bt 4 u 5 v 6 w 7 t 8 cu 9 v 10 dw 11 t 12 u 13 v 14 w 15 

Pregunta 1: Selección de un solo artículo
¿Cómo selecciono las filas que tienen “a” en el nivel “uno”?

  col one two at 0 u 1 v 2 w 3 

Además, ¿cómo podría bajar el nivel “uno” en la salida?

  col two t 0 u 1 v 2 w 3 

Pregunta 1b
¿Cómo puedo dividir todas las filas con el valor “t” en el nivel “dos”?

  col one two at 0 bt 4 t 8 dt 12 

Pregunta 2: Selección de múltiples valores en un nivel
¿Cómo puedo seleccionar las filas correspondientes a los elementos “b” y “d” en el nivel “uno”?

  col one two bt 4 u 5 v 6 w 7 t 8 dw 11 t 12 u 13 v 14 w 15 

Pregunta 2b
¿Cómo obtendría todos los valores correspondientes a “t” y “w” en el nivel “dos”?

  col one two at 0 w 3 bt 4 w 7 t 8 dw 11 t 12 w 15 

Pregunta 3: Cortar una única sección transversal (x, y)
¿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 por

  col one two cu 9 

Pregunta 4: Cortar varias secciones transversales [(a, b), (c, d), ...]
¿Cómo selecciono las dos filas correspondientes a ('c', 'u') y ('a', 'w') ?

  col one two cu 9 aw 3 

Pregunta 5: Un artículo cortado por nivel
¿Cómo puedo recuperar todas las filas correspondientes a “a” en el nivel “uno” y “u” en el nivel “dos”?

  col one two at 0 u 1 v 2 w 3 bt 4 t 8 dt 12 

Pregunta 6: Rebanado arbitrario
¿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 au 1 v 2 bu 5 v 6 dw 11 w 15 

La pregunta 7 utilizará una configuración única que consiste en un nivel numérico:

 np.random.seed(0) mux2 = pd.MultiIndex.from_arrays([ list('aaaabbbbbccddddd'), np.random.choice(10, size=16) ], names=['one', 'two']) df2 = pd.DataFrame({'col': np.arange(len(mux2))}, mux2) col one two a 5 0 0 1 3 2 3 3 b 7 4 9 5 3 6 5 7 2 8 c 4 9 7 10 d 6 11 8 12 8 13 1 14 6 15 

Pregunta 7: Filtrado basado en la desigualdad en niveles numéricos
¿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 

MultiIndex / Indexación Avanzada

Nota
Este post se estructurará de la siguiente manera:

  1. Las preguntas planteadas en el OP serán abordadas una por una.
  2. Para cada pregunta, se demostrarán uno o más métodos aplicables para resolver este problema y obtener el resultado esperado.

Se incluirán notas (muy parecidas a esta) para los lectores interesados ​​en aprender sobre la funcionalidad adicional, los detalles de la implementación y otra información sobre el tema en cuestión. Estas notas han sido comstackdas a través de la exploración de los documentos y el descubrimiento de varias características oscuras, y de mi propia experiencia (la verdad es que es limitada).

Todos los ejemplos de código se han creado y probado en pandas v0.23.4, python3.7 . Si algo no está claro, es incorrecto 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 (de aquí en adelante denominados los Cuatro Idiotas) que volveremos a visitar con frecuencia.

  1. DataFrame.loc : una solución general para la selección por etiqueta (+ pd.IndexSlice para aplicaciones más complejas que involucran segmentos)

  2. DataFrame.xs – Extrae una sección transversal particular de un Series / DataFrame.

  3. DataFrame.query : especifique dinámicamente las operaciones de DataFrame.query y / o filtrado (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 en MultiIndexes.

  4. La indexación booleana con una máscara generada mediante MultiIndex.get_level_values (a menudo junto con Index.isin , especialmente cuando se filtra con múltiples valores). Esto también es bastante útil en algunas circunstancias.

Será beneficioso observar los diversos problemas de división y filtrado en términos de los Cuatro Modismos para obtener una mejor comprensión de lo que se puede aplicar a una situación determinada. Es muy importante entender que no todos los modismos funcionarán igual de bien (si es que lo hacen) en todas las circunstancias. Si un idioma no se ha listado como una solución potencial 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 las filas que tienen “a” en el nivel “uno”?

  col one two at 0 u 1 v 2 w 3 

Puede usar loc como una solución de propósito general aplicable a la mayoría de las situaciones:

 df.loc[['a']] 

En este punto, si consigues

 TypeError: Expected tuple, got str 

Eso significa que estás usando una versión anterior de pandas. Considere la posibilidad de actualizar! De lo contrario, use df.loc[('a', slice(None)), :] .

Alternativamente, puede usar xs aquí, ya que estamos extrayendo una sola sección transversal. Tenga en cuenta los levels y los argumentos del axis (se pueden asumir valores predeterminados razonables aquí).

 df.xs('a', level=0, axis=0, drop_level=False) # df.xs('a', drop_level=False) 

Aquí, se necesita el argumento drop_level=False para evitar que xs caiga el nivel “uno” en el resultado (el nivel en el que cortamos).

Otra opción aquí es usar la query :

 df.query("one == 'a'") 

Si el índice no tenía un nombre, tendría que cambiar la 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 se puede hacer fácilmente 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') 

Tenga en cuenta que podemos omitir el argumento drop_level (se asume que es True de forma predeterminada).

Nota
Puede observar que un DataFrame filtrado aún 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 at 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 utilizando 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 puedo dividir todas las filas con el valor “t” en el nivel “dos”?

  col one two at 0 bt 4 t 8 dt 12 

Intuitivamente, querrá algo que involucre slice() :

 df.loc[(slice(None), 't'), :] 

¡Simplemente funciona! ™ Pero es torpe. Podemos facilitar una syntax de corte más natural utilizando la API de pd.IndexSlice aquí.

 idx = pd.IndexSlice df.loc[idx[:, 't'], :] 

Esto es mucho, mucho más limpio.

Nota
¿Por qué es la porción final : través de las columnas requeridas? Esto se debe a que se puede usar loc para seleccionar y dividir a lo largo de ambos ejes ( axis=0 o axis=1 ). Sin dejar explícitamente en claro en qué eje se va a realizar el corte, la operación se vuelve ambigua. Vea el recuadro rojo grande en la documentación sobre rebanado .

Si desea eliminar cualquier sombra de ambigüedad, loc acepta un parámetro de axis :

 df.loc(axis=0)[pd.IndexSlice[:, 't']] 

Sin el parámetro de axis (es decir, solo haciendo df.loc[pd.IndexSlice[:, 't']] ), se supone que el corte está en las columnas, y en este caso se levantará un KeyError .

Esto está documentado en máquinas de cortar . Para el propósito de este post, sin embargo, especificaremos explícitamente todos los ejes.

Con xs , es

 df.xs('t', axis=0, level=1, drop_level=False) 

Con query , es

 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'] 

Todos al mismo efecto.


Pregunta 2

¿Cómo puedo seleccionar las filas correspondientes a los elementos “b” y “d” en el nivel “uno”?

  col one two bt 4 u 5 v 6 w 7 t 8 dw 11 t 12 u 13 v 14 w 15 

Usando loc, esto se hace de una manera similar al especificar una lista.

 df.loc[['b', 'd']] 

Para resolver el problema anterior de seleccionar “b” y “d”, también puede usar la 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 syntax no es convencionalmente python. El analizador de Pandas genera un árbol de análisis ligeramente diferente 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 expresión dinámica 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 at 0 w 3 bt 4 w 7 t 8 dw 11 t 12 w 15 

Con loc , esto es posible solo en pd.IndexSlice con pd.IndexSlice .

 df.loc[pd.IndexSlice[:, ['t', 'w']], :] 

Los primeros dos puntos : en pd.IndexSlice[:, ['t', 'w']] significa dividir el primer nivel. A medida que aumente la profundidad del nivel que se está consultando, deberá especificar más segmentos, uno por nivel que se divide. Sin embargo, no necesitará especificar más niveles más allá del que se está cortando.

Con query , esto 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 e Index.isin (similar a arriba):

 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 por

  col one two cu 9 

Use loc especificando una tupla de claves:

 df.loc[('c', 'u'), :] 

O,

 df.loc[pd.IndexSlice[('c', 'u')]] 

Nota
En este punto, puede encontrarse con una PerformanceWarning que se parece a esto:

 PerformanceWarning: indexing past lexsort depth may impact performance. 

Esto solo significa que su índice no está ordenado. Los pandas dependen de la clasificación del índice (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 utilizando DataFrame.sort_index . Esto es especialmente deseable desde el punto de vista del rendimiento si planea realizar múltiples consultas de este tipo en conjunto:

 df_sort = df.sort_index() df_sort.loc[('c', 'u')] 

También puede usar MultiIndex.is_lexsorted() para verificar si el índice está ordenado o no. Esta función devuelve True o False consecuencia. Puede llamar a esta función para determinar si se requiere un paso de clasificación adicional o no.

Con xs , esto es, nuevamente, simplemente pasando una sola tupla como primer argumento, con todos los demás argumentos establecidos a sus valores predeterminados apropiados:

 df.xs(('c', 'u')) 

Con la 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 múltiples niveles, se pueden usar get_level_values , 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 cu 9 aw 3 

Con loc , esto sigue siendo tan simple como:

 df.loc[[('c', 'u'), ('a', 'w')]] # df.loc[pd.IndexSlice[[('c', 'u'), ('a', 'w')]]] 

Con la query , deberá generar dinámicamente una cadena de consulta mediante la iteración de sus secciones y niveles transversales:

 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 RECOMIENDA! Pero es posible.


Pregunta 5

¿Cómo puedo recuperar todas las filas correspondientes a “a” en el nivel “uno” y “u” en el nivel “dos”?

  col one two at 0 u 1 v 2 w 3 bt 4 t 8 dt 12 

Esto es realmente muy difícil de hacer con loc mientras se asegura la corrección y se mantiene la claridad del código. df.loc[pd.IndexSlice['a', 't']] es incorrecto, se interpreta como df.loc[pd.IndexSlice[('a', 't')]] (es decir, seleccionando una sección transversal ). Puede pensar en una solución con pd.concat para manejar cada etiqueta por separado:

 pd.concat([ df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:] ]) col one two at 0 u 1 v 2 w 3 t 0 # Does this look right to you? No, it isn't! bt 4 t 8 dt 12 

Pero te darás cuenta de que una de las filas está duplicada. Esto se debe a que esa fila satisfizo ambas condiciones de corte, y así apareció dos veces. En su lugar, tendrá que hacer

 v = pd.concat([ df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:] ]) v[~v.index.duplicated()] 

Pero si su DataFrame contiene índices duplicados (que usted desea), esto no los retendrá. Utilizar con extrema precaución .

Con la 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') == 'c') m2 = (df.index.get_level_values('two') == 'u') 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 au 1 v 2 bu 5 v 6 dw 11 w 15 

Este es un caso especial que he agregado para ayudar a entender la aplicabilidad de los Cuatro Modismos. Este es un caso donde 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, problemas de corte como este requerirán pasar explícitamente una lista de claves para loc . Una forma de hacerlo 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 dividir “a”, “b” y sus subniveles, por lo que podemos dividir la tarea de división en dos partes 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 ligeramente 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 mediante query ,

 df2.query("two > 5") 

Y get_level_values .

 df2[df2.index.get_level_values('two') > 5] 

Nota
Al igual que en este ejemplo, podemos filtrar en función de cualquier condición arbitraria utilizando estas construcciones. En general, es útil recordar que loc y xs son específicamente para la indexación basada en tags, mientras que query y get_level_values son útiles para crear máscaras condicionales generales para el filtrado.


Pregunta extra

¿Qué pasa si necesito cortar una columna MultiIndex ?

En realidad, la mayoría de las soluciones aquí también son aplicables a las columnas, con pequeños cambios. 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 ABCD two efghefghefghefgh 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 idiomas para que trabajen con columnas.

  1. Para rebanar con loc , use

     df3.loc[:, ....] # Notice how we slice across the index with `:`. 

    O,

     df3.loc[:, pd.IndexSlice[...]] 
  2. Para usar xs según sea apropiado, simplemente pase un argumento axis=1 .

  3. Puede acceder a los valores de nivel de columna directamente con df.columns.get_level_values . A continuación, tendrá que hacer algo como

     df.loc[:, {condition}] 

    Donde {condition} representa alguna condición creada usando columns.get_level_values .

  4. Para usar la query , su única opción es transponer, consultar en el índice y transponer nuevamente:

     df3.T.query(...).T 

    No recomendado, utilice una de las otras 3 opciones.