[zero-base/] DS Part 4. EDA - 34일차 스터디 노트

손윤재·2024년 1월 18일

제로베이스 DS 22기

목록 보기
35/55
post-thumbnail

실습 프로젝트 7️⃣

【Cartogram 시각화】 인구 현황 데이터 분석

  • 카르토그램은 지리적 공간을 나타내는 지도에서 지역의 상대적 크기를 표현하는 방식을 말한다.

  • 일반적으로 지도에서는 지역의 면적이나 크기가 실제 지리적 크기와 일치하지만, 카르토그램은 특정 변수의 값을 반영하여 지역의 크기를 변형하여 나타낸다.

  • 카르토그램은 통계적 혹은 시각적으로 중요한 데이터를 강조하거나 특정 지역 간의 비교를 용이하게 하기 위해 사용된다.

  • 일반적으로 인구, 경제적 측면, 환경적 특성 등과 같은 변수를 나타내는 카르토그램이 흔히 사용된다.


1. 인구 소멸 위기 지역 파악


🔰 데이터 정제

import numpy as np
import pandas as pd

population = pd.read_excel("../data/07_population_raw_data.xlsx")
population.head()

  • 엑셀 데이터의 셀이 병합되어 있어 커럼이 2중으로 잡히고 행에 NaN 값이 채워져 있다.

  • 읽어올 때 필요 없는 맨 윗줄(column_level=0)은 제거하고 가져온다.
	population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1)
  • fillna() : NaN 값 채우기
	population.fillna(method="pad", inplace=True)
	population

  • 행과 열을 편집하고 값을 변경해 분석이 가능한 형태의 데이터로 정제한다.


🔰 인구 소멸 위험 지역 조사

  • 소멸위험지수는?
    65세 고령인구 대비 20-39세 여성인구 비중으로 비율이 0.5이하이면 30년 내 소멸 가능성이 크다고 판단한다.

  • 소멸위험지수 = (20-39세 여성 인구수) / (65세 이상 인구수)

  • 소멸위험지수가 0.5미만이면 인구 소멸 위험 지역으로 구분한다.


  • 20~39세 여성 인구와 65세 이상 인구수를 구하기 위해 나뉘어진 나이 컬럼을 합쳐준다.
    population["20-39세"] = (
          population["20 - 24세"] + population["25 - 29세"]
        + population["30 - 34세"] + population["35 - 39세"]
	)

    population["65세이상"] = (
          population["65 - 69세"] + population["70 - 74세"]
        + population["75 - 79세"] + population["80 - 84세"]
        + population["85 - 89세"] + population["90 - 94세"]
        + population["95 - 99세"] + population["100+"]
	)
  • 인구 소멸 위험 지역을 조사하기 위해 필요한 컬럼들을 모아 새로운 데이터프레임을 만든다.
    popul_pivot = pd.pivot_table(
          data=population,
          index=["광역시도", "시도"],
          columns=["구분"],
          values=["인구수", "20-39세", "65세이상"]
    )
  • 노인 인구 대비 2030여성 인구 비율을 계산한다.
    popul_pivot["소멸지수"]
      = popul_pivot["20-39세", "여자"] / popul_pivot["65세이상", "합계"]
  • "소멸위험지역" 컬럼을 만들고 "소멸지수"가 0.5보다 작은 지역에 True 값을 준다.
	popul_pivot["소멸위기지역"] = popul_pivot["소멸지수"] < 0.5

  • 멀티 인덱스와 멀티 컬럼을 단일 값으로 만들어 준다.
    // 인덱스 재정렬
    popul_pivot.reset_index(inplace=True)
    popul_pivot.head()

    // 컬럼 더하기
    tmp_cols = [
        popul_pivot.columns.get_level_values(0)[n]
        + popul_pivot.columns.get_level_values(1)[n]
        for n in range(len(popul_pivot.columns.get_level_values(0)))
    ]
    popul_pivot.columns = tmp_cols


