[제로베이스] 인구분석

허재훈·2023년 4월 13일
0

EDA

목록 보기
8/14




목표

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

2. 인구 소멸 위기 지역의 지도 표현

3. 지도 표현에 대한 카르토그램 표현











데이터 읽고 인구 소멸 지역 계산하기

# 아래 코드가 set_matplotlib_hangul.py

# matplolib 한글 대응 작업

import platform
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc

path = "c:/Windows/Fonts/malgun.ttf"

if platform.system() == "Darwin":
    print("Hangul OK in your MAC !!!")
    rc("font", family="AppleGothic")
elif platform.system() == "Windows":
    font_name = font_manager.FontProperties(fname=path).get_name()
    print("Hangul OK in your Windows !!!")
    rc("font", family=font_name)
else:
    print("Unknown system... sorry~~~~")

plt.rcParams["axes.unicode_minus"] = False
---------------------------------
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 !!!

# 위 엑셀로 보면 제목 행이 2개이다.

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

# 엑셀의 제목 행 2줄(0번째, 1번째) 중에서 1번째만 나타내겠다.
population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1)
population

filna()

- fillna 메서드는 DataFrame에서 결측값을 원하는 값으로 변경하는 메서드입니다.
- 결측값 변경 (fillna / backfill / bfill / pad / ffill)
- DataFrame.fillna(value=None, method=None, axis=None, inplace=False, limit=None, downcast=None)
- DataFrame.backfill( ) / DataFrame.bfill : DataFrame.fillna(mathod='bfill')과 동일합니다.
- DataFrame.pad / DataFrame.ffill : DataFrame.fillna(method='ffill')과 동일합니다.

# 기본 사용법
df.fillna(value=None, method=None, axis=None, inplace=False, limit=None, downcast=None)
value : 결측값을 대체할 값입니다. dict형태로도 가능합니다.
method : 결측값을 변경할 방식입니다. 
bfill, backfill 로 할경우 결측값을 바로 아래 값과 동일하게 변경합니다.
ffill, pad 로 할 경우 결측값을 바로 위 값과 동일하게 변경합니다.
axis : {0 : index / 1 : columns} fillna 메서드를 적용할 레이블입니다.
inplace : 원본을 변경할지 여부입니다. True일 경우 원본을 변경하게 됩니다.
limit : 결측값을 변경할 횟수입니다. 위에서부터 limit로 지정된 갯수만큼만 변경합니다.
downcast : 다운캐스트할지 여부입니다. downcast='infer'일 경우 float64를 int64로 변경합니다.
-----------------------------------
# 실험할 데이터를 만든다
datas = {
    "A" : np.random.randint(1, 45, 8),
    "B" : np.random.randint(1, 45, 8),
    "C" : np.random.randint(1, 45, 8)
}
datas

>>
{'A': array([16, 26,  5, 34, 34, 19, 14,  4]),
 'B': array([34, 18, 36, 41, 13, 35, 40, 39]),
 'C': array([33,  6, 19, 38,  8, 19, 15,  9])}
--------------------------------------
# 데이터 프레임으로 만든다
fillna_df = pd.DataFrame(datas)
fillna_df

>>
	A	B	C
0	16	34	33
1	26	18	6
2	5	36	19
3	34	41	38
4	34	13	8
5	19	35	19
6	14	40	15
7	4	39	9
--------------------------
# NaN 값을 여기저기에 만든다
fillna_df.loc[2:4, ["A"]] = np.NaN
fillna_df.loc[3:5, ["B"]] = np.NaN
fillna_df.loc[4:7, ["C"]] = np.NaN
fillna_df
>>

# fillna()를 써서 NaN 값을 채워보자
# 옵션을 줄 수 있는데 제일 기본 옵션은 value= 옵션이다
fillna_df.fillna(value=1) # 1로 모두 채워라

>>

# fillna()를 써서 NaN 값을 채워보자
# method="pad" : 앞의 값을 가져와서 채워준다
fillna_df.fillna(method="pad")

>>

# fillna()를 써서 NaN 값을 채워보자
# method="backfill" : 뒤의 값을 가져와서 채워준다
# C 컬럼은 뒤의 값이 없으므로 그대로 유지된다.
fillna_df.fillna(method="backfill")

>>

# fillna()를 써서 NaN 값을 채워보자
# method="pad" : 앞의 값을 가져와서 채워준다
# 기본이 세로 기준이고,
# axis=1 을 주면 가로기준으로 바뀐다
fillna_df.fillna(method="pad", axis=1)

>>

# 엑셀을 읽어오자
# 실제 엑셀을 보면 '전국'이 3개 셀을 다 차지하고 있어서 NanN 값이 있었음 > 이부분도 캡처해서 올리자
population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1)
population.fillna(method="pad", inplace=True) # fillna: NaN 이 있다면 채워라
population.head()

# non-null : NaN 값이 없다는 뜻?
population.info()

