Repository: scikit-learn/scikit-learn
Pull request: add feature_names_in_ and n_features_in_ attributes to dummy estimators
<!--
Thanks for contributing a pull request! Please ensure you have taken a look at
the contribution guidelines: https://github.com/scikit-learn/scikit-learn/blob/main/CONTRIBUTING.md
-->

#### Reference Issues/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->

Fixes #27907

#### What does this implement/fix? Explain your changes.

Add the attributes `feature_names_in_` and `n_features_in_` to the Dummy estimators.


<!--
Please be aware that we are a loose team of volunteers so patience is
necessary; assistance handling other issues is very welcome. We value
all user contributions, no matter how minor they are. If we are slow to
review, either the pull request needs some benchmarking, tinkering,
convincing, etc. or more likely the reviewers are simply busy. In either
case, we ask for your understanding during the review process.
For more information, see our FAQ on this topic:
http://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->


Problem statement:
Dummy estimators don't have the `feature_names_in_` nor `n_features_in_` attributes
### Describe the bug

`DummyClassifier` and `DummyRegressor` estimators don't have the `feature_names_in_` nor `n_features_in_` attributes. The reason is that they don't call `self._validate_data` during `fit` like other estimators do.

### Steps/Code to Reproduce

```python
from sklearn.datasets import load_breast_cancer
from sklearn.dummy import DummyClassifier

X, y = load_breast_cancer(return_X_y=True, as_frame=True)

dummy = DummyClassifier().fit(X, y)
print(dummy.feature_names_in_)  # Fails
```

### Expected Results

No errors.

### Actual Results

```
Traceback (most recent call last):
  File "C:\Users\Mavs\Documents\Python\ATOM\venv311\Lib\site-packages\IPython\core\interactiveshell.py", line 3548, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-3-bf0e7a849755>", line 7, in <module>
    print(dummy.feature_names_in_)  # Fails
          ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'DummyClassifier' object has no attribute 'feature_names_in_'
```

### Versions

```shell
System:
    python: 3.11.2 (tags/v3.11.2:878ead1, Feb  7 2023, 16:38:35) [MSC v.1934 64 bit (AMD64)]
executable: C:\Users\Mavs\Documents\Python\ATOM\venv311\Scripts\python.exe
   machine: Windows-10-10.0.19045-SP0
Python dependencies:
      sklearn: 1.3.2
          pip: 23.3.1
   setuptools: 68.2.2
        numpy: 1.24.4
        scipy: 1.11.3
       Cython: 3.0.5
       pandas: 2.1.2
   matplotlib: 3.8.0
       joblib: 1.3.2
threadpoolctl: 3.2.0
Built with OpenMP: True
threadpoolctl info:
       user_api: blas
   internal_api: openblas
    num_threads: 16
         prefix: libopenblas
       filepath: C:\Users\Mavs\Documents\Python\ATOM\venv311\Lib\site-packages\numpy\.libs\libopenblas64__v0.3.21-gcc_10_3_0.dll
        version: 0.3.21
threading_layer: pthreads
   architecture: Zen
       user_api: openmp
   internal_api: openmp
    num_threads: 16
         prefix: vcomp
       filepath: C:\Users\Mavs\Documents\Python\ATOM\venv311\Lib\site-packages\sklearn\.libs\vcomp140.dll
        version: None
       user_api: blas
   internal_api: openblas
    num_threads: 16
         prefix: libopenblas
       filepath: C:\Users\Mavs\Documents\Python\ATOM\venv311\Lib\site-packages\scipy.libs\libopenblas_v0.3.20-571-g3dec11c6-gcc_10_3_0-c2315440d6b6cef5037bad648efc8c59.dll
        version: 0.3.21.dev
threading_layer: pthreads
   architecture: Zen
       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libiomp
       filepath: C:\Users\Mavs\Documents\Python\ATOM\venv311\Lib\site-packages\torch\lib\libiomp5md.dll
        version: None
       user_api: openmp
   internal_api: openmp
    num_threads: 1
         prefix: libiomp
       filepath: C:\Users\Mavs\Documents\Python\ATOM\venv311\Lib\site-packages\torch\lib\libiompstubs5md.dll
        version: None
```

Hints from issue discussion:
probably it's more a feature request than a bug. I am willing to make a PR for this.
these estimators are not really checking the input `X` and hence they don't store feature names. I'm not sure if it's a good idea to add that validation step.
It makes sense to not do the validation, but they could still store feature names, just for continuity of the API.
WDYT @thomasjpfan ?
I think it's okay to store the `feature_names_in_` and `n_features_in_` in `fit`, but not validate elsewhere. We do the same thing in `FunctionTransformer`.

Implementation-wise, we can use `self._validate_data(X, cast_to_ndarray=False)` at the beginning of `Dummy*.fit`.

Commit message: add changelog and fix test

