Repository navigation
Conversation
Calibrator::get_iqr() partitions data around lower_index = std::round(0.25*data.size()) via std::nth_element, but then reads the lower quartile value from data[data.size()/4] -- a different index, using truncating integer division instead of std::round. The upper quartile does this correctly and consistently (partitions and reads both use upper_index). std::round(0.25*n) and n/4 only coincide when n mod 4 is 0 or 1; when n mod 4 is 2 or 3 the two indices differ by 1 (e.g. n=10: lower_index = round(2.5) = 3, but data.size()/4 = 2). std::nth_element only guarantees the element at the position it was given ends up correctly placed -- it gives no guarantee about any other index. So data[data.size()/4] after partitioning on lower_index is an arbitrary value merely known to be <= the true lower_index-th order statistic, not the actual lower-quartile value. Verified with a standalone simulation (200 random trials, n in [5,100]): 108/200 have lower_index != n/4, and for n=10 specifically std::round(0.25*10)=3 while 10/4=2, reproducing the exact mismatch. Impact: get_iqr() is only used by Calibrator::finalize() for the automatic (Freedman-Diaconis) bin-width calculation when a caller does not explicitly request a fixed bin count -- the default behavior of mrhistogram, mrhistmatch, mrstats, and dwi_brain_mask's internal histogram calibration whenever -bins isn't passed. A wrong (frequently too-small) IQR silently produces a wrong bin width and bin count. Fix: read data[lower_index] instead of data[data.size()/4], matching the already-correct upper-quartile pattern. Signed-off-by: Siddhardha Nanda <99672439+SID-6921@users.noreply.github.com>
|
Both CI failures look unrelated to this diff (which only touches
Happy to help if either turns out to be something I should address instead. |
Good catch; thanks for posting! |
What
Calibrator::get_iqr()(core/algo/histogram.cpp) partitionsdataaroundlower_index = std::round(0.25*data.size())viastd::nth_element, but then reads the lower quartile value fromdata[data.size()/4]— a different index, using truncating integer division instead ofstd::round. The upper quartile does this correctly and consistently (upper_indexis used for both the partition and the read).std::round(0.25*n)andn/4only coincide whenn mod 4is 0 or 1; whenn mod 4is 2 or 3 the two indices differ by 1 (e.g.n=10:lower_index = round(2.5) = 3, butdata.size()/4 = 2).std::nth_elementonly guarantees the element at the position it was given ends up correctly placed — it gives no guarantee about any other index. Sodata[data.size()/4]after partitioning onlower_indexis an arbitrary value merely known to be<=the truelower_index-th order statistic, not the actual lower-quartile value.Impact
get_iqr()is only used byCalibrator::finalize()for the automatic (Freedman-Diaconis) bin-width calculation when a caller does not explicitly request a fixed bin count — the default behavior ofmrhistogram,mrhistmatch,mrstats, anddwi_brain_mask's internal histogram calibration whenever-binsisn't passed. A wrong (frequently too-small) IQR silently produces a wrong bin width and bin count.This is a distinct bug from #3454 (which is about
Data::bin()dropping the max sample during binning) — same file, same general family of boundary/index-arithmetic issues, but a different function and a different root cause.Fix
Read
data[lower_index]instead ofdata[data.size()/4], matching the already-correct upper-quartile pattern.Verification
Verified with a standalone simulation (200 random trials, n in [5,100], using C++
std::round's round-half-away-from-zero semantics, not Python's banker's-roundinground()): 108/200 trials havelower_index != n/4, and forn=10specifically,std::round(0.25*10)=3while10/4=2, reproducing the exact mismatch described above.