Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions python-package/lightgbm/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,7 @@ def __init__(
#
# This is here mostly for scikit-learn's benefit, as it tracks whether input data had feature names.
self._has_non_default_feature_names: bool = False
self.original_feature_name_: np.ndarray = np.array([], dtype=object)

def __del__(self) -> None:
try:
Expand Down Expand Up @@ -2061,14 +2062,19 @@ def _lazy_init(
categorical_feature=categorical_feature,
pandas_categorical=self.pandas_categorical,
)
self.original_feature_name_ = np.array(feature_name, dtype=object)
if nwd.is_into_dataframe(data) and feature_name == "auto":
feature_name = nw.from_native(data).schema.names()
self.original_feature_name_ = np.array(feature_name, dtype=object)

# 'feature_name == "auto"' after the block above means no feature names were provided
# by either the data type (DataFrame/pyarrow) or the user's 'feature_name' argument.
# LightGBM will assign auto-generated names like Column_0, Column_1, etc.
self._has_non_default_feature_names = feature_name != "auto"

if feature_name != "auto":
self.original_feature_name_ = np.array(feature_name, dtype=object)

# process for args
params = {} if params is None else params
args_names = inspect.signature(self.__class__._lazy_init).parameters.keys()
Expand Down Expand Up @@ -2966,6 +2972,7 @@ def set_feature_name(self, feature_name: _LGBM_FeatureNameConfiguration) -> "Dat
if feature_name != "auto":
self.feature_name = feature_name
self._has_non_default_feature_names = True
self.original_feature_name_ = np.array(feature_name, dtype=object)
if self._handle is not None and feature_name is not None and feature_name != "auto":
if len(feature_name) != self.num_feature():
raise ValueError(
Expand Down
5 changes: 4 additions & 1 deletion python-package/lightgbm/sklearn.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ def __init__(
self._classes: Optional[np.ndarray] = None
self._n_classes: int = -1
self.set_params(**kwargs)
self.original_feature_name_: np.ndarray = np.array([], dtype=object)

# scikit-learn 1.6 introduced an __sklearn__tags() method intended to replace _more_tags().
# _more_tags() can be removed whenever lightgbm's minimum supported scikit-learn version
Expand Down Expand Up @@ -1146,6 +1147,8 @@ def fit(
# is set BEFORE fitting.
self._n_features = self._Booster.num_feature()

self.original_feature_name_ = train_set.original_feature_name_

# This attribute informs self.features_in_, but isn't set until here
# because Dataset.construct(), called by train(), is responsible for updating it.
self._fitted_with_feature_names = train_set._has_non_default_feature_names
Expand Down Expand Up @@ -1382,7 +1385,7 @@ def feature_names_in_(self) -> np.ndarray:
"The training data did not have feature names "
"(e.g. was a numpy array rather than a pandas DataFrame)."
)
return np.array(self.feature_name_)
return np.array(self.original_feature_name_, dtype=object)

@feature_names_in_.deleter
def feature_names_in_(self) -> None:
Expand Down
32 changes: 29 additions & 3 deletions tests/python_package_test/test_sklearn.py
Original file line number Diff line number Diff line change
Expand Up @@ -1801,7 +1801,7 @@ def test_feature_names_in_and_predict_warning(
# feature_name_: always accessible, reflects actual names used internally
# feature_names_in_: absent when no named features, present otherwise
if fit_X_type in types_with_feat_names:
np_assert_array_equal(model.feature_names_in_, np.array(col_names), strict=True)
np_assert_array_equal(model.feature_names_in_, np.array(col_names, dtype=object), strict=True)
assert model.feature_name_ == col_names
else:
assert model.feature_name_ == default_names
Expand Down Expand Up @@ -1832,9 +1832,9 @@ def test_feature_names_in_and_predict_warning(
model = lgb.LGBMClassifier(n_estimators=2, num_leaves=3).fit(X_fit, y, feature_name=custom_names)

# feature names from keyword arg should be used, not any from the input data
np_assert_array_equal(model.feature_names_in_, np.array(custom_names), strict=True)
np_assert_array_equal(model.feature_names_in_, np.array(custom_names, dtype=object), strict=True)
assert model.feature_name_ == custom_names
np_assert_array_equal(model.feature_names_in_, np.array(custom_names), strict=True)
np_assert_array_equal(model.feature_names_in_, np.array(custom_names, dtype=object), strict=True)
assert model.n_features_in_ == n_features

# predict() should not raise a warning if input has feature names
Expand Down Expand Up @@ -1872,6 +1872,32 @@ def _get_expected_failed_tests(estimator):
return estimator._more_tags()["_xfail_checks"]


def test_feature_names_in_():
"""
Test that feature_names_in_ returns the same feature names as the input.
"""
pd = pytest.importorskip("pandas")

X = pd.DataFrame(
{
"feature age": [1.0, 2.0, 3.0, 4.0],
"body mass index": [5.0, 6.0, 7.0, 8.0],
}
)
y = np.array([0, 1, 0, 1])

model = lgb.LGBMClassifier(
n_estimators=2,
num_leaves=3,
verbosity=-1,
).fit(X, y)

np_assert_array_equal(
model.feature_names_in_,
X.columns.to_numpy(dtype=object),
strict=True,
)

@parametrize_with_checks(
[ExtendedLGBMClassifier(), ExtendedLGBMRegressor(), lgb.LGBMClassifier(), lgb.LGBMRegressor()],
expected_failed_checks=_get_expected_failed_tests,
Expand Down
Loading