diff --git a/docs/sig_tests.rst b/docs/sig_tests.rst
index 86b8865..8359a30 100644
--- a/docs/sig_tests.rst
+++ b/docs/sig_tests.rst
@@ -18,6 +18,8 @@ Functions
.. autofunction:: yli.ttest_ind
+.. autofunction:: yli.ttest_ind_multiple
+
Result classes
--------------
@@ -36,6 +38,9 @@ Result classes
.. autoclass:: yli.sig_tests.MannWhitneyResult
:members:
+.. autoclass:: yli.sig_tests.MultipleTTestResult
+ :members:
+
.. autoclass:: yli.sig_tests.PearsonChiSquaredResult
:members:
:inherited-members:
diff --git a/yli/__init__.py b/yli/__init__.py
index 9c5f614..0dd33f5 100644
--- a/yli/__init__.py
+++ b/yli/__init__.py
@@ -21,7 +21,7 @@ from .distributions import beta_oddsratio, beta_ratio, hdi, transformed_dist
from .graphs import init_fonts
from .io import pickle_read_compressed, pickle_read_encrypted, pickle_write_compressed, pickle_write_encrypted
from .regress import IntervalCensoredCox, Logit, OLS, OrdinalLogit, PenalisedLogit, regress, vif
-from .sig_tests import anova_oneway, auto_univariable, chi2, mannwhitney, pearsonr, spearman, ttest_ind
+from .sig_tests import anova_oneway, auto_univariable, chi2, mannwhitney, pearsonr, spearman, ttest_ind, ttest_ind_multiple
from .survival import kaplanmeier, logrank, turnbull
from .utils import as_ordinal
diff --git a/yli/sig_tests.py b/yli/sig_tests.py
index 359c016..24b6d98 100644
--- a/yli/sig_tests.py
+++ b/yli/sig_tests.py
@@ -180,6 +180,98 @@ def ttest_ind(df, dep, ind, *, nan_policy='warn'):
delta=Estimate(delta, ci0, ci1),
delta_direction='{} > {}'.format(group1, group2))
+class MultipleTTestResult:
+ """
+ Result of multiple Student's *t* tests, adjusted for multiplicity
+
+ See :func:`yli.ttest_ind_multiple`.
+ """
+
+ def __init__(self, *, dep, results):
+ #: Name of the dependent variable (*str*)
+ self.dep = dep
+ #: Results of the *t* tests (*List[*\ :class:`TTestResult`\ *]*)
+ self.results = results
+
+ def _comparison_table(self, html):
+ """Return a table showing the means/SDs for each group"""
+
+ group1 = self.results[0].group1
+ group2 = self.results[0].group2
+
+ # TODO: Render HTML directly so can have proper HTML p values
+ table_data = []
+ for row in self.results:
+ cell1 = '{:.2f} ({:.2f})'.format(row.mu1, row.sd1)
+ cell2 = '{:.2f} ({:.2f})'.format(row.mu2, row.sd2)
+ cell_pvalue = fmt_p(row.pvalue, PValueStyle.TABULAR)
+
+ # Display the cells the right way around
+ if row.group1 == group1 and row.group2 == group2:
+ table_data.append([cell1, cell2, cell_pvalue])
+ elif row.group1 == group2 and row.group2 == group1:
+ table_data.append([cell2, cell1, cell_pvalue])
+ else:
+ raise Exception('t tests have different groups')
+
+ if html:
+ table = pd.DataFrame(table_data, index=pd.Index([row.ind for row in self.results], name='\ue000 (SD)'), columns=pd.Index([self.results[0].group1, self.results[0].group2, '\ue001'], name=self.dep)) # U+E000 is in Private Use Area, mark μ symbol
+ table_str = table._repr_html_()
+ return table_str.replace('\ue000', 'μ').replace('\ue001', 'p')
+ else:
+ table = pd.DataFrame(table_data, index=pd.Index([row.ind for row in self.results], name='μ (SD)'), columns=pd.Index([self.results[0].group1, self.results[0].group2, 'p'], name=self.dep))
+ return str(table)
+
+ def __repr__(self):
+ if config.repr_is_summary:
+ return self.summary()
+ return super().__repr__()
+
+ def _repr_html_(self):
+ return self._comparison_table(True)
+
+ def summary(self):
+ """
+ Return a stringified summary of the *t* tests
+
+ :rtype: str
+ """
+ return str(self._comparison_table(False))
+
+def ttest_ind_multiple(df, dep, inds, *, nan_policy='warn', method='hs'):
+ """
+ Perform independent 2-sample Student's *t* tests with multiple independent variables, adjusting for multiplicity
+
+ :param df: Data to perform the test on
+ :type df: DataFrame
+ :param dep: Column in *df* for the dependent variable (numeric)
+ :type dep: str
+ :param ind: Columns in *df* for the independent variables (dichotomous)
+ :type ind: List[str]
+ :param nan_policy: How to handle *nan* values (see :ref:`nan-handling`)
+ :type nan_policy: str
+ :param method: Method to apply for multiplicity adjustment (see `statsmodels multipletests `_)
+ :type method: str
+
+ :rtype: :class:`yli.sig_tests.MultipleTTestResult`
+ """
+
+ # TODO: Unit testing
+ # FIXME: Assert groups of independent variables have same levels
+
+ # Perform t tests
+ results = []
+ for ind in inds:
+ results.append(ttest_ind(df, dep, ind, nan_policy=nan_policy))
+
+ # Adjust for multiplicity
+ _, pvalues_corrected, _, _ = sm.stats.multipletests([result.pvalue for result in results], alpha=config.alpha, method=method)
+
+ for result, pvalue_corrected in zip(results, pvalues_corrected):
+ result.pvalue = pvalue_corrected
+
+ return MultipleTTestResult(dep=dep, results=results)
+
# -------------
# One-way ANOVA