tutorial español python regex python-3.x string performance pandas numpy

español - pandas python



Eliminación rápida de la puntuación con pandas (3)

Esta es una publicación auto contestada. A continuación, describo un problema común en el dominio de la PNL y propongo algunos métodos para resolverlo.

A menudo surge la necesidad de eliminar la puntuación durante la limpieza y el preprocesamiento del texto. La puntuación se define como cualquier carácter en string.punctuation :

>>> import string string.punctuation ''!"#$%&/'()*+,-./:;<=>?@[//]^_`{|}~''

Este es un problema bastante común y se ha preguntado antes de nauseam. La solución más idiomática utiliza pandas str.replace . Sin embargo, para situaciones que involucran una gran cantidad de texto, es posible que se deba considerar una solución más eficaz.

¿Cuáles son algunas alternativas str.replace y str.replace para str.replace al tratar con cientos de miles de registros?


Preparar

Para fines de demostración, consideremos este DataFrame.

df = pd.DataFrame({''text'':[''a..b?!??'', ''%hgh&12'',''abc123!!!'', ''$$$1234'']}) df text 0 a..b?!?? 1 %hgh&12 2 abc123!!! 3 $$$1234

A continuación, enumero las alternativas, una por una, en orden creciente de rendimiento.

str.replace

Esta opción se incluye para establecer el método predeterminado como un punto de referencia para comparar otras soluciones de mayor rendimiento.

Esto utiliza la función str.replace incorporada de str.replace que realiza el reemplazo basado en str.replace regulares.

