利用lightgbm预测adult数据集

数据探索阶段

adult数据集链接
首先下载并提取出数据

  • adult.data改为csv
  • adult.test后缀名也要改为csv,并且不能和adult重名
  • adult.name是介绍

字段名:特征字段一共有13个,y值为二分类标签,目标就是预测y值

注意:给的adult.csv是不包括字段名的,所以需要我们自己设置字段名,方便后续dataframe索引与数据处理

age:年龄(离散数值,整数)
workclass:工作类别(如私人公司、自雇、政府、无薪工作等)
fnlwgt:人口普查使用的加权值(连续数值,用于抽样权重)
education:教育水平(如学士、硕士、博士、小学等)
education-num:对应教育水平的数值表示(离散数值,整数)
marital-status:婚姻状况(如已婚、离婚、未婚、分居等)
occupation:职业类型(如销售、技术支持、管理者、军人等)
relationship:与家庭关系(如丈夫、妻子、子女、非家庭成员等)
race:种族(如白人、黑人、亚太裔、美洲印第安人等)
sex:性别(男性或女性)
capital-gain:资本收益(连续数值)
capital-loss:资本损失(连续数值)
hours-per-week:每周工作小时数(离散数值,整数)
native-country:出生国家或地区(如美国、加拿大、中国、墨西哥等)
# 附上处理源码
def get_data(type_):
    """type_: 训练还是测试数据"""
    if type_ == "train":
        data = pd.read_csv(base_dir + "/adult/adult.csv", header=None, sep=", ", engine="python")
    else:
        data = pd.read_csv(base_dir + "/adult/adult_test.csv", header=None, sep=", ", engine="python")
    print(data.head(1).values)  # 观察一下数据
    data.columns = X_feature + ["y_label"]  # X_feature就是上面一大堆的字段名
    name = "<=50K" if type_ == "train" else "<=50K."  # 训练集和测试集的y值不一样!!!!!!!!!!!!!
    data["y_label"] = data["y_label"].apply(lambda x: 0 if x == name else 1)  # 判断薪资进行标注
    return data[X_feature], data["y_label"]  # 返回X和y

