tpattern 0.1.5__tar.gz → 0.1.7__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.5/tpattern.egg-info → tpattern-0.1.7}/PKG-INFO +1 -1
- {tpattern-0.1.5 → tpattern-0.1.7}/pyproject.toml +1 -1
- tpattern-0.1.7/tests/test_report_dendrogram.py +51 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/__init__.py +1 -1
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/advisor.py +47 -4
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/report.py +7 -2
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/viz.py +3 -1
- {tpattern-0.1.5 → tpattern-0.1.7/tpattern.egg-info}/PKG-INFO +1 -1
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern.egg-info/SOURCES.txt +1 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/LICENSE +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/README.md +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/setup.cfg +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_calibrated_pins.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_contrast.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_dedup.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_guided.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_null_compare.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_reproduction.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_support_reporting.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tests/test_synthetic.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/ci.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/contrast.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/detect.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/diagnostics.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/guided.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/io.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/methods.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/pattern.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/randomise.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/significance.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern/synthetic.py +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern.egg-info/dependency_links.txt +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/tpattern.egg-info/requires.txt +0 -0
- {tpattern-0.1.5 → tpattern-0.1.7}/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.7"
|
|
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,51 @@
|
|
|
1
|
+
"""Regression: report()/patterns_overview must handle a single composite pattern.
|
|
2
|
+
|
|
3
|
+
With cols=2, plt.subplots returns a 2-axis ndarray even for one pattern; the old
|
|
4
|
+
code wrapped that array in a list and passed it to pattern_dendrogram, which then
|
|
5
|
+
called ndarray.plot(). The de-duplication work collapsed the quickstart to exactly
|
|
6
|
+
one composite, exposing the crash in report().
|
|
7
|
+
"""
|
|
8
|
+
import matplotlib
|
|
9
|
+
matplotlib.use("Agg")
|
|
10
|
+
|
|
11
|
+
from tpattern import Config, Engine, calibrate, report
|
|
12
|
+
from tpattern.viz import patterns_overview
|
|
13
|
+
from tpattern.synthetic import SyntheticSpec, make_dataset
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _planted():
|
|
17
|
+
obs, _ = make_dataset(SyntheticSpec(
|
|
18
|
+
n_seq=300, planted=[("A", 0), ("B", 1500)], plant_fraction=0.15,
|
|
19
|
+
jitter=200, bg_rate=2.5, seed=7))
|
|
20
|
+
return obs
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_patterns_overview_single_composite(tmp_path):
|
|
24
|
+
obs = _planted()
|
|
25
|
+
comps = [p for p in Engine(obs, Config(min_lag=1)).detect() if p.level >= 1]
|
|
26
|
+
assert comps, "planted data should yield at least one composite"
|
|
27
|
+
out = tmp_path / "ov.png"
|
|
28
|
+
patterns_overview(comps[:1], str(out)) # the n == 1 path
|
|
29
|
+
assert out.exists() and out.stat().st_size > 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_report_writes_dendrogram(tmp_path):
|
|
33
|
+
obs = _planted()
|
|
34
|
+
r = calibrate(obs, Config(min_lag=1), null="profile", B=50, seed=1)
|
|
35
|
+
import os
|
|
36
|
+
w = report(r, str(tmp_path / "out"), title="t")
|
|
37
|
+
assert os.path.exists(w["dendrograms"]) and os.path.getsize(w["dendrograms"]) > 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_forest_plot_degenerate_ci(tmp_path):
|
|
41
|
+
"""An odds-ratio CI can hit 0 or inf (a group with a zero cell); forest_plot
|
|
42
|
+
must not set a non-positive/inf limit on its log x-axis."""
|
|
43
|
+
import warnings
|
|
44
|
+
from tpattern import forest_plot
|
|
45
|
+
items = [{"label": "a", "or": 2.0, "lo": 0.0, "hi": float("inf")},
|
|
46
|
+
{"label": "b", "or": 1.5, "lo": 0.8, "hi": 3.0}]
|
|
47
|
+
out = tmp_path / "forest.png"
|
|
48
|
+
with warnings.catch_warnings():
|
|
49
|
+
warnings.simplefilter("error", UserWarning)
|
|
50
|
+
forest_plot(items, str(out))
|
|
51
|
+
assert out.exists() and out.stat().st_size > 0
|
|
@@ -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.
|
|
9
|
+
__version__ = "0.1.7" # defined before submodule imports (methods.py reads it)
|
|
10
10
|
|
|
11
11
|
from .io import Observation, read_observation, read_sample, read_table
|
|
12
12
|
from .pattern import Instance, Pattern
|
|
@@ -50,12 +50,55 @@ class Recommendation:
|
|
|
50
50
|
notes: list[str] = field(default_factory=list)
|
|
51
51
|
|
|
52
52
|
def __str__(self) -> str:
|
|
53
|
-
|
|
53
|
+
"""Readable plain-text view: each setting, its recommended value, then the
|
|
54
|
+
wrapped reason. Kept ANSI/markup-free so it can be pasted verbatim."""
|
|
55
|
+
import textwrap
|
|
56
|
+
w = 76
|
|
57
|
+
|
|
58
|
+
def wrap(text, first, rest):
|
|
59
|
+
return textwrap.fill(text, width=w, initial_indent=first,
|
|
60
|
+
subsequent_indent=rest)
|
|
61
|
+
|
|
62
|
+
out = ["=" * w,
|
|
63
|
+
f"RECOMMENDED SETTINGS ({self.n_obs:,} observations, "
|
|
64
|
+
f"{self.n_events:,} events)",
|
|
65
|
+
"=" * w]
|
|
54
66
|
for c in self.choices:
|
|
55
|
-
|
|
67
|
+
out += ["", f" {c.option} -> {c.recommended}",
|
|
68
|
+
wrap(c.rationale, " ", " ")]
|
|
56
69
|
for n in self.notes:
|
|
57
|
-
|
|
58
|
-
return "\n".join(
|
|
70
|
+
out += ["", wrap(n, " * ", " ")]
|
|
71
|
+
return "\n".join(out)
|
|
72
|
+
|
|
73
|
+
def _repr_html_(self) -> str:
|
|
74
|
+
"""Rich rendering in notebooks (Colab/Jupyter): the recommended value in
|
|
75
|
+
bold, the setting name and explanation in normal weight. Shown when you
|
|
76
|
+
display the object (a bare `rec` or `display(rec)`); `print(rec)` still
|
|
77
|
+
gives the plain-text view above."""
|
|
78
|
+
import html
|
|
79
|
+
|
|
80
|
+
def esc(s):
|
|
81
|
+
return html.escape(str(s))
|
|
82
|
+
|
|
83
|
+
rows = "".join(
|
|
84
|
+
'<div style="margin:0 0 11px 0;">'
|
|
85
|
+
f'<div><span style="color:#6b7280;">{esc(c.option)}:</span> '
|
|
86
|
+
f'<strong style="color:#111;">{esc(c.recommended)}</strong></div>'
|
|
87
|
+
f'<div style="color:#4b5563;margin-top:2px;">{esc(c.rationale)}</div>'
|
|
88
|
+
'</div>'
|
|
89
|
+
for c in self.choices)
|
|
90
|
+
notes = ""
|
|
91
|
+
if self.notes:
|
|
92
|
+
lis = "".join(f'<li style="color:#4b5563;margin:2px 0;">{esc(n)}</li>'
|
|
93
|
+
for n in self.notes)
|
|
94
|
+
notes = f'<ul style="margin:6px 0 0 0;padding-left:18px;">{lis}</ul>'
|
|
95
|
+
return (
|
|
96
|
+
'<div style="font-family:system-ui,-apple-system,Segoe UI,sans-serif;'
|
|
97
|
+
'font-size:13px;line-height:1.5;max-width:780px;">'
|
|
98
|
+
'<div style="font-weight:600;font-size:14px;margin-bottom:8px;'
|
|
99
|
+
'padding-bottom:5px;border-bottom:1px solid #e5e7eb;">'
|
|
100
|
+
f'Recommended settings · {self.n_obs:,} observations, '
|
|
101
|
+
f'{self.n_events:,} events</div>{rows}{notes}</div>')
|
|
59
102
|
|
|
60
103
|
def methods_text(self) -> str:
|
|
61
104
|
"""A ready-to-adapt Methods paragraph, grounded in the measured values."""
|
|
@@ -141,10 +141,15 @@ def forest_plot(items, outfile: str, title: str = "Effect sizes (odds ratios)",
|
|
|
141
141
|
fig, ax = plt.subplots(figsize=(9, 0.6 * n + 1.4))
|
|
142
142
|
ys = list(range(n, 0, -1)) # top-to-bottom in given order
|
|
143
143
|
|
|
144
|
-
# fix x-range up front so label anchoring is stable on the log scale
|
|
144
|
+
# fix x-range up front so label anchoring is stable on the log scale.
|
|
145
|
+
# An odds-ratio CI can hit 0 or inf when a group has a zero cell, and neither
|
|
146
|
+
# is a valid limit on a log axis, so fall back to sensible finite bounds.
|
|
145
147
|
los = [it["lo"] for it in items]; his = [it["hi"] for it in items]
|
|
148
|
+
pos_lo = [x for x in los if x and x > 0]
|
|
149
|
+
fin_hi = [x for x in his if x not in (float("inf"), float("nan")) and x > 0]
|
|
146
150
|
ax.set_xscale("log")
|
|
147
|
-
ax.set_xlim(min(
|
|
151
|
+
ax.set_xlim((min(pos_lo) if pos_lo else 0.1) * 0.7,
|
|
152
|
+
(max(fin_hi) if fin_hi else 10.0) * 1.35)
|
|
148
153
|
ax.set_ylim(0.3, n + 0.9)
|
|
149
154
|
# label in axes-x (fixed left/right), data-y
|
|
150
155
|
tx = blended_transform_factory(ax.transAxes, ax.transData)
|
|
@@ -99,7 +99,9 @@ def patterns_overview(patterns: list, outfile: str, max_rows: int = 8,
|
|
|
99
99
|
cols = 2
|
|
100
100
|
rows = (n + cols - 1) // cols
|
|
101
101
|
fig, axes = plt.subplots(rows, cols, figsize=(12, 3.0 * rows))
|
|
102
|
-
|
|
102
|
+
# plt.subplots returns an ndarray whenever rows*cols > 1 (cols is always 2 here,
|
|
103
|
+
# so even a single pattern yields a 2-axis array); flatten it, else wrap the lone Axes.
|
|
104
|
+
axes = axes.flatten() if hasattr(axes, "flatten") else [axes]
|
|
103
105
|
for ax, p in zip(axes, comps):
|
|
104
106
|
pattern_dendrogram(p, title=f"N={p.N}, level {p.level}", ci_unit=ci_unit, ax=ax)
|
|
105
107
|
for ax in axes[n:]:
|
|
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
|
|
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
|