feat(aggregators): add general aggregator for evaluation reports - #230
feat(aggregators): add general aggregator for evaluation reports#230venkatkrish543re wants to merge 4 commits into
Conversation
Adds a general aggregator over EvaluationReport lists. Groups results by (case_key, evaluator_name), filters corrupted trials, computes descriptive stats and optional efficiency rollup, and runs paired statistics (Wilcoxon, paired-t, McNemar) when exactly two conditions are detected with trial_idx pairing. LLM summary is optional via a configurable system_prompt. JSON serialization and terminal display included.
… rollup, raw_values, trajectory_pointers, and corruption filtering from the base. Exposed _group_key / _filter_entry / _build_result as overridable hooks so project-specific behavior can layer on top without reimplementing core grouping.
|
Issue: The PR description links to issue #114 (Chaos/Resiliency Evaluation) which appears unrelated to aggregation functionality. This makes it harder to trace the motivation and requirements for this feature. Suggestion: Please link to the correct issue that tracks the aggregator feature, or create one if it doesn't exist. This helps reviewers understand the acceptance criteria and future readers understand the context. |
|
Issue: This PR introduces a new public API (
The PR also lacks the Suggestion:
from strands_evals.aggregators import Aggregator, AggregationReport
agg = Aggregator(name="my-experiment")
report: AggregationReport = agg.aggregate(evaluation_reports)
report.run_display()
report.to_file("results.json")
|
Review SummaryAssessment: Request Changes The aggregator concept is well-designed with clean extension points ( Review Categories
The extensibility design with the three hooks is thoughtful and the test coverage is solid for the current scope. |
…hods on EvaluationReport
2bfaea1 to
3dddc9d
Compare
…e aggregation package
| _BASE_COLUMNS = [CASE, EVALUATOR, SCORE, PASSED, REASON] | ||
|
|
||
|
|
||
| def _case_key(case: dict, index: int) -> str: |
There was a problem hiding this comment.
Let's tuck these 2 private methods into the class where they're called.
| @@ -0,0 +1,370 @@ | |||
| """Tests for the pandas-style surface on ``EvaluationReport``. | |||
|
|
|||
| Consolidates what were previously test_coerce / test_frame / test_mixin, now | |||
There was a problem hiding this comment.
Remove this legacy slop
| """Group rows by one or more columns. Mirrors ``DataFrame.groupby``.""" | ||
| return ReportGroupBy(self._df.groupby(by, **kwargs)) | ||
|
|
||
| def filter( |
There was a problem hiding this comment.
All the helper methods — filter, describe, display, to_file/from_file, to_pandas, the __getattr__ passthrough, __len__, __repr__ — are nice to have that mirrors Pandas dataframe indeed. But these speculative API surfaces are not really consumed by EvaluationReport or any other class in the codebase. I worry that they only add dead weights to the codebase.
| @@ -0,0 +1,370 @@ | |||
| """Tests for the pandas-style surface on ``EvaluationReport``. | |||
There was a problem hiding this comment.
Add these tests to test_evaluation_report.py instead of creating this new test file.
| result = self._groupby.agg(*args, **kwargs) | ||
| return ReportFrame(result.reset_index()) | ||
|
|
||
| def __getattr__(self, name: str) -> Any: |
There was a problem hiding this comment.
This method is not needed either. If a user genuinely needs raw pandas, they do report.frame().df.groupby(...)
| """ | ||
| return self.frame().group_by(by, **kwargs) | ||
|
|
||
| def agg(self, *args: Any, **kwargs: Any) -> ReportFrame: |
There was a problem hiding this comment.
Ideally, .agg should still return a EvaluationReport, not a ReportFrame, so the users can directly
aggregated = report.group_by("evaluator").agg(
mean_score=("score", "mean"),
pass_rate=("passed", "mean"),
)
aggregated.display() # ← same CollapsibleTableReportDisplay experience
and so you don't need to implement display for ReportFrame either.
The intention is all the pandas df should be hiding beneath EvaluationReport, while its interface remains the same except newly supported .group_by().agg().
Here's the shape (Delete ReportFrame entirely):
class EvaluationReport(BaseModel):
def group_by(self, by: str | list[str]) -> "ReportGroupBy":
"""Group cases. Returns an intermediate that resolves back to EvaluationReport."""
df = self._to_dataframe() # private — hidden from users
return ReportGroupBy(df.groupby(by))
class ReportGroupBy:
"""Internal. Users only see .agg() on this."""
def __init__(self, groupby):
self._groupby = groupby
def agg(self, **kwargs) -> "EvaluationReport":
"""Aggregate and return a new EvaluationReport."""
result_df = self._groupby.agg(**kwargs).reset_index()
# Convert aggregated rows back into EvaluationReport's structure
return _dataframe_to_report(result_df)
def _dataframe_to_report(df: pd.DataFrame) -> EvaluationReport:
"""Convert an aggregated DataFrame back into an EvaluationReport."""
cases = []
scores = []
test_passes = []
reasons = []
for _, row in df.iterrows():
case = {"name": str(row.get("case", row.get("evaluator", "")))}
# Stuff remaining columns into metadata
case["metadata"] = {
k: v for k, v in row.items()
if k not in ("case", "evaluator", "score", "passed", "reason")
}
cases.append(case)
scores.append(float(row.get("score", row.get("mean_score", 0.0))))
test_passes.append(bool(row.get("passed", row.get("pass_rate", 0) >= 0.5)))
reasons.append(str(row.get("reason", "")))
overall = sum(scores) / len(scores) if scores else 0.0
return EvaluationReport(
evaluator_name="Aggregated",
overall_score=overall,
scores=scores,
cases=cases,
test_passes=test_passes,
reasons=reasons,
)
|
Closing per working group discussion. With #241 returning a single flattened report, grouping and aggregation are straightforward with pandas directly on the report, So a dedicated aggregator in the library is not needed |
Description
Adds a general aggregator over EvaluationReport lists. Groups results by (case_key, evaluator_name), computes descriptive stats. The three overridable hooks(_group_key, _filter_entry, _build_result) for project specific subclasses
LLM summary is optional via a configurable system_prompt. JSON serialization and terminal display included.
Related Issues
#114
Documentation PR
Type of Change
New feature
Testing
How have you tested the change? Verify that the changes do not break functionality or introduce warnings in consuming repositories: agents-docs, agent-tools, agents-cli
hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.