<jupyter_start><jupyter_text>Introdução ao NumPy e Pandas___ Conteúdo1. [Introdução](intro)2. [Jupyter Básico](basics)3. [Pacotes (Packages)](pacotes)4. [Encerramento](encerramento) 1. IntroduçãoBem-vindo a Trilha de Data Science! É um prazer tê-lo como aluno nesta jornada. O objetivo deste curso é fornecer uma introdução ao mundo de data science e machine learning de forma que você seja capaz de abordar problemas nestes domínios. Caso tenha interesse em se aprofundar ainda mais, recomendamos a inscrição em nosso curso na [VAI Academy](https://www.vai.academy/).Nesta primeira aula, vamos te introduzir a duas das bibliotecas mais importantes em análise de dados: as bibliotecas NumPy e Pandas.Utilizaremos o Jupyter Notebook para o acompanhamento da aula.  2. Jupyter e Colab Básicos 2.1. O que é Jupyter e Colab?"Jupyter" é uma aplicação *open source* para criar e compartilhar documentos que contém códigos executáveis, equações, visualizações e texto. O nome é um acrônimo que significa Julia, Python, and R. Essas linguagens de programação foram as primeiras utilizadas na aplicação Jupyter, mas, atualmente, o notebook também suporta  [muitas outras linguagens](https://github.com/jupyter/jupyter/wiki/Jupyter-kernels).Existem diferentes formas de abrir este tipo de arquivo. Neste curso, recomendamos o uso do Google Colab, pois ele possui algumas vantagens, como:* Nenhuma configuração necessária* Execução de código em núvem (servidores Google)* Ambiente isolado para instalação de bibliotecas com diversas já pré-instaladas* Integração com o Google DriveCaso queira se habituar com o Colab e praticar, use este [notebook introdutório](https://colab.research.google.com/notebooks/welcome.ipynb).Você pode também utilizar atalhos de teclado para agilizar a manipulação do notebook. No Google Colab, os principais são:* ```Ctrl+Enter```: Executar célula selecionada* ```Shift+Enter```: Executar célula e selecionar a célula seguinte* ```Ctrl+M B```: Inserir célula de código abaixo* ```Ctrl+M B```: Excluir célula/seleção* ```Ctrl+M Z``` ou ```Ctrl+Shift+Z```: Desfazer a última ação em célulasPara ver a lista completa de todos os atalhos, vá em Ferramentas > Atalhos do Teclado.  2.2 Execução de célulasNão podemos aprender Data Science sem antes conhecer nossas ferramentas, né?Clique na célula abaixo e pressione ```Ctrl+Enter``` para executar o código.<jupyter_code>print("Hello World!") #Lembre-se que shift+enter vai executar a célula atual e selecionar a próxima célula<jupyter_output>Hello World!<jupyter_text>Parabéns!!! Você acabou de executar sua primeira linha de código no Jupyter. Foi bem fácil, né?É assim que o notebook funciona, você pode executar o documento célula a célula e observar o que está sendo realizado. Vamos, agora, acompanhar uma seção sobre os pacotes básicos mais utilizados para análise de dados em Python: o **Numpy** e o **Pandas**!  3. Pacotes (Packages)Um *package* é um diretório de scripts Python, também chamados de modules, com um objetivo em comum. Isso significa que cada script é um módulo que define funções, métodos e tipos. E esse módulos estão organizados em *packages*.Quando você inicializa o Jupyter, apenas o package built-in é carregado. Para usar qualquer função, método ou objeto definido em outro módulo, você deve, primeiramente, importá-lo.Vamos tentar fazer isso com o NumPy, um pacote que lida eficientemente com arrays e matrizes, que você certamente utilizará bastante em sua vida com o Python.  3.1 NumPyNumPy é a abreviação de ‘Numerical Python’ ou ‘Numeric Python’. Ela é uma biblioteca *open-source* (ou seja, é um software cujo código original é disponibilizado livremente e pode ser distribuido e modificado) que oferece suporte à arrays e matrizes multidimensionais, provendo diversas funções matemáticas úteis em computação científica.Mas por que você deveria utilizar o NumPy? É simples! As listas do Python funcionam como as arrays, no entanto, são lentas para utilização com grandes volumes de dados. No ramo de Ciência de Dados, velocidade e recursos durante o processamento são bem importantes! Dessa forma, o NumPy nos possibilita utilizar os objetos arrays, que são bem mais rápidos que as listas tradicionais do Python.O **ndarray** é o objeto fundamental do NumPy. Este objeto é uma matriz N-dimensional, vamos entender melhor como este objeto funciona nas células abaixo.  3.1.1 Importando o NumPy<jupyter_code># Primeiramente, o que você acha que aconteceria se tentássemos utilizar algumas coisa de um pacote que ainda não foi carregado?
# Nós podemos testar isso com a função array, do módulo numpy, que retorna um array para uma lista dada
array([1, 2, 3])<jupyter_output><empty_output><jupyter_text>Como esperado, tivemos um erro. Nós deveríamos carregar o pacote, então.Para fazer isso, utilizamos a keyword 'import' seguida do nome do package.<jupyter_code>import numpy
numpy.array([1, 2, 3])<jupyter_output><empty_output><jupyter_text>Repare como precisamos especificar qual é o pacote ao qual a função ```array``` pertence. Poderia ficar muito chato se você tivesse que digitar os nomes dos pacotes todas as vezes que quisesse utilizar alguma coisa deles.Para te ajudar com isso, o Python permite que você utilize um alias para o nome do pacote, através da keyword 'as', tornando seu uso um pouco mais prático, como mostrado a seguir.<jupyter_code>import numpy as np
np.array([1, 2, 3])<jupyter_output><empty_output><jupyter_text>Por último, é possível carregar apenas uma parte de um pacote com a expressão  ```from ... import ...```. Fazendo isso, você estará permitido a utilizar apenas a parte que você carregou do pacote, sem a necessidade de especificar o nome do pacote posteriormente. Veja.<jupyter_code>from numpy import array
array([1, 2, 3])<jupyter_output><empty_output><jupyter_text>Ótimo, você já sabe importar os pacotes que precisar!Vamos observar o porquê o NumPy é tão poderoso e preferível às listas do Python.Imagine que você tenha duas listas de dados de indivíduos, com alturas e pesos, e queira calcular o IMC (Índice de Massa Corporal) de cada um deles, como mostrado abaixo:<jupyter_code>altura = [1.81, 1.77, 1.69, 1.91]
peso = [89.0, 77.3, 55.9, 99.4]

# Calculando o IMC
imc = peso / altura ** 2<jupyter_output><empty_output><jupyter_text>Observe que o Python nos retornou um erro porque não é possivel realizar cálculos com listas, para isso vamos utilizar o NumPy!<jupyter_code># Criando os arrays com o NumPy
np_altura = np.array(altura)
np_peso = np.array(peso)

# Calculando o IMC
imc = np_peso / np_altura ** 2
imc<jupyter_output><empty_output><jupyter_text>O Numpy consegue realizar perfeitamente as operações elemento a elemento!Mas preste bem atenção, pois algumas operações podem funcionar de forma diferente do que você imagina. Veja o exemplo abaixo:<jupyter_code># Lista do Python
altura * 2
# NumPy array
np_altura * 2<jupyter_output><empty_output><jupyter_text>3.1.2 Selecionando subconjuntos de NumPy ArraysA seleção de subconjuntos de NumPy arrays funciona de forma similar à listas de Python.<jupyter_code># retornando o terceiro elemento da array
imc[2]<jupyter_output><empty_output><jupyter_text>Você também pode selecionar subconjuntos baseados em condições, de forma que apenas os valores que satisfazem as condições serão retornados.<jupyter_code>imc[imc > 25]<jupyter_output><empty_output><jupyter_text>Vamos fazer um exercício para fixação.  Exercício 3.1<jupyter_code># lista de pesos de castanhas
c_peso = [0.946, 0.918, 0.906, 0.904, 0.858, 0.774, 0.652, 0.516, 0.478, 0.404, 0.396, 0.364, 0.342, 0.304, 
            0.262, 0.208, 0.134, 0.974, 0.792, 0.792, 0.628, 0.552, 0.506, 0.478, 0.462, 0.436, 0.408, 0.378, 
            0.3, 0.298, 0.268, 0.252, 0.16, 0.114, 0.092, 0.936, 0.894, 0.744, 0.706, 0.694, 0.69, 0.652, 0.518, 
            0.508, 0.502, 0.5, 0.47, 0.44, 0.39, 0.384]

# Importe o numpy como np
import numpy as np

# Crie um numpy array a partir de c_peso: np_c_peso
np_c_peso = np.array(c_peso)

# Imprima o tipo de np_c_peso
type(np_c_peso)
c_kg_preco = 45.00

# Crie um array a partir de c_peso com a quantia gasta em cada compra: np_c_despesa
np_c_despesa = c_kg_preco * np_c_peso
# Imprima o peso no indíce 20
np_c_despesa[20]<jupyter_output><empty_output><jupyter_text>3.1.3  Array N-dimensionalVamos verificar o tipo dos arrays criados acima!<jupyter_code>print(type(np_peso))<jupyter_output><class 'numpy.ndarray'><jupyter_text>**ndarrays** significam arrays N-dimensionais, vamos criar um numpy array multi-dimensional a partir de listas tradicionais do Python.<jupyter_code>np_2d = np.array([[1.81, 1.77, 1.69, 1.91],
                  [89.0, 77.3, 55.9, 99.4]])
np_2d<jupyter_output><empty_output><jupyter_text>Cada sublista da lista corresponde a uma linha da array bi-dimensional criada.Nós podemos verificar o tamanho da array usando o atributo "shape".<jupyter_code>np_2d.shape<jupyter_output><empty_output><jupyter_text>Podemos ver que o np_2d tem 2 linhas e 4 colunas. Assim como a array unidimensional, também podemos selecionar um subconjunto de uma array multi-dimensional, usando o índice da linha e coluna como abaixo.![Subsetting](https://imgur.com/08EIOjy.png)Veja alguns exemplos abaixo.<jupyter_code>np_2d[0]
# Selecionando a altura (primeira linha) do terceiro elemento
np_2d[0][2]<jupyter_output><empty_output><jupyter_text>Basicamente nós selecionamos a linha, e a partir daquela linha fazemos outra seleção.Também é possível selecionar utilizando vírgulas dentro de colchetes: array[linha, coluna]<jupyter_code># Primeira linha e terceira colna
np_2d[0, 2]
# Todas as linhas e segunda e terceira colunas
np_2d[:, 1:3]<jupyter_output><empty_output><jupyter_text>Exercício 3.2Abaixo temos uma lista de listas contendo informações de vendas de castanhas de uma loja. Cada lista representa uma venda que foi realizada. O primeiro elemento de cada lista é o dia que  venda foi feita, o segundo elemento representa o peso das castanhas compradas. Por fim, o terceiro elemento é a quantia paga pelas castanhas.Com isso em mente, faça os seguintes exercícios:<jupyter_code>castanha = [[2, 0.946, 66.1], 
          [2, 0.918, 32.96], 
          [2, 0.906, 58.76],
          [2, 0.904, 29.14], 
          [2, 0.858, 59.96],
          [2, 0.774, 27.77],
          [2, 0.652, 42.3],
          [2, 0.516, 18.51], 
          [2, 0.478, 17.15],
          [2, 0.404, 28.22], 
          [2, 0.396, 7.88], 
          [2, 0.364, 7.24],
          [2, 0.342, 22.18], 
          [2, 0.304, 10.91], 
          [2, 0.262, 9.41], 
          [2, 0.208, 4.13],
          [2, 0.134, 9.36],
          [4, 0.974, 34.95],
          [4, 0.792, 51.38],
          [4, 0.792, 51.38], 
          [4, 0.628, 12.48], 
          [4, 0.552, 19.81], 
          [4, 0.506, 25], 
          [4, 0.478, 31], 
          [4, 0.462, 32.24],
          [4, 0.436, 28.28],
          [4, 0.408, 14.64],
          [4, 0.378, 13.56],
          [4, 0.3, 19.46],
          [4, 0.298, 10.69],
          [4, 0.268, 9.62],
          [4, 0.252, 16.34],
          [4, 0.16, 3.18],
          [4, 0.114, 4.09],
          [4, 0.092, 5.97],
          [5, 0.936, 65.33],
          [5, 0.894, 32.07],
          [5, 0.744, 48.28], 
          [5, 0.706, 25.34],
          [5, 0.694, 24.91], 
          [5, 0.69, 13.72], 
          [5, 0.652, 42.32], 
          [5, 0.518, 33.6], 
          [5, 0.508, 18.23],
          [5, 0.502, 35.09],
          [5, 0.5, 27.45], 
          [5, 0.47, 9.35], 
          [5, 0.44, 28.54],
          [5, 0.39, 7.76], 
          [5, 0.384, 21.08]]

# Crie um numpy array 2d a partir de castanha: np_castanha
np_castanha = np.array(castanha)

# Imprima o tipo de np_castanha
print(type(np_castanha))

# Imprima a forma de np_castanha
np_castanha.shape
# Selecione toda a segunda coluna de np_castanha: np_c_peso
np_c_peso = np_castanha[:, 1]

# Imprima o preço da 14 venda
print(np_castanha[13][2])

# Imprima todas as vendas feitas após o dia 2
np_castanha[17:,:]<jupyter_output>10.91<jupyter_text>3.1.4 Estatística básica com NumPyCostumeiramente o primeiro passo para analisar nossos dados é conhecê-los através de estatística descritiva. O NumPy pode ser usado para obter essa visão inicial dos dados mesmo com grande quantidade de observações.Vamos usar alguns atributos do NumPy para começar a analisar nossos dados.<jupyter_code>np_a_p = np.array([[1.81, 89.0],
                  [1.77, 77.3],
                  [1.69, 55.9],
                  [1.91, 99.4]])

# Calculando a media dos pesos
np.mean(np_a_p[:, 1])
# calculando a mediana dos pesos
np.median(np_a_p[:, 1])
# calculando os coeficientes de correlação entre pesos e alturas
np.corrcoef(np_a_p[:, 0], np_a_p[:, 1])
# calculando o desvio padrão dos pesos
np.std(np_a_p[:, 1])
# calculando a soma dos pesos
np.sum(np_a_p[:, 1])<jupyter_output><empty_output><jupyter_text>Alguns desses atributos já estão disponíveis no Python, no entanto, a principal diferença entre eles é a performance. Os atributos do NumPy são mais rápidos na execução do que os básicos do Python.Lembrando que sempre que tiver dificuldade para entender algum atributo, você pode consultar a documentação do [Numpy](https://numpy.org/doc/).Agora que aprendemos como o NumPy funciona, vamos aprender sobre uma das bibliotecas mais utilizadas para manipulação de dados em Python, o **Pandas**!  3.2 O que é o Pandas?Pandas é uma biblioteca *open source*, que proporciona estruturas de dados e ferramentas de análise de dados de alta performance e fáceis de usar para Python. Vamos entender melhor o que alguns termos significam: - *open source*: assim como o NumPy, seu código original é disponibilizado livremente e pode ser distribuido e modificado. Isso significa que qualquer um pode contribuir para a evolução do Pandas! - alta performance: Pandas é escrito em Python, Cython e C. Isso permite que os cientistas de dados consigam o utilizar para lidar com conjuntos de dados muito grandes (daqueles que o excel não conseguiria nem abrir) e fazer operações sobre esses dados com facilidade. Dessa forma, Pandas torna nosso trabalho melhor e mais fácil provendo ótima performânce. - estruturas de dados e análises de dados: o motivo pelo qual Pandas existe. Muitas vezes precisamos obter algo com significado de dados crus como documentos de textos, tabelas e etc. Pandas é capaz de lidar com esses tipos de dados para que possamos analisá-los.Em resumo, **Pandas fornece estrutura de dados especializadas e ferramentas para manipulação de dados**. Sua ótima performance, facilidade de uso e comunidade dedicada são as principais razões de sua vasta adoção entre cientistas de dados. Vamos utilizá-lo! 3.3 Começando com Pandas 3.3.1 Como importá-lo?Por algum motivo, todo mundo importa o Pandas da mesma forma, como mostramos abaixo. Aproveitamos também para importar o Numpy, biblioteca que já apresentamos anteriormente.<jupyter_code>import pandas as pd
import numpy as np<jupyter_output><empty_output><jupyter_text>3.3.2 Objetos do PandasExistem 2 principais tipos de objetos no Pandas: as [*Series*](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.htmlpandas.Series) e os *DataFrames*. As *Series* são sequências de uma dimensão  de elementos (pra ser mais específicos *ndarray*), todos do mesmo tipo de dados, com rótulos/índices (*labels*). São o objeto primário do Pandas, tudo vai funcionar baseado nelas. Pra criar um objeto do tipo *Series*, podemos fazer o seguinte:<jupyter_code>s = pd.Series([1, 3, 5, np.nan, 6, 8])
s<jupyter_output><empty_output><jupyter_text>Podemos ver acima que cada elemento da *Series* tem um rótulo relacionado. Esses rótulos podem ser tanto numéricos quanto de texto! Ao final do objeto temos a informação sobre o tipo de dados da *Series*: nesse caso, números *float64*. O outro objeto principal do Pandas é o [*DataFrame*](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.htmlpandas.DataFrame), que é basicamente uma coleção de *Series* com rótulos em comum. O *DataFrame* é bem parecido com uma tabela de Excel, com seus índices e colunas. Tanto suas linhas quanto suas colunas tem rótulos, nos permitindo acessar qualquer célula pela sua coordenada. Há diversas formas de criar um *DataFrame*, vamos começar com o mais simples:<jupyter_code>dates = pd.date_range('20130101', periods=6) #estamos criando uma lista de datas entre 01/01/2013 e 06/01/2013. Note que estamos usando uma função do Pandas
df = pd.DataFrame(
    np.random.randn(6, 4), #apenas números aleatórios nas células
    index=dates,  #especificando quais são os índices. Eles aceitam até datas como índice! Isso é muito bom para lidar com dados seriados no tempo
    columns=list('ABCD')) #especificando como quero que sejam os nomes das colunas passando uma lista de letras
df<jupyter_output><empty_output><jupyter_text>Isso é um *DataFrame*! Uma coisa boa do Pandas com o Jupyter Notebook é que eles mostram o *DataFrame* de uma forma bastante amigável. Agora que sabemos sobre as duas principais estruturas de dados do Pandas, podemos aprender sobre os principais métodos e funcionalidades dessa biblioteca, e para isso vamos utilizar dados reais sobre um das marcas mais conhecidas dos desenhos e videogames :)Observação: um *DataFrame* pode ser visto como um dicionário de listas. 3.3.3 Carregando o conjunto de dadosUm dos tipos de dados mais comuns para se guardar arquivos são os CSVs. O Pandas tem diversas funções para transformar os mais variados tipos de arquivos em *DataFrames*, como csv, excel, json e etc. Nesse exemplo, vamos usar o [leitor de csv](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html).Você pode importar arquivos no Colab de duas maneiras diferentes:1. **Upload manual**: este método tem a desvantagem de que o arquivo é deletado quando o Colab é fechado. Para subir o arquivo, clique no ícone de pasta à esquerda. Isto vai abrir a barra lateral da sessão de arquivos. Nela, clique no primeiro ícone e selecione o arquivo do seu computador para subir2. **Conectando ao seu Google Drive**: este método dá acesso a todos os seus arquivos e pastas do Google Drive. Para isso, abra novamente a sessão de arquivos, clique no terceiro ícone (aquele com o logo do Drive) e siga as instruções.Para obter o caminho do arquivo, você pode encontrá-lo navegando através da sessão de arquivos na esquerda, clicar com o botão direito do mouse no arquivo desejado e clicar para copiar o caminho (path).<jupyter_code># pkmn = pd.read_csv(
#     '/dados/aula_1_pokemon.csv', #o caminho para o arquivo que se quer ler
#     sep=',') #o caracter utilizado para separar os valores
# Usando outro caminho para obter os csv do google drive
from google.colab import drive
drive.mount('/content/drive')
!ln -s /content/drive/MyDrive/Data\ Science/Entrega1 /mydrive
!ls /mydrive
pkmn = pd.read_csv(
    '/mydrive/dados/aula_1_pokemon.csv', #o caminho para o arquivo que se quer ler
    sep=',') #o caracter utilizado para separar os valores<jupyter_output><empty_output><jupyter_text>3.3.4 Visualizações iniciaisÓtimo! Acabamos de criar um *DataFrame* a partir de um arquivo csv. Mas como gostamos de verificar as coisas, seria interessante saber algumas coisas como o que os dados contêm, como é, se tem valores nulos e etc. O Pandas tem 4 métodos principais para isso:<jupyter_code>pkmn.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 800 entries, 0 to 799
Data columns (total 13 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   #           800 non-null    int64 
 1   Name        800 non-null    object
 2   Type 1      800 non-null    object
 3   Type 2      414 non-null    object
 4   Total       800 non-null    int64 
 5   HP          800 non-null    int64 
 6   Attack      800 non-null    int64 
 7   Defense     800 non-null    int64 
 8   Sp. Atk     800 non-null    int64 
 9   Sp. Def     800 non-null    int64 
 10  Speed       800 non-null    int64 
 11  Generation  800 non-null    int64 
 12  Legendary   800 non-null    bool  
dtypes: bool(1), int64(9), object(3)
memory usage: 75.9+ KB<jupyter_text>A primeira coisa útil de fazer após ler um conjunto de dados em um *DataFrame* é utilizar o método *info()*. Ele mostra informações como: - a classe do objeto criado - o intervalo do índice e quantas linhas de dados se tem - as colunas, seus nomes e tipos de dado - quais os tipos de dados presentes no *DataFrame* e quantas colunas de cada - a quantidade de memória utilizada pelo computador para guardar esses dadosPodemos ver que temos uma base sobre Pokémon, com 800 linhas com índices númericos de 0 a 799, 13 colunas de três tipos de dados diferentes, usando cerca de ~76kB de memória RAM. As informações presentes sobre os Pokémons são o número, o nome, o tipo (alguns tem subtipo, mas não todos, por isso dos dados faltantes na coluna *Type 2*), estatísticas de ataque, defesa e velocidade, a geração e a indicação se ele é lendário (como uma espécie mística).  Todas essas informações a gente conseguiu descobrir com apenas uma linha de código! Vamos então ver como o *DataFrame* realmente é. Temos dois métodos para isso:<jupyter_code>pkmn.head()
pkmn.tail()<jupyter_output><empty_output><jupyter_text>O método *.head()* e *.tail()* mostram, respectivamente, as primeiras e últimas n linhas do *DataFrame* (por padrão, n=5, mas você pode passar qualquer número como parâmetro) mostrando os índices e os nomes das colunas como numa tabela.<jupyter_code>pkmn.describe()<jupyter_output><empty_output><jupyter_text>Por fim, o método *.describe()* mostra um resumo estatístico de todas as colunas numéricas. É um método bom para ter uma ideia inicial sobre o que ocorre nas colunas de uma perspectiva estatística. Exercício 3.3Use o arquivo de jogadores do FIFA Ultimate Team para os exercícios de Pandas. Caso você não conheça, o Ultimate Team (FUT) é um modo de jogo do FIFA onde você monta seu próprio time comprando jogadores do jogo. Substitua os \____ abaixo para ler o arquivo e siga as instruções para ter as visualizações iniciais.<jupyter_code># leia o arquivo (tente abrir num editor de texto antes para verificar o separador)
fut_players = pd.read_csv(
    '/mydrive/dados/aula_1_fut_players.csv',
    sep=',')

# mostre as primeiras 10 linhas de dados
fut_players.head(n=10)
# mostre as últimas 10 linhas de dados
fut_players.tail(n=10)
# use o método .info() no DataFrame
fut_players.info()
# mostre o resumo estatístico das colunas numericas
fut_players.describe()<jupyter_output><empty_output><jupyter_text>3.4 Filtrando (*Filtering*) e fatiando (*slicing*) os dados*Filtering* e *slicing* são técnicas utilizadas para isolar partes específicas do *DataFrame*, sejam linhas, colunas ou células. Isso é muito útil pois diversas vezes queremos analisar alguns dados ao invés da base inteira. O Pandas tem ferramentas próprias para isso.  3.4.1 *Slicing*No Pandas existem duas principais formas de fatiar os dados, isto é, selecionar apenas uma parte de acordo com as linhas e colunas do *DataFrame*: utilizando o nome das partes ou com os métodos *.loc()* e *.iloc()*. Vamos começar pelo primeiro. Para fazer isso, imagine que queremos apenas os nomes e o poder de ataque dos Pokémons e veja o exemplo:<jupyter_code>pkmn[['Name','Attack']].head(7)<jupyter_output><empty_output><jupyter_text>Duas coisas importantes aqui: - Juntamos o *slicing* com o método *.head()* na mesma linha, para que fosse possível ver o resultado do fatiamento. Ao usar o Pandas é possível e comum fazer esse tipo de agrupamento de operações. - Foram utilizadas chaves duplas [[]] no fatiamento. Ao fazer isso, estou explicitando que quero um objeto do tipo *DataFrame*. Se eu quisesse objetos do tipo *Series* usaria chaves simples. Podemos ver um exemplo disso abaixo:<jupyter_code>type(pkmn[['Name']])
type(pkmn['Name'])<jupyter_output><empty_output><jupyter_text>Outra forma de se obter um objeto do tipo *Series* é passando a coluna como se fosse um atributo do *DataFrame*:<jupyter_code>pkmn.Name<jupyter_output><empty_output><jupyter_text>O único problema desse formato é que colunas cujo nome contém espaços não funcionarão, como é o caso das colunas *Type 1*, *Type 2*, *Sp. Atk* e *Sp. Def*. Para resolver isso, vamos renomeá-las com o método *.rename()*.<jupyter_code>pkmn.rename(
    columns={'Type 1':'Type_1', 'Type 2':'Type_2', 'Sp. Atk':'Sp_Atk','Sp. Def':'Sp_Def'}, #passando o nome antigo e novo como um dicionário
    inplace = True #algumas operações com Pandas criam uma cópia do DataFrame e não alteram o objeto em si, alteramos isso mudando o parâmetro inplace para verdadeiro
)
pkmn.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 800 entries, 0 to 799
Data columns (total 13 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   #           800 non-null    int64 
 1   Name        800 non-null    object
 2   Type_1      800 non-null    object
 3   Type_2      414 non-null    object
 4   Total       800 non-null    int64 
 5   HP          800 non-null    int64 
 6   Attack      800 non-null    int64 
 7   Defense     800 non-null    int64 
 8   Sp_Atk      800 non-null    int64 
 9   Sp_Def      800 non-null    int64 
 10  Speed       800 non-null    int64 
 11  Generation  800 non-null    int64 
 12  Legendary   800 non-null    bool  
dtypes: bool(1), int64(9), object(3)
memory usage: 75.9+ KB<jupyter_text>Agora que os nomes foram trocados, podemos obter um objeto *Series* do tipo do Pokémon como a seguir:<jupyter_code>pkmn.Type_1<jupyter_output><empty_output><jupyter_text>Outra forma de selecionar partes dos dados é usando os métodos *.loc()* e *.iloc()*.Para usar a localização númerica utilizamos o *iloc*. Como você pode imaginar, a linhas e colunas são ordenadas por números inteiros sequenciais, começando do 0, como nas listas. Dessa forma, se você sabe o número da linha e da coluna, você pode usar o *iloc*. Por exemplo, se quisermos a coluna HP, que é a 6ª, poderíamos fazer o seguinte:<jupyter_code>pkmn.iloc[:,5].head()<jupyter_output><empty_output><jupyter_text>A sintaxe do *iloc* é como [x,y], que significa que queremos a (x+1)ª linha e (y+1)ª coluna. Se utilizarmos ':' no lugar de x ou y significa que queremos a coluna ou linha completa, respectivamente. Vamos pegar o HP do Bulbasaur, o primeiro Pokémon do nosso *DataFrame*:<jupyter_code>print("O HP do Bulbasaur é "+str(pkmn.iloc[0,5]))<jupyter_output>O HP do Bulbasaur é 45<jupyter_text>O método *.loc()* usa o rótulo para acessar os valores. Dessa forma, ao invés de passarmos as coordenadas numéricas, passamos o nome da linha e da coluna, como a seguir:<jupyter_code>pkmn.loc[0, :]<jupyter_output><empty_output><jupyter_text>No caso, os rótulos das linhas são iguais às suas coordenadas, por isso ficou parecido com o *iloc*. Vamos fazer o teste com as colunas também para ver a diferença. Abaixo pegaremos novamente o HP do Bulbasaur:<jupyter_code>print("O HP do Bulbasaur é "+str(pkmn.loc[0,'HP']))<jupyter_output>O HP do Bulbasaur é 45<jupyter_text>3.4.2 Filtros (*Filtering*)Uma vez sabendo isolar partes do *DataFrame* de acordo com a localização dos dados, podemos partir para isolar de acordo com condições, ou seja, filtrar os dados.Para conseguir fazer isso no Pandas, fazemos o seguinte: passamos uma expressão condicional e o Pandas retorna apenas as partes que teriam a condição como verdade. Para testar isso, vamos ver a defesa média de todas os Pokémons e depois ver se os tipos 'Rock' e 'Steel' tem defesas maiores:<jupyter_code>pkmn.Defense.mean() #note que operações comuns como média (mean), mediana (median) e soma (sum) são métodos do Pandas
pkmn.loc[pkmn.Type_1=='Rock'].Defense.mean()
pkmn[pkmn.Type_1=='Steel'].Defense.mean()<jupyter_output><empty_output><jupyter_text>De fato, parece que os tipos selecionados tem média acima dos demais Pokémons. Para verificar isso, passamos a condição pkmn.Type_1=='Steel' entre chaves, o que retorna apenas as linhas de tal tipo. Com isso, selecionamos apenas a coluna de defesa e calculamos a média. Vamos ver agora os Pokémons com defesa maior que 150, cujo tipo principal não é 'Rock' nem 'Steel':<jupyter_code>pkmn[(pkmn.Defense > 150)&(pkmn.Type_1!='Rock')&(pkmn.Type_1!='Steel')]<jupyter_output><empty_output><jupyter_text>Podemos ver acima que é possível juntar condições com os operadores E (&) e OU (|), como vimos no inicio da aula.Vamos dizer agora que você quer apenas alguns Pokémons em específico, por exemplo Venusaur, Charizard e Blastoise. Criar uma condição para cada e uní-las com o operador & pode ser difícil, ainda mais se for uma quantidade grande de opções. Podemos facilitar isso passando uma tupla ao método *.isin()*, como abaixo:<jupyter_code>aux = ('Venusaur', 'Charizard', 'Blastoise')
pkmn[pkmn.Name.isin(aux)]<jupyter_output><empty_output><jupyter_text>Finalmente, podemos criar novos *DataFrames* de um já existente selecionando apenas algumas linhas ou colunas dele:<jupyter_code>offensive_stats = pkmn[['#','Name','Attack','Sp_Atk','Speed']] #selecionando apenas estatísticas ofensivas
defensive_stats = pkmn[['#','Name', 'HP','Defense','Sp_Def']] #selecionando apenas estatísticas defensivas
offensive_stats.head()
defensive_stats.head()
fire_pkmn = pkmn[(pkmn.Type_1=='Fire')|(pkmn.Type_2=='Fire')] #filtrando apenas linhas com algumas condições
fire_pkmn.head()
water_pkmn = pkmn[(pkmn.Type_1=='Water')|(pkmn.Type_2=='Water')] #filtrando apenas linhas com algumas condições
water_pkmn.head()<jupyter_output><empty_output><jupyter_text>Exercício 3.4Siga as instruções e substitua os \____ para exercitar o que aprendemos.<jupyter_code># mostre as 5 primeiras linhas das colunas player_name, position and nationality
fut_players[['player_name','position','nationality']].head()
#renomeie as colunas player_id, player_name and player_extended_name para id, name and extended_name, respectivamente
fut_players.rename(
    columns={'player_id':'id', 'player_name': 'name', 'player_extended_name': 'extended_name'},
    inplace=True
)

fut_players.info()
#imprima a coluna extended_name do 4534º jogador usando loc e iloc
print(fut_players.iloc[4533,2])
print(fut_players.loc[4533,'extended_name'])
#nosso DataFrame tem muitas colunas
#crie outro DataFrame (fut_players_2) apenas com as colunas na lista abaixo
selected_columns = ['id', 'name', 'overall', 'nationality', 'position', 'pref_foot', 'base_id']

fut_players_2 = fut_players[selected_columns]

fut_players_2.info()
#queremos ver os melhores jogadores nascidos no Brasil (Brazil), isto é, aqueles com médio (overall) acima de 90
#mostre os 15 primeiros
aux_1 = fut_players_2[(fut_players.overall > 90)&(fut_players.nationality=='Brazil')] #.drop_duplicates(subset=['name']) Para retirar elementos repetidos
aux_1.head(n=15)
#vários jogadores bons!
#agora mostre os jogadores brasileiros que sejam canhotos (pref_foot é Left) ou que sejam goleiros (position é GK)
aux_2 = fut_players_2[(fut_players.nationality=='Brazil')&(fut_players.pref_foot=='Left')&(fut_players.position=='GK')]
aux_2.head(20)<jupyter_output><empty_output><jupyter_text>3.4 Juntando DataFramesÉ muito comum ter a necessidade de juntar *DataFrames* diferentes. Se você já utilizou SQL ou qualquer outro banco de dados relacional, deve conhecer isso como *join*. O Pandas também tem a mesma função utilizando o método *.merge()*. Antes do exemplo, vamos relembrar os tipos de *joins* mais comuns:![Joining Methods](https://arquivo.devmedia.com.br/artigos/Fernanda_sallai/sql_join/image001.jpg) Agora, vamos testar os *merge* nos *DataFrames* filtrados que criamos anteriormente:<jupyter_code>all_stats = pd.merge(
    offensive_stats, #o DataDrame da esquerda
    defensive_stats, #o DataDrame da direita
    how='inner', #o tipo de join que queremos fazer
    on=['#','Name']) #baseado em quais valores em comum
all_stats.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 800 entries, 0 to 799
Data columns (total 8 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   #        800 non-null    int64 
 1   Name     800 non-null    object
 2   Attack   800 non-null    int64 
 3   Sp_Atk   800 non-null    int64 
 4   Speed    800 non-null    int64 
 5   HP       800 non-null    int64 
 6   Defense  800 non-null    int64 
 7   Sp_Def   800 non-null    int64 
dtypes: int64(7), object(1)
memory usage: 56.2+ KB<jupyter_text>Ótimo! Conseguimos fazer o *merge* (termo mais utilizado no Pandas) de dois *DataDrames*. Lembre-se que *inner*, *left*, *right* e *outer* terão resultados diferentes:<jupyter_code>fire_pkmn.info()
water_pkmn.info()
left_fire_water = pd.merge(fire_pkmn, water_pkmn, how='left', on=['#','Name'])
left_fire_water.info()
right_fire_water = pd.merge(fire_pkmn, water_pkmn, how='right', on=['#','Name'])
right_fire_water.info()
inner_fire_water = pd.merge(fire_pkmn, water_pkmn, how='inner', on=['#','Name'])
inner_fire_water.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 1 entries, 0 to 0
Data columns (total 24 columns):
 #   Column        Non-Null Count  Dtype 
---  ------        --------------  ----- 
 0   #             1 non-null      int64 
 1   Name          1 non-null      object
 2   Type_1_x      1 non-null      object
 3   Type_2_x      1 non-null      object
 4   Total_x       1 non-null      int64 
 5   HP_x          1 non-null      int64 
 6   Attack_x      1 non-null      int64 
 7   Defense_x     1 non-null      int64 
 8   Sp_Atk_x      1 non-null      int64 
 9   Sp_Def_x      1 non-null      int64 
 10  Speed_x       1 non-null      int64 
 11  Generation_x  1 non-null      int64 
 12  Legendary_x   1 non-null      bool  
 13  Type_1_y      1 non-null      object
 14  Type_2_y      1 non-null      object
 15  Total_y       1 non-null      int64 
 16  HP_y          1 non-null      int64 
 17  Attack_y      1 non-null      int64 
 18  Defense_y     1 non-null      int64 
 19  Sp_Atk_y    [...]<jupyter_text>Como podemos ver, os resultados são de fato bem diferentes.Podemos também querer apenas concatenar dois *DataDrames*, isto é, juntá-los colocando um abaixo ou ao lado do outro. Para isso, utilizamos o método *.concat()*:<jupyter_code>fire_and_water = pd.concat([fire_pkmn, water_pkmn], ignore_index=True)
fire_and_water.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 190 entries, 0 to 189
Data columns (total 13 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   #           190 non-null    int64 
 1   Name        190 non-null    object
 2   Type_1      190 non-null    object
 3   Type_2      103 non-null    object
 4   Total       190 non-null    int64 
 5   HP          190 non-null    int64 
 6   Attack      190 non-null    int64 
 7   Defense     190 non-null    int64 
 8   Sp_Atk      190 non-null    int64 
 9   Sp_Def      190 non-null    int64 
 10  Speed       190 non-null    int64 
 11  Generation  190 non-null    int64 
 12  Legendary   190 non-null    bool  
dtypes: bool(1), int64(9), object(3)
memory usage: 18.1+ KB<jupyter_text>Acima fizemos a concatenação vertical. Vamos fazer a horizontal abaixo:<jupyter_code>atk_and_defense = pd.concat([offensive_stats, defensive_stats], axis=1)
atk_and_defense.head()<jupyter_output><empty_output><jupyter_text>Exercício 3.5Mais uma vez, substitua os \____ de acordo com as instruções<jupyter_code>#the_best é um DataDrame dos melhores jogadores em drible (dribbling) e chute (shooting)
the_best = fut_players[(fut_players.dribbling > 90) & (fut_players.shooting > 90)][['id', 'name', 'position', 'dribbling', 'shooting', 'overall']]

#nationalities é um DataDrame da nacionalidade dos jogadores
nationalities = fut_players[['id', 'name', 'nationality']]

#faça um merge dos dois DataDrames para obter a nacionalidade dos melhores jogadores (dica: chave é o id)
the_best_nationality = pd.merge(
                        the_best, 
                        nationalities, 
                        how='inner', 
                        on=['id']) 

the_best_nationality.head()<jupyter_output><empty_output><jupyter_text>3.5 Operações em grupoCom Pandas nós podemos aplicar operações em grupos usando o método *.groupby()*. Ele é muito útil por ser uma forma bem simples de extrair informação de dados agregados. Para utilizá-lo, passamos as colunas nas quais queremos agrupar os dados e a operação que queremos fazer. Para exemplificar, vamos ver quantos Pokémons lendários cada geração tem:<jupyter_code>pkmn.groupby('Generation').Legendary.sum() #fazendo uma soma pois a coluna Legendary é boolean<jupyter_output><empty_output><jupyter_text>Podemos obter um relatório da média de diversas colunas para cada tipo de Pokémon:<jupyter_code>pkmn.groupby('Type_1')[['HP','Attack','Defense','Sp_Atk','Sp_Def','Speed']].mean()<jupyter_output><empty_output><jupyter_text>Isso é realmente muito importante e extremamente utilizado com pandas pois conseguimos fazer análises dos grupos com apenas uma linha de código. Podemos perceber, por exemplo, que Pokémons do tipo *Flying* são especialistas em velocidade enquanto *Dragon* e *Fighting* são especialistas em ataque. Exercício 3.6Use o método *.groupby()* para descobrir qual país tem o melhor *overall* médio.<jupyter_code>#cria o DataDrame country_avg_overall, que tem o overall médio de cada país (nationality), usando groupby
country_avg_overall = fut_players.groupby('nationality').overall.mean()

#usamos o método idxmax() para encontrar o maior overall médio
print("Melhor overall médio: \n", country_avg_overall.loc[country_avg_overall.idxmax()])
print("Overall médio do Brasil: ", country_avg_overall.loc["Brazil"])<jupyter_output>Melhor overall médio: 
 79.0
Overall médio do Brasil:  75.21915584415585<jupyter_text>3.6 Aplicando funções no PandasCom Pandas, nós temos um grande nível de controle de nossos dados, e somos capazes de transformá-los conforme queiramos. Nós podemos, até mesmo, executar funções em DataFrames e manipulá-lo como quisermos. Vamos revisitar o método head():<jupyter_code>pkmn.head(13)<jupyter_output><empty_output><jupyter_text>Existem algumas mega evoluções misturadas no dataset. Não seria legal se nós tivéssemos alguma flag que nos diria se um pokémon é mega ou não? E, por um acaso, será que os pokémons mega são mais poderosos?Você deve ter percebido que evoluções mega têm um padrão em nosso DataFrame, algo como 'PokemonMega Pokemon'. Se nós tivermos esse padrão, podemos construir uma função que retorna True is este padrão for detectado:<jupyter_code>def is_it_mega(pokemon_name):
    """
    Recebe um nome de pokemon e diz se é uma mega evolução ou não
    I: string pokemon_name
    O: boolean para Mega evos
    """
    if 'Mega ' in pokemon_name: #é importante usar 'Mega ' e não 'mega', pois há um pokemon chamado called Yanmega e outro chamado Meganium que não são uma mega evolução
        return True
    else:
        return False<jupyter_output><empty_output><jupyter_text>Vamos ver se funciona:<jupyter_code>is_it_mega('VenusaurMega Venusaur')
is_it_mega('Squirtle')<jupyter_output><empty_output><jupyter_text>Excelente! Seria ótimo se conseguíssemos aplicar essa função em todo nosso DataFrame. Para fazer isso, usaremos o método .apply(). Também criaremos uma coluna que é uma flag se o pokémon é mega:<jupyter_code>pkmn['Mega'] = pkmn.apply(
    lambda row: is_it_mega(row['Name']), #chamando uma função lambda que acabamos de construir
    axis=1 #qual direção queremos executar a função. 0 para horizontal, 1 para vertical
)
pkmn.head()<jupyter_output><empty_output><jupyter_text>Agora, vamos verificar quão poderosos são os pokémons mega:<jupyter_code>pkmn.groupby('Mega').Total.mean()<jupyter_output><empty_output><jupyter_text>Uau! Eles têm quase 200 stat points a mais que pokémons normais! Evoluções mega são, sim, muito poderosos! Uma boa prática é sempre tentar manter nosso DataFrame organizado. A forma como os pokémons mega estão nomeados não é muito ótima, e nós já temos uma coluna com a flag para pokémons Mega, então, vamos atacar isso! A estrutura do nome de um pokémon mega é da seguinte forma: 'NomeMega Nome'. Portanto, se nós pegarmos o que vem após o caractere ' ', teremos o nome original do pokémon!<jupyter_code>pkmn.Name.nunique() #numero de elementos unicos na coluna
def get_original_name(s):
    """
    Recebe um nome de pokemon e retorna seu nome original
    I: s string
    O: string
    """
    return s.split(' ')[-1]

pkmn['Name'] = pkmn.Name.apply(lambda s: get_original_name(s)) #sobreescrevendo a coluna Name
pkmn.Name.nunique()
pkmn.head()<jupyter_output><empty_output><jupyter_text>Parece que conseguimos limpar o nome deles! Agora nós cobrimos toda a parte básica de Pandas! Mas vamos a um exercício final. Exercício 3.7Crie uma função que retorna a classificação para o jogador de acordo com as instruções abaixo, então aplique isso para o dataframe fut_players.<jupyter_code>def get_classification(overall):
    """
    Recebe um overall de algum jogador e retorna a classificação conforme a seguir:
    Overall -> classification
    -50     -> "Amador"
    50-60   -> "Ruim"
    60-70   -> "Ok"
    70-80   -> "Bom"
    80-90   -> "Ótimo"
    90+     -> "Lenda"
    
    I: int overall
    O: string
    """
    if overall < 50:
      return "Amador"
    elif (overall >= 50)&(overall < 60):
      return "Ruim"
    elif (overall >= 60)&(overall < 70):
      return "Ok"
    elif (overall >= 70)&(overall < 80):
      return "Bom"
    elif (overall >= 80)&(overall < 90):
      return "Ótimo"
    else:
      return "Lenda"
    
fut_players["classification"] = fut_players.apply(
    lambda row: get_classification(int(row['overall'])), 
    axis=1)
fut_players.head()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08813941224411526, 'fraction_numerical': 0.034853061028812506, 'mean_word_length': 4.100745812446509, 'pattern_counts': {'":': 0, '<': 221, '<?xml version=': 0, '>': 233, 'https://': 9, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '55', 'dir': 'jupyter-structured-clean-dedup', 'id': '461042', 'license': 'MIT', 'n_tokens_mistral': 16006, 'n_tokens_neox': 14776, 'n_words': 5127, 'path': 'NumPy_e_Pandas.ipynb', 'repo_name': 'Bruno-Messias/data_science_visagio', 'size': '41718'}