Sam & Max » numpy http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Jouons un peu avec Python 3.5 27 http://sametmax.com/jouons-un-peu-avec-python-3-5/ http://sametmax.com/jouons-un-peu-avec-python-3-5/#comments Wed, 16 Sep 2015 16:31:38 +0000 http://sametmax.com/?p=16918 fantastique article présentant Python 3.5, je ne vais donc pas pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.]]> Zeste de savoir a fait un fantastique article présentant Python 3.5, je ne vais donc pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.

La release est récente, mais fort heureusement on peut facilement l’installer. Sous Windows et Mac, il y a des builds tout chauds.

Pour linux, en attendant un repo tierce partie ou l’upgrade du système, on peut l’installer quelques commandes depuis les sources. Par exemple pour les distros basées sur Debian comme Ubuntu, ça ressemble à :

$ # dependances pour compiler python
$ sudo apt-get install build-essential libreadline-dev tk8.4-dev libsqlite3-dev libgdbm-dev libreadline6-dev liblzma-dev libbz2-dev libncurses5-dev libssl-dev python3-dev tk-dev
$ sudo apt-get build-dep python3 # juste pour etre sur :)
 
$ # téléchargement des sources
$ cd /tmp
$ wget https://www.python.org/ftp/python/3.5.0/Python-3.5.0.tar.xz
$ tar -xvf Python-3.5.0.tar.xz
$ cd Python-3.5.0
 
$ # et on build
$ ./configure
$ make
$ sudo make altinstall 
# pas 'make install' qui écrase le python du système !
 
$ python3.5 # ahhhhhh
Python 3.5.0 (default, Sep 16 2015, 10:44:14) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Sur les centos-likes, c’est grosso merdo la même chose, sans le build-dep (mais plutôt un truc genre sudo yum groupinstall 'Development Tools'), et en remplaçant les -dev par -devel.

Nouvel opérateur

@ est maintenant le nouvel opérateur de produit matriciel, mais il ne fait officiellement rien.

Comprenez par là que Python implémente l’opérateur, mais pas le produit en lui-même, la feature ayant été spécialement incluse pour faire plaisir aux utilisateurs de libs scientifiques type numpy.

On va donc tester ça sur le terrain. On se fait un petit env temporaire avec pew et on s’installe numpy :

pew mktmpenv -p python3.5
pip install pip setuptools --upgrade
pip install numpy 
# encore un peu de compilation

Testons mon bon. L’ancienne manière de faire :

>>> a = np.array([[1, 0], [0, 1]])
>>> b = np.array([[4, 1], [2, 2]])
>>> np.dot(a, b)
array([[4, 1],
       [2, 2]])

Et la nouvelle :

>>> a @ b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-3d41f06f59bb> in <module>()
----> 1 a @ b
 
TypeError: unsupported operand type(s) for @: 'numpy.ndarray' and 'numpy.ndarray'

Woops, apparemment numpy n’a pas encore implémenté le truc.

Bon. Bon, bon, bon. Comment on va tester alors… Ah, oui, y a une magic method :

class Array(np.ndarray):
    def __matmul__(self, other):
        return np.dot(self, other)
 
>>> a = a.view(Array)
>>> b = b.view(Array)
>>> a @ b
Array([[4, 1],
       [2, 2]])

Bon, voilà ce que ça donnera quand les devs de numpy auront implémenté le bouzin (la dernière ligne hein, pas tout le bordel avant).

Apparemment ça fait bander les matheux, donc je suppose que c’est une super nouvelle.

% is back on bytes

En python 2, on pouvait faire "truc %s" % "bidule" et u"truc %s" % u"bidule" et b"truc %s" % u"bidule" et ça a été viré en python 3 qui ne garde % que pour str et pas pour bytes.

Ca n’aurait pas été un problème si ce n’est que Python est très utilisé pour le réseau, et que construire un paquet qui mélange de la sémantique binaire et textuelle devient soudainement une grosse soupe de decode() et encode().

Jour 1, test 3, suspense…

>>> bytearray([66, 108, 117, 101, 32, 112, 114, 105, 101, 115, 116, 32, 115, 97, 121, 115, 58, 32, 37, 115]) % b"wololo"
bytearray(b'Blue priest says: wololo')

Voilà ça c’est fait !

os.scandir()

os.path.walk() est dans mon top 10 des APIs que je déteste le plus en Python, juste à côté de la gestion timezone. Avoir os.walk() en Python 3 qui retourne un générateur me ravit. Avoir une version 10 X plus rapide avec scandir, n’est-ce pas choupinet ?

>>> import os
>>> list(os.scandir('/tmp/'))
                       [<DirEntry 'systemd-private-316509818ceb41488a4721c78dabb603-colord.service-eXUfPo'>,
 <DirEntry 'unity_support_test.0'>,
 <DirEntry 'config-err-7UpWeO'>,
 <DirEntry '.ICE-unix'>,
 <DirEntry 'pip-rw_63q0_-unpack'>,
 <DirEntry 'systemd-private-316509818ceb41488a4721c78dabb603-systemd-timesyncd.service-eZumpq'>]

C’est très dommage que ça ne retourne pas des objets Path de pathlib, mais bon, les perfs, tout ça…

Zipapp, le grand inaperçu

Le saviez-vous ? Python peut exécuter un zip, ce qui permet de créer un script en plusieurs fichiers et de le partager comme un seul fichier. Non vous ne le saviez-vous-te-pas car personne n’en parle jamais.

La 3.5 vient avec un outil en ligne de commande pour faciliter la création de tels zip et une nouvelle extension (que l’installeur fera reconnaitre à Windows) pour cesdits fichiers : .pyz.

Je fais mon script :

foo
├── bar.py
├── __init__.py
└── __main__.py

__main__.py est obligatoire, c’est ce qui sera lancé quand on exécutera notre script. Dedans je mets import bar et dans bar print('wololo again').

Ensuite je fusionne tout ça :

