1. 为什么桑基图是土地利用变化分析的“神器”如果你做过土地利用变化分析或者看过相关的论文大概率见过那种密密麻麻的表格——转移矩阵。它长这样行是起始年份的土地类型列是终止年份的土地类型中间的数字就是从A类型变成了B类型的面积。数据是精确的但说实话看久了眼睛累想从里面快速看出“哪些地类转出最多”、“哪种转化路径最主流”真的挺费劲。这时候桑基图Sankey Diagram就该登场了。我第一次在能源领域看到这种图就被惊艳到了能源从源头到最终消耗流量大小一目了然。后来一想这不正适合我们搞土地变化的吗土地类型之间的“流量”就是面积转移啊简单来说桑基图用**“流”** 来可视化数据。在土地利用场景里节点Nodes就是你的土地类型比如“森林”、“耕地”、“建设用地”。每个年份的土地类型都会作为一个独立的节点。连线Links就是土地类型之间的转化。连线的粗细直观代表了转移面积的大小。一条从“2000年森林”流向“2005年耕地”的粗线比一条细线更能告诉你这期间有大片森林被开垦了。方向清晰展示了变化的时间序列从早期流向晚期。所以相比静态的表格桑基图能让你一眼抓住重点谁是主要的“贡献者”转出大户谁是主要的“接收者”转入大户以及变化的主要路径是什么。这对于向非专业人士比如项目汇报时的领导或公众展示研究成果或者自己快速把握数据宏观规律都极其有用。下面我们就手把手教你用Python从原始栅格数据开始一步步做出属于自己的动态土地利用桑基图。2. 从栅格到表格数据准备与预处理全流程原始文章里用了一个很巧妙的栅格计算器方法把多期数据编码成一个值。我们来详细拆解一下这个过程并说说我踩过的坑和优化建议。2.1 理解数据编码的“魔法公式”假设我们有五期数据2000.tif, 2005.tif, 2010.tif, 2015.tif, 2019.tif。每期数据的像元值DN代表土地类型编码比如1森林2草地...9裸地。那个10000*2000.tif 1000*2005.tif 100*2010.tif 10*2015.tif 2019.tif的公式本质上是在做位权组合。它确保每个年份的编码占据结果数字的一个固定数位从而将一个像元在五个年份的类型变化历史压缩成一个唯一的整数。举个例子某个像元五期的类型编码依次是 [1, 2, 3, 2, 4]。 那么计算过程就是10000*1 1000*2 100*3 10*2 4 12324。 这个“12324”就唯一对应了“1→2→3→2→4”这条变化轨迹。导出属性表后你会得到很多这样的“Value”和对应的像元数量“Count”。注意这个方法要求你的土地类型编码必须是个位数0-9因为每个数位只能放一个数字。如果你的编码是10以上的两位数这个公式就不适用了需要调整权重比如乘以100、10000等。2.2 使用Python拆解编码并生成标签拿到包含“Value”和“Count”两列的CSV文件后我们就要在Python里进行反向拆解并给每个年份的土地类型生成好读的标签。import pandas as pd import numpy as np # 读取数据 df pd.read_csv(2000_2005_2010_2015_2019.csv) print(df.head()) # 查看一下数据长什么样 # 1. 拆解Value值还原各期土地类型编码 df[a1] df[Value] // 10000 # 2000年编码 df[a2] (df[Value] % 10000) // 1000 # 2005年编码 df[a3] (df[Value] % 1000) // 100 # 2010年编码 df[a4] (df[Value] % 100) // 10 # 2015年编码 df[a5] df[Value] % 10 # 2019年编码 # 2. 定义土地类型名称映射字典 # 这里假设你的编码1-9对应以下9种类型请根据你的实际情况修改 land_class_names { 1: Forest, 2: Grassland, 3: Shrubland, 4: Cultivated Land, 5: Artifical Surface, 6: Water Body, 7: Wetland, 8: Snow and Ice, 9: Bare Land } # 3. 根据编码和年份生成带年份的标签列 # 这个函数会帮我们批量处理 def create_label_column(code_series, year): 根据编码序列和年份生成标签列 labels [] for code in code_series: # 从映射字典里取名字后面加上年份 labels.append(f{land_class_names[code]}({year})) return labels df[name1] create_label_column(df[a1], 2000) df[name2] create_label_column(df[a2], 2005) df[name3] create_label_column(df[a3], 2010) df[name4] create_label_column(df[a4], 2015) df[name5] create_label_column(df[a5], 2019) # 4. 删除中间过程列只保留我们需要的 # 我们需要的是每个年份的标签name1-name5和对应的像元数量Count df_clean df[[name1, name2, name3, name4, name5, Count]].copy() df_clean.to_csv(processed_land_use_flow.csv, indexFalse) print(数据处理完成已保存为 processed_land_use_flow.csv) print(df_clean.head())经过这一步你的数据就从神秘的“12324”变成了清晰的“Forest(2000)”、“Grassland(2005)”这样的行。每一行代表一条独特的、贯穿五年的土地转化路径及其发生的面积通过像元数量×像元面积可以换算。3. 构建桑基图核心Nodes和Links数据桑基图绘图库比如我们后面要用的pyecharts通常需要两个核心数据结构nodes所有节点的列表和links所有连线的列表。这一步就是从我们整理好的表格里生成它们。3.1 提取唯一的节点Nodes节点就是所有出现的土地类型标签的集合。注意“Forest(2000)”和“Forest(2005)”在桑基图里是两个不同的节点因为它们属于不同时间点。# 接上一段代码或读取处理好的数据 df pd.read_csv(processed_land_use_flow.csv) # 初始化节点列表 nodes [] # 用一个集合来临时存储节点名避免重复虽然按列取唯一值本身不会重复但这是好习惯 node_names_set set() # 遍历每一列每一期数据的标签 for col in [name1, name2, name3, name4, name5]: unique_values df[col].unique() # 获取该列所有不重复的标签 for value in unique_values: if value not in node_names_set: node_names_set.add(value) # 桑基图节点通常要求是一个字典至少包含name键 nodes.append({name: value}) print(f共提取了 {len(nodes)} 个节点。) print(前10个节点示例, nodes[:10])3.2 构建节点间的流量LinksLinks描述的是节点之间的流动。在我们的多期数据中流动发生在相邻的年份之间2000→2005, 2005→2010, 2010→2015, 2015→2019。我们需要计算每两个相邻年份之间所有类型组合的转移面积总和。# 计算相邻年份之间的转移量 links_data_frames [] # 用来存放每个阶段的转移DataFrame # 定义相邻的列对 stage_pairs [(name1, name2), (name2, name3), (name3, name4), (name4, name5)] for source_col, target_col in stage_pairs: # 按“源”和“目标”分组并对“Count”求和 stage_flow df.groupby([source_col, target_col])[Count].sum().reset_index() # 重命名列以符合links的标准格式source, target, value stage_flow.columns [source, target, value] links_data_frames.append(stage_flow) # 将所有阶段的转移数据合并成一个大的DataFrame all_links_df pd.concat(links_data_frames, ignore_indexTrue) # 将DataFrame转换为桑基图需要的links列表格式 links [] for _, row in all_links_df.iterrows(): links.append({ source: row[source], target: row[target], value: int(row[value]) # 确保value是数值类型 }) print(f共生成 {len(links)} 条流量连线。) print(前5条连线示例, links[:5])到这里最核心、也最容易出错的数据准备工作就完成了。我建议在生成nodes和links后都打印出来看看前几项检查一下格式是否正确比如节点名有没有乱码流量值是不是整数。这些小检查能省去后面绘图时很多调试时间。4. 使用Pyecharts绘制交互式桑基图数据齐备就可以开始画图了。这里我强烈推荐pyecharts库它是基于ECharts的Python接口生成的桑基图是交互式HTML文件。你可以用浏览器打开鼠标悬停在节点或连线上查看详细信息非常利于探索数据。4.1 基础绘图与美化首先确保安装了pyechartspip install pyecharts。from pyecharts.charts import Sankey from pyecharts import options as opts # 创建桑基图对象 sankey ( Sankey() .add( series_name土地利用变化, # 系列名称会显示在提示框里 nodesnodes, linkslinks, # --- 关键美化参数 --- linestyle_optopts.LineStyleOpts( opacity0.4, # 连线透明度0-1适当调低避免过于杂乱 curve0.5, # 连线弯曲度0为直线1为最大弯曲 colorsource, # 连线颜色继承自源节点颜色这样同一源头的流颜色一致好看 ), label_optsopts.LabelOpts( positionright, # 节点标签显示位置可选top, bottom, left, right font_size10, ), node_gap15, # 同一层级节点之间的间隔 node_width20, # 节点的宽度 node_alignjustify, # 节点对齐方式justify效果通常不错 ) .set_global_opts( title_optsopts.TitleOpts( title2000-2019年土地利用类型转移桑基图, subtitle数据来源你的数据说明, title_textstyle_optsopts.TextStyleOpts(font_size18) ), tooltip_optsopts.TooltipOpts( triggeritem, formatter{b} # 提示框格式{b}代表数据项名称 ), ) ) # 渲染生成HTML文件 sankey.render(land_use_sankey.html) print(桑基图已生成请用浏览器打开 land_use_sankey.html 查看。)运行这段代码你就会得到一个基础的交互式桑基图。在浏览器里打开生成的HTML文件试着把鼠标移到图上悬停在节点上会高亮显示所有从它出发和到达它的流悬停在连线上会显示这条流的具体来源、目标和流量值。这个交互特性是静态图片无法比拟的。4.2 解决常见问题布局优化与数据过滤第一次生成的图可能不太理想比如节点挤在一起、线条太乱。别急我们可以调整。问题一节点太多图太拥挤如果你的土地类型多比如超过15类又跨了5个年份节点总数类型数×年份数可能超过70个图会非常复杂。解决办法是数据聚合。合并次要类型将面积占比很小的土地类型如“冰雪”、“裸地”合并到“其他”类别。聚焦核心变化只关心某几类重要土地类型如“耕地”、“建设用地”、“森林”之间的转化可以在生成links前过滤数据。设置阈值只绘制流量转移面积大于某个阈值的连线小流量忽略不计。这可以在生成links列表时通过判断row[‘value’]来实现。问题二想突出显示特定流向比如你想高亮显示所有流向“建设用地”的流。可以在设置linestyle_opt时不用color“source”而是自定义一个函数来根据target节点决定颜色。# 示例自定义连线颜色 def line_color(params): 根据目标节点决定连线颜色 if params.data[target].startswith(Artifical Surface): return #d62728 # 如果目标是建设用地用红色 else: return #a6bddb # 其他用淡蓝色 # 然后在.add()参数里替换 linestyle_opt linestyle_optopts.LineStyleOpts(opacity0.5, curve0.5, colorJsCode(f({line_color}))),注意这里用到了JsCode需要从pyecharts.commons.utils导入。这属于进阶美化能让你的图表更有故事性。5. 进阶实战让桑基图“动”起来静态的桑基图已经能展示多期变化但如果我们有超过5期、甚至10期以上的逐年数据呢把所有年份平铺出来图会横向拉得非常长。这时动态桑基图或称“流式桑基图”就是一个更优雅的解决方案。它的核心思想是只显示相邻两个时间点的流动通过一个滑块或播放按钮让用户自己控制查看哪个时间段的转移。虽然pyecharts的标准桑基图组件不支持内置动画但我们可以利用其时间线Timeline功能来模拟实现。思路是为每两个相邻年份创建一个独立的桑基图然后用时间线串联起来。from pyecharts.charts import Sankey, Timeline from pyecharts import options as opts # 假设我们已经有了按年份分好段的links数据 # links_2000_2005, links_2005_2010, links_2010_2015, links_2015_2019 # 以及对应的nodes数据可以每段用自己独立的节点也可以用一个全局节点集 # 创建一个时间线对象 tl Timeline() # 定义时间段 periods [ (2000-2005, nodes_00_05, links_00_05), (2005-2010, nodes_05_10, links_05_10), (2010-2015, nodes_10_15, links_10_15), (2015-2019, nodes_15_19, links_15_19), ] for period_name, period_nodes, period_links in periods: sankey_chart ( Sankey() .add( period_name, period_nodes, period_links, linestyle_optopts.LineStyleOpts(opacity0.5, curve0.5, colorsource), label_optsopts.LabelOpts(positionright), ) .set_global_opts(title_optsopts.TitleOpts(titlef土地利用转移: {period_name})) ) # 将这一年的桑基图添加到时间线 tl.add(sankey_chart, period_name) # 设置时间线自动播放等属性 tl.add_schema( is_auto_playTrue, # 自动播放 play_interval2000, # 播放间隔毫秒 is_timeline_showTrue, # 显示时间线轴 is_loop_playTrue, # 循环播放 ) tl.render(dynamic_land_use_sankey.html)这样生成的文件你打开后可以看到一个时间轴控件点击不同年份标签或点击播放图表就会在不同年份对的桑基图之间切换。这虽然不是严格意义上的“流动画”但能非常清晰地展示出变化是如何一步步发生的尤其适合做汇报演示。6. 解读你的桑基图从图形到洞察图画好了怎么从中读出有价值的信息呢我结合自己的经验分享几个解读视角找“枢纽”和“终端”看看哪些节点最宽流入流出总量最大这些往往是变化最活跃的土地类型。再看看哪些节点只有流入或流出很细这些可能是变化的起点或终点。比如如果“耕地(2000)”很宽但“耕地(2019)”变窄了同时“建设用地”系列节点在不断变宽那很可能揭示了城镇扩张占用耕地的过程。追踪主要流找到图上最粗的那几条连线追踪它们从哪来到哪去。这直接告诉你面积最大的土地转化类型是什么。是“草地→灌丛”这种自然演替还是“森林→耕地”这种人类活动对比不同时期的流模式如果你做了动态图仔细观察时间播放时主要流的粗细和连接关系如何变化。某个时期是否出现了新的粗流是否有的流中途消失了这能帮你定位变化发生的关键时间点。发现异常流一些非常细、但连接着不常见类型的流比如“冰雪→建设用地”可能意味着数据错误如冰川区分类错误或者发生了特殊事件如工程临时用地值得你回到原始数据或影像上去核实。我第一次用桑基图分析一个区域十年变化时一眼就发现了一条从“林地”直接到“裸地”的粗流这在我们预期中通常是林地先变耕地或草地是不常见的。回去检查发现那几年该区域发生过大规模山火遥感影像上确实有大片烧痕地。这个视觉线索让我快速定位到了关键事件这是看表格很难一下子发现的。最后别忘了桑基图是一个探索和展示工具而不是统计分析的全部。它最适合用来提出假设和讲好故事。具体的面积变化速率、驱动因子分析还需要结合其他统计模型。但有了这张直观的图你向别人解释你的研究发现了什么或者为自己下一步的深入分析找准方向都会事半功倍。