Skip to content

Fix "ValueError: could not convert string to float" in sklearn

Tested with: scikit-learn 1.9.1, pandas 3.0.6, numpy 2.5.3, scipy 1.18.1, Python 3.12.3, Linux (Intel i5-7500). Last run 2026-09-27.

TL;DR: the quoted value in the message ('Santiago', '$52,000', 'sqft') is the first cell NumPy could not turn into a float. Find which column it lives in, then:
  1. Real categories (city, color, product): put OneHotEncoder(handle_unknown='ignore') in a ColumnTransformer inside a Pipeline. pd.get_dummies() also fixes fit(), but on a new one-row DataFrame it produced 2 columns instead of 4 and predict() failed.
  2. Numbers stored as text ('$52,000', 'n/a', a repeated header row): clean the column, then pd.to_numeric(..., errors='coerce'). Coercing without stripping $ and , first turned every value into NaN.
  3. Ordered categories (low/medium/high): OrdinalEncoder(categories=[[...]]). Do not use LabelEncoder on features.

This error shows up the moment a scikit-learn estimator's fit() or predict() receives a column it can't treat as a number. Usually that is a categorical or text column left unencoded in a DataFrame that otherwise looks numeric; the other common cause is a numeric column that pandas read as text. This article reproduces both, covers the encoding fixes and where each one breaks, and the predict-time bug that a naive fix introduces without you noticing at training time.

Reproduced: Each Cause and the Fix That Worked

Every row below was run on the versions in the header. The error text is copied from the output. The estimator does not matter: LinearRegression, LogisticRegression, RandomForestClassifier and HistGradientBoostingClassifier all raised the same message on the same data, because the conversion happens in scikit-learn's shared input check before any model code runs.

What was passed to fit()Exact errorFix that worked
DataFrame with a city text columnValueError: could not convert string to float: 'Santiago'OneHotEncoder in a ColumnTransformer + Pipeline
Income column like '$52,000'ValueError: could not convert string to float: '$52,000'str.replace(r'[$,]', '', regex=True), then pd.to_numeric
Placeholder 'n/a' in a numeric columnValueError: could not convert string to float: 'n/a'pd.to_numeric(errors='coerce'), then impute or drop the NaN
Empty string in a numeric columnValueError: could not convert string to float: ''Same as 'n/a'
CSV with the header repeated mid-fileValueError: could not convert string to float: 'sqft'df.apply(pd.to_numeric, errors='coerce').dropna()
String target y like '1,000' in a regressorValueError: could not convert string to float: np.str_('1,000')Clean y the same way (classifiers accept string labels as-is)
get_dummies() output at predict timeValueError: The feature names should match those that were passed during fit.Use the pipeline, or reindex(columns=train_cols, fill_value=0)
Unseen category, OneHotEncoder() defaultsValueError: Found unknown categories ['Valparaiso'] in column 0 during transformhandle_unknown='ignore' (encodes it as all zeros)
Old tutorial code: OneHotEncoder(sparse=False)TypeError: OneHotEncoder.__init__() got an unexpected keyword argument 'sparse'sparse_output=False
Old tutorial code: enc.get_feature_names()AttributeError: 'OneHotEncoder' object has no attribute 'get_feature_names'get_feature_names_out()

Two things that did not raise: a text column holding clean numbers ('52000', even ' 75000 ' with spaces) converted silently, and LogisticRegression accepted string class labels in y without any encoding.

Reproducing the Error

Any DataFrame with a mix of numeric and string columns triggers this the moment it's passed to fit() without preprocessing:

import pandas as pd
from sklearn.linear_model import LinearRegression

df = pd.DataFrame({
    'sqft': [1000, 1500, 2000, 1200],
    'city': ['Santiago', 'Vina', 'Concepcion', 'Santiago'],
    'price': [100000, 150000, 200000, 110000]
})

X = df[['sqft', 'city']]
y = df['price']

LinearRegression().fit(X, y)
# ValueError: could not convert string to float: 'Santiago'

