機械学習を使って一番安い家賃の家に住む。〜前処理編〜

# はじめに
こちらは「機械学習を使って一番安い家賃の家に住む。〜スクレイピング編〜」の続きになります。
前回の記事は[こちら](https://qiita.com/tommy24/items/52048b98bc1169b95c05)から。
（前回の記事で最後ageの列を結合し忘れていました。コードは修正済みです。大変失礼しました。）

# 行ったこと
１　SUUMOから物件データをスクレイピング
２　前処理←今回はここだけ。
３　lightGBMを使って学習、予測。
４　最安値の家に住み、めでたしめでたし。

# 環境
・Mac OS Big sur
・Jupyter Lab
・Python 3.7.0
・Google Chrome

# 早速やってみる。
まずは必要なモジュールのimportとデータを読み込んでみます。

```python
# モジュールの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")
```

![スクリーンショット 2021-03-15 16.26.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/978166/17b894eb-7dbf-6e6f-86f2-e2296aaa286b.png)

うーん、結構汚いデータですね。。。笑
基本的には左から列ごとに前処理をやっていきたいと思います。
ゴールはモデルに入るように全て数値型にすることです。
ちなみに欠損値はなく、データ型は基本的にはobject型でした。

### price,kanrihi,sikikin,reikin,hosyokin,sikibikiについて
家賃や管理費など値段の列です。ex 6.3万
数値型として扱いたいので基本的には「万」「円」は削除して、数値型に変換するだけです。
ただ敷金と礼金には注意が必要です。というのも敷金と礼金は家賃の一ヶ月分となっていることが多く、実質目的変数なので、そのまま使うとリークを起こしてしまいます。
よって、何ヶ月分かに変換しました。

```python
# 不要な文字列の削除
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 東京都西多摩郡瑞穂町大字箱根ケ崎
今回はある特定の市区町村しかスクレイピングしていないので、住所が精度の向上に大きく貢献することは考えられませんが、全国の都道府県のデータを扱うのであれば、かなり大事になってきます。
とはいったものの、今回も郡以降の住所は異なるわけですから、多摩郡以降の部分を１つの特徴量として考え、新たな列を作ります。（同じ郡の中でも場所で家賃の相場は変わると考えられる。）

```python
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 ＪＲ八高線/箱根ヶ崎駅 歩7分
nearest_stationから「何線」「何駅」「歩きかバスか」「時間」の４つの列を新たに作ります。


```python
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すると、「ＪＲ八高線」と「箱根ヶ崎駅 歩7分」で分かれる。
    line_list.append(i[0]) #ＪＲ八高線
    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: #歩１０分やバス２０分などが混在してるので、正規表現で数字だけ取り出す。 
    pattern="\d+" #\dで数字、+で連続して取得。+がないと「１」「０」となってしまう。
    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
これはどう処理するか迷ったのですが、[こちら](https://qiita.com/haraso_1130/items/8ea9ba66f9d5f0fc2157)の記事を参考にさせていただきました。
どうするかというとL,D,K,R,Sという５つの列を作り、該当するものに「1」をしないものに「0」を立てます。
ただ、1LDKと2LDKの違いをどう表現したらいいのか迷いました。単純にLの列だけ2にすればいいのか、LDKの列全てを2にするのか。
まぁこだわっても仕方ないので2LDKの場合はLの部分だけ2にすることにします。
他も同様で2KならKの列を「2」にしています。
また、1Rはワンルーム表記であることに注意です。

```python
#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)
```
1LDKだったらこんな感じです。
![スクリーンショット 2021-03-15 17.34.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/978166/e2ea5e09-dba4-8e26-33f1-c5b89acb01f6.png)

### areaについて
専有面積です。ex 50m2
m2を削除して、数値型にするだけです。

```python
df["area"]=df["area"].str.replace("m2","")
df["area"]=pd.to_numeric(df["area"])
```

### ageについて
築年数です。ex 築30年
基本的には「築」と「年」を消して、数値型にします。
1年は「新築」となっているので、新築は1年に変換します。

```python
# 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に変換しました。

```python
#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: #「３階建」だけのときは３を取り出す。
            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年
中身を見てみると

```python
df["insurance"].unique()
```

![スクリーンショット 2021-03-16 12.17.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/978166/bdd89a2c-6eef-7265-5c73-13001d60f1b2.png)
◯万円△年となっているのがほとんどです。これらは万円の部分だけ採用します。
それ以外の「要」→1.0、「-」→0に変換しました。

```python
# 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についてはありか無しかだけ見ます。
値段も考慮したほうがいいと思いますが、今回はありか無しだけで見てみます。

（実際どう処理すればいいかよく分からなかっただけです。笑
やるなら「ありか無しの列」と「ありならいくらの列」を作ればいいんですかね。。。
でもそうすると、「ありならいくらの列」の「０」という値が駐車場付きで０（無料）なのか、なくて０なのかが区別つかないなぁと。
ただlightGBMは交互作用も考慮してくれるので気にする必要ないかな。。。とか思ってたり。諦めてありか無しの列のみです。）

```python
#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"はすべてラベルエンコーディングします。

```python
# 数値変数をラベルエンコーディング
# 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単体のモデルを使いたいと思います。

```python
# 特徴量作成
df["kaisuritsu"]=df["floor_self"]/df["floor_max"] #階数率。５階建ての２階であれば、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つしか作くれてないです。笑
この当たりはお好きな特徴量を作ってみていただければと思います。

最後に列が多いので全てはお見せ出来ませんがデータはこんな感じです。

![スクリーンショット 2021-03-16 16.09.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/978166/112b8d85-fde4-4895-5204-acad1d7b586a.png)

以上が前処理になります。次の記事では学習編を行いたいと思います。

ご覧頂きありがとうございました。