<jupyter_start><jupyter_text>Pré-processamentoEste notebook é responsável por pré-processar os dados realizando as seguintes tarefas:- Tratamento de dados ausentes- Definição de tipos- Normalização e discretização- Limpeza de dados (univariado, bivariado e multivariado)Este notebook usa o 'clean_vehicles.csv' como dataset, que já não tem mais as colunas 'county', 'region_url' e 'image_url'.<jupyter_code>from Utils import *
from tqdm import tqdm

df = pd.read_csv(CLEAN_VEHICLES_DATASET)<jupyter_output><empty_output><jupyter_text>Definição de tiposOlhando para a tabela que descreve o dataset em '1-initial-analysis.ipynb' e pela análise feita no notebook, os dados foram definidos nos formatos abaixo. Colunas categóricos- type- region- transmission- manufacturer- model- condition- cylinders- fuel- title_status- drive- size (removido)- paint_color- state Colunas do tipo inteiro- id (removida)- price- year- odometer Coluna do tipo float- lat- long Colunas do tipo object- url- description- vin (removida)  Tratamento de dados ausentes ou nulosAntes de podermos definir os tipos dos dados vamos primeiro lidar com os dados nulos ou ausentes.<jupyter_code>df.info()
# Verifica quais dados são nulos.
df.isnull().sum()<jupyter_output><empty_output><jupyter_text>Foi notado que as colunas 'vin' (código único do veículo, não será útil) e 'size' ('type' já fornece a mesma informação) apresentam muitos dados nulos, mais de 50% do tamanho do dataset. Elas serão removidas. A coluna 'id' também será removida pois não será útil, é apenas um identificador da oferta no craig list.<jupyter_code>df.drop(columns=['vin', 'size', 'id'], inplace=True)
df.columns<jupyter_output><empty_output><jupyter_text>Removendo linhas com muitos valores vaziosDeixa no DataFrame apenas amostras que contém valores não-nulos em pelo 14 colunas (75% do número total de colunas) das 19 colunas. Reduzindo em 9% o tamanho da base.<jupyter_code>drop_thresh = int(len(df.columns) * 0.75)
original_size = len(df)
df.dropna(thresh=drop_thresh, inplace=True)
print("Redução de %.2f%% no tamanho da base." % ((1 - (len(df)/original_size)) * 100))<jupyter_output>Redução de 8.94% no tamanho da base.<jupyter_text>Tratamento de dados ausentes em Dados do tipo inteiroPara os dados do tipo inteiro foi verificado que existe dados nulos apenas nas coluna 'year' e 'odometer'.  Coluna 'year'Poucas amostras possui valor nulo na coluna 'year', representando 0,2% do dataset. Como o ano é um fator importante para a compra e venda de carros, queremos manter essas amostras.Foi feita uma análise e notou-se que nesses casos o ano é na maioria das vezes os 4 primeiros dígitos da descrição seguidos da marca e modelo do carro. Usamos a função 'get_year_from_description' para realizar esse processo, a definição da função está no Utils.py.<jupyter_code>df['year'].isnull().sum()
# mostra os 20 primeiros caracteres da descrição das linhas que possuem a coluna ano nula
for elem in df[df['year'].isnull().values]['description']:
    try:
        print(elem[0:40])
    except:
        print(elem)
get_year_from_description(df)
df.dropna(subset=['description'], inplace=True)
df.isnull().sum()
df[df['year'].isnull()]<jupyter_output><empty_output><jupyter_text>Sobram três linhas em que o ano é nulo:<jupyter_code>for elem in df[df['year'].isnull()]['description']:
    print(elem)
    print('\n')<jupyter_output>03 jeep liberty 2 wheel drive v6 runs good cold air and hot heat all windows work real nice on the inside and out it does have a reconstructed title 423twofivetwosevenzeroninesix


92 toyota 4x4 king cab automatic, v6, JBL  blue tooth stereo, clean title, 3rd owner, no accident, runs great


