寫在前面: 博主是一名軟件工程系大數據應用開發專業大二的學生,暱稱來源於《愛麗絲夢遊仙境》中的Alice和自己的暱稱。作爲一名互聯網小白,
寫博客一方面是爲了記錄自己的學習歷程,一方面是希望能夠幫助到很多和自己一樣處於起步階段的萌新
。由於水平有限,博客中難免會有一些錯誤,有紕漏之處懇請各位大佬不吝賜教!個人小站:http://alices.ibilibili.xyz/ , 博客主頁:https://alice.blog.csdn.net/
儘管當前水平可能不及各位大佬,但我還是希望自己能夠做得更好,因爲一天的生活就是一生的縮影
。我希望在最美的年華,做最好的自己
!
不知不覺,這已經是關於挖掘型標籤開發的第三篇博客了。前面兩篇已經爲大家分別介紹了基於RFE和RFM模型的標籤開發過程。感興趣的朋友可以待到文末,乘坐博客直通車直達,一飽眼福。
本篇博客,我們來學習的是關於另一種模型——價格敏感度模型PSM
的挖掘型標籤開發過程。有意思的是,這個模型牽扯出了一個發生在我們身邊,卻鮮有人去探討的問題——大數據殺熟…
PSM模型引入
在正式講之前,我們先來看幾篇關於大數據殺熟的報道。
大數據殺熟
【http://www.sohu.com/a/240094402_374686】
【http://www.qianjia.com/zhike/html/2019-10/11_13453.html】
【https://cloud.tencent.com/developer/article/1450512】
以第一篇報道爲例,相信大家對於各行各業無良商家們的大數據殺熟手段都或多或少有所瞭解。
尤其是一些購票,訂酒店的軟件,針對不同的客戶羣體,相同的服務會給出不同的價格,讓人着實氣憤!
不久前,北京市消費者協會發布了“大數據殺熟”問題調查結果。結論顯示被人們普遍認爲存在的“大數據殺熟”現象,在實際體驗調查中問題並不明顯,由於其存在的複雜性和隱蔽性,維權舉證存在困難。
“大數據只是一種手段和工具,‘殺熟’歸根到底是企業的一種運營手段。”一業內人士解釋道。衆所周知,新用戶的粘性比較低,對價格更爲敏感,如果平臺希望能夠將其很好的留存,成爲忠實用戶,就會提供更多的優惠措施來留住他。相反,老用戶則是平臺的忠實用戶,對於平臺的信任度高,所以也就不會再有貨比三家的操作了。
作爲一名技術人,我們雖然干預不到商家們的交易,但是我們可以摸清這裏面所涉及到的"套路"。這裏面涉及到了一種價格敏感度模型,也就是我們下面要講的PSM
…
PSM模型在網遊中的運用
PSM(Price Sensitivity Measurement)模型是在70年代由Van Westendrop所創建,其目的在於衡量目標用戶對不同價格的滿意及接受程度,瞭解其認爲合適的產品價格,從而得到產品價格的可接受範圍。PSM的定價是從消費者接受程度的角度來進行的,既考慮了消費者的主觀意願,又兼顧了企業追求最大利益的需求。此外,其價格測試過程完全基於所取購買對象的自然反應,沒有涉及到任何競爭對手的信息。雖然缺少競品信息是PSM的缺陷所在,由於每個網絡遊戲均自成一個虛擬的社會體系,一般來說,其中每個道具或服務的銷售均沒有競品(除非開發組自己開發了類似的道具或服務,產生了內部競爭),從這個角度來說,PSM模型比較適合用於網遊中的道具或服務的定價。此外,該模型簡潔明瞭,操作簡單,使用非常方便。
PSM模型實施具體步驟
第一步:通過定性研究,設計出能夠涵蓋產品可能的價格區間的價格梯度表。該步驟通常對某一產品或服務追問被訪者4個問題,並據此獲得價格梯度表。梯度表的價格範圍要涵蓋所有可能的價格點,最低和最高價格一般要求低於或高出可能的市場價格的三倍以上。
- 便宜的價格:對您而言什麼價格該道具/服務是很划算,肯定會購買的?
- 太便宜的價格:低到什麼價格,您覺得該道具/服務會因爲大家都可以隨便用,而對自己失去吸引力(或對遊戲造成一些不良影響)?
- 貴的價格:您覺得“有點高,但自己能接受”的價格是多少?
- 太貴的價格:價格高到什麼程度,您肯定會放棄購買?
第二步:取一定數量有代表性的樣本,被訪者在價格梯度表上做出四項選擇:有點低但可以接受的價格,太低而不會接受的價格,有點高但可以接受的價格,太高而不會接受的價格。
第三步:對所獲得的樣本數據繪製累計百分比曲線圖,四條曲線的交點得出產品的合適價格區間以及最優定價點和次優定價點。
如下圖, 對“便宜”和“太便宜”向下累計百分數(因爲價格越低消費者越覺得便宜,即認爲某價格便宜的消費者也會認爲低於此價格的價格便宜),“貴”和“太貴”向上累計百分數(因爲價格越高消費者越覺得貴,即認爲某價格貴的消費者也會認爲高於此價格的價格貴),能夠得到四條累計百分比曲線。
“太便宜”和“貴”的交點意味着此價格能夠讓最多的人覺得“不會便宜到影響購買意願,即使可能有點貴也是能夠接受的”,“便宜”和“太貴”的交點意味着此價格能夠讓最多的人覺得“不會貴到不能接受,還是挺划算的”,因此這兩個交點分別爲價格區間的下限和上限。低於前者,消費者會因爲擔心“過於大衆不能體現優越感,或會給遊戲帶來不好影響(如遊戲平衡性)”而不願購買;高於後者,消費者會認爲價錢太高而不能接受。
一般來說,“太便宜”和“太貴”的交點作爲最優價格點,因爲在此處覺得“不過於便宜也不過於昂貴”的消費者最多。但是也有人認爲“便宜”和“貴”的交點是最優價格,因爲該交點取得了“划算,肯定會買”及“貴,但能接受”的平衡點,是能讓最多消費者滿意的價格。
PSM模型的缺陷及解決
- 存在的問題
第一:只考慮到了消費者的接受率,忽視了消費者的購買能力,即只追求最大的目標人羣數。但事實上,即使消費者覺得價格合理,受限於購買力等因素,也無法購買。
第二:研究中消費者可能出於各種因素(比如讓價格更低能讓自己收益,出於面子問題而擡高自己能接受的價格等)有意或無意地擡高或壓低其接受的價格。由於消費者知道虛擬世界中的產品(道具或服務)沒有成本,其壓低價格的可能性較高。
第三:沒有考慮價格變化導致的購買意願(銷量)變化。
- 解決方案
第一:爲了避免購買力的影響, 問卷或訪談研究中要強調“定這個價格,以自己目前的情況是否會購買”,而非僅僅去客觀判斷該產品值多少錢。
第二:爲了解決玩家擡高或壓低價格的問題,可以增大樣本量,預期隨機誤差可以相互抵消。
第三:僅僅從曲線獲得最優價格,受到玩家壓低或擡高價格的影響較大。由於該誤差可能是系統誤差,對此,可以用所獲得的價格區間設計不同的價格方案,然後設計組間實驗設計,每個參與研究的消費者只接觸其中一種或幾種價格方案,並對該價格方案下是否購買及購買數量做出決策,通過計算那種價格方案下玩家消費金錢量最高來分析出最佳價格方案。如下表。
第四:通過前一條中提到的組間實驗設計,可以計算出不同價格下玩家購買意願的變化,從而得知價格調整會對整體收益帶來的影響。此外,價格接受比例還可以作爲消費者對某價格滿意度的指標,用於計算某價格下企業該產品的良性收益。
注意,我們的上述對策部分基於統計學和實驗心理學理論,部分基於我們工作中的實踐,歡迎大家討論和優化。
以上都是基於大量理論後得出的結果,下面我們來結合用戶畫像的項目,來“模擬”商家給不同價格敏感度的用戶打上不同標籤的過程。
用戶畫像
首先我們在web頁面,先添加上我們本次需要給用戶開發的標籤和標籤值。
查看數據庫
現在有了數據,我們本該就可以直接上手代碼了。但爲了簡便開發,避免重複的步驟,我們先來分析一下業務需求。
業務分析
我們如果要實現根據不同的人給出不同的價格,那麼如何才能確定用戶的價格敏感度( PSM
)?
這裏有一個公式:
psm = 優惠訂單佔比 + 平均優惠金額佔比 + 優惠總金額佔比
是不是剛看有點懵,我們再來細化一下。
優惠訂單佔比 = 優惠次數/總購買次數
平均優惠金額佔比 = 平均優惠金額/平均應收金額
平均優惠金額=優惠總金額/優惠總次數
平均應收金額 = 應收總金額 /訂單總次數
優惠總金額佔比 = 優惠總金額/應收總金額
綜上所述,我們可以將計算PSM的目標進一步變成計算以下四個數據,只要將它們計算出來,PSM就迎刃而解。
優惠次數
總購買次數
優惠總金額
應收總金額
前三個數據,我們可以提供統計型函數計算得出, 而 應收總金額= 優惠金額 + 成交金額 這一點我們明確了之後,就可以很好的上手代碼了。
業務代碼
算上這次,博主已經是第三次開發挖掘型標籤了。所以就不單獨把每一步具體實現的業務拿出來慢慢敘述了。更多的細節已經每步實現的效果已經用註釋的方式貼在代碼中了,有興趣嘗試的朋友,或者想要借鑑結果的朋友可以一睹代碼究竟。
import com.czxy.base.BaseModel
import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.sql._
import org.apache.spark.sql.expressions.UserDefinedFunction
import scala.collection.immutable
/*
* @Author: Alice菌
* @Date: 2020/6/26 11:17
* @Description:
基於PSM模型,對用戶的價格敏感度進行挖掘
*/
object PSMModel extends BaseModel {
override def setAppName: String = "PSMModel"
override def setFourTagId: String = "181"
override def getNewTag(spark: SparkSession, fiveTagDF: DataFrame, hbaseDF: DataFrame): DataFrame = {
// 五級標籤的數據
fiveTagDF.show()
//+---+----+
//| id|rule|
//+---+----+
//|182| 1|
//|183| 2|
//|184| 3|
//|185| 4|
//|186| 5|
//+---+----+
// HBase的數據
hbaseDF.show()
//+---------+--------------------+-----------+---------------+
//| memberId| orderSn|orderAmount|couponCodeValue|
//+---------+--------------------+-----------+---------------+
//| 13823431|gome_792756751164275| 2479.45| 0.00|
//| 4035167|jd_14090106121770839| 2449.00| 0.00|
//| 4035291|jd_14090112394810659| 1099.42| 0.00|
//| 4035041|amazon_7877495617...| 1999.00| 0.00|
// tdonr 優惠訂單佔比(優惠訂單數 / 訂單總數)
// adar 平均優惠金額佔比(平均優惠金額 / 平均每單應收金額)
// tdar 優惠金額佔比(優惠總金額 / 訂單總金額)
//psm = 優惠訂單佔比 + 平均優惠金額佔比 + 優惠總金額佔比
//只需要求取下面的字段,即可獲取到最終的結果
//優惠次數
//總購買次數
//優惠總金額
//應收總金額 = 優惠金額+成交金額
// 引入隱式轉換
import spark.implicits._
//引入java 和scala相互轉換
import scala.collection.JavaConverters._
//引入sparkSQL的內置函數
import org.apache.spark.sql.functions._
// 優惠次數
var preferentialCount:String = "preferential"
// 訂單總數
var orderCount:String = "orderCount"
// 總優惠金額
var couponCodeValue:String = "couponCodeValue"
// 應收總金額
var totalValue:String = "totalValue"
// 優惠次數
var getPreferentialCount:Column= count(
when(col("couponCodeValue") !== 0.00,1)
) as preferentialCount
// 總購買次數
val getOrderCount: Column = count("orderSn") as orderCount
// 優惠總金額
var getCouponCodeValue:Column= sum("couponCodeValue") as couponCodeValue
// 應收總金額
var getTotalValue: Column = (sum("orderAmount") + sum("couponCodeValue") ) as totalValue
// 特徵單詞
val featureStr: String = "feature" // 向量
val predictStr: String = "predict" // 分類
// 進行查詢
val getPSMDF01: DataFrame = hbaseDF.groupBy("memberId")
.agg(getPreferentialCount, getOrderCount, getCouponCodeValue,getTotalValue)
//展示結果
getPSMDF01.show(5)
//+---------+------------+-----------+---------------+------------------+
//| memberId|preferential|orderAmount|couponCodeValue| totalValue|
//+---------+------------+-----------+---------------+------------------+
//| 4033473| 3| 142| 500.0|252430.91999999998|
//| 13822725| 4| 116| 800.0| 180098.34|
//| 13823681| 1| 108| 200.0|169946.09999999998|
//|138230919| 3| 125| 600.0|240661.56999999998|
//| 13823083| 3| 132| 600.0| 234124.17|
//+---------+------------+-----------+---------------+------------------+
// 先設置上我們常用的單詞
// 優惠訂單佔比
var preferentiaOrderlPro:String = "preferentiaOrderlPro"
// 平均優惠金額佔比
var avgPreferentialPro:String = "avgPreferentialPro"
// 優惠金額佔比
val preferentialMoneyPro: String = "preferentialMoneyPro"
/* 獲取到想要的字段結果 */
// 優惠訂單佔比(優惠訂單數 / 訂單總數)
val getPSMDF02: DataFrame = getPSMDF01.select(col("memberId"),col("preferential") / col("orderCount") as preferentiaOrderlPro,
// 平均優惠金額佔比(平均優惠金額 / 平均每單應收金額)
(col("couponCodeValue") / col("preferential")) / (col("totalValue") / col("orderCount")) as avgPreferentialPro,
// 優惠金額佔比(優惠總金額 / 訂單總金額)
col("couponCodeValue") / col("totalValue") as preferentialMoneyPro)
getPSMDF02.show()
//+---------+--------------------+-------------------+--------------------+
//| memberId|preferentiaOrderlPro| avgPreferentialPro|preferentialMoneyPro|
//+---------+--------------------+-------------------+--------------------+
//| 4033473| 0.02112676056338028|0.09375502282631092|0.001980739918865724|
//| 13822725|0.034482758620689655| 0.1288185110423561| 0.00444201762215021|
//| 13823681|0.009259259259259259|0.12709912142732316|0.001176843716919...|
//|138230919| 0.024|0.10388031624658645|0.002493127589918075|
//| 13823083|0.022727272727272728|0.11276067737901643|0.002562742667704919|
//| 13823431| 0.01639344262295082|0.13461458465166434|0.002206796469699...|
//| 4034923|0.009259259259259259|0.12882071966768546|0.001192784441367458|
//| 4033575| 0.032|0.07938713328518321|0.002540388265125...|
//| 13822841| 0.0| null| 0.0|
val getPSMDF03: DataFrame = getPSMDF02.select(col("memberId"),col("preferentiaOrderlPro"),col("avgPreferentialPro"),col("preferentialMoneyPro"),(col("preferentiaOrderlPro")+col("avgPreferentialPro")+col("preferentialMoneyPro")) as "PSM" ).filter('PSM isNotNull)
getPSMDF03.show()
//+---------+--------------------+-------------------+--------------------+-------------------+
//| memberId|preferentiaOrderlPro| avgPreferentialPro|preferentialMoneyPro| PSM|
//+---------+--------------------+-------------------+--------------------+-------------------+
//| 4033473| 0.02112676056338028|0.09375502282631092|0.001980739918865724|0.11686252330855693|
//| 13822725|0.034482758620689655| 0.1288185110423561| 0.00444201762215021|0.16774328728519597|
//| 13823681|0.009259259259259259|0.12709912142732316|0.001176843716919...|0.13753522440350205|
//|138230919| 0.024|0.10388031624658645|0.002493127589918075| 0.1303734438365045|
//| 13823083|0.022727272727272728|0.11276067737901643|0.002562742667704919| 0.1380506927739941|
// 爲了方便K-Means計算,我們將數據轉換成向量
val PSMFeature: DataFrame = new VectorAssembler()
.setInputCols(Array("PSM"))
.setOutputCol(featureStr)
.transform(getPSMDF03)
PSMFeature.show()
//+---------+--------------------+--------------------+--------------------+-------------------+--------------------+
//| memberId|preferentiaOrderlPro| avgPreferentialPro|preferentialMoneyPro| PSM| feature|
//+---------+--------------------+--------------------+--------------------+-------------------+--------------------+
//| 4033473| 0.02112676056338028| 0.09375502282631092|0.001980739918865724|0.11686252330855693|[0.11686252330855...|
//| 13822725|0.034482758620689655| 0.1288185110423561| 0.00444201762215021|0.16774328728519597|[0.16774328728519...|
//| 13823681|0.009259259259259259| 0.12709912142732316|0.001176843716919...|0.13753522440350205|[0.13753522440350...|
//|138230919| 0.024| 0.10388031624658645|0.002493127589918075| 0.1303734438365045|[0.1303734438365045]|
//| 13823083|0.022727272727272728| 0.11276067737901643|0.002562742667704919| 0.1380506927739941|[0.1380506927739941]|
//| 13823431| 0.01639344262295082| 0.13461458465166434|0.002206796469699...|0.15321482374431458|[0.15321482374431...|
//| 4034923|0.009259259259259259| 0.12882071966768546|0.001192784441367458|0.13927276336831218|[0.13927276336831...|
//| 4033575| 0.032| 0.07938713328518321|0.002540388265125...|0.11392752155030907|[0.11392752155030...|
//| 13823153|0.045112781954887216| 0.10559805877421218|0.004763822200340399|0.15547466292943982|[0.15547466292943...|
// 利用KMeans算法,進行數據的分類
val KMeansModel: KMeansModel = new KMeans()
.setK(5) // 設置4類
.setMaxIter(5) // 迭代計算5次
.setFeaturesCol(featureStr) // 設置特徵數據
.setPredictionCol(predictStr) // 計算完畢後的標籤結果
.fit(PSMFeature)
// 將其轉換成DF
val KMeansModelDF: DataFrame = KMeansModel.transform(PSMFeature)
KMeansModelDF.show()
//+---------+--------------------+--------------------+--------------------+-------------------+--------------------+-------+
//| memberId|preferentiaOrderlPro| avgPreferentialPro|preferentialMoneyPro| PSM| feature|predict|
//+---------+--------------------+--------------------+--------------------+-------------------+--------------------+-------+
//| 4033473| 0.02112676056338028| 0.09375502282631092|0.001980739918865724|0.11686252330855693|[0.11686252330855...| 4|
//| 13822725|0.034482758620689655| 0.1288185110423561| 0.00444201762215021|0.16774328728519597|[0.16774328728519...| 4|
//| 13823681|0.009259259259259259| 0.12709912142732316|0.001176843716919...|0.13753522440350205|[0.13753522440350...| 4|
//|138230919| 0.024| 0.10388031624658645|0.002493127589918075| 0.1303734438365045|[0.1303734438365045]| 4|
//| 13823083|0.022727272727272728| 0.11276067737901643|0.002562742667704919| 0.1380506927739941|[0.1380506927739941]| 4|
//| 13823431| 0.01639344262295082| 0.13461458465166434|0.002206796469699...|0.15321482374431458|[0.15321482374431...| 4|
//| 4034923|0.009259259259259259| 0.12882071966768546|0.001192784441367458|0.13927276336831218|[0.13927276336831...| 4|
//| 4033575| 0.032| 0.07938713328518321|0.002540388265125...|0.11392752155030907|[0.11392752155030...| 4|
// 計算用戶的價值
val clusterCentersSum: immutable.IndexedSeq[(Int, Double)] = for(i <- KMeansModel.clusterCenters.indices) yield (i,KMeansModel.clusterCenters(i).toArray.sum)
val clusterCentersSumSort: immutable.IndexedSeq[(Int, Double)] = clusterCentersSum.sortBy(_._2).reverse
clusterCentersSumSort.foreach(println)
//(3,0.5563226557645843)
//(1,0.31754213552513205)
//(2,0.21020766974136296)
//(4,0.131618637271183)
//(0,0.08361272609460167)
// 獲取到每種分類以及其對應的索引
val clusterCenterIndex: immutable.IndexedSeq[(Int, Int)] = for(a <- clusterCentersSumSort.indices) yield (clusterCentersSumSort(a)._1,a)
clusterCenterIndex.foreach(println)
//(3,0)
//(1,1)
//(2,2)
//(4,3)
//(0,4)
// 類別的價值從高到低,角標依次展示
// 將其轉換成DF
val clusterCenterIndexDF: DataFrame = clusterCenterIndex.toDF(predictStr,"index")
clusterCenterIndexDF.show()
//+-------+-----+
//|predict|index|
//+-------+-----+
//| 3| 0|
//| 1| 1|
//| 2| 2|
//| 4| 3|
//| 0| 4|
//+-------+-----+
val JoinDF: DataFrame = fiveTagDF.join(clusterCenterIndexDF,fiveTagDF.col("rule") === clusterCenterIndexDF.col("index"))
JoinDF.show()
//+---+----+-------+-----+
//| id|rule|predict|index|
//+---+----+-------+-----+
//|182| 0| 3| 0|
//|183| 1| 1| 1|
//|184| 2| 2| 2|
//|185| 3| 4| 3|
//|186| 4| 0| 4|
//+---+----+-------+-----+
val JoinDFS: DataFrame = JoinDF.select(predictStr,"id")
//fiveTageList
val fiveTageMap: Map[String, String] = JoinDFS.as[(String,String)].collect().toMap
// 獲得數據標籤(udf)
// 需要自定義UDF函數
val getRFMTags: UserDefinedFunction = udf((featureOut: String) => {
fiveTageMap.get(featureOut)
})
val PriceSensitiveTag: DataFrame = KMeansModelDF.select('memberId .as("userId"),getRFMTags('predict).as("tagsId"))
PriceSensitiveTag.show()
//+---------+------+
//| userId|tagsId|
//+---------+------+
//| 4033473| 185|
//| 13822725| 185|
//| 13823681| 185|
//|138230919| 185|
PriceSensitiveTag
}
def main(args: Array[String]): Unit = {
exec()
}
}
代碼已經爲大家展示完了,如果是有編程功底和大數據基礎的朋友,理解起來一定不會感到困難。而看不懂的朋友也不用擔心,因爲其背後具體的邏輯,博主已經將其說明的很清楚了。如果對代碼過程中,有任何疑惑,不明白的地方,歡迎與我聯繫。
檢驗成功
程序運行完了,我們來檢驗一下結果。
打開我們的Hbase數據庫,scan "test",{LIMIT => 10}
查看我們存放用戶標籤信息的test數據庫中的10條信息。
我們可以看到,已成功將用戶的價格敏感度標籤打入到了標籤系統的數據庫。換做實際企業的業務場景中,要對不同的用戶採用不同的價格策略就可以從這裏"下手"…
結語
大數據是“雙刃劍”,要想用好,離不開政府的管控。有業內人士表示,我國適用價格歧視的法律主要有價格法和反壟斷法。但是,這兩部法律與基於大數據分析的網絡時代商業不夠匹配。互聯網行業很多頭部企業,也具有天然壟斷性。顯然,面對出現的新問題,我們有理由拿出新舉措,完善監管方式,並推動形成相應的制度建設。
本篇博客的內容到這裏就結束了,對前面兩篇挖掘型算法標籤開發感興趣的朋友可以👇
基於RFE模型的挖掘型標籤開發
基於RFM模型的挖掘型標籤開發
如果以上過程中出現了任何的紕漏錯誤,煩請大佬們指正😅
受益的朋友或對大數據技術感興趣的夥伴記得點贊關注支持一波🙏
希望我們都能在學習的道路上越走越遠😉