tpattern 0.1.7__tar.gz → 0.1.9__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.7/tpattern.egg-info → tpattern-0.1.9}/PKG-INFO +1 -1
- {tpattern-0.1.7 → tpattern-0.1.9}/pyproject.toml +1 -1
- tpattern-0.1.9/tests/test_sofcoder.py +67 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/__init__.py +3 -3
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/io.py +96 -7
- {tpattern-0.1.7 → tpattern-0.1.9/tpattern.egg-info}/PKG-INFO +1 -1
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern.egg-info/SOURCES.txt +1 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/LICENSE +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/README.md +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/setup.cfg +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_calibrated_pins.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_contrast.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_dedup.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_guided.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_null_compare.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_report_dendrogram.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_reproduction.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_support_reporting.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_synthetic.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/advisor.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/ci.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/contrast.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/detect.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/diagnostics.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/guided.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/methods.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/pattern.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/randomise.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/report.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/significance.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/synthetic.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/viz.py +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern.egg-info/dependency_links.txt +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/tpattern.egg-info/requires.txt +0 -0
- {tpattern-0.1.7 → tpattern-0.1.9}/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.9"
|
|
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.9" # 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",
|
|
@@ -116,8 +116,8 @@ def read_sample(folder: str | Path, pattern: str = "*.txt") -> list[Observation]
|
|
|
116
116
|
|
|
117
117
|
def read_table(path: str | Path, *, observation: str = "observation",
|
|
118
118
|
event: str = "event", start: str = "start",
|
|
119
|
-
end: str | None =
|
|
120
|
-
obs_end: str | None =
|
|
119
|
+
end: str | None = "end", obs_start: str | None = "obs_start",
|
|
120
|
+
obs_end: str | None = "obs_end", build_event_from: list[str] | None = None,
|
|
121
121
|
time_unit: str = "s", sep: str = ",") -> list[Observation]:
|
|
122
122
|
"""Read the canonical flat event table (see SCHEMA.md) into a sample.
|
|
123
123
|
|
|
@@ -135,11 +135,14 @@ def read_table(path: str | Path, *, observation: str = "observation",
|
|
|
135
135
|
taken from the min/max event `start`.
|
|
136
136
|
obs_start, obs_end : str, optional
|
|
137
137
|
Columns giving the *observation window* (constant within an observation).
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
These default to the conventional column names ("obs_start"/"obs_end"), so a
|
|
139
|
+
table that includes them is read correctly with **no arguments**; they are
|
|
140
|
+
ignored silently if the columns are absent. Supply a window when the unit has
|
|
141
|
+
real bounds that extend beyond its first and last event — a possession, rally
|
|
142
|
+
or bout usually does. The window length T = obs_end - obs_start is the
|
|
143
|
+
denominator of the NX/T baseline, so deriving it from the events instead will
|
|
144
|
+
shift every baseline probability. If absent, the window falls back to the
|
|
145
|
+
first/last event time.
|
|
143
146
|
build_event_from : list[str], optional
|
|
144
147
|
If given, the event code is built by joining these columns with '_'
|
|
145
148
|
(for coders who keep the code split across descriptor columns) instead of
|
|
@@ -212,3 +215,89 @@ def read_table(path: str | Path, *, observation: str = "observation",
|
|
|
212
215
|
o_end = max(ends_by_obs[obs_id]) if ends_by_obs.get(obs_id) else evs[-1][0]
|
|
213
216
|
sample.append(Observation(name=obs_id, start=o_start, end=o_end, events=evs))
|
|
214
217
|
return sample
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def read_sofcoder(path: str | Path, *, dataname: str = "DATANAME",
|
|
221
|
+
t: str = "T", events: str = "Events", sep: str | None = None,
|
|
222
|
+
code_sep: str = ",", frames_per_second: float | None = None,
|
|
223
|
+
start_marker: str = ":", end_marker: str = "&") -> list[Observation]:
|
|
224
|
+
"""Read a SOF-CODER / THEME data file (DATANAME, T, Events) into a sample.
|
|
225
|
+
|
|
226
|
+
SOF-CODER (Jonsson et al., 2006) exports one row per time frame, each row
|
|
227
|
+
carrying several comma-separated category codes active at that frame (its
|
|
228
|
+
multi-criteria "field format"). This reader explodes each row so every code
|
|
229
|
+
becomes one event at time ``T``; codes that share a frame are co-timed, and
|
|
230
|
+
``min_lag``/``collapse_duplicates`` then decide how co-occurrence is treated.
|
|
231
|
+
Each ``DATANAME`` becomes one :class:`Observation`. Rows whose Events field is
|
|
232
|
+
the start (``:``) or end (``&``) marker set the observation window
|
|
233
|
+
``[start, end]`` (``T = end - start`` is the NX/T baseline denominator); if the
|
|
234
|
+
markers are absent the window falls back to the first/last event.
|
|
235
|
+
|
|
236
|
+
The outer column delimiter is auto-detected (tab if present, else any
|
|
237
|
+
whitespace) unless ``sep`` is given; ``code_sep`` splits the Events field
|
|
238
|
+
(default ``','``). ``T`` is kept in the file's native frame unit unless
|
|
239
|
+
``frames_per_second`` is supplied, in which case frames are converted to
|
|
240
|
+
milliseconds. A header row naming the columns is used if present, else the
|
|
241
|
+
columns are taken positionally as (DATANAME, T, Events).
|
|
242
|
+
|
|
243
|
+
This is the on-ramp for data collected in the SOF-CODER/THEME ecosystem; for
|
|
244
|
+
a coder that already exports one row per single event, use :func:`read_table`.
|
|
245
|
+
"""
|
|
246
|
+
path = Path(path)
|
|
247
|
+
scale = (1000.0 / frames_per_second) if frames_per_second else 1.0
|
|
248
|
+
|
|
249
|
+
def split_row(line: str) -> list[str]:
|
|
250
|
+
if sep is not None:
|
|
251
|
+
parts = line.split(sep)
|
|
252
|
+
elif "\t" in line:
|
|
253
|
+
parts = line.split("\t")
|
|
254
|
+
else:
|
|
255
|
+
parts = line.split(None, 2) # DATANAME, T, Events (Events holds no spaces)
|
|
256
|
+
return [p.strip() for p in parts]
|
|
257
|
+
|
|
258
|
+
with open(path, newline="") as fh:
|
|
259
|
+
lines = [ln.strip() for ln in fh if ln.strip()]
|
|
260
|
+
if not lines:
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
header = [h.lower() for h in split_row(lines[0])]
|
|
264
|
+
if dataname.lower() in header and events.lower() in header:
|
|
265
|
+
di, ti, ei = (header.index(dataname.lower()), header.index(t.lower()),
|
|
266
|
+
header.index(events.lower()))
|
|
267
|
+
body = lines[1:]
|
|
268
|
+
else:
|
|
269
|
+
di, ti, ei, body = 0, 1, 2, lines # positional fallback
|
|
270
|
+
|
|
271
|
+
rows_by_obs: dict[str, list] = defaultdict(list)
|
|
272
|
+
starts: dict[str, int] = {}
|
|
273
|
+
ends: dict[str, int] = {}
|
|
274
|
+
for line in body:
|
|
275
|
+
parts = split_row(line)
|
|
276
|
+
if len(parts) <= max(di, ti, ei):
|
|
277
|
+
continue
|
|
278
|
+
obs_id = parts[di]
|
|
279
|
+
if not obs_id:
|
|
280
|
+
continue
|
|
281
|
+
try:
|
|
282
|
+
tv = int(round(float(parts[ti]) * scale))
|
|
283
|
+
except (ValueError, TypeError):
|
|
284
|
+
continue
|
|
285
|
+
raw = parts[ei]
|
|
286
|
+
if raw == start_marker:
|
|
287
|
+
starts.setdefault(obs_id, tv)
|
|
288
|
+
continue
|
|
289
|
+
if raw == end_marker:
|
|
290
|
+
ends[obs_id] = tv
|
|
291
|
+
continue
|
|
292
|
+
for code in raw.split(code_sep):
|
|
293
|
+
code = code.strip()
|
|
294
|
+
if code and code not in (start_marker, end_marker):
|
|
295
|
+
rows_by_obs[obs_id].append((tv, code))
|
|
296
|
+
|
|
297
|
+
sample: list[Observation] = []
|
|
298
|
+
for obs_id, evs in rows_by_obs.items():
|
|
299
|
+
evs.sort(key=lambda e: e[0])
|
|
300
|
+
o_start = starts.get(obs_id, evs[0][0])
|
|
301
|
+
o_end = ends.get(obs_id, evs[-1][0])
|
|
302
|
+
sample.append(Observation(name=obs_id, start=o_start, end=o_end, events=evs))
|
|
303
|
+
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
|
|
File without changes
|