Squad Audit
Under the hood
The Squad Audit feature is something I built entirely as an excuse to have some radar charts. My love of radar charts dates back to far too many hours lost to Pro Evolution Soccer when I was younger. The player rating radar charts in the PES series were pretty simplistic, with every player having a point on the chart for things like Attack, Defence, Technique, Speed and Teamwork, with no differentiation between positions.
I wanted to have more positionally-aware charts, which required some thought about how to declare the types. The database schema defines some stats that are required by all players - things like name and position, but also strength, stamina, composure, and a few others that are important for every position. As a very basic side project, I value simplicity in my tables, so rather than having tables per position that joined to a table with the required stats, I just have all the other fields as optional. As I actually got round to putting some thought to the charts, this meant I had to think about how to handle the fact that the chart only knows that some fields will be present, and that other fields have to come from the optional fields.
To achieve this, I defined a PlayerAuditBase interface and a PlayerAuditOptional interface like so:
export interface PlayerAuditBase {
id: string;
name: string;
position: string;
strength: number;
stamina: number;
pace: number;
acceleration: number;
composure: number;
}
export interface PlayerAuditOptional {
// gk specific
handling?: number;
shotstopping?: number;
presence?: number;
distribution?: number;
// def specific
tackling?: number;
marking?: number;
positioning?: number;
anticipation?: number;
// mid specific
creativity?: number;
flair?: number;
// shared by some positions
finishing?: number;
longShots?: number;
output?: number; // e.g. goals + assists
passing?: number;
dribbling?: number;
heading?: number;
crossing?: number;
pressing?: number;
penalties?: number;
setPieces?: number;
leadership?: number;
}
The type then becomes:
export type PlayerAudit = PlayerAuditBase & Partial<PlayerAuditOptional>;
With the types sorted, the fun part is turning the verbose list of stats into something that looks reasonable for the player and makes sense as a set of stats for the position. While I'm sure AI would do a spectacular job of this, for a little hobby project I prefer tweaking it myself until it feels roughly right. For each stat that appears on the chart for a player, there's a list of relevant base stats, with each one given a weighting to designate how important it is in the calculation of the chart statistic.
For example, here's how the "Technical" stat, applied to all outfield players, is weighted:
technical: [
{ label: "creativity", weight: 3 },
{ label: "flair", weight: 2 },
{ label: "passing", weight: 4 },
{ label: "dribbling", weight: 5 },
{ label: "crossing", weight: 1 },
{ label: "setPieces", weight: 3 },
],
In this case, the most important base stat for calculating the Technical stat is dribbling, which is afforded 5 times more weight than crossing. The function that calculates the final stat based on the weighting looks like this (note the typing of the weight label as keyof PlayerStats):
const calculateWeightedStat = (
stats: Partial<PlayerStats>,
weights: Statweight[]
): number => {
const totalWeight = weights.reduce((sum, w) => sum + w.weight, 0);
const weightedSum = weights.reduce((sum, w) => {
const statValue = stats[w.label as keyof PlayerStats] ?? 0;
return sum + statValue * w.weight;
}, 0);
return Math.round(weightedSum / totalWeight);
};
So pulling that all together, here's how it looks for myself, using my own 5-a-side performances as a baseline...
Nick Holvast
Oh dear.
