Gráficas interactivas con Plotly

Por Pablo Fernández

Podéis conseguir el notebook y los archivos asociados en GitHub.

En un artículo anterior ya vimos como pasar las gráficas creadas con matplotlib a plotly para obtener cierto nivel de interacción con los datos en la web. Ahora, lo que vamos a ver es cómo crear gráficas directamente con plotly. Para ello vamos a utilizar:

  • pandas 0.14.1, para trabajar con tablas de datos.
  • plotly 1.2.6, para crear las gráficas.

Importamos los paquetes de la manera habitual en cada caso.

import pandas as pd
import plotly.plotly as py
from plotly.graph_objs import *

En el caso de Plotly hemos importado además unos objetos graph_objs que nos serán de ayuda a la hora de crear las gráficas. Trabajar con la API de Plotly en Python se resume en trabajar con listas y diccionarios de Python. Los graph_objs de Plotly nos ayudarán a trabajar con estas listas y diccionarios proporcionando ayuda y validando los datos y parámetros introducidos.

Entre los objetos que importamos tenemos los bloques principales de trabajo:

  • Figure, diccionario que representa la figura a representar en plotly.
  • Data, lista para englobar todos los trazos a representar en una gráfica, ya sean tipo Scatter, Heatmap, Box, etc.
  • Layout, diccionario que define los detalles de la disposición de la figura.
help(Figure)

Help on class Figure in module plotly.graph_objs.graph_objs:
class Figure(PlotlyDict)
 |  A dictionary-like object representing a figure to be rendered in plotly.
 |
 |      This is the container for all things to be rendered in a figure.
 |
 |      For help with setting up subplots, run:
 |      `help(plotly.tools.get_subplots)`
 |
 |
 |  Quick method reference:
 |
 |      Figure.update(changes)
 |      Figure.strip_style()
 |      Figure.get_data()
 |      Figure.to_graph_objs()
 |      Figure.validate()
 |      Figure.to_string()
 |      Figure.force_clean()
 |
 |  Valid keys:
 |
 |      data [required=False] (value=Data object | array-like of one or several
 |      dictionaries):
 |          A list-like array of the data trace(s) that is/are to be visualized.
 |
 |          For more, run `help(plotly.graph_objs.Data)`
 |
 |      layout [required=False] (value=Layout object | dictionary-like):
 |          A dictionary-like object that contains the layout parameters (e.g.
 |          information about the axis, global settings and layout information
 |          related to the rendering of the figure).
 |
 |          For more, run `help(plotly.graph_objs.Layout)`
 |
[...]

:::python
help(Data)

Help on class Data in module plotly.graph_objs.graph_objs:
class Data(PlotlyList)
 |  A list of traces to be shown on a plot/graph.
 |
 |      Any operation that can be done with a standard list may be used with Data.
 |      Instantiation requires an iterable (just like list does), for example:
 |
 |      Data([Scatter(), Heatmap(), Box()])
 |
 |      Valid entry types: (dict or any subclass of Trace, i.e., Scatter, Box, etc.)
 |
[...]

:::python
help(Layout)

Help on class Layout in module plotly.graph_objs.graph_objs:
class Layout(PlotlyDict)
 |  A dictionary-like object holding plot settings for plotly figures.
 |
 |
 |  Quick method reference:
 |
 |      Layout.update(changes)
 |      Layout.strip_style()
 |      Layout.get_data()
 |      Layout.to_graph_objs()
 |      Layout.validate()
 |      Layout.to_string()
 |      Layout.force_clean()
 |
 |  Valid keys:
 |
 |      title [required=False] (value=string):
 |          The title of the figure.
 |
[...]

Como vemos, Figure y Layout son objetos de tipo diccionario y Data es un objeto de tipo lista. En el caso de Data, el orden es importante pues determina el orden de composición de los trazos, empezando por el primero objeto en Data. Por ejemplo, representar una línea sobre una barras no produce, normalmente, el mismo resultado que representar unas barras sobre una línea.

En Plotly cada tipo de gráfica tiene su propio objeto (trace graph object) como son Scatter, Bar o Histogram.

help(Scatter)

Help on class Scatter in module plotly.graph_objs.graph_objs:
class Scatter(PlotlyTrace)
 |  A dictionary-like object for representing a scatter plot in plotly.
 |
 |      Example:
 |
 |          py.plot([Scatter(name=';tacters';, x=[1,4,2,3], y=[1,6,2,1])])
 |
 |
 |  Quick method reference:
 |
 |      Scatter.update(changes)
 |      Scatter.strip_style()
 |      Scatter.get_data()
 |      Scatter.to_graph_objs()
 |      Scatter.validate()
 |      Scatter.to_string()
 |      Scatter.force_clean()
 |
 |  Valid keys:
 |
[...]

Ejemplo práctico: aeropuerto de Heathrow

Hecha la presentación de Plotly pasamos a un ejemplo práctico de uso de la herramienta. Para ello tomaremos datos de estadísticas de tráfico del aeropuerto de Heathrow, el tercer mayor del mundo por tráfico de pasajeros.

Datos estadísticos

Los datos son proporcionados en un fichero Excel que podemos importar con Pandas.

pasajeros = pd.io.excel.read_excel('07-Heathrow_Monthly_Traffic_Statistics_(Jan_2005-Jul_2014).xls',
                                   sheetname=0, # tomamos la primera hoja del archivo
                                   header=2, # la cebecera empieza en la fila 3
                                   index_col=0) # empleamos las fechas como indices
pasajeros.head(5)
Heathrow Southampton Glasgow Aberdeen Non-London Airports UK Total
Month
2005-01-01 5141123 99945 504138 186378 790461 5931584
2005-02-01 4753591 109120 506851 189925 805896 5559487
2005-03-01 5708627 131983 603225 227621 962829 6671456
2005-04-01 5573022 145749 641107 232191 1019047 6592069
2005-05-01 5636621 168971 795732 242493 1207196 6843817
mercancias = pd.io.excel.read_excel('07-Heathrow_Monthly_Traffic_Statistics_(Jan_2005-Jul_2014).xls',
                                    sheetname=5, # tomamos la sexta hoja del archivo
                                    header=2, # la cebecera empieza en la fila 3
                                    index_col=0) # empleamos las fechas como indices
mercancias.head(5)
Heathrow Southampton Glasgow Aberdeen Non-London Airports UK Total
Month
2005-01-01 98781.175 16.237 491.582 304.274 812.093 99593.268
2005-02-01 99555.454 16.554 545.170 310.282 872.006 100427.460
2005-03-01 109387.896 18.830 578.286 368.156 965.272 110353.168
2005-04-01 108057.553 18.277 569.431 321.004 908.712 108966.265
2005-05-01 110612.691 17.466 661.753 369.324 1048.543 111661.234

Como podemos ver, se trata de una serie temporal. Y puesto que los datos se proporcionan mes a mes, podríamos deshacernos del día del mes indicandole a Pandas que se trata de un periodo de frecuencia mensual con to_period. Pero no es necesario, pues como veremos más adelante, Plotly es capaz de intuir que queremos representar los datos mes a mes.

pasajeros.to_period('M').head(2)
Heathrow Southampton Glasgow Aberdeen Non-London Airports UK Total
Month
2005-01 5141123 99945 504138 186378 790461 5931584
2005-02 4753591 109120 506851 189925 805896 5559487

Representación gráfica de los datos

Si ya hemos guardado nuestras credenciales de Plotly en el ordenador, al importar el paquete como

import plotly.plotly as py

ya nos logueamos automáticamente en el servidor sin tener que acceder mediante

py.sign_in('username', 'api_key')

Una figura (Figure) Plotly se compone de los datos a representar (Data) y de un formato (Layout), y estos a su vez no son más que un conjunto de listas y diccionarios que Plotly se encargará de convertir a un formato JSON. Como hemos mencionado arriba, Plotly proporciona una serie de graph_objs que nos facilitarán la tarea de componer gráficas interactivas y que veremos a continuación.

Data

Empezamos por el conjunto de datos a representar. En este caso vamos a representar el tráfico mensual de pasajeros en los aeropuertos británicos del grupo Heathrow, participada al 25% por Ferrovial, que incluye los aeropuertos de:

  • London Heathrow Airport
  • Southampton Airport
  • Glasgow Airport
  • Aberdeen Airport

Para representar estos datos nos valdremos de la herramienta Data que, como hemos visto anteriormente, admite una lista de objetos. En nuestro caso, líneas. Para ello nos valdremos de Scatter (dispersión) al cual pasaremos los siguentes parámetros:

  • name, nombre que damos a la línea, en nuestro caso, el nombre del aeropuerto.
  • x, array con los meses del año que corresponden al index de nuestro DataFrame.
  • y, array con el número de pasajeros correspondientes a cada aeropuerto.
  • mode, cadena de texto que indica el tipo de representación que queremos, ya sea 'lines', 'markers', 'text' o una combinación de ellos como podría ser 'lines+markers'.

Puesto que se trata de una lista con cuatro líneas a representar, haremos uso de las list comprehensions de Python.

