KAIST Junior · 2026년 1학기 · 데이터
🍳

까다로운 데이터
요리하기

[Challenge] 데이터 사이언스 · 2차시

현실의 데이터는 깨끗하지 않아요. 결측값, 형식 불일치, 중복이 가득한 날 데이터(Raw Data)
groupby · merge · loc · sort_values · 기술통계 · 히스토그램으로 가공하고 살펴봅니다.
서울시 자치구별 버스 승객수 데이터를 직접 다루며 데이터 사이언스의 핵심 도구를 익혀봅시다.

📑 목차

1
들어가기
학습목표 · 학습내용 · 과제 안내
2
1. 데이터 가공의 필요성
날 데이터(Raw Data)의 문제 · S-DoT 사례
3
2. 데이터 가공 — groupby & merge
groupby로 집계 · merge로 결합 (Inner / Left / Right / Outer Join)
4
3. 까다로운 데이터 살펴보기 — loc · iloc · sort_values
조건 추출과 정렬
5
4. 기술통계와 시각화
평균/중앙값/최빈값 · 분산/표준편차 · 히스토그램과 분포의 치우침(Skew)
6
5. 마무리하며
2차시 요약 · 다음 시간 예고
7
과제 안내
3가지 과제 (groupby / loc·sort_values / 히스토그램)
Part 1

들어가기

왜 데이터를 가공해야 할까?

지난 시간 우리는 데이터 사이언스의 전체 그림을 그렸다면, 이번 시간에는 "데이터를 다루는 손기술"을 본격적으로 익힙니다. 현실의 데이터는 깨끗하지 않아요. 컬럼 이름이 한글이고 인코딩이 EUC-KR이며 결측값이 섞여 있고 형식도 들쭉날쭉합니다. 이 까다로운 데이터를 요리해서 우리가 원하는 답을 얻는 과정 — 그게 이번 차시의 핵심입니다.

🍳 요리 비유로 보는 데이터 전처리

집에서 요리할 때 양파는 껍질을 벗기고 썰고, 당근은 흙을 씻고 다듬고, 고기는 핏물을 빼고 자릅니다. 손질 없이 통째로 냄비에 넣으면 원하는 요리가 안 나오죠. 데이터도 똑같습니다. 현실의 데이터는 시장에서 갓 사온 재료처럼 손질이 필요합니다.

요리사 = 데이터 과학자. 재료 손질 = 데이터 전처리. 칼·도마 = 판다스 등 데이터 핸들링 도구.

현실의 데이터가 가공이 필요한 이유

🌆 범위 좁히기

서울시 전체 데이터인데 우리 동네만 보고 싶을 때

📅 시간 단위 바꾸기

일별 데이터인데 월별 평균이 필요할 때

🧮 새 지표 계산

인구수와 면적이 따로 있는데 인구밀도가 필요할 때

🔍 의미 찾기

숫자만 나열되어 있어 의미 파악이 어려울 때

📚 두 가지 핵심 용어

📦 Data Preprocessing (데이터 전처리)
분석 목적에 맞춰 데이터의 특성을 이해하고 가공하는 전체 과정. 결측·오타·형식 정리부터 새 변수 생성까지.

🛠 Data Handling (데이터 핸들링)
판다스 등으로 데이터를 자유자재로 다루는 손기술. 추출·필터·집계·결합·정렬을 자유롭게.

🧰 오늘 사용할 도구

🗺 이번 차시 학습 흐름

  1. 이상적 vs 현실 데이터 비교 → 왜 가공이 필요한지 체감
  2. 원하는 정보 추출 (groupby, merge, loc, iloc, sort_values)
  3. 기술통계 개념으로 데이터 특성 이해 (중심·흩어짐)
  4. 시각화로 분포 살펴보기 (히스토그램, skew)

복잡해 보이는 숫자 뭉치를 명확하고 의미 있는 정보로 변화시키는 마법 같은 과정. 데이터 사이언스의 요리사가 될 준비가 되었나요?

학습목표

학습내용

핵심 키워드

🍳 데이터 가공

Aggregation (groupby) · Merge (Inner/Left/Right/Outer Join)

🔍 데이터 추출

loc(라벨) · iloc(인덱스) · sort_values(정렬)

📊 기술통계 & 시각화

평균 · 중앙값 · 최빈값 · 분산 · 표준편차 · 히스토그램

과제 안내 — 미리보기

이번 차시 과제는 서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용한 3가지입니다. 첨부된 두 파일을 다운로드하여 사용하세요. 모든 과제는 코드 + 결과값 캡쳐를 함께 제출하고, 최종 결과는 1개의 압축 파일(zip)로 묶어서 냅니다. 세부 풀이 가이드와 코드 템플릿은 마지막 "과제 안내" 섹션에 모두 정리되어 있습니다.

📂 첨부 파일bus.csv (서울시 자치구별 버스 승객수) · master.csv (자치구·행정동 매핑) → merge 후 bus_merged 사용

📝 과제 1. 자치구별 평균·최대

groupby('자치구_명칭') 로 집계.
1-1. 강남구의 평균 승객 수
1-2. mean()max() 로 바꿔, 최대 승객 수가 가장 많은 자치구

🔍 과제 2. 조건 추출 + 다중 정렬

loc·sort_values() 활용.
2-1. 강남구 이거나(|) 승객 수 ≥ 30,000명 → 행정동·승객수 추출
2-2. 자치구→행정동 가나다순 정렬 후 첫 행

📊 과제 3. 영등포구 히스토그램

