tpattern 0.1.10__tar.gz → 0.1.12__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 (40) hide show
  1. {tpattern-0.1.10/tpattern.egg-info → tpattern-0.1.12}/PKG-INFO +1 -1
  2. {tpattern-0.1.10 → tpattern-0.1.12}/pyproject.toml +1 -1
  3. tpattern-0.1.12/tests/test_outputs.py +86 -0
  4. tpattern-0.1.12/tests/test_reference.py +54 -0
  5. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/__init__.py +12 -4
  6. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/guided.py +2 -2
  7. tpattern-0.1.12/tpattern/reference.py +214 -0
  8. tpattern-0.1.12/tpattern/report.py +409 -0
  9. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/viz.py +126 -6
  10. {tpattern-0.1.10 → tpattern-0.1.12/tpattern.egg-info}/PKG-INFO +1 -1
  11. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern.egg-info/SOURCES.txt +3 -0
  12. tpattern-0.1.10/tpattern/report.py +0 -220
  13. {tpattern-0.1.10 → tpattern-0.1.12}/LICENSE +0 -0
  14. {tpattern-0.1.10 → tpattern-0.1.12}/README.md +0 -0
  15. {tpattern-0.1.10 → tpattern-0.1.12}/setup.cfg +0 -0
  16. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_calibrated_pins.py +0 -0
  17. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_contrast.py +0 -0
  18. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_dedup.py +0 -0
  19. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_guided.py +0 -0
  20. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_null_compare.py +0 -0
  21. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_read_table_validation.py +0 -0
  22. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_report_dendrogram.py +0 -0
  23. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_reproduction.py +0 -0
  24. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_sofcoder.py +0 -0
  25. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_support_reporting.py +0 -0
  26. {tpattern-0.1.10 → tpattern-0.1.12}/tests/test_synthetic.py +0 -0
  27. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/advisor.py +0 -0
  28. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/ci.py +0 -0
  29. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/contrast.py +0 -0
  30. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/detect.py +0 -0
  31. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/diagnostics.py +0 -0
  32. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/io.py +0 -0
  33. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/methods.py +0 -0
  34. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/pattern.py +0 -0
  35. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/randomise.py +0 -0
  36. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/significance.py +0 -0
  37. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern/synthetic.py +0 -0
  38. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern.egg-info/dependency_links.txt +0 -0
  39. {tpattern-0.1.10 → tpattern-0.1.12}/tpattern.egg-info/requires.txt +0 -0
  40. {tpattern-0.1.10 → tpattern-0.1.12}/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.10
3
+ Version: 0.1.12
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.10"
7
+ version = "0.1.12"
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,54 @@
1
+ """The reference registry: coverage, explain(), and the renamed output columns."""
2
+ from tpattern import (SETTINGS, COLUMNS, explain, explain_settings, explain_columns,
3
+ parameters_markdown, read_table, Config, calibrate,
4
+ patterns_table, Observation)
5
+
6
+
7
+ def test_every_setting_is_complete():
8
+ for s in SETTINGS:
9
+ for field in ("key", "form", "group", "does", "range", "recommend", "effect"):
10
+ assert s.get(field), f"{s.get('form')} missing {field}"
11
+ assert s["group"] in ("A", "B", "C")
12
+
13
+
14
+ def test_explain_by_key_and_form_name():
15
+ a = explain("min_lag")
16
+ b = explain("minimum_lag_frames")
17
+ assert a == b and "recommended" in a and "range" in a
18
+
19
+
20
+ def test_explain_column_and_unknown():
21
+ assert "Benjamini" in explain("fdr_q")
22
+ assert "No setting or column" in explain("does_not_exist")
23
+
24
+
25
+ def test_parameters_markdown_covers_all():
26
+ md = parameters_markdown()
27
+ for s in SETTINGS:
28
+ assert s["form"] in md
29
+ for c in COLUMNS:
30
+ assert c["key"] in md
31
+
32
+
33
+ def _sample():
34
+ obs = []
35
+ for i in range(6):
36
+ ev = [(0, "A"), (300, "B")] + ([(600, "C")] if i < 3 else [])
37
+ obs.append(Observation(name=f"o{i}", start=0, end=1000, events=ev))
38
+ return obs
39
+
40
+
41
+ def test_output_columns_renamed():
42
+ res = calibrate(_sample(), Config(min_lag=1), null="profile", B=50, q_target=.05, seed=1)
43
+ row = patterns_table(res)[0]
44
+ assert "monte_carlo_p" in row and "survives_fwer_holm" in row
45
+ assert "p_emp" not in row and "fwer_keep" not in row
46
+ # every reported column has a registry entry (the tutorial table generates from it)
47
+ keys = {c["key"] for c in COLUMNS}
48
+ for col in ("monte_carlo_p", "fdr_q", "survives_fwer_holm", "N", "level", "bout_support"):
49
+ assert col in keys
50
+
51
+
52
+ def test_sort_tolerates_old_name():
53
+ res = calibrate(_sample(), Config(min_lag=1), null="profile", B=50, q_target=.05, seed=1)
54
+ patterns_table(res, sort="p_emp") # must not raise
@@ -6,7 +6,7 @@ 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.10" # defined before submodule imports (methods.py reads it)
9
+ __version__ = "0.1.12" # defined before submodule imports (methods.py reads it)
10
10
 
