1. Overview

This session contains a complete, self-contained set of R commands that

  1. simulate a data set on lung cancer status and six explanatory variables.

  2. assemble the variables into a data frame with properly labelled factors, and

  3. fit a binary logistic regression model whose parameters are reported as odds ratios with 95% confidence intervals.

Because the data are simulated from a model with known parameters, the estimated odds ratios can be compared directly with the true values used to generate the data.

2. The statistical model

The outcome is binary (lung cancer: No / Yes), so a logistic regression model is used. On the logit scale the model is: logit[ P(lung cancer = Yes) ] = b0 + b1(smoking) + b2(sex) + b3(residence) + b4(married) + b5(mining) + b6(age)

2.1 Variables and the true parameters used in the simulation

Table 1: Description of the variables

Variable Type Categories Reference True OR used
lung_cancer Factor No, Yes No -
smoking Factor No, Yes No 4.0
sex Factor Female, Male Female 1.6
residence Factor Rural, Urban Rural 1.3
married Factor No, Yes No 0.8
mining Factor No, Yes No 2.5
age Numeric Years (18–85) - 1.05 per year

In R, the first level of a factor is automatically the reference category, so the levels are deliberately ordered No, Female and Rural first.

3. Sample size and simulations

Step 1 — Set the seed and the sample size

The seed makes the simulation reproducible; n is the number of individuals.

set.seed(12345)          # makes the simulation reproducible
n <- 3000                # number of individuals to simulate

Step 2 — Simulate the regressors

Age is drawn from a normal distribution and trimmed to a plausible adult range. The five binary variables are drawn from Bernoulli distributions using rbinom() with size = 1; the probability supplied is the prevalence of the “Yes” category.

# Age: numeric, mean 45 years, SD 12, restricted to 18-85
age <- round(rnorm(n, mean = 45, sd = 12))
age[age < 18] <- 18
age[age > 85] <- 85
 
# Binary regressors, coded 0/1 for now (labels are added in Step 4)
smoking   <- rbinom(n, 1, 0.45)   # 45% smokers
sex       <- rbinom(n, 1, 0.50)   # 50% male
residence <- rbinom(n, 1, 0.40)   # 40% urban
married   <- rbinom(n, 1, 0.60)   # 60% married
mining    <- rbinom(n, 1, 0.15)   # 15% work in mining

Step 3 — Simulate the outcome from a known logistic model

The linear predictor is built from the true log-odds (the logarithms of the odds ratios in Table 1), converted to a probability with the inverse-logit function, and the outcome is then drawn from a Bernoulli distribution with that probability.

# True regression coefficients = log(odds ratio)
b0        <- -5.5          # intercept, controls overall prevalence
b_smoking <- log(4.0)      # OR = 4.0 for smokers
b_sex     <- log(1.6)      # OR = 1.6 for males
b_resid   <- log(1.3)      # OR = 1.3 for urban residents
b_married <- log(0.8)      # OR = 0.8 for married
b_mining  <- log(2.5)      # OR = 2.5 for mine workers
b_age     <- log(1.05)     # OR = 1.05 per additional year
 
# Linear predictor
eta <- b0 + b_smoking * smoking + b_sex * sex + b_resid * residence +
       b_married * married + b_mining * mining + b_age * age
 
# Convert to a probability and draw the outcome
p <- 1 / (1 + exp(-eta))          # same as plogis(eta)
lung_cancer <- rbinom(n, 1, p)

Step 4 — Create the data frame and label the factors

factor() converts the 0/1 codes into labelled categorical variables. The order given in levels = decides the reference category, which is what the odds ratios will be compared against. Age is left as numeric.

lung <- data.frame(lung_cancer = factor(lung_cancer, levels = c(0, 1), labels = c("No", "Yes")),
  smoking     = factor(smoking,     levels = c(0, 1), labels = c("No", "Yes")),
  sex         = factor(sex,         levels = c(0, 1), labels = c("Female", "Male")),
  residence   = factor(residence,   levels = c(0, 1), labels = c("Rural", "Urban")),
  married     = factor(married,     levels = c(0, 1), labels = c("No", "Yes")),
mining      = factor(mining,      levels = c(0, 1), labels = c("No", "Yes")),
  age         = age
)

Step 5 — Inspect the simulated data

str(lung)
## 'data.frame':    3000 obs. of  7 variables:
##  $ lung_cancer: Factor w/ 2 levels "No","Yes": 2 1 1 1 1 1 1 1 1 1 ...
##  $ smoking    : Factor w/ 2 levels "No","Yes": 2 1 1 1 1 2 1 2 2 1 ...
##  $ sex        : Factor w/ 2 levels "Female","Male": 2 2 1 1 2 1 1 2 2 1 ...
##  $ residence  : Factor w/ 2 levels "Rural","Urban": 1 2 1 2 1 1 2 1 1 1 ...
##  $ married    : Factor w/ 2 levels "No","Yes": 2 2 1 2 1 2 2 1 1 1 ...
##  $ mining     : Factor w/ 2 levels "No","Yes": 1 1 2 1 1 1 1 1 1 1 ...
##  $ age        : num  52 54 44 40 52 23 53 42 42 34 ...
head(lung, n = 5)
summary(lung)
##  lung_cancer smoking        sex       residence    married    mining    
##  No :2670    No :1669   Female:1490   Rural:1820   No :1171   No :2550  
##  Yes: 330    Yes:1331   Male  :1510   Urban:1180   Yes:1829   Yes: 450  
##                                                                         
##                                                                         
##                                                                         
##                                                                         
##       age       
##  Min.   :18.00  
##  1st Qu.:37.00  
##  Median :45.00  
##  Mean   :45.02  
##  3rd Qu.:53.00  
##  Max.   :85.00
table(lung$lung_cancer, lung$smoking, dnn = c("Lung cancer", "Smoking"))
##            Smoking
## Lung cancer   No  Yes
##         No  1570 1100
##         Yes   99  231
table(lung$lung_cancer, lung$mining,  dnn = c("Lung cancer", "Mining"))
##            Mining
## Lung cancer   No  Yes
##         No  2308  362
##         Yes  242   88

