3 façons simples de traiter de plus gros ensembles de données avec Pandas

3 façons simples de traiter de plus gros ensembles de données avec Pandas

Les types que nous choisissons pour représenter nos données ont un impact sur la taille sous-jacente de leur représentation. Il existe une multitude de types de données, chacun ayant des précisions différentes et une consommation de mémoire différente. Pour être aussi économe en mémoire que possible, vous devez comprendre vos données et choisir les types les plus précis possibles pour les représenter.

Trouvez le code sur GitHub.

Concept

Pandas est incroyable pour traiter des données sur une seule machine, sur un seul cœur et lorsque cela tient en mémoire. Si vous travaillez avec de gros ensembles de données, vous pouvez facilement dépasser ces paramètres et devrez peut-être vous tourner vers d'autres outils pour gérer vos données. Avant de passer à l'informatique distribuée, avec des technologies comme Spark, il existe quelques optimisations simples qui vous aideront à mettre à l'échelle des ensembles de données plus importants.

Le principal facteur limitant est la quantité de mémoire nécessaire pour charger les données en mémoire. Si nous réduisons la taille des données, nous pouvons charger plus de données, les traiter plus rapidement... Considérez un simple fichier CSV avec deux colonnes : âge et emploi.

CSV avec deux colonnes. CSV avec deux colonnes.

Lorsque vous chargez des données en mémoire, vous devez définir les types pour chaque colonne de votre ensemble de données, ce qui est souvent fait par défaut. Pandas utilise des tableaux numpy pour représenter chaque colonne de votre dataframe. Ces tableaux numpy ont un dtype, qui est le type sous-jacent de tous les éléments du tableau. Lorsque vous utilisez pd.read_csv, Pandas devine les types de données de chaque colonne et attribue des tableaux numpy avec des types par défaut. Cependant, les types par défaut sont larges, utilisant plus d'espace que nécessaire pour représenter les différentes colonnes.

Représentation en mémoire des données chargées par Pandas sans optimisation. Représentation en mémoire des données chargées par Pandas sans optimisation.

Dans notre cas, nous obtiendrions une colonne d'âge avec un type int64 et une colonne d'emploi avec un type object. Cela n'est pas optimal car un âge a une plage [0, 100], n'ayant besoin que de 8 bits au lieu de 64. De plus, un emploi n'est pas « n'importe quelle chaîne », mais il est catégorique, nous avons très probablement une liste limitée d'emplois possibles et chaque emploi est un élément de cette liste.

Utilisez le bon dtype pour les colonnes numériques

Pandas n'infère pas le meilleur type pour nos colonnes numériques. Dans notre cas, nous pourrions représenter un âge avec un type uint8, car un âge est non signé (toujours positif) et sa plage est [0, 100]. Cependant, par défaut, Pandas utilise un int64 qui consomme 8 fois plus de mémoire.

La première étape consiste à forcer l'utilisation de types optimisés pour les colonnes numériques.

Utilisez le bon dtype pour les colonnes catégoriques

Lorsqu'une colonne est catégorique avec une faible cardinalité (le nombre de valeurs uniques est inférieur au nombre total de lignes -> beaucoup de répétitions), la représentation en chaîne de caractères est la pire. La chaîne de caractères est un type coûteux, selon le format (utf-8, ascii, ...) que nous utilisons entre 1 et 4 octets par caractère. Elle consomme rapidement une énorme quantité de mémoire lorsqu'elle est répétée des milliers de fois.

Mais il y a une meilleure façon. Il est plus efficace d'avoir un dictionnaire associant chaque valeur unique à un index avec un petit type, par exemple 1 octet. Ensuite, nous ne répétons qu'un seul octet des milliers de fois au lieu de la chaîne complète qui est beaucoup plus volumineuse. Heureusement, Pandas dispose d'un dtype pour gérer cela automatiquement pour vous, il vous suffit d'utiliser le dtype category.

Cela apporte généralement la plus grande amélioration de la consommation de mémoire. Le traitement des données catégoriques avec des chaînes de caractères simples est une énorme perte de ressources. Le type str est lourd au lieu d'une correspondance avec des indices répétés avec un type catégorique approprié.