>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 846 entries, 0 to 845
Data columns (total 16 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   행정구역(동읍면)(1)  846 non-null    object 
 1   행정구역(동읍면)(2)  846 non-null    object 
 2   항목             846 non-null    object 
 3846 non-null    float64
 4   20 - 24846 non-null    float64
 5   25 - 29846 non-null    float64
 6   30 - 34846 non-null    int64  
 7   35 - 39846 non-null    float64
 8   65 - 69846 non-null    float64
 9   70 - 74846 non-null    float64
 10  75 - 79846 non-null    int64  
 11  80 - 84846 non-null    float64
 12  85 - 89846 non-null    float64
 13  90 - 94846 non-null    float64
 14  95 - 99846 non-null    int64  
 15  100+           846 non-null    float64
dtypes: float64(10), int64(3), object(3)
memory usage: 105.9+ KB
-----------------------------

# 일부 컬럼의 이름도 바꿔주자
population.rename(
    columns={"행정구역(동읍면)별(1)":"광역시도", "행정구역(동읍면)별(2)":"시도", "계":"인구수"}, inplace=True
)
population.head()

# 소계는 제거하자
# ["시도"] != "소계" 인 것들만 추려낸다
# 자동적으로 광역시도 컬럼도 "전국" 인 컬럼은 걸려졌다
population = population[(population["시도"] != "소계")]

population.head()

population.rename(columns={"항목":"구분"}, inplace=True)
population.head()

population.loc[population["구분"] == "총인구수 (명)"]

population.loc[population["구분"] == "총인구수 (명)", "구분"] 

# 총인구수 (명) => 합계로 바꾸기
population.loc[population["구분"] == "총인구수 (명)", "구분"] = "합계"
population.head()

# 컬럼명이 길어서 컬럼명 변경

population.is_copy = False
# 위는 현재로서는 안해도 상관은 없음
# copy를 했을 때 warning 을 내보내지 말아달라는 뜻

# 남자인구수 (명) => 남자
# 여자인구수 (명) => 여자 로 바꾸기

population.loc[population["구분"] == "남자인구수 (명)", "구분"] = "남자"
population.loc[population["구분"] == "여자인구수 (명)", "구분"] = "여자"

population.head()

# 소멸지역을 조사하기 위한 데이터를 만들어두자
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+"]
    )

population.head()

# 데이터의 모양이 가로에 지역
# 세로에 연령대별, 남여구분이 모두 위치하도록 변경 >> 판다스 피봇테이블
# values 목록별로 columns 구분이 나타난다 
# 
pop = pd.pivot_table(
    data = population, 
    index=["광역시도", "시도"], 
    columns=["구분"], 
    values=["인구수", "20 - 39세", "65세 이상"]
)
pop

# 소멸비율계산

# 인구소멸위기 지역의 정의 > 소멸 위험 지역의 정의
# 65세 이상 노인 인구와 20~39세 여성 인구를 비교해 
# 젊은 여성 인구가 노인 인구의 절반에 미달할 경우 
# 드디어 소멸 비율을 계산할 수 있게 되었다.
pop["소멸비율"] = pop["20 - 39세", "여자"] / (pop["65세 이상", "합계"] / 2)
pop.head()

# 소멸위기지역 컬럼 생성

# 소멸 위기 지역인지도 체크해 둘 수 있다.
pop["소멸위기지역"] = pop["소멸비율"] < 1.0
pop

# 소멸위기지역 조회
pop["소멸위기지역"] == True

pop[pop["소멸위기지역"] == True] # True 인 곳만 나온다

# 소멸위기지역을 바로 조회해볼 수 있다. 대부분 '군'지역
# 시도가 현재 컬럼이 아닌 인덱스로 들어가 있으므로 .index
# get_level_values(0) : 광역시도
# get_level_values(1) : 시도
pop[pop["소멸위기지역"] == True].index.get_level_values(1)

>>
Index(['고성군', '삼척시', '양양군', '영월군', '정선군', '평창군', '홍천군', '횡성군', '가평군', '양평군',
       '연천군', '거창군', '고성군', '남해군', '밀양시', '산청군', '의령군', '창녕군', '하동군', '함안군',
       '함양군', '합천군', '고령군', '군위군', '문경시', '봉화군', '상주시', '성주군', '영덕군', '영양군',
       '영주시', '영천시', '예천군', '울릉군', '울진군', '의성군', '청도군', '청송군', '동구', '영도구',
       '강화군', '옹진군', '강진군', '고흥군', '곡성군', '구례군', '담양군', '보성군', '신안군', '영광군',
       '영암군', '완도군', '장성군', '장흥군', '진도군', '함평군', '해남군', '화순군', '고창군', '김제시',
       '남원시', '무주군', '부안군', '순창군', '임실군', '장수군', '정읍시', '진안군', '공주시', '금산군',
       '논산시', '보령시', '부여군', '서천군', '예산군', '청양군', '태안군', '홍성군', '괴산군', '단양군',
       '보은군', '영동군', '옥천군'],
      dtype='object', name='시도')
---------------------------
pop[pop["소멸위기지역"] == True].index.get_level_values(0)

>>
Index(['강원도', '강원도', '강원도', '강원도', '강원도', '강원도', '강원도', '강원도', '경기도', '경기도',
       '경기도', '경상남도', '경상남도', '경상남도', '경상남도', '경상남도', '경상남도', '경상남도', '경상남도',
       '경상남도', '경상남도', '경상남도', '경상북도', '경상북도', '경상북도', '경상북도', '경상북도', '경상북도',
       '경상북도', '경상북도', '경상북도', '경상북도', '경상북도', '경상북도', '경상북도', '경상북도', '경상북도',
       '경상북도', '부산광역시', '부산광역시', '인천광역시', '인천광역시', '전라남도', '전라남도', '전라남도',
       '전라남도', '전라남도', '전라남도', '전라남도', '전라남도', '전라남도', '전라남도', '전라남도', '전라남도',
       '전라남도', '전라남도', '전라남도', '전라남도', '전라북도', '전라북도', '전라북도', '전라북도', '전라북도',
       '전라북도', '전라북도', '전라북도', '전라북도', '전라북도', '충청남도', '충청남도', '충청남도', '충청남도',
       '충청남도', '충청남도', '충청남도', '충청남도', '충청남도', '충청남도', '충청북도', '충청북도', '충청북도',
       '충청북도', '충청북도'],
      dtype='object', name='광역시도')
      
