Repository: neurodata/hyppo (language: python)

Issue:
Fast Two-Sample Testing with Analytic Representations of Probability Measures
https://arxiv.org/abs/1506.04725

#187 


Discussion / hints:

Fix patch:
diff --git a/docs/api/index.rst b/docs/api/index.rst
index 7bb5ec50a..4d52bda2f 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -54,7 +54,8 @@ Independence
    DISCO
    MANOVA
    Hotelling
-
+   SmoothCFTest
+   MeanEmbeddingTest
 
 
 .. automodule:: hyppo.time_series
@@ -163,6 +164,8 @@ Miscellaneous
    tools.multi_perm_test
    tools.chi2_approx
    tools.power
+   ksample.smoothCF.smooth_cf_distance
+   ksample.mean_embedding.mean_embed_distance
 
 
 
diff --git a/docs/refs.bib b/docs/refs.bib
index 4b8351877..cf09484ed 100644
--- a/docs/refs.bib
+++ b/docs/refs.bib
@@ -472,6 +472,18 @@ @article{shenChiSquareTestDistance2021
   annotation = {\_eprint: https://doi.org/10.1080/10618600.2021.1938585},
 }
 
+@article{chwialkowski2015fast,
+      title={Fast Two-Sample Testing with Analytic Representations of Probability Measures},
+      author={Kacper Chwialkowski and Aaditya Ramdas and Dino Sejdinovic and Arthur Gretton},
+      year={2015},
+      journal={arXiv:1506.04725 [math, stat]},
+      print={1506.04725},
+      eprinttype={arxiv},
+      abstract={We propose a class of nonparametric two-sample tests with a cost linear in the sample size. Two tests are given, both based on an ensemble of distances between analytic functions representing each of the distributions. The first test uses smoothed empirical characteristic functions to represent the distributions, the second uses distribution embeddings in a reproducing kernel Hilbert space. Analyticity implies that differences in the distributions may be detected almost surely at a finite number of randomly chosen locations/frequencies. The new tests are consistent against a larger class of alternatives than the previous linear-time tests based on the (non-smoothed) empirical characteristic functions, while being much faster than the current state-of-the-art quadratic-time kernel-based or energy distance-based tests. Experiments on artificial benchmarks and on challenging real-world testing problems demonstrate that our tests give a better power/time tradeoff than competing approaches, and in some cases, better outright power than even the most expensive quadratic-time tests. This performance advantage is retained even in high dimensions, and in cases where the difference in distributions is not observable with low order statistics.},
+      archivePrefix={arXiv},
+      primaryClass={stat.ML}
+}
+
 @article{grettonKernelJointIndependence2016,
   title = {{Kernel-based Tests} for {Joint Independence}},
   author = {Pfister, Nikolas and Buhlmann, Peter and Scholkopf, Bernhard and Peters, Jonas},
@@ -484,4 +496,4 @@ @article{grettonKernelJointIndependence2016
   abstract = {We investigate the problem of testing whether d random variables, which may or may not be continuous, are jointly (or mutually) independent. Our method builds on ideas of the two variable Hilbert-Schmidt independence criterion (HSIC) but allows for an arbitrary number of variables. We embed the d-dimensional joint distribution and the product of the marginals into a reproducing kernel Hilbert space and define the d-variable Hilbert-Schmidt independence criterion (dHSIC) as the squared distance between the embeddings. In the population case, the value of dHSIC is zero if and only if the d variables are jointly independent, as long as the kernel is characteristic. Based on an empirical estimate of dHSIC, we define three different non-parametric hypothesis tests: a permutation test, a bootstrap test and a test based on a Gamma approximation. We prove that the permutation test achieves the significance level and that the bootstrap test achieves pointwise asymptotic significance level as well as pointwise asymptotic consistency (i.e., it is able to detect any type of fixed dependence in the large sample limit). The Gamma approximation does not come with these guarantees; however, it is computationally very fast and for small d, it performs well in practice. Finally, we apply the test to a problem in causal discovery.},
   archiveprefix = {arXiv},
   keywords = {Math - Statistics Theory, Statistics - Machine Learning},
- }
\ No newline at end of file
+ }
diff --git a/hyppo/independence/__init__.py b/hyppo/independence/__init__.py
index 1c5826af9..fe96d0a06 100644
--- a/hyppo/independence/__init__.py
+++ b/hyppo/independence/__init__.py
@@ -8,6 +8,7 @@
 from .mgc import MGC
 from .rv import RV
 
+
 __all__ = [s for s in dir()]  # add imported tests to __all__
 
 
diff --git a/hyppo/ksample/__init__.py b/hyppo/ksample/__init__.py
index a575245b2..2aadb269e 100644
--- a/hyppo/ksample/__init__.py
+++ b/hyppo/ksample/__init__.py
@@ -6,6 +6,9 @@
 from .manova import MANOVA
 from .mmd import MMD
 
+from .smoothCF import SmoothCFTest, smooth_cf_distance
+from .mean_embedding import MeanEmbeddingTest, mean_embed_distance
+
 __all__ = [s for s in dir()]  # add imported tests to __all__
 
 
@@ -16,4 +19,6 @@
     "ksample": KSample,
     "manova": MANOVA,
     "mmd": MMD,
+    "smoothCF": SmoothCFTest,
+    "mean_embedding": MeanEmbeddingTest,
 }