Représentation en mémoire des données avec une optimisation des types. Pour la colonne AGE, nous utilisons uint8 au lieu de int64. Pour la colonne JOB, Student est associé à 0 et Data Scientist à 1. Ensuite, nous répétons 0 et 1 dans les différentes lignes de données au lieu de la lourde représentation en chaîne. Représentation en mémoire des données avec une optimisation des types. Pour la colonne AGE, nous utilisons uint8 au lieu de int64. Pour la colonne JOB, Student est associé à 0 et Data Scientist à 1. Ensuite, nous répétons 0 et 1 dans les différentes lignes de données au lieu de la lourde représentation en chaîne.

Dans le calcul ci-dessus, nous supposons que le plus petit morceau de mémoire que nous pouvons allouer est de 1 octet (8 bits). En effet, nous n'avons que deux valeurs différentes pour le vocabulaire, Data Scientist et Student. Pour représenter deux valeurs différentes, nous n'avons besoin que de 1 bit : soit 0 ou 1. Mais comme nous avons supposé que nous pouvons allouer un entier d'1 octet au minimum, nous obtenons 1 octet pour chaque entrée.

Chargez uniquement les colonnes pertinentes

Il est probable que vous n'utilisiez pas toutes les colonnes de votre ensemble de données pour mener une analyse spécifique. En utilisant l'argument usecols, vous pouvez charger uniquement les colonnes qui vous intéressent. C'est une astuce simple, mais elle vous aide à gérer facilement des ensembles de données volumineux.

Représentation en mémoire des données après avoir chargé uniquement les colonnes pertinentes (en supposant que nous n'avons besoin que de la colonne AGE) Représentation en mémoire des données après avoir chargé uniquement les colonnes pertinentes (en supposant que nous n'avons besoin que de la colonne AGE)

Les dtypes de Pandas contiennent tous les dtypes de numpy et des types spécifiques à Pandas. Vous pouvez trouver la liste détaillée de tous les types : pour Pandas et pour numpy.

En pratique

Passons à un véritable ensemble de données pour démontrer l'importance des types de données. Nous utiliserons un ensemble de données public provenant de Kaggle intitulé : Resale Flat Prices in Sigapore (entre 1990 et 1999).

Tout d'abord, importons les bibliothèques nécessaires et construisons le chemin d'accès à l'ensemble de données.

from pathlib import Path

import numpy as np
import pandas as pd

# Source de données :
# <https://www.kaggle.com/sveneschlbeck/resale-flat-prices-in-singapore>
data_path = Path(__file__).parents[0] / "flat-prices.zip"

Ensuite, définissons deux fonctions d'aide :

Nous utilisons df.memory_usage(deep=True) pour obtenir la taille mémoire par colonne et la sommons pour obtenir la taille totale. L'argument deep=True est important pour obtenir la quantité réelle de mémoire consommée.

def to_mb(octets: float) -> float:
    """Fonction pour convertir des octets en méga-octets (Mo)"""
    return octets / 1024 ** 2

def analyze(df: pd.DataFrame, title: str) -> float:
    """Affiche la mémoire utilisée par le dataframe en Mo et renvoie les
    octets"""
    octets = df.memory_usage(deep=True).sum()
    mo = to_mb(octets)
    print(f"{title} : {mo:.2f} Mo")
    print(df.dtypes)
    return mo

Ensuite, chargeons les données en utilisant pd.read_csv sans aucune optimisation :

def sans_optimisation() -> pd.DataFrame:
    """Charge les données sans aucune optimisation"""
    df = pd.read_csv(data_path)
    return df

...

if __name__ == "__main__":
    # Nous commençons d'abord sans aucune optimisation
    df_sans_optim = sans_optimisation()
    mo_sans_optim = analyze(df_sans_optim, "Sans aucune optimisation")
Dtypes des colonnes
Sans aucune optimisation : 131,36 Mo
mois                             object
cite                             object
type_appart                     object
bloc                             object
nom_rue                          object
plage_etages                     object
surface_habitable_de_l_appart    float64
modele_appart                    object
annee_location                    int64
prix_revente                      int64
dtype: object

Le dataframe prend 131.36 Mo en mémoire. Nous examinons également les types de colonnes et remarquons que certains types numériques peuvent être améliorés. Intéressons-nous aux colonnes surface_habitable_de_l_appart et prix_revente :

Données d'exemple de deux colonnes numériques
surface_habitable_de_l_appart  prix_revente
                       31.0          9000
                       31.0          6000
                       31.0          8000
                       31.0          6000
                       73.0         47200
              ...
                      142.0        456000
                      142.0        408000
                      146.0        469000
                      146.0        440000
                      145.0        484000

