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

6.8 KiB
Raw Permalink Blame History

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.