supp-5: 기말고사 풀이

Author

최규빈

Published

December 19, 2023

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.io as pio
import json
import requests
import pickle
pd.options.plotting.backend = "plotly"
pio.templates.default = "plotly_white"

1. NYCTaxi 자료 분석 (dashboard) – 100점

아래는 NYCTaxi자료에서 기본적인 전처리를 수행한 데이터프레임이다.

df = pd.read_csv("https://raw.githubusercontent.com/guebin/DV2023/main/posts/NYCTaxi.csv").assign(
    log_trip_duration = lambda df: np.log(df.trip_duration),
    pickup_datetime = lambda df: df.pickup_datetime.apply(pd.to_datetime),
    dropoff_datetime = lambda df: df.dropoff_datetime.apply(pd.to_datetime),
    dist = lambda df: np.sqrt((df.pickup_latitude-df.dropoff_latitude)**2 + (df.pickup_longitude-df.dropoff_longitude)**2),
    #---#
    vendor_id = lambda df: df.vendor_id.map({1:'A',2:'B'})
).assign(
    speed = lambda df: df.dist / df.trip_duration,
    pickup_hour = lambda df: df.pickup_datetime.dt.hour,
    dropoff_hour = lambda df: df.dropoff_datetime.dt.hour,
    dayofweek = lambda df: df.pickup_datetime.dt.dayofweek
)
df_small = df[::100].reset_index(drop=True)

주어진 자료를 이용하여 (1)-(3)에 해당하는 시각화를 대시보드로 구현하고, 홈페이지를 남겨라. 답안 예시는 아래와 같다.

(답안예시)

주의사항

  • 제출시간 이후에 대시보드 생성을 시도할 경우 부정행위로 간주하여 기말고사 전체를 0점처리함. (git에 기록남아있음)
  • 대시보드 구현이 되어있지 않은 경우 0점 처리함
  • xaxis, yaxis, lengend 등을 조정하는 것이 문제의도임. (구현되어있지 않으면 0점처리함)

힌트

