Skip to content

Commit cf2f9ce

Browse files
Feat:Deployment Recommendation Explanation Feature (#114)
* feat:Implemented Deployment Recommendation Explanation feature * Changelog:Added a changelog entry for Implemented feature (Deployment Recommendation Explanation feature) * fixed the code formatting issues and pre commit hooks * added example usage in the examples/quickstart.py file * Update trustlens/report.py Co-authored-by: llamapreview[bot] <184758061+llamapreview[bot]@users.noreply.github.com> * Update trustlens/report.py Co-authored-by: llamapreview[bot] <184758061+llamapreview[bot]@users.noreply.github.com> * Fixed the formatting issues caused by commiting changes recommended by llama --------- Co-authored-by: llamapreview[bot] <184758061+llamapreview[bot]@users.noreply.github.com>
1 parent 78e37d8 commit cf2f9ce

4 files changed

Lines changed: 247 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313
- **Model Zoo Benchmark**: Introduced a comprehensive scientific validation notebook (`examples/trustlens_model_zoo_benchmark.ipynb`) that systematically evaluates TrustLens across 6 model architectures and multiple data corruption scenarios with statistical aggregation.
14+
- **Deployment Recommendation Explanations**: Added `TrustReport.deployment_explanation` and `TrustReport.deployment_summary` to provide structured deployment verdict explanations, identify primary risks, and surface actionable recommendations based on Trust Score penalties and sub-scores.
1415

1516
### Fixed
1617
- Fixed incorrect `top_mistake_indices` in `misclassification_summary()` to return **global dataset indices** instead of local filtered subset positions, improving downstream EDA and debugging workflows for high-confidence model errors. (PR #104) Thanks @dicnunz 🙌

‎examples/quickstart.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def main():
9797
print("\n[4/4] Report Summary:")
9898
report.show()
9999

100+
print("\n" + "=" * 40)
101+
print("DEPLOYMENT RECOMMENDATION EXPLANATION")
102+
print("=" * 40)
103+
print(report.deployment_summary)
104+
print("=" * 40 + "\n")
105+
100106
# Visualize fairness deep-dive
101107
print("\n[*] Generating fairness summary plot...")
102108
report.plot_bias(mode="summary", save_path="examples/output/quickstart_bias.png")
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import numpy as np
2+
import pytest
3+
4+
from trustlens.report import TrustReport
5+
from trustlens.trust_score import TrustScoreResult
6+
7+
8+
@pytest.fixture
9+
def mock_report():
10+
"""Create a minimal TrustReport with empty metrics to inject test states."""
11+
y_true = np.array([0, 1, 0, 1])
12+
y_pred = np.array([0, 1, 0, 1])
13+
y_prob = np.array([[0.9, 0.1], [0.1, 0.9], [0.9, 0.1], [0.1, 0.9]])
14+
15+
return TrustReport(
16+
results={}, model=None, X=np.zeros((4, 2)), y_true=y_true, y_pred=y_pred, y_prob=y_prob
17+
)
18+
19+
20+
def test_deployment_explanation_pass(mock_report):
21+
# Mock an "A" grade TrustScoreResult with no penalties
22+
mock_report.trust_score = TrustScoreResult(
23+
score=95,
24+
grade="A",
25+
verdict="High Trust",
26+
sub_scores={"calibration": 90.0, "failure": 95.0, "bias": 95.0},
27+
penalties_applied={},
28+
is_blocked=False,
29+
)
30+
31+
exp = mock_report.deployment_explanation
32+
33+
assert exp["verdict"] == "PASS"
34+
assert len(exp["recommendations"]) == 1
35+
assert "meets all trustworthiness criteria" in exp["recommendations"][0]
36+
37+
# All reasons should be pass
38+
for r in exp["reasons"]:
39+
assert r["status"] == "pass"
40+
assert "assessment completed" in r["message"]
41+
42+
assert exp["primary_risk"] is not None
43+
assert exp["primary_risk"]["metric"] in ["Calibration", "Failure", "Fairness"]
44+
45+
46+
def test_deployment_explanation_caution_with_multiple_penalties(mock_report):
47+
mock_report.trust_score = TrustScoreResult(
48+
score=65,
49+
grade="B",
50+
verdict="Good Trust",
51+
sub_scores={"calibration": 80.0, "failure": 75.0, "bias": 70.0},
52+
penalties_applied={"Fairness": 10.0, "Calibration": 5.0},
53+
is_blocked=False,
54+
)
55+
56+
exp = mock_report.deployment_explanation
57+
58+
assert exp["verdict"] == "CAUTION"
59+
assert len(exp["recommendations"]) == 2
60+
61+
# Check reasons
62+
fail_reasons = [r for r in exp["reasons"] if r["status"] == "fail"]
63+
assert len(fail_reasons) == 2
64+
assert any("Fairness penalty applied" in r["message"] for r in fail_reasons)
65+
assert any("Calibration penalty applied" in r["message"] for r in fail_reasons)
66+
67+
pass_reasons = [r for r in exp["reasons"] if r["status"] == "pass"]
68+
assert len(pass_reasons) == 1
69+
assert "Failure assessment completed" in pass_reasons[0]["message"]
70+
71+
# Primary risk must be the one with the highest penalty (Fairness)
72+
assert exp["primary_risk"]["metric"] == "Fairness"
73+
assert exp["primary_risk"]["value"] == 10.0
74+
75+
76+
def test_deployment_explanation_block(mock_report):
77+
mock_report.trust_score = TrustScoreResult(
78+
score=20,
79+
grade="D",
80+
verdict="Low Trust",
81+
sub_scores={"calibration": 20.0, "failure": 30.0},
82+
penalties_applied={"Failure": 15.0},
83+
is_blocked=True,
84+
)
85+
86+
exp = mock_report.deployment_explanation
87+
assert exp["verdict"] == "BLOCK"
88+
assert exp["primary_risk"]["metric"] == "Failure"
89+
assert len(exp["recommendations"]) == 1
90+
assert "Inspect high-confidence misclassifications" in exp["recommendations"][0]
91+
92+
93+
def test_deployment_explanation_missing_modules(mock_report):
94+
# What if only calibration was evaluated?
95+
mock_report.trust_score = TrustScoreResult(
96+
score=55,
97+
grade="C",
98+
verdict="Moderate Trust",
99+
sub_scores={"calibration": 55.0},
100+
penalties_applied={"Calibration": 12.0},
101+
is_blocked=False,
102+
)
103+
104+
exp = mock_report.deployment_explanation
105+
assert len(exp["reasons"]) == 1
106+
assert exp["reasons"][0]["message"] == "Calibration penalty applied"
107+
assert len(exp["recommendations"]) == 1
108+
109+
110+
def test_deployment_summary_formatting(mock_report):
111+
mock_report.trust_score = TrustScoreResult(
112+
score=75,
113+
grade="C",
114+
verdict="Moderate Trust",
115+
sub_scores={"calibration": 90.0, "bias": 60.0},
116+
penalties_applied={"Fairness": 15.0},
117+
is_blocked=False,
118+
)
119+
120+
summary = mock_report.deployment_summary
121+
assert "Deployment Verdict: CAUTION" in summary
122+
assert "✓ Calibration assessment completed" in summary
123+
assert "✗ Fairness penalty applied" in summary
124+
assert "Primary Risk:\nFairness" in summary
125+
assert "• Investigate subgroup performance disparities" in summary
126+
127+
128+
def test_serialization_includes_explanation(mock_report):
129+
mock_report.trust_score = TrustScoreResult(
130+
score=95,
131+
grade="A",
132+
verdict="High Trust",
133+
sub_scores={"calibration": 90.0},
134+
penalties_applied={},
135+
is_blocked=False,
136+
)
137+
138+
flat_dict = mock_report.to_dict()
139+
assert "deployment_verdict" in flat_dict
140+
assert flat_dict["deployment_verdict"] == "PASS"
141+
assert "deployment_primary_risk_metric" in flat_dict

‎trustlens/report.py‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,93 @@ def _format_score_explanation(self) -> list[str]:
125125
lines.append(f" - {label:<16}: {name} (-{val:.1f})")
126126
return lines
127127

128+
@property
129+
def deployment_explanation(self) -> dict[str, Any]:
130+
"""Provide a structured explanation for the deployment verdict."""
131+
ts = self.trust_score
132+
grade_map = {"A": "PASS", "B": "CAUTION", "C": "CAUTION", "D": "BLOCK"}
133+
verdict = "BLOCK" if ts.is_blocked else grade_map.get(ts.grade, "PASS")
134+
135+
reasons = []
136+
recommendations = []
137+
penalties = getattr(ts, "penalties_applied", {})
138+
139+
# Recommendations mapping
140+
rec_map = {
141+
"calibration": "Consider probability calibration techniques such as temperature scaling or isotonic regression before deployment.",
142+
"fairness": "Investigate subgroup performance disparities and consider fairness constraints before deployment.",
143+
"failure": "Inspect high-confidence misclassifications and verify confidence-weighted error distributions.",
144+
"representation": "Review latent embedding quality; poor representation may indicate failure to capture fundamental class separation.",
145+
}
146+
147+
# Map dimension to standard penalty name logic
148+
dim_to_penalty_key = {
149+
"calibration": "Calibration",
150+
"failure": "Failure",
151+
"bias": "Fairness",
152+
"representation": "Representation",
153+
}
154+
155+
# Case-insensitive penalty key matching
156+
penalties_lower = {k.lower(): (k, v) for k, v in penalties.items()}
157+
158+
for dim in self.trust_score.sub_scores.keys():
159+
expected_penalty_key = dim_to_penalty_key.get(dim, dim.title())
160+
161+
if expected_penalty_key.lower() in penalties_lower:
162+
actual_key, _ = penalties_lower[expected_penalty_key.lower()]
163+
reasons.append({"status": "fail", "message": f"{actual_key} penalty applied"})
164+
# Map back to our recommendation keys
165+
rec_key = "fairness" if dim == "bias" else dim
166+
if rec_key in rec_map:
167+
recommendations.append(rec_map[rec_key])
168+
else:
169+
reasons.append(
170+
{"status": "pass", "message": f"{expected_penalty_key} assessment completed"}
171+
)
172+
173+
if not recommendations:
174+
recommendations.append("Model meets all trustworthiness criteria for deployment.")
175+
176+
# Determine primary risk
177+
primary_risk = None
178+
if penalties:
179+
# Largest penalty
180+
matched_key = max(penalties.items(), key=lambda x: x[1])[0]
181+
primary_risk = {"metric": matched_key, "value": penalties[matched_key]}
182+
elif ts.sub_scores:
183+
# Lowest sub-score
184+
lowest_dim = min(ts.sub_scores.items(), key=lambda x: x[1])[0]
185+
primary_risk = {
186+
"metric": dim_to_penalty_key.get(lowest_dim, lowest_dim.title()),
187+
"value": ts.sub_scores[lowest_dim],
188+
}
189+
190+
return {
191+
"verdict": verdict,
192+
"reasons": reasons,
193+
"primary_risk": primary_risk,
194+
"recommendations": recommendations,
195+
}
196+
197+
@property
198+
def deployment_summary(self) -> str:
199+
"""Format the deployment explanation into a human-readable string."""
200+
exp = self.deployment_explanation
201+
lines = [f"Deployment Verdict: {exp['verdict']}", "", "Reasons:"]
202+
for reason in exp["reasons"]:
203+
icon = "✗" if reason["status"] == "fail" else "✓"
204+
lines.append(f"{icon} {reason['message']}")
205+
206+
if exp["primary_risk"]:
207+
lines.extend(["", "Primary Risk:", str(exp["primary_risk"]["metric"])])
208+
209+
lines.extend(["", "Recommendations:"])
210+
for rec in exp["recommendations"]:
211+
lines.append(f"• {rec}")
212+
213+
return "\n".join(lines)
214+
128215
# ------------------------------------------------------------------
129216
# Internal helpers
130217
# ------------------------------------------------------------------
@@ -1242,6 +1329,7 @@ def save(self, path: str = "trust_report", **kwargs) -> Path:
12421329
"trust_score": self.trust_score.score,
12431330
"grade": self.trust_score.grade,
12441331
"sub_scores": self.trust_score.sub_scores,
1332+
"deployment_explanation": self.deployment_explanation,
12451333
}
12461334
p.write_text(json.dumps(data, indent=2), encoding="utf-8")
12471335
logger.info("Unified Report JSON saved to: %s", p)
@@ -1281,6 +1369,7 @@ def save(self, path: str = "trust_report", **kwargs) -> Path:
12811369
"sub_scores": ts.sub_scores,
12821370
"weights_used": ts.weights_used,
12831371
"breakdown": ts.breakdown,
1372+
"deployment_explanation": self.deployment_explanation,
12841373
},
12851374
indent=2,
12861375
),
@@ -1328,6 +1417,16 @@ def to_dict(self) -> dict[str, Any]:
13281417
flat["framework"] = self.metadata.get("framework", "unknown")
13291418
flat["trustlens_version"] = self.metadata["trustlens_version"]
13301419

1420+
# Flatten deployment explanation for MLflow/W&B tracking
1421+
exp = self.deployment_explanation
1422+
flat["deployment_verdict"] = exp["verdict"]
1423+
flat["deployment_primary_risk_metric"] = (
1424+
exp["primary_risk"].get("metric") if exp["primary_risk"] else None
1425+
)
1426+
flat["deployment_primary_risk_value"] = (
1427+
exp["primary_risk"].get("value") if exp["primary_risk"] else None
1428+
)
1429+
13311430
for dim, score in self.trust_score.sub_scores.items():
13321431
flat[f"trust_{dim}_score"] = score
13331432
return flat

0 commit comments

Comments
 (0)