프로젝트 - 인구 현황 데이터 분석 (catogram 시각화)

허재정·2024년 3월 18일

EDA

목록 보기
13/13

카토그램

  • 지리적 공간을 나타내는 지도에서 지역의 상대적 크기를 표현하는 방식
  • 특정 변수의 값을 반영하여 지역의 크기를 변형하여 나타냄. (일반적으로 지도에서는 지역의 면적이나 크기가 실제 지리적 크기와 일치)
  • 통계적 혹은 시각적으로 중요한 데이터를 강조하거나 특정 지역 간의 비교를 용이하게 하기 위해 사용
  • 일반적으로 인구, 경제적 측면, 환경적 특성 등과 같은 변수를 나타내는 경우 사용

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

(1) 데이터 정제

import numpy as np
import pandas as pd

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

Excel 데이터 cell 병합으로 인해 컬럼이 2중으로 되어 있음

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

  • 행과 열 편집, 값 변경 등 데이터 정제

(2) 인구 소멸 위험 지역 조사

  • 65세 고령 인구 대비 20-39세 여성인구 비중으로 비율이 < 0.5 이면 30년 내 소멸 가능성이 크다고 봄

  • 소멸 위험지수 = (2-19세 여성인구수) / (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+"]
	)

  • 인구 소멸 위험 지역을 조사하기 위해 필요한 컬럼들을 모아 새로운 DataFrame 생성
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값 아니면 False
popul_pivot["소멸위기지역"] = popul_pivot["소멸지수"] < 0.5

  • Multi index와 Multi column을 단일값으로 만들어 줌
# index 재정렬
popul_pivot.reset_index(inplace=True)
    popul_pivot.head()

# column 추가
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

(3) 지역별 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

(1) 카토그램 구조

  • Excel로 만든 카토그램 큰 구조 읽기
draw_korea_raw = pd.read_excel("../data/07_draw_korea_raw.xlsx")

(2) 시각화할 좌표 값 지정

    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()
    Excel에 그려진 각 행정구역 별 위치 좌표 얻어 옴
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
    )

(3) Sample 카토그램

(4) 데이터 병합

  • 인구 데이터와 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()
  • 두 DataFrame을 ID 기준으로 병합
population = pd.merge(
          population, draw_kr_carto, how="left", on="ID"
    )

(5) 카토그램 그리기

  • 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()

(6) 인구수 합계

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

(7) 소멸 위기 지역

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"))

(1) 인구수 합계

	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

(2) 소멸 위기 지역

    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 / 지도

profile
Data Science 스터디로그

0개의 댓글