diff --git a/hyppo/ksample/mean_embedding.py b/hyppo/ksample/mean_embedding.py
new file mode 100644
index 000000000..0647137b0
--- /dev/null
+++ b/hyppo/ksample/mean_embedding.py
@@ -0,0 +1,173 @@
+import numpy as np
+from scipy.stats import chi2
+from ._utils import _CheckInputs
+from .base import KSampleTestOutput, KSampleTest
+
+
+class MeanEmbeddingTest(KSampleTest):
+    r"""
+    Mean Embedding test statistic and p-value.
+
+    The Mean Embedding test is a two-sample test that uses
+    differences in (analytic) mean embeddings of two data
+    distributions in a reproducing kernel Hilbert space.
+    :footcite:p:`chwialkowski2015fast`.
+
+    Parameters
+    ----------
+    num_randfreq: int
+        Used to construct random array with size ``(p, q)`` where `p` is the number of
+        dimensions of the data and `q` is the random frequency at which the
+        test is performed. These are the random test points at which test occurs (see notes).
+
+    Notes
+    -----
+    The test statistic, like the Smooth CF statistic, takes on the following form:
+
+    .. math::
+
+        W_n\Sigma_n^{-1}W_n
+
+    As seen in the above formulation, this test-statistic takes the same form as
+    the Hotelling :math:`T^2` statistic found in :class:`hyppo.ksample.Hotelling`.
+    However, the components are defined differently in this case. Given data sets
+    X and Y, define the following as :math:`Z_i`, the vector of differences:
+
+    .. math::
+
+        Z_i = (k(X_i, T_1) - k(Y_i, T_1), \ldots,
+        k(X_i, T_J) - k(Y_i, T_J)) \in \mathbb{R}^J
+
+    The above is the vector of differences between kernels at test points,
+    :math:`T_j`. The kernel maps into the reproducing kernel Hilbert space.
+    This same formulation is used in the Mean Embedding Test.
+    Moving forward, :math:`W_n` can be defined:
+
+    .. math::
+
+        W_n = \frac{1}{n} \sum_{i = 1}^n Z_i
+
+    This leaves :math:`\Sigma_n`, the covariance matrix as:
+
+    .. math::
+
+        \Sigma_n = \frac{1}{n}ZZ^T
+
+    Once :math:`S_n` is calculated, a threshold :math:`r_{\alpha}` corresponding to the
+    :math:`1 - \alpha` quantile of a Chi-squared distribution w/ J degrees of freedom
+    is chosen. Null is rejected if :math:`S_n` is larger than this threshold.
+
+    References
+    ----------
+    .. footbibliography::
+    """
+
+    def __init__(self, num_randfreq=5):
+        self.num_randfreq = num_randfreq
+        KSampleTest.__init__(self)
+
+    def statistic(self, x, y, random_state):
+        r"""
+        Calculates the mean embedding test statistic.
+
+        Parameters
+        ----------
+        x,y : ndarray of float
+            Input data matrices. ``x`` and ``y`` must have the same number of
+            dimensions. That is, the shapes must be ``(n, p)`` and ``(m, p)`` where
+            `n` is the number of samples and `p` and `q` are the number of
+            dimensions.
+        random_state: int
+            Set random seed for generation of test points
+
+        Returns
+        -------
+        stat : float
+            The computed mean embedding statistic.
+        """
+        _, p = np.shape(x)
+
+        if random_state:
+            np.random.seed(random_state)
+        points = np.random.randn(self.num_randfreq, p)
+
+        ra = np.zeros((self.num_randfreq, x.shape[0]))
+        for idx, point in enumerate(points):
+
+            ra[idx] = np.exp(-(np.linalg.norm(x - point, axis=1) ** 2) / 2) - np.exp(
+                -(np.linalg.norm(y - point, axis=1) ** 2) / 2
+            )
+
+        obs = ra.T
+        return mean_embed_distance(obs, self.num_randfreq)
+
+    def test(self, x, y, random_state=None):
+        r"""
+        Calculates the mean embedding test statistic and p-value.
+
+        Parameters
+        ----------
+        x,y : ndarray of float
+            Input data matrices. ``x`` and ``y`` must have the same number of
+            dimensions. That is, the shapes must be ``(n, p)`` and ``(m, p)`` where
+            `n` is the number of samples and `p` and `q` are the number of
+            dimensions.
+        random_state: int
+            Set random seed for generation of test points
+
+        Returns
+        -------
+        stat : float
+            The computed mean embedding statistic.
+        pvalue : float
+            The computed mean embedding p-value.
+
+        Examples
+        --------
+        >>> import numpy as np
+        >>> from hyppo.ksample import MeanEmbeddingTest
+        >>> np.random.seed(1234)
+        >>> x = np.random.randn(500, 10)
+        >>> y = np.random.randn(500, 10)
+        >>> stat, pvalue = MeanEmbeddingTest().test(x, y, random_state=1234)
+        >>> '%.2f, %.3f' % (stat, pvalue)
+        '5.33, 0.377'
+        """
+        check_input = _CheckInputs(inputs=[x, y], indep_test=None)
+        x, y = check_input()
+
+        stat = self.statistic(x, y, random_state)
+        pvalue = chi2.sf(stat, self.num_randfreq)
+        self.stat = stat
+        self.pvalue = pvalue
+
+        return KSampleTestOutput(stat, pvalue)
+
+
+def mean_embed_distance(difference, num_randfeatures):
+    r"""
+    Calculates the Mean Embedding test statistic using the vector of differences.
+
+    Parameters
+    ----------
+    difference : ndarray of float
+        The vector of differences which indicates distance between mean embeddings.
+    num_randfeatures : int
+        The number of test frequencies
+
+    Returns
+    -------
+    stat : float
+        The computed mean embedding statistic.
+    """
+    num_samples, _ = np.shape(difference)
+    sigma = np.cov(np.transpose(difference))
+
+    mu = np.mean(difference, 0)
+
+    if num_randfeatures == 1:
+        stat = float(num_samples * mu ** 2) / float(sigma)
+    else:
+        stat = num_samples * mu.dot(np.linalg.solve(sigma, np.transpose(mu)))
+
+    return stat
diff --git a/hyppo/ksample/smoothCF.py b/hyppo/ksample/smoothCF.py
new file mode 100644
index 000000000..7beaf5305
--- /dev/null
+++ b/hyppo/ksample/smoothCF.py
@@ -0,0 +1,183 @@
+import numpy as np
+from scipy.stats import chi2
+from ._utils import _CheckInputs
+from hyppo.ksample.base import KSampleTest, KSampleTestOutput
+
+
+class SmoothCFTest(KSampleTest):
+    r"""
+    Smooth Characteristic Function test statistic and p-value
+
+    The Smooth Characteristic Function test is a two-sample test that uses
+    differences in the smoothed (analytic) characteristic function of
+    two data distributions in order to determine how different
+    the two data are :footcite:p:`chwialkowski2015fast`.
+
+    Parameters
+    ----------
+    num_randfreq: int
+        Used to construct random array with size ``(p, q)`` where `p` is the number of
+        dimensions of the data and `q` is the random frequency at which the
+        test is performed. These are the random test points at which test occurs (see notes).
+
+    Notes
+    -----
+    The test statistic takes on the following form:
+
+    .. math::
+
+        nW_n\Sigma_n^{-1}W_n
+
+    As seen in the above formulation, this test-statistic takes the same form as
+    the Hotelling :math:`T^2` statistic. However, the components are
+    defined differently in this case. Given data sets
+    X and Y, define the following as :math:`Z_i`, the vector of differences:
+
+    .. math::
+
+        Z_i = (k(X_i, T_1) - k(Y_i, T_1), \ldots,
+        k(X_i, T_J) - k(Y_i, T_J)) \in \mathbb{R}^J
+
+    The above is the vector of differences between kernels at test points, :math:`T_j`.
+    This same formulation is used in the Mean Embedding Test.
+    Moving forward, :math:`W_n` can be defined:
+
+    .. math::
+
+        W_n = \frac{1}{n} \sum_{i = 1}^n Z_i
+
+    This leaves :math:`\Sigma_n`, the covariance matrix as:
+
+    .. math::
+
+        \Sigma_n = \frac{1}{n}ZZ^T
+
+    In the specific case of the Smooth Characteristic function test,
+    the vector of differences can be defined as follows:
+
+    .. math::
+
+        Z_i = (f(X_i)\sin(X_iT_1) - f(Y_i)\sin(Y_iT_1),
+        f(X_i)\cos(X_iT_1) - f(Y_i)\cos(Y_iT_1),\cdots) \in \mathbb{R}^{2J}
+
+    Once :math:`S_n` is calculated, a threshold :math:`r_{\alpha}` corresponding to the
+    :math:`1 - \alpha` quantile of a Chi-squared distribution w/ J degrees of freedom
+    is chosen. Null is rejected if :math:`S_n` is larger than this threshold.
+
+    References
+    ----------
+    .. footbibliography::
+    """
+
+    def __init__(self, num_randfreq=5):
+
+        self.num_randfreq = num_randfreq
+        KSampleTest.__init__(self)
+
+    def statistic(self, x, y, random_state):
+        r"""
+        Calculates the smooth CF test statistic.
+
+        Parameters
+        ----------
+        x,y : ndarray of float
+            Input data matrices. ``x`` and ``y`` must have the same number of
+            dimensions. That is, the shapes must be ``(n, p)`` and ``(m, p)`` where
+            `n` is the number of samples and `p` and `q` are the number of
+            dimensions.
+        random_state: int
+            Set random seed for generation of test points
+
+        Returns
+        -------
+        stat : float
+            The computed Smooth CF statistic.
+        """
+        _, p = np.shape(x)
+        if random_state:
+            np.random.seed(random_state)
+        random_frequencies = np.random.randn(p, self.num_randfreq)
+
+        x_smooth = np.exp(-np.linalg.norm(x, axis=1) ** 2 / 2).reshape(-1, 1)
+        y_smooth = np.exp(-np.linalg.norm(y, axis=1) ** 2 / 2).reshape(-1, 1)
+
+        x_mat = x.dot(random_frequencies)
+        y_mat = y.dot(random_frequencies)
+
+        x_smooth_cf = np.concatenate(
+            (np.sin(x_mat) * x_smooth, np.cos(x_mat) * x_smooth), 1
+        )
+        y_smooth_cf = np.concatenate(
+            (np.sin(y_mat) * y_smooth, np.cos(y_mat) * y_smooth), 1
+        )
+
+        difference = x_smooth_cf - y_smooth_cf
+        return smooth_cf_distance(difference)
+
+    def test(self, x, y, random_state=None):
+        r"""
+        Calculates the smooth CF test statistic and p-value.
+
+        Parameters
+        ----------
+        x,y : ndarray of float
+            Input data matrices. ``x`` and ``y`` must have the same number of
+            dimensions. That is, the shapes must be ``(n, p)`` and ``(m, p)`` where
+            `n` is the number of samples and `p` and `q` are the number of
+            dimensions.
+        random_state: int
+            Set random seed for generation of test points
+
+        Returns
+        -------
+        stat : float
+            The computed Smooth CF statistic.
+        pvalue : float
+            The computed smooth CF p-value.
+
+        Examples
+        --------
+        >>> import numpy as np
+        >>> from hyppo.ksample import SmoothCFTest
+        >>> np.random.seed(1234)
+        >>> x = np.random.randn(500, 10)
+        >>> y = np.random.randn(500, 10)
+        >>> stat, pvalue = SmoothCFTest().test(x, y, random_state=1234)
+        >>> '%.2f, %.3f' % (stat, pvalue)
+        '4.70, 0.910'
+        """
+        check_input = _CheckInputs(inputs=[x, y], indep_test=None)
+        x, y = check_input()
+
+        stat = self.statistic(x, y, random_state)
+        pvalue = chi2.sf(stat, 2 * self.num_randfreq)
+        self.stat = stat
+        self.pvalue = pvalue
+
+        return KSampleTestOutput(stat, pvalue)
+
+
+def smooth_cf_distance(difference):
+    r"""
+    Calculates the Smooth CF test statistic using the vector of differences.
+
+    Parameters
+    ----------
+    difference : ndarray of float
+        The vector of differences which indicates distance between mean embeddings.
+    num_randfeatures : int
+        The number of test frequencies
+
+    Returns
+    -------
+    stat : float
+        The computed smooth CF statistic.
+    """
+    num_samples, _ = np.shape(difference)
+    sigma = np.cov(np.transpose(difference))
+
+    mu = np.mean(difference, 0)
+
+    stat = num_samples * mu.dot(np.linalg.solve(sigma, np.transpose(mu)))
+
+    return stat
diff --git a/tutorials/dvariate.py b/tutorials/dvariate.py
index e9d732d9d..6c1dd5c01 100644
--- a/tutorials/dvariate.py
+++ b/tutorials/dvariate.py
@@ -59,4 +59,3 @@
 print(stat, pvalue)
 
 ########################################################################################