🔰 지역별 ID 만들기

  • 지도 시각화를 위해 지역별 ID가 필요하다.

  • 일반 시에 있는 행정구 데이터를 정리한다.

    admin_gu = {
        "수원": ["장안구", "권선구", "팔달구", "영통구"],
        "성남": ["수정구", "중원구", "분당구"],
        "안양": ["만안구", "동안구"],
        "안산": ["상록구", "단원구"],
        "고양": ["덕양구", "일산동구", "일산서구"],
        "용인": ["처인구", "기흥구", "수지구"],
        "청주": ["상당구", "서원구", "흥덕구", "청원구"],
        "천안": ["동남구", "서북구"],
        "전주": ["완산구", "덕진구"],
        "포항": ["남구", "북구"],
        "창원": ["의창구", "성산구", "진해구", "마산합포구", "마산회원구"],
        "부천": ["오정구", "원미구", "소사구"],
    }
  • 일반 시

    • 일반 도에 포함된 일반 시나 군의 지역 ID는 "시"를 뺀 지역명으로 한다.
    • row["시도"][:-1] --> 마지막 한 글자를 제외한 전체
      • ex. '강릉시' --> '강릉', '옥천군' --> '옥천'
    • 고성군은 강원도와 경상남도에 동일한 이름으로 존재하므로 구분해준다.
      • ex. 강원도 '고성군' --> '고성(강원)', 경상남도 '고성군' --> '고성(경남)'
  • 세종특별자치시는 하나밖에 없다.

  • 일반 구

    • 특별시, 광역시의 구는 해당 시의 앞 두글자[:2]와 구 이름을 합친다.
    • 구 이름이 세글자 이상이면 '구'를 제외한 글자[:-1]를 합친다.
  • 행정구

    • 일반시(12개)의 행정구도 해상 시의 앞 두글자와 구이름을 합친다.
    • 구 이름이 세글자 이상이면 '구'를 제외한 글자[:-1]만 합친다.
  def find_key_by_value(dictionary, value):
      for key, val in dictionary.items():
          if value in val:
              return key
      return None  // 값이 없는 경우

  for idx, row in popul_pivot[["광역시도", "시도"]].iterrows():
      if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
          # 행정구
          if row["시도"][-1:] == "구":
              key = find_key_by_value(admin_gu, row["시도"])            
              if len(key) > 0:
                  if len(row["시도"]) == 2:
                      sidoID[idx] = key + " " + row["시도"]                    
                  elif row["시도"] in ["마산합포구", "마산회원구"]:
                      sidoID[idx] = key + " " + row["시도"][2:-1]                
                  else:
                      sidoID[idx] = key + " " + row["시도"][:-1]
          # 고성군
          elif row["시도"] == "고성군":
              if row["광역시도"] == "강원도":
                  sidoID[idx] = "고성(강원)"
              else: sidoID[idx] = "고성(경남)"
          # 일반시
          else:
              sidoID[idx] = row["시도"][:-1]

      # 세종특별자치시
      elif row["광역시도"] == "세종특별자치시":
          sidoID[idx] = "세종"

      # 특별시/광역시 구
      else:
          if len(row["시도"]) == 2: // 서구, 중구, 북구, ...
              sidoID[idx] = row["광역시도"][:2] + " " + row["시도"]
          else:
              sidoID[idx] = row["광역시도"][:2] + " " + row["시도"][:-1]

2. 카르토그램 시각화

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    import set_matplotlib_hangul
    import warnings

    warnings.filterwarnings(action="ignore")
    %matplotlib inline
    # Hangul OK in your Windows!!!

🔰 카르토그램 뼈대

  • 엘셀로 만든 카르토그램 뼈대를 읽어온다.
    draw_korea_raw = pd.read_excel("../data/07_draw_korea_raw.xlsx")

