17  실습: 시계열 EDA

이 장에서는 pandas와 PyArrow 백엔드를 활용하여 시계열 데이터의 탐색적 데이터 분석(EDA) 과정을 실습합니다. 주요 학습 내용은 시계열 데이터의 날짜 인덱스 설정, 리샘플링과 이동평균을 활용한 추세 분석, Wide와 Long 형식 변환, 여러 만기(미국 국채 5년, 10년, 30년물)별 수익률 곡선 시각화와 스프레드(만기 간 격차) 시각화입니다. 결측치 처리, 데이터 정제, 피벗 변환 뿐만 아니라 시계열의 전반적 탐색, 기간별 집계, 윈도우 연산 등 연속된 시계열 데이터를 다루는 핵심 EDA 기법을 살펴봅니다.

특히 대용량 데이터 로드 및 수치 연산 효율을 극대화하기 위해 PyArrow 엔진(engine="pyarrow", dtype_backend="pyarrow")을 적용합니다. PyArrow 백엔드를 활용하면 Arrow 기반의 네이티브 데이터 타입(double[pyarrow], string[pyarrow] 등)으로 메모리를 최적화하고 빠른 연산 속도를 확보할 수 있습니다.

17.1 미국 국채 채권 수익률

미국 국채 수익률 데이터를 로드한 뒤, 시계열 인덱스 설정, 리샘플링, 이동 윈도우, 수익률 곡선, 스프레드 분석을 단계적으로 진행합니다. 미국 국채 5년물(US5YT), 10년물(US10YT), 30년물(US30YT) 수익률 일별 데이터를 시계열 데이터 관점에서 탐색합니다. 날짜를 인덱스로 두고 리샘플링, 이동평균, 스프레드를 활용해 기간 구조와 추세를 파악합니다.

17.1.1 데이터 로드 및 초기 탐색

시계열 분석에 필요한 라이브러리를 불러오고 한글 폰트를 설정한 뒤, PyArrow 백엔드로 CSV를 로드합니다. 날짜 열은 pd.to_datetime()으로 DatetimeIndex에 적합하게 변환하여 이후 set_index, resample, rolling 등 시간 기반 연산을 손쉽게 사용할 수 있도록 준비합니다. Long 형식이므로 각 행은 (날짜, 만기(symbol), 수익률)이 하나의 관측값에 해당합니다.

import platform
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 운영체제별 한글 폰트 설정
if platform.system() == "Darwin":
    plt.rcParams["font.family"] = "Apple SD Gothic Neo"
elif platform.system() == "Windows":
    plt.rcParams["font.family"] = "Malgun Gothic"
else:
    plt.rcParams["font.family"] = "NanumGothic"

plt.rcParams["axes.unicode_minus"] = False

# PyArrow 엔진 및 백엔드를 적용한 CSV 데이터 로드
df = pd.read_csv(
    "../data/fred_us_bond_yield.csv",
    engine="pyarrow",
    dtype_backend="pyarrow",
)
df["date"] = pd.to_datetime(df["date"])

print("데이터 상위 3행:")
print(df.head(3))

print("\n데이터 타입 (PyArrow 백엔드):")
print(df.dtypes)

print("\n만기(symbol)별 관측치 수:")
print(df["symbol"].value_counts())

print(f"\n관측 기간: {df['date'].min().strftime('%Y-%m-%d')} ~ {df['date'].max().strftime('%Y-%m-%d')}")
데이터 상위 3행:
        date   Open  ...  Adj Close  symbol
0 1979-12-31  10.38  ...      10.38   US5YT
1 1980-01-01   <NA>  ...       <NA>   US5YT
2 1980-01-02  10.52  ...      10.52   US5YT

[3 rows x 8 columns]

데이터 타입 (PyArrow 백엔드):
date           datetime64[s]
Open         double[pyarrow]
High         double[pyarrow]
                  ...       
Volume       double[pyarrow]
Adj Close    double[pyarrow]
symbol       string[pyarrow]
Length: 8, dtype: object

만기(symbol)별 관측치 수:
symbol
US5YT     12025
US10YT    12025
US30YT    12025
Name: count, dtype: int64[pyarrow]

관측 기간: 1979-12-31 ~ 2026-01-30

