Séminaire RUSS - 4 Avril 2019

Introduction Python pour les utilisateurs de R

Slides : https://mthh.github.io/RUSS_190404/
Matthieu Viry (matthieu.viry@univ-grenoble-alpes.fr)
Laboratoire d'informatique de Grenoble
(Univ. Grenoble Alpes / CNRS / Grenoble INP)

Pour naviguer dans ces slides :

Au programme :

  • présentation du langage Python (spécificités, fondements, etc.)
  • Conseils sur la stack techno mobilisable (distributions Python, choix d'un IDE, etc.)
  • calcul scientifique (Numpy, matplotlib et RPy2)
  • préparation et analyse de données (Pandas, seaborn et xarray)
  • analyse statistique, modélisation et apprentissage (Scipy, scikit-learn et statsmodels)
  • manipulation de données géospatiales (shapely, geopandas, rasterio et folium)
  • publication de données sur le Web (Dash et hug)

Python :

  • Libre (régit par la Python Software Foundation License, équivalent à BSD)
  • Créé dans les années 90.
  • Langage de haut niveau, interprété, multi-paradigme (impératif, fonctionnel, OO, ..)
  • Présence d'un système de gestion des erreurs et d'une gestion automatique de la mémoire (par comptage des références)
  • Typage dynamique fort (et duck typing)

Premiers pas en Python

L'intérpréteur fourni par défaut

  • Éxécuter un script python :
python mon_script.py
  • Ouvrir un intepréteur interactif :
python
  • Exemple :
mthh@mthh:~$ python3

Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print('Hello!')
Hello!
>>> 38
38
>>> exit()
  • Comparable à taper
mthh@mthh:~$ R

R version 3.4.4 (2018-03-15) -- "Someone to Lean On"
Copyright (C) 2018 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu (64-bit)

R est un logiciel libre livré sans AUCUNE GARANTIE.
Vous pouvez le redistribuer sous certaines conditions.
Tapez 'license()' ou 'licence()' pour plus de détails.

R est un projet collaboratif avec de nombreux contributeurs.
Tapez 'contributors()' pour plus d'information et
'citation()' pour la façon de le citer dans les publications.

Tapez 'demo()' pour des démonstrations, 'help()' pour l'aide
en ligne ou 'help.start()' pour obtenir l'aide au format HTML.
Tapez 'q()' pour quitter R.

[Sauvegarde de la session précédente restaurée]

> print('Hello!')
[1] "Hello!"
> q('no')
  • On utilisera IPython par la suite, un terminal interactif très riche.
mthh@mthh:~$ ipython3

Python 3.6.7 (default, Oct 22 2018, 11:32:17)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: print('Hello!')
Hello!

In [2]: 12
Out[2]: 12

In [3]: exit()
  • Pas depuis un terminal système, mais depuis l'un des IDE présentés ensuite et qui offrent des environnements MATLAB-like, proche de l'environnement également offert par RStudio.

Les types de données de base

Entier (int)
In [3]:
a = 12
print(a, '\n', type(a))
12 
 <class 'int'>
Flottant (float)
In [4]:
a = 12.3
print(a, '\n', type(a))
print(a.is_integer())
12.3 
 <class 'float'>
False
Booléen (bool)
In [5]:
a = True
b = not a
print(b, '\n', type(b))
False 
 <class 'bool'>
Liste (list)

Les listes en Python peuvent contenir des objets hétérogènes.

In [6]:
a = []
a.append(1)
a.append("yo")
a.append([1, 2, 3])
a.append(4)
a.append(23.9)
a.append("Une autre chaine de caractères")
a
Out[6]:
[1, 'yo', [1, 2, 3], 4, 23.9, 'Une autre chaine de caractères']

On va utiliser les crochets pour indexer les éléments de la listes (en Python on commence à compter à 0)

In [7]:
a[0] # <- Le premier élément de la liste
a[2] # <- Le troisième

# Il existe des méthodes pour enlever le dernier élément
# ou un élément particulier, etc.
a.pop()
Out[7]:
1
Out[7]:
[1, 2, 3]
Out[7]:
'Une autre chaine de caractères'
In [8]:
a.index('yo')
Out[8]:
1
In [9]:
a.remove('yo')
a
Out[9]:
[1, [1, 2, 3], 4, 23.9]
In [10]:
# Une syntaxe permet également de créer une liste non-vide :
my_list = [1, 2, 5, 8, 12, 17]
In [11]:
for item in my_list:
    print(item)
1
2
5
8
12
17
Chaine de caractères (str)
In [12]:
a = "Ceci est une chaine de caractères"
print(type(a))
<class 'str'>

Les objets de ce type disposent de nombreuses méthodes :

In [13]:
a.lower()
a.upper()
a.isdigit()
a.isprintable()
Out[13]:
'ceci est une chaine de caractères'
Out[13]:
'CECI EST UNE CHAINE DE CARACTÈRES'
Out[13]:
False
Out[13]:
True

Comme avec les listes, on peut accéder à une position particulière ou obtenir un slice de l'objet.

In [14]:
a[2] # La troisième lettre
a[:12] # Jusqu'à la 12ème lettre
a[2:5] # ...
Out[14]:
'c'
Out[14]:
'Ceci est une'
Out[14]:
'ci '
In [15]:
# À vrai dire, c'est un `iterable` comme les listes :
for letter in "abcde":
    print(letter)
a
b
c
d
e
N-uplets (tuple)

Il s'agit d'un containeur immutable (contrairement à une liste on peut pas ajouter / supprimer / modifier ses éléments) qui peut contenir des objets hétérogènes.
Comme pour les listes, on peut utiliser les crochets pour appeler ses éléments.

In [16]:
# On peut l'utiliser pour stocker des informations structurées
# (où le N-ième élément a toujours la même signification)
pt1 = (12.1, 34.9) # (x, y)
pt2 = (34.4, 21.1) # (x, y)

info_consumers = [
    ('marie', 38, 21900.12), # (name, age, salary)
    ('bob', 22, 13900.12),
    ('alice', 48, 20900.12),
]
In [17]:
age_mean = 0
for consumer in info_consumers:
    age_mean += consumer[1] / len(info_consumers)

age_mean
Out[17]:
36.0
Dictionnaire (dict)

Il s'agit d'une HashMap (principe clé -> valeur) relativement performante, pouvant contenir des éléments hétérogènes.

In [18]:
d1 = {}
d1['john'] = "1293038"
d1['eva'] = "1204004"
d1['rick'] = "1398091"
d1
Out[18]:
{'john': '1293038', 'eva': '1204004', 'rick': '1398091'}
In [19]:
d1['john']
Out[19]:
'1293038'
In [20]:
# Quelques méthodes des dictionnaires :
d1.get('rick')
Out[20]:
'1398091'
In [21]:
d1.get('kdgjkdgjkd', "0") # avec valeur par défaut si absence de la clé
Out[21]:
'0'
In [22]:
d1.keys() # Retourne une vue des clefs du dictionnaire
Out[22]:
dict_keys(['john', 'eva', 'rick'])
In [23]:
d1.values() # Retourne une vue des valeurs du dictionnaire
Out[23]:
dict_values(['1293038', '1204004', '1398091'])
In [24]:
# Fusion avec un autre dictionnaire
d1.update({'alice': '129874', 'alex': '2100012'})
print(d1)
{'john': '1293038', 'eva': '1204004', 'rick': '1398091', 'alice': '129874', 'alex': '2100012'}
In [25]:
# Tester la présence d'une clé :
'matt' in d1
Out[25]:
False
In [26]:
d1['matt'] # <- oh no !
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-26-06a69f48afe9> in <module>
----> 1 d1['matt'] # <- oh no !

KeyError: 'matt'
Erreur (Exception)
In [27]:
try:
    # Le bloc de code à 'surveiller' :
    print(d1['bob'])
    
except KeyError as e:
    # Si l'erreur rencontrée est de type `KeyError`
    print('{} is missing 0_0'.format(e))

except Exception:
    # Si un autre type d'erreur est survenu
    print('Something else happened...')
    
else:
    # Si aucune erreur ne s'est produite
    print('doo')

finally:
    # Ce bloc est exécuté dans tous les cas
    print('We safely escaped from here !')
'bob' is missing 0_0
We safely escaped from here !
In [28]:
# Création d'un type d'erreur personnalisé
class UnrecoverableError(Exception): pass

if not 'bob' in d1:
    # Déclenchement de cette erreur
    # (en utilisant un message explicatif)
    raise UnrecoverableError('Where is bob ?')
---------------------------------------------------------------------------
UnrecoverableError                        Traceback (most recent call last)
<ipython-input-28-f5dcca34a3b6> in <module>
      5     # Déclenchement de cette erreur
      6     # (en utilisant un message explicatif)
----> 7     raise UnrecoverableError('Where is bob ?')

UnrecoverableError: Where is bob ?
Ensemble (set)
In [29]:
seen = set()

for item in [1, 2, 4, 1, 5, 2, 9, 4, 1, 9]:
    seen.add(item)

print('{} éléments uniques : {}'.format(len(seen), list(seen)))
5 éléments uniques : [1, 2, 4, 5, 9]
In [30]:
s1 = {'rick', 'bob', 'alice', 'alex'}
s2 = {'john', 'eva', 'bob'}

# Des méthodes intéressantes
s1.intersection(s2) # -> {'bob'}
s2.issubset(s1) # -> False
s1.symmetric_difference(s2) # -> {'alex', 'alice', 'eva', 'john', 'rick'}
Out[30]:
{'bob'}
Out[30]:
False
Out[30]:
{'alex', 'alice', 'eva', 'john', 'rick'}
Et les autres...

Une dizaine d'autres types natifs existent. Ils sont parfois manipulés de manière moins explicite (ou moins souvent!)

In [31]:
fset = frozenset(seen) # <- Un `set` auquel on ne pourra plus ajouter d'éléments
print(type(fset))
1 in fset  # Utile pour vérifier l'absence ou la présence d'un élément par exemple
12 in fset # ... et rapide
<class 'frozenset'>
Out[31]:
True
Out[31]:
False

Rappel : tout est objet en Python

In [32]:
isinstance(33, int)
isinstance(int, type)
isinstance(int, object)
isinstance(type, object)

issubclass(int, object)
Out[32]:
True
Out[32]:
True
Out[32]:
True
Out[32]:
True
Out[32]:
True

Les fonctions natives (built-in functions)

  • isinstance(33, int) # -> True
  • len([1, 2, 3]) # -> 3
  • type(33) # -> int
  • dir() pour connaitre l'ensemble des attributs d'un objets directement dans l'interpréteur
In [33]:
# Imprimer l'ensemble des attributs d'un objet dans l'interpréteur
li = [1, 2, 3]
print(dir(li))
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

Contrôle du flux d'éxecution, comparaison, etc.

In [34]:
# Still looking for Bob !

my_list = list(d1.keys())
print(my_list)

for name in my_list:
    name_lower = name.lower()

    if name_lower == 'bob':
        print('Finally you\'re here..!')
    elif name_lower == 'eva':
        print('Oh hi Eva!')
    else:
        print('...')
['john', 'eva', 'rick', 'alice', 'alex']
...
Oh hi Eva!
...
...
...
In [35]:
def create_empty_consumer_info(names):
    result = []
    for name in names:
        result.append({
          "name": name, "age": None, "salary": 0  
        })
    return result


def set_age(info_consumers, name, age):
    for info in info_consumers:
        if info['name'] == name:
            info['age'] = age


infos = create_empty_consumer_info(my_list)
set_age(infos, 'alex', 33)
infos
Out[35]:
[{'name': 'john', 'age': None, 'salary': 0},
 {'name': 'eva', 'age': None, 'salary': 0},
 {'name': 'rick', 'age': None, 'salary': 0},
 {'name': 'alice', 'age': None, 'salary': 0},
 {'name': 'alex', 'age': 33, 'salary': 0}]
In [36]:
# Avec une approche plus 'fonctionnelle' ?
infos = list(map(lambda x: {"name": x, "age": None, "salary": 0}, my_list))

# Utilisation de la fonction précédement définie
set_age(infos, 'alex', 22)
infos
Out[36]:
[{'name': 'john', 'age': None, 'salary': 0},
 {'name': 'eva', 'age': None, 'salary': 0},
 {'name': 'rick', 'age': None, 'salary': 0},
 {'name': 'alice', 'age': None, 'salary': 0},
 {'name': 'alex', 'age': 22, 'salary': 0}]
In [37]:
# Une approche plus fonctionnelle et plus pythonique ?
infos = [{"name": x, "age": None, "salary": 0} for x in my_list]

# En utilisant le même type d'approche pour modifier un élément spécifique
[i for i in infos if i['name'] == 'alex'][0]['age'] = 107
infos
Out[37]:
[{'name': 'john', 'age': None, 'salary': 0},
 {'name': 'eva', 'age': None, 'salary': 0},
 {'name': 'rick', 'age': None, 'salary': 0},
 {'name': 'alice', 'age': None, 'salary': 0},
 {'name': 'alex', 'age': 107, 'salary': 0}]
In [38]:
# Un rapide aperçu de l'approche orientée objet ?

class Consumer:
    def __init__(self, name, age=None, salary='0'):
        self.name = name
        self.age = age
        self.salary = salary

    def __repr__(self):
        return 'Consumer {}. age : {}. salary : {}'.format(self.name, self.age, self.salary)

    
class ConsumerInfos:
    def __init__(self, consumers):
        self.consumers = {c.name: c for c in consumers}
        
    def get_by_name(self, name):
        return self.consumers[name]

    def __repr__(self):
        return ''.join(['[','\n'.join(str(c) for c in self.consumers.values()),']'])


infos = ConsumerInfos(Consumer(name) for name in my_list)
infos.get_by_name('alex').age = 43
infos
Out[38]:
[Consumer john. age : None. salary : 0
Consumer eva. age : None. salary : 0
Consumer rick. age : None. salary : 0
Consumer alice. age : None. salary : 0
Consumer alex. age : 43. salary : 0]

Bloc d'instructions et indentation

L'indentation !

In [39]:
li = [1, 2, 3]
for item in li:
    if item == 38:
        print('38 !!!!')
    else:
    print('Not 38 ..')
  File "<ipython-input-39-7d74bf765d5d>", line 6
    print('Not 38 ..')
        ^
IndentationError: expected an indented block
  • Respecter les règles d'identations est nécessaire (comprendre obligatoire) en Python.

  • Ce n'est pas une contrainte lors d'une session de travail car les IDE guident la position du curseur.

  • Cette indentation a un rôle direct sur le contrôle du flux d'éxecution.

  • Elle permet d'éviter l'utilisation d'accolades (curly brackets) pour délimiter les blocs et de point-virgules pour délimiter les instructions.

In [40]:
# Ce code est volontairement incorrect /!\

result = []
li1 = [1, 2, 3, 4, 5, 6]
li2 = [4, 20, 31, 87, 123, 621]

# Ajoutons 1 à chaque élément
for item1, item2 in zip(li1, li2):
    new_item = item1 + item2
result.append(new_item)
# Cette instruction est éxécutée une seule fois
# après que l'ensemble des itérations de la boucle ai été effectué

print(result)
[627]
In [41]:
result = []
li1 = [1, 2, 3, 4, 5, 6]
li2 = [4, 20, 31, 87, 123, 621]

# Ajoutons 1 à chaque élément
for item1, item2 in zip(li1, li2):
    new_item = item1 + item2
    result.append(new_item)
    # Cette instruction est éxécutée à
    # chaque itération de la boucle.

print(result)
[5, 22, 34, 91, 128, 627]

Communauté, packages, documentation, etc.

La communauté Python compte de nombreux membres (c'est un des langages les plus en vogue 1, 2), dans des thématiques variées (géospatial, astronomie, bio-informatique, etc.).

En plus de sa facilité d'apprentissage, la polyvalence du langage est une des raisons de sa forte utilisation : il est facile d'utiliser le Python pour écrire des tests pour une bibliothèque d'un autre langage ou pour faire la "glue" entre plusieurs composants d'un système.

1 : https://insights.stackoverflow.com/survey/2018#technology
2 : https://stackoverflow.blog/2017/09/06/incredible-growth-python/

La gestion des packages se fait via l'utilitaire pip.
Contrairement à R où les bibliothèques sont généralement installées depuis l'interpréteur du langage, en Python elles sont installées depuis le terminal du système d'exploitation.

Les bibliothèques disponible via pip sont celle du Python Package Index : PyPI : https://pypi.org/.

Contrairement au CRAN, il s'agit d'un index non-supervisé et dans une certaine mesure on en restera conscient lorsqu'on l'utilise (risque de typosquatting, etc.).
Ce risque est toutefois très fortement réduit lors de l'utilisation du package manager d'une distribution Python telle qu'Anaconda ou Canopy qui seront présentées ensuite.

Utilisation basique :

pip install numpy

Si plusieurs installations de Python sont présentes sur le système d'exploitation (généralement sur GNU/Linux), il pourra être nécessaire de préciser pour laquelle on souhaite voir l'action réalisée:

pip3 install numpy

L'utilitaire pip peut également être utilisé pour installer du code depuis un repository git ou depuis un dossier local par exemple.

pip install git+https://github.com/user/repo.git@branch

Spécifier avec précision la version d'une bibliothèque à installer :

pip install pandas==0.23.4

Supprimer une bibliothèque installée :

pip uninstall pandas

La documentation officielle Python est très agréable, complète (documentation de l'ensemble des fonctions natives et de l'ensemble des modules de la bibliothèques standard, exemples, tutoriels, documentations pour les développeurs, etc.) et est disponible en français.

Elle est générée avec Sphinx, outil développé à l'origine pour générer cette documentation, et dont la maturité lui permet d'être utilisé pour générer la documentation de différents types de projets informatiques.