Patch to review:
diff --git a/doc/whats_new/v1.4.rst b/doc/whats_new/v1.4.rst
index a2aca5841cd9e..8e832db2da603 100644
--- a/doc/whats_new/v1.4.rst
+++ b/doc/whats_new/v1.4.rst
@@ -35,6 +35,10 @@ random sampling procedures.
       solvers (when fit on the same data again). The amount of change depends on the
       specified `tol`, for small values you will get more precise results.
 
+- |Enhancement| :class:`dummy.DummyClassifier` and :class:`dummy.DummyRegressor` now
+  have the `n_features_in_` and `feature_names_in_` attributes after `fit`.
+  :pr:`27937` by :user:`Marco vd Boom <tvdboom>`.
+
 - |Fix| fixes a memory leak seen in PyPy for estimators using the Cython loss functions.
   :pr:`27670` by :user:`Guillaume Lemaitre <glemaitre>`.
 
@@ -343,6 +347,13 @@ Changelog
   version 1.6. Use the default value instead.
   :pr:`27834` by :user:`Guillaume Lemaitre <glemaitre>`.
 
+:mod:`sklearn.dummy`
+.......................
+
+- |Enhancement| :class:`dummy.DummyClassifier` and :class:`dummy.DummyRegressor` now
+  have the `n_features_in_` and `feature_names_in_` attributes after `fit`.
+  :pr:`27937` by :user:`Marco vd Boom <tvdboom>`.
+
 :mod:`sklearn.ensemble`
 .......................
 
diff --git a/sklearn/dummy.py b/sklearn/dummy.py
index 63318b07ce580..d92a9b5f80a32 100644
--- a/sklearn/dummy.py
+++ b/sklearn/dummy.py
@@ -110,6 +110,13 @@ class prior probabilities.
         Frequency of each class observed in `y`. For multioutput classification
         problems, this is computed independently for each output.
 
+    n_features_in_ : int
+        Number of features seen during fit.
+
+    feature_names_in_ : ndarray of shape (`n_features_in_,`)
+        Names of features seen during fit. Defined only when `X` has feature
+        names that are all strings.
+
     n_outputs_ : int
         Number of outputs.
 
@@ -170,6 +177,8 @@ def fit(self, X, y, sample_weight=None):
         self : object
             Returns the instance itself.
         """
+        self._validate_data(X, cast_to_ndarray=False)
+
         self._strategy = self.strategy
 
         if self._strategy == "uniform" and sp.issparse(y):
@@ -488,6 +497,13 @@ class DummyRegressor(MultiOutputMixin, RegressorMixin, BaseEstimator):
         Mean or median or quantile of the training targets or constant value
         given by the user.
 
+    n_features_in_ : int
+        Number of features seen during fit.
+
+    feature_names_in_ : ndarray of shape (`n_features_in_,`)
+        Names of features seen during fit. Defined only when `X` has feature
+        names that are all strings.
+
     n_outputs_ : int
         Number of outputs.
 
@@ -545,6 +561,8 @@ def fit(self, X, y, sample_weight=None):
         self : object
             Fitted estimator.
         """
+        self._validate_data(X, cast_to_ndarray=False)
+
         y = check_array(y, ensure_2d=False, input_name="y")
         if len(y) == 0:
             raise ValueError("y must not be empty.")
diff --git a/sklearn/tests/test_dummy.py b/sklearn/tests/test_dummy.py
index 14bab1e0ffe97..a43cd309da507 100644
--- a/sklearn/tests/test_dummy.py
+++ b/sklearn/tests/test_dummy.py
@@ -376,7 +376,7 @@ def test_quantile_invalid():
 
 def test_quantile_strategy_empty_train():
     est = DummyRegressor(strategy="quantile", quantile=0.4)
-    with pytest.raises(ValueError):
+    with pytest.raises(IndexError):
         est.fit([], [])
 
 


Code review comments:
diff_hunk: @@ -110,6 +110,13 @@ class prior probabilities.
         Frequency of each class observed in `y`. For multioutput classification
         problems, this is computed independently for each output.
 
+    n_features_in_ : int
+        Number of features seen during fit.
+
+    feature_names_in_ : ndarray of shape (`n_features_in_,`)
+        Names of features seen during fit. Defined only when `X` has feature
path: sklearn/dummy.py
text: @user1:
```suggestion
    feature_names_in_ : ndarray of shape (`n_features_in_`,)
        Names of features seen during :term:`fit`. Defined only when `X` has feature
```

diff_hunk: @@ -488,6 +497,13 @@ class DummyRegressor(MultiOutputMixin, RegressorMixin, BaseEstimator):
         Mean or median or quantile of the training targets or constant value
         given by the user.
 
+    n_features_in_ : int
+        Number of features seen during fit.
+
+    feature_names_in_ : ndarray of shape (`n_features_in_,`)
+        Names of features seen during fit. Defined only when `X` has feature
path: sklearn/dummy.py
text: @user1:
```suggestion
    feature_names_in_ : ndarray of shape (`n_features_in_`,)
        Names of features seen during :term:`fit`. Defined only when `X` has feature
```