Skip to main content

A python package for assessing machine learning fairness with multiple contextual norms.

Project description

License: MIT pypy: v

ContextualFairness is a Python package for assessing machine learning fairness with multiple contextual norms. The packages provides functions and classes for defining contextual norms and calculating a fairness score for a model based on these norms for binary classification and regression tasks using tabular data.

The contextual norms allow for not only considering the equality norms, but also other norms such as equity or need. This is important because depening on the context equality is not the only fairness norm that is suitable or even suitable at all.

ContextualFairness allows for a fairness analysis on three levels, the global level, the between-group level (e.g., old vs young people), and the in-group level.

This three level analysis allows for a more detailed fairness analysis and combined with the contextual norms, ContextualFairness allows for more nuanced evalutions of fairness with respect to the societal context an ML system operates in.

Contents

  1. Installation with pip
  2. Usage
  3. Example
  4. Limitations

Installation with pip

  1. (Optionally) create a virtual environment
python3 -m venv .venv
source .venv/bin/activate
  1. Install via pip
pip install contextualfairness

Usage

Formulating contextual norms

The first step in using ContextualFairness is to elicit and define the relevant norms for a specific ML model in a specific context. This is not a technical step, but rather a societal step. For this elicitation, all relevant stakeholders in a specific context should be considered. By using stakeholder elicitation techniques, fairness norms can formulated. Note that this is not a straightforward step and requires careful consideration of the societal context and stakeholders.

An example of formulated norms for an income prediction scenario could be:

  • Everybody should get the same prediction.
  • People who work more hours should earn more.
  • People with a lower education level should earn more.

Operationalizing norms

To use ContextualFairness, we must first operationalize the norms, to this end ContextualFairness provides three classes: BinaryClassificationEqualityNorm, RegressionEqualityNorm, and RankNorm. The first two are specific for the ML task at hand and the last one can be used for both binary classification and regression.

The BinaryClassificationEqualityNorm is operationalized as follows:

from contextualfairness.norms import BinaryClassificationEqualityNorm

binary_classification_equality_norm = BinaryClassificationEqualityNorm()

in this case equality means being equal to the majority class in the predictions and we calculate a score for this norm by counting the number of samples that are not predicted the majority class. Alternatively, we can also specify a positive class. In this case, equality means being predicted the positive class.

binary_classification_equality_norm = BinaryClassificationEqualityNorm(positive_class_value=1)

The RegressionEqualityNorm is operationalized as follows:

from contextualfairness.norms import RegressionEqualityNorm

regression_equality_norm = RegressionEqualityNorm()

Here equality, means having the maximum predicted value. Therefore, we calculate a score for this norm by taking the (absolute) difference between the prediction for each sample and the maximum prediction.

To operationalize a RankNorm, we must first specify a statement to rank all samples in the dataset with respect to the norm. As ContextualFairness uses polars under the hood, this must be formulated as a polars expression. For example, for the norms defined above, rank by hours worked if people who work more hours should earn more. This gives te following operationalization:

import polars as pl

from contextualfairness.norms import RankNorm

more_hours_worked_is_preferred = pl.col("hours_worked") # assuming the column `hours_worked` exists in the polars DataFrame

rank_norm = RankNorm(norm_statement=more_hours_worked_is_preferred, name="Work more hours")

For rank norms we calculate a score, by for each sample counting the number of samples that are ranked lower with respect to the norm_statement but higher with respect to an outcome_score. This outcome_score is the probability of being predicted a (positive) class for binary classification or the predicted value for regression.

Calculating contextual fairness

After operationalizing the norms, we provide these norms to contextual_fairness_score to calculate the contextual fairness score for a specific model. We also specifiy a list of weights that will weigh the results for each norm for the total score.

For binary classification this looks as follows:

from contextualfairness.scorer import contextual_fairness_score

norms = [binary_classification_equality_norm, rank_norm]

result = contextual_fairness_score(
    norms=norms,
    data=data, # Dictionary with each key corresponding to column in the data
    y_pred=y_pred, # Assume the existence of some array-like of predictions
    outcome_scores=y_pred_probas, # Assume the existence of some array-like of outcome_scores
    weights=[0.6, 0.4]
)

Alternatively, we can also not specify the weights to get uniform weights:

result = contextual_fairness_score(norms=norms, data=data, y_pred=y_pred, outcome_scores=y_pred_probas)

For regression, we do it as follows:

norms = [regression_equality_norm, rank_norm]

result = contextual_fairness_score(
    norms=norms,
    X=X, # Assume the existence of some pandas.DataFrame dataset
    y_pred=y_pred, # Assume the existence of some array-like of predictions
)

Note, that not specifying the outcome_scores results in setting outcome_scores=y_pred, which is useful for regression.

Analyze the results

After calculating the score, we can analyze the results on three levels:

The total score:

result.total_score()

The between-group and in-group level:

