# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import numpy as np import pandas as pd import pickle from typing import Any, Union, List, Tuple, Callable, Dict, Optional from pyspark import keyword_only from pyspark.sql import DataFrame from pyspark.ml.param.shared import HasInputCol, HasOutputCol from pyspark.ml.connect.base import Estimator, Model from pyspark.ml.connect.io_utils import ParamsReadWrite, CoreModelReadWrite from pyspark.ml.connect.summarizer import summarize_dataframe class MaxAbsScaler(Estimator, HasInputCol, HasOutputCol, ParamsReadWrite): """ Rescale each feature individually to range [-1, 1] by dividing through the largest maximum absolute value in each feature. It does not shift/center the data, and thus does not destroy any sparsity. .. versionadded:: 3.5.0 Examples -------- >>> from pyspark.ml.connect.feature import MaxAbsScaler >>> scaler = MaxAbsScaler(inputCol='features', outputCol='scaled_features') >>> dataset = spark.createDataFrame([ ... ([1.0, 2.0],), ... ([2.0, -1.0],), ... ([-3.0, -2.0],), ... ], schema=['features']) >>> scaler_model = scaler.fit(dataset) >>> transformed_dataset = scaler_model.transform(dataset) >>> transformed_dataset.show(truncate=False) +------------+--------------------------+ |features |scaled_features | +------------+--------------------------+ |[1.0, 2.0] |[0.3333333333333333, 1.0] | |[2.0, -1.0] |[0.6666666666666666, -0.5]| |[-3.0, -2.0]|[-1.0, -1.0] | +------------+--------------------------+ """ _input_kwargs: Dict[str, Any] @keyword_only def __init__(self, *, inputCol: Optional[str] = None, outputCol: Optional[str] = None) -> None: """ __init__(self, \\*, inputCol=None, outputCol=None) """ super().__init__() kwargs = self._input_kwargs self._set(**kwargs) def _fit(self, dataset: Union["pd.DataFrame", "DataFrame"]) -> "MaxAbsScalerModel": input_col = self.getInputCol() stat_res = summarize_dataframe(dataset, input_col, ["min", "max", "count"]) min_values = stat_res["min"] max_values = stat_res["max"] n_samples_seen = stat_res["count"] max_abs_values = np.maximum(np.abs(min_values), np.abs(max_values)) model = MaxAbsScalerModel(max_abs_values, n_samples_seen) model._resetUid(self.uid) return self._copyValues(model) class MaxAbsScalerModel(Model, HasInputCol, HasOutputCol, ParamsReadWrite, CoreModelReadWrite): """ Model fitted by MaxAbsScaler. .. versionadded:: 3.5.0 """ def __init__( self, max_abs_values: Optional["np.ndarray"] = None, n_samples_seen: Optional[int] = None ) -> None: super().__init__() self.max_abs_values = max_abs_values if max_abs_values is not None: # if scale value is zero, replace it with 1.0 (for preventing division by zero) self.scale_values = np.where(max_abs_values == 0.0, 1.0, max_abs_values) self.n_samples_seen = n_samples_seen def _input_columns(self) -> List[str]: return [self.getInputCol()] def _output_columns(self) -> List[Tuple[str, str]]: return [(self.getOutputCol(), "array")] def _get_transform_fn(self) -> Callable[..., Any]: scale_values = self.scale_values def transform_fn(series: Any) -> Any: def map_value(x: "np.ndarray") -> "np.ndarray": return x / scale_values return series.apply(map_value) return transform_fn def _get_core_model_filename(self) -> str: return self.__class__.__name__ + ".sklearn.pkl" def _save_core_model(self, path: str) -> None: from sklearn.preprocessing import MaxAbsScaler as sk_MaxAbsScaler sk_model = sk_MaxAbsScaler() sk_model.scale_ = self.scale_values sk_model.max_abs_ = self.max_abs_values sk_model.n_features_in_ = len(self.max_abs_values) # type: ignore[arg-type] sk_model.n_samples_seen_ = self.n_samples_seen with open(path, "wb") as fp: pickle.dump(sk_model, fp) def _load_core_model(self, path: str) -> None: with open(path, "rb") as fp: sk_model = pickle.load(fp) self.max_abs_values = sk_model.max_abs_ self.scale_values = sk_model.scale_ self.n_samples_seen = sk_model.n_samples_seen_ class StandardScaler(Estimator, HasInputCol, HasOutputCol, ParamsReadWrite): """ Standardizes features by removing the mean and scaling to unit variance using column summary statistics on the samples in the training set. .. versionadded:: 3.5.0 Examples -------- >>> from pyspark.ml.connect.feature import StandardScaler >>> scaler = StandardScaler(inputCol='features', outputCol='scaled_features') >>> dataset = spark.createDataFrame([ ... ([1.0, 2.0],), ... ([2.0, -1.0],), ... ([-3.0, -2.0],), ... ], schema=['features']) >>> scaler_model = scaler.fit(dataset) >>> transformed_dataset = scaler_model.transform(dataset) >>> transformed_dataset.show(truncate=False) +------------+------------------------------------------+ |features |scaled_features | +------------+------------------------------------------+ |[1.0, 2.0] |[0.3779644730092272, 1.1208970766356101] | |[2.0, -1.0] |[0.7559289460184544, -0.3202563076101743] | |[-3.0, -2.0]|[-1.1338934190276817, -0.8006407690254358]| +------------+------------------------------------------+ """ _input_kwargs: Dict[str, Any] @keyword_only def __init__(self, inputCol: Optional[str] = None, outputCol: Optional[str] = None) -> None: """ __init__(self, \\*, inputCol=None, outputCol=None) """ super().__init__() kwargs = self._input_kwargs self._set(**kwargs) def _fit(self, dataset: Union[DataFrame, pd.DataFrame]) -> "StandardScalerModel": input_col = self.getInputCol() stat_result = summarize_dataframe(dataset, input_col, ["mean", "std", "count"]) mean_values = stat_result["mean"] std_values = stat_result["std"] n_samples_seen = stat_result["count"] model = StandardScalerModel(mean_values, std_values, n_samples_seen) model._resetUid(self.uid) return self._copyValues(model) class StandardScalerModel(Model, HasInputCol, HasOutputCol, ParamsReadWrite, CoreModelReadWrite): """ Model fitted by StandardScaler. .. versionadded:: 3.5.0 """ def __init__( self, mean_values: Optional["np.ndarray"] = None, std_values: Optional["np.ndarray"] = None, n_samples_seen: Optional[int] = None, ) -> None: super().__init__() self.mean_values = mean_values self.std_values = std_values if std_values is not None: # if scale value is zero, replace it with 1.0 (for preventing division by zero) self.scale_values = np.where(std_values == 0.0, 1.0, std_values) self.n_samples_seen = n_samples_seen def _input_columns(self) -> List[str]: return [self.getInputCol()] def _output_columns(self) -> List[Tuple[str, str]]: return [(self.getOutputCol(), "array")] def _get_transform_fn(self) -> Callable[..., Any]: mean_values = self.mean_values scale_values = self.scale_values def transform_fn(series: Any) -> Any: def map_value(x: "np.ndarray") -> "np.ndarray": return (x - mean_values) / scale_values return series.apply(map_value) return transform_fn def _get_core_model_filename(self) -> str: return self.__class__.__name__ + ".sklearn.pkl" def _save_core_model(self, path: str) -> None: from sklearn.preprocessing import StandardScaler as sk_StandardScaler sk_model = sk_StandardScaler(with_mean=True, with_std=True) sk_model.scale_ = self.scale_values sk_model.var_ = self.std_values * self.std_values # type: ignore[operator] sk_model.mean_ = self.mean_values sk_model.n_features_in_ = len(self.std_values) # type: ignore[arg-type] sk_model.n_samples_seen_ = self.n_samples_seen with open(path, "wb") as fp: pickle.dump(sk_model, fp) def _load_core_model(self, path: str) -> None: with open(path, "rb") as fp: sk_model = pickle.load(fp) self.std_values = np.sqrt(sk_model.var_) self.scale_values = sk_model.scale_ self.mean_values = sk_model.mean_ self.n_samples_seen = sk_model.n_samples_seen_