SaTScan时空扫描统计从原理到实战:聚集性检测与P值解读
简介SaTScan是一款用于空间、时间和时空数据扫描统计的自由软件广泛应用于疾病监测、集群检测与早期预警也可用于考古、生态、犯罪学等领域的空间模式分析。这份中文说明书面向公共卫生、流行病学及地理信息相关的研究人员和学生帮助读者理解软件核心功能、数据模型与操作流程。资源包仅含1个doc文件体积485KB以Word文档形式系统介绍了SaTScan的用途、离散与连续扫描统计模型包括泊松模型、伯努利模型、时空置换模型、指数模型等以及病例数据、人口数据和坐标文件的格式规范。文档还结合新墨西哥脑肿瘤、儿童白血病、纽约发热病例等示例数据集详细说明参数文件设置和实际分析步骤并给出了软件下载安装指引。目前已有2794人学习下载适合需要开展空间流行病学分析或时空扫描统计实践的使用者参考。文档兼顾原理与实操可帮助读者快速掌握SaTScan的核心用法并在真实项目中应用。1. SaTScan 中文说明书容易让人误以为只是一份翻译文档但真正需要它的场景是疾控、流行病学或公卫数据分析的人手头有一份病例时空数据想知道“这一片、这个时间段发病是不是多到不正常”。SaTScan 是一个做时空扫描统计的桌面工具你给我病例的坐标和发病时间它用一个可移动、可变大小的圆柱体窗口在地图上扫找出病例数显著增多的空间区域或时间区间。它的核心输出不是一张好看的热力图而是一串带 P 值的聚集簇。下面的内容按一线使用视角写从原理、数据准备、参数设置到典型坑读完你至少能自己跑通一个最小案例并知道怎么解读结果。2. SaTScan 的核心逻辑扫描窗口、概率模型和蒙特卡洛先把原理讲透不是让你把它当黑匣子而是因为 SaTScan 的输出好不好懂完全取决于你理不理解它背后的三个机制窗口怎么扫模型怎么比P 值是怎么来的。2.1 扫描窗口怎么“扫”——圆柱形窗口和时空维度SaTScan 的“扫描”不是传统意义上的全图遍历。它的基本操作对象是一个圆柱体窗口圆柱的底面是一个圆形区域对应空间范围圆柱的高对应时间范围。实际运行时软件在底图上逐点移动这个圆柱体同时不断改变底面半径和高度形成成千上万个大大小小的窗口。每到一个位置、每换一个尺度它都会对窗口内外的病例数做一次统计比较。为什么要用圆柱体而不是正方体因为圆形窗口对方向不敏感符合大多数疾病聚集“从源头向外扩散”的直觉。时空扫描就是让这个圆柱体同时具备空间半径和时间高度纯空间扫描可以看成高度为零、只有一个底面的退化形态而纯时间扫描则退化成一段连续时间。这样设计的好处是你不需要预先认定聚集是圆形的还是长条形的软件会尝试各种半径直到找出“最不可能随机出现”的那个窗口。需要注意的是SaTScan 不会只报告一个窗口。它会先找似然比最大的那个窗口作为一级聚集簇然后在扣除该簇影响的条件下在剩下的区域里继续找二级、三级簇。这个过程叫做顺序推断。所以输出结果里你会看到多个聚集簇每个簇都有自己的 P 值而一级簇往往是信号最强、最值得关注的那个。提示如果你在输出里看到很多个簇不要急着把所有簇都当作“独立暴发”。它们之间有可能是同一个大簇的碎片化表现尤其是当最大空间尺度设得偏小时一个真实聚集容易被拆成多个相邻小簇。2.2 概率模型怎么选泊松、伯努利还是时空模型扫描窗口只是一个“筛子”真正决定窗口内外差异是否显著的是概率模型。SaTScan 支持多种模型常用的有三个。离散泊松模型是最常用的。它假定每个地点某个时间段的病例数服从泊松分布而分布均值与该地点的人口数成正比。适合的场景是你有每个乡镇或街道的人口数以及按地点、按时间汇总的病例数比如传染病月报数据、肿瘤登记数据。SaTScan 会计算每个窗口的期望病例数——把总病例按人口比例分摊到窗口内——然后用实际病例数去比。伯努利模型则适合病例-对照设计。比如你有一组病例点再配一组对照点或者用“发病地点”和“未发病地点”两类二元结果。这时不需要人口数只需要在病例文件里放病例点对照文件里放对照点。模型会比较两类点在窗口内外的比例是否失衡。需要注意对照点的选取方式直接影响结果对照点的空间分布应当代表人群的风险暴露面而不是随便撒一把点。除此之外还有多变量泊松模型、有序模型和时空模型。这里最容易翻车的是把“时空扫描”和“空间扫描”当成同一个东西。时空扫描要求数据带有时间字段比如发病周或发病月输出会给出每个簇的时间区间纯空间扫描则忽略时间输出只有空间范围。如果你的数据有时间但你想先看空间热点就做纯空间扫描如果你怀疑暴发是某几个月内出现的就做时空扫描。选模型的底线是先问自己手里有什么数据。有人口和汇总病例数用泊松只有病例点和对照组用伯努利既想控制混杂又想看多个类型的联合聚集再去研究多变量模型。不要一上来就挑战复杂模型默认的离散泊松加纯空间扫描已经能回答大多数“有没有聚集”的问题。2.3 蒙特卡洛 P 值为什么 SaTScan 要跑几千次随机模拟SaTScan 报告的 P 值不是你在教科书里常见的基于正态近似的 P 值而是一个蒙特卡洛 P 值。原因是扫描统计里你尝试了成千上万个窗口每个窗口都在做一次假设检验如果不做校正很容易出现假阳性。蒙特卡洛模拟可以一次性把“做了多次检验”这件事纳入参考分布。具体逻辑是先计算真实数据的最大似然比统计量然后保持人口数和病例总数不变把所有病例的位置和时间随机打乱重新做一遍扫描得到一个新的最大统计量。这算一次模拟。默认设置下 SaTScan 会重复 999 次于是你就得到 999 个随机最大值。把真实统计量放进这一千个值里排序如果真实值排第 900 名说明只有 10% 的随机数据能超过它P 值约等于 0.1如果排第 1000 名P 值就是 0.001。所以 P 值的小数位数和模拟次数直接相关。默认 999 次时最小可报告的 P 值是 0.001但分辨率只有 0.001如果你把模拟次数提高到 9999 次最小可以到 0.0001结果也更稳定。代价是运行时间几乎线性增加。我一般在探索阶段用 999 次等确定参数组合后再换 9999 次跑最终版本。模拟次数越高软件的随机数状态对结果的影响越小这个细节常被忽略。提示SaTScan 每次运行都会给出一组随机数种子。如果完全不设种子两次运行同一份数据P 值可能会有小幅度浮动这是正常的。想要严格复现就在设置里固定随机数种子并记录在分析报告里。这些原理是后面调参和理解输出结果的地基。下一步我们把数据准备好才能真正开始跑分析。3. 把数据喂给 SaTScan病例文件、人口文件与坐标文件的准备3.1 SaTScan 要哪三类文件字段怎么设计SaTScan 的典型输入是三个文本文件病例文件、人口文件和坐标文件。如果你用的是伯努利模型第二个文件变成对照文件。这三个文件结构上并不复杂但很多刚接触的人会把“一行一个病例”的明细数据直接导入结果全乱套。先说字段设计。病例文件每行代表一个“地点-时间”组合的病例计数而不是一个病例。常用字段包括地点编号、时间可选、病例数。时间字段的常见格式是整数比如第 1 周到第 52 周或者 2023 年 1 月。人口文件每行是地点编号和人口数这个人口数最好是“每个时间段对应的人群”或“整个研究期的平均人口”。坐标文件则每行一个地点编号和 X、Y 坐标。文件必需字段可选字段典型行示例病例文件location_id, casestime, sex, ageLOC01, 1, 5人口文件location_id, populationtimeLOC01, 23451坐标文件location_id, x, y无LOC01, 103.856, 30.311注意坐标文件的坐标系单位。如果 x、y 是经纬度导入时必须告诉软件如果直接用了米制投影也要保持一致不能混用。坐标单位和字段命名这些问题在导入时不会被软件检查出来出错时结果会非常离谱。3.2 用 CSV 组织数据的一个可用模板下面是我常用的一组 CSV 模板直接用文本编辑器另存为 csv 就能导入。第一份是病例文件我命名为 case.csvlocation_id,time,cases LOC01,1,3 LOC02,1,0 LOC03,1,1 LOC01,2,2 LOC02,2,1 LOC03,2,0第二份是人口文件 population.csv假设研究期内人口不变location_id,population LOC01,12000 LOC02,8900 LOC03,4350第三份是坐标文件 coord.csvx、y 用经纬度location_id,x,y LOC01,103.856,30.311 LOC02,103.912,30.405 LOC03,103.771,30.276这份模板的意思是每个 location_id 代表一个村镇或社区time 字段是时间周期编号cases 是该地点该周期的病例数。population 是单个周期的基础人口可以是研究期内的平均数。x、y 是经纬度坐标。导入 SaTScan 时需要告诉软件第一行是字段名并指定时间字段的编码规则是“数值递增、间隔相等”。如果原始数据是明细病例列表而不是按地点汇总的计数我会先用一个简单脚本聚合。常见做法是用 Python 的 pandas 做 groupbyimport pandas as pd # 明细数据每行一个病例包含 location_id 和发病日期 df pd.read_csv(cases_detail.csv, parse_dates[date]) df[time] df[date].dt.isocalendar().week.astype(int) agg df.groupby([location_id, time]).size().reset_index(namecases) agg.to_csv(case.csv, indexFalse)这段代码把病例明细按地点和发病周次汇总成 SaTScan 需要的计数格式。isocalendar().week 会直接得到 1 到 52 的周次编号。如果你的研究期跨越 3 个月把 isocalendar 换成 year 加 month 的组合会更直观如果跨年建议用统一的整数周期编号比如 202301 表示 2023 年 1 月避免第 40 周的编号在下一年又出现一次。参数上parse_dates 让 pandas 自动解析日期列groupby 后 size() 统计频数reset_index 把分组索引转成普通列。3.3 地理坐标的坑经纬度、投影和重复点坐标是 SaTScan 最容易出问题的地方而且问题往往在最开始看不出来。第一个坑是坐标单位不统一。有人从 GPS 设备拿的是经纬度有人从 GIS 里导出来的是 UTM 米制坐标混在同一张表里会让聚集簇的位置彻底错乱。我一般把所有坐标先统一成 WGS84 经纬度。在 SaTScan 的会话设置里导入坐标文件时把它标记为 Longitude/Latitude软件内部会自己处理球面距离不需要你事先投影。第二个坑是重复的 location_id。历史数据里同一个地点可能因为名称写法不同而出现两次比如“城关镇”和“城关镇街道”被当成两个地点。这样病例数会被拆散扫描窗口很难把它识别为一个聚集点。解决办法是导入前先做一次去重和合并确认每个 location_id 唯一。第三个坑是坐标精度不足。如果病例数据只记录到“某某街道”多个病例被地理编码到街道中心点那么在空间上会完全重合。SaTScan 对重复坐标很敏感它会把重合点所在的窗口判成极不均衡。遇到这种情况不要试图用更小的扫描尺度去“剖开”它而是应该按街道或社区层级聚合把同一坐标的多个病例合并成该地点的病例总数。提示与其在导入后才发现坐标有问题不如先画一张散点图。把 coord.csv 用任意绘图工具标出来肉眼看一下分布是否符合预期比如病例是否集中在城区、人口密度高地。这一眼能省掉后面读一堆乱结果的时间。4. 用 SaTScan 跑一次聚集性检测从界面到参数逐项解释数据到位后打开软件按下面的步骤走一遍。下面讲的是标准流程各版本菜单位置可能有差异但逻辑一致。4.1 新建 Session 与导入文件打开 SaTScan 后第一步是新建一个 Session。Session 可以理解为一个完整的分析任务配置文件里面保存了所有输入文件路径、参数设置和输出目录。在菜单里选 File - New Session然后依次设置输出文件名称和输出目录。导入文件的入口在左侧的 Input 面板。你需要依次指定 Case File、Population File 和 Coordinates File。点开每个字段旁边的按钮选择 CSV 文件然后勾选“File contains column headers”告诉软件第一行是字段名而不是数据。导入时如果出现文件格式不匹配的报错最常见的原因是分隔符。CSV 文件在中文环境下可能用分号分隔而 SaTScan 默认识别逗号。我一般用纯文本编辑器把分隔符统一成半角逗号并去掉所有中文全角空格。时间字段的编码也要提前想好比如用 2023-01-01 这种日期格式需要指定时间解析规则用整数周期的格式则更省事软件会按 1、2、3 递增理解。4.2 分析类型和概率模型的对应关系在 Analysis 面板里第一步是选分析类型。SaTScan 的选项包括纯空间、纯时间、时空和时空排列等。分析类型决定了窗口的形状和扫描维度也决定结果显示成什么样。第二步是选概率模型。离散泊松模型对应“病例数 人口数”伯努利模型对应“病例点 对照点”。这里有一个常见误区选了“时空扫描”不等于选了“时空排列”。时空排列模型不要求人口数据通过病例点之间的时间分布来推断聚集但统计功效不如有人口数据的泊松模型。日常分析还是优先选“Space-time Discrete Poisson”这是最稳妥的组合。用表格对照一下你想回答的问题分析类型概率模型这个地区有没有病例热点纯空间扫描离散泊松 / 伯努利有没有某个时间段发病异常纯时间扫描离散泊松哪个区域、哪个时间段同时异常时空扫描离散泊松 / 伯努利没有人口数据只有病例位置和时间时空排列无需人口用病例自身的时空分布推断选定后把时间字段告知软件。如果数据里 time 列是整数选 Numeric Time如果 time 列是日期字符串选 Date 并指定格式。不要小看这一步时间格式识别错误会导致整个时空扫描不出结果。4.3 必调参数最大空间尺度、时间尺度和扫描类型参数面板里真正影响结果的是三个最大空间尺度、最大时间尺度、窗口形状。最大空间尺度默认是总人口的 50%。软件会把窗口的半径限制在“窗口内包含的人口不超过总人口的 50%”。这个值决定了聚集簇的上限。如果只有两个聚集点相距很远中间人口很少窗口照样能把它们圈在一起因为人口上限没超。我一般把这个比例从 50% 往下调调到 20% 或 10%让扫描更聚焦于局部聚集。观察的疾病如果主要是近距离传播10% 到 20% 比较合适如果是水源污染这类可能远距离传播的可以放到 30%。最大时间尺度同样默认是总研究期的一半。比如你扫一年的周数据时间窗口最长是 26 周。时间窗口太长会让聚集“糊成一片”我通常限制在 4 到 8 周适合绝大多数急性传染病。慢性病聚集则可以把时间窗口放大到一年甚至整个研究期因为慢性病没有短时暴发的特征。窗口形状有圆形和椭圆两种。圆形是标准配置对大多数聚集足够。椭圆窗口能识别拉长的聚集形态比如沿着河流或公路分布的聚集但计算量大很多而且对参数更敏感。新手不要一上来就开椭圆先用圆形找主信号再考虑精细分析。最后把蒙特卡洛模拟次数从 999 改为 9999。这一步会让运行时间变长但 P 值更稳定。如果只是快速试探保留 999 即可。4.4 输出结果怎么读聚集簇、相对风险、P 值跑完后SaTScan 会在输出目录生成一个主文本报告通常以 .txt 结尾。打开后最先看到的是总结段然后是聚集簇列表。每个聚集簇的关键字段包括簇序号、坐标、半径、时间范围如果有、窗口内病例数、窗口内期望病例数、相对风险、似然比、P 值。相对风险的计算逻辑是窗口内病例数除以期望数再除以窗口外病例数与期望数的比值。它衡量的是“窗口内发病强度比窗外高多少倍”。相对风险 2 意味着窗口内的发病密度是窗口外的两倍而不是“病例多两倍”。P 值这一行是核心结论。如果 P 值小于 0.05意味着在随机模拟下出现比这个簇更极端结果的概率低于 5%可以认为存在统计显著的聚集。需要提醒的是P 值的大小受模拟次数影响0.049 和 0.051 之间没有本质差异不要因为卡线就过度兴奋。把多个簇排序后一级簇通常是最强的信号但也可能是数据异常点造成的要结合地图复核。如果你需要把结果放进 GISSaTScan 会一并生成 KML 文件。KML 是地图软件通用的格式可以直接叠加在底图上。我会用它和医疗机构坐标核对确认聚集簇不是围绕某个诊所单点形成的编码假象。5. SaTScan 使用避坑5 个会直接影响结论的常见问题下面这些坑是我跑数据时踩过的每一个都真实地影响过结论轻则白跑几个小时重则写进报告后被退回。5.1 病例数据里混入了重复记录现象跑出来一个亮眼的一级簇相对风险高得吓人但簇半径只有几百米病例几乎都集中在同一个坐标点。复查数据发现同一批病例在明细表里出现两次。原因个案数据在合并过程中没有做唯一性校验。比如医院系统导出的门诊记录和住院记录有重叠或者两张报表在拼接时没有去重。解决进入 SaTScan 之前先按 location_id 和时间做 groupby 求和并在脚本里用 drop_duplicates 按病例编号去重。更重要的是养成保存“按地点-时间聚合后的计数文件”的习惯而不是反复从明细表导入。5.2 人口数据分母不对导致假性聚集现象某乡镇连续多周被识别为聚集簇相对风险超过 3但该乡镇的病例数并不多只是人口数写成了几百人。原因人口数据用了不同年份或不同口径。有的乡镇用户籍人口有的用常住人口流动人口比例高的地方差异很大。SaTScan 的期望病例数完全依赖人口数分母越小期望值越低哪怕只有几个病例也会被判定为显著。解决人口文件必须统一来源、统一年份、统一统计口径。最好在数据说明里记录每个地点的口径分析前抽查人口数量级是否合理。如果人口数是动态变化的还要拆到每个时间周期而不是用一个总数糊过去。5.3 最大空间尺度设太大把整个地图扫成一个簇现象输出里一级簇是一个覆盖全区域的大圆半径拉满把高发病例都包进去相对风险接近 1P 值却显著。原因最大空间尺度默认是总人口的 50%。如果区域面积小、人口集中软件很容易选一个包含四成人口的大窗口。这种大窗口内部病例分布比较均匀相对风险被摊薄P 值却因为样本量大而变得显著结论没有实际指导意义。解决把最大空间尺度下调到 10% 到 20%让它只能“看”局部聚集。调完以后通常会出现多个半径更小、相对风险更高的簇那才是值得进一步调查的对象。血的教训是不要舍不得调小尺度先跑一次 10% 看看簇的位置是否发生变化。5.4 坐标精度不足导致聚集簇偏移现象地图上聚集簇的中心落在某街道的几何中心但实际病例散布在街道的多个村庄现场调查时找不到集中的暴露源。原因地址编码只到街道软件把街道内所有病例都赋予了同一个坐标点扫描窗口自然围绕这个点展开。这是地理编码精度不够导致的“假聚集”。解决尽量把病例坐标精确到村或社区级。如果原始数据只到街道就把空间分析单位定为街道不要再把街道内的随机点散布那样看起来精度提高了实际上只是制造了不存在的空间分布。5.5 把纯空间结果当成时空因果现象纯空间扫描报告了一个显著簇但有人解读成“近段时间该区域发病上升”。原因纯空间扫描没有时间维度它只能说明“在研究期内这片区域病例密度偏高”不能说明“最近在暴发”。很多人忽略了这一点把空间热点直接等同于时间暴发。解决想要结论落到时间上必须用时空扫描或者在纯空间扫描前先做纯时间扫描确认总体病例数有没有上升趋势。我现在的习惯是先跑纯时间扫描看全局再跑时空扫描看具体簇最后再跑一次纯空间扫描做交叉验证。三层结果都指向同一个区域时才敢写报告。6. 进阶用 SaTScan 做周期性聚集检测与结果可视化进阶我只讲三个我常用的东西它们能让结果从“统计显著”变成“可以拿去汇报”。6.1 用 SaTScan 扫“时间规律”SaTScan 不只做空间扫描。把分析类型改成“纯时间扫描”它会用一个一维窗口沿时间轴移动找出发病数显著偏高的连续时间段。这个功能很适合做流感高峰期、呼吸道疾病季节性回看。参数上只需要设置最大时间窗口的比例比如一年数据的 20% 大约是一个半月。输出里的 Time Frame 字段就是显著时间段后面跟的 P 值用来判断这个高峰期是不是随机波动。6.2 把结果导出成 KML 在 GIS 里核对我拿到结果的第一件事不是读文字报告而是把 KML 拖进地图软件核对。SaTScan 生成的 KML 里包含每个聚集簇的圆面我可以直接叠加病例点、人口数据、医疗机构位置。如果簇的中心周围没有人口密集区也没有医疗机构那很可能就是编码误差或极端值导致的。反过来簇的中心恰好是一个农贸市场或学校周边这个结果就值得深挖。6.3 一个验证习惯换参数重跑看簇是否稳定最后分享一个我坚持用的验证习惯换参数重跑。具体做法是把最大空间尺度从 20% 改成 15%把蒙特卡洛模拟次数从 999 改成 9999保持其他参数不变再跑一次。如果两次一级簇坐标基本重叠、半径相差不超过三成、相对风险方向一致说明结果稳健如果簇的位置完全漂移就要怀疑数据质量或参数选择不合理。这个习惯救我很多次。有一次我调完参数后原来的两个显著簇消失了换成了一个覆盖全区域的大簇查下去才发现是人口文件里有一个乡镇的人口数少写了一个数量级。SaTScan 能给出统计上漂亮的输出但它不会告诉你数据本身是不是干净的。参数换一换、结果抖一抖数据里的问题就藏不住了。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取