Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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
---------------------------------------------------------------------------
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://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'])
# 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)
totals

digression

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

  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]))
# et maintenant grâce au point 2, on obtient .. ce qu'on voulait

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

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(',')

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'})
complete

fillna

# on en profite pour remettre des entiers ...

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

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
# à 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

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

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

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