Python随机列表推导式

7

I have a list similar to:

[1 2 1 4 5 2 3 2 4 5 3 1 4 2] 

我希望从此列表中创建一个包含x个不同元素的随机列表。难点在于我想使用列表推导来完成这个任务...

当x=3时,可能的结果如下:

[1 2 3]
[2 4 5]
[3 1 4]
[4 5 1]

谢谢!

我应该说明一下,我无法将列表转换为集合。抱歉! 我需要随机选择的数字具有加权值。因此,如果1在列表中出现4次,3在列表中出现2次,那么选择1的概率是选择3的两倍...


6
你有想过一种不使用列表推导的方式来完成它吗? - John La Rooy
你考虑过使用集合吗? - Asad Saeeduddin
4
请问您的问题需要澄清:类似于[1, 2, 1]这样的结果是否可行,换句话说,是否允许子列表中有两个相同的值(在本例中为1)? - FMc
@FMc,那个特定的从句还有什么其他含义吗? - Free Monica Cellio
йӮЈд№ҲдҪ зҡ„ж„ҸжҖқжҳҜ[1 2 3]жҳҜдёҖдёӘжңүж•Ҳзҡ„з»“жһңпјҢдҪҶ[1 1 2]дёҚжҳҜпјҢеӣ дёә1жҳҜзӣёеҗҢзҡ„пјҹ - Claudiu
显示剩余4条评论
9个回答

11

免责声明:要求“使用列表推导式”是荒谬的。

此外,如果您想使用权重,Eli Bendersky在加权随机抽样页面上列出了许多优秀的方法。

以下代码效率低下,不具有可扩展性等等问题。

尽管如此,它有不止一个(两个!)列表推导式,返回一个列表,从不重复元素,并在某种程度上尊重权重:

>>> s = [1, 2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2]
>>> [x for x in random.choice([p for c in itertools.combinations(s, 3) for p in itertools.permutations(c) if len(set(c)) == 3])]
[3, 1, 2]
>>> [x for x in random.choice([p for c in itertools.combinations(s, 3) for p in itertools.permutations(c) if len(set(c)) == 3])]
[5, 3, 4]
>>> [x for x in random.choice([p for c in itertools.combinations(s, 3) for p in itertools.permutations(c) if len(set(c)) == 3])]
[1, 5, 2]

.. 或者,按照 FMc 的简化说法:

>>> [x for x in random.choice([p for p in itertools.permutations(s, 3) if len(set(p)) == 3])]
[3, 5, 2]

(我会保留x for x,即使不把它写成list(random.choice(..))或者直接留作元组也有点难受)


1
聪明的回答对于一个疯狂的问题。也许你只是想提高列表推导的统计数据,但是itertools.permutations(s, 3)不就足够了吗?没有必要同时使用组合和排列。 - FMc
@FMc:不,我只是在最后一秒意识到“组合”不够用了,然后不加思考地添加了“排列”。做得好 :^) - DSM
@mgilson: 我一开始阅读时并没有觉得提问者的问题有歧义,但在你和其他人提出的所有观点之后,我已经完全不知道发生了什么了。:^) 你说得对,我将其视为一个抽取硬币直到获得三个不同硬币的问题。 - DSM
我认为我们中没有人知道发生了什么。我们都尽力猜测... - mgilson
@DSM:好答案!@mgilson:这取决于您正在使用的对象的__hash____eq__的作用。如果基于对象ID对象是等效的,则定义这些方法,此方法将起作用。 - Claudiu
显示剩余5条评论

6

通常情况下,不建议在列表推导中做这种事情——这会导致代码难以阅读。但是,如果你真的必须这样做,我们可以写一个非常糟糕的一行代码:

>>> values = [random.randint(0,10) for _ in xrange(12)]
>>> values
[1, 10, 6, 6, 3, 9, 0, 1, 8, 9, 1, 2]
>>> # This is the 1 liner -- The other line was just getting us a list to work with.
>>> [(lambda x=random.sample(values,3):any(values.remove(z) for z in x) or x)() for _ in xrange(4)]
[[6, 1, 8], [1, 6, 10], [1, 0, 2], [9, 3, 9]]

