Select distinct groups of rows according to average
algorithm, c#-4.0, linq-to-sql, sql-server-2008
Solution
Here is a T-SQL procedural version that is somewhat like a draft, only draft order is optimized each round according to need.
The "competitive" nature of this seems to lead to slightly less than perfect ratios if all items are to be picked, but the up-side is that you basically have an O(N^2) algorithm since it's essentially a min function in a loop (maybe that's optimistic considering the `group by` clauses). It's also deterministic, and should be fairly straightforward to implement in another layer if necessary.
-- SET THESE!
declare @numberOfGroups int = 3
declare @itemsPerGroup int = 25
declare @targetRatio decimal(8,4) = .8
-- /SET
set nocount on
-- Create a table of items
declare @t_stats table (
id int not null primary key
, ratio decimal(8,4) not null
, grp int null
)
insert into @t_stats (id, ratio) values
(24,0.930000), (25,0.390000), (26,0.800000),
(27,0.920000), (28,0.550000), (30,0.810000),
(31,0.770000), (32,0.800000), (33,0.590000),
(36,0.760000), (37,0.910000), (40,0.690000),
(43,0.390000), (45,0.310000), (46,0.760000),
(47,0.710000), (54,0.710000), (55,0.950000),
(57,0.920000), (60,0.890000), (62,0.700000),
(66,0.890000), (68,0.950000), (107,0.760000),
(559,0.990000), (560,0.540000), (565,0.430000),
(566,0.830000), (568,0.590000), (579,0.970000),
(599,0.900000), (623,0.450000), (749,0.800000),
(750,0.970000), (753,0.820000), (754,0.730000),
(766,0.620000), (768,0.430000), (770,0.790000),
(838,0.700000), (875,0.835000), (987,0.900000),
(988,0.740000), (1157,0.850000), (1250,0.630000),
(1328,0.860000), (2171,0.900000), (2176,0.520000),
(2177,0.980000), (2178,0.940000), (2180,0.970000),
(2184,0.990000), (2187,0.950000), (2188,0.940000),
(2189,0.920000), (2195,0.990000), (2233,0.900000),
(2234,0.940000), (2235,0.950000), (2240,0.980000),
(2243,0.920000), (2253,0.900000), (2266,0.530000),
(2269,0.920000), (2270,0.970000), (2271,0.750000),
(2272,0.820000), (2275,0.910000), (2277,0.930000),
(2281,0.690000), (2282,0.710000), (2288,0.840000),
(2528,0.870000), (2778,0.950000), (2814,0.990000)
-- Create a table of groups
declare @groups table (
grp int not null primary key identity
)
while (select isnull(max(grp), 0) from @groups) < @numberOfGroups begin
insert into @groups default values
end
-- Check that we have enough items to fill all groups
if @numberOfGroups * @itemsPerGroup <= (select count(*) from @t_stats) begin
-- Groups now pick the best-fitting items one at a time
while (select count(*) from @t_stats where grp is not null) < (select count(*) * @itemsPerGroup from @groups) begin
declare @grp int, @Num int, @ratio decimal(8,4), @id int
-- Find the group with the least number of items or the worst ratio
select top 1 @grp = grp, @Num = Num, @ratio = ratio
from (
select g.grp
, count(i.grp) as Num
, isnull(avg(i.ratio), 0.0) as ratio
, abs(@targetRatio - avg(i.ratio)) as RatioDist
from @groups g
left join @t_stats i on g.grp = i.grp
group by g.grp
) as a
order by Num, RatioDist, grp
-- Let that group make their best pick
select top 1 @id = id
from (
select id
, abs(((ratio + (@ratio * @Num)) / (@Num + 1)) - @targetRatio) as NewRatioDist
from @t_stats
where grp is null
) as a
order by NewRatioDist
-- Update the items table based upon the pick
update @t_stats set grp = @grp where id = @id
end
end
else begin
-- Not enought items
raiserror('Too many groups or items per group.', 17, 0)
end
-- Display the results
select grp, count(*) as Num, avg(ratio) as ratio
from @t_stats
group by grp
order by grp
Problem
I have table `t_stats` with column `id (INT)` and column `ratio (DECIMAL(8,4))`. `id` is unique. I want to query table `t_stats` in order to select 3 groups with the same `AVG(ratio)` (closest possible). Can be done using temporary tables, as long as I can run it as a script or stored procedure. EDIT: Here is the concrete example: INPUT: ``` id ratio -- ----- 24 0.930000 25 0.390000 26 0.800000 27 0.920000 28 0.550000 30 0.810000 31 0.770000 32 0.800000 33 0.590000 36 0.760000 37 0.910000 40 0.690000 43 0.390000 45 0.310000 46 0.760000 47 0.710000 54 0.710000 55 0.950000 57 0.920000 60 0.890000 62 0.700000 66 0.890000 68 0.950000 107 0.760000 559 0.990000 560 0.540000 565 0.430000 566 0.830000 568 0.590000 579 0.970000 599 0.900000 623 0.450000 749 0.800000 750 0.970000 753 0.820000 754 0.730000 766 0.620000 768 0.430000 770 0.790000 838 0.700000 875 0.835000 987 0.900000 988 0.740000 1157 0.850000 1250 0.630000 1328 0.860000 2171 0.900000 2176 0.520000 2177 0.980000 2178 0.940000 2180 0.970000 2184 0.990000 2187 0.950000 2188 0.940000 2189 0.920000 2195 0.990000 2233 0.900000 2234 0.940000 2235 0.950000 2240 0.980000 2243 0.920000 2253 0.900000 2266 0.530000 2269 0.920000 2270 0.970000 2271 0.750000 2272 0.820000 2275 0.910000 2277 0.930000 2281 0.690000 2282 0.710000 2288 0.840000 2528 0.870000 2778 0.950000 2814 0.990000 ``` OUTPUT: ``` groupId id ratio ------- -- ----- 1 24 0.930000 1 25 0.390000 1 27 0.920000 1 30 0.810000 1 32 0.800000 1 36 0.760000 1 54 0.710000 1 60 0.890000 1 559 0.990000 1 560 0.540000 1 566 0.830000 1 568 0.590000 1 623 0.450000 1 750 0.970000 1 838 0.700000 1 987 0.900000 1 1157 0.850000 1 2178 0.940000 1 2180 0.970000 1 2253 0.900000 1 2269 0.920000 1 2271 0.750000 1 2281 0.690000 1 2778 0.950000 1 2814 0.990000 2 26 0.800000 2 28 0.550000 2 31 0.770000 2 40 0.690000 2 45 0.310000 2 55 0.950000 2 57 0.920000 2 66 0.890000 2 107 0.760000 2 565 0.430000 2 579 0.970000 2 753 0.820000 2 754 0.730000 2 766 0.620000 2 875 0.835000 2 1328 0.860000 2 2176 0.520000 2 2177 0.980000 2 2184 0.990000 2 2187 0.950000 2 2189 0.920000 2 2233 0.900000 2 2234 0.940000 2 2275 0.910000 2 2282 0.710000 3 33 0.590000 3 37 0.910000 3 43 0.390000 3 46 0.760000 3 47 0.710000 3 62 0.700000 3 68 0.950000 3 599 0.900000 3 749 0.800000 3 768 0.430000 3 770 0.790000 3 988 0.740000 3 1250 0.630000 3 2171 0.900000 3 2188 0.940000 3 2195 0.990000 3 2235 0.950000 3 2240 0.980000 3 2243 0.920000 3 2266 0.530000 3 2270 0.970000 3 2272 0.820000 3 2277 0.930000 3 2288 0.840000 3 2528 0.870000 ``` So I want to make 3 groups of `n` values and aim for a specific average value `x`. (Exemple with `n=30` and `0.75 < x < 0.85` would look like 3 groups of 30 values each where each group has `0.75 < AVG(ratio) < 0.85` and an `id` can only belong to 1 group.) So average is almost same in each group, and close to `x`: ``` groupId avg(ratio) ------- ---------- 1 0.805600 2 0.789000 3 0.797600 ```