Python实战:用GSP算法挖掘电商用户购买序列中的隐藏规律
如果你在电商平台工作过,或者负责过用户行为分析,一定遇到过这样的困惑:用户今天买了手机壳,明天买了充电宝,后天又买了耳机——这些看似随机的购买行为背后,是否存在某种规律?能否预测用户下一步会买什么?这就是序列模式挖掘要解决的问题。
传统的关联规则挖掘(比如经典的Apriori算法)只能告诉你“尿布和啤酒经常一起买”,但它忽略了时间顺序。而现实世界中,用户的行为是有先后顺序的:先买手机,再买手机壳,然后买贴膜,最后买充电宝。GSP(Generalized Sequential Pattern)算法正是为了挖掘这种时间序列中的频繁模式而生的。
我在实际电商数据分析项目中,多次使用GSP算法发现了令人惊讶的用户行为模式。有一次,我们分析一个母婴电商平台的数据,发现了一个清晰的序列:[孕妇装] → [婴儿床] → [纸尿裤] → [辅食机]。这个序列不仅揭示了用户从孕期到育儿期的完整消费路径,还让我们能够精准预测用户在不同阶段的需求,从而设计出更有效的营销策略。
本文将带你从零开始,用Python实现GSP算法,并应用到真实的电商数据分析场景中。我会分享实际项目中的代码优化技巧、参数调优经验,以及如何将算法结果转化为可落地的业务策略。
1. 序列模式挖掘:从理论到电商实践
1.1 什么是序列模式挖掘?
序列模式挖掘是数据挖掘的一个重要分支,专门用于发现数据序列中的频繁模式。与传统的关联规则挖掘不同,序列模式挖掘关注的是事件发生的顺序。
举个例子:
- 关联规则:
{尿布, 啤酒}(同时购买) - 序列模式:
<尿布> → <啤酒>(先买尿布,后买啤酒)
在电商场景中,每个用户的购买历史可以看作一个序列:
用户A: <手机, 手机壳, 充电宝, 耳机>
用户B: <手机, 贴膜, 手机壳>
用户C: <充电宝, 耳机, 手机壳>
GSP算法要做的就是找出所有用户中频繁出现的购买序列模式。
1.2 GSP算法的核心思想
GSP算法由R. Srikant和R. Agrawal于1996年提出,是Apriori算法在序列数据上的扩展。它的核心思想是逐层搜索和剪枝:
- 生成-测试框架:从频繁1-序列开始,逐步生成更长的候选序列
- Apriori性质:如果一个序列是频繁的,那么它的所有子序列也必须是频繁的
- 时间约束:可以设置最小间隔(min_gap)和最大间隔(max_gap)来限制序列中事件的时间关系
算法流程可以用下面的伪代码表示:
# GSP算法伪代码
输入:序列数据库D,最小支持度阈值min_sup
输出:所有频繁序列
L1 = 找出所有频繁1-序列
k = 2
while L(k-1) 非空:
Ck = 从L(k-1)生成候选k-序列
for 每个序列s in 数据库D:
for 每个候选c in Ck:
if s包含c:
c.count += 1
Lk = {c ∈ Ck | c.count ≥ min_sup}
k += 1
返回所有Lk的并集
1.3 电商场景中的序列数据特点
在电商数据分析中,用户购买序列有几个重要特点:
| 特征 | 描述 | 对GSP算法的影响 |
|---|---|---|
| 时间戳精度 | 购买时间可能精确到秒 | 需要合理定义时间窗口 |
| 会话划分 | 用户多次访问形成多个会话 | 需要按会话分组序列 |
| 商品层级 | 商品有类目、品牌等多级分类 | 可以在不同粒度上挖掘模式 |
| 稀疏性 | 用户购买行为相对稀疏 | 需要适当降低支持度阈值 |
提示:在实际项目中,我通常会将原始购买日志按用户会话(session)进行分割,每个会话内的购买行为作为一个序列。会话的超时时间通常设置为30分钟,即用户两次操作间隔超过30分钟就认为是新的会话。
2. 数据准备与预处理:电商购买数据的实战处理
2.1 原始数据格式与清洗
电商平台的原始购买数据通常存储在数据库或日志文件中,格式可能如下:
user_id,timestamp,product_id,category,price
1001,2024-01-01 10:30:25,P001,Electronics,2999.00
1001,2024-01-01 10:35:42,P002,Accessories,89.00
1002,2024-01-01 11:15:10,P003,Home,599.00
我们需要将这些数据转换为GSP算法需要的序列格式。首先,让我们加载并查看数据:
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 读取原始数据
def load_ecommerce_data(file_path):
"""
加载电商购买数据
"""
df = pd.read_csv(file_path, parse_dates=['timestamp'])
# 数据质量检查
print(f"数据总行数: {len(df)}")
print(f"唯一用户数: {df['user_id'].nunique()}")
print(f"商品种类数: {df['product_id'].nunique()}")
print(f"时间范围: {df['timestamp'].min()} 到 {df['timestamp'].max()}")
# 检查缺失值
missing_values = df.isnull().sum()
print("\n缺失值统计:")
print(missing_values[missing_values > 0])
return df
# 示例数据生成(实际项目中从文件读取)
def generate_sample_data(n_users=1000, n_transactions=5000):
"""
生成模拟电商数据用于演示
"""
np.random.seed(42)
user_ids = [f'U{str(i).zfill(4)}' for i in range(1, n_users+1)]
product_ids = [f'P{str(i).zfill(3)}' for i in range(1, 101)]
categories = ['Electronics', 'Clothing', 'Home', 'Books', 'Sports']
data = []
start_date = datetime(2024, 1, 1)
for _ in range(n_transactions):
user = np.random.choice(user_ids)
product = np.random.choice(product_ids)
category = np.random.choice(categories)
# 生成时间戳(在30天内随机分布)
days_offset = np.random.randint(0, 30)
hours_offset = np.random.randint(0, 24)
minutes_offset = np.random.randint(0, 60)
timestamp = start_date + timedelta(days=days_offset,
hours=hours_offset,
minutes=minutes_offset)
price = np.random.uniform(10, 5000)
data.append({
'user_id': user,
'timestamp': timestamp,
'product_id': product,
'category': category,
'price': round(price, 2)
})
return pd.DataFrame(data)
# 使用示例数据
df = generate_sample_data()
print(df.head())
2.2 会话分割与序列构建
用户购买行为通常以会话为单位。我们需要将用户的连续购买行为分割成不同的会话:
def create_user_sessions(df, session_timeout=30):
"""
将用户购买记录分割为会话
session_timeout: 会话超时时间(分钟)
"""
# 按用户和时间排序
df = df.sort_values(['user_id', 'timestamp'])
sessions = []
current_session = []
last_user = None
last_time = None
for _, row in df.iterrows():
current_time = row['timestamp']
# 如果是新用户或时间间隔超过阈值,开始新会话
if (last_user != row['user_id'] or
(last_time and (current_time - last_time).total_seconds() > session_timeout*60)):
if current_session:
sessions.append(current_session)
current_session = []
current_session.append(row['product_id'])
last_user = row['user_id']
last_time = current_time
if current_session:
sessions.append(current_session)
return sessions
# 创建购买序列
sessions = create_user_sessions(df)
print(f"总共创建了 {len(sessions)} 个购买会话")
print(f"平均会话长度: {np.mean([len(s) for s in sessions]):.2f}")
# 查看前5个会话
for i, session in enumerate(sessions[:5]):
print(f"会话{i+1}: {session}")
2.3 数据转换与编码
GSP算法通常处理的是离散的项集序列。我们需要将商品ID转换为算法友好的格式:
from collections import defaultdict
def prepare_gsp_input(sessions, min_support=0.01):
"""
准备GSP算法输入数据
"""
# 统计每个商品的出现频率
item_counts = defaultdict(int)
total_sessions = len(sessions)
for session in sessions:
unique_items = set(session)
for item in unique_items:
item_counts[item] += 1
# 过滤低频商品(可选)
min_count = total_sessions * min_support
frequent_items = {item for item, count in item_counts.items()
if count >= min_count}
# 创建商品到整数的映射(提高算法效率)
item_to_id = {item: i for i, item in enumerate(sorted(frequent_items))}
id_to_item = {i: item for item, i in item_to_id.items()}
# 转换序列
encoded_sessions = []
for session in sessions:
# 只保留频繁商品,并去重(同一会话中同一商品只计一次)
session_items = []
seen = set()
for item in session:
if item in frequent_items and item not in seen:
session_items.append(item_to_id[item])
seen.add(item)
if session_items: # 只保留非空会话
encoded_sessions.append(session_items)
print(f"处理后会话数: {len(encoded_sessions)}")
print(f"商品种类数: {len(item_to_id)}")
return encoded_sessions, id_to_item
# 准备GSP算法输入
encoded_sessions, id_mapping = prepare_gsp_input(sessions, min_support=0.02)
print(f"\n商品映射示例(前10个):")
for i in range(min(10, len(id_mapping))):
print(f" {i}: {id_mapping[i]}")
3. GSP算法Python实现:从理论到代码
3.1 GSP算法核心类设计
让我们从零开始实现GSP算法。首先设计几个核心类:
class Sequence:
"""序列类,表示一个购买序列"""
def __init__(self, items=None):
self.items = items if items else []
self.support = 0
def __len__(self):
return len(self.items)
def __getitem__(self, index):
return self.items[index]
def __repr__(self):
return f"Sequence({self.items}, support={self.support})"
def is_subsequence(self, other):
"""判断当前序列是否是other的子序列"""
i = 0
for item in other.items:
if i < len(self.items) and self.items[i] == item:
i += 1
return i == len(self.items)
def get_prefix(self):
"""获取前缀(去掉最后一个元素)"""
if len(self.items) <= 1:
return None
return Sequence(self.items[:-1])
def get_suffix(self):
"""获取后缀(去掉第一个元素)"""
if len(self.items) <= 1:
return None
return Sequence(self.items[1:])
class GSP:
"""GSP算法主类"""



被折叠的 条评论
为什么被折叠?