아래와 같은 양식으로 qmd를 만들면 그림이 중복되어 출력되는 문제1가 발생하지 않음.

  • 1 현재 quarto dashboard가 불완전(정식버전이 아니라 prereleased version임)하여 생기는 버그인듯합니다

  • NYCTaxi.qmd
    ---
    title: "NYCTaxi"
    author: "최규빈"
    format: dashboard
    execute: 
      enabled: true
      cache: false
      freeze: false
    ---
    
    ```{python}
    #| output: false
    # 여기에 온갖코드를 넣음.
    # 1-(1),(2),(3) 에 대응하는 plotly figure를 아래와 같은 이름으로 저장
    # fig1 = ...
    # fig2 = ...
    # fig3 = ...
    ```
    
    # 기말고사1-(1),(2)
    
    ```{python}
    #| title: 요일,시간에 따른 평균속력 시각화        
    fig1.show()
    ```
    
    ```{python}
    #| title: 요일,시간에 따른 평균이동거리 시각화        
    fig2.show()
    ```
    
    # 기말고사1-(3)
    
    ```{python}
    #| title: 속력별 경로 시각화
    fig3.show()
    ```

    (1) 요일,시간에 따른 평균속력 시각화 – 25점

    자료 df에서 시간에 따른 평균속력을 구하고 이를 대시보드에 시각화하라.

    README

    • 요일은 {0:'월',1:'화',2:'수',3:'목',4:'금',5:'토',6:'일'}의 규칙에 따라 변환할 것

    (2) 요일,시간에 따른 평균이동거리 시각화 – 25점

    자료 df에서 시간에 따른 평균이동거리를 구하고 이를 대시보드에 시각화하라.

    README

    • 요일은 {0:'월',1:'화',2:'수',3:'목',4:'금',5:'토',6:'일'}의 규칙에 따라 변환할 것

    (3) 속력별 경로시각화 – 50점

    자료 df_small에서 속력을 quatile에 따라 4개의 구간으로 나누고, 구간별 이동경로를 대시보드에 시각화하라.

    README

    • Zoom = 11 로 설정할것. Figure의 width, height는 설정하지 말것
    • 기타 설정값에 대해서는 궁금한것이 있다면 질문할 것

    힌트:

    힌트1: 아래의 코드를 관찰하세요.

    speed = pd.Series([1,1,2,2,3,3,4,4])
    print(pd.qcut(speed,4))
    print(pd.qcut(speed,4,labels=['매우느림','조금느림','조금빠름','매우빠름']))
    0    (0.999, 1.75]
    1    (0.999, 1.75]
    2      (1.75, 2.5]
    3      (1.75, 2.5]
    4      (2.5, 3.25]
    5      (2.5, 3.25]
    6      (3.25, 4.0]
    7      (3.25, 4.0]
    dtype: category
    Categories (4, interval[float64, right]): [(0.999, 1.75] < (1.75, 2.5] < (2.5, 3.25] < (3.25, 4.0]]
    0    매우느림
    1    매우느림
    2    조금느림
    3    조금느림
    4    조금빠름
    5    조금빠름
    6    매우빠름
    7    매우빠름
    dtype: category
    Categories (4, object): ['매우느림' < '조금느림' < '조금빠름' < '매우빠름']

    힌트2: 1-(3)에 해당하는 그림을 fig3로 저장한후 아래의 코드를 관찰하세요

    for i in range(150):
        print(fig3.data[i].mode)

    이를 이용하여 legend를 수정하는 방법을 생각해보세요.

    (풀이)

    2. 에너지사용량 (지리정보시각화) – 50점

    아래는 대한민국의 행정구역을 나타내는 json 파일과 2018-2021 기간동안의 에너지사용량이 저장된 url들이다.

    # Json
    https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-provinces-2018-geo.json
    https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-municipalities-2018-geo.json
    # DataFrame
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Seoul2018.csv
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Seoul2019.csv
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Seoul2020.csv
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Seoul2021.csv
    ...
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Busan2018.csv
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Busan2019.csv
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Busan2020.csv
    https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/Busan2021.csv

    주어진 자료를 활용하여 아래의 물음에 답하라.

    주의사항

    • ‘전주시완산구’,’완산구’와 같은 지역명은 ’전주시-완산구’와 같은 양식으로 정리하라.
    • 인천광역시 남구는 새로운 이름인 미추홀구로 변경하라.

    힌트

    문제가 되는 지역명을 정리하면 아래와 같다.

    s = pd.Series(['인천광역시-미추홀구',
                   '경기도-고양시-덕양구','경기도-고양시-일산동구','경기도-고양시-일산서구',
                   '경기도-성남시-분당구','경기도-성남시-수정구','경기도-성남시-중원구',
                   '경기도-수원시-권선구','경기도-수원시-영통구', '경기도-수원시-장안구', '경기도-수원시-팔달구',
                   '경기도-안산시-단원구', '경기도-안산시-상록구',
                   '경기도-안양시-동안구', '경기도-안양시-만안구',
                   '경기도-용인시-기흥구', '경기도-용인시-수지구', '경기도-용인시-처인구',
                   '경상남도-창원시-마산합포구', '경상남도-창원시-마산회원구', '경상남도-창원시-성산구', '경상남도-창원시-의창구', '경상남도-창원시-진해구',
                   '경상북도-포항시-남구', '경상북도-포항시-북구',
                   '전라북도-전주시-덕진구', '전라북도-전주시-완산구',
                   '충청남도-천안시-동남구', '충청남도-천안시-서북구',
                   '충청북도-청주시-상당구', '충청북도-청주시-서원구', '충청북도-청주시-청원구', '충청북도-청주시-흥덕구'])

    (사전풀이)

    dct1 = {f'{prov}-{district}':f'{prov}-{city}-{district}' for prov,city,district in s[1:].str.split('-')}
    dct2 = {f'{prov}-{city}{district}':f'{prov}-{city}-{district}' for prov,city,district in s[1:].str.split('-')}
    dct2['인천광역시-남구'] = '인천광역시-미추홀구'
    print(dct1)
    {'경기도-덕양구': '경기도-고양시-덕양구', '경기도-일산동구': '경기도-고양시-일산동구', '경기도-일산서구': '경기도-고양시-일산서구', '경기도-분당구': '경기도-성남시-분당구', '경기도-수정구': '경기도-성남시-수정구', '경기도-중원구': '경기도-성남시-중원구', '경기도-권선구': '경기도-수원시-권선구', '경기도-영통구': '경기도-수원시-영통구', '경기도-장안구': '경기도-수원시-장안구', '경기도-팔달구': '경기도-수원시-팔달구', '경기도-단원구': '경기도-안산시-단원구', '경기도-상록구': '경기도-안산시-상록구', '경기도-동안구': '경기도-안양시-동안구', '경기도-만안구': '경기도-안양시-만안구', '경기도-기흥구': '경기도-용인시-기흥구', '경기도-수지구': '경기도-용인시-수지구', '경기도-처인구': '경기도-용인시-처인구', '경상남도-마산합포구': '경상남도-창원시-마산합포구', '경상남도-마산회원구': '경상남도-창원시-마산회원구', '경상남도-성산구': '경상남도-창원시-성산구', '경상남도-의창구': '경상남도-창원시-의창구', '경상남도-진해구': '경상남도-창원시-진해구', '경상북도-남구': '경상북도-포항시-남구', '경상북도-북구': '경상북도-포항시-북구', '전라북도-덕진구': '전라북도-전주시-덕진구', '전라북도-완산구': '전라북도-전주시-완산구', '충청남도-동남구': '충청남도-천안시-동남구', '충청남도-서북구': '충청남도-천안시-서북구', '충청북도-상당구': '충청북도-청주시-상당구', '충청북도-서원구': '충청북도-청주시-서원구', '충청북도-청원구': '충청북도-청주시-청원구', '충청북도-흥덕구': '충청북도-청주시-흥덕구'}
    print(dct2)
    {'경기도-고양시덕양구': '경기도-고양시-덕양구', '경기도-고양시일산동구': '경기도-고양시-일산동구', '경기도-고양시일산서구': '경기도-고양시-일산서구', '경기도-성남시분당구': '경기도-성남시-분당구', '경기도-성남시수정구': '경기도-성남시-수정구', '경기도-성남시중원구': '경기도-성남시-중원구', '경기도-수원시권선구': '경기도-수원시-권선구', '경기도-수원시영통구': '경기도-수원시-영통구', '경기도-수원시장안구': '경기도-수원시-장안구', '경기도-수원시팔달구': '경기도-수원시-팔달구', '경기도-안산시단원구': '경기도-안산시-단원구', '경기도-안산시상록구': '경기도-안산시-상록구', '경기도-안양시동안구': '경기도-안양시-동안구', '경기도-안양시만안구': '경기도-안양시-만안구', '경기도-용인시기흥구': '경기도-용인시-기흥구', '경기도-용인시수지구': '경기도-용인시-수지구', '경기도-용인시처인구': '경기도-용인시-처인구', '경상남도-창원시마산합포구': '경상남도-창원시-마산합포구', '경상남도-창원시마산회원구': '경상남도-창원시-마산회원구', '경상남도-창원시성산구': '경상남도-창원시-성산구', '경상남도-창원시의창구': '경상남도-창원시-의창구', '경상남도-창원시진해구': '경상남도-창원시-진해구', '경상북도-포항시남구': '경상북도-포항시-남구', '경상북도-포항시북구': '경상북도-포항시-북구', '전라북도-전주시덕진구': '전라북도-전주시-덕진구', '전라북도-전주시완산구': '전라북도-전주시-완산구', '충청남도-천안시동남구': '충청남도-천안시-동남구', '충청남도-천안시서북구': '충청남도-천안시-서북구', '충청북도-청주시상당구': '충청북도-청주시-상당구', '충청북도-청주시서원구': '충청북도-청주시-서원구', '충청북도-청주시청원구': '충청북도-청주시-청원구', '충청북도-청주시흥덕구': '충청북도-청주시-흥덕구', '인천광역시-남구': '인천광역시-미추홀구'}
    global_dict = json.loads(requests.get('https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-provinces-2018-geo.json').text)
    local_dict = json.loads(requests.get('https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-municipalities-2018-geo.json').text)
    df_global = pd.DataFrame([l['properties'] for l in global_dict['features']]).drop(['base_year','name_eng'],axis=1)
    df_local = pd.DataFrame([l['properties'] for l in local_dict['features']]).drop(['base_year','name_eng'],axis=1)\
    .rename({'name':'name_local','code':'code_local'},axis=1)\
    .assign(code = lambda df: df.code_local.str[:2])
    df_json = pd.merge(df_local,df_global)\
    .assign(on = lambda df: df['name'] + '-' + df['name_local'])\
    .set_index('on').rename(dct2).reset_index()\
    .drop(['name_local','name'],axis=1)
    df_json
    on code_local code
    0 서울특별시-종로구 11010 11
    1 서울특별시-중구 11020 11
    2 서울특별시-용산구 11030 11
    3 서울특별시-성동구 11040 11
    4 서울특별시-광진구 11050 11
    ... ... ... ...
    245 경상남도-함양군 38380 38
    246 경상남도-거창군 38390 38
    247 경상남도-합천군 38400 38
    248 제주특별자치도-제주시 39010 39
    249 제주특별자치도-서귀포시 39020 39

    250 rows × 3 columns

    url = 'https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/{}.csv'
    prov = ['Seoul', 'Busan', 'Daegu', 'Incheon', 
            'Gwangju', 'Daejeon', 'Ulsan', 'Sejongsi', 
            'Gyeonggi-do', 'Gangwon-do', 'Chungcheongbuk-do', 
            'Chungcheongnam-do', 'Jeollabuk-do', 'Jeollanam-do', 
            'Gyeongsangbuk-do', 'Gyeongsangnam-do', 'Jeju-do']
    df = pd.concat([pd.read_csv(url.format(p+y)).assign(년도=y, 시도=p) for p in prov for y in ['2018', '2019', '2020', '2021']]).reset_index(drop=True)\
    .assign(년도 = lambda df: df.년도.astype(int))\
    .set_index(['년도','시도','지역']).applymap(lambda x: int(str(x).replace(',','')))\
    .reset_index()\
    .assign(시도 = lambda df: df.시도.map({l['properties']['name_eng']:l['properties']['name'] for l in global_dict['features']}))\
    .assign(on = lambda df: df.시도 + '-' + df.지역)\
    .set_index('on').rename(dct1).reset_index()\
    .drop(['지역','시도'],axis=1)
    display('df',df.head())
    df2 = pd.merge(df_json,df).assign(
        시도 = lambda df: df['on'].str.split('-').str[0],
        지역 = lambda df: df['on'].str.split('-').str[1:].str.join('-')
    ).drop('on',axis=1).set_index(['시도','지역']).reset_index()
    display('df2[200:210]',df2[200:210])
    display('df2[370:380]',df2[370:380])
    /tmp/ipykernel_1181513/17364776.py:9: FutureWarning:
    
    DataFrame.applymap has been deprecated. Use DataFrame.map instead.
    
    'df'
    on 년도 건물동수 연면적 에너지사용량(TOE)/전기 에너지사용량(TOE)/도시가스 에너지사용량(TOE)/지역난방
    0 서울특별시-종로구 2018 17929 9141777 64818 82015 111
    1 서울특별시-중구 2018 10598 10056233 81672 75260 563
    2 서울특별시-용산구 2018 17201 10639652 52659 85220 12043
    3 서울특별시-성동구 2018 14180 11631770 60559 107416 0
    4 서울특별시-광진구 2018 21520 12054796 70609 130308 0
    'df2[200:210]'
    시도 지역 code_local code 년도 건물동수 연면적 에너지사용량(TOE)/전기 에너지사용량(TOE)/도시가스 에너지사용량(TOE)/지역난방
    200 인천광역시 동구 23020 23 2018 5414 2094127 10459 18695 0
    201 인천광역시 동구 23020 23 2019 5157 2084655 10098 17304 0
    202 인천광역시 동구 23020 23 2020 5096 2080453 10053 17113 0
    203 인천광역시 동구 23020 23 2021 5015 2186129 10336 17704 0
    204 인천광역시 미추홀구 23030 23 2018 28305 14479403 67154 125211 2282
    205 인천광역시 미추홀구 23030 23 2019 27416 14515405 64888 115779 3161
    206 인천광역시 미추홀구 23030 23 2020 26646 14476580 63312 114153 2314
    207 인천광역시 미추홀구 23030 23 2021 25676 14830633 65328 116521 5337
    208 인천광역시 연수구 23040 23 2018 5982 15321162 70421 55642 47047
    209 인천광역시 연수구 23040 23 2019 6019 16083104 71719 53937 48334
    'df2[370:380]'
    시도 지역 code_local code 년도 건물동수 연면적 에너지사용량(TOE)/전기 에너지사용량(TOE)/도시가스 에너지사용량(TOE)/지역난방
    370 경기도 고양시-일산동구 31103 31 2020 11496 11754447 52447 33546 50595
    371 경기도 고양시-일산동구 31103 31 2021 11530 11882593 53565 33855 51300
    372 경기도 고양시-일산서구 31104 31 2018 6766 11412557 49505 28483 59278
    373 경기도 고양시-일산서구 31104 31 2019 6823 11722973 49429 27013 56862
    374 경기도 고양시-일산서구 31104 31 2020 6839 11738763 49913 27720 58026
    375 경기도 고양시-일산서구 31104 31 2021 6861 11779013 51686 27710 58009
    376 경기도 과천시 31110 31 2018 2290 2089132 11629 10145 8707
    377 경기도 과천시 31110 31 2019 2311 2099538 10883 9811 8387
    378 경기도 과천시 31110 31 2020 2264 2040791 9089 9695 8631
    379 경기도 과천시 31110 31 2021 2339 2615206 9984 9818 10977

    (1) 에너지사용량차이(전기-도시가스) 시각화 – 25점

    에너지사용량(TOE)/전기에너지사용량(TOE)/도시가스의 차이를 계산하여 에너지사용량차이(전기-도시가스)라는 새로운 열로 추가하라. 수도권지역에 한정하여 에너지사용량차이(전기-도시가스)를 시각화하라. 시각화를 위해 plotlychoropleth_mapbox를 사용하고, 다음 요구사항을 충족시켜라.

    • 색상은 에너지사용량차이(전기-도시가스) 값에 따라 달라져야 하며, 색상 범위(range_color)는 해당 열의 최소값과 최대값으로 설정하라.
    • 애니메이션 프레임은 년도를 기준으로 하라.
    • 호버 데이터는 시도지역을 포함해야 한다.
    • 투명도는 0.5로 설정하라.
    • 지도 스타일은 ’carto-positron’을 사용하며, 중심 좌표는 위도 37.5642135, 경도 127.0016985로 설정하라.
    • 지도의 줌 레벨은 7.5로, 높이는 800, 너비는 750으로 설정하라.
    • 스크롤 줌 기능은 비활성화하라.

    주의사항

    • 수도권지역은 서울,경기,인천을 의미한다.
    • 호버데이터시 ‘고양시-덕양구’와 같은 양식이 아니라 ’고양시덕양구’ 혹은 ’덕양구’와 같은 방식으로 호버될 경우 0점 처리함. (인천광역시의 미추홀구 역시 구지명(인천광역시 남구)로 호버될경우 0점처리함)

    (풀이)

    metro_dict = local_dict.copy() 
    metro_dict['features'] = [l for l in metro_dict['features'] if  (l['properties']['code'][:2] == '31' or l['properties']['code'][:2] == '23' or l['properties']['code'][:2] == '11')]
    #---#
    tidydata = df2.assign(diff = lambda df : df['에너지사용량(TOE)/전기']- df['에너지사용량(TOE)/도시가스'])\
    .rename({'diff':'에너지사용량차이(전기-도시가스)'},axis=1)\
    .query('시도 in ["서울특별시","경기도","인천광역시"]')
    tidydata
    #---# 
    range_color = tidydata['에너지사용량차이(전기-도시가스)'].min(), tidydata['에너지사용량차이(전기-도시가스)'].max()
    fig = px.choropleth_mapbox(
        geojson = metro_dict,
        featureidkey = 'properties.code',
        data_frame = tidydata,
        locations = 'code_local',
        color = '에너지사용량차이(전기-도시가스)',
        animation_frame= '년도',
        hover_data = ['시도','지역'],
        opacity = 0.5,
        #---#
        mapbox_style="carto-positron",
        range_color= range_color,
        center={"lat": 37.5642135, "lon": 127.0016985},
        zoom=7.5,
        height=800,
        width=750    
    )
    fig.show(config={'scrollZoom':False})

    (2) (서울+김포)특별시? – 25점

    김포시의 서울편입이슈에 대한 아래의 제시문을 읽으라.

    김포시의 서울편입 이슈

    김포시를 서울특별시로 편입하자는 주장에 대한 논의가 활발해졌다. 2023년 10월 23일, 국민의힘 소속 김포 지역 정치인들이 2026년 신설 예정인 경기북부특별자치도에 김포시가 포함되는 것에 반대하며 서울특별시 편입을 제안했다. 이 제안은 국민의힘 지도부의 호응을 받았고, 이로 인해 10월 30일에는 이 문제가 국민의힘 내에서 중요한 논의 주제로 부상했다. 이후, 이 주제는 제22대 국회의원 선거의 중요한 정치적 이슈로 자리 잡으며, 많은 논쟁과 대중의 관심을 불러일으켰다. 이러한 정치적 변화는 한국 정치 무대에서 논쟁의 새로운 중심점이 되었으며, 김포시의 행정적 미래에 대한 광범위한 토론을 촉발하고 있다.

    김포시가 서울시에 편입되었다고 가정하고, (서울+김포)특별시 내 각 구별 에너지사용비율을 아래의 수식을 참고하여 연도별로 계산하라.

    :::{.callout-note}

    2018년 김포시의 에너지사용비율, 2018년 서울시 서초구의 에너지사용비율은 각각 아래와 같이 계산한다.

    • \(\text{에너지사용비율}_{\text{김포시,2018}} = \frac{\text{에너지사용량}_\text{김포시,2018}}{\text{에너지사용량}_\text{김포시,2018}+\text{에너지사용량}_\text{서울시강남구,2018}+...+\text{에너지사용량}_\text{서울시중랑구,2018}}\)
    • \(\text{에너지사용비율}_{\text{서울시서초구,2018}} = \frac{\text{에너지사용량}_\text{서울시서초구,2018}}{\text{에너지사용량}_\text{김포시,2018}+\text{에너지사용량}_\text{서울시강남구,2018}+...+\text{에너지사용량}_\text{서울시중랑구,2018}}\)

    이때 에너지사용량은 전기,도시가스,지역난방의 합을 의미한다. 계산된 데이터를 바탕으로, 구별 에너시사용비율을 시각화 하라. 시각화를 위해 plotlychoropleth_mapbox를 사용하고, 다음 요구사항을 충족시켜라.

    • 색상은 ’에너지사용비율’에 따라 달라져야 하며, range_color에너시사용비율 열의 최소값과 최대값으로 설정하라.
    • 애니메이션 프레임은 ’년도’를 기준으로 설정하라.
    • 호버 데이터에는 ’시도’와 ’지역’을 포함시켜라.
    • 지도의 투명도는 0.5로 설정하라.
    • 지도 스타일은 ’carto-positron’을 사용하고, 중심 좌표는 위도 37.5612, 경도 126.8228로 설정하라.
    • 지도의 줌 레벨은 9로, 높이는 800, 너비는 750으로 설정하라.
    • 스크롤 줌 기능은 비활성화라.

    (풀이)

    df_local[df_local['name_local'] == '김포시']
    name_local code_local code
    108 김포시 31230 31
    new_seoul_dict = local_dict.copy() 
    new_seoul_dict['features'] = [l for l in metro_dict['features'] if  (l['properties']['code'] == '31230' or l['properties']['code'][:2] == '11')]
    #---#
    df_big = df2.query('시도 == "서울특별시" or 지역 =="김포시"').assign(에너지사용량=lambda df: df['에너지사용량(TOE)/전기']+df['에너지사용량(TOE)/도시가스']+df['에너지사용량(TOE)/지역난방'])
    df_small = df_big.groupby('년도').agg({'에너지사용량':'sum'}).reset_index().rename({'에너지사용량':'합'},axis=1)
    tidydata = df_big.merge(df_small).assign(구별사용비율 = lambda df: df['에너지사용량']/df['합'])
    #---#
    range_color = tidydata['구별사용비율'].min(), tidydata['구별사용비율'].max()
    fig = px.choropleth_mapbox(
        geojson = new_seoul_dict,
        featureidkey = 'properties.code',
        data_frame = tidydata,
        locations = 'code_local',
        color = '구별사용비율',
        animation_frame= '년도',
        hover_data = ['시도','지역'],
        opacity = 0.5,
        #---#
        mapbox_style="carto-positron",
        range_color= range_color,
        center={"lat": 37.5612, "lon": 126.8228},
        zoom=9,
        height=800,
        width=750    
    )
    fig.show(config={'scrollZoom':False})

    3. FIFA – 50점

    아래는 FIFA 자료를 불러오는 코드이다.

    df = pd.read_csv('https://raw.githubusercontent.com/guebin/DV2021/master/_notebooks/2021-10-25-FIFA22_official_data.csv').drop(['Loaned From','Marking'],axis=1).dropna()
    df.head()
    ID Name Age Photo Nationality Flag Overall Potential Club Club Logo ... SlidingTackle GKDiving GKHandling GKKicking GKPositioning GKReflexes Best Position Best Overall Rating Release Clause DefensiveAwareness
    0 212198 Bruno Fernandes 26 https://cdn.sofifa.com/players/212/198/22_60.png Portugal https://cdn.sofifa.com/flags/pt.png 88 89 Manchester United https://cdn.sofifa.com/teams/11/30.png ... 65.0 12.0 14.0 15.0 8.0 14.0 CAM 88.0 €206.9M 72.0
    1 209658 L. Goretzka 26 https://cdn.sofifa.com/players/209/658/22_60.png Germany https://cdn.sofifa.com/flags/de.png 87 88 FC Bayern München https://cdn.sofifa.com/teams/21/30.png ... 77.0 13.0 8.0 15.0 11.0 9.0 CM 87.0 €160.4M 74.0
    2 176580 L. Suárez 34 https://cdn.sofifa.com/players/176/580/22_60.png Uruguay https://cdn.sofifa.com/flags/uy.png 88 88 Atlético de Madrid https://cdn.sofifa.com/teams/240/30.png ... 38.0 27.0 25.0 31.0 33.0 37.0 ST 88.0 €91.2M 42.0
    3 192985 K. De Bruyne 30 https://cdn.sofifa.com/players/192/985/22_60.png Belgium https://cdn.sofifa.com/flags/be.png 91 91 Manchester City https://cdn.sofifa.com/teams/10/30.png ... 53.0 15.0 13.0 5.0 10.0 13.0 CM 91.0 €232.2M 68.0
    4 224334 M. Acuña 29 https://cdn.sofifa.com/players/224/334/22_60.png Argentina https://cdn.sofifa.com/flags/ar.png 84 84 Sevilla FC https://cdn.sofifa.com/teams/481/30.png ... 82.0 8.0 14.0 13.0 13.0 14.0 LB 84.0 €77.7M 80.0

    5 rows × 63 columns

    아래는 {소속대륙:국가} 및 {포지션:세부포지션}에 정보를 담은 딕셔너리이다.

    continent_dict = {
        'Asia': ['Afghanistan', 'Japan', 'Macau', 'Chinese Taipei', 'Indonesia', 'Korea Republic', 'Kazakhstan', 'Kyrgyzstan', 'Bhutan', 'Philippines', 'Syria', 'China PR', 'Oman', 'Guam', 'Vietnam', 'Jordan', 'Palestine', 'Malaysia', 'Hong Kong', 'Korea DPR', 'Lebanon', 'Uzbekistan', 'India','Iraq', 'Iran', 'Saudi Arabia', 'United Arab Emirates','Australia'],
        'Europe': ['Portugal', 'Germany', 'Belgium', 'Netherlands', 'Croatia', 'Spain', 'Austria', 'Italy', 'France', 'Serbia', 'England', 'Poland', 'Ukraine', 'Wales', 'Scotland', 'Czech Republic', 'Slovakia', 'Romania', 'Bosnia and Herzegovina', 'Republic of Ireland', 'Norway', 'Sweden', 'Bulgaria', 'Lithuania', 'Estonia', 'Latvia', 'Liechtenstein','Albania','Denmark','Finland','Greece','Hungary','Iceland','Luxembourg','Northern Ireland','Slovenia','Switzerland','Andorra','Azerbaijan','Belarus','Cyprus','Faroe Islands','Georgia','Kosovo','Malta','Moldova','Montenegro','North Macedonia','Armenia','Gibraltar','Russia','Turkey','Israel'],
        'South America': ['Uruguay', 'Argentina', 'Brazil', 'Chile', 'Colombia', 'Ecuador', 'Paraguay', 'Venezuela', 'Suriname', 'Bolivia','Peru','Guyana'],
        'Africa': ['Egypt', "Côte d'Ivoire", 'Senegal', 'Morocco', 'Ghana', 'Algeria', 'Guinea', 'Mali', 'Congo DR', 'Liberia', 'Cameroon', 'Tunisia', 'Comoros', 'Kenya', 'South Africa', 'Zimbabwe', 'Madagascar', 'Mozambique', 'Equatorial Guinea', 'Congo', 'Burundi', 'Grenada', 'Thailand', 'Togo', 'Sudan', 'Mauritania','Guinea Bissau','Libya','Nigeria','Zambia','Angola','Benin','Burkina Faso','Cape Verde Islands','Central African Republic','Chad','Eritrea','Gabon','Gambia','Mauritius','Namibia','Rwanda','Sierra Leone','South Sudan','São Tomé e Príncipe','Uganda','Niger'],
        'North and Central America': ['Antigua and Barbuda', 'Barbados', 'Belize', 'Bermuda', 'Canada', 'Costa Rica', 'Cuba', 'Curacao', 'Dominican Republic', 'El Salvador', 'Guatemala', 'Haiti', 'Honduras', 'Jamaica', 'Mexico', 'Montserrat', 'Panama', 'Puerto Rico', 'Saint Kitts and Nevis', 'Saint Lucia', 'Trinidad and Tobago', 'United States'],
        'Oceania': ['New Zealand', 'Fiji', 'Papua New Guinea','New Caledonia'],
    }
    position_dict = {
        'GOALKEEPER':{'GK'},
        'DEFENDER':{'CB','RCB','LCB','RB','LB','RWB','LWB'},
        'MIDFIELDER':{'CM','RCM','LCM','CDM','RDM','LDM','CAM','RAM','LAM','RM','LM'},
        'FORWARD':{'ST','CF','RF','LF','RW','LW','RS','LS'},
        'SUB':{'SUB'},
        'RES':{'RES'}
    }

    주어진 자료를 활용하여 아래를 잘 읽고 물음에 답하라.

    (데이터정리)

    df = pd.read_csv('https://raw.githubusercontent.com/guebin/DV2021/master/_notebooks/2021-10-25-FIFA22_official_data.csv').drop(['Loaned From','Marking'],axis=1).dropna()\
    .assign(
        Position = lambda df: df.Position.str.split('>').str[-1],
    ).assign(
        Continent = lambda df: [k for x in df['Nationality'] for k,v in continent_dict.items() if x in v],
        Position = lambda df: [k for x in df['Position'] for k,v in position_dict.items() if x in v],
        Wage = lambda df: df.Wage.str[1:].str.replace('M','*1000000').str.replace('K','*1000').apply(eval), 
    
    )
    df.head()
    ID Name Age Photo Nationality Flag Overall Potential Club Club Logo ... GKDiving GKHandling GKKicking GKPositioning GKReflexes Best Position Best Overall Rating Release Clause DefensiveAwareness Continent
    0 212198 Bruno Fernandes 26 https://cdn.sofifa.com/players/212/198/22_60.png Portugal https://cdn.sofifa.com/flags/pt.png 88 89 Manchester United https://cdn.sofifa.com/teams/11/30.png ... 12.0 14.0 15.0 8.0 14.0 CAM 88.0 €206.9M 72.0 Europe
    1 209658 L. Goretzka 26 https://cdn.sofifa.com/players/209/658/22_60.png Germany https://cdn.sofifa.com/flags/de.png 87 88 FC Bayern München https://cdn.sofifa.com/teams/21/30.png ... 13.0 8.0 15.0 11.0 9.0 CM 87.0 €160.4M 74.0 Europe
    2 176580 L. Suárez 34 https://cdn.sofifa.com/players/176/580/22_60.png Uruguay https://cdn.sofifa.com/flags/uy.png 88 88 Atlético de Madrid https://cdn.sofifa.com/teams/240/30.png ... 27.0 25.0 31.0 33.0 37.0 ST 88.0 €91.2M 42.0 South America
    3 192985 K. De Bruyne 30 https://cdn.sofifa.com/players/192/985/22_60.png Belgium https://cdn.sofifa.com/flags/be.png 91 91 Manchester City https://cdn.sofifa.com/teams/10/30.png ... 15.0 13.0 5.0 10.0 13.0 CM 91.0 €232.2M 68.0 Europe
    4 224334 M. Acuña 29 https://cdn.sofifa.com/players/224/334/22_60.png Argentina https://cdn.sofifa.com/flags/ar.png 84 84 Sevilla FC https://cdn.sofifa.com/teams/481/30.png ... 8.0 14.0 13.0 13.0 14.0 LB 84.0 €77.7M 80.0 South America

    5 rows × 64 columns

    (1) 대륙별 인적자원 – 20점

    각 대륙 및 국가별 인적 자원 현황을 시각화하고자 한다. 이를 위해 두 가지 형태의 시각화를 구현했다.

    fig = pio.from_json(requests.get('https://raw.githubusercontent.com/guebin/DV2023/main/posts/figure_31.json').text)
    pio.show(fig,config={'scrollZoom':False})
    fig = pio.from_json(requests.get('https://raw.githubusercontent.com/guebin/DV2023/main/posts/figure_32.json').text)
    pio.show(fig,config={'scrollZoom':False})

    첫 번째 시각화(Figure 1)를 통해, 각 대륙별로 선수들의 평균 능력치, 평균 급여, 그리고 선수층의 깊이를 비교할 수 있었다. 이를 바탕으로, 남미 대륙이 평균 능력치에서, 아프리카가 평균 급여에서 높은 값을 보였으며, 유럽은 가장 깊은 선수층을 가지고 있는 것으로 나타났다. 두 번째 시각화(Figure 2)는 나라별로 세분화된 인적 자원을 조사하는 데 중점을 두었다. 이 그림에서 브라질은 선수 수, 평균 능력치, 평균 급여 등 여러 면에서 우수함을 확인할 수 있었다. 반면, 우크라이나는 선수들의 평균 능력치는 높지만 선수 수와 평균 급여는 상대적으로 낮은 값을 보었다. 두 번째 시각화를 이용해 국가별 특색을 좀 더 면밀히 파악할 수 있었다.

    Figure 1과 Figure 2를 구현하라.

    (풀이)

    fig = df.assign(
        logWage = lambda df: np.log(df.Wage)
    ).groupby(['Continent']).agg({'Overall':['mean','size'],'logWage':'mean'})\
    .set_axis(['Overall(Mean)','PlayerSize','logWage(Mean)'],axis=1).reset_index()\
    .assign(logPlayerSize = lambda df: np.log(df['PlayerSize']))\
    .plot.scatter(
        x='Overall(Mean)',
        y='logWage(Mean)',
        size='PlayerSize',
        text='Continent',
        hover_data = ['Continent'],
        color = 'Continent',
        title = 'Figure1: 인적자원 (대륙별)',
        #---#
        height=400,
        width=750
    )
    fig.show(config={'scrollZoom':False})
    fig = df.assign(
        logWage = lambda df: np.log(df.Wage)
    ).groupby(['Nationality','Continent']).agg({'Overall':['mean','size'],'logWage':'mean'})\
    .set_axis(['Overall(Mean)','PlayerSize','logWage(Mean)'],axis=1).reset_index()\
    .assign(logPlayerSize = lambda df: np.log(df['PlayerSize']))\
    .plot.scatter(
        x='Overall(Mean)',
        y='logWage(Mean)',
        size='PlayerSize',
        hover_data = ['Continent','Nationality'],
        color = 'Continent',
        title = 'Figure2: 인적자원 (국가별)',
        #---#
        height=400,
        width=750
    )
    fig.show(config={'scrollZoom':False})
    with open('/home/cgb2/Dropbox/07_lectures/2023-09-DV2023/posts/figure_32.json', 'w') as file:
        file.write(fig.to_json())

    (2) 아시안컵은 누가 차지할까 – 30점(+\(\alpha\))

    곧 시작될 아시안컵의 우승 국가를 예측하기 위해, 아시아 국가들의 축구 선수들을 기준으로 한 새로운 강함 지표를 개발했다. 이 지표는 선수들의 평균 능력치와 선수 수를 결합한 것이다. 예를 들어, 한국과 일본의 Strength 지표는 각각 다음과 같이 계산된다:

    • 한국의 Strength = 한국 선수들의 평균 능력치 + 한국 선수들의 수 / 5
    • 일본의 Strength = 일본 선수들의 평균 능력치 + 일본 선수들의 수 / 5

    이를 바탕으로 아래와 같은 두가지 형태의 시각화를 구현했다.

    fig = pio.from_json(requests.get('https://raw.githubusercontent.com/guebin/DV2023/main/posts/figure_33.json').text)
    pio.show(fig,config={'scrollZoom':False})
    fig = pio.from_json(requests.get('https://raw.githubusercontent.com/guebin/DV2023/main/posts/figure_34.json').text)
    pio.show(fig,config={'scrollZoom':False})

    이 첫 번째 시각화(Figure 1)를 통해, 각 국가의 Strength를 기반으로 8강 진출 국가를 예상했다. 그 결과 일본,한국,호주,사우디아라비아,이란,중국,시리아,우즈베키스탄 순으로 8강진출국이 예상되었다. 또한 두 번째 시각화 (Figure22)에서는 Strength가 높은 상위 6개 국가를 선별하여, 국가별 포지션에 따른 평균 능력치(Overall)를 비교함으로써 각 국가의 강점과 약점을 분석했다. 이 분석에 따르면, 일본은 모든 포지션에서 뛰어난 능력치를 보였고, 이란은 특히 골키퍼와 공격수 포지션에서 매우 우수한 것으로 나타났다.

    Figure1,2를 재현하라.

    주의사항

    • Figure1에서 투명도는 구현하지 않아도 무방하며 투명도 구현시 10점의 가산점이 있음. (Figure1에서 8강 진출이 예상되는 국가에 대한 투명도는 0.9로, 그렇지 않는 국가에 대한 투명도는 0.5로 설정)
    • Figure2에서 카테고리들의 정렬은 구현하지 않아도 무방함. (즉 국가가 일본,한국,…,중국 순서로 정렬될 필요는 없으며 포지션도 골키퍼,수비수,미드필더,공격수 순으로 정렬될 필요없음) 구현시 10점 가산점 있음.
    • 이외의 사항은 모두 구현되어야 하며 올바르게 구현되지 않은 경우 0점 처리함.

    (풀이)

    tidydata = df.query('Continent == "Asia"')\
    .groupby('Nationality').agg({'Overall':['size','mean']})\
    .set_axis(['PlayerSize','Overall(mean)'],axis=1).reset_index()\
    .assign(Strength = lambda df: df['Overall(mean)']+df['PlayerSize']/5)\
    .sort_values('Strength',ascending=False).reset_index(drop=True)\
    .assign(Top8 = lambda df: df.index<8)\
    .rename({'Top8':'8강 가능성'},axis=1)\
    .sort_values('Strength')\
    .assign(Text = lambda df: [f'{x:.2f} + {y/5:.2f} = {z:.2f}' for x,y,z in zip(df['Overall(mean)'],df['PlayerSize'],df['Strength'])])
    #---#
    fig = px.bar(
        tidydata,
        x='Strength',y='Nationality',
        color='8강 가능성',
        text='Text',
        title='Figure1: Overall + PlayerSize/5 = Strength',
        opacity=0.9,
        #---#
        width=750,
        height=1500
    )
    fig.data[0]['marker']['opacity'] = 0.5
    fig
    tidydata = df.query('Nationality in ["Japan","Korea Republic","Australia","Saudi Arabia","Iran","China PR"]')\
    .query('Position != "RES" and Position != "SUB"')\
    .groupby(['Nationality','Position']).agg({'Overall':'mean'})\
    .reset_index().rename({'Nationality':'국가','Position':'포지션','Overall':'평균능력치'},axis=1)
    #---#
    fig = px.density_heatmap(
        tidydata,
        x='국가',
        y='포지션',
        z='평균능력치',
        width=750,
        height=450,
        title='Figure2: 상위8개국에 대한 포지션별 히트맵',
        category_orders={
            '국가':["Japan","Korea Republic","Australia","Saudi Arabia","Iran","China PR"],
            '포지션':['FORWARD','MIDFIELDER','DEFENDER','GOALKEEPER']
        }
    )
    fig.layout['coloraxis']['colorbar']['title']['text'] = '평균능력치'
    fig.data[0]['hovertemplate'] = '국가=%{x}<br>포지션=%{y}<br>평균능력치=%{z}<extra></extra>'
    fig