p_data = Data([Scatter(name=col,
                       x=pasajeros.index,
                       y=pasajeros[col],
                       mode='lines') for col in pasajeros.columns.values[:4]])

Layout

Ya con los datos a representar definidos, ahora podemos pasar a retocar el layout de la figura. Para ello vamos a añadir un título a la gráfica y a los ejes x e y. Otra cosa que haremos también es encuadrar la gráfica con

showline=True, mirror='ticks', linewidth=2

y reducir los margenes derecho r y superior t para aprovechar mejor el espacio.

p_layout = Layout(title='Tráfico mensual de pasajeros en aeropuertos del grupo Heathrow',
                  xaxis=XAxis(title='Mes', showgrid=False, showline=True, mirror='ticks', linewidth=2),
                  yaxis=YAxis(title='Pasajeros', zeroline=False, showline=True, mirror='ticks', linewidth=2),
                  margin=Margin(r=20, t=80))

Figure

Una vez ya tenemos los datos y el layout podemos pasar a componer la figura y subirla al servidor.

p_fig = Figure(data=p_data, layout=p_layout)
p_plot = py.iplot(p_fig, filename='pybonacci/heathrow-pasajeros')

Diccionarios y listas

Tanto Figure como Layout, XAxis, YAxis y Margin se podrían substituir por la expresión dict() pues, como ya hemos mencionados, Plotly trabaja con diccionarios y listas de Python. Sin embargo, el utilizar estas herramientas de plotly.graph_objs nos da acceso a la ayuda, y nos permite validar los parámetros introducidos.

m_data = Data([Scatter(name=col,
                       x=mercancias.index,
                       y=mercancias[col],
                       mode='lines') for col in pasajeros.columns.values[:4]])

Podemos hacer lo mismo con dict(), pero cualquier error pasará desapercibido hasta el final.

m_layout = dict(title='Tráfico mensual de mercancías en aeropuertos del grupo Heathrow',
                xaxis=dict(title='Mes', showgrid=False, showline=True, mirror='ticks', linewidth=2),
                yaxis=dict(title='Mercancías (t)', zeroline=False, showline=True, mirror='ticks', linewidth=2),
                margin=dict(r=20, t=80))

:::python
m_fig = Figure(data=m_data, layout=m_layout)
m_plot = py.iplot(m_fig, filename='pybonacci/heathrow-mercancias')

Interpretación de los datos

Disponemos de una muestra lo suficientemente grande como desaprovechar la oportunidad de extraer alguna conclusión al respecto. Y contamos con la ventaja de poder hacer zoom en los datos, lo cual resulta especialmente útil en le tráfico de mercancías, donde el aeropuerto de Heathrow está varios órdenes de magnitud por encima del resto.

Pasajeros

Vemos claramente que en los meses de verano hay un aumento del número de pasajeros en todos los aeropuertos del grupo. También se aprecia un ligero repunte en el mes de diciembre con motivo, previsiblemente, de las vacaciones de navidad. Esto lo podemos visualizar de otra manera mediante un Heatmap del aeropuerto de Heathrow.

Para ello vamos a utilizar el paquete calendar que nos permitirá crear una lista con los nombres de los meses; y Numpy para crear una lista con los años.

import calendar
import numpy as np

Para representar el Heatmap necesitaremos agrupar los datos por años o meses, en función del eje y que tomemos. En este caso hemos decido representar los meses en el eje de ordenadas, por lo tanto agruparemos los datos por meses. Para ello nos valdremos de una función anónima.

gb = lambda x: x.month

:::python
data = Data([Heatmap(x=np.arange(2005,2015),
                     y=calendar.month_abbr[1:],
                     z=[grp['Heathrow'] for key, grp in pasajeros.groupby(gb)],
                     colorscale='Portland')])

En el eje x hemos colocado los años, en el eje y los meses, y la intensidad del color viene determinada por el número de pasajeros.

Con Layout añadimos el título de la gráfica y los nombres de los ejes, y también podemos especificar el tamaño de la gráfica deshabilitando el autosize y definiendo nuestro propio ancho y alto.

layout = Layout(title='Tráfico de pasajeros en el aeropuerto de Heathrow',
                autosize=False,
                width=550,
                height=550,
                xaxis=XAxis(title='Año', showgrid=False),
                yaxis=YAxis(title='Mes', showgrid=False))

Ya podemos publicar nuestra gráfica de la manera habitual.

heatmap_plot = py.iplot(Figure(data=data,layout=layout), filename='pybonacci/heathrow-heatmap')

Mercancías

