【问题标题】:Python: Most efficient way to get two Boolean property frequencies in a list of objects?Python:在对象列表中获取两个布尔属性频率的最有效方法?
【发布时间】:2013-03-11 17:19:52
【问题描述】:

我有一个用户对象,有两个布尔属性,像这样:

class User(object):
  def __init__(self, a, b):
    self.a = a  # Always a bool
    self.b = b  # Always a bool

我有一个名为user_list 的对象列表,并且我想获得有多少对象具有 a == True、a == False、b == True 和 b == False 的频率计数。

我最初的方法是使用 collections.Counter,但这需要在列表中循环两次:

a_count = collections.Counter(u.a for u in user_list)
b_count = collections.Counter(u.b for u in user_list)
print a_count[True], a_count[False], b_count[True], b_count[False]

我也想过只使用 4 个计数器,但这很丑,而且感觉不像 Python:

a_true_count = 0
a_false_count = 0
b_true_count = 0
b_false_count = 0
for u in user_list:
  if u.a:
    a_true_count += 1
  else:
    a_false_count += 1
  if u.b:
    b_true_count += 1
  else:
    a_false_count += 1
print a_true_count, a_false_count, b_true_count, b_false_count

有没有更有效的方法来做到这一点?输出可以是任何东西:4 个单独的变量、一个带有值的 dict、一个列表、元组等等,只要其中包含这 4 个值。

提前致谢!

【问题讨论】:

  • 感谢大家的建议。我在所有解决方案上运行了 100000 次。我会将结果作为 cmets 放在每个答案中。最好的是 Kyle Strand 的 2 个计数器的解决方案,然后从列表长度中减去。一般来说,任何使用 collections.Counter() 的东西都非常慢。上面两个解决方案的运行时间(以秒为单位),以进行比较: Counter() 解决方案:5.78 循环解决方案 w/4 计数器变量:1.16

标签: python list properties frequency


【解决方案1】:

我认为使用collections.Counter 是正确的想法,只需以更通用的方式使用单个Counter 和单个循环即可:

from collections import Counter

user_list = [User(True, False), User(False, True), User(True, True), User(False, False)]
user_attr_count = Counter()

for user in user_list:
    user_attr_count['a_%s' % user.a] += 1
    user_attr_count['b_%s' % user.b] += 1

print user_attr_count
# Counter({'b_False': 2, 'a_True': 2, 'b_True': 2, 'a_False': 2})

【讨论】:

  • 我同意这是一个好主意,但在我的 100000 次运行测试中,它变得非常慢。 collections.Counter() 似乎效率很低。 timeit时间是9.61
【解决方案2】:

为什么不使用两个计数器,并从user_list 的长度中减去以找到其他两个值?

a_false_count = len(user_list) - a_true_count

b_false_count = len(user_list) - b_true_count

这样的显式循环可能是最有效的时间解决方案,但如果您正在寻找更简洁的代码,您可以尝试filter()

a_false_count = len(filter(lambda x: x.a,user_list))
b_false_count = len(filter(lambda x: x.b,user_list))

【讨论】:

  • 您甚至可以使用operator.attrgetter('b') 而不是lambda。这可能会让它走得更快一点。
  • 两个计数器+长度减法是最快的解决方案。谢谢! 100000 次运行时间给了0.89。对于过滤器解决方案,时间是1.62
  • 哇,过滤比上述Counter() 解决方案快3.5 倍以上?这对我来说真的很令人惊讶。感谢您计算数字并分享结果!
  • @Kyle 是的,我今天学到的主要教训是,如果您关心性能,请不要使用 Counter()。 :)
【解决方案3】:
from collections import Counter

c = Counter()
for u in user_list:
    c['a'] += u.a
    c['b'] += u.b

print c['a'], len(user_list) - c['a'], c['b'], len(user_list) - c['b']

【讨论】:

  • 我喜欢这个解决方案,但与其他使用 collections.Counter() 的解决方案一样,它的执行速度很慢。在6.83 运行 100000 次。
【解决方案4】:

您可以使用位掩码:

def count(user_list,mask):
    return Counter((u.a<<1 | u.b)&mask for u in user_list)

