機器學習特征工程的最佳實踐
特征工程是模型訓練之前運行的關鍵過程,因為輸入數據的質量直接決定了模型輸出的質量。
雖然深度學習模型擅長從圖像或文本等非結構化數據中自動學習特征,但明確的特征工程對于表格數據集仍然至關重要。
在本文中,云朵君將展示特征工程對回歸任務的影響,特別關注具有混合數字、分類和基于時間的特征的大型表格數據集。
什么是特征工程
特征工程是從原始數據中選擇、轉換和創建新特征以提高機器學習模型性能的過程。
它涉及使用領域知識從數據中提取最相關的信息,并以適合所選機器學習算法的方式表示它。
特征工程的好處
精心設計的特征可以顯著提高模型的預測能力,即使是簡單的模型也可以掌握復雜的關系,因為它可以:
- 降低數據稀疏性:大多數現實世界的數據集都是稀疏的,包含許多零值和缺失值。特征工程可以整合信息并創建更密集的表示,使模型更容易學習。
- 處理多種數據類型: 原始數據有多種格式,例如數值、分類、文本和時間。特征工程將這些類型轉換為模型可以處理的數字格式。
- 解決數據噪聲和異常值:特征工程減輕了噪聲數據和異常值的影響,從而產生了更穩健的模型。
當我們可以將特征格式化為直接對應于問題域中的有意義的概念時,它可以使模型的決策更具解釋性、準確性和穩健性。
特征工程中的常用技術
一些常見的特征工程技術包括:
對數轉換
- 將對數應用于數值特征。
- 可以使分布更加對稱,使模型呈現正態分布。
- 最佳情況:處理傾斜的數值特征。
多項式特征創建
- 通過將現有特征提升到冪(例如 x2、x3)來生成新特征。
- 創建交互項(例如,x?y)。
- 最佳情況:捕捉非線性關系。
分箱(離散化)
- 將連續的數值分組到箱中。
- 可以減少微小波動的影響并使關系更加線性。
- 最佳情況:用線性模型處理非線性關系,數據包含顯著的異常值或偏差。
基于時間的特征
- 提取星期幾、月份、年份、小時、季度,甚至更復雜的特征,如“是周末”、“是假期”。
- 計算事件之間的時間差。
- 最佳情況:季節性影響預測。
工作流程
雖然沒有一刀切的方法,但特征工程的一般工作流程通常涉及從定義問題和成功指標開始的整個項目生命周期。
在本文中,我將演示第 1 階段和第 2 階段(圖中的藍色框),特別關注特征工程。
圖:具有特征工程的機器學習項目工作流程
對于第 3 階段,“通過機器學習實現準確性”涵蓋了泛化的核心概念,假設了損失的根本原因。!
第一階段:基礎
1. 問題
對于在線零售業務,了解未來客戶支出對于營銷、庫存管理和戰略規劃至關重要。
讓我們想象這樣一個場景:企業在銷售增長方面苦苦掙扎,尋找可行的見解。
2. 成功指標
我選擇平均絕對誤差 (MAE)作為主要指標,因為它對偏斜數據具有穩健性(稍后,在 EDA 期間,我將討論目標變量:銷售額的數據分布)并選擇MSE作為支持指標。
3. EDA 和特征工程
這是初始階段數據準備的基礎部分。
為了演示,我將使用來自加州大學歐文分校機器學習庫的在線零售數據:直接在@公眾號:數據STUDIO 原文文末留言需要即可,云朵君將回復給大家:
圖:變量表
import os
import pandas as pd
df = pd.read_csv(csv_file_path)
df.info()
圖片
該數據集有541,909 個數據點,具有八個特征:
已加載數據集
實際上,這些數據可以是簡單的 Excel 表,也可以是存儲在云服務器上的信息,或者我們可以組合多個數據源。
數據清理
在進入 EDA 之前,我將按照一般原則清理數據:
- 保留數字類型: 如果某列應為數字(例如quantity、unitprice、customerid),請保留該列。使用數字類型進行數字運算會更加高效且更有意義。
- 混合/字符串數據的對象類型: Pandas 中的dtypeobject指的是包含混合類型或主要為字符串的列。如果某列確實包含數字和字符串的混合,object則 dtype 為默認值。
- 各種缺失數據: Pandas 會處理NaN缺失的數字數據和None缺失的對象數據。然而,當我們讀取數據時,缺失值可能表示為空字符串、特定文本(例如“N/A”),甚至只是空格,而 Pandas 可能無法自動將其解釋為NaN。我們需要識別這些值。
識別 NaN
對于具有混合數據類型的列,我將潛在的缺失值(如空格或“nan”字符串)轉換為NumPy’s NaN。
這對于后期的準確歸集是一個至關重要的準備。
import numpy as np
obj_cols = [ 'invoiceno' , 'stockcode' , 'country' , 'invoicedate' ]
# 列出潛在的 NaN 值
null_vals = [ '' , 'nan' , 'N/A' , None , 'na' , 'None' , 'none' ]
removed_null = { item: np.nan for item in null_vals }
for col in obj_cols:
df[col].replace(replaced_null, inplace= True )
df.info()
圖片
現在,識別每列中缺失的數據(NumPy 的 NaN):
對于i, col in enumerate (df.columns):
unique_num = df[col].nunique()
nan_num = df[col].isna(). sum ()
print(f'{i}. {col} - {unique_num:,} data points (missing data: {nan_num:,})')
圖:每列中發現的缺失值
處理description和customerid列中缺失的數據:
- 我決定刪除description列,因為假設它對未來預測的影響有限。 注意:文本數據對于產品因式分解可能很有價值,尤其是在缺失部分比較有限的情況下。
- 我保留了原有的customerid列來評估唯一用戶的影響,同時引入了一個新的is_registered二分類列(1:已注冊,0:未注冊),該列對應于每個客戶 ID,假設沒有 ID 的客戶未注冊。 注意:customerid這會導致基數過高。稍后我將使用二分類編碼來解決這個問題。
# 工程化之前復制基礎數據集
df_rev = df.copy()
df_rev = df_rev.drop(columns='description')
df_rev['is_registered'] = np.where(df_rev['customerid'].isna(), 0, 1)轉換數據類型
最后,考慮到潛在的特征工程,我轉換了invoicedate和 customerid的數據類型:
import pandas as pd
df_rev['invoicedate'] = pd.to_datetime(df_rev['invoicedate'])
df_rev['customerid'] = df_rev['customerid'].astype('Int64')
df_rev.info()
添加了第 7 列。第 5 列的數據類型已更新。
探索性數據分析(EDA)和特征工程
清理完數據集后,我們現在可以轉到 EDA。
探索性數據分析 (EDA) 是一種專注于總結和可視化數據以了解其主要特征的數據分析技術。
從技術角度來說,我們可以執行無數次 EDA,尤其是在復雜數據集上。但我們的主要重點是揭示需要設計哪些特征,并深入了解數據預處理,從而提升模型性能。
任何其他分析都應該由模型本身處理,因為真正的底層模式要么太微妙,要么太復雜,我們無法手動發現。
因此,EDA 成為模型確定哪些分析(例如隱藏趨勢、群體差異)值得轉化為預測的第一步。
EDA 必須包含:
- 用于理解數據的基本分析(以單變量為重點) ;
- 基于與項目目標直接相關的假設進行的項目特定分析(以雙變量為重點)
1)基本分析(單變量重點)
這個初始階段是通過單獨分析每個變量來從根本上了解數據集。
首先,為了準備 EDA,我從列中提取了year、month,并按以下順序對數據進行排序:day_of_week,invoicedate,invoicedate
df_rev['invoicedate'] = pd.to_datetime(df_rev['invoicedate'])
df_rev['year'] = df_rev['invoicedate'].dt.year
df_rev['year_month'] = df_rev['invoicedate'].dt.to_period('M')
df_rev['month_name'] = df_rev['invoicedate'].dt.strftime('%b')
df_rev['day_of_week'] = df_rev['invoicedate'].dt.strftime('%a')
df_rev = df_rev.sort_values('invoicedate')還推出了sales銷售分析專欄:
df_rev['sales'] = df_rev['quantity'] * df_rev['unitprice']數據集如下:
圖片
添加了第 8 列至第 12 列
了解數據分布
我繪制了數值特征的 PDF 和分類特征的直方圖,以識別異常值、傾斜和重尾等特征。
盡管真實的數據分布過于復雜而難以完全掌握,但分析對于有效的預處理和模型選擇至關重要。
數值列的 PDF
unitprice 和sales都是稀疏的,并且尾部嚴重,存在顯著的異常值。我將使用MAE作為評估指標,因為它對傾斜數據具有較好的魯棒性。
圖:unitprice和sales的PDF
圖:unitprice和sales的PDF
- unitprice:最大值:38,970.0,最小值:-11,062.1,平均值:4.6,標準差:96.8
- sales:最大值:168,469.6,最小值:-168,469.6,平均值:18.0,標準差:378.8
分類特征直方圖
invoiceno、year_month和day_of_week均勻分布:
圖片
圖片
圖片
stockcode左側有一個峰值,右側有一個長尾,而quantity和country則顯示出退化分布,其中數據集中在幾個類別中:
圖片
圖片
圖片
is_registered和year結果也是二分類的:
圖片
圖片
在此基礎上,我將進行針對特定項目的 EDA,以找出額外的特征工程機會。
2)項目特定的EDA
此階段深入研究,特別是尋找變量之間的關系,特別是潛在特征和目標變量之間的關系:sales。
首先,我將根據“銷售增長”這一挑戰的潛在解決方案提出三個假設。實際上,可以利用商業和專家的見解來完善這些假設。
假設1
“銷售趨勢是由一周中的某天或一個月中的某天決定的。”
鑒于數據集有限的 13 個月的銷售數據(二進制year),我關注較短的趨勢周期。
- 需要設計的潛在特性is_weekend:day_of_month
- 潛在商業解決方案:順應趨勢的大量促銷。
假設2
“產品銷售受時間和價格點驅動。”
需要設計的潛在特性:
- unit_price_bin:unitprice離散化為“低”、“中”、“高”類別,直接解決價格影響和非線性。
- product_avg_quantity_last_month:計算每個產品quantity在上一日歷月的平均銷量stockcode并獲取近期產品的受歡迎程度。
- product_sales_growth_last_month:從 2 個月前到上個月stockcode的銷售額百分比變化,以確定流行產品。
潛在的商業解決方案:
- 動態定價(通過促銷時機預測最佳價格點的模型)。
- 定制產品推薦(預測產品因素相似性的模型)。
假設3
“活躍的顧客往往會購買更多商品,從而促進銷售。”
需要設計的潛在特性:
- customer_recency_days:預測日期(上個月底)與客戶上次購買日期之間的天數,以評估近期購買的可能性。
- customer_total_spend_ltm:客戶過去三個月產生的總銷售收入。這是對客戶近期貨幣價值的直接衡量。
- customer_freq_ltm:過去三個月內客戶開具的唯一發票總數。這是直接影響銷售額的參與度指標之一。
潛在的商業解決方案:
- 分層客戶忠誠度計劃(預測唯一用戶保留時間的模型)
- 營銷媒體組合優化(預測新客戶價值的模型)
現在,執行 EDA 并決定要設計哪些特性。
假設 1
“銷售趨勢受一周中的某天或一個月中的某天的影響。”
除了11月的峰值之外,按月和按周劃分的銷售趨勢沒有出現明顯的模式。因此,我選擇不添加基于此假設的其他特征。
圖:按月份和星期幾劃分的銷售趨勢
假設2
“產品銷售受時間和價格點驅動。”
對于unit_price_bin,幾乎所有月份的三個價格區間的中線都接近于零。所有區間的四分位距 (IQR) 也很短,這表明 25-75 百分位數數據落在一個非常小的低量范圍內。
然而,我們可以看到異常值占據了主導地位,形成了明顯的分層。
因此,我決定添加特征unit_price,同時保留原有的粒度,使用箱內的精確值來預測數量。
圖:按價格范圍(低、中、高)劃分的月銷售總量及中位數和四分位距
添加unit_price_bin到最終數據集:
import pandas as pd
# df_fin 將成為我們模型訓練的主要數據
df_fin = df_rev.copy()
# 創建臨時數據
_df_prod_month_agg = df_fin.copy().groupby(['stockcode', 'year_month']).agg(
prod_total_monthly_quantity=('quantity', 'sum'),
prod_ave_monthly_price=('unitprice', 'mean')
).reset_index().sort_values(by=['stockcode', 'year_month'])
_df_prod_month_agg['unit_price_bin'] = pd.qcut(
_df_prod_month_agg['prod_ave_monthly_price'],
q=3,
labels=['low', 'mid', 'high'],
duplicates='drop'
)
_df_prod_bin_per_stockcode = _df_prod_month_agg.groupby('stockcode')['unit_price_bin'].agg(
lambda x: x.mode()[0] ifnot x.mode().empty elseNone
).reset_index()
# 合并到主數據集 (df_fin)
df_fin = pd.merge(
df_fin,
_df_prod_bin_per_stockcode[['stockcode', 'unit_price_bin']],
notallow='stockcode',
how='left'
)
df_fin.info()
添加了第 13 列
product_avg_quantity_last_month也顯示出非常強的正相關性,這可以作為一個動量特征,表明上個月銷量好的產品本月也容易銷量好。我會添加這個特征。
圖:本月和上個月銷售的平均產品數量
添加product_avg_quantity_last_month到最終數據集(也處理插補):
import pandas as pd
_df_prod_month_agg['product_avg_quantity_last_month'] = _df_prod_month_agg.groupby('stockcode')['prod_total_monthly_quantity'].shift(1)
_df_prod_last_month_agg = _df_prod_month_agg.groupby('stockcode')['product_avg_quantity_last_month'].mean().reset_index()
df_fin = pd.merge(
df_fin,
_df_prod_last_month_agg [['stockcode', 'product_avg_quantity_last_month']],
notallow='stockcode',
how='left'
)
# 缺失數據意味著在該期限內沒有售出任何產品。用零進行插補。
df_fin['product_avg_quantity_last_month'] = df_fin['product_avg_quantity_last_month'].fillna(value=0)
df_fin.info()
添加了第 14 列
另一方面,product_sales_growth_last_month它沒有表現出很強的線性/單調關系。考慮到這個特征的預測能力有限,我選擇不添加它。
圖:月度產品數量與上月銷售額增長率
假設3
“活躍顧客傾向于購買更多產品并對銷售做出貢獻。”
customer_recency_days表明新近度較低的客戶(最近的購買,例如 x < 60 天)往往表現出更高的月銷售收入,表明呈反比關系(圖中紅色虛線)。
我將添加此特征來預測每月的銷售收入。
圖:月銷售額與客戶最近消費天數
添加customer_recency_days到數據集:
import pandas as pd
# 創建臨時數據集
_df_all_customers_year_month = pd.MultiIndex.from_product(
[df_fin['customerid'].unique(), df_fin['year_month'].unique()], # type: ignore
names=['customerid', 'year_month']
).to_frame(index=False).sort_values(by=['customerid', 'year_month']).reset_index(drop=True)
_df_customer_monthly_agg = df_fin.copy().groupby(['customerid', 'year_month']).agg(
monthly_sales=('sales', 'sum'),
monthly_unique_invoices=('invoiceno', 'nunique'),
monthly_last_purchase_date=('invoicedate', 'max')
).reset_index()
_df_cus = _df_all_customers_year_month.merge(_df_customer_monthly_agg, notallow=['customerid', 'year_month'], how='left').sort_values(by=['customerid', 'year_month'])
# 添加時間戳
_df_cus['pfin_last_purchase_date'] = _df_cus.groupby('customerid')['monthly_last_purchase_date'].shift(1)
_df_cus['invoice_timestamp_end'] = _df_cus['year_month'].dt.end_time
# 計算新近天數
_df_cus['customer_recency_days'] = (_df_cus['invoice_timestamp_end'] - _df_cus['pfin_last_purchase_date']).dt.days
# 合并和估算
df_fin['customer_recency_days'] = _df_cus['customer_recency_days']
max_recency = _df_cus['customer_recency_days'].max()
df_fin['customer_recency_days'] = df_fin['customer_recency_days'].fillna(value=max_recency + 30)
df_fin.info()
添加了第 15 列
customer_total_spend_ltm顯示出客戶過去三個月的總支出與其當前月銷售收入之間存在明顯的正相關關系。這表明,過去支出越高,當前收入就越高,這是一個非常有效的預測特征。我會添加這個特征。
圖:過去三個月的月銷售額與客戶總支出
添加customer_total_spend_ltm:
_df_cus['customer_total_spend_ltm'] = _df_cus.groupby('customerid')['monthly_sales'].rolling(window=3, closed='left').sum().reset_index(level=0, drop=True)
df_fin['customer_total_spend_ltm'] = _df_cus['customer_total_spend_ltm']
df_fin['customer_total_spend_ltm'] = df_fin['customer_total_spend_ltm'].fillna(value=0)
df_fin.info()
添加了第 16 列
customer_freq_ltm還展示了客戶過去三個月的購買頻率與其當前月銷售收入之間的正相關關系。過去三個月擁有更多獨立發票的客戶往往能帶來更高的月收入。我也會添加此特征。
圖:過去三個月的月銷售額與客戶頻率
添加customer_freq_ltm:
_df_cus['customer_freq_ltm'] = _df_cus.groupby('customerid')['monthly_unique_invoices'].rolling(window=3, closed='left').sum().reset_index(level=0, drop=True)
df_fin['customer_freq_ltm'] = _df_cus['customer_freq_ltm']
df_fin['customer_freq_ltm'] = df_fin['customer_freq_ltm'].fillna(value=0)
df_fin.info()
添加了第 17 列
對缺失值的最終檢查
特征工程完成后,我檢查了剩余的缺失值,在更新后的數據集中的stockcode、quantity、unit_price_bin和country列中發現了五個缺失項:
df_fin.isna().sum ()
我會在編碼過程中處理丟失的客戶 ID)
我會在編碼過程中處理丟失的客戶 ID)
逐一檢查這些缺失的項目并進行估算。
注意:鑒于 540k+ 個樣本中最多只有 20 個有缺失值,因此可以選擇按行刪除(即從數據集中刪除這些樣本)。
對于stockcode和unit_price_bin,樣本中缺失stockcode或unit_price_bin的其他值看起來是合法的。
我用“unknown”(字符串)和“low”換替換了stockcode和unit_price_bin中的缺失的值:
df_null = df_fin[df_fin['stockcode'].isnull()]
df_null.head().transpose()
圖片
df_fin['stockcode'] = df_fin['stockcode'].fillna(value='unknown')
df_fin['unit_price_bin'] = df_fin['unit_price_bin'].fillna(value='low')country采取同樣的過程,和列中的缺失值quantity分別用其眾數值和銷售額/單價值填充:
import numpy as np
df_fin['country'] = df_fin['country'].fillna(value=df_fin['country'].mode().iloc[0])
df_fin['quantity'] = df_fin['quantity'].fillna(value=np.floor(df_fin['sales'] / df_fin['unitprice']))最后,轉換數據類型以最終確定數據集:
df_fin['year_month'] = df_fin['year_month'].dt.month
df_fin['invoicedate'] = df_fin['invoicedate'].astype(int) / 10 ** 9
df_fin = df_fin.drop(columns=['month_name'], axis='columns')
df_fin.info()
最終數據集
該數據集的最終版本有541,909 個樣本,包含17 個特征:
cat_cols = [
'invoiceno' ,
'stockcode' ,
'quantity' ,
'customerid' ,
'country' ,
'year' ,
'year_month' ,
'day_of_week' ,
'is_registered' ,
'unit_price_bin' ,
'customer_recency_days' ,
]
num_cols = [
'unitprice' ,
'product_avg_quantity_last_month' ,
'customer_total_spend_ltm' ,
'customer_freq_ltm' ,
'invoicedate'
]
target_col = 'sales'回顧——第一階段的特征工程
根據 EDA 結果,我添加了 11 個特征:
- 來自單變量EDA :is_registered,year,year_month,month_name,day_of_week,sales
- 來自雙變量EDA:unit_price_bin,product_avg_quantity_last_month,customer_recency_days,customer_total_spend_ltm,customer_freq_ltm
并刪除了一個特征:description由于其缺失值量較大且對預測的影響有限。
一文帶你用sklearn做特征工程
一文詳盡特征工程與數據預處理
4. 模型選擇
鑒于數據集復雜且龐大,我選擇了以下三種模型:
- 彈性網絡:正則化線性回歸模型,適合作為線性可分數據的基線。
- 隨機森林:一種能夠捕捉復雜、非線性關系的強大機器學習模型。
- 深度前饋網絡:一種深度學習模型,可作為非線性可分離數據的強大基礎。為了有效地管理大型數據集,我使用了PyTorch庫。
原理+代碼,總結了 11 種回歸模型
萬字長文,演繹八種線性回歸算法最強總結!
總結了九種機器學習集成分類算法(原理+代碼)
理論+股市數據實戰,總結了五種常用聚類分析算法
總結了17個機器學習的常用算法!
5. 在預處理數據上訓練模型
首先,我將數據集分成所有模型的訓練集、驗證集和測試集。
我故意沒有對數據集進行打亂,以保留其時間順序。
from sklearn.model_selection import train_test_split
target_col = 'sales'
X = df_fin.copy().drop(columns=target_col)
y = df_fin.copy()[target_col]
test_size = 50000
X_tv, X_test, y_tv, y_test = train_test_split(X, y, test_size=test_size, random_state= 42 )
X_train, X_val, y_train, y_val = train_test_split(X_tv, y_tv, test_size=test_size, random_state= 42 )每個模型對預處理的需求不同:
圖:按模型劃分的數據預處理要求
因此,我將準備用于分別訓練每個模型的數據集。
彈性網絡
彈性網絡需要在縮放和編碼的數據集上進行訓練。
對于數值特征,我應用RobustScaler來處理我們在 EDA 期間發現的顯著異常值。
對于分類特征,我應用了BinaryEncoder來限制維度的增加,同時用零替換
customerid列中的缺失值:
from sklearn.preprocessing import RobustScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from category_encoders import BinaryEncoder
# num
num_transformer = Pipeline(steps=[
( 'scaler' , RobustScaler(with_centering= True , with_scaling= True ))
])
# cat
cat_transformer = Pipeline(steps=[
( 'encoder' , BinaryEncoder(cols=cat_cols, handle_missing= '0' ))
])
# 定義一個預處理器
preprocessor_en = ColumnTransformer(
transforms=[
( 'num' , num_transformer, num_cols),
( 'cat' , cat_transformer, cat_cols)
],
remainder= 'passthrough' ,
)
# 變換
X_train_processed = preprocessor_en.fit_transform(X_train)
X_val_processed = preprocessor_en.transform(X_val)
X_test_processed = preprocessor_en.transform(X_test)
# 啟動并訓練模型
from sklearn.linear_model import ElasticNet
elastic_net = ElasticNet(
alpha= 1 , # 正則化的總強度
l1_ratio= 0.5 , # l1 到 l2 的比例 = 1:1
fit_intercept= True , # 通過計算 y 截距進行擬合
precompute= False , # 不使用預先計算的 Gram 矩陣
max_iter= 5000 , # 1,000 個 epochs
copy_X= True , # 擬合前復制 X
tol= 1e-5 , # tol 停止迭代
random_state= 42 , # 隨機數生成器的種子
warm_start= False , # 忽略前一次擬合調用中的解
positive= False , # 可以同時 選擇負系數和正系數"cyclic" # 循環地逐個更新系數(與隨機相比) ).fit(X_train_processed, y_train)隨機森林
對于隨機森林,我們可以跳過縮放部分:
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestRegressor
from category_encoders import BinaryEncoder
# 定義預處理器
cat_transformer = Pipeline(steps=[
( 'encoder' , BinaryEncoder(cols=cat_cols, handle_missing= '0' ))
])
preprocessor_rf = ColumnTransformer(
transforms=[
( 'cat' , cat_transformer, cat_cols)
],
remainder= 'passthrough' ,
)
# 變換
X_train_processed = preprocessor_rf.fit_transform(X_train)
X_val_processed = preprocessor_rf.transform(X_val)
X_test_processed = preprocessor_rf.transform(X_test)
# 啟動并訓練模型
random_forest = RandomForestRegressor(
n_estimators=1000,
criterinotallow="squared_error",
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0,
max_features='sqrt',
max_leaf_nodes=None,
min_impurity_decrease=1e-10,
bootstrap=True,
oob_score=True,
n_jobs=-1,
random_state=42,
verbose=0,
warm_start=False,
ccp_alpha=0,
max_samples=None,
).fit(X_train_processed, y_train)DFN
DFN 需要縮放和編碼。對于數值特征,我使用了StandardScaler,因為它在處理復雜數據方面具有良好的魯棒性。之后,數據集被轉換為TensorDataset:
import torch
from torch.utils.data import DataLoader, TensorDataset
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from category_encoders import BinaryEncoder
num_transformer = Pipeline(steps=[('scaler', StandardScaler())])
cat_transformer = Pipeline(steps=[('encoder', BinaryEncoder(cols=cat_cols, handle_missing='0'))])
# 定義一個預處理器
preprocessor_dfn = ColumnTransformer(
transforms=[
( 'num' , num_transformer, num_cols),
( 'cat' , cat_transformer, cat_cols)
],
remainder= 'passthrough'
)
# 轉換
X_train_processed_dfn = preprocessor_dfn.fit_transform(X_train)
X_val_processed_dfn = preprocessor_dfn.transform(X_val)
X_test_processed_dfn = preprocessor_dfn.transform(X_test)
# 將 NumPy 數組轉換為 PyTorch 張量
X_train_tensor = torch.tensor(X_train_processed_dfn, dtype=torch.float32)
X_val_tensor = torch.tensor(X_val_processed_dfn, dtype=torch.float32)
X_test_tensor = torch.tensor(X_test_processed_dfn, dtype=torch.float32)
# 轉換為 1D 張量
y_train_tensor = torch.tensor(y_train.values, dtype=torch.float32).view(- 1 , 1 )
y_val_tensor = torch.tensor(y_val.values, dtype=torch.float32).view(- 1 , 1 )
y_test_tensor = torch.tensor(y_test.values, dtype=torch.float32).view(- 1 , 1 )
# 轉換為 TensorDataset
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val_dataset = TensorDataset(X_val_tensor, y_val_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
# 批處理
batch_size = 32
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=False)
val_loader = DataLoader(dataset=val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)然后,啟動模型:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
classDFN(nn.Module):
def__init__(self, input_dim):
super(DFN, self).__init__()
self.fc1 = nn.Linear(input_dim, 32)
self.relu1 = nn.ReLU()
self.dropout1 = nn.Dropout(0.1)
self.fc2 = nn.Linear(32, 16)
self.relu2 = nn.ReLU()
self.dropout2 = nn.Dropout(0.1)
self.fc3 = nn.Linear(16, 1)
defforward(self, x):
x = self.fc1(x)
x = self.relu1(x)
x = self.dropout1(x)
x = self.fc2(x)
x = self.relu2(x)
x = self.dropout2(x)
x = self.fc3(x)
return x
input_dim = X_train_processed_dfn.shape[1]
device = torch.device('cuda'if torch.cuda.is_available() else'cpu')
model = DFN(input_dim).to(device)
criterion = nn.L1Loss()
optimizer = optim.Adam(model.parameters(), lr=0.001)訓練模型:
from sklearn.metrics import mean_squared_error, mean_absolute_error
num_epochs = 100
best_val_loss = float('inf')
patience = 10
patience_counter = 0
min_delta = 1e-4
history = {
'train_loss': [],
'val_loss': [],
'train_mse': [],
'val_mse': [],
'train_mae': [],
'val_mae': []
}
for epoch in range(num_epochs):
model.train()
running_train_loss = 0.0
all_train_preds = []
all_train_targets = []
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
outputs = model(data)
loss = criterion(outputs, target)
loss.backward()
optimizer.step()
running_train_loss += loss.item() * data.size(0)
all_train_preds.extend(outputs.detach().cpu().numpy())
all_train_targets.extend(target.detach().cpu().numpy())
epoch_train_loss = running_train_loss / len(train_dataset)
train_mse = mean_squared_error(np.array(all_train_targets), np.array(all_train_preds))
train_mae = mean_absolute_error(np.array(all_train_targets), np.array(all_train_preds))
model.eval()
running_val_loss = 0.0
all_val_preds = []
all_val_targets = []
with torch.no_grad():
for data, target in val_loader:
data, target = data.to(device), target.to(device)
outputs = model(data)
loss = criterion(outputs, target)
running_val_loss += loss.item() * data.size(0)
all_val_preds.extend(outputs.cpu().numpy())
all_val_targets.extend(target.cpu().numpy())
epoch_val_loss = running_val_loss / len(val_dataset)
val_mse = mean_squared_error(np.array(all_val_targets), np.array(all_val_preds))
val_mae = mean_absolute_error(np.array(all_val_targets), np.array(all_val_preds))
history['train_loss'].append(epoch_train_loss)
history['val_loss'].append(epoch_val_loss)
history['train_mse'].append(train_mse)
history['val_mse'].append(val_mse)
history['train_mae'].append(train_mae)
history['val_mae'].append(val_mae)結果
平均輔助能量吸收
- 彈性網絡:訓練:19.773 → 驗證:18.508
- 隨機森林:訓練:4.147 → 驗證:10.551
- 深度前饋網絡:訓練:10.570 → 驗證:10.987
Elastic Net 的泛化能力良好(訓練集 19.77,驗證集 18.51),但平均誤差最高。其預測與實際銷售額的偏差約為18.50 美元至 19.77 美元。
隨機森林過擬合嚴重(訓練集 4.15,驗證集 10.55)。平均而言,其對新數據的預測偏差約為10.55 美元。
深度前饋網絡 (DFN)表現出了出色的泛化能力(訓練集 10.57,驗證集 10.99),并且在未見數據上實現了較低的平均誤差。其預測偏差約為10.99 美元。
總而言之,隨機森林是表現最好的模型,但 DFN 在泛化方面也表現出色。
圖片
圖片
圖片
圖:實際銷售額與預測銷售額(左:彈性網絡,中:隨機森林),DFN 的損失歷史記錄(右)
第二階段:迭代改進
第一階段的結果表明,這三個模型的泛化能力仍有提升空間。
我對銷售值應用了對數轉換,以便為模型的目標變量創建更加對稱的分布。
為了區分退款(sales列中的負銷售額)和正銷售額,我創建了一個is_return二分類token(1 表示退款,0 表示銷售額)。這樣一來,sales列就可以只關注正銷售額。
從數學上講,對負值取對數的結果是NaN。因此,我先用零替換負銷售額,然后應用拉普拉斯平滑法。這也能避免對數銷售額中出現負無窮值。
import numpy as np
# 使用新數據集
df_fin_rev = df_fin.copy()
# 添加 is_return 標志
df_fin_rev['is_return'] = (df_fin_rev['sales'] < 0).astype(int)
# sales 列中的零值或正值
df_fin_rev[ 'sales' ] = df_fin_rev[ 'sales' ].apply( lambda x: max (x, 0 ))
# 在取對數之前應用拉普拉斯平滑
alpha = 1
df_fin_rev['sales'] = np.log(df_fin_rev['sales'] + alpha)
df_fin_rev.info()
添加第 17 列。變換第 11 列。
在確保數據集中除 customerid 列外不存在缺失值后:
df_fin_rev.isna().sum ()
圖片
我使用相同的預處理步驟和超參數重新訓練了模型:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error, mean_absolute_error
from category_encoders import BinaryEncoder
# 創建數據集
X = df_fin_rev.copy().drop(columns=target_col)
y = df_fin_rev.copy()[target_col]
test_size = 50000
X_tv, X_test, y_tv, y_test = train_test_split(X, y, test_size=test_size, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_tv, y_tv, test_size=test_size, random_state=42)
# 預處理
num_transformer = Pipeline(steps=[('scaler', StandardScaler())])
cat_transformer = Pipeline(steps=[('encoder', BinaryEncoder(cols=cat_cols, handle_missing='0'))])
preprocessor_en = ColumnTransformer(
transformers=[
('num', num_transformer, num_cols),
('cat', cat_transformer, cat_cols)
],
remainder='passthrough'
)
X_train_processed_en = preprocessor_en.fit_transform(X_train)
X_val_processed_en = preprocessor_en.transform(X_val)
X_test_processed_en = preprocessor_en.transform(X_test)
# 模型訓練
elastic_net.fit(X_train_processed_en, y_train)
# 預測 (對數銷售額)
y_pred_train = elastic_net.predict(X_train_processed_en)
y_pred_val = elastic_net.predict(X_val_processed_en)
y_pred_test = elastic_net.predict(X_test_processed_en)
# 評估 - 對數銷售額 - 使用 MSE 進行評估
mse_train = mean_squared_error(y_train, y_pred_train)
mse_val = mean_squared_error(y_val, y_pred_val)
mse_test = mean_squared_error(y_test, y_pred_test)
# 評估 - 實際銷售額 - 使用 MAE 進行評估
mae_train_exp = mean_absolute_error(np.exp(y_train), np.exp(y_pred_train))
mae_val_exp = mean_absolute_error(np.exp(y_val), np.exp(y_pred_val))
mae_test_exp = mean_abolute_error(np.exp(y_test), np.exp(y_pred_test))結果
使用記錄的銷售數據的MSE和實際值銷售的MAE來評估模型性能:
彈性網絡:
- 對數銷售的 MSE:訓練集:1.133 → 1.132,泛化集:1.122
- 實值銷售的 MAE:訓練集:15.825 → 14.714,泛化集:16.509
隨機森林:
- 對數銷售額的 MSE:訓練集:0.020 → 0.175,泛化集:0.176
- 實值銷售的 MAE:訓練集:4.135 → 7.187,泛化集:9.041
DFN:
- 對數銷售額的 MSE:訓練集:1.079 → 0.165 泛化集:0.079
- 實值銷售的 MAE:訓練集:5.644 → 5.016,泛化集:6.197
(基于 50,000 個測試樣本的概括。)
與第一階段相比,所有模型中實際銷售額的 MAE 都有所提高,這表明目標變量密度的重要性。
其中,DFN 在訓練集(5.64)和泛化集(6.20)中均表現出較低的 MAE,展現出最佳性能,表明其在復雜、大型數據集上的學習和泛化能力較強。其對未見數據的預測偏差約為6.20 美元。
Elastic Net表現出了極好的泛化能力,但其對未見數據的預測偏差為16.51 美元,是所有模型中偏差最大的,這表明其在處理復雜數據集時遇到了困難。
隨機森林表現出嚴重的過擬合,其較低的訓練 MAE(4.14)與較高的泛化 MAE(9.04)之間存在較大差距。該模型的下一步可以進行超參數調整,以收緊正則化變量和樹結構。
實驗總結
實驗表明,PyTorch 上的 DFN 在具有 EDA 期間識別的特征的轉換數據集上表現最佳。
回到業務解決方案的初始假設,我們可以將這一發現直接用于營銷媒體組合優化,例如,使 DFN 能夠預測新客戶的終身價值并優化對高價值客戶渠道的預算分配。
下一步,我們可以在第 2 階段進一步探索特征工程,或者進入第 3 階段調整超參數以完善結果。
寫在最后
特征工程不僅僅是數據操作;它是一種從原始數據中獲取強大洞察力并顯著提高模型解決當前問題的能力的戰略方法。
在我們的實驗中,我們觀察到特征工程顯著提升了模型的性能,尤其是在與 EDA 和業務目標緊密結合的情況下。通過與領域專家和業務利益相關者合作完善假設,我們有望實現進一步的改進。
通過投入時間和精力來制定有效的輸入,我們從根本上增強了模型的學習、概括和提供卓越預測性能的能力。































