Python: Calculer les classes ABC grâce à Pandas

Pandas propose une fonction qui permet de calculer très facilement les classes ABC.
Cette fonction se nomme cut

>>> import pandas as pd
>>> import numpy as np

Pour générer un dataframe avec des données aléatoires, voici une petite astuce:

>>> import random
>>> random.seed()
>>> df = pd.DataFrame(np.random.randint(0,1000,100), index=random.sample(range(100000, 999999), 100), columns=['stocks'])

Ca permet de générer un dataframe de 100 lignes avec une colonne "stocks" et contenant des valeurs comprises entre 0 et 1000 et en index, une liste d'articles uniques compris entre 100000 et 999999.

Exemple avec le dataframe suivant:

>>> df
             stocks
761920     965
636899     147
835336     324
366511     536
852098     544
        ...
170837     319
886380      98
676491     201
639583     233
854389     292

[100 rows x 1 columns]

En index, les références de mes articles et les quantités en stock dans la colonne 'stocks'.

J'ai au total 100 articles dans mon dataframe.

Premièrement, trier les données par stocks décroissant:

>>> df = df.sort_values('stocks', ascending=False)
>>> df
              stocks
506289     979
549641     977
351719     967
761920     965
874506     962
        ...
332012      67
797080      41
347642      38
417762      28
284562      13
[100 rows x 1 columns]

Ensuite, calculer le cumul des stocks:

>>> df['cumulStocks'] = df['stocks'].cumsum()
>>> df
              stocks  cumulStocks
506289     979          979
549641     977         1956
351719     967         2923
761920     965         3888
874506     962         4850
        ...          ...
332012      67        49118
797080      41        49159
347642      38        49197
417762      28        49225
284562      13        49238
[100 rows x 2 columns]

Après, il faut calculer le pourcentage du cumul du stock par rapport à la somme totale du stock:

>>> df['%cumulStocks'] = round(100 * df['cumulStocks'] / df['stocks'].sum(), 3)
>>> df
              stocks  cumulStocks  %cumulStocks
506289     979          979         1.988
549641     977         1956         3.973
351719     967         2923         5.936
761920     965         3888         7.896
874506     962         4850         9.850
        ...          ...           ...
332012      67        49118        99.756
797080      41        49159        99.840
347642      38        49197        99.917
417762      28        49225        99.974
284562      13        49238       100.000
[100 rows x 3 columns]

Nous ajoutons une colonne afin d'indiquer le rang des articles:

>>> df['rang'] = df.rank(ascending=False)
>>> df
               stocks  cumulStocks  %cumulStocks  rang
506289     979          979         1.988     1.0
549641     977         1956         3.973     2.0
351719     967         2923         5.936     3.0
761920     965         3888         7.896     4.0
874506     962         4850         9.850     5.0
        ...          ...           ...   ...
332012      67        49118        99.756    96.0
797080      41        49159        99.840    97.0
347642      38        49197        99.917    98.0
417762      28        49225        99.974    99.0
284562      13        49238       100.000   100.0
[100 rows x 4 columns]

Pour finir, calculons les classes ABC avec les pourcentages suivants:
Je veux que ma classe A concerne 80% des stocks cumulés, ma classe B les 15% suivants et ma classe C les 5 % restants.

>>> classes = ['A', 'B', 'C']
>>> pourcent = [0, 80, 95, 100]
>>> df['classe'] = pd.cut(df['%cumulStocks'], bins=pourcent, labels=classes)
>>> df
        stocks  cumulStocks  %cumulStocks  rang classe
506289     979          979         1.988     1.0      A
549641     977         1956         3.973     2.0      A
351719     967         2923         5.936     3.0      A
761920     965         3888         7.896     4.0      A
874506     962         4850         9.850     5.0      A
        ...          ...           ...   ...    ...
332012      67        49118        99.756    96.0      C
797080      41        49159        99.840    97.0      C
347642      38        49197        99.917    98.0      C
417762      28        49225        99.974    99.0      C
284562      13        49238       100.000   100.0      C
[100 rows x 5 columns]

Je me retrouve avec la répartition suivante, 56 articles en classe A, 22 articles en classe B et 22 articles en classe C

>>> df['classe'].value_counts(sort=False)
A    56
B    22
C    22
Name: classe, dtype: int64

La classe A contient bien tous les articles dont le pourcentage du stock cumulé est inférieur ou égal à 80%

>>> df[df['classe']=='A']['%cumulStocks'].describe()[['min','max']]
min       1.988000
max      79.776000
Name: %cumulStocks, dtype: float64

La classe B contient tous les articles supérieur à 80% et inférieur ou égal à 95%

>>> df[df['classe']=='B']['%cumulStocks'].describe()[['min','max']]
min      80.584000
max      94.549000
Name: %cumulStocks, dtype: float64

Enfin, la classe C contient tous les articles supérieur à 95% et inférieur ou égal à 100%

>>> df[df['classe']=='C']['%cumulStocks'].describe()[['min','max']]
min       95.022000
max      100.000000
Name: %cumulStocks, dtype: float64

Ajoutons une colonne indiquant le pourcentage du rang:

>>> df['%rang'] = round(100 * df['rang'] / len(df), 3)
>>> df
        stocks  cumulStocks  %cumulStocks  rang classe  %rang
506289     979          979         1.988     1.0      A    1.0
549641     977         1956         3.973     2.0      A    2.0
351719     967         2923         5.936     3.0      A    3.0
761920     965         3888         7.896     4.0      A    4.0
874506     962         4850         9.850     5.0      A    5.0
        ...          ...           ...   ...    ...    ...
332012      67        49118        99.756    96.0      C   96.0
797080      41        49159        99.840    97.0      C   97.0
347642      38        49197        99.917    98.0      C   98.0
417762      28        49225        99.974    99.0      C   99.0
284562      13        49238       100.000   100.0      C  100.0
[100 rows x 6 columns]

Vérifions maintenant si Pareto a toujours raison, à savoir 20% de mes articles doivent représenter 80% de mon stock (plus haut, la répartition indiquait 56 articles dans la classe A, par conséquent 56% puisque j'ai un échantillon de 100 articles):

>>> df[df['classe']=='A'].describe().loc['max',['%cumulStocks','%rang']]
%cumulStocks    79.776
%rang           56.000
Name: max, dtype: float64

On voit bien que 80% de mon stock est constitué par 56% de mes articles.

Sacré Pareto...