Compruebe si la columna pandas contiene todos los elementos de una lista

20

Tengo un df como este:

frame = pd.DataFrame({'a' : ['a,b,c', 'a,c,f', 'b,d,f','a,z,c']})

Y una lista de artículos:

letters = ['a','c']

Mi objetivo es obtener todas las filas frameque contienen al menos los 2 elementos enletters

Se me ocurrió esta solución:

for i in letters:
    subframe = frame[frame['a'].str.contains(i)]

Esto me da lo que quiero, pero podría no ser la mejor solución en términos de escalabilidad. ¿Hay alguna solución 'vectorizada'? Gracias

Kauber
fuente
44
Le dará solo filas que contienen la última letra porque anula el subtrama en cualquier iteración
Tom Ron
@TomRon Tienes razón, qué error :)
Kauber

Respuestas:

12

Construiría una lista de Series y luego aplicaría un vectorizado np.all:

contains = [frame['a'].str.contains(i) for i in letters]
resul = frame[np.all(contains, axis=0)]

Da como se esperaba:

       a
0  a,b,c
1  a,c,f
3  a,z,c
Serge Ballesta
fuente
3
¡Felicidades por 100k!
Peter Haddad
14

Una forma es dividir los valores de las columnas en listas usando str.split, y verificar si set(letters)es una subsetde las listas obtenidas:

letters_s = set(letters)
frame[frame.a.str.split(',').map(letters_s.issubset)]

     a
0  a,b,c
1  a,c,f
3  a,z,c

Punto de referencia:

def serge(frame):
    contains = [frame['a'].str.contains(i) for i in letters]
    return frame[np.all(contains, axis=0)]

def yatu(frame):
    letters_s = set(letters)
    return frame[frame.a.str.split(',').map(letters_s.issubset)]

def austin(frame):
    mask =  frame.a.apply(lambda x: np.intersect1d(x.split(','), letters).size > 0)
    return frame[mask]

def datanovice(frame):
    s = frame['a'].str.split(',').explode().isin(letters).groupby(level=0).cumsum()
    return frame.loc[s[s.ge(2)].index.unique()]

perfplot.show(
    setup=lambda n: pd.concat([frame]*n, axis=0).reset_index(drop=True), 

    kernels=[
        lambda df: serge(df),
        lambda df: yatu(df),
        lambda df: df[df['a'].apply(lambda x: np.all([*map(lambda l: l in x, letters)]))],
        lambda df: austin(df),
        lambda df: datanovice(df),
    ],

    labels=['serge', 'yatu', 'bruno','austin', 'datanovice'],
    n_range=[2**k for k in range(0, 18)],
    equality_check=lambda x, y: x.equals(y),
    xlabel='N'
)

ingrese la descripción de la imagen aquí

yatu
fuente
Me sale TypeError: unhashable type: 'set'cuando ejecuto su código? lo ejecutó en el marco proporcionado anteriormente
Datanovice
Que versión @Datanovice Comprobación doble y todo parece estar bien
yatu
mis pandas son 1.0.3y python 3.7probablemente solo soy yo
Datanovice
3
@Datanovice, creo que necesitas Python 3.8 para esto :)
anky
2
Gracias, recibo el mismo error que @Datanovice y desafortunadamente no puedo saltar a Python 3.8
Kauber
7

Puedes usar np.intersect1d:

import pandas as pd
import numpy as np

frame = pd.DataFrame({'a' : ['a,b,c', 'a,c,f', 'b,d,f','a,z,c']})
letters = ['a','c']

mask =  frame.a.apply(lambda x: np.intersect1d(x.split(','), letters).size > 0)
print(frame[mask])

    a
0  a,b,c
1  a,c,f
3  a,z,c
Austin
fuente
7

Esto también lo resuelve:

frame[frame['a'].apply(lambda x: np.all([*map(lambda l: l in x, letters)]))]
Bruno Mello
fuente
6

Utilice set.issubset :

frame = pd.DataFrame({'a' : ['a,b,c', 'a,c,f', 'b,d,f','a,z,c','x,y']})
letters = ['a','c']

frame[frame['a'].apply(lambda x: set(letters).issubset(x))]

Out:

       a
0  a,b,c
1  a,c,f
3  a,z,c
ManojK
fuente
5

IIUC explodey un filtro booleano

la idea es crear una serie única, luego podemos agrupar por índice el recuento de las ocurrencias verdaderas de su lista usando una suma acumulativa

s = frame['a'].str.split(',').explode().isin(letters).groupby(level=0).cumsum()

print(s)

0    1.0
0    1.0
0    2.0
1    1.0
1    2.0
1    2.0
2    0.0
2    0.0
2    0.0
3    1.0
3    1.0
3    2.0

frame.loc[s[s.ge(2)].index.unique()]

out:

       a
0  a,b,c
1  a,c,f
3  a,z,c
Datanovice
fuente
1
frame.iloc[[x for x in range(len(frame)) if set(letters).issubset(frame.iloc[x,0])]]

salida:

        a
 0  a,b,c
 1  a,c,f
 3  a,z,c

cronométralo

%%timeit
#hermes
frame.iloc[[x for x in range(len(frame)) if set(letters).issubset(frame.iloc[x,0])]]

salida

300 µs ± 32.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Hermes Morales
fuente