콘텐츠로 이동

2차시: Pandas로 데이터와 대화하기 - DataFrame 탐색과 전처리

⏰ 80분 · DataFrame · 결측치 처리 · 조건 필터링 · groupby · 난이도 ●●○○○

학습목표: 타이타닉 데이터셋을 불러와 탐색하고, 결측치를 처리하며, 그룹별 생존율을 계산할 수 있습니다.

오늘의 질문: “타이타닉 침몰 사고에서 살아남을 확률이 가장 높았던 사람은 누구였을까요? 그 답을 데이터로 찾을 수 있을까요?”


엑셀 시트처럼 생긴 2차원 자료구조를 파이썬에서 자유자재로 다루는 법

head, info, describe로 첫인상 파악하고 결측치·중복을 정리

“여성 1등석 승객의 생존율”처럼 질문을 코드로 바꾸는 훈련

생존율이 가장 높은 집단을 직접 찾아내는 개인 과제


1단계: 도입 & DataFrame 첫 만남 (15분)

섹션 제목: “1단계: 도입 & DataFrame 첫 만남 (15분)”

Pandas가 왜 필요한지 공감하고, Series와 DataFrame의 구조를 직접 만들어 봅니다. NumPy와 무엇이 다른지 비교합니다.

2단계: 타이타닉 데이터 탐색 (20분)

섹션 제목: “2단계: 타이타닉 데이터 탐색 (20분)”

실제 타이타닉 CSV를 불러와 head(), info(), describe()로 데이터의 첫인상을 파악합니다.

3단계: 결측치 처리 & 필터링 (20분)

섹션 제목: “3단계: 결측치 처리 & 필터링 (20분)”

결측치를 탐지하고 fillna, dropna로 처리합니다. 불리언 인덱싱으로 “여성 승객만” 같은 조건 추출을 연습합니다.

groupby를 사용해 성별·객실등급별 생존율을 계산합니다.

5단계: 미니 챌린지 & 정리 (10분)

섹션 제목: “5단계: 미니 챌린지 & 정리 (10분)”

“생존율이 가장 높은 집단은?”을 개인별로 해결하고, 결과를 공유한 뒤 형성 평가로 마무리합니다.


1. 🧠 DataFrame, 파이썬 속의 엑셀 시트

섹션 제목: “1. 🧠 DataFrame, 파이썬 속의 엑셀 시트”

엑셀을 써본 경험을 떠올려 보세요. 행에는 사람이, 열에는 이름·나이·점수가 있고, 원하는 열만 뽑거나 조건에 맞는 행만 필터링합니다. Pandas의 DataFrame은 바로 이 엑셀 시트를 파이썬 코드로 조작할 수 있게 만든 자료구조입니다.

DataFrame은 2차원 표 형태의 자료구조이고, 그 한 열을 떼어내면 Series라는 1차원 자료구조가 됩니다. 둘 모두 인덱스(index)라는 행 이름표를 갖는다는 점이 리스트나 NumPy 배열과 다른 점입니다.

flowchart LR
  A[Series<br/>1차원 + 인덱스] --> C[DataFrame<br/>2차원 표]
  B[여러 Series를<br/>열로 결합] --> C
  C --> D[행: 하나의 관측치<br/>열: 하나의 변수]
항목NumPy ndarrayPandas DataFrame
차원N차원2차원 (주로)
데이터 타입한 종류만열마다 다른 타입 가능
이름표없음 (정수 인덱스만)행/열 이름 가능
주 용도수치 계산표 형태 데이터 분석

숫자 계산만 빠르게 한다면 NumPy가 유리하지만, “이름이 Kim인 사람의 점수”처럼 의미 있는 이름표로 데이터를 다룰 때는 Pandas가 훨씬 편합니다.

실행 환경: Python 3.10+, pip install pandas 필요

import pandas as pd # 관례적으로 pd로 줄여 씁니다
# 딕셔너리로 간단히 DataFrame 만들기 (키=열 이름, 값=열 데이터)
data = {
"이름": ["지민", "하윤", "서준"],
"나이": [15, 16, 15],
"점수": [88, 92, 75]
}
df = pd.DataFrame(data) # <- 여기가 DataFrame 생성
print(df)
이름 나이 점수
0 지민 15 88
1 하윤 16 92
2 서준 15 75

출력 왼쪽의 0, 1, 2는 자동으로 붙은 행 인덱스입니다. 위 코드 10번째 줄 pd.DataFrame(data)가 바로 딕셔너리를 표로 변환하는 과정입니다.

점수_시리즈 = df["점수"] # <- 대괄호로 열 이름을 지정하면 Series 반환
print(점수_시리즈)
print("타입:", type(점수_시리즈).__name__)
0 88
1 92
2 75
Name: 점수, dtype: int64
타입: Series

DataFrame에서 df["열이름"]으로 꺼낸 한 열은 Series가 됩니다. 값(88, 92, 75)에 인덱스(0, 1, 2)가 따라붙은 형태입니다.


2. 🔍 데이터의 첫인상 파악하기 — head, info, describe

섹션 제목: “2. 🔍 데이터의 첫인상 파악하기 — head, info, describe”

낯선 데이터를 받으면 먼저 어떤 데이터인지 훑어보는 작업이 필요합니다. 처음 보는 요리를 먹기 전에 색깔과 냄새를 맡아보는 것과 같습니다. Pandas는 이 “첫인상 파악”을 위한 세 가지 도구를 제공합니다.

  • head(n): 상위 n개 행을 보여줍니다 (기본 5개). 데이터 생김새 확인용
  • info(): 행·열 개수, 각 열의 데이터 타입, 결측치 개수를 요약
  • describe(): 숫자 열의 평균·표준편차·최소·최대 등 기초 통계

타이타닉 데이터셋은 1912년 침몰한 타이타닉호 승객 정보를 담고 있습니다. 실습용으로 seaborn 라이브러리에 내장되어 있어 인터넷 연결만 되면 바로 쓸 수 있습니다.

import pandas as pd
import seaborn as sns # 실습용 데이터셋 제공 라이브러리
# 타이타닉 데이터 불러오기
titanic = sns.load_dataset("titanic")
# 상위 5개 행만 확인 (전체를 다 출력하면 너무 기니까)
print(titanic.head())
survived pclass sex age sibsp parch fare embarked class ...
0 0 3 male 22.0 1 0 7.2500 S Third ...
1 1 1 female 38.0 1 0 71.2833 C First ...
2 1 3 female 26.0 0 0 7.9250 S Third ...
3 0 3 male 35.0 1 0 53.1000 S First ...
4 0 3 male 35.0 0 0 8.0500 S Third ...

주요 열의 의미는 다음과 같습니다.

열 이름의미
survived생존 여부 (0=사망, 1=생존)
pclass객실 등급 (1=1등석, 2=2등석, 3=3등석)
sex성별
age나이
fare요금
embarked탑승 항구 (S=사우샘프턴, C=셰르부르, Q=퀸스타운)
titanic.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 15 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 survived 891 non-null int64
1 pclass 891 non-null int64
2 sex 891 non-null object
3 age 714 non-null float64 <- 결측치 존재!
...
7 embarked 889 non-null object <- 결측치 존재!

info() 출력에서 중요한 정보는 Non-Null Count입니다. 전체 891행인데 age는 714개만 값이 있으므로, 891 - 714 = 177개의 결측치가 있다는 뜻입니다.

# 숫자 열만 요약 통계를 보여줍니다
print(titanic.describe())
survived pclass age sibsp parch fare
count 891.000000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 0.383838 2.308642 29.699118 0.523008 0.381594 32.204208
std 0.486592 0.836071 14.526497 1.102743 0.806057 49.693429
min 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 0.000000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 1.000000 3.000000 38.000000 1.000000 0.000000 31.000000
max 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

survived의 평균(mean)이 0.38이라는 건 전체 생존율이 약 38%였다는 의미입니다. age의 최솟값이 0.42(약 5개월)이고 최댓값이 80이라는 정보도 한눈에 보입니다.


3. 🧹 결측치 처리와 조건 필터링

섹션 제목: “3. 🧹 결측치 처리와 조건 필터링”

데이터에 빈칸(결측치)이 있으면 평균을 계산할 때 오류가 나거나, 모델이 학습을 못 하는 일이 생깁니다. 빠진 퍼즐 조각을 어떻게 처리할지 결정해야 합니다. 선택지는 두 가지입니다.

  • dropna(): 결측치가 있는 행/열을 버린다
  • fillna(값): 결측치를 특정 값으로 채운다 (평균, 중앙값, 0 등)