python -m zipapp foo

Et pouf, j’ai mon fichier foo.pyz :

$ python3.5  foo.pyz
wololo again

Attention aux imports dedans, ils sont assez chiants à gérer.

L’unpacking généralisé

J’adore cette feature. J’adore toutes les features de la 3.5. Cette release est fantastique. Depuis la 3.3 chaque release est fantastique.

Mais bon, zeste de savoir l’a traité en long et en large donc rien à dire de plus, si ce n’est que j’avais raté un GROS truc :

  • On peut faire de l’unpacking sur n’importe quel itérable.
  • On peut faire de l’unpacking dans les tuples.
  • Les parenthèses des tuples sont facultatives.

Donc ces syntaxes sont valides :

>>> *range(2), *[1, 3], *'ea'
(0, 1, 1, 3, 'e', 'a')
>>> *[x * x for x in range(3)], *{"a": 1}.values()
(0, 1, 4, 1)

Ce qui peut être très chouette et aussi la porte ouverte à l’implémentation d’un sous-ensemble de Perl en Python. C’est selon l’abruti qui code.

Type hints

Ce qu’il faut bien comprendre avec les types hints, c’est que Python ne s’en sert pas. Il n’en fait rien. Que dalle. Nada. Peau de balle. Zob. Niet. Zero. La bulle. Néant. Null. None. Réforme gouvernementale.

Les types hints sont disponibles, mais Python ne va pas les traiter différemment d’autres annotations. Le but est de permettre à des outils externes (linter, IDE, etc) de se baser sur ces informations pour ajouter des fonctionnalités.

Pour l’instant, un seul le fait : mypy.

Et là on sent bien que tout ça est tout neuf car si on fait pip install mypy-lang, on tombe sur une version buggée. Il faut donc l’installer directement depuis le repo, soit :

pip install https://github.com/JukkaL/mypy/archive/master.zip

Puis écriture d’une fonction annotée avec des types hints :

 
from typing import Iterable, Tuple
 
PixelArray = Iterable[Tuple[int, int, int]]
 
def rgb2hex(pixels: PixelArray) -> list:
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]
 
 
# ça marche :
rgb2hex([(1, 2, 3), (1, 2, 3)])
# ['#010203', '#010203']

La preuve que Python n’en fait rien :

>>> hex("fjdkls")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-be62b8f062fe> in <module>()
----> 1 hex("fjdkls")
 
TypeError: 'str' object cannot be interpreted as an integer

Même la doc profite peu du typage :

Help on function rgb2hex in module __main__:

rgb2hex(pixels:typing.Iterable) -> list

Mais si on met ça dans un fichier foo.py :

from essai import rgb2hex
 
print(rgb2hex("fdjksl"))
res = rgb2hex([(1, 2, 3), (3, 4, 5)])
print(res + 1)

Et qu’on le passe à la moulinette :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Ensuite j’ai essayé de créer un stub file, c’est-à-dire de mettre les hints dans un fichier à part plutôt que directement dans le code. Ma fonction redevient :

def rgb2hex(pixels):
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]

Et mon fichier stub (même nom, mais avec extension .pyi) contient :

from typing import Iterable, Tuple
 
PixelArray = Iterable[Tuple[int, int, int]]
 
def rgb2hex(pixels: PixelArray) -> list:...

Les stubs sont donc bien des fichiers Python valides, mais avec une extension différente, et juste les signatures des fonctions (le corps du bloc est une Ellipsis).

Et poof, ça marche pareil :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Il y a un repo qui contient des fichiers stubs pour la stdlib. Vous pouvez y participer, c’est un moyen simple de contribuer à Python.

Bref, pour le moment ça demande encore un peu de maturité, mais je pense que d’ici quelques mois on aura des outils bien rodés pour faire tout ça automatiquement.

Async/await

La feature pub. Techniquement le truc qui a fait dire à tous ceux qui voulaient de l’asyncrone que Python en fait, c’était trop cool. Sauf que Python pouvait faire ça avec yield from avant, mais c’est sur que c’était super confusionant.

Maintenant on a un truc propre : pas de décorateur @coroutine, pas de syntaxe semblable aux générateurs, mais des méthodes magiques comme __await__ et de jolis mots-clés async et await.

Vu que Crossbar est maintenant compatible Python 3, et qu’il supporte asyncio pour les clients… Si on s’implémentait un petit wrapper WAMP pour s’amuser à voir ce que ressemblerait une API moderne pour du Websocket en Python ?

pip install crossbar
crossbar init
crossbar start

(Ouhhhh, plein de zolies couleurs apparaissent dans ma console ! Ils ont fait des efforts cosmétiques chez Tavendo)

Bien, voici maintenant l’exemple d’un client WAMP de base codé avec asyncio selon l’ancienne API :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
 
class MyComponent(ApplicationSession):
 
    @asyncio.coroutine
    def onJoin(self, details):
 
        # on marque cette fonction comme appelable
        # a distance en RPC
        def add(a, b):
            return a + b
        self.register(add, "add")
 
        # et on triche en l'appelant cash. J'ai
        # la flemme de coder un deuxième client
        # et ça passe quand même par le routeur
        # donc merde
        res = yield from self.call("add", 2, 3)
        print("Got result: {}".format(res))
 
 
if __name__ == '__main__':
    runner = ApplicationRunner("ws://127.0.0.1:8080/ws",
        u"crossbardemo",
        debug_wamp=False,  # optional; log many WAMP details
        debug=False,  # optional; log even more details
    )
    runner.run(MyComponent)

Et ça marche nickel en 3.5. Mais qu’est-ce que c’est moche !

On est en train de bosser sur l’amélioration de l’API, mais je pense que ça va reste plus bas niveau que je le voudrais.

Donc, amusons-nous un peu à coder un truc plus sexy. Je vous préviens, le code du wrapper est velu, j’avais envie de me marrer un peu après les exemples ballots plus haut :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
 
