1. 从数据到图表:一个Python数据分析师的日常起点
如果你刚开始用Python处理数据,或者经常需要把一堆数字变成直观的图表,那么“用Python读取多列数据并用matplotlib画图”这个操作,几乎就是你的日常起手式。这听起来简单,但里面藏着不少能让效率翻倍、让图表更专业的细节。很多人拿到一个CSV或Excel文件,第一反应就是用pandas的read_csv读进来,然后plt.plot画出去,结果不是编码报错,就是图表丑得没法看,或者数据根本对不上。这背后涉及数据读取的陷阱、matplotlib的默认审美,以及如何把多列数据组织成有意义的可视化故事。
我处理过大量从传感器、日志文件、业务报表导出的多列数据集,核心痛点从来不是“会不会画”,而是“怎么画得对、画得好、画得快”。比如,一个包含时间、温度、湿度、电压四列的数据文件,你是画四条线在一个图里,还是分成四个子图?坐标轴刻度怎么设置才清晰?数据里有缺失值怎么办?这次,我就以这个最基础的场景为切入点,拆解从读取到出图的完整链条,分享那些官方文档里不会写的、我踩过坑后才总结出的实战经验。无论你是学生处理实验数据,还是工程师分析监控日志,这些步骤都能直接套用。
2. 数据读取:远不止pd.read_csv那么简单
读取数据是第一步,也是最容易埋坑的一步。你的数据源可能千奇百怪,但核心逻辑是相通的:将外部数据无损、正确地加载到Python的数据结构中(通常是pandas的DataFrame),为后续绘图做好准备。
2.1 核心工具选型:为什么是Pandas?
虽然Python内置了csv模块,第三方库也有xlrd、openpyxl等,但对于读取多列数据并进行图表制作,Pandas是几乎唯一的选择。原因很简单:它提供了一个名为DataFrame的二维表格型数据结构,这与我们的多列数据(如Excel表)在概念上完全一致。DataFrame的每一列可以有不同的数据类型(数字、字符串、时间),并且它原生与matplotlib集成,绘图时可以直接按列名调用数据,极其方便。相比之下,用纯csv.reader读出来的列表嵌套列表,你需要手动处理列索引、类型转换和缺失值,繁琐且易错。
2.2 不同数据源的读取实战与避坑指南
假设我们有一个名为sensor_data.csv的文件,包含timestamp(时间戳)、temperature(温度)、humidity(湿度)、voltage(电压)四列。
1. 读取CSV文件——默认参数下的陷阱
import pandas as pd # 最常见,但也最容易出错的写法 df = pd.read_csv('sensor_data.csv') print(df.head())这段代码能工作,但隐患很多:
- 编码问题:如果文件包含中文或其他非ASCII字符,默认的
utf-8编码可能报错。你需要指定encoding='gbk'或encoding='utf-8-sig'。 - 表头识别:
read_csv默认将第一行作为列名(header=0)。如果你的文件没有表头,需要设置header=None,然后通过df.columns = ['timestamp', 'temperature', 'humidity', 'voltage']手动指定。 - 时间戳解析:
timestamp列会被读成字符串(object类型)。对于绘图,特别是时间序列图,我们需要将其转换为datetime类型。应该在读取时或读取后立即转换。 - 缺失值标记:默认将空单元格读作
NaN,但有时数据中用-999、NULL表示缺失。需要用na_values参数指定。
稳健的读取方式应该是这样的:
import pandas as pd df = pd.read_csv( 'sensor_data.csv', encoding='utf-8-sig', # 处理带BOM的UTF-8或中文 parse_dates=['timestamp'], # 将指定列解析为日期时间 na_values=['NA', 'NULL', '-999'], # 将特定字符串识别为缺失值 # thousands=',', # 如果数字有千位分隔符,如“1,234” # comment='#' # 忽略以#开头的行(注释) ) # 检查数据类型和基本信息 print(df.info()) print(df.head())2. 读取Excel文件——注意版本与引擎对于.xlsx或.xls文件,使用pd.read_excel。需要额外安装openpyxl(用于.xlsx)或xlrd(用于旧版.xls,注意版本兼容性)。
# 确保已安装:pip install openpyxl df_excel = pd.read_excel('sensor_data.xlsx', sheet_name=0, engine='openpyxl')注意:
sheet_name参数可以指定工作表名或索引(从0开始)。如果文件较大,openpyxl引擎比默认的xlrd(对于.xlsx)内存效率更高。
3. 处理其他格式或特殊情况
- 从数据库读取:使用
pd.read_sql_query,需要配合SQLAlchemy等库建立连接。 - 读取剪切板数据:
pd.read_clipboard(),临时分析时非常方便。 - 读取JSON数据:如果数据是JSON格式的,特别是嵌套结构,需要先用
json.loads解析,再通过pd.json_normalize将其“扁平化”为DataFrame。
关键经验:读取数据后,立刻使用df.info()和df.head()查看数据概览和前几行,用df.isnull().sum()检查缺失值数量。这一步的仔细能避免后续绘图时80%的诡异错误。
3. 数据清洗与预处理:让图表数据“干净”可用
直接从文件读入的数据很少是完美的。在投入matplotlib之前,必须进行清洗和预处理,否则画出的图可能是扭曲或错误的。
3.1 处理缺失值:删除还是填充?
多列数据中,某一行的某个传感器可能失效,导致该列出现缺失值(NaN)。matplotlib无法直接绘制NaN。
- 删除法:如果缺失值很少,可以直接删除所在行。
df.dropna(inplace=True)。但需谨慎,这可能破坏数据的时间连续性。 - 填充法:更常用的方法是填充。对于时间序列,常用前后值的均值或插值法填充。
选择哪种方法取决于你的数据特性和业务逻辑。对于温度数据,线性插值可能比前向填充更合理。# 用前一行的值填充(前向填充),适合变化缓慢的数据 df.fillna(method='ffill', inplace=True) # 或用线性插值 df.interpolate(method='linear', inplace=True)
3.2 数据类型转换与索引设置
- 确保数值列是数字类型:有时数字会被读成字符串(object类型),需要转换。
df['temperature'] = pd.to_numeric(df['temperature'], errors='coerce')errors='coerce'会将无法转换的字符串变为NaN,便于后续统一处理。 - 将时间列设为索引:对于时间序列图,将时间戳设为DataFrame的索引会极大方便绘图。
设置后,df.set_index('timestamp', inplace=True)df.plot()会默认将索引作为X轴。
3.3 数据切片与多列选取
我们可能不需要绘制所有列,或者需要基于条件筛选部分数据。
# 选取特定的列 cols_to_plot = ['temperature', 'humidity'] df_subset = df[cols_to_plot] # 基于时间范围切片(设置时间索引后) df_recent = df['2023-10-01':'2023-10-31'] # 基于条件筛选,例如只绘制温度高于25度的数据 df_high_temp = df[df['temperature'] > 25]预处理后的DataFrame,才是matplotlib绘图的最佳原料。
4. Matplotlib绘图核心:从基础图表到专业定制
数据准备好了,现在进入核心的绘图环节。Matplotlib功能强大但默认样式比较简陋,我们需要一步步把它调整成专业、易懂的图表。
4.1 单图绘制多列数据:折线图实战
假设我们要在一个坐标系里绘制温度和湿度随时间的变化。
import matplotlib.pyplot as plt # 方法1:直接使用DataFrame的plot方法(最简洁) df[['temperature', 'humidity']].plot(figsize=(10, 6)) plt.title('Temperature and Humidity Over Time') plt.xlabel('Timestamp') plt.ylabel('Values') plt.grid(True, linestyle='--', alpha=0.7) plt.tight_layout() # 自动调整子图参数,避免标签重叠 plt.show()df[['col1', 'col2']].plot()会以DataFrame的索引为X轴,将选中的两列分别绘制为两条折线,并自动生成图例。figsize=(width, height)控制图片大小(单位英寸)。
方法2:使用面向对象的API(更灵活,推荐用于复杂图表)
fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(df.index, df['temperature'], label='Temperature (°C)', linewidth=2) ax.plot(df.index, df['humidity'], label='Humidity (%)', linewidth=2, linestyle='--') ax.set_title('Temperature and Humidity Over Time', fontsize=14) ax.set_xlabel('Timestamp', fontsize=12) ax.set_ylabel('Values', fontsize=12) ax.legend(loc='upper left') ax.grid(True, linestyle=':', alpha=0.5) # 设置X轴时间格式,避免重叠 fig.autofmt_xdate() plt.tight_layout() plt.show()面向对象的方式将图形(Figure)和坐标系(Axes)分开,你可以对ax进行更精细的控制,例如添加第二个Y轴。
4.2 多子图绘制:并排对比不同指标
当多列数据的量纲差异大(如温度单位是°C,电压单位是V),放在一起会让某一根线“压扁”另一根。这时应该使用子图(Subplots)。
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(12, 8), sharex=True) # 2行1列,共享X轴 # 第一个子图:温度 axes[0].plot(df.index, df['temperature'], color='tab:red', linewidth=1.5) axes[0].set_ylabel('Temperature (°C)', fontsize=11) axes[0].set_title('Temperature Trend', fontsize=12) axes[0].grid(True, alpha=0.3) # 第二个子图:湿度 axes[1].plot(df.index, df['humidity'], color='tab:blue', linewidth=1.5) axes[1].set_xlabel('Timestamp', fontsize=11) axes[1].set_ylabel('Humidity (%)', fontsize=11) axes[1].set_title('Humidity Trend', fontsize=12) axes[1].grid(True, alpha=0.3) fig.suptitle('Sensor Data Analysis', fontsize=16, y=1.02) # 总标题 plt.tight_layout() plt.show()sharex=True让两个子图共享X轴,节省空间且对齐时间点。你可以通过axes[0]、axes[1]来分别操作每个子图。
4.3 样式美化:告别“科研风”默认图表
Matplotlib的默认样式被戏称为“科研风”,用于报告或展示不够美观。美化主要涉及以下几点:
- 设置中文字体(如果需要):
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 - 使用Seaborn风格或自定义rcParams:
import seaborn as sns sns.set_style("whitegrid") # 设置Seaborn风格,会让图表立即变得美观 # 或者直接修改matplotlib的全局参数 plt.rcParams['figure.dpi'] = 150 # 提高分辨率 plt.rcParams['savefig.dpi'] = 300 # 保存图片的分辨率 plt.rcParams['axes.labelsize'] = 12 plt.rcParams['axes.titlesize'] = 14 - 定制颜色和线型:不要用默认的颜色循环。可以使用
tab10等色彩映射,或者手动指定。colors = ['#2E86AB', '#A23B72', '#F18F01'] # 自定义颜色 linestyles = ['-', '--', '-.', ':'] for i, col in enumerate(['temperature', 'humidity', 'voltage']): ax.plot(df.index, df[col], color=colors[i], linestyle=linestyles[i], label=col)
5. 高级技巧与实战场景深度解析
掌握了基础绘图后,我们来看几个能显著提升图表表现力和分析效率的高级场景。
5.1 双Y轴图表:关联两个量纲不同的指标
有时我们需要对比两个量纲不同但存在关联的指标,比如温度(°C)和电压(V)。使用双Y轴(次坐标轴)非常有效。
fig, ax1 = plt.subplots(figsize=(10, 6)) color = 'tab:red' ax1.set_xlabel('Timestamp') ax1.set_ylabel('Temperature (°C)', color=color) # 绘制温度,并设置颜色 line1 = ax1.plot(df.index, df['temperature'], color=color, label='Temperature') ax1.tick_params(axis='y', labelcolor=color) # 创建共享X轴的第二个Y轴 ax2 = ax1.twinx() color = 'tab:blue' ax2.set_ylabel('Voltage (V)', color=color) # 绘制电压 line2 = ax2.plot(df.index, df['voltage'], color=color, linestyle='--', label='Voltage') ax2.tick_params(axis='y', labelcolor=color) # 合并图例(需要一点技巧) lines = line1 + line2 labels = [l.get_label() for l in lines] ax1.legend(lines, labels, loc='upper left') plt.title('Temperature and Voltage Correlation') fig.tight_layout() plt.show()关键点是ax2 = ax1.twinx(),它创建了一个与ax1共享X轴的新Y轴。注意图例的合并需要手动处理线条和标签。
5.2 动态数据更新与实时图表模拟
对于需要监控实时数据的场景(如传感器数据流),我们可以使用FuncAnimation来制作动态图表。这里模拟一个不断追加新数据点的场景。
import matplotlib.animation as animation import numpy as np fig, ax = plt.subplots(figsize=(10, 5)) x_data, y_data = [], [] # 初始化空列表存储数据 line, = ax.plot([], [], 'b-') ax.set_xlim(0, 100) # 预设X轴范围 ax.set_ylim(20, 30) # 预设Y轴范围 ax.set_xlabel('Time Step') ax.set_ylabel('Value') ax.set_title('Real-time Data Simulation') ax.grid(True) def init(): line.set_data([], []) return line, def update(frame): # 模拟新数据:这里用随机数,实际中从队列或文件读取 new_y = np.random.rand() * 5 + 22 x_data.append(frame) y_data.append(new_y) # 保持最近100个点 if len(x_data) > 100: x_data.pop(0) y_data.pop(0) line.set_data(x_data, y_data) ax.set_xlim(max(0, frame-100), frame+10) # X轴动态右移 return line, ani = animation.FuncAnimation(fig, update, frames=range(200), init_func=init, blit=True, interval=100) plt.show() # 如需保存为GIF:ani.save('realtime.gif', writer='pillow')这个例子展示了动态图表的骨架。在实际应用中,update函数里的数据来源应替换为你的实际数据流(如从串口、网络或文件尾部读取)。
5.3 批量处理与自动化出图
当你有几十个类似的数据文件需要生成图表时,手动操作是不可接受的。我们需要脚本化、批量化的能力。
import os import glob # 找到所有CSV文件 data_files = glob.glob('./data/*.csv') output_dir = './charts/' os.makedirs(output_dir, exist_ok=True) for file_path in data_files: # 读取数据 df = pd.read_csv(file_path, parse_dates=['timestamp']) df.set_index('timestamp', inplace=True) # 创建图表 fig, axes = plt.subplots(2, 1, figsize=(10, 8)) df['temperature'].plot(ax=axes[0], title='Temperature', color='red') df['humidity'].plot(ax=axes[1], title='Humidity', color='blue') # 格式化 for ax in axes: ax.set_xlabel('') ax.grid(True) fig.suptitle(os.path.basename(file_path)) plt.tight_layout() # 保存图片 output_path = os.path.join(output_dir, os.path.basename(file_path).replace('.csv', '.png')) plt.savefig(output_path, dpi=150) plt.close(fig) # 关键!关闭图形以释放内存 print(f"已完成 {len(data_files)} 个图表的生成。")关键点在于使用循环遍历文件,并在每次保存后调用plt.close(fig)。如果不关闭,所有图形对象会留在内存中,处理大量文件时可能导致内存耗尽。
6. 性能优化与常见“坑”点排查
当数据量很大(几十万、上百万行)时,绘图可能变得非常缓慢。此外,一些细节问题会导致图表出现意料之外的结果。
6.1 大数据量绘图性能优化
- 数据降采样:在绘图前,对数据进行聚合或均匀采样。例如,如果你有每秒一个点、长达一个月的温度数据,画成折线图时屏幕像素根本分辨不出每个点。可以按小时或天取平均值。
# 假设df.index是DatetimeIndex,按小时重采样并取均值 df_resampled = df['temperature'].resample('1H').mean() df_resampled.plot() - 使用更高效的后端:Matplotlib默认使用GUI后端进行交互式显示。在脚本中批量生成图片时,可以使用非交互式后端(如
Agg),它能更快地生成图片文件。import matplotlib matplotlib.use('Agg') # 必须在导入pyplot之前设置 import matplotlib.pyplot as plt - 简化图表元素:关闭不必要的网格、图例(如果需要),或者使用更简单的线型(
linewidth=0.5)。
6.2 常见问题排查清单
- 图表不显示或只显示空白:
- 检查是否调用了
plt.show()(在脚本中)或在Jupyter Notebook中是否使用了%matplotlib inline魔法命令。 - 检查数据中是否包含NaN或Inf值。用
df.isnull().sum()和np.isinf(df).sum()检查。 - 检查X轴和Y轴的数据范围。可能你的数据点都在
[1e6, 1e6+1]这个极小的区间内,而Y轴范围默认是从0开始,导致线被“压”在顶部。使用ax.set_ylim(df['col'].min()*0.9, df['col'].max()*1.1)手动设置。
- 检查是否调用了
- 中文显示为方框:
- 确保已正确设置中文字体rcParams(见4.3节)。
- 在某些环境中,可能需要指定字体文件的绝对路径。
- 如果保存的图片中文字体丢失,尝试在
savefig时指定bbox_inches='tight'。
- 图例显示不正常或重复:
- 确保在每次
ax.plot()时都指定了label参数。 - 如果你在循环中绘图,确保没有重复调用
ax.legend()。 - 使用
handles, labels = ax.get_legend_handles_labels()获取当前的图例项,检查是否有重复。
- 确保在每次
- 保存的图片分辨率低或尺寸不对:
plt.savefig('figure.png', dpi=300)通过dpi参数提高分辨率。- 保存前使用
plt.tight_layout()自动调整布局,防止标签被截断。 - 保存的图片尺寸由
figsize参数决定,与屏幕显示的大小无关。
6.3 我的个人工具箱与习惯
最后,分享几个让我事半功倍的小习惯:
- 使用Jupyter Notebook/Lab进行探索:在最终脚本化之前,用Notebook进行交互式的数据读取、清洗和绘图尝试,非常高效。
- 封装常用绘图函数:如果你经常绘制风格一致的图表,将绘图代码封装成函数,传入DataFrame和列名即可出图,能节省大量重复劳动。
- 图片元数据与版本管理:在保存图片时,我习惯在文件名中包含关键参数或日期,如
temperature_trend_20231027_v1.png。对于重要图表,我还会用fig.text()在图片角落添加一行小字,注明数据来源、生成脚本版本和日期。 - 矢量图与位图的选择:对于论文或印刷出版物,保存为PDF或SVG格式的矢量图,放大不会失真。对于网页展示,PNG格式更合适。使用
plt.savefig('figure.pdf', format='pdf')来保存。