Nice truck 4wD. Everything works new brakes 80 percent tires 4.6 v8<jupyter_text>Pela descrição so consegue-se informação do ano para linha com indice 170030 e 15321, o toyota de ano 1992 e o jeep de 2003, respectivamente.<jupyter_code>df.at[170030, 'year'] = 1992
df.at[15321, 'year'] = 2003<jupyter_output><empty_output><jupyter_text>As outra linha vamos descartar<jupyter_code>df.dropna(subset=['year'], inplace=True)
df.isnull().sum()<jupyter_output><empty_output><jupyter_text>O ano foi inferido como float64 pelo pandas, verificamos se há ou não casas decimais no com a função 'get_decimal' definida no script Utils.py para cada linha do dataframe:<jupyter_code>df['year'].dtype<jupyter_output><empty_output><jupyter_text>Como a serie retornada é vazia, todos os anos são números inteiros.<jupyter_code>teste = df['year'].apply(get_decimal)
teste = teste[teste > 0]
teste<jupyter_output><empty_output><jupyter_text>Coluna 'odometer'Vamos tentar pegar informações sobre o odômetro na descrição da oferta.<jupyter_code># quantidade de nulos na coluna odometer
df['odometer'].isnull().sum()<jupyter_output><empty_output><jupyter_text>miles_df contém todas entradas com coluna odometer nula e que a descrição contém a alguma informação sobre as milhas do carro<jupyter_code>miles_df = df[df['odometer'].isnull() & df['description'].str.contains('[Mm][Ii][Ll][Ee][Ss]*|[Mm][Ii][Ll][Ee][Aa][Gg][Ee]')]
miles_df
# tenta obter informação sobre as milhas de um carro na descrição do anúncio
get_odometer_from_description(df)
df.isnull().sum()
# verifica se há amostras do odometro que possam conter casas decimais
teste = df[df['odometer'].notnull()]['odometer'].apply(get_decimal)
teste = teste[teste > 0]
teste<jupyter_output><empty_output><jupyter_text>Para as linhas restantes que ainda possuem o valor 'odometer' nulo, atribuimos o valor de -1.<jupyter_code>df['odometer'].fillna(-1, inplace=True)
df['odometer'].isnull().sum()<jupyter_output><empty_output><jupyter_text>Tratamento de dados ausentes em Dados categóricosPara os dados categóricos iremos criar uma nova categoria 'undefined' para substituir os dados ausentes.<jupyter_code>categoric_types = ['type', 'region', 'transmission', 'manufacturer', 'model', 'condition', 'cylinders', 'fuel', 'title_status', 'drive', 'paint_color', 'state']

for column in categoric_types:
    df[column] = df[column].fillna('undefined')
# confirmando que não há mais amostras com dados categoricos nulos no novo dataframe
df.isnull().sum()<jupyter_output><empty_output><jupyter_text>Tratamento de dados ausentes em Dados do tipo objectComo retiramos a coluna 'vin' do dataset, não há mais colunas de tipo object com valores nulos.<jupyter_code>df[['url', 'description']].isnull().sum()<jupyter_output><empty_output><jupyter_text>Tratamento de dados ausentes em Dados do tipo FloatOs únicos dados floats nulos no dataset são as coordenadas (latitude e longitude). Latitude e LongitudePara as colunas de latitude e longitude, decidimos pegar as coordenadas dos estados nas linhas que possuiam latitude ou longitude nulas. Foi usado o módulo 'Nominatin' da biblioteca 'geopy', esse módulo permite obter informações como endereço, latitude, longitude e outros dados de localização a partir de uma string passada contendo o nome do local a ser consultado.Para nosso projeto listamos todos os 51 estados dos EUA, eles estão definidos em formato de dicionário no arquivo 'Constants.py' na variável STATES_DICT. Usando esse dicionário fizemos chamadas ao módulo 'Nominatin' para obter as coordenadas centrais de cada um dos estados dos Estados Unidos. Finalmente, substituímos os dados nulos de coordenadas no dataframe por esses retornados pelo módulo 'Nominatin'. A função responsável é 'put_coordinates_in_dataframe' e está no 'Utils.py'.<jupyter_code>df[['lat', 'long']].isnull().sum()
null_coords_df = df[df['lat'].isnull() & df['long'].isnull()]
null_coords_df
put_coordinates_in_dataframe(df)
df.isnull().sum()<jupyter_output><empty_output><jupyter_text>Definindo os tipos dos dados no dataframeCom todos os dados nulos devidamente tratados podemos setar os tipos dos dados no dataframe.<jupyter_code>set_df_vehicles_data_types(df)
df.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 396901 entries, 0 to 396900
Data columns (total 19 columns):
 #   Column        Non-Null Count   Dtype   
