"""
![](https://cdn-images-1.medium.com/max/2000/1*d-ZbdImPx4zRW0zK4QL49w.jpeg)
"""
%%html
<style> 
@import url('https://fonts.googleapis.com/css?family=Orbitron|Roboto');
body {background-color: gainsboro;} 
a {color: #37c9e1; font-family: 'Roboto';} 
h1 {color: #37c9e1; font-family: 'Orbitron'; text-shadow: 4px 4px 4px #aaa;} 
h2, h3 {color: slategray; font-family: 'Orbitron'; text-shadow: 4px 4px 4px #aaa;}
h4 {color: #818286; font-family: 'Roboto';}
#span {text-shadow: 4px 4px 4px #aaa;}
div.output_prompt, div.output_area pre {color: white;}
div.input_prompt, div.output_subarea {color: #37c9e1;}      
div.output_stderr pre {background-color: gainsboro;}  
div.output_stderr {background-color: slategrey;}       
</style>
"""
# &#128203;  Introduction

Lorsque vous travaillez avec pandas sur des petits dataframe (moins de 100 mégaoctets), la performance est rarement un problème. Lorsque nous passons à des données plus volumineuses (de 100 mégaoctets à plusieurs gigaoctets), les problèmes de performance peuvent allonger considérablement la durée d'exécution et faire échouer le code en raison d'une mémoire insuffisante.

Dans cet article, nous allons apprendre à optimiser  la mémoire, comment réduire l'empreinte mémoire d'un dataframe sur les courses hippiques de près de 90%, simplement en sélectionnant les types de données appropriés pour les colonnes.
"""
# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load in 

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import seaborn as sn
import matplotlib
import matplotlib.pyplot as plt

# Input data files are available in the "../input/" directory.
# For example, running this (by clicking run or pressing Shift+Enter) will list the files in the input directory

import os
print(os.listdir("../input"))

# Any results you write to the current directory are saved as output.
pd.options.display.max_columns = 999
courses = pd.read_csv("../input/export-1-pt-utf.csv", parse_dates=['cr-Date'])
courses.head()


dataTypeDf = pd.DataFrame(courses.dtypes.value_counts()).reset_index().rename(columns={"index":"variableType",0:"count"})
fig,ax = plt.subplots()
fig.set_size_inches(20,5)
sn.barplot(data=dataTypeDf,x="variableType",y="count",ax=ax,color="#34495e")
ax.set(xlabel='Type de Variables', ylabel='Quantité',title="Quantité variables par type")


"""
# &#128203;  Analyse de la mémoire

Par défaut, pandas se rapproche de l'utilisation de la mémoire de la trame de données pour gagner du temps. Parce que nous sommes intéressés par la précision, nous allons régler le paramètre memory_usage sur'deep' pour obtenir un nombre précis.
"""
courses.info(memory_usage='deep')
"""
Nous pouvons voir que nous avons 19 869 lignes et 69 colonnes. Pandas a détecté automatiquement les types pour nous, avec 63 colonnes numériques et 6 colonnes d'objets. Les colonnes d'objets sont utilisées pour les chaînes de caractères ou lorsqu'une colonne contient des types de données mixtes.

Afin de mieux comprendre où nous pouvons réduire l'utilisation de la mémoire, examinons comment pandas stockent les données en mémoire.

Dans Pandas, chaque type de données entiers, flottants et objets sont stockés séparemment. Chaque type a une classe spécialisée dans le module pandas.core.internals. Pandas utilise la classe ObjectBlock pour représenter le bloc contenant les colonnes de chaînes et la classe FloatBlock pour représenter le bloc contenant les colonnes de floats et IntBlock pour les entiers.

Comme chaque type de données est stocké séparément, nous allons examiner l'utilisation de la mémoire par type de données. Commençons par examiner l'utilisation moyenne de la mémoire pour le type de données.
"""
for dtype in ['float','int','object']:
    # Pour retourner un sous ensemble d'un Dataframe en fonction du type des colonne
    selected_dtype = courses.select_dtypes(include=[dtype])
    # memory_usage retourne la mémoire utilisée en byte des colonnes
    usage_moyen_b = selected_dtype.memory_usage(deep=True).mean()
    usage_moyen_mb = usage_moyen_b / 1024 ** 2
    print("Usage moyen de la mémoire pour le type {} : {:03.2f} MB".format(dtype,usage_moyen_mb))
"""
Nous pouvons voir au premier coup d'oeil que la majeure partie de notre mémoire est utilisée par nos colonnes d'objets. Nous y reviendrons plus tard, mais voyons d'abord si nous pouvons améliorer l'utilisation de la mémoire pour nos colonnes numériques.

# &#128203; Différents types d'entiers et de flottants

On trouve plusieurs types d'entiers :
* int8
* uint8
* uint16
* int16
* int32
* uint32
* int64
* uint64

On trouve plusieurs types de flottants :
* float16
* float32
* float64

Numpy nous permet d'avoir des infos sur les différents types. La fonction numpy.iinfo nous donne les infos sur les valeurs max et min du type de variable.

"""
int_types = ["uint8", "int8", "int16", "uint16", "int32", "uint32", "int64", "uint64"]
for it in int_types:
    print(np.iinfo(it))
"""
On peut voir de suite les différences avec les uint qui ne sont pas signés.

# &#128203;  Optimiser nos colonnes en fonction du sous type

Pandas nous propose une fonction pour changer le type d'une colonne. Cette fonction est **to_numeric()**.
Comme nous l'avons vue précédemment, nous utiliserons DataFrame.select_dtypes pour ne sélectionner que les colonnes entières, puis nous optimiserons les types et comparerons l'utilisation de la mémoire.

"""
# Création d'une fonction de mesure de la mémoire
def usage_memoire(pandas_obj):
    # Notre objet est il un Dataframe ?
    if isinstance(pandas_obj,pd.DataFrame):
        # Somme de toutes nos colonnes
        usage_b = pandas_obj.memory_usage(deep=True).sum()
    else: # C'est alors une série
        usage_b = pandas_obj.memory_usage(deep=True)
    # convertion des bytes en Mo
    usage_mb = usage_b / 1024 ** 2 
    return "{:03.2f} MB".format(usage_mb)

#On créé un nouveau dataFrame avec uniquement les colonnes de types entiers
courses_int = courses.select_dtypes(include=['int'])
# On va forcer la conversion en entier non signé
convertion_int = courses_int.apply(pd.to_numeric,downcast='unsigned')

print(usage_memoire(courses_int))
print(usage_memoire(convertion_int))

# on va créer un nouveau dataframe pour voir les changement de type
compare_ints = pd.concat([courses_int.dtypes,convertion_int.dtypes],axis=1)
compare_ints.columns = ['avant','apres']
compare_ints.apply(pd.Series.value_counts)
"""
On remarque que nous sommes passés d'une taille de 1,52 Mb à 0,19 Mb soit une baisse de 87% de l'occupation en mémoire !

On peut voir dans le tableau qu'avant nos entiers étaient de la forme int64. Désormais, ils sont au forat unint8.

Nous allons poursuivre en faisant la même chose mais avec nos flottants.

"""
courses_float = courses.select_dtypes(include=['float'])
convertion_float = courses_float.apply(pd.to_numeric,downcast='float')

print(usage_memoire(courses_float))
print(usage_memoire(convertion_float))

compare_floats = pd.concat([courses_float.dtypes,convertion_float.dtypes],axis=1)
compare_floats.columns = ['avant','apres']
compare_floats.apply(pd.Series.value_counts)
"""
Nous sommes passés de float 64 en float 32. Du coup, on gagne presque 50% en mémoire.

A partir de cela, on peut créer un DataFrame avec ces nouveaux types.
"""
optimise_gl = courses.copy()

optimise_gl[convertion_int.columns] = convertion_int
optimise_gl[convertion_float.columns] = convertion_float

print(usage_memoire(courses))
print(usage_memoire(optimise_gl))
"""
On peut voir que globalement on a gagné environ 30% en mémoire. Ce qui est déjà pas mal. Imaginez si notre dataFrame faisait plusieurs Gigas...

Maintenant essayons de nous attaquer au type Objet.

Les chaînes de caractères sont stockées de manière fragmentée, ce qui consomme plus de mémoire et ralentit l'accès. Chaque élément d'une colonne d'objet est en réalité un pointeur qui contient l'"adresse" de l'emplacement de la valeur réelle dans la mémoire.

# &#128203; Optimisation des types Object

Pandas possède un type Categoricals. Ce type utilise des valeurs entières sous le capot pour représenter les valeurs dans une colonne, plutôt que les valeurs brutes. Pandas utilise un dictionnaire de mappage séparé qui mappe les valeurs entières sur les valeurs brutes. Cette disposition est utile lorsqu'une colonne contient un ensemble limité de valeurs. Lorsque nous convertissons une colonne dans la catégorie dtype, pandas utilise le sous-type int le plus efficace en espace qui peut représenter toutes les valeurs uniques dans une colonne.

![](http://www.logiciels-professionnels.com/Capture.PNG)

Pour avoir un aperçu des colonnes où nous pourrions utiliser ce type pour réduire la mémoire, jetons un coup d'oeil au nombre de valeurs uniques de chacun de nos types d'objets.


"""
# On créé un dataframe avec uniquement les colonnes de types objet
courses_obj = courses.select_dtypes(include=['object']).copy()
courses_obj.describe()
"""
Un coup d'œil rapide révèle de nombreuses colonnes où il y a peu de valeurs uniques par rapport à l'ensemble des 19869 lignes  de notre ensemble de données.

Avant d'aller plus loin, nous allons commencer par sélectionner une seule de nos colonnes d'objets, et regarder ce qui se passe dans les coulisses lorsque nous la convertissons en type catégorique. Nous utiliserons la troisième colonne de notre ensemble de données, cr-etat du terrain.

En regardant le tableau ci-dessus, nous pouvons voir qu'il ne contient que six valeurs uniques. Nous allons le convertir en catégorique en utilisant la méthode .astype().
"""
etat_terrain = courses_obj["cr-etat du terrain"]
print(etat_terrain.head())

etat_terrain_cat = etat_terrain.astype('category')
print(etat_terrain_cat.head())
"""
Comme vous pouvez le voir, outre le fait que le type de colonne a changé, les données sont exactement les mêmes. 

Dans le code suivant, nous utilisons l'attribut Series.cat.codes pour retourner les valeurs entières que le type de catégorie utilise pour représenter chaque valeur.
"""
etat_terrain_cat.head(10).cat.codes
"""
Sur les premières courses l'état du terrain ne change pas et nous avons toujours la valeur 1, qui correspond à "terrain bon".

Vérifions ce que cela donne au niveau de la mémoire.

"""
print(usage_memoire(etat_terrain))
print(usage_memoire(etat_terrain_cat))
"""
Nous sommes passés de 1,3 Mo de mémoire utilisée à 0,02 Mo de mémoire utilisée, soit une réduction de 98 % ! En effet, nous sommes passés à 6 valeurs uniques sur plus de 19 000 lignes.

Bien que la conversion de toutes les colonnes de ce type semble attrayante, il est important d'être conscient des compromis à faire. Le plus important est l'incapacité d'effectuer des calculs numériques. Nous ne pouvons pas faire d'arithmétique avec des colonnes de catégories ou utiliser des méthodes comme Series.min() et Series.max() sans d'abord convertir en un vrai dtype numérique.

Nous devrions nous en tenir au type de catégorie principalement pour les colonnes d'objets où moins de 50% des valeurs sont uniques. Si toutes les valeurs d'une colonne sont uniques, le type de catégorie finira par utiliser plus de mémoire. 

Nous allons écrire une boucle à itérer sur chaque colonne d'objet, vérifier si le nombre de valeurs uniques est inférieur à 50%, et si oui, le convertir au type de catégorie.
"""
convertion_obj = pd.DataFrame()

# On parcours les différentes colonnes
for col in courses_obj.columns:
    # Nombre de valeurs uniques
    num_unique_valeurs = len(courses_obj[col].unique())
    # Nombre de valeurs
    num_total_valeurs = len(courses_obj[col])
    # Si moins de 50% de valeurs uniques alors on change le type de colonne
    if num_unique_valeurs / num_total_valeurs < 0.5:
        convertion_obj.loc[:,col] = courses_obj[col].astype('category')
    else:
        convertion_obj.loc[:,col] = courses_obj[col]
        
print(usage_memoire(courses_obj))
print(usage_memoire(convertion_obj))

compare_obj = pd.concat([courses_obj.dtypes,convertion_obj.dtypes],axis=1)
compare_obj.columns = ['avant','apres']
compare_obj.apply(pd.Series.value_counts)
"""
Dans ce cas, toutes nos colonnes d'objets ont été converties en type de catégorie, mais ce ne sera pas le cas avec tous les ensembles de données, vous devriez donc vous assurer d'utiliser le processus ci-dessus pour vérifier.

De plus, notre utilisation de mémoire pour nos colonnes d'objets est passée de 7,93 Mo à 0,15 Mo, soit une réduction de 95 %. Combinons ceci avec le reste de notre dataframe et voyons où nous nous situons par rapport à l'utilisation de mémoire de 17,1 MB que nous avons commencé avec.
"""
optimise_gl[convertion_obj.columns] = convertion_obj

usage_memoire(optimise_gl)
"""
Wow, on a vraiment fait des progrès ! Nous avons une autre optimisation que nous pouvons faire - si vous vous souvenez de notre tableau des types, il y avait un type datetime que nous pouvons utiliser pour la première colonne de notre ensemble de données.

Il nous reste encore un colone de type date. Pouvons nous essayer de l'optimiser ?

"""
date = optimise_gl["cr-Date"]
print(usage_memoire(date))
date.head()
"""
On remarque de suite que ma colonne cr-Date est déjà au format date. En effet, durant l'importation depuis csv, on avait spécifier que cette colonne était au format date. Cela nous permet d'optimiser dircetement cette colonne au chargement du dataframe.

# &#128203;  Importation des données en fixant le type

Jusqu'à présent, nous avons exploré des moyens de réduire l'empreinte mémoire d'un dataFrame existant. En important notre fichier et puis en changeant le types des colonnes, nous avons pu optimiser la taille de notre empreinte mémoire. 

Mais imaginons que notre fichier soit trop gros pour être chargé ou que nus ayons une mémoire trop petite pour le charger. Comment pouvons-nous appliquer des techniques d'économie de mémoire alors que nous ne pouvons même pas créer le dataframe ?

Heureusement, nous pouvons spécifier les types de colonnes  lorsque nous lisons l'ensemble des données. La fonction pandas.read_csv() a quelques paramètres  qui nous permettent de le faire. C'est ce que nous venons de voir avec l'importation de la date. 
Le paramètre dtype accepte un dictionnaire dont les clés sont des noms de colonnes (chaînes de caractères) et les valeurs des objets de type NumPy.

Tout d'abord, nous allons stocker les types finaux de chaque colonne dans un dictionnaire avec des clés pour les noms de colonnes, en supprimant d'abord la colonne de la date puisque celle-ci doit être traitée séparément.

"""
# On supprime la colonne cr-Date
dtypes = optimise_gl.drop('cr-Date',axis=1).dtypes

# Serie avec le nom de la colonne
dtypes_col = dtypes.index
# Serie avec le type de la colonne
dtypes_type = [i.name for i in dtypes.values]

#Création d'un dictionnaire 
column_types = dict(zip(dtypes_col, dtypes_type))

#Affichage des résultats
for keys,values in column_types.items():
    print(keys+" : "+values)


"""
Maintenant nous pouvons utiliser le dictionnaire, avec quelques paramètres pour la date à lire dans les données avec les types corrects en quelques lignes :
"""
courses_optimisees = pd.read_csv('../input/export-1-pt-utf.csv',dtype=column_types,parse_dates=['cr-Date'],infer_datetime_format=True)

print(usage_memoire(courses_optimisees))
courses_optimisees.head()
"""
Pour rappel, au début nous étions à 17,1 Mo et maintenant 4,5 Mo. On a gagné au final 73% de mémoire. &#x1F4A3; 
"""
dataTypeDf = pd.DataFrame(courses_optimisees.dtypes.value_counts()).reset_index().rename(columns={"index":"variableType",0:"count"})
fig,ax = plt.subplots()
fig.set_size_inches(20,5)
sn.barplot(data=dataTypeDf,x="variableType",y="count",ax=ax,color="#34495e")
ax.set(xlabel='Type de Variables', ylabel='Quantité',title="Quantité variables par type")
"""
Si vous avez apprécié ce petit article, merci de voter pour lui. 

Si vous voulez me joindre directement sur linkedin.
[https://fr.linkedin.com/in/developpeur-windev](https://fr.linkedin.com/in/developpeur-windev)


"""