Files
Experiments DB Dev 5314071c6b analysis(tdcs): GLM hypothesis tests + methods for the reaching study
Poisson GEE / binomial rate / learning-rate models, subject random-intercept
sensitivity, unknown-group classification and merge scenarios, plus METHODS.md
and learning-curve plots. Data reconstructed from the experiment DB and
verified against the exported matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:07:17 -04:00

159 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tDCS reaching study — GLM methods
This document explains the statistics in `tdcs_glm.py`: the models, the exact
formulas, how each subject's progress is accounted for, and the caveats.
## The question
Two **known** conditions anchor the performance scale and differ from each other:
- **H2 (anchor check):** `Electrode-Box-B2` performs **better** than `Electrode-Box-A2`.
Two **unknown** conditions are then **classified** against those anchors — for
each of `Electrode-Box-A` and `Right-Electrode`, is it **A2-like** or **B2-like**?
We do *not* assume either belongs to B2; each unknown is compared to *both*
anchors, and the anchor it cannot be distinguished from is its likely class.
There is also a **merged-assumption** mode (`--merge`): assume the two unknowns
resolve as `Right-Electrode == Box-B2` and `Electrode-Box-A == Box-A2`, fold them
into the anchors, and re-estimate everything with 4 subjects per group.
## Data and outcome
- **Outcome:** `success` = successful reaches in a session
(`analysis_summary.counts.Success`), a non-negative **count**.
- **Attempts:** `total` = reach attempts in the session (`analysis_summary.total`);
used as the denominator for the rate model. `success_rate == success / total`.
- **Time:** `day` = the "# Days Reach" field (training day, 0..26). Analyses use
days ≥ 0 (pre-training negative days and zero-attempt sessions are excluded;
zero-attempt sessions are undefined for the rate model).
- **Groups (subjects):** Naive (4), Box-A2 (3), Box-B2 (3), Box-A (1),
Right-Electrode (1). Under `--merge`: Box-A2 (4), Box-B2 (4), Naive (4).
**Provenance:** `tdcs_reach_data.csv` was reconstructed from the experiment
database and verified cell-by-cell against the exported matrix (71/71
unambiguous cells on days 05 matched exactly).
## The three models
All models use `Electrode-Box-B2` as the **reference** group, so each group term
is that group's contrast *versus Box-B2*. `day_c` is the centered training day and
`day_c2 = day_c²` captures the rise-then-plateau of the learning curve.
### (A) Level — count (primary)
Poisson GEE on the success counts, clustered by subject:
```
success ~ C(group, Treatment('Electrode-Box-B2')) + day_c + day_c2
family = Poisson (log link)
groups = subject # repeated-measures cluster
cov_struct = Exchangeable # working within-subject correlation
SE = robust (sandwich)
```
`exp(coef)` for a group term is an **incidence-rate ratio (IRR)**: expected
successes relative to Box-B2.
### (B) Level — rate
Binomial GLM on successes-out-of-attempts, with cluster-robust SEs by subject:
```
cbind(success, total - success) ~ C(group, Treatment('Electrode-Box-B2')) + day_c + day_c2
family = Binomial (logit link)
cov_type = cluster (groups = subject)
```
`exp(coef)` is an **odds ratio** for a successful reach relative to Box-B2. This
controls for differing numbers of attempts, so it answers "who is more accurate
per attempt?" rather than "who attempts more?"
### (C) Learning rate
Poisson GEE with a **group × day** interaction, to ask whether groups improve at
different *rates* (not just different levels):
```
success ~ C(group, Treatment('Electrode-Box-B2')) * day_c + day_c2
```
Each `group[T.X]:day_c` term is the difference in log-slope versus Box-B2; a joint
Wald test asks whether *any* group's slope differs. Large p ⇒ parallel learning.
### Anchors-only model
The A2-vs-B2 comparison, refit on **just the two anchor groups** so nothing else
influences the shared day terms or the dispersion/correlation nuisance:
```
# subset to {Box-A2, Box-B2}
success ~ C(group, Treatment('Electrode-Box-B2')) + day_c + day_c2 # count
cbind(success, total-success) ~ C(group, ...) + day_c + day_c2 # rate
```
With B2 as reference the single group term is the A2-vs-B2 effect; H2 is a
one-sided test that this coefficient is below zero.
## How each subject's progress is accounted for
Two distinct pieces:
1. **Progress over training** — the `day_c + day_c2` fixed terms model the average
learning curve, so groups are compared at comparable points in training rather
than being confounded by *when* each was measured.
2. **Repeated measures / individual baselines** — each subject contributes many
correlated sessions and has its own baseline. Handled two ways:
- **Primary (GEE):** subject is the cluster; an exchangeable working
correlation plus robust (sandwich) SEs give *population-average* group
effects whose inference is valid under within-subject correlation and
Poisson overdispersion.
- **Sensitivity (mixed model):** a Poisson model with a **per-subject random
intercept** (`(1 | subject)`) so each animal gets its own baseline level;
the group effects are estimated after allowing for that individual variation.
statsmodels has no frequentist Poisson GLMM, so this is the **MAP/Laplace**
fit (`fit_vb` diverges on these large counts). Its p-values are approximate —
read it as a direction/magnitude check that should agree with GEE.
## Classification logic (unknowns)
For each unknown group, `diff_contrast` builds a linear contrast of that group
against **each anchor** (both coded vs the B2 reference) and tests it:
- indistinguishable from B2 **and** different from A2 → **B2-like**
- indistinguishable from A2 **and** different from B2 → **A2-like**
- indistinguishable from both → **ambiguous** (report the numerically nearer one)
- different from both → **unlike both**
## Caveats
- **Tiny groups.** Box-A2/B2 have 3 subjects, Naive 4, and each unknown has
**n = 1 subject**. Classifying a one-subject condition is weak: "matches
anchor X" means "not statistically distinguishable from X," **not** proof of
equivalence.
- **Single-cluster fragility.** Cluster-robust/GEE inference with one cluster in a
group can produce artificially small SEs — most visibly the learning-rate
interaction for the n=1 groups; do not read those p-values literally.
- **Mixed-model confounding.** With a per-subject random intercept, a group made
of one subject is partly confounded with that subject's random intercept, so its
fixed effect is shrunk.
- **Rate vs count.** "Success" alone is a count; the rate model (success/attempts)
is the fairer accuracy comparison when attempt counts differ.
## Files and usage
- `tdcs_glm.py` — the analysis (run it directly).
- `tdcs_reach_data.csv` — the verified long-format data (`subject, group, day, success, total`).
- `tdcs_learning_curves*.png` — per-group learning curves (count and rate panels).
```
python3 analysis/tdcs_glm.py # all training days, 5 groups
python3 analysis/tdcs_glm.py --max-day 10 # restrict to days 010
python3 analysis/tdcs_glm.py --merge # assume Right==B2 and Box-A==A2 (3 groups)
```
Requires: pandas, numpy, scipy, statsmodels, matplotlib.