1. 从Excel到DataFrame为什么Pandas是数学建模的“数据清道夫”如果你参加过数学建模比赛或者正在准备大概率遇到过这样的场景题目给的数据是一个几百兆甚至上G的Excel文件打开都费劲更别说分析了。用Excel的筛选、透视表数据量一大卡顿、崩溃是家常便饭。这时候一个得心应手的工具就显得至关重要。Pandas这个基于Python的数据分析库就是解决这类问题的“瑞士军刀”更是处理建模前期脏活累活的“数据清道夫”。我见过太多队伍拿到数据后的第一反应是埋头用Excel手动处理花费数小时甚至一整天在重复的复制粘贴、格式调整上不仅效率低下而且过程不可复现一旦中间某一步出错可能就要推倒重来。数学建模的暑期集训核心目标之一就是建立一套高效、可靠、可复现的数据处理流水线。而Pandas正是这条流水线的核心发动机。它不仅能轻松处理远超Excel承载极限的数据量更能通过代码将整个数据清洗、转换、分析的过程固化下来确保每一步操作都清晰、可追溯。这对于团队协作和最终论文中“数据预处理”部分的撰写价值巨大。简单来说Pandas将Excel表格读入内存变成一个叫DataFrame的二维表格结构。之后所有的操作无论是筛选行、选取列、计算统计量、合并多个表格还是处理缺失值都变成了对DataFrame对象的函数调用。代码即文档过程即逻辑。本次实战我们就聚焦于数学建模中最常见也最头疼的环节如何用Pandas高效、优雅地“啃下”大赛提供的Excel大数据为后续的模型构建扫清障碍。2. 环境搭建与核心数据结构为实战铺平道路工欲善其事必先利其器。在开始处理具体数据之前我们需要一个稳定、高效的工作环境。对于数学建模而言我强烈推荐使用Anaconda来管理Python环境它集成了科学计算所需的大部分库包括Pandas、NumPy、Matplotlib等免去了逐个安装的麻烦。2.1 创建专属的建模环境虽然Anaconda自带基础环境但为了项目的纯净和依赖管理的方便最好为每个建模项目创建一个独立的环境。# 创建一个名为math_modeling的Python3.9环境 conda create -n math_modeling python3.9 # 激活该环境 conda activate math_modeling # 安装核心库 conda install pandas numpy matplotlib scikit-learn jupyter使用独立环境的好处是你可以随意安装、升级、降级库而不会影响其他项目。在集训或比赛中这能避免很多因库版本冲突导致的诡异错误。2.2 理解Pandas的两大核心Series与DataFramePandas的威力建立在两个核心数据结构上Series和DataFrame。这是你必须彻底理解的概念。Series可以看作是一个带标签的一维数组。标签就是索引index数组里的数据可以是任何类型整数、字符串、浮点数等。它就像Excel中的一列数据但更强大。import pandas as pd # 创建一个Series s pd.Series([85, 90, 78, 92], index[张三, 李四, 王五, 赵六]) print(s)输出张三 85 李四 90 王五 78 赵六 92 dtype: int64你可以通过索引名字来访问数据例如s[‘李四’]会返回90。这在处理时间序列数据如股票价格、温度变化时非常有用。DataFrame是Pandas的灵魂它是一个二维的、大小可变的、有标签的表格结构。你可以把它想象成一个Excel工作表或者一个SQL数据库表。它由行索引index、列索引columns和数据本身组成。# 创建一个DataFrame data { 姓名: [张三, 李四, 王五, 赵六], 数学: [85, 90, 78, 92], 语文: [88, 82, 95, 79], 班级: [A, B, A, B] } df pd.DataFrame(data) print(df)输出姓名 数学 语文 班级 0 张三 85 88 A 1 李四 90 82 B 2 王五 78 95 A 3 赵六 92 79 B默认的行索引是0,1,2,3。DataFrame的强大之处在于你可以用多种灵活的方式访问和操作其中的数据这是接下来所有实战操作的基础。注意很多新手会混淆df[‘列名’]和df.loc[行标签, 列名]。df[‘数学’]返回的是一个Series数学成绩这一列而df.loc[0, ‘数学’]返回的是标量85。在后续的筛选和赋值操作中正确使用这两种方式至关重要用错了可能导致报错或产生SettingWithCopyWarning警告。3. 数据加载的“第一公里”高效读取Excel大文件的技巧数学建模题目提供的原始数据往往是一个或多个Excel文件。如何快速、正确地将它们读入Pandas是万里长征的第一步这里面的坑不少。3.1 使用read_excel函数的核心参数Pandas的pd.read_excel()函数功能非常强大但默认参数可能不适合大数据文件。import pandas as pd # 基础读取 file_path 大数据文件.xlsx df pd.read_excel(file_path) # 针对大文件的优化读取 df_optimized pd.read_excel( file_path, sheet_name0, # 读取第一个工作表也可以用名字‘Sheet1’ header0, # 第一行作为列名 # usecolsA:D, F, # 只读取A到D列和F列极大减少内存占用对于几十列的表格特别有用。 # dtype{列名1: int32, 列名2: str}, # 指定列数据类型节省内存并避免自动类型推断错误 # nrows1000, # 先读取前1000行进行探索 engineopenpyxl # 对于.xlsx文件这是默认且稳定的引擎 )对于非常大的.xls文件老格式可能需要指定engine’xlrd’但需注意新版xlrd已不支持.xlsx。为什么usecols和dtype如此重要建模数据中经常包含大量的描述性文本列如备注、说明或者一些在后续分析中根本用不到的ID列。用usecols参数在读取时就直接过滤掉它们可以瞬间将需要加载的数据量减少一半甚至更多内存占用和读取速度都会得到极大改善。而dtype参数能防止Pandas进行耗时的类型推断尤其对于明确是分类如‘男’‘女’或整数ID的列指定为‘category’或‘int32’类型内存效率能提升数倍至数十倍。3.2 处理多个工作表和分表数据有时数据会分散在同一个Excel文件的多个工作表中或者按年份、地区分成了多个独立的Excel文件。读取单个文件的多张表# 方法1读取所有表到一个字典 all_sheets_dict pd.read_excel(data.xlsx, sheet_nameNone) # sheet_nameNone 读取所有 df_sheet1 all_sheets_dict[Sheet1] # 方法2读取指定多张表 df_list pd.read_excel(data.xlsx, sheet_name[0, 2, 月度数据]) # 按索引或名字合并多个Excel文件这是建模中更常见的场景比如给了2018-2023年每年的销售数据每个年份一个文件。import os import pandas as pd folder_path ./年度数据/ all_files [f for f in os.listdir(folder_path) if f.endswith(.xlsx)] df_list [] for file in all_files: file_path os.path.join(folder_path, file) # 可以在读取时提取文件名中的年份作为新列 year file.split(_)[1].split(.)[0] # 假设文件名格式为‘sales_2022.xlsx’ temp_df pd.read_excel(file_path) temp_df[年份] year # 添加年份列 df_list.append(temp_df) # 纵向合并所有DataFrame combined_df pd.concat(df_list, ignore_indexTrue) # ignore_index重置索引pd.concat()是纵向堆叠的利器。ignore_indexTrue保证了合并后的索引是连续的。如果多个文件结构不完全一致列顺序不同、有多余列Pandas会以并集的方式处理列缺失值用NaN填充这通常也是我们期望的行为。4. 数据清洗与预处理建模质量的基石数据读进来了但通常是“脏”的。缺失值、异常值、重复记录、不一致的格式这些问题不解决再高级的模型也是空中楼阁。数据清洗通常占据建模80%的时间而Pandas提供了全套工具。4.1 探索性数据查看与统计在动手清洗前先全面了解你的数据。# 查看数据形状行数列数 print(df.shape) # 查看前5行和后5行 print(df.head()) print(df.tail()) # 查看列名、数据类型和非空数量 print(df.info()) # 快速获取数值型列的统计摘要计数、均值、标准差、最小值、四分位数、最大值 print(df.describe()) # 查看唯一值数量 print(df.nunique()) # 检查缺失值情况 print(df.isnull().sum())df.info()是你的第一道安检门它能立刻告诉你是否有列因为读取错误变成了object类型通常是文本列里混入了数字或缺失值以及每列有多少非空值。df.describe()则能快速发现数值的异常比如某列最小值是-999这可能是缺失值的占位符或者标准差极大可能存在离谱的异常值。4.2 处理缺失值策略比删除更重要直接删除缺失值df.dropna()是最简单粗暴的但建模数据宝贵每一行都可能蕴含信息需谨慎。# 1. 删除缺失值 # 删除任何包含缺失值的行 df_dropped df.dropna() # 删除在特定列如‘关键指标’上有缺失的行 df_dropped_specific df.dropna(subset[关键指标]) # 2. 填充缺失值 # 用固定值填充 df_filled df.fillna(0) # 或 fillna(未知) # 用前向填充适用于时间序列 df_ffill df.fillna(methodffill) # 用后向填充 df_bfill df.fillna(methodbfill) # 用统计量填充常用 df[数值列].fillna(df[数值列].mean(), inplaceTrue) # 填充均值 df[类别列].fillna(df[类别列].mode()[0], inplaceTrue) # 填充众数 # 用插值法填充对于有序数据更合理 df[有序列].interpolate(methodlinear, inplaceTrue)选择哪种策略时间序列数据优先考虑前向填充(ffill)或插值(interpolate)因为相邻时间点的数据相关性高。类别数据填充“未知”或众数。数值数据如果缺失很少且数据分布比较对称可以用均值填充。但如果数据有偏存在极端值中位数是更好的选择。更高级的做法是使用回归或KNN算法基于其他列来预测缺失值这在scikit-learn中可以实现。关键特征缺失过多如果某列缺失率超过50%与其费力填充不如考虑是否直接舍弃该特征或者将其作为一个“是否缺失”的二元标志特征加入模型。踩坑实录在一次比赛中我们有一列“风速”数据缺失值用fillna(method’ffill’)填充。后来发现由于传感器故障连续缺失了48小时的数据。前向填充导致这48小时的风速全部变成了故障前的最后一个值严重扭曲了数据分布最终模型预测出现系统性偏差。教训对于连续大段缺失的数据填充要格外小心最好结合业务背景如传感器故障记录或使用更复杂的插值方法并评估填充带来的影响。4.3 处理异常值是噪音还是信号异常值可能是数据录入错误也可能是重要的特殊现象如金融欺诈。不能一概而论。识别异常值# 方法1描述性统计和箱线图 import matplotlib.pyplot as plt df[某数值列].plot(kindbox) plt.show() # 箱线图可以直观显示上下四分位点和离群点。 # 方法2标准差法假设数据近似正态分布 mean df[列].mean() std df[列].std() lower_bound mean - 3 * std upper_bound mean 3 * std outliers df[(df[列] lower_bound) | (df[列] upper_bound)] # 方法3分位数法更稳健不受极端值影响 Q1 df[列].quantile(0.25) Q3 df[列].quantile(0.75) IQR Q3 - Q1 lower_bound_iqr Q1 - 1.5 * IQR upper_bound_iqr Q3 1.5 * IQR outliers_iqr df[(df[列] lower_bound_iqr) | (df[列] upper_bound_iqr)]处理异常值删除如果确认是错误数据且数量很少。替换用上下限值替换缩尾处理或者用中位数、分位数替换。# 缩尾处理Winsorization def winsorize(series, limits[0.05, 0.05]): # limits[lower_limit, upper_limit] 表示两侧各截断的比例 s_sorted series.sort_values() n len(s_sorted) lower_idx int(n * limits[0]) upper_idx int(n * (1 - limits[1])) - 1 lower_bound s_sorted.iat[lower_idx] upper_bound s_sorted.iat[upper_idx] return series.clip(lower_bound, upper_bound) df[处理后的列] winsorize(df[原始列])分箱将连续值离散化异常值会被归入最高或最低的箱中。保留如果异常值代表一种重要模式如欺诈交易则不应处理反而应将其作为重点研究对象。4.4 处理重复值与格式统一# 检查完全重复的行 duplicates df[df.duplicated()] print(f完全重复的行数: {len(duplicates)}) # 基于关键列检查重复例如同一ID不应有两条记录 key_duplicates df[df.duplicated(subset[ID, 日期], keepFalse)] # keepFalse会标记出所有重复项方便查看所有重复记录 # 删除重复值保留第一条 df_cleaned df.drop_duplicates(subset[ID, 日期], keepfirst) # 格式统一字符串处理 df[城市] df[城市].str.strip() # 去除首尾空格 df[城市] df[城市].str.upper() # 统一为大写 df[城市] df[城市].replace({BeiJing: BEIJING, ShangHai: SHANGHAI}) # 替换不一致的写法格式不一致是隐形的“数据杀手”。“北京”、“Beijing”、“BEIJING”在计算机看来是三个不同的值会导致分组统计错误。在清洗初期就进行标准化能避免后续很多麻烦。5. 数据转换与特征工程从原始数据到模型输入清洗干净的数据只是原材料要喂给模型还需要进行转换和特征构建。这是提升模型性能的关键步骤也是Pandas大显身手的地方。5.1 类型转换与时间处理类型转换# 将字符串转换为数值 df[价格] pd.to_numeric(df[价格], errorscoerce) # 无法转换的变成NaN # 将数值转换为分类 df[等级] df[分数].apply(lambda x: A if x90 else (B if x80 else C)) df[等级] df[等级].astype(category) # 转换为分类类型节省内存并提高速度时间序列处理建模中极其常见# 将字符串列转换为datetime类型 df[日期] pd.to_datetime(df[日期字符串], format%Y/%m/%d) # 指定格式能加速转换 # 提取时间特征 df[年份] df[日期].dt.year df[月份] df[日期].dt.month df[季度] df[日期].dt.quarter df[星期几] df[日期].dt.dayofweek # 周一0, 周日6 df[是否周末] df[星期几].isin([5, 6]).astype(int) df[月初] (df[日期].dt.day 1).astype(int) # 是否为每月第一天 # 计算时间差 df[距今天数] (pd.Timestamp(2023-08-01) - df[日期]).dt.days时间特征的构建能极大地丰富模型的信息。例如在预测销量时“月份”、“季度”、“是否周末”、“是否节假日”都是强特征。5.2 数据分组与聚合多维度的洞察这是Pandas最强大的功能之一堪比Excel的数据透视表但更灵活。# 单维度分组聚合 grouped_by_city df.groupby(城市)[销售额].sum().sort_values(ascendingFalse) # 多维度分组聚合 pivot_result df.groupby([年份, 产品类别]).agg({ 销售额: [sum, mean, std], 利润: sum, 订单ID: count # 计算订单数 }) # 这会生成一个多级索引的DataFrame # 更直观的透视表 pivot_table pd.pivot_table(df, values销售额, index年份, columns产品类别, aggfuncsum, fill_value0, marginsTrue) # marginsTrue 添加总计groupby遵循“拆分-应用-合并”模式是进行多维统计分析的核心。agg函数允许对不同的列应用不同的聚合函数求和、平均、计数等非常灵活。5.3 创建新特征想象力的舞台特征工程是机器学习的灵魂好的特征往往比复杂的模型更有效。# 1. 简单计算特征 df[利润率] df[利润] / df[销售额] df[客单价] df[销售额] / df[订单数] # 2. 分箱离散化特征 df[年龄分段] pd.cut(df[年龄], bins[0, 18, 35, 60, 100], labels[少年, 青年, 中年, 老年]) # 3. 交互特征 df[城市_产品交互] df[城市] _ df[产品类别] # 4. 统计聚合特征需要结合groupby # 例如计算每个用户的历史平均消费 user_avg_spend df.groupby(用户ID)[消费金额].transform(mean) df[用户历史平均消费] user_avg_spend # 计算每个产品在所属大类中的价格排名 df[品类内价格排名] df.groupby(产品大类)[价格].rank(ascendingFalse) # 5. 滞后特征时间序列 df[销售额_滞后1天] df.groupby(店铺ID)[销售额].shift(1) df[销售额_7天移动平均] df.groupby(店铺ID)[销售额].rolling(window7).mean().valuestransform函数在分组后能返回一个与原始DataFrame长度相同的Series非常适合用来创建基于组统计的新特征而不会改变数据形状。shift和rolling是处理时间序列特征的神器。6. 大数据处理优化与性能技巧当数据量真的很大比如百万行以上时一些操作会变得很慢。掌握一些优化技巧能让你在集训和比赛中节省大量时间。6.1 选择高效的数据类型Pandas默认的数据类型可能不是最省内存的。# 查看当前数据类型 print(df.dtypes) # 向下转换数值类型 df[整数列] df[整数列].astype(int32) # 默认int64 df[小数列] df[小数列].astype(float32) # 默认float64 # 将低基数文本列转为分类类型 if df[省份].nunique() / len(df) 0.5: # 唯一值比例小于50% df[省份] df[省份].astype(category)使用category类型处理像“省份”、“性别”这样的列内存占用和分组、排序速度会有数量级的提升。6.2 避免链式赋值与使用.loc,.iloc链式赋值是性能杀手也容易引发SettingWithCopyWarning。# 不推荐链式索引赋值 df[df[年龄]60][折扣] 0.5 # 可能无效且警告 # 推荐使用.loc进行明确赋值 df.loc[df[年龄] 60, 折扣] 0.5 # 使用.iloc按位置索引更快 df.iloc[10:20, 2:5] 100 # 第10-19行第2-4列6.3 使用向量化操作替代循环Pandas底层基于NumPy向量化操作比Python循环快成百上千倍。# 慢使用apply循环 df[新列] df.apply(lambda row: row[A] * 2 row[B], axis1) # 快使用向量化操作 df[新列] df[A] * 2 df[B] # 对于更复杂的条件判断使用np.where或np.select import numpy as np df[等级] np.where(df[分数]90, 优, np.where(df[分数]80, 良, 及格)) conditions [df[分数]90, df[分数]80, df[分数]60] choices [优, 良, 及格] df[等级] np.select(conditions, choices, default不及格)6.4 分块处理与高效存储如果内存实在无法一次性加载全部数据可以考虑分块处理。chunk_size 100000 chunks [] for chunk in pd.read_excel(超大文件.xlsx, chunksizechunk_size): # 对每个块进行清洗和预处理 processed_chunk do_some_cleaning(chunk) chunks.append(processed_chunk) # 最后再合并如果最终结果可以放入内存 final_df pd.concat(chunks, ignore_indexTrue)处理完成后将清洗好的数据保存为更高效的格式如feather或parquet下次加载会快很多。# 保存 df.to_feather(清洗后数据.feather) # 读取 df_fast pd.read_feather(清洗后数据.feather)7. 实战案例电商销售数据清洗与分析全流程让我们用一个模拟的电商销售数据集串联起上述所有技能点。假设我们有一个sales_data.xlsx文件包含订单ID、用户ID、产品、数量、单价、订单日期、城市等字段数据有缺失、有异常、格式也不统一。第一步加载与探索import pandas as pd import numpy as np df pd.read_excel(sales_data.xlsx, usecols[订单ID,用户ID,产品,数量,单价,订单日期,城市]) print(f数据形状: {df.shape}) print(df.info()) print(df.head()) print(df.isnull().sum())第二步清洗# 1. 处理缺失值城市缺失用‘未知’填充单价缺失用同类产品均价填充 df[城市].fillna(未知, inplaceTrue) product_avg_price df.groupby(产品)[单价].transform(mean) df[单价].fillna(product_avg_price, inplaceTrue) # 2. 处理异常值数量为负或大于100的视为异常用中位数替换 q_low df[数量].quantile(0.01) q_high df[数量].quantile(0.99) df[数量] df[数量].clip(lowerq_low, upperq_high) # 3. 格式统一城市名大写 df[城市] df[城市].str.upper() df[产品] df[产品].str.strip()第三步转换与特征工程# 1. 计算衍生列 df[订单日期] pd.to_datetime(df[订单日期]) df[销售额] df[数量] * df[单价] df[月份] df[订单日期].dt.month df[星期几] df[订单日期].dt.dayofweek df[是否周末] df[星期几].isin([5,6]).astype(int) # 2. 创建用户行为特征需要分组 df[用户首次购买日期] df.groupby(用户ID)[订单日期].transform(min) df[用户购买频次] df.groupby(用户ID)[订单ID].transform(count) df[用户累计销售额] df.groupby(用户ID)[销售额].transform(sum)第四步分析与输出# 月度销售额分析 monthly_sales df.groupby(月份)[销售额].sum().reset_index() # 城市销售额排名 city_sales_rank df.groupby(城市)[销售额].sum().sort_values(ascendingFalse).head(10) # 周末 vs 工作日对比 weekend_sales df.groupby(是否周末)[销售额].mean() # 输出清洗后的数据供后续建模使用 df.to_csv(cleaned_sales_data.csv, indexFalse) print(数据清洗与特征工程完成已保存为 cleaned_sales_data.csv)通过这样一个完整的流程我们就把一个原始的、杂乱的大Excel文件变成了一份干净、富含特征、可以直接用于机器学习模型训练的数据集。这个过程是可复现、可解释的每一步操作都记录在代码中这正是用Pandas进行数学建模数据处理的精髓所在。