Compter efficacement les fréquences de mots en python
j'aimerais compter les fréquences de tous les mots dans un fichier texte.
>>> countInFile('test.txt')
doit retourner {'aaa':1, 'bbb': 2, 'ccc':1}
si le fichier cible est comme:
# test.txt
aaa bbb ccc
bbb
Je l'ai implémenté avec pur python après quelques messages . Cependant, j'ai découvert que les moyens python purs sont insuffisants en raison de la taille énorme du fichier (> 1 Go).
je pense qu'emprunter le pouvoir de sklearn est un candidat.
Si vous laissez CountVectorizer compter les fréquences pour chaque ligne, je suppose que vous obtiendrez des fréquences de mots en résumant chaque colonne. Mais ça semble un peu indirect.
Quelle est la manière la plus efficace et simple de compter les mots dans un fichier avec python?
mise à Jour
mon code (très lent) est ici:
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 réponses
l'approche la plus succincte est d'utiliser les outils que Python vous donne.
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)))
C'est ça. map(str.split, f)
fabrique un générateur qui retourne list
s de mots de chaque ligne. L'enroulement dans chain.from_iterable
convertit cela en un seul générateur qui produit un mot à la fois. Counter
prend une entrée itérable et compte de toutes les valeurs uniques. À la fin, vous return
un dict
-comme l'objet (un Counter
) qui stocke tous les mots uniques et leurs comptes, et pendant la création, vous ne stockez qu'une ligne de données à la fois et les comptes totaux, pas l'ensemble du fichier à la fois.
en théorie, sur Python 2.7 et 3.1, vous pourriez faire un peu mieux boucle sur les résultats enchaînés vous-même et en utilisant un dict
ou collections.defaultdict(int)
à compter (parce que Counter
est mis en œuvre en Python, ce qui peut le rendre plus lent dans certains cas), mais laisser Counter
faire le travail est plus simple et plus auto-documentée (je veux dire, le but entier compte, alors utilisez un Counter
). Au-delà de cela, sur CPython (l'interpréteur de référence) 3.2 et supérieur Counter
dispose d'un accélérateur de niveau C pour compter les entrées itérables qui seront exécutées plus rapidement que tout ce que vous pourriez écrire en Python pur.
mise à Jour: Vous semblez vouloir ponctuation dépouillé et l'insensibilité à la casse, donc voici une variante de mon code précédent qui fait que:
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))
votre code fonctionne beaucoup plus lentement parce qu'il crée et détruit beaucoup de petits Counter
et set
, plutôt que .update
-ing un seul Counter
une fois par ligne (ce qui, bien que légèrement plus lent que ce que j'ai donné dans le bloc de code mis à jour, serait au moins algorithmiquement similaire en facteur d'échelle).
un moyen efficace et précis de mémoire est de faire usage de
- CountVectorizer dans
scikit
(pour ngram extraction) - NLTK pour
word_tokenize
-
numpy
matrice de la somme à recueillir des comtes -
collections.Counter
pour la collecte des comptes et du vocabulaire
un exemple:
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 /q/numpy-matrix-to-array-1903/"""
: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)))
nous allons timeit
:
import time
start = time.time()
word_distribution = freq_dist(data)
print (time.time() - start)
[out]:
5.257147789001465
notez que CountVectorizer
peut également prendre un fichier au lieu d'une chaîne de caractères et t ici, pas besoin de lire le fichier entier dans la mémoire . En code:
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))
voici quelques repères. Ça va paraître étrange, mais le code le plus grossier gagne.
[code]:
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
[out]:
38.306814909
24.8241138458
12.1182529926
taille des Données (154MB) utilisé dans la référence ci-dessus:
$ wc -c /path/to/file
161680851
$ wc -l /path/to/file
2176141
Certaines choses sont à noter:
- avec la version
sklearn
, Il ya un rétroprojecteur de création vectorizer + numpy manipulation et la conversion en unCounter
objet - puis native
Counter
mise à jour de la version, il semble queCounter.update()
est une opération coûteuse
cela devrait suffire.
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
Skip CountVectorizer et scikit-learn.
Le fichier est trop grand pour charger en mémoire, mais je doute que le python dictionnaire devient trop grande. L'option la plus facile pour vous peut être de diviser le grand fichier en 10-20 plus petits fichiers et étendre votre code à boucle sur les plus petits fichiers.
au lieu de décoder les Octets entiers lus à partir de l'url, je traite les données binaires. Parce que bytes.translate
s'attend à ce que son second argument soit une chaîne de octets, I utf-8 encodera punctuation
. Après avoir enlevé les ponctuations, I utf-8 décoder la chaîne byte.
la fonction freq_dist
s'attend à un itérable. C'est pour ça que j'ai passé 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))
sortie;
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)]
il semble dict
est plus efficace que l'objet 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])
sortie;
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)]
pour être plus efficace de mémoire lors de l'ouverture de fichier énorme, vous devez passer juste l'url ouverte. Mais le timing inclura aussi le temps de téléchargement des fichiers.
data = urlopen(url)
word_dist = freq_dist(data)