La surface_habitable_de_l_appart peut s'adapter à une colonne float de 16 bits (avec une valeur maximale de 6,55 × 10e4) et peut être représentée par un float16. prix_revente peut s'insérer dans un int non signé de 32 bits (avec une valeur maximale de 4,29e+09) et peut être représenté par un uint32.

Vous pouvez consulter le nombre, le minimum, le maximum et d'autres statistiques pour chaque colonne de votre ensemble de données à l'aide de df.describe().

Cela est particulièrement utile pour trouver la plage des colonnes numériques et la cardinalité (comptage des valeurs uniques) d'une colonne catégorique.

  • Les colonnes numériques avec une petite plage peuvent être optimisées avec un type plus petit
  • Le type category pour une colonne de faible cardinalité permet de réaliser un gain de mémoire considérable

Appliquons maintenant nos optimisations de types numériques. Nous utilisons l'argument dtypes dans pd.read_csv pour spécifier un type par colonne.

def avec_types_numeriques() -> pd.DataFrame:
    """Charge les données avec les types numériques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
        },
    )
    return df

La taille du dataframe en mémoire est maintenant de 128.62 Mo ce qui correspond à une réduction légère de 2.09%.

Maintenant, intéressons-nous aux colonnes catégoriques. modele_appart, type_appart, plage_etages, bloc et cite sont de bons candidats pour des colonnes catégoriques. Ils ont une faible cardinalité, ce qui indique un grand potentiel d'amélioration de la mémoire.

Appliquons ces optimisations :

def avec_types_numeriques_et_catégoriques() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
            "modele_appart": "category",
            "type_appart": "category",
            "plage_etages": "category",
            "bloc": "category",
            "cite": "category",
        },
    )
    return df

La taille du dataframe en mémoire est maintenant de 42.54 Mo ce qui correspond à une énorme réduction de 67.61% par rapport au cas sans aucune optimisation !

def avec_types_numeriques_et_catégoriques_sans_colonnes_inutilisées() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés
    et sans les colonnes inutilisées"""
    dtype = {
        "surface_habitable_de_l_appart": np.float16,
        "prix_revente": np.uint32,
        "modele_appart": "category",
        "type_appart": "category",
        "plage_etages": "category",
        "bloc": "category",
        "cite": "category",
    }
    df = pd.read_csv(data_path, dtype=dtype, usecols=list(dtype.keys()))
    return df

La taille du dataframe en mémoire est maintenant de 3.39 Mo ce qui correspond à une énorme réduction de 97.42% par rapport au cas sans aucune optimisation !

Parfois, tout ce dont vous avez besoin est d'être attentif aux types de vos données et d'être clair sur les colonnes dont vous avez vraiment besoin.

from pathlib import Path

import numpy as np
import pandas as pd

# Source de données :
# <https://www.kaggle.com/sveneschlbeck/resale-flat-prices-in-singapore>
data_path = Path(__file__).parents[0] / "flat-prices.zip"


def to_mb(octets: float) -> float:
    """Fonction pour convertir des octets en méga-octets (Mo)"""
    return octets / 1024 ** 2


def sans_optimisation() -> pd.DataFrame:
    """Charge les données sans aucune optimisation"""
    df = pd.read_csv(data_path)
    return df


def avec_types_numeriques() -> pd.DataFrame:
    """Charge les données avec les types numériques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
        },
    )
    return df


def avec_types_numeriques_et_catégoriques() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
            "modele_appart": "category",
            "type_appart": "category",
            "plage_etages": "category",
            "bloc": "category",
            "cite": "category",
        },
    )
    return df


def avec_types_numeriques_et_catégoriques_sans_colonnes_inutilisées() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés
    et sans les colonnes inutilisées"""
    dtype = {
        "surface_habitable_de_l_appart": np.float16,
        "prix_revente": np.uint32,
        "modele_appart": "category",
        "type_appart": "category",
        "plage_etages": "category",
        "bloc": "category",
        "cite": "category",
    }
    df = pd.read_csv(data_path, dtype=dtype, usecols=list(dtype.keys()))
    return df


