568. ICR - Identifying Age-Related Conditions | icr-identify-age-related-conditions
坦白说,我对这个结果有些震惊。我原本预料到排名会有较大变动,但确实没想到能进入前十名。
业务背景:https://www.kaggle.com/competitions/icr-identify-age-related-conditions/overview
数据背景:https://www.kaggle.com/competitions/icr-identify-age-related-conditions/data
我首先专注于预测个体是否患有特定的年龄相关疾病,而不是直接预测类别。实践证明,这种方法比单纯预测类别更有效。本质上,我创建了一个专注于特定疾病的集成预测器系统。主要集成模型采用了XGBoost和TabPFN。
我的主要插补策略是使用XGBoost预测大部分缺失值,而不是直接删除、填充0值、均值或众数等。
与使用中位数插补相比,这种方法使公开分数提高了0.04(私有分数无变化)。
在本次提交中,训练集中有两个字段包含大部分缺失值。由于无法确定这种情况是否在其他数据集中也成立,我创建了一个函数来循环插补这些值,当目标中多个列存在缺失值时,在后续插补运行中会删除其他列。
# 用于浮点字段
def imputeRegressor(df, field, dl=[]):
print(f"使用XGBoost为{field}进行回归插补")
print(f"开始时的NaN值: {df[field].isna().sum().sum()}")
# 多个字段可能同时包含NA
drop_list = set(df.columns).intersection(set(dl))
imputeDF = df.copy()
imputeDF.drop(drop_list, axis=1, inplace=True)
impute_X = imputeDF.drop(field, axis=1).dropna()
impute_y = imputeDF[field]
imputeDF = impute_X.join(impute_y)
trainDF = imputeDF[imputeDF[field].notna()].dropna()
train_X = pd.get_dummies(trainDF.drop(field, axis=1))
train_y = trainDF[field]
# 只能解决其他字段不为na的情况
testDF = imputeDF[imputeDF[field].isna()]
test_X = testDF.drop(field, axis=1)
test_y = testDF[field]
test_X.dropna(inplace=True)
testDF = test_X.join(test_y)
test_y = testDF[field]
print(f"预测训练集{train_X.shape}和测试集{test_X.shape}")
if (len(test_X)) == 0:
print("无需预测")
return
# 处理不同特征数量
colsToUse = set(train_X.columns).intersection(set(test_X.columns))
train_X = train_X[colsToUse]
test_X = test_X[colsToUse]
modelOne = XGBRegressor(n_estimators=200).fit(train_X, train_y)
preds = modelOne.predict(pd.get_dummies(test_X))
predictedDF = test_X
predictedDF[field] = preds.astype(float)
# 将结果保存回基础DF
for i, row in predictedDF.iterrows():
df.loc[i, field] = row[field]
remainingToSolve = df[df[field].isna()].count()
print(f"结束时的NaN值: {df[field].isna().sum().sum()}")
除了上述插补步骤和将分类变量转换为整数外,我没有对浮点数据进行任何操作。存在相当多具有异常值的字段可能值得处理,至少有一个字段似乎达到了最大值。在这种情况下,我没有时间进行深入探索。
未经微调的XGBoost分类器(微调会导致过拟合)
TabPFN
作为XGBoost的评估指标,我使用了平衡对数损失(balanced log loss)和二分类逻辑回归目标。
每个模型都通过交叉验证进行训练,以平衡对数损失评分,并选择每次运行中表现最佳的模型。
希腊字母文件包含Alpha字段,该字段区分了可能导致Class值为1的具体疾病类型。希腊字母文件的其余部分被忽略。
我们不是使用单一集成模型预测Class,而是训练4个模型。我们训练集成模型来预测:
对于阳性预测,我们取B、D、G阳性预测的最大值,并与class的阳性预测取平均值。
对于阴性预测,我们取B、D、G阴性预测的最小值,并与class的阴性预测取平均值。
注意:在下面的代码中,我将B、D和G的预测器称为Beta、Delta和Gamma。这不要与同名字段混淆(这些字段未被使用)。
def ensembler(train_X, modelAlpha, modelBeta, modelDelta, modelGamma, postProcess=False):
"""
基于各自特征为Train_X创建集成预测
集成Alpha、Beta、Delta和Gamma预测器。
Beta、Delta和Gamma分别预测每种疾病,而Alpha预测整体疾病。
我们将这些模型结合起来生成集成集成。
"""
alphaPreds = modelAlpha.predict_proba(train_X, False)
betaPreds = modelBeta.predict_proba(train_X, False)
deltaPreds = modelDelta.predict_proba(train_X, False)
gammaPreds = modelGamma.predict_proba(train_X, False)
ensemblePreds = []
for i, pred in enumerate(betaPreds):
truthy = mean([max([betaPreds[i][1], deltaPreds[i][1], gammaPreds[i][1]]), alphaPreds[i][1]])
falsey = mean([min([betaPreds[i][0], deltaPreds[i][0], gammaPreds[i][0]]), alphaPreds[i][0]])
ensemblePreds.append([falsey, truthy])
# 后处理预测
if postProcess:
ensemblePreds = modelAlpha.post_process_proba(ensemblePreds)
return np.array(ensemblePreds)
本次竞赛不需要确保预测概率总和为1。许多公开笔记本使用了一个简单反转阳性预测的函数版本。这对于我上述描述的预测策略显然是不正确的,因为我的总概率可能大于或小于1。在本笔记本中,我确保使用的平衡对数损失函数独立处理完整的预测概率,而不是确保概率总和为1。两者理论上应该是等价的。
一个表现更佳的笔记本(私有分数0.33)完全忽略了Class模型。
我根据集成模型在训练期间对每个模型的预测权重以及在生成预测时的总体权重来平衡预测概率。在有限测试中,我发现这比根据训练集中的比例提前为XGBoost提供类别权重效果更好。
对于每个集成模型(Class, B, G, D),我通过循环移除特征重要性得分≤0的特征来检查哪个特征集在训练中表现最佳。我保留了表现最佳的运行对应的特征集。
一个表现更佳的笔记本(私有分数0.33)使用了所有特征。
我对每个模型(Class, B, D, G)运行了200次Optuna试验来调优超参数。最终发现调优导致公开和私有分数都变差了(下降了0.01到0.05)。然而,由于时间限制,我没有针对每个模型的缩减特征集重新进行调优。这为重新运行缩减特征集的超参数调优留下了空间。我仍然怀疑此时我们已经在针对我的核心解决方案进行过拟合。
我尝试了简单的预测后处理策略,将高值推向1,低值推向0。我也尝试了基于比率的方法。这两种方法都显著提高了训练结果分数(从约0.5降至约0.04范围)- 但这导致公开和私有数据集的分数显著下降。我在得分最高的公开笔记本(https://www.kaggle.com/code/vadimkamaev/postprocessin-ensemble)中观察到了这种策略,它似乎是许多高分数的来源。我现在很好奇这是否也损害了那些笔记本的表现,就像损害了我的表现一样。
启发了我后处理的失败尝试,也是我将预测平衡改为在预测时进行而非通过xgboost类别权重参数的原因(tabPFN似乎没有这个参数,混合匹配的尝试 unnecessarily complex)。