diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2a42bb8..3295960 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,11 @@ ========= CHANGELOG ========= +------------------------------------------------------------------------------- +September 7, 2026 1.2.1 +------------------------------------------------------------------------------- + +- Updated linear selector to support Python 3.12 and scikit-learn==1.9.0. Thanks to @rbaral for the contribution. ------------------------------------------------------------------------------- August 7, 2025 1.2.0 diff --git a/feature/_version.py b/feature/_version.py index 0f3c50c..c1cf75a 100644 --- a/feature/_version.py +++ b/feature/_version.py @@ -2,4 +2,4 @@ # Copyright FMR LLC # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.2.0" \ No newline at end of file +__version__ = "1.2.1" \ No newline at end of file diff --git a/feature/linear.py b/feature/linear.py index c931867..fedd1a4 100644 --- a/feature/linear.py +++ b/feature/linear.py @@ -3,18 +3,18 @@ # SPDX-License-Identifier: Apache-2.0 from typing import NoReturn, Tuple - +import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression, Lasso, Ridge from sklearn.linear_model import LogisticRegression, RidgeClassifier - +from sklearn.multiclass import OneVsRestClassifier from feature.base import _BaseSupervisedSelector, _BaseDispatcher from feature.utils import Num, get_task_string class _Linear(_BaseSupervisedSelector, _BaseDispatcher): - def __init__(self, seed: int, num_features: Num, regularization: str, alpha:Num): + def __init__(self, seed: int, num_features: Num, regularization: str, alpha: Num): super().__init__(seed) self.num_features = num_features # this could be int or float @@ -28,20 +28,17 @@ def __init__(self, seed: int, num_features: Num, regularization: str, alpha:Num) self.factory = {"regression_none": LinearRegression(), "regression_lasso": Lasso(random_state=self.seed), "regression_ridge": Ridge(random_state=self.seed), - # "classification_none": LogisticRegression(penalty="none"), # won't converge most times - "classification_none": LogisticRegression(random_state=self.seed, - multi_class="auto", solver="liblinear"), - "classification_lasso": LogisticRegression(random_state=self.seed, penalty='l1', - multi_class="auto", solver="liblinear"), + "classification_none": OneVsRestClassifier( + LogisticRegression(random_state=self.seed, solver="liblinear")), + "classification_lasso": OneVsRestClassifier( + LogisticRegression(random_state=self.seed, penalty='l1', solver="liblinear")), "classification_ridge": RidgeClassifier(random_state=self.seed)} def get_model_args(self, selection_method) -> Tuple: - # Pack model argument return selection_method.regularization def dispatch_model(self, labels: pd.Series, *args): - # Unpack model argument regularization = args[0] @@ -49,7 +46,18 @@ def dispatch_model(self, labels: pd.Series, *args): self.imp = self.factory.get(get_task_string(labels) + regularization) def fit(self, data: pd.DataFrame, labels: pd.Series) -> NoReturn: + """ + Fits the underlying linear model to the data and calculates absolute feature importances. + + This method identifies the appropriate model context (regression vs. classification), + fits it to the training data, and extracts the coefficient weights. For multi-class + classifiers, it computes a global score by averaging the class-specific absolute weights. + :param data: The input features dataframe of shape (n_samples, n_features). + :param labels: The target labels series. Automatically determines whether the task + is regression or classification. + + """ # Fit linear model self.imp.fit(X=data, y=labels) @@ -60,15 +68,19 @@ def fit(self, data: pd.DataFrame, labels: pd.Series) -> NoReturn: # But that does not necessarily mean they are more important # See more discussion here: # https://scikit-learn.org/stable/auto_examples/inspection/plot_linear_model_coefficient_interpretation.html#sphx-glr-auto-examples-inspection-plot-linear-model-coefficient-interpretation-py - self.abs_scores = abs(self.imp.coef_) - # LogisticRegression/RidgeClassifier returns a coef_ array of (n_classes, n_features) # These coefficients map the importance of the feature for a specific class. # One approach is to average the importances - if isinstance(self.imp, LogisticRegression) or isinstance(self.imp, RidgeClassifier): - self.abs_scores = abs(self.imp.coef_.mean(0)) + if isinstance(self.imp, OneVsRestClassifier): + coefficients = np.vstack([estimator.coef_ for estimator in self.imp.estimators_]) + else: + coefficients = np.asarray(self.imp.coef_) - def transform(self, data: pd.DataFrame) -> pd.DataFrame: + if isinstance(self.imp, (LogisticRegression, OneVsRestClassifier, RidgeClassifier)): + self.abs_scores = np.abs(coefficients.mean(0)) + else: + self.abs_scores = np.abs(coefficients) + def transform(self, data: pd.DataFrame) -> pd.DataFrame: # Select top-k from data based on abs_scores and num_features return self.get_top_k(data, self.abs_scores) diff --git a/feature/text_based.py b/feature/text_based.py index 532c0ad..b182c42 100644 --- a/feature/text_based.py +++ b/feature/text_based.py @@ -647,7 +647,7 @@ def process_category_data(input_df: pd.DataFrame, categories: List[str]) -> pd.D matrix = (input_df.labels.str.split('|', expand=True) .stack() .str.get_dummies() - .groupby(level=0, axis=0) + .groupby(level=0) .sum()).T check_true(matrix.ndim == 2, ValueError("Process Data Error: matrix should 2D")) diff --git a/feature/utils.py b/feature/utils.py index f3d03fe..e2ff4c8 100644 --- a/feature/utils.py +++ b/feature/utils.py @@ -16,7 +16,6 @@ from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler - Num = Union[int, float] """Num type is defined as integer or float.""" @@ -52,7 +51,6 @@ def get_data_label(sklearn_dataset): def get_task_string(labels: pd.Series): - if labels is None: return "unsupervised_" @@ -65,7 +63,6 @@ def is_classification(labels: pd.Series): def get_selector(score_func, k: Union[int, float]): - # Top K or Top Percentile if isinstance(k, int): return SelectKBest(score_func, k=k) @@ -131,7 +128,6 @@ class DataTransformer: """ def __init__(self): - # Imputation self.imp = SimpleImputer(strategy='median') @@ -206,7 +202,7 @@ def reduce_memory(df: pd.DataFrame, verbose=True) -> pd.DataFrame: # Print current column type if verbose: - print(20*"=") + print(20 * "=") print("Column ", i, ":", col) print("dtype_before: ", df[col].dtype) @@ -255,7 +251,7 @@ def reduce_memory(df: pd.DataFrame, verbose=True) -> pd.DataFrame: # Print new column type if verbose: print("dtype_after: ", df[col].dtype) - print(20*"=") + print(20 * "=") memory_after = df.memory_usage().sum() / 1024 ** 2