BTE3207 week 5

Minsik Kim

2026 Fall

Before we begin

Let’s use the SBP dataset again. We will run a few lines when each topic comes up in class. The order is: normal distribution, confidence intervals, then the t-distribution and small samples.

Open BTE3207_Advanced_Biostatistics.Rproj and run the chunks from the top. Keep the CSV in the dataset folder. The code uses base R; no extra analysis package is needed.

dataset_sbp <- read.csv("dataset/sbp_dataset_korea_2013-2014.csv")
head(dataset_sbp)
dim(dataset_sbp)
## [1] 1000000       7

What do these variables mean?

Variable Meaning
BTH_G Age-group code (연령군): 1 = ages 1–5, 2 = ages 6–10, 3 = ages 11–13. The remaining codes also represent groups; their age ranges are not specified here.
SBP Systolic blood pressure (수축기혈압), mmHg
DBP Diastolic blood pressure (이완기혈압), mmHg
FBS Fasting blood sugar (공복혈당), mg/dL
SEX Recorded sex: 1 = Male; 2 = Female
DIS Disease history: 1 = both hypertension and diabetes; 2 = hypertension only; 3 = diabetes only; 4 = neither
BMI Body mass index (체질량지수), kg/m²

Numbers can be category labels! BTH_G = 10 does not mean age 10, and the mean of DIS does not describe disease severity. We will keep the original columns and create new variables beside them.

table(dataset_sbp$SEX, useNA = "ifany")
## 
##      1      2 
## 510227 489773
table(dataset_sbp$DIS, useNA = "ifany")
## 
##      1      2      3      4 
##  53398 162826  43114 740662
colSums(is.na(dataset_sbp))
##   SEX BTH_G   SBP   DBP   FBS   DIS   BMI 
##     0     0     0     0     0     0     0

This file contains only the stated SEX/DIS codes and has no missing values. When using another file, check its codes and missing values first.

Changing 1 and 2 to 0 and 1

We want Female = 1, Male = 0. The variable name female tells us what 1 means. == asks whether two values are equal; <- saves a result.

dataset_sbp$female <- ifelse(dataset_sbp$SEX == 2, 1, 0)
table(dataset_sbp$SEX, dataset_sbp$female)
##    
##          0      1
##   1 510227      0
##   2      0 489773

ifelse(condition, yes, no) works like IF in Excel. For each row, R asks whether SEX is 2. If yes, put 1; otherwise, put 0. The table checks that 1 became 0 and 2 became 1.

mean(dataset_sbp$female)
## [1] 0.489773
mean(dataset_sbp$female) * 100
## [1] 48.9773

Voila! The mean of a 0/1 variable is the proportion coded 1. Here, it is the proportion of females: 48.98%. Reversing the coding would give the proportion of males instead.

Try this: run head(dataset_sbp$SEX - 1). Subtracting 1 also works here because the only original codes are 1 and 2. It would not be a general solution for other category codes. Do not overwrite SEX.

Disease history: OR means either condition

Hypertension history includes DIS 1 or 2. Diabetes history includes DIS 1 or 3. The | symbol means OR, and applies to each row.

dataset_sbp$htn_history <- ifelse(dataset_sbp$DIS == 1 |
                                  dataset_sbp$DIS == 2, 1, 0)
dataset_sbp$dm_history <- ifelse(dataset_sbp$DIS == 1 |
                                 dataset_sbp$DIS == 3, 1, 0)
table(dataset_sbp$DIS, dataset_sbp$htn_history)
##    
##          0      1
##   1      0  53398
##   2      0 162826
##   3  43114      0
##   4 740662      0
table(dataset_sbp$DIS, dataset_sbp$dm_history)
##    
##          0      1
##   1      0  53398
##   2 162826      0
##   3      0  43114
##   4 740662      0

Both indicators are 1 when DIS is 1. These variables describe recorded history, rather than a diagnosis made from today’s SBP or FBS.

A small sample for class

For this exercise, treat the CSV as a teaching population and take a random sample. We are practicing sampling uncertainty; these calculations alone do not establish estimates for all Koreans.

set.seed(20260005)
class_sbp <- dataset_sbp[sample(nrow(dataset_sbp), 113), ]
x <- class_sbp$SBP
c(n = length(x), mean = mean(x), SD = sd(x))
##         n      mean        SD 
## 113.00000 122.76106  15.72037

set.seed() makes the random sample reproducible. Run it together with the sampling line when you want the same sample again.

The normal distribution: a short recap

We can use R as a calculator. pnorm() gives the probability to the left of a value under a normal distribution; qnorm() works in the opposite direction. Both functions are in the stats package, loaded with R.

pnorm(1) - pnorm(-1)
## [1] 0.6826895
pnorm(2) - pnorm(-2)
## [1] 0.9544997
qnorm(0.975)
## [1] 1.959964

The middle 95% of a normal distribution is about ±1.96 SD. ±2 SD gives about 95.45%, so “two SD” is a convenient approximation.

z-score and normalization

Subtract the sample mean, then divide by the sample SD.

