ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python按列值拆分Excel:pandas groupby完整指南

用Python按列值拆分Excel:pandas groupby完整指南 这个需求我太熟了。月初、月底、季度末办公室里总有人在原始表上反复点“筛选”把同一列的值一个个筛出来复制到新表重命名保存再回来筛下一个。几百行数据还能忍几万行、几十个分类的时候一下午就耗进去了。用Python处理这件事本质就是三行代码的事但真正麻烦的从来不是代码本身而是你在动手前有没有把表格结构、拆分规则和输出需求想清楚。这篇文章就围绕“按某一列值拆分Excel”这件事把从需求确认、工具选型、核心代码、常见坑到性能优化完整讲一遍。无论你是刚接触Python的办公族还是已经能用pandas做简单清洗的分析师这篇都能帮你把这件小事做得更稳、更省心。1. 动手前先想清楚三件事拆哪列、怎么拆、拆完长什么样很多人拿到需求就开写代码结果代码没问题跑出来的结果却不是人想要的。问题通常出在需求没拆解清楚。按列拆分这件事表面上是“按列的值分组”但落到Excel上有三件事必须先确认。1.1 先看清你的表格结构别急着写代码拿到一个Excel文件先别急着pd.read_excel一把梭。我的习惯是先用两分钟把文件本身看清楚第一行是不是表头还是前面有几行标题、说明文字有没有合并单元格合并单元格在读进来之后会产生NaN填充直接影响拆分结果。要拆的那一列数据长什么样是“部门”“地区”这种短文本还是“日期”“月份”这种时间格式或者是“2024-01”这种字符串格式不同处理逻辑完全不一样。文件一共多大几千行和几十万行的处理方案不一样。我见过最典型的一次翻车同事说“按城市拆分”结果城市列里有“北京市”“北京”“市辖区”三种写法groupby一跑分出来40多个组实际就5个城市。数据本身的脏程度决定了你要不要先做清洗。1.2 确认拆分规则单列拆分、多列组合拆分还是条件拆分按列拆分的需求细分起来有三种常见形态按单列值拆分比如按“部门”列每个部门一个文件。按多列组合值拆分比如按“地区产品线”每个组合一个文件文件名类似“华东_A产品.xlsx”。按条件拆分不是按列的值分组而是按条件把数据分成“满足条件”和“不满足条件”两份比如“销售额大于1万的”和“其他的”。这三种需求在代码上是同一套思路的不同变体核心都是groupby但第二步“根据分组信息生成输出”略有区别。第一步就要确认清楚不然做到一半才发现规则错了返工成本很高。1.3 想清楚输出形态按值取名、多Sheet还是一份汇总同一份拆分结果落到Excel里可以有三种完全不同的呈现方式输出方式适合场景优点缺点每个值一个独立xlsx文件需要把文件发给别人分发方便文件独立文件数量多管理麻烦一个xlsx每个值一个Sheet自己汇总查看一个文件搞定浏览方便Sheet太多时切换麻烦一个xlsx带一列“源分组”标识后续还要统一分析数据结构完整不碎片化严格说不算“拆分”需求方说要“拆分”往往自己也没想清楚要哪种结果。我的做法是先问一句“拆完之后是发邮件用还是自己看还是还要再汇总回来”这个问题一出来对方的真实需求就清楚了。2. 为什么我首选 pandas.groupby而不是 openpyxl 或 VBA做Excel拆分可选的技术路线不止一条。VBA、openpyxl、pandas都能做但适用场景差别很大。2.1 各自的适用边界先说VBA。Excel自带的VBA确实能录宏、能写循环、能不装Python环境直接跑。如果你公司电脑装不了Python或者同事都只会用ExcelVBA是合理的选型。但VBA的问题在于处理大数据量时效率低而且代码可读性和可维护性都比较差。用VBA写一个按列拆分的宏少说二十几行还得处理Copy、Paste这些剪贴板操作速度慢而且容易触发“复制粘贴无响应”一类的Excel诡异问题。再说openpyxl。这个库的优势是能读取和保留原始Excel文件的格式、公式、样式适合对体裁要求极高的场景。但它没有groupby这种分组概念要自己遍历每一行、手动判断值是否变化、再往新Sheet里append代码写起来非常啰嗦。而且openpyxl的写入性能偏慢几万行数据写起来肉眼可见地卡。pandas就不一样了。read_excel读进来是一个DataFramegroupby是DataFrame的原生能力拆分成多个小DataFrame之后再循环to_excel写出逻辑非常直白。我平时做数据分析用的就是这一套顺手、好记、不容易错。2.2 groupby 拆分的底层逻辑groupby的官方叫法是“分组聚合”但按列拆分只用到它的分组能力不需要聚合运算。它的工作过程可以理解为三步扫描“部门”这一列的全部值把相同的值归到一个组里。每个组对应一个独立的子DataFrame保留了原始数据的全部行和列。遍历这些组把每个组写入一个目标文件。这个过程不需要你手动去重、不需要循环判断“当前值和上一个值是否一样”pandas内部用哈希索引做分组性能天然比手写循环高一个量级。所以说按列拆分Excel这个需求的本质就是一个groupby加一个to_excel。如果你此前没接触过pandas记住这个核心逻辑后面代码就顺理成章了。3. 核心实现一个函数搞定按列拆分代码本身不复杂但我平时写这类脚本时习惯把边界情况一起处理掉这样脚本才能给别人用、换张表也能直接用。下面分三版讲。3.1 最小可运行版本先看最核心的版本。假设你的Excel文件叫“销售明细.xlsx”要按“部门”列拆分import pandas as pd df pd.read_excel(销售明细.xlsx) for key, group in df.groupby(部门): group.to_excel(f拆分结果_{key}.xlsx, indexFalse)三行。真的就三行。第一行读文件第二行按部门分组第三行把每个分组写成独立文件。文件名自动带上部门名比如“拆分结果_销售部.xlsx”“拆分结果_市场部.xlsx”。indexFalse的意思是不要输出pandas默认的行号否则Excel里会多出一列无意义的数字。3.2 加点防御文件路径、空表、返回值实际业务中用户上传的Excel不一定那么规矩。可能文件名带空格可能某一列全是空值可能目标文件夹里已经有同名文件。所以我会写成下面这个带参数校验的完整函数import pandas as pd from pathlib import Path def split_excel_by_column(input_file, split_column, output_dir拆分结果): 按指定列的值拆分Excel文件。 Parameters ---------- input_file : str or Path 输入的Excel文件路径 split_column : str 用于拆分的列名 output_dir : str 输出文件夹名 Returns ------- dict 拆分结果统计key为分组值value为行数 input_path Path(input_file) out_path Path(output_dir) out_path.mkdir(exist_okTrue) df pd.read_excel(input_path) if split_column not in df.columns: raise KeyError(f表格中找不到列{split_column}当前列名为{list(df.columns)}) if df[split_column].isna().all(): raise ValueError(f列 {split_column} 全为空值无法拆分) summary {} for key, group in df.groupby(split_column, dropnaFalse): # 处理空值作为分组key的情况 file_key 空值 if pd.isna(key) else str(key) # 清理文件名中Windows不支持的字符 file_key file_key.replace(\\, _).replace(/, _).replace(:, _) out_file out_path / f{input_path.stem}_{file_key}.xlsx group.to_excel(out_file, indexFalse) summary[file_key] len(group) return summary重点在几个细节dropnaFalsepandas的groupby默认会丢弃NaN如果不加这个参数拆列里有空值的行会直接消失这在业务上可能是不能接受的。文件名清理Windows文件名不允许包含\ / : * ? |分组值里如果带这些字符写文件时会直接报错。返回值返回一个统计字典方便在调用端打印“拆分完成共X个文件总行数Y”。至于为什么用Path而不是srt拼接用Path在Windows和Mac上都能正确处理路径分隔符不会出现“反斜杠还是正斜杠”的问题。3.3 多级拆分按两列组合拆有些需求是按两列的组合来拆。比如“地区”“产品线”希望“华东_A产品”成一个文件“华东_B产品”成一个文件。实现方式是给groupby传一个列表df[分组] df[地区].astype(str) _ df[产品线].astype(str) for key, group in df.groupby(分组): group.to_excel(f拆分结果_{key}.xlsx, indexFalse)我先新造一列“分组”把两列的值拼起来作为新的分组依据然后再按这一列拆。这样做的好处是拆分的逻辑集中在“分组”列上后续要改成按三列拆只需改拼接那一行。缺点是多了一列输出时如果不需要可以在to_excel前drop掉group.drop(columns[分组]).to_excel(...)注意拼接前用astype(str)做类型转换。如果“地区”列里有数值型的数据比如编码是数字不转换的话str int会直接报TypeError。4. 真实业务中的坑我从报错中总结的5条经验代码写出来容易但跑在真实数据上总有各种意料之外的问题。这几年我用这个功能处理过几十种表格有几个坑反复出现每次都是现场排查半天最后发现原因特别基础。4.1 列名前后有空格groupby 静默分成两组这个坑最阴险。你看到的表头叫“部门”但表头实际是“部门 ”末尾有个空格。df[部门]取列时明明也成功了groupby也跑了但输出文件比预想的多了一倍——“销售部”和“销售部 ”被当成了两个组。排查方法特别简单print(df.columns) # 输出会带引号比如 [姓名, 部门 , 销售额]看到列名末尾有空格用strip处理一遍df.columns [col.strip() for col in df.columns]同理如果列名里还有全角空格、不间断空格strip可能不够需要replace(\u3000, )。这类问题在中文Excel表头里非常普遍。4.2 数字与字符串混排2024和“2024”是两拨人拆序列里的值看着一样但底层数据类型不同。比如“年份”列里有些单元格是数值型2024有些单元格是文本型“2024”左上角有个小三角那种pandas会警告“混入了类型”groupby直接把它俩分成两组。结果就是同一个年份拆出了两个文件。这种情况我一般在读表时直接指定dtype强制把这一列读成字符串让所有值统一口径df pd.read_excel(销售明细.xlsx, dtype{年份: str})如果你事先不知道哪一列会混类型可以用一个粗暴但有效的办法拆分前把整列统一转成字符串df[年份] df[年份].astype(str).str.strip()这样“2024”和2024都会被转成相同的字符串“2024”分组自然就合并了。4.3 空值拆分出来的“nan”文件默认情况下df.groupby(部门)会丢弃拆序列中的NaN行这些行不会出现在任何输出文件里。但如果你用了dropnaFalse像我上面建议的那样空值分组就会成为一个key名为NaN文件名就会带“nan”或“空值”。真实业务中拆序列有空值是很常见的。这时候你要想清楚空值的行是单独放一个文件还是归到一个“待处理”文件里我的做法是单独放一个因为空值行通常意味着数据录入不完整需要人工回头核对单独放一个文件方便处理。4.4 输出Excel后公式全没了如果我读进来的是带“销售额单价*数量”这种公式的Excel用pandas处理后再写出去公式会消失只剩下当时计算出的静态值。原因很简单pandas读Excel时是把单元格“值”读进来不是读公式本身。好在pandas有个参数可以主动读取公式df pd.read_excel(销售明细.xlsx, engineopenpyxl, data_onlyFalse)data_onlyFalse是读取公式默认行为也是读公式但要注意对方文件如果是用公式计算但没保存过结果读出来的值会是None。如果你希望拆分后的文件保留计算值建议第一次打开文件时让它把公式算一遍并保存再交给pandas处理。这个逻辑很反直觉但这是Excel公式的工作机制决定的。4.5 大批量数据拆分时的性能瓶颈几个文件、几万行数据上面代码毫无压力。真正让人抓狂的是几十万行、拆成几百个文件的场景。这时候有两个性能瓶颈read_excel读整个文件慢尤其是xlsx格式底层要做XML解析属于正常现象。to_excel高频写入慢每次调用to_excel都会创建一个新的Excel写入器。针对后者我有个变通方案如果输出是一个xlsx的多Sheet结构可以用pd.ExcelWriter复用写入器import pandas as pd df pd.read_excel(大文件.xlsx) with pd.ExcelWriter(拆分结果.xlsx, engineopenpyxl) as writer: for key, group in df.groupby(部门): group.to_excel(writer, sheet_namestr(key)[:31], indexFalse)这里有个细节Excel的Sheet名最长是31个字符如果拆列值太长会直接报错所以要截断。这段代码拆几百个Sheet都能很快写完因为不用反复打开关闭文件。5. 从“能拆”到“好用”我给自己的脚本做的升级基础版能跑通之后我开始琢磨“给同事用”这件事。毕竟大家不是Python用户你交付一个命令行脚本人家不一定愿意用。我给脚本加了三个小功能实用价值提升很明显。5.1 按拆列值自动排序groupby在内部按哈希分组输出文件的顺序是乱的想要按拼音或字母顺序排列输出需要把分组结果先排序group_keys sorted(df[split_column].dropna().unique()) for key in group_keys: group df[df[split_column] key] ...如果分组值本身自带逻辑顺序比如“一季度、二季度、三季度、四季度”排序就按文本字典序来结果可能是“三季度”排在“一季度”前面。这种情况可以传入自定义顺序order [一季度, 二季度, 三季度, 四季度] for key in order: ...5.2 冻结首行、加筛选器、自适应列宽拆分出的Excel文件默认是纯数据没有格式。同事们收到表格第一印象就是“这个表没有筛选、没有冻结”。用openpyxl引擎可以在写出后补上这些格式from openpyxl import load_workbook from openpyxl.utils import get_column_letter def pretty_excel(file_path): wb load_workbook(file_path) ws wb.active ws.freeze_panes A2 ws.auto_filter.ref ws.dimensions for col_cells in ws.columns: max_length 0 col_letter get_column_letter(col_cells[0].column) for cell in col_cells: try: cell_length len(str(cell.value)) if cell_length max_length: max_length cell_length except Exception: pass ws.column_dimensions[col_letter].width min(max_length 4, 60) wb.save(file_path)这段代码做完三件事首行冻结、整表筛选、按内容长度自适应列宽。不要嫌它简单这三点正是把“程序员产物”变成“业务可用表格”的差距所在。如果想在写出时就带格式可以把to_excel的engine换成xlsxwriter配合add_format设置。但xlsxwriter不支持追加写入二选一我通常选先写数据再用openpyxl调格式因为通用性更强。5.3 汇总信息拆分数量、行数统计拆完文件客户或者领导一定会问“一共拆出来多少个文件每个文件多少行”如果拆了300个文件你不可能一个个去数。所以我的脚本最后会打印一个汇总表或者把汇总写成txt/Excelimport pandas as pd def summarize(splits: dict): pd.DataFrame( [{分组: k, 行数: v} for k, v in splits.items()] ).to_excel(拆分汇总.xlsx, indexFalse)有这份汇总后续对账、检查漏拆、判断数据总量都非常方便。它的底层逻辑就是用前面那个split_excel_by_column返回的字典转成DataFrame再写出去。6. 从按列值拆分延伸出去的几个高频需求按列值拆分只是数据处理链路里的一环。做多了之后你会发现它和几个常见需求经常一起出现。6.1 按条件拆而不是按值拆“销售额大于1万”和“小于等于1万”拆成两个文件这种按条件拆的需求groupby就不好用了得用布尔索引high df[df[销售额] 10000] low df[df[销售额] 10000] high.to_excel(高销售额.xlsx, indexFalse) low.to_excel(低销售额.xlsx, indexFalse)条件拆和值拆很容易混淆。一个是“分组”一个是“筛选”。本质区别是值拆是“每个不同的值单独成一个文件”条件拆是“满足条件的进A剩下的进B”。6.2 按日期列拆分成月报按日期列拆分是另一个高频变体通常拆成月份维度。做法是把日期列转成月份字符串再按这个新列拆分df[月份] pd.to_datetime(df[日期]).dt.to_period(M).astype(str) # 输出类似 2024-01 for key, group in df.groupby(月份): group.to_excel(f月报_{key}.xlsx, indexFalse)6.3 拆分前的“清洗”意识mapping、替换、去空格回到开头说的那个“北京市”“北京”“市辖区”的案例。如果一开始就知道拆列数据脏可以在拆分前做一次归一化city_map { 北京市: 北京, 市辖区: 北京, 上海市: 上海, } df[城市] df[城市].replace(city_map)做完这步再拆分35个“城市”就能收敛成真实的数量。不要小看这个预处理步骤它往往比拆分逻辑本身更重要——拆分是机器能干的活清洗才是需要经验和判断力的部分。就我自己的体会来说按列拆分Excel这个需求真正训练人的不是pandas语法而是思路拿到表先看结构拆之前先想输出跑完一定要验证。代码是工具思路才是决定交付质量的核心。最后分享一个日常工作上的小习惯——凡是给我自己或同事用的拆分脚本我都会在文件末尾加一行print(拆分完成共 {} 个文件总行数 {}输出目录{}.format(len(summary), sum(summary.values()), output_dir))别小看这一行输出它能让脚本的每一次运行都有反馈。出了问题能立刻知道是文件数不对还是总行数不对而不是默默跑完、什么都没发生还得再打开文件夹一个个数。这些小的“可观测性”设计才让脚本从“能跑”变成“好用”。
返回列表