GIS奮闘記

現役GISエンジニアの技術紹介ブログ。主にPythonを使用。

スポンサーリンク

SUUMO の中古物件情報を Tableau で分析してみる ~データ予測編~

さて、本日は 「SUUMO の中古物件情報を Tableau で分析してみる ~データ予測編~」です。データ収集編で SUUMO の情報をスクレイピング、データ分析編でその情報の分析をしましたが、今回は機械学習を使って販売価格の予測を行ってみようと思います。

なお、本シリーズは以下3エントリーにわたって SUUMO の中古物件情報を扱います。本エントリーはデータ予測編です。

  • データ収集編
    • 分析に必要なデータを収集します。
  • データ分析編
    • 収集したデータの分析を行います。
  • データ予測編
    • 機械学習を使用して物件価格の推論を行います。

使用する機械学習アルゴリズム

ランダムフォレストを使用します。以下エントリーでもランダムフォレストを使用していますが、この時は分類で使用しました。今回は回帰で使用します。

www.gis-py.com

関連エントリー

興味がある方はぜひ読んでみてください。

www.gis-py.com

www.gis-py.com

使用するデータ

データ を加工

処理の都合上、以下加工を行います。

  • ID 列追加
  • 販売価格列を一番最後に移動
  • ㎡ をを空白に置換(データ収集時に取り切れてなかったものがあったようです・・・)
  • 徒歩(分)、バス(分)が空白なレコードを削除
  • バルコニーが空白な場合、0に置換

f:id:sanvarie:20210522095137p:plain

フィールド

販売価格以外を学習に使用します。

フィールド名 説明
カテゴリ 中古マンション or 中古一戸建て
販売価格 販売価格
所在地 物件所在地
沿線 物件最寄沿線
最寄駅 物件最寄駅
徒歩(分) 物件から最寄駅までの徒歩時間。バスの場合は物件からバス停までの時間
バス(分) バス乗車時間。バスを使用しない場合は0
土地面積 土地面積。中古マンションは建物面積=土地面積とする
建物面積 中古一戸建てもしくは中古マンションの建物面積
バルコニー バルコニー面積。中古マンションのみ使用
間取り 物件の間取り
築年数 築年月から計算

予測するフィールド

販売価格を予測して、実際のデータと比較してみようと思います。

  • 販売価格

手順

  1. データ理解
  2. モデルの作成
  3. モデルの評価

環境

Windows10 64bit
Python3.8.5
Tableau Desktop Public Edition 2021.1.0

1.データ理解

販売価格を一億円以下に絞って確認します(その方がわかりやすいので)。

区ごとの販売価格分布

中古マンション

港北区の販売価格分布がやや高いことがわかります。

f:id:sanvarie:20210523075642p:plain

中古一戸建て

都筑区の販売価格分布が高いことがわかります。

f:id:sanvarie:20210523075749p:plain

建物面積と価格の散布図

中古マンション

あまり相関関係がないですね。 f:id:sanvarie:20210523075859p:plain

中古一戸建て

こちらもあまり相関関係がないですね。 f:id:sanvarie:20210523075953p:plain

築年数と価格の散布図

中古マンション

やや負の相関がありますね。 f:id:sanvarie:20210523080106p:plain

中古一戸建て

こちらはほとんど相関がないことがわかります。単純に築年数が高いと価格が安くなるとは言えないですね。

f:id:sanvarie:20210523080157p:plain

2.モデルの作成

以下のようにモデルを作成しました。

import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

# データ読込
df = pd.read_csv(r"D:\data\csv\property.csv")

df = df.astype({'建物面積': 'float64'})

# 出力変数
y = df["販売価格"]

# 入力変数
x = df.iloc[:,0:-1]

# ダミー変数化
x = pd.get_dummies(x)
y = df["販売価格"]

x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=50)

# モデル作成
rfr = RandomForestRegressor(n_estimators=100)
model = rfr.fit(x_train, y_train)

訓練データに対する精度とテストデータに対する精度を確認します。

print(model.score(x_train, y_train))
print(model.score(x_test, y_test))

訓練データ「0.9634692855923855」、テストデータ「0.9631908599251336」という結果でした。かなりいい感じだと思います。

f:id:sanvarie:20210523081050p:plain

予測値の計算

作成したモデルを使用して価格を推論します。

predict = model.predict(x)
predict = pd.DataFrame(predict, columns=["predict"])
predict.head()

このように価格が推論されました。

f:id:sanvarie:20210523083005p:plain

CSVを読み込んだデータフレームの右端に推論結果を追加します。

results = pd.concat([df,predict],axis=1)
results.head()

この結果をCSVで出力してTableau で確認します。

f:id:sanvarie:20210523083236p:plain

3.モデルの評価

予測値と実際の販売価格を比較して誤差の確認を行います。

「誤差」フィールド作成

