Gráficos com Bokeh

O que é Bokeh


Bokeh é uma biblioteca de visualização de dados interativa em Python que existe desde 2013. Ela pode ser usada para a plotagem de gráficos em diversos níveis de sofisticação, representando conjuntos simples ou complexos de dados. A biblioteca pode ser usada por usuários com pouca experiência em programação ou programadores experientes com acesso aos seus comandos mais intrincados. Os gráficos do Bokeh podem ser interativos e embutidos em páginas da web.

Algumas definições básicas na terminologia de Bokeh são necessárias:

Application um aplicativo Bokeh é um documento renderizado e executado no navegador.
Glyphs glifos são os blocos de construção do Bokeh como linhas, círculos, retângulos e outras formas,
Server o servidor Bokeh é usado para compartilhar e publicar gráficos e aplicativos interativos para um público de sua escolha
Widgets os widgets do Bokeh são controles tais como menus suspensos, controles deslizantes e outras ferramentas de interface gráfica com o usuário que permitem interatividade

Instalação

Para instalar o Bokeh, se você tem Anaconda ou Miniconda, basta usar o comando: conda install bokeh.
Usando pip a biblioteca pode ser instalada com: pip install bokeh.

Comandos básicos

Dois tipos de saídas podem ser obtidas: o gráfico enviado para um arquivo output_file('arquivo.html') ou embutidos no Jupyter Notebook, output_notebook(). Bokeh possui uma interface similar à do matplotlib, que é denominada bokeh.plotting. A classe principal dessa interface é Figure que contém os métodos para a inclusão de glyphs em um gráfico.

» # importar as classes necessárias
» from bokeh.io import output_notebook, show
» from bokeh.plotting import figure
» output_notebook()
Figura 1
» # dados a plotar » x = [0,1, 0,3] » y = [0,10,90,10] » # instanciar um objeto figure » fig = figure(plot_width=450, plot_height=300) » # desenhar uma linha ligando os pontos dados » fig.line(x,y) » # exibir a figura 1 » show(fig)

A variável fig contém um objeto da classe com largura e altura especificadas, e instrução relativas às ferramentas a serem apresentadas, do lado direito no caso. O comando fig.line(x,y) usa o glyph line (linha) para ligar os pontos dados nas duas listas.

Glyphs

Glyphs são todos os elementos gráficos como linhas, círculos e cruzes marcadores de pontos, etc. Diferentes glyphs podem ter parâmetros ajustáveis diferentes. No exemplo aplicamos uma cor de fundo à figura, largura e altura. fig.circle() recebe os parâmetros posição (x,y), tamanho, que no caso é variável, cada círculo com raio size=y, largura de linha (as circunferências) line_width=5, e cor color=['red', 'blue','green','yellow']. Cada um dos discos tem uma cor diferente.

» # define dados    
» x = [1,2,3,4]
» y = [10,40,90,160]
Figura 2
» # instancia figura com cor de fundo e dimensões dadas » fig = figure(background_fill_color='#aabbff', » plot_width=450, plot_height=300) » fig.circle(x,y, size=y, line_width=5, color=['red', 'blue','green','yellow'], alpha=.5) » # exibir figura 2 » show(fig)

Os seguintes glyphs estão disponíveis:

asterisk() cross() diamond() diamond_cross()
circle() circle_x() circle_cross() triangle()
inverted_triangle() square() square_x() square_cross() x()

Alguns exemplos de uso de glyphs line, circle, cross, asterisk, x estão abaixo. As ordenadas y foram calculadas para formarem uma sequência de parábolas empilhadas, exceto pela reta horizontal amarela larga de fundo.

» # define valores da abscissa. Ordenadas serão calculadas    
» x = np.arange(10)

» plot = figure(plot_width=650, plot_height=300)
» plot.line(x, 100, color='yellow', line_width=140, alpha=.2,)
» plot.circle(x, x**2, size = 20, color='red', alpha=.5, line_width=7)
» plot.cross(x, x**2+50, size = 20, color='blue', alpha=.8, line_width=7)
» plot.asterisk(x, x**2+100, size = 40, color='green', alpha=.8, line_width=7)
» plot.x(x, x**2+150, size = 40, color='black', alpha=.8, line_width=7)
# figura 3
» show(plot)
Figura 3

