tpattern 0.1.6__tar.gz → 0.1.8__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.6/tpattern.egg-info → tpattern-0.1.8}/PKG-INFO +1 -1
- {tpattern-0.1.6 → tpattern-0.1.8}/pyproject.toml +1 -1
- tpattern-0.1.8/tests/test_sofcoder.py +67 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/__init__.py +3 -3
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/advisor.py +47 -4
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/io.py +86 -0
- {tpattern-0.1.6 → tpattern-0.1.8/tpattern.egg-info}/PKG-INFO +1 -1
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern.egg-info/SOURCES.txt +1 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/LICENSE +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/README.md +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/setup.cfg +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_calibrated_pins.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_contrast.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_dedup.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_guided.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_null_compare.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_report_dendrogram.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_reproduction.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_support_reporting.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tests/test_synthetic.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/ci.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/contrast.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/detect.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/diagnostics.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/guided.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/methods.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/pattern.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/randomise.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/report.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/significance.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/synthetic.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern/viz.py +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern.egg-info/dependency_links.txt +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern.egg-info/requires.txt +0 -0
- {tpattern-0.1.6 → tpattern-0.1.8}/tpattern.egg-info/top_level.txt +0 -0
|
@@ -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.8"
|
|
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,67 @@
|
|
|
1
|
+
"""read_sofcoder: ingest a SOF-CODER / THEME DATANAME,T,Events file."""
|
|
2
|
+
from tpattern import read_sofcoder, Engine, Config
|
|
3
|
+
|
|
4
|
+
SAMPLE = (
|
|
5
|
+
"DATANAME\tT\tEvents\n"
|
|
6
|
+
"FCB-BETIS_FIRST\t1\t:\n"
|
|
7
|
+
"FCB-BETIS_FIRST\t375\tCE,UD,IR,RA\n"
|
|
8
|
+
"FCB-BETIS_FIRST\t650\tCE,UD,FT,RA\n"
|
|
9
|
+
"FCB-BETIS_FIRST\t3775\tCE,C,MM\n"
|
|
10
|
+
"FCB-BETIS_FIRST\t4000\t&\n"
|
|
11
|
+
"RM-VALENCIA\t1\t:\n"
|
|
12
|
+
"RM-VALENCIA\t500\tIZ,O,AR\n"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _write(tmp_path, text, name="sof.csv"):
|
|
17
|
+
p = tmp_path / name
|
|
18
|
+
p.write_text(text)
|
|
19
|
+
return str(p)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_sessions_and_explode(tmp_path):
|
|
23
|
+
obs = read_sofcoder(_write(tmp_path, SAMPLE))
|
|
24
|
+
assert [o.name for o in obs] == ["FCB-BETIS_FIRST", "RM-VALENCIA"]
|
|
25
|
+
fcb = obs[0]
|
|
26
|
+
# 3 event-rows exploded: 4 + 4 + 3 codes = 11 events (markers excluded)
|
|
27
|
+
assert len(fcb.events) == 11
|
|
28
|
+
# CE,UD,IR,RA all land at t=375 as co-timed events
|
|
29
|
+
assert sorted(c for t, c in fcb.events if t == 375) == ["CE", "IR", "RA", "UD"]
|
|
30
|
+
# no marker leaked into the events
|
|
31
|
+
assert all(c not in (":", "&") for _, c in fcb.events)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_window_from_markers(tmp_path):
|
|
35
|
+
# the : and & markers define the observation window (the NX/T denominator)
|
|
36
|
+
obs = read_sofcoder(_write(tmp_path, SAMPLE))
|
|
37
|
+
fcb = obs[0]
|
|
38
|
+
assert (fcb.start, fcb.end) == (1, 4000)
|
|
39
|
+
# a session without an & marker falls back to the last event
|
|
40
|
+
rm = obs[1]
|
|
41
|
+
assert rm.start == 1 and rm.end == 500
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_whitespace_delimiter(tmp_path):
|
|
45
|
+
text = "DATANAME T Events\nS1 1 :\nS1 100 A,B\nS1 200 A,C\nS1 300 &\n"
|
|
46
|
+
obs = read_sofcoder(_write(tmp_path, text, "ws.csv"))
|
|
47
|
+
assert obs[0].events == [(100, "A"), (100, "B"), (200, "A"), (200, "C")]
|
|
48
|
+
assert (obs[0].start, obs[0].end) == (1, 300)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_frames_per_second_scaling(tmp_path):
|
|
52
|
+
# frames -> ms: at 25 fps, frame 25 == 1000 ms
|
|
53
|
+
text = "DATANAME T Events\nS1 25 A,B\nS1 50 A\n"
|
|
54
|
+
obs = read_sofcoder(_write(tmp_path, text, "fps.csv"), frames_per_second=25)
|
|
55
|
+
assert obs[0].events[0][0] == 1000
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_flows_into_detection(tmp_path):
|
|
59
|
+
obs = read_sofcoder(_write(tmp_path, SAMPLE))
|
|
60
|
+
pats = Engine(obs, Config(min_lag=0)).detect()
|
|
61
|
+
assert isinstance(pats, list) # runs without error on SOF-CODER-sourced data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_positional_no_header(tmp_path):
|
|
65
|
+
text = "S1 10 A,B\nS1 20 C\n"
|
|
66
|
+
obs = read_sofcoder(_write(tmp_path, text, "noheader.csv"))
|
|
67
|
+
assert obs[0].name == "S1" and len(obs[0].events) == 3
|
|
@@ -6,9 +6,9 @@ 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.8" # defined before submodule imports (methods.py reads it)
|
|
10
10
|
|
|
11
|
-
from .io import Observation, read_observation, read_sample, read_table
|
|
11
|
+
from .io import Observation, read_observation, read_sample, read_table, read_sofcoder
|
|
12
12
|
from .pattern import Instance, Pattern
|
|
13
13
|
from .ci import find_critical_interval, CIResult
|
|
14
14
|
from .detect import Config, Engine
|
|
@@ -23,7 +23,7 @@ from .guided import run_analysis, launch
|
|
|
23
23
|
from .contrast import group_contrast, contrast_items
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
|
-
"Observation", "read_observation", "read_sample", "read_table",
|
|
26
|
+
"Observation", "read_observation", "read_sample", "read_table", "read_sofcoder",
|
|
27
27
|
"Instance", "Pattern",
|
|
28
28
|
"find_critical_interval", "CIResult",
|
|
29
29
|
"Config", "Engine",
|
|
@@ -50,12 +50,55 @@ class Recommendation:
|
|
|
50
50
|
notes: list[str] = field(default_factory=list)
|
|
51
51
|
|
|
52
52
|
def __str__(self) -> str:
|
|
53
|
-
|
|
53
|
+
"""Readable plain-text view: each setting, its recommended value, then the
|
|
54
|
+
wrapped reason. Kept ANSI/markup-free so it can be pasted verbatim."""
|
|
55
|
+
import textwrap
|
|
56
|
+
w = 76
|
|
57
|
+
|
|
58
|
+
def wrap(text, first, rest):
|
|
59
|
+
return textwrap.fill(text, width=w, initial_indent=first,
|
|
60
|
+
subsequent_indent=rest)
|
|
61
|
+
|
|
62
|
+
out = ["=" * w,
|
|
63
|
+
f"RECOMMENDED SETTINGS ({self.n_obs:,} observations, "
|
|
64
|
+
f"{self.n_events:,} events)",
|
|
65
|
+
"=" * w]
|
|
54
66
|
for c in self.choices:
|
|
55
|
-
|
|
67
|
+
out += ["", f" {c.option} -> {c.recommended}",
|
|
68
|
+
wrap(c.rationale, " ", " ")]
|
|
56
69
|
for n in self.notes:
|
|
57
|
-
|
|
58
|
-
return "\n".join(
|
|
70
|
+
out += ["", wrap(n, " * ", " ")]
|
|
71
|
+
return "\n".join(out)
|
|
72
|
+
|
|
73
|
+
def _repr_html_(self) -> str:
|
|
74
|
+
"""Rich rendering in notebooks (Colab/Jupyter): the recommended value in
|
|
75
|
+
bold, the setting name and explanation in normal weight. Shown when you
|
|
76
|
+
display the object (a bare `rec` or `display(rec)`); `print(rec)` still
|
|
77
|
+
gives the plain-text view above."""
|
|
78
|
+
import html
|
|
79
|
+
|
|
80
|
+
def esc(s):
|
|
81
|
+
return html.escape(str(s))
|
|
82
|
+
|
|
83
|
+
rows = "".join(
|
|
84
|
+
'<div style="margin:0 0 11px 0;">'
|
|
85
|
+
f'<div><span style="color:#6b7280;">{esc(c.option)}:</span> '
|
|
86
|
+
f'<strong style="color:#111;">{esc(c.recommended)}</strong></div>'
|
|
87
|
+
f'<div style="color:#4b5563;margin-top:2px;">{esc(c.rationale)}</div>'
|
|
88
|
+
'</div>'
|
|
89
|
+
for c in self.choices)
|
|
90
|
+
notes = ""
|
|
91
|
+
if self.notes:
|
|
92
|
+
lis = "".join(f'<li style="color:#4b5563;margin:2px 0;">{esc(n)}</li>'
|
|
93
|
+
for n in self.notes)
|
|
94
|
+
notes = f'<ul style="margin:6px 0 0 0;padding-left:18px;">{lis}</ul>'
|
|
95
|
+
return (
|
|
96
|
+
'<div style="font-family:system-ui,-apple-system,Segoe UI,sans-serif;'
|
|
97
|
+
'font-size:13px;line-height:1.5;max-width:780px;">'
|
|
98
|
+
'<div style="font-weight:600;font-size:14px;margin-bottom:8px;'
|
|
99
|
+
'padding-bottom:5px;border-bottom:1px solid #e5e7eb;">'
|
|
100
|
+
f'Recommended settings · {self.n_obs:,} observations, '
|
|
101
|
+
f'{self.n_events:,} events</div>{rows}{notes}</div>')
|
|
59
102
|
|
|
60
103
|
def methods_text(self) -> str:
|
|
61
104
|
"""A ready-to-adapt Methods paragraph, grounded in the measured values."""
|
|
@@ -212,3 +212,89 @@ def read_table(path: str | Path, *, observation: str = "observation",
|
|
|
212
212
|
o_end = max(ends_by_obs[obs_id]) if ends_by_obs.get(obs_id) else evs[-1][0]
|
|
213
213
|
sample.append(Observation(name=obs_id, start=o_start, end=o_end, events=evs))
|
|
214
214
|
return sample
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def read_sofcoder(path: str | Path, *, dataname: str = "DATANAME",
|
|
218
|
+
t: str = "T", events: str = "Events", sep: str | None = None,
|
|
219
|
+
code_sep: str = ",", frames_per_second: float | None = None,
|
|
220
|
+
start_marker: str = ":", end_marker: str = "&") -> list[Observation]:
|
|
221
|
+
"""Read a SOF-CODER / THEME data file (DATANAME, T, Events) into a sample.
|
|
222
|
+
|
|
223
|
+
SOF-CODER (Jonsson et al., 2006) exports one row per time frame, each row
|
|
224
|
+
carrying several comma-separated category codes active at that frame (its
|
|
225
|
+
multi-criteria "field format"). This reader explodes each row so every code
|
|
226
|
+
becomes one event at time ``T``; codes that share a frame are co-timed, and
|
|
227
|
+
``min_lag``/``collapse_duplicates`` then decide how co-occurrence is treated.
|
|
228
|
+
Each ``DATANAME`` becomes one :class:`Observation`. Rows whose Events field is
|
|
229
|
+
the start (``:``) or end (``&``) marker set the observation window
|
|
230
|
+
``[start, end]`` (``T = end - start`` is the NX/T baseline denominator); if the
|
|
231
|
+
markers are absent the window falls back to the first/last event.
|
|
232
|
+
|
|
233
|
+
The outer column delimiter is auto-detected (tab if present, else any
|
|
234
|
+
whitespace) unless ``sep`` is given; ``code_sep`` splits the Events field
|
|
235
|
+
(default ``','``). ``T`` is kept in the file's native frame unit unless
|
|
236
|
+
``frames_per_second`` is supplied, in which case frames are converted to
|
|
237
|
+
milliseconds. A header row naming the columns is used if present, else the
|
|
238
|
+
columns are taken positionally as (DATANAME, T, Events).
|
|
239
|
+
|
|
240
|
+
This is the on-ramp for data collected in the SOF-CODER/THEME ecosystem; for
|
|
241
|
+
a coder that already exports one row per single event, use :func:`read_table`.
|
|
242
|
+
"""
|
|
243
|
+
path = Path(path)
|
|
244
|
+
scale = (1000.0 / frames_per_second) if frames_per_second else 1.0
|
|
245
|
+
|
|
246
|
+
def split_row(line: str) -> list[str]:
|
|
247
|
+
if sep is not None:
|
|
248
|
+
parts = line.split(sep)
|
|
249
|
+
elif "\t" in line:
|
|
250
|
+
parts = line.split("\t")
|
|
251
|
+
else:
|
|
252
|
+
parts = line.split(None, 2) # DATANAME, T, Events (Events holds no spaces)
|
|
253
|
+
return [p.strip() for p in parts]
|
|
254
|
+
|
|
255
|
+
with open(path, newline="") as fh:
|
|
256
|
+
lines = [ln.strip() for ln in fh if ln.strip()]
|
|
257
|
+
if not lines:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
header = [h.lower() for h in split_row(lines[0])]
|
|
261
|
+
if dataname.lower() in header and events.lower() in header:
|
|
262
|
+
di, ti, ei = (header.index(dataname.lower()), header.index(t.lower()),
|
|
263
|
+
header.index(events.lower()))
|
|
264
|
+
body = lines[1:]
|
|
265
|
+
else:
|
|
266
|
+
di, ti, ei, body = 0, 1, 2, lines # positional fallback
|
|
267
|
+
|
|
268
|
+
rows_by_obs: dict[str, list] = defaultdict(list)
|
|
269
|
+
starts: dict[str, int] = {}
|
|
270
|
+
ends: dict[str, int] = {}
|
|
271
|
+
for line in body:
|
|
272
|
+
parts = split_row(line)
|
|
273
|
+
if len(parts) <= max(di, ti, ei):
|
|
274
|
+
continue
|
|
275
|
+
obs_id = parts[di]
|
|
276
|
+
if not obs_id:
|
|
277
|
+
continue
|
|
278
|
+
try:
|
|
279
|
+
tv = int(round(float(parts[ti]) * scale))
|
|
280
|
+
except (ValueError, TypeError):
|
|
281
|
+
continue
|
|
282
|
+
raw = parts[ei]
|
|
283
|
+
if raw == start_marker:
|
|
284
|
+
starts.setdefault(obs_id, tv)
|
|
285
|
+
continue
|
|
286
|
+
if raw == end_marker:
|
|
287
|
+
ends[obs_id] = tv
|
|
288
|
+
continue
|
|
289
|
+
for code in raw.split(code_sep):
|
|
290
|
+
code = code.strip()
|
|
291
|
+
if code and code not in (start_marker, end_marker):
|
|
292
|
+
rows_by_obs[obs_id].append((tv, code))
|
|
293
|
+
|
|
294
|
+
sample: list[Observation] = []
|
|
295
|
+
for obs_id, evs in rows_by_obs.items():
|
|
296
|
+
evs.sort(key=lambda e: e[0])
|
|
297
|
+
o_start = starts.get(obs_id, evs[0][0])
|
|
298
|
+
o_end = ends.get(obs_id, evs[-1][0])
|
|
299
|
+
sample.append(Observation(name=obs_id, start=o_start, end=o_end, events=evs))
|
|
300
|
+
return sample
|
|
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
|