Nosso codificador único cria um recurso para cada nível do recurso categórico original:
from evalml.pipelines import OneHotEncoder
import pandas as pd
df = pd.DataFrame({"category": ["a", "b"], "number": [4,5 ]})
OneHotEncoder().fit_transform(df).to_dataframe()
As colunas category_a
e category_b
são completamente colineares, o que as torna redundantes. Isso pode ter efeitos adversos no ajuste do estimador. Acho que devemos deixar cair um por padrão.
Para sua informação
💯% devemos descartar a coluna de caso negativo.
Se nós mesmos fizermos o OHE primeiro, então o sklearn esperançosamente não os expandirá. Como Freddy disse, você pode pensar nisso como gerar duas colunas que têm colinearidade perfeita.
Há dois problemas que vejo ao expandir um binário em duas colunas em vez de uma:
@freddyaboulton P: O dataframe acima para as colunas OHE as mostra como floats. Isso é realmente verdade?
@rpeck Sim!
@freddyaboulton O quê ? Isso é estranho. Eu nunca vi nada além de booleanos verdadeiros ou inteiros 0/1. Eu me pergunto como os modelos de árvore realmente lidam com isso. Tem um cheiro ruim para mim.
Terceira lei do código: não farás == comparações com carros alegóricos
(ok, a menos que seja com Math.NaN
)
Hmm, eu pensei que estávamos fazendo isso!
Concordo que devemos. Eu pensei que era apenas um sinalizador que tínhamos que definir no impl subjacente.
@dsherry @freddyaboulton Parece que temos suporte para isso por meio de nosso parâmetro drop
mas leva em consideração apenas a entrada do usuário e não é usado pelo nosso impl, então esse problema apenas rastreia a configuração do padrão para drop
para algo diferente de Nenhum?
https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html
Poderíamos ir com first
ou if_binary
, sem saber qual é a decisão certa.
@angela97lin Você está certo que alterar o valor padrão seria suficiente! Acho que first
é o caminho a seguir, pois devemos evitar recursos perfeitamente colineares, mesmo quando o número de categorias > 2. O que você acha @rpeck ?
Estava lendo isso um pouco e encontrei este link: https://inmachineswetrust.com/posts/drop-first-columns/
Principais conclusões:
O primeiro comentário de RE @rpeck : "Como outras formas de colinearidade de recursos, isso atrapalha muitas coisas na interpretabilidade, porque o efeito de uma coluna de origem original é dividido entre as duas colunas OHE. Os novos rollups SHAP de Freddy abordam isso, obviamente, mas coisas como gráficos de Importância de Recurso e Dependência Parcial ainda terão o problema."
Isso faz sentido para casos binários, mas no caso em que temos várias categorias, descartar uma coluna ainda terá esse problema.
Talvez não devêssemos fazer isso por padrão, mas devemos atualizar make_pipeline
para criar um OHE com first
como parâmetro se o estimador for um regressor linear?
Infelizmente, não tenho uma boa compreensão da matemática subjacente para fazer o julgamento, então adoraria ouvir seus pensamentos, @freddyaboulton @rpeck @dsherry
Pós-discussão com @freddyaboulton @rpeck @dsherry @chukarsten @jeremyliweishih
@angela97lin parece um bom comportamento padrão de RE. Outro bom ter: capacidade de substituir esse comportamento padrão por meio dos parâmetros do componente
@dsherry Se estou entendendo corretamente, já que estamos atualizando o valor padrão de drop
(um parâmetro), os usuários poderão substituir isso definindo o parâmetro do componente manualmente?
Cavou ao redor para ver o que era necessário para implementar isso. Em particular, eu estava curioso sobre o quão difícil seria sempre remover a classe minoritária no caso binário.
O resultado dessa escavação é:
drop
(https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html). No entanto, depois de experimentá-lo, é necessário que um valor de índice seja especificado para cada coluna. Portanto, o seguinte, que está tentando remover a categoria especificada no índice 0 para a coluna 0 e nenhum outro valor para os erros das colunas 1 e 2:import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder
X = pd.DataFrame({'col_1': ["a", "b", "b", "a", "b"],
'col_2': ["a", "b", "a", "c", "b"],
'col_3': ["a", "a", "a", "a", "a"]})
indices_to_drop = np.array([0, None, None])
ohe = OneHotEncoder(drop=indices_to_drop)
ohe.fit(X)
ValueError Traceback (most recent call last)
<ipython-input-4-a099fa2fc4a7> in <module>
----> 1 ohe.fit(X)
~/Desktop/venv/lib/python3.7/site-packages/sklearn/preprocessing/_encoders.py in fit(self, X, y)
417 self._fit(X, handle_unknown=self.handle_unknown,
418 force_all_finite='allow-nan')
--> 419 self.drop_idx_ = self._compute_drop_idx()
420 return self
421
~/Desktop/venv/lib/python3.7/site-packages/sklearn/preprocessing/_encoders.py in _compute_drop_idx(self)
394 ["Category: {}, Feature: {}".format(c, v)
395 for c, v in missing_drops])))
--> 396 raise ValueError(msg)
397 return np.array(drop_indices, dtype=object)
398
ValueError: The following categories were supposed to be dropped, but were not found in the training data.
Category: 0, Feature: 0
Category: 1, Feature: None
Category: 2, Feature: None
Acredito que isso também seja metade do que esse problema aponta: https://github.com/scikit-learn/scikit-learn/issues/16511
Uma alternativa que podemos fazer para dar suporte a isso é acompanhar manualmente quais colunas e quais valores queremos eliminar durante o ajuste. Passe os dados para scikit-learn. Em seguida, remova as colunas que armazenamos e especificamos que queremos eliminar. No entanto, isso requer alguma manipulação lógica para determinar o original (recurso, valor) do nome da coluna transformada. (Temos essa lógica em get_feature_names
mas isso nos ajuda a conectar os nomes das colunas assumindo que nada deve ser descartado...)
Tudo isso para dizer que talvez apenas usar o padrão scikit-learn if_binary
seja suficiente por enquanto, e podemos registrar um problema separado para sempre usar a classe minoritária. Honestamente, também a favor de se afastar da implementação de OHE do scikit-learn, dado o quanto tivemos que trabalhar em torno disso.
Recursos úteis:
Documento OHE: https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html
Código no scikit-learn causando inflexibilidade: https://github.com/scikit-learn/scikit-learn/blob/95119c13af77c76e150b753485c662b7c52a41a2/sklearn/preprocessing/_encoders.py#L338
Problema relacionado: https://github.com/scikit-learn/scikit-learn/issues/16511
Para usar if_binary
: scikit-learn requer que handle_unknown
seja error
. Isso não funciona bem com nossos parâmetros top_n
, que descarta tudo, exceto as categorias N principais, porque os dados a serem transformados não saberão o que fazer com as novas categorias. Como Becca observou em https://github.com/alteryx/evalml/pull/830 , teríamos que definir top_n
como Nenhum para que esses parâmetros funcionassem.
Com isso em mente, talvez seja melhor apenas rolar nosso próprio imp 🤔
Comentários muito úteis
Terceira lei do código: não farás == comparações com carros alegóricos