import platform
import matplotlib.pyplot as plt
if platform.system() == "Windows":
plt.rc("font", family="Malgun Gothic")
elif platform.system() == "Darwin":
plt.rc("font", family="AppleGothic")
else:
plt.rc("font", family="NanumBarunGothic")
plt.rcParams["axes.unicode_minus"] = False8 matplotlib 심화: 경제학자의 그래프 그리기 (7주차)
8.1 이번 장에서 배우는 것
4장에서 첫 그래프를 그렸고, 6장에서는 수요·공급 곡선을 그렸습니다. 지금까지의 그래프가 “곡선이 화면에 나오면 성공”이었다면, 이번 장의 기준은 다릅니다. 그래프가 무엇을 말하는지, 그리고 그 말이 정직한지를 따집니다. 장을 마칠 때 여러분의 손에 남을 것은 다음과 같습니다.
- 같은 데이터로 그린 두 그래프가 왜 다른 인상을 주는지 설명하고, 모든 그래프에 “한 문장 캡션”을 다는 습관을 들입니다.
- 선·막대·산점도라는 기본 3종을, 강조 1색과 회색이라는 절제된 스타일로 다시 그립니다.
fill_between으로 소비자·생산자 잉여를 색칠하고,annotate의 화살표로 곡선의 이동을 가리키고,subplots로 비교정학을 나란히 놓습니다.- 소득 10분위 자료로 로렌츠 곡선을 그리고, 지니계수를 사다리꼴 근사로 직접 계산합니다. 이 장의 중심 과제입니다.
- 생산가능곡선의 기회비용 체증과 필립스 곡선의 상충관계를 그림 한 장으로 옮깁니다.
rcParams로 우리 교재의 표준 스타일을 만들어, 남은 장에서 계속 재사용합니다.
8.2 여는 질문: 사장님의 그래프는 정직한가
이콘 카페 사장님이 은행에 운영 자금 대출을 신청하면서 매출 그래프를 한 장 만들었습니다. 3월부터 8월까지 월 매출을 막대로 그렸더니, 8월의 막대가 3월의 막대보다 다섯 배쯤 높게 솟아 있습니다. 사장님은 흐뭇합니다. “장사가 이렇게 잘 됩니다.” 그런데 숫자를 보면 3월 매출은 1,520만 원, 8월은 1,690만 원. 반년 동안 약 11% 늘었을 뿐입니다. 성실한 성장이지만, 다섯 배로 보일 일은 아닙니다.
거짓말은 없었습니다. 데이터도 진짜고, 막대의 높이도 계산대로입니다. 다만 세로축을 1,500만 원부터 시작하도록 잘랐을 뿐이지요. 그 결과 170만 원의 차이가 화면 전체를 차지하게 되었고, 보는 사람의 눈은 축의 숫자보다 막대의 비율을 먼저 읽기 때문에 “다섯 배”라는 인상이 남습니다. 뉴스와 광고, 그리고 가끔은 논문에서도 이런 그래프를 만나게 됩니다. 이번 장은 이런 그래프를 만드는 기술과 알아보는 눈을 함께 기르는 장입니다. 만드는 기술이 늘수록 알아보는 눈도 날카로워집니다.
본격적으로 시작하기 전에, 사장님의 그래프와 우리가 다시 그린 그래프를 나란히 놓고 비교해 보겠습니다. 그 전에 준비 운동이 하나 필요합니다.
8.3 그래프 준비
이번 장은 처음부터 끝까지 그래프를 그립니다. 그래프의 제목과 축 라벨에 한글을 쓸 것이므로, 4장에서 익힌 한글 글꼴 설정 셀을 가장 먼저 실행해 둡니다. 운영체제를 스스로 판별해 맞는 글꼴을 고르는 코드입니다.
!pip install koreanize-matplotlib
import koreanize_matplotlib구글 Colab에서 실습 중이라면 서버에 한글 글꼴 자체가 없어서 글꼴 지정만으로는 부족합니다. 그 경우에만 바로 위 셀을 실행해 koreanize-matplotlib 패키지를 설치하세요. 설치와 설정이 한 번에 끝납니다. 자기 컴퓨터의 주피터 환경이라면 위 셀은 건너뛰면 됩니다.
8.4 8.1 그래프는 문장이다
이제 두 그래프를 나란히 봅니다. 왼쪽이 사장님의 그래프, 오른쪽이 같은 데이터를 다시 그린 것입니다. 두 그래프를 한 화면에 배치하는 plt.subplots 문법은 이 장 중반에 배우니, 지금은 결과만 보면 됩니다.
months = ["3월", "4월", "5월", "6월", "7월", "8월"]
sales = [1520, 1580, 1610, 1650, 1630, 1690] # 이콘 카페 월 매출(만 원)
fig, axes = plt.subplots(1, 2, figsize=(11, 4.2))
ax = axes[0] # 왼쪽: 사장님의 그래프
ax.bar(months, sales, color=["red", "orange", "green", "blue", "purple", "brown"])
ax.set_ylim(1500, 1700) # 세로축을 1,500부터 잘랐다
ax.grid(True, color="0.2")
ax.set_title("매출")
ax = axes[1] # 오른쪽: 같은 데이터, 다시 그리기
ax.bar(months, sales, color="0.8")
ax.bar(["8월"], [1690], color="tab:blue") # 말하고 싶은 막대 하나만 강조색
ax.set_ylim(0, 1800) # 세로축은 0부터
ax.grid(True, axis="y", linestyle=":", alpha=0.4)
ax.set_title("이콘 카페 월 매출: 반년 새 11% 증가 (만 원)")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.show()
이 그래프의 한 문장. 이콘 카페의 월 매출은 반년 동안 약 11% 늘었다.
방금 그 문장이 오른쪽 그래프에는 그대로 담겨 있고, 왼쪽 그래프에서는 읽어 낼 수 없습니다. 왼쪽 그래프의 문제를 하나씩 짚어 보면, 첫째로 세로축이 1,500에서 시작해 증가 폭이 실제보다 수십 배 과장되어 보입니다. 막대그래프의 높이는 “양”을 나타내므로, 특별한 사정이 없는 한 막대는 0에서 출발해야 합니다. 둘째로 여섯 막대가 여섯 가지 색인데, 색이 다르다는 것은 보는 사람에게 “이 막대들은 종류가 다르다”는 신호를 줍니다. 같은 카페의 같은 매출이니 색이 다를 이유가 없습니다. 셋째로 격자가 데이터보다 진해서 눈이 격자에 먼저 갑니다. 넷째로 제목 “매출”은 아무 정보가 없습니다. 무엇의 매출이, 언제, 어떻게 되었다는 것인지 제목이 말해 줘야 합니다.
오른쪽 그래프가 한 일은 반대입니다. 축은 0에서 출발하고, 막대는 회색으로 통일하되 이야기의 주인공인 8월 하나만 강조색을 입혔으며, 격자는 점선으로 옅게 깔고, 제목이 결론을 문장으로 말합니다. 여기서 이 장 전체를 관통하는 관점이 나옵니다. 그래프는 장식이 아니라 문장입니다. 주어와 서술어가 있는 하나의 주장이고, 좋은 그래프란 그 주장이 데이터와 일치하는 그래프입니다. 그래서 이번 장부터 규칙을 하나 세우겠습니다. 그래프를 그렸으면 그 아래에 “이 그래프의 한 문장”을 반드시 적습니다. 방금 위에서 한 것처럼 말이지요. 한 문장으로 요약되지 않는 그래프는 대개 무엇을 말할지 정하지 않고 그린 그래프입니다. 연습문제와 미니 프로젝트에서도 이 규칙을 계속 요구할 것입니다.
8.5 8.2 기본 3종을 다시, 이번에는 제대로
matplotlib의 그래프 종류는 수십 가지지만, 경제학과 학부에서 만나는 그래프의 대부분은 선, 막대, 산점도 세 가지로 그려집니다. 셋 다 이전 장들에서 스쳐 지나갔던 것들인데, 이번에는 각각이 어떤 질문에 답하는 도구인지, 그리고 절제된 스타일로 어떻게 그리는지를 기준으로 다시 배웁니다.
선 그래프: “시간에 따라 어떻게 변했는가”
선 그래프는 가로축이 시간(연도, 월, 일)이고 점들이 순서대로 이어질 때 씁니다. 이어진 선의 기울기가 곧 변화의 속도라서, 추세를 읽는 데 이만한 형식이 없습니다. 최소한의 코드로 시작하겠습니다. 다섯 해 동안의 한국 GDP를 어림한 가상 수치입니다.
years = [2020, 2021, 2022, 2023, 2024]
gdp_kr = [1.65, 1.72, 1.80, 1.85, 1.90] # 한국 GDP(조 달러), 교육용 근사치
plt.plot(years, gdp_kr)
plt.show()
한 줄씩 뜯어보기
years = [...],gdp_kr = [...]— 그래프의 재료는 언제나 데이터입니다. 가로축에 놓을 리스트와 세로축에 놓을 리스트를 같은 길이로 준비합니다. 4장의 시계열 리스트가 그대로 그래프의 재료가 됩니다.plt.plot(years, gdp_kr)— 첫 인자가 가로축, 둘째 인자가 세로축입니다.plot은 점들을 찍고 순서대로 선으로 잇습니다. 이 순서를 뒤집으면 그래프가 눕습니다. 에러는 나지 않으니 더 조심해야 합니다.plt.show()— 지금까지 쌓인 그리기 명령을 마무리하고 화면에 내보냅니다. 노트북에서는 생략해도 그림이 나올 때가 많지만, 명시하는 습관을 들이면 스크립트 파일로 옮길 때(7장에서 해 본 그 작업입니다) 코드를 고칠 필요가 없습니다.
그림은 나왔지만 아직 문장이 아닙니다. 제목도, 축의 이름도, 단위도 없어서 이 곡선이 무엇인지 그림만 봐서는 알 수 없습니다. 여기에 일본을 나란히 놓아 비교의 맥락을 만들고, 스타일 원칙 하나를 처음부터 적용해 보겠습니다. 원칙은 이렇습니다. 강조할 대상 하나에만 색을 주고, 나머지는 회색으로 물러나게 한다. 모든 선이 화려하면 아무 선도 눈에 들어오지 않기 때문입니다.
gdp_jp = [5.0, 5.1, 4.4, 4.2, 4.5] # 일본 GDP(조 달러), 교육용 근사치
plt.figure(figsize=(8, 5))
plt.plot(years, gdp_kr, color="tab:blue", linewidth=2.5)
plt.plot(years, gdp_jp, color="0.7", linewidth=2)
plt.text(2024.1, 1.90, "한국", color="tab:blue", va="center", fontweight="bold")
plt.text(2024.1, 4.5, "일본", color="0.5", va="center")
plt.title("한국 GDP는 오르막, 일본은 제자리걸음 (조 달러, 가상)")
plt.xlabel("연도")
plt.ylabel("GDP(조 달러)")
plt.xticks(years)
plt.xlim(2019.7, 2025.3)
plt.ylim(0, 6)
plt.grid(True, axis="y", linestyle=":", alpha=0.4)
plt.show()
이 그래프의 한 문장. 다섯 해 동안 한국의 GDP는 꾸준히 늘었고, 일본은 제자리를 오갔다.
코드에서 스타일과 관련된 결정을 몇 가지 했습니다. 색은 강조 대상인 한국에만 tab:blue를 주고 일본은 "0.7", 즉 밝은 회색으로 두었습니다. matplotlib에서 "0.0"은 검정, "1.0"은 흰색이고 그 사이 숫자는 회색의 밝기입니다. 범례 상자 대신 plt.text로 선이 끝나는 자리에 이름을 직접 붙였는데, 눈이 범례와 선 사이를 왕복하지 않아도 되니 읽기가 빨라집니다. 격자는 axis="y"로 가로줄만, 그것도 점선으로 옅게 깔았습니다. 세로축 값을 읽는 데는 가로 격자만 있으면 충분하고, 격자는 어디까지나 조연이기 때문입니다. 눈금은 plt.xticks(years)로 실제 연도 자리에만 찍었습니다. 그래프의 잉크는 데이터를 보여 주는 데 먼저 쓰고, 장식은 그다음이라는 것이 이 모든 결정의 공통 원리입니다.
한 가지 주의할 점이 있습니다. 이 그래프의 세로축은 0부터 시작했지만, 선 그래프에서는 축을 0부터 그리는 것이 항상 정답은 아닙니다. 막대의 높이는 양 그 자체라서 0이 기준이어야 하지만, 선 그래프의 관심사는 변화의 모양이라서, 예컨대 환율이 1,300원과 1,400원 사이를 오가는 추세를 보려고 축을 0부터 그리면 오히려 변화가 뭉개집니다. 축을 자를 때의 기준은 하나입니다. 자른 축이 만들어 내는 인상이, 내가 쓴 한 문장 캡션과 일치하는가.
실수 노트: 한글이 네모로 나오거나, 범례가 안 나올 때
그래프에 한글을 넣으면 두 가지 사고가 자주 납니다. 첫째, 제목과 라벨의 한글이 전부 네모 상자로 나오는 경우. 셀 출력에 이런 경고가 함께 떠 있을 것입니다.
findfont: Font family 'Malgun Gothic' not found.
글꼴 설정 셀을 실행하지 않았거나, 커널을 재시작해서 설정이 날아간 경우입니다. 커널을 재시작하면 변수만 사라지는 것이 아니라 글꼴 설정도 초기화된다는 점을 기억하세요. 이 장 맨 앞의 그래프 준비 셀을 다시 실행하면 해결됩니다.
둘째, label="한국"을 분명히 적었는데 범례가 안 보이는 경우입니다. label은 이름표를 준비할 뿐이고, 그것을 화면에 꺼내 놓는 것은 plt.legend() 호출입니다. 반대로 plt.legend()를 불렀는데 이런 경고가 나기도 합니다.
No artists with labels found to put in legend.
“이름표가 붙은 그림 요소가 하나도 없다”는 뜻으로, 이번에는 plot에 label= 인자를 빠뜨린 것입니다. 이름표를 붙이는 일과 범례를 꺼내는 일, 두 가지가 항상 짝이라는 것만 기억하면 됩니다.
막대 그래프: “누가 크고 누가 작은가”
선 그래프의 가로축이 시간이라면, 막대 그래프의 가로축은 범주입니다. 국가, 산업, 메뉴처럼 순서가 본질이 아닌 항목들의 크기를 비교할 때 씁니다. 주요국의 GDP를 놓고, 조금 전의 원칙을 그대로 적용해 보겠습니다. 이야기의 주인공은 한국이므로 한국 막대에만 색을 줍니다.
countries = ["미국", "중국", "일본", "독일", "한국"] # 큰 순서로 정렬해 두었다
gdp_values = [27.0, 19.0, 4.5, 4.2, 1.8] # GDP(조 달러), 교육용 근사치
bar_colors = ["0.8", "0.8", "0.8", "0.8", "tab:blue"]
plt.figure(figsize=(8, 5))
plt.bar(countries, gdp_values, color=bar_colors)
plt.title("한국 경제는 미국의 15분의 1 규모 (2024년 GDP, 조 달러)")
plt.ylabel("GDP(조 달러)")
plt.grid(True, axis="y", linestyle=":", alpha=0.4)
plt.show()
이 그래프의 한 문장. 한국의 경제 규모는 주요국 가운데 가장 작고, 미국의 15분의 1 수준이다.
막대를 큰 순서로 정렬해 둔 것도 스타일이 아니라 내용의 결정입니다. 가나다순이나 입력한 순서 그대로 두면 보는 사람이 눈으로 순위를 다시 매겨야 하지만, 크기순으로 세우면 순위가 공짜로 읽힙니다. 항목 이름이 길거나 항목이 많아지면 가로 막대 plt.barh가 낫습니다. 이름이 옆으로 누울 필요 없이 왼쪽에 가지런히 붙기 때문입니다. 어느 지역의 산업별 고용을 그려 보면 이렇습니다. barh는 아래에서 위로 쌓으므로, 가장 큰 항목이 맨 위에 오도록 오름차순으로 넣습니다.
industries = ["농업", "건설업", "IT", "제조업", "서비스업"] # 작은 것부터: barh는 아래에서 위로 쌓인다
employees = [15.0, 35.2, 45.5, 55.8, 120.5] # 고용자 수(만 명), 가상 지역
plt.figure(figsize=(8, 4.5))
plt.barh(industries, employees, color=["0.8", "0.8", "0.8", "0.8", "tab:blue"])
plt.title("이 지역 일자리 열 개 중 넷은 서비스업 (고용자 수, 만 명)")
plt.xlabel("고용자 수(만 명)")
plt.grid(True, axis="x", linestyle=":", alpha=0.4)
plt.show()
이 그래프의 한 문장. 이 지역 고용의 44%가 서비스업 한 부문에 몰려 있다.
크기 비교 이야기가 나온 김에, 많이들 쓰는 원그래프 plt.pie도 짚고 넘어가겠습니다. 원그래프는 전체에서 각 부분이 차지하는 몫을 보여 주는 형식인데, 문제는 사람의 눈이 각도를 길이만큼 정확히 비교하지 못한다는 데 있습니다. GDP의 지출 구성으로 실험해 보지요. 같은 데이터를 원과 막대로 나란히 그립니다.
파이썬 조각 지식: 리스트 뒤집기 [::-1]
아래 코드에 names[::-1]처럼 대괄호 안에 콜론이 들어간 표현이 나옵니다. 슬라이싱의 세 번째 자리는 “몇 칸씩 건너뛸지”를 뜻하는데, 여기에 -1을 주면 뒤에서부터 한 칸씩 훑어 리스트를 통째로 뒤집습니다. 가로 막대(barh)는 목록의 첫 항목을 맨 아래에 그리기 때문에, 목록을 뒤집어 넘겨야 원래 순서대로 위에서부터 읽힙니다.
gdp_components = {"민간 소비(C)": 1000.5, "정부 지출(G)": 450.0,
"투자(I)": 350.2, "순수출(NX)": 50.8} # 조 원, 가상 국가
names = list(gdp_components.keys())
values = list(gdp_components.values())
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
axes[0].pie(values, labels=names, startangle=90)
axes[0].set_title("원그래프: 몫은 보이지만 비교가 느리다")
axes[1].barh(names[::-1], values[::-1], color=["0.8", "0.8", "tab:blue", "0.8"])
axes[1].set_title("막대그래프: 정부 지출 > 투자가 즉시 보인다")
axes[1].set_xlabel("지출(조 원)")
axes[1].grid(True, axis="x", linestyle=":", alpha=0.4)
plt.tight_layout()
plt.show()
이 그래프의 한 문장. 이 나라 총지출의 절반 이상은 민간 소비가 차지하고, 정부 지출은 투자보다 크다.
원그래프에서 “정부 지출과 투자 중 어느 쪽이 큰가”를 판단하려면 두 부채꼴의 각도를 눈으로 재야 하는데, 24%와 19%의 각도 차이는 생각보다 빨리 읽히지 않습니다. 막대에서는 길이 차이가 즉시 보입니다. 그래서 이 교재의 권고는 이렇습니다. 부분의 합이 전체라는 사실 자체가 메시지일 때만 원을 쓰고, 크기 비교가 목적이면 막대를 쓴다. 딕셔너리에 담긴 데이터를 list(딕셔너리.keys())와 list(딕셔너리.values())로 풀어 그래프 재료로 쓰는 요령은 5장에서 배운 그대로입니다.
여기까지가 크기 비교의 문법입니다. 배운 것을 이콘 카페의 메뉴판에 적용해 볼 차례입니다.
# [직접 해보기]
# 이콘 카페 메뉴별 하루 평균 판매량입니다.
# 메뉴: 아메리카노 58잔, 라떼 41잔, 샌드위치 17잔, 딸기스무디 22잔, 허브티 9잔
# TODO 1) 메뉴 리스트와 판매량 리스트를 만들되, 판매량이 큰 항목이 맨 위에 오도록 barh용 순서로 정렬하세요.
# TODO 2) 가장 많이 팔리는 메뉴 하나만 강조색, 나머지는 회색인 가로 막대 그래프를 그리세요.
# TODO 3) 제목을 결론이 담긴 문장으로 짓고, 그래프 아래 마크다운 셀에 "이 그래프의 한 문장"을 적으세요.산점도: “두 변수는 함께 움직이는가”
세 번째 형식은 산점도입니다. 선 그래프가 시간과 변수의 관계라면, 산점도는 변수와 변수의 관계입니다. 관측치 하나가 점 하나가 되어, 점 구름의 모양이 두 변수가 함께 움직이는지를 보여 줍니다. 열 명의 학생에게 시험 전 공부 시간과 점수를 물었다고 합시다.
study_hours = [2, 3, 5, 1, 6, 7, 4, 8, 9, 5.5]
exam_scores = [65, 70, 75, 60, 85, 90, 72, 92, 95, 80]
plt.figure(figsize=(8, 5))
plt.scatter(study_hours, exam_scores, color="tab:blue", s=60, alpha=0.8)
plt.title("공부 시간이 길수록 점수가 높은 경향 (학생 10명, 가상)")
plt.xlabel("공부 시간(시간)")
plt.ylabel("시험 점수(점)")
plt.grid(True, linestyle=":", alpha=0.4)
plt.show()
이 그래프의 한 문장. 이 열 명 사이에서는 공부 시간이 긴 학생일수록 대체로 점수가 높았다.
캡션의 표현을 눈여겨보세요. “공부를 하면 점수가 오른다”가 아니라 “긴 학생일수록 높았다”입니다. 산점도가 보여 주는 것은 상관, 즉 두 변수가 같은 방향으로 움직였다는 사실까지이고, 무엇이 무엇의 원인인지는 그림이 말해 주지 않습니다. 원래 성실한 학생이 공부도 오래 하고 시험도 잘 보는 것일 수도 있습니다. 상관과 인과의 구분은 11장의 캡스톤에서 정면으로 다루겠지만, 캡션을 쓰는 오늘부터 표현을 조심하는 습관을 들이는 편이 좋습니다. scatter의 스타일 인자로는 점 크기 s와 투명도 alpha를 기억해 두세요. 특히 점이 수백 개로 늘어나면 alpha를 낮춰야 겹친 점들이 짙게 뭉쳐 밀도까지 보입니다.
8.6 8.3 그래프 해부학: 부품의 이름을 알아야 설명서가 읽힌다
지금까지는 필요한 명령을 그때그때 가져다 썼습니다. 여기서 잠시 멈추고, matplotlib이 그리는 그림이 어떤 부품으로 조립되어 있는지 전체 지도를 그려 두겠습니다. 부품의 이름을 알아야 공식 문서와 검색 결과가 읽히기 때문입니다. “축의 테두리를 없애고 싶다”를 검색하면 답은 spine을 다루라고 나오는데, spine이 무엇인지 모르면 답을 손에 쥐고도 쓰지 못합니다.
| 이름 | 무엇인가 | 우리가 쓴 명령 |
|---|---|---|
| Figure | 그림 전체가 놓이는 도화지 | plt.figure(figsize=...) |
| Axes | 도화지 위에서 실제 플롯이 그려지는 영역 | plt.subplots가 만들어 줌 |
| Axis | 가로축과 세로축 각각 | plt.xlabel, plt.xlim, plt.xticks |
| Title | 축 영역의 제목 | plt.title |
| Legend | 이름표가 붙은 요소들의 안내판 | plt.legend |
| Grid | 값을 읽기 쉽게 하는 격자 | plt.grid |
| Spine | 축 영역의 테두리 선 네 개 | spines["top"].set_visible(False) |
| Tick | 축 위의 눈금과 눈금 글자 | plt.xticks, plt.yticks |
이름과 실물을 연결해 봅니다. 아래 코드는 사인 곡선 하나를 그려 놓고, 조금 뒤에 정식으로 배울 annotate로 각 부품에 화살표를 달아 이름을 붙입니다. import numpy as np는 7장에서 배운 별명 import 그대로입니다.
import numpy as np
fig, ax = plt.subplots(figsize=(9, 5.5))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), color="tab:blue", label="사인 곡선")
ax.set_title("그래프 해부도: 각 부품의 이름")
ax.set_xlabel("x축 라벨 (xlabel)")
ax.set_ylabel("y축 라벨 (ylabel)")
ax.legend(loc="lower left")
ax.grid(True, linestyle=":", alpha=0.5)
ax.set_xlim(0, 10)
ax.set_ylim(-1.6, 1.6)
arrow = dict(arrowstyle="->", color="0.35")
ax.annotate("선: plot()이 그린 데이터", xy=(1.7, np.sin(1.7)), xytext=(2.8, 1.35), arrowprops=arrow)
ax.annotate("격자(grid)", xy=(8.0, -0.55), xytext=(8.35, -1.0), arrowprops=arrow)
ax.annotate("범례(legend)", xy=(1.3, -1.3), xytext=(3.1, -1.1), arrowprops=arrow)
ax.annotate("테두리 선(spine)", xy=(10.0, 0.9), xytext=(7.0, 0.72), arrowprops=arrow)
ax.annotate("눈금(tick)", xy=(6.0, -1.58), xytext=(5.3, -1.25), arrowprops=arrow)
plt.show()
이 코드에는 지금까지와 다른 점이 하나 숨어 있습니다. plt.title이 아니라 ax.set_title, plt.xlabel이 아니라 ax.set_xlabel을 썼습니다. 첫 줄의 fig, ax = plt.subplots()가 그 차이의 출발점입니다.
fig, ax = plt.subplots(figsize=(8, 4.5))
months = ["3월", "4월", "5월", "6월", "7월", "8월"]
sales = [1520, 1580, 1610, 1650, 1630, 1690]
ax.plot(months, sales, color="tab:blue", linewidth=2.5, marker="o")
ax.set_title("이콘 카페 월 매출 (만 원)")
ax.set_ylabel("매출(만 원)")
ax.set_ylim(0, 1800)
ax.grid(True, axis="y", linestyle=":", alpha=0.4)
plt.show()
한 줄씩 뜯어보기
fig, ax = plt.subplots(figsize=(8, 4.5))—plt.subplots()는 도화지(Figure)와 플롯 영역(Axes)을 한 번에 만들어 두 개를 튜플로 돌려줍니다. 5장에서 배운 튜플 언패킹이 여기서 다시 나옵니다. 왼쪽 변수fig가 도화지,ax가 플롯 영역을 받습니다.ax.plot(...)— 이제 그리기 명령을 “현재 활성화된 어딘가”가 아니라ax라는 특정 영역에 직접 보냅니다. 받는 주소가 명시된 편지인 셈입니다.ax.set_title(...),ax.set_ylabel(...)—plt.title류의 명령은ax방식에서set_이 붙은 이름으로 바뀝니다. 규칙적이라 외울 것은 많지 않습니다.plt.show()— 마무리는 같습니다.
플롯 영역이 하나뿐일 때는 두 방식의 결과가 같아서 굳이 ax를 쓸 이유가 없어 보입니다. 하지만 잠시 뒤 한 도화지에 플롯을 여러 개 놓는 순간, “어느 영역에 그릴 것인가”를 지목할 수단이 필요해지고, 그때부터는 ax 방식이 사실상 유일한 길입니다. 검색과 공식 문서의 예제도 대부분 이 방식으로 쓰여 있으니, 여기서부터는 우리도 ax 방식을 기본으로 삼겠습니다.
8.7 8.4 경제학자의 도구 상자
기본기를 갖췄으니 이제 경제학 그래프의 단골 장면들을 하나씩 완성해 봅니다. 재료는 6장에서 다뤘던 이콘 카페의 아메리카노 시장입니다. 수요와 공급을 이렇게 두겠습니다. 가격이 \(P\)원일 때 하루 수요량은 \(Q_d = 500 - 0.1P\), 공급량은 \(Q_s = -100 + 0.1P\). 그래프에 그리기 좋게 역수요·역공급 형태로 쓰면 \(P = 5000 - 10Q\), \(P = 1000 + 10Q\)이고, 연립하면 균형은 하루 200잔, 잔당 3,000원입니다.
잉여를 색으로: fill_between
6장에서 소비자 잉여와 생산자 잉여를 숫자로 계산했습니다. 3,000원에 살 수 있었는데 4,000원까지 낼 의향이 있던 학생은 1,000원을 번 셈이고, 그런 이득을 모두 합친 것이 소비자 잉여였지요. 그때 아쉬웠던 것은 그 값이 그래프 위의 어느 영역인지 눈으로 보여 줄 방법이 없었다는 점입니다. fill_between이 정확히 그 일을 합니다. 이름 그대로 “두 선 사이를 채우는” 함수입니다.
q = np.linspace(0, 300, 301) # 수량 축: 0잔부터 300잔까지
p_demand = 5000 - 10 * q # 역수요곡선
p_supply = 1000 + 10 * q # 역공급곡선
q_eq, p_eq = 200, 3000 # 균형
fig, ax = plt.subplots(figsize=(8, 5.5))
ax.plot(q, p_demand, color="tab:blue", linewidth=2, label="수요")
ax.plot(q, p_supply, color="tab:red", linewidth=2, label="공급")
mask = q <= q_eq # 균형 수량까지만 칠한다
ax.fill_between(q[mask], p_demand[mask], p_eq, color="tab:blue", alpha=0.2)
ax.fill_between(q[mask], p_eq, p_supply[mask], color="tab:red", alpha=0.2)
ax.scatter([q_eq], [p_eq], color="black", zorder=5)
ax.text(206, 3050, "균형 E (200잔, 3,000원)")
ax.text(40, 3600, "소비자 잉여", color="tab:blue", fontweight="bold")
ax.text(40, 2250, "생산자 잉여", color="tab:red", fontweight="bold")
ax.set_title("아메리카노 시장: 거래가 만들어 낸 하루 40만 원의 이득")
ax.set_xlabel("수량(잔/일)")
ax.set_ylabel("가격(원)")
ax.set_xlim(0, 310)
ax.set_ylim(0, 5300)
ax.legend(frameon=False)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.show()
cs = 0.5 * q_eq * (5000 - p_eq) # 삼각형 넓이로 검산
ps = 0.5 * q_eq * (p_eq - 1000)
print(f"소비자 잉여 {cs:,.0f}원, 생산자 잉여 {ps:,.0f}원")
소비자 잉여 200,000원, 생산자 잉여 200,000원
한 줄씩 뜯어보기
mask = q <= q_eq— numpy 배열에 비교 연산을 하면 원소별 참·거짓 배열이 나옵니다.q[mask]는 그중 참인 원소, 즉 200잔 이하의 수량만 남깁니다. 잉여는 실제로 거래된 수량까지만 발생하므로 색칠도 거기까지만 해야 합니다.ax.fill_between(q[mask], p_demand[mask], p_eq, ...)— 인자는 순서대로 가로축 값, 위(또는 아래)의 경계 하나, 또 하나의 경계입니다. 수요곡선과 균형가격 수평선 사이가 칠해집니다. 둘째·셋째 인자에는 이렇게 배열과 상수를 섞어 줄 수 있습니다.alpha=0.2— 채우기는 반드시 옅게. 진하게 칠하면 곡선이 파묻히고, 영역은 어디까지나 곡선이 만들어 낸 결과라는 논리가 뒤집혀 보입니다.zorder=5— 그림 요소가 겹칠 때 누가 위에 올라올지 정하는 우선순위입니다. 균형점이 색칠 영역에 가려지지 않게 앞으로 꺼냈습니다.
이 그래프의 한 문장. 균형 거래는 소비자와 생산자에게 각각 하루 20만 원, 합쳐 40만 원의 잉여를 만들어 낸다.
색칠된 두 삼각형의 넓이를 밑변과 높이로 검산한 것이 마지막 print입니다. 그림과 산수가 같은 답을 내는지 확인하는 습관은 모형을 다룰 때 언제나 값어치를 합니다.
이동을 화살표로: annotate
비교정학의 핵심 문장은 “곡선이 이동한다”입니다. 말로는 익숙한데, 그래프에 곡선을 두 개 그려 놓기만 하면 보는 사람은 어느 것이 원래 곡선이고 어느 쪽으로 움직였는지 알 수 없습니다. 화살표가 필요한 이유입니다. 해부도에서 잠깐 썼던 annotate를 이제 정식으로 씁니다. 개강과 함께 학생이 돌아와 아메리카노 수요가 늘어난 상황, 즉 수요곡선이 \(P = 5400 - 10Q\)로 오른쪽 이동한 경우입니다.
q = np.linspace(0, 320, 321)
d_old = 5000 - 10 * q
d_new = 5400 - 10 * q
s_curve = 1000 + 10 * q
fig, ax = plt.subplots(figsize=(8, 5.5))
ax.plot(q, d_old, color="0.65", linewidth=2)
ax.plot(q, d_new, color="tab:blue", linewidth=2)
ax.plot(q, s_curve, color="tab:red", linewidth=2)
ax.text(311, 1880, "D", color="0.5")
ax.text(311, 2280, "D'", color="tab:blue", fontweight="bold")
ax.text(311, 4080, "S", color="tab:red")
ax.annotate("", xy=(120, 4200), xytext=(120, 3800),
arrowprops=dict(arrowstyle="-|>", color="tab:blue", linewidth=2))
ax.text(127, 3980, "개강: 수요 증가", color="tab:blue")
ax.scatter([200, 220], [3000, 3200], color="black", zorder=5)
ax.annotate("", xy=(219, 3190), xytext=(201, 3010),
arrowprops=dict(arrowstyle="->", color="0.3"))
ax.text(206, 2850, "E → E'")
ax.set_title("수요가 늘면 균형은 오른쪽 위로: 220잔, 3,200원")
ax.set_xlabel("수량(잔/일)")
ax.set_ylabel("가격(원)")
ax.set_xlim(0, 335)
ax.set_ylim(1400, 5700)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.show()
이 그래프의 한 문장. 개강으로 수요가 늘자 균형은 200잔·3,000원에서 220잔·3,200원으로 옮겨 갔다.
annotate의 문법을 정리해 두겠습니다. 첫 인자는 화살표 옆에 붙일 글이고, xy=가 화살표의 머리(가리키는 지점), xytext=가 글과 화살표 꼬리의 위치입니다. 화살표의 생김새는 arrowprops=dict(...)에 담는데, arrowstyle="->"가 보통 화살표, "-|>"가 머리가 채워진 화살표입니다. 위 코드처럼 첫 인자를 빈 문자열 ""로 두면 글 없는 화살표만 그려지므로, 화살표와 글의 위치를 따로 정하고 싶을 때는 annotate로 화살표를, ax.text로 글을 각각 놓는 편이 다루기 쉽습니다. 곡선 이동의 화살표는 두 곡선을 수직으로 잇게 찍는 것이 관례이고, 옛 곡선을 회색으로 물려 두는 것 역시 “강조 1색 + 회색” 원칙의 연장입니다. 눈은 자연스럽게 새 곡선을 따라갑니다.
나란히 놓고 비교: subplots
이제 이 장 앞머리에서 예고만 했던 문법입니다. 아메리카노 가격이 3,000원에서 3,200원으로 올랐다는 사실만 보고, 우리는 원인을 알 수 있을까요? 수요가 늘어도 가격은 오르고, 원두값 급등으로 공급이 줄어도 가격은 오릅니다. 두 시나리오를 한 도화지에 나란히 놓아 보면, 가격만으로는 구분할 수 없던 두 세계가 수량에서 갈라지는 것이 보입니다. plt.subplots(1, 2)는 1행 2열, 즉 플롯 영역 두 개를 나란히 만들고 axes라는 배열로 돌려줍니다.
q = np.linspace(0, 320, 321)
d0 = 5000 - 10 * q
s0 = 1000 + 10 * q
fig, axes = plt.subplots(1, 2, figsize=(11, 4.8), sharex=True, sharey=True)
ax = axes[0] # 왼쪽 패널: 수요 증가
ax.plot(q, d0, color="0.65", linewidth=1.8)
ax.plot(q, 5400 - 10 * q, color="tab:blue", linewidth=2)
ax.plot(q, s0, color="tab:red", linewidth=2)
ax.scatter([200, 220], [3000, 3200], color="black", zorder=5, s=25)
ax.annotate("", xy=(219, 3190), xytext=(201, 3010),
arrowprops=dict(arrowstyle="->", color="0.3"))
ax.set_title("수요 증가: 가격 상승, 수량 증가")
ax.set_xlabel("수량(잔/일)")
ax.set_ylabel("가격(원)")
ax = axes[1] # 오른쪽 패널: 공급 감소
ax.plot(q, d0, color="tab:blue", linewidth=2)
ax.plot(q, s0, color="0.65", linewidth=1.8)
ax.plot(q, 1400 + 10 * q, color="tab:red", linewidth=2)
ax.scatter([200, 180], [3000, 3200], color="black", zorder=5, s=25)
ax.annotate("", xy=(181, 3190), xytext=(199, 3010),
arrowprops=dict(arrowstyle="->", color="0.3"))
ax.set_title("공급 감소: 가격 상승, 수량 감소")
ax.set_xlabel("수량(잔/일)")
axes[0].set_xlim(100, 320)
axes[0].set_ylim(2000, 4400)
plt.tight_layout()
plt.show()
이 그래프의 한 문장. 가격 상승은 두 패널이 같지만, 거래량은 수요 증가에서 늘고 공급 감소에서 줄어든다. 범인을 가리는 지문은 수량에 있다.
코드에서 챙길 부분이 셋 있습니다. 첫째, axes[0], axes[1]처럼 패널을 인덱스로 지목합니다. 4장에서 배운 리스트 인덱싱이 플롯 영역에도 그대로 적용되는 셈입니다. 둘째, sharex=True, sharey=True는 두 패널이 축 범위를 공유하게 만듭니다. 비교가 목적인 그래프에서 두 패널의 축이 다르면, 여는 질문의 잘린 축과 똑같은 왜곡이 생깁니다. 나란히 놓는 순간 축 통일은 선택이 아니라 의무입니다. 셋째, plt.tight_layout()은 패널끼리 제목과 라벨이 겹치지 않게 간격을 자동 조정합니다. 패널이 두 개 이상이면 습관처럼 넣어 두세요.
실수 노트: subplots를 쓰기 시작하면 저지르는 두 가지
첫째, 패널을 여러 개 만들어 놓고 plt.title("제목")을 부르는 실수입니다. 에러는 나지 않는데 제목이 마지막에 만진 패널에만 붙습니다. plt.title은 “지금 활성화된 축”에 작동하기 때문입니다. 패널이 여러 개인 순간부터 제목은 반드시 axes[0].set_title(...)처럼 주소를 지정해 붙이고, 도화지 전체의 제목이 필요하면 fig.suptitle(...)을 쓰세요.
둘째, fill_between의 인자 순서를 헷갈리는 실수입니다. 첫 인자는 언제나 가로축 값이고 둘째·셋째가 세로 방향의 두 경계인데, 이를 잊고 fill_between(p_demand, q, ...)처럼 축을 뒤집어 넣으면 에러 없이 나비넥타이 모양의 이상한 영역이 칠해집니다. 에러가 나는 실수보다 에러 없이 틀리는 실수가 위험하다는 것을 이 장에서 여러 번 보게 됩니다. 그래프가 이상하면 메시지가 없어도 인자 순서부터 의심하세요.
# [직접 해보기]
# 원두 값 급등으로 공급곡선이 P = 1000 + 10Q 에서 P = 1800 + 10Q 로 이동했다고 합시다.
# TODO 1) 옛 공급곡선(회색), 새 공급곡선(강조색), 수요곡선을 한 플롯에 그리세요.
# TODO 2) 두 균형점을 계산해 점으로 찍고, annotate 화살표로 균형의 이동을 표시하세요.
# (새 균형: 5000 - 10Q = 1800 + 10Q 를 손으로 풀면 나옵니다)
# TODO 3) 곡선 이동 방향을 가리키는 화살표와 원인("원두값 상승")을 적으세요.
# TODO 4) 제목을 결론 문장으로 짓고, 아래 마크다운 셀에 "이 그래프의 한 문장"을 다세요.분포를 한눈에: hist
지금까지의 그래프는 값 몇 개 또는 곡선을 다뤘습니다. 그런데 “우리나라 가구의 소득”처럼 관측치가 수백, 수천 개일 때는 개별 값이 아니라 값들이 어떻게 흩어져 있는지, 즉 분포가 관심사가 됩니다. 히스토그램은 값의 범위를 여러 구간으로 자르고 각 구간에 몇 개가 들어가는지를 막대 높이로 보여 줍니다. 가상의 소득 자료 1,000건을 만들어 그려 보지요. 난수를 쓰므로 np.random.seed로 시드를 고정합니다. 시드를 고정하면 실행할 때마다 같은 난수가 나와서, 여러분의 그래프와 교재의 그래프가 일치합니다.
np.random.seed(7) # 시드 고정: 언제 실행해도 같은 결과
incomes = np.random.lognormal(np.log(280), 0.55, 1000) # 가구 월소득(만 원), 가상 자료 1,000건
mean_inc = np.mean(incomes)
median_inc = np.median(incomes)
fig, ax = plt.subplots(figsize=(8, 5))
ax.hist(incomes, bins=40, color="0.8", edgecolor="white")
ax.axvline(median_inc, color="tab:blue", linewidth=2, label=f"중앙값 {median_inc:.0f}만 원")
ax.axvline(mean_inc, color="tab:red", linewidth=2, linestyle="--", label=f"평균 {mean_inc:.0f}만 원")
ax.set_title("소득 분포는 오른쪽 꼬리가 길다 (가상 1,000가구)")
ax.set_xlabel("월소득(만 원)")
ax.set_ylabel("가구 수")
ax.legend(frameon=False)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.show()
이 그래프의 한 문장. 가구의 절반은 월소득 273만 원에 못 미치지만, 오른쪽으로 길게 뻗은 꼬리가 평균을 317만 원까지 끌어올렸다.
bins=40은 구간의 개수입니다. 너무 적으면 분포의 모양이 뭉개지고 너무 많으면 들쭉날쭉 소음만 보이니, 몇 가지 값을 바꿔 보며 모양이 안정되는 지점을 찾는 것이 실무의 요령입니다. 세로선을 긋는 axvline은 분포 위에 기준점을 표시할 때 요긴합니다. 수평선이 필요하면 짝꿍인 axhline이 있습니다. 경제학의 눈으로 볼 대목은 평균과 중앙값의 간격입니다. 소득처럼 오른쪽 꼬리가 긴 분포에서는 고소득 소수가 평균을 끌어올려, “평균 소득”이 보통 가구의 형편을 과장하곤 합니다. 그렇다면 이 꼬리의 길이, 즉 불평등의 정도를 숫자 하나로 잴 수는 없을까요? 이 질문은 다음 절의 주인공인 로렌츠 곡선으로 이어집니다. 그 전에 조심해야 할 함정 하나만 더 보고 가겠습니다.
이중축의 유혹
서로 단위가 다른 두 변수를 한 그래프에 넣고 싶을 때, 왼쪽과 오른쪽에 세로축을 하나씩 두는 이중축 그래프를 쓰고 싶어집니다. twinx() 한 줄이면 되니 기술적으로는 쉽습니다. 문제는 이 형식이 여는 질문의 잘린 축보다 한 수 위의 함정이라는 점입니다. 최저시급과 이콘 카페 아메리카노 가격, 7년 치를 놓고 실험해 보겠습니다.
years7 = list(range(2018, 2025))
wage = [7530, 8350, 8590, 8720, 9160, 9620, 9860] # 최저시급(원)
americano = [2500, 2600, 2700, 2800, 2900, 3100, 3300] # 이콘 카페 아메리카노(원), 가상
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))
ax1.plot(years7, wage, color="tab:blue", linewidth=2) # 왼쪽 패널: 이중축
ax1.set_ylabel("최저시급(원)", color="tab:blue")
ax1.set_ylim(0, 20000) # 시급 축은 넉넉하게
twin = ax1.twinx()
twin.plot(years7, americano, color="tab:red", linewidth=2)
twin.set_ylabel("아메리카노(원)", color="tab:red")
twin.set_ylim(2400, 3400) # 커피 축은 빠듯하게
ax1.set_title("이중축: 커피값만 폭등한 것처럼 보인다")
wage_idx = [w / wage[0] * 100 for w in wage] # 오른쪽 패널: 지수화(2018 = 100)
americano_idx = [a / americano[0] * 100 for a in americano]
ax2.plot(years7, wage_idx, color="tab:blue", linewidth=2, label="최저시급")
ax2.plot(years7, americano_idx, color="tab:red", linewidth=2, label="아메리카노")
ax2.set_title("지수화(2018=100): 사실은 나란히 올랐다")
ax2.set_ylabel("2018년 = 100")
ax2.legend(frameon=False)
ax2.grid(True, axis="y", linestyle=":", alpha=0.4)
plt.tight_layout()
plt.show()
이 그래프의 한 문장. 7년 동안 최저시급은 31%, 아메리카노 가격은 32% 올라 사실상 나란히 움직였다. 왼쪽 그래프의 극적인 대비는 축 설계가 만든 착시다.
왼쪽 패널에서 커피 가격 선이 시급 선보다 훨씬 가파른 이유는 데이터가 아니라 제가 정한 축 범위 때문입니다. 시급 축은 0부터 20,000원까지 넉넉하게, 커피 축은 2,400원부터 3,400원까지 빠듯하게 잡았습니다. 축 범위를 반대로 주면 정반대의 인상을 만들 수도 있습니다. 이중축 그래프에서 두 선의 기울기 비교와 교차점 위치는 전적으로 그린 사람의 재량이라는 뜻입니다. 그래서 이 교재의 권고는 분명합니다. 단위가 다른 두 변수의 움직임을 비교하려면 이중축 대신, 기준 시점을 100으로 놓는 지수화를 하거나 패널을 나눠 그리세요. 지수화는 리스트 컴프리헨션 한 줄로 충분하다는 것을 오른쪽 패널 코드가 보여 줍니다. 이중축을 꼭 써야 한다면, 적어도 두 축의 눈금 색을 선 색과 맞추고 캡션에 축 설계를 밝히는 것이 최소한의 정직함입니다.
8.8 8.5 로렌츠 곡선과 지니계수: 불평등을 한 장에 담기
히스토그램 절에서 남겨 둔 질문으로 돌아옵니다. 한 나라의 소득 불평등을 그림 한 장, 나아가 숫자 하나로 요약할 수 있을까요? 뉴스에서 “지니계수가 0.01 올랐다”는 문장을 본 적이 있을 것입니다. 이 절이 끝나면 그 숫자를 직접 계산하게 됩니다.
재료는 소득 10분위 자료입니다. 전체 가구를 소득 순으로 줄 세워 10등분한 것이 분위이고, 1분위가 가장 가난한 10%, 10분위가 가장 부유한 10%입니다. 아래는 어느 가상 국가의 분위별 평균 월소득으로, 교재 데이터 폴더의 data/income_deciles.csv에 담긴 자료입니다. 파일을 코드로 읽는 법은 9장에서 판다스와 함께 배우고, 오늘은 열 개뿐이니 리스트로 직접 적습니다.
deciles = [95, 160, 215, 268, 322, 381, 450, 542, 685, 1120] # 1~10분위 평균 월소득(만 원)
total = sum(deciles)
cum_shares = [0.0] # 그릇 준비: 인구 0%는 소득 0%를 가진다
running = 0
for income in deciles:
running += income # 돌면서 채우기: 소득을 누적
cum_shares.append(running / total)
print(f"전체 소득 합계: {total}만 원")
for i in range(len(cum_shares)):
print(f"하위 {i * 10:3d}% 인구가 가진 소득: {cum_shares[i] * 100:5.1f}%")전체 소득 합계: 4238만 원
하위 0% 인구가 가진 소득: 0.0%
하위 10% 인구가 가진 소득: 2.2%
하위 20% 인구가 가진 소득: 6.0%
하위 30% 인구가 가진 소득: 11.1%
하위 40% 인구가 가진 소득: 17.4%
하위 50% 인구가 가진 소득: 25.0%
하위 60% 인구가 가진 소득: 34.0%
하위 70% 인구가 가진 소득: 44.6%
하위 80% 인구가 가진 소득: 57.4%
하위 90% 인구가 가진 소득: 73.6%
하위 100% 인구가 가진 소득: 100.0%
한 줄씩 뜯어보기
cum_shares = [0.0]— 4장의 누적 변수 패턴 그대로, 결과를 담을 그릇을 먼저 준비합니다. 다만 이번에는 빈 그릇이 아니라0.0하나를 미리 담아 둡니다. “인구의 하위 0%는 소득의 0%를 가진다”는 자명한 출발점인데, 잠시 뒤 곡선을 원점에서 출발시키려면 이 점이 꼭 필요합니다.running += income— 소득을 아래 분위부터 차곡차곡 쌓는 누적 합계입니다. 90일 매출을 더하던 4장의 그 동작입니다.cum_shares.append(running / total)— 누적액을 전체로 나눠 비중으로 바꿔 담습니다. 1분위까지 담고 나면 “하위 10%의 소득 비중”, 5분위까지 담고 나면 “하위 50%의 소득 비중”이 됩니다.- 마지막
for— 출력의{i * 10:3d}는 2장에서 배운 서식 지정으로, 자리수를 맞춰 표처럼 보이게 했습니다.
출력을 읽어 보세요. 하위 50% 인구가 가진 소득이 전체의 25.0%입니다. 인구의 절반이 소득의 4분의 1을 가져간다는 뜻입니다. 반대편에서는 상위 10%, 즉 10분위 혼자 26.4%를 가져갑니다. 하위 절반과 상위 한 분위의 몫이 엇비슷한 셈입니다. 이 누적 비중을 그대로 좌표평면에 옮긴 것이 로렌츠 곡선(Lorenz curve)입니다. 가로축은 인구 누적 비중, 세로축은 소득 누적 비중입니다.
pop = [i / 10 for i in range(11)] # 인구 누적 비중: 0.0, 0.1, ..., 1.0
fig, ax = plt.subplots(figsize=(6.5, 6.5))
ax.plot([0, 1], [0, 1], color="0.6", linestyle="--", label="완전평등선")
ax.plot(pop, cum_shares, color="tab:blue", linewidth=2, marker="o",
markersize=4, label="로렌츠 곡선")
ax.fill_between(pop, cum_shares, pop, color="tab:blue", alpha=0.15)
ax.text(0.55, 0.42, "면적 A", color="tab:blue")
ax.set_title("하위 50%의 손에는 소득의 25%가 있다")
ax.set_xlabel("인구 누적 비중(하위부터)")
ax.set_ylabel("소득 누적 비중")
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_aspect("equal") # 가로세로 비율 1:1 - 45도선이 진짜 45도가 되게
ax.legend(frameon=False, loc="upper left")
plt.show()
이 그래프의 한 문장. 로렌츠 곡선이 대각선에서 처져 내려간 깊이만큼, 이 나라의 소득은 불평등하다.
읽는 법을 정리합니다. 점선의 대각선은 완전평등선입니다. 모두의 소득이 똑같다면 하위 30% 인구는 정확히 소득의 30%를 갖게 되어 곡선이 대각선과 포개집니다. 현실의 곡선은 그 아래로 처지는데, 하위 분위의 몫이 인구 비중보다 작기 때문입니다. 처짐이 깊을수록 불평등이 심한 것이고, 대각선과 곡선 사이의 색칠된 영역이 그 처짐의 총량입니다. fill_between이 여기서 다시 등장한 것을 보셨을 겁니다. 잉여를 칠할 때는 두 곡선 사이였고, 이번에는 대각선과 곡선 사이입니다. 또 하나, ax.set_aspect("equal")은 가로와 세로의 축척을 맞춰 정사각형 플롯을 만듭니다. 로렌츠 곡선은 대각선과의 비교가 생명이므로, 대각선이 진짜 45도로 보이게 하는 이 설정이 사실상 필수입니다.
이제 숫자 하나로 압축할 차례입니다. 지니계수(Gini coefficient)는 방금 색칠한 면적을 \(A\), 곡선 아래 면적을 \(B\)라 할 때
\[G = \frac{A}{A + B}\]
로 정의됩니다. 완전평등이면 곡선이 대각선과 겹쳐 \(A = 0\)이므로 \(G = 0\), 한 사람이 소득을 전부 가지면 \(B\)가 0으로 줄어 \(G\)는 1에 다가갑니다. 대각선 아래 전체 면적이 \(\frac{1}{2}\)이므로 \(A + B = \frac{1}{2}\)이고, 정의를 다시 쓰면 \(G = 1 - 2B\). 결국 곡선 아래 면적 \(B\)만 구하면 됩니다.
곡선 아래 면적을 어떻게 구할까요? 우리의 로렌츠 곡선은 점 11개를 직선으로 이은 것이므로, 아래 영역은 폭이 \(\frac{1}{10}\)로 일정한 사다리꼴 10개로 정확히 쪼개집니다. 사다리꼴 하나의 넓이는 (두 평행변의 평균) × (폭), 즉
\[B \approx \sum_{i=1}^{10} \frac{y_{i-1} + y_i}{2} \times \frac{1}{10}\]
입니다. 여기서 \(y_i\)가 방금 계산한 누적 소득 비중입니다. 이 계산을 함수로 포장하겠습니다. 앞으로 여러 분포를 비교할 것이므로, 5장에서 배운 대로 “여러 번 쓸 계산은 함수로” 만드는 것입니다.
def gini(incomes):
"""분위별 소득 리스트(오름차순)를 받아 지니계수를 사다리꼴 근사로 계산한다."""
total = sum(incomes)
n = len(incomes)
cum = [0.0]
running = 0
for x in incomes:
running += x
cum.append(running / total)
area_b = 0.0
for i in range(n): # 사다리꼴 n개의 넓이를 누적
area_b += (cum[i] + cum[i + 1]) / 2 * (1 / n)
return 1 - 2 * area_b
g = gini(deciles)
print(f"이 나라의 지니계수: {g:.3f}")이 나라의 지니계수: 0.357
0.357이 나왔습니다. 참고로 통계청이 발표하는 한국의 처분가능소득 지니계수는 최근 몇 년 0.32~0.33 수준이니, 우리의 가상 국가는 한국보다 조금 더 불평등한 나라인 셈입니다. 숫자 하나만 보면 감이 잘 오지 않으니, 비교 대상을 만들어 보겠습니다. 모두가 똑같이 버는 완전평등 분포, 방금의 현실적인 분포, 그리고 상위 10%가 소득의 절반 이상을 가져가는 더 불평등한 가상 분포. 셋을 한 플롯에 겹쳐 그립니다. 세 번째 분포의 합계는 현실 분포와 같은 4,238만 원으로 맞춰 두었습니다. 파이의 크기는 같고 나누는 방식만 다른 것입니다.
아래 equal = [424] * 10은 값 424 하나를 열 번 반복해 길이 10짜리 리스트를 만드는 표현입니다. 리스트에 정수를 곱하면 그 리스트가 곱한 횟수만큼 이어 붙습니다. 여기서는 열 개 분위가 모두 똑같이 424만 원을 버는 “완전 평등” 분포를 만드는 데 씁니다.
def cum_share_curve(incomes):
"""로렌츠 곡선의 세로축 값(누적 소득 비중 리스트)을 돌려준다."""
total = sum(incomes)
cum = [0.0]
running = 0
for x in incomes:
running += x
cum.append(running / total)
return cum
equal = [424] * 10 # 완전평등: 모두 같은 소득
unequal = [20, 40, 60, 90, 130, 180, 260, 400, 800, 2258] # 더 불평등한 가상 분포
pop = [i / 10 for i in range(11)]
fig, ax = plt.subplots(figsize=(6.5, 6.5))
ax.plot(pop, cum_share_curve(equal), color="0.55", linestyle="--",
label=f"완전평등 (지니 {gini(equal):.3f})")
ax.plot(pop, cum_share_curve(deciles), color="tab:blue", linewidth=2,
label=f"현실적인 분포 (지니 {gini(deciles):.3f})")
ax.plot(pop, cum_share_curve(unequal), color="tab:red", linewidth=2,
label=f"더 불평등한 분포 (지니 {gini(unequal):.3f})")
ax.set_title("곡선이 처질수록 지니계수가 커진다")
ax.set_xlabel("인구 누적 비중(하위부터)")
ax.set_ylabel("소득 누적 비중")
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_aspect("equal")
ax.legend(frameon=False, loc="upper left")
plt.show()
이 그래프의 한 문장. 같은 크기의 파이라도 나누는 방식에 따라 지니계수는 0에서 0.654까지 벌어진다.
완전평등 분포의 곡선은 예상대로 대각선에 포개졌고 지니계수는 정확히 0입니다. 빨간 곡선의 나라에서는 하위 50%가 소득의 8%만을 갖고, 상위 10%가 53%를 가져갑니다. 지니계수 0.654는 현실에서도 보기 드문 수준의 불평등입니다. 이렇게 로렌츠 곡선은 분포 전체의 모양을, 지니계수는 그것을 요약한 숫자 하나를 줍니다. 요약에는 대가가 따른다는 점도 기억해 두세요. 서로 다른 모양의 로렌츠 곡선이 같은 지니계수를 가질 수 있습니다. 중산층이 두터운 나라와 중간이 비고 양끝이 두터운 나라가 같은 숫자를 받을 수 있다는 뜻입니다. 이 한계는 이번 장 미니 프로젝트의 글쓰기 주제이기도 합니다.
# [직접 해보기]
# TODO 1) 현실 분포 deciles에서 10분위의 소득만 1120에서 2240으로 두 배가 된 새 리스트를 만드세요.
# (나머지 아홉 분위는 그대로)
# TODO 2) gini 함수로 새 분포의 지니계수를 계산해 원래의 0.357과 비교해 출력하세요.
# TODO 3) 반대로 1분위의 소득만 95에서 190으로 두 배가 되면 지니계수가 얼마나 변하는지도 계산하세요.
# TODO 4) 두 변화 중 어느 쪽이 지니계수를 더 크게 움직였는지, 주석 한 문장으로 적으세요.8.9 8.6 두 곡선 더: 생산가능곡선과 필립스 곡선
경제학원론의 그래프 목록에서 아직 못 그린 단골 둘을 마저 그리겠습니다. 하나는 원론 1장에 나오는 생산가능곡선이고, 다른 하나는 거시경제학의 필립스 곡선입니다. 새 문법은 없습니다. 배운 도구로 어디까지 갈 수 있는지 보는 절입니다.
생산가능곡선: 기회비용은 왜 체증하는가
이콘 카페의 주방 인력이 하루에 만들 수 있는 것은 샌드위치와 커피 두 가지입니다. 모든 시간을 커피에 쓰면 200잔, 모든 시간을 샌드위치에 쓰면 40개를 만들 수 있고, 그 사이의 조합이 생산가능곡선(PPF)을 이룹니다. 곡선을 원점에 대해 오목한 사분원으로 그리는 이유가 곧 경제학입니다. 직원마다 잘하는 일이 달라서, 샌드위치 생산을 늘릴수록 커피에 더 능숙한 직원까지 샌드위치로 돌려야 하고, 그때마다 포기하는 커피의 양이 커집니다. 기회비용 체증의 법칙입니다.
x = np.linspace(0, 40, 200) # 샌드위치(개/일)
y = 200 * np.sqrt(1 - (x / 40) ** 2) # 커피(잔/일): 원점에 오목한 사분원
fig, ax = plt.subplots(figsize=(8, 5.5))
ax.plot(x, y, color="tab:blue", linewidth=2.5)
ax.scatter([20], [80], color="0.45", zorder=5)
ax.annotate("내부: 비효율(자원이 논다)", xy=(20, 80), xytext=(5, 40),
arrowprops=dict(arrowstyle="->", color="0.45"))
ax.scatter([36], [150], color="tab:red", zorder=5)
ax.annotate("외부: 지금 자원으로는 불가능", xy=(36, 150), xytext=(18, 178),
arrowprops=dict(arrowstyle="->", color="tab:red"))
ax.annotate("", xy=(10, 193.6), xytext=(0, 200),
arrowprops=dict(arrowstyle="-|>", color="0.2"))
ax.text(1.5, 210, "샌드위치 +10개: 커피 -6잔")
ax.annotate("", xy=(40, 0), xytext=(30, 132.3),
arrowprops=dict(arrowstyle="-|>", color="0.2"))
ax.text(30.5, 80, "샌드위치 +10개:\n커피 -132잔")
ax.set_title("같은 10개인데 값이 다르다: 기회비용 체증")
ax.set_xlabel("샌드위치(개/일)")
ax.set_ylabel("커피(잔/일)")
ax.set_xlim(0, 45)
ax.set_ylim(0, 235)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.show()
이 그래프의 한 문장. 처음 샌드위치 10개의 값은 커피 6잔이지만, 마지막 10개의 값은 커피 132잔이다.
곡선 위 두 화살표가 같은 “샌드위치 10개”라는 이동을 나타내는데, 왼쪽 끝에서는 커피를 6잔만 포기하면 되고 오른쪽 끝에서는 132잔을 포기해야 합니다. 곡선이 오목하다는 기하학적 사실과 기회비용이 체증한다는 경제학적 문장이 같은 말이라는 것이 이 그림의 요점입니다. 내부의 회색 점은 자원을 놀리고 있는 비효율적 생산점이고, 바깥의 빨간 점은 현재 자원과 기술로는 닿을 수 없는 조합입니다. 성장이란 이 곡선 자체가 바깥으로 이동하는 일이라는 데까지 생각이 이어지면, 원론 수업의 앞 단원이 그래프 한 장에 정리된 셈입니다.
필립스 곡선: 산점도가 이론이 되기까지
생산가능곡선이 미시의 단골이라면, 필립스 곡선은 거시의 단골입니다. 1958년 윌리엄 필립스가 영국의 약 100년 치 자료에서 발견한 것은 실업률이 낮은 해일수록 임금과 물가가 빨리 오르더라는 경험적 패턴이었습니다. 이론이 아니라 산점도에서 출발한 곡선이라는 점이 우리에게는 반갑습니다. 우리도 산점도로 재현할 수 있으니까요. 실제 자료 대신, 그 패턴을 닮게 만든 가상 자료 20개 연도분을 만들어 그립니다. 흩어진 점들 사이로 추세선을 긋는 데는 numpy의 np.polyfit을 빌립니다. 점들과 가장 가깝게 지나는 직선의 기울기와 절편을 찾아 주는 함수입니다.
np.random.seed(42) # 시드 고정
unemployment = np.linspace(3, 10, 20) # 실업률(%): 3~10%
inflation = 10 - 0.8 * unemployment + np.random.randn(20) * 0.5 # 물가상승률(%) + 잡음
slope, intercept = np.polyfit(unemployment, inflation, 1) # 1차(직선) 추세선
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(unemployment, inflation, color="tab:blue", s=45, alpha=0.85)
ax.plot(unemployment, slope * unemployment + intercept, color="0.5",
linestyle="--", label=f"추세선: 기울기 {slope:.2f}")
ax.set_title("실업률이 낮은 해는 물가가 빨리 올랐다 (가상 20개 연도)")
ax.set_xlabel("실업률(%)")
ax.set_ylabel("물가상승률(%)")
ax.legend(frameon=False)
ax.grid(True, linestyle=":", alpha=0.4)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.show()
이 그래프의 한 문장. 이 자료에서 실업률이 1%포인트 높은 해는 물가상승률이 평균 0.9%포인트가량 낮았다.
자료를 만들 때 심어 둔 관계는 -0.8이었는데, polyfit이 찾아낸 추세선의 기울기는 -0.94입니다. 틀린 것이 아닙니다. 잡음이 섞인 유한한 표본에서는 추정치가 참값 주위에서 흔들리기 마련이고, 우리는 지금 그 흔들림을 눈으로 본 것입니다. 통계학 수업에서 표본오차라는 이름으로 다시 만날 현상입니다. 캡션의 문구가 또 조심스럽다는 점도 봐 두세요. “실업률을 낮추면 물가가 오른다”는 인과의 문장이 아니라 “높은 해는 낮았다”는 관찰의 문장입니다. 실제 거시경제학에서도 이 상충관계가 안정적으로 성립하는지는 단기에 한해서라는 조건이 붙고, 1970년대의 스태그플레이션 이후 장기 필립스 곡선은 수직에 가깝다는 견해가 표준이 되었습니다. 10장에서 세 나라의 거시 지표를 진단할 때 이 논점을 다시 만나게 됩니다.
8.10 8.7 우리 교재 표준 스타일 만들기
이 장 내내 같은 스타일 코드를 반복해서 쳤습니다. 테두리 두 개를 지우고, 격자를 점선으로 옅게 깔고, 강조색과 회색을 고르고. 좋은 습관이지만 매번 대여섯 줄씩 붙이는 것은 번거롭습니다. matplotlib은 이런 설정을 한꺼번에 관리하는 길을 두 가지 열어 둡니다. 남이 만든 스타일을 통째로 입는 스타일시트와, 설정 항목을 직접 고치는 rcParams입니다.
스타일시트부터 봅니다. plt.style.use("스타일이름") 한 줄이면 그 뒤의 모든 그래프에 해당 스타일이 적용됩니다.
print(plt.style.available[:8]) # 쓸 수 있는 스타일 일부
plt.style.use("ggplot") # R의 ggplot2를 흉내 낸 스타일
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(["3월", "4월", "5월", "6월", "7월", "8월"],
[1520, 1580, 1610, 1650, 1630, 1690], linewidth=2, marker="o")
ax.set_title("ggplot 스타일을 입힌 카페 매출")
ax.set_ylabel("매출(만 원)")
plt.show()['Solarize_Light2', '_classic_test_patch', '_mpl-gallery', '_mpl-gallery-nogrid', 'bmh', 'classic', 'dark_background', 'fast']