As propriedades de cada glyph podem ser calculadas e dependentes em qualquer fonte de dados. Na caso abaixo usamos a própria ordenada x para calcular alguns desses parâmetros. A propriedade color=['yellow','blue']*5 garante que os 10 ‘diamantes’ plotados alternem entre as cores amarelo e azul.

» x = np.arange(10)    
» plot = figure(plot_width=650, plot_height=300)
» plot.circle_cross(x, x, size = 5+x, color='#ffaaff', alpha=1, line_width=7+x)
» plot.circle_dot(x, x, size = 30-2*x, color='#66aaff', alpha=.5, line_width=2)
» plot.inverted_triangle(x, x+5, size = 30-2*x, color='red', alpha=.9, line_width=2)
» plot.diamond(x, x+5, size = 30-2*x, color=['yellow','blue']*5, alpha=.8, line_width=2)
» show(plot)
» # figura 4 é plotada

» # outro plot com tamnho e cor variáveis
» x = np.arange(10)
» plot = figure(plot_width=600, plot_height=300)

» for k in range(100):
»     plot.circle(k, (k-50)**2, size = k*2, color=(255*k/100, 200, 255),
»                 fill_color=(2.5*k, 100, 255-2.5*k), alpha=.4, line_width=2)
» show(plot)
» # figura 5 é plotada

Gráficos de Barras (Bar Plots )

Para gráficos de barras a sintaxe é um pouco diferente. As coordenadas x são o ponto central da barra vertical, top é a altura. A largura width= 1 significa nenhum espaçamento entre barras. As cores podem ser uma só ou uma lista, de mesmo tamanho que o número de barras. Para as barras horizontais o comprimento das barras é dado por right e a largura da barra é height.

» x = [8,9,10]
» y = [1,4,2]

» # barras verticais
» plot = figure(plot_width=600, plot_height=300)
» # plot.vbar para traçar barras verticais
» plot.vbar(x,top = y, color = ['blue','red','green'], width= .8, alpha=.5)
» show(plot)     # exibe gráfico 6

» # barras horizontais
» plot = figure(plot_width=600, plot_height=300)
» plot.hbar(x, right = y, color = ['#77aaff','#aa77ff','#ff77aa'], height= .9, alpha=.5)
» show(plot)     # exibe gráfico 7


O desenho da regiões ou patches é feito com plot.patches. As regiões são descritas por meios das coordenadas de suas arestas, dois pares de listas para cada figura. As propriedades fill_color, line_color, line_width, alpha receberam listas de 3 elementos, um para cada figura. Se um valor único for passado ele será válido para todas as figuras.

» # regiões a colorir
» x_coords = [[1,1,3,], [2,2,2.5], [1.5,1.5,4,4]]
» y_coords = [[2,6,4], [3,6,7], [3,6,7,2]]

» plot = figure(plot_width=600, plot_height=300)
» plot.patches(x_coords, y_coords, fill_color = ['#77aaff','#aa77ff','#ff77aa'],
               line_color ='black', alpha=.4)
» show(plot)      # figura 8
Figura 8

Gráficos de Dispersão (Scatter Plots )

Gráficos de dispersão podem ser feitos com qualquer um dos glyphs. No exemplo abaixo a mesma plotagem é feita com círculos e com cruzes de tamanhos diversos, para efeito estético.

» from bokeh.models import Range1d
» plot = figure(plot_width=400, plot_height=250,
»               x_axis_label = 'Coordenada x (abcissa)',
»               y_axis_label = 'Ordenada y', title='Gráfico de dispersão')
» plot.x_range = Range1d(0, 5)
» plot.y_range = Range1d(0, 8)
» fcor = ['red','green','blue','brown','violet']
» x = np.array([1,2,3,4,4])
» y = np.array([5,6,2,2,4])
» plot.circle(x,y, size =x*15, color = '#aa55ff', fill_color=fcor, fill_alpha=.3)
» plot.diamond(x,y, size = x*15, color = 'red', alpha=.5,
»              fill_alpha=.4, fill_color=fcor[::-1])

» show(plot)    # figura 9
Figura 9

Observe que as coordenadas x, y poderiam ser listas. Como são arrays (do numpy) as operações para o cálculo do tamanho são permitidas. As faixas de coordenadas e ordenadas plotadas são controladas por x_range, y_range e estabelecidas por meio da função Range1d(m, n) (importada de bokeh.models). Os parâmetros color e alpha se referem ao traçado do glyph, enquanto fill_color e fill_alpha ao seu preenchimento. Relembrando, fcor[::-1] retorna a lista em ordem reversa.