a=0b10
b=0b01
aANDb=0b11
print count(user_list,aANDb)

【讨论】:

  • 我认为这个解决方案读起来有点混乱。无论如何,它在4.62 运行 100000 次时只运行了一点点。
【解决方案5】:

我喜欢使用 zipmap 来处理这些东西:

from collections import Counter
# for test, import random:
import random

# define class
class User(object):
  def __init__(self, a, b):
    self.a = a  # Always a bool
    self.b = b  # Always a bool

# create an arbitrary set
users = [ User( r % 2 == 0, r % 3 == 0 ) for r in (random.randint(0,100) for x in xrange(100)) ]

# and... count
aCounter, bCounter = map(Counter, zip(*((u.a, u.b) for u in users)))

更新: map(sum, zip(*tuples)) 在较小的样本量上比 for 循环稍快,但对于较大的样本量,for 循环的扩展性要好得多。. for 循环不会像处理元组列表那样获得太多的性能提升其他方法。可能是因为它已经非常理想了。

collections.Counter 还是很慢。

import random
import itertools
import time
from collections import Counter 

# define class
class User(object):
  def __init__(self, a, b):
    self.a = a  # Always a bool
    self.b = b  # Always a bool

# create an arbitrary sample
users = [ User( r % 2 == 0, r % 3 == 0 ) for r in (random.randint(0,100) for x in xrange(100)) ]
# create a list of tuples of the arbitrary sample
users2 = [ ( u.a,u.b) for u in users ] 

# useful function-timer decorator           
def timer(times=1):
    def outer(fn):
        def wrapper(*args, **kwargs):
            t0 = time.time()
            for n in xrange(times):
                r = fn(*args, **kwargs)
            dt = time.time() - t0
            print '{} ran {} times in {} seconds with {:f} ops/sec'.format(fn.__name__, times, dt, times/dt)
            return r
        return wrapper
    return outer 

# now create the timeable functions         
n=10000
@timer(times=n)
def time_sum():
    return map(sum, zip(*((u.a, u.b) for u in users)))
@timer(times=n)
def time_counter():
    return map(Counter, zip(*((u.a, u.b) for u in users)))
@timer(times=n)
def time_for():
    a,b=0,0
    for u in users:
        if u.a is True:
            a += 1
        if u.b is True:
            b += 1
    return a,b
@timer(times=n)
def time_itermapzip():
    return list(itertools.imap(sum, itertools.izip(*((u.a, u.b) for u in users))))

@timer(times=n)
def time_sum2():
    return map(sum, zip(*users2))
@timer(times=n)
def time_counter2():
    return map(Counter, zip(*users2))
@timer(times=n)
def time_for2():
    a,b=0,0
    for _a,_b in users2:
        if _a is True:
            a += 1
        if _b is True:
            b += 1
    return a,b
@timer(times=n)
def time_itermapzip2():
    return list(itertools.imap(sum, itertools.izip(*users2))) 

v = time_sum()
v = time_counter()
v = time_for()
v = time_itermapzip()

v = time_sum2()
v= time_counter2()
v = time_for2()
v = time_itermapzip2() 

# time_sum ran 10000 times in 0.446894168854 seconds with 22376.662523 ops/sec
# time_counter ran 10000 times in 1.29836297035 seconds with 7702.006471 ops/sec
# time_for ran 10000 times in 0.267076015472 seconds with 37442.523554 ops/sec
# time_itermapzip ran 10000 times in 0.459508895874 seconds with 21762.364319 ops/sec
# time_sum2 ran 10000 times in 0.174293994904 seconds with 57374.323226 ops/sec
# time_counter2 ran 10000 times in 0.989939928055 seconds with  10101.623055 ops/sec
# time_for2 ran 10000 times in 0.183295965195 seconds with 54556.574605 ops/sec
# time_itermapzip2 ran 10000 times in 0.193426847458 seconds with 51699.131384 ops/sec

print "True a's: {}\t False a's: {}\nTrue b's: {}\t False b's:{}".format(v[0], len(users)-v[0], v[1], len(users)-v[1]) 
# True a's: 53   False a's: 47
# True b's: 31   False b's:69
v
# [53, 31]

样本大小为 1000 的相同代码:

