🚢 titanic

[python] 항해일지 04. 갑판 위 풍경 스케치: matplotlib

Floaty 2026. 6. 26. 10:00

이제 드디어 시각화를 해보겠네요. 먼저 matplotlib에 대해서 좀 더 이야기를 해봐야 할 것 같아요. matplotlib는 파이썬 시각화의 기본이 되는 라이브러리입니다. 세밀한 커스터마이징이 가능하긴 하지만 코드가 다소 길어질 수 있어요. 타이타닉 데이터를 가지고 막대그래프, 파이 차트, 히스토그램, 산점도, 선 그래프 5가지 방식으로 그려볼까 해요.

 

그런데 그 전에 matplotlib 라이브러리를 통해서 그릴 수 있는 그래프에 대해서 먼저 알아봐야겠어요. 그릴 수 있는 게 정말 정말 정말 많거든요. 리스트를 적어보자면, `bar`, `pie`, `hist`, `scatter`, `plot`, `boxplot`, `violinplot`, `heatmap`, `barh`, `stackplot`, `errorbar`, `contour`, `stem`, `step` 등이 있어요. 정말 많죠? 그런데 이 중에서는 막대, 파이, 히스토그램, 산점도, 선 그래프를 제일 많이 사용해요. `boxplot`이나 `heatmap`도 사용하지만 좀 더 깊이 들어갈 때 쓰는 거라 matplotlib가 처음이니 5개만 그려보려고 합니다. 

 

 

 

라이브러리 불러오기

import pandas as pd
import matplotlib.pyplot as plt
from utils.theme import set_plot_theme, get_palette
from utils.font import set_korean_font
from utils.save import save_fig

# 그래프 전체 테마 설정(배경색, 텍스트 색상 등), 한글 폰트 설정
set_plot_theme()
set_korean_font()

df = pd.read_csv("data/titanic.csv")

먼저 각종 라이브러리를 불러오겠습니다. titanic.csv를 읽기 위한 pandas, 그림을 그리기 위한 matplotlib, 그래프 전체 테마를 위한 `set_plot_theme`, 색상 팔레트를 위한 `get_palette`, 한글 폰트 설정을 위한 `set_korean_font`, 그래프를 이미지로 저장하기 위한 `save_fig`까지 불러오겠습니다. 

 

그리고 그래프 전체 테마 설정을 위해 `set_plot_theme()`를 호출합니다. 배경색, 텍스트 색상 등 전반적인 테마를 한 번에 적용하기 때문에 호출만으로 설정이 완료됩니다. 그리고 한글 설정을 위한 `set_korean_font()`도 호출합니다. 여기서 주의해야 할 점은 순서를 지켜야 한다는 건데요. `set_plot_theme()`를 나중에 호출할 경우 한글 폰트 설정이 초기화될 수 있기 때문에 `set_plot_theme()`를 반드시 먼저 호출해야 합니다.

 

하는 김에 데이터도 읽어올게요. 아래에서 막대 그래프와 파이 차트, 히스토그램, 산점도, 선 그래프를 그릴 건데, 위의 라이브러리 불러오기 코드는 모두 동일하니까 이 점 참고하고 봐주세요.

 

 

 

막대그래프

data = df["survived"].value_counts().sort_index()

fig, ax = plt.subplots(figsize=(6, 4.5))

bars = ax.bar(             # 막대 그래프를 그림
    ["사망(0)", "생존(1)"],  # x축 레이블
    data.values,           # y축 레이블
    color=get_palette(2),  # 막대 색상
)
ax.bar_label(          # 막대 위에 값 표시
    bars,              # 막대 객체
    fontweight="bold", # 글씨 굵기
    padding=1,         # 막대와 텍스트 사이의 간격
)

ax.set_title("생존 여부별 인원 수")
ax.set_ylabel("인원 수")

plt.tight_layout()
save_fig(fig, "01-bar_people_by_survival", "tutorial")
plt.show()

막대그래프는 범주별 개수나 양을 비교할 때 가장 직관적입니다. 이 그래프는 두 범주(생존/사망)의 인원수 차이를 한눈에 비교하기 좋습니다. 길이로 양을 직관적으로 비교할 수 있어 이런 단순 비교에 가장 알맞은 차트입니다.

 