Dataframes e ColumnDataSource

Usamos, até aqui, listas e arrays como fonte de nossos dados e serem plotados. Também podemos usar dataframes como fontes e o processo não é muito diferente. Se um dataframe tem uma coluna x e outra y plotamos o gráfico x × y simplesmente passando as series como parâmetros para x e y: plot.line(x = df['x'], y = df['y']).

Para montar um gráfico um pouco mais elaborado vamos usar os dados já descritos na seção sobre matplotlib. São dados sobre o número de nascimentos em países do mundo de 1950 até 2020, e a estimativa à partir de 2021. Importamos o arquivo .csv para o dataframe dfBrasil e selecionamos apenas as linhas relativas ao Brasil, até o ano de 2020. Esse dataframe é usado para plotar o gráfico de linhas. Outro dataframe, dfDecada, contendo apenas linhas com anos múltiplos de 10, é usado para plotar círculos. O raio do círculo é proporcional ao número de nascimentos.

» import pandas as pd
» dfNasc = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecionamos apenas linhas sobre o Brasil, até 2020
» dfBrasil = dfNasc[(dfNasc['Entity']=='Brazil') & (dfNasc['Year'] < 2021)]
» dfBrasil = dfBrasil.rename(columns={'Year':'ano', dfBrasil.columns[3]:'nasc'})
» # mantemos apenas colunas 'ano', 'nasc'
» dfBrasil = dfBrasil[['ano', 'nasc']]
» dfBrasil.head(2)
↳          ano         nasc
  4050    1950    2439820.0
  4051    1951    2467186.0

» # criamos outro df, apenas com anos multiplos de 10
» dfDecada = dfBrasil[dfBrasil['ano']%10==0]

» cor = ['salmon','gold','teal','plum','powderblue','coral','wheat','azure']
» plot = figure(plot_width=400, plot_height=250,
»               x_axis_label = 'Ano',
»               y_axis_label = 'Nascimentos (milhões)',
»               title='Número de Nascimentos no Brasil')
» plot.line(x = dfBrasil['ano'], y = dfBrasil['nasc']/1e6, color='black')
» plot.circle(x = dfDecada['ano'], y = dfDecada['nasc']/1e6,
»             size=dfDecada['nasc']/1e5, fill_color = cor,
»             fill_alpha=.5)
» show(plot)    # figura 10
Figura 10

Uma forma útil de fazer a conexão com os dados é o objeto ColumnDataSource. Ela é especialmente útil quando se usa a mesma fonte para diversas plotagens e para vários widgets. ColumnDataSource cria um dicionário onde as chaves podem ter nomes definidos pelo usuário e as valores correspondentes são os dados contidos em colunas do dataframe (ou outra fonte).

Vamos retornar aos dados relativos aos nascimentos nos países do mundo. Dessa vez vamos manter apenas dados sobre o Brasil e a Indonésia (escolhido porque é um país que tem população próxima à brasileira), apenas nos anos de 1950 até 2020. Nessa tabela os países recebem os códigos Code='BRA' e 'IDN', respectivamente.

» dfNasc = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecionamos as linhas sobre o Brasil e a Indonésia, até 2020
» dfBI = dfNasc[((dfNasc['Code']=='BRA') | (dfNasc['Code']=='IDN')) & (dfNasc['Year'] < 2021)]
» dfBI = dfBI.rename(columns={'Year':'ano', dfBI.columns[3]:'nasc'})

Desses dados criamos um dataframe apenas com dados brasileiros, outro com dados sobre a Indonésia. Para mesclar esses dataframes alteramos as colunas ‘nasc’ respectivamente para ‘BRA’ e ‘IDN’.

» dfB = dfBI[['ano','nasc']][dfBI['Code']=='BRA'].rename(columns={'nasc':'BRA'})
» dfB.head(3)
↳          ano          BRA
  4050    1950    2439820.0
  4051    1951    2467186.0
  4052    1952    2523577.0

» dfI = dfBI[['ano','nasc']][dfBI['Code']=='IDN'].rename(columns={'nasc':'IDN'})
» dfI.head(3)
↳          ano          IDN
  14700    1950    2867664.0
  14701    1951    2939269.0
  14702    1952    3078414.0