def analyze(df: pd.DataFrame, title: str) -> float:
    """Affiche la mémoire utilisée par le dataframe en Mo et renvoie les
    octets"""
    octets = df.memory_usage(deep=True).sum()
    mo = to_mb(octets)
    print(f"{title} : {mo:.2f} Mo")
    print(df.dtypes)
    return mo


if __name__ == "__main__":
    # Nous commençons d'abord sans aucune optimisation
    df_sans_optim = sans_optimisation()
    mo_sans_optim = analyze(df_sans_optim, "Sans aucune optimisation")

    print()

    # Ensuite, nous utilisons les types numériques appropriés
    df_types_appropriés = avec_types_numeriques()
    mo_types_appropriés = analyze(df_types_appropriés, "Types numériques appropriés")
    réduction = (mo_sans_optim - mo_types_appropriés) / mo_sans_optim * 100
    print(f"Taille réduite de : {réduction:.2f}%")

    print()

    # Ensuite, nous utilisons les types numériques et catégoriques appropriés
    df_catégoriques = avec_types_numeriques_et_catégoriques()
    mo_catégoriques = analyze(df_catégoriques, "Types numériques et catégoriques appropriés")
    réduction = (mo_sans_optim - mo_catégoriques) / mo_sans_optim * 100
    print(f"Taille réduite de : {réduction:.2f}%")

    print()

    # Pas besoin de charger des colonnes inutilisées. Nous utilisons souvent un sous-ensemble des
    # colonnes, pertinentes pour notre analyse.
    # Nous supposons que nous ne sommes intéressés que par les colonnes où nous
    # avons défini les types (numériques et catégoriques)
    df_sans_colonnes_inutilisées = (
        avec_types_numeriques_et_catégoriques_sans_colonnes_inutilisées()
    )
    mo_sans_colonnes_inutilisées = analyze(
        df_sans_colonnes_inutilisées,
        "Types numériques et catégoriques appropriés, sans colonnes inutilisées",
    )
    réduction = (mo_sans_optim - mo_sans_colonnes_inutilisées) / mo_sans_optim * 100
    print(f"Taille réduite de : {réduction:.2f}%")





3 façons simples de traiter de plus gros ensembles de données avec Pandas

3 façons simples de traiter de plus gros ensembles de données avec Pandas

Les types que nous choisissons pour représenter nos données ont un impact sur la taille sous-jacente de leur représentation. Il existe une multitude de types de données, chacun ayant des précisions différentes et une consommation de mémoire différente. Pour être aussi économe en mémoire que possible, vous devez comprendre vos données et choisir les types les plus précis possibles pour les représenter.

Trouvez le code sur GitHub.

Concept

Pandas est incroyable pour traiter des données sur une seule machine, sur un seul cœur et lorsque cela tient en mémoire. Si vous travaillez avec de gros ensembles de données, vous pouvez facilement dépasser ces paramètres et devrez peut-être vous tourner vers d'autres outils pour gérer vos données. Avant de passer à l'informatique distribuée, avec des technologies comme Spark, il existe quelques optimisations simples qui vous aideront à mettre à l'échelle des ensembles de données plus importants.

Le principal facteur limitant est la quantité de mémoire nécessaire pour charger les données en mémoire. Si nous réduisons la taille des données, nous pouvons charger plus de données, les traiter plus rapidement... Considérez un simple fichier CSV avec deux colonnes : âge et emploi.

CSV avec deux colonnes. CSV avec deux colonnes.

Lorsque vous chargez des données en mémoire, vous devez définir les types pour chaque colonne de votre ensemble de données, ce qui est souvent fait par défaut. Pandas utilise des tableaux numpy pour représenter chaque colonne de votre dataframe. Ces tableaux numpy ont un dtype, qui est le type sous-jacent de tous les éléments du tableau. Lorsque vous utilisez pd.read_csv, Pandas devine les types de données de chaque colonne et attribue des tableaux numpy avec des types par défaut. Cependant, les types par défaut sont larges, utilisant plus d'espace que nécessaire pour représenter les différentes colonnes.

Représentation en mémoire des données chargées par Pandas sans optimisation. Représentation en mémoire des données chargées par Pandas sans optimisation.

Dans notre cas, nous obtiendrions une colonne d'âge avec un type int64 et une colonne d'emploi avec un type object. Cela n'est pas optimal car un âge a une plage [0, 100], n'ayant besoin que de 8 bits au lieu de 64. De plus, un emploi n'est pas « n'importe quelle chaîne », mais il est catégorique, nous avons très probablement une liste limitée d'emplois possibles et chaque emploi est un élément de cette liste.

