1. ホーム
  2. python

パンダを使った高速な句読点除去

2023-08-26 20:16:37

質問

これは自己回答の投稿です。以下では、NLPドメインにおける一般的な問題の概要を説明し、それを解決するためのいくつかの実行可能なメソッドを提案します。

しばしば、以下のような問題を解決する必要が生じます。 句読点 を削除する必要が生じることがあります。句読点とは string.punctuation :

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

これは十分に一般的な問題で、以前から何度も質問されています。最も一般的な解決策は、パンダを使うことです。 str.replace . しかし ロット のテキストを含む状況では、よりパフォーマンスの高い解決策を検討する必要があるかもしれません。

の代替となる、性能の良いものは何でしょうか? str.replace に代わるパフォーマンス的な方法は何でしょうか?

どのように解決するのですか?

設定方法

デモのために、この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

これには 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()]

df
     text
0      ab
1   hgh12
2  abc123
3    1234

注意 データにNaN値がある場合、この方法(以下の次の方法と同様)はそのままでは動作しません。詳細は「"」の項を参照してください。 その他の検討事項 "を参照してください。


str.translate

pythonの str.translate 関数は C で実装されているため 非常に高速です。 .

この仕組みは

  1. まず、すべての文字列を結合して、1つの 巨大な 文字列を結合します。 セパレータ その あなた を選択します。あなたは は、データ内に属さないことを保証できる文字/サブストリングを使用する必要があります。
  2. 実行する str.translate を実行し、句読点を削除します (ステップ 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('|')

df
     text
0      ab
1   hgh12
2  abc123
3    1234


パフォーマンス

str.translate は圧倒的に良いパフォーマンスを示します。下のグラフには、別のバリアント Series.str.translate から MaxUの回答 .

(興味深いことに、これを2回目に再実行したところ、結果が以前と若干異なっています。2回目の実行の間、どうやら re.sub に勝っていたようです。 str.translate に勝っていました)。

を使用することには、固有のリスクがあります。 translate (の問題)。 を自動化する の自動化の問題は自明ではありません)、しかし、トレードオフはリスクに見合うものです。


その他の考慮事項

リスト内包メソッドでNaNを処理する。 このメソッド(と次のメソッド)は、データにNaNがない限り動作することに注意してください。NaNを扱う場合、NULLでない値のインデックスを決定し、その値のみを置き換える必要があります。以下のような方法を試してみてください。

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

DataFrameを扱う。 DataFrameを扱う場合、どこで それぞれ カラムの交換が必要ですが、手順は簡単です。

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

または

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

なお translate 関数はベンチマークコードと一緒に以下で定義されていることに注意してください。

すべてのソリューションにはトレードオフがあり、どのソリューションが自分のニーズに最も合うかを決定することは、何を犠牲にしても構わないかどうかによります。2 つの非常に一般的な考慮事項は、パフォーマンス (これはすでに見てきました) とメモリ使用量です。 str.translate はメモリを大量に消費するソリューションなので、注意して使用してください。

もう一つの考慮点は、正規表現の複雑さです。英数字や空白でないものをすべて削除したい場合もあります。また、ハイフン、コロン、文の終端記号など、特定の文字を残す必要がある場合もあります。 [.!?] . これらを明示的に指定すると、正規表現が複雑になり、その結果、これらのソリューションのパフォーマンスに影響を与える可能性があります。これらの解決策は、必ず自分のデータでテストして をテストしてから、使用するものを決定してください。

最後に、このソリューションではユニコード文字が削除されます。正規表現 (正規表現ベースのソリューションを使用している場合) を微調整するか、単に str.translate を使用することもできます。

についても より パフォーマンス (より大きな N の場合) は ポール・パンザー .


付録

機能紹介

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()