GIS奮闘記

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

スポンサーリンク

Python で無相関検定をしてみる

さて、本日は Python を使って無相関検定をしてみようと思います。以下二つのエントリーで「相関係数の算出」と「データの散布図へのプロット」を実施しましたが、使用した二つの変数(築年数と販売価格)に対して無相関検定を実施します。

www.gis-py.com

www.gis-py.com

相関と仮説検定

データ分析を行なっていると、「たまたま」相関係数の値が大きくなってしまうことがよくあります。算出した相関係数が「たまたま」出たものなのか、それとも相関係数に有意性があるのかを確かめる手法が仮説検定です。今回は「無相関検定」というものを使ってみます。

無相関検定とは

「相関係数が r = 0である」ことを帰無仮説として採用して、「本当は相関が無いのに、たまたまデータに見られた相関係数の値が発生する確率」を p値として計算します。この値が事前に設定した有意水準を下回っていれば、対立仮説となる「相関係数が0ではない」という主張を支持することができるのが無相関検定です。

つまり、無相関検定は二つの変数間に相関が無いと仮定して、標本にみられる相関がどれだけ「たまたま」発生するかを評価します。

p値とは

統計的仮説検定において、帰無仮説の元で検定統計量がその値となる確率のこと。P値が小さいほど、検定統計量がその値となることはあまり起こりえないことを意味する。一般的にP値が5%または1%以下の場合に帰無仮説を偽として棄却し、対立仮説を採択する。(出典:統計WEB)

有意水準とは

有意水準は、検定において帰無仮説を設定したときにその帰無仮説を棄却する基準となる確率のことです。(アルファ)で表され、5%(0.05)や1%(0.01)といった値がよく使われます。有意水準は検定を行う前に設定しておきます。(出典:統計WEB)

本エントリーでは有意水準を5%(0.05)とします。

相関係数r と p 値のパターンとその解釈

  1. r の絶対値が大きく、p値も有意水準を下回っている場合
    二つの変数の間に何らかの関係があると解釈します。

  2. r の絶対値は小さいが、p値は有意水準を下回っている場合
    相関は存在すると考えられるが、関係の強さは弱いと解釈します。

  3. p値が有意水準を上回っている場合
    標本から得られた r の値は相関の無い変数の間からでもたまたま生じうる程度のものなので、相関関係があるかどうかはこのデータからは結論づけられないと解釈します。

使用するデータ

以下のエントリーで収集した SUUMO の中古物件情報を使います。

www.gis-py.com

相関関係を分析する属性

SUUMO の中古物件情報の以下の属性を使います。

  • 築年数
  • 販売価格

実行環境

macOS 12.3.1
Python 3.9.6

サンプルコード

「築年数」「販売価格」の二変数に対して無相関検定を実施するサンプルです。

import pandas as pd
from scipy.stats import pearsonr

df = pd.read_csv("property.csv")

# 築年数と販売価格で無相関検定を実施
age_of_building = df["築年数"].values
price = df["販売価格"].values

#相関係数とp値を算出
res = pearsonr(age_of_building, price)
r_value = res[0]
p_value = res[1]

print('相関係数:', r_value)
print('p値:', p_value)

p値が有意水準(今回は0.05)を上回っているため、帰無仮説は棄却せずに二つの変数に相関関係があるかどうかはこのデータからは結論づけられないと言えるかと思います。

さいごに

最近は GIS ではなく統計に関するエントリーが増えてきてしまっていますが、GIS の世界でも統計的な処理をする機会は少なくないかと思います。興味のある方は相関分析など統計的な処理にチャレンジしてみてください。

Python で相関分析に使うデータを散布図で可視化してみる

さて、本日は相関分析に使うデータを散布図で可視化してみようと思います。前回エントリーでは相関係数の計算を行いました。今回は相関係数算出に使ったデータを散布図で可視化してみようと思います。

散布図とは

散布図は、横軸と縦軸にそれぞれ別の量をとり、データが当てはまるところに点を打って示す(「プロットする」といいます。)グラフです。2つの量に関係があるかどうかをみるのに非常に便利なグラフです。(出典:なるほど統計学園)

使用するデータ

前回エントリーと同じく、以下のエントリーで収集した SUUMO の中古物件情報を使います。

www.gis-py.com

相関関係を分析する属性

SUUMO の中古物件情報の以下の属性を使います。

  • 築年数
  • 販売価格
二つの属性に相関関係がある場合

築年数が高くなれば販売価格が高くなり、築年数が低くなれば販売価格が低くなります。

使用するライブラリ

Pythonでグラフを作るためのライブラリである seaborn を使います。

作成する散布図

以下二つの散布図を作成します。

  • 散布図
  • 散布図+回帰線

実行環境

macOS 12.3.1
Python 3.9.6

サンプルコード

散布図

中古一戸建てとマンションで色分けし、プロットしたポイントに対してラベリングを行なっています。

import seaborn as sns
import pandas as pd
from matplotlib import pyplot as plt
import numpy as np
import japanize_matplotlib

jitter_value = 2

def label_point(x, y, val, ax):
    a = pd.concat({"x": jitter(x, jitter_value), "y": jitter(y, jitter_value), "val": val}, axis=1)
    for i, point in a.iterrows():
        ax.text(point["x"]+.02, point["y"], str(point["val"]))

def jitter(values, j):
    return values + np.random.normal(j, 0.1, values.shape)

# データ読み込み(数が多いのでデータを間引いて読み込む)
df_top = pd.read_csv("property.csv")[0:20]
df_bottom = pd.read_csv("property.csv")[3400:3420]
df_correlation = df_top.append(df_bottom)

# グラフサイズ設定
plt.figure(figsize = (20, 10))

# 散布図作成(ポイントが重ならないようにジッター処理を実施)
ax = sns.scatterplot(data = df_correlation, 
                     x = jitter(df_correlation["販売価格"], jitter_value), 
                     y = jitter(df_correlation["築年数"], jitter_value),
                     hue = "カテゴリ")

# 散布図にタイトルを設定
ax.set_title("SUUMO中古物件相関分析", pad = 8, fontsize = 20, color = 'blue')

# プロットにラベリング
label_point(df_correlation["販売価格"], 
            df_correlation["築年数"], 
            df_correlation["所在地"], 
            ax)

