Python Pandas批量处理多个Excel文件,包括处理、统计和改进思路

处理目标:读取800多个excel中存储的各个城市一段时间的企业信息(每个城市都至少有一个excel的数据),统计每个城市2012-2023年每年各个二级制造业的企业数量

数据大小:800多个excel,共计45GB大小,单个excel大小在1MB-250MB之间

需求分析:由于需要二级制造业和年份两个维度,加上excel中的行和列,不难联想到pandas中的Dataframe;除此之外还需要考虑到大量数据下,普通性能的笔记本要如何简化处理流程,缩短程序的运行时间,字符串的处理和输入、处理、输出的细节;最后代码编写成功后需要先对单个excel进行测试,再对多个excel进行测试,最后加上一些输出信息(为了监控程序的运行进度),再对整体数据跑,最终得出结果

程序设计:为了缩短程序的运行时间,所以先将所有需要的数据在内存中处理好,最后一次性输出到excel中;按照信息专家的设计模式来说,年份和制造子行业的信息都是城市所拥有的信息,所以需要城市类;其次还需要一些筛选符合条件的数据的函数;以及处理单个excel的函数和持久化存储的函数

先看处理单个excel的函数,读入excel之前就要确定excel的列有两种情况,所以先用

pd.read_excel(file_path, engine='openpyxl', nrows=1).columns.tolist()

读取出列并存为列表,将分成两中处理方式,在将excel中的数据读入时,有个很有效提高效率的方式,就是只读取需要的列,read_excel函数的usercols参数就起到这个作用,只有它含有的列才会被读入存储为dataframe;filtered_df生成的方式是原df符合要求的数据才会被保留,.str[:4]指只取前4位,.fillna('0') 指的是空置添0 ,.astype(int)指的是转换成int型变量,.between(2012,2023) 当前列在这个范围中的数据才会被保留,&连接下一个条件,当条件被写成函数时,要用df[column].apply(function_name)来应用函数,数据会被作为参数传入

再按照行业分类和年份作为列和行创建元素全为0的Dataframe,对其进行初始化;对filtered_df逐行遍历按照其中的一列数据为行,一列数据为列的原则对应在result_df上计数,最后返回result_df,这就是处理单个excel的逻辑

# 对每一个excel进行处理
def data_analyse(file_path):
    print(file_path)
    cols = pd.read_excel(file_path, engine='openpyxl', nrows=1).columns.tolist()
    # 存在二级行业分类的excel
    if '二级行业分类' in cols:
        df = pd.read_excel(file_path, engine='openpyxl', usecols=['成立日期', '二级行业分类'])  # 只取两列
        # 只要12-23年和31个制造子行业的数据
        filtered_df = df[
            df['成立日期'].str[:4].fillna('0').astype(int).between(2012, 2023) & df['二级行业分类'].apply(
                is_manufacture_industry)]
        # 计算企业总数量
        count = len(filtered_df)
        # 建立行业为col和年份为index且初始元素都为0的dataframe
        index_years = list(range(2012, 2024))
        columns_names = manufacturing_industries
        result_df = pd.DataFrame(0, index=index_years, columns=columns_names)
        # 遍历符合要求的每行数据并用计数df进行计数
        for index, row in filtered_df.iterrows():
            year = int(row['成立日期'][:4])
            sub_industry = row['二级行业分类']
            result_df.at[year, sub_industry] += 1
        # 对result_df的每一行求和,计算当前城市的年度制造业总量
        result_df['年度制造业总量'] = result_df.sum(axis=1)
        return result_df, count
    # 没有二级行业分类只有所属行业的excel
    else:
        df = pd.read_excel(file_path, engine='openpyxl', usecols=['成立日期', '所属行业'])  # 只取两列
        # 只要12-23年和31个制造子行业的数据
        filtered_df = df[
            df['成立日期'].str[:4].fillna('0').astype(int).between(2012, 2023) & df['所属行业'].apply(
                is_manufacture_industry)]
        # 计算企业总数量
        count = len(filtered_df)
        # 建立行业为col和年份为index且初始元素都为0的dataframe
        index_years = list(range(2012, 2024))
        columns_names = manufacturing_industries
        result_df = pd.DataFrame(0, index=index_years, columns=columns_names)
        # 遍历符合要求的每行数据并用计数df进行计数
        for index, row in filtered_df.iterrows():
            year = int(row['成立日期'][:4])
            sub_industry = row['所属行业']
            result_df.at[year, sub_industry] += 1
        # 对result_df的每一行求和,计算当前城市的年度制造业总量
        result_df['年度制造业总量'] = result_df.sum(axis=1)
        return result_df, count