Step 6 — Fit the logistic regression model

glm() with family = binomial(link = "logit") fits the model. Because lung_cancer is a factor with levels No then Yes, R models the probability of the second level, Yes, which is what is wanted.

model <- glm(lung_cancer ~ smoking + sex + residence + married + mining + age,
             data = lung, family = binomial(link = "logit"))
summary(model)
## 
## Call:
## glm(formula = lung_cancer ~ smoking + sex + residence + married + 
##     mining + age, family = binomial(link = "logit"), data = lung)
## 
## Coefficients:
##                 Estimate Std. Error z value Pr(>|z|)    
## (Intercept)    -5.908473   0.328692 -17.976  < 2e-16 ***
## smokingYes      1.238108   0.130794   9.466  < 2e-16 ***
## sexMale         0.423950   0.125319   3.383 0.000717 ***
## residenceUrban  0.221638   0.124875   1.775 0.075918 .  
## marriedYes     -0.274972   0.124859  -2.202 0.027648 *  
## miningYes       0.898748   0.147135   6.108 1.01e-09 ***
## age             0.058129   0.005588  10.402  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 2079.1  on 2999  degrees of freedom
## Residual deviance: 1806.9  on 2993  degrees of freedom
## AIC: 1820.9
## 
## Number of Fisher Scoring iterations: 5

The Estimate column contains log-odds, not odds ratios. The next step converts them.

Step 7 — Report the parameters as odds ratios

Exponentiating the coefficients and their confidence limits gives the odds ratios.

  • confint.default() returns Wald intervals;

  • confint() returns profile-likelihood intervals, which are preferable in small samples but slower.

# Quick version: odds ratios with 95% Wald confidence intervals
exp(cbind(OR = coef(model), confint.default(model)))
##                         OR       2.5 %      97.5 %
## (Intercept)    0.002716333 0.001426264 0.005173278
## smokingYes     3.449080933 2.669141768 4.456922979
## sexMale        1.527984959 1.195218266 1.953398890
## residenceUrban 1.248120090 0.977152480 1.594227913
## marriedYes     0.759593703 0.594704071 0.970201183
## miningYes      2.456526698 1.841109998 3.277655016
## age            1.059851851 1.048307386 1.071523449
# Tidy table with p-values
OR   <- exp(coef(model))
CI   <- exp(confint.default(model))
pval <- summary(model)$coefficients[, 4]
 
or_table <- data.frame(
  Term     = names(OR),
  OR       = round(OR, 3),
  Lower_95 = round(CI[, 1], 3),
  Upper_95 = round(CI[, 2], 3),
  p_value  = round(pval, 4),
  row.names = NULL
)
print(or_table)
##             Term    OR Lower_95 Upper_95 p_value
## 1    (Intercept) 0.003    0.001    0.005  0.0000
## 2     smokingYes 3.449    2.669    4.457  0.0000
## 3        sexMale 1.528    1.195    1.953  0.0007
## 4 residenceUrban 1.248    0.977    1.594  0.0759
## 5     marriedYes 0.760    0.595    0.970  0.0276
## 6      miningYes 2.457    1.841    3.278  0.0000
## 7            age 1.060    1.048    1.072  0.0000

Interpretation of the fitted model

  • Smokers have about 3.4 times the odds of lung cancer compared with non-smokers, after adjusting for the other variables (OR = 3.45, 95% CI 2.67–4.46).

  • Working in mining is associated with about 2.5 times the odds of lung cancer relative to not working in mining (OR = 2.46, 95% CI 1.84–3.28).

  • Males have roughly 53% higher odds than females (OR = 1.53, 95% CI 1.20–1.95).

  • Each additional year of age raises the odds by about 6% (OR = 1.06, 95% CI 1.05–1.07). Multiplying over 10 years gives 1.06^10 ≈ 1.78, i.e. 78% higher odds per decade.

  • Being married is associated with lower odds (OR = 0.76, 95% CI 0.60–0.97); the confidence interval excludes 1, so the association is statistically significant at the 5% level.

  • Urban residence shows a raised but non-significant odds ratio (OR = 1.25, 95% CI 0.98–1.59, p = 0.076). The interval includes 1, so with this sample size the effect cannot be distinguished from no effect even though a true effect of 1.30 was built into the simulation — a useful reminder about power.

Step 8 — Overall model fit

We perform overall model fit to see whether the fitted model is significant (i.e. the regressors included in the model explains the response).

lr <- model$null.deviance - model$deviance      # likelihood-ratio statistic
pchisq(lr, df = 6, lower.tail = FALSE)          # p-value for the whole model
## [1] 7.5042e-56
AIC(model)
## [1] 1820.937

The likelihood-ratio chi-square is significant (p < 0.001), so the six predictors jointly explain a significant amount of variation in lung cancer status. AIC = 1820.94.

4. Practical notes and key take-aways