class App:
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.procedures = []
        self.subscriptions = []
        self.event_handlers  = {}
 
    def run(self, url="ws://127.0.0.1:8080/ws",
                realm="realm1", debug_wamp=False, debug=False):
        runner = ApplicationRunner(url, realm,
                                                     debug_wamp=debug_wamp,
                                                     debug=debug)
        runner.run(self)
 
    def run_cmd(self, *args, **kwargs):
        # et on pourrait même ici mettre du parsing d'argument
        # et de os.environ, mais j'ai la flemme
        if __name__ == '__main__':
            self.run(*args, **kwargs)
 
    # quelques décorateurs pour faire du déclaratif
    # et remettre les paramètres dans le bon ordre
    def register(self, name, *args, **kwargs):
            def wrapper(proc):
                self.procedures.append([name, proc, args, kwargs])
                return proc
            return wrapper
 
    def subscribe(self, topic, *args, **kwargs):
            def wrapper(callback):
                self.procedures.append([topic, callback, args, kwargs])
                return callback
            return wrapper
 
    # un système d'event interne
    def on(self, event):
            def wrapper(callback):
                self.event_handlers.setdefault(event, []).append(callback)
                return callback
            return wrapper
 
    async def trigger(self, event):
        for callback in self.event_handlers.get(event, ()):
            await callback(self.session)
 
    # un peu de code de compatibilité avec l'API initiale
    def __call__(self, *args):
        class CustomeSession(ApplicationSession):
            async def onJoin(session_self, details):
 
                # on joint on fait tous les registers et tous les
                # subscribes
                for name, proc, args, kwargs in self.procedures:
                     session_self.register(proc, name, *args, **kwargs)
 
                for topic, callback, args, kwargs in self.subscriptions:
                     session_self.subscribe(proc, topic, *args, **kwargs)
 
                # on appelle les handlers de notre event
                await self.trigger('joined')
        self.session = CustomeSession(*args)
        return self.session

Évidement la coloration syntaxique ne suit pas sur nos async/await.

Bon, vous allez me dire, mais ça quoi ça sert tout ça ? Et bien, c’est une version tronquée et codée à l’arrache de l’API Application pour Twisted… mais version asyncio.

C’est-à-dire que c’est une lib qui permet de faire le même exemple que le tout premier qu’on a vu dans cette partie – qui souvenez-vous était fort moche -, mais comme ça :

app = App()
 
@app.register('add')
async def add(a, b):
    return a + b
 
@app.on('joined')
async def _(session):
    res = await session.call("add", 2, 3)
    print("Got result: {}".format(res))
 
app.run_cmd()

Des jolis décorateurs ! Des jolis async ! Des jolis await !

Et tout ça tourne parfaitement sur 3.5 messieurs-dames.

Bref, on peut faire du WAMP avec une syntaxe claire et belle, il faut juste se bouger le cul pour coder une abstraction un peu propre.

Je pense que autobahn restera toujours un peu bas niveau. Donc il va falloir que quelqu’un se colle à faire une lib pour wrapper tout ça.

Des volontaires ?

Arf, je savais bien que ça allait me retomber sur la gueule.

]]>
http://sametmax.com/jouons-un-peu-avec-python-3-5/feed/ 27
Le pandas c’est bon, mangez en 7 http://sametmax.com/le-pandas-cest-bon-mangez-en/ http://sametmax.com/le-pandas-cest-bon-mangez-en/#comments Sat, 10 May 2014 06:30:35 +0000 http://sametmax.com/?p=10113 Les bases de Numpy, je m'en vais vous présenter une lib qui roxx du poney dans le calcul numérique : Pandas.]]> Ceci est un post invité de joshuafr posté sous licence creative common 3.0 unported.

Bonjour à tous, jeunes tailleurs de bambou, suite à un article d’introduction à numpy par le grand maître Sam Les bases de Numpy, je m’en vais vous présenter une lib qui roxx du poney dans le calcul numérique : Pandas.

Pour faire simple, Pandas apporte à Python la possibilité de manipuler de grands volumes de données structurées de manière simple et intuitive, chose qui faisait défaut jusqu’ici. Il y a bien eu quelques tentatives comme larry, mais rien n’avait jamais pu égaler les fonctionnalités du langage R. Aujourd’hui Pandas y arrive en fournissant notamment le célèbre type dataframe de R, avec en prime tout un tas d’outils pour agréger, combiner, transformer des données, et tout ça sans se casser le cul. Que du bonheur!

Donc pour commencer, on installe le bousin par un simple : pip install pandas qui va si vous ne l’avez pas déjà fait, aussi télécharger/compiler/installer tout un tas de librairies dont numpy. Je vous conseille aussi d’utiliser ipython afin d’avoir une meilleure interaction avec les libs, notamment avec matplotlib en utilisant le switch ipython --pylab afin d’avoir directement les graphiques en mode interactif, ainsi que toute la bibliothèque numpy directement importée (en interne, ipython fera un import numpy as np).
On appelle la bête d’un simple:

In [1]: import pandas as pd

Oui je sais, la grande classe…

Tout est Series

Le type de base en Pandas est la Series. On peut le voir comme un tableau de données à une dimension:

In [2]: pd.Series(np.arange(1,5))
Out[2]: 
0    1
1    2
2    3
3    4
dtype: int64

La colonne de gauche représente l’index de la Series, normalement unique pour chaque entrée. La colonne de droite correspond à nos valeurs sur lesquelles nous voulons travailler.
L’index n’est pas forcément une suite d’entiers, et la Series peut être nommée:

In [3]: s=pd.Series([1,2,3.14,1e6], index=list('abcd'), name='ma_series')
In [4]: s
Out[4]: 
a          1.00
b          2.00
c          3.14
d    1000000.00
Name: ma_series, dtype: float64

A noter qu’un type-casting est systématiquement appliqué afin d’avoir un tableau de type uniforme (ici le data-type est du float64) qui peut être modifié (dans une certaine mesure) via Series.astype.