-
diff --git a/tutorials/ksample.py b/tutorials/ksample.py
index d0d8ba071..2171591a1 100644
--- a/tutorials/ksample.py
+++ b/tutorials/ksample.py
@@ -164,6 +164,81 @@
 # Any addition, if the bias variant of the test statistic is required, then the ``bias``
 # parameter can be set to ``True``. In general, we do not recommend doing this.
 # Otherwise, these tests runs like :ref:`any other test<general indep>`.
+
+########################################################################################
+# Smooth Characteristic Function Test
+# --------------------------------------------
+#
+# The **Smooth Characteristic Function Test** (Smooth CF), is a form of non-parametric two-sample
+# tests. The Smooth CF test utilizes smoothed empirical characteristic functions to represent
+# two data distributions. Characteristic functions completely define the probability distribution
+# of a random variable. In hypothesis testing, it is useful to estimate characteristic functions
+# for given data. However, empirical characteristic functions can be very complex and therefore
+# expensive to compute. The smooth characteristic function can serve as a heuristic in place of
+# the empirical function which is much faster w.r.t. computation times. More information can be
+# found at :class:`hyppo.ksample.SmoothCFTest`.
+#
+# .. note::
+#
+#    :Pros: - Very fast computation time
+#           - Faster than current, state-of-the-art quadratic-time kernel-based tests
+#    :Cons: - Heuristic method, checking more frequencies will give more power.
 #
