fix simulation reporting and research resilience
This commit is contained in:
@@ -17,12 +17,16 @@ METRIC_LABELS = {
|
||||
"purchase_intent": "Purchase Intent",
|
||||
"word_of_mouth": "Word of Mouth",
|
||||
"churn_risk": "Churn Risk",
|
||||
"adoption_rate": "Adoption Rate",
|
||||
}
|
||||
"adoption_rate": "Adoption Rate",
|
||||
"information_spread": "Information Spread",
|
||||
"echo_chamber_index": "Echo Chamber Index",
|
||||
"rumor_distortion": "Rumor Distortion",
|
||||
}
|
||||
|
||||
INVERSE_METRICS = {"conflict", "churn_risk"}
|
||||
|
||||
MARKET_METRICS = {"brand_sentiment", "purchase_intent", "word_of_mouth", "churn_risk", "adoption_rate"}
|
||||
MARKET_METRICS = {"brand_sentiment", "purchase_intent", "word_of_mouth", "churn_risk", "adoption_rate"}
|
||||
INFORMATION_METRICS = {"information_spread", "echo_chamber_index", "rumor_distortion"}
|
||||
|
||||
SOCIAL_METRICS = {"stability", "prosperity", "trust", "freedom", "conflict"}
|
||||
|
||||
@@ -56,9 +60,11 @@ class ReportAnalyzer:
|
||||
if not metrics_history:
|
||||
return []
|
||||
|
||||
active_keys = set(SOCIAL_METRICS)
|
||||
if is_market:
|
||||
active_keys |= MARKET_METRICS
|
||||
# Metrics are persisted as a complete WorldMetrics snapshot. Keep the
|
||||
# report scorecard complete as well; hiding market or information
|
||||
# fields based on the scenario made readers mistake omitted fields for
|
||||
# missing data.
|
||||
active_keys = set(METRIC_LABELS)
|
||||
|
||||
results = []
|
||||
for key in active_keys:
|
||||
@@ -86,7 +92,91 @@ class ReportAnalyzer:
|
||||
"inverse": inverse,
|
||||
})
|
||||
|
||||
return sorted(results, key=lambda x: x["metric"])
|
||||
return sorted(results, key=lambda x: x["metric"])
|
||||
|
||||
@staticmethod
|
||||
def detect_data_gaps(
|
||||
metrics_history: list[dict],
|
||||
actions: list[ActionEntry],
|
||||
is_market: bool,
|
||||
tension_event_count: int = 0,
|
||||
fallback_count: int = 0,
|
||||
) -> list[dict]:
|
||||
"""Find deterministic limitations worth surfacing in a report.
|
||||
|
||||
These are observations about this run, not claims that the simulator
|
||||
is universally incapable of producing the signal. LLM prose can then
|
||||
explain the finding without inventing a missing data source.
|
||||
"""
|
||||
gaps: list[dict] = []
|
||||
if not metrics_history:
|
||||
return [{
|
||||
"key": "metrics_history",
|
||||
"gap": "No metric history was recorded.",
|
||||
"why_it_matters": "The report cannot establish trends or inflection points.",
|
||||
"suggested_fix": "Check persistence and rerun the simulation.",
|
||||
}]
|
||||
|
||||
action_types = {a.action_type.value for a in actions}
|
||||
values_by_key = {
|
||||
key: [float(row.get(key, 0.5)) for row in metrics_history]
|
||||
for key in METRIC_LABELS
|
||||
}
|
||||
|
||||
def add(key: str, gap: str, why: str, fix: str) -> None:
|
||||
gaps.append({"key": key, "gap": gap, "why_it_matters": why, "suggested_fix": fix})
|
||||
|
||||
if fallback_count > 0:
|
||||
add(
|
||||
"citizen_generation",
|
||||
f"{fallback_count} citizen(s) entered through degraded generation.",
|
||||
"Their initial identity fields may be less representative than normally generated citizens.",
|
||||
"Retry cast generation and inspect the affected residents before treating their behavior as evidence.",
|
||||
)
|
||||
|
||||
if is_market and all(abs(v) < 1e-9 for v in values_by_key["adoption_rate"]):
|
||||
add(
|
||||
"adoption_rate",
|
||||
"Adoption rate stayed at 0.00 for the whole run.",
|
||||
"The report cannot infer an adoption trajectory from this simulation.",
|
||||
"Increase the horizon or verify that purchase/proposal actions can occur.",
|
||||
)
|
||||
if "DEFECT" not in action_types and "ABANDON" not in action_types:
|
||||
add(
|
||||
"defection_or_abandonment",
|
||||
"No DEFECT or ABANDON action occurred.",
|
||||
"Churn, defection, and trust-loss conclusions are weakly evidenced.",
|
||||
"Run a longer or more disruptive scenario, or report this as a limitation.",
|
||||
)
|
||||
if "PROPOSE_RULE" not in action_types and not is_market:
|
||||
add(
|
||||
"rule_evolution",
|
||||
"No PROPOSE_RULE action occurred.",
|
||||
"Institutional or rule-evolution claims are not supported by observed actions.",
|
||||
"Include a rule-change pressure or extend the simulation horizon.",
|
||||
)
|
||||
if tension_event_count > 0:
|
||||
add(
|
||||
"tension_events",
|
||||
f"{tension_event_count} tension event(s) occurred but are not represented as a first-class report section.",
|
||||
"Readers may miss event-driven causes of later metric changes.",
|
||||
"Include event descriptions and affected days in the report timeline.",
|
||||
)
|
||||
|
||||
flat_keys = ["trust", "conflict", "information_spread"]
|
||||
if is_market:
|
||||
flat_keys.append("purchase_intent")
|
||||
for key in flat_keys:
|
||||
values = values_by_key[key]
|
||||
if len(values) >= 3 and max(values) - min(values) < 0.005:
|
||||
add(
|
||||
key,
|
||||
f"{METRIC_LABELS[key]} stayed effectively flat ({values[0]:.2f} → {values[-1]:.2f}).",
|
||||
"A flat series does not show whether the mechanism is stable or simply inactive at this horizon.",
|
||||
"Inspect the triggering actions and compare with a longer or counterfactual run.",
|
||||
)
|
||||
|
||||
return gaps
|
||||
|
||||
@staticmethod
|
||||
def compute_agent_stats(agents: list[AgentPersona], actions: list[ActionEntry]) -> list[dict]:
|
||||
|
||||
Reference in New Issue
Block a user