Baixe o app para aproveitar ainda mais
Prévia do material em texto
Prof. Giovane Barcelos giovane_barcelos@uniritter.edu.br Engenharia de Dados ED0501 mailto:giovane_barcelos@uniritter.edu.br Pág. 2 Engenharia de Dados De 66 Plano de Ensino Conteúdo programático N2N2 N1N1 1. Construindo os pipelines de dados: extrair, transformar e carregar 1.1 O que é engenharia de dados? (a) 1.2 Construindo nossa infraestrutura de engenharia de dados (b) 1.3 Leitura e gravação de arquivos 1.4 Trabalhando com bancos de dados (c, d) 1.5 Limpeza, transformação e enriquecimento de dados 1.6 Construindo o pipeline de dados (h) 2. Implantação de pipelines de dados na produção 2.1 Recursos de um pipeline em produção (f, g) 2.2 Controle de versão com o NiFi Registry (e) 2.3 Monitorando pipelines de dados 2.4 Implantando pipelines de dados 2.5 Construindo um pipeline de dados em produção (i) 3 Além do Lote (batch) - criando pipelines de dados em tempo real 3.1 Construindo um cluster Kafka (b) 3.2 Streaming de dados com Apache Kafka (e) 3.3 Processamento de dados com Apache Spark (e) 3.4 Dados de borda (edge) em tempo real com MiNiFi, Kafka e Spark 3.5 Construção, implantação e gerenciamento de pipelines: uma revisão geral Pág. 3 Engenharia de Dados De 66 ➢ Anteriormente aprendemos como construir pipelines de dados que podiam ler e gravar em arquivos e bancos de dados ➢ Em muitos casos, essas habilidades por si só permitirão que você crie pipelines de dados de produção ➢ Por exemplo, você lerá arquivos de um data lake e os inserirá em um banco de dados ➢ Às vezes, no entanto, você precisará fazer algo com os dados após a extração, mas antes do carregamento ➢ O que você precisa fazer é limpar os dados. Limpeza é um termo vago. ➢ Mais especificamente, você precisará verificar a validade dos dados e responder a perguntas como as seguintes: ✔ Está completo? ✔ Os valores estão dentro dos intervalos adequados? ✔ As colunas são do tipo adequado? ✔ Todas as colunas são úteis? Limpeza, Transformação e Enriquecimento de Dados O que significa limpar, transformar e enriquecer os dados? Pág. 4 Engenharia de Dados De 66 ➢ Execução de análise exploratória de dados em Python ➢ Lidar com problemas comuns de dados usando o pandas ➢ Limpeza de dados usando o Airflow Limpeza, Transformação e Enriquecimento de Dados O que vamos aprender? Pág. 5 Engenharia de Dados De 66 ➢ Antes de limpar seus dados, você precisa saber como eles se parecem ➢ Como engenheiro de dados, você não é o especialista no domínio e não é o usuário final dos dados, mas deve saber para que os dados serão usados e como seriam os dados válidos ➢ Por exemplo, você não precisa ser um demógrafo para saber que um campo de idade não deve ser negativo e a frequência de valores acima de 100 será baixa Limpeza, Transformação e Enriquecimento de Dados Executando análise exploratória de dados em Python Pág. 6 Engenharia de Dados De 66 ➢ Vamos utilizar dados reais de e-scooters (patinetes) da cidade de Albuquerque ➢ Os dados contêm viagens feitas com e-scooters em 22/06/2019 ➢ Você precisará baixar os dados dos e-scooters em https://github.com/giovanebarcelos/ED20212/blob/main/datasets/scooter.csv Limpeza, Transformação e Enriquecimento de Dados Baixando os dados https://github.com/giovanebarcelos/ED20212/blob/main/datasets/scooter.csv Pág. 7 Engenharia de Dados De 66 ➢ Antes de limpar seus dados, você precisa saber como eles se parecem ➢ O processo de compreensão de seus dados é chamado de análise exploratória de dados ➢ Você observará a forma de seus dados, o número de linhas e colunas, bem como os tipos de dados nas colunas e os intervalos de valores ➢ Pode realizar uma análise muito mais aprofundada, como a distribuição dos dados ou a assimetria ➢ Para começar, precisamos importar com o pandas e ler o arquivo .csv: import pandas as pd df=pd.read_csv('scooter.csv') ➢ Com os dados em um DataFrame, agora podemos explorá-los e, em seguida, analisá-los Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 8 Engenharia de Dados De 66 ➢ Agora você pode começar a examinar os dados ➢ A primeira coisa que você provavelmente desejará fazer é imprimi-lo ➢ Mas antes de chegar a isso, dê uma olhada nas colunas e nos tipos de dados usando columns e dtypes: Limpeza, Transformação e Enriquecimento de Dados Explorando os dados >>> df.columns Index(['month', 'trip_id', 'region_id', 'vehicle_id', 'started_at', 'ended_at', 'DURATION', 'start_location_name', 'end_location_name', 'user_id', 'trip_ledger_id'], dtype='object') >>> df.dtypes month object trip_id int64 region_id int64 vehicle_id int64 started_at object ended_at object DURATION object start_location_name object end_location_name object user_id int64 trip_ledger_id int64 dtype: object Pág. 9 Engenharia de Dados De 66 ➢ Você verá que tem onze colunas, cinco das quais são inteiros (todas as colunas com ID em seus nomes) e as demais são objetos ➢ Objetos são o que um DataFrame usa como dtype quando há tipos mistos ➢ Além disso, DURATION deve se destacar por ser o único nome de coluna em maiúsculas ➢ Em seguida corrigiremos os erros comuns, como os casos das colunas não são uniformes (todas em minúsculas ou maiúsculas) e fará com que os tipos de objeto dtypes sejam os tipos adequados, como strings para dados de texto e datetimes para datas e horas ➢ Agora que você sabe o que tem de colunas e tipos, vamos examinar os dados. Você pode imprimir os primeiros cinco registros usando head(): df.head() Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 10 Engenharia de Dados De 66 ➢ O oposto de head() é tail() ➢ Ambos os métodos padrão mostram 5 linhas ➢ No entanto, você pode passar um número inteiro como um parâmetro que especifica quantas linhas mostrar ➢ Por exemplo, você pode passar o head(10) para ver as primeiras 10 linhas ➢ Observe na saída head() e tail() que a terceira coluna é ..., e há mais duas colunas depois disso ➢ A tela está cortando as colunas do meio ➢ Se você fosse imprimir todo o DataFrame, a mesma coisa aconteceria com as linhas também ➢ Para exibir todas as colunas, você pode alterar o número de colunas a serem mostradas usando o método set_options: pd.set_option('display.max_columns', 500) Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 11 Engenharia de Dados De 66 ➢ Os métodos head e tail exibem todas as colunas, mas se você estiver interessado apenas em uma única coluna, poderá especificá-la como faria em um dicionário Python ➢ O código a seguir imprime a coluna DURATION: >>> df['DURATION'] 0 0:07:03 1 0:04:57 2 0:01:14 3 0:06:58 4 0:03:06 ... 34221 0:14:00 34222 0:08:00 34223 1:53:00 34224 0:12:00 34225 1:51:00 Name: DURATION, Length: 34226, dtype: object Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 12 Engenharia de Dados De 66 ➢ Assim como você pode exibir uma única coluna, você pode exibir uma lista de colunas usando double [], conforme mostrado no seguinte bloco de código: >>> df[['trip_id','DURATION','start_location_name']] trip_id DURATION start_location_name 0 1613335 0:07:03 1901 Roma Ave NE, Albuquerque, NM 87106, USA 1 1613639 0:04:57 1 Domenici Center en Domenici Center, Albuquer... 2 1613708 0:01:14 1 Domenici Center en Domenici Center, Albuquer... 3 1613867 0:06:58 Rotunda at Science & Technology Park, 801 Univ... 4 1636714 0:03:06 401 2nd St NW, Albuquerque, NM 87102, USA ... ... ... ... 34221 2482235 0:14:00 Central @ Broadway, Albuquerque, NM 87102, USA 34222 24822540:08:00 224 Central Ave SW, Albuquerque, NM 87102, USA 34223 2482257 1:53:00 105 Stanford Dr SE, Albuquerque, NM 87106, USA 34224 2482275 0:12:00 100 Broadway Blvd SE, Albuquerque, NM 87102, USA 34225 2482335 1:51:00 105 Stanford Dr SE, Albuquerque, NM 87106, USA [34226 rows x 3 columns] Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 13 Engenharia de Dados De 66 ➢ Você também pode extrair uma amostra de seus dados usando sample() ➢ Os métodos de amostra permitem que você especifique quantas linhas deseja extrair ➢ Os resultados são mostrados no seguinte bloco de código: >>> df.sample(5) month trip_id region_id ... end_location_name user_id trip_ledger_id 9906 June 1834177 202 ... 413 2nd St SW, Albuquerque, NM 87102, USA 35920330 1702781 11381 June 1858309 202 ... 1720 Central Ave SW, Albuquerque, NM 87104, USA 36647277 1725629 28090 July 2265900 202 ... 2101 Mountain Rd NW, Albuquerque, NM 87104, USA 42440628 2123432 32020 July 2396678 202 ... NaN 42227670 2256679 8006 June 1804406 202 ... 330 Tijeras Ave NW, Albuquerque, NM 87102, USA 37175121 1673778 [5 rows x 11 columns] ➢ Observe que o índice das linhas não é incremental, mas sim salta, pois é uma amostra Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 14 Engenharia de Dados De 66 ➢ Você também pode dividir os dados ➢ O fatiamento assume o formato de [início: fim], onde um espaço em branco é a primeira ou a última linha, dependendo de qual posição está em branco ➢ Para fatiar as primeiras 3 linhas, você pode usar a seguinte notação: >>> df[:3] month trip_id region_id ... user_id trip_ledger_id 0 May 1613335 202 ... 8417864 1488546 1 May 1613639 202 ... 8417864 1488838 2 May 1613708 202 ... 8417864 1488851 ➢ Da mesma forma, para pegar as linhas de 10 ao final (34.225), você pode usar a seguinte notação: df[10:] Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 15 Engenharia de Dados De 66 ➢ Você também pode fatiar o quadro começando na terceira linha e terminando antes da sexta, conforme mostrado no seguinte bloco de código: >>> df[3:6] month trip_id region_id ... user_id trip_ledger_id 3 May 1613867 202 ... 8417864 1489064 4 May 1636714 202 ... 35436274 1511212 5 May 1636780 202 ... 34352757 1511371 [3 rows x 11 columns] Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 16 Engenharia de Dados De 66 ➢ Às vezes, você sabe a linha exata que deseja e, em vez de fatiá-la, pode selecioná-la usando loc() ➢ O método loc leva o nome do índice, que, neste exemplo, é um inteiro ➢ O código e a saída a seguir mostram uma única linha selecionada com loc(): >>> df.loc[34221] month July trip_id 2482235 region_id 202 vehicle_id 2893981 started_at 7/21/2019 23:51 ended_at 7/22/2019 0:05 DURATION 0:14:00 start_location_name Central @ Broadway, Albuquerque, NM 87102, USA end_location_name 1418 4th St NW, Albuquerque, NM 87102, USA user_id 42559731 trip_ledger_id 2340035 Name: 34221, dtype: object Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 17 Engenharia de Dados De 66 ➢ Usando at(), com a posição, como fez nos exemplos de fatiamento, e um nome de coluna, você pode selecionar um único valor ➢ Por exemplo, isso pode ser feito para saber a duração da viagem na segunda linha: >>> df.at[2,'DURATION'] '0:01:14' Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 18 Engenharia de Dados De 66 ➢ Fatiar e usar loc() e at() extrai dados com base na posição, mas você também pode usar DataFrames para selecionar linhas com base em alguma condição ➢ Usando o método where, você pode passar uma condição, conforme mostrado no seguinte bloco de código: >>> user=df.where(df['user_id']==8417864) >>> user month trip_id region_id ... user_id trip_ledger_id 0 May 1613335.0 202.0 ... 8417864.0 1488546.0 1 May 1613639.0 202.0 ... 8417864.0 1488838.0 2 May 1613708.0 202.0 ... 8417864.0 1488851.0 ... ... ... ... ... ... ... 34221 NaN NaN NaN ... NaN NaN 34222 NaN NaN NaN ... NaN NaN 34223 NaN NaN NaN ... NaN NaN [34226 rows x 11 columns] Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 19 Engenharia de Dados De 66 ➢ O código e os resultados anteriores mostram os resultados de where com a condição do ID do usuário sendo igual a 8417864 ➢ Os resultados substituem os valores que não atendem aos critérios como NaN ➢ Isso será abordado adiante ➢ Pode-se obter os mesmos resultados semelhantes ao exemplo anterior, exceto pelo uso de uma notação diferente, e este método não incluirá as linhas NaN ➢ Você pode passar a condição para o DataFrame como fez com os nomes das colunas. O exemplo a seguir mostra como: df [(df ['user_id'] == 8417864)] ➢ Os resultados do código anterior são iguais aos do exemplo where(), mas sem as linhas NaN, portanto, o DataFrame terá apenas quatro linhas Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 20 Engenharia de Dados De 66 ➢ Usando ambas as notações, você pode combinar declarações condicionais ➢ Usando a mesma condição de ID de usuário, você pode adicionar uma condição de ID de viagem. O exemplo a seguir mostra como: ➢>>> one=df['user_id']==8417864 ➢>>> two=df['trip_ledger_id']==1488838 ➢>>> df.where(one & two) ➢ month trip_id region_id ... user_id trip_ledger_id ➢0 NaN NaN NaN ... NaN NaN ➢1 May 1613639.0 202.0 ... 8417864.0 1488838.0 ➢2 NaN NaN NaN ... NaN NaN ➢... ... ... ... ... ... ... ➢34221 NaN NaN NaN ... NaN NaN ➢34222 NaN NaN NaN ... NaN NaN Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 21 Engenharia de Dados De 66 ➢ Usando a segunda notação, a saída é a seguinte: >>> df[(one)&(two)] month trip_id region_id ... user_id trip_ledger_id 1 May 1613639 202 ... 8417864 1488838 [1 rows x 11 columns] ➢ Nos exemplos anteriores, as condições foram atribuídas a uma variável e combinadas na notação where e secundária, gerando os resultados esperados Limpeza, Transformação e Enriquecimento de Dados Exploração básica de dados Pág. 22 Engenharia de Dados De 66 ➢ Agora que você viu os dados, pode começar a analisá-los ➢ Usando o método describe, você pode ver uma série de estatísticas relativas aos seus dados ➢ Nas estatísticas, há um conjunto de estatísticas conhecido como resumo de cinco números, e describe() é uma variante disso: >>> df.describe() trip_id region_id vehicle_id user_id trip_ledger_id count 3.422600e+04 34226.0 3.422600e+04 3.422600e+04 3.422600e+04 mean 2.004438e+06 202.0 5.589507e+06 3.875420e+07 1.869549e+06 std 2 .300476e+05 0.0 2.627164e+06 4.275441e+06 2.252639e+05 min 1.613335e+06 202.0 1.034847e+06 1.080200e+04 1.488546e+06 25% 1.813521e+06 202.0 3.260435e+06 3.665710e+07 1.683023e+06 50%1.962520e+06 202.0 5.617097e+06 3.880750e+07 1.827796e+06 75% 2.182324e+06 202.0 8.012871e+06 4.222774e+07 2.042524e+06 max 2.482335e+06 202.0 9.984848e+06 4.258732e+07 2.342161e+06 Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 23 Engenharia de Dados De 66 ➢ O método de descrição (describe) não é muito útil, a menos que você tenha dados numéricos ➢ Se você estivesse observando as idades, por exemplo, isso mostraria rapidamente a distribuição das idades, e você seria capaz de ver rapidamente erros como idades negativas ou muitas idades acima de 100 ➢ O uso de describe() em uma única coluna às vezes é mais útil ➢ Vamos tentar olhar para a coluna start_location_name. O código e os resultados são mostrados no seguinte bloco de código: >>> df['start_location_name'].describe() count 34220 unique 2972 top 1898 Mountain Rd NW, Albuquerque, NM 87104, USA freq 1210 Name: start_location_name, dtype: object Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 24 Engenharia de Dados De 66 ➢ Os dados não são numéricos, portanto, obtemos um conjunto diferente de estatísticas, mas eles fornecem alguns insights ➢ Dos 34220 locais de partida, existem, na verdade, 2972 locais exclusivos ➢ O local superior (1898 Mountain Rd NW) é responsável por 1210 locais de início de viagem ➢ Posteriormente, você irá geocodificar esses dados - adicionar coordenadas ao endereço - e saber os valores exclusivos significa que você só precisa geocodificar aqueles 2.972 e não os 34.220 completos Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 25 Engenharia de Dados De 66 ➢ Outro método que permite ver detalhes sobre seus dados é value_counts ➢ O método value_counts fornecerá o valor e a contagem de todos os valores exclusivos ➢ Precisamos chamá-lo para uma única coluna, o que é feito no seguinte trecho: >>> df['DURATION'].value_counts() 0:04:00 825 0:03:00 807 0:05:00 728 0:06:00 649 0:07:00 627 ... 0:40:15 1 39:24:42 1 0:43:09 1 1:05:10 1 1:12:07 1 Name: DURATION, Length: 4135, dtype: int64 Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 26 Engenharia de Dados De 66 ➢ A partir desse método, você pode ver que 0:04:00 está no topo com uma frequência de 825 - que você poderia ter descoberto com describe () - mas também pode ver a frequência de todos os outros valores ➢ Para ver a frequência como uma porcentagem, você pode passar o parâmetro normalize (que é False por padrão): >>> df['DURATION'].value_counts(normalize=True) 0:04:00 0.025847 0:03:00 0.025284 0:05:00 0.022808 0:06:00 0.020333 ... 0:40:15 0.000031 39:24:42 0.000031 0:43:09 0.000031 1:05:10 0.000031 Name: DURATION, Length: 4135, dtype: float64 Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 27 Engenharia de Dados De 66 ➢ Pode-se notar que nenhum valor representa uma porcentagem significativa da duração ➢ Você também pode passar o parâmetro dropna ➢ Por padrão, value_counts() define como True e você não os verá. Definindo como False, você pode ver que end_location_name está sem 2070 entradas: >>> df['end_location_name'].value_counts(dropna=False) NaN 2070 1898 Mountain Rd NW, Albuquerque, NM 87104, USA 802 Central @ Tingley, Albuquerque, NM 87104, USA 622 330 Tijeras Ave NW, Albuquerque, NM 87102, USA 529 2550 Central Ave NE, Albuquerque, NM 87106, USA 478 ... 116 Washington St SE, Albuquerque, NM 87108, USA 1 716 Commercial St SE, Albuquerque, NM 87102, USA 1 119 Walter St SE, Albuquerque, NM 87102, USA 1 3801 Carlisle Blvd NE, Albuquerque, NM 87107, USA 1 Name: end_location_name, Length: 4264, dtype: int64 Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 28 Engenharia de Dados De 66 ➢ A melhor maneira de descobrir quantos valores ausentes você tem em suas colunas é usar o método isnull(). O código a seguir combina isnull() com sum() para obter as contagens: >>> df.isnull().sum() month 0 trip_id 0 region_id 0 vehicle_id 0 started_at 0 ended_at 0 DURATION 2308 start_location_name 6 end_location_name 2070 user_id 0 trip_ledger_id 0 dtype: int64 Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 29 Engenharia de Dados De 66 ➢ Outro parâmetro de value_counts() são bins ➢ O dataset da scooter não tem uma boa coluna para isso, mas usando uma coluna numérica, você obteria resultados como o seguinte: >>> df ['trip_id']. value_counts (bins = 10) (1787135.0, 1874035.0] 5561 (1700235.0, 1787135.0] 4900 (1874035.0, 1960935.0] 4316 (1960935.0, 2047835.0] 3922 (2047835.0, 2134735.0] 3296 (2221635.0, 2308535.0] 2876 (2308535.0, 2395435.0] 2515 (2134735.0, 2221635.0] 2490 (2395435.0, 2482335.0] 2228 (1612465.999, 1700235.0] 2122 Name: trip_id, dtype: int64 Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 30 Engenharia de Dados De 66 ➢ Esses resultados são bastante insignificantes, mas se for usado em uma coluna como idade, seria útil, pois você pode criar grupos de idade rapidamente e ter uma ideia da distribuição ➢ Agora que você explorou e analisou os dados, deve ter uma compreensão do que são os dados e quais são os problemas - por exemplo, nulos, dtypes impróprios, combinados e campos ➢ Com esse conhecimento, você pode começar a limpar os dados ➢ Em seguida será explicado como corrigir problemas comuns de dados Limpeza, Transformação e Enriquecimento de Dados Analisando os dados Pág. 31 Engenharia de Dados De 66 ➢ Seus dados podem parecer especiais, são únicos, você criou os melhores sistemas do mundo para coletá-los e fez tudo o que podia para garantir que fossem limpos e precisos ➢ Parabéns! Mas é quase certo que seus dados tenham alguns problemas, e esses problemas não são especiais ou únicos e são provavelmente resultado de seus sistemas ou da entrada de dados ➢ O conjunto de dados da e-scooter é coletado usando GPS com pouca ou nenhuma entrada humana, mas faltam localizações finais ➢ Como é possível que uma scooter tenha sido alugada, conduzida e parada, mas os dados não sabem onde ela parou? ➢ Parece estranho, mas aqui estamos ➢ Vamos aprender a lidar com problemas comuns de dados usando o conjunto de dados e-scooter Limpeza, Transformação e Enriquecimento de Dados Lidando com problemas comuns de dados usando o pandas Pág. 32 Engenharia de Dados De 66 ➢ Antes de modificar qualquer campo em seus dados, você deve primeiro decidir se vai usar todos os campos ➢ Olhando para os dados da e-scooter, há um campo denominado region_id ➢ Este campo é um código usado pelo fornecedor para rotular Albuquerque ➢ Como estamos usando apenas os dados de Albuquerque, não precisamos desse campo, pois ele não adiciona nada aos dados ➢ Você pode descartar colunas usando o método de eliminação ➢ O método permitirá que você especifique se deseja descartar uma linha ou uma coluna ➢ Linhas são o padrão, portanto, especificaremos colunas, conforme mostrado no seguinte bloco de código: df.drop(columns=['region_id'], inplace=True) Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 33 Engenharia de Dados De 66 ➢ Especificando as colunas a serem eliminadas, você também precisa adicionar inplace para fazê-lomodificar o DataFrame original ➢ Para eliminar uma linha, você só precisa especificar o índice em vez das colunas ➢ Para eliminar a linha com o índice de 34225, você precisa usar o seguinte código: df.drop(index=[34225],inplace=True) Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 34 Engenharia de Dados De 66 ➢ O código anterior funciona quando você deseja descartar uma coluna ou linha inteira, mas e se você quisesse eliminá-los com base em condições? ➢ A primeira condição que você pode querer considerar é onde há nulos. Se você não tiver dados, a coluna e a linha podem não ser úteis, ou podem distorcer os dados. Para lidar com isso, você pode usar dropna(). ➢ Ao usar dropna(), você pode passar o eixo, como, o limite, o subconjunto e o local nos parâmetros: ✔ axis especifica linhas ou colunas com índices ou colunas (0 ou 1). O padrão é linhas. ✔ how especifica se deve-se descartar linhas ou colunas se todos os valores forem nulos ou se algum valor for nulo (todos ou algum). O padrão é qualquer ✔ thresh permite mais controle do que permitir que você especifique um valor inteiro de quantos nulos devem estar presentes ✔ subset permite que você especifique uma lista de linhas ou colunas a serem pesquisadas ✔ inplace permite que você modifique o DataFrame existente. O padrão é False. Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 35 Engenharia de Dados De 66 ➢ Observando os dados da e-scooter, há seis linhas sem nome de local de início: >>> df['start_location_name'][(df['start_location_name'].isnull())] 26042 NaN 26044 NaN 26046 NaN 26048 NaN 26051 NaN 26053 NaN Name: start_location_name, dtype: object Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 36 Engenharia de Dados De 66 ➢ Para eliminar essas linhas, você pode usar dropna em axis = 0 com how = any, que são os padrões ➢ Isso, no entanto, excluirá as linhas onde existem outros nulos, como end_location_name ➢ Portanto, você precisará especificar o nome da coluna como um subconjunto, conforme mostrado no seguinte bloco de código: df.dropna(subset=['start_location_name'],inplace=True) ➢ Então, ao selecionar nulos no campo start_location_name como no bloco de código anterior, você obterá uma série vazia: >>> df['start_location_name'][(df['start_location_name'].isnull())] Series([], Name: start_location_name, dtype: object) Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 37 Engenharia de Dados De 66 ➢ Eliminar uma coluna inteira com base em valores ausentes só pode fazer sentido se uma certa porcentagem de linhas for nula ➢ Por exemplo, se mais de 25% das linhas forem nulas, você pode descartá-las ➢ Você pode especificar isso no limite usando algo como o seguinte código para o parâmetro de limite: thresh=int(len(df)*.25) ➢ Antes de mostrar filtros mais avançados para descartar linhas, você pode não querer descartar nulos ➢ Você pode querer preenchê-los com um valor. Você pode usar fillna() para preencher colunas ou linhas nulas: df.fillna(value='00:00:00',axis='columns') Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 38 Engenharia de Dados De 66 ➢ E se você quiser usar fillna(), mas usar valores diferentes dependendo da coluna? ➢ Você não gostaria de ter que especificar uma coluna toda vez e executar fillna() múltiplas vezes ➢ Pode-se especificar um objeto para mapear para o DataFrame e passá-lo como o parâmetro de valor ➢ No código a seguir, copiaremos as linhas em que os locais de início e término são nulos. Em seguida, criaremos um objeto de valor que atribui um nome de rua ao campo start_location_name e um endereço de rua diferente ao campo end_location_name ➢ Usando fillna(), passamos o valor para o parâmetro de valor e, em seguida, imprimimos essas duas colunas no DataFrame, mostrando a alteração: startstop=df[(df['start_location_name'].isnull())&(df['end_location_name'].isnull())] value={'start_location_name':'Start St.','end_location_name':'Stop St.'} startstop=startstop.fillna(value=value) startstop[['start_location_name','end_location_name']] Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 39 Engenharia de Dados De 66 ➢ Você pode descartar linhas com base em filtros mais avançados ➢ Por exemplo, se quiser descartar todas as linhas em que o mês é maio? ➢ Pode-se iterar por meio do DF e descartar se for maio ➢ Poderia também filtrar as linhas e passar o índice para o método drop ➢ É possível filtrar o DataFrame e passá-lo para um novo: >>> may=df[(df['month']=='May')] >>> may month trip_id region_id ... user_id trip_ledger_id 0 May 1613335 202 ... 8417864 1488546 1 May 1613639 202 ... 8417864 1488838 2 May 1613708 202 ... 8417864 1488851 ... ... ... ... ... ... ... 4220 May 1737356 202 ... 35714580 1608429 4221 May 1737376 202 ... 37503537 1608261 4222 May 1737386 202 ... 37485128 1608314 [4225 rows x 11 columns] Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 40 Engenharia de Dados De 66 ➢ Também pode-se usar o drop() no DataFrame original e passar o índice para as linhas no DataFrame de may criado anteriormente, como mostrado: df.drop(index=may.index,inplace=True) ➢ Agora, se você olhar para os meses no DataFrame original, verá que está faltando maio: >>> df['month'].value_counts() June 20259 July 9742 Name: month, dtype: int64 ➢ Agora que você removeu as linhas e colunas desnecessárias ou inutilizáveis devido à falta de dados, é hora de formatá-las Limpeza, Transformação e Enriquecimento de Dados Eliminando linhas e colunas Pág. 41 Engenharia de Dados De 66 ➢ A primeira coisa que se destacou anteriormente foi que havia uma única coluna, duration, que estava toda em maiúsculas ➢ A capitalização é um problema comum ➢ Muitas vezes você encontrará colunas com todas as letras maiúsculas ou com letras maiúsculas - onde a primeira letra de cada palavra é maiúscula - e se um codificador a escreveu, você pode encontrar letras maiúsculas - onde a primeira letra é minúscula e a primeira letra da próxima palavra é maiúscula sem espaços, como em camelCase ➢ O código a seguir tornará todas as colunas em letras minúsculas: >>> df.columns=[x.lower() for x in df.columns] >>> print(df.columns) Index(['month', 'trip_id', 'region_id', 'vehicle_id', 'started_at', 'ended_at', 'duration', 'start_location_name', 'end_location_name', 'user_id', 'trip_ledger_id'], dtype='object') Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 42 Engenharia de Dados De 66 ➢ O código anterior é uma versão condensada de um loop for ➢ O que acontece no loop vem antes do loop for ➢ O código anterior diz que, para cada item em df.columns, torne-o em minúsculas e atribua-o de volta a df.columns ➢ Você também pode usar capitalize(), que é titlecase, ou upper() conforme mostrado: >>> df.columns=[x.upper() for x in df.columns] >>> print(df.columns) Index(['MONTH', 'TRIP_ID', 'REGION_ID', 'VEHICLE_ID', 'STARTED_AT', 'ENDED_AT','DURATION', 'START_LOCATION_NAME', 'END_LOCATION_NAME', 'USER_ID','TRIP_LEDGER_ID'], dtype='object') Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 43 Engenharia de Dados De 66 ➢ Você notará um parâmetro no local definido como True ➢ Quando usamos psycopg2 para modificar bancos de dados, precisamos usar conn.commit() para torná-lo permanente, e você precisa fazer o mesmo com DataFrames ➢ Quando se modifica um DataFrame, o resultado é retornado ➢ Você pode armazenar esse novo DataFrame (resultado) em uma variável e o DataFrame original permanece inalterado ➢ Se você deseja modificar o DataFrameoriginal e não atribuí-lo a outra variável, deve usar o parâmetro inplace ➢ O método de renomeação funciona para corrigir o caso dos nomes das colunas, mas não é a melhor escolha ➢ É melhor usado para realmente alterar nomes de colunas múltiplas ➢ Você pode passar um objeto com remapeamento de vários nomes de coluna ➢ Por exemplo, você pode remover o sublinhado em region_id usando renomear ➢ No próximo código alteraremos a coluna DURATION para minúsculas e removemos o sublinhado em region_id Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 44 Engenharia de Dados De 66 ➢ Neste código alteramos a coluna DURATION para minúsculas e removemos o sublinhado em region_id df.rename(columns={'DURATION':'duration','region_id':'region'},inplace =True) ➢ É bom conhecer diferentes maneiras de realizar a mesma tarefa e você pode decidir qual faz mais sentido para o seu caso de uso ➢ Agora que você aplicou as alterações aos nomes das colunas, também pode aplicar essas funções aos valores nas colunas ➢ Em vez de usar df.columns, você especificará qual coluna modificar e, em seguida, se deseja torná-la upper(), lower() ou capitalize() ➢ No seguinte snippet de código, tornamos a coluna do mês toda em maiúsculas: df['month']=df['month'].str.upper() df['month'].head() Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 45 Engenharia de Dados De 66 ➢ Pode não importar a capitalização nos nomes das colunas ou nos valores ➢ No entanto, é melhor ser consistente ➢ No caso dos dados da scooter, ter um nome de coluna em maiúsculas, enquanto o resto era todo inferior, ficaria confuso ➢ Imagine um cientista de dados consultando dados de vários bancos de dados ou de seu data warehouse e tendo que lembrar que todas as suas consultas precisavam levar em conta o campo de duração em letras maiúsculas e, quando eles se esqueciam, seu código falhava ➢ Você pode adicionar dados ao DataFrame criando colunas usando o formato df['nome da nova coluna'] = valor ➢ O formato anterior criaria uma nova coluna e atribuiria o valor a cada linha ➢ Você pode iterar por meio de um DataFrame e adicionar um valor com base em uma condição, como no exemplo a seguir: Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 46 Engenharia de Dados De 66 ➢ Você pode iterar por meio de um DataFrame e adicionar um valor com base em uma condição, como no exemplo a seguir: for i,r in df.head().iterrows(): if r['trip_id']==1613335: df.at[i,'new_column']='Yes' else: df.at[i,'new_column']='No' df[['trip_id','new_column']].head() ➢ A iteração por meio de DataFrames funciona, mas pode ser muito lenta. Para realizar a mesma coisa que o exemplo anterior, mas com mais eficiência, você pode usar loc() e passar a condição, o nome da coluna e o valor. O exemplo a seguir mostra o código e os resultados: df.loc[df['trip_id']==1613335,'new_column']='1613335' df[['trip_id','new_column']].head() Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 47 Engenharia de Dados De 66 ➢ Outra maneira de criar colunas é dividir os dados e, em seguida, inseri-los no DataFrame ➢ Você pode usar str.split() em uma série para dividir o texto em qualquer separador, ou um pat(abreviação de pattern) como o parâmetro é chamado ➢ Pode-se especificar quantas divisões deseja que ocorram, -1 e 0 significam todas as divisões, mas qualquer número inteiro é permitido ➢ Por exemplo, se você tem 1.000.000 e deseja apenas dois itens, pode dividir (2) na vírgula e obter 1 e 000.000 ➢ Você também pode expandir as divisões em colunas usando (expand = True). ➢ Se você não definir a expansão como True, obterá uma lista na coluna, que é o padrão ➢ Além disso, se você não especificar um separador, serão usados espaços em branco. Os padrões são mostrados: df['started_ad=df[['trip_id','started_at']].head() df['started_at'].str.split() df Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 48 Engenharia de Dados De 66 ➢ Você pode expandir os dados e passá-los para uma nova variável ➢ Em seguida, você pode atribuir as colunas a uma coluna no DataFrame original ➢ Por exemplo, se você quiser criar uma coluna de data e hora, poderá fazer o seguinte: Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas >>> new=df['started_at'].str.split(expand=True) >>> new 0 1 4225 6/1/2019 0:00 4226 6/1/2019 0:01 ... ... ... 34221 7/21/2019 23:51 34222 7/21/2019 23:52 [30001 rows x 2 columns] >>> df['date']=new[0] >>> df['time']=new[1] >>> df month trip_id region_id ... date time 4225 June 1737416 202 ... 6/1/2019 0:00 4226 June 1737432 202 ... 6/1/2019 0:01 ... ... … ... ... ... ... 34221 July 2482235 202 ... 7/21/2019 23:51 34222 July 2482254 202 ... 7/21/2019 23:52 [30001 rows x 14 columns] Pág. 49 Engenharia de Dados De 66 ➢ A coluna started_at é um objeto e, olhando para ela, deve ficar claro que é um objeto datetime ➢ Se você tentar filtrar no campo started_at usando uma data, ele retornará todas as linhas, conforme mostrado: >>> when = '2019-05-23' >>> x=df[(df['started_at']>when)] >>> len(x) 34226 ➢ O comprimento de todo o DataFrame é 34226, portanto, o filtro retornou todas as linhas. Não era isso que queríamos Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 50 Engenharia de Dados De 66 ➢ Usando to_datetime(), você pode especificar a coluna e o formato ➢ Você pode atribuir o resultado à mesma coluna ou especificar um novo ➢ No exemplo a seguir, a coluna started_at é substituída pelo novo tipo de dados datetime: df['started_at']=pd.to_datetime(df['started_at'], format='%m/%d/%Y %H:%M') df.dtypes ➢ Agora, a coluna started_at é um tipo de dados datetime e não um objeto ➢ Com isto pode-se executar consultas usando datas, como tentamos anteriormente no DataFrame completo e falhou: when = '2019-05-23' df[(df['started_at']>when)] trip_id started_at 4 1636714 2019-05-24 13:38:00 ➢O restante das linhas ocorreu em 21/05/2019, portanto, obtivemos os resultados que esperávamos ➢ Agora que você pode adicionar e remover linhas e colunas, substituir nulos e criar colunas, vamos aprender como enriquecer seus dados com fontes externas Limpeza, Transformação e Enriquecimento de Dados Criação e modificação de colunas Pág. 51 Engenharia de Dados De 66 ➢ Os dados da e-scooter são dados geográficos - contêm localizações - mas carecem de coordenadas ➢ Se você deseja mapear ou realizar consultas espaciais nesses dados, você precisará de coordenadas ➢ Você pode obter coordenadas geocodificando o local ➢ Por sorte, a cidade de Albuquerque tem um geocodificador público que podemos usar ➢ Para este exemplo, vamos pegar um subconjunto dos dados ➢ Usaremos os cinco principais locais de partida mais frequentes ➢ Em seguida, os colocaremos em um DataFrame usando o seguinte código: new=pd.DataFrame(df['start_location_name'].value_counts().head()) new.reset_index(inplace=True) new.columns=['address','count'] new Limpeza, Transformação e Enriquecimento de Dados Enriquecimento de dados Pág. 52 Engenharia de Dados De 66 ➢ O campo de endereço contém mais informações do que precisamos para geocodificar ➢ Precisamos apenas do endereço da rua ➢ Você também notará que o segundo registro é uma interseção - Central @ Tingley ➢ O geocodificador vai querer a palavra e entre as ruas ➢ Vamos limpar os dados e colocá-los em sua própria coluna: n=new['address'].str.split(pat=',',n=1,expand=True) replaced=n[0].str.replace("@","and") new['street']=n[0] new['street']=replaced new Limpeza, Transformaçãoe Enriquecimento de Dados Enriquecimento de dados Pág. 53 Engenharia de Dados De 66 ➢ Agora você pode iterar por meio do DataFrame e geocodificar o campo rua ➢ Para esta seção, você usará outro CSV e o unirá ao DataFrame ➢ Você pode enriquecer os dados combinando-os com outras fontes de dados ➢ Assim como você pode juntar dados de duas tabelas em um banco de dados, você pode fazer o mesmo com um DataFrame do pandas ➢ Você pode baixar o arquivo geocodedstreet.csv do repositório GitHub em https://github.com/giovanebarcelos/ED20212/blob/main/datasets/geocodedstreet.csv ➢ Carregue os dados usando pd.read_csv() e você terá um DataFrame com uma coluna de rua, bem como uma coluna para as coordenadas x e y. O resultado é mostrado da seguinte forma: geo=pd.read_csv('geocodedstreet.csv') geo Limpeza, Transformação e Enriquecimento de Dados Enriquecimento de dados https://github.com/giovanebarcelos/ED20212/blob/main/datasets/geocodedstreet.csv Pág. 54 Engenharia de Dados De 66 ➢ Para enriquecer o DataFrame original com esses novos dados, você pode unir ou mesclar os DataFrames ➢ Usando uma junção, você pode começar com um DataFrame e adicionar o outro como parâmetro ➢ Você pode passar como juntar usando left, right ou inner, assim como faria no SQL ➢ Você pode adicionar um sufixo esquerdo e direito para que as colunas que se sobrepõem possam determinar de onde vieram ➢ Unimos os dois DataFrames no seguinte exemplo: joined=new.join(other=geo,how='left',lsuffix='_new',rsuffix='_geo') joined[['street_new','street_geo','x','y']] Limpeza, Transformação e Enriquecimento de Dados Enriquecimento de dados Pág. 55 Engenharia de Dados De 66 ➢ A coluna da rua está duplicada e possui um sufixo à esquerda e à direita ➢ Isso funciona, mas é desnecessário, e acabaríamos descartando uma coluna e renomeando a coluna restante, o que é apenas um trabalho extra ➢ Você pode usar mesclar para unir os DataFrames em uma coluna e não ter duplicatas ➢ Merge permite que você passe os DataFrames para mesclar, bem como o campo para juntar, como mostrado: >>> merged=pd.merge(new,geo,on='street') >>> merged.columns Index(['address', 'count', 'street', 'x', 'y'], dtype='object') Limpeza, Transformação e Enriquecimento de Dados Enriquecimento de dados Pág. 56 Engenharia de Dados De 66 ➢ Observe como os novos campos x e y do merge vieram para o novo DataFrame, mas há apenas uma única coluna de rua ➢ Isso é muito mais limpo ➢ Em ambos os casos, unidos ou mesclados, você só pode usar o índice se ele estiver definido em ambos os DataFrames ➢ Agora que você sabe como limpar, transformar e enriquecer os dados, é hora de reunir essas habilidades e construir um pipeline de dados usando esse novo conhecimento ➢ Em seguida usaremos o Airflow e o NiFi para construir um pipeline de dados Limpeza, Transformação e Enriquecimento de Dados Enriquecimento de dados Pág. 57 Engenharia de Dados De 66 ➢ Agora que você pode limpar seus dados em Python, pode criar funções para realizar diferentes tarefas ➢ Ao combinar as funções, você pode criar um pipeline de dados no Airflow ➢ O exemplo a seguir limpará os dados e, em seguida, os filtrará e gravará no disco ➢ Começando com o mesmo código do Airflow que você usou nos exemplos anteriores, configure as importações e os argumentos padrão, conforme mostrado: Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow Pág. 58 Engenharia de Dados De 66 import datetime as dt from datetime import timedelta from airflow import DAG from airflow.operators.bash_operator import BashOperator from airflow.operators.python_operator import PythonOperator import pandas as pd default_args = { 'owner': 'penelopecharmosa', 'start_date': dt.datetime(2021, 8, 13), 'retries': 1, 'retry_delay': dt.timedelta(minutes=5), } Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow – Código Python Pág. 59 Engenharia de Dados De 66 ➢ Agora você pode escrever as funções que realizarão as tarefas de limpeza ➢ Primeiro, você precisa ler o arquivo, depois pode eliminar o ID da região, converter as colunas em minúsculas e alterar o campo started_at para um tipo de dados datetime ➢ Por último, grave as alterações em um arquivo. A seguir está o código: def cleanScooter(): df=pd.read_csv('scooter.csv') df.drop(columns=['region_id'], inplace=True) df.columns=[x.lower() for x in df.columns] df['started_at']=pd.to_datetime(df['started_at'], format='%m/%d/%Y %H:%M') df.to_csv('cleanscooter.csv') Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow Pág. 60 Engenharia de Dados De 66 ➢ Em seguida, o pipeline lerá os dados limpos e filtrará com base em uma data de início e término. O código é o seguinte: def filterData(): df=pd.read_csv('cleanscooter.csv') fromd = '2019-05-23' tod='2019-06-03' tofrom = df[(df['started_at']>fromd)&(df['started_at']<tod)] tofrom.to_csv('may23-june3.csv') Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow Pág. 61 Engenharia de Dados De 66 ➢ Essas duas funções devem parecer familiares, pois o código é linha por linha igual aos exemplos anteriores, apenas reagrupado ➢ Em seguida, você precisa definir os operadores e tarefas ➢ Você usará PythonOperator e apontará para suas funções ➢ Crie o DAG e as tarefas conforme mostrado: with DAG('CleanData', default_args=default_args, schedule_interval=timedelta(minutes=5), # '0 * * * *', ) as dag: cleanData = PythonOperator(task_id='clean', python_callable=cleanScooter) selectData = PythonOperator(task_id='filter', python_callable=filterData) Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow Pág. 62 Engenharia de Dados De 66 ➢ Neste exemplo, adicionaremos outra tarefa usando o BashOperator novamente ➢ Anteriormente apenas para imprimimos uma mensagem no terminal ➢ Desta vez, você o usará para mover o arquivo da tarefa selectData e copiá-lo para a área de trabalho. O código é o seguinte: moveFile = BashOperator( task_id='move', bash_command='mv may23-june3.csv /path/.') Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow Pág. 63 Engenharia de Dados De 66 ➢ O comando anterior apenas usa o comando de cópia do Linux para fazer uma cópia do arquivo ➢ Ao trabalhar com arquivos, você precisa ter cuidado para que suas tarefas possam acessá-los ➢ Se vários processos tentarem tocar no mesmo arquivo ou um usuário tentar acessá-lo, você poderá interromper o pipeline ➢ Por último, especifique a ordem das tarefas - crie a direção do DAG conforme mostrado: cleanData >> selectData >> copyFile Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow Pág. 64 Engenharia de Dados De 66 ➢ Agora inicialize o webserver e scheduler do airflow ➢ Agora você pode navegar até http://localhost:8080/ admin para visualizar a GUI. Selecione seu novo DAG e poderá ativá-lo e executá-lo. Na captura de tela a seguir, você verá o DAG e as execuções de cada tarefa: Limpeza, Transformação e Enriquecimento de Dados Limpeza de dados usando o Airflow Pág. 65 Engenharia de Dados De 66 ➢ Aprendemos a realizar o EDA básico com o objetivo de localizar erros ou problemas em seus dados ➢ Em seguida, você aprendeu como limpar seus dados e corrigir problemas comuns de dados ➢ Com esse conjunto de habilidades, você construiu um pipeline de dados no Apache Airflow ➢ Em seguida percorreremos um projeto, construindo um pipeline de dados 311 e um painel em Kibana ➢ Este projeto utilizará todas as habilidades que você adquiriu até este ponto e apresentará uma série de novas habilidades- como construir painéis e fazer chamadas de API Limpeza, Transformação e Enriquecimento de Dados Resumo Pág. 66 Engenharia de Dados De 66 Lembre-seLembre-se “Havia 5 exabytes de informações criados entre o início da civilização até 2003, mas essa quantidade de informações agora é criada a cada dois dias.” Eric Schmidt Slide 1 Slide 2 Slide 3 Slide 4 Slide 5 Slide 6 Slide 7 Slide 8 Slide 9 Slide 10 Slide 11 Slide 12 Slide 13 Slide 14 Slide 15 Slide 16 Slide 17 Slide 18 Slide 19 Slide 20 Slide 21 Slide 22 Slide 23 Slide 24 Slide 25 Slide 26 Slide 27 Slide 28 Slide 29 Slide 30 Slide 31 Slide 32 Slide 33 Slide 34 Slide 35 Slide 36 Slide 37 Slide 38 Slide 39 Slide 40 Slide 41 Slide 42 Slide 43 Slide 44 Slide 45 Slide 46 Slide 47 Slide 48 Slide 49 Slide 50 Slide 51 Slide 52 Slide 53 Slide 54 Slide 55 Slide 56 Slide 57 Slide 58 Slide 59 Slide 60 Slide 61 Slide 62 Slide 63 Slide 64 Slide 65 Slide 66
Compartilhar