iBetter Books
수정

공공데이터 시각화

현우가 공공데이터포털에서 CSV 파일을 내려받았습니다. 서울시 월별 기온 데이터였습니다. 파일을 열자마자 문제가 보였습니다. 일부 셀이 비어 있었고, 숫자 열에 "N/A"라는 문자열이 섞여 있었습니다.

"이거 그냥 읽으면 차트 안 나오지?"

지윤이 고개를 끄덕였습니다. "전처리 먼저 해야 해. 결측치 채우고, 타입 변환하고."

이 절에서는 CSV 데이터를 직접 딕셔너리로 생성해서 실습합니다. 공공데이터포털에서 받는 파일과 동일한 구조로 만들었기 때문에 실제 파일을 받아도 같은 코드로 처리할 수 있습니다.

데이터 준비와 전처리

# 새 파일: chapter04_03_public_data.pyimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport pandas as pd# 서울시 월별 평균 기온 데이터 (교재용 샘플)temp_data = {    "연도": [2021] * 12 + [2022] * 12 + [2023] * 12,    "월": list(range(1, 13)) * 3,    "기온": [        -2.5, 0.8, 6.2, 12.5, 18.3, 23.1,        27.8, 28.2, 22.4, 15.1, 6.8, 0.2,   # 2021        -3.1, 1.2, 7.8, 13.2, 19.5, 23.8,        28.5, 29.1, "N/A", 16.3, 7.2, 1.1,   # 2022 (9월 결측치)        -1.8, 2.1, 8.3, 14.1, 19.8, 24.2,        28.9, 29.5, 23.1, 15.8, 7.5, 1.3,   # 2023    ]}df_temp = pd.DataFrame(temp_data)print("원본 데이터:")print(df_temp[df_temp["기온"] == "N/A"])print(f"데이터 타입: {df_temp.dtypes}")

결측치와 타입 문제를 확인했습니다. 이제 전처리합니다.

# 수정: chapter04_03_public_data.pyimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport pandas as pdtemp_data = {    "연도": [2021] * 12 + [2022] * 12 + [2023] * 12,    "월": list(range(1, 13)) * 3,    "기온": [        -2.5, 0.8, 6.2, 12.5, 18.3, 23.1,        27.8, 28.2, 22.4, 15.1, 6.8, 0.2,        -3.1, 1.2, 7.8, 13.2, 19.5, 23.8,        28.5, 29.1, "N/A", 16.3, 7.2, 1.1,        -1.8, 2.1, 8.3, 14.1, 19.8, 24.2,        28.9, 29.5, 23.1, 15.8, 7.5, 1.3,    ]}df_temp = pd.DataFrame(temp_data)# 전처리: "N/A" 문자열을 NaN으로 변환 후 숫자 타입으로df_temp["기온"] = pd.to_numeric(df_temp["기온"], errors="coerce")# 결측치 확인print(f"결측치 수: {df_temp['기온'].isna().sum()}")# 선형 보간으로 결측치 채우기df_temp["기온"] = df_temp["기온"].interpolate(method="linear")print(f"결측치 처리 후: {df_temp['기온'].isna().sum()}")# 연월 문자열 열 추가 (축 레이블용)df_temp["연월"] = df_temp["연도"].astype(str) + "-" + df_temp["월"].astype(str).str.zfill(2)print(df_temp.head())

pd.to_numeric(errors="coerce")는 숫자로 변환할 수 없는 값을 NaN으로 바꿉니다. interpolate()는 앞뒤 값의 평균으로 결측치를 채웁니다.

선 그래프: 월별 기온 추이

# 수정: chapter04_03_public_data.pyimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport pandas as pdtemp_data = {    "연도": [2021] * 12 + [2022] * 12 + [2023] * 12,    "월": list(range(1, 13)) * 3,    "기온": [        -2.5, 0.8, 6.2, 12.5, 18.3, 23.1,        27.8, 28.2, 22.4, 15.1, 6.8, 0.2,        -3.1, 1.2, 7.8, 13.2, 19.5, 23.8,        28.5, 29.1, "N/A", 16.3, 7.2, 1.1,        -1.8, 2.1, 8.3, 14.1, 19.8, 24.2,        28.9, 29.5, 23.1, 15.8, 7.5, 1.3,    ]}df_temp = pd.DataFrame(temp_data)df_temp["기온"] = pd.to_numeric(df_temp["기온"], errors="coerce")df_temp["기온"] = df_temp["기온"].interpolate(method="linear")df_temp["연월"] = df_temp["연도"].astype(str) + "-" + df_temp["월"].astype(str).str.zfill(2)# 선 그래프: 연도별 월별 기온 추이fig_line = px.line(    df_temp,    x="월",    y="기온",    color="연도",    markers=True,    title="서울시 월별 평균 기온 (2021~2023)",    labels={"기온": "평균 기온 (°C)", "월": "월"})fig_line.update_xaxes(tickmode="array", tickvals=list(range(1, 13)),                      ticktext=[f"{m}월" for m in range(1, 13)])fig_line.add_hline(y=0, line_dash="dash", line_color="gray",                   annotation_text="0°C")fig_line.show()