어느 쪽이 정답인지는 상황에 따라 다릅니다. 데이터가 많고 결측치가 적으면 버려도 되지만, 소중한 데이터라면 평균값으로 채우는 게 낫습니다.

# 열별 결측치 개수 (True는 1로 계산되므로 sum으로 카운트)
print(titanic.isnull().sum())
survived 0
pclass 0
sex 0
age 177 <- 177개 결측
...
embarked 2
deck 688 <- 대부분이 결측
...

v1: 결측치 있는 행 다 버리기 (위험한 방법)

섹션 제목: “v1: 결측치 있는 행 다 버리기 (위험한 방법)”
df_dropped = titanic.dropna() # 결측치 있는 행을 모두 제거
print("원본:", len(titanic), "")
print("제거 후:", len(df_dropped), "")
원본: 891 행
제거 후: 182 행

891행 중 182행만 남았습니다. deck 열이 대부분 결측이라 무턱대고 버리면 79%의 데이터를 잃습니다. 좋은 전략이 아닙니다.

# 분석에 쓸 열만 선별 (deck처럼 결측이 많은 열은 제외)
cols = ["survived", "pclass", "sex", "age", "fare", "embarked"]
df = titanic[cols].copy() # 원본 보존을 위해 복사본 생성
# age는 중앙값으로 채우기 (평균보다 이상치에 덜 민감)
df["age"] = df["age"].fillna(df["age"].median())
# embarked는 결측이 2개뿐 → 최빈값으로 채우기
df["embarked"] = df["embarked"].fillna(df["embarked"].mode()[0])
print(df.isnull().sum())
survived 0
pclass 0
sex 0
age 0
fare 0
embarked 0

결측치가 모두 사라졌습니다. 위 코드 7번째 줄 df["age"].fillna(df["age"].median())평균 대신 중앙값으로 채우는 전략입니다. 극단적으로 어린 승객이나 노인이 평균을 왜곡하는 것을 막을 수 있습니다.

“여성 승객만 보고 싶다”를 코드로 옮기면 이렇게 됩니다.

# 조건식은 True/False 시리즈를 반환
is_female = df["sex"] == "female"
print(is_female.head())
0 False
1 True
2 True
3 True
4 False
Name: sex, dtype: bool
# True인 행만 추출 (대괄호 안에 조건식을 넣음)
females = df[df["sex"] == "female"]
print(f"여성 승객 수: {len(females)}")
print(f"여성 생존율: {females['survived'].mean():.2%}")
여성 승객 수: 314
여성 생존율: 74.20%

위 코드 2번째 줄 df[df["sex"] == "female"]가 바로 불리언 인덱싱입니다. 대괄호 안에 True/False 시리즈를 넣으면, True인 행만 남습니다.

# '&'는 and, '|'는 or (파이썬의 and/or은 안 됨!) 각 조건은 반드시 괄호로 감쌉니다
# 1등석 여성 승객만 추출
first_class_female = df[(df["pclass"] == 1) & (df["sex"] == "female")]
print(f"1등석 여성: {len(first_class_female)}명, 생존율: {first_class_female['survived'].mean():.2%}")
1등석 여성: 94명, 생존율: 96.81%

1등석 여성의 생존율은 96.81%입니다. 전체 평균 38%와 비교하면 엄청난 차이입니다.

⚠️ 흔한 에러: 괄호를 빼먹으면?

섹션 제목: “⚠️ 흔한 에러: 괄호를 빼먹으면?”
# 이 코드를 실행하면 어떤 에러가 날까요?
first_class_female = df[df["pclass"] == 1 & df["sex"] == "female"]
TypeError: Cannot perform 'rand_' with a dtyped [object] array and scalar of type [bool]

원인: & 연산자가 ==보다 우선순위가 높아서 1 & df["sex"]가 먼저 계산됩니다. 파이썬이 “1과 문자열을 어떻게 AND 하나요?”라며 혼란에 빠집니다.

수정: 각 조건을 반드시 소괄호로 감싸세요.

first_class_female = df[(df["pclass"] == 1) & (df["sex"] == "female")] # <- 괄호 필수

4. 👥 groupby — 집단별 비교의 핵심 무기

섹션 제목: “4. 👥 groupby — 집단별 비교의 핵심 무기”

