データサイエンティストが単回帰で最適な中古車を探す話

# 背景
車中泊ができて格好良い車がほしい！
→マツダのCX-8プロアクティブ良くない？
→中古車サイト、探すのめんどくさい
→最も求めている車をデータサイエンスの力で探し出そう！←ｲﾏｺｺ

というわけで、中古車サイトからデータを引っ張ってきて希望に合う車を見つけるための分析をしてみました。
最終的にそれっぽい車が見つかれば良いので、細かいことはあんまり考えずにやっていきます。

# 実装
### スクレイピング
今回は中古車販売サイトの[カーセンサー](https://www.carsensor.net/)さんからデータをスクレイピングしてきました。
一応robots.txtは確認済です。
以下コード

~~~Python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import requests
from bs4 import BeautifulSoup
import time
from tqdm import tqdm

cars_info = []
for i in tqdm(range(17)):
  i += 1
  target_url = f"https://www.carsensor.net/usedcar/bMA/s094/index{i}.html?LP=MA_S094"
  r = requests.get(target_url)
  soup = BeautifulSoup(r.text, "lxml")
  for car_elem in soup.find_all(class_="cassetteMain__carInfoContainer"):
    total_price_main = car_elem.find(class_="totalPrice__mainPriceNum").get_text()
    try:
      total_price_sub = car_elem.find(class_="totalPrice__subPriceNum").get_text()
    except AttributeError: # 小数点以下がない場合
      total_price_sub = ""
    total_price = float(total_price_main + total_price_sub)

    car_info = []
    for elem in car_elem.find_all(class_="specList__emphasisData"):
      car_info.append(elem.get_text())

    cars_info.append(car_info + [total_price])
    time.sleep(1)
~~~
これでcars_infoリスト内に総額、走行距離、年式を取得できました。

### 分析
データが揃ったら、次は前処理をします。
まずは型変更。
取得時に型変更をしておけばよかった、、、（直すのもめんどくさいのでここで処理）
~~~python
df = pd.DataFrame(cars_info, columns=["model_year", "mileage", "price"])
df["mileage"] = df["mileage"].astype(float)
df["model_year"] = df["model_year"].astype(int)
~~~
外れ値データがあったので真っ当そうなデータだけに絞ります。
~~~python
df = df.query("mileage<20").reset_index(drop=True)
~~~
きれいなデータになったところで可視化して見ていきます。
まずは簡単にヒストグラム。
~~~python
for col in df.columns:
  plt.hist(df[col], bins=30)
  plt.title(col)
  plt.show()
~~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219734/c32c38ae-b18d-58e5-d0e4-6af74774628b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219734/e04ef9b7-574a-b707-a517-94d70a3ebadd.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219734/9db155f7-2885-f39e-e243-ccc6939e2523.png)

ボリュームゾーンが一目瞭然ですね。

次は散布図を描いてみます。
今回は「とにかく走行距離に対してお得感のある車がほしい」ので、
年式は一旦置いておいて走行距離と価格の関係性に着目します。
~~~python
plt.scatter(df["mileage"], df["price"], s=1)
~~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219734/23e7e91e-7e70-ca92-77c7-0dce56fbce2c.png)

負に相関してそうですがちょっと線形っぽくないところもあるので、
価格を対数変換してみます。
~~~python
plt.scatter(df["mileage"], np.log(df["price"]), s=1)
~~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219734/7e7d830b-e0a1-5ffd-6da2-3acc2a0c4b7c.png)
ちょっとはマシになったかな。
本来であればちゃんと検証した方がいいですが、今回は見た目で対数変換後の値を採用。

~~~python
df["log_price"] = np.log(df["price"])
~~~

線形回帰をして回帰直線を引いてみます。
~~~python
from sklearn.linear_model import LinearRegression

lr = LinearRegression()
lr.fit(df[["mileage"]].values, df["log_price"])
pred_price = lr.predict(df[["mileage"]].values)

plt.scatter(df["mileage"], df["log_price"], s=1)
plt.plot(df["mileage"], pred_price, color="red")
~~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219734/4a150f27-6073-8a7f-1298-9f561100f3b9.png)
大体こんなもんでしょう。
この回帰直線がいわば”相場”なので、
回帰直線の下側かつ回帰直線から最も遠いデータが”最もお得な車”と考えられます。

~~~python
error = (pred_price - df['log_price'])
df.iloc[error.argmax()]
~~~
~~~
model_year    2019.000000
mileage          3.000000
price          189.500000
log_price        5.244389
Name: 215, dtype: float64
~~~
これで最もお得な車を特定することができました。
3万キロしか走っていなくて190万円程度はかなりお得です。

### 後日談
やっぱ車中泊するならSUVじゃなくない？となりお蔵入り。
諸々雑ですがCX−８がほしい方は参考にどうぞ。