Le slicing c’est comme du fisting avec une bonne dose de vaseline, ça glisse tout seul:

In [5]: s['b':'d']
Out[5]: 
b          2.00
c          3.14
d    1000000.00
Name: ma_series, dtype: float64

Et oui, la sélection par indexation se fait sur… l’index de la Series. Ainsi s[‘a’] renverra la ligne dont l’index est ‘a’, mais Pandas est assez intelligent pour reconnaître si on lui demande de nous renvoyer des valeurs suivant l’ordonnancement du tableau de valeurs (comme numpy). Ainsi s[0] renverra la première valeur du tableau, qui ici est égale à s[‘a’].
Là où ça peut poser problème c’est quand notre index est une suite d’entiers, comme par exemple avec x=pd.Series(np.arange(1,5), index=np.arange(1,5)). Si vous demandez x[1], Pandas ne retrouve pas ses petits et vous retournera une zolie KeyError. Pour ces cas ambigus, il existe l’indexation stricte sur le nom de index de la Series via x.loc[nom_d'index], et l’indexation stricte sur le numéro d’ordre dans le tableau via x.iloc[numéro_d'ordre]. Essayez x.loc[0] et x.iloc[0] pour vous rendre compte de la différence.
Comme pour les préliminaires où il est bon de tâter un peu de tout avec de pénétrer dans le vif du sujet, laissons pour le moment l’indexation sur laquelle nous reviendrons plus tard, pour regarder d’un peu plus près comment faire joujou avec nos valeurs.

Un peu à la manière des arrays de numpy, on peut appliquer des fonctions mathématiques directement sur la Serie, ou passer par des fonctions raccourcis:

In [6]: s.sum()
Out[6]: 1000006.14

Ce qui revient au même que de faire np.sum(s) (rappelez vous, ipython avec –pylab a importé numpy dans la variable np).

La fonction describe est bien utile pour avoir un aperçu rapide de ce à quoi on a affaire:

In [7]: s.describe()
Out[7]: 
count          4.000000
mean      250001.535000
std       499998.976667
min            1.000000
25%            1.750000
50%            2.570000
75%       250002.355000
max      1000000.000000
Name: ma_series, dtype: float64

ce qui donne le nombre de données, la moyenne globale, la déviation standard, le minimum, les quartiles et le maximum de la Serie.

Le truc à retenir est que c’est l’index qui est primordial dans un grand nombre d’opérations. Ainsi si l’on veut additionner 2 Series ensemble, il faut que leurs index soient alignés :

In [8]: s2=pd.Series(np.random.rand(4), index=list('cdef'), name='autre_serie')
 
In [9]: s+s2
Out[9]: 
a               NaN
b               NaN
c          4.021591
d    1000000.401511
e               NaN
f               NaN
dtype: float64

Ici, seuls les index ‘c’ et ‘d’ étaient présents dans les 2 Series, Pandas effectuant avant l’opération d’addition une union basée sur l’index. Les autres entrées sont marquées en NaN, soit Not a Number. Une possibilité pour contrer ce phénomène et de dire à Pandas de remplacer les résultats manquants lors de l’union par 0:

In [10]: s.add(s2, fill_value=0)
Out[10]: 
a          1.000000
b          2.000000
c          4.021591
d    1000000.401511
e          0.563508
f          0.655915
Name: ma_series, dtype: float64

Mais si ce sont uniquement les valeurs qui nous intéressent, et non les indexations, il est possible de les supprimer:

In [11]: s.reset_index(drop=True)+s2.reset_index(drop=True)
Out[11]: 
0          1.881591
1          2.401511
2          3.703508
3    1000000.655915
dtype: float64

Oh joie, oh bonheur, je peux faire ce que je veux avec mes cheveux, enfin mes données…

Et PAN! dans ta frame

La DataFrame est l’extension en 2 dimensions des Series. Elle peut être vue comme un empilement de Series dont les index sont partagés (et donc intrinsèquement alignés), ou comme dans un tableur où les index sont les numéros de lignes et les noms des Series les noms des colonnes. Je ne vais pas décrire toutes les manières de créer une DataFrame, sachez juste qu’on peut les obtenir à partir de dictionnaires, de liste de liste ou de liste de Series, d’arrays ou de records numpy, de fichier excel ou csv et même depuis des bases de données, de fichier JSON ou HTML, et depuis le presse-papiers.

In [14]: genre=[['femme','homme'][x] for x in np.random.random_integers(0,1,100)]
 
In [15]: lateral=[['droite','gauche'][x] for x in np.random.random_integers(0,1,100)]
 
In [16]: age=np.random.random_integers(1,100,100)
 
In [17]: df=pd.DataFrame({'Genre':genre, 'Lateral':lateral, 'Age':age})
 
In [18]: df
Out[18]: 
    Age  Genre Lateral
0    69  femme  droite
1    46  homme  droite
2    89  homme  droite
3    14  homme  droite
4    74  homme  droite
5     5  femme  gauche
6    66  femme  droite
7    73  homme  gauche
8    99  homme  gauche
9    17  homme  gauche
    ...    ...     ...
 
[100 rows x 3 columns]

L’affichage par défaut depuis la version 0.13 est en mode ‘truncate’ où la fin de la DataFrame est tronquée suivant la hauteur de votre terminal, mais ça peut se changer via les divers paramètres à regarder sous pd.options.display.

Là donc nous avons une DataFrame de 3 colonnes (plus un index), chaque colonne étant en réalité une Serie :

In [20]: type(df['Age'])
Out[20]: pandas.core.series.Series

La sélection peut se faire de plusieurs manières, à chacun de choisir sa préférée (moi c’est Dafnée avec ses gros nénés). Ainsi pour avoir les 3 premières lignes des âges

In [21]: df['Age'][0:3]
Out[21]: 
0    69
1    46
2    89
Name: Age, dtype: int64
 
