paint-brush
Python과 Folium을 사용하여 야외 활동을 시각화하는 방법~에 의해@lukaskrimphove
7,784 판독값
7,784 판독값

Python과 Folium을 사용하여 야외 활동을 시각화하는 방법

~에 의해 Lukas Krimphove14m2023/09/06
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

탐험과 매핑 탐험을 시작하세요! GPX 파일에 생명을 불어넣고 Folium을 사용하여 대화형 지도를 만드는 방법을 알아보세요.
featured image - Python과 Folium을 사용하여 야외 활동을 시각화하는 방법
Lukas Krimphove HackerNoon profile picture
0-item

나는 뮌헨에서 베니스까지 하이킹을 하고, 아름다운 알프스를 횡단하는 트레킹을 꿈꿉니다. 하지만 여전히 일상생활을 해야 하기 때문에 나의 여행은 각 모험 사이에 몇 주, 몇 달, 심지어 몇 년이 걸리는 여러 단계로 구성되어야 합니다. 괜찮습니다. 목적지보다 여행이 더 중요하기 때문입니다. 그러나 나는 항상 이러한 경로를 시각적으로 되돌아보고 내가 얼마나 멀리 왔는지, 목표에 얼마나 가까이 왔는지 확인할 수 있는 방법을 원했습니다. 나는 내가 이룩한 진전을 축하하고 더 나아갈 수 있도록 동기를 부여할 수 있는 방법을 원했습니다.


다행스럽게도 Adidas Running, Komoot, Strava 등과 같은 많은 야외 및 스포츠 앱에서는 활동을 GPX 파일로 내보낼 수 있습니다. 그러나 해당 GPX 파일을 사용할 수 있는 곳은 없습니다.


이것이 PythonFolium이 작동하는 곳입니다. 나는 최근 대화형 지도를 만들기 위한 강력한 Python 라이브러리인 Folium을 우연히 발견했습니다. GPX 파일과 같은 지리 데이터를 쉽게 통합할 수 있으며 사용자 정의 및 탐색이 가능합니다. 제가 수집한 풍부한 GPS 데이터를 활용하여 Folium을 사용하여 야외 여행을 생생하게 표현하는 지도를 만드는 실험을 시작했습니다.


몇 가지 연구와 많은 테스트 끝에 저는 과거의 야외 활동을 다시 살펴볼 수 있는 지도를 생각해 냈습니다.

따라서 당신이 나와 같은 사람이고 아무런 목적도 없이 GPS 데이터의 보물 창고를 가지고 있다면 내가 어떻게 거기에 도달했는지 궁금할 것입니다. 그래서 이 글에서는 GPX 파일에 생명을 불어넣는 방법을 설명하겠습니다.


탐사와 매핑 탐험을 시작해 보세요!

Jupyter 노트북 시작하기

이 모험을 시작하기 위해 Jupyter Notebook을 사용하겠습니다. 왜 주피터 노트북인가? 이는 코드, 시각화 및 텍스트를 결합할 수 있는 환상적인 대화형 컴퓨팅 환경으로, 데이터와 Folium 라이브러리를 실험하기에 완벽합니다.


아직 Jupyter Notebook을 설치하지 않았다면 공식 웹사이트 의 지침을 따르세요. 설치가 완료되면 새로운 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 트레일 그리기

이제 Pandas DataFrame에 구성된 GPS 데이터를 사용하여 대화형 지도에서 야외 활동을 시각화할 수 있습니다. 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' } }
  • 특정 위치를 중심으로 지도를 생성합니다. 하나를 선택하거나 코드가 데이터를 기반으로 중심을 결정하도록 할 수 있습니다.
  • 하이킹, 달리기, 자전거 타기, 스키 등 각 활동 유형에 고유한 색상과 아이콘을 할당합니다. 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를 사용하여 지도가 포함된 웹사이트 배포
  • Python 및 Plotly를 사용하여 고도 및 속도 프로필 플로팅
  • 한쪽에서 찍은 사진으로 트레일 개선하기
  • 그리고 훨씬 더

참고자료

  • Jupyter Notebook을 포함한 모든 코드는 내 GitHub 에 있습니다.
  • Folium에 대해 조사하던 중, 제가 계획했던 것과 똑같은 일을 했던 Patrick의 훌륭한 글을 발견했습니다. 그의 작업은 내 솔루션을 구축하는 데 훌륭한 기반이 되었으므로 꼭 확인해 보세요.
  • 주피터 노트북
  • 폴리엄
  • 팬더

여기에도 게시되었습니다.