メルカリのkaggleコンペデータ分析に初心者が挑戦してみる

###【自己紹介】
社会人3年目、完全な文系出身です。
1年ほどゆるゆると独学でプログラミングを学習し、
直近3か月間、Aidemyさんでスクールを受講しました。

本ブログの内容が初めてのデータ分析実践となります。
まずは自分が興味を持てる内容として、
実際に使用しているメルカリのコンペデータを分析してみました。


###【使用するデータについて】
[Mercari Price Suggestion Challenge](https://www.kaggle.com/c/mercari-price-suggestion-challenge/overview)
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/14171491-a1d9-3bf5-0e10-584a8df00e8f.png">

今回の分析で使用するデータは、コンペサイトkaggleで提供されている
フリマアプリmercariの商品情報データです。

本コンペでは、テストデータに含まれる商品の
販売価格を予想することが分析の目的となっています。


###【本分析の目的】
今回の分析では、Aidemyで学習した内容を一通り試しながら、
作成したモデルの精度改善を図っていくことを目的としています。


##【分析の流れ】
・データ観察
・データ前処理
・モデル作成
・スコア確認
・データ再処理
・再度モデル作成
・スコア確認


###【データ観察】
####○データの概要
データを読み込み、"price"をターゲットに、
そしてtrainとtestを結合してフラグを付けておきます。

```python
train_df = pd.read_csv('/content/drive/MyDrive/datasets/mercari/mercari_train.tsv', delimiter='\t')
test_df = pd.read_csv('/content/drive/MyDrive/datasets/mercari/mercari_test.tsv', delimiter='\t')

train_df = train_df.rename(columns = {"train_id":"id"})
test_df = test_df.rename(columns = {"test_id":"id"})
target = train_df["price"]

all_df = pd.concat([train_df.drop(["price"], axis=1), test_df],axis=0).reset_index(drop=True)
all_df['Test_Flag'] = 0
all_df.loc[train_df.shape[0]: , 'Test_Flag'] = 1
```

<br>
まずは読み込んだデータを確認します。

```python
print(train_df.shape)
print(test_df.shape)
```

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/1e64855b-4ff5-0151-fbfb-5bdb5fb3507a.png)


各columnの詳細は以下の通りです。
・***train_id or test_id*** / 各商品投稿についたID。
・***name*** / 各投稿のタイトル。nameに価格が含まれていた場合は[rm]で修正。
　　   　例）”食パン 30$ 限界価格” → “食パン [rm] 限界価格” 
・***item_condition_id*** / 出品者が提供する商品の状態。
・***category_name*** / 投稿商品のカテゴリー。
・***brand_name*** / 投稿商品のブランド名。
・***price*** / 最終的に取引された価格。本コンペのターゲット。train限定。
・***shipping*** / 送料を出品者が負担の場合は1、買い手が負担の場合は0
・***item_description*** / 商品説明の全文。nameと同様価格の表記は[rm]に修正。

```python
train_df.head()
```

![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/554825c0-35bc-2f1c-160e-aace77b93628.png)


####○“price”について
価格は最低が0＄、最大が2009＄、
全体的に小さい方に偏ったデータになっています。

```python
target.describe()

import matplotlib.pyplot as plt
plt.hist(train_df["price"],bins=50, edgecolor='white', range=[0, 250])
plt.show()
```

<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/20470493-0e8e-fac0-fc5d-6f1f15657396.png"> <img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/4556f297-1969-99c3-d765-9533adac7a53.png">


####○欠損値の確認
欠損値はcategory_nameとbrand_nameの中で多く見られます。

```python
all_df.isnull().sum()
```

![5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/50ab605e-041f-d8a8-4a00-5233fc110acb.png)


####○現時点での考察
各columnについて、価格に対する影響の仕方を考えてみる。
・***name*** / 何の商品か説明する項目なので最重要。
　　　  　最初の分析ではnameに含まれる単語量(どれだけ説明されているか)で処理。
・***item_condition_id*** / 新品の方が高くなる傾向はあるが、重要度は低め。
・***category_name*** / 投稿の必須項目ではないため、重要度は低め。
・***brand_name*** / ブランド毎にある程度の価格帯があると予想されるため、重要度は中。
・***shipping*** / 送料込(出品者負担)の方が販売価格は高くなるが、重要度は低め。
・***item_description*** / こちらも最重要。nameと同様に処理。


####○分析手法について
今回は連続した数値の予測ということなので、
<b>Lasso、Ridge、ElasticNet</b>を使ってみることにしました。

<img width="500" alt="cheat-sheet.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/0558648f-aab6-08a4-3f17-54ecdf6d2ae5.jpeg">