------------------------------------
# 다음 단계를 위해서 인덱스를 새로 잡음
# reset_index > 인덱스를 리셋함
# 인덱스를 리셋해서 '시도' 데이터가 컬럼으로 빠져서 데이터로 사용할 수 있게 됨
# 광역시도, 시도가 인덱스에서 빠져서 컬럼으로 들어왔다
# 구분이라는 인덱스가 새로 생김
pop.reset_index(inplace=True)
pop.head()

pop.columns # MultiIndex 이다.

>>
MultiIndex([(    '광역시도',   ''),
            (      '시도',   ''),
            ('20 - 39세', '남자'),
            ('20 - 39세', '여자'),
            ('20 - 39세', '합계'),
            (  '65세 이상', '남자'),
            (  '65세 이상', '여자'),
            (  '65세 이상', '합계'),
            (     '인구수', '남자'),
            (     '인구수', '여자'),
            (     '인구수', '합계'),
            (    '소멸비율',   ''),
            (  '소멸위기지역',   '')],
           names=[None, '구분'])
------------------------------------
# 컬럼의 길이 (광역시도 ~ 소멸위기지역까지 컬럼 13개)
len(pop.columns.get_level_values(0))
>> 13
-----------------------------------
pop.columns.get_level_values(0)
>>
Index(['광역시도', '시도', '20 - 39세', '20 - 39세', '20 - 39세', '65세 이상', '65세 이상',
       '65세 이상', '인구수', '인구수', '인구수', '소멸비율', '소멸위기지역'],
      dtype='object')

-----------------------------------
pop.columns.get_level_values(0)[0]

>> '광역시도'
-----------------------------------
pop.columns.get_level_values(1)

>>
Index(['', '', '남자', '여자', '합계', '남자', '여자', '합계', '남자', '여자', '합계', '', ''], dtype='object', name='구분')

-----------------------------------
# 컬럼명 정리
# 컬럼명이 총 2줄인데 합쳐서 1줄로 만든다
tmp_columns = [
    pop.columns.get_level_values(0)[n] + pop.columns.get_level_values(1)[n]
    for n in range(0, len(pop.columns.get_level_values(0)))
]
pop.columns = tmp_columns
pop.head()

지도 시각화를 위한 지역별 ID 만들기

pop.info()

