The purpose of this report is to analyse runners’ results data from the 46. Warsaw Marathon (Poland, 2024) by application of unsupervised learning methods listed below.
Dimension Reduction technique: PCA
Clustering Analysis methods: K-Means, PAM, CLARA, Hierarchical, DBSCAN
I will begin with preparing the dataset and showing some initial analysis of the data.
df <- read.csv("results_clean.csv")
colnames(df)
## [1] "Position" "Bib" "Team" "Country"
## [5] "Year.of.Birth" "Category" "Gender.Position" "Surname"
## [9] "Name" "City" "Age.Position" "X5km"
## [13] "X10km" "X15km" "X20km" "X21km"
## [17] "X25km" "X30km" "X35km" "X40km"
## [21] "Chip.Time" "Gun.Time"
# Remove unnecessary columns
df <- df %>% dplyr::select(-Name, -Team, -Surname, -Bib, -City, -Gun.Time)
str(df)
## 'data.frame': 7080 obs. of 16 variables:
## $ Position : int 1 2 3 4 5 6 7 8 9 10 ...
## $ Country : chr "HUN" "POL" "UKR" "BEL" ...
## $ Year.of.Birth : int 2000 1997 1995 1997 1982 1980 1992 1991 1985 1988 ...
## $ Category : chr "M20" "M20" "M20" "M20" ...
## $ Gender.Position: int 1 2 3 4 5 6 7 8 9 10 ...
## $ Age.Position : int 1 2 3 4 1 2 1 2 3 4 ...
## $ X5km : chr "00:15:39" "00:15:40" "00:15:40" "00:15:40" ...
## $ X10km : chr "00:30:57" "00:30:58" "00:30:57" "00:30:57" ...
## $ X15km : chr "00:46:23" "00:46:23" "00:46:23" "00:46:22" ...
## $ X20km : chr "01:02:19" "01:02:20" "01:02:20" "01:02:19" ...
## $ X21km : chr "01:06:16" "01:05:50" "01:05:50" "01:05:49" ...
## $ X25km : chr "01:18:12" "01:18:12" "01:18:12" "01:18:11" ...
## $ X30km : chr "01:33:38" "01:33:39" "01:33:39" "01:33:44" ...
## $ X35km : chr "01:48:45" "01:48:46" "01:49:33" "01:50:03" ...
## $ X40km : chr "02:03:58" "02:04:18" "02:06:08" "02:06:20" ...
## $ Chip.Time : chr "02:10:43" "02:11:15" "02:13:13" "02:13:29" ...
# Separate the first character (K/M) into Gender and the rest into Age.Group
df <- df %>%
mutate(Gender = substr(Category, 1, 1),
Age.Group = substr(Category, 2, nchar(Category)))
# Convert Gender
df$Gender <- ifelse(df$Gender == "K", "Female", "Male")
# Check result
head(df[, c("Category", "Gender", "Age.Group")])
## Category Gender Age.Group
## 1 M20 Male 20
## 2 M20 Male 20
## 3 M20 Male 20
## 4 M20 Male 20
## 5 M40 Male 40
## 6 M40 Male 40
df$Country <- substr(df$Country, nchar(df$Country) - 2, nchar(df$Country))
sum(df$Country == "")
## [1] 59
df$Country[df$Country == ""] <- "POL"
df$Country <- as.factor(df$Country)
df$Age <- as.numeric(format(Sys.Date(), "%Y")) - df$Year.of.Birth
df$Category <- as.factor(df$Category)
df$Gender.Position <- as.integer(df$Gender.Position)
df$Age.Position <- as.integer(df$Age.Position)
df <- df %>% select(-Year.of.Birth, -Age.Group)
df$Gender <- as.factor(df$Gender)
convert_to_minutes <- function(time_str) {
hms <- strptime(time_str, format = "%H:%M:%S")
return(as.numeric(hms$hour) * 60 + as.numeric(hms$min) + as.numeric(hms$sec) / 60)
}
time_columns <- c("X5km", "X10km", "X15km", "X20km", "X21km",
"X25km", "X30km", "X35km", "X40km", "Chip.Time")
df[, time_columns] <- lapply(df[, time_columns], convert_to_minutes)
str(df)
## 'data.frame': 7080 obs. of 17 variables:
## $ Position : int 1 2 3 4 5 6 7 8 9 10 ...
## $ Country : Factor w/ 65 levels "ALB","ARG","ARM",..: 25 49 62 7 62 38 25 49 18 49 ...
## $ Category : Factor w/ 14 levels "","K20","K30",..: 8 8 8 8 10 10 9 9 9 9 ...
## $ Gender.Position: int 1 2 3 4 5 6 7 8 9 10 ...
## $ Age.Position : int 1 2 3 4 1 2 1 2 3 4 ...
## $ X5km : num 15.7 15.7 15.7 15.7 15.7 ...
## $ X10km : num 31 31 31 31 31 ...
## $ X15km : num 46.4 46.4 46.4 46.4 46.4 ...
## $ X20km : num 62.3 62.3 62.3 62.3 62.4 ...
## $ X21km : num 66.3 65.8 65.8 65.8 65.8 ...
## $ X25km : num 78.2 78.2 78.2 78.2 78.3 ...
## $ X30km : num 93.6 93.7 93.7 93.7 94.5 ...
## $ X35km : num 109 109 110 110 111 ...
## $ X40km : num 124 124 126 126 128 ...
## $ Chip.Time : num 131 131 133 133 136 ...
## $ Gender : Factor w/ 2 levels "Female","Male": 2 2 2 2 2 2 2 2 2 2 ...
## $ Age : num 26 29 31 29 44 46 34 35 41 38 ...
summary(df)
## Position Country Category Gender.Position Age.Position
## Min. : 1 POL :6293 M30 :1792 Min. : 1 Min. : 0.0
## 1st Qu.:1770 GBR : 157 M40 :1700 1st Qu.: 884 1st Qu.: 202.8
## Median :3540 UKR : 85 M20 :1358 Median :2214 Median : 501.0
## Mean :3540 FRA : 65 M50 : 691 Mean :2462 Mean : 632.7
## 3rd Qu.:5309 USA : 54 K30 : 454 3rd Qu.:3983 3rd Qu.:1027.0
## Max. :7079 GER : 38 K40 : 426 Max. :5754 Max. :1792.0
## NA's :2 (Other): 388 (Other): 659 NA's :2
## X5km X10km X15km X20km
## Min. : 15.65 Min. : 30.95 Min. : 46.37 Min. : 62.32
## 1st Qu.: 25.32 1st Qu.: 50.53 1st Qu.: 75.48 1st Qu.:101.08
## Median : 27.93 Median : 55.85 Median : 83.38 Median :111.73
## Mean : 28.50 Mean : 56.54 Mean : 84.34 Mean :113.20
## 3rd Qu.: 30.32 3rd Qu.: 60.87 3rd Qu.: 91.26 3rd Qu.:122.77
## Max. :604.88 Max. :638.02 Max. :675.38 Max. :712.63
## NA's :7 NA's :8 NA's :9 NA's :12
## X21km X25km X30km X35km
## Min. : 65.82 Min. : 78.18 Min. : 93.63 Min. :108.8
## 1st Qu.:106.80 1st Qu.:127.05 1st Qu.:153.37 1st Qu.:180.6
## Median :118.00 Median :140.06 Median :168.73 Median :198.2
## Mean :119.60 Mean :142.43 Mean :172.26 Mean :203.8
## 3rd Qu.:129.80 3rd Qu.:155.14 3rd Qu.:188.78 3rd Qu.:223.8
## Max. :720.92 Max. :750.23 Max. :788.22 Max. :828.8
## NA's :8 NA's :8 NA's :14 NA's :18
## X40km Chip.Time Gender Age
## Min. :124.0 Min. :130.7 Female:1325 Min. :20.00
## 1st Qu.:208.9 1st Qu.:220.6 Male :5755 1st Qu.:32.00
## Median :230.6 Median :244.6 Median :40.00
## Mean :236.7 Mean :249.9 Mean :40.23
## 3rd Qu.:261.6 3rd Qu.:277.2 3rd Qu.:47.00
## Max. :869.3 Max. :443.2 Max. :89.00
## NA's :16 NA's :2 NA's :2
# Total number of missing values
sum(is.na(df))
## [1] 108
# Fill missing values in time columns with average time for Category and Country
df <- df %>%
group_by(Category, Country) %>%
mutate(across(all_of(time_columns), ~ ifelse(is.na(.), mean(., na.rm = TRUE), .))) %>%
ungroup()
colSums(is.na(df))
## Position Country Category Gender.Position Age.Position
## 2 0 0 2 0
## X5km X10km X15km X20km X21km
## 0 0 0 0 0
## X25km X30km X35km X40km Chip.Time
## 0 0 0 0 2
## Gender Age
## 0 2
# Remove missing values
df <- na.omit(df)
# Total number of missing values
sum(is.na(df))
## [1] 0
df <- df[!apply(df[, time_columns] > 500, 1, any), ]
summary(df)
## Position Country Category Gender.Position Age.Position
## Min. : 1 POL :6283 M30 :1790 Min. : 1 Min. : 1.0
## 1st Qu.:1769 GBR : 157 M40 :1699 1st Qu.: 884 1st Qu.: 203.0
## Median :3536 UKR : 85 M20 :1356 Median :2214 Median : 501.0
## Mean :3538 FRA : 65 M50 : 691 Mean :2462 Mean : 632.6
## 3rd Qu.:5306 USA : 54 K30 : 453 3rd Qu.:3981 3rd Qu.:1027.0
## Max. :7079 GER : 38 K40 : 424 Max. :5754 Max. :1792.0
## (Other): 388 (Other): 657
## X5km X10km X15km X20km
## Min. :15.65 Min. : 30.95 Min. : 46.37 Min. : 62.32
## 1st Qu.:25.32 1st Qu.: 50.53 1st Qu.: 75.48 1st Qu.:101.09
## Median :27.93 Median : 55.85 Median : 83.37 Median :111.72
## Mean :27.87 Mean : 55.91 Mean : 83.71 Mean :112.57
## 3rd Qu.:30.32 3rd Qu.: 60.85 3rd Qu.: 91.22 3rd Qu.:122.73
## Max. :47.33 Max. :101.43 Max. :155.53 Max. :200.25
##
## X21km X25km X30km X35km
## Min. : 65.82 Min. : 78.18 Min. : 93.63 Min. :108.8
## 1st Qu.:106.80 1st Qu.:127.04 1st Qu.:153.37 1st Qu.:180.6
## Median :118.00 Median :140.05 Median :168.73 Median :198.2
## Mean :118.96 Mean :141.79 Mean :171.62 Mean :203.2
## 3rd Qu.:129.73 3rd Qu.:155.05 3rd Qu.:188.65 3rd Qu.:223.6
## Max. :210.77 Max. :249.60 Max. :306.47 Max. :356.9
##
## X40km Chip.Time Gender Age
## Min. :124.0 Min. :130.7 Female:1322 Min. :20.00
## 1st Qu.:208.9 1st Qu.:220.6 Male :5748 1st Qu.:32.00
## Median :230.6 Median :244.6 Median :40.00
## Mean :236.1 Mean :249.9 Mean :40.23
## 3rd Qu.:261.5 3rd Qu.:277.2 3rd Qu.:47.00
## Max. :408.8 Max. :443.2 Max. :89.00
##
Dimension reduction simplifies data, enhances analysis, and improves model performance while minimizing information loss. The goal is to reduce dimensions while retaining critical information. Keeping redundant features may not benefit models, whereas reducing dimensions saves time, optimizes storage, enhances pattern recognition, and improves data interpretation.
df_scaled <- scale(df[, sapply(df, is.numeric)])
colnames(df_scaled)
## [1] "Position" "Country" "Gender.Position" "Age.Position"
## [5] "X5km" "X10km" "X15km" "X20km"
## [9] "X21km" "X25km" "X30km" "X35km"
## [13] "X40km" "Chip.Time" "Gender" "Age"
pca_result <- prcomp(df_scaled, center = TRUE, scale = TRUE)
df_pca <- as.data.frame(pca_result$x[, 1:10])
str(df_pca)
## 'data.frame': 7070 obs. of 10 variables:
## $ PC1 : num -9.8 -9.72 -9.64 -9.8 -9.57 ...
## $ PC2 : num -0.67 -0.692 -0.7 -0.616 -0.586 ...
## $ PC3 : num -0.448 0.996 1.749 -1.922 0.775 ...
## $ PC4 : num -2.762 -0.167 1.256 -4.442 1.923 ...
## $ PC5 : num -0.722 -0.841 -0.936 -0.757 -1.155 ...
## $ PC6 : num -0.365 -0.423 -0.471 -0.262 -0.275 ...
## $ PC7 : num 0.637 0.617 0.583 0.611 0.506 ...
## $ PC8 : num 0.09453 0.07763 0.05371 0.04935 0.00511 ...
## $ PC9 : num 0.564 0.559 0.538 0.515 0.493 ...
## $ PC10: num -0.221 -0.221 -0.2 -0.198 -0.193 ...
fviz_eig(pca_result)
A Scree Plot helps determine the optimal number of principal components to retain in PCA. It shows the percentage of explained variance for each principal component (PC). The first principal component (PC1) explains the largest portion of the variance (over 70%). PC2 explains much smaller amount of variance (around 10%). PC3 explain less than 10% of variance. The plot suggests that 3 principal components may be sufficient to capture most of the variance in the data. Keeping fewer components helps reduce computational complexity without losing significant information.
explained_var <- cumsum(pca_result$sdev^2 / sum(pca_result$sdev^2))
ggplot(data.frame(PC = seq_along(explained_var), Explained = explained_var), aes(x = PC, y = Explained)) +
geom_line() +
geom_point() +
geom_hline(yintercept = 0.9, linetype = "dashed", color = "red") +
labs(title = "Cumulative Explained Variance", x = "Principal Component", y = "Variance Explained") +
theme_minimal()
Cumulative Explained Variance Plot helps determine the number of principal components (PCs) to retain in PCA by showing how much variance is explained as more components are added. The first 3 principal components capture around 90% of the variance.
Below are visualizations presenting data using 3 principal components.
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## ℹ The deprecated feature was likely used in the ggpubr package.
## Please report the issue at <https://github.com/kassambara/ggpubr/issues>.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## ℹ The deprecated feature was likely used in the factoextra package.
## Please report the issue at <https://github.com/kassambara/factoextra/issues>.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
On the first plot the color gradient shows the contribution of variables to the principal components (red contribute the most, blue - the least). Longer arrows indicate variables that strongly influence the principal components. Dim1 explains most of the variance (71.1%), Dim2 explains 11.7% variance in the data.
Times, Age Position and Gender Position have a strong positive correlation with Dim1. Dim1 likely represents a performance-related factor.
Gender, Gender Position and Age Position have strong negative correlation with Dim2. Dim2 may reflect demographic influences.
On the second plot we can see also features for PC3 - strong positive correlation for Country and negative for Age.
Clustering is an unsupervised machine learning technique used to group similar data points together based on shared characteristics. It helps in discovering natural patterns within data, which is useful for market segmentation, anomaly detection, and data simplification. A good clustering result ensures high intra-cluster similarity (data points within a cluster are similar) and high inter-cluster dissimilarity (clusters are distinct from each other).
Below code helps determine the optimal number of clusters by
measuring how well each point fits within its assigned cluster. A higher
silhouette width indicates better-defined clusters. The
NbClust() function performs clustering using Euclidean
distance and evaluates numbers of clusters from 2 to 10 using the
k-means algorithm, providing recommendations based on different
criteria. These methods help select the best K-value for k-means
clustering.
hopkins_stat <- hopkins(df_pca3)
cat("Hopkins Statistic:", hopkins_stat, "\n")
## Hopkins Statistic: 0.9986139
Hopkins value is close to 1, what suggests that the data is highly structured and clusterable, indicating a strong presence of natural clustering patterns.
fviz_nbclust(df_pca3, kmeans, method = "wss")
## Warning: The `size` argument of `element_line()` is deprecated as of ggplot2 3.4.0.
## ℹ Please use the `linewidth` argument instead.
## ℹ The deprecated feature was likely used in the ggpubr package.
## Please report the issue at <https://github.com/kassambara/ggpubr/issues>.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: The `size` argument of `element_rect()` is deprecated as of ggplot2 3.4.0.
## ℹ Please use the `linewidth` argument instead.
## ℹ The deprecated feature was likely used in the ggpubr package.
## Please report the issue at <https://github.com/kassambara/ggpubr/issues>.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
In elbow method, total within-cluster sum of squares (WSS) decreases as k increases. The sharpest drop occurs at k = 2, suggesting that 2 clusters provide the optimal balance between compactness and separation.
fviz_nbclust(df_pca3, kmeans, method = "silhouette")
Silhouette width peaks at k=2, indicating that this is the number of clusters where data points are well-separated and well-clustered. High silhouette value means that points are well assigned to their clusters.
gap_stat <- clusGap(df_pca3, FUN = kmeans, K.max = 10, B = 50)
## Warning: Quick-TRANSfer stage steps exceeded maximum (= 353500)
## Warning: Quick-TRANSfer stage steps exceeded maximum (= 353500)
## Warning: did not converge in 10 iterations
## Warning: did not converge in 10 iterations
## Warning: did not converge in 10 iterations
## Warning: did not converge in 10 iterations
## Warning: did not converge in 10 iterations
## Warning: did not converge in 10 iterations
## Warning: Quick-TRANSfer stage steps exceeded maximum (= 353500)
fviz_gap_stat(gap_stat)
Gap statistics, for 10 tested clusters, shows the biggest difference between observed clustering performance and a random clustering reference for k=10. However, choosing k=10 may result in over-segmentation.
NbClust(df_pca3, distance = "euclidean", min.nc = 2, max.nc = 10, method = "kmeans")
## *** : The Hubert index is a graphical method of determining the number of clusters.
## In the plot of Hubert index, we seek a significant knee that corresponds to a
## significant increase of the value of the measure i.e the significant peak in Hubert
## index second differences plot.
##
## *** : The D index is a graphical method of determining the number of clusters.
## In the plot of D index, we seek a significant knee (the significant peak in Dindex
## second differences plot) that corresponds to a significant increase of the value of
## the measure.
##
## *******************************************************************
## * Among all indices:
## * 7 proposed 2 as the best number of clusters
## * 4 proposed 3 as the best number of clusters
## * 3 proposed 4 as the best number of clusters
## * 3 proposed 6 as the best number of clusters
## * 2 proposed 7 as the best number of clusters
## * 1 proposed 8 as the best number of clusters
## * 1 proposed 9 as the best number of clusters
## * 2 proposed 10 as the best number of clusters
##
## ***** Conclusion *****
##
## * According to the majority rule, the best number of clusters is 2
##
##
## *******************************************************************
## $All.index
## KL CH Hartigan CCC Scott Marriot TrCovW
## 2 3.5376 7378.009 3013.1390 -20.9819 7304.794 1.159685e+13 341994244
## 3 3.6842 6767.266 1704.7113 -21.1856 12470.467 1.256628e+13 143345330
## 4 0.6077 6167.147 1479.3839 -18.1149 18590.653 9.400151e+12 112845747
## 5 0.8066 5962.758 1353.5413 -15.6150 21944.930 9.139217e+12 71810990
## 6 1.3820 5953.967 1090.2959 -8.1088 30148.730 4.124110e+12 58137427
## 7 54.1488 5908.325 651.6622 -2.7555 31351.876 4.734969e+12 44144575
## 8 0.0313 5623.829 768.9184 -2.8346 32872.900 4.987327e+12 35332002
## 9 1.1047 5551.966 690.1949 0.2223 33736.324 5.586435e+12 26700140
## 10 6.6376 5493.383 489.6385 2.9742 35504.236 5.370946e+12 21826922
## TraceW Friedman Rubin Cindex DB Silhouette Duda Pseudot2 Beale
## 2 49559.38 1.8075 2.0439 0.2006 0.9781 0.4222 0.8741 755.6162 0.2451
## 3 34746.64 4.7448 2.9152 0.1868 1.0614 0.3600 1.9360 -1623.0067 -0.8227
## 4 27993.91 5.8517 3.6184 0.1585 1.0573 0.3832 1.5537 -1413.7029 -0.6064
## 5 23147.58 8.1726 4.3759 0.1683 1.0178 0.3672 1.4591 -741.8972 -0.5353
## 6 19425.89 14.5062 5.2143 0.1483 0.9850 0.3689 0.9591 94.9545 0.0726
## 7 16828.49 16.1855 6.0191 0.1445 1.0248 0.3510 2.1690 -1115.0917 -0.9163
## 8 15406.98 16.3654 6.5745 0.1360 1.0731 0.3442 1.5917 -889.1966 -0.6324
## 9 13894.16 18.0580 7.2903 0.1497 1.0507 0.3461 1.5337 -618.3950 -0.5920
## 10 12656.98 18.7246 8.0029 0.1536 1.0043 0.3545 1.2228 -354.2553 -0.3100
## Ratkowsky Ball Ptbiserial Frey McClain Dunn Hubert SDindex Dindex
## 2 0.1958 24779.689 0.5291 0.5918 0.5217 0.0023 0 0.9389 2.4011
## 3 0.2120 11582.212 0.5495 0.4283 0.8475 0.0023 0 0.9674 2.0024
## 4 0.3035 6998.477 0.5542 0.6261 1.0124 0.0008 0 0.9030 1.7512
## 5 0.2858 4629.516 0.5280 0.6198 1.2981 0.0030 0 1.0583 1.6018
## 6 0.2801 3237.648 0.5113 0.4607 1.4575 0.0029 0 1.0524 1.4547
## 7 0.2616 2404.070 0.4969 0.9838 1.6121 0.0028 0 1.1383 1.3477
## 8 0.2783 1925.872 0.4607 0.2252 1.9359 0.0020 0 1.5201 1.2842
## 9 0.2656 1543.796 0.4581 0.2985 1.9762 0.0027 0 1.5281 1.2332
## 10 0.2656 1265.698 0.4530 1.1929 2.0308 0.0045 0 1.5878 1.1834
## SDbw
## 2 1.2998
## 3 0.6721
## 4 0.4817
## 5 0.3791
## 6 0.4109
## 7 0.3372
## 8 0.3334
## 9 0.2955
## 10 0.3050
##
## $All.CriticalValues
## CritValue_Duda CritValue_PseudoT2 Fvalue_Beale
## 2 0.7525 1725.8683 0.8648
## 3 0.7379 1192.2735 1.0000
## 4 0.7362 1421.3477 1.0000
## 5 0.7342 853.6986 1.0000
## 6 0.7312 817.7587 0.9747
## 7 0.7063 860.3939 1.0000
## 8 0.7276 895.6252 1.0000
## 9 0.7258 671.3624 1.0000
## 10 0.7249 737.6135 1.0000
##
## $Best.nc
## KL CH Hartigan CCC Scott Marriot
## Number_clusters 7.0000 2.000 3.000 10.0000 6.000 6.000000e+00
## Value_Index 54.1488 7378.009 1308.428 2.9742 8203.799 5.625967e+12
## TrCovW TraceW Friedman Rubin Cindex DB Silhouette
## Number_clusters 3 3.000 6.0000 7.0000 8.000 2.0000 2.0000
## Value_Index 198648914 8060.012 6.3336 -0.2495 0.136 0.9781 0.4222
## Duda PseudoT2 Beale Ratkowsky Ball PtBiserial Frey
## Number_clusters 2.0000 2.0000 2.0000 4.0000 3.00 4.0000 1
## Value_Index 0.8741 755.6162 0.2451 0.3035 13197.48 0.5542 NA
## McClain Dunn Hubert SDindex Dindex SDbw
## Number_clusters 2.0000 10.0000 0 4.000 0 9.0000
## Value_Index 0.5217 0.0045 0 0.903 0 0.2955
##
## $Best.partition
## [1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [38] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [75] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [112] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [149] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [186] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [223] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [260] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [297] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [334] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [371] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [408] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [445] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [482] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [519] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [556] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [593] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [630] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [667] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [704] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [741] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [778] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [815] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [852] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [889] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [926] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [963] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1000] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1037] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1074] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1111] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1148] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1185] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1222] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1259] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1296] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1333] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1370] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1407] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1444] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1481] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1518] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1555] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1592] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1629] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1666] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1703] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1740] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1777] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1814] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1851] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1888] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1925] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1962] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [1999] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2036] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2073] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2110] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2147] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2184] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2221] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2258] 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2295] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2332] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2369] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2406] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2443] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2480] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2517] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2554] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2591] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2628] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2665] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2702] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2739] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2776] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2813] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2
## [2850] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2887] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2924] 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2961] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [2998] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3035] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3072] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3109] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3146] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3183] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1
## [3220] 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3257] 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3294] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3331] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1
## [3368] 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [3405] 1 1 1 2 2 1 1 2 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 2 1 1 1 2 1 1 1 1 1 1 1 1 2
## [3442] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1
## [3479] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1
## [3516] 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 2 1 1 1 1 1 1 2
## [3553] 1 1 2 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 2 1 1 1 1 1 1 1 1 1
## [3590] 2 1 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 2
## [3627] 2 2 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 2 1 1 2 1 1 2 1 2 2 2 1 1 1 2 1 1 1 1
## [3664] 1 1 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 2 1 1 1 1 1 1 2 1 2
## [3701] 1 1 2 2 1 1 1 1 2 1 1 2 2 1 2 2 2 2 2 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 1 1 1
## [3738] 1 1 2 2 1 1 1 1 2 1 1 2 1 1 1 2 1 2 1 1 1 2 1 1 2 1 1 2 1 2 1 1 1 2 1 2 2
## [3775] 1 2 1 1 2 1 2 1 2 1 1 1 2 2 1 2 2 2 2 1 1 1 2 1 2 1 2 2 2 1 1 1 1 1 2 2 2
## [3812] 2 1 1 1 2 1 2 1 1 1 2 2 1 1 2 2 2 2 1 1 1 2 2 1 2 2 2 2 1 1 2 2 1 2 1 1 1
## [3849] 1 1 1 2 1 2 2 1 2 2 1 1 1 2 1 1 1 2 2 1 2 1 2 1 1 2 1 2 1 1 1 1 2 1 2 2 2
## [3886] 2 2 2 1 2 2 1 2 2 1 2 1 1 1 1 2 1 1 2 1 2 1 1 2 1 1 2 1 2 2 1 2 1 1 1 2 2
## [3923] 1 2 1 1 2 2 2 1 1 1 1 2 1 1 2 2 1 1 1 1 1 2 1 1 1 1 1 2 2 2 2 1 2 1 2 1 2
## [3960] 1 2 2 2 1 2 2 1 1 1 2 1 1 1 1 1 2 2 2 2 2 1 2 1 1 2 2 1 2 1 2 2 1 1 2 2 1
## [3997] 1 1 2 1 2 1 1 1 1 2 1 2 1 2 1 2 2 1 2 2 1 1 2 1 2 2 1 2 1 2 2 2 2 1 2 1 1
## [4034] 2 2 1 1 1 2 2 1 1 1 1 2 1 1 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 1 2 1 1 2 1 2
## [4071] 1 2 1 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 2 2 2 1 1 2 1 2 1 2 1 1 2
## [4108] 2 2 2 2 2 2 1 1 2 1 2 1 2 2 2 2 2 1 1 2 2 2 1 2 1 1 2 1 2 1 2 2 1 1 2 2 1
## [4145] 2 2 2 2 1 2 2 2 2 2 2 1 1 2 2 2 1 2 1 2 2 2 2 2 2 2 2 1 2 1 2 1 2 2 2 2 2
## [4182] 2 2 1 2 2 1 2 1 2 2 1 2 2 2 1 2 1 1 2 2 2 1 2 1 2 1 2 2 2 2 2 2 2 1 2 1 2
## [4219] 2 2 2 1 2 2 2 2 2 2 2 1 2 2 2 2 1 1 2 2 2 2 1 2 2 2 2 2 2 2 2 1 2 2 2 1 1
## [4256] 2 2 2 1 1 2 2 2 1 2 2 2 2 2 1 2 1 1 2 1 2 2 2 1 2 2 1 2 2 2 2 1 1 2 2 1 2
## [4293] 1 2 2 2 1 1 2 1 2 2 2 2 2 2 1 2 2 2 1 2 2 1 2 2 2 1 2 2 2 2 2 2 2 2 2 1 2
## [4330] 2 2 2 2 1 1 1 2 2 2 2 2 2 1 2 2 1 2 2 2 1 2 2 2 2 2 2 2 2 2 2 1 2 2 1 1 1
## [4367] 2 2 1 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 1 2 2 2 1 2 2 2 1 2 2 2 1 1 1 2 2 2 2
## [4404] 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2
## [4441] 2 2 2 2 1 2 2 2 2 2 1 2 2 2 1 2 2 2 1 2 2 2 1 2 2 2 2 2 2 1 2 2 2 2 2 2 2
## [4478] 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2
## [4515] 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 1 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [4552] 2 2 2 1 2 2 1 2 1 1 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [4589] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2
## [4626] 2 2 1 2 1 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 1 2 2 2 2
## [4663] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1
## [4700] 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 1 2 2 2 2 2 1 2 2
## [4737] 2 2 2 2 2 2 2 2 2 2 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2
## [4774] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [4811] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [4848] 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2
## [4885] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2
## [4922] 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [4959] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [4996] 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 1 2 2 2
## [5033] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5070] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5107] 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2
## [5144] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5181] 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5218] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5255] 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5292] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5329] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2
## [5366] 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5403] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5440] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5477] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2
## [5514] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5551] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5588] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5625] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5662] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2
## [5699] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5736] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5773] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5810] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5847] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5884] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5921] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5958] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [5995] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6032] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6069] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6106] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6143] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6180] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6217] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6254] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6291] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6328] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6365] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6402] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6439] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6476] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6513] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6550] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6587] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6624] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6661] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6698] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6735] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6772] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6809] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6846] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6883] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6920] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [6957] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2
## [6994] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [7031] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [7068] 2 2 2
NbClust package provides 30 indices for determining the
number of clusters and proposes to user the best clustering scheme from
the different results obtained by varying all combinations of number of
clusters, distance measures, and clustering methods.
For further analysis I select k=2 as numbers of clusters, as suggested.
K-means is a centroid-based clustering algorithm that partitions data into k clusters by minimizing the variance within each cluster. It iteratively assigns points to the nearest cluster center (centroid), then recalculates centroids based on the assigned points until convergence. It requires the number of clusters k to be predefined and struggles with non-spherical or unevenly sized clusters. It is sensitive to outliers and initial centroid placement, which can lead to different results across runs.
kmeans_result <- kmeans(df_pca3, 2)
fviz_silhouette(silhouette(kmeans_result$cluster, dist(df_pca3[, c("PC1", "PC2", "PC3")])))
## cluster size ave.sil.width
## 1 1 3989 0.47
## 2 2 3081 0.36
The k-means clustering results show clusters with some overlapping data points in the center. Average silhouette width of is 0.42, which suggests moderate clustering quality. High silhouette score (>0.7) would suggest that points within clusters are well-grouped and distinct from other clusters. Low average silhouette width suggest that some data points may be misclassified or the clusters may not be well-separated.
Cluster 1 contains 3,989 data points with an average silhouette width of 0.47, which suggests moderate clustering quality.
Cluster 2 contains 3,081 data points with an average silhouette width of 0.36, suggesting that data points are more dispersed.
Partitioning Around Medoids (PAM) is a clustering algorithm that selects representative points, called medoids, as cluster centers instead of centroids like k-means. It begins with an initial set of medoids and iteratively replace them with non-medoid points to minimize the total dissimilarity within clusters. PAM is useful for small datasets and is more robust to noise and outliers since medoids are less influenced by extreme values than means.
pam_result <- pam(df_pca3, k = 2)
## cluster size ave.sil.width
## 1 1 4095 0.46
## 2 2 2975 0.37
The PAM clustering results show that average silhouette width of is 0.42. Like for k-means clustering, this is moderate silhouette score.
Cluster 1 contains 4,095 data points with an average silhouette width of 0.46.
Cluster 2 contains 2,975 data points with an average silhouette width of 0.37, suggesting that data points are more dispersed.
CLARA (Clustering Large Applications) is an extension of the PAM algorithm for large datasets. It works by drawing multiple samples from the dataset, applying PAM to each sample, and selecting the best clustering result. The main advantage of CLARA is its ability to cluster larger datasets efficiently compared to PAM. However, its effectiveness depends on the sample size, and if the sample is biased, the clustering results may not generalize well to the entire dataset.
clara_result <- clara(df_pca3, k = 2)
## cluster size ave.sil.width
## 1 1 3524 0.48
## 2 2 3546 0.34
The CLARA clustering results show that average silhouette width of is 0.41. Like for 2 previous clustering methods, this is moderate silhouette score.
Cluster 1 contains 3,524 data points with an average silhouette width of 0.48.
Cluster 2 contains 3,546 data points with an average silhouette width of 0.34, suggesting that data points are more dispersed.
Hierarchical clustering builds a hierarchy of clusters using either an agglomerative (bottom-up) or divisive (top-down) approach. It does not require the number of clusters to be predefined and can be visualized using a dendrogram, which helps determine the optimal number of clusters. However, it is computationally expensive for large datasets and does not allow for cluster reassignment after merging or splitting. Different linkage methods influence the shape and size of the resulting clusters.
hclust_result <- hclust(dist(df_pca3), method = "ward.D2")
hclust_clusters <- cutree(hclust_result, k = 2)
Dendrogram visualizes the hierarchical clustering of data based on Ward’s method and the Euclidean distance. Y-axis represents the level at which clusters are merged. Higher values indicate that clusters are more dissimilar. The red rectangles indicate the cut-off level for defining two clusters. Since the two main clusters merge at a relatively high height, it suggests they are well-separated.
DBSCAN groups data points based on density, allowing for clusters of arbitrary shapes and the identification of noise and outliers. It defines core points with a minimum number of neighbors within a specified radius (eps) and expands clusters from them. Unlike k-means, it does not require specifying the number of clusters but is sensitive to the choice of parameters (eps and MinPts). It performs well with spatial and non-linearly separable data but struggles when clusters have varying densities.
dbscan_result <- dbscan::dbscan(df_pca3, eps = 1.5, minPts = 5)
db <- df_pca3[, c("PC1", "PC2")]
fviz_cluster(dbscan_result, db, stand = FALSE, frame = FALSE, geom = "point", main = "2D PCA with DBSCAN Clustering", ggtheme = theme_minimal())
## Warning: argument frame is deprecated; please use ellipse instead.
print(dbscan_result)
## DBSCAN clustering for 7070 objects.
## Parameters: eps = 1.5, minPts = 5
## Using euclidean distances and borderpoints = TRUE
## The clustering contains 2 cluster(s) and 8 noise points.
##
## 0 1 2
## 8 5746 1316
##
## Available fields: cluster, eps, minPts, metric, borderPoints
## cluster size ave.sil.width
## 0 0 8 -0.13
## 1 1 5746 0.23
## 2 2 1316 0.33
First 2 plots show 2 clusters identified by DBSCAN, which contain the main data points (5746 and 1316 respectively). Black dots or cluster 0 represent noise points. Third plot presents silhouette analysis with low average silhouette width 0.25, indicating weak clustering quality. Cluster 2 has the highest silhouette width (0.33), meaning it is better defined than cluster 1 (0.23) and cluster 0 (noise) (-0.13), confirming that these points do not fit well into any cluster.
Despite the low silhouette score, clusters on the first 2 plots appear well-separated with minimal overlap. DBSCAN effectively identified groups of data points and filtered outliers.
Given the loading factors of PC2 (e.g., for Gender and Gender Position), the analysis of raw data groupped by Gender and clusters’ size after DBSCAN clustering reveals a clear separation of data points based on Gender. This suggests that Gender significantly influence the clustering structure, as captured by the second principal component (PC2).
The table below presents the cluster’s number assigned to few data point after applying various clustering methods.
df_pca3$kmeans_cluster <- factor(kmeans_result$cluster)
df_pca3$pam_cluster <- factor(pam_result$cluster)
df_pca3$clara_cluster <- factor(clara_result$cluster)
df_pca3$hclust_cluster <- factor(hclust_clusters)
df_pca3$dbscan_cluster <- factor(dbscan_result$cluster)
head(df_pca3)
## PC1 PC2 PC3 kmeans_cluster pam_cluster clara_cluster
## 1 -9.795978 -0.6704226 -0.4476079 1 1 1
## 2 -9.718017 -0.6923608 0.9956973 1 1 1
## 3 -9.642648 -0.6999396 1.7485630 1 1 1
## 4 -9.802982 -0.6160933 -1.9222013 1 1 1
## 5 -9.565574 -0.5861277 0.7750301 1 1 1
## 6 -9.231714 -0.4519810 -1.0537336 1 1 1
## hclust_cluster dbscan_cluster
## 1 1 1
## 2 1 1
## 3 1 1
## 4 1 1
## 5 1 1
## 6 1 1
One of the quality measures for clustering is the silhouette method. Below is a summary table presenting the average silhouette scores for all applied clustering methods.
library(knitr)
sil1 <- silhouette(kmeans_result$cluster, dist(df_pca3[, c("PC1", "PC2", "PC3")]))
sil2 <- silhouette(pam_result$cluster, dist(df_pca3[, c("PC1", "PC2", "PC3")]))
sil3 <- silhouette(clara_result$cluster, dist(df_pca3[, c("PC1", "PC2", "PC3")]))
sil4 <- silhouette(hclust_clusters, dist(df_pca3[, c("PC1", "PC2", "PC3")]))
sil5 <- silhouette(dbscan_result$cluster, dist(df_pca3[, c("PC1", "PC2", "PC3")]))
mean_sil_scores <- data.frame(
"Clustering Method" = c("K-Means", "PAM", "CLARA", "Hierarchical", "DBSCAN"),
"Mean Silhouette Score" = c(
mean(sil1[, 3]),
mean(sil2[, 3]),
mean(sil3[, 3]),
mean(sil4[, 3]),
mean(sil5[, 3])
)
)
kable(mean_sil_scores)
| Clustering.Method | Mean.Silhouette.Score |
|---|---|
| K-Means | 0.4221516 |
| PAM | 0.4226154 |
| CLARA | 0.4100061 |
| Hierarchical | 0.3940863 |
| DBSCAN | 0.2506902 |
Average silhouette scores, below 0.5, for all applied clustering methods indicate moderate or low clustering quality. This means that some data points may not be well classified, or clusters may not be well-separated.
Because PC1 mainly represents a performance-related factor, I wanted verify whether there are differences for the average times for each cluster across all clustering methods.
## # A tibble: 11 × 14
## Cluster Method X5km X10km X15km X20km X21km X25km X30km X35km X40km
## <fct> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 K-Means 25.3 50.7 75.7 101. 107. 127. 153. 180. 208.
## 2 2 K-Means 31.1 62.7 94.0 127. 134. 161. 196. 233. 272.
## 3 1 PAM 25.4 50.9 76.0 102. 108. 128. 154. 181. 209.
## 4 2 PAM 31.2 62.9 94.3 127. 135. 161. 196. 234. 273.
## 5 1 CLARA 25.0 50.0 74.7 100. 106. 125. 151. 177. 205.
## 6 2 CLARA 30.7 61.8 92.6 125. 132. 158. 192. 229. 267.
## 7 1 Hierarchical 25.8 51.7 77.3 104. 109. 130. 156. 184. 213.
## 8 2 Hierarchical 31.3 62.9 94.4 128. 135. 162. 197. 235. 274.
## 9 0 DBSCAN 33.9 70.9 108. 145. 153. 182. 221. 260. 299.
## 10 1 DBSCAN 27.5 55.0 82.4 111. 117. 139. 169. 200. 232.
## 11 2 DBSCAN 29.7 59.6 89.4 121. 128. 152. 184. 218. 252.
## # ℹ 3 more variables: Chip.Time <dbl>, Avg_Age <dbl>, Total_Count <int>
As presented above, differences in runners’ performance are clearly visible for K-Means, PAM, CLARA, and Hierarchical clustering methods, even though they were not so clear in previous analyses. Average silhouette scores for all applied clustering methods indicated moderate or low clustering quality, however, the clusters might be better defined if only Chip Time were considered.
For DBSCAN, the clustering rather reflects a gender split, as discussed earlier. The average times for clusters 1 and 2 are similar, whereas the noise cluster shows significantly higher average times.
Above analysis of runners’ performance data from the 46. Warsaw Marathon (2024) shows application of unsupervised learning techniques, including PCA for dimension reduction and clustering methods such as K-Means, Hierarchical, PAM, CLARA, and DBSCAN. K-Means performed best in terms of clustering quality, while DBSCAN effectively identified outliers but had much lower average silhouette width. NbClust package analysis indicated that two clusters provide the best separation. Overall, clustering helped reveal distinct performance groups among runners, with key insights into race times.