Repository: scikit-learn/scikit-learn
Pull request: DOC add example with ``set_params``.
#### 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
-->
Attempt to fix #29101 

#### What does this implement/fix? Explain your changes.
Show with an example that after calling ``sklearn.pipeline.Pipeline.set_params`` the ``set_output`` of the transformer is "gone".

Problem statement:
When a Pipeline step is changed via set_params, the set_output state is cleared
### Describe the bug

When a Pipeline step is set via `set_params`, the subsequent output of `fit_transform` is a numpy ndarray even if previously the pipeline's output was set to be of type pandas.DataFrame via a call to `set_output(transform= 'pandas')`.

This only happens when the entire step is set: `set_params(step= some_value)`, not when only a step's parameters are set: `set_params(step__some_param= some_value)`.

The issue doesn't occur if, instead of calling `set_output(transform= 'pandas')` on the Pipeline object, the option is set globally with `sklearn.set_config(transform_output= 'pandas')`.

The same problem may or may not occur with ColumnTransformer, I haven't checked.

### Steps/Code to Reproduce

```
from pandas import DataFrame
from numpy import NaN
from sklearn.preprocessing import MinMaxScaler
from sklearn.pipeline import Pipeline

X = DataFrame({'N': [1, 2]})
pipe = Pipeline([('scale', MinMaxScaler())]).set_output(transform='pandas')
pipe.set_params(scale= MinMaxScaler())
pipe.fit_transform(X)
```

### Expected Results

A pandas DataFrame

### Actual Results

A numpy ndarray

### Versions

```shell
System:
    python: 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
executable: /usr/bin/python3
   machine: Linux-6.1.85+-x86_64-with-glibc2.35

Python dependencies:
      sklearn: 1.2.2
          pip: 23.1.2
   setuptools: 67.7.2
        numpy: 1.25.2
        scipy: 1.11.4
       Cython: 3.0.10
       pandas: 2.0.3
   matplotlib: 3.7.1
       joblib: 1.4.2
threadpoolctl: 3.5.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
    num_threads: 2
         prefix: libopenblas
       filepath: /usr/local/lib/python3.10/dist-packages/numpy.libs/libopenblas64_p-r0-5007b62f.3.23.dev.so
        version: 0.3.23.dev
threading_layer: pthreads
   architecture: Haswell

       user_api: openmp
   internal_api: openmp
    num_threads: 2
         prefix: libgomp
       filepath: /usr/local/lib/python3.10/dist-packages/scikit_learn.libs/libgomp-a34b3233.so.1.0.0
        version: None

       user_api: blas
   internal_api: openblas
    num_threads: 2
         prefix: libopenblas
       filepath: /usr/local/lib/python3.10/dist-packages/scipy.libs/libopenblasp-r0-23e5df77.3.21.dev.so
        version: 0.3.21.dev
threading_layer: pthreads
   architecture: Haswell
```

Hints from issue discussion:
I think this happens because `pipe.set_output` will call `.set_output` to all its underlying estimators in particular `pipe['scale']` i.e. its `MinMaxScaler` estimator. When you do `pipe.set_params(scale=MinMaxScaler())` you replace `pipe['scale']` with an estimator that does not have `set_output` set.

The easiest work-arounds is probably to make sure to call `pipe.set_output` last:

```py
from pandas import DataFrame
from numpy import NaN
from sklearn.preprocessing import MinMaxScaler
from sklearn.pipeline import Pipeline

X = DataFrame({'N': [1, 2]})
pipe = Pipeline([('scale', MinMaxScaler())])
# Call pipe.set_output last
pipe.set_params(scale= MinMaxScaler()).set_output(transform='pandas')
pipe.fit_transform(X)
```

I would agree that this is slightly confusing. At the same time, fixing this would probably involve going over all the pipeline estimators and set their outputs at fit time, so not sure this is worth the additional complexity?
> At the same time, fixing this would probably involve going over all the pipeline estimators and set their outputs at fit time, so not sure this is worth the additional complexity?

Since we are not cloning estimator in `Pipeline`, making `set_params` call `set_output` would change the state of the external estimator. I think that I'm not a big fan of that.

I would not consider fixing it but instead we could document it in a section that we discuss `pipeline.set_output` where we could mention the behaviour of `set_params`
So I would probably suggest to amend the `examples/miscellaneous/plot_set_output.py` example in the pipeline section just to mention the subtlety. 
/take

Commit message: DOC Linting

Patch to review:
diff --git a/examples/miscellaneous/plot_set_output.py b/examples/miscellaneous/plot_set_output.py
index 9baa71a1b3648..2e8a65cb82d6e 100644
--- a/examples/miscellaneous/plot_set_output.py
+++ b/examples/miscellaneous/plot_set_output.py
@@ -63,6 +63,12 @@
 # means that the final logistic regression step contains the feature names of the input.
 clf[-1].feature_names_in_
 
+# %%
+# As a side note, if one uses the method `set_params`, the transformer will be
+# replaced by a new one which does not have `set_output` set:
+clf.set_params(standardscaler=StandardScaler())
+clf["standardscaler"].fit_transform(X_train)[:5]
+
 # %%
 # Next we load the titanic dataset to demonstrate `set_output` with
 # :class:`compose.ColumnTransformer` and heterogeneous data.


Code review comments:
diff_hunk: @@ -63,6 +63,12 @@
 # means that the final logistic regression step contains the feature names of the input.
 clf[-1].feature_names_in_
 
+# %%
+# As a side note, if one uses the method `set_params`, the transformer will be
+# replaced by a new one which does not have `set_output` set:
+clf.set_params(standardscaler=StandardScaler())
+clf["standardscaler"].fit_transform(X_train)[:5]
+
path: examples/miscellaneous/plot_set_output.py
text: @user1:
```suggestion
# %%
# .. note:: If one uses the method `set_params`, the transformer will be
#    replaced by a new one which does not have `set_output` set:
clf.set_params(standardscaler=StandardScaler())
clf.fit(X_train, y_train)
clf[-1].feature_names_in_

# %%
# To keep the intended behavior, use `set_output` on the new transformer
# beforehand
scaler = StandardScaler().set_output(transform='pandas')
clf.set_params(standardscaler=scaler)
clf.fit(X_train, y_train)
clf[-1].feature_names_in_

```