Skip to content

Export and style results

Model Auditor result objects support four distinct reporting needs:

  1. exact numeric access through nested dataclasses;
  2. unrounded long-form exports through to_numeric_dataframe();
  3. compact display tables through to_dataframe(); and
  4. optional relative notebook coloring through style_dataframe().

Use the representation that matches the next consumer.

For calculations, select the LevelMetric object:

metric = (
results.features["region"]
.levels["North"]
.metrics["sensitivity"]
)
score = float(metric.score)
lower, upper = metric.interval

This path preserves numeric types.

The hierarchy is:

ScoreEvaluation
→ FeatureEvaluation
→ LevelEvaluation
→ LevelMetric

See Schemas and result objects for every field.

For analysis and durable export, start with:

numeric_long = results.to_numeric_dataframe()
metadata = numeric_long.attrs["metadata"]

Each row contains stable score, feature, level, and metric identifiers plus the estimate, bounds, support, interval diagnostics, direction, parameters, and excluded-row count.

table = results.to_dataframe(
n_decimals=3,
metric_labels=False,
)

Default columns use stable metric names such as sensitivity, auroc, and n_pos.

Use display labels when preparing a human-facing table:

labeled_table = results.to_dataframe(
n_decimals=2,
metric_labels=True,
)

Cells are formatted strings. A CI-bearing metric appears as:

0.81 (0.74, 0.87)

A count appears with integer formatting. Do not feed this table directly into numeric aggregation without parsing it.

region_table = results.features["region"].to_dataframe(
metric_labels=True,
)
north_table = (
results.features["region"]
.levels["North"]
.to_dataframe(metric_labels=True)
)

FeatureEvaluation.to_dataframe() uses feature levels as rows. LevelEvaluation.to_dataframe() creates one row.

The add_index option can add a feature or score label as an outer index at the feature and score scopes. The level method retains an add_index argument for API consistency but does not use it in v0.1.16.

Use a categorical dtype before add_data():

data["severity"] = pd.Categorical(
data["severity"],
categories=["Low", "Moderate", "High", "Critical"],
ordered=True,
)

All declared categories appear in performance and error tables in that order. An unobserved category remains as an explicit NaN row.

display(
results.features["region"].style_dataframe(
n_decimals=3,
metric_labels=True,
rank=True,
)
)

Styling is neutral by default. With rank=True, eligible metric columns are divided into relative thirds:

  • high tier: #d4edda;
  • medium tier: #fff3cd;
  • low tier: #f8d7da.

FPR and FNR are automatically inverted so lower values receive the high tier. Count metrics are not styled unless requested:

display(
results.style_dataframe(
metric_labels=True,
include_count_metrics=True,
)
)

Custom colors are supported:

styled = results.style_dataframe(
low_color="#fde2e2",
medium_color="#fff4c2",
high_color="#d9f2df",
)

Metric direction controls ranking. Counts, prevalence, calibration intercept and slope, and enrichment odds ratios receive no performance coloring.

error_table = error_results.to_dataframe(metric_labels=False)

ErrorEvaluation.to_dataframe() is numeric, not display-formatted. It returns MultiIndex columns for support, percentages, odds ratios, and CI bounds. Its n_decimals argument is retained for compatibility but does not round the numeric output.

For a compact notebook view:

display(
error_results.style_dataframe(
n_decimals=3,
metric_labels=True,
)
)

The styled error view folds CI bounds into the odds-ratio display and colors only the odds-ratio columns.

Use the display table for presentation-oriented CSV output:

labeled_table.to_csv("subgroup-performance-display.csv")

Use explicitly extracted numeric records for analysis. One practical pattern is:

records = []
for feature_name, feature in results.features.items():
for level_name, level in feature.levels.items():
for metric_name, metric in level.metrics.items():
records.append(
{
"feature": feature_name,
"level": level_name,
"metric": metric_name,
"score": metric.score,
"ci_lower": None if metric.interval is None else metric.interval[0],
"ci_upper": None if metric.interval is None else metric.interval[1],
}
)
numeric_long = pd.DataFrame.from_records(records)
numeric_long.to_parquet("subgroup-performance.parquet", index=False)

This avoids parsing formatted strings and gives each metric one long-form row.