paint-brush
Python と Folium を使用してアウトドア活動を視覚化した方法by@lukaskrimphove
7,290
7,290

Python と Folium を使用してアウトドア活動を視覚化した方法

Lukas Krimphove14m2023/09/06
Read on Terminal Reader

探検と地図作成の旅に出かけましょう! GPX ファイルに命を吹き込み、Folium を使用してインタラクティブなマップを作成する方法を学びます。
featured image - Python と Folium を使用してアウトドア活動を視覚化した方法
Lukas Krimphove HackerNoon profile picture
0-item

私の夢は、ミュンヘンからヴェネツィアまでハイキングし、美しいアルプスをトレッキングすることです。しかし、それでも日常生活を送らなければならないので、私の旅は複数の段階で構成され、各冒険の間には数週間、数か月、場合によっては数年もかかる必要があります。目的地よりも旅そのものなので、それは問題ありません。しかし、私は常にこれらのパスを視覚的にたどって、自分がどこまで到達し、目標にどれだけ近づいているかを確認する方法を望んでいました。私は自分の進歩を祝い、さらに前進するよう自分を奮い立たせる方法が欲しかったのです。


幸いなことに、Adidas Running、Komoot、Strava などの多くのアウトドア アプリやスポーツ アプリでは、アクティビティを GPX ファイルとしてエクスポートできます。ただし、これらの GPX ファイルを保存する場所はありません。


そこで Python とFolium が登場します。私は最近、インタラクティブなマップを作成するための強力な Python ライブラリである Folium に出会いました。 GPX ファイルなどの地理データを簡単に組み込むことができ、カスタマイズや探索が可能になります。収集した豊富な GPS データを利用して、アウトドア旅行に命を吹き込む地図を作成するために Folium の実験を開始しました。


いくつかの調査と多くのテストを経て、過去のアウトドア活動を振り返ることができる地図を思いつきました。

したがって、あなたが私と同じで、何の目的もなく GPS データの宝庫を持っているなら、私がどうやってそこにたどり着いたのか不思議に思うかもしれません。そこでこの記事では、GPX ファイルに命を吹き込む方法を説明します。


探検と地図作成の旅に出かけましょう!

Jupyter Notebook の入門

この冒険を始めるには、Jupyter Notebook を使用します。 Jupyter Notebook を選ぶ理由これは、コード、ビジュアライゼーション、テキストを組み合わせることができる素晴らしいインタラクティブ コンピューティング環境で、データと Folium ライブラリを実験するのに最適です。


Jupyter Notebook をまだインストールしていない場合は、公式 Web サイトの手順に従ってください。インストールしたら、新しい Jupyter Notebook を作成して、旅の準備を始めることができます。

証跡データの GPX ファイルの解析

次に、マップの原材料である GPX ファイルが必要です。 GPX (GPS Exchange Format) は、緯度、経度、標高、時間などの位置データを保存する広く使用されているファイル形式であり、屋外アクティビティの追跡に最適です。


あなたが熱心なハイカー、ランナー、サイクリスト、またはスキーヤーであれば、アウトドア アプリやスポーツ アプリを使用してさまざまな旅行をすでに追跡している可能性があります。これらのアプリの多くでは、アクティビティを GPX 形式でエクスポートできます。 GPX ファイルを集めて、始めましょう!


Python コードでは、gpxpy ライブラリを使用して GPX ファイルを解析し、緯度、経度、標高、速度、距離などの重要なトレイル データを抽出します。 parse_gpx() 関数は、面倒な作業をすべて行ってくれます。


 ### READ GPX FILES import gpxpy import pandas as pd import numpy as np import haversine as hs from pathlib import Path def parse_gpx(file_path): # parse gpx file to pandas dataframe gpx = gpxpy.parse(open(file_path), version='1.0') data = [] points = [] for track in gpx.tracks: for segment in track.segments: for point_idx, point in enumerate(segment.points): points.append(tuple([point.latitude, point.longitude])) # calculate distances between points if point_idx == 0: distance = np.nan else: distance = hs.haversine( point1=points[point_idx-1], point2=points[point_idx], unit=hs.Unit.METERS ) data.append([point.longitude, point.latitude,point.elevation, point.time, segment.get_speed(point_idx), distance]) columns = ['Longitude', 'Latitude', 'Elevation', 'Time', 'Speed', 'Distance'] gpx_df = pd.DataFrame(data, columns=columns) return points, gpx_df activities = {} frames = [] for activity_type in ACTIVITY_TYPES: activities[activity_type] = {} pathlist = Path(activity_type).glob('**/*.gpx') for path in pathlist: # exclude hidden directories and files # will lead to performance issues if there are lots of hidden files if any(part.startswith('.') for part in path.parts): continue activity_group = path.parts[1] activity_name = path.parts[2] if activity_group not in activities[activity_type]: activities[activity_type][activity_group] = [] points, gpx_df = parse_gpx(path) gpx_df['Elevation_Diff'] = np.round(gpx_df['Elevation'].diff(), 2) gpx_df['Cum_Elevation'] = np.round(gpx_df['Elevation_Diff'].cumsum(), 2) gpx_df['Cum_Distance'] = np.round(gpx_df['Distance'].cumsum(), 2) gpx_df['Gradient'] = np.round(gpx_df['Elevation_Diff'] / gpx_df['Distance'] * 100, 1) activities[activity_type][activity_group].append({ 'name': activity_name.replace('.gpx', '').replace('_', ' '), 'points': points, 'gpx_df': gpx_df }) frames.append(gpx_df) df = pd.concat(frames) df