请不要使用这段代码——我只是出于娱乐和学术目的发布它。
这是它的工作原理:
我在列表推导式中创建一个带有默认参数的函数,该参数是从输入列表中随机选择的3个元素。在函数内部,我从values中删除这些元素,以便它们不能再被选择到。由于list.remove返回None,因此我可以使用any(lst.remove(x) for x in ...)来删除这些值并返回False。由于any返回False,我们进入or子句,该子句仅在调用函数时返回x(具有3个随机选择的默认值)。然后只需调用函数,让魔法发生即可。
唯一的问题是,您需要确保您请求的组数(这里选择了4)乘以每组的项目数(这里选择了3)小于或等于输入列表中的值数。虽然这似乎很明显,但还是值得提一下...
这是另一个版本,其中我将shuffle引入了列表推导式中:
>>> lst = [random.randint(0,10) for _ in xrange(12)]
>>> lst
[3, 5, 10, 9, 10, 1, 6, 10, 4, 3, 6, 5]
>>> [lst[i*3:i*3+3] for i in xrange(shuffle(lst) or 4)]
[[6, 10, 6], [3, 4, 10], [1, 3, 5], [9, 10, 5]]

这比我的第一次尝试要好得多,但大多数人仍然需要停下来,思考一下才能弄清楚这段代码在做什么。我仍然坚持认为使用多行更好。


@Asad -- 据我所知,没有仅由列表推导式组成的可读版本,就像OP所要求的那样。其他答案在给出易读版本方面做得非常好,如果“仅限于列表推导式”这个条件被移除,我会考虑使用它们来解决这个问题--因此,我并不觉得需要重复这种努力。 - mgilson
@mgilson,如果你真的想要,在列表推导式中加入洗牌是非常容易的。 - John La Rooy
@mgilson,您似乎错过了第一个约束条件(请参见gnibbler答案上的评论)。 - Asad Saeeduddin
1
如果Asad的解释是正确的 - 这也是我最初的理解方式,那么列表推导式的要求简直荒谬。这就是我在第一个评论中对问题的看法。 - John La Rooy
1
shuffle 返回 None,所以例如 xrange(shuffle(...) or 4) 就可以工作。 - John La Rooy
显示剩余6条评论

2
如果我正确理解了您的问题,这应该可以解决:

如果我正确理解了您的问题,这应该可以解决:

def weighted_sample(L, x):
    # might consider raising some kind of exception of len(set(L)) < x

    while True:
        ans = random.sample(L, x)
        if len(set(ans)) == x:
            return ans

然后,如果您想要许多这样的样本,您可以执行以下操作:
[weighted_sample(L, x) for _ in range(num_samples)]

我很难想象一种简单易懂的抽样逻辑理解方式,因为这个逻辑有点复杂。对我来说,它听起来像是随意加上去的作业。

如果你不喜欢无限循环,我虽然没有尝试过,但我认为这个方法会起作用:

def weighted_sample(L, x):

    ans = []        
    c = collections.Counter(L)  

    while len(ans) < x:
        r = random.randint(0, sum(c.values())
        for k in c:
            if r < c[k]:
                ans.append(k)
                del c[k]
                break
            else:
                r -= c[k]
        else:
            # maybe throw an exception since this should never happen on valid input

     return ans

我不确定这是否会引入偏差,但比完全丢弃重复的随机选择更有效的方法是从尚未在集合中的值中随机选择一个值。到目前为止,这似乎是唯一真正满足OP要求的答案。 - Asad Saeeduddin
是的,问题在于保留原始权重。我正在努力改进它。 :) - Free Monica Cellio
+1 我猜 o.o 我不知道频率必须加权。我删除了我的答案。 - Shashank
是的。我至少会在循环中设置一个上限,并在尝试获取非重复选择时花费太长时间时抛出异常。最好的方法是基于数组中值的频率创建概率密度函数,并使用它来选择一个值,这将使其成为线性时间。然而,我找不到任何内置的方法来实现这一点。 - Asad Saeeduddin
如果原始列表中的不同值少于x个,则在没有有效解决方案的情况下可能会出现无限循环的危险。 - John La Rooy
@gnibbler 我在注释中写了如何捕获那个情况(两个版本都适用)。 - Free Monica Cellio