+# This test is also initialized with the ``num_randfreq`` parameter. This parameter can be
+# thought of as the degrees of freedom associated with the test and also dictates the number
+# of test points used in the test (see :class:`hyppo.ksample.SmoothCFTest`). If data
+# is kept constant, increasing the magnitude of this parameter will generally result in
+# larger magnitude test statistics while magnitude of the p-value will fluctuate:
+
+import numpy as np
+from hyppo.ksample import SmoothCFTest
+
+np.random.seed(1234)
+x = np.random.randn(500, 10)
+y = np.random.randn(500, 10)
+
+stat1, pvalue1 = SmoothCFTest(num_randfreq=5).test(x, y, random_state=1234)
+stat2, pvalue2 = SmoothCFTest(num_randfreq=10).test(x, y, random_state=1234)
+
+print("5 degrees of freedom (stat, pval):\n", stat1, pvalue1)
+print("10 degrees of freedom (stat, pval):\n", stat2, pvalue2)
+
+########################################################################################
+# Mean Embedding Test
+# --------------------------------------------
+#
+# The **Mean Embedding Test** is another non-parametric two-sample statistical test. This test
+# is based on analytic mean embeddings of data distributions in a reproducing kernel hilbert
+# space (RKHS). Hilbert spaces allow the representation of functions as points; thus, if
+# mean embeddings can be determined for two data distributions then the distance between
+# these two distributions in the hilbert space can be determined. In other words, the RKHS
+# allows the mapping of probability measures into a finite dimensional Euclidean space. More
+# details can be found at :class:`hyppo.ksample.MeanEmbeddingTest`.
+#
+# .. note::
+#
+#    :Pros: - Very fast computation time
+#           - Faster than current, state-of-the-art quadratic-time kernel-based tests
+#    :Cons: - Heuristic method, checking more frequencies will give more power.
+#
+# This test is also initialized with the ``num_randfreq`` parameter. This parameter can be
+# thought ofas the degrees of freedom associated with the test and also dictates the number
+# of test points used in the test (see :class:`hyppo.ksample.MeanEmbeddingTest`). If data
+# is kept constant, increasing the magnitude of this parameter will generally result in
+# larger magnitude test statistics while magnitude of the p-value will fluctuate:
+
+from hyppo.ksample import MeanEmbeddingTest
+
+np.random.seed(1234)
+x = np.random.randn(500, 10)
+y = np.random.randn(500, 10)
+
+stat1, pval1 = MeanEmbeddingTest(num_randfreq=5).test(x, y, random_state=1234)
+stat2, pval2 = MeanEmbeddingTest(num_randfreq=10).test(x, y, random_state=1234)
+
+print("5 degrees of freedom (stat, pval):\n", stat1, pval1)
+print("10 degrees of freedom (stat, pval):\n", stat2, pval2)
+
+########################################################################################
 # .. _[1]: https://link.springer.com/article/10.1007/s10182-020-00378-1
 # .. _[2]: https://arxiv.org/abs/1910.08883


