Nombre d'occurences d'un mot dans un texte en Python

Posté en december 2008


Juste un petit snippet car j'en ai eu besoin récemment pour faire des statistiques sur des termes recherchés et je pense que ça peut être utile :

from itertools import groupby

def word_frequencies(content, blacklist):
    """
    Count the number of words in a content, excluding blacklisted terms.
    Return a generator of tuples (count, word) sorted by descending frequency.

    Example::

        >>> song = 'Ob la di ob la da "rla di da" da "da"'
        >>> for count, word in word_frequencies(song, ['di']):
        ...     print "%s %s" % (count, word)
        ...
        4 da
        2 la
        2 ob
        1 rla
    """
    sorted_words = sorted(word \
                        for word in content.lower().replace('"', '').split() \
                            if word not in blacklist)
    return ((len(list(group)), word) for word, group in groupby(sorted_words))

if __name__ == "__main__":
    import doctest
    doctest.testmod(verbose=True)

À adapter selon votre convenance, si vous avez mieux je suis preneur, comme toujours.


17 Commentaires

en perl, on a droit ?

(my $string = 'Ob la di ob la da "rla di da" da "da"' ) =~ s/"//g;
my %hash;
map { $hash{$_} = (defined $hash{$_}) ? 1 : $hash{$_} + 1; } (split(" ",$string);
map { print "$_ -> $hash{$_}\n"; } (keys %hash);

1 | kim, le 6 December 2008 à 17h

kim: voilà qui illustre bien la différence de lisibilité entre le python et le perl ;)

2 | maxime, le 6 December 2008 à 18h

@kim : tiens ça me fait penser que je pourrais effectivement virer les " dès le début. Ça me fait aussi penser que perl tiens bien sa réputation ;-).

3 | David, biologeek, le 6 December 2008 à 18h

@tous les deux : en même temps, j'ai fait exprès de "compresser le code"... M'enfin je trouve le perl plus lisible que le python, question d'habitude :)
Cela dit je remarque que j'ai oublié le blacklist et le lowercase. Donc voilà pareil, en reprenant le principe de ton code python (sort + j'ai ajouté lc & blacklist), et sans hash remplie au fur et à mesure :

(my $string = lc('Ob la di ob la da "rla di da" da "da"') ) =~ s/"//g;
my @string = sort(split(" ",$string));
my ($last,%hash) = ("",);
foreach @string {
next if($last eq $_);
next if(!grep {/^$_$/} @blacklist);
$last = $_;
$hash{$_} = scalar(grep { /^$_$/ } @string);
}
# un peu d'affichage si on veut :
#map { print "$_ -> $hash{$_}\n" } (keys %hash);
return %hash;

Après, l'utilisation de sort, en perl comme en python, est discutable. Est-ce que trier puis faire un groupby est meilleur en temps & mémoire, par rapport à un stockage "au fil de la lecture" (donc, pas de sort). Dans ton exemple, le texte est court, quid de si on fait le même test sur le texte du code civil par exemple ? ça pourrait être intéressant de faire des tests sur le sujet ;) Je pense que ce genre de questions peut facilement rendre le code un moins "oneline" :)

4 | kim, le 6 December 2008 à 19h

@kim, même sans la compression c'est ingérable du code comme ça. dans 6 mois tu retournes dans ta fonction, il te faudra trop de temps pour savoir ce qu'elle fait.

Avant j'aimais bien le perl mais qd on a goûté à la facilité du python, c'est comme une drogue.

5 | Jdoe, le 7 December 2008 à 00h

hum hum !

Si je n'ai pas mal compris la doc string de ta fonction :

"""
Compte le nombre d'ocuurences des mots contenus dans un texte (exceptés ceux présents dans la black list).

Retourne un générateur de tuples classés par ordre décroissant de fréquences
"""

Alors ton snipet est buggué dans le sens où il retourne les tuples classés par ordre alphabétique des mots, et non comme indiqué.

Un heureux hasard, trompe le lecteur car ta chanson de départ contient 4 "da".

remplace les par "za"

>>> song = 'Ob la di ob la za "rla di za" za "za"'

Failed example:
for count, word in word_frequencies(song, ['di']):
print "%s %s" % (count, word)
Expected:
4 za
2 la
2 ob
1 rla
Got:
2 la
2 ob
1 rla
4 za
1 items had no tests:
__main__
**********************************************************************
1 items had failures:
1 of 2 in __main__.word_frequencies
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

