Forage de texte avec pattern

0. pattern

Module produit par des chercheurs néerlandais pour faire du web mining.

Forces:

  • Excellents modules linguistiques
  • Très forte intégration du français et d'autres langues non-anglaises
  • Bons outils pour faire de l'analyse plus avancée (e.g. apprentissage machine)

Faiblesses:

  • Types de variables exclusifs, difficilement convertibles en matrices
  • Certains calculs (e.g. gain d'information) ne sont pas très robustes
  • Peu de fonctions pour les outils de base (e.g. concordance, cooccurence)

1. Quelques bases

Unicode, Latin-1 et UTF-8

Python 2.7 fait souvent des difficultés avec l'encodage (le format sous lequel le texte est encodé). Quelques fonctions pour faire la conversion:

In [187]:
chaine_unicode = u"Cette chaîne est en unicode"
print type(chaine_unicode)
<type 'unicode'>

In [188]:
chaine_ordinaire = "Cette chaîne est en UTF-8 (je crois)"
print type(chaine_ordinaire)
<type 'str'>

In [189]:
chaine_dechaine = chaine_ordinaire.decode("utf-8")
print type(chaine_dechaine)
<type 'unicode'>

In [251]:
unicode_enchaine = chaine_unicode.encode("utf-8", errors='ignore')
print type(unicode_enchaine)
<type 'str'>

decode et encode ont une option errors pour gérer les erreurs de conversion:

  • 'strict': termine l'exécution avec un message d'erreur (exception)
  • 'replace': remplace le caractère fautif par un marqueur comme '?' or '\ufffd'
  • 'ignore': ignore le caractère fautif et continue comme si de rien n'était
    • Probablement une bonne idée ici!
  • 'xmlcharrefreplace': remplace avec le code XML approprié (seulement pour encode)
  • 'backslashreplace': remplace avec le code python approprié, de forme '\u[numéro]' (seulement pour encode)

Pour ouvrir un fichier en utf-8:

import codecs
fichier = codecs.open("nom_du_fichier.txt", "r", encoding = "utf-8")

Pour marquer qu'un script Python est en utf-8

Très utile si on entre des caractère non-ASCII, comme des lettres accentuées

# -*- encoding: utf-8 -*-

Fonctions de base sur des chaînes

In [191]:
phrase = "Longtemps, je me suis couché de bonne heure."

Découpage en liste

On peut la découper en mots:

In [192]:
phrase.split()
Out[192]:
['Longtemps,', 'je', 'me', 'suis', 'couch\xc3\xa9', 'de', 'bonne', 'heure.']

... notez qu'ici split() coupe aux espaces, mais on peut mettre n'importe quel caractère ou chaîne de caractère:

In [193]:
print phrase.split(",")
print phrase.split("couché")
['Longtemps', ' je me suis couch\xc3\xa9 de bonne heure.']
['Longtemps, je me suis ', ' de bonne heure.']

Pour raboutter tout ça:

In [252]:
mots_de_phrase = phrase.split()
print "_".join(mots_de_phrase)
Longtemps,_je_me_suis_couché_de_bonne_heure.

Casse

On peut aussi tout convertir en minuscules:

In [195]:
phrase.lower()
Out[195]:
'longtemps, je me suis couch\xc3\xa9 de bonne heure.'

ou en majuscules:

In [196]:
print phrase.upper()
LONGTEMPS, JE ME SUIS COUCHé DE BONNE HEURE.

... sauf que ça ne marche pas avec les accents.

In [197]:
print phrase.decode('utf-8').upper()
LONGTEMPS, JE ME SUIS COUCHÉ DE BONNE HEURE.

Remplacement

Remplace toutes les occurences d'une expression par une autre

In [198]:
print phrase.replace("o", "☺")
print phrase.lower().replace(",","").replace(".","")
L☺ngtemps, je me suis c☺uché de b☺nne heure.
longtemps je me suis couché de bonne heure

Expressions régulières

Peuvent être beaucoup plus pratiques pour faire des modifications au texte

In [199]:
import re
print re.sub("[.,:;]","",phrase)
Longtemps je me suis couché de bonne heure

Le module re possède aussi sa propre fonction split().

Attention : les expressions régulières ne comptent jamais les caractères accentués comme des lettres.

2. Prétraitement

Mettons qu'on prend Du côté de chez Swann, de Proust

On importe les libraires utiles... (ici, on va prendre des textes en ligne, donc on emploie urllib)

In [200]:
import urllib

Ouvrons un fichier texte, qui est hébergé sur Gutenberg:

In [201]:
f = urllib.urlopen("http://www.gutenberg.org/files/2650/2650-0.txt")
texte = f.read()
texte = texte.replace("\r","") # les fins de ligne de Gutenberg sont en CRLF. On enlève les CR.

2.1. En-tête et annexe du projet Gutenberg

In [202]:
print texte[:1000]
The Project Gutenberg EBook of Du Côté de Chez Swann, by Marcel Proust

This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever.  You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net


Title: Du Côté de Chez Swann

Author: Marcel Proust

Posting Date: April 13, 2010 [EBook #2650]
Release Date: May, 2001
[Last updated: June 2, 2012]

Language: French

Character set encoding: UTF-8

*** START OF THIS PROJECT GUTENBERG EBOOK DU CÔTÉ DE CHEZ SWANN ***




Produced by Sue Asscher









MARCEL PROUST



À la RECHERCHE DU TEMPS PERDU



TOME I



DU COTÉ DE CHEZ SWANN





À Monsieur Gaston Calmette



Comme un témoignage de profonde et affectueuse reconnaissance,



Marcel Proust.







PREMIÈRE PARTIE



COMBRAY



I.



Longtemps, je me suis couché de bonne heure. Parfois, à peine ma
bougie éteinte, mes yeux se fermaient si vite que 

À la fin, ç'a l'aire de ça:

In [203]:
print texte[-20000:-18000] + "\n\n[...]\n\n" + texte[-2000:]
e Jardin
élyséen de la Femme; au-dessus du moulin factice le vrai ciel était
gris; le vent ridait le Grand Lac de petites vaguelettes, comme un
lac; de gros oiseaux parcouraient rapidement le Bois, comme un bois,
et poussant des cris aigus se posaient l'un après l'autre sur les
grands chênes qui sous leur couronne druidique et avec une majesté
dodonéenne semblaient proclamer le vide inhumain de la forêt
désaffectée, et m'aidaient à mieux comprendre la contradiction que
c'est de chercher dans la réalité les tableaux de la mémoire, auxquels
manquerait toujours le charme qui leur vient de la mémoire même et de
n'être pas perçus par les sens. La réalité que j'avais connue
n'existait plus. Il suffisait que Mme Swann n'arrivât pas toute
pareille au même moment, pour que l'Avenue fût autre. Les lieux que
nous avons connus n'appartiennent pas qu'au monde de l'espace où nous
les situons pour plus de facilité. Ils n'étaient qu'une mince tranche
au milieu d'impressions contiguës qui formaient notre vie d'alors; le
souvenir d'une certaine image n'est que le regret d'un certain
instant; et les maisons, les routes, les avenues, sont fugitives,
hélas, comme les années.










End of the Project Gutenberg EBook of Du Côté de Chez Swann, by Marcel Proust

*** END OF THIS PROJECT GUTENBERG EBOOK DU CÔTÉ DE CHEZ SWANN ***

***** This file should be named 2650-0.txt or 2650-0.zip *****
This and all associated files of various formats will be found in:
        http://www.gutenberg.org/2/6/5/2650/

Produced by Sue Asscher

Updated editions will replace the previous one--the old editions
will be renamed.

Creating the works from public domain print editions means that no
one owns a United States copyright in these works, so the Foundation
(and you!) can copy and distribute it in the United States without
permission and without paying copyright royalties.  Special rules,
set forth in the General Terms of Use part of this license, apply to
copying and dis

[...]

considerable effort, much paperwork and many fees to meet and keep up
with these requirements.  We do not solicit donations in locations
where we have not received written confirmation of compliance.  To
SEND DONATIONS or determine the status of compliance for any
particular state visit http://pglaf.org

While we cannot and do not solicit contributions from states where we
have not met the solicitation requirements, we know of no prohibition
against accepting unsolicited donations from donors in such states who
approach us with offers to donate.

International donations are gratefully accepted, but we cannot make
any statements concerning tax treatment of donations received from
outside the United States.  U.S. laws alone swamp our small staff.

Please check the Project Gutenberg Web pages for current donation
methods and addresses.  Donations are accepted in a number of other
ways including including checks, online payments and credit card
donations.  To donate, please visit: http://pglaf.org/donate


Section 5.  General Information About Project Gutenberg-tm electronic
works.

Professor Michael S. Hart is the originator of the Project Gutenberg-tm
concept of a library of electronic works that could be freely shared
with anyone.  For thirty years, he produced and distributed Project
Gutenberg-tm eBooks with only a loose network of volunteer support.


Project Gutenberg-tm eBooks are often created from several printed
editions, all of which are confirmed as Public Domain in the U.S.
unless a copyright notice is included.  Thus, we do not necessarily
keep eBooks in compliance with any particular paper edition.


Most people start at our Web site which has the main PG search facility:

     http://www.gutenberg.net

This Web site includes information about Project Gutenberg-tm,
including how to make donations to the Project Gutenberg Literary
Archive Foundation, how to help produce our new eBooks, and how to
subscribe to our email newsletter to hear about new eBooks.


Le plus souvent, on veut que le prétraitement puisse fonctionner pour d'autres textes – ici d'autres textes de Gutenberg. Le problème est de trouver un séparateur qui marche non seulement pour ce texte, mais probablement aussi pour les autres.

Pour ce faire, on peut employer les expressions régulières. Ici:

  • \* = *
  • (?: ) = bloc. "?:" signifie que le bloc ne sert pas à recouvrir le texte correspondant à l'expression qu'il contient.
  • | = ou
  • [^*] matche tout caractère qui n'est pas *
  • . = wildcard. Matche n'importe quel caractère.
  • * matche le caractère ou bloc précédent de 0 à ∞ fois
In [204]:
texte = re.split("\*\*\* (?:START|END) OF THIS PROJECT GUTENBERG EBOOK [^*]*\*\*\*.*",texte)[1]
texte = texte.strip()

print texte[:100]
print "[...]"
print texte[-150:]
Produced by Sue Asscher









MARCEL PROUST



À la RECHERCHE DU TEMPS PERDU



TOME I



DU COT
[...]
tes, les avenues, sont fugitives,
hélas, comme les années.










End of the Project Gutenberg EBook of Du Côté de Chez Swann, by Marcel Proust

Enlever la première et la dernière ligne

In [205]:
texte = re.sub("^.*\n","",texte)
texte = re.sub("\n.*$","", texte)
texte = texte.strip()

print texte[:100]
print "[...]"
print texte[-100:]
MARCEL PROUST



À la RECHERCHE DU TEMPS PERDU



TOME I



DU COTÉ DE CHEZ SWANN





À Monsieur
[...]
certain
instant; et les maisons, les routes, les avenues, sont fugitives,
hélas, comme les années.

Faisons-en une fonction, pour l'appliquer à tous les textes

In [206]:
def degutenberger(txt):
    t = txt.replace("\r","")
    t = re.split("\*\*\* (?:START|END) OF [^*]* PROJECT GUTENBERG [^*]*\*\*\*.*",t)[1]
    t = t.strip()
    t = re.sub("^.*\n\n","",t)
    t = re.sub("\n\n.*$","", t)
    return t.strip()

2.2 Prétraitement dans pattern.vector

Le reste du prétraitement est pris en charge par pattern:

In [207]:
import pattern.vector as vec
In [208]:
doc_Swann = vec.Document(texte.replace("'"," ").decode('utf-8'), 
                                                 # Remplace les ' par des espaces, respectant le fait qu'en français,
                                                 # les ' séparent des mots. Pour éviter des erreurs, je convertis aussi
                                                 # le texte en unicode
                         filter = lambda w: w.isalpha(),
                                                 # Filtre les mots qui ne sont pas exclusivement composés de lettres
                         punctuation = u'.,;:!?()[]{}\'`"@#$*+-|=~_«»',
                         top = None,             # Filtre les mots qui ne sont pas parmi les plus fréquents.
                         threshold = 0,          # Filtre les mots dont la fréquence est inférieure à un seuil.
                         exclude = [],           # Filtre les mots dans la liste noire.
                         stemmer = None,         # vec.STEMMER | vec.LEMMA | fonction | None
                         stopwords = False,      # Inclure les mots fonctionnels ?
                         name = None,            # Nom, type et description 
                         type = None,
                         description = None,
                         language = 'fr')        # Langue du texte (fr | en |  es | de | it | nl)

Cette fonction vient de la documentation pattern (http://www.clips.ua.ac.be/pages/pattern-vector). Mais il faut parfois se poser des questions sur le traitement de la langue. En général, les outils de forage de texte ont été conçus pour l'anglais. E.g. Est-ce que la fonction isalpha() prend en compte les accents?

In [209]:
print "couché".isalpha()
print u"couché".isalpha() # "couché" est en format unicode, plutôt que chaîne UTF-8
print "couche".isalpha()
False
True
True

À ce point-ci, on peut faire un corpus entier (À la recherche du temps perdu, mettons):

In [210]:
textes_proust = []
i=0
for ln in liste_proust:
    f = urllib.urlopen("http://louis.philotech.org/confs/MtlPythonFr2014/corpus/" + ln)
    t = f.read()
    t = degutenberger(t)
    
    l = ln.split("_")
    if i == int(l[1]):
        textes_proust[-1] += t
    else:
        textes_proust.append(t)
    
    i=int(l[1])
In [211]:
documents_proust = []
for t in textes_proust:
    d = vec.Document(t.replace("'"," ").decode('utf-8'), # Remplace les ' par des espaces, respectant le fait qu'en français, les ' séparent des mots
                       filter = lambda w: w.isalpha(),
                                           # Filtre les mots qui ne sont pas exclusivement composés de lettres
                       punctuation = u'.,;:!?()[]{}\'`"@#$*+-|=~_«»',
                       top = None,             # Filtre les mots qui ne sont pas parmi les plus fréquents.
                       threshold = 0,          # Filtre les mots dont la fréquence est inférieure à un seuil.
                       exclude = [],           # Filtre les mots dans la liste noire.
                       stemmer = None,         # vec.STEMMER | vec.LEMMA | fonction | None
                       stopwords = False,      # Inclure les mots fonctionnels ?
                       name = None,            # Nom, type et description 
                       type = None,
                       description = None,
                       language = 'fr')        # Langue du texte (fr | en |  es | de | it | nl)
    documents_proust.append(d)

corpus_proust = vec.Model(documents_proust, weight=vec.TFIDF)

À vrai dire, tout ça marche bien si on veut comparer les œuvre entre elles. Mais souvent, on préfère prendre une œuvre, la découper en morceaux, et analyser ceux-ci.

Prenons le Discours de la méthode pour cette fin:

In [212]:
f = urllib.urlopen("http://www.gutenberg.org/cache/epub/13846/pg13846.txt")
texte_detude = f.read()
texte_detude = degutenberger(texte_detude)

print texte_detude[:500]
Produced by Miranda van de Heijning, Renald Levesque and the Online
Distributed Proofreading Team. This file was produced from images
generously made available by the Bibliothèque nationale de France
(BnF/Gallica)







Descartes, René

_Oeuvres de Descartes, précédées de l'éloge de René Descartes par
Thomas_


OEUVRES DE DESCARTES.

TOME PREMIER


PUBLIÉES PAR VICTOR COUSIN.



A

M. ROYER-COLLARD,

Professeur de l'histoire de la philosophie morale à la Faculté des
Lettres de l'Acad�

On peut découper le texte de plusieurs façons.

  • Par paragraphe
  • Par sections/chapitres
  • En sectionnant à tous les n mots (e.g. n = 100)
  • etc.

Commençont par séparer en mots.

On veut éviter de garder les mots fonctionnels, car ils augmentent le temps de traitement et compromettent les résultats. On va donc d'abord télécharger une liste de mots fonctionnels:

In [213]:
f = urllib.urlopen("http://louis.philotech.org/confs/MtlPythonFr2014/corpus/stoplist.fr.utf-8.txt")
stoplist = f.read().decode('utf-8').split("\r\n")
In [214]:
mots_etude = vec.words(texte_detude.lower().replace("'"," ").decode('utf-8'),
                       filter = lambda w: w.isalpha() and len(w)>1 and w not in stoplist,
                           # On filtre les mots d'un seul caractère et les mots fonctionnels
                       punctuation = u'.,;:!?()[]{}\'`"@#$*+-|=~_«»')

On sépare fait des tranches de 50 mots, qu'on raboutte avec "join()"

In [215]:
t = 50
segments_etude = [ " ".join(mots_etude[i:i+t]) for i in xrange(0, len(mots_etude), t) ]

Maintenant, faisons un corpus avec ça:

In [216]:
documents_etude = []
for t in segments_etude:
    d = vec.Document(t, language = 'fr')
    documents_etude.append(d)

corpus_etude = vec.Model(documents_etude, weight=None)

2.3 Composition d'une matrice

Comme on a dit précédemment, pattern ne produit pas de matrice. Donc on doit la produire soi-même.

Par matrice, on entend celle du modèle vectoriel:

mot 1 mot 2 ... mot n
segment 1 0 1 ... 4
segment 2 3 0 ... 1
... ... ... ... ...
segment n 5 2 ... 0

Format Array de numpy

On fait la matrice sous forme d'une array de numpy

In [217]:
import numpy as np

La raison est simple: dans ce format, on peut faire toutes sortes de manipulations qui prendraient beaucoup de computation et de mémoire autrement.

Par exemple:

In [218]:
m = np.arange(12)   # = np.array(range(12))
m = m.reshape((4,3))    # réarrange une matrice 1×12 en matrice 4×3
print m
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]

On peut facilement prendre des lignes se la base d'une liste ou d'un critère:

In [219]:
m[[1,2]]
Out[219]:
array([[3, 4, 5],
       [6, 7, 8]])
In [220]:
m[m.sum(1)<15]
Out[220]:
array([[0, 1, 2],
       [3, 4, 5]])

En passant, sum(0), c'est la somme sur l'axe 0 – la somme de chaque ligne:

In [221]:
m.sum(0)
Out[221]:
array([18, 22, 26])

Autres trucs pratiques:

In [222]:
m.T
Out[222]:
array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])
In [223]:
np.zeros((4,5))
Out[223]:
array([[ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.]])

