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.
- {tpattern-0.1.9/tpattern.egg-info → tpattern-0.1.11}/PKG-INFO +1 -1
- {tpattern-0.1.9 → tpattern-0.1.11}/pyproject.toml +1 -1
- tpattern-0.1.11/tests/test_outputs.py +86 -0
- tpattern-0.1.11/tests/test_read_table_validation.py +121 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/__init__.py +11 -5
- tpattern-0.1.11/tpattern/io.py +679 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/methods.py +10 -0
- tpattern-0.1.11/tpattern/report.py +405 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/viz.py +126 -6
- {tpattern-0.1.9 → tpattern-0.1.11/tpattern.egg-info}/PKG-INFO +1 -1
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern.egg-info/SOURCES.txt +2 -0
- tpattern-0.1.9/tpattern/io.py +0 -303
- tpattern-0.1.9/tpattern/report.py +0 -220
- {tpattern-0.1.9 → tpattern-0.1.11}/LICENSE +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/README.md +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/setup.cfg +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_calibrated_pins.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_contrast.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_dedup.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_guided.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_null_compare.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_report_dendrogram.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_reproduction.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_sofcoder.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_support_reporting.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tests/test_synthetic.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/advisor.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/ci.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/contrast.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/detect.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/diagnostics.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/guided.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/pattern.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/randomise.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/significance.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern/synthetic.py +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern.egg-info/dependency_links.txt +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/tpattern.egg-info/requires.txt +0 -0
- {tpattern-0.1.9 → tpattern-0.1.11}/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.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
|
+
__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,
|
|
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",
|