Predict Podcast Listening Time
題意:
給你沒個播客的信息,讓你預測觀眾的聆聽時間。
數據處理:
1.構造新特征收聽效率進行分組
2.對數據異常處理
3.對時間情緒等進行數值編碼
4.求某特征值求多項式特征
5.生成特征組合
6.交叉驗證并encoder編碼
建立模型:
1.創建xgb訓練回調函數,動態調整學習率
2.DMatrix優化數據,訓練模型
代碼:
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 tqdmfor 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×6sns.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) + 1pbar.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: float64print(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.columnsencoder = TargetEncoder(n_folds=5, seed=seed, stat="mean")# Apply Target Encodingfor 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)