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) }
23
demandé sur Community 2016-03-08 04:52:21

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

33
répondu ShadowRanger 2017-10-27 20:42:51

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))
6
répondu alvas 2016-10-06 07:22:42

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 un Counter objet
  • puis native Counter mise à jour de la version, il semble que Counter.update() est une opération coûteuse
3
répondu nat gillin 2016-10-06 08:59:16

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
2
répondu Goodies 2016-03-08 02:14:34

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.

0
répondu Stephen Grimes 2016-03-08 02:10:43

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)
0
répondu Nizam Mohamed 2016-03-13 19:52:46