Talip7/scikit-learn-issues-multilabel huggingface dataset:
Pipelines are permitted to have no steps and are displayed as fitted

### Describe the bug

Pipeline without defined steps is displayed in HTML as fitted.  




### Steps/Code to Reproduce


```
from sklearn.pipeline import Pipeline

pipe = Pipeline([])

pipe
```

### Expected Results

Maybe empty list should not be accepted. And it should rise a ValueError with a message asking to add steps.




### Actual Results

Using vscode jupyter extension:

<img width="401" alt="Image" src="https://github.com/user-attachments/assets/f0ad0033-1b86-4a91-a30b-969a5d2ea22e" />

Note: Accepting an empty list is one issue, and showing that it is fitted is another.
The former occurs when a `Pipeline` is initialized. The latter, I believe, is a design flaw in `sklearn/utils/_repr_html/estimator.py.`

### Versions

```shell
System:
    python: 3.12.2 (v3.12.2:6abddd9f6a, Feb  6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)]
executable: /Users/dealeon/projects/scikit-learn/sklearn-env/bin/python
   machine: macOS-15.5-arm64-arm-64bit

Python dependencies:
      sklearn: 1.8.dev0
          pip: 25.1
   setuptools: 75.8.0
        numpy: 2.1.1
        scipy: 1.14.1
       Cython: 3.0.11
       pandas: 2.2.3
   matplotlib: 3.9.2
       joblib: 1.4.2
threadpoolctl: 3.5.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libomp
       filepath: /opt/homebrew/Cellar/libomp/19.1.7/lib/libomp.dylib
        version: None
```

COMMENT:
The following fails with a weird error message, so I think we should consider it a mistake to create an empty `Pipeline` and raise a better error message. As we only raise the error in `fit` (no validation in `__init__` is a scikit-learn principle) I think we need to add something to `__sklearn_is_fitted__` to handle this case. It seems weird to display it as fitted and at the same time you can't use it.
```python
from sklearn.pipeline import Pipeline
from sklearn.datasets import make_classification

X, y = make_classification()
p = Pipeline([])
# This fails with an not super helpful exception
p.fit(X, y)
# This fails with a more sensible exception
p.predict(X)
```

Labels: Bug
Pipelines are permitted to have no steps and are displayed as fitted

### Describe the bug

Pipeline without defined steps is displayed in HTML as fitted.  




### Steps/Code to Reproduce


```
from sklearn.pipeline import Pipeline

pipe = Pipeline([])

pipe
```

### Expected Results

Maybe empty list should not be accepted. And it should rise a ValueError with a message asking to add steps.




### Actual Results

Using vscode jupyter extension:

<img width="401" alt="Image" src="https://github.com/user-attachments/assets/f0ad0033-1b86-4a91-a30b-969a5d2ea22e" />

Note: Accepting an empty list is one issue, and showing that it is fitted is another.
The former occurs when a `Pipeline` is initialized. The latter, I believe, is a design flaw in `sklearn/utils/_repr_html/estimator.py.`

### Versions

```shell
System:
    python: 3.12.2 (v3.12.2:6abddd9f6a, Feb  6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)]
executable: /Users/dealeon/projects/scikit-learn/sklearn-env/bin/python
   machine: macOS-15.5-arm64-arm-64bit

Python dependencies:
      sklearn: 1.8.dev0
          pip: 25.1
   setuptools: 75.8.0
        numpy: 2.1.1
        scipy: 1.14.1
       Cython: 3.0.11
       pandas: 2.2.3
   matplotlib: 3.9.2
       joblib: 1.4.2
threadpoolctl: 3.5.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libomp
       filepath: /opt/homebrew/Cellar/libomp/19.1.7/lib/libomp.dylib
        version: None
```

COMMENT:
> no validation in __init__ is a scikit-learn principle

Thanks, I didn't know.

I wonder if a `FutureWarning` needs to be done when a validation is added to `__sklearn_is_fitted__`. Since this `p = Pipeline([])` doesn't raise. Actually, should it raise or not? I thought it should. 