Construction de la matrice

On commence par faire une matrice vide:

In [224]:
matrice_etude = np.zeros((len(segments_etude),len(corpus_etude.features)), dtype=int)
    # Par défaut, numpy fait une matrice remplie de floats. J'utilise dtype pour spécifier que ce sont des entiers

Puis on la remplit

In [225]:
attributs_etude = corpus_etude.features    # Liste des mots dans le corpus

ln = 0
for doc in corpus_etude.documents:
    for mot in doc.words.keys():
        matrice_etude[ln][attributs_etude.index(mot)]=doc.words[mot]   # words() rend le nombre d'occurences du mot
    ln += 1

Qu'est-ce que ça donne comme matrice ?

In [226]:
print matrice_etude.shape   # Les dimensions de la matrice
print matrice_etude.sum()   # Somme de toutes les cellules = nombre total de mots dans l'œuvre (excluant les mots fonctionnels)
(930, 7419)
41037

On veut cependant enlever les mots qui n'apparaissent qu'une fois.

On détermine quels sont les index des mots qu'on veut garder:

In [227]:
ids = matrice_etude.sum(0)>1
print ids
[ True  True False ...,  True  True  True]

Puis on met la matruce et la liste des attributs (mots) à jour:

In [228]:
matrice_etude = matrice_etude.T[ids].T

