1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-04-18

初めに

今までBigQueryに触れたことがなかった(それどころかGCP自体も触れていなかった)ので勉強を兼ねて触ってみることにしました。
この記事はそのアウトプットになります。

流れ

GCPのVMインスタンス上にjupyter notebookの環境を構築し、クラウド上でsuumoの賃貸物件情報をスクレイピングしました。そのデータをGCSのバケットに転送しBigQueryで価格を予測するモデルの作成を行いました。
スクレイピングは以前の記事で行なったものとほぼ同じです。
なお、この記事ではGCPの設定方法や環境構築については記載いたしません。

クローリング&スクレイピング

jupyter notebookで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)
        )

大体の流れはこの記事と同じです。データは、物件を新着順に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

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

BigQueryでの線形回帰

まずはトレーニングデータを用いてモデルを実装します。今回は説明変数として"time_to_log","area_log","age_log","walk"の4つを使用していきたいと思います。"name"や"address"といった文字型のデータは使用しませんでした。また、移動手段を表すのカラムである"walk"と"bus"ですが、どちらのデータもone-hot表現であり、片方のカラムだけで移動手段が徒歩かバスかがわかるので今回は"walk"のみを使用しました。
以下のクエリでBigqueryで線形回帰ができます。

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に変更する必要があります。

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

スクリーンショット 2022-04-18 11.07.46.png

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

次に、テストデータを用いてモデルの評価を行います。

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
これにより、テストデータを用いた時の平均二乗誤差や決定係数を知ることができます。
決定係数を見ると、トレーニングデータでは0.8035、テストデータでは0.7913なので、ほんの少し過学習していることがわかります。

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

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
predicted_labelが予測結果になります。

また、以下のクエリを実行すると、モデルの予測だけでなく、その予測に対する説明変数の寄与率を確認することができます。

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
上の結果では、行1の予測は"age_log"の数値が最も予測に使われていることがわかります。

モデル全体の寄与率を見るには次のクエリを実行します。

SELECT
  *
FROM
  ML.GLOBAL_EXPLAIN(MODEL `suumo_dataset.suumo_reg_model`)
;
スクリーンショット 2022-04-18 11.33.52.png

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

まとめ

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

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?