diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 7a3258e..35b3495 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -4,7 +4,8 @@ "name": "CMake deps", "includePath": [ "${workspaceFolder}/include", - "${workspaceFolder}/build/_deps/**/include" + "${workspaceFolder}/build/_deps/**/include", + "${workspaceFolder}/build/_deps/**/cpp", ] } ], diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d05bc1..b147973 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,15 +9,17 @@ if(NOT CMAKE_BUILD_TYPE) endif() set(CMAKE_CXX_STANDARD 17) + if(USE_OPENMP) if(APPLE) find_package(OpenMP) + if(NOT OpenMP_FOUND) # libomp 15.0+ from brew is keg-only, so have to search in other locations. # See https://github.com/Homebrew/homebrew-core/issues/112107#issuecomment-1278042927. execute_process(COMMAND brew --prefix libomp - OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX - OUTPUT_STRIP_TRAILING_WHITESPACE) + OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE) set(OpenMP_C_FLAGS "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") set(OpenMP_C_LIB_NAMES omp) @@ -25,6 +27,7 @@ if(USE_OPENMP) set(OpenMP_omp_LIBRARY ${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib) endif() endif() + find_package(OpenMP REQUIRED) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") endif() @@ -36,14 +39,16 @@ endif() if(UNIX) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fPIC -O0 -g -Wall -Wextra -Wpedantic") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fPIC -O3 -Wall -Wextra -Wpedantic") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-pragmas") else() set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2 /Ob2 /Ot /Oy /W4") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4068") endif() if(SKBUILD) set(LIBRARY_OUTPUT_PATH ${SKBUILD_PLATLIB_DIR}/coreforecast/lib) else() - set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/coreforecast/lib) + set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/python/coreforecast/lib) endif() include_directories(include) @@ -52,10 +57,28 @@ FetchContent_Declare( GIT_REPOSITORY https://github.com/jmoralez/stl-cpp.git GIT_TAG 13d26c0d0653ddcdbf853de3f92f56faa831a330 ) -FetchContent_MakeAvailable(stl-cpp) -include_directories(${stl-cpp_SOURCE_DIR}/include) -file(GLOB SOURCES src/*.cpp) +FetchContent_GetProperties(stl-cpp) + +if(NOT stl-cpp_POPULATED) + FetchContent_Populate(stl-cpp) + include_directories(${stl-cpp_SOURCE_DIR}/include) +endif() + +FetchContent_Declare( + skiplist + GIT_REPOSITORY https://github.com/paulross/skiplist.git + GIT_TAG aace571a8564067820ff7a5e34ba68d0a9782153 +) +FetchContent_GetProperties(skiplist) + +if(NOT skiplist_POPULATED) + FetchContent_Populate(skiplist) + include_directories(${skiplist_SOURCE_DIR}/src/cpp) +endif() + +file(GLOB SOURCES src/*.cpp ${skiplist_SOURCE_DIR}/src/cpp/SkipList.cpp) add_library(coreforecast SHARED ${SOURCES}) + if(MSVC) set_target_properties(coreforecast PROPERTIES OUTPUT_NAME "libcoreforecast") endif() diff --git a/include/rolling.h b/include/rolling.h index ec39728..bc8aefe 100644 --- a/include/rolling.h +++ b/include/rolling.h @@ -1,5 +1,7 @@ #pragma once +#include "SkipList.h" + #include "grouped_array.h" #include "stats.h" @@ -111,56 +113,20 @@ template inline void RollingQuantileTransform(const T *data, int n, T *out, int window_size, int min_samples, T p) { int upper_limit = std::min(window_size, n); - T *buffer = new T[upper_limit]; - int *positions = new int[upper_limit]; - min_samples = std::min(min_samples, upper_limit); - for (int i = 0; i < min_samples - 1; ++i) { - buffer[i] = data[i]; - positions[i] = i; - out[i] = std::numeric_limits::quiet_NaN(); - } - if (min_samples > 2) { - std::sort(buffer, buffer + min_samples - 2); - } - for (int i = min_samples - 1; i < upper_limit; ++i) { - int idx = std::lower_bound(buffer, buffer + i, data[i]) - buffer; - for (int j = 0; j < i - idx; ++j) { - buffer[i - j] = buffer[i - j - 1]; - positions[i - j] = positions[i - j - 1]; + OrderedStructs::SkipList::HeadNode sl; + for (int i = 0; i < upper_limit; ++i) { + sl.insert(data[i]); + if (i + 1 < min_samples) { + out[i] = std::numeric_limits::quiet_NaN(); + } else { + out[i] = SortedQuantile(sl, p, i + 1); } - buffer[idx] = data[i]; - positions[idx] = i; - out[i] = SortedQuantile(buffer, p, i + 1); } for (int i = window_size; i < n; ++i) { - int remove_idx = - std::min_element(positions, positions + window_size) - positions; - int idx; - if (data[i] <= buffer[remove_idx]) { - idx = std::lower_bound(buffer, buffer + remove_idx, data[i]) - buffer; - for (int j = 0; j < remove_idx - idx; ++j) { - buffer[remove_idx - j] = buffer[remove_idx - j - 1]; - positions[remove_idx - j] = positions[remove_idx - j - 1]; - } - } else { - idx = (std::lower_bound(buffer + remove_idx - 1, buffer + window_size, - data[i]) - - buffer) - - 1; - if (idx == window_size) { - --idx; - } - for (int j = 0; j < idx - remove_idx; ++j) { - buffer[remove_idx + j] = buffer[remove_idx + j + 1]; - positions[remove_idx + j] = positions[remove_idx + j + 1]; - } - } - buffer[idx] = data[i]; - positions[idx] = i; - out[i] = SortedQuantile(buffer, p, window_size); + sl.remove(data[i - window_size]); + sl.insert(data[i]); + out[i] = SortedQuantile(sl, p, window_size); } - delete[] buffer; - delete[] positions; } template diff --git a/include/stats.h b/include/stats.h index 3e2f276..3ff6b08 100644 --- a/include/stats.h +++ b/include/stats.h @@ -1,5 +1,7 @@ #pragma once +#include "SkipList.h" + #include #include @@ -10,19 +12,21 @@ template inline T Quantile(T *data, T p, int n) { std::nth_element(data, data + i, data + n); T out = data[i]; if (g > 0.0) { - std::nth_element(data, data + i + 1, data + n); - out += g * (data[i + 1] - out); + auto it = std::min_element(data + i + 1, data + n); + out += g * (*it - out); } return out; } -template inline T SortedQuantile(T *data, T p, int n) { +template +inline T SortedQuantile(OrderedStructs::SkipList::HeadNode &data, T p, + int n) { T i_plus_g = p * (n - 1); int i = static_cast(i_plus_g); T g = i_plus_g - i; - T out = data[i]; + T out = data.at(i); if (g > 0.0) { - out += g * (data[i + 1] - out); + out += g * (data.at(i + 1) - out); } return out; } diff --git a/pyproject.toml b/pyproject.toml index 7ce9f84..8d152f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] description = "Fast implementations of common forecasting routines" -authors = [ - {name = "José Morales", email = "jmoralz92@gmail.com"}, -] +authors = [{ name = "José Morales", email = "jmoralz92@gmail.com" }] readme = "README.md" keywords = ["forecasting", "time-series"] @@ -39,15 +37,11 @@ logging.level = "INFO" sdist.exclude = ["tests", "*.yml"] sdist.reproducible = true wheel.install-dir = "coreforecast" -wheel.packages = ["coreforecast"] +wheel.packages = ["python/coreforecast"] wheel.py-api = "py3" [tool.cibuildwheel] archs = "all" build-verbosity = 3 -macos.before-build = [ - "brew install libomp", - "./scripts/switch_xcode", -] -test-requires = "pandas pytest window-ops" -test-command = "pytest {project}/tests -k correct" +macos.before-build = ["brew install libomp", "./scripts/switch_xcode"] +test-command = 'python -c "import coreforecast._lib"' diff --git a/coreforecast/__init__.py b/python/coreforecast/__init__.py similarity index 100% rename from coreforecast/__init__.py rename to python/coreforecast/__init__.py diff --git a/coreforecast/_lib.py b/python/coreforecast/_lib.py similarity index 100% rename from coreforecast/_lib.py rename to python/coreforecast/_lib.py diff --git a/coreforecast/differences.py b/python/coreforecast/differences.py similarity index 100% rename from coreforecast/differences.py rename to python/coreforecast/differences.py diff --git a/python/coreforecast/expanding.py b/python/coreforecast/expanding.py new file mode 100644 index 0000000..f78a922 --- /dev/null +++ b/python/coreforecast/expanding.py @@ -0,0 +1,91 @@ +__all__ = [ + "expanding_mean", + "expanding_std", + "expanding_min", + "expanding_max", + "expanding_quantile", +] + +import ctypes +from typing import Callable + +import numpy as np + +from ._lib import _LIB +from .utils import ( + _data_as_void_ptr, + _ensure_float, + _float_arr_to_prefix, + _pyfloat_to_np_c, +) + + +def _expanding_stat(x: np.ndarray, stat: str) -> np.ndarray: + x = _ensure_float(x) + prefix = _float_arr_to_prefix(x) + out = np.empty_like(x) + _LIB[f"{prefix}_Expanding{stat}Transform"]( + _data_as_void_ptr(x), + ctypes.c_int(x.size), + _data_as_void_ptr(out), + ) + return out + + +def _expanding_docstring(*args, **kwargs) -> Callable: + base_docstring = """Compute the {} of the input array. + + Args: + x (np.ndarray): Input array. + + Returns: + np.ndarray: Array with the expanding statistic + """ + + def docstring_decorator(function: Callable): + function.__doc__ = base_docstring.format(function.__name__) + return function + + return docstring_decorator(*args, **kwargs) + + +@_expanding_docstring +def expanding_mean(x: np.ndarray) -> np.ndarray: + return _expanding_stat(x, "Mean") + + +@_expanding_docstring +def expanding_std(x: np.ndarray) -> np.ndarray: + return _expanding_stat(x, "Std") + + +@_expanding_docstring +def expanding_min(x: np.ndarray) -> np.ndarray: + return _expanding_stat(x, "Min") + + +@_expanding_docstring +def expanding_max(x: np.ndarray) -> np.ndarray: + return _expanding_stat(x, "Max") + + +def expanding_quantile(x: np.ndarray, p: float) -> np.ndarray: + """Compute the expanding_quantile of the input array. + + Args: + x (np.ndarray): Input array. + p (float): Quantile to compute. + + Returns: + np.ndarray: Array with the expanding statistic + """ + x = _ensure_float(x) + prefix = _float_arr_to_prefix(x) + out = np.empty_like(x) + _LIB[f"{prefix}_ExpandingQuantileTransform"]( + _data_as_void_ptr(x), + ctypes.c_int(x.size), + _pyfloat_to_np_c(p, x.dtype), + _data_as_void_ptr(out), + ) + return out diff --git a/python/coreforecast/exponentially_weighted.py b/python/coreforecast/exponentially_weighted.py new file mode 100644 index 0000000..600dd89 --- /dev/null +++ b/python/coreforecast/exponentially_weighted.py @@ -0,0 +1,35 @@ +__all__ = ["exponentially_weighted_mean"] + +import ctypes + +import numpy as np + +from ._lib import _LIB +from .utils import ( + _ensure_float, + _float_arr_to_prefix, + _data_as_void_ptr, + _pyfloat_to_np_c, +) + + +def exponentially_weighted_mean(x: np.ndarray, alpha: float) -> np.ndarray: + """Compute the exponentially weighted mean of the input array. + + Args: + x (np.ndarray): Input array. + alpha (float): Weight parameter. + + Returns: + np.ndarray: Array with the exponentially weighted mean. + """ + x = _ensure_float(x) + prefix = _float_arr_to_prefix(x) + out = np.empty_like(x) + _LIB[f"{prefix}_ExponentiallyWeightedMeanTransform"]( + _data_as_void_ptr(x), + ctypes.c_int(x.size), + _pyfloat_to_np_c(alpha, x.dtype), + _data_as_void_ptr(out), + ) + return out diff --git a/coreforecast/grouped_array.py b/python/coreforecast/grouped_array.py similarity index 100% rename from coreforecast/grouped_array.py rename to python/coreforecast/grouped_array.py diff --git a/coreforecast/lag_transforms.py b/python/coreforecast/lag_transforms.py similarity index 100% rename from coreforecast/lag_transforms.py rename to python/coreforecast/lag_transforms.py diff --git a/python/coreforecast/rolling.py b/python/coreforecast/rolling.py new file mode 100644 index 0000000..64f020e --- /dev/null +++ b/python/coreforecast/rolling.py @@ -0,0 +1,246 @@ +__all__ = [ + "rolling_mean", + "rolling_std", + "rolling_min", + "rolling_max", + "rolling_quantile", + "seasonal_rolling_mean", + "seasonal_rolling_std", + "seasonal_rolling_min", + "seasonal_rolling_max", + "seasonal_rolling_quantile", +] + +import ctypes +from typing import Callable, Optional + +import numpy as np + +from ._lib import _LIB +from .utils import ( + _data_as_void_ptr, + _ensure_float, + _float_arr_to_prefix, + _pyfloat_to_np_c, +) + + +def _rolling_stat( + x: np.ndarray, + stat: str, + window_size: int, + min_samples: Optional[int] = None, +) -> np.ndarray: + x = _ensure_float(x) + prefix = _float_arr_to_prefix(x) + out = np.empty_like(x) + if min_samples is None: + min_samples = window_size + _LIB[f"{prefix}_Rolling{stat}Transform"]( + _data_as_void_ptr(x), + ctypes.c_int(x.size), + ctypes.c_int(window_size), + ctypes.c_int(min_samples), + _data_as_void_ptr(out), + ) + return out + + +def _seasonal_rolling_stat( + x: np.ndarray, + stat: str, + season_length: int, + window_size: int, + min_samples: Optional[int] = None, +) -> np.ndarray: + x = _ensure_float(x) + prefix = _float_arr_to_prefix(x) + out = np.empty_like(x) + if min_samples is None: + min_samples = window_size + _LIB[f"{prefix}_SeasonalRolling{stat}Transform"]( + _data_as_void_ptr(x), + ctypes.c_int(x.size), + ctypes.c_int(season_length), + ctypes.c_int(window_size), + ctypes.c_int(min_samples), + _data_as_void_ptr(out), + ) + return out + + +def _rolling_docstring(*args, **kwargs) -> Callable: + base_docstring = """Compute the {} of the input array. + + Args: + x (np.ndarray): Input array. + window_size (int): The size of the rolling window. + min_samples (int, optional): The minimum number of samples required to compute the statistic. + If None, it is set to `window_size`. + + Returns: + np.ndarray: Array with the rolling statistic + """ + + def docstring_decorator(function: Callable): + function.__doc__ = base_docstring.format(function.__name__) + return function + + return docstring_decorator(*args, **kwargs) + + +def _seasonal_rolling_docstring(*args, **kwargs) -> Callable: + base_docstring = """Compute the {} of the input array + + Args: + x (np.ndarray): Input array. + season_length (int): The length of the seasonal period. + window_size (int): The size of the rolling window. + min_samples (int, optional): The minimum number of samples required to compute the statistic. + If None, it is set to `window_size`. + + Returns: + np.ndarray: Array with the seasonal rolling statistic + """ + + def docstring_decorator(function: Callable): + function.__doc__ = base_docstring.format(function.__name__) + return function + + return docstring_decorator(*args, **kwargs) + + +@_rolling_docstring +def rolling_mean( + x: np.ndarray, window_size: int, min_samples: Optional[int] = None +) -> np.ndarray: + return _rolling_stat(x, "Mean", window_size, min_samples) + + +@_rolling_docstring +def rolling_std( + x: np.ndarray, window_size: int, min_samples: Optional[int] = None +) -> np.ndarray: + return _rolling_stat(x, "Std", window_size, min_samples) + + +@_rolling_docstring +def rolling_min( + x: np.ndarray, window_size: int, min_samples: Optional[int] = None +) -> np.ndarray: + return _rolling_stat(x, "Min", window_size, min_samples) + + +@_rolling_docstring +def rolling_max( + x: np.ndarray, window_size: int, min_samples: Optional[int] = None +) -> np.ndarray: + return _rolling_stat(x, "Max", window_size, min_samples) + + +def rolling_quantile( + x: np.ndarray, p: float, window_size: int, min_samples: Optional[int] = None +) -> np.ndarray: + """Compute the rolling_quantile of the input array. + + Args: + x (np.ndarray): Input array. + q (float): Quantile to compute. + window_size (int): The size of the rolling window. + min_samples (int, optional): The minimum number of samples required to compute the statistic. + If None, it is set to `window_size`. + + Returns: + np.ndarray: Array with rolling statistic + """ + x = _ensure_float(x) + prefix = _float_arr_to_prefix(x) + out = np.empty_like(x) + if min_samples is None: + min_samples = window_size + _LIB[f"{prefix}_RollingQuantileTransform"]( + _data_as_void_ptr(x), + ctypes.c_int(x.size), + _pyfloat_to_np_c(p, x.dtype), + ctypes.c_int(window_size), + ctypes.c_int(min_samples), + _data_as_void_ptr(out), + ) + return out + + +@_seasonal_rolling_docstring +def seasonal_rolling_mean( + x: np.ndarray, + season_length: int, + window_size: int, + min_samples: Optional[int] = None, +) -> np.ndarray: + return _seasonal_rolling_stat(x, "Mean", season_length, window_size, min_samples) + + +@_seasonal_rolling_docstring +def seasonal_rolling_std( + x: np.ndarray, + season_length: int, + window_size: int, + min_samples: Optional[int] = None, +) -> np.ndarray: + return _seasonal_rolling_stat(x, "Std", season_length, window_size, min_samples) + + +@_seasonal_rolling_docstring +def seasonal_rolling_min( + x: np.ndarray, + season_length: int, + window_size: int, + min_samples: Optional[int] = None, +) -> np.ndarray: + return _seasonal_rolling_stat(x, "Min", season_length, window_size, min_samples) + + +@_seasonal_rolling_docstring +def seasonal_rolling_max( + x: np.ndarray, + season_length: int, + window_size: int, + min_samples: Optional[int] = None, +) -> np.ndarray: + return _seasonal_rolling_stat(x, "Max", season_length, window_size, min_samples) + + +def seasonal_rolling_quantile( + x: np.ndarray, + p: float, + season_length: int, + window_size: int, + min_samples: Optional[int] = None, +) -> np.ndarray: + """Compute the seasonal_rolling_quantile of the input array. + + Args: + x (np.ndarray): Input array. + q (float): Quantile to compute. + season_length (int): The length of the seasonal period. + window_size (int): The size of the rolling window. + min_samples (int, optional): The minimum number of samples required to compute the statistic. + If None, it is set to `window_size`. + + Returns: + np.ndarray: Array with rolling statistic + """ + x = _ensure_float(x) + prefix = _float_arr_to_prefix(x) + out = np.empty_like(x) + if min_samples is None: + min_samples = window_size + _LIB[f"{prefix}_SeasonalRollingQuantileTransform"]( + _data_as_void_ptr(x), + ctypes.c_int(x.size), + ctypes.c_int(season_length), + _pyfloat_to_np_c(p, x.dtype), + ctypes.c_int(window_size), + ctypes.c_int(min_samples), + _data_as_void_ptr(out), + ) + return out diff --git a/coreforecast/scalers.py b/python/coreforecast/scalers.py similarity index 100% rename from coreforecast/scalers.py rename to python/coreforecast/scalers.py diff --git a/coreforecast/seasonal.py b/python/coreforecast/seasonal.py similarity index 100% rename from coreforecast/seasonal.py rename to python/coreforecast/seasonal.py diff --git a/coreforecast/utils.py b/python/coreforecast/utils.py similarity index 100% rename from coreforecast/utils.py rename to python/coreforecast/utils.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_expanding.py b/tests/test_expanding.py new file mode 100644 index 0000000..47b829c --- /dev/null +++ b/tests/test_expanding.py @@ -0,0 +1,27 @@ +import numpy as np +import pytest +import window_ops.expanding as wops_expanding + +import coreforecast.expanding as cf_expanding +from .test_lag_transforms import pd_rolling_quantile, pd_seasonal_rolling_quantile + +quantile_ops = ["expanding_quantile"] +other_ops = [op for op in cf_expanding.__all__ if op not in quantile_ops] + + +@pytest.mark.parametrize("op", other_ops) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_rolling(op, dtype): + x = np.random.rand(100).astype(dtype) + cf_res = getattr(cf_expanding, op)(x) + wo_res = getattr(wops_expanding, op)(x) + np.testing.assert_allclose(cf_res, wo_res, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("p", [0.1, 0.5, 0.9]) +def test_quantiles(dtype, p): + x = np.random.rand(100).astype(dtype) + pd_res = pd_rolling_quantile(x, 0, p, x.size, 1) + cf_res = cf_expanding.expanding_quantile(x, p) + np.testing.assert_allclose(pd_res, cf_res, atol=1e-5) diff --git a/tests/test_exponentially_weighted.py b/tests/test_exponentially_weighted.py new file mode 100644 index 0000000..0ae660f --- /dev/null +++ b/tests/test_exponentially_weighted.py @@ -0,0 +1,14 @@ +import numpy as np +import pytest +from window_ops.ewm import ewm_mean + +from coreforecast.exponentially_weighted import exponentially_weighted_mean + + +@pytest.mark.parametrize("alpha", [0.1, 0.5, 0.9]) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_exponentially_weighted_mean(alpha, dtype): + x = np.random.rand(100).astype(dtype) + cf_res = exponentially_weighted_mean(x, alpha) + wo_res = ewm_mean(x, alpha) + np.testing.assert_allclose(cf_res, wo_res, rtol=1e-5) diff --git a/tests/test_lag_transforms.py b/tests/test_lag_transforms.py index 5fc49c1..3595e46 100644 --- a/tests/test_lag_transforms.py +++ b/tests/test_lag_transforms.py @@ -15,7 +15,6 @@ min_samples = 2 lengths = np.random.randint(low=100, high=200, size=100) indptr = np.append(0, lengths.cumsum()).astype(np.int32) -data = 10 * np.random.rand(indptr[-1]) def transform(data, indptr, updates_only, lag, func, *args) -> np.ndarray: diff --git a/tests/test_rolling.py b/tests/test_rolling.py new file mode 100644 index 0000000..7b8c390 --- /dev/null +++ b/tests/test_rolling.py @@ -0,0 +1,46 @@ +import numpy as np +import pytest +import window_ops.rolling as wops_rolling + +import coreforecast.rolling as cf_rolling +from .test_lag_transforms import pd_rolling_quantile, pd_seasonal_rolling_quantile + +quantile_ops = ["rolling_quantile", "seasonal_rolling_quantile"] +other_ops = [op for op in cf_rolling.__all__ if op not in quantile_ops] + + +@pytest.mark.parametrize("op", other_ops) +@pytest.mark.parametrize("min_samples", [None, 5]) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_rolling(op, min_samples, dtype): + window_size = 10 + season_length = 4 + x = np.random.rand(100).astype(dtype) + if op.startswith("seasonal"): + args = (season_length, window_size, min_samples) + else: + args = (window_size, min_samples) + cf_res = getattr(cf_rolling, op)(x, *args) + wo_res = getattr(wops_rolling, op)(x, *args) + np.testing.assert_allclose(cf_res, wo_res, rtol=1e-5) + + +@pytest.mark.parametrize("op", quantile_ops) +@pytest.mark.parametrize("min_samples", [None, 5]) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("p", [0.1, 0.5, 0.9]) +def test_quantiles(op, min_samples, dtype, p): + window_size = 10 + season_length = 4 + x = np.random.rand(100).astype(dtype) + if op == "seasonal_rolling_quantile": + pd_res = pd_seasonal_rolling_quantile( + x, 0, p, season_length, window_size, min_samples + ) + cf_res = cf_rolling.seasonal_rolling_quantile( + x, p, season_length, window_size, min_samples + ) + else: + pd_res = pd_rolling_quantile(x, 0, p, window_size, min_samples) + cf_res = cf_rolling.rolling_quantile(x, p, window_size, min_samples) + np.testing.assert_allclose(pd_res, cf_res, atol=1e-5)