宙畑 Sorabatake

解析ノートブック

風が強いテニスコートはどこだ! 過去10年間のインターハイ決勝会場比較

Tellusに搭載されている分刻みの風速データを用いて、過去10年間のうちに硬式テニスのインターハイ決勝が行われた会場を比較してみました。

錦織圭選手、大阪なおみ選手、国枝慎吾選手……2020五輪の国内注目選手が集まる種目として、硬式テニスはそのひとつに入るといっても過言ではないでしょう。

競技が行われるのは有明テニスの森公園という会場のようなのですが、いったいどのようなテニスコートなのでしょうか。

今回はテニスのプレイに影響を与えるひとつの要素「風の強さ」に焦点を当て、衛星データプラットフォーム「Tellus」に搭載されているデータを用いて、2009年から2019年の間にインターハイ(硬式テニス)が行われた会場と比較してみました。

1.調査するテニスコートと調査方法

まず、今回の調査対象となったテニスコートは以下。

そして、Tellus上にもプロットしてみました。

Tellusで右側には標高データを並べてみました。海に近いテニスコートは風が強く、内陸に近く西側に山があれば風が弱いのではないかとなんとなくの予測はできますが実際はどうなのでしょうか。

過去11年間の会場を調べてみるとどうやらテニスコートは12の会場が利用されたようです。2020年の東京五輪が行われる有明テニスの森公園も入っていますね。

では、どのように風速を比較するかについて、今回は以下の要件で風速を比較してみました。

★★★
データの取得方法:Tellus APIのアメダスデータ
期間:2017年7月25日~2017年8月2日
取得するデータ:上記期間の9時から17時までの、各時刻の平均風速を算出
地点:テニスコート最寄りの風速計がある地点
比較方法:最小値、最大値、平均値、中央値の4つの比較を実施
★★★

それではさっそく結果を見てみましょう。

2.結果発表

TellusのAPIを用いてデータを抽出した結果、以下のようになりました。最小値、最大値、平均値、中央値の4つについて、並べています。
※グラフ画像、会場の表記順序は中央値の昇順

実数は以下になります。

データを見ると、東京五輪会場(有明テニスの森公園)は中央値で見ると中間ですが、最大値は他会場と比較してもっとも大きく、最小値も3位という結果に。テニスプレイヤーにとっては、風が強い会場と言えるでしょう。

また、結果だけ見ると青森会場と沖縄会場はどの値をとっても風が強くいようで、風が苦手なテニスプレイヤーにとってはプレイしづらい会場なのかもしれません。

逆に、三重県の四日市ドーム、新潟の大原運動公園は風が弱く、伸び伸びとテニスを楽しめそうですね。

※台風の時期ということもあり、データをより正確に検証するためには、外れ値を除外したり、もっと多くのデータを取得する必要がありますが、今回は簡単な比較のみでとどめております。

3.Tellus APIを用いたデータの抽出方法

では、本データをどのようにして取得するのか。今回の作業を大きくまとめると以下3つの工程になります。

①Tellus APIを叩く準備
②取得したい場所とデータを指定してAPIを叩く
③抽出したデータの保存

これから、3つの工程を一つずつ解説します。

※TellusでJupyter Lab(Jupyter Notebook)を使う方法は、「Tellusの開発環境でAPI引っ張ってみた」をご覧ください。

①Tellus APIを叩く準備

まずはTellusのAPIを叩くための準備が必要になります。

今回は毎分のデータを1時間ごとに平均して取得します。その設定の他にも、また、無効の値や信頼性の低いデータを判別して除外する、観測データから指定の観測値データを取り出すなど、あらかじめ準備が必要なのです。

これをまとめたサンプルコードが以下になります。

import requests, json  
from datetime import datetime  
from datetime import timedelta  
from dateutil.relativedelta import relativedelta  
import numpy as np  
 
TOKEN = 'ここには自分のアカウントのトークンを貼り付ける'  
 
