19  실습: DuckDB (글로벌 금융 시장 데이터)

이 장에서는 글로벌 금융 시장 데이터(global_financial_markets_2000_Now.csv)를 활용하여 DuckDB와 pandas를 함께 사용하는 방법을 연습합니다. 앞서 배운 SQL 기본 구문, 윈도우 함수, ASOF JOIN 등을 적극 활용하여 메모리 효율적으로 데이터를 다루는 흐름을 익혀보세요.

19.1 환경 준비

먼저 필요한 라이브러리를 불러옵니다.

import duckdb
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from platform import system

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

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

# 데이터 경로 변수 설정
DATA_PATH = "../data/global_financial_markets_2000_Now.csv"

19.2 DuckDB로 직접 조회 및 요약

Pandas의 read_csv()를 사용하지 않고 DuckDB를 이용해 데이터를 직접 조회해 봅니다.

  1. SUMMARIZE 명령어를 사용하여 CSV 파일의 전체적인 요약 통계를 확인하세요.

  2. asset_type별로 관측치(행) 수, 데이터의 시작일(MIN(date)), 종료일(MAX(date))을 구하는 SQL을 작성하고, 결과를 pandas DataFrame으로 받아 출력하세요.

# 1-1. SUMMARIZE를 통한 데이터 요약 확인
summary_df = duckdb.sql(
    f"""
    SELECT column_name, column_type, min, max, count, null_percentage
    FROM (SUMMARIZE SELECT * FROM '{DATA_PATH}')
"""
).df()

print("=== 데이터 요약 통계 ===")
print(summary_df)


# 1-2. asset_type별 관측 수와 기간 확인
asset_summary = duckdb.sql(
    f"""
    SELECT asset_type, 
           COUNT(*) AS total_rows, 
           MIN(date) AS start_date, 
           MAX(date) AS end_date
    FROM '{DATA_PATH}'
    GROUP BY asset_type
    ORDER BY total_rows DESC
"""
).df()

print("\n=== 자산 유형별 요약 ===")
print(asset_summary)
=== 데이터 요약 통계 ===
   column_name column_type  ...   count null_percentage
0         date        DATE  ...  189813             0.0
1         open      DOUBLE  ...  189813             0.0
2         high      DOUBLE  ...  189813             0.0
..         ...         ...  ...     ...             ...
7   asset_name     VARCHAR  ...  189813             0.0
8   asset_type     VARCHAR  ...  189813             0.0
9       region     VARCHAR  ...  189813             0.0

[10 rows x 6 columns]

=== 자산 유형별 요약 ===
       asset_type  total_rows start_date   end_date
0     Stock Index       64196 2000-01-03 2026-08-21
1       Commodity       50429 2000-07-17 2026-08-21
2        Currency       48109 2000-01-03 2026-08-21
3  Cryptocurrency       27079 2014-09-17 2026-08-21

19.3 QUALIFY와 윈도우 함수를 활용한 최근 데이터 추출

데이터셋에 있는 각 자산(symbol)별로 가장 최근(가장 늦은 date)의 종가(close)가 얼마인지 조회하세요.

  1. QUALIFY와 윈도우 함수(ROW_NUMBER())를 사용하여 각 심볼별 최신 데이터 1건씩만 추출해야 합니다.

  2. 결과를 심볼 이름순(ORDER BY symbol)으로 정렬하고 pandas DataFrame으로 결과를 확인하세요.

# QUALIFY를 사용하여 심볼별 최신 데이터 추출
latest_prices = duckdb.sql(
    f"""
    SELECT symbol, asset_name, date, close
    FROM '{DATA_PATH}'
    QUALIFY ROW_NUMBER() OVER (
        PARTITION BY symbol 
        ORDER BY date DESC
    ) = 1
    ORDER BY symbol
"""
).df()

print(latest_prices.head(10))
       symbol       asset_name       date      close
0   000001.SS         Shanghai 2026-08-21  3905.2029
1     ADA-USD          Cardano 2026-08-21     0.2292
2    AUDUSD=X          AUD/USD 2026-08-21     0.7119
..        ...              ...        ...        ...
7    CHFUSD=X          CHF/USD 2026-08-21     1.2487
8        CL=F  Crude Oil (WTI) 2026-08-21    87.0600
9    CNYUSD=X          CNY/USD 2026-08-21     0.1488

[10 rows x 4 columns]

PARTITION BY symbol ORDER BY date DESC를 통해 심볼별로 날짜 내림차순 순위를 매길 수 있습니다. 그리고 QUALIFY 절을 통해 이 순위가 1인 행, 즉 가장 최근 행만 깔끔하게 걸러낼 수 있습니다. pandas의 .groupby().tail(1)이나 .sort_values().drop_duplicates()보다 복잡한 윈도우 로직을 한 번에 처리할 수 있습니다.

19.4 윈도우 함수를 이용한 이동평균 계산

비트코인(BTC-USD)의 최근 1년치 데이터(예: 2023년 1월 1일 이후)를 조회하면서, 종가(close)의 30일 이동평균을 함께 계산하세요.

  1. 윈도우 함수를 사용할 때 ROWS 29 PRECEDING을 활용하여 현재 행 포함 30일의 평균을 구하세요.

  2. 반환받은 DataFrame을 활용하여 pandas와 matplotlib로 종가와 이동평균선 선 그래프를 그리세요.

