Kaggleの Mercari Price Suggestion Challengeにチャレンジしてみた


今までチュートリアルのコンペに参加してみましたが、今回は初めてちゃんとしたコンペに参加してみます。今回はMercari Price Suggestion Challengeです。

##前回の記事
https://qiita.com/HayatoYamaguchi/items/30e93a43d519f8c8aea0

##Mercari Price Suggestion Challengeとは
なんか一年くらい前にすごい話題になったものらしく、カーネル（お手本の投稿みたいなやつ）もすごく多いのでこれをやってみた。データの特徴としては、特徴量はそんなに多くないが、とにかくデータが今までのと比べてものすごく多い。訓練データだけでも1482531件もあります、しかもその上にItem descriptionという特徴があり、これは言語なので自然言語処理をしないといけず、むちゃくちゃデータが重くて待つのが大変だった（）

##前処理
今回はkaggleのカーネルを参考にやっていきました。
まずデータをsalesPrice（予測すべきもの）だけ落として訓練データとテストデータを連結させます。

```python

path = "../input/"
train = pd.read_csv(path+"train.tsv", header=0, sep='\t')
test = pd.read_csv(path+"test.tsv", header=0, sep='\t')
train_df = train
test_df = test
drop_train_df = train_df.drop('price',axis=1)
allData = pd.concat([drop_train_df,test_df],axis=0)
```

特徴量は７つあるので一つずつ見ていきます

##単純な処理の特徴量

brand_name
これは単純にブランドの名前です。ブランドの名前は大きく価格に影響するので、今回はこれの全てをLabelEncoderを用いて数値化していきます、あとでまとめてやります

category_name
Men/Tops/T-shirts、Women/Tops & Blouses/Blouseのように最大２つのスラッシュによって３つに分かれています。また、欠損値になっているものも多く存在するので今回はこれをスラッシュの最初と真ん中と最後の三つの要素に分解し、それぞれ新たな特徴量としていきます。

```python
def split_cat(text):
    try: return text.split("/")
    except: return ("No Label", "No Label", "No Label")
allData['general_cat'], allData['subcat_1'], allData['subcat_2'] = zip(*allData['category_name'].apply(lambda x: split_cat(x)))
allData.head()

```

item_condition_id
商品の状態ごとのスコアです。１から４までありますがそれぞれがどういう意味なのかは分かりません笑

shipping
客と店どちらが送料を負担しているかというものです。０と１の値をとります。

ここまではやっていることはそこまで難しくありませんでしたが、次からが自然言語処理を行うのでやったことがない自分にとってはとても大変でした（汗）

##自然言語処理を行う特徴量

item_description
その商品の説明が英語で書かれたものです。No description yetと書かれたものも多くあるほか、４つほど欠損データがテストにありました
これらは全て訓練データにあるので、データを落とします。

```python
allData = allData.dropna(subset=['item_description'])
```
次に新しくis_descriptionという、descriptionがあるかどうかを判定する特徴量を作成します。

```python
def if_description(row):
    if row == 'No description yet':
        a = 0
    else:
        a = 1
    return a

allData['is_description'] = allData["item_description"].apply(lambda row : if_description(row))
```

さて、ここからが自然言語処理の本番です。正直自分はまだまだ全然理解できていないですが、今回はTfidfVectorizerという手法を用いてdescriptionに存在する単語の出現回数を数えてそれをベクトル化して、Tfidfを算出します。この辺からむちゃくちゃ重くなりましたw
その後にSVDという手法を使います。これはPCAと類似したもので、次元削減をします。これにより、たくさんの単語をベクトル化した後、そのベクトルの次元削減をすることができます。
ちなみに時間が出てきますが、これはどれくらい時間がかかったかをプリントしてるだけで、特にそれ以上の意味はありません。
ちなみに自然言語処理はよくわからなかったので
https://aiacademy.jp
↑以下のサイトで無料でやってみました、今回やっているのとは内容は異なりましたがどんな感じでtokenizeするのかとかの感覚は分かりました。

```python
from sklearn.feature_extraction.text import TfidfVectorizer

train_df = allData[allData['test_id'].isnull()]
test_df = allData[allData['train_id'].isnull()]

from sklearn.decomposition import TruncatedSVD
import time
start = time.time()
tfidf_vec = TfidfVectorizer(stop_words='english', ngram_range=(1,1))
full_tfidf = tfidf_vec.fit_transform(allData['item_description'].astype('U').values.tolist())
print("tfidfallData終わり")
train_tfidf = tfidf_vec.transform(train_df['item_description'].astype('U').values.tolist())
test_tfidf = tfidf_vec.transform(test_df['item_description'].astype('U').values.tolist())
print("tfidf終わり")
n_comp = 20
svd_obj = TruncatedSVD(n_components=n_comp, algorithm='arpack')
svd_obj.fit(full_tfidf)
print("svdのfit終わり")
train_svd = pd.DataFrame(svd_obj.transform(train_tfidf))
test_svd = pd.DataFrame(svd_obj.transform(test_tfidf))
print("svdのtransform終わり")
train_svd.columns = ['svd_item_'+str(i) for i in range(n_comp)]
test_svd.columns = ['svd_item_'+str(i) for i in range(n_comp)]
train_df = pd.concat([train_df, train_svd], axis=1)
test_df = pd.concat([test_df, test_svd], axis=1)
end = time.time()
print("time taken {}".format(end - start))
```

