Pickleファイルの読み込み

サイズの大きなCSVをそのまま読み込むと4分とか時間がかかって困ることがあったので、pickleに変換保存して以降はそれを読み込むことで早くなった。pickleとはデータフレームに限らずクラスなどオブジェクトであればなんでもpickleとして保存できるみたい。

詳しいことはわからないけど、読み込むのは早くなる一方でファイルが壊れると直しようがないみたい。大事なデータはCSVでも持っておいた方がよさそう。あと、pickleだとファイルサイズが大きくなるみたい。

参考サイト

csvをpickleで保存して読み取りを速くする - Qiita

やってみた

Ubiquant Market Predictionのデータ読み込みについてやってみた。データ数は以下の通り300万ほど。

train.shape

(3141410, 304)

CSVをそのまま読み込むと7分かかった。

%%time

path = '../input/ubiquant-market-prediction/train.csv'
basecols = ['row_id', 'time_id', 'investment_id', 'target']
features = [f'f_{i}' for i in range(300)]

dtypes = {
    'row_id': 'str',
    'time_id': 'uint16',
    'investment_id': 'uint16',
    'target': 'float32',
}
for col in features:
    dtypes[col] = 'float32'

train = pd.read_csv(
    path,
    usecols= basecols+features,
    dtype=dtypes
)

CPU times: user 4min 20s, sys: 17.8 s, total: 4min 38s
Wall time: 7min 9s

pickleで保存してから読み込むと1分もかからず。これはこっちのがよいわ。

%%time
train_pkl = pd.read_pickle("./train.pkl")

CPU times: user 1.77 s, sys: 5.79 s, total: 7.56 s
Wall time: 43.7 s

Jigsaw Rate Severity of Toxic Comments(正規表現エスケープ+顔文字+大文字小文字)

置換その2。正規表現エスケープ+顔文字抽出+大文字小文字。

参考サイト

正規表現のエスケープ処理とは? | ポテパンスタイル

やってみた

正規表現エスケープ

最初のきっかけはnotebookそのまま使ったけど置換できてなくない?から。正規表現モジュールreを使わずに.replaceの中で正規表現エスケープの記載を入れてもうまくいかないよなと。多分そういうことだと理解。他のNotebookをそのまま使う前にはちょっと確認しないとなと。

tmp = "You, sir, are my hero. Any chance you remember what page that's on?"
tmp1 = tmp.replace(r"\'s", " ")
tmp2 = re.sub(r"\'s", " ", tmp)

print(f"tmp:{tmp}") # tmp:You, sir, are my hero. Any chance you remember what page that's on?
print(f"tmp1:{tmp1}") # tmp1:You, sir, are my hero. Any chance you remember what page that's on?
print(f"tmp2:{tmp2}") # tmp2:You, sir, are my hero. Any chance you remember what page that  on?
def short_word_rep(text):
    
    text = text.replace(r"what's", "what is ")    
    text = text.replace(r"can't", "cannot ")
    text = text.replace(r"n't", " not ")
    text = text.replace(r"i'm", "i am ")
    text = text.replace(r"I'm", "i am ")
#     text = text.replace(r"\'ve", " have ")
#     text = text.replace(r"\'re", " are ")
#     text = text.replace(r"\'d", " would ")
#     text = text.replace(r"\'ll", " will ")
#     text = text.replace(r"\'scuse", " excuse ")
#     text = text.replace(r"\'s", " ")
    
    text= re.sub(r"\'s", " ", text)
    text= re.sub(r"\'ll", " will ", text)
    text= re.sub(r"\'scuse", " excuse ", text)
    text= re.sub(r"\'d", " would ", text)
    text= re.sub(r"\'re'", " are ", text)
    text= re.sub(r"\'ve'", " have ", text)
    return text

