diff --git a/assets/profiler-ui.js b/assets/profiler-ui.js
index 881318f..8da8ca3 100644
--- a/assets/profiler-ui.js
+++ b/assets/profiler-ui.js
@@ -497,10 +497,26 @@
'
' + g.count.toLocaleString() + ' | ' +
' | ';
}).join('');
+
+ var referenceHtml = '';
+ if (d.reference) {
+ var refRows = d.reference.groups.slice(0, DISPLAY_GROUPS).map(function (g) {
+ return '| ' + esc(g.label) + ' | ' +
+ '' + (g.expected * 100).toFixed(1) + '% | ' +
+ '' + (g.actual * 100).toFixed(1) + '% | ' +
+ '' + (g.delta > 0 ? '+' : '') + (g.delta * 100).toFixed(1) + ' pp |
';
+ }).join('');
+ referenceHtml = 'Reference ' +
+ 'deviation ' + (d.reference.deviation * 100).toFixed(1) + '%
' +
+ '
| Expected | ' +
+ 'Actual | Delta |
' +
+ refRows + '
';
+ }
+
return '' + esc(d.name) +
' ' + esc(d.kind) + ' ' +
'' + d.dimension_score + '/100
' +
- '';
+ '' + referenceHtml + '';
}).join('');
var flagHtml = '';
@@ -530,6 +546,10 @@
' tr.under td.bar span { background:var(--accent); }\n' +
' tr.under td:first-child::after { content:\' (under-represented)\'; color:var(--accent); font-size:11px; }\n' +
' tr.small-group td:first-child::before { content:\'⚠ small group \'; color:var(--accent); }\n' +
+ ' .reference { margin-top:10px; padding-top:10px; border-top:1px dashed var(--border); }\n' +
+ ' .reference h3 { font-size:.75em; margin:0 0 6px; }\n' +
+ ' .reference th { text-align:right; font-size:11px; color:var(--muted); font-weight:normal; }\n' +
+ ' .reference th:first-child { text-align:left; }\n' +
' .flags ul { list-style:none; padding:0; }\n' +
' .flags li { background:#fbeae3; border-left:3px solid var(--accent); padding:8px 12px; margin:6px 0; border-radius:0 4px 4px 0; }\n' +
' .head { border-bottom:2px solid var(--accent); padding-bottom:12px; }\n' +
diff --git a/faircode/report.py b/faircode/report.py
index 0001326..086b325 100644
--- a/faircode/report.py
+++ b/faircode/report.py
@@ -197,10 +197,24 @@ def esc(s) -> str:
f'{ref_rows}'
)
+ meta_parts = []
+ if d["imbalance_ratio"] is not None:
+ meta_parts.append(f"imbalance {d['imbalance_ratio']:.1f}x")
+ elif d["n_groups"] > 1:
+ meta_parts.append("imbalance inf (empty subgroup)")
+ if d["missing_pct"] > 0:
+ meta_parts.append(f"missing {d['missing_pct'] * 100:.1f}%")
+ if d["skewness"] is not None:
+ meta_parts.append(f"skew {d['skewness']:+.2f}")
+ meta_html = (
+ f' ({esc(" ".join(meta_parts))})'
+ if meta_parts else ""
+ )
+
dim_blocks.append(
f'{esc(d["name"])} '
f'{esc(d["kind"])} '
- f'{d["dimension_score"]}/100
'
+ f'{d["dimension_score"]}/100{meta_html}'
f'Group breakdown - {esc(d["name"])}'
f'| Group | Share | '
f'95% CI | Count | '
@@ -238,6 +252,7 @@ def esc(s) -> str:
h1 {{ font-family:Georgia,serif; }}
.score {{ color:var(--accent3); font-size:.7em; font-weight:600; }}
.kind {{ color:var(--muted); font-size:.6em; text-transform:uppercase; letter-spacing:.08em; }}
+ .meta {{ color:var(--muted); font-size:.6em; }}
.dim {{ background:var(--surface); border:1px solid var(--border); border-radius:8px;
padding:16px 20px; margin:16px 0; }}
table {{ width:100%; border-collapse:collapse; }}
diff --git a/tests/test_report.py b/tests/test_report.py
index 9fc70cd..e158271 100644
--- a/tests/test_report.py
+++ b/tests/test_report.py
@@ -86,6 +86,9 @@ def test_to_html_renders_key_figures(mock_profile_result):
assert "88/100" in html_out
assert "B+" in html_out
assert "Gender" in html_out
+ assert "imbalance 1.6x" in html_out
+ assert "missing 2.0%" in html_out
+ assert "skew +0.30" in html_out
def test_to_html_renders_proxy_hints(mock_profile_result):
|---|