Utilisez le bon dtype pour les colonnes numériques

Pandas n'infère pas le meilleur type pour nos colonnes numériques. Dans notre cas, nous pourrions représenter un âge avec un type uint8, car un âge est non signé (toujours positif) et sa plage est [0, 100]. Cependant, par défaut, Pandas utilise un int64 qui consomme 8 fois plus de mémoire.

La première étape consiste à forcer l'utilisation de types optimisés pour les colonnes numériques.

Utilisez le bon dtype pour les colonnes catégoriques

Lorsqu'une colonne est catégorique avec une faible cardinalité (le nombre de valeurs uniques est inférieur au nombre total de lignes -> beaucoup de répétitions), la représentation en chaîne de caractères est la pire. La chaîne de caractères est un type coûteux, selon le format (utf-8, ascii, ...) que nous utilisons entre 1 et 4 octets par caractère. Elle consomme rapidement une énorme quantité de mémoire lorsqu'elle est répétée des milliers de fois.

Mais il y a une meilleure façon. Il est plus efficace d'avoir un dictionnaire associant chaque valeur unique à un index avec un petit type, par exemple 1 octet. Ensuite, nous ne répétons qu'un seul octet des milliers de fois au lieu de la chaîne complète qui est beaucoup plus volumineuse. Heureusement, Pandas dispose d'un dtype pour gérer cela automatiquement pour vous, il vous suffit d'utiliser le dtype category.

Cela apporte généralement la plus grande amélioration de la consommation de mémoire. Le traitement des données catégoriques avec des chaînes de caractères simples est une énorme perte de ressources. Le type str est lourd au lieu d'une correspondance avec des indices répétés avec un type catégorique approprié.

Représentation en mémoire des données avec une optimisation des types. Pour la colonne AGE, nous utilisons uint8 au lieu de int64. Pour la colonne JOB, Student est associé à 0 et Data Scientist à 1. Ensuite, nous répétons 0 et 1 dans les différentes lignes de données au lieu de la lourde représentation en chaîne. Représentation en mémoire des données avec une optimisation des types. Pour la colonne AGE, nous utilisons uint8 au lieu de int64. Pour la colonne JOB, Student est associé à 0 et Data Scientist à 1. Ensuite, nous répétons 0 et 1 dans les différentes lignes de données au lieu de la lourde représentation en chaîne.

Dans le calcul ci-dessus, nous supposons que le plus petit morceau de mémoire que nous pouvons allouer est de 1 octet (8 bits). En effet, nous n'avons que deux valeurs différentes pour le vocabulaire, Data Scientist et Student. Pour représenter deux valeurs différentes, nous n'avons besoin que de 1 bit : soit 0 ou 1. Mais comme nous avons supposé que nous pouvons allouer un entier d'1 octet au minimum, nous obtenons 1 octet pour chaque entrée.

Chargez uniquement les colonnes pertinentes

Il est probable que vous n'utilisiez pas toutes les colonnes de votre ensemble de données pour mener une analyse spécifique. En utilisant l'argument usecols, vous pouvez charger uniquement les colonnes qui vous intéressent. C'est une astuce simple, mais elle vous aide à gérer facilement des ensembles de données volumineux.

Représentation en mémoire des données après avoir chargé uniquement les colonnes pertinentes (en supposant que nous n'avons besoin que de la colonne AGE) Représentation en mémoire des données après avoir chargé uniquement les colonnes pertinentes (en supposant que nous n'avons besoin que de la colonne AGE)

Les dtypes de Pandas contiennent tous les dtypes de numpy et des types spécifiques à Pandas. Vous pouvez trouver la liste détaillée de tous les types : pour Pandas et pour numpy.

En pratique

Passons à un véritable ensemble de données pour démontrer l'importance des types de données. Nous utiliserons un ensemble de données public provenant de Kaggle intitulé : Resale Flat Prices in Sigapore (entre 1990 et 1999).

Tout d'abord, importons les bibliothèques nécessaires et construisons le chemin d'accès à l'ensemble de données.

from pathlib import Path

import numpy as np
import pandas as pd

