GIS奮闘記

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

スポンサーリンク

FIFA 20 のプレイヤーをクラスタリングしてみよう!

さて、本日は機械学習のクラスタリングという手法を使ってFIFA 20 のプレイヤーをグルーピングしてみようと思います。

FIFA20 とは

『FIFA 20』は、EAスポーツが開発し、エレクトロニック・アーツより2019年9月27日に世界同時発売されたサッカーゲーム。(出典:Wikipedia)

使用するデータデータ

Kaggle の「FIFA 20 complete player dataset」にある「players_20.csv」を使用します。

www.kaggle.com

Kaggle とは

Kaggleは企業や研究者がデータを投稿し、世界中の統計家やデータ分析家がその最適モデルを競い合う、予測モデリング及び分析手法関連プラットフォーム及びその運営会社である。 (出典:Wikipedia)

クラスタリングとは

(統計学)データ解析手法の1つ。「クラスタ解析」、「クラスター分析」とも。機械学習やデータマイニング、パターン認識、イメージ解析やバイオインフォマティックスなど多くの分野で用いられる(データ・クラスタリングを参照)。クラスタリングではデータの集合を部分集合(クラスタ)に切り分けて、それぞれの部分集合に含まれるデータが(理想的には)ある共通の特徴を持つようにする。この特徴は多くの場合、類似性や、ある定められた距離尺度に基づく近さで示される。(出典:Wikipedia)

使用するクラスタリング手法

k-meansクラスタリングを使用します。

関連エントリー

機械学習関連のエントリーです。興味のある方はぜひ読んでみてください。

www.gis-py.com

www.gis-py.com

www.gis-py.com

環境

Windows10 64bit
Python3.8.5

手順

  1. データ分析
  2. クラスタリング
  3. 可視化

1.データ分析

データ読込

プレイヤーのデータを読み込みます。

import pandas as pd
data = pd.read_csv(r"players_20.csv")
data.head()

なんと104カラムもありますね。

f:id:sanvarie:20210725054512p:plain

データ探索

カラムの型や基本統計量などを確認します。

data.shape

data.info()

data.describe()

f:id:sanvarie:20210725055033p:plain

カラム選定

クラスタリングを行うにあたって必要なカラムを選択します。また、フィールドプレイヤーとゴールキーパーによって使用するカラムを分けます。

################################################
# フィールドプレイヤーの分析に必要なカラムをセット #
################################################

data_field_players = data[data.player_positions != "GK"]
field_players_features = ['short_name','overall','potential', 'skill_moves',
                          'pace','shooting','passing','dribbling','defending','physic']

# 名前ありデータフレーム
data_field_players = data_field_players[field_players_features]                       

# 名前なしデータフレーム
data_field_players_drop = data_field_players.drop(columns = ['short_name'])

################################################
# ゴールキーパーの分析に必要なカラムをセット       #
################################################
data_goal_keepers = data[data.player_positions == "GK"]
goal_keepers_features = ['short_name','overall','potential', 'skill_moves',
                         'gk_diving','gk_handling','gk_kicking','gk_reflexes','gk_speed','gk_positioning']

# 名前ありデータフレーム
data_goal_keepers = data_goal_keepers[goal_keepers_features]

# 名前なしデータフレーム
data_goal_keepers_drop = data_goal_keepers.drop(columns = ['short_name'])

2.クラスタリング

まずはスケーリングをします。

スケーリング(フィールドプレイヤー)
# スケーリング(フィールドプレイヤー)
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
scaler.fit(data_field_players_drop)

data_field_players_drop_scaled = scaler.transform(data_field_players_drop)

field_players_df = pd.DataFrame(data_field_players_drop_scaled)

field_players_features.remove('short_name')

field_players_df.columns = field_players_features
field_players_df

f:id:sanvarie:20210725084637p:plain

スケーリング(ゴールキーパー)
# スケーリング(ゴールキーパー)
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
scaler.fit(data_goal_keepers_drop)

data_goal_keepers_drop_scaled = scaler.transform(data_goal_keepers_drop)

goal_keepers_df = pd.DataFrame(data_goal_keepers_drop_scaled)