再看主程序

city_flag是一个开关变量,其作用是判断相邻的两个excel是否是同一个城市的数据,判断的函数是check_cities,其中对excel文件名进行判断,使用全局变量last_city记录上一个excel的数据所属城市,若城市相同则对两个excel的result_df的对应元素相加,使用city_merge函数实现;若不同则创建新的城市对象加入城市列表中,最后完成之后进行持久化存储

if __name__ == '__main__':
    # 用以区别是否为一同城市的不同数据表
    city_flag = False  # 默认是不同的城市
    last_city = ''
    city_list = []  # 所有城市对象的容器
    directory_path = r'G:\中国工商注册企业全量信息(2023.9更新)\全国数据'
    all_files = os.listdir(directory_path)

    # 开始循环处理目录中的文件
    for file in all_files:
        # 检查城市是否已经有存在的数据了,并调整city_flag
        last_city = check_cities(file)
        # 文件绝对路径
        file_path = os.path.join(directory_path, file)
        # 数量矩阵
        result_df, count = data_analyse(file_path)
        if city_flag:  # 不同的excel,相同的城市,进行df对应元素相加
            city_merge(result_df, count)
        else:
            # 新的城市,创建对象,并加入城市列表
            temp_city = City(last_city, result_df, count)
            city_list.append(temp_city)

    # 进行excel结果输出
    save_as_excel()
    print('done!')

check_cities函数,对城市名称进行检查,并更新last_city;提取城市名时使用了正则表达式,表达式会匹配所有的汉字,返回值为更新后的last_city

# 城市名称检查,返回值为该次的城市名,将作为下一次检查的lastcity
def check_cities(file):
    global city_flag  # 声明全局变量
    global last_city
    # 提取城市名
    simplified_name = ''.join(re.findall(r'[\u4e00-\u9fa5]', file))
    if last_city != simplified_name:
        city_flag = False
        last_city = simplified_name
    else:
        city_flag = True
    return last_city

对相同的城市数据进行合并,若有相同的城市,说明城市列表中最后一个元素与当前城市相同,所以对city_list的最后一个元素的result_df进行修改,加上当前的result_df,就完成了合并

# 相同城市,对应元素合并
def city_merge(result_df, count):
    global city_list
    city_list[-1].result_df = city_list[-1].result_df + result_df  # 最新加入的城市的dataframe和其后续的excel表的df对应元素相加
    city_list[-1].count = city_list[-1].count + count

持久化存存储结果

最后的内存中的结果是一个city_list,主要是一些格式上的要求:dataframe的前一行要有city_name,且只有第一个dataframe需要列名(因为excel中只有第一行有列名);主要用到两种写法:1.ws.cell,直接对单元格进行写入  2.dataframe.to_excel函数可以整体的将dataframe写入excel

# 存储到excel中
def save_as_excel():
    with pd.ExcelWriter('result.xlsx', engine='openpyxl') as writer:
        startrow = 0
        for idx, city in enumerate(city_list):

            # 将city_name写入到当前的startrow位置
            ws = writer.sheets['Sheet1'] if 'Sheet1' in writer.sheets else None
            if not ws:
                ws = writer.book.create_sheet('Sheet1')
            ws.cell(row=startrow + 1, column=1, value=city.city_name)

            # 对每个城市的12-23年11年制造业总量求和
            # ws.cell(row=startrow + 1, column=2, value='2012-2023年制造业总量')
            # ws.cell(row=startrow + 1, column=3, value=city.count)

            # 如果是第一个DataFrame,写入header
            if idx == 0:
                header = True
            else:
                header = False

            # 更新startrow为city_name之后的行,并将DataFrame写入到这个位置
            startrow += 1
            city.result_df.to_excel(writer, sheet_name='Sheet1', startrow=startrow, index=True, header=header)

            # 更新下一个dataframe的起始行,包括DataFrame本身的长度和一个额外的行为下一个city_name预留的位置
            startrow += city.result_df.shape[0] + 1

所有源码:

import os
import re
import pandas as pd

# 制造业二级分类
manufacturing_industries = ['农副食品加工业', '食品制造业', '酒、饮料和精制茶制造业', '烟草制品业', '纺织业',
                            '纺织服装、服饰业', '皮革、毛皮、羽毛及其制品和制鞋业', '木材加工和木、竹、藤、棕、草制品业',
                            '家具制造业', '造纸和纸制品业', '印刷和记录媒介复制业',
                            '文教、工美、体育和娱乐用品制造业', '石油、煤炭及其他燃料加工业',
                            '化学原料和化学制品制造业', '医药制造业', '化学纤维制造业', '橡胶和塑料制品业',
                            '非金属矿物制品业', '黑色金属冶炼和压延加工业', '有色金属冶炼和压延加工业',
                            '金属制品业', '通用设备制造业', '专用设备制造业', '汽车制造业',
                            '铁路、船舶、航空航天和其他运输设备制造业', '电气机械和器材制造业',
                            '计算机、通信和其他电子设备制造业', '仪器仪表制造业', '其他制造业',
                            '废弃资源综合利用业', '金属制品、机械和设备修理业']


