Team Style Profiles
Summary
All other analysis pages answer "how good is this team?" (Elo, xPts, SQI). Style profiles answer "what kind of team is this?" by building per-team-season style vectors from ~20 stored-but-unused stats that describe how a team plays rather than how well they perform.
The feature lives at /analysis/style with two visualizations:
- PCA scatter showing every team-season as a dot in 2D style space, with K-means cluster labels identifying playing archetypes
- Radar charts showing each team's 5-dimension style fingerprint
Dimensions
Each dimension aggregates related per-game averages into a single composite z-score. Individual stat z-scores are also exposed.
Pressing Height
How high up the pitch a team wins the ball back.
| Stat key | Direction | Description |
|---|---|---|
possWonAtt3rd |
+ | Possession won in the attacking third |
possWonMid3rd |
+ | Possession won in the middle third |
possWonDef3rd |
inverted | Possession won in the defensive third |
A team that wins the ball back high presses aggressively. A team that wins it deep plays a low block.
Directness
How quickly and directly the team moves the ball forward.
| Stat key | Direction | Description |
|---|---|---|
totalLongBalls |
+ | Total long balls attempted |
accurateLongBalls |
+ | Accurate long balls |
fwdPass |
+ | Forward passes |
totalFinalThirdPasses |
+ | Passes in the final third |
longPassOwnToOpp |
+ | Long passes from own to opponent half |
backwardPass |
inverted | Backward passes |
High directness = a team that plays forward quickly with long balls. Low directness = patient, backward-passing buildup play.
Width / Penetration
How much a team uses the flanks and how often it enters dangerous areas.
| Stat key | Direction | Description |
|---|---|---|
leftsidePass |
+ | Passes on the left side |
rightsidePass |
+ | Passes on the right side |
totalCross |
+ | Crosses attempted |
accurateCross |
+ | Accurate crosses |
finalThirdEntries |
+ | Entries into the final third |
penAreaEntries |
+ | Entries into the penalty area |
Defensive Activity
How active the team is defensively (tackling, challenging).
| Stat key | Direction | Description |
|---|---|---|
totalTackle |
+ | Total tackles |
challengeLost |
+ | Challenges lost (high = aggressive but imprecise) |
possLostAll |
inverted | Total possession lost |
possLostCtrl |
inverted | Controlled possession lost |
The inversions on possession loss mean that a team which loses the ball a lot scores lower on defensive activity (they're being dispossessed, not actively defending).
Ball Retention
How well the team keeps the ball.
| Stat key | Direction | Description |
|---|---|---|
openPlayPass |
+ | Open-play passes |
possessionPercentage |
+ | Possession percentage |
dispossessed |
inverted | Times dispossessed |
Normalization
Stats are z-scored within each season. For each stat key, the mean and standard deviation are computed across all teams in that season, and each team's value is transformed to a z-score:
z = (value - season_mean) / season_std
When standard deviation is zero (all teams identical on a stat), the z-score is set to 0.
Within-season normalization means a team is compared to its contemporaries, not to teams from different eras. This handles secular drift in league-wide stats (e.g. if all teams pass more in 2025 than 2019, a 2025 team with average passing doesn't get inflated on the retention dimension).
Minimum matches filter: team-seasons with fewer than 5 matches are excluded to avoid noisy per-game averages. This primarily affects teams that join or leave mid-season.
Dimension Score
Each dimension's composite score is the mean of its constituent z-scores (with inversions applied). A dimension score of +1.0 means the team is roughly one standard deviation above the league average on that dimension. Zero means league average.
PCA
Principal Component Analysis reduces the 5 dimension scores to 2 coordinates for the scatter plot. PCA runs on the full matrix of ALL qualifying team-seasons (across all years), not per-season, so that the 2D projection is globally consistent.
PC1 typically captures the largest source of style variation in the league (often a possession-vs-directness axis). PC2 captures the next largest orthogonal variation.
The variance explained by each component is stored and displayed on the chart axes (e.g. "PC1 (69%)").
Instability note: PCA coordinates change when the dataset changes (new season, updated match data). The scatter plot is exploratory, not a fixed coordinate system. Team-season positions relative to each other are more meaningful than absolute coordinates.
Clustering
K-means with k=4 groups team-seasons into playing archetypes.
random_state=42 ensures reproducibility given the same data.
Cluster labels are assigned heuristically from the cluster centroids: the dimension with the highest absolute centroid value names the cluster. A secondary modifier is added when another dimension also has a notable signal (absolute value > 0.3).
Examples of cluster labels:
- "High Press / Wide" (pressing is the strongest signal, width is also notable)
- "Possession" (retention dominates)
- "Direct / Aggressive" (directness dominates, defensive activity is also high)
Stability note: like PCA, cluster assignments can shift when new data is added. Labels are descriptive, not prescriptive. A team switching clusters between runs does not mean it changed style; it may mean the cluster boundary moved.
Data
- Source:
team_season_stats.averages_json(precomputed per-game averages fromcompute_team_stats.py) - Time range: 2019-2025, all matches with match_stats data
- Minimum: 5 matches per team-season
- Total rows: ~55 team-seasons across 7 years
Failure Modes
- Small sample per season. Only 7-8 teams per season means z-scores have high variance. A single team's outlier performance can shift the league mean significantly.
- Stats are counts, not spatial.
totalLongBallscounts long balls but says nothing about where they land or whether they create chances. Two teams with 45 long balls per game could be playing very differently. - No formation context.
formationUsedis stored but not included in v1 (it's a string, not numeric, and requires separate per-match handling). - Clustering is exploratory. With 55 data points and 5 dimensions, K-means with k=4 is a rough grouping, not a statistically validated taxonomy. The labels are convenient shorthands, not rigorous classifications.
- Cross-season comparison is limited. Because z-scores are within-season, you cannot directly compare a 2019 team's style score to a 2024 team's. The PCA projection allows visual comparison across seasons, but the underlying z-scores are relative to different baselines.
What Would Make It Better
- Raw per-game rates mode. An alternative normalization using raw per-game averages instead of z-scores, enabling direct cross-season comparison. Trade-off: susceptible to secular drift.
- Formation analysis.
formationUsedis stored per match. Win rate by formation, most common formation per team-season, formation as a categorical dimension in the style profile. - UMAP alternative. Better cluster separation than PCA but harder to interpret and non-deterministic without careful seeding.
- Interactive radar overlay. Select two teams to overlay their radars for direct comparison.
- Style evolution timeline. Show how a team's style dimensions change across seasons as a small-multiples line chart.
- Style matchup integration. Do certain style combinations predict match outcomes? E.g. high-press teams vs possession teams. Could feed into the match prediction model.