Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Game Accuracy not explained well on website #15164

Open
cmwetherell opened this issue Apr 28, 2024 · 1 comment
Open

Game Accuracy not explained well on website #15164

cmwetherell opened this issue Apr 28, 2024 · 1 comment
Labels
non technical Artwork, content, documentation

Comments

@cmwetherell
Copy link

cmwetherell commented Apr 28, 2024

The page on the Lichess accuracy metric talks about Win% and Accuracy% as a move by move metric, but doesn't discuss the gameAccuracy function defined in the source code below. In the game review section, the "information" button near the Accuracy % for the game links to the page without any discussion of the comprehensive game accuracy.

Just sharing this picture as an example. The blue "i" made me think the page I was going to would explain Game Accuracy.
image

I think this page would be improved if there is a more clear explanation of converting the move by move Accuracy% to game accuracy. For example, why was volatility weighted and harmonic means chosen? Why are they averaged? Why is the volatility clamped to 0.5 - 12? I'd find this information interesting, I think others would as well.

def gameAccuracy(startColor: Color, cps: List[Eval.Cp]): Option[ByColor[AccuracyPercent]] =
val allWinPercents = (Eval.Cp.initial :: cps).map(WinPercent.fromCentiPawns)
val windowSize = (cps.size / 10).atLeast(2).atMost(8)
val allWinPercentValues = WinPercent.raw(allWinPercents)
val windows =
List
.fill(windowSize.atMost(allWinPercentValues.size) - 2)(allWinPercentValues.take(windowSize))
.toList ::: allWinPercentValues.sliding(windowSize).toList
val weights = windows.map { xs => Maths.standardDeviation(xs).orZero.atLeast(0.5).atMost(12) }
val weightedAccuracies: Iterable[((Double, Double), Color)] = allWinPercents
.sliding(2)
.zip(weights)
.zipWithIndex
.collect { case ((List(prev, next), weight), i) =>
val color = Color.fromWhite((i % 2 == 0) == startColor.white)
val accuracy =
AccuracyPercent.fromWinPercents(color.fold(prev, next), color.fold(next, prev)).value
((accuracy, weight), color)
}
.to(Iterable)
// cps.zip(weightedAccuracies) foreach { case (eval, ((acc, weight), color)) =>
// println(s"$eval $color ${weight.toInt} ${acc.toInt}")
// }
def colorAccuracy(color: Color) = for
weighted <- Maths.weightedMean:
weightedAccuracies.collect:
case (weightedAccuracy, c) if c == color => weightedAccuracy
harmonic <- Maths.harmonicMean:
weightedAccuracies.collect:
case ((accuracy, _), c) if c == color => accuracy
yield AccuracyPercent((weighted + harmonic) / 2)
ByColor(colorAccuracy)

@kraktus kraktus added the non technical Artwork, content, documentation label Apr 29, 2024
@kraktus
Copy link
Member

kraktus commented Apr 29, 2024

For example, why was volatility weighted and harmonic means chosen? Why are they averaged? Why is the volatility clamped to 0.5 - 12?

As far as I remember, these were just heuristics tweaked manually until it matched the most with what a human would feel about accuracy. It is not a scientific metric.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
non technical Artwork, content, documentation
Projects
None yet
Development

No branches or pull requests

2 participants