java漏斗代码_集算示例:10 行代码解决漏斗转换计算
銷售過程是一個多環節的過程,哪個步驟有了過大瑕疵,都會導致業績急劇下滑。而診斷出哪個步驟有瑕疵,除了無形的經驗,還有量化的診斷方式,就是今天要討論的主角:轉化漏斗模型。
示例數據
為了詳細討論這個漏斗的實現過程,我們舉一個具體的網上商城的例子,被分析的數據也不復雜,只有一個事件表:
用戶 ID:用戶編碼
事件 ID:事件編碼
事件屬性:不同事件有不同屬性;json 格式,{“content”:”computer”,”page_num”:1}
時間:事件發生的時間
事件類型和事件屬性如下所示
需求定義
目標結果是獲得某個操作流程在每個操作的客戶流失率,如下圖,登錄的用戶有 20000 人;其中有 12000 人進行了:登錄 -> 搜索商品;其中有 8000 人進行了:登錄 -> 搜索商品 -> 查看商品。如下圖所示:
每個事件后都可能流失一些用戶,整個圖示就象一個漏斗形狀,所以被稱為漏斗轉換分析。
我們來研究這個運算的一些需求點:
針對同一個用戶,我們觀察下面這兩組數據,因為事件順序關系,我們認為 1000001 用戶只發生了登錄行為,而 1000002 的三個事件符合目標順序,到查看商品事件才流失
用戶 ID
事件 ID
事件名稱
時間
1000001
10002
登錄
2017/2/3 0:01
1000001
10004
瀏覽產品
2017/2/3 0:03
1000001
10003
搜索產品
2017/2/3 0:08
1000001
10007
生成訂單
2017/2/3 0:12
用戶 ID
事件 ID
事件名稱
時間
1000002
10002
登錄
2017/2/3 0:01
1000002
10003
搜索產品
2017/2/3 0:03
1000002
10004
瀏覽產品
2017/2/3 0:08
上面這些事件,有一些事件有必然的前后關系,比如退訂商品肯定發生在訂單付款之后,訂單付款肯定發生在生成訂單之后;而收藏商品和加入購物車就不一定誰先誰后了,退訂商品前也不一定發生評價商品的事件。這些不穩定性背后隱藏著用戶行為,通過對一組有序事件的漏斗分析,就找到了這組行為用戶在各個階段的流失率。這是第一個需求點:事件要順序發生,且能靈活定義。
第二個需求點是能對事件屬性自由定義條件,如 brand=’APPLE’,price>10。
第三個需求點是定義時間段,這段時間之外的數據不在考察范圍內,如 2017-02-012017-0228。
第四個需求點仍然是和時間有關,窗口時間,只有在窗口時間以內順序發生的事件才符合要求,比如 5 分鐘、3 個自然天;上面圖中的 1000002 用戶在 5 分鐘的窗口時間條件下,那只有前兩個事件符合要求,因為瀏覽產品事件遲于登錄 7 分鐘而超出窗口時間了。
第五個需求點是每個用戶只記錄一次符合要求的最長事件序列。
額外多提一句,現實業務中發生的漏斗分析不一定和上面這些需求細節完全一致,完全有可能更復雜,更個性化,我們這里是為了容易說明問題而假定了這些需求細節。
算法詳述
因為事件要求順序發生,所以我們第一步應該把數據先按照時間排序了,這樣同一個窗口期的數據就匯聚到比較集中的一塊了,而且窗口期內數據的位置也能表示先后次序了。數據量大大超出內存,用可以借用集算器的 sortx 對原始數據游標進行外存排序。
A
B
1
=file(fPath+”event.data”).cursor@t(用戶 ID, 事件 ID, 事件屬性, 時間)
/ 以游標方式加載文件數據
2
=A1.sortx(時間)
/ 用時間字段外存排序
3
=file(fPath+”eventOrdered.data”).export@b(A2)
/ 排序結果存入目標文件
最終的結果只需要每個用戶最長符合條件的事件序列的長度:代表該用戶發生流失時的最后一個事件。因為查找的過程中,不確定哪個事件序列最長,所以聚合信息里會保持住多個事件序列的信息。
events:定義目標事件順序數組:[A,B,C,D,E],事件序號分別為 1,2,3,4,5
UserList:定義空序表,每個用戶的信息聚合成一條信息存入這個序表,單個用戶聚合后的信息用 JSON 格式說明如下
{
用戶 ID:1000001
maxLen當前已找到的最大事件序列長度:3
seqs多個符合要求的事件序列數組:[
{// 第一個事件序列
該事件序列的開始時間:2017-02-03 21:18:18
, 該事件序列最后事件序號:2
},
{// 第二個事件序列
該事件序列的開始時間:2017-02-04 08:08:08
, 該事件序列最后事件序號:3
},
……
]
}
在定義了上面變量的基礎上,寫段偽代碼來描述算法過程:
for ( 按時間排好序的原始數據,循環逐條處理){
// 當前記錄里四個個變量:當前用戶、當前事件、當前時間,當前事件屬性
if (當前時間不在查詢時間段內 || 當前事件屬性不符合要求 ) continue;
if ( 在UserList里找到當前用戶){
if (maxLen == events長度 ) {
continue; // 已經找到當前用戶完整的目標事件序列,不用處理了,直接跳過
} else {
if (當前事件 == events第一個事件 ) {
新建一個事件序列追加入events。
} else {
for(events){
// 變量:事件序列=events [i]
if (當前事件序號 == 事件序列的最大序號+1 AND 當前時間在事件序列的窗口期內 ) {
事件序列的最大序號增加 1
if(事件序列的最大序號>maxLen){
maxLen增加 1
}
}
}
}
}
} else {// 沒找到
新建當前用戶的聚合信息,然后追加到UserList里
}
}
解決方案
sql 或存儲過程。雖然這個計算針對單表,但過程復雜,還對數據有序性有要求,這特點就正好是 sql 的軟肋。 能用 sql 寫出來的人,估計是鳳毛麟角,理論上能不能寫出來也存疑。退一步講,即便是寫出來了,性能的可控又是一大難題。反正我是沒有去細研究了,假如有研究出來的同學,可以反饋給我學習下。
UDF?那相當于直接用高級語言硬編碼了,代碼量可想而知(比如用 Java 不會少于 200 行),不光寫出來難度很大,以后再修改維護都是頭疼的事,這種 UDF 又沒什么通用性,需求變了就得重寫。
MapReduce 以及 Spark 之類的東西?MapReduce 對付這種有序的運算還真不好想。只是這個算法用 Scala 確實也寫得出來,也不算太長,不過,其中的問題卻是……,算了,這次先不提,以后專門細說,總之 Scala 是不合適。
想要一種能精確描述這個計算過程,并且描述方法符合人類自然思維習慣,并且能清楚知道每個步驟結果,并且能對步驟里的性能優化也能精確控制,并且代碼量不大,并且代碼容易復用……! 這么多貪心的“并且”,那就只能推薦這個專門處理數據的集算器腳本語言了。直接看代碼
A
B
C
D
E
1
>begin=date(string(begin),”yyyyMMdd”)
>end=date(string(end),”yyyyMMdd”)
>dateWindow=eval(dateWindow)
2
=create(用戶 ID,maxLen,seqs).keys(用戶 ID)
=now()
3
=file(fPath+”event30.txt”).cursor@t()
=A3.select(時間 >=begin&& 時間 0 && ${filter})
4
for C3
>user=A2.find@b(A4. 用戶 ID)
5
if user==null
>if (A4. 事件 ID==events(1),A2.insert(,A4. 用戶 ID: 用戶 ID,1:maxLen,[[A4. 時間,1]]:seqs))
6
next
7
if user.maxLen==events.len()
next
8
for user.seqs
>nextXh=B8(2)+1,time1=long(A4. 時間), time2=long(B8(1))+dateWindow, outWindow=time1>time2,nextEvt=A4. 事件 ID==events(nextXh)
if(outWindow||!nextEvt)
next
9
>B8(2)=nextXh,if(nextXh>user.maxLen,user.maxLen=nextXh),if(nextXh==events.len(),user.seqs=null)
next A4
10
if A4. 事件 ID==events(1)
>user.seqs.insert(0,[[A4. 時間,1]]),if(user.maxLen==0,user.maxLen=1)
11
=[A2.len()]
for events.len()-1
>A11.insert(0,A11(B11)-(A2.select(maxLen==B11).len()))
12
=interval@ms(C2,now())
針對單用戶的聚合代碼是第 4~10 行,規模和上面的偽代碼相當,基本上就是按自然思路去寫出算法。如果用 Java 類語言起碼是 10 倍長度了,代碼長了就要翻好幾頁,看到后面就會忘了前面,而集算器的代碼很短,一屏就能呈現出來,整個算法過程一目了然。
如果以前沒接觸過集算器的話,可能會看不懂這些代碼,不過沒關系了,掌握任何一門語言的語法都需要一個學習過程,我稍微解釋一些關鍵點:
變量說明:開始事件 begin,結束時間 end,窗口期毫秒數 dateWindow,目標事件序列 events,事件屬性過濾條件 filter。
A2:定義一個空序表,也就是偽代碼中的UserList。以用戶 ID為主鍵。
A3:定義被分析數據文件的游標,這樣多大的文件都能分批加載入內存進行計算了。
C3:對數據游標進行條件過濾,效果類比 SQL 語句里的 where 子句。
A4:從 C3 游標里循環取數據,每次取一行記錄做處理。
B4:用二分法從UserList里找當前記錄的用戶。
第 5、6 行:UserList里不存在當前用戶的處理分支,按照主鍵用戶 ID順序自動找正確的位置插入。
第 7 行:當前用戶已經找到完整目標事件序列,直接跳過。
第 8~9 行:已找到的多個事件序列進行循環處理,試圖把當前用戶信息融入某個符合條件的事件序列。融入成功,跳出到 A4 執行下一條;融入失敗,執行第 10 行。
第 11 行:用UserList計算出每個目標事件存留的用戶數,也就是漏斗需要的各層數據了。
A12:以秒為單位計算出 C2 執行到 A12 的耗時。
實現上面這個功能,無論用哪門語言,程序邏輯應該沒多大變化,關鍵就是看方便程度。這段流程還算繁雜的程序,寫完之后執行,只改了兩三處小毛病就跑通了,運行到哪個格子發生什么錯誤;哪個格子運算后的結果是啥都會一目了然。
為了驗證這段程序是否正確,只剩下 1000001 用戶如下的 9 條數據:
用戶 ID
事件 ID
時間
事件屬性
1000001
10001
2017/2/3 8:11
{}
1000001
10002
2017/2/3 8:12
{}
1000001
10003
2017/2/3 8:13
{“content”: “watch”, “page_num”: 3}
1000001
10004
2017/2/3 8:14
{“brand”: “Apple”, “price”: 2500}
1000001
10005
2017/2/3 8:15
{}
1000001
10006
2017/2/3 8:16
{}
1000001
10007
2017/2/3 8:17
{“price_all”: 3500}
1000001
10008
2017/2/3 8:18
{}
1000001
10009
2017/2/3 8:19
{“how”: -1}
不同條件的執行結果:
窗口期 1 分鐘,事件序列 [10003,10004,10005],算出來最大事件序號是 2
增長窗口期到 5 分鐘,結果是 3,找到了完整的目標事件序列。
目標事件序列反序 [10005,10004,10003] 測試,結果是 1,因為不存在這種順序。
修改事件屬性 Huawei,結果同樣為 1,因為沒有符合條件的 10004 事件
結語及預告
6 萬條符合條件的記錄,聚合出 3 萬個用戶的事件數據,耗時1.8 秒。目標數據 6 億條時,性能即便是線性的也需要 5 個小時,還很可能不是線性的,這就不能容忍了。
理論上能完成的任務在性能不達標的情況下,等同于不能完成。實際上好些生產中的業務就因為耗不起時間和計算資源,不得不作罷。可以預告下我們已經驗證了更多優化辦法,不僅限于修改這段程序的邏輯,還有發生在數據預處理階段的。正是在逐步優化、反復試錯的過程中才真切體會到一個順手工具的重要性。敬請關注!
總結
以上是生活随笔為你收集整理的java漏斗代码_集算示例:10 行代码解决漏斗转换计算的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 220V接LED指示灯电阻需要多大
- 下一篇: 漏斗周期漏斗数据关联优化