Population by gender and age

  • Matplotlib 객체지향 방식은 훌륭합니다.
  • 객체를 섬세하게 제어할 수 있고, 시각화 함수를 만들기도 좋습니다.
  • 데이터 정리부터 객체지향 방식으로 그림을 그리는 예제를 만들어 보았습니다.

1. 목표

  • 우리나라 인구 데이터로부터 이런 그림을 그릴 것입니다.


  • 한번 그리고 말 것이 아닙니다.

  • 시나 도 이름을 입력하면 데이터만 바뀐 같은 형식의 그림을 찍어낼 것입니다.


2. 데이터 준비

모두의 데이터 분석 with 파이썬
주민등록 인구 및 세대현황

  • 모두의 데이터 분석 with 파이썬에는 인구 데이터를 내려받아 분포를 bar plot으로 표현하는 내용이 있습니다.
  • 같은 데이터를 조금 더 깔끔하게, 조금 체계적으로 만들어 보겠습니다.

2.1. 데이터 다운로드

  • 아래와 같이 주민등록 인구 및 세대현황자료에서 csv 파일을 다운받습니다.

  • 파일 다운로드

  • 주피터 노트북을 켜고 데이터 분석을 준비합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
%matplotlib inline

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from copy import deepcopy

# 시각화 설정
sns.set_context("talk")
sns.set_style("white")

# Linux 한글 사용 설정
plt.rcParams['font.family']=['NanumGothic', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False

2.2. 데이터 전처리

  • 다운받은 파일을 파이썬으로 열어봅니다.
1
2
df_popkr = pd.read_csv("202108_202108_연령별인구현황_월간.csv", encoding="euc-kr")
df_popkr.head()


  • 연령대와 성별이 column으로 정리되어 있고 행정구역들이 첫번째 column에 나열되어 있습니다.
  • 분석하기에 좋은 모양은 아닙니다. 전처리를 들어갑니다.

2.2.1. 데이터 내용 정리

  • 행정구역에서 불필요한 번호를 떼어냅니다.
1
2
df_popkr["행정구역"] = df_popkr["행정구역"].str.split("(").str[0]
df_popkr.head()


  • 행정구역에서 번호가 떨어진 뒤에 빈 칸이 붙어 있습니다.
  • 숫자들 사이에는 자리수를 표현하는 쉼표(,)가 붙어 있습니다.
  • df.replace()로 처리합니다.
1
2
3
df_popkr.replace(",", "", regex=True, inplace=True)
df_popkr.replace(" ", "", regex=True, inplace=True)
df_popkr.head()


2.2.2. 성별 분리

  • 남성과 여성 데이터를 따로 그릴 것입니다. 데이터부터 분리합니다.
  • 먼저 남성 데이터를 정리합니다.
  • 컬럼명에 “남”이 있는 것들을 골라냅니다.
  • 그 중에서도 불필요한 내용을 제거하기 위해 “세”가 있는 것만 또 따로 분리합니다.
1
2
df_popkrM = df_popkr.filter(like="남").filter(like="세")
df_popkrM.head()


2.2.3. 행-열 전환

  • 데이터를 편하게 다루려면 열에 지역명, 행에 나이대를 놓는 것이 좋습니다.
  • df.T로 행과 열을 바꾼 뒤 df.astype(int)로 정수형으로 변환합니다.
1
2
3
4
df_popkrMT = df_popkrM.T
df_popkrMT.columns = df_popkr["행정구역"].values
df_popkrMT = df_popkrMT.astype(int)
df_popkrMT.head()


2.2.4. “나이”만 남기기

  • 나이 정보가 index로 오긴 했지만 불필요한 정보들이 많습니다.
  • pd.Series.str.split()을 사용해 나이만 남깁니다.
1
2
3
df_popkrMT["나이"] = df_popkrMT.index.str.split("_").str[2]
df_popkrMT.reset_index(drop=True, inplace=True)
df_popkrMT


2.2.5. 여성 데이터 정리

  • 같은 요령으로 여성 데이터도 정리합니다.
  • 위에서 뭔가 복잡하게 한 것 같지만 코드 6줄로 정리됩니다.
1
2
3
4
5
6
7
df_popkrF = df_popkr.filter(like="여").filter(like="세")
df_popkrFT = df_popkrF.T
df_popkrFT.columns = df_popkr["행정구역"].values
df_popkrFT = df_popkrFT.astype(int)
df_popkrFT["나이"] = df_popkrFT.index.str.split("_").str[2]
df_popkrFT.reset_index(drop=True, inplace=True)
df_popkrFT.head(3)


3. 데이터 시각화

  • 이제 정리한 데이터를 그림으로 표현할 차례입니다.

  • 글의 맨 처음에서 어떻게 그리겠다는 것을 먼저 보여드리긴 했지만 여기서 고민이 필요합니다.

  • 남 vs 여구도를 어떻게 살릴지,

  • 연령대간 변화율이 아닌 연령대별 데이터를 어떻게 표현할지에 대한 고민입니다.

  • 성별, 연령별 인구 분포는 많이 사용하는 형식이 있습니다.

  • 등을 맞대고 있는 구도남 vs 여를 표현합니다.

  • bar plot으로 해당 구간의 데이터에 집중합니다.

    대강 이런 그림입니다.

3.1. Axes 제작

  • plt.subplots()명령으로 fig(Figure)와 axs(Axes)를 동시에 생성합니다.
  • ncols=2로 bar plot이 들어갈 Axes를 두 개 만듭니다.
  • gridspec_kw={"wspace":0}으로 Axes 사이 간격을 없앱니다.
  • sharey=True로 두 Axes의 y 범위를 통일합니다.
1
2
fig, axs = plt.subplots(ncols=2, sharey=True, 
figsize=(10, 5), gridspec_kw={"wspace":0})


3.2. bar plot

  • 공간이 준비됐으니 데이터를 넣습니다.
  • ax.barh()명령으로 가로 bar plot을 그립니다.
  • x에 나이, y에 전국 인구 수를 넣습니다.
  • 남성은 green, 여성은 darkorange로 표현합니다.
1
2
3
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})
axs[0].barh(df_popkrMT["나이"], df_popkrMT["전국"], color="green")
axs[1].barh(df_popkrFT["나이"], df_popkrFT["전국"], color="darkorange")


