Desenvolvimento

5 ago, 2016

Como estar entre os 15 melhores da competição Kaggle usando Python – Parte 02

Publicidade

Na primeira parte, você viu aspectos gerais da competição Expedia Kaggle, como explorar os dados Kaggle em Python, aprendeu mais sobre previsões e viu alguns exemplos. Aqui segue a segunda parte.

Criando melhores previsões para nossa entrada Kaggle

Esses dados para essa competição são muito difíceis de fazer previsões sobre o uso de machine learning por alguns motivos:

  • Há milhões de linhas, o que aumenta o uso do tempo de execução e memória para os algoritmos.
  • Há 100 clusters diferentes e, de acordo com os administradores da competição, os limites são muito confusos, então será provavelmente difícil fazer previsões. À medida que o número de clusters aumenta, classificadores geralmente diminuem de precisão.
  • Nada é linearmente correlacionado com o alvo (hotel_clusters), o que significa que não é possível usar técnicas de machine learning rápidas, como a regressão linear.

Por essas razões, machine learning provavelmente não vai funcionar bem em nossos dados, mas podemos tentar um algoritmo e descobrir.

Gerando recursos

O primeiro passo na aplicação de machine learning é gerar os recursos. Podemos gerar recursos utilizando tanto o que está disponível nos dados de treinamento, como o que está disponível em destinos. Nós ainda não olhamos para os destinos ainda, então vamos dar uma espiada.

Gerando recursos a partir de destinos

Destinos contêm um ID que corresponde a srch_destination_id, juntamente com 149 colunas de informações latentes sobre esse destino. Aqui está um exemplo:

Confira a tabela no artigo original.

A competição não nos diz exatamente o que cada recurso latente é, mas é seguro assumir que é uma combinação de características de destino, como nome, descrição, e muito mais. Essas características latentes foram convertidas em números, para que pudessem ser anônimos.

Podemos usar as informações de destino como recursos em um algoritmo de machine learning, mas vamos precisar comprimir o número de colunas primeiro, para minimizar o tempo de execução. Podemos usar PCA para fazer isso. PCA irá reduzir o número de colunas numa matriz durante a tentativa de preservar a mesma quantidade de variância por linha. Idealmente, PCA irá comprimir toda a informação contida em todas as colunas, mas, na prática, algumas informações serão perdidas.

No código abaixo, nós:

  • Inicializamos um modelo PCA usando scikit-learn.
  • Especificamos que queremos ter somente 3 colunas em nossos dados.
  • Transformamos as colunas d1-d149 em 3 colunas.
from sklearn.decomposition import PCA

pca = PCA(n_components=3)
dest_small = pca.fit_transform(destinations[["d{0}".format(i + 1) for i in range(149)]])
dest_small = pd.DataFrame(dest_small)
dest_small["srch_destination_id"] = destinations["srch_destination_id"]

O código acima comprime as 149 colunas de destinos em até 3 colunas, e cria uma nova DataFrame chamada dest_small. Preservamos a maior parte da variância nos destinos ao fazer isso, então não perdemos um monte de informações, mas guardamos um monte de tempo de execução para um algoritmo de machine learning.

Gerando recursos

Agora que as preliminares estão encerradas, podemos gerar os nossos recursos. Vamos fazer o seguinte:

  • Gerar novos recursos de data com base em date_time, srch_ci e srch_co.
  • Remover colunas não numéricas como date_time.
  • Acrescentar recursos de dest_small.
  • Substituir quaisquer valores que faltam com -1.
