Головоломка: равномерное распределение чисел по группам

это скорее головоломка на самом деле. Его, вероятно, спрашивали в другом месте раньше, но я ничего не мог найти, поэтому решил поделиться вопросом.

Я пытаюсь реализовать какую-то балансировку нагрузки в приложении и уменьшил проблему до того, что я считаю простым упражнением TSQL (приложение преимущественно находится в домене SQL Server (SQL Server 2008 R2)).

в принципе у меня есть таблица с двумя целыми числами; уникальный порядковый идентификатор и не уникальное значение. Таблица может содержать любое количество записей, и я хотел бы создать таблицу данных, в которой первые n самых больших значений разбиваются на отдельные "группировки", а затем второй набор n самых больших значений разбивается на отдельные "группировки".

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

DECLARE @GroupCount INT = 5

-- Set up the test data
DECLARE @test TABLE (Id INT IDENTITY(1, 1), Value INT)
INSERT  @Test (Value)
VALUES  (100), (456), (121), (402), (253), (872), (765), (6529), (1029), (342), (98), (1), (0), (4), (46), (23), (456), (416), (2323), (4579)


--Order by Value descending
;WITH cte AS
(
    SELECT  *
            ,ROW_NUMBER() OVER (ORDER BY Value DESC) RowNum
    FROM    @Test
)
--use modulus to split into grouping
, cte2 AS
(
    SELECT  *
            ,ROW_NUMBER() OVER (PARTITION BY RowNum % @GroupCount ORDER BY RowNum DESC) Rnk
    FROM    cte
)
SELECT  ROW_NUMBER() OVER (PARTITION BY Rnk ORDER BY Value DESC) AS 'Grouping'
    ,Value
    ,Id
FROM    cte2
ORDER BY [Grouping], Value ASC

это работает и создает следующий набор данных:

Grouping,   Value,      Id
========    =====       ==
1           46          15
1           342         10
1           765         7
1           6529        8
2           23          16
2           253         5
2           456         2
2           4579        20
3           4           14
3           121         3
3           456         17
3           2323        19
4           1           12
4           100         1
4           416         18
4           1029        9
5           0           13
5           98          11
5           402         4
5           872         6

возвращаемый набор данных верен в том, что первое n наибольшие значения разбиваются на отдельные группы и т. д., но общие значения в каждой группе сильно отличаются в группе 1 по сравнению с группой 5 (например).

когда сгруппированы и суммированы, мы можем видеть un-even spread:

Grouping,   SummedValues
========    ============
1           7682
2           5311
3           2904
4           1546
5           1372

как можно меньше строк, как я могу лучше сбалансировать значения, чтобы общие значения в каждой группе распределялись более равномерно?

5 ответов


это неправильно,но не страшно для данных примера. Ваш пробег может отличаться.

declare @groupcount int = 5;
create table t (id int identity(1, 1), value int);
insert  t values 
    (100),(456),(121),(402),(253),(872),(765),(6529),(1029),(342)
  , (98),(1),(0),(4),(46),(23),(456),(416),(2323),(4579);
;with cte as (
  select *
      , rn = row_number() over (order by value asc)
      , pct = value/sum(value+.0) over()
      , target = 1.0 / @groupcount 
  from t
)
, remaining as (
select id, value, rn
  , grp = convert(int,(sum(value) over (order by rn)/sum(value+.0) over())*@groupCount)+1
from cte
)
select
    grp = row_number() over (order by sum(value) desc)
  , sumValue = sum(value)
from remaining
group by grp

rextester demo:http://rextester.com/UNV61100

результаты:

+-----+----------+
| grp | sumValue |
+-----+----------+
|   1 |     6529 |
|   2 |     4579 |
|   3 |     3483 |
|   4 |     2323 |
|   5 |     1901 |
+-----+----------+


Совместимая версия Sql Server 2008:
declare @groupcount int = 5;
create table t (id int identity(1, 1), value int);
insert  t values 
    (100),(456),(121),(402),(253),(872),(765),(6529),(1029),(342)
  , (98),(1),(0),(4),(46),(23),(456),(416),(2323),(4579);
;with cte as (
  select *
      , rn = row_number() over (order by value asc)
      , pct = value/tv.TotalValue
      , target = 1.0 / @groupcount 
  from t
    cross join (select TotalValue = sum(value+.0) from t) tv
)
, remaining as (
select id, value, rn
  , grp = convert(int,((x.sumValueOver/TotalValue)*@groupcount)+1)
from cte
  outer apply (
    select sumValueOver = sum(value) 
    from cte i
    where i.rn <= cte.rn
      ) x
)
select
    grp = row_number() over (order by sum(value) desc)
  , sumValue = sum(value)
from remaining
group by grp

rextester demo:http://rextester.com/DEUDJ77007

возвращает:

+-----+----------+
| grp | sumValue |
+-----+----------+
|   1 |     6529 |
|   2 |     4579 |
|   3 |     3483 |
|   4 |     2323 |
|   5 |     1901 |
+-----+----------+

здесь NTILE функция в sql server может вам помочь.

DECLARE @GroupCount INT = 5

-- Set up the test data
DECLARE @test TABLE (Id INT IDENTITY(1, 1), Value INT)
INSERT  @Test (Value)
SELECT  100
UNION ALL
SELECT  456
UNION ALL
SELECT  121
UNION ALL
SELECT  402
UNION ALL
SELECT  253
UNION ALL
SELECT  872
UNION ALL
SELECT  765
UNION ALL
SELECT  6529
UNION ALL
SELECT  1029
UNION ALL
SELECT  342
UNION ALL
SELECT  98
UNION ALL
SELECT  1
UNION ALL
SELECT  0
UNION ALL
SELECT  4
UNION ALL
SELECT  46
UNION ALL
SELECT  23
UNION ALL
SELECT  456
UNION ALL
SELECT  416
UNION ALL
SELECT  2323
UNION ALL
SELECT  4579