def text_cleaning(text):
    
    text = short_word_rep(text)
    template = re.compile(r'https?://\S+|www\.\S+')
    text = template.sub(r'', text)
    soup = BeautifulSoup(text, 'lxml')
    only_text = soup.get_text()
    text = only_text
    emoji_pattern = re.compile("["
                               u"\U0001F600-\U0001F64F"
                               u"\U0001F300-\U0001F5FF"
                               u"\U0001F680-\U0001F6FF"
                               u"\U0001F1E0-\U0001F1FF"
                               u"\U00002702-\U000027B0"
                               u"\U000024C2-\U0001F251"
                               "]+", flags = re.UNICODE)
    text = emoji_pattern.sub(r'', text)
#     text = re.sub(r"[^a-zA-Z\d]", " ", text)
    text = re.sub(r"[^a-zA-Z\d|!]", " ", text)
    text = re.sub(r"!+", r"!", text)
    text = re.sub(' +', ' ', text)
    text = text.strip()
    return text

clean_df = df.copy(deep= True)
clean_df["text"] = df["text"].apply(text_cleaning)

顔文字

試しに「:(」という顔文字だけを抽出。この顔文字がある52コメンと少ないけど攻撃的なスコア高い!この情報を落とすのはよくにだろう。こんなに低いのであれば、この顔文字があるかも攻撃的スコア自体に組み込んでもよいのでは??とりあえずsadという単語に変換するけど。。

df["face"] = df["text"].apply(lambda x: re.search(r"\:\(", x))
df["face_flg"] = df["face"].apply(lambda x: "NOT_sad_face" if x is None else "sad_face")

agg_d = {
    "y": ["mean", "count"]
}
group = df.groupby("face_flg").agg(agg_d)
group.columns = ["_".join(i) for i in group.columns]
group

f:id:iiiiikamirin:20220120164043p:plain

tmp = df[df.face_flg == "sad_face"].iloc[0,0]
tmp_sub = re.sub(r"\:\(", "sad", tmp)
print(f"tmp:{tmp}") 
print(f"tmp1:{tmp_sub}")
tmp:closedmouth is a DICK-FUCK 

this guy is a jackass who sux cock 4 a fuckin job y the fuck does the trans do it?????cuz its into tht kinda shit >:(
tmp1:closedmouth is a DICK-FUCK 

this guy is a jackass who sux cock 4 a fuckin job y the fuck does the trans do it?????cuz its into tht kinda shit > sad 

逆に笑顔顔文字はスコアが低いわけでもなかった。よって情報なしということで削除してしまう。

df["face"] = df["text"].apply(lambda x: re.search(r"\(\:", x))
df["face_flg"] = df["face"].apply(lambda x: "NOT_happy_face0" if x is None else "happy_face0")
# df["face_flg"] = df["face_flg"].mask(df.face is not None, "happy_face0")
# df["face"] = df["text"].apply(lambda x: re.search(r"\:\)", x))
# # df["face_flg"] = df["face"].apply(lambda x: x if x is None else "happy_face1")
# # df["face_flg"] = df["face"].apply(lambda x: "NOT_happy_face" if x is None else "happy_face")
# df["face_flg"] = df["face_flg"].mask(df.face is not None, "happy_face1")

agg_d = {
    "y": ["mean", "count"]
}
group = df.groupby("face_flg").agg(agg_d)
group.columns = ["_".join(i) for i in group.columns]
group

f:id:iiiiikamirin:20220120173514p:plain

大文字小文字

GOODとgoodだったらなんとなく大文字の方が強い表現に見えるので。単語内に2文字以上大文字があったら大文字に、1文字以下であれば小文字にする。

text = "He is Very VeRy GOOD boy YEah"
text = text.split(" ")
text = [str.upper(i) if len(re.findall(r"[A-Z]", i)) >= 2 else str.lower(i) for i in text]
text = " ".join(text)
text # 'he is very VERY GOOD boy YEAH'

実際、大文字が10文字以上あるテキストの攻撃性yは通常のものより高そう。

tmp = clean_df.copy(deep= True)
tmp["upper_flg"] = tmp["text"].apply(lambda x: "zero" if len(re.findall(r"[A-Z]", x)) <= 10 else "NOT_zero")

agg_d = {
    "y": ["mean", "std", "count"]
}
tmp.groupby("upper_flg").agg(agg_d)

f:id:iiiiikamirin:20220120181311p:plain

Jigsaw Rate Severity of Toxic Comments(文字列の正規表現)

文字列について記号やhttpリンクの置換を行う上では文字列の正規表現についてはやらないといかんなと思い。

参考サイト

分かりやすいpythonの正規表現の例 - Qiita

やってみる

「文字列から数字だけ抽出したいな~~」というときに、「0,1,2,,,,9」のどれかに一致するやつを検索するより「\d」と一文字で表せたら楽だよね(それを連続しているもの単位で抽出できたら尚更)という話と理解。まずはhttpリンクを抽出。

r'https?://\S+|www\.\S+'は縦線「|」は和集合なのでhttps?://\S+www\.\S+のどちらかに一致するものを抽出。?は直前文字が0or1文字なのでhttporhttps://はそのままで、\S+は任意の空白文字が1回以上の繰り返し。www\.\S+\.が任意の一文字\S+が任意の空白以外文字。違いは。。?

template = re.compile(r'https?://\S+|www\.\S+')
tmp0 = df["text"].apply(lambda x: template.search(x))

# 具体的な文字列で一応確認
text = tmp0.dropna().iloc[0].group() # http://www.laayouneinvest.ma/fr/index.asp)
template.sub(r"", text) # ''

a-zA-Z\dについて、a-zは小文字アルファベットとA-Zは大文字アルファベットと\dは数字0-9。^はそれ以外なので、記号をすべて落とす。個人的に!は記号だけど残してよいのでは?と思う。!!!が多いと何となく攻撃的に見えそう。実際に!を1つ以上含むコメントと含まないものでは、含むものの方がスコアyが大きい(攻撃的)。ということで、!だけは残す。ただ、10個も連続しているやつはなんか邪魔になりそうなので一つにしてしまう。

text = "dfd.adjaofspfdu304738"
text = re.sub(r"[^a-zA-Z\d]", " ", text) # 'dfd adjaofspfdu304738'
df["extram_cnt"] = df["text"].apply(lambda x: x.count("!"))
df["extram_cnt_01"] = df["extram_cnt"].apply(lambda x: "not_zero" if x != 0 else "zero")
agg_d = {
    "y": ["mean", "count"]
}

tmp = df.groupby("extram_cnt_01").agg(agg_d)
tmp.columns = ["_".join(i) for i in tmp.columns]
tmp

f:id:iiiiikamirin:20220120110216p:plain

text = "dfd!!adjaofspfd@@@u304738"
text = re.sub(r"[^a-zA-Z\d]", " ", text) # 'dfd  adjaofspfd   u304738'
text = re.sub(r"[^a-zA-Z\d|!]", " ", text) # 'dfd!!adjaofspfd   u304738'

text = "dfhafos!!!fhdof!!!"
text = re.sub(r"!+", r"!", text) # 'dfhafos!fhdof!'

あとは連続するスペースを削除。

text = "dfd!!adjaofs      pfd@@@u304738"
text = re.sub(' +', ' ', text) # 'dfd!!adjaofs pfd@@@u304738'

最後にユニコードによって絵文字を除去。日本語やハングルも削除できた。ユニコードの詳細はちょっとわからず。。これは所与として受け入れる!一応みてみると下2つのユニコードだけでできた。絵文字を含むテキストは1259 (/159571)。日本語もそれほど汚い文字は入ってなかったので削除してしまってよいでしょう!

def emoji_flg(text):
    
    emoji_pattern = re.compile("["
                               u"\U0001F600-\U0001F64F"
                               u"\U0001F300-\U0001F5FF"
                               u"\U0001F680-\U0001F6FF"
                               u"\U0001F1E0-\U0001F1FF"
                               u"\U00002702-\U000027B0"
                               u"\U000024C2-\U0001F251"
                               "]+", flags = re.UNICODE)
#     text = emoji_pattern.sub(r'', text)
    text = emoji_pattern.search(text)
    return text if text is None else text.group()
df["emoji_flg"] = df["text"].apply(emoji_flg)
len(df.emoji_flg.dropna()) # 1259

あと、タグを外す。そのためにbeautifulsoupのget_textを使用。<*>みたいなやつが抜ける。

from bs4 import BeautifulSoup

def f_test(text):
    soup = BeautifulSoup(text, 'lxml')
    only_text = soup.get_text()
    return True if text == only_text else False

df["bs_text"] = df["text"].apply(f_test)
bs_diff = df[~df.bs_text].copy()

i = 0
text = bs_diff.iloc[i,0]
soup = BeautifulSoup(text, 'lxml')
only_text = soup.get_text()
print(f"text:{text}")
print(f"only_text:{only_text}")
text:1)I am an admin, so I guess you were only re-stating the obvious, and 2) the article has been restored and reverted to the more complete version.   - '''' - <*>
only_text:1)I am an admin, so I guess you were only re-stating the obvious, and 2) the article has been restored and reverted to the more complete version.   - '''' - 