def calc_fast_features(df):
    df["date_time"] = pd.to_datetime(df["date_time"])
    df["srch_ci"] = pd.to_datetime(df["srch_ci"], format='%Y-%m-%d', errors="coerce")
    df["srch_co"] = pd.to_datetime(df["srch_co"], format='%Y-%m-%d', errors="coerce")
    
    props = {}
    for prop in ["month", "day", "hour", "minute", "dayofweek", "quarter"]:
        props[prop] = getattr(df["date_time"].dt, prop)
    
    carryover = [p for p in df.columns if p not in ["date_time", "srch_ci", "srch_co"]]
    for prop in carryover:
        props[prop] = df[prop]
    
    date_props = ["month", "day", "dayofweek", "quarter"]
    for prop in date_props:
        props["ci_{0}".format(prop)] = getattr(df["srch_ci"].dt, prop)
        props["co_{0}".format(prop)] = getattr(df["srch_co"].dt, prop)
    props["stay_span"] = (df["srch_co"] - df["srch_ci"]).astype('timedelta64[h]')
        
    ret = pd.DataFrame(props)
    
    ret = ret.join(dest_small, on="srch_destination_id", how='left', rsuffix="dest")
    ret = ret.drop("srch_destination_iddest", axis=1)
    return ret

df = calc_fast_features(t1)
df.fillna(-1, inplace=True)

Isso irá calcular recursos como duração da estadia, o dia do check in, e o mês do check out. Esses recursos irão nos ajudar a treinar um algoritmo de machine learning mais tarde.

Substituir os valores em falta com -1 não é a melhor escolha, mas vai funcionar muito bem por enquanto, e sempre podemos otimizar o comportamento mais tarde.

Machine learning

Agora que temos os recursos para os nossos dados de treinamento, podemos tentar o machine learning. Usaremos a validação cruzada 3-fold em todo o conjunto de treinamento para gerar uma estimativa de erro de confiança. Validação cruzada divide o conjunto de treinamento em 3 partes, em seguida, prevê o hotel_cluster para cada peça usando as outras partes para treinar.

Vamos gerar previsões usando o algoritmo Random Forest. Ele constrói árvores, que podem se encaixar nas tendências não-lineares em dados. Isso nos permitirá fazer previsões, mesmo que nenhuma das nossas colunas estejam linearmente relacionadas.

Vamos primeiro inicializar o modelo e calcular pontuações de validação cruzada:

predictors = [c for c in df.columns if c not in ["hotel_cluster"]]
from sklearn import cross_validation
from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier(n_estimators=10, min_weight_fraction_leaf=0.1)
scores = cross_validation.cross_val_score(clf, df[predictors], df['hotel_cluster'], cv=3)
scores
array([ 0.06203556,  0.06233452,  0.06392277])

O código acima não nos dá muito boa precisão, e confirma a nossa suspeita inicial de que machine learning não é uma grande abordagem para esse problema. No entanto, os classificadores tendem a ter uma precisão inferior quando existe uma elevada contagem de cluster. Em vez disso, podemos tentar treinar 100 classificadores binários. Cada classificador só vai determinar se uma linha está em seu cluster ou não. Isso implicará a formação de um classificador por etiqueta no hotel_cluster.

Classificadores binários

Mais uma vez vamos treinar Random Forests, mas cada floresta irá prever apenas um único cluster de hotel. Usaremos validação cruzada 2-fold para mais velocidade, e só treinaremos 10 árvores por etiqueta.

No código abaixo, nós:

  • Faremos um loop em cada circuito único através do hotel_cluster: treinar um classificador Random Forest utilizando validação cruzada 2-fold; extrairemos as probabilidades do classificador que a linha única está no hotel_cluster.
  • Combinar todas as probabilidades.
  • Para cada linha, encontrar as 5 maiores probabilidades, e atribuir esses valores ao hotel_cluster como previsões.
  • Calcular precisão usando mapk.
from sklearn.ensemble import RandomForestClassifier
from sklearn.cross_validation import KFold
from itertools import chain

all_probs = []
unique_clusters = df["hotel_cluster"].unique()
for cluster in unique_clusters:
    df["target"] = 1
    df["target"][df["hotel_cluster"] != cluster] = 0
    predictors = [col for col in df if col not in ['hotel_cluster', "target"]]
    probs = []
    cv = KFold(len(df["target"]), n_folds=2)
    clf = RandomForestClassifier(n_estimators=10, min_weight_fraction_leaf=0.1)
    for i, (tr, te) in enumerate(cv):
        clf.fit(df[predictors].iloc[tr], df["target"].iloc[tr])
        preds = clf.predict_proba(df[predictors].iloc[te])
        probs.append([p[1] for p in preds])
    full_probs = chain.from_iterable(probs)
    all_probs.append(list(full_probs))

