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.
Files changed (35) hide show
  1. {tpattern-0.1.7/tpattern.egg-info → tpattern-0.1.9}/PKG-INFO +1 -1
  2. {tpattern-0.1.7 → tpattern-0.1.9}/pyproject.toml +1 -1
  3. tpattern-0.1.9/tests/test_sofcoder.py +67 -0
  4. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/__init__.py +3 -3
  5. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/io.py +96 -7
  6. {tpattern-0.1.7 → tpattern-0.1.9/tpattern.egg-info}/PKG-INFO +1 -1
  7. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern.egg-info/SOURCES.txt +1 -0
  8. {tpattern-0.1.7 → tpattern-0.1.9}/LICENSE +0 -0
  9. {tpattern-0.1.7 → tpattern-0.1.9}/README.md +0 -0
  10. {tpattern-0.1.7 → tpattern-0.1.9}/setup.cfg +0 -0
  11. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_calibrated_pins.py +0 -0
  12. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_contrast.py +0 -0
  13. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_dedup.py +0 -0
  14. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_guided.py +0 -0
  15. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_null_compare.py +0 -0
  16. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_report_dendrogram.py +0 -0
  17. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_reproduction.py +0 -0
  18. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_support_reporting.py +0 -0
  19. {tpattern-0.1.7 → tpattern-0.1.9}/tests/test_synthetic.py +0 -0
  20. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/advisor.py +0 -0
  21. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/ci.py +0 -0
  22. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/contrast.py +0 -0
  23. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/detect.py +0 -0
  24. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/diagnostics.py +0 -0
  25. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/guided.py +0 -0
  26. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/methods.py +0 -0
  27. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/pattern.py +0 -0
  28. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/randomise.py +0 -0
  29. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/report.py +0 -0
  30. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/significance.py +0 -0
  31. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/synthetic.py +0 -0
  32. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern/viz.py +0 -0
  33. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern.egg-info/dependency_links.txt +0 -0
  34. {tpattern-0.1.7 → tpattern-0.1.9}/tpattern.egg-info/requires.txt +0 -0
  35. {tpattern-0.1.7 → tpattern-0.1.9}/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.9
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.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.7" # defined before submodule imports (methods.py reads it)
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 = None, obs_start: str | None = None,
120
- obs_end: str | None = None, build_event_from: list[str] | None = 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
- Supply these when the unit has real bounds that extend beyond its first and
139
- last event — a possession, rally or bout usually does. The window length
140
- T = obs_end - obs_start is the denominator of the NX/T baseline, so deriving
141
- it from the events instead will shift every baseline probability. If absent,
142
- the window falls back to the first/last event time.
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tpattern
3
- Version: 0.1.7
3
+ Version: 0.1.9
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