Это не тривиальная проблема, так как вы, вероятно, соберетесь из-за полного отсутствия ответов через 10 дней, даже с вознаграждением. Как это бывает, я думаю, что это большая проблема для размышлений об алгоритмах и оптимизациях, поэтому спасибо за публикацию.
Первое, что я хотел бы отметить, это то, что вы абсолютно правы, что это не та проблема с что попробовать грубую силу. Вы можете приблизиться к правильному ответу, но с нетривиальным количеством выборок и точек распространения вы не найдете оптимального решения. Вам нужен итеративный подход, который перемещает элементы, только если они улучшают подбор, а алгоритм должен останавливаться, когда он не может сделать его лучше.
Мой подход здесь состоит в том, чтобы разбить проблему на три этапа :
- Вырезать данные в приблизительно правильные ячейки в первом приближении
- Переместить элементы из ячеек, которые тоже немного большой для тех, кто слишком мал. Делайте это итеративно до тех пор, пока больше нет движений, которые оптимизируют ячейки.
- Поменяйте местами элементы между столбцами для точной настройки подгонки, пока перестановки не станут оптимальными.
Причина для того, чтобы сделать это в таком порядке, состоит в том, что каждый шаг в вычислительном отношении обходится дороже, поэтому вы хотите передать лучшее приближение каждому шагу, прежде чем позволить ему делать свое дело.
Давайте начнем с функции для обрезки данных. примерно в правильные ячейки:
cut_elements <- function(j, dist)
{
# Specify the sums that we want to achieve in each partition
partition_sizes <- dist * sum(j)
# The cumulative partition sizes give us our initial cuts
partitions <- cut(cumsum(j), cumsum(c(0, partition_sizes)))
# Name our partitions according to the given distribution
levels(partitions) <- levels(cut(seq(0,1,0.001), cumsum(c(0, dist))))
# Return our partitioned data as a data frame.
data.frame(data = j, group = partitions)
}
Нам нужен способ оценить, насколько близко это приближение (и последующие приближения) к нашему ответу. Мы можем составить график в зависимости от целевого распределения, но также будет полезно иметь числовой показатель для оценки пригодности для включения в наш график. Здесь я буду использовать сумму квадратов разностей между выборочными ячейками и целевыми ячейками. Мы будем использовать журнал, чтобы сделать числа более сопоставимыми. Чем меньше число, тем лучше подходит.
library(dplyr)
library(ggplot2)
library(tidyr)
compare_to_distribution <- function(df, dist, title = "Comparison")
{
df %>%
group_by(group) %>%
summarise(estimate = sum(data)/sum(j)) %>%
mutate(group = factor(cumsum(dist))) %>%
mutate(target = dist) %>%
pivot_longer(cols = c(estimate, target)) ->
plot_info
log_ss <- log(sum((plot_info$value[plot_info$name == "estimate"] -
plot_info$value[plot_info$name == "target"])^2))
ggplot(data = plot_info, aes(x = group, y = value, fill = name)) +
geom_col(position = "dodge") +
labs(title = paste(title, ": log sum of squares =", round(log_ss, 2)))
}
Так что теперь мы можем сделать:
cut_elements(j, dist) %>% compare_to_distribution(dist, title = "Cuts only")
![enter image description here](https://i.stack.imgur.com/WFCOw.png)
Мы Можно видеть, что подгонка уже довольно хороша при простом разрезе данных, но мы можем сделать намного лучше, переместив элементы соответствующего размера из ячеек большого размера в ящики меньшего размера. Мы делаем это итеративно, пока никакие движения не улучшат нашу посадку. Мы используем два вложенных цикла while
, что должно заставить нас беспокоиться о времени вычислений, но мы начали с близкого совпадения, поэтому нам не нужно слишком много ходов до того, как l oop остановится:
move_elements <- function(df, dist)
{
ignore_max = length(dist);
while(ignore_max > 0)
{
ignore_min = 1
match_found = FALSE
while(ignore_min < ignore_max)
{
group_diffs <- sort(tapply(df$data, df$group, sum) - dist*sum(df$data))
group_diffs <- group_diffs[ignore_min:ignore_max]
too_big <- which.max(group_diffs)
too_small <- which.min(group_diffs)
swap_size <- (group_diffs[too_big] - group_diffs[too_small])/2
which_big <- which(df$group == names(too_big))
candidate_row <- which_big[which.min(abs(swap_size - df[which_big, 1]))]
if(df$data[candidate_row] < 2 * swap_size)
{
df$group[candidate_row] <- names(too_small)
ignore_max <- length(dist)
match_found <- TRUE
break
}
else
{
ignore_min <- ignore_min + 1
}
}
if (match_found == FALSE) ignore_max <- ignore_max - 1
}
return(df)
}
Давайте посмотрим, что это сделало:
cut_elements(j, dist) %>%
move_elements(dist) %>%
compare_to_distribution(dist, title = "Cuts and moves")
![enter image description here](https://i.stack.imgur.com/TTOUS.png)
Теперь вы можете видеть, что матч настолько близок, что мы пытаемся понять, есть разница между целью и разделенными данными. Вот почему нам понадобилась числовая мера GOF.
Тем не менее, давайте получим это соответствие как можно лучше путем перестановки элементов между столбцами для их точной настройки. Этот шаг требует значительных вычислительных ресурсов, но опять-таки мы уже даем ему близкое приближение, поэтому он не должен много делать:
swap_elements <- function(df, dist)
{
ignore_max = length(dist);
while(ignore_max > 0)
{
ignore_min = 1
match_found = FALSE
while(ignore_min < ignore_max)
{
group_diffs <- sort(tapply(df$data, df$group, sum) - dist*sum(df$data))
too_big <- which.max(group_diffs)
too_small <- which.min(group_diffs)
current_excess <- group_diffs[too_big]
current_defic <- group_diffs[too_small]
current_ss <- current_excess^2 + current_defic^2
all_pairs <- expand.grid(df$data[df$group == names(too_big)],
df$data[df$group == names(too_small)])
all_pairs$diff <- all_pairs[,1] - all_pairs[,2]
all_pairs$resultant_big <- current_excess - all_pairs$diff
all_pairs$resultant_small <- current_defic + all_pairs$diff
all_pairs$sum_sq <- all_pairs$resultant_big^2 + all_pairs$resultant_small^2
improvements <- which(all_pairs$sum_sq < current_ss)
if(length(improvements) > 0)
{
swap_this <- improvements[which.min(all_pairs$sum_sq[improvements])]
r1 <- which(df$data == all_pairs[swap_this, 1] & df$group == names(too_big))[1]
r2 <- which(df$data == all_pairs[swap_this, 2] & df$group == names(too_small))[1]
df$group[r1] <- names(too_small)
df$group[r2] <- names(too_big)
ignore_max <- length(dist)
match_found <- TRUE
break
}
else ignore_min <- ignore_min + 1
}
if (match_found == FALSE) ignore_max <- ignore_max - 1
}
return(df)
}
Давайте посмотрим, что это сделало:
cut_elements(j, dist) %>%
move_elements(dist) %>%
swap_elements(dist) %>%
compare_to_distribution(dist, title = "Cuts, moves and swaps")
![enter image description here](https://i.stack.imgur.com/Bf6uM.png)
Довольно близко к идентичному. Давайте подсчитаем, что:
tapply(df$data, df$group, sum)/sum(j)
# (0,0.3] (0.3,0.5] (0.5,0.6] (0.6,0.65] (0.65,0.715] (0.715,0.9]
# 0.30000025 0.20000011 0.10000014 0.05000010 0.06499946 0.18500025
# (0.9,1]
# 0.09999969
Итак, мы имеем исключительно близкое совпадение: каждый раздел находится на расстоянии менее одной части на миллион от целевого распределения. Весьма впечатляюще, учитывая, что у нас было всего 500 измерений для размещения в 7 бинах.
С точки зрения извлечения ваших данных мы не касались порядка j
в пределах фрейма данных df
:
all(df$data == j)
# [1] TRUE
и все разделы содержатся в df$group
. Поэтому, если мы хотим, чтобы одна функция возвращала только разделы j
с учетом dist
, мы можем просто сделать:
partition_to_distribution <- function(data, distribution)
{
cut_elements(data, distribution) %>%
move_elements(distribution) %>%
swap_elements(distribution) %>%
`[`(,2)
}
В заключение, мы создали алгоритм, который создает исключительно близкое совпадение. Тем не менее, это не хорошо, если это займет слишком много времени для запуска. Давайте проверим это:
microbenchmark::microbenchmark(partition_to_distribution(j, dist), times = 100)
# Unit: milliseconds
# expr min lq mean median uq
# partition_to_distribution(j, dist) 47.23613 47.56924 49.95605 47.78841 52.60657
# max neval
# 93.00016 100
Всего 50 миллисекунд, чтобы соответствовать 500 выборкам. Кажется, достаточно хорошо для большинства приложений. Он будет расти экспоненциально с большими выборками (около 10 секунд на моем P C для 10 000 выборок), но к этому моменту относительная тонкость выборок означает, что cut_elements %>% move_elements
уже дает логарифмическую сумму квадратов ниже -30 и следовательно, было бы исключительно хорошее совпадение без точной настройки swap_elements
. Это займет около 30 мс с 10 000 выборок.