Skip to article frontmatterSkip to article content
import pandas as pd

on a fait un sondage, on a demandé à des gens ce qu’ils pensaient devoir être amélioré dans leur environnement

les réponses se présentent comme cela, chaque ligne correspond à une réponse

df = pd.read_csv(
    'data/split-count-clean.csv', sep=';')
df
Loading...

on veut faire une synthèse, et pour cela on voudrait transformer ça pour en faire ceci

citybinsflowerstoiletshomeanswers
aberdeen13213
...
london12102

par exemple pour pouvoir les dessiner simplement

remarquez la colonne answers qui décompte le nombre total de réponses dans cette ville

on suppose pour commencer que toutes les réponses sont “propres” c’est-à-dire que toutes les réponses qui parlent de fleurs sont orthographiées de la même façon

groupement

il faut donc grouper les lignes par ville, et faire une sorte de somme sur les sous-groupes

idée #1

groups = df.groupby(by='city')
groups.aggregate("sum")
Loading...

mais ça c’est pas terrible parce qu’on a des chaines et qu’on va avoir du mal à compter; et aussi vous remarquez le flowersbathroom parce qu’on a additionné les chaines brutalement..

donc ce serait mieux de penser en termes de liste

idée #2

avant de faire la somme, on éclate les chaines en utilisant le séparateur ‘,’

comment on fait ça ? c’est le propos de la méthode str.split()

et rappelez vous, .str est utile lorsqu’on veut appliquer une méthode de chaine sur une df

df2 = df.copy()
df2['to-improve'] = (
    df2['to-improve']
        .str.replace(' ','')  # ça ne fait pas de mal de nettoyer
        .str.split(',')
)
df2
Loading...
# maintenant je peux faire la somme entre ces listes

totals = df2.groupby('city').aggregate(sum)
totals
Loading...

ça progresse...

faire le compte

pour faire le compte sur chaque liste, il y a en Python de base une classe Counter

https://docs.python.org/3/library/collections.html#collections.Counter

from collections import Counter

c’est peut-être un peu inhabituel pour les gens qui ne pratiquent pas Python, mais comme c’est une classe, on peut l’appeler pour construire un objet - de type Counter donc

# un exemple d'appel de Counter sur une liste

Counter(['john', 'mary', 'mary', 'john', 'john', 'john', 'mary'])
Counter({'john': 4, 'mary': 3})
# je peux donc utiliser apply sur la série 'to-improve'
# pour calculer une nouvelle série dont les éléments sont des objets 'Counter'

totals['to-improve'].apply(Counter)
city aberdeen {'bins': 1, 'flowers': 3, 'toilets': 2, 'home'... london {'flowers': 2, 'toilets': 1, 'bins': 1} oxford {'bins': 3, 'flowers': 2, 'toilets': 1, 'home'... Name: to-improve, dtype: object
# et pour l'insérer dans la dataframe, à la place de la précédente

totals['to-improve'] = totals['to-improve'].apply(Counter)
totals
Loading...

digression

# il faut savoir qu'un objet Counter est aussi un dictionnaire

c = Counter(['john', 'mary', 'mary', 'john', 'john', 'john', 'mary'])
isinstance(c, dict)
True
# on peut donc créer une Series à partir d'un Counter

pd.Series(c)
john 4 mary 3 dtype: int64

éclater

Maintenant ce qu’on va faire c’est en gros éclater chaque cellule de droite en ... une nouvelle Series
et là c’est un peu magique il faut bien avouer:

  1. d’abord la création de la Series à partir de Counter; qui fait exactement ce qu’on veut, les clés dans l’objet Counter servent à remplir l’index de la Series

  2. ensuite en remplaçant chaque valeur dans la Series par une nouvelle Series, on crée .. une dataframe

# pour bien voir le point #1:

pd.Series(Counter([True, False, False, True, True, True, False]))
True 4 False 3 dtype: int64
# et maintenant grâce au point 2, on obtient .. ce qu'on voulait

improvements = totals['to-improve'].apply(pd.Series)
improvements
Loading...

enfin presque

à ce stade, il nous reste à faire:

  1. compter le nombre de réponses (la colonne answers)

  2. remplacer les n/a par zéro

  3. enfin si on regarde attentivement, il y a une colonne en trop, avec un nom vide

c’est lié à la ligne #4 dans la df de départ, la chaine se termine par une ,
du coup quand on fait le split() ça nous ajoute une chaine vide, parce que:

# remarquez la chaine vide à la fin du résultat

"a,b,c,".split(',')
['a', 'b', 'c', '']

le nombre de réponses

answers = df.groupby(by='city').aggregate('count')

complete = improvements.join(answers)
complete
Loading...
# en option on peut renommer la colonne 'to-improve'

complete = complete.rename(columns={'to-improve': 'answers'})
complete
Loading...

fillna

# on en profite pour remettre des entiers ...

complete.fillna(value=0, inplace=True, downcast='infer')
complete
Loading...

la colonne ‘chaine vide’

on aurait pu traiter le problème à un stade plus précoce, mais à ce stade-ci on peut toujours simplement enlever la colonne

complete.columns
Index(['bins', 'flowers', 'toilets', 'home', '', 'answers'], dtype='object')
# à n'exécuter qu'une seule fois

# del complete['']

# du coup juste pour être tolérant par rapport à un éventuel double-run
if '' in complete:
    del complete['']
complete
Loading...

dessiner

# ça nécessite un `pip install ipympl`

%matplotlib ipympl
import matplotlib.pyplot as plt
# pas forcément très intéressant, car df.plot() dessine les colonnes

# complete.plot();
# du coup c'est plus pertinent de le faire sur la transposée

complete.T.plot();
Loading...

v2

pour les rapides, à titre d’exercice:

en vrai les données ne sont pas propres, les gens ont utilisé des synonymes

df = pd.read_csv('data/split-count.csv', sep=';')
df
Loading...

pour essayer de gérer la diversité on se définit - à la main - un tableau de synonymes

synonyms = {
  'bins': ['bin', 'trash', 'dump'],
  'toilets': ['toilet', 'bathroom', 'restroom'],
}

et on va utiliser ça pour dire que, par exemple tous les mots qui contiennent “bathroom” ou “restroom” ou “toilet” seront comptabilisés dans la colonne “toilets”

# à vous de jouer...