日萌社
人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度學習實戰(不定時更新)
三 CTR預估數據準備
3.1 分析並預處理raw_sample數據集
# 從HDFS中加載樣本數據信息
df = spark.read.csv("hdfs://localhost:9000/data/raw_sample.csv", header=True)
df.show() # 展示數據,默認前20條
df.printSchema()
顯示結果:
+------+----------+----------+-----------+------+---+
| user|time_stamp|adgroup_id| pid|nonclk|clk|
+------+----------+----------+-----------+------+---+
|581738|1494137644| 1|430548_1007| 1| 0|
|449818|1494638778| 3|430548_1007| 1| 0|
|914836|1494650879| 4|430548_1007| 1| 0|
|914836|1494651029| 5|430548_1007| 1| 0|
|399907|1494302958| 8|430548_1007| 1| 0|
|628137|1494524935| 9|430548_1007| 1| 0|
|298139|1494462593| 9|430539_1007| 1| 0|
|775475|1494561036| 9|430548_1007| 1| 0|
|555266|1494307136| 11|430539_1007| 1| 0|
|117840|1494036743| 11|430548_1007| 1| 0|
|739815|1494115387| 11|430539_1007| 1| 0|
|623911|1494625301| 11|430548_1007| 1| 0|
|623911|1494451608| 11|430548_1007| 1| 0|
|421590|1494034144| 11|430548_1007| 1| 0|
|976358|1494156949| 13|430548_1007| 1| 0|
|286630|1494218579| 13|430539_1007| 1| 0|
|286630|1494289247| 13|430539_1007| 1| 0|
|771431|1494153867| 13|430548_1007| 1| 0|
|707120|1494220810| 13|430548_1007| 1| 0|
|530454|1494293746| 13|430548_1007| 1| 0|
+------+----------+----------+-----------+------+---+
only showing top 20 rows
root
|-- user: string (nullable = true)
|-- time_stamp: string (nullable = true)
|-- adgroup_id: string (nullable = true)
|-- pid: string (nullable = true)
|-- nonclk: string (nullable = true)
|-- clk: string (nullable = true)
- 分析數據集字段的類型和格式
- 查看是否有空值
- 查看每列數據的類型
- 查看每列數據的類別情況
print("樣本數據集總條目數:", df.count())
# 約2600w
print("用戶user總數:", df.groupBy("user").count().count())
# 約 114w,略多餘日誌數據中用戶數
print("廣告id adgroup_id總數:", df.groupBy("adgroup_id").count().count())
# 約85w
print("廣告展示位pid情況:", df.groupBy("pid").count().collect())
# 只有兩種廣告展示位,佔比約爲六比四
print("廣告點擊數據情況clk:", df.groupBy("clk").count().collect())
# 點和不點比率約: 1:20
顯示結果:
樣本數據集總條目數: 26557961
用戶user總數: 1141729
廣告id adgroup_id總數: 846811
廣告展示位pid情況: [Row(pid='430548_1007', count=16472898), Row(pid='430539_1007', count=10085063)]
廣告點擊數據情況clk: [Row(clk='0', count=25191905), Row(clk='1', count=1366056)]
- 使用dataframe.withColumn更改df列數據結構;使用dataframe.withColumnRenamed更改列名稱
# 更改表結構,轉換爲對應的數據類型
from pyspark.sql.types import StructType, StructField, IntegerType, FloatType, LongType, StringType
# 打印df結構信息
df.printSchema()
# 更改df表結構:更改列類型和列名稱
raw_sample_df = df.\
withColumn("user", df.user.cast(IntegerType())).withColumnRenamed("user", "userId").\
withColumn("time_stamp", df.time_stamp.cast(LongType())).withColumnRenamed("time_stamp", "timestamp").\
withColumn("adgroup_id", df.adgroup_id.cast(IntegerType())).withColumnRenamed("adgroup_id", "adgroupId").\
withColumn("pid", df.pid.cast(StringType())).\
withColumn("nonclk", df.nonclk.cast(IntegerType())).\
withColumn("clk", df.clk.cast(IntegerType()))
raw_sample_df.printSchema()
raw_sample_df.show()
顯示結果:
root
|-- user: string (nullable = true)
|-- time_stamp: string (nullable = true)
|-- adgroup_id: string (nullable = true)
|-- pid: string (nullable = true)
|-- nonclk: string (nullable = true)
|-- clk: string (nullable = true)
root
|-- userId: integer (nullable = true)
|-- timestamp: long (nullable = true)
|-- adgroupId: integer (nullable = true)
|-- pid: string (nullable = true)
|-- nonclk: integer (nullable = true)
|-- clk: integer (nullable = true)
+------+----------+---------+-----------+------+---+
|userId| timestamp|adgroupId| pid|nonclk|clk|
+------+----------+---------+-----------+------+---+
|581738|1494137644| 1|430548_1007| 1| 0|
|449818|1494638778| 3|430548_1007| 1| 0|
|914836|1494650879| 4|430548_1007| 1| 0|
|914836|1494651029| 5|430548_1007| 1| 0|
|399907|1494302958| 8|430548_1007| 1| 0|
|628137|1494524935| 9|430548_1007| 1| 0|
|298139|1494462593| 9|430539_1007| 1| 0|
|775475|1494561036| 9|430548_1007| 1| 0|
|555266|1494307136| 11|430539_1007| 1| 0|
|117840|1494036743| 11|430548_1007| 1| 0|
|739815|1494115387| 11|430539_1007| 1| 0|
|623911|1494625301| 11|430548_1007| 1| 0|
|623911|1494451608| 11|430548_1007| 1| 0|
|421590|1494034144| 11|430548_1007| 1| 0|
|976358|1494156949| 13|430548_1007| 1| 0|
|286630|1494218579| 13|430539_1007| 1| 0|
|286630|1494289247| 13|430539_1007| 1| 0|
|771431|1494153867| 13|430548_1007| 1| 0|
|707120|1494220810| 13|430548_1007| 1| 0|
|530454|1494293746| 13|430548_1007| 1| 0|
+------+----------+---------+-----------+------+---+
only showing top 20 rows
-
特徵選取(Feature Selection)
-
特徵選擇就是選擇那些靠譜的Feature,去掉冗餘的Feature,對於搜索廣告,Query關鍵詞和廣告的匹配程度很重要;但對於展示廣告,廣告本身的歷史表現,往往是最重要的Feature。
根據經驗,該數據集中,只有廣告展示位pid對比較重要,且數據不同數據之間的佔比約爲6:4,因此pid可以作爲一個關鍵特徵
nonclk和clk在這裏是作爲目標值,不做爲特徵
-
-
熱獨編碼 OneHotEncode
-
熱獨編碼是一種經典編碼,是使用N位狀態寄存器(如0和1)來對N個狀態進行編碼,每個狀態都由他獨立的寄存器位,並且在任意時候,其中只有一位有效。
假設有三組特徵,分別表示年齡,城市,設備;
["男", "女"][0,1]
["北京", "上海", "廣州"][0,1,2]
["蘋果", "小米", "華爲", "微軟"][0,1,2,3]
傳統變化: 對每一組特徵,使用枚舉類型,從0開始;
["男“,”上海“,”小米“]=[ 0,1,1]
["女“,”北京“,”蘋果“] =[1,0,0]
傳統變化後的數據不是連續的,而是隨機分配的,不容易應用在分類器中
而經過熱獨編碼,數據會變成稀疏的,方便分類器處理:
["男“,”上海“,”小米“]=[ 1,0,0,1,0,0,1,0,0]
["女“,”北京“,”蘋果“] =[0,1,1,0,0,1,0,0,0]
這樣做保留了特徵的多樣性,但是也要注意如果數據過於稀疏(樣本較少、維度過高),其效果反而會變差
-
-
Spark中使用熱獨編碼
-
注意:熱編碼只能對字符串類型的列數據進行處理
StringIndexer:對指定字符串列數據進行特徵處理,如將性別數據“男”、“女”轉化爲0和1
OneHotEncoder:對特徵列數據,進行熱編碼,通常需結合StringIndexer一起使用
Pipeline:讓數據按順序依次被處理,將前一次的處理結果作爲下一次的輸入
-
-
特徵處理
'''特徵處理'''
'''
pid 資源位。該特徵屬於分類特徵,只有兩類取值,因此考慮進行熱編碼處理即可,分爲是否在資源位1、是否在資源位2 兩個特徵
'''
from pyspark.ml.feature import OneHotEncoder
from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline
# StringIndexer對指定字符串列進行特徵處理
stringindexer = StringIndexer(inputCol='pid', outputCol='pid_feature')
# 對處理出來的特徵處理列進行,熱獨編碼
encoder = OneHotEncoder(dropLast=False, inputCol='pid_feature', outputCol='pid_value')
# 利用管道對每一個數據進行熱獨編碼處理
pipeline = Pipeline(stages=[stringindexer, encoder])
pipeline_model = pipeline.fit(raw_sample_df)
new_df = pipeline_model.transform(raw_sample_df)
new_df.show()
顯示結果:
+------+----------+---------+-----------+------+---+-----------+-------------+
|userId| timestamp|adgroupId| pid|nonclk|clk|pid_feature| pid_value|
+------+----------+---------+-----------+------+---+-----------+-------------+
|581738|1494137644| 1|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|449818|1494638778| 3|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|914836|1494650879| 4|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|914836|1494651029| 5|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|399907|1494302958| 8|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|628137|1494524935| 9|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|298139|1494462593| 9|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|775475|1494561036| 9|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|555266|1494307136| 11|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|117840|1494036743| 11|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|739815|1494115387| 11|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|623911|1494625301| 11|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|623911|1494451608| 11|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|421590|1494034144| 11|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|976358|1494156949| 13|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|286630|1494218579| 13|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|286630|1494289247| 13|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|771431|1494153867| 13|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|707120|1494220810| 13|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|530454|1494293746| 13|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
+------+----------+---------+-----------+------+---+-----------+-------------+
only showing top 20 rows
- 返回字段pid_value是一個稀疏向量類型數據 pyspark.ml.linalg.SparseVector
from pyspark.ml.linalg import SparseVector
# 參數:維度、索引列表、值列表
print(SparseVector(4, [1, 3], [3.0, 4.0]))
print(SparseVector(4, [1, 3], [3.0, 4.0]).toArray())
print("*********")
print(new_df.select("pid_value").first())
print(new_df.select("pid_value").first().pid_value.toArray())
顯示結果:
(4,[1,3],[3.0,4.0])
[0. 3. 0. 4.]
*********
Row(pid_value=SparseVector(2, {0: 1.0}))
[1. 0.]
- 查看最大時間
new_df.sort("timestamp", ascending=False).show()
+------+----------+---------+-----------+------+---+-----------+-------------+
|userId| timestamp|adgroupId| pid|nonclk|clk|pid_feature| pid_value|
+------+----------+---------+-----------+------+---+-----------+-------------+
|177002|1494691186| 593001|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|243671|1494691186| 600195|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|488527|1494691184| 494312|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|488527|1494691184| 431082|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
| 17054|1494691184| 742741|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
| 17054|1494691184| 756665|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|488527|1494691184| 687854|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|839493|1494691183| 561681|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|704223|1494691183| 624504|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|839493|1494691183| 582235|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|704223|1494691183| 675674|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|628998|1494691180| 618965|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|674444|1494691179| 427579|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|627200|1494691179| 782038|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|627200|1494691179| 420769|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|674444|1494691179| 588664|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|738335|1494691179| 451004|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|627200|1494691179| 817569|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|322244|1494691179| 820018|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|322244|1494691179| 735220|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
+------+----------+---------+-----------+------+---+-----------+-------------+
only showing top 20 rows
# 本樣本數據集共計8天數據
# 前七天爲訓練數據、最後一天爲測試數據
from datetime import datetime
datetime.fromtimestamp(1494691186)
print("該時間之前的數據爲訓練樣本,該時間以後的數據爲測試樣本:", datetime.fromtimestamp(1494691186-24*60*60))
顯示結果:
該時間之前的數據爲訓練樣本,該時間以後的數據爲測試樣本: 2017-05-12 23:59:46
- 訓練樣本
# 訓練樣本:
train_sample = raw_sample_df.filter(raw_sample_df.timestamp<=(1494691186-24*60*60))
print("訓練樣本個數:")
print(train_sample.count())
# 測試樣本
test_sample = raw_sample_df.filter(raw_sample_df.timestamp>(1494691186-24*60*60))
print("測試樣本個數:")
print(test_sample.count())
# 注意:還需要加入廣告基本特徵和用戶基本特徵才能做程一份完整的樣本數據集
顯示結果:
訓練樣本個數:
23249291
測試樣本個數:
3308670
3.2 分析並預處理ad_feature數據集
# 從HDFS中加載廣告基本信息數據,返回spark dafaframe對象
df = spark.read.csv("hdfs://localhost:9000/data/ad_feature.csv", header=True)
df.show() # 展示數據,默認前20條
顯示結果:
+----------+-------+-----------+--------+------+-----+
|adgroup_id|cate_id|campaign_id|customer| brand|price|
+----------+-------+-----------+--------+------+-----+
| 63133| 6406| 83237| 1| 95471|170.0|
| 313401| 6406| 83237| 1| 87331|199.0|
| 248909| 392| 83237| 1| 32233| 38.0|
| 208458| 392| 83237| 1|174374|139.0|
| 110847| 7211| 135256| 2|145952|32.99|
| 607788| 6261| 387991| 6|207800|199.0|
| 375706| 4520| 387991| 6| NULL| 99.0|
| 11115| 7213| 139747| 9|186847| 33.0|
| 24484| 7207| 139744| 9|186847| 19.0|
| 28589| 5953| 395195| 13| NULL|428.0|
| 23236| 5953| 395195| 13| NULL|368.0|
| 300556| 5953| 395195| 13| NULL|639.0|
| 92560| 5953| 395195| 13| NULL|368.0|
| 590965| 4284| 28145| 14|454237|249.0|
| 529913| 4284| 70206| 14| NULL|249.0|
| 546930| 4284| 28145| 14| NULL|249.0|
| 639794| 6261| 70206| 14| 37004| 89.9|
| 335413| 4284| 28145| 14| NULL|249.0|
| 794890| 4284| 70206| 14|454237|249.0|
| 684020| 6261| 70206| 14| 37004| 99.0|
+----------+-------+-----------+--------+------+-----+
only showing top 20 rows
# 注意:由於本數據集中存在NULL字樣的數據,無法直接設置schema,只能先將NULL類型的數據處理掉,然後進行類型轉換
from pyspark.sql.types import StructType, StructField, IntegerType, FloatType
# 替換掉NULL字符串,替換掉
df = df.replace("NULL", "-1")
# 打印df結構信息
df.printSchema()
# 更改df表結構:更改列類型和列名稱
ad_feature_df = df.\
withColumn("adgroup_id", df.adgroup_id.cast(IntegerType())).withColumnRenamed("adgroup_id", "adgroupId").\
withColumn("cate_id", df.cate_id.cast(IntegerType())).withColumnRenamed("cate_id", "cateId").\
withColumn("campaign_id", df.campaign_id.cast(IntegerType())).withColumnRenamed("campaign_id", "campaignId").\
withColumn("customer", df.customer.cast(IntegerType())).withColumnRenamed("customer", "customerId").\
withColumn("brand", df.brand.cast(IntegerType())).withColumnRenamed("brand", "brandId").\
withColumn("price", df.price.cast(FloatType()))
ad_feature_df.printSchema()
ad_feature_df.show()
顯示結果:
root
|-- adgroup_id: string (nullable = true)
|-- cate_id: string (nullable = true)
|-- campaign_id: string (nullable = true)
|-- customer: string (nullable = true)
|-- brand: string (nullable = true)
|-- price: string (nullable = true)
root
|-- adgroupId: integer (nullable = true)
|-- cateId: integer (nullable = true)
|-- campaignId: integer (nullable = true)
|-- customerId: integer (nullable = true)
|-- brandId: integer (nullable = true)
|-- price: float (nullable = true)
+---------+------+----------+----------+-------+-----+
|adgroupId|cateId|campaignId|customerId|brandId|price|
+---------+------+----------+----------+-------+-----+
| 63133| 6406| 83237| 1| 95471|170.0|
| 313401| 6406| 83237| 1| 87331|199.0|
| 248909| 392| 83237| 1| 32233| 38.0|
| 208458| 392| 83237| 1| 174374|139.0|
| 110847| 7211| 135256| 2| 145952|32.99|
| 607788| 6261| 387991| 6| 207800|199.0|
| 375706| 4520| 387991| 6| -1| 99.0|
| 11115| 7213| 139747| 9| 186847| 33.0|
| 24484| 7207| 139744| 9| 186847| 19.0|
| 28589| 5953| 395195| 13| -1|428.0|
| 23236| 5953| 395195| 13| -1|368.0|
| 300556| 5953| 395195| 13| -1|639.0|
| 92560| 5953| 395195| 13| -1|368.0|
| 590965| 4284| 28145| 14| 454237|249.0|
| 529913| 4284| 70206| 14| -1|249.0|
| 546930| 4284| 28145| 14| -1|249.0|
| 639794| 6261| 70206| 14| 37004| 89.9|
| 335413| 4284| 28145| 14| -1|249.0|
| 794890| 4284| 70206| 14| 454237|249.0|
| 684020| 6261| 70206| 14| 37004| 99.0|
+---------+------+----------+----------+-------+-----+
only showing top 20 rows
- 查看各項數據的特徵
print("總廣告條數:",df.count()) # 數據條數
_1 = ad_feature_df.groupBy("cateId").count().count()
print("cateId數值個數:", _1)
_2 = ad_feature_df.groupBy("campaignId").count().count()
print("campaignId數值個數:", _2)
_3 = ad_feature_df.groupBy("customerId").count().count()
print("customerId數值個數:", _3)
_4 = ad_feature_df.groupBy("brandId").count().count()
print("brandId數值個數:", _4)
ad_feature_df.sort("price").show()
ad_feature_df.sort("price", ascending=False).show()
print("價格高於1w的條目個數:", ad_feature_df.select("price").filter("price>10000").count())
print("價格低於1的條目個數", ad_feature_df.select("price").filter("price<1").count())
顯示結果:
總廣告條數: 846811
cateId數值個數: 6769
campaignId數值個數: 423436
customerId數值個數: 255875
brandId數值個數: 99815
+---------+------+----------+----------+-------+-----+
|adgroupId|cateId|campaignId|customerId|brandId|price|
+---------+------+----------+----------+-------+-----+
| 485749| 9970| 352666| 140520| -1| 0.01|
| 88975| 9996| 198424| 182415| -1| 0.01|
| 109704| 10539| 59774| 90351| 202710| 0.01|
| 49911| 7032| 129079| 172334| -1| 0.01|
| 339334| 9994| 310408| 211292| 383023| 0.01|
| 6636| 6703| 392038| 46239| 406713| 0.01|
| 92241| 6130| 72781| 149714| -1| 0.01|
| 20397| 10539| 410958| 65726| 79971| 0.01|
| 345870| 9995| 179595| 191036| 79971| 0.01|
| 77797| 9086| 218276| 31183| -1| 0.01|
| 14435| 1136| 135610| 17788| -1| 0.01|
| 42055| 9994| 43866| 113068| 123242| 0.01|
| 41925| 7032| 85373| 114532| -1| 0.01|
| 67558| 9995| 90141| 83948| -1| 0.01|
| 149570| 7043| 126746| 176076| -1| 0.01|
| 518883| 7185| 403318| 58013| -1| 0.01|
| 2246| 9996| 413653| 60214| 182966| 0.01|
| 290675| 4824| 315371| 240984| -1| 0.01|
| 552638| 10305| 403318| 58013| -1| 0.01|
| 89831| 10539| 90141| 83948| 211816| 0.01|
+---------+------+----------+----------+-------+-----+
only showing top 20 rows
+---------+------+----------+----------+-------+-----------+
|adgroupId|cateId|campaignId|customerId|brandId| price|
+---------+------+----------+----------+-------+-----------+
| 658722| 1093| 218101| 207754| -1| 1.0E8|
| 468220| 1093| 270719| 207754| -1| 1.0E8|
| 179746| 1093| 270027| 102509| 405447| 1.0E8|
| 443295| 1093| 44251| 102509| 300681| 1.0E8|
| 31899| 685| 218918| 31239| 278301| 1.0E8|
| 243384| 685| 218918| 31239| 278301| 1.0E8|
| 554311| 1093| 266086| 207754| -1| 1.0E8|
| 513942| 745| 8401| 86243| -1|8.8888888E7|
| 201060| 745| 8401| 86243| -1|5.5555556E7|
| 289563| 685| 37665| 120847| 278301| 1.5E7|
| 35156| 527| 417722| 72273| 278301| 1.0E7|
| 33756| 527| 416333| 70894| -1| 9900000.0|
| 335495| 739| 170121| 148946| 326126| 9600000.0|
| 218306| 206| 162394| 4339| 221720| 8888888.0|
| 213567| 7213| 239302| 205612| 406125| 5888888.0|
| 375920| 527| 217512| 148946| 326126| 4760000.0|
| 262215| 527| 132721| 11947| 417898| 3980000.0|
| 154623| 739| 170121| 148946| 326126| 3900000.0|
| 152414| 739| 170121| 148946| 326126| 3900000.0|
| 448651| 527| 422260| 41289| 209959| 3800000.0|
+---------+------+----------+----------+-------+-----------+
only showing top 20 rows
價格高於1w的條目個數: 6527
價格低於1的條目個數 5762
-
特徵選擇
- cateId:脫敏過的商品類目ID;
- campaignId:脫敏過的廣告計劃ID;
- customerId:脫敏過的廣告主ID;
- brandId:脫敏過的品牌ID;
以上四個特徵均屬於分類特徵,但由於分類值個數均過於龐大,如果去做熱獨編碼處理,會導致數據過於稀疏 且當前我們缺少對這些特徵更加具體的信息,(如商品類目具體信息、品牌具體信息等),從而無法對這些特徵的數據做聚類、降維處理 因此這裏不選取它們作爲特徵
而只選取price作爲特徵數據,因爲價格本身是一個統計類型連續數值型數據,且能很好的體現廣告的價值屬性特徵,通常也不需要做其他處理(離散化、歸一化、標準化等),所以這裏直接將當做特徵數據來使用
3.3 分析並預處理user_profile數據集
# 從HDFS加載用戶基本信息數據
df = spark.read.csv("hdfs://localhost:9000/data/user_profile.csv", header=True)
# 發現pvalue_level和new_user_class_level存在空值:(注意此處的null表示空值,而如果是NULL,則往往表示是一個字符串)
# 因此直接利用schema就可以加載進該數據,無需替換null值
df.show()
顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+---------------------+
|userid|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level |
+------+---------+------------+-----------------+---------+------------+--------------+----------+---------------------+
| 234| 0| 5| 2| 5| null| 3| 0| 3|
| 523| 5| 2| 2| 2| 1| 3| 1| 2|
| 612| 0| 8| 1| 2| 2| 3| 0| null|
| 1670| 0| 4| 2| 4| null| 1| 0| null|
| 2545| 0| 10| 1| 4| null| 3| 0| null|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2|
| 6211| 0| 9| 1| 3| null| 3| 0| 2|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2|
| 9293| 0| 5| 2| 5| null| 3| 0| 4|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2|
| 10549| 0| 4| 2| 4| 2| 3| 0| null|
| 10812| 0| 4| 2| 4| null| 2| 0| null|
| 10912| 0| 4| 2| 4| 2| 3| 0| null|
| 10996| 0| 5| 2| 5| null| 3| 0| 4|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4|
+------+---------+------------+-----------------+---------+------------+--------------+----------+---------------------+
# 注意:這裏的null會直接被pyspark識別爲None數據,也就是na數據,所以這裏可以直接利用schema導入數據
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, LongType, FloatType
# 構建表結構schema對象
schema = StructType([
StructField("userId", IntegerType()),
StructField("cms_segid", IntegerType()),
StructField("cms_group_id", IntegerType()),
StructField("final_gender_code", IntegerType()),
StructField("age_level", IntegerType()),
StructField("pvalue_level", IntegerType()),
StructField("shopping_level", IntegerType()),
StructField("occupation", IntegerType()),
StructField("new_user_class_level", IntegerType())
])
# 利用schema從hdfs加載
user_profile_df = spark.read.csv("hdfs://localhost:9000/data/user_profile.csv", header=True, schema=schema)
user_profile_df.printSchema()
user_profile_df.show()
顯示結果:
root
|-- userId: integer (nullable = true)
|-- cms_segid: integer (nullable = true)
|-- cms_group_id: integer (nullable = true)
|-- final_gender_code: integer (nullable = true)
|-- age_level: integer (nullable = true)
|-- pvalue_level: integer (nullable = true)
|-- shopping_level: integer (nullable = true)
|-- occupation: integer (nullable = true)
|-- new_user_class_level: integer (nullable = true)
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
| 234| 0| 5| 2| 5| null| 3| 0| 3|
| 523| 5| 2| 2| 2| 1| 3| 1| 2|
| 612| 0| 8| 1| 2| 2| 3| 0| null|
| 1670| 0| 4| 2| 4| null| 1| 0| null|
| 2545| 0| 10| 1| 4| null| 3| 0| null|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2|
| 6211| 0| 9| 1| 3| null| 3| 0| 2|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2|
| 9293| 0| 5| 2| 5| null| 3| 0| 4|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2|
| 10549| 0| 4| 2| 4| 2| 3| 0| null|
| 10812| 0| 4| 2| 4| null| 2| 0| null|
| 10912| 0| 4| 2| 4| 2| 3| 0| null|
| 10996| 0| 5| 2| 5| null| 3| 0| 4|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
only showing top 20 rows
- 顯示特徵情況
print("分類特徵值個數情況: ")
print("cms_segid: ", user_profile_df.groupBy("cms_segid").count().count())
print("cms_group_id: ", user_profile_df.groupBy("cms_group_id").count().count())
print("final_gender_code: ", user_profile_df.groupBy("final_gender_code").count().count())
print("age_level: ", user_profile_df.groupBy("age_level").count().count())
print("shopping_level: ", user_profile_df.groupBy("shopping_level").count().count())
print("occupation: ", user_profile_df.groupBy("occupation").count().count())
print("含缺失值的特徵情況: ")
user_profile_df.groupBy("pvalue_level").count().show()
user_profile_df.groupBy("new_user_class_level").count().show()
t_count = user_profile_df.count()
pl_na_count = t_count - user_profile_df.dropna(subset=["pvalue_level"]).count()
print("pvalue_level的空值情況:", pl_na_count, "空值佔比:%0.2f%%"%(pl_na_count/t_count*100))
nul_na_count = t_count - user_profile_df.dropna(subset=["new_user_class_level"]).count()
print("new_user_class_level的空值情況:", nul_na_count, "空值佔比:%0.2f%%"%(nul_na_count/t_count*100))
顯示內容:
分類特徵值個數情況:
cms_segid: 97
cms_group_id: 13
final_gender_code: 2
age_level: 7
shopping_level: 3
occupation: 2
含缺失值的特徵情況:
+------------+------+
|pvalue_level| count|
+------------+------+
| null|575917|
| 1|154436|
| 3| 37759|
| 2|293656|
+------------+------+
+--------------------+------+
|new_user_class_level| count|
+--------------------+------+
| null|344920|
| 1| 80548|
| 3|173047|
| 4|138833|
| 2|324420|
+--------------------+------+
pvalue_level的空值情況: 575917 空值佔比:54.24%
new_user_class_level的空值情況: 344920 空值佔比:32.49%
-
缺失值處理
-
注意,一般情況下:
- 缺失率低於10%:可直接進行相應的填充,如默認值、均值、算法擬合等等;
- 高於10%:往往會考慮捨棄該特徵
- 特徵處理,如1維轉多維
但根據我們的經驗,我們的廣告推薦其實和用戶的消費水平、用戶所在城市等級都有比較大的關聯,因此在這裏pvalue_level、new_user_class_level都是比較重要的特徵,我們不考慮捨棄
-
-
缺失值處理方案:
- 填充方案:結合用戶的其他特徵值,利用隨機森林算法進行預測;但產生了大量人爲構建的數據,一定程度上增加了數據的噪音
- 把變量映射到高維空間:如pvalue_level的1維數據,轉換成是否1、是否2、是否3、是否缺失的4維數據;這樣保證了所有原始數據不變,同時能提高精確度,但這樣會導致數據變得比較稀疏,如果樣本量很小,反而會導致樣本效果較差,因此也不能濫用
-
填充方案
- 利用隨機森林對pvalue_level的缺失值進行預測
from pyspark.mllib.regression import LabeledPoint
# 剔除掉缺失值數據,將餘下的數據作爲訓練數據
# user_profile_df.dropna(subset=["pvalue_level"]): 將pvalue_level中的空值所在行數據剔除後的數據,作爲訓練樣本
train_data = user_profile_df.dropna(subset=["pvalue_level"]).rdd.map(
lambda r:LabeledPoint(r.pvalue_level-1, [r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation])
)
# 注意隨機森林輸入數據時,由於label的分類數是從0開始的,但pvalue_level的目前只分別是1,2,3,所以需要對應分別-1來作爲目標值
# 自然那麼最終得出預測值後,需要對應+1才能還原回來
# 我們使用cms_segid, cms_group_id, final_gender_code, age_level, shopping_level, occupation作爲特徵值,pvalue_level作爲目標值
- Labeled point
A labeled point is a local vector, either dense or sparse, associated with a label/response. In MLlib, labeled points are used in supervised learning algorithms. We use a double to store a label, so we can use labeled points in both regression and classification. For binary classification, a label should be either 0 (negative) or 1 (positive). For multiclass classification, labels should be class indices starting from zero: 0, 1, 2, …. 標記點是與標籤/響應相關聯的密集或稀疏的局部矢量。在MLlib中,標記點用於監督學習算法。我們使用double來存儲標籤,因此我們可以在迴歸和分類中使用標記點。對於二分類情況,目標值應爲0(負)或1(正)。對於多分類,標籤應該是從零開始的類索引:0, 1, 2, …。
Python A labeled point is represented by LabeledPoint. 標記點表示爲 LabeledPoint。 Refer to the LabeledPoint Python docs for more details on the API. 有關API的更多詳細信息,請參閱LabeledPointPython文檔。
from pyspark.mllib.linalg import SparseVector
from pyspark.mllib.regression import LabeledPoint
# Create a labeled point with a positive label and a dense feature vector.
pos = LabeledPoint(1.0, [1.0, 0.0, 3.0])
# Create a labeled point with a negative label and a sparse feature vector.
neg = LabeledPoint(0.0, SparseVector(3, [0, 2], [1.0, 3.0]))
from pyspark.mllib.tree import RandomForest
# 訓練分類模型
# 參數1 訓練的數據
#參數2 目標值的分類個數 0,1,2
#參數3 特徵中是否包含分類的特徵 {2:2,3:7} {2:2} 表示 在特徵中 第二個特徵是分類的: 有兩個分類
#參數4 隨機森林中 樹的棵數
model = RandomForest.trainClassifier(train_data, 3, {}, 5)
# 預測單個數據
# 注意用法:https://spark.apache.org/docs/latest/api/python/pyspark.mllib.html?highlight=tree%20random#pyspark.mllib.tree.RandomForestModel.predict
model.predict([0.0, 4.0 ,2.0 , 4.0, 1.0, 0.0])
顯示結果:
1.0
- 篩選出缺失值條目
pl_na_df = user_profile_df.na.fill(-1).where("pvalue_level=-1")
pl_na_df.show(10)
def row(r):
return r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation
# 轉換爲普通的rdd類型
rdd = pl_na_df.rdd.map(row)
# 預測全部的pvalue_level值:
predicts = model.predict(rdd)
# 查看前20條
print(predicts.take(20))
print("預測值總數", predicts.count())
# 這裏注意predict參數,如果是預測多個,那麼參數必須是直接有列表構成的rdd參數,而不能是dataframe.rdd類型
# 因此這裏經過map函數處理,將每一行數據轉換爲普通的列表數據
顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
| 234| 0| 5| 2| 5| -1| 3| 0| 3|
| 1670| 0| 4| 2| 4| -1| 1| 0| -1|
| 2545| 0| 10| 1| 4| -1| 3| 0| -1|
| 6211| 0| 9| 1| 3| -1| 3| 0| 2|
| 9293| 0| 5| 2| 5| -1| 3| 0| 4|
| 10812| 0| 4| 2| 4| -1| 2| 0| -1|
| 10996| 0| 5| 2| 5| -1| 3| 0| 4|
| 11602| 0| 5| 2| 5| -1| 3| 0| 2|
| 11727| 0| 3| 2| 3| -1| 3| 0| 1|
| 12195| 0| 10| 1| 4| -1| 3| 0| 2|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
only showing top 10 rows
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0]
預測值總數 575917
- 轉換爲pandas dataframe
# 這裏數據量比較小,直接轉換爲pandas dataframe來處理,因爲方便,但注意如果數據量較大不推薦,因爲這樣會把全部數據加載到內存中
temp = predicts.map(lambda x:int(x)).collect()
pdf = pl_na_df.toPandas()
import numpy as np
# 在pandas df的基礎上直接替換掉列數據
pdf["pvalue_level"] = np.array(temp) + 1 # 注意+1 還原預測值
pdf
- 與非缺失數據進行拼接,完成pvalue_level的缺失值預測
new_user_profile_df = user_profile_df.dropna(subset=["pvalue_level"]).unionAll(spark.createDataFrame(pdf, schema=schema))
new_user_profile_df.show()
# 注意:unionAll的使用,兩個df的表結構必須完全一樣
顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
| 523| 5| 2| 2| 2| 1| 3| 1| 2|
| 612| 0| 8| 1| 2| 2| 3| 0| null|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2|
| 10549| 0| 4| 2| 4| 2| 3| 0| null|
| 10912| 0| 4| 2| 4| 2| 3| 0| null|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4|
| 11739| 20| 3| 2| 3| 2| 3| 0| 4|
| 12549| 33| 4| 2| 4| 2| 3| 0| 2|
| 15155| 36| 5| 2| 5| 2| 1| 0| null|
| 15347| 20| 3| 2| 3| 2| 3| 0| 3|
| 15455| 8| 2| 2| 2| 2| 3| 0| 3|
| 15783| 0| 4| 2| 4| 2| 3| 0| null|
| 16749| 5| 2| 2| 2| 1| 3| 1| 4|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
only showing top 20 rows
- 利用隨機森林對new_user_class_level的缺失值進行預測
from pyspark.mllib.regression import LabeledPoint
# 選出new_user_class_level全部的
train_data2 = user_profile_df.dropna(subset=["new_user_class_level"]).rdd.map(
lambda r:LabeledPoint(r.new_user_class_level - 1, [r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation])
)
from pyspark.mllib.tree import RandomForest
model2 = RandomForest.trainClassifier(train_data2, 4, {}, 5)
model2.predict([0.0, 4.0 ,2.0 , 4.0, 1.0, 0.0])
# 預測值實際應該爲2
顯示結果:
1.0
nul_na_df = user_profile_df.na.fill(-1).where("new_user_class_level=-1")
nul_na_df.show(10)
def row(r):
return r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation
rdd2 = nul_na_df.rdd.map(row)
predicts2 = model.predict(rdd2)
predicts2.take(20)
- 顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
| 612| 0| 8| 1| 2| 2| 3| 0| -1|
| 1670| 0| 4| 2| 4| -1| 1| 0| -1|
| 2545| 0| 10| 1| 4| -1| 3| 0| -1|
| 10549| 0| 4| 2| 4| 2| 3| 0| -1|
| 10812| 0| 4| 2| 4| -1| 2| 0| -1|
| 10912| 0| 4| 2| 4| 2| 3| 0| -1|
| 12620| 0| 4| 2| 4| -1| 2| 0| -1|
| 14437| 0| 5| 2| 5| -1| 3| 0| -1|
| 14574| 0| 1| 2| 1| -1| 2| 0| -1|
| 14985| 0| 11| 1| 5| -1| 2| 0| -1|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
only showing top 10 rows
[1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.0,
1.0,
0.0,
0.0,
1.0]
- 總結:可以發現由於這兩個字段的缺失過多,所以預測出來的值已經大大失真,但如果缺失率在10%以下,這種方法是比較有效的一種
user_profile_df = user_profile_df.na.fill(-1)
user_profile_df.show()
# new_df = new_df.withColumn("pvalue_level", new_df.pvalue_level.cast(StringType()))\
# .withColumn("new_user_class_level", new_df.new_user_class_level.cast(StringType()))
顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
| 234| 0| 5| 2| 5| -1| 3| 0| 3|
| 523| 5| 2| 2| 2| 1| 3| 1| 2|
| 612| 0| 8| 1| 2| 2| 3| 0| -1|
| 1670| 0| 4| 2| 4| -1| 1| 0| -1|
| 2545| 0| 10| 1| 4| -1| 3| 0| -1|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2|
| 6211| 0| 9| 1| 3| -1| 3| 0| 2|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2|
| 9293| 0| 5| 2| 5| -1| 3| 0| 4|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2|
| 10549| 0| 4| 2| 4| 2| 3| 0| -1|
| 10812| 0| 4| 2| 4| -1| 2| 0| -1|
| 10912| 0| 4| 2| 4| 2| 3| 0| -1|
| 10996| 0| 5| 2| 5| -1| 3| 0| 4|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
only showing top 20 rows
- 低維轉高維方式
- 我們接下來採用將變量映射到高維空間的方法來處理數據,即將缺失項也當做一個單獨的特徵來對待,保證數據的原始性 由於該思想正好和熱獨編碼實現方法一樣,因此這裏直接使用熱獨編碼方式處理數據
from pyspark.ml.feature import OneHotEncoder
from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline
# 使用熱獨編碼轉換pvalue_level的一維數據爲多維,其中缺失值單獨作爲一個特徵值
# 需要先將缺失值全部替換爲數值,與原有特徵一起處理
from pyspark.sql.types import StringType
user_profile_df = user_profile_df.na.fill(-1)
user_profile_df.show()
# 熱獨編碼時,必須先將待處理字段轉爲字符串類型纔可處理
user_profile_df = user_profile_df.withColumn("pvalue_level", user_profile_df.pvalue_level.cast(StringType()))\
.withColumn("new_user_class_level", user_profile_df.new_user_class_level.cast(StringType()))
user_profile_df.printSchema()
# 對pvalue_level進行熱獨編碼,求值
stringindexer = StringIndexer(inputCol='pvalue_level', outputCol='pl_onehot_feature')
encoder = OneHotEncoder(dropLast=False, inputCol='pl_onehot_feature', outputCol='pl_onehot_value')
pipeline = Pipeline(stages=[stringindexer, encoder])
pipeline_fit = pipeline.fit(user_profile_df)
user_profile_df2 = pipeline_fit.transform(user_profile_df)
# pl_onehot_value列的值爲稀疏向量,存儲熱獨編碼的結果
user_profile_df2.printSchema()
user_profile_df2.show()
顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
| 234| 0| 5| 2| 5| -1| 3| 0| 3|
| 523| 5| 2| 2| 2| 1| 3| 1| 2|
| 612| 0| 8| 1| 2| 2| 3| 0| -1|
| 1670| 0| 4| 2| 4| -1| 1| 0| -1|
| 2545| 0| 10| 1| 4| -1| 3| 0| -1|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2|
| 6211| 0| 9| 1| 3| -1| 3| 0| 2|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2|
| 9293| 0| 5| 2| 5| -1| 3| 0| 4|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2|
| 10549| 0| 4| 2| 4| 2| 3| 0| -1|
| 10812| 0| 4| 2| 4| -1| 2| 0| -1|
| 10912| 0| 4| 2| 4| 2| 3| 0| -1|
| 10996| 0| 5| 2| 5| -1| 3| 0| 4|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+
only showing top 20 rows
root
|-- userId: integer (nullable = true)
|-- cms_segid: integer (nullable = true)
|-- cms_group_id: integer (nullable = true)
|-- final_gender_code: integer (nullable = true)
|-- age_level: integer (nullable = true)
|-- pvalue_level: string (nullable = true)
|-- shopping_level: integer (nullable = true)
|-- occupation: integer (nullable = true)
|-- new_user_class_level: string (nullable = true)
root
|-- userId: integer (nullable = true)
|-- cms_segid: integer (nullable = true)
|-- cms_group_id: integer (nullable = true)
|-- final_gender_code: integer (nullable = true)
|-- age_level: integer (nullable = true)
|-- pvalue_level: string (nullable = true)
|-- shopping_level: integer (nullable = true)
|-- occupation: integer (nullable = true)
|-- new_user_class_level: string (nullable = true)
|-- pl_onehot_feature: double (nullable = false)
|-- pl_onehot_value: vector (nullable = true)
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|pl_onehot_feature|pl_onehot_value|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+
| 234| 0| 5| 2| 5| -1| 3| 0| 3| 0.0| (4,[0],[1.0])|
| 523| 5| 2| 2| 2| 1| 3| 1| 2| 2.0| (4,[2],[1.0])|
| 612| 0| 8| 1| 2| 2| 3| 0| -1| 1.0| (4,[1],[1.0])|
| 1670| 0| 4| 2| 4| -1| 1| 0| -1| 0.0| (4,[0],[1.0])|
| 2545| 0| 10| 1| 4| -1| 3| 0| -1| 0.0| (4,[0],[1.0])|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2| 1.0| (4,[1],[1.0])|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2| 1.0| (4,[1],[1.0])|
| 6211| 0| 9| 1| 3| -1| 3| 0| 2| 0.0| (4,[0],[1.0])|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4| 2.0| (4,[2],[1.0])|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1| 1.0| (4,[1],[1.0])|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2| 1.0| (4,[1],[1.0])|
| 9293| 0| 5| 2| 5| -1| 3| 0| 4| 0.0| (4,[0],[1.0])|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2| 1.0| (4,[1],[1.0])|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2| 1.0| (4,[1],[1.0])|
| 10549| 0| 4| 2| 4| 2| 3| 0| -1| 1.0| (4,[1],[1.0])|
| 10812| 0| 4| 2| 4| -1| 2| 0| -1| 0.0| (4,[0],[1.0])|
| 10912| 0| 4| 2| 4| 2| 3| 0| -1| 1.0| (4,[1],[1.0])|
| 10996| 0| 5| 2| 5| -1| 3| 0| 4| 0.0| (4,[0],[1.0])|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3| 2.0| (4,[2],[1.0])|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4| 2.0| (4,[2],[1.0])|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+
only showing top 20 rows
- 使用熱編碼轉換new_user_class_level的一維數據爲多維
stringindexer = StringIndexer(inputCol='new_user_class_level', outputCol='nucl_onehot_feature')
encoder = OneHotEncoder(dropLast=False, inputCol='nucl_onehot_feature', outputCol='nucl_onehot_value')
pipeline = Pipeline(stages=[stringindexer, encoder])
pipeline_fit = pipeline.fit(user_profile_df2)
user_profile_df3 = pipeline_fit.transform(user_profile_df2)
user_profile_df3.show()
顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+-------------------+-----------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|pl_onehot_feature|pl_onehot_value|nucl_onehot_feature|nucl_onehot_value|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+-------------------+-----------------+
| 234| 0| 5| 2| 5| -1| 3| 0| 3| 0.0| (4,[0],[1.0])| 2.0| (5,[2],[1.0])|
| 523| 5| 2| 2| 2| 1| 3| 1| 2| 2.0| (4,[2],[1.0])| 1.0| (5,[1],[1.0])|
| 612| 0| 8| 1| 2| 2| 3| 0| -1| 1.0| (4,[1],[1.0])| 0.0| (5,[0],[1.0])|
| 1670| 0| 4| 2| 4| -1| 1| 0| -1| 0.0| (4,[0],[1.0])| 0.0| (5,[0],[1.0])|
| 2545| 0| 10| 1| 4| -1| 3| 0| -1| 0.0| (4,[0],[1.0])| 0.0| (5,[0],[1.0])|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|
| 6211| 0| 9| 1| 3| -1| 3| 0| 2| 0.0| (4,[0],[1.0])| 1.0| (5,[1],[1.0])|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4| 2.0| (4,[2],[1.0])| 3.0| (5,[3],[1.0])|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1| 1.0| (4,[1],[1.0])| 4.0| (5,[4],[1.0])|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|
| 9293| 0| 5| 2| 5| -1| 3| 0| 4| 0.0| (4,[0],[1.0])| 3.0| (5,[3],[1.0])|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|
| 10549| 0| 4| 2| 4| 2| 3| 0| -1| 1.0| (4,[1],[1.0])| 0.0| (5,[0],[1.0])|
| 10812| 0| 4| 2| 4| -1| 2| 0| -1| 0.0| (4,[0],[1.0])| 0.0| (5,[0],[1.0])|
| 10912| 0| 4| 2| 4| 2| 3| 0| -1| 1.0| (4,[1],[1.0])| 0.0| (5,[0],[1.0])|
| 10996| 0| 5| 2| 5| -1| 3| 0| 4| 0.0| (4,[0],[1.0])| 3.0| (5,[3],[1.0])|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3| 2.0| (4,[2],[1.0])| 2.0| (5,[2],[1.0])|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4| 2.0| (4,[2],[1.0])| 3.0| (5,[3],[1.0])|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+-------------------+-----------------+
only showing top 20 rows
- 用戶特徵合併
from pyspark.ml.feature import VectorAssembler
feature_df = VectorAssembler().setInputCols(["age_level", "pl_onehot_value", "nucl_onehot_value"]).setOutputCol("features").transform(user_profile_df3)
feature_df.show()
顯示結果:
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+-------------------+-----------------+--------------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|pl_onehot_feature|pl_onehot_value|nucl_onehot_feature|nucl_onehot_value| features|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+-------------------+-----------------+--------------------+
| 234| 0| 5| 2| 5| -1| 3| 0| 3| 0.0| (4,[0],[1.0])| 2.0| (5,[2],[1.0])|(10,[0,1,7],[5.0,...|
| 523| 5| 2| 2| 2| 1| 3| 1| 2| 2.0| (4,[2],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,3,6],[2.0,...|
| 612| 0| 8| 1| 2| 2| 3| 0| -1| 1.0| (4,[1],[1.0])| 0.0| (5,[0],[1.0])|(10,[0,2,5],[2.0,...|
| 1670| 0| 4| 2| 4| -1| 1| 0| -1| 0.0| (4,[0],[1.0])| 0.0| (5,[0],[1.0])|(10,[0,1,5],[4.0,...|
| 2545| 0| 10| 1| 4| -1| 3| 0| -1| 0.0| (4,[0],[1.0])| 0.0| (5,[0],[1.0])|(10,[0,1,5],[4.0,...|
| 3644| 49| 6| 2| 6| 2| 3| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,2,6],[6.0,...|
| 5777| 44| 5| 2| 5| 2| 3| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,2,6],[5.0,...|
| 6211| 0| 9| 1| 3| -1| 3| 0| 2| 0.0| (4,[0],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,1,6],[3.0,...|
| 6355| 2| 1| 2| 1| 1| 3| 0| 4| 2.0| (4,[2],[1.0])| 3.0| (5,[3],[1.0])|(10,[0,3,8],[1.0,...|
| 6823| 43| 5| 2| 5| 2| 3| 0| 1| 1.0| (4,[1],[1.0])| 4.0| (5,[4],[1.0])|(10,[0,2,9],[5.0,...|
| 6972| 5| 2| 2| 2| 2| 3| 1| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,2,6],[2.0,...|
| 9293| 0| 5| 2| 5| -1| 3| 0| 4| 0.0| (4,[0],[1.0])| 3.0| (5,[3],[1.0])|(10,[0,1,8],[5.0,...|
| 9510| 55| 8| 1| 2| 2| 2| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,2,6],[2.0,...|
| 10122| 33| 4| 2| 4| 2| 3| 0| 2| 1.0| (4,[1],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,2,6],[4.0,...|
| 10549| 0| 4| 2| 4| 2| 3| 0| -1| 1.0| (4,[1],[1.0])| 0.0| (5,[0],[1.0])|(10,[0,2,5],[4.0,...|
| 10812| 0| 4| 2| 4| -1| 2| 0| -1| 0.0| (4,[0],[1.0])| 0.0| (5,[0],[1.0])|(10,[0,1,5],[4.0,...|
| 10912| 0| 4| 2| 4| 2| 3| 0| -1| 1.0| (4,[1],[1.0])| 0.0| (5,[0],[1.0])|(10,[0,2,5],[4.0,...|
| 10996| 0| 5| 2| 5| -1| 3| 0| 4| 0.0| (4,[0],[1.0])| 3.0| (5,[3],[1.0])|(10,[0,1,8],[5.0,...|
| 11256| 8| 2| 2| 2| 1| 3| 0| 3| 2.0| (4,[2],[1.0])| 2.0| (5,[2],[1.0])|(10,[0,3,7],[2.0,...|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4| 2.0| (4,[2],[1.0])| 3.0| (5,[3],[1.0])|(10,[0,3,8],[4.0,...|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+-------------------+-----------------+--------------------+
only showing top 20 rows
feature_df.select("features").show()
顯示結果:
+--------------------+
| features|
+--------------------+
|(10,[0,1,7],[5.0,...|
|(10,[0,3,6],[2.0,...|
|(10,[0,2,5],[2.0,...|
|(10,[0,1,5],[4.0,...|
|(10,[0,1,5],[4.0,...|
|(10,[0,2,6],[6.0,...|
|(10,[0,2,6],[5.0,...|
|(10,[0,1,6],[3.0,...|
|(10,[0,3,8],[1.0,...|
|(10,[0,2,9],[5.0,...|
|(10,[0,2,6],[2.0,...|
|(10,[0,1,8],[5.0,...|
|(10,[0,2,6],[2.0,...|
|(10,[0,2,6],[4.0,...|
|(10,[0,2,5],[4.0,...|
|(10,[0,1,5],[4.0,...|
|(10,[0,2,5],[4.0,...|
|(10,[0,1,8],[5.0,...|
|(10,[0,3,7],[2.0,...|
|(10,[0,3,8],[4.0,...|
+--------------------+
only showing top 20 rows
- 特徵選取
除了前面處理的pvalue_level和new_user_class_level需要作爲特徵以外,(能體現出用戶的購買力特徵),還有:
前面分析的以下幾個分類特徵值個數情況:
- cms_segid: 97
- cms_group_id: 13
- final_gender_code: 2
- age_level: 7
- shopping_level: 3
- occupation: 2
-pvalue_level
-new_user_class_level
-price
根據經驗,以上幾個分類特徵都一定程度能體現用戶在購物方面的特徵,且類別都較少,都可以用來作爲用戶特徵
數據準備:CTR(點擊率)預估的數據準備
特徵選取(Feature Selection)
特徵選擇就是選擇那些靠譜的Feature,去掉冗餘的Feature,對於搜索廣告,Query關鍵詞和廣告的匹配程度很重要;
但對於展示廣告,廣告本身的歷史表現,往往是最重要的Feature。
根據經驗,該數據集中,只有廣告展示位pid對比較重要,且數據不同數據之間的佔比約爲6:4,因此pid可以作爲一個關鍵特徵
nonclk和clk在這裏是作爲目標值,不做爲特徵
Spark中使用熱獨編碼
注意:熱編碼只能對字符串類型的列數據進行處理
StringIndexer:對指定字符串列數據進行特徵處理,如將性別數據“男”、“女”轉化爲0和1
OneHotEncoder:對特徵列數據,進行熱編碼,通常需結合StringIndexer一起使用
Pipeline:讓數據按順序依次被處理,將前一次的處理結果作爲下一次的輸入
from pyspark.ml.feature import OneHotEncoder
from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline
#特徵處理:pid一列表示廣告資源展位,該特徵屬於分類特徵,只有兩類取值,因此考慮進行熱編碼處理即可,分爲是否在資源位1、是否在資源位2 這兩個特徵
# StringIndexer對指定字符串列進行特徵處理爲索引序列
stringindexer = StringIndexer(inputCol='pid', outputCol='pid_feature')
# 對處理出來的特徵索引序列再進行onehot化熱獨編碼
# dropLast=False:默認值,表示不丟棄最後一條
encoder = OneHotEncoder(dropLast=False, inputCol='pid_feature', outputCol='pid_value')
# 利用管道對每一個數據進行熱獨編碼處理
# stages=[stringindexer, encoder]:使用管道順序處理先執行stringindexer索引序列化,再執行onehot化
pipeline = Pipeline(stages=[stringindexer, encoder])
# 構建好Pipeline對象之後然後進行fit擬合
pipeline_model = pipeline.fit(raw_sample_df)
# 使用訓練好的模型進行transform轉換
new_df = pipeline_model.transform(raw_sample_df)
new_df.show()
顯示結果:
pid_feature:代表有2種分類的值的索引值0.0和1.0
pid_value:例如 (2,[1],[1.0]) 中的 2表示有2種分類的數值,[1]表示索引值位置爲1上有值,那麼索引值位置爲1上的值爲[1.0]
+------+----------+---------+-----------+------+---+-----------+-------------+
|userId| timestamp|adgroupId| pid|nonclk|clk|pid_feature| pid_value|
+------+----------+---------+-----------+------+---+-----------+-------------+
|581738|1494137644| 1|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|449818|1494638778| 3|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|298139|1494462593| 9|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|775475|1494561036| 9|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
|555266|1494307136| 11|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|117840|1494036743| 11|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|739815|1494115387| 11|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|530454|1494293746| 13|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
+------+----------+---------+-----------+------+---+-----------+-------------+
only showing top 20 rows
#返回字段pid_value是一個稀疏向量類型數據 pyspark.ml.linalg.SparseVector
from pyspark.ml.linalg import SparseVector
# 參數:維度、索引列表、值列表
print(SparseVector(4, [1, 3], [3.0, 4.0]))
print(SparseVector(4, [1, 3], [3.0, 4.0]).toArray())
顯示結果:
(4,[1,3],[3.0,4.0])
[0. 3. 0. 4.]
print(new_df.select("pid_value").first())
print(new_df.select("pid_value").first().pid_value.toArray())
顯示結果:
Row(pid_value=SparseVector(2, {0: 1.0}))
[1. 0.]
#因爲需要用前面7天的做訓練樣本(20170506-20170512),用第8天的做測試樣本(20170513),所以進行如下操作
#查看最大時間。ascending=False:降序排列。
new_df.sort("timestamp", ascending=False).show()
顯示結果:
timestamp=1494691186:即爲第8天的最後1秒,那麼1494691186-24*60*60(第8天的最後1秒減去1天)等於第7天的最後1秒
+------+----------+---------+-----------+------+---+-----------+-------------+
|userId| timestamp|adgroupId| pid|nonclk|clk|pid_feature| pid_value|
+------+----------+---------+-----------+------+---+-----------+-------------+
|177002|1494691186| 593001|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|839493|1494691183| 561681|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|704223|1494691183| 624504|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
|839493|1494691183| 582235|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|704223|1494691183| 675674|430539_1007| 1| 0| 1.0|(2,[1],[1.0])|
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
|322244|1494691179| 820018|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
|322244|1494691179| 735220|430548_1007| 1| 0| 0.0|(2,[0],[1.0])|
+------+----------+---------+-----------+------+---+-----------+-------------+
only showing top 20 rows
# 本樣本數據集共計8天數據
# 前七天爲訓練數據、最後一天爲測試數據
from datetime import datetime
datetime.fromtimestamp(1494691186)
#timestamp=1494691186:即爲第8天的最後1秒,那麼1494691186-24*60*60(第8天的最後1秒減去1天)等於第7天的最後1秒
print("該時間之前的數據爲訓練樣本,該時間以後的數據爲測試樣本:", datetime.fromtimestamp(1494691186-24*60*60))
顯示結果: 該時間之前的數據爲訓練樣本,該時間以後的數據爲測試樣本: 2017-05-12 23:59:46
# 訓練樣本:
train_sample = raw_sample_df.filter(raw_sample_df.timestamp<=(1494691186-24*60*60))
print("訓練樣本個數:")
print(train_sample.count())
# 測試樣本
test_sample = raw_sample_df.filter(raw_sample_df.timestamp>(1494691186-24*60*60))
print("測試樣本個數:")
print(test_sample.count())
# 注意:還需要加入廣告基本特徵和用戶基本特徵才能做程一份完整的樣本數據集
顯示結果:
訓練樣本個數:
23249291
測試樣本個數:
3308670
# 注意:由於本數據集中存在NULL字樣的數據,無法直接設置schema,只能先將NULL類型的數據處理掉,然後進行類型轉換
from pyspark.sql.types import StructType, StructField, IntegerType, FloatType
# 替換掉NULL字符串,"NULL"替換爲"-1"
df = df.replace("NULL", "-1")
# 打印df結構信息
df.printSchema()
# 更改df表結構:更改列類型和列名稱
# 比如 "adgroup_id"字段類型cast轉換爲IntegerType(),"adgroup_id"字段名替換爲"adgroupId"
ad_feature_df = df.\
withColumn("adgroup_id", df.adgroup_id.cast(IntegerType())).withColumnRenamed("adgroup_id", "adgroupId").\
withColumn("cate_id", df.cate_id.cast(IntegerType())).withColumnRenamed("cate_id", "cateId").\
withColumn("campaign_id", df.campaign_id.cast(IntegerType())).withColumnRenamed("campaign_id", "campaignId").\
withColumn("customer", df.customer.cast(IntegerType())).withColumnRenamed("customer", "customerId").\
withColumn("brand", df.brand.cast(IntegerType())).withColumnRenamed("brand", "brandId").\
withColumn("price", df.price.cast(FloatType()))
ad_feature_df.printSchema()
ad_feature_df.show()
特徵選擇
cateId:脫敏過的商品類目ID;
campaignId:脫敏過的廣告計劃ID;
customerId:脫敏過的廣告主ID;
brandId:脫敏過的品牌ID;
以上四個特徵均屬於分類特徵,但由於分類值個數均過於龐大,如果去做熱獨編碼處理,會導致數據過於稀疏 且當前我們缺少對這些特徵更加具體的信息,
(如商品類目具體信息、品牌具體信息等),從而無法對這些特徵的數據做聚類、降維處理 因此這裏不選取它們作爲特徵。
而只選取price作爲特徵數據,因爲價格本身是一個統計類型連續數值型數據,且能很好的體現廣告的價值屬性特徵,通常也不需要做其他處理(離散化、歸一化、標準化等),
所以這裏直接將當做特徵數據來使用。
隨機森林對缺失值進行處理
缺失值處理
注意,一般情況下:
缺失率低於10%:可直接進行相應的填充,如默認值、均值、算法擬合等等;
高於10%:往往會考慮捨棄該特徵
特徵處理,如1維轉多維
但根據我們的經驗,我們的廣告推薦其實和用戶的消費水平、用戶所在城市等級都有比較大的關聯,
因此在這裏pvalue_level、new_user_class_level都是比較重要的特徵,我們不考慮捨棄。
缺失值填充方案1:隨機森林:分類算法
使用隨機森林對空值做預測,適用於缺失值比例比較低的情況下,一般低於10%。
那麼便可以把非空的字段列[cms_segid、r.cms_group_id、final_gender_code、age_level、shopping_level、occupation]作爲特徵值,
把包含空值的pvalue_level字段列值作爲目標值,然後通過隨機森林分類算法結合特徵值對pvalue_level字段列值進行預測。
但產生了大量人爲構建的數據,一定程度上增加了數據的噪音。
缺失值填充方案2:低維轉高維方式
把變量映射到高維空間:比如pvalue_level這一字段列數據包含有null空值,並且pvalue_level字段列值還包含3種類型的值,即[1、2、3],
那麼再增加null缺失值也作爲一種新的數據分類值添加進去變成[1、2、3、null]的4維數據,也即是變成是否1、是否2、是否3、是否null的4維數據,
這樣保證了所有原始數據不變,同時能提高精確度,但這樣會導致數據變得比較稀疏,如果樣本量很小,反而會導致樣本效果較差,因此也不能濫用。
總結:
pvalue_level的空值情況: 575917 空值佔比:54.24%
new_user_class_level的空值情況: 344920 空值佔比:32.49%
下面開始對pvalue_level和new_user_class_level進行隨機森林預測,但是可以發現由於這兩個字段的缺失過多,所以預測出來的值已經大大失真,
但如果缺失率在10%以下,這種方法是比較有效的一種。
Labeled point(標籤點)
標籤點是與標籤/響應相關聯的密集或稀疏的局部矢量。在MLlib中,標記點用於監督學習算法。
我們使用double來存儲標籤,因此我們可以在迴歸和分類中使用標記點。
對於二分類情況,目標值應爲0(負)或1(正)。對於多分類,標籤應該是從零開始的類索引:0, 1, 2, …。
from pyspark.mllib.linalg import SparseVector
from pyspark.mllib.regression import LabeledPoint
"""
LabeledPoint(目標值, [特徵值])
稠密向量(矩陣)“LabeledPoint(1.0, [1.0, 0.0, 3.0])” 和 稀疏向量(矩陣)“LabeledPoint(0.0, SparseVector(3, [0, 2], [1.0, 3.0]))”
SparseVector(3, [0, 2], [1.0, 3.0]) 實際指的就是 [1.0, 0.0, 3.0]
"""
# Labeled point實際就是目標值和特徵值的組合。
# 創建一個帶有正標籤和密集特徵向量的標籤點(Labeled point)。
# 3個特徵值:[1.0, 0.0, 3.0],相當於有x1/x2/x3。那麼這3個特徵值對應目標值1.0,相當於y。
pos = LabeledPoint(1.0, [1.0, 0.0, 3.0]) #稠密向量(矩陣)。[1.0, 0.0, 3.0] 相當於 SparseVector(3, [0, 2], [1.0, 3.0])。
# 創建一個帶有負標籤和稀疏特徵向量的標籤點。
# SparseVector(3, [0, 2], [1.0, 3.0]):稀疏矩陣,第1個參數表示有3種數值,第2個參數表示有第0位和第2位上都有值,第3個參數表示第0位上的值爲1.0,第2位上的值爲3.0
neg = LabeledPoint(0.0, SparseVector(3, [0, 2], [1.0, 3.0])) #稀疏向量(矩陣)。SparseVector(3, [0, 2], [1.0, 3.0]) 實際指的就是 [1.0, 0.0, 3.0]。
缺失值填充方案1:隨機森林:分類算法
使用隨機森林對空值做預測,適用於缺失值比例比較低的情況下,一般低於10%。
那麼便可以把非空的字段列[cms_segid、r.cms_group_id、final_gender_code、age_level、shopping_level、occupation]作爲特徵值,
把包含空值的pvalue_level字段列值作爲目標值,然後通過隨機森林分類算法結合特徵值對pvalue_level字段列值進行預測。
但產生了大量人爲構建的數據,一定程度上增加了數據的噪音。
pvalue_level的空值情況: 575917 空值佔比:54.24%
下面開始對pvalue_level進行隨機森林預測,但是可以發現由於這兩個字段的缺失過多,所以預測出來的值已經大大失真,
但如果缺失率在10%以下,這種方法是比較有效的一種。
from pyspark.mllib.regression import LabeledPoint
# 剔除掉缺失值數據,將餘下的數據作爲訓練數據
# user_profile_df.dropna(subset=["pvalue_level"]): 將pvalue_level中的空值所在行數據剔除後的數據,作爲訓練樣本。
# user_profile_df.dropna(subset=["目標值"]) 也即是作爲目標值pvalue_level字段列中空值所在的這一行數據都會被丟棄。
train_data = user_profile_df.dropna(subset=["pvalue_level"]).rdd.map(
lambda r:LabeledPoint(r.pvalue_level-1, [r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation])
)
"""
1.注意隨機森林輸入數據時,由於label的分類數是從0開始的,但pvalue_level字段列中由於把null值刪除掉了,pvalue_level字段列中目前只存在是3種值(1、2、3),
因此作爲目標值的pvalue_level字段列中的每個值分別進行-1變成0、1、2。那麼最終自然得出預測值後,需要對應進行+1才能還原回來。
2.LabeledPoint(目標值, [特徵值]):使用非空的字段列[cms_segid、r.cms_group_id、final_gender_code、age_level、shopping_level、occupation]作爲特徵值,
r.pvalue_level-1作爲目標值,也就是說使用非空的字段列(不含有空值的列)作爲特徵值來預測目標值。
3.下面是特徵值中每個特徵對應的特徵值的類別個數
cms_segid: 97
cms_group_id: 13
final_gender_code: 2
age_level: 7
shopping_level: 3
occupation: 2
"""
# 隨機森林:pyspark.mllib.tree.RandomForest
from pyspark.mllib.tree import RandomForest
# trainClassifier訓練分類模型
# 參數1 訓練的數據,LabeledPoint(目標值, [特徵值])數據
# 參數2 目標值的分類個數,由於pvalue_level字段列中目前pvalue_level-1之後只存在3種值(0、1、2),因此目標值的分類個數爲3
# 參數3 特徵中是否包含分類的特徵。比如 {0:97,1:13,2:2,3:7} 中的0:97 表示的是 第1個特徵cms_segid對應的特徵值的類別個數爲97。而使用{}表示自動識別。
# 參數4 隨機森林中樹的棵數,5代表5棵決策樹分別進行決策得出衆數,隨機森林是一個包含多個決策樹的分類器,並且其輸出的類別是由個別樹輸出的類別的衆數而定。
model = RandomForest.trainClassifier(train_data, 3, {}, 5) #此處使用{}表示自動識別
# 隨機森林模型:pyspark.mllib.tree.RandomForestModel
# 隨機森林預測單個目標值:predict([若干個特徵列的特徵值]),即[cms_segid,cms_group_id,final_gender_code,age_level,shopping_level,occupation]這些特徵列的特徵值
# 注意用法:https://spark.apache.org/docs/latest/api/python/pyspark.mllib.html?highlight=tree%20random#pyspark.mllib.tree.RandomForestModel.predict
model.predict([0.0, 4.0 ,2.0 , 4.0, 1.0, 0.0])
#顯示結果: 1.0
#篩選出缺失值條目。
# 把user_profile_df表中爲na(空值)都替換爲-1,然後僅獲取出pvalue_level字段列中值爲-1的整行數據。
pl_na_df = user_profile_df.na.fill(-1).where("pvalue_level=-1")
pl_na_df.show(10)
def row(r):
return r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation
# 因此這裏經過map函數處理,將每一行數據轉換爲普通的列表數據
# 轉換爲普通的rdd類型
# 把包含pvalue_level字段列中值爲-1的每行數據進行遍歷執行row函數,
# 然後僅返回6個特徵列[cms_segid、r.cms_group_id、final_gender_code、age_level、shopping_level、occupation]的值。
rdd = pl_na_df.rdd.map(row)
# 這裏注意predict參數,如果是預測多個,那麼參數必須是直接有列表構成的rdd參數,而不能是dataframe.rdd類型
# 預測pvalue_level字段列中空值應爲多少:predict輸入的是訓練模型時輸入的6個特徵列的值
predicts = model.predict(rdd)
# 查看前20條
print(predicts.take(20))
print("預測值總數", predicts.count())
# 轉換爲pandas dataframe
# 這裏數據量比較小,直接轉換爲pandas dataframe來處理,因爲方便,但注意如果數據量較大不推薦,因爲這樣會把全部數據加載到內存中
temp = predicts.map(lambda x:int(x)).collect()
pdf = pl_na_df.toPandas()
import numpy as np
# 在pandas df的基礎上直接替換掉列數據
pdf["pvalue_level"] = np.array(temp) + 1 # 因爲在模型訓練中pvalue_level字段列中的每個值都分別進行了-1,因此還原預測值時還需要每個進行+1
pdf
# 注意:unionAll的使用,兩個df的表結構必須完全一樣
#與非缺失數據進行拼接,完成pvalue_level的缺失值預測
new_user_profile_df = user_profile_df.dropna(subset=["pvalue_level"]).unionAll(spark.createDataFrame(pdf, schema=schema))
new_user_profile_df.show()
new_user_class_level的空值情況: 344920 空值佔比:32.49%
下面開始對new_user_class_level進行隨機森林預測,但是可以發現由於這兩個字段的缺失過多,所以預測出來的值已經大大失真,
但如果缺失率在10%以下,這種方法是比較有效的一種。
#利用隨機森林對new_user_class_level的缺失值進行預測
from pyspark.mllib.regression import LabeledPoint
# 選出new_user_class_level全部的
train_data2 = user_profile_df.dropna(subset=["new_user_class_level"]).rdd.map(
lambda r:LabeledPoint(r.new_user_class_level - 1, [r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation])
)
from pyspark.mllib.tree import RandomForest
model2 = RandomForest.trainClassifier(train_data2, 4, {}, 5)
model2.predict([0.0, 4.0 ,2.0 , 4.0, 1.0, 0.0])
# 預測值實際應該爲2
# 顯示結果: 1.0
nul_na_df = user_profile_df.na.fill(-1).where("new_user_class_level=-1")
nul_na_df.show(10)
def row(r):
return r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation
rdd2 = nul_na_df.rdd.map(row)
predicts2 = model.predict(rdd2)
predicts2.take(20)
低維轉高維方式對缺失值進行處理
缺失值填充方案2:低維轉高維方式
把變量映射到高維空間:比如pvalue_level這一字段列數據包含有null空值,並且pvalue_level字段列值還包含3種類型的值,即[1、2、3],
那麼再增加null缺失值也作爲一種新的數據分類值添加進去變成[1、2、3、null]的4維數據,也即是變成是否1、是否2、是否3、是否null的4維數據,
這樣保證了所有原始數據不變,同時能提高精確度,但這樣會導致數據變得比較稀疏,如果樣本量很小,反而會導致樣本效果較差,因此也不能濫用。
#低維轉高維方式
#我們接下來採用將變量映射到高維空間的方法來處理缺失值數據,即將缺失值也當做一個單獨的特徵來對待,保證數據的原始性
#由於該思想正好和熱獨編碼實現方法一樣,因此這裏直接使用熱獨編碼方式處理數據
from pyspark.ml.feature import OneHotEncoder
from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline
# 使用熱獨編碼轉換pvalue_level的一維數據爲多維,其中缺失值單獨作爲一個新的特徵值
# 需要先將缺失值全部替換爲數值-1,與原有特徵一起處理
from pyspark.sql.types import StringType
# 把空值全部替換爲數值-1
user_profile_df = user_profile_df.na.fill(-1)
user_profile_df.show()
# 熱獨編碼時,必須先將待處理字段轉爲字符串類型纔可處理
# 把包含空值的pvalue_level和new_user_class_level字段列的類型都轉換爲string類型
user_profile_df = user_profile_df.withColumn("pvalue_level", user_profile_df.pvalue_level.cast(StringType()))\
.withColumn("new_user_class_level", user_profile_df.new_user_class_level.cast(StringType()))
user_profile_df.printSchema()
# 對pvalue_level進行熱獨編碼,求值
# StringIndexer對指定字符串列進行特徵處理爲索引序列
stringindexer = StringIndexer(inputCol='pvalue_level', outputCol='pl_onehot_feature')
# 對處理出來的特徵索引序列再進行onehot化熱獨編碼
# dropLast=False:默認值,表示不丟棄最後一條
encoder = OneHotEncoder(dropLast=False, inputCol='pl_onehot_feature', outputCol='pl_onehot_value')
# 利用管道對每一個數據進行熱獨編碼處理
# stages=[stringindexer, encoder]:使用管道順序處理先執行stringindexer索引序列化,再執行onehot化
pipeline = Pipeline(stages=[stringindexer, encoder])
# 構建好Pipeline對象之後然後進行fit擬合
pipeline_fit = pipeline.fit(user_profile_df)
# 使用訓練好的模型進行transform轉換
user_profile_df2 = pipeline_fit.transform(user_profile_df)
# pl_onehot_value列的值爲稀疏向量,存儲熱獨編碼的結果
user_profile_df2.printSchema()
user_profile_df2.show()
pl_onehot_feature:代表有4種分類的值的索引值0.0、1.0、2.0、3.0
pl_onehot_value:例如 (4,[0],[1.0]) 中的 4表示有4種分類的數值,[0]表示索引值位置爲0上有值,那麼索引值位置爲0上的值爲[1.0]
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+
|userId|cms_segid|cms_group_id|final_gender_code|age_level|pvalue_level|shopping_level|occupation|new_user_class_level|pl_onehot_feature|pl_onehot_value|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+
| 234| 0| 5| 2| 5| -1| 3| 0| 3| 0.0| (4,[0],[1.0])|
| 523| 5| 2| 2| 2| 1| 3| 1| 2| 2.0| (4,[2],[1.0])|
。。。。。。。。。。。。。。。。。。。。。。。。。。。。
| 11256| 8| 2| 2| 2| 1| 3| 0| 3| 2.0| (4,[2],[1.0])|
| 11310| 31| 4| 2| 4| 1| 3| 0| 4| 2.0| (4,[2],[1.0])|
+------+---------+------------+-----------------+---------+------------+--------------+----------+--------------------+-----------------+---------------+
only showing top 20 rows
#使用熱編碼轉換new_user_class_level的一維數據爲多維
stringindexer = StringIndexer(inputCol='new_user_class_level', outputCol='nucl_onehot_feature')
encoder = OneHotEncoder(dropLast=False, inputCol='nucl_onehot_feature', outputCol='nucl_onehot_value')
pipeline = Pipeline(stages=[stringindexer, encoder])
pipeline_fit = pipeline.fit(user_profile_df2)
user_profile_df3 = pipeline_fit.transform(user_profile_df2)
user_profile_df3.show()
nucl_onehot_feature:代表有5種分類的值的索引值0.0、1.0、2.0、3.0、4.0
nucl_onehot_value:例如 (5,[2],[1.0]) 中的 5表示有5種分類的數值,[2]表示索引值位置爲2上有值,那麼索引值位置爲2上的值爲[1.0]
+------+...+---------+------------+...+--------------------+-----------------+---------------+-------------------+-----------------+
|userId|...|age_level|pvalue_level|...|new_user_class_level|pl_onehot_feature|pl_onehot_value|nucl_onehot_feature|nucl_onehot_value|
+------+...+---------+------------+...+--------------------+-----------------+---------------+-------------------+-----------------+
| 234|...| 5| -1|...| 3| 0.0| (4,[0],[1.0])| 2.0| (5,[2],[1.0])|
| 523|...| 2| 1|...| 2| 2.0| (4,[2],[1.0])| 1.0| (5,[1],[1.0])|
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
| 11256|...| 2| 1|...| 3| 2.0| (4,[2],[1.0])| 2.0| (5,[2],[1.0])|
| 11310|...| 4| 1|...| 4| 2.0| (4,[2],[1.0])| 3.0| (5,[3],[1.0])|
+------+...+---------+------------+...+--------------------+-----------------+---------------+-------------------+-----------------+
only showing top 20 rows
#用戶特徵合併
from pyspark.ml.feature import VectorAssembler
# 把"age_level"、"pl_onehot_value"、"nucl_onehot_value"3列數據合併爲"features"一列
feature_df = VectorAssembler().setInputCols(["age_level", "pl_onehot_value", "nucl_onehot_value"]).setOutputCol("features").transform(user_profile_df3)
feature_df.show()
因爲把"age_level"、"pl_onehot_value"、"nucl_onehot_value" 3列數據合併爲了"features"一列。
"age_level"有1個類型的值,"pl_onehot_value"有4個類型的值,"nucl_onehot_value"有5個類型的值。
(10,[0,1,7],[5.0, 1.0, 1.0]):
10表示有1+4+5=10種分類的值。
[0,1,7]中的0表示索引位置0上有"age_level"值爲5.0;1表示索引位置1上有"pl_onehot_value"值爲1.0,7表示索引位置7上有"nucl_onehot_value"值爲1.0。
[0,1,7]代表one-hot向量[1, 1,0,0,0, 0,1,0,0,0]
+------+...+---------+------------+...+--------------------+-----------------+---------------+-------------------+-----------------+--------------------+
|userId|...|age_level|pvalue_level|...|new_user_class_level|pl_onehot_feature|pl_onehot_value|nucl_onehot_feature|nucl_onehot_value| features|
+------+...+---------+------------+...+--------------------+-----------------+---------------+-------------------+-----------------+--------------------+
| 234|...| 5| -1|...| 3| 0.0| (4,[0],[1.0])| 2.0| (5,[2],[1.0])|(10,[0,1,7],[5.0,...|
| 523|...| 2| 1|...| 2| 2.0| (4,[2],[1.0])| 1.0| (5,[1],[1.0])|(10,[0,3,6],[2.0,...|
| 612|...| 2| 2|...| -1| 1.0| (4,[1],[1.0])| 0.0| (5,[0],[1.0])|(10,[0,2,5],[2.0,...|
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
+------+...+---------+------------+...+--------------------+-----------------+---------------+-------------------+-----------------+--------------------+
only showing top 20 rows
特徵選取
除了前面處理的pvalue_level和new_user_class_level需要作爲特徵以外,(能體現出用戶的購買力特徵),還有:
前面分析的以下幾個分類特徵值個數情況:
- cms_segid: 97 #如果把這個特徵進行onehot的話,稀疏矩陣就有點太稀疏了
- cms_group_id: 13
- final_gender_code: 2
- age_level: 7
- shopping_level: 3
- occupation: 2
-pvalue_level
-new_user_class_level
-price
根據經驗,以上幾個分類特徵都一定程度能體現用戶在購物方面的特徵,且類別都較少,都可以用來作爲用戶特徵