Better inference of the columns remainder dtype in `transformers_` from `ColumnTransformer`

A typical use case is to fit a `ColumnTransfomrer` on a pandas dataframe such as:

```python
# %%
from sklearn.datasets import load_iris

df, y = load_iris(return_X_y=True, as_frame=True)

# %%
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer

cols = ["sepal length (cm)", "sepal width (cm)"]
ct = ColumnTransformer([("scale", StandardScaler(), cols)], remainder="passthrough")
ct.fit_transform(df)
```

While investigating the `transformers_`, the columns exposed for `remainder` is a bit weird:

```python
ct.transformers_
```

```
[('scale', StandardScaler(), ['sepal length (cm)', 'sepal width (cm)']),
 ('remainder', 'passthrough', [2, 3])]
```

In terms of UX, I assume that one would expect to get the feature names in the remainder instead of the feature indices. However, our API to pass the information of the columns in scikit-learn is pretty flexible: we can have missed type of slice, array of int, etc.

I would suggest that when the columns passed by the user are of a single type, then we make sure that the column type of the `remainder` is of the same type: if only indices are passed, we show indices, if names are passed, we show names, and if boolean are passed, we show boolean.

With mixed types, we cannot really decide and we can keep the current behaviour. On a UX perspective, I assume this is better (together with the change in #27204).

Any thoughts @lorentzenchr @adrinjalali @ogrisel @betatim @jeremiedbb

COMMENT:
I would like to work on this one if that's ok.

IIUC those should be the types of the remainder columns (ie `self.transformers_[-1][-1]`) according to the types of the `transformers` columns:

```
input columns → remainder columns:

    mixed → [2, 3] (int)
    all str → ['petal length (cm)', 'petal width (cm)'] (str)
    all int → [2, 3] (int)
    all bool → [False, False, True, True] (bool)
    all callable → [2, 3] (int)
```

<details>

<pre>

from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer


df, _ = load_iris(return_X_y=True, as_frame=True)

print("input columns → remainder columns:\n")

ct = ColumnTransformer(
    [
        ("scale", StandardScaler(), ["sepal length (cm)"]),
        ("scale1", StandardScaler(with_mean=False), [1]),
    ],
    remainder="passthrough",
)
ct.fit_transform(df)
r = ct.transformers_[-1][-1]
print(f"    mixed → {r} ({type(r[0]).__name__})")

ct = ColumnTransformer(
    [
        ("scale", StandardScaler(), ["sepal length (cm)"]),
        ("scale1", StandardScaler(with_mean=False), ["sepal width (cm)"]),
    ],
    remainder="passthrough",
)
ct.fit_transform(df)
r = ct.transformers_[-1][-1]
print(f"    all str → {r} ({type(r[0]).__name__})")


ct = ColumnTransformer(
    [
        ("scale", StandardScaler(), [0]),
        ("scale1", StandardScaler(with_mean=False), [1]),
    ],
    remainder="passthrough",
)
ct.fit_transform(df)
r = ct.transformers_[-1][-1]
print(f"    all int → {r} ({type(r[0]).__name__})")



ct = ColumnTransformer(
    [
        ("scale", StandardScaler(), [True, False, False, False]),
        ("scale1", StandardScaler(with_mean=False), [False, True, False, False]),
    ],
    remainder="passthrough",
)

ct.fit_transform(df)
r = ct.transformers_[-1][-1]
print(f"    all bool → {r} ({type(r[0]).__name__})")



ct = ColumnTransformer(
    [
        ("scale", StandardScaler(), lambda x: [0]),
        ("scale1", StandardScaler(with_mean=False), lambda x: [1]),
    ],
    remainder="passthrough",
)

ct.fit_transform(df)
r = ct.transformers_[-1][-1]
print(f"    all callable → {r} ({type(r[0]).__name__})")
</pre>
</details>



When the inputs all use callables for their columns, above I had the remainder use integer indices rather than a callable, too. We could have it be a callable, in this case I guess we would need to check the types that the other callables return, and if they all return the same type (ie int, str, bool), use that as well for the remainder. I'm not sure if it's worth the added complexity, WDYT?

Also, as this change might break some user code, should there be a deprecation cycle? And if so, does that mean we need to add a parameter that controls whether we apply the old or the new behavior?

Labels: Enhancement