goal_keepers_features.remove('short_name')

goal_keepers_df.columns = field_players_features
goal_keepers_df

f:id:sanvarie:20210725084703p:plain

エルボー法(フィールドプレイヤー)

エルボー法で最適なクラスター数を検討します。

import matplotlib.pyplot as plt
distortions = []
for i  in range(1,11):                
    km = KMeans(n_clusters=i,
                init='k-means++',     
                n_init=10,
                max_iter=16242,
                random_state=0)
    km.fit(field_players_df)                         
    distortions.append(km.inertia_)   

plt.plot(range(1,11),distortions,marker='o')
plt.xlabel('Number of clusters')
plt.ylabel('SSE')
plt.show()

「クラスター数3が最適なクラスター数」と判断します。

f:id:sanvarie:20210725084352p:plain

エルボー法(ゴールキーパー)
import matplotlib.pyplot as plt
distortions = []
for i  in range(1,11):                
    km = KMeans(n_clusters=i,
                init='k-means++',     
                n_init=10,
                max_iter=2036,
                random_state=0)
    km.fit(data_goal_keepers_drop)                         
    distortions.append(km.inertia_)   

plt.plot(range(1,11),distortions,marker='o')
plt.xlabel('Number of clusters')
plt.ylabel('Distortion')
plt.show()

こちらも「クラスター数3が最適なクラスター数」と判断します。

f:id:sanvarie:20210725084413p:plain

さて、いよいよクラスタリングですね。

クラスタリング(フィールドプレイヤー)
from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters = 3)
kmeans.fit(field_players_df)
result = kmeans.predict(field_players_df)

# 名前ありデータフレームにクラスタリングの結果を格納
data_field_players['cluster_id'] = result

# 名前なし、かつ、スケーリング済みのデータフレームにクラスタリングの結果を格納
field_players_df['cluster_id'] = result

field_players_df.head()

f:id:sanvarie:20210725065946p:plain

クラスタリング(ゴールキーパー)
from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters = 3)
kmeans.fit(goal_keepers_df)
result = kmeans.predict(goal_keepers_df)

# 名前ありデータフレームにクラスタリングの結果を格納
data_goal_keepers['cluster_id'] = result

# 名前なし、かつ、スケーリング済みのデータフレームにクラスタリングの結果を格納
goal_keepers_df['cluster_id'] = result

goal_keepers_df.head()

f:id:sanvarie:20210725093222p:plain

3.可視化

クラスタリングの結果を可視化します。

フィールドプレイヤー
# 可視化
import matplotlib.pyplot as plt

clusterinfo = pd.DataFrame()
for i in range(3):
    clusterinfo['cluster' + str(i)] = field_players_df[field_players_df['cluster_id'] == i].mean()
clusterinfo = clusterinfo.drop('cluster_id')
 
my_plot = clusterinfo.T.plot(kind='bar', stacked=True, title="Mean Value of 3 Clusters")
my_plot.set_xticklabels(my_plot.xaxis.get_majorticklabels(), rotation=0)
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0, fontsize=18)

結果が出ました。以下のような傾向があることがわかります。

  • cluster = 0 のプレイヤー:全体的に能力が高い。スタープレイヤーと考えられる。
  • cluster = 1 のプレイヤー:スピードとドリブル能力が高い。ウイングのようなタイプと考えられる。
  • cluster = 2 のプレイヤー:フィジカルとディフェンス能力が高い。ディフェンダーと考えられる。

f:id:sanvarie:20210725090022p:plain

それぞれのクラスターに属する選手です。cluster = 0 はメッシやロナウドなどが所属しており、スタープレイヤーのグループという推測はあってそうですね。

f:id:sanvarie:20210725090755p:plain

f:id:sanvarie:20210725090811p:plain

f:id:sanvarie:20210725090827p:plain

ゴールキーパー
# 可視化
import matplotlib.pyplot as plt

clusterinfo = pd.DataFrame()
for i in range(3):
    clusterinfo['cluster' + str(i)] = goal_keepers_df[goal_keepers_df['cluster_id'] == i].mean()
clusterinfo = clusterinfo.drop('cluster_id')
 