In [22]: df[0:3]['Age']
Out[22]: 
0    69
1    46
2    89
Name: Age, dtype: int64
 
In [23]: df.Age[0:3]
Out[23]: 
0    69
1    46
2    89
Name: Age, dtype: int64
 
In [24]: df.loc[0:3, 'Age']
Out[24]: 
0    69
1    46
2    89
3    14
Name: Age, dtype: int64

et oui, les noms de colonnes peuvent aussi être utilisés comme des attributs de la DataFrame. Pratique (qu’on n’attend pas).

L’une des forces de Pandas est de nous proposer tout un tas de solutions pour répondre à des questions existentielles tel que “quel est l’âge moyen par genre et par latéralité?”. Comme en SQL où la réponse sortirait du fondement d’une clause GROUP BY et d’une fonction d’agrégation, il en va de même ici :

In [25]: df.groupby(['Genre','Lateral']).aggregate(np.mean)
Out[25]: 
                     Age
Genre Lateral           
femme droite   45.476190
      gauche   49.208333
homme droite   41.571429
      gauche   55.823529
 
[4 rows x 1 columns]

OMG! c’est quoi c’t’index de malade? Un MultiIndex jeune padawan, qui te permettra d’organiser tes données par catégorie/niveau, et d’y accèder par le paramètre level dans pas mal de fonctions, mais ça je te laisse le découvrir par toi-même. Je ne vais pas non plus m’étendre plus sur toutes les possibilités offertes par les DataFrame, il y a tellement à dire qu’il faudrait plusieurs articles pour en faire le tour. Juste conclusionner sur la facilité d’intégration Pandas/matplotlib en vous disant que les Series et DataFrame ont une fonction plot permettant directement de visualiser les données, et ça, c’est juste jouissif.

Datetime dans les index

Je vous avez dit qu’on reviendrait sur les indexes, et là c’est pour rentrer dans le lourd (mais non pas toi Carlos). Pandas donc supporte l’indexation sur les dates, en reprenant et en élargissant les possibilités offertes par feu le module scikits.timeseries.
Prenons l’exemple de données (complètement bidons) fournies par un capteur à intervalle régulier sur un pas de temps horaire:

In [26]: dtindex=pd.date_range(start='2014-04-28 00:00', periods=96, freq='H')
 
In [27]: data=np.random.random_sample(96)*50
 
In [28]: df=pd.DataFrame(data, index=dtindex, columns=['mesure'])
 
In [29]: df.head()
Out[29]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 01:00:00   1.910280
2014-04-28 02:00:00   7.534761
2014-04-28 03:00:00  39.416415
2014-04-28 04:00:00  44.213409
 
[5 rows x 1 columns]
 
In [30]: df.tail()
Out[30]: 
                        mesure
2014-05-01 19:00:00  25.291453
2014-05-01 20:00:00  26.520291
2014-05-01 21:00:00  33.459766
2014-05-01 22:00:00  44.521813
2014-05-01 23:00:00  28.486003
 
[5 rows x 1 columns]

dtindex est un DatetimeIndex initialisé au 28 avril 2014 à 0 heure comportant 96 périodes de fréquence horaire, soit 4 jours. La fonction date_range peut aussi prendre en arguments des objets datetime purs au lieu de chaine de caractère (manquerait plus que ça…), et le nombre de périodes peut être remplacé par une date de fin.
Si l’on veut calculer, disons le maximum (horaire) par jour, rien de plus simple, il suffit de “resampler” en données journalières (‘D’ pour Day) et de dire comment aggréger le tout:

In [31]: df.resample('D', how=np.max)
Out[31]: 
               mesure
2014-04-28  26.298282
2014-04-29  28.385418
2014-04-30  26.723353
2014-05-01  24.106092
 
[4 rows x 1 columns]

Mais on peut aussi convertir en données quart-horaire (upsampling) en remplissant les données manquantes par celles de l’heure fixe:

In [32]:  df[:3].resample('15min', fill_method='ffill')
Out[32]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  49.253929
2014-04-28 00:30:00  49.253929
2014-04-28 00:45:00  49.253929
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00   1.910280
2014-04-28 01:30:00   1.910280
2014-04-28 01:45:00   1.910280
2014-04-28 02:00:00   7.534761
 
[9 rows x 1 columns]

Cependant, Pandas propose aussi d’autres possibilités non dépendantes des DatetimeIndex mais qu’il est bon de connaître, notamment celle pour remplacer les données manquantes avec fillna ou celle pour interpoler entre les données valides avec interpolate

In [52]:  df[:3].resample('15min')
Out[52]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00        NaN
2014-04-28 00:30:00        NaN
2014-04-28 00:45:00        NaN
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00        NaN
2014-04-28 01:30:00        NaN
2014-04-28 01:45:00        NaN
2014-04-28 02:00:00   7.534761
 
[9 rows x 1 columns]
 
In [53]:  df[:3].resample('15min').fillna(df.mean())
Out[53]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  26.378286
2014-04-28 00:30:00  26.378286
2014-04-28 00:45:00  26.378286
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00  26.378286
2014-04-28 01:30:00  26.378286
2014-04-28 01:45:00  26.378286
2014-04-28 02:00:00   7.534761
 
[9 rows x 1 columns]
 
In [54]:  df[:3].resample('15min').interpolate()
Out[54]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  37.418016
2014-04-28 00:30:00  25.582104
2014-04-28 00:45:00  13.746192
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00   3.316400
2014-04-28 01:30:00   4.722520
2014-04-28 01:45:00   6.128641
2014-04-28 02:00:00   7.534761
 
[9 rows x 1 columns]

Voilà, j’espère que vous aurez plaisir à travailler avec cette librairie, il manquait vraiment un outil de cette trempe en Python pour l’analyse de données et je pense qu’on n’a plus trop grand chose à envier maintenant par rapport à des langages spécilisés. Je n’ai pas parlé de Panel qui est le passage à la troisième dimension, ni des possibilités d’export, notamment la df.to_html que je vous laisse le soin de découvrir.

