<jupyter_start><jupyter_text>IntroduçãoNesse notebook é realizada a análise preliminar dos dados completos e suaseparação em dados de treinamento e teste. Nessa análise preliminar iremosfazer diagnósticos da qualidade dos dados e quando necessário ajustá-lo. Isso,compreende correção do tipo dos dados, avaliação da consistência, análise dedados faltantes (sem considerar técnicas estatísticas de imputação) e remoçãode dados duplicados.O segundo objetivo é a separação dos dados em dados de treinamento e teste paraque os modelos de classificação e recomendação possam vir a ter sua capacidadede generalização validada em etapa futura. A nossa base de dados é respectiva a registros de resultados de queries. Estessão dados úteis para a modelagem do ranqueamento de resultados a partir de umaquery. Porém, para a modelagem da predição de categorias de produtos precisamosde dados relativos apenas a produtos. Dessa forma, iremos separar os dados emdois dataset, e em seguida separar cada um destes em treinamento e testes,totalizando ao todo 4 datasets. Para o caso dos dados de produtos, precisamosgarantir ainda que ambos os dados de treinamento quanto teste possuam ascategorias representadas corretamente.  Imports<jupyter_code>import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
from sklearn.model_selection import train_test_split<jupyter_output><empty_output><jupyter_text>Carregamento dos dados<jupyter_code>df = pd.read_csv(r"..\data\complete_dataset.csv")
df.head()
df.shape<jupyter_output><empty_output><jupyter_text>Os dados foram carregados corretamente e ao todo temos 38.507 registro com15 atributos.  Análise preliminar dos dados Dados faltantes<jupyter_code>df.isna().mean().mul(100).round(2)<jupyter_output><empty_output><jupyter_text>Apenas a coluna `order_countts` possui uma quantidade de dados faltantessignificativa (52,95%). Outras colunas que também possuem dados faltantes são`weight` (0,01%) e `concatenated_tags` (0,15%), porém estes, em um níveldesprezível.<jupyter_code>(df["order_counts"] == 0).any()<jupyter_output><empty_output><jupyter_text>O fato de não termos nenhum registro de `order_counts` como sendo igual a 0.Pode ser um indicativo de que caso um item não receba ordens na query, aobservação ficará como `missing`. Supondo que esse realmente seja o caso (o que parece ser bastante razoável), para sanarmos essa grande quantidade de dados faltantes em `order_counts` basta preencher dados faltantes dessa coluna com`0`.<jupyter_code>df["order_counts"].fillna(0, inplace=True)
df["order_counts"].isna().any()<jupyter_output><empty_output><jupyter_text>Como a quantidade de dados faltantes nas outras variáveis é muito pequena,optou-se por remover as observações que possuam dados faltantes.<jupyter_code>df.dropna(inplace=True)<jupyter_output><empty_output><jupyter_text>Verificando o tipo das variáveisPara simplificar a conformação dos dados vamos tratar variáveis categóricascomo textuais `object`.<jupyter_code>df.dtypes<jupyter_output><empty_output><jupyter_text>A primeira variável que será modificada é a `creation_date`. Como nãoespecificamos no momento do carregamento dos dados qual coluna do datasetdeveria ser parseado para data, esse campo foi carregado como um `object` aoinvés de um `datetime`.Avaliando as primeiras linhas dessa coluna, vemos que ela aparentemente estáformatada corretamente. Para nos certificarmos que a conversão ocorrerá comoesperado ao invés de deixar que o método de conversão infira o formato da dataesse formato será especificado (`Y-m-d H:M:S`).<jupyter_code>df["creation_date"].head()
df["creation_date"] = pd.to_datetime(df["creation_date"],
                                     format="%Y-%m-%d %H:%M:%S")
df["creation_date"].head()<jupyter_output><empty_output><jupyter_text>Apesar de menor importância outra variável que pode ter seu tipo modificado éa `order_counts` que antes estava definida como `float`. A variável estavatipada dessa forma pois possuia registros faltantes entre suas observações.Como já acabamos com esse problema, faz sentido para esse tipo de dadoconvertê-lo para número inteiro.<jupyter_code>df["order_counts"] = df["order_counts"].astype("int")
df["order_counts"].head()<jupyter_output><empty_output><jupyter_text>Por fim, vamos verificar se as variáveis categórias `express_delivery` e`category`, possuem entre as suas observações apenas valores referentes às suascategorias.<jupyter_code># verificando se temos apenas registros com observações iguais às esperadas
# tanto para a variável express_delivery quanto para category
assert set(df["express_delivery"].unique()) == set([1, 0])
cat_list = ["Bebê", "Bijuterias e Jóias", "Decoração",
            "Lembrancinhas", "Papel e Cia", "Outros"]