my_plot = clusterinfo.T.plot(kind='bar', stacked=True, title="Mean Value of 3 Clusters")
my_plot.set_xticklabels(my_plot.xaxis.get_majorticklabels(), rotation=0)
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0, fontsize=18)

結果が出ました。以下のような傾向があることがわかります。

  • cluster = 0 のプレイヤー:全体的に能力が低い。若手やレベルの低いリーグのキーパーと考えられる。
  • cluster = 1 のプレイヤー:全体的に能力が中くらい。中堅レベルのキーパーと考えられる。
  • cluster = 2 のプレイヤー:全体的に能力が高い。スタープレイヤーと考えられる。

f:id:sanvarie:20210725091224p:plain

それぞれのクラスターに属する選手です。知っている選手がいないのですが、能力を見る限り推測は正しそうな気がします。

f:id:sanvarie:20210725091419p:plain

f:id:sanvarie:20210725091440p:plain

f:id:sanvarie:20210725091456p:plain

さいごに

いかがでしたでしょうか?クラスタリングを使用するとデータセットの中から類似データごとにグループ分けをしてくれます。とても便利ですね。大量のデータの中から何かしらの法則を見つける方法はクラスタリング以外にもありますので、今後はそういったものも紹介していこうと思います。本日は以上です。

Amazon Location Service を使ってみよう!

さて、本日は Amazon Location Service を使用してみようと思います。とうとう AW が GIS の世界に進出してきましたね。サービス開始時からどんな感じかずっと気になっていたので今回紹介したいと思います。

Amazon Location Service とは

Amazon Location Service とは、デベロッパーがマップ、特定のポイント (POI)、ジオコーディング、ルーティング、トラッキング、ジオフェンシングなどの位置情報機能を、データセキュリティ、ユーザーのプライバシー、データ品質、コストを犠牲にすることなくアプリケーションに簡単に追加できるフルマネージドサービスです。(出典:AWS)

機能

  • Maps・・・開発するアプリにマップを表示したり、マップにデータを追加したりすることができます。
  • Place indexes・・・場所検索、ジオコーディング、逆ジオコーディングなどができます。
  • Route calculators・・・ルート検索ができます。
  • Geofence collections・・・ジオフェンスを使用することができます。
  • Trackers・・・デバイスのトラッキングなどができます。

f:id:sanvarie:20210723213418p:plain

今回試してみること

Amazon Location Service で作成したマップをブラウザ上で表示してみようと思います。

手順

  1. マップの作成
  2. AWS Cognito の設定
  3. マップの表示

1.マップの作成

Esri と HERE のマップが使えるみたいです。今回は Esri Light を使用してみます。

f:id:sanvarie:20210723213935p:plain

f:id:sanvarie:20210723214007p:plain

マップを作成するとコンソール上でマップを使うことができるようになります。

f:id:sanvarie:20210723214225p:plain

2.AWS Cognito の設定

マップの表示には Cognito Identity Pool ID というものをコード内にセットする必要があります。以下のようなサイトを参照し設定をしてください。

qiita.com

qiita.com

3.マップの表示

マップの表示をするためのサンプルコードです。コード内の以下の変数にそれぞれ値を設定してください。

  • identityPoolId ・・・AWS Cognito を設定し取得してください。ap-northeast-1:--***** のような形式です。
  • mapName ・・・作成したマップ名をセットしてください。
