在电商数据仓库建设与OLAP分析中,指标体系是连接底层日志与上层报表的语义层。本文不聊业务价值,只谈指标口径定义、技术拆解与SQL/Python实现逻辑,面向数仓开发与数据分析师。
一、数据采集与事件模型(埋点基座)
一切指标源于事件。APP/H5/Web端埋点日志落库时,至少携带以下原子字段:
{
// 系统演示、API调用控制台:http://console.open.onebound.cn/console/?i=NewRookie
"event_id": "string", // 全局唯一ID
"user_id": "string", // 登录态ID,未登录置空
"device_id": "string", // 设备指纹
"session_id": "string", // 会话ID(后端生成,30分钟超时切割)
"event_type": "string", // 枚举:page_view, click, add_cart, submit_order, pay_success
"page_url": "string",
"product_sku_id": "string",
"referrer": "string", // 站内外来源
"event_time": "bigint", // 毫秒级时间戳
"extra_params": "map<string,string>" // 透传业务参数
}
会话切割逻辑(Hive UDF或Flink KeyedProcessFunction):
-
同一
device_id下,相邻事件间隔>30分钟则划归新session_id。 -
跨天会话按自然日切割(00:00强制断开会话),否则"访问次数(Visit)"口径会膨胀。
二、网站流量指标(Web/Mobile Web)
流量域为最上游,计算时必须区分设备ID与用户ID ,未登录用户用device_id代理。
2.1 UV / PV / Visit 的 Hive 计算
-- 日粒度 UV (独立访客)
SELECT
dt,
COUNT(DISTINCT device_id) AS uv
FROM dwd_event_log
WHERE event_type = 'page_view'
AND dt = '${bizdate}'
GROUP BY dt;
-- PV (页面浏览量) 直接计数
SELECT
dt,
COUNT(1) AS pv
FROM dwd_event_log
WHERE event_type = 'page_view'
GROUP BY dt;
-- Visit (访问次数) 需依赖预处理好的 session_id
SELECT
dt,
COUNT(DISTINCT session_id) AS visit_cnt
FROM dws_session_agg -- 预聚合会话宽表
WHERE dt = '${bizdate}';
2.2 跳出率与退出率(重点讲口径)
跳出(Bounce) :指该session_id下仅包含1个page_view事件,且无任何交互事件(click、add_cart等)。
sql
-- 跳出率计算
WITH bounce_sessions AS (
SELECT
session_id,
COUNT(CASE WHEN event_type = 'page_view' THEN 1 END) AS pv_cnt,
COUNT(CASE WHEN event_type IN ('click','add_cart','pay') THEN 1 END) AS action_cnt
FROM dwd_event_log
WHERE dt = '${bizdate}'
GROUP BY session_id
HAVING pv_cnt = 1 AND action_cnt = 0
)
SELECT
COUNT(DISTINCT b.session_id) / COUNT(DISTINCT e.session_id) AS bounce_rate
FROM dwd_event_log e
LEFT JOIN bounce_sessions b ON e.session_id = b.session_id;
退出率(Exit Rate) :针对特定页面。若用户最后一条page_view落在该页面,则计为退出。
sql
-- 页面退出率
WITH last_page AS (
SELECT
session_id,
page_url,
ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY event_time DESC) AS rn
FROM dwd_event_log
WHERE event_type = 'page_view'
)
SELECT
page_url,
COUNT(CASE WHEN rn = 1 THEN 1 END) AS exit_cnt,
COUNT(1) AS page_pv,
COUNT(CASE WHEN rn = 1 THEN 1 END) / COUNT(1) AS exit_rate
FROM last_page
GROUP BY page_url;
坑点:跳出率分母为"落地页访问量",分子为"落地上即跳出的会话量";退出率分母为该页面的总PV。两者切勿混淆。
三、交易域指标(订单与商品)
交易域事实表通常设计为事务事实表,一条记录对应一个订单明细(SKU粒度)。
sql
-- 事实表结构示例
CREATE TABLE dwd_order_detail (
dt STRING COMMENT '分区日期',
order_id STRING,
sku_id STRING,
user_id STRING,
sku_price DECIMAL(18,2), -- 成交单价
sku_quantity INT,
order_status STRING, -- 枚举:已支付、已取消、已退款、已妥投
pay_amount DECIMAL(18,2), -- 该SKU分摊后的实付金额
coupon_discount DECIMAL(18,2),
freight DECIMAL(18,2)
);
3.1 GMV、销售额与有效订单金额(必须严打口径)
-
GMV(网站成交总金额) :包含所有状态 (已支付、未支付、取消、拒收、退货)的订单金额总和。
SUM(sku_price * sku_quantity)。 -
商品销售总额 :仅统计已妥投状态(或至少已支付未退款)的金额。
-
平均订单金额(AOV) :
有效订单总金额 / 去重后的有效订单数(注意用户粒度与订单粒度的差异)。
sql
-- 严格过滤有效状态(业务侧需定义状态机)
SELECT
dt,
SUM(sku_price * sku_quantity) AS gmv,
SUM(CASE WHEN order_status IN ('PAID','DELIVERED','FINISHED')
THEN pay_amount ELSE 0 END) AS valid_sales
FROM dwd_order_detail
GROUP BY dt;
3.2 客单价与件单价
客单价(Per Customer Transaction)容易与AOV混淆,技术口径:
-
AOV = 总销售额 / 订单数(Order粒度)
-
客单价 = 总销售额 / 购买用户数(User粒度),即
SUM(pay_amount) / COUNT(DISTINCT user_id)
3.3 购物车放弃率(漏斗监控)
漏斗步骤:浏览→加购→结算→支付。
sql
-- 每日加购->支付转化(基于用户/设备去重)
WITH funnel AS (
SELECT
device_id,
MAX(CASE WHEN event_type = 'add_cart' THEN 1 ELSE 0 END) AS has_cart,
MAX(CASE WHEN event_type = 'pay_success' THEN 1 ELSE 0 END) AS has_pay
FROM dwd_event_log
WHERE dt = '${bizdate}'
GROUP BY device_id
)
SELECT
SUM(has_cart) AS cart_uv,
SUM(CASE WHEN has_cart = 1 AND has_pay = 0 THEN 1 ELSE 0 END) AS abandon_uv,
SUM(CASE WHEN has_cart = 1 AND has_pay = 0 THEN 1 ELSE 0 END) / SUM(has_cart) AS cart_abandon_rate
FROM funnel;
四、会员域指标(生命周期与RFM)
会员分析依赖累积宽表(用户维度快照表),每日更新。
4.1 RFM 模型的 Python 实现(离线批量打标)
RFM(Recency, Frequency, Monetary)是经典用户分层算法。取近90天数据。
python
import pandas as pd
import numpy as np
def calc_rfm(df: pd.DataFrame) -> pd.DataFrame:
"""
df 字段: user_id, order_date, pay_amount
需预先过滤有效订单
"""
# 计算 Reference Date(以最新分区日期为准)
ref_date = df['order_date'].max()
rfm_df = df.groupby('user_id').agg({
'order_date': lambda x: (ref_date - x.max()).days, # R: 最近一次距今的天数
'order_id': 'count', # F: 订单频次
'pay_amount': 'sum' # M: 总消费金额
}).reset_index()
rfm_df.columns = ['user_id', 'R', 'F', 'M']
# 基于分位数打分 (1-5分),也可使用业务自定义阈值
for col in ['R', 'F', 'M']:
# R 值越小越好,需反转打分
if col == 'R':
rfm_df[f'{col}_score'] = pd.qcut(rfm_df[col].rank(method='first'), 5, labels=[5,4,3,2,1])
else:
rfm_df[f'{col}_score'] = pd.qcut(rfm_df[col], 5, labels=[1,2,3,4,5])
# 拼接RFM总得分或分层标签(如 555 为高价值)
rfm_df['rfm_score'] = rfm_df['R_score'].astype(str) + rfm_df['F_score'].astype(str) + rfm_df['M_score'].astype(str)
return rfm_df
4.2 留存率(Cohort Analysis)SQL实现
以用户首次购买月份为同期群,追踪后续各月复购率。
sql
WITH first_order AS (
SELECT
user_id,
DATE_FORMAT(MIN(order_date), 'yyyy-MM') AS cohort_month
FROM dwd_order_detail
WHERE order_status IN ('PAID','FINISHED')
GROUP BY user_id
),
user_orders AS (
SELECT
o.user_id,
DATE_FORMAT(o.order_date, 'yyyy-MM') AS order_month,
f.cohort_month
FROM dwd_order_detail o
JOIN first_order f ON o.user_id = f.user_id
WHERE o.order_status IN ('PAID','FINISHED')
GROUP BY o.user_id, DATE_FORMAT(o.order_date, 'yyyy-MM'), f.cohort_month
)
SELECT
cohort_month,
order_month,
COUNT(DISTINCT user_id) AS active_users,
FIRST_VALUE(COUNT(DISTINCT user_id)) OVER (PARTITION BY cohort_month ORDER BY order_month) AS cohort_size,
COUNT(DISTINCT user_id) / FIRST_VALUE(COUNT(DISTINCT user_id)) OVER (PARTITION BY cohort_month ORDER BY order_month) AS retention_rate
FROM user_orders
GROUP BY cohort_month, order_month
ORDER BY cohort_month, order_month;
五、仓储与供应链指标(滞后性指标计算)
仓储指标依赖库存变动事实表 与周期快照事实表。
5.1 库存周转天数(需关联销售速度)
公式:库存周转天数 = (当前可用库存金额 / 过去30天平均销售成本) * 30。
代码实现(Hive):
sql
SELECT
sku_id,
current_inventory_amount, -- 当前库存成本
COALESCE(avg_30d_sales_cost, 0.01) AS avg_sales_cost, -- 防止除零
(current_inventory_amount / COALESCE(avg_30d_sales_cost, 0.01)) * 30 AS inventory_turnover_days
FROM dwd_inventory_snapshot i
LEFT JOIN (
SELECT
sku_id,
AVG(sales_cost) AS avg_30d_sales_cost
FROM dwd_order_detail
WHERE dt BETWEEN DATE_SUB('${bizdate}', 30) AND '${bizdate}'
AND order_status = 'FINISHED'
GROUP BY sku_id
) s ON i.sku_id = s.sku_id
WHERE i.dt = '${bizdate}';
5.2 缺货率(分母口径差异)
缺货率存在两种口径:
-
订单视角 :
因库存不足导致缺货的订单数 / 总订单数(需业务打标缺货标记)。 -
SKU视角 :
缺货SKU数 / 在售SKU总数(定时任务巡检库存水位,若current_inventory < 安全库存则标记缺货)。
推荐技术方案:在订单submit环节实时校验库存,若校验失败则将order_status置为OUT_OF_STOCK,离线ETL直接统计该状态占比即可。
六、物流配送指标(状态流转监控)
物流指标的本质是订单履约状态机的时间差分析。
sql
-- 订单履约时效明细(小时粒度)
SELECT
order_id,
(pay_time - submit_time) / 3600 AS pay_time_gap, -- 支付时效
(deliver_time - pay_time) / 3600 AS warehouse_process, -- 仓库出库时效
(sign_time - deliver_time) / 3600 AS shipping_duration -- 在途时长
FROM dwd_order_lifecycle -- 该表由Flink CDC同步订单状态变更日志生成
WHERE dt = '${bizdate}';
满载率 属于物流运力调度指标,需要车辆任务表(配送任务明细),计算逻辑为:SUM(实际件数或体积) / SUM(车辆核定载量),此处不赘述。
七、数据质量校验(防止指标异动)
7.1 同环比阈值监控(基于Z-Score)
每日指标产出后,需跑自动化巡检:
python
import numpy as np
from scipy import stats
def detect_anomaly(metric_series, current_val, window=30, z_threshold=3.0):
"""
metric_series: 近N天历史值列表
"""
mean = np.mean(metric_series)
std = np.std(metric_series)
if std == 0:
return abs(current_val - mean) > 0.1 * mean # 标准差为零时改用波动百分比
z_score = (current_val - mean) / std
return abs(z_score) > z_threshold
7.2 漏斗一致性校验(防埋点丢失)
校验公式:上一环节UV >= 下一环节UV。若加入购物车UV > 商品详情页UV,则触发告警,原因通常是埋点上报时机混乱(如加购事件在未浏览详情页时也能触发)。
八、总结:数仓分层对应关系
| 数据域 | ODS层 | DWD层(明细) | DWS层(汇总) | ADS层(应用) |
|---|---|---|---|---|
| 流量 | 原始日志 | 事件明细表 | 会话聚合表、日活宽表 | 跳出率、漏斗转化 |
| 交易 | 业务订单表 | 订单明细事实表 | 用户日交易汇总 | GMV、客单价、复购率 |
| 仓储 | 库存快照表 | 库存变动事实表 | SKU日周转汇总 | 缺货预警、滞销清单 |
| 会员 | 用户信息表 | 登录/注册日志 | 用户累积特征宽表 | RFM分层、留存Cohort |
开发规范 :所有指标必须附带口径说明字段(是否去重、是否过滤退款、时间窗口定义),写入指标字典元数据中心,避免"一个GMV十个数"的窘境。