“남자와 여자의 생존율을 각각 비교하고 싶다”면 조건 필터링을 두 번 해도 됩니다. 하지만 “모든 객실등급별, 모든 성별별, 모든 항구별 생존율”을 알고 싶다면 일일이 필터링하는 건 비효율적입니다. 이때 groupby를 씁니다.

groupby는 “같은 값을 가진 행끼리 묶어서 → 각 그룹에 통계 함수를 적용”하는 분할-적용-결합(split-apply-combine) 패턴입니다.

flowchart LR
  A[전체 데이터] --> B[성별로 분할<br/>split]
  B --> C[각 그룹별<br/>평균 계산<br/>apply]
  C --> D[결과 합치기<br/>combine]
# sex 열로 묶고, survived 열의 평균을 계산
result = df.groupby("sex")["survived"].mean()
print(result)
sex
female 0.742038
male 0.188908
Name: survived, dtype: float64

여성 74.2%, 남성 18.9%. 극명한 차이입니다.

# 성별 + 객실등급 두 기준으로 묶기
result = df.groupby(["sex", "pclass"])["survived"].mean()
print(result)
sex pclass
female 1 0.968085
2 0.921053
3 0.500000
male 1 0.368852
2 0.157407
3 0.135447
Name: survived, dtype: float64

위 코드 2번째 줄 groupby(["sex", "pclass"])두 개의 기준으로 동시에 집단을 나누는 코드입니다. 결과에서 1등석 여성은 96.8%, 3등석 남성은 13.5% 생존이라는 사실을 바로 읽을 수 있습니다.

# agg로 여러 통계 함수를 동시에 적용
result = df.groupby("pclass")["fare"].agg(["mean", "min", "max", "count"])
print(result)
mean min max count
pclass
1 84.154687 0.0000 512.3292 216
2 20.662183 0.0000 73.5000 184
3 13.675550 0.0000 69.5500 491

1등석 평균 요금은 $84, 3등석은 $13로 6배 이상 차이가 납니다.


🔧 실습 활동: 타이타닉 데이터 완전 정복앞서 배운 네 가지 개념(DataFrame 구조 · 첫인상 파악 · 결측치/필터링 · groupby)을 하나의 스크립트로 연결해 보는 실습입니다. Jupyter Notebook이나 Google Colab에서 셀 단위로 복사해 실행하세요.

섹션 제목: “🔧 실습 활동: 타이타닉 데이터 완전 정복앞서 배운 네 가지 개념(DataFrame 구조 · 첫인상 파악 · 결측치/필터링 · groupby)을 하나의 스크립트로 연결해 보는 실습입니다. Jupyter Notebook이나 Google Colab에서 셀 단위로 복사해 실행하세요.”
import pandas as pd
import seaborn as sns
titanic = sns.load_dataset("titanic")
print(f"데이터 크기: {titanic.shape}") # (행 수, 열 수)
print(f"열 목록: {list(titanic.columns)}")
데이터 크기: (891, 15)
열 목록: ['survived', 'pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked', 'class', 'who', 'adult_male', 'deck', 'embark_town', 'alive', 'alone']
# 분석에 필요한 열만 선택 + 원본 보호를 위해 copy
cols = ["survived", "pclass", "sex", "age", "fare", "embarked"]
df = titanic[cols].copy()
# 결측치 처리 (age는 중앙값, embarked는 최빈값)
df["age"] = df["age"].fillna(df["age"].median())
df["embarked"] = df["embarked"].fillna(df["embarked"].mode()[0])
print("결측치 처리 후:", df.isnull().sum().sum(), "개 남음")
결측치 처리 후: 0 개 남음

Step 3: 나이대(연령 그룹) 열 추가하기

섹션 제목: “Step 3: 나이대(연령 그룹) 열 추가하기”

숫자 나이를 그대로 쓰는 대신 “어린이/청년/중년/노년”으로 묶으면 집단 비교가 쉬워집니다. Pandas의 pd.cut이 이 일을 해줍니다.

# 구간 나누기: 0-12(어린이), 13-19(청소년), 20-39(청년), 40-59(중년), 60+(노년)
bins = [0, 12, 19, 39, 59, 100]
labels = ["어린이", "청소년", "청년", "중년", "노년"]
df["age_group"] = pd.cut(df["age"], bins=bins, labels=labels)
print(df[["age", "age_group"]].head(6))
age age_group
0 22.0 청년
1 38.0 청년
2 26.0 청년
3 35.0 청년
4 35.0 청년
5 28.0 청년

