Подтвердить что ты не робот

Эффективно рассчитывать частоты слов в python

Я хотел бы считать частоты всех слов в текстовом файле.

>>> countInFile('test.txt')

должен возвращать {'aaa':1, 'bbb': 2, 'ccc':1}, если целевой текстовый файл похож:

# test.txt
aaa bbb ccc
bbb

Я реализовал его с чистым python после нескольких сообщений. Тем не менее, я обнаружил, что пути pure-python недостаточны из-за огромного размера файла ( > 1 ГБ).

Я думаю, что заимствование силы склеарна является кандидатом.

Если вы подсчитаете частоты CountVectorizer для каждой строки, я думаю, вы получите частоты слов, суммируя каждый столбец. Но это звучит немного косвенно.

Каков наиболее эффективный и простой способ подсчета слов в файле с помощью python?

Update

Мой (очень медленный) код находится здесь:

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) }
4b9b3361

Ответ 1

Самый сжатый подход - использовать инструменты, предоставляемые 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 слов из каждой строки. Обтекание в chain.from_iterable преобразует его в один генератор, который генерирует слово за раз. Counter выполняет ввод итерации и подсчитывает в нем все уникальные значения. В конце вы return 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 - один одиночный Counter один раз в строке (который, хотя и немного медленнее, чем я дал в обновленном кодовом блоке, по крайней мере, алгоритмически схожи по коэффициенту масштабирования).

Ответ 2

Эффективный и точный способ хранения памяти - использовать

  • 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 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))

Ответ 3

Этого должно быть достаточно.

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

Ответ 4

Вот несколько эталонных тестов. Это будет выглядеть странно, но побеждает самый грубый код.

[код]:

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

Размер данных (154 МБ), используемый в вышеприведенном тесте:

$ wc -c /path/to/file
161680851

$ wc -l /path/to/file
2176141

Некоторые примечания:

  • С версией sklearn существуют накладные расходы на создание векторизации + манипуляция и преобразование numpy в объект Counter
  • Тогда версия native Counter, похоже, Counter.update() - дорогостоящая операция

Ответ 5

Пропустить CountVectorizer и scikit-learn.

Файл может быть слишком большим для загрузки в память, но я сомневаюсь, что словарь python становится слишком большим. Самый простой вариант для вас - разбить большой файл на 10-20 файлов меньшего размера и расширить ваш код, чтобы перебрать более мелкие файлы.

Ответ 6

Вместо того, чтобы декодировать все байты, считанные с url, я обрабатываю двоичные данные. Поскольку bytes.translate ожидает, что его второй аргумент будет байтовой строкой, я utf-8 закодирует punctuation. После удаления пунктуаций, я utf-8 декодирует строку байта.

Функция freq_dist ожидает итерации. Вот почему я прошел 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 более эффективен, чем объект Counter.

def freq_dist(data):
    """
    :param data: A string with sentences separated by '\n'
    :type data: str
    """
    d = {}
    punc = punctuation.encode('utf-8')
    words = (word for line in data for word in line.translate(None, punc).decode('utf-8').split())
    for word in words:
        d[word] = d.get(word, 0) + 1
    return d

start = time()
word_dist = freq_dist(data.splitlines())
print('elapsed: {}'.format(time() - start))
pprint(sorted(word_dist.items(), key=lambda x: (x[1], x[0]), reverse=True)[:10])

Выход;

elapsed: 0.642680168152

[(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)]

Чтобы увеличить эффективность памяти при открытии огромного файла, вам нужно передать только открытый URL. Но время будет также включать время загрузки файла.

data = urlopen(url)
word_dist = freq_dist(data)