🔰 시각화할 좌표 값 세팅

  • 행정 구역 경계가 되는 좌표 값들을 따온다.
    BORDER_LINES = [
        [(5, 1), (5, 2), (7, 2), (7, 3), (11, 3), (11, 0)], # 인천
        [(5, 4), (5, 5), (2, 5), (2, 7), (4, 7), (4, 9), (7, 9),
         (7, 7), (9, 7), (9, 5), (10, 5), (10, 4), (5, 4)], # 서울
        [(1, 7), (1, 8), (3, 8), (3, 10), (10, 10), (10, 7),
         (12, 7), (12, 6), (11, 6), (11, 5), (12, 5), (12, 4),
         (11, 4), (11, 3)], # 경기도
        [(8, 10), (8, 11), (6, 11), (6, 12)], # 강원도
        [(12, 5), (13, 5), (13, 4), (14, 4), (14, 5),
         (15, 5), (15, 4), (16, 4), (16, 2)], # 충청북도
        [(16, 4), (17, 4), (17, 5), (16, 5), (16, 6), (19, 6), (19, 5),
         (20, 5), (20, 4), (21, 4), (21, 3), (19, 3), (19, 1)], # 전북
        [(13, 5), (13, 6), (16, 6)], # 대전시
        [(13, 5), (14, 5)], # 세종시
        [(21, 2), (21, 3), (22, 3), (22, 4), (24, 4), (24, 2), (21, 2)], # 광주
        [(20, 5), (21, 5), (21, 6), (23, 6)], # 전남
        [(10, 8), (12, 8), (12, 9), (14, 9), (14, 8), (16, 8), (16, 6)], # 충북
        [(14, 9), (14, 11), (14, 12), (13, 12), (13, 13)], # 경북
        [(15, 8), (17, 8), (17, 10), (16, 10), (16, 11), (14, 11)], # 대구
        [(17, 9), (18, 9), (18, 8), (19, 8), (19, 9),
         (20, 9), (20, 10), (21, 10)], # 부산
        [(16, 11), (16, 13)], # 울산
        [(27, 5), (27, 6), (25, 6)] # 제주도
    ]
  • stack()
    엑셀에 그려진 각 행정 구역별 위치 좌표를 얻어온다.
    draw_kr_carto = pd.DataFrame(draw_korea_raw.stack())
    draw_kr_carto.reset_index(inplace=True)
    draw_kr_carto.rename(
    	columns={"level_0": "y", "level_1": "x", 0: "ID"},
      	inplace=True
    )

🔰 샘플 카로토그램


🔰 데이터 병합

  • 인구 데이터와 Cartogram 좌표 데이터를 병합하기 전에 검정 과정을 거친다.
    set(draw_kr_carto["ID"].unique()) - set(population["ID"].unique())
    //==> set()

    tmp_list = list(
      set(population["ID"].unique()) - set(draw_kr_carto["ID"].unique())
    )
    //==> ['수원', '전주', '청주', '천안', '성남', '부천',
    //     '고양', '포항', '안양', '용인', '창원', '안산']

    for item in tmp_list:
        population.drop(
            population[population["ID"] == item].index,
            inplace=True
        )

    set(population["ID"].unique()) - set(draw_kr_carto["ID"].unique())
    //==> set()
  • 두 데이터 프레임을 ID를 기준으로 병합한다.
    population = pd.merge(
          population, draw_kr_carto, how="left", on="ID"
    )

