Python数据分析实战:从数学建模到Pandas核心操作与可视化
2026/8/22 5:45:59 网站建设 项目流程

1. 项目概述:从数学建模赛题到Python实战

最近在整理过去带学生做数学建模竞赛的资料,翻到了2011年的A题,一个关于城市表层土壤重金属污染分析的问题。这道题当年难倒了不少队伍,核心难点之一就在于如何处理那份庞大的、包含多种重金属元素在不同功能区采样点的浓度数据。你需要做的,不是简单地算个平均值,而是要对数据进行分类统计(比如工业区、生活区、公园绿地区各自的污染特征),计算每个采样点的综合污染指数,统计不同污染等级的区域个数,最后用直观的图表(比如饼图)来展示污染程度的构成比。这不就是一个典型的数据处理与分析流程吗?用Python的Pandas和Matplotlib来搞定再合适不过了。

很多朋友学Python数据处理,跟着教程把df.groupby()df.plot.pie()跑一遍就结束了,但一到自己面对真实、杂乱的数据就无从下手。问题出在哪?在于缺少一个从“原始需求”到“代码实现”的完整思维链条,以及对这个链条上每个环节“为什么这么做”和“可能踩什么坑”的深刻理解。今天,我就以这道建模题为背景,抛开教科书的简单示例,带你走一遍一个数据分析任务中那些看似基础却至关重要的操作:如何安全地复制DataFrame以免污染原始数据、如何高效地对每一行或列进行计算、如何统计某一列里不同元素的个数,以及如何画出一张既专业又美观的饼图。无论你是正在备战数模的学生,还是日常需要处理报表的职场人,这套思路和技巧都能直接套用。

2. 核心需求解析与数据准备

2.1 数学建模(2011 A)题背景与数据特点

2011年高教社杯全国大学生数学建模竞赛A题《城市表层土壤重金属污染分析》提供了一个经典的数据分析场景。题目给出了一个城市区域内大量采样点的空间坐标(x, y)、所属功能区类型(如1-生活区,2-工业区,3-山区等)以及多种重金属元素(如砷、镉、铬等)的浓度测量值。

我们的核心分析任务通常包括:

  1. 分类统计:计算不同功能区(分类变量)内各种重金属浓度的平均值、最大值、最小值等描述性统计量,以比较污染特征。
  2. 综合评价:对每个采样点,需要根据多种重金属的浓度计算一个综合污染指数(如内梅罗指数)。这涉及到对每个样本(每一行数据)进行多列数据的数学运算。
  3. 现状评估:根据综合污染指数划分污染等级(如清洁、轻度污染、重度污染),并统计每个等级的区域数量(即统计某一列中不同元素/类别的个数)。
  4. 成果可视化:将污染等级的分布情况用饼图展示,直观呈现各等级占比。

原始数据通常是一个CSV或Excel文件,我们假设其结构如下表所示(仅为示例):

采样点IDx坐标y坐标功能区砷(As)镉(Cd)铬(Cr)...
1100200112.50.845.2...
2150180225.62.189.7...
........................

2.2 环境准备与数据加载

首先,确保你的Python环境安装了必要的库。打开你的终端或命令提示符,执行以下命令进行安装或更新:

pip install pandas matplotlib numpy -U

注意:建议在虚拟环境中操作,避免包版本冲突。可以使用venvconda创建独立环境。

接下来,我们开始加载数据。这里我模拟生成一份符合题目背景的数据用于演示,在实际比赛中你直接使用pd.read_csv加载即可。

