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"). Либо выбирайте по обоим уровням, либо заранее убирайте неиспользуемый уровень перед расчётом производных столбцов. Когда метки выровнены, обновление «на месте» проходит как задумано.