f:id:sanvarie:20210523083926p:plain

推論値と実際の価格の散布図

推論値をX軸に、実際の価格をY軸にして散布図を書きます。おおむねY=Xの線上に分布しているようですね。これは推論が的中していることを示します。

中古マンション

f:id:sanvarie:20210523091059p:plain

中古一戸建て

f:id:sanvarie:20210523091142p:plain

区別誤差の分布

中古マンション

誤差の分布はかなり狭いですね。

f:id:sanvarie:20210523084319p:plain

しかし、一部のデータで大きく誤差が出ているものもあります。

f:id:sanvarie:20210523085009p:plain

f:id:sanvarie:20210523085055p:plain

中古一戸建て

こちらも誤差の分布はかなり狭く、中古マンションと同じように一部のデータで大きく誤差が出ていました。

f:id:sanvarie:20210523084841p:plain

沿線別誤差の分布

中古マンション

誤差の分布はかなり狭いですね。

f:id:sanvarie:20210523085722p:plain

中古一戸建て

こちらはみなとみらい線にやや大きな分布がみられますね。

f:id:sanvarie:20210523085759p:plain

さいごに

本シリーズ最後のエントリーでしたが、いかがでしたでしょうか。一部のデータを除き実際の価格と予測値の誤差はかなり小さい結果になりました(それでも数十万円~300万円くらいの誤差が出ているものも多かったですが)。学習に使用するデータをもう少し精査することによってより精度の高い結果が出るかと思いますが、その辺は色々勉強していきたいと思います。本日は以上です。

Python で四分位数を計算する方法

さて、本日は Python で四分位数を計算する方法を紹介します。

四分位数とは

統計では、四分位数は、データポイントの数をほぼ等しいサイズの4つの部分、つまり4分の1に分割する分位数の一種です。(出典:Wikipedia)

つまり、データを四等分する三つのデータのことですね。それぞれ第1四分位数Q1(下から25%番目のデータ)、第2四分位数Q2(上から50%番目のデータ。中央値)、第3四分位数Q3(上から25%番目のデータ)と言います。

使用するライブラリ

pandas を使用します。

使用するデータ

以下で収集したSUUMOの中古物件の販売価格を使用します。

www.gis-py.com

使用するレコード

以下の10件を使用します。

import pandas as pd
df = pd.read_csv(r"D:\data\csv\property.csv")
df = df.head(10)
df

f:id:sanvarie:20210519111300p:plain

環境

Windows10 64bit
Python3.8.5

サンプルコード

四分位数を計算するサンプルです。pandas の quantile メソッドを使用します。

quantile1,quantile2,quantile3 = df["販売価格"].quantile([0.25, 0.5, 0.75])
print("第1四分位数" + str(quantile1))
print("第2四分位数" + str(quantile2))
print("第3四分位数" + str(quantile3))

このような結果になりました。

f:id:sanvarie:20210519113107p:plain

参考

外れ値を計算する場合は以下のようにすれば大丈夫です。

# 四分位範囲
iqr = quantile3 - quantile1

lower_bound = quantile1 - (iqr * 1.5)
upper_bound = quantile3 + (iqr * 1.5)

print("小さいほうの外れ値" + str(lower_bound))
print("大きい方の外れ値" + str(upper_bound))

小さいほうの外れ値より小さい値、もしくは、大きい方の外れ値より大きい値があれば外れ値とします。

f:id:sanvarie:20210519113201p:plain

さいごに

いかがでしょうか。pandas を使用すれば簡単に四分位数の計算ができます。興味のある方はぜひ使ってみてください。本日は以上です。

SUUMO の中古物件情報を Tableau で分析してみる ~データ分析編~

さて、本日は 「SUUMO の中古物件情報を Tableau で分析してみる ~データ分析編~」です。前回のエントリーで SUUMO の情報をスクレイピングしましたが、今回はTableau を使ってそのデータを分析してみようと思います。

なお、本シリーズは以下3エントリーにわたって SUUMO の中古物件情報を扱います。本エントリーはデータ分析編です。

使用するデータ

  • 前回のエントリーで作成した property.csv
  • 全国市区町村界データ
    • ESRIジャパンさんが公開しているデータです。最近は本ブログのメインテーマである GIS 要素が少なかったので今回は GIS 的に分析しようと思います。

環境

Windows10 64bit
Tableau Desktop Public Edition 2021.1.0

分析

区ごとの販売価格傾向(平均)

上段が中古マンション、下段が中古一戸建てです。また、価格が高いほど色がオレンジ色に近くなっています。

f:id:sanvarie:20210515170227p:plain

傾向
  • 中古マンション
    • 以外にも区ごとに大きな差はありませんでした。ただ、やはり西区や中区などの繁華街が他の区と比べると価格が高い傾向にありますね。
  • 中古一戸建て
    • 中古マンションに比べると差が出ましたね。中区に加えて東京寄りの区が他の区と比べると価格が高い傾向にありますね。西区の価格が低いことが少し意外でした。