Je laisse aux autres lecteurs le soin de corriger le code à titre d'exercice :)

@++

6 | Jean-Philippe Camguilhem, le 7 December 2008 à 10h

@jdoe : bon on va éviter de rentrer dans le troll de bas étage :) mais même sans commentaire, je trouve le code lisible et maintenable. Après, si ce n'est qu'une question de commentaire, c'était voulu, David les avait mis dans son code, je vais pas les *rappeler*.

J'ai curieusement fait exactement l'inverse de toi, j'aimais bien le perl, j'ai goûté au python pendant 6 mois dans le cadre d'un job, hé ben j'en suis revenu au perl pour trois raisons tout à fait subjectives :
* plus souple
* plus agréable
* on n'a pas à s'emmerder avec l'indentation. Et là, par contre, c'est une plaie à mon sens : j'ai récupéré du code modifié par deux personnes, on retrouvait des tabulations, des espaces, c'est super lourd à maintenir (sans compter les copier/coller qu'il faut systématiquement réindenter : le python *nécessite* un éditeur de texte adapté, ce qui n'est pas normal à mon sens).

Après, je suis d'accord que "à lire", le python peut paraître plus agréable (bon, peut être pas dans l'exemple ci dessus parce qu'il se trouve que le code est *aussi* condensé et on arrive à deux lignes de code contenant au total une douzaine d'instruction...)

7 | kim, le 7 December 2008 à 10h

en me e temps ce pourrait être plus somple en python :

>>> songs = 'Ob la di ob la da "rla di da" da "da"'
>>> lsongs = [song.replace('"', '').lower() for song in songs.split()]
>>> freqs = [(- lsongs.count(song), song) for song in set(lsongs)]
>>> print "\n".join("%-10s : %s" % (n, -f) for f, n in sorted(freqs))
da : 4
di : 2
la : 2
ob : 2
rla : 1

8 | benoitc, le 7 December 2008 à 10h

autre possibilité :

>>> songs = 'Ob la di ob la da "rla di da" da "da"'
>>> lsongs = [song.replace('"', '').lower() for song in songs.split()]
>>> freqs = [lsongs.count(song) for song in lsongs]
>>> dict(zip(lsongs, freqs))
{'da': 4, 'di': 2, 'rla': 1, 'ob': 2, 'la': 2}

perso dans ls 2 cas je trouve le python plus lisible que le perl .... les $_ me semblant pas naturel (surtout pour un français). Pour les badwords il suffit de mettre un son in songs.split() and not in badwords .

9 | benoitc, le 7 December 2008 à 10h

Bon benoitc n'a pas trainé pour corriger.

J'aime beaucoup sa façon originale de trier de façon inverse en passant par des valeurs négatives dans sa première proposition.

J'en profite cependant pour placer une méthode souvent méconnue pour les tris de listes à plusieurs dimensions.

sorted tri sur le premier élément, mais on peut lui demander de trier sur un autre élément via operator :

>>> import operator
>>> words_frequencies=(('za', 4), ('rla', 1), ('la', 2), ('ob', 2))
>>> print sorted(words_frequencies, key=operator.itemgetter(1), reverse=True)
[('za', 4), ('la', 2), ('ob', 2), ('rla', 1)]

@++

@kim pour connaître la réponse au match perl vs python
il faudra repasser vendredi sur cette url :)
http://jp.camguilhem.net/?user=kim&cool=perl&bad=python

10 | jean-Philippe Camguilhem, le 7 December 2008 à 11h

Je n'utilis epas svt operator, une autre solution si on veut trier :

>>> songs = 'Ob la di ob la da "rla di da" da "da"'
>>> lsongs = [song.replace('"', '').lower() for song in songs.split()]
>>> freqs = [lsongs.count(song) for song in set(lsongs)]
>>> a = zip(lsongs,freqs)
>>> a.sort(lambda a,b: cmp(a[1],b[1]))
>>> a
[('di', 1), ('la', 2), ('ob', 2), ('la', 2), ('ob', 4)]

11 | benoitc, le 7 December 2008 à 11h