# 散布図を画像として保存(おまけ)
plt.savefig("test.png")

# 散布図表示
plt.show()

このように散布図が作成されました。

散布図+回帰線

lmplot というメソッドを使うことで「回帰モデルの傾向線」を可視化することができます。

import seaborn as sns
import pandas as pd
from matplotlib import pyplot as plt
import numpy as np
import japanize_matplotlib

# データ読み込み(数が多いのでデータを間引いて読み込む)
df_top = pd.read_csv("property.csv")[0:20]
df_bottom = pd.read_csv("property.csv")[3400:3420]
df_correlation = df_top.append(df_bottom)

# グラフサイズ設定
plt.figure(figsize = (20, 10))

ax = sns.lmplot(x = "販売価格", 
                y = "築年数",
                hue = "カテゴリ",
                data = df_correlation,
                x_jitter = 2,
                y_jitter = 2,
                height = 10,
                aspect = 2,
                legend = False)

ax.set(title = "SUUMO中古物件相関分析")
plt.legend(loc = 'upper right')

# プロットにラベリング
for ax in ax.axes.ravel():
    for t, x, y in df_correlation[['所在地', '販売価格', '築年数']].values.tolist():
        ax.annotate(t, (x, y))

# 散布図を画像として保存(おまけ)
plt.savefig("test.png")

# 散布図表示
plt.show()

傾向線付きの散布図が作成されました(どうしてもタイトルが切れてしまいますが、とりあえず作成はできました)。中古一戸建てに関しては販売価格と築年数は負の相関関係にありますが、中古マンションに関しては正の相関関係にあることがわかります。

さいごに

このような感じで散布図を作ることができます。相関分析を実施する際は相関係数の算出だけではなく散布図や傾向線の作成は必須かと思いますので、興味のある方はぜひ試してみてください。本日は以上です。

Python で相関係数を算出する方法

さて、本日は Python で相関係数を算出してみようと思います。今の業務で Tableau を使って相関係数を計算しているのですが、計算対象のデータパターンが多くて(相関係数を算出する対象が何十、何百パターンもある)、 Python で一括処理をしたいと思っているというのが背景にあります。

相関係数とは

相関係数とは、二つの変数の関係性の強さを示す数値である。変数とはたとえば人の体温や身長、体重、気温や株価など、刻々と変化する様々な数値のことである。特定の二つの変数の関係性の強さを表す場合に相関係数が用いられ、相関係数を導き出す手法が相関分析である。(出典:DIAMOND Chain Store 相関係数とは?相関係数を知ることで何がわかる?Excelを使った計算方法から、実例まで解説!)

正の相関と負の相関

相関係数は-1から1の値をとり1に近いほど二つの変数の相関関係が強いと言えます。(以下画像は「5分でわかる!「正の相関」「負の相関」と「相関係数」」より抜粋)

使用するデータ

以下のエントリーで収集した SUUMO の中古物件情報を使います。

www.gis-py.com

相関関係を分析する属性

SUUMO の中古物件情報の以下の属性を使います。

  • 築年数
  • 販売価格
二つの属性に相関関係がある場合

築年数が高くなれば販売価格が高くなり、築年数が低くなれば販売価格が低くなります。

実行環境

macOS 12.3.1
Python 3.9.6

サンプルコード

築年数と販売価格の相関係数を算出するサンプルコードです。

import csv
import pandas as pd

input_file = "property.csv"
attribute_list = []

with open(input_file, 'r', encoding='utf-8') as f:
    header = next(csv.reader(f))
    reader = csv.reader(f)

    price_list = []
    building_area_list = []
    for row in reader:
        
        # 販売価格を格納
        price_list.append(int(row[1]))

        # 築年数を格納
        building_area_list.append(int(row[12]))

price_ser = pd.Series(price_list)
building_area_ser = pd.Series(building_area_list)

# 相関係数を算出
res = price_ser.corr(building_area_ser)

print(res)

相関係数は約-0.32という結果になりました。そのため、築年数と販売価格には負の相関関係にあると言えます。

さいごに

大量のデータパターン(相関係数を算出する対象が何十、何百パターンもある場合)での相関分析をする際は Python での相関係数の算出が役に立ちそうですね。ちなみにですが、 GIS の世界にも空間相関分析というものがあるみたいですね。本日は以上です。

Python と気象庁の天気予報 JSON を使って全国の天気予報を取得してみる

さて、本日は気象庁の天気予報 JSON で遊んでみようと思います。気象庁のデータに関しては以下のエントリーでも扱いましたが、この時はスクレイピングを行いました。今回は公開されている JSON を活用して天気情報を取得してみたいと思います。

www.gis-py.com

気象庁天気予報 JSON とは

気象庁のWebサイトのリニューアルに伴い、天気予報情報がJSON形式で取得できるようになりました。正式公開の WebAPI ではないのですが、「政府標準利用規約に準拠して利用可能」なようです。

気象庁天気予報 JSON の種類

  1. エリアコード
  2. コンテンツ種別
  3. 各地の気象情報

今回は「3.各地の気象情報」を利用します。以下のようなURLにリクエストを投げるとエリアコードに応じた結果が取得できます。

# 東京の場合はhttps://www.jma.go.jp/bosai/forecast/data/forecast/130000.json
https://www.jma.go.jp/bosai/forecast/data/forecast/エリアコード.json 

東京のエリアコードでリクエストを投げると以下のような結果になります。東京の各エリアに関して三日間(以下の場合3/30,3/30,4/1)の天気情報を取得することができます。