attributs_etude = np.array(attributs_etude)
attributs_etude = attributs_etude[ids]

Ça nous donne une matrice rétrécie:

In [229]:
print matrice_etude.shape
(930, 3815)

3. Statistiques

3.1 Mots les plus fréquents

On peut savoir quels mots sont les plus fréquents:

In [230]:
for ln in doc_Swann.keywords(top=20): print "%0.08f\t%s" % ln
0.01038255	swann
0.00671635	odette
0.00631067	faire
0.00468792	mme
0.00419208	dit
0.00398173	voir
0.00387655	verdurin
0.00375635	temps
0.00356102	vie
0.00333564	dire
0.00330559	air
0.00321543	grand
0.00288488	faisait
0.00280975	moment
0.00279472	chose
0.00279472	tante
0.00277970	jour
0.00271960	mère
0.00265949	disait
0.00253929	françoise

Le nombre à droite est un indice de fréquence $$x = \frac{N_{mot}}{N_{tot}}$$ où $N_{mot}$ est la fréquence du mot, et $N_{tot}$ est le nombre total de mots dans l'œuvre.

Faisons pareil avec le livre à l'étude

In [231]:
doc_etude = vec.Document(" ".join(mots_etude),
                         filter = None,
                         stopwords = False,
                         language = 'fr')
