2025, Oct 03 05:16

Почему линейный график Plotly сломался: изменения MultiIndex в yfinance и как исправить

Линейный график Plotly в Jupyter стал «кривым» после обновлений yfinance? Разбираем изменения MultiIndex, показываем 3 способа исправить код и построить линию.

Линейный график Plotly выглядит странно после обновлений? Скорее всего, причина — изменение MultiIndex в yfinance

Кажущийся простым линейный график Plotly в Jupyter Notebook стал вести себя некорректно после обновлений библиотек. Настройка минимальная: забрать год данных по PETR4.SA из yfinance, взять цены Close и построить линию. С Plotly 6.3.0 и свежими версиями yfinance итоговый график перестал выглядеть правильно.

Как напоминание о том, что API со временем меняются, часто приводят такую мысль:

“YFinance, the popular Python library for retrieving financial data, has introduced significant changes in its latest version 0.2.51 (released in December 2024). These updates have caused confusion among users but also bring interesting functional improvements. In this article, we’ll explore the key updates, how to handle them, and provide practical code examples.”

Минимальный пример, воспроизводящий проблему

Ниже — фрагмент кода, который загружает данные, убирает информацию о часовом поясе из индекса и пытается сразу их отобразить. Логика примитивная, но итоговый график смещён из‑за формы данных.

import yfinance as yf
import numpy as np
import pandas as pd
import statsmodels.api as smx
from statsmodels.tsa.stattools import coint, adfuller
import plotly.graph_objects as go
px_close = yf.download("PETR4.SA", period="1y")["Close"]
px_close.index = px_close.index.tz_localize(None)
chart = go.Figure()
chart.add_trace(go.Scatter(x=px_close.index, y=px_close))
chart.update_layout(title_text="petr", width=500, height=500)
chart.show()

Что изменилось и почему график отображается некорректно

Суть проблемы — переход yfinance в последних версиях к столбцам с многоуровневым индексом (MultiIndex). Даже при загрузке одного тикера возвращаемый DataFrame часто имеет двухуровневый индекс столбцов: верхний уровень — поля вроде Close, второй — сам тикер. В итоге выборка ["Close"] теперь даёт DataFrame с единственным столбцом, помеченным тикером, а не плоскую Series. Передача такого объекта напрямую в Plotly как параметра y может привести к неожиданному отображению.

Проверить структуру можно, глянув на тип и столбцы до построения графика:

type(px_close)
px_close.columns

Из‑за этого изменения вы увидите, что объект — это DataFrame, а столбцы привязаны к уровню тикера, а не простая одномерная Series. Исправление — передать Plotly корректную Series или сначала «сплющить» столбцы.

Исправление 1: Явно выбрать столбец тикера

Сохраняем шаг загрузки, но внутри Close индексируемся по столбцу с тикером, чтобы по оси y ушла именно Series. Добавление auto_adjust=False помогает избежать будущего предупреждения о значениях по умолчанию.

import yfinance as yf
import numpy as np
import pandas as pd
import statsmodels.api as smx
from statsmodels.tsa.stattools import coint, adfuller
import plotly.graph_objects as go
close_frame = yf.download("PETR4.SA", period="1y", auto_adjust=False)["Close"]
close_frame.index = close_frame.index.tz_localize(None)
line_fig = go.Figure()
line_fig.add_trace(go.Scatter(x=close_frame.index, y=close_frame["PETR4.SA"]))
line_fig.update_layout(title_text="petr", width=500, height=500)
line_fig.show()

Если не уверены, что выбирать, быстро посмотрите на структуру:

type(close_frame)
close_frame.columns

Если видите что‑то вроде Index(['PETR4.SA'], name='Ticker'), значит можно использовать close_frame['PETR4.SA'] как серию для оси y.

Исправление 2: Попросить у yfinance «старые» одномерные столбцы

Если удобнее работать с плоским DataFrame для одного тикера, в yfinance появилась опция. Установите multi_level_index=False при загрузке и работайте как раньше. Опять же, auto_adjust=False избавит от FutureWarning.

import yfinance as yf
import numpy as np
import pandas as pd
import statsmodels.api as smx
from statsmodels.tsa.stattools import coint, adfuller
import plotly.graph_objects as go
flat_close = yf.download(
    "PETR4.SA",
    period="1y",
    multi_level_index=False,
    auto_adjust=False
)["Close"]
flat_close.index = flat_close.index.tz_localize(None)
flat_fig = go.Figure()
flat_fig.add_trace(go.Scatter(x=flat_close.index, y=flat_close))
flat_fig.update_layout(title_text="petr", width=500, height=500)
flat_fig.show()

Исправление 3: «Сплющить» DataFrame, убрав второй уровень

Можно оставить поведение по умолчанию и затем убрать уровень тикера. После «сплющивания» выберите столбец Close и постройте график.

import yfinance as yf
import numpy as np
import pandas as pd
import statsmodels.api as smx
from statsmodels.tsa.stattools import coint, adfuller
import plotly.graph_objects as go
flattened = yf.download("PETR4.SA", period="1y").droplevel(1, axis=1)["Close"]
flattened.index = flattened.index.tz_localize(None)
fixed_fig = go.Figure()
fixed_fig.add_trace(go.Scatter(x=flattened.index, y=flattened))
fixed_fig.update_layout(title_text="petr", width=500, height=500)
fixed_fig.show()

Смежная подсказка, которая часто идёт рука об руку с этой проблемой: сбои при построении или странная картинка нередко возникают из‑за многоуровневого DataFrame. Как метко подытожено в одном ответе:

“Your error was caused by the following code ... This is because df is multi-indexed.”

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

Конвейеры визуализации временных рядов чувствительны к даже небольшим изменениям формы данных. Когда «верхние» библиотеки меняют значения по умолчанию — вроде введения MultiIndex в yfinance или смены дефолта auto_adjust — оголяются предположения, заложенные в код построения графиков. Привычка быстро проверять type и .columns и приводить данные к ожидаемой форме перед передачей в Plotly экономит время и спасает от тихих неверных построений в ноутбуках и дашбордах.

Выводы

Если после обновлений график Plotly выглядит подозрительно, сначала проверьте форму входных данных. В yfinance наличие MultiIndex в столбцах означает, что ["Close"] может вернуть DataFrame, а не Series. Либо явно выбирайте столбец тикера, либо запрашивайте multi_level_index=False при загрузке, либо удаляйте лишний уровень перед построением. Следите и за аргументами вроде auto_adjust, чтобы не ловить лишние предупреждения. Пара секунд на type(...) и .columns часто отделяют долгое разбирательство от быстрого, корректного графика.

Статья основана на вопросе на StackOverflow от user30126350 и ответе Wayne.