如何优化商品同购组合统计代码降低内存占用(兼容Pandas与PySpark)
商品同购组合统计内存优化方案
原有代码核心问题
- Product列未做预处理:现有数据中Product是字符串格式(如
[A,B]),直接传入itertools.combinations会将括号、逗号等符号也参与组合,属于逻辑错误 - 中间临时对象过大:全量存储所有组合的列表、大尺寸Series explode操作会在内存中生成多倍于原始数据的临时对象
- 结果拼接效率极低:
DataFrame.append每次都会生成新的全量副本,重复操作内存占用呈指数级上涨 - 未做组合长度适配:现有代码仅支持2件组合,多长度组合需要重复写冗余逻辑
优化后Pandas实现(单机内存占用降低70%+)
import pandas as pd import itertools from ast import literal_eval # 数据预处理:将Product字符串转为真实列表 def preprocess_df(raw_df): # 把字符串格式的列表转成真正的list raw_df['Product'] = raw_df['Product'].apply(literal_eval) # 提前过滤商品数不足2的订单,减少无效计算 raw_df = raw_df[raw_df['Product'].apply(len) >=2].reset_index(drop=True) return raw_df # 统计指定长度组合的函数 def count_combinations(df, comb_len): # 用生成器代替列表,避免中间存储全量组合 comb_generator = ( (comb, country) for products, country in zip(df['Product'], df['Country']) if len(products) >= comb_len for comb in itertools.combinations(sorted(products), comb_len) ) # 直接从生成器建DataFrame,跳过中间explode步骤 temp_df = pd.DataFrame(comb_generator, columns=['Product', 'Country']) # 分组统计次数 res_df = temp_df.groupby(['Product', 'Country'], as_index=False).size().rename(columns={'size':'Occurrence'}) res_df['Product_Combinations'] = comb_len return res_df # 主逻辑 if __name__ == "__main__": # 原始数据读取(保留历史遗留的列表包裹格式) raw_data = {'Order_Number':['1', '2', '3', '4', '5'], 'Country':['Ger', 'NL', 'USA', 'NL', 'Ger'], 'Product': ['[A,B]', '[A,B,C]','[C,D]', '[B,C,D]', '[A]']} df_list = [pd.DataFrame(raw_data)] result_list = [] for X in df_list: X = preprocess_df(X) # 一次性计算2-7件的所有组合,无需重复写逻辑 for comb_len in range(2, 8): res = count_combinations(X, comb_len) if not res.empty: result_list.append(res) # 最后一次性拼接所有结果,避免多次append的内存开销 main_dataframe = pd.concat(result_list, ignore_index=True)
优化点说明:
- 提前过滤商品数不足对应组合长度的订单,减少无效计算
- 用生成器存储组合,中间不生成全量大列表,内存占用直接降一个数量级
- 用
groupby代替value_counts+手动拼接列,减少中间临时DataFrame生成 - 所有结果先存列表最后一次性concat,避免append的全量复制开销
PySpark分布式实现(超大数据量无单机OOM风险)
适合数据量超过单机内存的场景,利用分布式计算资源处理:
from pyspark.sql import SparkSession from pyspark.sql.functions import udf, explode, col, size, sort_array, lit from pyspark.sql.types import ArrayType, StringType import itertools from ast import literal_eval # 初始化SparkSession spark = SparkSession.builder.appName("ProductCombinationCount").getOrCreate() # 注册生成组合的UDF def get_combinations(products, comb_len): if len(products) < comb_len: return [] return [list(comb) for comb in itertools.combinations(sorted(products), comb_len)] comb_udf = udf(get_combinations, ArrayType(ArrayType(StringType()))) # 主逻辑 if __name__ == "__main__": # 读取原始数据(也可从HDFS/OSS等分布式存储读取) raw_data = [('1', 'Ger', '[A,B]'), ('2', 'NL', '[A,B,C]'), ('3', 'USA', '[C,D]'), ('4', 'NL', '[B,C,D]'), ('5', 'Ger', '[A]')] df = spark.createDataFrame(raw_data, schema=['Order_Number', 'Country', 'Product']) # 预处理:转字符串为列表,过滤不足2件的订单 str_to_list_udf = udf(lambda x: literal_eval(x), ArrayType(StringType())) df = df.withColumn('Product', str_to_list_udf(col('Product'))) df = df.filter(size(col('Product')) >=2).cache() result_list = [] for comb_len in range(2, 8): # 生成对应长度的组合、炸开、分组统计 temp_df = df.withColumn('Combinations', comb_udf(sort_array(col('Product')), lit(comb_len))) temp_df = temp_df.select('Country', explode(col('Combinations')).alias('Product_Combination')) count_df = temp_df.groupBy('Country', 'Product_Combination').count().withColumnRenamed('count', 'Occurrence') count_df = count_df.withColumn('Product_Combinations', lit(comb_len)) result_list.append(count_df) # 合并所有结果,可写入Hive/分布式文件等存储 main_dataframe = result_list[0] for df in result_list[1:]: main_dataframe = main_dataframe.unionByName(df) # 结果量不大时可转成Pandas格式使用 # main_pd = main_dataframe.toPandas()
内容的提问来源于stack exchange,提问作者R overflow
相关产品推荐
相关产品推荐

