Эффективно подсчитывать частоты слов в python
Я хотел бы считать частоты всех слов в текстовом файле.
>>> countInFile('test.txt')
должен возвратить {'aaa':1, 'bbb': 2, 'ccc':1}
если целевой текстовый файл типа:
# test.txt
aaa bbb ccc
bbb
я реализовал его с помощью pure python после некоторые должности. Тем не менее, я обнаружил, что Pure-python недостаточно из-за огромного размера файла (> 1GB).
Я думаю, что власть sklearn заимствования является кандидат.
если вы позволите CountVectorizer считать частоты для каждой строки, Я думаю вы получите частоты слов, суммируя каждый столбец. Но, это звучит немного непрямо.
каков наиболее эффективный и простой способ подсчета слов в файле с python?
обновление
мой (очень медленный) код здесь:
from collections import Counter
def get_term_frequency_in_file(source_file_path):
wordcount = {}
with open(source_file_path) as f:
for line in f:
line = line.lower().translate(None, string.punctuation)
this_wordcount = Counter(line.split())
wordcount = add_merge_two_dict(wordcount, this_wordcount)
return wordcount
def add_merge_two_dict(x, y):
return { k: x.get(k, 0) + y.get(k, 0) for k in set(x) | set(y) }
6 ответов
самый краткий подход-использовать инструменты, которые дает вам Python.
from future_builtins import map # Only on Python 2
from collections import Counter
from itertools import chain
def countInFile(filename):
with open(filename) as f:
return Counter(chain.from_iterable(map(str.split, f)))
вот и все. map(str.split, f)
- это делает генератор, который возвращает list
s слов из каждой строки. Оборачивать в chain.from_iterable
преобразует это в один генератор, который производит слово за раз. Counter
принимает входной iterable и подсчитывает все уникальные значения в нем. В конце концов, ты!--6--> a dict
- подобный объект (a Counter
), что сохраняет все уникальные слова и их количество, и во время создания, вы только магазин строка данных за раз и общее количество, а не весь файл сразу.
теоретически, на Python 2.7 и 3.1, вы могли бы сделать немного лучше цикл над прикованными результатами самостоятельно и с помощью dict
или collections.defaultdict(int)
рассчитывать (потому что Counter
реализован в Python, что может сделать его медленнее в некоторых случаях), но позволяет Counter
сделать работу проще и более самодокументировать (я имею в виду, что вся цель подсчитывает, поэтому используйте Counter
). Помимо этого, на CPython (ссылка переводчика) 3.2 и выше Counter
имеет ускоритель уровня C для подсчета итерационных входов, который будет работать быстрее, чем все, что вы могли бы написать в чистом Python.
обновление: вы, похоже, хотите, чтобы пунктуация была лишена и нечувствительность к регистру, поэтому вот вариант моего более раннего кода, который делает это:
from string import punctuation
def countInFile(filename):
with open(filename) as f:
linewords = (line.translate(None, punctuation).lower().split() for line in f)
return Counter(chain.from_iterable(linewords))
ваш код работает намного медленнее, потому что он создает и уничтожает много маленьких Counter
и set
объектов, а не .update
- ing один Counter
один раз в строке (что, хотя немного медленнее, чем то, что я дал в обновленном блоке кода, было бы, по крайней мере, алгоритмически похоже на коэффициент масштабирования).
память эффективным и точным способом является использование
- CountVectorizer в
scikit
(для извлечения ngram) - NLTK для
word_tokenize
-
numpy
матрица сумма для сбора графов -
collections.Counter
для сбора подсчетов и словарного запаса
пример:
import urllib.request
from collections import Counter
import numpy as np
from nltk import word_tokenize
from sklearn.feature_extraction.text import CountVectorizer
# Our sample textfile.
url = 'https://raw.githubusercontent.com/Simdiva/DSL-Task/master/data/DSLCC-v2.0/test/test.txt'
response = urllib.request.urlopen(url)
data = response.read().decode('utf8')
# Note that `ngram_range=(1, 1)` means we want to extract Unigrams, i.e. tokens.
ngram_vectorizer = CountVectorizer(analyzer='word', tokenizer=word_tokenize, ngram_range=(1, 1), min_df=1)
# X matrix where the row represents sentences and column is our one-hot vector for each token in our vocabulary
X = ngram_vectorizer.fit_transform(data.split('\n'))
# Vocabulary
vocab = list(ngram_vectorizer.get_feature_names())
# Column-wise sum of the X matrix.
# It's some crazy numpy syntax that looks horribly unpythonic
# For details, see http://stackoverflow.com/questions/3337301/numpy-matrix-to-array
# and http://stackoverflow.com/questions/13567345/how-to-calculate-the-sum-of-all-columns-of-a-2d-numpy-array-efficiently
counts = X.sum(axis=0).A1
freq_distribution = Counter(dict(zip(vocab, counts)))
print (freq_distribution.most_common(10))
[выход]:
[(',', 32000),
('.', 17783),
('de', 11225),
('a', 7197),
('que', 5710),
('la', 4732),
('je', 4304),
('se', 4013),
('на', 3978),
('na', 3834)]
по сути, вы также можете сделать это:
from collections import Counter
import numpy as np
from nltk import word_tokenize
from sklearn.feature_extraction.text import CountVectorizer
def freq_dist(data):
"""
:param data: A string with sentences separated by '\n'
:type data: str
"""
ngram_vectorizer = CountVectorizer(analyzer='word', tokenizer=word_tokenize, ngram_range=(1, 1), min_df=1)
X = ngram_vectorizer.fit_transform(data.split('\n'))
vocab = list(ngram_vectorizer.get_feature_names())
counts = X.sum(axis=0).A1
return Counter(dict(zip(vocab, counts)))
давайте timeit
:
import time
start = time.time()
word_distribution = freq_dist(data)
print (time.time() - start)
[выход]:
5.257147789001465
отметим, что CountVectorizer
также можно взять файл вместо строки и tздесь нет необходимости читать весь файл в память. В коде:
import io
from collections import Counter
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
infile = '/path/to/input.txt'
ngram_vectorizer = CountVectorizer(analyzer='word', ngram_range=(1, 1), min_df=1)
with io.open(infile, 'r', encoding='utf8') as fin:
X = ngram_vectorizer.fit_transform(fin)
vocab = ngram_vectorizer.get_feature_names()
counts = X.sum(axis=0).A1
freq_distribution = Counter(dict(zip(vocab, counts)))
print (freq_distribution.most_common(10))
вот некоторые ориентиры. Это будет выглядеть странно, но самый грубый код победит.
[код]:
from collections import Counter, defaultdict
import io, time
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
infile = '/path/to/file'
def extract_dictionary_sklearn(file_path):
with io.open(file_path, 'r', encoding='utf8') as fin:
ngram_vectorizer = CountVectorizer(analyzer='word')
X = ngram_vectorizer.fit_transform(fin)
vocab = ngram_vectorizer.get_feature_names()
counts = X.sum(axis=0).A1
return Counter(dict(zip(vocab, counts)))
def extract_dictionary_native(file_path):
dictionary = Counter()
with io.open(file_path, 'r', encoding='utf8') as fin:
for line in fin:
dictionary.update(line.split())
return dictionary
def extract_dictionary_paddle(file_path):
dictionary = defaultdict(int)
with io.open(file_path, 'r', encoding='utf8') as fin:
for line in fin:
for words in line.split():
dictionary[word] +=1
return dictionary
start = time.time()
extract_dictionary_sklearn(infile)
print time.time() - start
start = time.time()
extract_dictionary_native(infile)
print time.time() - start
start = time.time()
extract_dictionary_paddle(infile)
print time.time() - start
[выход]:
38.306814909
24.8241138458
12.1182529926
размер данных (154MB), используемый в бенчмарке выше:
$ wc -c /path/to/file
161680851
$ wc -l /path/to/file
2176141
обратите внимание:
- С
sklearn
версия, есть накладные расходы на создание векторизатора + манипуляция numpy и преобразование вCounter
объект - затем уроженца
Counter
обновить версию, похоже,Counter.update()
is дорогостоящая операция
этого должно хватить.
def countinfile(filename):
d = {}
with open(filename, "r") as fin:
for line in fin:
words = line.strip().split()
for word in words:
try:
d[word] += 1
except KeyError:
d[word] = 1
return d
пропустить CountVectorizer и scikit-узнать.
файл может быть слишком большим для загрузки в память, но я сомневаюсь, что словарь python становится слишком большим. Самый простой вариант для вас может состоять в том, чтобы разделить большой файл на 10-20 меньших файлов и расширить свой код для цикла над меньшими файлами.
вместо декодирования целых байтов, считанных с url, я обрабатываю двоичные данные. Потому что bytes.translate
ожидает, что его второй аргумент будет строкой байтов, I utf-8 encode punctuation
. После удаления знаков препинания я utf-8 декодирую байтовую строку.
функции freq_dist
ожидает iterable. Вот почему я прошел data.splitlines()
.
from urllib2 import urlopen
from collections import Counter
from string import punctuation
from time import time
import sys
from pprint import pprint
url = 'https://raw.githubusercontent.com/Simdiva/DSL-Task/master/data/DSLCC-v2.0/test/test.txt'
data = urlopen(url).read()
def freq_dist(data):
"""
:param data: file-like object opened in binary mode or
sequence of byte strings separated by '\n'
:type data: an iterable sequence
"""
#For readability
#return Counter(word for line in data
# for word in line.translate(
# None,bytes(punctuation.encode('utf-8'))).decode('utf-8').split())
punc = punctuation.encode('utf-8')
words = (word for line in data for word in line.translate(None, punc).decode('utf-8').split())
return Counter(words)
start = time()
word_dist = freq_dist(data.splitlines())
print('elapsed: {}'.format(time() - start))
pprint(word_dist.most_common(10))
выход;
elapsed: 0.806480884552
[(u'de', 11106),
(u'a', 6742),
(u'que', 5701),
(u'la', 4319),
(u'je', 4260),
(u'se', 3938),
(u'\u043d\u0430', 3929),
(u'na', 3623),
(u'da', 3534),
(u'i', 3487)]
кажется dict
более эффективно, чем