///|
pub(all) struct FeatureImpact {
feature : String
transitions : Int
outcome_flips : Int
flips_to_true : Int
flips_to_false : Int
unknown_transitions : Int
} derive(Debug, Eq)
///|
pub(all) struct ImpactReport {
policy_name : String
impacts : Array[FeatureImpact]
source_rows : Int
truncated : Bool
} derive(Debug, Eq)
///|
/// Measure how often changing one feature changes the outcome across a bounded
/// decision table. Counts are directional and therefore suitable for ranking,
/// not probability estimates.
pub fn Policy::impact_report(
self : Policy,
table_limit? : Int = 256,
) -> Result[ImpactReport, Diagnostic] {
let table = match self.decision_table(limit=table_limit) {
Ok(value) => value
Err(error) => return Err(error)
}
let impacts : Array[FeatureImpact] = []
for feature in self.features {
if feature.locked {
continue
}
let mut transitions = 0
let mut outcome_flips = 0
let mut flips_to_true = 0
let mut flips_to_false = 0
let mut unknown_transitions = 0
for row in table.rows {
let current = environment_get(row.environment, feature.name)
for candidate in feature.domain {
if current == Some(candidate) {
continue
}
transitions = transitions + 1
let changed = environment_set(row.environment, feature.name, candidate)
let next = evaluate(self.goal, changed).truth
if next == Unknown || row.outcome == Unknown {
unknown_transitions = unknown_transitions + 1
}
if next != row.outcome && next != Unknown && row.outcome != Unknown {
outcome_flips = outcome_flips + 1
if next == Yes {
flips_to_true = flips_to_true + 1
} else {
flips_to_false = flips_to_false + 1
}
}
}
}
impacts.push({
feature: feature.name,
transitions,
outcome_flips,
flips_to_true,
flips_to_false,
unknown_transitions,
})
}
impacts.sort_by((left, right) => {
let by_flips = right.outcome_flips.compare(left.outcome_flips)
if by_flips != 0 {
by_flips
} else {
left.feature.compare(right.feature)
}
})
Ok({
policy_name: self.name,
impacts,
source_rows: table.rows.length(),
truncated: table.truncated,
})
}
///|
pub fn ImpactReport::to_markdown(self : ImpactReport) -> String {
let out = StringBuilder::new()
out.write_string("# Feature impact: \{self.policy_name}\n\n")
out.write_string(
"| feature | transitions | flips | to true | to false | unknown |\n",
)
out.write_string("| --- | ---: | ---: | ---: | ---: | ---: |\n")
for item in self.impacts {
out.write_string(
"| \{item.feature} | \{item.transitions} | \{item.outcome_flips} | \{item.flips_to_true} | \{item.flips_to_false} | \{item.unknown_transitions} |\n",
)
}
if self.truncated {
out.write_string("\n_Report is based on a truncated decision table._\n")
}
out.to_string()
}