Procesamiento de texto basado en NLTK con pandas

La puntuación y los valores en minúscula y minúscula no funcionan mientras se usa nltk.

Mi código

stopwords=nltk.corpus.stopwords.words('english')+ list(string.punctuation) user_defined_stop_words=['st','rd','hong','kong'] new_stop_words=stopwords+user_defined_stop_words def preprocess(text): return [word for word in word_tokenize(text) if word.lower() not in new_stop_words and not word.isdigit()] miss_data['Clean_addr'] = miss_data['Adj_Addr'].apply(preprocess) 

Entrada de muestra

 23FLOOR 9 DES VOEUX RD WEST HONG KONG PAG CONSULTING FLAT 15 AIA CENTRAL 1 CONNAUGHT RD CENTRAL C/O CITY LOST STUDIOS AND FLAT 4F 13-15 HILLIER ST SHEUNG HONG KONG 

Rendimiento esperado

  floor des voeux west pag consulting flat aia central connaught central co city lost studios flat f hillier sheung 

Su función es lenta y está incompleta. Primero, con los problemas –

  1. Usted no está en minúsculas sus datos.
  2. No te estás deshaciendo de los dígitos y la puntuación correctamente.
  3. No estás devolviendo una cadena (deberías unirte a la lista usando str.join y devolverla)
  4. Además, una lista de comprensión con procesamiento de texto es una forma principal de introducir problemas de legibilidad, sin mencionar las posibles redundancias (puede llamar a una función varias veces, para cada condición if aparece).

A continuación, hay un par de ineficiencias evidentes con su función, especialmente con el código de eliminación de palabras de parada.

  1. Su estructura de stopwords es una lista , y in verificaciones las listas son lentas . Lo primero que se debe hacer es convertir eso en un set , haciendo que el tiempo de not in verificación sea constante.

  2. Estás usando nltk.word_tokenize que es innecesariamente lento.

  3. Por último, no siempre debe confiar en la apply , incluso si está trabajando con NLTK, donde rara vez hay una solución vectorizada disponible. Casi siempre hay otras formas de hacer exactamente lo mismo. A menudo, incluso un bucle de python es más rápido. Pero esto no está escrito en piedra.

Primero, crea tus stopwords mejoradas como un conjunto

 user_defined_stop_words = ['st','rd','hong','kong'] i = nltk.corpus.stopwords.words('english') j = list(string.punctuation) + user_defined_stop_words stopwords = set(i).union(j) 

La siguiente solución es deshacerse de la comprensión de la lista y convertirla en una función multilínea. Esto hace que las cosas sean mucho más fáciles de trabajar. Cada línea de su función debe estar dedicada a resolver una tarea en particular (por ejemplo, deshacerse de los dígitos / puntuación, o deshacerse de las palabras clave, o minúsculas) –

 def preprocess(x): x = re.sub('[^az\s]', '', x.lower()) # get rid of noise x = [w for w in x.split() if w not in set(stopwords)] # remove stopwords return ' '.join(x) # join the list 

Como ejemplo. Esto se aplicaría entonces a su columna –

 df['Clean_addr'] = df['Adj_Addr'].apply(preprocess) 

Como alternativa, aquí hay un enfoque que no se basa en apply . Esto debería funcionar bien para oraciones pequeñas.

Cargue sus datos en una serie –

 v = miss_data['Adj_Addr'] v 0 23FLOOR 9 DES VOEUX RD WEST HONG KONG 1 PAG CONSULTING FLAT 15 AIA CENTRAL 1 CONNAUGHT... 2 C/O CITY LOST STUDIOS AND FLAT 4F 13-15 HILLIE... Name: Adj_Addr, dtype: object 

Ahora viene el trabajo pesado.

  1. Minúsculas con str.lower
  2. Eliminar el ruido usando str.replace
  3. str.split palabras en celdas separadas usando str.split
  4. Aplique la eliminación de pd.DataFrame.isin pd.DataFrame.where utilizando pd.DataFrame.isin + pd.DataFrame.where
  5. Finalmente, únete a la ttwig de datos usando agg .
 v = v.str.lower().str.replace('[^az\s]', '').str.split(expand=True) v.where(~v.isin(stopwords) & v.notnull(), '')\ .agg(' '.join, axis=1)\ .str.replace('\s+', ' ')\ .str.strip() 0 floor des voeux west 1 pag consulting flat aia central connaught central 2 co city lost studios flat f hillier sheung dtype: object 

Para usar esto en varias columnas, coloque este código en una función preprocess2 y llame a apply

 def preprocess2(v): v = v.str.lower().str.replace('[^az\s]', '').str.split(expand=True) return v.where(~v.isin(stopwords) & v.notnull(), '')\ .agg(' '.join, axis=1)\ .str.replace('\s+', ' ')\ .str.strip() 

 c = ['Col1', 'Col2', ...] # columns to operate df[c] = df[c].apply(preprocess2, axis=0) 

Aún necesitará una llamada de apply , pero con un pequeño número de columnas, no debería escalar demasiado. Si no te gusta apply , entonces aquí hay una variante descabellada para ti:

 for _c in c: df[_c] = preprocess2(df[_c]) 

Veamos la diferencia entre nuestra versión no loopy y la original –

 s = pd.concat([s] * 100000, ignore_index=True) s.size 300000 

Primero, un chequeo de cordura.

 preprocess2(s).eq(s.apply(preprocess)).all() True 

Ahora vienen los tiempos.

 %timeit preprocess2(s) 1 loop, best of 3: 13.8 s per loop 

 %timeit s.apply(preprocess) 1 loop, best of 3: 9.72 s per loop 

Esto es sorprendente, ya que la apply rara vez es más rápida que una solución no loca. Pero esto tiene sentido en este caso porque hemos optimizado el preprocess bastante, y las operaciones de cadena en los pandas rara vez se vectorizan (por lo general lo son, pero la ganancia de rendimiento no es tanto como se esperaría).

Veamos si podemos hacerlo mejor, omitiendo la apply , usando np.vectorize

 preprocess3 = np.vectorize(preprocess) %timeit preprocess3(s) 1 loop, best of 3: 9.65 s per loop 

Lo que es idéntico de apply pero resulta ser un poco más rápido debido a la reducción de la sobrecarga alrededor del bucle “oculto”.