一度簡単な処理で精度を出して確認し、
その後処理の方法を変えて精度の変化を確認します。


###【データ前処理：1回目】
####○欠損値の確認
欠損は以下の2項目に見られる。
***・brand_name
・category_name***
上記についてはこの後、欠損値の処理を含めつつ、
数値に変換していく作業を行っていきます。

####○nameとitem_descriptionの処理
最初手をつけた時、自然言語を扱うのはなんとなく
難しく感じたため、とりあえず単語量のみを数値に変換しました。
(詳細を書けば書くほど価格が上がるという安直な予想)

```python
#item_description：文章量に含まれる単語数をカウント
def words_count_description(text):
  if text == "No description yet":
    return 0
  else:
    return len(str(text).split())

all_df["words_of_description"] = all_df["item_description"].map(words_count_description)

#name：含まれる単語量をカウント
def words_count_name(text):
  if text == "NaN":
    return 0
  else:
    return len(str(text).split())

all_df["words_of_name"] = all_df["name"].map(words_count_name)
```

####○brand_nameの処理
まずはシンプルにブランドの登録がある場合は1、
ない場合は0として処理を行なった。

```python
#brand_name：ある場合は1、ない場合は0に変換
all_df["is_brand_name"] = np.where(all_df["brand_name"].isnull(),0,1)
```

####〇category_nameの処理
こちらはcategoryを詳細に分けると特徴量が多かったので、
とりあえず各categoryの最上位のジャンルのみ抽出する処理を行なった。
***例)men > tops > shirts の場合は「men」を抽出***

```python
#category_name：カテゴリ最初の単語を抽出
def pick_first_category(text):
  if text == False:
    return "NaN"
  else:
    return str(text).split("/")[0]

all_df["first_category_name"] = all_df["category_name"].map(pick_first_category)

#カテゴリー名を数値にエンコード
Encode_category_list = all_df["first_category_name"].unique().tolist()

def encode_category(ctg):
  return Encode_category_list.index(ctg)

all_df["first_category_name"] = all_df["first_category_name"].map(encode_category)
```

ちなみに各最上位カテゴリーの割合は以下の通り。
女性向けの商品が半数近く占めてます。

```python
all_df["category_name"].map(pick_first_category).value_counts(normalize=True)
```

![6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/a5c7b470-6ded-d0d7-210e-020c95ded74d.png)


####〇trainデータとtestデータを分離
上記の処理によって、全ての特徴量を数値に変換することができたため、
モデルの作成に向けてデータをtrainとtestに戻しておく。

```python
#不要な特徴量を削除したdataframeを作成
all_df_for_model = all_df[['id', 'item_condition_id', 'shipping', 'Test_Flag', 'is_brand_name',
       'words_of_description', 'words_of_name', 'first_category_name']]

#念のためint型に変換
all_df_for_model = all_df_for_model.astype(int)

#train,test用のドロップカラム
drop_col = ["id", "Test_Flag"]

test = all_df_for_model.loc[all_df_for_model["Test_Flag"]==1].drop(drop_col, axis=1).reset_index(drop=True)
train = all_df_for_model.loc[all_df_for_model["Test_Flag"]==0].drop(drop_col, axis=1)
```

一応今の時点でデータフレームはこんな感じ。

```python
all_df_for_model.head()
```

![7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/9fc86b09-47f7-8f66-6408-37180d3c2497.png)


###【モデル作成と学習：1回目】
前述の通り、まずはLasso、Ridge、ElasticNetで試しに作成。

学習のためにtrainデータの7割を使用し、
残りの3割はValidationデータとして精度評価に使用する。

```python
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(train, target, test_size=0.3)

from sklearn.linear_model import Lasso
model_Lasso = Lasso()
model_Lasso.fit(X_train, y_train)

print("Lasso")
print(f"train_score:{model_Lasso.score(X_train, y_train)}")
print(f"val_score:{model_Lasso.score(X_val, y_val)}")


from sklearn.linear_model import ElasticNet
model_Ela = ElasticNet()
model_Ela.fit(X_train, y_train)

print("")
print("ElasticNet")
print(f"train_score:{model_Ela.score(X_train, y_train)}")
print(f"val_score:{model_Ela.score(X_val, y_val)}")


from sklearn.linear_model import Ridge
model_Ridge = Ridge()
model_Ridge.fit(X_train, y_train)

print("")
print("Ridge")
print(f"train_score:{model_Ridge.score(X_train, y_train)}")
print(f"val_score:{model_Ridge.score(X_val, y_val)}")
```