>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 264 entries, 0 to 263
Data columns (total 13 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   광역시도        264 non-null    object 
 1   시도          264 non-null    object 
 2   20 - 39세남자  264 non-null    float64
 3   20 - 39세여자  264 non-null    float64
 4   20 - 39세합계  264 non-null    float64
 5   65세 이상남자    264 non-null    float64
 6   65세 이상여자    264 non-null    float64
 7   65세 이상합계    264 non-null    float64
 8   인구수남자       264 non-null    float64
 9   인구수여자       264 non-null    float64
 10  인구수합계       264 non-null    float64
 11  소멸비율        264 non-null    float64
 12  소멸위기지역      264 non-null    bool   
dtypes: bool(1), float64(10), object(2)
memory usage: 25.1+ KB
---------------------------------
# 43 44 45
pop["시도"].unique()

>> 
array(['강릉시', '고성군', '동해시', '삼척시', '속초시', '양구군', '양양군', '영월군', '원주시',
       '인제군', '정선군', '철원군', '춘천시', '태백시', '평창군', '홍천군', '화천군', '횡성군',
       '가평군', '고양시', '과천시', '광명시', '광주시', '구리시', '군포시', '권선구', '기흥구',
       '김포시', '남양주시', '단원구', '덕양구', '동두천시', '동안구', '만안구', '부천시', '분당구',
       '상록구', '성남시', '소사구', '수원시', '수정구', '수지구', '시흥시', '안산시', '안성시',
       '안양시', '양주시', '양평군', '여주시', '연천군', '영통구', '오산시', '오정구', '용인시',
       '원미구', '의왕시', '의정부시', '이천시', '일산동구', '일산서구', '장안구', '중원구', '처인구',
       '파주시', '팔달구', '평택시', '포천시', '하남시', '화성시', '거제시', '거창군', '김해시',
       '남해군', '마산합포구', '마산회원구', '밀양시', '사천시', '산청군', '성산구', '양산시', '의령군',
       '의창구', '진주시', '진해구', '창녕군', '창원시', '통영시', '하동군', '함안군', '함양군',
       '합천군', '경산시', '경주시', '고령군', '구미시', '군위군', '김천시', '남구', '문경시',
       '봉화군', '북구', '상주시', '성주군', '안동시', '영덕군', '영양군', '영주시', '영천시',
       '예천군', '울릉군', '울진군', '의성군', '청도군', '청송군', '칠곡군', '포항시', '광산구',
       '동구', '서구', '달서구', '달성군', '수성구', '중구', '대덕구', '유성구', '강서구', '금정구',
       '기장군', '동래구', '부산진구', '사상구', '사하구', '수영구', '연제구', '영도구', '해운대구',
       '강남구', '강동구', '강북구', '관악구', '광진구', '구로구', '금천구', '노원구', '도봉구',
       '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '성북구', '송파구', '양천구',
       '영등포구', '용산구', '은평구', '종로구', '중랑구', '세종특별자치시', '울주군', '강화군', '계양구',
       '남동구', '부평구', '연수구', '옹진군', '강진군', '고흥군', '곡성군', '광양시', '구례군',
       '나주시', '담양군', '목포시', '무안군', '보성군', '순천시', '신안군', '여수시', '영광군',
       '영암군', '완도군', '장성군', '장흥군', '진도군', '함평군', '해남군', '화순군', '고창군',
       '군산시', '김제시', '남원시', '덕진구', '무주군', '부안군', '순창군', '완산구', '완주군',
       '익산시', '임실군', '장수군', '전주시', '정읍시', '진안군', '서귀포시', '제주시', '계룡시',
       '공주시', '금산군', '논산시', '당진시', '동남구', '보령시', '부여군', '서북구', '서산시',
       '서천군', '아산시', '예산군', '천안시', '청양군', '태안군', '홍성군', '괴산군', '단양군',
       '보은군', '상당구', '서원구', '영동군', '옥천군', '음성군', '제천시', '증평군', '진천군',
       '청원구', '청주시', '충주시', '흥덕구'], dtype=object)
-------------------------------------
# len(pop) 은 264

si_name = [None] * len(pop)
si_name

>>
[None,
 None,
 None,
 None,
 None,
 None
 ...
 None,
 None,
 None,
 None]
---------------------------------
# 자치구는 어차피 자료에서 나타나니, 행정구를 가진 지역만 신경쓰면 된다
tmp_gu_dict = {
    "수원": ["장안구", "권선구", "팔달구", "영통구"],
    "성남": ["수정구", "중원구", "분당구"],
    "안양": ["만안구", "동안구"],
    "안산": ["상록구", "단원구"],
    "고양": ["덕양구", "일산동구", "일산서구"],
    "용인": ["처인구", "기흥구", "수지구"],
    "청주": ["상당구", "서원구", "흥덕구", "청원구"],
    "천안": ["동남구", "서북구"],
    "전주": ["완산구", "덕진구"],
    "포항": ["남구", "북구"],
    "창원": ["의창구", "성산구", "진해구", "마산합포구", "마산회원구"],
    "부천": ["오정구", "원미구", "소사구"]
}

pop.head()

pop["광역시도"].unique()

>>
array(['강원도', '경기도', '경상남도', '경상북도', '광주광역시', '대구광역시', '대전광역시', '부산광역시',
       '서울특별시', '세종특별자치시', '울산광역시', '인천광역시', '전라남도', '전라북도', '제주특별자치도',
       '충청남도', '충청북도'], dtype=object)
       
------------------------------
pop["시도"].unique()

>>
array(['강릉시', '고성군', '동해시', '삼척시', '속초시', '양구군', '양양군', '영월군', '원주시',
       '인제군', '정선군', '철원군', '춘천시', '태백시', '평창군', '홍천군', '화천군', '횡성군',
       '가평군', '고양시', '과천시', '광명시', '광주시', '구리시', '군포시', '권선구', '기흥구',
       '김포시', '남양주시', '단원구', '덕양구', '동두천시', '동안구', '만안구', '부천시', '분당구',
       '상록구', '성남시', '소사구', '수원시', '수정구', '수지구', '시흥시', '안산시', '안성시',
       '안양시', '양주시', '양평군', '여주시', '연천군', '영통구', '오산시', '오정구', '용인시',
       '원미구', '의왕시', '의정부시', '이천시', '일산동구', '일산서구', '장안구', '중원구', '처인구',
       '파주시', '팔달구', '평택시', '포천시', '하남시', '화성시', '거제시', '거창군', '김해시',
       '남해군', '마산합포구', '마산회원구', '밀양시', '사천시', '산청군', '성산구', '양산시', '의령군',
       '의창구', '진주시', '진해구', '창녕군', '창원시', '통영시', '하동군', '함안군', '함양군',
       '합천군', '경산시', '경주시', '고령군', '구미시', '군위군', '김천시', '남구', '문경시',
       '봉화군', '북구', '상주시', '성주군', '안동시', '영덕군', '영양군', '영주시', '영천시',
       '예천군', '울릉군', '울진군', '의성군', '청도군', '청송군', '칠곡군', '포항시', '광산구',
       '동구', '서구', '달서구', '달성군', '수성구', '중구', '대덕구', '유성구', '강서구', '금정구',
       '기장군', '동래구', '부산진구', '사상구', '사하구', '수영구', '연제구', '영도구', '해운대구',
       '강남구', '강동구', '강북구', '관악구', '광진구', '구로구', '금천구', '노원구', '도봉구',
       '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '성북구', '송파구', '양천구',
       '영등포구', '용산구', '은평구', '종로구', '중랑구', '세종특별자치시', '울주군', '강화군', '계양구',
       '남동구', '부평구', '연수구', '옹진군', '강진군', '고흥군', '곡성군', '광양시', '구례군',
       '나주시', '담양군', '목포시', '무안군', '보성군', '순천시', '신안군', '여수시', '영광군',
       '영암군', '완도군', '장성군', '장흥군', '진도군', '함평군', '해남군', '화순군', '고창군',
       '군산시', '김제시', '남원시', '덕진구', '무주군', '부안군', '순창군', '완산구', '완주군',
       '익산시', '임실군', '장수군', '전주시', '정읍시', '진안군', '서귀포시', '제주시', '계룡시',
       '공주시', '금산군', '논산시', '당진시', '동남구', '보령시', '부여군', '서북구', '서산시',
       '서천군', '아산시', '예산군', '천안시', '청양군', '태안군', '홍성군', '괴산군', '단양군',
       '보은군', '상당구', '서원구', '영동군', '옥천군', '음성군', '제천시', '증평군', '진천군',
       '청원구', '청주시', '충주시', '흥덕구'], dtype=object)

만들고자 하는 ID 형태

  • 서울 중구
  • 통영
  • 남양주
  • 포항 북구
  • 인천 남동
  • 안산 단원
    ...

(1) 일반 시의 이름과, 세종시, 그리고 광역시도의 일반 구를 정리한다

# 위 '광역시도'와 '시도'를 합쳐서
# 서울 중구, 남양주, 포항 북구, 안산 단원 이런식으로 만들어준다


# (1) 일반 시의 이름과, 세종시, 그리고 광역시도의 일반 구를 정리한다

for idx, row in pop.iterrows():
    # 광역시나 특별시, 자치시가 아닌 경우의 행정구에 대해서만 적용한다
    # 광역시도 컬럼의 마지막 3글자가 '광역시', '특별시', '자치시'가 아니라면
    # 일반 '도'에 있는 도시이다. 그래서 마지막 한글자를 빼라 
    # 강릉시 => 강릉
    # 춘천시 => 춘천
    # 이런식으로 정리한다
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        si_name[idx] = row["시도"][:-1] # 진주시 > 진주

    # 세종시는 특별히 관리하고
    # 세종특별자치시 > 세종
    elif row["광역시도"] == "세종특별자치시":
        si_name[idx] = "세종"

    # 위 두개를 제외하면 나머지는 광역시뿐인데,
    # row["광역시도"][:2] 이 부분은 서울, 부산, 대구 이렇게 앞글자 2개만 따고
    # row["시도"] 중구 => 중구
    # row["시도"][:-1] 강남구 => 강남과 같이 정리한다.
    # 결국 서울 서초, 서울 중구 이런식으로 
    else:
        if len(row["시도"]) == 2: # 서구, 동구, 중구...
            # 서울 중구, 부산 금정
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"]
        else:
            # 서울 영등포, 서울 서대문
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"][:-1]
            
 -------------------------------------
 si_name

>>
['강릉',
 '고성',
 '동해',
 '삼척',
 '속초',
 '양구',
 '양양',
 '영월',
 '원주',
 '인제',
 '정선',
 '철원',
 '춘천',
 '태백',
 '평창',
 ...
 '대전 서구',
 '대전 유성',
 '대전 중구',
 '부산 강서',
 '부산 금정',
 '부산 기장',
 '부산 남구',
 '부산 동구',
 '부산 동래',
 '부산 부산진',
 '부산 북구',
 '부산 사상',
 '부산 사하',
 '부산 서구',
 '부산 수영',
 '부산 연제',
 '부산 영도',
 '부산 중구',
 '부산 해운대',
 '서울 강남',
 '서울 강동',
 '서울 강북',
 '서울 강서',
 '서울 관악',
 '서울 광진',
 '서울 구로',
 '서울 금천',
 '서울 노원',
 '서울 도봉',
 '서울 동대문',
 '서울 동작',
 '서울 마포',
 '서울 서대문',
 '서울 서초',
 '서울 성동',
 '서울 성북',
 '서울 송파',
 '서울 양천',
 '서울 영등포',
 '서울 용산',
 '서울 은평',
 '서울 종로',
 '서울 중구',
 '서울 중랑',
 '세종',
 '울산 남구',
 '울산 동구',
 '울산 북구',
 '울산 울주',
 '울산 중구',
 '인천 강화',
 '인천 계양',
 '인천 남구',
 '인천 남동',
 '인천 동구',
 '인천 부평',
 '인천 서구',
 '인천 연수',
 '인천 옹진',
 '인천 중구',
 '강진',
 ...
 '청원',
 '청주',
 '충주',
 '흥덕']

(2) 행정 구를 정리한다

# 행정구에 대해 특별시 다시 계산한다

for idx, row in pop.iterrows():
    # 광역시나 특별시, 자치시가 아닌 경우의 행정구에 대해서만 적용한다
    # 광역시도 컬럼의 마지막 3글자가 '광역시', '특별시', '자치시'가 아니라면
    # tmp_gu_dict 에서 하나씩 가지고 온다
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        # 행정구를 지정한 dict형 자료에 있는 지역인지 검색하고
        for keys, values in tmp_gu_dict.items():
            # 행정구를 가진 시의 구 이름을 tmp_gu_dict 컬럼이 가지고 있다면
            if row["시도"] in values:
            
                # 분당구 => 분당, 북구 => 북구
                # 특별히 너무 긴 이름의 구는 짧게 처리한다
                if len(row["시도"]) == 2:
                    si_name[idx] = keys + " " + row["시도"]
                elif row["시도"] in ["마산합포구", "마산회원구"]:
                    si_name[idx] = keys + " " + row["시도"][2:-1] # 합포, 회원만 가지고 온다
                else:
                    si_name[idx] = keys + " " + row["시도"][:-1]
--------------------------------
si_name

>>
['강릉',
 '고성',
 '동해',
 '삼척',
 ...
'광주',
 '구리',
 '군포',
 '수원 권선',
 '용인 기흥',
 '김포',
 '남양주',
 '안산 단원',
 '고양 덕양',
 '동두천',
 '안양 동안',
 '안양 만안',
 '부천',
 '성남 분당',
 '안산 상록',
 '성남',
 '부천 소사',
 '수원',
 '성남 수정',
 '용인 수지',
 '시흥',
 '안산',
 '안성',
 ...
  '태안',
 '홍성',
 '괴산',
 '단양',
 '보은',
 '청주 상당',
 '청주 서원',
 '영동',
 '옥천',
 '음성',
 '제천',
 '증평',
 '진천',
 '청주 청원',
 '청주',
 '충주',
 '청주 흥덕']
  

(3) 고성군 정리

# 특별히 고성군에 대해 한번 더 신경써준다(강원 고성, 경남 고성)
for idx, row in pop.iterrows():
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        if row["시도"][:-1] == "고성" and row["광역시도"] == "강원도":
            si_name[idx] = "고성(강원)"
        elif row["시도"][:-1] == "고성" and row["광역시도"] == "경상남도":
            si_name[idx] = "고성(경남)"
            
---------------------
si_name

>>
['강릉',
 '고성(강원)',
 '동해',
 '삼척',
 '속초',
 '양구',
 '양양',
 '영월',
 ...
'의정부',
 '이천',
 '고양 일산동',
 '고양 일산서',
 '수원 장안',
 '성남 중원',
 '용인 처인',
 '파주',
 '수원 팔달',
 '평택',
 '포천',
 '하남',
 '화성',
 '거제',
 '거창',
 '고성(경남)',
 '김해',
 '남해',
 '창원 합포',
 '창원 회원',
 '밀양',
 '사천',
 '산청',
 '창원 성산',
 '양산',
  ...
 '단양',
 '보은',
 '청주 상당',
 '청주 서원',
 '영동',
 '옥천',
 '음성',
 '제천',
 '증평',
 '진천',
 '청주 청원',
 '청주',
 '충주',
 '청주 흥덕']
-------------------------
# ID 란 컬럼을 만들어주고 여기에 넣어준다
pop["ID"] = si_name
pop

# 인구 소멸 위기 지역 파악도 끝냈고,
# 지도에 그리기 위한 ID도 다 생성했다.
# 필요없는 컬럼은 없애준다
del pop["20 - 39세남자"]
del pop["65세 이상남자"]
del pop["65세 이상여자"]

pop.head()

4. 지도 그리기 : 카르토그램

# 엑셀에서 그린 지도 모양을 읽어온다
draw_korea_raw = pd.read_excel("../data/07_draw_korea_raw.xlsx")
draw_korea_raw

# 각 지역별 위치가 나타났다
# 0번 인덱스에는 7, 8 , 9, 10에 값이 채워져있고, 값은 철원~~ 등등이다
draw_korea_raw_stacked = pd.DataFrame(draw_korea_raw.stack())
draw_korea_raw_stacked

# 인덱스로 나타난 좌표를 데이터로 사용하기 위해 reset_index
draw_korea_raw_stacked.reset_index(inplace=True)
draw_korea_raw_stacked

# 이름도 바꿔주고
draw_korea_raw_stacked.rename(
    columns={"level_0":"y", "level_1":"x", 0:"ID"}, inplace=True
)
draw_korea_raw_stacked

# 변수명도 그냥 바꿔준다
draw_korea = draw_korea_raw_stacked
----------------------------
# 경계선을 수작업으로 만들어준다...
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)]
]
------------------------------
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)]]
 --------------------------------------