# 城市类
class City:
    def __init__(self, city_name, result_df, count):
        self.city_name = city_name
        self.result_df = result_df
        self.result_df.index.name = city_name  # 给这个城市的df标注城市名称
        self.count = count  # 12-23年该城市的制造业企业总数


# 判断时候为制造业,返回值为布尔值
def is_manufacture_industry(value):
    # 31个制造子行业
    manufacturing_industries = ['农副食品加工业', '食品制造业', '酒、饮料和精制茶制造业', '烟草制品业', '纺织业',
                                '纺织服装、服饰业', '皮革、毛皮、羽毛及其制品和制鞋业', '木材加工和木、竹、藤、棕、草制品业',
                                '家具制造业', '造纸和纸制品业', '印刷和记录媒介复制业',
                                '文教、工美、体育和娱乐用品制造业', '石油、煤炭及其他燃料加工业',
                                '化学原料和化学制品制造业', '医药制造业', '化学纤维制造业', '橡胶和塑料制品业',
                                '非金属矿物制品业', '黑色金属冶炼和压延加工业', '有色金属冶炼和压延加工业',
                                '金属制品业', '通用设备制造业', '专用设备制造业', '汽车制造业',
                                '铁路、船舶、航空航天和其他运输设备制造业', '电气机械和器材制造业',
                                '计算机、通信和其他电子设备制造业', '仪器仪表制造业', '其他制造业',
                                '废弃资源综合利用业', '金属制品、机械和设备修理业']
    return value in manufacturing_industries


# 对每一个excel进行处理
def data_analyse(file_path):
    print(file_path)
    cols = pd.read_excel(file_path, engine='openpyxl', nrows=1).columns.tolist()
    # 存在二级行业分类的excel
    if '二级行业分类' in cols:
        df = pd.read_excel(file_path, engine='openpyxl', usecols=['成立日期', '二级行业分类'])  # 只取两列
        # 只要12-23年和31个制造子行业的数据
        filtered_df = df[
            df['成立日期'].str[:4].fillna('0').astype(int).between(2012, 2023) & df['二级行业分类'].apply(
                is_manufacture_industry)]
        # 计算企业总数量
        count = len(filtered_df)
        # 建立行业为col和年份为index且初始元素都为0的dataframe
        index_years = list(range(2012, 2024))
        columns_names = manufacturing_industries
        result_df = pd.DataFrame(0, index=index_years, columns=columns_names)
        # 遍历符合要求的每行数据并用计数df进行计数
        for index, row in filtered_df.iterrows():
            year = int(row['成立日期'][:4])
            sub_industry = row['二级行业分类']
            result_df.at[year, sub_industry] += 1
        # 对result_df的每一行求和,计算当前城市的年度制造业总量
        result_df['年度制造业总量'] = result_df.sum(axis=1)
        return result_df, count
    # 没有二级行业分类只有所属行业的excel
    else:
        df = pd.read_excel(file_path, engine='openpyxl', usecols=['成立日期', '所属行业'])  # 只取两列
        # 只要12-23年和31个制造子行业的数据
        filtered_df = df[
            df['成立日期'].str[:4].fillna('0').astype(int).between(2012, 2023) & df['所属行业'].apply(
                is_manufacture_industry)]
        # 计算企业总数量
        count = len(filtered_df)
        # 建立行业为col和年份为index且初始元素都为0的dataframe
        index_years = list(range(2012, 2024))
        columns_names = manufacturing_industries
        result_df = pd.DataFrame(0, index=index_years, columns=columns_names)
        # 遍历符合要求的每行数据并用计数df进行计数
        for index, row in filtered_df.iterrows():
            year = int(row['成立日期'][:4])
            sub_industry = row['所属行业']
            result_df.at[year, sub_industry] += 1
        # 对result_df的每一行求和,计算当前城市的年度制造业总量
        result_df['年度制造业总量'] = result_df.sum(axis=1)
        return result_df, count


# 城市名称检查,返回值为该次的城市名,将作为下一次检查的lastcity
def check_cities(file):
    global city_flag  # 声明全局变量
    global last_city
    # 提取城市名
    simplified_name = ''.join(re.findall(r'[\u4e00-\u9fa5]', file))
    if last_city != simplified_name:
        city_flag = False
        last_city = simplified_name
    else:
        city_flag = True
    return last_city


