tpattern 0.1.0__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.0/LICENSE +21 -0
- tpattern-0.1.0/PKG-INFO +211 -0
- tpattern-0.1.0/README.md +175 -0
- tpattern-0.1.0/pyproject.toml +49 -0
- tpattern-0.1.0/setup.cfg +4 -0
- tpattern-0.1.0/tests/test_calibrated_pins.py +34 -0
- tpattern-0.1.0/tests/test_contrast.py +40 -0
- tpattern-0.1.0/tests/test_dedup.py +33 -0
- tpattern-0.1.0/tests/test_guided.py +28 -0
- tpattern-0.1.0/tests/test_reproduction.py +61 -0
- tpattern-0.1.0/tests/test_synthetic.py +41 -0
- tpattern-0.1.0/tpattern/__init__.py +36 -0
- tpattern-0.1.0/tpattern/advisor.py +195 -0
- tpattern-0.1.0/tpattern/ci.py +300 -0
- tpattern-0.1.0/tpattern/contrast.py +90 -0
- tpattern-0.1.0/tpattern/detect.py +334 -0
- tpattern-0.1.0/tpattern/diagnostics.py +120 -0
- tpattern-0.1.0/tpattern/guided.py +372 -0
- tpattern-0.1.0/tpattern/io.py +214 -0
- tpattern-0.1.0/tpattern/methods.py +132 -0
- tpattern-0.1.0/tpattern/pattern.py +129 -0
- tpattern-0.1.0/tpattern/randomise.py +206 -0
- tpattern-0.1.0/tpattern/report.py +201 -0
- tpattern-0.1.0/tpattern/significance.py +218 -0
- tpattern-0.1.0/tpattern/synthetic.py +95 -0
- tpattern-0.1.0/tpattern/viz.py +107 -0
- tpattern-0.1.0/tpattern.egg-info/PKG-INFO +211 -0
- tpattern-0.1.0/tpattern.egg-info/SOURCES.txt +29 -0
- tpattern-0.1.0/tpattern.egg-info/dependency_links.txt +1 -0
- tpattern-0.1.0/tpattern.egg-info/requires.txt +11 -0
- tpattern-0.1.0/tpattern.egg-info/top_level.txt +1 -0
tpattern-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Callaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tpattern-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tpattern
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open, auditable temporal & sequential pattern analysis (T-patterns) for sport and behaviour
|
|
5
|
+
Author-email: Andrew Callaway <acallaway@bournemouth.ac.uk>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ajcallaway/TPattern
|
|
8
|
+
Project-URL: Repository, https://github.com/ajcallaway/TPattern
|
|
9
|
+
Project-URL: Archive (DOI), https://doi.org/10.5281/zenodo.21397543
|
|
10
|
+
Project-URL: Tagging tool (OpenTag), https://opentag.studio
|
|
11
|
+
Keywords: t-pattern,THEME,temporal,sequence,sport,behaviour,notation analysis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Intended Audience :: Science/Research
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy>=1.21
|
|
27
|
+
Requires-Dist: scipy>=1.7
|
|
28
|
+
Requires-Dist: matplotlib>=3.4
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
31
|
+
Provides-Extra: gui
|
|
32
|
+
Requires-Dist: ipywidgets>=8; extra == "gui"
|
|
33
|
+
Requires-Dist: pandas>=1.3; extra == "gui"
|
|
34
|
+
Requires-Dist: openpyxl>=3; extra == "gui"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# tpattern — open, auditable analysis of recurring patterns in event sequences
|
|
38
|
+
|
|
39
|
+
[](https://colab.research.google.com/github/ajcallaway/TPattern/blob/main/colab/tpattern_guided_analysis.ipynb)
|
|
40
|
+
[](https://doi.org/10.5281/zenodo.21397543)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
|
|
43
|
+
`tpattern` detects recurring temporal patterns (T-patterns; Magnusson, 2000) in
|
|
44
|
+
**any sequence of timed, labelled events** — behaviour, sport, clinical or
|
|
45
|
+
physiological logs, interaction data — validates them properly against chance, and
|
|
46
|
+
produces clean, publication-ready output (tables, figures, pattern dendrograms). It
|
|
47
|
+
is a free, transparent, reproducible alternative to the THEME software, and the
|
|
48
|
+
analysis companion to the [OpenTag.Studio](https://opentag.studio) tagging tool.
|
|
49
|
+
|
|
50
|
+
## Two ways to use it
|
|
51
|
+
|
|
52
|
+
- 🚀 **Google Colab — no coding.** [**Open the guided notebook in Colab**](https://colab.research.google.com/github/ajcallaway/TPattern/blob/main/colab/tpattern_guided_analysis.ipynb): upload your events, follow the guided steps, download a report with a plain-English verdict for every pattern and a paste-ready Methods paragraph. See [`colab/`](colab/).
|
|
53
|
+
- 🐍 **Python library.** `pip install` and script it — see [Quickstart](#quickstart) and [`examples/`](examples/).
|
|
54
|
+
|
|
55
|
+
Bring your own data (one row per event — see [`SCHEMA.md`](SCHEMA.md)) or try the
|
|
56
|
+
included [`data/`](data/) example.
|
|
57
|
+
|
|
58
|
+
## Why it exists
|
|
59
|
+
|
|
60
|
+
T-pattern analysis finds behaviourally meaningful structure that event counts and
|
|
61
|
+
simple transition tables miss. It is most established through Magnusson's THEME
|
|
62
|
+
software, the reference implementation. `tpattern` is an open, complementary
|
|
63
|
+
implementation that reproduces THEME's detection and adds a reproducible chance
|
|
64
|
+
model and clear reporting:
|
|
65
|
+
|
|
66
|
+
- **reproduces** THEME's core detection (critical intervals, Nx/T baseline,
|
|
67
|
+
completeness competition) — validated to the pattern for published datasets;
|
|
68
|
+
- **adds a chance model** — every pattern is tested against a surrogate null, with
|
|
69
|
+
p-values **corrected for repeated testing** (false-discovery rate and
|
|
70
|
+
family-wise);
|
|
71
|
+
- **handles concurrency explicitly** — events sharing a timestamp are treated as
|
|
72
|
+
co-occurrence rather than sequence;
|
|
73
|
+
- **reports clearly** — tidy tables, effect sizes with confidence intervals,
|
|
74
|
+
purposeful figures, and clean **dendrograms** of the detected patterns.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
From source (until published on PyPI):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/ajcallaway/TPattern.git
|
|
82
|
+
cd tpattern
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Requires Python ≥ 3.9 (depends on numpy, scipy, matplotlib).
|
|
87
|
+
|
|
88
|
+
## Quickstart
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from tpattern import read_table, Config, recommend, calibrate, report, methods_text
|
|
92
|
+
|
|
93
|
+
# 1. Load your events — one row per event (observation, event, start).
|
|
94
|
+
# Pass time_unit="ms" if your times are in milliseconds (default is seconds).
|
|
95
|
+
# See SCHEMA.md for the (simple) required format.
|
|
96
|
+
observations = read_table("events.csv", time_unit="s")
|
|
97
|
+
|
|
98
|
+
# 2. Let the tool inspect the data and recommend the method — which surrogate
|
|
99
|
+
# null, whether to require a genuine lag, which error control — with reasons.
|
|
100
|
+
print(recommend(observations))
|
|
101
|
+
|
|
102
|
+
# 3. Detect and calibrate in one call: every pattern is tested against a
|
|
103
|
+
# surrogate null with correction for repeated testing.
|
|
104
|
+
# (calibrate() runs the detection for you — you don't call detect() first.)
|
|
105
|
+
result = calibrate(observations, Config(), null="profile", B=200, q_target=0.05)
|
|
106
|
+
survivors = result.kept("fdr") # patterns that survive false-discovery control
|
|
107
|
+
|
|
108
|
+
# 4. Report: a tidy table (with q-values), pattern dendrograms and a summary —
|
|
109
|
+
# plus a paste-ready Methods paragraph stating every setting and what it does.
|
|
110
|
+
report(result, "report_out", title="My analysis")
|
|
111
|
+
print(methods_text(Config(), observations=observations, calibration=result))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Two runnable examples:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install -e .
|
|
118
|
+
python examples/quickstart.py # no data needed (synthetic ground-truth demo)
|
|
119
|
+
python examples/reproduce_worldcup.py # a real read_table + full analysis on shipped data
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Just want the detected patterns without calibration? `Engine(observations,
|
|
123
|
+
Config()).detect()` returns them directly.
|
|
124
|
+
|
|
125
|
+
**Comparing two groups** (winner vs loser, goal vs non-goal, condition A vs B)?
|
|
126
|
+
`group_contrast(observations, group_of)` tests each pattern's occurrence between
|
|
127
|
+
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot`.
|
|
128
|
+
|
|
129
|
+
## The workflow
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
tag events export analyse report
|
|
133
|
+
(OpenTag / Sportscode) ──────▶ standard ──▶ tpattern ──▶ tables · figures · dendrograms
|
|
134
|
+
table (detect · validate ·
|
|
135
|
+
calibrate)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
1. **Tag** your video/events in any coding tool.
|
|
139
|
+
2. **Export** to the standard input structure — one row per event
|
|
140
|
+
(`observation, event, start`, plus optional `end`, descriptor and group
|
|
141
|
+
columns). See [`SCHEMA.md`](SCHEMA.md).
|
|
142
|
+
3. **Set up your coding** so the export is analysis-ready — one event per action,
|
|
143
|
+
explicit outcomes, unit boundaries. See [`CODING_GUIDE.md`](CODING_GUIDE.md).
|
|
144
|
+
4. **Detect** T-patterns, **validate** them against a surrogate null with corrected
|
|
145
|
+
significance (`recommend` advises the settings; `calibrate` runs it), and
|
|
146
|
+
**report** (tables, dendrograms, and a `methods_text` Methods paragraph).
|
|
147
|
+
|
|
148
|
+
## Relationship to OpenTag.Studio
|
|
149
|
+
|
|
150
|
+
[OpenTag.Studio](https://opentag.studio) is a free, browser-based sports video
|
|
151
|
+
tagging tool. It is the natural front end for `tpattern`: tag your footage, export
|
|
152
|
+
a session, and analyse it here — no proprietary software anywhere in the pipeline.
|
|
153
|
+
OpenTag's **THEME & GSEQ setup guide** covers how to design a coding window so the
|
|
154
|
+
export is analysis-ready, and that guidance applies directly to `tpattern`
|
|
155
|
+
(`CODING_GUIDE.md` is the short bridge). Together they form a complete, open
|
|
156
|
+
tag → analysis pipeline.
|
|
157
|
+
|
|
158
|
+
## Reproducing the published results
|
|
159
|
+
|
|
160
|
+
The derived 2022 World Cup event sequences ship in [`data/`](data/), so the
|
|
161
|
+
published analyses reproduce from this repository alone:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
python examples/reproduce_worldcup.py # concordance, exclusion, concurrency (~5 s)
|
|
165
|
+
python examples/reproduce_worldcup.py --calibrate # also the surrogate null (~10 min)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This regenerates the tpattern-vs-THEME pattern counts by level, the interception
|
|
169
|
+
exclusion divergence, the co-occurrence-vs-sequence analysis, and how many patterns
|
|
170
|
+
survive the surrogate null. All seeded and deterministic. See
|
|
171
|
+
[`data/README.md`](data/README.md) for provenance and how the data was derived.
|
|
172
|
+
|
|
173
|
+
## Documentation
|
|
174
|
+
|
|
175
|
+
- [`SCHEMA.md`](SCHEMA.md) — the input data structure (what your export must contain).
|
|
176
|
+
- [`CODING_GUIDE.md`](CODING_GUIDE.md) — how to set up a coding window to produce it.
|
|
177
|
+
- [`data/`](data/) — the derived World Cup sequences, in the canonical format.
|
|
178
|
+
- [`examples/`](examples/) — a runnable quickstart and the reproduction script.
|
|
179
|
+
|
|
180
|
+
## Tests
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pip install -e .[test]
|
|
184
|
+
pytest -q
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Both the synthetic ground-truth tests and the THEME reproduction tests run
|
|
188
|
+
everywhere — the latter check the detector still reproduces THEME's published
|
|
189
|
+
occurrence counts to the exact N.
|
|
190
|
+
|
|
191
|
+
## Citation
|
|
192
|
+
|
|
193
|
+
If you use `tpattern` in research, please cite the software (a machine-readable
|
|
194
|
+
version is in [`CITATION.cff`](CITATION.cff)):
|
|
195
|
+
|
|
196
|
+
Callaway, A. (2026). *tpattern: open, auditable temporal & sequential analysis for
|
|
197
|
+
sport and behaviour* [Computer software]. Zenodo.
|
|
198
|
+
https://doi.org/10.5281/zenodo.21397543
|
|
199
|
+
|
|
200
|
+
This DOI is permanent and always resolves to the current release. To pin a specific
|
|
201
|
+
version instead, use that release's own DOI, listed on the Zenodo record.
|
|
202
|
+
|
|
203
|
+
And the method:
|
|
204
|
+
|
|
205
|
+
Magnusson, M. S. (2000). Discovering hidden time patterns in behavior: T-patterns
|
|
206
|
+
and their detection. *Behavior Research Methods, Instruments, & Computers*, 32(1),
|
|
207
|
+
93–110. https://doi.org/10.3758/BF03200792
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT — see [`LICENSE`](LICENSE).
|
tpattern-0.1.0/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# tpattern — open, auditable analysis of recurring patterns in event sequences
|
|
2
|
+
|
|
3
|
+
[](https://colab.research.google.com/github/ajcallaway/TPattern/blob/main/colab/tpattern_guided_analysis.ipynb)
|
|
4
|
+
[](https://doi.org/10.5281/zenodo.21397543)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
`tpattern` detects recurring temporal patterns (T-patterns; Magnusson, 2000) in
|
|
8
|
+
**any sequence of timed, labelled events** — behaviour, sport, clinical or
|
|
9
|
+
physiological logs, interaction data — validates them properly against chance, and
|
|
10
|
+
produces clean, publication-ready output (tables, figures, pattern dendrograms). It
|
|
11
|
+
is a free, transparent, reproducible alternative to the THEME software, and the
|
|
12
|
+
analysis companion to the [OpenTag.Studio](https://opentag.studio) tagging tool.
|
|
13
|
+
|
|
14
|
+
## Two ways to use it
|
|
15
|
+
|
|
16
|
+
- 🚀 **Google Colab — no coding.** [**Open the guided notebook in Colab**](https://colab.research.google.com/github/ajcallaway/TPattern/blob/main/colab/tpattern_guided_analysis.ipynb): upload your events, follow the guided steps, download a report with a plain-English verdict for every pattern and a paste-ready Methods paragraph. See [`colab/`](colab/).
|
|
17
|
+
- 🐍 **Python library.** `pip install` and script it — see [Quickstart](#quickstart) and [`examples/`](examples/).
|
|
18
|
+
|
|
19
|
+
Bring your own data (one row per event — see [`SCHEMA.md`](SCHEMA.md)) or try the
|
|
20
|
+
included [`data/`](data/) example.
|
|
21
|
+
|
|
22
|
+
## Why it exists
|
|
23
|
+
|
|
24
|
+
T-pattern analysis finds behaviourally meaningful structure that event counts and
|
|
25
|
+
simple transition tables miss. It is most established through Magnusson's THEME
|
|
26
|
+
software, the reference implementation. `tpattern` is an open, complementary
|
|
27
|
+
implementation that reproduces THEME's detection and adds a reproducible chance
|
|
28
|
+
model and clear reporting:
|
|
29
|
+
|
|
30
|
+
- **reproduces** THEME's core detection (critical intervals, Nx/T baseline,
|
|
31
|
+
completeness competition) — validated to the pattern for published datasets;
|
|
32
|
+
- **adds a chance model** — every pattern is tested against a surrogate null, with
|
|
33
|
+
p-values **corrected for repeated testing** (false-discovery rate and
|
|
34
|
+
family-wise);
|
|
35
|
+
- **handles concurrency explicitly** — events sharing a timestamp are treated as
|
|
36
|
+
co-occurrence rather than sequence;
|
|
37
|
+
- **reports clearly** — tidy tables, effect sizes with confidence intervals,
|
|
38
|
+
purposeful figures, and clean **dendrograms** of the detected patterns.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
From source (until published on PyPI):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/ajcallaway/TPattern.git
|
|
46
|
+
cd tpattern
|
|
47
|
+
pip install -e .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requires Python ≥ 3.9 (depends on numpy, scipy, matplotlib).
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from tpattern import read_table, Config, recommend, calibrate, report, methods_text
|
|
56
|
+
|
|
57
|
+
# 1. Load your events — one row per event (observation, event, start).
|
|
58
|
+
# Pass time_unit="ms" if your times are in milliseconds (default is seconds).
|
|
59
|
+
# See SCHEMA.md for the (simple) required format.
|
|
60
|
+
observations = read_table("events.csv", time_unit="s")
|
|
61
|
+
|
|
62
|
+
# 2. Let the tool inspect the data and recommend the method — which surrogate
|
|
63
|
+
# null, whether to require a genuine lag, which error control — with reasons.
|
|
64
|
+
print(recommend(observations))
|
|
65
|
+
|
|
66
|
+
# 3. Detect and calibrate in one call: every pattern is tested against a
|
|
67
|
+
# surrogate null with correction for repeated testing.
|
|
68
|
+
# (calibrate() runs the detection for you — you don't call detect() first.)
|
|
69
|
+
result = calibrate(observations, Config(), null="profile", B=200, q_target=0.05)
|
|
70
|
+
survivors = result.kept("fdr") # patterns that survive false-discovery control
|
|
71
|
+
|
|
72
|
+
# 4. Report: a tidy table (with q-values), pattern dendrograms and a summary —
|
|
73
|
+
# plus a paste-ready Methods paragraph stating every setting and what it does.
|
|
74
|
+
report(result, "report_out", title="My analysis")
|
|
75
|
+
print(methods_text(Config(), observations=observations, calibration=result))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Two runnable examples:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install -e .
|
|
82
|
+
python examples/quickstart.py # no data needed (synthetic ground-truth demo)
|
|
83
|
+
python examples/reproduce_worldcup.py # a real read_table + full analysis on shipped data
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Just want the detected patterns without calibration? `Engine(observations,
|
|
87
|
+
Config()).detect()` returns them directly.
|
|
88
|
+
|
|
89
|
+
**Comparing two groups** (winner vs loser, goal vs non-goal, condition A vs B)?
|
|
90
|
+
`group_contrast(observations, group_of)` tests each pattern's occurrence between
|
|
91
|
+
the groups (Fisher's exact test, odds ratio + 95% CI) and feeds `forest_plot`.
|
|
92
|
+
|
|
93
|
+
## The workflow
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
tag events export analyse report
|
|
97
|
+
(OpenTag / Sportscode) ──────▶ standard ──▶ tpattern ──▶ tables · figures · dendrograms
|
|
98
|
+
table (detect · validate ·
|
|
99
|
+
calibrate)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
1. **Tag** your video/events in any coding tool.
|
|
103
|
+
2. **Export** to the standard input structure — one row per event
|
|
104
|
+
(`observation, event, start`, plus optional `end`, descriptor and group
|
|
105
|
+
columns). See [`SCHEMA.md`](SCHEMA.md).
|
|
106
|
+
3. **Set up your coding** so the export is analysis-ready — one event per action,
|
|
107
|
+
explicit outcomes, unit boundaries. See [`CODING_GUIDE.md`](CODING_GUIDE.md).
|
|
108
|
+
4. **Detect** T-patterns, **validate** them against a surrogate null with corrected
|
|
109
|
+
significance (`recommend` advises the settings; `calibrate` runs it), and
|
|
110
|
+
**report** (tables, dendrograms, and a `methods_text` Methods paragraph).
|
|
111
|
+
|
|
112
|
+
## Relationship to OpenTag.Studio
|
|
113
|
+
|
|
114
|
+
[OpenTag.Studio](https://opentag.studio) is a free, browser-based sports video
|
|
115
|
+
tagging tool. It is the natural front end for `tpattern`: tag your footage, export
|
|
116
|
+
a session, and analyse it here — no proprietary software anywhere in the pipeline.
|
|
117
|
+
OpenTag's **THEME & GSEQ setup guide** covers how to design a coding window so the
|
|
118
|
+
export is analysis-ready, and that guidance applies directly to `tpattern`
|
|
119
|
+
(`CODING_GUIDE.md` is the short bridge). Together they form a complete, open
|
|
120
|
+
tag → analysis pipeline.
|
|
121
|
+
|
|
122
|
+
## Reproducing the published results
|
|
123
|
+
|
|
124
|
+
The derived 2022 World Cup event sequences ship in [`data/`](data/), so the
|
|
125
|
+
published analyses reproduce from this repository alone:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python examples/reproduce_worldcup.py # concordance, exclusion, concurrency (~5 s)
|
|
129
|
+
python examples/reproduce_worldcup.py --calibrate # also the surrogate null (~10 min)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This regenerates the tpattern-vs-THEME pattern counts by level, the interception
|
|
133
|
+
exclusion divergence, the co-occurrence-vs-sequence analysis, and how many patterns
|
|
134
|
+
survive the surrogate null. All seeded and deterministic. See
|
|
135
|
+
[`data/README.md`](data/README.md) for provenance and how the data was derived.
|
|
136
|
+
|
|
137
|
+
## Documentation
|
|
138
|
+
|
|
139
|
+
- [`SCHEMA.md`](SCHEMA.md) — the input data structure (what your export must contain).
|
|
140
|
+
- [`CODING_GUIDE.md`](CODING_GUIDE.md) — how to set up a coding window to produce it.
|
|
141
|
+
- [`data/`](data/) — the derived World Cup sequences, in the canonical format.
|
|
142
|
+
- [`examples/`](examples/) — a runnable quickstart and the reproduction script.
|
|
143
|
+
|
|
144
|
+
## Tests
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install -e .[test]
|
|
148
|
+
pytest -q
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Both the synthetic ground-truth tests and the THEME reproduction tests run
|
|
152
|
+
everywhere — the latter check the detector still reproduces THEME's published
|
|
153
|
+
occurrence counts to the exact N.
|
|
154
|
+
|
|
155
|
+
## Citation
|
|
156
|
+
|
|
157
|
+
If you use `tpattern` in research, please cite the software (a machine-readable
|
|
158
|
+
version is in [`CITATION.cff`](CITATION.cff)):
|
|
159
|
+
|
|
160
|
+
Callaway, A. (2026). *tpattern: open, auditable temporal & sequential analysis for
|
|
161
|
+
sport and behaviour* [Computer software]. Zenodo.
|
|
162
|
+
https://doi.org/10.5281/zenodo.21397543
|
|
163
|
+
|
|
164
|
+
This DOI is permanent and always resolves to the current release. To pin a specific
|
|
165
|
+
version instead, use that release's own DOI, listed on the Zenodo record.
|
|
166
|
+
|
|
167
|
+
And the method:
|
|
168
|
+
|
|
169
|
+
Magnusson, M. S. (2000). Discovering hidden time patterns in behavior: T-patterns
|
|
170
|
+
and their detection. *Behavior Research Methods, Instruments, & Computers*, 32(1),
|
|
171
|
+
93–110. https://doi.org/10.3758/BF03200792
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tpattern"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Open, auditable temporal & sequential pattern analysis (T-patterns) for sport and behaviour"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Andrew Callaway", email = "acallaway@bournemouth.ac.uk" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["t-pattern", "THEME", "temporal", "sequence", "sport", "behaviour", "notation analysis"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
"Topic :: Scientific/Engineering",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"numpy>=1.21",
|
|
31
|
+
"scipy>=1.7",
|
|
32
|
+
"matplotlib>=3.4",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
test = ["pytest>=7"]
|
|
37
|
+
gui = ["ipywidgets>=8", "pandas>=1.3", "openpyxl>=3"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/ajcallaway/TPattern"
|
|
41
|
+
Repository = "https://github.com/ajcallaway/TPattern"
|
|
42
|
+
"Archive (DOI)" = "https://doi.org/10.5281/zenodo.21397543"
|
|
43
|
+
"Tagging tool (OpenTag)" = "https://opentag.studio"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
packages = ["tpattern"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
tpattern-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Regression guards for the calibrated pipeline (panel Review-Extras §C):
|
|
2
|
+
the headline q-value the validation paper turns on, detection determinism, and
|
|
3
|
+
the occurrence-equivalence invariant. Nothing else pins these."""
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
7
|
+
from tpattern import read_table, Config, Engine, calibrate # noqa: E402
|
|
8
|
+
from tpattern.synthetic import SyntheticSpec, make_dataset # noqa: E402
|
|
9
|
+
|
|
10
|
+
DATA = Path(__file__).resolve().parents[1] / "data"
|
|
11
|
+
|
|
12
|
+
def test_headline_calibrated_number_is_pinned():
|
|
13
|
+
obs = read_table(DATA / "worldcup_goals.csv", obs_start="obs_start",
|
|
14
|
+
obs_end="obs_end", time_unit="ms")
|
|
15
|
+
r = calibrate(obs, Config(min_lag=1), null="profile", B=2000, seed=20260714)
|
|
16
|
+
kept = r.kept("fdr")
|
|
17
|
+
assert len(kept) == 1
|
|
18
|
+
c = kept[0]
|
|
19
|
+
assert c.pattern.signature() == "(Cross_Incomplete_NoPressure Shot_Goal_Pressure)"
|
|
20
|
+
assert c.N == 14
|
|
21
|
+
assert abs(c.fdr_q - 0.006) < 1e-3 # MC p is deterministic at fixed seed
|
|
22
|
+
|
|
23
|
+
def test_detection_is_deterministic():
|
|
24
|
+
obs, _ = make_dataset(SyntheticSpec(seed=7))
|
|
25
|
+
a = {p.signature(): p.N for p in Engine(obs, Config()).detect()}
|
|
26
|
+
b = {p.signature(): p.N for p in Engine(obs, Config()).detect()}
|
|
27
|
+
assert a == b
|
|
28
|
+
|
|
29
|
+
def test_no_occurrence_duplicates_after_collapse():
|
|
30
|
+
obs = read_table(DATA / "worldcup_nongoals.csv", obs_start="obs_start",
|
|
31
|
+
obs_end="obs_end", time_unit="ms")
|
|
32
|
+
pats = [p for p in Engine(obs, Config()).detect() if p.level >= 1]
|
|
33
|
+
keys = [frozenset((i.obs, tuple(sorted(i.tokens))) for i in p.instances) for p in pats]
|
|
34
|
+
assert len(keys) == len(set(keys)) # one representative per occurrence set
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""group_contrast must recover a pattern planted more heavily in one group, with
|
|
2
|
+
an odds ratio > 1 and a small p-value (ground truth)."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
8
|
+
from tpattern import group_contrast # noqa: E402
|
|
9
|
+
from tpattern.synthetic import SyntheticSpec, make_dataset # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _labelled_sample():
|
|
13
|
+
hi, _ = make_dataset(SyntheticSpec(n_seq=120, planted=[("A", 0), ("B", 1500)],
|
|
14
|
+
plant_fraction=0.40, seed=1))
|
|
15
|
+
lo, _ = make_dataset(SyntheticSpec(n_seq=120, planted=[("A", 0), ("B", 1500)],
|
|
16
|
+
plant_fraction=0.05, seed=2))
|
|
17
|
+
for o in hi:
|
|
18
|
+
o.name = "hi_" + o.name
|
|
19
|
+
for o in lo:
|
|
20
|
+
o.name = "lo_" + o.name
|
|
21
|
+
return hi + lo
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_group_contrast_recovers_planted_difference():
|
|
25
|
+
obs = _labelled_sample()
|
|
26
|
+
rows = group_contrast(obs, group_of=lambda o: "high" if o.name.startswith("hi")
|
|
27
|
+
else "low")
|
|
28
|
+
planted = next(r for r in rows if r["signature"] == "(A B)")
|
|
29
|
+
assert planted["odds_ratio"] > 2.0
|
|
30
|
+
assert planted["p"] < 0.01
|
|
31
|
+
assert planted["more_common_in"] == "high"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_group_contrast_requires_two_groups():
|
|
35
|
+
obs = _labelled_sample()
|
|
36
|
+
try:
|
|
37
|
+
group_contrast(obs, group_of=lambda o: "only_one")
|
|
38
|
+
assert False, "should reject a single group"
|
|
39
|
+
except ValueError:
|
|
40
|
+
pass
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Occurrence-identical patterns (co-timed reversals, equivalent bracketings) must
|
|
2
|
+
collapse to one representative when collapse_equivalent is on, so the pattern count
|
|
3
|
+
and the multiple-comparison family reflect distinct hypotheses (panel Essential 1)."""
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
7
|
+
from tpattern import read_table, Config, Engine # noqa: E402
|
|
8
|
+
|
|
9
|
+
DATA = Path(__file__).resolve().parents[1] / "data"
|
|
10
|
+
|
|
11
|
+
def _occ_keys(pats):
|
|
12
|
+
return [frozenset((i.obs, tuple(sorted(i.tokens))) for i in p.instances)
|
|
13
|
+
for p in pats if p.level >= 1]
|
|
14
|
+
|
|
15
|
+
def test_collapse_removes_occurrence_identical_duplicates():
|
|
16
|
+
obs = read_table(DATA / "worldcup_nongoals.csv", obs_start="obs_start",
|
|
17
|
+
obs_end="obs_end", time_unit="ms")
|
|
18
|
+
on = [p for p in Engine(obs, Config(min_lag=0, collapse_equivalent=True)).detect()
|
|
19
|
+
if p.level >= 1]
|
|
20
|
+
off = [p for p in Engine(obs, Config(min_lag=0, collapse_equivalent=False)).detect()
|
|
21
|
+
if p.level >= 1]
|
|
22
|
+
keys_on = _occ_keys(on)
|
|
23
|
+
# with collapse on, every composite has a distinct occurrence set
|
|
24
|
+
assert len(keys_on) == len(set(keys_on)), "duplicates survived collapse"
|
|
25
|
+
# collapse actually removed duplicates that existed without it
|
|
26
|
+
assert len(on) < len(off), "expected duplicates to be present without collapse"
|
|
27
|
+
|
|
28
|
+
def test_collapse_does_not_touch_headline():
|
|
29
|
+
obs = read_table(DATA / "worldcup_goals.csv", obs_start="obs_start",
|
|
30
|
+
obs_end="obs_end", time_unit="ms")
|
|
31
|
+
pats = [p for p in Engine(obs, Config(min_lag=1)).detect() if p.level >= 1]
|
|
32
|
+
keys = _occ_keys(pats)
|
|
33
|
+
assert len(keys) == len(set(keys)) # headline path has no duplicates anyway
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The guided wizard's engine (run_analysis) must complete end-to-end and write
|
|
2
|
+
the expected outputs. The widget layer (launch) is UI-only and not tested here."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
10
|
+
from tpattern import run_analysis # noqa: E402
|
|
11
|
+
|
|
12
|
+
DATA = Path(__file__).resolve().parents[1] / "data" / "worldcup_goals.csv"
|
|
13
|
+
|
|
14
|
+
pytestmark = pytest.mark.skipif(not DATA.exists(), reason="shipped data not present")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_run_analysis_end_to_end(tmp_path):
|
|
18
|
+
res = run_analysis(str(DATA), obs_start="obs_start", obs_end="obs_end",
|
|
19
|
+
time_unit="ms", null="profile", min_lag=1, B=50,
|
|
20
|
+
outdir=str(tmp_path / "out"), title="test")
|
|
21
|
+
# it read the 163 goal sequences and captured a recommendation
|
|
22
|
+
assert "163 observations" in res.recommendation
|
|
23
|
+
assert res.n_detected > 0
|
|
24
|
+
# it produced a table, a summary and a Methods statement
|
|
25
|
+
assert Path(res.files["table"]).exists()
|
|
26
|
+
assert Path(res.files["summary"]).exists()
|
|
27
|
+
assert "T-pattern detection was performed with tpattern" in res.methods
|
|
28
|
+
assert "min_lag = 1" in res.methods # the chosen setting is documented
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reproduction test — the anchor for the paper's reproducibility claim.
|
|
3
|
+
|
|
4
|
+
The detector must reproduce THEME's published occurrence counts for the World Cup
|
|
5
|
+
Goals sample to the exact N. These anchors were read from the audited manuscript.
|
|
6
|
+
The derived data ships in data/, so this runs everywhere (including CI) rather
|
|
7
|
+
than being skipped.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
16
|
+
from tpattern import read_table, Engine, Config # noqa: E402
|
|
17
|
+
|
|
18
|
+
DATA = Path(__file__).resolve().parents[1] / "data" / "worldcup_goals.csv"
|
|
19
|
+
|
|
20
|
+
# (A, B, expected N) — from the audited World Cup manuscript (Goals sample).
|
|
21
|
+
ANCHORS = [
|
|
22
|
+
("Challenge_Pressure", "Shot_Goal_Pressure", 21),
|
|
23
|
+
("Challenge_Pressure", "Pass_Complete_Pressure", 21),
|
|
24
|
+
("Cross_Incomplete_NoPressure", "Shot_Goal_Pressure", 14),
|
|
25
|
+
("Cross_Incomplete_NoPressure", "Shot_Goal_NoPressure", 8),
|
|
26
|
+
("Shot_Goal_Pressure", "Challenge_Pressure", 16),
|
|
27
|
+
("Shot_Goal_NoPressure", "Challenge_Pressure", 7),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(scope="module")
|
|
32
|
+
def goals_patterns():
|
|
33
|
+
obs = read_table(DATA, obs_start="obs_start", obs_end="obs_end", time_unit="ms")
|
|
34
|
+
assert len(obs) == 163, "expected the 163 goal sequences"
|
|
35
|
+
return {p.signature(): p for p in Engine(obs, Config()).detect()}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize("a,b,expected", ANCHORS)
|
|
39
|
+
def test_level1_anchor_counts(goals_patterns, a, b, expected):
|
|
40
|
+
sig = f"({a} {b})"
|
|
41
|
+
assert sig in goals_patterns, f"pattern {a} -> {b} not detected"
|
|
42
|
+
assert goals_patterns[sig].N == expected
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_univariate_count_matches_theme(goals_patterns):
|
|
46
|
+
# THEME reported 19 univariate (Level 0) event types for Goals.
|
|
47
|
+
l0 = [p for p in goals_patterns.values() if p.level == 0]
|
|
48
|
+
assert len(l0) == 19
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_key_trivariate_present(goals_patterns):
|
|
52
|
+
sig = "(Cross_Incomplete_NoPressure (Challenge_Pressure Shot_Goal_Pressure))"
|
|
53
|
+
assert sig in goals_patterns
|
|
54
|
+
assert goals_patterns[sig].N == 6
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_observation_window_is_preserved():
|
|
58
|
+
"""The NX/T baseline depends on the observation window, so the shipped data
|
|
59
|
+
must carry it explicitly rather than have it inferred from the events."""
|
|
60
|
+
obs = read_table(DATA, obs_start="obs_start", obs_end="obs_end", time_unit="ms")
|
|
61
|
+
assert sum(max(o.T, 1) for o in obs) == 1064634
|