scistackplotdb 0.1.44__tar.gz → 0.1.45__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scistackplotdb
3
- Version: 0.1.44
3
+ Version: 0.1.45
4
4
  Summary: scidb-backed plotting: load variables into long tables and plot them with scistackplot
5
5
  Author: SciStack Contributors
6
6
  License-Expression: MIT
@@ -17,10 +17,10 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Scientific/Engineering :: Visualization
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
- Requires-Dist: sciduckdb==0.1.44
21
- Requires-Dist: scistack-db==0.1.44
22
- Requires-Dist: scistacklog==0.1.44
23
- Requires-Dist: scistackplot==0.1.44
20
+ Requires-Dist: sciduckdb==0.1.45
21
+ Requires-Dist: scistack-db==0.1.45
22
+ Requires-Dist: scistacklog==0.1.45
23
+ Requires-Dist: scistackplot==0.1.45
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: matplotlib>=3.6; extra == 'dev'
26
26
  Requires-Dist: pytest-cov>=4.0; extra == 'dev'
@@ -818,9 +818,7 @@ class ScidbSource(BaseSource):
818
818
  return frame, attached
819
819
 
820
820
  for group in factor_variables:
821
- name = group.variable
822
- variable = self._variable_frame(name)
823
- source_column = self._factor_source_column(variable, group)
821
+ variable = self._variable_frame(group.variable)
824
822
  factor = group.factor_name
825
823
  if factor in frame.columns:
826
824
  # Never merge over a column that is already there. pandas would
@@ -846,10 +844,7 @@ class ScidbSource(BaseSource):
846
844
  f"attach one of its values to each row. Group by a variable "
847
845
  f"recorded at or above the level of the data."
848
846
  )
849
- on = list(variable.levels)
850
- right = self._pinned_variant(variable, group)
851
- right = right.rename(columns={source_column: factor})
852
- right = self._one_label_per_location(right, on, factor, group)
847
+ on, right = self._keyed_values(variable, group)
853
848
  frame = frame.merge(right, on=on, how="left")
854
849
 
855
850
  # A row the grouping variable says nothing about keeps its place and
@@ -895,6 +890,53 @@ class ScidbSource(BaseSource):
895
890
  )
896
891
  return frame, attached
897
892
 
893
+ def _keyed_values(self, variable_frame, group: FactorVariable):
894
+ """``(on, right)``: one grouping's or covariate's values, pinned to its
895
+ variant, one per schema location, in a column named
896
+ ``group.factor_name``; ``on`` = the schema keys it is recorded at.
897
+
898
+ The ONE loader behind both joins: a grouping variable merged onto the
899
+ long table (:meth:`_attach_factor_variables`) and a covariate merged
900
+ onto StatStudio's analysis rows (:meth:`covariate_values`), so "which
901
+ value does this location get" cannot mean two things.
902
+ """
903
+ source_column = self._factor_source_column(variable_frame, group)
904
+ on = list(variable_frame.levels)
905
+ right = self._pinned_variant(variable_frame, group)
906
+ right = right.rename(columns={source_column: group.factor_name})
907
+ right = self._one_label_per_location(right, on, group.factor_name, group)
908
+ return on, right
909
+
910
+ def covariate_values(self, group: FactorVariable):
911
+ """``(keys, frame)``: a numeric covariate for StatStudio's mixed model
912
+ (``scistackstats.tables.load_covariates``), keyed by the schema keys
913
+ it is recorded at. ``frame`` has ``[*keys, group.factor_name]``.
914
+
915
+ Joined per analysis ROW by the caller, after the figure's reduction,
916
+ so a covariate recorded below the rows' level is refused there with
917
+ the reason. A text column is refused here: a covariate is a number.
918
+ """
919
+ import pandas as pd
920
+
921
+ variable = self._variable_frame(group.variable)
922
+ on, right = self._keyed_values(variable, group)
923
+ values = right[group.factor_name]
924
+ numeric = pd.to_numeric(values, errors="coerce")
925
+ if values.notna().any() and numeric.notna().sum() == 0:
926
+ raise ValueError(
927
+ f"{group.label} holds text, not numbers: a covariate is numeric "
928
+ f"(group by it instead)"
929
+ )
930
+ right = right.assign(**{group.factor_name: numeric})
931
+ Log.info(
932
+ "covariate %r: %d value(s) keyed by %s",
933
+ group.label,
934
+ int(numeric.notna().sum()),
935
+ on,
936
+ layer=LAYER,
937
+ )
938
+ return on, right
939
+
898
940
  def _pinned_variant(self, variable_frame, group: FactorVariable):
899
941
  """The grouping variable's rows, narrowed to the pinned variant.
900
942
 
@@ -1748,6 +1790,52 @@ class ScidbSource(BaseSource):
1748
1790
  )
1749
1791
  return {"offered": offered, "rejected": rejected}
1750
1792
 
1793
+ def covariate_variables(self, measure: str) -> dict:
1794
+ """Which VARIABLES may supply a numeric covariate for ``measure`` —
1795
+ the DAG canvas half of StatStudio's Add covariate picker.
1796
+
1797
+ :meth:`groupable_variables`' offers with the meaning of "numeric"
1798
+ turned around: a whole variable with numeric values and every wide
1799
+ table (its columns are :meth:`covariate_columns`' question) are
1800
+ offered; a categorical variable is refused, with the reason. The
1801
+ plotted variable's own columns are not offered: covariates are joined
1802
+ onto the analysis rows, where the measure's own fields are not
1803
+ carried.
1804
+ """
1805
+ found = self.groupable_variables(measure)
1806
+ offered, rejected = [], dict(found["rejected"])
1807
+ for entry in found["offered"]:
1808
+ if entry.get("own"):
1809
+ continue
1810
+ if entry["kind"] == "categorical":
1811
+ rejected[entry["label"]] = "text values: a covariate is numeric (group by it instead)"
1812
+ continue
1813
+ offered.append(entry)
1814
+ Log.info("covariate_variables(%s): %d offered, %d rejected", measure, len(offered), len(rejected),
1815
+ layer=LAYER)
1816
+ return {"offered": offered, "rejected": rejected}
1817
+
1818
+ def covariate_columns(self, measure: str, variable: str) -> dict:
1819
+ """Which COLUMNS of ``variable`` may be a covariate: the numeric ones
1820
+ (the mirror of :meth:`groupable_columns`, which offers the text
1821
+ ones). Same ``{"offered", "rejected"}`` shape, same type pre-filter."""
1822
+ rejected: dict[str, str] = {}
1823
+ offered: list[dict] = []
1824
+ types = data_column_types_for(self._db, variable)
1825
+ with Log.timer(f"covariate_columns({variable})", layer=LAYER):
1826
+ for column, data_type in types.items():
1827
+ label = f"{variable}.{column}"
1828
+ if is_container_type(data_type):
1829
+ continue
1830
+ shape = classify_value(sample_column_value(self._db, variable, column))
1831
+ if shape in (Shape.SERIES_1D, Shape.MATRIX_2D, Shape.UNKNOWN):
1832
+ continue
1833
+ if shape is Shape.CATEGORICAL:
1834
+ rejected[label] = "text column: a covariate is numeric (group by it instead)"
1835
+ continue
1836
+ offered.append(self._offer(variable, column, []))
1837
+ return {"offered": offered, "rejected": rejected}
1838
+
1751
1839
  def _groupable_columns(
1752
1840
  self,
1753
1841
  variable: str,