Signature Description

enum class decompose_type : unsigned char  {
    additive = 1,        // Yt = Trend + Seasonal + Residual
    multiplicative = 2,  // Yt = Trend * Seasonal * Residual
};
The additive decomposition is the most appropriate if the magnitude of the seasonal fluctuations, or the variation around the trend-cycle, does not vary with the level of the time series. When the variation in the seasonal pattern, or the variation around the trend-cycle, appears to be proportional to the level of the time series, then a multiplicative decomposition is more appropriate. Multiplicative decompositions are common with economic time series.
An alternative to using a multiplicative decomposition is to first transform the data until the variation in the series appears to be stable over time, then use an additive decomposition. When a log transformation has been used, this is equivalent to using a multiplicative decomposition because:
Yt = T * S * R is equivalent to log(Yt) = log(T) + logt(S) + log(R)

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

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

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

template<typename T, typename I = unsigned long>
using decom_v = DecomposeVisitor<T, I>;
        
This is a “single action visitor”, meaning it is passed the whole data vector in one call and you must use the single_act_visit() interface.

This visitor creates a seasonal-trend (with Lowess, aka "STL") decomposition of observed time series data. This implementation is modeled after the statsmodels.tsa.seasonal_decompose method but substitutes a Lowess regression for a convolution in its trend estimation.
get_result() returns the Trend vector. There are 3 get result methods: get_trend(), get_seasonal(), get_residual(). They return the corresponding vectors
    explicit
    DecomposeVisitor(std::size_t s_period,
                     T frac,
                     T delta,
                     decompose_type type = decompose_type::additive);
        