위 코드 4번째 줄 pd.cut이 연속 숫자를 범주형 데이터로 변환하는 도구입니다.

Step 4: 슬라이더로 조건 바꿔가며 탐색하기 (ipywidgets)

섹션 제목: “Step 4: 슬라이더로 조건 바꿔가며 탐색하기 (ipywidgets)”

Jupyter 환경에서 슬라이더를 움직여 “몇 살 이상 승객의 생존율이 몇 %인가”를 실시간으로 확인해 봅니다.

# Colab/Jupyter 전용. 일반 파이썬 스크립트에서는 동작하지 않습니다
from ipywidgets import interact, IntSlider
@interact(min_age=IntSlider(min=0, max=80, step=5, value=20))
def survival_by_age(min_age):
subset = df[df["age"] >= min_age]
rate = subset["survived"].mean()
print(f"{min_age}세 이상 승객 {len(subset)}명, 생존율 {rate:.2%}")

슬라이더를 움직이면 아래처럼 결과가 실시간으로 바뀝니다.

20세 이상 승객 693명, 생존율 36.22%
40세 이상 승객 230명, 생존율 38.26%
60세 이상 승객 26명, 생존율 26.92%

Step 5: 종합 — 모든 집단의 생존율 표

섹션 제목: “Step 5: 종합 — 모든 집단의 생존율 표”
# 성별, 객실등급, 연령대를 모두 묶어 생존율 집계
summary = df.groupby(["sex", "pclass", "age_group"], observed=True)["survived"].agg(["mean", "count"])
summary.columns = ["생존율", "인원수"]
# 인원수 5명 이상인 집단만 신뢰성 있게 확인
summary = summary[summary["인원수"] >= 5].sort_values("생존율", ascending=False)
print(summary.head(10))
생존율 인원수
sex pclass age_group
female 2 어린이 1.000000 10
1 청년 0.978261 46
1 중년 0.967742 31
2 청년 0.918919 37
1 노년 1.000000 5
3 어린이 0.542857 35
2 중년 0.888889 18
...

위 결과만 봐도 “1-2등석에 탑승한 여성 어린이는 거의 100% 생존”이라는 패턴이 드러납니다.


🎯 미니 챌린지: 생존율이 가장 높은 집단 찾기

섹션 제목: “🎯 미니 챌린지: 생존율이 가장 높은 집단 찾기”

개인 과제 · 10분

타이타닉 승객을 성별, 객실등급, 연령대 세 가지 기준으로 묶었을 때, 생존율이 가장 높은 집단가장 낮은 집단을 찾아내고, 그 차이가 의미하는 바를 한 문장으로 적어 봅니다.

  • 인원수가 5명 이상인 집단만 대상으로 한다 (인원이 적으면 우연의 영향이 큼)
  • 생존율을 소수점 둘째 자리까지 표시
  • 결과를 해석하는 문장을 1-2줄 작성
🔎 힌트
  • groupby(["sex", "pclass", "age_group"], observed=True)["survived"].agg(["mean", "count"])로 요약 테이블 생성
  • summary[summary["인원수"] >= 5]로 필터링
  • sort_values("mean", ascending=False) 정렬 후 .head(1), .tail(1)
✅ 정답 코드
summary = df.groupby(["sex", "pclass", "age_group"], observed=True)["survived"].agg(["mean", "count"])
summary = summary[summary["count"] >= 5].sort_values("mean", ascending=False)
print("🏆 생존율 최고 집단")
print(summary.head(1))
print("\n💀 생존율 최저 집단")
print(summary.tail(1))

해석 예시: “1-2등석 여성은 거의 모두 살아남은 반면, 3등석 남성 중년 이상은 대부분 사망했다. 1912년 당시 ‘여성과 어린이 먼저’라는 구조 원칙과 객실 위치(1등석은 갑판 근처, 3등석은 배 아래쪽)의 영향이 함께 작용한 결과로 보인다.”


타이타닉 데이터에서 남성 승객의 평균 요금여성 승객의 평균 요금을 각각 출력하세요.

힌트

groupby("sex")로 묶고 ["fare"].mean()을 호출합니다.