# 相同城市,对应元素合并
def city_merge(result_df, count):
    global city_list
    city_list[-1].result_df = city_list[-1].result_df + result_df  # 最新加入的城市的dataframe和其后续的excel表的df对应元素相加
    city_list[-1].count = city_list[-1].count + count


# 存储到excel中
def save_as_excel():
    with pd.ExcelWriter('result.xlsx', engine='openpyxl') as writer:
        startrow = 0
        for idx, city in enumerate(city_list):

            # 将city_name写入到当前的startrow位置
            ws = writer.sheets['Sheet1'] if 'Sheet1' in writer.sheets else None
            if not ws:
                ws = writer.book.create_sheet('Sheet1')
            ws.cell(row=startrow + 1, column=1, value=city.city_name)

            # 对每个城市的12-23年11年制造业总量求和
            # ws.cell(row=startrow + 1, column=2, value='2012-2023年制造业总量')
            # ws.cell(row=startrow + 1, column=3, value=city.count)

            # 如果是第一个DataFrame,写入header
            if idx == 0:
                header = True
            else:
                header = False

            # 更新startrow为city_name之后的行,并将DataFrame写入到这个位置
            startrow += 1
            city.result_df.to_excel(writer, sheet_name='Sheet1', startrow=startrow, index=True, header=header)

            # 更新下一个dataframe的起始行,包括DataFrame本身的长度和一个额外的行为下一个city_name预留的位置
            startrow += city.result_df.shape[0] + 1


if __name__ == '__main__':
    # 用以区别是否为一同城市的不同数据表
    city_flag = False  # 默认是不同的城市
    last_city = ''
    city_list = []  # 所有城市对象的容器
    directory_path = r'G:\中国工商注册企业全量信息(2023.9更新)\全国数据'
    all_files = os.listdir(directory_path)

    # 开始循环处理目录中的文件
    for file in all_files:
        # 检查城市是否已经有存在的数据了,并调整city_flag
        last_city = check_cities(file)
        # 文件绝对路径
        file_path = os.path.join(directory_path, file)
        # 数量矩阵
        result_df, count = data_analyse(file_path)
        if city_flag:  # 不同的excel,相同的城市,进行df对应元素相加
            city_merge(result_df, count)
        else:
            # 新的城市,创建对象,并加入城市列表
            temp_city = City(last_city, result_df, count)
            city_list.append(temp_city)

    # 进行excel结果输出
    save_as_excel()
    print('done!')
    #   进行 时间优化,然后开始运行

改进思路:

对于excel的统计尤其是数据量比较大的excel,实际上是CPU密集型的;本程序单核运行,最后完成这45个g的数据的执行时间是20小时左右;CPU密集型的操作适合使用多进程编程来充分利用计算机的性能

用一个简单的多进程例子展示使用python的multiprocessing库来完成多进程编程

在进程函数中,由于使用了共享数据结构,所以在将结果添加到共享数据结构时,要使用.get_lock()方法加锁,来保证当某一个进程向共享数据结构中添加元素时,其他进程不能同时对其进行操作

import multiprocessing

# 步骤 2:定义一个函数,接受参数并执行任务
def process_function(arg1, shared_list):
    # 在这里执行任务,可以使用参数 arg1
    result = arg1 * 2

    # 将结果添加到共享列表
    with shared_list.get_lock():
        shared_list.append(result)

if __name__ == "__main__":
    # 步骤 1:导入 multiprocessing 模块

    # 步骤 3:创建一个共享列表
    manager = multiprocessing.Manager()
    shared_list = manager.list()

    # 步骤 4:创建一个进程池
    num_processes = multiprocessing.cpu_count()
    pool = multiprocessing.Pool(processes=num_processes)

    # 步骤 5:使用进程池提交任务,传递参数和共享列表
    arg1_values = [1, 2, 3, 4, 5]

    # 使用 partial 函数创建一个包装函数,传递共享列表作为参数
    from functools import partial
    process_function_with_shared_list = partial(process_function, shared_list=shared_list)

    # 使用 map 方法执行任务
    pool.map(process_function_with_shared_list, arg1_values)

    # 步骤 6:等待所有任务完成,关闭进程池
    pool.close()
    pool.join()

    # 打印共享列表的内容
    print("Shared List:", shared_list)

物联沃分享整理
物联沃-IOTWORD物联网 » Python Pandas批量处理多个Excel文件,包括处理、统计和改进思路

发表评论