본문 바로가기

데이터와 함께 탱고를/데이터 시각화

[지도 데이터 시각화] Part 6. pydeck 살펴보기

deck.gl 은 Uber 에서 만든 자바스크립트 공간 데이터 시각화 라이브러리입니다.
pydeck 은 이 라이브러리를 파이썬에서도 쓸 수 있도록 만든 패키지입니다.
이 또한 Uber 팀에서 공식적으로 만들었습니다.

특징으로는 대용량 데이터도 거뜬하게 렌더링할 수 있으며, 렌더링이 좀 버거울 수도 있는 일부 레이어는 GPU 연산을 제공합니다. (즉 대용량 처리 및 렌더링에 매우 용이하다는 말입니다.)

이전에 말씀드렸다싶이, 지금까지 소개한 2개의 파이썬 패키지(folium, mapboxgl)에 비해 상대적으로 매우 최근 라이브러리고, 기술적으로도 훨씬 진보한 라이브러리라, 꼭 알고가셨으면 좋겠습니다.
다만, 이전보다 좀 더 섬세하게 다룰 수 있는 부분이 많아, 조금 더 어려울 수는 있습니다.
아직 geojson 및 지도 데이터에 익숙치 않으신 분들은 이전 포스팅을 먼저 보기를 권장드립니다.

그럼 이제 시작해보겠습니다.

1. 설치 & 세팅

먼저 pip 를 통해 설치해줍니다.

pip install pydeck

이후 다음의 명령어로 nbextensions 에 추가해줍니다.

jupyter nbextension install --sys-prefix --symlink --overwrite --py pydeck
jupyter nbextension enable --sys-prefix --py pydeck

mapboxgl 때와 마찬가지로, 환경 변수에 자신의 mapbox api token 이 있어야 합니다.
https://www.mapbox.com/ 에 접속하시고 계정을 만드신 뒤, account 에 들어가시면 자신의 token 값에 대해 알 수 있습니다.

환경 변수로 다음과 같이 추가해줍니다.

MAPBOX_API_KEY = "자신의 mapbox api token (pk... 로 시작함)"

이제 쥬피터 노트북을 열어 pydeck 을 임포트합니다.

import pydeck as pdk

 


2. 샘플 하나 해보기

데이터는 이전처럼 서울시 행정동별 고령인구 지도 데이터를 사용합니다.
파일 내부에 데이터가 아래와 같이 저장되어 있습니다.