ちなみに中央値で見てみると以下のようになります。 中古マンションの平均販売価格で分析した傾向(西区や中区では価格が高い)が見れなくなりました。一部の高額なマンションが平均を押し上げていた可能性が高いですね。

f:id:sanvarie:20210515170746p:plain

間取りを4LDK以上(私が望む間取り)に絞る

中古マンション

一気に平均価格が上がりましたね。 f:id:sanvarie:20210515172623p:plain

中古一戸建て

むむむ、一戸建てだと平均5000万円以上の区が多くなりましたね。中古なのになんという値段でしょう・・・

f:id:sanvarie:20210515172958p:plain

中央値で見たら少し現実的な値段になった気がします。ここからの分析はすべて中央値で見てみようと思います。それでも西区などの繁華街、青葉区や都筑区などのハイソなエリアに家を買うのは現実的ではないことがわかりました。

f:id:sanvarie:20210515173233p:plain

世帯人数を確認

世帯人数が多いほど色がオレンジ色に近くなっています。オレンジ色の区はファミリー層が多く住んでいると考えられますね。私が狙うべきは以下の条件でしょうか。

  • ファミリー層が多い区
  • 価格が安めの物件が多い区
中古マンション

泉区や瀬谷区など郊外の区にファミリー層が多く住んでいることがわかります。

f:id:sanvarie:20210515175803p:plain

中古一戸建て

こちらも傾向は中古マンションと同じですね。

f:id:sanvarie:20210515180310p:plain

交通の便なども考えると「港南区」「戸塚区」あたりが落としどころな気がしますね。

バスを使用しない物件に絞る

今までの分析結果に加えてバスを使わない物件に絞ってみます。やはりバスは使用したくないですしね。

中古マンション

以外にも価格はあまり上がりませんでした。

f:id:sanvarie:20210515185021p:plain

中古一戸建て

こちらも同じで価格はあまり上がりませんでした。

f:id:sanvarie:20210515185200p:plain

最寄駅徒歩10分以内に絞る

今までの分析結果に加えて最寄駅徒歩10分以内の物件に絞ってみます。日常生活や売却のことを考えたらやはり駅近がいいですしね。

中古マンション

価格がぐっと上がりました。しかし、西区のデータが見えなくなってしまいました。データ不足ですね。

f:id:sanvarie:20210515185720p:plain

中古一戸建て

こちらも価格がぐっと上がりました。こちらもデータ不足で栄区のデータが見えなくなってしまいました。

f:id:sanvarie:20210515185852p:plain

価格的に駅近物件を手にするのは少しハードルが高そうですね。

路線ごとの販売価格傾向(中央値)

これ以降は地図上ではなくグラフでデータ分析を行います。

中古マンション

人気の東横線エリアの価格が高いことがわかります。

f:id:sanvarie:20210515183617p:plain

中古一戸建て

みなとみらい線が圧倒的すぎます。ちょっとこの辺に家を買うことは現実的とは思えないですね。

f:id:sanvarie:20210515183642p:plain

駅ごとの販売価格傾向(中央値)

中古マンション

石川町の価格が圧倒的に高いですね。さすが横浜随一の高級住宅街といったところでしょうか。

f:id:sanvarie:20210515183858p:plain

中古一戸建て

みなとみらい線の価格を押し上げていたのは元町・中華街のようですね。そして、東白楽や生麦の価格が高いのが不思議です。外れ値が入ってしまっているのしれませんね。

f:id:sanvarie:20210515184028p:plain

さいごに

淡々と分析結果を貼っただけですが、いかがでしたでしょうか?データさえあれば Tableau を使って今回紹介したような分析が簡単にできます。また、以下のようなデータを追加するとより面白い結果を得ることができる気がします。

  • 各区の病院数、学校数、交番数
  • 各区の企業数
  • 各区の平均年収
  • 地盤データ
  • etc

次回はいよいよ本シリーズの最後であるデータ予測編です。どんな結果になるかわかりませんが、楽しみにしててください。本日は以上です。

SUUMO の中古物件情報を Tableau で分析してみる ~データ収集編~

さて、本日は 「SUUMO の中古物件情報を Tableau で分析してみる ~データ収集編~」です。私が中古物件を探しているのですが、条件(最寄りの路線や駅近など)によって価格の変動やどういった傾向にあるのかを知りたかったため、Tableau を使って傾向を分析してみようと思います。

なお、本シリーズは以下3エントリーにわたって SUUMO の中古物件情報を扱います。本エントリーはデータ収集編です。

  • データ収集編
    • 分析に必要なデータを収集します。
  • データ分析編
    • 収集したデータの分析を行います。
  • データ予測編
    • 機械学習を使用して物件価格の推論を行います。

