Understanding Projection Systems
DARKO (Daily Adjusted Ratings Keeping Optimism) represents the current frontier of basketball projection systems, developed by Kostya Medvedovsky to predict future player performance rather than merely summarize past production. While evaluation metrics assess what players have done, projection systems estimate what they will do—a fundamentally different and arguably more useful task for team decision-making.
Projection systems emerged from the recognition that raw past performance poorly predicts future outcomes. Young players typically improve; veterans typically decline. Shooting percentages regress toward true talent levels. A sophisticated projection system accounts for all these factors.
The DARKO Methodology
DARKO builds projections through several integrated components. The foundation is a talent estimation model that uses current and previous season statistics to estimate true underlying ability levels. Age curves overlay these estimates with developmental expectations—shooting typically improves into the late twenties, athleticism peaks earlier.
Injury history informs playing time projections. A player projected for excellent per-minute production provides limited value if injury risk suggests they'll only play 40 games.
Regression to the Mean
Perhaps the most important concept in projection systems is regression to the mean. Extreme performances tend to move toward average over time because they contain large random components. A player shooting 45% from three on high volume likely had some luck; projecting continued 45% would overestimate future performance.
DARKO applies regression based on sample size and statistic reliability. Three-point percentage, with high variance, regresses aggressively. Free throw percentage, more stable, regresses less.
Contract and Trade Valuation
The primary application of projection systems is player valuation for roster decisions. DARKO provides not just next-season estimates but full career projections that can be summed and discounted to calculate total expected value. A free agent decision becomes a comparison of projected value to contract cost across the deal's duration.
Validation and Accuracy
Projection system quality is measured by comparing projections to actual outcomes. Sophisticated projection systems substantially outperform naive methods like assuming last season's statistics will repeat. The value of regression, age curves, and multi-year data integration shows clearly in reduced prediction errors.
Implementation in R
# Understanding DARKO projection system
library(tidyverse)
# Simplified DARKO-style projection
project_player <- function(player_history, age) {
# Age curve adjustments
peak_age <- 27
age_factor <- 1 - 0.02 * abs(age - peak_age)
player_history %>%
group_by(player_id) %>%
arrange(desc(season)) %>%
slice_head(n = 3) %>%
summarise(
# Weighted average of recent seasons
proj_pts = weighted.mean(pts, c(0.5, 0.3, 0.2)),
proj_ast = weighted.mean(ast, c(0.5, 0.3, 0.2)),
proj_reb = weighted.mean(reb, c(0.5, 0.3, 0.2)),
proj_bpm = weighted.mean(bpm, c(0.5, 0.3, 0.2)),
.groups = "drop"
) %>%
mutate(
# Apply age adjustment
proj_pts = proj_pts * age_factor,
proj_bpm = proj_bpm * age_factor
)
}
player_history <- read_csv("player_career_stats.csv")
projections <- project_player(player_history, age = 28)
print(projections)
# Daily projection updates
library(tidyverse)
update_daily_projection <- function(prior_projection, game_result) {
# Bayesian-style update
learning_rate <- 0.1
prior_projection %>%
left_join(game_result, by = "player_id") %>%
mutate(
# Update projections based on game performance
proj_pts = proj_pts + learning_rate * (game_pts - proj_pts),
proj_bpm = proj_bpm + learning_rate * (game_bpm - proj_bpm),
confidence = confidence * (1 + 0.01) # Increase confidence
)
}
prior <- read_csv("current_projections.csv")
game <- read_csv("last_game_stats.csv")
updated <- update_daily_projection(prior, game)
print(updated)
Implementation in Python
# Understanding DARKO projection system
import pandas as pd
import numpy as np
def project_player(player_history, player_ages):
"""Simplified DARKO-style projection"""
# Recent season weights
weights = [0.5, 0.3, 0.2]
# Get last 3 seasons per player
recent = player_history.sort_values(
["player_id", "season"], ascending=[True, False]
).groupby("player_id").head(3)
projections = recent.groupby("player_id").apply(
lambda x: pd.Series({
"proj_pts": np.average(x["pts"], weights=weights[:len(x)]),
"proj_ast": np.average(x["ast"], weights=weights[:len(x)]),
"proj_reb": np.average(x["reb"], weights=weights[:len(x)]),
"proj_bpm": np.average(x["bpm"], weights=weights[:len(x)])
})
).reset_index()
# Age curve adjustment
projections = projections.merge(player_ages, on="player_id")
peak_age = 27
projections["age_factor"] = 1 - 0.02 * abs(projections["age"] - peak_age)
projections["proj_pts"] *= projections["age_factor"]
projections["proj_bpm"] *= projections["age_factor"]
return projections
player_history = pd.read_csv("player_career_stats.csv")
ages = pd.read_csv("player_ages.csv")
projections = project_player(player_history, ages)
print(projections)
Implementation in R
# Understanding DARKO projection system
library(tidyverse)
# Simplified DARKO-style projection
project_player <- function(player_history, age) {
# Age curve adjustments
peak_age <- 27
age_factor <- 1 - 0.02 * abs(age - peak_age)
player_history %>%
group_by(player_id) %>%
arrange(desc(season)) %>%
slice_head(n = 3) %>%
summarise(
# Weighted average of recent seasons
proj_pts = weighted.mean(pts, c(0.5, 0.3, 0.2)),
proj_ast = weighted.mean(ast, c(0.5, 0.3, 0.2)),
proj_reb = weighted.mean(reb, c(0.5, 0.3, 0.2)),
proj_bpm = weighted.mean(bpm, c(0.5, 0.3, 0.2)),
.groups = "drop"
) %>%
mutate(
# Apply age adjustment
proj_pts = proj_pts * age_factor,
proj_bpm = proj_bpm * age_factor
)
}
player_history <- read_csv("player_career_stats.csv")
projections <- project_player(player_history, age = 28)
print(projections)
# Daily projection updates
library(tidyverse)
update_daily_projection <- function(prior_projection, game_result) {
# Bayesian-style update
learning_rate <- 0.1
prior_projection %>%
left_join(game_result, by = "player_id") %>%
mutate(
# Update projections based on game performance
proj_pts = proj_pts + learning_rate * (game_pts - proj_pts),
proj_bpm = proj_bpm + learning_rate * (game_bpm - proj_bpm),
confidence = confidence * (1 + 0.01) # Increase confidence
)
}
prior <- read_csv("current_projections.csv")
game <- read_csv("last_game_stats.csv")
updated <- update_daily_projection(prior, game)
print(updated)
Implementation in Python
# Understanding DARKO projection system
import pandas as pd
import numpy as np
def project_player(player_history, player_ages):
"""Simplified DARKO-style projection"""
# Recent season weights
weights = [0.5, 0.3, 0.2]
# Get last 3 seasons per player
recent = player_history.sort_values(
["player_id", "season"], ascending=[True, False]
).groupby("player_id").head(3)
projections = recent.groupby("player_id").apply(
lambda x: pd.Series({
"proj_pts": np.average(x["pts"], weights=weights[:len(x)]),
"proj_ast": np.average(x["ast"], weights=weights[:len(x)]),
"proj_reb": np.average(x["reb"], weights=weights[:len(x)]),
"proj_bpm": np.average(x["bpm"], weights=weights[:len(x)])
})
).reset_index()
# Age curve adjustment
projections = projections.merge(player_ages, on="player_id")
peak_age = 27
projections["age_factor"] = 1 - 0.02 * abs(projections["age"] - peak_age)
projections["proj_pts"] *= projections["age_factor"]
projections["proj_bpm"] *= projections["age_factor"]
return projections
player_history = pd.read_csv("player_career_stats.csv")
ages = pd.read_csv("player_ages.csv")
projections = project_player(player_history, ages)
print(projections)