Configuration
Pour les besoins de la démonstration, considérons ce DataFrame.
df = pd.DataFrame({'text':['a..b?!??', '%hgh&12','abc123!!!', '$$$1234']})
df
text
0 a..b?!??
1 %hgh&12
2 abc123!!!
3 $$$1234
Ci-dessous, j'énumère les alternatives, une par une, par ordre croissant de performance
str.replace
Cette option est incluse pour établir la méthode par défaut comme référence pour comparer d'autres solutions plus performantes.
Cela utilise la fonction intégrée de pandas str.replace
qui effectue un remplacement basé sur une expression rationnelle.
df['text'] = df['text'].str.replace(r'[^\w\s]+', '')
df
text
0 ab
1 hgh12
2 abc123
3 1234
C'est très facile à coder, et c'est assez lisible, mais lent.
regex.sub
Cela implique l'utilisation du sub
de la fonction re
bibliothèque. Pré-compilez un motif regex pour plus de performance, et appelez regex.sub
à l'intérieur d'une liste de compréhension. Convertir df['text']
à une liste au préalable si vous pouvez disposer d'un peu de mémoire, vous obtiendrez un petit gain de performance appréciable.
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
Note : Si vos données contiennent des valeurs NaN, cette méthode (ainsi que la suivante) ne fonctionnera pas telle quelle. Voir la section sur " Autres considérations ".
str.translate
de python str.translate
est implémentée en C, et est donc très rapide .
Voici comment cela fonctionne :
- D'abord, joignez toutes vos cordes ensemble pour former un énorme chaîne de caractères en utilisant un seul (ou plusieurs) caractère séparateur que vous choisir. Vous doit utilisez un caractère/sous-chaîne dont vous pouvez garantir qu'il n'appartient pas à vos données.
- Exécuter
str.translate
sur la grande chaîne, en supprimant la ponctuation (le séparateur de l'étape 1 exclu).
- Divisez la chaîne sur le séparateur qui a été utilisé pour joindre à l'étape 1. La liste résultante doit ont la même longueur que votre colonne initiale.
Ici, dans cet exemple, nous considérons le séparateur de tuyaux |
. Si vos données contiennent le tuyau, vous devez alors choisir un autre séparateur.
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
Performance
str.translate
est le plus performant, et de loin. Notez que le graphique ci-dessous inclut une autre variante Series.str.translate
de La réponse de MaxU .
(Il est intéressant de noter que j'ai refait l'opération une deuxième fois et que les résultats sont légèrement différents de ceux obtenus précédemment. Lors de la deuxième exécution, il semble re.sub
l'emportait sur str.translate
pour les très petites quantités de données).
Il y a un risque inhérent à l'utilisation translate
(en particulier, le problème de automatisation de le processus de décision concernant le séparateur à utiliser n'est pas trivial), mais le jeu en vaut la chandelle.
Autres considérations
Gestion des NaNs avec les méthodes de compréhension des listes ; Notez que cette méthode (et la suivante) ne fonctionnera que si vos données ne contiennent pas de NaN. Lorsque vous traitez les NaN, vous devez déterminer les indices des valeurs non nulles et ne remplacer que celles-ci. Essayez quelque chose comme ceci :
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
Traiter les DataFrames ; Si vous avez affaire à des DataFrames, dans lesquelles chaque doit être remplacée, la procédure est simple :
v = pd.Series(df.values.ravel())
df[:] = translate(v).values.reshape(df.shape)
Ou,
v = df.stack()
v[:] = translate(v)
df = v.unstack()
Notez que le translate
est définie ci-dessous avec le code de référence.
Chaque solution présente des inconvénients, de sorte que le choix de la solution la mieux adaptée à vos besoins dépendra de ce que vous êtes prêt à sacrifier. Deux considérations très courantes sont les performances (que nous avons déjà vues) et l'utilisation de la mémoire. str.translate
est une solution gourmande en mémoire, à utiliser avec précaution.
Une autre considération est la complexité de votre regex. Parfois, vous voudrez supprimer tout ce qui n'est pas alphanumérique ou espace. Dans d'autres cas, vous devrez conserver certains caractères, tels que les traits d'union, les deux-points et les terminaisons de phrase. [.!?]
. Le fait de les spécifier explicitement ajoute de la complexité à votre regex, ce qui peut à son tour avoir un impact sur les performances de ces solutions. Assurez-vous de tester ces solutions sur vos données avant de décider de leur utilisation.
Enfin, les caractères unicode seront supprimés avec cette solution. Vous pouvez modifier votre regex (si vous utilisez une solution basée sur le regex), ou simplement utiliser la solution suivante str.translate
autrement.
Pour même plus (pour les grands N), jetez un coup d'œil à cette réponse par Paul Panzer .
Annexe
Fonctions
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://stackoverflow.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))
Code d'évaluation comparative des performances
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()