# 시도의 이름을 표현하는 함수
# 현재는 데스트용.
# dispname는 실제로 나타낼 이름을 의미한다
def plot_text_simple(draw_korea):
    for idx, row in draw_korea.iterrows():
        # 공백으로 나눴을때 길이가 2이면 : 서울 중구, 용인 기흥 ...
        if len(row["ID"].split()) == 2:
            # {}\n{} : 다음 줄로 내려주라는 뜻(?) 엑셀 캡처처럼 서울\n마포 이런식으로 표현하기 위함
            dispname = "{}\n{}".format(row["ID"].split()[0], row["ID"].split()[1])

        # 강원고성, 경남고성은 위치적으로는 구분이 가능하므로 그냥 고성
        elif row["ID"][:2] == "고성":
            dispname = "고성"
        else:
            dispname = row["ID"] # 화성, 시흥, 진주,,

        # annotate 은 주석을 달기 위한 기능(글자를 나타내기 위함)
        # dispname의 글자를 쓴다
        plt.annotate(
            dispname,
            (row["x"] + 0.5, row["y"] + 0.5), # 경계에 글자가 있으면 안되므로 띄운다
            weight = "bold"
        ) 
-------------------------------------------------
# 간단히 경계선과 시도 이름만 먼저 도전
def simpleDraw(draw_korea):
    plt.figure(figsize=(8, 11))

    plot_text_simple(draw_korea)

    for path in BORDER_LINES:
        # BORDER_LINES 은 x, y 의 연속인데
        # 이걸 plot 으로 나타내면 x끼리, y끼리 모아야됨
        ys, xs = zip(*path)
        print(ys, xs) # y끼리, x끼리 모여있음