import pandas as pd import numpy as np # 模拟生成数据 np.random.seed(2023) # 固定随机种子,确保结果可复现 n_samples = 500 data = { '点ID': range(1, n_samples+1), 'x坐标': np.random.uniform(0, 1000, n_samples), 'y坐标': np.random.uniform(0, 1000, n_samples), '功能区': np.random.choice([1, 2, 3, 4, 5], n_samples, p=[0.3, 0.25, 0.2, 0.15, 0.1]), # 1:生活区, 2:工业区, 3:交通区, 4:公园, 5:山区 'As': np.random.lognormal(mean=2.0, sigma=0.8, size=n_samples), # 对数正态分布模拟浓度 'Cd': np.random.lognormal(mean=0.5, sigma=1.0, size=n_samples), 'Cr': np.random.lognormal(mean=3.5, sigma=0.6, size=n_samples), 'Pb': np.random.lognormal(mean=3.0, sigma=0.7, size=n_samples), } df_raw = pd.DataFrame(data) print("原始数据前5行:") print(df_raw.head()) print(f"\n数据形状:{df_raw.shape}")

运行后,你会看到一个包含500行样本的DataFrame。这里第一个关键操作来了:我们很少直接对df_raw进行操作。因为数据分析是一个探索和试错的过程,直接操作原始数据,一旦误操作(比如原地修改了某列),就很难回溯。所以,第一步通常是创建一个副本。

3. 数据处理基石:DataFrame的安全复制与理解

3.1 浅拷贝与深拷贝:一个必须厘清的概念

在Pandas中,直接赋值df = df_raw并不会创建副本,它只是创建了一个指向同一块内存数据的新引用。修改df会直接影响df_raw

df_reference = df_raw # 这只是引用,不是复制! df_reference['As'][0] = 999 # 这个修改会同步到df_raw print(f"df_raw的As第一行是否被修改? {df_raw['As'].iloc[0] == 999}")

为了避免这种情况,我们需要复制。Pandas提供了两种主要方式:

  1. df.copy(deep=False)(浅拷贝):只复制DataFrame的结构(索引、列名),但数据本身(底层的NumPy数组)仍然是共享的。对于纯数值型DataFrame,修改一个副本的通常不会影响另一个(因为NumPy数组在某些操作下会产生新数组),但对于包含可变对象(如列表)的列,仍有风险。一般不推荐在数据处理中主动使用浅拷贝。
  2. df.copy()df.copy(deep=True)(深拷贝):默认行为。完整地复制数据和结构,创建一个完全独立的新对象。这是最安全、最常用的方式。
df = df_raw.copy() # 深拷贝,安全独立 print(f"df是df_raw的副本吗? {df is not df_raw}") print(f"修改df会影响df_raw吗?") df['As'][0] = 888 print(f"df_raw的As第一行值:{df_raw['As'].iloc[0]:.2f}, df的As第一行值:{df['As'].iloc[0]:.2f}")

实操心得:养成“加载即拷贝”的习惯。在开始任何分析前,先用.copy()创建你的工作副本。这能为你省去大量因数据意外污染而导致的调试时间。尤其是在使用Jupyter Notebook进行交互式分析时,单元格反复执行,如果没有副本保护,原始数据很容易被改得面目全非。

3.2 数据初探与清洗准备

在深入分析前,先了解你的数据副本df

# 查看基本信息 print(df.info()) # 查看列类型、非空值数量 print(df.describe()) # 数值型列的快速统计摘要 # 检查缺失值 print(f"\n各列缺失值数量:\n{df.isnull().sum()}") # 查看分类列(功能区)的取值分布 print(f"\n‘功能区’取值分布:\n{df['功能区'].value_counts()}") print(f"‘功能区’唯一值个数:{df['功能区'].nunique()}")

df.info()df.describe()能让你快速把握数据全貌:有多少行、多少列、有无缺失、数值的大致范围。df['功能区'].value_counts()df['功能区'].nunique()则是我们进行“分类统计”和“不同元素个数统计”的前奏。value_counts()直接给出了每个类别的频数,而nunique()则告诉你一共有多少种不同的类别。如果nunique()的结果异常大(比如接近总行数),那可能意味着这个字段是ID或存在大量脏数据,不适合作为分类依据。

4. 核心操作一:分类统计(GroupBy Aggregation)

分类统计是数据分析的“灵魂”操作之一。在本题中,我们需要按“功能区”分组,然后对各个重金属浓度列进行统计。