prediction_frame = pd.DataFrame(all_probs).T
prediction_frame.columns = unique_clusters
def find_top_5(row):
    return list(row.nlargest(5).index)

preds = []
for index, row in prediction_frame.iterrows():
    preds.append(find_top_5(row))

metrics.mapk([[l] for l in t2.iloc["hotel_cluster"]], preds, k=5)
0.041083333333333326

Nossa precisão aqui é pior do que antes, e as pessoas na tabela de pontos têm pontuações muito melhores de precisão. Vamos precisar abandonar o machine learning e passar para a próxima técnica, a fim de competir. Machine learning pode ser uma poderosa técnica, mas nem sempre é a abordagem certa para cada problema.

Principais clusters com base em hotel_cluster

Existem alguns Kaggle Scripts para a competição que envolvem agregação do hotel_cluster baseado em orig_destination_distance ou srch_destination_id. Agregar em orig_destination_distance vai explorar um vazamento de dados na competição, e tentar colocar os usuários juntos. Agregar em srch_destination_id vai encontrar os clusters mais populares de hotéis para cada destino. Vamos, então, ser capazes de prever que um usuário que procura um destino vai a um dos mais populares clusters de hotéis para esse destino. Pense nisso como uma versão mais granular da técnica de clusters mais comuns que usamos anteriormente.

Podemos primeiro gerar pontuações para cada hotel_cluster em cada srch_destination_id. Vamos dar um peso maior para reservas do que para os cliques. Isso ocorre porque os dados de test são todos dados de reserva, e é isso que queremos prever. Queremos incluir informações do clique, mas reduzir para refletir isso. Passo a passo, teremos:

  • Grupo t1 para srch_destination_id e hotel_cluster.
  • Que iterar cada grupo, e: atribuir 1 ponto a cada cluster hotel onde is_booking é True; atribuir .15 pontos para cada cluster hotel onde is_booking é False; atribuir a pontuação para a combinação srch_destination_id/hotel_cluster em um dicionário.

Aqui está o código para realizar os passos acima:

def make_key(items):
    return "_".join([str(i) for i in items])

match_cols = ["srch_destination_id"]
cluster_cols = match_cols + ['hotel_cluster']
groups = t1.groupby(cluster_cols)
top_clusters = {}
for name, group in groups:
    clicks = len(group.is_booking[group.is_booking == False])
    bookings = len(group.is_booking[group.is_booking == True])
    
    score = bookings + .15 * clicks
    
    clus_name = make_key(name[:len(match_cols)])
    if clus_name not in top_clusters:
        top_clusters[clus_name] = {}
    top_clusters[clus_name][name[-1]] = score

No final, teremos um dicionário onde cada tecla é uma srch_destination_id. Cada valor no dicionário será mais um dicionário, que contém conjuntos de hotéis como chaves com pontuação como valores. Aqui está como ele se parece:

{'39331': {20: 1.15, 30: 0.15, 81: 0.3},
'511': {17: 0.15, 34: 0.15, 55: 0.15, 70: 0.15}}

Depois vamos transformar esse dicionário para encontrar os top 5 clusters de hotéis para cada srch_destination_id. A fim de fazer isso, vamos:

  • Fazer loop através de cada chave em top_clusters.
  • Encontrar os top 5 clusters para essa chave.
  • Atribuir os top 5 clusters para um novo dicionário, cluster_dict.

Aqui está o código:

import operator

cluster_dict = {}
for n in top_clusters:
    tc = top_clusters[n]
    top = [l[0] for l in sorted(tc.items(), key=operator.itemgetter(1), reverse=True)[:5]]
    cluster_dict[n] = top

Fazendo previsões com base no destino

Uma vez que sabemos os principais clusters para cada srch_destination_id, podemos rapidamente fazer previsões. Para fazer previsões, tudo o que temos a fazer é:

  • Iterar cada linha de t2.
  • Extrair o srch_destination_id da linha.
  • Encontrar os principais clusters para cada id de destino.
  • Anexar os principais clusters para preds.

Aqui está o código:

