Repository: graspologic-org/graspologic

Issue:
returning test statistic in LDT
some practitioners (read: Vince, cep) only care about the test statistic and not the p-value. obviously one can still extract it if they perform the full test. however, that wastes time and resources. one can set the number of iterations to 1 to minimize that, but we can still do less. i propose to allow the number of permutations to be set to 0 (hyppo allows that, so really it is just a change in argument check). i am happy to do this, but:

this brings up the following questions: what should be happening to the fit_predict in that case? should it return the test statistic instead? or the p-value of 1? or NaN? should we be raising warnings?

and on a larger scale: should we really have this API? should fit predict return p-value, or a tuple of a p-value and a test statistic, like many other tests in python? furthremore, should it really be a class? once again, most tests in python that i have seen (scipy, statsmodels) are functions, not classes.


Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@
     "scipy>=1.1.0",
     "seaborn>=0.9.0",
     "matplotlib>=3.0.0",
-    "hyppo>=0.1.2",
+    "hyppo>=0.1.3",
 ]
 
 


Test patch:
diff --git a/graspy/inference/latent_distribution_test.py b/graspy/inference/latent_distribution_test.py
--- a/graspy/inference/latent_distribution_test.py
+++ b/graspy/inference/latent_distribution_test.py
@@ -12,57 +12,92 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import warnings
+
 import numpy as np
+from scipy import stats
 
 from ..embed import select_dimension, AdjacencySpectralEmbed
-from ..utils import import_graph, is_symmetric
+from ..utils import import_graph
 from .base import BaseInference
 from sklearn.metrics import pairwise_distances
+from sklearn.metrics.pairwise import pairwise_kernels
 from sklearn.metrics.pairwise import PAIRED_DISTANCES
+from sklearn.metrics.pairwise import PAIRWISE_KERNEL_FUNCTIONS
 from hyppo.ksample import KSample
+from hyppo._utils import gaussian
+
+_VALID_DISTANCES = list(PAIRED_DISTANCES.keys())
+_VALID_KERNELS = list(PAIRWISE_KERNEL_FUNCTIONS.keys())
+_VALID_KERNELS.append("gaussian")  # can use hyppo's medial gaussian kernel too
+_VALID_METRICS = _VALID_DISTANCES + _VALID_KERNELS
 
-_VALID_METRICS = list(PAIRED_DISTANCES.keys())
-_VALID_METRICS.append("gaussian")  # we have a gaussian kernel implemented too
 _VALID_TESTS = ["cca", "dcorr", "hhg", "rv", "hsic", "mgc"]
 
 
 class LatentDistributionTest(BaseInference):
-    """
-    Two-sample hypothesis test for the problem of determining whether two random
+    """Two-sample hypothesis test for the problem of determining whether two random
     dot product graphs have the same distributions of latent positions.
 
-    This test can operate on two graphs where there is no known matching between
-    the vertices of the two graphs.
-    Currently, testing is only supported for undirected graphs.
+    This test can operate on two graphs where there is no known matching
+    between the vertices of the two graphs, or even when the number of vertices
+    is different. Currently, testing is only supported for undirected graphs.
 
     Read more in the :ref:`tutorials <inference_tutorials>`
 
     Parameters
     ----------
-    test : str
+    test : str (default="hsic")
         Backend hypothesis test to use, one of ["cca", "dcorr", "hhg", "rv", "hsic", "mgc"].
         These tests are typically used for independence testing, but here they
         are used for a two-sample hypothesis test on the latent positions of
         two graphs. See :class:`hyppo.ksample.KSample` for more information.
 
