2025, Oct 03 05:00

How to fix misplotted Plotly line charts caused by yfinance’s MultiIndex columns update

Plotly line chart wrong after updates? yfinance’s MultiIndex breaks Series selection. See three fixes: select ticker, flatten columns, or disable multi-level.

Plotly line chart looks wrong after updates? The yfinance MultiIndex change is the likely culprit

A seemingly straightforward Plotly line chart in Jupyter Notebook started misbehaving after library updates. The setup was minimal: grab one year of PETR4.SA from yfinance, take the Close prices, and draw a line. With Plotly 6.3.0 and recent yfinance releases, the output line plot no longer looked right.

As a reminder that APIs evolve, there’s a useful framing often cited around these changes:

“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.”

A minimal example that reproduces the issue

The following snippet fetches the data, removes timezone info from the index, and attempts to plot it directly. The logic is simple, yet the resulting chart is off because of how the data is shaped.

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()

What changed and why the plot looks wrong

The core of the problem is yfinance’s shift to MultiIndex columns in recent versions. Even when you download a single ticker, the returned DataFrame often has a two-level column index, with the top level representing fields like Close and the second level holding the ticker symbol. That means selecting ["Close"] now yields a DataFrame with a single column labeled by the ticker, not a flat Series. Passing that object directly to Plotly as y can produce unexpected rendering.

You can verify the structure by inspecting the type and columns before plotting:

type(px_close)
px_close.columns

With this change, you’ll see that the object is a DataFrame and the columns are tied to the ticker level, instead of a simple one-dimensional Series. The fix is to provide Plotly with a proper Series or flatten the columns first.

Fix 1: Select the ticker column explicitly

Keep the download step but index into the ticker column under Close so the y-axis receives a Series. Adding auto_adjust=False helps avoid a future warning about defaults.

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()

If you’re unsure what to select, quickly check the structure first:

type(close_frame)
close_frame.columns

Seeing something like Index(['PETR4.SA'], name='Ticker') confirms you can use close_frame['PETR4.SA'] as the y-series.

Fix 2: Ask yfinance for the legacy one-level columns

If you prefer a flat, old-style DataFrame for a single ticker, yfinance now provides a switch. Set multi_level_index=False in the download call and proceed as before. Again, auto_adjust=False avoids a 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()

Fix 3: Flatten the DataFrame by dropping the second level

You can also keep the default behavior and then remove the ticker level. After flattening, select the Close column and plot.

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()

A related hint that often appears side by side with this issue is that plotting failures or odd visuals can stem from a multi-indexed DataFrame. As one succinct summary puts it:

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

Why this is worth knowing

Time-series visualization pipelines are sensitive to subtle data-shape changes. When upstream libraries modify defaults—like yfinance introducing MultiIndex for downloads or changing the default of auto_adjust—assumptions baked into plotting code are exposed. Learning to quickly inspect type and columns, and to normalize shape before handing data to Plotly, saves time and avoids silent misplots in notebooks and dashboards.

Takeaways

When a Plotly chart looks off after an update, check the shape of your input first. With yfinance, a MultiIndex on columns means ["Close"] can return a DataFrame rather than a Series. Either select the ticker column explicitly, request multi_level_index=False at download time, or drop the extra level before plotting. Keeping an eye on arguments like auto_adjust also minimizes noisy warnings. A few seconds spent with type(...) and .columns can make the difference between puzzling over a broken chart and shipping a clean, accurate visualization.

The article is based on a question from StackOverflow by user30126350 and an answer by Wayne.