4.1 基础分组聚合

使用groupby()方法。

# 按‘功能区’分组,计算各重金属浓度的平均值 group_mean = df.groupby('功能区')[['As', 'Cd', 'Cr', 'Pb']].mean() print("按功能区分组的平均浓度:") print(group_mean) # 同时计算多个统计量 group_stats = df.groupby('功能区')[['As', 'Cd', 'Cr', 'Pb']].agg(['mean', 'std', 'min', 'max', 'count']) print("\n按功能区分组的详细统计(多指标):") print(group_stats)

groupby('功能区')创建了一个分组对象,[['As', 'Cd', ...]]指定了我们要分析的列,.mean().agg()则指定了聚合函数。.agg()非常强大,可以传入一个函数列表(如['mean', 'std']),也可以传入一个字典来为不同列指定不同的聚合函数(如{'As': 'mean', 'Cd': ['min', 'max']})。

4.2 高级分组技巧与结果处理

分组后的结果是一个新的DataFrame,其索引默认是分组键(‘功能区’)。有时我们需要将分组键重置为普通列。

# 重置索引,让‘功能区’变回列 group_stats_reset = group_stats.reset_index() print("重置索引后的分组统计:") print(group_stats_reset.head()) # 更复杂的聚合:计算每个功能区污染最严重的元素(浓度最大值所在的列名) def top_pollutant(series): # series 是一个功能区下,所有重金属浓度列的一行数据(一个Series) return series.idxmax() # 返回最大值对应的索引(即元素名) df_group_max = df.groupby('功能区')[['As', 'Cd', 'Cr', 'Pb']].apply(lambda x: x.max().idxmax()) print("\n每个功能区主要污染物(浓度最高者):") print(df_group_max)

注意事项groupby操作默认会忽略缺失值(NaN)。如果你的数据有NaN,聚合函数(如mean)会跳过它们。如果你希望NaN被当作0或其他值参与计算,需要在分组前用fillna()处理。另外,分组操作可能会产生多层索引(MultiIndex),使用.reset_index().unstack()可以将其展平,便于后续分析和绘图。

5. 核心操作二:行/列数据计算(Apply, Vectorization)

接下来是计算每个采样点的综合污染指数。这需要用到每一行中多个列的数据。假设我们使用一个简单的综合指数公式:sqrt( (平均浓度)^2 + (最大浓度)^2 ) / 2。当然,实际建模中可能会用更专业的指数(如内梅罗指数),但计算逻辑相通。

5.1 低效循环 vs. 高效向量化

新手常犯的错误是使用循环:

# 不推荐:逐行循环,效率极低 def calc_index_row(row): conc = [row['As'], row['Cd'], row['Cr'], row['Pb']] avg = np.mean(conc) maxc = np.max(conc) return np.sqrt(avg**2 + maxc**2) / 2 # 这会非常慢,尤其数据量大时 df['index_loop'] = df.apply(calc_index_row, axis=1)

正确做法是使用向量化操作:

# 推荐:向量化计算,利用NumPy/Pandas的广播机制,效率极高 conc_cols = ['As', 'Cd', 'Cr', 'Pb'] df['avg_concentration'] = df[conc_cols].mean(axis=1) # 沿行方向求平均 df['max_concentration'] = df[conc_cols].max(axis=1) df['pollution_index'] = np.sqrt(df['avg_concentration']**2 + df['max_concentration']**2) / 2 print("计算综合污染指数后的数据前5行:") print(df[['点ID', 'avg_concentration', 'max_concentration', 'pollution_index']].head())

axis=1参数是关键,它指示操作沿着行的方向进行。df[conc_cols].mean(axis=1)会为每一行计算这四个浓度的平均值,返回一个长度与df相同的Series。整个计算过程没有显式循环,代码简洁,速度比循环快成百上千倍。

5.2 灵活应用Apply函数