회색 배경과 흰 격자가 ggplot 스타일의 표식입니다. 한 줄로 분위기가 통째로 바뀌는 것은 편리하지만, 남의 옷은 어딘가 안 맞기 마련입니다. 색 순환도, 격자의 농도도 우리가 정한 원칙과 다릅니다. 그래서 마지막 단계로, 이 장에서 지켜 온 원칙들을 rcParams에 직접 새겨 우리 교재의 표준 스타일을 만들겠습니다. rcParams는 matplotlib의 전역 설정 딕셔너리입니다. 글꼴 설정 셀에서 plt.rcParams["axes.unicode_minus"] = False라고 쓸 때 이미 한 번 만졌던 바로 그것입니다.
plt.style.use("default") # 우선 기본값으로 되돌린다
import platform # default가 글꼴 설정까지 지웠으므로 다시
if platform.system() == "Windows":
plt.rc("font", family="Malgun Gothic")
elif platform.system() == "Darwin":
plt.rc("font", family="AppleGothic")
else:
plt.rc("font", family="NanumBarunGothic")
plt.rcParams["axes.unicode_minus"] = False
ACCENT = "tab:blue" # 강조 1색
GRAY = "0.75" # 조연들의 회색
plt.rcParams.update({
"figure.figsize": (8, 4.5), # 기본 도화지 크기
"figure.dpi": 100, # 해상도(1인치당 점 개수)
"axes.spines.top": False, # 위·오른쪽 테두리는 항상 제거
"axes.spines.right": False,
"axes.grid": True, # 격자는 기본으로 켜되
"grid.linestyle": ":", # 점선으로
"grid.alpha": 0.4, # 옅게
"axes.titlesize": 13,
"legend.frameon": False, # 범례 테두리 상자 제거
})
fig, ax = plt.subplots() # figsize를 적지 않아도 표준 크기가 적용된다
ax.plot(["3월", "4월", "5월", "6월", "7월", "8월"],
[1520, 1580, 1610, 1650, 1630, 1690],
color=ACCENT, linewidth=2.5, marker="o")
ax.set_title("교재 표준 스타일: 설정은 한 번, 적용은 계속")
ax.set_ylabel("매출(만 원)")
ax.set_ylim(0, 1800)
plt.show()
이 그래프의 한 문장. 스타일 결정을 rcParams에 한 번 새겨 두면, 이후의 모든 그래프가 같은 원칙을 물려받는다.
이 셀을 실행한 뒤부터는 테두리 제거도, 격자 설정도, 도화지 크기도 더 쓸 필요가 없습니다. 그래프마다 남는 일은 데이터와 강조색, 그리고 제목뿐입니다. 9장부터는 이 설정 셀을 노트북 첫머리에 붙여 두고 시작할 것이고, 여러분의 과제와 미니 프로젝트에서도 그대로 가져다 쓰면 됩니다. 한 걸음 더 가고 싶다면 7장에서 배운 모듈이 정확히 이럴 때를 위한 도구입니다. 이 설정 코드를 econ_style.py로 저장해 두면 어느 노트북에서든 import econ_style 한 줄로 표준 스타일이 입혀집니다. 7장에서 만든 econo_analyzer 패키지의 utils/plotting.py에 넣는 것도 좋은 자리입니다.
실수 노트: 스타일을 바꿨더니 한글이 다시 깨진다
plt.style.use("default")는 이름과 달리 얌전한 명령이 아닙니다. 모든 설정을 공장 초기 상태로 되돌리므로, 우리가 애써 잡은 한글 글꼴과 마이너스 기호 설정까지 함께 지워 버립니다. 스타일을 바꾼 직후의 그래프에서 한글이 네모로 나오거나 y축의 음수가 이상한 기호로 표시된다면 십중팔구 이 경우입니다. 그래서 위 셀도 style.use("default") 바로 다음에 글꼴 설정을 다시 실행했습니다. 규칙으로 기억해 두세요. 스타일시트를 적용하면 글꼴 설정을 다시 한다. 또 스타일시트를 이것저것 실험하다 보면 설정이 겹겹이 쌓여 원인 모를 모양이 되기도 하는데, 그때는 당황하지 말고 plt.style.use("default")로 전부 초기화한 뒤 글꼴부터 다시 잡으면 됩니다.
8.11 연습문제
모든 문제에 공통 요구가 둘 있습니다. 그래프에는 결론이 담긴 제목과 축 라벨(단위 포함)을 달고, 그래프 아래에 “이 그래프의 한 문장”을 적으세요.
손풀기
1. 수출입 추이 — 어느 나라의 5년 치 수출액과 수입액입니다(억 달러). years = [2020, 2021, 2022, 2023, 2024], exports = [510, 620, 680, 650, 700], imports = [480, 580, 650, 630, 680]. 두 계열을 한 플롯의 선 그래프로 그리되, 수출은 강조색, 수입은 회색으로 구분하고 범례 대신 선 끝에 text로 이름을 붙이세요.
2. 분기별 영업이익 — 어느 기업의 분기 실적입니다. quarterly_profit = {"1분기": 35.2, "2분기": 41.5, "3분기": 28.8, "4분기": 45.0}(억 원). 딕셔너리에서 키와 값을 꺼내 막대 그래프를 그리고, axhline으로 연평균 수준의 수평 기준선을 점선으로 그은 뒤, 연평균을 넘긴 분기만 강조색을 주세요.
3. 광고비와 매출 — 어느 기업의 월별 광고비와 매출입니다(백만 원). ad_cost = [10, 15, 12, 20, 18, 25, 22], revenue = [100, 140, 110, 180, 160, 210, 190]. 산점도를 그리고, 캡션은 상관과 인과를 구분한 문장으로 쓰세요.
응용
4. 가격상한제의 풍경 — 본문의 아메리카노 시장(\(P = 5000 - 10Q\), \(P = 1000 + 10Q\))에 학생회가 가격상한 2,600원을 관철시켰다고 합시다. 수요·공급 곡선을 그리고, axhline으로 상한선을 그은 뒤, 상한 가격에서의 수요량과 공급량을 각각 계산해 점으로 찍고, 두 점 사이의 초과수요(부족분)를 annotate 화살표와 수치로 표시하세요.
5. 두 분포 나란히 — np.random.seed(0)으로 시드를 고정하고, a = np.random.normal(300, 40, 500)과 b = np.random.lognormal(np.log(260), 0.5, 500)이라는 가상 소득 자료 두 벌을 만드세요. subplots(1, 2)에 두 히스토그램을 나란히 그리되 x축 범위를 통일하고, 각 패널에 평균(빨강 파선)과 중앙값(파랑 실선)을 axvline으로 표시하세요. 캡션에는 어느 분포에서 평균과 중앙값의 간격이 넓은지, 그것이 분포의 어떤 특징 때문인지 쓰세요.
6. 나쁜 그래프 비평 (서술형) — 아래 코드를 실행하면 어느 해 언론 기사풍의 그래프가 나옵니다(수치는 대략적인 값입니다).
years = [2019, 2020, 2021, 2022, 2023]
debt = [723, 847, 971, 1067, 1127] # 국가채무(조 원)
plt.figure(figsize=(3.5, 6))
plt.bar(years, debt, color=["red", "orange", "green", "blue", "purple"])
plt.ylim(700, 1150)
plt.title("국가채무")
plt.show()이 그래프의 문제점을 세 가지 이상 찾아 마크다운 셀에 서술하고(무엇이 왜 문제인지, 보는 사람이 어떤 잘못된 인상을 받는지), 이 장의 원칙대로 고쳐 그린 개선판을 제시하세요.
도전
7. 로렌츠 보고서 함수 — 분위별 소득 리스트와 라벨 문자열을 받아 (1) 로렌츠 곡선을 완전평등선과 함께 그리고 (2) 지니계수를 계산해 범례에 포함시키고 (3) 지니계수를 반환하는 함수 lorenz_report(incomes, label)를 작성하세요. 본문의 deciles, 그리고 여러분이 직접 설계한 5분위 데이터(원소 5개 리스트)에 적용해, 분위 수가 달라도 작동함을 보이세요.
8. 비교정학 대시보드 — subplots(2, 2, figsize=(11, 9))로 수요 증가, 수요 감소, 공급 증가, 공급 감소 네 시나리오를 2행 2열에 그리세요. 네 패널 모두 본문의 아메리카노 시장에서 출발하고, 곡선 이동 폭은 400원으로 통일합니다. 요구사항: 네 패널의 축 범위 통일(sharex, sharey), 각 패널에 옛 곡선은 회색·새 곡선은 강조색, 균형 이동 화살표, 패널 제목에 가격·수량의 변화 방향 명시, fig.suptitle로 전체 제목. 완성하면 원론 교과서 비교정학 단원의 네 그림을 한 장에 담은 셈입니다.
풀이는 부록 C에 있습니다.
8.12 미니 프로젝트: 생각을 코드로, 코드를 글로
과제: 불평등 리포트 — 두 나라 이야기 (예상 소요 30~60분)
가상의 두 나라, 알파국과 베타국의 소득 10분위 자료입니다(분위별 평균 월소득, 만 원).
alpha = [150, 200, 240, 275, 310, 350, 400, 460, 560, 800]
beta = [60, 90, 120, 160, 210, 270, 350, 480, 750, 1750]
두 나라 중 어느 쪽이 더 불평등한지, 이 장에서 만든 도구로 진단하고 세 문단의 짧은 보고서로 정리하는 것이 과제입니다.
요구사항
- 본문의
gini함수와cum_share_curve함수를 가져와(직접 다시 타이핑해도, 복사해도 좋습니다) 두 나라의 지니계수를 계산해 출력하세요. - 한 플롯에 완전평등선과 두 나라의 로렌츠 곡선을 함께 그리세요. 더 불평등한 나라에 강조색을 주고, 범례에 나라 이름과 지니계수를 담으세요.
set_aspect("equal")을 잊지 마세요. - 각 나라에서 하위 50%의 소득 비중과 상위 10%의 소득 비중을 계산해 출력하세요. 2문단의 근거 재료입니다.
- (선택)
subplots(1, 2)로 두 나라의 분위별 소득 막대 그래프를 나란히 그려 보세요. 축 범위를 통일해야 정직한 비교가 됩니다.
3문단 글쓰기 틀 — 그래프 아래 마크다운 셀에 작성합니다.
- 1문단(주장): 어느 나라가 더 불평등한지, 그 차이가 큰지 작은지를 명확한 문장으로 요약합니다. “베타국의 소득 분배는 알파국보다 뚜렷이 불평등하며, 그 격차는 지니계수로 0.2 이상이다” 같은 문장이 출발점입니다.
- 2문단(근거): 두 나라의 지니계수, 하위 50%와 상위 10%의 소득 비중 등 계산한 수치를 두 개 이상 직접 인용하고, 로렌츠 곡선 그래프의 어느 부분에서 그 차이가 보이는지 한 문장으로 안내합니다.
- 3문단(한계와 확장): 지니계수라는 요약 지표의 한계를 최소 한 가지 명시합니다. 모양이 다른 분포가 같은 지니계수를 가질 수 있다는 점, 이 자료가 세전 소득인지 세후 소득인지에 따라 해석이 달라진다는 점, 소득 불평등과 자산 불평등은 다르다는 점이 모두 좋은 후보입니다. 마지막으로 이 진단을 보강할 다음 질문을 한 문장으로 적습니다.
자기 점검 체크리스트
# 미니 프로젝트 작업 공간
alpha = [150, 200, 240, 275, 310, 350, 400, 460, 560, 800] # 알파국 10분위 월소득(만 원)
beta = [60, 90, 120, 160, 210, 270, 350, 480, 750, 1750] # 베타국 10분위 월소득(만 원)
# TODO 1) gini 함수와 cum_share_curve 함수로 두 나라의 지니계수 계산·출력
# TODO 2) 완전평등선 + 두 나라 로렌츠 곡선을 한 플롯에 (강조색은 더 불평등한 나라에)
# TODO 3) 각 나라의 하위 50% 소득 비중과 상위 10% 소득 비중 계산·출력
# TODO 4) (선택) 분위별 소득 막대 그래프를 subplots로 나란히8.13 요약과 다음 장 예고
이번 장의 출발점은 기술이 아니라 관점이었습니다. 그래프는 장식이 아니라 주어와 서술어를 갖춘 문장이며, 잘린 축과 이중축처럼 데이터를 건드리지 않고도 문장을 왜곡하는 길이 얼마든지 있다는 것을 사장님의 대출용 그래프에서 확인했습니다. 그 관점 위에서 선·막대·산점도 기본 3종을 강조 1색과 회색의 절제된 스타일로 다시 그렸고, Figure와 Axes, spine과 tick이라는 부품의 이름을 익혀 fig, ax 방식으로 갈아탔습니다. 경제학 쪽의 수확은 묵직합니다. fill_between으로 소비자·생산자 잉여를 색칠해 하루 40만 원의 거래 이득을 눈에 보이게 만들었고, annotate의 화살표로 곡선의 이동을, subplots로 수요 충격과 공급 충격이 수량에서 갈라지는 것을 보였습니다. 히스토그램의 긴 오른쪽 꼬리에서 출발한 질문은 로렌츠 곡선과 지니계수로 이어져, 소득 10분위 리스트와 4장의 누적 패턴만으로 불평등을 0.357이라는 숫자 하나에 담아냈습니다. 생산가능곡선의 오목함이 기회비용 체증과 같은 말이라는 것, 필립스 곡선이 이론 이전에 산점도였다는 것도 확인했습니다. 마지막으로 이 모든 스타일 결정을 rcParams에 새겨 교재 표준 스타일로 만들었으니, 다음 장부터는 설정이 아니라 데이터에 집중하면 됩니다.
다음 장의 주인공은 데이터 그 자체입니다. 지금까지 우리는 그래프의 재료를 리스트에 손으로 적어 왔지만, 현실의 데이터는 수백 줄짜리 파일로 옵니다. 9장에서는 판다스라는 라이브러리로 이콘 카페의 판매 기록 450줄과 한국 거시 지표 25년 치를 표 하나로 읽어 들이고, 자르고, 묶고, 오늘 만든 표준 스타일의 그래프로 옮기는 법을 배웁니다. 이 장에서 리스트로 그린 모든 그래프가, 다음 장에서는 파일에서 곧바로 태어납니다.