plt.hist() + axvline().
평균(빨강)·중앙값(주황) 수직선 표시
3-1. 분포 특징 상세 서술 (skew·평균-중앙값 차이·극단값·집중 구간)
📅 1차 제출 마감: 2026년 5월 3일(일) 23:59
1차 기간 내 제출 시 원 점수 100% · 2차 기간 내 제출 시 원 점수 80% 적용
📦 제출 형식: 코드(.py 또는 .ipynb) + 결과 캡쳐 이미지 → 1개의 zip 파일로 압축
Part 2

1. 데이터 가공의 필요성

날 데이터(Raw Data)의 문제와 S-DoT 사례

"Garbage In, Garbage Out"

"Garbage in, garbage out"이라는 말이 있습니다. 쓰레기를 넣으면 쓰레기가 나온다는 뜻이죠. 데이터 사이언스에서 가장 중요한 원칙 중 하나입니다. 데이터의 질이 분석 결과의 질을 결정합니다. 아무리 뛰어난 분석 기법이 있더라도, 다루는 데이터가 엉망이라면 좋은 결과를 기대할 수 없어요.

현실에서 우리가 만나는 데이터는 대부분 날 데이터(Raw Data)입니다. 즉, 가공되지 않은 상태의 데이터입니다. 결측값이 있고, 중복이 있고, 표준화되지 않은 형식이 섞여 있는 데이터를 만나게 됩니다.

이상적 데이터 vs 현실 데이터 — 무엇이 다를까?

✨ 이상적 데이터 (교과서 속)

모든 칸이 채워져 있고, 형식이 일관되며, 중복·오타·이상치가 없는 분석 가능한 깔끔한 표. → 그대로 분석에 투입 가능

🌪️ 현실 데이터 (수집된 그대로)

결측·불일치·오타·이상치·중복이 뒤섞여 있는 날 데이터(Raw Data). → 가공(정제) 단계 없이는 분석이 어긋남
📌 핵심 원칙 — 분석가의 역량은 데이터를 가공하는 단계에서 가장 빛납니다. 데이터의 질이 결국 분석 결과의 질을 결정하기 때문에, 가공은 "분석을 시작하기 전 필수 단계"입니다. 가공은 크게 데이터 정제(Cleaning)데이터 변환(Transformation)으로 나뉘는데, 본 차시에서는 정제에 초점을 맞춥니다.

현실 데이터에서 자주 만나는 8가지 문제

문제 유형짧은 정의 / 예시
결측 값 (Missing)값이 비어 있거나 측정되지 않아 모르는 칸 (NaN)
불일치한 값표기 방식·단위가 통일되지 않은 값 (예: kg vs g, "서울" vs "Seoul")
잘못된 값·오타실제와 다르게 기재된 값 (예: 나이가 999, 음수)
이상한 값 (Outlier)다른 값들에 비해 너무 크거나 너무 작아 흐름과 어긋나는 값
중복된 값같은 의미의 값이 두 번 이상 입력된 경우
다른 형식의 값날짜가 '2023-01-01', '23.1.1', '2023년 1월 1일' 등 다양한 형식으로 섞여 있음
불필요한 값분석 목적과 관련 없는 컬럼·행이 데이터 더미에 포함되어 있음
분류되지 않은 값카테고리(범주) 분류가 안 되어 있어 그룹별 비교가 어려움

실제 사례 — 서울시 S-DoT 데이터

S-DoT(Smart Seoul Data of Things)은 서울시가 도시 데이터를 수집하기 위해 설치한 IoT 센서입니다. 미세먼지, 소음, 온습도, 자외선 등 다양한 도시 환경 데이터를 실시간으로 수집해 무료로 제공합니다.

📍 S-DoT 데이터 다운로드
서울 열린데이터 광장(data.seoul.go.kr)의 "도시데이터 센서(S-DoT) 환경정보" 페이지에서 받을 수 있습니다.

그런데 S-DoT 데이터를 받아 열어보면 어떨까요? 아래는 실제 S-DoT 환경 정보 데이터의 일부입니다.

SDOT_ID설치일설치 위치자치구온도(°C)습도(%)
SDOT0012023-09-2main streetEunpyeongJingwan-d16.8NaN
SDOT0012023-09-2residentialGwanak-guSinllim-doNaNNaN
SDOT0012019-09-2traditionalGangbuk-Mia-dong20.878
SDOT0012019-09-2traditionalYangcheonSinwol1(Il)22.272
SDOT0012023-09-2residentialGwanak-guSinllim-do18.3NaN
SDOT0012019-09-2traditionalYangcheonSinwol1(Il)21.570

설치 일자, 설치 위치, 자치구가 영문으로 입력되어 있고, 같은 자치구나 같은 위치 데이터가 수집되지 않은 곳이 보입니다. 어떤 곳은 한꺼번에 데이터가 수집되어 있는 경우도 있죠. 이렇게 우리가 사용하기에는 까다로운 데이터를 가공해야 합니다.

🎯 데이터 가공 = 데이터 사이언스의 시작

"Garbage in, Garbage out"의 대표적인 사례입니다. 어떻게 좋은 분석을 하고, 의미 있는 결과를 도출할지 결정하는 가장 중요한 시작점이죠.

📖 알아두기 — 정제된 데이터(Cleaned Data)란?
결측·오타·이상치·중복·형식 불일치 등을 처리해 분석 가능한 형태로 다듬어진 데이터를 말합니다. 결측치 처리, 오타 수정, 단위 통일, 이상치 제거, 중복 제거 등을 거친 결과물이 곧 정제 데이터입니다.

데이터 가공의 두 기둥 — Aggregation & Merge

까다로운 데이터를 다루기 위해 우리는 두 가지 핵심 기법을 사용합니다.