The end of the real traceback (paths shortened):

  File ".../sklearn/linear_model/_base.py", line 681, in fit
    X, y = validate_data(
  File ".../sklearn/utils/validation.py", line 1330, in check_X_y
    X = check_array(
  File ".../sklearn/utils/validation.py", line 1038, in check_array
    array = _asarray_with_order(array, order=order, dtype=dtype, xp=xp)
  File ".../sklearn/utils/_array_api.py", line 983, in _asarray_with_order
    array = numpy.asarray(array, order=order, dtype=dtype)
  File ".../pandas/core/generic.py", line 2027, in __array__
    arr = np.asarray(values, dtype=dtype)
ValueError: could not convert string to float: 'Santiago'

The traceback ends inside pandas and NumPy, and it never names the column. The only clue is the quoted value. On a wide DataFrame, search for it directly instead of scanning dtypes by eye:

X.columns[(X == 'Santiago').any()].tolist()   # ['city']

# or list every non-numeric column at once
X.select_dtypes(exclude='number').columns.tolist()   # ['city']
Note: This is not sklearn-specific plumbing, it's NumPy underneath. np.asarray(df[['sqft', 'city']], dtype=float) raises the identical could not convert string to float: 'Santiago' on its own, so the DataFrame's dtypes are the cause, not the estimator. In pandas 3 the city column has dtype str instead of object; the error is the same.

Numbers Stored as Strings

If the quoted value looks like a number ('$52,000') or a placeholder ('n/a', '') or a column name ('sqft'), the column is not categorical at all. pandas read it as text because some cells did not parse, and encoding it would be wrong. Clean it back into numbers instead:

d = pd.DataFrame({'income': ['$52,000', '$61,500', '$48,250', '$75,000'],
                  'age': [30, 41, 25, 50]})

# Wrong: coercing first turns every value into NaN
pd.to_numeric(d['income'], errors='coerce').tolist()
# [nan, nan, nan, nan]

# Right: strip the symbols, then convert
d['income'] = pd.to_numeric(d['income'].str.replace(r'[$,]', '', regex=True),
                            errors='coerce')
d['income'].tolist()
# [52000, 61500, 48250, 75000]

Without errors='coerce', pd.to_numeric tells you exactly where the first bad cell is: ValueError: Unable to parse string "$52,000" at position 0. To list every bad cell before deciding how to handle them, compare the raw and coerced versions:

s = df['income']   # here: ['52000', '61500', '48250', 'n/a']
bad = s[pd.to_numeric(s, errors='coerce').isna() & s.notna()]
print(bad.to_dict())   # {3: 'n/a'}

A header row repeated in the middle of a CSV (common after concatenating exported files) makes pandas read every column as text, and the error quotes the column name, 'sqft'. Coercing the whole frame and dropping the rows that failed removed it: 5 rows in, 4 numeric rows out, both columns float64.

df = df.apply(pd.to_numeric, errors='coerce').dropna()

Better still, fix it at read time. pd.read_csv(path, thousands=',') parsed "52,000" as the integer 52000, and thousands='.', decimal=',' parsed a European-format "100.000,5" as 100000.5.

Quick Fix: pd.get_dummies()

For a one-off script, one-hot encoding the categorical column with pandas directly is the fastest path to something that runs:

X_dummies = pd.get_dummies(X, columns=['city'])
print(X_dummies.columns.tolist())
# ['sqft', 'city_Concepcion', 'city_Santiago', 'city_Vina']

model = LinearRegression().fit(X_dummies, y)  # works

Since pandas 2.0 the dummy columns are bool, not uint8; scikit-learn accepted them without any cast. This resolves the immediate error. It also sets up a second failure at predict time, on different data.

The Hidden Bug: get_dummies() at Predict Time

pd.get_dummies() only creates columns for the categories actually present in whatever DataFrame you pass it. Training data and new data rarely contain the exact same set of categories, so calling it separately on each produces two DataFrames with different columns:

X_new = pd.DataFrame({'sqft': [1300], 'city': ['Vina']})
X_new_dummies = pd.get_dummies(X_new, columns=['city'])
print(X_new_dummies.columns.tolist())
# ['sqft', 'city_Vina']   -- only one city column, because only one city appears here

model.predict(X_new_dummies)
# ValueError: The feature names should match those that were passed during fit.
# Feature names seen at fit time, yet now missing:
# - city_Concepcion
# - city_Santiago

Training used three city columns because the training data contained three cities; this single-row prediction request only contains one, so get_dummies() only generates one. Scikit-learn correctly refuses to guess what the missing columns should contain and raises, rather than silently treating them as zero. Converting to a NumPy array to dodge the name check only changes the message: ValueError: X has 2 features, but LinearRegression is expecting 4 features as input.

If you have to keep get_dummies(), align the new frame to the training columns before predicting. This worked and gave the same prediction (140000.0) as the pipeline below:

X_new_aligned = X_new_dummies.reindex(columns=X_dummies.columns, fill_value=0)
model.predict(X_new_aligned)
Why this is worse than it looks: in a notebook, training and evaluation data are often built from the same one-time get_dummies() call, so this bug never appears during development. It first shows up in a serving path, on a single new row with fewer categories than the training data.

The LabelEncoder Trap

Reaching for LabelEncoder instead of one-hot encoding avoids the column-mismatch problem entirely, since it always produces exactly one integer column. That's also what makes it the wrong tool here:

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
codes = le.fit_transform(['Santiago', 'Vina', 'Concepcion', 'Santiago'])
print(codes, le.classes_.tolist())
# [1 2 0 1] ['Concepcion', 'Santiago', 'Vina']

This fits without error. The problem is what it encodes: alphabetically assigned integers with no real relationship to each other. A linear model fitted on this column gets one coefficient for "city", so it assumes Concepcion, Santiago and Vina sit on a line at equal steps, in alphabetical order. That is an artifact of the encoding, not a fact about the data. Tree-based models are less affected since they only ever compare against a threshold, but for anything based on distance or a linear coefficient, this distorts the feature, and nothing warns you. LabelEncoder's own documentation states it's intended for encoding the target variable y, not input features.

It also does not remove the predict-time problem. A city it never saw raises ValueError: y contains previously unseen labels: np.str_('Valparaiso'), and it has no handle_unknown option. If the categories really are ordered, use OrdinalEncoder with the order spelled out:

from sklearn.preprocessing import OrdinalEncoder

oe = OrdinalEncoder(categories=[['low', 'medium', 'high']],
                    handle_unknown='use_encoded_value', unknown_value=-1)
oe.fit(pd.DataFrame({'size': ['high', 'low', 'medium']}))
oe.transform(pd.DataFrame({'size': ['high', 'low', 'medium']})).ravel()
# [2. 0. 1.]
oe.transform(pd.DataFrame({'size': ['huge']}))
# [[-1.]]   (without handle_unknown: "Found unknown categories ['huge'] in column 0 during transform")

The Robust Fix: ColumnTransformer + OneHotEncoder

The version that avoids both problems above, fake ordering and predict-time column mismatches, wraps OneHotEncoder in a ColumnTransformer inside a Pipeline. The encoder remembers the exact categories seen at fit time and applies that same column layout to any future input, including handling a category it has never seen before:

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LinearRegression

preprocessor = ColumnTransformer(
    [('city_ohe', OneHotEncoder(handle_unknown='ignore'), ['city'])],
    remainder='passthrough'
)

pipe = Pipeline([
    ('prep', preprocessor),
    ('model', LinearRegression())
])

pipe.fit(X, y)

# Same single-row prediction that broke get_dummies() above
pipe.predict(pd.DataFrame({'sqft': [1300], 'city': ['Vina']}))
# array([140000.])

# A city never seen during training
pipe.predict(pd.DataFrame({'sqft': [1300], 'city': ['Valparaiso']}))
# returns a prediction: handle_unknown='ignore' encodes it as all zeros

pipe[:-1].get_feature_names_out().tolist()
# ['city_ohe__city_Concepcion', 'city_ohe__city_Santiago',
#  'city_ohe__city_Vina', 'remainder__sqft']

The default is handle_unknown='error'. With it, the same unseen city raised ValueError: Found unknown categories ['Valparaiso'] in column 0 during transform. Which one you want is a real choice: 'ignore' keeps a serving path up but quietly treats the new city as "none of the known cities", while 'error' tells you the first time new data drifts. If you pick 'ignore', log unseen categories somewhere so they don't go unnoticed.

For many text columns, select them by dtype instead of listing names. In pandas 3, make_column_selector(dtype_include=object) still matched the new str columns but emitted a Pandas4Warning saying that behavior is deprecated. make_column_selector(dtype_exclude='number') selected the same column with no warning, and the pipeline fit with both LogisticRegression and RandomForestClassifier:

from sklearn.compose import make_column_selector

preprocessor = ColumnTransformer(
    [('cat', OneHotEncoder(handle_unknown='ignore'),
      make_column_selector(dtype_exclude='number'))],
    remainder='passthrough'
)
Updating old code: OneHotEncoder(sparse=False) now fails with TypeError: OneHotEncoder.__init__() got an unexpected keyword argument 'sparse'; the parameter is sparse_output. And get_feature_names() is gone (AttributeError); use get_feature_names_out().

Summary: Which Fix Applies

Situation                                          Fix
────────────────────────────────────────────      ─────────────────────────────────────────────
Quoted value is '$52,000', 'n/a', '' or a header      Clean + pd.to_numeric(errors='coerce'); don't encode
One-off script or notebook, no future predict()     pd.get_dummies(), fine as-is
Model will call predict() on new data later          ColumnTransformer + OneHotEncoder in a Pipeline
Categories have a genuine order (low/medium/high)    OrdinalEncoder with an explicit categories= list
Encoding the target y for classification             LabelEncoder (its actual intended use)
Encoding an unordered input feature column            Never LabelEncoder; see ColumnTransformer fix above