2013-03-22 41 views
3

我有一個用戶對象,有兩個布爾屬性,就像這樣:Python:在對象列表中獲取兩個布爾屬性頻率的最有效方法?

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

我有這些對象的列表,稱爲user_list,我想要得到多少對象有==頻率計數確實,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個單獨的變量,一個帶有值的字典,一個列表,元組,任何,只要它有4個值。

在此先感謝!

+0

感謝大家的建議。我在所有解決方案上運行了100000次timeit。我會把結果作爲評論在每個答案中。最好的是凱爾斯特蘭德的2個計數器的解決方案,然後從列表長度中減去。一般來說,任何使用collections.Counter()的東西都非常慢。 使用timeit運行(以秒爲單位)我的上述兩種解決方案,進行比較: 計數器()解決方案:'5.78' 循環液W/4計數器瓦爾:'1.16' – Chad 2013-03-22 02:33:22

回答

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)) 
+2

你甚至可以使用'operator.attrgetter(」 b')'而不是'lambda'。這可能會讓它變得更快一點。 – mgilson 2013-03-22 00:09:53

+1

兩個計數器+長度減法是最快的解決方案。謝謝! 100000個timeit運行給出了0.89。對於濾波器解決方案,時間爲1.62' – Chad 2013-03-22 02:36:13

+0

哇,濾波比上述的'Counter()'解決方案快3.5倍以上?這對我來說確實非常令人驚訝。感謝您運行號碼並分享結果! – 2013-03-22 04:36:20

3

我想用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}) 
+0

不錯,不錯的想法 – jamylak 2013-03-22 00:24:19

+0

我同意這是一個好主意,但在我的100000 timeit運行測試中,它變得非常緩慢。 collections.Counter()似乎效率很低。時間是'9.61'。 – Chad 2013-03-22 02:37:45

1

你可以使用位掩碼:

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) 
+0

我認爲這個解決方案有點混亂。無論如何,它只在100000次運行時以'4.62'執行一些緩慢。 – Chad 2013-03-22 02:38:49

1
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'] 
+0

我喜歡這個解決方案,但像其他使用collections.Counter()的表現相當緩慢。 100000個時鐘運行在'6.83'。 – Chad 2013-03-22 02:39:41

1

這裏有一個解決方案,它靠近你有什麼第一次不同的是它僅在列表上循環一次。它創建兩個計數器,遍歷列表,併爲每個用戶更新每個計數器。進行計數的實際步驟如下:

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] 
+0

從可讀性的角度來看,這可能是我最喜歡的解決方案。這是很好,乾淨。不幸的是,表現糟糕透頂。 :)關於低效集合的組合.Counter()和update()方法使它在'40.68'中運行100000個時間。 – Chad 2013-03-22 02:41:15

+0

奇怪......你用什麼輸入?我試着用相同的用戶列表,但乘以「100,000」,只花了3秒鐘。這對我來說似乎是合理的,因爲你一次只是在內存中創建大小爲400,000的元素。 – martega 2013-03-22 07:53:11

+0

啊,我看到你使用'timeit'而不是'unix'time'工具。那麼,就像一個基線一樣,我使用'timeit'來計算上面的代碼,但是用一個空的user_list。這隻需要33秒,所以我不確定更新函數是否真的是代碼太慢的原因。據我所知,更新功能非常快。 – martega 2013-03-22 08:03:43

1

我喜歡用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-loop可以更好地進行縮放。與其他方法一樣,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 
+0

Personally我發現拉鍊和地圖有點難以閱讀,所以我不經常使用它們。就像使用collections.Counter()的其他人一樣,這對於性能來說並不是那麼好。 100000個時間點運行在'6.32'上。如果它使用zip和map重寫,但沒有Counter,它可能會更快。 – Chad 2013-03-22 02:43:07

+1

@Chad這兩點都非常有效。我想如果速度是目標,那麼你不會首先使用對象實例,並且可能還會使用itertools進行循環。至於地圖和zip,根據我的經驗,他們變得更容易閱讀我使用它們的越多,但這不一定是可以概括的。 – 2013-03-22 02:52:34

+1

@Chad所以我重新映射了映射(Counter,..),映射(sum,...),...和itertools.imap(...)在對象實例的元組列表上,並且10000次運行隨機設置的100沒有0。元組上的map(sum,...)爲17s。 for循環對於對象來說更快,但對於元組來說sum更快。我的猜測是,它必須處理元組中的zip比在對象實例上訪問對象屬性更快。 – 2013-03-22 04:15:51

相關問題