def get_amedas_1min(year, month, day, hour, minute, payload={}):  
    """  
    /api/v1/amedas/1minを叩く  
 
    Parameters  
    ----------  
    year : string  
        西暦年 (4桁の数字で指定) UTC  
    month : string  
        月 01~12 (2桁の数字で指定) UTC  
    day : string  
        日 01~31 (2桁の数字で指定) UTC  
    hour : string  
        時 00~23 (2桁の数字で指定) UTC  
    minute : dict  
        分 00~59 (2桁の数字で指定) UTC  
    payload : dict  
        パラメータ(min_lat,min_lon,max_lat,max_lon)  
     
    Returns  
    -------  
    content : list  
        結果  
    """  
 
    url = 'https://gisapi.tellusxdp.com/api/v1/amedas/1min/{}/{}/{}/{}/{}/'.format(year, month, day, hour, minute)  
     
    headers = {  
        'Authorization': 'Bearer ' + TOKEN  
    }  
    r = requests.get(url, headers=headers, params=payload)  
 
    if not r.status_code == requests.codes.ok:  
        r.raise_for_status()  
    return json.loads(r.content)  
 
def get_hourly_data(target_datetime, station_ids, data_type, data_name, flag_name=None, max_flag=11, payload={}, hour=1):  
    """  
    1時間毎のデータを取得(平均値)  
 
    Parameters  
    ----------  
    datetime : datetime.datetime  
        取得する日時(日本標準時)  
    station_ids : list of int  
        取得する観測所のidの配列  
    data_type : string  
        観測データ種別  
    data_name : string  
        データ名  
    flag_name : string  
        フラグ名  
    max_flag : int  
        正常フラグ上限  
    payload : dict  
        パラメータ(min_lat,min_lon,max_lat,max_lon)  
    hour : int  
        取得時間幅  
         
    Returns  
    -------  
    content : dict  
        結果  
    """  
    def is_valid(value_obj, flag_obj={}, max_flag=11):  
        """  
        無効値や信頼性の低い観測データを判別  
        """  
        byte_size = {  
            1: 127,  
            2: 32767,  
            4: 2147483647  
        }  
        return (flag_obj == {} or flag_obj['value'] <= max_flag) and value_obj['value'] != byte_size[value_obj['size']]  
     
     
    data_dict = {}  
    for id in station_ids:  
        data_dict[id] = []  
    
 
    def get_hourly(target_datetime):  
        begin_datetime = datetime(target_datetime.year, target_datetime.month, target_datetime.day, target_datetime.hour, 0, 0, 0)  
        end_datetime = begin_datetime + timedelta(hours=1)  
        temp_datetime = begin_datetime  
         
        while temp_datetime < end_datetime:  
            temp_datetime_utc = temp_datetime - timedelta(hours=9)  
         
            year = str(temp_datetime_utc.year)  
            month = str(temp_datetime_utc.month).zfill(2)  
            day = str(temp_datetime_utc.day).zfill(2)  
            hour = str(temp_datetime_utc.hour).zfill(2)  
            minute = str(temp_datetime_utc.minute).zfill(2)  
            try:  
                # 1分毎のデータを取得  
                data = get_amedas_1min(year, month, day, hour, minute, payload)  
            except Exception as e:  
                data =[]  
            temp_datetime += timedelta(minutes=1)  
 
            for id in station_ids:   
                # 取得したデータから観測所のデータを取り出す  
                station_data = next((d[data_type] for d in data if d['station']['prefecture_no']['value']*1000 + d['station']['observatory_no']['value'] == id) ,{})  
                 
                value_obj = station_data[data_name]  
                if flag_name == None:  
                    flag_obj = {}  
                else:  
                    flag_obj = station_data[flag_name]  
 
                if(is_valid(value_obj, flag_obj, max_flag)):  
                    # 観測所のデータから取得したい値を取り出す  
                    data_dict[id].append(station_data[data_name]['value'])    
                   
        result = {}  
        # 観測所ごとの1時間の平均値を算出  
        for station_id in data_dict.keys():  
            if len(data_dict) > 0:  
                result[station_id] = np.mean(np.array(data_dict[station_id])/10)  
            else:  
                result[station_id] = 0.0  
        return result  
     
    begin_datetime = datetime(target_datetime.year, target_datetime.month, target_datetime.day, target_datetime.hour, 0, 0, 0)  
    end_datetime = begin_datetime + timedelta(hours=hour)  
 
    temp_datetime = begin_datetime  
    result = {}  
    while temp_datetime < end_datetime:  
        result[temp_datetime.hour] = get_hourly(temp_datetime)  
        temp_datetime += timedelta(hours=1)  
    return result  

