<jupyter_start><jupyter_text>Introdução a Pandas Nessa aula (ou melhor, notebook ~~valeu Corona~~ ) vamos falar um pouco sobre a biblioteca Pandas, sobre como utilizá-la e como fazer as primeiras análises.    Sumário- [Extras de Jupyter](Extras-de-Jupyter)- [O que é Pandas](O-que-é-Pandas?) - [Setup Inicial](Setup-Inicial?)- [Mãos à Obra](Mãos-à-obra!)    - [Importando a Biblioteca](Importando-a-biblioteca)    - [Tipos de Estruturas](Tipos-de-estruturas)    - [Leitura de dados](Leitura-de-dados)    - [Visualização do Dataframe](Visualização-do-Dataframe)    - [Seleção dos Dados](Seleção-dos-dados)    - [Indexação booleana](Indexação-booleana)    - [Operações](Operações)    - [Apply](Apply)    - [Funções Úteis](Funções-Úteis)    - [Dados Faltantes](Dados-faltantes)    - [Plot](Plot)    - [Exportação](Exportação)- [Exercícios](Exercícios)- [Conclusão](Conclusão)- [Referências](Referências)- [Dúvidas?](Dúvidas?)  Extras de JupyterJá vimos um pouco sobre o que é e como utilizar um Jupyter Notebook, mas esta é uma ferramenta muito poderosa e interessante, então vejamos mais algumas possibilidades legais que ela nos oferece:- %%time: calcula o tempo de execução de uma célula, para utilizar essa função basta inserir *%%time* no início de uma célula.- ??: sabe quando você quer usar uma função, mas não lembra os parâmetros? É para isso que serve o *??*. Escrevendo as duas interrogações antes de uma função, ficará visível sua documentação no próprio jupyter!- ?: uma única interrogação é utilizada para obter informações sobre uma variável.Os comandos acima podem ser usados assim:<jupyter_code># informações sobre a função range, que vamos usar logo mais
??range
%%time
# calculando o tempo de execução do laço de repetição que utiliza o range e altera a variável soma

soma = 0
for i in range(1000):
    soma += i