Para ler mais sobre a operação do pandas realizada, similar a um INNER JOIN do sql, consulte o artigo Pandas e SQL Comparados, nesse site.

Ambos os dataframes têm 71 linhas. Usamos pandas.merge() para juntar esses dataframes pelo campo ‘ano’, um processo similar ao INNER JOIN do sql. Depois criamos três novas colunas: (1) campo dif, com a diferença por ano entre os números brasileiros e indonésios, (2), difM, a média entre os dois e (3) raio, descrito no comentário † abaixo.

» dfBI = pd.merge(dfB, dfI, on='ano')
» dfBI.head(3)
↳       ano          BRA          IDN
  0    1950    2439820.0    2867664.0
  1    1951    2467186.0    2939269.0
  2    1952    2523577.0    3078414.0

» dfBI['dif'] = dfBI['IDN'] - dfBI['BRA']
» dfBI['difM'] = (dfBI['IDN'] + dfBI['BRA'])*.5
» dfBI['raio'] = dfBI['dif']/33000                   # veja comentário †

» # o dataframe fica assim:
» dfBI
↳          ano           BRA           IDN          dif          difM         raio
    0     1950     2439820.0     2867664.0     427844.0     2653742.0     12.964970
    1     1951     2467186.0     2939269.0     472083.0     2703227.5     14.305545
    2     1952     2523577.0     3078414.0     554837.0     2800995.5     16.813242

() A terceira coluna adicional, raio, é a diferença vezes um fator para que os discos em plot.circle() preencham o espaço entre os nascimentos nos dois países, centrados na média. Essa plotagem aqui tem apenas efeito visual e para demonstrar os parâmetros do plot.

» from bokeh.models import Range1d
» from bokeh.plotting import ColumnDataSource
    
» # cria o objeto ColumnDataSource
» data = ColumnDataSource(dfBI)
» plot = figure(width=900, height=250, x_axis_label = 'Ano', y_axis_label = 'Nascimentos e diferenças',
»               background_fill_color='#cfefff', border_fill_color='#ddeeff',
»               title='Nascimentos no Brasil e Indonésia')

» plot.x_range = Range1d(1950, 2035)
» plot.y_range = Range1d(0, 5.5E6)

» plot.line(x = 'ano', y = 'BRA', source = data, color = 'red', legend_label = "Brasil")
» plot.line(x = 'ano', y = 'IDN', source = data, color = 'green', legend_label = "Indonésia")
» plot.x(x = 'ano', y = 'dif', source = data, color = 'blue', legend_label = "diferença")
» plot.asterisk(x = 'ano', y = 'difM', source = data, color = 'black', legend_label = "média")
» plot.circle(x = 'ano', y = 'difM', source = data, fill_color = 'whitesmoke', alpha=.2, size = 'raio')

» show(plot)    # figura 11
Figura 11

Nesse gráfico introduzimos as legendas para cada plot. O campo difM foi plotado duas vezes, uma com um asterisco, outro com círculos com tamanhos determinados pelo campo raio. As faixas de plotagem, ranges, foram determinados para incluir gráfico e legendas. Cor de fundo para o gráfico e bordas são definidas com background_fill_color e border_fill_color.

Para o próximo gráfico baixamos para a subpasta dados do atual projeto o arquivo owid-covid-data.csv, publicado por Our World in Data com dados diários sobre a vacinação mundial contra o covid, entre 01/01/2020 e 26/09/2021. Deste aproveitamos apenas algumas colunas para plotar gráficos para efeito de demonstração do bokeh.

» # importamos os dados para um dataframe
» dfVacina = pd.read_csv('./dados/owid-covid-data.csv')

» # o dataframe tem 64 colunas e 119454 linhas
» dfVacina.shape      # (119454, 64)

» # podemos ver os nomes das colunas com
» dfVacina.columns    # nomes omitidos aqui

» # usamos apenas as colunas no dicionário
» colunas = {'date':'data',
»            'iso_code':'code',
»            'total_cases':'total',
»            'gdp_per_capita':'pib',
»            'human_development_index':'idh',
»            'life_expectancy':'expVida',
»            'total_deaths_per_million':'mortes',
»            'people_vaccinated_per_hundred':'vacinados'           
»           }
» # renomeamos as colunas
» dfVacina = dfVacina.rename(columns=colunas)

