Repository: scikit-learn/scikit-learn

Issue:
# FeatureUnion with polars output fails due to missing column renaming in adapter interface

### Describe the bug

When using `FeatureUnion` with `set_config(transform_output="polars")`, the operation fails with `polars.exceptions.DuplicateError` because the `ContainerAdapterProtocol.hstack()` interface is incomplete - it doesn't accept `feature_names` parameter to apply column renaming before concatenation.

The `verbose_feature_names_out=True` (default) mechanism should ensure unique column names via `get_feature_names_out()`, but this information cannot be passed to the adapter's `hstack()` method due to interface design limitation.

### Steps/Code to Reproduce

```python
from sklearn.pipeline import FeatureUnion
from sklearn.preprocessing import StandardScaler
from sklearn import set_config
import numpy as np
set_config(transform_output="polars")
X = np.array([[1, 2], [3, 4], [5, 6]])
union = FeatureUnion([
    ("scaler1", StandardScaler()),
    ("scaler2", StandardScaler())
])
result = union.fit_transform(X)
```

### Expected Results

The code should work and produce a polars DataFrame with uniquely named columns like `['scaler1__x0', 'scaler1__x1', 'scaler2__x0', 'scaler2__x1']`, consistent with the design of `verbose_feature_names_out=True`.

### Actual Results

```python
Traceback (most recent call last):
  File "<stdin>", line 10, in <module>
  File "/path/to/sklearn/utils/_set_output.py", line 316, in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/path/to/sklearn/pipeline.py", line 1971, in fit_transform
    return self._hstack(Xs)
           ^^^^^^^^^^^^^^^^
  File "/path/to/sklearn/pipeline.py", line 2052, in _hstack
    return adapter.hstack(Xs)
           ^^^^^^^^^^^^^^^^^^
  File "/path/to/sklearn/utils/_set_output.py", line 183, in hstack
    return pl.concat(Xs, how="horizontal")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/path/to/polars/functions/eager.py", line 256, in concat
    out = wrap_df(plr.concat_df_horizontal(elems))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
polars.exceptions.DuplicateError: column with name 'x0' has more than one occurrence
```

### Versions

```shell
System:
    python: 3.11.14 | packaged by conda-forge | (main, Oct 22 2025, 22:46:25) [GCC 14.3.0]
executable: /opt/conda/envs/sklearn-dev/bin/python
   machine: Linux-6.8.0-65-generic-x86_64-with-glibc2.36

Python dependencies:
      sklearn: 1.9.dev0
          pip: 25.3
   setuptools: 80.9.0
        numpy: 2.3.5
        scipy: 1.16.3
       Cython: 3.2.2
       pandas: 2.3.3
   matplotlib: None
       joblib: 1.5.2
threadpoolctl: 3.6.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
    num_threads: 48
         prefix: libopenblas
       filepath: /opt/conda/envs/sklearn-dev/lib/libopenblasp-r0.3.30.so
        version: 0.3.30
threading_layer: pthreads
   architecture: SkylakeX

       user_api: openmp
   internal_api: openmp
    num_threads: 48
         prefix: libgomp
       filepath: /opt/conda/envs/sklearn-dev/lib/libgomp.so.1.0.0
        version: None
```

Implementation patch:
diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py
index 32dc4dd187d84..3896beb6b70d8 100644
--- a/sklearn/pipeline.py
+++ b/sklearn/pipeline.py
@@ -1995,7 +1995,7 @@ def _hstack(self, Xs):
 
         adapter = _get_container_adapter("transform", self)
         if adapter and all(adapter.is_supported_container(X) for X in Xs):
-            return adapter.hstack(Xs)
+            return adapter.hstack(Xs, self.get_feature_names_out())
 
         if any(sparse.issparse(f) for f in Xs):
             return sparse.hstack(Xs).tocsr()
diff --git a/sklearn/utils/_set_output.py b/sklearn/utils/_set_output.py
index 3b4fb6b546a3c..220dc69f3390d 100644
--- a/sklearn/utils/_set_output.py
+++ b/sklearn/utils/_set_output.py
@@ -95,7 +95,7 @@ def rename_columns(self, X, columns):
             Container with new names.
         """
 
-    def hstack(self, Xs):
+    def hstack(self, Xs, feature_names=None):
         """Stack containers horizontally (column-wise).
 
         Parameters
@@ -103,6 +103,10 @@ def hstack(self, Xs):
         Xs : list of containers
             List of containers to stack.
 
+        feature_names : array-like of str, default=None
+            The feature names for the stacked container. If provided, the
+            columns of the result will be renamed to these names.
+
         Returns
         -------
         stacked_Xs : container
@@ -147,9 +151,12 @@ def rename_columns(self, X, columns):
         X.columns = columns
         return X
 
-    def hstack(self, Xs):
+    def hstack(self, Xs, feature_names=None):
         pd = check_library_installed("pandas")
-        return pd.concat(Xs, axis=1)
+        result = pd.concat(Xs, axis=1)
+        if feature_names is not None:
+            self.rename_columns(result, feature_names)
+        return result
 
 
 class PolarsAdapter:
@@ -178,8 +185,16 @@ def rename_columns(self, X, columns):
         X.columns = columns
         return X
 
-    def hstack(self, Xs):
+    def hstack(self, Xs, feature_names=None):
         pl = check_library_installed("polars")
+        if feature_names is not None:
+            # Rename columns in each X before concat to avoid duplicates
+            start = 0
+            for X in Xs:
+                n_features = X.shape[1]
+                names = feature_names[start : start + n_features]
+                self.rename_columns(X, names)
+                start += n_features
         return pl.concat(Xs, how="horizontal")
 
 



Test patch:
diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py
index 063450f50a162..6abc64b6658d5 100644
--- a/sklearn/tests/test_pipeline.py
+++ b/sklearn/tests/test_pipeline.py
@@ -1850,20 +1850,22 @@ def test_pipeline_set_output_integration():
     assert_array_equal(feature_names_in_, log_reg_feature_names)
 
 
-def test_feature_union_set_output():
+@pytest.mark.parametrize("df_library", ["pandas", "polars"])
+def test_feature_union_set_output(df_library):
     """Test feature union with set_output API."""
-    pd = pytest.importorskip("pandas")
+    lib = pytest.importorskip(df_library)
 
     X, _ = load_iris(as_frame=True, return_X_y=True)
     X_train, X_test = train_test_split(X, random_state=0)
     union = FeatureUnion([("scalar", StandardScaler()), ("pca", PCA())])
-    union.set_output(transform="pandas")
+    union.set_output(transform=df_library)
     union.fit(X_train)
 
     X_trans = union.transform(X_test)
-    assert isinstance(X_trans, pd.DataFrame)
+    assert isinstance(X_trans, lib.DataFrame)
     assert_array_equal(X_trans.columns, union.get_feature_names_out())
-    assert_array_equal(X_trans.index, X_test.index)
+    if df_library == "pandas":
+        assert_array_equal(X_trans.index, X_test.index)
 
 
 def test_feature_union_getitem():