pandas 合集-数据累加,应用到指定的列

78 阅读10分钟

原始excel数据,列数据 子产品,用户类型,版本.

image.png

需要实现的效果,统计子产品,用户类型,版本的数量

image.png

一.具体的实现 定义 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 会被当作 1False 会被当作 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)