A python package for assessing machine learning fairness with multiple contextual norms.
Project description
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
Installation with pip
- (Optionally) create a virtual environment
python3 -m venv .venv
source .venv/bin/activate
- 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 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 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 function for ranking all samples in the dataset with respect to the norm. 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:
from contextualfairness import RankNorm
def more_hours_worked_is_preferred(x):
x.hours_worked # assuming x has the attribute `hours_worked`
rank_norm = RegressionEqualityNorm(norm_function=more_hours_worked_is_preferred, name="Work more hours")
Alternative, we can also not specify a name, in this case the function name is used to name the norm:
rank_norm = RegressionEqualityNorm(norm_function=more_hours_worked_is_preferred)
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_function but higher with respect to an output_score. This output_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 import contextual_fairness_score
norms = [binary_classification_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
outcome_scores=y_pred_probas, # Assume the existence of some array-like of outcome_scores
weights=[0.5, 0.4]
)
Alternatively, we can also not specify the weights to get uniform weights:
result = contextual_fairness_score(norms=norms, X=X, 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 for regression not specifying the outcome_scores results in setting outcome_scores=y_pred.
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"]) # assuming existence of `sex` and `age` attribute in X
print(group_scores["sex=male;age=young"]["score"])
print(group_scores["sex=male;age=young"]["data"])
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;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)
Finally, for additional analyses the pandas.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
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")
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]
# Define norms
def work_more_hours(x):
return x["WKHP"]
def lower_education(x):
return -x["SCHL"]
norms = [
BinaryClassificationEqualityNorm(positive_class_value=True),
RankNorm(norm_function=work_more_hours),
RankNorm(norm_function=lower_education),
]
# Calculate contextual fairness
result = contextual_fairness_score(
norms=norms, X=X_test, y_pred=y_pred, outcome_scores=y_pred_probas
)
# Analysis
print(result.total_score())
print(result.group_scores(attributes=["SEX"], scaled=True))
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_functionfor other types of data such as image, sound or text data is much harder.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file contextualfairness-0.0.1.tar.gz.
File metadata
- Download URL: contextualfairness-0.0.1.tar.gz
- Upload date:
- Size: 13.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b089ed9d0e8a352fa492c01d935de18c13f67c4179a5d2859103fee874fdc649
|
|
| MD5 |
c8f35454f615cb12f40a798f1520f02e
|
|
| BLAKE2b-256 |
3c8d09995781f079b9b99e71b233c042f987d0388c0ac0c766ebdb2981416e94
|
File details
Details for the file contextualfairness-0.0.1-py3-none-any.whl.
File metadata
- Download URL: contextualfairness-0.0.1-py3-none-any.whl
- Upload date:
- Size: 10.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7c36cffa70ff797a7226c7549799ada31b381412bc4b2c1e5b05f45c5adb3b45
|
|
| MD5 |
8d25afa6f12891fbfc4da33a8a3aa3ac
|
|
| BLAKE2b-256 |
215e9f096f4c87c11938fe3f6a6cfb82e0b0e12348635f2751e9a49c43a53bab
|