# informações sobre a variável soma
?soma<jupyter_output><empty_output><jupyter_text>O que é Pandas?Pandas é uma biblioteca Python. Ela permite que façamos uma série de manipulações e análises de dados de forma simples e eficiente.  Setup InicialAssim como boa parte das bibliotecas, é preciso instalar Pandas no seu computador. Isso vai depender um pouco do seu Sistema Operacional, por isso recomendo seguirem os [tutoriais de instalação](https://pandas.pydata.org/pandas-docs/stable/getting_started/install.html).  Além disso, para que esse notebook funcione adequadamente é preciso que seja feito o download e a extração desse [aquivo](https://www.kaggle.com/sudalairajkumar/novel-corona-virus-2019-dataset)  e que ele se encontre na mesma pasta do arquivo desse notebook.  Mãos à obra!  Importando a bibliotecaO primeiro passo é importar a biblioteca. Para isso, utilizamos o comando abaixo.<jupyter_code>import pandas as pd<jupyter_output><empty_output><jupyter_text>Tipos de estruturasExistem basicamente dois tipos de estruturas de dados em Pandas: as Series e os Dataframes. Uma **Serie** é como uma lista, ou um array unidimensional. Ela é capaz de armazenar diversos tipos de dados (inteiros, strings, floats, objetos....), mas é preciso que os dados de uma Serie sejam todos de um mesmo tipo, ao contrário de uma lista normal que permite misturas. Podemos criar uma serie passando uma lista de valores para a função que a gera:<jupyter_code>quantidades = pd.Series([10, 9, 9, 4, 5, 7])

quantidades<jupyter_output><empty_output><jupyter_text>Falamos acima que uma Serie é um array unidimensional, então por que vemos duas colunas ao imprimir Notas?Bom, isso acontece pois a Serie possui uma coluna de Index, ou seja, uma coluna de rótulos para os seus elementos. Dessa forma, podemos definir nossos próprios indices para uma Serie:<jupyter_code>frutas_serie = pd.Series(data = [10, 9, 9, 4, 5, 7], 
                        index = ['Maçã', 'Banana', 'Pera', 'Uva', 'Mamão', 'Laranja'])

frutas_serie<jupyter_output><empty_output><jupyter_text>Para facilitar a criação de Series com idices, podemos utilizar dicionários (vistos na [aula 0](https://github.com/icmc-data/Intro-DS-2020.1/blob/master/Aula0/Jupyter%20e%20Python.ipynb)) da seguinte forma:<jupyter_code>dict_frutas = {'Maçã' : 10, 'Banana' : 9, 'Pera' : 9, 'Uva' : 4, 'Mamão' : 5, 'Laranja' : 7}
frutas_serie = pd.Series(dict_frutas)

frutas_serie<jupyter_output><empty_output><jupyter_text>Já um **Dataframe** é mais próximo de uma planilha ou uma tabela, ou seja, uma estrutura **multi**dimensional de dados. Podemos pensar num Dataframe como um conjunto de Series, onde cada uma das colunas da tabela é uma Serie. Cada linha da planilha será um *Exemplo* e terá diversas informações, as quais serão nomeadas nas colunas, e esses são os nossos *Atributos* ou *Features*. Assim como para as Series, podemos criar um Dataframe de diversas maneiras. Por exemplo utilizando um dicionário:<jupyter_code>dict_frutas = {'Nome': ['Maçã', 'Banana', 'Pera', 'Uva', 'Mamão', 'Laranja'], 
               'Quantidade': [10, 9, 9, 4, 5, 7], 
               'Preço': [1, 0.5, 9, 4.99, 2.50, 1.99],
               'Fornecedor': ['Beto', 'Ze', 'Chico', 'Tião', 'Jão', 'Marcão']}

frutas_df = pd.DataFrame(dict_frutas)
frutas_df<jupyter_output><empty_output><jupyter_text>No exemplo acima, temos quatro features (Nome, Quantidade, Preço e Fornecedor) e seis exemplos. Cada exemplo tem um valor para cada uma das suas features.  Essa é a ideia do Dataframe: linhas de dados separados em colunas.  Leitura de dadosEm geral os nossos dados são armazenados em arquivos. Precisamos, assim, que as informações sejam lidas do arquivo para que possamos tratá-lo como um Dataframe e utilizar seus métodos.Existem diferentes funções para ler os arquivos, a depender do seu formato. Por exemplo:- [read_csv](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html): lê arquivos no formato csv (comma-separated values)- [read_excel](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_excel.html): lê planilhas do excel (é preciso instalar uma [extensão](https://pypi.org/project/xlrd/) para utilizá-la)- [read_htlm](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_html.html): lê tabelas direto de um siteCada uma dessas funções tem diversos parâmetros que serão mais explorados conforme formos utilizando a biblioteca.O que todos têm em como é que devemos informar o nome do arquivo jutamente com caminho completo para alcançá-lo.  Vamos utilizar, no exemplo, a função para ler arquivos no formato csv. Esse tipo de arquivo tem seus valores separados por vírgula, ponto-e-vírgula ou outro caracter. Alguns dos parâmetros relevantes são:- filepath: o caminho para o arquivo (único parâmetro obrigatório)- sep: caracter utilizado como separador no arquivo, por padrão é ','- index_col = coluna que define o indiceO arquivo que utilizaremos nessa aula contém dados sobre o Coronavírus (sempre legal usar dados reais e recentes).<jupyter_code>df = pd.read_csv('covid_19_data.csv', index_col = 0)
df<jupyter_output><empty_output><jupyter_text>Visualização do DataframeMuitas vezes queremos visualizar o nosso Dataframe para obter novas informações, debugar (T.T), tirar dúvidas e muito mais. Podemos ver a dimensão do dataframe e, assim, saber quantas linhas (exemplos) e quantas colunas (features) temos. Com o atributo **shape** teremos uma tupla com a quantidade de linhas e a quantidade de colunas, respectivamente:<jupyter_code>df.shape<jupyter_output><empty_output><jupyter_text>É possível ver os indices do dataframe com o atributo **index**:<jupyter_code>df.index<jupyter_output><empty_output><jupyter_text>E os nomes das colunas (features) com o atributo **columns**:<jupyter_code>df.columns<jupyter_output><empty_output><jupyter_text>Os tipos dos dados de cada um das colunas com o **dtypes**:<jupyter_code>df.dtypes<jupyter_output><empty_output><jupyter_text>Os primeiros exemplos do dataframe com o método **head**:<jupyter_code>df.head()<jupyter_output><empty_output><jupyter_text>E os últimos exemplos do dataframe com o método **tail**:<jupyter_code>df.tail()<jupyter_output><empty_output><jupyter_text>Uma breve descrição estatística de cada uma das features numéricas com o **describe**:<jupyter_code>df.describe()<jupyter_output><empty_output><jupyter_text>Seleção dos dadosOutra funcionalidade muito útil e importante é a de selecionar e acessar parte dos dados. Podemos, por exemplo, selecionar uma feature específica. Para isso, acessamos o dataframe como se fosse um dicionário (visto na [Aula 0](https://github.com/icmc-data/Intro-DS-2020.1/blob/master/Aula0/Jupyter%20e%20Python.ipynb)), colocando entre colchetes o nome da feature que queremos.  Vimos quais são as colunas do dataframe com o comando utilizando seu atributo [columns](Visualização-do-Dataframe), então agora basta acessar qualquer uma delas.<jupyter_code>df['Province/State']
df['ObservationDate']<jupyter_output><empty_output><jupyter_text>Outra possibilidade é a de selecionar uma linha do dataframe. É possível fazê-lo utilizando comandos diferentes:- [iloc[]](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.iloc.html): acessa uma linha pela sua posição no dataframe. Se acessarmos a posição 1, teremos o segundo exemplo do dataframe.- [loc[]](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.loc.html): acessa uma linha por meio de uma *label*. Pode ser, por exemplo, uma indexação booleana (como veremos a seguir) ou o indice da linha. Se acessarmos o indice 1, teremos o primeiro exemplo do dataframe, já que esse é o indice.Abaixo vemos a execução dos dois. Note que apesar de acessarmos '1' em ambos os casos, com iloc temos o exemplo cujo Name é 2, ou seja, o segundo exemplo do dataframe, enquanto no caso do loc temos o exemplo com Name 1, ou seja, o primeiro exemplo do dataframe.<jupyter_code>df.iloc[1]
df.loc[1]<jupyter_output><empty_output><jupyter_text>É importante ressaltar que tanto *loc* quanto *iloc* são ideais para o acesso de linhas, mas não para a sua modificação. Para tanto é preciso utilizar *at* e *iat*, que se comportam de forma semelhante ao *loc* e *iloc* respectivamente.  Indexação booleanaJá vimos como selecionar linhas e colunas do nosso dataframe, mas e se quisermos selecionar apenas as linhas que cumprem certo requisito?  Para isso, utilizamos a chamada indexação booleana. Utiliza-se uma expressão que retorna uma Serie ou um Dataframe apenas com valores booleanos (True ou False), o qual é utilizado para fazer a seleção do Dataframe. Se quisermos, por exemplo, ver apenas as linhas que tem informações dos dias nos quais não houve nenhuma uma morte devido ao vírus. Primeiro geramos uma Serie com valores booleanos:<jupyter_code>df['Deaths'] == 0<jupyter_output><empty_output><jupyter_text>Em seguida utilizamos tal Serie para indexar o Dataframe:<jupyter_code>df[df['Deaths'] == 0]<jupyter_output><empty_output><jupyter_text>Também é possível que mais de uma condição seja considerada. Nesse caso utilizamos os operadores and, or e not como operadores bitwise, ou seja, como &, | e ~.  Vamos selecionar as linhas que indicam dias nos quais não houve nenhuma morte e nenhum caso confirmado:<jupyter_code>df[(df['Deaths'] == 0) & (df['Confirmed'] == 0)]<jupyter_output><empty_output><jupyter_text>Se quisermos saber quanto exemplos satisfazem essas condições, é possível utilizar os operadores (que serão vistos logo mais) nesse novo dataframe filtrado:<jupyter_code>df[(df['Deaths'] == 0) & (df['Confirmed'] == 0)].count()<jupyter_output><empty_output><jupyter_text>Selecionemos, agora, os exemplos a respeito dos Estados Unidos ou no Brasil.<jupyter_code>df[(df['Country/Region'] == 'Brazil') | (df['Country/Region'] == 'US')]<jupyter_output><empty_output><jupyter_text>OperaçõesPodemos fazer diversas operações com os nossos dados. Muitas deles já são nativas do próprio Pandas, mas tabém podemos criar as nossas próprias. Com funções nativas podemos somar todos os valores de cada uma das fetures, encontrar os valores máximo e mínimo, o desvio padrão, a média e muito mais.<jupyter_code># média
df.mean()
# desvio padrão
df.std()
# soma
df.sum()
# máximo
df.max()
# mínimo
df.min()
# contagem dos valores não nulos
df.count()<jupyter_output><empty_output><jupyter_text>Se quisermos a média de apenas uma única feature, basta selecioná-la e utilizar o método:<jupyter_code>df['Confirmed'].mean()<jupyter_output><empty_output><jupyter_text>Também é possível fazer operações com os valores das features.  No caso do dataset que estamos usando, temos a quantidade de casos confirmados e recuperados. Podemos utilizar essas informações para criar uma nova feature, a proporção entre confirmações e recuperações. Para isso basta dividir as features:<jupyter_code>df['Proportion'] = df['Recovered'] / df['Confirmed']
df.head()<jupyter_output><empty_output><jupyter_text>Podemos ver se a operação deu certo utilizando uma indexação booleana:<jupyter_code>df[df['Proportion'] > 0]<jupyter_output><empty_output><jupyter_text>ApplyPode ser necessário aplicar uma operação que não está definida na biblioteca aos exemplos. Se quisermos, por exemplo, iterar sobre todas as linhas e deixar os nomes de todos os países com letras minúsculas.Poderíamos pensar em utilizar uma estrutura de repetição para criar essa nova feature, iterando sobre cada uma das linhas do dataframe. Essa solução não está errada.<jupyter_code>%%time

for i in range(df.shape[0]):
    new_value = df.iloc[i]['Country/Region'].lower()
    df['Country/Region'].iat[i] = new_value<jupyter_output>CPU times: user 3.74 s, sys: 13.4 ms, total: 3.75 s
Wall time: 3.8 s<jupyter_text>Porém existe uma forma mais elegante e eficiente de fazer tal operação: usando o método **apply**. Com ele podemos aplicar uma função a cada uma das linhas do dataframe:<jupyter_code>%%time

df['Country/Region'].apply(lambda x : x.lower())<jupyter_output>CPU times: user 12 ms, sys: 0 ns, total: 12 ms
Wall time: 11.7 ms<jupyter_text>Funções ÚteisExistem diversas funções que auxiliam a análise dos dados. Vamos ver algumas delas: O método **info** dá informações sobre o Dataframe incluindo os tipos do index e das colunas, valores não nulos e uso de memória:<jupyter_code>df.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 11614 entries, 1 to 11614
Data columns (total 8 columns):
ObservationDate    11614 non-null object
Province/State     6128 non-null object
Country/Region     11614 non-null object
Last Update        11614 non-null object
Confirmed          11614 non-null float64
Deaths             11614 non-null float64
Recovered          11614 non-null float64
Proportion         11397 non-null float64
dtypes: float64(4), object(4)
memory usage: 1.1+ MB<jupyter_text>Já vimos antes, mas temos a função **describe**, que nos dá um breve resumo estatístico sobre as features do dataframe:<jupyter_code>df.describe()<jupyter_output><empty_output><jupyter_text>Também é possível ver os valores únicos que uma feature pode assumir utilizando o **unique**:<jupyter_code>df['Country/Region'].unique()
df['ObservationDate'].unique()<jupyter_output><empty_output><jupyter_text>Podemos contar, para cada valor único da feature, quantos exemplo são desse tipo com o método **value_counts**, isto é, gerar um histograma:<jupyter_code>df['Country/Region'].value_counts()<jupyter_output><empty_output><jupyter_text>Utilizando o value_counts, podemos selecionar os valores mais frequentes. Se quisermos, por exemplo, ver quais são os 10 países mais frequentes no Dataframe:<jupyter_code>df['Country/Region'].value_counts()[:10]<jupyter_output><empty_output><jupyter_text>Dados faltantesEm geral, quando utilizamos tabelas reais, temos muitos valores faltantes. Isso significa que, para alguns exemplos, não temos informações para algumas de suas features.  Como cientistas de dados, é importante que saibamos encontrar tais dados e lidar com eles. É possível ver, para cada exemplo, quais são as features que tem valores faltantes:<jupyter_code>df.isna()<jupyter_output><empty_output><jupyter_text>Podemos somar as colunas que indicam se o dado é faltante ou não, o que nos dá uma contagem desses valores.<jupyter_code>df.isna().sum()<jupyter_output><empty_output><jupyter_text>É possível lidar com esses valores nulos de diversas formas. Uma delas é apenas ignorar os exemplos que possuem valores nulos, removendo-os do dataframe. Essa função é não é o que chamados de *inplace*, ou seja, as alterações que ela faz são retornadas como uma cópia do dataframe, não alterando-o diretamente.Vimos acima que temos apenas duas features com valores nulos: Province/State e Proportion. Os valores faltantes de Province/State já vieram assim no arquivo que utilizamos, já os faltantes de Proportion surgiram quando fizemos uma divisão por zero ao gerar a nova feature.  Por hora vamos ignorar as linhas que tem sua província/estado nula:<jupyter_code>print('Dimensões antes de excluir linhas: ', df.shape)
df = df.dropna(subset = ['Province/State'])
print('Dimensões após de excluir linhas: ', df.shape)<jupyter_output>Dimensões antes de excluir linhas:  (11614, 8)
Dimensões após de excluir linhas:  (6128, 8)<jupyter_text>Note que antes nossas dimensões eram (11614, 8) e passaram a ser (6128, 8), justamente pois excluimos as linhas que não tinham a informação do Estado.  Vamos ver quantos valores nulos temos agora:<jupyter_code>df.isna().sum()<jupyter_output><empty_output><jupyter_text>Ainda temos proporções que são valores nulos. Vamos lidar com esses valores de outra forma: substituindo-os. Vamos substituir os valores faltantes pela média dos valores existentes em cada uma das features.<jupyter_code>df = df.fillna(df.mean())
df.head()<jupyter_output><empty_output><jupyter_text>Vejamos novamente como estamos com nossos dados faltantes:<jupyter_code>df.isna().sum()<jupyter_output><empty_output><jupyter_text>Agora não temos mais valores nulos no nosso Dataframe!  PlotUtilizando a própria biblioteca Pandas podemos plotar alguns gráficos com os dados do DataFrame. Os métodos que nos permitem utilizar tal funcionalidade são feitos com base na biblioteca Matplotlib. Tal biblioteca é mais completa e mais eficiente, mas falaremos mais sobre ela na próxima aula. Por enquanto continuaremos utilizando Pandas, que tem uma sintaxe simples para necessidades iniciais.  Todos os gráficos podem ser utilizado com o atributo **plot**, seguido pelo tipo de gráfico desejado. Vejamos abaixo alguns deles. **Histograma**Podemos observar a distribuição dos dados utilizando um histograma. Vejamos a distribuição da contagem das frequências de cada Provincia/Estado:<jupyter_code>df['Province/State'].value_counts().plot.hist()<jupyter_output><empty_output><jupyter_text>**Gráfico de barra**Um gráfico com barras proporcionais aos valores que são representados em seus eixos. Aqui temos os 10 países/regiões mais frequentes.<jupyter_code>df['Country/Region'].value_counts()[:10].plot.bar()<jupyter_output><empty_output><jupyter_text>**Gráfico de barra horizontal**Mesma ideia do gráfico anterior, mas com as barras horizontais.<jupyter_code>df['Country/Region'].value_counts()[:10].plot.barh()<jupyter_output><empty_output><jupyter_text>**Scatter Plot**Os gráficos de dispersão são utilizados para analisar a relação entre duas variáveis. Vemos, por exemplo, que conforme o número de casos confirmados cresce, o número de casos recuperados também o faz.<jupyter_code>df.plot.scatter(x = 'Confirmed', y = 'Recovered')<jupyter_output><empty_output><jupyter_text>**Pie Plot**Gráficos de pizza são representações proporcionais de dados numéricos. Aqui vemos, dentre os 10 países/regiões mais frequentes, qual a proporção de cada um deles no total.<jupyter_code>df['Country/Region'].value_counts()[:10].plot.pie()<jupyter_output><empty_output><jupyter_text>ExportaçãoAssim como lemos os dados de um arquivo, é possível salvar o dataframe criado/modificado num arquivo também. Algumas das possíveis funções são:- [to_csv](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_csv.html): exporta o arquivo no formato CSV.- [to_excel](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_excel.html): exporta o arquivo como uma planilha Excel.- [to_json](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_json.html): exporta o arquivo como o Json.- [to_latex](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_latex.html): exporta o arquivo para Latex.<jupyter_code>df.to_csv('covid_after.csv', sep = ';')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08883582671409776, 'fraction_numerical': 0.010330880561376151, 'mean_word_length': 5.744002628984555, 'pattern_counts': {'":': 0, '<': 174, '<?xml version=': 0, '>': 175, 'https://': 14, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '44', 'dir': 'jupyter-structured-clean-dedup', 'id': '401300', 'license': 'MIT', 'n_tokens_mistral': 7364, 'n_tokens_neox': 7144, 'n_words': 2313, 'path': 'Aula2/introducao_pandas.ipynb', 'repo_name': 'leomaurodesenv/Intro-DS-2020.1', 'size': '20521'}