This seems simple, but none of the solutions I've tried work well.
For example, for a range [0, 67), how can you split it up into 20 approximately equal sized discrete bins without introducing significant artifacts due to rounding?
I've tried the following solutions, but none of them seems ideal:
A. Round The Bin Size Then Floor
bin i start = floor(size/n*i)
bin i end = floor(size/n*(i+1))
This results in the following bins:
[0, 3), [3, 6), [6, 10), [10, 13), [13, 16), [16, 20), [20, 23), [23, 26), [26, 30), [30, 33), [33, 36), [36, 40), [40, 43), [43, 46), [46, 50), [50, 53), [53, 56), [56, 60), [60, 63), [63, 67)
With the bin sizes graphed as

Pros
- all bins are about the same size, either 3 or 4
- the entire range is covered by the bins
Cons
- an artificial pattern is introduced into the data, e.g. so if you graph values broken into these bins then you tend to see a jagged line, which is just an artifact of the rounding method
B. Skip The End
bin i start = floor(size/n)*i
bin i end = floor(size/n)*(i+1)
This results in the following bins:
[0, 3), [3, 6), [6, 9), [9, 12), [12, 15), [15, 18), [18, 21), [21, 24), [24, 27), [27, 30), [30, 33), [33, 36), [36, 39), [39, 42), [42, 45), [45, 48), [48, 51), [51, 54), [54, 57), [57, 60)
With the bin sizes graphed as

Pros
- all bins are exactly the same size (3)
- no artificial pattern is introduced in the data (besides skipping the end)
Cons
- the end of the range [60, 67) is excluded from the bins
C. Adjust The Last Bin
bin i start = round(size/n)*i
bin i end = if not last: round(size/n)*(i+1)
if last: size
This results in the following bins:
[0, 3), [3, 6), [6, 9), [9, 12), [12, 15), [15, 18), [18, 21), [21, 24), [24, 27), [27, 30), [30, 33), [33, 36), [36, 39), [39, 42), [42, 45), [45, 48), [48, 51), [51, 54), [54, 57), [57, 67)
With the bin sizes graphed as

Pros
- the entire range is covered by the bins
- no artificial pattern is introduced in the data (besides the last bin)
Cons
- the last bin is about 3x as large as all the other bins