A plus, et amusez vous bien avec votre bambou.

\o/

]]>
http://sametmax.com/le-pandas-cest-bon-mangez-en/feed/ 7
Les bases de Numpy 22 http://sametmax.com/les-bases-de-numpy/ http://sametmax.com/les-bases-de-numpy/#comments Wed, 19 Feb 2014 19:18:54 +0000 http://sametmax.com/?p=9198 Numpy est une lib destinée à la manipulation de grands ensembles de nombres et est très utilisée par la communauté scientifique.

Elle propose des types et des opérations beaucoup plus performants que ceux de la lib standard, et possède des raccourcis pour les traitements de masse.

Malheureusement, c’est aussi une lib complexe, et, comme souvent dans le monde de la science, les tutos pourraient être plus clairs.

Cet article ne prétend pas à une couverture exhaustive de Numpy, d’autant que je n’ai pas le niveau en maths pour faire une simple dérivée alors des opérations matricielles complexes…

Mais ça devrait permettre de démystifier le truc pour les gens qui regardent ça de loin comme si c’était un pingouin au Mali.

Commencez par installer la bestiole avec un pip install numpy. Faites-vous un café pendant que ça compile.

array sur image

A la base de Numpy, il y a la manipulation d’ensembles ordonnés de nombres. On peut faire les opérations voulues sur un type list ordinaire, mais ce serait lent, et ça prendrait pas mal de mémoire.

L’alternative est d’utiliser un type optimisé comme ndarray, fourni par Numpy.

Cela se manipule comme une tuple, avec une différence majeure : il ne peut contenir qu’un seul type de données. Donc on ne met que des int, ou que des str, que des bool, etc.

On peut construire un array à partir de n’importe quel itérable :

>>> from numpy import array
>>> array([1, 2, 3])
array([1, 2, 3])
>>> array(u"azerty")
array(u'azerty',
      dtype='<U6')
>>> array((1.0, 2.0, 3.0))
array([ 1.,  2.,  3.])

Mais souvent on utilisera une fonction générant un array automatiquement afin d’éviter de créer deux structures de données (la liste, puis l’array par exemple).

On peut utiliser arange, qui est l’équivalent de range, mais pour les arrays :

>>> from numpy import arange
>>> arange(1, 100, 2)
array([ 1,  3,  5,  7,  9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33,
       35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67,
       69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99])

Ainsi que des choses plus perfectionnées comme linspace, qui retourne un array de n nombre de valeurs uniformément réparties entre deux bornes:

>>> from numpy import linspace
>>> linspace(0, 100, 15) # 15 valeur entre 0 et 100
array([   0.        ,    7.14285714,   14.28571429,   21.42857143,
         28.57142857,   35.71428571,   42.85714286,   50.        ,
         57.14285714,   64.28571429,   71.42857143,   78.57142857,
         85.71428571,   92.85714286,  100.        ])

Comme les tuples, les arrays sont itérables, sliceables, indexables et de taille fixe :

>>> sistance = arange(10)
>>> for x in a: # iterable
...     print x
...
0
1
2
3
4
5
6
7
8
9
>>> sistance[2:4] # sliceable
array([2, 3])
>>> sistance[-1] # indexable
9
>>> sistance.append(11) # taille fixe
Traceback (most recent call last):
  File "<ipython-input-17-389b8ea2fe68>", line 1, in <module>
    sistance.append(11)
AttributeError: 'numpy.ndarray' object has no attribute 'append'

L’array représente donc une photographie figée de vos données, mais comme vous allez le voir, rapide et précise à manipuler.

Opérations groupées

La caractéristique marquante de l’array, c’est que si vous lui appliquez un opérateur mathématique, un nouvel array est retourné dont TOUTES les valeurs ont été modifiées.

Par exemple, si vous multipliez un array, un nouvel array est retourné avec toutes les valeurs multipliées :

>>> duku = array([1, 2, 3])
>>> au_milieu = duku * 2
>>> au_milieu
array([2, 4, 6])

En fait, numpy fait une boucle implicite – et performante – sur tout l’array pour chaque opération mathématique. Et ça devient intéressant quand on veut faire des opérations entre plusieurs arrays entre eux :

>>> duku
array([1, 2, 3])
>>> au_milieu
array([2, 4, 6])
>>> de_tram = duku + au_milieu
>>> de_tram
array([3, 6, 9])

Une autre dimension

Si on travaille sur une liste plate, l’array est pratique, mais on en reste là. Néanmoins sa grande force est sa capacité à travailler sur plusieurs dimensions, et donc modifier tout aussi facilement des arrays d’arrays d’arrays d’arrays (arrête !) :

