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

99 阅读5分钟

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

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

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


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

传统分析序列分析
只看页面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不产生页面栈变化

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