This training module develops practical skills in data inspection, data cleaning, descriptive statistics, exploratory data analysis (EDA), and data visualization in R.
The examples use the diarrhea dataset available from:
https://raw.githubusercontent.com/bijayprad/data/refs/heads/main/dirrhea.csv
Dataset note: The training examples use the variables exactly as represented in the source CSV. Always verify definitions, coding, and units against the original data documentation before using the dataset for substantive research.
The purpose is not only to produce graphs and numerical summaries. Trainees should learn to:
By the end of this module, trainee should be able to:
ggplot2;Install packages once if necessary:
Load the packages:
library(tidyverse)
library(janitor)
library(skimr)
library(scales)
library(patchwork)
theme_set(theme_classic())url <- "https://raw.githubusercontent.com/bijayprad/data/refs/heads/main/dirrhea.csv"
dirdata <- read_csv(
url,
na = c("", "NA")
)## Rows: 1296 Columns: 9
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): Department, Fever, DegreeDehy, OutComeDisc
## dbl (5): ID, Sex, Age, DiaMaxNo_24hrs, DiaDurInDays
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## [1] 1296 9
## [1] "ID" "Department" "Sex" "Age"
## [5] "Fever" "DiaMaxNo_24hrs" "DiaDurInDays" "DegreeDehy"
## [9] "OutComeDisc"
The current CSV contains the following variables:
| Variable | Description for training | Suggested type |
|---|---|---|
ID |
Record identifier | Integer |
Department |
Department/category such as OPD or IPD | Factor |
Sex |
Sex coded as 1/2 in the source | Factor |
Age |
Age value recorded in the source | Numeric |
Fever |
Fever status | Factor |
DiaMaxNo_24hrs |
Maximum number of diarrhea episodes in 24 hours | Integer |
DiaDurInDays |
Duration of diarrhea | Numeric |
DegreeDehy |
Degree of dehydration | Factor |
OutComeDisc |
Recorded outcome/disposition | Factor |
Data documentation note: The CSV records
Ageas a numerical variable andSexas codes 1 and 2. The unit ofAgeis not encoded in the CSV itself, so the original data documentation should be checked before describing it as months, years, or another unit.
A good statistical analysis starts with an audit. Do not immediately make graphs from an unfamiliar dataset.
## spc_tbl_ [1,296 × 9] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ ID : num [1:1296] 1 2 3 4 5 6 7 8 9 10 ...
## $ Department : chr [1:1296] "OPD" "OPD" "IPD" "IPD" ...
## $ Sex : num [1:1296] 1 2 1 1 1 2 2 1 2 1 ...
## $ Age : num [1:1296] 6 8 12 2 12 12 10 21 6 37 ...
## $ Fever : chr [1:1296] "No" "No" "No" "Yes" ...
## $ DiaMaxNo_24hrs: num [1:1296] 7 5 10 9 8 7 12 8 8 10 ...
## $ DiaDurInDays : num [1:1296] 2 1 3 1 2 5 2 2 2 2 ...
## $ DegreeDehy : chr [1:1296] "some" "some" "some" "some" ...
## $ OutComeDisc : chr [1:1296] NA NA "Improved" "Improved" ...
## - attr(*, "spec")=
## .. cols(
## .. ID = col_double(),
## .. Department = col_character(),
## .. Sex = col_double(),
## .. Age = col_double(),
## .. Fever = col_character(),
## .. DiaMaxNo_24hrs = col_double(),
## .. DiaDurInDays = col_double(),
## .. DegreeDehy = col_character(),
## .. OutComeDisc = col_character()
## .. )
## - attr(*, "problems")=<pointer: 0x000001db41464fb0>
## Rows: 1,296
## Columns: 9
## $ ID <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
## $ Department <chr> "OPD", "OPD", "IPD", "IPD", "IPD", "OPD", "IPD", "IPD",…
## $ Sex <dbl> 1, 2, 1, 1, 1, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 2, 1, 1, 1…
## $ Age <dbl> 6, 8, 12, 2, 12, 12, 10, 21, 6, 37, 12, 3, 23, 7, 6, 4,…
## $ Fever <chr> "No", "No", "No", "Yes", "Yes", "No", "No", "No", "No",…
## $ DiaMaxNo_24hrs <dbl> 7, 5, 10, 9, 8, 7, 12, 8, 8, 10, 7, 9, 3, 11, 10, 6, 12…
## $ DiaDurInDays <dbl> 2, 1, 3, 1, 2, 5, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 3, 2, 2…
## $ DegreeDehy <chr> "some", "some", "some", "some", "some", "some", "some",…
## $ OutComeDisc <chr> NA, NA, "Improved", "Improved", "Improved", NA, "Improv…
## ID Department Sex Age
## Min. : 1.0 Length :1296 Min. :1.000 Min. : 1.0
## 1st Qu.: 324.8 N.unique : 2 1st Qu.:1.000 1st Qu.: 5.0
## Median : 648.5 N.blank : 0 Median :2.000 Median : 9.0
## Mean : 648.5 Min.nchar: 3 Mean :1.667 Mean :11.2
## 3rd Qu.: 972.2 Max.nchar: 3 3rd Qu.:2.000 3rd Qu.:12.0
## Max. :1296.0 Max. :2.000 Max. :59.0
##
## Fever DiaMaxNo_24hrs DiaDurInDays DegreeDehy
## Length :1296 Min. : 2.000 Min. : 1.000 Length :1296
## N.unique : 2 1st Qu.: 7.000 1st Qu.: 1.000 N.unique : 4
## N.blank : 0 Median : 9.000 Median : 2.000 N.blank : 0
## Min.nchar: 2 Mean : 9.836 Mean : 2.316 Min.nchar: 4
## Max.nchar: 3 3rd Qu.:12.000 3rd Qu.: 3.000 Max.nchar: 6
## NAs : 1 Max. :35.000 Max. :52.000
## NAs :2
## OutComeDisc
## Length :1296
## N.unique : 1
## N.blank : 0
## Min.nchar: 8
## Max.nchar: 8
## NAs : 196
##
## ID Department Sex Age Fever
## "numeric" "character" "numeric" "numeric" "character"
## DiaMaxNo_24hrs DiaDurInDays DegreeDehy OutComeDisc
## "numeric" "numeric" "character" "character"
## $Department
## [1] "OPD" "IPD"
##
## $Sex
## [1] 1 2
##
## $Fever
## [1] "No" "Yes" NA
##
## $DegreeDehy
## [1] "some" "Severe" "severe" "Some"
##
## $OutComeDisc
## [1] NA "Improved"
Cleaning should be transparent and reproducible. Do not delete observations simply because they look unusual.
The source contains inconsistent capitalization in
DegreeDehy, such as some and
Some, and severe and Severe. We
can standardize these values without changing their meaning.
dirdata <- dirdata %>%
mutate(
Department = factor(str_to_upper(str_trim(Department))),
Sex = factor(Sex, levels = c(1, 2), labels = c("Male", "Female")),
Fever = factor(str_to_title(str_trim(Fever))),
DegreeDehy = factor(str_to_title(str_trim(DegreeDehy))),
OutComeDisc = factor(str_trim(OutComeDisc)),
ID = as.integer(ID)
)
glimpse(dirdata)## Rows: 1,296
## Columns: 9
## $ ID <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
## $ Department <fct> OPD, OPD, IPD, IPD, IPD, OPD, IPD, IPD, IPD, IPD, IPD, …
## $ Sex <fct> Male, Female, Male, Male, Male, Female, Female, Male, F…
## $ Age <dbl> 6, 8, 12, 2, 12, 12, 10, 21, 6, 37, 12, 3, 23, 7, 6, 4,…
## $ Fever <fct> No, No, No, Yes, Yes, No, No, No, No, No, No, No, No, N…
## $ DiaMaxNo_24hrs <dbl> 7, 5, 10, 9, 8, 7, 12, 8, 8, 10, 7, 9, 3, 11, 10, 6, 12…
## $ DiaDurInDays <dbl> 2, 1, 3, 1, 2, 5, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 3, 2, 2…
## $ DegreeDehy <fct> Some, Some, Some, Some, Some, Some, Some, Some, Some, S…
## $ OutComeDisc <fct> NA, NA, Improved, Improved, Improved, NA, Improved, Imp…
## [1] 0
Check duplicated IDs:
An observation that looks duplicated should be investigated against the original records. Never remove duplicates automatically.
dirdata %>%
summarise(
Age_min = min(Age, na.rm = TRUE),
Age_max = max(Age, na.rm = TRUE),
DiaMax_min = min(DiaMaxNo_24hrs, na.rm = TRUE),
DiaMax_max = max(DiaMaxNo_24hrs, na.rm = TRUE),
DiaDur_min = min(DiaDurInDays, na.rm = TRUE),
DiaDur_max = max(DiaDurInDays, na.rm = TRUE)
)Examples in this dataset include:
DepartmentSexFeverDegreeDehyOutComeDiscExamples include:
AgeDiaMaxNo_24hrsDiaDurInDaysDiaMaxNo_24hrs is a count and is therefore discrete in
its measurement scale, while Age and
DiaDurInDays are numerical measurements as recorded in the
dataset.
| Question | Variable structure | Suitable graph |
|---|---|---|
| How common is each category? | One categorical | Bar chart |
| How is a numerical variable distributed? | One numerical | Histogram/density |
| Are there potential outliers? | One numerical | Boxplot |
| How does a numerical variable differ by group? | Categorical + numerical | Boxplot/violin plot |
| Are two numerical variables associated? | Two numerical | Scatterplot |
| How do two categorical variables compare? | Two categorical | Grouped/proportional bar chart |
| How does a distribution differ by group? | Numerical + categorical | Faceted histogram/density |
The arithmetic mean is
\[\bar{x} = \frac{1}{n}\sum_{i=1}^{n}x_i.\]
## [1] 11.2037
## [1] 1
## [1] 59
## [1] 1 59
## [1] 58
## [1] 97.14921
## [1] 9.85643
## 25% 50% 75%
## 5 9 12
## [1] 7
describe_numeric <- function(x) {
tibble(
n = sum(!is.na(x)),
missing = sum(is.na(x)),
mean = mean(x, na.rm = TRUE),
median = median(x, na.rm = TRUE),
sd = sd(x, na.rm = TRUE),
min = min(x, na.rm = TRUE),
q1 = quantile(x, 0.25, na.rm = TRUE),
q3 = quantile(x, 0.75, na.rm = TRUE),
max = max(x, na.rm = TRUE),
IQR = IQR(x, na.rm = TRUE)
)
}
describe_numeric(dirdata$Age)##
## IPD OPD
## 1017 279
##
## Male Female
## 432 864
##
## No Yes
## 869 426
##
## Severe Some
## 238 1058
##
## Improved
## 1100
##
## IPD OPD
## 78.47222 21.52778
A tidy version:
##
## Male Female
## IPD 341 676
## OPD 91 188
##
## Male Female
## IPD 33.52999 66.47001
## OPD 32.61649 67.38351
barplot(table(dirdata$Department),
main = "Records by Department",
xlab = "Department",
ylab = "Frequency",
col = "skyblue")barplot(table(dirdata$Department),
horiz = TRUE,
main = "Records by Department",
xlab = "Frequency",
col = "lightgreen",
las = 1)dept <- prop.table(table(dirdata$Department)) * 100
barplot(dept,
main = "Percentage by Department",
ylab = "Percentage",
col = "lightblue")first we need a cross table
##
## Male Female
## IPD 341 676
## OPD 91 188
Bar diagram
tab <- table(dirdata$Department, dirdata$Sex)
barplot(tab,
beside = TRUE,
col = c("skyblue", "pink"),
main = "Department by Sex",
xlab = "Department",
ylab = "Frequency",
legend.text = colnames(tab))plot(dirdata$Age,
dirdata$DiaDurInDays,
main = "Age and Diarrhea Duration",
xlab = "Age",
ylab = "Duration",
pch = 16,
col = "blue")plot(dirdata$Age,
dirdata$DiaDurInDays,
main = "Age and Diarrhea Duration",
xlab = "Age",
ylab = "Duration",
pch = 16,
col = "gray")
model <- lm(DiaDurInDays ~ Age, data = dirdata)
abline(model,
col = "red",
lwd = 2)hist(dirdata$Age,
breaks = 20,
main = "Distribution of Age",
xlab = "Age",
ylab = "Frequency",
col = "lightblue")par(mfrow = c(1, 3))
hist(dirdata$Age,
breaks = 10,
main = "10 Bins",
col = "skyblue")
hist(dirdata$Age,
breaks = 20,
main = "20 Bins",
col = "lightgreen")
hist(dirdata$Age,
breaks = 30,
main = "30 Bins",
col = "pink")hist(dirdata$Age,
breaks = 20,
col = "lightblue",
main = "Age with Mean and Median",
xlab = "Age")
abline(v = mean(dirdata$Age, na.rm = TRUE),
col = "red",
lwd = 2,
lty = 2)
abline(v = median(dirdata$Age, na.rm = TRUE),
col = "blue",
lwd = 2,
lty = 3)
legend("topright",
legend = c("Mean", "Median"),
col = c("red", "blue"),
lty = c(2, 3),
lwd = 2)boxplot(Age ~ Department,
data = dirdata,
main = "Age by Department",
xlab = "Department",
ylab = "Age",
col = "lightblue")boxplot(DiaDurInDays ~ Department,
data = dirdata,
main = "Diarrhea Duration by Department",
xlab = "Department",
ylab = "Duration (Days)",
col = "lightpink")## [1] 37 23 23 32 24 48 24 48 27 24 49 24 24 48 24 36 32 24 28 24 24 59 48 36 24
## [26] 24 48 24 48 36 24 48 24 48 24 24 36 31 25 28 56 24 42 39 36 24 24 24 24 24
## [51] 36 48 23 24 36 23 26 36 25 36 36 48 36 45 24 24 24 24 24 24 24 48 48 24 48
## [76] 24 24 24 48 24 56 48 24 36 48 48 36 36 24 24 24 24 24 36 24 24 36 24 36 27
## [101] 24 24 36 23 24 31 23 49 36 42 24 48 24 29 30 30 24 30 36 48 30 29 24 24 29
## [126] 24 24 56 59 24 37 59 48 36 36 36 23 29 36 48 36 30 24 42 36 42 52 24 30 30
## [151] 24 24 59 36 24 36 24 36 36
plot(density(dirdata$Age, na.rm = TRUE),
main = "Density Distribution of Age",
xlab = "Age",
ylab = "Density",
col = "blue",
lwd = 2)hist(dirdata$Age,
probability = TRUE,
breaks = 20,
col = "lightblue",
main = "Histogram with Density Curve",
xlab = "Age")
lines(density(dirdata$Age, na.rm = TRUE),
col = "red",
lwd = 2)## [1] 0.1011935
##
## Pearson's product-moment correlation
##
## data: dirdata$Age and dirdata$DiaDurInDays
## t = 3.6561, df = 1292, p-value = 0.0002664
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.04695768 0.15483435
## sample estimates:
## cor
## 0.1011935
## Warning in cor.test.default(dirdata$Age, dirdata$DiaDurInDays, method =
## "spearman", : cannot compute exact p-value with ties
##
## Spearman's rank correlation rho
##
## data: dirdata$Age and dirdata$DiaDurInDays
## S = 313095263, p-value = 1.582e-06
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## 0.1329879
numeric_data <- dirdata[, c(
"Age",
"DiaMaxNo_24hrs",
"DiaDurInDays"
)]
cor_matrix <- cor(numeric_data,
use = "pairwise.complete.obs")
cor_matrix## Age DiaMaxNo_24hrs DiaDurInDays
## Age 1.00000000 -0.04324656 0.10119346
## DiaMaxNo_24hrs -0.04324656 1.00000000 0.06194965
## DiaDurInDays 0.10119346 0.06194965 1.00000000
image(1:ncol(cor_matrix),
1:nrow(cor_matrix),
cor_matrix,
axes = FALSE,
col = hcl.colors(20, "Blue-Red 3"),
main = "Correlation Matrix")
axis(1,
at = 1:ncol(cor_matrix),
labels = colnames(cor_matrix))
axis(2,
at = 1:nrow(cor_matrix),
labels = rownames(cor_matrix))par(mfrow = c(2, 2))
barplot(table(dirdata$Department),
main = "Bar Diagram",
col = "skyblue")
pie(table(dirdata$Sex),
main = "Pie Diagram")
hist(dirdata$Age,
main = "Histogram",
col = "lightgreen")
boxplot(dirdata$Age,
main = "Boxplot",
col = "pink")ggplot is a function from the ggplot2 package in R used to create high-quality and attractive graphs and charts.
ggplot2 is one of the most widely used packages for data visualization in R.
The ggplot2 has not been designed in our course however the trainee can practice using this tutorials.
ggplot(dirdata, aes(x = Age)) +
geom_histogram(
bins = 20,
color = "white"
) +
labs(
title = "Distribution of Age",
x = "Age",
y = "Frequency"
)The number of bins affects the appearance of a histogram.
Try several values:
p10 <- ggplot(dirdata, aes(x = Age)) +
geom_histogram(bins = 10, color = "white") +
labs(title = "10 bins")
p20 <- ggplot(dirdata, aes(x = Age)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "20 bins")
p30 <- ggplot(dirdata, aes(x = Age)) +
geom_histogram(bins = 30, color = "white") +
labs(title = "30 bins")
p10 + p20 + p30ggplot(dirdata, aes(x = Age)) +
geom_density(na.rm = TRUE) +
labs(
title = "Density of Age",
x = "Age",
y = "Density"
)ggplot(dirdata, aes(x = Age)) +
geom_histogram(
aes(y = after_stat(density)),
bins = 20,
color = "white"
) +
geom_density(linewidth = 1) +
labs(
title = "Age Distribution with Density Curve",
x = "Age",
y = "Density"
)age_mean <- mean(dirdata$Age, na.rm = TRUE)
age_median <- median(dirdata$Age, na.rm = TRUE)
ggplot(dirdata, aes(x = Age)) +
geom_histogram(bins = 20, color = "white") +
geom_vline(
xintercept = age_mean,
linetype = "dashed",
linewidth = 1
) +
geom_vline(
xintercept = age_median,
linetype = "dotted",
linewidth = 1
) +
labs(
title = "Age Distribution with Mean and Median",
x = "Age",
y = "Frequency"
)ggplot(dirdata, aes(y = Age)) +
geom_boxplot() +
labs(
title = "Boxplot of Age",
y = "Age",
x = NULL
)## $stats
## [1] 1 5 9 12 22
##
## $n
## [1] 1296
##
## $conf
## [1] 8.692778 9.307222
##
## $out
## [1] 37 23 23 32 24 48 24 48 27 24 49 24 24 48 24 36 32 24 28 24 24 59 48 36 24
## [26] 24 48 24 48 36 24 48 24 48 24 24 36 31 25 28 56 24 42 39 36 24 24 24 24 24
## [51] 36 48 23 24 36 23 26 36 25 36 36 48 36 45 24 24 24 24 24 24 24 48 48 24 48
## [76] 24 24 24 48 24 56 48 24 36 48 48 36 36 24 24 24 24 24 36 24 24 36 24 36 27
## [101] 24 24 36 23 24 31 23 49 36 42 24 48 24 29 30 30 24 30 36 48 30 29 24 24 29
## [126] 24 24 56 59 24 37 59 48 36 36 36 23 29 36 48 36 30 24 42 36 42 52 24 30 30
## [151] 24 24 59 36 24 36 24 36 36
## [1] 37 23 23 32 24 48 24 48 27 24 49 24 24 48 24 36 32 24 28 24 24 59 48 36 24
## [26] 24 48 24 48 36 24 48 24 48 24 24 36 31 25 28 56 24 42 39 36 24 24 24 24 24
## [51] 36 48 23 24 36 23 26 36 25 36 36 48 36 45 24 24 24 24 24 24 24 48 48 24 48
## [76] 24 24 24 48 24 56 48 24 36 48 48 36 36 24 24 24 24 24 36 24 24 36 24 36 27
## [101] 24 24 36 23 24 31 23 49 36 42 24 48 24 29 30 30 24 30 36 48 30 29 24 24 29
## [126] 24 24 56 59 24 37 59 48 36 36 36 23 29 36 48 36 30 24 42 36 42 52 24 30 30
## [151] 24 24 59 36 24 36 24 36 36
A boxplot outlier is an observation identified by a statistical rule. It is not automatically an error. Check the original record before deciding whether an observation should be corrected, retained, or excluded.
ggplot(dirdata, aes(x = Department, y = Age)) +
geom_boxplot() +
labs(
title = "Age Distribution by Department",
x = "Department",
y = "Age"
)ggplot(dirdata, aes(x = Department, y = Age)) +
geom_boxplot(outlier.shape = NA) +
geom_jitter(
width = 0.12,
alpha = 0.5
) +
labs(
title = "Age by Department with Individual Observations",
x = "Department",
y = "Age"
)ggplot(dirdata, aes(x = Department, y = DiaDurInDays)) +
geom_boxplot(outlier.shape = NA) +
geom_jitter(width = 0.12, alpha = 0.4) +
labs(
title = "Diarrhea Duration by Department",
x = "Department",
y = "Duration"
)## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_point()`).
ggplot(
dirdata,
aes(x = Age, y = DiaDurInDays)
) +
geom_point(alpha = 0.6) +
labs(
title = "Age and Diarrhea Duration",
x = "Age",
y = "Duration"
)## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_point()`).
ggplot(
dirdata,
aes(x = Age, y = DiaDurInDays)
) +
geom_point(alpha = 0.5) +
geom_smooth(
method = "lm",
se = TRUE
) +
labs(
title = "Age and Diarrhea Duration",
x = "Age",
y = "Duration"
)## `geom_smooth()` using formula = 'y ~ x'
## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_point()`).
The regression line is a summary of the observed association. It does not establish causation.
ggplot(
dirdata,
aes(x = DiaMaxNo_24hrs, y = DiaDurInDays)
) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = TRUE) +
labs(
title = "Maximum Episodes and Diarrhea Duration",
x = "Maximum episodes in 24 hours",
y = "Duration"
)## `geom_smooth()` using formula = 'y ~ x'
## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_point()`).
## [1] 0.1011935
##
## Pearson's product-moment correlation
##
## data: dirdata$Age and dirdata$DiaDurInDays
## t = 3.6561, df = 1292, p-value = 0.0002664
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.04695768 0.15483435
## sample estimates:
## cor
## 0.1011935
For ordinal, non-normal, or strongly skewed variables, Spearman correlation may be useful:
## Warning in cor.test.default(dirdata$Age, dirdata$DiaDurInDays, use =
## "complete.obs", : cannot compute exact p-value with ties
##
## Spearman's rank correlation rho
##
## data: dirdata$Age and dirdata$DiaDurInDays
## S = 313095263, p-value = 1.582e-06
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## 0.1329879
Correlation measures association, not causation. Also inspect a scatterplot before relying on a single correlation coefficient.
numeric_data <- dirdata %>%
select(
Age,
DiaMaxNo_24hrs,
DiaDurInDays
)
cor(
numeric_data,
use = "pairwise.complete.obs"
)## Age DiaMaxNo_24hrs DiaDurInDays
## Age 1.00000000 -0.04324656 0.10119346
## DiaMaxNo_24hrs -0.04324656 1.00000000 0.06194965
## DiaDurInDays 0.10119346 0.06194965 1.00000000
A simple visual representation:
cor_matrix <- cor(
numeric_data,
use = "pairwise.complete.obs"
)
image(
1:ncol(cor_matrix),
1:nrow(cor_matrix),
cor_matrix,
axes = FALSE,
xlab = "",
ylab = "",
col = hcl.colors(20, "Blue-Red 3")
)
axis(
1,
at = 1:ncol(cor_matrix),
labels = colnames(cor_matrix)
)
axis(
2,
at = 1:nrow(cor_matrix),
labels = rownames(cor_matrix)
)Faceting is useful when the same graph should be repeated across groups.
numeric_summary <- dirdata %>%
summarise(
across(
c(Age, DiaMaxNo_24hrs, DiaDurInDays),
list(
N = ~ sum(!is.na(.)),
Mean = ~ mean(.x, na.rm = TRUE),
SD = ~ sd(.x, na.rm = TRUE),
Median = ~ median(.x, na.rm = TRUE),
IQR = ~ IQR(.x, na.rm = TRUE),
Min = ~ min(.x, na.rm = TRUE),
Max = ~ max(.x, na.rm = TRUE)
)
)
)
numeric_summarydirdata %>%
group_by(Department) %>%
summarise(
n = n(),
Mean_Age = mean(Age, na.rm = TRUE),
SD_Age = sd(Age, na.rm = TRUE),
Median_Age = median(Age, na.rm = TRUE),
IQR_Age = IQR(Age, na.rm = TRUE),
Mean_Duration = mean(DiaDurInDays, na.rm = TRUE),
Median_Duration = median(DiaDurInDays, na.rm = TRUE),
.groups = "drop"
)A confidence interval describes uncertainty around an estimated mean.
mean_ci <- function(x, conf = 0.95) {
x <- x[!is.na(x)]
n <- length(x)
m <- mean(x)
se <- sd(x) / sqrt(n)
tcrit <- qt((1 + conf) / 2, df = n - 1)
tibble(
n = n,
mean = m,
lower = m - tcrit * se,
upper = m + tcrit * se
)
}
mean_ci(dirdata$Age)By Department:
ci_department <- dirdata %>%
group_by(Department) %>%
summarise(
mean_ci(Age),
.groups = "drop"
)
ci_departmentPlot:
ggplot(
ci_department,
aes(x = Department, y = mean)
) +
geom_point(size = 3) +
geom_errorbar(
aes(ymin = lower, ymax = upper),
width = 0.15
) +
labs(
title = "Mean Age with 95% Confidence Intervals",
x = "Department",
y = "Mean Age"
)The main purpose of this module is descriptive analysis. If students continue to inferential statistics, the dataset can be used for introductory examples.
Example research question:
Is Department associated with Sex?
##
## Pearson's Chi-squared test with Yates' continuity correction
##
## data: tab
## X-squared = 0.046246, df = 1, p-value = 0.8297
Before interpreting the result, inspect expected counts:
##
## Male Female
## IPD 339 678
## OPD 93 186
A t-test may be considered when its assumptions are reasonably supported:
##
## Welch Two Sample t-test
##
## data: Age by Department
## t = 4.0155, df = 692.95, p-value = 6.581e-05
## alternative hypothesis: true difference in means between group IPD and group OPD is not equal to 0
## 95 percent confidence interval:
## 1.066338 3.106847
## sample estimates:
## mean in group IPD mean in group OPD
## 11.652901 9.566308
A non-parametric alternative is:
##
## Wilcoxon rank sum test with continuity correction
##
## data: Age by Department
## W = 151322, p-value = 0.08727
## alternative hypothesis: true location shift is not equal to 0
These tests should not be presented as automatic steps. Students should first examine the study design, measurement scale, distribution, independence, missing data, and assumptions.
A graph should lead to a description supported by the graph, not an unsupported conclusion.
A useful interpretation discusses:
Avoid:
“Age causes longer diarrhea duration.”
A scatterplot cannot support a causal statement.
Prefer:
“The scatterplot shows the observed association between age and diarrhea duration. The direction and strength should be assessed from the pattern of points and an appropriate correlation or model.”
A good interpretation may discuss:
A research graph should have:
Example:
p <- ggplot(
dirdata,
aes(x = Department, y = DiaDurInDays)
) +
geom_boxplot(outlier.shape = NA) +
geom_jitter(
width = 0.1,
alpha = 0.35
) +
labs(
title = "Distribution of Diarrhea Duration by Department",
x = "Department",
y = "Diarrhea duration"
) +
theme_classic()
p## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_point()`).
ggsave(
"diarrhea_duration_by_department.png",
plot = p,
width = 7,
height = 5,
units = "in",
dpi = 300
)For PDF:
The following workflow is recommended for a real analysis.
## Rows: 1296 Columns: 9
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): Department, Fever, DegreeDehy, OutComeDisc
## dbl (5): ID, Sex, Age, DiaMaxNo_24hrs, DiaDurInDays
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
### Clean variable types
data <- data %>%
mutate(
Department = factor(str_to_upper(str_trim(Department))),
Sex = factor(Sex, levels = c(1, 2),
labels = c("Male", "Female")),
Fever = factor(str_to_title(str_trim(Fever)),
levels = c("No", "Yes")),
DegreeDehy = factor(
str_to_lower(str_trim(DegreeDehy)),
levels = c("some", "severe"),
labels = c("Some", "Severe")
),
OutComeDisc = factor(str_trim(OutComeDisc))
)
### Audit
dim(data)## [1] 1296 9
## tibble [1,296 × 9] (S3: tbl_df/tbl/data.frame)
## $ ID : num [1:1296] 1 2 3 4 5 6 7 8 9 10 ...
## $ Department : Factor w/ 2 levels "IPD","OPD": 2 2 1 1 1 2 1 1 1 1 ...
## $ Sex : Factor w/ 2 levels "Male","Female": 1 2 1 1 1 2 2 1 2 1 ...
## $ Age : num [1:1296] 6 8 12 2 12 12 10 21 6 37 ...
## $ Fever : Factor w/ 2 levels "No","Yes": 1 1 1 2 2 1 1 1 1 1 ...
## $ DiaMaxNo_24hrs: num [1:1296] 7 5 10 9 8 7 12 8 8 10 ...
## $ DiaDurInDays : num [1:1296] 2 1 3 1 2 5 2 2 2 2 ...
## $ DegreeDehy : Factor w/ 2 levels "Some","Severe": 1 1 1 1 1 1 1 1 1 1 ...
## $ OutComeDisc : Factor w/ 1 level "Improved": NA NA 1 1 1 NA 1 1 1 1 ...
## ID Department Sex Age Fever
## 0 0 0 0 1
## DiaMaxNo_24hrs DiaDurInDays DegreeDehy OutComeDisc
## 0 2 0 196
### Describe numerical variables
data %>%
summarise(
across(
c(Age, DiaMaxNo_24hrs, DiaDurInDays),
list(
mean = ~ mean(.x, na.rm = TRUE),
median = ~ median(.x, na.rm = TRUE),
sd = ~ sd(.x, na.rm = TRUE),
IQR = ~ IQR(.x, na.rm = TRUE)
)
)
)### Describe categorical variables
data %>%
count(Department) %>%
mutate(Percent = 100 * n / sum(n))### Visualize
ggplot(data, aes(x = Age)) +
geom_histogram(bins = 20, color = "white") +
labs(
title = "Distribution of Age",
x = "Age",
y = "Frequency"
)### Compare groups
ggplot(data, aes(x = Department, y = Age)) +
geom_boxplot() +
labs(
title = "Age by Department",
x = "Department",
y = "Age"
)### Examine a relationship
ggplot(data, aes(x = Age, y = DiaDurInDays)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = TRUE)## `geom_smooth()` using formula = 'y ~ x'
## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_point()`).
Using dirdata:
DegreeDehy before
cleaning?Calculate for Age, DiaMaxNo_24hrs, and
DiaDurInDays:
Then explain why the mean and median may differ.
Calculate frequency and percentage tables for:
Then create a bar chart for each variable.
Compare Age between Departments using:
Write a short paragraph describing what the graphs show.
Study DiaDurInDays:
Do not remove observations simply because they are outliers.
Investigate:
Age versus DiaDurInDays;DiaMaxNo_24hrs versus DiaDurInDays.For each:
Investigate the relationship between:
For each:
Create one final figure suitable for inclusion in a report or manuscript. It must contain:
Write 150–250 words describing the dataset using:
Do not make causal claims.
Conduct a complete exploratory analysis of the diarrhea dataset.
Your report should contain:
Report appropriate statistics for numerical variables.
Report frequencies and percentages.
Include at least:
Discuss:
Explain what the analysis shows without making unsupported causal claims.
Counts such as DiaMaxNo_24hrs are discrete counts.
Do not label Age as months, years, or another unit
unless the original documentation confirms it.
A missing value is not automatically zero.
First investigate whether the value is a valid observation.
A correlation does not demonstrate a causal relationship.
Bar charts generally make category comparisons easier.
Use colour when it communicates information, not simply for decoration.
Statistical analysis should also communicate effect size, uncertainty, direction, and practical meaning where appropriate.
| Purpose | R function |
|---|---|
| Mean | mean() |
| Median | median() |
| Minimum | min() |
| Maximum | max() |
| Range | range() |
| Variance | var() |
| Standard deviation | sd() |
| Quartiles | quantile() |
| IQR | IQR() |
| Five-number summary | fivenum() |
| Purpose | R function |
|---|---|
| Frequency | table() / count() |
| Proportion | prop.table() |
| Cross-tabulation | table() |
| Tidy cross-tabulation | count() |
| Purpose | Function |
|---|---|
| Start graph | ggplot() |
| Histogram | geom_histogram() |
| Density | geom_density() |
| Boxplot | geom_boxplot() |
| Violin plot | geom_violin() |
| Points | geom_point() |
| Jitter | geom_jitter() |
| Bar chart | geom_bar() |
| Trend line | geom_smooth() |
| Labels | labs() |
| Faceting | facet_wrap() / facet_grid() |
| Flip coordinates | coord_flip() |
| Save figure | ggsave() |
START
|
+-- What type of variable?
|
+-- Categorical
| |
| +-- One variable
| | -> Bar chart
| |
| +-- Two variables
| -> Grouped / proportional bar chart
|
+-- Numerical
|
+-- One variable
| -> Histogram / Density / Boxplot
|
+-- Numerical + categorical
| -> Boxplot / Violin / Faceted distribution
|
+-- Two numerical variables
-> Scatterplot
-> Add trend line if appropriate
-> Consider correlation
Before presenting an analysis, ask:
Good statistical analysis is more than producing numbers and attractive graphs.
A strong exploratory analysis follows a logical sequence:
The diarrhea dataset provides a useful teaching example because it
contains numerical and categorical variables, missing values,
inconsistent text labels, group structure, and observations that warrant
investigation. These features make it suitable not only for learning
ggplot2 and descriptive statistics, but also for learning
an essential research skill: checking the data before trusting
the analysis.