La conception et l’élaboration de modèles d’apprentissage automatique applicables dans le monde réel ont toujours suscité un grand intérêt chez les scientifiques des données. Cela les a inévitablement conduits à exploiter des méthodes optimisées, efficaces et précises à grande échelle.
L’optimisation, tant au niveau de l’exécution que de la mémoire, joue un rôle fondamental dans la fourniture durable de solutions logicielles concrètes et orientées vers l’utilisateur.
Dans cet article, je vais te présenter une 7 techniques incroyables pour optimiser l’utilisation de la mémoire de tes DataFrames Pandas.
Ces astuces t’aideront à effectuer efficacement tes tâches typiques d’analyse, de gestion et de traitement des données tabulaires dans Pandas.
#1 – Apporter des modifications “inplace” au DataFrame
Une fois que nous avons chargé un DataFrame dans l’environnement Python, nous effectuons généralement un large éventail de modifications sur le DataFrame. Il s’agit notamment d’ajouter de nouvelles colonnes, de renommer les en-têtes, de supprimer des colonnes, de modifier les valeurs des lignes, de remplacer les valeurs NaN, et bien d’autres choses encore.
Ces opérations peuvent être effectuées de deux manières différentes, comme indiqué ci-dessous :
Affectation standard
L’affectation standard vise à créer une nouvelle copie du DataFrame après transformation, en laissant le DataFrame d’origine intact.
df_copy = df.fillna(0) print(df_copy)
colA colB colC 0 1.0 A 1.3 1 2.0 B 2.9 2 0.0 C 5.6
En raison de l’affectation standard, deux DataFrames Pandas distincts (celui d’origine et un transformé) coexistent dans l’environnement (df et df_copy ci-dessus), ce qui double l’utilisation de la mémoire.
Affectation inplace
Contrairement aux opérations d’affectation standard, les opérations d’affectation “inplace” visent à modifier le DataFrame d’origine sans créer un nouvel objet Pandas DataFrame. La démonstration est faite ci-dessous :
df.fillna(0, inplace = True) print(df)
colA colB colC 0 1.0 A 1.3 1 2.0 B 2.9 2 0.0 C 5.6
Par conséquent, si la copie intermédiaire du DataFrame (df_copy ci-dessus) n’est d’aucune utilité dans ton projet, l’affectation inplace est la solution idéale pour les applications à mémoire limitée.
Principaux enseignements/réflexions finales :
- Utilise l’affectation standard (ou inplace=False) lorsque le DataFrame intermédiaire est nécessaire et que tu ne souhaites pas modifier l’entrée.
- Utilise l’affectation inplace (ou inplace=True) si tu travailles avec des contraintes de mémoire et que tu n’as pas d’usage particulier du DataFrame intermédiaire.
#2 – Lire uniquement les colonnes requises à partir d’un CSV
Imagine la situation dans laquelle tu as des centaines de colonnes dans ton fichier CSV, et dont seul un sous-ensemble de colonnes t’intéresse.
Prenons par exemple les 5 premières lignes du DataFrame fictif que j’ai créé avec 25 colonnes et 10⁵ lignes à l’aide de Faker (nom de fichier : dummy_dataset.csv) :
Parmi ces 25 colonnes, disons que seules 5 colonnes nous intéressent au plus haut point et que tu souhaites les charger sous la forme d’un DataFrame Pandas. Ces colonnes sont les suivantes : Employee_ID, First_Name, Salary, Rating et Company.
- Chargement de TOUTES les colonnes
Si tu devais lire l’intégralité du fichier CSV dans l’environnement Python, cela obligerait Pandas à charger les colonnes qui ne sont pas utiles et à déduire leurs types de données, ce qui entraînerait une augmentation du temps d’exécution et de l’utilisation de la mémoire.
Nous pouvons connaître l’utilisation de la mémoire d’un DataFrame Pandas en utilisant la méthode info() comme indiqué ci-dessous :
data = pd.read_csv("dummy_dataset.csv") data.info(memory_usage = "deep")
<class 'pandas.core.frame.DataFrame'> RangeIndex: 100000 entries, 0 to 99999 Data columns (total 25 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Employee_ID 100000 non-null int64 1 First_Name 100000 non-null object 2 Last_Name 100000 non-null object 3 DOB 100000 non-null object 4 Address 100000 non-null object 5 Zipcode 100000 non-null int64 6 Salary 100000 non-null float64 7 Employment_Type 100000 non-null object 8 Rating 60107 non-null float64 9 City 100000 non-null object 10 State 100000 non-null object 11 Country 100000 non-null object 12 Company 100000 non-null object 13 Company_Email 100000 non-null object 14 Country_Code 99532 non-null object 15 Language 100000 non-null object 16 Domain_Name 100000 non-null object 17 Currenct_Code 100000 non-null object 18 Color 100000 non-null object 19 URL 100000 non-null object 20 Fav_Number 100000 non-null int64 21 Father_Name 100000 non-null object 22 Father_DOB 100000 non-null object 23 Mother_Name 100000 non-null object 24 Mother_DOB 100000 non-null object dtypes: float64(2), int64(3), object(20) memory usage: 137.0 MB
Le DataFrame occupe 137 Mo d’espace en mémoire avec les 25 colonnes chargées. Le temps d’exécution pour charger le fichier CSV est calculé ci-dessous :
%timeit pd.read_csv("dummy_dataset.csv")
728 ms ± 19.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
- Chargement des colonnes nécessaires :
Contrairement à la lecture de toutes les colonnes, si seul un sous-ensemble de colonnes nous intéresse, tu peux les passer sous la forme de liste à l’argument usecols de la méthode pd.read_csv().
Le calcul de l’utilisation de la mémoire est illustré ci-dessous :
col_list = ["Employee_ID", "First_Name", "Salary", "Rating", "Company"] data = pd.read_csv("dummy_dataset.csv", usecols=col_list) data.info(memory_usage = "deep")
<class 'pandas.core.frame.DataFrame'> RangeIndex: 100000 entries, 0 to 99999 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Employee_ID 100000 non-null int64 1 First_Name 100000 non-null object 2 Salary 100000 non-null float64 3 Rating 60107 non-null float64 4 Company 100000 non-null object dtypes: float64(2), int64(1), object(2) memory usage: 15.3 MB
Le chargement des seules colonnes intéressantes a permis de réduire l’utilisation de la mémoire de près de 9 fois, en occupant environ 15 Mo d’espace au lieu de 137 Mo auparavant.
Le temps d’exécution du chargement est également réduit de manière significative, offrant un gain de près de 4 fois par rapport au chargement de toutes les colonnes.
%timeit pd.read_csv("dummy_dataset.csv", usecols=col_list)
271 ms ± 19.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Principaux enseignements/réflexions finales :
- Le fait de ne charger que les colonnes nécessaires peut améliorer de manière significative le temps d’exécution et l’utilisation de la mémoire. Par conséquent, avant de charger un fichier CSV volumineux, ne charge que quelques lignes (par exemple les 5 premières) et répertorie les colonnes qui t’intéressent.
#3 à #5 Modifier le type de données des colonnes
Par défaut, Pandas attribue toujours le type de données de mémoire le plus élevé aux colonnes. Par exemple, si Pandas interprète une colonne comme étant de type entier (integer), il est possible de choisir entre quatre sous-catégories :
- int8 : entier de 8 bits qui couvre les entiers de [-2⁷, 2⁷].
- int16 : entier de 16 bits couvrant les entiers compris entre [-2¹⁵, 2¹⁵].
- int32 : entier de 32 bits couvrant les entiers de [-2³¹, 2³¹].
- int64 : entier de 64 bits qui couvre les entiers de [-2⁶³, 2⁶³].
Cependant, Pandas attribuera toujours int64 comme type de données de la colonne à valeurs entières, quelle que soit la plage des valeurs actuelles de la colonne.
Des connotations similaires existent également pour les nombres à valeurs flottantes : float16, float32 et float64.
Remarque : je vais me référer au même ensemble de données fictives (dummy_dataset.csv) que celui dont nous avons parlé dans la partie précédente. De nouveau, voici les types de données :
data = pd.read_csv("dummy_dataset.csv") data.info(memory_usage = "deep")
<class 'pandas.core.frame.DataFrame'> RangeIndex: 100000 entries, 0 to 99999 Data columns (total 25 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Employee_ID 100000 non-null int64 1 First_Name 100000 non-null object 2 Last_Name 100000 non-null object 3 DOB 100000 non-null object 4 Address 100000 non-null object 5 Zipcode 100000 non-null int64 6 Salary 100000 non-null float64 7 Employment_Type 100000 non-null object 8 Rating 60107 non-null float64 9 City 100000 non-null object 10 State 100000 non-null object 11 Country 100000 non-null object 12 Company 100000 non-null object 13 Company_Email 100000 non-null object 14 Country_Code 99532 non-null object 15 Language 100000 non-null object 16 Domain_Name 100000 non-null object 17 Currenct_Code 100000 non-null object 18 Color 100000 non-null object 19 URL 100000 non-null object 20 Fav_Number 100000 non-null int64 21 Father_Name 100000 non-null object 22 Father_DOB 100000 non-null object 23 Mother_Name 100000 non-null object 24 Mother_DOB 100000 non-null object dtypes: float64(2), int64(3), object(20) memory usage: 137.0 MB
L’utilisation actuelle de la mémoire du DataFrame est de 137 Mo.
#3 – Modification du type de données des colonnes d’entiers
Considérons la colonne Employee_ID et trouvons ses valeurs maximale et minimale.
print("Le type de données de la colonne Employee_ID est", data.Employee_ID.dtype) print("La valeur maximale de la colonne Employee_ID est de", data.Employee_ID.max()) print("La valeur minimale de la colonne Employee_ID est de", data.Employee_ID.min())
Le type de données de la colonne Employee_ID est int64 La valeur maximale de la colonne Employee_ID est de 100000 La valeur minimale de la colonne Employee_ID est de 1
Remarque que même si la colonne peut être potentiellement interprétée comme int32 (2¹⁵< 10⁵ < 2³¹), Pandas a quand même adopté le type int64 pour la colonne.
Heureusement, Pandas offre la possibilité de changer le type de données d’une colonne en utilisant la méthode astype().
La conversion de la colonne Employee_ID, ainsi que l’utilisation de la mémoire avant et après la conversion, sont démontrées ci-dessous :
print("Utilisation de la mémoire avant la modification du type de données : ", data.Employee_ID.memory_usage()) data["Employee_ID"] = data.Employee_ID.astype(np.int32) print("Utilisation de la mémoire après modification du type de données :", data.Employee_ID.memory_usage())
Utilisation de la mémoire avant la modification du type de données : 800128 Utilisation de la mémoire après modification du type de données : 400128
La mémoire totale utilisée par la colonne Employee_ID a été divisée par 2 grâce à cette simple transformation de type de données (en une seule ligne).
Avec une analyse min-max similaire, tu peux également modifier le type de données d’autres colonnes à valeurs entières et flottantes.
#4 – Modification du type de données des colonnes représentant des données catégorielles
Comme son nom l’indique, une colonne catégorielle est une colonne qui ne comporte que quelques valeurs uniques répétées à l’infini dans toute la colonne.
Par exemple, trouvons le nombre de valeurs uniques dans ces quelques colonnes en utilisant la méthode nunique() comme indiqué ci-dessous :
print("Nombre d'enregistrements uniques :", data.shape[0]) print("Nombre de pays uniques :", data.Country.nunique()) print("Nombre de types d'emploi uniques :", data.Employment_Type.nunique()) print("Nombre de langues uniques :", data.Language.nunique()) print("Nombre de couleurs uniques :", data.Color.nunique()) print("Nombre de codes pays uniques :", data.Country_Code.nunique())
Nombre d'enregistrements uniques : 100000 Nombre de pays uniques : 243 Nombre de types d'emploi uniques : 2 Nombre de langues uniques : 182 Nombre de couleurs uniques : 140 Nombre de codes pays uniques : 194
Le nombre de valeurs uniques dans ces colonnes par rapport à la taille du DataFrame indique qu’il s’agit de colonnes catégorielles.
Cependant, par défaut, Pandas a déduit que le type de données de toutes ces colonnes était object, qui est essentiellement un type de chaîne string.
print("Datatype de Countries :", data.Country.dtype) print("Datatype de Employment Types :", data.Employment_Type.dtype) print("Datatype de Languages :", data.Language.dtype) print("Datatype de Color :", data.Color.dtype) print("Datatype de Country Codes :", data.Country_Code.dtype)
Datatype de Countries : object Datatype de Employment Types : object Datatype de Languages : object Datatype de Color : object Datatype de Country Codes : object
En utilisant la méthode astype(), tu peux changer le type de données d’une colonne catégorielle en category. La réduction de l’utilisation de la mémoire est démontrée ci-dessous :
print("Utilisation de la mémoire avant la modification du type de données :", data.Country.memory_usage()) data["Country"] = data.Country.astype("category") print("Utilisation de la mémoire après modification du type de données :", data.Country.memory_usage())
Utilisation de la mémoire avant la modification du type de données : 800128 Utilisation de la mémoire après modification du type de données : 210368
Avec la conversion de string à categorical, nous remarquons une diminution de l’utilisation de la mémoire de 75%, ce qui est énorme.
Avec une analyse similaire de l’élément unique, tu peux modifier le type de données d’autres colonnes catégorielles potentielles.
#5 – Modification du type de données des colonnes contenant des valeurs NaN
Les valeurs manquantes sont inévitables dans les ensembles de données du monde réel, n’est-ce pas ? Considérons qu’une colonne de notre DataFrame comporte une proportion importante de valeurs NaN, comme illustré ci-dessous :
print("Nombre d'enregistrements :", data.shape[0]) print("Nombre de valeurs NaN :", data.Rating.isna().sum()) print("Datatype de Rating :", data.Rating.dtype)
Nombre d'enregistrements : 100000 Nombre de valeurs NaN : 39893 Datatype de Rating : float64
Dans un tel scénario, la représentation de la colonne en tant que structure de données Sparse peut permettre d’améliorer considérablement l’efficacité de la mémoire.
En utilisant la méthode astype(), tu peux changer le type de données d’une colonne sparse en type de données Sparse[str] / Sparse[float] / Sparse[int]. La réduction de l’utilisation de la mémoire et la conversion du type de données sont démontrées ci-dessous :
print("Utilisation de la mémoire avant la modification du type de données :", data.Rating.memory_usage()) data["Rating"] = data.Rating.astype("Sparse[float32]") print("Utilisation de la mémoire après modification du type de données :", data.Rating.memory_usage())
Utilisation de la mémoire avant la modification du type de données : 800128 Utilisation de la mémoire après modification du type de données : 480984
La conversion de float32 en Sparse[float32] réduit l’utilisation de la mémoire de près de 40 %, ce qui correspond approximativement au pourcentage de valeurs NaN dans la colonne Rating.
Principaux enseignements/réflexions finales :
- Pandas interprète toujours ses colonnes avec le plus grand type de données de la mémoire. Si la plage de valeurs de ta colonne ne couvre pas la portée du type de données, envisage de rétrograder le type de données de la colonne vers le type le plus optimal.
Tu peux trouver un code de référence pour exécuter ces conversions de type de données dans ce post StackOverflow.
#6 – Spécifier le type de données d’une colonne lors de la lecture d’un CSV
Les conseils présentés dans les sections #3 à #5 ci-dessus supposent que tu as déjà chargé un DataFrame Pandas dans l’environnement Python. En d’autres termes, il s’agit de techniques d’optimisation de l’utilisation de la mémoire après l’entrée.
Cependant, dans les situations où le chargement de l’ensemble de données est le principal défi, tu peux prendre le contrôle de la tâche d’interprétation du type de données effectuée par Pandas pendant l’entrée et spécifier le type de données particulier que tu veux pour trs colonnes.
Tu peux y parvenir en passant l’argument dtype à la méthode pd.read_csv() comme suit :
col_list = ["Employee_ID", "First_Name", "Salary", "Rating", "Country_Code"] data = pd.read_csv("dummy_dataset.csv", usecols=col_list, dtype = {"Employee_ID":np.int32, "Country_Code":"category"}) data.info(memory_usage = "deep")
<class 'pandas.core.frame.DataFrame'> RangeIndex: 100000 entries, 0 to 99999 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Employee_ID 100000 non-null int32 1 First_Name 100000 non-null object 2 Salary 100000 non-null float64 3 Rating 60107 non-null float64 4 Country_Code 99532 non-null category dtypes: category(1), float64(2), int32(1), object(1) memory usage: 8.1 MB
Comme indiqué ci-dessus, l’argument dtype attend un dictionnaire de correspondance entre le nom de la colonne (column-name) et le type de données (data-type).
Principaux enseignements/réflexions finales :
- Si tu connais le type de données contenues dans certaines (ou toutes) les colonnes d’un CSV, que ce soit par le biais d’un dictionnaire de données ou d’une autre source, essaye de déduire toi-même le type de données le plus approprié et passe-le à l’argument dtype de la méthode pd.read_csv().
#7 – Lire des données en morceaux à partir d’un fichier CSV
Enfin, supposons que tu aies fait tout ce que vous pouviez faire dans l’astuce #6, mais que le CSV soit toujours impossible à charger en raison de contraintes de mémoire.
Bien que ma dernière technique ne permette pas d’optimiser l’utilisation nette de la mémoire, il s’agit plutôt d’une solution de contournement pour le chargement de grands ensembles de données, que tu peux utiliser dans de telles situations extrêmes.
Les méthodes d’entrée de Pandas sont sérialisées. Par conséquent, elles ne lisent qu’une ligne à la fois à partir d’un fichier CSV.
Si le nombre de lignes est trop important pour être chargé en mémoire en une seule fois, tu peux charger un segment (ou morceau = chunk en anglais) de lignes, le traiter, puis lire le segment suivant du fichier CSV. Cette méthode est illustrée ci-dessous :
Tu peux tirer parti du processus d’entrée basé sur les morceaux (chunks) ci-dessus en passant l’argument chunksize à la méthode pd.read_csv() comme suit :
for chunk in pd.read_csv("dummy_dataset.csv", chunksize=50000): ## processus chunk pass
Chaque objet chunk est un DataFrame Pandas, et nous pouvons le vérifier en utilisant la méthode type() de Python comme suit :
for chunk in pd.read_csv("dummy_dataset.csv", chunksize=50000): print(type(chunk))
<class 'pandas.core.frame.DataFrame'> <class 'pandas.core.frame.DataFrame'>
Principaux enseignements/réflexions finales :
- Si le fichier CSV est trop volumineux pour être chargé et pour tenir dans la mémoire, utilise la méthode de découpage en morceaux pour charger des segments du CSV et les traiter l’un après l’autre.
- L’un des principaux inconvénients de cette méthode c’est qu’elle ne permet pas d’effectuer des opérations nécessitant l’intégralité du DataFrame. Par exemple, supposons que tu souhaites effectuer une opération groupby() sur une colonne. Dans ce cas, il est possible que les lignes correspondant à un groupe se trouvent dans des morceaux/chunks différents.
Pour conclure, dans cet article, j’ai discuté de 7 techniques incroyables d’optimisation de la mémoire avec Pandas, que tu peux directement exploiter dans tes prochains projets de Data Science.
À mon avis, les domaines que j’ai abordés dans cet article sont des moyens subtils d’optimiser l’utilisation de la mémoire, que l’on oublie souvent de chercher à optimiser. Néanmoins, j’espère que cet article t’aura permis de mieux comprendre ces fonctions quotidiennes de Pandas.
Merci de m’avoir lu !