649. Playground Series - Season 5, Episode 4 | playground-series-s5e4
仓库:https://github.com/masaishi/kaggle-s5e4-6th-solution
更新: 我发布了训练代码,使用 5 折交叉验证的单个 LightGBM 模型可以达到 11.63 的私有分数。
代码:Public 11.70 | Private 11.63 | Single LGB Fold 5
我验证了 AngelosMar 在 这个讨论帖 中最初分享的数据泄露。我自己的验证 EDA 可以在这里找到:Decimal Digits Analysis EDA
分析显示,Episode_Length_minutes 中超过 2 位小数 的数据点与实际的 Listening_Time_minutes strongly correlated (强相关)。在集成预测后,我使用公式 Episode_Length_minutes * 0.9554 覆盖了这些值。
df_train.filter(pl.col("Episode_Length_minutes_Decimal_Len") > 2)
https://www.kaggle.com/code/masaishi/decimal-digits-analysis-eda?scriptVersionId=236025089&cellId=7
通常,Number_of_Ads 的范围是 0-3。然而,我发现了 7 个实例,其值超过 3,这些异常值与 Listening_Time_minutes closely matched ( closely 匹配)。对于这些情况,我在集成预测后应用了 Number_of_Ads * 1.0588。
df_train.filter(pl.col("Number_of_Ads") > 3.0)
https://www.kaggle.com/code/masaishi/decimal-digits-analysis-eda?scriptVersionId=236025089&cellId=8
由于本次比赛使用的是合成数据(如描述中所述),我假设某些特征组合将具有一致的收听时间。我系统地评估了特征组合,以识别收听时间变异性低的组(使用组均值预测时 RMSE 低)。我稍后会写如何计算。
以下四个特征组合在用组均值覆盖预测时显示出显著的 LB 改进:
[
"Host_Popularity_percentage-Guest_Popularity_percentage-Episode_Num",
"Host_Popularity_percentage-Guest_Popularity_percentage-ELen_Int",
"Publication_Day-Guest_Popularity_percentage-ELen_Int-HPperc_Int",
"Guest_Popularity_percentage-Episode_Num-ELen_Int-HPperc_Int",
]
我实施了许多特征转换,包括:
df = df.with_columns(
# Day features
pl.col("Publication_Day").cast(pl_f_type).mul(2 * np.pi / 7).sin().alias("Day_sin"),
pl.col("Publication_Day").cast(pl_f_type).mul(2 * np.pi / 7).cos().alias("Day_cos"),
pl.col("Publication_Day").cast(pl_f_type).mul(4 * np.pi / 7).sin().alias("Day_sin2"),
pl.col("Publication_Day").cast(pl_f_type).mul(4 * np.pi / 7).cos().alias("Day_cos2"),
# Time features
pl.col("Publication_Time").cast(pl_f_type).mul(2 * np.pi / 4).sin().alias("Time_sin"),
pl.col("Publication_Time").cast(pl_f_type).mul(2 * np.pi / 4).cos().alias("Time_cos"),
pl.col("Publication_Time").cast(pl_f_type).mul(4 * np.pi / 24).sin().alias("Time_sin2"),
pl.col("Publication_Time").cast(pl_f_type).mul(4 * np.pi / 24).cos().alias("Time_cos2"),
# Ratio features
(pl.col("Episode_Length_minutes") / (pl.col("Number_of_Ads") + 1)).fill_null(0).alias("Length_per_Ads"),
(pl.col("Episode_Length_minutes") / (pl.col("Host_Popularity_percentage") + 1)).fill_null(0).alias("Length_per_Host"),
(pl.col("Episode_Length_minutes") / (pl.col("Guest_Popularity_percentage") + 1)).fill_null(0).alias("Length_per_Guest"),
# Episode length features
pl.col("Episode_Length_minutes").floor().alias("ELen_Int"),
(pl.col("Episode_Length_minutes") - pl.col("Episode_Length_minutes").floor()).alias("ELen_Dec"),
pl.col("Host_Popularity_percentage").floor().alias("HPperc_Int"),
(pl.col("Host_Popularity_percentage") - pl.col("Host_Popularity_percentage").floor()).alias("HPperc_Dec"),
# Sentiment features
(pl.col("Episode_Sentiment") == "2").cast(pl.Int8).alias("Is_Positive_Sentiment"),
pl.when(pl.col("Episode_Sentiment") == "2").then(0.75).otherwise(0.717).cast(pl_f_type).alias("Sentiment_Multiplier"),
# Squared features
(pl.col("Episode_Length_minutes") ** 2).alias("Episode_Length_squared"),
(pl.col("Episode_Length_minutes") ** 3).alias("Episode_Length_squared2"),
)
df = df.with_columns(
(np.sin(2 * np.pi * pl.col("Episode_Num") / 100)).alias("Long_Term_Cycle_Sin"),
(np.cos(2 * np.pi * pl.col("Episode_Num") / 100)).alias("Long_Term_Cycle_Cos"),
(pl.col("Episode_Length_minutes") * pl.col("Sentiment_Multiplier")).alias("Expected_Listening_Time_Sentiment"),
)
df = df.with_columns(
(
(pl.col("Episode_Length_minutes") - pl.col("Episode_Length_minutes").median()).pow(2)
+ (pl.col("Host_Popularity_percentage") - pl.col("Host_Popularity_percentage").median()).pow(2)
+ (pl.col("Guest_Popularity_percentage") - pl.col("Guest_Popularity_percentage").median()).pow(2)
+ (pl.col("Number_of_Ads") - pl.col("Number_of_Ads").median()).pow(2)
).alias("Diff_Squared")
)
最有影响力的特征仅仅是 pl.col("Episode_Length_minutes").floor().alias("ELen_Int") - 提取剧集长度的整数部分。
我通过两种方式 Incorporate 了这个原始数据集:
对于第二种方法,我的灵感来自 Chris Deotte 在 Binary Prediction with a Rainfall Dataset 比赛 (Playground Series - Season 5, Episode 3) 中的解决方案帖子,分享在 这里。
该技术涉及在原始数据集中查找与我 target encoding 过程中识别的特征组组合相匹配的行。当我找到匹配的特征组时,我将这些原始行及其实际的 Listening_Time_minutes 值作为新训练数据添加。这本质上允许我用来自原始来源的额外 ground truth 值来丰富我的数据集。
for cols in combinations_list:
n = f"pte_{'_'.join(cols)}"
means = df_pltpd.group_by(cols).agg(pl.col("Listening_Time_minutes").mean().alias("mean_listening_time"))
df = df.join(means, on=cols, how="left").with_columns(pl.col("mean_listening_time").fill_null(m).alias(n)).drop("mean_listening_time")
代码:https://www.kaggle.com/code/masaishi/select-feature-combinations-by-rmse
我想分享我通过目标编码寻找最佳特征组合的方法。该方法系统地评估不同的特征分组,以识别哪些组合最能预测播客收听时间。
这是我实施的逐步过程:
# For first feature in combination
if comb[0] in df.select(cs.numeric()).columns:
concat_expr = pl.col(comb[0]).round(round_num).cast(pl.Utf8)
else:
concat_expr = pl.col(comb[0]).cast(pl.Utf8)
# Add remaining features to create group identifier
for col_name in comb[1:]:
if col_name in df.select(cs.numeric()).columns:
concat_expr = concat_expr + "_" + pl.col(col_name).round(round_num).cast(pl.Utf8)
else:
concat_expr = concat_expr + "_" + pl.col(col_name).cast(pl.Utf8)
df_train = df_train.with_columns(
concat_expr.alias("group").cast(pl.Categorical)
)
df_group = df_train.group_by("group").agg(
pl.col("Listening_Time_minutes").count().alias("count"),
pl.col("Listening_Time_minutes").std().alias("std_listening_time"),
pl.col("Listening_Time_minutes").mean().alias("mean_listening_time"),
pl.col("Episode_Length_minutes").mean().alias("mean_episode_length"),
)
df_valid = df_valid.with_columns(
concat_expr.alias("group").cast(pl.Categorical)
)
df_valid = df_valid.join(
df_group[["group", "mean_listening_time"]],
on="group",
how="left",
)
std_distributes.append({
"group": "-".join(comb),
"cover_rate": df_group["count"].sum() / df_train["Listening_Time_minutes"].count(),
"len": len(df_group),
"mean_count": df_group["count"].mean(),
"median_count": df_group["count"].median(),
"std_count": df_group["count"].std(),
"mean_std_listening_time": df_group["std_listening_time"].mean(),
"rmse": calculate_rmse(df_valid["Listening_Time_minutes"], df_valid["mean_listening_time"]),
})
std_distributes = std_distributes.sort("rmse")
这种方法本质上是一种跨不同特征组合执行目标编码的系统方式。最佳组合提供了关于哪些播客属性最强烈影响收听时间的宝贵见解。
对于我的最终提交,我创建了一个集成,结合了几种不同的回归模型:
有效的实验管理对我在这次比赛中的成功至关重要。我使用 Wandb 跟踪实验参数,同时实施了一个自动提交系统,其中包括验证分数和 Wandb 实验名称在提交消息中。这使我很容易识别哪些代码变更产生了最佳结果。
在整个比赛过程中进行了超过 1,000 次实验,从一开始就建立这种结构化环境被证明是无价的。截图示例显示了我的跟踪系统如何在视觉上组织结果。
我还仔细构建了代码架构以最大化灵活性:
src/
├── config.py
├── data
│ ├── data_class.py
│ ├── data_process.py
│ ├── default_selecteds.py
│ ├── encoders.py
│ ├── feature_eng.py
│ ├── simple_data_process.py
│ ├── simple_feature_eng.py
│ └── tabnet_feature_eng.py
├── main.py
├── models
│ ├── hgbr.py
│ ├── lgb.py
│ ├── svr.py
│ ├── tabnet.py
│ ├── test.py
│ └── xgb.py
└── utils.py
每个模型都遵循一致的接口,具有标准化的输入/输出格式:
def train_model(fold: int, datasetXy: DatasetXy) -> tuple[float, list | None]:
# Model implementation
@dataclass
class DatasetXy:
X_train: pl.DataFrame
y_train: pl.Series
X_valid: pl.DataFrame
y_valid: pl.Series
X_test: Optional[pl.DataFrame] = None
y_test: Optional[pl.Series] = None
这种设计使我很容易测试新模型或将成功的特征工程技术应用于不同的算法。我的配置系统还允许在预测和评估模式之间快速切换。
虽然这些细节可能看起来微不足道,但它们显著增强了我在整个比赛过程中高效迭代的能力。