虽然向量化是首选,但有些复杂逻辑无法直接向量化,这时可以使用apply函数。例如,我们想根据污染指数添加一个污染等级标签。

# 定义一个分类函数 def classify_pollution(index): if index < 10: return '清洁' elif index < 30: return '轻度污染' elif index < 60: return '中度污染' else: return '重度污染' # 对‘pollution_index’列应用这个函数 df['pollution_level'] = df['pollution_index'].apply(classify_pollution) print("\n添加污染等级后的数据:") print(df[['点ID', 'pollution_index', 'pollution_level']].head(10))

apply函数将自定义函数映射到Series的每个元素上。对于更复杂的、涉及多列的行级操作,可以使用df.apply(func, axis=1),但需注意其性能开销。

实操心得“向量化优先”是Python数据分析的性能黄金法则。在写任何for循环或apply之前,先思考能否用Pandas/Numpy的内置函数(如mean,sum,diff,shift)或数组运算完成。这不仅能提升代码速度,也使代码更清晰。对于简单的映射关系(如本例的分类),pd.cut()pd.qcut()函数是比apply更好的选择,它们直接基于数值区间进行分箱,效率更高。

6. 核心操作三:统计一列中不同元素的个数

现在我们有了一列新的分类数据pollution_level,我们需要统计每个污染等级有多少个采样点。这其实就是对分类变量进行频数统计。

6.1 使用value_counts()

这是最直接、最常用的方法。

level_counts = df['pollution_level'].value_counts() print("污染等级统计(默认降序):") print(level_counts) print(type(level_counts)) # 这是一个Series

value_counts()返回一个Series,索引是唯一的类别,值是对应的计数。它默认按计数降序排列。

6.2 控制排序与处理缺失值

# 按类别名称排序 level_counts_sorted = df['pollution_level'].value_counts().sort_index() print("\n污染等级统计(按类别名排序):") print(level_counts_sorted) # 包含缺失值的统计(如果存在) # df['pollution_level_with_na'] = df['pollution_level'].copy() # df.loc[0, 'pollution_level_with_na'] = np.nan # 模拟一个缺失值 # print(df['pollution_level_with_na'].value_counts(dropna=False)) # dropna=False 会统计NaN

6.3 获取唯一值列表与个数

有时我们不需要计数,只需要知道有哪些类别,或者有多少种类别。

unique_levels = df['pollution_level'].unique() n_unique_levels = df['pollution_level'].nunique() print(f"\n唯一的污染等级有:{unique_levels}") print(f"共有 {n_unique_levels} 种不同的污染等级")

unique()返回一个NumPy数组,包含所有唯一值(顺序是出现顺序)。nunique()返回唯一值的数量,默认忽略NaN。

注意事项value_counts()默认不包含NaN。如果你的数据有缺失,并且你想知道缺失了多少,需要使用dropna=False参数。另外,value_counts()的结果是一个Series,非常适合直接用于绘图(比如作为饼图的数据源)。nunique()在数据探索阶段非常有用,可以快速识别出那些本应是分类变量、但取值却异常多的字段(可能是数据录入错误)。

7. 核心操作四:使用Matplotlib绘制专业饼图

统计结果出来了,用饼图展示最直观。但Matplotlib默认的饼图可能有些“简陋”,我们需要调整一下让它更专业。

7.1 基础饼图绘制

import matplotlib.pyplot as plt # 设置中文字体(如果标签需要中文) plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 # 准备数据 labels = level_counts.index.tolist() # 等级标签 sizes = level_counts.values.tolist() # 对应数量 colors = ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3'] # 自定义颜色序列 # 创建图形 fig, ax = plt.subplots(figsize=(8, 8)) # 绘制饼图 wedges, texts, autotexts = ax.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90, textprops={'fontsize': 12}) # 美化 ax.set_title('采样点污染等级分布', fontsize=16, fontweight='bold', pad=20) # 设置autotexts(百分比数字)的样式 for autotext in autotexts: autotext.set_color('white') autotext.set_fontweight('bold') autotext.set_fontsize(11) # 确保饼图是正圆 ax.axis('equal') plt.tight_layout() plt.show()

