我已经用不同的方式玩(在Python 2.7中),从语料库或字符串列表中提取(单词,频率)元组列表,并比较其效率.据我所知,在正常情况下,一个未排序的列表,收集模块中的Countermethod优于任何我在别处找到的东西,但似乎并没有太多优势一个预先排序的列表,我已经提出了在这种特殊情况下很容易击败的方法.那么简而言之,有没有什么内置的方式来通知柜台呢,这个事实呢已经列出了一个列表来进一步加速呢?
(接下来的部分是Counter的工作是非法的,你可能希望跳到最后,在处理排序列表时失去它的魅力.)
未排序的输入列表
一种不行的方法
天真的方法是使用sorted([(word,corpus.count(word))在集合(语料)]中的单词),但是一旦你的语料库是几千个项目长的话,那个可靠的可靠地让你进入运行时问题 – 毫不奇怪,因为你遍历了n个单词的整个列表很多次,其中m是唯一的单词的数量.
排序列表本地搜索
所以我试图做的事情之前,我发现计数器是确保所有的搜索是严格本地的,首先排序输入列表(我也必须删除数字和标点符号,并将所有条目转换为小写,以避免重复像“foo”,’Foo’和’foo:’).
#Natural Language Toolkit,for access to corpus; any other source for a long text will do,though. import nltk # nltk corpora come as a class of their own,as I udnerstand it presenting to the # outside as a unique list but underlyingly represented as several lists,with no more # than one ever loaded into memory at any one time,which is good for memory issues # but rather not so for speed so let's disable this special feature by converting it # back into a conventional list: corpus = list(nltk.corpus.gutenberg.words()) import string drop = string.punctuation+string.digits def wordcount5(corpus,Case=False,lower=False,StrippedSorted=False): '''function for extracting word frequencies out of a corpus. Returns an alphabetic list of tuples consisting of words contained in the corpus with their frequencies. Default is case-insensitive,but if you need separate entries for upper and lower case spellings of the same words,set option Case=True. If your input list is already sorted and stripped of punctuation marks/digits and/or all lower case,you can accelerate the operation by a factor of 5 or so by declaring so through the options "Sorted" and "lower".''' # you can ignore the following 6 lines for now,they're only relevant with a pre-processed input list if lower or Case: if StrippedSorted: sortedc = corpus else: sortedc = sorted([word.replace('--',' ').strip(drop) for word in sorted(corpus)]) # here we sort and purge the input list in the default case: else: sortedc = sorted([word.lower().replace('--',' ').strip(drop) for word in sorted(corpus)]) # start iterating over the (sorted) input list: scindex = 0 # create a list: freqs = [] # identify the first token: currentword = sortedc[0] length = len(sortedc) while scindex < length: wordcount = 0 # increment a local counter while the tokens == currentword while scindex < length and sortedc[scindex] == currentword: scindex += 1 wordcount += 1 # store the current word and final score when a) a new word appears or # b) the end of the list is reached freqs.append((currentword,wordcount)) # if a): update currentword with the current token if scindex < length: currentword = sortedc[scindex] return freqs
查找收藏
这是更好的,但仍然不如使用集合模块中的Counter类,它创建一个{word:word}的条目的字典(我们仍然要做同样的剥离和降低,但没有排序) :
from collections import Counter cnt = Counter() for word in [token.lower().strip(drop) for token in corpus]: cnt[word] += 1 # optionally,if we want to have the same output format as before,# we can do the following which negligibly adds in runtime: wordfreqs = sorted([(word,cnt[word]) for word in cnt])
在古腾堡语料库计数器方法在我的机器上快了30%(5秒,而不是7.2),这主要通过排序例程来解释,这个程序吃了大约2.1秒(如果你没有,不想安装nltk包(自然语言工具包),它提供对这个语料库的访问,任何其他足够长的文本被正确分割成词级的字符串列表将显示相同的信息)
比较性能
以我的特殊方法使用重言式作为延迟执行的条件,这给了我们计数器的方法:
import time >>> if 1: ... start = time.time() ... cnt = Counter() ... for word in [token.lower().strip(drop) for token in corpus if token not in [" ",""]]: ... cnt[word] += 1 ... time.time()-start ... cntgbfreqs = sorted([(word,cnt[word]) for word in cnt]) ... time.time()-start ... 4.999882936477661 5.191655874252319
(我们看到,将结果格式化为元组列表的最后一步占用时间的不到5%).
与我的功能相比:
>>> if 1: ... start = time.time() ... gbfreqs = wordcount5(corpus) ... time.time()-start ... 7.261770963668823
排序的输入列表 – 当计数器“失败”
但是,您可能已经注意到,我的功能允许指定输入已经被排序,剥离了标点符号垃圾,并转换为小写.如果我们已经为某些其他操作创建了这样的转换版本,使用它(并声明如此)可以极大地加速我的wordcount5的操作:
>>> sorted_corpus = sorted([token.lower().strip(drop) for token in corpus if token not in [" ",""]]) >>> if 1: ... start = time.time() ... strippedgbfreqs2 = wordcount5(sorted_corpus,lower=True,StrippedSorted=True) ... time.time()-start ... 0.9050078392028809
在这里,我们已经将运行时减少了一个因子. 8通过不必排序语料库并转换项目.当然,后者在使用这个新列表的计数器时也是如此,所以期望它也有点快一些,但是似乎没有利用它被排序的事实,现在需要两倍于我的功能,比以前快30%
>>> if 1: ... start = time.time() ... cnt = Counter() ... for word in sorted_corpus: ... cnt[word] += 1 ... time.time()-start ... strippedgbfreqs = [(word,cnt[word])for word in cnt] ... time.time()-start ... 1.9455058574676514 2.0096349716186523
当然,我们可以使用与wordcount5中使用相同的逻辑 – 递增一个本地计数器,直到我们遇到一个新的单词,然后才把最后一个单词与计数器的当前状态一起存储,并将该计数器复位为0表示下一个单词 – 只使用Counter作为存储,但Counter方法的固有效率似乎丢失了,性能在我的功能的范围内,用于创建一个字典,转换成一个元组列表的额外负担现在看起来比使用更麻烦当我们处理原始语料库时:
>>> def countertest(): ... start = time.time() ... sortedcnt = Counter() ... c = 0 ... length = len(sorted_corpus) ... while c < length: ... wcount = 0 ... word = sorted_corpus[c] ... while c < length and sorted_corpus[c] == word: ... wcount+=1 ... c+=1 ... sortedcnt[word] = wcount ... if c < length: ... word = sorted_corpus[c] ... print time.time()-start ... return sorted([(word,sortedcnt[word]) for word in sortedcnt]) ... print time.time()-start ... >>> strippedbgcnt = countertest() 0.920727014542 1.08029007912
(结果的相似性并不是非常令人惊讶,因为我们实际上禁用了Counter自己的方法,并将其滥用作为使用与以前相同的方法获得的值的存储.)
因此,我的问题是:有更通用的方式来通知计数器,它的输入列表已经被排序,并使它将当前的密钥保存在内存中,而不是每次重新查找 – 可预测的 – 遇到下一个令牌同一个字?换句话说,是否可以通过将计数器/字典类的固有效率与排序列表的明显好处相结合来进一步提高预排序列表的性能,或者我已经在0.9秒的硬限制上用于统计2M条目的列表?
可能没有很大的改进空间 – 在做最简单的事情时,我可以想到的时间在55秒左右,仍然需要遍历相同的列表并检查每个单独的值,而.25为set(语料库)没有计数,但也许有一些itertools魔术在那里有助于接近这些数字?
(注意:我是Python的一个相对新手,一般来说是编程,所以如果我错过了一些明显的事情,那么可以借口)
编辑12月1日:
另外一件事,除了排序本身,这使我的所有方法都比较慢,将2M字符串中的每一个字符串转换为小写,并剥离它们可能包含的任何标点符号或数字.我以前尝试过快捷方式,通过计算未处理的字符串,然后转换结果并删除重复,同时添加他们的计数,但我必须做错了,它使事情变得如此慢一些.因此,我恢复到以前的版本,转换原始语料库中的所有内容,现在不能完全重建我在那里做的.
如果我现在尝试,我会改进转换字符串最后.我仍然通过循环列表(结果)来完成.我所做的是写出几个函数,它们之间转换JF Sebastian的获胜的default_dict方法(格式为[(“word”,int),(“Word”,int)],(“word2”)的输出中的键,…])变成小写并剥离它们的标点符号,并折叠在该操作之后保持相同的所有键的计数(代码如下).优点是我们现在正在处理大约5万个条目的列表,而不是> 2M在语料库中.这样我现在在1.25秒内从语料库(作为一个列表)到一个不区分大小写的字数,忽略我的机器上的标点符号,从大约4.5,以Counter方法和字符串转换为第一步.但也可能有一个基于字典的方法,我在sum_sorted()中做什么呢?
码:
def striplast(resultlist,lower_or_Case=False): """function for string conversion of the output of any of the `count_words*` methods""" if lower_or_Case: strippedresult = sorted([(entry[0].strip(drop),entry[1]) for entry in resultlist]) else: strippedresult = sorted([(entry[0].lower().strip(drop),entry[1]) for entry in resultlist]) strippedresult = sum_sorted(strippedresult) return strippedresult def sum_sorted(inputlist): """function for collapsing the counts of entries left identical by striplast()""" ilindex = 0 freqs = [] currentword = inputlist[0][0] length = len(inputlist) while ilindex < length: wordcount = 0 while ilindex < length and inputlist[ilindex][0] == currentword: wordcount += inputlist[ilindex][1] ilindex += 1 if currentword not in [""," "]: freqs.append((currentword,wordcount)) if ilindex < length and inputlist[ilindex][0] > currentword: currentword = inputlist[ilindex][0] return freqs def count_words_defaultdict2(words,loc=False): """modified version of J.F. Sebastian's winning method,added a final step collapsing the counts for words identical except for punctuation and digits and case (for the latter,unless you specify that you're interested in a case-sensitive count by setting l(ower_)o(r_)c(ase) to True) by means of striplast().""" d = defaultdict(int) for w in words: d[w] += 1 if col=True: return striplast(sorted(d.items()),lower_or_case=True) else: return striplast(sorted(d.items()))
我做了一些首先尝试使用groupy来完成sum_sorted()和/或striplast()所做的工作,但是我无法完全弄清楚如何把它引入到[entry [1]]的列表中count_words结果中的条目按条目[0]排序.我最接近的是:
# "i(n)p(ut)list",toylist for testing purposes: list(groupby(sorted([(entry[0].lower().strip(drop),entry[1]) for entry in iplist]))) # returns: [(('a',1),<itertools._grouper object at 0x1031bb290>),(('a',2),<itertools._grouper object at 0x1031bb250>),3),<itertools._grouper object at 0x1031bb210>),5),<itertools._grouper object at 0x1031bb2d0>),8),<itertools._grouper object at 0x1031bb310>),(('b',<itertools._grouper object at 0x1031bb350>),7),<itertools._grouper object at 0x1031bb390>)] # So what I used instead for striplast() is based on list comprehension: list(sorted([(entry[0].lower().strip(drop),entry[1]) for entry in iplist])) # returns: [('a',('a',('b',7)]
解决方法
from itertools import groupby some_data = ['a','a','b','c','c'] count = dict( (k,sum(1 for i in v)) for k,v in groupby(some_data) ) # or count = {k:sum(1 for i in v) for k,v in groupby(some_data)} # {'a': 2,'c': 3,'b': 1}