feat(matlab): add Satterthwaite DF + honest random-slope test to LME reports

Every fitlme-based report (lme_*, paper_*, phase_*, and the variations'
analyze.m) now shows, per effect: residual-DF p, Satterthwaite-DF p, and --
for the interaction -- an HONEST test from a per-animal random-SLOPE model
(day|rat), whose Satterthwaite DF collapses toward the animal count.

New: tdcs_random_slope_interaction.m (shared helper). Wired into tdcs_lme,
tdcs_paper_lme, tdcs_phase_lme, variation_analyze; SUMMARY.csv gains
interaction_p_satt / interaction_p_rs. Regenerated all results/, variations/,
matched-effort outputs.

Key point this surfaces: Satterthwaite ~= residual on the random-INTERCEPT
model (the slope's error is at session level), so it does NOT fix
pseudoreplication; the random-slope model does. Effect: full-range mergeA2
interaction 0.009 -> 0.75 (collapses); unmerge_d0_5 0.015 -> 0.13 (n.s.);
the pooled-control early windows survive honestly (naive_a2_d0_5 0.001 ->
0.028; naive_boxa_d0_5 0.003 -> 0.036). Suite 42/42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-07-22 15:45:19 -04:00
parent 5fcd97f81a
commit 8a18c894dd
77 changed files with 2066 additions and 345 deletions
+75
View File
@@ -0,0 +1,75 @@
# Experiments Database — Backup
## Location
```
/home/sam/synology/Backups/Experiments-DB-Backup/
├── daily/ ← up to 5 change-triggered daily backups
├── weekly/ ← up to 2 unconditional weekly backups
├── .last_backup_state ← DB fingerprint from last backup run
└── backup.log ← all run output
```
## Scripts
```
scripts/
├── backup.sh ← main backup script
└── setup_cron.sh ← installs / updates cron entries (idempotent)
```
## Schedule
| Cron | Mode | Behaviour |
|------|------|-----------|
| `0 2 * * *` (daily 02:00) | `daily` | Compares DB fingerprint; skips if nothing changed |
| `0 3 * * 0` (Sunday 03:00) | `weekly` | Always dumps; ignores fingerprint |
To re-install cron entries after a path change:
```bash
./scripts/setup_cron.sh
```
## Backup Format
Files are `pg_dump --format=custom --compress=9` (`.pgdump`).
**Restore a backup:**
```bash
# Restore into a running container
docker exec -i experiments_postgres pg_restore \
-U expuser -d experiments_db --clean \
< /home/sam/synology/Backups/Experiments-DB-Backup/daily/<file>.pgdump
# Or restore to a fresh database
docker exec -i experiments_postgres createdb -U expuser experiments_db_restore
docker exec -i experiments_postgres pg_restore \
-U expuser -d experiments_db_restore \
< /path/to/file.pgdump
```
## Change Detection
The daily run fingerprints the database as:
```
<experiments count>,<animals count>,<daily_statuses count>,
<daily_analyses count>,<session_files count>,<latest audit_log timestamp>
```
Any insert, update, or delete on any table shifts this string and triggers a backup.
## Retention Policy
| Type | Keep |
|------|------|
| Daily | 5 most recent |
| Weekly | 2 most recent |
Older files are automatically deleted after each successful dump.
## Manual Run
```bash
# Force a backup right now (ignores change detection)
./scripts/backup.sh weekly
# Run change-detect check manually
./scripts/backup.sh daily
```
## Verify Cron Is Installed
```bash
crontab -l | grep ExpDB
```
+293
View File
@@ -0,0 +1,293 @@
# Experiments Database — n8n Workflows
n8n is running at **http://localhost:5678**
The Postgres credential **"Experiments DB (PostgreSQL)"** (id: `9iJuyA9iR5KUzmj5`) connects to `experiments_postgres:5432 / experiments_db` via the shared `llm-net` Docker network.
---
## Workflow Index
| ID | Workflow | Entity |
|----|----------|--------|
| `jjmtecjuHJPKZ4T7` | ExpDB · Experiments | `experiments` table |
| `HYnlIscuKwMW7blG` | ExpDB · Animals | `animals` table |
| `9emayzMpnhIZQxqd` | ExpDB · Daily Statuses | `daily_statuses` table |
| `hso5z2W1uO44bTP6` | ExpDB · Daily Analyses | `daily_analyses` table |
| `2s02OX1yibfqp6jK` | ExpDB · Session Files | `session_files` table |
| `T76NAYiFcCxHDKQB` | ExpDB · Audit Logs | `audit_logs` table |
| `lq3R3YqeHgxixrbZ` | ExpDB · Reports | cross-table queries |
---
## How to Call a Workflow
All workflows use an **Execute Workflow Trigger** — call them with an **Execute Workflow** node from any parent workflow. Pass a JSON object with `action` + the required fields.
```
Execute Workflow node
→ Workflow: <select workflow by name>
→ Input: { "action": "...", ...fields }
```
---
## ExpDB · Experiments
### `list`
Returns all experiments ordered by creation date.
```json
{ "action": "list" }
```
### `get`
```json
{ "action": "get", "id": "<experiment_uuid>" }
```
### `create`
```json
{ "action": "create", "title": "My Experiment" }
```
Creates with empty `template` and `subject_info_template` (`[]`). UUID auto-generated.
### `update`
```json
{ "action": "update", "id": "<experiment_uuid>", "title": "New Title" }
```
### `delete`
```json
{ "action": "delete", "id": "<experiment_uuid>" }
```
Returns `{ id }` of deleted row.
---
## ExpDB · Animals
### `list`
Returns all animals in an experiment, with `daily_status_count`.
```json
{ "action": "list", "experiment_id": "<experiment_uuid>" }
```
### `get`
Returns animal + `experiment_title`.
```json
{ "action": "get", "id": "<animal_uuid>" }
```
### `create`
```json
{
"action": "create",
"experiment_id": "<experiment_uuid>",
"animal_id_string": "M001",
"animal_name": "Mouse 1",
"subject_info": { "<fieldId>": "value" }
}
```
### `update`
```json
{
"action": "update",
"id": "<animal_uuid>",
"animal_id_string": "M001",
"animal_name": "Mouse 1",
"subject_info": { "<fieldId>": "value" }
}
```
### `delete`
```json
{ "action": "delete", "id": "<animal_uuid>" }
```
Cascades to `daily_statuses`.
---
## ExpDB · Daily Statuses
### `list`
Returns statuses for an animal ordered by date descending.
```json
{ "action": "list", "animal_id": "<animal_uuid>" }
```
### `get`
Returns status + animal + experiment context.
```json
{ "action": "get", "id": "<status_uuid>" }
```
### `create`
```json
{
"action": "create",
"animal_id": "<animal_uuid>",
"date": "2025-04-25",
"experiment_description": "...",
"vitals": "...",
"treatment": "...",
"notes": "...",
"custom_fields": { "<fieldId>": "value" }
}
```
All fields except `animal_id` and `date` are optional (default `null`).
### `update`
```json
{
"action": "update",
"id": "<status_uuid>",
"experiment_description": "...",
"vitals": "...",
"treatment": "...",
"notes": "...",
"custom_fields": { "<fieldId>": "value" }
}
```
### `delete`
```json
{ "action": "delete", "id": "<status_uuid>" }
```
Cascades to `daily_analyses` and `session_files`.
---
## ExpDB · Daily Analyses
### `list`
Returns analysis headers (no heavy JSON blobs) for a daily status.
```json
{ "action": "list", "daily_status_id": "<status_uuid>" }
```
### `get`
Returns full analysis including `sequence`, `category_counts`, `consecutive_stats`, `runs`.
```json
{ "action": "get", "id": "<analysis_uuid>" }
```
### `push_summary`
Writes an `analysis_summary` blob back to a daily status (same as the "Save metrics" button in the UI).
```json
{
"action": "push_summary",
"daily_status_id": "<status_uuid>",
"analysis_summary": {
"counts": { "Success": 12, "Failure": 3 },
"total": 15,
"success_rate": 0.8,
"computed_at": "2025-04-25T10:00:00.000Z"
}
}
```
---
## ExpDB · Session Files
### `list`
```json
{ "action": "list", "daily_status_id": "<status_uuid>" }
```
### `get`
```json
{ "action": "get", "id": "<file_uuid>" }
```
---
## ExpDB · Audit Logs
### `list_by_record`
All changes to a specific record (e.g. one animal, one daily status).
```json
{
"action": "list_by_record",
"table_name": "daily_statuses",
"record_id": "<status_uuid>",
"limit": 50
}
```
Valid `table_name` values: `experiments`, `animals`, `daily_statuses`.
### `list_by_table`
Recent changes across an entire table.
```json
{
"action": "list_by_table",
"table_name": "animals",
"limit": 100
}
```
---
## ExpDB · Reports
### `subjects_summary`
Per-subject stats for an experiment: total days logged, last entry date, days with saved metrics.
```json
{ "action": "subjects_summary", "experiment_id": "<experiment_uuid>" }
```
### `daily_summary`
All days × subjects where `analysis_summary` exists — useful for CSV export or charting outside the app.
```json
{ "action": "daily_summary", "experiment_id": "<experiment_uuid>" }
```
### `recent_activity`
Latest audit log entries across all tables.
```json
{ "action": "recent_activity", "limit": 50 }
```
---
## Database Schema Reference
```
experiments
id (uuid PK) title created_at template (json) subject_info_template (json)
animals
id (uuid PK) experiment_id (FK→experiments)
animal_id_string animal_name subject_info (json?)
daily_statuses
id (uuid PK) animal_id (FK→animals) date
experiment_description? vitals? treatment? notes?
custom_fields (json?) analysis_summary (json?)
daily_analyses
id (uuid PK) daily_status_id (FK→daily_statuses) created_at
file_name? timestamp_col note_col total_note_rows session_end_ts?
classifications (json) sequence (json) category_counts (json)
consecutive_stats (json) runs (json)
session_files
id (uuid PK) daily_status_id (FK→daily_statuses)
filename file_size file_type? last_modified?
matched_identifier? animal_id_string_snap? animal_name_snap?
notes? created_at
audit_logs
id (uuid PK) table_name record_id action changes (json) timestamp
```
`custom_fields` and `subject_info` are keyed by `fieldId` (UUID), matching the `template` / `subject_info_template` arrays on the experiment. Field keys can be renamed without data loss; only the UUID is used for storage.
---
## Infrastructure Notes
- `experiments_postgres` is connected to both `experiments-database_default` and `llm-net` networks, making it reachable from n8n as hostname `experiments_postgres`.
- The n8n API key `n8n_api_ca07bd874c628ed0f52d9a2e65073960f9f617c5` was created for the account `quocsam93@gmail.com` and is stored in the `user_api_keys` table of the n8n Postgres.
- Workflows were generated by `n8n_create_workflows.py` in this repo root.
+4 -3
View File
@@ -74,7 +74,8 @@ for gi = 1:size(groupings, 1)
try
r = localRun(fullfile(folder, 'analyze.m'));
rows(end + 1, :) = {vname, gname, wname, r.nRats, r.nObs, ...
r.covEqual, r.interP, r.interEst, r.stimP, r.dayP}; %#ok<AGROW>
r.covEqual, r.interP, r.interPsatt, r.interPrs, r.interEst, ...
r.stimP, r.dayP}; %#ok<AGROW>
fprintf(' %-16s N=%2d obs=%4d interaction p=%.4g (%+.2f) %s\n', ...
vname, r.nRats, r.nObs, r.interP, r.interEst, ...
localTern(r.covEqual, 'equal-cov', 'UNEQUAL-cov'));
@@ -87,8 +88,8 @@ end
% Top-level index of all variations.
S = cell2table(rows, 'VariableNames', {'variation', 'grouping', 'window', ...
'nRats', 'nObs', 'covEqual', 'interaction_p', 'interaction_est', ...
'stim_p', 'day_p'});
'nRats', 'nObs', 'covEqual', 'interaction_p', 'interaction_p_satt', ...
'interaction_p_rs', 'interaction_est', 'stim_p', 'day_p'});
writetable(S, fullfile(root, 'SUMMARY.csv'));
fprintf('\nWrote %d variations under %s\n(index: variations/SUMMARY.csv)\n', ...
size(rows, 1), root);
+11 -5
View File
@@ -54,11 +54,17 @@ Group: Error
{'Res Std'} 13.04 11.2 15.183
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(79)= 1.51 F(1)= 2.292 p=0.134
days (learning) t(79)= 12.43 F(1)= 154.593 p=2.759e-20
tDCS (main, at Day 1) t(79)= 0.80 F(1)= 0.635 p=0.428
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(79)= 1.51 F(1)= 2.292 p=0.134 p=0.1338 (df=83)
days (learning) t(79)= 12.43 F(1)=154.593 p=2.759e-20 p=1.192e-20 (df=83)
tDCS (main, at Day 1) t(79)= 0.80 F(1)= 0.635 p=0.428 p=0.4278 (df=83)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,20.3)=1.35, p=0.258
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.1340, slope diff=1.38) -> slopes are parallel (no differential change over training).
+11 -5
View File
@@ -54,11 +54,17 @@ Group: Error
{'Res Std'} 14.123 12.278 16.245
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(94)= 1.36 F(1)= 1.844 p=0.1777
days (learning) t(94)= 13.14 F(1)= 172.537 p=5.403e-23
tDCS (main, at Day 1) t(94)= 1.12 F(1)= 1.264 p=0.2637
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(94)= 1.36 F(1)= 1.844 p=0.1777 p=0.1776 (df=98)
days (learning) t(94)= 13.14 F(1)=172.537 p=5.403e-23 p=2.463e-23 (df=98)
tDCS (main, at Day 1) t(94)= 1.12 F(1)= 1.264 p=0.2637 p=0.2636 (df=98)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,16.2)=1.37, p=0.2593
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.1777, slope diff=1.00) -> slopes are parallel (no differential change over training).
+11 -5
View File
@@ -57,11 +57,17 @@ Group: Error
{'Res Std'} 18.954 16.597 21.644
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(105)= -2.66 F(1)= 7.094 p=0.008953
days (learning) t(105)= 9.82 F(1)= 96.388 p=1.573e-16
tDCS (main, at Day 1) t(105)= 3.43 F(1)= 11.778 p=0.0008585
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(105)= -2.66 F(1)= 7.094 p=0.008953 p=0.008906 (df=109)
days (learning) t(105)= 9.82 F(1)= 96.388 p=1.573e-16 p=1.113e-16 (df=109)
tDCS (main, at Day 1) t(105)= 3.43 F(1)= 11.778 p=0.0008585 p=0.000848 (df=109)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,9.7)=0.11, p=0.746
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: SIGNIFICANT (p=0.0090, slope diff=-2.18) -> the tDCS (Box-B2) group improves SLOWER -- groups CONVERGE (Box-B2 is ahead early, the gap narrows).
+11 -5
View File
@@ -54,11 +54,17 @@ Group: Error
{'Res Std'} 11.955 10.184 14.034
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(79)= 1.91 F(1)= 3.632 p=0.06032
days (learning) t(79)= 10.55 F(1)= 111.334 p=9.547e-17
tDCS (main, at Day 1) t(79)= 0.07 F(1)= 0.004 p=0.9478
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(79)= 1.91 F(1)= 3.632 p=0.06032 p=0.06034 (df=79)
days (learning) t(79)= 10.55 F(1)=111.334 p=9.547e-17 p=8.241e-17 (df=80)
tDCS (main, at Day 1) t(79)= 0.07 F(1)= 0.004 p=0.9478 p=0.9482 (df=21)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,30.6)=3.00, p=0.09319
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.0603, slope diff=1.74) -> slopes are parallel (no differential change over training).
+11 -5
View File
@@ -54,11 +54,17 @@ Group: Error
{'Res Std'} 13.148 11.362 15.215
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(94)= 1.95 F(1)= 3.812 p=0.05385
days (learning) t(94)= 9.91 F(1)= 98.208 p=2.855e-16
tDCS (main, at Day 1) t(94)= 0.29 F(1)= 0.082 p=0.7757
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(94)= 1.95 F(1)= 3.812 p=0.05385 p=0.05377 (df=97)
days (learning) t(94)= 9.91 F(1)= 98.208 p=2.855e-16 p=1.981e-16 (df=98)
tDCS (main, at Day 1) t(94)= 0.29 F(1)= 0.082 p=0.7757 p=0.7776 (df=24)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,29.7)=3.25, p=0.08138
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.0538, slope diff=1.54) -> slopes are parallel (no differential change over training).
+11 -5
View File
@@ -57,11 +57,17 @@ Group: Error
{'Res Std'} 19.354 16.948 22.101
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(105)= -1.43 F(1)= 2.043 p=0.1559
days (learning) t(105)= 6.89 F(1)= 47.494 p=4.234e-10
tDCS (main, at Day 1) t(105)= 2.45 F(1)= 6.021 p=0.01578
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(105)= -1.43 F(1)= 2.043 p=0.1559 p=0.1558 (df=109)
days (learning) t(105)= 6.89 F(1)= 47.494 p=4.234e-10 p=3.734e-10 (df=109)
tDCS (main, at Day 1) t(105)= 2.45 F(1)= 6.021 p=0.01578 p=0.01572 (df=109)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,11.8)=0.02, p=0.9027
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.1559, slope diff=-1.47) -> slopes are parallel (no differential change over training).
+11 -5
View File
@@ -54,11 +54,17 @@ Group: Error
{'Res Std'} 12.508 10.37 15.086
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(57)= 1.49 F(1)= 2.208 p=0.1428
days (learning) t(57)= 10.08 F(1)= 101.556 p=2.832e-14
tDCS (main, at Day 1) t(57)= 0.68 F(1)= 0.466 p=0.4974
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(57)= 1.49 F(1)= 2.208 p=0.1428 p=0.1428 (df=57)
days (learning) t(57)= 10.08 F(1)=101.556 p=2.832e-14 p=1.931e-14 (df=59)
tDCS (main, at Day 1) t(57)= 0.68 F(1)= 0.466 p=0.4974 p=0.5037 (df=17)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,13.0)=1.46, p=0.2479
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.1428, slope diff=1.56) -> slopes are parallel (no differential change over training).
+11 -5
View File
@@ -54,11 +54,17 @@ Group: Error
{'Res Std'} 14.676 12.436 17.32
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(66)= 1.65 F(1)= 2.721 p=0.1038
days (learning) t(66)= 9.09 F(1)= 82.593 p=3.035e-13
tDCS (main, at Day 1) t(66)= 0.95 F(1)= 0.910 p=0.3436
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(66)= 1.65 F(1)= 2.721 p=0.1038 p=0.1035 (df=70)
days (learning) t(66)= 9.09 F(1)= 82.593 p=3.035e-13 p=1.823e-13 (df=70)
tDCS (main, at Day 1) t(66)= 0.95 F(1)= 0.910 p=0.3436 p=0.3434 (df=70)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,12.8)=1.64, p=0.2227
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.1038, slope diff=1.55) -> slopes are parallel (no differential change over training).
+11 -5
View File
@@ -54,11 +54,17 @@ Group: Error
{'Res Std'} 14.163 11.93 16.813
effect t (df) F (df1) p
----------------------------------------------------------------------
days x tDCS (interaction) t(67)= 1.02 F(1)= 1.041 p=0.3112
days (learning) t(67)= 9.20 F(1)= 84.665 p=1.671e-13
tDCS (main, at Day 1) t(67)= 1.04 F(1)= 1.086 p=0.3012
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
------------------------------------------------------------------------------
days x tDCS (interaction) t(67)= 1.02 F(1)= 1.041 p=0.3112 p=0.3111 (df=70)
days (learning) t(67)= 9.20 F(1)= 84.665 p=1.671e-13 p=1.04e-13 (df=71)
tDCS (main, at Day 1) t(67)= 1.04 F(1)= 1.086 p=0.3012 p=0.3116 (df=18)
HONEST LME -- per-animal random slope (day|subject): interaction F(1,8.8)=0.61, p=0.456
Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual
(the slope's error is at the session level), so it does NOT fix pseudoreplication.
Letting each animal have its OWN slope collapses the interaction DF toward the animal
count -- this, and the per-animal slope test, are the honest learning-rate inference.
INTERPRETATION
- days x tDCS interaction: n.s. (p=0.3112, slope diff=0.94) -> slopes are parallel (no differential change over training).
+10 -5
View File
@@ -57,13 +57,18 @@ Group: Error
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(105)= -2.66 F(1)= 7.094 p=0.008953
day (learning) t(105)= 9.82 F(1)= 96.388 p=1.573e-16
stim (main, Day 1) t(105)= 3.43 F(1)= 11.778 p=0.0008585
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(105)= -2.66 F(1)= 7.094 p=0.008953 p=0.008906 (df=109)
day (learning) t(105)= 9.82 F(1)= 96.388 p=1.573e-16 p=1.113e-16 (df=109)
stim (main, Day 1) t(105)= 3.43 F(1)= 11.778 p=0.0008585 p=0.000848 (df=109)
interaction 95% CI: [-3.80, -0.56]
HONEST LME -- per-rat random slope (day|rat): interaction F(1,9.7)=0.11, p=0.746
Satterthwaite DF on the random-INTERCEPT model above stays ~= residual (the
slope's error is at session level), so it does NOT fix pseudoreplication. A per-animal
random slope collapses the interaction DF toward the animal count -- the honest test.
INTERPRETATION
- stim x day interaction: SIGNIFICANT (p=0.0090, slope diff=-2.18) -> tDCS (Box-B2) improves SLOWER -- groups converge.
- stim main effect on Day 1 (our day 0): SIGNIFICANT (p=0.0009) -> groups already DIFFER on Day 1.
+16 -6
View File
@@ -1,7 +1,8 @@
==============================================================================
PAPER LME REPLICATION -- mergeB2
PAPER LME REPLICATION -- mergeB2 (full range)
==============================================================================
model: behavior ~ stim + day + stim:day + (1|rat) [stim: Electrode-Box-B2=1 vs Electrode-Box-A2=0]
behavior = successful reaches (COUNT per session)
N = 8 rats, 109 sessions (day raw; day 0 = paper "Day 1")
day coverage: stim(B2) 0..22, control(A2) 0..13
** WARNING: unequal day coverage -- the full-range stim:day interaction
@@ -56,12 +57,21 @@ Group: Error
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(105)= -1.43 F(1)= 2.043 p=0.1559
day (learning) t(105)= 6.89 F(1)= 47.494 p=4.234e-10
stim (main, Day 1) t(105)= 2.45 F(1)= 6.021 p=0.01578
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(105)= -1.43 F(1)= 2.043 p=0.1559 p=0.1558 (df=109)
day (learning) t(105)= 6.89 F(1)= 47.494 p=4.234e-10 p=3.734e-10 (df=109)
stim (main, Day 1) t(105)= 2.45 F(1)= 6.021 p=0.01578 p=0.01572 (df=109)
interaction 95% CI: [-3.50, +0.57]
HONEST LME -- per-rat random slope (day|rat): interaction F(1,11.8)=0.02, p=0.9027
Satterthwaite DF on the random-INTERCEPT model above stays ~= residual (the
slope's error is at session level), so it does NOT fix pseudoreplication. A per-animal
random slope collapses the interaction DF toward the animal count -- the honest test.
INTERPRETATION
- stim x day interaction: n.s. (p=0.1559, slope diff=-1.47) -> slopes parallel -- no differential learning rate over this window.
- stim main effect on Day 1 (our day 0): SIGNIFICANT (p=0.0158) -> groups already DIFFER on Day 1.
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008; day t(227)=9.64,
F(1)=267.64, p=1.2e-18; stim t(227)=0.23, F(1)=0.053, p=0.81.
+16 -6
View File
@@ -1,7 +1,8 @@
==============================================================================
PAPER LME REPLICATION -- mergeNaive
PAPER LME REPLICATION -- mergeNaive (full range)
==============================================================================
model: behavior ~ stim + day + stim:day + (1|rat) [stim: Electrode-Box-B2=1 vs Electrode-Box-A2=0]
behavior = successful reaches (COUNT per session)
N = 11 rats, 170 sessions (day raw; day 0 = paper "Day 1")
day coverage: stim(B2) 0..22, control(A2) 0..26
** WARNING: unequal day coverage -- the full-range stim:day interaction
@@ -56,12 +57,21 @@ Group: Error
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(166)= 2.70 F(1)= 7.299 p=0.007614
day (learning) t(166)= 10.58 F(1)= 111.990 p=2.508e-20
stim (main, Day 1) t(166)= 1.27 F(1)= 1.611 p=0.2062
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(166)= 2.70 F(1)= 7.299 p=0.007614 p=0.007598 (df=170)
day (learning) t(166)= 10.58 F(1)=111.990 p=2.508e-20 p=2.011e-20 (df=170)
stim (main, Day 1) t(166)= 1.27 F(1)= 1.611 p=0.2062 p=0.217 (df=23)
interaction 95% CI: [+0.43, +2.74]
HONEST LME -- per-rat random slope (day|rat): interaction F(1,11.4)=0.92, p=0.3565
Satterthwaite DF on the random-INTERCEPT model above stays ~= residual (the
slope's error is at session level), so it does NOT fix pseudoreplication. A per-animal
random slope collapses the interaction DF toward the animal count -- the honest test.
INTERPRETATION
- stim x day interaction: SIGNIFICANT (p=0.0076, slope diff=+1.58) -> tDCS (Box-B2) improves FASTER -- benefit accumulates over training.
- stim main effect on Day 1 (our day 0): n.s. (p=0.2062) -> groups are comparable on Day 1.
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008; day t(227)=9.64,
F(1)=267.64, p=1.2e-18; stim t(227)=0.23, F(1)=0.053, p=0.81.
@@ -56,13 +56,18 @@ Group: Error
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(111)= 1.97 F(1)= 3.880 p=0.05134
day (learning) t(111)= 15.57 F(1)= 242.567 p=1.081e-29
stim (main, Day 1) t(111)= 1.32 F(1)= 1.742 p=0.1895
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(111)= 1.97 F(1)= 3.880 p=0.05134 p=0.05149 (df=105)
day (learning) t(111)= 15.57 F(1)=242.567 p=1.081e-29 p=3.188e-29 (df=107)
stim (main, Day 1) t(111)= 1.32 F(1)= 1.742 p=0.1895 p=0.2003 (df=22)
interaction 95% CI: [-0.01, +3.25]
HONEST LME -- per-rat random slope (day|rat): interaction F(1,10.1)=2.00, p=0.1875
Satterthwaite DF on the random-INTERCEPT model above stays ~= residual (the
slope's error is at session level), so it does NOT fix pseudoreplication. A per-animal
random slope collapses the interaction DF toward the animal count -- the honest test.
INTERPRETATION
- stim x day interaction: n.s. (p=0.0513, slope diff=+1.62) -> slopes parallel -- no differential learning rate over this window.
- stim main effect on Day 1 (our day 0): n.s. (p=0.1895) -> groups are comparable on Day 1.
@@ -56,13 +56,18 @@ Group: Error
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(134)= 2.66 F(1)= 7.068 p=0.008801
day (learning) t(134)= 15.01 F(1)= 225.182 p=1.766e-30
stim (main, Day 1) t(134)= 1.28 F(1)= 1.629 p=0.204
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(134)= 2.66 F(1)= 7.068 p=0.008801 p=0.008831 (df=130)
day (learning) t(134)= 15.01 F(1)=225.182 p=1.766e-30 p=2.418e-30 (df=132)
stim (main, Day 1) t(134)= 1.28 F(1)= 1.629 p=0.204 p=0.2142 (df=24)
interaction 95% CI: [+0.46, +3.14]
HONEST LME -- per-rat random slope (day|rat): interaction F(1,54.8)=6.46, p=0.01387
Satterthwaite DF on the random-INTERCEPT model above stays ~= residual (the
slope's error is at session level), so it does NOT fix pseudoreplication. A per-animal
random slope collapses the interaction DF toward the animal count -- the honest test.
INTERPRETATION
- stim x day interaction: SIGNIFICANT (p=0.0088, slope diff=+1.80) -> tDCS (Box-B2) improves FASTER -- benefit accumulates over training.
- stim main effect on Day 1 (our day 0): n.s. (p=0.2040) -> groups are comparable on Day 1.
+18 -6
View File
@@ -1,9 +1,12 @@
==============================================================================
PAPER LME REPLICATION -- unmerged
PAPER LME REPLICATION -- unmerged (full range)
==============================================================================
model: behavior ~ stim + day + stim:day + (1|rat) [stim: Electrode-Box-B2=1 vs Electrode-Box-A2=0]
behavior = successful reaches (COUNT per session)
N = 6 rats, 71 sessions (day raw; day 0 = paper "Day 1")
day coverage: stim(B2) 0..14, control(A2) 0..13
(Equal day coverage -- the stim:day interaction over this window is NOT
confounded by the full-range coverage imbalance.)
==============================================================================
FULL MODEL SUMMARY -- fitlme: behavior ~ stim + day + stim:day + (1|rat)
@@ -53,12 +56,21 @@ Group: Error
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(67)= 1.02 F(1)= 1.041 p=0.3112
day (learning) t(67)= 9.20 F(1)= 84.665 p=1.671e-13
stim (main, Day 1) t(67)= 1.04 F(1)= 1.086 p=0.3012
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(67)= 1.02 F(1)= 1.041 p=0.3112 p=0.3111 (df=70)
day (learning) t(67)= 9.20 F(1)= 84.665 p=1.671e-13 p=1.04e-13 (df=71)
stim (main, Day 1) t(67)= 1.04 F(1)= 1.086 p=0.3012 p=0.3116 (df=18)
interaction 95% CI: [-0.90, +2.77]
HONEST LME -- per-rat random slope (day|rat): interaction F(1,8.8)=0.61, p=0.456
Satterthwaite DF on the random-INTERCEPT model above stays ~= residual (the
slope's error is at session level), so it does NOT fix pseudoreplication. A per-animal
random slope collapses the interaction DF toward the animal count -- the honest test.
INTERPRETATION
- stim x day interaction: n.s. (p=0.3112, slope diff=+0.94) -> slopes parallel -- no differential learning rate over this window.
- stim main effect on Day 1 (our day 0): n.s. (p=0.3012) -> groups are comparable on Day 1.
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008; day t(227)=9.64,
F(1)=267.64, p=1.2e-18; stim t(227)=0.23, F(1)=0.053, p=0.81.
+10 -8
View File
@@ -3,15 +3,17 @@ PHASED days x tDCS LME -- mergeA2 (Box-B2 vs Box-A2)
==============================================================================
model per phase: success ~ dayp*tDCS + (1|subject) [dayp = day - phaseStart]
phase N A2slope B2slope day p tDCS(lvl) p interaction p slopeDiff [95% CI]
--------------------------------------------------------------------------------------------
0-5 8 10.54 15.16 1.5e-10 0.693 0.014 +4.62 [+1.00, +8.24]
6-10 7 3.17 4.83 0.035 0.127 0.390 +1.66 [-2.22, +5.53]
6-13 7 2.92 4.47 0.00032 0.125 0.118 +1.55 [-0.41, +3.52]
phase N A2slp B2slp day p tDCS p intP(res) intP(Satt) intP(RS) slopeDiff [95% CI]
--------------------------------------------------------------------------------------------------------
0-5 8 10.54 15.16 1.5e-10 0.693 0.014 0.014 0.089 +4.62 [+1.00,+8.24]
6-10 7 3.17 4.83 0.035 0.127 0.390 0.390 0.466 +1.66 [-2.22,+5.53]
6-13 7 2.92 4.47 0.00032 0.125 0.118 0.118 0.303 +1.55 [-0.41,+3.52]
Note: the interaction p (and CI) use fitlme observation-level DF and are
ANTICONSERVATIVE at these small subject counts (see tdcs_power_sim). The early
phase carries the Box-B2 faster-acquisition signal; late phases converge.
Columns: intP(res)=observation-level DF (anticonservative); intP(Satt)=Satterthwaite
DF (~= residual on this random-intercept model); intP(RS)=per-animal random-slope
(dayp|subject) model, the honest test (DF collapses toward the animal count; 'n/a' if it
did not converge). The early phase carries the Box-B2 faster-acquisition signal; late
phases converge.
==============================================================================
FULL MODEL SUMMARY -- phase 0-5: success ~ dayp*tDCS + (1|subject)
==============================================================================
+10 -8
View File
@@ -3,15 +3,17 @@ PHASED days x tDCS LME -- mergeB2 (Box-B2 vs Box-A2)
==============================================================================
model per phase: success ~ dayp*tDCS + (1|subject) [dayp = day - phaseStart]
phase N A2slope B2slope day p tDCS(lvl) p interaction p slopeDiff [95% CI]
--------------------------------------------------------------------------------------------
0-5 8 9.86 14.65 2.9e-08 0.399 0.013 +4.79 [+1.06, +8.53]
6-10 7 2.40 4.80 0.18 0.142 0.253 +2.40 [-1.80, +6.60]
6-13 7 1.71 4.42 0.083 0.127 0.018 +2.71 [+0.50, +4.93]
phase N A2slp B2slp day p tDCS p intP(res) intP(Satt) intP(RS) slopeDiff [95% CI]
--------------------------------------------------------------------------------------------------------
0-5 8 9.86 14.65 2.9e-08 0.399 0.013 0.013 0.088 +4.79 [+1.06,+8.53]
6-10 7 2.40 4.80 0.18 0.142 0.253 0.254 0.328 +2.40 [-1.80,+6.60]
6-13 7 1.71 4.42 0.083 0.127 0.018 0.018 0.024 +2.71 [+0.50,+4.93]
Note: the interaction p (and CI) use fitlme observation-level DF and are
ANTICONSERVATIVE at these small subject counts (see tdcs_power_sim). The early
phase carries the Box-B2 faster-acquisition signal; late phases converge.
Columns: intP(res)=observation-level DF (anticonservative); intP(Satt)=Satterthwaite
DF (~= residual on this random-intercept model); intP(RS)=per-animal random-slope
(dayp|subject) model, the honest test (DF collapses toward the animal count; 'n/a' if it
did not converge). The early phase carries the Box-B2 faster-acquisition signal; late
phases converge.
==============================================================================
FULL MODEL SUMMARY -- phase 0-5: success ~ dayp*tDCS + (1|subject)
==============================================================================
+10 -8
View File
@@ -3,15 +3,17 @@ PHASED days x tDCS LME -- mergeNaive (Box-B2 vs Box-A2)
==============================================================================
model per phase: success ~ dayp*tDCS + (1|subject) [dayp = day - phaseStart]
phase N A2slope B2slope day p tDCS(lvl) p interaction p slopeDiff [95% CI]
--------------------------------------------------------------------------------------------
0-5 11 8.26 15.16 1.6e-10 0.852 0.000 +6.90 [+3.40, +10.41]
6-10 10 5.97 4.83 3.9e-05 0.007 0.584 -1.14 [-5.31, +3.03]
6-13 10 2.83 4.47 0.00071 0.012 0.196 +1.65 [-0.87, +4.17]
phase N A2slp B2slp day p tDCS p intP(res) intP(Satt) intP(RS) slopeDiff [95% CI]
--------------------------------------------------------------------------------------------------------
0-5 11 8.26 15.16 1.6e-10 0.852 0.000 0.000 0.013 +6.90 [+3.40,+10.41]
6-10 10 5.97 4.83 3.9e-05 0.007 0.584 0.585 0.643 -1.14 [-5.31,+3.03]
6-13 10 2.83 4.47 0.00071 0.012 0.196 0.196 0.333 +1.65 [-0.87,+4.17]
Note: the interaction p (and CI) use fitlme observation-level DF and are
ANTICONSERVATIVE at these small subject counts (see tdcs_power_sim). The early
phase carries the Box-B2 faster-acquisition signal; late phases converge.
Columns: intP(res)=observation-level DF (anticonservative); intP(Satt)=Satterthwaite
DF (~= residual on this random-intercept model); intP(RS)=per-animal random-slope
(dayp|subject) model, the honest test (DF collapses toward the animal count; 'n/a' if it
did not converge). The early phase carries the Box-B2 faster-acquisition signal; late
phases converge.
==============================================================================
FULL MODEL SUMMARY -- phase 0-5: success ~ dayp*tDCS + (1|subject)
==============================================================================
+10 -8
View File
@@ -3,15 +3,17 @@ PHASED days x tDCS LME -- unmerged (Box-B2 vs Box-A2)
==============================================================================
model per phase: success ~ dayp*tDCS + (1|subject) [dayp = day - phaseStart]
phase N A2slope B2slope day p tDCS(lvl) p interaction p slopeDiff [95% CI]
--------------------------------------------------------------------------------------------
0-5 6 9.86 15.22 1.6e-07 0.689 0.015 +5.36 [+1.11, +9.62]
6-10 5 2.40 4.17 0.23 0.098 0.486 +1.77 [-3.41, +6.95]
6-13 5 1.71 3.98 0.12 0.101 0.097 +2.27 [-0.44, +4.97]
phase N A2slp B2slp day p tDCS p intP(res) intP(Satt) intP(RS) slopeDiff [95% CI]
--------------------------------------------------------------------------------------------------------
0-5 6 9.86 15.22 1.6e-07 0.689 0.015 0.015 0.133 +5.36 [+1.11,+9.62]
6-10 5 2.40 4.17 0.23 0.098 0.486 0.486 0.555 +1.77 [-3.41,+6.95]
6-13 5 1.71 3.98 0.12 0.101 0.097 0.097 0.138 +2.27 [-0.44,+4.97]
Note: the interaction p (and CI) use fitlme observation-level DF and are
ANTICONSERVATIVE at these small subject counts (see tdcs_power_sim). The early
phase carries the Box-B2 faster-acquisition signal; late phases converge.
Columns: intP(res)=observation-level DF (anticonservative); intP(Satt)=Satterthwaite
DF (~= residual on this random-intercept model); intP(RS)=per-animal random-slope
(dayp|subject) model, the honest test (DF collapses toward the animal count; 'n/a' if it
did not converge). The early phase carries the Box-B2 faster-acquisition signal; late
phases converge.
==============================================================================
FULL MODEL SUMMARY -- phase 0-5: success ~ dayp*tDCS + (1|subject)
==============================================================================
+14 -6
View File
@@ -32,24 +32,32 @@ T.tDCS = double(T.group == cfg.anchorHigh); % Box-B2 = 1, Box-A2 = 0
lme = fitlme(T, 'success ~ day*tDCS + (1|subject)');
C = lme.Coefficients;
A = anova(lme);
As = anova(lme, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
L.lme = lme;
L.nSubjects = numel(unique(T.subject));
L.nObs = height(T);
L.maxDayA = max(T.day(T.group == cfg.anchorLow)); % last day Box-A2 has data
L.maxDayB = max(T.day(T.group == cfg.anchorHigh)); % last day Box-B2 has data
L.interaction = localTerm(C, A, 'day:tDCS');
L.day = localTerm(C, A, 'day');
L.tDCS = localTerm(C, A, 'tDCS');
L.interaction = localTerm(C, A, As, 'day:tDCS');
L.day = localTerm(C, A, As, 'day');
L.tDCS = localTerm(C, A, As, 'tDCS');
% Honest test: refit with a per-subject random SLOPE (see
% tdcs_random_slope_interaction) so the interaction DF collapses toward n.
L.interRS = tdcs_random_slope_interaction(T, ...
'success ~ day*tDCS + (day|subject)', 'day:tDCS');
end
function e = localTerm(C, A, name)
%LOCALTERM Pull one term's coefficient (t/df/p) and ANOVA (F/df/p) stats.
function e = localTerm(C, A, As, name)
%LOCALTERM Pull one term's coefficient (t/df/p), residual-DF ANOVA (F/df/p),
% and Satterthwaite denominator DF + p (.dfSatt, .pSatt).
ci = strcmp(C.Name, name);
ai = strcmp(A.Term, name);
si = strcmp(As.Term, name);
e = struct( ...
'estimate', C.Estimate(ci), 'se', C.SE(ci), ...
't', C.tStat(ci), 'df', C.DF(ci), 'p', C.pValue(ci), ...
'F', A.FStat(ai), 'df1', A.DF1(ai), 'df2', A.DF2(ai), 'Fp', A.pValue(ai));
'F', A.FStat(ai), 'df1', A.DF1(ai), 'df2', A.DF2(ai), 'Fp', A.pValue(ai), ...
'dfSatt', As.DF2(si), 'pSatt', As.pValue(si));
end
+21 -3
View File
@@ -25,11 +25,12 @@ s = [s sprintf('\n')];
s = [s tdcs_model_summary(L.lme, 'fitlme: success ~ day*tDCS + (1|subject)') sprintf('\n')];
s = [s sprintf('%-27s %-14s %-13s %s\n', 'effect', 't (df)', 'F (df1)', 'p')];
s = [s sprintf('%s\n', repmat('-', 1, 70))];
s = [s sprintf('%-27s %-20s %-12s %s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)')];
s = [s sprintf('%s\n', repmat('-', 1, 78))];
s = [s localRow('days x tDCS (interaction)', L.interaction)];
s = [s localRow('days (learning)', L.day)];
s = [s localRow('tDCS (main, at Day 1)', L.tDCS)];
s = [s localHonest(L.interRS)];
s = [s sprintf('\nINTERPRETATION\n')];
if L.interaction.p >= 0.05
@@ -66,7 +67,24 @@ fprintf(fid, '%s', s);
end
function r = localRow(name, e)
r = sprintf('%-27s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', name, e.df, e.t, e.df1, e.F, e.p);
r = sprintf('%-27s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', ...
name, e.df, e.t, e.df1, e.F, e.p, e.pSatt, e.dfSatt);
end
function s = localHonest(rs)
%LOCALHONEST Report the random-slope interaction (the honest LME test) + why
% Satterthwaite on the random-intercept model above barely changes the DF.
if rs.ok
s = sprintf(['\nHONEST LME -- per-animal random slope (day|subject): ' ...
'interaction F(%d,%.1f)=%.2f, p=%.4g\n'], rs.df1, rs.df2, rs.F, rs.p);
else
s = sprintf(['\nHONEST LME -- per-animal random slope (day|subject): ' ...
'model did not converge for this window.\n']);
end
s = [s sprintf([' Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual\n' ...
' (the slope''s error is at the session level), so it does NOT fix pseudoreplication.\n' ...
' Letting each animal have its OWN slope collapses the interaction DF toward the animal\n' ...
' count -- this, and the per-animal slope test, are the honest learning-rate inference.\n'])];
end
function t = localSig(p)
+30 -8
View File
@@ -44,6 +44,7 @@ tbl.rat = A.subject;
model = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = model.Coefficients; An = anova(model); ci = coefCI(model);
Ans = anova(model, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
L.model = model;
L.windowKey = windowKey;
@@ -51,24 +52,30 @@ L.nRats = numel(unique(tbl.rat));
L.nObs = height(tbl);
L.maxDayCtrl = max(tbl.day(tbl.stim == 0));
L.maxDayStim = max(tbl.day(tbl.stim == 1));
L.stim = localTerm(C, An, ci, 'stim');
L.day = localTerm(C, An, ci, 'day');
L.interaction = localTerm(C, An, ci, 'day:stim'); % MATLAB canonicalizes stim:day -> day:stim
L.stim = localTerm(C, An, Ans, ci, 'stim');
L.day = localTerm(C, An, Ans, ci, 'day');
L.interaction = localTerm(C, An, Ans, ci, 'day:stim'); % MATLAB canonicalizes stim:day -> day:stim
% Honest test: refit with a per-rat random SLOPE so the interaction DF
% collapses toward the animal count (see tdcs_random_slope_interaction).
L.interRS = tdcs_random_slope_interaction(tbl, ...
'behavior ~ stim + day + stim:day + (day|rat)', 'day:stim');
localReport(L, mergeKey, cfg);
end
function e = localTerm(C, An, ci, name)
function e = localTerm(C, An, Ans, ci, name)
i = strcmp(C.Name, name);
if ~any(i)
error('tdcs_paper_lme:missingTerm', 'No "%s" coefficient (have: %s).', ...
name, strjoin(C.Name, ', '));
end
ai = strcmp(An.Term, name);
si = strcmp(Ans.Term, name);
e = struct('estimate', C.Estimate(i), 'se', C.SE(i), 't', C.tStat(i), ...
'df', C.DF(i), 'p', C.pValue(i), 'F', An.FStat(ai), 'df1', An.DF1(ai), ...
'df2', An.DF2(ai), 'Fp', An.pValue(ai), 'ci', ci(i, :));
'df2', An.DF2(ai), 'Fp', An.pValue(ai), 'ci', ci(i, :), ...
'dfSatt', Ans.DF2(si), 'pSatt', Ans.pValue(si));
end
function localReport(L, mergeKey, cfg)
@@ -101,12 +108,13 @@ else
end
s = [s sprintf('\n')];
s = [s tdcs_model_summary(L.model, 'fitlme: behavior ~ stim + day + stim:day + (1|rat)') sprintf('\n')];
s = [s sprintf('\n%-26s %-13s %-13s %s\n', 'effect', 't (df)', 'F (df1)', 'p')];
s = [s sprintf('%s\n', repmat('-', 1, 66))];
s = [s sprintf('\n%-26s %-18s %-12s %s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)')];
s = [s sprintf('%s\n', repmat('-', 1, 76))];
s = [s localRow('stim x day (interaction)', L.interaction)];
s = [s localRow('day (learning)', L.day)];
s = [s localRow('stim (main, Day 1)', L.stim)];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', L.interaction.ci(1), L.interaction.ci(2))];
s = [s localHonest(L.interRS)];
s = [s sprintf('\nINTERPRETATION\n')];
if L.interaction.p >= 0.05
interTxt = 'slopes parallel -- no differential learning rate over this window';
@@ -133,7 +141,21 @@ fprintf(fid, '%s', s);
end
function r = localRow(name, e)
r = sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', name, e.df, e.t, e.df1, e.F, e.p);
r = sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', ...
name, e.df, e.t, e.df1, e.F, e.p, e.pSatt, e.dfSatt);
end
function s = localHonest(rs)
if rs.ok
s = sprintf(['\nHONEST LME -- per-rat random slope (day|rat): ' ...
'interaction F(%d,%.1f)=%.2f, p=%.4g\n'], rs.df1, rs.df2, rs.F, rs.p);
else
s = sprintf(['\nHONEST LME -- per-rat random slope (day|rat): ' ...
'model did not converge for this window.\n']);
end
s = [s sprintf([' Satterthwaite DF on the random-INTERCEPT model above stays ~= residual (the\n' ...
' slope''s error is at session level), so it does NOT fix pseudoreplication. A per-animal\n' ...
' random slope collapses the interaction DF toward the animal count -- the honest test.\n'])];
end
function t = localSigTxt(p)
+15 -7
View File
@@ -30,8 +30,8 @@ s = sprintf('%s\n', bar);
s = [s sprintf('PHASED days x tDCS LME -- %s (Box-B2 vs Box-A2)\n', mergeKey)];
s = [s sprintf('%s\n', bar)];
s = [s sprintf('model per phase: success ~ dayp*tDCS + (1|subject) [dayp = day - phaseStart]\n\n')];
s = [s sprintf('%-9s N A2slope B2slope day p tDCS(lvl) p interaction p slopeDiff [95%% CI]\n', 'phase')];
s = [s sprintf('%s\n', repmat('-', 1, 92))];
s = [s sprintf('%-8s N A2slp B2slp day p tDCS p intP(res) intP(Satt) intP(RS) slopeDiff [95%% CI]\n', 'phase')];
s = [s sprintf('%s\n', repmat('-', 1, 104))];
rawSummaries = '';
for k = 1:numel(phases)
@@ -42,6 +42,9 @@ for k = 1:numel(phases)
lme = fitlme(Tp, 'success ~ dayp*tDCS + (1|subject)');
C = lme.Coefficients; A = anova(lme); ci = coefCI(lme);
As = anova(lme, 'DFMethod', 'satterthwaite');
rs = tdcs_random_slope_interaction(Tp, ...
'success ~ dayp*tDCS + (dayp|subject)', 'dayp:tDCS');
ii = strcmp(C.Name, 'dayp:tDCS');
di = strcmp(C.Name, 'dayp');
@@ -53,22 +56,27 @@ for k = 1:numel(phases)
e.dayP = A.pValue(strcmp(A.Term, 'dayp'));
e.tDCSlevelP = A.pValue(strcmp(A.Term, 'tDCS'));
e.interP = A.pValue(strcmp(A.Term, 'dayp:tDCS'));
e.interPsatt = As.pValue(strcmp(As.Term, 'dayp:tDCS')); % Satterthwaite DF
e.interPrs = rs.p; e.rsOk = rs.ok; % honest random-slope
e.interEst = C.Estimate(ii);
e.interCI = ci(ii, :);
P.phases{k} = e;
s = [s sprintf('%d-%-6d %d %6.2f %6.2f %-9.2g %-11.3f %-13.3f %+.2f [%+.2f, %+.2f]\n', ...
if rs.ok; rsStr = sprintf('%.3f', rs.p); else; rsStr = 'n/a'; end
s = [s sprintf('%d-%-5d %d %5.2f %5.2f %-8.2g %-8.3f %-9.3f %-10.3f %-9s %+.2f [%+.2f,%+.2f]\n', ...
ph(1), ph(2), e.nSub, e.a2Slope, e.b2Slope, e.dayP, e.tDCSlevelP, ...
e.interP, e.interEst, e.interCI(1), e.interCI(2))]; %#ok<AGROW>
e.interP, e.interPsatt, rsStr, e.interEst, e.interCI(1), e.interCI(2))]; %#ok<AGROW>
rawSummaries = [rawSummaries tdcs_model_summary(lme, ...
sprintf('phase %d-%d: success ~ dayp*tDCS + (1|subject)', ph(1), ph(2))) ...
sprintf('\n')]; %#ok<AGROW>
end
s = [s sprintf(['\nNote: the interaction p (and CI) use fitlme observation-level DF and are\n' ...
'ANTICONSERVATIVE at these small subject counts (see tdcs_power_sim). The early\n' ...
'phase carries the Box-B2 faster-acquisition signal; late phases converge.\n'])];
s = [s sprintf(['\nColumns: intP(res)=observation-level DF (anticonservative); intP(Satt)=Satterthwaite\n' ...
'DF (~= residual on this random-intercept model); intP(RS)=per-animal random-slope\n' ...
'(dayp|subject) model, the honest test (DF collapses toward the animal count; ''n/a'' if it\n' ...
'did not converge). The early phase carries the Box-B2 faster-acquisition signal; late\n' ...
'phases converge.\n'])];
s = [s rawSummaries];
@@ -0,0 +1,33 @@
function rs = tdcs_random_slope_interaction(T, formula, term)
%TDCS_RANDOM_SLOPE_INTERACTION Honest LME test of a slope-interaction TERM.
% RS = TDCS_RANDOM_SLOPE_INTERACTION(T, FORMULA, TERM) refits the linear mixed
% model FORMULA (which must include a per-subject random SLOPE, e.g.
% 'success ~ day*tDCS + (day|subject)') on table T and returns the
% Satterthwaite-DF marginal F-test of TERM (e.g. 'day:tDCS').
%
% Why this is the honest test. The paper's model has only a random INTERCEPT,
% so it assumes every animal shares one true slope and estimates the slope /
% interaction with session-level precision -- Satterthwaite DF on that model
% stays ~= residual DF and does NOT fix the pseudoreplication. Giving each
% animal its OWN slope (a random slope) lets the between-animal slope variance
% enter the standard error, and the Satterthwaite denominator DF then collapses
% toward the number of animals -- matching the per-animal (cluster-honest)
% test. RS fields: .ok (false if the richer model failed to fit), .F, .df1,
% .df2 (Satterthwaite), .p.
rs = struct('ok', false, 'F', NaN, 'df1', NaN, 'df2', NaN, 'p', NaN);
w = warning('off', 'all'); cleanup = onCleanup(@() warning(w)); %#ok<NASGU>
try
lme = fitlme(T, formula);
A = anova(lme, 'DFMethod', 'satterthwaite');
i = strcmp(A.Term, term);
if any(i)
rs = struct('ok', true, 'F', A.FStat(i), 'df1', A.DF1(i), ...
'df2', A.DF2(i), 'p', A.pValue(i));
end
catch
% Random-slope model unidentifiable / non-convergent (common in short
% windows or with very few animals): leave rs.ok = false.
end
end
+21
View File
@@ -123,6 +123,27 @@ classdef tLme < matlab.unittest.TestCase
testCase.verifyFalse(any(sg.group == 'Naive'));
end
function testLmeReportsHaveSatterthwaiteAndHonest(testCase)
% Every fitlme report adds Satterthwaite DF and the honest
% random-slope interaction test.
cfg = tdcs_config();
L = tdcs_lme(tdcs_scenario_data('mergeA2_full'), cfg);
testCase.verifyTrue(isfield(L.interaction, 'pSatt'));
testCase.verifyTrue(isfield(L.interaction, 'dfSatt'));
testCase.verifyTrue(isfield(L, 'interRS') && islogical(L.interRS.ok));
% The full-range mergeA2 interaction is significant under obs-level
% DF but NOT under the honest per-animal random slope (it collapses).
testCase.verifyLessThan(L.interaction.p, 0.05);
testCase.verifyTrue(L.interRS.ok);
testCase.verifyGreaterThan(L.interRS.p, 0.05);
% Report text carries the Satterthwaite column and honest block.
here = fileparts(fileparts(mfilename('fullpath')));
evalc("tdcs_glm('lme_mergeA2_full')");
txt = fileread(fullfile(here, 'results', 'lme_mergeA2_full.txt'));
testCase.verifyTrue(contains(txt, 'Satterthwaite'));
testCase.verifyTrue(contains(txt, 'HONEST LME'));
end
function testPaperFormulaReplicatesInteraction(testCase)
% The paper's exact formula (behavior ~ stim + day + stim:day +
% (1|rat)) on mergeA2 reproduces the paper's interaction
+27 -3
View File
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
+21 -21
View File
@@ -1,21 +1,21 @@
variation,grouping,window,nRats,nObs,covEqual,interaction_p,interaction_est,stim_p,day_p
unmerge_d0_10,unmerge,d0_10,6,61,1,0.142831730250263,1.55766564374789,0.497389134925754,2.83166776054496e-14
unmerge_d0_13,unmerge,d0_13,6,70,1,0.1037612192755,1.54665472676189,0.343602740986316,3.0346358823857e-13
unmerge_d0_5,unmerge,d0_5,6,36,1,0.015149263054839,5.36190476190475,0.688839435920596,1.57055423419133e-07
unmerge_d6_10,unmerge,d6_10,5,25,1,0.485980598616448,1.76666666666666,0.0975215172907211,0.227259314093653
unmerge_d6_13,unmerge,d6_13,5,34,1,0.0971009786566259,2.26696773140328,0.101395665857404,0.117493504965036
right_only_d0_10,right_only,d0_10,7,72,1,0.072180139606096,1.73004431141938,0.715879551990348,5.94289273347193e-16
right_only_d0_13,right_only,d0_13,7,84,1,0.0480416099228058,1.7001712792199,0.528517264593511,1.17078898575779e-14
right_only_d0_5,right_only,d0_5,7,42,1,0.00589730538358453,5.30714285714286,0.469556667463413,1.45827052706827e-08
right_only_d6_10,right_only,d6_10,6,30,1,0.288979620167748,2.425,0.119238243812818,0.200957769624237
right_only_d6_13,right_only,d6_13,6,42,1,0.0258497485171371,2.75792254325755,0.109557181635453,0.0976080018855909
naive_a2_d0_10,naive_a2,d0_10,10,104,1,0.125891264174292,1.43371876298297,0.141836960529428,1.59796354184479e-27
naive_a2_d0_13,naive_a2,d0_13,10,124,1,0.0397967517409217,1.61366204723722,0.153633906173184,2.5469330031931e-28
naive_a2_d0_5,naive_a2,d0_5,10,59,1,0.00107331575782455,6.96223661591522,0.960550975833492,1.1254222201539e-09
naive_a2_d6_10,naive_a2,d6_10,9,45,1,0.449922139297732,-1.8,0.00929038977362001,8.02730473236241e-05
naive_a2_d6_13,naive_a2,d6_13,9,65,1,0.439759713746994,1.15527349799111,0.0165675905421826,0.00123453301903328
naive_boxa_d0_10,naive_boxa,d0_10,11,115,1,0.194116831474294,1.17792772470162,0.124842156542362,1.7431506758907e-32
naive_boxa_d0_13,naive_boxa,d0_13,11,138,1,0.0848056366333164,1.30637615965008,0.140502409490992,1.21292542612342e-34
naive_boxa_d0_5,naive_boxa,d0_5,11,65,1,0.00254470765200637,6.40329932104724,0.881419104187055,2.50618640106882e-11
naive_boxa_d6_10,naive_boxa,d6_10,10,50,1,0.46817073850354,-1.61904761904762,0.014689379915428,1.87688778586107e-05
naive_boxa_d6_13,naive_boxa,d6_13,10,73,1,0.530807487326757,0.873689202983884,0.0303916991304334,6.44631091768978e-05
variation,grouping,window,nRats,nObs,covEqual,interaction_p,interaction_p_satt,interaction_p_rs,interaction_est,stim_p,day_p
unmerge_d0_10,unmerge,d0_10,6,61,1,0.142831730250263,0.142797566084653,0.247946685371652,1.55766564374789,0.497389134925754,2.83166776054496e-14
unmerge_d0_13,unmerge,d0_13,6,70,1,0.1037612192755,0.103490077847449,0.222686826261486,1.54665472676189,0.343602740986316,3.0346358823857e-13
unmerge_d0_5,unmerge,d0_5,6,36,1,0.015149263054839,0.0154996899553134,0.133145634512884,5.36190476190475,0.688839435920596,1.57055423419133e-07
unmerge_d6_10,unmerge,d6_10,5,25,1,0.485980598616448,0.486366669144796,0.554919025308352,1.76666666666666,0.0975215172907211,0.227259314093653
unmerge_d6_13,unmerge,d6_13,5,34,1,0.0971009786566259,0.0970128901807668,0.13816578257917,2.26696773140328,0.101395665857404,0.117493504965036
right_only_d0_10,right_only,d0_10,7,72,1,0.072180139606096,0.0721794737454714,0.124574209202325,1.73004431141938,0.715879551990348,5.94289273347193e-16
right_only_d0_13,right_only,d0_13,7,84,1,0.0480416099228058,0.0478799998667326,0.102857124294903,1.7001712792199,0.528517264593511,1.17078898575779e-14
right_only_d0_5,right_only,d0_5,7,42,1,0.00589730538358453,0.00612750828678172,0.0876259332262465,5.30714285714286,0.469556667463413,1.45827052706827e-08
right_only_d6_10,right_only,d6_10,6,30,1,0.288979620167748,0.289798539517805,0.378365014630273,2.425,0.119238243812818,0.200957769624237
right_only_d6_13,right_only,d6_13,6,42,1,0.0258497485171371,0.0259134063416617,0.0369743190514673,2.75792254325755,0.109557181635453,0.0976080018855909
naive_a2_d0_10,naive_a2,d0_10,10,104,1,0.125891264174292,0.126071775330177,0.30932733005532,1.43371876298297,0.141836960529428,1.59796354184479e-27
naive_a2_d0_13,naive_a2,d0_13,10,124,1,0.0397967517409217,0.0398599319703312,0.0578149015961064,1.61366204723722,0.153633906173184,2.5469330031931e-28
naive_a2_d0_5,naive_a2,d0_5,10,59,1,0.00107331575782455,0.00115218904921896,0.028152310883297,6.96223661591522,0.960550975833492,1.1254222201539e-09
naive_a2_d6_10,naive_a2,d6_10,9,45,1,0.449922139297732,0.450525162691322,0.522263670801849,-1.8,0.00929038977362001,8.02730473236241e-05
naive_a2_d6_13,naive_a2,d6_13,9,65,1,0.439759713746994,0.439915577313227,0.563543413702517,1.15527349799111,0.0165675905421826,0.00123453301903328
naive_boxa_d0_10,naive_boxa,d0_10,11,115,1,0.194116831474294,0.194284662755081,0.383551190839722,1.17792772470162,0.124842156542362,1.7431506758907e-32
naive_boxa_d0_13,naive_boxa,d0_13,11,138,1,0.0848056366333164,0.0848864932608599,0.140324990567599,1.30637615965008,0.140502409490992,1.21292542612342e-34
naive_boxa_d0_5,naive_boxa,d0_5,11,65,1,0.00254470765200637,0.00267934065308424,0.0356145839564805,6.40329932104724,0.881419104187055,2.50618640106882e-11
naive_boxa_d6_10,naive_boxa,d6_10,10,50,1,0.46817073850354,0.468724498567948,0.534407300553877,-1.61904761904762,0.014689379915428,1.87688778586107e-05
naive_boxa_d6_13,naive_boxa,d6_13,10,73,1,0.530807487326757,0.530942127564996,0.696589180707779,0.873689202983884,0.0303916991304334,6.44631091768978e-05
1 variation grouping window nRats nObs covEqual interaction_p interaction_p_satt interaction_p_rs interaction_est stim_p day_p
2 unmerge_d0_10 unmerge d0_10 6 61 1 0.142831730250263 0.142797566084653 0.247946685371652 1.55766564374789 0.497389134925754 2.83166776054496e-14
3 unmerge_d0_13 unmerge d0_13 6 70 1 0.1037612192755 0.103490077847449 0.222686826261486 1.54665472676189 0.343602740986316 3.0346358823857e-13
4 unmerge_d0_5 unmerge d0_5 6 36 1 0.015149263054839 0.0154996899553134 0.133145634512884 5.36190476190475 0.688839435920596 1.57055423419133e-07
5 unmerge_d6_10 unmerge d6_10 5 25 1 0.485980598616448 0.486366669144796 0.554919025308352 1.76666666666666 0.0975215172907211 0.227259314093653
6 unmerge_d6_13 unmerge d6_13 5 34 1 0.0971009786566259 0.0970128901807668 0.13816578257917 2.26696773140328 0.101395665857404 0.117493504965036
7 right_only_d0_10 right_only d0_10 7 72 1 0.072180139606096 0.0721794737454714 0.124574209202325 1.73004431141938 0.715879551990348 5.94289273347193e-16
8 right_only_d0_13 right_only d0_13 7 84 1 0.0480416099228058 0.0478799998667326 0.102857124294903 1.7001712792199 0.528517264593511 1.17078898575779e-14
9 right_only_d0_5 right_only d0_5 7 42 1 0.00589730538358453 0.00612750828678172 0.0876259332262465 5.30714285714286 0.469556667463413 1.45827052706827e-08
10 right_only_d6_10 right_only d6_10 6 30 1 0.288979620167748 0.289798539517805 0.378365014630273 2.425 0.119238243812818 0.200957769624237
11 right_only_d6_13 right_only d6_13 6 42 1 0.0258497485171371 0.0259134063416617 0.0369743190514673 2.75792254325755 0.109557181635453 0.0976080018855909
12 naive_a2_d0_10 naive_a2 d0_10 10 104 1 0.125891264174292 0.126071775330177 0.30932733005532 1.43371876298297 0.141836960529428 1.59796354184479e-27
13 naive_a2_d0_13 naive_a2 d0_13 10 124 1 0.0397967517409217 0.0398599319703312 0.0578149015961064 1.61366204723722 0.153633906173184 2.5469330031931e-28
14 naive_a2_d0_5 naive_a2 d0_5 10 59 1 0.00107331575782455 0.00115218904921896 0.028152310883297 6.96223661591522 0.960550975833492 1.1254222201539e-09
15 naive_a2_d6_10 naive_a2 d6_10 9 45 1 0.449922139297732 0.450525162691322 0.522263670801849 -1.8 0.00929038977362001 8.02730473236241e-05
16 naive_a2_d6_13 naive_a2 d6_13 9 65 1 0.439759713746994 0.439915577313227 0.563543413702517 1.15527349799111 0.0165675905421826 0.00123453301903328
17 naive_boxa_d0_10 naive_boxa d0_10 11 115 1 0.194116831474294 0.194284662755081 0.383551190839722 1.17792772470162 0.124842156542362 1.7431506758907e-32
18 naive_boxa_d0_13 naive_boxa d0_13 11 138 1 0.0848056366333164 0.0848864932608599 0.140324990567599 1.30637615965008 0.140502409490992 1.21292542612342e-34
19 naive_boxa_d0_5 naive_boxa d0_5 11 65 1 0.00254470765200637 0.00267934065308424 0.0356145839564805 6.40329932104724 0.881419104187055 2.50618640106882e-11
20 naive_boxa_d6_10 naive_boxa d6_10 10 50 1 0.46817073850354 0.468724498567948 0.534407300553877 -1.61904761904762 0.014689379915428 1.87688778586107e-05
21 naive_boxa_d6_13 naive_boxa d6_13 10 73 1 0.530807487326757 0.530942127564996 0.696589180707779 0.873689202983884 0.0303916991304334 6.44631091768978e-05
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 8.5397 6.1599 11.839
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(20)= 3.27 F(1)= 10.700 p=0.003822
day (learning) t(20)= 2.89 F(1)= 8.337 p=0.009105
stim (main, window start) t(20)= -1.42 F(1)= 2.025 p=0.1701
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(20)= 3.27 F(1)= 10.700 p=0.003822 p=0.004244 (df=18)
day (learning) t(20)= 2.89 F(1)= 8.337 p=0.009105 p=0.009807 (df=18)
stim (main, window start) t(20)= -1.42 F(1)= 2.025 p=0.1701 p=0.1703 (df=20)
interaction 95% CI: [+3.70, +16.70]
HONEST LME (per-animal random slope, day|rat): interaction F(1,9.8)=9.76, p=0.01107
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.003822, slope diff=+10.20)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 13.78 11.942 15.902
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(100)= 1.54 F(1)= 2.382 p=0.1259
day (learning) t(100)= 15.09 F(1)= 227.710 p=1.598e-27
stim (main, window start) t(100)= 1.48 F(1)= 2.192 p=0.1418
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(100)= 1.54 F(1)= 2.382 p=0.1259 p=0.1261 (df=95)
day (learning) t(100)= 15.09 F(1)=227.710 p=1.598e-27 p=4.059e-27 (df=96)
stim (main, window start) t(100)= 1.48 F(1)= 2.192 p=0.1418 p=0.1543 (df=20)
interaction 95% CI: [-0.41, +3.28]
HONEST LME (per-animal random slope, day|rat): interaction F(1,9.0)=1.16, p=0.3093
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.1259, slope diff=+1.43)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 15.264 13.405 17.381
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(120)= 2.08 F(1)= 4.320 p=0.0398
day (learning) t(120)= 14.57 F(1)= 212.424 p=2.547e-28
stim (main, window start) t(120)= 1.44 F(1)= 2.062 p=0.1536
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(120)= 2.08 F(1)= 4.320 p=0.0398 p=0.03986 (df=117)
day (learning) t(120)= 14.57 F(1)=212.424 p=2.547e-28 p=3.246e-28 (df=119)
stim (main, window start) t(120)= 1.44 F(1)= 2.062 p=0.1536 p=0.1655 (df=21)
interaction 95% CI: [+0.08, +3.15]
HONEST LME (per-animal random slope, day|rat): interaction F(1,41.7)=3.81, p=0.05781
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.0398, slope diff=+1.61)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -34,7 +50,7 @@ if abs(maxT - maxC) > 2
else
cov = '(equal day coverage over this window)';
end
y
ii = gi('day:stim'); pI = C.pValue(ii); eI = C.Estimate(ii);
if pI >= 0.05
verdict = 'n.s. -- slopes parallel (no differential learning rate)';
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -28,27 +28,41 @@ Model fit statistics:
489.28 501.74 -238.64 477.28
Fixed effects coefficients (95% CIs):
Name Estimate SE tStat DF pValue Lower Upper
{'(Intercept)'} 9.0584 5.0267 1.8021 55 0.077018 -1.0153 19.132
{'day' } 8.2568 1.1279 7.3206 55 1.1254e-09 5.9965 10.517
{'stim' } 0.44958 9.048 0.049688 55 0.96055 -17.683 18.582
{'day:stim' } 6.9622 2.0162 3.4532 55 0.0010733 2.9217 11.003
Name Estimate SE tStat DF pValue
{'(Intercept)'} 9.0584 5.0267 1.8021 55 0.077018
{'day' } 8.2568 1.1279 7.3206 55 1.1254e-09
{'stim' } 0.44958 9.048 0.049688 55 0.96055
{'day:stim' } 6.9622 2.0162 3.4532 55 0.0010733
Lower Upper
-1.0153 19.132
5.9965 10.517
-17.683 18.582
2.9217 11.003
Random effects covariance parameters (95% CIs):
Group: rat (10 Levels)
Name1 Name2 Type Estimate Lower Upper
{'(Intercept)'} {'(Intercept)'} {'std'} 9.6432 5.5002 16.907
Name1 Name2 Type Estimate
{'(Intercept)'} {'(Intercept)'} {'std'} 9.6432
Lower Upper
5.5002 16.907
Group: Error
Name Estimate Lower Upper
{'Res Std'} 12.109 9.9317 14.763
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(55)= 3.45 F(1)= 11.924 p=0.001073
day (learning) t(55)= 7.32 F(1)= 53.591 p=1.125e-09
stim (main, window start) t(55)= 0.05 F(1)= 0.002 p=0.9606
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(55)= 3.45 F(1)= 11.924 p=0.001073 p=0.001152 (df=49)
day (learning) t(55)= 7.32 F(1)= 53.591 p=1.125e-09 p=2.092e-09 (df=49)
stim (main, window start) t(55)= 0.05 F(1)= 0.002 p=0.9606 p=0.9609 (df=20)
interaction 95% CI: [+2.92, +11.00]
HONEST LME (per-animal random slope, day|rat): interaction F(1,9.7)=6.65, p=0.02815
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.001073, slope diff=+6.96)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 10.552 8.376 13.294
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(41)= -0.76 F(1)= 0.582 p=0.4499
day (learning) t(41)= 4.38 F(1)= 19.183 p=8.027e-05
stim (main, window start) t(41)= 2.73 F(1)= 7.453 p=0.00929
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(41)= -0.76 F(1)= 0.582 p=0.4499 p=0.4505 (df=36)
day (learning) t(41)= 4.38 F(1)= 19.183 p=8.027e-05 p=9.813e-05 (df=36)
stim (main, window start) t(41)= 2.73 F(1)= 7.453 p=0.00929 p=0.01541 (df=15)
interaction 95% CI: [-6.57, +2.97]
HONEST LME (per-animal random slope, day|rat): interaction F(1,9.0)=0.44, p=0.5223
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.4499, slope diff=-1.80)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 12.033 10.004 14.473
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(61)= 0.78 F(1)= 0.605 p=0.4398
day (learning) t(61)= 3.39 F(1)= 11.485 p=0.001235
stim (main, window start) t(61)= 2.46 F(1)= 6.072 p=0.01657
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(61)= 0.78 F(1)= 0.605 p=0.4398 p=0.4399 (df=58)
day (learning) t(61)= 3.39 F(1)= 11.485 p=0.001235 p=0.001274 (df=57)
stim (main, window start) t(61)= 2.46 F(1)= 6.072 p=0.01657 p=0.02473 (df=17)
interaction 95% CI: [-1.82, +4.13]
HONEST LME (per-animal random slope, day|rat): interaction F(1,7.7)=0.36, p=0.5635
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.4398, slope diff=+1.16)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 13.706 11.962 15.704
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(111)= 1.31 F(1)= 1.707 p=0.1941
day (learning) t(111)= 16.91 F(1)= 285.824 p=1.743e-32
stim (main, window start) t(111)= 1.55 F(1)= 2.392 p=0.1248
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(111)= 1.31 F(1)= 1.707 p=0.1941 p=0.1943 (df=104)
day (learning) t(111)= 16.91 F(1)=285.824 p=1.743e-32 p=6.758e-32 (df=106)
stim (main, window start) t(111)= 1.55 F(1)= 2.392 p=0.1248 p=0.1361 (df=22)
interaction 95% CI: [-0.61, +2.96]
HONEST LME (per-animal random slope, day|rat): interaction F(1,10.1)=0.83, p=0.3836
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.1941, slope diff=+1.18)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 15.18 13.424 17.167
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(134)= 1.74 F(1)= 3.015 p=0.08481
day (learning) t(134)= 16.74 F(1)= 280.201 p=1.213e-34
stim (main, window start) t(134)= 1.48 F(1)= 2.198 p=0.1405
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(134)= 1.74 F(1)= 3.015 p=0.08481 p=0.08489 (df=129)
day (learning) t(134)= 16.74 F(1)=280.201 p=1.213e-34 p=2.161e-34 (df=131)
stim (main, window start) t(134)= 1.48 F(1)= 2.198 p=0.1405 p=0.1519 (df=23)
interaction 95% CI: [-0.18, +2.79]
HONEST LME (per-animal random slope, day|rat): interaction F(1,25.3)=2.32, p=0.1403
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.08481, slope diff=+1.31)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 12.477 10.33 15.071
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(61)= 3.15 F(1)= 9.909 p=0.002545
day (learning) t(61)= 8.14 F(1)= 66.293 p=2.506e-11
stim (main, window start) t(61)= 0.15 F(1)= 0.022 p=0.8814
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(61)= 3.15 F(1)= 9.909 p=0.002545 p=0.002679 (df=54)
day (learning) t(61)= 8.14 F(1)= 66.293 p=2.506e-11 p=5.765e-11 (df=54)
stim (main, window start) t(61)= 0.15 F(1)= 0.022 p=0.8814 p=0.8822 (df=24)
interaction 95% CI: [+2.34, +10.47]
HONEST LME (per-animal random slope, day|rat): interaction F(1,10.5)=5.80, p=0.03561
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.002545, slope diff=+6.40)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 10.142 8.1466 12.627
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(46)= -0.73 F(1)= 0.535 p=0.4682
day (learning) t(46)= 4.77 F(1)= 22.779 p=1.877e-05
stim (main, window start) t(46)= 2.54 F(1)= 6.429 p=0.01469
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(46)= -0.73 F(1)= 0.535 p=0.4682 p=0.4687 (df=40)
day (learning) t(46)= 4.77 F(1)= 22.779 p=1.877e-05 p=2.436e-05 (df=40)
stim (main, window start) t(46)= 2.54 F(1)= 6.429 p=0.01469 p=0.02198 (df=16)
interaction 95% CI: [-6.07, +2.84]
HONEST LME (per-animal random slope, day|rat): interaction F(1,10.0)=0.41, p=0.5344
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.4682, slope diff=-1.62)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 11.525 9.6827 13.718
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(69)= 0.63 F(1)= 0.397 p=0.5308
day (learning) t(69)= 4.26 F(1)= 18.109 p=6.446e-05
stim (main, window start) t(69)= 2.21 F(1)= 4.886 p=0.03039
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(69)= 0.63 F(1)= 0.397 p=0.5308 p=0.5309 (df=65)
day (learning) t(69)= 4.26 F(1)= 18.109 p=6.446e-05 p=6.931e-05 (df=64)
stim (main, window start) t(69)= 2.21 F(1)= 4.886 p=0.03039 p=0.0412 (df=17)
interaction 95% CI: [-1.89, +3.64]
HONEST LME (per-animal random slope, day|rat): interaction F(1,8.5)=0.16, p=0.6966
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.5308, slope diff=+0.87)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 4.4892 4.0771 4.9429
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(227)= 2.66 F(1)= 7.088 p=0.008315
day (learning) t(227)= 8.94 F(1)= 79.838 p=1.43e-16
stim (main, window start) t(227)= 0.92 F(1)= 0.839 p=0.3606
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(227)= 2.66 F(1)= 7.088 p=0.008315 p=0.008365 (df=208)
day (learning) t(227)= 8.94 F(1)= 79.838 p=1.43e-16 p=2.16e-16 (df=209)
stim (main, window start) t(227)= 0.92 F(1)= 0.839 p=0.3606 p=0.3656 (df=37)
interaction 95% CI: [+0.15, +0.97]
HONEST LME (per-animal random slope, day|rat): interaction F(1,24.0)=3.01, p=0.09541
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.008315, slope diff=+0.56)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 11.956 10.063 14.205
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(68)= 1.83 F(1)= 3.336 p=0.07218
day (learning) t(68)= 10.55 F(1)= 111.222 p=5.943e-16
stim (main, window start) t(68)= 0.37 F(1)= 0.134 p=0.7159
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(68)= 1.83 F(1)= 3.336 p=0.07218 p=0.07218 (df=68)
day (learning) t(68)= 10.55 F(1)=111.222 p=5.943e-16 p=4.533e-16 (df=70)
stim (main, window start) t(68)= 0.37 F(1)= 0.134 p=0.7159 p=0.7187 (df=19)
interaction 95% CI: [-0.16, +3.62]
HONEST LME (per-animal random slope, day|rat): interaction F(1,20.5)=2.56, p=0.1246
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.07218, slope diff=+1.73)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 14.12 12.139 16.425
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(80)= 2.01 F(1)= 4.031 p=0.04804
day (learning) t(80)= 9.45 F(1)= 89.224 p=1.171e-14
stim (main, window start) t(80)= 0.63 F(1)= 0.401 p=0.5285
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(80)= 2.01 F(1)= 4.031 p=0.04804 p=0.04788 (df=84)
day (learning) t(80)= 9.45 F(1)= 89.224 p=1.171e-14 p=7.518e-15 (df=84)
stim (main, window start) t(80)= 0.63 F(1)= 0.401 p=0.5285 p=0.5284 (df=84)
interaction 95% CI: [+0.02, +3.39]
HONEST LME (per-animal random slope, day|rat): interaction F(1,20.2)=2.92, p=0.1029
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.04804, slope diff=+1.70)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 9.9638 7.8829 12.594
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(38)= 2.92 F(1)= 8.511 p=0.005897
day (learning) t(38)= 7.17 F(1)= 51.382 p=1.458e-08
stim (main, window start) t(38)= -0.73 F(1)= 0.534 p=0.4696
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(38)= 2.92 F(1)= 8.511 p=0.005897 p=0.006128 (df=35)
day (learning) t(38)= 7.17 F(1)= 51.382 p=1.458e-08 p=2.321e-08 (df=35)
stim (main, window start) t(38)= -0.73 F(1)= 0.534 p=0.4696 p=0.4735 (df=20)
interaction 95% CI: [+1.62, +8.99]
HONEST LME (per-animal random slope, day|rat): interaction F(1,7.0)=3.94, p=0.08763
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.005897, slope diff=+5.31)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 8.1802 6.1646 10.855
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(26)= 1.08 F(1)= 1.172 p=0.289
day (learning) t(26)= 1.31 F(1)= 1.722 p=0.201
stim (main, window start) t(26)= 1.61 F(1)= 2.596 p=0.1192
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(26)= 1.08 F(1)= 1.172 p=0.289 p=0.2898 (df=24)
day (learning) t(26)= 1.31 F(1)= 1.722 p=0.201 p=0.2019 (df=24)
stim (main, window start) t(26)= 1.61 F(1)= 2.596 p=0.1192 p=0.1297 (df=14)
interaction 95% CI: [-2.18, +7.03]
HONEST LME (per-animal random slope, day|rat): interaction F(1,6.0)=0.90, p=0.3784
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.289, slope diff=+2.42)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 7.3669 5.8525 9.2731
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(38)= 2.32 F(1)= 5.379 p=0.02585
day (learning) t(38)= 1.70 F(1)= 2.885 p=0.09761
stim (main, window start) t(38)= 1.64 F(1)= 2.685 p=0.1096
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(38)= 2.32 F(1)= 5.379 p=0.02585 p=0.02591 (df=38)
day (learning) t(38)= 1.70 F(1)= 2.885 p=0.09761 p=0.09766 (df=38)
stim (main, window start) t(38)= 1.64 F(1)= 2.685 p=0.1096 p=0.1304 (df=11)
interaction 95% CI: [+0.35, +5.17]
HONEST LME (per-animal random slope, day|rat): interaction F(1,21.2)=4.96, p=0.03697
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.02585, slope diff=+2.76)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 12.508 10.37 15.086
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(57)= 1.49 F(1)= 2.208 p=0.1428
day (learning) t(57)= 10.08 F(1)= 101.556 p=2.832e-14
stim (main, window start) t(57)= 0.68 F(1)= 0.466 p=0.4974
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(57)= 1.49 F(1)= 2.208 p=0.1428 p=0.1428 (df=57)
day (learning) t(57)= 10.08 F(1)=101.556 p=2.832e-14 p=1.931e-14 (df=59)
stim (main, window start) t(57)= 0.68 F(1)= 0.466 p=0.4974 p=0.5037 (df=17)
interaction 95% CI: [-0.54, +3.66]
HONEST LME (per-animal random slope, day|rat): interaction F(1,13.0)=1.46, p=0.2479
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.1428, slope diff=+1.56)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 14.676 12.436 17.32
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(66)= 1.65 F(1)= 2.721 p=0.1038
day (learning) t(66)= 9.09 F(1)= 82.593 p=3.035e-13
stim (main, window start) t(66)= 0.95 F(1)= 0.910 p=0.3436
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(66)= 1.65 F(1)= 2.721 p=0.1038 p=0.1035 (df=70)
day (learning) t(66)= 9.09 F(1)= 82.593 p=3.035e-13 p=1.823e-13 (df=70)
stim (main, window start) t(66)= 0.95 F(1)= 0.910 p=0.3436 p=0.3434 (df=70)
interaction 95% CI: [-0.33, +3.42]
HONEST LME (per-animal random slope, day|rat): interaction F(1,12.8)=1.64, p=0.2227
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.1038, slope diff=+1.55)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 10.703 8.3104 13.785
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(32)= 2.57 F(1)= 6.588 p=0.01515
day (learning) t(32)= 6.67 F(1)= 44.528 p=1.571e-07
stim (main, window start) t(32)= -0.40 F(1)= 0.163 p=0.6888
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(32)= 2.57 F(1)= 6.588 p=0.01515 p=0.0155 (df=30)
day (learning) t(32)= 6.67 F(1)= 44.528 p=1.571e-07 p=2.162e-07 (df=30)
stim (main, window start) t(32)= -0.40 F(1)= 0.163 p=0.6888 p=0.6906 (df=19)
interaction 95% CI: [+1.11, +9.62]
HONEST LME (per-animal random slope, day|rat): interaction F(1,6.0)=3.02, p=0.1331
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.01515, slope diff=+5.36)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 8.6289 6.3295 11.764
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(21)= 0.71 F(1)= 0.503 p=0.486
day (learning) t(21)= 1.24 F(1)= 1.547 p=0.2273
stim (main, window start) t(21)= 1.73 F(1)= 3.008 p=0.09752
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(21)= 0.71 F(1)= 0.503 p=0.486 p=0.4864 (df=20)
day (learning) t(21)= 1.24 F(1)= 1.547 p=0.2273 p=0.2279 (df=20)
stim (main, window start) t(21)= 1.73 F(1)= 3.008 p=0.09752 p=0.1098 (df=11)
interaction 95% CI: [-3.41, +6.95]
HONEST LME (per-animal random slope, day|rat): interaction F(1,5.0)=0.40, p=0.5549
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.486, slope diff=+1.77)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m); ci = coefCI(m);
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
% Honest test: refit with a per-animal random SLOPE so the interaction DF
% collapses toward the animal count (guarded -- may not converge in short windows).
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
wst = warning('off', 'all');
try
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
Ar = anova(mr, 'DFMethod', 'satterthwaite');
ri = strcmp(Ar.Term, 'day:stim');
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
catch
end
warning(wst);
gi = @(t) find(strcmp(C.Name, t), 1);
ga = @(t) find(strcmp(A.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
gs = @(t) find(strcmp(As.Term, t), 1);
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
As.pValue(gs(t)), As.DF2(gs(t)));
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
s = [s row('stim x day (interaction)', 'day:stim')];
s = [s row('day (learning)', 'day')];
s = [s row('stim (main, window start)', 'stim')];
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
if rsOk
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
else
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
end
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
@@ -70,5 +93,6 @@ fclose(fid);
% Machine-readable handoff for the summary table (see make_variations.m).
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'covEqual', abs(maxT - maxC) <= 2);
@@ -55,11 +55,14 @@ Group: Error
{'Res Std'} 7.7328 5.9847 9.9914
effect t (df) F (df1) p
------------------------------------------------------------------
stim x day (interaction) t(30)= 1.71 F(1)= 2.933 p=0.0971
day (learning) t(30)= 1.61 F(1)= 2.598 p=0.1175
stim (main, window start) t(30)= 1.69 F(1)= 2.856 p=0.1014
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
----------------------------------------------------------------------------
stim x day (interaction) t(30)= 1.71 F(1)= 2.933 p=0.0971 p=0.09701 (df=30)
day (learning) t(30)= 1.61 F(1)= 2.598 p=0.1175 p=0.1174 (df=30)
stim (main, window start) t(30)= 1.69 F(1)= 2.856 p=0.1014 p=0.1269 (df=9)
interaction 95% CI: [-0.44, +4.97]
HONEST LME (per-animal random slope, day|rat): interaction F(1,13.9)=2.48, p=0.1382
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
model above is the honest learning-rate test -- DF collapses toward the animal count.)
INTERPRETATION: stim x day interaction n.s. -- slopes parallel (no differential learning rate) (p=0.0971, slope diff=+2.27)
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
+345
View File
@@ -0,0 +1,345 @@
"""
Creates n8n workflow definitions for every entity in the experiments database.
Each workflow has an Execute Workflow Trigger + Switch (routes by `action` field)
+ one Postgres node per operation. They act as reusable sub-workflows.
"""
import json, uuid, requests
API = "http://localhost:5678/api/v1"
KEY = "n8n_api_ca07bd874c628ed0f52d9a2e65073960f9f617c5"
CRED = {"id": "9iJuyA9iR5KUzmj5", "name": "Experiments DB (PostgreSQL)"}
HEADS = {"X-N8N-API-KEY": KEY, "Content-Type": "application/json"}
def uid(): return str(uuid.uuid4())
# ── Node builders ──────────────────────────────────────────────────────────────
def trigger_node(x, y):
return {
"id": uid(), "name": "Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [x, y],
"parameters": {},
}
def switch_node(name, actions, x, y):
"""Route by $json.action — one output per action string."""
rules = []
for action in actions:
rules.append({
"conditions": {
"options": {"caseSensitive": False, "leftValue": "", "typeValidation": "loose"},
"combinator": "and",
"conditions": [{
"leftValue": "={{ $json.action }}",
"rightValue": action,
"operator": {"type": "string", "operation": "equals"},
}],
},
"renameOutput": True,
"outputKey": action,
})
return {
"id": uid(), "name": name,
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [x, y],
"parameters": {"rules": {"values": rules}, "options": {}},
}
def pg_node(name, query, params, x, y):
"""Postgres executeQuery node. params is a list of n8n expressions for $1,$2,..."""
node = {
"id": uid(), "name": name,
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [x, y],
"credentials": {"postgres": CRED},
"parameters": {
"operation": "executeQuery",
"query": query,
"options": {},
},
}
if params:
node["parameters"]["options"]["queryReplacement"] = "={{ [" + ", ".join(params) + "] }}"
return node
# ── Connection helpers ──────────────────────────────────────────────────────────
def conn(src, src_idx, dst):
"""Return a connections dict fragment."""
return (src["name"], src_idx, dst["name"])
def build_connections(pairs):
"""
pairs: list of (src_node, output_index, dst_node)
Returns n8n connections object.
"""
conns = {}
for src, idx, dst in pairs:
conns.setdefault(src, {"main": []})
while len(conns[src]["main"]) <= idx:
conns[src]["main"].append([])
conns[src]["main"][idx].append({"node": dst, "type": "main", "index": 0})
return conns
# ── Workflow factory ───────────────────────────────────────────────────────────
def make_workflow(name, trigger, switch, pg_nodes, connections):
return {
"name": name,
"nodes": [trigger, switch] + pg_nodes,
"connections": connections,
"settings": {"executionOrder": "v1"},
}
def post_workflow(wf):
r = requests.post(f"{API}/workflows", headers=HEADS, json=wf)
if r.ok:
d = r.json()
print(f" ✓ [{d['id']}] {wf['name']}")
else:
print(f"{wf['name']}{r.status_code} {r.text[:200]}")
# ── Layouts ────────────────────────────────────────────────────────────────────
# Nodes stacked vertically, 220px apart; Switch at col 300, pg nodes at col 560
def pg_y(i): return i * 220 - 440 # center the stack
# ══════════════════════════════════════════════════════════════════════════════
# 1. EXPERIMENTS
# ══════════════════════════════════════════════════════════════════════════════
def wf_experiments():
t = trigger_node(60, 0)
sw = switch_node("Route", ["list","get","create","update","delete"], 300, 0)
ops = [
pg_node("List Experiments",
"SELECT id, title, created_at FROM experiments ORDER BY created_at DESC",
[], 560, pg_y(0)),
pg_node("Get Experiment",
"SELECT * FROM experiments WHERE id = $1",
["$json.id"], 560, pg_y(1)),
pg_node("Create Experiment",
"INSERT INTO experiments (id, title, template, subject_info_template) "
"VALUES (gen_random_uuid(), $1, '[]'::jsonb, '[]'::jsonb) RETURNING *",
["$json.title"], 560, pg_y(2)),
pg_node("Update Experiment",
"UPDATE experiments SET title = $1 WHERE id = $2 RETURNING *",
["$json.title", "$json.id"], 560, pg_y(3)),
pg_node("Delete Experiment",
"DELETE FROM experiments WHERE id = $1 RETURNING id",
["$json.id"], 560, pg_y(4)),
]
pairs = [(t["name"], 0, sw["name"])] + \
[(sw["name"], i, ops[i]["name"]) for i in range(len(ops))]
return make_workflow("ExpDB · Experiments", t, sw, ops, build_connections(pairs))
# ══════════════════════════════════════════════════════════════════════════════
# 2. ANIMALS
# ══════════════════════════════════════════════════════════════════════════════
def wf_animals():
t = trigger_node(60, 0)
sw = switch_node("Route", ["list","get","create","update","delete"], 300, 0)
ops = [
pg_node("List Animals",
"SELECT a.*, COUNT(ds.id)::int AS daily_status_count "
"FROM animals a "
"LEFT JOIN daily_statuses ds ON ds.animal_id = a.id "
"WHERE a.experiment_id = $1 "
"GROUP BY a.id ORDER BY a.animal_name",
["$json.experiment_id"], 560, pg_y(0)),
pg_node("Get Animal",
"SELECT a.*, e.title AS experiment_title "
"FROM animals a JOIN experiments e ON e.id = a.experiment_id "
"WHERE a.id = $1",
["$json.id"], 560, pg_y(1)),
pg_node("Create Animal",
"INSERT INTO animals (id, experiment_id, animal_id_string, animal_name, subject_info) "
"VALUES (gen_random_uuid(), $1, $2, $3, $4::jsonb) RETURNING *",
["$json.experiment_id", "$json.animal_id_string", "$json.animal_name",
"JSON.stringify($json.subject_info ?? {})"], 560, pg_y(2)),
pg_node("Update Animal",
"UPDATE animals SET animal_id_string=$1, animal_name=$2, subject_info=$3::jsonb "
"WHERE id=$4 RETURNING *",
["$json.animal_id_string", "$json.animal_name",
"JSON.stringify($json.subject_info ?? {})", "$json.id"], 560, pg_y(3)),
pg_node("Delete Animal",
"DELETE FROM animals WHERE id = $1 RETURNING id",
["$json.id"], 560, pg_y(4)),
]
pairs = [(t["name"], 0, sw["name"])] + \
[(sw["name"], i, ops[i]["name"]) for i in range(len(ops))]
return make_workflow("ExpDB · Animals", t, sw, ops, build_connections(pairs))
# ══════════════════════════════════════════════════════════════════════════════
# 3. DAILY STATUSES
# ══════════════════════════════════════════════════════════════════════════════
def wf_daily_statuses():
t = trigger_node(60, 0)
sw = switch_node("Route", ["list","get","create","update","delete"], 300, 0)
ops = [
pg_node("List Daily Statuses",
"SELECT ds.id, ds.date, ds.experiment_description, ds.vitals, "
"ds.treatment, ds.notes, ds.custom_fields, ds.analysis_summary, "
"a.animal_name, a.animal_id_string "
"FROM daily_statuses ds JOIN animals a ON a.id = ds.animal_id "
"WHERE ds.animal_id = $1 ORDER BY ds.date DESC",
["$json.animal_id"], 560, pg_y(0)),
pg_node("Get Daily Status",
"SELECT ds.*, a.animal_name, a.animal_id_string, "
"a.experiment_id, e.title AS experiment_title "
"FROM daily_statuses ds "
"JOIN animals a ON a.id = ds.animal_id "
"JOIN experiments e ON e.id = a.experiment_id "
"WHERE ds.id = $1",
["$json.id"], 560, pg_y(1)),
pg_node("Create Daily Status",
"INSERT INTO daily_statuses "
"(id, animal_id, date, experiment_description, vitals, treatment, notes, custom_fields) "
"VALUES (gen_random_uuid(), $1, $2::date, $3, $4, $5, $6, $7::jsonb) RETURNING *",
["$json.animal_id", "$json.date", "$json.experiment_description ?? null",
"$json.vitals ?? null", "$json.treatment ?? null", "$json.notes ?? null",
"JSON.stringify($json.custom_fields ?? {})"], 560, pg_y(2)),
pg_node("Update Daily Status",
"UPDATE daily_statuses SET "
"experiment_description=$1, vitals=$2, treatment=$3, notes=$4, "
"custom_fields=$5::jsonb "
"WHERE id=$6 RETURNING *",
["$json.experiment_description ?? null", "$json.vitals ?? null",
"$json.treatment ?? null", "$json.notes ?? null",
"JSON.stringify($json.custom_fields ?? {})", "$json.id"], 560, pg_y(3)),
pg_node("Delete Daily Status",
"DELETE FROM daily_statuses WHERE id = $1 RETURNING id",
["$json.id"], 560, pg_y(4)),
]
pairs = [(t["name"], 0, sw["name"])] + \
[(sw["name"], i, ops[i]["name"]) for i in range(len(ops))]
return make_workflow("ExpDB · Daily Statuses", t, sw, ops, build_connections(pairs))
# ══════════════════════════════════════════════════════════════════════════════
# 4. DAILY ANALYSES (read + summary-push)
# ══════════════════════════════════════════════════════════════════════════════
def wf_daily_analyses():
t = trigger_node(60, 0)
sw = switch_node("Route", ["list","get","push_summary"], 300, 0)
ops = [
pg_node("List Analyses",
"SELECT id, daily_status_id, created_at, file_name, "
"timestamp_col, note_col, total_note_rows, session_end_ts "
"FROM daily_analyses WHERE daily_status_id = $1 ORDER BY created_at DESC",
["$json.daily_status_id"], 560, pg_y(0)),
pg_node("Get Analysis",
"SELECT * FROM daily_analyses WHERE id = $1",
["$json.id"], 560, pg_y(1)),
pg_node("Push Analysis Summary",
"UPDATE daily_statuses SET analysis_summary = $1::jsonb WHERE id = $2 RETURNING id, analysis_summary",
["JSON.stringify($json.analysis_summary)", "$json.daily_status_id"],
560, pg_y(2)),
]
pairs = [(t["name"], 0, sw["name"])] + \
[(sw["name"], i, ops[i]["name"]) for i in range(len(ops))]
return make_workflow("ExpDB · Daily Analyses", t, sw, ops, build_connections(pairs))
# ══════════════════════════════════════════════════════════════════════════════
# 5. SESSION FILES
# ══════════════════════════════════════════════════════════════════════════════
def wf_session_files():
t = trigger_node(60, 0)
sw = switch_node("Route", ["list","get"], 300, 0)
ops = [
pg_node("List Session Files",
"SELECT * FROM session_files WHERE daily_status_id = $1 ORDER BY created_at DESC",
["$json.daily_status_id"], 560, pg_y(0)),
pg_node("Get Session File",
"SELECT * FROM session_files WHERE id = $1",
["$json.id"], 560, pg_y(1)),
]
pairs = [(t["name"], 0, sw["name"])] + \
[(sw["name"], i, ops[i]["name"]) for i in range(len(ops))]
return make_workflow("ExpDB · Session Files", t, sw, ops, build_connections(pairs))
# ══════════════════════════════════════════════════════════════════════════════
# 6. AUDIT LOGS
# ══════════════════════════════════════════════════════════════════════════════
def wf_audit_logs():
t = trigger_node(60, 0)
sw = switch_node("Route", ["list_by_record","list_by_table"], 300, 0)
ops = [
pg_node("List Logs by Record",
"SELECT * FROM audit_logs "
"WHERE table_name = $1 AND record_id = $2 "
"ORDER BY timestamp DESC LIMIT $3",
["$json.table_name", "$json.record_id", "$json.limit ?? 50"],
560, pg_y(0)),
pg_node("List Logs by Table",
"SELECT * FROM audit_logs "
"WHERE table_name = $1 "
"ORDER BY timestamp DESC LIMIT $2",
["$json.table_name", "$json.limit ?? 100"],
560, pg_y(1)),
]
pairs = [(t["name"], 0, sw["name"])] + \
[(sw["name"], i, ops[i]["name"]) for i in range(len(ops))]
return make_workflow("ExpDB · Audit Logs", t, sw, ops, build_connections(pairs))
# ══════════════════════════════════════════════════════════════════════════════
# BONUS: Cross-subject report — one query to summarise all subjects in an experiment
# ══════════════════════════════════════════════════════════════════════════════
def wf_reports():
t = trigger_node(60, 0)
sw = switch_node("Route", ["subjects_summary","daily_summary","recent_activity"], 300, 0)
ops = [
pg_node("Subjects Summary",
"SELECT a.id, a.animal_name, a.animal_id_string, a.subject_info, "
"COUNT(ds.id)::int AS total_days, "
"MAX(ds.date) AS last_entry, "
"COUNT(ds.analysis_summary)::int AS days_with_metrics "
"FROM animals a "
"LEFT JOIN daily_statuses ds ON ds.animal_id = a.id "
"WHERE a.experiment_id = $1 "
"GROUP BY a.id ORDER BY a.animal_name",
["$json.experiment_id"], 560, pg_y(0)),
pg_node("Daily Summary",
"SELECT ds.date, a.animal_name, "
"ds.analysis_summary->>'total' AS total_attempts, "
"ds.analysis_summary->>'success_rate' AS success_rate "
"FROM daily_statuses ds JOIN animals a ON a.id = ds.animal_id "
"WHERE a.experiment_id = $1 AND ds.analysis_summary IS NOT NULL "
"ORDER BY ds.date, a.animal_name",
["$json.experiment_id"], 560, pg_y(1)),
pg_node("Recent Activity",
"SELECT al.timestamp, al.table_name, al.action, al.record_id, al.changes "
"FROM audit_logs al "
"ORDER BY al.timestamp DESC LIMIT $1",
["$json.limit ?? 50"], 560, pg_y(2)),
]
pairs = [(t["name"], 0, sw["name"])] + \
[(sw["name"], i, ops[i]["name"]) for i in range(len(ops))]
return make_workflow("ExpDB · Reports", t, sw, ops, build_connections(pairs))
# ── Main ───────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
workflows = [
wf_experiments(),
wf_animals(),
wf_daily_statuses(),
wf_daily_analyses(),
wf_session_files(),
wf_audit_logs(),
wf_reports(),
]
print(f"Creating {len(workflows)} workflows…\n")
for wf in workflows:
post_workflow(wf)
print("\nDone.")
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# =============================================================================
# Experiments DB — incremental backup script
#
# Usage:
# backup.sh daily backup only if DB changed since last run; keep 5
# backup.sh weekly unconditional backup; keep 2
#
# Cron (added by setup_cron.sh):
# 0 2 * * * /home/sam/docker-images/experiments-database/scripts/backup.sh daily
# 0 3 * * 0 /home/sam/docker-images/experiments-database/scripts/backup.sh weekly
# =============================================================================
set -euo pipefail
# ── Config ────────────────────────────────────────────────────────────────────
MODE="${1:-daily}"
BACKUP_ROOT="/home/sam/synology/Backups/Experiments-DB-Backup"
DAILY_DIR="$BACKUP_ROOT/daily"
WEEKLY_DIR="$BACKUP_ROOT/weekly"
STATE_FILE="$BACKUP_ROOT/.last_backup_state"
LOG_FILE="$BACKUP_ROOT/backup.log"
KEEP_DAILY=5
KEEP_WEEKLY=2
PG_CONTAINER="experiments_postgres"
PG_USER="expuser"
PG_DB="experiments_db"
# ── Helpers ───────────────────────────────────────────────────────────────────
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
die() { log "ERROR: $*"; exit 1; }
# Ensure directories exist
mkdir -p "$DAILY_DIR" "$WEEKLY_DIR"
# Check postgres container is running
if ! docker inspect --format '{{.State.Running}}' "$PG_CONTAINER" 2>/dev/null | grep -q true; then
die "Container $PG_CONTAINER is not running."
fi
# ── DB state fingerprint ──────────────────────────────────────────────────────
# Combines row counts from all key tables + latest audit_log timestamp.
# Changes in any table (insert, update, delete) alter this string.
get_db_state() {
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d "$PG_DB" -t -A -c "
SELECT
(SELECT COUNT(*) FROM experiments) || ',' ||
(SELECT COUNT(*) FROM animals) || ',' ||
(SELECT COUNT(*) FROM daily_statuses) || ',' ||
(SELECT COUNT(*) FROM daily_analyses) || ',' ||
(SELECT COUNT(*) FROM session_files) || ',' ||
COALESCE(
(SELECT MAX(timestamp)::text FROM audit_logs),
'no-audit'
)
AS fingerprint;
" 2>/dev/null | tr -d '[:space:]'
}
# ── Dump function ─────────────────────────────────────────────────────────────
run_dump() {
local dest="$1"
local tmp="${dest}.tmp"
log "Dumping → $dest"
docker exec "$PG_CONTAINER" pg_dump \
-U "$PG_USER" \
--format=custom \
--compress=9 \
"$PG_DB" > "$tmp" \
&& mv "$tmp" "$dest" \
|| { rm -f "$tmp"; die "pg_dump failed."; }
local size
size=$(du -sh "$dest" | cut -f1)
log "Backup written ($size): $(basename "$dest")"
}
# ── Prune old backups ─────────────────────────────────────────────────────────
prune() {
local dir="$1"
local keep="$2"
local count
count=$(ls -1 "$dir"/*.pgdump 2>/dev/null | wc -l)
if (( count > keep )); then
ls -1t "$dir"/*.pgdump | tail -n "+$((keep + 1))" | while read -r f; do
log "Pruning old backup: $(basename "$f")"
rm -f "$f"
done
fi
}
# ── Main ──────────────────────────────────────────────────────────────────────
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
if [[ "$MODE" == "daily" ]]; then
log "--- Daily backup check ---"
CURRENT_STATE=$(get_db_state)
if [[ -z "$CURRENT_STATE" ]]; then
die "Could not read DB state."
fi
LAST_STATE=$(cat "$STATE_FILE" 2>/dev/null || echo "")
if [[ "$CURRENT_STATE" == "$LAST_STATE" ]]; then
log "No changes detected since last backup. Skipping."
exit 0
fi
DEST="$DAILY_DIR/experiments_db_daily_${TIMESTAMP}.pgdump"
run_dump "$DEST"
# Save new state only after successful dump
echo "$CURRENT_STATE" > "$STATE_FILE"
log "State fingerprint updated."
prune "$DAILY_DIR" "$KEEP_DAILY"
log "Daily backup complete."
elif [[ "$MODE" == "weekly" ]]; then
log "--- Weekly backup (unconditional) ---"
DEST="$WEEKLY_DIR/experiments_db_weekly_${TIMESTAMP}.pgdump"
run_dump "$DEST"
prune "$WEEKLY_DIR" "$KEEP_WEEKLY"
log "Weekly backup complete."
else
die "Unknown mode '$MODE'. Use 'daily' or 'weekly'."
fi
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# =============================================================================
# Installs (or updates) cron entries for the experiments DB backup.
# Safe to run multiple times — replaces existing ExpDB entries.
# =============================================================================
SCRIPT="$(cd "$(dirname "$0")" && pwd)/backup.sh"
if [[ ! -x "$SCRIPT" ]]; then
chmod +x "$SCRIPT"
fi
# Remove any previous ExpDB backup lines, then append fresh ones
TMPFILE=$(mktemp)
crontab -l 2>/dev/null | grep -v "# ExpDB-backup" > "$TMPFILE" || true
cat >> "$TMPFILE" <<EOF
# ExpDB-backup — daily change-detect backup at 02:00
0 2 * * * $SCRIPT daily >> /home/sam/synology/Backups/Experiments-DB-Backup/backup.log 2>&1 # ExpDB-backup
# ExpDB-backup — weekly unconditional backup every Sunday at 03:00
0 3 * * 0 $SCRIPT weekly >> /home/sam/synology/Backups/Experiments-DB-Backup/backup.log 2>&1 # ExpDB-backup
EOF
crontab "$TMPFILE"
rm -f "$TMPFILE"
echo "Cron entries installed:"
crontab -l | grep "ExpDB-backup"