LoginSignup
28
26

More than 3 years have passed since last update.

pandasでfor文を使わない日本語テキスト前処理

Last updated at Posted at 2020-01-27

pandasで、テキストに対して処理する時、「よくわからないからfor文使うか」とならないための備忘録。

日本語テキストの前処理を目的として、情報をまとめる。

もっと良い処理方法があれば教えていただけると幸いです。

実行環境
- macOS Catalina
- Python 3.7.4

pandas 0.25.3

TL;DR

  • 簡単な処理は df["カラム名"].str のメソッドに実装されている
  • pandasに実装されていない処理をしたい場合 df["カラム名"].apply()

サンプルデータ

HPからスクレイピングしてきたレディースのファッションブランドの店舗情報。
csvには企業名・ブランド名・店名・住所が保存されている。

複数のHPからスクレイピングしているため、半角全角だったり、空白など統一されていない。郵便番号が含まれていたりいなかったりもする。

下記の表はデータの一例。
このデータを例にして実行結果を併記する。

company brand location address
pal BONbazaar ボンバザール 東京ドームシティラクーア店 東京都文京区春日1丁目1-1 ラクーアビル 2F
world index エミオスタイル 東京都新宿区高田馬場1-35-3 エミオスタイル 1F
pal Whim Gazette 丸の内店 〒100-6390 東京都千代田区丸の内2-4-1 丸の内ビルディングB1F
stripe SEVENDAYS=SUNDAY イオンモール浦和美園 2F 埼玉県さいたま市緑区美園5丁目50番地1 イオンモール浦和美園
pal mystic 船橋店 〒273-0012千葉県船橋市浜町2-1-1 ららぽーとTOKYO-BAYららぽーと3
pal pual ce cin 大船ルミネウイング店 〒247-0056 神奈川県鎌倉市大船1-4-1 ルミネウイング4F
stripe Green Parks sara シャポー小岩店 東京都江戸川区南小岩7-24-15シャポー小岩 1F
pal Discoat Discoat Petit 池袋ショッピングパーク 〒171-8532東京都豊島区南池袋1-29-1池袋SP B1F
adastoria niko and... アトレ川越 埼玉県川越市脇田町 105 アトレ川越4F
pal CIAOPANIC TYPY 亀有店 〒125-0061東京都葛飾区亀有3-49-3アリオ亀有2F

ほぼ 1つのカラム (address カラム) しか説明に使わないため、紹介するのはほぼSeriesの処理だが、DataFrameでの処理も出来る限り併記する。

処理

基本的には、Series.str で、pythonの文字列メソッド正規表現操作を呼び出せるので、これらを使えば良い。

文字列メソッド系

strip

Series.str.strip

文字列の先頭・末尾の空白文字を削除する。

df['strip']=df['address'].str.strip()

もちろん、先頭だけ削除する Series.str.rstrip と末尾だけ削除するlstrip も実装されている。

split, rsplit

Series.str.split

  • 文字列を指定したセパレータで分割して リスト で返す
  • expand=True にすると、複数のカラムに分割できる
  • 一番splitされたカラムに数を合わせるので注意
    • 下の例の場合、3つのカラムにわかれているが、3つに分割できないテキストの場合は None が挿入されている
df['address'].str.split(expand=True)
0 1 2
東京都文京区春日1丁目1-1 ラクーアビル 2F
東京都新宿区高田馬場1-35-3 エミオスタイル 1F
〒100-6390 東京都千代田区丸の内2-4-1 丸の内ビルディングB1F
埼玉県さいたま市緑区美園5丁目50番地1 イオンモール浦和美園 None
〒273-0012千葉県船橋市浜町2-1-1 ららぽーとTOKYO-BAYららぽーと3 None
〒247-0056 神奈川県鎌倉市大船1-4-1 ルミネウイング4F
東京都江戸川区南小岩7-24-15シャポー小岩 1F None
〒171-8532東京都豊島区南池袋1-29-1池袋SP B1F None
埼玉県川越市脇田町 105 アトレ川越4F
〒125-0061東京都葛飾区亀有3-49-3アリオ亀有2F None None
  • rsplit
    • 右側から分割する
    • n を指定することで、分割の数を調整できる
    • 例えば n=1 にすれば、1回しか分割しないため、カラム数は2つで固定になる
