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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9, '3.10', 3.11]
python-version: ['3.10', 3.11, 3.12, 3.13]

env:
UCI_DB: "https://archive.ics.uci.edu/ml/machine-learning-databases"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ Supported Python Configurations:

| OS | Python version |
| ------- | -------------- |
| macOS | 3.8 – 3.11 |
| Ubuntu | 3.8 – 3.11 |
| Windows | 3.8 – 3.11 |
| macOS | 3.10 – 3.13 |
| Ubuntu | 3.10 – 3.13 |
| Windows | 3.10 – 3.13 |

### (Optional) Create a virtual environment

Expand Down
9 changes: 5 additions & 4 deletions aif360/sklearn/inprocessing/infairness.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@ def __init__(self, *args, criterion, train_split=None, regression='auto',
dataset=dataset, **kwargs)
self.regression = regression

@property
def _estimator_type(self):
def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
if hasattr(self, "regression_"):
return 'regressor' if self.regression_ else 'classifier'
tags.estimator_type = 'regressor' if self.regression_ else 'classifier'
elif self.regression != 'auto':
return 'regressor' if self.regression else 'classifier'
tags.estimator_type = 'regressor' if self.regression else 'classifier'
else:
raise NotFittedError("regression is set to 'auto'. Call 'fit' with "
"appropriate arguments or set regression manually.")
return tags

def get_loss(self, y_pred, y_true, X=None, training=False):
"""Return the loss for this batch.
Expand Down
27 changes: 18 additions & 9 deletions aif360/sklearn/postprocessing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
Post-processing algorithms modify predictions to be more fair (predictions in,
predictions out).
"""
from dataclasses import dataclass
from logging import warning

import pandas as pd
from sklearn.base import BaseEstimator, MetaEstimatorMixin, clone
from sklearn.model_selection import train_test_split
from sklearn.utils.metaestimators import available_if
from sklearn.utils import get_tags, Tags


@dataclass
class PostProcessingTags(Tags):
requires_proba: bool = True


