scistackplot 0.1.26__py3-none-any.whl
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.
- scistackplot/__init__.py +239 -0
- scistackplot/capability.py +656 -0
- scistackplot/codegen.py +886 -0
- scistackplot/groups.py +110 -0
- scistackplot/reduce.py +1475 -0
- scistackplot/render/__init__.py +26 -0
- scistackplot/render/base.py +262 -0
- scistackplot/render/mpl.py +458 -0
- scistackplot/render/plotly_.py +537 -0
- scistackplot/resolved.py +240 -0
- scistackplot/roles.py +413 -0
- scistackplot/shape.py +106 -0
- scistackplot/sources/__init__.py +7 -0
- scistackplot/sources/base.py +256 -0
- scistackplot/sources/csv.py +67 -0
- scistackplot/sources/frame.py +45 -0
- scistackplot/spec.py +698 -0
- scistackplot/table.py +328 -0
- scistackplot/variants.py +743 -0
- scistackplot/xaxis.py +183 -0
- scistackplot/ylimits.py +451 -0
- scistackplot-0.1.26.dist-info/METADATA +212 -0
- scistackplot-0.1.26.dist-info/RECORD +24 -0
- scistackplot-0.1.26.dist-info/WHEEL +4 -0
scistackplot/groups.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Derived grouping factors: bucketing a factor's levels into a new factor.
|
|
3
|
+
|
|
4
|
+
``session ∈ {pre, post1, post2}`` becomes ``Phase ∈ {baseline, post}`` — a
|
|
5
|
+
factor you can colour or facet by, without editing any data and without the
|
|
6
|
+
database having to have recorded it.
|
|
7
|
+
|
|
8
|
+
This is a **derived table**, the same shape as
|
|
9
|
+
:func:`~scistackplot.variants.apply_variant_sets`: the spec decides it, so it is
|
|
10
|
+
recomputed wherever the spec is read, and everything downstream sees one
|
|
11
|
+
ordinary factor with no idea it was synthesized. See
|
|
12
|
+
``docs/claude/synthetic-factors.md`` for why that matters and for the three call
|
|
13
|
+
sites this has to be wired into.
|
|
14
|
+
|
|
15
|
+
The source factor **stays**. Unlike a variant selection — where keeping the
|
|
16
|
+
column would state the same thing twice and ``roles.validate`` would refuse the
|
|
17
|
+
figure — ``session`` and ``Phase`` are independently useful: sessions along x,
|
|
18
|
+
phases in colour. Neither is a variant factor, so no pooling guard is involved.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import replace
|
|
24
|
+
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from scistacklog import Log
|
|
27
|
+
|
|
28
|
+
from .spec import LevelGroup, PlotSpec
|
|
29
|
+
from .table import FactorInfo, LongTable
|
|
30
|
+
|
|
31
|
+
LAYER = "scistackplot"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def apply_level_groups(spec: PlotSpec, table: LongTable) -> LongTable:
|
|
35
|
+
"""Add one factor per :class:`~scistackplot.spec.LevelGroup` in ``spec``.
|
|
36
|
+
|
|
37
|
+
Returns ``table`` untouched when the spec defines none, so a project that
|
|
38
|
+
never buckets anything pays nothing.
|
|
39
|
+
"""
|
|
40
|
+
groups = [group for group in spec.level_groups if group.name and group.source]
|
|
41
|
+
if not groups:
|
|
42
|
+
return table
|
|
43
|
+
|
|
44
|
+
frame = table.frame
|
|
45
|
+
factors = list(table.factors)
|
|
46
|
+
|
|
47
|
+
for group in groups:
|
|
48
|
+
if group.source not in frame.columns:
|
|
49
|
+
# A spec outlives the table it was written against — the source
|
|
50
|
+
# factor may have been filtered away or belong to another variable.
|
|
51
|
+
Log.warn(
|
|
52
|
+
"level group %r reads %r, which this table has no column for "
|
|
53
|
+
"— skipped",
|
|
54
|
+
group.name,
|
|
55
|
+
group.source,
|
|
56
|
+
layer=LAYER,
|
|
57
|
+
)
|
|
58
|
+
continue
|
|
59
|
+
frame, levels = _apply_one(frame, group)
|
|
60
|
+
factors.append(FactorInfo(name=group.name, levels=levels))
|
|
61
|
+
Log.info(
|
|
62
|
+
"level group %r: %d level(s) of %r -> %s",
|
|
63
|
+
group.name,
|
|
64
|
+
len(group.mapping),
|
|
65
|
+
group.source,
|
|
66
|
+
levels,
|
|
67
|
+
layer=LAYER,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return replace(table, frame=frame, factors=factors)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _apply_one(frame: pd.DataFrame, group: LevelGroup) -> tuple[pd.DataFrame, list]:
|
|
74
|
+
"""Map one source column into a new column, and report its level order."""
|
|
75
|
+
mapping = {str(key): value for key, value in group.mapping.items()}
|
|
76
|
+
mapped = frame[group.source].astype(str).map(mapping)
|
|
77
|
+
|
|
78
|
+
if group.unmatched is None:
|
|
79
|
+
# Drop rows the mapping did not name. "Just these two groups, ignore
|
|
80
|
+
# the rest" is the common intent, and the alternative — keeping them as
|
|
81
|
+
# NaN — makes them a silent extra series in every legend.
|
|
82
|
+
keep = mapped.notna()
|
|
83
|
+
dropped = int((~keep).sum())
|
|
84
|
+
if dropped:
|
|
85
|
+
Log.info(
|
|
86
|
+
"level group %r dropped %d row(s) whose %s was not in the "
|
|
87
|
+
"mapping",
|
|
88
|
+
group.name,
|
|
89
|
+
dropped,
|
|
90
|
+
group.source,
|
|
91
|
+
layer=LAYER,
|
|
92
|
+
)
|
|
93
|
+
frame = frame[keep]
|
|
94
|
+
mapped = mapped[keep]
|
|
95
|
+
else:
|
|
96
|
+
mapped = mapped.fillna(group.unmatched)
|
|
97
|
+
|
|
98
|
+
frame = frame.assign(**{group.name: mapped})
|
|
99
|
+
|
|
100
|
+
# Declared order: the order the user wrote the buckets in, then the
|
|
101
|
+
# catch-all last. Reading it off the data would reorder a legend whenever a
|
|
102
|
+
# filter happened to remove a group's last row.
|
|
103
|
+
order: list = []
|
|
104
|
+
for value in mapping.values():
|
|
105
|
+
if value not in order:
|
|
106
|
+
order.append(value)
|
|
107
|
+
if group.unmatched is not None and group.unmatched not in order:
|
|
108
|
+
order.append(group.unmatched)
|
|
109
|
+
present = set(frame[group.name].dropna().astype(str))
|
|
110
|
+
return frame, [level for level in order if str(level) in present]
|