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"] = False11 캡스톤: 간이 연구 프로젝트와 논리적 글쓰기
12 11장 캡스톤: 간이 연구 프로젝트와 논리적 글쓰기
이 책의 마지막 본문 장입니다. 새로 배우는 문법은 하나도 없습니다. 1장부터 10장까지 쌓아 온 도구를 전부 꺼내, 질문 하나를 처음부터 끝까지 밀고 나가는 것이 이 장의 전부입니다. 그리고 그 끝은 코드가 아니라 글입니다.
이번 장에서 배우는 것
- 좋은 연구 질문의 세 가지 조건을 익히고, 넓은 궁금증을 답할 수 있는 크기로 좁히는 절차를 연습합니다.
np.random으로 가상 설문 데이터를 만들고, 구간별 평균으로 수요 곡선을 추정합니다.- 중간점 공식으로 탄력성을 계산하고, 그 숫자를 보고서의 문장으로 옮깁니다.
- 주장-근거-한계 구조로 분석 보고서를 씁니다. 그래프 인용법과 수치 표기 원칙을 포함해서요.
- 상관과 인과의 혼동, 축 조작, 성급한 일반화 — 분석 글을 망치는 세 가지 오류를 스스로 걸러냅니다.
- 자기 평가 루브릭과 동료 피드백으로 프로젝트 한 편을 완성합니다.
12.1 여는 질문: “그럼 이제 배달 끊는다”는 진심일까
시험 기간의 과 단톡방을 상상해 보지요. 배달 앱 공지가 캡처되어 올라옵니다. “플랫폼 수수료 조정에 따라 다음 달부터 배달비가 1,000원 인상됩니다.” 반응은 즉각적입니다. “미쳤네, 이제 배달 끊는다.” “어차피 시킬 사람은 시켜.” 5분 만에 논쟁이 붙었는데, 양쪽 다 근거는 없습니다. 목소리가 큰 쪽이 이기는 싸움이지요.
경제학과 학생이라면 여기서 질문을 바꿀 수 있습니다. 끊는다, 안 끊는다가 아니라 — 얼마나 줄이는가. 그리고 이 책을 여기까지 읽었다면 그 질문에 숫자로 답할 도구도 이미 손에 있습니다. 데이터를 담는 리스트와 데이터프레임(4장, 9장), 반복 계산을 맡기는 for문(4장), 모델을 부품으로 만드는 함수(5장), 민감도를 재는 탄력성(5장), 결과를 보여 주는 그래프(8장). 남은 일은 하나입니다. 이 부품들을 질문 하나에 맞춰 조립하고, 그 결과를 글로 완성하는 것.
그래서 이 마지막 장은 코딩 수업이라기보다 작문 수업에 가깝습니다. 분석은 코드가 끝나는 곳에서 끝나지 않고, 읽는 사람이 설득되는 곳에서 끝납니다. 순서는 이렇습니다. 좋은 질문을 세우는 법(1부), 질문 하나를 끝까지 완주하는 예시(2부), 분석 글쓰기의 도구상자(3부), 여러분이 고를 프로젝트 스케치(4부), 완성물을 점검하는 루브릭(5부).
그래프 준비
이 장에서도 그래프가 네 번 등장합니다. 한글 폰트 설정부터 실행해 둡니다.
# Colab에서 실행 중이라면 이 셀을 실행하세요 (로컬에서는 필요 없습니다)
!pip install koreanize-matplotlib
import koreanize_matplotlibColab은 한글 폰트가 없는 리눅스 환경이라 위 셀로 koreanize-matplotlib을 설치해야 그래프의 한글이 깨지지 않습니다. 설치는 세션당 한 번이면 됩니다.
12.2 1부 좋은 질문 세우기
연구의 절반은 질문에서 결정됩니다. 과장이 아닙니다. 조별 과제에서 “청년 실업에 대해 분석해 오세요”라는 지시를 받아 본 적이 있다면, 그 막막함을 기억할 겁니다. 무엇부터 찾아야 할지, 어디서 멈춰야 할지 모르는 상태. 그 막막함의 정체는 능력 부족이 아니라 질문의 부재입니다. “청년 실업”은 주제이지 질문이 아니거든요. 주제는 방향만 가리키고, 질문은 목적지를 찍어 줍니다. 목적지가 없으면 아무리 좋은 도구를 들고 있어도 출발을 못 합니다.
좋은 연구 질문의 세 가지 조건
이 책에서는 연구 질문을 세 가지 조건으로 검사합니다.
첫째, 답할 수 있어야 합니다. 질문이 측정 가능한 양으로 표현되어야 한다는 뜻입니다. “한국 경제는 좋아질까?”는 무엇이 얼마가 되면 “좋아진” 것인지 정의가 없어서, 어떤 데이터를 들이밀어도 답이 되지 않습니다. 반면 “얼마나 줄까?”라는 질문은 숫자 하나가 답이 됩니다. 답의 형태를 미리 정할 수 있는 질문이 답할 수 있는 질문입니다.
둘째, 재료가 있어야 합니다. 데이터를 구할 수 있거나, 데이터가 없다면 가정을 명시한 모형이라도 세울 수 있어야 합니다. 재료가 전혀 없는 질문은 좋은 질문이라도 이번 학기의 질문은 아닙니다.
셋째, 범위가 좁아야 합니다. 한 학기짜리가 아니라 한두 주 안에 완주할 수 있는 크기여야 합니다. 여기서 많은 학생이 머뭇거립니다. 질문을 좁히면 시시해지는 것 같거든요. 실제로는 반대입니다. 좁은 질문에 확실히 답한 보고서가, 넓은 질문에 흐릿하게 답한 보고서보다 언제나 읽을 가치가 있습니다. 좁은 답은 쌓이지만, 흐릿한 답은 쌓이지 않기 때문입니다.
질문 좁히기: “한국 경제는 좋아질까?”에서 출발
나쁜 질문을 버리는 게 아니라 좁혀서 살리는 과정을 한번 따라가 보겠습니다. 출발점은 누구나 궁금해할 만한, 그러나 세 조건을 전부 탈락하는 질문입니다.
한국 경제는 좋아질까?
“좋아진다”의 측정치가 없으니 답할 수 없고, “한국 경제” 전체를 다룰 데이터는 한 학기에 못 모으고, 범위는 나라 하나입니다. 전부 탈락. 그럼 한 단계씩 조입니다.
1단계 — “좋아진다”를 측정치로 바꿉니다. 내가 정말 궁금한 게 뭘까요? 물가, 취업, 소비… 이 중에서 하나만 고릅니다. 배달 음식을 자주 시키는 학생이라면 소비, 그중에서도 배달 소비가 피부에 와닿겠지요. 질문은 “배달 음식 소비는 어떻게 변할까?”로 좁아집니다. 아직 넓습니다.
2단계 — “누구의” 이야기인지 정합니다. 전 국민의 배달 소비는 내가 관찰할 수 없습니다. 관찰할 수 있는 집단으로 내립니다. 우리 학교 학생. 질문은 “우리 학교 학생의 배달 주문은 어떻게 변할까?”가 됩니다.
3단계 — 무엇이 변할 때의 이야기인지 정합니다. “어떻게 변할까”는 원인이 비어 있는 질문입니다. 여는 질문의 그 공지를 가져옵니다. 배달비 1,000원 인상. 이제 원인 변수가 생겼습니다.
4단계 — 답의 형태를 숫자로 정합니다. “줄어들까?”(예/아니오)보다 “얼마나 줄까?”(월 몇 회)가 훨씬 많은 정보를 담습니다. 최종 질문은 이렇습니다.
배달비가 1,000원 오르면, 우리 학교 학생의 월 배달 주문 횟수는 얼마나 줄까?
세 조건에 다시 대 봅니다. 답할 수 있는가 — 답은 “월 ○회 감소”라는 숫자 하나입니다. 재료가 있는가 — 학생들에게 설문을 돌리면 됩니다(이 장에서는 설문을 시뮬레이션으로 만듭니다). 범위가 좁은가 — 한 학교, 한 품목, 변수 하나. 통과입니다. “한국 경제”에서 출발한 질문이 네 번의 결정을 거쳐 완주 가능한 질문이 되었습니다. 각 단계가 무언가를 버리는 결정이었다는 점에 주목하세요. 질문을 세운다는 것은 무엇을 다루지 않을지 정하는 일이기도 합니다.
# [직접 해보기] 여러분의 질문을 같은 방식으로 좁혀 보세요. 주석으로 채우면 됩니다.
# TODO 원래 질문 (넓어도 됩니다):
# TODO 1단계 - 측정치 하나 고르기:
# TODO 2단계 - 관찰할 수 있는 집단으로 좁히기:
# TODO 3단계 - 원인 변수 정하기:
# TODO 4단계 - 답의 형태를 숫자로:
# TODO 조건 검사 - 답할 수 있다( ) / 재료가 있다( ) / 범위가 좁다( )12.3 2부 완주 예시: 배달비 인상과 주문 수요
방금 세운 질문을 끝까지 밀고 가겠습니다. 계획은 네 단계입니다. ① 데이터를 만들고(설문 시뮬레이션), ② 구간별 평균으로 수요 곡선을 추정하고, ③ 탄력성으로 민감도를 재고, ④ 보고서를 씁니다. 여러분이 자기 프로젝트에서 밟을 단계도 정확히 이 네 개입니다.
시작 전에 한 가지를 분명히 해 두겠습니다. 여기서 쓰는 설문은 가상 데이터입니다. 실제 설문은 시간이 걸리고, 이 장의 목적은 결과가 아니라 절차를 보여 주는 것이니까요. 가상 데이터에는 교육용으로 좋은 성질이 하나 있습니다. 정답을 우리가 알고 시작한다는 점입니다. 데이터를 만들 때 “배달비 1,000원당 주문 1.8회 감소”라는 참 모형을 심어 둘 텐데, 우리의 분석이 그 값을 복원해 내는지가 이 예시의 관전 포인트입니다. 방법이 옳다면 심은 값이 나와야 합니다. 물론 진짜 보고서라면 데이터가 가상이라는 사실을 반드시 밝혀야 하고, 2.4의 완성 보고서에서 실제로 그렇게 합니다.
2.1 설문을 시뮬레이션으로 옮기기
설문 설계는 이렇습니다. 학생 240명을 48명씩 다섯 그룹으로 나누고, 그룹마다 다른 배달비 — 1,000원, 2,000원, 3,000원, 4,000원, 5,000원 — 를 제시한 뒤 “배달비가 이 금액이라면 한 달에 몇 번 주문하시겠습니까?”를 묻습니다. 그룹마다 다른 값을 제시하는 이유는 수요 곡선이 원래 그런 물건이기 때문입니다. 배달비가 3,000원일 때의 주문량, 4,000원일 때의 주문량… 가격마다 하나씩 점을 찍어야 곡선이 그려집니다.
같은 배달비를 받아도 응답은 사람마다 다를 겁니다. 용돈이 다르고, 자취 여부가 다르고, 그날의 식욕이 다르니까요. 이 개인차를 정규분포 노이즈로 표현합니다. 평균은 참 모형이 정하고, 그 주위로 응답이 흩어지는 구조입니다.
import numpy as np
import pandas as pd
rng = np.random.default_rng(410) # 시드 고정: 언제 다시 실행해도 같은 설문 결과
fee_levels = [1000, 2000, 3000, 4000, 5000] # 그룹마다 제시한 배달비(원)
n_per_fee = 48 # 그룹당 응답자 수 -> 모두 240명
records = []
for fee in fee_levels:
mean_orders = 11 - 1.8 * (fee / 1000) # 참 모형: 1,000원당 1.8회 감소
answers = rng.normal(mean_orders, 1.6, n_per_fee) # 개인차: 표준편차 1.6의 흩어짐
answers = np.clip(np.round(answers), 0, None) # 반올림하고, 음수 응답은 0으로
for a in answers:
records.append({"배달비": fee, "주문횟수": int(a)})
survey = pd.DataFrame(records)
print(survey.shape)
survey.head()(240, 2)
| 배달비 | 주문횟수 | |
|---|---|---|
| 0 | 1000 | 9 |
| 1 | 1000 | 9 |
| 2 | 1000 | 11 |
| 3 | 1000 | 7 |
| 4 | 1000 | 10 |
한 줄씩 뜯어보기
np.random.default_rng(410)— 난수 생성기를 만들면서 시드를 410으로 고정합니다. 410이라는 숫자 자체에는 아무 의미가 없습니다. 중요한 것은 숫자를 적어 두었다는 사실이고, 덕분에 누가 언제 실행해도 같은 설문 결과가 나옵니다.fee_levels,n_per_fee— 설문 설계를 변수로 올려 둡니다. 나중에 “그룹을 10개로 늘리면?” 같은 실험을 할 때 이 두 줄만 고치면 됩니다.mean_orders = 11 - 1.8 * (fee / 1000)— 참 모형입니다. 배달비가 0원일 때 월 11회, 1,000원 오를 때마다 1.8회씩 줄어드는 직선. 분석이 끝났을 때 우리가 복원해야 할 목표값이 바로 이 기울기입니다.rng.normal(mean_orders, 1.6, n_per_fee)— 참 평균 주위로 표준편차 1.6만큼 흩어진 응답 48개를 뽑습니다. 개인차를 코드로 옮긴 줄입니다.np.clip(np.round(answers), 0, None)— 주문 횟수는 정수이고 음수일 수 없습니다. 현실의 제약을 데이터에 반영하는 줄입니다.records.append({...})— 응답 한 건을 딕셔너리 하나(표의 한 행)로 만들어 쌓습니다. 5장에서 “딕셔너리는 표의 행”이라고 했던 그 구조 그대로, 마지막 줄에서DataFrame이 됩니다.
실수 노트: 어제는 9.0회였는데 오늘 다시 실행하니 8.9회?
이 실수는 에러 메시지가 없습니다. 그래서 더 위험합니다. np.random.default_rng()를 시드 없이 쓰면 실행할 때마다 다른 난수가 나옵니다. 코드는 멀쩡히 돌아가는데, 보고서에 적어 둔 숫자와 노트북을 다시 실행한 결과가 미묘하게 어긋나기 시작하지요. 발표 날 교수님이 노트북을 실행해 봤는데 보고서와 다른 숫자가 나온다면, 분석 전체의 신뢰가 무너집니다. 처방은 두 가지입니다. 난수를 쓰는 모든 프로젝트에서 시드를 고정할 것, 그리고 보고서에 시드를 명시할 것. 5부 루브릭의 “재현성” 항목이 바로 이것을 검사합니다.
survey.groupby("배달비")["주문횟수"].agg(["count", "mean", "std"]).round(2)| count | mean | std | |
|---|---|---|---|
| 배달비 | |||
| 1000 | 48 | 9.04 | 1.84 |
| 2000 | 48 | 7.94 | 1.86 |
| 3000 | 48 | 5.52 | 1.46 |
| 4000 | 48 | 3.75 | 1.48 |
| 5000 | 48 | 1.83 | 1.74 |
데이터의 첫인상을 확인했습니다. count 열은 다섯 그룹 모두 48명 — 설계대로입니다. mean 열은 배달비가 오를수록 착실히 내려갑니다. std 열은 1.4에서 1.9 사이로, 우리가 심은 개인차(1.6)에서 크게 벗어나지 않습니다. 실제 설문이었다면 이 표가 “데이터가 이상하지 않은지” 확인하는 첫 관문이 됩니다. 그룹 하나만 응답자가 5명이라거나, 표준편차가 유독 크다거나 하면 분석 전에 데이터부터 의심해야 하니까요.
2.2 구간별 평균: 점 다섯 개짜리 수요 곡선
경제학원론의 수요 곡선은 “각 가격에서 사람들이 사려는 양”을 이은 선입니다. 우리 설문에서 그 “각 가격에서의 양”에 해당하는 것이 바로 그룹별 평균 주문 횟수입니다. 개별 응답은 개인차 때문에 들쭉날쭉하지만, 48명을 평균하면 플러스 쪽 개인차와 마이너스 쪽 개인차가 서로 상쇄되면서 참 평균 근처의 값이 남습니다. 표본이 클수록 평균이 참값에 다가간다는 이 성질은 통계학에서 큰 수의 법칙(law of large numbers)이라고 부르는데, 여기서는 “평균은 노이즈를 지운다” 정도로 기억해 두면 충분합니다.
그러니 다섯 그룹의 평균을 구하면, 수요 곡선 위의 점 다섯 개를 추정한 셈이 됩니다.
demand = survey.groupby("배달비")["주문횟수"].mean()
print(demand.round(2))
drops = demand.values[:-1] - demand.values[1:] # 인접 구간의 감소 폭
print("구간별 감소 폭:", drops.round(2))
print("1,000원당 평균 감소:", drops.mean().round(2), "회")배달비
1000 9.04
2000 7.94
3000 5.52
4000 3.75
5000 1.83
Name: 주문횟수, dtype: float64
구간별 감소 폭: [1.1 2.42 1.77 1.92]
1,000원당 평균 감소: 1.8 회
한 줄씩 뜯어보기
survey.groupby("배달비")["주문횟수"].mean()— 9장에서 배운 문법 그대로입니다. 240행짜리 표를 배달비 값별로 접어, 묶음마다 주문횟수의 평균을 냅니다. 결과는 배달비를 인덱스로 갖는 Series — 점 다섯 개짜리 수요 곡선입니다.demand.values[:-1] - demand.values[1:]— 슬라이싱 두 번으로 “한 칸 어긋난” 배열 두 개를 만들어 뺍니다. 앞 배열은 1,000~4,000원의 평균, 뒤 배열은 2,000~5,000원의 평균이니, 빼면 인접 구간의 감소 폭 네 개가 한 번에 나옵니다. 4장에서 시계열의 전월 대비 증감을 구할 때 썼던 발상과 같습니다.drops.mean()— 감소 폭 네 개의 평균. 질문이 요구한 “1,000원당 몇 회”의 답입니다.
결과를 읽어 보지요. 배달비 1,000원 그룹의 평균은 월 9.04회, 5,000원 그룹은 1.83회입니다. 그리고 1,000원당 평균 감소는 1.80회 — 데이터를 만들 때 심어 둔 참 기울기 1.8을 거의 정확히 복원했습니다. 방법이 통했다는 뜻입니다.
다만 구간별 감소 폭은 1.10회부터 2.42회까지 출렁입니다. 참 모형의 기울기는 모든 구간에서 1.8로 같은데도 그렇습니다. 그룹당 48명이라는 표본 크기에서 노이즈가 완전히 지워지지 않은 흔적이고, 뒤에서 성급한 일반화를 이야기할 때 이 출렁임이 다시 등장합니다. 어쨌든 질문의 1차 답은 나왔습니다. “평균적으로 월 1.8회 줄어든다.” 그런데 이 “평균적으로”가 숨기고 있는 것이 있습니다. 같은 1,000원 인상이라도 지금 배달비가 2,000원인 학생과 4,000원인 학생의 반응이 다르다면요? 그걸 재는 도구가 탄력성입니다. 먼저 지금까지의 결과를 그림으로 보겠습니다.
jitter = rng.normal(0, 90, len(survey)) # 점이 겹치지 않게 좌우로 살짝 흔들기
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.scatter(survey["배달비"] + jitter, survey["주문횟수"],
s=14, alpha=0.25, color="#8d99ae", label="개별 응답 240명")
ax.plot(demand.index, demand.values, marker="o", markersize=7,
color="#d1495b", linewidth=2, label="구간별 평균 (추정 수요 곡선)")
ax.set_xlabel("배달비 (원)")
ax.set_ylabel("월 주문 횟수 (회)")
ax.set_title("그림 1. 배달비별 주문 의향: 개별 응답과 구간별 평균")
ax.set_xticks(fee_levels)
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.legend()
plt.show()
그래프가 말하는 문장: 개별 응답은 흩어져 있지만, 평균은 배달비가 오를수록 계단처럼 내려간다.
회색 점(개별 응답)만 보면 무늬가 잘 안 보입니다. 같은 배달비 안에서도 0회부터 두 자릿수까지 응답이 퍼져 있으니까요. 그 흩어짐을 뚫고 빨간 평균선이 또렷하게 우하향합니다. 개인 한 명 한 명에게서는 안 보이던 수요의 법칙이 평균에서 모습을 드러내는 장면입니다. 참고로 jitter는 x축 위 같은 자리에 겹칠 240개의 점을 좌우로 조금씩 흔들어 흩어 놓는 표시용 장치일 뿐, 분석에는 쓰지 않습니다. 이 그림이 잠시 뒤 보고서에서 “그림 1”로 인용됩니다.
2.3 탄력성: 민감도를 숫자 하나로
5장에서 배운 탄력성을 다시 부릅니다. 가격이 1% 변할 때 수요량이 몇 % 변하는가 — 단위가 없어서 어떤 시장끼리도 비교할 수 있는 민감도 지표였지요. 방향에 따라 변화율이 달라지는 문제를 피하려고 변화 전후의 중간값을 분모로 쓰는 중간점 공식(midpoint formula)도 그때 나왔습니다.
\[ \varepsilon = \frac{(Q_2 - Q_1)\,/\,\bar{Q}}{(P_2 - P_1)\,/\,\bar{P}}, \qquad \bar{Q} = \frac{Q_1 + Q_2}{2},\quad \bar{P} = \frac{P_1 + P_2}{2} \]
한 가지를 정직하게 밝혀 둘 필요가 있습니다. 여기서 \(P\)는 음식값 전체가 아니라 배달비입니다. 그러니 지금 계산하는 것은 정확히 말해 “주문의 배달비 탄력성”이지, 교과서의 가격 탄력성 그 자체는 아닙니다. 이런 구분을 보고서에 명시하는 것 자체가 좋은 글쓰기 훈련입니다. 점 다섯 개 사이의 네 구간 각각에 대해 계산해 보겠습니다.
rows = []
for i in range(len(fee_levels) - 1):
p1, p2 = fee_levels[i], fee_levels[i + 1]
q1, q2 = demand[p1], demand[p2]
pct_q = (q2 - q1) / ((q1 + q2) / 2) # 주문량 변화율 (중간점 기준)
pct_p = (p2 - p1) / ((p1 + p2) / 2) # 배달비 변화율 (중간점 기준)
rows.append({"구간": f"{p1}~{p2}원", "탄력성": round(pct_q / pct_p, 2)})
elastic = pd.DataFrame(rows)
elastic| 구간 | 탄력성 | |
|---|---|---|
| 0 | 1000~2000원 | -0.20 |
| 1 | 2000~3000원 | -0.90 |
| 2 | 3000~4000원 | -1.34 |
| 3 | 4000~5000원 | -3.09 |
한 줄씩 뜯어보기
range(len(fee_levels) - 1)— 점이 5개면 이웃한 점 사이의 구간은 4개입니다. 그래서 반복 횟수가 하나 적습니다. 이 “하나 적음”을 잊으면 바로 아래 실수 노트의 에러를 만나게 됩니다.demand[p1]—groupby가 만든 Series는 배달비가 인덱스라서, 대괄호에 배달비를 넣으면 그 그룹의 평균이 나옵니다. 딕셔너리에서 키로 값을 꺼내던 5장의 감각과 같습니다.pct_q,pct_p— 분자와 분모를 각각 중간값으로 나눈 변화율입니다. 두 변화율의 비가 탄력성이고,round(..., 2)로 소수 둘째 자리까지만 남깁니다.rows.append({...})— 구간 하나의 결과를 딕셔너리 한 행으로 쌓아 마지막에 표로 만듭니다. 데이터를 만들 때와 같은 패턴이 분석 결과 정리에도 그대로 쓰이는 셈입니다.
표를 읽습니다. 1,000~2,000원 구간의 탄력성은 -0.20 — 배달비가 1% 오를 때 주문은 0.2%만 줄어드는 비탄력 구간입니다. 배달비가 쌀 때의 1,000원 인상은 불평은 낳아도 주문은 크게 줄이지 못합니다. 그런데 구간이 오른쪽으로 갈수록 탄력성의 크기가 커져서, 4,000~5,000원 구간에서는 -3.09까지 뜁니다. 배달비가 1% 오르면 주문이 3% 넘게 줄어드는, 매우 탄력적인 구간입니다. 크기 1을 넘어서는 경계는 3,000원 부근입니다(2,000~3,000원 구간 -0.90, 3,000~4,000원 구간 -1.34). 경제학 문장으로 옮기면 이렇습니다. 배달비가 이미 높은 소비자일수록 추가 인상에 민감하다. 같은 1,000원짜리 정책이라도 어느 구간에서 시행되느냐에 따라 효과가 완전히 다르다는 것 — 평균 1.8회라는 답 하나만 봤다면 놓쳤을 이야기입니다.
실수 노트: ZeroDivisionError — 무료 배달 그룹을 넣는 순간
설문에 “배달비 0원” 그룹을 추가하고, 중간점 공식 대신 단순 변화율 (p2 - p1) / p1로 계산을 바꿨다고 합시다. p1 = 0인 구간에서 파이썬은 이렇게 멈춥니다.
ZeroDivisionError: division by zero
에러를 만나면 트레이스백의 마지막 줄부터 읽는 습관을 들이세요. 에러의 이름(무엇이 잘못됐나)이 거기 있고, 바로 위에 화살표가 가리키는 줄(어디서 잘못됐나)이 있습니다. 이 경우 원인은 코드가 아니라 수식 선택입니다. 기준값 \(P_1\)이 0이면 변화율이 정의되지 않으니까요. 중간점 공식은 분모가 두 값의 평균(0원과 1,000원이면 500원)이라 같은 상황에서도 멈추지 않습니다. 공식 하나를 고르는 일에도 이런 실무적인 이유가 숨어 있습니다. 에러 읽기의 전체 요령은 부록 B에 있습니다.
실수 노트: 점은 5개인데 구간은 4개 — 길이가 안 맞는 그래프
탄력성을 그래프로 그리면서 x축에 fee_levels를 그대로 넣으면 이렇게 됩니다.
ValueError: x and y must have the same first dimension,
but have shapes (5,) and (4,)
메시지의 shapes (5,) and (4,)가 결정적인 힌트입니다. x로 준 배열은 길이 5, y로 준 배열은 길이 4라서 짝을 지을 수 없다는 뜻이지요. 근본 원인은 개념에 있습니다. 탄력성은 점 하나의 성질이 아니라 구간의 성질이라서, 점 5개에서 구간 4개가 나옵니다. 해결은 x축에 배달비가 아니라 구간 라벨(“1000~2000원” 같은 문자열 4개)을 쓰는 것. 바로 아래에서 그렇게 그립니다.
colors = ["#d1495b" if abs(e) > 1 else "#8d99ae" for e in elastic["탄력성"]]
fig, ax = plt.subplots(figsize=(8, 4.2))
ax.bar(elastic["구간"], elastic["탄력성"].abs(), color=colors, width=0.6)
ax.axhline(1, color="black", linewidth=1, linestyle="--")
ax.text(0.02, 1.08, "크기 1: 여기부터 탄력적",
transform=ax.get_yaxis_transform(), fontsize=10)
ax.set_xlabel("배달비 구간")
ax.set_ylabel("탄력성의 크기 (절댓값)")
ax.set_title("그림 2. 구간별 배달비 탄력성 (중간점 공식)")
ax.grid(alpha=0.3, axis="y")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.show()
그래프가 말하는 문장: 같은 1,000원 인상이라도, 배달비가 이미 높은 구간에서는 주문 이탈이 훨씬 크다.
막대 색은 정보를 담고 있습니다. 회색은 크기가 1보다 작은 비탄력 구간, 빨간색은 1을 넘는 탄력 구간입니다. 점선(크기 1)을 어느 막대가 넘는지가 이 그림의 핵심이라 보조선으로 박아 두었습니다. 리스트 컴프리헨션으로 조건에 따라 색을 갈라 넣는 colors 줄은 6장에서 배운 문법의 재활용입니다. 이 그림이 보고서의 “그림 2”가 됩니다.
2.4 완성 보고서 전문
계산은 끝났지만, 이 결과를 아는 사람은 아직 노트북을 실행한 나 하나입니다. 분석이 일이 되려면 읽는 사람에게 건너가야 합니다. 아래는 이 프로젝트의 완성 보고서 전문입니다. 700자 안팎, 서론-분석-결론의 3부 구성이고, 여러분이 쓸 보고서의 견본이기도 합니다. 먼저 통째로 읽어 보세요. 어느 문장이 어느 코드에서 왔는지는 바로 다음 절에서 해부합니다.
배달비 인상은 주문을 얼마나 줄이는가: 교내 설문 시뮬레이션 분석
배달 플랫폼의 배달비 인상 공지가 뜰 때마다 “이제 끊겠다”는 반응이 쏟아지지만, 실제로 주문이 얼마나 줄어드는지는 감이 아니라 수치로 답해야 할 질문이다. 이 보고서는 “배달비가 1,000원 오르면 우리 학교 학생의 월 배달 주문 횟수는 얼마나 줄까”라는 질문에 답하기 위해, 배달비를 1,000원부터 5,000원까지 다섯 수준으로 나누어 제시한 가상 설문(응답자 240명, 시뮬레이션 데이터)을 분석했다.
그림 1에서 보듯 제시된 배달비가 높은 집단일수록 월 평균 주문 의향이 낮았다. 배달비 1,000원 집단의 평균은 월 9.0회였지만 5,000원 집단은 1.8회로, 네 구간에 걸쳐 모두 7.2회(약 80%)가 줄었다. 배달비 1,000원 인상당 평균 1.8회씩 감소한 셈이다. 다만 감소의 속도는 구간마다 달랐다. 그림 2의 중간점 공식 탄력성을 보면 1,000~2,000원 구간은 -0.20으로 비탄력적이었으나 4,000~5,000원 구간은 -3.09에 달했다. 배달비가 이미 높은 구간에서는 같은 1,000원 인상이 훨씬 큰 주문 이탈로 이어진다는 뜻이다.
요약하면, 설문 기준으로 배달비 1,000원 인상은 학생 한 명당 월 약 1.8회의 주문 감소로 이어지며 그 충격은 현재 배달비가 높을수록 크다. 다만 이 결과는 세 가지 한계 안에서 읽어야 한다. 첫째, 데이터가 실제 주문 기록이 아니라 주문 “의향”을 물은 가상 설문이어서 실제 행동과 다를 수 있다. 둘째, 표본이 한 학교 학생에 국한되어 전체 소비자로 일반화하기 어렵다. 셋째, 음식 가격이나 시험 기간 같은 다른 요인을 통제하지 않았다. 실제 주문 데이터로 같은 분석을 반복하는 것이 다음 단계다. (분석 코드와 난수 시드 410은 첨부 노트북에 있다.)
2.5 문단 바이 그래프: 글과 코드의 대응
코드를 한 줄씩 뜯어봤듯이, 이번에는 보고서를 문단 단위로 뜯어봅니다. 좋은 분석 보고서의 문단들은 각자 담당하는 코드 구역이 있습니다.
| 보고서 문단 | 대응하는 코드·그림 | 그대로 건너간 숫자 |
|---|---|---|
| 서론 (질문과 데이터 소개) | 1부의 질문 좁히기, 2.1의 설문 설계 셀 | 1,000원, 다섯 수준, 240명 |
| 분석 앞부분 (수요 곡선) | 그림 1, 2.2의 demand 출력 |
9.0회, 1.8회, 7.2회, 약 80% |
| 분석 뒷부분 (탄력성) | 그림 2, 2.3의 elastic 표 |
-0.20, -3.09 |
| 결론 (요약과 한계) | 새 계산 없음 | 새 숫자 없음 |
서론에는 그림이 없습니다. 서론의 일은 숫자를 보여 주는 것이 아니라 질문을 세우는 것이니까요. 분석 문단은 정확히 “한 문단에 그림 하나”의 리듬으로 움직입니다. 앞 문단은 그림 1을 인용하며 수요 곡선의 숫자를 말하고, 뒤 문단은 그림 2를 인용하며 탄력성의 숫자를 말합니다. 코드를 라인 바이 라인으로 쌓았듯, 보고서는 문단 바이 그래프로 쌓는 셈입니다.
표의 마지막 행이 이 절에서 가장 중요한 규칙입니다. 결론 문단은 새 숫자를 만들지 않습니다. 결론에서 처음 등장하는 수치는 근거를 거치지 않은 주장이라서, 읽는 사람이 확인할 방법이 없습니다. 결론의 일은 분석 문단에 이미 나온 숫자를 요약하고, 그 숫자의 유효 범위(한계)를 긋는 것까지입니다.
숫자의 모양도 눈여겨보세요. 코드 출력은 9.04였는데 보고서는 9.0회라고 적었습니다. 소수 둘째 자리는 “얼마나 줄까”라는 질문의 답에 아무 기여를 하지 않으니 반올림한 것입니다. 대신 원자료는 노트북에 그대로 있으니, 더 정밀한 값이 필요한 독자는 언제든 확인할 수 있습니다. 보고서의 모든 숫자에 출처 셀이 있다는 것 — 재현 가능한 분석이란 결국 이 상태를 말합니다.
# [직접 해보기] 보고서 검산: "약 80%가 줄었다"는 문장을 코드로 재확인해 보세요.
# TODO demand[1000] 과 demand[5000] 을 이용해 감소율(%)을 계산하세요.
# TODO 계산 결과를 소수 첫째 자리로 반올림하면 보고서의 "약 80%"와 맞는지 확인하세요.12.4 3부 논리적 글쓰기 도구상자
2부에서 보고서 한 편을 통째로 보았으니, 이제 부품 단위로 내려갑니다. 문단은 어떻게 짜는지, 그래프는 글에서 어떻게 부르는지, 숫자는 어떻게 적는지 — 그리고 분석 글을 조용히 무너뜨리는 오류 세 가지를 나쁜 예와 좋은 예의 쌍으로 살펴봅니다. 이 절의 내용은 파이썬과 무관해 보이지만, 5부 루브릭의 “글쓰기” 항목이 정확히 여기서 나옵니다.
3.1 문단의 뼈대: 주장-근거-한계
분석 보고서의 문단 하나는 세 부품으로 짜입니다. 문단이 말하려는 것 한 문장(주장), 그 문장을 떠받치는 수치와 그림(근거), 그리고 그 주장이 유효한 조건(한계). 2부 보고서의 분석 뒷문단을 뼈대만 남기면 이렇게 보입니다.
- 주장: “감소의 속도는 구간마다 달랐다.”
- 근거: 그림 2, 탄력성 -0.20에서 -3.09까지.
- 한계(조건): 어디까지나 설문 “의향” 기준 — 결론 문단에서 명시.
무너지는 방식은 두 가지입니다. 근거 없이 주장만 잇달아 놓으면 감상문이 됩니다(“배달비 인상은 심각한 문제다. 학생들의 부담이 크다.”). 주장 없이 숫자만 나열하면 출력 로그가 됩니다(“평균은 9.04였다. 탄력성은 -0.20이었다. 표준편차는 1.84였다.”). 문단을 다 쓴 뒤 “이 문단의 주장 한 문장은 무엇인가, 그 근거는 어느 그림인가”를 자문해서 둘 중 하나라도 답이 없으면 그 문단은 다시 써야 합니다.
3.2 그래프를 글에서 인용하는 법
그래프는 스스로 말하지 않습니다. 글이 불러 줘야 말합니다. 규칙은 네 개면 충분합니다.
첫째, 모든 그림에 번호와 제목을 붙입니다. “그림 1. 배달비별 주문 의향”처럼 번호가 있어야 글에서 지목할 수 있습니다. 둘째, 본문에서 반드시 번호로 부릅니다. “그림 1에서 보듯 …” 또는 문장 끝의 “(그림 2)”. 나쁜 예가 바로 “아래 그래프를 보면 알 수 있다”입니다. 아래가 어디인지, 무엇을 알 수 있는지 아무것도 지목하지 않으니까요. 좋은 예는 “그림 1에서 보듯 배달비 1,000원 집단의 평균 주문은 월 9.0회다”처럼 번호와 읽어 낼 내용을 함께 주는 문장입니다. 셋째, 그림마다 “그래프가 말하는 문장”을 한 줄 답니다. 이 장의 그림들에 계속 달아 온 굵은 한 줄이 그것입니다. 넷째, 본문이 한 번도 부르지 않는 그림은 뺍니다. 인용되지 않는 그림은 장식이고, 장식은 분석 보고서의 신뢰를 깎습니다.
3.3 수치 표기 원칙: 단위, 자릿수, 수준과 비율
숫자를 적는 방식에도 문법이 있습니다. 세 가지만 지키면 보고서의 인상이 달라집니다.
단위 없는 숫자는 정보가 아닙니다. “주문이 1.8 줄었다”는 1.8회인지 1.8%인지 1.8만 원인지 알 수 없습니다. “월 1.8회 줄었다”라고 적습니다.
자릿수는 주장에 필요한 만큼만 남깁니다. 파이썬 출력을 그대로 복사하면 “평균 9.041666…회” 같은 문장이 나오는데, 소수 여섯째 자리는 아무 주장도 거들지 않습니다. f-string의 f"{value:.1f}" 포맷(2장)으로 한 자리면 충분합니다. 반올림은 본문에서 하고, 원값은 노트북에 남깁니다.
변화는 수준과 비율을 함께 적습니다. “80% 줄었다”만으로는 9회에서 1.8회가 된 것인지 0.5회에서 0.1회가 된 것인지 알 수 없습니다. “월 9.0회에서 1.8회로(약 80%) 줄었다”처럼 두 정보를 같이 주면 오해의 여지가 사라집니다.
3.4 흔한 오류 ①: 상관은 인과가 아니다
여름 한 철의 일별 데이터를 모으면 아이스크림 판매량과 물놀이 사고 건수가 나란히 움직입니다. 상관계수도 높게 나옵니다. 그렇다면 아이스크림 판매를 금지하면 사고가 줄어들까요? 당연히 아니지요. 더운 날 사람들이 아이스크림도 사 먹고 물가에도 놀러 가는 것뿐입니다. 기온이라는 공통 원인이 두 변수를 동시에 밀어 올리는 구조 — 이럴 때 두 변수는 서로 아무 인과가 없어도 강한 상관을 보입니다.
말로만 들으면 당연한데, 자기 데이터 앞에서는 다들 이 함정에 빠집니다. 그러니 함정을 직접 만들어 보는 것이 최고의 예방입니다. 아래 코드는 아이스크림과 사고 사이에 어떤 인과도 넣지 않고, 둘 다 기온의 함수로만 만든 가상 데이터입니다.
sim = np.random.default_rng(7)
temp = sim.uniform(18, 34, 60) # 60일간의 낮 최고기온(도)
ice = 20 * temp + sim.normal(0, 40, 60) # 아이스크림 판매지수: 기온의 함수
accident = 0.35 * temp + sim.normal(0, 1.2, 60) # 물놀이 사고(건): 역시 기온의 함수
accident = np.clip(accident, 0, None)
print("아이스크림-사고 상관계수:", np.corrcoef(ice, accident)[0, 1].round(2))
fig, ax = plt.subplots(figsize=(8, 4.2))
sc = ax.scatter(ice, accident, c=temp, cmap="coolwarm", s=30)
fig.colorbar(sc, label="낮 최고기온 (도)")
ax.set_xlabel("아이스크림 판매지수")
ax.set_ylabel("물놀이 사고 (건)")
ax.set_title("그림 3. 상관은 있지만 인과는 없다")
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.show()아이스크림-사고 상관계수: 0.81