-    metric : str or function, (default="euclidean")
-        Distance metric to use, either a callable or a valid string.
-        The callable should behave similarly to :func:`sklearn.metrics.pairwise_distances`,
-        if a string should be one of the keys in `sklearn.metrics.pairwise.PAIRED_DISTANCES`
-        or "gaussian" which will use a Gaussian kernel on Euclidean distances
-        with an adaptively selected bandwidth.
-
-    n_components : int or None, optional (default=None)
+    metric : str or function (default="gaussian")
+        Distance or a kernel metric to use, either a callable or a valid string.
+        If a callable, then it should behave similarly to either
+        :func:`sklearn.metrics.pairwise_distances` or to
+        :func:`sklearn.metrics.pairwise.pairwise_kernels`.
+        If a string, then it should be either one of the keys in either
+        `sklearn.metrics.pairwise.PAIRED_DISTANCES` or in
+        `sklearn.metrics.pairwise.PAIRWISE_KERNEL_FUNCTIONS`, or "gaussian",
+        which will use a gaussian kernel with an adaptively selected bandwidth.
+        It is recommended to use kernels (e.g. "gaussian") with kernel-based
+        hsic test and distances (e.g. "euclidean") with all other tests.
+
+    n_components : int or None (default=None)
         Number of embedding dimensions. If None, the optimal embedding
         dimensions are found by the Zhu and Godsi algorithm.
         See :func:`~graspy.embed.selectSVD` for more information.
 
     n_bootstraps : int (default=200)
-        Number of bootstrap iterations for the backend hypothesis test. See
-        :class:`hyppo.ksample.KSample` for more information.
+        Number of bootstrap iterations for the backend hypothesis test.
+        See :class:`hyppo.ksample.KSample` for more information.
 
-    workers : int, optional (default=1)
+    workers : int (default=1)
         Number of workers to use. If more than 1, parallelizes the code.
+        Supply -1 to use all cores available to the Process.
+
+    size_correction: bool (default=True)
+        Ignored when the two graphs have the same number of vertices. The test
+        degrades in validity as the number of vertices of the two graphs
+        diverge from each other, unless a correction is performed.
+        If True, when the two graphs have different numbers of vertices,
+        estimates the plug-in estimator for the variance and uses it to correct
+        the embedding of the larger graph.
+        If False, does not perform any modifications (not recommended).
+
+    pooled: bool (default=False)
+        Ignored whenever the two graphs have the same number of vertices or
+        size_correction is set to False. In order to correct the adjacency
+        spectral embedding used in the test, it is needed to estimate the
+        variance for each of the latent position estimates in the larger graph,
+        which requires to compute different sample moments. These moments can
+        be computed either over the larger graph (False), or over both graphs
+        (True). Setting it to True should not affect the behavior of the test
+        under the null hypothesis, but it is not clear whether it has more
+        power or less power under which alternatives. Generally not recomended,
+        as it is untested and included for experimental purposes.
 
     Attributes
     ----------
@@ -90,6 +125,10 @@ class LatentDistributionTest(BaseInference):
        "Improving Power of 2-Sample Random Graph Tests with Applications in Connectomics"
        arXiv:1911.02741
 