0
def sample(self, population, k):
    n = len(population)
    if not 0 <= k <= n:
        raise ValueError("sample larger than population")
    result = [None] * k
    try:
        selected = set()
        selected_add = selected.add
        for i in xrange(k):
            j = int(random.random() * n)
            while j in selected:
                j = int(random.random() * n)
            selected_add(j)
            result[i] = population[j]
    except (TypeError, KeyError):   # handle (at least) sets
        if isinstance(population, list):
            raise
        return self.sample(tuple(population), k)
    return result

以上是一个简化版的样本函数Lib/random.py。我只删除了一些针对小数据集的优化代码。这些代码直接告诉我们如何实现自定义的样本函数:

  1. 获取一个随机数
  2. 如果该数字之前出现过,就放弃它并获取一个新的随机数
  3. 重复上述步骤,直到获得您想要的所有样本数字。

然后真正的问题变成了如何按权重从列表中获取随机值。这可以通过Python标准库中的原始random.sample(population, 1)(这里有点过度),但很简单。

下面是一种实现方式,因为在给定列表中,重复表示权重,所以我们可以使用int(random.random() * array_length)来获取您的数组的随机索引。

import random
arr = [1, 2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2]

def sample_by_weight( population, k):
    n = len(population)
    if not 0 <= k <= len(set(population)):
        raise ValueError("sample larger than population")
    result = [None] * k
    try:
        selected = set()
        selected_add = selected.add
        for i in xrange(k):
            j = population[int(random.random() * n)]
            while j in selected:
                j = population[int(random.random() * n)]
            selected_add(j)
            result[i] = j
    except (TypeError, KeyError):   # handle (at least) sets
        if isinstance(population, list):
            raise
        return self.sample(tuple(population), k)
    return result

[sample_by_weight(arr,3) for i in range(10)]

这里理解的目的是什么?random.sample(arr,3)已经从数组中返回了3个元素的样本。 - Asad Saeeduddin
这样做并不能保证我选择的元素一定不同。这已经接近我所需要的了... 如果random.sample能够返回3个不同的元素,那就完美了! - braden.groom
@Asad 如果我没有误解,OP说他/她想要一个列表,其中的元素是由3个整数组成的列表。 - Leonardo.Z
@JoranBeasley,arr中的数据由OP提供,我在这里使用range(10),因为OP没有提到他想要的列表的大小。 - Leonardo.Z
@braden.groom 我认为我的更新答案可以在按权重选择样本结果的同时保持其唯一性。 - Leonardo.Z

0

首先,我希望你的列表能够像这样

[1,2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2]  

如果你想打印给定列表的大小为3的排列,可以按照以下步骤进行。

import itertools

l = [1,2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2]

for permutation in itertools.permutations(list(set(l)),3):
    print permutation,  

输出:

(1, 2, 3) (1, 2, 4) (1, 2, 5) (1, 3, 2) (1, 3, 4) (1, 3, 5) (1, 4, 2) (1, 4, 3) (1, 4, 5) (1, 5, 2) (1, 5, 3) (1, 5, 4) (2, 1, 3) (2, 1, 4) (2, 1, 5) (2, 3, 1) (2, 3, 4) (2, 3, 5) (2, 4, 1) (2, 4, 3) (2, 4, 5) (2, 5, 1) (2, 5, 3) (2, 5, 4) (3, 1, 2) (3, 1, 4) (3, 1, 5) (3, 2, 1) (3, 2, 4) (3, 2, 5) (3, 4, 1) (3, 4, 2) (3, 4, 5) (3, 5, 1) (3, 5, 2) (3, 5, 4) (4, 1, 2) (4, 1, 3) (4, 1, 5) (4, 2, 1) (4, 2, 3) (4, 2, 5) (4, 3, 1) (4, 3, 2) (4, 3, 5) (4, 5, 1) (4, 5, 2) (4, 5, 3) (5, 1, 2) (5, 1, 3) (5, 1, 4) (5, 2, 1) (5, 2, 3) (5, 2, 4) (5, 3, 1) (5, 3, 2) (5, 3, 4) (5, 4, 1) (5, 4, 2) (5, 4, 3)   

希望这能有所帮助。 :)

0
>>> from random import shuffle
>>> L = [1, 2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2]
>>> x=3
>>> shuffle(L)
>>> zip(*[L[i::x] for i in range(x)])
[(1, 3, 2), (2, 2, 1), (4, 5, 3), (1, 4, 4)]

