refactor!: scores_with_noise, and History::quality
Two names that described the wrong thing.
`scores_with_sigma(scores, sigma)` reads as "these scores have prior
sigma 2.0". The quantity is observation noise on the score *margin*, in
the units of the scores, and it is spelled `score_sigma` at every config
site — `HistoryBuilder::score_sigma`, `GameOptions::score_sigma`,
`EventKind::Scored { score_sigma }` — so this was the one place the
crate used a third meaning of "sigma" for it. Its own doc had to
disambiguate itself: "`sigma` overrides `HistoryBuilder::score_sigma`".
`scores_with_noise(scores, score_sigma)` on both `Outcome` and
`EventBuilder`.
`predict_quality` predicts nothing. Its own doc says it answers "is this
matchup *fair*", not "what will happen", and the `predict_*` family is
otherwise exactly the methods returning a probability or a distribution
over outcomes. `History::quality` also makes the free/method pair
consistent: free `quality` pairs with `History::quality` the way free
`expected_information_gain` already pairs with
`History::expected_information_gain`. The rule that was already being
followed and never stated — a free function scores a hypothetical from
explicit parameters, the same-named method asks it against the fit — is
now written on the method.
Closes #75. Refs #78 (part 4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
+20
-13
@@ -1188,7 +1188,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
// `converge` refuses to report a NaN fit, but nothing stopped a
|
||||
// caller ignoring that error and predicting anyway. Measured on
|
||||
// a point-mass-prior history with `beta(0.0)`, after `converge`
|
||||
// returned `NonFiniteResult`: `predict_quality` gave `Ok(NaN)`,
|
||||
// returned `NonFiniteResult`: `quality` gave `Ok(NaN)`,
|
||||
// `predict_outcome().total()` gave `NaN`, and
|
||||
// `predict_win_probabilities` gave `Ok([0.0, 0.0])` — finite,
|
||||
// plausible, and summing to zero against a doc that promises
|
||||
@@ -1226,7 +1226,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
//
|
||||
// Every prediction here is a statement about how performances *vary*,
|
||||
// and in this configuration nothing varies. The consequences were three
|
||||
// different wrong answers rather than one error. `predict_quality`
|
||||
// different wrong answers rather than one error. `quality`
|
||||
// **panicked** — "cannot invert a singular matrix", from a
|
||||
// `Result`-returning method, on a history that had converged cleanly —
|
||||
// because the contrast covariance `beta^2 A^T A + A^T S A` is exactly
|
||||
@@ -1301,14 +1301,21 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw-probability quality metric for the given teams (key slices).
|
||||
/// How fair a matchup between these teams would be, against the fit.
|
||||
///
|
||||
/// Values range roughly `[0, 1]`; 1 == perfectly matched. Supports any
|
||||
/// Values range roughly `[0, 1]`; 1 is perfectly matched. Supports any
|
||||
/// number of teams.
|
||||
///
|
||||
/// Note this answers "is this matchup *fair*", which is not the same as
|
||||
/// "is this matchup *informative*" — the two coincide for two evenly
|
||||
/// matched teams and diverge elsewhere.
|
||||
/// The method form of the free [`quality`](crate::quality), which scores a
|
||||
/// hypothetical from explicit skill distributions instead. That is the rule
|
||||
/// the whole family follows: a free function takes parameters, the
|
||||
/// same-named `History` method asks the question against what was fitted.
|
||||
///
|
||||
/// It was `predict_quality` until #78 pointed out that it predicts nothing
|
||||
/// — it answers "is this matchup *fair*", not "what will happen". Fair is
|
||||
/// also not the same as *informative*: the two coincide for two evenly
|
||||
/// matched teams and diverge elsewhere. See
|
||||
/// [`History::expected_information_gain`] for the other question.
|
||||
///
|
||||
/// # Preconditions
|
||||
///
|
||||
@@ -1329,7 +1336,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||
/// and every skill is a point mass, leaving no performance distribution to
|
||||
/// predict from.
|
||||
pub fn predict_quality<Q>(&self, teams: &[&[&Q]]) -> Result<f64, InferenceError>
|
||||
pub fn quality<Q>(&self, teams: &[&[&Q]]) -> Result<f64, InferenceError>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||
@@ -1826,7 +1833,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// ones that would actually be fitted if the matchup were played and
|
||||
/// recorded.
|
||||
///
|
||||
/// Distinct from [`History::predict_quality`], which measures *fairness*.
|
||||
/// Distinct from [`History::quality`], which measures *fairness*.
|
||||
/// The two coincide for two evenly matched competitors and diverge
|
||||
/// elsewhere. See [`expected_information_gain`](crate::expected_information_gain)
|
||||
/// for the scale, the analytic `ln k` ceiling, and the cost.
|
||||
@@ -4153,7 +4160,7 @@ mod tests {
|
||||
crate::Team::with_members([crate::Member::new("a")]),
|
||||
crate::Team::with_members([crate::Member::new("b")]),
|
||||
],
|
||||
outcome: Outcome::scores_with_sigma([3.0, 1.0], 0.5),
|
||||
outcome: Outcome::scores_with_noise([3.0, 1.0], 0.5),
|
||||
}])
|
||||
.unwrap();
|
||||
let _ = h_a.converge().unwrap();
|
||||
@@ -4195,7 +4202,7 @@ mod tests {
|
||||
crate::Team::with_members([crate::Member::new("a")]),
|
||||
crate::Team::with_members([crate::Member::new("b")]),
|
||||
],
|
||||
outcome: Outcome::scores_with_sigma([3.0, 1.0], 2.0),
|
||||
outcome: Outcome::scores_with_noise([3.0, 1.0], 2.0),
|
||||
}])
|
||||
.unwrap();
|
||||
let _ = h_a.converge().unwrap();
|
||||
@@ -4261,7 +4268,7 @@ mod tests {
|
||||
h_a.event(0_i64)
|
||||
.team(["a"])
|
||||
.team(["b"])
|
||||
.scores_with_sigma([3.0, 1.0], 2.0)
|
||||
.scores_with_noise([3.0, 1.0], 2.0)
|
||||
.commit()
|
||||
.unwrap();
|
||||
let _ = h_a.converge().unwrap();
|
||||
@@ -4274,7 +4281,7 @@ mod tests {
|
||||
crate::Team::with_members([crate::Member::new("a")]),
|
||||
crate::Team::with_members([crate::Member::new("b")]),
|
||||
],
|
||||
outcome: Outcome::scores_with_sigma([3.0, 1.0], 2.0),
|
||||
outcome: Outcome::scores_with_noise([3.0, 1.0], 2.0),
|
||||
}])
|
||||
.unwrap();
|
||||
let _ = h_b.converge().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user