设置
出于演示的目的,让我们考虑这个 DataFrame。
df = pd.DataFrame({'text':['a..b?!??', '%hgh&12','abc123!!!', '$$$1234']})
df
text
0 a..b?!??
1 %hgh&12
2 abc123!!!
3 $$$1234
下面,我按性能升序一一列出替代方案
str.replace
包含此选项是为了建立默认方法作为比较其他性能更高的解决方案的基准。
这使用了 pandas 内置的 str.replace 函数,该函数执行基于正则表达式的替换。
df['text'] = df['text'].str.replace(r'[^\w\s]+', '')
df
text
0 ab
1 hgh12
2 abc123
3 1234
这很容易编码,可读性很好,但速度很慢。
regex.sub
这涉及使用re 库中的sub 函数。为性能预编译正则表达式模式,并在列表理解中调用regex.sub。如果您可以腾出一些内存,请事先将df['text'] 转换为列表,这样您将获得不错的性能提升。
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
注意:如果您的数据具有 NaN 值,则此方法(以及下面的下一个方法)将无法按原样工作。请参阅“其他注意事项”部分。
str.translate
python 的str.translate 函数是用C 实现的,因此非常快。
这是如何工作的:
- 首先,使用您选择的单个(或多个)字符分隔符将所有字符串连接在一起形成一个巨大字符串。您必须使用您可以保证不属于您的数据的字符/子字符串。
- 对大字符串执行
str.translate,删除标点符号(第 1 步中的分隔符除外)。
- 在第 1 步中用于连接的分隔符上拆分字符串。生成的列表必须与您的初始列具有相同的长度。
在此示例中,我们考虑管道分隔符|。如果您的数据包含管道,那么您必须选择另一个分隔符。
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
性能
str.translate 迄今为止表现最好。请注意,下图包含来自MaxU's answer 的另一个变体Series.str.translate。
(有趣的是,我重新运行了第二次,结果与以前略有不同。在第二次运行期间,re.sub 似乎在非常少量的数据上胜过str.translate。)
使用translate 存在固有风险(特别是自动化决定使用哪个分隔符的过程并非微不足道),但权衡取舍是值得的风险。
其他注意事项
使用列表解析方法处理 NaN; 请注意,此方法(以及下一种方法)仅在您的数据没有 NaN 时才有效。处理 NaN 时,您必须确定非空值的索引并仅替换它们。试试这样的:
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
处理DataFrames;如果您处理的是DataFrames,其中每一列都需要替换,过程很简单:
v = pd.Series(df.values.ravel())
df[:] = translate(v).values.reshape(df.shape)
或者,
v = df.stack()
v[:] = translate(v)
df = v.unstack()
请注意,translate 函数在下面的基准测试代码中定义。
每个解决方案都有权衡取舍,因此决定哪种解决方案最适合您的需求将取决于您愿意牺牲什么。两个非常常见的考虑因素是性能(我们已经看到)和内存使用情况。 str.translate 是一种占用大量内存的解决方案,因此请谨慎使用。
另一个考虑因素是您的正则表达式的复杂性。有时,您可能想要删除任何不是字母数字或空格的内容。其他时候,您需要保留某些字符,例如连字符、冒号和句子终止符[.!?]。明确指定这些会增加正则表达式的复杂性,进而可能会影响这些解决方案的性能。确保您测试这些解决方案
在决定使用什么之前先检查您的数据。
最后,此解决方案将删除 unicode 字符。您可能想要调整您的正则表达式(如果使用基于正则表达式的解决方案),或者直接使用 str.translate 否则。
对于甚至 更多 性能(对于更大的 N),请查看Paul Panzer 的这个答案。
附录
函数
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))
性能基准代码
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()