1. 项目概述与核心价值
最近在复盘一些老项目,特别是数学建模竞赛的代码,发现很多数据处理和分析的思路,即便过了这么多年,依然非常经典和实用。就拿2011年的数学建模A题来说,题目本身是关于城市表层土壤重金属污染分析的,但其中涉及的数据处理环节——比如对大量采样点按功能区进行分类统计、计算污染指数、可视化展示污染构成——这些操作几乎是任何数据分析项目的通用模板。
很多朋友在学Python数据分析时,常常陷入一个误区:把pandas、matplotlib的每个函数都学一遍,但遇到真实项目,还是不知道从哪里下手,代码写得又慢又乱。其实,掌握几个核心的“组合拳”就能解决80%的问题。今天,我就以这个老题目为背景,不深究其复杂的数学模型,而是聚焦于如何用Python高效、清晰地完成数据预处理、统计分析和结果可视化这一整套流程。你会看到如何对DataFrame进行安全的复制以避免连锁错误,如何灵活地对每一行或每一列进行计算,如何快速统计某个特征的不同类别数量,以及如何用matplotlib画出既专业又美观的饼图来展示构成比例。
无论你是正在准备数学建模竞赛的学生,还是日常需要处理业务数据的分析师,甚至是刚开始接触Python数据处理的新手,这篇内容都能给你提供一套可直接“抄作业”的实战代码和避坑思路。我们用的工具无非就是pandas、numpy和matplotlib,但重点在于理解每一步“为什么”要这么做,以及有哪些细节决定了成败。
2. 数据处理的核心思路与设计
当我们拿到像2011年A题这样的数据集时,通常是一个包含了多个采样点、每个采样点有地理位置(如坐标)、所属功能区类型(如生活区、工业区、交通区等)以及多种重金属元素浓度值的表格。我们的分析目标很明确:首先,要按功能区进行分类,看看不同区域的污染状况有何不同;其次,可能需要计算每个采样点的综合污染指数(这通常涉及行方向的计算);然后,我们还需要知道整个区域或某个功能区里,究竟出现了哪几种重金属元素(这涉及列方向的值唯一性统计);最后,将污染物的构成比例用饼图直观地展示出来。
这个流程看似线性,但在用代码实现时,有几个关键的设计点需要提前想清楚,否则很容易写出低效或容易出错的代码。
2.1 为何要重视DataFrame的复制?
这是新手,甚至是一些有经验的开发者最容易踩坑的地方。Pandas的DataFrame在进行赋值或切片操作时,其行为并不总是符合直觉。
import pandas as pd # 假设df是我们的原始数据 df = pd.read_csv('soil_data.csv') # 方式一:直接赋值(这是危险的!) df_new = df # 方式二:使用.copy()方法(这是安全的) df_new_safe = df.copy()如果你用df_new = df,那么df_new仅仅是df的一个视图或说另一个引用。你对df_new进行的任何修改(例如df_new['pH'] = 7),都会直接且同步地反映到原始的df上。这在进行探索性数据分析时是灾难性的,因为你可能无意中污染了原始数据源,导致后续步骤全部基于被修改的错误数据。
而df.copy(deep=True)(deep=True是默认值)会创建数据的一个完全独立的副本,两者在内存中是分开的,互不影响。在开始任何数据清洗、转换或衍生计算之前,先做一次安全的复制,是一个必须养成的好习惯。这相当于在实验室里,把原始样品妥善保存,只在副本上进行各种试剂测试。
2.2 分类统计的逻辑与实现选择
“按功能区分类统计”通常意味着我们要计算每个功能区下,各个重金属浓度的平均值、中位数、最大值、最小值等。这立刻让人想到groupby操作。但这里有一个细节:我们的数据中,一个功能区(如“工业区”)下可能有数十个采样点,每个采样点有8种重金属的浓度。我们是想得到每个功能区下、每种重金属的一个统计值(例如平均浓度),这实际上是一个分组聚合操作。
df.groupby('功能区')['铅', '镉', '铬'].mean()这行代码就能生成一个新的DataFrame,其行索引是各个功能区,列是各种重金属,值是平均值。这是后续画图(比如分组柱状图)和对比分析的基础。
2.3 行、列计算与元素统计的场景辨析
在分析中,我们可能需要在两个维度上进行计算:
- 行方向计算(每一行数据计算):例如,计算每个采样点的内梅罗综合污染指数。这需要用到该采样点所有重金属的浓度值。我们可以使用
df.apply(lambda row: custom_function(row), axis=1),或者更向量化地,直接对多列进行运算df['综合指数'] = (df[['铅','镉']]**2).mean(axis=1)**0.5。axis=1这个参数指明了计算是沿着行方向进行的。 - 列方向统计:例如,我们想知道“砷”这一列中,到底出现了哪几种不同的浓度值(或者更常见的,想知道“功能区”这一列有哪几种类型)。这时我们会用到
df['功能区'].unique()来获取唯一值数组,或者用df['功能区'].nunique()直接得到唯一值的个数。这对于了解数据的基本构成,或者为后续的groupby分组提供依据非常有用。
2.4 可视化载体:为何选择饼图?
在本题中,要展示不同重金属对污染的“贡献率”或构成比例,饼图是一个直观的选择。它能清晰地显示各部分占总体的百分比。Matplotlib的plt.pie()函数是绘制饼图的核心。但默认生成的饼图可能比较简陋,我们需要关注几个关键参数来提升其表现力:autopct用于显示百分比,startangle用于调整起始角度使图表更美观,explode用于将某一部分“拉出来”以强调,以及colors用于设置一套协调的颜色。同时,为饼图的每一块添加清晰的图例(legend)是必不可少的,否则读者无法将色块与数据对应起来。
3. 核心工具链与数据准备
工欲善其事,必先利其器。我们这场数据分析“手术”需要一套标准且锋利的“手术刀”。
3.1 环境与库的配置
首先确保你的Python环境(推荐使用Anaconda,它集成了大部分科学计算库)中已安装以下核心库:
- pandas: 数据操作的基石,用于数据读取、清洗、转换和分析。
- numpy: 提供高效的数组运算和数学函数支持,pandas的底层依赖它。
- matplotlib: 绘图库,用于生成各种静态图表。
安装命令非常简单,在终端或命令提示符中执行:
pip install pandas numpy matplotlib如果你使用Jupyter Notebook或JupyterLab进行交互式分析,那将是更好的选择,因为它允许你分段执行代码并立即看到结果和图表。
3.2 模拟数据集的构建
由于原始竞赛数据不易公开获取,我们根据题目描述构建一个高度仿真的模拟数据集。这不仅能保护隐私,也更有利于我们理解数据结构和分析目标。
import pandas as pd import numpy as np import matplotlib.pyplot as plt # 设置随机种子以保证结果可复现 np.random.seed(2023) # 定义数据规模 n_samples = 200 # 200个采样点 functional_areas = ['生活区', '工业区', '交通区', '公园绿地区', '山区'] metals = ['铅(Pb)', '镉(Cd)', '铬(Cr)', '汞(Hg)', '砷(As)', '铜(Cu)', '锌(Zn)', '镍(Ni)'] # 生成模拟数据 data = { '采样点ID': [f'SP{i:03d}' for i in range(1, n_samples+1)], '经度': np.random.uniform(116.2, 116.6, n_samples).round(4), '纬度': np.random.uniform(39.8, 40.1, n_samples).round(4), '功能区': np.random.choice(functional_areas, n_samples, p=[0.3, 0.25, 0.2, 0.15, 0.1]) # 各区出现概率不同 } # 为每种重金属生成浓度数据,不同功能区设置不同的基准浓度和波动范围 # 模拟工业区污染更重 base_concentration = {'生活区': 10, '工业区': 50, '交通区': 30, '公园绿地区': 5, '山区': 2} for metal in metals: concentrations = [] for area in data['功能区']: base = base_concentration[area] # 添加随机波动和少量异常高值 conc = base * np.random.lognormal(mean=0, sigma=0.3) if np.random.random() < 0.02: # 2%的概率出现异常值 conc *= np.random.uniform(5, 10) concentrations.append(round(conc, 2)) data[metal] = concentrations # 创建DataFrame df_raw = pd.DataFrame(data) print("原始数据前5行:") print(df_raw.head()) print(f"\n数据形状:{df_raw.shape}") print(f"\n各功能区采样点数量:") print(df_raw['功能区'].value_counts())运行这段代码,我们会得到一个包含200行、12列(ID、坐标、功能区+8种金属)的DataFrame。看一眼数据预览和功能区分布,我们对数据集就有了初步的感知。
注意:在实际项目中,你的数据可能来自CSV、Excel或数据库。使用
pd.read_csv()或pd.read_excel()读取后,第一步也应该是像这样查看数据概览(df.head(),df.info(),df.describe()),了解数据规模、类型和基本统计信息。
4. 安全数据操作:DataFrame的复制与子集选取
现在,我们有了原始的df_raw。按照最佳实践,在开始任何分析之前,我们先创建一个工作副本。
# 关键步骤:创建数据副本 df = df_raw.copy(deep=True) print(f"原始df_raw的id: {id(df_raw)}") print(f"副本df的id: {id(df)}") print("两者id不同,说明是独立对象。")id()函数返回对象的内存地址,两个地址不同,证实了这是两个独立的对象。接下来,我们可能只需要关注重金属浓度数据,而暂时不需要坐标信息。这时,我们需要选取数据的子集。
# 选取需要分析的列,构成新的工作DataFrame metal_columns = ['铅(Pb)', '镉(Cd)', '铬(Cr)', '汞(Hg)', '砷(As)', '铜(Cu)', '锌(Zn)', '镍(Ni)'] df_metals = df[['功能区'] + metal_columns].copy() # 这里也做了copy,确保df_metals独立 print(f"分析用数据子集形状:{df_metals.shape}")这里有一个细节:df_metals = df[['功能区'] + metal_columns]这种切片操作返回的默认是一个视图。但紧接着我们调用了.copy(),所以df_metals也是独立的。如果确定后续不会修改df,且df_metals只是用于只读分析,可以省略这个.copy()以节省内存。但在不确定的情况下,加上.copy()是更稳妥的做法。
实操心得:我习惯在每一个可能产生新DataFrame的关键步骤后,都检查一下其
id或者通过简单的修改测试(如df_test.iloc[0,0] = 999,再查看原数据是否变化)来确认数据的独立性。尤其是在编写复杂的数据处理管道时,这个习惯能帮你节省大量调试时间。
5. 分类统计:洞见不同功能区的污染特征
我们的核心任务之一是比较不同功能区的污染状况。使用groupby可以轻松实现。
# 按功能区进行分组,并计算每种重金属的平均浓度 grouped_mean = df_metals.groupby('功能区')[metal_columns].mean().round(2) print("各功能区重金属平均浓度:") print(grouped_mean) # 除了平均值,我们可能还想看中位数(对异常值不敏感)和标准差(看数据波动) grouped_median = df_metals.groupby('功能区')[metal_columns].median().round(2) grouped_std = df_metals.groupby('功能区')[metal_columns].std().round(2) print("\n各功能区重金属浓度中位数:") print(grouped_median.head()) print("\n各功能区重金属浓度标准差:") print(grouped_std.head())观察grouped_mean这个DataFrame,行是功能区,列是重金属,值就是平均浓度。一眼就能看出,“工业区”的各项金属平均浓度远高于其他区域,这符合我们的常识预期。“山区”的浓度则普遍最低。grouped_std则告诉我们,同一功能区内的采样点,其浓度波动大小。例如,工业区的标准差可能也很大,说明区内污染分布很不均匀,有的点极重,有的点相对较轻。
为了更直观,我们可以快速可视化这个结果:
# 绘制各功能区铅(Pb)平均浓度的柱状图 plt.figure(figsize=(10, 6)) grouped_mean['铅(Pb)'].sort_values(ascending=False).plot(kind='bar', color='skyblue', edgecolor='black') plt.title('各功能区铅(Pb)平均浓度对比', fontsize=15, fontweight='bold') plt.xlabel('功能区') plt.ylabel('平均浓度 (mg/kg)') plt.xticks(rotation=45) # 旋转x轴标签 plt.grid(axis='y', linestyle='--', alpha=0.7) plt.tight_layout() plt.show()这张简单的柱状图已经能有力地支持“工业区污染最重”的结论。groupby是pandas中最强大的操作之一,结合agg函数可以一次性计算多种统计量:
# 使用agg进行聚合统计 summary_stats = df_metals.groupby('功能区')[metal_columns].agg(['mean', 'median', 'std', 'count']).round(2) # 查看铅(Pb)的统计摘要 print(summary_stats['铅(Pb)'])6. 行与列的计算:从点到面的污染评估
分类统计是从“面”(功能区)的视角看问题。接下来我们从“点”(每个采样点)的视角,评估每个点的综合污染情况。这里就需要用到行方向的计算。
6.1 计算每个采样点的综合污染指数
假设我们采用一种简化的综合指数计算方法:某点的综合指数 = 该点所有重金属浓度与各自背景值(这里用所有点该金属的平均值模拟)比值的平均值。这实际上是一个行方向的计算。
# 步骤1:计算每种重金属的背景值(这里用全局平均值模拟) background_values = df_metals[metal_columns].mean() print("背景值(全局平均):") print(background_values) # 步骤2:计算每个采样点各金属的污染系数(浓度/背景值) # 这里利用DataFrame的广播机制,每一行数据都会除以background_values这个Series pollution_coefficient = df_metals[metal_columns].div(background_values, axis='columns') print("\n前5个采样点的污染系数:") print(pollution_coefficient.head()) # 步骤3:计算每个采样点的综合污染指数(行方向求平均) # axis=1 表示沿着每一行进行计算 df_metals['综合污染指数'] = pollution_coefficient.mean(axis=1).round(3) print("\n添加了综合污染指数后的前5行数据:") print(df_metals[['功能区', '综合污染指数']].head())df.div(background_values, axis=‘columns’)是点睛之笔。axis=‘columns’指定了除法操作是按列对齐的,即df的每一列(每种金属)分别除以background_values中对应金属的值。pollution_coefficient.mean(axis=1)则是对每一行(每个采样点)的所有金属污染系数求平均值,得到了该点的综合指数。
6.2 统计单列不同元素个数
现在,假设我们想单纯地了解一下“功能区”这一列到底有多少种类型,以及每种类型的具体名称。这虽然简单,但在数据探索阶段非常有用。
# 方法1:获取唯一值数组 unique_areas = df_metals['功能区'].unique() print("功能区唯一值数组:", unique_areas) # 方法2:获取唯一值个数 num_unique_areas = df_metals['功能区'].nunique() print("功能区唯一值个数:", num_unique_areas) # 方法3:获取各唯一值的计数(频率分布) area_counts = df_metals['功能区'].value_counts() print("\n各功能区采样点数量分布:") print(area_counts)value_counts()的结果是一个按计数降序排列的Series,它比单纯看唯一值更能反映数据的分布情况。例如,我们可以立刻知道哪个功能区的采样点最多,数据是否均衡。
7. 数据可视化:用Matplotlib绘制专业饼图
有了综合污染指数,我们可以从整体上看看污染“贡献”主要来自哪些功能区。一种方法是将所有采样点按功能区分组,计算各组综合指数的总和,然后绘制饼图展示各区的“总污染负荷”占比。
7.1 准备饼图数据
# 计算各功能区综合污染指数的总和 pollution_by_area = df_metals.groupby('功能区')['综合污染指数'].sum().sort_values(ascending=False) print("各功能区总污染负荷:") print(pollution_by_area) # 计算百分比 pollution_percentage = (pollution_by_area / pollution_by_area.sum() * 100).round(1) print("\n各功能区污染负荷占比(%):") print(pollution_percentage)7.2 绘制基础饼图
plt.figure(figsize=(9, 9)) # 绘制饼图 # autopct: 显示百分比格式 # startangle: 起始角度,90度表示从12点钟方向开始 wedges, texts, autotexts = plt.pie(pollution_by_area.values, labels=pollution_by_area.index, autopct='%1.1f%%', startangle=90, colors=plt.cm.Set3(np.arange(len(pollution_by_area))/len(pollution_by_area))) # 使用Set3色图 # 美化字体 for autotext in autotexts: autotext.set_color('black') autotext.set_fontsize(11) autotext.set_fontweight('bold') plt.title('各功能区总污染负荷占比', fontsize=16, fontweight='bold', pad=20) plt.show()这张图已经能说明问题,但我们可以做得更专业、更突出。
7.3 绘制强调重点的爆炸式饼图
假设我们想强调“工业区”的贡献,可以将其扇形略微分离。
# 创建“爆炸”距离数组,对应每个扇形 explode = [0, 0.1, 0, 0, 0] # 只“炸开”第二个扇形(工业区) # 确保explode顺序与数据顺序一致 sorted_areas = pollution_by_area.index.tolist() if '工业区' in sorted_areas: idx = sorted_areas.index('工业区') explode = [0] * len(sorted_areas) explode[idx] = 0.1 plt.figure(figsize=(10, 10)) wedges, texts, autotexts = plt.pie(pollution_by_area.values, explode=explode, labels=pollution_by_area.index, autopct=lambda pct: f'{pct:.1f}%\n({pct*sum(pollution_by_area.values)/100:.0f})', startangle=120, shadow=True, # 添加阴影 colors=plt.cm.Paired(np.arange(len(pollution_by_area)))) # 使用Paired色图 # 进一步美化文本 plt.setp(autotexts, size=10, weight="bold", color='darkred') plt.setp(texts, size=11) # 添加图例,并放到图表外侧 plt.legend(wedges, pollution_by_area.index, title="功能区", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1)) # 将图例放在图表右侧 plt.title('各功能区总污染负荷占比(突出工业区)', fontsize=16, fontweight='bold', pad=20) plt.tight_layout() # 自动调整布局,为图例留出空间 plt.show()在这段代码中,我们做了几处优化:
explode参数使“工业区”的扇形分离,视觉上得到强调。autopct使用了自定义函数,同时显示百分比和实际数值,信息更丰富。shadow=True增加了立体感。- 使用了更美观的
Paired颜色映射。 - 将图例放置在图表右侧,避免了与标签的重叠,使图表更清晰。
注意事项:饼图适用于展示部分与整体的比例关系,且部分数量不宜过多(通常不超过6-7个)。如果类别太多,饼图会显得杂乱,此时考虑使用条形图(柱状图)会更合适。另外,当各部分比例相差不大时,饼图是有效的;如果某个部分占比超过50%,饼图也能很好体现。Matplotlib的饼图默认不会自动显示百分比,必须通过
autopct参数设置。
8. 完整流程串联与代码封装
我们将上述所有步骤整合到一个连贯的、可复用的分析流程中,并封装成函数,方便应用于类似的数据集。
def analyze_soil_pollution_data(df_raw, metal_list, area_col='功能区'): """ 对土壤重金属污染数据进行完整分析。 参数: df_raw: 原始DataFrame,包含功能区列和多种金属浓度列。 metal_list: 重金属列名的列表。 area_col: 功能区列的名称。 返回: 包含多个结果的字典。 """ results = {} # 1. 数据安全复制与准备 df_work = df_raw[[area_col] + metal_list].copy() results['df_work'] = df_work # 2. 分类统计(平均值) area_mean = df_work.groupby(area_col)[metal_list].mean().round(3) results['area_mean'] = area_mean # 3. 计算综合污染指数(行方向计算) background = df_work[metal_list].mean() pollution_idx = df_work[metal_list].div(background, axis='columns').mean(axis=1).round(3) df_work['综合污染指数'] = pollution_idx results['background'] = background results['df_with_index'] = df_work # 4. 按功能区汇总污染负荷 total_pollution_by_area = df_work.groupby(area_col)['综合污染指数'].sum().sort_values(ascending=False) results['total_pollution_by_area'] = total_pollution_by_area # 5. 统计功能区类别 area_stats = { 'unique_areas': df_work[area_col].unique().tolist(), 'num_areas': df_work[area_col].nunique(), 'area_counts': df_work[area_col].value_counts().to_dict() } results['area_stats'] = area_stats return results # 使用函数进行分析 metal_cols = ['铅(Pb)', '镉(Cd)', '铬(Cr)', '汞(Hg)', '砷(As)', '铜(Cu)', '锌(Zn)', '镍(Ni)'] analysis_results = analyze_soil_pollution_data(df_raw, metal_cols, '功能区') # 打印部分结果 print("各功能区平均浓度:") print(analysis_results['area_mean']) print("\n背景浓度值:") print(analysis_results['background']) print("\n各功能区总污染负荷:") print(analysis_results['total_pollution_by_area']) print("\n功能区统计信息:") print(f"唯一值列表:{analysis_results['area_stats']['unique_areas']}") print(f"个数:{analysis_results['area_stats']['num_areas']}")这个函数将数据复制、分组统计、行计算、汇总和基本统计全部打包,返回一个包含所有中间结果和最终结果的字典。这种结构化的输出非常有利于后续的进一步分析或报告生成。
9. 常见问题与排查技巧实录
在实际操作中,你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方法。
9.1 数据读取与编码问题
问题:用pd.read_csv()读取数据时,出现UnicodeDecodeError,或者中文显示为乱码。排查:这通常是因为文件保存的编码格式与read_csv默认的utf-8不一致。常见的编码还有gbk、gb2312、latin1等。解决:
# 尝试不同的编码 try: df = pd.read_csv('data.csv', encoding='utf-8') except UnicodeDecodeError: try: df = pd.read_csv('data.csv', encoding='gbk') except UnicodeDecodeError: df = pd.read_csv('data.csv', encoding='latin1')更稳妥的方法是,用文本编辑器(如VS Code、Notepad++)打开CSV文件,查看其编码格式,然后在read_csv中明确指定。
9.2 Groupby操作后数据结构混乱
问题:执行groupby().mean()后,得到的DataFrame的列变成了多层索引(MultiIndex),不方便后续操作。排查:当你对多列进行聚合,且聚合函数不止一个时,就会产生多层列索引。解决:
# 聚合前,如果只需要单列 area_pb_mean = df.groupby('功能区')['铅(Pb)'].mean() # 聚合后,如果列是多层索引,可以将其压平 summary = df.groupby('功能区')[metal_cols].agg(['mean', 'std']) summary.columns = ['_'.join(col).strip() for col in summary.columns.values] # 或者直接选取你需要的层级 pb_mean = summary[('铅(Pb)', 'mean')]9.3 饼图标签重叠或显示不全
问题:当饼图分区较多或百分比很小时,标签和百分比文字会挤在一起,难以辨认。排查:默认的标签布局算法可能无法处理复杂情况。解决:
- 使用图例代替扇区标签:这是最清晰的方法。设置
labels=None,然后通过plt.legend()添加图例,如7.3节所示。 - 调整字体和位置:通过
textprops参数调整标签字体大小,或手动调整autotexts和texts的位置(较复杂)。 - 过滤小扇区:将占比小于某个阈值(如2%)的扇区合并为“其他”类别。
threshold = 2.0 # 百分比阈值 pollution_by_area_filtered = pollution_by_area[pollution_percentage >= threshold] other_sum = pollution_by_area[pollution_percentage < threshold].sum() if other_sum > 0: pollution_by_area_filtered['其他'] = other_sum # 然后用过滤后的数据画图
9.4 行计算(axis=1)速度慢
问题:当DataFrame行数很大(几十万以上)时,使用df.apply(func, axis=1)进行行计算会异常缓慢。排查:apply(axis=1)是逐行操作的,在Python层面循环,效率低下。解决:尽量使用向量化操作。例如,计算综合指数时,我们用的是df[metal_cols].div(background, axis=‘columns’).mean(axis=1),这完全是pandas/numpy的向量化计算,比apply快几个数量级。如果计算逻辑确实复杂,无法向量化,可以考虑使用numba加速,或者将数据转换为numpy数组后用np.apply_along_axis,但后者提升有限。终极方案是重新审视计算逻辑,看能否用矩阵运算表达。
9.5 图表中文字体显示为方框
问题:在Matplotlib图表中,中文字符显示为小方框。排查:系统缺少中文字体,或Matplotlib没有使用中文字体。解决:最一劳永逸的方法是配置Matplotlib的字体。
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans'] # 指定默认字体 plt.rcParams['axes.unicode_minus'] = False # 解决负号‘-’显示为方块的问题确保你的系统中已安装这些字体(如SimHei是黑体)。在Jupyter中,这段配置代码通常需要放在绘图代码之前,或者写在单独的配置文件中。
整个流程走下来,从数据的安全复制、分类聚合、行列计算到最终的可视化,我们不仅完成了一个具体的数据分析案例,更掌握了一套应对类似问题的通用方法和避坑指南。数据处理的核心在于对工具(pandas)内在逻辑的理解和对数据本身结构的洞察,而可视化则是将这种洞察转化为直观故事的最后一环。记住,多动手试错,多看文档,遇到报错把错误信息完整地复制出来去搜索,你解决问题的能力会增长得飞快。