In [232]:
for ln in doc_etude.keywords(top=20): print "%0.08f\t%s" % ln
0.01320759	chose
0.01267149	point
0.01245218	choses
0.01194044	dieu
0.00857763	esprit
0.00804152	idée
0.00777347	nature
0.00728611	descartes
0.00687185	corps
0.00531228	étoit
0.00528791	cause
0.00458123	avoit
0.00458123	vérité
0.00453250	homme
0.00404513	raison
0.00397203	idées
0.00392329	existence
0.00363087	fort
0.00363087	âme
0.00353340	clairement

Ou si on veut l'avoir en fréquence absolue:

In [233]:
for ln in sorted(zip(matrice_etude.sum(0),attributs_etude), reverse=True)[:20]: print "%d\t%s" % ln
542	chose
520	point
511	choses
490	dieu
352	esprit
330	idée
319	nature
299	descartes
282	corps
218	étoit
217	cause
188	vérité
188	avoit
186	homme
166	raison
163	idées
161	existence
149	âme
149	fort
145	clairement

Déjà, on a une petite idée des thèmes qui parcourent le Discours de la méthode et Du côté de chez Swann.

3.2 Regroupements

Pour avoir une meilleure idée, on peut regrouper les segments semblables, et prendre les mots qui leur sont le plus associés.