데이터를 불러온 후 head()로 데이터 구조를 확인하고, dtypes를 통해 PyArrow 기반의 double[pyarrow], string[pyarrow] 타입이 정상 적용되었음을 확인합니다. symbol 열의 고유 만기별 관측치 수와 전체 관측 기간도 함께 파악합니다.

17.1.2 결측 처리 및 수익률 열 정리

원본에는 Open, High, Low, Close, Volume, Adj Close가 포함되어 있으며 채권 수익률 분석에는 일반적으로 종가인 Close를 사용합니다. 결측인 행은 dropna(subset=['Close'])로 제거합니다. 여기서는 분석용으로 수익률 열을 yield로 이름을 바꾸고 날짜, 만기, 수익률 열만 선택하여 정제합니다.

df = df.dropna(subset=["Close"]).copy()
df = df.rename(columns={"Close": "yield"})
df = df[["date", "symbol", "yield"]]

print("정제 후 데이터 크기:", df.shape)
print(df.head(3))
정제 후 데이터 크기: (34656, 3)
        date symbol  yield
0 1979-12-31  US5YT  10.38
2 1980-01-02  US5YT  10.52
3 1980-01-03  US5YT  10.54

17.1.3 시계열 인덱스 설정 및 Wide 변환

날짜를 인덱스로 두면 resample, rolling, 기간 슬라이싱(wide.loc['2020':]) 등을 직관적으로 수행할 수 있습니다. 만기별 시계열을 한 번에 비교하거나 스프레드를 계산할 때는 Long 형식보다 Wide 형식으로 변환하는 것이 유리합니다. pivot_table을 이용해 date를 인덱스로, symbol을 열로 펼칩니다.

wide = df.pivot_table(index="date", columns="symbol", values="yield")
wide = wide.sort_index()

print("Wide 형태 데이터 상위 3행:")
print(wide.head(3))
print("\nWide 데이터 타입:")
print(wide.dtypes)
Wide 형태 데이터 상위 3행:
symbol      US10YT  US30YT  US5YT
date                             
1979-12-31   10.33   10.11  10.38
1980-01-02    10.5   10.23  10.52
1980-01-03    10.6   10.31  10.54

Wide 데이터 타입:
symbol
US10YT    double[pyarrow]
US30YT    double[pyarrow]
US5YT     double[pyarrow]
dtype: object

17.1.4 리샘플링 (주별, 월별)

일별 데이터를 적절한 구간으로 묶어 중장기 추세를 파악하기 위해 리샘플링(Resampling)을 수행합니다. resample('W')는 주말 기준, resample('ME')는 월말 기준으로 데이터를 그룹화합니다. 수익률 데이터는 구간 내 평균(mean())으로 다운샘플링하는 것이 일반적입니다.

# 주별 평균 수익률
weekly = wide.resample("W").mean()

# 월별 평균 수익률
monthly = wide.resample("ME").mean()
print("월별 리샘플링 데이터 크기:", monthly.shape)
print("\n월별 평균 수익률 하위 3행:")
print(monthly.tail(3))
월별 리샘플링 데이터 크기: (554, 3)

월별 평균 수익률 하위 3행:
symbol        US10YT    US30YT     US5YT
date                                    
2025-11-30  4.089789  4.701474  3.675842
2025-12-31  4.141409  4.805273  3.705909
2026-01-31   4.20515     4.843    3.7812

mean() 외에도 sum(), max(), min(), first(), last(), std() 등 다양한 집계 함수를 리샘플링에 활용할 수 있습니다.

17.1.5 이동 평균과 이동 변동성

이동 윈도우(Rolling Window) 연산으로 단기 노이즈를 완화한 추세와 변동성을 확인합니다. rolling(window=21, min_periods=1).mean()은 약 1거래월(21영업일) 이동평균을, rolling(window=21, min_periods=1).std()는 21영업일 이동 표준편차를 계산합니다. min_periods=1을 지정하면 윈도우 초기 구간에서도 가능한 관측치로 계산하여 결측(NaN) 발생을 최소화합니다.

window = 21
wide_ma = wide.rolling(window=window, min_periods=1).mean()
wide_std = wide.rolling(window=window, min_periods=1).std()