值分为数值型与字符串型,大部分为离散型,只有6个字段为数值型

  1. 字符串型数据:由于字符串的数据无法作为一个特征输入模型,所以需要对数据进行编码,常见的有独热编码(和哑变量有点像,但是可以保留所有列,只有01,一共n列),标签编码(直接根据标签类数进行编码,有n种值取值范围为[0, n - 1],一共1列
  2. 数值型数据:常见的有标准化,离散的数值变量可以通过哑变量(要记得去掉一列,防止多重共线性,只有01,一共n-1列)输入线性模型。因为采用lgbm树模型所以标准化与哑变量对树的拟合差别不大,甚至容易维度爆炸

数据清洗

缺失值处理

  1. 预测建模非常重要的就是观察原始数据分布与数值分析

  2. 注意到存在问号缺失值,所以必须查看什么值缺失,如何处理缺失image-20250603183030087

  3. 缺失集中在workclass,occupation,native-country,分别是工作类别,职业类型,出生国家

    这些数值都是字符串型变量,可以通过“大众化”,也就是利用出现次数最多的工作类别,职业类型,出生国家来填补缺失值
    如果是数值型变量,可以采用均值的方式填充(如果是时间序列也可以采用上一个时间节点的数据

异常值处理

利用describe,对所有数值型变量看一下分布

image-20250603183628152

每一个特征都看过去

  1. age,fnlwgt,education-num,hours-per-week都很正常

  2. capital-gain和capital-loss的数值非常诡异,std非常大,而且上四分位数均为0,说明0是大多数,需要考虑处理这些异常值

    1. 删除特值法删除的好处在于模型不会拟合到异常值,是非常好用的方法,并且删除这几条数据对整体影响很小(不同数据集需要不同的讨论方法,如果时间序列删除这段时间就需要考虑时变特征会不会收到影响(滞后项,滑动窗口等))

    2. 二值化:由于等于0的值占大多数,所以我们可以将数值分为两类,一类等于0的值为0,一类大于0的值为1直接区分有无capital,防止过大的capital异常值影响模型判断

      缺点:无法分清capital中的数值差异,如果capital_gain为1和99999都将被归类到1,会影响模型的准确程度(也可以按照多层次划分,但是大于0的数是小部分,意味着划分出来其他类别的数字占比也会很小,可能1000个0,10个1,3个2这种特征对模型的拟合起不到很好的作用)

      1. 删除特征法:既然大部分数据都是0,那么通过删除这两个特征,就可以在保留数据集大小的情况下进行训练

        缺点:可能这个特征是树模型中具有高信息增益的靠近根节点的分裂节点(所以可以在删除特征前后对比一下准确率或者F-score)

数据分析与特征工程

数据可视化

对于分类任务,最好先看看每一个种类的分布情况,这是一个不均衡的样本!!!

img

可视化出来一下

img

接下来探究特征分布与y值之间的关系

代码如下

def feature_explain(ax, data, feature_name, y, use_int=False):
    """特征解释函数,通过输入特征名称,获取该特征的分布情况与薪水分类的关系,同于数值型的特征!!!"""
    feature_one = []
    feature_zero = []
    feature_range = []
    bins = 10  # 分桶数量

    explain_data = data.copy()  # 不要对原数据进行修改,血的教训
    bin_range = np.linspace(explain_data[feature_name].min(), explain_data[feature_name].max(), num=bins)  # 分桶
    delta =  bin_range[1] - bin_range[0]  # 间隔,后续用于确定范围与柱状图柱宽
    explain_data["y"] = y  # 把y对应上,否则加入mask后会错位

    for i in bin_range: 
        next_i = i + delta
        range_mask = (explain_data[feature_name] >= i) & (explain_data[feature_name] < i + bins)  # 取出范围内的值
        if use_int: 
            feature_range.append(f"{int(i)}-{int(next_i)}")  # 使用int就直接按照整数输出,否则保留一位小数
        else:  # 因为大部分都是整数,所以没有必要保留一位小数点(虽然用int不严谨,精度损失太大)
            feature_range.append(f"{i:.1f}-{next_i:.1f}")
        range_data = explain_data[range_mask]
        feature_zero.append(range_data[range_data["y"] == 0].count()[0])  # 取出y==0的数量
        feature_one.append(range_data[range_data["y"] == 1].count()[0])
    percent_one = ["{:.2f}%".format(feature_one[i] / (feature_one[i] + feature_zero[i]) * 100) for i in range(len(feature_one))]
    percent_zero = ["{:.2f}%".format(feature_zero[i] / (feature_one[i] + feature_zero[i]) * 100) for i in range(len(feature_zero))]
    zero_bar = plt.bar(bin_range, feature_zero, width=delta * 0.75, label="<=50k", align="center", tick_label=feature_range)  # 将0值的样本数放在下面
    one_bar = plt.bar(bin_range, feature_one, width=delta * 0.75, label=">50k", bottom=feature_zero, align="center")  # 利用bottom参数,将1值得样本数叠放在上面
    ax.bar_label(one_bar, labels=percent_one, label_type="center", fontsize=10, color="white")  # 设置柱状图的内部文字,可以控制位置
    ax.bar_label(zero_bar, labels=percent_zero, label_type="center", fontsize=10, color="white")
    ax.set_title(f"{feature_name}与薪水分类分布图")
    ax.set_ylabel("样本数")
    ax.set_xlabel(f"{feature_name}范围")
    ax.legend()

# for i in range(1, 7):
#     feature_explain(plt.subplot(int(f"23{i}")), data, continue_name[i - 1], y, use_int=True)
# 封装成函数就是方便调用,一次性能研究很多个特征与y的关系
fig = plt.figure(figsize=(12, 14))
feature_explain(plt.subplot(311), data, "age", y, use_int=True)
feature_explain(plt.subplot(312), data, "education-num", y, use_int=True)
feature_explain(plt.subplot(313), data, "hours-per-week", y, use_int=True)
plt.savefig("feature_explain.png", dpi=300, bbox_inches='tight')
```

img

ps:这个绘图还有一些不足就是占比很小的时候,很难判断占比值

对这三个特征进行分析

  1. 一般人到中年更加容易高薪,且样本中青中年人数占比更高
  2. 学历越高,高薪概率越大,不过学历高的占比也很低
  3. 每周工作时间越长, 越容易高薪**,但是大部分人的工作市场集中在33-44小时,国外8小时工作制加劳动法保护,所以40个工作小时的人数最多

特征工程

只做了3件事,字符串编码,缺失值填充,异常值处理

附上详解代码

def feature_create(data):
    """data是传入的数据"""
    dtype = data.dtypes.to_dict()  # 获得每一个特征的类型(数值如果混了字符串也是object)
    le = LabelEncoder()  # 标签encoder,是sklearn的包
    need_encoder = []  # 判断是否需要标签编码化


    for k, v in dtype.items():
        if str(v) == "object":
            print(f"{k}为字符串型:" + data[k].value_counts().index[0])
            data[k] = data[k].replace("?", data[k].value_counts().index[0])  # 缺失值按照最多出现的词填充
            need_encoder.append(k)
        else:
            print(f"{k}为数值型:" + f"{data[k].mean()}")
            data[k] = data[k].replace("?", data[k].mean())  # 按照均值,虽然用不上,这里如果用上round或者int更好一点(都是整数),用不上也不用改
    
    # 该如何处理异常值才是最优结果?
    # data["capital-gain"] = np.where(data["capital-gain"] > 0, 1, 0)  # 二值化
    # data["capital-loss"] = np.where(data["capital-loss"] > 0, 1, 0)  
    mask = (data["capital-gain"] == 0) | (data["capital-loss"] == 0)  # 删除非0值
    data = data[mask]
    # data.drop(["capital-loss", "capital-gain"], axis=1, inplace=True)  # 删除这两个特征
    
    for i in need_encoder:
        data[i] = le.fit_transform(data[i])  # 针对每一个feature进行编码
    # data[need_encoder] = data[need_encoder].astype("category")  # 将需要编码的特征转换为独热编码
    # data = pd.get_dummies(data)  # 独热编码特征对树模型没啥效果
    return data

训练与测试

训练

写了一个通用的训练函数,只需要利用sklearn的train_test_split分割训练集与验证集,直接输入就可以直接训练

并且支持多个参数,代码详解如下

def explain(y_pred, y_val):
    """预测值和实际值,不仅可以用于验证集,测试集也可以"""
    acc = accuracy_score(y_val, y_pred)  # 计算准确率
    rec = recall_score(y_val, y_pred, average='macro')  # 计算召回率,macro是正负召回率平均,micro只考虑正召回率

    print(f"准确率 (Accuracy): {acc:.4f}")
    print(f"召回率 (Recall):   {rec:.4f}")
    print(classification_report(y_val, y_pred, target_names=["<=50K", ">50K"], digits=4))  # 这是非常好用的展示分类效果的函数,强烈推荐,sklearn里面和accuracy在同一个包里

def train_model(X_train, y_train, X_val, y_val, model, grid_search=False, name=False):
    """
    可选参数
    :param model: 选用的模型,xgb或者lgb
    :param grid_search: 是否采用网格搜索
    :param name:模型的名称
    :return: 返回模型值
    支持xgb和lgb的模型训练(这俩树模型一起调用sklearn的接口)
    """
    model_name = {LGBMClassifier: "lgb", XGBClassifier: "xgb"}
    if grid_search:
        param_grid = {
            "learning_rate": [0.01, 0.05, 0.1, ],
            "max_depth": [3, 5, 7],
            "subsample": [0.8, 1.0],
            "n_estimators": [100, 200, 300]
        }

        grid_search = GridSearchCV(
            estimator=model(random_state=42, objective='binary'),  # 如果是二分类
            param_grid=param_grid, # 参数网格
            cv=3,  # 交叉验证次数
            scoring='accuracy',  # 利用准确率进行评分
            n_jobs=10 # 采用核心数量
        )

        grid_search.fit(X_train, y_train)  # 训练比较久,xgb更久,建议lgb
        best_params = grid_search.best_params_
        print("最佳参数:", best_params)
    else:
        best_params = {'learning_rate': 0.01, 'max_depth': 7, 'n_estimators': 100, 'subsample': 1}  # 可以随意改参数,手动试参数(
    use_model = model(**best_params, random_state=42, objective='binary', n_jobs=4)  # is_unbalance=True看情况加
    use_model.fit(X_train, y_train)


    y_pred = use_model.predict(X_val)  # 预测值
    acc = accuracy_score(y_val, y_pred)  # 准确率
    explain(y_pred, y_val)  # 这里是验证的展示函数
    if type(name) == str:
        joblib.dump(use_model, get_dir_name(True) + f"/models/{model_name[model]}_{name}_acc{acc:.3f}.pkl")
    return use_model

# 在准备好数据之后就可以进行处理了 
data = feature_create(data)  # 前面提到的特征工程
print(data.iloc[0])  # 展示一下输入的内容
X_train, X_test, y_train, y_test = train_test_split(data, y, test_size=0.2)  # 划分测试集和验证集,验证集占总数的20%
model = train_model(X_train, y_train, X_test, y_test, LGBMClassifier, grid_search=False)  # 这里可以调整参数

最后结果如下(下面那个矩阵就是classification_report的结果,非常清晰明了)

img

测试

使用封装好的函数,测试也只是调用自己之前写过的函数

img

EX:查看特征重要性

img

plt.show()booster = model.booster_
fig = plt.figure(figsize=(20, 6))
lgb.plot_importance(booster, max_num_features=20, ax=plt.subplot(121), importance_type="gain", figsize=(10, 6))# 不同的importance_type对应了不同重要性方法
plt.title("信息增益代表的重要性")
lgb.plot_importance(booster, max_num_features=20, ax=plt.subplot(122), importance_type="split", figsize=(10, 6))
plt.title("分裂次数代表的重要性")
plt.show()
fig.savefig("feature_importance.png", dpi=300, bbox_inches='tight')

重点讲一下importance_type

就是指定 按什么指标衡量“重要性”

  • split:计数特征被用来做分裂的次数
  • gain:累计该特征在所有节点带来的 信息增益(loss 降低量)
  • 想同时看两种就传入ax=fig.subplot(121)或者122吧(放到同一张图)

为什么relationship明明信息增益很大,但是分裂次数很少呢

答:因为其更靠近根节点的分裂节点,分裂次数更少,但是其信息增益很大

为什么age明明在分裂次数中排第二,而且和第一相差无几,但是其信息增益才为第一的 1 5 \frac{1}{5} 51

答:因为其更靠近叶节点,末端信息增益的均值小,虽然分裂次数多,但是提供总的信息增益不行

靠近根节点覆盖的样本多,因此对损失下降做出了更大贡献,信息增益更大

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值