nba_tree <- rpart(
top_50 ~ pos + fg + fgp + thr + thrp + efg + trb + ast + stl + blk + tov + pf,
data = nba_training,
method = "class"
)Classification and Clustering Analysis
Introduction
This report applies classification and clustering techniques to two sports datasets using R and Quarto. In Part A, classification tree and binary logistic regression models are used to predict whether an NBA player is included in ESPN’s Top 50 players. In Part B, hierarchical clustering and K-means clustering are used to identify groups of FIFA players based on selected performance attributes. The report presents the key outputs, visualisations, and interpretations for each method.
NBA Classification
fancyRpartPlot(nba_tree, sub = "")var_imp_tree <- tibble(
Variable = names(nba_tree$variable.importance),
Importance = as.numeric(nba_tree$variable.importance)
) |>
arrange(desc(Importance)) |>
mutate(Importance = round(Importance, 4))
var_imp_tree |>
gt() |>
tab_header(
title = "Variable importance for the classification tree"
) |>
cols_align(
align = "center",
columns = everything()
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(420),
table.font.size = px(16),
data_row.padding = px(3)
)| Variable importance for the classification tree | |
| Variable | Importance |
|---|---|
| fg | 16.3355 |
| thr | 6.1996 |
| tov | 5.4452 |
| pf | 3.5572 |
| blk | 2.8196 |
| trb | 2.7911 |
| pos | 1.9380 |
| ast | 1.8660 |
| thrp | 1.3761 |
| stl | 1.0882 |
| fgp | 0.8043 |
#| results: asis
cat("<div style='height: 40px;'></div>")<div style='height: 40px;'></div>
Training Dataset Accuracy
train_pred <- predict(nba_tree, newdata = nba_training, type = "class")
train_tab <- table(Actual = nba_training$top_50, Predicted = train_pred)
train_tab Predicted
Actual N Y
N 91 8
Y 9 22
train_accuracy <- sum(diag(train_tab)) / sum(train_tab)
cat("Training accuracy =", round(train_accuracy * 100, 1), "%")Training accuracy = 86.9 %
Testing Dataset Accuracy
test_pred <- predict(nba_tree, newdata = nba_testing, type = "class")
test_tab <- table(Actual = nba_testing$top_50, Predicted = test_pred)
test_tab Predicted
Actual N Y
N 48 4
Y 3 11
test_accuracy <- sum(diag(test_tab)) / sum(test_tab)
cat("Testing accuracy =", round(test_accuracy * 100, 1), "%")Testing accuracy = 89.4 %
Interpretation of the Classification Tree
Rule for Predicting a Top 50 Player
The rule for predicting that a player is classified as being in the Top 50 is when the player has at least 7.1 field goals made per game (fg≥7.1). In the classification tree, the first split is fg<7.1, so players who do not satisfy this condition go to the right-hand terminal leaf, which predicts Y (Top 50). The leaf has class probabilities of 0.19 for N and 0.81 for Y, meaning that 81% of players in this leaf were Top 50 players. This indicates a fairly pure Top 50 leaf.
Rule for Predicting a Player Outside the Top 50
One rule for predicting that a player is classified as being outside the Top 50 is when the player has fewer that 7.1 field goals made per game (fg<7.1), fewer than 2.3 three-pointers made per game (thr<2.3) and a total rebound percentage below 6.4 (trb<6.4). This path leads to a terminal leaf that predicts N (not Top 50). The leaf has a class probabilities of 0.99 for N and 0.01 for Y, meaning that 99% of players in this leaf were not Top 50 players. Therefore, this indicates a very pure leaf.
Most Important Variables in the Classification Tree
The most important variable in the classification tree was field goals made per game (fg), with a variable importance value of 16.3355244. The next most important variables were three-pointers made per game (thr), with an importance value of 6.1996468 and turnovers (tov) with a value of 5.4451748. Other variables that contributed to the model included personal fouls (pf)= 3.5572075, blocks (blk)= 2.8196389, total rebound percentage (trb)= 2.7911430, position (pos)= 1.9380366, assists (ast)= 1.8660357, three-point percentage (thrp)=1.3760600, steal (stl)=1.0881536 and field goal percentage (fgp)=0.8043276. Overall, this suggests that field goal scoring, three-point scoring and turnovers were the strongest contributors to whether a player was classified as being inside or outside ESPN’s Top 50.
Accuracy of the Classification Tree
The classification tree was assessed using both the training and testing datasets.
For the training dataset, the confusion matrix showed that:
91 players who were actually not in the Top 50 were correctly classified as N
22 players who were actually in the Top 50 were correctly classified as Y
8 players were incorrectly classified as Top 50 when they were not
9 players were incorrectly classified as not Top 50 when they were actually in the Top 50
This gave an overall training accuracy of 86.9%.
For the testing dataset, the confusion matrix showed that:
48 players who were actually not in the Top 50 were correctly classified as N
11 players who were actually in the Top 50 were correctly classified as Y
4 players were incorrectly classified as Top 50 when they were not
3 players were incorrectly classified as not Top 50 when they were actually in the Top 50
This gave an overall testing accuracy of 89.4%.
Overall, the classification tree performed well on both datasets, with similar levels of accuracy for the training and testing data.
Overfitting
Based on these results, there is no clear evidence that the classification tree is overfitting the training dataset. If the model was overfitting, we would normally expect it to perform much better on the training data than on the testing data, because it would be learning patterns that only fit the training sample and do not work as well on new data. In this case, the training accuracy was 86.9%, while the testing accuracy was slightly higher at 89.4%. As the model performed similarly on both datasets, it does not appear to lose accuracy when applied to unseen data. This suggests that the classification tree generalises well rather than overfitting.
Binary Logistic Regression Method
logit_table |>
gt() |>
tab_header(
title = "Binary Logistic Regression Coefficients"
) |>
cols_label(
term = "Predictor",
estimate = "Estimate",
std.error = "Std. Error",
statistic = "z value",
p.value = "p-value",
significance = "Significance"
) |>
cols_align(
align = "left",
columns = term
) |>
cols_align(
align = "center",
columns = c(estimate, std.error, statistic, p.value, significance)
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(700),
table.font.size = px(16),
data_row.padding = px(3)
)| Binary Logistic Regression Coefficients | |||||
| Predictor | Estimate | Std. Error | z value | p-value | Significance |
|---|---|---|---|---|---|
| (Intercept) | -18.6844 | 7.3431 | -2.5445 | 0.0109 | * |
| posPF | -2.1358 | 1.3850 | -1.5421 | 0.1230 | |
| posPG | -1.8758 | 2.0854 | -0.8995 | 0.3684 | |
| posSF | -0.6826 | 1.6757 | -0.4074 | 0.6837 | |
| posSG | -0.3048 | 1.7937 | -0.1699 | 0.8651 | |
| fg | 1.1205 | 0.4981 | 2.2498 | 0.0245 | * |
| fgp | 16.4680 | 41.3104 | 0.3986 | 0.6902 | |
| thr | 2.3870 | 1.5595 | 1.5306 | 0.1259 | |
| thrp | -4.5043 | 5.9837 | -0.7528 | 0.4516 | |
| efg | -0.5173 | 41.2340 | -0.0125 | 0.9900 | |
| trb | 0.1575 | 0.2460 | 0.6404 | 0.5219 | |
| ast | 0.5552 | 0.4049 | 1.3712 | 0.1703 | |
| stl | 1.3078 | 1.1259 | 1.1616 | 0.2454 | |
| blk | 1.3646 | 0.9495 | 1.4372 | 0.1507 | |
| tov | -1.7518 | 1.1043 | -1.5864 | 0.1127 | |
| pf | 0.2102 | 0.9898 | 0.2124 | 0.8318 | |
Interpretation of the Logistic Regression Model
Preparation of the Response Variable
The response variable top_50 was converted to a factor and the levels were ordered as N followed by Y. This is important in binary logistic regression because the model predicts the probability of the second level being the outcome of interest. In this case, ordering the levels as N and Y means that the model predicts the probability that a player is in ESPN’s Top 50.
Regression Equation
\[ \begin{aligned} \log\left(\frac{\pi}{1-\pi}\right) =&\,-18.6844 - 2.1358(\text{posPF}) - 1.8758(\text{posPG}) - 0.6826(\text{posSF}) \\ &- 0.3048(\text{posSG}) + 1.1205(\text{fg}) + 16.4680(\text{fgp}) + 2.3870(\text{thr}) \\ &- 4.5043(\text{thrp}) - 0.5173(\text{efg}) + 0.1575(\text{trb}) + 0.5552(\text{ast}) \\ &+ 1.3078(\text{stl}) + 1.3646(\text{blk}) - 1.7518(\text{tov}) + 0.2102(\text{pf}) \end{aligned} \]
where \(\pi\) represents the probability that a player is in ESPN’s Top 50.
Important Predictor Variables
Based on the logistic regression output, the only predictor variable that was statistically significant at the 5% level was field goals made per game (fg), with a p-value of 0.0245. Since this p-value is less than 0.05, fg can be considered an important predictor of whether a player is classified as being in ESPN’s Top 50. All other predictor variables had p-values greater than 0.05 and were therefore not statistically significant in this model.
Impact on the Odds
For the significant predictor variable field goals made per game (fg), the coefficient in the logistic regression model was 1.1205. Taking the exponential of this coefficient gives:
\(e^{1.1205} \approx 3.07\)
This means that for every 1 additional field goal made per game, the odds of a player being in ESPN’s Top 50 are multiplied by approximately 3.07, assuming that all other variables remain constant. In percentage terms, this corresponds to an increase of about 207% in the odds of being in the Top 50. Therefore, field goals made per game had a strong positive effect on the likelihood of a player being classified as a Top 50 player.
Accuracy of the Logistic Regression Model
For the training dataset, the logistic regression model correctly classified 94 players who were not in the Top 50 and 25 players who were in the Top 50. The overall training accuracy was 91.5%.
For the testing dataset, the model correctly classified 50 players who were not in the Top 50 and 10 players who were in the Top 50. The overall testing accuracy was 90.9%.
Overall, the logistic regression model performed very well on both datasets, with very similar levels of accuracy for the training and testing data. This suggests that the model generalised well to unseen data and did not show a noticeable drop in performance when applied to the testing dataset.
Training Dataset Accuracy
train_pi <- predict(nba_logit, newdata = nba_training, type = "response")
train_logit_final <- nba_training |>
mutate(
pi = train_pi,
train_logit_pred = case_when(
pi >= 0.5 ~ "Y",
pi < 0.5 ~ "N"
)
)
train_logit_final$train_logit_pred <- factor(train_logit_final$train_logit_pred, levels = c("N", "Y"))
train_logit_tab <- table(
Actual = train_logit_final$top_50,
Predicted = train_logit_final$train_logit_pred
)
train_logit_tab Predicted
Actual N Y
N 94 5
Y 6 25
train_logit_acc <- sum(diag(train_logit_tab)) / sum(train_logit_tab)
cat("Training accuracy =", round(train_logit_acc * 100, 1), "%")Training accuracy = 91.5 %
Testing Dataset Accuracy
test_pi <- predict(nba_logit, newdata = nba_testing, type = "response")
test_logit_final <- nba_testing |>
mutate(
pi = test_pi,
test_logit_pred = case_when(
pi >= 0.5 ~ "Y",
pi < 0.5 ~ "N"
)
)
test_logit_final$test_logit_pred <- factor(test_logit_final$test_logit_pred, levels = c("N", "Y"))
test_logit_tab <- table(
Actual = test_logit_final$top_50,
Predicted = test_logit_final$test_logit_pred
)
test_logit_tab Predicted
Actual N Y
N 50 2
Y 4 10
test_logit_acc <- sum(diag(test_logit_tab)) / sum(test_logit_tab)
cat("Testing accuracy =", round(test_logit_acc * 100, 1), "%")Testing accuracy = 90.9 %
Model Accuracy Comparison
The binary logistic regression model was slightly more accurate than the classification tree model. The classification tree achieved an accuracy of 86.9% on the training dataset and 89.4% on the testing dataset. In comparison, the binary logistic regression model achieved an accuracy of 91.5% on the training dataset and 90.9% on the testing dataset. As the logistic regression model produced the higher accuracy on both datasets, it can be considered the more accurate of the two models in this analysis.
Comparison of Important Predictor Variables
The two models identified some common patterns, but they differed in how they assessed variable importance. In the classification tree model, the most important predictor was field goals made per game (fg), with a variable importance value of 16.3355. This was followed by three-pointers made per game (thr), with an importance value of 6.1996, and turnovers (tov), with an importance value of 5.4452. Other variables, such as personal fouls (pf), blocks (blk), and total rebound percentage (trb), also contributed to the tree, although to a lesser extent.
In the binary logistic regression model, the only predictor variable that was statistically significant at the 5% significance level was field goals made per game (fg), with a p-value of 0.0245. All other predictors had p-values greater than 0.05 and were therefore not statistically significant in the model.
This suggests that both models agreed on the importance of field goals made per game as the key predictor of whether a player was classified as being in ESPN’s Top 50. However, the classification tree indicated that a broader set of performance variables contributed to the classification, whereas the logistic regression model was more selective and identified only fg as a statistically significant predictor.
Clustering Soccer Players
Data
fifa |>
select(acceleration, ball_control, dribbling, shot_power, short_passing, sprint_speed) |>
slice(1:6) |>
gt() |>
tab_header(
title = "Selected FIFA Attributes Used for Clustering"
) |>
cols_align(
align = "center",
columns = everything()
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
opt_row_striping() |>
tab_options(
table.width = px(700),
table.font.size = px(16),
data_row.padding = px(3),
row.striping.background_color = "#F5F9FF",
table.border.top.color = "black",
table.border.bottom.color = "black",
heading.border.bottom.color = "black",
column_labels.border.top.color = "black",
column_labels.border.bottom.color = "black",
table_body.hlines.color = "black",
table_body.vlines.color = "black"
)| Selected FIFA Attributes Used for Clustering | |||||
| acceleration | ball_control | dribbling | shot_power | short_passing | sprint_speed |
|---|---|---|---|---|---|
| 94 | 95 | 96 | 80 | 81 | 90 |
| 92 | 95 | 97 | 85 | 88 | 87 |
| 88 | 91 | 86 | 87 | 83 | 77 |
| 89 | 93 | 91 | 94 | 83 | 91 |
| 79 | 89 | 85 | 88 | 83 | 83 |
| 93 | 92 | 93 | 79 | 86 | 87 |
Scaling of the Clustering Variables
The data does not appear to need scaling in this case. According to the assignment brief, the FIFA performance attributes are scored on a 1–100 scale and the six variables selected for clustering are all taken from these performance ratings. Because the variables are already measured on the same scale, it is reasonable to use the original values when calculating the Euclidean distance matrix and when applying the K-means algorithm. Scaling is generally more important when variables are measured in different units or have very different ranges, as this can cause some variables to dominate the clustering solution. In this case, that issue should not arise, so I would not scale the data before carrying out either hierarchical clustering or K-means clustering.
Hierarchical Clustering
fifa_cluster_vars <- fifa |>
select(acceleration, ball_control, dribbling, shot_power, short_passing, sprint_speed)
d_fifa <- dist(fifa_cluster_vars, method = "euclidean")dist_snippet <- as.matrix(d_fifa)[1:6, 1:6] |>
as.data.frame()
dist_snippet |>
tibble::rownames_to_column("Player") |>
gt() |>
fmt(
columns = -Player,
fns = function(x) {
ifelse(round(x, 2) == 0, "0", sprintf("%.2f", round(x, 2)))
}
) |>
tab_header(
title = "Snippet of the Euclidean Distance Matrix for the FIFA Players"
) |>
cols_align(
align = "center",
columns = everything()
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(720),
table.font.size = px(14),
data_row.padding = px(3)
)| Snippet of the Euclidean Distance Matrix for the FIFA Players | ||||||
| Player | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 1 | 0 | 9.38 | 19.34 | 15.97 | 22.34 | 7.35 |
| 2 | 9.38 | 0 | 16.79 | 13.08 | 19.97 | 8.12 |
| 3 | 19.34 | 16.79 | 0 | 16.58 | 11.09 | 15.75 |
| 4 | 15.97 | 13.08 | 16.58 | 0 | 15.87 | 16.46 |
| 5 | 22.34 | 19.97 | 11.09 | 15.87 | 0 | 19.36 |
| 6 | 7.35 | 8.12 | 15.75 | 16.46 | 19.36 | 0 |
plot(h_fifa, hang = -1, labels = FALSE, main = "Dendrogram of FIFA Players", xlab = "", sub = "")The dendrogram provides a visual summary of the hierarchical clustering of the FIFA players based on the six selected performance attributes. The length of the vertical lines represents the height or distance at which clusters are merged. Therefore, players that are joined together at lower heights are more similar to each other, while players or groups that merge at greater heights are less similar. The dendrogram suggests that there may be some grouping structure within the data, although this will be assessed more clearly using the heatmap and the 4-cluster solution.
Heatmap Interpretation
heatmap(
as.matrix(d_fifa),
Rowv = as.dendrogram(h_fifa),
Colv = "Rowv",
labRow = FALSE,
labCol = FALSE
)The heatmap shows the Euclidean distance matrix for the FIFA players, with the rows and columns arranged according to the hierarchical clustering result. In a heatmap like this, evidence of clustering is usually seen as lighter coloured blocks around the diagonal. In this case, there do appear to be some lighter regions along the diagonal, which suggests that there may be some clustering structure in the data. However, the pattern is not very clear or sharply separated, probably because the dataset contains a large number of players. For that reason, I would say the heatmap provides some evidence of clustering structure, but the structure does not appear to be especially strong.
4-Cluster Solution and Cluster Quality
sil_hier <- silhouette(clusters_hier, d_fifa)
sil_sum <- summary(sil_hier)
sil_table <- tibble(
Cluster = paste0("Cluster ", 1:4),
Size = as.numeric(sil_sum$clus.sizes),
Average_Silhouette_Width = round(as.numeric(sil_sum$clus.avg.widths), 4)
)
sil_table |>
gt() |>
tab_header(
title = "Silhouette Results for the 4-Cluster Hierarchical Solution"
) |>
cols_label(
Cluster = "Cluster",
Size = "Size",
Average_Silhouette_Width = "Average Silhouette Width"
) |>
cols_align(
align = "center",
columns = everything()
) |>
cols_width(
Cluster ~ px(180),
Size ~ px(120),
Average_Silhouette_Width ~ px(250)
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(650),
table.font.size = px(16),
data_row.padding = px(3)
)| Silhouette Results for the 4-Cluster Hierarchical Solution | ||
| Cluster | Size | Average Silhouette Width |
|---|---|---|
| Cluster 1 | 492 | 0.3059 |
| Cluster 2 | 193 | 0.2883 |
| Cluster 3 | 107 | 0.6946 |
| Cluster 4 | 208 | 0.0823 |
The quality of the 4-cluster hierarchical clustering solution was assessed using silhouette scores. The overall mean silhouette score was 0.2976, which suggests that the clustering structure is relatively weak overall. This indicates that while some grouping is present in the data, the separation between clusters is not especially strong.
Looking at the individual clusters, Cluster 3 had the highest average silhouette width (0.6946), indicating that it was the most clearly defined cluster in the solution. In contrast, Clusters 1 and 2 had lower average silhouette widths (0.3059 and 0.2883, respectively), suggesting weaker clustering structure. Cluster 4 had the lowest average silhouette width (0.0823), which indicates that this cluster was not well separated from the others. Overall, the 4-cluster solution appears to capture some meaningful structure, but the clustering quality is uneven across the four clusters.
Cluster Performance Profile
hier_perf_profile |>
gt() |>
tab_header(
title = "Average Performance of Each Hierarchical Cluster"
) |>
cols_label(
Hier_Cluster = "Cluster",
acceleration = "Acceleration",
ball_control = "Ball Control",
dribbling = "Dribbling",
shot_power = "Shot Power",
short_passing = "Short Passing",
sprint_speed = "Sprint Speed"
) |>
cols_align(
align = "center",
columns = everything()
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(820),
table.font.size = px(13),
data_row.padding = px(3)
)| Average Performance of Each Hierarchical Cluster | ||||||
| Cluster | Acceleration | Ball Control | Dribbling | Shot Power | Short Passing | Sprint Speed |
|---|---|---|---|---|---|---|
| 1 | 80.84 | 81.08 | 80.42 | 76.97 | 77.89 | 80.10 |
| 2 | 65.15 | 79.02 | 73.90 | 77.24 | 79.44 | 65.21 |
| 3 | 48.50 | 23.71 | 16.11 | 25.06 | 33.05 | 49.23 |
| 4 | 57.02 | 66.33 | 55.82 | 63.29 | 70.33 | 60.78 |
Cluster Demographic and Financial Profile
hier_demo_profile |>
gt() |>
tab_header(
title = "Average Age, Value and Wage of Each Hierarchical Cluster"
) |>
cols_label(
Hier_Cluster = "Cluster",
age = "Age",
value = "Club Value (€)",
wage = "Wage (€)"
) |>
fmt_number(
columns = age,
decimals = 2
) |>
fmt_currency(
columns = c(value, wage),
currency = "EUR",
decimals = 0
) |>
cols_align(
align = "center",
columns = everything()
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(750),
table.font.size = px(15),
data_row.padding = px(3)
)| Average Age, Value and Wage of Each Hierarchical Cluster | |||
| Cluster | Age | Club Value (€) | Wage (€) |
|---|---|---|---|
| 1 | 26.31 | €21,355,081 | €79,226 |
| 2 | 28.07 | €16,098,446 | €65,772 |
| 3 | 29.10 | €14,350,935 | €51,766 |
| 4 | 28.03 | €13,387,500 | €58,260 |
Graphical Profile of the Hierarchical Clusters
ggplot(hier_perf_long, aes(x = Attribute, y = Average_Value, color = Hier_Cluster, group = Hier_Cluster)) +
geom_line(linewidth = 1) +
geom_point(size = 2) +
labs(
title = "Average Performance Attributes by Hierarchical Cluster",
x = "Attribute",
y = "Average Value",
color = "Cluster"
) +
scale_x_discrete(labels = c(
acceleration = "Acceleration",
ball_control = "Ball\nControl",
dribbling = "Dribbling",
shot_power = "Shot\nPower",
short_passing = "Short\nPassing",
sprint_speed = "Sprint\nSpeed"
)) +
theme_minimal() +
theme(
axis.text.x = element_text(size = 10),
plot.title = element_text(face = "bold")
)Interpretation of the Hierarchical Cluster Profiles
Performance Attributes
The four hierarchical clusters show clear differences in their average performance across the six FIFA attributes. Cluster 1 had the strongest overall profile, with the highest or near-highest average values across all six attributes. In particular, it had the highest averages for acceleration (80.84), ball control (81.08), dribbling (80.42), and sprint speed (80.10). This suggests that Cluster 1 contains the most technically strong and athletically explosive players in the dataset.
Cluster 2 also performed well, but its profile was slightly less balanced than Cluster 1. It had strong averages for ball control (79.02), dribbling (73.90), shot power (77.24) and short passing (79.44), although it was clearly lower than Cluster 1 in acceleration (65.15) and sprint speed (65.21). This suggests that Cluster 2 includes players with good technical quality, but less pace than the players in Cluster 1.
Cluster 3 had the weakest overall profile by a large margin. It recorded the lowest average values across all six attributes, including ball control (23.71), dribbling (16.11), shot power (25.06), and short passing (33.05). This indicates that Cluster 3 contains players who are much weaker than the other clusters on the selected performance variables.
Cluster 4 appears to sit between Clusters 2 and 3. Its average values were moderate across the six variables, with acceleration (57.02), ball control (66.33), dribbling (55.82), shot power (63.29), short passing (70.33) and sprint speed (60.78). This suggests that Cluster 4 contains players with a reasonable but not outstanding performance profile.
Overall, the performance profile suggests a clear ranking across the clusters. Cluster 1 appears to represent the strongest all-round players, Cluster 2 strong technical players with lower pace, Cluster 4 moderate players and Cluster 3 the weakest group on the selected attributes.
Age, Club Value and Wage
The clusters also differ in terms of age, club value, and wage. Cluster 1 had the youngest average age (26.31) and also the highest average club value (€21,355,081) and wage (€79,226). This suggests that the strongest-performing cluster also contains the most valuable and highest-paid players.
Cluster 2 had an average age of 28.07, with a lower average club value (€16,098,446) and wage (€65,772) than Cluster 1. This indicates that although Cluster 2 players still appear to be high-level players, they are less valuable and less highly paid than those in Cluster 1.
Cluster 3 had the oldest average age (29.10) and relatively low average club value (€14,350,935) and wage (€51,766). This fits with its weak performance profile, suggesting that this cluster contains older players who may be less effective on the selected technical and physical attributes.
Cluster 4 had an average age of 28.03 and the lowest average club value (€13,387,500), although its average wage (€58,260) was slightly higher than that of Cluster 3. This suggests that Cluster 4 contains players with a moderate playing profile but relatively limited market value compared with Clusters 1 and 2.
Overall, the financial and age profile broadly matches the performance results. The highest-performing cluster, Cluster 1, is also the youngest, most valuable, and highest paid, while the weakest-performing clusters tend to be older and less financially valuable. That suggests the clustering solution is capturing meaningful differences not only in performance attributes, but also in player status and market value.
K-means Clustering
K-means Cluster Quality
knitr::kable(
sil_kmeans_table,
col.names = c("Cluster", "Size", "Average Silhouette Width"),
align = c("c", "c", "c"),
format = "html"
) |>
kableExtra::kable_styling(full_width = FALSE) |>
kableExtra::row_spec(0, bold = TRUE, background = "#DCE9F9") |>
kableExtra::add_header_above(
c("Silhouette Results for the 4-Cluster K-means Solution" = 3),
background = "#7EA6E0",
bold = TRUE
)| Cluster | Size | Average Silhouette Width |
|---|---|---|
| Cluster 1 | 174 | 0.1777 |
| Cluster 2 | 431 | 0.3803 |
| Cluster 3 | 289 | 0.2178 |
| Cluster 4 | 106 | 0.6883 |
The overall mean silhouette score for the 4-cluster K-means solution was 0.3307, which suggests that the clustering structure is weak overall, although it appears slightly stronger than the hierarchical clustering solution. Looking at the individual clusters, Cluster 4 had the strongest average silhouette width, while Cluster 1 had the weakest. Overall, the K-means solution appears to identify some meaningful structure, but the separation between clusters is not especially strong.
K-means Cluster Performance Profile
kmeans_perf_profile |>
gt() |>
tab_header(
title = "Average Performance of Each K-means Cluster"
) |>
cols_label(
Kmeans_Cluster = "Cluster",
acceleration = "Acceleration",
ball_control = "Ball Control",
dribbling = "Dribbling",
shot_power = "Shot Power",
short_passing = "Short Passing",
sprint_speed = "Sprint Speed"
) |>
cols_align(
align = "center",
columns = everything()
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(820),
table.font.size = px(13),
data_row.padding = px(3)
)| Average Performance of Each K-means Cluster | ||||||
| Cluster | Acceleration | Ball Control | Dribbling | Shot Power | Short Passing | Sprint Speed |
|---|---|---|---|---|---|---|
| 1 | 58.17 | 64.53 | 53.31 | 60.33 | 68.73 | 62.16 |
| 2 | 82.06 | 81.31 | 81.06 | 77.12 | 77.85 | 81.36 |
| 3 | 65.00 | 78.60 | 73.58 | 76.91 | 78.98 | 65.10 |
| 4 | 48.31 | 23.44 | 15.89 | 25.07 | 32.85 | 49.18 |
K-means Performance Graph
ggplot(kmeans_perf_long, aes(x = Attribute, y = Average_Value, color = Kmeans_Cluster, group = Kmeans_Cluster)) +
geom_line(linewidth = 1) +
geom_point(size = 2) +
labs(
title = "Average Performance Attributes by K-means Cluster",
x = "Attribute",
y = "Average Value",
color = "Cluster"
) +
scale_x_discrete(labels = c(
acceleration = "Acceleration",
ball_control = "Ball\nControl",
dribbling = "Dribbling",
shot_power = "Shot\nPower",
short_passing = "Short\nPassing",
sprint_speed = "Sprint\nSpeed"
)) +
theme_minimal() +
theme(
axis.text.x = element_text(size = 10),
plot.title = element_text(face = "bold")
)The K-means clusters show clear differences in their average performance across the six FIFA attributes. Cluster 2 had the strongest overall profile, with the highest average values across most of the selected attributes, including acceleration, ball control, dribbling and sprint speed. This suggests that Cluster 2 contains the most technically strong and athletically dynamic players in the dataset. Cluster 3 also performed well, particularly in ball control, dribbling, shot power and short passing, but it was clearly lower than Cluster 2 in acceleration and sprint speed. This suggests that Cluster 3 contains technically strong players, but with less pace than those in Cluster 2. Cluster 1 had moderate values across all six attributes and appears to represent players with an average performance profile. In contrast, Cluster 4 had by far the weakest overall profile, with the lowest average values across all six attributes. Overall, the K-means solution appears to identify one very strong cluster, one strong but less explosive technical cluster, one average cluster, and one weak cluster.
K-means Demographic and Financial Profile
kmeans_demo_profile |>
gt() |>
tab_header(
title = "Average Age, Value and Wage of Each K-means Cluster"
) |>
cols_label(
Kmeans_Cluster = "Cluster",
age = "Age",
value = "Club Value (€)",
wage = "Wage (€)"
) |>
fmt_number(
columns = age,
decimals = 2
) |>
fmt_currency(
columns = c(value, wage),
currency = "EUR",
decimals = 0
) |>
cols_align(
align = "center",
columns = everything()
) |>
tab_style(
style = list(
cell_fill(color = "#7EA6E0"),
cell_text(weight = "bold", align = "center")
),
locations = cells_title(groups = "title")
) |>
tab_style(
style = list(
cell_fill(color = "#DCE9F9"),
cell_text(weight = "bold", align = "center")
),
locations = cells_column_labels(everything())
) |>
tab_options(
table.width = px(750),
table.font.size = px(15),
data_row.padding = px(3)
)| Average Age, Value and Wage of Each K-means Cluster | |||
| Cluster | Age | Club Value (€) | Wage (€) |
|---|---|---|---|
| 1 | 27.73 | €13,317,241 | €58,540 |
| 2 | 26.17 | €21,964,037 | €80,708 |
| 3 | 28.12 | €15,971,626 | €65,225 |
| 4 | 29.04 | €14,475,000 | €51,972 |
The K-means clusters also differed in terms of age, club value, and wage. Cluster 2, which had the strongest performance profile, also had the youngest average age and the highest average club value and wage. This suggests that the strongest-performing players in the dataset also tend to be the most valuable and highest paid. Cluster 3 also contained relatively valuable players, although its average age, club value and wage were lower than those of Cluster 2. Cluster 1 had a more moderate financial profile, which fits with its more average performance profile. Cluster 4 had the weakest overall performance profile and also the lowest financial value, suggesting that this cluster contains the least highly rated and least valuable players in the dataset. Overall, the age and financial results broadly support the performance profiles produced by the K-means clustering.
Comparison of Cluster Quality
The K-means clustering algorithm produced the higher quality clustering solution. This conclusion is based on the overall mean silhouette scores for the two 4-cluster solutions. The hierarchical clustering solution had an overall mean silhouette score of 0.2976, whereas the K-means solution had a slightly higher mean silhouette score of 0.3307. Although both values suggest that the clustering structure is relatively weak overall, the K-means result indicates a somewhat better separation between clusters. Therefore, based on silhouette scores, K-means produced the higher quality clusters.
Comparison of Cluster Profiles
Overall, both hierarchical clustering and K-means produced clusters with similar profiles. In both methods, one cluster represented the strongest all-round players, one cluster contained technically strong players with lower pace, one cluster had a more moderate profile, and one cluster represented the weakest group of players. This suggests that both algorithms identified a broadly similar structure within the FIFA dataset.
There were some small differences in the exact average values and in the overall cluster quality, but the general pattern was consistent across both methods. Therefore, it can be concluded that both algorithms produced clusters with a similar profile, while K-means performed slightly better interms of cluster quality.