你也可以使用生成器表达式来代替列表推导式。
>>> zip(*(L[i::x] for i in range(x)))
[(1, 3, 2), (2, 2, 1), (4, 5, 3), (1, 4, 4)]

你的输出有重复的值,例如 (2, 2, 1)(1, 4, 4) - Asad Saeeduddin
1
@mgilson 不,不是这样的。引用原帖子的话:“我想从这个列表中创建一个包含x个随机元素的列表 其中没有选择的元素相同”。 - Asad Saeeduddin
@mgilson 对不起,我误读了FMc的评论,以为是来自OP。不过请看一下OP在glasslion(现已删除)的回答中的评论。 - Asad Saeeduddin
OP在glasslion删除的回答上发表了评论。在我看来,那个评论仍然含糊不清。例如,当重复调用时,sample可以从原始列表返回重复项。你也可以理解为它允许在结果列表中出现重复项。 - John La Rooy
@gnibbler -- 是的,我看了那个并且感到疑惑。我的观点是这里至少存在歧义。 - mgilson
显示剩余6条评论

0

从不使用列表推导式的方式开始:

import random
import itertools


alphabet = [1, 2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2]


def alphas():
    while True:
        yield random.choice(alphabet)


def filter_unique(iter):
    found = set()
    for a in iter:
        if a not in found:
            found.add(a)
            yield a


def dice(x):
    while True:
        yield itertools.islice(
            filter_unique(alphas()),
            x
        )

for i, output in enumerate(dice(3)):
    print list(output)
    if i > 10:
        break

列表推导式在filter_unique()这个部分遇到了麻烦,因为列表推导式没有“记忆”它所输出的内容。可能的解决方案是在找到好的输出之前生成许多输出,就像@DSM suggested所建议的那样。

0

缓慢、天真的方法是:

import random
def pick_n_unique(l, n):
    res = set()
    while len(res) < n:
        res.add(random.choice(l))
    return list(res)

这将挑选元素,并仅在有 n 个唯一元素时退出:

>>> pick_n_unique([1, 2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2], 3)
[2, 3, 4]
>>> pick_n_unique([1, 2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2], 3)
[3, 4, 5]

然而,如果您有一个包含三十个1和一个2的列表,那么它可能会变得很慢,因为一旦它有了一个1,它就会一直旋转直到最终命中一个2。更好的方法是计算每个唯一元素的出现次数,选择一个按其出现次数加权的随机元素,从计数列表中删除该元素,并重复此过程,直到您拥有所需数量的元素:

def weighted_choice(item__counts):
    total_counts = sum(count for item, count in item__counts.items())
    which_count = random.random() * total_counts
    for item, count in item__counts.items():
        which_count -= count
        if which_count < 0:
            return item
    raise ValueError("Should never get here")

def pick_n_unique(items, n):
    item__counts = collections.Counter(items)
    if len(item__counts) < n:
        raise ValueError(
            "Can't pick %d values with only %d unique values" % (
                n, len(item__counts))

    res = []
    for i in xrange(n):
        choice = weighted_choice(item__counts)
        res.append(choice)
        del item__counts[choice]
    return tuple(res)

无论如何,这个问题并不适合使用列表推导式来解决。

0

使用以下设置:

from random import shuffle
from collections import deque

l = [1, 2, 1, 4, 5, 2, 3, 2, 4, 5, 3, 1, 4, 2] 

这段代码:

def getSubLists(l,n):
    shuffle(l) #shuffle l so the elements are in 'random' order
    l = deque(l,len(l)) #create a structure with O(1) insert/pop at both ends
    while l: #while there are still elements to choose
        sample = set() #use a set O(1) to check for duplicates
        while len(sample) < n and l: #until the sample is n long or l is exhausted
            top = l.pop() #get the top value in l
            if top in sample: 
                l.appendleft(top) #add it to the back of l for a later sample
            else:
                sample.add(top) #it isn't in sample already so use it
        yield sample #yield the sample

你最终得到:
for s in getSubLists(l,3):
    print s
>>> 
set([1, 2, 5])
set([1, 2, 3])
set([2, 4, 5])
set([2, 3, 4])
set([1, 4])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接