python - Seleccionar filas en pandas MultiIndex DataFrame
slice multi-index (1)
MultiIndex / Indexación Avanzada
Nota
Este post se estructurará de la siguiente manera:
- Las preguntas planteadas en el OP serán abordadas una por una.
- 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 compiladas 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.
-
DataFrame.loc
: una solución general para la selección por etiqueta (+pd.IndexSlice
para aplicaciones más complejas que involucran segmentos) -
DataFrame.xs
- Extrae una sección transversal particular de un Series / DataFrame. -
DataFrame.query
: especifique dinámicamente las operaciones deDataFrame.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. -
La indexación booleana con una máscara generada mediante
MultiIndex.get_level_values
(a menudo junto conIndex.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 a t 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 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 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 a t 0 b t 4 t 8 d t 12
Intuitivamente, querrá algo que involucre
slice()
:
df.loc[(slice(None), ''t''), :]
¡Simplemente funciona! ™ Pero es torpe.
Podemos facilitar una sintaxis 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 usarloc
para seleccionar y dividir a lo largo de ambos ejes (axis=0
oaxis=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 deaxis
:
df.loc(axis=0)[pd.IndexSlice[:, ''t'']]
Sin el parámetro de
axis
(es decir, solo haciendodf.loc[pd.IndexSlice[:, ''t'']]
), se supone que el corte está en las columnas, y en este caso se levantará unKeyError
.Esto está documentado en slicers de slicers . 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 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 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 sintaxis 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 a t 0 w 3 b t 4 w 7 t 8 d w 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 c u 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 unaPerformanceWarning
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 devuelveTrue
oFalse
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 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 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" 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
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
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 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 a u 1 v 2 b u 5 v 6 d w 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 queloc
yxs
son específicamente para la indexación basada en etiquetas, mientras quequery
yget_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 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 idiomas para que trabajen con columnas.
-
Para rebanar con
loc
, usedf3.loc[:, ....] # Notice how we slice across the index with `:`.
O,
df3.loc[:, pd.IndexSlice[...]]
-
Para usar
xs
según sea apropiado, simplemente pase un argumentoaxis=1
. -
Puede acceder a los valores de nivel de columna directamente usando
df.columns.get_level_values
. A continuación, tendrá que hacer algo comodf.loc[:, {condition}]
Donde
{condition}
representa alguna condición creada usandocolumns.get_level_values
. -
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.
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 objetivo 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 etiquetas 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 a t 0 u 1 v 2 w 3 b t 4 u 5 v 6 w 7 t 8 c u 9 v 10 d w 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
a t 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
a t 0
b t 4
t 8
d t 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
b t 4
u 5
v 6
w 7
t 8
d w 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
a t 0
w 3
b t 4
w 7
t 8
d w 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
c u 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
c u 9
a w 3
Pregunta 5:
Un artículo cortado por nivel
¿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
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
a u 1
v 2
b u 5
v 6
d w 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