» # uma lista dos novos nomes:
» lst = list(colunas.values())
» # geramos novo df apenas com essas colunas
» df = dfVacina[lst]
» # eliminamos os linhas com NaN
» df = df.fillna(method='bfill')      # veja comentário ‡

» # as três primeiras linhas são
» df.head(3)
↳              dia    code   total         pib      idh   expVida    mortes   vacinados
   0    2020-02-24     AFG     5.0    1803.987    0.511     64.83     0.025         0.0
   1    2020-02-25     AFG     5.0    1803.987    0.511     64.83     0.025         0.0
   2    2020-02-26     AFG     5.0    1803.987    0.511     64.83     0.025         0.0

» # finalmente montamos um dataframe contendo apenas o último dia registrado
» dfUltimo = dfU[dfU['dia']=='2021-09-26']

() O método df.fillna(method='bfill') preenche valores nulos com o valor encontrado na mesma coluna, em linha posterior. (Leia aqui sobre tratamento de dados ausentes).

Lembramos que code identifica o país, total é o número total de casos de infecção por covid, mortes é o número total de mortes, por milhão e vacinados é o número de pessoas vacinadas, por 100 mil.

Podemos, em alguns casos, desejar incluir no gráfico um valor calculado a partir de um ou mais campos da tabela. Por ex., considerando que o campo idh varia entre 0,4 até 0,95, podemos usar esse campo, multiplicado por um fator, como informação do tamanho dos círculos plotados. Para fazer isso poderíamos incluir uma coluna extra com esse valor, como já foi feito em exemplos anteriores. Mas quando usamos o ColumnDataSource temos uma forma mais direta de fazer o mesmo. Podemos passar valores calculados no dicionário de valores que alimenta o ColumnDataSource.

» from bokeh.plotting import ColumnDataSource
» data = ColumnDataSource(data = {
»                        'idh' : dfUltimo['idh'],
»                        'expVida' : dfUltimo['expVida'],
»                        'tamanho': dfUltimo['idh']*20,
»                        'grande': dfUltimo['idh']*40,
»                        'alfa': dfUltimo['idh']*.08})
» plot = figure(width=600, height=300, x_axis_label = 'IDH',
»               y_axis_label = 'Exp. Vida', outline_line_color='black',
»               background_fill_color='#F5F1E3', title='IDH x Expectativa de Vida')

» plot.circle(x = 'idh', y = 'expVida', source = data, color='blue', alpha=.6,
»             fill_color = 'white', fill_alpha=1,  size = 'tamanho')
» plot.circle(x = 'idh', y = 'expVida', source = data, color='black', alpha= .1,
»            fill_color = 'red', fill_alpha='alfa', size = 'grande')

» show(plot)     # figura 12
Figura 12

Os campos do dataframe foram passados como valores em um dicionário cujas chaves são usadas como nome de campos nas plotagens. Os campos 'tamanho': dfUltimo['idh']*20 e 'grande': dfUltimo['idh']*40 são calculados para servir como informação para o tamanho (size ) dos círculos. O segundo círculo plotado tem apenas efeito estético, com um tamanho maior que o primeiro. O campo calculado alfa (uma fração do idh) é usado para regular a transparência dos discos vermelhos maiores.

O uso de ColumnDataSource permite que mais de um dataframe forneça dados para o gráfico. No entanto todas as series envolvidas devem ter o mesmo tamanho. Para ver isso vamos separar os dados sobre o Brasil e os EUA em duas tabelas separadas.

» # separa os dados relativos ao Brasil e os EUA
» dfBU = df[(df['code']=='BRA') | (df['code']=='USA')].copy()    # comentário §

» # para usar as datas no eixo x transformamos o campo 'dia' de string em datetime
» dfBU.loc[:,'dia'] = pd.to_datetime(df.loc[:,'dia'], format='%Y/%m/%d')    

» # com essa transformação a coluna passa a conter um datetime (timestamp). Por ex.:
» dfBU.loc[15250][0]
Timestamp('2020-02-26 00:00:00')

» # criamos dataframes para os dois países                      # comentário ‡
» dfUS = dfBU[(dfBU['code']=='USA') & (dfBU['dia'] &ge '2020-02-26')]
» dfBR = dfBU[dfBU['code']=='BRA']