z_sbp <- (x - mean(x)) / sd(x)
c(mean = mean(z_sbp), SD = sd(z_sbp))
##          mean            SD 
## -1.385323e-16  1.000000e+00
hist(z_sbp, main = "Standardized SBP", xlab = "z-score")

The mean is approximately 0 and SD is 1. A value such as 1e-16 is a tiny numerical rounding error. Standardization does not make a skewed distribution normal.

Min–max scaling is another transformation. It sets this sample’s minimum to 0 and maximum to 1, while keeping the shape.

sbp_01 <- (x - min(x)) / (max(x) - min(x))
range(sbp_01)
## [1] 0 1

Unlike female, sbp_01 is continuous: a value of 1 now means the largest SBP in this sample, not membership in a category.

An observed proportion and a normal approximation

mean(x <= 130)
## [1] 0.6637168
pnorm(130, mean = mean(x), sd = sd(x))
## [1] 0.6774147

The first line counts what we observed. The second uses a fitted normal curve. They need not agree exactly. Here, 130 is just a point on the SBP scale for this calculation.

hist(class_sbp$FBS, main = "FBS in our sample", xlab = "FBS (mg/dL)")

FBS can be right-skewed. A z-score is still defined, but pnorm(z) is not automatically a correct percentile for skewed data.

Confidence interval of a mean

SD describes variation between people. SE describes variation between sample means. For independent observations, we estimate SE by \(s / \sqrt{n}\).

n <- length(x)
x_bar <- mean(x)
se_mean <- sd(x) / sqrt(n)
c(mean = x_bar, SD = sd(x), SE = se_mean)
##       mean         SD         SE 
## 122.761062  15.720366   1.478848

An approximate 95% CI is sample mean ± 1.96 × SE. It is centered on the sample mean; the population mean is what we are trying to estimate.

ci_mean_normal <- x_bar + c(-1, 1) * qnorm(0.975) * se_mean
ci_mean_normal
## [1] 119.8626 125.6596

Our sample mean is 122.76 mmHg, and this approximate interval is 119.86 to 125.66 mmHg. Later in this lesson, we will use a t critical value because we estimated the population SD from the sample.

Changing the confidence level

x_bar + c(-1, 1) * qnorm(0.95) * se_mean   # 90% CI
## [1] 120.3286 125.1935
x_bar + c(-1, 1) * qnorm(0.975) * se_mean  # 95% CI
## [1] 119.8626 125.6596
x_bar + c(-1, 1) * qnorm(0.995) * se_mean  # 99% CI
## [1] 118.9518 126.5703

Higher confidence gives a wider interval for the same data and method.

Try this: replace x with class_sbp$DBP and calculate its mean, SE and approximate 95% CI. Use the DBP sample SD and its sample size.

FBS uses the same SE calculation

fbs <- class_sbp$FBS
se_fbs <- sd(fbs) / sqrt(length(fbs))
mean(fbs) + c(-1, 1) * qnorm(0.975) * se_fbs
## [1]  94.46032 104.01756

The denominator uses the number of people in this sample. It does not use the number of simulation repetitions. A skewed distribution may require a larger sample for the normal approximation to work well; no single sample-size cutoff works for every distribution.

Confidence interval of a proportion

Now use the 0/1 variable we made earlier. Let the outcome be recorded hypertension history, and estimate its proportion in our sample.

yes <- sum(class_sbp$htn_history)
n_people <- nrow(class_sbp)
p_hat <- yes / n_people
se_p <- sqrt(p_hat * (1 - p_hat) / n_people)
c(yes = yes, n = n_people, proportion = p_hat, SE = se_p)
##          yes            n   proportion           SE 
##  32.00000000 113.00000000   0.28318584   0.04238379
ci_p_normal <- p_hat + c(-1, 1) * qnorm(0.975) * se_p
ci_p_normal
## [1] 0.2001151 0.3662566
ci_p_normal * 100
## [1] 20.01151 36.62566

This is the normal approximation, often called a Wald interval. Its performance can be poor with small counts or a proportion near 0 or 1. We will compare it with an exact interval below.

Try this: use female instead of htn_history. What does the estimated proportion refer to now? Keep the original data unchanged.

What does 95% mean?

A CI estimates a population mean or proportion, not a range containing 95% of individual SBP values. Under the method’s assumptions, about 95% of intervals from repeated samples would contain the fixed true parameter. After we calculate one interval, it either contains that parameter or it does not.

Optional: see repeated intervals

This is a short demonstration to watch in class. replicate() repeats the calculation. We can check coverage here because the whole teaching population is available.

set.seed(20260501)
ci_many <- replicate(100, {
  one_sample <- sample(dataset_sbp$SBP, 113, replace = TRUE)
  mean(one_sample) + c(-1, 1) * qnorm(0.975) *
    sd(one_sample) / sqrt(113)
})
pool_mean <- mean(dataset_sbp$SBP)
covered <- ci_many[1, ] <= pool_mean & pool_mean <= ci_many[2, ]
mean(covered)
## [1] 0.94
plot(NA, xlim = c(1, 100), ylim = range(ci_many),
     xlab = "Sample number", ylab = "95% CI for mean SBP (mmHg)")