ax.pie()是关键函数。autopct控制饼图内显示百分比的格式,startangle设置第一块饼的起始角度(90度表示从12点钟方向开始)。wedges,texts,autotexts分别返回了饼图扇形块、外部标签文本、内部百分比文本的对象,方便我们后续进行个性化设置。

7.2 高级美化:突出显示与图例

在汇报或论文中,我们可能希望突出显示最重要的部分(比如“重度污染”),并添加图例。

# 假设我们想突出“重度污染”部分 explode = (0, 0, 0, 0.1) # 只将第四块(对应‘重度污染’)突出0.1 fig, ax = plt.subplots(figsize=(10, 8)) wedges, texts, autotexts = ax.pie(sizes, explode=explode, labels=labels, colors=colors, autopct=lambda pct: f'{pct:.1f}%\n({int(pct/100.*sum(sizes))})', startangle=90, shadow=True) # 添加图例 ax.legend(wedges, labels, title="污染等级", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1)) ax.set_title('采样点污染等级分布(突出显示重度污染)', fontsize=16, fontweight='bold', pad=20) ax.axis('equal') plt.tight_layout() plt.show()

这里做了几处改进:

  1. explode参数:通过一个元组指定每块饼的“爆炸”距离,0表示不突出。
  2. 自定义autopct:使用了一个lambda函数,在百分比后面加上了实际数量,信息更丰富。
  3. shadow=True:添加了阴影,增加立体感。
  4. 添加图例:使用ax.legend(),并将wedgeslabels传进去。bbox_to_anchor参数用于将图例放置在图形外部右侧。

7.3 避免饼图陷阱与替代方案

虽然饼图直观,但在某些场景下需谨慎使用:

  • 类别过多(超过6-7个)时,饼图会显得杂乱,不易比较。此时条形图(Bar Chart)是更好的选择
  • 需要精确比较数值大小时,人眼对角度和面积的感知不如对长度敏感,条形图更佳。
# 使用条形图展示相同数据 fig, ax = plt.subplots(figsize=(10, 6)) bars = ax.bar(labels, sizes, color=colors) ax.set_ylabel('采样点数量', fontsize=12) ax.set_title('采样点污染等级分布(条形图)', fontsize=16, fontweight='bold', pad=20) # 在条形顶端添加数量标签 for bar in bars: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height + 0.5, f'{int(height)}', ha='center', va='bottom', fontsize=11) plt.tight_layout() plt.show()

实操心得:绘制用于报告或出版的图表时,细节决定专业度。务必注意:1)字体清晰可读,特别是中文字体配置;2)颜色搭配,使用色盲友好的配色方案(如viridis,plasma等Matplotlib色彩映射,或Tableau配色);3)信息密度适中,避免在图上堆砌过多文字;4)始终提供替代方案,如考虑用条形图补充或替代饼图。保存图表时,使用高DPI(如plt.savefig('output.png', dpi=300, bbox_inches='tight'))以确保印刷质量。

8. 完整流程串联与代码整合

现在,让我们把上述所有步骤串联起来,形成一个从数据加载到结果可视化的完整脚本。

