Visualisation des réponses aux questions ouvertes du Grand Débat National
Ce post est adapté d’un notebook présentant mon approche au hackathon organisé par l’Assemblée Nationale en mars 2019 qui avait pour but d’analyser les réponses aux questionnaires en ligne du Grand Débat. J’ai choisi d’en faire une visualisation en deux dimensions via une représentation vectorielle des réponses. Cela a plus tard mené à la publication de cet article Medium pour le compte de LightOn, la start-up pour laquelle je travaille.
from collections import Counter
import io
import multiprocessing as mp
import numpy as np
import pandas as pd
import plotly.express as px
from sklearn.decomposition import PCA, TruncatedSVD
import spacy
import textwrap
import umap
Préparation des données
Concentrons-nous sur le questionnaire portant sur l’écologie.
data = pd.read_csv('data/Le Grand Debat/LA_TRANSITION_ECOLOGIQUE.csv', index_col=0, dtype=str)
dropped = ['id', 'publishedAt', 'updatedAt', 'trashed', 'trashedStatus',
'authorId', 'authorType', 'authorZipCode', 'createdAt']
data = data.drop(dropped, axis=1)
data.head()
reference | title | QUXVlc3Rpb246MTYw - Quel est aujourd'hui pour vous le problème concret le plus important dans le domaine de l'environnement ? | QUXVlc3Rpb246MTYx - Que faudrait-il faire selon vous pour apporter des réponses à ce problème ? | QUXVlc3Rpb246MTQ2 - Diriez-vous que votre vie quotidienne est aujourd'hui touchée par le changement climatique ? | QUXVlc3Rpb246MTQ3 - Si oui, de quelle manière votre vie quotidienne est-elle touchée par le changement climatique ? | QUXVlc3Rpb246MTQ4 - À titre personnel, pensez-vous pouvoir contribuer à protéger l'environnement ? | QUXVlc3Rpb246MTQ5 - Si oui, que faites-vous aujourd'hui pour protéger l'environnement et/ou que pourriez-vous faire ? | QUXVlc3Rpb246MTUw - Qu'est-ce qui pourrait vous inciter à changer vos comportements comme par exemple mieux entretenir et régler votre chauffage, modifier votre manière de conduire ou renoncer à prendre votre véhicule pour de très petites distances ? | QUXVlc3Rpb246MTUx - Quelles seraient pour vous les solutions les plus simples et les plus supportables sur un plan financier pour vous inciter à changer vos comportements ? | QUXVlc3Rpb246MTUy - Par rapport à votre mode de chauffage actuel, pensez-vous qu'il existe des solutions alternatives plus écologiques ? | QUXVlc3Rpb246MTUz - Si oui, que faudrait-il faire pour vous convaincre ou vous aider à changer de mode de chauffage ? | QUXVlc3Rpb246MTU0 - Avez-vous pour vos déplacements quotidiens la possibilité de recourir à des solutions de mobilité alternatives à la voiture individuelle comme les transports en commun, le covoiturage, l'auto-partage, le transport à la demande, le vélo, etc. ? | QUXVlc3Rpb246MTU1 - Si oui, que faudrait-il faire pour vous convaincre ou vous aider à utiliser ces solutions alternatives ? | QUXVlc3Rpb246MjA3 - Si non, quelles sont les solutions de mobilité alternatives que vous souhaiteriez pouvoir utiliser ? | QUXVlc3Rpb246MTU3 - Et qui doit selon vous se charger de vous proposer ce type de solutions alternatives ? | QUXVlc3Rpb246MTU4 - Que pourrait faire la France pour faire partager ses choix en matière d'environnement au niveau européen et international ? | QUXVlc3Rpb246MTU5 - Y a-t-il d'autres points sur la transition écologique sur lesquels vous souhaiteriez vous exprimer ? | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2-4 | transition écologique | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | Enseignement du tri sélectif à l'école | Multiplier les centrales géothermiques |
1 | 2-5 | La surpopulation | Les problèmes auxquels se trouve confronté l’e... | Les problèmes auxquels se trouve confronté l’e... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | Mettre en oeuvre au niveau national ses engage... | Les problèmes auxquels se trouve confronté l’e... |
2 | 2-6 | climat | Les dérèglements climatiques (crue, sécheresse) | pour éviter les inondations obliger les rivera... | Non | NaN | Non | NaN | développer les transports en commun , | NaN | Non | NaN | Oui | NaN | Les transports en commun|L'auto partage|Le tra... | NaN | NaN | NaN |
3 | 2-7 | POLLUTION AIR EAU | La pollution de l'air | Il faut taxer les gros pollueurs : Entreprises... | Non | NaN | Non | NaN | NaN | Plus de transports publics dans les petites co... | Non | NaN | Non | Le co-voiturage ne correspond pas toujours aux... | Les transports en commun | L'Etat | Elle n'a aucun pouvoir. Impossible de contrain... | NaN |
4 | 2-8 | Economie vs Ecologie | La biodiversité et la disparition de certaines... | Changer notre mode de vie, impulser une nouvel... | Oui | Pollution de l'air, pollution de nos aliments,... | Oui | En consommant autrement, en vivant autrement. | Aménagement de piste cyclable, développement d... | Détaxer le mode de chauffage écologique, une a... | Oui | une aide significative pour de l'éolien ou du ... | Non | Une piste cyclable pour éviter de risquer ma v... | Les transports en commun|Le covoiturage|Le vélo | La commune, le département, la région | Demander à Nicolas Hulot | Une vrai politique écologique et non économique |
question_dic = {}
qids = []
for col in data.columns[2:]:
qid, text = col.split(' - ')
question_dic[qid] = text
qids.append(qid)
data.columns = list(data.columns[:2]) + qids
# il y a eu une erreur de parsing sur cette entrée et elle fait bugger la ligne suivante
data = data.drop(data.index[27725])
data.index = data.index.astype('int')
Lemmatisation
nlp = spacy.load('fr')
nlp.disable_pipes('tagger', 'ner', 'parser') # enlever ces fonctionnalités accélère le calcul
stopwords = spacy.lang.fr.stop_words.STOP_WORDS
def lemmatize(s):
try:
doc = nlp(s)
return [d.lemma_
for d in doc
if not (d.lemma_.lower() in stopwords
or len(d) == 1
or str(d) == '...'
or str(d).isspace())]
except TypeError:
return ''
def process_column(column):
s = data[column]
s = s.map(lambda text: lemmatize(text))
return s
pool = mp.Pool(mp.cpu_count())
res = pool.map(process_column, data.columns[2:])
Après avoir retiré les traitements inutiles de spacy et parallelisé les calculs, la lemmatisation passe de 1h30 à 50 s.
lemm = data.copy()
lemm.iloc[:,2:] = pd.DataFrame(res).transpose()
print(data.iloc[1, 2])
print('-' * 100)
print(' '.join(lemm.iloc[1, 2]))
Les problèmes auxquels se trouve confronté l’ensemble de la planète et que dénoncent, dans le plus parfait désordre, les gilets jaunes de France ne sont-ils pas dus, avant tout, à la surpopulation mondiale ? Cette population est passée d’1,5 milliards d’habitants en 1900 à 7 milliards en 2020 et montera bientôt à 10 milliards vers 2040. Avec les progrès de la communication dans ce village mondial, chaque individu, du fin fond de l’Asie au fin fond de l’Afrique, en passant par les « quartiers » et les « campagnes » de notre pays, aspire à vivre – et on ne peu l’en blâmer – comme les moins mal lotis de nos concitoyens (logement, nourriture, biens de consommation, déplacement,etc.). Voilà la mère de tous les problèmes. Si tel est bien le cas, la solution à tous les problèmes (stabilisation de la croissance démographique, partage des richesses, partage des terres, partage de l’eau, protection de la biodiversité, règlement des conflits, lutte contre la déforestation, lutte contre dérèglement climatique, règlement des conflits, stabilisation des migrations, concurrence commerciale mondiale, etc.) ne sera ni française, ni européenne, mais mondiale. La France se doit d’y jouer un rôle moteur. Le reste, autour duquel se déroulera « le Grand débat », paraît assez anecdotique.
----------------------------------------------------------------------------------------------------
problème trouver confronter ensemble planète dénoncer parfait désordre gilet jaune France devoir surpopulation mondial population passée d’1,5 milliard habitant 1900 milliard 2020 monter bientôt 10 milliard 2040 progrès communication village mondial individu fin fond Asie fin fond Afrique passant quartier campagne pays aspirer vivre blâmer mal lotir concitoyen logement nourriture consommation déplacement mère problème cas solution problème stabilisation croissance démographique partage richesse partage terrer partage eau protection biodiversité règlement conflit lutte déforestation lutte dérèglement climatique règlement conflit stabilisation migration concurrence commerciale mondial etc. français européen mondial France jouer rôle moteur autour dérouler Grand débat paraître anecdotique
Pour une approche bag of words (où l’ordre des mots ne compte pas) comme celle que nous allons adopter, le résultat semble tout à fait satisfaisant. Nous allons fonder la suite du traitement sur la version lemmatisée des réponses mais garder leur version originale pour l’affichage final. En regardant d’autres réponses on réalise que c’est encore largement améliorable mais nous nous contenterons de cela.
Calcul de la fréquence des mots
text_cols = lemm.columns[2:]
closed_questions = ['QUXVlc3Rpb246MTQ2', 'QUXVlc3Rpb246MTQ4', 'QUXVlc3Rpb246MTUy', 'QUXVlc3Rpb246MTU0']
all_text = []
for col in text_cols:
if col not in closed_questions:
all_text += list(lemm[col])
tokall = [word for answer in all_text for word in answer]
len(tokall)
Sortie : 10184700
Tout de même un milliard de mots en tout.
counts = Counter(tokall)
counts.most_common(10)
Sortie:
[('transport', 148757),
('faire', 100273),
('pouvoir', 88144),
('produit', 71253),
('falloir', 70599),
('voiture', 67852),
('commun', 67773),
('énergie', 51550),
('pollution', 48041),
('vélo', 44288)]
On est bien dans le thème. Si nous n’avions pas retiré les stop words, ce top aurait été sans intérêt.
total = sum(counts.values())
probas = {word: v / total for word, v in counts.items()}
Récupération des vecteurs de mots
Nous utiliserons les vecteurs FastText et la technique décrite dans cet article.
word_vec = {}
with io.open('wordemb/fasttext/cc.fr.300.vec', 'r', encoding='utf-8') as f:
next(f)
for line in f:
word, vec = line.split(' ', 1)
if word in probas:
word_vec[word] = np.fromstring(vec, sep=' ')
print('{} vecteurs de mots trouvés sur {} mots dans le texte lemmatisé.'.format(len(word_vec), len(probas)))
Sortie: 81469 vecteurs de mots trouvés sur 139753 mots dans le texte lemmatisé.
Les mots non reconnus sont ceux qui sont mal orthographiés (et encore FastText en contient déjà beaucoup), certains noms propres (certaines personnes donnent carrément leur coordonnées), et tyiquement ceux juste après un signe de ponctuation sans espace. Plus d’efforts pourraient bien sûr améliorer cela.
def answer2vec(answer):
a = 1e-3
vec = []
weights = []
for word in answer:
if word in word_vec or word.lower() in word_vec:
if word.lower() in word_vec and (word not in word_vec or probas[word.lower()] > probas[word]):
word = word.lower()
weight = a / (a + probas[word])
weights.append(weight)
vec.append(word_vec[word])
if not vec: # si la phrase ne contient aucun mot pour lequel nous avons un vecteur
weights = [1]
vec.append(np.zeros(300))
vec = np.average(vec, axis=0, weights=weights)
return vec
def column2vec(column):
s = lemm[column]
s = s.map(lambda x: answer2vec(x))
return s
svd = TruncatedSVD(n_components=1, n_iter=7, random_state=0)
res = []
for c in lemm.columns[2:]:
if c not in closed_questions:
vec_series = column2vec(c)
embeddings = np.vstack(vec_series.values)
svd.fit(embeddings)
pc = svd.components_
embeddings -= embeddings.dot(pc.transpose()) * pc
vec_series = pd.Series(data=list(embeddings), index=vec_series.index, name=vec_series.name)
res.append(vec_series)
vecs = pd.DataFrame(res).transpose()
Projection en dimension 2
Nous avons désormais une représentation vectorielle pour chaque réponse. Elle est en dimension 300 et personnellement j’ai du mal à visualiser les choses à partir de la dimension 250. Il paraît même que certaines personnes ont du mal au-delà de 3. Nous allons donc la réduire en dimension 2. Pour cela nous allons utiliser UMAP, la meilleure technique de réduction dimensionnelle que je connaisse. Les maths de l’article original sont incompréhensibles mais il y a heureusement une librairie facile à utiliser. Pour rendre le graphe plus facile à lire et alléger les calculs, nous allons sélectionner 5000 réponses au hazard à une question donnée.
n = 5000
qid = 'QUXVlc3Rpb246MTYw'
sample_vecs = vecs.sample(5000).sort_index()
sample_idx = sample_vecs.index
sample_text = data.loc[sample_idx]
embeddings = np.vstack(sample_vecs[qid].values)
print(embeddings.shape)
reducer = umap.UMAP(n_components=2, metric='cosine', n_neighbors=200)
reduced = reducer.fit_transform(embeddings)
print(reduced.shape)
Sortie:
(5000, 300)
(5000, 2)
Visualisation
J’utilise ici la librairie Plotly car elle rend la création de graphes interactifs très facile.
final_df = pd.DataFrame({
'text': sample_text[qid],
'x': reduced[:, 0],
'y': reduced[:, 1],
}, index=sample_idx)
def wrap_answer(txt, width=50):
if isinstance(txt, str):
return '<br>'.join(textwrap.wrap(txt, width=width))
else:
return txt
# permet d'afficher les réponses longues sur plusieurs lignes
final_df['display_text'] = final_df.text.apply(wrap_answer)
fig = px.scatter(final_df.dropna(), x='x', y='y', hover_data=['display_text'], width=1000, height=750,
title=wrap_answer(question_dic[qid], width=100))
fig.show()
Si le graphe n’apparaît pas, rechargez la page.
On voit deux grands clusters : les gens qui n’ont rien répondu à gauche et les autres à droite. En zoomant sur le second, on voit des concentrations de points à plusieurs endroits. Ces points correspondent à des réponses similaires. Cela marche plutôt bien sur cette question. On pourrait envisager de calculer les vecteurs de phrases avec un réseau de neurone pré-entraîné pour un résultat encore meilleur, mais je vais m’arrêter là pour le moment.
Conclusion
J’ai ici présenté une technique rapide et efficace pour visualiser un grand nombre de réponses à une question ouverte. Cette technique pourrait permettre une rapide exploration des grands thèmes présents dans les réponses afin de pouvoir plus tard les classifier avec un autre algorithme de NLP, voir tout simplement avec un classifieur linéaire sur les vecteurs obtenus par cette méthode. Elle est évidemment généralisable à plus que du texte, du moment que nous avons un moyen de transformer la donnée en vecteur.