diff --git a/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.ipynb b/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.ipynb index 34d8ec2..ef681bf 100644 --- a/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.ipynb +++ b/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.ipynb @@ -15,9 +15,15 @@ "\n", "An omnibus analysis across groups\n", "is combined with posthoc analysis between each set of the separate groups.\n", + "The omnibus test's adjusted p-value and rejection flag are in `padj` and\n", + "`rejected`; pairwise posthoc results use `posthoc pvalue adj` and\n", + "`posthoc rejected`.\n", + "`Method` records the selected omnibus test (`One-way anova` here); use\n", + "`run_ancova` when covariates should be included in the omnibus model.\n", "\n", "The function is the same as for the two groups case. The `group1` and\n", - "`group2` columns give the posthoc comparison.\n" + "`group2` columns give the posthoc comparison, also available as\n", + "`posthoc comparison` in the form `group1~~group2`.\n" ] }, { @@ -85,7 +91,6 @@ "id": "2d8f94e4", "metadata": {}, "source": [ - "# ANOVA analysis for two groups\n", "Use combined dataset for ANOVA analysis." ] }, @@ -176,8 +181,18 @@ "lines_to_next_cell": 2 }, "source": [ - "## With four groups\n", - "Acore make each combinatorial comparison between groups in the group column." + "# Run ANOVA with four groups\n", + "Acore runs an omnibus test asking if any of the groups are significantly different, so\n", + "if a feature as any effect (F-test between a regression with group dummies and without\n", + "any) and also runs pairwise (posthoc) tests to identify which groups are different and\n", + "in which direction. The omnibus test's adjusted p-value and rejection flag are in\n", + "`padj` and `rejected`; pairwise posthoc results use `posthoc pvalue adj` and `posthoc\n", + "rejected`. `Method` records the selected omnibus test (`One-way anova` here); use\n", + "`run_ancova` when covariates should be included in the omnibus model.\n", + "\n", + "The function is the same as for the two groups case. The `group1` and `group2` columns\n", + "give the posthoc comparison, also available as `posthoc comparison` in the form\n", + "`group1~~group2`." ] }, { @@ -206,7 +221,8 @@ "id": "59527f87", "metadata": {}, "source": [ - "### pairwise t-test results:" + "## pairwise (posthoc) t-test results\n", + "posthoc t-test results to identify which groups are different and in which direction." ] }, { @@ -223,6 +239,7 @@ "cols_pairwise_ttest = [\n", " # \"group1\",\n", " # \"group2\",\n", + " \"posthoc comparison\",\n", " \"mean(group1)\",\n", " \"std(group1)\",\n", " \"mean(group2)\",\n", @@ -234,6 +251,7 @@ " \"posthoc tail\",\n", " \"posthoc pvalue\",\n", " \"posthoc pvalue adj\",\n", + " \"posthoc rejected\",\n", " \"posthoc BF10\",\n", " \"posthoc effsize\",\n", " # \"identifier\",\n", @@ -293,9 +311,11 @@ "lines_to_next_cell": 2 }, "source": [ - "## Volcano plot of ANOVA results\n", - "- volcano plot of ANOVA (omnibus test) results\n", - "- more than one group present in the example." + "## Posthoc volcano plot: WRP versus WT\n", + "Select one pairwise comparison so each identifier has one point.\n", + "The x-axis shows its `log2FC`, the y-axis its posthoc p-value, and\n", + "the color its posthoc FDR significance decision (`posthoc rejected`).\n", + "Change `comparison` to another value from the `posthoc comparison` column." ] }, { @@ -309,17 +329,20 @@ }, "outputs": [], "source": [ + "comparison = \"WRP~~WT\"\n", + "pairwise_results = anova.loc[anova[\"posthoc comparison\"] == comparison].reset_index()\n", + "\n", "scatter_plot_adv = vuecore.plots.basic.scatter.create_scatter_plot(\n", - " data=anova.reset_index(),\n", + " data=pairwise_results,\n", " x=\"log2FC\",\n", - " y=\"-log10 pvalue\",\n", - " color=\"rejected\",\n", - " title=\"Simple Volcano Plot\",\n", - " subtitle=\"Visualizing ANOVA results\",\n", + " y=\"-log10 posthoc pvalue\",\n", + " color=\"posthoc rejected\",\n", + " title=\"Posthoc Volcano Plot\",\n", + " subtitle=comparison.replace(\"~~\", \" versus \"),\n", " labels={\n", - " \"log2FC\": \"Log2 Fold Change\",\n", - " \"-log10 pvalue\": \"-log10(p-value)\",\n", - " \"rejected\": \"FDR corrected Significant\",\n", + " \"log2FC\": f\"log2 fold change ({comparison.replace('~~', ' / ')})\",\n", + " \"-log10 posthoc pvalue\": \"-log10(posthoc p-value)\",\n", + " \"posthoc rejected\": \"Posthoc FDR corrected significant\",\n", " \"identifier\": \"Protein Identifier\",\n", " },\n", " hover_data=[\"identifier\", \"group1\", \"group2\"],\n", diff --git a/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.py b/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.py index e13638f..7ba281c 100644 --- a/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.py +++ b/docs/examples_single_analysis/diff_regulation_anova_more_than_two_groups.py @@ -20,9 +20,15 @@ # # An omnibus analysis across groups # is combined with posthoc analysis between each set of the separate groups. +# The omnibus test's adjusted p-value and rejection flag are in `padj` and +# `rejected`; pairwise posthoc results use `posthoc pvalue adj` and +# `posthoc rejected`. +# `Method` records the selected omnibus test (`One-way anova` here); use +# `run_ancova` when covariates should be included in the omnibus model. # # The function is the same as for the two groups case. The `group1` and -# `group2` columns give the posthoc comparison. +# `group2` columns give the posthoc comparison, also available as +# `posthoc comparison` in the form `group1~~group2`. # # %% tags=["hide-output"] @@ -53,7 +59,6 @@ factor_and_covars: list[str] = [group, *covariates] # %% [markdown] -# # ANOVA analysis for two groups # Use combined dataset for ANOVA analysis. # %% tags=["hide-input"] @@ -83,8 +88,18 @@ # %% [markdown] -# ## With four groups -# Acore make each combinatorial comparison between groups in the group column. +# # Run ANOVA with four groups +# Acore runs an omnibus test asking if any of the groups are significantly different, so +# if a feature as any effect (F-test between a regression with group dummies and without +# any) and also runs pairwise (posthoc) tests to identify which groups are different and +# in which direction. The omnibus test's adjusted p-value and rejection flag are in +# `padj` and `rejected`; pairwise posthoc results use `posthoc pvalue adj` and `posthoc +# rejected`. `Method` records the selected omnibus test (`One-way anova` here); use +# `run_ancova` when covariates should be included in the omnibus model. +# +# The function is the same as for the two groups case. The `group1` and `group2` columns +# give the posthoc comparison, also available as `posthoc comparison` in the form +# `group1~~group2`. # %% @@ -102,12 +117,14 @@ anova.head().T # %% [markdown] -# ### pairwise t-test results: +# ## pairwise (posthoc) t-test results +# posthoc t-test results to identify which groups are different and in which direction. # %% tags=["hide-input"] cols_pairwise_ttest = [ # "group1", # "group2", + "posthoc comparison", "mean(group1)", "std(group1)", "mean(group2)", @@ -119,6 +136,7 @@ "posthoc tail", "posthoc pvalue", "posthoc pvalue adj", + "posthoc rejected", "posthoc BF10", "posthoc effsize", # "identifier", @@ -143,23 +161,28 @@ view # %% [markdown] -# ## Volcano plot of ANOVA results -# - volcano plot of ANOVA (omnibus test) results -# - more than one group present in the example. +# ## Posthoc volcano plot: WRP versus WT +# Select one pairwise comparison so each identifier has one point. +# The x-axis shows its `log2FC`, the y-axis its posthoc p-value, and +# the color its posthoc FDR significance decision (`posthoc rejected`). +# Change `comparison` to another value from the `posthoc comparison` column. # %% tags=["hide-input"] +comparison = "WRP~~WT" +pairwise_results = anova.loc[anova["posthoc comparison"] == comparison].reset_index() + scatter_plot_adv = vuecore.plots.basic.scatter.create_scatter_plot( - data=anova.reset_index(), + data=pairwise_results, x="log2FC", - y="-log10 pvalue", - color="rejected", - title="Simple Volcano Plot", - subtitle="Visualizing ANOVA results", + y="-log10 posthoc pvalue", + color="posthoc rejected", + title="Posthoc Volcano Plot", + subtitle=comparison.replace("~~", " versus "), labels={ - "log2FC": "Log2 Fold Change", - "-log10 pvalue": "-log10(p-value)", - "rejected": "FDR corrected Significant", + "log2FC": f"log2 fold change ({comparison.replace('~~', ' / ')})", + "-log10 posthoc pvalue": "-log10(posthoc p-value)", + "posthoc rejected": "Posthoc FDR corrected significant", "identifier": "Protein Identifier", }, hover_data=["identifier", "group1", "group2"], diff --git a/docs/markdown_ref/acore.differential_regulation.md b/docs/markdown_ref/acore.differential_regulation.md index 03800bb..97589f7 100644 --- a/docs/markdown_ref/acore.differential_regulation.md +++ b/docs/markdown_ref/acore.differential_regulation.md @@ -68,7 +68,8 @@ if permutations>0 and Benjamini/Hochberg if permutations=0. * **non_par** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool)) – if True, normality and variance equality assumptions are checked and non-parametric test Mann Whitney U test if not passed * **Returns:** - DataFrame adhering to AnovaSchema or AnovaSchemaMultiGroup. + DataFrame adhering to AnovaSchema or AnovaSchemaMultiGroup. For more than + two groups, `posthoc comparison` labels each pair as `group1~~group2`. * **Return type:** DataFrame[[AnovaSchema](acore.types.md#acore.types.differential_analysis.AnovaSchema)] | DataFrame[[AnovaSchemaMultiGroup](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup)] diff --git a/docs/markdown_ref/acore.md b/docs/markdown_ref/acore.md index 30a7098..24d33e2 100644 --- a/docs/markdown_ref/acore.md +++ b/docs/markdown_ref/acore.md @@ -220,10 +220,12 @@ * [`AnovaSchema.to_yaml()`](acore.types.md#acore.types.differential_analysis.AnovaSchema.to_yaml) * [`AnovaSchema.validate()`](acore.types.md#acore.types.differential_analysis.AnovaSchema.validate) * [`AnovaSchemaMultiGroup`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup) + * [`AnovaSchemaMultiGroup.posthoc_comparison`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.posthoc_comparison) * [`AnovaSchemaMultiGroup.t_statistics`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.t_statistics) * [`AnovaSchemaMultiGroup.posthoc_pvalue`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.posthoc_pvalue) * [`AnovaSchemaMultiGroup.f_statistics`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.f_statistics) * [`AnovaSchemaMultiGroup.posthoc_padj`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.posthoc_padj) + * [`AnovaSchemaMultiGroup.posthoc_rejected`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.posthoc_rejected) * [`AnovaSchemaMultiGroup.posthoc_paired`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.posthoc_paired) * [`AnovaSchemaMultiGroup.posthoc_parametric`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.posthoc_parametric) * [`AnovaSchemaMultiGroup.posthoc_dof`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.posthoc_dof) @@ -260,8 +262,10 @@ * [`AnovaSchemaMultiGroup.to_yaml()`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.to_yaml) * [`AnovaSchemaMultiGroup.validate()`](acore.types.md#acore.types.differential_analysis.AnovaSchemaMultiGroup.validate) * [`AncovaSchema`](acore.types.md#acore.types.differential_analysis.AncovaSchema) + * [`AncovaSchema.posthoc_comparison`](acore.types.md#acore.types.differential_analysis.AncovaSchema.posthoc_comparison) * [`AncovaSchema.t_statistics`](acore.types.md#acore.types.differential_analysis.AncovaSchema.t_statistics) * [`AncovaSchema.posthoc_pvalue`](acore.types.md#acore.types.differential_analysis.AncovaSchema.posthoc_pvalue) + * [`AncovaSchema.posthoc_rejected`](acore.types.md#acore.types.differential_analysis.AncovaSchema.posthoc_rejected) * [`AncovaSchema.coef`](acore.types.md#acore.types.differential_analysis.AncovaSchema.coef) * [`AncovaSchema.std_err`](acore.types.md#acore.types.differential_analysis.AncovaSchema.std_err) * [`AncovaSchema.conf_int_low`](acore.types.md#acore.types.differential_analysis.AncovaSchema.conf_int_low) diff --git a/docs/markdown_ref/acore.types.md b/docs/markdown_ref/acore.types.md index 066c636..ab3af0e 100644 --- a/docs/markdown_ref/acore.types.md +++ b/docs/markdown_ref/acore.types.md @@ -201,7 +201,7 @@ Load a schema from YAML. * **Returns:** the backend-specific schema object. -#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None) +#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] Provide metadata for columns and schema level @@ -267,6 +267,8 @@ Bases: [`AnovaSchema`](#acore.types.differential_analysis.AnovaSchema) Schema for more than two groups +#### posthoc_comparison *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'posthoc comparison'* + #### t_statistics *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 'posthoc T-Statistics'* #### posthoc_pvalue *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 'posthoc pvalue'* @@ -275,6 +277,8 @@ Schema for more than two groups #### posthoc_padj *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 'posthoc pvalue adj'* +#### posthoc_rejected *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= 'posthoc rejected'* + #### posthoc_paired *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= 'posthoc Paired'* #### posthoc_parametric *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= 'posthoc Parametric'* @@ -437,7 +441,7 @@ Load a schema from YAML. * **Returns:** the backend-specific schema object. -#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None) +#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] Provide metadata for columns and schema level @@ -525,10 +529,14 @@ Bases: [`DataFrameModel`](https://pandera.readthedocs.io/en/stable/reference/gen Schema for the enrichment analysis results DataFrame. +#### posthoc_comparison *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'posthoc comparison'* + #### t_statistics *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 'posthoc T-Statistics'* #### posthoc_pvalue *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 'posthoc pvalue'* +#### posthoc_rejected *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= 'posthoc rejected'* + #### coef *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 'coef'* #### std_err *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 'std err'* @@ -683,7 +691,7 @@ Load a schema from YAML. * **Returns:** the backend-specific schema object. -#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None) +#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] Provide metadata for columns and schema level @@ -915,7 +923,7 @@ Load a schema from YAML. * **Returns:** the backend-specific schema object. -#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None) +#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] Provide metadata for columns and schema level @@ -1131,7 +1139,7 @@ Load a schema from YAML. * **Returns:** the backend-specific schema object. -#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None) +#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] Provide metadata for columns and schema level @@ -1341,7 +1349,7 @@ Load a schema from YAML. * **Returns:** the backend-specific schema object. -#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None) +#### *classmethod* get_metadata() → [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] Provide metadata for columns and schema level diff --git a/src/acore/differential_regulation/__init__.py b/src/acore/differential_regulation/__init__.py index a86ea62..c7624da 100644 --- a/src/acore/differential_regulation/__init__.py +++ b/src/acore/differential_regulation/__init__.py @@ -97,7 +97,8 @@ def run_anova( :param bool is_logged: whether data is log-transformed :param bool non_par: if True, normality and variance equality assumptions are checked and non-parametric test Mann Whitney U test if not passed - :return: DataFrame adhering to AnovaSchema or AnovaSchemaMultiGroup. + :return: DataFrame adhering to AnovaSchema or AnovaSchemaMultiGroup. For more than + two groups, ``posthoc comparison`` labels each pair as ``group1~~group2``. :rtype: DataFrame[AnovaSchema] | DataFrame[AnovaSchemaMultiGroup] Example:: diff --git a/src/acore/differential_regulation/tests.py b/src/acore/differential_regulation/tests.py index beb0be9..3cec07a 100644 --- a/src/acore/differential_regulation/tests.py +++ b/src/acore/differential_regulation/tests.py @@ -491,6 +491,10 @@ def format_anova_table( res["log2FC"] = np.nan res = res.reset_index() + if {"group1", "group2"}.issubset(res.columns): + res["posthoc comparison"] = ( + res["group1"].astype(str) + "~~" + res["group2"].astype(str) + ) res["rejected"] = res["padj"] < alpha # ! double check this if "posthoc pvalue" in res.columns: diff --git a/src/acore/multiple_testing/__init__.py b/src/acore/multiple_testing/__init__.py index 01dd98c..159333f 100644 --- a/src/acore/multiple_testing/__init__.py +++ b/src/acore/multiple_testing/__init__.py @@ -213,12 +213,22 @@ def correct_pairwise_ttest(df, alpha, correction="fdr_bh", pvalue_col="posthoc p for comparison in df.groupby(["group1", "group2"]).groups: index = df.groupby(["group1", "group2"]).groups.get(comparison) posthoc_pvalues = df.loc[index, pvalue_col].tolist() - _, _padj = apply_pvalue_correction( + rejected, padj = apply_pvalue_correction( posthoc_pvalues, alpha=alpha, method=correction ) - _posthoc_df = pd.DataFrame({"index": index, f"{pvalue_col} adj": _padj}) - posthoc_df.append(_posthoc_df) + posthoc_df.append( + pd.DataFrame( + { + "index": index, + f"{pvalue_col} adj": padj, + "posthoc rejected": pd.Series(rejected) + .fillna(False) + .astype(bool) + .to_numpy(), + } + ) + ) posthoc_df = pd.concat(posthoc_df) posthoc_df = posthoc_df.set_index("index") df = df.join(posthoc_df) diff --git a/src/acore/types/differential_analysis.py b/src/acore/types/differential_analysis.py index f48eea2..0abb115 100644 --- a/src/acore/types/differential_analysis.py +++ b/src/acore/types/differential_analysis.py @@ -39,10 +39,12 @@ class AnovaSchemaMultiGroup(AnovaSchema): Schema for more than two groups """ + posthoc_comparison: str = Field(alias="posthoc comparison", nullable=False) t_statistics: float = Field(alias="posthoc T-Statistics") posthoc_pvalue: float = Field(alias="posthoc pvalue") f_statistics: float = Field(alias="F-statistics") posthoc_padj: float = Field(alias="posthoc pvalue adj", ge=0, le=1) + posthoc_rejected: bool = Field(alias="posthoc rejected", nullable=False) posthoc_paired: bool = Field(alias="posthoc Paired", ge=0, le=1) posthoc_parametric: bool = Field(alias="posthoc Parametric", ge=0, le=1) @@ -58,8 +60,10 @@ class AncovaSchema(DataFrameModel): Schema for the enrichment analysis results DataFrame. """ + posthoc_comparison: str = Field(alias="posthoc comparison", nullable=False) t_statistics: float = Field(alias="posthoc T-Statistics") posthoc_pvalue: float = Field(alias="posthoc pvalue") + posthoc_rejected: bool = Field(alias="posthoc rejected", nullable=False) coef: float = Field() std_err: float = Field(alias="std err") conf_int_low: float = Field(alias="Conf. Int. Low") diff --git a/tests/test_differential_regulation.py b/tests/test_differential_regulation.py index 0e88229..60bb70e 100644 --- a/tests/test_differential_regulation.py +++ b/tests/test_differential_regulation.py @@ -283,6 +283,52 @@ def test_run_anova_three_groups_drop_cols_empty(self): self.assertIsInstance(result, pd.DataFrame) self.assertIn("identifier", result.columns) + def test_run_anova_posthoc_comparisons(self): + for subject in (None, "subject"): + with self.subTest(subject=subject): + result = dr.run_anova( + self.df_3groups, + drop_cols=[], + subject=subject, + group="group", + ) + self.assertEqual( + set( + result[["posthoc comparison", "group1", "group2"]] + .drop_duplicates() + .itertuples(index=False, name=None) + ), + {("A~~B", "A", "B"), ("A~~C", "A", "C"), ("B~~C", "B", "C")}, + ) + self.assertEqual( + result.groupby(["identifier", "posthoc comparison"]).size().max(), + 1, + ) + + def test_run_anova_distinguishes_omnibus_and_posthoc_rejections(self): + result = dr.run_anova( + self.df_3groups, + drop_cols=[], + subject=None, + group="group", + ) + + self.assertIn("rejected", result.columns) + self.assertIn("posthoc rejected", result.columns) + pd.testing.assert_series_equal( + result["rejected"], + result["padj"] < 0.05, + check_names=False, + ) + pd.testing.assert_series_equal( + result["posthoc rejected"], + result["posthoc pvalue adj"] < 0.05, + check_names=False, + ) + self.assertEqual( + result.groupby("identifier")["F-statistics"].nunique().max(), 1 + ) + def test_run_anova_paired_three_groups_drop_cols_none(self): """run_anova should not crash when drop_cols=None (>2-group paired case).