Jigsaw Rate Severity of Toxic Comments(googletransで翻訳)

文字列データは逆翻訳というもので水増しするといいらしい!

参考サイト

逆翻訳を使ったテキストデータ水増し – Kaggle Note

Using Google Translate for NLP Augmentation | Kaggle

やってみる

早速エラーだ!

!pip install --quiet googletrans
from googletrans import Translator

translator = Translator()

#この2行で日本語から英語に翻訳しています
#この処理のときにgoogle翻訳のサービスを使っている
# translation = translator.translate("ここに翻訳したい文字列を入れる", src='ja', dest="en")
translation = translator.translate("ここに翻訳したい文字列を入れる", dest="en")

#翻訳されて英語を出力できます!
print(translation.text)
AttributeError: 'NoneType' object has no attribute 'group'

調べてみたところ、新しいライブラリをインストールするとうまくいくとのこと、たしかにうまくいった!一度読み込んだ古いほうのライブラリをアンインストールせずに新しいバージョンのライブラリをインストールしようとしてもエラーはきえなかったけど、新しくカーネル立ち上げて新しいバージョン読み込んだらうまくいった。理由はあまりわからない。

【python】googletransの『AttributeError: 'NoneType' object has no attribute 'group'』対策【2021/01/12追記】 - Qiita

