2025, Oct 16 12:17

ValueError при сравнении в pandas после yfinance: решаем проблему MultiIndex

Разбираем, почему после yfinance столбцы с MultiIndex в pandas вызывают ValueError при сравнении, и показываем два рабочих решения: точный ключ и «сплющивание».

Исправляем несоответствие при сравнении в pandas после yfinance: столбцы MultiIndex дают о себе знать

Получать OHLCV‑данные через yfinance и затем добавлять собственные технические столбцы несложно — пока вы не начинаете сравнивать колонки с разными метками. Если при «in‑place» обновлении DataFrame появляются сообщения ValueError, виновник часто скрывается в индексе столбцов. Ниже — практический разбор того, что именно идёт не так и как это аккуратно исправить.

Воспроизводим проблему

Цель проста: посчитать 200‑дневную простую скользящую среднюю (SMA), взять последний рабочий день каждого месяца и выставить индикатор в зависимости от того, закрытие выше или ниже SMA.

import pandas as pd
import yfinance as yf
from datetime import date
from dateutil.relativedelta import relativedelta

sym = 'AAPL'  # Apple Inc.
today_dt = date.today()
until_dt = today_dt
span_years = 3
since_dt = today_dt - relativedelta(days=span_years * 365.25 + 200)

try:
    quotes = yf.download(sym, start=since_dt, end=until_dt, auto_adjust=True)
except Exception as err:
    print(f"Error downloading data: {err}")
    raise SystemExit

if quotes.empty:
    print("No data downloaded. Please check the ticker and dates.")
    raise SystemExit

quotes['200_SMA'] = quotes['Close'].rolling(window=200).mean()
month_end = quotes.resample('BME').last().dropna()

month_end['Indicator'] = 'Hold'
# Эта строка вызывает ValueError
month_end['Indicator'] = [
    'Above' if row['Close'] > row['200_SMA'] else 'Below'
    for _, row in month_end.iterrows()
]

Типичные ошибки выглядят так:

ValueError: Можно сравнивать только объекты Series с идентичными метками
ValueError: Операнды не выровнены. Выполните left, right = left.align(right, axis=1, copy=False) перед операцией.

Что на самом деле происходит

yfinance возвращает DataFrame, у которого столбцы — это MultiIndex. Один уровень содержит имя ценового поля, например Close, High, Low, Open, Volume. Другой уровень — тикер. Для загрузки одного символа вы увидите кортежи вроде ("Close", "AAPL"). Когда вы добавляете пользовательский столбец вроде 200_SMA, у него оказывается другой ярлык второго уровня — ("200_SMA", "").

Теперь посмотрите, что делает код: он пытается вычислить row['Close'] > row['200_SMA']. Под капотом это превращается в сравнение Series с метками ("Close", "AAPL") и ("200_SMA", ""). Поскольку метки не совпадают, pandas отказывается выполнять операцию и выбрасывает ошибку выравнивания. Если позже вы загрузите больше тикеров, появятся и ("Close", "AMZN"), и ("Close", "GOOG") — это ещё нагляднее показывает, что второй уровень имеет значение.

Два способа это исправить

Вы можете либо обращаться к точным ключам MultiIndex, либо «сплющить» столбцы до одного уровня, чтобы сравнение шло по совпадающим меткам.

Вариант 1: Явно указывать полный MultiIndex

Мы сохраняем MultiIndex и выбираем по обоим уровням при сравнении.

import pandas as pd
import yfinance as yf
from datetime import date
from dateutil.relativedelta import relativedelta

sym = 'AAPL'
today_dt = date.today()
until_dt = today_dt
span_years = 3
since_dt = today_dt - relativedelta(days=span_years * 365.25 + 200)

quotes = yf.download(sym, start=since_dt, end=until_dt, auto_adjust=True)
if quotes.empty:
    raise SystemExit("No data downloaded. Please check the ticker and dates.")

quotes['200_SMA'] = quotes['Close'].rolling(window=200).mean()
month_end = quotes.resample('BME').last().dropna()

month_end['Indicator'] = [
    'Above' if row[('Close', 'AAPL')] > row[('200_SMA', '')] else 'Below'
    for _, row in month_end.iterrows()
]

Здесь ('Close', 'AAPL') и ('200_SMA', '') совпадают с реальными ярлыками столбцов, поэтому pandas может выровнять Series, и сравнение работает.

Вариант 2: Убрать уровень тикера (сплющить в один уровень)

Удаляем уровень "Ticker", чтобы и Close, и 200_SMA оказались в одноуровневом индексе столбцов.

import pandas as pd
import yfinance as yf
from datetime import date
from dateutil.relativedelta import relativedelta

sym = 'AAPL'
today_dt = date.today()
until_dt = today_dt
span_years = 3
since_dt = today_dt - relativedelta(days=span_years * 365.25 + 200)

quotes = yf.download(sym, start=since_dt, end=until_dt, auto_adjust=True)
if quotes.empty:
    raise SystemExit("No data downloaded. Please check the ticker and dates.")

# «Сплющиваем» столбцы, убрав второй уровень
flat = quotes.droplevel('Ticker', axis=1).copy()

flat['200_SMA'] = flat['Close'].rolling(window=200).mean()
month_end = flat.resample('BME').last().dropna()

month_end['Indicator'] = [
    'Above' if row['Close'] > row['200_SMA'] else 'Below'
    for _, row in month_end.iterrows()
]

В качестве альтернативы можно «сплющить» столбцы, сопоставив первый элемент каждого кортежа: flat.columns = [c[0] for c in flat.columns]. Главное — получить Series с идентичными метками к моменту сравнения.

Почему это важно

pandas выравнивает по меткам, а не по позициям, когда сравнивает Series или распространяет операции по DataFrame. С MultiIndex в столбцах легко случайно сравнить объекты, которые выглядят совместимыми, но различаются на одном из уровней индекса. Сообщения об ошибках подсказывают, что провалилось выравнивание, а не что «плохие» сами значения. Будьте явны с метками столбцов или сплющивайте их на нужном этапе — так вы избежите скрытых багов, особенно когда код начинает работать с несколькими тикерами.

Итоги

Если сравнения или присваивания падают с “Can only compare identically-labeled Series objects” или “Operands are not aligned”, проверьте индекс столбцов — не работаете ли вы с MultiIndex. В выводе yfinance ожидайте кортежи вроде ("Close", "AAPL"). Либо выбирайте по обоим уровням, либо заранее убирайте неиспользуемый уровень перед расчётом производных столбцов. Когда метки выровнены, обновление «на месте» проходит как задумано.

Статья основана на вопросе на StackOverflow от user23435723 и ответе от furas.