(§) O uso de df2 = df1.copy() realiza uma cópia e não apenas pega um slice de df1. Esse procedimento evita mensagens de erro na linha seguinte, quando um campo do dataframe será alterado.

() No dataframe original existe um número maior de valores para os EUA. O corte na data especificada faz com que dfUS e dfBR tenham o mesmo tamanho.

Podemos agora plotar gráficos do número de mortes por COVID no Brasil e EUA, no mesma figura.

» cds = ColumnDataSource(data = {
»                        'dataBRA' : dfBR['dia'],
»                        'dataUSA' : dfUS['dia'],
»                        'mortesBRA' : dfBR['mortes'],
»                        'mortesUSA' : dfUS['mortes']
»                        })

» plot = figure(width=600, height=300,
»               x_axis_type = 'datetime', x_axis_label = 'data', y_axis_label = 'mortes',
»               background_fill_color='#fafaff', title='Mortes no Brasil e EUA')

» plot.circle(x = 'dataBRA', y = 'mortesBRA', source = cds, color='green' ,alpha=.2,
»             fill_color = 'yellow', fill_alpha=.3, size = 15, legend_label='EUA')


» plot.circle(x = 'dataBRA', y = 'mortesUSA', source = cds, color='blue' ,alpha=.2,
»             fill_color = 'red', fill_alpha=.3, size = 15, legend_label='EUA')

» plot.legend.location = 'top_left'

» show(plot)    # figura 13
Figura 13

Introduzimos nesse gráfico o uso de x_axis_type = 'datetime' para informar que o eixo x receberá dados de uma series temporal. plot.legend.location = 'top_left' informa a posição para as legendas.

Layouts

Layouts permitem a organização de gráficos em linhas e colunas múltiplas. Neles é possível vincular escalas de eixos entre gráficos diferentes.

Para explorar os layouts vamos usar o dataframe já montado df, que contém os campos dia, code, total, pib, idh, expVida, mortes, vacinados, descritos acima. Com ele construiremos 4 gráficos e os exibiremos em linhas, colunas e matrizes. A tabela inclui dados dos países ao longo de vários anos e, portanto, não há uma interpretação muito clara de seu significado. O objetivo é apenas o aprendizado da técnica.

» # transformando a coluna dia para um datetime
» df.loc[:,'dia'] = pd.to_datetime(df.loc[:,'dia'], format='%Y/%m/%d')

» #  a fonte de todos os gráficos é a mesma, nesse caso
» from bokeh.plotting import ColumnDataSource
» cds = ColumnDataSource(data = df)

» # gráfico 1
» plot1 = figure(width=300, height=200, x_axis_type = 'datetime',
»                x_axis_label = 'Data', y_axis_label = 'Mortes',
»                background_fill_color='#fafaff', title='Mortes no Mundo')

» plot1.dot(x = 'dia', y = 'mortes', source = cds, color='rosybrown' ,alpha=.5)

» # gráfico 2
» plot2 = figure(width=300, height=200,
»                x_axis_label = 'Expectativa de vida', y_axis_label = 'mortes',
»                background_fill_color='#fafffa', title='Expectativa de Vida x PIB')

» plot2.dot(x = 'expVida', y = 'pib', source = cds, color='red' ,alpha=.1)

» # gráfico 3
» plot3 = figure(width=300, height=200,
»                x_axis_type = 'datetime', x_axis_label = 'data', y_axis_label = 'mortes',
»                background_fill_color='#ffefff', title='PIB x Mortes')

» plot3.dot(x = 'pib', y = 'mortes', source = cds, color='blue' ,alpha=.05)

» # gráfico 4
» plot4 = figure(width=300, height=200, x_axis_label = 'PIB', y_axis_label = 'IDH',
»                background_fill_color='#9f9fff', title='PIB x IDH no mundo')
» plot4.dot(x = 'pib', y = 'idh', source = cds, color='yellow')

No código acima construimos quatro gráficos. Abaixo exploramos as possibilidades de layouts em linha, em coluna e em matriz.

» from bokeh.layouts import row, column
» # agrupar 2 gráficos em uma linha
» linha_layout = row(plot1,plot2)
» show(linha_layout)

» coluna_layout = column(plot3,plot4)
» show(coluna_layout)


» matriz_layout = column(row(plot1,plot2), row(plot3,plot4))
» show(matriz_layout)

