原始excel数据,列数据 子产品,用户类型,版本.
需要实现的效果,统计子产品,用户类型,版本的数量
一.具体的实现 定义 merge_dataframe方法 接收一个dataFrame,需要排序的列表字段,需要求和的字段 ( df: pd.DataFrame, group_by_columns: List[str], sum_columns: List[str]) df: 输入 DataFrame group_by_columns: 分组列(第一个为主分组列,如'子产品';其他为需格式化的列) sum_columns: 需要累加的列(如 ['问题数量'])
1.添加问题数量
if '问题数量' not in df.columns: df['问题数量'] = 1
2.计算主要分组问题的数量
main_group_totals = df.groupby(main_group_col, as_index=False)['问题数量'].sum()
实现的效果
子产品 问题数量
0 云空间 14
3.修改问题数量的名称
main_group_totals.columns = [main_group_col, 'total_问题数量']
子产品 total_问题数量
0 云空间 14
4.合并总量并格式化主分组列
df = pd.merge(df, main_group_totals, on=main_group_col, how='left')
# df:主 DataFrame,合并操作后会保留其所有行。
# main_group_totals:要合并进去的 DataFrame,它包含每个主分组的总问题数量。
# on=main_group_col:指定合并依据的列名,main_group_col 是一个变量,代表主分组列(如 '子产品')。合并时,会比较两个 DataFrame 中该列的值,将值相同的行合并。
# how='left':指定合并方式为左连接。这意味着最终结果会保留 df 里的所有行,若 main_group_totals 中没有匹配的行,对应列会填充为 NaN。
# 10 云空间 PC 1.67.7 1 14
# 11 云空间 PC 1.67.7 1 14
# 12 云空间 移动 1.67.10 1 14
# 13 云空间 移动 1.67.10 1 14
5.格式化main_group_col
# 把 apply 方法返回的新字符串序列赋值给 DataFrame 的 main_group_col 列,从而更新该列的值。
df[main_group_col] = df.apply(
lambda x: f"{x[main_group_col]}({int(x['total_问题数量'])}个)",
axis=1
)
# 10 云空间(14个) PC 1.67.7 1 14
# 11 云空间(14个) PC 1.67.7 1 14
# 12 云空间(14个) 移动 1.67.10 1 14
# 13 云空间(14个) 移动 1.67.10 1 14
6.格式化其他需要统计的列
for col in group_by_columns[1:]:
df = self.format_column_with_count(
df=df,
group_by_main=main_group_col,
column_to_format=col,
count_column='问题数量'
)
7. 清理临时列
df.drop(columns=['total_问题数量', '问题数量'], inplace=True, errors='ignore')
完善format_column_with_count方法
循环从1开始没有计算主分组列 接收的参数
df: pd.DataFrame,
group_by_main: str, # 主分组列(如'子产品')
column_to_format: str, # 要格式化的列名(如'模块'、'产品形态')
count_column: str # 用于统计的数量列(如'问题数量')
8. 按照主分组排序
df = df.sort_values(by=[group_by_main], kind='mergesort')
df = df.copy()
# group_by_main:子产品,column_to_format:用户类型,count_column:问题数量
# 1. 按主分组排序,保持原始顺序
df = df.sort_values(by=[group_by_main], kind='mergesort')
9.创建连续的块标识,在主分组内部
df['block_id'] = ((df[group_by_main] != df[group_by_main].shift(1)) | (df[column_to_format] != df[column_to_format].shift(1)))
# shift(1):pandas 的 shift 方法,将列中的值向下移动一行。第一行的值会被填充为 NaN。
# df[group_by_main] != df[group_by_main].shift(1):比较主分组列当前行的值和上一行的值,如果不相等则返回 True,否则返回 False。
# df[column_to_format] != df[column_to_format].shift(1):比较要格式化的列当前行的值和上一行的值,如果不相等则返回 True,否则返回 False。
10.给连续相同的值分配一个块id
df['block_id'] = df['block_id'].cumsum()
# .cumsum():pandas 的累积求和方法,对布尔类型的 block_id 列进行累积求和。True 会被当作 1,False 会被当作 0。
# 这样,每当遇到 True 时,block_id 的值就会加 1,从而为每个连续相同值的块分配一个唯一的标识符。
11. 计算每个连续块问题的总和
block_counts = df.groupby(['block_id'], as_index=False)[count_column].sum()
# df.groupby(['block_id']):groupby 是 pandas DataFrame 对象的方法,用于按指定列对 DataFrame 进行分组。['block_id'] 表示依据 block_id 列的值进行分组,相同 block_id 值的行归为一组。
# as_index=False:该参数控制分组后的索引设置。设置为 False 时,分组列 block_id 不会作为新 DataFrame 的索引,而是作为普通列保留。
# [count_column]:从分组后的 DataFrame 中选取 count_column 列。count_column 是一个变量,代表用于统计数量的列,例如 '问题数量'。
# .sum():对选取的 count_column 列进行求和操作,计算每个分组内该列值的总和。
# block_id 问题数量
# 0 1 6
# 1 2 6
# 2 3 2
12. 重命名问题数量-->block_count (其实在上一步可以直接实现)
block_counts.rename(columns={count_column: 'block_count'}, inplace=True)
# block_id block_count
# 0 1 6
# 1 2 6
# 2 3 2
13. block_counts合并原始数据
df = pd.merge(df, block_counts, on='block_id', how='left')
# group_by_main:子产品,column_to_format:用户类型,count_column:问题数量
# 子产品 用户类型 版本 问题数量 total_问题数量 block_id block_count
# 0 云空间(14个) PC 1.67.10 1 14 1 12
# 1 云空间(14个) PC 1.67.10 1 14 1 12
# 2 云空间(14个) PC 1.67.10 1 14 1 12
# group_by_main:子产品,column_to_format:版本,count_column:问题数量
# 子产品 用户类型 版本 问题数量 total_问题数量 block_id block_count
# 0 云空间(14个) PC(12个) 1.67.10 1 14 1 6
# 1 云空间(14个) PC(12个) 1.67.10 1 14 1 6
# 2 云空间(14个) PC(12个) 1.67.10 1 14 1 6
14.格式化需要格式的列
df[column_to_format] = df.apply(
lambda x: f"{x[column_to_format]}({int(x['block_count'])}个)",
axis=1
)
15. 清理临时列
df.drop(columns=['block_id', 'block_count'], inplace=True)
16. 返回df
return df
完整代码
import pandas as pd
from typing import List
import datetime
class DataFrameFormatter:
@staticmethod
def format_column_with_count(
df: pd.DataFrame,
group_by_main: str, # 主分组列(如'子产品')
column_to_format: str, # 要格式化的列名(如'模块'、'产品形态')
count_column: str # 用于统计的数量列(如'问题数量')
) -> pd.DataFrame:
"""
对指定列进行格式化,添加(X个)信息。
块划分基于:主分组列 + 当前列的连续相同值(仅限当前主分组内部)
"""
if column_to_format not in df.columns:
raise ValueError(f"列 '{column_to_format}' 不存在于DataFrame中")
if count_column not in df.columns:
raise ValueError(f"列 '{count_column}' 不存在于DataFrame中")
df = df.copy()
print(f"group_by_main:{group_by_main},column_to_format:{column_to_format},count_column:{count_column}")
# group_by_main:子产品,column_to_format:用户类型,count_column:问题数量
# 1. 按主分组排序,保持原始顺序
df = df.sort_values(by=[group_by_main], kind='mergesort')
# 2. 创建连续块标识(在主分组内部)
df['block_id'] = ((df[group_by_main] != df[group_by_main].shift(1)) | (df[column_to_format] != df[column_to_format].shift(1)))
# shift(1):pandas 的 shift 方法,将列中的值向下移动一行。第一行的值会被填充为 NaN。
# df[group_by_main] != df[group_by_main].shift(1):比较主分组列当前行的值和上一行的值,如果不相等则返回 True,否则返回 False。
# df[column_to_format] != df[column_to_format].shift(1):比较要格式化的列当前行的值和上一行的值,如果不相等则返回 True,否则返回 False。
df['block_id'] = df['block_id'].cumsum()
# .cumsum():pandas 的累积求和方法,对布尔类型的 block_id 列进行累积求和。True 会被当作 1,False 会被当作 0。
# 这样,每当遇到 True 时,block_id 的值就会加 1,从而为每个连续相同值的块分配一个唯一的标识符。
# 3. 计算每个连续块的问题数量总和
block_counts = df.groupby(['block_id'], as_index=False)[count_column].sum()
# df.groupby(['block_id']):groupby 是 pandas DataFrame 对象的方法,用于按指定列对 DataFrame 进行分组。['block_id'] 表示依据 block_id 列的值进行分组,相同 block_id 值的行归为一组。
# as_index=False:该参数控制分组后的索引设置。设置为 False 时,分组列 block_id 不会作为新 DataFrame 的索引,而是作为普通列保留。
# [count_column]:从分组后的 DataFrame 中选取 count_column 列。count_column 是一个变量,代表用于统计数量的列,例如 '问题数量'。
# .sum():对选取的 count_column 列进行求和操作,计算每个分组内该列值的总和。
# block_id 问题数量
# 0 1 6
# 1 2 6
# 2 3 2
block_counts.rename(columns={count_column: 'block_count'}, inplace=True)
# block_id block_count
# 0 1 6
# 1 2 6
# 2 3 2
# 4. 合并回原始数据
df = pd.merge(df, block_counts, on='block_id', how='left')
# group_by_main:子产品,column_to_format:用户类型,count_column:问题数量
# 子产品 用户类型 版本 问题数量 total_问题数量 block_id block_count
# 0 云空间(14个) PC 1.67.10 1 14 1 12
# 1 云空间(14个) PC 1.67.10 1 14 1 12
# 2 云空间(14个) PC 1.67.10 1 14 1 12
# group_by_main:子产品,column_to_format:版本,count_column:问题数量
# 子产品 用户类型 版本 问题数量 total_问题数量 block_id block_count
# 0 云空间(14个) PC(12个) 1.67.10 1 14 1 6
# 1 云空间(14个) PC(12个) 1.67.10 1 14 1 6
# 2 云空间(14个) PC(12个) 1.67.10 1 14 1 6
# 5. 格式化列(保留原始值,添加块内统计)
df[column_to_format] = df.apply(
lambda x: f"{x[column_to_format]}({int(x['block_count'])}个)",
axis=1
)
# 6. 清理临时列
df.drop(columns=['block_id', 'block_count'], inplace=True)
return df
def merge_dataframe(self, df: pd.DataFrame, group_by_columns: List[str], sum_columns: List[str]) -> pd.DataFrame:
"""
df: 输入 DataFrame
group_by_columns: 分组列(第一个为主分组列,如'子产品';其他为需格式化的列)
sum_columns: 需要累加的列(如 ['问题数量'])
"""
if not group_by_columns:
raise ValueError("必须指定分组列")
if any(col not in df.columns for col in group_by_columns):
missing_cols = [col for col in group_by_columns if col not in df.columns]
raise ValueError(f"以下分组列不存在于DataFrame中: {', '.join(missing_cols)}")
main_group_col = group_by_columns[0]
# 添加默认问题数量列(每行为1)
if '问题数量' not in df.columns:
df['问题数量'] = 1
# 1. 先计算每个主分组的总问题数量
main_group_totals = df.groupby(main_group_col, as_index=False)['问题数量'].sum()
# 子产品 问题数量
# 0 云空间 14
main_group_totals.columns = [main_group_col, 'total_问题数量'] # 修改问题数量的名称
# 子产品 total_问题数量
# 0 云空间 14
# 2. 合并总量并格式化主分组列(使用整个主分组的总量)
df = pd.merge(df, main_group_totals, on=main_group_col, how='left')
# df:主 DataFrame,合并操作后会保留其所有行。
# main_group_totals:要合并进去的 DataFrame,它包含每个主分组的总问题数量。
# on=main_group_col:指定合并依据的列名,main_group_col 是一个变量,代表主分组列(如 '子产品')。合并时,会比较两个 DataFrame 中该列的值,将值相同的行合并。
# how='left':指定合并方式为左连接。这意味着最终结果会保留 df 里的所有行,若 main_group_totals 中没有匹配的行,对应列会填充为 NaN。
# 10 云空间 PC 1.67.7 1 14
# 11 云空间 PC 1.67.7 1 14
# 12 云空间 移动 1.67.10 1 14
# 13 云空间 移动 1.67.10 1 14
df[main_group_col] = df.apply(
lambda x: f"{x[main_group_col]}({int(x['total_问题数量'])}个)",
axis=1
)
#云空间 ==>云空间(14个) # 把 apply 方法返回的新字符串序列赋值给 DataFrame 的 main_group_col 列,从而更新该列的值。
# 3. 格式化其他分组列(使用连续块统计)
for col in group_by_columns[1:]:
df = self.format_column_with_count(
df=df,
group_by_main=main_group_col,
column_to_format=col,
count_column='问题数量'
)
# 4. 清理临时列
df.drop(columns=['total_问题数量', '问题数量'], inplace=True, errors='ignore')
# 5. 输出结果
# time_str = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
# if '子产品' in main_group_col:
# output_path = f'E:/excel-test/关键问题{time_str}排序数据.xlsx'
# sheet_name = '关键问题数据'
# else:
# output_path = f'E:/excel-test/本周TOP问题{time_str}排序数据.xlsx'
# sheet_name = '本周TOP问题数据'
# df.to_excel(output_path, sheet_name=sheet_name, index=False)
return df
if __name__ == '__main__':
excel_path = 'E:/excel-test/工作总结.xlsx'
mdf = DataFrameFormatter()
df = pd.read_excel(excel_path)
group_by_columns=['子产品','用户类型','版本']
sum_columns = ['问题数量']
fileter_df = mdf.merge_dataframe(df,group_by_columns,sum_columns)