import numpy as np
import pandas as pd
import plotly.offline as py
import plotly.graph_objects as go
py.init_notebook_mode(connected=True)
Surface¶
On commence avec l'équivalent d'un tracé de courbe en 2D à savoir un tracé de surface. Pour cela on définit
- un maillage en (x,y) avec
np.meshgrid
après avoir indiqué le nombre de points désirés en x et en y avecnp.linspace
(100 points dans l'exemple qui suit), - les valeurs en z de la surface en tout point du maillage (
mz
dans notre exemple)
Il ne reste plus qu'à utiliser le maillage et les valeurs en z pour définir la Surface
qu'on affiche avec iplot
.
x = np.linspace(0,9,100)
y = np.linspace(0,3,100)
mx, my = np.meshgrid(x,y)
mz = np.sin(my) * np.cos(mx)
trace = go.Surface(x=mx, y=my, z=mz)
py.iplot([trace], show_link=False)
Malheureusement le résultat ne correspond pas à ce qu'on attend lorsqu'on connait la forme de $\sin(x) \times \cos(y)$, chaque bosse doit être symétrique et avoir une base carrée.
Définir la scène¶
Le problème vient du fait que par défault Plotly veut faire tenir le résultat dans un cube et donc les axes ne sont pas
normés ! Aussi il faut indiquer que la vision du résultat respecte les proportions à savoir que la boîte qui représente la scène du film est proportionnelle aux min et max des données suivant les axes. Pour cela on doit indiquer dans la mise
en page que l'aspect de la scene
respecte les données.
On verra par la suite que le nom des axes est aussi défini dans la scene
du layout
et non plus directement dans le
layout
.
layout = go.Layout(scene = {'aspectmode':'data'})
fig = go.Figure(data=[trace], layout=layout)
py.iplot(fig, show_link=False)
Maillage surfacique en 3D¶
La Surface
vue précédemment correspond à une valeur z pour tout point (x,y) du plan. Si on désire afficher une sphère cela ne marche plus. Il faut utiliser Mesh
qui permet d'afficher n'importe quelle forme 3D avec un ensemble de triangles, ensemble appelé maillage.
L'intérêt d'un tel maillage est d'afficher la variation d'une donnée sur une surface complexe. Cela peut être la pression sur la surface d'un avion par exemple.
Un maillage est défini par
- ses noeuds avec pour chaque noeud ses coordonnées
x
,y
,z
- ses triangles avec pour chaque triangle les numéros
i
,j
,k
des 3 noeuds qui le forme - la valeur (
intensity
) en tout noeud qui va définir la couleur affichée
Voici un maillage très simple avec 3 triangles :
data = go.Mesh3d(
# 0 1 2 3 4 5 6 7 (node number)
x = [0, 0, 1, 1, 0, 0, 1, 1],
y = [0, 1, 1, 0, 0, 1, 1, 0],
z = [0, 0, 0, 0, 1, 1, 1, 1],
i = [0, 0, 5],
j = [1, 4, 2],
k = [2, 7, 3],
# 0 1 2 3 4 5 6 7
intensity = [0.1, 0.2, 0.7, 1, 1.1, 1.3, 0.5, 0.6]
)
py.iplot([data])
On notera que le noeud numéro 6 n'a pas été utilisé.
Bien sûr en pratique on écrit un programme qui génère les coordonnées et la valeur des noeuds de notre maillage ainsi que la liste des triangles. En fait, en pratique on utilise un programme dédié à la simulation numérique.
Nuage de points et caméra¶
Regardons la relation entre le classement au tennis, la taille et l'âge. Les données sont prises du site de l'ATP (https://datahub.io/sports-data/atp-world-tour-tennis-data) et datent de fin 2017.
from dateutil.relativedelta import relativedelta
ranks = pd.read_csv("data/atp_rank.csv")
players = pd.read_csv("data/atp_player.csv", parse_dates=['birthdate'])
players = pd.merge(ranks, players, on='player_id')
players = players[["rank_number", "first_name", "last_name", "birthdate", "birthplace" , "height_cm", "weight_kg"]]
players['age'] = [relativedelta(pd.Timestamp.now(), bi).years for bi in players["birthdate"] ]
players['country'] = players['birthplace'].apply(lambda x:x.split(', ')[-1])
countries = set(players['country'].values) # let define one color per country
colors = np.random.randint(0xFFFFFF, size=(len(countries)))
countries_colors = {n:f"#{colors[i]:06x}" for i,n in enumerate(countries)}
players['country_color'] = players['country'].apply(lambda x: countries_colors[x])
players.head()
rank_number | first_name | last_name | birthdate | birthplace | height_cm | weight_kg | age | country | country_color | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | Rafael | Nadal | 1986-06-03 | Manacor, Mallorca, Spain | 185.0 | 85.0 | 37 | Spain | #08ce9a |
1 | 2 | Roger | Federer | 1981-08-08 | Basel, Switzerland | 185.0 | 85.0 | 42 | Switzerland | #6386f7 |
2 | 3 | Grigor | Dimitrov | 1991-05-16 | Haskovo, Bulgaria | 191.0 | 80.0 | 32 | Bulgaria | #e33962 |
3 | 4 | Alexander | Zverev | 1997-04-20 | Hamburg, Germany | 198.0 | 86.0 | 26 | Germany | #69a26d |
4 | 5 | Dominic | Thiem | 1993-09-03 | Wiener Neustadt, Austria | 185.0 | 82.0 | 30 | Austria | #e8dc21 |
L'affichage d'un nuage de point en 3D se fait avec la commande Scatter3d
.
S'il est possible de bouger la figure avec la souris, il est aussi possible de définir l'angle de vue initial en
placant la camera dans la scène avec scene_camera
.
trace = go.Scatter3d(
x = players['age'],
y = players['height_cm'],
z = players['rank_number'],
text = players['first_name'] + " " + players['last_name'] + " (" + players['country'] + ")",
mode = 'markers',
marker = dict(
color = players['country_color'],
opacity = 0.8),
)
layout2 = go.Layout(scene = {'aspectmode':'cube'})
layout2['scene'].update(xaxis = {'title':'Age'}, yaxis = {'title':'Height'}, zaxis = {'title':'Rank'})
layout2['title'] = 'TOP 50 tennis players in nov. 2017'
camera = dict( up=dict(x=1, y=0, z=0), # determine the up direction on the page (here x i.e. Age)
center=dict(x=0, y=0, z=0), # (0,0,0) is always the center of the domain, no matter data values
eye=dict(x=0.25, y=1.7, z=-1.2) # x 2 to value unzoom by 2, < 1 and you are in the domaine
)
layout2['scene_camera'] = camera
fig = go.Figure(data=[trace], layout=layout2)
py.iplot(fig, show_link=False)