-----------------------------------------------
simpleDraw(draw_korea)
# 아래 (5, 5, 7, 7, 11, 11)은 BORDER_LINES에서 인천의 x좌표만 모은 것임
# 아래 (1, 2, 2, 3, 3, 0)은 BORDER_LINES에서 인천의 y좌표만 모은 것임

>>
(5, 5, 7, 7, 11, 11) (1, 2, 2, 3, 3, 0)
(5, 5, 2, 2, 4, 4, 7, 7, 9, 9, 10, 10, 5) (4, 5, 5, 7, 7, 9, 9, 7, 7, 5, 5, 4, 4)
(1, 1, 3, 3, 10, 10, 12, 12, 11, 11, 12, 12, 11, 11) (7, 8, 8, 10, 10, 7, 7, 6, 6, 5, 5, 4, 4, 3)
(8, 8, 6, 6) (10, 11, 11, 12)
(12, 13, 13, 14, 14, 15, 15, 16, 16) (5, 5, 4, 4, 5, 5, 4, 4, 2)
(16, 17, 17, 16, 16, 19, 19, 20, 20, 21, 21, 19, 19) (4, 4, 5, 5, 6, 6, 5, 5, 4, 4, 3, 3, 1)
(13, 13, 16) (5, 6, 6)
(13, 14) (5, 5)
(21, 21, 22, 22, 24, 24, 21) (2, 3, 3, 4, 4, 2, 2)
(20, 21, 21, 23) (5, 5, 6, 6)
(10, 12, 12, 14, 14, 16, 16) (8, 8, 9, 9, 8, 8, 6)
(14, 14, 14, 13, 13) (9, 11, 12, 12, 13)
(15, 17, 17, 16, 16, 14) (8, 8, 10, 10, 11, 11)
(17, 18, 18, 19, 19, 20, 20, 21) (9, 9, 8, 8, 9, 9, 10, 10)
(16, 16) (11, 13)
(27, 27, 25) (5, 6, 6)

