微信小程序浏览行为序列分析:用户在小程序里的真实动线

用户在小程序里的每一步操作,都是一条时间序列数据。

"首页→搜索→详情→加购→支付"是一条序列,"首页→退出"也是一条序列。

这篇文章,我会教你用序列模式挖掘 ,发现用户在小程序里的真实行为动线


一、为什么需要序列分析?

传统分析 序列分析
只看页面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个要点:

  1. 页面栈限制:最多10层,路径不会太长
  2. 场景值入口:不同场景值的起点不同
  3. 会话中断:用户可能随时切到微信聊天
  4. TabBar切换:switchTab不产生页面栈变化

按场景值分组分析序列,才能发现不同入口用户的真实行为差异。