+    .. [4] Alyakin, A., Agterberg, J., Helm, H., Priebe, C. (2020)
+       "Correcting a Nonparametric Two-sample Graph Hypothesis Test for Graphs with Different Numbers of Vertices"
+       TODO cite the arXiv whenever possible
+
     """
 
     def __init__(
@@ -99,6 +138,8 @@ def __init__(
         n_components=None,
         n_bootstraps=200,
         workers=1,
+        size_correction=True,
+        pooled=False,
     ):
 
         if not isinstance(test, str):
@@ -125,38 +166,70 @@ def __init__(
         if not isinstance(n_bootstraps, int):
             msg = "n_bootstraps must be an int, not {}".format(type(n_bootstraps))
             raise TypeError(msg)
-        elif n_bootstraps < 1:
-            msg = "{} is invalid number of bootstraps, must be greater than 1"
+        elif n_bootstraps < 0:
+            msg = "{} is invalid number of bootstraps, must be non-negative"
             raise ValueError(msg.format(n_bootstraps))
 
         if not isinstance(workers, int):
             msg = "workers must be an int, not {}".format(type(workers))
             raise TypeError(msg)
-        elif workers <= 0:
-            msg = "{} is invalid number of workers, must be greater than 0"
-            raise ValueError(msg.format(workers))
+
+        if not isinstance(size_correction, bool):
+            msg = "size_correction must be a bool, not {}".format(type(size_correction))
+            raise TypeError(msg)
+
+        if not isinstance(pooled, bool):
+            msg = "pooled must be a bool, not {}".format(type(pooled))
+            raise TypeError(msg)
 
         super().__init__(n_components=n_components)
 
         if callable(metric):
             metric_func = metric
         else:
-            if metric == "gaussian":
-                metric_func = _medial_gaussian_kernel
+            if metric in _VALID_DISTANCES:
+                if test == "hsic":
+                    msg = (
+                        f"{test} is a kernel-based test, but {metric} "
+                        "is a distance. results may not be optimal. it is "
+                        "recomended to use either a different test or one of "
+                        f"the kernels: {_VALID_KERNELS} as a metric."
+                    )
+                    warnings.warn(msg, UserWarning)
+
+                def metric_func(X, Y=None, metric=metric, workers=None):
+                    return pairwise_distances(X, Y, metric=metric, n_jobs=workers)
+
+            elif metric == "gaussian":
+                if test != "hsic":
+                    msg = (
+                        f"{test} is a distance-based test, but {metric} "
+                        "is a kernel. results may not be optimal. it is "
+                        "recomended to use either a hisc as a test or one of "
+                        f"the distances: {_VALID_DISTANCES} as a metric."
+                    )
+                    warnings.warn(msg, UserWarning)
+                metric_func = gaussian
             else:
+                if test != "hsic":
+                    msg = (
+                        f"{test} is a distance-based test, but {metric} "
+                        "is a kernel. results may not be optimal. it is "
+                        "recomended to use either a hisc as a test or one of "
+                        f"the distances: {_VALID_DISTANCES} as a metric."
+                    )
+                    warnings.warn(msg, UserWarning)
 
                 def metric_func(X, Y=None, metric=metric, workers=None):
-                    return pairwise_distances(X, Y, metric=metric)
+                    return pairwise_kernels(X, Y, metric=metric, n_jobs=workers)
 
         self.test = KSample(test, compute_distance=metric_func)
         self.n_bootstraps = n_bootstraps
         self.workers = workers
+        self.size_correction = size_correction
+        self.pooled = pooled
 
     def _embed(self, A1, A2):
-        if not is_symmetric(A1) or not is_symmetric(A2):
-            msg = "currently, testing is only supported for undirected graphs"
-            raise NotImplementedError(msg)  # TODO asymmetric case
-
         if self.n_components is None:
             num_dims1 = select_dimension(A1)[0][-1]
             num_dims2 = select_dimension(A2)[0][-1]
@@ -170,10 +243,44 @@ def _embed(self, A1, A2):
             X1_hat = np.concatenate(X1_hat, axis=-1)
             X2_hat = np.concatenate(X2_hat, axis=-1)
         elif isinstance(X1_hat, tuple) ^ isinstance(X2_hat, tuple):
-            raise ValueError("Input graphs do not have same directedness")
+            msg = (
+                "input graphs do not have same directedness. "
+                "consider symmetrizing the directed graph."
+            )
+            raise ValueError(msg)
 
         return X1_hat, X2_hat
 
+    def _sample_modified_ase(self, X, Y, pooled=False):
+        N, M = len(X), len(Y)
+
+        # return if graphs are same order, else else ensure X the larger graph.
+        if N == M:
+            return X, Y
+        elif M > N:
+            reverse_order = True
+            X, Y = Y, X
+            N, M = M, N
+        else:
+            reverse_order = False
+
+        # estimate the central limit theorem variance
+        if pooled:
+            two_samples = np.concatenate([X, Y], axis=0)
+            get_sigma = _fit_plug_in_variance_estimator(two_samples)
+        else:
+            get_sigma = _fit_plug_in_variance_estimator(X)
+        X_sigmas = get_sigma(X) * (N - M) / (N * M)
+
+        # increase the variance of X by sampling from the asy dist
+        X_sampled = np.zeros(X.shape)
+        # TODO may be parallelized, but requires keeping track of random state
+        for i in range(N):
+            X_sampled[i, :] = X[i, :] + stats.multivariate_normal.rvs(cov=X_sigmas[i])
+
+        # return the embeddings in the appropriate order
+        return (Y, X_sampled) if reverse_order else (X_sampled, Y)
+
     def fit(self, A1, A2):
         """
         Fits the test to the two input graphs
@@ -193,6 +300,11 @@ def fit(self, A1, A2):
         X1_hat, X2_hat = self._embed(A1, A2)
         X1_hat, X2_hat = _median_sign_flips(X1_hat, X2_hat)
 