データ取得方法

Beautiful Soup を使ったスクレイピングでデータを取得します。関連エントリーを以下に記載しますので、興味がある方はぜひ読んでみてください。

www.gis-py.com

スクレイピング対象データ

f:id:sanvarie:20210508130543p:plain

f:id:sanvarie:20210508130624p:plain

対象エリア

横浜市

理由

横浜市で中古物件の購入を検討しているため

アウトプット

以下のような形でデータを取得し、CSV に出力します。

フィールド名 説明
カテゴリ 中古マンション or 中古一戸建て
販売価格 販売価格
所在地 物件所在地
沿線 物件最寄沿線
最寄駅 物件最寄駅
徒歩(分) 物件から最寄駅までの徒歩時間。バスの場合は物件からバス停までの時間
バス(分) バス乗車時間。バスを使用しない場合は0
土地面積 土地面積。中古マンションは建物面積=土地面積とする
建物面積 中古一戸建てもしくは中古マンションの建物面積
バルコニー バルコニー面積。中古マンションのみ使用
間取り 物件の間取り
築年数 築年月から計算

欲しかったデータ

以下のようなデータも取得できればより詳細な分析ができそうですが、簡単に取得はできなさそうなので今回は諦めます・・・

  • 向き(日当たり)
  • 駐車場有無
  • 庭有無
  • 周辺環境

データ加工内容

以下にデータ加工内容を記載します。ただスクレイピングするだけなら楽なのですが、分析用となるとデータをかなり加工しなければならないのでそれなりに手間がかかりますね。

  • 69.98m2(登記)
  • 2LDK+S(納戸)
  • 103.51m2(実測)
  • 50.11m2(壁芯)
    • ()とその内容を除去する。また、m2を除去する
  • JR
    • JR に変換する
  • ◯◯万円
    • 数値に変換する
  • バルコニーの「-」の表記
    • 0に変換する
  • ◯◯万円~◯◯万円
    • 大きい方の値を使用する(間取りや専有面積も同様)
  • ◯◯万円※権利金含む◯◯万円
    • ※ 以降を除去する
  • LDK+S
    • +S を除去する
  • LK
    • LDKに変換する
  • LLDDKK
    • LDKに変換する(LDDKのように似たようなものも同様)
  • ワンルーム
    • 1Kに変換する
  • 湘南新宿ライン
    • 湘南新宿ライン高海と湘南新宿ライン宇須を湘南新宿ラインに変換する

加工しきれなかったデータ

存在しない沿線(神奈川中央交通など)や駅名(横浜パークタウンなど)の記載があり、そのあたりのデータはとりあえず今回収集して分析時に除外しようと思います。(SUUMO さんには正確なデータを入力してほしいものです)

環境

Windows10 64bit
Python3.8.5

サンプルコード

思ったよりもデータの加工が必要でコードが長くなってしまいました。もう少し改善の余地はあると思いますが、とりあえずサンプルとしては以下です。

# -*- coding: utf-8 -*-
import urllib.request
from bs4 import BeautifulSoup
import pandas as pd

# 中古一戸建てと中古マンションのurl
url_list = ["https://suumo.jp/jj/bukken/ichiran/JJ010FJ001/?ar=030&bs=021&ta=14"  \
            "&jspIdFlg=patternShikugun&sc=14101&sc=14102&sc=14103&sc=14104&sc=14105&sc=14106"  \
            "&sc=14107&sc=14108&sc=14109&sc=14110&sc=14111&sc=14112&sc=14113&sc=14114"  \
            "&sc=14115&sc=14116&sc=14117&sc=14118&kb=1&kt=9999999&tb=0&tt=9999999&hb=0"  \
            "&ht=9999999&ekTjCd=&ekTjNm=&tj=0&cnb=0&cn=9999999&srch_navi=1&pn={}",

            "https://suumo.jp/jj/bukken/ichiran/JJ010FJ001/?ar=030&bs=011&ta=14" \
            "&jspIdFlg=patternShikugun&sc=14101&sc=14102&sc=14103&sc=14104&sc=14105&sc=14106" \
            "&sc=14107&sc=14108&sc=14109&sc=14110&sc=14111&sc=14112&sc=14113&sc=14114" \
            "&sc=14115&sc=14116&sc=14117&sc=14118&kb=1&kt=9999999&mb=0&mt=9999999" \
            "&ekTjCd=&ekTjNm=&tj=0&cnb=0&cn=9999999&srch_navi=1&pn={}"]

# 収集するデータのカラム
cols = ["カテゴリ", "販売価格", "所在地", "区", "沿線", "最寄駅", "徒歩(分)", "バス(分)",
        "土地面積", "建物面積", "バルコニー", "間取り", "築年数"]