BTW `/take` is not working as stated in the [contribution instructions](https://scikit-learn.org/stable/developers/contributing.html#:~:text=/take%20on%20it%20for%20the%20CI%20to).

Labels: Bug
Pipelines are permitted to have no steps and are displayed as fitted

### Describe the bug

Pipeline without defined steps is displayed in HTML as fitted.  




### Steps/Code to Reproduce


```
from sklearn.pipeline import Pipeline

pipe = Pipeline([])

pipe
```

### Expected Results

Maybe empty list should not be accepted. And it should rise a ValueError with a message asking to add steps.




### Actual Results

Using vscode jupyter extension:

<img width="401" alt="Image" src="https://github.com/user-attachments/assets/f0ad0033-1b86-4a91-a30b-969a5d2ea22e" />

Note: Accepting an empty list is one issue, and showing that it is fitted is another.
The former occurs when a `Pipeline` is initialized. The latter, I believe, is a design flaw in `sklearn/utils/_repr_html/estimator.py.`

### Versions

```shell
System:
    python: 3.12.2 (v3.12.2:6abddd9f6a, Feb  6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)]
executable: /Users/dealeon/projects/scikit-learn/sklearn-env/bin/python
   machine: macOS-15.5-arm64-arm-64bit

Python dependencies:
      sklearn: 1.8.dev0
          pip: 25.1
   setuptools: 75.8.0
        numpy: 2.1.1
        scipy: 1.14.1
       Cython: 3.0.11
       pandas: 2.2.3
   matplotlib: 3.9.2
       joblib: 1.4.2
threadpoolctl: 3.5.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libomp
       filepath: /opt/homebrew/Cellar/libomp/19.1.7/lib/libomp.dylib
        version: None
```

COMMENT:
> I wonder if a `FutureWarning` needs to be done when a validation is added to `__sklearn_is_fitted__`. Since this `p = Pipeline([])` doesn't raise. Actually, should it raise or not? I thought it should.

No exception should be raised on the line `p = Pipeline([])`. I don't think we need a `FutureWarning` as I'd consider it a bug fix. It is a bug because you can't call `predict()` or `transform()` on the `Pipeline`, even though it shows as "fitted" -> it should not show as fitted

Labels: Bug
Pipelines are permitted to have no steps and are displayed as fitted

### Describe the bug

Pipeline without defined steps is displayed in HTML as fitted.  




### Steps/Code to Reproduce


```
from sklearn.pipeline import Pipeline

pipe = Pipeline([])

pipe
```

### Expected Results

Maybe empty list should not be accepted. And it should rise a ValueError with a message asking to add steps.




### Actual Results

Using vscode jupyter extension:

<img width="401" alt="Image" src="https://github.com/user-attachments/assets/f0ad0033-1b86-4a91-a30b-969a5d2ea22e" />

Note: Accepting an empty list is one issue, and showing that it is fitted is another.
The former occurs when a `Pipeline` is initialized. The latter, I believe, is a design flaw in `sklearn/utils/_repr_html/estimator.py.`

### Versions

```shell
System:
    python: 3.12.2 (v3.12.2:6abddd9f6a, Feb  6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)]
executable: /Users/dealeon/projects/scikit-learn/sklearn-env/bin/python
   machine: macOS-15.5-arm64-arm-64bit

Python dependencies:
      sklearn: 1.8.dev0
          pip: 25.1
   setuptools: 75.8.0
        numpy: 2.1.1
        scipy: 1.14.1
       Cython: 3.0.11
       pandas: 2.2.3
   matplotlib: 3.9.2
       joblib: 1.4.2
threadpoolctl: 3.5.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libomp
       filepath: /opt/homebrew/Cellar/libomp/19.1.7/lib/libomp.dylib
        version: None
```

COMMENT:
Thanks for clarifying. 

Now, when I add a check to `__sklearn_is_fitted__`, and call `p.fit` with no steps, I still get the unhelpful exception. 

It seems that the not-so-helpful message comes from `names, estimators = zip(*self.steps)` 
in [`_validate_steps`](https://github.com/scikit-learn/scikit-learn/blob/cfd5f7833dfb3794e711e79e4a3373e599d5a1f0/sklearn/pipeline.py#L322). I added a check there and seems to do the trick. 😇  -- but maybe there is another scikit-learn principle I'm missing.

Labels: Bug
Pipelines are permitted to have no steps and are displayed as fitted

### Describe the bug

Pipeline without defined steps is displayed in HTML as fitted.  




### Steps/Code to Reproduce


```
from sklearn.pipeline import Pipeline

pipe = Pipeline([])

pipe
```

### Expected Results

Maybe empty list should not be accepted. And it should rise a ValueError with a message asking to add steps.




### Actual Results

Using vscode jupyter extension:

<img width="401" alt="Image" src="https://github.com/user-attachments/assets/f0ad0033-1b86-4a91-a30b-969a5d2ea22e" />

Note: Accepting an empty list is one issue, and showing that it is fitted is another.
The former occurs when a `Pipeline` is initialized. The latter, I believe, is a design flaw in `sklearn/utils/_repr_html/estimator.py.`

### Versions

```shell
System:
    python: 3.12.2 (v3.12.2:6abddd9f6a, Feb  6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)]
executable: /Users/dealeon/projects/scikit-learn/sklearn-env/bin/python
   machine: macOS-15.5-arm64-arm-64bit

Python dependencies:
      sklearn: 1.8.dev0
          pip: 25.1
   setuptools: 75.8.0
        numpy: 2.1.1
        scipy: 1.14.1
       Cython: 3.0.11
       pandas: 2.2.3
   matplotlib: 3.9.2
       joblib: 1.4.2
threadpoolctl: 3.5.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libomp
       filepath: /opt/homebrew/Cellar/libomp/19.1.7/lib/libomp.dylib
        version: None
```

COMMENT:
`_validate_steps` sounds like a good place to add this check.

If you have the beginnings of a Pull Request you could open one and mark it as draft. That makes it easier to look at things together.

Labels: Bug
Pipelines are permitted to have no steps and are displayed as fitted

### Describe the bug

Pipeline without defined steps is displayed in HTML as fitted.  




### Steps/Code to Reproduce


```
from sklearn.pipeline import Pipeline

pipe = Pipeline([])

pipe
```

### Expected Results

Maybe empty list should not be accepted. And it should rise a ValueError with a message asking to add steps.




### Actual Results

Using vscode jupyter extension:

<img width="401" alt="Image" src="https://github.com/user-attachments/assets/f0ad0033-1b86-4a91-a30b-969a5d2ea22e" />

Note: Accepting an empty list is one issue, and showing that it is fitted is another.
The former occurs when a `Pipeline` is initialized. The latter, I believe, is a design flaw in `sklearn/utils/_repr_html/estimator.py.`

### Versions

```shell
System:
    python: 3.12.2 (v3.12.2:6abddd9f6a, Feb  6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)]
executable: /Users/dealeon/projects/scikit-learn/sklearn-env/bin/python
   machine: macOS-15.5-arm64-arm-64bit

Python dependencies:
      sklearn: 1.8.dev0
          pip: 25.1
   setuptools: 75.8.0
        numpy: 2.1.1
        scipy: 1.14.1
       Cython: 3.0.11
       pandas: 2.2.3
   matplotlib: 3.9.2
       joblib: 1.4.2
threadpoolctl: 3.5.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libomp
       filepath: /opt/homebrew/Cellar/libomp/19.1.7/lib/libomp.dylib
        version: None
```

COMMENT:
Sure. It was planned. I should have mentioned it. :-)

Labels: Bug
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
I think something like

```
X.data[X.data > 1.0] = 1.0
X.data[X.data < -1.0] = -1.0
X.eliminate_zeros()
```

should preserve sparsity and clip values on transform stage.

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
Hello @glevv,

thanks for opening the issue. That sounds useful to me.

> I think something like
> 
> X.data[X.data > 1.0] = 1.0
> X.data[X.data < -1.0] = -1.0
> X.eliminate_zeros()
> 
> should preserve sparsity and clip values on transform stage.

I would think yes, scaling and then clipping could not destroy sparsity, right?

Two and a half things I am wondering about:
- On the opposite sign as the max absolute value was found, a value can still be out of the feature range when inside [-1,1]. For instance if `X` that was used in `fit` only has positive values, but `X` used in `transform` has positive and negative values. Maybe that's okay if we document it?
- `X.eliminate_zeros()` would be used to make any new 0s be stored as 0s in the sparse matrix? But I don't see how by scaling or clipping any new 0s could be produced? Is it really needed?
- (Somehow I think `clip` is rather a param of `transform`, instead of a constructor argument, as in `MinMaxScaler`.)

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
> * On the opposite sign as the max absolute value was found, a value can still be out of the feature range when inside [-1,1]. For instance if `X` that was used in `fit` only has positive values, but `X` used in `transform` has positive and negative values. Maybe that's okay if we document it?

Can you make an example? I'm thinking that if the largest absolute value we see during `fit` is, say, +3 and the smallest (absolute) value we see is 0. Then all negative values during `transform` will be at least -3, because if a value is, say, -5 it will get scaled to -3 because the maximum absolute value is 3.

I think this could be useful to users and would match `MinMaxScaler`.

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
> Can you make an example?

I mean that using the same max value (max abs) for both signs (positive and negative values) is only strictly guarding the feature range for the sign the max absolute value was taken from, but not for the opposite sign.

My assumption here is that `clip` is to make sure that the `X_transform` is always in the same feature range as the `X` `MaxAbsScaler` was trained on.

For an example (building on yours):

```py
import numpy as np

X_train = np.array([[1,0],[0,3]])
max_abs = np.max(np.abs(X_train)) #3
X_train_scaled = X_train/max_abs
print(X_train_scaled)
# [[0.33333333 0.        ]
#  [0.         1.        ]]

X_test = np.array([[1,0],[0,-6]])
X_test_scaled = X_test/max_abs
print(X_test_scaled)
# [[ 0.33333333  0.        ]
#  [ 0.         -2.        ]]

X_test_scaled_cliped = X_test_scaled
X_test_scaled_cliped[X_test_scaled_cliped > 1] = 1
X_test_scaled_cliped[X_test_scaled_cliped < -1] = -1
print(X_test_scaled_cliped)
# [[ 0.33333333  0.        ]
#  [ 0.         -1.        ]]
```
We would allow `X_transform` to be outside of the feature range.

Maybe that's okay (as long as we document it) because it is still useful. 

It depends a bit what people would expect to happen using `clip` and how we communicate the idea. Or, if `clip` is some kind of reoccurring concept then we should maybe better match it, so people are not too confused?

Edit: Ah, now I've noted that it's per column, of cause.

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
> Hello [@glevv](https://github.com/glevv),
> 
> thanks for opening the issue. That sounds useful to me.
> 
> > I think something like
> > X.data[X.data > 1.0] = 1.0
> > X.data[X.data < -1.0] = -1.0
> > X.eliminate_zeros()
> > should preserve sparsity and clip values on transform stage.
> 
> I would think yes, scaling and then clipping could not destroy sparsity, right?
> 
> Two and a half things I am wondering about:
> 
>     * On the opposite sign as the max absolute value was found, a value can still be out of the feature range when inside [-1,1]. For instance if `X` that was used in `fit` only has positive values, but `X` used in `transform` has positive and negative values. Maybe that's okay if we document it?
> 
>     * `X.eliminate_zeros()` would be used to make any new 0s be stored as 0s in the sparse matrix? But I don't see how by scaling or clipping any new 0s could be produced? Is it really needed?
> 
>     * (Somehow I think `clip` is rather a param of `transform`, instead of a constructor argument, as in `MinMaxScaler`.)

- About features outside of the fit range, I think there should be clear separation of terms. I think what you are describing is a feature drift - our distribution of values on test time differ from train time. This could also happen with `MinMaxScaler`, it's just more obvious with `MaxAbsScaler` and your example clearly illustrates it. The `clip` parameter does not prevent feature drift per se, it tries to prevent out of range values, which could lead to unintended results for algorithms with extrapolation capabilities (for example linear regression). I think it's possible to document this, something like '_clipping of the values does not prevent feature drift, it only prevents out of range values after transform is called_'.
- Yes, I agree, `eliminate_zeros` is unnecessary, it would do nothing in this case.
- When I was writing the feature request, I was copying existing API from `MinMaxScaler`, that has `clip` parameter in class constructor. I think both options have their advantages and disadvantages, but in the end, API should be consistent, and if it is decided to implement `clip` as argument to `transform` method, then it should be the same for `MinMaxScaler`

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
`clip` has to be a constructor argument

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
> I think what you are describing is a feature drift - our distribution of values on test time differ from train time. This could also happen with MinMaxScaler, it's just more obvious with MaxAbsScaler and your example clearly illustrates it. The clip parameter does not prevent feature drift per se, it tries to prevent out of range values, which could lead to unintended results for algorithms with extrapolation capabilities (for example linear regression).

Thanks for your explanations, @glevv. That distinction is in fact new to me. 
So, if I understand right then `clip` would serve to make sure the values after scaling are in the same range as the rest after scaling, which is important to not produce unrealistic predictions or wrong scores for models that extrapolate. That makes sense to me. 👍 

> clip has to be a constructor argument

Sure, it should be consistent.

@betatim, do you think that will be a good enhancement?

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
I think adding `clip` to mirror `MinMaxScaler` sounds like a useful addition to the library.

Labels: Enhancement, API, Needs Investigation
ENH Add clip parameter to MaxAbsScaler

### Describe the workflow you want to enable

Add a `clip` parameter to `MaxAbsScaler` that will allow for clipping values that exceed the maximum value seen during the training stage.

### Describe your proposed solution

Similar to `MinMaxScaler`, but in this case it will clip [-1, +1].

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

I'm not sure if it is possible to implement it without breaking sparsity of the inputs, which is the main problem.

COMMENT:
Ok, I'll draft a PR then

Labels: Enhancement, API, Needs Investigation