preds = []
for index, row in t2.iterrows():
    key = make_key([row[m] for m in match_cols])
    if key in cluster_dict:
        preds.append(cluster_dict[key])
    else:
        preds.append([])

No final do ciclo, preds será uma lista de listas contendo as nossas previsões. Ele será parecido com isto:

[
  [2, 25, 28, 10, 64], 
  [25, 78, 64, 90, 60],
  ...
]

Calculando erro

Uma vez que temos as nossas previsões, podemos calcular a nossa precisão usando a função de mapk de anteriormente:

metrics.mapk([[l] for l in t2["hotel_cluster"]], preds, k=5)
0.22388136288998359

Estamos indo muito bem! Nós impulsionamos nossa precisão em 4x sobre a melhor abordagem do machine learning, e fizemos com uma abordagem muito mais rápida e mais simples.

Você deve ter notado que ests valor é um pouco menor do que as precisões sobre o leaderboard. Testes locais resultam em precisão menor do que submetido, assim essa abordagem vai realmente se sair muito bem na tabela de classificação. As diferenças na classificação de pontuação e na pontuação local podem vir para baixo com alguns fatores:

  • Diferentes dados locais no conjunto oculto da tabela de pontuações são computadas. Por exemplo, estamos computando erros em uma amostra do conjunto de treinamento, e a pontuação do ranking é calculada sobre o conjunto de testes.
  • Técnicas que resultam em maior precisão, com mais dados de treinamento. Nós só estamos usando um pequeno subconjunto de dados para treinamento, e isso pode ser mais precisos quando usamos o conjunto de treinamento completo.
  • Randomização diferente. Com alguns algoritmos, números aleatórios estão envolvidos, mas nós não estamos usando qualquer um destes.

Gerando melhores previsões para sua submissão Kaggle

Os fóruns são muito importantes no Kaggle, e muitas vezes podem ajudar a encontrar informação que permitirá aumentar a sua pontuação. A competição Expedia não é exceção. Este artigo detalha um vazamento de dados que permite que você combine os usuários no conjunto de treinamento a partir do conjunto de teste usando um conjunto de colunas incluindo user_location_country e user_location_region.

Usaremos as informações do artigo para relacionar os usuários do teste definido de volta para o conjunto de treinamento, o que irá aumentar a nossa pontuação. Com base no tópico do fórum, está tudo bem fazer isso, e a competição não será atualizada como resultado do vazamento.

Encontrando usuários correspondentes

O primeiro passo é encontrar os usuários no conjunto de treinamento que correspondem aos usuários no conjunto de testes.

A fim de fazer isso, precisamos:

  • Dividir os dados de treinamento em grupos com base nas colunas correspondentes.
  • Fazer um loop através dos dados de teste.
  • Criar um índice com base nas colunas correspondentes.
  • Obter qualquer correspondência entre os dados de teste e os dados de treinamento usando os grupos.

Aqui está o código para fazer isso:

match_cols = ['user_location_country', 'user_location_region', 'user_location_city', 'hotel_market', 'orig_destination_distance']

groups = t1.groupby(match_cols)
    
def generate_exact_matches(row, match_cols):
    index = tuple([row[t] for t in match_cols])
    try:
        group = groups.get_group(index)
    except Exception:
        return []
    clus = list(set(group.hotel_cluster))
    return clus

exact_matches = []
for i in range(t2.shape[0]):
    exact_matches.append(generate_exact_matches(t2.iloc[i], match_cols))

No final desse loop, teremos uma lista de listas que conterá qualquer correspondência exata entre o conjunto de treinamento e o de teste. No entanto, não há muitas correspondências. Para avaliar com precisão os erros, nós vamos ter que combinar essas previsões com as nossas previsões anteriores. Caso contrário, vamos obter um valor muito baixo de precisão, porque a maioria das linhas tem listas vazias para previsões.

Combinando previsões

Podemos combinar listas diferentes de previsões para aumentar a precisão. Fazer isso também vai nos ajudar a ver como está nossa estratégia de correspondência exata. Para fazer isso, teremos que:

  • Combinar exact_matches, preds e most_common_clusters.
  • Apenas tomar as previsões únicas, em ordem sequencial, usando a função F5 daqui.
  • Ter certeza de que temos um máximo de 5 previsões para cada linha no conjunto de testes.