3.3. 남성 좌우 반전

  • 남녀가 등을 맞대고 있도록 표현하려면 남성 데이터의 좌우를 뒤집어야 합니다.
  • 데이터 범위의 최대값xmax을 충분히 큰 수로 지정하고,
  • 남성은 xmax에서 0으로, 여성은 0에서 xmax로 가도록 지정합니다.
  • ax.set_xlim()을 사용합니다.
1
2
3
4
5
6
7
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})
axs[0].barh(df_popkrMT["나이"], df_popkrMT["전국"], color="green")
axs[1].barh(df_popkrFT["나이"], df_popkrFT["전국"], color="darkorange")

xmax = 4.5e6
axs[0].set_xlim(xmax, 0)
axs[1].set_xlim(0, xmax)


3.4. xticklabels 수정

  • x축 눈금 레이블(xticklabels)이 0부터 4까지 적혀있고 1e6이라는 숫자가 함께 있습니다.
  • $0 \times 10^6$ ~ $4\times 10^6$ 이라는 의미이지만 잘 읽히지 않습니다.
  • $4\times 10^6$은 우리 뇌 속에서 4,000,000이 되고, 다시 4백만이 됩니다.
  • 이럴 바에는 그냥 4백만이라고 써 주는게 낫습니다.
  • f-string과 list comprehension을 이용해 xticklabels를 수정합니다.
  • Axes 두 개에 똑같은 일을 해야 합니다. for loop으로 반복시킵니다.
1
2
3
4
5
6
7
8
9
10
11
12
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})
axs[0].barh(df_popkrMT["나이"], df_popkrMT["전국"], color="green")
axs[1].barh(df_popkrFT["나이"], df_popkrFT["전국"], color="darkorange")

xmax = 4.5e6
axs[0].set_xlim(xmax, 0)
axs[1].set_xlim(0, xmax)

xticks = np.arange(0, xmax, 1e6)
for ax in axs:
ax.set_xticks(xticks)
ax.set_xticklabels([f"{int(x*1e-6)}백만" if x != 0 else "0" for x in xticks])


3.5. title, grid 추가

  • 남성과 여성을 나란히 그렸으니 어디가 어디인지 적어줘야 합니다.
  • 중앙에서 좌우로 뻗은 막대들이 언뜻 보면 비슷합니다.
  • 정량적인 비교를 돕기 위해 grid를 추가합니다.
  • 제목은 ax.set_title(), 눈금은 ax.grid()명령입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})
axs[0].barh(df_popkrMT["나이"], df_popkrMT["전국"], color="green")
axs[1].barh(df_popkrFT["나이"], df_popkrFT["전국"], color="darkorange")