정답
print(df.groupby("sex")["fare"].mean())
sex
female 44.479818
male 25.523893
Name: fare, dtype: float64

여성 승객의 평균 요금이 거의 2배 높습니다. 이는 고급 객실에 여성이 많이 탑승했음을 시사합니다.

탑승 항구(embarked)별로 생존율을 계산하고, 어느 항구에서 탑승한 승객의 생존율이 가장 높은지 찾으세요.

힌트

groupby("embarked")["survived"].mean()sort_values(ascending=False).

정답
rate = df.groupby("embarked")["survived"].mean().sort_values(ascending=False)
print(rate)
embarked
C 0.553571
Q 0.389610
S 0.339009
Name: survived, dtype: float64

셰르부르(C)에서 탑승한 승객 생존율이 55%로 가장 높습니다. 이 항구에서 1등석 승객이 많이 탑승했기 때문입니다.

“혼자 탑승한 승객”“가족과 함께 탑승한 승객”의 생존율을 비교하세요. (힌트: sibsp는 형제/배우자 수, parch는 부모/자녀 수입니다.)

힌트

sibsp + parch == 0이면 혼자 탑승입니다. 새 열 alone_flag를 만들고 groupby를 적용하세요.

정답
titanic_full = sns.load_dataset("titanic")
# 가족 수 = 형제/배우자 + 부모/자녀
titanic_full["family_size"] = titanic_full["sibsp"] + titanic_full["parch"]
titanic_full["alone_flag"] = titanic_full["family_size"] == 0
result = titanic_full.groupby("alone_flag")["survived"].agg(["mean", "count"])
result.index = ["가족 동반", "혼자 탑승"]
print(result)
mean count
가족 동반 0.505650 354
혼자 탑승 0.303538 537

가족과 함께 탑승한 승객 생존율(50.6%)이 혼자 탑승(30.4%)보다 20%p 높습니다. 위기 상황에서 가족 단위로 구조된 경향을 보여줍니다.


모둠(4인) · 10분 · 미니 챌린지 결과를 공유한 후 아래 질문에 대해 의견을 나누세요.

  • 1등석과 3등석의 생존율 차이가 이렇게 큰 이유는 단순히 “요금 차이”로 설명할 수 있을까요? 다른 요인은 무엇이 있을까요?
  • age 열의 결측치 177개를 중앙값으로 채웠습니다. 만약 평균값으로 채웠다면 결과가 어떻게 달라졌을까요? 둘 중 무엇이 더 “공정한” 선택일까요?
  • 데이터 분석가가 결측치를 무작정 버리면(dropna) 왜 위험한 결정일까요? 실제로 이번 실습에서 무작정 버렸다면 어떤 결론이 나왔을지 추측해 보세요.

오늘 배운 Pandas 기능은 AI/머신러닝 프로젝트의 데이터 전처리 단계에서 80% 이상을 차지합니다.

배운 기능실무 활용 예시
read_csv, head, info고객 이탈 예측 프로젝트에서 수십만 건의 로그 데이터 첫 검토
fillna, dropna의료 데이터에서 빠진 검사 항목을 중앙값으로 채워 모델 학습 가능하게 만들기
불리언 인덱싱”최근 7일 이내 구매 고객”만 추출해 타겟 마케팅 목록 생성
groupby + agg쇼핑몰에서 카테고리별·지역별 매출 요약 대시보드 생성
pd.cut (구간 분할)신용 점수를 “저/중/고” 등급으로 변환 후 모델 입력 피처로 사용

특히 groupbySQL의 GROUP BY와 동일한 개념이라 데이터베이스 경험이 있는 사람에게는 익숙합니다. 머신러닝 엔지니어가 모델을 학습하기 전에 수행하는 EDA(Exploratory Data Analysis, 탐색적 데이터 분석)의 핵심 도구입니다.



객관식 1. DataFrame의 한 열만 뽑았을 때 반환되는 자료구조는 무엇입니까?

① list ② numpy.ndarray ③ Series ④ dict

정답 확인

정답: ③ df["열이름"]으로 꺼낸 한 열은 값과 인덱스를 함께 가진 1차원 자료구조인 Series입니다. DataFrame은 여러 Series가 열로 결합된 형태입니다.