# -*- coding: utf-8 -*- """ 数学建模(2011 A)风格数据分析完整流程示例 功能:分类统计、行计算、频数统计、饼图绘制 """ import pandas as pd import numpy as np import matplotlib.pyplot as plt # 1. 模拟/加载数据并创建安全副本 print("步骤1: 加载数据并创建副本...") np.random.seed(2023) n_samples = 500 data = { '点ID': range(1, n_samples+1), 'x坐标': np.random.uniform(0, 1000, n_samples), 'y坐标': np.random.uniform(0, 1000, n_samples), '功能区': np.random.choice([1, 2, 3, 4, 5], n_samples, p=[0.3, 0.25, 0.2, 0.15, 0.1]), 'As': np.random.lognormal(mean=2.0, sigma=0.8, size=n_samples), 'Cd': np.random.lognormal(mean=0.5, sigma=1.0, size=n_samples), 'Cr': np.random.lognormal(mean=3.5, sigma=0.6, size=n_samples), 'Pb': np.random.lognormal(mean=3.0, sigma=0.7, size=n_samples), } df_raw = pd.DataFrame(data) df = df_raw.copy() # 关键的安全拷贝 # 2. 分类统计:按功能区统计重金属浓度 print("\n步骤2: 按功能区进行重金属浓度分类统计...") conc_cols = ['As', 'Cd', 'Cr', 'Pb'] func_area_stats = df.groupby('功能区')[conc_cols].agg(['mean', 'std', 'max']) print(func_area_stats.round(2)) # 保留两位小数 # 3. 行计算:计算每个采样点的综合污染指数 print("\n步骤3: 计算每个采样点的综合污染指数...") df['avg_conc'] = df[conc_cols].mean(axis=1) df['max_conc'] = df[conc_cols].max(axis=1) df['pollution_index'] = np.sqrt(df['avg_conc']**2 + df['max_conc']**2) / 2 # 4. 分类与统计:划分污染等级并计数 print("\n步骤4: 划分污染等级并统计数量...") def classify_pollution(index): if index < 10: return '清洁' elif index < 30: return '轻度污染' elif index < 60: return '中度污染' else: return '重度污染' df['pollution_level'] = df['pollution_index'].apply(classify_pollution) level_counts = df['pollution_level'].value_counts() print("污染等级分布:") print(level_counts) # 5. 可视化:绘制饼图 print("\n步骤5: 绘制污染等级分布饼图...") plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False labels = level_counts.index.tolist() sizes = level_counts.values.tolist() colors = ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3'] explode = (0, 0, 0, 0.1) # 突出“重度污染” fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) # 子图1:饼图 wedges, texts, autotexts = ax1.pie(sizes, explode=explode, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90, shadow=True) for autotext in autotexts: autotext.set_color('white') autotext.set_fontweight('bold') ax1.set_title('污染等级分布饼图', fontsize=14, fontweight='bold') ax1.axis('equal') # 子图2:条形图(作为对比/补充) bars = ax2.bar(labels, sizes, color=colors, edgecolor='black') ax2.set_ylabel('采样点数量', fontsize=12) ax2.set_title('污染等级分布条形图', fontsize=14, fontweight='bold') for bar in bars: height = bar.get_height() ax2.text(bar.get_x() + bar.get_width()/2., height + 0.5, f'{int(height)}', ha='center', va='bottom', fontsize=11) ax2.grid(axis='y', linestyle='--', alpha=0.7) plt.suptitle('城市表层土壤重金属污染分析结果', fontsize=16, fontweight='bold') plt.tight_layout() plt.savefig('pollution_analysis_result.png', dpi=300, bbox_inches='tight') plt.show() print("\n分析完成!结果已保存至 'pollution_analysis_result.png'")

这个脚本整合了所有核心操作,并输出了一个包含饼图和条形图的对比可视化结果,更全面地展示了数据。

9. 常见问题与排查技巧实录

在实际操作中,你肯定会遇到各种报错和意外情况。下面是我总结的一些典型问题及其解决方法。

9.1 数据加载与类型问题

  • 问题:用pd.read_csv加载数据后,某些数值列被识别成了object类型,无法进行数学运算。
  • 排查:立即使用df.info()df.head()查看列数据类型和前几行数据。很可能数据中混入了非数字字符(如“-”、“NA”、“<0.01”)。
  • 解决
    1. 指定数据类型pd.read_csv('data.csv', dtype={'浓度列': float})
    2. 强制转换并处理错误pd.to_numeric(df['列名'], errors='coerce')errors='coerce'会将无法转换的值设为NaN。
    3. 清洗数据:使用df['列名'].replace(['-', 'NA'], np.nan, inplace=True)替换特定字符串。

