sklearn.LabelEncoder mit nie zuvor gesehenen Werten

73

Wenn a sklearn.LabelEncoderin ein Trainingsset eingebaut wurde, kann es brechen, wenn es bei Verwendung in einem Testset auf neue Werte stößt.

Die einzige Lösung, die ich dafür finden könnte, besteht darin, alles Neue im Testsatz (dh keine zu einer vorhandenen Klasse gehörend) zuzuordnen "<unknown>"und anschließend explizit eine entsprechende Klasse hinzuzufügen LabelEncoder:

# train and test are pandas.DataFrame's and c is whatever column
le = LabelEncoder()
le.fit(train[c])
test[c] = test[c].map(lambda s: '<unknown>' if s not in le.classes_ else s)
le.classes_ = np.append(le.classes_, '<unknown>')
train[c] = le.transform(train[c])
test[c] = le.transform(test[c])

Das funktioniert, aber gibt es eine bessere Lösung?

Aktualisieren

Wie @sapo_cosmico in einem Kommentar hervorhebt, scheint das oben Gesagte nicht mehr zu funktionieren, da ich davon ausgehe, dass es sich um eine Implementierungsänderung handelt LabelEncoder.transform, die jetzt zu verwenden scheint np.searchsorted(ich weiß nicht, ob dies zuvor der Fall war). Anstatt die <unknown>Klasse an die LabelEncoderListe der bereits extrahierten Klassen anzuhängen, muss sie in sortierter Reihenfolge eingefügt werden:

import bisect
le_classes = le.classes_.tolist()
bisect.insort_left(le_classes, '<unknown>')
le.classes_ = le_classes

Da sich dies jedoch insgesamt ziemlich klobig anfühlt, bin ich mir sicher, dass es dafür einen besseren Ansatz gibt.

cjauvin
quelle

Antworten:

42

Aufgrund dieses Problems mit unsichtbaren Daten wechselte ich schließlich zu Pandas ' get_dummies .

  • Erstellen Sie die Dummies auf den Trainingsdaten
    dummy_train = pd.get_dummies(train)
  • Erstellen Sie die Dummies in den neuen (unsichtbaren Daten)
    dummy_new = pd.get_dummies(new_data)
  • Indizieren Sie die neuen Daten erneut in die Spalten der Trainingsdaten und füllen Sie die fehlenden Werte mit 0
    dummy_new.reindex(columns = dummy_train.columns, fill_value=0)

Tatsächlich werden alle neuen Funktionen, die kategorisch sind, nicht in den Klassifikator aufgenommen, aber ich denke, das sollte keine Probleme verursachen, da es nicht wissen würde, was mit ihnen zu tun ist.

sapo_cosmico
quelle
2
Meinst dummies.columnsdu stattdessen dummy_train.columns?
Kevin Markham
1
@ KevinMarkham ein großes Lob an Sie, Sir, hat einen Fehler entdeckt, der seit fast einem Jahr da ist :)
sapo_cosmico
Speichern Sie das Modell beim Speichern (Beizen) dummy_train.columnsin einer eigenen Datei?
Matthiash
2
@matthiash Im Allgemeinen werde ich es in einem Pipeline-Objekt verwenden. Ich kann nicht sagen, dass ich genug über Beizen weiß, ich vermeide es im Allgemeinen, würde aber eine Vermutung wagen, dass der Zustand in der Pipeline diese Spalten halten und behalten sollte
sapo_cosmico
@matthiash In meinem Fall habe ich die Spalten in derselben Datei wie das Modell gespeichert. Stellen Sie einfach sicher, dass Sie in derselben Reihenfolge schreiben und lesen!
Shikhanshu
34

LabelEncoder ist im Grunde ein Wörterbuch. Sie können es extrahieren und für zukünftige Codierungen verwenden:

from sklearn.preprocessing import LabelEncoder

le = preprocessing.LabelEncoder()
le.fit(X)

le_dict = dict(zip(le.classes_, le.transform(le.classes_)))

Rufen Sie die Bezeichnung für ein einzelnes neues Element ab. Wenn das Element fehlt, setzen Sie den Wert als unbekannt

le_dict.get(new_item, '<Unknown>')

Beschriftungen für eine Dataframe-Spalte abrufen:

df[your_col] = df[your_col].apply(lambda x: le_dict.get(x, <unknown_value>))
Rani
quelle
Diese Antwort ist ziemlich präzise und effektiv;) (Gegenstimme)
Ausgestoßener
24

Ich habe eine Klasse erstellt, um dies zu unterstützen. Wenn Sie ein neues Etikett haben, wird es als unbekannte Klasse zugewiesen.