코드를 보면, 먼저 `value_counts()`로 생존(1)과 사망(0) 각각의 인원수를 집계하고 `sort_index()`로 0→1 순서로 정렬합니다. 이때 `value_counts()`는 원본 데이터를 바꾸지 않고 집계 결과만 새로 반환해요. 그다음 `bar()`로 막대그래프를 그리고 `bar_label()`로 각 막대 위에 값을 표시합니다. 

 

 

 

파이 차트

data = df["survived"].value_counts().sort_index()

fig, ax = plt.subplots(figsize=(5.5, 5.5))

ax.pie(                           # 파이 차트를 그림
    data.values,                  # 각 조각의 크기 값
    labels=["사망(0)", "생존(1)"],  # 조각 레이블
    autopct="%1.1f%%",            # 퍼센트 표시 형식(소수점 1자리까지)
    colors=get_palette(2),        # 조각 색상
    startangle=90,                # 첫 조각 시작 각도(90 = 12시 방향부터 시작)
    wedgeprops={                  # 조각 스타일
        "edgecolor": "white",     # 조각 사이 경계선 색상
        "linewidth": 1.5,         # 경계선 두께
    },
)

ax.set_title("생존 여부 비율")
plt.tight_layout()

save_fig(fig, "02-pie_survival_rate", "tutorial")
plt.show()

파이 차트는 전체에 대한 비율(퍼센트)을 강조할 때 사용합니다. 다만 지금처럼 범주가 2개뿐일 때는 막대그래프보다 정보 전달력이 크게 높지는 않아요. 파이 차트는 범주가 3~5개 정도이고 '전체 대비 비중'을 강조하고 싶을 때 더 효과적입니다. 타이타닉 데이터로 봤을 때는 1/2/3등석 객실 등급별 비율처럼 3개 범주를 비교할 때가 더 잘 어울립니다.

 

코드를 보면 앞서 집계한 데이터를 그대로 사용해 `pie()`로 파이 차트를 그립니다. 조각의 크기 값, 레이블, 퍼센트 표시 형식, 색상, 시작 각도, 경계선 스타일 등을 입력합니다. 

 

 

 

히스토그램

data = df["age"].dropna()

fig, ax = plt.subplots(figsize=(6.5, 4.5))

ax.hist(  # 히스토그램을  그림: 연속형 데이터의 분포를 막대로 표현
    data,                  # 데이터
    bins=30,               # 막대 개수
    color=get_palette(1),  # 막대 색상
)


ax.set_title("승객 나이 분포")
ax.set_xlabel("나이")
ax.set_ylabel("빈도")
plt.tight_layout()

save_fig(fig, "03-histogram_age_distribution", "tutorial")
plt.show()


히스토그램은 수치형 데이터의 분포나 빈도를 구간별로 보여줍니다. 나이처럼 연속적인 수치형 데이터가 어디에 몰려 있는지, 좌우로 치우침은 없는지 파악하는 데 최적의 차트입니다. 이 그래프를 보면 20~40대에 승객이 가장 많이 몰려 있는 것을 확인할 수 있어요.

 

코드에서 주목할 점은 `dropna()`입니다. 타이타닉 데이터에는 나이가 비어 있는 행이 있는데, `dropna()`는 결측값이 있는 행을 제외한 새로운 데이터를 반환합니다. 원본 `df`는 그대로 유지돼요. 결측값이 있는 상태로 히스토그램을 그리면 오류가 발생하기 때문에 반드시 제거해야 합니다. 

 

 

 

 

산점도

fig, ax = plt.subplots(figsize=(6.5, 4.6))

survived_color, dead_color = get_palette(2)

# 생존 여부에 따른 점 색상 매핑
colors = df["survived"].map({0: survived_color, 1: dead_color})


ax.scatter(     # 산점도를 그림: 두 변수 간의 관계를 점으로 표현
    df["age"],  # x축 값
    df["fare"], # y축 값
    c=colors,   # 점 색상
    alpha=0.5,  # 투명도(0: 완전 투명, 1: 불투명)
    s=20,       # 점 크기
)

ax.set_title("나이 vs 요금")
ax.set_xlabel("나이")
ax.set_ylabel("요금")
plt.tight_layout()
save_fig(fig, "04-scatter_age_vs_fare", "tutorial")
plt.show()

 

