はじめに
こちらは「機械学習を使って一番安い家賃の家に住む。〜スクレイピング編〜」の続きになります。
前回の記事はこちらから。
(前回の記事で最後ageの列を結合し忘れていました。コードは修正済みです。大変失礼しました。)
行ったこと
1 SUUMOから物件データをスクレイピング
2 前処理←今回はここだけ。
3 lightGBMを使って学習、予測。
4 最安値の家に住み、めでたしめでたし。
環境
・Mac OS Big sur
・Jupyter Lab
・Python 3.7.0
・Google Chrome
早速やってみる。
まずは必要なモジュールのimportとデータを読み込んでみます。
# モジュールのimport
from tqdm import tqdm as tqdm
import pandas as pd
import numpy
import re
from sklearn.preprocessing import LabelEncoder
#最大表示列数の指定(ここでは50列を指定)
pd.set_option('display.max_columns', 50)
df=pd.read_csv("suumo.csv")
うーん、結構汚いデータですね。。。笑
基本的には左から列ごとに前処理をやっていきたいと思います。
ゴールはモデルに入るように全て数値型にすることです。
ちなみに欠損値はなく、データ型は基本的にはobject型でした。
price,kanrihi,sikikin,reikin,hosyokin,sikibikiについて
家賃や管理費など値段の列です。ex 6.3万
数値型として扱いたいので基本的には「万」「円」は削除して、数値型に変換するだけです。
ただ敷金と礼金には注意が必要です。というのも敷金と礼金は家賃の一ヶ月分となっていることが多く、実質目的変数なので、そのまま使うとリークを起こしてしまいます。
よって、何ヶ月分かに変換しました。
# 不要な文字列の削除
df["price"]=df["price"].str.replace("万円","")
df["price"]=df["price"].str.replace("円","")
df["price"]=pd.to_numeric(df["price"])
df["price"]=df["price"]*10000
df["kanrihi"]=df["kanrihi"].str.replace("万円","")
df["kanrihi"]=df["kanrihi"].str.replace("円","")
df["kanrihi"]=df["kanrihi"].str.replace("-","0")
df["kanrihi"]=pd.to_numeric(df["kanrihi"])
df["sikikin"]=df["sikikin"].str.replace("万円","")
df["sikikin"]=df["sikikin"].str.replace("円","")
df["sikikin"]=df["sikikin"].str.replace("-","0")
df["sikikin"]=pd.to_numeric(df["sikikin"])
df["sikikin"]=df["sikikin"]*10000
df["sikikin_month"]=df["sikikin"]/df["price"] #月で換算
df=df.drop(columns=["sikikin"],axis=1)
df["reikin"]=df["reikin"].str.replace("万円","")
df["reikin"]=df["reikin"].str.replace("円","")
df["reikin"]=df["reikin"].str.replace("-","0")
df["reikin"]=pd.to_numeric(df["reikin"])
df["reikin"]=df["reikin"]*10000
df["reikin_month"]=df["reikin"]/df["price"] #月で換算
df=df.drop(columns=["reikin"],axis=1)
df["hosyokin"]=df["hosyokin"].str.replace("万円","")
df["hosyokin"]=df["hosyokin"].str.replace("円","")
df["hosyokin"]=df["hosyokin"].str.replace("-","0")
df["hosyokin"]=pd.to_numeric(df["hosyokin"])
df["sikibiki"]=df["sikibiki"].str.replace("万円","")
df["sikibiki"]=df["sikibiki"].str.replace("円","")
df["sikibiki"]=df["sikibiki"].str.replace("-","0")
df["sikibiki"]=pd.to_numeric(df["sikibiki"])
addressについて
住所になります。ex 東京都西多摩郡瑞穂町大字箱根ケ崎
今回はある特定の市区町村しかスクレイピングしていないので、住所が精度の向上に大きく貢献することは考えられませんが、全国の都道府県のデータを扱うのであれば、かなり大事になってきます。
とはいったものの、今回も郡以降の住所は異なるわけですから、多摩郡以降の部分を1つの特徴量として考え、新たな列を作ります。(同じ郡の中でも場所で家賃の相場は変わると考えられる。)
address_list=[]
for i in df["address_s"].str.split("郡"):
address_list.append(i[1])
df.drop("address_s",axis=1,inplace=True)
address_series=pd.Series(address_list,name="address")
df=pd.concat([df,address_series],axis=1)
nearest_stationについて
最寄り駅です。ex JR八高線/箱根ヶ崎駅 歩7分
nearest_stationから「何線」「何駅」「歩きかバスか」「時間」の4つの列を新たに作ります。
line_list=[]
station_onfoot_list=[]
station_list=[]
times_list=[]
on_foot_list=[]
bus_list=[]
times_list2=[]
for i in df["nearest_station"].str.split("/"):#「/」でsplitすると、「JR八高線」と「箱根ヶ崎駅 歩7分」で分かれる。
line_list.append(i[0]) #JR八高線
station_onfoot_list.append(i[1]) #箱根ヶ崎駅 歩7分
line_series=pd.Series(line_list)
station_series=pd.Series(station_onfoot_list)
for i in station_series.str.split(" "):#「箱根ヶ崎駅 歩7分」はスペースでsplitすると「箱根ヶ崎駅」と「歩7分」になる。
station_list.append(i[0]) #箱根ヶ崎駅
times_list.append(i[1]) #歩7分
line_s=pd.Series(line_list,name="line")
station_s=pd.Series(station_list,name="station")
times_series=pd.Series(times_list,name="time")
for i in times_series: #歩10分やバス20分などが混在してるので、正規表現で数字だけ取り出す。
pattern="\d+" #\dで数字、+で連続して取得。+がないと「1」「0」となってしまう。
match=re.findall(pattern,i)[0]
times_list2.append(match)
for i in range(len(times_series)):#歩きならon_foot_listに、バスならbus_listに追加。
if "歩" in times_series[i]:
on_foot_list.append(1)
else:
on_foot_list.append(0)
if "バス" in times_series[i]:
bus_list.append(1)
else:
bus_list.append(0)
times_s=pd.Series(times_list2,name="time")
on_foot_s=pd.Series(on_foot_list,name="on_foot")
bus_s=pd.Series(bus_list,name="bus")
df.drop("nearest_station",axis=1,inplace=True)
df=pd.concat([df,line_s,station_s,on_foot_s,bus_s,times_s],axis=1)
df["time"]=pd.to_numeric(df["time"])
layoutについて
間取りです。ex 1LDK
これはどう処理するか迷ったのですが、こちらの記事を参考にさせていただきました。
どうするかというとL,D,K,R,Sという5つの列を作り、該当するものに「1」をしないものに「0」を立てます。
ただ、1LDKと2LDKの違いをどう表現したらいいのか迷いました。単純にLの列だけ2にすればいいのか、LDKの列全てを2にするのか。
まぁこだわっても仕方ないので2LDKの場合はLの部分だけ2にすることにします。
他も同様で2KならKの列を「2」にしています。
また、1Rはワンルーム表記であることに注意です。
#layout
L_list=[]
D_list=[]
K_list=[]
R_list=[]
S_list=[]
df_layout=pd.DataFrame(columns=["L","D","K","R","S"])
for i in range(len(df["layout"])):
if "6L" in df["layout"][i]:
L_list.append(6)
elif "5L" in df["layout"][i]:
L_list.append(5)
elif "4L" in df["layout"][i]:
L_list.append(4)
elif "3L" in df["layout"][i]:
L_list.append(3)
elif "2L" in df["layout"][i]:
L_list.append(2)
elif "1L" in df["layout"][i]:
L_list.append(1)
else:
L_list.append(0)
for i in range(len(df["layout"])):
if "3D" in df["layout"][i]:
D_list.append(3)
elif "2D" in df["layout"][i]:
D_list.append(2)
elif "D" in df["layout"][i]:
D_list.append(1)
else:
D_list.append(0)
for i in range(len(df["layout"])):
if "3K" in df["layout"][i]:
K_list.append(3)
elif "2K" in df["layout"][i]:
K_list.append(2)
elif "K" in df["layout"][i]:
K_list.append(1)
else:
K_list.append(0)
for i in range(len(df["layout"])):
if "ワンルーム" in df["layout"][i]:
R_list.append(1)
else:
R_list.append(0)
for i in range(len(df["layout"])):
if "3S" in df["layout"][i]:
S_list.append(3)
elif "2S" in df["layout"][i]:
S_list.append(2)
elif "1S" in df["layout"][i]:
S_list.append(1)
else:
S_list.append(0)
L=pd.Series(L_list,name="L")
D=pd.Series(D_list,name="D")
K=pd.Series(K_list,name="K")
R=pd.Series(R_list,name="R")
S=pd.Series(S_list,name="S")
df.drop("layout",axis=1,inplace=True)
df=pd.concat([df,L,D,K,R,S],axis=1)
areaについて
専有面積です。ex 50m2
m2を削除して、数値型にするだけです。
df["area"]=df["area"].str.replace("m2","")
df["area"]=pd.to_numeric(df["area"])
ageについて
築年数です。ex 築30年
基本的には「築」と「年」を消して、数値型にします。
1年は「新築」となっているので、新築は1年に変換します。
# age
df["age"]=df["age"].str.replace("築","")
df["age"]=df["age"].str.replace("年","")
df["age"]=df["age"].str.replace("新","1")
df["age"]=df["age"].astype("int64")
floor_maxについて
何階中の何階かです。ex 2階/2階建
ここが一番大変でした。
まずは単純に「/」でsplitして列を分け、「階」と「建」を削除します。
注意したいのは「1-2階」などメゾネット、「平屋」、地下がある家です。
ちなみにメゾネットは全て1.5、平屋は1に変換しました。
#floor_max
floor_self_list=[] #何階か
floor_max_list=[]
floor_max_list2=[]#何階建てか
underground=[] #地下があるか
for i in df["floor_max"].str.split("/"):
try:
floor_max_list.append(i[1])
floor_self_list.append(i[0])
except:
if "平" in i[0]:
floor_self_list.append("1")
floor_max_list.append("1")
else: #「3階建」だけのときは3を取り出す。
pattern="\d+"
match=re.match(pattern,i[0]).group()
floor_self_list.append(str(match))
floor_max_list.append(str(match))
test_list=[]
floor_max_list2=[]
for i in floor_max_list: #「地下1地上7階建」などの処理。
if "地上" in i:
j=i.split("地上") #
k=j[1]
floor_max_list2.append(k)
else:
floor_max_list2.append(i)
floor_self_s=pd.Series(floor_self_list)
floor_max_s=pd.Series(floor_max_list2)
# 1-2階などメゾネットの処理。
for index,values in enumerate(floor_self_s):
if "-" in values:
floor_self_s[index]="1.5"
else:
pass
# 地下1地上7階建などの処理。
for i in floor_max_list:
if "地下" in i:
underground.append(1)
else:
underground.append(0)
underground_s=pd.Series(underground,name="underground")
df=df.drop(["floor_max","floor"],axis=1)
df["underground"]=underground_s
df["floor_self"]=floor_self_s
df["floor_max"]=floor_max_s
df["floor_self"]=df["floor_self"].str.replace("階","")
df["floor_self"]=df["floor_self"].str.replace("建","")
df["floor_max"]=df["floor_max"].str.replace("階建","")
df["floor_max"]=pd.to_numeric(df["floor_max"])
df["floor_self"]=pd.to_numeric(df["floor_self"])
insuranceについて
保証金です。ex 1.8万円2年
中身を見てみると
df["insurance"].unique()
◯万円△年となっているのがほとんどです。これらは万円の部分だけ採用します。
それ以外の「要」→1.0、「-」→0に変換しました。
# insurance
insurrance_list=[]
for i in df["insurance"].str.split("万円"):
insurrance_list.append(i[0])
insurrance_s=pd.Series(insurrance_list)
insurrance_s=insurrance_s.str.replace("要","1.0")
insurrance_s=insurrance_s.str.replace("-","0")
df.drop("insurance",axis=1)#もとのinsuranceをdrop
df["insurance"]=insurrance_s
df["insurance"]=pd.to_numeric(df["insurance"])
parkingについて
parkingについてはありか無しかだけ見ます。
値段も考慮したほうがいいと思いますが、今回はありか無しだけで見てみます。
(実際どう処理すればいいかよく分からなかっただけです。笑
やるなら「ありか無しの列」と「ありならいくらの列」を作ればいいんですかね。。。
でもそうすると、「ありならいくらの列」の「0」という値が駐車場付きで0(無料)なのか、なくて0なのかが区別つかないなぁと。
ただlightGBMは交互作用も考慮してくれるので気にする必要ないかな。。。とか思ってたり。諦めてありか無しの列のみです。)
#parking
parking_list=[]
for i in range(len(df["parking"])):
parking=df["parking"][i]
if "-" in parking:
parking_list.append(0)
else:
parking_list.append(1)
df=df.drop(columns="parking")
parking_s=pd.Series(parking_list,name="parking")
df=pd.concat([df,parking_s],axis=1)
df["parking"]=pd.to_numeric(df["parking"])
残りは全てラベルエンコーディング
残りの列と上記で変換して出来た列は
"house_name","direction","house_type","construction","address","line","station"はすべてラベルエンコーディングします。
# 数値変数をラベルエンコーディング
# labelencoderを使って、カテゴリ変数を変換。
le=LabelEncoder()
df_categorical = df[["house_name","direction","house_type","construction","address","line","station"]]\
.apply(le.fit_transform)
df_categorical = df_categorical.rename(columns={"house_name":"house_name_c","direction":"direction_c",\
"house_type":"house_type_c","construction":"construction_c",\
"address":"address_c","line":"line_c","station":"station_c"})
df = pd.concat([df,df_categorical],axis=1)
特徴量作成
GBDTはパラメータチューニングでは精度はあまり変わらず、いかに良い特徴量を作るかが鍵になります。Kaggleなどでも特徴量作成は皆さん力をいれてる印象です。
あとはスタッキングなどアンサンブル学習をすると精度は上がりますが、今回はそこまでやる意味はないので、lightGBM単体のモデルを使いたいと思います。
# 特徴量作成
df["kaisuritsu"]=df["floor_self"]/df["floor_max"] #階数率。5階建ての2階であれば、2/5=0.4。
df["per_room_size"]=df["area"]/(df["L"]+df["D"]+df["K"]+df["R"]+df["S"]) #部屋ひとつあたりの面積。
df["area_ratio"]=df["area"]/(df["area"].mean()) #専有面積の平均値との比率
# 目的変数の編集。実際は家賃+管理費が目的変数。
df["rent"]=df["price"]+df["kanrihi"]
df=df.drop(columns=["price","kanrihi","house_name_c"],axis=1)
特徴量作成が鍵となる言いつつ、3つしか作くれてないです。笑
この当たりはお好きな特徴量を作ってみていただければと思います。
最後に列が多いので全てはお見せ出来ませんがデータはこんな感じです。
以上が前処理になります。次の記事では学習編を行いたいと思います。
ご覧頂きありがとうございました。