tpattern 0.1.11__tar.gz → 0.1.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tpattern-0.1.11/tpattern.egg-info → tpattern-0.1.13}/PKG-INFO +71 -7
- {tpattern-0.1.11 → tpattern-0.1.13}/README.md +70 -6
- {tpattern-0.1.11 → tpattern-0.1.13}/pyproject.toml +1 -1
- tpattern-0.1.13/tests/test_reference.py +54 -0
- tpattern-0.1.13/tests/test_starter.py +34 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/__init__.py +6 -1
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/guided.py +2 -2
- tpattern-0.1.13/tpattern/reference.py +214 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/report.py +11 -7
- tpattern-0.1.13/tpattern/starter.py +147 -0
- {tpattern-0.1.11 → tpattern-0.1.13/tpattern.egg-info}/PKG-INFO +71 -7
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern.egg-info/SOURCES.txt +4 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/LICENSE +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/setup.cfg +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_calibrated_pins.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_contrast.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_dedup.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_guided.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_null_compare.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_outputs.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_read_table_validation.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_report_dendrogram.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_reproduction.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_sofcoder.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_support_reporting.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tests/test_synthetic.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/advisor.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/ci.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/contrast.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/detect.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/diagnostics.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/io.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/methods.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/pattern.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/randomise.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/significance.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/synthetic.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern/viz.py +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern.egg-info/dependency_links.txt +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern.egg-info/requires.txt +0 -0
- {tpattern-0.1.11 → tpattern-0.1.13}/tpattern.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tpattern
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
4
4
|
Summary: Open, auditable temporal & sequential pattern analysis (T-patterns) for sport and behaviour
|
|
5
5
|
Author-email: Andrew Callaway <acallaway@bournemouth.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -54,6 +54,60 @@ analysis companion to the [OpenTag.Studio](https://opentag.studio) tagging tool.
|
|
|
54
54
|
Bring your own data (one row per event — see [`SCHEMA.md`](SCHEMA.md)) or try the
|
|
55
55
|
included [`data/`](data/) example.
|
|
56
56
|
|
|
57
|
+
## Getting started in a minute
|
|
58
|
+
|
|
59
|
+
Install, generate a starter script, point it at your data, run it:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install tpattern
|
|
63
|
+
python -c "import tpattern; tpattern.new_analysis()" # writes my_analysis.py
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Open `my_analysis.py`, change the two lines marked `# <-- EDIT` (your file and its time
|
|
67
|
+
unit), and run `python my_analysis.py`. It loads your events, **checks the file and tells
|
|
68
|
+
you in plain language about any problem**, recommends the analysis settings with reasons,
|
|
69
|
+
calibrates every pattern against a chance model, and writes a folder of results with a
|
|
70
|
+
plain-English verdict for each pattern.
|
|
71
|
+
|
|
72
|
+
Doing something more specific? Generate a template for it:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from tpattern import new_analysis
|
|
76
|
+
new_analysis() # one group: detect, calibrate, report
|
|
77
|
+
new_analysis(template="two_groups") # compare two groups (odds ratios + forest plot)
|
|
78
|
+
new_analysis(template="null_choice") # core vs shell: which null a pattern survives
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Not sure what a setting means? Ask the code — every setting and output column is documented:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from tpattern import explain
|
|
85
|
+
print(explain("min_lag")) # what it does, its range, the recommended value, higher vs lower
|
|
86
|
+
print(explain("fdr_q")) # what an output column means
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What you get
|
|
90
|
+
|
|
91
|
+
- **Forgiving data import.** `read_table` checks your file as it reads it and reports, in
|
|
92
|
+
plain language, any problem, how much of the file it affects, and how to fix it — instead
|
|
93
|
+
of silently dropping rows. Each check has a stable code (Appendix A of the tutorial; also
|
|
94
|
+
[`PARAMETERS.md`](PARAMETERS.md)).
|
|
95
|
+
- **A proper chance model.** Every pattern is tested against a surrogate null, with p-values
|
|
96
|
+
corrected for repeated testing (Benjamini–Hochberg FDR for screening, Holm family-wise for
|
|
97
|
+
confirmatory claims), stratified by hierarchy level.
|
|
98
|
+
- **Temporal reporting.** Beyond *which* patterns are real, the output says *when*: each
|
|
99
|
+
pattern's occurrence timing (median lag and spread) is reported by default — on the
|
|
100
|
+
dendrogram, in the table, and in the plain-English verdict — for every pattern.
|
|
101
|
+
- **Readable output.** Results-table columns are named for what they are (`monte_carlo_p`,
|
|
102
|
+
`fdr_q`, `survives_fwer_holm`, `bout_support`, `duration_median_ms`), and each pattern
|
|
103
|
+
carries a plain-English `interpretation`. `explain()` documents every setting and column;
|
|
104
|
+
the full reference is in [`PARAMETERS.md`](PARAMETERS.md).
|
|
105
|
+
- **One reference, everywhere.** Settings and columns live in one registry that generates the
|
|
106
|
+
code's `explain()`, `PARAMETERS.md`, the Colab form labels, and the tutorial's tables — so
|
|
107
|
+
the software, the paper and the notebook can't drift apart.
|
|
108
|
+
- **Clean figures** — dendrograms, temporal signatures, forest plots — that share one
|
|
109
|
+
customisable style (`tpattern.viz.STYLE`).
|
|
110
|
+
|
|
57
111
|
## Why it exists
|
|
58
112
|
|
|
59
113
|
T-pattern analysis finds behaviourally meaningful structure that event counts and
|
|
@@ -62,7 +116,7 @@ software, the reference implementation. `tpattern` is an open, complementary
|
|
|
62
116
|
implementation that reproduces THEME's detection and adds a reproducible chance
|
|
63
117
|
model and clear reporting:
|
|
64
118
|
|
|
65
|
-
- **reproduces** THEME's core detection (critical intervals,
|
|
119
|
+
- **reproduces** THEME's core detection (critical intervals, NX/T baseline,
|
|
66
120
|
completeness competition) — validated to the pattern for published datasets;
|
|
67
121
|
- **adds a chance model** — every pattern is tested against a surrogate null, with
|
|
68
122
|
p-values **corrected for repeated testing** (false-discovery rate and
|
|
@@ -109,12 +163,18 @@ print(recommend(observations))
|
|
|
109
163
|
result = calibrate(observations, Config(), null="profile", B=200, q_target=0.05)
|
|
110
164
|
survivors = result.kept("fdr") # patterns that survive false-discovery control
|
|
111
165
|
|
|
112
|
-
# 4. Report: a tidy table (
|
|
113
|
-
#
|
|
114
|
-
|
|
166
|
+
# 4. Report: a tidy results table (monte_carlo_p, fdr_q, survives_fwer_holm, the observed
|
|
167
|
+
# timing, and a plain-English `interpretation` per pattern), pattern dendrograms, the
|
|
168
|
+
# temporal-signature and occurrence plots, the event-type roles/connections tables, and a
|
|
169
|
+
# paste-ready Methods paragraph stating every setting and what it does.
|
|
170
|
+
report(result, "report_out", observations=observations, title="My analysis")
|
|
115
171
|
print(methods_text(Config(), observations=observations, calibration=result))
|
|
116
172
|
```
|
|
117
173
|
|
|
174
|
+
Prefer not to write the pipeline yourself? `python -c "import tpattern;
|
|
175
|
+
tpattern.new_analysis()"` writes a commented [`my_analysis.py`](examples/my_analysis.py)
|
|
176
|
+
you edit and run (see [Getting started](#getting-started-in-a-minute)).
|
|
177
|
+
|
|
118
178
|
Two runnable examples:
|
|
119
179
|
|
|
120
180
|
```bash
|
|
@@ -128,7 +188,8 @@ Config()).detect()` returns them directly.
|
|
|
128
188
|
|
|
129
189
|
**Comparing two groups** (winner vs loser, goal vs non-goal, condition A vs B)?
|
|
130
190
|
`group_contrast(observations, group_of)` tests each pattern's occurrence between
|
|
131
|
-
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot
|
|
191
|
+
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot` — or run
|
|
192
|
+
`new_analysis(template="two_groups")` for a ready-made script.
|
|
132
193
|
|
|
133
194
|
## The workflow
|
|
134
195
|
|
|
@@ -179,7 +240,10 @@ survive the surrogate null. All seeded and deterministic. See
|
|
|
179
240
|
- [`SCHEMA.md`](SCHEMA.md) — the input data structure (what your export must contain).
|
|
180
241
|
- [`CODING_GUIDE.md`](CODING_GUIDE.md) — how to set up a coding window to produce it.
|
|
181
242
|
- [`data/`](data/) — the derived World Cup sequences, in the canonical format.
|
|
182
|
-
- [`examples/`](examples/) — a runnable quickstart
|
|
243
|
+
- [`examples/`](examples/) — a runnable quickstart, the reproduction script, and the
|
|
244
|
+
`my_analysis.py` / `my_group_comparison.py` / `my_null_choice.py` starter templates.
|
|
245
|
+
- [`PARAMETERS.md`](PARAMETERS.md) — every setting and output column, with range, recommended
|
|
246
|
+
value, and what higher/lower does (generated from the code, so it never drifts).
|
|
183
247
|
|
|
184
248
|
## Tests
|
|
185
249
|
|
|
@@ -19,6 +19,60 @@ analysis companion to the [OpenTag.Studio](https://opentag.studio) tagging tool.
|
|
|
19
19
|
Bring your own data (one row per event — see [`SCHEMA.md`](SCHEMA.md)) or try the
|
|
20
20
|
included [`data/`](data/) example.
|
|
21
21
|
|
|
22
|
+
## Getting started in a minute
|
|
23
|
+
|
|
24
|
+
Install, generate a starter script, point it at your data, run it:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install tpattern
|
|
28
|
+
python -c "import tpattern; tpattern.new_analysis()" # writes my_analysis.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Open `my_analysis.py`, change the two lines marked `# <-- EDIT` (your file and its time
|
|
32
|
+
unit), and run `python my_analysis.py`. It loads your events, **checks the file and tells
|
|
33
|
+
you in plain language about any problem**, recommends the analysis settings with reasons,
|
|
34
|
+
calibrates every pattern against a chance model, and writes a folder of results with a
|
|
35
|
+
plain-English verdict for each pattern.
|
|
36
|
+
|
|
37
|
+
Doing something more specific? Generate a template for it:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from tpattern import new_analysis
|
|
41
|
+
new_analysis() # one group: detect, calibrate, report
|
|
42
|
+
new_analysis(template="two_groups") # compare two groups (odds ratios + forest plot)
|
|
43
|
+
new_analysis(template="null_choice") # core vs shell: which null a pattern survives
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Not sure what a setting means? Ask the code — every setting and output column is documented:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from tpattern import explain
|
|
50
|
+
print(explain("min_lag")) # what it does, its range, the recommended value, higher vs lower
|
|
51
|
+
print(explain("fdr_q")) # what an output column means
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What you get
|
|
55
|
+
|
|
56
|
+
- **Forgiving data import.** `read_table` checks your file as it reads it and reports, in
|
|
57
|
+
plain language, any problem, how much of the file it affects, and how to fix it — instead
|
|
58
|
+
of silently dropping rows. Each check has a stable code (Appendix A of the tutorial; also
|
|
59
|
+
[`PARAMETERS.md`](PARAMETERS.md)).
|
|
60
|
+
- **A proper chance model.** Every pattern is tested against a surrogate null, with p-values
|
|
61
|
+
corrected for repeated testing (Benjamini–Hochberg FDR for screening, Holm family-wise for
|
|
62
|
+
confirmatory claims), stratified by hierarchy level.
|
|
63
|
+
- **Temporal reporting.** Beyond *which* patterns are real, the output says *when*: each
|
|
64
|
+
pattern's occurrence timing (median lag and spread) is reported by default — on the
|
|
65
|
+
dendrogram, in the table, and in the plain-English verdict — for every pattern.
|
|
66
|
+
- **Readable output.** Results-table columns are named for what they are (`monte_carlo_p`,
|
|
67
|
+
`fdr_q`, `survives_fwer_holm`, `bout_support`, `duration_median_ms`), and each pattern
|
|
68
|
+
carries a plain-English `interpretation`. `explain()` documents every setting and column;
|
|
69
|
+
the full reference is in [`PARAMETERS.md`](PARAMETERS.md).
|
|
70
|
+
- **One reference, everywhere.** Settings and columns live in one registry that generates the
|
|
71
|
+
code's `explain()`, `PARAMETERS.md`, the Colab form labels, and the tutorial's tables — so
|
|
72
|
+
the software, the paper and the notebook can't drift apart.
|
|
73
|
+
- **Clean figures** — dendrograms, temporal signatures, forest plots — that share one
|
|
74
|
+
customisable style (`tpattern.viz.STYLE`).
|
|
75
|
+
|
|
22
76
|
## Why it exists
|
|
23
77
|
|
|
24
78
|
T-pattern analysis finds behaviourally meaningful structure that event counts and
|
|
@@ -27,7 +81,7 @@ software, the reference implementation. `tpattern` is an open, complementary
|
|
|
27
81
|
implementation that reproduces THEME's detection and adds a reproducible chance
|
|
28
82
|
model and clear reporting:
|
|
29
83
|
|
|
30
|
-
- **reproduces** THEME's core detection (critical intervals,
|
|
84
|
+
- **reproduces** THEME's core detection (critical intervals, NX/T baseline,
|
|
31
85
|
completeness competition) — validated to the pattern for published datasets;
|
|
32
86
|
- **adds a chance model** — every pattern is tested against a surrogate null, with
|
|
33
87
|
p-values **corrected for repeated testing** (false-discovery rate and
|
|
@@ -74,12 +128,18 @@ print(recommend(observations))
|
|
|
74
128
|
result = calibrate(observations, Config(), null="profile", B=200, q_target=0.05)
|
|
75
129
|
survivors = result.kept("fdr") # patterns that survive false-discovery control
|
|
76
130
|
|
|
77
|
-
# 4. Report: a tidy table (
|
|
78
|
-
#
|
|
79
|
-
|
|
131
|
+
# 4. Report: a tidy results table (monte_carlo_p, fdr_q, survives_fwer_holm, the observed
|
|
132
|
+
# timing, and a plain-English `interpretation` per pattern), pattern dendrograms, the
|
|
133
|
+
# temporal-signature and occurrence plots, the event-type roles/connections tables, and a
|
|
134
|
+
# paste-ready Methods paragraph stating every setting and what it does.
|
|
135
|
+
report(result, "report_out", observations=observations, title="My analysis")
|
|
80
136
|
print(methods_text(Config(), observations=observations, calibration=result))
|
|
81
137
|
```
|
|
82
138
|
|
|
139
|
+
Prefer not to write the pipeline yourself? `python -c "import tpattern;
|
|
140
|
+
tpattern.new_analysis()"` writes a commented [`my_analysis.py`](examples/my_analysis.py)
|
|
141
|
+
you edit and run (see [Getting started](#getting-started-in-a-minute)).
|
|
142
|
+
|
|
83
143
|
Two runnable examples:
|
|
84
144
|
|
|
85
145
|
```bash
|
|
@@ -93,7 +153,8 @@ Config()).detect()` returns them directly.
|
|
|
93
153
|
|
|
94
154
|
**Comparing two groups** (winner vs loser, goal vs non-goal, condition A vs B)?
|
|
95
155
|
`group_contrast(observations, group_of)` tests each pattern's occurrence between
|
|
96
|
-
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot
|
|
156
|
+
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot` — or run
|
|
157
|
+
`new_analysis(template="two_groups")` for a ready-made script.
|
|
97
158
|
|
|
98
159
|
## The workflow
|
|
99
160
|
|
|
@@ -144,7 +205,10 @@ survive the surrogate null. All seeded and deterministic. See
|
|
|
144
205
|
- [`SCHEMA.md`](SCHEMA.md) — the input data structure (what your export must contain).
|
|
145
206
|
- [`CODING_GUIDE.md`](CODING_GUIDE.md) — how to set up a coding window to produce it.
|
|
146
207
|
- [`data/`](data/) — the derived World Cup sequences, in the canonical format.
|
|
147
|
-
- [`examples/`](examples/) — a runnable quickstart
|
|
208
|
+
- [`examples/`](examples/) — a runnable quickstart, the reproduction script, and the
|
|
209
|
+
`my_analysis.py` / `my_group_comparison.py` / `my_null_choice.py` starter templates.
|
|
210
|
+
- [`PARAMETERS.md`](PARAMETERS.md) — every setting and output column, with range, recommended
|
|
211
|
+
value, and what higher/lower does (generated from the code, so it never drifts).
|
|
148
212
|
|
|
149
213
|
## Tests
|
|
150
214
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tpattern"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.13"
|
|
8
8
|
description = "Open, auditable temporal & sequential pattern analysis (T-patterns) for sport and behaviour"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""The reference registry: coverage, explain(), and the renamed output columns."""
|
|
2
|
+
from tpattern import (SETTINGS, COLUMNS, explain, explain_settings, explain_columns,
|
|
3
|
+
parameters_markdown, read_table, Config, calibrate,
|
|
4
|
+
patterns_table, Observation)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_every_setting_is_complete():
|
|
8
|
+
for s in SETTINGS:
|
|
9
|
+
for field in ("key", "form", "group", "does", "range", "recommend", "effect"):
|
|
10
|
+
assert s.get(field), f"{s.get('form')} missing {field}"
|
|
11
|
+
assert s["group"] in ("A", "B", "C")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_explain_by_key_and_form_name():
|
|
15
|
+
a = explain("min_lag")
|
|
16
|
+
b = explain("minimum_lag_frames")
|
|
17
|
+
assert a == b and "recommended" in a and "range" in a
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_explain_column_and_unknown():
|
|
21
|
+
assert "Benjamini" in explain("fdr_q")
|
|
22
|
+
assert "No setting or column" in explain("does_not_exist")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parameters_markdown_covers_all():
|
|
26
|
+
md = parameters_markdown()
|
|
27
|
+
for s in SETTINGS:
|
|
28
|
+
assert s["form"] in md
|
|
29
|
+
for c in COLUMNS:
|
|
30
|
+
assert c["key"] in md
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sample():
|
|
34
|
+
obs = []
|
|
35
|
+
for i in range(6):
|
|
36
|
+
ev = [(0, "A"), (300, "B")] + ([(600, "C")] if i < 3 else [])
|
|
37
|
+
obs.append(Observation(name=f"o{i}", start=0, end=1000, events=ev))
|
|
38
|
+
return obs
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_output_columns_renamed():
|
|
42
|
+
res = calibrate(_sample(), Config(min_lag=1), null="profile", B=50, q_target=.05, seed=1)
|
|
43
|
+
row = patterns_table(res)[0]
|
|
44
|
+
assert "monte_carlo_p" in row and "survives_fwer_holm" in row
|
|
45
|
+
assert "p_emp" not in row and "fwer_keep" not in row
|
|
46
|
+
# every reported column has a registry entry (the tutorial table generates from it)
|
|
47
|
+
keys = {c["key"] for c in COLUMNS}
|
|
48
|
+
for col in ("monte_carlo_p", "fdr_q", "survives_fwer_holm", "N", "level", "bout_support"):
|
|
49
|
+
assert col in keys
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_sort_tolerates_old_name():
|
|
53
|
+
res = calibrate(_sample(), Config(min_lag=1), null="profile", B=50, q_target=.05, seed=1)
|
|
54
|
+
patterns_table(res, sort="p_emp") # must not raise
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""The copy-and-adapt analysis template generator and its variants."""
|
|
2
|
+
import ast, pytest
|
|
3
|
+
from tpattern import new_analysis
|
|
4
|
+
from tpattern.starter import TEMPLATES
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_all_templates_are_valid_python():
|
|
8
|
+
assert set(TEMPLATES) == {"basic", "two_groups", "null_choice"}
|
|
9
|
+
for name, src in TEMPLATES.items():
|
|
10
|
+
ast.parse(src) # each generated script is valid Python
|
|
11
|
+
assert "# <-- EDIT" in src and "read_table" in src
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_new_analysis_default_and_named(tmp_path):
|
|
15
|
+
p = new_analysis(tmp_path / "a.py")
|
|
16
|
+
assert "recommend" in open(p).read() and "calibrate" in open(p).read()
|
|
17
|
+
g = new_analysis(tmp_path / "g.py", template="two_groups")
|
|
18
|
+
assert "group_contrast" in open(g).read() and "forest_plot" in open(g).read()
|
|
19
|
+
n = new_analysis(tmp_path / "n.py", template="null_choice")
|
|
20
|
+
assert "compare_nulls" in open(n).read()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_overwrite_guard_and_bad_name(tmp_path):
|
|
24
|
+
f = tmp_path / "a.py"
|
|
25
|
+
new_analysis(f)
|
|
26
|
+
with pytest.raises(FileExistsError):
|
|
27
|
+
new_analysis(f)
|
|
28
|
+
new_analysis(f, overwrite=True)
|
|
29
|
+
with pytest.raises(ValueError):
|
|
30
|
+
new_analysis(tmp_path / "x.py", template="nope")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_basic_template_uses_readable_columns():
|
|
34
|
+
assert "monte_carlo_p" in TEMPLATES["basic"] and "survives_fwer_holm" in TEMPLATES["basic"]
|
|
@@ -6,7 +6,7 @@ behavior: T-patterns and their detection. Behavior Research Methods,
|
|
|
6
6
|
Instruments, & Computers, 32(1), 93-110.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.1.
|
|
9
|
+
__version__ = "0.1.13" # defined before submodule imports (methods.py reads it)
|
|
10
10
|
|
|
11
11
|
from .io import (Observation, read_observation, read_sample, read_table,
|
|
12
12
|
read_sofcoder, DataFormatError, check_reference, CHECKS)
|
|
@@ -24,6 +24,9 @@ from .advisor import recommend
|
|
|
24
24
|
from .methods import methods_text
|
|
25
25
|
from .guided import run_analysis, launch
|
|
26
26
|
from .contrast import group_contrast, contrast_items
|
|
27
|
+
from .reference import (explain, explain_settings, explain_columns,
|
|
28
|
+
parameters_markdown, SETTINGS, COLUMNS)
|
|
29
|
+
from .starter import new_analysis, TEMPLATE
|
|
27
30
|
|
|
28
31
|
__all__ = [
|
|
29
32
|
"Observation", "read_observation", "read_sample", "read_table", "read_sofcoder",
|
|
@@ -40,4 +43,6 @@ __all__ = [
|
|
|
40
43
|
"recommend", "methods_text",
|
|
41
44
|
"run_analysis", "launch",
|
|
42
45
|
"group_contrast", "contrast_items",
|
|
46
|
+
"explain", "explain_settings", "explain_columns", "parameters_markdown",
|
|
47
|
+
"SETTINGS", "COLUMNS", "new_analysis",
|
|
43
48
|
]
|
|
@@ -104,8 +104,8 @@ def _write_excel(result, q_target, path):
|
|
|
104
104
|
rows.append({
|
|
105
105
|
"pattern": str(c.pattern), "N": c.N, "level": c.level,
|
|
106
106
|
"critical_interval": f"[{c.ci[0]},{c.ci[1]}]" if c.ci else "",
|
|
107
|
-
"
|
|
108
|
-
"
|
|
107
|
+
"monte_carlo_p": round(c.p_emp, 4), "fdr_q": round(c.fdr_q, 4),
|
|
108
|
+
"survives_fdr": int(c.fdr_q <= q_target), "survives_fwer_holm": int(c.fwer_keep),
|
|
109
109
|
})
|
|
110
110
|
try:
|
|
111
111
|
import pandas as pd
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
One reference for every setting and every output column.
|
|
3
|
+
=========================================================
|
|
4
|
+
|
|
5
|
+
The problem this solves: a user meets these names in three places — the Python
|
|
6
|
+
`Config`, the Colab settings form, and the results table — and needs, for each, the
|
|
7
|
+
same four things: what it *is*, what range it takes, what value is *recommended*, and
|
|
8
|
+
what *higher or lower* does to the analysis. Rather than scatter that across
|
|
9
|
+
docstrings, a paper table and the advisor, it lives here once. Everything else is
|
|
10
|
+
generated from it: :func:`explain`, the ``PARAMETERS.md`` reference, the tutorial's
|
|
11
|
+
settings and output-column tables, and the plain-language field labels in the Colab
|
|
12
|
+
form. So the code, the paper and the notebook cannot drift.
|
|
13
|
+
|
|
14
|
+
Each setting record has:
|
|
15
|
+
key the ``Config`` (or read_table/calibrate) argument name
|
|
16
|
+
form the friendly name shown in the Colab form and the paper
|
|
17
|
+
group A = the data can decide it (the advisor recommends);
|
|
18
|
+
B = your policy (the data cannot decide it);
|
|
19
|
+
C = a structural input fixed before analysis
|
|
20
|
+
does what it does to the detection, in one line
|
|
21
|
+
range the values it accepts
|
|
22
|
+
recommend the recommended value, and when
|
|
23
|
+
effect what higher / lower (or on / off) means
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
# --- settings ---------------------------------------------------------------
|
|
29
|
+
SETTINGS: list[dict] = [
|
|
30
|
+
# A. the data can decide these (the advisor recommends them)
|
|
31
|
+
dict(key="null", form="surrogate_null", group="A",
|
|
32
|
+
does="Which chance model each detected pattern is tested against.",
|
|
33
|
+
range="profile, rotation or shuffle.",
|
|
34
|
+
recommend="profile when event types have their own timing (the advisor decides this from the conditional-uniformity check).",
|
|
35
|
+
effect="profile tests coupling beyond each type's own timing; rotation and shuffle test whether any temporal structure is present at all."),
|
|
36
|
+
dict(key="min_lag", form="minimum_lag_frames", group="A",
|
|
37
|
+
does="The smallest genuine time gap required between two linked events.",
|
|
38
|
+
range="a whole number of time units, 0 or more.",
|
|
39
|
+
recommend="1 when many events share a timestamp (the advisor recommends it when more than ~10% of neighbouring events are co-timed); 0 to match THEME.",
|
|
40
|
+
effect="higher treats more co-timed pairs as co-occurrence, not sequence; 0 lets co-timed events count as ordered (THEME-compatible, but sub-frame order is undefined)."),
|
|
41
|
+
dict(key="freq_exclude", form="frequent_event_exclusion", group="A",
|
|
42
|
+
does="Sets aside an over-common event type from building patterns (it is still counted in the baseline).",
|
|
43
|
+
range="a positive number (mean occurrences per observation, under the default rule); off if empty.",
|
|
44
|
+
recommend="the advisor flags a type averaging more than ~1.5 occurrences per observation.",
|
|
45
|
+
effect="lower excludes more (rarer) types; off keeps every type eligible and can flood the results with 'this common event, then anything'."),
|
|
46
|
+
dict(key="min_occurrence", form="minimum_occurrence", group="A",
|
|
47
|
+
does="How many times a pattern must recur before it is tested.",
|
|
48
|
+
range="a whole number, 2 or more (3 is the usual floor).",
|
|
49
|
+
recommend="3, the floor at which the surrogate test can be calibrated.",
|
|
50
|
+
effect="higher keeps only more frequent patterns; below 3 the chance test cannot resolve."),
|
|
51
|
+
dict(key="error_control", form="error_control", group="A",
|
|
52
|
+
does="Which multiple-testing correction is treated as primary.",
|
|
53
|
+
range="fdr or fwer.",
|
|
54
|
+
recommend="fdr to screen many patterns; fwer for a single confirmatory claim.",
|
|
55
|
+
effect="fdr controls the expected share of false discoveries; fwer controls any false positive at all (stricter, and needs a large B)."),
|
|
56
|
+
dict(key="B", form="surrogates_B", group="A",
|
|
57
|
+
does="How many surrogate datasets are drawn for the Monte-Carlo p-value.",
|
|
58
|
+
range="a whole number, typically hundreds to thousands.",
|
|
59
|
+
recommend="200 to screen; 2000 or more for a confirmatory or family-wise claim (the smallest possible p is 1/(B+1)).",
|
|
60
|
+
effect="higher gives a finer p-value and more confidence, at more compute."),
|
|
61
|
+
# B. your policy (the data cannot decide these)
|
|
62
|
+
dict(key="alpha", form="detection_alpha", group="B",
|
|
63
|
+
does="The critical-interval acceptance threshold: a screening gate deciding which candidates are detected, not the basis of inference.",
|
|
64
|
+
range="between 0 and 1, small (e.g. .005 to .05).",
|
|
65
|
+
recommend=".005, a convention in sport T-pattern work; the surrogate calibration, not this threshold, is what a claim rests on.",
|
|
66
|
+
effect="a looser value lets more candidates through to calibration, but does not change which patterns survive it."),
|
|
67
|
+
dict(key="q_target", form="false_discovery_q", group="B",
|
|
68
|
+
does="The false-discovery-rate threshold a pattern's q-value must clear.",
|
|
69
|
+
range="between 0 and 1 (e.g. .05).",
|
|
70
|
+
recommend=".05.",
|
|
71
|
+
effect="lower is stricter (fewer patterns reported)."),
|
|
72
|
+
dict(key="alpha_fwer", form="family_wise_alpha", group="B",
|
|
73
|
+
does="The family-wise error rate used for confirmatory claims (Holm).",
|
|
74
|
+
range="between 0 and 1 (e.g. .005).",
|
|
75
|
+
recommend=".005; a family-wise claim needs B in the thousands to resolve.",
|
|
76
|
+
effect="lower is stricter; too small a B cannot reach it."),
|
|
77
|
+
dict(key="min_samples_frac", form="bout_support_percent", group="B",
|
|
78
|
+
does="The minimum fraction of observations a pattern must appear in to be kept.",
|
|
79
|
+
range="0 to 100 percent; 0 is off.",
|
|
80
|
+
recommend="off. Report prevalence rather than filtering on it (Section 4.1 shows why a threshold cuts along the wrong axis).",
|
|
81
|
+
effect="raising it deletes rare-but-genuine patterns, including well-evidenced ones specific to a subgroup."),
|
|
82
|
+
dict(key="lumping_factor", form="lumping_factor", group="B",
|
|
83
|
+
does="If one event nearly always follows another, treat the pair as a single unit so the combinations do not explode.",
|
|
84
|
+
range="between 0 and 1; off when 0.",
|
|
85
|
+
recommend="off. The THEME manual illustrates it with 0.9 (a near-deterministic link).",
|
|
86
|
+
effect="a higher threshold lumps fewer pairs; used mainly to tame very dense data."),
|
|
87
|
+
dict(key="completeness", form="completeness_competition", group="B",
|
|
88
|
+
does="Drops a pattern whose every occurrence sits inside a longer detected pattern.",
|
|
89
|
+
range="on or off.",
|
|
90
|
+
recommend="on.",
|
|
91
|
+
effect="off keeps redundant sub-patterns and inflates the counts."),
|
|
92
|
+
dict(key="collapse_equivalent", form="collapse_equivalent_patterns", group="B",
|
|
93
|
+
does="Merges patterns with an identical set of occurrences (the two directions of a co-timed pair, or different bracketings of one chain) to one representative.",
|
|
94
|
+
range="on or off.",
|
|
95
|
+
recommend="on.",
|
|
96
|
+
effect="off double-counts perfectly-dependent duplicates and enlarges the multiple-comparison family."),
|
|
97
|
+
dict(key="include_univariate", form="include_univariate_patterns", group="B",
|
|
98
|
+
does="Keeps single-event-type entries (Level 0) in the output.",
|
|
99
|
+
range="on or off.",
|
|
100
|
+
recommend="on.",
|
|
101
|
+
effect="off drops the univariate baseline rows."),
|
|
102
|
+
dict(key="exclude_events", form="force_exclude_events", group="B",
|
|
103
|
+
does="An explicit list of event types to bar from building patterns, overriding the frequency rule.",
|
|
104
|
+
range="comma-separated type names, or empty.",
|
|
105
|
+
recommend="empty, so the frequency rule applies.",
|
|
106
|
+
effect="listing a type removes it from pattern-building whatever its frequency (used to reproduce a specific analysis)."),
|
|
107
|
+
# C. structural inputs (fixed before analysis, change every result)
|
|
108
|
+
dict(key="time_unit", form="time_unit", group="C",
|
|
109
|
+
does="The unit of the time columns in your file.",
|
|
110
|
+
range="s or ms.",
|
|
111
|
+
recommend="whatever your data uses; times are held internally in milliseconds.",
|
|
112
|
+
effect="sets the unit intervals are reported in; the wrong choice mis-scales every time."),
|
|
113
|
+
dict(key="obs_window", form="use_observation_window_columns", group="C",
|
|
114
|
+
does="Reads each observation's window from obs_start / obs_end columns instead of its first and last event.",
|
|
115
|
+
range="on or off.",
|
|
116
|
+
recommend="on when your observations have real bounds (a possession, rally or bout usually does).",
|
|
117
|
+
effect="off takes the window from the events, which shifts every NX/T baseline probability."),
|
|
118
|
+
dict(key="max_level", form="maximum_depth", group="C",
|
|
119
|
+
does="A cap on how deep the pattern hierarchy can go.",
|
|
120
|
+
range="a whole number, 1 or more.",
|
|
121
|
+
recommend="8; it is rarely reached.",
|
|
122
|
+
effect="lower yields fewer and simpler patterns and can prevent overload on dense data."),
|
|
123
|
+
dict(key="collapse_duplicates", form="same_instant_duplicates", group="C",
|
|
124
|
+
does="Collapses two records of the same event type at an identical timestamp to one point.",
|
|
125
|
+
range="on or off.",
|
|
126
|
+
recommend="on for finely-timed data (a double-entry is one point in time); off only on coarsely-timed data where two same-type events might genuinely share a coded time.",
|
|
127
|
+
effect="off keeps both records, inflating that type's baseline rate."),
|
|
128
|
+
dict(key="max_edges", form="candidate_edge_cap", group="C",
|
|
129
|
+
does="A cap on candidate window boundaries per pair (an approximate search for very dense data).",
|
|
130
|
+
range="a whole number, 0 or more; 0 means exact.",
|
|
131
|
+
recommend="0 (exact). Set it only on very dense data where the exact search does not finish.",
|
|
132
|
+
effect="lower is faster but makes the window search approximate rather than exhaustive."),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# --- output columns ---------------------------------------------------------
|
|
136
|
+
COLUMNS: list[dict] = [
|
|
137
|
+
dict(key="pattern", does="The pattern as text (the signature is its canonical bracketed form)."),
|
|
138
|
+
dict(key="N", does="Number of occurrences of the pattern across all observations."),
|
|
139
|
+
dict(key="bouts", does="Number of distinct observations the pattern occurs in."),
|
|
140
|
+
dict(key="bout_support", does="bouts as a fraction of all observations (the pattern's prevalence)."),
|
|
141
|
+
dict(key="length", does="Number of event types in the pattern."),
|
|
142
|
+
dict(key="level", does="Hierarchy depth: 0 is a single event type, 1 a pair, higher levels longer chains."),
|
|
143
|
+
dict(key="loop", does="1 if the pattern repeats an event type, otherwise 0."),
|
|
144
|
+
dict(key="critical_interval", does="The accepted time window [d1, d2] within which the later event follows the earlier."),
|
|
145
|
+
dict(key="duration_median_ms", does="The observed timing of the occurrences: the median gap (with duration_iqr_ms giving the middle half), in milliseconds."),
|
|
146
|
+
dict(key="monte_carlo_p", does="The Monte-Carlo empirical p-value: the fraction of surrogate datasets in which the pattern is as strong or stronger."),
|
|
147
|
+
dict(key="fdr_q", does="The Benjamini-Hochberg false-discovery q-value (the screening decision)."),
|
|
148
|
+
dict(key="survives_fwer_holm", does="The Holm family-wise decision: 1 if the pattern survives family-wise control, 0 if not."),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
_GROUP = {"A": "The data can decide it (the advisor recommends)",
|
|
152
|
+
"B": "Your policy (the data cannot decide it)",
|
|
153
|
+
"C": "Structural input (fixed before analysis)"}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def explain(name: str | None = None) -> str:
|
|
157
|
+
"""Plain-language reference for a setting or output column, or for everything.
|
|
158
|
+
|
|
159
|
+
``explain("min_lag")`` (or its form name ``"minimum_lag_frames"``) describes one
|
|
160
|
+
setting; ``explain("fdr_q")`` an output column; ``explain()`` prints them all.
|
|
161
|
+
"""
|
|
162
|
+
if name is None:
|
|
163
|
+
return explain_settings() + "\n\n" + explain_columns()
|
|
164
|
+
for s in SETTINGS:
|
|
165
|
+
if name in (s["key"], s["form"]):
|
|
166
|
+
return (f"{s['form']} (Config: {s['key']}) [{_GROUP[s['group']]}]\n"
|
|
167
|
+
f" what it does : {s['does']}\n"
|
|
168
|
+
f" range : {s['range']}\n"
|
|
169
|
+
f" recommended : {s['recommend']}\n"
|
|
170
|
+
f" higher/lower : {s['effect']}")
|
|
171
|
+
for c in COLUMNS:
|
|
172
|
+
if name == c["key"]:
|
|
173
|
+
return f"{c['key']} — {c['does']}"
|
|
174
|
+
return f"No setting or column named {name!r}. Try explain() to list them all."
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def explain_settings() -> str:
|
|
178
|
+
out = ["SETTINGS"]
|
|
179
|
+
for g in ("A", "B", "C"):
|
|
180
|
+
out.append(f"\n[{g}] {_GROUP[g]}")
|
|
181
|
+
for s in SETTINGS:
|
|
182
|
+
if s["group"] == g:
|
|
183
|
+
out.append(f" {s['form']:28s} {s['does']}")
|
|
184
|
+
out.append(f" {'':28s} range: {s['range']} recommended: {s['recommend']}")
|
|
185
|
+
return "\n".join(out)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def explain_columns() -> str:
|
|
189
|
+
return "OUTPUT COLUMNS\n" + "\n".join(f" {c['key']:22s} {c['does']}" for c in COLUMNS)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def parameters_markdown() -> str:
|
|
193
|
+
"""The full reference as Markdown (used to write PARAMETERS.md)."""
|
|
194
|
+
L = ["# tpattern parameters and outputs",
|
|
195
|
+
"",
|
|
196
|
+
"Every setting and output column, with what it does, its range, the recommended "
|
|
197
|
+
"value, and what higher or lower means. Generated from `tpattern/reference.py`, "
|
|
198
|
+
"which is also the source for the tutorial's tables, the Colab form labels and "
|
|
199
|
+
"`tpattern.explain()`.", ""]
|
|
200
|
+
for g in ("A", "B", "C"):
|
|
201
|
+
L += [f"## {_GROUP[g]}", "",
|
|
202
|
+
"| Setting (form name / `Config`) | What it does | Range | Recommended | Higher / lower |",
|
|
203
|
+
"|---|---|---|---|---|"]
|
|
204
|
+
for s in SETTINGS:
|
|
205
|
+
if s["group"] == g:
|
|
206
|
+
L.append(f"| **{s['form']}** / `{s['key']}` | {s['does']} | {s['range']} "
|
|
207
|
+
f"| {s['recommend']} | {s['effect']} |")
|
|
208
|
+
L.append("")
|
|
209
|
+
L += ["## Output columns", "",
|
|
210
|
+
"| Column | What it is |", "|---|---|"]
|
|
211
|
+
for c in COLUMNS:
|
|
212
|
+
L.append(f"| `{c['key']}` | {c['does']} |")
|
|
213
|
+
L.append("")
|
|
214
|
+
return "\n".join(L)
|
|
@@ -102,8 +102,8 @@ def patterns_table(source, outfile: str | None = None, ci_unit: str = "ms",
|
|
|
102
102
|
"""Build a results table from a list of `Pattern` or a `CalibrationResult`.
|
|
103
103
|
|
|
104
104
|
Returns a list of row dicts. If `outfile` is given, also writes CSV.
|
|
105
|
-
When given a CalibrationResult, includes
|
|
106
|
-
|
|
105
|
+
When given a CalibrationResult, includes monte_carlo_p, fdr_q, survives_fwer_holm and sorts by
|
|
106
|
+
monte_carlo_p; otherwise sorts by N (descending).
|
|
107
107
|
|
|
108
108
|
Every row carries `bouts` (distinct observations the pattern spans) and
|
|
109
109
|
`bout_support` (that as a fraction of the sample). Prevalence is reported rather
|
|
@@ -141,19 +141,23 @@ def patterns_table(source, outfile: str | None = None, ci_unit: str = "ms",
|
|
|
141
141
|
"duration_range_ms": f"[{dur['min']},{dur['max']}]" if dur else None,
|
|
142
142
|
}
|
|
143
143
|
if calibrated:
|
|
144
|
-
|
|
144
|
+
# readable column names (see tpattern/reference.py); the internal
|
|
145
|
+
# Calibrated attributes (p_emp, fwer_keep) keep their names.
|
|
146
|
+
row["monte_carlo_p"] = round(it.p_emp, 4)
|
|
145
147
|
row["fdr_q"] = round(it.fdr_q, 4)
|
|
146
|
-
row["
|
|
148
|
+
row["survives_fwer_holm"] = int(it.fwer_keep)
|
|
147
149
|
row["analytic_strength"] = round(it.strength, 2) # -log10(analytic p); comparison only
|
|
148
150
|
row["interpretation"] = interpret(p, it.N, it.fdr_q, ci_unit=ci_unit, dur=dur)
|
|
149
151
|
rows.append(row)
|
|
150
152
|
|
|
151
153
|
key = sort
|
|
152
154
|
if sort == "auto":
|
|
153
|
-
key = "
|
|
155
|
+
key = "monte_carlo_p" if calibrated else "N"
|
|
156
|
+
if key in ("p_emp", "fwer_keep"): # tolerate the old names
|
|
157
|
+
key = {"p_emp": "monte_carlo_p", "fwer_keep": "survives_fwer_holm"}[key]
|
|
154
158
|
if key == "N":
|
|
155
159
|
rows.sort(key=lambda r: (-r["N"], r["level"]))
|
|
156
|
-
elif key in ("
|
|
160
|
+
elif key in ("monte_carlo_p", "fdr_q") and calibrated:
|
|
157
161
|
rows.sort(key=lambda r: (r[key], -r["N"]))
|
|
158
162
|
|
|
159
163
|
if outfile:
|
|
@@ -400,6 +404,6 @@ def report(source, outdir: str, *, ci_unit: str = "ms", title: str = "T-pattern
|
|
|
400
404
|
for r in rows[:15]:
|
|
401
405
|
line = f" N={r['N']:>3} L{r['level']} {r['pattern']}"
|
|
402
406
|
if calibrated:
|
|
403
|
-
line += f"
|
|
407
|
+
line += f" monte_carlo_p={r['monte_carlo_p']} q={r['fdr_q']}"
|
|
404
408
|
fh.write(line + "\n")
|
|
405
409
|
return written
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Starting points students can copy and adapt.
|
|
3
|
+
=============================================
|
|
4
|
+
|
|
5
|
+
The tutorial teaches the method on the worked example; this gives a learner a clean
|
|
6
|
+
template for their *own* data. `new_analysis()` writes a heavily commented starter
|
|
7
|
+
script, with every line to change marked and pointers to `explain()` for each setting.
|
|
8
|
+
Three flavours cover the common designs:
|
|
9
|
+
|
|
10
|
+
new_analysis() # one group: detect, calibrate, report
|
|
11
|
+
new_analysis(template="two_groups") # compare two groups (odds ratios + forest plot)
|
|
12
|
+
new_analysis(template="null_choice") # core vs shell: which null a pattern survives
|
|
13
|
+
|
|
14
|
+
It is the "now do it on your own research" bridge.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
_BASIC = '''\
|
|
22
|
+
"""
|
|
23
|
+
My T-pattern analysis. (tpattern.new_analysis — copy and adapt.)
|
|
24
|
+
|
|
25
|
+
Point this at your own event table and run it. Every line to change is marked # <-- EDIT.
|
|
26
|
+
For any setting, see what it does, its range and the recommended value with:
|
|
27
|
+
from tpattern import explain; print(explain("min_lag"))
|
|
28
|
+
Your data: one row per event, with columns observation, event, start
|
|
29
|
+
(which session/rally/bout the event belongs to, what happened, and when). See SCHEMA.md
|
|
30
|
+
and the tutorial paper for the full walk-through.
|
|
31
|
+
"""
|
|
32
|
+
from tpattern import read_table, recommend, Config, calibrate, report, explain
|
|
33
|
+
|
|
34
|
+
# 1. LOAD -----------------------------------------------------------------------
|
|
35
|
+
DATA_FILE = "my_events.csv" # <-- EDIT: your event table (CSV)
|
|
36
|
+
TIME_UNIT = "ms" # <-- EDIT: "s" or "ms", the unit of your start column
|
|
37
|
+
observations = read_table(DATA_FILE, time_unit=TIME_UNIT, report="all")
|
|
38
|
+
# read_table checks the file as it reads it and prints any problems by code (Appendix A).
|
|
39
|
+
|
|
40
|
+
# 2. INSPECT + RECOMMEND --------------------------------------------------------
|
|
41
|
+
# The advisor inspects your data and recommends the null, the minimum lag and the error
|
|
42
|
+
# control, each with its reason. Read it, then decide (you are not obliged to follow it).
|
|
43
|
+
print(recommend(observations))
|
|
44
|
+
|
|
45
|
+
# 3. CONFIGURE ------------------------------------------------------------------
|
|
46
|
+
# Start from the advisor's recommendation. To understand any setting, print its help:
|
|
47
|
+
# print(explain("min_lag")) print(explain("surrogate_null")) print(explain("frequent_event_exclusion"))
|
|
48
|
+
cfg = Config(
|
|
49
|
+
min_lag=1, # <-- 1 requires a genuine time gap (recommended for frame-coded data); 0 = THEME-style
|
|
50
|
+
freq_exclude=1.5, # <-- set a very common event aside from pattern-building (kept in the counts)
|
|
51
|
+
# everything else is left at sensible defaults; run explain() to see them
|
|
52
|
+
)
|
|
53
|
+
NULL = "profile" # <-- "profile" tests coupling beyond each type's own timing (the usual choice)
|
|
54
|
+
B = 2000 # <-- surrogates: 200 to screen many patterns, 2000+ for a confirmatory claim
|
|
55
|
+
|
|
56
|
+
# 4. CALIBRATE + REPORT ---------------------------------------------------------
|
|
57
|
+
result = calibrate(observations, cfg, null=NULL, B=B, q_target=0.05)
|
|
58
|
+
report(result, "my_output", observations=observations, title="My analysis")
|
|
59
|
+
|
|
60
|
+
# my_output/ now holds the results table (patterns_table.csv, with monte_carlo_p, fdr_q and
|
|
61
|
+
# survives_fwer_holm), the pattern dendrograms, the temporal-signature plot, the event-type
|
|
62
|
+
# roles and connections tables, and a plain-text SUMMARY. Each pattern's "interpretation"
|
|
63
|
+
# column reads it in plain English. To understand a column: print(explain("fdr_q"))
|
|
64
|
+
print("\\nDone. Open the my_output/ folder.")
|
|
65
|
+
'''
|
|
66
|
+
|
|
67
|
+
_TWO_GROUPS = '''\
|
|
68
|
+
"""
|
|
69
|
+
My T-pattern group comparison. (tpattern.new_analysis(template="two_groups"))
|
|
70
|
+
|
|
71
|
+
Do patterns occur differently between two groups (winners vs losers, condition A vs B,
|
|
72
|
+
goals vs non-goals)? For each pattern this tests its occurrence between the groups
|
|
73
|
+
(Fisher's exact test, odds ratio with a 95% confidence interval) and draws a forest plot.
|
|
74
|
+
Lines to change are marked # <-- EDIT. Run explain("min_lag") etc. for any setting.
|
|
75
|
+
"""
|
|
76
|
+
import csv
|
|
77
|
+
from tpattern import read_table, Config, group_contrast, forest_plot
|
|
78
|
+
|
|
79
|
+
# 1. LOAD your events (one row per event: observation, event, start).
|
|
80
|
+
DATA_FILE = "my_events.csv" # <-- EDIT
|
|
81
|
+
TIME_UNIT = "ms" # <-- EDIT: "s" or "ms"
|
|
82
|
+
observations = read_table(DATA_FILE, time_unit=TIME_UNIT, report="all")
|
|
83
|
+
|
|
84
|
+
# 2. GROUP each observation. group_of maps an observation's name to its group label.
|
|
85
|
+
# Easiest: a two-column CSV (observation,group). <-- EDIT the path and column names.
|
|
86
|
+
group_of = {row["observation"]: row["group"]
|
|
87
|
+
for row in csv.DictReader(open("my_groups.csv"))}
|
|
88
|
+
|
|
89
|
+
# 3. CONTRAST: detect patterns and compare their occurrence between the two groups.
|
|
90
|
+
# min_count drops patterns too rare to compare; raise it to be more conservative.
|
|
91
|
+
items = group_contrast(observations, group_of, Config(min_lag=1), min_count=5)
|
|
92
|
+
|
|
93
|
+
# 4. PLOT the odds ratios with 95% CIs (a point whose CI excludes 1 differs between groups).
|
|
94
|
+
forest_plot(items, "group_contrast.png", title="Pattern occurrence: group comparison")
|
|
95
|
+
print("wrote group_contrast.png (odds ratios with 95% confidence intervals)")
|
|
96
|
+
'''
|
|
97
|
+
|
|
98
|
+
_NULL_CHOICE = '''\
|
|
99
|
+
"""
|
|
100
|
+
My null-choice comparison. (tpattern.new_analysis(template="null_choice"))
|
|
101
|
+
|
|
102
|
+
Which patterns are genuine coupling, and which are just shared timing? This calibrates
|
|
103
|
+
your data under two nulls and splits the survivors into the CORE (survives both nulls =
|
|
104
|
+
coupling beyond each event's own timing) and the SHELL (survives the rotation null only =
|
|
105
|
+
co-occurrence driven by shared timing). Lines to change are marked # <-- EDIT.
|
|
106
|
+
"""
|
|
107
|
+
from tpattern import read_table, Config, compare_nulls, null_comparison_plot
|
|
108
|
+
|
|
109
|
+
DATA_FILE = "my_events.csv" # <-- EDIT
|
|
110
|
+
TIME_UNIT = "ms" # <-- EDIT: "s" or "ms"
|
|
111
|
+
observations = read_table(DATA_FILE, time_unit=TIME_UNIT, report="all")
|
|
112
|
+
|
|
113
|
+
# Compare the profile-preserving null (coupling) against the rotation null (any structure).
|
|
114
|
+
# B = 2000 gives a fine Monte-Carlo p; use 200 to explore quickly first.
|
|
115
|
+
cmp = compare_nulls(observations, Config(min_lag=1), nulls=("profile", "rotation"), B=2000)
|
|
116
|
+
print(cmp) # lists the core and the shell, with each pattern's q under each null
|
|
117
|
+
|
|
118
|
+
null_comparison_plot(cmp, "null_comparison.png")
|
|
119
|
+
print("\\nwrote null_comparison.png (core = survives both nulls; shell = rotation only)")
|
|
120
|
+
'''
|
|
121
|
+
|
|
122
|
+
TEMPLATES = {"basic": _BASIC, "two_groups": _TWO_GROUPS, "null_choice": _NULL_CHOICE}
|
|
123
|
+
_DEFAULT_NAME = {"basic": "my_analysis.py", "two_groups": "my_group_comparison.py",
|
|
124
|
+
"null_choice": "my_null_choice.py"}
|
|
125
|
+
|
|
126
|
+
# kept for backward compatibility (0.1.12 exported TEMPLATE)
|
|
127
|
+
TEMPLATE = _BASIC
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def new_analysis(path: str | Path | None = None, *, template: str = "basic",
|
|
131
|
+
overwrite: bool = False) -> str:
|
|
132
|
+
"""Write a copy-and-adapt analysis template to `path`. Returns the path written.
|
|
133
|
+
|
|
134
|
+
`template` is one of ``"basic"`` (one group), ``"two_groups"`` (compare two groups),
|
|
135
|
+
or ``"null_choice"`` (core vs shell). If `path` is omitted a sensible filename is used.
|
|
136
|
+
|
|
137
|
+
>>> from tpattern import new_analysis
|
|
138
|
+
>>> new_analysis() # writes ./my_analysis.py
|
|
139
|
+
>>> new_analysis(template="two_groups") # writes ./my_group_comparison.py
|
|
140
|
+
"""
|
|
141
|
+
if template not in TEMPLATES:
|
|
142
|
+
raise ValueError(f"template must be one of {sorted(TEMPLATES)}, got {template!r}")
|
|
143
|
+
p = Path(path) if path is not None else Path(_DEFAULT_NAME[template])
|
|
144
|
+
if p.exists() and not overwrite:
|
|
145
|
+
raise FileExistsError(f"{p} already exists; pass overwrite=True to replace it.")
|
|
146
|
+
p.write_text(TEMPLATES[template])
|
|
147
|
+
return str(p)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tpattern
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
4
4
|
Summary: Open, auditable temporal & sequential pattern analysis (T-patterns) for sport and behaviour
|
|
5
5
|
Author-email: Andrew Callaway <acallaway@bournemouth.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -54,6 +54,60 @@ analysis companion to the [OpenTag.Studio](https://opentag.studio) tagging tool.
|
|
|
54
54
|
Bring your own data (one row per event — see [`SCHEMA.md`](SCHEMA.md)) or try the
|
|
55
55
|
included [`data/`](data/) example.
|
|
56
56
|
|
|
57
|
+
## Getting started in a minute
|
|
58
|
+
|
|
59
|
+
Install, generate a starter script, point it at your data, run it:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install tpattern
|
|
63
|
+
python -c "import tpattern; tpattern.new_analysis()" # writes my_analysis.py
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Open `my_analysis.py`, change the two lines marked `# <-- EDIT` (your file and its time
|
|
67
|
+
unit), and run `python my_analysis.py`. It loads your events, **checks the file and tells
|
|
68
|
+
you in plain language about any problem**, recommends the analysis settings with reasons,
|
|
69
|
+
calibrates every pattern against a chance model, and writes a folder of results with a
|
|
70
|
+
plain-English verdict for each pattern.
|
|
71
|
+
|
|
72
|
+
Doing something more specific? Generate a template for it:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from tpattern import new_analysis
|
|
76
|
+
new_analysis() # one group: detect, calibrate, report
|
|
77
|
+
new_analysis(template="two_groups") # compare two groups (odds ratios + forest plot)
|
|
78
|
+
new_analysis(template="null_choice") # core vs shell: which null a pattern survives
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Not sure what a setting means? Ask the code — every setting and output column is documented:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from tpattern import explain
|
|
85
|
+
print(explain("min_lag")) # what it does, its range, the recommended value, higher vs lower
|
|
86
|
+
print(explain("fdr_q")) # what an output column means
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What you get
|
|
90
|
+
|
|
91
|
+
- **Forgiving data import.** `read_table` checks your file as it reads it and reports, in
|
|
92
|
+
plain language, any problem, how much of the file it affects, and how to fix it — instead
|
|
93
|
+
of silently dropping rows. Each check has a stable code (Appendix A of the tutorial; also
|
|
94
|
+
[`PARAMETERS.md`](PARAMETERS.md)).
|
|
95
|
+
- **A proper chance model.** Every pattern is tested against a surrogate null, with p-values
|
|
96
|
+
corrected for repeated testing (Benjamini–Hochberg FDR for screening, Holm family-wise for
|
|
97
|
+
confirmatory claims), stratified by hierarchy level.
|
|
98
|
+
- **Temporal reporting.** Beyond *which* patterns are real, the output says *when*: each
|
|
99
|
+
pattern's occurrence timing (median lag and spread) is reported by default — on the
|
|
100
|
+
dendrogram, in the table, and in the plain-English verdict — for every pattern.
|
|
101
|
+
- **Readable output.** Results-table columns are named for what they are (`monte_carlo_p`,
|
|
102
|
+
`fdr_q`, `survives_fwer_holm`, `bout_support`, `duration_median_ms`), and each pattern
|
|
103
|
+
carries a plain-English `interpretation`. `explain()` documents every setting and column;
|
|
104
|
+
the full reference is in [`PARAMETERS.md`](PARAMETERS.md).
|
|
105
|
+
- **One reference, everywhere.** Settings and columns live in one registry that generates the
|
|
106
|
+
code's `explain()`, `PARAMETERS.md`, the Colab form labels, and the tutorial's tables — so
|
|
107
|
+
the software, the paper and the notebook can't drift apart.
|
|
108
|
+
- **Clean figures** — dendrograms, temporal signatures, forest plots — that share one
|
|
109
|
+
customisable style (`tpattern.viz.STYLE`).
|
|
110
|
+
|
|
57
111
|
## Why it exists
|
|
58
112
|
|
|
59
113
|
T-pattern analysis finds behaviourally meaningful structure that event counts and
|
|
@@ -62,7 +116,7 @@ software, the reference implementation. `tpattern` is an open, complementary
|
|
|
62
116
|
implementation that reproduces THEME's detection and adds a reproducible chance
|
|
63
117
|
model and clear reporting:
|
|
64
118
|
|
|
65
|
-
- **reproduces** THEME's core detection (critical intervals,
|
|
119
|
+
- **reproduces** THEME's core detection (critical intervals, NX/T baseline,
|
|
66
120
|
completeness competition) — validated to the pattern for published datasets;
|
|
67
121
|
- **adds a chance model** — every pattern is tested against a surrogate null, with
|
|
68
122
|
p-values **corrected for repeated testing** (false-discovery rate and
|
|
@@ -109,12 +163,18 @@ print(recommend(observations))
|
|
|
109
163
|
result = calibrate(observations, Config(), null="profile", B=200, q_target=0.05)
|
|
110
164
|
survivors = result.kept("fdr") # patterns that survive false-discovery control
|
|
111
165
|
|
|
112
|
-
# 4. Report: a tidy table (
|
|
113
|
-
#
|
|
114
|
-
|
|
166
|
+
# 4. Report: a tidy results table (monte_carlo_p, fdr_q, survives_fwer_holm, the observed
|
|
167
|
+
# timing, and a plain-English `interpretation` per pattern), pattern dendrograms, the
|
|
168
|
+
# temporal-signature and occurrence plots, the event-type roles/connections tables, and a
|
|
169
|
+
# paste-ready Methods paragraph stating every setting and what it does.
|
|
170
|
+
report(result, "report_out", observations=observations, title="My analysis")
|
|
115
171
|
print(methods_text(Config(), observations=observations, calibration=result))
|
|
116
172
|
```
|
|
117
173
|
|
|
174
|
+
Prefer not to write the pipeline yourself? `python -c "import tpattern;
|
|
175
|
+
tpattern.new_analysis()"` writes a commented [`my_analysis.py`](examples/my_analysis.py)
|
|
176
|
+
you edit and run (see [Getting started](#getting-started-in-a-minute)).
|
|
177
|
+
|
|
118
178
|
Two runnable examples:
|
|
119
179
|
|
|
120
180
|
```bash
|
|
@@ -128,7 +188,8 @@ Config()).detect()` returns them directly.
|
|
|
128
188
|
|
|
129
189
|
**Comparing two groups** (winner vs loser, goal vs non-goal, condition A vs B)?
|
|
130
190
|
`group_contrast(observations, group_of)` tests each pattern's occurrence between
|
|
131
|
-
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot
|
|
191
|
+
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot` — or run
|
|
192
|
+
`new_analysis(template="two_groups")` for a ready-made script.
|
|
132
193
|
|
|
133
194
|
## The workflow
|
|
134
195
|
|
|
@@ -179,7 +240,10 @@ survive the surrogate null. All seeded and deterministic. See
|
|
|
179
240
|
- [`SCHEMA.md`](SCHEMA.md) — the input data structure (what your export must contain).
|
|
180
241
|
- [`CODING_GUIDE.md`](CODING_GUIDE.md) — how to set up a coding window to produce it.
|
|
181
242
|
- [`data/`](data/) — the derived World Cup sequences, in the canonical format.
|
|
182
|
-
- [`examples/`](examples/) — a runnable quickstart
|
|
243
|
+
- [`examples/`](examples/) — a runnable quickstart, the reproduction script, and the
|
|
244
|
+
`my_analysis.py` / `my_group_comparison.py` / `my_null_choice.py` starter templates.
|
|
245
|
+
- [`PARAMETERS.md`](PARAMETERS.md) — every setting and output column, with range, recommended
|
|
246
|
+
value, and what higher/lower does (generated from the code, so it never drifts).
|
|
183
247
|
|
|
184
248
|
## Tests
|
|
185
249
|
|
|
@@ -8,9 +8,11 @@ tests/test_guided.py
|
|
|
8
8
|
tests/test_null_compare.py
|
|
9
9
|
tests/test_outputs.py
|
|
10
10
|
tests/test_read_table_validation.py
|
|
11
|
+
tests/test_reference.py
|
|
11
12
|
tests/test_report_dendrogram.py
|
|
12
13
|
tests/test_reproduction.py
|
|
13
14
|
tests/test_sofcoder.py
|
|
15
|
+
tests/test_starter.py
|
|
14
16
|
tests/test_support_reporting.py
|
|
15
17
|
tests/test_synthetic.py
|
|
16
18
|
tpattern/__init__.py
|
|
@@ -24,8 +26,10 @@ tpattern/io.py
|
|
|
24
26
|
tpattern/methods.py
|
|
25
27
|
tpattern/pattern.py
|
|
26
28
|
tpattern/randomise.py
|
|
29
|
+
tpattern/reference.py
|
|
27
30
|
tpattern/report.py
|
|
28
31
|
tpattern/significance.py
|
|
32
|
+
tpattern/starter.py
|
|
29
33
|
tpattern/synthetic.py
|
|
30
34
|
tpattern/viz.py
|
|
31
35
|
tpattern.egg-info/PKG-INFO
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|