+        if self.size_correction:
+            X1_hat, X2_hat = self._sample_modified_ase(
+                X1_hat, X2_hat, pooled=self.pooled
+            )
+
         data = self.test.test(
             X1_hat, X2_hat, reps=self.n_bootstraps, workers=self.workers, auto=False
         )
@@ -204,17 +316,6 @@ def fit(self, A1, A2):
         return self
 
 
-def _medial_gaussian_kernel(X, Y=None, workers=None):
-    """Baseline medial gaussian kernel similarity calculation
-    Y is dummy to mimic sklearn pairwise_distances"""
-    l1 = pairwise_distances(X, Y=Y, metric="cityblock")
-    mask = np.ones(l1.shape, dtype=bool)
-    np.fill_diagonal(mask, 0)
-    gamma = 1.0 / (2 * (np.median(l1[mask]) ** 2))
-    K = np.exp(-gamma * pairwise_distances(X, Y=Y, metric="sqeuclidean"))
-    return 1 - K / np.max(K)
-
-
 def _median_sign_flips(X1, X2):
     X1_medians = np.median(X1, axis=0)
     X2_medians = np.median(X2, axis=0)
@@ -222,3 +323,59 @@ def _median_sign_flips(X1, X2):
     t = (val > 0) * 2 - 1
     X1 = np.multiply(t.reshape(-1, 1).T, X1)
     return X1, X2
+
+
+def _fit_plug_in_variance_estimator(X):
+    """
+    Takes in ASE of a graph and returns a function that estimates
+    the variance-covariance matrix at a given point using the
+    plug-in estimator from the RDPG Central Limit Theorem.
+
+    Parameters
+    ----------
+    X : np.ndarray, shape (n, d)
+        adjacency spectral embedding of a graph
+
+    Returns
+    -------
+    plug_in_variance_estimtor: functions
+        a function that estimates variance (see below)
+    """
+
+    n = len(X)
+
+    # precompute the Delta and the middle term matrix part
+    delta = 1 / (n) * (X.T @ X)
+    delta_inverse = np.linalg.inv(delta)
+    middle_term_matrix = np.einsum("bi,bo->bio", X, X)
+
+    def plug_in_variance_estimator(x):
+        """
+        Takes in a point of a matrix of points in R^d and returns an
+        estimated covariance matrix for each of the points
+
+        Parameters:
+        -----------
+        x: np.ndarray, shape (n, d)
+            points to estimate variance at
+            if 1-dimensional - reshaped to (1, d)
+
+        Returns:
+        -------
+        covariances: np.ndarray, shape (n, d, d)
+            n estimated variance-covariance matrices of the points provided
+        """
+        if x.ndim < 2:
+            x = x.reshape(1, -1)
+        # the following two lines are a properly vectorized version of
+        # middle_term = 0
+        # for i in range(n):
+        #     middle_term += np.multiply.outer((x @ X[i] - (x @ X[i]) ** 2),
+        #                                      np.outer(X[i], X[i]))
+        # where the matrix part does not involve x and has been computed above
+        middle_term_scalar = x @ X.T - (x @ X.T) ** 2
+        middle_term = np.tensordot(middle_term_scalar, middle_term_matrix, axes=1)
+        covariances = delta_inverse @ (middle_term / n) @ delta_inverse
+        return covariances
+
+    return plug_in_variance_estimator
diff --git a/tests/test_latentdistributiontest.py b/tests/test_latentdistributiontest.py
--- a/tests/test_latentdistributiontest.py
+++ b/tests/test_latentdistributiontest.py
@@ -1,9 +1,5 @@
-# Bijan Varjavand
-# bvarjavand [at] jhu.edu
-# 02.26.2019
-
+import pytest
 import unittest
-
 import numpy as np
 from sklearn.metrics import pairwise_distances
 