!pip install --quiet googletrans==4.0.0-rc1
from googletrans import Translator

translator = Translator()

#この2行で日本語から英語に翻訳しています
#この処理のときにgoogle翻訳のサービスを使っている
translation = translator.translate("ここに翻訳したい文字列を入れる", src='ja', dest="en")
# translation = translator.translate("ここに翻訳したい文字列を入れる", dest="en")

#翻訳されて英語を出力できます!
print(translation.text) # Insert the string you want to translate here

ひとまずちょいと元のテキストが何語なのかを判定。英語以外が混じっているかもしれないし。そしたらなんかエラーでてきた。。理由がよくわからないが、おそらく多くの処理をかけすぎてはじかれているっぽい。エラーハンドリングしてみる。ほぼほぼ英語ですな。ただ、これものすごい時間がかかった。。

def detect_lang(x):
    try:
        ret = translator.detect(x).lang
    except Exception as e:    
        ret = "dummy"
    return ret

translator = Translator()

i= 0
step = 500
adds = []
while i <= 10000:
# while i <= len(df):
    print(i)
    i+= step
    add = df["text"].iloc[i - step:i].apply(detect_lang)
    adds.append(add)
    
add_lang = pd.concat(adds, axis= 0, sort= True)
add_lang.name = "org_lang"
df = pd.merge(df, add_lang, left_index= True, right_index= True, how= "left")
df.org_lang.value_counts()
en       7131
dummy    3335
fr          5
de          5
es          2
pl          2
ja          2
fy          2
nl          2
yo          1
ar          1
mt          1
hr          1
pt          1
cy          1
el          1
hu          1
vi          1
ha          1
iw          1
gd          1
bg          1
eo          1
Name: org_lang, dtype: int64

