-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path5_ANOVA.qmd
More file actions
1614 lines (1276 loc) · 65.5 KB
/
Copy path5_ANOVA.qmd
File metadata and controls
1614 lines (1276 loc) · 65.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: "Statistical Tests of Comparison: ANOVA"
subtitle: "v1.0.0"
author: "Allan Omondi"
date: today
date-format: "DD MMMM YYYY"
engine: knitr
editor: visual
format:
html:
toc: true
toc-depth: 4
toc-float: true
number-sections: true
fig-width: 6
fig-height: 6
embed-resources: true
keep-md: false
df-print: paged
theme: flatly
highlight-style: tango
docx:
toc: true
toc-depth: 4
number-sections: true
fig-width: 6
keep-md: false
pdf:
toc: true
toc-depth: 4
number-sections: true
fig-width: 6
fig-height: 6
fig-crop: false
keep-tex: false
pdf-engine: xelatex
execute:
echo: fenced
message: false
warning: false
fig-width: 9
fig-height: 5
knitr:
opts_chunk:
comment: "#>"
---
```{r knitr_setup}
#| include: false
#| message: false
#| warning: false
# `knitr::opts_chunk$set()` is used to set the default options for code chunks
# in a Quarto document.
# The `echo = TRUE` option means that, by default, the code will be displayed in
# the output document when it is rendered. This allows readers to see the code
# that was used to generate the results, which can be helpful for understanding
# how the analysis was performed and for reproducing the results.
# `#| echo: false` can be used to override the default options for a specific
# code chunk. This would imply that the output of the code in that chunk will
# not be displayed in the document when it is rendered. This can be useful for
# hiding code that is not relevant to the reader or for keeping the output
# document clean and focused on the results.
# An alternative is to use `#| include: false` which hides both the code and
# the output of a code chunk.
knitr::opts_chunk$set(echo = TRUE)
# `normalizePath(".")` returns the absolute path of the current working
# directory. This can be useful for ensuring that the code is running in the
# correct directory and can find the files it requires.
knitr::opts_knit$set(root.dir = normalizePath("."))
# cat("Current working directory:", getwd(), "\n")
# list.files(".")
```
```{r install_dependencies}
#| echo: true
#| message: false
#| warning: false
# `installed.packages()` retrieves a matrix of all installed packages
# `[, "Package"]` extracts on the "Package" column from the matrix of all
# packages. This contains the name of the package.
# The %in% operator is used to test if the specified package is in the matrix of
# all packages
# `dependencies = TRUE` instructs R to install not only the specified package but
# also its dependencies
if (!"pacman" %in% installed.packages()[, "Package"]) {
install.packages("pacman", dependencies = TRUE)
library("pacman")
}
# For reading data from a CSV file
pacman::p_load("readr")
# For data visualization
pacman::p_load("ggplot2")
# For data visualization of missing data
pacman::p_load("naniar")
# "Companion to Applied Regression" for diagnostic EDA using
pacman::p_load("car")
# For the Durbin-Watson Test (test of independence of errors [autocorrelation])
pacman::p_load("lmtest")
# For performing a test of normality for MANOVA
pacman::p_load("mvnormtest")
# For visualizing hypothesis tests in multivariate linear models
pacman::p_load("heplots")
```
# Generate the Synthetic Dataset
## 4 Algorithms, 10 independent datasets, and 5 independent runs
This simulates data collected from a Machine Learning model comparison
experiment. The research question in this simulation is:
> Is there a statistically significant difference in mean classification
> accuracy across the Machine Learning algorithms?
There are 4 algorithms, 10 independent datasets, and 5 independent runs per
algorithm–dataset pair.
Execute the following R script to generate the synthetic dataset:
`./data/synthetic-data-for-ml-model-comparison.R`
“**4 × 10 × 5 = 200 observations**” means:
- 4 algorithms
- 10 [**independent datasets**]{.underline}
- 5 independent runs per algorithm–dataset pair
A "run" means:
- Different random seed
- Different initialization
- Possibly different train/test split
Therefore, the unit of analysis is the dataset, not the run. The 5 runs are
replicates, not new experimental units.
## NOT the same as Cross-Validation with Repeats
A run set up as a 10-Fold cross-validation with 5 repeats means:
For each algorithm on one dataset:
- The dataset is split into 10 folds
- The algorithm is trained and tested 10 times
- The entire 10-fold process is repeated 5 times
That gives 50 performance estimates per algorithm per dataset. However — and
this is the critical part — those 50 values are [**NOT independent
observations**]{.underline}.
They are correlated because:
- The same data points appear in multiple folds
- Training sets overlap heavily
- Test sets are reused across repeats
This disqualifies it from being used in statistical tests of comparison that
expect independence of observations.
## Valid ways to Create Independent Datasets
1. Naturally distinct datasets
- Different real-world sources
- Different domains or tasks
- Examples: Different UCI datasets, different companies’ sales datasets,
different medical studies
2. Independent data-generating processes
- Simulated datasets with independent random draws
- Different parameterizations (noise level, class imbalance, feature
correlations)
3. Independent experimental units
- Different time periods where stationarity holds
- Different geographical regions
- Different populations
# Load the Dataset
```{r load_dataset}
#| echo: true
#| message: false
#| warning: false
ml_performance_data <- read_csv(
"data/ml_results.csv",
col_types = cols(
Dataset = col_factor(
levels = c("Dataset_1", "Dataset_2", "Dataset_3",
"Dataset_4", "Dataset_5", "Dataset_6",
"Dataset_7", "Dataset_8", "Dataset_9",
"Dataset_10")),
Run = col_factor(levels = c("1", "2", "3", "4", "5")),
Algorithm = col_factor(levels = c("Logistic Regression",
"Random Forest",
"SVM", "Gradient Boosting")),
Accuracy = col_double(),
Precision = col_double(),
Recall = col_double()
)
)
head(ml_performance_data)
```
# Initial EDA
[**View the Dimensions**]{.underline}
The number of observations and variables.
```{r show_dimensions}
#| echo: true
#| message: false
#| warning: false
dim(ml_performance_data)
```
[**View the Data Types**]{.underline}
```{r show_data_types_1}
#| echo: true
#| message: false
#| warning: false
# sapply() is designed to apply a function to a variable in a dataset
# Example: `sapply(clv_data, class)` applies the `class()` function to each
# variable in the `clv_data` dataset, returning the data type of each variable.
sapply(ml_performance_data, class)
```
```{r show_data_types_2}
#| echo: true
#| message: false
#| warning: false
str(ml_performance_data)
```
[**Descriptive Statistics**]{.underline}
Understanding your data can lead to:
- **Data cleaning:** To remove extreme outliers or impute missing data.
- **Data transformation:** To reduce skewness
- **Hypothesis formulation:** Formulate a hypothesis based on the patterns you
identify
- **Choosing the appropriate statistical test:** You may notice properties of
the data such as distributions or data types that suggest the use of
parametric or non-parametric statistical tests and algorithms
Descriptive statistics can be used to understand your data. Typical descriptive
statistics include:
1. **Measures of frequency:** count and percent
2. **Measures of central tendency:** mean, median, and mode
3. **Measures of distribution/dispersion/spread/scatter/variability:** minimum,
quartiles, maximum, variance, standard deviation, coefficient of variation,
range, interquartile range (IQR) \[includes a box and whisker plot for
visualization\], kurtosis, skewness \[includes a histogram for
visualization\]).
4. **Measures of relationship:** covariance and correlation \[includes a
correlation plot and a scatter plot for visualization\].
## [**Measures of Frequency**]{.underline}
```{r measures_of_frequency}
#| echo: true
#| message: false
#| warning: false
ml_performance_data_freq <- ml_performance_data$Algorithm
cbind(frequency = table(ml_performance_data_freq),
percentage = prop.table(table(ml_performance_data_freq)) * 100)
```
## [**Measures of Central Tendency**]{.underline}
The median and the mean of each numeric variable:
```{r central_tendency}
#| echo: true
#| message: false
#| warning: false
summary(ml_performance_data)
```
The first 5 observations (rows) in the dataset:
```{r first_five_observations}
#| echo: true
#| message: false
#| warning: false
head(ml_performance_data, 5)
```
The last 5 observations (rows) in the dataset:
```{r last_five_observations}
#| echo: true
#| message: false
#| warning: false
tail(ml_performance_data, 5)
```
## [**Measures of Distribution**]{.underline}
Measuring the variability in the dataset is important because the amount of
variability determines **how well you can generalize** results from the sample
to a new observation in the population.
Low variability is ideal because it means that you can better predict
information about the population based on the sample data. High variability
means that the values are less consistent, thus making it harder to make
predictions.
The syntax `dataset[rows, columns]` can be used to specify the exact rows and
columns to be considered. `dataset[, columns]` implies all rows will be
considered. For example, specifying `BostonHousing[, -4]` implies all the
columns except column number 4. This can also be stated as
`BostonHousing[, c(1,2,3,5,6,7,8,9,10,11,12,13,14)]`. This allows us to perform
calculations on only columns that are numeric, thus leaving out the columns
termed as “factors” (categorical) or those that have a string data type.
### **Variance**
```{r distribution_variance}
#| echo: true
#| message: false
#| warning: false
# `sapply()` is designed to apply a function to a variable in a dataset.
# In this case, we use `sapply()` to apply the `var()` function used to
# compute the variance.
sapply(ml_performance_data[,c(4)], var)
```
### **Standard Deviation**
```{r distribution_standard_deviation}
#| echo: true
#| message: false
#| warning: false
sapply(ml_performance_data[,c(4)], sd)
```
### **Kurtosis (Pearson)**
The Kurtosis informs us of how often outliers occur in the results. There are
different formulas for calculating kurtosis. Specifying “type = 2” allows us to
use the 2^nd^ formula which is the same kurtosis formula used in other
statistical software like SPSS and SAS. It is referred to as "Pearson's
definition of kurtosis".
In “type = 2” (used in SPSS and SAS):
1. Kurtosis \< 3 implies a low number of outliers → platykurtic
2. Kurtosis = 3 implies a medium number of outliers → mesokurtic
3. Kurtosis \> 3 implies a high number of outliers → leptokurtic
High kurtosis (leptokurtic) affects models that are sensitive to outliers.
Estimates of the variance are also inflated. Low kurtosis (platykurtic) implies
a possible underestimation of real-world variability. The typical remedy
includes trimming outliers or using robust statistical methods that are less
affected by outliers.
```{r distribution_kurtosis}
#| echo: true
#| message: false
#| warning: false
pacman::p_load("e1071")
sapply(ml_performance_data[,c(4)], kurtosis, type = 2)
```
### **Skewness**
The skewness is used to identify the asymmetry of the distribution of results.
Similar to kurtosis, there are several ways of computing the skewness.
Using “type = 2” (common in other statistical software like SPSS and SAS) can be
interpreted as:
1. Skewness between -0.4 and 0.4 (inclusive) implies that there is no skew in
the distribution of results; the distribution of results is symmetrical; it
is a normal distribution; a Gaussian distribution.
2. Skewness above 0.4 implies a positive skew; a right-skewed distribution.
3. Skewness below -0.4 implies a negative skew; a left-skewed distribution.
Skewed data results in misleading averages and potentially biased model
coefficients. The typical remedy to skewed data involves applying data
transformations such as logarithmic, square-root, or Box–Cox, etc. to reduce
skewness.
```{r distribution_skewness}
#| echo: true
#| message: false
#| warning: false
sapply(ml_performance_data[,c(4)], skewness, type = 2)
```
As a data analyst, you need to confirm if the distortion in kurtosis or skewness
is a data problem or it is a real-world insight. For example, a real-world
insight could be that few customers drive most of the value. This is as opposed
to always looking it at it as a distortion that needs to be corrected.
## [**Basic Visualizations**]{.underline}
### **Histogram**
```{r visualization_histogram}
#| echo: true
#| message: false
#| warning: false
# `col_index` identifies which column(s) (by position) are being plotted;
# seq_along() generates 1, 2, ..., ncol(data_frame) automatically,
# so every column in the data frame is covered without manually listing
# positions
col_index <- seq_along(ml_performance_data)
# the right-hand `col_index` (the full vector) is evaluated once when the
# loop starts; col_index is then reassigned to one position per iteration.
for (col_index in col_index) {
# skip silently, with no message/warning, if the position does not exist
# in the data (out of range) or if the column at that position is not
# numeric; `next` moves straight to the following value of col_index
if (col_index > ncol(ml_performance_data) ||
!is.numeric(ml_performance_data[[col_index]])) {
next
}
col_name <- names(ml_performance_data)[col_index]
# for a histogram, the continuous variable goes on x; geom_histogram()
# bins it and computes the count on y automatically, so y is left
# unmapped here (the reverse of the boxplot version, where x was a
# blank placeholder and y held the continuous variable)
p <- ggplot2::ggplot(ml_performance_data, aes(x = .data[[col_name]])) +
geom_histogram(bins = 30, fill = "#4F6EA4", color = "#FFFFFF") +
# reduces the default 5% padding on the x and y axes
scale_x_continuous(expand = expansion(mult = c(0.00, 0.00))) +
scale_y_continuous(expand = expansion(mult = c(0.00, 0.00))) +
labs(
title = paste0("Histogram of the Variable '", col_name, "'"),
subtitle = paste0("EDA for distribution shape and outlier check ",
"before statistical tests and modelling"),
x = col_name,
y = "Count",
caption = paste0("Source: ML Model Performance Data | n = ",
nrow(ml_performance_data), ", ",
"M = ",
sprintf("%.2f",
mean(ml_performance_data[[col_name]], na.rm = TRUE)),
", ",
"sd = ",
sprintf("%.2f",
sd(ml_performance_data[[col_name]], na.rm = TRUE)))
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#1D1D1D"),
axis.title = element_text(color = "#1D1D1D"),
axis.text = element_text(color = "#898989"),
axis.text.x = element_text(angle = 30, hjust = 1),
plot.caption = element_text(color = "#898989"),
# removes vertical gridlines
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
# horizontal gridlines are kept (at major breaks only) since they
# help the reader judge bar heights against the y-axis scale
panel.grid.minor.y = element_blank(),
# draws only the x-axis and y-axis lines (the "L" frame)
axis.line.x = element_line(color = "#1D1D1D"),
axis.line.y = element_line(color = "#1D1D1D")
# panel.border = element_rect(color = "#1D1D1D", fill = NA,
# linewidth = 0.5)
)
# ggplot objects do not auto-print inside a for-loop, therefore,
# each one must be printed explicitly otherwise it will never appear
print(p)
}
```
### **Box and Whisker Plot**
```{r visualization_boxplot}
#| echo: true
#| message: true
#| warning: false
# `col_index` identifies which column(s) (by position) are being plotted;
# seq_along() generates 1, 2, ..., ncol(data_frame) automatically,
# so every column in the data frame is covered without manually listing
# positions
col_index <- seq_along(ml_performance_data)
# the right-hand `col_index` (the full vector) is evaluated once when the
# loop starts; col_index is then reassigned to one position per iteration.
for (col_index in col_index) {
# skip silently, with no message/warning, if the position does not exist
# in the data (out of range) or if the column at that position is not
# numeric; `next` moves straight to the following value of col_index
if (col_index > ncol(ml_performance_data) ||
!is.numeric(ml_performance_data[[col_index]])) {
next
}
col_name <- names(ml_performance_data)[col_index]
p <- ggplot2::ggplot(ml_performance_data, aes(x = "", y = .data[[col_name]])) +
stat_boxplot(geom = "errorbar", width = 0.15, color = "#1D1D1D") +
geom_boxplot(fill = "#4F6EA4", color = "#1D1D1D", width = 0.3) +
# reduces the default 5% padding on the x and y axes
# scale_x_continuous(expand = expansion(mult = c(0.00, 0.00))) +
# scale_y_continuous(expand = expansion(mult = c(0.00, 0.00))) +
labs(
title = paste0("Box and Whisker Plot of the Variable '", col_name, "'"),
subtitle = paste0("EDA for spread and outlier check before ",
"statistical tests and modelling"),
x = NULL, # no x-axis label needed
y = col_name,
caption = paste0("Source: ML Model Performance Data | n = ",
nrow(ml_performance_data), ", ",
"M = ",
sprintf("%.2f",
mean(ml_performance_data[[col_name]], na.rm = TRUE)),
", ",
"sd = ",
sprintf("%.2f",
sd(ml_performance_data[[col_name]], na.rm = TRUE)))
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#1D1D1D"),
axis.title = element_text(color = "#1D1D1D"),
axis.text = element_text(color = "#898989"),
# axis.text.x = element_text(angle = 30, hjust = 1),
plot.caption = element_text(color = "#898989"),
# removes vertical gridlines
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
# horizontal gridlines are kept (at major breaks only) since they
# help the reader judge bar heights against the y-axis scale
panel.grid.minor.y = element_blank(),
# draws only the x-axis and y-axis lines (the "L" frame)
axis.line.x = element_line(color = "#1D1D1D"),
axis.line.y = element_line(color = "#1D1D1D")
# panel.border = element_rect(color = "#1D1D1D", fill = NA,
# linewidth = 0.5)
)
# ggplot objects do not auto-print inside a for-loop, therefore,
# each one must be printed explicitly otherwise it will never appear
print(p)
}
```
### **Missing Data Plot**
```{r missing_data_plot}
#| echo: true
#| message: false
#| warning: false
# `vis_miss()` plots a matrix of observations (rows) by variables (columns),
# shading each cell by whether the value is missing or present; this is
# the ggplot2-native counterpart to Amelia's missmap()
naniar::vis_miss(ml_performance_data) +
labs(
title = "Missing Data Overview",
subtitle = "EDA for missingness pattern before data imputation or data removal",
caption = paste0("Source: ML Model Performance Data | n = ",
nrow(ml_performance_data))
) +
# overrides vis_miss()'s default black/grey fill; vis_miss() has no
# built-in color argument, but since it returns a standard ggplot
# object, the fill scale can be replaced afterward like any other
scale_fill_manual(values = c("Present" = "#4F6EA4", "Missing" = "#E03C31")) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#1D1D1D"),
axis.title = element_text(color = "#1D1D1D"),
axis.text = element_text(color = "#898989"),
# axis.text.x = element_text(angle = 30, hjust = 1),
plot.caption = element_text(color = "#898989"),
# removes vertical gridlines
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
# horizontal gridlines are kept (at major breaks only) since they
# help the reader judge bar heights against the y-axis scale
panel.grid.minor.y = element_blank(),
# draws only the x-axis and y-axis lines (the "L" frame)
# axis.line.x = element_line(color = "#1D1D1D"),
# axis.line.y = element_line(color = "#1D1D1D")
# panel.border = element_rect(color = "#1D1D1D", fill = NA,
# linewidth = 0.5)
)
```
### **Scatter Plot**
```{r scatter_plot_with_linear_regression_line}
#| echo: true
#| message: false
#| warning: false
ggplot2::ggplot(ml_performance_data,
aes(x = Algorithm, y = Accuracy)) +
# points use the blue-grey theme discussed in class,
# with partial transparency (alpha) so overlapping points are visible
# rather than solid black dots which obscure density.
geom_point(color = "#4F6EA4", alpha = 0.6, size = 2) +
# regression line in the near-black used for borders/axis text elsewhere,
# with a light blue-grey confidence ribbon instead of the default grey
geom_smooth(method = lm, color = "#1D1D1D", fill = "#A9C0DA") +
labs(
title = "Relationship between Algorithm and Accuracy",
subtitle = "EDA for linear association before modelling",
x = "Algorithm",
y = "Accuracy",
caption = paste0("Source: ML Model Performance Data | n = ",
nrow(ml_performance_data))
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#1D1D1D"),
axis.title = element_text(color = "#1D1D1D"),
axis.text = element_text(color = "#898989"),
# axis.text.x = element_text(angle = 30, hjust = 1),
plot.caption = element_text(color = "#898989"),
# removes vertical gridlines
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
# horizontal gridlines are kept (at major breaks only) since they
# help the reader judge point position against the y-axis scale
panel.grid.minor.y = element_blank(),
# draws only the x-axis and y-axis lines (the "L" frame)
axis.line.x = element_line(color = "#1D1D1D"),
axis.line.y = element_line(color = "#1D1D1D")
# panel.border = element_rect(color = "#1D1D1D", fill = NA,
# linewidth = 0.5)
)
```
`geom_smooth(method = "lm")` fits a linear regression, which requires a
continuous x-variable; `Algorithm` is categorical, therefore, this is not
useful. Below is an alternative for a case where you have categorical data.
```{r scatter_plot_with_box_plot}
#| echo: true
#| message: false
#| warning: false
ggplot2::ggplot(ml_performance_data,
aes(x = Algorithm, y = Accuracy)) +
stat_boxplot(geom = "errorbar", width = 0.15, color = "#1D1D1D") +
# layering the raw data points underneath the boxes with geom_jitter()
# shows you both the summary statistics and the actual spread/sample size
# per category, which a box alone hides, for instance you cannot tell
# from a box alone whether one category was tested on 5 runs and another
# on 50.
geom_jitter(width = 0.1, alpha = 0.4, color = "#1D1D1D", size = 1.5) +
# one box per category
geom_boxplot(fill = "#4F6EA4", color = "#1D1D1D", width = 0.4) +
labs(
title = "Distribution of Accuracy by Algorithm",
subtitle = "EDA for comparing model performance before selection",
x = "Algorithm",
y = "Accuracy",
caption = paste0("Source: ML Model Performance Data | n = ",
nrow(ml_performance_data))
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#1D1D1D"),
axis.title = element_text(color = "#1D1D1D"),
axis.text = element_text(color = "#898989"),
# uncomment if algorithm names are long enough to overlap when
# printed horizontally (e.g. "Random Forest", "Gradient Boosting")
axis.text.x = element_text(angle = 30, hjust = 1),
plot.caption = element_text(color = "#898989"),
# vertical gridlines removed: even with categorical groups on x,
# there is no continuous scale for the lines to reference, so they
# add no information
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
panel.grid.minor.y = element_blank(),
# draws only the x-axis and y-axis lines (the "L" frame)
axis.line.x = element_line(color = "#1D1D1D"),
axis.line.y = element_line(color = "#1D1D1D")
# panel.border = element_rect(color = "#1D1D1D", fill = NA,
# linewidth = 0.5)
)
```
# Statistical Test: ANOVA
## One-Way ANOVA
**Purpose:** Compares the means of three or more groups defined by a single
categorical independent variable, to test whether at least one group differs
from the others.\
**Variables:** One categorical independent variable (three or more levels); one
continuous dependent variable.\
**Output:** A single F-statistic and p-value; if significant, followed by a
post-hoc test (e.g., Tukey's HSD) to identify which specific groups differ.\
**Example:** Comparing Accuracy across four algorithms used to train ML models
(accuracy depends on the chosen algorithm).
```{r one_way_anova}
#| echo: true
#| message: false
#| warning: false
one_way_anova_result <- aov(Accuracy ~ Algorithm, data = ml_performance_data)
summary(one_way_anova_result)
```
Effect size addresses a gap that the F-test and p-value do not: those tell you
whether the groups differ by more than chance, but not how much of the outcome's
variability is actually attributable to the grouping factor. Eta squared (η²)
answers that second question. It is defined as the proportion of total variance
in the dependent variable explained by the factor.
```{r eta_squared_for_one_way_anova}
#| echo: true
#| message: false
#| warning: false
# effect size (eta squared): proportion of total variance explained by
# Algorithm; computed directly from the Sum Sq values already in the
# summary table, no extra package required
ss <- summary(one_way_anova_result)[[1]][["Sum Sq"]]
eta_squared_oneway <- ss[1] / sum(ss)
eta_squared_oneway
```
```{r post_hoc_tukey_for_one_way_anova}
#| echo: true
#| message: false
#| warning: false
TukeyHSD(one_way_anova_result)
```
## Two-Way ANOVA
**Purpose:** Examines the effects of two categorical independent variables on a
continuous outcome simultaneously, including whether the two variables interact,
meaning the effect of one depends on the level of the other.\
**Variables:** Two categorical independent variables; one continuous dependent
variable.\
**Output:** An F-statistic and p-value for each main effect, plus a separate
F-statistic and p-value for the interaction effect; a significant interaction is
interpreted before the main effects.\
**Example:** Comparing `Accuracy` across `Algorithm` and `Dataset`, and testing
whether an algorithm's relative performance changes depending on which dataset
it was run on.
```{r two_way_anova}
#| echo: true
#| message: false
#| warning: false
ml_performance_data$Algorithm <- as.factor(ml_performance_data$Algorithm)
ml_performance_data$Dataset <- as.factor(ml_performance_data$Dataset)
two_way_anova_result <- aov(Accuracy ~ Algorithm * Dataset, data = ml_performance_data)
shapiro.test(residuals(two_way_anova_result))
leveneTest(Accuracy ~ Algorithm * Dataset, data = ml_performance_data)
# `Algorithm * Dataset` tests both main effects and their interaction;
# a significant interaction would mean an algorithm's relative
# performance depends on which dataset it was run on, not just on
# the algorithm itself
summary(two_way_anova_result)
```
Effect size addresses a gap that the F-test and p-value do not: those tell you
whether the groups differ by more than chance, but not how much of the outcome's
variability is actually attributable to the grouping factor. Eta squared (η²)
answers that second question. It is defined as the proportion of total variance
in the dependent variable explained by the factor.
```{r eta_squared_for_two_way_anova}
#| echo: true
#| message: false
#| warning: false
ss <- summary(two_way_anova_result)[[1]][["Sum Sq"]]
row_names <- rownames(summary(two_way_anova_result)[[1]])
eta_squared_twoway <- ss / sum(ss)
names(eta_squared_twoway) <- trimws(row_names)
eta_squared_twoway
```
```{r post_hoc_tukey_for_two_way_anova}
#| message: false
#| warning: false
#| include: false
TukeyHSD(two_way_anova_result)
```
## Multiple Analysis of Variance (MANOVA)
**Purpose:** Compares group means across two or more continuous dependent
variables at the same time, accounting for the correlation between them, rather
than testing each outcome separately.\
**Variables:** One or more categorical independent variables; two or more
continuous dependent variables, analyzed jointly.\
**Output:** A multivariate test statistic (commonly Pillai's Trace); if
significant, followed by univariate ANOVAs on each dependent variable
individually to see which specific outcome(s) drove the result.\
**Example:** Comparing Accuracy, Precision, and Recall together across
Algorithm, rather than running three separate one-way ANOVAs.
```{r manova}
#| echo: true
#| message: false
#| warning: false
# requires at least two continuous DVs; this will not run on the
# current Accuracy-only dataset and is included as the template for
# when additional metrics are added per run
manova_result <- manova(cbind(Accuracy, Precision, Recall) ~ Algorithm,
data = ml_performance_data)
# Pillai's trace is the recommended default test statistic, since it is
# the most robust to violations of multivariate normality and unequal
# covariance matrices among the four test statistics MANOVA offers
summary(manova_result, test = "Pillai")
# univariate follow-up ANOVAs per outcome variable, the MANOVA
# equivalent of Tukey HSD follow-up after a significant one-way ANOVA
summary.aov(manova_result)
```
Effect size addresses a gap that the F-test and p-value do not: those tell you
whether the groups differ by more than chance, but not how much of the outcome's
variability is actually attributable to the grouping factor. Eta squared (η²)
answers that second question. It is defined as the proportion of total variance
in the dependent variable explained by the factor.
```{r eta_squared_for_manova}
#| echo: true
#| message: false
#| warning: false
manova_result <- manova(cbind(Accuracy, Precision, Recall) ~ Algorithm,
data = ml_performance_data)
aov_accuracy <- aov(Accuracy ~ Algorithm, data = ml_performance_data)
ss <- summary(aov_accuracy)[[1]][["Sum Sq"]]
eta_squared_accuracy <- ss[1] / sum(ss)
paste0("eta_squared_accuracy = ", eta_squared_accuracy)
aov_precision <- aov(Precision ~ Algorithm, data = ml_performance_data)
ss <- summary(aov_precision)[[1]][["Sum Sq"]]
eta_squared_precision <- ss[1] / sum(ss)
paste0("eta_squared_precision = ", eta_squared_precision)
aov_recall <- aov(Recall ~ Algorithm, data = ml_performance_data)
ss <- summary(aov_recall)[[1]][["Sum Sq"]]
eta_squared_recall <- ss[1] / sum(ss)
paste0("eta_squared_recall = ", eta_squared_recall)
```
```{r post_hoc_tukey_for_manova}
#| echo: true
#| message: false
#| warning: false
TukeyHSD(aov(Accuracy ~ Algorithm, data = ml_performance_data))
TukeyHSD(aov(Precision ~ Algorithm, data = ml_performance_data))
TukeyHSD(aov(Recall ~ Algorithm, data = ml_performance_data))
```
# Diagnostic EDA (Model Diagnostic)
## [**Test of Linearity — Not Applicable**]{.underline}
Linearity only exists between a predictor and outcome in a regression model.
Group membership has no inherent order or scale, so there is no "relationship"
between predictor and outcome whose shape (linear or otherwise) could be tested.
## [**Test of Independence of Errors (Autocorrelation)**]{.underline}
When observations are collected in a sequence (for example, repeated
measurements over time, or runs executed one after another), the assumption of
independence may be violated by autocorrelation: a correlation between residuals
based on their order or proximity in the sequence. The Durbin-Watson test can be
used to detect this. This test is not generally required for ANOVA designs
without a natural ordering, since independence in those cases follows from
random assignment and study design rather than from a statistical test.
Autocorrelation is consequential because, when positive, it leads to
underestimated standard errors and inflated t-statistics, which can make
findings appear more significant than they actually are. Negative
autocorrelation has the opposite effect: standard errors become overestimated
and tests become conservative.
A Durbin-Watson statistic close to 2 suggests no autocorrelation, while values
approaching 0 indicate positive autocorrelation and values approaching 4
indicate negative autocorrelation.
For the Durbin-Watson test, as implemented by default in R's `dwtest()`:
- The null hypothesis, H~0~, is that there is no autocorrelation (ρ = 0).
- The alternative hypothesis, H~a~, is that there is positive autocorrelation (ρ
\> 0).
By default, `dwtest()` tests only for positive autocorrelation, since this is
the form most commonly encountered in sequential data. If a test against
negative autocorrelation, or against autocorrelation in either direction, is
needed instead, the `alternative` argument can be set to `"less"` or
`"two.sided"`, respectively.
If the p-value of the Durbin-Watson statistic is greater than 0.05, there is no
evidence to reject the null hypothesis of no autocorrelation.
Example: Algorithm benchmarks run sequentially on the same machine, where
thermal throttling, caching, or background load could make consecutive runs
correlated.
```{r test_of_independence_of_errors}
#| echo: true
#| message: false
#| warning: false
lmtest::dwtest(one_way_anova_result)
lmtest::dwtest(two_way_anova_result)
lmtest::dwtest(manova_result)
```
**Interpretation:** The Durbin-Watson statistic for one-way ANOVA (DW = 2.1638),
two-way ANOVA (DW=2.1142), and multiple analysis of variance (MANOVA)
(DW=2.1559) are all close to 2, indicating little evidence of autocorrelation
among the residuals. The one-way ANOVA p-value (0.8292), two-way ANOVA p-value
(0.7862), and MANOVA p-value (0.8146) are far greater than 0.05, so there is no
evidence to reject the null hypothesis. The assumption of independent errors is
therefore reasonably supported.
## [**Test of Normality of the Distribution of the Errors**]{.underline}
The Shapiro–Wilk test assesses whether a sample of data could reasonably have
come from a normally distributed population. It enables you to answer the
question, "Is this data “normal enough” for methods that assume normality?"
It tests the following hypothesis: