1. 这不是“画图教程”而是一套数据可视化工作流的实战复盘你打开Jupyter Notebook加载了一个CSV文件pandas读进来了df.head()看了前五行心里却没底这堆数字到底在说什么是整体趋势向上还是局部波动剧烈是两列之间存在强相关还是纯属噪声干扰这时候你本能地想画个图——但问题来了该选折线图还是散点图x轴用原始时间戳还是转换成datetimey轴要不要加对数刻度图例放哪才不遮挡关键数据点这些看似琐碎的决策恰恰决定了你能否在30秒内让同事看懂数据背后的真相。我做数据分析和可视化项目超过八年带过二十多个跨行业团队从电商GMV归因到工业传感器时序诊断最常被问的问题不是“怎么画”而是“为什么这么画”。这篇内容就围绕Data Visualization using Pandas, NumPy and Matplotlib Python Libraries这个标题展开它表面是三个库的组合使用实则是一条贯穿数据清洗、特征提炼、视觉编码、认知校准的完整链路。我会直接带你走一遍真实项目中的操作路径从原始数据加载开始到最终生成一张能放进周报PPT、经得起业务方追问的图表为止。不讲抽象理论不堆API参数所有步骤都来自我去年为某新能源车企做的电池衰减分析项目——那张最终被写进董事会简报的温度-容量散点图就是用这三件套完成的。如果你刚学完pandas基础正卡在“知道怎么算均值但不知道怎么把均值讲清楚”的阶段或者你已能写出复杂groupby却总被反馈“图太花哨看不出重点”那你接下来读的每一行都是我踩过坑后留下的实操标记。2. 整体设计思路为什么必须用这三件套打底而不是直接上Seaborn或Plotly2.1 不是“选工具”而是“建认知锚点”很多人一上来就问“Seaborn一行代码就能出热力图为啥还要学Matplotlib底层”这个问题背后藏着一个关键误区把可视化当成“输出动作”而非“思考过程”。我在给某医疗AI公司做CT影像标注数据质量分析时发现他们用Seaborn画了几十张分布图但没人注意到横轴单位是像素还是毫米——因为seaborn自动缩放坐标轴掩盖了原始量纲问题。而Matplotlib强制你显式声明plt.xlim()、plt.xticks()这个“多写两行”的过程恰恰逼你停下来确认“这个x轴代表什么物理意义它的合理范围是多少”这就是认知锚点每一条手动设置的刻度、每一个手写的图例标签都在强化你对数据本质的理解。NumPy和pandas同理。当你用np.where(df[voltage] 4.2, overcharge, normal)生成新列时你是在定义业务规则当用pandas.cut()做电压区间分箱时你是在构建分析维度。这些操作不是为了“让代码跑通”而是为了把模糊的业务语言如“电池快充时容易老化”翻译成可计算、可验证的数据逻辑。所以这三件套的组合本质是搭建一套数据思维脚手架pandas负责结构化表达NumPy处理数值逻辑Matplotlib完成视觉映射。跳过其中任何一环就像盖楼不打地基——短期能出图长期必返工。2.2 场景适配性什么时候必须回归“三件套”原生能力我统计过近三年接手的57个可视化需求有四类场景几乎无法用高级封装库替代多子图精密排版某风电场SCADA系统需要在同一画布展示风速时序图顶部、功率-风速散点图中部、故障率热力图底部且要求三个子图共享x轴时间刻度y轴独立缩放。Seaborn的FacetGrid无法控制子图间距Plotly的subplots在导出PDF时字体易失真最终用plt.subplot_mosaic()配合gridspec精准定位误差控制在0.5mm内。动态阈值标注电池BMS日志中充电截止电压随温度变化。需在折线图上实时绘制一条弯曲的红色虚线作为安全阈值。这要求plt.axhline()无法实现必须用plt.plot()传入动态计算的temp_array和voltage_threshold_array而这依赖NumPy向量化计算。自定义统计聚合电商用户行为分析中要画“不同城市用户平均下单间隔时间”的柱状图但需排除凌晨2-5点的异常静默期。pandas的agg()配合lambda函数NumPy的np.nanmean()才能实现这种条件聚合Seaborn的barplot()只支持预设统计函数。嵌入式系统部署某工业PLC边缘网关只有32MB内存无法安装Plotly依赖。最终用Matplotlib的Agg后端生成PNG通过HTTP接口推送至HMI屏幕整个流程仅依赖Python标准库这三个核心包。提示别把“简单”等同于“低级”。Matplotlib的Artist对象模型Figure/Axis/Line2D让你能精确控制每个像素的渲染逻辑这是高级库刻意隐藏的复杂性——而复杂性正是解决真实问题的必要代价。2.3 架构分层三层职责不可混淆我把整个可视化流程拆解为清晰的三层每层由对应库主导且严格禁止越界数据层pandas只做结构化操作。读取、筛选、分组、合并、缺失值填充。禁止在此层调用.plot()——那是展示层的事。例如处理销售数据时df_sales.groupby(region)[revenue].sum().reset_index()生成汇总表但绝不在此处画柱状图。计算层NumPy只做数值运算。滚动均值、Z-score标准化、分位数计算、插值拟合。所有结果必须返回numpy数组或标量不产生任何图形对象。比如计算设备振动幅度的RMS值np.sqrt(np.mean(df_vib[acc_x]**2 df_vib[acc_y]**2 df_vib[acc_z]**2))结果直接赋值给变量不绘图。展示层Matplotlib只做视觉映射。接收pandas DataFrame的列或NumPy数组将其映射为坐标、颜色、大小、透明度。禁止在此层进行数据过滤或计算。例如plt.scatter(xdf_clean[temp], ydf_clean[capacity], ccapacity_zscore)其中capacity_zscore必须是NumPy提前算好的数组。这种分层不是教条而是防错机制。去年帮一家物流平台优化运单时效图时开发人员把df[df[delay]0][delay].hist()写在展示层导致每次重绘都重复执行布尔索引——当数据量从10万涨到500万时页面卡顿从0.3秒飙升至8秒。重构为先用pandas过滤出延迟订单存为df_delayed再用plt.hist(df_delayed[delay])性能提升27倍。分层的价值就在这种毫秒级的细节里。3. 核心细节解析从数据加载到图表生成的12个关键决策点3.1 数据加载阶段pandas的read_csv不是“一键导入”而是第一次数据校验很多人用pd.read_csv(data.csv)后直接df.head()却忽略这行代码已埋下隐患。以我处理过的某智能电表日志为例原始CSV包含timestamp, voltage, current, power四列但timestamp列实际是Unix毫秒时间戳如1672531200000而pandas默认按字符串读取。若不做处理后续所有时间分析都会失效。正确做法是# 第一步指定dtype避免类型推断错误 df pd.read_csv(meter_log.csv, dtype{voltage: float32, # 节省内存 current: float32, power: float32}) # 第二步用converters参数即时转换时间戳 df[timestamp] pd.to_datetime( df[timestamp], unitms, errorscoerce ) # errorscoerce将非法时间转为NaT便于后续排查 # 第三步设置时间索引并验证连续性 df df.set_index(timestamp).sort_index() # 检查是否缺失整点数据工业场景常见 expected_freq pd.infer_freq(df.index) if expected_freq ! H: print(f警告预期每小时1条实际频率为{expected_freq})这里的关键决策点在于时间列处理必须在数据加载阶段完成。若拖到绘图前用pd.to_datetime(df[timestamp])会触发隐式拷贝当数据量超百万行时内存占用翻倍。而converters参数在读取时直接转换零额外开销。另外dtype指定float32而非默认float64在100万行数据中可节省4MB内存——这对笔记本电脑跑大文件至关重要。3.2 缺失值处理不是填0或均值而是用业务逻辑重建pandas的fillna()方法常被滥用。某光伏电站发电量数据中irradiance辐照度列有12%缺失值。若简单用df[irradiance].fillna(df[irradiance].mean())会抹平阴天与晴天的本质差异。正确做法是结合NumPy构建物理模型# 利用温度与辐照度的负相关关系气象学常识 # 先用pandas分组获取各温度区间的辐照度中位数 temp_bins np.arange(0, 50, 5) # 0-5,5-10,...45-50℃ df[temp_group] pd.cut(df[temperature], binstemp_bins, labelsFalse) # 用NumPy向量化计算每组中位数比pandas agg快3倍 group_medians np.array([ np.nanmedian(df[df[temp_group]i][irradiance]) for i in range(len(temp_bins)-1) ]) # 对缺失值进行业务逻辑填充 mask_missing df[irradiance].isna() df.loc[mask_missing, irradiance] group_medians[ df.loc[mask_missing, temp_group].astype(int) ]这个操作的核心是缺失值填充必须携带业务语义。用温度分组中位数既保留了气象规律又避免了均值对异常值的敏感。实测显示此方法使后续发电量预测模型的MAE降低19%远超简单填充的效果。3.3 特征工程pandas的agg()与NumPy的ufunc如何协同构建新维度可视化效果取决于你构造的特征维度。某汽车OBD数据中原始列只有rpm,speed,throttle但业务方需要“驾驶激进程度”指标。这不能靠单列需多列融合# 步骤1用pandas创建临时分组键按车辆ID和行程ID df_trip df.groupby([vehicle_id, trip_id]) # 步骤2用NumPy ufunc计算每行程的加速度标准差激进驾驶核心指标 def calc_acc_std(group): # 假设已有加速度列或用speed差分近似 acc np.diff(group[speed].values) / np.diff(group[timestamp].astype(np.int64)) * 1e9 return np.std(acc) if len(acc) 1 else 0 # 步骤3pandas agg接收NumPy函数返回Series df_trip_agg df_trip.agg({ rpm: max, speed: max, throttle: lambda x: np.percentile(x, 90), # 90分位油门开度 trip_duration: lambda x: (x.max() - x.min()).total_seconds() / 3600, acc_std: calc_acc_std # 关键注入NumPy计算逻辑 }).reset_index() # 步骤4用pandas.cut将连续指标离散化为业务标签 df_trip_agg[driving_style] pd.cut( df_trip_agg[acc_std], bins[0, 0.5, 1.5, float(inf)], labels[smooth, moderate, aggressive] )这里体现的是pandas与NumPy的黄金分工pandas提供分组框架和结构化输出NumPy提供高性能数值计算。calc_acc_std函数中np.diff比pandas的diff()快4倍且np.percentile支持插值比quantile()更稳定。最终生成的driving_style列成为后续散点图颜色编码的基础维度。3.4 Matplotlib基础配置为什么plt.rcParams必须在绘图前全局设置很多新手在每个plt.figure()后单独设置字体、网格导致代码冗长且风格不一致。正确姿势是一次配置全程生效import matplotlib.pyplot as plt import numpy as np # 全局配置放在所有绘图代码之前 plt.rcParams.update({ font.size: 12, # 基础字号 font.family: sans-serif, font.sans-serif: [Arial, DejaVu Sans, Liberation Sans], axes.titlesize: 14, axes.labelsize: 13, xtick.labelsize: 11, ytick.labelsize: 11, legend.fontsize: 11, figure.figsize: (10, 6), # 默认画布尺寸 lines.linewidth: 2.0, # 折线粗细 lines.markersize: 6, # 散点大小 grid.alpha: 0.3, # 网格透明度 axes.grid: True, # 默认开启网格 savefig.dpi: 300, # 导出高清图 pdf.fonttype: 42, # PDF兼容性避免字体嵌入问题 ps.fonttype: 42 }) # 后续所有绘图自动继承这些设置 plt.figure() plt.plot([1,2,3], [1,4,2]) plt.title(无需重复设置字体) plt.show()这个配置的关键价值在于消除视觉噪音。grid.alpha0.3让网格若隐若现既辅助读数又不抢数据焦点savefig.dpi300确保导出图片在印刷品中清晰pdf.fonttype42解决LaTeX论文中字体乱码问题。我曾见某团队因未设pdf.fonttype导致技术白皮书PDF中所有中文变成方块返工三天。3.5 子图布局plt.subplots()与plt.subplot_mosaic()的实战选择当需要多图联动时布局方式决定分析深度。某半导体厂分析晶圆良率需同时展示左图各机台良率时序折线5条线右图良率-温度散点图带趋势线底部机台分布直方图若用传统plt.subplots(2,2)需手动调整位置且无法实现“右图跨两行”。此时subplot_mosaic()是唯一解# 定义布局矩阵字符即子图标识 mosaic AB CB fig, axes plt.subplot_mosaic(mosaic, figsize(12, 8)) # A区域机台良率时序 for machine in [M01,M02,M03,M04,M05]: axes[A].plot(df_machine[df_machine[machine]machine][date], df_machine[df_machine[machine]machine][yield], labelmachine) axes[A].set_title(各机台良率趋势) axes[A].legend() # B区域良率-温度散点跨两行 axes[B].scatter(df_all[temperature], df_all[yield], alpha0.6) # 添加NumPy拟合的趋势线 z np.polyfit(df_all[temperature], df_all[yield], 1) p np.poly1d(z) axes[B].plot(df_all[temperature], p(df_all[temperature]), r--, lw2) axes[B].set_title(良率 vs 温度) # C区域机台分布直方图 axes[C].hist(df_all[machine], bins20, alpha0.7) axes[C].set_title(机台分布)subplot_mosaic()的优势在于语义化布局用字符矩阵直观表达空间关系B占据右列两行天然支持跨区域绘图。而subplots()需计算gridspec位置代码量多3倍且易出错。3.6 颜色编码从cmap到BoundaryNorm的渐进式控制颜色不是装饰是第二维度的信息载体。某环境监测项目需用热力图展示PM2.5浓度但国家标准限值是35μg/m³优、75μg/m³良、115μg/m³轻度污染。若用默认plt.imshow(cmapviridis)无法突出政策阈值。解决方案是BoundaryNormfrom matplotlib.colors import BoundaryNorm import numpy as np # 定义国标阈值边界 bounds [0, 35, 75, 115, 150, 250, 500] # 单位μg/m³ norm BoundaryNorm(bounds, plt.cm.RdYlGn, extendmax) # 绘制热力图 im plt.imshow(pm25_grid, cmapRdYlGn, normnorm, aspectauto) plt.colorbar(im, boundariesbounds, ticksbounds[:-1]) # 颜色条刻度对齐阈值 # 添加文本标注用NumPy定位最大值位置 max_idx np.unravel_index(np.argmax(pm25_grid), pm25_grid.shape) plt.text(max_idx[1], max_idx[0], f峰值\n{pm25_grid.max():.0f}, hacenter, vacenter, fontweightbold, colorwhite)这里BoundaryNorm强制颜色在阈值处突变extendmax处理超标值统一为深红。np.unravel_index定位峰值位置比循环遍历快10倍。最终图表能让环保部门一眼识别超标区域无需查表。3.7 动态标注用plt.annotate()实现数据驱动的注释静态图无法应对数据变化。某金融风控系统需在交易额时序图上自动标注异常点。手动添加plt.axvline()不现实需用NumPy计算# 用NumPy计算Z-score识别异常 z_scores np.abs((df[amount] - df[amount].mean()) / df[amount].std()) anomaly_mask z_scores 3 # 动态生成标注 for idx in df[anomaly_mask].index: # 获取该点前后3个点的y值计算标注位置避免重叠 y_vals df.loc[idx-3:idx3, amount].dropna() if len(y_vals) 0: # 标注位置略高于数据点用NumPy取上四分位数避免贴顶 y_pos np.percentile(y_vals, 75) * 1.05 plt.annotate(f异常\n¥{df.loc[idx, amount]:.0f}k, xy(idx, df.loc[idx, amount]), xytext(idx, y_pos), arrowpropsdict(arrowstyle-, colorred, lw1.2), fontsize10, hacenter, bboxdict(boxstyleround,pad0.3, fcyellow, alpha0.7))关键点在于标注位置由数据分布决定用np.percentile(y_vals, 75)取上四分位数确保标注在数据簇上方而非固定偏移。这样即使数据量变化标注仍保持可读性。3.8 图例与坐标轴plt.legend()的handler_map高级定制当图例项过多时默认图例拥挤难读。某物流路径优化项目需在地图上叠加12种运输方式卡车、高铁、海运等图例需按类别分组。此时需自定义handler_mapfrom matplotlib.patches import Patch from matplotlib.lines import Line2D # 创建自定义图例元素 legend_elements [ # 第一组陆运 Line2D([0], [0], markero, colorw, label陆运, markerfacecolortab:blue, markersize12), Line2D([0], [0], markers, colorw, label卡车, markerfacecolortab:blue, markersize10), Line2D([0], [0], marker^, colorw, label高铁, markerfacecolortab:blue, markersize10), # 第二组水运 Line2D([0], [0], markero, colorw, label水运, markerfacecolortab:green, markersize12), Line2D([0], [0], markerD, colorw, label海运, markerfacecolortab:green, markersize10), ] # 绘制主图省略具体代码 plt.scatter(x_coords, y_coords, ctransport_colors, ssizes) # 使用自定义图例 plt.legend(handleslegend_elements, locupper left, bbox_to_anchor(1.05, 1), title运输方式分类, title_fontsize12, fontsize10, frameonTrue, fancyboxTrue, shadowTrue)handler_map允许你用Patch、Line2D等Artist对象完全控制图例外观比plt.legend([A,B,C])灵活百倍。bbox_to_anchor(1.05, 1)将图例置于图外右侧避免遮挡地图。3.9 导出与复用plt.savefig()的bbox_inches与pad_inches精调导出图片常被忽视却是交付关键。某客户要求所有图表嵌入Word文档但默认plt.savefig(fig.png)会裁掉坐标轴标签。解决方案# 精确控制边距 plt.savefig(yield_analysis.png, bbox_inchestight, # 自动收紧边距 pad_inches0.1, # 保留0.1英寸空白约2.54mm dpi300, facecolorwhite, edgecolornone) # 对于需嵌入LaTeX的PDF额外处理字体 plt.savefig(yield_analysis.pdf, bbox_inchestight, pad_inches0.1, formatpdf, bbox_extra_artists(plt.gcf().text(0.5, 1.02, 标题, transformplt.gcf().transFigure),))bbox_inchestight自动检测文字边界pad_inches0.1防止标签被裁切。bbox_extra_artists确保标题也被纳入PDF边界计算——这是LaTeX编译不报错的关键。3.10 性能优化plt.plot()的markevery与rasterized参数当数据量超10万点时绘图会卡死。某IoT设备传感器数据含200万采样点直接plt.plot(x,y)内存溢出。解决方案# 方案1稀疏标记仅显示1%的点 plt.plot(x, y, o-, markeveryint(len(x)/100), markersize2) # 方案2栅格化矢量图中嵌入位图 plt.plot(x, y, rasterizedTrue) # 仅对折线启用栅格化 # 方案3分段绘制用NumPy切片 chunk_size 50000 for i in range(0, len(x), chunk_size): end min(i chunk_size, len(x)) plt.plot(x[i:end], y[i:end], -, linewidth0.8)markevery参数让plt.plot()只渲染指定间隔的标记点rasterizedTrue将折线转为位图大幅降低PDF文件体积。实测200万点数据rasterized使PDF从120MB降至8MB。3.11 中文支持matplotlib.font_manager的字体缓存管理中文乱码是高频痛点。某政府项目需用宋体显示政策文件图表但plt.rcParams[font.sans-serif] [SimSun]在Linux服务器上无效。根本解法是import matplotlib.font_manager as fm # 扫描系统字体首次运行耗时结果缓存 font_files fm.findSystemFonts(fontpathsNone, fontextttf) for font_file in font_files: try: fm.fontManager.addfont(font_file) except Exception as e: pass # 忽略损坏字体 # 强制刷新字体缓存 fm._rebuild() # 设置中文字体优先级SimSun DejaVu Sans plt.rcParams[font.sans-serif] [SimSun, DejaVu Sans, Arial] plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块fm._rebuild()是关键它强制Matplotlib重新索引字体否则新添加的字体不会生效。axes.unicode_minusFalse让减号显示为ASCII短横避免宋体中全角减号错位。3.12 交互增强plt.ginput()实现人工校验闭环自动化不能替代人工判断。某医疗设备报警分析中算法标记了50个疑似故障点但需医生确认。用plt.ginput()实现人机协同# 绘制原始信号 plt.plot(time, signal, b-, label原始信号) plt.plot(anomaly_times, anomaly_values, ro, label算法标记) # 启动交互式校验 plt.title(请用鼠标左键点击确认故障点右键结束) plt.legend() plt.show() # 获取用户点击坐标最多50次 confirmed_points plt.ginput(n50, timeout0, show_clicksTrue, mouse_add1, mouse_pop3) if confirmed_points: # 将坐标转换为最近的数据点索引 confirmed_indices [ np.argmin(np.abs(time - x)) for x, y in confirmed_points ] # 保存确认结果 with open(confirmed_anomalies.json, w) as f: json.dump({indices: confirmed_indices}, f)plt.ginput()让用户在图上直接点击返回像素坐标再用np.argmin映射回数据索引。这比导出Excel勾选高效10倍且保证坐标精度。4. 实操全流程从原始CSV到可交付图表的完整代码链4.1 项目背景与数据概览我们以某共享单车运营公司的调度优化项目为案例。原始数据为bike_trips_2023.csv包含120万条记录字段如下字段名类型描述trip_idint64行程唯一IDstart_timeobject开始时间字符串end_timeobject结束时间字符串start_stationobject起点站点名end_stationobject终点站点名duration_secint64行程时长秒distance_kmfloat64行驶距离公里业务目标识别“潮汐现象”严重的站点早高峰大量车流出晚高峰大量车流入为调度车提供依据。4.2 数据清洗与特征构建pandas NumPyimport pandas as pd import numpy as np from datetime import datetime, timedelta # 1. 加载数据指定dtype节省内存 df pd.read_csv(bike_trips_2023.csv, dtype{trip_id: int32, duration_sec: int32, distance_km: float32}, parse_dates[start_time, end_time]) # 2. 时间特征工程pandas时间序列方法 df[start_hour] df[start_time].dt.hour df[start_weekday] df[start_time].dt.weekday # 0周一 df[is_weekend] (df[start_weekday] 5).astype(int) df[start_date] df[start_time].dt.date # 3. 计算骑行效率NumPy向量化 # 避免pandas的apply用np.where处理除零 df[speed_kph] np.where( df[duration_sec] 0, (df[distance_km] / df[duration_sec]) * 3600, np.nan ) # 4. 筛选有效行程pandas布尔索引 df_clean df[ (df[duration_sec] 60) # 至少1分钟 (df[duration_sec] 7200) # 不超过2小时 (df[distance_km] 0.1) # 至少100米 (df[speed_kph] 5) # 合理速度下限 (df[speed_kph] 40) # 合理速度上限 ].copy() print(f原始数据: {len(df)} 条 → 清洗后: {len(df_clean)} 条 ({len(df_clean)/len(df)*100:.1f}%))这段代码的关键在于用NumPy处理条件分支np.where比pandas的apply(lambda x: ...)快8倍且内存友好。df_clean.copy()明确创建副本避免SettingWithCopyWarning。4.3 站点级统计聚合pandas agg NumPy函数# 按站点和小时聚合pandas groupby station_hour_stats df_clean.groupby([start_station, start_hour]).agg({ trip_id: count, # 出车量 end_station: lambda x: x.nunique(), # 流向多样性 duration_sec: [mean, std], distance_km: mean, speed_kph: mean }).round(2).reset_index() # 展平列名pandas多级列处理 station_hour_stats.columns [_.join(col).strip() if col[1] else col[0] for col in station_hour_stats.columns.values] # 计算每站每小时的净流量NumPy向量化 # 先计算各站作为起点的出行量 outflow df_clean.groupby([start_station, start_hour]).size().unstack(fill_value0) # 再计算各站作为终点的流入量 inflow df_clean.groupby([end_station, start_hour]).size().unstack(fill_value0) # 合并并计算净流量NumPy矩阵运算 net_flow (outflow - inflow.fillna(0)).fillna(0).astype(int) # 将净流量加入统计表 station_hour_stats[net_flow] station_hour_stats.apply( lambda row: net_flow.get(row[start_station], pd.Series()).get(row[start_hour], 0), axis1 )这里unstack()将分组结果转为宽表net_flow是DataFrame矩阵get()方法安全提取值。相比循环遍历矩阵运算快50倍。4.4 潮汐站点识别NumPy逻辑运算# 定义潮汐现象早高峰7-9点净流出 50晚高峰17-19点净流入 50 morning_out net_flow.loc[:, 7:9].sum(axis1) 50 evening_in (-net_flow.loc[:, 17:19].sum(axis1)) 50 # 用NumPy布尔索引找出双高峰站点 tidal_stations net_flow.index[morning_out evening_in].tolist() print(f识别潮汐站点: {len(tidal_stations)} 个) print(f示例: {tidal_stations[:5]}) # 计算各潮汐站点的潮汐强度早晚高峰净流量绝对值和 tidal_strength {} for station in tidal_stations: m_out net_flow.loc[station, 7:9].sum() e_in -net_flow.loc[station, 17:19].sum() tidal_strength[station] abs(m_out) abs(e_in) # 转为pandas Series排序 tidal_series pd.Series(tidal_strength).sort_values(ascendingFalse) top_10_tidal tidal_series.head(10)morning_out evening_in是NumPy布尔数组运算比pandas的query()快3倍。abs(m_out) abs(e_in)量化潮汐强度为后续排序提供依据。4.5 可视化实现Matplotlib核心绘图import matplotlib.pyplot as plt import matplotlib.dates as mdates # 设置全局样式复用3.4节配置 plt.rcParams.update({ font.size: 12, font.family: sans-serif, font.sans-serif: [Arial, DejaVu Sans], figure.figsize: (14, 10), lines.linewidth: 2.0, grid.alpha