諸々やりましたが翻訳するには先にテキストをきれいにしてからの方がよさそうなので、そっち先にやります!

Jigsaw Rate Severity of Toxic Comments(絵文字削除)

前処理その1:絵文字の処理

まずは絵文字を含むテキストを判定。含まれた絵文字を削除するかどうかはまた判断。

参考サイト

SNSテキストから顔文字・絵文字・URLを抽出する - Qiita

やってみた

まずは絵文字を含むテキストを抽出。テキストをみてみたところ、絵文字が攻撃的な意味を直接もつものはそれほどなかった💩。👎とかあんのかなと思ってたけど。なので、絵文字を削除しても情報は抜けないと思うので削除。for文でひとつずつ消したけど、もうちょっとうまくできないものかねえ。

import emoji
import unicodedata

def emoji_detect(text):
    def extract_emoji(words):
        return [w for w in words if w in emoji.UNICODE_EMOJI_ENGLISH]

    text = unicodedata.normalize('NFKC', text) # NFKC正規化
    emojis = extract_emoji(text)
    return emojis

df["text_emoji"] = df["text"].apply(lambda x: emoji_detect(x))
df["emoji_check"] = df["text_emoji"].apply(lambda x: True if len(x) != 0 else False)

tmp_emoji = df.loc[df['emoji_check']].copy()
tmp_NOT_emoji = df.loc[~df["emoji_check"]].copy()

emoji_list = []
for i in range(len(tmp_emoji.text_emoji)):
    emoji_list.extend(tmp_emoji.text_emoji.iloc[i])
emoji_list = list(set(emoji_list))

for i in emoji_list:
    tmp_emoji["text"] = tmp_emoji["text"].str.replace(i, "")    

print(tmp_emoji.shape) # (1133, 4)
print(tmp_NOT_emoji.shape) # (158438, 4)
print(emoji_list) # ['♠', '☎', '♨', '☀' '✒', '▶', '🎄',...

df = pd.concat([tmp_emoji, tmp_NOT_emoji], axis= 0, sort= True).loc[:, ["text", "y"]].copy()
del tmp_emoji, tmp_NOT_emoji

Jigsaw Rate Severity of Toxic Comments(TF-IDF)

sklearnでTF-IDF。用意された文書の集まりから語彙リストのようなものを作成して、頻度が低いほど点数高くする(IDF)。各文書について語彙ごとの出現頻度も出す(TF)。それを掛け合わせることで、各文書ごとに語彙のベクトル的なものを作成。

sklearn.feature_extraction.text.TfidfVectorizer — scikit-learn 1.0.2 documentation

https://www.takapy.work/entry/2019/01/14/141423

https://gotutiyan.hatenablog.com/entry/2020/09/10/181919

手を動かしてみる

こんなデータをいじる。 f:id:iiiiikamirin:20220117211920p:plain

とりあえず参考にしたNotebookの通りのパラメータとかでやってみる。termsを最初の10個ほどみてみたけど、単語がばらされすぎてこれでうまく特徴づけられるのか??という感じ。analyzerは分析上は意図をもってchar_wbにしているみたいではあるけど。。とにもかくにもワードごとのベクトルとしたところでその要素数が43448個もあるのではなにが特徴づけているのかもよくわからない。。ということで、次元削減したい→PCAしてみる。

from sklearn.feature_extraction.text import TfidfVectorizer  # for convert a collection of raw documents to a matrix of TF-IDF features