![8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/d0e90963-1609-f92f-687f-20d2040c995c.png)


最もスコアの高いRidgeモデルでも4%弱という悲しい結果であった。
3つのモデルの中ではRidge回帰が最もスコアが高かったため、
次の試行ではRidge回帰でのスコア改善を図る。


##○ここまでの考察
やはり価格を決める要素として、検索の対象となる
***・name
・item_description***
の処理に一番の課題があると考えた。

次の段階では、文章や含まれる単語の意味も
考慮した状態での処理を行うこととした。


###【データ前処理：2回目】
####〇priceの処理
本コンペの評価指標はRMSLE(対数平均二乗誤差)というもので、
対数を取ったでの誤差を計算必要があるとのこと。
なので先にpriceをnp.log1pで処理しておく。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/603e2965-172b-72bc-0f86-7fdb52fcd3ab.png">


```python
target = np.log1p(train_df["price"].values)
```

ちなみにpriceのlogを取った状態でヒストグラムを見ると、
グラフの形がこんな風に変換される。

```python
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (10,3))

#画像左側
ax1 = fig.add_subplot(121)
ax1.set_title("original")
ax1.hist(train_df["price"], bins=50, edgecolor='white', range=[0, 250])

#画像右側
ax2 = fig.add_subplot(122)
ax2.set_title("log1p")
ax2.hist(target, bins=50, edgecolor='white', range=[0, 10])
plt.show()
```

![10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/55214daa-d86b-e82e-2628-ebe57fa74eb7.png)

####〇欠損値の処理
1回目の処理では他の作業に含めていたが、
今回は事前にまとめて以下の処理を行う。

```python
#欠損値を処理
all_df["category_name"].fillna("NaN", inplace=True)
all_df["brand_name"].fillna("None", inplace=True)
all_df["item_description"].fillna("No description yet", inplace=True)
```

####○brand_nameの処理
ブランドの登録数が多ければ、そのブランドの価格帯が大まかに分かり、
価格予測に役立つと考え、以下の処理を行うことにした。

```python
#ブランド上位300個のみ抽出、それ以外はNoneに変換
drop_brand_list = all_df["brand_name"].value_counts().index[300:]

def drop_brand(brand):
  if brand in drop_brand_list:
    return "None"
  else:
    return brand

all_df["brand_name"] = all_df["brand_name"].map(drop_brand)
```

value_countsで数の多い方から上位300個のブランドを抽出。
上位300個に含まれないブランドのデータは、“None”に変換を行なった。

変換後、件数の下位15ブランドは以下の通りとなっており、
最低でも400件以上投稿されているブランドに絞られた。

```python
all_df["brand_name"].value_counts()[-15:]
```

![11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/0492d2a6-0ee5-1980-32db-a8248dd49fc4.png)


####○自然言語処理の活用
***・name
・item_description***
上記2点について自然言語処理で学んだ内容を適用してみることにした。

nameは単語をメインとした簡単な文章のため、
CountVectorizerを使用して、ベクトル変換を行う。

item_descriptionはnameよりも文章としての意味合いが強いため、
TfidfVectorizerを使用する。

```python
count_name = CountVectorizer(min_df=10)
X_name = count_name.fit_transform(all_df["name"])

tfidf_description = TfidfVectorizer(max_features = 200, stop_words = "english", ngram_range = (1,3))
X_description = tfidf_description.fit_transform(all_df["item_description"])
```

最初nameもtf-idfで試したが試行の結果、
最終的にCountVectorizerの方がスコアは高くなった。

>***nameをTfidfVectorizerで変換して学習した時***
>train_score:0.1463342867889036
>val_score:0.14966227842923285

原因としてはやはり、
名前と説明で分の構成が大きく変わり、
単語の持つ影響力に差があったためかと思われる。

####○category_nameの処理
前回は各カテゴリーの最上位を抽出したが、
今回は各カテゴリーの最下位を抽出し、より詳細な分類を行う。
***例)men > tops > shirts の場合は「shirts」を抽出***

```python
def pick_last_category(text):
  if text == "NaN":
    return "NaN"
  else:
    return str(text).split("/")[-1]

all_df["last_category_name"] = all_df["category_name"].map(pick_last_category)
```

####○ダミー変数化
***・item_condition_id
・shipping
・brand_name
・last_category_name***
こちらの4点に関しては、
pandasのget_dummiesを使用し、ダミー変数化を行う。
引数にsparse=Trueとして、疎行列で出力を行う。