객관식 2. 다음 중 df[(df["age"] >= 20) & (df["sex"] == "female")]의 실행 결과로 올바른 것은?

① 20세 이상 OR 여성인 행만 추출 ② 20세 이상 AND 여성인 행만 추출 ③ 에러 발생 — and 키워드를 써야 함 ④ 20세 이상인 행의 sex 열만 추출

정답 확인

정답: ② &는 두 조건의 AND를 의미하며, 각 조건은 괄호로 감싸야 합니다. 파이썬의 and 키워드는 Series 연산에 사용할 수 없어 &(비트 연산자)를 씁니다.

객관식 3. 결측치 처리 전략으로 가장 적절하지 않은 것은?

① 결측이 2-3개뿐이면 dropna로 그 행만 제거 ② 나이 같은 숫자 열은 중앙값으로 fillna ③ 범주형 열(항구명 등)은 최빈값으로 fillna ④ 결측치가 많아도 dropna()로 전체 제거해 깔끔하게 만든다

정답 확인

정답: ④ 실습에서 본 것처럼 deck 열 때문에 dropna()를 그냥 호출하면 891행이 182행으로 줄어 80%의 데이터가 사라집니다. 결측이 많은 열을 제외하거나 열별로 다른 전략을 쓰는 것이 안전합니다.

서술형 1. 타이타닉 데이터에서 “1등석 여성의 생존율 96.8%” vs “3등석 남성의 생존율 13.5%“라는 결과가 나왔습니다. 이 차이를 단순히 “운이 좋았다”로 설명할 수 있을까요? 데이터 분석가의 관점에서 어떤 요인들을 더 조사해야 할지 3가지 이상 제시하세요.

예시 답안

단순한 운의 차이로 보기 어렵습니다. 추가로 조사해야 할 요인은 다음과 같습니다.

  1. 객실 위치와 구조: 1등석은 주로 상갑판에, 3등석은 배 하부에 위치했습니다. 물이 차오른 순서와 구명보트까지의 동선이 달랐습니다.
  2. 구조 우선순위 규칙: “여성과 어린이 먼저” 원칙이 실제 어떻게 적용되었는지, 등급별로 다르게 집행되었는지 확인이 필요합니다.
  3. 정보 접근성: 경고 방송이 영어로 이루어졌기 때문에 이민자가 많았던 3등석은 정보 전달이 늦었을 가능성이 있습니다. embarked와 교차 분석할 수 있습니다.
  4. 표본 크기: 1등석 여성은 94명이고 3등석 남성은 347명입니다. 두 집단의 인원수 차이도 통계 해석에 영향을 줍니다.

자기점검 체크리스트

  • DataFrame에서 열을 선택하고, 불리언 인덱싱으로 조건에 맞는 행을 추출할 수 있다
  • isnull().sum()으로 결측치를 탐지하고, fillna/dropna를 상황에 맞게 선택할 수 있다
  • 타이타닉 데이터를 불러와 head, info, describe로 첫인상을 파악할 수 있다
  • groupby로 두 개 이상의 기준을 묶어 집단별 통계를 계산할 수 있다
  • “데이터로 답을 찾을 수 있는가?”라는 오늘의 질문에 내 말로 답할 수 있다

아래 질문에 자신만의 답을 적어 보세요. 정답이 아니라 자기 언어로 정리하는 과정이 중요합니다.

  • 이번 차시에서 가장 “아하!” 했던 순간은 언제였나요? (예: 괄호 빼먹고 에러가 났을 때, groupby 결과가 한눈에 보였을 때)
  • 결측치를 중앙값으로 채우는 것과 평균으로 채우는 것 중 어느 쪽이 더 적절한지, 오늘 이후 여러분의 기준은 무엇인가요?
  • “데이터가 곧 증거”라는 말에 대한 여러분의 생각은 어떻게 바뀌었나요? 데이터 분석에도 해석의 여지가 있다는 점을 어떻게 받아들이고 있습니까?

3차시에서는 오늘 정리한 데이터를 그림으로 바꾸는 작업을 시작합니다. Matplotlib과 Seaborn으로 막대그래프·히스토그램·상관관계 히트맵을 그리며, “표로는 안 보이던 패턴이 그래프로는 한눈에 보인다”는 경험을 하게 됩니다. 타이타닉 생존율을 그래프로 표현하면 어떤 이야기가 새로 보일까요?