add_hline(y=0)으로 0°C 기준선을 추가합니다. 영하로 내려가는 구간이 시각적으로 명확해집니다.

막대 그래프: 시도별 인구 비교

# 수정: chapter04_03_public_data.pyimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport pandas as pdtemp_data = {    "연도": [2021] * 12 + [2022] * 12 + [2023] * 12,    "월": list(range(1, 13)) * 3,    "기온": [        -2.5, 0.8, 6.2, 12.5, 18.3, 23.1,        27.8, 28.2, 22.4, 15.1, 6.8, 0.2,        -3.1, 1.2, 7.8, 13.2, 19.5, 23.8,        28.5, 29.1, "N/A", 16.3, 7.2, 1.1,        -1.8, 2.1, 8.3, 14.1, 19.8, 24.2,        28.9, 29.5, 23.1, 15.8, 7.5, 1.3,    ]}df_temp = pd.DataFrame(temp_data)df_temp["기온"] = pd.to_numeric(df_temp["기온"], errors="coerce")df_temp["기온"] = df_temp["기온"].interpolate(method="linear")df_temp["연월"] = df_temp["연도"].astype(str) + "-" + df_temp["월"].astype(str).str.zfill(2)fig_line = px.line(    df_temp,    x="월",    y="기온",    color="연도",    markers=True,    title="서울시 월별 평균 기온 (2021~2023)",    labels={"기온": "평균 기온 (°C)", "월": "월"})fig_line.update_xaxes(tickmode="array", tickvals=list(range(1, 13)),                      ticktext=[f"{m}월" for m in range(1, 13)])fig_line.add_hline(y=0, line_dash="dash", line_color="gray",                   annotation_text="0°C")fig_line.show()# 전국 시도별 인구 데이터 (교재용 샘플, 2023년 기준, 단위: 만 명)pop_data = {    "시도": ["서울", "부산", "대구", "인천", "광주", "대전", "울산",            "세종", "경기", "강원", "충북", "충남", "전북", "전남",            "경북", "경남", "제주"],    "인구": [940, 333, 238, 300, 143, 145, 112,            38, 1362, 153, 160, 212, 176, 182,            258, 326, 68]}df_pop = pd.DataFrame(pop_data)df_pop = df_pop.sort_values("인구", ascending=False)fig_bar = px.bar(    df_pop,    x="시도",    y="인구",    color="인구",    color_continuous_scale="Viridis",    text="인구",    title="전국 시도별 인구 (2023년, 단위: 만 명)")fig_bar.update_traces(texttemplate="%{text}만", textposition="outside")fig_bar.update_layout(    xaxis_title="시도",    yaxis_title="인구 (만 명)",    coloraxis_showscale=False)fig_bar.show()

sort_values("인구", ascending=False)로 미리 정렬해두면 차트에서 큰 순서대로 막대가 배치됩니다.

히트맵: 월별 기온 매트릭스

연도를 행, 월을 열로 펼친 매트릭스를 히트맵으로 만듭니다.

# 수정: chapter04_03_public_data.pyimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport pandas as pdtemp_data = {    "연도": [2021] * 12 + [2022] * 12 + [2023] * 12,    "월": list(range(1, 13)) * 3,    "기온": [        -2.5, 0.8, 6.2, 12.5, 18.3, 23.1,        27.8, 28.2, 22.4, 15.1, 6.8, 0.2,        -3.1, 1.2, 7.8, 13.2, 19.5, 23.8,        28.5, 29.1, "N/A", 16.3, 7.2, 1.1,        -1.8, 2.1, 8.3, 14.1, 19.8, 24.2,        28.9, 29.5, 23.1, 15.8, 7.5, 1.3,    ]}df_temp = pd.DataFrame(temp_data)df_temp["기온"] = pd.to_numeric(df_temp["기온"], errors="coerce")df_temp["기온"] = df_temp["기온"].interpolate(method="linear")df_temp["연월"] = df_temp["연도"].astype(str) + "-" + df_temp["월"].astype(str).str.zfill(2)pop_data = {    "시도": ["서울", "부산", "대구", "인천", "광주", "대전", "울산",            "세종", "경기", "강원", "충북", "충남", "전북", "전남",            "경북", "경남", "제주"],    "인구": [940, 333, 238, 300, 143, 145, 112,            38, 1362, 153, 160, 212, 176, 182,            258, 326, 68]}df_pop = pd.DataFrame(pop_data)df_pop = df_pop.sort_values("인구", ascending=False)# 히트맵: 연도 x 월 기온 매트릭스pivot_temp = df_temp.pivot_table(    values="기온",    index="연도",    columns="월")month_labels = [f"{m}월" for m in range(1, 13)]fig_heat = px.imshow(    pivot_temp,    x=month_labels,    text_auto=".1f",    color_continuous_scale="RdBu_r",   # 파란색=추위, 빨간색=더위    title="서울시 월별 기온 히트맵 (2021~2023)")fig_heat.update_layout(    xaxis_title="월",    yaxis_title="연도",    coloraxis_colorbar_title="기온 (°C)")fig_heat.show()