상관계수 0.81. 인과를 한 줄도 넣지 않았는데 이 정도 상관이 나옵니다. 점의 색이 진범을 보여 줍니다. 오른쪽 위(아이스크림도 사고도 많은 날)는 죄다 붉은색 — 더운 날입니다. 왼쪽 아래는 푸른색, 서늘한 날이고요. 두 변수를 잇는 선이 아니라, 둘 뒤에 서 있는 세 번째 변수가 무늬를 만든 것입니다.
- 나쁜 예: “아이스크림 판매와 물놀이 사고의 상관계수는 0.81이다. 아이스크림 판매를 규제하면 사고를 줄일 수 있다.”
- 좋은 예: “두 변수의 상관계수는 0.81로 높지만, 더운 날 둘 다 늘어나는 공통 원인(기온)의 결과일 가능성이 크다. 이 데이터만으로 인과를 말할 수는 없다.”
우리 배달비 프로젝트는 어떨까요? 관찰 데이터였다면 같은 걱정을 해야 합니다. 배달비가 높은 동네가 마침 소득도 낮다면, 주문 감소가 배달비 탓인지 소득 탓인지 데이터만으로는 가를 수 없으니까요. 다행히 2부의 설문은 그룹에 배달비를 우리가 무작위로 제시했습니다. 원인 변수를 연구자가 통제하는 실험 설계에서는 인과 해석의 여지가 훨씬 넓어집니다. 여러분의 프로젝트가 관찰 데이터를 쓴다면, 한계 문단에서 이 구분을 반드시 언급하세요.
3.5 흔한 오류 ②: 축 조작 — 같은 데이터, 다른 인상
거짓말은 숫자가 아니라 축에서 시작되기도 합니다. 학교 편의점 도시락의 월평균 가격이 여섯 달 동안 4,100원에서 4,250원으로 올랐다고 합시다. 상승률로는 약 3.7%입니다. 이 똑같은 데이터를 y축만 다르게 해서 두 번 그려 보겠습니다.
months = ["1월", "2월", "3월", "4월", "5월", "6월"]
lunch = [4100, 4130, 4150, 4180, 4220, 4250] # 편의점 도시락 평균 가격(원)
fig, axes = plt.subplots(1, 2, figsize=(9, 3.8))
for ax in axes:
ax.plot(months, lunch, marker="o", color="#2b6cb0")
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
axes[0].set_ylim(0, 5000)
axes[0].set_title("(a) y축이 0부터: 완만한 상승")
axes[0].set_ylabel("가격 (원)")
axes[1].set_ylim(4080, 4270)
axes[1].set_title("(b) y축을 자르면: 폭등처럼")
fig.suptitle("그림 4. 같은 데이터, 다른 인상 - 도시락 가격 6개월")
plt.tight_layout(rect=(0, 0, 1, 0.94))
plt.show()
왼쪽 (a)는 y축이 0원부터 시작합니다. 3.7% 상승이 눈에도 3.7%처럼 — 거의 평평하게 — 보입니다. 오른쪽 (b)는 y축을 4,080~4,270원으로 잘랐습니다. 선이 그래프를 대각선으로 가로지르며 치솟습니다. 데이터는 한 글자도 다르지 않은데 인상은 “안정”과 “폭등”으로 갈립니다.
- 나쁜 예: (b)만 보여 주며 제목을 “도시락 가격 폭등”이라고 붙이고, 축 범위는 언급하지 않는다.
- 좋은 예: (b)를 쓰되 “y축은 4,080~4,270원 구간을 확대한 것”이라고 밝히고, 본문에 “6개월간 150원(3.7%) 상승”이라는 수치를 함께 적는다.
축을 자르는 것 자체가 죄는 아닙니다. 기준금리 0.25%포인트 변화처럼 미세한 움직임이 관심사일 때는 잘라야 보입니다. 죄는 잘랐다는 사실을 숨기는 것, 그리고 커진 인상을 수치 없이 그대로 결론에 쓰는 것입니다. 남의 그래프를 읽을 때도 순서는 같습니다. 제목보다 y축 눈금부터 확인하세요.
3.6 흔한 오류 ③: 성급한 일반화 — 표본이 말할 수 있는 범위
2부의 설문은 우리 학교 학생 240명이었습니다. 이 결과를 어디까지 넓혀 말할 수 있을까요?
- 나쁜 예: “우리 학교 240명 설문 결과, 한국 소비자는 배달비 1,000원 인상에 월 1.8회 주문을 줄인다.”
- 좋은 예: “우리 학교 학생 240명 표본에서는 월 1.8회 줄었다. 소득과 소비 패턴이 다른 집단에서는 결과가 다를 수 있다.”
대학생은 전체 소비자보다 소득이 낮고 배달 의존도는 높은 집단입니다. 그렇다면 배달비 인상에 더 민감하게 — 즉 더 탄력적으로 — 반응했을 가능성이 있지요. 표본의 특성이 결과를 어느 방향으로 끌었을지 한 문장 추론해 덧붙이는 것이 우수한 보고서의 표지입니다. “다를 수 있다”에서 멈추지 않고 “아마 이쪽으로 다를 것”까지 가는 것이니까요.
표본의 크기도 같은 문제를 만듭니다. 2.2에서 구간별 감소 폭이 1.10회에서 2.42회까지 출렁였던 것을 기억하세요. 참 기울기는 모든 구간에서 1.8로 같은데도, 그룹당 48명으로는 노이즈가 다 지워지지 않았던 겁니다. 표본이 작을수록 우연이 무늬 행세를 합니다. 20명짜리 설문으로 프로젝트를 한다면 이 사실을 한계 문단에 적는 것이 정직한 글쓰기입니다.
12.5 4부 여러분의 프로젝트: 스케치 두 개
완주 예시를 봤으니 이제 여러분 차례입니다. 아래 두 스케치 중 하나를 골라 완주하는 것이 이 장의, 그리고 이 책의 마지막 과제입니다. 스케치에는 절차만 있고 코드는 없습니다. 코드는 여러분의 몫입니다. 하나는 모형으로 답하는 프로젝트, 하나는 데이터로 답하는 프로젝트라서, 어느 쪽을 골라도 2부의 네 단계(질문 → 재료 → 분석 → 보고서)를 그대로 밟게 됩니다.
스케치 A. 최저임금 인상과 편의점 알바 시장
편의점 알바 시급은 대체로 최저임금에 붙어 움직입니다. 최저임금이 오르면 알바생의 시급은 오르지만, 사장님 입장에서는 야간 알바를 줄이고 본인이 카운터에 서는 선택지가 생깁니다. 그렇다면 —
최저임금이 시간당 1,000원 오르면, 편의점 알바 시장의 고용 시간은 얼마나 줄까?
실제 고용 데이터는 구하기 어려우니, 이 프로젝트는 재료의 두 번째 갈래 — 가정을 명시한 모형 — 로 갑니다. 절차는 여섯 단계입니다.
- 노동 수요 함수(시급이 오르면 사장님이 원하는 고용 시간은 감소)와 노동 공급 함수(시급이 오르면 일하려는 학생의 시간은 증가)를 5장 방식으로 정의합니다. 기울기와 절편은 스스로 가정하되, 가정한 값을 보고서에 명시합니다.
np.linspace로 시급 구간을 만들고, 6장의 수치 탐색(수요량과 공급량의 격차가 가장 작은 지점 찾기)으로 균형 시급과 균형 고용 시간을 구합니다.- 균형보다 높은 최저임금을 도입합니다. 그 시급에서의 수요량과 공급량을 계산하면, 둘의 차이가 초과 공급 — 일하고 싶지만 자리를 못 구하는 시간 — 입니다.
- 최저임금 수준을 여러 개로 바꿔 가며(4장
for) 고용량과 초과 공급을 표로 정리합니다. - 그래프: 수요·공급 곡선 위에 최저임금 수평선을 얹고, 고용량과 초과 공급 구간을
annotate와fill_between(8장)으로 표시합니다. - 보고서: 이 결과는 전적으로 가정한 기울기에 달려 있습니다. 기울기를 바꿔 결과가 얼마나 흔들리는지(민감도)를 확인하고 한계 문단에 적습니다.
주로 다시 볼 장은 5, 6, 8장. 결과물은 그래프 1~2개, 표 1개, 보고서 한 편입니다.
# [직접 해보기] 스케치 A 시작 틀
# TODO 1) labor_demand(wage), labor_supply(wage) 함수를 정의하세요.
# (기울기와 절편은 여러분의 가정 - 보고서에 명시할 것)
# TODO 2) np.linspace로 시급 구간을 만들고 균형 시급을 수치 탐색으로 찾으세요.
# TODO 3) 균형보다 높은 최저임금을 넣어 고용량과 초과 공급을 계산하세요.스케치 B. 이콘 카페 한 학기 장부 분석
9장에서 다뤘던 이콘 카페의 판매 기록 data/cafe_sales.csv를 다시 꺼냅니다. 90일 동안 다섯 메뉴, 모두 450행. 열은 날짜(date), 요일(weekday), 날씨(weather), 메뉴(menu), 가격(price), 판매량(quantity)입니다. 이번에는 연습이 아니라 사장님에게 제출할 보고서를 쓴다고 생각하세요. 질문 후보는 세 개입니다. 하나만 골라 1부의 방식으로 좁히세요.
- 비 오는 날 매출은 정말 줄어들까? 줄어든다면 얼마나?
- 요일별로 잘 팔리는 메뉴가 다를까? 다르다면 발주를 요일별로 바꿔야 할까?
- 딸기스무디는 지금처럼 계속 팔아야 할까, 발주를 줄여야 할까?
절차는 여섯 단계입니다.
pd.read_csv("data/cafe_sales.csv")로 읽고head·info로 구조를 확인합니다(9장).- 매출액 열을 만듭니다: 가격 곱하기 판매량.
- 질문에 맞는
groupby를 설계합니다. 날씨별 평균 매출, 요일별·메뉴별 판매량 등. - 불리언 필터로 파고듭니다. 예컨대 특정 메뉴만 골라 날씨별로 비교.
- 그래프 2개: 예를 들어 날씨별 평균 매출 막대그래프와, 관심 메뉴의 일별 판매 추이 선그래프(8장 스타일 적용).
- 보고서: 한 학기(봄) 데이터라는 한계 — 여름의 스무디 판매는 전혀 다를 수 있습니다 — 와, 날씨·요일이 얽혀 생기는 상관·인과 문제(3.4)를 한계 문단에 적습니다.
주로 다시 볼 장은 8, 9장. 결과물은 그래프 2개, 사장님이 실행할 수 있는 제안 1개, 보고서 한 편입니다.
# [직접 해보기] 스케치 B 시작 틀
# TODO 1) pandas로 data/cafe_sales.csv 를 읽고 head()와 info()로 구조를 확인하세요.
# TODO 2) 매출액 열(가격 x 판매량)을 만드세요.
# TODO 3) 고른 질문에 맞는 groupby를 설계하고 결과를 표로 확인하세요.두 스케치가 모두 마음에 들지 않는다면 자기 질문으로 가도 됩니다. 조건은 하나, 1부의 세 가지 조건을 통과해야 한다는 것. 재료를 구하는 길은 세 갈래입니다. 첫째, 2부처럼 가정을 심어 직접 만들기 — 시드와 가정을 반드시 명시합니다. 둘째, 이 책의 data/ 폴더 — 카페 판매 외에도 한국 거시 지표(korea_macro.csv), 소득 10분위(income_deciles.csv)가 있습니다. 셋째, 진짜 설문 — 구글 폼으로 20명만 받아도 프로젝트 재료로는 충분합니다. 단, 표본 20명이 말할 수 있는 범위를 3.6처럼 한계에 적어야 합니다. 어느 길이든 지도 원칙은 같습니다. 데이터가 없어서 질문을 포기하지 말고, 질문을 데이터의 크기에 맞게 줄이세요.
12.6 5부 점검: 스스로, 그리고 서로
자기 평가 루브릭
다 쓴 보고서는 제출 전에 세 번 읽되, 매번 다른 안경을 쓰고 읽습니다. 아래 루브릭의 다섯 항목이 그 안경입니다. 자기 프로젝트가 각 행의 어느 칸에 있는지 표시해 보고, 1수준 칸이 하나라도 있으면 그것부터 고칩니다.
| 항목 | 1수준: 다시 손보기 | 2수준: 기본 충족 | 3수준: 한 걸음 더 |
|---|---|---|---|
| 질문 | 범위가 넓고 측정 대상이 불분명하다 (“~는 좋아질까?”) | 세 조건을 통과한 질문이 한 문장으로 서 있다 | 질문에 예상 답(가설)과 그 이유까지 붙어 있다 |
| 분석 | 코드가 끝까지 실행되지 않거나, 계산 결과가 본문에 없다 | 코드가 끝까지 실행되고 핵심 수치를 2개 이상 계산했다 | 가정을 바꾼 민감도 확인을 1개 이상 수행했다 |
| 시각화 | 그래프가 없거나 축 라벨·단위가 빠져 있다 | 축 라벨·단위·제목을 갖춘 그래프가 1개 이상 있다 | 그림마다 “말하는 문장” 캡션이 있고 본문이 번호로 인용한다 |
| 글쓰기 | 주장 없이 코드 설명이나 숫자만 나열했다 | 서론-분석-결론 구조에 주장-근거-한계가 갖춰졌다 | 반대 해석 가능성을 스스로 제기하고 답했다 |
| 재현성 | 다시 실행하면 다른 숫자가 나온다 | 시드가 고정되고, 처음부터 끝까지 한 번에 실행된다 | 데이터 출처·생성 방법·실행 순서 안내까지 적혀 있다 |
제출 체크리스트
제출 직전, 기계적으로 확인하는 목록입니다. 루브릭이 “얼마나 좋은가”를 묻는다면 체크리스트는 “빠진 게 없는가”를 묻습니다.
동료 피드백 가이드
보고서를 친구와 바꿔 읽는다면, 다음 세 가지만 해 줘도 서로에게 꽤 좋은 심사자가 됩니다.
첫째, 질문 되말하기. 보고서만 읽고 “이 보고서의 질문은 ~이고, 답은 ~이다”를 한 문장으로 저자에게 되말해 줍니다. 저자의 의도와 다르게 읽혔다면 바로 그 지점에서 글이 새고 있는 것입니다. 어느 문단에서 길을 잃었는지 알려 주세요.
둘째, 숫자 대조. 본문에 나온 수치를 하나하나 그림·표와 대조합니다. 본문은 1.8회인데 표는 1.83회라면, 반올림 규칙이 있는지 물어봐 주세요. 틀린 숫자를 찾아 주는 것이 동료가 해 줄 수 있는 가장 고마운 일입니다.
셋째, 한계 하나 더. 저자가 쓰지 않은 한계를 하나 제안합니다. 이때 평가(“이건 일반화가 심한데”)가 아니라 질문(“자취생과 기숙사생이 다르게 반응할 가능성은 없을까?”)의 어투를 쓰면, 같은 내용도 훨씬 잘 전달됩니다.
12.7 연습문제
이 장에는 손풀기-응용-도전 문제 세트가 없습니다. 4부의 스케치 하나(또는 1부의 조건을 통과한 여러분의 질문)를 완주하는 것 — 그것이 이 장의 연습문제이자 이 책의 기말 과제입니다. 앞선 장들의 연습문제가 문법 근육을 만드는 반복 훈련이었다면, 이번에는 실전 한 판인 셈입니다.
같은 이유로 부록 C에도 이 장의 “풀이”는 없습니다. 대신 2부의 예시 프로젝트를 다르게 설계하는 대안 아이디어 — 설문 대신 주문 기록 형태로 데이터를 만드는 법, 구간별 탄력성 대신 직선을 맞춰 기울기 하나로 답하는 법, 그림을 다르게 구성하는 법 — 를 적어 두었습니다. 프로젝트가 막힐 때 힌트로 쓰세요.
12.8 미니 프로젝트: 생각을 코드로, 코드를 글로
장마다 이어 온 이 절이 이번에는 장 전체와 하나로 합쳐집니다. 이전 장들의 미니 프로젝트가 30~60분짜리 연습이었다면, 이번 과제는 같은 형식의 최종판입니다. 한 번에 몰아서 하지 말고 세 토막으로 나누세요. 질문 다듬기와 재료 준비에 30분, 분석과 그래프에 60분, 글쓰기와 퇴고에 30분.
할 일 — 4부의 스케치 A 또는 B를 고르거나, 1부의 세 조건을 통과하는 자기 질문으로 간이 연구 프로젝트 한 편을 완주합니다.
결과물
- 커널 재시작 후 처음부터 끝까지 한 번에 실행되는 노트북 1개
- 번호·제목·캡션을 갖춘 그래프 또는 표 1개 이상 (권장 2개)
- 600~1,000자 보고서 (노트북 마지막 마크다운 셀에)
글쓰기 틀 (3문단)
- 1문단 — 주장: “이 보고서는 ~라는 질문에 답한다. 분석 결과, ~였다.” 질문과 핵심 답을 첫 문단에서 미리 보여 줍니다.
- 2문단 — 근거: “그림 1에서 보듯 ~. 구체적으로 ~에서 ~로(약 ~%) 변했다.” 모든 수치는 3.3의 표기 원칙(단위, 자릿수, 수준과 비율)을 따르고, 모든 그림은 번호로 인용합니다.
- 3문단 — 한계와 확장: 서로 다른 한계 2개 이상 + “실제 ~ 데이터로 같은 분석을 반복한다” 같은 다음 단계 1개.
자기 점검 체크리스트
완성했다면 5부의 루브릭으로 자기 평가를 하고, 가능하다면 동료와 바꿔 읽으며 피드백 가이드의 세 항목을 주고받으세요.
12.9 요약: 질문에서 문장까지
이 장은 새 문법 대신 순서를 다뤘습니다. 넓은 궁금증을 답할 수 있는 질문으로 좁히고, 질문에 맞는 재료를 만들거나 구하고, 구간별 평균과 탄력성으로 답을 계산하고, 그 답을 주장-근거-한계의 구조에 담는 순서입니다. 배달비 예시에서 보았듯 좋은 보고서의 모든 숫자에는 출처가 되는 코드 셀이 있고, 결론 문단은 새 숫자를 만들지 않습니다. 상관을 인과로 읽지 않았는지, 축이 인상을 조작하지 않는지, 표본 밖으로 함부로 일반화하지 않았는지 — 이 세 질문은 남의 분석을 읽을 때도 그대로 쓰는 필터입니다. 이제 남은 것은 여러분의 완주입니다.
본문은 여기서 끝나지만 책은 조금 더 이어집니다. 프로젝트 중에 터미널 명령이 낯설면 부록 A를, 에러 메시지 앞에서 막히면 부록 B를, 다른 장의 연습문제를 확인하고 싶으면 부록 C를 펴면 됩니다. 그리고 프로젝트까지 끝났다면, 1장에서 print 한 줄을 실행하던 날과 오늘 사이의 거리를 한 번 재 보세요. 그 거리가 이 책이 하려던 이야기의 전부입니다.