diff --git a/pyproject.toml b/pyproject.toml index cff5f29..3dc6c7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ dependencies = [ # * Plotting "matplotlib", "seaborn<=0.12.2", #!! 0.13 has issues with hue + "Pillow>=10.2.0", #!! github security risk #* Statistics "scipy", # "statannot", #' Superseded by statannotations diff --git a/requirements.txt b/requirements.txt index af1decc..925a080 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ pandas-flavor==0.6.0 parso==0.8.3 patsy==0.5.3 pexpect==4.8.0 -Pillow==10.1.0 +Pillow>=10.2.0 pingouin==0.5.3 platformdirs==4.0.0 prompt-toolkit==3.0.41 diff --git a/src/plotastic/dimensions/dataframetool.py b/src/plotastic/dimensions/dataframetool.py index 82c40d3..9cc6aca 100644 --- a/src/plotastic/dimensions/dataframetool.py +++ b/src/plotastic/dimensions/dataframetool.py @@ -494,7 +494,7 @@ def data_iter__allkeys_group_skip_empty(self) -> Generator: # == TRANSFORM ===================================================== @staticmethod - def _rename_y(y: str, func: str) -> str: + def _rename_y(y: str, func: str | Callable) -> str: """Renames the y column to reflect the transformation Args: @@ -514,7 +514,7 @@ def _add_transform_col( df: pd.DataFrame, y_raw: str, y_new: str, - func: str, + func: Callable, ) -> pd.DataFrame: """Adds a column to the dataframe that contains the transformed data @@ -542,7 +542,7 @@ def _add_transform_col( return df def transform_y( - self, func: str | Callable, inplace=True + self, func: str | Callable, inplace=False ) -> "DataFrameTool | DataAnalysis": """DOC: Transforms the data, changes dv property""" diff --git a/src/plotastic/stat/assumptions.py b/src/plotastic/stat/assumptions.py index 2aee06c..6044102 100644 --- a/src/plotastic/stat/assumptions.py +++ b/src/plotastic/stat/assumptions.py @@ -154,6 +154,12 @@ def check_sphericity( :return: _description_ :rtype: pd.DataFrame """ + ### Make sure subject is specified + if self.subject is None: + raise ValueError( + "Testing sphericity requires a subject to be specified." + ) + # TODO: Add option to use x or hue as within-factors ### All diff --git a/src/plotastic/stat/statresults.py b/src/plotastic/stat/statresults.py index e856c57..00cc07c 100644 --- a/src/plotastic/stat/statresults.py +++ b/src/plotastic/stat/statresults.py @@ -24,23 +24,23 @@ class StatResults: # == INIT ========================================================================== def __init__(self): ### Data Tables - self.DF_normality: pd.DataFrame = self.DEFAULT_UNCHECKED - self.DF_homoscedasticity: pd.DataFrame = self.DEFAULT_UNCHECKED - self.DF_sphericity: pd.DataFrame = self.DEFAULT_UNCHECKED + self.DF_normality: pd.DataFrame | str = self.DEFAULT_UNCHECKED + self.DF_homoscedasticity: pd.DataFrame | str = self.DEFAULT_UNCHECKED + self.DF_sphericity: pd.DataFrame | str = self.DEFAULT_UNCHECKED - self.DF_omnibus_anova: pd.DataFrame = self.DEFAULT_UNTESTED - self.DF_omnibus_rmanova: pd.DataFrame = self.DEFAULT_UNTESTED - self.DF_omnibus_kruskal: pd.DataFrame = self.DEFAULT_UNTESTED - self.DF_omnibus_friedman: pd.DataFrame = self.DEFAULT_UNTESTED - self.DF_posthoc: pd.DataFrame = self.DEFAULT_UNTESTED - self.DF_bivariate: pd.DataFrame = self.DEFAULT_UNTESTED + self.DF_omnibus_anova: pd.DataFrame | str = self.DEFAULT_UNTESTED + self.DF_omnibus_rmanova: pd.DataFrame | str = self.DEFAULT_UNTESTED + self.DF_omnibus_kruskal: pd.DataFrame | str = self.DEFAULT_UNTESTED + self.DF_omnibus_friedman: pd.DataFrame | str = self.DEFAULT_UNTESTED + self.DF_posthoc: pd.DataFrame | str = self.DEFAULT_UNTESTED + self.DF_bivariate: pd.DataFrame | str = self.DEFAULT_UNTESTED ### Assessments = Summarizing results from multiple groups - self._normal: bool = self.DEFAULT_UNASSESSED - self._homoscedastic: bool = self.DEFAULT_UNASSESSED - self._spherical: bool = self.DEFAULT_UNASSESSED + self._normal: bool | str = self.DEFAULT_UNASSESSED + self._homoscedastic: bool | str = self.DEFAULT_UNASSESSED + self._spherical: bool | str = self.DEFAULT_UNASSESSED - self._parametric: bool = self.DEFAULT_UNASSESSED + self._parametric: bool | str = self.DEFAULT_UNASSESSED # == # == Summarize Results ============================================================= @@ -116,7 +116,7 @@ def assess_parametric(self): # == # == EXPORT ======================================================================== - def save(self, fname: str = "plotastic_results") -> None: + def save(self, fname: str | Path = "plotastic_results", verbose=True) -> None: """Exports all statistics to one excel file. Different sheets for different tests @@ -138,6 +138,10 @@ def save(self, fname: str = "plotastic_results") -> None: ### Save writer.close() + + ### Tell save location + if verbose: + print(f"Saved results to {fname.resolve()}") # !! diff --git a/tests/DA_configs.py b/tests/DA_configs.py index 2c9195a..ba6f03d 100644 --- a/tests/DA_configs.py +++ b/tests/DA_configs.py @@ -132,6 +132,8 @@ # %% # == Make Dataanalysis objects ========================================= + + def make_DA_statistics(dataset: str = "qpcr") -> plst.DataAnalysis: """Makes a DA object with every possible data stored in it @@ -170,6 +172,7 @@ def make_DA_statistics(dataset: str = "qpcr") -> plst.DataAnalysis: def make_DA_plot(dataset: str = "qpcr") -> plst.DataAnalysis: + """A DA that has a plot""" with warnings.catch_warnings(): warnings.simplefilter("ignore") ### Load example data @@ -184,6 +187,7 @@ def make_DA_plot(dataset: str = "qpcr") -> plst.DataAnalysis: def make_DA_all(dataset: str) -> plst.DataAnalysis: + """A DA with all possible statistics and a plot""" with warnings.catch_warnings(): warnings.simplefilter("ignore") diff --git a/tests/DA_configs2.py b/tests/DA_configs2.py new file mode 100644 index 0000000..4697c16 --- /dev/null +++ b/tests/DA_configs2.py @@ -0,0 +1,9 @@ + +### They are all non-empty +StatTestCases = [ + "all", + "paired", + "unpaired", + "unpaired", + "parametric_paired", +] \ No newline at end of file diff --git a/tests/DA_utils.py b/tests/DA_utils.py new file mode 100644 index 0000000..509a5b8 --- /dev/null +++ b/tests/DA_utils.py @@ -0,0 +1,81 @@ +"""A utility class that creates DataAnalysis objects for testing""" +# %% + + +import pandas as pd + +import plotastic as plst +from plotastic.dataanalysis.dataanalysis import DataAnalysis +from plotastic.utils.subcache import SubCache + +import DA_configs as dac + +# %% +# == Class CreateDA ==================================================== + + +class TestDA(DataAnalysis): + def __init__( + self, + data: pd.DataFrame, + dims: dict, + subject: str = None, + levels: list[tuple[str]] = None, + title: str = "untitled", + verbose=False, + ) -> DataAnalysis: + kws = dict( + data=data, + dims=dims, + subject=subject, + levels=levels, #' Introduced by DataFrameTool + title=title, #' Introduced by DataAnalysis + verbose=verbose, #' Introduced by DataAnalysis + ) + + super().__init__(**kws) + + def perform_statistics_unpaired(self, parametric=True) -> "TestDA": + """Perform unpaired statistics""" + ### Assumptions + self.check_normality() + self.check_homoscedasticity() + + ### Omnibus + if parametric: + self.omnibus_anova() + else: + self.omnibus_kruskal() + + ### PostHoc + self.test_pairwise(parametric=parametric) + + return self + + def perform_statistics_paired(self, parametric=True) -> "TestDA": + """Perform unpaired statistics""" + ### Assumptions + self.check_normality() + self.check_homoscedasticity() + self.check_sphericity() + + ### Omnibus + if parametric: + self.omnibus_anova() + else: + self.omnibus_kruskal() + + ### PostHoc + self.test_pairwise(parametric=parametric) + + return self + + + +if __name__ == "__main__": + pass + # %% + dims = dac.dims_withempty_tips[0] + data = dac.DF_tips + DA = TestDA(data=data, dims=dims) + \ No newline at end of file diff --git a/tests/_hierarchical_dims_test.py b/tests/_hierarchical_dims_test.py new file mode 100644 index 0000000..e69de29