[{'publishingOffice': '気象庁',
  'reportDatetime': '2022-03-30T17:00:00+09:00',
  'timeSeries': [{'timeDefines': ['2022-03-30T17:00:00+09:00',
     '2022-03-31T00:00:00+09:00',
     '2022-04-01T00:00:00+09:00'],
    'areas': [{'area': {'name': '東京地方', 'code': '130010'},
      'weatherCodes': ['201', '212', '201'],
      'weathers': ['くもり\u3000夜のはじめ頃\u3000晴れ',
       'くもり\u3000夜\u3000雨',
       'くもり\u3000時々\u3000晴れ'],
      'winds': ['南の風\u300023区西部\u3000では\u3000南の風\u3000やや強く',
       '南の風\u3000後\u3000北東の風\u300023区西部\u3000では\u3000後\u3000北東の風\u3000やや強く',
       '北の風'],
      'waves': ['1メートル\u3000後\u30000.5メートル',
       '0.5メートル\u3000後\u30001メートル',
       '1メートル']},
     {'area': {'name': '伊豆諸島北部', 'code': '130020'},
      'weatherCodes': ['200', '212', '313'],
      'weathers': ['くもり\u3000所により\u3000雨',
       'くもり\u3000夜遅く\u3000雨',
       '雨\u3000後\u3000くもり'],
      'winds': ['南の風\u3000後\u3000南西の風',
       '南西の風\u3000後\u3000やや強く',
       '北東の風\u3000やや強く'],
      'waves': ['2メートル', '2メートル', '2.5メートル']},
     {'area': {'name': '伊豆諸島南部', 'code': '130030'},
      'weatherCodes': ['200', '212', '313'],
      'weathers': ['くもり\u3000所により\u3000雨',
       'くもり\u3000夜遅く\u3000雨',
       '雨\u3000後\u3000くもり'],
      'winds': ['南の風', '西の風\u3000後\u3000やや強く', '北東の風\u3000やや強く'],
      'waves': ['2.5メートル\u3000ただし\u3000三宅島\u3000では\u30002メートル',
       '2メートル',
       '2.5メートル\u3000後\u30004メートル\u3000ただし\u3000三宅島\u3000では\u30002.5メートル\u3000後\u30003メートル']},
--- 省略 ---

今回のゴール

全国の気象庁天気予報 JSON の中から「天気」、「風」、「波」の情報を CSVとして出力(降水確率や気温などは今回は対象外としています)

実行環境

macOS 11.3.1
Python 3.9.6

サンプルコード

# -*- coding: utf-8 -*-
import requests
import json
import csv

# エリアコード
area_dic = {'北海道/釧路':'014100',
            '北海道/旭川':'012000',
            '北海道/札幌':'016000',
            '青森県':'020000',
            '岩手県':'030000',
            '宮城県':'040000',
            '秋田県':'050000',
            '山形県':'060000',
            '福島県':'070000',
            '茨城県':'080000',
            '栃木県':'090000',
            '群馬県':'100000',
            '埼玉県':'110000',
            '千葉県':'120000',
            '東京都':'130000',
            '神奈川県':'140000',
            '新潟県':'150000',
            '富山県':'160000',
            '石川県':'170000',
            '福井県':'180000',
            '山梨県':'190000',
            '長野県':'200000',
            '岐阜県':'210000',
            '静岡県':'220000',
            '愛知県':'230000',
            '三重県':'240000',
            '滋賀県':'250000',
            '京都府':'260000',
            '大阪府':'270000',
            '兵庫県':'280000',
            '奈良県':'290000',
            '和歌山県':'300000',
            '鳥取県':'310000',
            '島根県':'320000',
            '岡山県':'330000',
            '広島県':'340000',
            '山口県':'350000',
            '徳島県':'360000',
            '香川県':'370000',
            '愛媛県':'380000',
            '高知県':'390000',
            '福岡県':'400000',
            '佐賀県':'410000',
            '長崎県':'420000',
            '熊本県':'430000',
            '大分県':'440000',
            '宮崎県':'450000',
            '鹿児島県':'460100',
            '沖縄県/那覇':'471000',
            '沖縄県/石垣':'474000'
            }

# CSV出力先
output_file = "weather_report.csv"

# CSVヘッダー
header = ["都道府県","データ配信元","報告日時",
          "地方名","予報日時","天気","風","波"]

def main():
    make_csv()

def make_csv():
    with open(output_file, 'w', encoding='utf-8') as f:
        writer = csv.writer(f, lineterminator="\n")
        writer.writerow(header)

        # JSONから情報を取得
        write_lists = get_info()

        # CSV書き込み
        writer.writerows(write_lists)

def get_info():
    write_lists = []
    base_url = "https://www.jma.go.jp/bosai/forecast/data/forecast/"
    for k, v in area_dic.items():

        if k.find("/"):
            prefecture = k[0:k.find("/")]
        else:
            prefecture = k

        url = base_url + v + ".json"

        res = requests.get(url).json()

        for re in res:
            publishingOffice = re["publishingOffice"]
            reportDatetime = re["reportDatetime"]

            timeSeries = re["timeSeries"]

            for time in timeSeries:
                #降水確率など今回のターゲット以外は除外する
                if 'pops' in time["areas"][0]:
                    pass
                elif 'temps' in time["areas"][0]:
                    pass
                elif 'tempsMax' in time["areas"][0]:
                    pass
                else:
                    for i in range(len(time["areas"])):

                        local_name = time["areas"][i]["area"]["name"]

                        for j in range(len(timeSeries[0]["timeDefines"])):

                            if 'weathers' not in time["areas"][i]:
                                weather = ""
                            else:
                                weather = time["areas"][i]["weathers"][j]

                            if 'winds' not in time["areas"][i]:
                                wind = ""
                            else:
                                wind = time["areas"][i]["winds"][j]

                            if 'waves' not in time["areas"][i]:
                                wave = ""
                            else:
                                wave = time["areas"][i]["waves"][j]

                            timeDefine = time["timeDefines"][j]

                            # 各情報をリストに格納
                            write_list = []
                            write_list.append(prefecture)
                            write_list.append(publishingOffice)
                            write_list.append(reportDatetime)
                            write_list.append(local_name)
                            write_list.append(timeDefine)
                            write_list.append(weather)
                            write_list.append(wind)
                            write_list.append(wave)

                            write_lists.append(write_list)
    return write_lists

if __name__ == '__main__':
    main()

結果は以下のようになりました。今回のゴールである「天気」、「風」、「波」の情報の取得に成功しました。 f:id:sanvarie:20220331152133p:plain

さいごに

このJSONを使えば簡単に天気予報情報を取得できることがわかりました。今回は省略しましたが、気温や降水確率なども取得できるので興味のある方はぜひチャレンジしてみてください。本日は以上です。

Python で気象庁防災情報XMLの地震情報をCSV出力する方法

さて、本日は気象庁防災情報XMLの地震情報をCSV出力しようと思います。

気象庁防災情報XMLとは

気象庁は過去長年にわたり、それぞれの防災情報毎に情報の性質・利用形態などを考慮し、気象庁独自の電文形式(フォーマット)を作成してきました。この方式は、防災情報の種類が少なく、情報の伝達がFAXや低速の通信回線の時代はそれぞれの情報に適していましたが、高度にICT化された社会において、より詳細で高度化された防災情報をより効果的に活用していただくために、新たな防災情報の提供様式を検討すべきと考え、「気象庁防災情報XMLフォーマット」を策定することとし、平成23年5月12日より運用を開始しました。(出典:気象庁

気象庁防災情報XMLの仕様について

気象庁のサイトにまとまっているのでこちらをご参照ください。

また、地震情報に関してはこちらにもまとまっています。

今回取得するデータ

こちらのサイトから地震情報を取得します。(今回は長期フィードを使用)

f:id:sanvarie:20220320070157p:plain

この中には火山情報も入っており、そこを抜いて地震情報のみを取得します。以下画像の赤枠が地震情報となっており、同じようにURLに「VXSE」を含んだXMLから地震情報を取得したいと思います。

f:id:sanvarie:20220321101929p:plain

取得する項目

さまざまな項目があるのですが、今回は重要そうな以下の項目を取得しようと思います。

  • イベントID
  • 地震発生場所
  • 地震発生時刻
  • 観測点で地震を検知した時刻
  • 緯度
  • 経度
  • マグニチュード
  • 県名称
  • 市町村名称
  • 最大震度
  • 震度観測点の情報(一つの地震に対して複数の情報があるため、セル内で半角スペースで区切ります)

実行環境

macOS 11.3.1
Python 3.9.6

サンプルコード

気象庁防災情報XMLから地震情報を取得するサンプルです。

# -*- coding: utf-8 -*-
import re
import csv
import urllib.request
from bs4 import BeautifulSoup

# CSV出力先
output_file = "eathquake.csv"

# CSVヘッダー
header = ["イベントID","地震発生場所","地震発生時刻","観測点で地震を検知した時刻","緯度","経度",
          "マグニチュード","県名称","市町村名称","最大震度","震度観測点の情報"]

with open(output_file, 'w', encoding='utf-8') as f:
    writer = csv.writer(f, lineterminator="\n")
    writer.writerow(header)

    # 対象URLにリクエストを送る
    url = "https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml"
    res = urllib.request.urlopen(url).read()
    soup = BeautifulSoup(res)

    # 地震に関する情報のみを抽出
    search = re.compile('.*VXSE.*')

    for vxse in soup.find_all(text=search):

        # 対象URLにリクエストを送る
        res = urllib.request.urlopen(vxse).read()
        soup = BeautifulSoup(res)

        # イベントIDを取得
        event_id = soup.find("eventid").text

        for content in soup.find_all("body"):
            earthquake = content.find("earthquake")

            if earthquake is not None: # earthquakeの情報がない地震情報は対象外とする
                # 地震の各種情報を取得
                location = earthquake.find("name").text # 地震発生場所
                origintime = earthquake.find("origintime").text # 地震の発生した時刻
                arrivaltime = earthquake.find("arrivaltime").text # 観測点で地震を検知した時刻
                
                coordinate = earthquake.find("jmx_eb:coordinate").text # "北緯37.8度 東経141.6度 深さ 50km" +37.8+141.6-50000
                lat = coordinate[1:coordinate[1:].find("+") + 1]
                lon = coordinate[coordinate[1:].find("+") + 2:coordinate[1:].find("+") + 7]
                lat = int(lat[0:2])+(int(lat[3:])/60)+(0/3600)
                lon = int(lon[0:3])+(int(lon[4:])/60)+(0/3600)
                magnitude = earthquake.find("jmx_eb:magnitude").text 

            intensity = content.find("intensity")
            if intensity is not None: # intensityの情報がない地震情報は対象外とする:
                for pref in intensity.find_all("pref"):
                    
                    prefectuer = pref("name")[0].text                  

                    for city in pref.find_all("city"):
                        city_name = city("name")[0].text
                        maxint = city("maxint")[0].text #地震情報(震源・震度に関する情報)の各市町村での震度観測点の最大震度。

                        write_list = []
                        write_list.append(event_id)
                        write_list.append(location)
                        write_list.append(origintime)
                        write_list.append(arrivaltime)
                        write_list.append(lat)
                        write_list.append(lon)
                        write_list.append(magnitude)
                        write_list.append(prefectuer)
                        write_list.append(city_name)
                        write_list.append(maxint)

                        intensity_station = ""
                        for intensitystation in pref.find_all("intensitystation"): #地震情報(震源・震度に関する情報)の震度観測点の情報 *印は気象庁以外の震度観測点についての情報
                            intensity_station = intensity_station + intensitystation("name")[0].text + " " # 半角スペースで区切る
                        
                        write_list.append(intensity_station)

                        # CSV書き込み
                        writer.writerow(write_list)

出力結果は以下のようになりました。 f:id:sanvarie:20220321102555p:plain

さいごに

いかがでしたでしょうか?位置情報を含んでいるので出力結果を可視化してみてもいいかもしれないですね。また、気象庁防災情報XMLは地震情報以外にも様々な情報を提供しているので今後は他の情報でも遊んでみようと思います。本日は以上です。

Elasticsearch と Python を使って空間検索をしてみる

皆様、お久しぶりです。前回のエントリーから間が空いてしまいましたが、また少しずつブログの更新をしていきたいと思っています。今回は Elasticsearch と Python を使って空間検索をしてみようと思います。

以前にも Elasticsearch に関して以下のエントリーを書いているのでぜひ読んでみてください。

www.gis-py.com

なお、Elasticsearch のインストールや設定などについては本エントリーでは割愛しています。

実行環境

macOS 11.3.1
Python 3.9.6
Elasticsearch 7.17.0
elasticsearch 8.0.0(Elasticsearch の Python クライアント)

手順

  1. インデックスの作成
  2. 位置情報データを Elasticsearch に格納
  3. 空間検索

1.インデックスの作成

Elasticsearch 上にインデックスを作成します。詳細については以下をご参照ください。

www.elastic.co

PUT culture_sports_park
{
  "mappings": {
    "properties": {
      "location": {
        "type": "geo_point"
      }
    }
  }
}

2.位置情報データを Elasticsearch に格納

今回は「東京都オープンデータカタログサイト」から文化・スポーツ施設・公園の geojsonデータをダウンロードしてみたいと思います。

catalog.data.metro.tokyo.lg.jp

Elasticsearch の Python クライアント である elasticsearch を使って以下のようにデータを格納します。

import geojson
from elasticsearch import Elasticsearch, helpers

with open("culture_sports_park.geojson") as f:
    gj = geojson.load(f)

    es = Elasticsearch("http://localhost:9200")

    k = ({
        "_index": "culture_sports_park",
        "_source": {
            "name": feature["properties"]["施設名称"],
            "address": feature["properties"]["所在地"],
            "note": feature["properties"]["備考"],
            "location": {
                "lat":feature["geometry"]["coordinates"][1],
                "lon": feature["geometry"]["coordinates"][0]
            }
        }

    } for feature in gj['features'])

    helpers.bulk(es, k)

以下のコマンドでデータが格納されているか確認します。

GET culture_sports_park/_search

ばっちりですね。

f:id:sanvarie:20220314075335p:plain

これを Kibana 上で可視化してみると以下のようになります。

f:id:sanvarie:20220314075904p:plain

3.空間検索

蒲田駅から半径5kmにある文化・スポーツ施設・公園を検索してみます。

from elasticsearch import Elasticsearch

es = Elasticsearch("http://localhost:9200")

# 蒲田駅から半径5kmにあるポイントを検索
query = {
  "query": {
    "geo_distance": {
      "distance": "5km",
      "location": {
        "lat": 35.562479,
        "lon": 139.716073
      }
    }
  }
}

# 検索
result = es.search(index="culture_sports_park", body=query, size=100)

# 結果表示
for document in result["hits"]["hits"]:
    print(document["_source"])

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

{'name': '旗の台文化センター', 'address': '東京都品川区旗の台5-19-5', 'note': '所在地:旗の台5-19-5電話:3786-5191 施設内容:第1〜2会議室 レクリエーションホール スポーツ室 グループ室 休館日:年末年始・毎月第4日曜日', 'location': {'lat': 35.603845, 'lon': 139.700253}}
{'name': '南大井文化センター', 'address': '東京都品川区南大井1-12-6', 'note': '所在地:南大井1-12-6 電話:3764-6511 施設内容:第1〜3講習室\u3000美術工芸室\u3000レクリエーションホール\u3000スポーツ室\u3000託児室\u3000グループ室 休館日:年末年始・毎月第4日曜日', 'location': {'lat': 35.596235, 'lon': 139.737582}}
{'name': '南大井図書館', 'address': '東京都品川区南大井3-7-13保育園等と併設', 'note': '品川区南大井3-7-13併設南大井児童センター南大井保育園南大井シルバーセンター', 'location': {'lat': 35.591586, 'lon': 139.734398}}
{'name': '源氏前図書館', 'address': '東京都品川区中延4-14-17保育園と併設', 'note': '品川区中延4-14-17併設源氏前保育園', 'location': {'lat': 35.605478, 'lon': 139.709904}}
{'name': '大井図書館', 'address': '東京都品川区大井5-19-14滝王子児童センターと併設', 'note': '品川区大井5-19-14併設滝王子児童センター', 'location': {'lat': 35.598196, 'lon': 139.729134}}
{'name': '品川歴史館', 'address': '東京都品川区大井6-11-1', 'note': '品川区大井6-11-1電話:3777-4060 交通:大井町駅から東急バス「鹿島神社前」下車徒歩1分、JR大森駅山王北口下車徒歩10分 開館時間:午前9時〜午後5時(入館は4時30分まで) 休館日:年末年始、月曜日および祝日', 'location': {'lat': 35.596791, 'lon': 139.730102}}
{'name': 'メイプルセンター', 'address': '東京都品川区西大井1-4-25', 'note': '(公財)品川文化振興事業団が運営するカルチャーセンター。 多様な講座を開催。所在地:西大井1-4-25電話:3774-5050 交通:JR横須賀線西大井駅 休館日:毎月第4日曜日、年末年始 開館時間:午前9時〜午後9時30分(土・日・祝日は午後5時まで)', 'location': {'lat': 35.602061, 'lon': 139.722429}}
{'name': 'しながわ水族館', 'address': '東京都品川区勝島3-2-1しながわ区民公園内', 'note': '品川区勝島3-2-1しながわ区民公園内電話:3762-3431 交通: JR大井町駅から無料バス開館時間: 午前10時〜午後5時(入館は午後4時30分まで) 休館日: 毎週火曜日、1月1日 *春休み、ゴールデンウィーク、夏休みは営業', 'location': {'lat': 35.588634, 'lon': 139.737364}}
{'name': '大井ふ頭中央海浜公園野球場', 'address': '東京都品川区八潮4-1-19', 'note': '品川区八潮4-1-19', 'location': {'lat': 35.590939, 'lon': 139.752439}}
{'name': 'しながわ区民公園野球場', 'address': '東京都品川区勝島3-2-2', 'note': '品川区勝島3-2-2 京浜急行\u3000立会川駅、大森海岸駅から徒歩約5分公園内には、しながわ水族館、野球場、庭球場、ディキャンプ場、プール(夏季)あり。', 'location': {'lat': 35.594479, 'lon': 139.740205}}
{'name': 'しながわ区民公園庭球場', 'address': '東京都品川区勝島3-2-2', 'note': '品川区勝島3-2-2 野球場、庭球場ディキャンプ場あり', 'location': {'lat': 35.593656, 'lon': 139.739353}}
{'name': '大井ふ頭中央海浜公園', 'address': '東京都品川区八潮4-1-19', 'note': '品川区八潮4-1-19', 'location': {'lat': 35.592503, 'lon': 139.752289}}
{'name': '大井ふ頭中央海浜公園陸上競技場・球技場', 'address': '東京都品川区八潮4-1-19', 'note': '品川区八潮4-1-19', 'location': {'lat': 35.594031, 'lon': 139.753818}}
{'name': 'しながわ区民公園デイキャンプ場', 'address': '東京都品川区勝島3-2-2', 'note': '品川区勝島3-2-2 野球場、テニス場ディキャンプ場あり', 'location': {'lat': 35.592863, 'lon': 139.738629}}
{'name': '大井海岸公園', 'address': '東京都品川区南大井3-27-5', 'note': '品川区南大井3-27-5', 'location': {'lat': 35.588463, 'lon': 139.733647}}
{'name': '大井水神公園', 'address': '東京都品川区南大井5-16-1', 'note': '品川区南大井5-16-1、南大井6-14-2、南大井6-15-2', 'location': {'lat': 35.595355, 'lon': 139.732493}}
{'name': '鈴ケ森公園', 'address': '東京都品川区南大井4-18-14', 'note': '品川区南大井4-18-14', 'location': {'lat': 35.59351, 'lon': 139.734935}}
{'name': '大井中央公園', 'address': '東京都品川区大井1-46-8', 'note': '品川区大井1-46-8', 'location': {'lat': 35.604517, 'lon': 139.732352}}
{'name': '大井坂下公園', 'address': '東京都品川区南大井6-23-11', 'note': '品川区南大井6-23-11', 'location': {'lat': 35.589516, 'lon': 139.730786}}
{'name': '金子山公園', 'address': '東京都品川区西大井4-11-6', 'note': '品川区西大井4-11-6', 'location': {'lat': 35.59583, 'lon': 139.720733}}
{'name': '新浜川公園', 'address': '東京都品川区東大井2-26-18', 'note': '品川区東大井2-26-18', 'location': {'lat': 35.597859, 'lon': 139.740928}}
{'name': 'わかくさ公園', 'address': '東京都品川区勝島1-6-1', 'note': '品川区勝島1-6-1', 'location': {'lat': 35.598043, 'lon': 139.743905}}
{'name': '原っぱ公園', 'address': '東京都品川区西大井6-1-14', 'note': '品川区西大井6-1-14', 'location': {'lat': 35.601099, 'lon': 139.714796}}
{'name': '宮下公園', 'address': '東京都品川区大井2-7-16', 'note': '品川区大井2-7-16', 'location': {'lat': 35.605213, 'lon': 139.728399}}
{'name': '浜川公園', 'address': '東京都品川区南大井4-8-22', 'note': '品川区南大井4-8-22', 'location': {'lat': 35.597232, 'lon': 139.735757}}
{'name': '滝王子公園', 'address': '東京都品川区大井5-19-5', 'note': '品川区大井5-19-5', 'location': {'lat': 35.598389, 'lon': 139.728806}}
{'name': '出石公園', 'address': '東京都品川区西大井3-16-27', 'note': '品川区西大井3-16-27', 'location': {'lat': 35.594866, 'lon': 139.722095}}
{'name': '浜川北公園', 'address': '東京都品川区東大井3-26-6', 'note': '品川区東大井3-26-6', 'location': {'lat': 35.599143, 'lon': 139.737533}}
{'name': '桐畑公園', 'address': '東京都品川区南大井6-1-17', 'note': '品川区南大井6-1-17', 'location': {'lat': 35.593052, 'lon': 139.73167}}
{'name': '西の森公園', 'address': '東京都品川区西大井4-2-1', 'note': '品川区西大井4-2-1', 'location': {'lat': 35.597725, 'lon': 139.721316}}
{'name': '関ケ原公園', 'address': '東京都品川区東大井6-12-21', 'note': '品川区東大井6-12-21', 'location': {'lat': 35.602174, 'lon': 139.735626}}
{'name': '倉田公園', 'address': '東京都品川区大井4-29-23', 'note': '品川区大井4-29-23', 'location': {'lat': 35.59851, 'lon': 139.73236}}
{'name': '西大井広場公園', 'address': '東京都品川区西大井1-4-10', 'note': '品川区西大井1-4-10、二葉2-19-7', 'location': {'lat': 35.602258, 'lon': 139.723426}}
{'name': '元芝公園', 'address': '東京都品川区東大井3-7-18', 'note': '品川区東大井3-7-18', 'location': {'lat': 35.602744, 'lon': 139.739034}}
{'name': '西大井緑地公園', 'address': '東京都品川区西大井6-10-16', 'note': '品川区西大井6-10-16', 'location': {'lat': 35.600333, 'lon': 139.720565}}
{'name': 'しおじ公園', 'address': '東京都品川区八潮5-8-1', 'note': '品川区八潮5-8-1、八潮5-6-9', 'location': {'lat': 35.596166, 'lon': 139.750656}}
{'name': '大井鹿島公園', 'address': '東京都品川区大井6-8-2', 'note': '品川区大井6-8-2', 'location': {'lat': 35.596954, 'lon': 139.732443}}
{'name': '谷垂公園', 'address': '東京都品川区西大井6-13-10', 'note': '品川区西大井6-13-10', 'location': {'lat': 35.599954, 'lon': 139.717892}}
{'name': 'やまなか公園', 'address': '東京都品川区大井3-22-3', 'note': '品川区大井3-22-3', 'location': {'lat': 35.601039, 'lon': 139.727831}}
{'name': '大森貝塚遺跡庭園', 'address': '東京都品川区大井6-21-6', 'note': '品川区大井6-21-6', 'location': {'lat': 35.593454, 'lon': 139.729945}}
{'name': '旗の台公園', 'address': '東京都品川区旗の台5-19-9', 'note': '品川区旗の台5-19-9', 'location': {'lat': 35.604026, 'lon': 139.700415}}
{'name': '旗の台南公園', 'address': '東京都品川区旗の台5-11-11', 'note': '品川区旗の台5-11-11', 'location': {'lat': 35.603715, 'lon': 139.70271}}
{'name': '東中延公園', 'address': '東京都品川区東中延2-10-2', 'note': '品川区東中延2-10-2', 'location': {'lat': 35.606891, 'lon': 139.712651}}
{'name': '荏原町公園', 'address': '東京都品川区中延5-14-5', 'note': '品川区中延5-14-5', 'location': {'lat': 35.602276, 'lon': 139.708936}}
{'name': '大原公園', 'address': '東京都品川区戸越6-14-1', 'note': '品川区戸越6-14-1', 'location': {'lat': 35.60706, 'lon': 139.71428}}
{'name': '豊町公園', 'address': '東京都品川区豊町6-16-3', 'note': '品川区豊町6-16-3', 'location': {'lat': 35.604645, 'lon': 139.716402}}
{'name': '二葉公園', 'address': '東京都品川区二葉4-13-5', 'note': '品川区二葉4-13-5', 'location': {'lat': 35.603668, 'lon': 139.718124}}
{'name': '旗の台北公園', 'address': '東京都品川区旗の台3-10-15', 'note': '品川区旗の台3-10-15', 'location': {'lat': 35.605695, 'lon': 139.704672}}
{'name': '源氏前公園', 'address': '東京都品川区中延6-4-15', 'note': '中延 6-4-15', 'location': {'lat': 35.602198, 'lon': 139.712407}}
{'name': '旗の台なか公園', 'address': '東京都品川区旗の台3-9-12', 'note': '品川区旗の台3-9-12', 'location': {'lat': 35.604708, 'lon': 139.705797}}
{'name': '戸越南公園', 'address': '東京都品川区戸越6-8-8', 'note': '品川区戸越6-8-8', 'location': {'lat': 35.607305, 'lon': 139.718587}}
{'name': '旗の台広場公園', 'address': '東京都品川区旗の台3-1-5', 'note': '品川区旗の台3-1-5', 'location': {'lat': 35.606235, 'lon': 139.705977}}
{'name': '二鳳公園', 'address': '東京都品川区豊町4-19-20', 'note': '品川区豊町4-19-20', 'location': {'lat': 35.605414, 'lon': 139.721282}}
{'name': '鹿島庚塚児童遊園', 'address': '東京都品川区大井7-29-11', 'note': '品川区大井7-29-11', 'location': {'lat': 35.59392, 'lon': 139.72937}}
{'name': '富士見ケ丘児童遊園', 'address': '東京都品川区西大井5-7', 'note': '品川区西大井5-7-3', 'location': {'lat': 35.599427, 'lon': 139.716358}}
{'name': '北浜川児童遊園', 'address': '東京都品川区東大井2-25-22', 'note': '品川区東大井2-25-22', 'location': {'lat': 35.597968, 'lon': 139.739288}}
{'name': '出石児童遊園', 'address': '東京都品川区西大井3-1-5', 'note': '品川区西大井3-1-5', 'location': {'lat': 35.597804, 'lon': 139.723842}}
{'name': '関ケ原児童遊園', 'address': '東京都品川区南大井5-2-15', 'note': '品川区南大井5-2-15', 'location': {'lat': 35.600409, 'lon': 139.734917}}
{'name': '東大井三丁目児童遊園', 'address': '東京都品川区東大井3-29-6', 'note': '品川区東大井3-29-6', 'location': {'lat': 35.598928, 'lon': 139.737048}}
{'name': '西大井ちびっこ児童遊園', 'address': '東京都品川区西大井2-24-9', 'note': '品川区西大井2-24-9', 'location': {'lat': 35.597834, 'lon': 139.72503}}
{'name': '作守児童遊園', 'address': '東京都品川区大井4-12-14', 'note': '品川区大井4-12-14', 'location': {'lat': 35.601587, 'lon': 139.733211}}
{'name': '桜橋児童遊園', 'address': '東京都品川区東大井3-22-1', 'note': '品川区東大井3-22-1', 'location': {'lat': 35.599858, 'lon': 139.735899}}
{'name': '西大井二丁目児童遊園', 'address': '東京都品川区西大井2-22-7', 'note': '品川区西大井2-22-7', 'location': {'lat': 35.598816, 'lon': 139.724691}}
{'name': '大井倉田児童遊園', 'address': '東京都品川区大井4-22-30', 'note': '品川区大井4-22-30', 'location': {'lat': 35.600254, 'lon': 139.732213}}
{'name': '鈴ケ森道路児童遊園', 'address': '東京都品川区南大井1-22-1', 'note': '品川区南大井1-22-1', 'location': {'lat': 35.594342, 'lon': 139.736671}}
{'name': 'みなみ児童遊園', 'address': '東京都品川区南大井1-13-8', 'note': '品川区南大井1-13-8、南大井1-12-9', 'location': {'lat': 35.595655, 'lon': 139.737085}}
{'name': '旗岡児童遊園', 'address': '東京都品川区旗の台3-6-12', 'note': '品川区旗の台3-6-12', 'location': {'lat': 35.604515, 'lon': 139.708511}}
{'name': '上神明児童遊園', 'address': '東京都品川区二葉4-26-11', 'note': '品川区二葉4-26-11、二葉4-3-15', 'location': {'lat': 35.602209, 'lon': 139.714168}}
{'name': '豊町5丁目児童遊園', 'address': '東京都品川区豊町5-14-3', 'note': '品川区豊町5-14-3', 'location': {'lat': 35.604875, 'lon': 139.720592}}
{'name': '立会川児童遊園(その4)', 'address': '東京都品川区旗の台3-7', 'note': '品川区旗の台3-7地先、旗の台3-7-3-9地先', 'location': {'lat': 35.604264, 'lon': 139.706422}}
{'name': '立会川児童遊園(その2)', 'address': '東京都品川区旗の台2-7', 'note': '品川区旗の台2-7地先、旗の台2-7-2-1地先', 'location': {'lat': 35.606224, 'lon': 139.704371}}
{'name': '立会川児童遊園(その3)', 'address': '東京都品川区旗の台3-11', 'note': '品川区旗の台3-11地先、旗の台3-11-3-10地先', 'location': {'lat': 35.60551, 'lon': 139.704621}}
{'name': '豊町児童遊園', 'address': '東京都品川区豊町4-1-12', 'note': '品川区豊町4-1-12', 'location': {'lat': 35.60722, 'lon': 139.7192}}
{'name': '伊藤児童遊園', 'address': '東京都品川区西大井6-17-9', 'note': '品川区西大井6-17-9', 'location': {'lat': 35.600127, 'lon': 139.715824}}
{'name': '京浜運河緑道公園', 'address': '東京都品川区八潮1', 'note': '品川区八潮1、八潮5', 'location': {'lat': 35.597029, 'lon': 139.748997}}
{'name': 'しながわ区民公園屋外プール', 'address': '東京都品川区勝島3-2-2', 'note': '野球場、庭球場ディキャンプ場あり', 'location': {'lat': 35.593391, 'lon': 139.739778}}
{'name': 'しながわ区民公園', 'address': '東京都品川区勝島3-2-2', 'note': '品川区南大井2-3-17品川区勝島3-2-2', 'location': {'lat': 35.592575, 'lon': 139.738882}}
{'name': '森前公園', 'address': '東京都品川区西大井1-1-10', 'note': '品川区西大井1-1-10', 'location': {'lat': 35.600512, 'lon': 139.721299}}
{'name': '弁天通り公園', 'address': '東京都品川区中延5-3-8', 'note': '品川区中延5-3-8', 'location': {'lat': 35.603752, 'lon': 139.708745}}
{'name': '庚申公園', 'address': '東京都品川区中延5-13-17', 'note': '品川区中延5-13-17', 'location': {'lat': 35.60169, 'lon': 139.708685}}
{'name': '都立京浜運河緑道公園', 'address': '東京都品川区八潮1-', 'note': '品川区八潮1、八潮5', 'location': {'lat': 35.596782, 'lon': 139.748972}}
{'name': '都立大井ふ頭中央海浜公園', 'address': '東京都品川区八潮4-1-19', 'note': '東京都品川区八潮4-1-19', 'location': {'lat': 35.592501, 'lon': 139.752291}}
{'name': '旗の台東広場', 'address': '東京都品川区旗の台4-12-16', 'note': '品川区旗の台4-12-16', 'location': {'lat': 35.60214, 'lon': 139.70387}}
{'name': 'ごこう公園', 'address': '東京都品川区中延5-9-23', 'note': '品川区中延5-9-23区へ寄付された用地を公園として整備。周辺に花壇を施したシンプルなつくり。', 'location': {'lat': 35.602062, 'lon': 139.706797}}
{'name': 'ゆたか南公園', 'address': '東京都品川区豊町6-29-2', 'note': '品川区豊町6-29-2住宅の密集著しい地区の中にあるため、広場スペースが広く、防災機能を多く取り入れている。', 'location': {'lat': 35.604056, 'lon': 139.716339}}
{'name': '西大井四丁目特定児童遊園', 'address': '東京都品川区西大井4-23-12', 'note': '品川区西大井4-23-12', 'location': {'lat': 35.592907, 'lon': 139.719381}}
{'name': '南大井四丁目特定児童遊園', 'address': '東京都品川区南大井4-6', 'note': '品川区南大井4-6-20', 'location': {'lat': 35.596968, 'lon': 139.736822}}
{'name': '源氏前特定児童遊園', 'address': '東京都品川区中延6-4', 'note': '品川区中延6-4-8', 'location': {'lat': 35.602272, 'lon': 139.712272}}
{'name': '大井二丁目特定児童遊園', 'address': '東京都品川区大井2-5-15', 'note': '品川区大井2-5-15', 'location': {'lat': 35.604237, 'lon': 139.727564}}
{'name': '西大井六丁目特定児童遊園', 'address': '東京都品川区西大井6-5-5', 'note': '西大井6-5-5', 'location': {'lat': 35.60112, 'lon': 139.718687}}
{'name': '西大井三丁目特定児童遊園', 'address': '東京都品川区西大井3-4-14', 'note': '品川区西大井3-4-14', 'location': {'lat': 35.596942, 'lon': 139.723904}}
{'name': '豊四中央防災広場', 'address': '東京都品川区豊町4-17-1', 'note': '品川区豊町4-17-1', 'location': {'lat': 35.606713, 'lon': 139.720686}}
{'name': 'しながわ花海道水辺広場', 'address': '東京都品川区東大井1-13', 'note': '品川区東大井1-13先外勝島運河周辺の防潮堤上部。', 'location': {'lat': 35.598598, 'lon': 139.741246}}
{'name': '上蛇広場', 'address': '東京都品川区二葉4-3-10', 'note': '二葉4-3-10', 'location': {'lat': 35.602658, 'lon': 139.714435}}
{'name': 'ゆたか防災広場', 'address': '東京都品川区豊町6-11-1', 'note': '豊町6-11-1', 'location': {'lat': 35.605595, 'lon': 139.717657}}
{'name': '西大井六丁目ふれあい広場', 'address': '東京都品川区西大井6-3-1', 'note': '西大井6-3-1', 'location': {'lat': 35.601176, 'lon': 139.716261}}
{'name': '中延みちしるべ防災広場', 'address': '東京都品川区中延5-12-11', 'note': '住所\u3000中延5-12-11', 'location': {'lat': 35.601184, 'lon': 139.70821}}
{'name': '二葉中央のんき通り広場', 'address': '東京都品川区二葉3-17-14', 'note': '住所\u3000二葉3-17-14', 'location': {'lat': 35.604129, 'lon': 139.721404}}

ちなみに条件を3.5キロにすると以下のような結果になり、きちんと空間検索の条件がきいていることがわかります。

{'name': 'しながわ水族館', 'address': '東京都品川区勝島3-2-1しながわ区民公園内', 'note': '品川区勝島3-2-1しながわ区民公園内電話:3762-3431 交通: JR大井町駅から無料バス開館時間: 午前10時〜午後5時(入館は午後4時30分まで) 休館日: 毎週火曜日、1月1日 *春休み、ゴールデンウィーク、夏休みは営業', 'location': {'lat': 35.588634, 'lon': 139.737364}}
{'name': '大井海岸公園', 'address': '東京都品川区南大井3-27-5', 'note': '品川区南大井3-27-5', 'location': {'lat': 35.588463, 'lon': 139.733647}}
{'name': '大井坂下公園', 'address': '東京都品川区南大井6-23-11', 'note': '品川区南大井6-23-11', 'location': {'lat': 35.589516, 'lon': 139.730786}}
{'name': '西大井四丁目特定児童遊園', 'address': '東京都品川区西大井4-23-12', 'note': '品川区西大井4-23-12', 'location': {'lat': 35.592907, 'lon': 139.719381}}

さいごに

いかがでしたでしょうか?Elasticsearch を使えば 位置情報データの可視化や検索などが簡単にできます。Elasticsearch は年々人気が増しているサービスなので、今後のアップデートに関しても注目ですね。本日は以上です。

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

さいごに

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