def identify_floor_plan(floor_plan):
    # 間取りの種類が多すぎるのでまとめる
    if floor_plan.find("ワンルーム") > -1:
        floor_plan = "1K"
    if floor_plan.find("+") > -1:
        floor_plan = floor_plan[:floor_plan.find("+")]
    if floor_plan.find("LK") > -1:
        floor_plan = floor_plan[0:1] + "LDK"
    if floor_plan.find("LL") > -1 or \
       floor_plan.find("DD") > -1 or \
       floor_plan.find("KK") > -1:
        floor_plan = floor_plan = floor_plan[0:1] + "LDK"

    return floor_plan

def convert_price(price):
    # 万円※権利金含むの処理
    if price.find("※") > -1:
        price = price[:price.find("※") ]

    # ○○万円~○○万円の処理
    if price.find("~") > -1:
        price = price[price.find("~") + 1: ]

        if price.find("億") > -1:
            # 1億円ジャストのような場合の処理
            if price.find("万") == -1:
                price = int(price[:price.find("億")]) * 100000000
            else:
                oku = int(price[:price.find("億")]) * 100000000
                price = oku + int(price[price.find("億") + 1:-2]) * 10000
        else:
            price = int(price[:price.find("万円")]) * 10000
    else:
        if price.find("億") > -1:
            # 1億円ジャストのような場合の処理
            if price.find("万") == -1:
                price = int(price[:price.find("億")]) * 100000000
            else:
                oku = int(price[:price.find("億")]) * 100000000
                price = oku + int(price[price.find("億") + 1:-2]) * 10000
        else:
            price = int(price[:-2]) * 10000

    return price

def remove_brackets(data):
    # ○○~○○の処理
    if data.find("~") > -1:
        data = data[data.find("~") + 1:]

    # m2以降を除去
    if data.find("m2") > -1:
        data = data[:data.find("m")]

    # ()を除去
    if data.find("(") > -1:
        data = data[:data.find("(")]

    return data

def get_line_station(line_station):

    # JRをJRに変換
    if line_station.find("JR") > -1:
        line_station = line_station.replace("JR", "JR")

    # バスと徒歩の時間を取得
    if line_station.find("バス") > -1:
        bus_time = line_station[line_station.find("バス") + 2 : line_station.find("分")]

        # バスの場合は徒歩時間=(バス時間×10) + バス停までの徒歩時間とする
        walk_time = line_station[line_station.find("停歩") + 2 : line_station.rfind("分")]

    else:
        bus_time = 0
        walk_time = line_station[line_station.find("徒歩") + 2 : line_station.rfind("分")]

    # 沿線と駅を取得
    line = line_station[ : line_station.find("「") ]

    if line.find("湘南新宿ライン高海") > -1 or \
       line.find("湘南新宿ライン宇須") > -1:
        line = "湘南新宿ライン"

    station = line_station[line_station.find("「") + 1 : line_station.find("」")]

    return line, station, bus_time, walk_time

def get_page_count(hit_count):

    # データ整形
    hit_count = hit_count.strip()
    hit_count = hit_count.replace(",", "")
    hit_count = hit_count.replace("件", "")

    # ページ数計算
    page_count = divmod(int(hit_count), 30)
    if page_count[1] == 0:
        page_count = page_count[0]
    else:
        page_count = page_count[0] + 1

    return page_count