RdBu_r는 빨강-파랑 컬러 스케일을 역전시킨 것입니다. 낮은 값(추운 겨울)이 파란색, 높은 값(더운 여름)이 빨간색으로 표시됩니다. 기온 데이터에 직관적으로 어울립니다.

make_subplots로 대시보드형 배치

앞에서 만든 세 차트를 한 화면에 배치합니다.

# 수정: chapter04_03_public_data.pyimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport pandas as pdtemp_data = {    "연도": [2021] * 12 + [2022] * 12 + [2023] * 12,    "월": list(range(1, 13)) * 3,    "기온": [        -2.5, 0.8, 6.2, 12.5, 18.3, 23.1,        27.8, 28.2, 22.4, 15.1, 6.8, 0.2,        -3.1, 1.2, 7.8, 13.2, 19.5, 23.8,        28.5, 29.1, "N/A", 16.3, 7.2, 1.1,        -1.8, 2.1, 8.3, 14.1, 19.8, 24.2,        28.9, 29.5, 23.1, 15.8, 7.5, 1.3,    ]}df_temp = pd.DataFrame(temp_data)df_temp["기온"] = pd.to_numeric(df_temp["기온"], errors="coerce")df_temp["기온"] = df_temp["기온"].interpolate(method="linear")df_temp["연월"] = df_temp["연도"].astype(str) + "-" + df_temp["월"].astype(str).str.zfill(2)pop_data = {    "시도": ["서울", "부산", "대구", "인천", "광주", "대전", "울산",            "세종", "경기", "강원", "충북", "충남", "전북", "전남",            "경북", "경남", "제주"],    "인구": [940, 333, 238, 300, 143, 145, 112,            38, 1362, 153, 160, 212, 176, 182,            258, 326, 68]}df_pop = pd.DataFrame(pop_data)df_pop = df_pop.sort_values("인구", ascending=False)pivot_temp = df_temp.pivot_table(    values="기온",    index="연도",    columns="월")month_labels = [f"{m}월" for m in range(1, 13)]# 서브플롯으로 대시보드 구성 (2행 2열)fig_dashboard = make_subplots(    rows=2, cols=2,    subplot_titles=[        "월별 기온 추이 (연도별)",        "시도별 인구",        "기온 히트맵",        ""    ],    specs=[        [{"type": "scatter"}, {"type": "bar"}],        [{"type": "heatmap"}, {"type": "table"}]    ])# (1,1) 선 그래프for year in [2021, 2022, 2023]:    subset = df_temp[df_temp["연도"] == year]    fig_dashboard.add_trace(        go.Scatter(            x=subset["월"],            y=subset["기온"],            mode="lines+markers",            name=str(year)        ),        row=1, col=1    )# (1,2) 막대 그래프 (상위 10개 시도)top10 = df_pop.head(10)fig_dashboard.add_trace(    go.Bar(x=top10["시도"], y=top10["인구"], name="인구",           marker_color="steelblue", showlegend=False),    row=1, col=2)# (2,1) 히트맵fig_dashboard.add_trace(    go.Heatmap(        z=pivot_temp.values,        x=month_labels,        y=pivot_temp.index.tolist(),        colorscale="RdBu_r",        showscale=False,        text=pivot_temp.values.round(1),        texttemplate="%{text}",    ),    row=2, col=1)# (2,2) 요약 테이블summary = df_temp.groupby("연도")["기온"].agg(["min", "max", "mean"]).reset_index()summary.columns = ["연도", "최저", "최고", "평균"]fig_dashboard.add_trace(    go.Table(        header=dict(values=["연도", "최저(°C)", "최고(°C)", "평균(°C)"],                   fill_color="steelblue", font_color="white"),        cells=dict(values=[summary["연도"], summary["최저"].round(1),                           summary["최고"].round(1), summary["평균"].round(1)])    ),    row=2, col=2)fig_dashboard.update_layout(    title="공공데이터 분석 대시보드",    height=700,    showlegend=True)fig_dashboard.show()

specs 파라미터에 {"type": "heatmap"}, {"type": "table"}처럼 각 서브플롯의 타입을 미리 선언해야 합니다. 선언하지 않으면 히트맵이나 테이블을 넣을 때 오류가 발생합니다.