assert set(df["category"].unique()) == set(cat_list)<jupyter_output><empty_output><jupyter_text>Após essa última verificação vemos que ambas as variáveis tem o preenchimentocorreto de suas categoriais.  Eliminando registros duplicadosNessa etapa vamos remover os registros duplicados. Porém antes de fazer issoprecisamos saber de que tipo de registro nós queremos remover os registrosduplicados. A nossa base de dados é de resultados de queries, com isso sequisermos identificar informações unificadas por produto, precisamos remover osregistros duplicados de produto e unificar as variações entre os registrosduplicados.Já para os dados de resultados de queries, não é claro o que devemos considerarpara remover os registros duplicados. Essas informações de queriesaparentemente são extraídas ao longo do tempo, de modo que para queriesdistintas, um mesmo produto pode conter variações referentes às atualizaçõesque o vendedor faz no produto ao longo do tempo.Seria esperado que para uma combinação de querie e produto nãohajam variações, o que não é o que observamos. Isso significa que dada umaquery um mesmo produto pode vir a ser listado como resultado mais de uma vez.Apesar de ser intrigante não é claro que essas variações devam ser eliminadas,uma vez que variações em quantidade mínima e preço, podem afetar a posição deum item em um resultado de busca. Sendo assim, podem ser úteis para o nossosegundo modelo, o ranqueamento de itens a partir de uma query. Analisando registros duplicadosAntes de fazer a eliminação dos registros duplicados devemos analisar comoesses registros duplicados podem afetar os dados. Uma forma de avaliar isso,é primeiro identificando quais colunas possuem as observações unicamenterelacionadas com os identificadores únicos dos registros (dos tipos query eproduto) e em seguida, como as colunas que não são unicamente relacionadoscom os identificadores únicos variam por registros duplicados.Para fazer essa identificação das colunas que variam e não variam com os idsde registros a função `check_unity` será utilizada. Essa função compara aquantidade de ids únicos com a quantidade de combinações únicas entre cadavariável e o id. Se ambos os números forem iguais, a variável é associadaunicamente, caso contrário existem variações nessa coluna para um mesmoid de registro.<jupyter_code>def check_unity(df: pd.DataFrame, id_group: list[str]) -> list[str]:
    """Verifica, para cada variável do dataset, quais variáveis são unicamente
    relacionadas com as variáveis que identificam unicamente os registros
    id_group. As variáveis que não se relacionam unicamente são retornadas
    como resultado da função.

    Args:
        df (pd.DataFrame): dataset
        id_group (list[str]): variáveis que combinadas identificam unicamente
        os registros

    Returns:
        list[str]: lista de variáveis que não se relacionam unicamente com
        id_group
    """
    unique_regs = df[id_group].drop_duplicates().shape[0]
    with_variability = list()
    without_variability = list()
    for col in df.columns:
        if col in id_group:
            continue
        else:
            cols_select = id_group + [col]
            cols_id_uniques = df[cols_select].drop_duplicates().shape[0]
            is_equal = unique_regs == cols_id_uniques
            if is_equal:
                without_variability.append(col)
            else:
                with_variability.append(col)
    
    print(f"Únicas em relação a {id_group}")
    print(" | ".join(without_variability))
    print()
    print(f"Com variações em relação a {id_group}")
    print(" | ".join(with_variability))
    return with_variability<jupyter_output><empty_output><jupyter_text>Nos casos das colunas numéricas que forem identificadas variações para um mesmoidentificador único de registro, serão analisadas a distribuição doscoeficientes de variação entre os registros duplicados. Ou seja, iremos avaliarcomo os registros com um mesmo id variam entre si para cada variável em queessa variação exista. A estatística utilizada é o coeficiente de variação poisuniformiza a unidade de análise da variação ao dividir o desvio padrão pelamédia da distribuição. Essa análise será gráfica e serão realizados gráficosde boxplot e histograma com essas distribuições. Adicionalmente, será utilizadoum gráfico de barras com a distribuição da quantidade de registros duplicadospara avaliarmos a frequência que o problema de registro duplicado ocorre.<jupyter_code>def plot_duplicates_distribution(df: pd.DataFrame,
                                 id_group: list[str],
                                 figsize: tuple[int] = (15, 5)) -> None:
    """Faz o gráfico com a distribuição de contagem de registros por id_group

    Args:
        df (pd.DataFrame): dataset.
        id_group (list[str]): colunas que combinas identificam unicamente os
        registros.
        figsize (tuple[int], optional): tamanho da figura. Defaults to (15, 5).
    """    
    title = "Distribuição da quantidade de duplicados"
    (df.groupby(id_group).
     size().value_counts(normalize=True).sort_index().
     plot(kind="bar", figsize=figsize, title=title))
    plt.xticks(rotation=0)
    plt.show()