📦 데이터 집계 (Aggregation)

관련된 데이터를 하나로 묶어 평균, 합계, 최댓값 같은 의미 있는 정보로 요약. → groupby()

🔗 데이터 결합 (Merge)

서로 다른 데이터를 공통의 기준으로 연결해 더 풍부한 정보를 만듦. → merge()
Part 3

2. 데이터 가공 — groupby & merge

집계로 요약하고, 결합으로 풍부하게

2-1. groupby() — 그룹별로 집계하기

groupby() 함수는 특정 열을 기준으로 데이터를 묶어 각 그룹별로 통계를 계산할 수 있게 해줍니다. 예를 들어 학급별 수학 점수의 평균을 구하고 싶다면 다음과 같이 작성합니다.

Pythondf1.groupby('class')['math'].mean()
Outputclass
1반    85.000000
2반    88.333333
Name: math, dtype: float64

이 코드는 "학급(class)을 기준으로 묶고, 수학 점수(math)의 평균(mean)을 계산하라"는 의미입니다.

집계 함수 (Aggregation Functions)

평균(mean()) 외에도 다양한 집계 함수가 있습니다.

함수의미언제 쓸까?
mean()평균"학급별 평균 점수가 얼마야?"
sum()합계"자치구별 총 매출이 얼마야?"
count()개수"각 학급에 몇 명이 있어?"
max()최댓값"학급별 최고 점수는?"
min()최솟값"학급별 최저 점수는?"
std()표준편차"학급별 점수가 얼마나 흩어져 있어?"

2-2. merge() — 데이터 결합하기

서로 다른 두 데이터를 하나로 합치려면 merge() 함수를 사용합니다. 다른 정보를 어떻게 합칠지를 how 매개변수로 4가지 방식 중 선택할 수 있어요.

Pythonmerged = df1.merge(df2, on='name', how='inner')
how설명이미지
inner양쪽에 모두 존재하는 행만 결합 (교집합)🔵 ∩ 🟢
left왼쪽 데이터프레임 기준, 오른쪽은 매칭만🔵 ⊃ 🟢
right오른쪽 데이터프레임 기준, 왼쪽은 매칭만🔵 ⊂ 🟢
outer모든 행 포함 (합집합), 빠진 값은 NaN으로 채움🔵 ∪ 🟢

예시 — 수학 점수와 영어 점수 결합

두 데이터프레임이 다음과 같이 있다고 합시다.

df1 (수학)

김민수, 이도현, 박지민, 정수아, 한지우의 수학 점수

df2 (영어)

김민수, 이도현, 박지민, 한지우, 송재현, 윤서연의 영어 점수

① Inner Join

Result — Inner Joinname   class_x   math   class_y   english
0  김민수    1반     85      1반       88
1  이도현    1반     92      1반       90
2  박지민    1반     78      1반       82
3  정수아    2반     94      2반       98

두 데이터프레임에서 둘 다 존재하는 이름만 결합 — 김민수, 이도현, 박지민, 정수아 4명만 출력됩니다.

② Left Join

Result — Left Joinname   class_x   math   class_y   english
0  김민수    1반     85      1반       88.0
1  이도현    1반     92      1반       90.0
2  박지민    1반     78      1반       82.0
3  한지우    1반     89      NaN       NaN
4  정수아    2반     94      2반       98.0

왼쪽 데이터프레임(수학)을 기준으로 결합. 한지우는 영어 데이터가 없어 NaN으로 채워집니다.

③ Outer Join

Result — Outer Joinname   class_x   math   class_y   english
0  김민수    1반     85.0    1반       88.0
1  이도현    1반     92.0    1반       90.0
2  박지민    1반     78.0    1반       82.0
3  송재현    NaN     NaN     2반       90.0
4  정수아    2반     94.0    2반       98.0
5  한지우    1반     89.0    NaN       NaN
6  윤서연    NaN     NaN     2반       79.0

모든 데이터를 포함. 빠진 값은 NaN(Not a Number)으로 채워집니다.

💡 NaN이란? "Not a Number"의 약자로, 값이 없거나 비어 있다는 표시입니다. 분석 시에는 .dropna().fillna()로 처리해야 해요.

2-3. 실습 — 버스 승객수 데이터 결합하기

이제 실습으로 가봅시다. 서울시 열린데이터 광장에서 "서울시 버스노선 정류장별 승하차 승객수 정보"를 검색하고, 기준_날짜20250815를 입력해 광복절 2025년 8월 15일의 데이터를 받아봅시다. 이 데이터를 다음 파일 이름으로 bus.csv로 저장합니다.

Python — Step 1: 두 CSV 불러오기import pandas as pd

# 한글 컬럼명이라서 인코딩은 EUC-KR (혹은 cp949)
bus = pd.read_csv("bus.csv", encoding="EUC-KR")
master = pd.read_csv("master.csv", encoding="EUC-KR")

print(bus.shape, master.shape)   # (425, 27) (2269, 4)
print(bus.columns.tolist()[:5])
# ['기준_날짜', '행정동_ID', '버스_승객_수', '버스_승객_수_00시', '버스_승객_수_01시']
Python — Step 2: 행정동_ID로 merge# bus에는 '행정동_ID' 만 있고 자치구 이름이 없음 → master로 보강
bus_merged = bus.merge(master, on="행정동_ID", how="inner")

print(bus_merged[["행정동_명칭", "자치구_명칭", "버스_승객_수"]].head())
Output — bus_merged.head()   행정동_ID  버스_승객_수  행정동_명칭  자치구_명칭  시도_명칭
0  11010530    16072    사직동      종로구    서울
1  11010540     2174    삼청동      종로구    서울
2  11010550     6393    부암동      종로구    서울
3  11010560     6300    평창동      종로구    서울
4  11010570     2260    무악동      종로구    서울

