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/shape.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Measure shape classification.
|
|
3
|
+
|
|
4
|
+
The *shape* of a measure — scalar, 1-D series, 2-D matrix — is the single
|
|
5
|
+
input that decides which plot kinds are even meaningful for it (see
|
|
6
|
+
``capability.available_plots``). Classification is deliberately based on
|
|
7
|
+
**observed values**, not on declared dtypes or SQL type-name strings: a pandas
|
|
8
|
+
object column holding numpy arrays and a DuckDB LIST column both arrive here
|
|
9
|
+
as "a cell that contains a sequence", and the value is the only reliable
|
|
10
|
+
common ground. (The GUI's pre-existing ``_numeric_plot_kind`` in
|
|
11
|
+
``scistack_gui/api/variables.py`` made the same call for the same reason.)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Shape(str, Enum):
|
|
24
|
+
"""What one cell of a measure column holds."""
|
|
25
|
+
|
|
26
|
+
SCALAR = "scalar"
|
|
27
|
+
SERIES_1D = "1d"
|
|
28
|
+
MATRIX_2D = "2d"
|
|
29
|
+
CATEGORICAL = "categorical"
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str: # keeps f-strings and log lines readable
|
|
33
|
+
return self.value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_number(value: Any) -> bool:
|
|
37
|
+
# bool is an int subclass; a column of True/False is categorical, not scalar.
|
|
38
|
+
if isinstance(value, bool) or isinstance(value, np.bool_):
|
|
39
|
+
return False
|
|
40
|
+
return isinstance(value, (int, float, np.integer, np.floating))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def classify_value(value: Any) -> Shape:
|
|
44
|
+
"""Classify a single cell value."""
|
|
45
|
+
if value is None or (isinstance(value, float) and np.isnan(value)):
|
|
46
|
+
return Shape.UNKNOWN
|
|
47
|
+
# bool is an int subclass, so this must precede the numeric check: a
|
|
48
|
+
# True/False column groups data, it is not something to plot on an axis.
|
|
49
|
+
if isinstance(value, (bool, np.bool_)):
|
|
50
|
+
return Shape.CATEGORICAL
|
|
51
|
+
if _is_number(value):
|
|
52
|
+
return Shape.SCALAR
|
|
53
|
+
if isinstance(value, str):
|
|
54
|
+
return Shape.CATEGORICAL
|
|
55
|
+
if isinstance(value, np.ndarray):
|
|
56
|
+
if value.ndim == 1:
|
|
57
|
+
return Shape.SERIES_1D if value.size else Shape.UNKNOWN
|
|
58
|
+
if value.ndim == 2:
|
|
59
|
+
return Shape.MATRIX_2D
|
|
60
|
+
return Shape.UNKNOWN
|
|
61
|
+
if isinstance(value, (list, tuple)):
|
|
62
|
+
if not value:
|
|
63
|
+
return Shape.UNKNOWN
|
|
64
|
+
first = value[0]
|
|
65
|
+
if _is_number(first):
|
|
66
|
+
return Shape.SERIES_1D
|
|
67
|
+
if isinstance(first, (list, tuple, np.ndarray)):
|
|
68
|
+
return Shape.MATRIX_2D
|
|
69
|
+
return Shape.UNKNOWN
|
|
70
|
+
return Shape.UNKNOWN
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def classify_column(series: pd.Series) -> Shape:
|
|
74
|
+
"""
|
|
75
|
+
Classify a whole measure column.
|
|
76
|
+
|
|
77
|
+
A column is uniformly typed in every source we support (a DuckDB column,
|
|
78
|
+
or a DataFrame column built from one), so the first non-null value decides.
|
|
79
|
+
We still scan a few values rather than exactly one: a column can lead with
|
|
80
|
+
nulls, and an all-null column must classify as UNKNOWN rather than crash.
|
|
81
|
+
"""
|
|
82
|
+
if series is None or len(series) == 0:
|
|
83
|
+
return Shape.UNKNOWN
|
|
84
|
+
|
|
85
|
+
if pd.api.types.is_bool_dtype(series):
|
|
86
|
+
return Shape.CATEGORICAL
|
|
87
|
+
if pd.api.types.is_numeric_dtype(series):
|
|
88
|
+
return Shape.SCALAR
|
|
89
|
+
if isinstance(series.dtype, pd.CategoricalDtype):
|
|
90
|
+
return Shape.CATEGORICAL
|
|
91
|
+
|
|
92
|
+
for value in series.head(_SCAN_LIMIT):
|
|
93
|
+
shape = classify_value(value)
|
|
94
|
+
if shape is not Shape.UNKNOWN:
|
|
95
|
+
return shape
|
|
96
|
+
return Shape.UNKNOWN
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
#: How many leading values to inspect before giving up on a column. Small on
|
|
100
|
+
#: purpose — this runs on every measure of every table the GUI describes.
|
|
101
|
+
_SCAN_LIMIT = 20
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def is_plottable(shape: Shape) -> bool:
|
|
105
|
+
"""Whether any plot kind can render this shape."""
|
|
106
|
+
return shape in (Shape.SCALAR, Shape.SERIES_1D, Shape.MATRIX_2D)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The ``DataSource`` protocol — the one seam that makes this package both
|
|
3
|
+
standalone and scistack-compatible.
|
|
4
|
+
|
|
5
|
+
``scistackplot`` ships CSV and DataFrame implementations; ``scistackplotdb``
|
|
6
|
+
ships the scidb one. The GUI talks only to this protocol and never learns which
|
|
7
|
+
implementation it has, so the same application serves a lone CSV file and a
|
|
8
|
+
full scidb project. Everything above this line is pure long-table logic.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from ..table import LongTable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class DataSource(Protocol):
|
|
20
|
+
"""Supplies long-format tables and the metadata needed to build controls."""
|
|
21
|
+
|
|
22
|
+
def describe(self) -> dict:
|
|
23
|
+
"""
|
|
24
|
+
Factors, measures and shapes — everything the GUI needs to render its
|
|
25
|
+
controls before any data is fetched.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def get_table(
|
|
30
|
+
self,
|
|
31
|
+
measures: list[str],
|
|
32
|
+
*,
|
|
33
|
+
x_measure: str | None = None,
|
|
34
|
+
factor_variables: list[str] | None = None,
|
|
35
|
+
) -> LongTable:
|
|
36
|
+
"""The long-format table for a plot's measures.
|
|
37
|
+
|
|
38
|
+
Several ``measures`` **stack** into one value column plus a
|
|
39
|
+
``Variable`` column; ``x_measure`` **joins** as an x axis instead.
|
|
40
|
+
"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def joinable_with(self, measure: str) -> list[str]:
|
|
44
|
+
"""
|
|
45
|
+
Measures that can supply an x axis for ``measure``.
|
|
46
|
+
|
|
47
|
+
Trivially "all the others" for a flat CSV. For scidb it is a real
|
|
48
|
+
question — two variables can only share a plot if their schema levels
|
|
49
|
+
can be joined — and answering it honestly keeps the GUI from offering
|
|
50
|
+
combinations that cannot be built.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def stackable_with(self, measure: str) -> list[str]:
|
|
55
|
+
"""Measures that can be plotted as another series alongside ``measure``."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def groupable_with(self, measure: str) -> list[str]:
|
|
59
|
+
"""Variables usable as a grouping FACTOR for ``measure`` — recorded at
|
|
60
|
+
or above its schema level, so each row gets exactly one value."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
#: How many built tables a source keeps. Small on purpose: the entries are whole
|
|
65
|
+
#: frames (millions of rows for a 1-D measure), and the access pattern they serve
|
|
66
|
+
#: is narrow — the panel asks for the SAME table over and over while the user
|
|
67
|
+
#: moves controls, and only changes which one when a variant row names another
|
|
68
|
+
#: variable. Four covers that with room to spare; a larger cache would mostly
|
|
69
|
+
#: hold frames nobody is going to ask for again.
|
|
70
|
+
TABLE_CACHE_ENTRIES = 4
|
|
71
|
+
|
|
72
|
+
#: Placeholder the stacked value column carries during the melt, before it is
|
|
73
|
+
#: renamed to the primary measure. See :meth:`BaseSource._stacked` for why the
|
|
74
|
+
#: rename is unavoidable. Deliberately not a plausible column name: it only has
|
|
75
|
+
#: to survive one call, and it must not collide with a real one.
|
|
76
|
+
_STACK_VALUE = "__stacked_value__"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BaseSource:
|
|
80
|
+
"""Small shared implementation for sources backed by a single table.
|
|
81
|
+
|
|
82
|
+
Also owns the built-table cache for *every* source, ``ScidbSource``
|
|
83
|
+
included — see :meth:`get_table`.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
name: str = "table"
|
|
87
|
+
|
|
88
|
+
def _table(self) -> LongTable: # pragma: no cover - overridden
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
|
|
91
|
+
def describe(self) -> dict:
|
|
92
|
+
return self._table().describe()
|
|
93
|
+
|
|
94
|
+
def get_table(
|
|
95
|
+
self,
|
|
96
|
+
measures: list[str],
|
|
97
|
+
*,
|
|
98
|
+
x_measure: str | None = None,
|
|
99
|
+
factor_variables: list[str] | None = None,
|
|
100
|
+
) -> LongTable:
|
|
101
|
+
"""The long table for these measures, built once per distinct request.
|
|
102
|
+
|
|
103
|
+
**Why this is memoized.** Building the table is melt + stack + join, and
|
|
104
|
+
the panel asks for the same one repeatedly: ``plot_resolve`` and
|
|
105
|
+
``plot_capabilities`` each build it on every control change, so a single
|
|
106
|
+
click paid for it twice, and four concurrent resolves paid for it eight
|
|
107
|
+
times. The 2026-09-11 log shows exactly that — ``melted 'FilteredEMG'``
|
|
108
|
+
and ``stacked [...]`` repeating in pairs on every action while
|
|
109
|
+
``load_variable`` (the layer below, already cached) ran once.
|
|
110
|
+
|
|
111
|
+
Keyed on everything that changes the result. Note ``measures`` is
|
|
112
|
+
order-sensitive and stays a tuple rather than a set: the order decides
|
|
113
|
+
which measure is primary and what the ``Variable`` column's level order
|
|
114
|
+
is.
|
|
115
|
+
|
|
116
|
+
**Sharing a table between callers is safe** because nothing downstream
|
|
117
|
+
mutates it — ``apply_variant_sets`` copies before writing its factor,
|
|
118
|
+
and every other step (filters, explode, aggregate) builds a new frame.
|
|
119
|
+
There is no in-place write anywhere in ``scistackplot`` or
|
|
120
|
+
``scistackplotdb``, and ``test_a_cached_table_is_not_mutated_by_use``
|
|
121
|
+
exists to keep it that way.
|
|
122
|
+
|
|
123
|
+
Staleness is the caller's business, as it already was for the frames
|
|
124
|
+
underneath: a run that writes records drops the whole source
|
|
125
|
+
(``plot_service.invalidate``), and this cache goes with it.
|
|
126
|
+
"""
|
|
127
|
+
memo = self._table_cache()
|
|
128
|
+
key = (
|
|
129
|
+
tuple(measures),
|
|
130
|
+
x_measure,
|
|
131
|
+
tuple(factor_variables or ()),
|
|
132
|
+
)
|
|
133
|
+
if key in memo:
|
|
134
|
+
return memo[key]
|
|
135
|
+
|
|
136
|
+
table = self._build_table(
|
|
137
|
+
measures, x_measure=x_measure, factor_variables=factor_variables
|
|
138
|
+
)
|
|
139
|
+
memo[key] = table
|
|
140
|
+
while len(memo) > TABLE_CACHE_ENTRIES:
|
|
141
|
+
# Insertion-ordered dict: the oldest key is the first one.
|
|
142
|
+
memo.pop(next(iter(memo)))
|
|
143
|
+
return table
|
|
144
|
+
|
|
145
|
+
def _table_cache(self) -> dict:
|
|
146
|
+
"""The memo, created on first use.
|
|
147
|
+
|
|
148
|
+
Lazy rather than set in ``__init__`` so every source inherits the cache
|
|
149
|
+
without having to remember to call up — including the ones that define
|
|
150
|
+
no ``__init__`` at all.
|
|
151
|
+
"""
|
|
152
|
+
memo = getattr(self, "_built_tables", None)
|
|
153
|
+
if memo is None:
|
|
154
|
+
memo = {}
|
|
155
|
+
self._built_tables = memo
|
|
156
|
+
return memo
|
|
157
|
+
|
|
158
|
+
def invalidate_tables(self) -> None:
|
|
159
|
+
"""Drop built tables. Call when the rows underneath may have changed."""
|
|
160
|
+
self._table_cache().clear()
|
|
161
|
+
|
|
162
|
+
def _build_table(
|
|
163
|
+
self,
|
|
164
|
+
measures: list[str],
|
|
165
|
+
*,
|
|
166
|
+
x_measure: str | None = None,
|
|
167
|
+
factor_variables: list[str] | None = None,
|
|
168
|
+
) -> LongTable:
|
|
169
|
+
# `factor_variables` is accepted and ignored: a flat table's factors are
|
|
170
|
+
# already columns of every row, so there is nothing to join in
|
|
171
|
+
# (`groupable_with` returns none). Accepting it keeps one call shape for
|
|
172
|
+
# every source rather than making callers branch on which they hold.
|
|
173
|
+
table = self._table()
|
|
174
|
+
requested = [*measures, *([x_measure] if x_measure else [])]
|
|
175
|
+
unknown = [m for m in requested if m not in table.measure_names]
|
|
176
|
+
if unknown:
|
|
177
|
+
raise KeyError(
|
|
178
|
+
f"Unknown measure(s) {unknown}. Available: {table.measure_names}"
|
|
179
|
+
)
|
|
180
|
+
# A flat table already carries every column, so an x measure needs no
|
|
181
|
+
# join here — only stacking changes the frame's shape.
|
|
182
|
+
return self._stacked(table, measures) if len(measures) > 1 else table
|
|
183
|
+
|
|
184
|
+
def _stacked(self, table: LongTable, measures: list[str]) -> LongTable:
|
|
185
|
+
"""Melt several measure columns into one, plus a ``Variable`` column.
|
|
186
|
+
|
|
187
|
+
The CSV equivalent of ``ScidbSource._stacked_table``: two columns of a
|
|
188
|
+
gait CSV are as much "Raw vs Filtered" as two scidb variables are, and
|
|
189
|
+
keeping the standalone path capable of it is what keeps the DataSource
|
|
190
|
+
protocol honest rather than scidb-shaped.
|
|
191
|
+
|
|
192
|
+
The melt goes via :data:`_STACK_VALUE` and is renamed afterwards. It has
|
|
193
|
+
to: the stacked column takes the PRIMARY measure's name, the primary is
|
|
194
|
+
always one of the columns being melted, and ``DataFrame.melt`` refuses a
|
|
195
|
+
``value_name`` that matches any column it is melting. Passing the name
|
|
196
|
+
directly raised ``ValueError`` for every possible input, so stacking on
|
|
197
|
+
a CSV or DataFrame source never worked at all — found 2026-09-11 by the
|
|
198
|
+
first test to ask for it.
|
|
199
|
+
"""
|
|
200
|
+
from ..variants import VARIABLE_COLUMN
|
|
201
|
+
|
|
202
|
+
primary = measures[0]
|
|
203
|
+
frame = table.frame
|
|
204
|
+
id_vars = [c for c in frame.columns if c not in table.measure_names]
|
|
205
|
+
melted = frame.melt(
|
|
206
|
+
id_vars=id_vars,
|
|
207
|
+
value_vars=list(measures),
|
|
208
|
+
var_name=VARIABLE_COLUMN,
|
|
209
|
+
value_name=_STACK_VALUE,
|
|
210
|
+
).rename(columns={_STACK_VALUE: primary})
|
|
211
|
+
level_order = {f.name: list(f.levels) for f in table.factors}
|
|
212
|
+
# Declared order, not observed: the user listed the measures.
|
|
213
|
+
level_order[VARIABLE_COLUMN] = list(measures)
|
|
214
|
+
return LongTable.from_frame(
|
|
215
|
+
melted,
|
|
216
|
+
factors=[*table.factor_names, VARIABLE_COLUMN],
|
|
217
|
+
measures=[primary],
|
|
218
|
+
level_order=level_order,
|
|
219
|
+
index_column=table.index_column,
|
|
220
|
+
name=primary,
|
|
221
|
+
schema_levels=table.schema_levels,
|
|
222
|
+
measure_labels={primary: " / ".join(measures)},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def joinable_with(self, measure: str) -> list[str]:
|
|
226
|
+
return [m for m in self._table().measure_names if m != measure]
|
|
227
|
+
|
|
228
|
+
def groupable_with(self, measure: str) -> list[str]:
|
|
229
|
+
"""A flat table's factors are already columns of every row.
|
|
230
|
+
|
|
231
|
+
Grouping variables exist because scidb records a subject-level fact
|
|
232
|
+
separately from trial-level data; a CSV has no such split, so there is
|
|
233
|
+
nothing to join in.
|
|
234
|
+
"""
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
def stackable_with(self, measure: str) -> list[str]:
|
|
238
|
+
"""Same shape, so the two share an axis. A flat table has no levels to
|
|
239
|
+
reconcile, which is the only other thing scidb has to check."""
|
|
240
|
+
table = self._table()
|
|
241
|
+
try:
|
|
242
|
+
shape = table.shape_of(measure)
|
|
243
|
+
except KeyError:
|
|
244
|
+
return []
|
|
245
|
+
return [
|
|
246
|
+
name
|
|
247
|
+
for name in table.measure_names
|
|
248
|
+
if name != measure and table.shape_of(name) is shape
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
def default_measure(self) -> str | None:
|
|
252
|
+
measures = self._table().measure_names
|
|
253
|
+
return measures[0] if measures else None
|
|
254
|
+
|
|
255
|
+
def metadata(self) -> dict[str, Any]:
|
|
256
|
+
return {"source": type(self).__name__, "name": self.name}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CSV source — the standalone entry point, and the direct descendant of the
|
|
3
|
+
R/Shiny proof of concept.
|
|
4
|
+
|
|
5
|
+
Its factor/measure split follows the same rule the proof of concept used
|
|
6
|
+
(``getColNames.R``: factor columns are the non-numeric ones), but derived from
|
|
7
|
+
observed value shapes rather than R's ``is.factor``, so a column of integer
|
|
8
|
+
subject IDs read from CSV can still be declared a factor explicitly.
|
|
9
|
+
|
|
10
|
+
This path is kept first-class — the VS Code extension routes "Plot" on a .csv
|
|
11
|
+
file here, with no project and no database — so the standalone claim stays
|
|
12
|
+
exercised rather than aspirational.
|
|
13
|
+
|
|
14
|
+
One inference limit worth knowing: a column of purely numeric IDs (``1, 2, 3``
|
|
15
|
+
for subject) is indistinguishable from a measurement by value alone, so it is
|
|
16
|
+
classified as a measure. Pass ``factors=["subject", ...]`` — or read it as text
|
|
17
|
+
with ``dtype={"subject": str}`` — when that is not what you want. A scidb
|
|
18
|
+
source never hits this: it knows which columns are schema keys.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Iterable
|
|
25
|
+
|
|
26
|
+
import pandas as pd
|
|
27
|
+
from scistacklog import Log
|
|
28
|
+
|
|
29
|
+
from ..table import LongTable
|
|
30
|
+
from .base import BaseSource
|
|
31
|
+
|
|
32
|
+
LAYER = "scistackplot"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CsvSource(BaseSource):
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
path: str | Path,
|
|
39
|
+
*,
|
|
40
|
+
factors: Iterable[str] | None = None,
|
|
41
|
+
measures: Iterable[str] | None = None,
|
|
42
|
+
level_order: dict[str, list[Any]] | None = None,
|
|
43
|
+
index_column: str | None = None,
|
|
44
|
+
**read_csv_kwargs: Any,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.path = Path(path)
|
|
47
|
+
self.name = self.path.name
|
|
48
|
+
frame = pd.read_csv(self.path, **read_csv_kwargs)
|
|
49
|
+
self._long = LongTable.from_frame(
|
|
50
|
+
frame,
|
|
51
|
+
factors=factors,
|
|
52
|
+
measures=measures,
|
|
53
|
+
level_order=level_order,
|
|
54
|
+
index_column=index_column,
|
|
55
|
+
name=self.name,
|
|
56
|
+
)
|
|
57
|
+
Log.info(
|
|
58
|
+
"loaded %s: %d row(s), %d factor(s), %d measure(s)",
|
|
59
|
+
self.path.name,
|
|
60
|
+
len(frame),
|
|
61
|
+
len(self._long.factors),
|
|
62
|
+
len(self._long.measures),
|
|
63
|
+
layer=LAYER,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _table(self) -> LongTable:
|
|
67
|
+
return self._long
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""In-memory DataFrame source — the scifor-equivalent entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ..table import LongTable
|
|
10
|
+
from .base import BaseSource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataFrameSource(BaseSource):
|
|
14
|
+
"""
|
|
15
|
+
Wrap a DataFrame you already have.
|
|
16
|
+
|
|
17
|
+
This is the direct analogue of ``scifor.for_each`` taking a plain table:
|
|
18
|
+
no database, no configuration, no file IO. Column roles are inferred from
|
|
19
|
+
observed value shapes when not given (see ``LongTable.from_frame``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
frame: pd.DataFrame,
|
|
25
|
+
*,
|
|
26
|
+
factors: Iterable[str] | None = None,
|
|
27
|
+
measures: Iterable[str] | None = None,
|
|
28
|
+
level_order: dict[str, list[Any]] | None = None,
|
|
29
|
+
variant_factors: Iterable[str] = (),
|
|
30
|
+
index_column: str | None = None,
|
|
31
|
+
name: str = "dataframe",
|
|
32
|
+
) -> None:
|
|
33
|
+
self.name = name
|
|
34
|
+
self._long = LongTable.from_frame(
|
|
35
|
+
frame,
|
|
36
|
+
factors=factors,
|
|
37
|
+
measures=measures,
|
|
38
|
+
level_order=level_order,
|
|
39
|
+
variant_factors=variant_factors,
|
|
40
|
+
index_column=index_column,
|
|
41
|
+
name=name,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _table(self) -> LongTable:
|
|
45
|
+
return self._long
|