데이터분석

Multilndex

장수우 2025. 10. 9. 22:01

yfinance로 수집한 주가 데이터를 MySQL에 저장하고 다시 불러오는 과정에서, Unknown column 'Date', Unknown column 'Open' 이라는 에러에 대해 제가 해결했던 방법을 기록합니다.

Pandas의 'MultiIndex(다중 인덱스)' 구조를 이해하고 해결하는 과정을 담은 기록입니다.


yf.download()로 데이터를 수집하고, to_sql()로 DB에 저장한 뒤, read_sql()로 다시 데이터를 읽어오는 간단한 코드였습니다. 하지만 SQL 쿼리 실행 단계에서 계속해서 특정 컬럼을 찾을 수 없다는 ProgrammingError가 발생했습니다.

# 문제가 발생했던 SQL 쿼리
sql_query = "SELECT Open, High, Low, Close FROM ohlcv ORDER BY Date" 
# -> ProgrammingError: Unknown column 'Open' in 'field list'

 

Pandas의 MultiIndex(또는 Multi-level Index) 입니다. 여러 종목의 데이터를 한 번에 다룰 때, 어떤 데이터가 어떤 종목에 속하는지 구분하기 위해 이름표를 이중으로 붙이는 기능이죠. 예를 들어 ('Open', 'AAPL')은 'AAPL 종목의 시가'를, ('Open', 'GOOG')은 'GOOG 종목의 시가'를 의미합니다.


해결책: get_level_values()로 이름표 단일화하기

원인을 알았으니 해결은 간단합니다. 데이터베이스에 저장하기 전에, 단순한 단일 이름표로 바꿔주면 됩니다.

# yfinance로 데이터를 가져오면 MultiIndex 컬럼이 생성됨
df = yf.download(['AAPL', 'GOOG'], start='2025-01-01')

# 이 부분이 핵심!
# 이중 이름표 중 위에 있는 진짜 이름('Open', 'High'...)만 떼어내서
# 컬럼 이름으로 다시 설정합니다.
df.columns = df.columns.get_level_values(0)

print(df.head())
# 이제 컬럼이 'Open', 'High', 'Low', 'Close' 등으로 깔끔하게 정리됩니다.

추가학습내용