```python
#get_dummiesを使用するため、カテゴリー型に変換
all_df["brand_name"] = all_df["brand_name"].astype("category")
all_df["item_condition_id"] = all_df["item_condition_id"].astype("category")
all_df["shipping"] = all_df["shipping"].astype("category")

import scipy
X_dummies = scipy.sparse.csr_matrix(pd.get_dummies(all_df[["item_condition_id", "shipping", "last_category_name", "brand_name"]], sparse = True).values)
```

####○各列の結合
scipyのsparse.hstackを使用し、全ての要素を結合する。
そのデータを元のtrainとtestに分割し、
後は1回目と同様にモデル作成、学習を行える状態となった。

```python
X = scipy.sparse.hstack((X_name , X_description, X_dummies)).tocsr()

shape = train_df.shape[0]
train = X[:shape]
test = X[shape:]

X_train, X_val, y_train, y_val = train_test_split(train, target, test_size=0.3)
```

###【モデル作成と学習：2回目】
今回はモデル作成に使用する特徴量がとても多く、
全ての特徴量が予測に重要になるため、
Ridge回帰での学習にて比較を行うこととした。

```python
model_Ridge = Ridge()
model_Ridge.fit(X_train, y_train)

print(f"train_score:{model_Ridge.score(X_train, y_train)}")
print(f"val_score:{model_Ridge.score(X_val, y_val)}")
```

![12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/8c4b2f75-e8b6-e8a5-74f8-f48121c2ec66.png)


特にパラメータは設定せずに学習を行なったが、
前回と比べて大幅な改善となった。

####○パラメータ「alpha」の調整
調べたらRidge回帰にはalphaというパラメータがあるのが分かったため、
最も精度が高まるalphaを一応調べてみた。

alpha = 1 〜 10で試行し、各結果をリストに格納。
trainとvalidationのスコアを描画し、
またtrainとvalidationの差分も同じように描画行う。

```python
int_list = np.arange(1,11)
train_score = []
val_score = []

for i in int_list:
  
  model_Ridge = Ridge(alpha=i)
  model_Ridge.fit(X_train, y_train)
  train_score.append(model_Ridge.score(X_train, y_train))
  val_score.append(model_Ridge.score(X_val, y_val))

print("alpha1 ～ alpha5")
print(train_score[:6])
print(val_score[:6])

print("")
print("alpha6 ～ alpha10")
print(train_score[6:11])
print(val_score[6:11])
```

![17.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/236bcdc9-1f93-80c8-6b49-fe4ee171b43b.png)



```python
import matplotlib.pyplot as plt

dif_list = []
for train, val in zip(train_score, val_score):
  dif_list.append(train - val)

fig = plt.figure(figsize = (10,3))

ax1 = fig.add_subplot(121)
ax1.set_title("score")
ax1.plot(int_list, train_score, color="y", label="train")
ax1.plot(int_list, val_score, color="b", label="val")
ax1.legend()

ax2 = fig.add_subplot(122)
ax2.set_title("different_train_val")
ax2.plot(int_list, dif_list, color="r")

plt.show()
```

![18.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/cc7f1230-4627-528c-8566-67e3095d626d.png)



上のグラフからも、alpha=4 のときにvalidationに対する精度が高く、
alphaが大きいほど、trainとvalidationの誤差は小さくなることが分かった。
最終的なモデルの完成には、alpha=4とすることにした。

最後にモデルを再度作成し、
validationに対してのRMSLEを算出する。

```python
model_Ridge = Ridge(alpha=4)
model_Ridge.fit(X_train, y_train)

y_pred = model_Ridge.predict(X_val)
RMSLE = np.sqrt(np.mean(np.square(y_pred - y_val)))
print(RMSLE)
```

![19.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/3cb8424c-4c69-0589-e59d-fb449da23b67.png)
今回作成したモデルでのRMSLEは0.4939程度であった。

本コンペで1stを取ったカーネルが0.3875であり、
実際の価格3,000円の場合、2,036円~4,420円程度の誤差範囲にある。

RRMSLEが0.4939だと、
実際の価格3,000円の場合、1,831円~4,916円程度の誤差範囲にある。
初めて分析したにしては、それなりの結果のようにも見える、かも知れない。


###【サブミット】
最後は今までに作ったモデルでテストデータを予測して、
kaggleのweb上にてサブミットするためのデータを出力する。

```python
test_pred = model_Ridge.predict(test)
sample_sub = pd.read_csv("/content/drive/MyDrive/datasets/mercari/sample_submission.csv")
sample_sub["price"] = np.expm1(test_pred)
sample_sub.to_csv("submission.csv", index=False)
```

##【最後に】
今回のコードの全容は以下の通りです。