이제 bus_merged는 행정동 ID, 행정동 이름, 자치구 이름, 시간대별 승객수까지 한 데이터프레임에 담겨 있습니다. 행정동만 알 수 있던 bus에 자치구·시도 정보가 결합되었으니, 이제 "자치구별 평균 승객 수" 같은 그룹별 집계가 가능해진 셈이죠. 본격적으로 분석할 준비가 끝났어요.

📝 과제 1 미리보기 — 다음 챕터에서 다루게 될 과제 1은, 지금 만든 bus_merged를 활용해 자치구별 평균 버스 승객 수(groupby('자치구_명칭')['버스_승객_수'].mean())와 최대 버스 승객 수(.max())를 구하는 실습입니다. 즉 오늘 배운 merge → groupby 흐름을 그대로 따라가면 됩니다.
Part 4

3. 까다로운 데이터 살펴보기

loc · iloc · sort_values

3-1. loc vs iloc — 라벨로 vs 인덱스로

특정 행이나 열만 고르고 싶을 때, 우리는 두 가지 방식을 사용할 수 있습니다.

📌 loc — Label(이름)로 접근

"강남구의 행을 보여줘" → 이름으로 찾기

df.loc[행 이름, 열 이름]

📍 iloc — Index(번호)로 접근

"3번째 행을 보여줘" → 번호로 찾기

df.iloc[행 번호, 열 번호]

loc 사용하기 — "강남구의 버스 승객수가 6,000명 이상인 행만 보고 싶어"

Python# 두 조건을 & 연산자로 결합
mask = (bus_merged["자치구_명칭"] == "강남구") & (bus_merged["버스_승객_수"] >= 6000)
result = bus_merged.loc[mask, ["행정동_명칭", "버스_승객_수"]]
print(result)
Output (예시)      행정동_명칭   버스_승객_수
0      신사동      16072
2      논현1동       6393
3      논현2동       6300
5      삼성1동       7110
7  역삼1·2·3·4동    46764
8     역삼5,6동      9622
9      개포1동       9551
10     개포2동       9113
16     세곡동       14815
💡 꼭 기억하세요! 두 조건을 연결할 때는 and가 아니라 &를 씁니다. 그리고 각 조건은 반드시 괄호로 감싸야 해요.

iloc 사용하기 — "처음 5개 행, 처음 3개 열"

Pythonresult = bus_merged.iloc[:5, :3]   # 행 0~4, 열 0~2
print(result)
Output (예시)   행정동_ID    버스_승객_수    행정동_명칭
0   …            16072      신사동
1   …             2174      논현동
2   …             6393      논현1동
3   …             6300      논현2동
4   …             2260      삼성1동
📌 loc vs iloc 한 줄 정리
loc: df.loc[조건, 열 이름 목록] — 이름으로 골라야 할 때
iloc: df.iloc[행 번호, 열 번호] — 위치로 골라야 할 때

3-2. sort_values() — 데이터 정렬하기

데이터를 분석하기 위해서는, 우리가 원하는 기준으로 정렬할 줄 알아야 합니다. 가령, "어느 자치구의 승객 수가 가장 많지?", "어떤 행정동에 승객이 가장 적지?"와 같은 질문에 답하려면 데이터를 우리가 원하는 기준에 맞게 정렬해야 해요.

오름차순 vs 내림차순

↑ Ascending (오름차순)

작은 값부터 큰 값 순서로 정렬
5,1,4,3,2 → 1,2,3,4,5

↓ Descending (내림차순)

큰 값부터 작은 값 순서로 정렬
5,1,4,3,2 → 5,4,3,2,1

실습 — 버스 승객수가 많은 순으로 정렬

Python — 오름차순 (기본값)result = bus_merged.sort_values(by="버스_승객_수", ascending=True).head()
print(result)
Output     행정동_ID   버스_승객_수    자치구_명칭   행정동_명칭   시도_명칭
11    …          313       강남구       청담동       서울
311   …          831       송파구       방이1동      서울
12    …          912       강남구       대치동       서울
257   …          941       동작구       사당2동      서울
31    …         1142       강북구       삼각산동      서울
Python — 내림차순result = bus_merged.sort_values(by="버스_승객_수", ascending=False).head()
print(result)
Output     행정동_ID   버스_승객_수    자치구_명칭     행정동_명칭   시도_명칭
210   …          54011       서대문구    충현동           서울
7     …          46764       강남구      역삼1·2·3·4동   서울
301   …          33656       송파구      잠실본동         서울
204   …          32106       마포구      서교동          서울
286   …          29803       서초구      반포본동         서울

다중 정렬 — "자치구별로 묶고, 자치구 안에서는 승객수 많은 순"

Pythonresult = bus_merged.sort_values(
    by=["자치구_명칭", "버스_승객_수"],
    ascending=[True, False]    # 자치구는 가나다순, 승객수는 많은 순
).head(10)
print(result)

1순위는 자치구가 가나다순으로 오름차순으로 정렬되고, 2순위로 자치구 내에서 승객 수가 많은 순서로 내림차순 정렬됩니다.

💡 다중 정렬by=[열1, 열2]처럼 리스트로 넘기면 됩니다. ascending=[True, False]로 각 열의 정렬 방향을 따로 지정할 수 있어요.
Part 5

4. 기술통계와 시각화

데이터의 한가운데와 흩어짐을 숫자와 그림으로

4-1. 기술통계(Descriptive Statistics)란?