9.2 GroupBy操作结果异常

  • 问题:分组后得到的统计量全是NaN,或者分组数量远多于预期。
  • 排查
    1. 检查分组键列是否存在大量NaN。groupby默认会忽略NaN,但如果你希望NaN自成一组,需要先填充或使用dropna=False参数(但需注意,这可能会将多个NaN行归为一组)。
    2. 检查分组键列的数据类型。如果是字符串,注意首尾空格(df['列名'].str.strip())。如果是数值型但想作为分类,确认其唯一值数量是否合理。
    3. 使用df['分组列'].value_counts(dropna=False)查看具体分布。
  • 解决:在分组前进行数据清洗,确保分组键列干净、类型正确。

9.3 Apply函数运行缓慢或内存溢出

  • 问题:对大数据集使用apply(axis=1)进行行计算时,程序变得极慢甚至崩溃。
  • 排查apply是灵活的,但本质上是Python级循环,对于大规模数据效率低下。
  • 解决
    1. 优先寻找向量化方法:这是根本解决方案。回顾第5节,大部分行计算都能转化为列与列之间的向量运算。
    2. 使用Swifter库:对于复杂的、确实无法向量化的函数,可以尝试import swifter; df.swifter.apply(...),它尝试利用多核并行加速。
    3. 分批处理:如果数据太大,考虑使用df.iloc[start:end]进行分批处理。

9.4 绘图时中文显示为方框

  • 问题:在Matplotlib图表中,中文字符显示为“□□□”。
  • 解决
    1. 指定中文字体(推荐):如前面代码所示,在绘图前设置plt.rcParams。你需要确保系统中存在指定的字体(如‘SimHei’是黑体,‘Microsoft YaHei’是微软雅黑)。
    2. 更稳健的方法(指定字体路径)
      import matplotlib zh_font = matplotlib.font_manager.FontProperties(fname='C:/Windows/Fonts/msyh.ttc') # 指定字体文件路径 # 然后在需要设置字体的地方传入fontproperties参数,如: ax.set_title('标题', fontproperties=zh_font) ax.legend(..., prop=zh_font)

9.5 保存的图片分辨率不足或布局错乱

  • 问题:保存的PNG图片在论文或报告中放大后模糊,或者饼图标签被截断。
  • 解决
    1. 提高DPIplt.savefig('output.png', dpi=300)。DPI(每英寸点数)越高,图像越清晰,文件也越大。300 DPI通常满足印刷要求。
    2. 使用bbox_inches='tight':这个参数会自动裁剪图片周围的空白区域,并确保所有图形元素(如标签、图例)都被包含在保存的图像中。plt.savefig('output.png', dpi=300, bbox_inches='tight')
    3. 调整图形尺寸:在创建图形时使用fig, ax = plt.subplots(figsize=(width, height))提前设置合适的宽高比。

9.6 性能优化小技巧

当处理非常大的DataFrame时(比如数十万行以上),一些操作会变慢。除了前面提到的向量化,还有以下技巧:

  • 选择合适的数据类型:使用df['列名'].astype('category')将低基数(唯一值少)的字符串列转换为分类类型,可以大幅节省内存和提高groupby速度。
  • 避免链式赋值:类似df[df['A']>0]['B'] = 1的操作可能引发SettingWithCopyWarning且效率不高。应使用df.loc[df['A']>0, 'B'] = 1
  • 使用查询(Query):对于复杂的布尔索引,df.query('A > 10 & B < 5')有时比df[(df['A']>10) & (df['B']<5)]更清晰,且在某些情况下性能略优。

数据处理和分析是一个“脏活累活”,80%的时间可能花在数据清洗和调试上。掌握这些常见的排查思路和技巧,能让你在遇到问题时不再慌张,快速定位并解决。记住,清晰的思路和稳健的代码习惯,比任何炫酷的技巧都更重要。从创建一个干净的数据副本开始,一步步验证,你的分析之路就会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询