StatisticsCalculationService API Reference
Complete API reference for the StatisticsCalculationService - the analytics engine for performance statistics and score calculations.
Overview
File: domain/services/StatisticsCalculationService.kt
Type: Business Logic Service
Status: ✅ Production | 📝 Needs comprehensive documentation
Purpose
StatisticsCalculationService encapsulates all statistical calculations and analytics for rounds, providing:
- Score aggregation and totals
- Average calculations
- Performance trends
- Equipment correlation analysis
- Session analytics
- Distribution analysis
Key Responsibilities
- Score Aggregation - Total scores, averages, maximums
- Performance Metrics - Trends, consistency, improvement
- Equipment Analytics - Performance by setup
- Distribution Analysis - Arrow value frequency
- Session Statistics - Per-session metrics
API Methods
1. Round Statistics
Calculate Round Total
fun calculateRoundTotal(endScores: List<EndScore>): IntPurpose: Calculates total score for a round from end scores
Parameters:
endScores- List of end scores for the round
Returns: Total score across all ends
Example:
val endScores = repository.getEndScoresForRound(roundId).getOrThrow()
val totalScore = service.calculateRoundTotal(endScores)
println("Round total: $totalScore")Implementation:
fun calculateRoundTotal(endScores: List<EndScore>): Int =
endScores.sumOf { it.totalScore }Calculate Round Average
fun calculateRoundAverage(endScores: List<EndScore>): DoublePurpose: Calculates average score per end
Returns: Average end score, or 0.0 if no ends
Example:
val average = service.calculateRoundAverage(endScores)
println("Average per end: ${"%.2f".format(average)}")Implementation:
fun calculateRoundAverage(endScores: List<EndScore>): Double {
if (endScores.isEmpty()) return 0.0
return endScores.map { it.totalScore }.average()
}Calculate Arrow Average
fun calculateArrowAverage(arrowScores: List<ArrowScore>): DoublePurpose: Calculates average score per arrow
Example:
val arrowScores = repository.getArrowScoresForRound(roundId).getOrThrow()
val arrowAverage = service.calculateArrowAverage(arrowScores)
println("Average per arrow: ${"%.2f".format(arrowAverage)}")2. X-Count and 10-Count Statistics
Calculate Total X-Count
fun calculateTotalXCount(endScores: List<EndScore>): IntPurpose: Counts total X-ring hits across all ends
Example:
val xCount = service.calculateTotalXCount(endScores)
println("Total X-ring hits: $xCount")Calculate X-Count Percentage
fun calculateXCountPercentage(
xCount: Int,
totalArrows: Int
): DoublePurpose: Calculates percentage of arrows in X-ring
Returns: Percentage (0.0 to 100.0)
Example:
val xCount = 45
val totalArrows = 60
val percentage = service.calculateXCountPercentage(xCount, totalArrows)
println("X-ring percentage: ${"%.1f".format(percentage)}%")
// Output: "X-ring percentage: 75.0%"Implementation:
fun calculateXCountPercentage(xCount: Int, totalArrows: Int): Double {
if (totalArrows == 0) return 0.0
return (xCount.toDouble() / totalArrows) * 100.0
}3. Performance Trends
Calculate Performance Trend
fun calculatePerformanceTrend(
rounds: List<Round>
): PerformanceTrendPurpose: Analyzes performance trend over multiple rounds
Returns: PerformanceTrend with direction and percentage change
Example:
data class PerformanceTrend(
val direction: TrendDirection, // IMPROVING, DECLINING, STABLE
val percentageChange: Double, // e.g., +5.2%
val confidence: Double // 0.0 to 1.0
)
val recentRounds = repository.getRecentRounds(10).getOrThrow()
val trend = service.calculatePerformanceTrend(recentRounds)
when (trend.direction) {
TrendDirection.IMPROVING ->
println("Performance improving by ${trend.percentageChange}%")
TrendDirection.DECLINING ->
println("Performance declining by ${trend.percentageChange}%")
TrendDirection.STABLE ->
println("Performance is stable")
}Algorithm:
fun calculatePerformanceTrend(rounds: List<Round>): PerformanceTrend {
if (rounds.size < 3) {
return PerformanceTrend(TrendDirection.STABLE, 0.0, 0.0)
}
val scores = rounds.map { it.finalScore }
val recent = scores.take(scores.size / 2).average()
val older = scores.drop(scores.size / 2).average()
val change = ((recent - older) / older) * 100.0
val direction = when {
change > 2.0 -> TrendDirection.IMPROVING
change < -2.0 -> TrendDirection.DECLINING
else -> TrendDirection.STABLE
}
val confidence = minOf(rounds.size / 10.0, 1.0)
return PerformanceTrend(direction, change, confidence)
}Calculate Consistency Score
fun calculateConsistencyScore(endScores: List<EndScore>): DoublePurpose: Measures shooting consistency (0.0 = random, 1.0 = perfect)
Returns: Consistency score based on standard deviation
Example:
val consistency = service.calculateConsistencyScore(endScores)
println("Consistency: ${"%.2f".format(consistency)}")
// 0.95 = Very consistent
// 0.75 = Moderately consistent
// 0.50 = InconsistentImplementation:
fun calculateConsistencyScore(endScores: List<EndScore>): Double {
if (endScores.size < 2) return 1.0
val scores = endScores.map { it.totalScore.toDouble() }
val mean = scores.average()
val variance = scores.map { (it - mean).pow(2) }.average()
val stdDev = sqrt(variance)
// Normalize: lower stdDev = higher consistency
val maxStdDev = mean // Maximum expected deviation
return 1.0 - (stdDev / maxStdDev).coerceIn(0.0, 1.0)
}4. Score Distribution
Calculate Score Distribution
fun calculateScoreDistribution(
arrowScores: List<ArrowScore>
): Map<Int, Int>Purpose: Counts frequency of each arrow value
Returns: Map of score value to count
Example:
val arrows = repository.getArrowScoresForRound(roundId).getOrThrow()
val distribution = service.calculateScoreDistribution(arrows)
distribution.forEach { (score, count) ->
println("Score $score: $count arrows")
}
// Output:
// Score 10: 25 arrows
// Score 9: 18 arrows
// Score 8: 12 arrows
// Score 7: 5 arrowsVisualization:
fun printDistributionChart(distribution: Map<Int, Int>) {
distribution.toSortedMap(reverseOrder()).forEach { (score, count) ->
val bar = "█".repeat(count)
println("$score │ $bar $count")
}
}
// Output:
// 10 │ █████████████████████████ 25
// 9 │ ██████████████████ 18
// 8 │ ████████████ 12
// 7 │ █████ 55. Equipment Correlation
Calculate Equipment Performance
fun calculateEquipmentPerformance(
rounds: List<Round>,
equipmentSetup: EquipmentSetup
): EquipmentPerformancePurpose: Analyzes performance with specific equipment
Returns: Performance metrics for given equipment setup
Example:
data class EquipmentPerformance(
val averageScore: Double,
val roundsCount: Int,
val xCountPercentage: Double,
val consistency: Double,
val comparedToOverall: Double // e.g., +3.5% better
)
val setup = repository.getBowSetup(setupId).getOrThrow()
val rounds = repository.getRoundsByEquipment(setupId).getOrThrow()
val performance = service.calculateEquipmentPerformance(rounds, setup)
println("With ${setup.name}:")
println(" Average: ${performance.averageScore}")
println(" X-count: ${"%.1f".format(performance.xCountPercentage)}%")
println(" Consistency: ${"%.2f".format(performance.consistency)}")
println(" vs Overall: ${performance.comparedToOverall:+.1f}%")6. Session Analytics
Calculate Session Statistics
fun calculateSessionStatistics(
rounds: List<Round>,
sessionDate: LocalDate
): SessionStatisticsPurpose: Aggregates statistics for a practice session
Returns: Comprehensive session metrics
Example:
data class SessionStatistics(
val totalRounds: Int,
val totalArrows: Int,
val averageScore: Double,
val highestRound: Int,
val lowestRound: Int,
val xCountTotal: Int,
val duration: Duration,
val improvement: Double // vs previous session
)
val today = LocalDate.now()
val todayRounds = repository.getRoundsByDate(today).getOrThrow()
val stats = service.calculateSessionStatistics(todayRounds, today)
println("Session Summary for $today:")
println(" Rounds: ${stats.totalRounds}")
println(" Arrows: ${stats.totalArrows}")
println(" Average: ${"%.1f".format(stats.averageScore)}")
println(" Best: ${stats.highestRound}")
println(" X-count: ${stats.xCountTotal}")7. Comparative Analytics
Compare Performance Periods
fun comparePerformancePeriods(
period1: List<Round>,
period2: List<Round>
): PerformanceComparisonPurpose: Compares performance between two time periods
Example:
data class PerformanceComparison(
val averageChange: Double, // Percentage change
val consistencyChange: Double, // Consistency improvement
val xCountChange: Double, // X-count change
val trend: String // "Improving", "Declining", "Stable"
)
val thisMonth = repository.getRoundsInMonth(currentMonth).getOrThrow()
val lastMonth = repository.getRoundsInMonth(previousMonth).getOrThrow()
val comparison = service.comparePerformancePeriods(thisMonth, lastMonth)
println("Month-over-month performance:")
println(" Average: ${comparison.averageChange:+.1f}%")
println(" Consistency: ${comparison.consistencyChange:+.2f}")
println(" X-count: ${comparison.xCountChange:+.1f}%")
println(" Trend: ${comparison.trend}")Complex Use Cases
Personal Best Detection
suspend fun findPersonalBests(userId: Long): PersonalBests {
val allRounds = repository.getAllRoundsForUser(userId).getOrThrow()
return PersonalBests(
highestRoundScore = allRounds.maxByOrNull { it.finalScore },
highestEndScore = findHighestEndScore(allRounds),
mostXCount = allRounds.maxByOrNull { it.xCount },
bestConsistency = findMostConsistentRound(allRounds),
longestStreak = findLongestPerfectEndStreak(allRounds)
)
}
private fun findMostConsistentRound(rounds: List<Round>): Round? {
return rounds.maxByOrNull { round ->
val endScores = getEndScoresForRound(round.id)
service.calculateConsistencyScore(endScores)
}
}Goal Progress Tracking
suspend fun calculateGoalProgress(
userId: Long,
goal: PerformanceGoal
): GoalProgress {
val recentRounds = repository.getRecentRounds(userId, 10).getOrThrow()
val currentAverage = service.calculateRoundAverage(
recentRounds.flatMap { getEndScoresForRound(it.id) }
)
val progress = ((currentAverage - goal.baseline) /
(goal.target - goal.baseline)) * 100.0
return GoalProgress(
current = currentAverage,
target = goal.target,
progressPercentage = progress.coerceIn(0.0, 100.0),
onTrack = progress >= goal.expectedProgressByNow()
)
}Testing
Unit Test Examples
class StatisticsCalculationServiceTest {
private lateinit var service: StatisticsCalculationService
@Before
fun setup() {
service = StatisticsCalculationService()
}
@Test
fun `calculateRoundTotal sums all end scores`() {
val endScores = listOf(
EndScore(totalScore = 54),
EndScore(totalScore = 56),
EndScore(totalScore = 58)
)
val total = service.calculateRoundTotal(endScores)
assertEquals(168, total)
}
@Test
fun `calculateRoundAverage returns correct average`() {
val endScores = listOf(
EndScore(totalScore = 50),
EndScore(totalScore = 60),
EndScore(totalScore = 55)
)
val average = service.calculateRoundAverage(endScores)
assertEquals(55.0, average, 0.01)
}
@Test
fun `calculateRoundAverage returns zero for empty list`() {
val average = service.calculateRoundAverage(emptyList())
assertEquals(0.0, average, 0.01)
}
@Test
fun `calculateXCountPercentage returns correct percentage`() {
val percentage = service.calculateXCountPercentage(
xCount = 45,
totalArrows = 60
)
assertEquals(75.0, percentage, 0.01)
}
@Test
fun `calculateConsistencyScore returns 1_0 for identical scores`() {
val endScores = List(10) { EndScore(totalScore = 60) }
val consistency = service.calculateConsistencyScore(endScores)
assertEquals(1.0, consistency, 0.01)
}
@Test
fun `calculateScoreDistribution counts correctly`() {
val arrows = listOf(
ArrowScore(score = 10),
ArrowScore(score = 10),
ArrowScore(score = 9),
ArrowScore(score = 10),
ArrowScore(score = 8)
)
val distribution = service.calculateScoreDistribution(arrows)
assertEquals(3, distribution[10])
assertEquals(1, distribution[9])
assertEquals(1, distribution[8])
}
}Best Practices
1. Handle Edge Cases
// GOOD: Defensive programming
fun calculateAverage(scores: List<Int>): Double {
if (scores.isEmpty()) return 0.0
return scores.average()
}
// BAD: Can crash with empty list
fun calculateAverage(scores: List<Int>): Double {
return scores.average() // Throws if empty
}2. Use Appropriate Number Types
// GOOD: Use Double for percentages and averages
fun calculatePercentage(part: Int, total: Int): Double {
return (part.toDouble() / total) * 100.0
}
// BAD: Integer division loses precision
fun calculatePercentage(part: Int, total: Int): Int {
return (part / total) * 100 // Always 0 if part < total
}3. Document Statistical Methods
/**
* Calculates standard deviation of end scores.
*
* Uses sample standard deviation formula:
* σ = sqrt(Σ(xi - μ)² / (n - 1))
*
* @param scores List of end scores
* @return Standard deviation, or 0.0 if < 2 samples
*/
fun calculateStandardDeviation(scores: List<Double>): Double4. Provide Context with Statistics
data class StatisticWithContext(
val value: Double,
val label: String,
val comparisonToAverage: Double,
val percentile: Int,
val trend: TrendDirection
)Performance Considerations
Caching Calculations
class StatisticsCalculationService {
private val cache = LruCache<String, Any>(maxSize = 50)
fun calculateRoundStatistics(roundId: Long): RoundStatistics {
val cacheKey = "round_stats_$roundId"
return cache.get(cacheKey) as? RoundStatistics
?: computeRoundStatistics(roundId).also { stats ->
cache.put(cacheKey, stats)
}
}
}Efficient Distribution Calculation
// GOOD: Single pass
fun calculateScoreDistribution(arrows: List<ArrowScore>): Map<Int, Int> {
return arrows.groupingBy { it.score }.eachCount()
}
// BAD: Multiple iterations
fun calculateScoreDistribution(arrows: List<ArrowScore>): Map<Int, Int> {
val distribution = mutableMapOf<Int, Int>()
(0..10).forEach { score ->
distribution[score] = arrows.count { it.score == score }
}
return distribution
}Related Documentation
Architecture:
Related Services:
Related Components:
Flows:
- Analytics Flow (Coming soon)
Contributing
When modifying StatisticsCalculationService:
- Add tests - Unit tests for all calculations
- Validate inputs - Handle edge cases (empty lists, division by zero)
- Document formulas - Statistical methods need clear documentation
- Consider precision - Use appropriate number types (Double vs Int)
- Cache results - Expensive calculations should be cached
Status: ✅ Production | 📝 Needs comprehensive documentation Test Coverage: Unit tests for core calculations Last Updated: 2025-11-01