segments(1:100, ci_many[1, ], 1:100, ci_many[2, ],
         col = ifelse(covered, "grey50", "red"))
abline(h = pool_mean, col = "blue", lwd = 2)

The blue line is the teaching population mean. Red intervals missed it. The observed coverage need not be exactly 95% in just 100 repetitions, and these intervals use an approximation.

Comparing two groups: a preview

Let’s look at SBP by recorded sex. Give the categories readable labels.

class_sbp$sex <- factor(class_sbp$SEX, levels = c(1, 2),
                        labels = c("Male", "Female"))
boxplot(SBP ~ sex, data = class_sbp,
        ylab = "SBP (mmHg)", xlab = "Recorded sex")

Overlapping boxes do not answer whether population means differ. Next week, we will estimate and test the difference between means. We do not pair unrelated people by subtracting their rows one by one.

The t-distribution and degrees of freedom

When estimating one mean, the degrees of freedom are \(n - 1\). The t critical value accounts for uncertainty from estimating the population SD. It also applies to large samples, where it approaches the normal critical value.

qnorm(0.975)
## [1] 1.959964
qt(0.975, df = 4)
## [1] 2.776445
qt(0.975, df = n - 1)
## [1] 1.981372
curve(dnorm(x), from = -4, to = 4,
      ylab = "Density", xlab = "Value", lwd = 2)
curve(dt(x, df = 4), add = TRUE, col = "red", lwd = 2)
legend("topright", c("Standard normal", "t, df = 4"),
       col = c("black", "red"), lwd = 2)

The t-distribution has heavier tails. A small sample does not become normal just because we use a t method. The usual t interval is exact for independent normal observations and approximate more generally.

t.test() calculates the mean CI

ci_mean_t <- x_bar + c(-1, 1) * qt(0.975, df = n - 1) * se_mean
ci_mean_t
## [1] 119.8309 125.6912
t.test(x)$conf.int
## [1] 119.8309 125.6912
## attr(,"conf.level")
## [1] 0.95

The endpoints agree. $conf.int extracts only the confidence interval; we will discuss the test’s p-value next week. With a smaller sample, the interval can be much wider.

set.seed(20260502)
small_sbp <- sample(dataset_sbp$SBP, 12)
t.test(small_sbp)$conf.int
## [1] 109.2854 127.3812
## attr(,"conf.level")
## [1] 0.95

A small sample proportion

Use a small sample from the same dataset and count people with a history of diabetes. binom.test() takes a count of successes, not a proportion.

set.seed(20260503)
small_data <- dataset_sbp[sample(nrow(dataset_sbp), 20), ]
yes_small <- sum(small_data$dm_history)
c(yes = yes_small, n = nrow(small_data))
## yes   n 
##   0  20
binom.test(x = yes_small, n = nrow(small_data))$conf.int
## [1] 0.0000000 0.1684335
## attr(,"conf.level")
## [1] 0.95

Even when we observe 0 cases, the population proportion need not be 0. Check the upper endpoint before interpreting the result.

This is a Clopper–Pearson exact binomial interval. “Exact” refers to its binomial probability calculation; it does not fix biased sampling or dependent observations. Score intervals are another option, so proportion intervals do not all have to use the same method.

Try this: the lecture’s cold-symptom example has 3 successes out of 20. Run binom.test(3, 20)$conf.int. Compare it with 0.15 + c(-1, 1) * 1.96 * sqrt(0.15 * 0.85 / 20). The normal interval extends below 0, which cannot be a proportion.

Before next class

Can you explain what 1 means in female, htn_history, and dm_history? Can you identify whether an interval refers to a mean or a proportion? Next week, we will keep using the same dataset to compare groups.

Function details: t.test, binom.test.

Bibliography

R Core Team (2024). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. https://www.R-project.org/.

Xie Y (2025). knitr: A General-Purpose Package for Dynamic Report Generation in R. R package version 1.50, https://yihui.org/knitr/.

Xie Y (2015). Dynamic Documents with R and knitr, 2nd edition. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 978-1498716963, https://yihui.org/knitr/.

Xie Y (2014). “knitr: A Comprehensive Tool for Reproducible Research in R.” In Stodden V, Leisch F, Peng RD (eds.), Implementing Reproducible Computational Research. Chapman and Hall/CRC. ISBN 978-1466561595.

Allaire J, Xie Y, Dervieux C, McPherson J, Luraschi J, Ushey K, Atkins A, Wickham H, Cheng J, Chang W, Iannone R (2025). rmarkdown: Dynamic Documents for R. R package version 2.30, https://github.com/rstudio/rmarkdown.

Xie Y, Allaire J, Grolemund G (2018). R Markdown: The Definitive Guide. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 9781138359338, https://bookdown.org/yihui/rmarkdown.

Xie Y, Dervieux C, Riederer E (2020). R Markdown Cookbook. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 9780367563837, https://bookdown.org/yihui/rmarkdown-cookbook.

Barnier J (2022). rmdformats: HTML Output Formats and Templates for ‘rmarkdown’ Documents. R package version 1.0.4, https://CRAN.R-project.org/package=rmdformats.