# 시도의 이름을 표현하는 함수
# 현재는 데스트용.
# dispname는 실제로 나타낼 이름을 의미한다
def plot_text_simple(draw_korea):
    for idx, row in draw_korea.iterrows():
        # 공백으로 나눴을때 길이가 2이면 : 서울 중구, 용인 기흥 ...
        if len(row["ID"].split()) == 2:
            # {}\n{} : 다음 줄로 내려주라는 뜻(?) 엑셀 캡처처럼 서울\n마포 이런식으로 표현하기 위함
            dispname = "{}\n{}".format(row["ID"].split()[0], row["ID"].split()[1])

        # 강원고성, 경남고성은 위치적으로는 구분이 가능하므로 그냥 고성
        elif row["ID"][:2] == "고성":
            dispname = "고성"
        else:
            dispname = row["ID"] # 화성, 시흥, 진주,,

        # 이름이 3글자 이상이면 폰트크기를 좀 줄여라
        if len(dispname.splitlines()[-1]) >= 3:
            fontsize, linespacing = 9.5, 1.5
        else:
            fontsize, linespacing = 11, 1.2

        # annotate 은 주석을 달기 위한 기능(글자를 나타내기 위함)
        # dispname의 글자를 쓴다
        plt.annotate(
            dispname,
            (row["x"] + 0.5, row["y"] + 0.5), # 경계에 글자가 있으면 안되므로 띄운다
            weight = "bold",
            fontsize = fontsize, # 위에서 설정한 fontsize 
            ha = "center", # 수평 정렬 (좌우 가운데 정렬)
            va = "center", # 수직 정렬 (위아래 가운데 정렬)
            linespacing = linespacing #  위에서 설정한 linespacing
        )
------------------------------------
# 간단히 경계선과 시도 이름만 먼저 도전
def simpleDraw(draw_korea):
    plt.figure(figsize=(8, 11))

    plot_text_simple(draw_korea)

    for path in BORDER_LINES:
        # BORDER_LINES 은 x, y 의 연속인데
        # 이걸 plot 으로 나타내면 x끼리, y끼리 모아야됨
        ys, xs = zip(*path)
        plt.plot(xs, ys, c="black", lw= 1.5)

    # invert_yaxis() : matplotlib 그래프에서는 y 값이 올라가면 밑에서 위로 올라가는데
    # 그런데 엑셀은 0부터 아래로 증가하는 방향이므로 방향이 반대다
    plt.gca().invert_yaxis()
    plt.axis("off")
    plt.tight_layout()
    plt.show()

simpleDraw(draw_korea)

검증 작업 set()

# 데이터 검증
# 데이터를 합칠때 ID 를 기준으로 합칠건데, 그 전에 검증을 한다
# 반환 값으로 set() 이 나와야 정상이다.
set(draw_korea["ID"].unique()) - set(pop["ID"].unique())

>> set()
--------------------------
# 근데 반대로 하면 차집합이 있다.
# 차집합은 교환법칙이 성립하지 않는다.
# 광역시는 아닌데 행정구를 가지고 있던 도시들이다.
set(pop["ID"].unique()) - set(draw_korea["ID"].unique())

>>
{'고양', '부천', '성남', '수원', '안산', '안양', '용인', '전주', '창원', '천안', '청주', '포항'}
---------------------------
# 이 경우는 지우면 된다. 왜?
# 이미 행정구별로 인구현황을 잡아놨기 때문(?)
tmp_list = list(set(pop["ID"].unique()) - set(draw_korea["ID"].unique()))

for tmp in tmp_list:
    pop = pop.drop(pop[pop["ID"] == tmp].index)

print(set(pop["ID"].unique()) - set(draw_korea["ID"].unique()))
# set() 이 나왔다

>> set()
------------------------------
# 그럼 지도를 그리기 위한 데이터와 인구현황 데이터를 합치자
# ID 를 기준으로 합친다
# how="left" 아래 pop, draw_korea 코드에서 왼쪽 데이터인 pop 을 기준으로 한다
#   on=["ID"] : ID 컬럼을 기준으로 합친다
pop = pd.merge(pop, draw_korea, how="left", on=["ID"])

pop.head()

그림을 그리기 위한 데이터를 계산하는 함수

  • 색상을 만들 때, 최소값을 흰색으로
    blockedMap : 인구현황(pop)
  • targetData : 그리고 싶은 컬럼
# 그림을 그리기 위한 데이터를 계산하는 함수
# 이 함수는 색상을 만들때 최소값을 흰색으로 한다
# blockedMap은 인구현황이고, targetData 는 그리고 싶은 컬럼
# blockedMap : 인구현황(pop)
# targetData : 그리고 싶은 컬럼
def get_data_info(targetData, blockedMap):
    # 배경색에 맞는 글자색의 경계선을 지정
    whitelabelmin = (
        max(blockedMap[targetData]) - min(blockedMap[targetData])
    ) * 0.25 + min(blockedMap[targetData])
    vmin = min(blockedMap[targetData])
    vmax = max(blockedMap[targetData])

    # 엑셀로 만든 지도에서 시도 이름에 숫자가 들어간다
    mapdata = blockedMap.pivot_table(index="y", columns="x", values=targetData)

    return mapdata, vmax, vmin, whitelabelmin
------------------------------------
# 그림을 그리기 위한 데이터를 계산하는 함수
# 이 함수는 색상을 만들때 중간값을 흰색으로 한다
# blockedMap은 인구현황이고, targetData 는 그리고 싶은 컬럼
def get_data_info_for_zero_center(targetData, blockedMap):
    whitelabelmin = 5

    tmp_max = max(
        [np.abs(min(blockedMap[targetData])), np.abs(max(blockedMap[targetData]))]
    )
    
    vmin, vmax = -tmp_max, tmp_max
    
    mapdata = blockedMap.pivot_table(index="y", columns="x", values=targetData)

    return mapdata, vmax, vmin, whitelabelmin
--------------------------------------------
# dispname는 실제로 나타낼 이름을 의미한다
def plot_text(targetData, blockedMap, whilelabelmin):
    for idx, row in blockedMap.iterrows():
        # 공백으로 나눴을때 길이가 2이면 : 서울 중구, 용인 기흥 ...
        if len(row["ID"].split()) == 2:
            # {}\n{} : 다음 줄로 내려주라는 뜻(?) 엑셀 캡처처럼 서울\n마포 이런식으로 표현하기 위함
            dispname = "{}\n{}".format(row["ID"].split()[0], row["ID"].split()[1])

        # 강원고성, 경남고성은 위치적으로는 구분이 가능하므로 그냥 고성
        elif row["ID"][:2] == "고성":
            dispname = "고성"
        else:
            dispname = row["ID"] # 화성, 시흥, 진주,,

        # 이름이 3글자 이상이면 폰트크기를 좀 줄여라
        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]) > whilelabelmin else "black"

        # annotate 은 주석을 달기 위한 기능(글자를 나타내기 위함)
        # dispname의 글자를 쓴다
        plt.annotate(
            dispname,
            (row["x"] + 0.5, row["y"] + 0.5), # 경계에 글자가 있으면 안되므로 띄운다
            weight = "bold",
            color = annocolor, 
            fontsize = fontsize, # 위에서 설정한 fontsize 
            ha = "center", # 수평 정렬 (좌우 가운데 정렬)
            va = "center", # 수직 정렬 (위아래 가운데 정렬)
            linespacing = linespacing #  위에서 설정한 linespacing
        )
------------------------------------------
def drawKorea(targetData, blockedMap, cmapname, zeroCenter=False):
    # zeroCenter=True 이면 아래가 실행됨
    if zeroCenter:
        masked_mapdata, vmax, vmin, whitelabelmin = get_data_info_for_zero_center(targetData, blockedMap)

    # zeroCenter=False 이면 아래가 실행됨
    if not zeroCenter:
        masked_mapdata, vmax, vmin, whitelabelmin = 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, whitelabelmin)

    for path in BORDER_LINES:
        # BORDER_LINES 은 x, y 의 연속인데
        # 이걸 plot 으로 나타내면 x끼리, y끼리 모아야됨
        ys, xs = zip(*path)
        plt.plot(xs, ys, c="black", lw=2)

    plt.gca().invert_yaxis()
    plt.axis("off")
    cb=plt.colorbar(shrink=0.1, aspect=10)
    cb.set_label(targetData)
    plt.tight_layout()
    plt.show()
--------------------------------------
drawKorea("인구수합계", pop, "Blues")

pop["소멸위기지역"] = [1 if con else 0 for con in pop["소멸위기지역"]]
drawKorea("소멸위기지역", pop, "Reds")

pop["여성비"] = (pop["인구수여자"] / pop["인구수합계"] - 0.5) * 100
drawKorea("여성비", pop, "RdBu", zeroCenter=True)

pop["2030여성비"] = (pop["20 - 39세여자"] / pop["20 - 39세합계"] - 0.5) * 100
drawKorea("2030여성비", pop, "RdBu", zeroCenter=True)

import folium
import json

pop_folium = pop.set_index("ID") # ID 가 인덱스로 들어감
pop_folium.head()

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

mymap = folium.Map(location=[36.2002, 127.054], zoom_start=7)
# choropleth : 경계선 그리기
mymap.choropleth(
    geo_data = geo_str,
    data = pop_folium["인구수합계"],
    columns = [pop_folium.index, pop_folium["인구수합계"]],
    fill_color = "YlGnBu", # PuRd, YlGnBu
    key_on = "feature.id"
)

mymap

mymap = folium.Map(location=[36.2002, 127.054], zoom_start=7)
mymap.choropleth(
    geo_data = geo_str,
    data = pop_folium["소멸위기지역"],
    columns = [pop_folium.index, pop_folium["소멸위기지역"]],
    fill_color = "PuRd", # PuRd, YlGnBu
    key_on = "feature.id"
)

mymap

# 데이터 저장
draw_korea.to_csv("../data/the-end", encoding="utf-8", sep=",")

위 글은 제로베이스 데이터 취업 스쿨의 강의자료를 참고하여 작성되었습니다.

profile
허재

0개의 댓글

관련 채용 정보