기술통계는 데이터를 요약해서 한눈에 파악하는 방법입니다. 여러 가지 데이터 속의 기본 분포를 한 줄 한 줄로 나타내야 하므로, 한번에 그 의미를 파악하기 쉽지 않은 경우가 많습니다. 그래서 우리는 데이터를 다음과 같이 두 가지 관점에서 살펴봅니다.

📍 데이터의 중심 (Center)

평균(Mean), 중앙값(Median), 최빈값(Mode)

↔️ 데이터의 흩어짐 (Spread)

분산(Variance), 표준편차(Standard Deviation)

4-2. 데이터의 중심 — 평균 · 중앙값 · 최빈값

평균 (Mean)
모든 값을 다 더한 뒤 개수로 나눈 값. 가장 흔히 쓰지만, 극단값(outlier)에 약함.
중앙값 (Median)
값을 작은 순서대로 정렬했을 때 정 가운데 있는 값. 데이터가 홀수 개면 가운데 1개, 짝수 개면 가운데 두 값의 평균.
최빈값 (Mode)
가장 자주 등장하는 값. 데이터에 따라 최빈값이 여러 개일 수도 있음. 평균·중앙값처럼 중심 경향을 나타내지만, 특히 범주형 데이터(예: 가장 많이 팔린 상품, 가장 인기 있는 색상)에서 의미가 큼.

예시 1 — 다음 7개 점수의 평균/중앙값/최빈값을 구해 봅시다

데이터: 6, 7, 8, 8, 8, 8, 20

통계량계산결과
평균 (Mean)(6+7+8+8+8+8+20) / 79.29
중앙값 (Median)정렬 후 (n+1)/2 = 4번째 값8
최빈값 (Mode)8이 4번 등장8
⚠️ 극단값(Extreme value)에 주의!
위 예시에서 평균(9.29)과 중앙값(8)이 다른 이유는 20이라는 큰 값 하나 때문이에요. 데이터가 한쪽으로 치우쳤을 때(skew), 평균만 보면 데이터의 모습이 왜곡될 수 있습니다. 이럴 땐 중앙값이 더 신뢰할 수 있어요.

예시 2 — 학생 A·B 그룹 키 비교 (이상치의 위력)

이상치가 한 명 끼면 평균이 어떻게 흔들리는지 직접 비교해 봅시다.

그룹키 데이터 (cm)평균중앙값
학생 A (6명) 168, 165, 170, 168, 167, 158 ≈ 166.0 167.5
학생 B (7명) 165, 170, 175, 170, 168, 158, 220 ≈ 175.1 170
📍 관찰 포인트
한 명의 키가 220 cm라는 극단값이 끼니 B 그룹의 평균은 175.1 cm로 크게 튀지만, 중앙값은 170 cm로 안정적이에요. 220 cm 한 명을 빼면 두 그룹의 키 분포는 거의 비슷한데, 평균만 보면 9 cm나 차이 나는 것처럼 오해할 수 있죠. 이상치가 있는 데이터에서는 중앙값이 더 솔직한 '중심'입니다.

코드로 계산하기 — 버스 승객수의 평균과 중앙값

Pythonmean_pa = bus_merged["버스_승객_수"].mean()
median_pa = bus_merged["버스_승객_수"].median()

print(f"평균: {mean_pa:.2f}")
print(f"중앙값: {median_pa:.2f}")
Output (예시)평균: 5678.73
중앙값: 3056.00
📊 강의 표본(전체 정류장 단위) 결과는 살짝 달라요
KAIST 강의 자료에서는 같은 코드가 평균 ≈ 7,567명 / 중앙값 ≈ 5,991명으로 출력돼요. 표본의 단위(행정동 vs 정류장)에 따라 절대값은 다르지만, "평균 > 중앙값"이라는 흐름은 동일합니다. 뒤에 나올 히스토그램 캡션의 7,567/5,991은 이 강의 표본 기준이에요.

평균이 중앙값보다 약 1,000 정도 큰 모습입니다. 즉, 일부 행정동에 승객이 매우 많이 몰리고 있다는 것을 알 수 있죠. 한 번 누가 그렇게 많은지 살펴볼까요?

Pythontop5 = bus_merged.sort_values(by="버스_승객_수", ascending=False).head(5)
print(top5[["행정동_명칭", "자치구_명칭", "버스_승객_수"]])
Output     행정동_명칭          자치구_명칭   버스_승객_수
210   충현동              서대문구       54011
7     역삼1·2·3·4동       강남구         46764
301   잠실본동             송파구         33656
204   서교동              마포구         32106
286   반포본동             서초구         29803

충현동, 역삼1·2·3·4동, 잠실본동, 서교동, 반포본동 등 5곳이 다른 지역의 승객수보다 압도적으로 높은 모습을 볼 수 있어요. 비단 평균값이 아닌 분포를 보고 알아내려고 해야 데이터를 더 잘 이해할 수 있다는 점을 알 수 있죠.

4-3. 데이터의 흩어짐 — 분산 · 표준편차

데이터의 중심이 어디인지를 봤다면, 이제 "데이터가 얼마나 흩어져 있는가?"를 봐야 해요. 같은 평균이어도 흩어짐 정도에 따라 데이터의 모습이 완전히 다릅니다.

둘 다 평균은 75점이지만, 1반은 "다 고만고만하다", 2반은 "차이가 크다"는 사실을 평균만 보고는 알 수 없어요. 이 흩어짐을 숫자로 표현한 것이 분산표준편차입니다.

분산 (Variance)