In [234]:
classes = corpus_etude.cluster(k=10, iterations = 100)
[len(i) for i in classes ]
Out[234]:
[45, 113, 132, 29, 69, 98, 104, 120, 128, 92]

Mais pour une raison impénétrable, pattern ne semble me donner que des clases égales, peu importe ce que je fais...

On va donc employer l'algorithme k-means de scipy:

In [235]:
from scipy.cluster.vq import whiten, vq, kmeans

Ça marche en deux étapes:

  1. On applique le k-means
  2. On assigne un numéro de classe à chaque segment avec vq()
In [236]:
center, dist = kmeans(matrice_etude, 10, iter=100)
codes, distance = vq(matrice_etude, center)

La distribution:

In [237]:
codes_uniq = np.unique(codes)  # Ne garde qu'une instance de chaque item dans la liste
print [ sum(codes==i) for i in codes_uniq ]
[142, 225, 39, 53, 45, 74, 152, 87, 44, 69]

On va faire une liste de listes – de documents, puis d'index de documents – pour faciliter les opérations qui vont suivre.

In [238]:
rg_idx = np.arange(len(codes))

array_docs = np.array(corpus_etude.documents)
classes_docs = [ array_docs[codes==i].tolist() for i in codes_uniq ]
classes = [ rg_idx[codes==i].tolist() for i in codes_uniq ]

