---
title: "Running an N-of-1 study end to end"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Running an N-of-1 study end to end}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

```{r setup}
library(nof1kit)
```

A single-case (N-of-1) experiment has a lifecycle: design a randomization
schedule, work out what it can detect, collect data against it, check what came
back, and analyze it. Packages for single-case analysis are plentiful. The
earlier stages are usually hand-rolled, which is where the errors live.

This vignette walks the whole lifecycle on a real study: 70 days, three prompts
a day, melatonin randomized nightly. The data shipped with the package are the
ones actually collected.

## 1. Design

`design_schedule()` produces a balanced sequence with a cap on how many identical
days may run consecutively. Long runs of the same condition confound the
intervention with time, so a run-length constraint is standard practice in N-of-1
designs. The seed is required, not optional: a preregistered schedule has to be
reproducible from what is written in the preregistration.

```{r}
sched <- design_schedule(n_days = 70, max_run = 2, seed = 20260218)
head(sched, 12)
```

The sampling is exact rather than by rejection. For a balanced 70-day design with
`max_run = 2`, fewer than one permutation in a million satisfies the constraint,
so drawing until one fits is not workable. Instead the number of valid completions
from each intermediate state is counted by dynamic programming, and the sequence
is drawn day by day in proportion to those counts. The result is a uniform draw
over all valid schedules: no admissible sequence is more likely than any other.
That is a stronger property than blocked randomization, which restricts the
sample space in ways that are rarely stated.

`check_schedule()` reports what a reviewer, or a preregistration reader, would
want to see.

```{r}
check_schedule(sched)
```

Balanced counts, no run longer than two, and a negative lag-1 autocorrelation,
which is what the run cap buys: the sequence alternates more than chance would.

## 2. How much can this design actually detect

Power formulas assume independent observations. Daily measurements from one
person are not: today's mood is correlated with yesterday's, so each new day
carries less information than a genuinely new observation would.

`sim_power()` simulates studies of the given length and analyses each one twice,
with and without an AR(1) error structure.

```{r}
sim_power(n_days = 70, effect = 0.5, phi = 0.4, n_sims = 200, seed = 2)
```

Ignoring the dependence costs power here, but it does not create false positives.
That is a property of the schedule, not a general fact. Compare the same
autocorrelation under a design that runs control for the first half and treatment
for the second:

```{r}
sim_power(n_days = 70, effect = 0, phi = 0.7, n_sims = 200, seed = 4,
          schedule = rep(c(0, 1), each = 35))
```

With no effect at all, ordinary least squares rejects the null in roughly four
tests out of ten, at a nominal five percent level. A slowly changing schedule is
nearly collinear with a slowly drifting error, so the drift is readily mistaken
for an effect. A rapidly alternating one is nearly orthogonal to it, and the
drift cancels across adjacent days instead.

This is what the `max_run` constraint is protecting. It is usually justified as
avoiding confounding with time, which is true but understates it: the constraint
is what keeps the inference valid when the residuals are dependent, which in
intensive longitudinal data they always are.

## 3. Hand the schedule to the collection tool

`write_schedule()` writes the day-to-condition mapping as JSON.

```{r}
f <- tempfile(fileext = ".json")
write_schedule(sched, f)
cat(substr(readLines(f), 1, 90))
```

That format is what the companion iOS app and Shortcut both read, so a schedule
designed here can be loaded onto a phone without an intermediate step. Generation
and execution stay separate on purpose: an instrument that can also rewrite the
randomization is an instrument that can silently unblind a study.

## 4. Read what came back, and check it

The study data are bundled with the package. Real files rarely use the column name
you expect, so `read_ema()` takes the name of the timestamp column.

```{r, error = TRUE}
path <- system.file("extdata", "melatonin_ema.csv", package = "nof1kit")
ema <- read_ema(path, start_date = "2026-02-18", timestamp_col = "datetime")
```

The file carries its own `study_day` column, and it does not match the calendar:
collection ran across 71 calendar days while the preregistered protocol covered
70, so one study day spans two dates. The analysis was preregistered over 70 days
and indexed by `study_day`, which is the correct scope, but a file whose index and
calendar disagree is worth surfacing rather than silently accepting.

Passing `start_date` makes the two indices meet, so the discrepancy is raised at
read time. Omitting it accepts the file's index, and `validate_ema()` reports it
instead:

```{r}
ema <- read_ema(path, timestamp_col = "datetime")

v <- validate_ema(
  ema,
  ranges = list(mood = c(0, 100), agency = c(0, 100),
                metacognition = c(0, 100), melatonin = c(0, 1)),
  n_days = 70
)
v
subset(v$issues, check == "inconsistent_study_day")
```

The point of the check is that this is invisible to everything else a pipeline
usually does: the values are all in range, no timestamps are duplicated, nothing
is missing, and every `study_day` sits inside `1:70`. An index that has drifted
from the calendar is only visible if something compares the two.

Whether the drift matters depends on the design. Here it does not, because the
analysis was preregistered over 70 study days and indexed by `study_day`
throughout. In a study indexed by date, or one where the two were assumed
interchangeable, the same drift would silently misalign a third of the records.

The result is data, not a printed report, so a pipeline that must not proceed on
bad input can act on it:

```{r, error = TRUE}
stopifnot(v$n_issues == 0)
```

## 5. Compliance, and why the definition matters

Compliance in EMA studies is usually reported as records divided by prompts. On
these data that gives:

```{r}
records <- nrow(ema)
prompts <- 70 * 3
round(100 * records / prompts, 1)
```

`compliance()` computes something different: the proportion of scheduled prompts
that received a response *inside their response window*.

```{r}
cp <- compliance(
  ema,
  start_date = "2026-02-18",
  n_days = 70,
  times = c("10:00", "16:00", "22:00"),
  window = 3
)
cp
```

The two numbers differ by about three points on the same data, for two reasons.
A prompt answered three times counts once here, not three times. And a record
arriving outside every window is data, but it is not evidence that a prompt was
answered when it was asked.

Neither number is wrong. They answer different questions, and a paper reporting
"compliance" without saying which one leaves the reader unable to tell. The
prompt-level detail is available when the headline number needs unpacking:

```{r}
tapply(cp$prompts$answered, cp$prompts$time, mean)
```

Response rates were not equal across the day. Whether the pattern is large enough
to matter is a judgement about the study, but it is the kind of thing worth seeing
before treating missingness as random.

## 6. Analysis

From here the data are ordinary. `nof1kit` deliberately stops at the boundary of
analysis: intensive longitudinal data are well served by existing tools, and the
gap this package fills is upstream of them.

```{r, eval = FALSE}
library(lme4)
lmer(mood ~ melatonin + (1 | study_day), data = ema)
```

## Summary

```{r, eval = FALSE}
sched <- design_schedule(n_days = 70, max_run = 2, seed = 20260218)
write_schedule(sched, "schedule.json")     # onto the phone

ema <- read_ema("export.csv", start_date = "2026-02-18")
validate_ema(ema, ranges = list(mood = c(0, 100)), n_days = 70)
compliance(ema, start_date = "2026-02-18", n_days = 70,
           times = c("09:00", "15:00", "21:00"))
```

Four functions cover design, handoff, quality control, and compliance. What
happens after that is what the rest of the R ecosystem is for.
