QuantConnect/Lean · error · AssertionError

Could not unstack df. Columns: {', '.join(df.columns)} | {co

Error message

Could not unstack df. Columns: {', '.join(df.columns)} | {column}

What it means

Guard in assert_history_index that the expected column exists in the dataframe before unstacking/accessing it. The helper later does df[column].unstack(level=0) and df.at[(...), column], so the column must be present. The assertion fires when the history frame's columns do not include the requested column name, indicating the unstack/indexing strategy will not work for this data.

Source

Thrown at Algorithm.Python/PandasDataFrameHistoryAlgorithm.py:99

        # we can use these TradeBars to initialize indicators or perform other math
        self.spy_daily_sma.reset()
        for index, trade_bar in trade_bar_history.loc["SPY"].iterrows():
            self.spy_daily_sma.update(index, trade_bar["close"])

        # we can loop over the return values from these functions and we'll get Quandl data
        # this can be used in much the same way as the trade_bar_history above
        self.spy_daily_sma.reset()
        for index, quandl in quandl_history.loc["CHRIS/CME_SP1"].iterrows():
            self.spy_daily_sma.update(index, quandl["settle"])

        self.set_holdings(self.eur, 1)

    def assert_history_index(self, df, column, expected, ticker, symbol):

        if df.empty:
            raise AssertionError(f"Empty history data frame for {symbol}")
        if column not in df:
            raise AssertionError(f"Could not unstack df. Columns: {', '.join(df.columns)} | {column}")

        value = df.iat[0,0]
        df2 = df.xs(df.index.get_level_values('time')[0], level='time')
        df3 = df[column].unstack(level=0)

        try:

            # str(Symbol.ID)
            self.assert_history_count(f"df.iloc[0]", df.iloc[0], len(df.columns))
            self.assert_history_count(f"df.loc[str({symbol.id})]", df.loc[str(symbol.id)], expected)
            self.assert_history_count(f"df.xs(str({symbol.id}))", df.xs(str(symbol.id)), expected)
            self.assert_history_count(f"df.at[(str({symbol.id}),), '{column}']", list(df.at[(str(symbol.id),), column]), expected)
            self.assert_history_count(f"df2.loc[str({symbol.id})]", df2.loc[str(symbol.id)], len(df2.columns))
            self.assert_history_count(f"df3[str({symbol.id})]", df3[str(symbol.id)], expected)
            self.assert_history_count(f"df3.get(str({symbol.id}))", df3.get(str(symbol.id)), expected)

            # str(Symbol)
            self.assert_history_count(f"df.loc[str({symbol})]", df.loc[str(symbol)], expected)

View on GitHub (pinned to d2c3659f87)

Solutions

  1. Log df.columns to see the actual available columns and pick the correct name.
  2. If using custom data, verify the data type's properties and value_column_name still produce the expected column.
  3. After a Lean/pandas upgrade, re-check the column-generation contract for that data type and update the expected column name.
  4. Confirm the dataframe is at the expected index level (symbol/time) so column lookup is valid.

Example fix

# before
if column not in df:
    raise AssertionError(f"Could not unstack df. Columns: {', '.join(df.columns)} | {column}")

# after (report columns in a stable, sorted form and hint at the mismatch)
if column not in df.columns:
    raise AssertionError(
        f"Could not unstack df; '{column}' missing. "
        f"Available columns: {', '.join(map(str, sorted(df.columns)))}")
Defensive patterns

Strategy: validation

Validate before calling

# Verify the column exists (and report actual columns) before unstacking
def assert_history_index(self, df, column, expected, ticker, symbol):
    if column not in df.columns:
        self.debug(f"'{column}' not in {sorted(map(str, df.columns))} for {symbol}")
        return

Type guard

def has_column(df, column):
    """True when the dataframe exposes the requested column."""
    return df is not None and column in df.columns

Prevention

When it happens

Trigger: column not in df.columns for a history dataframe. This happens when the data type's properties do not include the expected column (e.g. asking for 'settle' on data that does not expose it), when the dataframe is multi-indexed and the column lives at a different level, or when Lean's PandasData column generation for that type changed/renamed the field.

Common situations: A custom data type (e.g. QuandlFuture expecting 'Settle') no longer exposes the column after a rename or a change to value_column_name. The history frame came back with a different column layout after a Lean or pandas upgrade. The caller passed the wrong column name for the requested data type.

Related errors


AI-assisted analysis of QuantConnect/Lean@d2c3659f87 (2026-08-13). Data as JSON: /api/errors/6a23ec75782e6935. Report an issue: GitHub.