btc_ma = duckdb.sql(
    f"""
    SELECT date::DATE AS date, close,
           AVG(close) OVER (
               ORDER BY date
               ROWS 29 PRECEDING
           ) AS ma_30
    FROM '{DATA_PATH}'
    WHERE symbol = 'BTC-USD' AND date >= '2023-01-01'
    ORDER BY date
"""
).df()

# 시각화 (pandas + matplotlib)
plt.figure(figsize=(10, 5))
plt.plot(
    btc_ma["date"], btc_ma["close"], label="종가", alpha=0.5
)
plt.plot(
    btc_ma["date"],
    btc_ma["ma_30"],
    label="30일 이동평균",
    color="red",
    linewidth=2,
)
plt.title("비트코인(BTC-USD) 종가 및 30일 이동평균")
plt.xlabel("날짜")
plt.ylabel("가격 (USD)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
그림 19.1: DuckDB 윈도우 함수로 계산한 비트코인 종가 및 30일 이동평균선

시계열 데이터를 다룰 때 AVG() OVER (ORDER BY ... ROWS ... PRECEDING) 패턴을 사용하면, 그룹핑뿐만 아니라 윈도우(창) 크기를 자유롭게 지정해 이동평균을 쉽게 구할 수 있습니다.

19.5 ASOF JOIN을 활용한 이종 자산 결합

주가지수인 S&P500(^GSPC)과 환율인 JPY/USD(JPYUSD=X)는 서로 휴일이나 거래일이 달라 날짜가 완벽히 일치하지 않을 수 있습니다. S&P500의 거래일을 기준으로 가장 최근에 고시된 엔/달러 환율을 ASOF JOIN을 통해 결합하세요.

  1. 분석 기간은 2024년 1월 1일 이후로 한정합니다.

  2. S&P500의 날짜(trade_date), S&P500 종가(sp500_close), 환율 종가(jpy_usd)를 포함하는 DataFrame을 만드세요.

sp500_jpy = duckdb.sql(
    f"""
    SELECT s.date AS trade_date, 
           s.close AS sp500_close, 
           j.close AS jpy_usd
    FROM (
        SELECT date::DATE AS date, close
        FROM '{DATA_PATH}'
        WHERE symbol = '^GSPC' AND date >= '2024-01-01'
    ) s
    ASOF JOIN (
        SELECT date::DATE AS date, close
        FROM '{DATA_PATH}'
        WHERE symbol = 'JPYUSD=X' AND date >= '2024-01-01'
    ) j
      ON s.date >= j.date
    ORDER BY s.date
"""
).df()

print(sp500_jpy.head())
  trade_date  sp500_close  jpy_usd
0 2024-01-02    4742.8301   0.0071
1 2024-01-03    4704.8101   0.0070
2 2024-01-04    4688.6802   0.0070
3 2024-01-05    4697.2402   0.0069
4 2024-01-08    4763.5400   0.0069

s.date >= j.date 조건은 “주식 거래일보다 같거나 이전인 날짜 중 가장 가까운 날짜의 환율 데이터를 가져오라”는 의미입니다. 이를 통해 주말이나 휴일 등 거래일이 불일치해서 발생하는 결측치 문제를 우아하게 해결할 수 있습니다. 미래의 데이터를 훔쳐보는 Look-ahead 편향도 방지합니다.

19.6 DuckDB로 줄이고 pandas로 마무리하기

sp500_jpy DataFrame을 활용합니다. DuckDB가 조인과 필터링을 통해 작고 깨끗하게 만들어준 데이터를 이어받아, pandas의 기능을 이용해 파생 변수를 추가해 봅니다.

  1. S&P500 지수를 환율로 곱하여(또는 나누어, 해당 데이터 기준에 맞춰) “엔화 환산 S&P500 지수”라는 파생 변수를 추가하세요.

  2. (통상 JPY/USD는 1엔당 달러 가치이거나 1달러당 엔화 가치입니다. 편의상 sp500_close / jpy_usd 또는 데이터의 스케일을 확인해 적절히 연산하세요. 여기서는 JPY/USD 수치를 1엔당 달러 가치로 가정하고 sp500_close / jpy_usd로 환산된 엔화 가치를 구해봅니다.)

# pandas를 활용한 파생 변수 생성 및 마무리
# (데이터를 확인해보면 JPYUSD=X가 약 0.006~0.01 범위의 값을 가집니다. 즉 1엔 = x달러)
# 달러 기준 지수를 엔화 가치로 나누면 엔화 기준 지수가 됩니다.

sp500_jpy_final = sp500_jpy.assign(
    sp500_jpy_converted=lambda df: df["sp500_close"]
    / df["jpy_usd"]
)

print(sp500_jpy_final.head())
  trade_date  sp500_close  jpy_usd  sp500_jpy_converted
0 2024-01-02    4742.8301   0.0071        668004.239437
1 2024-01-03    4704.8101   0.0070        672115.728571
2 2024-01-04    4688.6802   0.0070        669811.457143
3 2024-01-05    4697.2402   0.0069        680759.449275
4 2024-01-08    4763.5400   0.0069        690368.115942

핵심 패턴인 “DuckDB가 줄이고, pandas가 마무리한다”를 충실히 따르는 과정입니다. 무거운 조인(특히 ASOF 조인)이나 큰 윈도우 집계는 DuckDB SQL로 데이터베이스 엔진의 최적화 이점을 누리고, 열 간의 간단한 사칙연산, 포맷팅, 시각화는 pandas의 유연하고 친숙한 문법을 활용하는 것이 실무 분석 파이프라인에서 가장 효율적입니다.