def plot_variability_boxplot(duplic_df: pd.DataFrame,
                             figsize: tuple[int] = (15, 8)):
    """Faz o gráfico de boxplot das estatísticas de coeficiente de variação dos
    registros duplicados.

    Args:
        duplic_df (pd.DataFrame): coeficiente de variação de registros
        duplicados
        figsize (tuple[int], optional): tamanho da figura. Defaults to (15, 8).
    """    
    title = "Boxplot dos coeficientes de variação entre registros duplicados"
    duplic_df.boxplot(figsize=figsize)
    plt.title(title)
    plt.show()
    

def plot_variability_histogram(duplic_df: pd.DataFrame,
                               figsize: tuple[int] = (15, 8),
                               layout: tuple[int] = (3, 3),
                               bins: int = 40):
    """Faz o gráfico de histograma das estatísticas de coeficiente de variação
    dos registros duplicados.

    Args:
        duplic_df (pd.DataFrame): coeficiente de variação de registros
        duplicados
        figsize (tuple[int], optional): tamanho da figura. Defaults to (15, 8).
        layout (tuple[int], optional): layout dos subplots. Defaults to (3, 3).
        bins (int, optional): quantidade de bins dos histogramas.
        Defaults to 40.
    """    
    title = "Histograma dos coeficientes de variação entre registros duplicados"
    duplic_df.plot(subplots=True,
                   kind="hist",
                   layout=layout,
                   figsize=figsize,
                   bins=bins,
                   density=True,
                   title=title)
    plt.show()

def var_coef(num_series: pd.Series) -> float:
    """Calcula o coeficiente de variação para uma série

    Args:
        num_series (pd.Series): série de valores numéricos

    Returns:
        float: coeficiente de variação
    """    
    return num_series.std() / num_series.mean()


def plot_variability(df: pd.DataFrame,
                     id_group: list[str],
                     col_list: list[str]) -> None:
    """Executa os gráficos de análise de dados duplicados e variabilidade entre
    registros duplicados

    Args:
        df (pd.DataFrame): dataset.
        id_group (list[str]): combinações de variáveis que identificam.
        unicamente um registro.
        col_list (list[str]): variáveis que terão a variabilidade analisada.
    """    
    
    duplicated_df = (df.loc[df[id_group].
                        duplicated(keep=False)].
                     sort_values(id_group))
    
    plot_duplicates_distribution(df=df, id_group=id_group)
    
    duplicated_var_coef = (duplicated_df.
                           groupby(id_group)[col_list].
                           apply(var_coef).
                           fillna(0))
    
    plot_variability_boxplot(duplic_df=duplicated_var_coef)
    plot_variability_histogram(duplic_df=duplicated_var_coef)<jupyter_output><empty_output><jupyter_text>Dados de produtosComo dados de produtos, vamos considerar todos as colunas que não sejamreferentes uma query. Ou seja, a `query` em si executada e o seu resultado`search_page` e `position`. Todas as outras colunas serão consideradas comoatributos de produto.<jupyter_code>product_df = df.drop(columns=["query", "search_page", "position"])
product_df.head()
id_group = ["product_id"]
product_df.duplicated(id_group).mean()<jupyter_output><empty_output><jupyter_text>Como a base é de queries, que relacionam queries com produtos como resultadosda busca, temos uma quantidade considerável de registros duplicados de produto(22,6%).<jupyter_code>selected_cols = check_unity(df=product_df, id_group=id_group)<jupyter_output>Únicas em relação a ['product_id']
seller_id | title | concatenated_tags | creation_date | category