Bon suite à un question de Kael qui m'a titillé j'allais poser ici l'algo en erlang quand j'ai vu que mon dernier exemple était faux. Ceci est plus correct :

>>> songs = 'Ob la di ob la da "rla di da" da "da"'
>>> lsongs = [song.replace('"', '').lower() for song in songs.split()]
>>> freqs = [lsongs.count(song) for song in lsongs]
>>> a = dict(zip(lsongs, freqs))
>>> a
{'da': 4, 'di': 2, 'rla': 1, 'ob': 2, 'la': 2}
>>> items = a.items()
>>> items.sort(lambda a, b: cmp(a[1], b[1]))
>>> items
[('rla', 1), ('di', 2), ('ob', 2), ('la', 2), ('da', 4)]

et reverse pour l'inverse...

En erlang cela donne :

1> S = "Ob la di ob la da \"rla di da\" da \"da\"",
1> Map = lists:map(fun(Word) -> {string:to_lower(Word),1} end, string:tokens(S, " \"")),
1> Result = lists:foldl(fun({Word,_}, Dict) ->
1> case dict:is_key(Word, Dict) of
1> true -> dict:store(Word, dict:fetch(Word, Dict) + 1, Dict);
1> false -> dict:store(Word, 1, Dict)
1> end
1> end, dict:new(), Map),
1> dict:fold(fun(Word, Freq, Acc) -> [{Word,Freq}|Acc] end, [], Result).
[{"da",4},{"rla",1},{"ob",2},{"di",2},{"la",2}]

http://friendpaste.com/Hy6KzphN

(mis sur friendpaste car ton système n'accepte pas les "+ 1" dans les posts!)

12 | benoitc, le 7 December 2008 à 19h

En php :
<?php
$song = 'Ob la di ob la da "rla di da" da "da"';
$res = array();
foreach (preg_split('/\W/i', $song) as $w)
if(!empty($w)) ++$res[$w];
natsort($res);
var_export($res);
?>
Résultat :
array (
'Ob' => 1,
'rla' => 1,
'ob' => 1,
'di' => 2,
'la' => 2,
'da' => 4,
)
Avec array_reverse($res, true) pour le résultat inverse.

Après le choix du language, c'est bien souvent une question de goût :)

13 | scar, le 8 December 2008 à 14h

Sous (L)unix il existe une commande qui est "wc" qui renvoie le nombre de mots d'un texte et "wc -l" qui renvoie le nombre de ligne d'un texte :)

14 | vincent rabah, le 11 December 2008 à 14h

Un minuscule détail: je ne mettrais pas de backslash du tout, si j'étais toi. C'est même recommandé dans la doc python: http://docs.python.org/howto/doanddont.html#using-backslash-to-continue-statements

15 | Olivier, le 16 December 2008 à 08h

@vincent : un peu simpliste, non ? On demande pas le nombre de mots du texte mais le nombre d'occurences de chaque mot.

cEd

16 | DecIRC, le 18 December 2008 à 22h

oui en shell ce serait plutôt :

sed -e 's/\.//g' -e 's/\,//g' -e 's/ /\
/g' /filepath | tr 'A-Z' 'a-z' | sort | uniq -c | sort -nr

17 | benoitc, le 21 December 2008 à 15h

Ajouter un commentaire


Billets ★ choisis

★ Développer une application RESTful avec Django

Logo associé au billet intitulé Développer une application RESTful avec Django

Après vous avoir expliqué la théorie sur l'architecture REST, rien de vaut un exemple concret pour bien comprendre le mécanisme. J'ai longtemps hésité entre la classique todolist et un agrégateur pour l'exemple mais j'ai finalement opté ...

★ Le Web arriverait-il à maturité ?

Je n'ai pas la prétention d'être un vétéran du web mais certains signes me laissent à penser qu'internet devient plus mature au niveau de la qualité des services proposés mais aussi des habitudes comportementales des internautes

★ L'importance du rythme vertical en design CSS

Logo associé au billet intitulé L'importance du rythme vertical en design CSS

Franchement déçu par la qualité des sites participant au Concours Cascading Style Summer Refresh, je trouve qu'il y a un problème récurrent chez les participants : il manque la notion de rythme vertical donc je n'avais pas parlé lors ...


© 2004-2012 David Larlet - Licence (presque) libre - Site enfin propulsé par Django et hébergé par Typhon.