s_period: Length of a season in untis of one observation. There must be at least two seasons in the data
frac: Between 0 and 1. The fraction of the data used when estimating each y-value.
delta: Distance within which to use linear-interpolation instead of weighted regression.
type: Model type. See above.
T: Column data type.
I: Index type.
static void test_DecomposeVisitor()  {

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

    std::vector<double>         y_vec =
        { 131.157, 131.367, 132.215, 132.725, 132.648, 130.585, 130.701, 129.631, 129.168, 129.554, 129.467, 129.670, 128.397, 129.014,
          129.496, 131.067, 130.219, 128.947, 129.602, 128.118, 127.356, 127.231, 127.154, 128.417, 129.091, 129.082, 128.937, 130.441,
          129.371, 129.294, 129.381, 129.564, 129.708, 130.701, 130.663, 130.113, 130.046, 130.393, 128.026, 129.204, 130.530, 129.499,
          129.266, 129.357, 130.431, 131.810, 131.761, 131.675, 130.923, 131.694, 133.005, 133.323, 134.152, 138.702, 137.719, 135.492,
          133.622, 134.518, 132.725, 131.839, 138.548, 140.996, 143.734, 150.693, 151.108, 149.423, 150.416, 149.491, 151.273, 150.299,
          146.783, 147.173, 146.939, 147.290, 145.946, 142.624, 138.027, 136.118, 129.650, 126.767, 130.809, 125.550, 130.732, 126.183,
          124.410, 114.748, 121.527, 114.904, 100.138, 105.144,
    };
    MyDataFrame                 df;

    df.load_data(std::move(MyDataFrame::gen_sequence_index(0, y_vec.size(), 1)),
                 std::make_pair("IBM_closes", y_vec));

    DecomposeVisitor<double>    d_v (7);

    df.single_act_visit<double>("IBM_closes", d_v);

    auto    actual_trends = std::vector<double>
        { 130.613, 130.55, 130.489, 130.43, 130.372, 130.317, 130.263, 130.211, 130.161, 130.111, 130.064, 130.017, 129.972, 129.928, 129.885,
          129.842, 129.801, 129.76, 129.72, 129.681, 129.642, 129.603, 129.564, 129.526, 129.49, 129.458, 129.435, 129.459, 129.496, 129.546, 129.61,
          129.688, 129.78, 129.885, 130.002, 130.129, 130.267, 130.414, 130.568, 130.73, 130.898, 131.071, 131.248, 131.429, 131.613,
          131.801, 131.994, 132.193, 132.4, 132.618, 132.847, 133.089, 133.343, 133.594, 133.814, 133.958, 133.982, 133.867, 133.633, 133.329,
          133.013, 132.715, 132.449, 132.214, 131.88, 131.594, 131.336, 131.094, 130.862, 130.636, 130.412, 130.191, 129.97, 129.749,
          129.527, 129.305, 129.082, 128.858, 128.633, 128.406, 128.178, 127.949, 127.717, 127.484, 127.249, 127.012, 126.773, 126.531,
          126.288, 126.043
        };

    for (size_t idx = 0; idx < actual_trends.size(); ++idx)
        assert(fabs(d_v.get_trend()[idx] - actual_trends[idx]) < 0.001);

    auto    actual_seasonals = std::vector<double>
        { 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313,
          -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401,
          -0.138991, -0.774313, -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993, 0.499135,
          -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695,
          0.951993, 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401, -0.138991,
          -0.774313, -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993, 0.499135, -0.362488,
          -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993, 0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695, 0.951993,
          0.499135, -0.362488, -0.0226401, -0.138991, -0.774313, -0.152695
        };

    for (size_t idx = 0; idx < actual_seasonals.size(); ++idx)
        assert(fabs(d_v.get_seasonal()[idx] - actual_seasonals[idx]) < 0.00001);

    auto    actual_residuals = std::vector<double>
        { 0.0450645, 1.17948, 1.74866, 2.43421, 3.04989, 0.420796, -0.514129, -1.07918, -0.630027, -0.534809, -0.457752, 0.427013, -1.42233,
          -1.86582, -0.887751, 1.58716, 0.440736, -0.674248, 0.656095, -1.41002, -3.2376, -2.87093, -2.04782, -1.08684, -0.260342, 0.397977,
          -0.345524, 0.0298508, -0.623855, 0.110697, -0.206247, 0.014974, 0.702412, 0.968872, -0.290624, -0.515527, 0.141332, 0.0017837,
          -2.40348, -0.751754, -0.215182, -2.52399, -2.48155, -1.7098, -1.15976, 0.147774, 0.541487, -0.36517, -2.4292, -1.42281, 0.520609,
          0.256637, 0.94848, 5.88205, 4.05781, 0.58219, -0.858726, 1.01365, -0.88524, -1.35148, 6.30971, 8.4335, 10.3326, 17.98, 19.5905,
          17.8516, 19.2189, 19.171, 20.5634, 18.7112, 15.8714, 17.3448, 16.992, 17.6804, 17.1932, 13.4718, 7.99313, 6.76106, 1.3799,
          -1.61643, 2.7699, -1.62422, 3.16745, -2.25312, -3.33817, -11.9014, -5.22295, -11.4883, -25.3757, -20.7463
        };

    for (size_t idx = 0; idx < actual_residuals.size(); ++idx)
        assert(fabs(d_v.get_residual()[idx] - actual_residuals[idx]) < 0.0001);
}
// -----------------------------------------------------------------------------

static void test_IBM_data()  {

    std::cout << "\nTesting IBM_data(  ) ..." << std::endl;

    typedef StdDataFrame<std::string> StrDataFrame;

    StrDataFrame    df;

    df.read("IBM.csv", io_format::csv2);

    DecomposeVisitor<double, std::string> d_v(178, 2.0 / 3.0, 0, decompose_type::multiplicative);
    // decompose_type::additive);

    df.single_act_visit<double>("IBM_Adj_Close", d_v);

    const auto  &ibm_closes = df.get_column<double>("IBM_Adj_Close");

    for (std::size_t i = 0; i < ibm_closes.size(); ++i)
        std::cout << ibm_closes[i] << ","
                  << d_v.get_trend()[i] << ","
                  << d_v.get_seasonal()[i] << ","
                  << d_v.get_residual()[i] << std::endl;
}
C++ DataFrame