Capstone - Complete Analytics System
Learning Objectives
- Conduct comprehensive season performance reviews
- Analyze squad composition and identify gaps
- Build player development assessments
- Create data-driven transfer priorities
- Plan for next season using historical performance data
The end of a football season marks both an ending and a beginning. Analytics teams must evaluate what happened, identify what needs to change, and build plans for improvement. This chapter covers the complete season review process that sets the foundation for future success.
The Season Review Framework
A comprehensive season review answers fundamental questions: Did we achieve our objectives? Where did we succeed and fail? What changes do we need to make? The framework below structures this analysis.
- League position vs target
- Cup progress
- Home vs away splits
- Points by month
- xG vs actual goals
- Playing style evolution
- Set piece effectiveness
- Tactical patterns
- Individual player reviews
- Squad depth by position
- Age profile analysis
- Injury impact
- Transfer priorities
- Contract decisions
- Youth integration
- Tactical evolution
# Python: Season Review Framework
import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, List, Optional
@dataclass
class SeasonObjectives:
"""Season objectives and targets."""
league_position: int
cup_round: str # e.g., "Quarter-finals"
points_target: int
achieved: Dict[str, bool] = field(default_factory=dict)
@dataclass
class SeasonResults:
"""Season results summary."""
wins: int
draws: int
losses: int
points: int
goals_for: int
goals_against: int
xg_for: float
xg_against: float
league_position: int
class SeasonReview:
"""Comprehensive season review analysis."""
def __init__(self, season: str, team: str,
matches: pd.DataFrame,
player_stats: pd.DataFrame,
objectives: Optional[SeasonObjectives] = None):
self.season = season
self.team = team
self.matches = matches
self.player_stats = player_stats
self.objectives = objectives
self.results = self._calculate_results()
def _calculate_results(self) -> SeasonResults:
"""Calculate season results summary."""
team_matches = self.matches[self.matches["team"] == self.team]
return SeasonResults(
wins=len(team_matches[team_matches["result"] == "W"]),
draws=len(team_matches[team_matches["result"] == "D"]),
losses=len(team_matches[team_matches["result"] == "L"]),
points=(len(team_matches[team_matches["result"] == "W"]) * 3 +
len(team_matches[team_matches["result"] == "D"])),
goals_for=team_matches["goals_for"].sum(),
goals_against=team_matches["goals_against"].sum(),
xg_for=team_matches["xg"].sum(),
xg_against=team_matches["xg_against"].sum(),
league_position=self._get_league_position()
)
def _get_league_position(self) -> int:
"""Get final league position."""
# Would normally query from league table
return 4 # Placeholder
def assess_objectives(self) -> Dict:
"""Assess season against objectives."""
assessment = {}
if self.objectives:
# League position
assessment["league"] = {
"target": self.objectives.league_position,
"actual": self.results.league_position,
"achieved": self.results.league_position <= self.objectives.league_position,
"margin": self.objectives.league_position - self.results.league_position
}
# Points
assessment["points"] = {
"target": self.objectives.points_target,
"actual": self.results.points,
"achieved": self.results.points >= self.objectives.points_target
}
# xG analysis
xg_diff = self.results.goals_for - self.results.xg_for
assessment["xg_performance"] = {
"xg_for": round(self.results.xg_for, 1),
"actual_goals": self.results.goals_for,
"difference": round(xg_diff, 1),
"assessment": self._assess_xg_performance(xg_diff)
}
return assessment
def _assess_xg_performance(self, diff: float) -> str:
"""Assess performance vs xG."""
if diff > 5:
return "Significantly overperformed xG"
elif diff > 2:
return "Moderately overperformed xG"
elif diff < -5:
return "Significantly underperformed xG"
elif diff < -2:
return "Moderately underperformed xG"
return "Performed as expected"
def monthly_analysis(self) -> pd.DataFrame:
"""Analyze performance by month."""
team_matches = self.matches[self.matches["team"] == self.team].copy()
team_matches["month"] = pd.to_datetime(team_matches["date"]).dt.month
monthly = team_matches.groupby("month").agg({
"result": lambda x: (x == "W").sum() * 3 + (x == "D").sum(),
"goals_for": "sum",
"goals_against": "sum",
"xg": "sum"
}).reset_index()
monthly.columns = ["month", "points", "goals_for", "goals_against", "xg"]
monthly["matches"] = team_matches.groupby("month").size().values
monthly["ppg"] = monthly["points"] / monthly["matches"]
return monthly
print("Season review framework ready")# R: Season Review Framework
library(tidyverse)
library(R6)
# Season Review Class
SeasonReview <- R6Class("SeasonReview",
public = list(
season = NULL,
team = NULL,
matches = NULL,
player_stats = NULL,
# Results
league_position = NULL,
points = NULL,
target_position = NULL,
initialize = function(season, team, matches, player_stats,
target_position = NULL) {
self$season <- season
self$team <- team
self$matches <- matches
self$player_stats <- player_stats
self$target_position <- target_position
# Calculate basic results
self$calculate_results()
},
calculate_results = function() {
team_matches <- self$matches %>%
filter(team == self$team)
self$points <- team_matches %>%
summarise(
wins = sum(result == "W"),
draws = sum(result == "D"),
losses = sum(result == "L"),
points = wins * 3 + draws,
goals_for = sum(goals_for),
goals_against = sum(goals_against),
xg_for = sum(xg, na.rm = TRUE),
xg_against = sum(xg_against, na.rm = TRUE)
)
},
# Objective assessment
assess_objectives = function() {
assessment <- list()
# League position
if (!is.null(self$target_position) && !is.null(self$league_position)) {
assessment$league <- list(
target = self$target_position,
actual = self$league_position,
achieved = self$league_position <= self$target_position,
margin = self$target_position - self$league_position
)
}
# Points analysis
assessment$points <- list(
total = self$points$points,
ppg = self$points$points / (self$points$wins + self$points$draws + self$points$losses),
home_ppg = self$calculate_home_away_ppg()$home,
away_ppg = self$calculate_home_away_ppg()$away
)
# xG performance
assessment$xg <- list(
total_xg = self$points$xg_for,
total_xga = self$points$xg_against,
xg_diff = self$points$xg_for - self$points$xg_against,
goals_vs_xg = self$points$goals_for - self$points$xg_for,
assessment = case_when(
self$points$goals_for > self$points$xg_for + 5 ~ "Overperformed xG",
self$points$goals_for < self$points$xg_for - 5 ~ "Underperformed xG",
TRUE ~ "Performed as expected"
)
)
return(assessment)
},
calculate_home_away_ppg = function() {
team_matches <- self$matches %>%
filter(team == self$team)
home <- team_matches %>%
filter(venue == "Home") %>%
summarise(ppg = (sum(result == "W") * 3 + sum(result == "D")) / n())
away <- team_matches %>%
filter(venue == "Away") %>%
summarise(ppg = (sum(result == "W") * 3 + sum(result == "D")) / n())
list(home = home$ppg, away = away$ppg)
}
)
)
cat("Season review framework initialized\n")Squad Analysis
Understanding squad composition, utilization, and gaps is crucial for planning. This analysis identifies strengths, weaknesses, and priorities for improvement.
# Python: Comprehensive squad analysis
import pandas as pd
import numpy as np
from typing import Dict, List, Optional
from dataclasses import dataclass
from datetime import date
@dataclass
class SquadGap:
"""Identified squad gap."""
position: str
severity: str # High, Medium, Low
current_depth: int
recommendation: str
class SquadAnalyzer:
"""Analyze squad composition and identify gaps."""
def __init__(self, player_stats: pd.DataFrame,
contracts: Optional[pd.DataFrame] = None):
self.player_stats = player_stats
self.contracts = contracts
def analyze_utilization(self) -> pd.DataFrame:
"""Analyze playing time distribution."""
return self.player_stats.groupby("position_group").agg({
"minutes": ["sum", "mean", "max", "count"],
"player": "nunique"
}).reset_index()
def analyze_age_profile(self) -> pd.DataFrame:
"""Analyze squad age profile."""
# Filter to regular players
regulars = self.player_stats[self.player_stats["minutes"] >= 450].copy()
# Categorize by age
def age_group(age):
if age <= 21:
return "U21"
elif age <= 24:
return "22-24"
elif age <= 28:
return "25-28"
elif age <= 31:
return "29-31"
return "32+"
regulars["age_group"] = regulars["age"].apply(age_group)
profile = regulars.groupby("age_group").agg({
"player": "count",
"minutes": "sum",
"season_rating": "mean"
}).reset_index()
profile.columns = ["age_group", "n_players", "total_minutes", "avg_rating"]
profile["pct_minutes"] = (
profile["total_minutes"] / profile["total_minutes"].sum() * 100
)
return profile
def analyze_depth(self) -> pd.DataFrame:
"""Analyze squad depth by position."""
depth = self.player_stats.groupby("position_group").apply(
lambda x: pd.Series({
"starters": (x["minutes"] >= 2000).sum(),
"rotational": ((x["minutes"] >= 900) & (x["minutes"] < 2000)).sum(),
"fringe": ((x["minutes"] >= 450) & (x["minutes"] < 900)).sum(),
"minimal": (x["minutes"] < 450).sum(),
"total_players": len(x)
})
).reset_index()
# Calculate depth score
depth["depth_score"] = (
depth["starters"] * 3 +
depth["rotational"] * 2 +
depth["fringe"]
)
depth["assessment"] = depth["depth_score"].apply(
lambda x: "Strong" if x >= 8 else ("Adequate" if x >= 5 else "Weak")
)
return depth
def analyze_contracts(self) -> Optional[pd.DataFrame]:
"""Analyze contract situations."""
if self.contracts is None:
return None
df = self.contracts.copy()
df["years_remaining"] = (
(pd.to_datetime(df["contract_expiry"]) - pd.Timestamp.now()).dt.days / 365
)
df["situation"] = df["years_remaining"].apply(
lambda x: "Critical" if x <= 1 else ("Attention" if x <= 2 else "Secure")
)
return df.groupby("situation").agg({
"player": "count",
"market_value": "sum"
}).reset_index()
def identify_gaps(self) -> List[SquadGap]:
"""Identify squad gaps and priorities."""
gaps = []
depth = self.analyze_depth()
# Position gaps
for _, row in depth.iterrows():
if row["assessment"] == "Weak":
gaps.append(SquadGap(
position=row["position_group"],
severity="High",
current_depth=row["starters"] + row["rotational"],
recommendation=f"Urgent need for {row['position_group']} reinforcement"
))
# Age gaps
age_profile = self.analyze_age_profile()
senior_pct = age_profile[
age_profile["age_group"].isin(["29-31", "32+"])
]["pct_minutes"].sum()
if senior_pct > 40:
gaps.append(SquadGap(
position="Squad-wide",
severity="Medium",
current_depth=0,
recommendation="Prioritize younger signings - aging squad"
))
return gaps
def generate_report(self) -> Dict:
"""Generate complete squad analysis report."""
return {
"utilization": self.analyze_utilization(),
"age_profile": self.analyze_age_profile(),
"depth": self.analyze_depth(),
"contracts": self.analyze_contracts(),
"gaps": self.identify_gaps()
}
print("Squad analyzer ready")# R: Comprehensive squad analysis
library(tidyverse)
# Squad Analyzer
analyze_squad <- function(player_stats, contracts) {
analysis <- list()
# Playing time distribution
analysis$utilization <- player_stats %>%
group_by(position_group) %>%
summarise(
total_minutes = sum(minutes),
players_used = n_distinct(player),
avg_minutes = mean(minutes),
top_player_minutes = max(minutes),
concentration = max(minutes) / sum(minutes) * 100, # % held by top player
.groups = "drop"
)
# Age profile
analysis$age_profile <- player_stats %>%
filter(minutes >= 450) %>% # Regular players
mutate(
age_group = case_when(
age <= 21 ~ "U21",
age <= 24 ~ "22-24",
age <= 28 ~ "25-28",
age <= 31 ~ "29-31",
TRUE ~ "32+"
)
) %>%
group_by(age_group) %>%
summarise(
n_players = n(),
total_minutes = sum(minutes),
avg_rating = mean(season_rating, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
pct_minutes = total_minutes / sum(total_minutes) * 100
)
# Squad depth by position
analysis$depth <- player_stats %>%
group_by(position_group) %>%
summarise(
starters = sum(minutes >= 2000),
rotational = sum(minutes >= 900 & minutes < 2000),
fringe = sum(minutes >= 450 & minutes < 900),
minimal = sum(minutes < 450),
total_players = n(),
.groups = "drop"
) %>%
mutate(
depth_score = starters * 3 + rotational * 2 + fringe,
depth_assessment = case_when(
depth_score >= 8 ~ "Strong",
depth_score >= 5 ~ "Adequate",
TRUE ~ "Weak"
)
)
# Contract situations
if (!is.null(contracts)) {
analysis$contracts <- contracts %>%
mutate(
years_remaining = as.numeric(difftime(contract_expiry, Sys.Date(), units = "days")) / 365,
situation = case_when(
years_remaining <= 1 ~ "Critical",
years_remaining <= 2 ~ "Attention",
TRUE ~ "Secure"
)
) %>%
group_by(situation) %>%
summarise(
n_players = n(),
total_value = sum(market_value, na.rm = TRUE),
.groups = "drop"
)
}
return(analysis)
}
# Identify squad gaps
identify_squad_gaps <- function(squad_analysis, requirements) {
gaps <- list()
# Check depth by position
depth <- squad_analysis$depth
for (pos in unique(depth$position_group)) {
pos_depth <- depth %>% filter(position_group == pos)
if (pos_depth$depth_assessment == "Weak") {
gaps[[pos]] <- list(
severity = "High",
current_starters = pos_depth$starters,
current_rotation = pos_depth$rotational,
recommendation = sprintf("Need to add %s depth", pos)
)
}
}
# Age concerns
age_profile <- squad_analysis$age_profile
senior_minutes <- age_profile %>%
filter(age_group %in% c("29-31", "32+")) %>%
summarise(pct = sum(pct_minutes))
if (senior_minutes$pct > 40) {
gaps$age <- list(
severity = "Medium",
issue = "Squad aging - high reliance on 29+ players",
recommendation = "Prioritize younger signings for long-term"
)
}
return(gaps)
}
cat("Squad analysis functions ready\n")Player Development Review
Individual player reviews assess season performance, development trajectory, and future potential. This is particularly important for young players and high-value assets.
# Python: Player development assessment
import pandas as pd
import numpy as np
from typing import Dict, Optional
from dataclasses import dataclass
@dataclass
class PlayerDevelopment:
"""Player development trajectory."""
minutes_change: int
rating_change: float
key_metric_changes: Dict[str, float]
trajectory: str # Improving, Stable, Declining
class PlayerReviewGenerator:
"""Generate individual player season reviews."""
def generate_report(self, player_name: str,
current_stats: pd.DataFrame,
previous_stats: Optional[pd.DataFrame] = None) -> Dict:
"""Generate comprehensive player report card."""
current = current_stats[current_stats["player"] == player_name]
if current.empty:
raise ValueError(f"Player not found: {player_name}")
player = current.iloc[0]
report = {
"info": {
"player": player_name,
"age": player["age"],
"position": player["position"],
"minutes": player["minutes"],
"appearances": player.get("appearances", 0)
},
"performance": self._assess_performance(player),
"development": self._assess_development(player_name, player, previous_stats),
"rankings": self._get_percentile_rankings(player),
"assessment": self._generate_assessment(player)
}
return report
def _assess_performance(self, player: pd.Series) -> Dict:
"""Assess season performance."""
return {
"goals": player.get("goals", 0),
"assists": player.get("assists", 0),
"xg": round(player.get("xg", 0), 2),
"xa": round(player.get("xa", 0), 2),
"goals_vs_xg": round(player.get("goals", 0) - player.get("xg", 0), 2),
"overall_rating": player.get("season_rating", 6.0)
}
def _assess_development(self, player_name: str,
current: pd.Series,
previous: Optional[pd.DataFrame]) -> Optional[Dict]:
"""Compare with previous season."""
if previous is None or previous.empty:
return None
prev = previous[previous["player"] == player_name]
if prev.empty:
return None
last_season = prev.iloc[0]
rating_change = current.get("season_rating", 6) - last_season.get("season_rating", 6)
return {
"minutes_change": current["minutes"] - last_season["minutes"],
"rating_change": round(rating_change, 2),
"trajectory": self._classify_trajectory(rating_change)
}
def _classify_trajectory(self, rating_change: float) -> str:
"""Classify development trajectory."""
if rating_change > 0.3:
return "Strong improvement"
elif rating_change > 0:
return "Slight improvement"
elif rating_change < -0.3:
return "Decline"
return "Stable"
def _get_percentile_rankings(self, player: pd.Series) -> Dict:
"""Get percentile rankings vs position."""
return {
"xg_percentile": player.get("xg_percentile", 50),
"xa_percentile": player.get("xa_percentile", 50),
"progressive_percentile": player.get("progressive_percentile", 50),
"defensive_percentile": player.get("defensive_percentile", 50)
}
def _generate_assessment(self, player: pd.Series) -> Dict:
"""Generate overall assessment and recommendation."""
age = player["age"]
rating = player.get("season_rating", 6.0)
value = player.get("market_value", 0)
# Determine recommendation
if age <= 23 and rating >= 6.5:
recommendation = "Key prospect - increase playing time"
elif age <= 23 and rating < 6.0:
recommendation = "Consider loan for development"
elif age >= 30 and rating < 6.5:
recommendation = "Consider sale - declining asset"
elif rating >= 7.5:
recommendation = "Core player - prioritize extension"
else:
recommendation = "Maintain current role"
return {
"current_value": value,
"projected_value": self._project_value(age, rating, value),
"recommendation": recommendation
}
def _project_value(self, age: int, rating: float, current_value: float) -> float:
"""Project future market value."""
# Age curve
if age <= 22:
age_mult = 1.3
elif age <= 25:
age_mult = 1.15
elif age <= 28:
age_mult = 1.0
elif age <= 30:
age_mult = 0.85
else:
age_mult = 0.7
# Performance multiplier
perf_mult = 1 + (rating - 6) / 4
return round(current_value * age_mult * perf_mult, 1)
print("Player review generator ready")# R: Player development assessment
library(tidyverse)
# Generate player season report card
generate_player_report <- function(player_name, current_season_stats,
previous_seasons, expected_development) {
current <- current_season_stats %>%
filter(player == player_name)
if (nrow(current) == 0) {
stop(paste("Player not found:", player_name))
}
report <- list()
# Basic info
report$info <- list(
player = player_name,
age = current$age,
position = current$position,
minutes = current$minutes,
appearances = current$appearances
)
# Performance metrics
report$performance <- list(
goals = current$goals,
assists = current$assists,
xg = current$xg,
xa = current$xa,
goals_minus_xg = current$goals - current$xg,
assists_minus_xa = current$assists - current$xa,
overall_rating = current$season_rating
)
# Year-over-year development
if (!is.null(previous_seasons)) {
previous <- previous_seasons %>%
filter(player == player_name) %>%
arrange(desc(season))
if (nrow(previous) > 0) {
last_season <- previous[1, ]
report$development <- list(
minutes_change = current$minutes - last_season$minutes,
rating_change = current$season_rating - last_season$season_rating,
xg_per90_change = (current$xg / current$minutes * 90) -
(last_season$xg / last_season$minutes * 90),
trajectory = case_when(
current$season_rating > last_season$season_rating + 0.3 ~ "Strong improvement",
current$season_rating > last_season$season_rating ~ "Slight improvement",
current$season_rating < last_season$season_rating - 0.3 ~ "Decline",
TRUE ~ "Stable"
)
)
}
}
# Percentile rankings (vs position)
report$rankings <- list(
xg_percentile = current$xg_percentile,
xa_percentile = current$xa_percentile,
progressive_percentile = current$progressive_percentile,
defensive_percentile = current$defensive_percentile
)
# Future assessment
report$assessment <- list(
current_value = current$market_value,
potential_value = estimate_future_value(current),
recommendation = generate_player_recommendation(report)
)
return(report)
}
# Estimate future value
estimate_future_value <- function(player_data) {
age <- player_data$age
current_value <- player_data$market_value
rating <- player_data$season_rating
# Age curve factor
age_factor <- case_when(
age <= 22 ~ 1.3,
age <= 25 ~ 1.15,
age <= 28 ~ 1.0,
age <= 30 ~ 0.85,
TRUE ~ 0.7
)
# Performance factor
perf_factor <- (rating - 6) / 4 + 1 # 1.0 for 6.0 rating, up to 1.5 for 8.0
current_value * age_factor * perf_factor
}
# Generate recommendation
generate_player_recommendation <- function(report) {
age <- report$info$age
rating <- report$performance$overall_rating
if (age <= 23 && rating >= 6.5) {
return("Key development prospect - increase playing time")
}
if (age <= 23 && rating < 6.0) {
return("Consider loan to aid development")
}
if (age >= 30 && rating < 6.5) {
return("Consider selling - declining asset")
}
if (rating >= 7.5) {
return("Core player - prioritize contract extension")
}
return("Maintain current role")
}Transfer Planning & Priorities
Season reviews directly inform transfer strategy. By combining squad gaps, budget constraints, and market opportunities, we can generate data-driven transfer priorities.
# Python: Transfer priority generator
import pandas as pd
import numpy as np
from typing import Dict, List
from dataclasses import dataclass
@dataclass
class TransferTarget:
"""Potential transfer target."""
player: str
position: str
age: int
current_club: str
market_value: float
score: float
fit_rating: float
@dataclass
class PositionPriority:
"""Transfer priority for a position."""
position: str
rank: int
severity: str
budget_allocation: float
targets: List[TransferTarget]
class TransferPlanner:
"""Generate data-driven transfer priorities."""
def __init__(self, base_budget: float):
self.base_budget = base_budget
def generate_priorities(self, squad_gaps: List,
market_data: pd.DataFrame,
outgoing_revenue: float = 0) -> Dict:
"""Generate complete transfer priority plan."""
total_budget = self.base_budget + outgoing_revenue
# Score and rank gaps
gap_rankings = self._rank_gaps(squad_gaps)
# Generate position priorities
position_priorities = []
remaining_budget = total_budget
for i, gap in enumerate(gap_rankings):
# Allocate budget based on severity
allocation = self._calculate_allocation(
gap.severity, remaining_budget, len(gap_rankings) - i
)
# Find targets within budget
targets = self._find_targets(
gap.position, allocation, market_data
)
position_priorities.append(PositionPriority(
position=gap.position,
rank=i + 1,
severity=gap.severity,
budget_allocation=allocation,
targets=targets
))
remaining_budget -= allocation
return {
"total_budget": total_budget,
"priorities": position_priorities,
"remaining_reserve": remaining_budget
}
def _rank_gaps(self, gaps: List) -> List:
"""Rank gaps by severity."""
severity_order = {"High": 3, "Medium": 2, "Low": 1}
return sorted(gaps, key=lambda x: severity_order.get(x.severity, 0), reverse=True)
def _calculate_allocation(self, severity: str,
remaining: float, positions_left: int) -> float:
"""Calculate budget allocation for position."""
pct = {"High": 0.40, "Medium": 0.30, "Low": 0.20}.get(severity, 0.15)
return min(remaining * pct, remaining / positions_left * 1.5)
def _find_targets(self, position: str, budget: float,
market_data: pd.DataFrame) -> List[TransferTarget]:
"""Find suitable targets for position."""
candidates = market_data[
(market_data["position_group"] == position) &
(market_data["market_value"] <= budget) &
(market_data["age"] <= 28)
].sort_values("score", ascending=False).head(5)
return [
TransferTarget(
player=row["player"],
position=row["position"],
age=row["age"],
current_club=row["team"],
market_value=row["market_value"],
score=row["score"],
fit_rating=row.get("fit_rating", 0)
)
for _, row in candidates.iterrows()
]
def format_plan(self, plan: Dict) -> str:
"""Format transfer plan for presentation."""
output = f"""
=== TRANSFER PRIORITY PLAN ===
Total Budget: €{plan["total_budget"]:.1f}M
Reserve: €{plan["remaining_reserve"]:.1f}M
POSITION PRIORITIES:
{"-" * 40}
"""
for p in plan["priorities"]:
output += f"""
{p.rank}. {p.position} [{p.severity}]
Budget: €{p.budget_allocation:.1f}M
Top Targets:
"""
for t in p.targets[:3]:
output += f" - {t.player} ({t.current_club}, {t.age}) - €{t.market_value:.1f}M\n"
return output
print("Transfer planner ready")# R: Transfer priority generator
library(tidyverse)
# Generate transfer priorities
generate_transfer_priorities <- function(squad_gaps, budget, market_data,
outgoing_decisions) {
priorities <- list()
# Calculate available budget
outgoing_revenue <- outgoing_decisions %>%
filter(decision %in% c("Sell", "Release")) %>%
summarise(revenue = sum(expected_fee, na.rm = TRUE))
total_budget <- budget + outgoing_revenue$revenue
# Prioritize gaps by severity
gap_priorities <- tibble()
for (pos in names(squad_gaps)) {
gap <- squad_gaps[[pos]]
gap_priorities <- bind_rows(gap_priorities, tibble(
position = pos,
severity = gap$severity,
priority_score = case_when(
gap$severity == "High" ~ 3,
gap$severity == "Medium" ~ 2,
TRUE ~ 1
)
))
}
gap_priorities <- gap_priorities %>%
arrange(desc(priority_score))
# For each priority, find market targets
for (i in 1:nrow(gap_priorities)) {
pos <- gap_priorities$position[i]
severity <- gap_priorities$severity[i]
# Budget allocation based on priority
allocation_pct <- case_when(
severity == "High" ~ 0.40,
severity == "Medium" ~ 0.25,
TRUE ~ 0.15
)
position_budget <- total_budget * allocation_pct
# Find suitable targets
targets <- market_data %>%
filter(position_group == pos) %>%
filter(market_value <= position_budget) %>%
filter(age <= 28) %>% # Reasonable age limit
arrange(desc(score)) %>%
head(5)
priorities[[pos]] <- list(
priority_rank = i,
severity = severity,
budget_allocation = position_budget,
top_targets = targets
)
}
return(list(
total_budget = total_budget,
gap_priorities = gap_priorities,
position_priorities = priorities
))
}
# Format transfer plan
format_transfer_plan <- function(priorities) {
cat(sprintf("\n=== TRANSFER PRIORITY PLAN ===\n"))
cat(sprintf("Total Budget: €%.1fM\n\n", priorities$total_budget))
cat("POSITION PRIORITIES:\n")
cat(paste(rep("-", 40), collapse = ""), "\n")
for (pos in names(priorities$position_priorities)) {
p <- priorities$position_priorities[[pos]]
cat(sprintf("\n%d. %s [%s]\n", p$priority_rank, pos, p$severity))
cat(sprintf(" Budget: €%.1fM\n", p$budget_allocation))
if (nrow(p$top_targets) > 0) {
cat(" Top Targets:\n")
for (j in 1:min(3, nrow(p$top_targets))) {
t <- p$top_targets[j, ]
cat(sprintf(" - %s (%s, %d) - €%.1fM\n",
t$player, t$team, t$age, t$market_value))
}
}
}
}Practice Exercises
Using FBref data, conduct a complete season review for a top-5 league club. Include results analysis, xG performance, monthly trends, and comparison to pre-season objectives.
Analyze a club's squad depth, age profile, and contract situations. Identify the top 3 positions needing reinforcement and justify your prioritization with data.
Given a €50M budget and identified squad gaps, create a complete transfer plan. Identify 3-5 realistic targets per position, estimate feasibility, and build a prioritized shortlist with alternatives.
Summary
Key Takeaways
- Structured framework: Season reviews cover results, performance, squad, and planning
- xG analysis: Compare actual results to expected to assess luck vs skill
- Squad depth: Quantify depth by position to identify vulnerability
- Age profiling: Balance youth development with experience
- Data-driven priorities: Let squad gaps and budget inform transfer targets
Season Review Timeline
| Week 1-2 | Results analysis, xG review, monthly breakdown |
|---|---|
| Week 2-3 | Individual player reviews, development assessments |
| Week 3-4 | Squad gap analysis, depth assessment |
| Week 4-5 | Transfer priorities, target identification |
| Week 5-6 | Board presentation, budget allocation |