{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Example usage of `error-parity` with other fairness-constrained classifiers\n", "\n", "Contents:\n", "1. Train a standard (unconstrained) model;\n", "2. Check attainable fairness-accuracy trade-offs via post-processing, with the `error-parity` package;\n", "3. Train fairness-constrained model (in-processing fairness intervention), with the `fairlearn` package;\n", "5. Map results for post-processing + in-processing interventions;\n", "\n", "---\n", "\n", "**NOTE**: This notebook has the following extra requirements: `fairlearn` `lightgbm`.\n", "\n", "Install them with ```pip install fairlearn lightgbm```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os\n", "import numpy as np" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "error-parity==0.3.11\n" ] } ], "source": [ "from error_parity import __version__\n", "print(f\"error-parity=={__version__}\")" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", "import seaborn as sns\n", "sns.set(palette=\"colorblind\", style=\"whitegrid\", rc={\"grid.linestyle\": \"--\", \"figure.dpi\": 200, \"figure.figsize\": (4,3)})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some useful global constants:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "SEED = 2\n", "\n", "TEST_SIZE = 0.3\n", "VALIDATION_SIZE = None\n", "\n", "PERF_METRIC = \"accuracy\"\n", "DISP_METRIC = \"equalized_odds_diff\"\n", "\n", "N_JOBS = max(2, os.cpu_count() - 2)\n", "\n", "np.random.seed(SEED)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Fetch UCI Adult data\n", "\n", "We'll use the `sex` column as the sensitive attribute.\n", "That is, false positive (FP) and false negative (FN) errors should not disproportionately impact individuals based on their sex." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "SENSITIVE_COL = \"sex\"\n", "sensitive_col_map = {\"Male\": 0, \"Female\": 1}\n", "\n", "# NOTE: You can also try to run this using the `race` column as sensitive attribute (as commented below).\n", "# SENSITIVE_COL = \"race\"\n", "# sensitive_col_map = {\"White\": 0, \"Black\": 1, \"Asian-Pac-Islander\": 1, \"Amer-Indian-Eskimo\": 1, \"Other\": 1}\n", "\n", "sensitive_col_inverse = {val: key for key, val in sensitive_col_map.items()}\n", "\n", "POS_LABEL = \">50K\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Download data." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from fairlearn.datasets import fetch_adult\n", "\n", "X, Y = fetch_adult(\n", " as_frame=True,\n", " return_X_y=True,\n", ")\n", "\n", "# Map labels and sensitive column to numeric data\n", "Y = np.array(Y == POS_LABEL, dtype=int)\n", "S = np.array([sensitive_col_map[elem] for elem in X[SENSITIVE_COL]], dtype=int)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Split in train/test/validation data." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", "\n", "X_train, X_other, y_train, y_other, s_train, s_other = train_test_split(\n", " X, Y, S,\n", " test_size=TEST_SIZE + (VALIDATION_SIZE or 0),\n", " stratify=Y, random_state=SEED,\n", ")\n", "\n", "if VALIDATION_SIZE is not None and VALIDATION_SIZE > 0:\n", " X_val, X_test, y_val, y_test, s_val, s_test = train_test_split(\n", " X_other, y_other, s_other,\n", " test_size=TEST_SIZE / (TEST_SIZE + VALIDATION_SIZE),\n", " stratify=y_other, random_state=SEED,\n", " )\n", "else:\n", " X_test, y_test, s_test = X_other, y_other, s_other\n", " X_val, y_val, s_val = X_train, y_train, s_train" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Log the accuracy attainable by a dummy constant classifier." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'train': 0.7607125098715961,\n", " 'test': 0.7607315908005187,\n", " 'validation': 0.7607125098715961}" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def compute_constant_clf_accuracy(labels: np.ndarray) -> float:\n", " return max((labels == const_pred).mean() for const_pred in np.unique(labels))\n", "\n", "constant_clf_accuracy = {\n", " \"train\": compute_constant_clf_accuracy(y_train),\n", " \"test\": compute_constant_clf_accuracy(y_test),\n", " \"validation\": compute_constant_clf_accuracy(y_val),\n", "}\n", "constant_clf_accuracy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Train a standard (unconstrained) classifier" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
LGBMClassifier(verbosity=-1)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
LGBMClassifier(verbosity=-1)
ExponentiatedGradient(constraints=<fairlearn.reductions._moments.utility_parity.EqualizedOdds object at 0x16dac2650>,\n", " estimator=LGBMClassifier(verbosity=-1), max_iter=10,\n", " nu=0.000851617415307666)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
ExponentiatedGradient(constraints=<fairlearn.reductions._moments.utility_parity.EqualizedOdds object at 0x16dac2650>,\n", " estimator=LGBMClassifier(verbosity=-1), max_iter=10,\n", " nu=0.000851617415307666)
LGBMClassifier(verbosity=-1)
LGBMClassifier(verbosity=-1)