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"] = False9 판다스 입문: 표로 생각하는 경제 데이터 (자율 학습)
10 9장 판다스 입문: 표로 생각하는 경제 데이터
자율 학습 장입니다. 수업 진도로는 7주차와 8주차 사이에 놓여 있지만, 8주 수업이 모두 끝난 뒤 방학에 혼자 읽어도 무리가 없도록 썼습니다. 필요한 배경은 4장의 리스트, 5장의 딕셔너리, 그리고 8장의 그래프 문법이 전부이고, 그마저도 본문에서 그때그때 다시 짚어 드립니다.
이번 장에서 배우는 것
- 리스트와 딕셔너리가 판다스의 Series·DataFrame과 어떻게 이어지는지 이해합니다.
read_csv로 데이터 파일을 읽고,head·info·describe로 첫인상을 파악하는 습관을 들입니다.- 열 선택과 불리언 필터로 표에서 원하는 행만 골라냅니다.
- 기존 열을 조합해 새 열을 만들고,
sort_values로 정렬해 순위를 읽습니다. groupby로 그룹별 통계를 내고, 그 결과를 그래프로 옮깁니다.- 경제학 쪽 목표는 두 가지입니다. 한국 거시 지표 25년 치를 표 하나로 훑는 것, 그리고 카페 판매 기록 450줄에서 사장님이 쓸 만한 의사결정 근거를 뽑아내는 것.
10.1 여는 질문: 450줄짜리 장부를 받았습니다
이콘 카페 사장님이 폐업 정리를 하다가… 는 아니고, 개강 후 90일 치 판매 기록을 파일 하나로 정리해서 건네주었다고 합시다. 날짜, 요일, 날씨, 메뉴, 가격, 판매량. 다섯 개 메뉴가 90일 동안 기록되었으니 모두 450줄입니다. 사장님의 질문은 구체적입니다. “비 오는 날에는 아메리카노가 더 팔리나요? 딸기스무디 재료 발주를 줄여야 하나 고민인데, 데이터로 좀 봐 주세요.”
4장에서 배운 대로라면 판매량 리스트를 만들고 for문을 돌려야 합니다. 날씨별로 나누려면 5장의 딕셔너리에 누적하는 코드를 짜야 하고요. 못 할 것은 없습니다. 그런데 450줄에 열이 여섯 개인 데이터를 리스트 여섯 개로 들고 다니는 것은 생각만 해도 손이 많이 갑니다. 엑셀을 써 본 사람이라면 이렇게 말할 겁니다. “그냥 표로 보면 안 되나요?”
됩니다. 그게 이번 장의 전부입니다. 사실 여러분은 표를 만들 재료를 이미 갖고 있습니다. 4장의 리스트는 표의 세로줄(열) 하나였고, 5장의 딕셔너리는 표의 가로줄(행) 하나였습니다. 그때 “이 구조가 나중에 표가 된다”고 예고했던 복선을 이 장에서 회수합니다. 리스트가 열이 되고, 딕셔너리가 행이 됩니다. 그리고 그 표를 다루는 도구의 이름이 판다스(pandas)입니다.
그래프 준비
이 장의 후반부에서 표를 곧장 그래프로 옮깁니다. 8장에서 쓰던 한글 폰트 설정을 먼저 실행해 둡니다.
# Colab에서 실행 중이라면 이 셀을 실행하세요 (로컬에서는 필요 없습니다)
!pip install koreanize-matplotlib
import koreanize_matplotlibColab은 한글 폰트가 없는 리눅스 환경이라 위 셀로 koreanize-matplotlib을 설치해야 그래프의 한글이 깨지지 않습니다. 설치는 세션당 한 번이면 됩니다.
10.2 1. 리스트는 열, 딕셔너리는 행
판다스로 건너가기 전에, 우리가 어디까지 왔는지 30초만 복습하겠습니다. 4장에서는 같은 종류의 값을 순서대로 담을 때 리스트를 썼습니다. 5장에서는 서로 다른 종류의 정보에 이름표를 붙여 묶을 때 딕셔너리를 썼고요. 이콘 카페의 기록을 그 두 방식으로 다시 적어 보면 이렇습니다.
# 4장 방식: 같은 항목을 날짜 순서대로 담은 리스트 (표의 세로줄 하나)
days = ["3월 3일", "3월 4일", "3월 5일"]
americano = [52, 54, 68]
# 5장 방식: 하루치 기록을 이름표와 함께 묶은 딕셔너리 (표의 가로줄 하나)
day_record = {"date": "3월 3일", "weather": "흐림", "americano": 52}
print(americano[0])
print(day_record["weather"])52
흐림
americano 리스트를 세로로 세우면 판매량 열이 됩니다. day_record 딕셔너리를 눕히면 3월 3일 행이 되고요. 엑셀 화면을 떠올리면 정확히 그 그림입니다. 열마다 이름(날짜, 날씨, 판매량)이 붙어 있고, 행마다 하루치 기록이 들어 있는 격자. 문제는 규모입니다. 90일, 메뉴 다섯 개, 열이 여섯 개면 리스트 여섯 개가 항상 같은 길이를 유지하도록 우리가 손으로 관리해야 합니다. 한 줄만 어긋나도 3월 5일의 판매량이 3월 6일의 날씨와 짝지어지는 사고가 납니다.
판다스는 이 격자 전체를 하나의 값으로 다루게 해 주는 라이브러리입니다. 이름이 귀여운 동물에서 온 것 같지만, 어원은 계량경제학 용어인 패널 데이터(panel data)입니다. 태생부터 경제 데이터를 다루려고 만든 도구라는 뜻이니, 경제학과 신입생과는 궁합이 좋은 편입니다. 관례상 import pandas as pd로 줄여서 불러옵니다. 7장에서 import 문과 별명 붙이기를 다뤘으니 낯설지 않을 겁니다.
10.3 2. Series: 이름표가 붙은 리스트
판다스의 첫 부품은 Series입니다. 표를 통째로 만들기 전에, 열 하나부터 만들어 보겠습니다. 최근 5년의 한국 경제성장률입니다.
import pandas as pd
growth = pd.Series([-0.7, 4.3, 2.6, 1.4, 2.0],
index=[2020, 2021, 2022, 2023, 2024])
growth2020 -0.7
2021 4.3
2022 2.6
2023 1.4
2024 2.0
dtype: float64
한 줄씩 뜯어보기
import pandas as pd— 판다스를 불러오고pd라는 별명을 붙입니다. 이후 판다스의 모든 기능은pd.뒤에 이어 씁니다.pd.Series([...], index=[...])— 첫 번째 재료는 값의 리스트, 두 번째 재료index=는 각 값에 붙일 이름표의 리스트입니다. 값 다섯 개에 이름표 다섯 개가 순서대로 짝지어집니다.- 마지막 줄의
growth— 셀의 마지막 줄에 변수 이름만 적으면print없이도 내용이 출력됩니다. 노트북이 주는 편의 기능인데, 판다스 객체는 이렇게 출력해야 표 모양이 곱게 나오므로 앞으로 자주 씁니다.
출력을 보면 왼쪽에 연도(인덱스), 오른쪽에 성장률(값)이 나란히 섰습니다. 리스트라면 “0번째 값”으로 불러야 했던 것을, Series에서는 5장의 딕셔너리처럼 이름표로 부를 수 있습니다.
print(growth[2020])
print(growth.mean())
print(growth.max())-0.7
1.92
4.3
growth[2020]은 딕셔너리에서 키로 값을 꺼내던 문법 그대로입니다. 그리고 mean(), max() 같은 통계가 바로 붙습니다. 4장에서 평균을 내려고 sum(값들) / len(값들)을 직접 조립했던 것과 비교하면 한결 짧습니다.
숫자도 잠깐 읽고 갑시다. 코로나 첫해의 -0.7%를 포함한 최근 5년 평균이 1.92%입니다. 잠시 뒤에 확인하겠지만 2000년 이후 25년 평균은 3.7% 언저리라, 최근 5년은 그 절반 수준입니다. 한국 경제의 성장 속도가 확연히 느려졌다는 이야기를 뉴스가 아니라 우리 손의 계산으로 확인한 셈입니다.
Series의 진짜 매력은 한꺼번에 계산하기입니다. 3장에서 조건문은 값 하나를 검사했습니다. Series에는 비교 연산을 통째로 적용할 수 있습니다.
growth >= 3.02020 False
2021 True
2022 False
2023 False
2024 False
dtype: bool
값 다섯 개가 동시에 검사되어 True/False가 다섯 개 담긴 Series가 나왔습니다. 최근 5년 중 성장률이 3%를 넘긴 해는 2021년뿐입니다. 지금은 “비교 연산이 열 전체에 한 번에 걸린다”는 감각만 챙겨 두세요. 4절에서 이 불리언 Series가 표를 걸러 내는 체가 됩니다.
# [직접 해보기]
# TODO 1: 같은 기간(2020~2024)의 물가상승률 0.5, 2.5, 5.1, 3.6, 2.3으로
# inflation이라는 Series를 만들어 보세요 (index는 연도).
# TODO 2: inflation의 평균과 최댓값을 출력해 보세요.
# TODO 3: 물가상승률이 3% 이상인 해를 True/False Series로 확인해 보세요.10.4 3. DataFrame: 열들을 묶으면 표가 된다
Series가 열 하나라면, 열 여러 개를 묶은 표 전체가 DataFrame입니다. 만드는 방법은 여는 질문의 복선 그대로입니다. “열 이름 → 리스트”의 딕셔너리를 넘기면, 리스트 하나하나가 열이 되어 표로 조립됩니다.
menu_table = pd.DataFrame({
"menu": ["아메리카노", "카페라떼", "딸기스무디", "크로플", "샌드위치"],
"price": [3000, 4000, 4500, 3500, 5500],
"avg_daily": [62, 42, 24, 31, 21],
})
menu_table| menu | price | avg_daily | |
|---|---|---|---|
| 0 | 아메리카노 | 3000 | 62 |
| 1 | 카페라떼 | 4000 | 42 |
| 2 | 딸기스무디 | 4500 | 24 |
| 3 | 크로플 | 3500 | 31 |
| 4 | 샌드위치 | 5500 | 21 |
한 줄씩 뜯어보기
pd.DataFrame({...})— 딕셔너리를 재료로 DataFrame을 만듭니다.- 딕셔너리의 키
"menu","price","avg_daily"— 각각 열 이름이 됩니다. - 딕셔너리의 값(리스트 세 개) — 각각 한 열의 내용이 됩니다. 세 리스트의 길이가 같아야 행이 어긋나지 않습니다.
- 출력의 맨 왼쪽 0~4 — 우리가 만들지 않은 숫자입니다. 판다스가 행마다 자동으로 붙인 인덱스로, 리스트의 자리 번호와 같은 역할을 합니다.
이콘 카페의 메뉴판이 표가 되었습니다. avg_daily는 하루 평균 판매량을 어림한 값입니다(정확한 값은 잠시 뒤 실제 데이터에서 계산합니다). 표의 생김새는 몇 가지 속성으로 확인합니다.
print(menu_table.columns)
print(menu_table.shape)
print(len(menu_table))Index(['menu', 'price', 'avg_daily'], dtype='str')
(5, 3)
5
columns는 열 이름들, shape는 (행 수, 열 수) 튜플, len()은 행 수입니다. 5행 3열짜리 작은 표라는 사실이 확인됩니다.
엑셀에 빗대면 이렇습니다. 엑셀 시트 하나가 DataFrame, 시트의 한 열이 Series입니다. 실제로 DataFrame에서 열 하나를 꺼내면 Series가 나옵니다.
prices = menu_table["price"]
print(prices)
print(type(prices))0 3000
1 4000
2 4500
3 3500
4 5500
Name: price, dtype: int64
<class 'pandas.Series'>
꺼내는 문법이 또 딕셔너리와 같습니다. menu_table["price"] — 키로 값을 꺼내듯, 열 이름으로 열을 꺼냅니다. 4장과 5장에서 익힌 대괄호 감각이 판다스에서 그대로 통한다는 것, 이 장에서 가장 여러 번 확인하게 될 사실입니다.
다만 손으로 딕셔너리를 적어 표를 만드는 일은 여기까지입니다. 다섯 줄짜리 메뉴판이야 손으로 치면 되지만, 450줄짜리 장부를 타이핑할 수는 없습니다. 진짜 데이터는 파일로 옵니다.
10.5 4. read_csv: 파일을 표로 읽어 오기
경제 데이터가 유통되는 가장 흔한 파일 형식은 CSV(comma-separated values)입니다. 이름 그대로 값들을 쉼표로 구분해 놓은 순수 텍스트 파일이라, 엑셀로도 열리고 메모장으로도 열리고 판다스로도 열립니다. 한국은행이나 통계청에서 자료를 내려받아도 대개 이 형식입니다.
먼저 읽을 파일은 data/korea_macro.csv. 2000년부터 2024년까지 한국의 경제성장률, 물가상승률, 실업률을 담은 연간 자료입니다. 한 가지 미리 밝혀 두면, 이 파일의 수치는 공식 통계를 바탕으로 교재 실습에 맞게 다듬은 교육용 근사치입니다. 큰 흐름은 실제와 같지만, 보고서에 인용할 때는 한국은행 경제통계시스템(ECOS)이나 통계청 KOSIS의 원자료를 쓰세요.
macro = pd.read_csv("data/korea_macro.csv")
macro.head()| year | gdp_growth | cpi_inflation | unemployment | |
|---|---|---|---|---|
| 0 | 2000 | 9.1 | 2.3 | 4.4 |
| 1 | 2001 | 4.9 | 4.1 | 4.0 |
| 2 | 2002 | 7.7 | 2.8 | 3.3 |
| 3 | 2003 | 3.1 | 3.5 | 3.6 |
| 4 | 2004 | 5.2 | 3.6 | 3.7 |
한 줄씩 뜯어보기
pd.read_csv("data/korea_macro.csv")— 따옴표 안은 파일의 경로입니다. 노트북이 있는 폴더 안의data폴더에서korea_macro.csv를 찾아 읽고, 그 내용 전체를 DataFrame으로 만들어 돌려줍니다. Colab이라면 왼쪽 파일 탭에 같은 구조로 파일을 올려 두어야 합니다.macro = ...— 그 표를macro라는 변수에 담습니다. 파일은 한 번만 읽고, 이후에는 변수만 다룹니다.macro.head()— 표의 머리, 즉 앞 다섯 행만 보여 줍니다.
25행을 다 쏟아 내는 대신 앞부분만 확인한 것에 주목하세요. 처음 만난 데이터를 다루는 요령은 전체를 훑는 게 아니라 짧은 인사를 세 번 건네는 것입니다. head()로 생김새를, info()로 구조를, describe()로 분포를 묻는, 말하자면 3단 인사입니다. 방금 첫인사를 했으니 두 번째로 넘어갑니다.
macro.info()<class 'pandas.DataFrame'>
RangeIndex: 25 entries, 0 to 24
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 year 25 non-null int64
1 gdp_growth 25 non-null float64
2 cpi_inflation 25 non-null float64
3 unemployment 25 non-null float64
dtypes: float64(3), int64(1)
memory usage: 932.0 bytes
info()는 표의 신상 명세입니다. 읽는 법을 짚어 보면, RangeIndex: 25 entries는 행이 25개(25개 연도)라는 뜻입니다. 열마다 붙은 25 non-null은 비어 있는 칸이 하나도 없다는 보고입니다. 실제 데이터에서는 이 숫자가 행 수보다 작은 경우가 흔한데, 그때는 결측치를 어떻게 처리할지부터 고민해야 합니다. 맨 오른쪽 Dtype은 각 열의 자료형입니다. year는 정수(int64), 나머지 세 열은 소수(float64) — 2장에서 배운 자료형 구분이 열 단위로 적용되어 있습니다.
macro.describe().round(2)| year | gdp_growth | cpi_inflation | unemployment | |
|---|---|---|---|---|
| count | 25.00 | 25.00 | 25.00 | 25.00 |
| mean | 2012.00 | 3.68 | 2.50 | 3.51 |
| std | 7.36 | 2.15 | 1.27 | 0.39 |
| min | 2000.00 | -0.70 | 0.40 | 2.70 |
| 25% | 2006.00 | 2.60 | 1.50 | 3.20 |
| 50% | 2012.00 | 3.20 | 2.50 | 3.60 |
| 75% | 2018.00 | 4.90 | 3.50 | 3.70 |
| max | 2024.00 | 9.10 | 5.10 | 4.40 |
세 번째 인사 describe()는 숫자 열마다 요약 통계를 한 상에 차려 줍니다. .round(2)는 소수점 둘째 자리까지만 보여 달라는 뒷정리이고요. 몇 줄만 읽어 보겠습니다.
gdp_growth의 평균(mean)은 3.68%. 25년 동안 한국 경제는 연평균 3.7%쯤 성장했습니다. 최솟값(min)은 -0.7%로, 어느 한 해 경제가 뒷걸음질쳤다는 뜻입니다. 어느 해인지는 잠시 뒤 필터로 찾아냅니다. 최댓값(max) 9.1%도 눈에 띕니다. 지금 감각으로는 비현실적인 숫자인데, 표의 첫 행(2000년)에서 이미 봤습니다. unemployment의 최솟값 2.7%와 최댓값 4.4%는 실업률이 생각보다 좁은 범위에서 움직였다는 사실을 알려 줍니다.
숫자 세 무더기를 훑는 데 셀 세 개, 1분이 걸리지 않았습니다. 앞으로 어떤 데이터를 만나든 이 3단 인사부터 건네는 습관을 들이면, 데이터의 함정 (결측치, 이상한 자료형, 말이 안 되는 극값)을 초반에 잡아낼 수 있습니다.
실수 노트: 파일이 안 읽힐 때 — 경로와 인코딩
read_csv에서 학생들이 처음 마주치는 에러는 대부분 둘 중 하나입니다.
첫째, FileNotFoundError: [Errno 2] No such file or directory: 'data/korea_macro.csv'. 파일이 없다는 말 그대로인데, 진짜 원인은 대개 “파일은 있는데 거기에 없는” 것입니다. 노트북 파일이 있는 폴더를 기준으로 경로를 찾기 때문에, 노트북과 data 폴더의 위치 관계가 예제와 다르면 이 에러가 납니다. 에러 메시지에 적힌 경로를 그대로 읽고, 그 위치에 파일이 실제로 있는지부터 확인하세요.
둘째, 한글 데이터에서 나는 UnicodeDecodeError, 또는 읽기는 됐는데 한글이 ë‚ ì§œ처럼 깨져 보이는 경우입니다. 텍스트 파일은 글자를 숫자로 바꿔 저장하는 규칙(인코딩)이 여러 가지인데, 파일을 쓴 규칙과 읽는 규칙이 어긋나면 이렇게 됩니다. 특히 한국에서 엑셀로 저장한 CSV는 cp949라는 한국어 인코딩인 경우가 많습니다. 그럴 때는 pd.read_csv("파일.csv", encoding="cp949")처럼 인코딩을 지정하고, 반대로 “UTF-8(BOM)”으로 저장된 파일은 encoding="utf-8-sig"가 해결책입니다. 이 두 옵션으로 한글 CSV 문제의 구 할이 풀립니다.
10.6 5. 열 고르기와 행 거르기
25행 4열의 표가 손에 들어왔으니, 이제 원하는 조각을 잘라 낼 차례입니다. 방향이 두 가지입니다. 세로로 자르면 열 선택, 가로로 자르면 행 필터입니다.
열 선택은 3절에서 이미 했습니다. 대괄호에 열 이름을 넣으면 Series가 나옵니다.
macro["gdp_growth"].head()0 9.1
1 4.9
2 7.7
3 3.1
4 5.2
Name: gdp_growth, dtype: float64
열 여러 개를 고를 때는 문법이 살짝 달라집니다. 대괄호 안에 열 이름의 리스트를 넣습니다. 대괄호가 두 겹으로 보이는 이유입니다.
macro[["year", "gdp_growth"]].head()| year | gdp_growth | |
|---|---|---|
| 0 | 2000 | 9.1 |
| 1 | 2001 | 4.9 |
| 2 | 2002 | 7.7 |
| 3 | 2003 | 3.1 |
| 4 | 2004 | 5.2 |
바깥 대괄호는 “표에서 꺼낸다”는 문법이고, 안쪽 대괄호는 ["year", "gdp_growth"]라는 리스트 자체입니다. 그래서 결과도 다릅니다. 한 겹이면 열 하나짜리 Series, 두 겹이면 (열이 하나뿐이어도) DataFrame입니다.
실수 노트: 대괄호 한 겹과 두 겹
이 구분에서 두 가지 에러가 자주 납니다. 열 두 개를 리스트 없이 macro["year", "gdp_growth"]로 쓰면 KeyError: ('year', 'gdp_growth')가 납니다. 판다스가 쉼표로 묶인 튜플을 통째로 열 이름인 줄 알고 찾다가 실패한 것입니다. 에러 메시지의 괄호 안에 우리가 쓴 이름이 튜플로 묶여 보인다면 십중팔구 안쪽 리스트 대괄호를 빠뜨린 경우입니다. 또 하나는 단순 오타입니다. macro["gdp_grwoth"]는 KeyError: 'gdp_grwoth'를 냅니다. KeyError는 항상 “네가 말한 이름표가 없다”는 뜻이므로, macro.columns를 출력해 정확한 열 이름을 확인하는 것이 가장 빠른 해결책입니다.
이제 가로 방향, 행 필터입니다. 질문 하나를 들고 갑니다. 25년 중 성장률이 3% 이상이었던 해는 몇 번이나 될까요? 감으로는 “옛날엔 흔했고 요즘은 드물다” 정도일 텐데, 정확히 세어 봅시다. 재료는 2절 끝에서 만들어 둔 불리언 Series입니다.
mask = macro["gdp_growth"] >= 3.0
print(mask.head())
print(mask.sum())0 True
1 True
2 True
3 True
4 True
Name: gdp_growth, dtype: bool
15
mask에는 25개 행 각각에 대한 True/False 판정이 담겨 있습니다. mask.sum()이 15가 나온 것은 파이썬이 True를 1로, False를 0으로 세기 때문입니다. 합계 15 = 조건을 만족한 행 15개. 그 15개 행을 실제로 꺼내려면, 이 판정표를 표의 대괄호 안에 넣습니다.
macro[macro["gdp_growth"] >= 3.0]| year | gdp_growth | cpi_inflation | unemployment | |
|---|---|---|---|---|
| 0 | 2000 | 9.1 | 2.3 | 4.4 |
| 1 | 2001 | 4.9 | 4.1 | 4.0 |
| 2 | 2002 | 7.7 | 2.8 | 3.3 |
| 3 | 2003 | 3.1 | 3.5 | 3.6 |
| 4 | 2004 | 5.2 | 3.6 | 3.7 |
| 5 | 2005 | 4.3 | 2.8 | 3.7 |
| 6 | 2006 | 5.3 | 2.2 | 3.5 |
| 7 | 2007 | 5.8 | 2.5 | 3.2 |
| 8 | 2008 | 3.0 | 4.7 | 3.2 |
| 10 | 2010 | 6.8 | 2.9 | 3.7 |
| 11 | 2011 | 3.7 | 4.0 | 3.4 |
| 13 | 2013 | 3.2 | 1.3 | 3.1 |
| 14 | 2014 | 3.2 | 1.3 | 3.5 |
| 17 | 2017 | 3.2 | 1.9 | 3.7 |
| 21 | 2021 | 4.3 | 2.5 | 3.7 |
macro[...]의 대괄호 안에 열 이름 대신 불리언 Series가 들어갔습니다. 판다스는 True인 행만 남기고 나머지를 걸러 냅니다. 체에 거른다고 생각하면 정확합니다. 결과를 읽어 보면 15개 연도 중 열한 번이 2011년 이전에 몰려 있고(2000~2008년은 9년 연속입니다), 2012년 이후로는 2013·2014·2017·2021년 네 번뿐입니다. “3% 성장은 옛날 얘기”라는 통념이 표 하나로 확인됩니다.
같은 체로 반대쪽 극단도 걸러 봅시다. 경제가 뒷걸음질친, 성장률이 마이너스인 해입니다.
macro[macro["gdp_growth"] < 0]| year | gdp_growth | cpi_inflation | unemployment | |
|---|---|---|---|---|
| 20 | 2020 | -0.7 | 0.5 | 4.0 |
25년 중 단 한 번, 2020년의 -0.7%입니다. 코로나가 덮친 해입니다. 흥미로운 비교 대상은 2009년입니다. 글로벌 금융위기의 한복판이었는데도 성장률은 0.8%로 플러스를 지켰습니다. 위기의 성격이 달랐다는 뜻이기도 하고, “마이너스 성장”이라는 사건이 한국 경제에서 얼마나 드문 일인지 보여 주는 결과이기도 합니다.
# [직접 해보기]
# TODO 1: 물가상승률(cpi_inflation)이 4% 이상이었던 해를 골라내 보세요.
# 어느 시기에 몰려 있나요?
# TODO 2: 실업률(unemployment)이 4.0% 이상이었던 해를 골라내 보세요.
# TODO 3: 두 결과에 겹치는 연도가 있는지 눈으로 비교해 보세요.10.7 6. 새 열 만들기와 정렬
표에 원래 없던 지표를 우리가 만들어 붙일 수도 있습니다. 성장률과 물가상승률을 한 화면에서 보다 보면 이런 질문이 생깁니다. 성장이 물가를 앞선 해와, 물가가 성장을 앞선 해는 언제였을까? 성장률에서 물가상승률을 뺀 값을 성장-물가 갭이라고 부르기로 합시다. 갭이 마이너스라면 경제가 커지는 속도보다 물가가 오르는 속도가 빨랐던, 말하자면 체감 경기가 유난히 쓰렸을 해입니다. 교과서에 있는 공식 지표는 아니고 이 장을 위한 단순 파생 지표이지만, 두 열을 묶어 읽는 연습으로는 충분합니다.
macro["gap"] = (macro["gdp_growth"] - macro["cpi_inflation"]).round(1)
macro.head()| year | gdp_growth | cpi_inflation | unemployment | gap | |
|---|---|---|---|---|---|
| 0 | 2000 | 9.1 | 2.3 | 4.4 | 6.8 |
| 1 | 2001 | 4.9 | 4.1 | 4.0 | 0.8 |
| 2 | 2002 | 7.7 | 2.8 | 3.3 | 4.9 |
| 3 | 2003 | 3.1 | 3.5 | 3.6 | -0.4 |
| 4 | 2004 | 5.2 | 3.6 | 3.7 | 1.6 |
왼쪽의 macro["gap"]은 아직 없는 열 이름입니다. 없는 이름에 값을 대입하면 새 열이 생깁니다 — 5장에서 딕셔너리에 새 키를 추가하던 문법과 같은 감각입니다. 오른쪽에서는 열 전체끼리 뺄셈이 한 줄로 일어납니다. 4장이라면 for문으로 25번 반복했을 계산입니다. .round(1)을 붙인 것은 컴퓨터의 소수 계산에 낀 부동소수점 오차(2.6 - 5.1이 -2.4999…로 계산되는 현상)를 소수 첫째 자리로 정리하기 위해서입니다.
그래서 갭이 가장 나빴던 해는 언제일까요? 표를 갭 순서로 줄 세우면 됩니다. 정렬 담당은 sort_values입니다.
macro.sort_values("gap").head()| year | gdp_growth | cpi_inflation | unemployment | gap | |
|---|---|---|---|---|---|
| 22 | 2022 | 2.6 | 5.1 | 2.9 | -2.5 |
| 23 | 2023 | 1.4 | 3.6 | 2.7 | -2.2 |
| 9 | 2009 | 0.8 | 2.8 | 3.6 | -2.0 |
| 8 | 2008 | 3.0 | 4.7 | 3.2 | -1.7 |
| 20 | 2020 | -0.7 | 0.5 | 4.0 | -1.2 |
sort_values("gap")은 gap 열 기준 오름차순(작은 값부터) 정렬이므로, head()와 붙이면 “갭 최악 5개 연도”가 됩니다. 결과가 흥미롭습니다. 1위 2022년(-2.5), 2위 2023년(-2.2) — 최근의 인플레이션 국면입니다. 그 뒤로 2009년(-2.0)과 2008년(-1.7)의 금융위기, 그리고 2020년(-1.2)의 코로나가 잇따릅니다. “요즘 살기 팍팍하다”던 2022~2023년의 체감이 25년 표 안에서도 금융위기급으로 기록된 셈입니다.
반대 방향, 큰 값부터 보려면 ascending=False 옵션을 붙입니다. 성장률 자체로 줄을 세워 보겠습니다.
macro.sort_values("gdp_growth", ascending=False).head(3)| year | gdp_growth | cpi_inflation | unemployment | gap | |
|---|---|---|---|---|---|
| 0 | 2000 | 9.1 | 2.3 | 4.4 | 6.8 |
| 2 | 2002 | 7.7 | 2.8 | 3.3 | 4.9 |
| 10 | 2010 | 6.8 | 2.9 | 3.7 | 3.9 |
2000년 9.1%, 2002년 7.7%, 2010년 6.8%. 최고 성장률 상위권은 모두 2010년 이전입니다. head(3)처럼 괄호에 숫자를 넣으면 보여 줄 행 수를 정할 수 있다는 것도 지나가며 확인해 두세요.
10.8 7. 그림으로 확인하기: 25년을 선 하나로
숫자로 확인한 이야기를 그래프로 옮깁니다. 판다스의 좋은 점은 DataFrame에 .plot()이 바로 붙어 있다는 것입니다. 내부적으로는 8장에서 배운 matplotlib를 호출하므로, .plot()으로 뼈대를 그리고 8장의 문법으로 다듬는 조합이 가능합니다.
ax = macro.plot(x="year", y="gdp_growth", figsize=(8, 4.5),
color="tab:blue", linewidth=2, legend=False)
ax.set_title("한국 경제성장률, 2000~2024")
ax.set_xlabel("연도")
ax.set_ylabel("실질 GDP 성장률 (%)")
ax.axhline(0, color="gray", linewidth=0.8)
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.annotate("2020년 코로나: -0.7%", xy=(2020, -0.7), xytext=(2011, 0.3),
arrowprops=dict(arrowstyle="->", color="gray"))
plt.tight_layout()
plt.show()
첫 줄이 핵심입니다. macro.plot(x="year", y="gdp_growth") — 어느 열을 가로축으로, 어느 열을 세로축으로 쓸지 이름으로 지정하면 선 그래프가 나옵니다. 4장에서 리스트 두 개를 plt.plot()에 넘기던 것보다 의도가 명확하게 읽힙니다. 반환값 ax는 8장에서 다룬 축 객체라서, 제목·라벨·격자 ·스파인 정리·주석까지 8장의 스타일 문법이 전부 그대로 붙습니다.
그래프가 말하는 문장을 한 줄로 적어 보면 이렇습니다. “한국의 성장률은 계단을 내려오듯 낮아져 왔고, 25년 중 유일한 마이너스는 2020년이었다.” 8장에서 강조했듯, 그래프를 그렸으면 이렇게 문장 하나로 요약하는 습관까지가 한 세트입니다.
성장-물가 갭 이야기도 그림으로 확인해 봅시다. 이번에는 y=에 열 이름의 리스트를 넘겨 선 두 개를 한 축에 그립니다.
ax = macro.plot(x="year", y=["gdp_growth", "cpi_inflation"], figsize=(8, 4.5),
color=["tab:blue", "tab:red"], linewidth=2)
ax.legend(["성장률", "물가상승률"])
ax.set_title("성장률과 물가상승률, 2000~2024")
ax.set_xlabel("연도")
ax.set_ylabel("%")
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.show()
파란 선(성장률)이 대체로 위에 있다가 빨간 선(물가)에 따라잡히는 구간이 몇 군데 보입니다. 2008년, 2020년, 그리고 두 선의 간격이 가장 크게 뒤집힌 2022~2023년. 6절에서 sort_values로 뽑았던 “갭 최악 연도” 명단과 정확히 같은 자리입니다. 표로 계산한 것과 그림으로 보이는 것이 서로를 확인해 줄 때, 분석은 비로소 믿을 만해집니다.
10.9 8. 이콘 카페의 90일: 진짜 분석 데이터
거시 지표로 몸을 풀었으니, 여는 질문의 장부를 열 차례입니다. data/cafe_sales.csv에는 이콘 카페의 90일 치 판매 기록이 들어 있습니다. (실제 가게가 아니라 이 교재를 위해 만든 가상 데이터입니다.) 3단 인사 중 첫인사부터.
cafe = pd.read_csv("data/cafe_sales.csv")
cafe.head()| date | weekday | weather | menu | price | quantity | |
|---|---|---|---|---|---|---|
| 0 | 2025-03-03 | 월 | 흐림 | 아메리카노 | 3000 | 52 |
| 1 | 2025-03-03 | 월 | 흐림 | 카페라떼 | 4000 | 40 |
| 2 | 2025-03-03 | 월 | 흐림 | 딸기스무디 | 4500 | 24 |
| 3 | 2025-03-03 | 월 | 흐림 | 크로플 | 3500 | 29 |
| 4 | 2025-03-03 | 월 | 흐림 | 샌드위치 | 5500 | 16 |
행 하나가 “어느 날짜에 어떤 메뉴가 몇 개 팔렸나” 하나에 해당합니다. 열은 여섯 개. 날짜(date), 요일(weekday), 날씨(weather), 메뉴(menu), 가격(price), 판매량(quantity)입니다. 90일 동안 매일 다섯 메뉴가 기록되었으니 90 × 5 = 450행이어야 합니다. info()로 확인해 봅시다.
cafe.info()<class 'pandas.DataFrame'>
RangeIndex: 450 entries, 0 to 449
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 450 non-null str
1 weekday 450 non-null str
2 weather 450 non-null str
3 menu 450 non-null str
4 price 450 non-null int64
5 quantity 450 non-null int64
dtypes: int64(2), str(4)
memory usage: 21.2 KB
450 entries, 결측치 없음. 문자열 열 네 개는 Dtype이 str로, 숫자 열 두 개는 int64로 잡혀 있습니다. 거시 데이터와 달리 이 표에는 범주형 열(요일, 날씨, 메뉴)이 많은데, 범주형 열에 건네는 인사는 따로 있습니다. “종류별로 몇 번 나오나요?”를 묻는 value_counts입니다.
cafe["weather"].value_counts()weather
맑음 260
흐림 105
비 85
Name: count, dtype: int64
맑음 260행, 흐림 105행, 비 85행. 여기서 한 번 멈춰서 단위를 생각해야 합니다. 이 숫자는 행 수이지 날 수가 아닙니다. 하루에 다섯 행씩 기록되므로, 날수로 바꾸려면 5로 나눠야 합니다. 맑은 날 52일, 흐린 날 21일, 비 온 날 17일. 데이터를 셀 때 “지금 세고 있는 단위가 무엇인가”를 자문하는 습관은 판다스 문법보다 중요합니다. 행 수를 날수로 착각하면 “석 달간 260일 맑았다”는 괴상한 문장이 보고서에 들어가게 됩니다.
다음으로, 사장님의 관심사인 돈 계산부터 해결해 둡시다. 표에는 가격과 판매량만 있고 매출액이 없습니다. 6절에서 배운 새 열 만들기로 매출액 열을 추가합니다. 매출액 = 가격 × 수량.
cafe["revenue"] = cafe["price"] * cafe["quantity"]
cafe.head(3)| date | weekday | weather | menu | price | quantity | revenue | |
|---|---|---|---|---|---|---|---|
| 0 | 2025-03-03 | 월 | 흐림 | 아메리카노 | 3000 | 52 | 156000 |
| 1 | 2025-03-03 | 월 | 흐림 | 카페라떼 | 4000 | 40 | 160000 |
| 2 | 2025-03-03 | 월 | 흐림 | 딸기스무디 | 4500 | 24 | 108000 |
450개 행의 곱셈이 한 줄에 끝났습니다. 첫 행을 검산해 보면 3,000원 × 52잔 = 156,000원. 맞습니다.
실수 노트: 걸러 낸 표에 값을 대입할 때 (판다스 3 기준)
새 열 만들기에서 한 가지 함정을 짚고 갑니다. 필터와 대입을 이어 붙인 cafe[cafe["menu"] == "아메리카노"]["quantity"] = 0 같은 코드는 원본 cafe를 바꾸지 못합니다. 우리가 쓰는 판다스 3에서는 필터 결과가 원본의 복사본으로 취급되어, 복사본에 쓴 값은 조용히 버려지기 때문입니다 (과거 버전에서는 이럴 때 SettingWithCopyWarning이라는 유명한 경고가 떴는데, 지금은 아예 동작 규칙이 “복사본 수정은 원본에 반영되지 않는다”로 정리되었습니다). 지금 단계에서 기억할 규칙은 하나면 충분합니다. 열을 만들거나 고칠 때는 cafe["열이름"] = ... 꼴로, 원본 표에 대괄호 한 번만 거쳐서 대입한다. 걸러 낸 부분 표는 읽기 전용이라고 생각하면 안전합니다.
10.10 9. groupby: 쪼개고, 계산하고, 합친다
이제 이 장의 본진입니다. 사장님의 질문들 — 메뉴별 매출은? 요일별 판매량은? 날씨별 차이는? — 은 전부 같은 모양입니다. “~별로 묶어서 통계를 내 달라.” 판다스에서 이 일을 하는 도구가 groupby입니다.
동작 원리는 세 단계입니다. 표를 그룹별로 쪼개고(예: 메뉴가 같은 행끼리 다섯 무더기로), 무더기마다 계산하고(각 무더기의 매출액 합계), 결과를 한 표로 합칩니다. 데이터 분석 분야에서는 이 리듬을 분할-적용-결합 (split-apply-combine)이라고 부릅니다. 사실 여러분은 이 일을 이미 해 봤습니다. 5장에서 빈 딕셔너리를 만들고 for문을 돌리며 키별로 값을 누적하던 코드, 그것이 손으로 짠 groupby였습니다. 이제 그 네댓 줄이 한 줄이 됩니다.
menu_revenue = cafe.groupby("menu")["revenue"].sum()
menu_revenuemenu
딸기스무디 9630000
샌드위치 10263000
아메리카노 16824000
카페라떼 15240000
크로플 9747500
Name: revenue, dtype: int64
한 줄씩 뜯어보기
cafe.groupby("menu")— 표를menu값이 같은 행끼리 다섯 무더기로 쪼갭니다. 이 시점에는 아직 아무 계산도 일어나지 않습니다.["revenue"]— 무더기마다 어느 열을 계산에 쓸지 고릅니다. 열 선택 대괄호가 그룹에도 그대로 쓰입니다..sum()— 무더기별로 합계를 내고, 결과를 하나의 Series로 합칩니다. 인덱스가 메뉴 이름, 값이 매출 합계입니다.
읽는 순서 그대로 번역하면 “카페 장부를 메뉴별로 나누고, 매출액 열의, 합계를 구하라”입니다. 판다스 문장 중에는 드물게 한국어 어순과 거의 일치해서, 소리 내어 읽어 보는 것이 문법 암기보다 효과적입니다.
순위로 보려면 6절의 sort_values를 이어 붙입니다.
menu_revenue.sort_values(ascending=False)menu
아메리카노 16824000
카페라떼 15240000
샌드위치 10263000
크로플 9747500
딸기스무디 9630000
Name: revenue, dtype: int64
90일 총매출 1위는 아메리카노(1,682만 원), 2위는 카페라떼(1,524만 원)로 커피 두 종이 압도적입니다. 사장님이 고민하던 딸기스무디는 963만 원으로 꼴찌입니다. 잔당 가격이 4,500원으로 두 번째로 비싼데도 총매출이 가장 낮다는 것은 판매량이 그만큼 부진하다는 뜻입니다.
다음 질문, 요일 효과입니다. 요일별 평균 판매량을 구해 봅시다.
weekday_avg = cafe.groupby("weekday")["quantity"].mean().round(1)
weekday_avgweekday
금 40.3
목 35.5
수 34.7
월 34.9
화 34.7
Name: quantity, dtype: float64
결과는 나왔는데 순서가 이상합니다. 금, 목, 수, 월, 화 — 판다스가 그룹 이름을 가나다순으로 정렬했기 때문입니다. 요일에는 요일의 순서가 있으니, reindex에 원하는 순서의 리스트를 넘겨 재배열합니다.
weekday_avg.reindex(["월", "화", "수", "목", "금"])weekday
월 34.9
화 34.7
수 34.7
목 35.5
금 40.3
Name: quantity, dtype: float64
월~목은 34~35잔 언저리에서 엎치락뒤치락하는데 금요일만 40.3잔으로 혼자 15%쯤 높습니다. 주말 직전의 들뜬 소비 심리라고 해석할 수도 있고, 금요일에 수업이 몰린 시간표 탓일 수도 있습니다. 데이터는 “금요일에 더 팔린다”까지만 말해 주고, 왜 그런지는 말해 주지 않습니다. 이 선 긋기는 잠시 뒤 날씨 분석에서 한 번 더 중요해집니다.
드디어 여는 질문입니다. 비 오는 날에는 아메리카노가 더 팔릴까요? 필요한 도구는 이미 다 배웠습니다. 5절의 필터로 아메리카노 행만 남기고, 그 부분 표를 날씨별로 groupby하면 됩니다.
americano = cafe[cafe["menu"] == "아메리카노"]
americano.groupby("weather")["quantity"].mean().round(1)weather
맑음 61.6
비 67.8
흐림 59.8
Name: quantity, dtype: float64
비 오는 날 평균 67.8잔. 맑은 날 61.6잔, 흐린 날 59.8잔. 비가 오면 아메리카노가 하루 6~8잔 더 나갑니다. 사장님의 감이 데이터로 확인되는 순간입니다. 그렇다면 반대쪽, 차가운 딸기스무디는 어떨까요?
smoothie = cafe[cafe["menu"] == "딸기스무디"]
smoothie.groupby("weather")["quantity"].mean().round(1)weather
맑음 25.1
비 18.1
흐림 25.0
Name: quantity, dtype: float64
예상대로 방향이 반대입니다. 맑은 날 25.1잔이 비 오는 날 18.1잔으로, 비율로 치면 30% 가까이 빠집니다. 두 결과를 붙여 읽으면 제법 그럴듯한 그림이 나옵니다. 궂은 날씨에 소비자들이 차가운 음료 대신 따뜻한 음료를 찾는, 말하자면 두 메뉴가 날씨를 사이에 둔 대체재처럼 움직이는 패턴입니다. 발주 담당자에게는 실전적인 함의가 있습니다. 일기예보에 비가 잡힌 주간에는 딸기 재료를 줄이고 원두를 넉넉히 두라는 것.
다만 한 가지 조심할 것이 있습니다. 우리가 확인한 것은 비 오는 날에 스무디가 덜 팔렸다는 동반 패턴이지, 비 때문에 덜 팔렸다는 인과의 증명이 아닙니다. 비 온 날이 17일뿐이라는 표본 크기도 마음에 걸립니다. 이 정도 데이터로는 “패턴이 보이니 발주에 참고하되, 계속 지켜보자”가 정직한 결론입니다. 상관과 인과를 가르는 문제는 11장에서 제대로 다룹니다.
마지막으로, 통계 여러 개를 한 번에 받고 싶을 때가 있습니다. .sum()이나 .mean() 하나 대신 agg에 원하는 통계들의 리스트를 넘깁니다.
cafe.groupby("menu")["quantity"].agg(["sum", "mean", "max"]).round(1)| sum | mean | max | |
|---|---|---|---|
| menu | |||
| 딸기스무디 | 2140 | 23.8 | 37 |
| 샌드위치 | 1866 | 20.7 | 32 |
| 아메리카노 | 5608 | 62.3 | 74 |
| 카페라떼 | 3810 | 42.3 | 61 |
| 크로플 | 2785 | 30.9 | 42 |
열마다 통계 하나씩, 메뉴별 판매량의 합계·평균·최댓값이 표 하나로 정리됩니다. 3절에서 메뉴판을 손으로 만들 때 어림값으로 적었던 하루 평균 판매량(아메리카노 62잔)이 실제 계산(62.3잔)과 거의 일치한다는 것도 여기서 확인할 수 있습니다. agg는 안에 넣는 이름만 바꾸면 되니, 필요한 통계가 생길 때마다 리스트를 조립해 쓰면 됩니다.
# [직접 해보기]
# TODO 1: 요일별 매출액(revenue) 합계를 구하고, 월~금 순서로 재배열해 보세요.
# TODO 2: 카페라떼만 골라 날씨별 평균 판매량을 구해 보세요.
# 아메리카노와 같은 방향의 패턴이 나타나나요?
# TODO 3: groupby("weather")["revenue"].mean()으로 날씨별 평균 매출액을 구하고,
# 행 단위가 무엇인지(하루? 메뉴 하나?) 한 문장으로 적어 보세요.10.11 10. 그림으로 확인하기: 사장님께 보여 드릴 두 장
분석 결과를 사장님께 보고한다면 표보다 그래프가 힘이 셉니다. 범주 간 비교이므로 이번에는 막대 그래프, .plot(kind="bar")입니다.
menu_rank = (menu_revenue / 10000).sort_values(ascending=False)
ax = menu_rank.plot(kind="bar", figsize=(7, 4), color="tab:blue")
ax.set_title("이콘 카페 메뉴별 총매출 (90일)")
ax.set_xlabel("")
ax.set_ylabel("총매출 (만 원)")
ax.grid(axis="y", alpha=0.3)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
groupby로 만든 Series에 .plot(kind="bar")만 붙이면 인덱스(메뉴 이름)가 가로축, 값이 막대 높이가 됩니다. 그리기 전에 매출을 10,000으로 나눠 단위를 만 원으로 바꿨습니다. 세로축에 16,824,000 같은 숫자가 그대로 붙으면 읽는 사람이 0을 세느라 그래프를 못 보기 때문입니다. plt.xticks(rotation=0)은 세로로 눕는 가로축 글자를 바로 세우는 손질입니다.
이 그래프의 한 줄 요약: “이콘 카페 매출의 절반 이상을 커피 두 종이 벌어 온다.”
두 번째 장은 날씨 이야기입니다. 아메리카노와 딸기스무디의 날씨별 평균 판매량을 막대 두 벌로 나란히 세워 보겠습니다. groupby 결과 두 개를 딕셔너리로 묶어 DataFrame을 만들면 — 3절에서 표를 만들던 바로 그 문법 — 그룹 막대 그래프가 공짜로 나옵니다.
weather_check = pd.DataFrame({
"아메리카노": americano.groupby("weather")["quantity"].mean(),
"딸기스무디": smoothie.groupby("weather")["quantity"].mean(),
}).reindex(["맑음", "흐림", "비"])
ax = weather_check.plot(kind="bar", figsize=(7, 4),
color=["tab:blue", "tab:pink"])
ax.set_title("날씨별 하루 평균 판매량")
ax.set_xlabel("")
ax.set_ylabel("평균 판매량 (잔)")
ax.grid(axis="y", alpha=0.3)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
파란 막대(아메리카노)는 비에서 가장 높고, 분홍 막대(딸기스무디)는 비에서 가장 낮습니다. 9절에서 숫자로 확인한 엇갈림이 한눈에 들어옵니다. 한 줄 요약: “비 오는 날, 손님들은 스무디 대신 아메리카노를 고른다.”
여기까지가 판다스 분석의 기본 리듬입니다. 읽고(read_csv), 인사하고 (head·info·describe), 자르고(열 선택·필터), 만들고(새 열), 묶어 계산하고(groupby), 그림으로 확인한다(.plot). 이 여섯 박자면 웬만한 표 데이터는 어디서 받든 첫 분석을 시작할 수 있습니다.
10.12 연습문제
손풀기
1. (Series 만들기) 등록금 인상률. 어느 대학의 최근 4년 등록금 인상률이 0.0, 1.2, 3.5, 4.2(%)라고 합니다. 연도 2022~2025를 인덱스로 하는 Series를 만들고, 평균 인상률을 출력하세요.
2. (열 선택) 고용 지표만 추리기. macro에서 year와 unemployment 두 열만 골라 DataFrame으로 출력하세요. 대괄호가 몇 겹이어야 하는지에 주의하세요.
3. (value_counts) 장부 검증. cafe의 menu 열에 value_counts를 적용해, 다섯 메뉴가 정말 90행씩 기록되었는지 확인하세요.
응용
4. (필터) 실업률이 높았던 해. macro에서 실업률이 4.0% 이상인 해를 골라내고, 그 연도들이 어떤 시기에 몰려 있는지 한 문장으로 설명하세요.
5. (새 열과 정렬) 최고 매출의 날. cafe의 revenue 열을 이용해 매출액 기준 상위 5개 행을 출력하세요(sort_values와 head를 이어 붙이면 됩니다). 상위 5행의 메뉴와 요일에 공통점이 있는지 관찰하세요.
6. (groupby) 요일별 매출 비중. 요일별 revenue 합계를 구한 뒤, 금요일 매출이 90일 총매출에서 차지하는 비율(%)을 계산하세요. 힌트: Series에서 특정 값은 합계결과["금"]으로 꺼낼 수 있습니다.
도전
7. (이중 groupby) 요일과 날씨의 조합. groupby에는 열 이름의 리스트를 넘길 수도 있습니다. cafe.groupby(["weekday", "weather"])를 이용해 요일-날씨 조합별 평균 판매량을 구하고, 평균이 가장 높은 조합을 찾아 그 이유를 추측하는 문장을 한 줄 쓰세요.
8. (거시 필터 종합) 좋았던 해 찾기. 성장률 3% 이상이면서 물가상승률 3% 이하인 해를 “지표가 좋았던 해”라고 정의합시다. 두 조건을 동시에 만족하는 해를 골라내세요. 힌트: 두 불리언 Series를 &로 묶고 각 조건을 괄호로 감쌉니다 — (조건1) & (조건2). 그런 해가 몇 번인지, 2015년 이후에는 몇 번인지 세어 보세요.
풀이는 부록 C에 있습니다.
10.13 미니 프로젝트: 생각을 코드로, 코드를 글로
과제: 카페 사장님께 드리는 보고서 (예상 소요 30~60분)
여러분은 이콘 카페의 데이터 컨설턴트입니다. cafe_sales.csv 90일 치 데이터를 분석해, 사장님이 메뉴 구성이나 재료 발주에 바로 참고할 수 있는 한 페이지 보고서를 작성하세요. 요구 사항은 다음과 같습니다.
groupby로 계산한 인사이트 두 개. 본문에서 다룬 분석(메뉴별 매출, 날씨 효과)을 그대로 내도 되지만, 적어도 하나는 본문에 없는 조합이면 좋습니다. 요일별 매출, 날씨별 크로플 판매, 요일-날씨 이중 groupby 등 재료는 충분합니다.- 인사이트를 뒷받침하는 그래프 두 개. 각 그래프에 제목과 축 라벨을 달고, 그래프마다 “이 그림이 말하는 문장”을 한 줄씩 적으세요.
- 마지막에 3문단 글을 붙입니다. 문단 구성은 아래 틀을 따르세요.
3문단 글쓰기 틀
- 1문단 — 주장: 사장님이 무엇을 해야 하는지 한 문장으로 제안하고 (예: “비 예보가 있는 주에는 스무디 재료 발주를 줄이십시오”), 왜 그런 제안을 하는지 요지를 밝힙니다.
- 2문단 — 근거: 1문단의 제안을
groupby결과의 구체적 수치와 그래프 두 개를 인용하며 뒷받침합니다. “비 오는 날 스무디 판매는 평균 18.1잔으로 맑은 날(25.1잔)보다 28% 적었다”처럼, 숫자가 문장 안에 들어가야 합니다. - 3문단 — 한계와 확장: 이 분석이 말할 수 없는 것(90일뿐인 표본, 상관과 인과의 구분, 가격이 변하지 않았다는 조건 등)을 한두 가지 밝히고, 데이터가 더 있다면 무엇을 추가로 보고 싶은지 적습니다.
자기 점검 체크리스트
- 보고서의 첫 문장이 코드 설명이 아니라 사장님을 위한 제안으로 시작하는가?
- 인사이트 두 개가 각각
groupby결과의 실제 수치를 인용하는가? - 그래프 두 개에 제목·축 라벨이 있고, 본문 문장에서 그래프를 직접 언급하는가?
- 제안(메뉴·발주 결정)이 수치와 논리적으로 연결되는가, 아니면 데이터 없이도 할 수 있는 말인가?
- 분석의 한계를 적어도 하나, 구체적으로 밝혔는가?
10.14 요약과 다음 장 예고
이 장의 출발점은 4장과 5장에 심어 둔 복선이었습니다. 리스트는 표의 열, 딕셔너리는 표의 행이었고, 판다스는 그 둘을 Series와 DataFrame이라는 격자로 묶어 통째로 다루게 해 주었습니다. 파일을 읽으면(read_csv) 우선 head·info·describe의 3단 인사로 데이터의 생김새와 함정을 확인했고, 대괄호 한 겹과 두 겹으로 열을 고르고 불리언 Series라는 체로 행을 걸렀습니다. 열끼리의 연산으로 매출액과 성장-물가 갭 같은 새 지표를 만들었고, sort_values로 줄을 세워 2022~2023년이 25년 표 안에서 금융위기급 체감 국면이었음을 읽어 냈습니다. 분석의 중심 도구는 “쪼개고, 계산하고, 합친다”의 groupby였습니다. 그 한 줄로 이콘 카페의 매출 순위와 금요일 효과, 그리고 비 오는 날 아메리카노와 딸기스무디가 엇갈리는 패턴까지 확인했습니다. 다만 데이터가 보여 준 것은 동반된 패턴이지 인과의 증명이 아니라는 선도 함께 그었습니다.
다음 10장은 8주차 수업의 종착지입니다. 지금까지 만든 수요·공급 함수와 그래프, 그리고 이 장의 데이터 감각을 모두 들고 가서, 세 나라의 거시 지표를 진단하고 세금이 시장 균형을 어떻게 움직이는지 분석합니다. 마지막 에는 그 분석 전체를 터미널에서 명령 한 줄로 실행되는 나만의 CLI 도구로 묶습니다. 7장에서 잠깐 맛본 argparse가 주인공으로 돌아옵니다.