산점도는 두 수치형 변수 간의 관계를 점으로 표현합니다. 이 데이터에서는 나이와 요금 사이에 뚜렷한 패턴이 없어서 점들이 흩어져 보일 뿐 큰 인사이트를 주지는 못합니다. 예를 들어 요금과 객실 등급처럼 관계가 강한 변수끼리 비교할 때 더 효과적입니다. 산점도는 변수 간 상관관계가 뚜렷할수록 빛을 발하는 차트니까요.

 

여기서 `s`는 점의 크기를 의미하는데 포인트 단위를 사용합니다. 도화지 크기처럼 인치 단위가 아니라 화면 해상도 기준이라 직관적이지 않아요. 데이터가 많을 경우 10~30, 일반적인 경우 50~100, 강조할 때는 100~200 정도를 권장합니다. 지금은 데이터가 많아서 `s=20`으로 작게 설정했습니다. 

 

코드를 보면, `map()`으로 생존 여부(0/1)에 따라 각 점의 색상을 다르게 매핑합니다. 그다음 `scatter()`로 나이(x축)와 요금(y축)을 기준으로 점을 찍고, 투명도(alpha)를 0.5로 설정해 점이 겹쳐도 밀도를 확인할 수 있게 했습니다. 

 

 

 

선그래프

age_bins = pd.cut(  # 연속형 데이터를 구간으로 나눔
    df["age"],      # 나눌 컬럼
    bins=[0, 10, 20, 30, 40, 50, 60, 70, 80],  # 구간 경계값 리스트
)

data = df.groupby(  # 그룹별로 묶어서 집계
    age_bins,       # 그룹 기준
    observed=False, # 데이터 없는 구간도 NaN으로 포함
)["survived"].mean()

fig, ax = plt.subplots(figsize=(6.5, 4.5))
ax.plot(                     # 선 그래프를 그림
    data.index.astype(str),  # x축 값
    data.values,             # y축 값
    marker="o",              # 데이터 포인트 마커 모양
    color=get_palette(1)[0], # 선 색상
    linewidth=2,             # 선 두께
)

ax.set_title("연령대별 생존율")
ax.set_xlabel("연령대")
ax.set_ylabel("생존율")
plt.xticks(       # 눈금 스타일 설정
    axis="x",     # 적용할 축(x, y, both)
    rotation=30,  # 레이블 회전 각도
)
plt.tight_layout()

save_fig(fig, "05-line_survival_rate_by_age_group", "tutorial")
plt.show()

선 그래프는 보통 시간에 따른 변화(시계열)를 보여줄 때 사용합니다. 선으로 이어져 있지만 실제로는 연속된 시간이 아닌 '구간'이라는 점에 주의해야 해요. 연령대는 이산적인 범주이고 각 구간 사이에 명확한 흐름이 있는 것은 아닙니다. 선으로 연결하면 마치 '나이가 들수록 생존율이 어떤 경로로 변화한다'는 시간적 인상을 줘서 오해를 부를 수 있으니, 이 경우에는 사실 막대그래프가 더 적합합니다.

 

코드에서 주목할 부분은 데이터 전처리입니다. `pd.cut()`으로 나이를 10세 단위 구간으로 나누고, `groupby()`로 구간별로 묶은 뒤 `mean()`으로 생존율을 계산합니다. 이때 `observed=False`를 설정하면 데이터가 없는 구간도 `NaN`으로 포함되어 구간이 누락되지 않아요. 이렇게 만들어진 데이터는 구간(index)과 생존율(values)로 구성되는데 `index`가 `pd.Interval` 타입이라 그대로 쓰면 x축 레이블로 표시가 안 돼서 `.astype(str)`로 문자열로 변환해 사용합니다. 

 

 

 

하나의 figure에 여러 개의 그래프 그리기

import pandas as pd
import matplotlib.pyplot as plt
from utils.theme import set_plot_theme, get_palette
from utils.font import set_korean_font
from utils.save import save_fig

# 그래프 전체 테마 설정(배경색, 텍스트 색상 등), 한글 폰트 설정
set_plot_theme()
set_korean_font()

df = pd.read_csv("data/titanic.csv")


# [0] 데이터 전처리
survived_counts = df["survived"].value_counts().sort_index()

age_bins = pd.cut(
    df["age"],
    bins=[0, 10, 20, 30, 40, 50, 60, 70, 80],
)

survival_rate_by_age = df.groupby(
    age_bins,
    observed=False,
)["survived"].mean()