xmax = 4.5e6
axs[0].set_xlim(xmax, 0)
axs[1].set_xlim(0, xmax)

xticks = np.arange(0, xmax, 1e6)
for ax, title in zip(axs, ["남성", "여성"]):
ax.set_xticks(xticks)
ax.set_xticklabels([f"{int(x*1e-6)}백만" if x != 0 else "0" for x in xticks])
ax.grid(c="lightgray")
ax.set_title(title, color="gray", fontweight="bold", pad=16)


  • 이제 남성과 여성의 차이가 조금 눈에 들어옵니다.
  • 70대 여성은 2백만명이 넘지만 70대 남성은 2백만명이 못 됩니다.
  • 10세 미만은 여성보다 남성이 더 많습니다.

3.6. text 추가

  • grid 덕택에 조금은 읽기가 수월해졌지만 정확한 값은 보이지 않습니다.

  • 숫자는 신빙성을 부여합니다. 활용성도 높아집니다.

  • ax.text()명령으로 글자를 넣을 수 있습니다.

  • 그런데 어디에 넣을까요?

  • 막대 끝에 넣으면 어떨까요? 데이터의 편차가 돋보일 것입니다.

  • bar plot은 Matplotlib이 patch라는 객체로 관리합니다.

  • ax.patches[0]이라면 맨 처음에 붙인 객체를 의미합니다.

  • ax.patches와 for loop을 결합하면 하나씩 순회하면서 숫자를 달아봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})
axs[0].barh(df_popkrMT["나이"], df_popkrMT["전국"], color="green")
axs[1].barh(df_popkrFT["나이"], df_popkrFT["전국"], color="darkorange")

xmax = 4.5e6
axs[0].set_xlim(xmax, 0)
axs[1].set_xlim(0, xmax)

xticks = np.arange(0, xmax, 1e6)
for ax, title in zip(axs, ["남성", "여성"]):
ax.set_xticks(xticks)
ax.set_xticklabels([f"{int(x*1e-6)}백만" if x != 0 else "0" for x in xticks])
ax.grid(c="lightgray")
ax.set_title(title, color="gray", fontweight="bold", pad=16)

for ax in axs:
for i, p in enumerate(ax.patches):
w = p.get_width()
ax.text(w, i, f" {format(w, ',')} ",
fontsize="x-small", va="center", ha="right")


  • 2중 for loop으로 Axes마다, patch마다 숫자를 달았습니다.
  • ax.patch.get_width()를 적용해서 위치를 구했습니다.
  • format()으로 천 단위마다 쉼표를 추가했고, ha=”right”로 우측 정렬을 했습니다.
  • 그런데 여성 데이터마저 우측 정렬이 되어버렸습니다. if를 사용해 여성은 좌측 정렬을 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})
axs[0].barh(df_popkrMT["나이"], df_popkrMT["전국"], color="green")
axs[1].barh(df_popkrFT["나이"], df_popkrFT["전국"], color="darkorange")

xmax = 6e6
axs[0].set_xlim(xmax, 0)
axs[1].set_xlim(0, xmax)

xticks = np.arange(0, xmax, 1e6)
for ax, title in zip(axs, ["남성", "여성"]):
ax.set_xticks(xticks)
ax.set_xticklabels([f"{int(x*1e-6)}백만" if x != 0 else "0" for x in xticks])
ax.grid(c="lightgray")
ax.set_title(title, color="gray", fontweight="bold", pad=16)

for ax in axs:
for i, p in enumerate(ax.patches):
w = p.get_width()
if ax == axs[0]:
ha = "right"
else:
ha = "left"

ax.text(w, i, f" {format(w, ',')} ",
fontsize="x-small", va="center", ha=ha)


3.7. 전체 제목 추가, 세부 수정

  • 이제 거의 다 그렸습니다. 한번씩 보면서 맘에 들지 않는 부분을 수정합니다.
  • 먼저 grid와 글자가 너무 난잡합니다.
  • ax.grid(axis="x")로 세로선만 남깁니다.
  • 숫자 색상에 남성과 여성에 사용한 색상을 적용합니다.
  • 그리고 왼쪽은 남성, 오른쪽은 여성이지만 이게 어디 데이터인지가 없네요.
  • fig.suptitle()로 추가합니다.
  • 구성요소간 간격 정리를 위해 fig.tight_layout()을 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})