from aif360.sklearn.postprocessing.calibrated_equalized_odds import CalibratedEqualizedOdds
from aif360.sklearn.postprocessing.reject_option_classification import RejectOptionClassifier, RejectOptionClassifierCV
Expand Down Expand Up @@ -54,9 +62,10 @@ def __init__(self, estimator, postprocessor, *, prefit=False, val_size=0.25,
self.val_size = val_size
self.options = options

@property
def _estimator_type(self):
return self.postprocessor._estimator_type
def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = get_tags(self.postprocessor).estimator_type
return tags

@property
def classes_(self):
Expand Down Expand Up @@ -85,8 +94,8 @@ def fit(self, X, y, sample_weight=None, **fit_params):
self.estimator_ = self.estimator if self.prefit else clone(self.estimator)

try:
use_proba = self.postprocessor._get_tags()['requires_proba']
except KeyError:
use_proba = get_tags(self.postprocessor).requires_proba
except AttributeError:
raise TypeError("`postprocessor` (type: {}) does not have a "
"'requires_proba' tag.".format(type(self.estimator)))
if use_proba and not hasattr(self.estimator, 'predict_proba'):
Expand Down Expand Up @@ -145,7 +154,7 @@ def predict(self, X):
Returns:
numpy.ndarray: Predicted class label per sample.
"""
use_proba = self.postprocessor_._get_tags()['requires_proba']
use_proba = get_tags(self.postprocessor_).requires_proba
y_score = (self.estimator_.predict_proba(X) if use_proba else
self.estimator_.predict(X))
y_score = pd.DataFrame(y_score, index=X.index).squeeze('columns')
Expand All @@ -169,7 +178,7 @@ def predict_proba(self, X):
in the model, where classes are ordered as they are in
``self.classes_``.
"""
use_proba = self.postprocessor_._get_tags()['requires_proba']
use_proba = get_tags(self.postprocessor_).requires_proba
y_score = (self.estimator_.predict_proba(X) if use_proba else
self.estimator_.predict(X))
y_score = pd.DataFrame(y_score, index=X.index).squeeze('columns')
Expand All @@ -193,7 +202,7 @@ def predict_log_proba(self, X):
the model, where classes are ordered as they are in
``self.classes_``.
"""
use_proba = self.postprocessor_._get_tags()['requires_proba']
use_proba = get_tags(self.postprocessor_).requires_proba
y_score = (self.estimator_.predict_proba(X) if use_proba else
self.estimator_.predict(X))
y_score = pd.DataFrame(y_score, index=X.index).squeeze('columns')
Expand All @@ -216,7 +225,7 @@ def score(self, X, y, sample_weight=None):
Returns:
float: Score value.
"""
use_proba = self.postprocessor_._get_tags()['requires_proba']
use_proba = get_tags(self.postprocessor_).requires_proba
y_score = (self.estimator_.predict_proba(X) if use_proba else
self.estimator_.predict(X))
y_score = pd.DataFrame(y_score, index=X.index).squeeze('columns')
Expand Down
10 changes: 8 additions & 2 deletions aif360/sklearn/postprocessing/calibrated_equalized_odds.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from dataclasses import asdict

import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.utils import check_random_state
from sklearn.utils.validation import check_is_fitted

from aif360.sklearn.metrics import difference, base_rate
from aif360.sklearn.metrics import generalized_fnr, generalized_fpr
from aif360.sklearn.postprocessing import PostProcessingTags
from aif360.sklearn.utils import check_inputs, check_groups


Expand Down Expand Up @@ -67,8 +70,11 @@ def __init__(self, prot_attr=None, cost_constraint='weighted',
self.cost_constraint = cost_constraint
self.random_state = random_state

def _more_tags(self):
return {'requires_proba': True}
def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags = PostProcessingTags(**asdict(tags))
tags.requires_proba = True
return tags

def _weighted_cost(self, y_true, probas_pred, pos_label=1,
sample_weight=None):
Expand Down
16 changes: 12 additions & 4 deletions aif360/sklearn/postprocessing/reject_option_classification.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import asdict
import warnings

import numpy as np
Expand All @@ -10,6 +11,7 @@
from aif360.sklearn.metrics import equal_opportunity_difference
from aif360.sklearn.metrics import disparate_impact_ratio
from aif360.sklearn.metrics import make_scorer
from aif360.sklearn.postprocessing import PostProcessingTags
from aif360.sklearn.utils import check_groups


Expand Down Expand Up @@ -90,8 +92,11 @@ def __init__(self, prot_attr=None, threshold=0.5, margin=0.1):
self.threshold = threshold
self.margin = margin

def _more_tags(self):
return {'requires_proba': True}
def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags = PostProcessingTags(**asdict(tags))
tags.requires_proba = True
return tags

def fit(self, X, y, labels=None, pos_label=1, priv_group=1,
sample_weight=None):
Expand Down Expand Up @@ -284,8 +289,11 @@ def __init__(self, prot_attr=None, *, scoring, step=0.05, refit=True, **kwargs):
super().__init__(RejectOptionClassifier(), {}, scoring=scoring,
refit=refit, **kwargs)

def _more_tags(self):
return {'requires_proba': True}
def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags = PostProcessingTags(**asdict(tags))
tags.requires_proba = True
return tags

def fit(self, X, y, **fit_params):
"""Run fit with all sets of parameters.
Expand Down
8 changes: 5 additions & 3 deletions aif360/sklearn/preprocessing/reweighing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numpy as np
from sklearn.base import BaseEstimator, MetaEstimatorMixin, clone
from sklearn.utils import get_tags
from sklearn.utils.metaestimators import available_if
from sklearn.utils.validation import has_fit_parameter

Expand Down Expand Up @@ -116,9 +117,10 @@ def __init__(self, estimator, reweigher=None):
self.reweigher = reweigher
self.estimator = estimator

@property
def _estimator_type(self):
return self.estimator._estimator_type
def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = get_tags(self.estimator).estimator_type
return tags

@property
def classes_(self):
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,18 @@
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
],
packages=find_packages(include=["aif360*"]),
python_requires='>=3.8',
install_requires=[
'numpy>=1.16',
'scipy>=1.2.0',
'pandas>=0.24.0',
'scikit-learn>=1.0',
'scikit-learn>=1.6',
'matplotlib',
],
extras_require=extras,
Expand Down
Loading