df['address'].str.rsplit(expand=True, n=1)
0 1
東京都文京区春日1丁目1-1 ラクーアビル 2F
東京都新宿区高田馬場1-35-3 エミオスタイル 1F
〒100-6390 東京都千代田区丸の内2-4-1 丸の内ビルディングB1F
埼玉県さいたま市緑区美園5丁目50番地1 イオンモール浦和美園
〒273-0012千葉県船橋市浜町2-1-1 ららぽーとTOKYO-BAYららぽーと3
〒247-0056 神奈川県鎌倉市大船1-4-1 ルミネウイング4F
東京都江戸川区南小岩7-24-15シャポー小岩 1F
〒171-8532東京都豊島区南池袋1-29-1池袋SP B1F
埼玉県川越市脇田町 105 アトレ川越4F
〒125-0061東京都葛飾区亀有3-49-3アリオ亀有2F None

find

Series.str.find

  • str.find と同じ機能
  • 含まれている場合は文字列の位置 、含まれていない場合は -1 を返す
df['address'].str.find('東京都')
>> 0    0
1    0
2    10
3    -1
4    -1
5    -1
6    0
7    9
8    -1
9    9
  • 返り値がbool値ではないため、以下のような使い方でデータの絞り込みはでき ない
    • df[df['address'].str.find('東京都')]
    • df.query('address.str.find("東京都")')
  • hoge in hogehoge のように、文字列が含まれているかどうかだけ判定するのであれば、 contains を使った方がよい
# 一応"東京都"が含まれていないカラムを指定することも可能
df.query('address.str.find("東京都")!=-1')

normalize

Series.str.normalize

主には全角数字・記号を半角に変換し、半角カタカナは全角に変換する。formには'NFKC' (正規形 KC) を指定する。

# string
import unicodedata
unicodedata.normalize('NFKC', '123!?@#ハンカクカタカナ')
>> '123!?@#ハンカクカタカナ '

# pandas
df['normalize'] = df['address'].str.normalize(form='NFKC')

参考: Pythonのunicodedata.normalize('NFKC', x)で正規化される文字一覧

日本語テキストの場合、neologdnを使った方が楽(後述)

正規表現系

findall

Series.str.findall

  • re.findall() と等価
  • str.find() とは異なり、こっちは正規表現が使える
  • 一致した単語全てを返す
df['address'].str.findall('(.{2}区)')
>> 0    [文京区]
1    [新宿区]
2    [代田区]
3    [市緑区]
4       []
5       []
6    [戸川区]
7    [豊島区]
8       []
9    [葛飾区]

contains

Series.str.contains

  • contain s なので注意
  • 機能的には re.match()
    • bool値が返ってくるため、データの絞り込みに利用できる
df['address'].str.contains('.{2}区')
>> 0     True
1     True
2     True
3     True
4    False
5    False
6     True
7     True
8    False
9     True

#  "○○区" を含むデータだけ表示
df.query('address.str.contains(".{2}区")')['address']
>> 0                     東京都文京区春日1丁目1-1 ラクーアビル 2F
1                  東京都新宿区高田馬場1-35-3 エミオスタイル 1F
2    100-6390 東京都千代田区丸の内2-4-1 丸の内ビルディングB1F
3              埼玉県さいたま市緑区美園5丁目50番地1 イオンモール浦和美園
6                   東京都江戸川区南小岩7-24-15シャポー小岩 1F
7             171-8532東京都豊島区南池袋1-29-1池袋SP B1F
9               125-0061東京都葛飾区亀有3-49-3アリオ亀有2F
文字列を含む列にフラグを立てる

bool値をint値に変換すれば、ある文字列を含むデータに 1 を立てたカラムを作成できる。
特徴量を作成する、特徴量エンジニアリング時に便利。

# 東京都を含むデータフラグ tokyo_flgを作成
df['tokyo_flg'] = df['address'].str.contains("東京都").astype(int)
df['tokyo_flg']
>> 0    1
1    1
2    1
3    0
4    0
5    0
6    1
7    1
8    0
9    1

extract

Series.str.extract

  • matchしたパターンを返す。
    • パターンがない場合 None を返すので、必要ない場合は dropna() する
  • 名前付きグループ名を設定すれば、その名前がそのままカラム名になる
    • 名前付きグループ名とは (?P<name>...) のこと
df['address'].str.extract('(東京都|神奈川県)([^区市]+[区市])').dropna()

df['address'].str.extract('(?P<pref>東京都|神奈川県)(?P<city>[^区市]+[区市])').dropna()
pref city
0 東京都 文京区
1 東京都 新宿区
2 東京都 千代田区
5 神奈川県 鎌倉市
6 東京都 江戸川区
7 東京都 豊島区
9 東京都 葛飾区

東京都○○区と神奈川県○○市のテーブルを作成できる。

replace

Series.str.replace

  • re.sub() と等価
    • Series.str.replace(pat, repl)pat にマッチする文字列を repl に変換する
  • ルールベースで余分な文字列を削除したいときによく使う
# 郵便番号を削除する
df['address'] = df['address'].str.replace("〒[0-9]{3}\-[0-9]{4}", "")