②取得したい場所とデータを指定してAPIを叩く

次に、取得したい場所とデータの指定です。

今回はアメダスのデータを用いるため、アメダスが管理する風速計のある場所のデータを指定する必要があります。テニスコートの地点にちょうど風速計があれば良いのですが、そんな都合の良いことはありませんでした。

そのため、アメダスの地図データから、テニスコート最寄りの風速計がある場所を探して指定しました。

テニスコート最寄りの計測地点の名前にカーソルを当てると表示されるURL「.html」手前の数字列を取得する「https://tenki.jp/amedas/3/16/44136.html」の場合は「44136」 Credit : tenki.jp

次に、取得するデータの指定です。詳細はTellusのAPIリファレンスになりますが、今回は風速を取得するため”wind”を指定します。

その他、取得する開始時間と、何時まで取得するのかなど、これをまとめたサンプルコードが以下になります。

# ここに会場最寄りのアメダス観測所IDを入れる  
ids = [64036, 91197, 31312, 54841, 54616, 54501, 82186, 44136, 62078, 68132, 36361, 53061, 87412, 45261]  
 
begin_hour = 9 # 取得開始時刻  
duration_hour = 9 # 取得範囲(時間)  
 
begin_datetime = datetime(2017,7,25) # 取得開始日  
end_datetime = begin_datetime + timedelta(days=8) # 取得終了日  
temp_datetime = begin_datetime  
 
data_type ='wind' # 取得するデータ種別  
data_name = 'velocity_ave' # 取得するデータ名  
flag_name = 'velocity_ave_flag' # 取得するデータの正常フラグ名  
 
result = {}  
while temp_datetime <= end_datetime:  
    print('search now {0:%Y-%m-%d}'.format(temp_datetime))  
    result['{0:%Y-%m-%d}'.format(temp_datetime)] = get_hourly_data(datetime(temp_datetime.year,temp_datetime.month,temp_datetime.day,begin_hour), ids, data_type, data_name, flag_name, payload={  
        'min_lat': -90,  
        'min_lon': -180,  
        'max_lat': 90,  
        'max_lon': 180  
    }, hour=duration_hour)  
     
    temp_datetime += timedelta(days=1)  
 
print('complete!')  

③抽出したデータの保存

最後に、以下のサンプルコードが抽出したデータを保存するためのものになります。

import csv
print('start!')
for id in ids:
    with open('amedas_{}.csv'.format(id), 'w') as f:
        writer = csv.writer(f, lineterminator='\n')
        writer.writerow(['id'] + list(range(begin_hour, begin_hour + duration_hour)))
    
        for day in result.keys():
            row = [day]
            for hour in range(begin_hour, begin_hour + duration_hour):
                row.append(result[day][hour][id])
            writer.writerow(row)
        print('saved {}'.format(id))
print('end.')

以上、3つのサンプルコードを用いて今回のテニスコートの比較を行いました。ぜひ気になる場所があれば試してみてください。

4.まとめと今後チャレンジしたいこと

今回は風速のみの比較でしたが、雨がよく振るか否か(365日中雨だった日は何日か)、湿度、日照時間、気温など、テニスプレイヤーにとって気になる項目を抽出すれば、各テニスコートのレーダーチャートも作れるかもしれません。

このようなものを各テニスコート毎に作りたい…!

そして、テニスコートだけではなく、気象が影響する様々なスポーツ・イベントの会場比較もきっと面白いでしょう。

フェス、お祭り、花火、野球、サッカー、競馬……などなど。

今回の記事をきっかけに様々な場所の比較データが誰にでも見える形で可視化されることを楽しみにしています。