|
9 | 9 | import logging |
10 | 10 | from collections.abc import Iterator |
11 | 11 | from textwrap import shorten |
12 | | -from typing import TYPE_CHECKING |
| 12 | +from typing import TYPE_CHECKING, Any |
13 | 13 |
|
14 | 14 | import cloudpickle |
15 | 15 | import pandas as pd |
|
21 | 21 | logger = logging.getLogger(__name__) |
22 | 22 |
|
23 | 23 |
|
| 24 | +def _generic_feature_names(num_output_features: int) -> list[str]: |
| 25 | + return [f"feature_{i}" for i in range(num_output_features)] |
| 26 | + |
| 27 | + |
| 28 | +def _call_get_feature_names_out(transformer: Any, feature_columns: list[str]) -> list[str]: |
| 29 | + try: |
| 30 | + names = transformer.get_feature_names_out(input_features=feature_columns) |
| 31 | + except TypeError: |
| 32 | + names = transformer.get_feature_names_out() |
| 33 | + return list(names) |
| 34 | + |
| 35 | + |
| 36 | +def _try_get_feature_names_out( |
| 37 | + fitted_pipeline: Pipeline, feature_columns: list[str] |
| 38 | +) -> tuple[list[str] | None, str | None]: |
| 39 | + strategies: list[tuple[str, Any]] = [] |
| 40 | + |
| 41 | + if hasattr(fitted_pipeline, "get_feature_names_out"): |
| 42 | + strategies.append(("pipeline.get_feature_names_out()", fitted_pipeline)) |
| 43 | + |
| 44 | + if isinstance(fitted_pipeline, Pipeline) and fitted_pipeline.steps: |
| 45 | + last_step_name, last_step = fitted_pipeline.steps[-1] |
| 46 | + |
| 47 | + if not hasattr(last_step, "get_feature_names_out") and len(fitted_pipeline.steps) > 1: |
| 48 | + strategies.append( |
| 49 | + (f"pipeline[:-1].get_feature_names_out() (skipped {last_step_name})", fitted_pipeline[:-1]) |
| 50 | + ) |
| 51 | + |
| 52 | + if hasattr(last_step, "get_feature_names_out"): |
| 53 | + strategies.append((f"{last_step_name}.get_feature_names_out()", last_step)) |
| 54 | + |
| 55 | + for label, transformer in strategies: |
| 56 | + try: |
| 57 | + names = _call_get_feature_names_out(transformer, feature_columns) |
| 58 | + except Exception: |
| 59 | + continue |
| 60 | + return names, label |
| 61 | + |
| 62 | + return None, None |
| 63 | + |
| 64 | + |
| 65 | +def _resolve_transformed_feature_names( |
| 66 | + fitted_pipeline: Pipeline, feature_columns: list[str], num_output_features: int |
| 67 | +) -> tuple[list[str], str]: |
| 68 | + names, source = _try_get_feature_names_out(fitted_pipeline, feature_columns) |
| 69 | + |
| 70 | + if names is None: |
| 71 | + return _generic_feature_names(num_output_features), "generic" |
| 72 | + |
| 73 | + if len(names) != num_output_features: |
| 74 | + return _generic_feature_names(num_output_features), "generic_mismatch" |
| 75 | + |
| 76 | + return names, source or "pipeline.get_feature_names_out()" |
| 77 | + |
| 78 | + |
24 | 79 | def transform_dataset_via_spark( |
25 | 80 | spark: SparkSession, |
26 | 81 | dataset_uri: str, |
@@ -101,20 +156,19 @@ def transform_dataset_via_spark( |
101 | 156 | sample_transformed_features = sample_transformed |
102 | 157 | else: |
103 | 158 | # Pipeline returned numpy array - need to assign column names |
104 | | - try: |
105 | | - # Try sklearn's get_feature_names_out() (sklearn 1.0+) |
106 | | - if hasattr(fitted_pipeline, "get_feature_names_out"): |
107 | | - transformed_feature_cols = fitted_pipeline.get_feature_names_out( |
108 | | - input_features=feature_columns |
109 | | - ).tolist() |
110 | | - logger.info("Using feature names from pipeline.get_feature_names_out()") |
111 | | - else: |
112 | | - raise AttributeError("No get_feature_names_out method") |
113 | | - except Exception as e: |
114 | | - # Fallback to generic names |
115 | | - num_output_features = sample_transformed.shape[1] |
116 | | - transformed_feature_cols = [f"feature_{i}" for i in range(num_output_features)] |
117 | | - logger.warning(f"Using generic feature names ({e})") |
| 159 | + num_output_features = sample_transformed.shape[1] |
| 160 | + transformed_feature_cols, feature_name_source = _resolve_transformed_feature_names( |
| 161 | + fitted_pipeline=fitted_pipeline, |
| 162 | + feature_columns=feature_columns, |
| 163 | + num_output_features=num_output_features, |
| 164 | + ) |
| 165 | + |
| 166 | + if feature_name_source == "generic": |
| 167 | + logger.debug("Falling back to generic feature names (pipeline has no get_feature_names_out).") |
| 168 | + elif feature_name_source == "generic_mismatch": |
| 169 | + logger.debug("Falling back to generic feature names (pipeline feature count mismatch).") |
| 170 | + else: |
| 171 | + logger.info("Using feature names from %s", feature_name_source) |
118 | 172 |
|
119 | 173 | sample_transformed_features = pd.DataFrame(sample_transformed, columns=transformed_feature_cols) |
120 | 174 |
|
|
0 commit comments