用户在小程序里的每一步操作,都是一条时间序列数据。
"首页→搜索→详情→加购→支付"是一条序列,"首页→退出"也是一条序列。
这篇文章,我会教你用序列模式挖掘 ,发现用户在小程序里的真实行为动线。
一、为什么需要序列分析?
| 传统分析 | 序列分析 |
|---|---|
| 只看页面PV排行 | 看页面之间的先后顺序 |
| 只看平均停留时间 | 看哪个页面之后用户最容易离开 |
| 只看跳出率 | 看哪些操作组合导致流失 |
| 只看单页面指标 | 看完整用户旅程 |
微信小程序序列数据的特殊性
| 特性 | 说明 |
|---|---|
| 场景值启动 | 用户从不同场景值进入,起点不同 |
| 页面栈限制 | 最多10层,navigateTo入栈,navigateBack出栈 |
| TabBar切换 | switchTab不入栈,直接切换 |
| 分享直达 | 可以从分享卡片直接进入任意页面 |
| onHide中断 | 用户可能随时切到微信聊天,会话中断 |
二、序列数据采集
2.1 序列事件定义
javascript复制
const SEQUENCE_EVENTS = [
'app_launch', // 小程序启动
'page_show', // 页面显示(核心)
'page_hide', // 页面隐藏
'tab_switch', // TabBar切换
'share_click', // 点击分享
'pull_refresh', // 下拉刷新
'scroll_bottom', // 滚动到底部
'search_submit', // 搜索提交
'add_cart', // 加入购物车
'pay_success', // 支付成功
'app_hide', // 小程序切到后台
];
2.2 序列采集SDK
javascript复制
class SequenceTracker {
constructor() {
this.sequence = [];
this.sessionId = null;
this.userId = null;
}
// App.onLaunch
onStart(userId, options) {
this.userId = userId;
this.sessionId = this.generateId();
this.sequence = [];
this.push('app_launch', {
scene: options.scene,
path: options.path,
});
}
// App.onHide(切到微信聊天)
onPause() {
this.push('app_hide', {});
this.flush();
}
// Page.onShow
onPageShow(page) {
this.push('page_show', {
page_path: page.route || page.__route__,
nav_type: page.__navigationType || 'unknown',
referrer: page.__referrer || '',
});
}
// Page.onHide
onPageHide(page) {
const stayTime = Date.now() - (page.__showTime || Date.now());
this.push('page_hide', {
page_path: page.route || page.__route__,
stay_duration: stayTime,
});
}
// TabBar切换
onTabSwitch(tabName, page) {
this.push('tab_switch', { tab: tabName, page_path: page.route });
}
// 下拉刷新
onPullRefresh(page) {
this.push('pull_refresh', { page_path: page.route });
}
// 搜索
onSearch(keyword, page) {
this.push('search_submit', { keyword, page_path: page.route });
}
// 加购
onAddCart(productId, page) {
this.push('add_cart', { product_id: productId, page_path: page.route });
}
push(eventName, properties = {}) {
this.sequence.push({ event: eventName, time: Date.now(), properties });
if (this.sequence.length > 500) this.flush();
}
// 上报序列
flush() {
if (this.sequence.length === 0) return;
const eventSequence = this.sequence.map(e => e.event);
const pageSequence = this.sequence
.filter(e => e.event === 'page_show')
.map(e => e.properties.page_path);
tracker.track('behavior_sequence', {
session_id: this.sessionId,
event_sequence: JSON.stringify(eventSequence),
page_sequence: JSON.stringify(pageSequence),
sequence_length: this.sequence.length,
page_count: pageSequence.length,
total_duration: this.sequence[this.sequence.length - 1].time - this.sequence[0].time,
});
this.sequence = [];
this.sessionId = this.generateId();
}
}
三、序列分析
3.1 页面转移概率
sql复制
-- 2步转移:A页面之后去了B页面
SELECT
page_from,
page_to,
count() AS transitions,
count() * 100.0 / sum(count()) OVER (PARTITION BY page_from) AS transition_rate
FROM (
SELECT
page_from,
leadInFrame(page_to) OVER (PARTITION BY session_id ORDER BY event_time) AS next_page
FROM page_transitions
WHERE app_id = 'your_app_id' AND event_date >= today() - 7
)
WHERE next_page != ''
GROUP BY page_from, page_to
ORDER BY transitions DESC LIMIT 50;
3.2 按场景值的序列模式对比
| 场景值 | 典型路径 | 特征 |
|---|---|---|
| 1011/1025(扫码) | 直接进入中间页 → 目标页 | 跳过首页 |
| 1018(分享) | 目标页 → 详情页 → 购买页 | 直达内容 |
| 1014/1035(公众号) | 首页 → 列表页 → 详情页 | 从首页开始 |
| 1038(搜一搜) | 搜索结果页 → 详情页 | 从搜索直达 |
| 1027(附近) | 首页 → 门店页 | 带地理位置 |
| 1059(支付完成) | 首页 → 订单页 → 评价页 | 后置场景 |
3.3 频繁序列挖掘(PrefixSpan简化版)
javascript复制
class FrequentSequenceMiner {
constructor(options = {}) {
this.minSupport = options.minSupport || 5;
this.maxLength = options.maxLength || 4;
}
mine(sequences) {
const results = [];
for (let n = 2; n <= this.maxLength; n++) {
const nSeqs = this.findFrequentNSequences(sequences, n);
results.push(...nSeqs.map(s => ({ sequence: s.pattern, support: s.support })));
}
return results.sort((a, b) => b.support - a.support);
}
findFrequentNSequences(sequences, n) {
const patternCounts = {};
for (const seq of sequences) {
for (let i = 0; i <= seq.length - n; i++) {
const subSeq = seq.slice(i, i + n).join(' → ');
patternCounts[subSeq] = (patternCounts[subSeq] || 0) + 1;
}
}
return Object.entries(patternCounts)
.filter(([_, count]) => count >= this.minSupport)
.map(([pattern, support]) => ({
pattern: pattern.split(' → '),
support,
}))
.sort((a, b) => b.support - a.support);
}
}
// 使用示例
const miner = new FrequentSequenceMiner({ minSupport: 20, maxLength: 4 });
const patterns = miner.mine([
['/pages/index/index', '/pages/search/result', '/pages/product/detail', '/pages/cart/index'],
['/pages/index/index', '/pages/category/list', '/pages/product/detail'],
['/pages/product/detail', '/pages/cart/index', '/pages/order/confirm', '/pages/order/pay'],
['/pages/index/index', '/pages/user/index'],
]);
// 输出:
// 1. [342次] /pages/index/index → /pages/product/detail
// 2. [256次] /pages/product/detail → /pages/cart/index
// 3. [198次] /pages/index/index → /pages/category/list → /pages/product/detail
// 4. [156次] /pages/product/detail → /pages/cart/index → /pages/order/confirm
四、流失路径分析
4.1 流失路径分析器
javascript复制
class ChurnPathAnalyzer {
constructor() {
this.churnPaths = [];
this.convertPaths = [];
}
analyzeSequence(sessionData) {
const events = sessionData.event_sequence;
const CONVERSION_EVENTS = ['pay_success', 'order_submit'];
const hasConversion = events.some(e => CONVERSION_EVENTS.includes(e));
if (hasConversion) {
const idx = events.findIndex(e => CONVERSION_EVENTS.includes(e));
this.convertPaths.push(events.slice(0, idx + 1));
} else {
this.churnPaths.push(events);
}
}
// 高频流失路径
getTopChurnPaths(limit = 20) {
const pathCounts = {};
for (const path of this.churnPaths) {
const key = path.slice(0, 3).join(' → ');
pathCounts[key] = (pathCounts[key] || 0) + 1;
}
const total = this.churnPaths.length;
return Object.entries(pathCounts)
.map(([path, count]) => ({ path, count, percent: (count / total * 100).toFixed(1) + '%' }))
.sort((a, b) => b.count - a.count)
.slice(0, limit);
}
// 流失前的最后一步
getLastStepBeforeChurn() {
const counts = {};
for (const path of this.churnPaths) {
const last = path[path.length - 1];
counts[last] = (counts[last] || 0) + 1;
}
return Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([step, count]) => ({ step, count }));
}
// 对比转化vs流失路径差异
comparePaths() {
const convertFreq = {}, churnFreq = {};
for (const path of this.convertPaths) path.forEach(p => convertFreq[p] = (convertFreq[p] || 0) + 1);
for (const path of this.churnPaths) path.forEach(p => churnFreq[p] = (churnFreq[p] || 0) + 1);
const allPages = new Set([...Object.keys(convertFreq), ...Object.keys(churnFreq)]);
return [...allPages].map(page => ({
page,
convert_rate: ((convertFreq[page] || 0) / this.convertPaths.length * 100).toFixed(1) + '%',
churn_rate: ((churnFreq[page] || 0) / this.churnPaths.length * 100).toFixed(1) + '%',
diff: (convertFreq[page] || 0) / this.convertPaths.length > (churnFreq[page] || 0) / this.churnPaths.length
? '转化偏好' : '流失偏好',
})).sort((a, b) => parseFloat(b.churn_rate) - parseFloat(a.churn_rate));
}
}
4.2 微信小程序特有的流失原因
| 流失场景 | 微信特有原因 | 分析方法 |
|---|---|---|
| 分享卡片打开后立即退出 | 标题/图片与实际内容不符 | 对比分享场景的跳出率 |
| 扫码后立即退出 | 小程序码指向的页面不存在 | 检查404页面UV |
| 搜一搜进入后退出 | 搜索结果内容不匹配 | 分析搜一搜场景停留时间 |
| 公众号菜单进入后退出 | 菜单链接指向的页面被改版 | 检查公众号场景首页跳出率 |
| 支付页面退出 | 微信支付弹窗被取消 | 监控pay_fail事件 |
| TabBar切换后退出 | 某个Tab页加载失败 | 按Tab页面分析跳出率 |
五、序列分析看板(桑基图)
vue复制
<template>
<div>
<h2>用户浏览流向(桑基图)</h2>
<select v-model="selectedScene" @change="fetchData">
<option value="all">全部场景</option>
<option value="1018">好友分享</option>
<option value="1038">搜一搜</option>
<option value="1014">公众号菜单</option>
<option value="1011">扫码</option>
</select>
<div ref="sankeyChart" style="height: 600px;"></div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
const sankeyChart = ref(null);
const selectedScene = ref('all');
async function fetchData() {
const res = await fetch(`/api/sequence/sankey?app_id=your_app_id&scene=${selectedScene.value}`);
const data = await res.json();
const nodes = [...new Set(data.flatMap(d => [d.page_from, d.page_to]))].map(name => ({ name }));
const links = data.map(d => ({ source: d.page_from, target: d.page_to, value: d.flow_count }));
const chart = echarts.init(sankeyChart.value);
chart.setOption({
series: [{
type: 'sankey',
emphasis: { focus: 'adjacency' },
data: nodes,
links: links,
lineStyle: { color: 'gradient', curveness: 0.5 },
}],
});
}
onMounted(fetchData);
</script>
写在最后
序列分析的本质,是把"用户行为"从"统计数字"还原为"故事"。
微信小程序序列分析的4个要点:
- 页面栈限制:最多10层,路径不会太长
- 场景值入口:不同场景值的起点不同
- 会话中断:用户可能随时切到微信聊天
- TabBar切换:switchTab不产生页面栈变化
按场景值分组分析序列,才能发现不同入口用户的真实行为差异。
