From 3c4a90e592fd71fc0f3925cfdb95a897226c15c5 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 07:58:12 -0700 Subject: [PATCH 1/4] Fix downsample_median returning the mean for even periods downsample_median short circuits to downsample_mean whenever the period is even, so perform_downsampling with AggOperations::MEDIAN is bit for bit identical to AggOperations::MEAN for every even period. That defeats the outlier rejection which is the reason to pick a median filter in the first place. A single large spike inside a window is passed straight through, divided by the period, instead of being discarded. The library already contains the right answer. RollingMedian in rolling_filter.h, which backs perform_rolling_filter, averages the two middle values for even window lengths. The two median paths disagreed with each other on identical input. This makes downsample_median use the same convention: sort first, then average the two middle values when the length is even. For a period of 2 the correct even median happens to equal the mean, so behaviour there is unchanged. Odd periods are unchanged as well. --- src/data_handler/inc/downsample_operators.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/data_handler/inc/downsample_operators.h b/src/data_handler/inc/downsample_operators.h index 7f7a0b968..4dec2956b 100644 --- a/src/data_handler/inc/downsample_operators.h +++ b/src/data_handler/inc/downsample_operators.h @@ -20,15 +20,17 @@ inline double downsample_each (double *data, int len) inline double downsample_median (double *data, int len) { - if (len % 2 == 0) - { - return downsample_mean (data, len); - } std::vector values; for (int i = 0; i < len; i++) { values.push_back (data[i]); } std::sort (values.begin (), values.end ()); + if (len % 2 == 0) + { + // for an even number of values the median is the mean of the two middle ones, + // same convention as RollingMedian in rolling_filter.h + return (values[len / 2 - 1] + values[len / 2]) / 2.0; + } return values[len / 2]; } From 9638cf690abed551008a3166ba5544d7b9bf7e14 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 20:25:51 -0700 Subject: [PATCH 2/4] Add downsampling median regression test Covers periods 2, 3 and 4 over a window with an outlier, and asserts period 4 returns the mean of the two sorted middle values rather than the full-window mean. --- .../examples/tests/downsampling_median.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 python_package/examples/tests/downsampling_median.py diff --git a/python_package/examples/tests/downsampling_median.py b/python_package/examples/tests/downsampling_median.py new file mode 100644 index 000000000..95f993aed --- /dev/null +++ b/python_package/examples/tests/downsampling_median.py @@ -0,0 +1,39 @@ +import numpy as np + +from brainflow.data_filter import AggOperations, DataFilter + + +def main(): + # One large outlier per window, so the median and the mean of a window are far apart + # and a median that silently falls back to the mean is visible. + data = [1.0, 2.0, 3.0, 100.0, 4.0, 5.0, 6.0, 200.0] + + # Period 2 is unchanged by design. With two values the median is their mean, so this + # pins that the even-period path did not shift the one case that was already correct. + period_2 = DataFilter.perform_downsampling(np.array(data), 2, AggOperations.MEDIAN.value) + assert np.allclose(period_2, [1.5, 51.5, 4.5, 103.0]), period_2 + + # Odd periods keep the existing single middle value. + period_3 = DataFilter.perform_downsampling(np.array(data), 3, AggOperations.MEDIAN.value) + assert np.allclose(period_3, [2.0, 5.0]), period_3 + + # Period 4 is the regression. Each window is the mean of the two sorted middle values, + # (2 + 3) / 2 and (5 + 6) / 2, rather than the full-window mean downsample_median + # used to return for every even period. + period_4 = DataFilter.perform_downsampling(np.array(data), 4, AggOperations.MEDIAN.value) + assert np.allclose(period_4, [2.5, 5.5]), period_4 + + # The same windows through MEAN, to show the two operations are now distinct where + # they used to return identical values. + mean_4 = DataFilter.perform_downsampling(np.array(data), 4, AggOperations.MEAN.value) + assert np.allclose(mean_4, [26.5, 53.75]), mean_4 + assert not np.allclose(period_4, mean_4), (period_4, mean_4) + + mean_3 = DataFilter.perform_downsampling(np.array(data), 3, AggOperations.MEAN.value) + assert not np.allclose(period_3, mean_3), (period_3, mean_3) + + print('downsampling median regression passed') + + +if __name__ == '__main__': + main() From 3f3dbb080d5144aa084c5114da5a1acdc14a9d1e Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 20:37:21 -0700 Subject: [PATCH 3/4] Run the new regression test in unix CI --- .github/workflows/run_unix.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 557001dae..a5978915a 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -354,6 +354,8 @@ jobs: run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/transforms.py - name: Downsampling Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/downsampling.py + - name: Downsampling Median Regression Python + run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/downsampling_median.py - name: ICA Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/ica.py - name: CSP Python From aad3ff13872c76e48e70ce87581ad0f661d19650 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 21:17:07 -0700 Subject: [PATCH 4/4] Run the new regression test in windows CI too --- .github/workflows/run_windows.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run_windows.yml b/.github/workflows/run_windows.yml index 244c3f0e5..c37732833 100644 --- a/.github/workflows/run_windows.yml +++ b/.github/workflows/run_windows.yml @@ -235,6 +235,9 @@ jobs: - name: Downsampling Python Test run: python %GITHUB_WORKSPACE%\python_package\examples\tests\downsampling.py shell: cmd + - name: Downsampling Median Regression Python Test + run: python %GITHUB_WORKSPACE%\python_package\examples\tests\downsampling_median.py + shell: cmd - name: CSP Python Test run: python %GITHUB_WORKSPACE%\python_package\examples\tests\csp.py shell: cmd