Cleuton Sampaio, DataLearningHub Nesta lição veremos como fornecer visualizações com mais de duas dimensões de dados.
Em casos que temos três características mensuráveis e, principalmente, plotáveis (dentro da mesma escala - ou podemos ajustar a escala), é interessante ver um gráfico de dispersão para podermos avaliar visualmente a distribuição das amostras. É o que veremos com a bilbioteca Matplotlib Toolkits, em especial a MPlot3D, que tem o objeto Axes3D para geração de gráficos tridimensionais.
In [3]:
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d, Axes3D # Objetos que usaremos em nosso gráfico
%matplotlib inline
df = pd.read_csv('../datasets/evasao.csv') # Dados de evasão escolar que coletei
df.head()
Out[3]:
Algumas explicações. Para começar, vejamos as colunas deste dataset:
Para podermos plotar um gráfico, precisamos reduzir a quantidade de dimensões, ou seja, as características. Farei isso da maneira mais "naive" possível, selecionando três características que mais influenciaram no resultado final, ou seja o abandono do aluno (Churn).
In [4]:
df2 = df[['periodo','repetiu','desempenho']][df.abandonou == 1]
df2.head()
Out[4]:
In [5]:
fig = plt.figure()
#ax = fig.add_subplot(111, projection='3d')
ax = Axes3D(fig) # Para Matplotlib 0.99
ax.scatter(xs=df2['periodo'],ys=df2['repetiu'],zs=df2['desempenho'], c='r',s=8)
ax.set_xlabel('periodo')
ax.set_ylabel('repetiu')
ax.set_zlabel('desempenho')
plt.show()
Simplesmente usei o Axes3D para obter um objeto gráfico tridimensional. O método "scatter" recebe três dimensões (xs, ys e zs), cada uma atribuída a uma das colunas do novo dataframe. O parâmetro "c" é a cor e o "s" é o tamanho de cada ponto. Informei os rótulos de cada eixo e pronto! Temos um gráfico 3D mostrando a distribuição espacial dos abandonos de curso, com relação às três variáveis.
Podemos avaliar muito melhor a tendência de dados, se olharmos em visualizações 3D. Vejamos um exemplo sintético. Vamos gerar alguns valores 3D:
In [6]:
import numpy as np
np.random.seed(42)
X = np.linspace(1.5,3.0,num=100)
Y = np.array([x**4 + (np.random.rand()*6.5) for x in X])
Z = np.array([(X[i]*Y[i]) + (np.random.rand()*3.2) for i in range(0,100)])
Primeiramente veremos como ficaria isso em visualização 2D:
In [7]:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(X, Y, c='b', s=20)
ax.set_xlabel('X')
ax.set_ylabel('Y')
plt.show()
Ok... Nada demais... Uma correlação não linear positiva, certo? Mas agora, vejamos isso com a matriz Z incluída:
In [8]:
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(X, Y, Z, c='r',s=8)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.show()
E isso fica mais interessante quando sobrepomos uma predição sobre os dados reais. Vamos usar um Decision Tree Regressor para criar um modelo preditivo para estes dados:
In [9]:
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split
features = pd.DataFrame({'X':X, 'Z':Z})
labels = pd.DataFrame({'Y':Y})
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.33, random_state=42)
dtr3d = DecisionTreeRegressor(max_depth=4, random_state=42)
dtr3d.fit(X_train,y_train)
print('R2',dtr3d.score(X_train,y_train))
In [10]:
yhat3d = dtr3d.predict(X_test)
fig = plt.figure()
ax = ax = fig.add_subplot(111, projection='3d')
ax.scatter(X, Y, Z, c='r',s=8)
ax.scatter(X_test['X'], yhat3d, X_test['Z'], c='k', marker='*',s=100)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.show()
Plotamos as predições usando marker do tipo estrela. Ficou bem interessante, não?
As vezes queremos demonstrar informações com mais de 3 dimensões, mas como fazer isso? Vamos supor que queiramos também incluir o percentual de bolsa como uma variável em nosso exemplo de evasão escolar. Como faríamos? Uma abordagem possível seria manipular os markers para que representem a bolsa. Podemos usar cores, por exemplo. Vejamos, primeiramente, precisamos saber quais faixas de bolsa existem no dataset:
In [11]:
print(df.groupby("bolsa").count())
Podemos criar uma tabela de cores, indexada pelo percentual de bolsa:
In [12]:
from decimal import Decimal
bolsas = {0.00: 'b',0.05: 'r', 0.10: 'g', 0.15: 'm', 0.20: 'y', 0.25: 'k'}
df['cor'] = [bolsas[float(round(Decimal(codigo),2))] for codigo in df['bolsa']]
df.head()
Out[12]:
Essa "maracutaia" merece uma explicação. Criei um dicionário indexado pelo valor da bolsa. Assim, pegamos o código da cor correspondente. Só que preciso incluir uma coluna no dataframe com esse valor, de modo a usar no gráfico. Só tem um problema: O dataset original está "sujo" (algo que acontece frequentemente) e o percentual 0.15 está como 0.1500000002. Posso retirar isso convertendo o falor de "float" para "Decimal", arredondanto e convertendo novamente em float.
Quando plotarmos, vamos procurar a cor no dicionário:
In [13]:
fig = plt.figure()
#ax = fig.add_subplot(111, projection='3d')
ax = Axes3D(fig) # Para Matplotlib 0.99
ax.scatter(xs=df['periodo'],ys=df['repetiu'],zs=df['desempenho'], c=df['cor'],s=50)
ax.set_xlabel('periodo')
ax.set_ylabel('repetiu')
ax.set_zlabel('desempenho')
plt.show()
Pronto! Temos ai a cor da bola dando a quarta dimensão: O percentual de bolsa
Vemos que já uma concentração de alunos com bolsa de 25% (cor preta) com poucas repetições, mas baixo desempenho, em todos os períodos.
Assim como mexemos com a cor, podemos mexer com o tamanho, criando algo como um "mapa de calor". Vamos transformar essa visão em 2D, colocando o "desempenho" com tamanho diferenciado.
In [14]:
fig, ax = plt.subplots()
ax.scatter(df['periodo'],df['repetiu'], c='r',s=df['desempenho']*30)
ax.set_xlabel('periodo')
ax.set_ylabel('repetiu')
plt.show()
Isso nos mostra um fato curioso. Temos alunos com bom desempenho (bolas grandes) em todos os períodos, sem repetir nenhuma disciplina, que abandonaram. O que os teria feito fazer isto? Talvez sejam condições financeiras, ou insatisfação com o curso. Um fato a ser investigado, que só foi revelado graças a esta visualização.
Muitas vezes temos datasets com informações geográficas e precisamos plotar os dados sobre um mapa. Vou mostrar aqui como fazer isso com um exemplo do dataset dos casos de Dengue de 2018 no Rio de Janeiro. Fonte: Data Rio: http://www.data.rio/datasets/fb9ede8d588f45b48b985e62c817f062_0
Eu criei um dataset georreferenciado, que está na pasta desta demonstração. Ele está em formato CSV, separado por ponto e vírgula, com separador decimal em português (vírgula):
In [15]:
df_dengue = pd.read_csv('./dengue2018.csv',decimal=',', sep=';')
df_dengue.head()
Out[15]:
Um simples gráfico de dispersão já dá uma boa noção do problema:
In [16]:
fig, ax = plt.subplots()
ax.scatter(df_dengue['longitude'],df_dengue['latitude'], c='r',s=15)
plt.show()
Podemos colocar o tamanho do ponto proporcional à quantidade de casos, aumentando a dimensão das informações:
In [17]:
fig, ax = plt.subplots()
ax.scatter(df_dengue['longitude'],df_dengue['latitude'], c='r',s=5+df_dengue['quantidade'])
plt.show()
Podemos manipular a cor e intensidade para criar um "mapa de calor" da Dengue:
In [18]:
def calcular_cor(valor):
cor = 'r'
if valor <= 10:
cor = '#ffff00'
elif valor <= 30:
cor = '#ffbf00'
elif valor <= 50:
cor = '#ff8000'
return cor
df_dengue['cor'] = [calcular_cor(codigo) for codigo in df_dengue['quantidade']]
In [19]:
df_dengue.head()
Out[19]:
E vamos ordenar para que as maiores quantidades fiquem por último:
In [20]:
dfs = df_dengue.sort_values(['quantidade'])
dfs.head()
Out[20]:
In [21]:
fig, ax = plt.subplots()
ax.scatter(dfs['longitude'],dfs['latitude'], c=dfs['cor'],s=10+dfs['quantidade'])
plt.show()
Pronto! Um mapa de calor da Dengue em 2018. Mas está faltando algo certo? Cadê o mapa do Rio de Janeiro? Muita gente usa o geopandas e baixa arquivos de mapas. Eu prefiro usar o Google Maps. Ele tem uma API chamada Static Maps que permite baixar mapas. Primeiramente, vou instalar o requests:
In [69]:
!pip install requests
Agora, vem uma parte um pouco mais "esperta". Eu tenho as coordenadas do centro do Rio de Janeiro (centro geográfico, não o centro da cidade). Vou montar um request à API Static Map para baixar um mapa. Veja bem, você tem que cadastrar uma API Key para usar esta API. Eu omiti a minha propositalmente. Aqui você tem as instruções para isto: https://developers.google.com/maps/documentation/maps-static/get-api-key
In [22]:
import requests
latitude = -22.9137528
longitude = -43.526409
zoom = 10
size = 800
scale = 1
apikey = "**INFORME SUA API KEY**"
gmapas = "https://maps.googleapis.com/maps/api/staticmap?center=" + str(latitude) + "," + str(longitude) + \
"&zoom=" + str(zoom) + \
"&scale=" + str(scale) + \
"&size=" + str(size) + "x" + str(size) + "&key=" + apikey
with open('mapa.jpg', 'wb') as handle:
response = requests.get(gmapas, stream=True)
if not response.ok:
print(response)
for block in response.iter_content(1024):
if not block:
break
handle.write(block)
Bom, o mapa foi salvo, agora eu preciso saber as coordenadas dos limites. A API do Google só permite que você informe o centro (latitude e longitude) e as dimensões da imagem em pixels. Mas, para ajustar o mapa às coordenadas em latitudes e longitudes, é preciso saber as coordenadas do retângulo da imagem. Há vários exemplos de como calcular isso e eu uso um exemplo Javascript que converti para Python há algum tempo. Este cálculo é baseado no script de: https://jsfiddle.net/1wy1mm7L/6/
In [23]:
import math
_C = { 'x': 128, 'y': 128 };
_J = 256 / 360;
_L = 256 / (2 * math.pi);
def tb(a):
return 180 * a / math.pi
def sb(a):
return a * math.pi / 180
def bounds(a, b, c):
if b != None:
a = max(a,b)
if c != None:
a = min(a,c)
return a
def latlonToPt(ll):
a = bounds(math.sin(sb(ll[0])), -(1 - 1E-15), 1 - 1E-15);
return {'x': _C['x'] + ll[1] * _J,'y': _C['y'] + 0.5 * math.log((1 + a) / (1 - a)) * - _L}
def ptToLatlon(pt):
return [tb(2 * math.atan(math.exp((pt['y'] - _C['y']) / -_L)) - math.pi / 2),(pt['x'] - _C['x']) / _J]
def calculateBbox(ll, zoom, sizeX, sizeY, scale):
cp = latlonToPt(ll)
pixelSize = math.pow(2, -(zoom + 1));
pwX = sizeX*pixelSize;
pwY = sizeY*pixelSize;
return {'ne': ptToLatlon({'x': cp['x'] + pwX, 'y': cp['y'] - pwY}),'sw': ptToLatlon({'x': cp['x'] - pwX, 'y': cp['y'] + pwY})}
limites = calculateBbox([latitude,longitude],zoom, size, size, scale)
print(limites)
A função "calculateBbox" retorna um dicionário contendo os pontos Nordeste e Sudoeste, com a latitude e longitude de cada um. Para usar isso no matplotlib, eu preciso usar o método imshow, só que eu preciso informar a escala, ou seja, qual é o intervalo de latitudes (vertical) e longitudes (horizontal) que o mapa representa. Assim, a plotagem de pontos ficará correta. Eu vou usar a biblioteca mpimg para ler o arquivo de imagem que acabei de baixar. Só que a função imshow usa as coordenadas no atributo extent na ordem: ESQUERDA, DIREITA, BAIXO, TOPO. Temos que organizar a passagem dos parâmetros para ela.
In [48]:
import matplotlib.image as mpimg
fig, ax = plt.subplots(figsize=(10, 10))
rio_mapa=mpimg.imread('./mapa.jpg')
plt.imshow(rio_mapa, extent=[limites['sw'][1],limites['ne'][1],limites['sw'][0],limites['ne'][0]], alpha=1.0)
ax.scatter(dfs['longitude'],dfs['latitude'], c=dfs['cor'],s=10+dfs['quantidade'])
plt.ylabel("Latitude", fontsize=14)
plt.xlabel("Longitude", fontsize=14)
plt.show()
Pronto! Ai está! Um mapa de calor georreferenciado da Dengue em 2018 no Rio de Janeiro