tpattern 0.1.7__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.
Files changed (35) hide show
  1. {tpattern-0.1.7/tpattern.egg-info → tpattern-0.1.8}/PKG-INFO +1 -1
  2. {tpattern-0.1.7 → tpattern-0.1.8}/pyproject.toml +1 -1
  3. tpattern-0.1.8/tests/test_sofcoder.py +67 -0
  4. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/__init__.py +3 -3
  5. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/io.py +86 -0
  6. {tpattern-0.1.7 → tpattern-0.1.8/tpattern.egg-info}/PKG-INFO +1 -1
  7. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern.egg-info/SOURCES.txt +1 -0
  8. {tpattern-0.1.7 → tpattern-0.1.8}/LICENSE +0 -0
  9. {tpattern-0.1.7 → tpattern-0.1.8}/README.md +0 -0
  10. {tpattern-0.1.7 → tpattern-0.1.8}/setup.cfg +0 -0
  11. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_calibrated_pins.py +0 -0
  12. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_contrast.py +0 -0
  13. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_dedup.py +0 -0
  14. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_guided.py +0 -0
  15. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_null_compare.py +0 -0
  16. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_report_dendrogram.py +0 -0
  17. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_reproduction.py +0 -0
  18. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_support_reporting.py +0 -0
  19. {tpattern-0.1.7 → tpattern-0.1.8}/tests/test_synthetic.py +0 -0
  20. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/advisor.py +0 -0
  21. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/ci.py +0 -0
  22. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/contrast.py +0 -0
  23. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/detect.py +0 -0
  24. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/diagnostics.py +0 -0
  25. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/guided.py +0 -0
  26. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/methods.py +0 -0
  27. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/pattern.py +0 -0
  28. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/randomise.py +0 -0
  29. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/report.py +0 -0
  30. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/significance.py +0 -0
  31. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/synthetic.py +0 -0
  32. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern/viz.py +0 -0
  33. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern.egg-info/dependency_links.txt +0 -0
  34. {tpattern-0.1.7 → tpattern-0.1.8}/tpattern.egg-info/requires.txt +0 -0
  35. {tpattern-0.1.7 → tpattern-0.1.8}/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.7
3
+ Version: 0.1.8
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tpattern"
7
- version = "0.1.7"
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.7" # defined before submodule imports (methods.py reads it)
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",
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tpattern
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -8,6 +8,7 @@ tests/test_guided.py
8
8
  tests/test_null_compare.py
9
9
  tests/test_report_dendrogram.py
10
10
  tests/test_reproduction.py
11
+ tests/test_sofcoder.py
11
12
  tests/test_support_reporting.py
12
13
  tests/test_synthetic.py
13
14
  tpattern/__init__.py
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