# Source de données :
# <https://www.kaggle.com/sveneschlbeck/resale-flat-prices-in-singapore>
data_path = Path(__file__).parents[0] / "flat-prices.zip"

Ensuite, définissons deux fonctions d'aide :

Nous utilisons df.memory_usage(deep=True) pour obtenir la taille mémoire par colonne et la sommons pour obtenir la taille totale. L'argument deep=True est important pour obtenir la quantité réelle de mémoire consommée.

def to_mb(octets: float) -> float:
    """Fonction pour convertir des octets en méga-octets (Mo)"""
    return octets / 1024 ** 2

def analyze(df: pd.DataFrame, title: str) -> float:
    """Affiche la mémoire utilisée par le dataframe en Mo et renvoie les
    octets"""
    octets = df.memory_usage(deep=True).sum()
    mo = to_mb(octets)
    print(f"{title} : {mo:.2f} Mo")
    print(df.dtypes)
    return mo

Ensuite, chargeons les données en utilisant pd.read_csv sans aucune optimisation :

def sans_optimisation() -> pd.DataFrame:
    """Charge les données sans aucune optimisation"""
    df = pd.read_csv(data_path)
    return df

...

if __name__ == "__main__":
    # Nous commençons d'abord sans aucune optimisation
    df_sans_optim = sans_optimisation()
    mo_sans_optim = analyze(df_sans_optim, "Sans aucune optimisation")
Dtypes des colonnes
Sans aucune optimisation : 131,36 Mo
mois                             object
cite                             object
type_appart                     object
bloc                             object
nom_rue                          object
plage_etages                     object
surface_habitable_de_l_appart    float64
modele_appart                    object
annee_location                    int64
prix_revente                      int64
dtype: object

Le dataframe prend 131.36 Mo en mémoire. Nous examinons également les types de colonnes et remarquons que certains types numériques peuvent être améliorés. Intéressons-nous aux colonnes surface_habitable_de_l_appart et prix_revente :

Données d'exemple de deux colonnes numériques
surface_habitable_de_l_appart  prix_revente
                       31.0          9000
                       31.0          6000
                       31.0          8000
                       31.0          6000
                       73.0         47200
              ...
                      142.0        456000
                      142.0        408000
                      146.0        469000
                      146.0        440000
                      145.0        484000

La surface_habitable_de_l_appart peut s'adapter à une colonne float de 16 bits (avec une valeur maximale de 6,55 × 10e4) et peut être représentée par un float16. prix_revente peut s'insérer dans un int non signé de 32 bits (avec une valeur maximale de 4,29e+09) et peut être représenté par un uint32.

Vous pouvez consulter le nombre, le minimum, le maximum et d'autres statistiques pour chaque colonne de votre ensemble de données à l'aide de df.describe().

Cela est particulièrement utile pour trouver la plage des colonnes numériques et la cardinalité (comptage des valeurs uniques) d'une colonne catégorique.

  • Les colonnes numériques avec une petite plage peuvent être optimisées avec un type plus petit
  • Le type category pour une colonne de faible cardinalité permet de réaliser un gain de mémoire considérable

Appliquons maintenant nos optimisations de types numériques. Nous utilisons l'argument dtypes dans pd.read_csv pour spécifier un type par colonne.

def avec_types_numeriques() -> pd.DataFrame:
    """Charge les données avec les types numériques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
        },
    )
    return df

La taille du dataframe en mémoire est maintenant de 128.62 Mo ce qui correspond à une réduction légère de 2.09%.

Maintenant, intéressons-nous aux colonnes catégoriques. modele_appart, type_appart, plage_etages, bloc et cite sont de bons candidats pour des colonnes catégoriques. Ils ont une faible cardinalité, ce qui indique un grand potentiel d'amélioration de la mémoire.

Appliquons ces optimisations :

def avec_types_numeriques_et_catégoriques() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
            "modele_appart": "category",
            "type_appart": "category",
            "plage_etages": "category",
            "bloc": "category",
            "cite": "category",
        },
    )
    return df

La taille du dataframe en mémoire est maintenant de 42.54 Mo ce qui correspond à une énorme réduction de 67.61% par rapport au cas sans aucune optimisation !

def avec_types_numeriques_et_catégoriques_sans_colonnes_inutilisées() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés
    et sans les colonnes inutilisées"""
    dtype = {
        "surface_habitable_de_l_appart": np.float16,
        "prix_revente": np.uint32,
        "modele_appart": "category",
        "type_appart": "category",
        "plage_etages": "category",
        "bloc": "category",
        "cite": "category",
    }
    df = pd.read_csv(data_path, dtype=dtype, usecols=list(dtype.keys()))
    return df

