knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(pcdreg)
Data preparation is most of the work in fitting these models, and most mistakes occur here. This vignette describes the inputs required by the two fitters, formula specification, conversion from common layouts, and the error messages.
A subject is examined at times $T_{i1} < \dots < T_{iJ_i}$. Each examination gives the number of events since the previous examination, but not their times. Two quantities are easy to confuse:
pcdreg always wants the increment. If your data records cumulative counts,
difference them first; there is a recipe below. Passing cumulative counts is not
an error the package can detect, and it will quietly give you the wrong answer.
pcd() builds the left-hand side of the formula, as Surv() does for survival
models. Choose its form according to one question: do any covariates change
value during follow-up?
visits <- data.frame( id = c(1, 1, 1, 2, 2), time = c(0.5, 1.1, 1.9, 0.7, 1.4), count = c(2, 0, 3, 1, 4), age = c(24, 24, 24, 31, 31), arm = c("drug", "drug", "drug", "placebo", "placebo") ) pcd(visits$id, visits$time, visits$count)
Interval starts come from each subject's previous examination, with follow-up beginning at zero. The printed intervals show this. Subject 1 contributes $(0, 0.5]$, $(0.5, 1.1]$ and $(1.1, 1.9]$ with 2, 0 and 3 events. Fit the model with
pcdreg(pcd(id, time, count) ~ age + arm, data = visits)
but not with these five rows and two subjects: age and arm are perfectly
confounded, so the information matrix is singular. Later examples use real
fits.
When a covariate changes between examinations, use one row for each interval
with constant covariates. This is the layout coxph() uses for time-dependent
covariates.
cp <- data.frame( id = c(1, 1, 1, 2), tstart = c(0.0, 0.5, 0.9, 0.0), tstop = c(0.5, 0.9, 1.2, 0.8), count = c(2, NA, 0, 3), dose = c(10, 10, 25, 25) ) pcd(cp$id, cp$tstart, cp$tstop, cp$count)
Subject 1 was examined at 0.5 with 2 events and at 1.2 with none. The dose changed from 10 to 25 at time 0.9, within the second examination interval.
The count field distinguishes the two row types:
NA means "this row ends at a covariate change, not an examination".There is no separate event indicator.
Use the three argument form when every covariate is fixed at baseline. Use the four argument form as soon as a covariate changes. When in doubt, use the four argument form. It can express anything the three argument form can.
The right-hand side is an ordinary model formula.
pcd(id, tstart, tstop, count) ~ x1 + x2 # main effects pcd(id, tstart, tstop, count) ~ x1 * x2 # interaction pcd(id, tstart, tstop, count) ~ x1 + log(x2) # transformation pcd(id, tstart, tstop, count) ~ x1 + factor(site) pcd(id, tstart, tstop, count) ~ . # everything else
Factors are expanded with your contrast settings, and predict() reuses the
coding the fit used.
set.seed(1) d <- sim_pcd(60) d$site <- factor(c("north", "south", "east")[1 + d$id %% 3]) fit <- pcdreg(pcd(id, tstart, tstop, count) ~ x1 + site, data = d) names(coef(fit))
The dot expands to every column outside the response, so id, tstart,
tstop and count are excluded automatically:
names(coef(pcdreg(pcd(id, tstart, tstop, count) ~ ., data = d)))
A constant column cannot be told apart from a rescaling of the baseline
function, so both fitters drop the intercept and report the baseline separately
through baseline(). Writing ~ x1 + x2 - 1 changes nothing, and adding + 0
is unnecessary.
Adding a constant $c$ to a covariate leaves $\beta$ unchanged and rescales the baseline by $e^{-c\beta}$. Centring covariates changes the baseline, not the coefficients.
Data are sorted internally by subject and time, so arrange() is unnecessary.
Subject identifiers need not be integers or consecutive. Numbers, characters,
and factors all work; predict(type = "mean") returns the original labels.
This is the most common and damaging mismatch. If count accumulates,
difference it within subject before fitting.
# A frame whose counts accumulate: 2, 2, 5 rather than 2, 0, 3. cumdat <- data.frame( id = c(1, 1, 1, 2, 2), time = c(0.5, 1.1, 1.9, 0.7, 1.4), count = c(2, 2, 5, 1, 5), age = c(24, 24, 24, 31, 31) ) cumdat$increment <- ave(cumdat$count, cumdat$id, FUN = function(v) v - c(0, v[-length(v)])) cumdat[, c("id", "time", "count", "increment")]
Then fit with increment. With NAs on covariate-change rows, difference only
the examination rows:
at <- !is.na(dat$count) dat$count[at] <- ave(dat$count[at], dat$id[at], FUN = function(v) v - c(0, v[-length(v)]))
panelEM layoutThis layout repeats each panel count on every row of its interval and marks examinations with a separate indicator. A repeated count carries no information beyond the examination row, so conversion takes one line:
new <- old new$count[old$event == 0] <- NA pcdreg(pcd(id, tstart, tstop, count) ~ x1 + x2, data = new)
If visits and covariate changes are in separate tables, stack them and carry
covariates forward. Examination rows retain their counts; covariate-change rows
receive NA.
exams <- data.frame(id = c(1, 1), time = c(0.5, 1.2), count = c(2, 0)) covar <- data.frame(id = 1, time = 0.9, dose = 25) start <- data.frame(id = 1, time = 0, dose = 10) both <- merge( data.frame(id = c(exams$id, covar$id), tstop = c(exams$time, covar$time), count = c(exams$count, NA)), rbind(start[, c("id", "time", "dose")], covar[, c("id", "time", "dose")]), by.x = c("id", "tstop"), by.y = c("id", "time"), all.x = TRUE ) both <- both[order(both$id, both$tstop), ] both$dose <- ave(both$dose, both$id, FUN = function(v) { for (i in seq_along(v)) if (is.na(v[i]) && i > 1) v[i] <- v[i - 1]; v }) both$tstart <- ave(both$tstop, both$id, FUN = function(v) c(0, v[-length(v)])) both[, c("id", "tstart", "tstop", "count", "dose")]
Reshape wide data to long first with stats::reshape() or
tidyr::pivot_longer(). Use one row per subject and visit, then the three
argument form.
Both fitters perform these checks before arithmetic and identify the offending subject when a check fails.
Follow-up starts at zero. The models integrate from 0, so the first interval of every subject must begin there.
bad <- data.frame(id = 1, tstart = 0.2, tstop = 1, count = 3, x = 1) pcdreg(pcd(id, tstart, tstop, count) ~ x, data = bad)
For calendar dates, or follow-up starting at enrolment, subtract each subject's own origin first.
Intervals are contiguous. Within a subject each interval must begin exactly where the previous one ended: no gaps, no overlaps.
bad <- data.frame(id = c(1, 1), tstart = c(0, 0.7), tstop = c(0.5, 1), count = c(2, 1), x = c(1, 1)) pcdreg(pcd(id, tstart, tstop, count) ~ x, data = bad)
A gap usually indicates a covariate history that does not cover all follow-up. Carry the last value forward instead of omitting the interval.
Every subject has at least one examination. A subject with count equal to
NA on every row contributes nothing and is more likely a data error than an
intentional record.
Counts are non-negative whole numbers. pcd() refuses other values. This
catches rates and averages passed by mistake.
pcd(1:2, c(1, 2), c(0.5, 1.2))
One row per subject may end at any given time. Duplicate rows at the same subject and time are rejected instead of silently combined.
Follow-up ends at a subject's last examination. The likelihood runs over $j = 1, \dots, J_i$ and has no information beyond $T_{iJ_i}$. Rows wholly after that time are dropped, with a message reporting how many.
# A censoring row appended after subject 1's final visit. last1 <- max(d$tstop[d$id == 1]) trailing <- transform(d[d$id == 1, ][1, ], tstart = last1, tstop = last1 + 0.5, count = NA) fit2 <- pcdreg(pcd(id, tstart, tstop, count) ~ x1 + x2, data = rbind(d, trailing))
The usual cause is a censoring row appended after the last visit. This is harmless: the interval contains no information.
Missing covariates are handled by na.action, which by default deletes rows —
and deleting a row in the middle of a subject breaks contiguity, so you will get
the gap error rather than a silently different fit. Resolve missing covariates
before fitting, by carrying values forward or by dropping whole subjects.
This processing explains the error messages.
The pooled grid $t_1 < \dots < t_K$ contains all distinct examination times. For the rate model, each subject's covariate trajectory is evaluated at each grid time during follow-up. The complete, contiguous trajectory is therefore required. The means model uses only each subject's examination times and is much cheaper.
fit <- pcdreg(pcd(id, tstart, tstop, count) ~ x1 + x2, data = d) c(subjects = fit$n, examinations = fit$nexam, grid_times = fit$ngrid, events = fit$nevent)
Ties across subjects reduce the grid, which drives the rate model's cost. Examination times recorded to the day or week tie naturally. Times kept at full numerical precision do not, so the grid grows with sample size. Round to the precision at which visits were scheduled.
These quantities are a quick sanity check. nexam should equal the expected
number of examinations, and nevent the total number of events. An unexpectedly
large nevent usually indicates cumulative counts.
c(rows_supplied = nrow(d), examination_rows = sum(!is.na(d$count)), events = sum(d$count, na.rm = TRUE))
vignette("pcdreg") for the models themselves, the choice of covariance
estimator, and the comparison between the rate and means models.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.