Source code for plotastic.dimensions.dims

#
# %% Importds

from typing import TYPE_CHECKING

from typing import Dict, Literal
from copy import copy, deepcopy

if TYPE_CHECKING:
    from plotastic.dataanalysis.dataanalysis import DataAnalysis


# TODO maybe refactor this to specify statistical type of data
# class Dimension:
#     def __init__(
#         self, name: str, scale_of_measurement: Literal["nominal", "ordinal", "cardinal"]
#     ) -> "Dimension":
#         """_summary_

#         Args:
#             name (str): _description_
#             scale_of_measurement (str):
#             (https://en.wikipedia.org/wiki/Level_of_measurement)
#             What's the scale of measurement?
#             * Nominal data:
#                 * Categorical data that has no order
#                 * e.g. colors, names, etc.
#             * Ordinal data:
#                 * Categorical data that has order
#                 * e.g. grades, sizes, etc.
#                 * This works independently from ordered pd.Categorical type. That is used to place plots in the right order.
#             * Cardinal data:
#                 * Numerical data that has order
#                 * Three types: interval, ratio, and absolute
#                 * Don't let them confuse you. It's an old scale and is often contested
#                 * Interval data:
#                     * Numerical data that has order and equal intervals
#                     * e.g. temperature [°C], dates, etc.
#                 * Ratio data:
#                     * Numerical data that has order, equal intervals, and a true zero
#                     * Might not have a unit, since it was divided by itself
#                     * e.g.  temperature [Kelvin], height, weight, etc.
#                 * Absolute data:
#                     * Numerical data that has order, equal intervals, a true zero, and an absolute scale

#         Returns:
#             Dimension: _description_
#         """
#         # * "nominal", "ordinal", "interval", "ratio

#         self.name = name
#         self.som = scale_of_measurement

#     #
#     #

# %% class Dims


[docs] class Dims: # == Init .... def __init__( self, y: str = None, x: str = None, hue: str = None, row: str = None, col: str = None, ): ### Define Dims self.y = y self.x = x self.hue = hue self.row = row self.col = col # self._by = None # self.som = dict(y="interval", x="ordinal", row="") # if som: # * SOM = Scale of Measurement / Skalenniveau # self.som = som # else: # self.som = dict(y= "continuous", ) # # # # == Properties .... @property def has_hue(self) -> bool: return not self.hue is None # @property # def by(self) -> list[str] | None: # if self._by: # return self._by # elif self.row and self.col: # return [self.row, self.col] # elif self.row: # return [self.row] # elif self.col: # return [self.col] # else: # return None
[docs] def asdict(self, incl_None=True) -> dict: d = dict(y=self.y, x=self.x, hue=self.hue, row=self.row, col=self.col) if not incl_None: d = {k: v for (k, v) in d.items() if (not v is None)} return d
[docs] def set(self, inplace=False, **kwargs) -> "Dims | DataAnalysis": newobj = self if inplace else copy(self) for k, v in kwargs.items(): v = v if not v == "none" else None setattr(newobj, k, v) return newobj
[docs] def getvalues(self, keys: list[str] | tuple[str], *args): """ Converts a list of dimensions into a list of dimension values, e.g. :param keys: ["x", "y", "col"] :return: e.g. ["smoker", "tips", "day"] """ defkeys = ("x", "y", "hue", "row", "col") l = [] keys = [keys] + [arg for arg in args] for key in keys: assert key in defkeys, f"#! '{key}' should have been one of {defkeys}" l.append(getattr(self, key)) return l
[docs] def switch( self, *keys: str, inplace=False, verbose=True, **kwarg: str | Dict[str, str] ) -> "Dims | DataAnalysis": """ Set attributes. Detects Duplicates, switches automatically :param keys: Two dimensions to switch. Only 2 Positional arguments allowed. Use e.g. dims.switch("x", "hue", **kwargs) :param inplace: Decide if this switching should change the dims object permanently (analogously to pandas dataframe). If False, you should pass return value into a variable :param verbose: Whether to print out switched values :param kwarg: e.g. dict(row="smoker") :return: dims object with switched parameters """ """HANDLE ARGUMENTS if keys are passed, e.g. dims.switch("x","row",**kwargs)""" if len(keys) == 0: pass elif len(keys) == 2: assert len(kwarg) == 0, "#! Can't switch when both keys and kwarg is passed" values = self.getvalues(*keys) kwarg[keys[0]] = values[1] else: raise AssertionError(f"#! '{keys}' should have been of length 2") assert len(kwarg) == 1, f"#! {kwarg} should be of length 1 " """PRINT FIRST LINE""" if verbose: todo = "RE-WRITING" if inplace else "TEMPORARY CHANGING:" print( f"#! {todo} {self.__class__.__name__} with keys: '{keys}' and kwarg: {kwarg}:" ) print(" (dim =\t'old' -> 'new')") ### SWITCH IT ### COPY OBJECT original: dict = deepcopy( self.asdict(incl_None=True), ) newobj = self if inplace else deepcopy(self) qK, qV = *kwarg.keys(), *kwarg.values() replace_v = "none" for oK, oV in original.items(): # Original Object if qK == oK: replace_v = oV setattr(newobj, qK, qV) elif qK != oK and oV == qV: replace_v = original[qK] setattr(newobj, oK, replace_v) assert ( replace_v != "none" ), f"#! Did not find {list(kwarg.keys())} in dims {list(original.keys())}" ### PRINT THE OVERVIEW OF THE NEW MAPPING""" if verbose: for (oK, oV), nV in zip(original.items(), newobj.asdict().values()): pre = " " if oV != nV and oV == replace_v: # or replace_v == "none": printval = f"'{replace_v}' -> '{qV}'" pre = ">>" elif oV != nV and oV != replace_v: printval = f"'{oV}' -> '{replace_v}'" pre = " <" else: # oV == nV printval = f"'{oV}'" if type(oV) is str else f"{oV}" if len(oK) < 3: oK = oK + " " printval = printval.replace("'None'", "None") # REMOVE QUOTES print(f" {pre} {oK} =\t{printval}") ### x AND y MUST NOT BE None""" assert not None in [self.y, self.x], "#! This switch causes x or y to be None" return newobj