# TF-IDFの計算
tfidf_vectorizer = TfidfVectorizer(
#     use_idf=True,
#     lowercase=False,
#     max_features= 20000,
    analyzer= "char_wb",
    min_df= 3, # 語彙構築の際にこれより頻度が低いワード削除
    max_df= 0.5, # 語彙構築の際にこれより頻度が高いワード削除
    ngram_range= (3,5),
)
# ("vect3", TfidfVectorizer(min_df= 3, max_df=0.5, analyzer = 'char_wb', ngram_range = (3,5))),

# 文章内の全単語のTfidf値を取得
tfidf_matrix = tfidf_vectorizer.fit_transform(df['text'])

# index 順の単語リスト
terms = tfidf_vectorizer.get_feature_names()

# 単語毎のtfidf値配列:TF-IDF 行列 (numpy の ndarray 形式で取得される)
# 1つ目の文書に対する、各単語のベクトル値
# 2つ目の文書に対する、各単語のベクトル値
# ・・・
# が取得できる(文書の数 * 全単語数)の配列になる。(toarray()で密行列に変換)
tfidfs = tfidf_matrix.toarray()

print(f"term length: {len(terms)}")
print(f"tfidfs shape: {tfidfs.shape}")
terms[:10]
term length: 43448
tfidfs shape: (5000, 43448)
[' ! ', ' !!', ' !! ', ' !!!', ' !!! ', ' !!!!', ' " ', ' ""', ' "" ', ' """']

PCAして主成分の大きい要素をみるとたしかにfから始まる言葉がたくさん☆そしてこれをみるとanalyzercharter_wbとすることで単語の中でngramに分けたり可能→1つのfuckからfuc, uck,,,,とカウントを増やしてfuckの存在感を高められているのかもしれないと思うようになった。あと!が多いやつは攻撃的に見えるのでしょうね。!の数はよい情報かもしれん。

from sklearn.decomposition import PCA

pca = PCA(n_components=2)
pca.fit(tfidfs)

comp = pd.DataFrame(pca.components_, columns= terms, index= ["PCA1", "PCA2"])
comp1 = comp.loc["PCA1",:]
comp1.abs().sort_values(ascending= False).head(15)

comp2 = comp.loc["PCA2",:]
comp2.abs().sort_values(ascending= False).head(15)

f:id:iiiiikamirin:20220117214719p:plain

f:id:iiiiikamirin:20220117214732p:plain

Jigsaw Rate Severity of Toxic Comments(文字列の変換)

言語使うものとかもやってみようかな~と。初心者向けの簡単にスコア提出までの流れをまとめてるNotebookをみていく。

Most VoteのNotebookをみると1. TF-IDF 2. リッジ回帰 3. RoBERTaの3つがメインっぽい。とりあえずTF-IDFとリッジを使ってるNotebookを手を動かしてみようと思う。

文字データをきれいに

文字列の変換も色々書き方がアルンデスネ。ちょっと細かくみれてないです

def clean(data, col):  # Replace each occurrence of pattern/regex in the Series/Index

    # Clean some punctutations
    data[col] = data[col].str.replace('\n', ' \n ')  
    data[col] = data[col].str.replace(r'([a-zA-Z]+)([/!?.])([a-zA-Z]+)',r'\1 \2 \3')
    # Replace repeating characters more than 3 times to length of 3
    data[col] = data[col].str.replace(r'([*!?\'])\1\1{2,}',r'\1\1\1')    
    # Add space around repeating characters
    data[col] = data[col].str.replace(r'([*!?\']+)',r' \1 ')    
    # patterns with repeating characters 
    data[col] = data[col].str.replace(r'([a-zA-Z])\1{2,}\b',r'\1\1')
    data[col] = data[col].str.replace(r'([a-zA-Z])\1\1{2,}\B',r'\1\1\1')
    data[col] = data[col].str.replace(r'[ ]{2,}',' ').str.strip()   
    
    return data  # the function returns the processed value