Fix "ValueError: Found input variables with inconsistent numbers of samples" in scikit-learn

Fix "ValueError: Found input variables with inconsistent numbers of samples" in scikit-learn

This one shows up after a pipeline has clearly worked before: the model trained fine yesterday, then a small upstream change, a new dropna call, a filter, a reshaped feature, and suddenly fit() refuses to run. Nothing about the error message points at the actual line that caused it. This article reproduces the error, walks through the two most common real-world causes (an asymmetric dropna, and a time-series shift() that trims one side but not the other), and gives a one-line check that catches the mismatch immediately instead of several steps downstream.

Reproducing the Error

Any estimator's fit(X, y) raises this the moment X and y disagree on how many rows they have, however that mismatch happened:

import numpy as np
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(0)
X = rng.random((10, 3))
y = rng.integers(0, 2, size=9)  # one row short

LogisticRegression().fit(X, y)
# ValueError: Found input variables with inconsistent numbers of samples: [10, 9]

The message names the two counts it found, 10 and 9, and stops there. It doesn't say which rows are missing, which of the two arrays is "wrong," or where in your code the mismatch was introduced. On a real pipeline, tracking that down is almost always more work than fixing it once found.

What scikit-learn Is Actually Checking

The check itself is simple and lives in sklearn.utils.validation.check_consistent_length, called internally by fit(), train_test_split(), and most other functions that take paired arrays:

from sklearn.utils.validation import check_consistent_length

check_consistent_length(X, y)
# ValueError: Found input variables with inconsistent numbers of samples: [10, 9]

It only compares len() (or .shape[0] for arrays) across every argument passed in. It has no idea what your data represents or how the two arrays are supposed to relate, so it can't tell you why they diverged, only that they did.

Cause 1: Dropping NaN Rows From Only One Side

By far the most common real cause: dropna() (or a boolean filter) applied to the features but not the target, or vice versa, because at the time it was written the two were still living in the same DataFrame and it was easy to forget they'd need to move together:

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression

df = pd.DataFrame({'a': [1, 2, np.nan, 4, 5], 'b': [10, 20, 30, 40, 50]})

X = df[['a']].dropna()   # drops the row where 'a' is NaN -> 4 rows
y = df['b']               # untouched -> 5 rows

print(len(X), len(y))
# 4 5

LinearRegression().fit(X, y)
# ValueError: Found input variables with inconsistent numbers of samples: [4, 5]

X lost a row that y never lost, because the dropna() call only ever touched X. The fix is to compute the drop condition once and apply it to both sides together, rather than dropping independently:

mask = df['a'].notna()
X_fixed = df.loc[mask, ['a']]
y_fixed = df.loc[mask, 'b']

print(len(X_fixed), len(y_fixed))
# 4 4

LinearRegression().fit(X_fixed, y_fixed)  # works
Simpler when it's a single DataFrame: if a and b live in the same frame, df.dropna(subset=['a']) then split into X/y afterward avoids the asymmetry entirely, since both come from the same already-filtered rows.

Cause 2: A Time-Series shift() That Isn't Matched on Both Sides

The second common source is a target built from shift(), common when predicting a future value from current features. shift() introduces a NaN at one edge of the series, and it's easy to clean up the target's NaN without remembering the features need the same rows removed:

df = pd.DataFrame({'price': [100, 102, 101, 105, 107, 110, 108]})
df['target'] = df['price'].shift(-1)  # predict next day's price

X = df[['price']]           # still 7 rows
y = df['target'].dropna()   # last row's target is NaN -> 6 rows

print(len(X), len(y))
# 7 6

LinearRegression().fit(X, y)
# ValueError: Found input variables with inconsistent numbers of samples: [7, 6]

Here too, the fix is to drop from the combined frame before splitting into X and y, so both are trimmed by the exact same row:

df_clean = df.dropna()
X_fixed = df_clean[['price']]
y_fixed = df_clean['target']

print(len(X_fixed), len(y_fixed))
# 6 6

LinearRegression().fit(X_fixed, y_fixed)  # works

The pattern in both causes is the same: whenever a filtering or shifting step is applied after X and y have already been split into separate objects, it's very easy for that step to touch only one of them. Doing the filtering while they're still one object, then splitting last, removes the whole failure mode.

What This Error Is Not: 1D vs. 2D Shape

It's worth being precise about what this error doesn't mean, since it's easy to conflate with a different, unrelated scikit-learn complaint. Passing a single feature as a 1D array (X.shape == (100,) instead of (100, 1)) raises "Expected 2D array, got 1D array instead", a separate error covered in a companion article, not this one. This article's error is specifically about the number of rows disagreeing between two or more arrays, regardless of how many columns or dimensions each one has.

The One-Line Check That Catches It Early

Because the error only surfaces at fit() time, several transformation steps removed from whatever caused it, a cheap assertion right after any row-count-changing step (a dropna, a filter, a merge, a shift()) pinpoints the actual offending line instead of leaving you to trace backward from the traceback:

assert X.shape[0] == len(y), f"X has {X.shape[0]} rows, y has {len(y)}"

Placed after every step that touches row count independently on either side, this turns a confusing fit()-time failure into an immediate, specific one at the exact transformation that broke alignment.

Summary: Which Fix Applies

Situation                                                Fix
────────────────────────────────────────────────────    ─────────────────────────────────────────────
dropna()/a filter applied to X or y separately            Apply the same mask/dropna to both, or filter
                                                              the combined frame before splitting
shift() used to build a target                            dropna() the combined frame first, then split
                                                              into X and y, don't dropna() y alone
Not sure where the mismatch was introduced                Add `assert X.shape[0] == len(y)` after each
                                                              row-count-changing step to localize it
Getting a different error about array shape (1D vs 2D)    That's a separate error, see the related
                                                              "Expected 2D array" post below