# [1] 그래프 그릴 공간 잡기
# 2행 x 3열 subplot 생성 (전체 크기: 15 x 8인치)
fig, axes = plt.subplots(  # subplot 생성
    2,  # 행 수
    3,  # 열 수
    figsize=(15, 8),  # 전체 그림 크기(가로, 세로)
)
# 2D[[ax0, ax1, ax2], [ax3, ax4, ax5]] → 1D[ax0 ~ ax5]
axes = axes.flatten()


# [2] 막대 그래프: 생존 여부별 인원 수
bars = axes[0].bar(
    ["사망(0)", "생존(1)"],
    survived_counts.values,
    color=get_palette(2),
)

axes[0].bar_label(
    bars,
    fontweight="bold",
    padding=1,
)
axes[0].set_title("생존 여부별 인원 수")
axes[0].set_ylabel("인원 수")


# [3] 파이 차트: 생존 여부 비율
axes[1].pie(
    survived_counts.values,
    labels=["사망(0)", "생존(1)"],
    autopct="%1.1f%%",
    colors=get_palette(2),
    startangle=90,
    wedgeprops={
        "edgecolor": "white",
        "linewidth": 1.5,
    },
)

axes[1].set_title("생존 여부 비율")


# [4] 히스토그램: 승객 나이 분포
axes[2].hist(
    df["age"].dropna(),
    bins=30,
    color=get_palette(1),
)

axes[2].set_title("승객 나이 분포")
axes[2].set_xlabel("나이")
axes[2].set_ylabel("빈도")


# [5] 산점도: 나이 vs. 요금
survived_color, dead_color = get_palette(2)
dot_colors = df["survived"].map({0: survived_color, 1: dead_color})

axes[3].scatter(
    df["age"],
    df["fare"],
    c=dot_colors,
    alpha=0.5,
    s=20,
)


axes[3].set_title("나이 vs. 요금")
axes[3].set_xlabel("나이")
axes[3].set_ylabel("요금")

# [6] 선 그래프: 연령대별 생존율
axes[4].plot(
    survival_rate_by_age.index.astype(str),
    survival_rate_by_age.values,
    marker="o",
    color=get_palette(1)[0],
    linewidth=2,
)

axes[4].set_title("연령대별 생존율")
axes[4].set_xlabel("연령대")
axes[4].set_ylabel("생존율")

axes[4].tick_params(
    axis="x",
    rotation=30,
)


# [7] 빈 칸 삭제
axes[5].set_visible(False)  # 2x3 중 마지막 칸 비우기


plt.tight_layout()  # subplot 간 간격을 자동으로 조정해 겹침 방지
save_fig(fig, "00-matplotlib", "tutorial")  # 파일 저장
plt.show()  # 화면 출력

코드가 126줄로 길지만 사실상 앞에 다뤘던 내용을 하나의 그림으로 만들기 위한 작업이에요. 앞에서는 data라는 변수명으로 데이터가 들어가는 위치를 명확히 했다면, 여기서는 `survived_counts`, `survival_rate_by_age`처럼 의미 있는 변수명을 사용해 가독성을 높였습니다.

 

여기서 중점적으로 봐야 할 것은 `subplots()`입니다. 그런데 `subplots()`를 이해하려면 먼저 subplot이 뭔지 알아야 해요. 이름이 s 하나 차이라 헷갈릴 수 있으니 주의해서 봐주세요.

 

subplot은 figure(도화지) 안에 여러 개의 그래프를 배치할 수 있는 칸이에요.

fig (도화지 전체)
┌─────────────────────────┐
│  ax0  │  ax1  │  ax2   │  ← 이 칸 하나하나가 subplot (axes)
│───────────────────────  │
│  ax3  │  ax4  │  ax5   │
└─────────────────────────┘

`fig`는 전체 도화지, `axes`는 그 안의 각 그래프 칸입니다. 그리고 `subplots()`는 이 도화지와 칸들을 한 번에 만들어주는 함수입니다. `plt.subplots(2, 3)`을 호출하면 2행 3열로 총 6개의 칸이 만들어집니다. 여기서 `axes`는 `[[ax0, ax1, ax2], [ax3, ax4, ax5]]` 형태의 2차원 배열로 반환되는데, `axes[0]`, `axes[1]`처럼 인덱스로 접근하기 위해 `flatten()`으로 1차원 배열로 변환합니다.