이번 에러를 계기로 함께 공부하면 좋은 Pandas의 개념들을 정리해 보았습니다.

  • MultiIndex(다중 인덱스) 구조의 이해:
    • 다중 인덱스는 행(row)에도, 열(column)에도 적용될 수 있습니다. 데이터를 그룹별로 계층적으로 분석할 때 매우 강력한 기능입니다. 공식 문서나 잘 정리된 튜토리얼을 통해 다중 인덱스를 직접 만들고, 특정 계층의 데이터를 선택하는 방법을 연습해보는 것이 좋습니다.
      import pandas as pd
      
      data = {'Date': ['2025-10-08', '2025-10-08', '2025-10-09', '2025-10-09'],
              'Ticker': ['AAPL', 'GOOG', 'AAPL', 'GOOG'],
              'Close': [170, 140, 172, 141],
              'Volume': [5000, 3000, 5500, 3200]}
      long_df = pd.DataFrame(data)
      
      print(long_df)
       아래와 같은 긴 형태의 주가 데이터가 있다고 가정하면 아래와 같은 출력이 나옵니다.
               Date Ticker  Close  Volume
      0  2025-10-08   AAPL    170    5000
      1  2025-10-08   GOOG    140    3000
      2  2025-10-09   AAPL    172    5500
      3  2025-10-09   GOOG    141    3200
  • stack() & unstack():
                       Close  Volume
    Date       Ticker               
    2025-10-08 AAPL       170    5000
               GOOG       140    3000
    2025-10-09 AAPL       172    5500
               GOOG       141    3200
    • stack()은 컬럼을 인덱스로 '쌓는'(=아래로) 기능이고
      # unstacked_df를 다시 stack
      stacked_df = unstacked_df.stack(level='Ticker')
      print(stacked_df)
    •                    Close  Volume
      Date       Ticker               
      2025-10-08 AAPL       170    5000
                 GOOG       140    3000
      2025-10-09 AAPL       172    5500
                 GOOG       141    3200

      • stack은 unstack의 정반대 작업입니다. 컬럼의 일부를 다시 행 인덱스로 '쌓아서' 데이터를 더 길게 만듭니다.
    • unstack()은 인덱스를 컬럼으로 '펼치는'(= 옆으로) 기능입니다.
      # Ticker 레벨의 인덱스를 컬럼으로 unstack
      unstacked_df = df_multi_index.unstack(level='Ticker')
      print(unstacked_df)
    •              Close         Volume       
      Ticker        AAPL   GOOG    AAPL   GOOG
      Date                                    
      2025-10-08     170    140    5000   3000
      2025-10-09     172    141    5500   3200

      • 보시는 것처럼 unstack은 행 인덱스의 일부였던 Ticker를 컬럼 레벨로 이동시켜, 우리가 yfinance에서 처음 봤던 것과 같은 MultiIndex 컬럼 구조를 만듭니다.
    • stack() & unstack()을 사용하면 넓은 형태(wide-format)의 데이터를 긴 형태(long-format)로 쉽게 바꿀 수 있습니다
  • pivot_table(): (= 넓게 펼치기)
    • 엑셀의 피벗 테이블과 동일한 기능입니다. 주어진 데이터로부터 원하는 행, 열, 값을 지정하여 데이터를 재구조화하고 요약 통계를 볼 수 있습니다.
    • pivot_table의 결과물은 MultiIndex 구조를 갖는 경우가 많아, 위 개념들과 함께 익히면 시너지가 좋습니다.
    • index : 새로운 표의 '행'이 될 칼럼(Data)
    • columns : 새로운 표의 '열'이 될 칼럼(Ticker)
      # Date를 행으로, Ticker를 열로, Close를 값으로 하는 피벗 테이블 생성
      wide_df = long_df.pivot_table(index='Date', columns='Ticker', values='Close')
      
      print(wide_df)
    •  
    • pivot_table(index='Date', columns='Ticker', values='Close')
      Ticker      AAPL   GOOG
      Date                   
      2025-10-08   170    140
      2025-10-09   172    141
      이 결과물은 'Date'를 행 인덱스로, 'Ticker'를 열 인덱스로 갖는 MultiIndex 컬럼 구조와 유사한 형태가 됩니다.

  • 메서드 체이닝 (Method Chaining):
    • 메서드 체이닝이란, 여러 개의 데이터 처리/조작 함수(메서드)를 점(.)으로 계속 연결하여 한 줄의 코드로 표현하는 기법입니다.
    • 이점
      1. 가독성: 데이터가 어떤 순서로 변환되는지 위에서 아래로, 왼쪽에서 오른쪽으로 자연스럽게 읽힙니다.
      2. "A하고, 그리고(.) B하고, 그리고(.) C한다" 처럼 이야기 흐름이 만들어집니다.
      3. 효율성: df1, df2, df3 와 같이 불필요한 중간 과정 변수를 만들지 않아 코드가 간결해지고 메모리를 효율적으로 사용할 수 있습니다.
      예제: 메서드 체이닝 사용 전 vs. 후
      1. 결측치가 있는 행을 제거한다.
      2. 'Volume'이 4000 이상인 데이터만 필터링한다.
      3. 'Ticker' 열을 기준으로 그룹화하여 'Close' 가격의 평균을 계산한다.
      4. 결과를 내림차순으로 정렬한다.
    • 사용 전
      # 예제 데이터
      data = {'Date': ['2025-10-08', '2025-10-08', '2025-10-09', '2025-10-09', '2025-10-10'],
              'Ticker': ['AAPL', 'GOOG', 'AAPL', 'GOOG', 'AAPL'],
              'Close': [170, 140, 172, 141, None], # 결측치 포함
              'Volume': [5000, 3000, 5500, 3200, 6000]}
      df = pd.DataFrame(data)
      
      # 1단계: 결측치 제거
      df_no_na = df.dropna()
      
      # 2단계: Volume 필터링
      df_filtered = df_no_na[df_no_na['Volume'] >= 4000]
      
      # 3단계: 그룹별 평균 계산
      df_grouped = df_filtered.groupby('Ticker')['Close'].mean()
      
      # 4단계: 정렬
      final_result = df_grouped.sort_values(ascending=False)
      
      print(final_result)
  • 사용 후
    • 위의 모든 단계를 하나의 흐름으로 연결합니다. 괄호 ()를 사용하면 여러 줄에 걸쳐 가독성 좋게 작성할 수 있습니다.
      # 예제 데이터 (동일)
      df = pd.DataFrame(data)
      
      # 모든 단계를 하나의 체인으로 연결
      final_result = (df.dropna()
                        .query("Volume >= 4000")  # query 메서드를 사용하면 필터링이 더 깔끔해집니다.
                        .groupby('Ticker')['Close']
                        .mean()
                        .sort_values(ascending=False)
                     )
      
      print(final_result)

 

 

 

 

 

 

반응형