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 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 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() 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]; }