Pandas é uma biblioteca para a linguagem Python para análise de dados, contendo métodos estatísticos e facilitando a manipulação de dados (https://pandas.pydata.org/). Já o PyDAL é uma camada de abstração de banco de dados, que permite acessar e manipular informações em tipos distintos de banco de dados utilizando puramente a linguagem Python.
O projeto final está disponível no meu github: https://github.com/duducosmos/ppmvc
A vantagem de utilizar uma camada de abstração de banco de dados está na portabilidade e separação completa entre código e gerenciador de banco de dados. Assim, se no futuro for mais conveniente utilizar um MongoDB no lugar de um Postgresql ou um MySQL, não será necessário reimplementar o código, bastando apenas realizar a migração das informações de um tipo de banco de dados para outro e passando o endereço para o novo banco para o PyDAL.
Originalmente, o PyDAl nasce como parte do framework web2py, posteriormente ele passou a ser disponibilizado como um framework específico.
Além do Pandas e PyDAL vamos utilizar o BeautifulSoup, para fazer scraping de dados da web, o Numpy, requests e requests_cache.
Para instalar as bibliotecas necessárias usamos o comando:
pip install pandas pydal numpy matplotlib beautifulsoup4 requests requests_cache
Não queremos “travar” o banco de dados ao utilizá-lo em nosso código. A primeira boa prática para esse caso é garantir que ao executar um determinado código iremos criar uma conexão única ao banco durante todo o processo de execução de uma determinada tarefa. Uma forma simples de fazer isso é criar um Singleton.
Singleton é um padrão de projeto que garante que teremos apenas um único objeto executando em memória e que poderá ser executado em diversas partes do código. Podemos dizer que é uma espécie de objeto global, como variáveis globais. A maneira mais simples de criar um Singleton em Python é escrever uma variável ou instanciar um objeto dentro de um módulo e realizar a importação desse elemento ao longo de outros módulos.
Para o banco de dados, vamos criar um arquivo chamado model.py, no qual iremos instanciar o banco de dados (BD) e criar as devidas tabelas.
from pydal import DAL, Field
def model(dbinfo="sqlite://storage.sqlite", dbfolder="./dados"):
db = DAL(dbinfo, folder=dbfolder, pool_size=1)
table(db)
return db
def table(db):
db.define_table("populacao_total",
Field("uf", type="string"),
Field("populacao", type="double")
)
db.define_table("uf_nome",
Field("uf", type="string"),
Field("nome", type="string")
)
DB = model()
No código anterior criamos duas funções, uma para o nosso modelo, que irá realizar a conexão com o banco de dados e outra para a definição de tabelas.
Ao final, criamos o objeto DB que representa a instância do BD.
A função model recebe como parâmetro o termo que indica as informações de conexão como o banco de dados. Nesse caso particular vamos utilizar o SQLite. O parâmetro ‘dbfolder’ indica qual pasta o PyDAL deve utilizar para armazenar as informações sobre criação das tabelas, arquivo de transição de escrita no banco de dados e no caso do SQLite, essa será a pasta para armazenar o próprio banco de dados.
Na função tabela criamos uma tabela chamada ‘populacao_total’ a qual contém os campos ‘uf’, que representa o nome do estado, e outro campo chamado ‘populacao’. A outra tabela terá a conversão de sigla para nome completo do estado.
Note que aqui estamos nos inspirando no padrão de projeto MVC (Model, View, Controller – Modelo, Visão, Controlador). Em ciência e engenharia de dados podemos usar o PyDAL para representar a abstração do banco de dados (Model). Já a criação de modelos de aprendizagem de máquina ou tratamento dos dados iremos criar módulos com funcionalidades específicas para esse tratamento, o que irão representar os nossos controladores (Controllers. Por fim, podemos utilizar o jupyter notebook como ferramenta para visualizar os resultados, ou as nossas visões (Views)
O próximo passo será o de preencher o nosso banco de dados.
Vamos criar um arquivo chamado get_pop_from_wik.py. Esse arquivo terá funções para coletar dados de uma tabela na wikipédia, a qual contém os dados de população por estado, disponível no link:
https://pt.wikipedia.org/wiki/Lista_de_unidades_federativas_do_Brasil_por_popula%C3%A7%C3%A3o
#!/usr/bin/env python
# -*- Coding: UTF-8 -*-
import pandas as pd
import requests
import requests_cache
from bs4 import BeautifulSoup
from numpy import array
from model import DB as db
requests_cache.install_cache('wikipedia_cache')
def coletar_limpar_dados():
url = "https://pt.wikipedia.org/wiki/Lista_de_unidades_federativas_do_Brasil_por_popula%C3%A7%C3%A3o"
header = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.75 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
r = requests.get(url, headers=header)
soup = BeautifulSoup(r.text, 'html.parser')
tables = soup.find('table')
table = tables.get_text().split("\n")
table = [ti for ti in table if ti != ""][6:]
table = array([table[i: i + 5] for i in range(0, len(table), 5)])
estados = array([ti.replace("\xa0", "") for ti in table[:, 1]])
populacao = {estados[i]: int(table[:, 2][i].replace(" ", "")) for i in range(table[:, 2].size)}
return estados, populacao
No código anterior usamos o requests para acessar os dados na wikipedia. O dicionários representado pela variável “header” é utilizado para “simular” um navegador ao acessar um determinado site. Em alguns casos muitos sites reconhecem que essas informações não estão presentes ao serem acessados e acabam por negar o acesso a informação. Com isso conseguimos ter uma maior garantia de acesso a informação.
Em seguida, foi utilizada a biblioteca BeautifulSoup para realizar o parser do HTML. A tabela que desejamos é justamente a primeira tabela, por isso pudemos utilizar o método find(‘table’), Na linha 26. Caso queria pegar todas as tabelas basta utilizar o método findAll(‘table’).
Existem casos em que a tabela está bem formatada e os dados presentes estão em formatos bem adequados. Quando isso ocorre, podemos utilizar o método do pandas chamado read_html e gerar a estrutura de dados adequada. O que não ocorreu nesse caso.
Aqui foi mais fácil identificar a tabela, obter apenas o texto plano, com o método get_text() que aparece na linha 28. Nessa mesma linha o texto plano é convertido em uma lista em que cada nova linha representa um novo elemento da lista. Depois foi realizada uma filtragem para remover caracteres vazios da lista. Essa etapa acaba sendo um processo empírico, sendo necessário avaliar qual a melhor forma de limpar os dados.
Ao final é realizada uma última limpeza nos dados e separação das informações de acordo como o necessário para a próxima etapa.
Esse passo foi o de Engenharia de dados, em que procuramos entender a fonte de informação e realizar a limpeza e tratamento prévio.
No código abaixo é apresentada a função para injeção das informações no nosso banco.
def injetar_dados():
estados, populacao = coletar_limpar_dados()
UFS = {'São Paulo': "SP", 'Minas Gerais': "MG",
'Rio de Janeiro': 'RJ',
'Bahia': 'BA', 'Paraná': 'PA',
'Rio Grande do Sul': 'RS', 'Pernambuco': 'PE',
'Ceará': 'CE', 'Pará': 'PR',
'Santa Catarina': 'SC', 'Goiás': 'GO',
'Maranhão': 'MA', 'Amazonas': 'AM',
'Espírito Santo': 'ES', 'Paraíba': 'PB',
'Rio Grande do Norte': 'RN', 'Mato Grosso': 'MT',
'Alagoas': 'AL', 'Piauí': 'PI',
'Distrito Federal': 'DF', 'Mato Grosso do Sul': 'MS',
'Sergipe': 'SE', 'Rondônia': 'RO', 'Tocantins': 'TO',
'Acre': 'AC', 'Amapá': "AP", 'Roraima': "RR"}
for es in estados:
db.populacao_total.update_or_insert(
(db.populacao_total.uf == UFS[es]),
uf=UFS[es],
populacao=populacao[es]
)
db.uf_nome.update_or_insert(
(db.uf_nome.uf == UFS[es]),
uf=UFS[es],
nome=es
)
db.commit()
Aqui criamos um dicionário para mapear o nome dos estados em termos de siglas, as quais serão armazenadas no nosso banco. Nesse exemplo a injeção da informação no banco de dados foi utilizando o método ‘update_or_insert’.
O primeiro parâmetro desse método indica qual a condição que deve ser avaliada para realizar a injeção ou a atualização no banco de dados. Caso o sistema encontre algo que satisfaça a condição, então só será realizada a atualização dos dados, do contrário uma nova linha será adicionada a tabela.
Ao final iremos executar a função de injeção das informações no nosso banco:
if __name__ == "__main__":
injetar_dados()
Abaixo está o código completo do módulo de limpeza e injeção dos dados no BD.
#!/usr/bin/env python
# -*- Coding: UTF-8 -*-
import pandas as pd
import requests
import requests_cache
from bs4 import BeautifulSoup
from numpy import array
from model import DB as db
requests_cache.install_cache('wikipedia_cache')
def coletar_limpar_dados():
url = "https://pt.wikipedia.org/wiki/Lista_de_unidades_federativas_do_Brasil_por_popula%C3%A7%C3%A3o"
header = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.75 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
r = requests.get(url, headers=header)
soup = BeautifulSoup(r.text, 'html.parser')
tables = soup.find('table')
table = tables.get_text().split("\n")
table = [ti for ti in table if ti != ""][6:]
table = array([table[i: i + 5] for i in range(0, len(table), 5)])
estados = array([ti.replace("\xa0", "") for ti in table[:, 1]])
populacao = {estados[i]: int(table[:, 2][i].replace(" ", ""))
for i in range(table[:, 2].size)}
return estados, populacao
def injetar_dados():
estados, populacao = coletar_limpar_dados()
UFS = {'São Paulo': "SP", 'Minas Gerais': "MG",
'Rio de Janeiro': 'RJ',
'Bahia': 'BA', 'Paraná': 'PA',
'Rio Grande do Sul': 'RS', 'Pernambuco': 'PE',
'Ceará': 'CE', 'Pará': 'PR',
'Santa Catarina': 'SC', 'Goiás': 'GO',
'Maranhão': 'MA', 'Amazonas': 'AM',
'Espírito Santo': 'ES', 'Paraíba': 'PB',
'Rio Grande do Norte': 'RN', 'Mato Grosso': 'MT',
'Alagoas': 'AL', 'Piauí': 'PI',
'Distrito Federal': 'DF', 'Mato Grosso do Sul': 'MS',
'Sergipe': 'SE', 'Rondônia': 'RO', 'Tocantins': 'TO',
'Acre': 'AC', 'Amapá': "AP", 'Roraima': "RR"}
for es in estados:
db.populacao_total.update_or_insert(
(db.populacao_total.uf == UFS[es]),
uf=UFS[es],
populacao=populacao[es]
)
db.uf_nome.update_or_insert(
(db.uf_nome.uf == UFS[es]),
uf=UFS[es],
nome=es
)
db.commit()
if __name__ == "__main__":
injetar_dados()
Agora iremos criar o Notebook para visualizar e compreender as informações coletadas.
Abaixo está o notebook para coletar os dados do BD e converter em um DataFrame do Pandas.
%matplotlib inline
import pandas as pd
import matplotlib.pyplot as plt
from model import DB as db
plt.rcParams["figure.figsize"] = [10, 10]
populacao = db().select(db.populacao_total.uf, db.populacao_total.populacao).as_list()
populacao = pd.DataFrame.from_dict(data=populacao)
populacao.index = populacao.uf
populacao = populacao['populacao']
populacao.plot.pie(subplots=True, y='populacao', title="Populacao total por Estado.",legend=False)
plt.show()