geo_data = 'data/older_seoul.geojson'
{'type': 'FeatureCollection',
 'crs': {'type': 'name',
 'properties': {'name': 'urn:ogc:def:crs:OGC:1.3:CRS84'}},
 'features': [{
   'type': 'Feature',
   'properties': {
    '시': '서울특별시',
    '구': '종로구',
    '동': '사직동',
    '행정동코드': 11110530,
    '인구': 9700,
    '남자': 4375,
    '여자': 5325},
   'geometry': {'type': 'MultiPolygon',
    'coordinates': [[[[126.97688884274817, 37.575650779448786],
       [126.9770344988775, 37.569194530054546],
       [126.97597472821249, 37.569336299425764],
       ....
     ]]]
   }, {
   'type': 'Feature',
   'properties': {
    '시': '서울특별시',
    '구': '종로구',
    '동': '삼청동',
    '행정동코드': 11110540,
    '인구': 3013,
    '남자': 1443,
    '여자': 1570},
   'geometry': {'type': 'MultiPolygon',
    'coordinates': [[[[126.98268938649305, 37.5950655194224],
       [126.98337258456999, 37.59435192551688],
       [126.98386809792802, 37.59385046812643],
       ....
     ]]]
   }]
}

MultiPolygon 단위로, 시군구 이름, 인구, 남자, 여자 데이터를 가지고 있는 일반적인 geojson 데이터 입니다.

pydeck 은 geojson 보다는 주로 pandas dataframe 을 input으로 받습니다.
따라서, 기존의 이 geojson 데이터를 dataframe 형태로 받아보겠습니다.

import geopandas as gpd

df = gpd.read_file(geo_data)
df.head()
  행정동코드 인구 남자 여자 geometry
0 서울특별시 종로구 사직동 11110530 9700 4375 5325 (POLYGON ((126.9768888427482 37.57565077944879...
1 서울특별시 종로구 삼청동 11110540 3013 1443 1570 (POLYGON ((126.982689386493 37.5950655194224, ...
2 서울특별시 종로구 부암동 11110550 10525 5002 5523 (POLYGON ((126.9758511377569 37.59656422224408...
3 서울특별시 종로구 평창동 11110560 18830 8817 10013 (POLYGON ((126.9750746678809 37.63138628651299...
4 서울특별시 종로구 무악동 11110570 8745 4078 4667 (POLYGON ((126.960673532739 37.58079784202972,...

잘 읽어온 것을 확인할 수 있습니다.

그런데 하나 수정해야할 것이 있습니다.
geometry 컬럼입니다. 현재 여기에 값들은 shapely.geometry.multipolygon 형태인데, pydeck 은 이런 형태를 읽지 못합니다.
pydeck 에서는 항상 geometry 에 연속된 포인트들을 갖는 리스트 값이 있어야 합니다.
따라서, 다소 귀찮지만 다음과 같은 작업들을 해줘야합니다.

def multipolygon_to_coordinates(x):
    lon, lat = x[0].exterior.xy
    return [[x, y] for x, y in zip(lon, lat)]

df['coordinates'] = df['geometry'].apply(multipolygon_to_coordinates)
del df['geometry']

이후 렌더링할 지도에는 '인구' 별로 색을 다르게 하려고 합니다.
인구 값에 따른 색상 지정을 데이터프레임에서 미리 작업해줄 필요가 있습니다. (사실 pydeck 은 데이터프레임만 보고 렌더링하기 때문에, 렌더링 전에 색상, 크기 시각화 하고싶은 기준들을 새로운 컬럼으로 추가해줘야 합니다.)

# 인구를 0 ~ 1 사이의 값으로 변환시킵니다.
df['정규화인구'] = df['인구'] / df['인구'].max()

한편, 지금 데이터프레임은 geopandas.dataframe 입니다.
pydeck 은 pandas.dataframe 형태만 인식 가능합니다. 따라서 데이터프레임을 변환하겠습니다.

import pandas as pd

df = pd.DataFrame(df)

최종적으로, pydeck 에 input 으로 줄 데이터프레임은 다음과 같이 생겼습니다.

df.head()
  행정동코드 인구 남자 여자 coordinates 정규화인구
0 서울특별시 종로구 사직동 11110530 9700 4375 5325 [[126.97688884274817, 37.575650779448786], [12... 0.167132
1 서울특별시 종로구 삼청동 11110540 3013 1443 1570 [[126.98268938649305, 37.5950655194224], [126.... 0.051914
2 서울특별시 종로구 부암동 11110550 10525 5002 5523 [[126.97585113775686, 37.59656422224408], [126... 0.181347
3 서울특별시 종로구 평창동 11110560 18830 8817 10013 [[126.97507466788086, 37.63138628651299], [126... 0.324443
4 서울특별시 종로구 무악동 11110570 8745 4078 4667 [[126.96067353273895, 37.580797842029725], [12... 0.150677

이제 pydeck 을 이용하여 각 데이터를 Choropleth 로 시각화 해보겠습니다.
pydeck 에서는 Choropleth 로 부르지않고 PolygonLayer 라고 부릅니다.

# Make layer
layer = pdk.Layer(
    'PolygonLayer', # 사용할 Layer 타입
    df, # 시각화에 쓰일 데이터프레임
    get_polygon='coordinates', # geometry 정보를 담고있는 컬럼 이름
    get_fill_color='[0, 255*정규화인구, 0]', # 각 데이터 별 rgb 또는 rgba 값 (0~255)
    pickable=True, # 지도와 interactive 한 동작 on
    auto_highlight=True # 마우스 오버(hover) 시 박스 출력
)

# Set the viewport location
center = [126.986, 37.565]
view_state = pdk.ViewState(
    longitude=center[0],
    latitude=center[1],
    zoom=10)

# Render
r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

pickable=True, auto_highlight=True 를 주면 위와 같이 마우스 오버시 데이터 정보가 뜹니다.

데이터 별 지도 정보에 높이를 주어 3d 로도 볼 수 있습니다.
색과 마찬가지로 '인구' 기준으로 높이를 줘보겠습니다.

layer.extruded = True;
layer.get_elevation = '인구';
layer.elevation_scale = 0.05

view_state.bearing=15
view_state.pitch=45

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

제대로 시각화 된 것을 확인하실 수 있습니다.


3. 동작 살펴보기

위 코드 기준으로 동작을 하나씩 살펴보겠습니다.

3.1. pydeck class

pdk.Layer

우리가 어떤 레이어를 지도 위에 올릴 때, 항상 이 클래스를 통해서 만듭니다.
위 코드에 주석으로도 설명해두었지만, 좀 더 구체적으로 어떤 파라미터(속성) 들이 있는지, 필요한 것만 살펴보겠습니다.

pdk.Layer(
    type = "미리 정의된 레이어 타입",
    id = "이 레이어의 아이디 (optional)",
    data = "pandas.DataFrame 또는 geojson url",
)

일단 이 위의 파라미터들이 기본입니다.
특히 id 의 경우 같은 타입의 여러 개의 레이어를 지도에 쌓는 경우, 각 레이어를 다른 id 로 설정해줘야 합니다.
예를 들어, 지도에 PolygonLayer 가 여러 개 있는 경우, 각 레이어의 id 를 'polygon1', 'polygon2' 이런 식으로 주어야 합니다.

pdk.ViewState

folium 이나 mapboxgl 에서도 흔히 본, 익숙한 파라미터들이 있습니다.
지도 렌더링할 때, 초기화 뷰에 대한 설정입니다.
익숙하실거라 생각하여 별도의 설명은 하지않겠습니다.

pdk.ViewState(
    longitude = "중심 경도 (default 0)",
    latitude = "중심 위도 (default 0)",
    zoom = "줌 레벨 (default 0)",
    pitch = (default 0),
    bearing = (default 0),
    **kwargs,
)

pdk.Deck

만들어진 레이어, 뷰 정보, 맵 스타일 등 모든 요소를 모아 출력시킬 수 있는 클래스 입니다.
기본 값으로 다음의 값들이 들어가있습니다.

pdk.Deck(
    layers=[],
    views=[{"controller": true, "type": "MapView"}],
    map_style='mapbox://styles/mapbox/dark-v9',
    mapbox_key=None,
    initial_view_state={"bearing": 0, 
                        "latitude": 0.0, 
                        "longitude": 0.0,
                        "maxZoom": 20,
                        "minZoom": 0, 
                        "pitch": 0, 
                        "zoom": 1},
    width='100%',
    height=500,
    tooltip=True,
)

레이어를 쌓고싶다면, layers 리스트에 추가시키면 됩니다.

3.2. pydeck 의 핵심

입력 데이터를 pandas.DataFrame 을 사용한다.

물론 geojson 을 사용할 수도 있습니다만, 대부분 pandas.DataFrame 사용하고있습니다.
mapboxgl 의 경우 geojson 을 파싱해와, 바로 데이터 값에 따른 색상, 높이 등의 시각화 요소를 다르게 할 수 있었지만, pydeck 은 이게 안됩니다.
입력 데이터를 어떻게든 pandas.DataFrame 로 만드는게 전처리에서 가장 중요한 부분이라 할 수 있습니다.
만약 렌더링이 잘 안되는 경우 pandas.DataFrame 이 맞는지 우선적으로 확인해볼 필요가 있습니다.

다양한 레이어가 많다.

말 그대로 입니다. 아래에서 더 살펴보겠습니다.

레이어를 쌓을 수 있다.

이게 mapboxgl 과 비교되는 가장 큰 장점입니다.
pdk.Decklayers 를 통해 레이어를 쌓을 수 있습니다!


4. 다른 레이어들 살펴보기

4.1. Point 단위

샘플 데이터는 서울시내 공중화장실 데이터를 사용하겠습니다.

geo_data = 'data/toilet_seoul_sample.geojson'

위 파일을 열어보면 다음과 같이 데이터가 저장되어 있습니다.

{'type': 'FeatureCollection',
 'features': [{'type': 'Feature',
   'geometry': {'type': 'Point', 'coordinates': [127.158647, 37.501401]},
   'properties': {'구명': '송파구', '법정동명': '마천동', '이용량': 248}},
  {'type': 'Feature',
   'geometry': {'type': 'Point', 'coordinates': [127.073728, 37.644937]},
   'properties': {'구명': '노원구', '법정동명': '하계동', '이용량': 252}},
  {'type': 'Feature',
   'geometry': {'type': 'Point', 'coordinates': [127.077637, 37.640335]},
   'properties': {'구명': '노원구', '법정동명': '하계동', '이용량': 192}},
  {'type': 'Feature',
   'geometry': {'type': 'Point', 'coordinates': [127.020449, 37.645724]},
   'properties': {'구명': '강북구', '법정동명': '수유동', '이용량': 409}},
  {'type': 'Feature',
   'geometry': {'type': 'Point', 'coordinates': [127.031187, 37.659783]},
   'properties': {'구명': '도봉구', '법정동명': '방학동', '이용량': 333}},
  {'type': 'Feature',
   'geometry': {'type': 'Point', 'coordinates': [127.123028, 37.502339]},
   'properties': {'구명': '송파구', '법정동명': '가락동', '이용량': 333}},
   ...
]}

Point 단위로 구명, 법정동명, 이용량을 가지고 있는 일반적인 geojson 데이터 입니다.
(이용량은 실제 값이 아니고, 샘플용으로 제가 랜덤으로 준 값입니다.)

먼저 데이터를 읽어와 df 에 저장하겠습니다.

import geopandas as gpd

df = gpd.read_file(geo_data)
df['lat'] = df['geometry'].apply(lambda coord: coord.y)
df['lng'] = df['geometry'].apply(lambda coord: coord.x)
del df['geometry']

df = pd.DataFrame(df)
df.head()
  구명 법정동명 이용량 lat lng
0 송파구 마천동 248 37.501401 127.158647
1 노원구 하계동 252 37.644937 127.073728
2 노원구 하계동 192 37.640335 127.077637
3 강북구 수유동 409 37.645724 127.020449
4 도봉구 방학동 333 37.659783 127.031187

ScatterplotLayer

모든 Point 들을 지도에 그대로 출력합니다.

layer = pdk.Layer(
    'ScatterplotLayer',
    df,
    get_position='[lng, lat]',
    get_radius=50,
    get_fill_color='[255, 255, 255]',
    pickable=True,
    auto_highlight=True
)

center = [126.986, 37.565]
view_state = pdk.ViewState(
    longitude=center[0],
    latitude=center[1],
    zoom=10)

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

서울시내 공중화장실 데이를 모두 시각화 했습니다.

이용량에 따라 색을 다르게 하고싶으면, 다음과 같이 get_fill_color 를 수정해주면 됩니다.
이 때, 색상은 0~255 값을 갖는 rgba 임을 잊지 말아주세요.

df['정규화이용량'] = df['이용량'] / df['이용량'].max()
layer.get_fill_color = '[255*정규화이용량, 255*정규화이용량, 255]'

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

이용량에 따라 색상을 다르게하여 시각화 했습니다.

이 외에 각 Point 의 지름 반지름 설정, 높이 설정 등은 get_radius, get_elevation 으로 바꿀 수 있습니다.
방법은 위와 동일합니다.

HeatmapLayer

Point 들의 밀집도를 한 눈에 파악할 수 있습니다.
밀도가 높은 곳은 더 진한 색상으로 표시됩니다.

layer = pdk.Layer(
    'HeatmapLayer',
    df,
    get_position='[lng, lat]'
)

center = [126.986, 37.565]
view_state = pdk.ViewState(
    longitude=center[0],
    latitude=center[1],
    zoom=10)

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

colorRangeintensity, getWeight 등의 추가적인 파라미터들을 통해 색상, 밀도 설정, 가중치를 섬세하게 설정할 수 있습니다.
이런 추가적인 파라미터 사용법은 이후에 설명드리겠습니다.

Grid Layers

1) CPUGridLayer (GPUGridLayer)

pydeck 에서만 제공하는 아주 강력한 레이어 중 하나입니다.
Point 들을 일정한 그리드 단위로 묶어서 표현합니다.

layer = pdk.Layer(
    'CPUGridLayer', # 대용량 데이터의 경우 'GPUGridLayer'
    df,
    get_position='[lng, lat]',
    pickable=True,
    auto_highlight=True
)

center = [126.986, 37.565]
view_state = pdk.ViewState(
    longitude=center[0],
    latitude=center[1],
    zoom=10)

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

특히, 다음과 같이 높이도 추가로 줄 수 있습니다.

layer.extruded = True
layer.elevation_scale = 3 # default 1

view_state.bearing = -15
view_state.pitch = 45

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

2) HexagonLayer

위와 동일하지만 모양이 육각형(Hexagon) 입니다.

layer.type = 'HexagonLayer'

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

3) ScreenGridLayer

위와 동일하지만 모양이 정사각형 스크린(Sreen) 입니다.

layer.type = 'ScreenGridLayer'
layer.cellSizePixels = 10 # default 100

view_state.bearing = 0
view_state.pitch = 0

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

TextLayer

데이터 Point 좌표에 텍스트를 출력합니다.
다만, 한글은 렌더링 되지 않습니다.
공식문서에 따르면 only characters in the Ascii code range 32-128 값만 렌더링이 된다고 합니다.

df['text'] = 'text'
layer = pdk.Layer(
    'TextLayer',
    df[:100],
    get_position='[lng, lat]',
    get_text='text',
    get_color='[0, 255, 255]',
    font_family='consolas',
    sizeScale=0.5,
    pickable=True,
    auto_highlight=True
)

center = [126.986, 37.565]
view_state = pdk.ViewState(
    longitude=center[0],
    latitude=center[1],
    zoom=10)

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

4.2. Points (Path or Polygon) 단위

geometry 데이터가 2개 이상의 좌표가 들어간 데이터들에 대해서 다룹니다.
Points(Path) 단위의 레이어들은 geometry 정보가 OD, LineString(또는 Polygon, MultiPolygon) 단위의 데이터들입니다.

OD layers

OD Path 레이어는 Point(Origin)와 Point(Destination) 를 연결하는 레이어입니다.

먼저 샘플로 따릉이 OD 데이터를 사용해보겠습니다.
다음과 같이 출발지와 도착지의 위도, 경도를 가지고 있는 데이터 입니다.

import pandas as pd

df = pd.read_csv('data/seoul_bike_sample.csv')
df.head()
  대여대여소명 반납대여소명 이용 이용시간(분) 이용거리(M) 대여대여소위도 대여대여소경도 대여대여소구명 반납대여소위도 반납대여소경도 반납대여소구명
0 여의나루역 1번출구 앞 DMC산학협력연구센터 앞 1 47.000000 11160.000000 37.526989 126.932098 영등포구 37.575802 126.890739 마포구
1 여의나루역 1번출구 앞 IFC몰 3 46.333333 5853.333333 37.526989 126.932098 영등포구 37.526066 126.925537 영등포구
2 여의나루역 1번출구 앞 KBS 앞 2 13.000000 2110.000000 37.526989 126.932098 영등포구 37.524666 126.918022 영등포구
3 여의나루역 1번출구 앞 KT앞 8 20.250000 1692.500000 37.526989 126.932098 영등포구 37.521908 126.918953 영등포구
4 여의나루역 1번출구 앞 NH농협은행 앞 7 9.428571 1262.857143 37.526989 126.932098 영등포구 37.522079 126.930367 영등포구
df['정규화이용'] = df['이용'] / df['이용'].max()
df['정규화이용시간'] = df['이용시간(분)'] / df['이용시간(분)'].max()

1) LineLayer

o-d 를 잇는 직선을 그립니다.

이용 값이 클수록 선의 굵기를 굵게하고,
이용시간 값에 따라 색상을 다르게 해보겠습니다.

layer = pdk.Layer(
    'LineLayer',
    df,
    get_source_position='[대여대여소경도, 대여대여소위도]',
    get_target_position='[반납대여소경도, 반납대여소위도]',
    get_width='1 + 10 * 정규화_이용',
    get_color='[255, 255 * 정규화이용시간, 120]',
    pickable=True,
    auto_highlight=True
)

# pydeck.data_utils.compute_view 는 Points 들의 경도, 위도를 리스트로 주면, 알아서 view_state 를 만들어줍니다.
view_state = pydeck.data_utils.compute_view(df[['대여대여소경도', '대여대여소위도']].values)
view_state.zoom = 13

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

2) ArcLayer

o-d 를 잇는 곡선을 그립니다.

이용 값이 클수록 선의 굵기를 굵게하고,
origin 과 destination 에 따라 색을 다르게 해보겠습니다.

layer = pdk.Layer(
    'ArcLayer',
    df,
    get_source_position='[대여대여소경도, 대여대여소위도]',
    get_target_position='[반납대여소경도, 반납대여소위도]',
    get_width='1 + 10 * 정규화_이용',
    get_source_color='[255, 255, 120]',
    get_target_color='[255, 0, 0]',
    pickable=True,
    auto_highlight=True
)

# pydeck.data_utils.compute_view 는 Points 들의 경도, 위도를 리스트로 주면, 알아서 view_state 를 만들어줍니다.
view_state = pydeck.data_utils.compute_view(df[['대여대여소경도', '대여대여소위도']].values)
view_state.zoom = 12
view_state.bearing = -15
view_state.pitch = 45

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

Path Layer

Path layer 는 한 데이터가 2개 이상의 Point 들을 가지고 있습니다.
즉, Point 들을 차례대로 연결시켜 Path 를 그려냅니다.

먼저, 샘플 데이터로 사용할 서울시 특정 지역의 도로 데이터를 가져오겠습니다.

geo_data = 'data/dongjak_road.geojson'

이 데이터는 대략 다음과 같이 생겼습니다.

{'type': 'FeatureCollection',
 'crs': {'type': 'name',
  'properties': {'name': 'urn:ogc:def:crs:OGC:1.3:CRS84'}},
 'features': [
  {'type': 'Feature',
   'properties': {'도로명': '만양로8길', '도로길이': 53, '도로폭': 7},
   'geometry': {'type': 'LineString',
    'coordinates': [[126.94777636499998, 37.50835655899999],
     [126.94782061399997, 37.50831506600002],
     [126.94786056500004, 37.50822010600001],
     [126.94791028600002, 37.50814785199998],
     [126.94795418900003, 37.508099371000014],
     [126.94805295399999, 37.508003151000025],
     [126.94809408200001, 37.507960638999975]]}},
  {'type': 'Feature',
   'properties': {'도로명': '동작대로39가길', '도로길이': 450, '도로폭': 5},
   'geometry': {'type': 'LineString',
    'coordinates': [[126.98097487099994, 37.493643061],
     [126.98093601699998, 37.49368160099999],
     [126.98089868099999, 37.493756701999985],
     [126.98072013800004, 37.493820518],
     [126.98048858200002, 37.493856463999975],
     [126.98042777299997, 37.49387360600002],
     ....

이를 pandas.dataframe 형태로 불러오겠습니다.

import geopandas as gpd
import shapely

# Shapely 형태의 데이터를 받아 내부 좌표들을 List안에 반환합니다.
def line_string_to_coordinates(line_string):
    if isinstance(line_string, shapely.geometry.linestring.LineString):
        lon, lat = line_string.xy
        return [[x, y] for x, y in zip(lon, lat)]
    elif isinstance(line_string, shapely.geometry.multilinestring.MultiLineString):
        ret = []
        for i in range(len(line_string)):
            lon, lat = line_string[i].xy
            for x, y in zip(lon, lat):
                ret.append([x, y])
        return ret

df = gpd.read_file(geo_data)
df['geometry'] = df['geometry'].apply(line_string_to_coordinates)
df = pd.DataFrame(df) # geopanadas 가 아닌 pandas 의 데이터프레임으로 꼭 바꿔줘야 합니다.
df.head()
  도로명 도로길이 도로폭 geometry
0 만양로8길 53 7 [[126.94777636499998, 37.50835655899999], [126...
1 동작대로39가길 450 5 [[126.98097487099994, 37.493643061], [126.9809...
2 등용로6길 65 4 [[126.93344854600002, 37.50607434300002], [126...
3 여의대방로10가길 249 6 [[126.91484482600004, 37.493450825000025], [12...
4 국사봉1나길 147 4 [[126.93478510399996, 37.49486913700002], [126...

이제, 도로폭에 따라 색과 굵기를 달리하여 시각화 해보겠습니다.

df['정규화도로폭'] = df['도로폭'] / df['도로폭'].max()
layer = pdk.Layer(
    'PathLayer',
    df,
    get_path='geometry',
    get_width='도로폭',
    get_color='[255, 255 * 정규화도로폭, 120]',
    pickable=True,
    auto_highlight=True
)

center = [126.950, 37.495]
view_state = pdk.ViewState(
    longitude=center[0],
    latitude=center[1],
    zoom=12)

r = pdk.Deck(layers=[layer], initial_view_state=view_state)
r.show()

Trips Layer

Trips Layer 는 Path Layer 와 동일하되 시간의 개념이 들어갑니다.
즉, [시간지점, 위도, 경도] 를 담고 있는 데이터를 입력으로 주면 시간에 따라 어떻게 움직이는지 시각화 해줍니다.

쥬피터에서 구현이 다소 복잡할 것 같아, 여기서는 이런게 있다는 정로도만 남겨두겠습니다.
어떤건지 보고싶으신 분은 여기서 확인하실 수 있습니다.


5. 맵 스타일링

5.1. 레이어별 스타일링과 파라미터

기본적으로 맵 스타일링은, 각 레이어별로 해야합니다.
위에서 보셨다 싶이, 색상은 항상 rgba (0~255값) 으로 해야하고,
그 외 파라미터들은 레이어마다 다릅니다.

각 레이어별 정확한 파라미터들은 deck.gl 공식 도큐먼트에서 확인하실 수 있습니다.
다만, 도큐먼트는 자바스크립트 기준으로 쓰인거라 파이썬에서는 조금 다릅니다.

  • camelCased 이 아닌 snake_cased 으로 바꿔야 합니다.
    • 예를 들어, deck.gl 에서 getPosition 이라는 파라미터는 pydeck 에서는 get_position 으로 쓰입니다.
    • 자바스크립트에서는 변수규칙을 camelCased 쓰는데, 파이썬에서는 snake_cased 규칙을 써서 그렇습니다.
  • 레이어 타입 이름은 deck.gl 과 동일합니다.
    • 예를 들어, deck.gl 에서 ArcLayer 는 pydeck 에서도 ArcLayer 입니다.

5.2. 베이스맵

pdk.Deck 클래스 내 map_style 값을 다르게 줌으로써 수정이 가능합니다.
미리 지정된 값들은 다음이 있습니다.

- mapbox://styles/mapbox/streets-v11
- mapbox://styles/mapbox/outdoors-v11
- mapbox://styles/mapbox/light-v10
- mapbox://styles/mapbox/dark-v10
- mapbox://styles/mapbox/satellite-v9
- mapbox://styles/mapbox/satellite-streets-v11
- mapbox://styles/mapbox/navigation-preview-day-v4
- mapbox://styles/mapbox/navigation-preview-night-v4
- mapbox://styles/mapbox/navigation-guidance-day-v4
- mapbox://styles/mapbox/navigation-guidance-night-v4

예를 들어, mapbox://styles/mapbox/outdoors-v11 로 값을 주면 다음과 같이 바뀝니다.

center = [126.950, 37.495]
view_state = pdk.ViewState(
    longitude=center[0],
    latitude=center[1],
    zoom=12)

r = pdk.Deck(layers=[], 
             initial_view_state=view_state,
             map_style='mapbox://styles/mapbox/outdoors-v11')
r.show()

map_style='mapbox://styles/mapbox/outdoors-v11'


6. 마무리

pydeck 에서 다루는 레이어는 얼추 대부분 다룬 듯 합니다.
세세한 스타일링 설정은 제가 모두 설명 못드릴만큼, pydeck 아니 deck.gl 자체가 조금 내용이 있습니다.
하지만, 위 정도의 설명이라면 당장 필요한 레이어는 써먹을 정도라고 생각이 드네요.
더 궁금하신 분들은 pydeck github 나 deck.gl 공식 사이트에서 추가적으로 확인하시면 좋을 것 같습니다.

드디어 pydeck 에 대한 설명이 끝났습니다.
이 외에 공간 데이터 시각툴로 Kepler.gl 도 있습니다.
이 역시 Uber 팀에서 만드는 매우 파워풀한 도구인데, Mapbox studio 와 비슷합니다. (오히려 상위 호환느낌 입니다.)
따로 포스팅하지는 않고, 이런게 있다 정도만 소개해드립니다.
코드보다 GUI 가 더 편하신 분들은 검색해서 찾아보시길 추천드립니다.

이 외에 공간데이터 시각화에 지속적으로 관심있으신 분들은 아래 블로그들을 구독하길 추천드립니다.
공간 데이터 분석을 통해, 로컬 인사이트(Location Inteligence) 를 얻는 과정들이 종종 포스팅되고 있는 블로그들입니다.

반응형
  • dq 2020.02.11 14:53

    좋은 자료 감사합니다.
    혹시 PolygonLayer 관련해서 색상을 지정할 때 transparent 비율도 지정할 수 있나요?
    모두 불투명해져서 지도 label 을 가립니다.
    그리고 마우스 오버시 박스가 출력되는 데, 항목들을 지정할 수 있나요? 필드가 모두 나오니까 복잡합니다. ^^
    공식 사이트 들어가서 단서를 찾으려 했으나 실패했습니다. 혹시 아신다면 알려주시면 감사하겠습니다.
    고맙습니다.

    • BlogIcon 흠시 2020.02.11 16:16 신고

      색상 설정은 대부분 get_color 파라미터로 rgba 값을 받습니다. 예를 들면 (255,128,255,0.3) 이런 식입니다. 마지막 a 가 투명도 이므로 이 값을 바꿔주시면 투명도 조절이 될 듯 합니다. 투명도 범위가 0~1 이었는지 0~255 였는지 지금 잘 기억이 안나는데, 두개 다 시도해보셔요 ㅎㅎ

    • BlogIcon 흠시 2020.02.11 16:20 신고

      tooltip 설정은 마찬가지로 tooltip 이란 파라미터로 받는데요.

      https://deckgl.readthedocs.io/en/latest/tooltip.html

      여기 보시면 자세한 설명이 나와있습니다 :)

  • 지도왕 2020.02.27 12:26

    지도 뒤에 배경이 나오지 않네요. ㅜㅜ
    정말 똑같이 따라 한 것 같은데요.

    • BlogIcon 흠시 2020.02.27 19:43 신고

      https://github.com/heumsi/geo_data_visualisation_introduction

      에 가시면, 위에 작성한 코드들이 포함된 노트북을 다운받으실 수 있습니다 ㅎㅎ
      해당 노트북 돌려보시면서 확인해보시는 건 어떠신가요?!

  • 초보 2020.03.13 12:45

    고퀄 강의급 자료 감사합니다.
    정리도 깔끔 요약도 심플.. 관련자료 많지 않은 '지도 데이터 시각화 분야'의 오아시스네요!!

    질문 하나 드리면 혹시 지도데이터를 다운 받은후에 폐쇄망에서도 구현이 가능 할까요?

    • BlogIcon 흠시 2020.03.13 13:58 신고

      아무래도 basemap (데이터 뒤에 기본적으로 깔리는 지도) 이 open street map 의 데이터를 웹에서 가져오는거다보니, 폐쇄망에서 하면 basemap 이 작동안할듯 하네요.

  • BlogIcon study&grow 2020.04.12 16:09 신고

    polygon layer에서 높이주는 부분이 아무리 해도 안되네요..

    layer.get_elevation = '인구';

    요 부분에 대해 좀 더 알 수 있을까요?

    • BlogIcon 흠시 2020.04.12 21:48 신고

      안녕하세요.

      https://github.com/heumsi/geo_data_visualisation_introduction

      위 링크로 가시면 포스트 내용을 담은 쥬피터 노트북을 확인하실 수 있습니다.
      뭔가 잘 안될 때, 위 깃허브 레포를 클론 받으셔서, 직접 돌려보시면 어떤 부분이 잘못됐는지 빠르게 아실 수 있으실 거에요 ㅎㅎ

  • 감사합니다 2020.04.23 14:56

    안녕하세요. 우선 좋은 내용 정리해주셔서 너무 감사드립니다.

    게시글 참고하여 따라가던 중, r.show() 이부분을 실행하면 지도 데이터 대신
    DeckGLWidget(json_input='{"initialViewState": {"bearing": 15, "latitude": 37.565, "longitude": 126.986, "pitch…
    와 같이 글 한줄만 나오는데, 작성자분 깃헙에 있는 쥬피터 노트북으로도 동일한 결과만 얻어집니다.
    그래서 r.to_html()로 내리면 그제서야 보이지만, 위의 지도왕님께서 남긴 문의글 처럼, 뒷 배경이 나오지 않습니다.
    혹시 결과가 글 한줄로만 나오는것, 배경이 나오지 않는것, 이부분 어떻게 해결하는지 아실까요?

    • BlogIcon 흠시 2020.04.23 22:16 신고

      안녕하세요.
      저는 이전에 올린 깃허브 repo 를 방금 git clone 받아 실행시켜보니, 잘 동작을 하고, 맵도 잘 뜨고 있습니다.
      (최신 버전 0.3.1 으로 up 했지만 이전에 되던게 안되지는 않습니다.)

      혹시 mapbox api 토큰 키를 설정 안하신게 아닐까요??

    • BlogIcon ㄴㅅㅁ 2020.09.19 13:28

      저도 r.show() 이부분을 실행하면

      Out[16]: DeckGLWidget(google_maps_key=None, json_input='{"initialViewState": {"bearing": -27.36, "latitude": 52.2323, "longitude": -1.415, "maxZoom": 15, "minZoom": 5, "pitch": 40.5, "zoom": 6}, "layers": [{"@@type": "HexagonLayer", "autoHighlight": true, "coverage": 1, "data": "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv", "elevationRange": [0, 3000], "elevation

      등등등.. 이 나오고 지도가 안 나오는데..
      혹시 해결하셨나요?

    • BlogIcon 궁그미 2021.01.16 05:30

      저도 같은 증상인데요.
      토큰문제인 것 같습니다.

      참고로 환경변수는
      os.environ["MAPBOX_API_KEY"]='토큰값'
      이렇게 추가했어요(환경상, 특정웹사이트에서 제공하는 쥬피터노트북을 사용하고 있어서, 제가 터미널에 직접 추가하는건 어려울것같습니다 ㅠㅠ)
      os.getenv로 확인해보면 제대로 환경변수는 설정되어있는것같아요.
      근데 왜 api 세팅안됐다고 문제가되는걸까요 ?

      이전 포스팅(mapboxxgl)을보니, 실제 패키지 함수를 사용하실때 access_token=token 이렇게 지정해두셨던데, 혹시 pydeck도 그렇게 토큰변수값을 줘야하는 함수가 있는지요.. ?
      pydeck 예제 그대로 코드 수행해도 똑같은 문제가 발생하는데..

      (+)
      https://deckgl.readthedocs.io/en/latest/deck.html
      여기서 보면 pdk.Deck() 함수를 써야할때 변수를 api_keys 를 줘야하는것같은데... 막상
      r = pdk.Deck(layers=[layer], initial_view_state=view_state, api_keys={'mapbox':token})
      이렇게 시도해보면
      TypeError: __init__() got an unexpected keyword argument 'api_keys'
      타입에러가 납니다 ㅜㅜ

    • BlogIcon 나그네 2021.01.25 00:12

      r = pdk.Deck(layers=[layer],
      mapbox_key = '자신의 토큰키',
      initial_view_state=view_state)
      r.to_html()
      이렇게 하시면 되는 것 같습니다 ㅎㅎ

  • 사랑해요 2020.04.29 16:05

    안녕하세요, 예전에 페이스북 그룹에서 소개글 보고 방문했다가
    최근 분석 프로젝트가 생겨 다시 찾아뵙습니다.

    저는 전국 단위로 진행했는데, folium에서 잘 뜨던게 해당 라이브러리에서는 적용이 안되더라고요..
    그래도 HexagonLayer로 대체해서 시각화에는 문제가 없게 진행했습니다.

    여튼.. 너무 감사해요.
    문돌이도 이해갈 수 있게 쉽게 작성해주셔서 이해하는데 문제가 없었습니다.
    좋은 나날만 가득하세요 🙇‍♀️

  • BlogIcon 혀기쥰 2020.10.07 16:53 신고

    안녕하세요, 좋은 글들이 너무 많아 계속 보다가 여기에 댓글 답니다.
    처음으로 데이터 시각화 관련 툴을 만들게 되어, 어렵게어렵게 folium으로 꾸역꾸역 완성해 나가고 있습니다..
    뭐든 새로 시작하기 힘들기 마련이지만 여기서 많은 공부와 도움을 받게 되었습니다.

    감사합니다..
    항상 행복한 날들만 되길 바랍니다~

  • 정재호 2020.10.26 14:53

    안녕하세요.
    우선 여러 복잡한 내용을 상세하게 설명해주시고 코드도 공유해주셔서 감사합니다. 많이 배울 수 있을 것 같습니다.

    제가 궁금한 게 있는데, 하나 여쭤봐도 될까요?
    제가 동별 노령인구수를 시각화하고 싶은데요, 저는 전국의 행정동을 나타내는 geojson 파일이 있고, 이와 별개로 동별 인구수 csv 파일이 있는 상황입니다. csv 파일에는 구, 동 등의 행정코드가 있고, geojson에도 행정동 코드가 있는데, 이 경우 우선 geojson파일과 csv 파일을 하나로 합쳐야 하나요?
    인터넷으로 파이썬 dataframe과 geojson을 하나로 합치는 방법을 찾아봤는데 없어서요..

    앞선 folium을 사용한 예제에서는 개별 파일을 key로 연결해서 시각화 한 것 같은데요.. 이런 방법을 pydeck에서도 사용할 수 있을까요?

    감사합니다!


    • BlogIcon 흠시 2020.10.26 17:21 신고

      geopandas 를 이용해서 두 파일 모두 dataframe 으로 불러온 뒤, 원하시는 후처리 (csv 혹은 geojson 으로 떨구는 작업) 을 하실 수 있을 거 같네요!

      geopandas 에 대해 찾아보셔요 ㅎㅎ

  • 질문 2020.11.17 15:56

    안녕하세요, 올려주신 포스팅 잘 보았습니다 감사합니다! 많은 도움이 되었습니다!!
    궁금한 것이 있는데요, CPUGridLayer나 HexagonLayer, ScreenGridLayer등의 레이어에서
    사용하는 단위도형의 면적을 알 수 있는 방법이 있을까요?

  • 김하늘 2020.11.25 12:38

    안녕하세요, 좋은 튜토리얼 감사합니다. 혹시 polygonLayer 와 ArcLayer 를 설정하는데 특정 Polygon을 누른다면 거기의 source, target arc 들만 보이게 할수 있는방법 아시나요?

  • 자낳괴 2021.03.05 17:20

    'Polygon' object is not subscriptable

    폴리곤과 멀티폴리곤이 섞여있는 geometry 를
    정의하신 함수로 x,y 분리 시도하고있습니다.
    왜인지 폴리곤을 처리하고 있지 못한듯한데 이유를 알수 있을까요?

  • 뭥먹개발자 2021.04.15 17:12

    안녕하세요
    정말 큰 도움을 받고 있는 개발자(?) 입니다.
    블로그 정리잘해주셔서 정말 자주 보고 있습니다.
    원래 이쪽분야가 아닌데...ㅜㅜ
    다름이 아니라 작성해주신 예제중에 path 그리는것 관려하여 궁금해서 글남겨요..ㅠㅠ
    아무리 찾아봐도 도로 위치정보가 예제에 들어가있는 'dongjak_road.geojson' 와 같은 형태의 데이터를 공공데이터로 만들어져 있지 않더군요..ㅠㅠ
    혹시 서울시 공공데이터 중 '도로구간 위치정보'를 후처리하여 만드신 파일인가요?
    만약 후처리를 하셨다면 어떻게 하신지 궁금해서 글 남깁니다.
    감사합니다.ㅠ

    • BlogIcon 흠시 2021.04.15 17:21 신고

      오래 전에 한거라 정확히 기억은 안나지만 geojson 은 일반적인 지도 데이터 타입이라 흔하게 보실 수 있습니다

      아마 공공 데이터엔 shp 파일 헝태로 많이 나와있을텐데 shp 파일을 geojson 으로 바꾸는 방법을 찾아보셔요 ㅎㅎ 금방 나올거에요!

    • BlogIcon 뭥먹개발자 2021.04.16 11:37

      감사합니다!
      찾아보니 SHP를 geojson으로 변환할수 있네요.
      감사합니다!

  • BlogIcon tyhlife 2021.05.06 14:22 신고

    너무 잘 읽었습니다 ~ ! 역시 갓하디 !
    - ㅋㄹㅅㅌㅍ-

  • 아무개 2021.10.05 14:48

    안녕하세요! 정리가 잘되어서 도움많이 받았습니다!!

    혹시.. 제가 지금 쓰고있는 데이터가 이런 형태인데,

    source target weight lat_x long_x lat_y long_y
    11110 11140 200 37.583744 126.9838 37.561872 126.989789

    데이터가 50개가 넘어가면 지도에 O/D Line이 안그려집니다.. 50개 미만이면 잘 나오고요..왜그런지 혹시 추측가능할까요?

    CSV파일을 사용하고있구요 별로크지 않은거 같은데..