Skip to content

Fix downsample_median returning the mean for even periods - #853

Open
adityasingh2400 wants to merge 4 commits into
brainflow-dev:masterfrom
adityasingh2400:fix-downsample-median-even
Open

Fix downsample_median returning the mean for even periods#853
adityasingh2400 wants to merge 4 commits into
brainflow-dev:masterfrom
adityasingh2400:fix-downsample-median-even

Conversation

@adityasingh2400

Copy link
Copy Markdown

downsample_median in src/data_handler/inc/downsample_operators.h short circuits to downsample_mean whenever the period is even. perform_downsampling with AggOperations::MEDIAN is therefore bit for bit identical to AggOperations::MEAN for every even period, which defeats the outlier rejection that is the only reason to choose a median filter. A single large spike inside a window passes straight through, divided by the period, instead of being discarded.

The library already contains the correct answer. RollingMedian in rolling_filter.h, which backs perform_rolling_filter, averages the two middle values for even window lengths. So BrainFlow has two median implementations that disagree with each other on identical input. This makes downsample_median follow the same convention: sort first, then average the two middle values when the length is even.

Two things worth stating precisely. perform_rolling_filter is not affected, only perform_downsampling. And at period 2 the correct even median is by definition the mean of the two values, so behaviour there is unchanged. Odd periods are unchanged.

Verified by building libDataHandler.dylib on macOS arm64. Input is a quiet baseline around 10.0 with one 1000.0 spike per window.

perform_downsampling with MEDIAN, window 0:

period MEDIAN before MEDIAN after MEAN true median
2 504.750000 504.750000 504.750000 504.750000
3 (odd) 10.500000 10.500000 340.000000 10.500000
4 257.250000 9.750000 257.250000 9.750000
5 (odd) 10.000000 10.000000 207.800000 10.000000
6 175.083333 10.250000 175.083333 10.250000
8 133.687500 10.000000 133.687500 10.000000

Spike leakage above the 10.0 baseline, which is the number that actually matters for a median filter:

period MEDIAN before MEDIAN after MEAN
4 +247.6250 +0.5000 +247.6250
6 +165.0833 +0.2500 +165.0833
8 +123.8125 +0.2500 +123.8125

Before the fix MEDIAN leaked the spike identically to MEAN. After, it rejects it.

Cross check of the two median paths on identical data, perform_downsampling MEDIAN against perform_rolling_filter MEDIAN aligned to the same windows: 10 of 10 windows mismatched at period 4 and 5 of 5 at period 8 before the fix, with 257.25 against 9.75, a 26x disagreement. 0 of 10 and 0 of 5 mismatched after.

All 9 signal processing examples that CI runs pass on this branch, and clang-format reports no changes on the touched file.

Disclosure: this change was prepared with AI assistance. I have reviewed and tested it.

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.

Copy link
Copy Markdown
Member

The implementation now matches RollingMedian and the conventional median definition for an even number of values. Period 2 remains unchanged, and odd periods retain the existing behavior.

Please add an automated downsampling case for periods 2, 3, and 4 with an outlier, asserting that period 4 returns the mean of the two sorted middle values rather than the full-window mean. Since this changes established output for every even period greater than 2, please also mention it in the release notes if numerical behavior changes are normally documented.

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.
@adityasingh2400

Copy link
Copy Markdown
Author

Added python_package/examples/tests/downsampling_median.py and wired it into run_unix.yml.

It runs periods 2, 3 and 4 over a window with an outlier. Period 4 asserts [2.5, 5.5], the mean of the two sorted middle values, against the [26.5, 53.75] full-window mean the old code returned, and also asserts MEDIAN and MEAN now differ where they used to be identical. Verified it fails on master with exactly [26.5, 53.75].

On release notes, I could not find a changelog file in the repo. If numerical changes get noted somewhere outside git, point me at it and I will add an entry.

@adityasingh2400

Copy link
Copy Markdown
Author

Follow-up: I had only wired the new test into run_unix.yml. The existing python DSP tests also run in run_windows.yml, so I added it there too, matching the shell: cmd style used by the downsampling.py step. run_alpine.yml does not run any python example tests, so I left it alone.

Both workflow files still parse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants