From 4d3fc8d825e9d5f8b0f916b129f3c1b0f673450b Mon Sep 17 00:00:00 2001 From: Victor Vargas Date: Wed, 28 May 2025 11:18:42 +0200 Subject: [PATCH 1/3] ENH: Added create_homogeneous_subsets_dataframe to the TukeyHSDResults --- examples/notebooks/stats_tukey_test.ipynb | 187 +++++ statsmodels/sandbox/stats/multicomp.py | 184 +++-- .../sandbox/stats/tests/results/__init__.py | 0 .../results/result_hsubsets_dataframe.py | 694 ++++++++++++++++++ .../sandbox/stats/tests/test_multicomp.py | 32 + 5 files changed, 1054 insertions(+), 43 deletions(-) create mode 100644 examples/notebooks/stats_tukey_test.ipynb create mode 100644 statsmodels/sandbox/stats/tests/results/__init__.py create mode 100644 statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py diff --git a/examples/notebooks/stats_tukey_test.ipynb b/examples/notebooks/stats_tukey_test.ipynb new file mode 100644 index 00000000000..7dd31ec7eb9 --- /dev/null +++ b/examples/notebooks/stats_tukey_test.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d5bb79cd", + "metadata": {}, + "source": [ + "# Tukey's multiple comparisons tests\n", + "\n", + "The Tukey Honest Significant Difference (HSD) test is a widely used post hoc analysis method for identifying which specific group means differ significantly after obtaining a statistically significant result from an ANOVA. While ANOVA can tell us that at least one group is different, it doesn't indicate which ones. The pairwise_tukeyhsd function in statsmodels fills this gap by performing all possible pairwise comparisons between group means while controlling the family-wise error rate. This makes it especially valuable when analysing categorical factors with three or more levels, allowing for detailed interpretation of group differences while maintaining statistical rigour." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a1bf6a0", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import display, Markdown\n", + "from statsmodels.stats.multicomp import pairwise_tukeyhsd\n", + "from statsmodels.formula.api import ols\n", + "from statsmodels.stats.anova import anova_lm\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "80ee232d", + "metadata": {}, + "source": [ + "## Generate random data\n", + "\n", + "First, we will generate synthetic data by sampling from normal distributions. Specifically, we will create 8 groups, each containing 30 samples drawn from a normal distribution with a fixed variance of 1. The group means will vary to ensure that some groups differ significantly from others, allowing us to demonstrate the utility of post hoc analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5aa208b7", + "metadata": {}, + "outputs": [], + "source": [ + "data = []\n", + "groups = []\n", + "locs = [0, 0.1, 1, 1.2, 1.5, 1.7, 2.0, 3.0]\n", + "np.random.seed(0)\n", + "for i in range(8):\n", + " group_data = np.random.normal(loc=locs[i], scale=1.0, size=30)\n", + " data.extend(group_data)\n", + " groups.extend([f'Group {i+1}'] * 30)" + ] + }, + { + "cell_type": "markdown", + "id": "6bc47f10", + "metadata": {}, + "source": [ + "## ANOVA I test\n", + "\n", + "Before applying the Tukey HSD test, we will first perform a one-way ANOVA to determine whether there are any statistically significant differences among the group means. ANOVA serves as an initial global test, assessing whether at least one group differs from the others. If the ANOVA result is significant, we can proceed with the Tukey HSD test to identify which specific pairs of groups are significantly different." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40c9bb58", + "metadata": {}, + "outputs": [], + "source": [ + "anova_model = ols('value ~ C(group)', data={'value': data, 'group': groups}).fit()\n", + "anova_results = anova_lm(anova_model, typ=2)\n", + "display(anova_results)" + ] + }, + { + "cell_type": "markdown", + "id": "3aeed8a9", + "metadata": {}, + "source": [ + "## Pairwise Tukey HSD test\n", + "\n", + "Since our factor has more than two levels, a significant ANOVA result only tells us that at least one group differs from the others, but not which ones. To investigate these differences in more detail, we will use the Tukey HSD test, which performs all pairwise comparisons between group means while controlling for the increased risk of Type I error due to multiple testing. This allows us to determine exactly which group pairs show statistically significant differences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35609eb9", + "metadata": {}, + "outputs": [], + "source": [ + "alpha = 0.05\n", + "\n", + "tukey = pairwise_tukeyhsd(data, groups, alpha=alpha)\n", + "print(tukey)" + ] + }, + { + "cell_type": "markdown", + "id": "c65b21c4", + "metadata": {}, + "source": [ + "### Tukey summary frame\n", + "\n", + "Using `tukey.summary_frame()` we can display a dataframe with the result of all the pairwise comparisons." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c29239c", + "metadata": {}, + "outputs": [], + "source": [ + "display(tukey.summary_frame())" + ] + }, + { + "cell_type": "markdown", + "id": "0cb787c8", + "metadata": {}, + "source": [ + "### Tukey Homogeneous Subsets\n", + "\n", + "Also, using `tukey.create_homogeneous_subsets_dataframe()`, we can display a summary table of the test results.\n", + "\n", + "This function constructs a DataFrame that groups factor levels into **homogeneous subsets** based on the results of the Tukey HSD test. Groups are considered part of the same subset if their pairwise differences are **not statistically significant** (i.e., *p* > alpha). Each group appears once in the table, with its mean shown under every subset to which it belongs. At the bottom of the table, a **\"min p-value\"** row shows the smallest p-value among all pairwise comparisons within each subset. This provides a clear visual summary of which groups are statistically similar, helping to interpret the post hoc results at a glance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7afb083", + "metadata": {}, + "outputs": [], + "source": [ + "hs_df = tukey.create_homogeneous_subsets_dataframe()\n", + "display(hs_df.fillna('')) # Fill NaN with empty strings for better display" + ] + }, + { + "cell_type": "markdown", + "id": "67a30aee", + "metadata": {}, + "source": [ + "### Tukey simultaneous plot\n", + "\n", + "To visualise group mean differences in a more interpretable way, we can use the `plot_simultaneous()` method to create a **universal confidence interval plot**. Instead of displaying all pairwise confidence intervals (which can be overwhelming with many groups), this method shows a **single confidence interval for each group mean**.\n", + "\n", + "This approach, originally proposed by Hochberg and Tamhane (1987), uses Tukey's Q critical value to calculate interval widths, allowing us to compare any two group means visually. If the confidence intervals of two groups **do not overlap**, the difference between them is statistically significant." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4075aa0b", + "metadata": {}, + "outputs": [], + "source": [ + "tukey.plot_simultaneous()\n", + "pass" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "statsmodels-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/statsmodels/sandbox/stats/multicomp.py b/statsmodels/sandbox/stats/multicomp.py index f35c2688447..ec8149e1bbb 100644 --- a/statsmodels/sandbox/stats/multicomp.py +++ b/statsmodels/sandbox/stats/multicomp.py @@ -76,6 +76,7 @@ from statsmodels.graphics import utils from statsmodels.iolib.table import SimpleTable from statsmodels.tools.sm_exceptions import ValueWarning +from itertools import combinations try: # Studentized Range in SciPy 1.7+ @@ -679,7 +680,6 @@ def __init__( nobs_group = self._multicomp.groupstats.groupnobs self.df_total_hsd = np.sum(nobs_group - 1) - def __str__(self): return str(self._results_table) @@ -704,15 +704,17 @@ def summary_frame(self): statsmodels. Do not use numeric indices for the DataFrame in order to be robust to the addition of columns. """ - frame = pd.DataFrame({ - "group_t": self.group_t, - "group_c": self.group_c, - "meandiff": self.meandiffs, - "p-adj": self.pvalues, - "lower": self.confint[:, 0], - "upper": self.confint[:, 1], - "reject": self.reject, - }) + frame = pd.DataFrame( + { + "group_t": self.group_t, + "group_c": self.group_c, + "meandiff": self.meandiffs, + "p-adj": self.pvalues, + "lower": self.confint[:, 0], + "upper": self.confint[:, 1], + "reject": self.reject, + } + ) return frame def _get_q_crit(self, hsd=True, alpha=None): @@ -914,6 +916,103 @@ def plot_simultaneous( ax1.set_ylabel(ylabel if ylabel is not None else "") return fig + def create_homogeneous_subsets_dataframe(self): + """ + Constructs a DataFrame displaying homogeneous subsets of group means based on + Tukey's HSD (Honestly Significant Difference) test results. + + Groups are considered part of the same subset if their mean differences are + not statistically significant (p > alpha). The resulting table contains: + - Each group listed once, with its mean value placed under all subsets it belongs to. + - A final row indicating the minimum p-value among all pairwise comparisons + within each subset. + + Returns + ------- + pd.DataFrame: A DataFrame where each column S1, S2, ..., Sn represents a + homogeneous subset of groups. The 'Group' index lists the group names, and + a final row 'min p-value' contains the minimum p-value within each subset. + """ + endog = self.data + groups = self.groups + + df = pd.DataFrame({"endog": endog, "groups": groups}) + means = df.groupby("groups")["endog"].mean().sort_values() + + # Tukey HSD test + tukey_df = pd.DataFrame( + data=self._results_table.data[1:], columns=self._results_table.data[0] + ) + + # Unique sorted groups and index mapping + unique_groups = means.index.tolist() + idx_map = {g: i for i, g in enumerate(unique_groups)} + n = len(unique_groups) + adj = np.eye(n, dtype=bool) + + # Populate adjacency and p-value dict + pval_dict = {} + for _, row in tukey_df.iterrows(): + g1, g2 = row["group1"], row["group2"] + i, j = idx_map[g1], idx_map[g2] + pval = row["p-adj"] + if not row["reject"]: + adj[i, j] = adj[j, i] = True + pval_dict[frozenset((g1, g2))] = pval + + # Find homogeneous subsets + def find_subsets(adj, labels): + subsets = [] + for size in range(n, 0, -1): + for comb in combinations(range(n), size): + if all(adj[i, j] for i, j in combinations(comb, 2)): + subset = set(labels[i] for i in comb) + if not any(subset <= s for s in subsets): + subsets.append(subset) + return subsets + + subsets = find_subsets(adj, unique_groups) + + # Get min p-value for each subset + subset_results = [] + for subset in subsets: + pairs = combinations(sorted(subset), 2) + min_p = min( + (pval_dict.get(frozenset(pair), 1.0) for pair in pairs), default=1.0 + ) + subset_results.append((sorted(subset), min_p)) + + # Build table + columns = ["Group"] + [f"S{i+1}" for i in range(len(subsets))] + data = {col: [] for col in columns} + for g in unique_groups: + data["Group"].append(g) + for i, subset in enumerate(subsets): + data[f"S{i+1}"].append(means[g] if g in subset else np.nan) + + # Add min p-value row + data["Group"].append("min p-value") + for i, (_, min_p) in enumerate(subset_results): + data[f"S{i+1}"].append(min_p) + + hs_df = pd.DataFrame(data) + + # Reorder subset columns so that groups are consecutively grouped + subset_columns = [] + for _, row in hs_df.iterrows(): + for col in hs_df.columns[1:]: + if pd.notna(row[col]) and col not in subset_columns: + subset_columns.append(col) + hs_df = hs_df[["Group"] + subset_columns] + + # Ensure column names match + hs_df.columns = ["Group"] + [f"S{i+1}" for i in range(len(subset_columns))] + + # Set index to Group + hs_df.set_index("Group", inplace=True) + + return hs_df + class MultiComparison: """Tests for multiple comparisons @@ -1119,33 +1218,33 @@ def allpairtest(self, testfunc, alpha=0.05, method="bonf", pvalidx=1): resarr, ) - def tukeyhsd(self, alpha=0.05, use_var='equal'): + def tukeyhsd(self, alpha=0.05, use_var="equal"): """ - Tukey's range test to compare means of all pairs of groups - - Parameters - ---------- - alpha : float, optional - Value of FWER at which to calculate HSD. - use_var : {"unequal", "equal"} - If ``use_var`` is "equal", then the Tukey-hsd pvalues are returned. - Tukey-hsd assumes that (within) variances are the same across groups. - If ``use_var`` is "unequal", then the Games-Howell pvalues are - returned. This uses Welch's t-test for unequal variances with - Satterthwait's corrected degrees of freedom for each pairwise - comparison. - - Returns - ------- - results : TukeyHSDResults instance - A results class containing relevant data and some post-hoc - calculations - - Notes - ----- - - .. versionadded:: 0.15 - ` The `use_var` keyword and option for Games-Howell test. + Tukey's range test to compare means of all pairs of groups + + Parameters + ---------- + alpha : float, optional + Value of FWER at which to calculate HSD. + use_var : {"unequal", "equal"} + If ``use_var`` is "equal", then the Tukey-hsd pvalues are returned. + Tukey-hsd assumes that (within) variances are the same across groups. + If ``use_var`` is "unequal", then the Games-Howell pvalues are + returned. This uses Welch's t-test for unequal variances with + Satterthwait's corrected degrees of freedom for each pairwise + comparison. + + Returns + ------- + results : TukeyHSDResults instance + A results class containing relevant data and some post-hoc + calculations + + Notes + ----- + + .. versionadded:: 0.15 + ` The `use_var` keyword and option for Games-Howell test. """ self.groupstats = GroupsStats( np.column_stack([self.data, self.groupintlab]), useranks=False @@ -1153,9 +1252,9 @@ def tukeyhsd(self, alpha=0.05, use_var='equal'): gmeans = self.groupstats.groupmean gnobs = self.groupstats.groupnobs - if use_var == 'unequal': + if use_var == "unequal": var_ = self.groupstats.groupvarwithin() - elif use_var == 'equal': + elif use_var == "equal": var_ = np.var(self.groupstats.groupdemean(), ddof=len(gmeans)) else: raise ValueError('use_var should be "unequal" or "equal"') @@ -1204,7 +1303,7 @@ def tukeyhsd(self, alpha=0.05, use_var='equal'): alpha=alpha, group_t=self.groupsunique[res[0][1]], group_c=self.groupsunique[res[0][0]], - ) + ) def rankdata(x): @@ -1473,7 +1572,7 @@ def tukeyhsd(mean_all, nobs_all, var_all, df=None, alpha=0.05, q_crit=None): var_pairs = var_all * varcorrection_pairs_unbalanced(nobs_all, srange=True) elif np.size(var_all) > 1: var_pairs, df_pairs_ = varcorrection_pairs_unequal(var_all, nobs_all, df) - var_pairs /= 2. + var_pairs /= 2.0 # check division by two for studentized range else: @@ -1613,7 +1712,7 @@ def distance_st_range(mean_all, nobs_all, var_all, df=None, triu=False): var_pairs = var_all * varcorrection_pairs_unbalanced(nobs_all, srange=True) elif np.size(var_all) > 1: var_pairs, df_sum = varcorrection_pairs_unequal(var_all, nobs_all, df) - var_pairs /= 2. + var_pairs /= 2.0 # check division by two for studentized range else: raise ValueError("not supposed to be here") @@ -1690,8 +1789,7 @@ def contrast_diff_mean(nm): def tukey_pvalues(std_range, nm, df): - """compute tukey p-values by numerical integration of multivariate-t distribution - """ + """compute tukey p-values by numerical integration of multivariate-t distribution""" # corrected but very slow with warnings about integration # need to increase maxiter or similar # nm = len(std_range) diff --git a/statsmodels/sandbox/stats/tests/results/__init__.py b/statsmodels/sandbox/stats/tests/results/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py b/statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py new file mode 100644 index 00000000000..13406fe8e67 --- /dev/null +++ b/statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py @@ -0,0 +1,694 @@ +import numpy as np +import pandas as pd + + +data = [ + 1.764052345967664, + 0.4001572083672233, + 0.9787379841057392, + 2.240893199201458, + 1.8675579901499675, + -0.977277879876411, + 0.9500884175255894, + -0.1513572082976979, + -0.10321885179355784, + 0.41059850193837233, + 0.144043571160878, + 1.454273506962975, + 0.7610377251469934, + 0.12167501649282841, + 0.44386323274542566, + 0.33367432737426683, + 1.4940790731576061, + -0.20515826376580087, + 0.31306770165090136, + -0.8540957393017248, + -2.5529898158340787, + 0.6536185954403606, + 0.8644361988595057, + -0.7421650204064419, + 2.2697546239876076, + -1.4543656745987648, + 0.04575851730144607, + -0.1871838500258336, + 1.5327792143584575, + 1.469358769900285, + 0.2549474256969163, + 0.4781625196021736, + -0.7877857476301128, + -1.8807964682239269, + -0.2479121493261526, + 0.2563489691039801, + 1.3302906807277208, + 1.3023798487844114, + -0.2873268174079523, + -0.20230275057533556, + -0.9485529650670926, + -1.320017937178975, + -1.6062701906250125, + 2.0507753952317898, + -0.4096521817516535, + -0.33807430161118635, + -1.152795360049926, + 0.87749035583191, + -1.5138978475579514, + -0.1127402802139687, + -0.7954665611936756, + 0.486902497859262, + -0.41080513756887305, + -1.080632184122412, + 0.07181777166134513, + 0.5283318705304176, + 0.1665172223831679, + 0.4024718977397814, + -0.5343220936809636, + -0.26274116598713815, + 0.32753955222404896, + 0.6404468384594587, + 0.18685371795554595, + -0.7262826023316769, + 1.1774261422537529, + 0.5982190637917382, + -0.6301983469660446, + 1.4627822555257741, + 0.09270163561675782, + 1.0519453957961389, + 1.7290905621775368, + 1.1289829107574108, + 2.1394006845433005, + -0.23482582035365263, + 1.4023416411775491, + 0.3151899090596868, + 0.12920285081811822, + 0.42115033523558454, + 0.6884474678726273, + 1.0561653422297455, + -0.16514984078335648, + 1.900826486954187, + 1.4656624397304598, + -0.5362436862772237, + 2.4882521937955997, + 2.8958891760305834, + 2.1787795711596507, + 0.8200751641876491, + -0.07075262151054251, + 2.054451726931137, + 0.7968230530268203, + 2.4224450703824276, + 1.4082749780768602, + 2.1766390364837127, + 1.556366397174402, + 1.9065731681919482, + 1.2105000207208205, + 2.985870493905835, + 1.32691209270362, + 1.6019893634447016, + 3.0831506970562543, + -0.1477590611424464, + -0.07048499848573364, + 2.1693967081580112, + 0.026876594885840044, + 3.143621185649293, + 0.7863810192402526, + 0.4525451885592422, + 3.1229420264803847, + 2.6805147914344243, + 3.06755896042657, + 2.106044658275385, + 0.3387743149452974, + 3.110064953099034, + 0.9319966290486196, + 2.0024563957963952, + 2.147251967773748, + 1.0449899069091657, + 1.8140793703460802, + 2.1222066715665266, + 1.8764255311556295, + 0.4005992094158055, + 1.798238174206056, + 2.8263858966870306, + 0.8054321402686345, + 1.3503654596723291, + 1.0648464482783626, + 3.349263728479342, + 2.1722947570124354, + 1.9074618362411104, + 0.7300839255546836, + 2.0392491912918174, + 0.8256673393426239, + 1.5318305582743512, + 0.864153921621119, + 2.1764332949464995, + 2.076590816614941, + 1.291701244422005, + 1.8960067126616453, + 0.4069384912694942, + 0.00874240729439446, + 1.939391701264537, + 1.666673495372529, + 2.135031436892106, + 3.883144774863942, + 2.4444794869904136, + 0.5871777745558414, + 2.617016288095853, + 0.18409258948847884, + 1.038415395185291, + 1.6317583946753687, + 3.4133427216493666, + 0.95524517795156, + 0.8735614613409856, + 1.6015474755745676, + 1.0365217136378926, + 2.826635922106507, + 0.6200684916365766, + 0.5525313475888975, + 1.262179955255566, + 1.2019675493076951, + 3.6295320538169857, + 2.649420806925761, + 1.7875512413851908, + 0.47456448116983196, + 2.544362976401547, + 0.6997846526104352, + 0.15522890322238836, + 2.8880297923523015, + 2.0169426119248497, + 2.620858823780819, + 2.018727652943021, + 2.556830611902691, + 1.0489744066998532, + 0.6657571582155353, + 2.381594518281627, + 0.8965903358261589, + 1.0104502222497995, + 1.2444674964826568, + 1.7174791590250567, + 1.646006088746516, + 0.6250487065819812, + 1.3563815971671094, + -0.22340315222442664, + 2.6252314510271875, + 0.3979423443932524, + 0.8956166605715494, + 2.0521650792609742, + 1.2604370036086867, + 3.543014595406736, + 0.7071430902765514, + 2.267050869349183, + 1.9607171817725044, + 0.8319065022588026, + 2.523276660531754, + 1.828453668777752, + 2.7717905512136674, + 2.8235041539637313, + 4.16323594928069, + 3.336527949436392, + 1.6308181620575564, + 1.7606208224240736, + 3.099659595887113, + 2.6552637307225977, + 2.640131526097592, + 0.3830439556891656, + 1.9756738756010643, + 1.2619690907943113, + 2.2799245990432384, + 1.9018496103570421, + 3.910178908092592, + 3.317218215191302, + 3.786327962108976, + 2.533580903264057, + 2.0555537440817497, + 2.5899503067974514, + 2.9829795861385593, + 3.379151735555082, + 5.259308950690852, + 2.9577428483393575, + 2.044054999507223, + 2.6540182243006134, + 2.5364040253539057, + 3.4814814737734623, + 1.4592029855553752, + 3.0632619942003316, + 3.1565065379653756, + 3.232181036200276, + 2.4026839310346375, + 2.762078270263993, + 1.5759390910174684, + 2.506680116637806, + 2.457138523983282, + 3.4160500462614256, + 1.8438175681780873, + 3.781198101709993, + 4.494484544491369, + 0.9300149749864675, + 3.426258730778101, + 3.6769080350302454, +] +groups = [ + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 1", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 2", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 3", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 4", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 5", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 6", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 7", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", + "Group 8", +] + + +expected_df_alpha001 = pd.DataFrame( + { + "S1": [ + -0.1895218561539811, + 0.44285644726317464, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 0.1985, + ], + "S2": [ + np.nan, + 0.44285644726317464, + 0.8662790048687181, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 0.7019, + ], + "S3": [ + np.nan, + np.nan, + 0.8662790048687181, + 1.5964711342473101, + 1.632750270531383, + 1.7108333884711162, + np.nan, + np.nan, + 0.0212, + ], + "S4": [ + np.nan, + np.nan, + np.nan, + 1.5964711342473101, + 1.632750270531383, + 1.7108333884711162, + 1.8993667306691449, + np.nan, + 0.9313, + ], + "S5": [ + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 2.922411879049647, + 1.0, + ], + } +) +expected_df_alpha001.index = [ + "Group 2", + "Group 1", + "Group 3", + "Group 5", + "Group 6", + "Group 4", + "Group 7", + "Group 8", + "min p-value", +] +expected_df_alpha001.index.name = "Group" + +expected_df_alpha005 = pd.DataFrame( + { + "S1": [ + -0.1895218561539811, + 0.44285644726317464, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 0.1985, + ], + "S2": [ + np.nan, + 0.44285644726317464, + 0.8662790048687181, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 0.7019, + ], + "S3": [ + np.nan, + np.nan, + 0.8662790048687181, + 1.5964711342473101, + 1.632750270531383, + np.nan, + np.nan, + np.nan, + 0.0533, + ], + "S4": [ + np.nan, + np.nan, + np.nan, + 1.5964711342473101, + 1.632750270531383, + 1.7108333884711162, + 1.8993667306691449, + np.nan, + 0.9313, + ], + "S5": [ + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 2.922411879049647, + 1.0, + ], + } +) +expected_df_alpha005.index = [ + "Group 2", + "Group 1", + "Group 3", + "Group 5", + "Group 6", + "Group 4", + "Group 7", + "Group 8", + "min p-value", +] +expected_df_alpha005.index.name = "Group" + +expected_df_alpha01 = pd.DataFrame( + { + "S1": [ + -0.1895218561539811, + 0.44285644726317464, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 0.1985, + ], + "S2": [ + np.nan, + 0.44285644726317464, + 0.8662790048687181, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 0.7019, + ], + "S3": [ + np.nan, + np.nan, + np.nan, + 1.5964711342473101, + 1.632750270531383, + 1.7108333884711162, + 1.8993667306691449, + np.nan, + 0.9313, + ], + "S4": [ + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + np.nan, + 2.922411879049647, + 1.0, + ], + } +) +expected_df_alpha01.index = [ + "Group 2", + "Group 1", + "Group 3", + "Group 5", + "Group 6", + "Group 4", + "Group 7", + "Group 8", + "min p-value", +] +expected_df_alpha01.index.name = "Group" diff --git a/statsmodels/sandbox/stats/tests/test_multicomp.py b/statsmodels/sandbox/stats/tests/test_multicomp.py index 71f59d92e05..9dfa6a35ff1 100644 --- a/statsmodels/sandbox/stats/tests/test_multicomp.py +++ b/statsmodels/sandbox/stats/tests/test_multicomp.py @@ -7,8 +7,10 @@ import numpy as np from numpy.testing import assert_almost_equal import pytest +import pandas as pd from statsmodels.sandbox.stats.multicomp import tukey_pvalues +from .results import result_hsubsets_dataframe @pytest.mark.skipif(not SP_LT_116, reason="mvndst removed in SciPy 1.16") @@ -18,3 +20,33 @@ def test_tukey_pvalues(): res = tukey_pvalues(3.649, 3, 16) assert_almost_equal(0.05, res[0], 3) assert_almost_equal(0.05 * np.ones(3), res[1], 3) + + +@pytest.mark.parametrize( + "alpha, expected_df", + [ + (0.01, result_hsubsets_dataframe.expected_df_alpha001), + (0.05, result_hsubsets_dataframe.expected_df_alpha005), + (0.1, result_hsubsets_dataframe.expected_df_alpha01), + ], +) +def test_create_homogeneous_subsets_dataframe( + alpha, + expected_df, +): + from statsmodels.stats.multicomp import pairwise_tukeyhsd + from statsmodels.sandbox.stats.multicomp import TukeyHSDResults + + data = result_hsubsets_dataframe.data + groups = result_hsubsets_dataframe.groups + + tukey = pairwise_tukeyhsd( + endog=data, + groups=groups, + alpha=alpha, + ) + assert isinstance(tukey, TukeyHSDResults) + hs_df = tukey.create_homogeneous_subsets_dataframe() + assert isinstance(hs_df, pd.DataFrame) + assert hs_df.equals(expected_df) + assert hs_df.index.name == "Group" From f489f4f89e0c3b57aa7c926d77f245a50e6c468f Mon Sep 17 00:00:00 2001 From: Victor Vargas Date: Thu, 29 May 2025 10:29:03 +0200 Subject: [PATCH 2/3] STY: Fixed formatting issues in multicomp.py --- statsmodels/sandbox/stats/multicomp.py | 94 +++++++++++++------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/statsmodels/sandbox/stats/multicomp.py b/statsmodels/sandbox/stats/multicomp.py index ec8149e1bbb..b6cd498a023 100644 --- a/statsmodels/sandbox/stats/multicomp.py +++ b/statsmodels/sandbox/stats/multicomp.py @@ -65,6 +65,7 @@ from statsmodels.compat.python import lrange, lzip from collections import namedtuple +from itertools import combinations import copy import math @@ -76,7 +77,6 @@ from statsmodels.graphics import utils from statsmodels.iolib.table import SimpleTable from statsmodels.tools.sm_exceptions import ValueWarning -from itertools import combinations try: # Studentized Range in SciPy 1.7+ @@ -680,6 +680,7 @@ def __init__( nobs_group = self._multicomp.groupstats.groupnobs self.df_total_hsd = np.sum(nobs_group - 1) + def __str__(self): return str(self._results_table) @@ -704,17 +705,15 @@ def summary_frame(self): statsmodels. Do not use numeric indices for the DataFrame in order to be robust to the addition of columns. """ - frame = pd.DataFrame( - { - "group_t": self.group_t, - "group_c": self.group_c, - "meandiff": self.meandiffs, - "p-adj": self.pvalues, - "lower": self.confint[:, 0], - "upper": self.confint[:, 1], - "reject": self.reject, - } - ) + frame = pd.DataFrame({ + "group_t": self.group_t, + "group_c": self.group_c, + "meandiff": self.meandiffs, + "p-adj": self.pvalues, + "lower": self.confint[:, 0], + "upper": self.confint[:, 1], + "reject": self.reject, + }) return frame def _get_q_crit(self, hsd=True, alpha=None): @@ -929,9 +928,9 @@ def create_homogeneous_subsets_dataframe(self): Returns ------- - pd.DataFrame: A DataFrame where each column S1, S2, ..., Sn represents a - homogeneous subset of groups. The 'Group' index lists the group names, and - a final row 'min p-value' contains the minimum p-value within each subset. + pd.DataFrame: A DataFrame where each column S1, S2, ..., Sn represents a + homogeneous subset of groups. The 'Group' index lists the group names, and + a final row 'min p-value' contains the minimum p-value within each subset. """ endog = self.data groups = self.groups @@ -1218,33 +1217,33 @@ def allpairtest(self, testfunc, alpha=0.05, method="bonf", pvalidx=1): resarr, ) - def tukeyhsd(self, alpha=0.05, use_var="equal"): + def tukeyhsd(self, alpha=0.05, use_var='equal'): """ - Tukey's range test to compare means of all pairs of groups - - Parameters - ---------- - alpha : float, optional - Value of FWER at which to calculate HSD. - use_var : {"unequal", "equal"} - If ``use_var`` is "equal", then the Tukey-hsd pvalues are returned. - Tukey-hsd assumes that (within) variances are the same across groups. - If ``use_var`` is "unequal", then the Games-Howell pvalues are - returned. This uses Welch's t-test for unequal variances with - Satterthwait's corrected degrees of freedom for each pairwise - comparison. - - Returns - ------- - results : TukeyHSDResults instance - A results class containing relevant data and some post-hoc - calculations - - Notes - ----- - - .. versionadded:: 0.15 - ` The `use_var` keyword and option for Games-Howell test. + Tukey's range test to compare means of all pairs of groups + + Parameters + ---------- + alpha : float, optional + Value of FWER at which to calculate HSD. + use_var : {"unequal", "equal"} + If ``use_var`` is "equal", then the Tukey-hsd pvalues are returned. + Tukey-hsd assumes that (within) variances are the same across groups. + If ``use_var`` is "unequal", then the Games-Howell pvalues are + returned. This uses Welch's t-test for unequal variances with + Satterthwait's corrected degrees of freedom for each pairwise + comparison. + + Returns + ------- + results : TukeyHSDResults instance + A results class containing relevant data and some post-hoc + calculations + + Notes + ----- + + .. versionadded:: 0.15 + ` The `use_var` keyword and option for Games-Howell test. """ self.groupstats = GroupsStats( np.column_stack([self.data, self.groupintlab]), useranks=False @@ -1252,9 +1251,9 @@ def tukeyhsd(self, alpha=0.05, use_var="equal"): gmeans = self.groupstats.groupmean gnobs = self.groupstats.groupnobs - if use_var == "unequal": + if use_var == 'unequal': var_ = self.groupstats.groupvarwithin() - elif use_var == "equal": + elif use_var == 'equal': var_ = np.var(self.groupstats.groupdemean(), ddof=len(gmeans)) else: raise ValueError('use_var should be "unequal" or "equal"') @@ -1303,7 +1302,7 @@ def tukeyhsd(self, alpha=0.05, use_var="equal"): alpha=alpha, group_t=self.groupsunique[res[0][1]], group_c=self.groupsunique[res[0][0]], - ) + ) def rankdata(x): @@ -1572,7 +1571,7 @@ def tukeyhsd(mean_all, nobs_all, var_all, df=None, alpha=0.05, q_crit=None): var_pairs = var_all * varcorrection_pairs_unbalanced(nobs_all, srange=True) elif np.size(var_all) > 1: var_pairs, df_pairs_ = varcorrection_pairs_unequal(var_all, nobs_all, df) - var_pairs /= 2.0 + var_pairs /= 2. # check division by two for studentized range else: @@ -1712,7 +1711,7 @@ def distance_st_range(mean_all, nobs_all, var_all, df=None, triu=False): var_pairs = var_all * varcorrection_pairs_unbalanced(nobs_all, srange=True) elif np.size(var_all) > 1: var_pairs, df_sum = varcorrection_pairs_unequal(var_all, nobs_all, df) - var_pairs /= 2.0 + var_pairs /= 2. # check division by two for studentized range else: raise ValueError("not supposed to be here") @@ -1789,7 +1788,8 @@ def contrast_diff_mean(nm): def tukey_pvalues(std_range, nm, df): - """compute tukey p-values by numerical integration of multivariate-t distribution""" + """compute tukey p-values by numerical integration of multivariate-t distribution + """ # corrected but very slow with warnings about integration # need to increase maxiter or similar # nm = len(std_range) From 387393ef08c9cb17e09943d4a94c17eb072ede41 Mon Sep 17 00:00:00 2001 From: Victor Vargas Date: Fri, 30 May 2025 09:06:59 +0200 Subject: [PATCH 3/3] TST: Added tukey homogeneous subsets unbalanced data tests and example --- examples/notebooks/stats_tukey_test.ipynb | 127 +++- .../results/result_hsubsets_dataframe.py | 547 ++---------------- .../tests/results/sample_data_balanced.csv | 241 ++++++++ .../tests/results/sample_data_unbalanced.csv | 187 ++++++ .../sandbox/stats/tests/test_multicomp.py | 47 +- 5 files changed, 658 insertions(+), 491 deletions(-) create mode 100644 statsmodels/sandbox/stats/tests/results/sample_data_balanced.csv create mode 100644 statsmodels/sandbox/stats/tests/results/sample_data_unbalanced.csv diff --git a/examples/notebooks/stats_tukey_test.ipynb b/examples/notebooks/stats_tukey_test.ipynb index 7dd31ec7eb9..c5695fc5a1b 100644 --- a/examples/notebooks/stats_tukey_test.ipynb +++ b/examples/notebooks/stats_tukey_test.ipynb @@ -29,7 +29,7 @@ "id": "80ee232d", "metadata": {}, "source": [ - "## Generate random data\n", + "## Generate random balanced data\n", "\n", "First, we will generate synthetic data by sampling from normal distributions. Specifically, we will create 8 groups, each containing 30 samples drawn from a normal distribution with a fixed variance of 1. The group means will vary to ensure that some groups differ significantly from others, allowing us to demonstrate the utility of post hoc analysis." ] @@ -161,6 +161,131 @@ "tukey.plot_simultaneous()\n", "pass" ] + }, + { + "cell_type": "markdown", + "id": "8f4ab9ff", + "metadata": {}, + "source": [ + "## Generate random unbalanced data\n", + "\n", + "Now we can repeat the same example with unbalanced data (i.e. different sample size for each group)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58dea005", + "metadata": {}, + "outputs": [], + "source": [ + "data = []\n", + "groups = []\n", + "locs = [0, 0.1, 1, 1.2, 1.5, 1.7, 2.0, 3.0]\n", + "sizes = [10, 8, 12, 50, 20, 11, 60, 15]\n", + "np.random.seed(0)\n", + "for i in range(8):\n", + " group_data = np.random.normal(loc=locs[i], scale=1.0, size=sizes[i])\n", + " data.extend(group_data)\n", + " groups.extend([f'Group {i+1}'] * sizes[i])" + ] + }, + { + "cell_type": "markdown", + "id": "0f9bf711", + "metadata": {}, + "source": [ + "## ANOVA I test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1e6d002", + "metadata": {}, + "outputs": [], + "source": [ + "anova_model = ols('value ~ C(group)', data={'value': data, 'group': groups}).fit()\n", + "anova_results = anova_lm(anova_model, typ=2)\n", + "display(anova_results)" + ] + }, + { + "cell_type": "markdown", + "id": "1791e15d", + "metadata": {}, + "source": [ + "## Pairwise Tukey HSD test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b499e1b7", + "metadata": {}, + "outputs": [], + "source": [ + "alpha = 0.05\n", + "\n", + "tukey = pairwise_tukeyhsd(data, groups, alpha=alpha)\n", + "print(tukey)" + ] + }, + { + "cell_type": "markdown", + "id": "c6232074", + "metadata": {}, + "source": [ + "### Tukey summary frame" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d68b993", + "metadata": {}, + "outputs": [], + "source": [ + "display(tukey.summary_frame())" + ] + }, + { + "cell_type": "markdown", + "id": "93979fe9", + "metadata": {}, + "source": [ + "### Tukey Homogeneous Subsets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb83eee3", + "metadata": {}, + "outputs": [], + "source": [ + "hs_df = tukey.create_homogeneous_subsets_dataframe()\n", + "display(hs_df.fillna('')) # Fill NaN with empty strings for better display" + ] + }, + { + "cell_type": "markdown", + "id": "4bf77d3e", + "metadata": {}, + "source": [ + "### Tukey simultaneous plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2dde4236", + "metadata": {}, + "outputs": [], + "source": [ + "tukey.plot_simultaneous()\n", + "pass" + ] } ], "metadata": { diff --git a/statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py b/statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py index 13406fe8e67..92cfc5d4a9f 100644 --- a/statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py +++ b/statsmodels/sandbox/stats/tests/results/result_hsubsets_dataframe.py @@ -2,492 +2,6 @@ import pandas as pd -data = [ - 1.764052345967664, - 0.4001572083672233, - 0.9787379841057392, - 2.240893199201458, - 1.8675579901499675, - -0.977277879876411, - 0.9500884175255894, - -0.1513572082976979, - -0.10321885179355784, - 0.41059850193837233, - 0.144043571160878, - 1.454273506962975, - 0.7610377251469934, - 0.12167501649282841, - 0.44386323274542566, - 0.33367432737426683, - 1.4940790731576061, - -0.20515826376580087, - 0.31306770165090136, - -0.8540957393017248, - -2.5529898158340787, - 0.6536185954403606, - 0.8644361988595057, - -0.7421650204064419, - 2.2697546239876076, - -1.4543656745987648, - 0.04575851730144607, - -0.1871838500258336, - 1.5327792143584575, - 1.469358769900285, - 0.2549474256969163, - 0.4781625196021736, - -0.7877857476301128, - -1.8807964682239269, - -0.2479121493261526, - 0.2563489691039801, - 1.3302906807277208, - 1.3023798487844114, - -0.2873268174079523, - -0.20230275057533556, - -0.9485529650670926, - -1.320017937178975, - -1.6062701906250125, - 2.0507753952317898, - -0.4096521817516535, - -0.33807430161118635, - -1.152795360049926, - 0.87749035583191, - -1.5138978475579514, - -0.1127402802139687, - -0.7954665611936756, - 0.486902497859262, - -0.41080513756887305, - -1.080632184122412, - 0.07181777166134513, - 0.5283318705304176, - 0.1665172223831679, - 0.4024718977397814, - -0.5343220936809636, - -0.26274116598713815, - 0.32753955222404896, - 0.6404468384594587, - 0.18685371795554595, - -0.7262826023316769, - 1.1774261422537529, - 0.5982190637917382, - -0.6301983469660446, - 1.4627822555257741, - 0.09270163561675782, - 1.0519453957961389, - 1.7290905621775368, - 1.1289829107574108, - 2.1394006845433005, - -0.23482582035365263, - 1.4023416411775491, - 0.3151899090596868, - 0.12920285081811822, - 0.42115033523558454, - 0.6884474678726273, - 1.0561653422297455, - -0.16514984078335648, - 1.900826486954187, - 1.4656624397304598, - -0.5362436862772237, - 2.4882521937955997, - 2.8958891760305834, - 2.1787795711596507, - 0.8200751641876491, - -0.07075262151054251, - 2.054451726931137, - 0.7968230530268203, - 2.4224450703824276, - 1.4082749780768602, - 2.1766390364837127, - 1.556366397174402, - 1.9065731681919482, - 1.2105000207208205, - 2.985870493905835, - 1.32691209270362, - 1.6019893634447016, - 3.0831506970562543, - -0.1477590611424464, - -0.07048499848573364, - 2.1693967081580112, - 0.026876594885840044, - 3.143621185649293, - 0.7863810192402526, - 0.4525451885592422, - 3.1229420264803847, - 2.6805147914344243, - 3.06755896042657, - 2.106044658275385, - 0.3387743149452974, - 3.110064953099034, - 0.9319966290486196, - 2.0024563957963952, - 2.147251967773748, - 1.0449899069091657, - 1.8140793703460802, - 2.1222066715665266, - 1.8764255311556295, - 0.4005992094158055, - 1.798238174206056, - 2.8263858966870306, - 0.8054321402686345, - 1.3503654596723291, - 1.0648464482783626, - 3.349263728479342, - 2.1722947570124354, - 1.9074618362411104, - 0.7300839255546836, - 2.0392491912918174, - 0.8256673393426239, - 1.5318305582743512, - 0.864153921621119, - 2.1764332949464995, - 2.076590816614941, - 1.291701244422005, - 1.8960067126616453, - 0.4069384912694942, - 0.00874240729439446, - 1.939391701264537, - 1.666673495372529, - 2.135031436892106, - 3.883144774863942, - 2.4444794869904136, - 0.5871777745558414, - 2.617016288095853, - 0.18409258948847884, - 1.038415395185291, - 1.6317583946753687, - 3.4133427216493666, - 0.95524517795156, - 0.8735614613409856, - 1.6015474755745676, - 1.0365217136378926, - 2.826635922106507, - 0.6200684916365766, - 0.5525313475888975, - 1.262179955255566, - 1.2019675493076951, - 3.6295320538169857, - 2.649420806925761, - 1.7875512413851908, - 0.47456448116983196, - 2.544362976401547, - 0.6997846526104352, - 0.15522890322238836, - 2.8880297923523015, - 2.0169426119248497, - 2.620858823780819, - 2.018727652943021, - 2.556830611902691, - 1.0489744066998532, - 0.6657571582155353, - 2.381594518281627, - 0.8965903358261589, - 1.0104502222497995, - 1.2444674964826568, - 1.7174791590250567, - 1.646006088746516, - 0.6250487065819812, - 1.3563815971671094, - -0.22340315222442664, - 2.6252314510271875, - 0.3979423443932524, - 0.8956166605715494, - 2.0521650792609742, - 1.2604370036086867, - 3.543014595406736, - 0.7071430902765514, - 2.267050869349183, - 1.9607171817725044, - 0.8319065022588026, - 2.523276660531754, - 1.828453668777752, - 2.7717905512136674, - 2.8235041539637313, - 4.16323594928069, - 3.336527949436392, - 1.6308181620575564, - 1.7606208224240736, - 3.099659595887113, - 2.6552637307225977, - 2.640131526097592, - 0.3830439556891656, - 1.9756738756010643, - 1.2619690907943113, - 2.2799245990432384, - 1.9018496103570421, - 3.910178908092592, - 3.317218215191302, - 3.786327962108976, - 2.533580903264057, - 2.0555537440817497, - 2.5899503067974514, - 2.9829795861385593, - 3.379151735555082, - 5.259308950690852, - 2.9577428483393575, - 2.044054999507223, - 2.6540182243006134, - 2.5364040253539057, - 3.4814814737734623, - 1.4592029855553752, - 3.0632619942003316, - 3.1565065379653756, - 3.232181036200276, - 2.4026839310346375, - 2.762078270263993, - 1.5759390910174684, - 2.506680116637806, - 2.457138523983282, - 3.4160500462614256, - 1.8438175681780873, - 3.781198101709993, - 4.494484544491369, - 0.9300149749864675, - 3.426258730778101, - 3.6769080350302454, -] -groups = [ - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 1", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 2", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 3", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 4", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 5", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 6", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 7", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", - "Group 8", -] - - expected_df_alpha001 = pd.DataFrame( { "S1": [ @@ -692,3 +206,64 @@ "min p-value", ] expected_df_alpha01.index.name = "Group" + +expected_df_unbalanced_alpha005 = pd.DataFrame( + { + "S1": [ + 0.668436023659397, + 0.738023170728835, + 0.885418477024479, + 1.113164460110977, + np.nan, + np.nan, + np.nan, + np.nan, + 0.924598821413583, + ], + "S2": [ + np.nan, + np.nan, + 0.885418477024479, + 1.113164460110977, + 1.921209214216464, + np.nan, + np.nan, + np.nan, + 0.090673607136369, + ], + "S3": [ + np.nan, + np.nan, + np.nan, + 1.113164460110977, + 1.921209214216464, + 2.124227257491577, + 2.164976646569281, + np.nan, + 0.08097476143252, + ], + "S4": [ + np.nan, + np.nan, + np.nan, + np.nan, + 1.921209214216464, + 2.124227257491577, + 2.164976646569281, + 2.511205239821201, + 0.737364397497922, + ], + } +) +expected_df_unbalanced_alpha005.index = [ + "Group 2", + "Group 1", + "Group 4", + "Group 3", + "Group 5", + "Group 7", + "Group 6", + "Group 8", + "min p-value", +] +expected_df_unbalanced_alpha005.index.name = "Group" \ No newline at end of file diff --git a/statsmodels/sandbox/stats/tests/results/sample_data_balanced.csv b/statsmodels/sandbox/stats/tests/results/sample_data_balanced.csv new file mode 100644 index 00000000000..3e2b4689bed --- /dev/null +++ b/statsmodels/sandbox/stats/tests/results/sample_data_balanced.csv @@ -0,0 +1,241 @@ +groups,data +Group 1,1.764052345967664 +Group 1,0.4001572083672233 +Group 1,0.9787379841057392 +Group 1,2.240893199201458 +Group 1,1.8675579901499675 +Group 1,-0.977277879876411 +Group 1,0.9500884175255894 +Group 1,-0.1513572082976979 +Group 1,-0.10321885179355784 +Group 1,0.41059850193837233 +Group 1,0.144043571160878 +Group 1,1.454273506962975 +Group 1,0.7610377251469934 +Group 1,0.12167501649282841 +Group 1,0.44386323274542566 +Group 1,0.33367432737426683 +Group 1,1.4940790731576061 +Group 1,-0.20515826376580087 +Group 1,0.31306770165090136 +Group 1,-0.8540957393017248 +Group 1,-2.5529898158340787 +Group 1,0.6536185954403606 +Group 1,0.8644361988595057 +Group 1,-0.7421650204064419 +Group 1,2.2697546239876076 +Group 1,-1.4543656745987648 +Group 1,0.04575851730144607 +Group 1,-0.1871838500258336 +Group 1,1.5327792143584575 +Group 1,1.469358769900285 +Group 2,0.2549474256969163 +Group 2,0.4781625196021736 +Group 2,-0.7877857476301128 +Group 2,-1.8807964682239269 +Group 2,-0.2479121493261526 +Group 2,0.2563489691039801 +Group 2,1.3302906807277208 +Group 2,1.3023798487844114 +Group 2,-0.2873268174079523 +Group 2,-0.20230275057533556 +Group 2,-0.9485529650670926 +Group 2,-1.320017937178975 +Group 2,-1.6062701906250125 +Group 2,2.0507753952317898 +Group 2,-0.4096521817516535 +Group 2,-0.33807430161118635 +Group 2,-1.152795360049926 +Group 2,0.87749035583191 +Group 2,-1.5138978475579514 +Group 2,-0.1127402802139687 +Group 2,-0.7954665611936756 +Group 2,0.486902497859262 +Group 2,-0.41080513756887305 +Group 2,-1.080632184122412 +Group 2,0.07181777166134513 +Group 2,0.5283318705304176 +Group 2,0.1665172223831679 +Group 2,0.4024718977397814 +Group 2,-0.5343220936809636 +Group 2,-0.26274116598713815 +Group 3,0.32753955222404896 +Group 3,0.6404468384594587 +Group 3,0.18685371795554595 +Group 3,-0.7262826023316769 +Group 3,1.1774261422537529 +Group 3,0.5982190637917382 +Group 3,-0.6301983469660446 +Group 3,1.4627822555257741 +Group 3,0.09270163561675782 +Group 3,1.0519453957961389 +Group 3,1.7290905621775368 +Group 3,1.1289829107574108 +Group 3,2.1394006845433005 +Group 3,-0.23482582035365263 +Group 3,1.4023416411775491 +Group 3,0.3151899090596868 +Group 3,0.12920285081811822 +Group 3,0.42115033523558454 +Group 3,0.6884474678726273 +Group 3,1.0561653422297455 +Group 3,-0.16514984078335648 +Group 3,1.900826486954187 +Group 3,1.4656624397304598 +Group 3,-0.5362436862772237 +Group 3,2.4882521937955997 +Group 3,2.8958891760305834 +Group 3,2.1787795711596507 +Group 3,0.8200751641876491 +Group 3,-0.07075262151054251 +Group 3,2.054451726931137 +Group 4,0.7968230530268203 +Group 4,2.4224450703824276 +Group 4,1.4082749780768602 +Group 4,2.1766390364837127 +Group 4,1.556366397174402 +Group 4,1.9065731681919482 +Group 4,1.2105000207208205 +Group 4,2.985870493905835 +Group 4,1.32691209270362 +Group 4,1.6019893634447016 +Group 4,3.0831506970562543 +Group 4,-0.1477590611424464 +Group 4,-0.07048499848573364 +Group 4,2.1693967081580112 +Group 4,0.026876594885840044 +Group 4,3.143621185649293 +Group 4,0.7863810192402526 +Group 4,0.4525451885592422 +Group 4,3.1229420264803847 +Group 4,2.6805147914344243 +Group 4,3.06755896042657 +Group 4,2.106044658275385 +Group 4,0.3387743149452974 +Group 4,3.110064953099034 +Group 4,0.9319966290486196 +Group 4,2.0024563957963952 +Group 4,2.147251967773748 +Group 4,1.0449899069091657 +Group 4,1.8140793703460802 +Group 4,2.1222066715665266 +Group 5,1.8764255311556295 +Group 5,0.4005992094158055 +Group 5,1.798238174206056 +Group 5,2.8263858966870306 +Group 5,0.8054321402686345 +Group 5,1.3503654596723291 +Group 5,1.0648464482783626 +Group 5,3.349263728479342 +Group 5,2.1722947570124354 +Group 5,1.9074618362411104 +Group 5,0.7300839255546836 +Group 5,2.0392491912918174 +Group 5,0.8256673393426239 +Group 5,1.5318305582743512 +Group 5,0.864153921621119 +Group 5,2.1764332949464995 +Group 5,2.076590816614941 +Group 5,1.291701244422005 +Group 5,1.8960067126616453 +Group 5,0.4069384912694942 +Group 5,0.00874240729439446 +Group 5,1.939391701264537 +Group 5,1.666673495372529 +Group 5,2.135031436892106 +Group 5,3.883144774863942 +Group 5,2.4444794869904136 +Group 5,0.5871777745558414 +Group 5,2.617016288095853 +Group 5,0.18409258948847884 +Group 5,1.038415395185291 +Group 6,1.6317583946753687 +Group 6,3.4133427216493666 +Group 6,0.95524517795156 +Group 6,0.8735614613409856 +Group 6,1.6015474755745676 +Group 6,1.0365217136378926 +Group 6,2.826635922106507 +Group 6,0.6200684916365766 +Group 6,0.5525313475888975 +Group 6,1.262179955255566 +Group 6,1.2019675493076951 +Group 6,3.6295320538169857 +Group 6,2.649420806925761 +Group 6,1.7875512413851908 +Group 6,0.47456448116983196 +Group 6,2.544362976401547 +Group 6,0.6997846526104352 +Group 6,0.15522890322238836 +Group 6,2.8880297923523015 +Group 6,2.0169426119248497 +Group 6,2.620858823780819 +Group 6,2.018727652943021 +Group 6,2.556830611902691 +Group 6,1.0489744066998532 +Group 6,0.6657571582155353 +Group 6,2.381594518281627 +Group 6,0.8965903358261589 +Group 6,1.0104502222497995 +Group 6,1.2444674964826568 +Group 6,1.7174791590250567 +Group 7,1.646006088746516 +Group 7,0.6250487065819812 +Group 7,1.3563815971671094 +Group 7,-0.22340315222442664 +Group 7,2.6252314510271875 +Group 7,0.3979423443932524 +Group 7,0.8956166605715494 +Group 7,2.0521650792609742 +Group 7,1.2604370036086867 +Group 7,3.543014595406736 +Group 7,0.7071430902765514 +Group 7,2.267050869349183 +Group 7,1.9607171817725044 +Group 7,0.8319065022588026 +Group 7,2.523276660531754 +Group 7,1.828453668777752 +Group 7,2.7717905512136674 +Group 7,2.8235041539637313 +Group 7,4.16323594928069 +Group 7,3.336527949436392 +Group 7,1.6308181620575564 +Group 7,1.7606208224240736 +Group 7,3.099659595887113 +Group 7,2.6552637307225977 +Group 7,2.640131526097592 +Group 7,0.3830439556891656 +Group 7,1.9756738756010643 +Group 7,1.2619690907943113 +Group 7,2.2799245990432384 +Group 7,1.9018496103570421 +Group 8,3.910178908092592 +Group 8,3.317218215191302 +Group 8,3.786327962108976 +Group 8,2.533580903264057 +Group 8,2.0555537440817497 +Group 8,2.5899503067974514 +Group 8,2.9829795861385593 +Group 8,3.379151735555082 +Group 8,5.259308950690852 +Group 8,2.9577428483393575 +Group 8,2.044054999507223 +Group 8,2.6540182243006134 +Group 8,2.5364040253539057 +Group 8,3.4814814737734623 +Group 8,1.4592029855553752 +Group 8,3.0632619942003316 +Group 8,3.1565065379653756 +Group 8,3.232181036200276 +Group 8,2.4026839310346375 +Group 8,2.762078270263993 +Group 8,1.5759390910174684 +Group 8,2.506680116637806 +Group 8,2.457138523983282 +Group 8,3.4160500462614256 +Group 8,1.8438175681780873 +Group 8,3.781198101709993 +Group 8,4.494484544491369 +Group 8,0.9300149749864675 +Group 8,3.426258730778101 +Group 8,3.6769080350302454 \ No newline at end of file diff --git a/statsmodels/sandbox/stats/tests/results/sample_data_unbalanced.csv b/statsmodels/sandbox/stats/tests/results/sample_data_unbalanced.csv new file mode 100644 index 00000000000..545483be74b --- /dev/null +++ b/statsmodels/sandbox/stats/tests/results/sample_data_unbalanced.csv @@ -0,0 +1,187 @@ +groups,data +Group 1,1.764052345967664 +Group 1,0.4001572083672233 +Group 1,0.9787379841057392 +Group 1,2.240893199201458 +Group 1,1.8675579901499675 +Group 1,-0.977277879876411 +Group 1,0.9500884175255894 +Group 1,-0.1513572082976979 +Group 1,-0.10321885179355784 +Group 1,0.41059850193837233 +Group 2,0.244043571160878 +Group 2,1.5542735069629752 +Group 2,0.8610377251469934 +Group 2,0.22167501649282842 +Group 2,0.5438632327454257 +Group 2,0.4336743273742668 +Group 2,1.5940790731576062 +Group 2,-0.10515826376580087 +Group 3,1.3130677016509014 +Group 3,0.14590426069827522 +Group 3,-1.5529898158340787 +Group 3,1.6536185954403606 +Group 3,1.8644361988595057 +Group 3,0.25783497959355806 +Group 3,3.2697546239876076 +Group 3,-0.45436567459876476 +Group 3,1.045758517301446 +Group 3,0.8128161499741664 +Group 3,2.5327792143584578 +Group 3,2.469358769900285 +Group 4,1.3549474256969163 +Group 4,1.5781625196021736 +Group 4,0.3122142523698872 +Group 4,-0.780796468223927 +Group 4,0.8520878506738474 +Group 4,1.35634896910398 +Group 4,2.4302906807277207 +Group 4,2.4023798487844115 +Group 4,0.8126731825920477 +Group 4,0.8976972494246644 +Group 4,0.15144703493290734 +Group 4,-0.22001793717897522 +Group 4,-0.5062701906250127 +Group 4,3.15077539523179 +Group 4,0.6903478182483465 +Group 4,0.7619256983888136 +Group 4,-0.05279536004992624 +Group 4,1.9774903558319101 +Group 4,-0.41389784755795156 +Group 4,0.9872597197860312 +Group 4,0.30453343880632433 +Group 4,1.586902497859262 +Group 4,0.6891948624311269 +Group 4,0.019367815877587846 +Group 4,1.1718177716613452 +Group 4,1.6283318705304177 +Group 4,1.2665172223831678 +Group 4,1.5024718977397813 +Group 4,0.5656779063190364 +Group 4,0.8372588340128618 +Group 4,0.5275395522240489 +Group 4,0.8404468384594587 +Group 4,0.3868537179555459 +Group 4,-0.5262826023316769 +Group 4,1.3774261422537528 +Group 4,0.7982190637917381 +Group 4,-0.43019834696604464 +Group 4,1.662782255525774 +Group 4,0.29270163561675777 +Group 4,1.2519453957961388 +Group 4,1.929090562177537 +Group 4,1.3289829107574107 +Group 4,2.3394006845433006 +Group 4,-0.03482582035365267 +Group 4,1.6023416411775488 +Group 4,0.5151899090596868 +Group 4,0.3292028508181182 +Group 4,0.6211503352355845 +Group 4,0.8884474678726273 +Group 4,1.2561653422297454 +Group 5,0.3348501592166435 +Group 5,2.400826486954187 +Group 5,1.9656624397304598 +Group 5,-0.03624368627722374 +Group 5,2.9882521937955997 +Group 5,3.3958891760305834 +Group 5,2.6787795711596507 +Group 5,1.3200751641876491 +Group 5,0.4292473784894575 +Group 5,2.554451726931137 +Group 5,1.0968230530268204 +Group 5,2.7224450703824274 +Group 5,1.7082749780768602 +Group 5,2.4766390364837125 +Group 5,1.8563663971744018 +Group 5,2.206573168191948 +Group 5,1.5105000207208206 +Group 5,3.2858704939058354 +Group 5,1.6269120927036198 +Group 5,1.9019893634447016 +Group 6,3.5831506970562543 +Group 6,0.3522409388575536 +Group 6,0.42951500151426636 +Group 6,2.6693967081580112 +Group 6,0.52687659488584 +Group 6,3.643621185649293 +Group 6,1.2863810192402525 +Group 6,0.9525451885592422 +Group 6,3.6229420264803847 +Group 6,3.1805147914344243 +Group 6,3.56755896042657 +Group 7,2.9060446582753854 +Group 7,1.1387743149452976 +Group 7,3.9100649530990337 +Group 7,1.7319966290486195 +Group 7,2.802456395796395 +Group 7,2.947251967773748 +Group 7,1.8449899069091658 +Group 7,2.6140793703460803 +Group 7,2.922206671566527 +Group 7,2.3764255311556295 +Group 7,0.9005992094158055 +Group 7,2.298238174206056 +Group 7,3.3263858966870306 +Group 7,1.3054321402686346 +Group 7,1.8503654596723291 +Group 7,1.5648464482783626 +Group 7,3.849263728479342 +Group 7,2.6722947570124354 +Group 7,2.4074618362411107 +Group 7,1.2300839255546836 +Group 7,2.5392491912918174 +Group 7,1.325667339342624 +Group 7,2.031830558274351 +Group 7,1.364153921621119 +Group 7,2.6764332949464995 +Group 7,2.576590816614941 +Group 7,1.791701244422005 +Group 7,2.396006712661645 +Group 7,0.9069384912694942 +Group 7,0.5087424072943945 +Group 7,2.4393917012645367 +Group 7,2.166673495372529 +Group 7,2.635031436892106 +Group 7,4.383144774863942 +Group 7,2.9444794869904136 +Group 7,1.0871777745558413 +Group 7,3.117016288095853 +Group 7,0.6840925894884788 +Group 7,1.538415395185291 +Group 7,1.9317583946753687 +Group 7,3.713342721649367 +Group 7,1.25524517795156 +Group 7,1.1735614613409857 +Group 7,1.9015474755745676 +Group 7,1.3365217136378926 +Group 7,3.126635922106507 +Group 7,0.9200684916365767 +Group 7,0.8525313475888976 +Group 7,1.562179955255566 +Group 7,1.5019675493076952 +Group 7,3.9295320538169856 +Group 7,2.949420806925761 +Group 7,2.087551241385191 +Group 7,0.774564481169832 +Group 7,2.844362976401547 +Group 7,0.9997846526104353 +Group 7,0.4552289032223884 +Group 7,3.1880297923523018 +Group 7,2.3169426119248495 +Group 7,2.920858823780819 +Group 8,3.3187276529430214 +Group 8,3.856830611902691 +Group 8,2.348974406699853 +Group 8,1.9657571582155353 +Group 8,3.681594518281627 +Group 8,2.196590335826159 +Group 8,2.3104502222497993 +Group 8,2.544467496482657 +Group 8,3.017479159025057 +Group 8,2.646006088746516 +Group 8,1.6250487065819812 +Group 8,2.3563815971671094 +Group 8,0.7765968477755734 +Group 8,3.6252314510271875 +Group 8,1.3979423443932524 \ No newline at end of file diff --git a/statsmodels/sandbox/stats/tests/test_multicomp.py b/statsmodels/sandbox/stats/tests/test_multicomp.py index 9dfa6a35ff1..8e2f0565c44 100644 --- a/statsmodels/sandbox/stats/tests/test_multicomp.py +++ b/statsmodels/sandbox/stats/tests/test_multicomp.py @@ -12,6 +12,15 @@ from statsmodels.sandbox.stats.multicomp import tukey_pvalues from .results import result_hsubsets_dataframe +@pytest.fixture +def sample_data_balanced(): + df = pd.read_csv('./tests/results/sample_data_balanced.csv') + return df + +@pytest.fixture +def sample_data_unbalanced(): + df = pd.read_csv('./tests/results/sample_data_unbalanced.csv') + return df @pytest.mark.skipif(not SP_LT_116, reason="mvndst removed in SciPy 1.16") def test_tukey_pvalues(): @@ -30,15 +39,45 @@ def test_tukey_pvalues(): (0.1, result_hsubsets_dataframe.expected_df_alpha01), ], ) -def test_create_homogeneous_subsets_dataframe( +def test_create_homogeneous_subsets_dataframe_balanced( + alpha, + expected_df, + sample_data_balanced +): + from statsmodels.stats.multicomp import pairwise_tukeyhsd + from statsmodels.sandbox.stats.multicomp import TukeyHSDResults + + data = sample_data_balanced["data"] + groups = sample_data_balanced["groups"] + + tukey = pairwise_tukeyhsd( + endog=data, + groups=groups, + alpha=alpha, + ) + assert isinstance(tukey, TukeyHSDResults) + hs_df = tukey.create_homogeneous_subsets_dataframe() + assert isinstance(hs_df, pd.DataFrame) + assert pd.testing.assert_frame_equal(hs_df, expected_df) is None + assert hs_df.index.name == "Group" + + +@pytest.mark.parametrize( + "alpha, expected_df", + [ + (0.05, result_hsubsets_dataframe.expected_df_unbalanced_alpha005), + ], +) +def test_create_homogeneous_subsets_dataframe_unbalanced( alpha, expected_df, + sample_data_unbalanced ): from statsmodels.stats.multicomp import pairwise_tukeyhsd from statsmodels.sandbox.stats.multicomp import TukeyHSDResults - data = result_hsubsets_dataframe.data - groups = result_hsubsets_dataframe.groups + data = sample_data_unbalanced["data"] + groups = sample_data_unbalanced["groups"] tukey = pairwise_tukeyhsd( endog=data, @@ -48,5 +87,5 @@ def test_create_homogeneous_subsets_dataframe( assert isinstance(tukey, TukeyHSDResults) hs_df = tukey.create_homogeneous_subsets_dataframe() assert isinstance(hs_df, pd.DataFrame) - assert hs_df.equals(expected_df) + assert pd.testing.assert_frame_equal(hs_df, expected_df) is None assert hs_df.index.name == "Group"