ちなみに、Series.replaceSeries.str.replace と異なり、辞書形式で渡すことができる

pandasにない処理をする

自作関数や、パッケージの関数 (neologdn, mecabなど) を使いたい場合

Series.apply を使う。
Series.apply(func) のように関数を渡すことで、Seriesのデータに対してその関数の処理を実行できる。lambda関数も渡せる。

# テキストの最初に '住所 ' を挿入
df['address'].apply(lambda x:  '住所 ' + x)

以降、実際のテキスト前処理についてまとめる。

neologdn (テキスト前処理)

neologdn 0.4

標準ライブラリの normalize だけでは処理しきれない、長音やチルダなども正規化できる日本語テキストの正規化パッケージ。

mecabで解析する前はもちろん、
正規表現で文字列を取得する前に使うと、書くべき正規表現も簡潔になるので、日本語テキストに対して最初に実行すると良い。

import neologdn
df['neologdn'] = df['address'].apply(neologdn.normalize)

# lambda関数を使えばDataFrame.applyでもできる 
df['neologdn'] = df.apply(lambda x: neologdn.normalize(x['address']), axis=1)

分かち書き

mecab-python3

mecab-python3 0.996.3

分かち書き結果だけ欲しい場合は、 -Owakati を指定する。

import MeCab

# `-d` で辞書のパスを指定
tagger = MeCab.Tagger('-Owakati -d /usr/local/lib/mecab/dic/ipadic/')
df['neologdn'].apply(tagger.parse)

これだと最後の改行 \n もついてくるので、取り除きたい場合は、自作関数を定義したり、lambda文で対応する。

tagger = MeCab.Tagger('-Owakati -d /usr/local/lib/mecab/dic/ipadic/')

# 関数を定義
def my_parser(text):
    res = tagger.parse(text)
    return res.strip()

df['neologdn'].apply(my_parser)


# lambda 関数を使えば関数を宣言する必要はない
df['neologdn'].apply(lambda x : tagger.parse(x).strip())
>> 0                  東京  文京  春日 1 丁目 1 - 1 ラクーアビル 2 F
1               東京  新宿  高田馬場 1 - 35 - 3 エミオスタイル 1 F
2           東京  千代田  丸の内 2 - 4 - 1 丸の内 ビルディング B 1 F
3       埼玉  さいたま    美園 5 丁目 50 番地 1 イオン モール 浦和 美園
4    千葉  船橋  浜町 2 - 1 - 1 ららぽーと TOKYO - BAY ららぽーと 3
5                 神奈川  鎌倉  大船 1 - 4 - 1 ルミネウイング 4 F
6              東京  江戸川  南小岩 7 - 24 - 15 シャポー 小岩 1 F
7                東京  豊島  南池袋 1 - 29 - 1 池袋 SP B 1 F
8                       埼玉  川越  脇田  105 アトレ 川越 4 F
9                  東京  葛飾  亀有 3 - 49 - 3 アリオ 亀有 2 F

Sudachipy

SudachiDict-core 20190718

SudachiPy 0.4.2

SudachiPyで、上記mecabのような分かち書き結果を取得したい場合、解析結果のオブジェクトから表層だけを返すような関数をつくる必要がある。

from sudachipy import tokenizer
from sudachipy import dictionary


tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.C

def sudachi_tokenize(text):
    res = tokenizer_obj.tokenize(text, mode)
    return ' '.join([m.surface() for m in res])

df['address'].apply(sudachi_tokenize)
>> 0                  東京都文京区春日 1 丁目 1 - 1 ラクーアビル 2 F
1               東京都新宿区高田馬場 1 - 35 - 3 エミオスタイル 1 F
2           東京都千代田区丸の内 2 - 4 - 1 丸の内 ビルディング B 1 F
3          埼玉県さいたま市緑区美園 5 丁目 50 番地 1 イオンモール 浦和 美園
4    千葉県船橋市浜町 2 - 1 - 1 ららぽーと TOKYO - BAY ららぽーと 3
5                神奈川県鎌倉市大船 1 - 4 - 1 ルミネ ウイング 4 F
6              東京都江戸川区南小岩 7 - 24 - 15 シャポー 小岩 1 F
7              東京都豊島区南池袋 1 - 29 - 1 池袋 SP   B 1 F
8                        埼玉県川越市脇田町 105 アトレ 川越 4 F
9                  東京都葛飾区亀有 3 - 49 - 3 アリオ 亀有 2 F

ちなみにmecabのipadicとは異なり、SudachiのSplitMode.C だと住所をひとまとめにする様子。
(都道府県+市区町村を固有表現として扱っている?)

mode を引数で渡す