---  ------        --------------   -----   
 0   url           396901 non-null  object  
 1   region        396901 non-null  category
 2   price         396901 non-null  int64   
 3   year          396901 non-null  int64   
 4   manufacturer  396901 non-null  category
 5   model         396901 non-null  category
 6   condition     396901 non-null  category
 7   cylinders     396901 non-null  category
 8   fuel          396901 non-null  category
 9   odometer      396901 non-null  int64   
 10  title_status  396901 non-null  category
 11  transmission  396901 non-null  category
 12  drive         396901 non-null  category
 13  type          396901 non-null  category
 14  paint_color   396901 non-null  category
 15  description   396901 non-null  object  
 16  state         396901 non-null  category
 17  lat           396901 non-null[...]<jupyter_text>Limpeza de dados<jupyter_code># Nenhum registro duplicado encontrado
df[df.duplicated()]<jupyter_output><empty_output><jupyter_text>Limpando coluna 'manufacturer'A coluna 'manufacturer' possui cerca de 15mil registros listados como 'undefined', que é equivalente a nulo. Criamos um método, 'get_brand_from_description', para tentar pegar informações na descrição do registro e então substituir o valor de manufacturer.<jupyter_code>df['manufacturer'].value_counts()
get_brand_from_description(df)
# após execução do método, conseguimos recuperar informação sobre a marca de cerca de 4mil registros.
df['manufacturer'].value_counts()<jupyter_output><empty_output><jupyter_text>Limpando coluna 'odometer' (univariado)Para limpar os outliers da coluna 'odometer' foi utilizado o o método z-score.<jupyter_code># Usando o log para melhor visualizar os dados
df['log_odometer'] = df['odometer'].apply(np.log10)
df[df['log_odometer'] >= 0]['log_odometer'].hist()
df[df['log_odometer'] >= 0].boxplot(column=['log_odometer'])
# Dataframe dos registros que possuem um z-score menor que 3.5 para a coluna 'log_odometer'
z_score_df = evaluate_df_z_score_for_column(df, 'log_odometer')
# É necessário colocar de volta os registros de carros com zero milhas, esses não são outliers
zero_miles_cars_df = df[df['odometer'] == 0]
z_score_df = z_score_df.append(zero_miles_cars_df)
z_score_df[z_score_df['log_odometer'] >= 0]['log_odometer'].hist()
z_score_df[z_score_df['log_odometer'] >= 0].boxplot(column=['log_odometer'])
# Redução de 9% no tamanho da base de dados após remover os outliers
len(z_score_df)/len(df)
# Remove a coluna de 'log_odometer' criada para a análise
z_score_df.drop(columns=['log_odometer'], inplace=True)
# Atribuindo o dataframe limpo ao nosso dataframe inicial
df = z_score_df
df<jupyter_output><empty_output><jupyter_text>Limpando coluna 'price' (univariado)Semelhante à coluna 'odometer', aplicou-se o z-score para tratar os dados da coluna 'odometer'.<jupyter_code>df['log_price'] = df['price'].apply(np.log10)
df[df['log_price'] >= 0]['log_price'].hist()
df[df['log_price'] >= 0].boxplot(column=['log_price'])
# Dataframe dos registros que possuem um z-score menor que 3.5 para a coluna 'log_price'
z_score_df_price = evaluate_df_z_score_for_column(df, 'log_price')
# Não faz sentido ter carros com preço igual a zero, logo serão removidos.
df[df['price'] == 0]
# Não há mais amostras com preço igual a zero no novo dataframe.
z_score_df_price[z_score_df_price['price'] == 0]
z_score_df_price['log_price'].hist()
z_score_df_price.boxplot(column=['log_price'])
# Redução de cerca de 9% no tamanho do dataset
len(z_score_df_price)/len(df)
# Remove a coluna de 'log_price' criada para a análise
z_score_df_price.drop(columns=['log_price'], inplace=True)
# Atribuindo o dataframe limpo ao nosso dataframe inicial
df = z_score_df_price
df<jupyter_output><empty_output><jupyter_text>Limpando colunas 'year' e 'odometer' (bivariado)Fizemos uma media de milhas rodadas por ano de uso de cada carro, criando assim a coluna 'miles_per_year' que é quantidade média que cada carro rodou em um ano. Dessa forma podemos fazer uma limpeza bivariada usando mais uma vez o z-score robusto.Pesquisamos e vimos que a média de milhas rodadas por ano dos carros dos EUA é entre 11.000 a 13.500 milhas, no nosso dataset, tivemos uma média de cerca de 11.077 milhas. Após a limpeza a média das milhas por ano foi para cerca 11.200 milhas.Os carros no craig list são carros usados, então vamos remover as instâncias com 'odometer' = 0.<jupyter_code># Verificando que não há nenhum carro com ano maior ou igual a 2021
len(df[df['year'] >= 2021])
# Verificando que os carros que possuem mais milhas estão entre 17 e 20 anos de uso
df['age'] = 2021 - df['year']
df.plot.scatter(x='age', y='odometer', figsize=(15,10))
# criando a coluna de 'milhas por ano' e a coluna do 'log de milhas por ano' para melhor visualizar os dados
df['miles_per_year'] = df['odometer'] / df['age']
df['log_miles_per_year'] = df['miles_per_year'].apply(np.log10)
df['miles_per_year'].describe()
# Mostra que o log_miles_per_year possui uma distribuição simétrica
df[df['log_miles_per_year'] >= 0]['log_miles_per_year'].hist(bins=40)
z_score_df = evaluate_df_z_score_for_column(df, 'log_miles_per_year')
z_score_df
# Calculando a redução do dataset. Redução de cerca de 3%
len(z_score_df) / len(df)
df = z_score_df
# A média aumentou um pouco de valor, se aproximando de 11.200 milhas
df['miles_per_year'].describe()
# Em comparação com o plot antes da limpeza, esse faz mais sentido tendo em vista que as milhas de
# carro tendem a aumentar com o passar dos anos de uso.
df.plot.scatter(x='age', y='odometer', figsize=(15,10))
# Remove as colunas geradas para a análise
df = df.drop(columns=['age', 'miles_per_year', 'log_miles_per_year'])
df.head()
df.to_csv(CLEAN_VEHICLES_2_DATASET, index=False)<jupyter_output><empty_output><jupyter_text>Limpando todas as colunas de uma vez com Ellipitic Envelope(multivariado)<jupyter_code>df = load_vehicles_dataset_and_set_types(CLEAN_VEHICLES_2_DATASET)
# Preparação para o usar o Ellipitc Envelope
# Setando os tipos categoricos para codigos
categoric_types = ['type', 'region', 'transmission', 'manufacturer', 'model', 'condition', 'cylinders', 'fuel', 'title_status', 'drive', 'paint_color', 'state']