Com variações em relação a ['product_id']
price | weight | express_delivery | minimum_quantity | view_counts | order_counts<jupyter_text>A grande parte das variáveis que são textuais ou categóricas se identificamunicamente com `product_id`. A excessão é a `express_delivery`. Todas as outrasvariáveis podem apresentar variações para um mesmo produto. Com isso podemoshipotetizar que as queries aconteçam ao longo do tempo e que no passar dessetempo tenhamos variações nos produtos. É esperado que os preços possam variarde modo a incorporar a inflação. Também é possível que vendedores mudem as suasestratégias e alterem a quantidade de itens enviadas por cada compra, e com issocondições de peso, entrega e quantidade mínima também variem. Por fim, com otempo a quantidade de pessoas que acessa a página e compra esses produtostambém tende a aumentar.<jupyter_code>selected_cols.pop(2)
plot_variability(df=product_df, id_group=["product_id"], col_list=selected_cols)<jupyter_output><empty_output><jupyter_text>Como podemos observar pela distribuição dos registros duplicados, vemos quepoucos produtos não aparecem mais de uma vez na base, quase 80% dos produtos.Um pouco mais de 10%, apenas 2 vezes. Dessa forma, vemos muito poucos produtosque são retornados por várias queries (o que deve estar relacionado a umagrande variabilidade nas queries de nossa amostra).Considerando os registros duplicados vemos que, entre as variáveis que não estãorelacionadas unicamente com os produtos, possuímos maior variabilidade nosindicadores de peso, e, principalmente, quantidade mínima no pedido. Ainda,assim, a maior parte dos registros repetidos possuem variabilidade muito baixa,bem próxima de zero.A menor quantidade de registros duplicados e a entre os registros duplicados aconcentração em baixa variabilidade entre as colunas que a margem para variação,são indícios de que a unificação dos registros em torno de medidas de tendênciacentral podem ocorrer sem que hajam muitos problemas. Dados de queriesOs dados de queries são os nossos dados originais. Estamos considerando comoregistros únicos desse tipo de dados as combinações entre produto e queries.Essa escolha pode ser questionada, principalmente se estamos interessados emutilizar esses dados para prever ranqueamento de produtos (`position` e/ou`search_page`) a partir da `query`. Nesses casos, mesmo pequenas variações deprodutos quanto aos seus preços e quantidade mínima podem ser importantes paradefinir quais produtos vão estar nas primerias posições do ranqueamento.Ainda assim, para fins comparativos com o que foi observado no caso dos dadosde produtos vamos prosseguir com a análise de dados duplicados para o caso dedados de registros de queries.<jupyter_code>id_group = ["product_id", "query"]
selected_cols = check_unity(df=df, id_group=id_group)
selected_cols.pop(4)
plot_variability(df=df, id_group=id_group, col_list=selected_cols)<jupyter_output><empty_output><jupyter_text>Como era de se esperar, no caso de registros de reultados de queries, temosuma menor parcela de registros que são duplicados. Quase 100% dos registrosúnicos (combinações de produto ou query) aparecem apenas uma vez entre osregistros. Dessa forma, é muito raro, para uma mesma query, vermos um produtorepetido mais de uma vez.É um pouco intrigante o porque isso ocorre. Mais que o produto aparecer maisde uma vez, ele aparece com variações de preço, quanidade mínima, peso...Se isso fosse mais frequente poderíamos começar a questionar o product_id comoidentificador único de produtos. Porém esses casos são tão excepcionais que épossível que sejam relativos a algum problema.Ainda assim, esses dados podem impactar na ordem dos resultados das queries.Dessa forma, não é tão seguro a sua remoção. Poderíamos perder informaçõesque ajudem a indentificar a ordem dos produtos para uma determinada query.Por conta disso, não iremos remover os dados duplicados de resultados dequeries.Outro ponto interessante é que ao analisar a distribuição dos coeficientesde variações entre os registros duplicados, vemos que essa distribuiçãoé ainda mais concentrada em menores valores de coeficienes de variação.Isso ocorre principalmente para os casos de `search_page` e `position`, o quepode ser um indicador de que as variações nas outras variáveis não sãocorrespondidas por variações no ranqueamento dos resultados de queries. Porfim, da mesma forma que ocorreu no caso dos registros duplicados de produtosos maiores valores de coeficientes de variação estão mais distribuídos paraas variáveis de peso e quantidade mínima.  Removendo os produtos duplicadosA remoção dos dados dupicados de produtos envolve três etapas. Na primeira,removemos todos os registros duplicados contidos nos dados originais. Nasegunda, pegamos os dados duplicados agrupamos por `product_id` e calculamosa moda para variáveis categóricas (e também a do tipo `datetime64`) e a médiapara variáveis numéricas. Por fim concatenamos os dois datasets, o primeirosem os produtos duplicados e o segundo onde os produtos duplicados foramunificados em torno das modas das variáveis categóricas e das médias paraas variáveis numéricas.<jupyter_code>def series_mode(serie: pd.Series):
    return stats.mode(serie)[0][0]