from sklearn.preprocessing import LabelEncoder
import numpy as np


class LabelEncoderExt(object):
    def __init__(self):
        """
        It differs from LabelEncoder by handling new classes and providing a value for it [Unknown]
        Unknown will be added in fit and transform will take care of new item. It gives unknown class id
        """
        self.label_encoder = LabelEncoder()
        # self.classes_ = self.label_encoder.classes_

    def fit(self, data_list):
        """
        This will fit the encoder for all the unique values and introduce unknown value
        :param data_list: A list of string
        :return: self
        """
        self.label_encoder = self.label_encoder.fit(list(data_list) + ['Unknown'])
        self.classes_ = self.label_encoder.classes_

        return self

    def transform(self, data_list):
        """
        This will transform the data_list to id list where the new values get assigned to Unknown class
        :param data_list:
        :return:
        """
        new_data_list = list(data_list)
        for unique_item in np.unique(data_list):
            if unique_item not in self.label_encoder.classes_:
                new_data_list = ['Unknown' if x==unique_item else x for x in new_data_list]

        return self.label_encoder.transform(new_data_list)

Die Beispielverwendung:

country_list = ['Argentina', 'Australia', 'Canada', 'France', 'Italy', 'Spain', 'US', 'Canada', 'Argentina, ''US']

label_encoder = LabelEncoderExt()

label_encoder.fit(country_list)
print(label_encoder.classes_) # you can see new class called Unknown
print(label_encoder.transform(country_list))


new_country_list = ['Canada', 'France', 'Italy', 'Spain', 'US', 'India', 'Pakistan', 'South Africa']
print(label_encoder.transform(new_country_list))
Vinoj John Hosan
quelle
Ihre Lösung ist bei weitem die einfachste. Vielen Dank!
Aleksandar Makragić
8

Ich habe den Eindruck, dass das, was Sie getan haben, dem sehr ähnlich ist, was andere Menschen in dieser Situation tun.