c_M = "green"
c_F = "darkorange"
axs[0].barh(df_popkrMT["나이"], df_popkrMT["전국"], color=c_M)
axs[1].barh(df_popkrFT["나이"], df_popkrFT["전국"], color=c_F)

xmax = 6e6
axs[0].set_xlim(xmax, 0)
axs[1].set_xlim(0, xmax)

xticks = np.arange(0, xmax, 1e6)
for ax, title in zip(axs, ["남성", "여성"]):
ax.set_xticks(xticks)
ax.set_xticklabels([f"{int(x*1e-6)}백만" if x != 0 else "0" for x in xticks])
ax.grid(axis="x", c="lightgray")
ax.set_title(title, color="gray", fontweight="bold", pad=16)

for ax in axs:
for i, p in enumerate(ax.patches):
w = p.get_width()
if ax == axs[0]:
ha = "right"
c = c_M
else:
ha = "left"
c = c_F

ax.text(w, i, f" {format(w, ',')} ",
c=c, fontsize="x-small", va="center", ha=ha,
fontweight="bold", alpha=0.5)

fig.suptitle(" 전국", fontweight="bold")
fig.tight_layout()


3.8. 함수로 만들기

  • 이제 조금 쓸만해진 것 같습니다.
  • 전국 말고 시도별 데이터도 시각화하고 싶은데, 그 때마다 코드를 복붙하기 불편합니다.
  • 함수로 만들어버립니다.
  • 변수 이름도 조금은 더 직관적으로 만들고, 인구 수에 따라 적절히 스케일링할 수 있는 장치를 추가합니다.
  • 지역의 인구에 따라 백만말고 십만, , 명이 더 유용할 때도 있겠죠.
  • fig를 return해서 여차하면 추가 수정을 할 수 있도록 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def plot_pop(loc, popmax=6e6, poptick=1e6):
fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(10, 5), gridspec_kw={"wspace":0})

c_M = "green"
c_F = "darkorange"
axs[0].barh(df_popkrMT["나이"], df_popkrMT[loc], color=c_M)
axs[1].barh(df_popkrFT["나이"], df_popkrFT[loc], color=c_F)

axs[0].set_xlim(popmax, 0)
axs[1].set_xlim(0, popmax)

xticks = np.arange(0, popmax, poptick)
if poptick >= 1e6:
factor, unit = 1e-6, "백만"
elif 1e5 <= poptick < 1e6:
factor, unit = 1e-5, "십만"
elif 1e4 <= poptick < 2e5:
factor, unit = 1e-4, "만"
elif 1e3 <= poptick < 2e4:
factor, unit = 1e-3, "천"

for ax, title in zip(axs, ["남성", "여성"]):
ax.set_xticks(xticks)
ax.set_xticklabels([f"{int(x*factor)}{unit}" if x != 0 else "0" for x in xticks])
ax.grid(axis="x", c="lightgray")
ax.set_title(title, color="gray", fontweight="bold", pad=16)

for ax in axs:
for i, p in enumerate(ax.patches):
w = p.get_width()
if ax == axs[0]:
ha = "right"
c = c_M
else:
ha = "left"
c = c_F

ax.text(w, i, f" {format(w, ',')} ",
c=c, fontsize="x-small", va="center", ha=ha,
fontweight="bold", alpha=0.5)

fig.suptitle(f" {loc}", fontweight="bold")
fig.tight_layout()

return fig
  • 이 함수로 전국 인구 분포를 그려봅시다.
1
fig = plot_pop("전국")


  • 똑같이 나왔습니다. :)

  • 서울시를 해볼까요? 구간별 인구 단위가 십만으로 줄어듭니다.

1
fig = plot_pop("서울특별시", popmax=1e6, poptick=2e5)


  • 이제는 세종시 차례입니다. 구간별 인구 단위가 단위로 줄어듭니다.
1
fig = plot_pop("세종특별자치시", popmax=1e6, poptick=2e5)


  • 같은 요령으로 영호남과 다른 광역시도 그릴 수 있습니다.


4. 결론

  • 수작업 대신 코딩을 왜 하냐는 질문에 가장 좋은 답변 중 하나는 반복 작업 처리일 것입니다.
  • 수작업보다 체계적인 작업도 답변이 될 수 있을 것입니다.
  • 데이터 시각화 작업을 체계적으로, 그리고 반복에 대응하는 방법은 객체지향 방식입니다.
  • Matplotlib을 다루시는 많은 분들께 평안이 깃들기를 기원합니다.


도움이 되셨나요? 카페인을 투입하시면 다음 포스팅으로 변환됩니다

Share