분산은 "각 데이터가 평균으로부터 얼마나 떨어져 있는가"를 나타내는 숫자입니다. 계산 공식은:

  1. 각 값에서 평균을 뺍니다.
  2. 그 차이를 제곱합니다 (음수를 양수로 만들기 위해).
  3. 제곱한 값들의 평균을 구합니다.
Pythonvariance = bus_merged["버스_승객_수"].var()
print(f"분산: {variance:.0f}")
Output (예시)분산: 34928857

분산이 34,928,857이라는 숫자로 나왔네요. 그런데 분산은 단위가 제곱이라 직관적이지 않아요. "평균에서 얼마나 떨어졌나"를 단위 그대로 보고 싶을 때 표준편차를 사용합니다.

표준편차 (Standard Deviation)

분산에 제곱근(√)을 씌운 값입니다. 데이터와 비슷한 크기로 돌아오죠.

Pythonstd = bus_merged["버스_승객_수"].std()
print(f"표준편차: {std:.0f}")
Output표준편차: 5910

이제 "평균 5,678명을 중심으로 약 ±5,910명 정도 차이가 난다"고 직관적으로 표현할 수 있게 됐어요.

🧮 표본/스케일이 달라지면 숫자도 달라집니다
KAIST 강의 자료 기준의 (전체 정류장 단위) 큰 표본에서는 같은 코드가 다음과 같이 출력돼요.
Output (강의 표본)분산: 345638057
표준편차: 18592
분산은 단위가 (명)²이라 숫자가 매우 크게 보이지만, √345,638,057 ≈ 18,592명으로 다시 명 단위로 돌려 놓으면 의미가 보입니다. 표본이 바뀌면 절대값은 바뀌지만 분산 → √ → 표준편차의 흐름은 동일해요.
📌 분산·표준편차 한 줄 정리
• 분산/표준편차가 크다 → 데이터가 평균에서 멀리 흩어져 있다 (변동성 ↑)
• 분산/표준편차가 작다 → 데이터가 평균 주변에 몰려 있다 (안정적)
표준편차 = √분산 → 단위가 원래 데이터와 같아져 해석이 쉬움

4-4. 시각화 — Matplotlib

지금까지 숫자(평균, 중앙값, 표준편차 등)로 데이터의 모습을 봤습니다. 하지만 사람은 그림으로 봐야 데이터의 직관적인 모습을 잘 이해할 수 있어요. 예를 들어 "표준편차가 5,910명이다"라는 문장보다 히스토그램 한 장이 훨씬 쉽게 와닿죠. 이때 사용하는 라이브러리가 Matplotlib입니다.

Matplotlib Anatomy of a figure
그림. Matplotlib 도면 구성요소 — Title / Axes / Legend / Markers / Line / X·Y axis label / Tick

Matplotlib의 구성요소 — Anatomy of a figure

위 그림에 나오는 핵심 부품을 한눈에 정리하면 다음과 같습니다. "도화지(Figure) → 그림 틀(Axes) → 그 안의 부품" 구조로 외우면 쉬워요.

구성요소역할
Figure (도화지)전체 그림판. A4 종이나 캔버스라고 생각하면 됩니다.
Axes (좌표 영역)도화지 위에 실제로 그래프가 그려지는 영역. 한 도화지 안에 여러 개 둘 수 있음.
Title (제목)그래프 위쪽 제목. 차트가 무엇을 보여주는지 한눈에 알리는 역할.
X axis label / Y axis labelX축, Y축이 어떤 변수·단위인지 표시하는 라벨.
Major tick / Major tick label주 눈금과 그 옆에 적히는 숫자(주 눈금 라벨). 큰 단위 눈금.
Minor tick / Minor tick label보조 눈금과 보조 눈금 라벨. 주 눈금 사이를 더 잘게 나누는 작은 눈금.
Spines (축 테두리)Axes를 둘러싼 네 변의 축선. 위·아래·왼쪽·오른쪽 테두리 선이에요.
Grid (격자)Axes 안에 그어진 가이드 격자선. 값을 읽기 편하게 도와줌. (plt.grid(True))
Line (line plot)점들을 이어 그린 선 그래프. 추세를 볼 때 유용.
Markers (scatter plot)각 데이터 포인트를 점·동그라미·세모 등으로 찍은 산점도 마커.
Legend (범례)여러 선·마커·막대를 한 그래프에 겹쳐 그렸을 때, 어떤 것이 무엇인지 구별해주는 작은 박스.
🎨 비유로 외우기 — Matplotlib은 미술 시간과 닮았습니다.
Figure: A4 도화지 한 장   • Axes: 그 위에 붙인 그림 틀(액자)   • Spines: 액자 테두리
Tick / Grid: 그림판에 미리 그어 둔 모눈   • Line / Marker: 실제로 그린 선과 점
Title / X·Y label / Legend: 그림 옆에 붙이는 설명 라벨

한글 폰트 설정 (필수!)

Matplotlib는 기본적으로 한글을 표시하지 못합니다. 그래프에 한글을 쓰면 "□□□"처럼 깨져 보이죠. 그리기 전에 반드시 한글 폰트를 설정해야 해요.