Es wurden einige Anstrengungen unternommen, um die Möglichkeit zum Codieren nicht sichtbarer Beschriftungen zum LabelEncoder hinzuzufügen (siehe insbesondere https://github.com/scikit-learn/scikit-learn/pull/3483 und https://github.com/scikit-learn/). scikit-learn / pull / 3599 ), aber das bestehende Verhalten zu ändern ist tatsächlich schwieriger, als es auf den ersten Blick scheint.

Im Moment sieht es so aus, als ob der Umgang mit "Out-of-Vocabulary" -Labels einzelnen Benutzern von Scikit-Learn überlassen bleibt.

lmjohns3
quelle
6

Ich bin kürzlich auf dieses Problem gestoßen und konnte eine ziemlich schnelle Lösung für das Problem finden. Meine Antwort löst ein wenig mehr als nur dieses Problem, aber es wird auch für Ihr Problem leicht funktionieren. (Ich finde es ziemlich cool)

Ich arbeite mit Pandas-Datenrahmen und habe ursprünglich den sklearns labelencoder () verwendet, um meine Daten zu codieren, die ich dann für andere Module in meinem Programm verwenden würde.

Der Label-Encoder in der Vorverarbeitung von sklearn kann dem Codierungsalgorithmus jedoch keine neuen Werte hinzufügen. Ich habe das Problem des Codierens mehrerer Werte und des Speicherns der Zuordnungswerte sowie des Hinzufügens neuer Werte zum Encoder durch gelöst (hier eine grobe Übersicht über meine Arbeit):

encoding_dict = dict()
for col in cols_to_encode:
    #get unique values in the column to encode
    values = df[col].value_counts().index.tolist()

    # create a dictionary of values and corresponding number {value, number}
    dict_values = {value: count for value, count in zip(values, range(1,len(values)+1))}

    # save the values to encode in the dictionary
    encoding_dict[col] = dict_values

    # replace the values with the corresponding number from the dictionary
    df[col] = df[col].map(lambda x: dict_values.get(x))

Anschließend können Sie das Wörterbuch einfach in einer JSON-Datei speichern und es abrufen und einen beliebigen Wert hinzufügen, indem Sie einen neuen Wert und den entsprechenden ganzzahligen Wert hinzufügen.

Ich werde einige Gründe für die Verwendung von map () anstelle von replace () erläutern. Ich fand heraus, dass die Verwendung der pandas replace () -Funktion über eine Minute dauerte, um ungefähr 117.000 Codezeilen zu durchlaufen. Die Verwendung der Karte brachte diese Zeit auf etwas mehr als 100 ms.

TLDR: Anstatt die Vorverarbeitung von sklearns zu verwenden, arbeiten Sie einfach mit Ihrem Datenrahmen, indem Sie ein Zuordnungswörterbuch erstellen und die Werte selbst zuordnen.

Ethan Kulla
quelle
3

Ich kenne zwei Entwickler, die daran arbeiten, Wrapper um Transformatoren und Sklearn-Pipelines zu bauen. Sie verfügen über 2 robuste Encodertransformatoren (einen Dummy- und einen Label-Encoder), die unsichtbare Werte verarbeiten können. Hier ist die Dokumentation zu ihrer Skutil-Bibliothek. Suche nach skutil.preprocessing.OneHotCategoricalEncoderoder skutil.preprocessing.SafeLabelEncoder. In ihren SafeLabelEncoder()werden unsichtbare Werte automatisch auf 999999 codiert.

Jason
quelle
2
Haben sie nicht versucht, sich sklearnselbst zu unterwerfen ? Dies ist ein universelles Problem. Offensichtlich parametrisieren wir den default_label_value.
smci
Nur neugierig, würde es überhaupt einen Vorteil haben, den Standardwert -1 anstelle von 999999 festzulegen? Angenommen, meine Kategorie hat 56 Kategorien. Ich denke, ich würde es vorziehen, wenn meine Beschriftungen zwischen -1 und 56 liegen, anstatt zwischen 0 und 56, wobei ein 999999 am Ende angeheftet ist. Wenn Sie die kategoriale Transformation vor dem Skalieren durchführen, können Sie die Zahlen auf einer Skala von 0 bis 1 zerquetschen oder sie richtig skalieren / zentrieren, ja? Wenn Sie 999999 verwenden, würde dies die Option für die weitere Verarbeitung ausschließen und möglicherweise die Skalierung Ihres Features um eine ganz andere Größe erweitern. Überdenke ich
TaylorV
Normalerweise werden in den meisten meiner Workflows unsichtbare Werte während der Inferenz- / Vorhersagezeit aus der Pipeline herausgefiltert. Für mich ist es also egal, ob es als -1 oder 999999 codiert ist.
Jason
2

Ich habe versucht, dieses Problem zu lösen, und zwei praktische Möglichkeiten gefunden, kategoriale Daten aus Zug- und Testsätzen mit und ohne LabelEncoder zu codieren. Neue Kategorien werden mit einer bekannten Kategorie "c" (wie "andere" oder "fehlende") gefüllt. Die erste Methode scheint schneller zu funktionieren. Hoffe das wird dir helfen.

import pandas as pd
import time
df=pd.DataFrame()

df["a"]=['a','b', 'c', 'd']
df["b"]=['a','b', 'e', 'd']


#LabelEncoder + map
t=time.clock()
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
suf="_le"
col="a"
df[col+suf] = le.fit_transform(df[col])
dic = dict(zip(le.classes_, le.transform(le.classes_)))
col='b'
df[col+suf]=df[col].map(dic).fillna(dic["c"]).astype(int)
print(time.clock()-t)

#---
#pandas category

t=time.clock()
df["d"] = df["a"].astype('category').cat.codes
dic =df["a"].astype('category').cat.categories.tolist()
df['f']=df['b'].astype('category',categories=dic).fillna("c").cat.codes
df.dtypes
print(time.clock()-t)
Yury Wallet
quelle
Im #pandas categoryAnsatz gibt die Zeile df['f']=df['b'].astype('category',categories=dic)........diesen Fehler aus:TypeError: astype() got an unexpected keyword argument 'categories'
edesz
2

Hier ist mit der Verwendung der relativ neuen Funktion von Pandas. Die Hauptmotivation ist, dass maschinelle Lernpakete wie 'lightgbm' Pandas-Kategorien als Feature-Spalten akzeptieren können und in einigen Situationen besser sind als die Verwendung einer Onehoten-Codierung. In diesem Beispiel gibt der Transformator eine Ganzzahl zurück, kann aber auch den Datumstyp ändern und durch die nicht sichtbaren kategorialen Werte durch -1 ersetzen.

from collections import defaultdict
from sklearn.base import BaseEstimator,TransformerMixin
from pandas.api.types import CategoricalDtype
import pandas as pd
import numpy as np

class PandasLabelEncoder(BaseEstimator,TransformerMixin):
    def __init__(self):
        self.label_dict = defaultdict(list)

    def fit(self, X):
        X = X.astype('category')
        cols = X.columns
        values = list(map(lambda col: X[col].cat.categories, cols))
        self.label_dict = dict(zip(cols,values))
        # return as category for xgboost or lightgbm 
        return self

    def transform(self,X):
        # check missing columns
        missing_col=set(X.columns)-set(self.label_dict.keys())
        if missing_col:
            raise ValueError('the column named {} is not in the label dictionary. Check your fitting data.'.format(missing_col)) 
        return X.apply(lambda x: x.astype('category').cat.set_categories(self.label_dict[x.name]).cat.codes.astype('category').cat.set_categories(np.arange(len(self.label_dict[x.name]))))


    def inverse_transform(self,X):
        return X.apply(lambda x: pd.Categorical.from_codes(codes=x.values,
                                                           categories=self.label_dict[x.name]))

dff1 = pd.DataFrame({'One': list('ABCC'), 'Two': list('bccd')})
dff2 = pd.DataFrame({'One': list('ABCDE'), 'Two': list('debca')})


enc=PandasLabelEncoder()
enc.fit_transform(dff1)
One Two
0   0   0
1   1   1
2   2   1
3   2   2
dff3=enc.transform(dff2)
dff3
    One Two
0   0   2
1   1   -1
2   2   0
3   -1  1
4   -1  -1
enc.inverse_transform(dff3)
One Two
0   A   d
1   B   NaN
2   C   b
3   NaN c
4   NaN NaN
Aung
quelle
0

Ich habe das gleiche Problem und stellte fest, dass mein Encoder irgendwie Werte in meinem Spalten-Datenrahmen mischte. Nehmen wir an, Sie führen Ihren Encoder für mehrere Spalten aus. Wenn Sie Beschriftungen Nummern zuweisen, schreibt der Encoder automatisch Zahlen darauf und stellt manchmal fest, dass Sie zwei verschiedene Spalten mit ähnlichen Werten haben. Um das Problem zu lösen, habe ich für jede Spalte in meinem Pandas DataFrame eine Instanz von LabelEncoder () erstellt, und ich habe ein schönes Ergebnis.

encoder1 = LabelEncoder()
encoder2 = LabelEncoder()
encoder3 = LabelEncoder()

df['col1'] = encoder1.fit_transform(list(df['col1'].values))
df['col2'] = encoder2.fit_transform(list(df['col2'].values))
df['col3'] = encoder3.fit_transform(list(df['col3'].values))

Grüße!!

nonameforpirate
quelle
0

LabelEncoder () sollte nur für die Codierung von Zieletiketten verwendet werden. Verwenden Sie zum Codieren kategorialer Features OneHotEncoder (), das unsichtbare Werte verarbeiten kann: https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html#sklearn.preprocessing.OneHotEncoder

Alex
quelle
Was ist, wenn die Funktionen eine Kardinalität von mehr als 10000+ haben?
Jeevs
Kommt auf den Fall an. Es sind mehrere Lösungen möglich. Vielleicht sollten Sie über Bucketing oder Einbettung nachdenken. Es ist schwierig, ohne den wirklichen Fall zu verstehen.
Alex
0

Wenn noch jemand danach sucht, ist hier mein Fix.

Angenommen, Sie haben
enc_list: Liste der bereits codierten Variablennamen
enc_map: das Wörterbuch mit Variablen aus enc_listund die entsprechende codierte Zuordnung
df: Datenrahmen mit Werten einer Variablen, die nicht in vorhanden sindenc_map

Dies funktioniert unter der Annahme, dass Sie in den codierten Werten bereits die Kategorie "NA" oder "Unbekannt" haben

for l in enc_list:  

    old_list = enc_map[l].classes_
    new_list = df[l].unique()
    na = [j for j in new_list if j not in old_list]
    df[l] = df[l].replace(na,'NA')
Preethi
quelle
-1

Wenn es nur darum geht, ein Modell zu trainieren und zu testen, warum nicht einfach den gesamten Datensatz beschriften? Verwenden Sie dann die generierten Klassen aus dem Encoder-Objekt.

encoder = LabelEncoder()
encoder.fit_transform(df["label"])
train_y = encoder.transform(train_y)
test_y = encoder.transform(test_y)
Namrata Tolani
quelle
10
Ich glaube, dies wäre ein Fall von Datenleckage (Kardinal-ML-Sünde).
Cjauvin
2
Dies scheint eine hervorragende Lösung zu sein. Wie ich sehe, gibt es kein Problem mit Leckagen, wenn wir nur eine Variable codieren.
Regi Mathew
Für neue Daten siehe meine Lösung: stackoverflow.com/questions/45495308/…
Namrata Tolani
1
Diese Lösung funktioniert, wenn wir vorher feste Testdaten haben. Dies ist jedoch in realen Anwendungen nicht möglich, in denen uns Testdaten meistens unbekannt sind.
Sufyan Khot