Pydeck Multipolygon 시각화 on Streamlit

Intro

Pydeck 은 JS의 지도 시각화 툴 deck.gl 의 파이썬 implementation 으로, 하나의 지도에 Layer 을 원하는 만큼 쌓아 보다 정교한 커스터마이징을 지원해주는 라이브러리다.

파이썬에서의 대표적인 시각화 라이브러리로 mapboxgl, folium 등을 고를 수 있지만 둘다 정식런칭된지 오래되었기 때문에 GUI 가 pydeck 보다 상대적으로 떨어진다.

하지만 pydeck 은 비교적 신상 라이브러리고 기본 base 가 JS 이기 때문에 documentation 이 상대적으로 부족하다는 단점도 있다. 그렇기 때문에 인자들에 대한 충분한 설명이 없어 처음 접하게 되면 꽤나 애를 먹을 수 있다.

나는 streamlit 을 이용해 웹앱을 만들며 pydeck 을 사용했기 때문에, 그저 jupyter 에서 pydeck 을 이용하려는 사용자들은

pip install pydeck

을 해주면 되지만, streamlit 의 웹서버에 임베딩하려면

pip install pydeck==0.5.0

을 추천한다. Pydeck 0.6.0 부터 지원하는 지도 api 가 mapbox 에서 openmaps 로 바뀌며 streamlit 프론트 상에서 예상치 못한 오류들이 뜨기 때문이다.

Main Function

Pydeck 의 핵심 함수들은 Layer 과 ViewState 다 (JS implementation 이라 그런지 함수명이 camel case 다).

def polygon():

    df = pd.read_pickle('data_upload/sales_eda_full.pkl') #전체 매출 Data
    seoul = pd.read_pickle('data_upload/seoul_coord_data.pkl') #서울시 동별 Multipolygon GeoJson 정보
    colname = st.selectbox('보고싶은 Data 를 선택하세요:', colnames)

    adder = []
    for area in areas:
        adder.append(df.loc[df['temp'] == area, colname].sum())  # 변수명을 기준으로 각 지역의 값을 합친다.

    df_new = pd.DataFrame()
    df_new['temp'] = areas
    df_new[colname + '_new'] = adder
    df_new = pd.merge(df_new, seoul, on='temp')

    if '매출_금액' in colname:
        df_new[f'{colname}_new_per_payment'] = (df_new[f'{colname}_new'] / seoul['분기당_매출_건수_full']) / seoul['점포수_full'] #매출금액 데이터는 해당 동의 매출건수와 점포수로 나눠준다
        df_new[f'{colname}_정규화'] = df_new[f'{colname}_new_per_payment'] / df_new[f'{colname}_new_per_payment'].max() #그리고 정규화

    elif '매출_건수' in colname:
        df_new[f'{colname}_new_per_store'] = df_new[f'{colname}_new'] / seoul['점포수_full'] #매출건수 데이터는 해당 동의 점포수로 나눠준다
        df_new[f'{colname}_정규화'] = df_new[f'{colname}_new'] / df_new[f'{colname}_new'].max() #정규화

    else:
        df_new[f'{colname}_정규화'] = df_new[f'{colname}_new'] / df_new[f'{colname}_new'].max() #매출비율 데이터는 바로 정규화

    layer = pdk.Layer(
        'PolygonLayer',
        df_new,
        get_polygon='coordinates',
        get_fill_color=f'[0, 1000*{colname}_정규화 ,0]',
        pickable=True,
        auto_highlight=True,
        extruded=True,

    )

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

        )

    tooltip = {
        "html": "<b>{temp}</b>, 분기당 매출 건수:<b>{분기당_매출_건수_full}</b>, 점포수: <b>{점포수_full}",
        "style": {"background": "grey", "color": "white", "font-family": '"Helvetica Neue", Arial', "z-index": "10000"},
    }


    r = pdk.Deck(layers=[layer],
                 map_provider='mapbox',
                 tooltip=tooltip,
                 initial_view_state=view_state)
    return r


if option == "동별 Polygon 분석":
    st.pydeck_chart(polygon())

Data Preprocessing

본인이 웹엡에 사용한 함수를 예시로 설명을 진행하자면, 먼저 두개의 .pkl 에서 데이터를 불러온다.

df 에는 서울의 각 점포들의 매출 비율, 매출 금액, 매출 건수들을 담고 있으며 seoul 에는 행정동의 Multipolygon 을 담고 있다. 두 데이터프레임의 PK 는 ‘temp’ 라는 column 인데, str 값으로 행정동 명칭을 담고 있다 (ex. ‘종로구 사직동’).