def main():

    df = pd.DataFrame(index=[], columns=cols)

    for i, url_base in enumerate(url_list):
        # 対象urlのデータを取得
        url = url_base.format(1)
        html = urllib.request.urlopen(url).read()
        soup = BeautifulSoup(html)
        hit_count = soup.find("div", class_="pagination_set-hit").text

        # 各urlのページ数計算
        page_count = get_page_count(hit_count)

        data = {}
        for page in range(1, page_count + 1):

            # ページごとにリクエスト
            if page != 1:
                url = url_base.format(page)
                html = urllib.request.urlopen(url).read()
                soup = BeautifulSoup(html)

            for s in soup.findAll('div', 'dottable-line'):

                if i == 0:
                    data["カテゴリ"] = "中古一戸建て"

                else:
                    data["カテゴリ"] = "中古マンション"

                if len(s.findAll('dt')) == 1:
                    if s.find('dt').text == "販売価格":

                        # ○○万円を数字に変換
                        price = convert_price(s.find("span").text)
                        data["販売価格"] = price

                if len(s.findAll('dt')) == 2:

                    if s.findAll('dt')[0].text == "所在地":
                        area = s.findAll("dd")[0].text
                        data["所在地"] = area
                        data["区"] = area[area.find("市") + 1:area.find("区") + 1]

                    if s.findAll('dt')[1].text == "沿線・駅":

                        # データ加工
                        line, station, bus_time, \
                            walk_time = get_line_station(s.findAll("dd")[1].text)

                        data["沿線"] = line
                        data["最寄駅"] = station
                        data["徒歩(分)"] = walk_time
                        data["バス(分)"] = bus_time

                if s.find('table', class_ = 'dottable-fix') != None:
                    if s.findAll('dt')[0].text == "土地面積":

                        land_area = remove_brackets(s.findAll("dd")[0].text)
                        data["土地面積"] = land_area

                    if s.findAll('dt')[1].text == "間取り":

                        floor_plan = remove_brackets(s.findAll("dd")[1].text)

                        # 間取りをまとめる
                        floor_plan = identify_floor_plan(floor_plan)
                        data["間取り"] = floor_plan

                    if s.findAll('dt')[0].text == "建物面積":

                        house_area = remove_brackets(s.findAll("dd")[0].text)
                        data["建物面積"] = house_area

                    if s.findAll('dt')[0].text == "専有面積":

                        house_area = remove_brackets(s.findAll("dd")[0].text)
                        data["建物面積"] = house_area

                        # 中古マンションは建物面積=土地面積とする
                        data["土地面積"] = house_area

                    if s.findAll('dt')[0].text == "バルコニー":

                        if s.findAll("dd")[0].text.find("-") > -1:
                            data["バルコニー"] = 0
                        else:
                            balcony_area = remove_brackets(s.findAll("dd")[0].text)
                            data["バルコニー"] = balcony_area

                    else: # 一戸建ての場合は0
                        data["バルコニー"] = 0

                    if s.findAll('dt')[1].text == "築年月":

                        # 築年数を算出
                        built_year = 2021 - int(s.findAll("dd")[1].text[:4])
                        data["築年数"] = built_year

                # データフレームに1物件ずつデータを格納
                if len(data) == 13:
                    df = df.append(data, ignore_index=True)
                    data = {}

    # CSV 出力
    df.to_csv(r"D:\data\csv\property.csv", index=False, encoding = "utf-8")

if __name__ == '__main__':
    main()

出力結果を確認

全部で8329件でした。一部抜粋したものを以下に載せます。いい感じにデータを取得できたと思います。

f:id:sanvarie:20210513113825p:plain

f:id:sanvarie:20210513113855p:plain

さいごに

細かいデータの加工がなかなか上手くいかず以外とコードを書くのに手こずってしまいましたが、何とか SUUMO から物件情報を取得することができました。このデータを使って次回のエントリーでTableau を使ったデータ分析をしようと思います。本日は以上です。

VSCode Map Preview を使って テキストエディタ上で地図を表示してみよう!

さて、本日はテキストエディタ上で地図を表示してみようと思います。使用するテキストエディタは Visual Studio Code です。

Visual Studio Code とは

Visual Studio CodeはMicrosoftが開発しているWindows、Linux、macOS用のソースコードエディタである。デバッグ、埋め込みGitコントロールとGitHub、シンタックスハイライト、インテリジェントなコード補完 、スニペット、コードリファクタリングのサポートが含まれる。(出典:Wikipedia)

VSCode Map Preview とは

Visual Studio Code で使用できるプラグインで、様々な地図データを扱うことができます。

marketplace.visualstudio.com

対応フォーマット

  • CSV files (as of 0.5.0)
  • GPX
  • GeoJSON
  • IGC
  • KML
  • TopoJSON
  • WFS
  • GML
  • GML2
  • GML3
  • WKT

使い方

まずは VSCode Map Preview をインストールします。

f:id:sanvarie:20210512111345p:plain

今回はこちらのサイトでダウンロードした日本地図の GeoJSON を使用してみます。対象の地図データをドラッグアンドドロップし、「Map Preview」ボタンを押します。

f:id:sanvarie:20210512112100p:plain

このように地図が表示されます。

f:id:sanvarie:20210512112202p:plain

属性を表示させることもできますね。

f:id:sanvarie:20210512112246p:plain

また、背景地図を変えることもできます。

f:id:sanvarie:20210512112322p:plain

さいごに

一昔前は地図データを扱うのは少し敷居が高かった気がしますが、最近は本当に色々なソフトで地図データの可視化を行うことができるようになっている気がします。これからさらにリッチな機能が追加されていくでしょうし、少し地図データを使うだけなら高価な GIS ソフトを買う必要は無くなりましたね。

機械学習で行った定期預金申し込み推論結果を Tableau で可視化してみよう!

さて、本日は機械学習で行った定期預金申し込み推論結果を Tableau で可視化してみようと思います。機械学習を行うことも難しいのですが、その結果をわかりやすく表示することも多くの方が頭を抱える悩みではないかと思います。Tableau を使えばそんな悩みもすぐに解決するのではないかと思うので、ぜひ一緒に使ってみましょう!

今回使用する機械学習アルゴリズム

今回はランダムフォレストを使用します。