<!-- index.html -->
<html>
  <head>
    <link
      href="https://unpkg.com/maplibre-gl@1.14.0/dist/maplibre-gl.css"
      rel="stylesheet"
    />
    <style>
      body {
        margin: 0;
      }
 
      #map {
        height: 100vh;
      }
    </style>
  </head>
 
  <body>
    <!-- map container -->
    <div id="map" />
    <!-- JavaScript dependencies -->
    <script src="https://unpkg.com/maplibre-gl@1.14.0/dist/maplibre-gl.js"></script>
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.784.0.min.js"></script>
    <script src="https://unpkg.com/@aws-amplify/core@3.7.0/dist/aws-amplify-core.min.js"></script>
    <script>
      // use Signer from @aws-amplify/core
      const { Signer } = window.aws_amplify_core;
 
      // configuration
      const identityPoolId = "";
      const mapName = ""; // Amazon Location Service Map Name
 
      // extract the region from the Identity Pool ID
      AWS.config.region = identityPoolId.split(":")[0];
 
      // instantiate a Cognito-backed credential provider
      const credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: identityPoolId,
      });
 
      /**
       * Sign requests made by MapLibre GL JS using AWS SigV4.
       */
      function transformRequest(url, resourceType) {
        if (resourceType === "Style" && !url.includes("://")) {
          // resolve to an AWS URL
          url = `https://maps.geo.${AWS.config.region}.amazonaws.com/maps/v0/maps/${url}/style-descriptor`;
        }
 
        if (url.includes("amazonaws.com")) {
          // only sign AWS requests (with the signature as part of the query string)
          return {
            url: Signer.signUrl(url, {
              access_key: credentials.accessKeyId,
              secret_key: credentials.secretAccessKey,
              session_token: credentials.sessionToken,
            }),
          };
        }
 
        // don't sign
        return { url };
      }
 
      /**
       * Initialize a map.
       */
      async function initializeMap() {
        // load credentials and set them up to refresh
        await credentials.getPromise();
 
        // Initialize the map
        const map = new maplibregl.Map({
          container: "map",
          center: [139.716073, 35.562479], // initial map centerpoint
          zoom: 17, // initial map zoom
          style: mapName,
          transformRequest,
        });
 
        map.addControl(new maplibregl.NavigationControl(), "top-left");
      }
 
      initializeMap();
    </script>
  </body>
</html>

上手くマップが表示されました!初期起動時の場所は蒲田駅にしてみました。

f:id:sanvarie:20210723215007p:plain

さいごに

まだサービスが公開されてから日が浅いので機能はこれからどんどん充実してくるのではないかと思います。また、料金も他のサービスと比べると安いですしアプリ内でちょっと地図を使うくらいだったら Amazon Location Service がファーストチョイスになる日も近いかもしれませんね。次回以降のエントリーで Amazon Location Service を使用したジオコーディングの仕方なども紹介してみようと思います。本日は以上です。

Amazon Comprehend と Python で自然言語処理をしてみよう!

さて、本日は自然言語処理について書いてみようと思います。様々な自然言語処理サービスがありますが、今回は Amazon Comprehend を使ってみようと思います。このサービスを使うと簡単にテキストの感情分析などができるようになります。

Amazon Comprehendとは

Amazon Comprehend は、機械学習を使用して構造化されていないデータから情報を見つける自然言語処理 (NLP) サービスです。ドキュメントを詳細に調べる必要がなく、プロセスが簡単で、隠れている情報を容易に把握できます。(出典:AWS)

主な特徴

Amazon Comprehend には以下のような特徴があります。

キーフレーズ抽出

キーフレーズ抽出 API は、キーフレーズまたは会話のポイント、およびそれがキーフレーズであることを裏付ける信頼性スコアを返します。

感情分析

感情分析 API は、テキストの全体的な感情 (肯定的、否定的、中立的、または混在) を返します。

構文解析

Amazon Comprehend Syntax API を使用すれば、お客様は、トークン分割や品詞 (PoS) を使用してテキストを分析したり、テキスト内の名詞や形容詞などの単語境界やラベルを識別したりできます。

※2021/7/22で日本語未対応のため、本エントリーでは取り上げません。早く日本語対応になってほしいですね。

エンティティ認識

エンティティ認識 API は、提供されたテキストに基づいて自動的に分類される、名前付きエンティティ (「人」、「場所」、「位置」など) を返します。

言語検出

言語検出 API は、100 を超える言語で書かれたテキストを自動的に識別し、主要言語と、言語が主要であることを裏付ける信頼性スコアを返します。

関連エントリー

自然言語処理に関するエントリーです。興味のある方はぜひ読んでみてください。

www.gis-py.com

www.gis-py.com

使用するテキスト

富士通さんのTwitterアカウントのつぶやきを使用します。なぜ富士通さんかは秘密です。

f:id:sanvarie:20210722185101p:plain

テキスト内容