Veja como podemos fazer isso:

def f5(seq, idfun=None): 
    if idfun is None:
        def idfun(x): return x
    seen = {}
    result = []
    for item in seq:
        marker = idfun(item)
        if marker in seen: continue
        seen[marker] = 1
        result.append(item)
    return result
    
full_preds = [f5(exact_matches[p] + preds[p] + most_common_clusters)[:5] for p in range(len(preds))]
mapk([[l] for l in t2["hotel_cluster"]], full_preds, k=5)
0.28400041050903119

Isso está ficando muito bom em termos de erro – melhoramos dramaticamente se comparado com o anterior! Nós poderíamos continuar e ganhar mais pequenas melhorias, mas estamos provavelmente prontos para enviar agora.

Fazendo um arquivo de apresentação Kaggle

Felizmente, por causa da maneira como escrevemos o código, tudo o que temos a fazer para enviar é atribuir o train à variável t1 e test à variável t2. Então, nós apenas temos que reexecutar o código para fazer as previsões. Reexecutar o código sobre os conjuntos train e test deve demorar menos de uma hora.

Uma vez que temos previsões, só temos de escrevê-las em um arquivo:

write_p = [" ".join([str(l) for l in p]) for p in full_preds]
write_frame = ["{0},{1}".format(t2["id"][i], write_p[i]) for i in range(len(full_preds))]
write_frame = ["id,hotel_clusters"] + write_frame
with open("predictions.csv", "w+") as f:
    f.write("\n".join(write_frame))

Então, teremos um arquivo de apresentação no formato certo para enviar. Quando este artigo foi escrito, ao fazer este envio, você poderá ir para o top 15.

Resumo

Tivemos um longo caminho neste artigo! Caminhamos do ponto de estar somente olhando os dados por todo o caminho até criar uma submissão e chegar ao leaderboard. Ao longo do caminho, alguns dos principais passos que tomamos foram:

  • Explorar os dados e entender o problema.
  • Criar uma forma de interagir rapidamente através de diferentes técnicas.
  • Criar uma maneira de descobrir precisão localmente.
  • Ler os fóruns, scripts e as descrições da competição com muita atenção para entender melhor a estrutura dos dados.
  • Tentar uma variedade de técnicas e não ter medo de não usar machine learning.

Esses passos irão atender bem em qualquer competição Kaggle.

Melhorias futuras

A fim de interagir rapidamente e explorar técnicas, velocidade é fundamental. Isso é difícil nessa competição, mas existem algumas estratégias para tentar:

  • Pegar uma amostragem ainda menor.
  • Paralelizar operações em vários núcleos.
  • Usar Spark ou outras ferramentas onde as tarefas podem ser executadas em parallel workers.
  • Explorar várias maneiras de escrever código e benchmarking para encontrar a abordagem mais eficiente.
  • Evitar iteração sobre o conjunto total de treinamento e teste, e em vez disso, usar grupos.

Escrever código rápido e eficiente é uma enorme vantagem nessa competição.

Técnicas futuras para experimentar

Quando você tiver uma base estável para executar o código, existem alguns caminhos para explorar em termos de técnicas para aumentar a precisão:

  • Encontrar semelhança entre os usuários, e em seguida ajustar contagens de cluster de hotel com base na similaridade.
  • Usar semelhança entre destinos para agrupar vários destinos juntos.
  • Aplicar machine learning dentro de subconjuntos de dados.
  • Combinar diferentes estratégias de predição de uma maneira menos ingênua.
  • Explorar mais o vínculo entre os clusters e regiões de hotéis.

Eu espero que você se divirta com esse concurso! Eu adoraria ouvir qualquer feedback que você tenha.

***

Vik Paruchuri é um cientista de dados e desenvolvedor que fundou a Dataquest. Besides Além disso, ele faz parte do time de colunistas internacionais do iMasters. A tradução deste artigo foi feita pela Redação iMasters, com autorização do autor. Este artigo foi escrito originalmente no blog Dataquest: https://www.dataquest.io/blog/kaggle-tutorial/