ランダムフォレストは2001年に Leo Breiman によって提案された[1]機械学習のアルゴリズムであり、分類、回帰、クラスタリングに用いられる。決定木を弱学習器とするアンサンブル学習アルゴリズムであり、この名称は、ランダムサンプリングされたトレーニングデータによって学習した多数の決定木を使用することによる。ランダムフォレストをさらに多層にしたアルゴリズムにディープ・フォレストがある。対象によっては、同じくアンサンブル学習を用いるブースティングよりも有効とされる。(出典:Wikipedia)

関連エントリー

興味がある方はぜひ読んでみてください。

www.gis-py.com

www.gis-py.com

今回使用するデータ

2008年から2011年の間のポルトガルの銀行顧客の行動履歴、経済指標と、その顧客が実際に定期預金を申し込んだかどうかの情報を使用します。以下サイトの「 bank-additional-full.csv」を使用します。

archive.ics.uci.edu

開くとこんな感じになっています。データがすごくきれいに整っているので、加工の必要は無さそうですね。

f:id:sanvarie:20210508193647p:plain

注意

このデータですが、「;」が区切り文字になっています。色々都合が悪かったので、「,」に変換しました。

フィールド

データに関しては以下を参照してください。全てのフィールドは使用せずに必要なもののみをピックアップして機械学習を行おうと思います。

フィールド名 説明
age 年齢
job 職業
marital 未婚・既婚
education 教育水準
default 債務不履行があるか
housing 住宅ローン有無
loan 個人ローン有無
contact 連絡方法
month 最終コンタクト月
day_of_week 最終コンタクト日
duration 最終コンタクト時間(秒)
campaign 現キャンペーンにおけるコンタクト回数
pdays 経過日数:全キャンペーンコンタクト後の日数
previous 現キャンペーン前のコンタクト回数
poutcome 前回キャンペーンの成果
emp.var.rate 雇用変動率
cons.price.idx 消費者物価指数
cons.conf.idx 消費者信頼感指数
euribor3m 欧州銀行間取引金利
nr.employed 被雇用者数
y 定期預金申し込み有無

環境

Windows10 64bit
Python3.8.5
Tableau Desktop Public Edition 2021.1.0

手順

  1. データ理解
  2. モデルの作成
  3. 推論の実施

1.データ理解

住宅ローン有無ごとの定期預金申し込み比率を確認しましたが、あまり差はなさそうですね。

f:id:sanvarie:20210509083647p:plain

コンタクト時間での申し込み比率を確認しました。コンタクト時間が長くなれば申し込み比率があがることがわかります。

f:id:sanvarie:20210509084306p:plain

コンタクト回数とコンタクト時間の相関関係を確認します。コンタクト回数が少なくコンタクト時間が長いと申し込みされる傾向にあることがわかります。

f:id:sanvarie:20210509084507p:plain

2.モデルの作成

上記で確認したデータを使用して学習モデルを作成します。

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

df = pd.read_csv(r"D:\data\machine-learning\bank-additional\bank-additional-full.csv")

# 出力変数
y = df["y"]

# 入力変数
x = df.iloc[:,0:-1]

# ダミー変数化
x = pd.get_dummies(x)
y = pd.get_dummies(y)

# 訓練データと検証データに分割
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=0)

# モデル作成
forest = RandomForestClassifier(max_depth = 10, n_estimators=50, max_features = 25, random_state=0)

# 訓練データをモデルに学習させる
forest.fit(x_train, y_train)

訓練データに対する精度とテストデータに対する精度を確認します。

print(forest.score(x_train, y_train))
print(forest.score(x_test, y_test))

訓練データ「0.9458094590657473」、テストデータ「0.9205593862289987」という結果でした。かなりいい感じだと思います。

ハイパーパラメータの調整

今回使用する RandomForestClassifier に対するパラメータ「max_depth」「 n_estimators」「max_features」ですが、「GridSearchCV」を使用してベストな値を確認しました。

  • max_depth・・・一つ一つの木の深さ
  • n_estimators・・・ランダムフォレストで使用する決定木の数
  • max_features・・・使用する特徴量
from sklearn.model_selection import GridSearchCV
estimator =RandomForestClassifier()

param_grid = {"max_depth": [1,10,25,50],
              "n_estimators": [1,10,25,50],
              "max_features": [1,10,25,50]}

cv =5

x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=0)
tuned_model =GridSearchCV(estimator=estimator, param_grid=param_grid)

tuned_model.fit(x_train, y_train)
pd.DataFrame(tuned_model.cv_results_).T

このような表で最適なハイパーパラメータの組み合わせを確認することができます。

f:id:sanvarie:20210509094847p:plain

ただ、表で確認せずとも best_params_ という値に最適なハイパーパラメータが入っているので、確認してみましょう。

tuned_model.best_params_

以下のような結果になりました。この結果を上記の「モデルの作成」のコードで使用しています。

f:id:sanvarie:20210509094955p:plain

