import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from mlxtend.frequent_patterns import apriori, association_rules
from mlxtend.preprocessing import TransactionEncoder
import os
import gc # 垃圾回收模块
import numpy as np # 用于热力图处理
# 设置中文字体,确保图表中文正常显示
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams['figure.dpi'] = 150
plt.rcParams['axes.unicode_minus'] = False
# ----------------------
# 1. 数据加载与预处理
# ----------------------
def load_and_preprocess(attach1_path, attach2_path, attach3_path, output_dir='results'):
"""加载数据并进行预处理,使用CSV格式保存以避免Parquet依赖"""
os.makedirs(output_dir, exist_ok=True)
# 定义需要的列,只加载必要数据
attach1_cols = ['单品编码', '单品名称', '分类名称']
attach2_cols = ['销售日期', '单品编码', '销量(千克)', '销售单价(元/千克)', '销售类型', '是否打折销售']
attach3_cols = ['单品编码', '批发价格(元/千克)']
# 加载数据时指定数据类型,减少内存占用
print("正在加载数据")
try:
# 加载附件1(品类信息)
dtype_attach1 = {
'单品编码': 'category',
'单品名称': 'category',
'分类名称': 'category'
}
df1 = pd.read_excel(attach1_path, engine='openpyxl', usecols=attach1_cols, dtype=dtype_attach1)
# 加载附件3(批发价格)
dtype_attach3 = {
'单品编码': 'category',
'批发价格(元/千克)': 'float32'
}
df3 = pd.read_excel(attach3_path, engine='openpyxl', usecols=attach3_cols, dtype=dtype_attach3)
# 去重,保留最新价格
df3 = df3.drop_duplicates('单品编码', keep='last')
# 加载附件2(销售流水)
dtype_attach2 = {
'单品编码': 'category',
'销量(千克)': 'float32',
'销售单价(元/千克)': 'float32',
'销售类型': 'category',
'是否打折销售': 'category'
}
df2 = pd.read_excel(attach2_path, engine='openpyxl',
usecols=attach2_cols, dtype=dtype_attach2)
# 处理日期列
df2['销售日期'] = pd.to_datetime(df2['销售日期'], errors='coerce')
df2 = df2.dropna(subset=['销售日期'])
# 保存原始数据量用于后续分析
original_count = len(df2)
print(f"原始销售记录数: {original_count}")
# 合并数据(优化内存使用)
print("开始合并数据...")
# 先合并附件1(品类信息)
df = pd.merge(df2, df1, on='单品编码', how='left')
# 删除不再需要的DataFrame并回收内存
del df1, df2
gc.collect()
# 再合并附件3(批发价格)
df = pd.merge(df, df3, on='单品编码', how='left')
del df3
gc.collect()
# 异常值处理
print("开始数据清洗...")
# 1. 处理退货异常
return_mask = df['销售类型'] == '退货'
return_outliers = df[return_mask].shape[0]
df = df[~return_mask]
print(f"删除退货记录: {return_outliers} 条")
# 2. 处理价格异常(不打折且售价低于批发价)
df['销售单价(元/千克)'] = pd.to_numeric(df['销售单价(元/千克)'], errors='coerce').fillna(0)
df['批发价格(元/千克)'] = pd.to_numeric(df['批发价格(元/千克)'], errors='coerce').fillna(0)
valid_price_mask = ~((df['是否打折销售'] == '否') &
(df['销售单价(元/千克)'] < df['批发价格(元/千克)']))
price_outliers = df[~valid_price_mask].shape[0]
df = df[valid_price_mask]
print(f"删除价格异常记录: {price_outliers} 条")
# 3. 按品类处理销量离群值(IQR法)
def remove_group_outliers(group):
if len(group) < 4: # 数据量太小时不处理
return group
q1 = group['销量(千克)'].quantile(0.25)
q3 = group['销量(千克)'].quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
return group[(group['销量(千克)'] >= lower) & (group['销量(千克)'] <= upper)]
# 按分类名称分组处理
has_category = df['分类名称'].notna()
# 添加observed=True参数解决FutureWarning
df_with_category = df[has_category].groupby('分类名称', group_keys=False, observed=True).apply(remove_group_outliers)
df = pd.concat([df_with_category, df[~has_category]], ignore_index=True)
del df_with_category
gc.collect()
print(f"清洗后剩余记录数: {len(df)} 条 (保留率: {len(df)/original_count:.2%})")
# 提取时间特征
df['年份'] = df['销售日期'].dt.year.astype('int16')
df['月份'] = df['销售日期'].dt.month.astype('int8')
df['日'] = df['销售日期'].dt.day.astype('int8')
df['年月'] = df['销售日期'].dt.to_period('M') # 用于按月聚合
df['年月日'] = df['销售日期'].dt.date # 用于按日聚合
df['季节'] = df['月份'].apply(lambda x:
'冬季' if x in [12, 1, 2] else
'春季' if x in [3, 4, 5] else
'夏季' if x in [6, 7, 8] else
'秋季'
).astype('category')
df['是否周末'] = df['销售日期'].dt.weekday.isin([5, 6]).astype('int8')
# 转换为更高效的数据类型
df['分类名称'] = df['分类名称'].astype('category')
df['单品名称'] = df['单品名称'].astype('category')
# 保存清洗后的数据(使用CSV格式替代Parquet,避免依赖问题)
csv_path = os.path.join(output_dir, '清洗后销售数据.csv')
# 对于大型数据集,使用适当的参数优化CSV保存
df.to_csv(csv_path, index=False, encoding='utf-8-sig') # 使用utf-8-sig确保中文正常
# 保存样本到Excel
sample_size = min(100000, len(df))
df.sample(sample_size).to_excel(os.path.join(output_dir, '清洗后销售数据样本.xlsx'), index=False)
print(f"清洗后的数据已保存至: {csv_path}")
return df
except Exception as e:
print(f"数据加载与预处理出错: {str(e)}")
raise
# ----------------------
# 2. 分布规律分析
# ----------------------
def analyze_distribution(df, output_dir='results'):
"""分析销售量的分布规律,包括六个时间维度折线图"""
os.makedirs(os.path.join(output_dir, '分布分析图表'), exist_ok=True)
time_chart_dir = os.path.join(output_dir, '分布分析图表', '时间维度销量变化')
os.makedirs(time_chart_dir, exist_ok=True)
result_df = pd.DataFrame()
try:
# 确定销售热度前10的单品
top10_items = df.groupby('单品名称', observed=True)['销量(千克)'].sum().sort_values(ascending=False).head(10).index.tolist()
# 确定主要品类(销量前10)
top10_categories = df.groupby('分类名称', observed=True)['销量(千克)'].sum().sort_values(ascending=False).head(10).index.tolist()
# 2.1 时间维度分析 - 六个折线图
print("\n时间维度分布分析 - 生成时间序列折线图")
# 辅助函数:生成折线图
def generate_line_chart(data, x_col, y_col, hue_col, title, filename, x_label):
plt.figure(figsize=(14, 8))
sns.lineplot(x=x_col, y=y_col, hue=hue_col, data=data, linewidth=2)
plt.title(title, fontsize=15)
plt.xlabel(x_label, fontsize=12)
plt.ylabel('销量(千克)', fontsize=12)
plt.xticks(rotation=45)
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.savefig(os.path.join(time_chart_dir, filename), bbox_inches='tight')
plt.close()
# 1. 按年维度 - 按种类
yearly_category_sales = df[df['分类名称'].isin(top10_categories)].groupby(
['年份', '分类名称'], observed=True)['销量(千克)'].sum().reset_index()
generate_line_chart(
yearly_category_sales,
'年份',
'销量(千克)',
'分类名称',
'各年份不同品类销量变化',
'按年-品类销量折线图.png',
'年份'
)
# 2. 按年维度 - 按单品(只保留有数据的单品)
yearly_item_data = df[df['单品名称'].isin(top10_items)]
yearly_item_sales = yearly_item_data.groupby(
['年份', '单品名称'], observed=True)['销量(千克)'].sum().reset_index()
# 转换为字符串类型,避免category保留无效类别
yearly_item_sales['单品名称'] = yearly_item_sales['单品名称'].astype(str)
generate_line_chart(
yearly_item_sales,
'年份',
'销量(千克)',
'单品名称',
'各年份不同单品销量变化(前10)',
'按年-单品销量折线图.png',
'年份'
)
# 3. 按月维度 - 按种类
monthly_category_sales = df[df['分类名称'].isin(top10_categories)].groupby(
['年月', '分类名称'], observed=True)['销量(千克)'].sum().reset_index()
monthly_category_sales['年月'] = monthly_category_sales['年月'].astype(str) # 转换为字符串以便显示
generate_line_chart(
monthly_category_sales,
'年月',
'销量(千克)',
'分类名称',
'各月份不同品类销量变化',
'按月-品类销量折线图.png',
'月份'
)
# 4. 按月维度 - 按单品(只保留有数据的单品)
monthly_item_data = df[df['单品名称'].isin(top10_items)]
monthly_item_sales = monthly_item_data.groupby(
['年月', '单品名称'], observed=True)['销量(千克)'].sum().reset_index()
monthly_item_sales['年月'] = monthly_item_sales['年月'].astype(str)
# 转换为字符串类型,避免category保留无效类别
monthly_item_sales['单品名称'] = monthly_item_sales['单品名称'].astype(str)
generate_line_chart(
monthly_item_sales,
'年月',
'销量(千克)',
'单品名称',
'各月份不同单品销量变化(前10)',
'按月-单品销量折线图.png',
'月份'
)
# 5. 按日维度 - 按种类(使用最近1095天数据)
recent_days = 1095
df_sorted = df.sort_values('销售日期')
recent_date = df_sorted['销售日期'].iloc[-1]
cutoff_date = recent_date - pd.Timedelta(days=recent_days)
daily_data = df[(df['销售日期'] >= cutoff_date) & (df['分类名称'].isin(top10_categories))]
daily_category_sales = daily_data.groupby(
['年月日', '分类名称'], observed=True)['销量(千克)'].sum().reset_index()
generate_line_chart(
daily_category_sales,
'年月日',
'销量(千克)',
'分类名称',
f'各天不同品类销量变化',
'按日-品类销量折线图.png',
'日期'
)
# 6. 按日维度 - 按单品(只保留有数据的单品)
daily_item_data = df[(df['销售日期'] >= cutoff_date) & (df['单品名称'].isin(top10_items))]
daily_item_sales = daily_item_data.groupby(
['年月日', '单品名称'], observed=True)['销量(千克)'].sum().reset_index()
# 转换为字符串类型,避免category保留无效类别
daily_item_sales['单品名称'] = daily_item_sales['单品名称'].astype(str)
generate_line_chart(
daily_item_sales,
'年月日',
'销量(千克)',
'单品名称',
f'各天不同单品销量变化(前10)',
'按日-单品销量折线图.png',
'日期'
)
# 原有时间维度分析
print("\n时间维度分布分析")
# 年度销售量趋势
yearly_sales = df.groupby('年份')['销量(千克)'].sum().reset_index()
plt.figure(figsize=(10, 6))
sns.barplot(x='年份', y='销量(千克)', data=yearly_sales)
plt.title('各年度蔬菜总销售量')
plt.ylabel('总销量(千克)')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '分布分析图表', '年度销售量.png'))
plt.close()
# 季节分布规律
seasonal_sales = df.groupby(['年份', '季节'])['销量(千克)'].sum().reset_index()
plt.figure(figsize=(12, 6))
sns.lineplot(x='季节', y='销量(千克)', hue='年份', data=seasonal_sales)
plt.title('各季节蔬菜销售量变化')
plt.ylabel('总销量(千克)')
plt.xticks(['春季', '夏季', '秋季', '冬季'])
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '分布分析图表', '季节销售量.png'))
plt.close()
del seasonal_sales
gc.collect()
# 月度分布规律
monthly_sales = df.groupby('月份')['销量(千克)'].mean().reset_index()
plt.figure(figsize=(12, 6))
sns.barplot(x='月份', y='销量(千克)', data=monthly_sales)
plt.title('月度平均销售量分布')
plt.ylabel('平均销量(千克)')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '分布分析图表', '月度销售量.png'))
plt.close()
# 周末与工作日差异
weekday_sales = df.groupby('是否周末')['销量(千克)'].mean().reset_index()
weekday_sales['类型'] = weekday_sales['是否周末'].apply(lambda x: '周末' if x == 1 else '工作日')
plt.figure(figsize=(8, 6))
sns.barplot(x='类型', y='销量(千克)', data=weekday_sales)
plt.title('周末与工作日销售量对比')
plt.ylabel('平均销量(千克)')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '分布分析图表', '周末工作日对比.png'))
plt.close()
del weekday_sales
gc.collect()
# 2.2 品类维度分析
print("\n=== 品类维度分布分析 ===")
# 各品类销量占比
category_sales = df.groupby('分类名称', observed=True)['销量(千克)'].sum().sort_values(ascending=False).reset_index()
total_sales = category_sales['销量(千克)'].sum()
category_sales['占比(%)'] = (category_sales['销量(千克)'] / total_sales) * 100
plt.figure(figsize=(12, 8))
sns.barplot(x='占比(%)', y='分类名称', data=category_sales)
plt.title('各品类销售量占比')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '分布分析图表', '品类销量占比.png'))
plt.close()
# 只分析销量前5的品类
top_categories = category_sales.head(5)['分类名称'].tolist()
cat_seasonal = df[df['分类名称'].isin(top_categories)].groupby(
['分类名称', '季节'], observed=True)['销量(千克)'].mean().reset_index()
plt.figure(figsize=(14, 8))
sns.lineplot(x='季节', y='销量(千克)', hue='分类名称', data=cat_seasonal)
plt.title('主要品类季节性销量变化')
plt.ylabel('平均销量(千克)')
plt.xticks(['春季', '夏季', '秋季', '冬季'])
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '分布分析图表', '品类季节变化.png'))
plt.close()
del cat_seasonal
gc.collect()
# 2.3 单品维度分析
print("\n单品维度分布分析")
# 各品类下销量前三的单品
top_categories = category_sales.head(3)['分类名称'].tolist()
filtered_df = df[df['分类名称'].isin(top_categories)]
top_items = filtered_df.groupby(['分类名称', '单品名称'], observed=True)['销量(千克)'].sum().reset_index()
top_items = top_items.sort_values(['分类名称', '销量(千克)'], ascending=[True, False])
top3_per_cat = top_items.groupby('分类名称', observed=True).head(3)
plt.figure(figsize=(14, 10))
sns.barplot(x='销量(千克)', y='单品名称', hue='分类名称', data=top3_per_cat)
plt.title('主要品类下销量前三的单品')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '分布分析图表', '品类TOP3单品.png'))
plt.close()
# 汇总关键结果
result_df = pd.concat([
result_df,
pd.DataFrame({
'分析维度': ['时间', '时间', '品类', '品类'],
'指标': ['年度总销量', '季节差异', '销量占比前三品类', '单品集中度'],
'结果': [
yearly_sales.set_index('年份')['销量(千克)'].to_dict(),
f"四季销量差异显著,详情见图表",
dict(zip(category_sales.head(3)['分类名称'], category_sales.head(3)['占比(%)'])),
f"各品类TOP3单品贡献超{top3_per_cat.groupby('分类名称', observed=True)['销量(千克)'].sum().sum()/total_sales:.2%}"
]
})
], ignore_index=True)
# 保存分析结果
result_df.to_excel(os.path.join(output_dir, '分布规律分析结果.xlsx'), index=False)
print(f"分布规律分析结果已保存至: {os.path.join(output_dir, '分布规律分析结果.xlsx')}")
print(f"六个时间维度销量折线图已保存至: {time_chart_dir}")
return df, result_df
except Exception as e:
print(f"分布规律分析出错: {str(e)}")
raise
# ----------------------
# 3. 相关性分析
# ----------------------
def analyze_correlation(df, output_dir='results'):
"""分析销量与价格、时间因素的相关性,生成六个热力图:
按年、月、日三个时间维度,以及品类和单品两个方面"""
os.makedirs(os.path.join(output_dir, '相关性分析图表'), exist_ok=True)
heatmap_dir = os.path.join(output_dir, '相关性分析图表', '热力图')
os.makedirs(heatmap_dir, exist_ok=True)
try:
# 确定分析对象
# 确定销售热度前5的单品(减少热力图复杂度)
top5_items = df.groupby('单品名称', observed=True)['销量(千克)'].sum().sort_values(ascending=False).head(5).index.tolist()
# 确定主要品类(销量前6)
top6_categories = df.groupby('分类名称', observed=True)['销量(千克)'].sum().sort_values(ascending=False).head(6).index.tolist()
# 辅助函数:生成热力图
def generate_heatmap(corr_matrix, title, filename, figsize=(10, 8)):
plt.figure(figsize=figsize)
sns.heatmap(
corr_matrix,
annot=True,
cmap='coolwarm',
fmt='.2f',
vmin=-1,
vmax=1,
cbar_kws={'label': '相关系数'}
)
plt.title(title)
plt.tight_layout()
plt.savefig(os.path.join(heatmap_dir, filename), bbox_inches='tight')
plt.close()
# 3.1 按年维度分析
print("\n=== 按年维度相关性分析 ===")
# 3.1.1 品类间年度销量相关性热力图
yearly_category_matrix = df[df['分类名称'].isin(top6_categories)].pivot_table(
index='年份',
columns='分类名称',
values='销量(千克)',
aggfunc='sum'
).fillna(0)
yearly_category_corr = yearly_category_matrix.corr()
generate_heatmap(
yearly_category_corr,
f'年度维度下{len(top6_categories)}个主要品类销量相关性热力图',
'年度-品类相关性热力图.png'
)
# 3.1.2 单品间年度销量相关性热力图
# 只筛选top5单品的数据
yearly_item_data = df[df['单品名称'].isin(top5_items)]
yearly_item_matrix = yearly_item_data.pivot_table(
index='年份',
columns='单品名称',
values='销量(千克)',
aggfunc='sum'
).fillna(0)
# 确保只包含top5单品
yearly_item_matrix = yearly_item_matrix.reindex(columns=top5_items)
yearly_item_corr = yearly_item_matrix.corr()
generate_heatmap(
yearly_item_corr,
f'年度维度下{len(top5_items)}个热销单品销量相关性热力图',
'年度-单品相关性热力图.png'
)
# 3.2 按月维度分析
print("\n=== 按月维度相关性分析 ===")
# 3.2.1 品类间月度销量相关性热力图
monthly_category_matrix = df[df['分类名称'].isin(top6_categories)].pivot_table(
index='年月',
columns='分类名称',
values='销量(千克)',
aggfunc='sum'
).fillna(0)
monthly_category_corr = monthly_category_matrix.corr()
generate_heatmap(
monthly_category_corr,
f'月度维度下{len(top6_categories)}个主要品类销量相关性热力图',
'月度-品类相关性热力图.png'
)
# 3.2.2 单品间月度销量相关性热力图
# 只筛选top5单品的数据
monthly_item_data = df[df['单品名称'].isin(top5_items)]
monthly_item_matrix = monthly_item_data.pivot_table(
index='年月',
columns='单品名称',
values='销量(千克)',
aggfunc='sum'
).fillna(0)
# 确保只包含top5单品
monthly_item_matrix = monthly_item_matrix.reindex(columns=top5_items)
monthly_item_corr = monthly_item_matrix.corr()
generate_heatmap(
monthly_item_corr,
f'月度维度下{len(top5_items)}个热销单品销量相关性热力图',
'月度-单品相关性热力图.png'
)
# 3.3 按日维度分析(统一使用最近1095天数据)
print("\n=== 按日维度相关性分析 ===")
# 统一使用1095天数据
recent_days = 1095
# 确定时间范围
df_sorted = df.sort_values('销售日期')
recent_date = df_sorted['销售日期'].iloc[-1]
cutoff_date = recent_date - pd.Timedelta(days=recent_days)
# 3.3.1 品类间每日销量相关性热力图(使用1095天)
daily_category_data = df[(df['销售日期'] >= cutoff_date) & (df['分类名称'].isin(top6_categories))]
daily_category_matrix = daily_category_data.pivot_table(
index='年月日',
columns='分类名称',
values='销量(千克)',
aggfunc='sum'
).fillna(0)
daily_category_corr = daily_category_matrix.corr()
generate_heatmap(
daily_category_corr,
f'各天{len(top6_categories)}个主要品类销量相关性热力图',
'每日-品类相关性热力图.png'
)
# 3.3.2 单品间每日销量相关性热力图(使用1095天)
daily_item_data = df[(df['销售日期'] >= cutoff_date) & (df['单品名称'].isin(top5_items))]
daily_item_matrix = daily_item_data.pivot_table(
index='年月日',
columns='单品名称',
values='销量(千克)',
aggfunc='sum'
).fillna(0)
# 确保只包含top5单品
daily_item_matrix = daily_item_matrix.reindex(columns=top5_items)
daily_item_corr = daily_item_matrix.corr()
generate_heatmap(
daily_item_corr,
f'各天{len(top5_items)}个热销单品销量相关性热力图',
'每日-单品相关性热力图.png'
)
# 3.4 价格与销量的相关性
print("\n=== 价格与销量相关性分析 ===")
# 对大型数据集使用样本
sample_ratio = 0.3 if len(df) > 100000 else 1.0
sample_df = df.sample(frac=sample_ratio, random_state=42)
# 计算各单品的平均价格和销量
item_stats = sample_df.groupby('单品编码', observed=True).agg({
'销量(千克)': 'mean',
'销售单价(元/千克)': 'mean',
'批发价格(元/千克)': 'mean'
}).reset_index().dropna()
# 计算相关系数
price_corr = item_stats[['销量(千克)', '销售单价(元/千克)']].corr().iloc[0, 1]
wholesale_corr = item_stats[['销量(千克)', '批发价格(元/千克)']].corr().iloc[0, 1]
print(f"销量与销售单价相关系数: {price_corr:.4f}")
print(f"销量与批发价格相关系数: {wholesale_corr:.4f}")
# 绘制散点图
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
sns.scatterplot(x='销售单价(元/千克)', y='销量(千克)', data=item_stats, alpha=0.6, s=30)
sns.regplot(x='销售单价(元/千克)', y='销量(千克)', data=item_stats, scatter=False, color='red')
plt.title(f'销量与销售单价 (r={price_corr:.4f})')
plt.subplot(1, 2, 2)
sns.scatterplot(x='批发价格(元/千克)', y='销量(千克)', data=item_stats, alpha=0.6, s=30)
sns.regplot(x='批发价格(元/千克)', y='销量(千克)', data=item_stats, scatter=False, color='red')
plt.title(f'销量与批发价格 (r={wholesale_corr:.4f})')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '相关性分析图表', '价格与销量相关性.png'))
plt.close()
# 3.5 时间因素与销量的相关性
print("\n=== 时间因素与销量相关性分析 ===")
time_corr = sample_df[['月份', '是否周末', '销量(千克)']].corr()
print("时间因素与销量的相关系数矩阵:")
print(time_corr)
# 绘制时间因素热力图
plt.figure(figsize=(8, 6))
sns.heatmap(time_corr, annot=True, cmap='coolwarm', vmin=-1, vmax=1)
plt.title('时间因素与销量的相关性热力图')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '相关性分析图表', '时间与销量相关性.png'))
plt.close()
# 清理内存
del sample_df, daily_item_matrix, monthly_item_matrix, yearly_item_matrix
del daily_category_matrix, monthly_category_matrix, yearly_category_matrix
gc.collect()
# 3.6 不同品类的价格弹性
top_categories = df.groupby('分类名称', observed=True)['销量(千克)'].sum().sort_values(ascending=False).head(3).index
elasticity_df = pd.DataFrame()
for cat in top_categories:
cat_sample = df[df['分类名称'] == cat].sample(frac=0.5, random_state=42)
corr = cat_sample[['销量(千克)', '销售单价(元/千克)']].corr().iloc[0, 1]
elasticity_df = pd.concat([elasticity_df, pd.DataFrame({
'品类': [cat],
'价格弹性系数': [corr]
})], ignore_index=True)
plt.figure(figsize=(10, 6))
sns.barplot(x='价格弹性系数', y='品类', data=elasticity_df)
plt.axvline(x=0, color='gray', linestyle='--')
plt.title('主要品类的价格弹性系数')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, '相关性分析图表', '品类价格弹性.png'))
plt.close()
# 汇总相关性分析结果
corr_results = pd.DataFrame({
'相关因素': [
'销量与销售单价',
'销量与批发价格',
'销量与月份',
'销量与是否周末'
],
'相关系数': [
price_corr,
wholesale_corr,
time_corr.loc['销量(千克)', '月份'],
time_corr.loc['销量(千克)', '是否周末']
],
'相关性描述': [
'负相关' if price_corr < 0 else '正相关',
'负相关' if wholesale_corr < 0 else '正相关',
'负相关' if time_corr.loc['销量(千克)', '月份'] < 0 else '正相关',
'负相关' if time_corr.loc['销量(千克)', '是否周末'] < 0 else '正相关'
]
})
# 合并品类弹性系数
corr_results = pd.concat([corr_results, elasticity_df.rename(columns={'品类': '相关因素', '价格弹性系数': '相关系数'})], ignore_index=True)
# 保存相关性分析结果
corr_results.to_excel(os.path.join(output_dir, '相关性分析结果.xlsx'), index=False)
print(f"相关性分析结果已保存至: {os.path.join(output_dir, '相关性分析结果.xlsx')}")
print(f"六个相关性热力图已保存至: {heatmap_dir}")
return corr_results
except Exception as e:
print(f"相关性分析出错: {str(e)}")
raise
# ----------------------
# 4. 关联规则挖掘(基于Apriori算法)
# ----------------------
def analyze_association_rules(df, output_dir='results', min_support=0.05, min_confidence=0.2, sample_ratio=0.2):
"""使用Apriori算法挖掘单品之间的关联销售规则,优化内存使用"""
print("\n关联规则挖掘(基于Apriori算法)")
os.makedirs(os.path.join(output_dir, '关联规则分析'), exist_ok=True)
assoc_dir = os.path.join(output_dir, '关联规则分析')
try:
# 对大型数据集使用更小的样本
if len(df) > 100000:
print(f"使用{sample_ratio*100}%的样本进行关联规则挖掘以节省内存")
df_sample = df.sample(frac=sample_ratio, random_state=42)
else:
df_sample = df
# 构建交易列表:按销售日期分组,1分钟内的购买视为同一交易
# 首先确保销售日期是datetime类型
df_sample['销售日期'] = pd.to_datetime(df_sample['销售日期'])
# 按1分钟窗口分组
df_sample = df_sample.sort_values('销售日期')
# 创建1分钟间隔的交易ID
time_diff = df_sample['销售日期'].diff().dt.total_seconds() > 60
df_sample['交易ID'] = time_diff.cumsum()
# 按交易ID分组,收集单品编码
basket_data = df_sample.groupby('交易ID', observed=True)['单品编码'].unique().reset_index()
transactions = basket_data['单品编码'].apply(list).tolist()
print(f"共构建 {len(transactions)} 个交易样本(1分钟内购买视为同一交易)")
# 过滤高频单品(提高阈值以减少商品数量)
min_transactions = max(5, int(len(transactions) * 0.02)) # 至少出现在2%的交易中或至少5次
item_counts = pd.Series([item for transaction in transactions for item in transaction]).value_counts()
frequent_items = item_counts[item_counts >= min_transactions].index.tolist()
transactions_filtered = [[item for item in trans if item in frequent_items] for trans in transactions]
# 只保留包含至少2个商品的交易
transactions_filtered = [trans for trans in transactions_filtered if len(trans) >= 2]
print(f"过滤后保留 {len(frequent_items)} 个高频单品,{len(transactions_filtered)} 个有效交易(每个交易至少包含2个商品)")
# 如果有效交易太少,直接返回
if len(transactions_filtered) < 100:
print("有效交易样本不足,无法进行关联规则挖掘")
return None
# Apriori算法步骤1:转换为交易矩阵(使用稀疏矩阵优化内存)
te = TransactionEncoder()
te_ary = te.fit(transactions_filtered).transform(transactions_filtered, sparse=True)
basket_df = pd.DataFrame.sparse.from_spmatrix(te_ary, columns=te.columns_)
# Apriori算法步骤2:挖掘频繁项集
try:
# 生成频繁项集:先找1-项集,再基于此生成高阶项集
frequent_itemsets = apriori(
basket_df,
min_support=min_support,
use_colnames=True,
max_len=3, # 限制最大项集长度为3
low_memory=True
)
frequent_itemsets['item_count'] = frequent_itemsets['itemsets'].apply(lambda x: len(x))
except MemoryError:
print("内存不足,尝试降低支持度阈值...")
frequent_itemsets = apriori(
basket_df,
min_support=min_support * 0.7, # 降低支持度阈值
use_colnames=True,
max_len=2, # 进一步限制最大项集长度为2
low_memory=True
)
frequent_itemsets['item_count'] = frequent_itemsets['itemsets'].apply(lambda x: len(x))
# 重命名频繁项集的列名为中文
frequent_itemsets_cn = frequent_itemsets.rename(columns={
'support': '支持度',
'itemsets': '项集',
'item_count': '项集长度'
})
# 转换项集中的单品编码为单品名称
item_name_map = dict(zip(df['单品编码'], df['单品名称']))
def items_to_names(items):
return '∩'.join([str(item_name_map.get(item, item)) for item in items])
frequent_itemsets_cn['项集'] = frequent_itemsets_cn['项集'].apply(items_to_names)
# 保存频繁项集
frequent_itemsets_cn.to_excel(os.path.join(assoc_dir, '频繁项集.xlsx'), index=False)
print(f"挖掘到 {len(frequent_itemsets_cn)} 个频繁项集,已保存至频繁项集.xlsx")
# 可视化不同长度的频繁项集分布
plt.figure(figsize=(8, 5))
sns.countplot(x='项集长度', data=frequent_itemsets_cn)
plt.title('不同长度频繁项集数量分布')
plt.xlabel('项集长度(k)')
plt.ylabel('数量')
plt.tight_layout()
plt.savefig(os.path.join(assoc_dir, '频繁项集长度分布.png'))
plt.close()
# Apriori算法步骤3:从频繁项集中生成关联规则
if len(frequent_itemsets) > 0:
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=min_confidence)
# 转换单品编码为单品名称,方便阅读
def encode_items(items):
return '∩'.join([str(item_name_map.get(item, item)) for item in items])
if not rules.empty:
# 重命名关联规则的列名为中文
rules_cn = rules.rename(columns={
'antecedents': '前项',
'consequents': '后项',
'antecedent support': '前项支持度',
'consequent support': '后项支持度',
'support': '支持度',
'confidence': '置信度',
'lift': '提升度',
'leverage': '杠杆度',
'conviction': '确信度'
})
# 转换前项和后项为单品名称
rules_cn['前项'] = rules_cn['前项'].apply(encode_items)
rules_cn['后项'] = rules_cn['后项'].apply(encode_items)
# 按提升度排序
rules_cn = rules_cn.sort_values('提升度', ascending=False)
# 保存关联规则
rules_cn.to_excel(os.path.join(assoc_dir, '关联规则.xlsx'), index=False)
print(f"挖掘到 {len(rules_cn)} 条关联规则,已保存至关联规则.xlsx")
# 可视化Top10关联规则
plt.figure(figsize=(12, 8))
top_rules = rules_cn.head(10)
sns.barplot(x='提升度', y='前项', hue='后项', data=top_rules, dodge=False)
plt.title('前几名关联规则(按提升度)')
plt.xlabel('提升度 (Lift)')
plt.ylabel('前项商品')
plt.tight_layout()
plt.savefig(os.path.join(assoc_dir, '关联规则前几名.png'))
plt.close()
# 清理内存
del basket_df, te_ary, df_sample
gc.collect()
return rules_cn
else:
print("未挖掘到满足条件的关联规则,可尝试降低min_support或min_confidence阈值")
return None
else:
print("未找到足够的频繁项集,无法生成关联规则")
return None
except Exception as e:
print(f"关联规则挖掘出错: {str(e)}")
raise
# ----------------------
# 主函数
# ----------------------
def main():
# 配置文件路径(请根据实际情况修改)
file_paths = {
'attach1': r"D:\Desktop\2023C题\附件1.xlsx",
'attach2': r"D:\Desktop\2023C题\附件2_处理后.xlsx",
'attach3': r"D:\Desktop\2023C题\附件3.xlsx"
}
output_dir = r"D:\Desktop\2023C题\第一题数据分析结果"
try:
# 1. 数据加载与预处理
df = load_and_preprocess(
file_paths['attach1'],
file_paths['attach2'],
file_paths['attach3'],
output_dir
)
# 2. 分布规律分析
df, dist_results = analyze_distribution(df, output_dir)
gc.collect()
# 3. 相关性分析
corr_results = analyze_correlation(df, output_dir)
gc.collect()
# 4. 关联规则挖掘(使用Apriori算法)
# 调整参数以获取更符合业务需求的结果
rules = analyze_association_rules(
df,
output_dir,
min_support=0.03, # 支持度阈值
min_confidence=0.2, # 置信度阈值
sample_ratio=0.3 # 样本比例
)
print("\n=== 分析完成 ===")
print(f"所有分析结果已保存至: {output_dir}")
print("包含以下关键分析内容:")
print("1. 清洗后的销售数据(CSV格式,兼容性更好)")
print("2. 销售量的时间、品类、单品分布规律及图表")
print("3. 价格与销量、时间与销量的相关性分析,包括六个时间维度的相关性热力图")
print("4. 基于Apriori算法的单品关联销售规则(频繁项集和关联规则)")
except Exception as e:
print(f"程序执行出错: {str(e)}")
if __name__ == "__main__":
main()
检查一下这段代码里面自定义函数analyze_association_rules有没有问题
最新发布