中古車価格予測チュートリアル
チュートリアル¶
SIGNATE Student Cup 2023 & Career Up Challenge 2023へようこそ!
このチュートリアルでは中古車の価格予測を行う機械学習アルゴリズムの作成を行います。
大まかな流れとして、
1. ライブラリの読み込み
2. データの読み込みと確認
3. データの可視化
4. データの前処理
5. 分割
6. モデルの学習・検証
7. 予測・提出ファイルの作成
までをこのチュートリアルで行います。
チュートリアル終了後の方針などを記載しておりますので、このコードをベースに精度を改善してみてください!
たくさんの投稿をお待ちしております!
1. ライブラリの読み込み¶
まずpythonでデータ分析を行うには、データ分析を行うライブラリが必要です。
ライブラリとは、Pythonで特定の目的や領域に特化した機能を提供するものです。
Pythonのライブラリを使用すると、短いコードや1行のコードで多くの処理を実装することができます。
データ分析を行うライブラリとして、有名なものが
・pandas(テーブルデータの処理に特化したライブラリ)
・numpy(数値計算に特化したライブラリ)
・matplotlib(データの可視化ができるライブラリ)
の3つがあります。
このライブラリを使用するには「import」というコマンドが必要です。
早速やってみましょう!
#ライブラリのimportを行います
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
ここで登場するasはpandasのライブラリなどをpdと省略して呼び出すことができます。
分析にはあまり重要な分野ではないので、ここでは「よくあるもの」として覚えておきましょう。
2.データの読み込みと確認¶
それではデータを読み込んでみましょう!
データを読み込むにはpd.read_csv()
を使います。
なお本チュートリアルでのディレクトリの構造は以下のような構造を想定しております。
StudentCup_CareerUpChallenge 2023
├─data
│ ├─train.csv
│ └─test.csv
│ └─submit_sample.csv
└─tutorial.ipynb
train = pd.read_csv("data/train.csv")
test = pd.read_csv("data/test.csv")
読み込めましたね. 次にデータの中身を簡単にみてみましょう!
train.head()
id | region | year | manufacturer | condition | cylinders | fuel | odometer | title_status | transmission | drive | size | type | paint_color | state | price | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | nashville | 1949 | bmw | excellent | 6 cylinders | gas | 115148 | clean | manual | rwd | mid-size | convertible | orange | NaN | 27587 |
1 | 1 | state college | 2013 | toyota | fair | 8 cylinders | gas | 172038 | clean | automatic | rwd | full-size | sedan | silver | pa | 4724 |
2 | 2 | wichita | 1998 | ford | good | 6 cylinders | gas | 152492 | clean | automatic | fwd | full-size | SUV | silver | ks | 10931 |
3 | 3 | albany | 2014 | ford | excellent | 4 cylinders | gas | 104118 | clean | manual | fwd | mid-size | SUV | blue | ny | 16553 |
4 | 4 | redding | 2005 | ford | excellent | 6 cylinders | gas | 144554 | clean | manual | fwd | mid-size | sedan | red | ca | 5158 |
test.head()
id | region | year | manufacturer | condition | cylinders | fuel | odometer | title_status | transmission | drive | size | type | paint_color | state | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 27532 | western slope | 2015 | chevrolet | excellent | 4 cylinders | gas | 92553 | clean | automatic | fwd | full-size | SUV | red | NaN |
1 | 27533 | roseburg | 2013 | nissan | like new | 4 cylinders | gas | 134385 | salvage | automatic | fwd | mid-size | sedan | black | or |
2 | 27534 | akron / canton | 2011 | volkswagen | good | 4 cylinders | gas | 102489 | clean | automatic | fwd | full-size | sedan | black | oh |
3 | 27535 | denver | 2016 | jeep | excellent | 6 cylinders | diesel | 64310 | clean | automatic | 4wd | mid-size | SUV | red | co |
4 | 27536 | hickory / lenoir | 1999 | honda | excellent | 8 cylinders | gas | 180839 | rebuilt | automatic | 4wd | mid-size | SUV | silver | nc |
データの一部が見えました。
このデータには、
・region(地域)
・year(年式)
・manufacturer(製造社)
・condition(車の状態)
・cylinders(シリンダーの数)
・fuel(燃料)
・odometer(走行距離)
・title_status(見た目の状態)
・transmission(AT車やMT車など)
・drive(運転時の駆動)
・size(大きさ)
・type(車のタイプ)
・paint_color(車の色)
・state(販売州)
・price(価格)
があることが確認できました。
本コンペではtestデータのpriceを正確に予測する必要があります。このため予測対象であるtestデータにはpriceがありません。
次にデータの形状をみてみましょう。
データがいくつ存在するのか、特徴量が何個あるかがわかります。
print(train.shape)
print(test.shape)
(27532, 16) (27537, 15)
訓練データは27532行・16列のデータがあります。
一方でテストデータは27537行・15列のデータがあることが確認できました。
テストデータが1列少ないのはテストデータには、priceの項目がないからですね。
3.データの可視化¶
それでは次にデータの可視化を行います。
データを可視化することでどのような特徴量が予測に重要かがわかります。
チュートリアルでは、予測対象のprice(価格)・odometer(走行距離)・condition(状態)・manufacture(製造社)の可視化を行います。
それではまず予測対象であるpriceの可視化を行ってみましょう!
plt.hist()で中古車価格の分布を見ることができます。
#histはhistgramの略です
plt.hist(train["price"])
#plt.xlabel(), plt.ylabel()で縦軸と横軸の名前を決められます。
plt.xlabel("price")
plt.ylabel("count")
plt.show()
他にも価格の平均値・最大値・最小値を見てみましょう。
pandasでも確認可能ですが、numpyを使ってみてみましょう。
#np.max(最大値を計算したいデータ)で対象のデータの最大値を見ることができます。
print(np.max(train["price"]))
#np.mean(平均値を計算したいデータ)で平均値を見ることができます。
print(np.mean(train["price"]))
#np.min(最小値を計算したいデータ)で平均値を見ることができます
print(np.min(train["price"]))
#pandasの場合、train["price"].max()で最大値を見ることができます。同様にmean()とmin()をつけると対応する値を見ることができます。
#print(train["price"].max())
#print(train["price"].mean())
#print(train["price"].min())
96818 13468.724829289555 1004
可視化した結果、価格が安い中古車のデータが多く、価格が高くなるほどデータ存在しなくなる傾向があることがわかりました。
では価格の高い車と安い車の違いは何が考えられますでしょうか?
それでは次に走行距離と価格の関係性を見ていきましょう!
走行距離が長いほど価格が落ちそうだと考えられますね。
走行距離と価格の関係性を見るには散布図が有効です。散布図で可視化してみましょう!
plt.scatter(train["odometer"], train["price"])
plt.xlabel("odometer")
plt.ylabel("price")
plt.show()
走行距離が長いほど高い価格の車が少なくなる傾向がありそうですね!
この特徴量は予測に使えそうなことがわかりました。
ですが、このグラフを見ると違和感があるのに気が付かないでしょうか?走行距離がマイナスであったり、極端に大きな値がありますね。
本チュートリアルではこの違和感のある価格について調査しませんが、チュートリアルからさらに精度を上げたい場合調査してみてください。
それでは次に車の状態(condition)が車の価格にどのような影響を与えるかみていきましょう!
良い状態であればあるほど高くなりそうですね!
その傾向を見るにはgroupbyを使います。
#groupbyで車の状態ごとの価格の平均を見ることができます。
train.groupby("condition")["price"].agg(["mean", "count"])
mean | count | |
---|---|---|
condition | ||
excellent | 14662.392076 | 15219 |
fair | 7210.345674 | 2404 |
good | 11022.550674 | 6009 |
like new | 16511.362730 | 3810 |
new | 16274.418182 | 55 |
salvage | 8640.685714 | 35 |
やはり状態が良いほど価格が良くなることがわかります!
次に作った会社でもみていきましょう!
会社によって製造する車の価格の方針は違うかもしれません。
#sort_values(by="count", ascending=False)はcount(要素の出現数)を大きい順に並び替えています。
train.groupby("manufacturer")["price"].agg(["mean", "count"]).sort_values(by="count", ascending=False)
mean | count | |
---|---|---|
manufacturer | ||
ford | 14835.941129 | 6166 |
chevrolet | 15060.421683 | 3339 |
bmw | 14014.950292 | 2736 |
toyota | 9230.764331 | 1570 |
honda | 8962.536697 | 1526 |
... | ... | ... |
MERCURY | 6302.000000 | 1 |
lexuѕ | 16867.000000 | 1 |
MITSUBISHI | 13765.000000 | 1 |
JAGUAR | 10655.000000 | 1 |
MINI | 6319.000000 | 1 |
125 rows × 2 columns
どこの製造社がどれくらいの価格かを見ることができました!
ですが、これらの表もグラフで可視化することができます。
実際にconditionで確認してみましょう。
grouped = train.groupby("condition")["price"].mean()
#plt.bar()で棒グラフを表示できる(grouped.indexはconditionの情報を表し、grouped.valuesはpriceの平均値を表している)
plt.bar(grouped.index, grouped.values)
plt.show()
実際に見ることができました!
チュートリアルではここまでですが、他にもtitle_status(見た目の状態)やtransmission(AT車やMT車などの情報)などもこの棒グラフで確認することができます。
train.groupby("確認したいカテゴリデータ")["price"].mean()
チュートリアルが終わり次第やってみましょう!
まず前処理が必要なのはconditionやmanufacturerです。 これらの特徴量は数値ではなく、文字で情報が表現されています。
このような数値ではなく文字で表現されている特徴量のことをカテゴリ変数といいます。
カテゴリ変数は機械学習ではそのままでは扱えないため、一度数値に変換する必要があります。
カテゴリ変数を数値に変更する方法は有名な方法では2つあり、
・ラベルエンコーディング
・ダミー変数
があります。 本チュートリアルではダミー変数を用いて変換を行います。
ダミー変数とはカテゴリデータ内の特定の要素が出現したか、していないかを0と1で表す方法です。
それではやってみましょう。
#pd.get_dummies(データ, columns=[ダミー変数にしたい特徴量])でダミー変数に変換することが可能。columns=[]でダミー変数化する特徴量を指定することができます。
train = pd.get_dummies(train, columns=["condition", "manufacturer"])
test = pd.get_dummies(test, columns=["condition", "manufacturer"])
train.head()
id | region | year | cylinders | fuel | odometer | title_status | transmission | drive | size | ... | manufacturer_mercury | manufacturer_mitsubishi | manufacturer_nissan | manufacturer_pontiac | manufacturer_ram | manufacturer_saturn | manufacturer_subaru | manufacturer_toyota | manufacturer_volkswagen | manufacturer_volvo | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | nashville | 1949 | 6 cylinders | gas | 115148 | clean | manual | rwd | mid-size | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | state college | 2013 | 8 cylinders | gas | 172038 | clean | automatic | rwd | full-size | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 2 | wichita | 1998 | 6 cylinders | gas | 152492 | clean | automatic | fwd | full-size | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 3 | albany | 2014 | 4 cylinders | gas | 104118 | clean | manual | fwd | mid-size | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | 4 | redding | 2005 | 6 cylinders | gas | 144554 | clean | manual | fwd | mid-size | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 rows × 145 columns
#manufacturer_toyotaを見てみよう
train["manufacturer_toyota"].head()
0 0 1 1 2 0 3 0 4 0 Name: manufacturer_toyota, dtype: uint8
できましたね。大量の特徴量が作成されたと思います。
先ほども説明したようにダミー変数は出現したカテゴリデータが出現したかしていないかを0と1で表す方法です。
例えば2行目のmanufacturer_toyotaは1になっていますが、実際に元のデータを確認すると2行目のmanufacturerはtoyotaになっていることを確認できると思います。
他にもconditionのデータでは、
・excellent
・fair
・good
・like new
・new
・salvage
がありましたが、作成されたconditionの特徴量を見てみると、
'condition_excellent', 'condition_fair', 'condition_good', 'condition_like new', 'condition_new', 'condition_salvage'
となっていることが確認できます。
ダミー変数はカテゴリ変数をわかりやすく数値に変換しますが、特徴量の数が多くなってしまう点に注意してください。
train[['condition_excellent', 'condition_fair', 'condition_good', 'condition_like new', 'condition_new', 'condition_salvage']].head()
condition_excellent | condition_fair | condition_good | condition_like new | condition_new | condition_salvage | |
---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 | 0 |
3 | 1 | 0 | 0 | 0 | 0 | 0 |
4 | 1 | 0 | 0 | 0 | 0 | 0 |
5.データの分割¶
それでは学習に使う特徴量を数値データに変換できたのでデータの分割を行います。学習と検証を行います。
データの分割は機械学習のモデルの性能を測るために重要になります。
それでは早速分割を行ってみましょう。
その前に使用する特徴量の選定を行います。
チュートリアルではodometer, condition, manufacturerを使用するのでそれ以外のデータを削除して学習を行います。
特徴量を削除するにはdropを使用します。
#priceは予測対象で学習に必要なため別途targetの変数に格納する
target = train["price"]
#odometer, condition, manufacturer以外を削除
train = train.drop(columns=['id', 'region', 'year', 'cylinders', 'fuel', 'title_status', 'transmission', 'drive', 'size', 'type', 'paint_color', 'state', "price"], axis=1)
test = test.drop(columns=['id', 'region', 'year', 'cylinders', 'fuel', 'title_status', 'transmission', 'drive', 'size', 'type', 'paint_color', 'state'], axis=1)
train.head()
odometer | condition_excellent | condition_fair | condition_good | condition_like new | condition_new | condition_salvage | manufacturer_ACURA | manufacturer_AUDI | manufacturer_BMW | ... | manufacturer_mercury | manufacturer_mitsubishi | manufacturer_nissan | manufacturer_pontiac | manufacturer_ram | manufacturer_saturn | manufacturer_subaru | manufacturer_toyota | manufacturer_volkswagen | manufacturer_volvo | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 115148 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 172038 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 152492 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 104118 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | 144554 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 rows × 132 columns
それではデータの分割を行います。
データの分割は、学習するモデルの性能を適切に計測することができる効果があり、実用化する際の機械学習の性能を担保するためには重要な作業となります。
データ分割の方法には様々な方法がありますが、本チュートリアルではtrain_test_split
でやってみましょう。
from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(train, target, random_state = 82)
print(X_train.shape, X_valid.shape, y_train.shape, y_valid.shape)
(20649, 132) (6883, 132) (20649,) (6883,)
データを分割することができました!
train_test_splitで出力されたX_train, X_valid, y_train, y_validは、分割後のデータです。 Xはデータの特徴量を表し、yは予測対象を表します。
X_train, y_trainはモデルの学習に使用するデータです。
X_valid, y_validはX_train, y_trainで学習したモデルの評価用に用います。
詳しくは次の学習と検証で確認してください!
6.学習と検証¶
それではAIの学習を行ってみましょう!
本チュートリアルでは機械学習アルゴリズムの1種である決定木を使って予測を行います。(決定木の詳細は調べてみてください。)
機械学習アルゴリズムはscikit-learnと呼ばれるライブラリからインポートできます。
また本コンペティションで使用する評価指標MAPE(mean absolute percentage error)のインポートもscikit-learnから可能です。
この二つをインポートしましょう。
#scikit-learnはsklearnでインポート可能
from sklearn.tree import DecisionTreeRegressor
#from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_percentage_error
それでは決定木を呼び出し、分割した学習用データX_train, y_trainで学習してみましょう!
#決定木を呼び出しています。決定木は実行するたびに学習結果が変わることがあるのでrandom_stateに数字を指定すると
model = DecisionTreeRegressor(random_state=42)
#model.fit()で学習します。()内に訓練データと訓練データの目的変数を入れると学習できます。
model.fit(X_train, y_train)
DecisionTreeRegressor(random_state=42)
#model = RandomForestRegressor(random_state=42)
#model.fit(X_train, y_train)
学習できました。
それでは次に予測をしてみましょう!
予測はmodel.predict()
で可能です。
分割した評価用データX_validを予測してみましょう!
#predict()で予測できます。()内に予測したいデータを入れることで予測できます。
pred = model.predict(X_valid)
#予測の中身確認
print(pred[:5])
[ 7148. 5577. 2846. 2458. 33331.]
これで予測できました!
それでは最後に検証データ(y_valid)と予測結果(pred)の目的変数と比較してモデルの精度を評価しましょう!
#MAPEの評価はmean_absolute_percentage_error(正解データ, 予測データ)で可能です。
score = mean_absolute_percentage_error(y_valid, pred)
print(score*100)
95.43942421293022
スコアは95.43942でした。
最後に予測結果と正解データの違いを散布図でみてみましょう。
#横軸が予測結果、縦軸が正解価格です
plt.scatter(pred, y_valid)
plt.xlabel("predict")
plt.ylabel("price")
plt.show()
散布図を見ると安い価格帯のデータ(10000未満)を高い価格(50000以上)として予測したり、高い価格帯の部分(50000以上)を安い価格(10000未満)として予測しており、ところどころで大きく外しているように見えます。
predict = model.predict(test)
それでは提出ファイルを作成します!
提出ファイルの見本はsubmit_sample.csvにあります。確認してみましょう。
#submit_sample.csvを読み込みます。
submit = pd.read_csv("data/submit_sample.csv", header=None)
submit.head()
0 | 1 | |
---|---|---|
0 | 27532 | 14994.540583 |
1 | 27533 | 10004.210369 |
2 | 27534 | 8000.623545 |
3 | 27535 | 15062.223593 |
4 | 27536 | 8994.715270 |
0にidと思わしき列があり、1にpriceと思わしき列があることが確認できました。
submit_sampleのidはテストデータのidの数値と同じなため、1の列の部分をモデルで予測した価格に変更することで問題なく提出できます。
それではやってみましょう。
#submit_sampleのpriceの数値部分を予測データpredictに変更する
submit[1] = predict
#確認してみましょう
submit.head()
0 | 1 | |
---|---|---|
0 | 27532 | 4143.0 |
1 | 27533 | 24638.0 |
2 | 27534 | 8758.0 |
3 | 27535 | 18111.0 |
4 | 27536 | 7649.0 |
priceの部分が予測データに変わっていることが確認できました!
ではこのデータをcsv形式で保存し提出しましょう!
to_csv()
でデータをcsvに保存することができます!
#submission.csvでデータを保存(提出様式はindex=False, header=Noneとなります。)
submit.to_csv("submission.csv", index=False, header=None)
これで本チュートリアルは終了です!
早速作成したsubmission.csvを早速提出してみましょう!
ここまでのチュートリアルお疲れ様でした!
精度を改善する方法¶
提出が完了したところでこのコンペティションは終わりではありません。次に精度を改善する必要があります。
ですがどうすれば精度を上げられるでしょうか?下記にヒントを掲載します。
・予測モデルを変更する(予測モデルを決定木からランダムフォレストに変更してみてください)
model = RandomForestRegressor(random_state=42)
model.fit(X_train, y_train)
・予測モデルのパラメータを変更する
決定木には様々なパラメータが存在します。max_depth(予測する木の深さ)などがあります。このパラメータを変更して、予測の性能がどう変わるかを確認してみましょう。
・予測に使用する特徴量を増やす
region, year, cylinders, fuel, title_status, transmission, drive, size, type, paint_color, stateはまだ使われていません。
・odometerのデータの違和感の調査
走行距離がマイナス・極端に大きい数値に何かあるか確認し、必要があれば修正してみましょう。
・manufacturerの違和感の確認
manufacturerが一部全角や大文字になっています。
この場合、toyotaとTOYOTAは異なるデータとして扱われますが、これは正しい扱い方でしょうか?問題がないかどうかを確認してみましょう。