name
このネームも上のitem_descriptionと同じような処理をします。Tfidfを算出した後にSVDを算出します。
ちなみにどうして他の次元削減の方法やトピックモデリングを使わずに今回これを選択したかというと、いくつか試した中でこれが一番早く動いたからですw
他のだと１時間かかっても次元削減すら終わらないみたいなことが普通に起きました、、、

```python
from sklearn.decomposition import TruncatedSVD
import time
start = time.time()
tfidf_vec = TfidfVectorizer(stop_words='english', ngram_range=(1,1))
full_tfidf = tfidf_vec.fit_transform(allData['name'].values.tolist())
print("tfidfallData終わり")
train_tfidf = tfidf_vec.transform(train_df['name'].values.astype('U').tolist())
test_tfidf = tfidf_vec.transform(test_df['name'].values.astype('U').tolist())
print("tfidf終わり")
n_comp = 20
svd_obj = TruncatedSVD(n_components=n_comp, algorithm='arpack')
svd_obj.fit(full_tfidf)
print("svdのfit終わり")
train_svd = pd.DataFrame(svd_obj.transform(train_tfidf))
test_svd = pd.DataFrame(svd_obj.transform(test_tfidf))
print("svdのtransform終わり")
train_svd.columns = ['svd_name_'+str(i) for i in range(n_comp)]
test_svd.columns = ['svd_name_'+str(i) for i in range(n_comp)]
train_df = pd.concat([train_df, train_svd], axis=1)
test_df = pd.concat([test_df, test_svd], axis=1)
end = time.time()
print("time taken {}".format(end - start))
```


##その他の前処理
ここまでで難しい前処理は終わりです。数値化できる特徴を数値化して、数値化した特徴量以外の特徴量を消し、その上でデータを標準化します。
使わない特徴量をリストに入れて、それを削除します。

```python

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
allData['general_cat'] = le.fit_transform(list(allData['general_cat']))
allData['subcat_1'] = le.fit_transform(list(allData['subcat_1']))
allData['subcat_2'] = le.fit_transform(list(allData['subcat_2']))
allData['brand_name']=allData['brand_name'].fillna('missing').astype(str)
allData['brand_name'] = le.fit_transform(list(allData["brand_name"]))

do_not_use_for_training = ['test_id','train_id','name', 'category_name','item_description']
feature_names = [f for f in train_df.columns if f not in do_not_use_for_training]
X_train = train_df[feature_names]
X_test = test_df[feature_names]
X_test.head()
```

次に、まとめて標準化します。外れ値が多そうなので、正規化ではなく標準化を使用しました。ちなみにこの標準化の処理はRidgeモデルを使用した時には役に立ちましたが、今回はXGboostを使用したので全く必要ではありませんでした笑

```python
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_train)
columns = X_train.columns.values
X_train_scaled = pd.DataFrame(scaler.transform(X_train),columns=columns)
X_test_scaled = pd.DataFrame(scaler.transform(X_test),columns=columns)
X_train_scaled.head()
```

最後にPriceについてです。訓練データのPriceを見ると右に長い分布になっています。そのため、この形が綺麗な二項分布になるようにします。

```python
y_train = np.log(train['price'].values + 1)
```

ここまでで前処理は終了です！
ここからモデルを設計していきます。

##モデルの設計
本当はこのような大量で複雑なデータを使用するときはニューラルネットワークを使用するのが基本であると思われます。しかし、今回はあまりにもこのデータが重かったので単純に時間を取られるなと思い、他の方法によってモデルを作り上げました、他にもやり方調べるのがだるry

まず試したのがRidgeです、いくつかのカーネルでこれが使用されていたため使ってみました。これは30秒もあれば回せる代わりに、スコア予想が低くなりました。
次に試したのがXGboostingです。これは実際のデータを回そうとすると提出までに１時間かかりましたwこれをprediction用にlogなどを直して提出します。

```python
import time
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_log_error
model = xgb.XGBRegressor(max_depth=15,colsample_bytree=0.5,eta=0.05,min_child_weight=20)
model.fit(X_train_scaler,y_train)
preds = model.predict(X_test_scaler)

test["price"] = np.expm1(preds)
test[["test_id", "price"]].to_csv("submission_ridge.csv", index = False)
```

ちなみにこのパラメータの設定は本当はグリッドサーチしないといけませんが、するとそれだけで多分10時間くらいパソコンを回さないといけないので、今回はカーネルにあったパラメータをそのままパクってきました。とりあえずこれで提出して、スコアは0.50315となり、上位40数パーセントくらいになりました。


##最後に
多分もっとしっかりパラメータを調整したり、学習曲線書いたりすれば前処理で次元削減の数とかを調整したりすればもっといいスコアが出ると思いますが、一回回すだけでも１時間くらいかかってしまうのでここで今回は終わりにしますw
自然言語処理ってコードが重くて大変ですね、、、
今度は画像処理とかもやってみようと考えています。


ちなみに参考までに交差検証を行った時のそれぞれのスコアです
ちなみに評価指標はRMSLEです、よく分からない人はググってくださいw
特徴量20個２つでRidge
0.17040984745724053
0.16994236305495025
特徴量20個２つでXGboosting(30分かかる)
0.10012834069970207
0.12882562545649595
特徴量30個２つでRidge
0.16885835791622594
0.16846895351513075