for column in categoric_types:
    df[column] = df[column].cat.codes

# Removendo colunas do tipo object
df = df.drop(columns=['url', 'description'])
from sklearn.covariance import EllipticEnvelope
detector = EllipticEnvelope(contamination=0.01)
detector.fit(df)
scores = detector.predict(df)
df['outlier'] = scores
df[df['outlier'] == -1]
# Redução de cerca de 1% no tamanho do dataset
len(df[df['outlier'] != -1]) / len(df)
# Salvando os indices a serem removidos
idxs_to_remove = df[df['outlier'] == -1].index.values.tolist()
len(idxs_to_remove)
# Carregando novamente o dataset para que se matenha os dados categóricos ao invés dos códigos
df = load_vehicles_dataset_and_set_types(CLEAN_VEHICLES_2_DATASET)
# Removendo os dados detectados como outliers pelo Elliptic Envelope e mantendo os dados categóricos 
# como categorias
df = df.drop(index=idxs_to_remove)
df
df.to_csv(CLEAN_VEHICLES_2_DATASET, index=False)<jupyter_output><empty_output><jupyter_text>NormalizaçãoAnalisando o dataset percebemos que a coluna 'odometer' e 'price' estão dominando o cálculo. Usaremos a normalização por min-max e normalização por média e desvio padrão para normalizar todos os atributos.<jupyter_code>from sklearn.preprocessing import StandardScaler, MinMaxScaler
df.describe()
df.head()
# Confirmação da dominância das colunas odometer e price
numeric_types = ['price', 'year', 'odometer', 'lat', 'long']
dist = np.linalg.norm(df[numeric_types].values[1] - df[numeric_types].values[2])
dist<jupyter_output><empty_output><jupyter_text>Usando MinMaxScaler do sklearn para normalizar todos os atributos numericos logo em seguida uma comparação das antigas e depois da normalização. Dessa forma todos as colunas númericas estão entre 0 e 1 e não há mais dominância das colunas odometer ou price.<jupyter_code>norm_numeric_types = ['price_norm', 'year_norm', 'odometer_norm', 'lat_norm', 'long_norm']
scaler = MinMaxScaler()
df[norm_numeric_types] = pd.DataFrame(scaler.fit_transform(df[numeric_types]), index=df.index)
df.describe()
# Podemos observar que agora as outras colunas possuem muito mais inflência no cálculo.
dist_norm = np.linalg.norm(df[norm_numeric_types].values[1] - df[norm_numeric_types].values[2])
print("Distância linear: {}\nDistancia linear normalizada: {}".format(dist, dist_norm))<jupyter_output>Distância linear: 123295.81792044346
Distancia linear normalizada: 0.2515333191940483<jupyter_text>Normalizando por Média e Desvio Padrão usando o Standard Scaler do sklearn, esse método de normalização não conserva o intervalo dos atributos entre 0 e 1.<jupyter_code>scaler = StandardScaler()
df[norm_numeric_types] = pd.DataFrame(scaler.fit_transform(df[numeric_types]), index=df.index)
df.describe()
# também podemos notar a diferença substancial na distância linear após a normalização. 
# Talvez um problema nessa normalização para o nosso dataset, é que o preço ficou com média negativa.
dist_norm = np.linalg.norm(df[norm_numeric_types].values[1] - df[norm_numeric_types].values[2])
print("Distância linear: {}\nDistancia linear normalizada: {}".format(dist, dist_norm))
# Removendo colunas usadas na análise
df.drop(columns=norm_numeric_types, inplace=True)<jupyter_output><empty_output><jupyter_text>Discretização Vamos discretizar o numero de anos de uso dos carros, para entendermos melhor qual a idade da maioria dos carros no nosso dataset.<jupyter_code>from matplotlib import pyplot as plt
import seaborn as sns
df = load_vehicles_dataset_and_set_types(CLEAN_VEHICLES_2_DATASET)
# coluna contendo os anos de uso de um carro
df['age'] = 2021 - df['year']
df['age'].describe()
# discretizando os anos de uso em intervalos, limitando ate 51 anos para melhor visualizar.
# São poucas amostras acima de 51 anos.
df['disct_age'] = pd.cut(df[df['age'] <= 51]['age'], 10)
df['disct_age'].value_counts()
# Podemos notar que a maioria dos carros anunciados possuem entre 1 a 10 anos de uso
sns.catplot(x="disct_age", kind="count", data=df, height=10)
# Podemos também discretizar de acordo com a frenquencia das amostras
df['disct_age'] = pd.qcut(df['age'],10)
df['disct_age'].value_counts()<jupyter_output><empty_output><jupyter_text>Permitindo assim ter uma visualização mais clara que a maior parte dos carros no nosso dataset são carros entre [1,4[ e carros entre [7, 9[<jupyter_code>sns.catplot(x="disct_age", kind="count", data=df, height=10)<jupyter_output><empty_output><jupyter_text>Cálculo da redução total no tamanho do dataset após todos os tratamentos realizados nesse notebook<jupyter_code># Redução de quase 30% no dataset, mas os dados que ficaram possuem uma qualidade muito superior
# em relação a antes.
len(df)/435489<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09692283247709338, 'fraction_numerical': 0.02203273131719718, 'mean_word_length': 5.941911764705883, 'pattern_counts': {'":': 0, '<': 115, '<?xml version=': 0, '>': 124, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, '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': '29', 'dir': 'jupyter-structured-clean-dedup', 'id': '641278', 'license': 'MIT', 'n_tokens_mistral': 6953, 'n_tokens_neox': 6604, 'n_words': 2012, 'path': 'Parte 1/notebooks/2-pre_processing.ipynb', 'repo_name': 'mbs8/IF679-ciencia-de-dados', 'size': '18881'}