
经常有朋友问论文里“空间变量 环境变量”两个集合画在一起的那张方块韦恩图到底怎么用 Python 画如果你的研究对象是物种、土壤、水质或城市指标同时想知道“空间位置带来的影响”和“环境因子带来的影响”各自解释多少那这篇文章基本可以解决这个问题。这里说的不是再学一个新模型而是把“方差分解 韦恩图 多面板期刊配图”完整串起来。传统做法通常依赖 R 的 vegan 包而这次完全用 Python 完成读取数据把特征分成空间组和环境组分别计算解释率再把多个响应变量的结果放在同一个大图里输出 300 dpi 的发表级图片。核心内容包括四点线性回归框架下的方差分解随机森林作为参考的机器学习分组贡献多响应变量的批量处理以及多面板韦恩图的版式控制。整个过程在普通 CPU 电脑上就能跑推荐 Python 3.9 以上环境正文会给出可复制代码。示例分析数据我会用随机模拟数据生成方便快速跑通最终换成你自己的论文数据即可。1. 核心能力速览这个方案本质是“统计分解方法 Python 科研绘图模板”。下表给出基本能力边界能力项说明项目类型群落生态/环境地理/城市数据中的方差分解与期刊绘图方案主要功能将变量分为“空间变量组”和“环境变量组”计算各自解释率和重叠解释率输出成果单面板/多面板韦恩图、解释率汇总 CSV、模型质量指标运行环境Python 脚本或 Jupyter Notebook普通 CPU 可运行核心依赖numpy、pandas、scikit-learn、matplotlib、matplotlib-venn模型选择线性回归用于传统方差分解随机森林用于机器学习解释率对照批量任务支持循环处理多个响应变量并统一出图API 接口不提供 Web API可通过 Python 函数集成到自己的分析脚本适合场景期刊论文、毕业论文、科研组会汇报中的空间/环境变量贡献展示主要局限经典的 Venn 图更适合线性模型二分分解非线性结果建议以指标表组重要性图展示需要注意这个流程并不会自动替你判断“空间变量组是否应该包含哪些特征”。空间变量可以是采样点的经纬度、坐标的多项式项、距离矩阵的主坐标也可以是环境学中常用的空间邻域特征环境变量可以是气候、土壤、土地利用等。实际选哪些特征要依据你的研究问题而定。2. 适用场景与使用边界这个方案最适合作答的问题是某个响应变量的方差有多少能被环境联合解释多少能被空间联合解释两者还有多大的重叠部分。多见于这些场景生态学中对物种多样性、生物量、群落组成进行空间和环境解释地学与城市研究中分析地表温度、空气质量、人口密度的空间分异来源土壤科学中判断土壤属性受成土母质控制还是受地形气候控制水环境中分析水质参数受流域因素还是采样空间结构影响。不适合用这个流程的场景也很多。如果所有自变量都是同一类型不存在“空间组 vs 环境组”的天然分组那就不要强行做两组韦恩图。如果样本量小于 20尽管代码能跑得到的解释率会非常不稳定不建议用于发表。如果特征之间严重重复解释率分解中的“重叠部分”可能为负数会让 Venn 图失去直观含义。合规和授权提醒同样要写清楚论文数据、采样点精确坐标、研究区地图底图、受版权保护的图件素材使用时都要先确认许可。空间采样点如果涉及自然保护区、重要设施或敏感地域公开发表时建议进行位置脱敏或按期刊要求申请数据共享豁免。用别人的物种分布数据、环境栅格数据做分析也必须在论文中规范引用来源。3. 方差分解原理与关键公式先明确“两组变量解释率”的经典线性分解。设一个响应变量为 y空间变量组为 S环境变量组为 E三个线性模型分别为模型1y ~ S E 得到 R²_all 模型2y ~ S 得到 R²_space 模型3y ~ E 得到 R²_env最小二乘线性模型满足加法分解关系R²_all pure_space pure_env overlap其中overlap R²_space R²_env - R²_all pure_space R²_space - overlap pure_env R²_env - overlap residual 1 - R²_all实际写代码时通常先用 sklearn 的LinearRegression训练模型再用r2_score得到 R²。后面代码里会给出一个可以直接套用的linear_variance_partition函数。这里有一个容易踩坑的点overlap在变量组负相关或模型出现抑制效应时可能为负。出现负数通常意味着当前特征分组的线性关系不够稳定常见处理方式是先检查变量相关性和异常样本再考虑删除高相关变量或改用机器学习归因。不要在发现负值时把负数强行塞进韦恩图这会误导读者。机器学习部分的逻辑不同。若使用随机森林或梯度提升树并没有天然等于 R² 分解的“纯 overlap”概念。更稳妥的做法是交叉验证全模型的 R²再比较去掉某一组特征后的模型 R² 下降程度这个下降量可以理解为该变量组的边际贡献。该值通常用于横向比较两组变量的相对作用不能完全等同于传统 Venn 图中的面积重叠。4. 环境准备与依赖安装4.1 Python 环境建议使用 Python 3.9 及以上版本。先为项目创建独立虚拟环境避免把系统环境的包弄乱。python -m venv venv_venn source venv_venn/bin/activateWindows 下激活命令是venv_venn\Scripts\activate4.2 安装依赖包需要安装的第三方包为numpy、pandas、scikit-learn、matplotlib、matplotlib-venn。用 pip 一次性安装pip install numpy pandas scikit-learn matplotlib matplotlib-venn如果之后需要做 SHAP 类机器学习解释再自行安装pip install shap在没有装过 Python 的电脑上建议先确认pip指向的 Python 版本是你要用的环境python --version pip --version4.3 字体与图形后端如果你打算在图上显示中文需要在 matplotlib 中指定包含中文的字体常见选择是 Windows 的Microsoft YaHei或 macOS 的PingFang SC。不过期刊投稿通常要求英文无衬线字体所以下面的模板默认使用英文标签避免不同操作系统字体差异导致乱码。5. 模拟数据与核心分析代码下面代码分四部分生成模拟数据、线性方差分解函数、随机森林分组贡献对照函数、多响应批量汇总。先把模拟数据生成和函数定义写出来。import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import KFold, cross_val_score def make_demo_data(n300, seed42): 随机模拟一份带空间坐标和环境变量的数据集仅供流程演示。 rng np.random.default_rng(seed) # 模拟采样点坐标 x rng.uniform(0, 100, n) y rng.uniform(0, 80, n) # 模拟环境变量 bio1 rng.normal(10, 3, n) bio2 rng.normal(200, 40, n) soil rng.normal(5, 1.5, n) # 让目标变量与环境变量、空间位置都保持一定关系 y1 ( 0.6 * bio1 - 0.3 * bio2 0.2 * soil 0.4 * x - 0.2 * y rng.normal(0, 1.0, n) ) y2 ( 0.4 * bio2 0.5 * soil - 0.1 * x 0.3 * y rng.normal(0, 1.2, n) ) df pd.DataFrame( { x: x, y: y, bio1: bio1, bio2: bio2, soil: soil, y1: y1, y2: y2, } ) return df def fit_linear_r2(X, y): 训练线性模型并返回 R2。 X np.asarray(X, dtypefloat) if X.ndim 1: X X.reshape(-1, 1) model LinearRegression() model.fit(X, y) pred model.predict(X) return r2_score(y, pred) def linear_variance_partition(X_space, X_env, y): 经典两组变量线性方差分解。 返回值说明 r2_all : 空间环境联合模型解释率 r2_space : 仅空间变量解释率 r2_env : 仅环境变量解释率 pure_space : 空间组独立解释率 pure_env : 环境组独立解释率 overlap : 两组共同解释率 residual : 未解释率 X_all np.hstack([np.asarray(X_space), np.asarray(X_env)]) y np.asarray(y) r2_all fit_linear_r2(X_all, y) r2_space fit_linear_r2(X_space, y) r2_env fit_linear_r2(X_env, y) overlap r2_space r2_env - r2_all pure_space r2_space - overlap pure_env r2_env - overlap residual 1.0 - r2_all if overlap 0: print( 警告overlap 为负当前变量分组可能存在共线性或抑制效应 建议检查变量相关性后再决定是否使用 Venn 图。 ) return { r2_all: r2_all, r2_space: r2_space, r2_env: r2_env, pure_space: pure_space, pure_env: pure_env, overlap: overlap, residual: residual, }接下来是随机森林交叉验证版本。这个版本不是直接替代线性 Venn 图而是从机器学习预测角度提供一组“组边际贡献”。def cv_rf_r2(X, y, n_splits5, n_estimators200, random_state42): 随机森林交叉验证 R2。 X np.asarray(X, dtypefloat) if X.ndim 1: X X.reshape(-1, 1) y np.asarray(y) model RandomForestRegressor( n_estimatorsn_estimators, random_staterandom_state, n_jobs-1, ) cv KFold(n_splitsn_splits, shuffleTrue, random_staterandom_state) scores cross_val_score(model, X, y, cvcv, scoringr2) return float(np.mean(scores)) def rf_group_partition(X_space, X_env, y, n_splits5): 随机森林分组贡献。 loss_space: 去掉空间组后模型 R2 下降量 loss_env : 去掉环境组后模型 R2 下降量 注意该结果不等同于传统 Venn 图区域面积 更适合作为机器学习解释率的相对比较。 X_all np.hstack([np.asarray(X_space), np.asarray(X_env)]) y np.asarray(y) r2_all cv_rf_r2(X_all, y, n_splitsn_splits) r2_no_space cv_rf_r2(X_env, y, n_splitsn_splits) r2_no_env cv_rf_r2(X_space, y, n_splitsn_splits) loss_space r2_all - r2_no_space loss_env r2_all - r2_no_env return { rf_r2_all: r2_all, rf_loss_space: loss_space, rf_loss_env: loss_env, }这份模拟数据里x、y作为空间变量组bio1、bio2、soil作为环境变量组。如果你的数据是真实的景观样本、样方或城市站点数据只需要把你的表读成 DataFrame再替换特征列和响应列即可。df make_demo_data(n300, seed42) space_cols [x, y] env_cols [bio1, bio2, soil] resp_cols [y1, y2] X_space df[space_cols].values X_env df[env_cols].values for resp in resp_cols: y df[resp].values res linear_variance_partition(X_space, X_env, y) rf_res rf_group_partition(X_space, X_env, y) print(resp, res) print(resp, rf_res)代码会输出每组的纯空间解释率、纯环境解释率、重叠解释率和残差。跑通之后再把它替换成你的实际数据分析多列响应变量时进入循环即可。6. 多面板方差分解韦恩图绘制绘制韦恩图使用matplotlib_venn。venn2的subsets参数接收三个值键含义10左边集合单独占有的区域01右边集合单独占有的区域11两个集合共同覆盖的区域在多面板布局中每个子图对应一个响应变量子图标题写响应变量名。由于残差部分无法画进两个圆内我通常在每个面板右上角或底部统一标注“总解释率”和“残差”。这样审稿人能很快看到模型整体解释力度。import matplotlib.pyplot as plt from matplotlib_venn import venn2, venn2_circles def build_venn_areas(partition): 把方差分解结果转成 venn2 需要的 subsets 字典。 如果 overlap 小于 0将 overlap 强行置 0 并在图中给出提示 避免绘制出无意义的反向集合区。 overlap max(partition[overlap], 0.0) areas { 10: max(partition[pure_space], 0.0), 01: max(partition[pure_env], 0.0), 11: overlap, } return areas def plot_venn_panels( df, space_cols, env_cols, resp_cols, ncols2, figsize(10, 4.5), dpi300, ): n len(resp_cols) nrows int(np.ceil(n / ncols)) fig, axes plt.subplots( nrows, ncols, figsizefigsize, squeezeFalse ) X_space df[space_cols].values X_env df[env_cols].values palette [#4C72B0, #DD8452] for idx, resp in enumerate(resp_cols): ax axes[idx // ncols][idx % ncols] y df[resp].values partition linear_variance_partition(X_space, X_env, y) areas build_venn_areas(partition) venn venn2( subsetsareas, set_labels(Space, Environment), set_colorspalette, alpha0.45, axax, ) venn2_circles( subsetsareas, linestyle--, linewidth0.8, axax, ) ax.set_title(resp, fontsize11) # 标出残差和总解释率 ax.text( 0.01, 0.01, fR2_all {partition[r2_all]:.2f}\n fResidual {partition[residual]:.2f}, transformax.transAxes, fontsize7, vabottom, haleft, bboxdict(boxstyleround,pad0.3, fcwhite, ecgray, lw0.5), ) # 如果 overlap 为负在面板上给出明显提示 if partition[overlap] 0: ax.text( 0.5, 0.97, negative overlap, transformax.transAxes, fontsize7, colorred, hacenter, vatop, ) # 隐藏无内容子图 for j in range(n, nrows * ncols): axes[j // ncols][j % ncols].axis(off) fig.suptitle(Variance Partitioning: Space vs Environment, fontsize13) fig.tight_layout(rect[0, 0, 1, 0.96]) fig.savefig(venn_multi_panel.png, dpidpi, bbox_inchestight) plt.show()把这份函数应用到模拟数据上plot_venn_panels( dfdf, space_colsspace_cols, env_colsenv_cols, resp_colsresp_cols, ncols2, )你会得到一张横向两个面板的图左边是y1的方差分解右边是y2的方差分解。如果以后要画 4 个响应或 6 个响应修改resp_cols再调整ncols3或ncols4函数会按照行列顺序自动排版。这里的重点是不要把注意力全部放在图好不好看。先看输出结果里overlap是否出现负数再看residual是否过高。如果残差超过 0.7说明当前的空间变量与环境变量很难解释该响应变量这时的 Venn 图无论画得多好看统计含义都比较弱。7. 汇总表格与批量分析在实际论文或研究报告中除了图通常还需要一张解释率汇总表。下面代码把各个响应变量的结果保存到 DataFrame并输出 CSV 文件。summary [] for resp in resp_cols: y df[resp].values lin_res linear_variance_partition(X_space, X_env, y) rf_res rf_group_partition(X_space, X_env, y) summary.append( { response: resp, r2_all: lin_res[r2_all], pure_space: lin_res[pure_space], pure_env: lin_res[pure_env], overlap: lin_res[overlap], residual: lin_res[residual], rf_loss_space: rf_res[rf_loss_space], rf_loss_env: rf_res[rf_loss_env], } ) summary_df pd.DataFrame(summary).round(3) print(summary_df) summary_df.to_csv(variance_partition_summary.csv, indexFalse)批量任务的逻辑很简单把所有响应变量放在resp_cols对每一个响应分别调用函数。前面代码里已经实现了循环理论上无论多少个响应变量都能跑但绘图版面会比较拥挤。一般期刊图推荐单张大图不超过 6 个面板面板较多时可以拆成 Figure 和 Supplementary Figure 分别输出。如果你的数据量较大比如几万行样方、几百个环境特征建议先做一轮特征筛选再进入方差分解。特征过多会让线性模型 R² 虚高也会让随机森林训练非常慢。做特征筛选时不能只看 p 值或相关性要结合生态或地理过程判断变量是否真的有因果路径。8. 资源占用与性能观察这个方案不需要 GPU。模拟数据量较小时整个脚本在普通办公笔记本上几秒内完成。当响应变量多、样本量达到几万、随机森林n_estimators500时CPU 会持续占用较高建议按以下方式观察和降低开销。第一个观察点是随机森林的n_estimators。先从100开始看 R² 是否稳定如果继续增大到300时结果没有明显变化就不需要更大了。第二个观察点是交叉验证折数。默认 5 折比较常见样本量较小时可以使用 3 折加快速度。第三个观察点是特征数量。空间变量组如果用坐标及其二次项最多保留 3 到 5 列环境变量组如果包含几十个高度相关的气候变量先做主成分或相关性筛选再进入分组分解。内存方面sklearn 的随机森林是并行训练n_jobs-1会调动全部 CPU 核心。如果你在跑其他大型任务建议把n_jobs改成2或4避免笔记本风扇狂转。输出图片时dpi300生成的 PNG 通常 1 到 3 MB完全满足期刊初审需求如果需要矢量图可以把保存格式改成 PDF 或 SVG。# 矢量导出示例适合期刊放大查看 fig.savefig(venn_multi_panel.pdf, formatpdf, bbox_inchestight)9. 常见问题与排查方法由于这项分析涉及数据、模型、绘图三层内容最容易出现的问题往往是叠加式的。下面表格整理了高频问题问题现象可能原因排查方式解决方案import matplotlib_venn 报错未安装包或安装到了其他 Python 环境pip show matplotlib-venn更新当前虚拟环境并重新安装图中出现重叠部分负数两组变量相关性或模型抑制效应检查 overlap 值调整变量组合或改用机器学习分组对比模型 R² 非常高但残差很低特征过多或存在严重过拟合观察样本量和特征数比例减少变量、加交叉验证、查看验证集表现随机森林每次结果不同未固定随机种子或数据量过大检查random_state所有关键函数统一固定随机种子图打开后中文显示为方块matplotlib 默认字体不含中文字形检查是否指定中文字体使用英文标签或配置系统字体面板间距拥挤响应变量多但 figsize 太小查看子图是否互相重叠增加 figsize减少 ncols 或拆图响应变量列中有缺失值模型无法处理 NaN查看 df.info() 和缺失值统计删除缺失样本或使用合理插补线性 R² 对比随机森林差异很大数据中的非线性关系未被线性模型捕捉对比散点图和模型结果在线性方差分解外补充机器学习结果overlap为负是最容易让人困惑的处理项。遇到这种问题时不要强行画图更不要手动把负值改成 0 后假装没有发生。正确思路是回到数据相关性矩阵删除高方差膨胀因子变量或把某一组特征压缩成一个到两个主成分后再进行分析。若删除后重叠仍为负可能是样本量过小导致的不稳定结果建议补充采样或改用偏最小二乘等更稳健的预测模型。运行代码时如果出现“Thesubset11value is negative”之类的报错原因就是 overlap 为负数传给了venn2。可以先打印线性分解结果确认pure_space、pure_env和overlap是否都大于零。只有这三个值都为正Venn 图才具备几何意义。由于论文中的第二版图并不适合把残差画成第三个圆通常的折中方案是在图中空白处列出残差再用文字说明“总解释率 纯空间 纯环境 重叠”。10. 最佳实践与使用建议第一永远保持一套最小可运行脚本。建议把数据读取、变量列名、方法参数集中在脚本的头部避免每个响应变量都复制一份代码否则改函数时会遗漏某些遗漏位置的代码。第二先画一张图再批量生成多图。不要直接对几十个响应变量批量跑因为某一个响应因变量可能因缺失值或异常值导致整体报错。先在模拟数据上跑通再用真实数据验证单响应结果最后才循环所有响应变量。第三不要盲目追求 R² 高。空间变量一旦在模型中加入坐标的多项式扩展很容易把局部空间结构化差异拟合得很好但这不代表每个环境变量都有明确的因果解释。期刊审稿人经常会看模型的变量选择依据和样本独立性设计而不是只看韦恩图面积。第四注意数据与版权合规。如果你使用公开的物种记录、自然保护区监测数据、公司内部土壤样本或精细地理坐标必须在论文中标注来源并取得授权。涉及网络爬取的数据、第三方版权底图、未开放的高分辨率遥感产品不要直接放进公开论文或代码仓库。第五不同包和方法的选择要在论文方法部分写清楚。传统生态学期刊更认可 vegan 风格中的方差分解和置换检验机器学习领域的读者更接受随机森林特征重要性。本文提供的线性方法和随机森林方法可以互相验证但不要让两类数字混在一张图中未加说明。如果需要在论文中做显著性检验建议在核心分析前增加n_perm999的置换检验每次随机打乱响应变量顺序重新计算某个解释率指标得到经验 p 值。线性方差分解的 R 包已经内置了这类检验Python 版本可以自行写一个简单的置换循环def permutation_test_r2(X, y, n_perm999, random_state42): 置换检验示例检验空间环境联合模型 R2 是否显著高于随机。 真实论文中建议同时对 pure_space 等端点分别做置换检验。 rng np.random.RandomState(random_state) r2_obs fit_linear_r2(X, y) count 0 for _ in range(n_perm): y_perm rng.permutation(y) r2_perm fit_linear_r2(X, y_perm) if r2_perm r2_obs: count 1 p_value (count 1) / (n_perm 1) return r2_obs, p_value11. 总结与后续可扩展方向这个 Python 工作流最适合解决一类高频需求把空间变量组与环境变量组对某个响应变量的贡献比例算出来并输出多面板韦恩图。第一次试验时建议先下载或构造模拟数据跑通linear_variance_partition和plot_venn_panels这两个核心函数再把自己的实际数据替换进去。最容易踩的坑是overlap为负数、matplotlib 中文字体缺失以及响应变量过多导致面板挤压这三类问题在前面的排查表里已经给出处理方法。如果你想继续扩展可以考虑几个方向把空间变量从原始坐标换成距离矩阵的 PCNM 或 MEM 特征在机器学习部分用 SHAP 做特征组归因或把多响应多组变量的分析封装成一个类批量读取目录下的多张 CSV一键生成“图 汇总表 方法文字描述”。这个方案本身不限定学科只要你的数据有明确的“响应变量 两组自变量”结构就能直接改造成自己的期刊配图流程。建议收藏备用下次需要画多面板韦恩图时直接复制代码改列名即可。