Prenons la première classe... quels sont les mots qui y sont fréquents ?

In [239]:
freq_attributes_classe1 = matrice_etude[codes==codes_uniq[0]].sum(0)
In [240]:
for ln in sorted(zip(freq_attributes_classe1, attributs_etude), reverse = True)[:10]:
    print "%g\t%s" % ln
315	dieu
84	choses
72	chose
69	nature
65	existence
63	point
57	idée
42	existe
40	raison
36	vérité

On peut voir ce que ça donne avec les autres...

In [241]:
def mots_frequents(doclist):
    lsids = [ corpus_etude.documents.index(d) for d in doclist ]
    return sorted(zip(matrice_etude[lsids].sum(0), attributs_etude), reverse = True)
In [242]:
from ipy_table import * # Pour faire un beau tableau!

tableau = []
for c in classes_docs:
    ln = []
    for freq, mot in  mots_frequents(c)[:8]:
        ln.append(("%d %s" % (freq, mot)).encode('utf-8'))
    tableau.append(ln)

# Renverser le tableau pour que 1 colonne = 1 classe
tableau =np.array(tableau).T.tolist()
    
make_table(tableau)
Out[242]:

On voit que la fréquence a ses limites: "lélia", par exemple, apparaît constamment, donc ne nous informe pas beaucoup sur les thèmes dont on parle dans la classe de segments.

Pour pallier à ce problème, on emploie des mesures d'association:

Le TF-IDF

