Predict Podcast Listening Time
题意:
给你没个播客的信息,让你预测观众的聆听时间。
数据处理:
1.构造新特征收听效率进行分组
2.对数据异常处理
3.对时间情绪等进行数值编码
4.求某特征值求多项式特征
5.生成特征组合
6.交叉验证并encoder编码
建立模型:
1.创建xgb训练回调函数,动态调整学习率
2.DMatrix优化数据,训练模型
代码:
python
import numpy as np
import pandas as pd
import os
import os
import warnings
import numpy as np
import pandas as pd
import xgboost as xgb
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import KFold
from sklearn.preprocessing import LabelEncoder
from cuml.preprocessing import TargetEncoder
from itertools import combinations
from tqdm.auto import tqdm
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' #仅输出错误日志
warnings.simplefilter('ignore') #忽略警告日志
pd.options.mode.copy_on_write = True #数据仅存一份,其他是视图
# 探索性数据分析 (EDA)
def basic_eda(df, name="Dataset"):
print(f"\n----- {name} EDA -----")
print(df.shape)
print(df.info())
print(df.describe())
print(df.isnull().sum().sort_values(ascending=False).head(10)) # 缺失值降序查看10个
print(f"Duplicated rows: {df.duplicated().sum()}") # 查看重复行的个数
#查看缺失值的热力图
plt.figure(figsize=(10, 6)) #大小10×6
sns.heatmap(df.isnull(), cbar=False, cmap="viridis") #绘制热力图
plt.title(f'Missing Values Heatmap - {name}')
plt.show()
# df.isnull(): 输入布尔矩阵
# cbar = False: 关闭颜色条
# cmap = "viridis": 使用Viridis颜色映射(黄 - 绿 - 蓝渐变)
# plt.title(...): 设置标题
# plt.show(): 显示图像
# 黄色区域表示缺失值
# 深色区域表示无缺失值
# 横向条纹:某列存在大量缺失值
# 纵向条纹:某行存在多个缺失值
# 特征组合生成
def process_combinations_fast(df, columns_to_encode, pair_sizes, max_batch_size=2000):
# columns_to_encode: 需要生成组合特征的列名列表
# pair_sizes: 组合大小列表(如[2, 3]表示生成2列和3列的组合)
# max_batch_size: 每批处理的最大组合数(默认2000,避免内存溢出)
# 将指定列转换为字符串类型,为后续拼接操作做准备(字符串拼接更直观)。
str_df = df[columns_to_encode].astype(str)
# 创建LabelEncoder实例,用于将拼接后的字符串编码为数值。
le = LabelEncoder()
total_new_cols = 0
# 遍历每个组合大小 r(如先处理所有2列组合,再处理3列组合)。
for r in pair_sizes:
print(f"\nProcessing {r}-combinations...")
# 计算从 columns_to_encode 列中选取 r 列的组合总数。
n_combinations = np.math.comb(len(columns_to_encode), r)
print(f"Total {r}-combinations: {n_combinations}")
# 使用itertools.combinations生成所有可能的列组合迭代器。
combos_iter = combinations(columns_to_encode, r)
# 初始化两个列表:
# batch_cols: 存储当前批次的列组合(如[['A', 'B'], ['A', 'C']])
# batch_names: 存储对应的新列名(如['A+B', 'A+C'])
batch_cols, batch_names = [], []
#创建进度条,总长度为组合总数 n_combinations。
with tqdm(total=n_combinations) as pbar:
# 进入无限循环,直到处理完所有组合。每次循环开始时清空批次列表。
while True:
batch_cols.clear()
batch_names.clear()
# 从迭代器中获取最多max_batch_size个组合:
# next(combos_iter): 获取下一个组合(如('A', 'B'))
# 转换为列表并添加到batch_cols
# 生成新列名(如'A+B')并添加到batch_names
# 迭代器耗尽时触发StopIteration,退出循环
for _ in range(max_batch_size):
try:
cols = next(combos_iter)
batch_cols.append(list(cols))
batch_names.append('+'.join(cols))
except StopIteration:
break
# 如果当前批次为空,说明所有组合已处理完毕,退出循环。
if not batch_cols:
break
# 遍历当前批次的所有组合:
# 字符串拼接:将组合内的列值按行拼接(如'A_val' + 'B_val')
# 标签编码:将拼接后的字符串转换为数值,并加1(避免0值)
# 更新进度条:每处理一个组合,进度条前进1步
for cols, new_name in zip(batch_cols, batch_names):
result = str_df[cols[0]].copy()
for col in cols[1:]:
result += str_df[col]
df[new_name] = le.fit_transform(result) + 1
pbar.update(1)
# 累计当前批次生成的新列数。
total_new_cols += len(batch_cols)
#打印当前组合大小的处理结果及总列数。
print(f"Completed {r}-combinations. Total columns now: {len(df.columns)}")
return df
# 动态调整学习率,115轮次前0.05,之后0.01
def learning_rate_scheduler(epoch):
return 0.05 if epoch < 115 else 0.01
# 数据预处理
df_train = pd.read_csv("/kaggle/input/playground-series-s5e4/train.csv")
df_test = pd.read_csv('/kaggle/input/playground-series-s5e4/test.csv')
df = pd.concat([df_train, df_test], axis=0, ignore_index=True)
df.drop(columns=['id'], inplace=True)
df = df.drop_duplicates()
# 新特征:收听效率 = 收听时长 / 节目时长
df1 = df.copy()
df1["Listening_Eff"] = df1["Listening_Time_minutes"] / df1["Episode_Length_minutes"]
genre = df1.groupby("Genre")["Listening_Eff"].mean().sort_values(ascending=False)
# 功能:按 Genre(流派)分组,计算每组的 Listening_Eff 均值,并按降序排列。
# 操作分解:
# 分组:df1.groupby("Genre") 将数据按流派划分。
# 列选择:["Listening_Eff"] 指定计算目标列为收听效率。
# 聚合:.mean() 计算每组的平均效率。
# 排序:.sort_values(ascending=False) 按效率值从高到低排序。
# 输出示例:
# Genre
# Comedy 0.82
# Drama 0.78
# Education 0.75
# ...
# Name: Listening_Eff, dtype: float64
print(genre)
# 展示关系图
plt.figure(figsize=(10, 6))
sns.barplot(x=genre.values, y=genre.index, palette="viridis")
plt.title("Average Listening Efficiency by Genre")
plt.xlabel("Listening_Time Eff")
plt.ylabel("Genre")
plt.show()
#进行eda查看
basic_eda(df, "Combined Dataset")
# 异常值处理
df['Episode_Length_minutes'] = np.clip(df['Episode_Length_minutes'], 0, 120) #节目时常限制在0-120
df['Host_Popularity_percentage'] = np.clip(df['Host_Popularity_percentage'], 20, 100) #将主持人热度百分比限制在[20, 100]
df['Guest_Popularity_percentage'] = np.clip(df['Guest_Popularity_percentage'], 0, 100) #将嘉宾热度百分比限制在 [0, 100]
df.loc[df['Number_of_Ads'] > 3, 'Number_of_Ads'] = 0 #将广告数量超过3个的节目标记为0
# 特征编码
# 自定义分类变量映射,然后应用映射
day_mapping = {'Monday':1, 'Tuesday':2, 'Wednesday':3, 'Thursday':4, 'Friday':5, 'Saturday':6, 'Sunday':7} #一周换为有序值
time_mapping = {'Morning':1, 'Afternoon':2, 'Evening':3, 'Night':4} #一天的时间换为有序数值
sentiment_mapping = {'Negative':1, 'Neutral':2, 'Positive':3} #将情感极性转换为有序数值
# 应用映射
df['Publication_Day'] = df['Publication_Day'].map(day_mapping)
df['Publication_Time'] = df['Publication_Time'].map(time_mapping)
df['Episode_Sentiment'] = df['Episode_Sentiment'].map(sentiment_mapping)
# 修正Episode_Title(移除"Episode "前缀并转为整数)
# 目标:从剧集标题中提取编号并转为整数
# 操作分解:
# 字符串替换:删除标题中的"Episode "前缀
# 类型转换:将结果转为整型(如"123"→123)
df['Episode_Title'] = df['Episode_Title'].str.replace('Episode ', '', regex=True).astype(int)
# 对剩余分类列进行标签编码
# 功能:创建Scikit-learn的LabelEncoder实例。
# 核心作用:将分类标签转换为0-based的整数编码(如['A','B','A']→[0,1,0])。
le = LabelEncoder()
for col in df.select_dtypes('object').columns: # 自动选择数据框中所有object类型的列(通常是字符串或混合类型列)。
df[col] = le.fit_transform(df[col]) + 1
# 特征工程
# 多项式特征
for col in ['Episode_Length_minutes']:
df[f"{col}_sqrt"] = np.sqrt(df[col]) # 平方根
df[f"{col}_squared"] = df[col] ** 2 # 平方
# 分组均值编码(Target Encoding)
group_cols = ['Episode_Sentiment', 'Genre', 'Publication_Day', 'Podcast_Name', 'Episode_Title',
'Guest_Popularity_percentage', 'Host_Popularity_percentage', 'Number_of_Ads']
# 使用tqdm库为循环添加进度条,提升大数据处理时的用户体验
for col in tqdm(group_cols, desc="Creating group mean features"):
df[f"{col}_EP"] = df.groupby(col)['Episode_Length_minutes'].transform('mean')
# 分组:df.groupby(col) 按当前列分组(如按Genre分组)
# 聚合计算:['Episode_Length_minutes'].transform('mean') 计算每组的节目时长均值
# 特征映射:将均值结果广播回原始数据框的每一行
# 对齐机制:保证新列与原始数据框行索引完全一致
# 内存高效:相比apply方法,transform在大数据集上性能更优
# 生成组合特征
combo_columns = ['Episode_Length_minutes', 'Episode_Title', 'Publication_Time', 'Host_Popularity_percentage',
'Number_of_Ads', 'Episode_Sentiment', 'Publication_Day', 'Podcast_Name', 'Genre', 'Guest_Popularity_percentage']
df = process_combinations_fast(df, combo_columns, pair_sizes=[2, 3, 5, 7], max_batch_size=1000)
# 降低数据精度节省内存
df = df.astype('float32')
# 模型训练与预测
# 分割数据集
df_train = df.iloc[:-len(df_test)]
df_test = df.iloc[-len(df_test):].reset_index(drop=True)
df_train = df_train[df_train['Listening_Time_minutes'].notnull()]
target = df_train.pop('Listening_Time_minutes')
df_test = df_test.drop(columns=['Listening_Time_minutes'])
# 交叉验证设置
# n_splits=7:将数据划分为7个互斥的子集(folds)
# shuffle=True:划分前打乱数据顺序(防止数据固有顺序影响验证)
# random_state=seed:确保每次运行洗牌结果一致
seed = 42
cv = KFold(n_splits=7, random_state=seed, shuffle=True)
pred_test = np.zeros((250000,)) #创建形状为(250000,)的全零数组
params = {
'objective': 'reg:squarederror',
'eval_metric': 'rmse',
'seed': seed,
'max_depth': 19,
'learning_rate': 0.03,
'min_child_weight': 50,
'reg_alpha': 5,
'reg_lambda': 1,
'subsample': 0.85,
'colsample_bytree': 0.6,
'colsample_bynode': 0.5,
'device': "cuda"
}
# 功能:创建XGBoost训练回调函数,用于动态调整学习率
# LearningRateScheduler: XGBoost内置的回调类
# learning_rate_scheduler: 用户自定义的学习率计算函数
lr_callback = xgb.callback.LearningRateScheduler(learning_rate_scheduler)
# 交叉验证循环
for fold, (idx_train, idx_valid) in enumerate(cv.split(df_train)):
print(f"\n--- Fold {fold + 1} ---")
# 分割训练/验证集
X_train, y_train = df_train.iloc[idx_train], target.iloc[idx_train]
X_valid, y_valid = df_train.iloc[idx_valid], target.iloc[idx_valid]
X_test = df_test[X_train.columns].copy()
# 初始化编码器
features = df_train.columns
encoder = TargetEncoder(n_folds=5, seed=seed, stat="mean")
# Apply Target Encoding
for col in tqdm(features[:20], desc="Target Encoding first 20 features"): # 前20列单独处理
# 拟合编码器(自动处理交叉验证)
X_train[f"{col}_te1"] = encoder.fit_transform(X_train[[col]], y_train)
# 验证集和测试集使用相同编码器
X_valid[f"{col}_te1"] = encoder.transform(X_valid[[col]])
X_test[f"{col}_te1"] = encoder.transform(X_test[[col]])
for col in tqdm(features[20:], desc="Target Encoding remaining features"):
# 拟合编码器(自动处理交叉验证)
X_train[col] = encoder.fit_transform(X_train[[col]], y_train)
# 验证集和测试集使用相同编码器
X_valid[col] = encoder.transform(X_valid[[col]])
X_test[col] = encoder.transform(X_test[[col]])
# 创建DMatrix(XGBoost专用数据结构)
# DMatrix是XGBoost定制的高性能数据结构,专为梯度提升算法优化
dtrain = xgb.DMatrix(X_train, label=y_train)
dval = xgb.DMatrix(X_valid, label=y_valid)
dtest = xgb.DMatrix(X_test)
# 训练模型(带早停和自定义学习率调度)
model = xgb.train(
params=params,
dtrain=dtrain,
num_boost_round=1_000_000,
evals=[(dtrain, 'train'), (dval, 'validation')],
early_stopping_rounds=30,
verbose_eval=500,
callbacks=[lr_callback]
)
# 预测并累积结果
val_pred = model.predict(dval)
pred_test += np.clip(model.predict(dtest), 0, 120) # 限制预测值范围
print("-" * 70)
pred_test /= 7 # 平均7折结果
# 生成提交文件
df_sub = pd.read_csv("/kaggle/input/playground-series-s5e4/sample_submission.csv")
df_sub['Listening_Time_minutes'] = pred_test
df_sub.to_csv('submission.csv', index=False)