ValueError: Found input variables with inconsistent numbers of samples" in scikit-learnThis 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.
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.
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.
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
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.
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.
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.
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.
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