GCP上でスクレイピングを行いBigQueryで機械学習をしてみた

# 初めに
今までBigQueryに触れたことがなかった(それどころかGCP自体も触れていなかった)ので勉強を兼ねて触ってみることにしました。
この記事はそのアウトプットになります。
# 流れ
GCPのVMインスタンス上にjupyter notebookの環境を構築し、クラウド上でsuumoの賃貸物件情報をスクレイピングしました。そのデータをGCSのバケットに転送しBigQueryで価格を予測するモデルの作成を行いました。
スクレイピングは[以前の記事](https://qiita.com/atsuken/items/a5c0de765944f97313c4)で行なったものとほぼ同じです。
なお、この記事ではGCPの設定方法や環境構築については記載いたしません。

# クローリング&スクレイピング
jupyter notebookでpythonを用いてクローリング&スクレイピングを行いました。
```python
import requests
from bs4 import BeautifulSoup
from tqdm import tqdm as tqdm 
import pandas as pd
import time
import re
from google.cloud import storage as gcs
from sklearn.model_selection import train_test_split


name = []
station = []
price = []
room = []
age = []
address = []

user_agent = "ユーザーエージェント名"
header = {'user-agent':user_agent}

# クローリング
for p in tqdm(range(1,51)):
        p=str(p)
        url="https://suumo.jp/jj/chintai/ichiran/FR301FC005/?ar=030&bs=040&ra=013&rn=0045&ek=004506820&cb=0.0&ct=9999999&mb=0&mt=9999999&et=9999999&cn=9999999&shkr1=03&shkr2=03&shkr3=03&shkr4=03&sngz=&po1=09&po2=99&pc=100" + "&page=" + p
        
        urls=requests.get(url,headers=header)
        time.sleep(3)
        urls.encoding = urls.apparent_encoding 
        soup=BeautifulSoup()
        soup=BeautifulSoup(urls.content,"html.parser")
        house_name = soup.find_all("a",class_="js-cassetLinkHref")
        station_name = soup.find_all("div",style="font-weight:bold")
        table_data = pd.read_html(urls.content)
        for h in house_name:
            name.append(h.text)
        for s in station_name:
            station.append(s.text)
        
        for i in range(100):
            table = table_data[i]
            price.append(table.iloc[0,0])
            room.append(table.iloc[0,2])
            age.append(table.iloc[0,3])
            address.append(table.iloc[0,4]) 
   
# 前処理         
time_move_list_pre = []            
time_move_list = []
bus_list = []
walk_list =[]
time_list = []
for i in station:
    time_move_list_pre.append(i.split(" ")[1])
for i in time_move_list_pre:
    if "歩" in i:
        bus_list.append(0)
        walk_list.append(1)
        time_list.append(re.sub(r"\D", "", i))
    else:
        bus_list.append(1)
        walk_list.append(0)
        time_list.append(re.sub(r"\D", "", i))
            
price_list = []
for i in price:
    price_list.append(i.split(" ")[0].replace("万円",""))

room_type_list = []
area_list = []
for i in room:
    room_type_list.append(i.split(" ")[0])
    area_list.append(i.split(" ")[2].replace("m2",""))

age_pre = []
for i in age:
    age_pre.append(i.split(" ")[2])
age_list = []
for i in age_pre:
    if i == "新築":
        age_list.append(0)
    else:
        age_list.append(re.sub(r"\D", "", i))
    
    
name_s=pd.Series(name)
time_s=pd.Series(time_list)
bus_s=pd.Series(bus_list)
walk_s =pd.Series(walk_list)
price_s=pd.Series(price_list)
room_s=pd.Series(room_type_list)
area_s=pd.Series(area_list)
age_s = pd.Series(age_list)
address_s=pd.Series(address)

df=pd.concat([name_s,time_s,bus_s,walk_s,price_s,room_s,area_s,age_s,address_s],axis=1)

df.columns=["name","time_to","bus","walk","price","room","area","age","address"]

# 対数変換
df["time_to"] = df["time_to"].astype(float)
df["price"] = df["price"].astype(float)
df["area"] = df["area"].astype(float)
df["age"] = df["age"].astype(float)

df["time_to_log"] = np.log1p(df["time_to"])
df["price_log"] = np.log1p(df["price"])
df["area_log"] = np.log1p(df["area"])
df["age_log"] = np.log1p(df["age"])

# 学習用にトレーニングデータとテストデータに分ける
train, test = train_test_split(df, random_state=10, test_size=0.30)

#GCSのバケットに転送
project_id = "プロジェクト名" 
bucket_name = "バケット名" 
gcs_path_1 = "data/scraping_data.csv"
gcs_path_2 = "data/train.csv"
gcs_path_3 = "data/test.csv"

client = gcs.Client(project_id)
bucket = client.get_bucket(bucket_name)
blob_gcs_1 = bucket.blob(gcs_path_1)
blob_gcs_2 = bucket.blob(gcs_path_2)
blob_gcs_3 = bucket.blob(gcs_path_3)

blob_gcs_1.upload_from_string(
            data=df.to_csv(index=False)
        )
blob_gcs_2.upload_from_string(
            data=train.to_csv(index=False)
        )
blob_gcs_3.upload_from_string(
            data=test.to_csv(index=False)
        )
```
大体の流れは[この記事](https://qiita.com/atsuken/items/a5c0de765944f97313c4)と同じです。データは、物件を新着順に5000件取得しました。
このデータのカラムは"name","time","bus","walk","price","room","area","age","address"となっており、内容はそれぞれ以下のようになっています。
|カラム名|説明|
| ---- | ---- |
|name|物件名  |
|time_to|駅までの時間|
|walk|駅までの移動手段が徒歩かどうか(0,1表記)|
|bus|駅までの移動手段がバスかどうか(0,1表記)|
|price|賃貸価格(万円)|
|room|部屋の間取り(1LDKなど)|
|area|部屋の面積($m^2$)|
|age|築年数|
|address|住所|
|time_to_log|time_toのデータを対数変換したもの|
|price_log|priceのデータを対数変換したもの※目的変数|
|area_log|areaのデータを対数変換したもの|
|age_log|ageのデータを対数変換したもの|

また、上記のコードでは前処理も同時に行なっています。特に"time_to"、"price"、"area"、"age"のデータは右裾が長い分布になっていたので対数変換しました。
![分布.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517778/37e853a2-a1a6-74b1-9019-9e6c26ed0c34.png)

得られたデータは、トレーニングデータ:テストデータ=7:3に分割し(トレーニングデータ３５00件、テストデータ1500件)、元のデータと合わせて3つのCSVファイルをGCSのバケットに移しました。

# BigQueryでの線形回帰
まずはトレーニングデータを用いてモデルを実装します。今回は説明変数として"time_to_log","area_log","age_log","walk"の４つを使用していきたいと思います。"name"や"address"といった文字型のデータは使用しませんでした。また、移動手段を表すのカラムである"walk"と"bus"ですが、どちらのデータもone-hot表現であり、片方のカラムだけで移動手段が徒歩かバスかがわかるので今回は"walk"のみを使用しました。
以下のクエリでBigqueryで線形回帰ができます。
```SQL
CREATE OR REPLACE MODEL `suumo_dataset.suumo_reg_model`
OPTIONS
  (model_type='linear_reg',
  enable_global_explain=TRUE) AS
SELECT
 price_log AS LABEL,
 time_to_log,
 age_log,
 area_log,
 walk	
FROM `バケット名.suumo_dataset.train_data` 
;

```
CREATE OR REPLACE MODEL 'データセット名.モデル名' でモデルの名前を決めます。OPTIONSのmodel_typeには実装したいモデル名を渡します。enable_global_explainはTrueとしておくことで、あとで説明変数の寄与率を確認することができるようになります。
SELECT以下はよくあるSQL構文です。SELECTでは予測に使う説明変数と目的変数を選びます。目的変数は名前をLABELに変更する必要があります。

クエリを実行すると結果からモデルの詳細を見ることができます。
線形回帰を行った場合、次のような結果を見ることができます。

<img alt="スクリーンショット 2022-04-18 11.07.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517778/3f6120ac-2eed-8b4a-5205-48164b7d0bf9.png" width="300">

この結果から平均二乗誤差や決定係数を知ることができます。

次に、テストデータを用いてモデルの評価を行います。
```SQL
SELECT
*
FROM
  ML.EVALUATE(MODEL `suumo_dataset.suumo_reg_model`,
    (
    SELECT
        price_log AS LABEL,
        time_to_log,
        age_log,
        area_log,
        walk
    FROM
      `バケット名.suumo_dataset.test_data`
    ))
```
モデルの評価の際はFROM句にML.EVALUATE(MODEL `データセット名.モデル名`)を入れる必要があります。
このクエリを実行すると次のような結果が表示されます。
![スクリーンショット 2022-04-18 11.17.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517778/d65e5ad2-6442-844b-6a57-43857f297c4e.png)
これにより、テストデータを用いた時の平均二乗誤差や決定係数を知ることができます。
決定係数を見ると、トレーニングデータでは0.8035、テストデータでは0.7913なので、ほんの少し過学習していることがわかります。

モデルを用いて値を予測する際は次のクエリを実行します。

```SQL
SELECT
*
FROM
  ML.PREDICT(MODEL `suumo_dataset.suumo_reg_model`,
    (
    SELECT
        time_to_log,
        age_log,
        area_log,
        walk
    FROM
      `バケット名.suumo_dataset.test_data`
    ))
```
先ほどのクエリのEVALUATEをPREDICTに変え、SELECT句に説明変数を渡すことで予測が行えます。
![スクリーンショット 2022-04-18 11.24.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517778/b8fbeb41-026c-4ede-d669-a510c5350d4d.png)
predicted_labelが予測結果になります。

また、以下のクエリを実行すると、モデルの予測だけでなく、その予測に対する説明変数の寄与率を確認することができます。
```SQL
SELECT
  *
FROM
  ML.EXPLAIN_PREDICT(MODEL `suumo_dataset.suumo_reg_model`,
    (
    SELECT
      area_log,
      time_to_log,
      walk,
      age_log
    FROM
      `バケット名.suumo_dataset.test_data`
))
```
![スクリーンショット 2022-04-18 11.29.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517778/194269cf-ebe1-a1e3-816a-7093b9a590aa.png)
上の結果では、行１の予測は"age_log"の数値が最も予測に使われていることがわかります。

モデル全体の寄与率を見るには次のクエリを実行します。
```SQL
SELECT
  *
FROM
  ML.GLOBAL_EXPLAIN(MODEL `suumo_dataset.suumo_reg_model`)
;
```
<img alt="スクリーンショット 2022-04-18 11.33.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517778/08746f5b-044f-00ad-c04d-33747edbccac.png" width="500">

この結果から、モデル全体では"area_log"が最も予測に寄与しており、次いで"age_log","time_to_log","walk"であることがわかりました。

# まとめ
はじめてBigQueryに触れてみましたが、一番苦労したのはGCPの設定回りでした。GCPでの作業に慣れるためにももう少し勉強する必要があると感じました。
Bigqueryの利点として、クエリの処理が早いのとSQL(のような構文)で機械学習を行えるといったことが挙げられると思います。しかし、私の場合、普段pythonを中心に使っていてSQLをあまり使っていなかったので、少しやりにくい部分はありました。
大規模なデータを扱うときはBigQueryを使う必要が出てくると思うので、SQLでの操作にも慣れていきたいと思います。

# 参考
https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial?hl=ja