Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion feature/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright FMR LLC <opensource@fidelity.com>
# SPDX-License-Identifier: Apache-2.0

__version__ = "1.2.0"
__version__ = "1.2.1"
42 changes: 27 additions & 15 deletions feature/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,28 +28,36 @@ 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]

# Set linear model
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the notes!


: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)

Expand All @@ -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)
2 changes: 1 addition & 1 deletion feature/text_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
8 changes: 2 additions & 6 deletions feature/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -52,7 +51,6 @@ def get_data_label(sklearn_dataset):


def get_task_string(labels: pd.Series):

if labels is None:
return "unsupervised_"

Expand All @@ -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)
Expand Down Expand Up @@ -131,7 +128,6 @@ class DataTransformer:
"""

def __init__(self):

# Imputation
self.imp = SimpleImputer(strategy='median')

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
Loading