La taille du dataframe en mémoire est maintenant de 3.39 Mo ce qui correspond à une énorme réduction de 97.42% par rapport au cas sans aucune optimisation !

Parfois, tout ce dont vous avez besoin est d'être attentif aux types de vos données et d'être clair sur les colonnes dont vous avez vraiment besoin.

from pathlib import Path

import numpy as np
import pandas as pd

# Source de données :
# <https://www.kaggle.com/sveneschlbeck/resale-flat-prices-in-singapore>
data_path = Path(__file__).parents[0] / "flat-prices.zip"


def to_mb(octets: float) -> float:
    """Fonction pour convertir des octets en méga-octets (Mo)"""
    return octets / 1024 ** 2


def sans_optimisation() -> pd.DataFrame:
    """Charge les données sans aucune optimisation"""
    df = pd.read_csv(data_path)
    return df


def avec_types_numeriques() -> pd.DataFrame:
    """Charge les données avec les types numériques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
        },
    )
    return df


def avec_types_numeriques_et_catégoriques() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés"""
    df = pd.read_csv(
        data_path,
        dtype={
            "surface_habitable_de_l_appart": np.float16,
            "prix_revente": np.uint32,
            "modele_appart": "category",
            "type_appart": "category",
            "plage_etages": "category",
            "bloc": "category",
            "cite": "category",
        },
    )
    return df


def avec_types_numeriques_et_catégoriques_sans_colonnes_inutilisées() -> pd.DataFrame:
    """Charge les données avec les types numériques et catégoriques appropriés
    et sans les colonnes inutilisées"""
    dtype = {
        "surface_habitable_de_l_appart": np.float16,
        "prix_revente": np.uint32,
        "modele_appart": "category",
        "type_appart": "category",
        "plage_etages": "category",
        "bloc": "category",
        "cite": "category",
    }
    df = pd.read_csv(data_path, dtype=dtype, usecols=list(dtype.keys()))
    return df


def analyze(df: pd.DataFrame, title: str) -> float:
    """Affiche la mémoire utilisée par le dataframe en Mo et renvoie les
    octets"""
    octets = df.memory_usage(deep=True).sum()
    mo = to_mb(octets)
    print(f"{title} : {mo:.2f} Mo")
    print(df.dtypes)
    return mo


if __name__ == "__main__":
    # Nous commençons d'abord sans aucune optimisation
    df_sans_optim = sans_optimisation()
    mo_sans_optim = analyze(df_sans_optim, "Sans aucune optimisation")

    print()

    # Ensuite, nous utilisons les types numériques appropriés
    df_types_appropriés = avec_types_numeriques()
    mo_types_appropriés = analyze(df_types_appropriés, "Types numériques appropriés")
    réduction = (mo_sans_optim - mo_types_appropriés) / mo_sans_optim * 100
    print(f"Taille réduite de : {réduction:.2f}%")

    print()

    # Ensuite, nous utilisons les types numériques et catégoriques appropriés
    df_catégoriques = avec_types_numeriques_et_catégoriques()
    mo_catégoriques = analyze(df_catégoriques, "Types numériques et catégoriques appropriés")
    réduction = (mo_sans_optim - mo_catégoriques) / mo_sans_optim * 100
    print(f"Taille réduite de : {réduction:.2f}%")

    print()

    # Pas besoin de charger des colonnes inutilisées. Nous utilisons souvent un sous-ensemble des
    # colonnes, pertinentes pour notre analyse.
    # Nous supposons que nous ne sommes intéressés que par les colonnes où nous
    # avons défini les types (numériques et catégoriques)
    df_sans_colonnes_inutilisées = (
        avec_types_numeriques_et_catégoriques_sans_colonnes_inutilisées()
    )
    mo_sans_colonnes_inutilisées = analyze(
        df_sans_colonnes_inutilisées,
        "Types numériques et catégoriques appropriés, sans colonnes inutilisées",
    )
    réduction = (mo_sans_optim - mo_sans_colonnes_inutilisées) / mo_sans_optim * 100
    print(f"Taille réduite de : {réduction:.2f}%")