python string pandas dataframe series

python - split pandas



Filtrado de pandas para mĂșltiples subcadenas en serie. (2)

Necesito filtrar filas en un marco de datos de pandas para que una columna de cadena específica contenga al menos una de una lista de subcadenas proporcionadas. Las subcadenas pueden tener caracteres inusuales / regex. La comparación no debe incluir expresiones regulares y no distingue entre mayúsculas y minúsculas.

Por ejemplo:

lst = [''kdSj;af-!?'', ''aBC+dsfa?/-'', ''sdKaJg|dksaf-*'']

Actualmente aplico la máscara de esta manera:

mask = np.logical_or.reduce([df[col].str.contains(i, regex=False, case=False) for i in lst]) df = df[mask]

Mi marco de datos es grande (~ 1mio filas) y lst tiene una longitud de 100. ¿Hay alguna forma más eficiente? Por ejemplo, si se encuentra el primer elemento en lst , no deberíamos tener que probar ninguna cadena posterior para esa fila.


Podrías intentar usar el algoritmo de Aho-Corasick . En el caso promedio, es O(n+m+p) donde n es la longitud de las cadenas de búsqueda y m es la longitud del texto buscado y p es el número de coincidencias de salida.

El algoritmo Aho-Corasick se usa a menudo para encontrar múltiples patrones (agujas) en un texto de entrada (el pajar).

pyahocorasick es un envoltorio de Python alrededor de una implementación en C del algoritmo.

Vamos a comparar lo rápido que es frente a algunas alternativas. A continuación se muestra un punto de referencia que muestra que using_aho_corasick es más de using_aho_corasick más rápido que el método original (que se muestra en la pregunta) en un caso de prueba DataFrame de 50K filas:

| | speed factor | ms per loop | | | compared to orig | | |--------------------+------------------+-------------| | using_aho_corasick | 30.7x | 140 | | using_regex | 2.7x | 1580 | | orig | 1.0x | 4300 |

In [89]: %timeit using_ahocorasick(col, lst) 10 loops, best of 3: 140 ms per loop In [88]: %timeit using_regex(col, lst) 1 loop, best of 3: 1.58 s per loop In [91]: %timeit orig(col, lst) 1 loop, best of 3: 4.3 s per loop

Aquí la configuración utilizada para el punto de referencia. También verifica que la salida coincida con el resultado devuelto por orig :

import numpy as np import random import pandas as pd import ahocorasick import re random.seed(321) def orig(col, lst): mask = np.logical_or.reduce([col.str.contains(i, regex=False, case=False) for i in lst]) return mask def using_regex(col, lst): """https://.com/a/48590850/190597 (Alex Riley)""" esc_lst = [re.escape(s) for s in lst] pattern = ''|''.join(esc_lst) mask = col.str.contains(pattern, case=False) return mask def using_ahocorasick(col, lst): A = ahocorasick.Automaton(ahocorasick.STORE_INTS) for word in lst: A.add_word(word.lower()) A.make_automaton() col = col.str.lower() mask = col.apply(lambda x: bool(list(A.iter(x)))) return mask N = 50000 # 100 substrings of 5 characters lst = [''''.join([chr(random.randint(0, 256)) for _ in range(5)]) for _ in range(100)] # N strings of 20 characters strings = [''''.join([chr(random.randint(0, 256)) for _ in range(20)]) for _ in range(N)] # make about 10% of the strings match a string from lst; this helps check that our method works strings = [_ if random.randint(0, 99) < 10 else _+random.choice(lst) for _ in strings] col = pd.Series(strings) expected = orig(col, lst) for name, result in [(''using_regex'', using_regex(col, lst)), (''using_ahocorasick'', using_ahocorasick(col, lst))]: status = ''pass'' if np.allclose(expected, result) else ''fail'' print(''{}: {}''.format(name, status))


Si te limitas a usar pandas puros, tanto para el rendimiento como para la practicidad, creo que deberías usar expresiones regulares para esta tarea. Sin embargo, primero tendrá que escapar de cualquier carácter especial en las subcadenas para asegurarse de que coincidan literalmente (y no se utilicen como metacaracteres de expresiones regulares).

Esto es fácil de hacer usando re.escape :

>>> import re >>> esc_lst = [re.escape(s) for s in lst]

Estas subcadenas escapadas se pueden unir usando una tubería de expresiones regulares | . Cada una de las subcadenas se puede comparar con una cadena hasta que una coincida (o todas se hayan probado).

>>> pattern = ''|''.join(esc_lst)

La etapa de enmascaramiento se convierte en un solo bucle de bajo nivel a través de las filas:

df[col].str.contains(pattern, case=False)

Aquí hay una configuración simple para obtener una sensación de rendimiento:

from random import randint, seed seed(321) # 100 substrings of 5 characters lst = [''''.join([chr(randint(0, 256)) for _ in range(5)]) for _ in range(100)] # 50000 strings of 20 characters strings = [''''.join([chr(randint(0, 256)) for _ in range(20)]) for _ in range(50000)] col = pd.Series(strings) esc_lst = [re.escape(s) for s in lst] pattern = ''|''.join(esc_lst)

El método propuesto toma aproximadamente 1 segundo (por lo tanto, tal vez hasta 20 segundos para 1 millón de filas):

%timeit col.str.contains(pattern, case=False) 1 loop, best of 3: 981 ms per loop

El método en la pregunta tomó aproximadamente 5 segundos usando los mismos datos de entrada.

Vale la pena señalar que estos tiempos son "peores casos" en el sentido de que no hubo coincidencias (por lo que se verificaron todas las subcadenas). Si hay coincidencias, el tiempo mejorará.