ml_meta_perf.practices
Turning a fitted equation into written guidance.
This is the payoff the whole project trades accuracy for, so it is worth being exact
about what it is and is not. A practice here is a statement of the form "across this
meta-dataset, higher X went with higher/lower MCC, worth about D MCC points". That is an
association measured on 20 datasets, not a causal claim and not a guarantee; the
confidence column carries the evidence needed to discount it.
The derivation is deliberately empirical rather than symbolic. Reading a sign off a weight is wrong as soon as a feature appears in more than one term, or appears inside a ratio's denominator, or appears under a transform that flips its monotonicity. Instead every term's contribution is evaluated over the real data, contributions are summed per feature, and the direction is the rank correlation between the feature and the total it drives. That answers "what does this equation actually do as this feature rises", which is the question a practitioner is asking.
Study chapter: 6. Best practices against the equation -- the rationale, in prose, with the figures.
1"""Turning a fitted equation into written guidance. 2 3This is the payoff the whole project trades accuracy for, so it is worth being exact 4about what it is and is not. A practice here is a statement of the form "across this 5meta-dataset, higher X went with higher/lower MCC, worth about D MCC points". That is an 6**association measured on 20 datasets**, not a causal claim and not a guarantee; the 7`confidence` column carries the evidence needed to discount it. 8 9The derivation is deliberately empirical rather than symbolic. Reading a sign off a 10weight is wrong as soon as a feature appears in more than one term, or appears inside a 11ratio's denominator, or appears under a transform that flips its monotonicity. Instead 12every term's contribution is evaluated over the real data, contributions are summed per 13feature, and the direction is the rank correlation between the feature and the total it 14drives. That answers "what does this equation actually do as this feature rises", which 15is the question a practitioner is asking. 16 17Study chapter: [6. Best practices against the equation][study-chapter] -- the rationale, in 18prose, with the figures. 19 20[study-chapter]: https://github.com/mariolpantunes/ml-meta-perf/blob/main/assets/docs/06-practices.md 21""" 22 23from __future__ import annotations 24 25import numpy as np 26import polars as pl 27 28from ml_meta_perf.attribution import contributions 29from ml_meta_perf.data import FEATURE_GLOSSARY 30from ml_meta_perf.model import Equation 31from ml_meta_perf.stats import spearman 32 33#: A feature has to move predicted MCC by at least this much, between the bottom and top 34#: decile of its own range, before it is worth writing down as guidance. 35MIN_EFFECT = 0.02 36 37#: And it has to have been selected in at least this fraction of cross-validation folds. 38#: A term picked in 4 of 20 folds is an artefact of which datasets landed in the training 39#: split; publishing it as advice would be the most misleading thing this module could do. 40MIN_STABILITY = 0.5 41 42#: And its relationship has to be monotone enough for "higher X" to mean anything. A 43#: feature can carry a large effect through a non-monotone term -- a product with another 44#: feature that changes sign, say -- and no directional sentence would be true of it. 45MIN_DIRECTION = 0.15 46 47#: Fraction of rows taken as the low and high end when measuring an effect. 48DECILE = 0.1 49 50 51def feature_practices( 52 equation: Equation, 53 columns: dict[str, np.ndarray], 54 stability: pl.DataFrame | None = None, 55) -> pl.DataFrame: 56 """One row per raw feature the equation uses, strongest effect first. 57 58 ``effect`` is signed: the mean MCC this feature's terms contribute in its top decile 59 minus the mean they contribute in its bottom decile. Ordering by the feature is the 60 whole point -- a quantile of the contributions alone would be order-invariant and 61 would describe the term's spread rather than its response to the feature. 62 63 ``direction`` is the rank correlation between the feature and the MCC it drives, so 64 it survives transforms and multiple appearances. ``stability`` is the mean selection 65 frequency of the terms involved, when a cross-validated stability table is supplied. 66 A published-equation term absent from a non-empty stability table was selected in zero 67 folds and therefore contributes zero to that mean. ``NaN`` is reserved for the case in 68 which no stability evaluation was supplied. 69 70 A feature appearing in a term shared with another feature is credited the whole of 71 that term, so two features sharing one dominant term will show the same effect. That 72 is honest -- the term cannot be split between them -- but it means ``effect`` reads 73 as "how much MCC moves across this feature's range", not "how much this feature owns". 74 """ 75 matrix = contributions(equation, columns) 76 frequency = _frequency_lookup(stability) 77 78 features = sorted({feature for term in equation.terms for feature in term.features}) 79 if not features: 80 # An empty frame built from an empty row list carries no schema, so every later 81 # column reference fails. Declare it instead. 82 return pl.DataFrame( 83 schema={ 84 "feature": pl.String, 85 "meaning": pl.String, 86 "n_terms": pl.Int64, 87 "direction": pl.Float64, 88 "effect": pl.Float64, 89 "stability": pl.Float64, 90 } 91 ) 92 rows: list[dict[str, object]] = [] 93 for feature in features: 94 indices = [index for index, term in enumerate(equation.terms) if feature in term.features] 95 total = matrix[:, indices].sum(axis=1) 96 values = columns[feature] 97 order = np.argsort(values) 98 edge = max(1, round(DECILE * values.shape[0])) 99 effect = float(total[order[-edge:]].mean() - total[order[:edge]].mean()) 100 rows.append( 101 { 102 "feature": feature, 103 "meaning": FEATURE_GLOSSARY.get(feature, feature), 104 "n_terms": len(indices), 105 "direction": spearman(values, total), 106 "effect": effect, 107 "stability": float( 108 np.mean([frequency.get(equation.terms[index].name, 0.0) for index in indices]) 109 ) 110 if frequency 111 else float("nan"), 112 } 113 ) 114 return pl.DataFrame(rows).sort(pl.col("effect").abs(), descending=True) 115 116 117def _frequency_lookup(stability: pl.DataFrame | None) -> dict[str, float]: 118 if stability is None or stability.height == 0: 119 return {} 120 return dict(zip(stability["term"].to_list(), stability["frequency"].to_list(), strict=True)) 121 122 123def _confidence(effect: float, stability: float) -> str: 124 if np.isnan(stability): 125 return "unrated" 126 if stability >= 0.85 and abs(effect) >= 0.10: 127 return "strong" 128 if stability >= MIN_STABILITY and abs(effect) >= 0.05: 129 return "moderate" 130 return "weak" 131 132 133def best_practices( 134 equation: Equation, 135 columns: dict[str, np.ndarray], 136 stability: pl.DataFrame | None = None, 137 *, 138 min_effect: float = MIN_EFFECT, 139 min_stability: float = MIN_STABILITY, 140 min_direction: float = MIN_DIRECTION, 141) -> pl.DataFrame: 142 """Written practices, filtered to those the evidence actually supports. 143 144 A feature is reported when it moves predicted MCC by at least ``min_effect``, its 145 relationship is monotone enough to describe in a sentence (``min_direction``), and 146 its terms survived at least ``min_stability`` of the cross-validation folds. Features 147 failing any test are dropped rather than reported with a caveat -- a practice nobody 148 should act on is better left unwritten. 149 """ 150 table = feature_practices(equation, columns, stability) 151 if table.height == 0: 152 return table.with_columns(pl.lit("").alias("practice"), pl.lit("").alias("confidence")) 153 154 kept = table.filter( 155 (pl.col("effect").abs() >= min_effect) 156 & (pl.col("direction").abs() >= min_direction) 157 # The two measurements have to agree on the sign, and occasionally they do not. 158 # ``direction`` is a rank correlation over the whole range; ``effect`` compares 159 # the top decile against the bottom. A feature can be monotonically negative 160 # overall and still contribute more MCC in its top decile than its bottom -- and 161 # the sentence would then say "went with lower MCC" while quoting a positive 162 # decile difference as its evidence, which is worse than saying nothing. On the 163 # published equation this drops exactly one feature, inference cost, whose 164 # direction is -0.17 against a decile difference of +0.03. 165 & (pl.col("direction").sign() == pl.col("effect").sign()) 166 ) 167 if stability is not None: 168 kept = kept.filter(pl.col("stability").is_nan() | (pl.col("stability") >= min_stability)) 169 170 statements: list[str] = [] 171 confidences: list[str] = [] 172 for row in kept.iter_rows(named=True): 173 rises = float(row["direction"]) > 0.0 174 statements.append( 175 f"Higher {row['meaning']} went with {'higher' if rises else 'lower'} MCC " 176 f"(about {abs(float(row['effect'])):.2f} MCC between its lowest and highest decile)." 177 ) 178 confidences.append(_confidence(float(row["effect"]), float(row["stability"]))) 179 180 return kept.with_columns( 181 pl.Series("practice", statements), 182 pl.Series("confidence", confidences), 183 ) 184 185 186def concordance( 187 first: Equation, 188 second: Equation, 189 columns: dict[str, np.ndarray], 190 *, 191 names: tuple[str, str] = ("first", "second"), 192) -> pl.DataFrame: 193 """Do two independently-discovered equations give the same advice? 194 195 This is the question that decides whether the extracted practices describe *the data* 196 or merely describe *one search*. Fit two equations by methods that share no machinery 197 -- an enumerated library with beam selection, and a dendrogram cut with no selection 198 at all -- then compare the guidance rather than the R2. 199 200 Agreement on **direction** is the claim that matters. If a short, less accurate 201 equation and a long, more accurate one both say that higher class-centre separation 202 goes with lower MCC, that statement is a property of the meta-dataset, and the 203 accuracy gap between the two equations is beside the point. 204 205 Agreement on **effect magnitude** is a separate and weaker claim, reported alongside 206 so the two are not conflated. Two equations can agree on every direction while 207 disagreeing on which effect is largest. 208 """ 209 left = {row["feature"]: row for row in feature_practices(first, columns).iter_rows(named=True)} 210 right = {row["feature"]: row for row in feature_practices(second, columns).iter_rows(named=True)} 211 212 rows: list[dict[str, object]] = [] 213 for feature in sorted(set(left) & set(right)): 214 a, b = left[feature], right[feature] 215 rows.append( 216 { 217 "feature": feature, 218 f"direction_{names[0]}": a["direction"], 219 f"direction_{names[1]}": b["direction"], 220 "agrees": bool(np.sign(a["direction"]) == np.sign(b["direction"])), 221 f"effect_{names[0]}": a["effect"], 222 f"effect_{names[1]}": b["effect"], 223 } 224 ) 225 if not rows: 226 return pl.DataFrame(schema={"feature": pl.String, "agrees": pl.Boolean}) 227 return pl.DataFrame(rows) 228 229 230def concordance_summary(table: pl.DataFrame, names: tuple[str, str] = ("first", "second")) -> dict[str, float]: 231 """Headline numbers from a concordance table: agreement rate and effect-rank correlation.""" 232 if table.height == 0 or "agrees" not in table.columns: 233 return {"shared": 0.0, "direction_agreement": float("nan"), "effect_rank_correlation": float("nan")} 234 agreement = float(table["agrees"].sum()) / table.height 235 left = np.abs(table[f"effect_{names[0]}"].to_numpy()) 236 right = np.abs(table[f"effect_{names[1]}"].to_numpy()) 237 return { 238 "shared": float(table.height), 239 "direction_agreement": agreement, 240 "effect_rank_correlation": spearman(left, right), 241 } 242 243 244def render(practices: pl.DataFrame) -> str: 245 """The practices as a numbered, readable list.""" 246 if practices.height == 0: 247 return "No practice met the effect and stability thresholds." 248 lines: list[str] = [] 249 for index, row in enumerate(practices.iter_rows(named=True), start=1): 250 lines.append(f"{index:2d}. [{row['confidence']:8s}] {row['practice']}") 251 return "\n".join(lines)
52def feature_practices( 53 equation: Equation, 54 columns: dict[str, np.ndarray], 55 stability: pl.DataFrame | None = None, 56) -> pl.DataFrame: 57 """One row per raw feature the equation uses, strongest effect first. 58 59 ``effect`` is signed: the mean MCC this feature's terms contribute in its top decile 60 minus the mean they contribute in its bottom decile. Ordering by the feature is the 61 whole point -- a quantile of the contributions alone would be order-invariant and 62 would describe the term's spread rather than its response to the feature. 63 64 ``direction`` is the rank correlation between the feature and the MCC it drives, so 65 it survives transforms and multiple appearances. ``stability`` is the mean selection 66 frequency of the terms involved, when a cross-validated stability table is supplied. 67 A published-equation term absent from a non-empty stability table was selected in zero 68 folds and therefore contributes zero to that mean. ``NaN`` is reserved for the case in 69 which no stability evaluation was supplied. 70 71 A feature appearing in a term shared with another feature is credited the whole of 72 that term, so two features sharing one dominant term will show the same effect. That 73 is honest -- the term cannot be split between them -- but it means ``effect`` reads 74 as "how much MCC moves across this feature's range", not "how much this feature owns". 75 """ 76 matrix = contributions(equation, columns) 77 frequency = _frequency_lookup(stability) 78 79 features = sorted({feature for term in equation.terms for feature in term.features}) 80 if not features: 81 # An empty frame built from an empty row list carries no schema, so every later 82 # column reference fails. Declare it instead. 83 return pl.DataFrame( 84 schema={ 85 "feature": pl.String, 86 "meaning": pl.String, 87 "n_terms": pl.Int64, 88 "direction": pl.Float64, 89 "effect": pl.Float64, 90 "stability": pl.Float64, 91 } 92 ) 93 rows: list[dict[str, object]] = [] 94 for feature in features: 95 indices = [index for index, term in enumerate(equation.terms) if feature in term.features] 96 total = matrix[:, indices].sum(axis=1) 97 values = columns[feature] 98 order = np.argsort(values) 99 edge = max(1, round(DECILE * values.shape[0])) 100 effect = float(total[order[-edge:]].mean() - total[order[:edge]].mean()) 101 rows.append( 102 { 103 "feature": feature, 104 "meaning": FEATURE_GLOSSARY.get(feature, feature), 105 "n_terms": len(indices), 106 "direction": spearman(values, total), 107 "effect": effect, 108 "stability": float( 109 np.mean([frequency.get(equation.terms[index].name, 0.0) for index in indices]) 110 ) 111 if frequency 112 else float("nan"), 113 } 114 ) 115 return pl.DataFrame(rows).sort(pl.col("effect").abs(), descending=True)
One row per raw feature the equation uses, strongest effect first.
effect is signed: the mean MCC this feature's terms contribute in its top decile
minus the mean they contribute in its bottom decile. Ordering by the feature is the
whole point -- a quantile of the contributions alone would be order-invariant and
would describe the term's spread rather than its response to the feature.
direction is the rank correlation between the feature and the MCC it drives, so
it survives transforms and multiple appearances. stability is the mean selection
frequency of the terms involved, when a cross-validated stability table is supplied.
A published-equation term absent from a non-empty stability table was selected in zero
folds and therefore contributes zero to that mean. NaN is reserved for the case in
which no stability evaluation was supplied.
A feature appearing in a term shared with another feature is credited the whole of
that term, so two features sharing one dominant term will show the same effect. That
is honest -- the term cannot be split between them -- but it means effect reads
as "how much MCC moves across this feature's range", not "how much this feature owns".
134def best_practices( 135 equation: Equation, 136 columns: dict[str, np.ndarray], 137 stability: pl.DataFrame | None = None, 138 *, 139 min_effect: float = MIN_EFFECT, 140 min_stability: float = MIN_STABILITY, 141 min_direction: float = MIN_DIRECTION, 142) -> pl.DataFrame: 143 """Written practices, filtered to those the evidence actually supports. 144 145 A feature is reported when it moves predicted MCC by at least ``min_effect``, its 146 relationship is monotone enough to describe in a sentence (``min_direction``), and 147 its terms survived at least ``min_stability`` of the cross-validation folds. Features 148 failing any test are dropped rather than reported with a caveat -- a practice nobody 149 should act on is better left unwritten. 150 """ 151 table = feature_practices(equation, columns, stability) 152 if table.height == 0: 153 return table.with_columns(pl.lit("").alias("practice"), pl.lit("").alias("confidence")) 154 155 kept = table.filter( 156 (pl.col("effect").abs() >= min_effect) 157 & (pl.col("direction").abs() >= min_direction) 158 # The two measurements have to agree on the sign, and occasionally they do not. 159 # ``direction`` is a rank correlation over the whole range; ``effect`` compares 160 # the top decile against the bottom. A feature can be monotonically negative 161 # overall and still contribute more MCC in its top decile than its bottom -- and 162 # the sentence would then say "went with lower MCC" while quoting a positive 163 # decile difference as its evidence, which is worse than saying nothing. On the 164 # published equation this drops exactly one feature, inference cost, whose 165 # direction is -0.17 against a decile difference of +0.03. 166 & (pl.col("direction").sign() == pl.col("effect").sign()) 167 ) 168 if stability is not None: 169 kept = kept.filter(pl.col("stability").is_nan() | (pl.col("stability") >= min_stability)) 170 171 statements: list[str] = [] 172 confidences: list[str] = [] 173 for row in kept.iter_rows(named=True): 174 rises = float(row["direction"]) > 0.0 175 statements.append( 176 f"Higher {row['meaning']} went with {'higher' if rises else 'lower'} MCC " 177 f"(about {abs(float(row['effect'])):.2f} MCC between its lowest and highest decile)." 178 ) 179 confidences.append(_confidence(float(row["effect"]), float(row["stability"]))) 180 181 return kept.with_columns( 182 pl.Series("practice", statements), 183 pl.Series("confidence", confidences), 184 )
Written practices, filtered to those the evidence actually supports.
A feature is reported when it moves predicted MCC by at least min_effect, its
relationship is monotone enough to describe in a sentence (min_direction), and
its terms survived at least min_stability of the cross-validation folds. Features
failing any test are dropped rather than reported with a caveat -- a practice nobody
should act on is better left unwritten.
187def concordance( 188 first: Equation, 189 second: Equation, 190 columns: dict[str, np.ndarray], 191 *, 192 names: tuple[str, str] = ("first", "second"), 193) -> pl.DataFrame: 194 """Do two independently-discovered equations give the same advice? 195 196 This is the question that decides whether the extracted practices describe *the data* 197 or merely describe *one search*. Fit two equations by methods that share no machinery 198 -- an enumerated library with beam selection, and a dendrogram cut with no selection 199 at all -- then compare the guidance rather than the R2. 200 201 Agreement on **direction** is the claim that matters. If a short, less accurate 202 equation and a long, more accurate one both say that higher class-centre separation 203 goes with lower MCC, that statement is a property of the meta-dataset, and the 204 accuracy gap between the two equations is beside the point. 205 206 Agreement on **effect magnitude** is a separate and weaker claim, reported alongside 207 so the two are not conflated. Two equations can agree on every direction while 208 disagreeing on which effect is largest. 209 """ 210 left = {row["feature"]: row for row in feature_practices(first, columns).iter_rows(named=True)} 211 right = {row["feature"]: row for row in feature_practices(second, columns).iter_rows(named=True)} 212 213 rows: list[dict[str, object]] = [] 214 for feature in sorted(set(left) & set(right)): 215 a, b = left[feature], right[feature] 216 rows.append( 217 { 218 "feature": feature, 219 f"direction_{names[0]}": a["direction"], 220 f"direction_{names[1]}": b["direction"], 221 "agrees": bool(np.sign(a["direction"]) == np.sign(b["direction"])), 222 f"effect_{names[0]}": a["effect"], 223 f"effect_{names[1]}": b["effect"], 224 } 225 ) 226 if not rows: 227 return pl.DataFrame(schema={"feature": pl.String, "agrees": pl.Boolean}) 228 return pl.DataFrame(rows)
Do two independently-discovered equations give the same advice?
This is the question that decides whether the extracted practices describe the data or merely describe one search. Fit two equations by methods that share no machinery -- an enumerated library with beam selection, and a dendrogram cut with no selection at all -- then compare the guidance rather than the R2.
Agreement on direction is the claim that matters. If a short, less accurate equation and a long, more accurate one both say that higher class-centre separation goes with lower MCC, that statement is a property of the meta-dataset, and the accuracy gap between the two equations is beside the point.
Agreement on effect magnitude is a separate and weaker claim, reported alongside so the two are not conflated. Two equations can agree on every direction while disagreeing on which effect is largest.
231def concordance_summary(table: pl.DataFrame, names: tuple[str, str] = ("first", "second")) -> dict[str, float]: 232 """Headline numbers from a concordance table: agreement rate and effect-rank correlation.""" 233 if table.height == 0 or "agrees" not in table.columns: 234 return {"shared": 0.0, "direction_agreement": float("nan"), "effect_rank_correlation": float("nan")} 235 agreement = float(table["agrees"].sum()) / table.height 236 left = np.abs(table[f"effect_{names[0]}"].to_numpy()) 237 right = np.abs(table[f"effect_{names[1]}"].to_numpy()) 238 return { 239 "shared": float(table.height), 240 "direction_agreement": agreement, 241 "effect_rank_correlation": spearman(left, right), 242 }
Headline numbers from a concordance table: agreement rate and effect-rank correlation.
245def render(practices: pl.DataFrame) -> str: 246 """The practices as a numbered, readable list.""" 247 if practices.height == 0: 248 return "No practice met the effect and stability thresholds." 249 lines: list[str] = [] 250 for index, row in enumerate(practices.iter_rows(named=True), start=1): 251 lines.append(f"{index:2d}. [{row['confidence']:8s}] {row['practice']}") 252 return "\n".join(lines)
The practices as a numbered, readable list.