# time_sum ran 10000 times in 9.30428719521 seconds with 1074.773359 ops/sec
# time_counter ran 10000 times in 16.7009849548 seconds with 598.767080 ops/sec
# time_for ran 10000 times in 2.61371207237 seconds with 3825.976130 ops/sec
# time_itermapzip ran 10000 times in 9.40824103355 seconds with 1062.897939 ops/sec
# time_sum2 ran 10000 times in 5.70988488197 seconds with 1751.348794 ops/sec
# time_counter2 ran 10000 times in 13.4643371105 seconds with 742.702735 ops/sec
# time_for2 ran 10000 times in 2.49017906189 seconds with 4015.775473 ops/sec
# time_itermapzip2 ran 10000 times in 6.10926699638 seconds with 1636.857581 ops/sec

【讨论】:

  • 就我个人而言,我发现 zip 和 map 乍一看有点难以阅读,所以我不经常使用它们。但是,与使用 collections.Counter() 的其他那些一样,这个在性能方面做得并不好。 100000 timeit 在6.32 运行。如果使用 zip 和 map 重写但没有 Counter 可能会更快。
  • @Chad 这两点都非常有效。我想如果速度是目标,那么您首先不会使用对象实例,并且可能还会使用 itertools 循环。至于 map 和 zip,根据我的经验,我使用的次数越多,它们就越容易阅读,但这不一定是普遍适用的。
  • @Chad 所以我重试了 map(Counter,..), map(sum,...), for... 和 itertools.imap(...) 插入的元组列表对象实例,并且 10000 次在随机的 100 组上运行,对于元组上的 map(sum,...) 执行 0.17 秒。 for 循环对对象来说更快,但 sum 对元组更快。我的猜测是它与 zip 在元组上比对象属性访问在对象实例上更快有关。
【解决方案6】:

这是一个与您第一次得到的解决方案接近的解决方案,只是它只迭代列表一次。它创建两个计数器,遍历列表,并为每个用户更新每个计数器。进行计数的实际步骤在这里:

for user in user_list:
    a_count.update([user.a])
    b_count.update([user.b])

它使用更新函数来更新每个计数器对象。您可以这样做,而不是像在第一个示例中那样使用生成器在一行中创建计数器。完整的代码示例在这里:

import collections

class User(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

user_list = [
    User(True, False),
    User(False, True),
    User(True, True),
    User(False, False)
]

a_count = collections.Counter()
b_count = collections.Counter()

for user in user_list:
    a_count.update([user.a])
    b_count.update([user.b])


print a_count[True], a_count[False], b_count[True], b_count[False]

【讨论】:

  • 从可读性的角度来看,这可能是我最喜欢的解决方案。它又好又干净。不幸的是,性能很糟糕。 :) 低效的 collections.Counter() 和 update() 方法的组合使它在 40.68 运行了 100000 次。
  • 奇怪...你用什么输入?我尝试在那里使用相同的用户列表,但乘以100,000,只用了 3 秒。这对我来说似乎是合理的,因为您一次在内存中构建了大小为 400,000 的整个元素列表。
  • 啊,我看到您使用了 timeit 而不是 unix time 实用程序。好吧,作为基线,我使用timeit 对上面的代码进行计时,但使用了一个空的 user_list。仅仅为此花了 33 秒,所以我不确定更新功能是否真的是代码如此缓慢的原因。据我所知,更新功能非常快。
  • 对于所有示例运行,我使用了一个包含 100 个元素的用户列表,使用 Nisan 的 zip 解决方案中的代码随机生成。这是我运行的代码(除了我缩短了 timeit 数字,因为 ideone 在 5s 执行时间时达到最大值)。 ideone.com/vZ2grN
  • 嗯,我用100,000 试了一下,确实花了很长时间,哈哈。我再次尝试使用 user_list 作为生成器而不是列表(将 user_list 更改为 '( ... stuff ... )' 而不是 '[ ... stuff ... ]')并且 timeit 返回 0.207654953003跨度>
猜你喜欢
  • 2011-03-11
  • 1970-01-01
  • 1970-01-01
  • 2012-07-20
  • 1970-01-01
  • 2022-01-07
  • 2015-08-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多