【问题标题】:grouping objects to achieve a similar mean property for all groups对对象进行分组以实现所有组的相似平均属性
【发布时间】:2011-05-26 15:21:56
【问题描述】:

我有一组对象,每个对象都有一个数字“权重”。我想创建这些对象的组,使每个组的对象权重的算术平均值大致相同。

这些组不一定有相同数量的成员,但组的大小将在彼此之内。在数量方面,将有 50 到 100 个对象,最大组大小约为 5。

这是一种众所周知的问题吗?这似乎有点像背包或分区问题。是否有已知的有效算法可以解决它?

作为第一步,我创建了一个 Python 脚本,该脚本通过按重量对对象进行排序、对这些对象进行子分组,然后将每个子组的成员分配到最终组之一来实现平均重量的非常粗略的等价。

我很喜欢用 python 编程,所以如果现有的包或模块可以实现部分功能,我会很高兴听到它们。

感谢您的帮助和建议。

【问题讨论】:

  • 这听起来也有点像装箱问题。无论是否,您都应该期望这在计算上很难获得最佳解决方案。

标签: python grouping combinations mean


【解决方案1】:

下面的程序是一个低成本的启发式程序。它所做的是通过在一轮中从排序列表的一端选择值,在下一轮从另一端选择值,将值分布在“桶”中,将大值与小值一起放置。以循环方式进行分配可确保满足有关每个桶的元素数量的规则。它是一种启发式而不是算法,因为它往往会产生好的解决方案,但不能保证不存在更好的解决方案。

理论上,如果有足够的值,并且它们是均匀分布或正态分布的,那么将值随机放入桶中可能会导致桶的均值相同。假设数据集很小,这种启发式方法提高了一个好的解决方案的机会。更多地了解数据集的大小和统计分布将有助于设计更好的启发式或算法。

from random import randint, seed
from itertools import cycle,chain

def chunks(q, n):
    q = list(q)
    for i in range(0, len(q), n):
       yield q[i:i+n]

def shuffle(q, n):
    q = list(q)
    m = len(q)//2
    left =  list(chunks(q[:m],n))
    right = list(chunks(reversed(q[m:]),n)) + [[]]
    return chain(*(a+b for a,b in zip(left, right)))

def listarray(n):
    return [list() for _ in range(n)]

def mean(q):
    return sum(q)/len(q)

def report(q):
    for x in q:
        print mean(x), len(x), x

SIZE = 5
COUNT= 37

#seed(SIZE)
data = [randint(1,1000) for _ in range(COUNT)]
data = sorted(data)
NBUCKETS = (COUNT+SIZE-1) // SIZE

order = shuffle(range(COUNT), NBUCKETS)
posts = cycle(range(NBUCKETS))
buckets = listarray(NBUCKETS)
for o in order:
    i = posts.next()
    buckets[i].append(data[o])
report(buckets)
print mean(data)

由于排序步骤,复杂性是对数的。这些是样本结果:

439 5 [15, 988, 238, 624, 332]
447 5 [58, 961, 269, 616, 335]
467 5 [60, 894, 276, 613, 495]
442 5 [83, 857, 278, 570, 425]
422 5 [95, 821, 287, 560, 347]
442 4 [133, 802, 294, 542]
440 4 [170, 766, 301, 524]
418 4 [184, 652, 326, 512]
440

请注意,对桶大小的要求占主导地位,这意味着如果原始数据的方差很大,则均值不会接近。你可以试试这个数据集:

data = sorted(data) + [100000]

包含100000 的桶将至少获得另外 3 个数据。

我想出了一个启发式的想法,如果一群孩子递给一包不同面额的钞票,并告诉他们按照这个游戏的规则分享,这就是他们会做的事情。它在统计上是合理的,并且 O(log(N))。

【讨论】:

    【解决方案2】:

    你可以试试k-means clustering:

    import scipy.cluster.vq as vq
    import collections
    import numpy as np
    
    def auto_cluster(data,threshold=0.1,k=1):
        # There are more sophisticated ways of determining k
        # See http://en.wikipedia.org/wiki/Determining_the_number_of_clusters_in_a_data_set
        data=np.asarray(data)
        distortion=1e20
        while distortion>threshold:
            codebook,distortion=vq.kmeans(data,k)
            k+=1   
        code,dist=vq.vq(data,codebook)    
        groups=collections.defaultdict(list)
        for index,datum in zip(code,data):
            groups[index].append(datum)
        return groups
    
    np.random.seed(784789)
    N=20
    weights=100*np.random.random(N)
    groups=auto_cluster(weights,threshold=1.5,k=N//5)
    for index,data in enumerate(sorted(groups.values(),key=lambda d: np.mean(d))):
        print('{i}: {d}'.format(i=index,d=data))
    

    上面的代码生成了一个包含 N 个权重的随机序列。 它使用scipy.cluster.vq.kmeans 将序列划分为靠近在一起的k 数字簇。如果失真高于阈值,则重新计算 kmeans,增加k。重复此过程,直到失真低于给定阈值。

    它会产生这样的集群:

    0: [4.9062151907551366]
    1: [13.545565038022112, 12.283828883935065]
    2: [17.395300245930066]
    3: [28.982058040201832, 30.032607500871023, 31.484125759701588]
    4: [35.449637591061979]
    5: [43.239840915978043, 48.079844689518424, 40.216494950261506]
    6: [52.123246083619755, 53.895726546070463]
    7: [80.556052179748079, 80.925071671718413, 75.211470587171803]
    8: [86.443868931310249, 82.474064251040375, 84.088655128258964]
    9: [93.525705849369416]
    

    请注意,k-means 聚类算法使用随机猜测来最初选择 k 组的中心。这意味着重复运行相同的代码会产生不同的结果,尤其是当权重没有将它们自己分成明显不同的组时。

    您还必须调整阈值参数以生成所需数量的组。

    【讨论】:

    • 感谢您关于使用聚类算法的建议。但是,我不知道这种方法将如何工作:正如我在原始帖子中提到的那样,我需要有一定数量的组,每个组都有指定数量的成员。这些数字是根据指定的每个组的最大对象数 nmax 以及没有任何组的对象数少于 (nmax-1) 个的要求来计算的。例如,假设我总共有 28 个对象,并且要求最多 5 个对象/组。这导致六个组,每个组的对象数量如下:[5,5,5,5,4,4]。
    • @cytochrome:哦,我没有意识到组大小有这么严格的要求。在那种情况下,似乎确实很少有候选解决方案。例如,在上面的示例中,解决方案必须以组大小为 [5,5,5,5,4,4] 的某种排列结束。 (只有6 choose 2 = 6*5/2 = 15 种可能性)。您可以对权重进行排序,并根据每个可能的排列将它们分组,然后选择失真最小的排列。
    • 谢谢,unutbu。尽管我的问题比示例大,但使用您推荐的策略应该仍然可以管理。我目前的代码已经完成了部分工作,因此更改应该很简单。
    • >> 任何组的对象都不会少于 (nmax-1) 个。如果是 27 个对象并且 nmax==5 怎么办?
    【解决方案3】:

    您也可以尝试基于质心的链接算法,它可以达到相同的效果。

    代码见this,理解见this

    UPGMA(又名基于质心)是您可能想要做的。

    【讨论】:

      猜你喜欢
      • 2018-10-24
      • 2020-05-26
      • 2016-01-13
      • 1970-01-01
      • 2022-01-11
      • 1970-01-01
      • 2015-12-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多