11
11
  from .io import (Observation, read_observation, read_sample, read_table,
12
12
  read_sofcoder, DataFormatError, check_reference, CHECKS)
@@ -15,13 +15,17 @@ from .ci import find_critical_interval, CIResult
15
15
  from .detect import Config, Engine
16
16
  from .randomise import run_null, NullResult, rotate, shuffle
17
17
  from .significance import calibrate, CalibrationResult, compare_nulls, NullComparison
18
- 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)
19
21
  from .viz import (pattern_dendrogram, patterns_overview, support_profile,
20
- null_comparison_plot)
22
+ null_comparison_plot, occurrence_plot, duration_plot, STYLE)
21
23
  from .advisor import recommend
22
24
  from .methods import methods_text
23
25
  from .guided import run_analysis, launch
24
26
  from .contrast import group_contrast, contrast_items
27
+ from .reference import (explain, explain_settings, explain_columns,
28
+ parameters_markdown, SETTINGS, COLUMNS)
25
29
 
26
30
  __all__ = [
27
31
  "Observation", "read_observation", "read_sample", "read_table", "read_sofcoder",
@@ -31,9 +35,13 @@ __all__ = [
31
35
  "Config", "Engine",
32
36
  "run_null", "NullResult", "rotate", "shuffle",
33
37
  "calibrate", "CalibrationResult",
34
- "patterns_table", "forest_plot", "report",
38
+ "patterns_table", "forest_plot", "report", "interpret", "duration_stats",
39
+ "pattern_durations", "event_type_roles", "connections", "data_summary",
40
+ "interval_counts", "occurrence_plot", "duration_plot", "STYLE",
35
41
  "pattern_dendrogram", "patterns_overview", "support_profile",
36
42
  "recommend", "methods_text",
37
43
  "run_analysis", "launch",
38
44
  "group_contrast", "contrast_items",
45
+ "explain", "explain_settings", "explain_columns", "parameters_markdown",
46
+ "SETTINGS", "COLUMNS",
39
47
  ]
@@ -104,8 +104,8 @@ def _write_excel(result, q_target, path):
104
104
  rows.append({
105
105
  "pattern": str(c.pattern), "N": c.N, "level": c.level,
106
106
  "critical_interval": f"[{c.ci[0]},{c.ci[1]}]" if c.ci else "",
107
- "p_empirical": round(c.p_emp, 4), "q_FDR": round(c.fdr_q, 4),
108
- "survives_FDR": int(c.fdr_q <= q_target), "survives_FWER": int(c.fwer_keep),
107
+ "monte_carlo_p": round(c.p_emp, 4), "fdr_q": round(c.fdr_q, 4),
108
+ "survives_fdr": int(c.fdr_q <= q_target), "survives_fwer_holm": int(c.fwer_keep),
109
109
  })
110
110
  try:
111
111
  import pandas as pd
@@ -0,0 +1,214 @@
1
+ """
2
+ One reference for every setting and every output column.
3
+ =========================================================
4
+
5
+ The problem this solves: a user meets these names in three places — the Python
6
+ `Config`, the Colab settings form, and the results table — and needs, for each, the
7
+ same four things: what it *is*, what range it takes, what value is *recommended*, and
8
+ what *higher or lower* does to the analysis. Rather than scatter that across
9
+ docstrings, a paper table and the advisor, it lives here once. Everything else is
10
+ generated from it: :func:`explain`, the ``PARAMETERS.md`` reference, the tutorial's
11
+ settings and output-column tables, and the plain-language field labels in the Colab
12
+ form. So the code, the paper and the notebook cannot drift.
13
+
14
+ Each setting record has:
15
+ key the ``Config`` (or read_table/calibrate) argument name
16
+ form the friendly name shown in the Colab form and the paper
17
+ group A = the data can decide it (the advisor recommends);
18
+ B = your policy (the data cannot decide it);
19
+ C = a structural input fixed before analysis
20
+ does what it does to the detection, in one line
21
+ range the values it accepts
22
+ recommend the recommended value, and when
23
+ effect what higher / lower (or on / off) means
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ # --- settings ---------------------------------------------------------------
29
+ SETTINGS: list[dict] = [
30
+ # A. the data can decide these (the advisor recommends them)
31
+ dict(key="null", form="surrogate_null", group="A",
32
+ does="Which chance model each detected pattern is tested against.",
33
+ range="profile, rotation or shuffle.",
34
+ recommend="profile when event types have their own timing (the advisor decides this from the conditional-uniformity check).",
35
+ effect="profile tests coupling beyond each type's own timing; rotation and shuffle test whether any temporal structure is present at all."),
36
+ dict(key="min_lag", form="minimum_lag_frames", group="A",
37
+ does="The smallest genuine time gap required between two linked events.",
38
+ range="a whole number of time units, 0 or more.",
39
+ recommend="1 when many events share a timestamp (the advisor recommends it when more than ~10% of neighbouring events are co-timed); 0 to match THEME.",
40
+ effect="higher treats more co-timed pairs as co-occurrence, not sequence; 0 lets co-timed events count as ordered (THEME-compatible, but sub-frame order is undefined)."),
41
+ dict(key="freq_exclude", form="frequent_event_exclusion", group="A",
42
+ does="Sets aside an over-common event type from building patterns (it is still counted in the baseline).",
43
+ range="a positive number (mean occurrences per observation, under the default rule); off if empty.",
44
+ recommend="the advisor flags a type averaging more than ~1.5 occurrences per observation.",
45
+ effect="lower excludes more (rarer) types; off keeps every type eligible and can flood the results with 'this common event, then anything'."),
46
+ dict(key="min_occurrence", form="minimum_occurrence", group="A",
47
+ does="How many times a pattern must recur before it is tested.",
48
+ range="a whole number, 2 or more (3 is the usual floor).",
49
+ recommend="3, the floor at which the surrogate test can be calibrated.",
50
+ effect="higher keeps only more frequent patterns; below 3 the chance test cannot resolve."),
51
+ dict(key="error_control", form="error_control", group="A",
52
+ does="Which multiple-testing correction is treated as primary.",
53
+ range="fdr or fwer.",
54
+ recommend="fdr to screen many patterns; fwer for a single confirmatory claim.",
55
+ effect="fdr controls the expected share of false discoveries; fwer controls any false positive at all (stricter, and needs a large B)."),
56
+ dict(key="B", form="surrogates_B", group="A",
57
+ does="How many surrogate datasets are drawn for the Monte-Carlo p-value.",
58
+ range="a whole number, typically hundreds to thousands.",
59
+ recommend="200 to screen; 2000 or more for a confirmatory or family-wise claim (the smallest possible p is 1/(B+1)).",
60
+ effect="higher gives a finer p-value and more confidence, at more compute."),
61
+ # B. your policy (the data cannot decide these)
62
+ dict(key="alpha", form="detection_alpha", group="B",
63
+ does="The critical-interval acceptance threshold: a screening gate deciding which candidates are detected, not the basis of inference.",
64
+ range="between 0 and 1, small (e.g. .005 to .05).",
65
+ recommend=".005, a convention in sport T-pattern work; the surrogate calibration, not this threshold, is what a claim rests on.",
66
+ effect="a looser value lets more candidates through to calibration, but does not change which patterns survive it."),
67
+ dict(key="q_target", form="false_discovery_q", group="B",
68
+ does="The false-discovery-rate threshold a pattern's q-value must clear.",
69
+ range="between 0 and 1 (e.g. .05).",
70
+ recommend=".05.",
71
+ effect="lower is stricter (fewer patterns reported)."),
72
+ dict(key="alpha_fwer", form="family_wise_alpha", group="B",
73
+ does="The family-wise error rate used for confirmatory claims (Holm).",
74
+ range="between 0 and 1 (e.g. .005).",
75
+ recommend=".005; a family-wise claim needs B in the thousands to resolve.",
76
+ effect="lower is stricter; too small a B cannot reach it."),
77
+ dict(key="min_samples_frac", form="bout_support_percent", group="B",
78
+ does="The minimum fraction of observations a pattern must appear in to be kept.",
79
+ range="0 to 100 percent; 0 is off.",
80
+ recommend="off. Report prevalence rather than filtering on it (Section 4.1 shows why a threshold cuts along the wrong axis).",
81
+ effect="raising it deletes rare-but-genuine patterns, including well-evidenced ones specific to a subgroup."),
82
+ dict(key="lumping_factor", form="lumping_factor", group="B",
83
+ does="If one event nearly always follows another, treat the pair as a single unit so the combinations do not explode.",
84
+ range="between 0 and 1; off when 0.",
85
+ recommend="off. The THEME manual illustrates it with 0.9 (a near-deterministic link).",
86
+ effect="a higher threshold lumps fewer pairs; used mainly to tame very dense data."),
87
+ dict(key="completeness", form="completeness_competition", group="B",
88
+ does="Drops a pattern whose every occurrence sits inside a longer detected pattern.",
89
+ range="on or off.",
90
+ recommend="on.",
91
+ effect="off keeps redundant sub-patterns and inflates the counts."),
92
+ dict(key="collapse_equivalent", form="collapse_equivalent_patterns", group="B",
93
+ does="Merges patterns with an identical set of occurrences (the two directions of a co-timed pair, or different bracketings of one chain) to one representative.",
94
+ range="on or off.",
95
+ recommend="on.",
96
+ effect="off double-counts perfectly-dependent duplicates and enlarges the multiple-comparison family."),
97
+ dict(key="include_univariate", form="include_univariate_patterns", group="B",
98
+ does="Keeps single-event-type entries (Level 0) in the output.",
99
+ range="on or off.",
100
+ recommend="on.",
101
+ effect="off drops the univariate baseline rows."),
102
+ dict(key="exclude_events", form="force_exclude_events", group="B",
103
+ does="An explicit list of event types to bar from building patterns, overriding the frequency rule.",
104
+ range="comma-separated type names, or empty.",
105
+ recommend="empty, so the frequency rule applies.",
106
+ effect="listing a type removes it from pattern-building whatever its frequency (used to reproduce a specific analysis)."),
107
+ # C. structural inputs (fixed before analysis, change every result)
108
+ dict(key="time_unit", form="time_unit", group="C",
109
+ does="The unit of the time columns in your file.",
110
+ range="s or ms.",
111
+ recommend="whatever your data uses; times are held internally in milliseconds.",
112
+ effect="sets the unit intervals are reported in; the wrong choice mis-scales every time."),
113
+ dict(key="obs_window", form="use_observation_window_columns", group="C",
114
+ does="Reads each observation's window from obs_start / obs_end columns instead of its first and last event.",
115
+ range="on or off.",
116
+ recommend="on when your observations have real bounds (a possession, rally or bout usually does).",
117
+ effect="off takes the window from the events, which shifts every NX/T baseline probability."),
118
+ dict(key="max_level", form="maximum_depth", group="C",
119
+ does="A cap on how deep the pattern hierarchy can go.",
120
+ range="a whole number, 1 or more.",
121
+ recommend="8; it is rarely reached.",
122
+ effect="lower yields fewer and simpler patterns and can prevent overload on dense data."),
123
+ dict(key="collapse_duplicates", form="same_instant_duplicates", group="C",
124
+ does="Collapses two records of the same event type at an identical timestamp to one point.",
125
+ range="on or off.",
126
+ recommend="on for finely-timed data (a double-entry is one point in time); off only on coarsely-timed data where two same-type events might genuinely share a coded time.",
127
+ effect="off keeps both records, inflating that type's baseline rate."),
128
+ dict(key="max_edges", form="candidate_edge_cap", group="C",
129
+ does="A cap on candidate window boundaries per pair (an approximate search for very dense data).",
130
+ range="a whole number, 0 or more; 0 means exact.",
131
+ recommend="0 (exact). Set it only on very dense data where the exact search does not finish.",
132
+ effect="lower is faster but makes the window search approximate rather than exhaustive."),
133
+ ]
134
+
135
+ # --- output columns ---------------------------------------------------------
136
+ COLUMNS: list[dict] = [
137
+ dict(key="pattern", does="The pattern as text (the signature is its canonical bracketed form)."),
138
+ dict(key="N", does="Number of occurrences of the pattern across all observations."),
139
+ dict(key="bouts", does="Number of distinct observations the pattern occurs in."),
140
+ dict(key="bout_support", does="bouts as a fraction of all observations (the pattern's prevalence)."),
141
+ dict(key="length", does="Number of event types in the pattern."),
142
+ dict(key="level", does="Hierarchy depth: 0 is a single event type, 1 a pair, higher levels longer chains."),
143
+ dict(key="loop", does="1 if the pattern repeats an event type, otherwise 0."),
144
+ dict(key="critical_interval", does="The accepted time window [d1, d2] within which the later event follows the earlier."),
145
+ dict(key="duration_median_ms", does="The observed timing of the occurrences: the median gap (with duration_iqr_ms giving the middle half), in milliseconds."),
146
+ dict(key="monte_carlo_p", does="The Monte-Carlo empirical p-value: the fraction of surrogate datasets in which the pattern is as strong or stronger."),
147
+ dict(key="fdr_q", does="The Benjamini-Hochberg false-discovery q-value (the screening decision)."),
148
+ dict(key="survives_fwer_holm", does="The Holm family-wise decision: 1 if the pattern survives family-wise control, 0 if not."),
149
+ ]
150
+
151
+ _GROUP = {"A": "The data can decide it (the advisor recommends)",
152
+ "B": "Your policy (the data cannot decide it)",
153
+ "C": "Structural input (fixed before analysis)"}
154
+
155
+
156
+ def explain(name: str | None = None) -> str:
157
+ """Plain-language reference for a setting or output column, or for everything.
158
+
159
+ ``explain("min_lag")`` (or its form name ``"minimum_lag_frames"``) describes one
160
+ setting; ``explain("fdr_q")`` an output column; ``explain()`` prints them all.
161
+ """
162
+ if name is None:
163
+ return explain_settings() + "\n\n" + explain_columns()
164
+ for s in SETTINGS:
165
+ if name in (s["key"], s["form"]):
166
+ return (f"{s['form']} (Config: {s['key']}) [{_GROUP[s['group']]}]\n"
167
+ f" what it does : {s['does']}\n"
168
+ f" range : {s['range']}\n"
169
+ f" recommended : {s['recommend']}\n"
170
+ f" higher/lower : {s['effect']}")
171
+ for c in COLUMNS:
172
+ if name == c["key"]:
173
+ return f"{c['key']} — {c['does']}"
174
+ return f"No setting or column named {name!r}. Try explain() to list them all."
175
+
176
+
177
+ def explain_settings() -> str:
178
+ out = ["SETTINGS"]
179
+ for g in ("A", "B", "C"):
180
+ out.append(f"\n[{g}] {_GROUP[g]}")
181
+ for s in SETTINGS:
182
+ if s["group"] == g:
183
+ out.append(f" {s['form']:28s} {s['does']}")
184
+ out.append(f" {'':28s} range: {s['range']} recommended: {s['recommend']}")
185
+ return "\n".join(out)
186
+
187
+
188
+ def explain_columns() -> str:
189
+ return "OUTPUT COLUMNS\n" + "\n".join(f" {c['key']:22s} {c['does']}" for c in COLUMNS)
190
+
191
+
192
+ def parameters_markdown() -> str:
193
+ """The full reference as Markdown (used to write PARAMETERS.md)."""
194
+ L = ["# tpattern parameters and outputs",
195
+ "",
196
+ "Every setting and output column, with what it does, its range, the recommended "
197
+ "value, and what higher or lower means. Generated from `tpattern/reference.py`, "
198
+ "which is also the source for the tutorial's tables, the Colab form labels and "
199
+ "`tpattern.explain()`.", ""]
200
+ for g in ("A", "B", "C"):
201
+ L += [f"## {_GROUP[g]}", "",
202
+ "| Setting (form name / `Config`) | What it does | Range | Recommended | Higher / lower |",
203
+ "|---|---|---|---|---|"]
204
+ for s in SETTINGS:
205
+ if s["group"] == g:
206
+ L.append(f"| **{s['form']}** / `{s['key']}` | {s['does']} | {s['range']} "
207
+ f"| {s['recommend']} | {s['effect']} |")
208
+ L.append("")
209
+ L += ["## Output columns", "",
210
+ "| Column | What it is |", "|---|---|"]
211
+ for c in COLUMNS:
212
+ L.append(f"| `{c['key']}` | {c['does']} |")
213
+ L.append("")
214
+ return "\n".join(L)