df[''text''] = df[''text''].str.replace(r''[^/w/s]+'', '''')

df text 0 ab 1 hgh12 2 abc123 3 1234

Esto es muy fácil de codificar, y es bastante legible, pero lento.

regex.sub

Esto implica usar la sub de la biblioteca re . Pre-compile un patrón de regex.sub regulares para el rendimiento, y llame a regex.sub dentro de una lista de comprensión. Convierta de antemano el df[''text''] a una lista. Si puede ahorrar algo de memoria, obtendrá un pequeño aumento de rendimiento.

import re p = re.compile(r''[^/w/s]+'') df[''text''] = [p.sub('''', x) for x in df[''text''].tolist()]

df text 0 ab 1 hgh12 2 abc123 3 1234

Nota: Si sus datos tienen valores de NaN, esto (así como el siguiente método a continuación) no funcionarán como están. Consulte la sección sobre " Otras consideraciones ".

str.translate

La función str.translate de python se implementa en C, y por lo tanto es muy rápida .

Cómo funciona esto es:

  1. Primero, une todas tus cadenas para formar una cadena enorme utilizando un separador de caracteres único (o más) que elijas. Debe usar un carácter / subcadena que pueda garantizar que no pertenezca a sus datos.
  2. Ejecute str.translate en la cadena grande, eliminando la puntuación (se excluye el separador del paso 1).
  3. Divida la cadena en el separador que se usó para unirse en el paso 1. La lista resultante debe tener la misma longitud que su columna inicial.

Aquí, en este ejemplo, consideramos el separador de tubería | . Si sus datos contienen la tubería, debe elegir otro separador.

import string punct = ''!"#$%&/'()*+,-./:;<=>?@[//]^_`{}~'' # `|` is not present here transtab = str.maketrans(dict.fromkeys(punct, '''')) df[''text''] = ''|''.join(df[''text''].tolist()).translate(transtab).split(''|'')

df text 0 ab 1 hgh12 2 abc123 3 1234

Actuación

str.translate realiza el mejor, con diferencia. Tenga en cuenta que la gráfica a continuación incluye otra variante Series.str.translate de la respuesta de MaxU .

(Curiosamente, lo repito por segunda vez, y los resultados son ligeramente diferentes a los de antes. Durante la segunda ejecución, parece que re.sub estaba ganando en str.translate para cantidades de datos realmente pequeñas).

Existe un riesgo inherente relacionado con el uso de la translate (en particular, el problema de automatizar el proceso de decidir qué separador usar no es trivial), pero las compensaciones valen el riesgo.

Otras Consideraciones

Manejo de NaNs con métodos de comprensión de lista; Tenga en cuenta que este método (y el siguiente) solo funcionará mientras sus datos no tengan NaN. Cuando maneje los NaN, deberá determinar los índices de valores no nulos y reemplazarlos solo. Intenta algo como esto:

df = pd.DataFrame({''text'': [ ''a..b?!??'', np.nan, ''%hgh&12'',''abc123!!!'', ''$$$1234'', np.nan]}) idx = np.flatnonzero(df[''text''].notna()) col_idx = df.columns.get_loc(''text'') df.iloc[idx,col_idx] = [ p.sub('''', x) for x in df.iloc[idx,col_idx].tolist()] df text 0 ab 1 NaN 2 hgh12 3 abc123 4 1234 5 NaN

Tratar con DataFrames; Si está tratando con DataFrames, donde cada columna requiere reemplazo, el procedimiento es simple:

v = pd.Series(df.values.ravel()) df[:] = translate(v).values.reshape(df.shape)

O,

v = df.stack() v[:] = translate(v) df = v.unstack()

Tenga en cuenta que la función de translate se define a continuación con el código de evaluación comparativa.

Cada solución tiene ventajas y desventajas, por lo que decidir qué solución se adapta mejor a sus necesidades dependerá de lo que esté dispuesto a sacrificar. Dos consideraciones muy comunes son el rendimiento (que ya hemos visto) y el uso de la memoria. str.translate es una solución que str.translate memoria, por lo tanto, utilice con precaución.

Otra consideración es la complejidad de su expresión regular. A veces, es posible que desee eliminar todo lo que no sea alfanumérico o espacio en blanco. En otras ocasiones, deberá conservar ciertos caracteres, como guiones, dos puntos y terminadores de oraciones [.!?] . Especificar esto agrega explícitamente complejidad a su expresión regular, lo que a su vez puede afectar el rendimiento de estas soluciones. Asegúrese de probar estas soluciones en sus datos antes de decidir qué usar.

Por último, los caracteres Unicode se eliminarán con esta solución. Es posible que desee ajustar su expresión regular (si utiliza una solución basada en str.translate ), o simplemente ir con str.translate contrario.

Para obtener aún más rendimiento (para N más grande), eche un vistazo a esta respuesta de Paul Panzer .

Apéndice

Funciones

def pd_replace(df): return df.assign(text=df[''text''].str.replace(r''[^/w/s]+'', '''')) def re_sub(df): p = re.compile(r''[^/w/s]+'') return df.assign(text=[p.sub('''', x) for x in df[''text''].tolist()]) def translate(df): punct = string.punctuation.replace(''|'', '''') transtab = str.maketrans(dict.fromkeys(punct, '''')) return df.assign( text=''|''.join(df[''text''].tolist()).translate(transtab).split(''|'') ) # MaxU''s version (https://.com/a/50444659/4909087) def pd_translate(df): punct = string.punctuation.replace(''|'', '''') transtab = str.maketrans(dict.fromkeys(punct, '''')) return df.assign(text=df[''text''].str.translate(transtab))

Código de rendimiento de referencia

from timeit import timeit import pandas as pd import matplotlib.pyplot as plt res = pd.DataFrame( index=[''pd_replace'', ''re_sub'', ''translate'', ''pd_translate''], columns=[10, 50, 100, 500, 1000, 5000, 10000, 50000], dtype=float ) for f in res.index: for c in res.columns: l = [''a..b?!??'', ''%hgh&12'',''abc123!!!'', ''$$$1234''] * c df = pd.DataFrame({''text'' : l}) stmt = ''{}(df)''.format(f) setp = ''from __main__ import df, {}''.format(f) res.at[f, c] = timeit(stmt, setp, number=30) ax = res.div(res.min()).T.plot(loglog=True) ax.set_xlabel("N"); ax.set_ylabel("time (relative)"); plt.show()


Con el uso de NUMPY podemos obtener una aceleración saludable con los mejores métodos publicados hasta ahora. La estrategia básica es similar: hacer una cuerda super grande. Pero el procesamiento parece mucho más rápido en números, probablemente porque explotamos completamente la simplicidad de la operación de reemplazo de nada por algo.

Para problemas más pequeños (menos de 0x110000 total) encontramos automáticamente un separador, para problemas más grandes utilizamos un método más lento que no se basa en str.split .

Tenga en cuenta que he movido todos los precomputables de las funciones. También tenga en cuenta que translate y pd_translate conocen el único separador posible para los tres problemas más grandes de forma gratuita, mientras que np_multi_strat tiene que calcularlo o recurrir a la estrategia sin separador. Y, finalmente, tenga en cuenta que para los últimos tres puntos de datos, cambio a un problema más "interesante"; pd_replace y re_sub porque no son equivalentes a los otros métodos que se tuvieron que excluir para eso.

En el algoritmo:

La estrategia básica es bastante simple. Sólo hay 0x110000 caracteres Unicode diferentes. Como OP encuadra el desafío en términos de grandes conjuntos de datos, vale la pena hacer una tabla de búsqueda que tenga True en los identificadores de caracteres que queremos mantener y False en los que deben ir: la puntuación en nuestro ejemplo.

Dicha tabla de búsqueda puede usarse para la búsqueda masiva utilizando la indexación avanzada de numpy. Como la búsqueda está completamente vectorizada y esencialmente equivale a eliminar la referencia a una serie de punteros, es mucho más rápida que, por ejemplo, la búsqueda en el diccionario. Aquí utilizamos la conversión de vistas numpy que permite reinterpretar los caracteres Unicode como enteros esencialmente de forma gratuita.

El uso de la matriz de datos que contiene solo una cadena de monstruos reinterpretada como una secuencia de números para indexar en la tabla de búsqueda da como resultado una máscara booleana. Esta máscara se puede usar para filtrar los caracteres no deseados. Usando la indexación booleana, esto también es una sola línea de código.

Hasta aquí tan simple. Lo difícil es cortar la cadena del monstruo de nuevo en sus partes. Si tenemos un separador, es decir, un carácter que no aparece en los datos o en la lista de puntuación, entonces es fácil. Usa este personaje para unirte y volver a dividir. Sin embargo, encontrar un separador automáticamente es un desafío y, de hecho, representa la mitad del lugar en la implementación a continuación.

Alternativamente, podemos mantener los puntos de división en una estructura de datos separada, rastrear cómo se mueven como consecuencia de la eliminación de caracteres no deseados y luego usarlos para cortar la cadena de monstruo procesada. Dado que cortar piezas en una longitud desigual no es el palo más fuerte de Numpy, este método es más lento que el str.split y solo se usa como respaldo cuando un separador sería demasiado caro para calcular si existió en primer lugar.

Código (tiempo / trazado basado en la publicación de @ COLDSPEED):

import numpy as np import pandas as pd import string import re spct = np.array([string.punctuation]).view(np.int32) lookup = np.zeros((0x110000,), dtype=bool) lookup[spct] = True invlookup = ~lookup OSEP = spct[0] SEP = chr(OSEP) while SEP in string.punctuation: OSEP = np.random.randint(0, 0x110000) SEP = chr(OSEP) def find_sep_2(letters): letters = np.array([letters]).view(np.int32) msk = invlookup.copy() msk[letters] = False sep = msk.argmax() if not msk[sep]: return None return sep def find_sep(letters, sep=0x88000): letters = np.array([letters]).view(np.int32) cmp = np.sign(sep-letters) cmpf = np.sign(sep-spct) if cmp.sum() + cmpf.sum() >= 1: left, right, gs = sep+1, 0x110000, -1 else: left, right, gs = 0, sep, 1 idx, = np.where(cmp == gs) idxf, = np.where(cmpf == gs) sep = (left + right) // 2 while True: cmp = np.sign(sep-letters[idx]) cmpf = np.sign(sep-spct[idxf]) if cmp.all() and cmpf.all(): return sep if cmp.sum() + cmpf.sum() >= (left & 1 == right & 1): left, sep, gs = sep+1, (right + sep) // 2, -1 else: right, sep, gs = sep, (left + sep) // 2, 1 idx = idx[cmp == gs] idxf = idxf[cmpf == gs] def np_multi_strat(df): L = df[''text''].tolist() all_ = ''''.join(L) sep = 0x088000 if chr(sep) in all_: # very unlikely ... if len(all_) >= 0x110000: # fall back to separator-less method # (finding separator too expensive) LL = np.array((0, *map(len, L))) LLL = LL.cumsum() all_ = np.array([all_]).view(np.int32) pnct = invlookup[all_] NL = np.add.reduceat(pnct, LLL[:-1]) NLL = np.concatenate([[0], NL.cumsum()]).tolist() all_ = all_[pnct] all_ = all_.view(f''U{all_.size}'').item(0) return df.assign(text=[all_[NLL[i]:NLL[i+1]] for i in range(len(NLL)-1)]) elif len(all_) >= 0x22000: # use mask sep = find_sep_2(all_) else: # use bisection sep = find_sep(all_) all_ = np.array([chr(sep).join(L)]).view(np.int32) pnct = invlookup[all_] all_ = all_[pnct] all_ = all_.view(f''U{all_.size}'').item(0) return df.assign(text=all_.split(chr(sep))) def pd_replace(df): return df.assign(text=df[''text''].str.replace(r''[^/w/s]+'', '''')) p = re.compile(r''[^/w/s]+'') def re_sub(df): return df.assign(text=[p.sub('''', x) for x in df[''text''].tolist()]) punct = string.punctuation.replace(SEP, '''') transtab = str.maketrans(dict.fromkeys(punct, '''')) def translate(df): return df.assign( text=SEP.join(df[''text''].tolist()).translate(transtab).split(SEP) ) # MaxU''s version (https://.com/a/50444659/4909087) def pd_translate(df): return df.assign(text=df[''text''].str.translate(transtab)) from timeit import timeit import pandas as pd import matplotlib.pyplot as plt res = pd.DataFrame( index=[''translate'', ''pd_replace'', ''re_sub'', ''pd_translate'', ''np_multi_strat''], columns=[10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000, 1000000], dtype=float ) for c in res.columns: if c >= 100000: # stress test the separator finder all_ = np.r_[:OSEP, OSEP+1:0x110000].repeat(c//10000) np.random.shuffle(all_) split = np.arange(c-1) + / np.sort(np.random.randint(0, len(all_) - c + 2, (c-1,))) l = [x.view(f''U{x.size}'').item(0) for x in np.split(all_, split)] else: l = [''a..b?!??'', ''%hgh&12'',''abc123!!!'', ''$$$1234''] * c df = pd.DataFrame({''text'' : l}) for f in res.index: if f == res.index[0]: ref = globals()[f](df).text elif not (ref == globals()[f](df).text).all(): res.at[f, c] = np.nan print(f, ''disagrees at'', c) continue stmt = ''{}(df)''.format(f) setp = ''from __main__ import df, {}''.format(f) res.at[f, c] = timeit(stmt, setp, number=16) ax = res.div(res.min()).T.plot(loglog=True) ax.set_xlabel("N"); ax.set_ylabel("time (relative)"); plt.show()


Es bastante interesante que el método vectorizado Series.str.translate es todavía un poco más lento en comparación con Vanilla Python str.translate() :

def pd_translate(df): return df.assign(text=df[''text''].str.translate(transtab))