print("10년물(US10YT) 21일 이동평균 하위 3행:")
print(wide_ma["US10YT"].tail(3))
10년물(US10YT) 21일 이동평균 하위 3행:
date
2026-01-28    4.192571
2026-01-29    4.197857
2026-01-30    4.203143
Name: US10YT, dtype: float64

17.1.6 만기별 수익률 시계열 시각화

Wide 형식의 일별 수익률을 하나의 차트에 만기별로 비교합니다. 2020년 이후의 최근 기간만 슬라이싱하여 시각화합니다.

plot_start = "2020-01-01"
to_plot = wide.loc[plot_start:]

plt.figure(figsize=(7, 5))
for col in to_plot.columns:
    plt.plot(to_plot.index, to_plot[col], label=col, alpha=0.8)
plt.title("미국 국채 만기별 수익률 (일별)")
plt.xlabel("날짜")
plt.ylabel("수익률 (%)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
그림 17.1: 미국 국채 만기별 수익률(일별).

17.1.7 수익률 곡선 시각화

특정 시점의 수익률 곡선(Yield Curve)은 만기(5년, 10년, 30년)를 \(X\)축, 수익률을 \(Y\)축으로 나타내는 선 그래프입니다. 과거 시점과 최근 시점의 곡선 형태를 겹쳐 그리면 금리 기간 구조의 변화를 한눈에 파악할 수 있습니다.

curve_dates = [wide.index[-1], wide.index[-252], wide.index[-504]]
maturities = [5, 10, 30]

plt.figure(figsize=(7, 5))
for d in curve_dates:
    row = wide.loc[d]
    plt.plot(
        maturities,
        [row["US5YT"], row["US10YT"], row["US30YT"]],
        marker="o",
        label=d.strftime("%Y-%m-%d"),
    )
plt.title("미국 국채 수익률 곡선 (선택 시점)")
plt.xlabel("만기 (년)")
plt.ylabel("수익률 (%)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
그림 17.2: 선택한 시점의 미국 국채 수익률 곡선.

17.1.8 만기 스프레드 분석

장단기 금리 스프레드는 경기 국면과 수익률 곡선 역전 여부를 파악하는 대표적인 지표입니다. 10년물−5년물, 30년물−10년물 스프레드를 계산하여 시계열로 나타내면 스프레드 축소 및 음수(역전, Inversion) 구간을 명확히 확인할 수 있습니다.

wide["spread_10y_5y"] = wide["US10YT"] - wide["US5YT"]
wide["spread_30y_10y"] = wide["US30YT"] - wide["US10YT"]

spread_start = "2010-01-01"
sp = wide.loc[spread_start:, ["spread_10y_5y", "spread_30y_10y"]].dropna(how="all")

plt.figure(figsize=(7, 5))
plt.plot(sp.index, sp["spread_10y_5y"], label="10Y-5Y", alpha=0.8)
plt.plot(sp.index, sp["spread_30y_10y"], label="30Y-10Y", alpha=0.8)
plt.axhline(0, color="gray", linestyle="--", alpha=0.5)
plt.title("만기 스프레드 시계열")
plt.xlabel("날짜")
plt.ylabel("스프레드 (%-point)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
그림 17.3: 만기 간 스프레드 시계열. 음수 구간이 장단기 금리 역전이다.

17.1.9 이동평균 및 이동 변동성 이중 축 시각화

10년물 국채 수익률에 대해 21일 이동평균선과 21일 이동 표준편차(변동성) 영역을 이중 축(twinx())으로 함께 시각화하여 가격 추세와 변동성 국면을 동시에 진단합니다.

col = "US10YT"
ts = wide[col].dropna()
ma = ts.rolling(window=21, min_periods=1).mean()
vol = ts.rolling(window=21, min_periods=1).std()
plot_start = "2015-01-01"
idx = ts.index >= plot_start

fig, ax1 = plt.subplots(figsize=(7, 5))
ax1.plot(ts.index[idx], ts.loc[idx], color="gray", alpha=0.5, label="일별 수익률")
ax1.plot(ma.index[idx], ma.loc[idx], color="C0", linewidth=2, label="21일 이동평균")
ax1.set_ylabel("수익률 (%)")
ax1.set_xlabel("날짜")
ax1.legend(loc="upper left")
ax1.grid(True, alpha=0.3)

ax2 = ax1.twinx()
ax2.fill_between(
    vol.index[idx], 0, vol.loc[idx], alpha=0.3, color="C1", label="21일 이동 표준편차"
)
ax2.grid(True, alpha=0.3)
ax2.set_ylabel("이동 표준편차")
ax2.legend(loc="upper right")
plt.title("10년물 국채 수익률: 이동평균 및 변동성")
plt.tight_layout()
plt.show()
그림 17.4: 10년물 국채 수익률의 이동평균과 변동성.

17.1.10 기간별 요약 통계

시계열을 연도별로 나누어 평균과 표준편차를 계산하여 장기적 구조 변화를 살펴봅니다.

by_year = wide[["US5YT", "US10YT", "US30YT"]].groupby(wide.index.year).agg(["mean", "std"])
print("연도별 수익률 요약 통계 (최근 5년):")
print(by_year.tail(5))
연도별 수익률 요약 통계 (최근 5년):
symbol     US5YT            ...    US30YT          
            mean       std  ...      mean       std
date                        ...                    
2022     3.00998  0.830384  ...  3.115721  0.621812
2023    4.068048  0.410224  ...  4.099664  0.413156
2024     4.12706  0.313566  ...  4.407861  0.203013
2025    3.917792  0.248289  ...  4.777916  0.131577
2026      3.7812  0.052396  ...     4.843  0.030783

[5 rows x 6 columns]

17.1.11 수익률의 일별 변화량 분석

diff()로 일별 변화량을 산출하여 극단값 분포와 변동 폭을 분석합니다.

d10 = wide["US10YT"].diff().dropna()
print("10년물 일별 변화량 기술통계:")
print(d10.describe())

recent = d10.loc["2024-01-01":]
plt.figure(figsize=(7, 5))
plt.bar(recent.index, recent.values, width=1, alpha=0.7, color="steelblue")
plt.axhline(0, color="gray", linestyle="-", alpha=0.5)
plt.title("10년물 수익률 일별 변화량 (최근 1년)")
plt.xlabel("날짜")
plt.ylabel("변화량 (%-point)")
plt.tight_layout()
plt.show()
10년물 일별 변화량 기술통계:
count     11551.0
mean    -0.000527
std      0.072647
           ...   
50%           0.0
75%         0.036
max      0.650001
Name: US10YT, Length: 8, dtype: double[pyarrow]
그림 17.5: 10년물 수익률의 일별 변화량(최근 1년).

17.2 Global Air Quality Dataset

전 세계 주요 도시의 대기질 데이터(PM2.5, PM10, NO2, O3, 온도, 습도 등)를 시계열 EDA 관점에서 탐색합니다. 도시별, 날짜별 관측이 Long 형식으로 되어 있으므로, 날짜를 인덱스로 두고 도시별 시계열로 변환한 뒤 리샘플링, 이동평균, 계절성, 도시 간 비교를 적용합니다.

해당 데이터는 Kaggle Global Air Quality Dataset에서 다운로드할 수 있습니다.

17.2.1 데이터 로드 및 초기 탐색

대기질 CSV 데이터를 PyArrow 엔진 및 백엔드(engine="pyarrow", dtype_backend="pyarrow")로 빠르게 로드합니다. 날짜 열은 pd.to_datetime()으로 파싱하여 DatetimeIndex 연산에 대비합니다.

df_air = pd.read_csv(
    "../data/global-air-quality-data-10000.csv",
    engine="pyarrow",
    dtype_backend="pyarrow",
)
df_air["Date"] = pd.to_datetime(df_air["Date"])

print("대기질 데이터 상위 3행:")
print(df_air.head(3))

print("\n대기질 데이터 타입:")
print(df_air.dtypes)

print(f"\n관측 기간: {df_air['Date'].min().strftime('%Y-%m-%d')} ~ {df_air['Date'].max().strftime('%Y-%m-%d')}")
print(f"고유 도시 수: {df_air['City'].nunique()}개")
print("\n관측 수가 많은 상위 5개 도시:")
print(df_air["City"].value_counts().head(5))
대기질 데이터 상위 3행:
             City   Country  ... Humidity  Wind Speed
0         Bangkok  Thailand  ...    59.35       13.76
1        Istanbul    Turkey  ...    67.51        6.36
2  Rio de Janeiro    Brazil  ...     29.3       12.87

[3 rows x 12 columns]

대기질 데이터 타입:
City           string[pyarrow]
Country        string[pyarrow]
Date             datetime64[s]
                    ...       
Temperature    double[pyarrow]
Humidity       double[pyarrow]
Wind Speed     double[pyarrow]
Length: 12, dtype: object

관측 기간: 2023-01-01 ~ 2023-12-28
고유 도시 수: 20개

관측 수가 많은 상위 5개 도시:
City
Mumbai          540
Seoul           522
Johannesburg    521
Dubai           520
Berlin          519
Name: count, dtype: int64[pyarrow]

17.2.2 결측 확인 및 Wide 형식 변환

동일한 도시와 날짜에 여러 관측값이 존재할 수 있으므로 pivot_table(..., aggfunc='mean')을 사용해 일별 평균으로 집계하며 Wide 형식으로 변환합니다.

wide_pm25 = df_air.pivot_table(
    index="Date", columns="City", values="PM2.5", aggfunc="mean"
)
wide_pm25 = wide_pm25.sort_index()

print("도시별 PM2.5 Wide 데이터 크기:", wide_pm25.shape)
print(wide_pm25.iloc[:3, :5])
도시별 PM2.5 Wide 데이터 크기: (336, 20)
City          Bangkok    Beijing  Berlin       Cairo  Dubai
Date                                                       
2023-01-01      127.1       <NA>    <NA>       81.91   <NA>
2023-01-02     104.98  70.933333    <NA>  120.323333   <NA>
2023-01-03  40.143333     134.74  129.68       69.04  63.59

17.2.3 시계열 인덱스 및 리샘플링

주별(resample('W')) 및 월별(resample('ME')) 평균 PM2.5 농도를 계산하여 장기 추세를 분석합니다.

weekly_pm25 = wide_pm25.resample("W").mean()
monthly_pm25 = wide_pm25.resample("ME").mean()

print("월별 PM2.5 리샘플링 크기:", monthly_pm25.shape)
월별 PM2.5 리샘플링 크기: (12, 20)

17.2.4 이동평균과 이동 변동성

7일 이동 윈도우(rolling(window=7, min_periods=1))를 적용하여 단기 변동을 완화한 7일 이동평균과 표준편차를 계산합니다.

window = 7
wide_ma = wide_pm25.rolling(window=window, min_periods=1).mean()
wide_std = wide_pm25.rolling(window=window, min_periods=1).std()

cities_sel = ["Seoul", "Beijing", "Paris", "New York"]
print("서울(Seoul) 7일 이동평균 하위 3행:")
print(wide_ma["Seoul"].tail(3))
서울(Seoul) 7일 이동평균 하위 3행:
Date
2023-12-26    73.516875
2023-12-27    61.778125
2023-12-28    59.446250
Name: Seoul, dtype: float64

17.2.5 도시별 시계열 시각화

주요 대표 도시의 일별 PM2.5 농도 시계열을 한 화면에서 비교합니다.

plot_cities = ["Seoul", "Beijing", "Paris", "Tokyo", "New York"]
to_plot = wide_pm25[plot_cities].dropna(how="all")

plt.figure(figsize=(7, 5))
for col in to_plot.columns:
    plt.plot(to_plot.index, to_plot[col], label=col, alpha=0.8)
plt.title("도시별 PM2.5 일별 시계열")
plt.xlabel("날짜")
plt.ylabel("PM2.5 농도")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
그림 17.6: 도시별 PM2.5 일별 시계열.

17.2.6 월별 계절성 분석

월별 평균 PM2.5 농도를 계산하여 도시별 계절적 패턴을 확인합니다.

monthly_avg = wide_pm25.groupby(wide_pm25.index.month).mean()

plt.figure(figsize=(7, 5))
for city in plot_cities:
    if city in monthly_avg.columns:
        plt.plot(
            monthly_avg.index,
            monthly_avg[city],
            marker="o",
            label=city,
            alpha=0.8,
        )
plt.title("월별 평균 PM2.5 (계절성)")
plt.xlabel("월")
plt.ylabel("PM2.5 평균 농도")
plt.legend()
plt.grid(True, alpha=0.3)
plt.xticks(range(1, 13))
plt.tight_layout()
plt.show()
그림 17.7: 월별 평균 PM2.5. 계절성이 뚜렷하다.

17.2.7 이동평균 시각화 (대표 도시: 서울)

서울의 일별 원계열과 7일 이동평균선을 함께 표시하여 단기 추세를 직관적으로 확인합니다.

city = "Seoul"
s = wide_pm25[city].dropna()
ma = wide_ma[city].loc[s.index]

plt.figure(figsize=(7, 5))
plt.plot(s.index, s.values, color="gray", alpha=0.5, label="일별 PM2.5")
plt.plot(ma.index, ma.values, color="C0", linewidth=2, label="7일 이동평균")
plt.title(f"{city} PM2.5 시계열 및 7일 이동평균")
plt.xlabel("날짜")
plt.ylabel("PM2.5 농도")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
그림 17.8: Seoul PM2.5 원계열과 7일 이동평균.

17.2.8 도시별 기간 평균 통계 비교

전체 기간에 대한 도시별 평균 PM2.5 농도를 가로 막대 그래프로 정렬하여 도시 간 대기질 수준을 비교합니다.

city_means = wide_pm25.mean().sort_values(ascending=False)
print("도시별 평균 PM2.5 상위 8개 도시:")
print(city_means.head(8))

plt.figure(figsize=(7, 5))
city_means.plot(kind="barh", alpha=0.8, color="steelblue")
plt.title("도시별 평균 PM2.5 농도")
plt.xlabel("PM2.5 평균")
plt.ylabel("도시")
plt.tight_layout()
plt.show()
도시별 평균 PM2.5 상위 8개 도시:
City
Dubai           80.430382
Mumbai          79.958338
Beijing         79.654332
                  ...    
Bangkok         78.289904
Johannesburg     77.75514
Sydney          77.690829
Length: 8, dtype: double[pyarrow]
그림 17.9: 도시별 평균 PM2.5.

17.2.9 다중 지표 시계열 (PM2.5 vs O3)

서울 데이터에 대해 PM2.5와 오존(O3) 농도를 이중 축으로 함께 그려 두 대기질 지표 간의 상대적 변화를 파악합니다.

seoul = (
    df_air[df_air["City"] == "Seoul"]
    .groupby("Date")
    .agg({"PM2.5": "mean", "O3": "mean"})
    .sort_index()
)

fig, ax1 = plt.subplots(figsize=(7, 5))
ax1.plot(seoul.index, seoul["PM2.5"], color="C0", label="PM2.5")
ax1.set_ylabel("PM2.5", color="C0")
ax1.legend(loc="upper left")
ax1.grid(True, alpha=0.3)

ax2 = ax1.twinx()
ax2.plot(seoul.index, seoul["O3"], color="C1", label="O3")
ax2.set_ylabel("O3", color="C1")
ax2.legend(loc="upper right")
plt.title("Seoul PM2.5 vs O3 시계열")
plt.xlabel("날짜")
plt.tight_layout()
plt.show()
그림 17.10: Seoul의 PM2.5와 O3 시계열 비교.

17.2.10 일별 변화량 분석

서울의 PM2.5 일별 변화량을 계산하여 급격한 대기질 악화 또는 개선 구간을 분석합니다.

seoul_pm = wide_pm25["Seoul"].dropna()
d_pm = seoul_pm.diff().dropna()

print("Seoul PM2.5 일별 변화량 요약 통계:")
print(d_pm.describe())

plt.figure(figsize=(7, 5))
plt.bar(d_pm.index, d_pm.values, width=1, alpha=0.7, color="steelblue")
plt.axhline(0, color="gray", linestyle="-", alpha=0.5)
plt.title("Seoul PM2.5 일별 변화량")
plt.xlabel("날짜")
plt.ylabel("변화량")
plt.tight_layout()
plt.show()
Seoul PM2.5 일별 변화량 요약 통계:
count        255.0
mean     -0.006706
std      47.953666
           ...    
50%         -2.395
75%         35.845
max        120.395
Name: Seoul, Length: 8, dtype: double[pyarrow]
그림 17.11: Seoul PM2.5의 일별 변화량.