web-dev-qa-db-fra.com

Compter efficacement les fréquences des mots en python

J'aimerais compter les fréquences de tous les mots dans un fichier texte.

>>> countInFile('test.txt')

devrait renvoyer {'aaa':1, 'bbb': 2, 'ccc':1} si le fichier texte cible ressemble à ceci:

# test.txt
aaa bbb ccc
bbb

Je l'ai implémenté avec du python pur après quelques posts . Cependant, j'ai découvert que les méthodes en python pur étaient insuffisantes en raison de la taille énorme du fichier (> 1 Go).

Je pense que le pouvoir d'emprunter Sklearn est un candidat.

Si vous laissez CountVectorizer compter les fréquences pour chaque ligne, je suppose que vous obtiendrez les fréquences Word en additionnant chaque colonne. Mais cela semble un peu indirect.

Quel est le moyen le plus efficace et le plus simple de compter les mots dans un fichier avec Python?

Mettre à 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) }
27
rkjt50r983

L’approche la plus succincte consiste à utiliser les outils que Python vous fournit.

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 tout. map(str.split, f) crée un générateur qui renvoie lists de mots de chaque ligne. L'emballage dans chain.from_iterable le convertit en un seul générateur qui produit un mot à la fois. Counter prend une entrée itérable et compte toutes ses valeurs uniques. À la fin, vous return un objet ressemblant à dict (un Counter) qui stocke tous les mots uniques et leur nombre, et lors de la création, vous ne stockez qu'une ligne de données à la fois et le nombre total, et non le fichier entier à la fois.

En théorie, sur Python 2.7 et 3.1, vous pouvez légèrement mieux boucler vous-même sur les résultats chaînés et utiliser dict ou collections.defaultdict(int) pour compter (car Counter est implémenté dans Python, ce qui peut le ralentir dans certains cas), mais laissez Counter choisir le travail est plus simple et s'auto-documente (je veux dire, l'objectif entier compte, utilisez donc 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 pouvant être itératives, qui s'exécutera plus rapidement que tout ce que vous pourriez écrire en Python pur.

Mise à jour: Vous semblez vouloir supprimer la ponctuation et l'insensibilité à la casse, voici donc une variante de mon code précédent qui le fait:

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 est beaucoup plus lent, car il crée et détruit de nombreux petits objets Counter et set, plutôt que .update- une seule Counter une fois par ligne (ce qui, même s'il est légèrement plus lent que ce que j'ai indiqué dans le bloc de code mis à jour, serait au moins algorithmiquement facteur d'échelle similaire).

35
ShadowRanger

Une méthode efficace et efficace pour la mémoire consiste à utiliser 

  • CountVectorizer in scikit (pour l'extraction de ngram)
  • NLTK pour Word_tokenize
  • numpy somme somme pour collecter les comptes
  • collections.Counter pour la collecte des chiffres 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 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))

[en dehors]:

[(',', 32000),
 ('.', 17783),
 ('de', 11225),
 ('a', 7197),
 ('que', 5710),
 ('la', 4732),
 ('je', 4304),
 ('se', 4013),
 ('на', 3978),
 ('na', 3834)]

Essentiellement, vous pouvez aussi faire ceci:

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)

[en dehors]:

5.257147789001465

Notez que CountVectorizer peut également prendre un fichier à la place d'une chaîne et tn'est pas nécessaire de lire le fichier entier en mémoire. Dans du 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))
8
alvas

Voici un point de repère. Ç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

[en dehors]:

38.306814909
24.8241138458
12.1182529926

Taille de données (154 Mo) utilisée dans le repère ci-dessus:

$ wc -c /path/to/file
161680851

$ wc -l /path/to/file
2176141

Quelques points à noter:

  • Avec la version sklearn, il y a une surcharge de création de vectoriseur + manipulation de numpy et conversion en un objet Counter
  • Puis la version de mise à jour Counter native, il semble que Counter.update() soit une opération coûteuse
3
nat gillin

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
Goodies

vous pouvez essayer avec sklearn

from sklearn.feature_extraction.text import CountVectorizer
    vectorizer = CountVectorizer()

    data=['i am student','the student suffers a lot']
    transformed_data =vectorizer.fit_transform(data)
    vocab= {a: b for a, b in Zip(vectorizer.get_feature_names(), np.ravel(transformed_data.sum(axis=0)))}
    print (vocab)
0
Murtadha Alrahbi

Au lieu de décoder les octets entiers lus à partir de l'URL, je traite les données binaires. Étant donné que bytes.translate s'attend à ce que son deuxième argument soit une chaîne d'octets, I utf-8 code punctuation. Après avoir enlevé les ponctuations, j'ai utf-8 décoder la chaîne d'octets. 

La fonction freq_dist attend un itérable. C'est pourquoi 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 que dict soit plus efficace que Counter object. 

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 en termes de mémoire lors de l’ouverture d’un fichier de grande taille, vous devez uniquement transmettre l’URL ouverte. Mais le timing inclura également le temps de téléchargement de fichier.

data = urlopen(url)
Word_dist = freq_dist(data)
0
Nizam Mohamed

Sautez CountVectorizer et scikit-learn.

Le fichier est peut-être trop volumineux pour être chargé en mémoire, mais je doute que le dictionnaire python devienne trop volumineux. L'option la plus simple pour vous consiste peut-être à diviser le fichier volumineux en 10 à 20 fichiers plus petits et à étendre votre code pour qu'il passe en boucle par-dessus les fichiers plus petits.

0
Stephen Grimes