;With cte
AS
(
    SELECT *, NTILE(@GroupCount) OVER(ORDER BY Value DESC) AS GroupNo FROM @Test
)
SELECT GroupNo, SUM(Value) AS SummedValues FROM cte
GROUP BY GroupNo

и я получаю такой результат.

GroupNo SummedValues
--------------------
1       14460
2       2549
3       1413
4       365
5       28

немного лучший способ сделать это было бы "змея" выбор. Вы выстраиваетесь в очередь на 1 - й, 6-й, 11-й самый высокий-конечно, это намного выше, чем 5-й, 10-й, 15-й.

лучше будет 1-й, 10-й, 11-й, против 5-го, 6-го, 15-го. Все еще не идеально, и с вашими конкретными данными все еще очень плохо, но немного лучше, чем ваше.

DECLARE @GroupCount INT = 5

-- Set up the test data
DECLARE @test TABLE (Id INT IDENTITY(1, 1), Value INT)
INSERT  @Test (Value)
SELECT  100
UNION ALL
SELECT  456
UNION ALL
SELECT  121
UNION ALL
SELECT  402
UNION ALL
SELECT  253
UNION ALL
SELECT  872
UNION ALL
SELECT  765
UNION ALL
SELECT  6529
UNION ALL
SELECT  1029
UNION ALL
SELECT  342
UNION ALL
SELECT  98
UNION ALL
SELECT  1
UNION ALL
SELECT  0
UNION ALL
SELECT  4
UNION ALL
SELECT  46
UNION ALL
SELECT  23
UNION ALL
SELECT  456
UNION ALL
SELECT  416
UNION ALL
SELECT  2323
UNION ALL
SELECT  4579


--Order by Value descending
;WITH cte AS
(
    SELECT  *
            ,ROW_NUMBER() OVER (ORDER BY Value DESC) RowNum
    FROM    @Test
)
--use modulus to split into grouping
, cte2 AS
(
    SELECT  *
            ,ROW_NUMBER() OVER (PARTITION BY RowNum % (@GroupCount*2 ) ORDER BY RowNum DESC) Rnk
    FROM    cte
)
select [Grouping], SUM(value) from (
SELECT  floor(abs(@GroupCount - (ROW_NUMBER() OVER (PARTITION BY Rnk ORDER BY Value DESC) - 0.5)) + 0.5) AS 'Grouping'
    ,Value
    ,Id
FROM    cte2
--ORDER BY [Grouping], Value ASC
) a group by [Grouping]
  order by [Grouping] ASC

в конечном счете, хотя я думаю, что случайное назначение, вероятно, лучше, чем это, может быть, случайное назначение в то время как проверка того, что сумма еще не равна 2*(1/группировка * итого).

действительно, я думаю, что это не проблема, хорошо решаемая TSQL или любым SQL; языки, которые могут управлять потоком по строкам, лучше послужат вам. Python, C#, SAS, любой другой инструмент, который находится в вашем наборе инструментов. (PL / SQL-это единственное место, куда я хотел бы пойти здесь...)

все, что позволит вам сказать на уровне строк: "отслеживая то, что я назначил до сих пор, назначьте этот конкретный случай ведро с самым низким номером до сих пор " действительно будет работать лучше.

Grouping Summed Values
---------------------

1       1781
2       1608
3       2904
4       5249
5       7273

С помощью ntile и row_number окно функционирует вместе, чтобы не только разделить его на четные группы (даже по количеству, а не суммировать), но и принять лучшее решение о том, какие значения включать в каждую группу, чтобы выровнять общую сумму в каждой группе как можно больше.

ответ:

select case b.grp_split when 1 then b.grp_split_rnk_desc else grp_split_rnk_asc end as [grouping]
, b.value
, b.id
from (
    select a.id
    , a.value
    , a.grp_split
    , row_number() over (partition by a.grp_split order by a.value desc) grp_split_rnk_desc
    , row_number() over (partition by a.grp_split order by a.value asc) grp_split_rnk_asc
    from (
        select t.id
        , t.value
        , ntile(@ntile_cnt) over (order by t.value desc) as grp_split
        from @test as t
        ) as a
    ) as b
order by case b.grp_split when 1 then b.grp_split_rnk_desc else grp_split_rnk_asc end asc
, b.value asc

результаты:

не идеально, но немного ближе.

Group   Total
1       7029
2       5096
3       2904
4       1761
5       2025

результат является первичным, определяемым первыми наибольшими значениями. Таким образом, вы можете попробовать заказать все остальное в обратном порядке

WITH cte AS
(
    SELECT  *
            ,ROW_NUMBER() OVER (ORDER BY Value DESC) RowNum
    FROM    @Test
)
--use modulus to split into grouping
, cte2 AS
(
    SELECT  *
            ,ROW_NUMBER() OVER (PARTITION BY RowNum % @GroupCount ORDER BY RowNum ) Rnk
    FROM    cte
)
,cte3 AS
(SELECT  ROW_NUMBER() OVER (PARTITION BY Rnk ORDER BY case rnk when 1 then Value else -Value end DESC) AS [Grouping]
    ,Value
    ,Id
FROM    cte2
 )
select [Grouping],sum(value)
from cte3
group by [Grouping]
order by [Grouping];

результат

  Grouping  (No column name)
1   1   7029
2   2   5096
3   3   2904
4   4   1761
5   5   2025