これにより、アクティビティに必要なすべてのデータ、つまりすべての GPS 座標のリストと、あらゆる種類のメトリクスを含む Pandas DataFrame が残ります。

インタラクティブマップ上に GPX 軌跡をプロットする

GPS データが Pandas DataFrame に整理されたことで、屋外アクティビティをインタラクティブな地図上で視覚化できるようになりました。 Folium ではこの作業が簡単になります。


 ### CONFIG # LOCATION = None LOCATION = [48.13743, 11.57549] # latitude, longitude ZOOM_START = 10 ACTIVITY_TYPES = { 'Hiking': { 'icon': 'person-hiking', 'color': 'green' }, 'Running': { 'icon': 'person-running', 'color': 'orange' }, 'Biking': { 'icon': 'person-biking', 'color': 'red' }, 'Skiing': { 'icon': 'person-skiing', 'color': 'blue' } }
  • 特定の場所を中心とするマップを作成します (1 つを選択することも、データに基づいてコードに中心を決定させることもできます)。
  • ハイキング、ランニング、サイクリング、スキーなどのアクティビティの種類ごとに固有の色とアイコンを割り当てます。 ACTIVITY_TYPES 辞書がこれに役立ちます。
 ### READ GPX FILES import gpxpy import pandas as pd import numpy as np import haversine as hs from pathlib import Path def parse_gpx(file_path): # parse gpx file to pandas dataframe gpx = gpxpy.parse(open(file_path), version='1.0') data = [] points = [] for track in gpx.tracks: for segment in track.segments: for point_idx, point in enumerate(segment.points): points.append(tuple([point.latitude, point.longitude])) # calculate distances between points if point_idx == 0: distance = np.nan else: distance = hs.haversine( point1=points[point_idx-1], point2=points[point_idx], unit=hs.Unit.METERS ) data.append([point.longitude, point.latitude,point.elevation, point.time, segment.get_speed(point_idx), distance]) columns = ['Longitude', 'Latitude', 'Elevation', 'Time', 'Speed', 'Distance'] gpx_df = pd.DataFrame(data, columns=columns) return points, gpx_df activities = {} frames = [] for activity_type in ACTIVITY_TYPES: activities[activity_type] = {} pathlist = Path(activity_type).glob('**/*.gpx') for path in pathlist: # exclude hidden directories and files # will lead to performance issues if there are lots of hidden files if any(part.startswith('.') for part in path.parts): continue activity_group = path.parts[1] activity_name = path.parts[2] if activity_group not in activities[activity_type]: activities[activity_type][activity_group] = [] points, gpx_df = parse_gpx(path) gpx_df['Elevation_Diff'] = np.round(gpx_df['Elevation'].diff(), 2) gpx_df['Cum_Elevation'] = np.round(gpx_df['Elevation_Diff'].cumsum(), 2) gpx_df['Cum_Distance'] = np.round(gpx_df['Distance'].cumsum(), 2) gpx_df['Gradient'] = np.round(gpx_df['Elevation_Diff'] / gpx_df['Distance'] * 100, 1) activities[activity_type][activity_group].append({ 'name': activity_name.replace('.gpx', '').replace('_', ' '), 'points': points, 'gpx_df': gpx_df }) frames.append(gpx_df) df = pd.concat(frames) df That leaves us with all the necessary data of an activity: a list of all the GPS coordinates and a Pandas DataFrame containing all kinds of metrics. Plotting GPX Trails on the Interactive Map With our GPS data now organized in a Pandas DataFrame, we can visualize our outdoor activities on an interactive map. Folium makes this task a breeze: ### CONFIG # LOCATION = None LOCATION = [48.13743, 11.57549] # latitude, longitude ZOOM_START = 10 ACTIVITY_TYPES = { 'Hiking': { 'icon': 'person-hiking', 'color': 'green' }, 'Running': { 'icon': 'person-running', 'color': 'orange' }, 'Biking': { 'icon': 'person-biking', 'color': 'red' }, 'Skiing': { 'icon': 'person-skiing', 'color': 'blue' } } We'll create a map centered at a specific location (you can choose one or let the code determine the center based on your data). We'll assign unique colors and icons to each activity type, such as hiking, running, biking, or skiing. The ACTIVITY_TYPES dictionary will help us with this. ### CREATE MAP import folium from folium import plugins as folium_plugins if LOCATION: location = LOCATION else: location=[df.Latitude.mean(), df.Longitude.mean()] map = folium.Map(location=location, zoom_start=ZOOM_START, tiles=None) folium.TileLayer('OpenStreetMap', name='OpenStreet Map').add_to(map) folium.TileLayer('Stamen Terrain', name='Stamen Terrain').add_to(map) ### MAP TRAILS def timedelta_formatter(td): td_sec = td.seconds hour_count, rem = divmod(td_sec, 3600) hour_count += td.days * 24 minute_count, second_count = divmod(rem, 60) return f'{hour_count}h, {minute_count}min, {second_count}s' def create_activity_popup(activity): df = activity['gpx_df'] attributes = { 'Date': { 'value': df['Time'][df.index[0]].strftime("%m/%d/%Y"), 'icon': 'calendar' }, 'Start': { 'value': df['Time'][df.index[0]].strftime("%H:%M:%S"), 'icon': 'clock' }, 'End': { 'value': df['Time'][df.index[-1]].strftime("%H:%M:%S"), 'icon': 'flag-checkered' }, 'Duration': { 'value': timedelta_formatter(df['Time'][df.index[-1]]-df['Time'][df.index[0]]), 'icon': 'stopwatch' }, 'Distance': { 'value': f"{np.round(df['Cum_Distance'][df.index[-1]] / 1000, 2)} km", 'icon': 'arrows-left-right' }, 'Average Speed': { 'value': f'{np.round(df.Speed.mean() * 3.6, 2)} km/h', 'icon': 'gauge-high' }, 'Max. Elevation': { 'value': f'{np.round(df.Elevation.max(), 2)} m', 'icon': 'mountain' }, 'Uphill': { 'value': f"{np.round(df[df['Elevation_Diff']>0]['Elevation_Diff'].sum(), 2)} m", 'icon': 'arrow-trend-up' }, 'Downhill': { 'value': f"{np.round(abs(df[df['Elevation_Diff']<0]['Elevation_Diff'].sum()), 2)} m", 'icon': 'arrow-trend-down' }, } html = f"<h4>{activity['name'].upper()}</h4>" for attribute in attributes: html += f'<i class="fa-solid fa-{attributes[attribute]["icon"]}" title="{attribute}"> {attributes[attribute]["value"]}</i></br>' return folium.Popup(html, max_width=300) feature_groups = {} for activity_type in activities: color = ACTIVITY_TYPES[activity_type]['color'] icon = ACTIVITY_TYPES[activity_type]['icon'] for activity_group in activities[activity_type]: # create and store feature groups # this allows different activity types in the same feature group if activity_group not in feature_groups: # create new feature group fg = folium.FeatureGroup(name=activity_group, show=True) feature_groups[activity_group] = fg map.add_child(fg) else: # use existing fg = feature_groups[activity_group] for activity in activities[activity_type][activity_group]: # create line on map points = activity['points'] line = folium.PolyLine(points, color=color, weight=4.5, opacity=.5) fg.add_child(line) # create marker marker = folium.Marker(points[0], popup=create_activity_popup(activity), icon=folium.Icon(color=color, icon_color='white', icon=icon, prefix='fa')) fg.add_child(marker) map.add_child(folium.LayerControl(position='bottomright')) folium_plugins.Fullscreen(position='topright').add_to(map) map
  • Foliums FeatureGroup の概念を使用して、グループ名に基づいてトレイルをグループ化します。これにより、後で特定のアクティビティ グループを表示または非表示にすることができます。
  • 次に、解析されたデータを反復処理し、Folium の PolyLine オブジェクトと Marker オブジェクトを使用して地図上に各トレイルをプロットします。 PolyLine は実際の軌跡を表し、Marker は各アクティビティの開始点として機能します。マーカーをクリックすると、対応するトレイルに関する関連情報がポップアップに表示されます。

結論

この探索とマッピングの旅で、私たちは Python と Folium を使用してありふれた GPX ファイルを動的でインタラクティブなマップに変換する方法を学びました。これで、アウトドアの冒険を追体験し、進歩を祝い、旅の次の段階に向けてモチベーションを維持することができます。


ただし、マップをカスタマイズ、拡張、改善する方法はたくさんあります。 GPX ファイルを取得し、Jupyter Notebook を起動して、過去のアウトドア アクティビティを地図上で生き生きとさせましょう。

マッピングを楽しんでください。

次は何ですか?

これからもたくさんのことがあるのでお楽しみに:


  • AWS を使用して地図を含む Web サイトをデプロイする
  • Python と Plotly を使用して標高と速度のプロファイルをプロットする
  • 途中で撮影した写真でトレイルを強化する
  • などなど

参考文献


ここでも公開されています。