Si en el transporte de pasajeros hay un patrón claro, el transporte de mercancías por avión no muestra signos de estacionalidad. Para verlo mejor podríamos volver a emplear un Heatmap, pero vamos a hacerlo con un diagrama de caja Box para el aeropuerto de Heathrow.

Aprovecharemos nuevamente la agrupación por meses que hemos empleado para el Heatmap de pasajeros. Nuevamente hacemos uso de las list comprehensions de Python para crear una lista de bloques, cada uno correspondiente a un mes. Lo mismo podríamos conseguirlo sin crear una lista y sin necesidad de agrupar si en vez de asignar un name asignamos un único array x con el valor del mes correspondiente a cada y. Con boxpoints='all' lo que hacemos es mostrar los puntos de la muestra al lado de cada bloque.

data = Data([Box(name=calendar.month_abbr[key],
                 y=grp['Heathrow'].values,
                 boxpoints='all') for key, grp in mercancias.groupby(gb)])

Añadimos, como es costumbre, un título a la gráfica y aprovechamos para ocultar la leyenda y ajustar los margenes.

layout = Layout(title='Tráfico de mercancías en el aeropuerto de Heathrow (2005-2014)',
                showlegend=False,
                margin=Margin(r=20, t=90))

:::python
box_plot = py.iplot(Figure(data=data, layout=layout), filename='pybonacci/heathrow-box')

Pasajeros vs mercancías

Hasta ahora hemos visto por separado los datos de pasajeros y mercancías. Compararemos en una única gráfica los datos del aeropuerto de Glasgow. Para facilitar la visualización y compensar la diferencia de magnitud utilizaremos múltiples ejes y, de paso, emplearemos diferentes representaciones para el tráfico de pasajeros y el de mercancías.

Los pasajeros los representaremos mediante líneas y puntos 'lines+markers' y le asignamos el segundo eje y 'y2', pues vamos a querer que nos lo represente por encima de las barras de tráfico de mercancías. El orden en Plotly es importante. Vamos a representar las lineas de color dorado primero como horizontales y luego verticales de un valor a otro con shape='hv'. Los puntos serán de un color dorado más claro con el borde también dorado.

pas = Scatter(name='Pasajeros',
              x=pasajeros.index,
              y=pasajeros['Glasgow'],
              yaxis='y2',
              mode='lines+markers',
              line=Line(shape='hv', color='darkgoldenrod'),
              marker=Marker(color='goldenrod',
                            line=Line(color='darkgoldenrod', width=2)))

Por su parte, el tráfico de mercancías lo representaremos como barras verticales de color gris claro. Por defecto se le asigna el primer eje y.

mer = Bar(name='Mercancías',
          x=pasajeros.index,
          y=mercancias['Glasgow'],
          marker=Marker(color='lightgray'))

Creamos la lista con los elementos a representar.

data = Data([mer, pas])

Por último configuramos el layout añadiendo un título a la gráfica y configurando los ejes.

layout = Layout(title='Tráfico de pasajeros y mercancías en el aeropuerto de Glasgow',
                showlegend=False,
                xaxis=XAxis(title='Mes'),
                yaxis=YAxis(title='Mercancías (t)',
                            showgrid=False),
                yaxis2=YAxis(title='Pasajeros',
                             showgrid=False,
                             overlaying='y',
                             side='right'))

Incluimos también una nota de texto indicando la fuente de los datos. Plotly nos permite untilizar un subconjunto de etiquetas HTML para dar formato a los textos para por ejemplo incluir nuevas líneas (<br>) o añadir hipervínculos (<a href='...'></a>) que podremos utilizar en cualquier texto de la gráfica (títulos y anotaciones).

fuente = Annotation(text="Fuente: LHR Airports Limited",
                    xref='paper', # empleamos coordenadas 'paper', con ello la nota no se moverá al mover los ejes.
                    yref='paper',
                    x=0,
                    y=-0.15,
                    showarrow=False)

Actualizamos el diccionario de layout de la gráfica.

layout.update(annotations=Annotations([fuente]))

:::python
ma_plot = py.iplot(Figure(data=data, layout=layout), filename='pybonacci/heathrow-multipleaxis')

Mucho más

Aquí sólo hemos mostrado una pequeña parte del potencial de Plotly. Todo aquel que quiere ampliar detalles encontrará mucha más información en el API library de Plotly. También se pueden sacar buenas ideas del graphing news feed viendo lo que publican otros usuarios.

No dudeis en contactar con nosotros o el equipo de Plotly por email o Twitter para cualquier duda o sugerencia.

Comentarios