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"] = False6 수요와 공급: 시장 균형을 코드로 찾기 (6주차)
6.1 이번 장에서 배우는 것
이번 장은 조립의 장입니다. 1장의 산술, 3장의 조건문, 4장의 반복문, 5장의 함수와 튜플이 전부 한 번씩 다시 등장해, 경제학원론의 한복판에 있는 수요·공급 모형 하나를 완성합니다. 새로 배우는 문법은 두 가지뿐이지만, 배운 것을 엮는 밀도는 지금까지 중 가장 높습니다.
- 5장에서 만든
quantity_demanded와quantity_supplied를 한 시장에 조립하고, 균형 가격 공식 \(P^* = \frac{a-c}{b+d}\)를 유도해find_equilibrium함수로 옮깁니다. - 공식이 통하지 않는 모형에 대비해, 가격 격자를 훑으며 수요와 공급의 격차가 가장 작은 지점을 찾는 수치 탐색을 익힙니다.
- 실수 격자를 만들어 주는
np.linspace를 소개받고, 리스트를 한 줄로 만드는 리스트 컴프리헨션(list comprehension)을 정식으로 배웁니다. - 가격을 세로축에 두는 경제학 그래프의 관례대로 수요·공급 곡선과 균형점을 그리고,
fill_between으로 소비자·생산자 잉여를 색칠합니다. - 초과수요가 가격을 밀어 올리는 조정 과정을
while로, 공급이 한 박자 늦는 농산물 시장의 가격 파동을 거미집 모형으로 시뮬레이션합니다. - 3장에서 구속력 판정만 하고 넘어갔던 가격상한제를 다시 불러와, 상한이 만드는 부족분을 계산하고 그림에 표시합니다.
6.2 여는 질문: 그 많은 카페는 왜 다 비슷한 값을 받을까
캠퍼스 정문에서 후문까지 걸으면 카페를 여남은 개는 지나칩니다. 이콘 카페도 그중 하나입니다. 그런데 메뉴판들을 유심히 보면 이상한 점이 있습니다. 아메리카노 가격이 죄다 1,800원 언저리라는 것입니다. 사장님들이 모여서 담합한 것도 아니고, 누가 고시한 것도 아닌데, 제각기 정한 가격이 좁은 범위에 모여 있습니다. 2,500원을 받던 가게는 손님이 빠져나가자 슬그머니 값을 내렸고, 1,200원 개업 이벤트를 하던 가게는 재료비를 감당하지 못해 도로 올렸습니다. 아무도 지휘하지 않았는데 가격이 한곳으로 끌려갑니다.
경제학원론은 이 현상에 시장 균형(market equilibrium)이라는 이름을 붙입니다. 사려는 양과 팔려는 양이 맞아떨어지는 가격이 존재하고, 가격이 거기서 벗어나면 시장 스스로 되돌리는 힘이 작동한다는 설명입니다. 지금까지 이 이야기를 칠판의 X자 그림으로만 봤다면, 이번 장에서는 코드로 다시 봅니다. 5장에서 만들어 둔 수요 함수와 공급 함수를 조립해 균형을 계산하고, 그림으로 그리고, 충격을 줘서 움직여 보고, 가격이 균형을 향해 한 걸음씩 기어가는 과정까지 재생해 볼 것입니다.
6.3 그래프 준비
이번 장은 그래프가 많이 나옵니다. 한글 라벨이 깨지지 않도록 폰트 설정 셀부터 실행해 둡니다.
!pip install koreanize-matplotlib
import koreanize_matplotlib구글 Colab에서 실습 중이라면 위의 폰트들이 설치되어 있지 않아 한글이 네모 상자로 나올 수 있습니다. 그럴 때만 바로 위의 koreanize-matplotlib 설치 셀을 실행해 주세요. 내 컴퓨터(Windows, macOS)에서 실습 중이라면 건너뜁니다.
6.4 시장을 수식 두 줄로 줄이기
먼저 무대를 정리합시다. 이번 장의 무대는 이콘 카페 한 곳이 아니라, 학교 앞 카페 전부와 커피를 마시는 학생 전부가 참여하는 대학가 아메리카노 시장입니다. 가격 \(P\)는 100원 단위로 재고(\(P = 18\)이면 1,800원), 수량 \(Q\)는 시장 전체에서 하루에 거래되는 잔 수로 잽니다.
수요 쪽부터 봅니다. 가격이 오르면 사려는 양이 줄어듭니다. 1,500원이면 매일 마시던 학생도 2,500원이 되면 학교 정수기 옆 믹스커피로 갈아탑니다. 이것이 수요의 법칙이고, 우리는 이 관계를 가장 단순한 직선으로 근사합니다. 공급 쪽은 반대입니다. 가격이 오르면 팔려는 양이 늘어납니다. 마진이 좋아지면 카페들은 영업시간을 늘리고, 구석 자리에 테이블을 더 놓고, 새 가게가 들어옵니다. 공급의 법칙입니다.
\[Q_d = a - bP, \qquad Q_s = c + dP\]
파라미터 네 개가 이 시장의 성격을 전부 결정합니다. \(a\)는 공짜일 때 소화될 총량, 즉 수요의 규모입니다. \(b\)는 가격이 1단위(100원) 오를 때 줄어드는 잔 수, 수요의 가격 민감도입니다. \(c\)는 가격과 거의 무관하게 나오는 기본 물량이고(이미 차려 놓은 가게는 어떻게든 영업합니다), \(d\)는 가격이 1단위 오를 때 늘어나는 잔 수, 공급의 가격 민감도입니다. 기본 시장의 값은 5장에서 함수의 기본값으로 심어 둔 그대로 \(a=100\), \(b=2\), \(c=10\), \(d=3\)을 씁니다. 부품도 5장의 것을 다시 조립합니다.
def quantity_demanded(price, a=100, b=2):
"""선형 수요 함수 Qd = a - bP 로 주어진 가격에서의 수요량을 계산합니다."""
return max(0, a - b * price)
def quantity_supplied(price, c=10, d=3):
"""선형 공급 함수 Qs = c + dP 로 주어진 가격에서의 공급량을 계산합니다."""
return max(0, c + d * price)
price = 15
print(f"가격 {price} (1잔 {price * 100:,}원)일 때")
print(f" 사려는 양 Qd = {quantity_demanded(price)}잔")
print(f" 팔려는 양 Qs = {quantity_supplied(price)}잔")가격 15 (1잔 1,500원)일 때
사려는 양 Qd = 70잔
팔려는 양 Qs = 55잔
5장과 달라진 곳이 하나 있습니다. 공급 함수에도 max(0, ...)을 둘렀습니다. 이번 장에서는 파라미터를 이리저리 흔들며 실험할 텐데, 어떤 조합에서는 공급량이 음수로 계산될 수 있기 때문입니다. 팔려는 양이 음수라는 것은 경제학적으로 뜻이 없으니 0에서 잘라 둡니다. 공식을 코드로 옮길 때는 공식이 유효한 범위까지 함께 옮긴다는 5장의 원칙 그대로입니다.
출력을 읽어 봅시다. 1,500원에서는 사려는 양이 70잔인데 팔려는 양은 55잔뿐입니다. 15잔어치의 손님은 살 수 없습니다. 3장에서 배운 말로 초과수요입니다. 그렇다면 다른 가격에서는 어떨까요. 가격을 하나씩 바꿔 가며 손으로 확인하는 대신, 4장의 for에게 시키겠습니다.
print("가격 | 수요량 | 공급량 | 시장 상태")
for price in range(10, 27, 2):
qd = quantity_demanded(price)
qs = quantity_supplied(price)
if qd > qs:
status = f"초과수요 {qd - qs}잔 (모자람)"
elif qd < qs:
status = f"초과공급 {qs - qd}잔 (남아돎)"
else:
status = "수요와 공급이 일치"
print(f"{price:4d} | {qd:5d}잔 | {qs:5d}잔 | {status}")가격 | 수요량 | 공급량 | 시장 상태
10 | 80잔 | 40잔 | 초과수요 40잔 (모자람)
12 | 76잔 | 46잔 | 초과수요 30잔 (모자람)
14 | 72잔 | 52잔 | 초과수요 20잔 (모자람)
16 | 68잔 | 58잔 | 초과수요 10잔 (모자람)
18 | 64잔 | 64잔 | 수요와 공급이 일치
20 | 60잔 | 70잔 | 초과공급 10잔 (남아돎)
22 | 56잔 | 76잔 | 초과공급 20잔 (남아돎)
24 | 52잔 | 82잔 | 초과공급 30잔 (남아돎)
26 | 48잔 | 88잔 | 초과공급 40잔 (남아돎)
표의 흐름이 보이십니까. 가격이 낮을 때는 모자라고, 높을 때는 남아돌고, 정확히 18(1,800원)에서 사려는 양과 팔려는 양이 64잔으로 일치합니다. 여는 질문의 답이 여기 있습니다. 학교 앞 카페들이 1,800원 언저리로 모여드는 것은 그 가격에서만 모자라지도 남지도 않기 때문입니다.
그런데 18이라는 숫자는 우연이 아닙니다. 파라미터 네 개가 정해지는 순간 균형은 하나로 결정됩니다. for로 훑어서 찾는 것도 방법이지만(뒤에서 이 방법을 진지하게 다시 씁니다), 선형 모형이라면 종이와 연필로 정확한 답을 꺼낼 수 있습니다.
6.5 종이와 연필: 균형 가격의 공식
균형은 수요량과 공급량이 같아지는 가격입니다. 조건을 그대로 쓰면 \(Q_d = Q_s\), 즉
\[a - bP^* = c + dP^*\]
입니다. \(P^*\)가 붙은 항을 오른쪽으로, 나머지를 왼쪽으로 모으면 \(a - c = (b + d)P^*\)가 되고, 양변을 \((b+d)\)로 나누면 균형 가격이 나옵니다.
\[P^* = \frac{a - c}{b + d}, \qquad Q^* = a - bP^*\]
균형 거래량 \(Q^*\)는 \(P^*\)를 수요식에 되넣은 것입니다(공급식에 넣어도 같은 값이 나옵니다. 같아지는 가격을 찾은 것이니까요). 기본 시장의 값을 대입하면 \(P^* = \frac{100-10}{2+3} = 18\), \(Q^* = 100 - 2 \times 18 = 64\). 방금 표에서 눈으로 찾은 그 지점입니다.
이 공식은 생김새 자체가 직관을 담고 있습니다. 분자 \(a - c\)는 가격이 0일 때의 초과수요, 말하자면 이 시장이 안고 있는 불일치의 규모입니다. 분모 \(b + d\)는 가격이 1단위 오를 때 그 불일치가 줄어드는 속도입니다(수요가 \(b\)만큼 줄고 공급이 \(d\)만큼 늘어서 격차는 \(b+d\)씩 좁혀집니다). 불일치의 크기를 조정 속도로 나눈 것이 균형 가격인 셈입니다. 다만 공식에는 함정이 둘 있습니다. 분모가 0이면 나눗셈이 불가능하고, \(a < c\)이면 균형 가격이 음수로 나옵니다. 두 경우를 정중히 거절하는 것까지 포함해서 함수로 만들어 둡시다.
def find_equilibrium(a=100, b=2, c=10, d=3):
"""선형 수요 Qd = a - bP, 공급 Qs = c + dP 의 균형 가격과 거래량을
튜플 (P*, Q*)로 반환합니다. 의미 있는 균형이 없으면 None을 반환합니다."""
if b + d == 0:
return None # 분모 0: 두 직선이 만나지 않습니다
p_star = (a - c) / (b + d)
if p_star < 0:
return None # 음수 가격: 이 모형에는 의미 있는 균형이 없습니다
q_star = a - b * p_star
return p_star, q_star
result = find_equilibrium() # 파라미터를 안 주면 기본 시장
print(result)
p_star, q_star = result # 5장에서 배운 튜플 언패킹
print(f"균형 가격 P* = {p_star:.1f} (1잔 {p_star * 100:,.0f}원)")
print(f"균형 거래량 Q* = {q_star:.0f}잔")(18.0, 64.0)
균형 가격 P* = 18.0 (1잔 1,800원)
균형 거래량 Q* = 64잔
함수의 구조를 짚어 두겠습니다. 매개변수 네 개에 기본값을 준 것은 5장의 규칙 그대로여서, find_equilibrium()처럼 빈 괄호로 부르면 기본 아메리카노 시장을 푼다는 뜻이 됩니다. 계산이 불가능하거나 무의미한 경우에는 None을 돌려줍니다. 5장의 실수 노트에서 None은 “돌려줄 것이 없다”는 뜻의 값이라고 했는데, 이번에는 그 값을 사고가 아니라 의도로 씁니다. “이 시장에는 답이 없다”는 정보 자체를 반환값으로 전달하는 것입니다. 답이 있을 때는 값이 두 개(가격, 거래량)라서 튜플로 묶여 나오고, 받는 쪽에서 p_star, q_star = result로 풀어 씁니다.
그런데 함수를 거치지 않고 공식을 직접 두드리다 보면 잘 나는 사고가 두 가지 있습니다. 첫 번째는 부호 사고입니다. 아래 셀은 일부러 사고를 내는 셀이니 실행하지 않고 눈으로 따라가도 됩니다.
a, b, c, d = 100, -2, 10, 3 # 실수: "기울기가 -2니까"라며 b에 음수를 넣음
p_star = (a - c) / (b + d)
print(p_star) # 90.0 -- 에러 없이 그럴듯한 오답이 나옵니다
a, b, c, d = 100, -3, 10, 3 # 수요 기울기가 공급 기울기와 정확히 상쇄되면
p_star = (a - c) / (b + d) # 여기서 프로그램이 멈춥니다첫 번째 계산은 90.0을 출력합니다. 에러가 없다는 점이 오히려 함정입니다. 균형 가격 9,000원이라는 터무니없는 값이 조용히 다음 계산으로 흘러 들어갑니다. 두 번째 계산은 이런 메시지와 함께 멈춥니다.
ZeroDivisionError Traceback (most recent call last)
Cell In[9], line 6
5 a, b, c, d = 100, -3, 10, 3
----> 6 p_star = (a - c) / (b + d)
ZeroDivisionError: division by zero
실수 노트: 균형 공식의 분모가 0이 될 때
트레이스백은 아래에서 위로 읽습니다. 마지막 줄
ZeroDivisionError: division by zero가 병명이고(0으로 나눴다), 화살표---->가 붙은 줄이 사고 지점입니다. 사고 지점의 분모는b + d뿐이니, 범인은b + d가 0이 된 것입니다. 이 사고의 원인은 십중팔구 부호입니다. 수요식 \(Q_d = a - bP\)에는 빼기 부호가 이미 들어 있으므로, \(Q_d = 100 - 2P\)라면b자리에는2를 넣어야지-2를 넣으면 안 됩니다. 이 책의 함수들은 전부 이 약속을 따릅니다. 그리고 방금 보았듯 진짜 무서운 쪽은 에러가 나는 두 번째가 아니라 조용히 오답을 내는 첫 번째입니다. 공식에 값을 넣기 전에print(b + d)한 줄로 분모부터 확인하는 습관이 두 사고를 모두 막아 줍니다.
실수 노트: 균형 가격이 음수로 나올 때
\(a=30\), \(c=60\)인 시장을 상상해 봅시다. 공짜로 줘도 하루 30잔밖에 안 나가는데, 카페들이 가격과 무관하게 기본 60잔을 쏟아내는 시장입니다. 공식은 군말 없이 \(P^* = \frac{30-60}{5} = -6\)을 내놓습니다. 파이썬은 음수 나눗셈에 아무 불만이 없기 때문에 에러도 나지 않습니다. 이때 “코드가 틀렸나”를 의심하기 전에 모형을 의심해야 합니다. 음수 균형 가격은 계산 실수가 아니라, 그 파라미터 조합에서는 돈을 받고 이루어지는 거래가 성립하지 않는다는 모형의 대답입니다.
find_equilibrium이 이 경우None을 돌려주도록 만든 이유입니다. 다만None을 받았는데 그대로 언패킹하면 이런 에러가 납니다.TypeError: cannot unpack non-iterable NoneType object“풀어 헤칠 수 없는
None을 풀려고 했다”는 뜻입니다. 균형을 계산하는 함수의 반환값은 언패킹하기 전에None인지부터 확인하세요.
result = find_equilibrium(a=30, c=60) # 공짜여도 수요가 기본 공급에 못 미치는 시장
print(result)
if result is None: # None인지 물을 때는 == 대신 is를 씁니다
print("이 시장에는 의미 있는 균형이 없습니다. 파라미터를 다시 살펴보세요.")
else:
p, q = result
print(f"균형: P* = {p:.1f}, Q* = {q:.1f}")None
이 시장에는 의미 있는 균형이 없습니다. 파라미터를 다시 살펴보세요.
6.6 컴퓨터답게 찾기: 가격을 하나씩 넣어 보는 방법
방금 균형 가격을 종이와 연필로 깔끔하게 풀었습니다. 그런데 이 방법에는 조건이 하나 붙어 있습니다. 수요와 공급이 둘 다 직선이어야 한다는 것입니다. 직선이니까 \(a - bP = c + dP\)를 한 줄로 정리해 \(P\)를 뽑아낼 수 있었습니다. 현실의 곡선은 그렇게 순순하지 않습니다. 수요가 곡선이거나, 공급에 계단이 있거나, 세금이 구간마다 다르게 붙으면, 손으로 푸는 공식이 나오지 않는 경우가 대부분입니다.
이럴 때 컴퓨터는 사람과 다른 방식으로 접근합니다. 식을 정리해서 답을 꺼내는 대신, 가능한 가격을 촘촘하게 늘어놓고 하나씩 대입해 보며 수요와 공급이 가장 가까워지는 지점을 찾습니다. 무식해 보이지만, 초당 수백만 번을 계산하는 기계에게는 이쪽이 오히려 자연스럽습니다. 4장에서 반복문을 배울 때 “언젠가 이 지루한 반복이 사람이 못 푸는 문제를 푼다”고 했던 그 약속을 지금 회수합니다.
먼저 대입해 볼 가격 후보들을 만들어야 합니다. 0원부터 5,000원까지 촘촘한 간격으로 가격을 늘어놓는 일인데, 이런 균등 격자를 만드는 데는 넘파이(numpy)의 linspace가 편합니다. 넘파이는 수치 계산에 특화된 라이브러리로, 앞으로 그래프를 그릴 때마다 함께 쓰게 됩니다.
import numpy as np # 수치 계산 라이브러리. 관례적으로 np라는 짧은 이름으로 부릅니다.
prices = np.linspace(0, 50, 501) # 0부터 50까지 501개 가격 -> 0.1 간격
print("만들어진 가격 개수:", len(prices))
print("앞의 다섯 개:", prices[:5])
print("뒤의 세 개:", prices[-3:])만들어진 가격 개수: 501
앞의 다섯 개: [0. 0.1 0.2 0.3 0.4]
뒤의 세 개: [49.8 49.9 50. ]
한 줄씩 뜯어보기
import numpy as np— 넘파이를 불러오되np라는 별명으로 씁니다. 앞으로 넘파이 기능은 전부np.을 앞에 붙여 부릅니다.np.linspace(0, 50, 501)— 0에서 50까지를 정확히 501개의 점으로 균등하게 쪼갭니다. 시작과 끝을 모두 포함하므로 간격은 \(50 \div 500 = 0.1\)이 됩니다. 4장의range가 정수만 셀 수 있었던 것과 달리,linspace는 0.1, 0.2 같은 실수 격자를 만들어 줍니다. 가격을 100원(\(P\) 기준 1) 단위보다 잘게 나눠 보려면 실수 격자가 필요합니다.prices[:5],prices[-3:]— 4장에서 배운 슬라이싱입니다. 만들어진 격자의 앞뒤를 확인해 봅니다.
이제 이 501개 가격을 하나씩 대입하며, 수요량과 공급량의 차이가 가장 작아지는 가격을 찾습니다.
best_price = None
best_gap = None # 아직 아무것도 못 찾았다는 뜻으로 None에서 출발
for p in prices:
qd = quantity_demanded(p)
qs = quantity_supplied(p)
gap = abs(qd - qs) # 수요와 공급의 벌어진 정도 (부호는 무시)
if best_gap is None or gap < best_gap:
best_gap = gap # 지금까지 중 가장 가까운 기록을 갱신
best_price = p
print(f"격자 탐색이 찾은 균형 가격: {best_price:.2f}")
print(f"그때 수요-공급 격차: {best_gap:.4f}잔")
print(f"공식이 준 정확한 답: {find_equilibrium()[0]:.2f}")격자 탐색이 찾은 균형 가격: 18.00
그때 수요-공급 격차: 0.0000잔
공식이 준 정확한 답: 18.00
한 줄씩 뜯어보기
best_price = None,best_gap = None— “아직 후보가 없다”는 상태입니다. 4장에서 최댓값을 찾을 때 첫 값을 잠정 우승자로 두던 패턴의 변형인데, 여기서는 아예 빈 상태에서 시작합니다.gap = abs(qd - qs)— 수요가 많든 공급이 많든 상관없이, 둘이 얼마나 벌어졌는지만 봅니다.abs로 부호를 떼어 내면 초과수요든 초과공급이든 하나의 잣대로 비교할 수 있습니다.if best_gap is None or gap < best_gap:— 아직 기록이 없거나(is None), 지금 가격이 지금까지보다 더 가까우면, 우승자를 갈아 치웁니다.is None을 앞에 둔 덕분에 첫 번째 반복에서도 탈 없이 통과합니다.- 반복이 끝나면
best_price에는 501개 후보 중 수요와 공급이 가장 붙었던 가격이 남습니다.
결과를 보면 격자 탐색이 찾은 값이 공식의 답 18.00과 정확히 맞아떨어집니다. 격자에 18.0이 들어 있었기 때문입니다. 만약 균형이 18.03처럼 격자 사이에 있었다면, 이 방법은 가장 가까운 격자점까지만 맞힙니다. 답의 정밀도가 격자의 촘촘함에 달려 있다는 뜻입니다. 더 정확히 알고 싶으면 격자를 더 잘게 나누면 됩니다(linspace의 세 번째 숫자를 키우면 됩니다). 공식이 있으면 공식이 언제나 낫지만, 공식이 없을 때 이 우직한 탐색이 우리를 구합니다.
# [직접 해보기]
# TODO 1) np.linspace로 0부터 60까지 0.05 간격의 가격 격자를 만드세요. (개수를 잘 계산!)
# TODO 2) 수요 Qd = 150 - 3P, 공급 Qs = 30 + 2P 에 대해 격자 탐색으로 균형 가격을 찾으세요.
# quantity_demanded(p, a=150, b=3) 처럼 파라미터를 바꿔 호출하면 됩니다.
# TODO 3) find_equilibrium(a=150, b=3, c=30, d=2) 의 공식 답과 비교해 보세요.6.7 눈으로 보는 균형: 왜 가격이 세로축인가
숫자로 확인한 균형을 이제 그림으로 옮깁니다. 그런데 그리기 전에 한 가지 관례를 짚어야 합니다. 경제학 교과서의 수요·공급 그림은 수학 시간에 배운 그래프와 축이 뒤바뀌어 있습니다. 우리가 다루는 함수는 가격 \(P\)를 넣으면 수량 \(Q\)가 나오는 \(Q = f(P)\) 꼴이니, 수학의 관례대로라면 가로축(독립변수)에 \(P\), 세로축(종속변수)에 \(Q\)를 놓아야 합니다. 그런데 경제학은 정반대로, 가격을 세로축, 수량을 가로축에 그립니다.
역사적 사연이 있습니다. 19세기 경제학자 마셜(Alfred Marshall)이 그렇게 그린 이후 관례로 굳었습니다. 그 바람에 그래프의 기울기를 읽을 때 주의해야 합니다. “수요 곡선이 우하향한다”고 말할 때 우리 눈에 보이는 것은 세로축 가격이 내려갈수록 가로축 수량이 늘어나는 모습입니다. 관례를 따르려면 곡선을 그릴 때 plot에 넣는 순서를 뒤집어, 수량을 가로, 가격을 세로로 넣어야 합니다.
그리는 절차는 세 단계입니다. 먼저 가격 후보를 격자로 만들고, 각 가격에서 수요량과 공급량을 계산해 리스트에 담고, 가로축에 수량 리스트·세로축에 가격 리스트를 넣어 plot을 부릅니다. 두 번째 단계, 즉 “각 가격마다 수요량을 계산해 리스트에 담기”를 짧게 쓰는 문법이 리스트 컴프리헨션(list comprehension)입니다.
a, b, c, d = 100, 2, 10, 3
price_range = np.linspace(0, 50, 100) # 그림용 가격 격자 (0 ~ 50, 100개)
# 리스트 컴프리헨션: 각 가격 p마다 수요량을 계산해 리스트로 모읍니다.
qd_values = [quantity_demanded(p, a, b) for p in price_range]
qs_values = [quantity_supplied(p, c, d) for p in price_range]
# 위 한 줄은 아래 for문 네 줄과 정확히 같은 일을 합니다.
# qd_values = []
# for p in price_range:
# qd_values.append(quantity_demanded(p, a, b))
print("가격 격자 개수:", len(price_range))
print("첫 수요량:", qd_values[0], "/ 마지막 수요량:", qd_values[-1])가격 격자 개수: 100
첫 수요량: 100.0 / 마지막 수요량: 0
한 줄씩 뜯어보기
[quantity_demanded(p, a, b) for p in price_range] 한 줄을 풀어 읽으면 이렇습니다.
- 오른쪽
for p in price_range—price_range의 값을 하나씩 꺼내p에 담습니다. 4장의for와 같은 순회입니다. - 왼쪽
quantity_demanded(p, a, b)— 그렇게 꺼낸p마다 이 식을 계산합니다. - 바깥의 대괄호
[ ]— 계산된 결과들을 순서대로 모아 새 리스트로 만듭니다.
즉 “price_range의 각 p에 대해 quantity_demanded(p, a, b)를 계산한 값들의 리스트”라고 소리 내어 읽으면 그대로 코드가 됩니다. 주석으로 적어 둔 네 줄짜리 for문과 결과는 완전히 같습니다. 리스트를 만드는 것이 목적일 때, 컴프리헨션은 빈 리스트를 두고 append하는 준비 과정을 생략해 줍니다. 다만 계산이 복잡하거나 조건 분기가 여럿이면 여전히 for문이 읽기 좋습니다. 짧고 단순할 때만 컴프리헨션을 쓰는 것이 요령입니다.
이제 곡선을 그립니다. 관례대로 가로축에 수량, 세로축에 가격을 넣는 것에 주목하세요.
이 그림부터는 지금까지 쓰던 plot 말고도 새 matplotlib 함수가 몇 개 등장합니다. scatter는 점 하나를 찍어 균형점을 표시하고, annotate와 text는 그래프 위에 글자를 얹고, axhline은 수평 기준선을 긋고, xlim·ylim은 축이 보여 줄 범위를 정합니다. 이 함수들은 8장에서 제대로 배웁니다. 지금은 “균형점을 찍고 그 옆에 이름표를 단다”는 뜻으로만 읽어도 그래프를 이해하는 데 지장이 없습니다.
p_star, q_star = find_equilibrium(a, b, c, d)
plt.figure(figsize=(8, 6))
plt.plot(qd_values, price_range, color="tab:blue", label="수요 (D)") # 가로=수량, 세로=가격
plt.plot(qs_values, price_range, color="tab:red", label="공급 (S)")
plt.scatter(q_star, p_star, color="black", zorder=5, s=80)
plt.plot([q_star, q_star], [0, p_star], color="gray", linestyle="--", linewidth=0.8)
plt.plot([0, q_star], [p_star, p_star], color="gray", linestyle="--", linewidth=0.8)
plt.annotate(f"균형 E ({q_star:.0f}, {p_star:.0f})", xy=(q_star, p_star),
xytext=(q_star + 3, p_star + 3))
plt.title("대학가 아메리카노 시장의 수요와 공급")
plt.xlabel("수량 Q (하루 판매 잔 수)")
plt.ylabel("가격 P (100원 단위)")
plt.xlim(left=0)
plt.ylim(bottom=0)
plt.legend()
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()
파란 수요선과 빨간 공급선이 만나는 검은 점이, 앞에서 숫자로 두 번 확인한 균형 \((Q^*, P^*) = (64, 18)\)입니다. 이 한 장의 그림이 이번 장 앞부분을 요약합니다. 균형보다 가격이 낮으면 수요선이 공급선보다 오른쪽에 있어(사려는 양이 더 많아) 물량이 모자라고, 높으면 반대로 남아돕니다. 오직 두 선이 겹치는 높이에서만 아무도 아쉬울 것 없이 거래가 청산됩니다.
실수 노트: 축을 뒤집어 그렸을 때
이 그림에서 가장 흔한 사고는
plot에 넣는 순서를 헷갈리는 것입니다.plt.plot(price_range, qd_values)처럼 무심코 가격을 먼저 넣으면, 수학 시간의 관례대로 가격이 가로축에 오면서 곡선이 통째로 눕습니다. 그러면 우하향해야 할 수요선이 우상향하는 것처럼 보이고, 경제학의 그림과 어긋납니다. 에러는 나지 않기 때문에 더 위험합니다. 그림이 어딘가 이상하다 싶으면,plot의 괄호 안 첫 번째 자리에 수량이, 두 번째 자리에 가격이 들어갔는지부터 확인하세요. 이 책의 모든 수요·공급 그림은plt.plot(수량, 가격)순서를 지킵니다.
# [직접 해보기]
# TODO 1) 수요 Qd = 150 - 3P, 공급 Qs = 30 + 2P 의 곡선을 하나의 그래프에 그리세요.
# TODO 2) find_equilibrium(a=150, b=3, c=30, d=2)로 균형점을 구해 scatter로 찍으세요.
# TODO 3) 가로축은 수량, 세로축은 가격입니다. 축 순서를 지켰는지 확인하세요.6.8 거래에서 누가 얼마나 이득을 보는가: 소비자·생산자 잉여
균형에서 64잔이 1,800원에 팔립니다. 그런데 이 거래로 사는 쪽과 파는 쪽은 각각 얼마나 남는 장사를 한 걸까요. 경제학은 이 이득을 잉여(surplus)라는 이름으로 잽니다.
소비자 잉여부터 봅니다. 어떤 학생은 아메리카노 한 잔에 3,000원까지 낼 생각이 있었는데 1,800원에 샀습니다. 이 학생은 1,200원어치 이득을 본 셈입니다. 수요 곡선은 바로 이 “낼 용의가 있는 최대 금액”을 잔마다 늘어놓은 것이므로, 수요 곡선과 실제 가격(\(P^*\)) 사이의 넓이가 소비자 전체의 이득, 곧 소비자 잉여가 됩니다. 반대로 어떤 카페는 1,000원만 받아도 팔 생각이 있었는데 1,800원을 받았습니다. 800원 이득입니다. 공급 곡선은 “받아야 팔 최소 금액”을 잔마다 늘어놓은 것이므로, 실제 가격과 공급 곡선 사이의 넓이가 생산자 잉여입니다.
우리 모형은 곡선이 전부 직선이라, 이 넓이가 삼각형으로 딱 떨어집니다. 수요 곡선이 세로축과 만나는 높이(수요가 0이 되는 가격) \(a/b\)부터 균형 가격 \(P^*\)까지가 소비자 잉여 삼각형의 높이이고, 밑변은 균형 거래량 \(Q^*\)입니다.
\[\text{소비자 잉여} = \tfrac{1}{2} \times Q^* \times \left(\tfrac{a}{b} - P^*\right), \qquad \text{생산자 잉여} = \tfrac{1}{2} \times Q^* \times \left(P^* - \left(-\tfrac{c}{d}\right)\right)\]
생산자 잉여 쪽 밑변의 아래 끝 \(-c/d\)는 공급 곡선을 세로축까지 늘렸을 때 만나는 높이인데, \(c\)가 양수면 이 값이 음수로 나옵니다. 아주 적은 물량 구간에서 선형 근사가 현실과 어긋나는 지점이지만, 삼각형 넓이를 잴 때는 이 절편을 그대로 씁니다. 공식을 함수로 옮겨 둡시다.
def surplus(a=100, b=2, c=10, d=3):
"""선형 시장의 소비자 잉여와 생산자 잉여를 (CS, PS) 튜플로 반환합니다.
균형이 없으면 None을 반환합니다."""
eq = find_equilibrium(a, b, c, d)
if eq is None:
return None
p_star, q_star = eq
demand_choke = a / b # 수요가 0이 되는 가격 (수요선의 세로축 절편)
supply_choke = -c / d # 공급이 0이 되는 가격 (음수일 수 있음)
cs = 0.5 * q_star * (demand_choke - p_star)
ps = 0.5 * q_star * (p_star - supply_choke)
return cs, ps
cs, ps = surplus()
print(f"소비자 잉여 CS = {cs:,.1f}")
print(f"생산자 잉여 PS = {ps:,.1f}")
print(f"총잉여 TS = {cs + ps:,.1f}")소비자 잉여 CS = 1,024.0
생산자 잉여 PS = 682.7
총잉여 TS = 1,706.7
기본 시장의 소비자 잉여는 1,024, 생산자 잉여는 약 682.7, 둘을 더한 총잉여는 약 1,706.7로 나옵니다. 이 세 숫자가 뒤에서 시장에 충격을 줄 때 어떻게 갈라지는지가 정책 분석의 핵심 질문입니다. 예컨대 가격상한을 씌우면 총잉여가 줄어드는데, 그 줄어든 몫이 어디로 사라지는지를 이 숫자들의 변화로 추적할 수 있습니다.
숫자만으로는 잘 와닿지 않으니 그림으로 칠해 봅시다. 곡선과 가격선 사이를 색으로 메우는 데는 fill_between을 씁니다. 8장에서 그래프를 본격적으로 꾸밀 때 다시 다루는 도구인데, 여기서는 잉여 영역을 보여 주는 용도로 맛만 봅니다.
잠깐, numpy 배열은 계산이 다릅니다
바로 앞 그래프에서는 가격 하나하나를 리스트 컴프리헨션으로 돌려 수요량을 구했습니다. 그런데 이번 셀의 p_demand = (a - q_fill) / b에는 반복문도 컴프리헨션도 없습니다. q_fill이 numpy 배열이기 때문입니다.
numpy 배열에 사칙연산을 걸면, 같은 계산이 배열의 모든 원소에 자동으로 한 번에 적용됩니다. 이 성질을 브로드캐스팅이라고 부릅니다. 지금은 “배열에 산술을 하면 원소마다 한꺼번에 계산된다” 정도로만 알면 충분하고, numpy의 자세한 쓰임새는 뒤에서 데이터를 본격적으로 다룰 때 다시 만납니다.
a, b, c, d = 100, 2, 10, 3
p_star, q_star = find_equilibrium(a, b, c, d)
q_fill = np.linspace(0, q_star, 100) # 0부터 균형 거래량까지의 수량 격자
p_demand = (a - q_fill) / b # 수요 역함수 P = (a - Q) / b
p_supply = (q_fill - c) / d # 공급 역함수 P = (Q - c) / d
plt.figure(figsize=(8, 6))
plt.plot(qd_values, price_range, color="tab:blue", label="수요 (D)")
plt.plot(qs_values, price_range, color="tab:red", label="공급 (S)")
# 소비자 잉여: 가격선 P* 위쪽, 수요선 아래쪽
plt.fill_between(q_fill, p_star, p_demand, color="tab:blue", alpha=0.25, label="소비자 잉여")
# 생산자 잉여: 가격선 P* 아래쪽, 공급선 위쪽
plt.fill_between(q_fill, p_supply, p_star, color="tab:red", alpha=0.25, label="생산자 잉여")
plt.axhline(0, color="gray", linewidth=0.6)
plt.scatter(q_star, p_star, color="black", zorder=5, s=70)
plt.annotate("E", xy=(q_star, p_star), xytext=(q_star + 2, p_star + 2))
plt.title("균형에서의 소비자 잉여와 생산자 잉여")
plt.xlabel("수량 Q (하루 판매 잔 수)")
plt.ylabel("가격 P (100원 단위)")
plt.xlim(0, 60)
plt.ylim(-6, 52)
plt.legend()
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()
fill_between(x, y1, y2)는 가로축 값 x를 따라가며 y1과 y2 두 높이 사이를 색으로 메웁니다. 소비자 잉여를 칠할 때는 아래 경계로 균형 가격 p_star를, 위 경계로 수요선 p_demand를 넣었습니다. 두 선 사이가 곧 소비자 잉여 삼각형입니다. 생산자 잉여는 반대로 공급선 p_supply와 가격선 사이를 칠했습니다. 공급선을 왼쪽 끝(수량 0)까지 늘리면 가격축을 음수 높이에서 만나기 때문에 붉은 삼각형의 뾰족한 끝이 0선 아래로 살짝 내려갑니다. 앞의 공식에서 \(-c/d\)가 음수였던 그 지점이 그림으로 보이는 것입니다.
이 그림이 말하는 경제학 문장은 한 줄로 요약됩니다. 균형에서 거래가 이루어지면 사는 쪽과 파는 쪽 모두 색칠된 삼각형만큼 이득을 얻고, 그 합인 총잉여가 이 시장이 만들어 내는 가치의 크기다.
# [직접 해보기]
# TODO 1) surplus(a=150, b=3, c=30, d=2)로 다른 시장의 소비자/생산자 잉여를 구해 출력하세요.
# TODO 2) 기본 시장과 비교해 어느 쪽 잉여가 더 큰지 주석으로 적어 보세요.6.9 충격을 주면 균형은 어디로 가는가: 비교정학
지금까지의 시장은 멈춰 있었습니다. 이제 흔들어 봅니다. 수요와 공급 곡선은 붙박이가 아니라, 가격 아닌 다른 사정이 바뀌면 통째로 옮겨 갑니다. 이 이동이 균형을 어디로 밀어내는지 비교하는 것을 비교정학(comparative statics)이라고 합니다.
수요를 옮기는 사정으로는 소득 변화, 대체재 가격 변화, 유행, 계절 같은 것들이 있습니다. 아메리카노 시장이라면 개강해서 학생이 몰리는 것이 수요 증가입니다. 우리 모형에서 수요의 규모는 파라미터 \(a\)가 쥐고 있으니, 수요 증가는 \(a\)를 키우는 것으로 표현됩니다. 공급을 옮기는 사정은 원두값, 임대료, 기술, 경쟁 카페 수 같은 것들이고, 우리 모형에서는 \(c\)가 그 규모를 담습니다.
먼저 개강 특수로 수요가 늘어 \(a\)가 100에서 130으로 뛴 경우를 봅니다. 원래 곡선은 파선으로, 옮겨 간 곡선은 실선으로 그려 비교합니다.
# 원래 시장과 수요가 증가한 시장의 균형을 각각 계산
p0, q0 = find_equilibrium(a=100, b=2, c=10, d=3) # 원래
p1, q1 = find_equilibrium(a=130, b=2, c=10, d=3) # 수요 증가
pr = np.linspace(0, 65, 100)
qd_old = [quantity_demanded(p, 100, 2) for p in pr]
qd_new = [quantity_demanded(p, 130, 2) for p in pr]
qs_now = [quantity_supplied(p, 10, 3) for p in pr]
plt.figure(figsize=(8, 6))
plt.plot(qd_old, pr, color="tab:blue", linestyle="--", label="원래 수요 D0")
plt.plot(qd_new, pr, color="tab:blue", linestyle="-", label="새 수요 D1")
plt.plot(qs_now, pr, color="tab:red", label="공급 S")
plt.scatter(q0, p0, color="gray", s=70, zorder=5, label=f"원래 균형 ({q0:.0f}, {p0:.0f})")
plt.scatter(q1, p1, color="black", s=80, zorder=5, label=f"새 균형 ({q1:.0f}, {p1:.0f})")
plt.title("수요 증가: 개강 특수로 학생이 몰리면")
plt.xlabel("수량 Q")
plt.ylabel("가격 P (100원 단위)")
plt.xlim(left=0)
plt.ylim(bottom=0)
plt.legend()
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()
print(f"가격 P: {p0:.0f} -> {p1:.0f} (100원 단위)")
print(f"수량 Q: {q0:.0f} -> {q1:.0f} (잔)")
가격 P: 18 -> 24 (100원 단위)
수량 Q: 64 -> 82 (잔)
수요선이 오른쪽으로 통째로 밀려나면서 균형점이 오른쪽 위로 옮겨 갔습니다. 가격은 18에서 24로, 거래량은 64잔에서 76잔으로 함께 올랐습니다. 개강하면 학교 앞 커피값이 슬며시 오르고 카페마다 줄이 길어지는 현상을 이 그림 하나가 설명합니다. 수요가 늘면 가격과 거래량이 같은 방향으로 움직인다는 것, 이것이 수요 이동의 규칙입니다.
이번에는 공급 쪽을 건드립니다. 근처에 대형 프랜차이즈가 새로 들어와 공급이 늘어난 경우, 즉 \(c\)가 10에서 40으로 커진 상황입니다.
p0, q0 = find_equilibrium(a=100, b=2, c=10, d=3) # 원래
p2, q2 = find_equilibrium(a=100, b=2, c=40, d=3) # 공급 증가
pr = np.linspace(0, 65, 100)
qd_now = [quantity_demanded(p, 100, 2) for p in pr]
qs_old = [quantity_supplied(p, 10, 3) for p in pr]
qs_new = [quantity_supplied(p, 40, 3) for p in pr]
plt.figure(figsize=(8, 6))
plt.plot(qd_now, pr, color="tab:blue", label="수요 D")
plt.plot(qs_old, pr, color="tab:red", linestyle="--", label="원래 공급 S0")
plt.plot(qs_new, pr, color="tab:red", linestyle="-", label="새 공급 S1")
plt.scatter(q0, p0, color="gray", s=70, zorder=5, label=f"원래 균형 ({q0:.0f}, {p0:.0f})")
plt.scatter(q2, p2, color="black", s=80, zorder=5, label=f"새 균형 ({q2:.0f}, {p2:.0f})")
plt.title("공급 증가: 대형 카페가 들어오면")
plt.xlabel("수량 Q")
plt.ylabel("가격 P (100원 단위)")
plt.xlim(left=0)
plt.ylim(bottom=0)
plt.legend()
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()
print(f"가격 P: {p0:.0f} -> {p2:.0f} (100원 단위)")
print(f"수량 Q: {q0:.0f} -> {q2:.0f} (잔)")
가격 P: 18 -> 12 (100원 단위)
수량 Q: 64 -> 76 (잔)
이번에는 공급선이 오른쪽으로 밀리면서 균형점이 오른쪽 아래로 내려갔습니다. 가격은 18에서 14로 떨어지고 거래량은 64잔에서 72잔으로 늘었습니다. 공급이 늘면 가격은 내리고 거래량은 오르는, 서로 반대 방향의 움직임입니다. 수요 이동과 나란히 놓고 보면 규칙이 또렷해집니다. 수요가 늘면 가격과 수량이 같이 오르고, 공급이 늘면 수량은 오르되 가격은 내립니다. 감소는 각각의 반대입니다. 이 네 가지 방향만 손에 익히면 웬만한 뉴스의 “가격이 왜 이렇게 됐나”는 그림 없이도 머릿속에서 풀립니다.
# [직접 해보기]
# TODO 1) 원두값 급등으로 공급이 줄어 c가 10에서 -20으로 내려간 상황을 그리세요.
# (quantity_supplied(p, c=-20, d=3), find_equilibrium(a=100,b=2,c=-20,d=3))
# TODO 2) 원래 공급선(파선)과 새 공급선(실선), 두 균형점을 한 그래프에 표시하세요.
# TODO 3) 가격과 거래량이 각각 어떻게 변했는지 print로 출력하세요.6.10 균형으로 가는 길: 가격은 어떻게 스스로 찾아가는가
지금까지 우리는 균형을 “이미 거기 있는 점”으로 다뤘습니다. 하지만 여는 질문을 떠올려 보세요. 처음부터 1,800원인 카페는 없었습니다. 어떤 곳은 높게 시작했다가 손님이 없어 내렸고, 어떤 곳은 낮게 시작했다가 재료비를 못 견뎌 올렸습니다. 균형은 주어진 것이 아니라, 시장이 더듬더듬 찾아가는 종착점입니다. 이 더듬는 과정을 코드로 재생해 봅시다.
원리는 3장에서 배운 초과수요·초과공급 판정 그대로입니다. 어떤 가격에서 사려는 양이 팔려는 양보다 많으면(초과수요), 못 산 손님들이 웃돈을 부르며 가격을 밀어 올립니다. 반대로 남아돌면(초과공급) 팔려고 가격을 내립니다. 가격을 올릴지 내릴지가 매번 초과수요의 부호에 달려 있고, 언제 멈출지는 미리 알 수 없습니다. 균형에 “충분히 가까워질 때까지” 반복하는 것이므로, 횟수가 정해진 for가 아니라 조건이 풀릴 때까지 도는 4장의 while이 맞습니다.
price = 5.0 # 시작 호가: 균형(18)보다 한참 낮게 잡아 봅니다
k = 0.05 # 조정 속도: 초과분에 이 비율만큼 가격을 움직입니다
step = 0
history = [price] # 가격이 움직인 자취를 기록
while step < 200:
qd = quantity_demanded(price)
qs = quantity_supplied(price)
gap = qd - qs # 양수면 초과수요, 음수면 초과공급
if abs(gap) < 0.5: # 균형에 충분히 가까워지면 멈춤
break
price = price + k * gap # 초과수요면 가격 상승, 초과공급이면 하락
history.append(price)
step += 1
print(f"{step}번 조정 끝에 가격이 {price:.2f}로 수렴했습니다.")
print(f"공식이 준 균형 가격: {find_equilibrium()[0]:.2f}")17번 조정 끝에 가격이 17.90로 수렴했습니다.
공식이 준 균형 가격: 18.00
한 줄씩 뜯어보기
price = 5.0— 일부러 균형에서 멀리 떨어진 곳에서 출발합니다. 500원짜리 아메리카노는 초과수요가 심할 것입니다.gap = qd - qs— 초과수요를 부호까지 살려 계산합니다. 양수면 모자라고, 음수면 남습니다.if abs(gap) < 0.5: break— 멈춤 조건입니다. 수요와 공급의 차이가 0.5잔 미만으로 좁혀지면 “충분히 균형”으로 보고 반복을 빠져나옵니다. 정확히 0이 될 때까지 기다리지 않는 이유는, 실수 계산에서는 딱 0에 떨어지지 않을 수 있기 때문입니다.price = price + k * gap— 조정의 핵심입니다. 초과수요(양수)면 가격이 오르고, 초과공급(음수)이면 내립니다.k는 한 번에 얼마나 움직일지를 정하는 보폭입니다.step += 1과while step < 200— 혹시 수렴하지 않고 무한히 도는 사태를 막는 안전장치입니다. 4장의 실수 노트에서 강조한,while에는 반드시 탈출구를 마련하라는 규칙 그대로입니다.
보폭 k를 너무 크게 잡으면 균형을 지나쳐 반대편으로 튕겨 나가며 오히려 발산할 수도 있습니다. 여기서는 얌전한 값을 골라 매끄럽게 수렴했습니다. 자취를 그림으로 보면 가격이 균형을 향해 잦아드는 모습이 한눈에 들어옵니다.
plt.figure(figsize=(8, 5))
plt.plot(range(len(history)), history, marker="o", markersize=3, color="tab:green")
plt.axhline(18, color="gray", linestyle="--", label="균형 가격 18")
plt.title("호가 조정 과정: 초과수요가 가격을 균형으로 끌어올린다")
plt.xlabel("조정 횟수")
plt.ylabel("가격 P (100원 단위)")
plt.legend()
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()
6.11 한 박자 늦는 공급: 거미집 모형
방금 조정 과정은 사는 쪽과 파는 쪽이 같은 순간에 가격을 보고 반응한다고 가정했습니다. 그런데 어떤 시장은 공급이 한 박자 늦습니다. 대표적인 예가 농산물입니다. 배추 농가는 올해 배추값이 좋으면 내년에 재배 면적을 늘리는데, 그렇게 늘어난 배추가 시장에 나오는 것은 한 해 뒤입니다. 즉 올해의 공급량은 올해 가격이 아니라 작년 가격을 보고 정해집니다.
이 시차를 모형에 넣으면 이렇게 됩니다. 이번 기의 공급량은 지난 기 가격으로 결정되고(\(Q_t^s = c + dP_{t-1}\)), 시장은 그 물량을 다 팔아 치우는 가격으로 청산됩니다(\(a - bP_t = Q_t^s\)). 두 식을 합치면 이번 기 가격이 지난 기 가격에 의해 정해지는 점화식이 나옵니다.
\[P_t = \frac{a - c}{b} - \frac{d}{b}\, P_{t-1}\]
가격이 균형에서 벗어난 정도는 매 기 \(-\dfrac{d}{b}\) 배로 바뀝니다. 그래서 이 배율의 크기, 즉 \(\dfrac{d}{b}\)가 1보다 작으냐 크냐가 시장의 운명을 가릅니다. 작으면 벗어난 폭이 매 기 줄어 균형으로 수렴하고, 크면 폭이 커지며 발산합니다. 기하로 옮기면 공급 곡선이 수요 곡선보다 가파를 때(\(b > d\)) 수렴하고, 완만할 때(\(b < d\)) 발산합니다. 두 경우를 나란히 시뮬레이션해 봅시다.
def cobweb(a, b, c, d, p0, periods=6):
"""거미집 모형에서 가격이 기(period)마다 어떻게 변하는지 리스트로 반환합니다.
이번 기 공급량은 지난 기 가격으로 결정됩니다."""
prices = [p0]
for t in range(periods):
p_prev = prices[-1]
qs = c + d * p_prev # 이번 기 공급량 = 지난 기 가격으로 결정
p_new = (a - qs) / b # 그 물량을 다 파는 청산 가격 (수요 역함수)
prices.append(p_new)
return prices
# 두 시장 모두 a=100, c=10 이라 균형 가격은 똑같이 18. 기울기 배율만 다릅니다.
converge = cobweb(a=100, b=3, c=10, d=2, p0=16) # d/b = 0.67 < 1 -> 수렴
diverge = cobweb(a=100, b=2, c=10, d=3, p0=16) # d/b = 1.5 > 1 -> 발산
print("수렴 시나리오 (d/b<1):", [round(p, 2) for p in converge])
print("발산 시나리오 (d/b>1):", [round(p, 2) for p in diverge])수렴 시나리오 (d/b<1): [16, 19.33, 17.11, 18.59, 17.6, 18.26, 17.82]
발산 시나리오 (d/b>1): [16, 21.0, 13.5, 24.75, 7.88, 33.19, -4.78]
한 줄씩 뜯어보기
prices = [p0]— 첫 가격 하나만 담고 시작하는 리스트입니다. 여기에 매 기의 가격을 이어 붙입니다.p_prev = prices[-1]— 리스트의 마지막 원소, 즉 바로 지난 기 가격을 꺼냅니다. 공급이 반응하는 대상이 지난 기 가격이라는 모형의 핵심이 이 한 줄에 들어 있습니다.qs = c + d * p_prev— 지난 기 가격으로 이번 기 공급량을 정합니다.p_new = (a - qs) / b— 그 공급량qs를 수요가 전부 흡수하려면 가격이 얼마여야 하는지를 수요식 \(Q_d = a - bP\)에서 거꾸로 풉니다.prices.append(p_new)— 새 가격을 자취에 더합니다. 다음 반복에서 이 값이 다시p_prev가 됩니다.
두 시나리오는 균형 가격이 18로 똑같고, 시작 가격도 16으로 똑같습니다. 다른 것은 오직 기울기 배율 \(d/b\)뿐입니다. 출력된 숫자를 보면 수렴 쪽은 18을 사이에 두고 진동 폭이 점점 줄고, 발산 쪽은 진동 폭이 점점 커지다 급기야 음수 가격까지 나옵니다. 음수 가격은 앞의 실수 노트에서 말했듯 모형이 현실과 어긋나기 시작하는 신호입니다. 그림으로 두 운명을 비교합니다.
plt.figure(figsize=(12, 4.5))
plt.subplot(1, 2, 1)
plt.plot(range(len(converge)), converge, marker="o", color="tab:blue")
plt.axhline(18, color="gray", linestyle="--")
plt.title("수렴: 공급이 더 가파를 때 (b=3, d=2)")
plt.xlabel("기(period)")
plt.ylabel("가격 P")
plt.grid(True, linestyle="--", alpha=0.5)
plt.subplot(1, 2, 2)
plt.plot(range(len(diverge)), diverge, marker="o", color="tab:red")
plt.axhline(18, color="gray", linestyle="--")
plt.title("발산: 수요가 더 가파를 때 (b=2, d=3)")
plt.xlabel("기(period)")
plt.ylabel("가격 P")
plt.grid(True, linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()
왼쪽은 진동이 잦아들며 18로 모여들고, 오른쪽은 진동이 벌어지며 균형에서 멀어집니다. plt.subplot(1, 2, 1)은 그림판을 1행 2열로 나눠 그 첫 칸에 그린다는 뜻이고, plt.tight_layout()은 두 그림이 겹치지 않게 간격을 정리해 줍니다. 이 배치 문법은 8장에서 자세히 다룹니다.
거미집이 말해 주는 것은, 시차가 있는 시장에서는 균형이 존재한다고 해서 시장이 반드시 거기에 도달하지는 않는다는 사실입니다. 농산물 가격이 해마다 오르내리기를 반복하는 현상, 이른바 돼지고기 파동이나 배추 파동의 뼈대가 바로 이 모형입니다.
6.12 가격에 뚜껑을 씌우면: 가격상한제
3장에서 가격상한이 균형보다 낮을 때만 실제로 구속력을 갖는다는 판정을 코드로 해 본 적이 있습니다. 그때는 “구속력이 있다/없다”까지만 답했는데, 이제 그 상한이 시장에 정확히 얼마만큼의 부족을 일으키는지 수량으로 잴 수 있게 되었습니다.
정부가 학생 복지를 명분으로 대학가 아메리카노 가격에 1,400원(\(P=14\)) 상한을 씌웠다고 합시다. 균형 가격 1,800원보다 낮으니 구속력이 있습니다. 이 가격에서 사려는 양과 팔려는 양을 각각 계산하면, 그 차이가 곧 시장에 생기는 부족분입니다.
ceiling = 14 # 가격상한: 1,400원
p_star, q_star = find_equilibrium()
qd_c = quantity_demanded(ceiling) # 상한 가격에서 사려는 양
qs_c = quantity_supplied(ceiling) # 상한 가격에서 팔려는 양
shortage = qd_c - qs_c # 부족분 (초과수요)
traded = min(qd_c, qs_c) # 실제로 거래되는 양은 적은 쪽 (팔 수 있는 만큼만)
print(f"균형 가격 {p_star:.0f} (1,800원)에 상한 {ceiling} (1,400원)를 씌우면")
print(f" 사려는 양 Qd = {qd_c}잔")
print(f" 팔려는 양 Qs = {qs_c}잔")
print(f" 부족분 = {shortage}잔 (이만큼의 손님은 사고 싶어도 못 삽니다)")
print(f" 실제 거래량 = {traded}잔")균형 가격 18 (1,800원)에 상한 14 (1,400원)를 씌우면
사려는 양 Qd = 72잔
팔려는 양 Qs = 52잔
부족분 = 20잔 (이만큼의 손님은 사고 싶어도 못 삽니다)
실제 거래량 = 52잔
pr = np.linspace(0, 50, 100)
qd_line = [quantity_demanded(p, 100, 2) for p in pr]
qs_line = [quantity_supplied(p, 10, 3) for p in pr]
plt.figure(figsize=(8, 6))
plt.plot(qd_line, pr, color="tab:blue", label="수요 (D)")
plt.plot(qs_line, pr, color="tab:red", label="공급 (S)")
plt.axhline(ceiling, color="purple", linestyle="--", label=f"가격상한 {ceiling}")
plt.scatter(q_star, p_star, color="black", s=60, zorder=5, label="원래 균형")
# 상한 가격에서 벌어진 부족분을 가로 화살표로 표시
plt.annotate("", xy=(qd_c, ceiling), xytext=(qs_c, ceiling),
arrowprops=dict(arrowstyle="<->", color="purple"))
plt.text((qs_c + qd_c) / 2, ceiling - 2.5, f"부족 {shortage}잔",
ha="center", color="purple")
plt.title("가격상한제: 균형보다 낮은 뚜껑이 만드는 부족")
plt.xlabel("수량 Q")
plt.ylabel("가격 P (100원 단위)")
plt.xlim(left=0)
plt.ylim(bottom=0)
plt.legend()
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()
상한선(보라 파선) 위에서 수요선은 오른쪽, 공급선은 왼쪽에 있고, 그 사이 벌어진 폭이 부족분 20잔입니다. 가격을 낮게 묶어 둔 대가로, 팔리는 양 자체가 균형 때의 64잔에서 52잔으로 줄었습니다. 싸게 사려던 정책이 오히려 살 수 있는 양을 줄인 셈입니다. 못 산 20잔어치의 손님은 줄을 서서 기다리거나, 웃돈을 얹은 뒷거래로 밀려나거나, 그냥 포기합니다. 가격에 뚜껑을 씌운다고 수요와 공급의 힘이 사라지지는 않는다는 것, 다만 그 힘이 가격이 아닌 다른 형태(줄서기, 품절, 암시장)로 새어 나온다는 것을 이 그림이 보여 줍니다.
6.13 연습문제
손풀기 1. 새 시장의 균형 — 어떤 시장의 수요가 \(Q_d = 200 - 4P\), 공급이 \(Q_s = 50 + P\)입니다. 본문의 find_equilibrium을 파라미터 a=200, b=4, c=50, d=1로 호출해 균형 가격과 거래량을 구하고, “균형 가격 ○○, 균형 거래량 ○○” 형식으로 출력하세요. 종이에 공식 \(P^* = (a-c)/(b+d)\)로도 풀어 코드 결과와 맞는지 확인하세요.
손풀기 2. 시장 상태 표 — 손풀기 1의 시장에 대해, 가격을 20부터 40까지 5 간격으로 바꿔 가며 각 가격의 수요량·공급량과 초과수요/초과공급 여부를 for문으로 한 줄씩 출력하세요. 본문 앞부분의 시장 상태 표를 그대로 응용하면 됩니다.
손풀기 3. 잉여 계산 — 본문의 surplus 함수로 기본 시장(\(a=100, b=2, c=10, d=3\))의 소비자 잉여, 생산자 잉여, 총잉여를 구해 출력하세요. 이어서 소비자 잉여가 생산자 잉여보다 큰지 작은지 if문으로 판정해 한 줄 출력하세요.
응용 1. 격자 탐색으로 균형 찾기 — 손풀기 1의 시장에 대해, np.linspace로 0부터 50까지 0.1 간격의 가격 격자를 만들고, for문으로 \(|Q_d - Q_s|\)가 가장 작아지는 가격을 찾으세요. 공식이 준 답과 비교하고, 격자를 0.01 간격으로 촘촘히 했을 때 결과가 어떻게 달라지는지 한 줄 주석으로 적으세요.
응용 2. 공급 감소 비교정학 — 손풀기 1의 시장에서 생산비가 올라 공급이 \(Q_s = 20 + P\)로 줄었습니다(\(c\)가 50에서 20으로 감소). 새 균형을 계산하고, 원래 공급선(파선)과 새 공급선(실선), 두 균형점을 한 그래프에 그리세요. 가격과 거래량이 각각 어떻게 변했는지 출력하세요.
응용 3. 가격상한의 효과 — 손풀기 1의 시장에 가격상한 \(P = 25\)를 씌운다고 합시다. 상한이 구속력이 있는지 균형 가격과 비교해 판정하고, 상한 가격에서의 부족분과 실제 거래량을 계산해 출력하세요. 본문의 가격상한 그림을 참고해 부족분을 표시한 그래프도 그리세요.
도전 1. 수렴과 발산의 경계 — 본문의 cobweb 함수에서 수요 기울기를 \(b=2\)로 고정하고, 공급 기울기 \(d\)를 1.0부터 0.2씩 키우며 각 경우의 가격 자취를 출력하세요. \(d\)가 어느 값을 넘어서는 순간 수렴에서 발산으로 바뀌는지 찾고, 그 값이 이론이 말하는 경계 \(d = b\)와 맞는지 확인하세요.
도전 2. 수요와 공급이 함께 움직일 때 — 개강 특수로 수요가 늘고(\(a\): 100 → 130) 동시에 새 카페가 들어와 공급도 늘어난(\(c\): 10 → 40) 경우를 생각합니다. 먼저 두 변화가 함께 일어난 새 균형을 계산하세요. 그다음, 공급 증가 폭만 \(c\): 10 → 25로 줄인 경우의 균형도 계산하세요. 두 경우에서 거래량은 어떻게 되고 가격은 어떻게 되는지 비교하고, “수요와 공급이 함께 늘면 거래량의 방향은 정해지지만 가격의 방향은 정해지지 않는다”는 명제가 왜 성립하는지 두세 문장으로 설명하세요.
풀이는 부록 C에 있습니다.
6.14 미니 프로젝트: 생각을 코드로, 코드를 글로
과제: 시장 충격 보고서 (예상 소요 30~60분)
대학가 아메리카노 시장에 실제로 있을 법한 충격을 하나 골라, 그 충격이 균형과 잉여를 어떻게 바꾸는지 코드로 분석하고 글로 보고하는 과제입니다. 이번 장의 도구를 전부 한 번씩 쓰게 됩니다.
- 아래 시나리오 중 하나를 고르거나, 비슷한 강도의 시나리오를 직접 만드세요.
- 한파로 원두 산지가 얼어붙어 원두값이 급등했다 (공급 감소: \(c\)를 낮춤).
- 방학이 시작돼 학생 대부분이 귀향했다 (수요 감소: \(a\)를 낮춤).
- 학교가 무상 커피 쿠폰을 뿌려 커피 소비가 늘었다 (수요 증가: \(a\)를 높임).
- 옆 건물에 로봇 카페가 들어와 공급이 늘었다 (공급 증가: \(c\)를 높임).
- 충격 전과 후의 파라미터를 정하고,
find_equilibrium으로 두 균형을 계산하세요. - 원래 곡선(파선)과 이동한 곡선(실선), 두 균형점을 한 그래프에 그리세요. 제목과 축 라벨은 필수입니다.
surplus함수로 충격 전후의 소비자 잉여·생산자 잉여·총잉여를 계산해, 표나 출력으로 전후를 나란히 보이세요.- 결과 아래에 마크다운 셀을 만들어 3문단 보고서를 쓰세요.
3문단 글쓰기 틀
- 1문단(주장): 고른 충격이 이 시장에 무엇을 했는지 한 문장으로 못 박으세요. 예: “원두값 급등은 아메리카노 균형 가격을 올리고 거래량을 줄였으며, 그 부담은 소비자 잉여의 감소로 집중되어 나타났다.”
- 2문단(근거): 주장을 숫자와 그래프로 뒷받침하세요. 충격 전후의 균형 가격·거래량, 소비자 잉여와 생산자 잉여의 변화량을 구체적 수치로 인용하고, 그래프에서 곡선이 어느 방향으로 얼마나 움직였는지 짚으세요.
- 3문단(한계와 확장): 이 분석의 한계를 최소 한 가지 지적하세요. 수요와 공급을 직선으로 가정한 것이 타당한지, 충격의 크기를 파라미터 몇 단위 변화로 정한 근거가 있는지, 조정에 시간이 걸리는 점(조정 과정이나 거미집)을 무시하지 않았는지 등이 후보입니다. 분석을 넓힐 방향도 한 문장 제안하세요.
자기 점검 체크리스트
- 충격을 파라미터 변화(어느 값을 얼마로)로 명확히 표현했는가.
- 충격 전후의 균형을 모두 계산해 수치로 제시했는가.
- 그래프에 원래 곡선과 이동한 곡선, 두 균형점이 함께 있고 제목·축 라벨이 있는가.
- 소비자 잉여와 생산자 잉여의 변화를 전후 비교로 보였는가.
- 3문단 글이 주장-근거-한계 구조를 갖추고, 본문에서 자신의 수치와 그래프를 직접 인용했는가.
# [미니 프로젝트 작업 공간] 시장 충격 보고서
# TODO 1) 시나리오를 하나 고르고, 충격 전/후 파라미터를 정하세요.
# TODO 2) find_equilibrium으로 전후 균형을 계산해 출력하세요.
# TODO 3) 원래 곡선(파선)과 이동한 곡선(실선), 두 균형점을 한 그래프에 그리세요.
# TODO 4) surplus 함수로 전후 소비자/생산자/총잉여를 계산해 나란히 출력하세요.
# TODO 5) 이 셀 아래에 마크다운 셀을 추가해 3문단 보고서를 쓰세요.6.15 요약과 다음 장 예고
이번 장은 흩어져 있던 부품을 하나로 조립하는 장이었습니다. 5장에서 만든 수요·공급 함수를 한 시장에 올려, 두 곡선이 만나는 균형을 종이와 연필의 공식 \(P^* = (a-c)/(b+d)\)로 정확히 풀었고, 공식이 통하지 않을 경우를 대비해 가격 격자를 훑는 수치 탐색도 익혔습니다. 균형을 가격이 세로축인 경제학의 관례대로 그리고, fill_between으로 소비자·생산자 잉여를 색칠해 거래가 만들어 내는 이득의 크기를 눈으로 확인했습니다. 시장에 충격을 줘서 균형이 어느 방향으로 움직이는지 비교정학으로 정리했고, while로는 초과수요가 가격을 균형으로 끌어가는 조정 과정을, 거미집 모형으로는 공급이 한 박자 늦는 시장이 기울기 조건에 따라 수렴하거나 발산하는 두 운명을 시뮬레이션했습니다. 마지막으로 3장에서 판정만 하고 넘어갔던 가격상한을 다시 불러와, 균형보다 낮은 뚜껑이 만드는 부족분을 수량으로 재고 그림에 표시했습니다.
여기까지 우리가 만든 quantity_demanded, quantity_supplied, find_equilibrium, surplus는 이제 한 노트북 안에 잘 작동하는 부품으로 쌓여 있습니다. 그런데 다음 시장을 분석할 때마다 이 함수들을 매번 복사해 붙이는 것은 번거롭습니다. 다음 장에서는 이 부품들을 별도의 파일로 묶어 어디서든 불러 쓰는 모듈과 패키지로 만들고, 노트북 밖의 검은 화면, 즉 터미널에서 파이썬을 직접 실행하는 법을 배웁니다. 우리 시장 분석 코드가 노트북을 벗어나 하나의 도구로 독립하는 첫걸음입니다.