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

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.

### Articles peut-être en rapport

- La documentation Django en local (html et pdf)
- Interfaces et promotion du Web Sémantique
- 24ways, le calendrier de l'Avent des geeks web

### Commentaires

kim le 06/12/2008 :

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

maxime le 06/12/2008 :

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

David, biologeek le 06/12/2008 :

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

kim le 06/12/2008 :

@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" :)

Jdoe le 07/12/2008 :

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

Jean-Philippe Camguilhem le 07/12/2008 :

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

@++

kim le 07/12/2008 :

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

benoitc le 07/12/2008 :

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

benoitc le 07/12/2008 :

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 .

jean-Philippe Camguilhem le 07/12/2008 :

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

benoitc le 07/12/2008 :

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

benoitc le 07/12/2008 :

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

scar le 08/12/2008 :

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

vincent rabah le 11/12/2008 :

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

Olivier le 16/12/2008 :

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

DecIRC le 18/12/2008 :

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

cEd

benoitc le 21/12/2008 :

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