<think>我们注意到在输出中,品类平均损耗率是一个空的DataFrame,这意味着在计算平均损耗率时没有生成任何数据。
根据之前的警告:“警告: 发现 6 条损耗率记录没有匹配的分类信息”,这可能是导致空DataFrame的原因。
问题分析:
1. 损耗率数据(附件4)中的单品编码在单品信息表(附件1)中找不到对应的分类名称。
2. 在合并损耗率数据和单品信息时,由于匹配不到分类名称,这些记录被移除(我们在步骤6中移除了没有匹配分类的记录)。
3. 如果所有损耗率记录都没有匹配到分类名称,那么最终按分类名称分组时就没有数据,导致空的DataFrame。
解决方案:
1. 检查附件4中的单品编码是否在附件1中存在。
2. 如果不存在,我们需要考虑如何处理这些损耗率记录。可能的做法包括:
a) 手动检查并修正单品编码,确保它们与附件1一致。
b) 如果无法修正,则考虑从其他途径获取这些单品的分类信息。
c) 如果这些记录数量不多,且对整体分析影响不大,可以忽略(但会导致部分品类损耗率缺失)。
3. 在代码中,我们可以尝试使用其他方式匹配,比如通过单品名称(如果附件4有单品名称的话),但附件4目前只有单品编码。
然而,我们的附件4只有单品编码和损耗率,所以只能通过单品编码与附件1匹配。
修改计划:
1. 在合并损耗率数据和单品信息时,不要移除未匹配的记录,而是保留它们,并尝试用其他方式填补分类名称(如果可能)。
2. 或者,在计算平均损耗率时,允许未匹配的记录存在,但这样会导致无法按分类名称分组。
由于我们按分类名称分组计算平均损耗率,没有分类名称的记录无法被分组,因此会被忽略。所以,如果所有记录都没有匹配到分类名称,那么结果就是空的。
因此,我们必须确保损耗率记录能够匹配到分类名称。
步骤:
1. 检查附件4中的单品编码是否在附件1的单品编码中出现。
2. 如果不在,检查附件4的单品编码与附件1的单品编码是否有格式上的差异(比如空格、数据类型等)。
在代码中,我们已经将单品编码都转换为字符串类型,并且进行了左连接。我们保存了未匹配的记录到文件“未匹配分类的损耗率记录.xlsx”。
因此,我们需要检查这个文件,找出为什么没有匹配上。
但是,在程序中,我们移除了这些未匹配的记录,导致后续分组时没有数据。
为了确保有结果,我们可以考虑:
- 不移除未匹配的记录,而是分配一个默认分类(如“未知分类”),然后按分类名称分组时,这些记录会被分到“未知分类”中。这样至少可以知道哪些损耗率记录是未匹配的。
- 然后,我们可以手动处理这些“未知分类”的记录。
修改代码:
在步骤6中,不要移除未匹配的记录,而是填充一个默认分类名称,例如“未知分类”。这样分组时就会有一个“未知分类”的组。
但是,注意:题目要求按品类(分类名称)汇总,所以未知分类可能不是我们想要的。但至少可以避免整个结果为空。
另一种做法:如果未匹配的记录很少,我们可以尝试用其他方式匹配(如通过其他表),但我们的数据中没有其他信息。
因此,我建议在合并后,对于没有匹配到分类名称的记录,我们将其分类名称填充为“未知分类”,然后继续计算平均损耗率。这样,在结果中就会有一个“未知分类”的平均损耗率。我们可以随后检查这个结果。
修改代码部分(步骤6):
```python
# 合并损耗率数据和单品信息
df_loss_merged = pd.merge(
df_loss[['单品编码', '损耗率(%)']], # 只保留需要的列
df_items[['单品编码', '分类名称']].drop_duplicates(),
on='单品编码',
how='left'
)
# 填充未匹配的分类名称为“未知分类”
df_loss_merged['分类名称'] = df_loss_merged['分类名称'].fillna('未知分类')
```
这样,未匹配的记录就会被归类到“未知分类”中。
但是,我们也要注意,可能有些损耗率记录对应多个单品编码(重复),但我们已经用`drop_duplicates()`去重了。
另外,我们还需要注意,如果附件4中同一个单品编码有多条记录,那么合并后也会有多个记录,然后按分类名称分组时,会计算这些记录的平均值。
因此,修改后的代码可以避免因未匹配记录被移除而导致结果为空的情况。
但是,我们也要考虑到,可能所有记录都被归类为“未知分类”,那么结果中就只有“未知分类”一个品类。这显然不是我们想要的。所以,我们还需要检查为什么有这么多未匹配的记录。
因此,我们修改步骤6的代码,同时保留未匹配记录的检查,并输出到文件,但不再移除,而是填充为“未知分类”。
修改后的步骤6代码:
```python
print("\n" + "="*50)
print("步骤6: 按品类计算平均损耗率...")
print("="*50)
try:
# 合并损耗率数据和单品信息
df_loss_merged = pd.merge(
df_loss[['单品编码', '损耗率(%)']], # 只保留需要的列
df_items[['单品编码', '分类名称']].drop_duplicates(),
on='单品编码',
how='left'
)
print(f"损耗率数据合并后记录数: {len(df_loss_merged)}")
# 检查损耗率合并后的缺失值
missing_loss = df_loss_merged[df_loss_merged['分类名称'].isna()]
if not missing_loss.empty:
print(f"警告: 发现 {len(missing_loss)} 条损耗率记录没有匹配的分类信息")
# 将这些记录的缺失分类名称填充为“未知分类”
df_loss_merged['分类名称'] = df_loss_merged['分类名称'].fillna('未知分类')
print("已将这些未匹配记录的分类名称填充为'未知分类'")
# 保存未匹配记录以便检查
missing_loss.to_excel("未匹配分类的损耗率记录.xlsx", index=False)
# 检查损耗率值
loss_rate_stats = df_loss_merged['损耗率(%)'].describe()
print("\n损耗率统计信息:")
print(loss_rate_stats)
# 按品类分组计算平均损耗率
category_loss_rate = df_loss_merged.groupby('分类名称')['损耗率(%)'].mean().reset_index()
category_loss_rate.rename(columns={'损耗率(%)': '平均损耗率(%)'}, inplace=True)
print("\n品类平均损耗率计算完成:")
print(category_loss_rate.head())
# 检查是否有空值(在平均损耗率列)
if category_loss_rate['平均损耗率(%)'].isna().any():
print("警告: 存在平均损耗率为空的品类,将用0填充")
category_loss_rate['平均损耗率(%)'] = category_loss_rate['平均损耗率(%)'].fillna(0)
except Exception as e:
print(f"计算平均损耗率失败: {str(e)}")
raise
```
这样,即使有未匹配的记录,它们也会被归为“未知分类”,从而避免整个结果为空。
但是,我们最终需要的是已知分类的损耗率,所以我们应该在程序运行后检查“未知分类”的记录,并手动修正数据。
另外,我们注意到原始输出中,品类平均损耗率是一个空DataFrame,列是[分类名称, 平均损耗率(%)],但没有任何行。这可能是由于所有记录都没有匹配到分类名称,并且我们在之前移除了这些记录。所以使用上述修改后,结果中至少会有“未知分类”。
我们重新运行修改后的代码,然后查看结果。
注意:如果损耗率数据中没有任何一条记录匹配到分类名称,那么结果中只有“未知分类”一条记录。我们需要根据实际情况处理。
因此,我们修改代码后再次运行。
由于我们无法直接运行,这里只提供修改后的完整代码(步骤6部分修改,其余不变)。
完整代码太长,这里只重写步骤6部分,并将修改后的完整代码放在下面。
注意:由于我们之前已经将未匹配的记录填充为“未知分类”,所以分组时不会丢失记录。
修改后的完整代码(仅步骤6部分有修改):</think># 损耗率记录未匹配分类信息的分析与解决方案
从您的输出中可以看出,程序发现了6条损耗率记录没有匹配到分类信息,导致最终生成的"品类平均损耗率"是一个空DataFrame。我将详细分析这个问题并提供完整的解决方案。
## 问题分析
### 1. 损耗率记录未匹配分类信息
- 警告信息:"警告: 发现 6 条损耗率记录没有匹配的分类信息"
- 这意味着附件4(损耗率数据)中的6个单品编码无法在附件1(单品信息)中找到对应的分类名称
### 2. 导致品类平均损耗率为空的原因
- 由于部分损耗率记录无法匹配到分类信息
- 程序在计算平均损耗率时移除了这些未匹配的记录
- 最终导致没有任何记录可用于计算品类平均损耗率
## 完整解决方案
以下是增强后的代码,专门解决损耗率记录未匹配分类信息的问题:
```python
import pandas as pd
import numpy as np
import os
import warnings
warnings.filterwarnings('ignore')
def preprocess_and_aggregate_data():
"""
处理销售订单数据,按单品和日期汇总,再按品类汇总,并计算平均损耗率
"""
print("="*50)
print("步骤1: 读取原始数据...")
print("="*50)
# 读取附件1(单品信息)
df_items = pd.read_excel(r"D:\虚拟c盘\大学\竞赛\数学建模\2023年C题\C题\附件1.xlsx",
engine='openpyxl',
usecols=['单品编码', '单品名称', '分类名称'])
df_items['单品编码'] = df_items['单品编码'].astype(str)
print(f"附件1读取成功,共 {len(df_items)} 条记录")
# 读取附件2(销售数据)
df_sales = pd.read_excel(r"D:\虚拟c盘\大学\竞赛\数学建模\2023年C题\C题\附件2.xlsx",
engine='openpyxl',
usecols=['销售日期', '单品编码', '销量(千克)', '销售单价(元/千克)', '销售类型'])
df_sales['单品编码'] = df_sales['单品编码'].astype(str)
print(f"附件2读取成功,共 {len(df_sales)} 条记录")
# 读取附件4(损耗率数据)
print("\n" + "="*50)
print("步骤2: 读取并处理损耗率数据...")
print("="*50)
df_loss = pd.read_excel(r"D:\虚拟c盘\大学\竞赛\数学建模\2023年C题\C题\附件4.xlsx",
engine='openpyxl')
# 重命名列
column_rename_map = {
'小分类编码': '单品编码',
'平均损耗率(%)_小分类编码_不同值': '损耗率(%)'
}
for old_name, new_name in column_rename_map.items():
if old_name in df_loss.columns:
df_loss.rename(columns={old_name: new_name}, inplace=True)
# 确保单品编码为字符串类型
df_loss['单品编码'] = df_loss['单品编码'].astype(str)
print(f"附件4读取成功,共 {len(df_loss)} 条记录")
# 检查并处理空值
if df_loss['损耗率(%)'].isna().sum() > 0:
print(f"警告: 发现 {df_loss['损耗率(%)'].isna().sum()} 条损耗率记录为空值,已用0填充")
df_loss['损耗率(%)'] = df_loss['损耗率(%)'].fillna(0)
print("\n" + "="*50)
print("步骤3: 合并数据集并处理退货记录...")
print("="*50)
# 合并销售数据和单品信息
df_merged = pd.merge(df_sales, df_items, on='单品编码', how='left')
# 检查合并后的缺失值
missing_items = df_merged[df_merged['分类名称'].isna()]
if not missing_items.empty:
print(f"警告: 发现 {len(missing_items)} 条销售记录没有匹配的分类信息")
# 保存未匹配记录以便检查
missing_items.to_excel("未匹配分类的销售记录.xlsx", index=False)
# 从主数据中移除这些记录
df_merged = df_merged.dropna(subset=['分类名称'])
# 处理退货
df_merged['金额'] = df_merged['销量(千克)'] * df_merged['销售单价(元/千克)']
is_return = df_merged['销售类型'] == '退货'
df_merged.loc[is_return, ['金额', '销量(千克)']] *= -1
print("\n" + "="*50)
print("步骤4: 按单品和日期汇总销售数据...")
print("="*50)
# 按单品和日期分组计算
daily_item_sales = df_merged.groupby(['单品编码', '单品名称', '分类名称', '销售日期']).agg(
总销量=('销量(千克)', 'sum'),
总金额=('金额', 'sum')
).reset_index()
# 计算单品平均价格
daily_item_sales['平均价格'] = np.where(
daily_item_sales['总销量'] == 0,
0,
daily_item_sales['总金额'] / daily_item_sales['总销量']
)
print("\n" + "="*50)
print("步骤5: 按品类和日期汇总销售数据...")
print("="*50)
# 按品类和日期分组计算
daily_category_sales = daily_item_sales.groupby(['分类名称', '销售日期']).agg(
品类总销量=('总销量', 'sum'),
品类总金额=('总金额', 'sum')
).reset_index()
# 计算品类平均价格
daily_category_sales['品类平均价格'] = np.where(
daily_category_sales['品类总销量'] == 0,
0,
daily_category_sales['品类总金额'] / daily_category_sales['品类总销量']
)
print("\n" + "="*50)
print("步骤6: 按品类计算平均损耗率...")
print("="*50)
# 创建完整的分类名称列表(从销售数据中获取)
all_categories = daily_category_sales['分类名称'].unique().tolist()
print(f"销售数据中包含的分类: {all_categories}")
# 合并损耗率数据和单品信息
df_loss_merged = pd.merge(
df_loss[['单品编码', '损耗率(%)']], # 只保留需要的列
df_items[['单品编码', '分类名称']].drop_duplicates(),
on='单品编码',
how='left'
)
# 检查并处理未匹配的分类信息
missing_loss = df_loss_merged[df_loss_merged['分类名称'].isna()]
if not missing_loss.empty:
print(f"警告: 发现 {len(missing_loss)} 条损耗率记录没有匹配的分类信息")
# 保存未匹配记录以便检查
missing_loss.to_excel("未匹配分类的损耗率记录.xlsx", index=False)
# 尝试从单品名称推断分类(如果可能)
print("尝试从单品名称推断分类...")
# 获取所有可能的分类名称
category_mapping = {}
for _, row in missing_loss.iterrows():
item_code = row['单品编码']
# 尝试在销售数据中查找相同的单品编码
matched_sales = daily_item_sales[daily_item_sales['单品编码'] == item_code]
if not matched_sales.empty:
category = matched_sales['分类名称'].iloc[0]
category_mapping[item_code] = category
print(f" 单品编码 {item_code} 通过销售数据匹配到分类: {category}")
# 应用推断的分类
if category_mapping:
df_loss_merged['分类名称'] = df_loss_merged.apply(
lambda row: category_mapping.get(row['单品编码'], row['分类名称']),
axis=1
)
# 重新检查缺失值
missing_loss_after = df_loss_merged[df_loss_merged['分类名称'].isna()]
print(f" 成功匹配 {len(missing_loss) - len(missing_loss_after)} 条记录")
# 如果仍有未匹配的记录,使用默认分类
if not missing_loss_after.empty:
print(f" 仍有 {len(missing_loss_after)} 条记录未匹配,使用默认分类")
df_loss_merged['分类名称'] = df_loss_merged['分类名称'].fillna('其他分类')
# 确保所有损耗率记录都有分类
if df_loss_merged['分类名称'].isna().any():
print("警告: 仍有损耗率记录没有分类信息,使用默认分类")
df_loss_merged['分类名称'] = df_loss_merged['分类名称'].fillna('其他分类')
# 按品类分组计算平均损耗率
category_loss_rate = df_loss_merged.groupby('分类名称')['损耗率(%)'].mean().reset_index()
category_loss_rate.rename(columns={'损耗率(%)': '平均损耗率(%)'}, inplace=True)
# 确保所有销售数据中的分类都有损耗率数据
missing_categories = set(all_categories) - set(category_loss_rate['分类名称'])
if missing_categories:
print(f"警告: 以下分类在损耗率数据中缺失: {missing_categories}")
# 为缺失的分类添加默认损耗率
for category in missing_categories:
category_loss_rate = pd.concat([
category_loss_rate,
pd.DataFrame([{'分类名称': category, '平均损耗率(%)': 0.0}])
], ignore_index=True)
print("已为缺失分类添加默认损耗率0%")
print("\n" + "="*50)
print("步骤7: 保存结果到Excel文件...")
print("="*50)
output_dir = r"D:\虚拟c盘\大学\竞赛\数学建模\2023年C题\C题\结果"
os.makedirs(output_dir, exist_ok=True)
# 单品级销售数据
item_sales_path = os.path.join(output_dir, "单品级销售数据.xlsx")
daily_item_sales.to_excel(item_sales_path, index=False)
# 品类级销售数据
category_sales_path = os.path.join(output_dir, "品类级销售数据.xlsx")
daily_category_sales.to_excel(category_sales_path, index=False)
# 品类平均损耗率
loss_rate_path = os.path.join(output_dir, "品类平均损耗率.xlsx")
category_loss_rate.to_excel(loss_rate_path, index=False)
print(f"处理完成!结果已保存至: {output_dir}")
return daily_item_sales, daily_category_sales, category_loss_rate
# 主程序
if __name__ == "__main__":
try:
print("="*70)
print("开始处理销售订单数据")
print("="*70)
item_sales, category_sales, category_loss = preprocess_and_aggregate_data()
print("\n处理结果预览:")
print("\n单品级销售数据前5行:")
print(item_sales.head())
print("\n品类级销售数据前5行:")
print(category_sales.head())
print("\n品类平均损耗率:")
print(category_loss)
# 检查损耗率数据
if category_loss.empty:
print("\n警告: 品类平均损耗率为空!")
else:
print(f"\n成功计算 {len(category_loss)} 个品类的平均损耗率")
print("\n" + "="*70)
print("数据处理完成!")
print("="*70)
except Exception as e:
print("\n" + "!"*70)
print(f"程序执行出错: {str(e)}")
print("!"*70)
import traceback
traceback.print_exc()
```
## 关键解决方案与解释
### 1. 多级分类匹配策略
```python
# 尝试从单品名称推断分类(如果可能)
print("尝试从单品名称推断分类...")
# 获取所有可能的分类名称
category_mapping = {}
for _, row in missing_loss.iterrows():
item_code = row['单品编码']
# 尝试在销售数据中查找相同的单品编码
matched_sales = daily_item_sales[daily_item_sales['单品编码'] == item_code]
if not matched_sales.empty:
category = matched_sales['分类名称'].iloc[0]
category_mapping[item_code] = category
print(f" 单品编码 {item_code} 通过销售数据匹配到分类: {category}")
```
这段代码:
1. 遍历所有未匹配的损耗率记录
2. 在销售数据中查找相同单品编码的记录
3. 如果找到,使用销售数据中的分类信息
4. 建立单品编码到分类名称的映射关系
### 2. 默认分类处理
```python
# 如果仍有未匹配的记录,使用默认分类
if not missing_loss_after.empty:
print(f" 仍有 {len(missing_loss_after)} 条记录未匹配,使用默认分类")
df_loss_merged['分类名称'] = df_loss_merged['分类名称'].fillna('其他分类')
```
对于仍然无法匹配分类的记录:
1. 使用"其他分类"作为默认分类
2. 确保所有损耗率记录都有分类信息
3. 避免因部分记录缺失导致整个结果为空
### 3. 确保所有销售分类都有损耗率数据
```python
# 确保所有销售数据中的分类都有损耗率数据
missing_categories = set(all_categories) - set(category_loss_rate['分类名称'])
if missing_categories:
print(f"警告: 以下分类在损耗率数据中缺失: {missing_categories}")
# 为缺失的分类添加默认损耗率
for category in missing_categories:
category_loss_rate = pd.concat([
category_loss_rate,
pd.DataFrame([{'分类名称': category, '平均损耗率(%)': 0.0}])
], ignore_index=True)
print("已为缺失分类添加默认损耗率0%")
```
这段代码:
1. 找出销售数据中存在但损耗率数据中缺失的分类
2. 为这些分类添加默认损耗率(0%)
3. 确保最终结果包含所有销售数据中的分类
### 4. 增强的日志和错误处理
```python
print(f"销售数据中包含的分类: {all_categories}")
print(f"警告: 发现 {len(missing_loss)} 条损耗率记录没有匹配的分类信息")
print("尝试从单品名称推断分类...")
print(f" 成功匹配 {len(missing_loss) - len(missing_loss_after)} 条记录")
print(f" 仍有 {len(missing_loss_after)} 条记录未匹配,使用默认分类")
```
详细的日志输出:
1. 帮助跟踪数据处理过程
2. 明确显示匹配成功和失败的记录数
3. 提供足够的调试信息
## 解决方案工作流程
1. **数据读取与预处理**:
- 读取所有附件数据
- 统一单品编码为字符串类型
- 处理损耗率数据中的空值
2. **销售数据处理**:
- 合并销售数据和单品信息
- 处理退货记录
- 按单品和日期汇总销售数据
- 按品类和日期汇总销售数据
3. **损耗率数据处理**:
- 尝试匹配分类信息
- 对于未匹配记录,尝试从销售数据中查找分类
- 对于仍然未匹配的记录,使用"其他分类"作为默认
- 确保所有销售分类都有对应的损耗率数据
4. **结果保存与验证**:
- 保存单品级销售数据
- 保存品类级销售数据
- 保存品类平均损耗率
- 验证结果是否为空
## 可能的原因及处理方式
### 1. 单品编码不一致
- **原因**:附件1和附件4中的单品编码格式不一致
- **解决方案**:代码中已统一转换为字符串类型
### 2. 分类信息缺失
- **原因**:附件1中缺少某些单品的分类信息
- **解决方案**:尝试从销售数据中推断分类
### 3. 数据错误
- **原因**:附件4中的单品编码在附件1中不存在
- **解决方案**:使用默认分类"其他分类",并保存未匹配记录供人工检查
### 4. 分类名称不一致
- **原因**:附件1和销售数据中的分类名称不一致
- **解决方案**:从销售数据中获取所有分类名称,确保损耗率数据覆盖所有分类
这个增强版的代码应该能够解决损耗率记录未匹配分类信息的问题,并确保最终生成完整的品类平均损耗率数据。