import pandas as pdon 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=';')
dfon veut faire une synthèse, et pour cela on voudrait transformer ça pour en faire ceci
| city | bins | flowers | toilets | home | answers |
|---|---|---|---|---|---|
| aberdeen | 1 | 3 | 2 | 1 | 3 |
| ... | |||||
| london | 1 | 2 | 1 | 0 | 2 |
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")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# maintenant je peux faire la somme entre ces listes
totals = df2.groupby('city').aggregate(sum)
totals---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 3
1 # maintenant je peux faire la somme entre ces listes
2
----> 3 totals = df2.groupby('city').aggregate(sum)
4 totals
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/generic.py:2335, in DataFrameGroupBy.aggregate(self, func, engine, engine_kwargs, *args, **kwargs)
2333 gba = GroupByApply(self, [func], args=(), kwargs={})
2334 try:
-> 2335 result = gba.agg()
2337 except ValueError as err:
2338 if "No objects to concatenate" not in str(err):
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/apply.py:297, in Apply.agg(self)
294 return self.agg_dict_like()
295 elif is_list_like(func):
296 # we require a list, but not a 'str'
--> 297 return self.agg_list_like()
299 # caller can react
300 return None
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/apply.py:414, in Apply.agg_list_like(self)
406 def agg_list_like(self) -> DataFrame | Series:
407 """
408 Compute aggregation in the case of a list-like argument.
409
(...) 412 Result of aggregation.
413 """
--> 414 return self.agg_or_apply_list_like(op_name="agg")
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/apply.py:1640, in GroupByApply.agg_or_apply_list_like(self, op_name)
1635 # Only set as_index=True on groupby objects, not Window or Resample
1636 # that inherit from this class.
1637 with com.temp_setattr(
1638 obj, "as_index", True, condition=hasattr(obj, "as_index")
1639 ):
-> 1640 keys, results = self.compute_list_like(op_name, selected_obj, kwargs)
1641 result = self.wrap_results_list_like(keys, results)
1642 return result
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/apply.py:473, in Apply.compute_list_like(self, op_name, selected_obj, kwargs)
467 colg = obj._gotitem(col, ndim=1, subset=selected_obj.iloc[:, index])
468 args = (
469 [self.axis, *self.args]
470 if include_axis(op_name, colg)
471 else self.args
472 )
--> 473 new_res = getattr(colg, op_name)(func, *args, **kwargs)
474 results.append(new_res)
475 indices.append(index)
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/generic.py:464, in SeriesGroupBy.aggregate(self, func, engine, engine_kwargs, *args, **kwargs)
462 kwargs["engine"] = engine
463 kwargs["engine_kwargs"] = engine_kwargs
--> 464 ret = self._aggregate_multiple_funcs(func, *args, **kwargs)
465 if relabeling:
466 # columns is not narrowed by mypy from relabeling flag
467 assert columns is not None # for mypy
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/generic.py:522, in SeriesGroupBy._aggregate_multiple_funcs(self, arg, *args, **kwargs)
520 for idx, (name, func) in enumerate(arg):
521 key = base.OutputKey(label=name, position=idx)
--> 522 results[key] = self.aggregate(func, *args, **kwargs)
524 if any(isinstance(x, DataFrame) for x in results.values()):
525 from pandas import concat
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/generic.py:493, in SeriesGroupBy.aggregate(self, func, engine, engine_kwargs, *args, **kwargs)
484 obj = self._obj_with_exclusions
485 return self._wrap_aggregated_output(
486 self.obj._constructor(
487 [],
(...) 491 )
492 )
--> 493 return self._python_agg_general(func, *args, **kwargs)
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/generic.py:501, in SeriesGroupBy._python_agg_general(self, func, *args, **kwargs)
498 f = lambda x: func(x, *args, **kwargs)
500 obj = self._obj_with_exclusions
--> 501 result = self._grouper.agg_series(obj, f)
502 res = obj._constructor(result, name=obj.name)
503 return self._wrap_aggregated_output(res)
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/ops.py:988, in BaseGrouper.agg_series(self, obj, func, preserve_dtype)
972 @final
973 def agg_series(
974 self, obj: Series, func: Callable, preserve_dtype: bool = False
975 ) -> ArrayLike:
976 """
977 Parameters
978 ----------
(...) 986 np.ndarray or ExtensionArray
987 """
--> 988 result = self._aggregate_series_pure_python(obj, func)
989 return obj.array._cast_pointwise_result(result)
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/ops.py:1001, in BaseGrouper._aggregate_series_pure_python(self, obj, func)
998 splitter = self._get_splitter(obj)
1000 for i, group in enumerate(splitter):
-> 1001 res = func(group)
1002 res = extract_result(res)
1004 if not initialized:
1005 # We only do this validation on the first iteration
File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/groupby/generic.py:498, in SeriesGroupBy._python_agg_general.<locals>.<lambda>(x)
497 def _python_agg_general(self, func, *args, **kwargs):
--> 498 f = lambda x: func(x, *args, **kwargs)
500 obj = self._obj_with_exclusions
501 result = self._grouper.agg_series(obj, f)
TypeError: unsupported operand type(s) for +: 'int' and 'list'ça progresse...
faire le compte¶
pour faire le compte sur chaque liste, il y a en Python de base une classe Counter
https://
from collections import Counterc’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'])# 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)# et pour l'insérer dans la dataframe, à la place de la précédente
totals['to-improve'] = totals['to-improve'].apply(Counter)
totalsdigression¶
# il faut savoir qu'un objet Counter est aussi un dictionnaire
c = Counter(['john', 'mary', 'mary', 'john', 'john', 'john', 'mary'])
isinstance(c, dict)# on peut donc créer une Series à partir d'un Counter
pd.Series(c)é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:
d’abord la création de la
Seriesà partir deCounter; qui fait exactement ce qu’on veut, les clés dans l’objetCounterservent à remplir l’index de laSeriesensuite 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]))# et maintenant grâce au point 2, on obtient .. ce qu'on voulait
improvements = totals['to-improve'].apply(pd.Series)
improvementsenfin presque¶
à ce stade, il nous reste à faire:
compter le nombre de réponses (la colonne
answers)remplacer les n/a par zéro
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(',')le nombre de réponses¶
answers = df.groupby(by='city').aggregate('count')
complete = improvements.join(answers)
complete# en option on peut renommer la colonne 'to-improve'
complete = complete.rename(columns={'to-improve': 'answers'})
completefillna¶
# on en profite pour remettre des entiers ...
complete.fillna(value=0, inplace=True, downcast='infer')
completela 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# à 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['']
completedessiner¶
# ça nécessite un `pip install ipympl`
%matplotlib ipymplimport 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();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=';')
dfpour 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...