2025, Nov 20 21:02

Почему np.histogram завышает последний бин на uint8 данных

Разбираем, почему при разбиении uint8 данных np.histogram завышает последний бин: полуоткрытые интервалы, включённая правая граница и правильный выбор bins.

Почему у на вид равномерного случайного изображения последняя корзина в np.histogram получается перекошенной? Если вы когда‑либо разбивали на бины данные uint8 и замечали необъяснимый всплеск ближе к верхней границе, дело не в случайности, а в том, как np.histogram трактует свои аргументы и обращается с границами бинов.

Воспроизводим поведение

Ниже приведённый фрагмент генерирует массив 500×500 со случайными значениями uint8 и строит две гистограммы. В первой используются границы шириной 25, полученные через Python‑овский range; во второй — шаг 16, подобранный так, чтобы симметрично покрыть весь диапазон 0–255 для uint8.

import numpy as np

img = np.random.randint(0, 256, (500, 500)).astype(np.uint8)

freq_25, edges_25 = np.histogram(img, range(0, 255, 25))
print(np.column_stack((freq_25, edges_25[:-1], edges_25[1:])))

freq_16, edges_16 = np.histogram(img, range(0, 257, 16))
print(np.column_stack((freq_16, edges_16[:-1], edges_16[1:])))

На практике первая распечатка показывает наибольшее количество в последнем указанном бине [225, 250), тогда как во второй распечатке все бины выглядят равномерными — что и ожидается для равномерно случайных данных uint8.

Что на самом деле происходит

Ключевой момент в том, что range(0, 255, 25) передаётся как параметр bins, а не как параметр range функции np.histogram. Это означает, что эти числа воспринимаются как явные границы бинов. Второй момент — семантика границ. Функция использует полуоткрытые интервалы для всех бинов, кроме последнего. Как сказано в документации:

Все, кроме последнего (самого правого) бина, — полуоткрытые. Иначе говоря, если bins равен:

[1, 2, 3, 4]

то первый бин — это [1,2) (включает 1, но исключает 2), второй — [2,3). Последний же бин — [3,4], он включает 4.

Применительно к границам, полученным из range(0, 255, 25), получаем [0, 25, 50, …, 250]. Каждый бин, кроме последнего, полуоткрыт, а последний замкнут справа. Именно эта «закрытость» справа и приводит к тому, что финальный бин собирает все значения, равные его правой границе. В такой конфигурации значения 250 попадают в последний бин, из‑за чего он стабильно выше остальных. Рост согласуется с тем, что у этого бина есть дополнительная включённая точка границы по сравнению с прочими бинами шириной 25.

Простой способ увидеть равномерность распределения

Когда разбиение согласовано с исходным диапазоном, результат получается интуитивным. Использование границ с шагом 16 до 257 включает 256 в качестве последней правой границы, которая в данных uint8 не встречается. Значит, все бины фактически ведут себя как полуоткрытые интервалы и дают ожидаемую равномерную форму.

import numpy as np

img = np.random.randint(0, 256, (500, 500)).astype(np.uint8)

freq_16, edges_16 = np.histogram(img, range(0, 257, 16))
print(np.column_stack((freq_16, edges_16[:-1], edges_16[1:])))

Это повторяет вторую часть предыдущей распечатки, где счётчики оказываются сбалансированными.

Почему это важно

Такое поведение не всегда очевидно с первого взгляда, но одну из границ диапазона где‑то необходимо включить. np.histogram выбирает включать правую границу в последний бин. Если верхняя граница соответствует значению, реально присутствующему в ваших данных, этот бин соберёт граничные значения и будет выглядеть завышенным. Когда вы явно задаёте границы бинов, эффект усиливается, если эти границы не покрывают диапазон данных аккуратно.

Выводы

Всегда обращайте внимание, какой параметр вы передаёте в np.histogram. Передача объекта range вторым позиционным аргументом создаёт явные бины, а не задаёт целевой диапазон. Помните, что все бины полуоткрытые, кроме последнего, который включает крайнее правое значение. Если вы ожидаете равномерного распределения по бинам, подбирайте границы так, чтобы включаемая правая граница не совпадала со значением внутри вашего диапазона данных — как в примере с шагом 16 до 257. Эта мелочь избавляет от сюрпризов и делает гистограмму такой, как подсказывает интуиция.