Test patch:
diff --git a/hyppo/ksample/tests/test_mean_embedding.py b/hyppo/ksample/tests/test_mean_embedding.py
new file mode 100644
index 000000000..d8fba751f
--- /dev/null
+++ b/hyppo/ksample/tests/test_mean_embedding.py
@@ -0,0 +1,52 @@
+import numpy as np
+import pytest
+from numpy.testing import assert_almost_equal
+
+from hyppo.tools import rot_ksamp
+from hyppo.ksample import MeanEmbeddingTest
+
+
+class TestMeanEmbedding:
+    @pytest.mark.parametrize(
+        "n, obs_stat, obs_pvalue",
+        [
+            (200, 77.452, 2.862e-15),
+            (700, 245.478, 5.132e-51),
+        ],
+    )
+    def test_linear_oned(self, n, obs_stat, obs_pvalue):
+        np.random.seed(123456789)
+        x, y = rot_ksamp("linear", n, 1, k=2)
+        stat, pvalue = MeanEmbeddingTest().test(x, y, random_state=1234)
+        print(stat, pvalue)
+        assert_almost_equal(stat, obs_stat, decimal=2)
+        assert_almost_equal(pvalue, obs_pvalue, decimal=10)
+
+    def test_null(self):
+        np.random.seed(120)
+        x = np.random.randn(500, 10)
+        y = np.random.randn(500, 10)
+        _, pval = MeanEmbeddingTest().test(x, y)
+        assert pval > 0.05
+
+    def test_alternative(self):
+        np.random.seed(120)
+        x = np.random.randn(500, 10)
+        x[:, 1] *= 3
+        y = np.random.randn(500, 10)
+        _, pval = MeanEmbeddingTest().test(x, y)
+        assert pval < 0.05
+
+    @pytest.mark.parametrize(
+        "obs_stat, obs_pval",
+        [
+            (0.0164, 0.8980),
+        ],
+    )
+    def test_one_frequency(self, obs_stat, obs_pval):
+        np.random.seed(123456789)
+        x = np.random.randn(500, 10)
+        y = np.random.randn(500, 10)
+        stat, pvalue = MeanEmbeddingTest(num_randfreq=1).test(x, y, random_state=1234)
+        assert_almost_equal(stat, obs_stat, decimal=2)
+        assert_almost_equal(pvalue, obs_pval, decimal=2)
diff --git a/hyppo/ksample/tests/test_smoothCF.py b/hyppo/ksample/tests/test_smoothCF.py
new file mode 100644
index 000000000..72f1f3e54
--- /dev/null
+++ b/hyppo/ksample/tests/test_smoothCF.py
@@ -0,0 +1,52 @@
+import numpy as np
+import pytest
+from numpy.testing import assert_almost_equal
+
+from hyppo.tools import rot_ksamp
+from hyppo.ksample import SmoothCFTest
+
+
+class TestSmoothCF:
+    @pytest.mark.parametrize(
+        "n, obs_stat, obs_pvalue",
+        [
+            (2000, 1317.0159, 8.134e-277),
+            (1000, 546.279, 5.608e-111),
+        ],
+    )
+    def test_linear_oned(self, n, obs_stat, obs_pvalue):
+        np.random.seed(123456789)
+        x, y = rot_ksamp("linear", n, 1, k=2)
+        stat, pvalue = SmoothCFTest().test(x, y)
+
+        assert_almost_equal(stat, obs_stat, decimal=1)
+        assert_almost_equal(pvalue, obs_pvalue, decimal=100)
+
+    def test_null(self):
+        np.random.seed(120)
+        x = np.random.randn(500, 10)
+        y = np.random.randn(500, 10)
+        _, pval = SmoothCFTest().test(x, y)
+        assert pval > 0.05
+
+    def test_alternative(self):
+        np.random.seed(120)
+        x = np.random.randn(500, 10)
+        x[:, 1] *= 3
+        y = np.random.randn(500, 10)
+        _, pval = SmoothCFTest().test(x, y)
+        assert pval < 0.05
+
+    @pytest.mark.parametrize(
+        "obs_stat, obs_pval",
+        [
+            (0.2707, 0.8734),
+        ],
+    )
+    def test_one_frequency(self, obs_stat, obs_pval):
+        np.random.seed(123456789)
+        x = np.random.randn(500, 10)
+        y = np.random.randn(500, 10)
+        stat, pvalue = SmoothCFTest(num_randfreq=1).test(x, y, random_state=1234)
+        assert_almost_equal(stat, obs_stat, decimal=2)
+        assert_almost_equal(pvalue, obs_pval, decimal=2)