```python
import pandas as pd
from IPython.display import display
from sklearn import metrics
from sklearn.model_selection import train_test_split
pd.set_option('display.float_format', lambda x:'%.3f' % x)
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

train_df = pd.read_csv('/content/drive/MyDrive/datasets/mercari/mercari_train.tsv', delimiter='\t')
test_df = pd.read_csv('/content/drive/MyDrive/datasets/mercari/mercari_test.tsv', delimiter='\t')

all_df = pd.concat([train_df.drop(["price"], axis=1), test_df],axis=0).reset_index(drop=True)
target = np.log1p(train_df["price"].values)
shape = train_df.shape[0]

#欠損値を処理
all_df["category_name"].fillna("NaN", inplace=True)
all_df["brand_name"].fillna("None", inplace=True)
all_df["item_description"].fillna("No description yet", inplace=True)

#ブランド上位300個のみ抽出、それ以外はNoneに変換
drop_brand_list = all_df["brand_name"].value_counts().index[300:]

def drop_brand(brand):
  if brand in drop_brand_list:
    return "None"
  else:
    return brand

all_df["brand_name"] = all_df["brand_name"].map(drop_brand)

#後にget_dummiesを利用するため、カテゴリー型に変換
all_df["brand_name"] = all_df["brand_name"].astype("category")
all_df["item_condition_id"] = all_df["item_condition_id"].astype("category")
all_df["shipping"] = all_df["shipping"].astype("category")

count_name = CountVectorizer(min_df=10)
X_name = count_name.fit_transform(all_df["name"])

tfidf_description = TfidfVectorizer(max_features = 200, stop_words = "english", ngram_range = (1,3))
X_description = tfidf_description.fit_transform(all_df["item_description"])

#各カテゴリーの末端を単語抽出
def pick_last_category(text):
  if text == "NaN":
    return "NaN"
  else:
    return str(text).split("/")[-1]

all_df["last_category_name"] = all_df["category_name"].map(pick_last_category)

#4項目をダミー変数化、疎行列変換
import scipy
X_dummies = scipy.sparse.csr_matrix(pd.get_dummies(all_df[["item_condition_id", "shipping", "last_category_name", "brand_name"]], sparse = True).values)

#データを結合し、trainとtestに分割
X = scipy.sparse.hstack((X_name , X_description, X_dummies)).tocsr()

train = X[:shape]
test = X[shape:]
X_train, X_val, y_train, y_val = train_test_split(train, target, test_size=0.3)

#モデル作成、学習の後、submitファイルを出力
model_Ridge = Ridge(alpha=4)
model_Ridge.fit(X_train, y_train)

test_pred = model_Ridge.predict(test)
sample_sub = pd.read_csv("/content/drive/MyDrive/datasets/mercari/sample_submission.csv")
sample_sub["price"] = np.expm1(test_pred)
sample_sub.to_csv("submission.csv", index=False)
```

初めて学習用以外のデータに触れて、
本当に色々なところで躓きがありました。

nameとdescriptionについて処理方法を変えるに至るまでにも、
学んだ内容以外のものも調べながら、
何度も何度もスコアを確認し直しました。

またpandasのデータフレーム以外で
モデルの学習を行うのも初めてだったので、
疎行列の扱いについても非常に勉強になりました。

今回のデータ分析は多くの部分で、
Aidemyで学習した内容や他の方のブログを参考に、
なんとか継ぎ接ぎしたような内容でした。

しかし少しでも、自分の考えた処理の工夫によって、
少しずつ精度が上がっていくのが純粋に楽しかったです。

今後は他のデータセットにも触れ、今後は自信で考え、
試行錯誤していける幅を広げていきたいと思います。


##参考ページ
[メルカリの適正価格推定](https://zenn.dev/kyosuke/articles/767e6f642b3ca34dfd27)
[Kaggle メルカリ価格予想チャレンジの初心者チュートリアル](https://www.codexa.net/kaggle-mercari-price-suggestion-challenge/)
[Kaggleは凄かった! 更に簡単な出品を目指して商品の値段推定精度を改善中](https://engineering.mercari.com/blog/entry/2018-11-14-172509/)
[機械学習 〜 テキスト特徴量（CountVectorizer, TfidfVectorizer） 〜](https://qiita.com/fujin/items/b1a7152c2ec2b4963160)
[RMSLEのはなし](https://qiita.com/shnchr/items/a88b1eb2bba8002ecd5d)
[Python, SciPy（scipy.sparse）で疎行列を生成・変換](https://note.nkmk.me/python-scipy-sparse-matrix-csr-csc-coo-lil/)