group_scores = result.group_scores(attributes=["sex", "age"]).collect() # assuming existence of `sex` and `age` attribute in X

print(group_scores.filter((pl.col("sex") == "male") & (pl.col("age") == "young"))) # To show the score for this group and the ids of the individuals belonging to the group.

This gives the score for all groups in the dataset (where a group is combination of values for the specified attributes, e.g., sex=male and age=young). These scores an be compared between the different groups. Additionally, the data used for calculating the group scores is also provided to analyze the scores with-in a group.

The group scores can also be scaled relative to their group sizes, as follows:

group_scores = result.group_scores(attributes=["sex", "age"], scaled=True).collect()

Finally, for additional analyses the polars.DataFrame containing the results can be accessed as follows:

result.df

Example

We show a short example on the ACSIncome data using a LogisticRegression, with the three following norms:

  • Everybody should get the same prediction.
  • People who work more hours should earn more.
  • People with a lower education level should earn more.
from folktables import ACSDataSource, ACSIncome
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

import polars as pl


from contextualfairness.scorer import contextual_fairness_score
from contextualfairness.norms import BinaryClassificationEqualityNorm, RankNorm


# load and prepare data
data_source = ACSDataSource(
    survey_year="2016", horizon="1-Year", survey="person", root_dir="examples/data/raw"
)
acs_data = data_source.get_data(states=["WY"], download="True")
X, y, _ = ACSIncome.df_to_pandas(acs_data)
y = y["PINCP"]
sensitive_attribute = X["SEX"].copy()

X_train, X_test, y_train, y_test, sens_train, sens_test = train_test_split(
    X, y, sensitive_attribute, test_size=0.2, random_state=0
)

# Train model
clf = LogisticRegression(max_iter=10_000, penalty="l2", random_state=42)
clf.fit(X_train, y_train)

# Predict for test data
y_pred = clf.predict(X_test)
y_pred_probas = clf.predict_proba(X_test)[:, 1]


norms = [
    BinaryClassificationEqualityNorm(positive_class_value=True),
    RankNorm(norm_statement=pl.col("WKHP"), name="work_more_hours"),
    RankNorm(norm_statement=-pl.col("SCHL"), name="lower_education"),
]

# Calculate contextual fairness
result = contextual_fairness_score(
    norms=norms,
    data=X_test.to_dict("list"),
    y_pred=y_pred,
    outcome_scores=y_pred_probas,
)

# Analysis
print(result.total_score())
print(result.group_scores(attributes=["SEX"], scaled=True).collect())
print(
    result.group_scores(attributes=["SEX"], scaled=True)
    .collect()
    .filter(pl.col("SEX") == 1.0)
)

Additional examples can be found in the examples folder.

Limitations

The most important limitations of the current implementation are:

  • On big datasets calculating rank norms becomes time consuming due to the required pairwise comparison of samples.
  • Norms are combined linearly, consequently ContextualFairness cannot capture conditional or hierarchical relations between norms, for example, when we want equity except in cases of need.
  • Rank norms can only be meaningfully defined for tabular data, as defining a norm_function for other types of data such as image, sound or text data is much harder.

Further limitations of ContextualFairness can be found in the paper.

Citing ContextualFairness

ContextualFairness is proposed in this paper, wich you can cite as follows:

@inproceedings{kerkhoven2025assessing,
  title={Assessing machine learning fairness with multiple contextual norms},
  author={Kerkhoven, Pim and Dignum, Virginia and Bhuyan, Monowar},
  booktitle={The 37th Benelux Conference on Artificial Intelligence and the 34th Belgian Dutch Conference on Machine Learning},
  year={2025}
}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

contextualfairness-0.0.2.tar.gz (13.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

contextualfairness-0.0.2-py3-none-any.whl (10.8 kB view details)

Uploaded Python 3

File details

Details for the file contextualfairness-0.0.2.tar.gz.

File metadata

  • Download URL: contextualfairness-0.0.2.tar.gz
  • Upload date:
  • Size: 13.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.1

File hashes

Hashes for contextualfairness-0.0.2.tar.gz
Algorithm Hash digest
SHA256 b0a4221784c91d31262b46ad0057ff0a76b5a5e7974d92ae0e338586a578c368
MD5 8092fd0621be2d6e0dafa85aa948d6d2
BLAKE2b-256 bce145d75aaa035f09eeabba1f0ec1734528d4338bbe61d5ba665da82ac28558

See more details on using hashes here.

File details

Details for the file contextualfairness-0.0.2-py3-none-any.whl.

File metadata

File hashes

Hashes for contextualfairness-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 b116ad08229550201707e4891bac7864959b46cb06a8019bee06901d71f3feac
MD5 06eb2cc2c7328b043300917dcbfa87fb
BLAKE2b-256 bd35d4094702fcc1fafbaf5cbf8804c8a475f75970b9bd2400191c12668270d8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page