Простой подход с параметром кластеризации не прошел. Поэтому предлагаю План Б
План Б.
Математическое ожидание квадрата расстояния между равномерно распределенными точками равно в n-мерном пространстве равно n/6
. По предельной теореме эта случайная величина нормально распределена. Чат-ГПТ подсказал мне, что sigma^2 = 7n/180
. Поэтому можно использовать оценку D^2
для проверки равномерности распределения
Пусть даны N точек, выберем из них k
, для каждой из точек выберем m
случайных. Вычислим средний квадрат для каждой из k
точек (усреднение по m), затем усредним по k
. И сравним c n/6
def mean_square_distance(points, k, m):
"""
Вычисляет среднее значение квадрата расстояния между случайно выбранными точками.
Возвращает среднее значение квадрата расстояния между k случайно выбранными точками,
усредненное по m случайным точкам, выбранным для каждой из k точек.
"""
points_num = len(points)
if k > points_num:
k = points_num
if m > points_num:
m = points_num
indices = np.random.choice(points_num, k, replace=False)
selected_points = points[indices]
distance_squares = []
for point in selected_points:
# Выбираем m случайных точек
random_indices = np.random.choice(points_num, m, replace=False)
random_points = points[random_indices]
# Вычисляем расстояния до m случайных точек
dists = np.linalg.norm(random_points - point, axis=1)
distance_squares.append(np.mean(dists**2)) # усредняем по m
return np.mean(distance_squares) # усредняем по k
def p_value_distance_squared(actual_mean_distance_square: float, dimensions: int, sample_size: int) -> float:
"""
Вычисляет p-value для наблюдаемого среднего квадрата расстояния (D^2)
между парами точек в dimensions-мерном пространстве.
Параметры:
actual_mean_distance_square — наблюдаемое значение среднего D^2
dimensions — размерность пространства
sample_size — число пар точек, по которым усреднено D^2
Возвращает:
p-value (вероятность того, что отклонение случайно)
"""
# Теоретическое среднее и дисперсия D^2
mu = dimensions / 6
var = (7 * dimensions) / 180
sigma_squared = var / sample_size
# Z-статистика: Z = (X̄ - μ) / σ
z_stat = (actual_mean_distance_square - mu) ** 2 / sigma_squared
# p-value для двустороннего критерия
p_value = 2 * stats.norm.sf(abs(z_stat))
return p_value
Я попробовал для 1000 точек в пространстве размерностью 10_000.
равномерное распределение, выборка 100 точек, для каждой случайные 100 соседей. Средний квадрат расстояний 1665.6168, теоретическое значение: 1666.6667
нормальное распределение с дисперсией 0.25, выборка 100 точек, для каждой случайные 100 соседей. Средний квадрат расстояний 1150.9005, теоретическое значение: 1666.6667
достоверность гипотезы о равномерном распределении для E[D^2] = 1665.6168
равна 0.9977
достоверность гипотезы при E[D^2] = 1150.9005
равна 0.
Ну вот и ответ.
План А.
Точной математической модели у меня нет, скорее соображения.
Если у вас точек не очень много, то вы можете для каждой точке посчитать расстояние до k
ближайших соседей. В качестве параметра "кластеризации" можно взять медианное расстояние до k
соседей, или среднее расстояние.
Затем посчитайте среднее значение параметра кластеризации и его дисперсию по всем точкам. Если дисперсия около нуля, то точки распределены регулярно. Если же дисперсия сравнима с параметром кластеризации, то скорее всего есть сгущения и разрежения, то есть распередение не совсем равномерное.
Либо же можно посмотреть на гистограмму параметра кластеризации для каждой точки. Если там несколько пиков, то значит несколько кластеров. Если есть чётко выраженный пик, то скорее всего распеределено равномерно (при неравномерном распеределении, например точки кучкуются вокруг некоторого центра, параметр кластеризации будет размазан, у дальних точек будет бОльшее расстояние до соседей, чем у центральных)
UPDATE
Так просто это не сработало, увы.
Вот гистограмма расстояния до ближайшего соседа в 1000-мерном пространстве для равномерно распределенных точек в единичном кубе

Среднее расстояние: 12.10480341041847
Дисперсия: 0.013365256264672094
А вот гистограмма для нормально распределенных вокруг центра единичного куба (стандартное отклонение 0.25)

Среднее расстояние: 10.019942895837655
Дисперсия расстояний: 0.014727889301131388
Видно, что распределения расстояний до ближайшего соседа в обоих случаях похожи.
Зато если есть кластеры с разными параметрами кластеризации, то гистограмма выглядит убедительно ))
(два центра с нормально распределенными точками, в распределениях разные дисперсии)
код для равномерного распределения
# Генерация N точек в n-мерном кубе, расчет расстояний до ближайшего соседа, гистограмма, среднее и дисперсия
import numpy as np
from scipy.spatial import cKDTree
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use("Qt5Agg")
n = 1000 # размерность пространства
N = 3000 # число точек
# Генерация точек
points = np.random.rand(N, n)
print(f'Сгенерированы {N} точек в {n}-мерном пространстве.')
# Поиск ближайших соседей через KD-дерево
tree = cKDTree(points)
distances, idx = tree.query(points, k=2) # k=2: первая - сама точка, вторая - ближайший сосед
nearest = distances[:, 1]
print(f'Найдено {len(nearest)} расстояний до ближайших соседей.')
# Гистограмма
plt.hist(nearest, bins=50, edgecolor='black')
plt.xlabel('Расстояние до ближайшего соседа')
plt.ylabel('Частота')
plt.title('Гистограмма расстояний до ближайшего соседа')
plt.show()
# Среднее и дисперсия
mean = np.mean(nearest)
var = np.var(nearest)
print(f'Среднее расстояние: {mean}')
print(f'Дисперсия: {var}')
код для нормально распределенных точек
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial import KDTree
import matplotlib
matplotlib.use("Qt5Agg")
n = 1000 # размерность пространства
N = 3000 # число точек
center = np.full(n, 0.5)
# Генерация точек: нормальное распределение вокруг центра, обрезка в пределах куба
points = np.random.normal(loc=center, scale=0.25, size=(N, n))
points = np.clip(points, 0, 1)
print(
f"Сгенерированы {len(points)} точек в {n}-мерном пространстве"
"с нормальным распределением вокруг центра единичного куба."
)
# Поиск ближайших соседей
kdtree = KDTree(points)
distances, _ = kdtree.query(points, k=2)
nearest_distances = distances[:, 1]
print(f"Найдено {len(nearest_distances)} расстояний до ближайших соседей.")
# Гистограмма
plt.hist(nearest_distances, bins=50, edgecolor="black")
plt.xlabel("Расстояние до ближайшего соседа")
plt.ylabel("Число точек")
plt.title("Гистограмма расстояний до ближайшего соседа (нормальное распределение)")
plt.show()
# Среднее и дисперсия
mean_dist = np.mean(nearest_distances)
var_dist = np.var(nearest_distances)
print(f"Среднее расстояние: {mean_dist}")
print(f"Дисперсия расстояний: {var_dist}")