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
6 changes: 4 additions & 2 deletions fairlearn/experimental/enable_metric_frame_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
The API and results of these estimators might change without any deprecation
cycle.

Importing this file imports the standalone function `plot_metric_frame`
that visualizes metrics and their statistical metric errors from MetricFrames.
Importing this file imports the standalone functions `plot_metric_frame`
and `plot_metric_frame_with_ci` that visualize metrics and their statistical
metric errors from MetricFrames.
"""

from ..metrics._plotter import plot_metric_frame # noqa: F401
from ..metrics._plot_with_ci import plot_metric_frame_with_ci # noqa: F401
173 changes: 173 additions & 0 deletions fairlearn/metrics/_plot_with_ci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Copyright (c) Fairlearn contributors.
# Licensed under the MIT License.

"""Plotting function for MetricFrame with built-in confidence intervals."""

from __future__ import annotations

import numpy as np
import pandas as pd
from matplotlib.lines import Line2D

from ._metric_frame import MetricFrame

_METRIC_FRAME_INVALID_ERROR = "Input metric_frame should be of type MetricFrame."
_METRIC_FRAME_NO_CI_ERROR = (
"MetricFrame does not have confidence intervals. "
"Please initialize with n_boot and ci_quantiles parameters."
)
_METRICS_NOT_LIST_OR_STR_ERROR = (
"Metric should be a string of a single metric or a list of the metrics "
"in provided MetricFrame, but {0} was provided"
)
_CI_QUANTILE_INDEX_ERROR = "ci_quantile_index must be between 0 and {0}"


def plot_metric_frame_with_ci(
metric_frame: MetricFrame,
*,
kind: str = "point",
metrics: list[str] | str | None = None,
ci_quantile_index: int = 0,
subplots: bool = True,
**kwargs,
):
"""Plot MetricFrame metrics with built-in confidence intervals as error bars.

This function plots metrics from a MetricFrame that has been initialized with
bootstrapping parameters (n_boot and ci_quantiles). It uses the built-in
confidence intervals as error bars.

Parameters
----------
metric_frame : fairlearn.metrics.MetricFrame
The MetricFrame with precomputed confidence intervals.
Must be initialized with n_boot and ci_quantiles parameters.

kind : str, default="point"
The type of plot to display, e.g., "point", "bar", "line", etc.
The supported values are "point" and those listed in pandas.DataFrame.plot

metrics : list[str] | str | None
The name of the metrics to plot.
Should match columns from the MetricFrame.
If None, plots all scalar metrics.

ci_quantile_index : int, default=0
Index of the confidence interval quantile to use for error bars.
For example, if ci_quantiles=[0.025, 0.975], use index 0 for lower bound
and index 1 for upper bound. The function automatically uses both bounds.

subplots : bool, default=True
Whether or not to plot metrics on separate subplots

**kwargs
Keyword arguments that are passed to pandas.DataFrame.plot

Returns
-------
matplotlib.axes.Axes or numpy.ndarray of them

