지난 시간 우리는 데이터 사이언스의 전체 그림을 그렸다면, 이번 시간에는 "데이터를 다루는 손기술"을 본격적으로 익힙니다. 현실의 데이터는 깨끗하지 않아요. 컬럼 이름이 한글이고 인코딩이 EUC-KR이며 결측값이 섞여 있고 형식도 들쭉날쭉합니다. 이 까다로운 데이터를 요리해서 우리가 원하는 답을 얻는 과정 — 그게 이번 차시의 핵심입니다.
집에서 요리할 때 양파는 껍질을 벗기고 썰고, 당근은 흙을 씻고 다듬고, 고기는 핏물을 빼고 자릅니다. 손질 없이 통째로 냄비에 넣으면 원하는 요리가 안 나오죠. 데이터도 똑같습니다. 현실의 데이터는 시장에서 갓 사온 재료처럼 손질이 필요합니다.
요리사 = 데이터 과학자. 재료 손질 = 데이터 전처리. 칼·도마 = 판다스 등 데이터 핸들링 도구.
서울시 전체 데이터인데 우리 동네만 보고 싶을 때
일별 데이터인데 월별 평균이 필요할 때
인구수와 면적이 따로 있는데 인구밀도가 필요할 때
숫자만 나열되어 있어 의미 파악이 어려울 때
📦 Data Preprocessing (데이터 전처리)
분석 목적에 맞춰 데이터의 특성을 이해하고 가공하는 전체 과정. 결측·오타·형식 정리부터 새 변수 생성까지.
🛠 Data Handling (데이터 핸들링)
판다스 등으로 데이터를 자유자재로 다루는 손기술. 추출·필터·집계·결합·정렬을 자유롭게.
복잡해 보이는 숫자 뭉치를 명확하고 의미 있는 정보로 변화시키는 마법 같은 과정. 데이터 사이언스의 요리사가 될 준비가 되었나요?
이번 차시 과제는 서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용한 3가지입니다. 첨부된 두 파일을 다운로드하여 사용하세요. 모든 과제는 코드 + 결과값 캡쳐를 함께 제출하고, 최종 결과는 1개의 압축 파일(zip)로 묶어서 냅니다. 세부 풀이 가이드와 코드 템플릿은 마지막 "과제 안내" 섹션에 모두 정리되어 있습니다.
bus.csv (서울시 자치구별 버스 승객수) · master.csv (자치구·행정동 매핑) → merge 후 bus_merged 사용
mean() → max() 로 바꿔, 최대 승객 수가 가장 많은 자치구
"Garbage in, garbage out"이라는 말이 있습니다. 쓰레기를 넣으면 쓰레기가 나온다는 뜻이죠. 데이터 사이언스에서 가장 중요한 원칙 중 하나입니다. 데이터의 질이 분석 결과의 질을 결정합니다. 아무리 뛰어난 분석 기법이 있더라도, 다루는 데이터가 엉망이라면 좋은 결과를 기대할 수 없어요.
현실에서 우리가 만나는 데이터는 대부분 날 데이터(Raw Data)입니다. 즉, 가공되지 않은 상태의 데이터입니다. 결측값이 있고, 중복이 있고, 표준화되지 않은 형식이 섞여 있는 데이터를 만나게 됩니다.
| 문제 유형 | 짧은 정의 / 예시 |
|---|---|
| 결측 값 (Missing) | 값이 비어 있거나 측정되지 않아 모르는 칸 (NaN) |
| 불일치한 값 | 표기 방식·단위가 통일되지 않은 값 (예: kg vs g, "서울" vs "Seoul") |
| 잘못된 값·오타 | 실제와 다르게 기재된 값 (예: 나이가 999, 음수) |
| 이상한 값 (Outlier) | 다른 값들에 비해 너무 크거나 너무 작아 흐름과 어긋나는 값 |
| 중복된 값 | 같은 의미의 값이 두 번 이상 입력된 경우 |
| 다른 형식의 값 | 날짜가 '2023-01-01', '23.1.1', '2023년 1월 1일' 등 다양한 형식으로 섞여 있음 |
| 불필요한 값 | 분석 목적과 관련 없는 컬럼·행이 데이터 더미에 포함되어 있음 |
| 분류되지 않은 값 | 카테고리(범주) 분류가 안 되어 있어 그룹별 비교가 어려움 |
S-DoT(Smart Seoul Data of Things)은 서울시가 도시 데이터를 수집하기 위해 설치한 IoT 센서입니다. 미세먼지, 소음, 온습도, 자외선 등 다양한 도시 환경 데이터를 실시간으로 수집해 무료로 제공합니다.
그런데 S-DoT 데이터를 받아 열어보면 어떨까요? 아래는 실제 S-DoT 환경 정보 데이터의 일부입니다.
| SDOT_ID | 설치일 | 설치 위치 | 자치구 | 동 | 온도(°C) | 습도(%) |
|---|---|---|---|---|---|---|
| SDOT001 | 2023-09-2 | main street | Eunpyeong | Jingwan-d | 16.8 | NaN |
| SDOT001 | 2023-09-2 | residential | Gwanak-gu | Sinllim-do | NaN | NaN |
| SDOT001 | 2019-09-2 | traditional | Gangbuk- | Mia-dong | 20.8 | 78 |
| SDOT001 | 2019-09-2 | traditional | Yangcheon | Sinwol1(Il) | 22.2 | 72 |
| SDOT001 | 2023-09-2 | residential | Gwanak-gu | Sinllim-do | 18.3 | NaN |
| SDOT001 | 2019-09-2 | traditional | Yangcheon | Sinwol1(Il) | 21.5 | 70 |
설치 일자, 설치 위치, 자치구가 영문으로 입력되어 있고, 같은 자치구나 같은 위치 데이터가 수집되지 않은 곳이 보입니다. 어떤 곳은 한꺼번에 데이터가 수집되어 있는 경우도 있죠. 이렇게 우리가 사용하기에는 까다로운 데이터를 가공해야 합니다.
"Garbage in, Garbage out"의 대표적인 사례입니다. 어떻게 좋은 분석을 하고, 의미 있는 결과를 도출할지 결정하는 가장 중요한 시작점이죠.
까다로운 데이터를 다루기 위해 우리는 두 가지 핵심 기법을 사용합니다.
groupby() 함수는 특정 열을 기준으로 데이터를 묶어 각 그룹별로 통계를 계산할 수 있게 해줍니다. 예를 들어 학급별 수학 점수의 평균을 구하고 싶다면 다음과 같이 작성합니다.
Pythondf1.groupby('class')['math'].mean()
Outputclass
1반 85.000000
2반 88.333333
Name: math, dtype: float64
이 코드는 "학급(class)을 기준으로 묶고, 수학 점수(math)의 평균(mean)을 계산하라"는 의미입니다.
평균(mean()) 외에도 다양한 집계 함수가 있습니다.
| 함수 | 의미 | 언제 쓸까? |
|---|---|---|
mean() | 평균 | "학급별 평균 점수가 얼마야?" |
sum() | 합계 | "자치구별 총 매출이 얼마야?" |
count() | 개수 | "각 학급에 몇 명이 있어?" |
max() | 최댓값 | "학급별 최고 점수는?" |
min() | 최솟값 | "학급별 최저 점수는?" |
std() | 표준편차 | "학급별 점수가 얼마나 흩어져 있어?" |
서로 다른 두 데이터를 하나로 합치려면 merge() 함수를 사용합니다. 다른 정보를 어떻게 합칠지를 how 매개변수로 4가지 방식 중 선택할 수 있어요.
Pythonmerged = df1.merge(df2, on='name', how='inner')
| how | 설명 | 이미지 |
|---|---|---|
| inner | 양쪽에 모두 존재하는 행만 결합 (교집합) | 🔵 ∩ 🟢 |
| left | 왼쪽 데이터프레임 기준, 오른쪽은 매칭만 | 🔵 ⊃ 🟢 |
| right | 오른쪽 데이터프레임 기준, 왼쪽은 매칭만 | 🔵 ⊂ 🟢 |
| outer | 모든 행 포함 (합집합), 빠진 값은 NaN으로 채움 | 🔵 ∪ 🟢 |
두 데이터프레임이 다음과 같이 있다고 합시다.
김민수, 이도현, 박지민, 정수아, 한지우의 수학 점수
김민수, 이도현, 박지민, 한지우, 송재현, 윤서연의 영어 점수
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명만 출력됩니다.
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으로 채워집니다.
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)으로 채워집니다.
이제 실습으로 가봅시다. 서울시 열린데이터 광장에서 "서울시 버스노선 정류장별 승하차 승객수 정보"를 검색하고, 기준_날짜에 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에 자치구·시도 정보가 결합되었으니, 이제 "자치구별 평균 승객 수" 같은 그룹별 집계가 가능해진 셈이죠. 본격적으로 분석할 준비가 끝났어요.
특정 행이나 열만 고르고 싶을 때, 우리는 두 가지 방식을 사용할 수 있습니다.
"강남구의 행을 보여줘" → 이름으로 찾기
df.loc[행 이름, 열 이름]
"3번째 행을 보여줘" → 번호로 찾기
df.iloc[행 번호, 열 번호]
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
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동
df.loc[조건, 열 이름 목록] — 이름으로 골라야 할 때df.iloc[행 번호, 열 번호] — 위치로 골라야 할 때
데이터를 분석하기 위해서는, 우리가 원하는 기준으로 정렬할 줄 알아야 합니다. 가령, "어느 자치구의 승객 수가 가장 많지?", "어떤 행정동에 승객이 가장 적지?"와 같은 질문에 답하려면 데이터를 우리가 원하는 기준에 맞게 정렬해야 해요.
5,1,4,3,2 → 1,2,3,4,5
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]로 각 열의 정렬 방향을 따로 지정할 수 있어요.
기술통계는 데이터를 요약해서 한눈에 파악하는 방법입니다. 여러 가지 데이터 속의 기본 분포를 한 줄 한 줄로 나타내야 하므로, 한번에 그 의미를 파악하기 쉽지 않은 경우가 많습니다. 그래서 우리는 데이터를 다음과 같이 두 가지 관점에서 살펴봅니다.
데이터: 6, 7, 8, 8, 8, 8, 20
| 통계량 | 계산 | 결과 |
|---|---|---|
| 평균 (Mean) | (6+7+8+8+8+8+20) / 7 | 9.29 |
| 중앙값 (Median) | 정렬 후 (n+1)/2 = 4번째 값 | 8 |
| 최빈값 (Mode) | 8이 4번 등장 | 8 |
이상치가 한 명 끼면 평균이 어떻게 흔들리는지 직접 비교해 봅시다.
| 그룹 | 키 데이터 (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 |
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
평균이 중앙값보다 약 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곳이 다른 지역의 승객수보다 압도적으로 높은 모습을 볼 수 있어요. 비단 평균값이 아닌 분포를 보고 알아내려고 해야 데이터를 더 잘 이해할 수 있다는 점을 알 수 있죠.
데이터의 중심이 어디인지를 봤다면, 이제 "데이터가 얼마나 흩어져 있는가?"를 봐야 해요. 같은 평균이어도 흩어짐 정도에 따라 데이터의 모습이 완전히 다릅니다.
둘 다 평균은 75점이지만, 1반은 "다 고만고만하다", 2반은 "차이가 크다"는 사실을 평균만 보고는 알 수 없어요. 이 흩어짐을 숫자로 표현한 것이 분산과 표준편차입니다.
분산은 "각 데이터가 평균으로부터 얼마나 떨어져 있는가"를 나타내는 숫자입니다. 계산 공식은:
Pythonvariance = bus_merged["버스_승객_수"].var() print(f"분산: {variance:.0f}")
Output (예시)분산: 34928857
분산이 34,928,857이라는 숫자로 나왔네요. 그런데 분산은 단위가 제곱이라 직관적이지 않아요. "평균에서 얼마나 떨어졌나"를 단위 그대로 보고 싶을 때 표준편차를 사용합니다.
분산에 제곱근(√)을 씌운 값입니다. 데이터와 비슷한 크기로 돌아오죠.
Pythonstd = bus_merged["버스_승객_수"].std() print(f"표준편차: {std:.0f}")
Output표준편차: 5910
이제 "평균 5,678명을 중심으로 약 ±5,910명 정도 차이가 난다"고 직관적으로 표현할 수 있게 됐어요.
Output (강의 표본)분산: 345638057
표준편차: 18592
분산은 단위가 (명)²이라 숫자가 매우 크게 보이지만, √345,638,057 ≈ 18,592명으로 다시 명 단위로 돌려 놓으면 의미가 보입니다. 표본이 바뀌면 절대값은 바뀌지만 분산 → √ → 표준편차의 흐름은 동일해요.
지금까지 숫자(평균, 중앙값, 표준편차 등)로 데이터의 모습을 봤습니다. 하지만 사람은 그림으로 봐야 데이터의 직관적인 모습을 잘 이해할 수 있어요. 예를 들어 "표준편차가 5,910명이다"라는 문장보다 히스토그램 한 장이 훨씬 쉽게 와닿죠. 이때 사용하는 라이브러리가 Matplotlib입니다.
위 그림에 나오는 핵심 부품을 한눈에 정리하면 다음과 같습니다. "도화지(Figure) → 그림 틀(Axes) → 그 안의 부품" 구조로 외우면 쉬워요.
| 구성요소 | 역할 |
|---|---|
| Figure (도화지) | 전체 그림판. A4 종이나 캔버스라고 생각하면 됩니다. |
| Axes (좌표 영역) | 도화지 위에 실제로 그래프가 그려지는 영역. 한 도화지 안에 여러 개 둘 수 있음. |
| Title (제목) | 그래프 위쪽 제목. 차트가 무엇을 보여주는지 한눈에 알리는 역할. |
| X axis label / Y axis label | X축, 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는 기본적으로 한글을 표시하지 못합니다. 그래프에 한글을 쓰면 "□□□"처럼 깨져 보이죠. 그리기 전에 반드시 한글 폰트를 설정해야 해요.
방법 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을 모두 대체)
히스토그램(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()
히스토그램을 그리면 데이터가 어느 한쪽으로 치우쳐 있는 경우가 많습니다. 이를 치우침(Skew)이라고 해요.
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)의 관계가 보일 거예요.
linestyle 로 선 스타일(점선, 실선 등)을 지정linewidth 로 선 굵기를 지정color 로 색깔을 지정
| 도구 | 역할 | 예시 코드 |
|---|---|---|
| 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차시에서 배웁니다.
이번 차시 과제는 서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용한 3가지입니다. 첨부된 파일(bus.csv, master.csv)을 다운로드하여 사용해 주세요. 과제가 모두 완료되면 1개의 압축파일(zip)으로 제출합니다.
서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용하여 다음 질문에 답해보고, 코드와 결과값을 캡쳐하여 같이 제출하세요.
'자치구_명칭' 컬럼과 groupby() 함수를 이용해서 '자치구별 평균 버스 승객 수'를 구하고, '강남구'의 평균 승객 수는 몇 명인지 찾아보세요.bus_merged.groupby('자치구_명칭')['버스_승객_수'].mean()result.loc['강남구']
mean() 대신 max()로 바꿔서 '자치구별 최대 버스 승객 수'를 구하고, 그 중 승객 수가 가장 많았던 자치구는 어디인지 찾아보세요.bus_merged.groupby('자치구_명칭')['버스_승객_수'].max().sort_values(ascending=False).head(1) 로 1위 찾기.idxmax() 로 자치구 이름만 바로 추출
서울시 자치구별 버스 승객수 데이터(bus_merged)를 활용하여 loc 함수와 sort_values 함수를 각각 이용해 문제에서 요구하는 조건에 맞는 데이터를 추출해보세요. 코드와 결과값을 캡쳐하여 같이 제출하세요.
'자치구_명칭'이 강남구이거나 '버스_승객_수'가 30000명 이상인 데이터의 '행정동_명칭', '버스_승객_수'를 loc 함수를 이용해 추출해보세요.mask = (bus_merged['자치구_명칭'] == '강남구') | (bus_merged['버스_승객_수'] >= 30000)result = bus_merged.loc[mask, ['행정동_명칭', '버스_승객_수']]| (or)이지 &(and)가 아닙니다!
sort_values 함수를 이용해서 다음 순서로 정렬한 후, 가장 첫 번째에 나오는 행정동의 이름과 버스 승객 수를 찾아보세요.result = bus_merged.sort_values(by=['자치구_명칭', '행정동_명칭'], ascending=[True, True])print(result[['자치구_명칭', '행정동_명칭', '버스_승객_수']].head(1))
bus_merged의 데이터에서 '자치구_명칭'이 '영등포구'인 데이터를 추출하여 영등포구의 버스 승객 수를 히스토그램으로 그려보고, 평균과 중앙값을 구하여 수직선을 함께 표시해보세요.
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()