【问题标题】:Dummify categorical variables for logistic regression with pandas and scikit (OneHotEncoder)使用 pandas 和 scikit (OneHotEncoder) 对逻辑回归的分类变量进行虚拟化
【发布时间】:2020-04-16 06:41:53
【问题描述】:

我在scikit 阅读了有关新事物的this 博客。 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)

我在这段代码中遇到了以下 python 错误,并且我还有一些其他问题。

ValueError: X 每个样本有 10 个特征;预计 11

从头开始......这个脚本是为来自 kaggle 的“泰坦尼克号”数据集编写的。我们有五个数字列PclassAgeSibSpParchFareSexEmbarked 列是类别 male/femaleQ/S/C(这是城市名称的缩写)。

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

我遇到了三个问题:

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

  2. 我想避免使用“dummy variable trap”。现在似乎创建了太多列。我期待 1 列用于 Sex(总选项 2 - 1 以避免陷阱)和 2 用于登船。加上额外的 5 个数字列,总计 8 个。

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

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

【问题讨论】:

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


    【解决方案1】:

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

    问题 1 的答案

    在您的代码中,您在火车和测试数据上都使用了fit_transform 方法,这不是正确的做法。通常,fit_transform 仅应用于您的训练数据集,它返回一个转换器,然后仅用于transform 您的测试数据集。当您在测试数据上应用fit_transform 时,您只需使用仅在您的测试数据集中可用的分类变量的选项/级别来转换您的测试数据,并且您的测试数据很可能不包含所有选项/所有分类变量的水平,因此您的训练和测试数据集的维度会有所不同,从而导致您得到的错误。

    所以正确的做法是:

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

    问题 2 的答案

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

    所以正确的做法是:

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

    问题 3 的答案

    目前get_feature_names 方法尚未在sklearn 中实现,该方法可以使用新的虚拟列重建您的数据框。解决此问题的一种方法是将ColumnTransformer 构造中的reminder 更改为drop,并分别构造您的数据框,如下所示:

    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
    

    当您执行X_train_t.head() 时,您会得到

    希望这会有所帮助!

    【讨论】:

    • 答案在第一印象中看起来非常好。如果测试数据包含额外的分类值,我想对训练数据和测试数据执行fitfit_transform)。然后将此拟合用于变换训练并分别进行测试。在您的代码中fit_transform 是否将“fit”保存到 ct 对象中以供以后用于测试数据的转换?
    • 在训练和测试数据集上使用 fit_transfom 将导致 data leaking 问题。当您尝试将测试数据中的额外分类值添加到列转换过程中时,您对测试数据集的计算性能度量存在严重偏差和过度膨胀,当您将模型用于正如您在测试数据集中观察到的那样。正如您提到的,我的代码确实 fit_transform 将“拟合”保存到 ct 对象中,以供以后用于测试数据的转换。
    • 我现在测试了这个答案。太棒了。谢谢您的帮助。赏金尚无法发放(16 小时)。
    • 谢谢,如果回答对您有帮助,请将其标记为经过验证的回答并给予支持,以便对以后可能访问此问题的其他人有所帮助。
    【解决方案2】:
    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

    根据我的经验,我为ColumnTransfomer 构建了这个包装器,它甚至可以支持pipelinesreminder=passthrough

    这还会获取get_feature_names 的功能名称,而不是将其称为x0, x1,因为我们使用_feature_names_in 知道ColumnTransformer 中的实际列名。

    
    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())
    

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

    【讨论】:

    • 感谢您提供更多信息。您的代码似乎对更优雅的解决方案很有用
    猜你喜欢
    • 2021-04-27
    • 2015-01-03
    • 2018-05-19
    • 2015-11-11
    • 2016-06-17
    • 2014-02-20
    • 2017-01-01
    • 2017-09-25
    • 2019-07-22
    相关资源
    最近更新 更多