⚠️ 코랩(Colab)에서는 시스템에 한글 폰트가 없습니다. 그래서 코랩 환경에서는 먼저 나눔글꼴을 설치한 뒤, 폰트 캐시를 비우고 런타임을 다시 시작해야 해요. 아래 세 가지 방법 중 한 가지를 골라 쓰면 됩니다.
방법 0 — 코랩에 나눔글꼴 설치 (방법 1을 쓰기 전 1회 실행)# 셸 명령(! 로 시작): apt-get으로 나눔글꼴 설치 → 폰트 캐시 갱신 → matplotlib 캐시 비우기
!sudo apt-get install -y fonts-nanum
!sudo fc-cache -fv
!rm -rf ~/.cache/matplotlib
# ↑ 실행 후 [런타임 → 런타임 다시 시작] 한 번 해줘야 폰트가 인식돼요
방법 1 — 시스템에 폰트가 있을 때 (방법 0 후, 또는 로컬 환경)import matplotlib.pyplot as plt
plt.rc("font", family="NanumGothic")
plt.rc("axes", unicode_minus=False)   # 마이너스 부호 깨짐 방지
방법 2 — Colab에서 koreanize_matplotlib 사용 (가장 간편)!pip install -q koreanize-matplotlib
import matplotlib.pyplot as plt
import koreanize_matplotlib   # 이 한 줄만으로 한글 자동 설정 (방법 0·1을 모두 대체)
한 줄 가이드 — 코랩에서는 방법 2 (koreanize_matplotlib)가 가장 간편하고, 폰트가 끝까지 잡히지 않을 때만 방법 0 + 방법 1 조합으로 직접 설치하세요.

4-5. 히스토그램 — 분포 시각화

히스토그램(Histogram)은 분포를 살펴보기 위한 가장 기본적인 도구입니다.

Python — 기본 히스토그램plt.figure(figsize=(8, 4))
plt.hist(bus_merged["버스_승객_수"],
         bins=30, alpha=0.7, color="skyblue", label="버스 승객수")

plt.title("서울시 행정동별 버스 승객수 분포")
plt.xlabel("버스 승객 수 (명)")
plt.ylabel("행정동 수")
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()
서울시 행정동별 버스 승객수 분포 히스토그램
그림. 위 코드 실행 결과 — 대부분의 행정동이 5~6천 명 구간에 몰려 있고, 오른쪽으로 긴 꼬리를 가진 분포 (Right skew).

분포의 치우침 (Skew)

히스토그램을 그리면 데이터가 어느 한쪽으로 치우쳐 있는 경우가 많습니다. 이를 치우침(Skew)이라고 해요.

Right-skewed + Left-skewed distribution
그림. (a) Right-skewed — 최빈값(Mode) < 중앙값(Median) < 평균(Mean) / (b) Left-skewed — 평균 < 중앙값 < 최빈값

↘️ Right-skewed (오른쪽 꼬리)

  • 분포가 왼쪽에 몰려 있고 오른쪽으로 긴 꼬리
  • 아주 큰 값(이상치) 일부가 평균을 끌어올림
  • 관계: Mode < Median < Mean
  • 예: 소득, 인구밀도, 버스 승객수

↙️ Left-skewed (왼쪽 꼬리)

  • 분포가 오른쪽에 몰려 있고 왼쪽으로 긴 꼬리
  • 아주 작은 값 일부가 평균을 끌어내림
  • 관계: Mean < Median < Mode
  • 예: 시험에서 만점에 가까운 점수가 많은 경우

평균/중앙값을 수직선으로 겹쳐 그리기

Pythonmean_val = bus_merged["버스_승객_수"].mean()
median_val = bus_merged["버스_승객_수"].median()

plt.figure(figsize=(8, 4))
plt.hist(bus_merged["버스_승객_수"],
         bins=30, alpha=0.7, color="skyblue", label="버스 승객수")

# 평균 수직선 (빨간 점선)
plt.axvline(mean_val, color="red", linestyle="--", linewidth=2,
            label=f"평균: {mean_val:.0f}")
# 중앙값 수직선 (주황 실선)
plt.axvline(median_val, color="orange", linestyle="-", linewidth=2,
            label=f"중앙값: {median_val:.0f}")

plt.title("서울시 행정동별 버스 승객수 분포")
plt.xlabel("버스 승객 수 (명)")
plt.ylabel("행정동 수")
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()

실제로 그려보면 Right skew의 형태로, 분포가 왼쪽으로 몰려 있고 오른쪽으로 긴 꼬리를 가진 모습이 나타납니다. 즉 최빈값(Mode) < 중앙값(Median) < 평균(Mean)의 관계가 보일 거예요.

히스토그램 + 평균·중앙값 수직선
그림. 위 코드 실행 결과 — 빨강 점선(평균 7,567명), 주황 점선(중앙값 5,991명). 평균이 중앙값보다 오른쪽에 있는 것은 Right skew의 전형적인 특징.
💡 plt.axvline() — 그래프 위에 수직선을 그릴 때 사용합니다.
linestyle 로 선 스타일(점선, 실선 등)을 지정
linewidth 로 선 굵기를 지정
color 로 색깔을 지정
Part 6

5. 마무리하며

2차시 핵심 정리 + 다음 시간 예고

오늘 배운 도구들 — 한눈에 정리

도구역할예시 코드
groupby()그룹별로 집계df.groupby('자치구')['승객수'].mean()
merge()두 데이터프레임 결합df1.merge(df2, on='ID', how='inner')
loc이름(라벨)으로 행/열 선택df.loc[df['자치구']=='강남구']
iloc번호(인덱스)로 행/열 선택df.iloc[:5, :3]
sort_values()특정 열 기준 정렬df.sort_values('승객수', ascending=False)
mean / median / mode중심 측정df['승객수'].mean()
var / std흩어짐 측정df['승객수'].std()
plt.hist()히스토그램으로 분포 시각화plt.hist(df['승객수'], bins=30)
plt.axvline()수직선 그리기 (평균/중앙값 표시)plt.axvline(mean_val, color='red')

🎯 데이터 사이언스의 시작은 결국 데이터 가공입니다