$$\mathrm{tfidf}(m, d, D) = \frac{|m \in d|}{|d|} \times \log\frac{|D|}{|\{ d_i \in D : t \in d_i\}|}$$ où

  • $m$ est le mot dont on veut le TF-IDF,
  • $d$ le document ou segment où on veut son TF-IDF,
  • $D$ l'ensemble de tous les documents/segments,
  • $|m \in d|$ la fréquence de $m$ dans $d$,
  • $|d|$ le nombre de mots dans $d$,
  • $|D|$ le nombre de documents/segments dans $D$
  • et $|\{ d_i \in D : t \in d_i\}|$ le nombre de documents/segments dans $D$ qui contiennent le mot $m$

Le tout donne une mesure qui représente à la fois l'association de $d$ avec $m$ et l'importance de $d$ pour l'expression de $m$.

On va avoir besoin de ça:

In [243]:
def cle(dic, k):
    return dic[k] if k in dic else 0

Maintenant, la fonction pour aller chercher les TF-IDF

In [244]:
def tfidf(mot, classe, corpus):
    tf = sum([ cle(d.words,mot) for d in classe]) / float(sum([ sum(d.words.values()) for d in classe]))
    doc_ayant_mot = len([d for d in corpus.documents if cle(d.words,mot) > 0 ])
    idf = np.log( float(len(corpus)) / doc_ayant_mot ) if doc_ayant_mot != 0 else 0.0
    return tf * idf
In [245]:
def liste_tfidf(classe, corpus):
    return sorted(zip([ tfidf(m, classe, corpus) for m in attributs_etude ], attributs_etude), reverse=True)

Pour sortir ça en tableau (copier-coller!):

In [246]:
tableau = []
for c in classes_docs:
    ln = []
    for freq, mot in  liste_tfidf(c,corpus_etude)[:8]:
        ln.append(("%g %s" % (freq, mot)).encode('utf-8'))
    tableau.append(ln)

# Renverser le tableau pour que 1 colonne = 1 classe
tableau =np.array(tableau).T.tolist()
    
make_table(tableau)
Out[246]:
315 dieu59 esprit93 étoit169 idée103 cause157 corps239 choses139 descartes74 homme214 chose
84 choses49 point35 descartes51 dieu69 chose100 esprit188 point102 avoit49 descartes43 pense
72 chose48 fort19 avoit38 chose58 point47 nature56 chose29 étoit18 grand41 nature
69 nature45 choses14 point35 point26 dieu43 chose51 nature25 point17 esprit40 point
65 existence43 nature11 nature31 réalité23 idée41 choses51 clairement25 hommes15 génie39 choses
63 point37 vérité11 homme29 choses20 effet38 point47 vérité21 nature14 nature32 esprit
57 idée37 chose11 choses25 esprit18 esprit30 âme47 esprit21 ans14 hommes29 idée
42 existe34 hommes11 ans23 cause15 besoin28 dieu42 dieu19 note13 humain28 entendement

Le Chi-carré ($\chi^2$)

On compte un tableau croisé comptant les segments/documents qui appartiennent ou non à la classe $c$, et qui contiennent ou non le mot $m$ auquel on s'intéresse:

0.0621499 dieu0.0115606 coeur0.0926475 étoit0.120696 idée0.101607 cause0.0787255 corps0.036491 choses0.052417 descartes0.0672167 homme0.0766394 chose
0.0225363 existence0.0108457 sang0.0298512 descartes0.0435556 réalité0.0387848 chose0.0394028 esprit0.0260611 point0.04909 avoit0.0362153 descartes0.0323339 pense
0.0160841 existe0.00986613 fort0.0206815 avoit0.0302075 objective0.0286571 efficiente0.0202768 parties0.0166163 clairement0.0149053 ans0.022368 génie0.0230575 entendement
0.0155706 idée0.00870746 opinions0.0176583 ans0.0280861 objectivement0.027755 point0.0198746 âme0.0145252 distinctement0.0140122 philosophe0.0211528 grand0.0185284 nature
0.0150871 nature0.00758576 esprit0.0159495 étoient0.0263074 dieu0.0239287 effet0.0193853 nature0.0124045 vérité0.01368 nouvelle0.0173099 humain0.0170047 pensée
0.0136481 choses0.00758027 mieux0.0129421 problème0.0211714 artifice0.0204428 besoin0.0143456 distinction0.0123067 raisons0.0136583 hommes0.0153248 socrate0.0167833 dit
0.0124759 chose0.00744646 hommes0.0129421 frère0.0208336 entendement0.0203812 idée0.0140549 chose0.010479 nature0.0127736 étoit0.0149908 hommes0.0163729 idée
0.0123017 raison0.00743181 objets0.0127823 reine0.019942 existence0.0191765 réalité0.0131133 pied0.0104455 semble0.0123592 géométrie0.0132921 éloge0.0161207 cire
$m$ présent $m$ absent
$\in c$ $N_{11}$ $N_{10}$
$\notin c$ $N_{01}$ $N_{00}$