def remove_duplicates(df: pd.DataFrame,
                      id_group: list[str],
                      mean_cols: list[str],
                      mode_cols: list[str]) -> pd.DataFrame:
    """Remoção de dados duplicados a partir de combinação de group. Utiliza
    mean_cols e mode_cols para unificar os registros em média e moda das
    observações duplicadas

    Args:
        data (pd.DataFrame): dataset
        group (list[str]): grupo que identifica unificamente o registro
        mean_cols (list[str]): variáveis a serem unificadas pela média
        mode_cols (list[str]): variáveis a serem unificadas pela moda

    Returns:
        pd.DataFrame: dataset sem registros duplicados
    """    
    duplicated_mask = df[id_group].duplicated(keep=False)
    
    dupli_data = df.loc[duplicated_mask]
    mode_stats = {col: series_mode for col in mode_cols}
    mean_stats = {col: "mean" for col in mean_cols}
    agg_stats = dict(**mode_stats, **mean_stats)
    unified_dupli_data = (dupli_data.
                          groupby(id_group, as_index=False).
                          agg(agg_stats))
    
    no_duplit_data = df.loc[~duplicated_mask]
    return pd.concat([no_duplit_data, unified_dupli_data]).set_index(id_group)
numeric_cols = ["price", "weight", "minimum_quantity",
                "view_counts", "order_counts"]
categorical_cols = ["seller_id", "title", "concatenated_tags",
                    "creation_date", "express_delivery", "category"]
product_df = remove_duplicates(df=product_df,
                               id_group=["product_id"],
                               mean_cols=numeric_cols,
                               mode_cols=categorical_cols)
product_df.head()
product_df.shape
product_df.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 29748 entries, 15534262 to 16687430
Data columns (total 11 columns):
 #   Column             Non-Null Count  Dtype         
---  ------             --------------  -----         
 0   seller_id          29748 non-null  int64         
 1   title              29748 non-null  object        
 2   concatenated_tags  29748 non-null  object        
 3   creation_date      29748 non-null  datetime64[ns]
 4   price              29748 non-null  float64       
 5   weight             29748 non-null  float64       
 6   express_delivery   29748 non-null  int64         
 7   minimum_quantity   29748 non-null  float64       
 8   view_counts        29748 non-null  float64       
 9   order_counts       29748 non-null  float64       
 10  category           29748 non-null  object        
dtypes: datetime64[ns](1), float64(5), int64(2), object(3)
memory usage: 2.7+ MB<jupyter_text>Separação entre treinamento e teste e salvando os dadosAgora temos os dois datasets prontos para serem salvos. Mas antes de salvarmosserá necessário separar os dados entre treinamento e teste. Isso é necessáriopara que depois seja possível testar os modelos em condições mais próximas àrealidade. Ou seja, não estamos interessados em modelos que se adaptem à todasas condições particulares dos dados em que estejam sendo treinados, mas queapenas sejam capaz de reproduzir seus padrões mais gerais e assim, performemem dados não antes vistos. Dados de produtoNo caso dos dados de produto, como iremos utilizá-los para prever as categorias,devemos garantir que todas as categorias sejam representadas adequadamentetanto nos dados de treinamento quanto nos dados de teste. Para tal iremosrealizar a separação dos dados com estratificação por `category`.<jupyter_code>train_prod_df, test_prod_df = train_test_split(product_df,
                                               test_size=0.3,
                                               random_state=23,
                                               stratify=product_df["category"])
for filename, data in [("train_prod.pickle", train_prod_df),
                       ("test_prod.pickle", test_prod_df)]:
    data.to_pickle("../data/" + filename)
(train_prod_df["category"].
 value_counts(ascending=True, normalize=True).
 to_frame("train").
 join(test_prod_df["category"].
      value_counts(ascending=True, normalize=True).
      to_frame("test"))
 ).plot(kind="barh", figsize=(12, 5), title="Distribuição (relativa) das categorias por base de dados")
plt.show()<jupyter_output><empty_output><jupyter_text>Query<jupyter_code>train_query_df, test_query_df = train_test_split(df,
                                                 test_size=0.3,
                                                 random_state=23)
for filename, data in [("train_query.pickle", train_query_df),
                       ("test_query.pickle", test_query_df)]:
    data.to_pickle("../data/" + filename)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.06392075519620709, 'fraction_numerical': 0.008720314947297126, 'mean_word_length': 3.8419758147161303, 'pattern_counts': {'":': 0, '<': 76, '<?xml version=': 0, '>': 81, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 13, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '393931', 'license': 'MIT', 'n_tokens_mistral': 7684, 'n_tokens_neox': 7289, 'n_words': 2476, 'path': 'notebooks/0 - data_prepare.ipynb', 'repo_name': 'leofmr/product_recommender', 'size': '23623'}