後続の処理で使用するので、best_estimator_ に入っている最適なモデルを引き継ぎましょう。

best_model = tuned_model.best_estimator_ 

3.推論の実施

作成されたモデルを使って新しいデータに対して推論を実施します。

サンプルデータ作成

推論用にサンプルデータを作成します。

import pandas as pd

df_original = pd.read_csv(r"D:\data\machine-learning\bank-additional\bank-additional-full.csv")
df_original.iloc[:, :] = df_original.iloc[:,:].sample(frac=1).reset_index(drop=True)
df_original.to_csv(r"D:\data\machine-learning\bank-additional\sample.csv")

元データ f:id:sanvarie:20210509103446p:plain

サンプルデータ f:id:sanvarie:20210509103503p:plain

サンプルデータから予測値を算出、結果をCSVとして出力し、Tableau で結果を確認します。

# サンプルデータ読込
sample = pd.read_csv(r"D:\data\machine-learning\bank-additional\sample.csv")

# 入力変数
sample_x = sample.iloc[:,1:-1]

# ダミー変数化
sample_x = pd.get_dummies(sample_x)

# 最適なモデルから予測値を取得
y_new = best_model.predict_proba(sample_x)

a = []
for i in y_new:
    for ii in i:
        a.append(ii[1])

df_y_new = pd.DataFrame(a, columns=["predict"])

sample_predict = pd.concat([sample,df_y_new],axis=1)

# CSV出力
sample_predict.to_csv(r"D:\data\machine-learning\bank-additional\sample_predict.csv")

Tableau で結果を確認

職業別ごとの申し込み確率

admin が申し込み確率が高いことがわかります。

f:id:sanvarie:20210509121000p:plain

コンタクト時間ごとの申し込み確率

コンタクト時間はおおよそ5分以内にした方がよいことがわかります。

f:id:sanvarie:20210509121528p:plain

個人ローン有無ごとの申し込み確率

個人ローンを組んでいない顧客の方が申し込み確率が高くなることがわかります。

f:id:sanvarie:20210509121734p:plain

住宅ローン有無ごとの申し込み確率

住宅ローンを組んでいる方がやや申し込み確率が高くなることがわかります。

f:id:sanvarie:20210509122002p:plain

最終コンタクト月ごとの申し込み確率

なぜか最終コンタクト月が5月だと申し込み確率が高くなっています。興味深いですね。

f:id:sanvarie:20210509122403p:plain

さいごに

いかがでしたでしょうか。ランダムフォレストを使うことによってかなり高い精度で定期預金申し込みの推論ができたかと思います。ここに詳細な顧客データなどを結合して Tableau で確認してみるともっと面白い結果が見れたのではないかと思います。機械学習については引き続き紹介していきたいと思います。本日は以上です。

Python での探索的データ解析には pandas-profiling を使おう

さて、本日は pandas-profiling について紹介してみようと思います。機械学習をやっていると避けて通れないのが探索的データ解析ですが、かなり面倒な作業ですよね。ただ、pandas-profilign を使えばすごく簡単に探索的データ解析を行うことができます。

探索的データ解析とは

統計では、探索的データ分析は、データセットを分析して主な特性を要約するアプローチであり、多くの場合、統計グラフィックスやその他のデータ視覚化手法を使用します。(出典:Wikipedia)

pandas-profiling とは

Pandas のデータフレームを読み込んでそのデータに対して探索的データ解析を行ってくれるライブラリです。詳細は以下を参照してください。

github.com

必要なライブラリ

pandas と pandas-profiling をインストールしてください。

環境

Windows10 64bit
Python3.8.5

使用するデータ

2008年から2011年の間のポルトガルの銀行顧客の行動履歴、経済指標と、その顧客が実際に定期預金を申し込んだかどうかのデータを使用します。以下サイトの「 bank-additional-full.csv」を使用します。

archive.ics.uci.edu

開くとこんな感じになっています。このデータですが、「;」が区切り文字になっていたので「,」に変換しました。

f:id:sanvarie:20210508193647p:plain

サンプルコード

import pandas as pd
import pandas_profiling as pdp

df = pd.read_csv('bank-additional-full.csv')
pdp.ProfileReport(df, title="Pandas Profiling Report")

これだけです。めちゃめちゃ簡単ですね。

概要

f:id:sanvarie:20210508202738p:plain

各フィールドの情報

f:id:sanvarie:20210508202811p:plain

f:id:sanvarie:20210508202834p:plain

選択したカラムのヒートマップ

f:id:sanvarie:20210508203700p:plain

f:id:sanvarie:20210508203719p:plain

相関関係

f:id:sanvarie:20210508203152p:plain

サンプルデータ

f:id:sanvarie:20210508203251p:plain

さいごに

pandas-profiling を使うことでこんなに簡単に探索的データ解析ができることがわかりました。ぜひ使ってみてください。本日は以上です。