tpattern 0.1.9__tar.gz → 0.1.11__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 (39) hide show
  1. {tpattern-0.1.9/tpattern.egg-info → tpattern-0.1.11}/PKG-INFO +1 -1
  2. {tpattern-0.1.9 → tpattern-0.1.11}/pyproject.toml +1 -1
  3. tpattern-0.1.11/tests/test_outputs.py +86 -0
  4. tpattern-0.1.11/tests/test_read_table_validation.py +121 -0
  5. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/__init__.py +11 -5
  6. tpattern-0.1.11/tpattern/io.py +679 -0
  7. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/methods.py +10 -0
  8. tpattern-0.1.11/tpattern/report.py +405 -0
  9. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/viz.py +126 -6
  10. {tpattern-0.1.9 → tpattern-0.1.11/tpattern.egg-info}/PKG-INFO +1 -1
  11. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern.egg-info/SOURCES.txt +2 -0
  12. tpattern-0.1.9/tpattern/io.py +0 -303
  13. tpattern-0.1.9/tpattern/report.py +0 -220
  14. {tpattern-0.1.9 → tpattern-0.1.11}/LICENSE +0 -0
  15. {tpattern-0.1.9 → tpattern-0.1.11}/README.md +0 -0
  16. {tpattern-0.1.9 → tpattern-0.1.11}/setup.cfg +0 -0
  17. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_calibrated_pins.py +0 -0
  18. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_contrast.py +0 -0
  19. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_dedup.py +0 -0
  20. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_guided.py +0 -0
  21. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_null_compare.py +0 -0
  22. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_report_dendrogram.py +0 -0
  23. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_reproduction.py +0 -0
  24. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_sofcoder.py +0 -0
  25. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_support_reporting.py +0 -0
  26. {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_synthetic.py +0 -0
  27. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/advisor.py +0 -0
  28. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/ci.py +0 -0
  29. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/contrast.py +0 -0
  30. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/detect.py +0 -0
  31. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/diagnostics.py +0 -0
  32. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/guided.py +0 -0
  33. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/pattern.py +0 -0
  34. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/randomise.py +0 -0
  35. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/significance.py +0 -0
  36. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/synthetic.py +0 -0
  37. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern.egg-info/dependency_links.txt +0 -0
  38. {tpattern-0.1.9 → tpattern-0.1.11}/tpattern.egg-info/requires.txt +0 -0
  39. {tpattern-0.1.9 → tpattern-0.1.11}/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.9
3
+ Version: 0.1.11
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.9"
7
+ version = "0.1.11"
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,86 @@
1
+ """Tests for the 0.1.11 output layer: per-pattern timing, and the THEME-style
2
+ tables (durations, event-type roles, connections, data summary, interval counts)."""
3
+ import os
4
+ from tpattern import (Observation, Engine, Config, calibrate, patterns_table,
5
+ duration_stats, pattern_durations, event_type_roles,
6
+ connections, data_summary, interval_counts, report,
7
+ occurrence_plot, duration_plot)
8
+
9
+
10
+ def sample():
11
+ # A -> B at a fixed 300 ms lag in every observation; half also carry a later C, so
12
+ # (A B) survives standalone (not fully absorbed) alongside the longer ((A B) C).
13
+ obs = []
14
+ for i in range(8):
15
+ ev = [(0, "A"), (300, "B")]
16
+ if i < 4:
17
+ ev.append((600, "C"))
18
+ obs.append(Observation(name=f"o{i}", start=0, end=1000, events=ev))
19
+ return obs
20
+
21
+
22
+ def patterns():
23
+ return Engine(sample(), Config(min_lag=1)).detect()
24
+
25
+
26
+ def test_duration_stats_from_instances():
27
+ p = next(p for p in patterns() if p.signature() == "(A B)")
28
+ d = duration_stats(p)
29
+ assert d["n"] == 8 and d["median"] == 300 and d["min"] == 300 and d["max"] == 300
30
+
31
+
32
+ def test_patterns_table_has_timing_columns():
33
+ rows = patterns_table(patterns(), n_observations=8)
34
+ ab = next(r for r in rows if r["signature"] == "(A B)")
35
+ assert ab["duration_median_ms"] == 300
36
+ assert ab["critical_interval"].endswith("ms") # unit is labelled
37
+ assert ab["duration_range_ms"] == "[300,300]"
38
+
39
+
40
+ def test_pattern_durations_one_row_per_occurrence():
41
+ rows = pattern_durations(patterns())
42
+ ab = [r for r in rows if r["signature"] == "(A B)"]
43
+ assert len(ab) == 8
44
+ assert all(r["duration_ms"] == r["end_ms"] - r["start_ms"] for r in rows)
45
+
46
+
47
+ def test_event_type_roles_first_and_last():
48
+ roles = {r["event_type"]: r for r in event_type_roles(patterns())}
49
+ # A only ever starts a pattern; C only ever ends one
50
+ assert roles["A"]["pct_first"] == 1.0 and roles["A"]["as_last"] == 0
51
+ assert roles["C"]["pct_last"] == 1.0 and roles["C"]["as_first"] == 0
52
+
53
+
54
+ def test_connections_direction():
55
+ conn = {r["event_type"]: r for r in connections(patterns())}
56
+ assert "B" in conn["A"]["followed_by"] and "C" in conn["A"]["followed_by"]
57
+ assert conn["A"]["n_followed_by"] >= 2
58
+
59
+
60
+ def test_data_summary_counts():
61
+ rows = data_summary(sample())
62
+ a = next(r for r in rows if r["event_type"] == "A")
63
+ assert a["count"] == 8 and a["observations"] == 8 and a["mean_per_observation"] == 1.0
64
+
65
+
66
+ def test_interval_counts_split():
67
+ rows = interval_counts(patterns(), [(0, 100), (100, 1000)], split_time=100)
68
+ ab = next(r for r in rows if r["signature"] == "(A B)")
69
+ # every A->B occurrence starts at t=0, so all are in [0,100] and before the split
70
+ assert ab["in[0,100]"] == 8 and ab["before_split"] == 8 and ab["after_split"] == 0
71
+
72
+
73
+ def test_report_writes_all_default_outputs(tmp_path):
74
+ res = calibrate(sample(), Config(min_lag=1), null="profile", B=50, q_target=0.05, seed=1)
75
+ w = report(res, str(tmp_path), observations=sample(), title="unit test")
76
+ for key in ("table", "durations", "roles", "connections", "data_summary",
77
+ "dendrograms", "durations_plot", "occurrence_plot", "summary"):
78
+ assert key in w and os.path.getsize(w[key]) > 0
79
+
80
+
81
+ def test_plots_render(tmp_path):
82
+ res = calibrate(sample(), Config(min_lag=1), null="profile", B=50, q_target=0.05, seed=1)
83
+ f1 = str(tmp_path / "occ.png"); f2 = str(tmp_path / "dur.png")
84
+ occurrence_plot(sample(), f1)
85
+ duration_plot(res, f2)
86
+ assert os.path.getsize(f1) > 0 and os.path.getsize(f2) > 0
@@ -0,0 +1,121 @@
1
+ """Data-check tests for read_table: the E/W/I codes, and that the clean path
2
+ stays silent and unchanged. See tpattern/io.py CHECKS."""
3
+ import pytest
4
+ from tpattern import read_table, DataFormatError, CHECKS, check_reference
5
+
6
+
7
+ def write(tmp_path, name, text):
8
+ p = tmp_path / name
9
+ p.write_text(text)
10
+ return str(p)
11
+
12
+
13
+ # ---- E codes: raise, nothing loaded ----
14
+
15
+ def test_E01_missing_column(tmp_path):
16
+ f = write(tmp_path, "m.csv", "observation,event,time\np1,pass,0\np1,shot,3\n")
17
+ with pytest.raises(DataFormatError) as e:
18
+ read_table(f)
19
+ assert "E01" in str(e.value) and "start" in str(e.value)
20
+
21
+
22
+ def test_E01_suggests_close_column(tmp_path):
23
+ f = write(tmp_path, "m.csv", "observation,event,Start\np1,pass,0\np1,shot,3\n")
24
+ with pytest.raises(DataFormatError) as e:
25
+ read_table(f)
26
+ assert "Start" in str(e.value) # suggested the near-match
27
+
28
+
29
+ def test_E02_wrong_separator(tmp_path):
30
+ f = write(tmp_path, "s.csv", "observation;event;start\np1;pass;0\np1;shot;3\n")
31
+ with pytest.raises(DataFormatError) as e:
32
+ read_table(f)
33
+ assert "E02" in str(e.value)
34
+
35
+
36
+ def test_E03_header_only(tmp_path):
37
+ f = write(tmp_path, "h.csv", "observation,event,start\n")
38
+ with pytest.raises(DataFormatError) as e:
39
+ read_table(f)
40
+ assert "E03" in str(e.value)
41
+
42
+
43
+ def test_E04_all_rows_blank(tmp_path):
44
+ f = write(tmp_path, "b.csv", "observation,event,start\n,pass,0\n,shot,3\n")
45
+ with pytest.raises(DataFormatError) as e:
46
+ read_table(f)
47
+ assert "E04" in str(e.value)
48
+
49
+
50
+ def test_E05_no_pattern_capable_observation(tmp_path):
51
+ # every observation has exactly one event
52
+ f = write(tmp_path, "one.csv",
53
+ "observation,event,start\np1,pass,0\np2,shot,0\np3,cross,0\n")
54
+ with pytest.raises(DataFormatError) as e:
55
+ read_table(f)
56
+ assert "E05" in str(e.value)
57
+
58
+
59
+ # ---- W codes: load anyway, reported ----
60
+
61
+ def test_W10_non_numeric_times_dropped(tmp_path, capsys):
62
+ f = write(tmp_path, "c.csv",
63
+ "observation,event,start\np1,pass,0\np1,cross,01:03\np1,shot,5\n"
64
+ "p2,pass,1\np2,shot,4\n")
65
+ obs = read_table(f) # default report: a W fired, so it prints
66
+ out = capsys.readouterr().out
67
+ assert "W10" in out and "left out" in out
68
+ # the one good p1 row + p2 both loaded; the mm:ss row dropped
69
+ assert sum(len(o.events) for o in obs) == 4
70
+
71
+
72
+ def test_W13_near_duplicate_labels(tmp_path, capsys):
73
+ f = write(tmp_path, "d.csv",
74
+ "observation,event,start\np1,Pass,0\np1,pass,1\np1,shot,2\n"
75
+ "p2,Pass,0\np2,shot,2\n")
76
+ read_table(f)
77
+ assert "W13" in capsys.readouterr().out
78
+
79
+
80
+ def test_clean_file_is_silent(tmp_path, capsys):
81
+ f = write(tmp_path, "ok.csv",
82
+ "observation,event,start\np1,pass,0\np1,shot,2\np2,pass,0\np2,shot,3\n")
83
+ obs = read_table(f)
84
+ assert capsys.readouterr().out == "" # nothing to warn about
85
+ assert len(obs) == 2
86
+
87
+
88
+ # ---- I codes: only shown with report="all" ----
89
+
90
+ def test_I20_effective_sample_only_with_all(tmp_path, capsys):
91
+ f = write(tmp_path, "e.csv",
92
+ "observation,event,start\np1,pass,0\np1,shot,2\np2,pass,0\n") # p2 has 1 event
93
+ read_table(f) # default: I-only, stays silent
94
+ assert capsys.readouterr().out == ""
95
+ read_table(f, report="all") # report='all' surfaces the info note
96
+ assert "I20" in capsys.readouterr().out
97
+
98
+
99
+ def test_report_false_silences_warnings(tmp_path, capsys):
100
+ f = write(tmp_path, "c.csv",
101
+ "observation,event,start\np1,pass,0\np1,x,bad\np1,shot,5\np2,a,0\np2,b,1\n")
102
+ read_table(f, report=False)
103
+ assert capsys.readouterr().out == ""
104
+
105
+
106
+ # ---- BOM (Excel) handled ----
107
+
108
+ def test_utf8_bom_is_stripped(tmp_path):
109
+ p = tmp_path / "bom.csv"
110
+ p.write_bytes("observation,event,start\np1,pass,0\np1,shot,2\n".encode("utf-8"))
111
+ obs = read_table(str(p)) # must not see the BOM as part of the column name
112
+ assert len(obs) == 1 and len(obs[0].events) == 2
113
+
114
+
115
+ # ---- registry integrity (Appendix A generates from this) ----
116
+
117
+ def test_check_reference_covers_every_code():
118
+ rows = check_reference()
119
+ assert {r["code"] for r in rows} == set(CHECKS)
120
+ for r in rows:
121
+ assert r["condition"] and r["fix"] and r["severity"]
@@ -6,17 +6,20 @@ 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" # defined before submodule imports (methods.py reads it)
9
+ __version__ = "0.1.11" # defined before submodule imports (methods.py reads it)
10
10
 
11
- from .io import Observation, read_observation, read_sample, read_table, read_sofcoder
11
+ from .io import (Observation, read_observation, read_sample, read_table,
12
+ read_sofcoder, DataFormatError, check_reference, CHECKS)
12
13
  from .pattern import Instance, Pattern
13
14
  from .ci import find_critical_interval, CIResult
14
15
  from .detect import Config, Engine
15
16
  from .randomise import run_null, NullResult, rotate, shuffle
16
17
  from .significance import calibrate, CalibrationResult, compare_nulls, NullComparison
17
- from .report import patterns_table, forest_plot, report
18
+ from .report import (patterns_table, forest_plot, report, interpret, duration_stats,
19
+ pattern_durations, event_type_roles, connections, data_summary,
20
+ interval_counts)
18
21
  from .viz import (pattern_dendrogram, patterns_overview, support_profile,
19
- null_comparison_plot)
22
+ null_comparison_plot, occurrence_plot, duration_plot, STYLE)
20
23
  from .advisor import recommend
21
24
  from .methods import methods_text
22
25
  from .guided import run_analysis, launch
@@ -24,12 +27,15 @@ from .contrast import group_contrast, contrast_items
24
27
 
25
28
  __all__ = [
26
29
  "Observation", "read_observation", "read_sample", "read_table", "read_sofcoder",
30
+ "DataFormatError", "check_reference", "CHECKS",
27
31
  "Instance", "Pattern",
28
32
  "find_critical_interval", "CIResult",
29
33
  "Config", "Engine",
30
34
  "run_null", "NullResult", "rotate", "shuffle",
31
35
  "calibrate", "CalibrationResult",
32
- "patterns_table", "forest_plot", "report",
36
+ "patterns_table", "forest_plot", "report", "interpret", "duration_stats",
37
+ "pattern_durations", "event_type_roles", "connections", "data_summary",
38
+ "interval_counts", "occurrence_plot", "duration_plot", "STYLE",
33
39
  "pattern_dendrogram", "patterns_overview", "support_profile",
34
40
  "recommend", "methods_text",
35
41
  "run_analysis", "launch",