上記結果以外にも、
Sudachiは分割単位が3つあるので (分割モード)、モードを指定できるように先ほどの関数 sudachi_tokenize をカスタマイズしてみる。

Series.applyは可変長引数を渡すことができるので、関数側に引数を追加し、apply側で引数を指定すればよい。

def sudachi_tokenize_with_mode(text, mode):
    res = tokenizer_obj.tokenize(text, mode)
    return ' '.join([m.surface() for m in res])

df['address'].apply(sudachi_tokenize_with_mode, mode=tokenizer.Tokenizer.SplitMode.A)
>> 0                  東京  文京  春日 1 丁目 1 - 1 ラクーアビル 2 F
1               東京  新宿  高田馬場 1 - 35 - 3 エミオスタイル 1 F
2           東京  千代田  丸の内 2 - 4 - 1 丸の内 ビルディング B 1 F
3       埼玉  さいたま    美園 5 丁目 50 番地 1 イオン モール 浦和 美園
4    千葉  船橋  浜町 2 - 1 - 1 ららぽーと TOKYO - BAY ららぽーと 3
5                神奈川  鎌倉  大船 1 - 4 - 1 ルミネ ウイング 4 F
6              東京  江戸川  南小岩 7 - 24 - 15 シャポー 小岩 1 F
7              東京  豊島  南池袋 1 - 29 - 1 池袋 SP   B 1 F
8                        埼玉  川越  脇田町 105 アトレ 川越 4 F
9                  東京  葛飾  亀有 3 - 49 - 3 アリオ 亀有 2 F

SplitMode.A にすると、 mecabの結果とほぼ同様の細かい分割になった。

expand を使う

Sudachiには シュミレーションシミュレーション に修正するような、Normalization 機能がある。

normalized_form も同時に返し、DataFrameにすることを考えてみる。

Series.apply には expand機能がないため、DataFrame.applyresult_type='expand'を指定して実行してみる。

def sudachi_tokenize_multi(text):
    res = tokenizer_obj.tokenize(text, mode)
    return ' '.join([m.surface() for m in res]), ' '.join([m.normalized_form() for m in res])

df.apply(lambda x: sudachi_tokenize_multi(x['neologdn']), axis=1, result_type='expand')
0 1
0 東京都文京区春日 1 丁目 1 - 1 ラクーアビル 2 F 東京都文京区春日 1 丁目 1 - 1 ラクーアビル 2 f
1 東京都新宿区高田馬場 1 - 35 - 3 エミオスタイル 1 F 東京都新宿区高田馬場 1 - 35 - 3 エミオスタイル 1 f
2 東京都千代田区丸の内 2 - 4 - 1 丸の内 ビルディング B 1 F 東京都千代田区丸の内 2 - 4 - 1 丸の内 ビルディング b 1 f
3 埼玉県さいたま市緑区美園 5 丁目 50 番地 1 イオンモール 浦和 美園 埼玉県さいたま市緑区美園 5 丁目 50 番地 1 イオンモール 浦和 美園
4 千葉県船橋市浜町 2 - 1 - 1 ららぽーと TOKYO - BAY ららぽーと 3 千葉県船橋市浜町 2 - 1 - 1 ららぽーと トウキョウ - ベイ ららぽーと 3
5 神奈川県鎌倉市大船 1 - 4 - 1 ルミネ ウイング 4 F 神奈川県鎌倉市大船 1 - 4 - 1 ルミネ ウイング 4 f
6 東京都江戸川区南小岩 7 - 24 - 15 シャポー 小岩 1 F 東京都江戸川区南小岩 7 - 24 - 15 シャッポ 小岩 1 f
7 東京都豊島区南池袋 1 - 29 - 1 池袋 SP B 1 F 東京都豊島区南池袋 1 - 29 - 1 池袋 SP b 1 f
8 埼玉県川越市脇田町 105 アトレ 川越 4 F 埼玉県川越市脇田町 105 アトレ 川越 4 f
9 東京都葛飾区亀有 3 - 49 - 3 アリオ 亀有 2 F 東京都葛飾区亀有 3 - 49 - 3 アリオ 亀有 2 f

住所の場合、特に影響がないかと思ったが、 シャポーシャッポ に、 TOKYO - BAYトウキョウ - ベイ に変換された。F のような英字もなぜか小文字になっている。

その他

個人的に前処理する順番としては、

  1. neologdn
  2. 正規表現で不必要な文字列を削除したり、欠損値を補完
  3. mecabで分かち書き

だが、SudachiPyを利用する場合は、正規化機能もついているので、先にトークナイズしてから文字の削除や補完をしてもいいかもしれない。

最初にも書きましたが、もっと良い処理方法があれば教えていただけると幸いです。

28
26
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
26