代码之家  ›  专栏  ›  技术社区  ›  cs95 abhishek58g

熊猫快速删除标点符号

  •  48
  • cs95 abhishek58g  · 技术社区  · 8 年前

    这是一个自我回复的帖子。下面,我概述了NLP领域中的一个常见问题,并提出了一些有效的解决方法。

    通常需要移除 标点符号 在文本清洗和预处理期间。标点符号定义为 string.punctuation :

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

    这是一个很常见的问题,在恶心之前就被问到了。最惯用的解决方案是熊猫 str.replace . 但是,对于涉及 许多 对于文本,可能需要考虑更高性能的解决方案。

    除了 str.替换 在处理数十万条记录时?

    3 回复  |  直到 7 年前
        1
  •  50
  •   cs95 abhishek58g    7 年前

    安装程序

    为了演示的目的,让我们考虑一下这个数据框架。

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

    下面,我按性能的增加顺序逐一列出了备选方案。

    str.replace

    此选项用于建立默认方法作为比较其他性能更高的解决方案的基准。

    这是用熊猫建造的 str.替换 执行基于regex的替换的函数。

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

    df
         text
    0      ab
    1   hgh12
    2  abc123
    3    1234
    

    这很容易编码,可读性很好,但速度很慢。


    regex.sub

    这涉及到使用 sub 函数来自 re 图书馆。预编译regex模式以提高性能,并调用 正则表达式Sub 在列表理解中。转换 df['text'] 如果你能预留一些内存的话,你会得到一个很好的性能提升。

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

    东风
    文本
    0 AB
    1小时12分
    2台ABC123
    3 1234号
    

    注: 如果数据具有NaN值,则此方法(以及下面的下一个方法)将无法正常工作。参见“的章节 其他注意事项 “。


    str.translate

    巨蟒的 str.翻译 函数在C中实现,因此 非常快 .

    工作原理是:

    1. 首先,将所有的字符串连接在一起形成一个 巨大的 使用单个(或多个)字符的字符串 分离器 那个 选择。你 必须 使用可以保证不属于数据的字符/子字符串。
    2. 执行 str.翻译 在大字符串上,删除标点符号(步骤1中的分隔符除外)。
    3. 拆分步骤1中用于联接的分隔符上的字符串。结果列表 必须 与初始列的长度相同。

    这里,在这个例子中,我们考虑管道分隔符 | . 如果数据包含管道,则必须选择其他分隔符。

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

    东风
    文本
    0 AB
    1小时12分
    2台ABC123
    3 1234号
    

    性能

    str.翻译 迄今为止表现最好。请注意,下面的图表包含另一个变量 Series.str.translate MaxU's answer .

    (有趣的是,我第二次重新运行,结果与以前略有不同。在第二次跑步中,似乎 re.sub 赢得了胜利 str.翻译 对于非常少量的数据。) enter image description here

    使用时存在固有风险 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
    

    处理数据帧; 如果你在处理数据帧, 每一个 列需要替换,过程很简单:

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

    或者,

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

    注意, 翻译 功能在下面的标准代码中定义。

    每一个解决方案都有权衡,所以决定什么解决方案最适合您的需求取决于您愿意牺牲什么。两个非常常见的考虑因素是性能(我们已经看到)和内存使用。 str.翻译 是一个内存不足的解决方案,因此请谨慎使用。

    另一个考虑因素是regex的复杂性。有时,您可能希望删除任何不是字母数字或空格的内容。其他情况下,您需要保留某些字符,如连字符、冒号和句子结束符。 [.!?] . 明确地指定这些参数会增加regex的复杂性,这反过来可能会影响这些解决方案的性能。确保测试这些解决方案 在决定使用什么之前,请先查看您的数据。

    最后,Unicode字符将随此解决方案一起删除。您可能需要调整regex(如果使用基于regex的解决方案),或者只需 str.翻译 否则。

    为偶数 更多 性能(对于较大的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()
    
        2
  •  28
  •   Paul Panzer    8 年前

    使用numpy,我们可以获得一个健康的提速超过目前发布的最佳方法。基本的策略是相似的---做一个大的超弦。但在numpy中,处理似乎要快得多,大概是因为我们充分利用了无需替换操作的简单性。

    对于较小的(小于 0x110000 字符总数)我们自动找到一个分隔符的问题,对于较大的问题,我们使用不依赖于 str.split .

    请注意,我已经将所有预计算项移出了函数。还要注意的是, translate pd_translate 免费了解三大问题唯一可能的分隔符,而 np_multi_strat 必须计算它或返回到无分隔符策略。最后,请注意,对于最后三个数据点,我切换到一个更“有趣”的问题; pd_replace re_sub 因为它们不等同于其他方法,所以必须排除在外。

    enter image description here

    关于算法:

    基本策略实际上相当简单。只有 0x11万 不同的Unicode字符。由于OP以巨大的数据集来描述这一挑战,因此非常值得制作一个具有 True 在我们要保留的字符ID处 False 在那些必须去的地方---我们例子中的标点符号。

    这样的查找表可以用于使用numpy的高级索引进行批量查找。由于查找是完全向量化的,基本上相当于取消对指针数组的引用,因此它比字典查找要快得多。这里我们使用numpy视图转换,它允许将unicode字符重新解释为整数,基本上是免费的。

    使用只包含一个被重新解释为数字序列的怪兽字符串的数据数组索引到查找表中会得到一个布尔值掩码。然后可以使用此掩码过滤掉不需要的字符。使用布尔索引也是一行代码。

    到目前为止还很简单。棘手的一点是把怪物的线切碎成它的部分。如果我们有一个分隔符,即数据或标点符号列表中没有出现的一个字符,那么它仍然很容易。使用此字符连接和重新点亮。然而,自动找到一个分隔符是一个挑战,实际上在下面的实现中占了一半的loc。

    或者,我们可以将分割点保存在单独的数据结构中,跟踪它们在删除不需要的字符后的移动方式,然后使用它们分割处理后的怪物字符串。由于将不均匀长度的部分切碎并不是numpy的最强项,所以这种方法比 结构拆分 只有当一个分隔符太贵而无法计算它是否存在时才用作回退。

    代码(计时/绘图主要基于@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://stackoverflow.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()
    
        3
  •  18
  •   MaxU - stand with Ukraine    8 年前

    有趣到矢量化 Series.str.translate 与香草蟒蛇相比,该方法仍然稍慢一些。 str.translate() :

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

    enter image description here