X does not have valid feature names" WarningSince scikit-learn 1.0, every estimator remembers the column names it saw during fit() if you passed a pandas DataFrame. If a later call to transform() or predict() gets a plain NumPy array instead, or a DataFrame with a different naming state, scikit-learn raises a UserWarning instead of silently proceeding. This article covers exactly what triggers it, when it's actually a bug versus harmless noise, and the real ValueError variant it's easy to confuse with.
Here is the warning in its two directions, both reproduced directly:
UserWarning: X does not have valid feature names, but StandardScaler was fitted with feature names
UserWarning: X has feature names, but StandardScaler was fitted without feature names
Both come from the same underlying mechanism: when fit() receives a pandas DataFrame, sklearn stores the column names on the estimator as feature_names_in_. Every subsequent call checks whether the new input's "has names" state matches what was recorded at fit time. A mismatch in either direction triggers the warning; it does not require the names themselves to be different, just present-vs-absent.
The most common trigger: training code uses a DataFrame (because that's what pd.read_csv() returns), but a downstream inference or serving path converts to a raw array first, often via .values, .to_numpy(), or a JSON payload turned straight into an array.
import pandas as pd
from sklearn.preprocessing import StandardScaler
df = pd.DataFrame({'age': [25, 32, 47], 'income': [40000, 55000, 82000]})
scaler = StandardScaler()
scaler.fit(df) # feature_names_in_ = ['age', 'income']
# Triggers the warning: NumPy array, but scaler expects named columns
scaled = scaler.transform(df.values)
# UserWarning: X does not have valid feature names, but StandardScaler was fitted with feature names
The identical pattern shows up with any estimator, not just transformers: predict() on a model fitted with a DataFrame behaves the same way.
from sklearn.linear_model import LinearRegression
X = pd.DataFrame({'x1': [1, 2, 3, 4], 'x2': [2, 4, 6, 8]})
y = [1, 2, 3, 4]
model = LinearRegression().fit(X, y)
predictions = model.predict(X.values)
# UserWarning: X does not have valid feature names, but LinearRegression was fitted with feature names
The mirror-image case, less common in practice but just as real, usually happens when a model is trained on array data (e.g. loaded from a .npy file or a non-pandas pipeline) and later reused with DataFrame input during evaluation or serving:
import pandas as pd
from sklearn.preprocessing import StandardScaler
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
scaler = StandardScaler()
scaler.fit(df.values) # no feature_names_in_ recorded, plain array
scaled = scaler.transform(df[['b', 'a']])
# UserWarning: X has feature names, but StandardScaler was fitted without feature names
Note that in this specific example, the column order was also swapped (['b', 'a'] instead of ['a', 'b']), and sklearn has no way to catch that, because it never recorded any names to check against in the first place. This is exactly the silent failure mode covered next.
A different, stricter case: if both sides have names, and the names themselves genuinely disagree, sklearn raises a hard ValueError instead of a warning. This one blocks execution rather than just alerting you:
import pandas as pd
from sklearn.preprocessing import StandardScaler
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
scaler = StandardScaler().fit(df)
df_renamed = df.rename(columns={'a': 'x', 'b': 'y'})
scaler.transform(df_renamed)
ValueError: The feature names should match those that were passed during fit.
Feature names unseen at fit time:
- x
- y
Feature names seen at fit time, yet now missing:
- a
- b
UserWarning variants (Causes 1 and 2 above) happen when one side has names and the other doesn't, so sklearn can't fully check and just warns. The ValueError happens when both sides have names and they don't match, so sklearn can fully check, and refuses to proceed. If you're only ever seeing the warning and never this error, your column names are consistent when both sides have them; the warning is purely about the has-names/no-names mismatch.
The warning itself does not mean your results are wrong. If a NumPy array's columns are in the exact same order sklearn learned during fit(), the underlying computation is identical with or without names attached, so the warning is purely informational in that case.
It becomes a real bug in one specific situation: a NumPy array's column order silently differs from what the estimator was fitted on. Because arrays carry no names to check, sklearn cannot detect this case at all. You get no warning, no error, just wrong predictions computed from mismatched features. This is precisely the failure mode the feature-names check was added to help catch, by encouraging DataFrame use (or explicit `feature_names_in_` checks) end-to-end instead of dropping to arrays partway through a pipeline.
| Situation | Risk |
|---|---|
| DataFrame → array, same column order preserved | Cosmetic warning only, results correct |
| DataFrame → array, column order silently changed | Real bug: wrong predictions, no error raised |
| DataFrame → DataFrame, columns renamed/reordered | Caught: raises ValueError, blocks execution |
The most robust fix is to simply never convert to a NumPy array between fit() and transform()/predict(). If your serving code receives a dict or JSON payload, build a single-row DataFrame from it instead of an array:
import pandas as pd
# Instead of: np.array([payload['age'], payload['income']]).reshape(1, -1)
X_new = pd.DataFrame([payload])[scaler.feature_names_in_]
scaled = scaler.transform(X_new) # no warning, and column order is enforced by name
Indexing with [scaler.feature_names_in_] is doing real work here beyond silencing the warning: it reorders the incoming columns to match what the estimator was fitted on, which is exactly the silent-bug scenario from the table above, closed by construction.
When a NumPy array is unavoidable (e.g. a hot inference path that can't afford DataFrame overhead), compare against feature_names_in_ once at startup rather than trusting positional order silently:
expected = list(scaler.feature_names_in_)
actual_order = ['age', 'income'] # whatever produces your array's column order
assert actual_order == expected, (
f"Column order mismatch: array provides {actual_order}, "
f"model expects {expected}"
)
scaled = scaler.transform(some_array) # still warns, but order is now verified safe
If you've verified column order is always consistent and the warning is just log noise, scope the suppression narrowly rather than globally:
import warnings
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=UserWarning, message='.*feature names.*')
scaled = scaler.transform(X_array)
Symptom Fix
──────────────────────────────────────────── ─────────────────────────────────────────────
UserWarning, array after DataFrame fit Keep DataFrames end-to-end (Fix 1), or verify order (Fix 2)
UserWarning, DataFrame after array fit Same fixes, reversed direction
ValueError: feature names should match Real mismatch, rename/reorder columns to match fit-time names
Warning is confirmed harmless, just noisy logs Scope a filterwarnings() suppression (Fix 3)