🔰 카르토그램 그리기 함수

  • 카르토그램을 그리기 위한 데이터를 계산하는 함수를 정의한다.
    • blockedMap: 인구현황(population)
    • targetData: population에서 그리고 싶은 컬럼
  def get_data_info(targetData, blockedMap):
      vmin = min(blockedMap[targetData])
      vmax = max(blockedMap[targetData])    
      white_label_min = (vmax - vmin) * 0.25 + vmin    
      map_data = blockedMap.pivot_table(
            index="y", columns="x", values=targetData
      )    
      return map_data, vmin, vmax, white_label_min
  def get_data_info_for_zero_center(targetData, blockedMap):
      white_label_min = 5    
      tmp_max = max(
          [np.abs(min(blockedMap[targetData])),
           np.abs(max(blockedMap[targetData]))]
      )
      vmin, vmax = -tmp_max, tmp_max    
      map_data = blockedMap.pivot_table(
          index="y", columns="x", values=targetData
      )    
      return map_data, vmin, vmax, white_label_min
  def plot_text(targetData, blockedMap, white_label_min):    
      for idx, row in blockedMap.iterrows():        
          if len(row["ID"].split()) == 2:
              dispname = "{}\n{}".format(
                  row["ID"].split()[0], row["ID"].split()[1]
              )
          elif row["ID"][:2] == "고성":
              dispname = "고성"
          else:
              dispname = row["ID"]

          if len(dispname.splitlines()[-1]) >= 3:
              fontsize, linespacing = 9.5, 1.5 
          else:
              fontsize, linespacing = 11, 1.2

          annocolor = "white" if np.abs(row[targetData]) > white_label_min else "black"

          plt.annotate(
              dispname,
              (row["x"] + 0.5, row["y"] + 0.5), # 글자가 중앙에 위치
              weight="bold",
              color=annocolor,
              fontsize=fontsize,
              linespacing=linespacing,
              ha="center", # 수평 정렬
              va="center", # 수직 정렬 
          )
  def drawKoreaCartogram(targetData, blockedMap, cmapName, zeroCenter=False):
      if zeroCenter:
          masked_mapdata, vmin, vmax, white_label_min
              = get_data_info_for_zero_center(targetData, blockedMap)
      else:
          masked_mapdata, vmin, vmax, white_label_min
              = get_data_info(targetData, blockedMap)

      plt.figure(figsize=(9, 11))
      plt.pcolor(
          masked_mapdata,
          vmin=vmin,
          vmax=vmax,
          cmap=cmapName,
          edgecolor="#aaaaaa",
          linewidth=0.5
      )    
      plot_text(targetData, blockedMap, white_label_min)

      for coord in BORDER_LINES:
          ys, xs = zip(*coord)
          plt.plot(xs, ys, c="black", lw=1.5)        

      plt.gca().invert_yaxis()
      plt.axis("off")
      plt.tight_layout()

      cb = plt.colorbar(shrink=0.1, aspect=10)
      cb.set_label(targetData)

      plt.show()

🔰 인구수합계

    drawKoreaCartogram(
        targetData="인구수합계", blockedMap=population, cmapName="Blues"
    )

🔰 소멸위기지역

    population["소멸위기지역"] = [
    	1 if val else 0 for val in population["소멸위기지역"]
	]
	drawKoreaCartogram("소멸위기지역", population, "Reds")


3. 지도 시각화

import pandas as pd
import folium
import json

geo_path = "../data/07_skorea_municipalities_geo_simple.json"
geo_json = json.load(open(geo_path, encoding="utf-8"))

🔰 인구수합계

    popmap = folium.Map(location=[36.3002, 127.554], zoom_start=7)

    folium.Choropleth(
            geo_data=geo_json,
            data=popul_folium["인구수합계"],
            key_on="feature.id",
            columns=[popul_folium.index, popul_folium["인구수합계"]],
            fill_color="YlGnBu"
    ).add_to(popmap)

    popmap

🔰 소멸위기지역

    popmap = folium.Map(location=[36.3002, 127.554], zoom_start=7)

    folium.Choropleth(
            geo_data=geo_json,
            data=popul_folium["인구수합계"],
            key_on="feature.id",
            columns=[popul_folium.index, popul_folium["인구수합계"]],
            fill_color="YlGnBu"
    ).add_to(popmap)

    popmap

4. Cartogram Vs. Map

👉 카트토그램으로 확인하는 것 보다 지도로 표현된 소멸 위험 지역의 크기가 훨씬 더 넓어 보인다.

profile
ISTP(정신승리), To Be Data Scientist

0개의 댓글