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