>>> thorique = array([ [1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> thorique
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
>>> thorique ** 3
array([[  1,   8,  27],
       [ 64, 125, 216],
       [343, 512, 729]])

L’opération a été appliquée à tous les éléments sans faire de boucle, et en suivant l’imbrication de la structure de données récursivement.

Mais la partie la plus funky, c’est que le slicing AUSSI, peut se faire sur plusieurs dimensions.

Vous connaissez le slicing à une dimension :

>>> import random
>>> ticence = array([[random.randint(0, 100) for x in range(5)] for x in range(5)])
>>> ticence
array([[77, 44, 93, 65,  3],
       [ 8, 64, 36, 80, 77],
       [69, 24, 57, 18, 99],
       [60, 33, 63, 71, 99],
       [33, 60, 98, 85, 70]])
>>> ticence[1:4] # récupération des élément de 1 (inclus) à 4 (exclus)
array([[ 8, 64, 36, 80, 77],
       [69, 24, 57, 18, 99],
       [60, 33, 63, 71, 99]])

Mais avec un array numpy, on peut utiliser une virgule après le premier slicing, et mettre un nouveau slicing qui va travailler sur la dimension suivante.

Par exemple, ici j’applique le slice 1:4 sur la première dimension (je diminue le nombre de lignes) et ensuite j’applique le slicing 0:3 sur la seconde dimension (je diminue le nombre d’éléments de chaque lignes restantes, donc des colonnes).

>>> ticence[1:4, 0:3]
array([[ 8, 64, 36],
       [69, 24, 57],
       [60, 33, 63]])

Ca marche comme ça :

array[slicing_sur_dimension1, slicing_sur_dimension2, slicing_sur_dimension3, etc]

Si on commence à avoir beaucoup de dimensions et qu’on ne veut toucher que la dernière dimension, on peut utiliser Ellipsis.

>>> d2 = array([[random.randint(0, 100) for x in range(5)] for x in range(5)])
>>> d3 = array([d2.copy() for x in range(5)])
>>> d4 = array([d3.copy()  for x in range(5)])
>>> d5 = array([d4.copy()  for x in range(5)])
>>> d6 = array([d5.copy()  for x in range(5)])
>>> d6[1:3,...,-3:-1]
          [... plein de trucs ...]
          [[ 9, 86],
           [16, 40],
           [63, 26],
           [51,  5],
           [ 3, 46]],
 
          [[ 9, 86],
           [16, 40],
           [63, 26],
           [51,  5],
           [ 3, 46]]]]]])

La dernière ligne, on prend un tableau à 6 dimensions, on applique un slicing 1:3 sur la première dimension, et un slicing -3:-1 sur la dernière dimension.

Il est vrai que je ne m’en sers pas souvent.

Ok, je ne m’en suis jamais servi de toute ma vie. A part pour ce tuto. Mais c’est super classe non ?

Attention, cela ne marche que si toutes les dimensions ont le même nombre d’éléments. Cela se voit facilement en cas d’erreur car si le nombre d’éléments n’est pas bon, numpy va afficher votre array en ligne et pas sous forme de tabulaire :

>>> array([[1, 2, 5, 7, 9, 7, 8],[1,9]])
array([[1, 2, 5, 7, 9, 7, 8], [1, 9]], dtype=object)

Vous voyez en plus qu’il précise ici dtype=object, alors qu’il ne l’a pas fait plus haut.

Matplotlib pour afficher tout ça

En théorie, la lib matplotlib n’a rien à voir avec numpy. En pratique les utilisateurs de numpy utilisent très souvent numpy + matplotlib + ipython pour avoir une équivalent de matlab en Python.

Matplotlib est une lib qui permet de dessiner des graphes, mais qui a la particularité d’être orienté interaction. C’est à dire qu’elle est plus destinée à fabriquer votre graphe à la main, en bidouillant vos données, et possède dont des facilités pour cela.

D’abord on pip install matplotlib, et on prie pour que ça marche car sur certains OS ça plante méchamment.

Et ensuite dans son shell, on peut créer un petit graphe facilement sans trop se soucier des réglages, ceux par défaut étant pas mal :

from pylab import plot, xlabel, ylabel, title, legend
from numpy import sin, pi, linspace
 
# On active le mode interactif.
# Cela permet de voir notre graph
# en popup et de le modifier
# en temps réel.
ion()
 
# Utilisation de mes vagues connaissances
# de trigo pour pondre une sinusite...
# Heu, une sinusoide.
 
# Un array de 50 points répartis uniformément
# entre 0 et 2pi. Ca va nous servir de
# première coordonnée pour nos points.
x = linspace(0, 2 * pi)
 
# La fonction sin() de numpy va
# faire un nouvel array avec le sinus
# des points de l'array précédent.
# Ca nous fait notre deuxième coordonnée.
y = sin(x)
 
# On dessine la courbe, et on lui donne un pti nom
plot(x, y, label=u"Moi")
# Si je me suis pas trop planté ça devrait osciller
# entre 1 et -1
 
# On labellise les abscisses et les ordonnées
# car des données sans une échelle claire ne
# servent à rien.
ylabel(u"Self esteem (sur l'echelle de Richter)")
xlabel(u'Temps passé sur Dota (en joule par km)')
 
# On titre notre œuvre
title("Brace yourself, the graph is comming")
# On active la légende car le sujet est légendaire
legend()

Ce qui nous affiche :

Graphe de courbe sinusoidale

J'imagine toujours un petit train sur ce genre de courbe

Vous ne vous transformerez pas tout de suite en chercheur du CNRS après avoir lu ce tuto, mais j’espère qu’il vous aura donné un peu envie de faire mumuse avec Python pour manipuler vos données scientifiques.

]]>
http://sametmax.com/les-bases-de-numpy/feed/ 22
Explication de code : callback à la mise à jour d’un array Numpy 2 http://sametmax.com/explication-de-code-callback-a-la-mise-a-jour-dun-array-numpy/ http://sametmax.com/explication-de-code-callback-a-la-mise-a-jour-dun-array-numpy/#comments Thu, 30 Aug 2012 14:32:13 +0000 http://sametmax.com/?p=1939 l'explication de code, voici un petit bout de numpyries envoyé par un lecteur.]]> Toujours dans l’esprit de l’explication de code, voici un petit bout de numpyries envoyé par un lecteur.

Lisez l’article sur les callbacks si vous n’êtes pas familiers avec le principe, et en avant:

# on importe numpy, une bibliothèque spécialisée dans le calculs impliquant de grands
# jeux de nombres et très utilisée par les scientifiques
import numpy as np
 