Raises
------
ValueError
If metric_frame is not a MetricFrame or doesn't have confidence intervals
"""
# Validate inputs
if not isinstance(metric_frame, MetricFrame):
raise ValueError(_METRIC_FRAME_INVALID_ERROR)

if metric_frame.ci_quantiles is None or len(metric_frame.ci_quantiles) == 0:
raise ValueError(_METRIC_FRAME_NO_CI_ERROR)

if not (isinstance(metrics, list) or isinstance(metrics, str) or metrics is None):
raise ValueError(_METRICS_NOT_LIST_OR_STR_ERROR.format(type(metrics)))

# Convert metrics to list
metrics = [metrics] if isinstance(metrics, str) else metrics

# Get the main data
df = metric_frame.by_group

# Filter to scalar metrics if none specified
if metrics is None:
metrics = []
for metric in list(df.columns):
# Check if the first value is scalar (not array-like)
first_val = df[metric].iloc[0]
if np.isscalar(first_val):
metrics.append(metric)

if len(metrics) == 0:
raise ValueError("No scalar metrics found to plot")

# Validate ci_quantile_index
n_quantiles = len(metric_frame.ci_quantiles)
if n_quantiles < 2:
raise ValueError("Need at least 2 quantiles for confidence intervals")

if ci_quantile_index < 0 or ci_quantile_index >= n_quantiles - 1:
raise ValueError(_CI_QUANTILE_INDEX_ERROR.format(n_quantiles - 2))

# Get confidence intervals
by_group_ci = metric_frame.by_group_ci
lower_ci = by_group_ci[ci_quantile_index]
upper_ci = by_group_ci[ci_quantile_index + 1]

# Calculate error bars (distance from mean to bounds)
yerr_data = {}
for metric in metrics:
if metric in df.columns:
mean_vals = df[metric]
lower_vals = lower_ci[metric] if metric in lower_ci.columns else mean_vals
upper_vals = upper_ci[metric] if metric in upper_ci.columns else mean_vals

# Error bars: [distance_to_lower, distance_to_upper]
yerr_data[metric] = [
(mean_vals - lower_vals).values,
(upper_vals - mean_vals).values
]

# Create the plot
if kind == "point":
axs = df[metrics].plot(
linestyle="", marker="o",
yerr=[np.array([yerr_data[m] for m in metrics]).T] if len(metrics) > 1
else yerr_data[metrics[0]],
subplots=subplots,
**kwargs
)
else:
axs = df[metrics].plot(
kind=kind,
yerr=[np.array([yerr_data[m] for m in metrics]).T] if len(metrics) > 1
else yerr_data[metrics[0]],
subplots=subplots,
**kwargs
)

# Add legend for confidence intervals
if isinstance(axs, np.ndarray):
for ax in axs.flatten():
_add_ci_legend(ax, kind, metric_frame.ci_quantiles, ci_quantile_index)
else:
_add_ci_legend(axs, kind, metric_frame.ci_quantiles, ci_quantile_index)

return axs


def _add_ci_legend(ax, kind, ci_quantiles, ci_quantile_index):
"""Add confidence interval legend to axis."""
color = ax.lines[0].get_color() if kind == "point" else "black"

# Create legend label
lower_q = ci_quantiles[ci_quantile_index]
upper_q = ci_quantiles[ci_quantile_index + 1]
ci_level = int((upper_q - lower_q) * 100)
legend_label = f"{ci_level}% Confidence Interval"

# Extend legend
handles, labels = ax.get_legend_handles_labels()
custom_line = [Line2D([0], [0], color=color, label=legend_label)]
handles.extend(custom_line)
ax.legend(handles=handles)
3 changes: 2 additions & 1 deletion fairlearn/preprocessing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""Preprocessing tools to help deal with sensitive attributes."""

from ._correlation_remover import CorrelationRemover
from ._optimized_preprocessing import OptimizedPreprocessor
from ._prototype_representation_learner import PrototypeRepresentationLearner

__all__ = ["CorrelationRemover", "PrototypeRepresentationLearner"]
__all__ = ["CorrelationRemover", "OptimizedPreprocessor", "PrototypeRepresentationLearner"]
31 changes: 31 additions & 0 deletions fairlearn/preprocessing/_learning_fair_representation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import numpy as np
from fairlearn.preprocessing import BasePreprocessor

class LearningFairRepresentation(BasePreprocessor):
def __init__(self, k=5, Ax=0.01, Az=50.0, Azx=0.1, max_iter=500):
super().__init__()
self.k = k
self.Ax = Ax
self.Az = Az
self.Azx = Azx
self.max_iter = max_iter
self.W = None

def fit(self, X, y, sensitive_features):
X = np.array(X)
y = np.array(y)
sensitive_features = np.array(sensitive_features)

n_samples, n_features = X.shape
self.W = np.random.randn(n_features, self.k) * 0.01

for _ in range(self.max_iter):
Z = X @ self.W
grad = self.Ax * X.T @ (X @ self.W - Z) + self.Az * self.W + self.Azx * X.T @ sensitive_features.reshape(-1, 1)
self.W -= 0.01 * grad

return self

def transform(self, X):
X = np.array(X)
return X @ self.W
Loading