Uma solução também interessante consiste em apresentar todos os gráficos no mesmo espaço, usando as classes Tabs e Panel. No código abaixo criamos 3 painéis e passamos nos argumentos os gráficos já construídos. Cada painel pode conter linhas e colunas, vistas anteriormente e passados no argumento child, além de um título que será usado nas guias ou tabs. Os painéis são inseridos em um objeto Tabs e exibidos.

» # importamos as classes necessárias
» from bokeh.models.widgets import Tabs, Panel
» # criamos 3 paineis
» tab1 = Panel(child = plot1, title = 'Mortes')
» tab2 = Panel(child = row(plot2,plot3), title = 'Exp Vida, PIBxMortes')
» tab3 = Panel(child = plot4, title = 'PIB x IDH')
» # insere os paineis no objeto Tabs
» objeto_tabs = Tabs(tabs = [tab1, tab2, tab3])
» # exibe o objeto
» show(objeto_tabs)

Ao clicar em uma guia o painés correspondente é exibido. Na figura estão mostrados a 1ª guia (figura 17) e a 3ª (figura 18).

Um layout de rede (grid layout) pode reunir gráficos em uma matriz, gerando resultado similar ao mostrado na figura 16. Para isso podemos usar o seguinte código.

» from bokeh.layouts import gridplot
» # cria uma rede ou grid
» grid_layout = gridplot([plot1, plot2], [plot3, plot4])
» show(grid_layout)
» # uma figura como a figura 16 é plotada.

Ao montar o grid_layout um espaço em branco pode ser inserido com None no lugar da variável do gráfico.

Algumas vezes é importante que dois ou mais gráficos tenham a mesma escala em um ou ambos os eixos. Para isso usamos o código como o seguinte.

» # criamos plots com a mesma escala (aqui no eixo do x)
» plot2.x_range = plot1.x_range
» # criamos um layout  (aqui em linha)
linha_layout = row(plot2, plot1)
show(linha_layout)

Anotações e Widgets

Para os próximos exemplos vamos usar o aqquivo population.csv, baixado do site Our World in Data, na página sobre população mundial.

O arquivo ./dados/population.csv foi baixado no link acima.

import pandas as pd
» # Importar dados para um dataframe
» df = pd.read_csv('./dados/population.csv')    

» # as colunas têm os nomes
» df.head(0)
↳ Entity   Code   Year   Total population (Gapminder, HYDE & UN)

» # 4 colunas e 53307 linhas
» df.shape # (53307, 4)

» # renomeamos as colunas
» colunas = {'Entity':'pais',
»            'Code':'codigo',
»            'Year':'ano',
»            'Total population (Gapminder, HYDE & UN)':'populacao'}
» df = df.rename(columns=colunas)

» # as colunas agora têm os nomes
» df.head(0)
↳ pais   codigo   ano   populacao

Já vimos como colocar títulos e legendas nas gráficos. No exemplo abaixo o título e posição são ajustados como uma propriedade de plot, diferente do parâmetro usado antes. Além disso podemos marcar regiões do gráficos com cores diferentes e incluir texto explicativo para realçar algum aspecto dos dados. Para isso usamos as classes Label e LabelSet.

Para alimentar esse gráfico vamos criar 3 ColumnDataSouces diferentes: para população e ano geramos cdsUSA para os EUA, cdsBRA para o Brasil, ambos após 1750. cdsLabel é usado para inserir anotações sobre os anos de independência e abolição da escravidão para os dois países.

» cdsUSA = ColumnDataSource(data = {
»     'ano' : df[(df['codigo']=='USA')  & (df['ano'] >= 1750)]['ano'],
»     'pop' : (df[(df['codigo']=='USA')  & (df['ano'] >= 1750)]['populacao'])/1e6,
» })
» cdsBRA = ColumnDataSource(data = {
»     'ano' : df[(df['codigo']=='BRA')  & (df['ano'] >= 1750)]['ano'],
»     'pop' : (df[(df['codigo']=='BRA')  & (df['ano'] >= 1750)]['populacao'])/1e6,
» })

» cdsLabel = ColumnDataSource(data=
»      dict(x=[1776, 1800, 1882, 1888],  y=[50, 100, 200, 260],
»           nota=['Indep. EUA (1776)', 'Abol. EUA (1857)',
»           'Indep. BR (1882)', 'Abol. BR (1888)']))

