使用pandas和scikit(OneHotEncoder)进行分类变量化以进行逻辑回归

时间:2019-12-25 19:39:20

标签: python pandas numpy machine-learning scikit-learn

我读了https://medium.com/dunder-data/from-pandas-to-scikit-learn-a-new-exciting-workflow-e88e2271ef62这个博客,介绍了scikit中的新事物。带字符串的OneHotEncoder似乎是一个有用的功能。在我尝试使用此功能的下方

import pandas as pd

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer

cols = ['Survived', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

train_df = pd.read_csv('../../data/train.csv', usecols=cols)
test_df = pd.read_csv('../../data/test.csv', usecols=[e for e in cols if e != 'Survived'])

train_df.dropna(inplace=True)
test_df.dropna(inplace=True)

X_train = train_df.drop("Survived", axis=1)
Y_train = train_df["Survived"]
X_test = test_df.copy()

ct = ColumnTransformer([("onehot", OneHotEncoder(sparse=False), ['Sex', 'Embarked'])], remainder='passthrough')

X_train_t = ct.fit_transform(train_df)
X_test_t  = ct.fit_transform(test_df)

print(X_train_t[0])
print(X_test_t[0])

# [ 0.    1.    0.    0.    1.    0.    3.   22.    1.    0.    7.25]
# [ 0.    1.    0.    1.    0.          3. 34.5     0.    0.  7.8292]

logreg = LogisticRegression(max_iter=5000)
logreg.fit(X_train_t, Y_train)
Y_pred = logreg.predict(X_test_t) # ValueError: X has 10 features per sample; expecting 11
acc_log = round(logreg.score(X_train, Y_train) * 100, 2)

print(acc_log)

我在使用此代码ValueError: X has 10 features per sample; expecting 11时遇到python错误,并且还有其他一些担忧。

从头开始..此脚本是为kaggle中的“ titanic”数据集编写的。我们有5个数字列Pclass,Age,SibSp,Parch和Fare。 “性别”和“登载”列是“男性/女性”和“ Q / S / C”类别(这是城市名称的缩写)。

我从OneHotEncoder了解到的是,它通过放置其他列来创建伪变量。好吧,实际上ct.fit_transform()的输出现在不再是pandas数据帧,而是一个numpy数组。但是从打印调试语句中可以看出,现在的原始列已超过了7列。

我遇到三个问题:

  1. 由于某种原因,test.csv的列减少了一列。这将向我表明,其中一种类别的选择较少。为了解决这个问题,我必须在两个训练和测试数据上找到类别中所有可用的选项。然后使用这些选项(例如,男性/女性)分别转换火车和测试数据。我不知道如何使用我正在使用的工具(熊猫,scikit等)来做到这一点。再三考虑..检查了数据之后,我在test.csv中找不到缺少的选项。

  2. 我想避免“虚拟变量陷阱”。 https://medium.com/datadriveninvestor/dummy-variable-trap-c6d4a387f10a现在似乎创建了太多列。我原本希望1列用于“性别”(共有2-1条以避免陷入陷阱)和2列。再加上5个数字列,总计8个。

  3. 我不再识别转换的输出。我希望有一个新的数据框,其中新的虚拟列已给出了自己的名称,例如Sex_male(1/0)Embarked_Q(1/0)和Embarked_S(1/0)

我只习惯使用gretl,在那里将变量虚拟化,而忽略一个选项是很自然的。我不知道在python中我做错了还是这种情况不是标准scikit工具包的一部分。有什么建议吗?也许我应该为此编写一个自定义编码器?

3 个答案:

答案 0 :(得分:10)

我将尝试单独回答您所有的问题。

问题1的答案

在您的代码中,您已经在训练和测试数据中使用了fit_transform方法,但这并不是正确的方法。通常,fit_transform仅应用于火车数据集,它返回一个变压器,然后将其用于transform测试数据集。在测试数据上应用fit_transform时,仅使用仅在测试数据集中可用的分类变量的选项/级别来转换测试数据,并且很有可能测试数据可能不包含所有所有类别变量的选项/级别,因此,火车和测试数据集的尺寸会有所不同,从而导致出现错误。

所以正确的做法是:

X_train_t = ct.fit_transform(X_train)
X_test_t  = ct.transform(X_test)

问题2的答案

如果要避免“虚拟变量陷阱”,可以在{{中创建drop对象时使用参数first(通过将其设置为OneHotEncoder)。 1}},这将导致仅为ColumnTransformer创建一列,为sex创建两列,因为它们分别具有两个和三个选项/级别。

所以正确的做法是:

Embarked

问题3的答案

到目前为止,ct = ColumnTransformer([("onehot", OneHotEncoder(sparse=False, drop="first"), ['Sex','Embarked'])], remainder='passthrough') 尚未实现get_feature_names方法,该方法可以使用新的虚拟列重建数据帧。解决此问题的一种方法是在sklearn构造中将reminder更改为drop并分别构造数据帧,如下所示:

ColumnTransformer

这将导致如下结果:

enter image description here

您的最终代码将如下所示:

ct = ColumnTransformer([("onehot", OneHotEncoder(sparse=False, drop="first"), ['Sex', 'Embarked'])], remainder='drop')
A = pd.concat([X_train.drop(["Sex", "Embarked"], axis=1), pd.DataFrame(X_train_t, columns=ct.get_feature_names())], axis=1) 
A.head()

当您import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer cols = ['Survived', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked'] train_df = pd.read_csv('train.csv', usecols=cols) test_df = pd.read_csv('test.csv', usecols=[e for e in cols if e != 'Survived']) cols = ['Survived', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked'] train_df = train_df.dropna() test_df = test_df.dropna() train_df = train_df.reset_index(drop=True) test_df = test_df.reset_index(drop=True) X_train = train_df.drop("Survived", axis=1) Y_train = train_df["Survived"] X_test = test_df.copy() categorical_values = ['Sex', 'Embarked'] X_train_cont = X_train.drop(categorical_values, axis=1) X_test_cont = X_test.drop(categorical_values, axis=1) ct = ColumnTransformer([("onehot", OneHotEncoder(sparse=False, drop="first"), categorical_values)], remainder='drop') X_train_categorical = ct.fit_transform(X_train) X_test_categorical = ct.transform(X_test) X_train_t = pd.concat([X_train_cont, pd.DataFrame(X_train_categorical, columns=ct.get_feature_names())], axis=1) X_test_t = pd.concat([X_test_cont, pd.DataFrame(X_test_categorical, columns=ct.get_feature_names())], axis=1) logreg = LogisticRegression(max_iter=5000) logreg.fit(X_train_t, Y_train) Y_pred = logreg.predict(X_test_t) acc_log = round(logreg.score(X_train_t, Y_train) * 100, 2) print(acc_log) 80.34 时,您会得到

enter image description here

希望这会有所帮助!

答案 1 :(得分:4)

  1. @Parthasarathy Subburaj的答案中建议采取建议的做法,但我在Kaggle或其他比赛中看到过,人们可以使用这些数据进行完整的训练(训练+测试)。如果您想尝试相同的方法,请使用以下格式
ct.fit(X_complete)
X_train_t, X_test_t  = ct.transform(X_test), ct.transform(X_test)
  1. 是的,使用drop='first'可以解决此问题。同时,请记住,对于诸如神经网络甚至决策树之类的非线性模型来说,多重共线性问题并不是什么大问题。我认为这就是为什么不将其保留为默认arg参数值的原因。

  2. get_feature_names并未针对sklearn中的管道和其他内容进行详尽的实现。因此,他们也支持ColumnTransformer中的complete。

根据我的经验,我为ColumnTransfomer构建了这个包装器,即使它具有pipelinesreminder=passthrough也可以支持。

这也为get_feature_names选择了功能名称,而不是将其称为x0, x1,因为我们知道ColumnTransformer中使用_feature_names_in的实际列名称。


from sklearn.compose import ColumnTransformer
from sklearn.utils.validation import check_is_fitted

def _get_features_out(name, trans, features_in):
    if hasattr(trans, 'get_feature_names'):
        return [name + "__" + f for f in
                                  trans.get_feature_names(features_in)]
    else:
        return features_in


class NamedColumnTransformer(ColumnTransformer):
    def get_feature_names(self):
        check_is_fitted(self)
        feature_names = []
        for name, trans, features, _ in self._iter(fitted=True):
            if trans == 'drop':
                continue
            if trans == 'passthrough':
                feature_names.extend(self._feature_names_in[features])
            elif hasattr(trans, '_iter'):
                for _, op_name, t in trans._iter():
                    features=_get_features_out(op_name, t, features)
                feature_names.extend(features)
            elif not hasattr(trans, 'get_feature_names'):
                raise AttributeError("Transformer %s (type %s) does not "
                                     "provide get_feature_names."
                                     % (str(name), type(trans).__name__))
            else:
                feature_names.extend(_get_features_out(name, trans, features))

        return feature_names

现在,例如

from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LogisticRegression

# you can fetch the titanic dataset using this
X, y = fetch_openml("titanic", version=1,
                    as_frame=True, return_X_y=True)

# removing the columns which you are not using 
X.drop(['name', 'ticket', 'cabin', 'boat', 'body', 'home.dest'],
       axis=1, inplace=True)

X.dropna(inplace=True)
X.reset_index(drop=True, inplace=True)
y = y[X.index]

categorical_values = ['sex', 'embarked']

ct = NamedColumnTransformer([("onehot", OneHotEncoder(
    sparse=False, drop="first"), categorical_values)], remainder='passthrough')


clf = Pipeline(steps=[('preprocessor', ct),
                      ('classifier', LogisticRegression(max_iter=5000))])

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

clf.fit(X_train, y_train)


clf[0].get_feature_names()
# ['onehot__sex_male',
#  'onehot__embarked_Q',
#  'onehot__embarked_S',
#  'pclass',
#  'age',
#  'sibsp',
#  'parch',
#  'fare']
pd.DataFrame(clf[0].transform(X_train), columns=clf[0].get_feature_names())

enter image description here

您也可以尝试NamedColumnTransformer来获得ColumnTransformer here的更有趣的示例。

答案 2 :(得分:-1)

您的问题不在编码中,它可以正常工作,请参见

print(X_train_t[0])
print(X_test_t[0])

# [ 0.    1.    0.    0.    1.    0.    3.   22.    1.    0.    7.25]
# [ 0.    1.    0.    1.    0.          3. 34.5     0.    0.  7.8292]

在性别/内隐变量中不再有男/女或南安普敦/瑟堡/皇后镇的地方,而不再是数字值。

注意:X_train_t有11列,而X_test_t有10列。

您的问题在这里

train_df = pd.read_csv('../../data/train.csv', usecols=cols)
test_df = pd.read_csv('../../data/test.csv', usecols=[e for e in cols if e != 'Survived'])

您应该同时删除Survivedtest_df中的train_df列,就像这样:

train_df = pd.read_csv('../../data/train.csv', usecols=[e for e in cols if e != 'Survived'])
test_df = pd.read_csv('../../data/test.csv', usecols=[e for e in cols if e != 'Survived'])

,因此两个数据框只有10列。 Survived列是您的Y,位于Y_train中。

Y_pred将正确地作为您可以与真实测试数据进行比较的预测:

Y_pred = logreg.predict(X_test_t)
Y_test = test_df["Survived"]
print(Y_pred)
print(Y_test)
print(Y_pred == Y_test)

[1 1 1 0 ... 0 1]
[0 0 1 0 ... 0 1]
[False False True True ... True True]