오늘 배운 groupby, merge, loc/iloc, sort_values는 데이터 사이언티스트가 매일 사용하는 도구입니다. 그리고 기술통계와 히스토그램은 데이터의 모양을 빠르게 이해하는 첫 번째 시각화예요. 이 도구들이 손에 익으면, 어떤 새로운 데이터셋을 만나도 "일단 한 번 살펴보자"고 자신 있게 말할 수 있게 됩니다.

다음 시간 예고

다음 시간에는 오늘 익힌 도구들을 토대로 더 복잡한 시각화 — 산점도(scatter plot), 막대 그래프(bar chart), 시계열(time series) 등 다양한 시각화 방법을 활용해 도시 데이터를 이야기하는 방법을 3차시에서 배웁니다.

Part 7

과제 안내

서울시 자치구별 버스 승객수 데이터 분석 (3가지 과제)

이번 차시 과제는 서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용한 3가지입니다. 첨부된 파일(bus.csv, master.csv)을 다운로드하여 사용해 주세요. 과제가 모두 완료되면 1개의 압축파일(zip)으로 제출합니다.

📅 1차 제출 마감: 2026년 5월 3일(일) 23:59
1차 기간 내 제출 시 원 점수 100% / 2차 기간 내 제출 시 원 점수 80% 적용

과제 1. 자치구별 평균 버스 승객 수 구하기

서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용하여 다음 질문에 답해보고, 코드와 결과값을 캡쳐하여 같이 제출하세요.

📝 1-1. '자치구_명칭' 컬럼과 groupby() 함수를 이용해서 '자치구별 평균 버스 승객 수'를 구하고, '강남구'의 평균 승객 수는 몇 명인지 찾아보세요.
접근 방법:
1. bus_merged.groupby('자치구_명칭')['버스_승객_수'].mean()
2. 결과에서 강남구 행을 찾기 → result.loc['강남구']
📝 1-2. 자치구별 집계 방식을 mean() 대신 max()로 바꿔서 '자치구별 최대 버스 승객 수'를 구하고, 그 중 승객 수가 가장 많았던 자치구는 어디인지 찾아보세요.
접근 방법:
1. bus_merged.groupby('자치구_명칭')['버스_승객_수'].max()
2. .sort_values(ascending=False).head(1) 로 1위 찾기
3. 또는 .idxmax() 로 자치구 이름만 바로 추출

과제 2. 특정 조건을 만족하는 데이터 추출하기

서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용하여 loc 함수와 sort_values 함수를 각각 이용해 문제에서 요구하는 조건에 맞는 데이터를 추출해보세요. 코드와 결과값을 캡쳐하여 같이 제출하세요.

📝 2-1. '자치구_명칭'이 강남구이거나 '버스_승객_수'가 30000명 이상인 데이터의 '행정동_명칭', '버스_승객_수'loc 함수를 이용해 추출해보세요.
접근 방법:
mask = (bus_merged['자치구_명칭'] == '강남구') | (bus_merged['버스_승객_수'] >= 30000)
result = bus_merged.loc[mask, ['행정동_명칭', '버스_승객_수']]

💡 주의: "이거나"는 | (or)이지 &(and)가 아닙니다!
📝 2-2. sort_values 함수를 이용해서 다음 순서로 정렬한 후, 가장 첫 번째에 나오는 행정동의 이름과 버스 승객 수를 찾아보세요.
정렬 순서: ① 자치구_명칭 (가나다순) ② 행정동_명칭 (가나다순)
접근 방법:
result = bus_merged.sort_values(by=['자치구_명칭', '행정동_명칭'], ascending=[True, True])
print(result[['자치구_명칭', '행정동_명칭', '버스_승객_수']].head(1))

과제 3. 영등포구의 버스 승객 수 히스토그램 그려보기

bus_merged의 데이터에서 '자치구_명칭''영등포구'인 데이터를 추출하여 영등포구의 버스 승객 수를 히스토그램으로 그려보고, 평균과 중앙값을 구하여 수직선을 함께 표시해보세요.

📝 3-1. 영등포구의 분포는 어떤 특징이 있나요? 히스토그램에서 관찰할 수 있는 특징을 상세하게 적어보세요.
관찰 포인트 (예시):
• Right skew인가, Left skew인가? 아니면 대칭에 가까운가?
• 평균과 중앙값의 차이는 얼마인가? 어느 쪽이 더 큰가?
• 극단값(매우 큰 승객수)이 보이는가?
• 어떤 값 구간에 가장 많이 몰려 있는가?
💡 힌트: 코드 작성 → 캡처 → 히스토그램 보면서 위 4가지 질문에 답하면 됩니다.

과제 3 코드 가이드

Python — 영등포구만 추출 → 히스토그램# 1. 영등포구 데이터만 추출
ydp = bus_merged.loc[bus_merged["자치구_명칭"] == "영등포구"]

# 2. 평균, 중앙값 계산
mean_val = ydp["버스_승객_수"].mean()
median_val = ydp["버스_승객_수"].median()

# 3. 히스토그램 + 수직선
plt.figure(figsize=(8, 4))
plt.hist(ydp["버스_승객_수"], bins=15, alpha=0.7, color="skyblue")
plt.axvline(mean_val, color="red", linestyle="--", linewidth=2,
            label=f"평균: {mean_val:.0f}")
plt.axvline(median_val, color="orange", linestyle="-", linewidth=2,
            label=f"중앙값: {median_val:.0f}")
plt.title("영등포구 행정동별 버스 승객수 분포")
plt.xlabel("버스 승객 수 (명)")
plt.ylabel("행정동 수")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

제출 체크리스트

Junior KAIST 2026년 1학기 · 데이터 2차시 수업자료 · 까다로운 데이터 요리하기