Agora estamos prontos para plotar esses dados. As únicas importações novas são das classes Label, LabelSet. Os dois gráficos de barra abaixo recebem os campos ano e pop, cada um relativo a um dos países.

» from bokeh.io import output_file, show, output_notebook
» from bokeh.plotting import figure
» from bokeh.plotting import ColumnDataSource
» from bokeh.models import Label, LabelSet

» output_notebook()

» grafico = figure(plot_width=600, plot_height=300, x_axis_label = 'ano',
                   y_axis_label = 'População (em milhões)')
» grafico.title.text = 'População do Brasil e do EUA de 1800 até o presente'
» grafico.title_location = 'above'

» grafico.vbar(x = 'ano', top = 'pop', source=cdsUSA,
               color = 'red', width= .1, legend_label = 'EUA')
» grafico.vbar(x = 'ano', top = 'pop', source=cdsBRA,
               color = 'green', width= 1, legend_label = 'Brasil')

» labels = LabelSet(x='x', y='y', text='nota', x_offset=0,
                    y_offset=0, source=cdsLabel, render_mode='canvas')

» texto = Label(x=1750, y=150, render_mode='css',
»               text='Independência e Abolição', text_color='blue',
»               border_line_color='#a0a0f0', border_line_alpha=1.0,
»               background_fill_color='linen', background_fill_alpha=1.0)

» grafico.add_layout(labels)
» grafico.add_layout(texto)
» grafico.legend.location = 'top_left'
» show(grafico)

Os objetos Label, LabelSet são criados com seus respectivos atributos e depois inseridos no grafico.

Usando mapas de cor

Para atribuir cores para uma categoria de dados, separando visualmente a informação para cada categoria, podemos atribuir uma cor a cada uma delas usando CategoricalColorMapper. Nele associamos a uma lista de fatores (factors ou dados categóricos) com uma lista de cores (em palette).

No exemplo inicializamos a variável mapaDeCor como um CategoricalColorMapper atribuindo os parâmetros factors e palette aos nomes das categorias e uma lista de cores. A associação é feita através do parâmetro transform no scatter plot. Novamente dois plots são traçados para efeito estético.

» from bokeh.io import output_notebook, show
» from bokeh.plotting import figure, CategoricalColorMapper
» from bokeh.models import ColumnDataSource, Range1d
» output_notebook()

» cor = ['salmon','gold','firebrick','plum','powderblue','teal','wheat','red']
» nome = ['Otto', 'Ana', 'Joana', 'Jorge', 'Marco', 'Agildo','Lu','Zana']
» dicio= dict(nome=nome,
»             altura=[1.70, 1.65, 1.48, 1.88, 1.58, 1.62, 1.83, 1.91],
»             peso=[97, 65, 89, 76, 67, 74,65, 94]
»            )
» mapaDeCor = CategoricalColorMapper(factors=nome, palette=cor)

» cds = ColumnDataSource(data=dicio)

» p = figure(title='Alunos: distribuição peso x altura',
»            x_range=Range1d(60, 110), y_range=Range1d(1.2, 2.2),
»            plot_width=400, plot_height=250)

» p.scatter(x='peso', y='altura', size=20, source=cds,
»           color=dict(field='nome', transform=mapaDeCor), alpha=.2)
» p.scatter(x='peso', y='altura', size=10, source=cds,
»           color=dict(field='nome', transform=mapaDeCor))
» p.xaxis[0].axis_label = 'Peso (kgs)'
» p.yaxis[0].axis_label = 'Altura (metros)'

» labels = LabelSet(x='peso', y='altura', text='nome',
                    x_offset=0, y_offset=8, source=cds)

» p.add_layout(labels)
» show(p)

Bibliografia

  • Jolly, Kevin: Hands-On Data Visualization with Bokeh, Interactive web plotting for Python using Bokeh, 2018 Packt Publishing, Mumbay.
  • Site Bokeh: Documentation, acessado em agosto de 2021.
  • Site Bokeh: First Steps, acessado em agosto de 2021.
  • Site Our World in Data, contendo grande variedade de tabelas com dados sobre vários temas, do mundo.
  • Rodés-Guirao, Lucas: COVID-19 Dataset by Our World in Data no Github. Acessado em outubro de 2021.