「明日は海の日ですね🌊⛵離れた場所から水中を体験することができる 水族館と教室を繋いだ #遠隔教育 をご紹介します✨5Gによる映像伝送や水中ドローンにVR技術などを活用し、校外学習に行かなくても子供たちはリアルタイムで #美ら海水族館 のサメを観察しました🙋」

環境

Windows10 64bit
Python3.6.6

サンプルコード

キーフレーズ抽出

import boto3

comprehend = boto3.client(service_name='comprehend', 
                                             region_name='ap-northeast-1',
                                             aws_access_key_id="***",
                                             aws_secret_access_key="***")

text = """明日は海の日ですね🌊⛵離れた場所から水中を体験することができる 水族館と教室を繋いだ 
          #遠隔教育 をご紹介します✨5Gによる映像伝送や水中ドローンにVR技術などを活用し、
          校外学習に行かなくても子供たちはリアルタイムで #美ら海水族館 のサメを観察しました🙋"""

result = comprehend.detect_key_phrases(Text = text, LanguageCode='ja')
for r in result["KeyPhrases"]:
    print(r)

このような結果になりました。スコアの高いキーフレーズを見ると確かにと思える結果ですね。

f:id:sanvarie:20210722190313p:plain

感情分析

import boto3

comprehend = boto3.client(service_name='comprehend', 
                                             region_name='ap-northeast-1',
                                             aws_access_key_id="***",
                                             aws_secret_access_key="***")

text = """明日は海の日ですね🌊⛵離れた場所から水中を体験することができる 水族館と教室を繋いだ 
          #遠隔教育 をご紹介します✨5Gによる映像伝送や水中ドローンにVR技術などを活用し、
          校外学習に行かなくても子供たちはリアルタイムで #美ら海水族館 のサメを観察しました🙋"""
 
result = comprehend.detect_sentiment(Text=text, LanguageCode='ja'),
for k, v in comprehend_result["SentimentScore"].items():
    print(f"    {k}: {v}")

今回選んだテキストは中立的な感情という結果になりました。

f:id:sanvarie:20210722190701p:plain

エンティティ認識

import boto3

comprehend = boto3.client(service_name='comprehend', 
                                             region_name='ap-northeast-1',
                                             aws_access_key_id="***",
                                             aws_secret_access_key="***")

text = """明日は海の日ですね🌊⛵離れた場所から水中を体験することができる 水族館と教室を繋いだ 
          #遠隔教育 をご紹介します✨5Gによる映像伝送や水中ドローンにVR技術などを活用し、
          校外学習に行かなくても子供たちはリアルタイムで #美ら海水族館 のサメを観察しました🙋"""

result = comprehend.detect_entities(Text=text, LanguageCode='ja')
for r in result["Entities"]:
    print(r)

以下のような単語た抽出されました。「ドローン」などもう少し多く抽出してほしいですね。

f:id:sanvarie:20210722204005p:plain

言語検出

import boto3

comprehend = boto3.client(service_name='comprehend', 
                                             region_name='ap-northeast-1',
                                             aws_access_key_id="***",
                                             aws_secret_access_key="***")

text = """明日は海の日ですね🌊⛵離れた場所から水中を体験することができる 水族館と教室を繋いだ 
          #遠隔教育 をご紹介します✨5Gによる映像伝送や水中ドローンにVR技術などを活用し、
          校外学習に行かなくても子供たちはリアルタイムで #美ら海水族館 のサメを観察しました🙋"""

result = comprehend.detect_dominant_language(Text = text)
print(result['Languages'])

日本語と判定されました。しかし、スコアは思ったよりも高くないですね。英文とかが混じった日本語とかの判断は難しそうですね。

f:id:sanvarie:20210722185330p:plain

さいごに

自分で自然言語処理などを開発しようとするとかなり大変だと思いますが、Amazon Comprehend を使えばそんな苦労をすることもなく簡単に実行できてしまいます。また、おそらく精度も相当高いと思われるのでテキストの感情分析などをしたいという案件が来た場合は開発するよりも Amazon Comprehend のようなサービスを使用するのがベターだと思います。本日は以上です。

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 を使ったデータ分析をしようと思います。本日は以上です。