@@ -15,23 +11,20 @@ class TestLatentDistributionTest(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         np.random.seed(123456)
-        cls.tests = ["dcorr", "mgc"]
-        cls.dists = ["euclidean", "gaussian"]
+        cls.tests = {"dcorr": "euclidean", "hsic": "gaussian", "mgc": "euclidean"}
         cls.A1 = er_np(20, 0.3)
         cls.A2 = er_np(20, 0.3)
 
     def test_fit_ase_works(self):
-        for dist in self.dists:
-            for test in self.tests:
-                ldt = LatentDistributionTest(test, dist, n_bootstraps=10)
-                assert ldt.fit(self.A1, self.A2) is ldt
+        for test in self.tests.keys():
+            ldt = LatentDistributionTest(test, self.tests[test], n_bootstraps=10)
+            assert ldt.fit(self.A1, self.A2) is ldt
 
     def test_fit_predict_ase_works(self):
-        for dist in self.dists:
-            for test in self.tests:
-                ldt = LatentDistributionTest(test, dist, n_bootstraps=10)
-                p = ldt.fit_predict(self.A1, self.A2)
-                assert float(p) <= 1 and float(p) >= 0
+        for test in self.tests:
+            ldt = LatentDistributionTest(test, self.tests[test], n_bootstraps=10)
+            p = ldt.fit_predict(self.A1, self.A2)
+            assert float(p) <= 1 and float(p) >= 0
 
     def test_workers(self):
         ldt = LatentDistributionTest("dcorr", "euclidean", n_bootstraps=4, workers=4)
@@ -51,25 +44,48 @@ def test_bad_kwargs(self):
             LatentDistributionTest(test="dcorr", n_components=-100)
         with self.assertRaises(ValueError):
             LatentDistributionTest(test="dcorr", n_bootstraps=-100)
-        with self.assertRaises(ValueError):
-            LatentDistributionTest(test="dcorr", workers=-1)
         with self.assertRaises(TypeError):
             LatentDistributionTest(test=0)
         with self.assertRaises(TypeError):
             LatentDistributionTest(test="dcorr", distance=0)
         with self.assertRaises(TypeError):
             LatentDistributionTest(test="dcorr", n_bootstraps=0.5)
-        with self.assertRaises(TypeError):
-            LatentDistributionTest(test="dcorr", n_components=0.5)
         with self.assertRaises(TypeError):
             LatentDistributionTest(test="dcorr", workers=0.5)
+        with self.assertRaises(TypeError):
+            LatentDistributionTest(size_correction=0)
+        with self.assertRaises(TypeError):
+            LatentDistributionTest(pooled=0)
 
     def test_n_bootstraps(self):
-        for test in self.tests:
-            ldt = LatentDistributionTest(test, n_bootstraps=123)
+        for test in self.tests.keys():
+            ldt = LatentDistributionTest(test, self.tests[test], n_bootstraps=123)
             ldt.fit(self.A1, self.A2)
             self.assertEqual(ldt.null_distribution_.shape[0], 123)
 
+    def test_pooled(self):
+        np.random.seed(123)
+        A1 = er_np(20, 0.3)
+        A2 = er_np(100, 0.3)
+
+        ldt = LatentDistributionTest(pooled=True)
+        ldt.fit(A1, A2)
+
+    def test_distances_and_kernels(self):
+        # some valid combinations of test and metric
+        with pytest.warns(None) as record:
+            for test in self.tests.keys():
+                ldt = LatentDistributionTest(test, self.tests[test])
+            ldt = LatentDistributionTest("hsic", "rbf")
+        assert len(record) == 0
+        # some invalid combinations of test and metric
+        with pytest.warns(UserWarning):
+            ldt = LatentDistributionTest("hsic", "euclidean")
+        with pytest.warns(UserWarning):
+            ldt = LatentDistributionTest("dcorr", "gaussian")
+        with pytest.warns(UserWarning):
+            ldt = LatentDistributionTest("dcorr", "rbf")
+
     def test_bad_matrix_inputs(self):
         ldt = LatentDistributionTest("dcorr")
 
@@ -81,54 +97,82 @@ def test_directed_inputs(self):
         np.random.seed(2)
         A = er_np(100, 0.3, directed=True)
         B = er_np(100, 0.3, directed=True)
+        C = er_np(100, 0.3, directed=False)
 
+        # two directed graphs is okay
         ldt = LatentDistributionTest("dcorr")
-        with self.assertRaises(NotImplementedError):
-            ldt.fit(A, B)
+        ldt.fit(A, B)
 
-    def test_SBM_euclidean(self):
-        np.random.seed(12345678)
-        B1 = np.array([[0.5, 0.2], [0.2, 0.5]])
+        # an undirected and a direced graph is not okay
+        with self.assertRaises(ValueError):
+            ldt.fit(A, C)
+        with self.assertRaises(ValueError):
+            ldt.fit(C, B)
 
-        B2 = np.array([[0.7, 0.2], [0.2, 0.7]])
-        b_size = 200
-        A1 = sbm(2 * [b_size], B1)
-        A2 = sbm(2 * [b_size], B1)
-        A3 = sbm(2 * [b_size], B2)
+    def test_SBM_dcorr(self):
+        for test in self.tests.keys():
+            np.random.seed(12345678)
+            B1 = np.array([[0.5, 0.2], [0.2, 0.5]])
+
+            B2 = np.array([[0.7, 0.2], [0.2, 0.7]])
+            b_size = 200
+            A1 = sbm(2 * [b_size], B1)
+            A2 = sbm(2 * [b_size], B1)
+            A3 = sbm(2 * [b_size], B2)
 
-        for test in self.tests:
             ldt_null = LatentDistributionTest(
-                test, "euclidean", n_components=2, n_bootstraps=50
+                test, self.tests[test], n_components=2, n_bootstraps=100
             )
             ldt_alt = LatentDistributionTest(
-                test, "euclidean", n_components=2, n_bootstraps=50
+                test, self.tests[test], n_components=2, n_bootstraps=100
             )
             p_null = ldt_null.fit_predict(A1, A2)
             p_alt = ldt_alt.fit_predict(A1, A3)
             self.assertTrue(p_null > 0.05)
             self.assertTrue(p_alt <= 0.05)
 
-    def test_SBM_gaussian(self):
-        np.random.seed(12345678)
-        B1 = np.array([[0.5, 0.2], [0.2, 0.5]])
+    def test_different_sizes_null(self):
+        np.random.seed(314)
 
-        B2 = np.array([[0.7, 0.2], [0.2, 0.7]])
-        b_size = 200
-        A1 = sbm(2 * [b_size], B1)
-        A2 = sbm(2 * [b_size], B1)
-        A3 = sbm(2 * [b_size], B2)
+        A1 = er_np(100, 0.8)
+        A2 = er_np(1000, 0.8)
 
-        for test in self.tests:
-            ldt_null = LatentDistributionTest(
-                test, "gaussian", n_components=2, n_bootstraps=50
-            )
-            ldt_alt = LatentDistributionTest(
-                test, "gaussian", n_components=2, n_bootstraps=50
-            )
-            p_null = ldt_null.fit_predict(A1, A2)
-            p_alt = ldt_alt.fit_predict(A1, A3)
-            self.assertTrue(p_null > 0.05)
-            self.assertTrue(p_alt <= 0.05)
+        ldt_not_corrected = LatentDistributionTest(
+            "hsic", "gaussian", n_components=2, n_bootstraps=100, size_correction=False
+        )
+        ldt_corrected_1 = LatentDistributionTest(
+            "hsic", "gaussian", n_components=2, n_bootstraps=100, size_correction=True
+        )
+        ldt_corrected_2 = LatentDistributionTest(
+            "hsic", "gaussian", n_components=2, n_bootstraps=100, size_correction=True
+        )
+
+        p_not_corrected = ldt_not_corrected.fit_predict(A1, A2)
+        p_corrected_1 = ldt_corrected_1.fit_predict(A1, A2)
+        p_corrected_2 = ldt_corrected_2.fit_predict(A2, A1)
+
+        self.assertTrue(p_not_corrected <= 0.05)
+        self.assertTrue(p_corrected_1 > 0.05)
+        self.assertTrue(p_corrected_2 > 0.05)
+
+    def test_different_sizes_null(self):
+        np.random.seed(314)
+
+        A1 = er_np(100, 0.8)
+        A2 = er_np(1000, 0.7)
+
+        ldt_corrected_1 = LatentDistributionTest(
+            "hsic", "gaussian", n_components=2, n_bootstraps=100, size_correction=True
+        )
+        ldt_corrected_2 = LatentDistributionTest(
+            "hsic", "gaussian", n_components=2, n_bootstraps=100, size_correction=True
+        )
+
+        p_corrected_1 = ldt_corrected_1.fit_predict(A1, A2)
+        p_corrected_2 = ldt_corrected_2.fit_predict(A2, A1)
+
+        self.assertTrue(p_corrected_1 <= 0.05)
+        self.assertTrue(p_corrected_2 <= 0.05)
 
 
 if __name__ == "__main__":