df = pd.read_pickle('data_upload/sales_eda_full.pkl') #전체 매출 Data
seoul = pd.read_pickle('data_upload/seoul_coord_data.pkl') #서울시 동별 Multipolygon GeoJson 정보
colname = st.selectbox('보고싶은 Data 를 선택하세요:', colnames)

adder = []
for area in areas:
    adder.append(df.loc[df['temp'] == area, colname].sum())  # 변수명을 기준으로 각 지역의 값을 합친다.

df_new = pd.DataFrame()
df_new['temp'] = areas
df_new[colname + '_new'] = adder
df_new = pd.merge(df_new, seoul, on='temp')

if '매출_금액' in colname:
    df_new[f'{colname}_new_per_payment'] = (df_new[f'{colname}_new'] / seoul['분기당_매출_건수_full']) / seoul['점포수_full'] #매출금액 데이터는 해당 동의 매출건수와 점포수로 나눠준다
    df_new[f'{colname}_정규화'] = df_new[f'{colname}_new_per_payment'] / df_new[f'{colname}_new_per_payment'].max() #그리고 정규화

elif '매출_건수' in colname:
    df_new[f'{colname}_new_per_store'] = df_new[f'{colname}_new'] / seoul['점포수_full'] #매출건수 데이터는 해당 동의 점포수로 나눠준다
    df_new[f'{colname}_정규화'] = df_new[f'{colname}_new'] / df_new[f'{colname}_new'].max() #정규화

else:
    df_new[f'{colname}_정규화'] = df_new[f'{colname}_new'] / df_new[f'{colname}_new'].max() #매출비율 데이터는 바로 정규화

사용자가 웹앱 상 보고싶은 정보를 선택하면, adder 란 함수가 행정동을 기준으로 해당 정보의 값들을 전부 더하고, 두개의 데이터프레임을 선택한 정보에 따라 행정동별 값과 해당 행정동의 Multipolygon 을 담은 새로운 데이터프레임을 return 한다. 그리고 해당 데이터프레임으로 pydeck 의 시각화를 진행하였다.

Layers

layer = pdk.Layer(
    'PolygonLayer',
    df_new,
    get_polygon='coordinates',
    get_fill_color=f'[0, 1000*{colname}_정규화 ,0]',
    pickable=True,
    auto_highlight=True,
    extruded=True,

)

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

    )

tooltip = {
    "html": "<b>{temp}</b>, 분기당 매출 건수:<b>{분기당_매출_건수_full}</b>, 점포수: <b>{점포수_full}",
    "style": {"background": "grey", "color": "white", "font-family": '"Helvetica Neue", Arial', "z-index": "10000"},
}


r = pdk.Deck(layers=[layer],
                map_provider='mapbox',
                tooltip=tooltip,
                initial_view_state=view_state)
return r

Pydeck 의 main 시각화 함수인 Layer 과 ViewState 를 이용한 모습이다. ‘PolygonLayer’ 은 해당 layer 의 인자명이며, 원하는 지도를 documentation 을 참고하며 바꿀 수 있다. get_fill_color 이란 인자는 행정동의 Multipolygon 의 색을 담당하는데, 데이터프레임 상으로 값들이 정규화가 되어있지 않으면 유의미한 색 차이가 없다.

ViewState 함수는 지도의 시작점을 정하는 함수다. 본인은 서울시의 위/경도를 center 라는 변수에 정의하고, 위도와 경도를 indexing 한 값으로 ViewState 의 인자들을 정의하였다.

tooltip 이란 사용자가 마우스 커서를 행정동 위에 올리면 원하는 정보를 동적으로 표시하는 tool이다. 원하는 인자들을 b 태그에 정의하면 된다.

그리고 마지막으로 layer 와 ViewState 를 Deck 이란 함수에 넣어 return 한다. 잘 보면 layers 라는 인자는 리스트값으로 정의되는데, 이는 pydeck 가 여러개의 layer 를 인자로 받을 수 있다는 뜻이다. 하나의 지도에 원하는 만큼 Layer 들을 정의하여 시각화 할 수 있다 (하지만 streamlit 의 서버상 4개 이상은 감당이 힘들어보인다).

Streamlit 시각화

if option == "동별 Polygon 분석":
    st.pydeck_chart(polygon())

마지막으로 streamlit 상에서 해당 deck 을 리턴하려면, pydeck_chart 라는 함수에 넣어 반환해야 한다. 만약 streamlit 이 아닌 단순 jupyter 환경에서 한다면, return r 대신 r.show() 를 사용하면 된다.

전체 결과물과 코드는

에서 확인 할 수 있다.