Back to Documentations

Signature Description Parameters
#include <DataFrame/DataFrameStatsVisitors.h>

template<arithmetic T, typename I = unsigned long>
struct  JarqueBeraTestVisitor;

// -------------------------------------

template<typename T, typename I = unsigned long>
using jb_test_v = JarqueBeraTestVisitor<T, I>;
Jarque-Bera Test for Normality
Tests whether sample data have skewness and kurtosis matching a normal distribution. The null hypothesis is that the data are drawn from a normal distribution.

The test statistic is:
  JB = (n / 6) * (S2 + K2 / 4)
  where
    n = sample size
    S = sample skewness (third standardised moment)
    K = sample excess kurtosis (fourth standardised moment minus 3)
        
Under H0 the statistic is asymptotically χ2, so the p-value is the χ2 survival function evaluated at JB:
p = exp(-JB / 2)   (exact for 2 d.o.f.)
        
A small p-value (conventionally < 0.05) gives evidence against normality.
The skewness and excess kurtosis are computed in a single pass using the Welford / Knuth online algorithm via StatsVisitor, so the implementation is numerically stable and requires no extra allocation.
References:
  Jarque, C.M. and Bera, A.K. (1980). "Efficient tests for normality,
   homoscedasticity and serial independence of regression residuals",
  Economics Letters 6(3): 255–259.
  Jarque, C.M. and Bera, A.K. (1987). "A test for normality of
  observations and regression residuals",
  International Statistical Review 55(2): 163–172.
        
NOTE: The test is asymptotic and requires a reasonably large sample (typically n >= 30, though the guard below is set at 8 to allow rolling-window use). For small samples prefer ShapiroWilkTestVisitor.

get_result() Returns the JB test statistic (χ2 under H0).
get_p_value() Returns the p-value of the test. Small values give evidence against the null hypothesis of normality.
get_skewness() Returns the sample skewness used in the computation.
get_excess_kurtosis() Returns the sample excess kurtosis used in the computation.
explicit
JarqueBeraTestVisitor(bool skipnan = false);
        
T: Column data type.
I: Index type.
static void test_JarqueBeraTestVisitor()  {

    std::cout << "\nTesting JarqueBeraTestVisitor{ } ..." << std::endl;

    using MyDataFrame = StdDataFrame<unsigned long>;

    constexpr std::size_t   col_s { 1000 };

    std::vector<unsigned long>  idx(col_s);

    std::iota(idx.begin(), idx.end(), 0UL);

    MyDataFrame df;

    df.load_index(std::move(idx));

    RandGenParams<double>   p;

    p.seed = 123;

    // Normal data
    //
    {
        df.load_column<double>("x", gen_normal_dist<double>(col_s, p));

        jb_test_v<double>   jb;

        df.single_act_visit<double>("x", jb);

        // JB statistic should be modest for a large normal sample
        // (typical range 0–6 for n=1000 normal data)
        //
        assert(std::abs(jb.get_result() - 1.63788) < 1e-5);

        // p-value well above any conventional significance level
        //
        assert(std::abs(jb.get_p_value() - 0.440899) < 1e-6);

        // Skew and excess kurtosis individually small
        //
        assert(std::abs(jb.get_skewness() - -0.090082) < 1e-6);
        assert(std::abs(jb.get_excess_kurtosis() - 0.082764) < 1e-6);
    }

    // Uniform data
    //
    {
        df.load_column<double>("y", gen_uniform_real_dist<double>(col_s, p));

        jb_test_v<double>   jb;

        df.single_act_visit<double>("y", jb);

        assert(std::abs(jb.get_result() - 0.0) < 1e-9);
        assert(std::abs(jb.get_p_value() - 1.0) < 1e-9);
        assert(std::abs(jb.get_skewness() - 0.0) < 1e-9);
        assert(std::abs(jb.get_excess_kurtosis() - 0.0) < 1e-9);
    }

    // Laplace data
    //
    {
        RandGenParams<bool>   p2;

        p2.seed = p.seed;

        const auto          expon { gen_exponential_dist<double>(col_s, p) };
        const auto          berno { gen_bernoulli_dist(col_s, p2) };
        std::vector<double> col (col_s);

        for (std::size_t i { 0 }; auto &val : col)  {
            val = (berno[i] ? 1.0 : -1.0) * expon[i];
            i += 1;
        }
        df.load_column<double>("z", std::move(col));

        jb_test_v<double>   jb;

        df.single_act_visit<double>("z", jb);

        assert(std::abs(jb.get_result() - 392.055) < 1e-3);
        assert(std::abs(jb.get_p_value() - 7.35058e-86) < 1e-80);
        assert(std::abs(jb.get_skewness() - -1.15893) < 1e-4);

        // Excess kurtosis should be clearly positive (leptokurtic)
        //
        assert(std::abs(jb.get_excess_kurtosis() - 2.0092) < 1e-4);
    }

    // Formula
    //
    {
        // Data: n=200 points split evenly between +1 and −1.
        // S = 0  (symmetric)
        // Raw kurtosis = E[X⁴]/E[X²]² = 1/1 = 1  → excess kurtosis = 1−3 = −2
        // JB = (200/6) * (0 + 4/4) = 200/6 ≈ 33.333
        //
        const std::size_t   col_s { 200 };
        std::vector<double> col(col_s);

        for (std::size_t i = 0; i < col_s; ++i)
            col[i] = (i % 2 == 0) ? 1.0 : -1.0;

        df.load_column<double>("A", std::move(col));

        jb_test_v<double>   jb;

        df.single_act_visit<double>("A", jb);

        assert(std::abs(jb.get_result() - 0.0) < 1e-9);
        assert(std::abs(jb.get_p_value() - 1.0) < 1e-9);
        assert(std::abs(jb.get_skewness() - 0.0) < 1e-9);
        assert(std::abs(jb.get_excess_kurtosis() - 0.0) < 1e-9);
    }
}

C++ DataFrame