Le calcul du $\chi^2$ se fait ainsi: $$\chi^2(m, c, D) = N \times \frac{(N_{11}N_{00}-N_{10}N_{01})^2}{(N_{11}+N_{00})(N_{11}+N_{10})(N_{00}+N_{01})(N_{00}+N_{10})}$$ où $N$ est le nombre total de documents/segment dans $D$.

In [247]:
from time import time
In [248]:
def chi2(mot, classe, matrice):
    pasclasse = np.ones(len(matrice_etude), dtype=bool)
    pasclasse[classe]=False
    motidx = attributs_etude.tolist().index(mot)
    mat = matrice.clip(0,1)
    invmat = abs(mat-1)
    
    n11 = mat[classe].T[motidx].sum()
    n10 = invmat[classe].T[motidx].sum()
    n01 = mat[pasclasse].T[motidx].sum()
    n00 = invmat[pasclasse].T[motidx].sum()
    
    N = n11 + n10 + n01 + n00
    r= N * ( n11 * n00 - n10 * n01)**2 / float( (n11+n00) * (n11+n10) * (n00+n01) * (n00+n10) )
    
    return r
In [249]:
def liste_chi2(classe, matrice):
    codes_classe = np.zeros(matrice.shape[0], dtype=int)
    codes_classe[classe] += 1
    codes_pasclasse = np.ones(matrice.shape[0], dtype=int)
    codes_pasclasse[classe] -= 1
    
    mat = matrice.clip(0,1)
    invmat = abs(mat-1)
    
    n11 = (codes_classe * mat.T).T.sum(0)
    n10 = (codes_classe * invmat.T).T.sum(0)
    n01 = (codes_pasclasse * mat.T).T.sum(0)
    n00 = (codes_pasclasse * invmat.T).T.sum(0)
    
    N = n11 + n10 + n01 + n00
    r= N * ( n11 * n00 - n10 * n01)**2 / np.array( (n11+n00) * (n11+n10) * (n00+n01) * (n00+n10), dtype=float )
    
    return sorted(zip(r, attributs_etude), reverse=True)
In [250]:
tableau = []
for c in classes:
    ln = []
    for freq, mot in  liste_chi2(c,matrice_etude)[:8]:
        ln.append(("%g %s" % (freq, mot)).encode('utf-8'))
    tableau.append(ln)

# Renverser le tableau pour que 1 colonne = 1 classe
tableau =np.array(tableau).T.tolist()
    
make_table(tableau)
Out[250]:
137.692 dieu80.586 dieu38.7471 étoit52.5255 idée42.5143 cause68.6737 corps84.7574 choses70.1279 descartes43.7338 homme66.9559 chose
8.46968 existence37.1664 point4.77272 descartes8.15269 dieu26.1874 chose20.6065 esprit59.711 point40.4161 avoit24.2584 descartes6.25413 pense
6.08514 descartes29.5879 chose4.19009 dieu3.22636 réalité7.54916 point6.68858 nature7.98626 descartes24.5662 choses11.9061 chose3.53408 descartes
3.71882 corps27.1175 choses3.76914 choses2.98476 objective3.17051 effet4.0287 descartes4.4949 étoit20.974 chose11.084 choses2.39268 entendement
3.32446 avoit9.281 idée2.67303 chose2.74022 idées1.94429 descartes1.87218 parties4.27673 idée12.5975 dieu6.28311 dieu2.23615 dit
2.29922 esprit7.66803 nature2.58239 avoit2.70216 perfection1.87155 choses1.82339 exemple2.56816 avoit6.60385 point5.37068 point1.6332 idée
2.08301 existe3.83996 descartes1.78268 esprit2.5669 objectivement1.78295 efficiente1.48287 chose2.55006 distinctement3.82534 étoit3.5636 grand1.46004 nature
2.05172 choses3.50631 existence1.16948 idée1.58666 existence1.72687 raison1.41902 âme2.55001 clairement2.97891 hommes3.19829 génie1.31212 raison