# On créé une classe qui hérite du type np.ndarray qui est un type d'array
# de taille fixe, et dont tous les objets doivent être du même type,
# mais qui peut avoir plusieurs dimensions et qui est très rapide à manipuler
# Notez que par convention NumPy n'utilise pas de majuscule pour le nom de 
# ces types pour matcher str, int, unicode, etc. Pour rester congruent,
# la classe enfant ne le fait pas non plus. Ne prenez pas cette habitude, 
# c'est un cas particulier.
class cbarray(np.ndarray):
 
 
    # __new__ est la seule méthode de classe par défaut, sans déclaration de @classmethode, 
    # donc le premier argument sera la classe en court. Normalement la convention
    # est d'appeler cet argument cls, mais ici l'auteur fait à sa sauce...
    #
    # Le reste des arguments sont les mêmes que pour __init__: ce sont ceux
    # attendus à l'instanciation de la classe cbarray:
    #   * data sont les données qu'on veut mettre dans l'array
    #   * cb est un callback qu'on appelera à chaque mise à jour de l'array
    #   * dtype est le type des données à mettre dans l'array (si on veut bypasser l'autodétection)
    #   * copy pour présicer si on veut copier les données, ou juste liér les données existantes
    # 
    # __new__ est appelée avant __init__: elle prend les paramètres qu'attend __init__, 
    # fabrique une instance, la retournepuis __init__ est appelée avec l'instance. 
    # On a rarement besoin d'overrider __new__, en générale __init__ est un meilleur choix
    # car c'est plus simple. Mais certains types NumPy ne permettent pas de faire 
    # autrement.
    def __new__(subtype, data, cb=None, dtype=None, copy=False):
 
        # On attache le callback à la CLASSE, et non à l'instance.
        # Ca peut paraitre étrange, mais on verra plus bas pour
        subtype.__defaultcb = cb
 
        # Selon que l'on souhaite copier les données, ou juste les lier
        # on créé une instance d'un type différent.
        # Notez que l'absence d'espace atour de "," et "=" n'est pas recommandé
        # pas le PEP8
        if copy:
            data = np.array(data,dtype=dtype)
        else:
            data = np.asarray(data,dtype=dtype)
 
        # Cette astuce permet d'utiliser un des deux types du dessus
        # mais au travers de l'API du type "subtype", c'est à dire notre 
        # classe.
        data = data.view(subtype)
 
        # On retourne l'instance ainsi créé.
        # Quand l'utilisateur fera cbarray(....), c'est cette instance
        # qu'il recevra
        return data
 
    # Juste une méthode qui appelle le callback si il existe
    def _notify(self):
        if self.cb is not None:
            self.cb()
 
    # Une propriété qui retourne un attribut de l'objet
    # en s'assurant qu'on utilise celui du parent.
    # C'est une supposition mais je pense que shape est une propriété du
    # parent, qu'elle n'est pas dispo sur le type array ou asarray, et 
    # que l'astuce data.view ne suffit pas à faire un proxy de celle-ci.
    # Donc je pense que ça sert à donner accès à cette donnée.
    def _get_shape(self):
        return super(cbarray, self).shape
    shape = property(_get_shape)
 
    # __setitem__ est une méthode "magique", appelée automatiquement qu'on
    # fait array[item] = val
    # Ici on l'utilise pour appeler _notify() à chaque mise à jour de l'array
    # et donc d'appeler le callback à chaque mise à jour.
    # Il est dommage de ne pas passer de paramètre à la méthode, comme 
    # l'ancienne valeur, l'item et la nouvelle valeur. Le callback va être
    # du coup assez limité. Mais ça suffira si par exemple tout ce qu'on
    # veut faire c'est écrire dans un fichier à chaque modification.
    def __setitem__(self, item, val):
        np.ndarray.__setitem__(self, item, val)
        self._notify()
 
    # NumPy permet de créer des sous types à partir du type de base, c'est
    # d'ailleurs une manière très courrante de créer des nouveaux conteneurs
    # de données. Mais ce faisant, NumPy bypass le mécanisme d'instanciation,
    # et __new__ et __init__ ne sont donc pas appelées. Pour y pallier, NumPy
    # ajoute la méthode __array_finalize__ qui est toujours appelée quand
    # un array est prêt à être utilisé. Elle peut être utilisée pour effectuer
    # un traitement pour chaque array créé, quelque soit sa provenance.
    # Ici, on l'utilise pour attacher le callback à "l'instance".
    # Souvenez-vous, plus haut on avait attaché le callback à la CLASSE.
    # Cette classe peut derrière être la source de nombreux arrays même si
    # __init__ n'est pas appelé pour eux :-(
    # La solution de l'auteur est donc de passer le callback à __new__, de 
    # l'attacher à la classe, et à travers __array_finalize__, de l'attacher
    # à "l'instance". Il faut garder en tête que tout nouvel appel à __new__
    # écrasera le callback pour toutes les instances suivantes. Mis à part
    # cela, ceci garanti que tout array aura le callback, et donc que 
    # _notify aura accès au callback, et donc que __setitem__ déclenchera le 
    # callback, et donc que la fonction sera bien appelée à chaque mise à jour
    # de l'array
    def __array_finalize__(self,obj):
        if not hasattr(self, "cb"):
            # The object does not yet have a `.cb` attribute
            self.cb = getattr(obj,'cb',self.__defaultcb)
 
    # Encore une méthode "magique" ajoutée par NumPy. Elle est appelée
    # quand on sérialise l'array et retourne des informations sur l'état
    # de l'array
    def __reduce__(self):
        object_state = list(np.ndarray.__reduce__(self))
        subclass_state = (self.cb,)
        object_state[2] = (object_state[2],subclass_state)
        return tuple(object_state)
 
    # inverse de __reduce__
    def __setstate__(self,state):
        nd_state, own_state = state
        np.ndarray.__setstate__(self,nd_state)
 
        cb, = own_state
        self.cb = cb
 
# Le callback qui doit être appelé à chaque mise à jour de l'array
# Je ne pense pas que ça puisse marcher, car il attend un argument,
# ce que _notify() ne lui passe pas.
def callback(arg):
    print 'array changed to',arg
 
 
# une petit démo du code si on run le script au lieu de l'importer
if __name__ == '__main__':
    x = cbarray([1,2,3], cb=callback)
    x[[0,1]] = 1.0
]]>
http://sametmax.com/explication-de-code-callback-a-la-mise-a-jour-dun-array-numpy/feed/ 2