前言#
在社会序列分析中,我们常常希望比较不同个体的“人生轨迹”有多相似。传统上,这类比较多采用编辑距离(如 Optimal Matching, OM),通过模拟状态的插入、删除、替换来衡量两个序列的“变形成本”。
但还有一种更直接、更结构取向的方式:我们可以先不关心状态发生的顺序,而是关心这些状态分别出现了多少次、在哪些时间段占据了多大比例。
于是,我们就可以采取一种更“分布导向”的计算方式,包含欧几里得距离法和卡方距离法。这一次,我们讲欧几里得距离法。
其核心思想是:把每一条状态序列,划分成若干时间段,每段转为一个“状态分布向量”,然后将这些段的分布拼接成一个超长向量,用欧几里得距离来度量个体间的差异。
换句话说,我们不是把整个轨迹看成一个流程(像 OM 那样),而是把它拆解成结构性片段,再把这些片段还原成向量,用最基本的欧几里得空间距离来捕捉两个轨迹在结构构成上的不同。
下面是这个计算方法的完整逻辑链条:
- 时间段切分:例如按固定步长切成 2、3、4 段;
- 每段生成状态频率向量:每段中“就业/失业/家务”等状态出现的比例;
- 将所有段拼接:得到一个超长向量;
- 两两个体之间计算欧几里得距离:也就是计算向量空间中的直线距离
- (可选)归一化:如果 `norm = TRUE`,最后除以段数开根号(控制不同分段数带来的偏差)
目录#
- 前言
- 一、OM vs OMspell vs 欧几里得距离计算:分析社会序列的差异,有不同的思路
- 二、压缩完每个序列成为状态向量之外,我也可以用其他的距离范数,为什么一定要用 L2 距离?
- 三、在社会序列分析中的用法:TraMineR 的
EUCLID
计算距离 - 四、总体分布距离 vs 分段分布距离
- 五、重要参数解析
- 六、TraMineR 与欧几里得相关的代码逻辑梳理
- 加餐:Aitchison 空间 vs 普通欧几里得空间
一、OM vs OMspell vs 欧几里得距离计算:分析社会序列的差异,有不同的思路#
在社会序列分析中,最常见的距离度量方式,莫过于 Optimal Matching(OM)。它的核心理念是:把两条完整的状态序列视为“字符串”(就像是生物信息学家将DNA看成字符串进行分析一样),通过插入、删除、替换等操作,将一个变成另一个,计算所需的“编辑成本”。
因此,OM 是一种“顺序敏感”的距离算法:它不仅关注状态的“有无”,更关注它们出现的时间长短(duration)和顺序逻辑(order/sequencing)。
欧几里得距离(Euclidean Distance)则完全不同。它采取的是一种“顺序无关”的、结构取向的路径比较方式。每一条状态序列,会先被转换为一个状态分布向量,也就是问下面这个问题:
在整条轨迹中,每种状态分别占了多少比例?
这就意味着,原本的序列:
[就业, 就业, 失业, 家务, 就业, 就业, 失业, 就业, 家务, 就业]
会被压缩成:
[就业: 0.6, 失业: 0.2, 家务: 0.2]
这类向量就像是个体社会经历的压缩表达:它不再关心时间顺序,但保留了参与状态的频率结构。
然后我们再用欧几里得距离,来计算得出不同个体之间这些向量的差异,就能形成一个距离矩阵(distance matrix)。因此,我们可以看到,虽然和OM一样都是最后得到距离矩阵,但是二者思路是很不一样的。
为什么这种“转向向量”的方法重要?#
这种方法的核心理念是:
-
我们不是关心时间顺序,而是关心“结构性特征”,比如,这个人在一生中,主要处在哪些社会角色?比例构成是怎样的?
-
这种简化更适合“高维探索性研究”:
- 聚类(K-Means)
- 可视化(MDS, t-SNE)
- 分类器输入特征
-
它为我们提供了一种“更低计算成本、更结构导向”的路径比对方式。
OMspell、OMloc 等“混合范式”的中间地带#
在 TraMineR 的扩展方法中,也有不少方法并非传统 OM:
例如 OMspell:
- 它并不再对状态进行逐一比对
- 而是将连续状态(即 spell)作为单位,比如,把“生活段落”(如连续 3 年照顾家庭)作为基本分析单元,而不是每一年的照顾家庭作为最基本的分析单位;
- 通过这种方式,它在时间成本、spell 重合等概念中重新定义了“距离”。
这种方式,其实是一种“带顺序信息的结构压缩”,介于纯粹编辑距离(OM)和纯向量距离(欧几里得)之间。
那我们可以怎么理解这些算法的差异?
方法类型 | 顺序是否重要 | 比较单位 | 典型代表 | 思维方式 |
---|---|---|---|---|
编辑距离型 | 是 | 状态(逐个比对) | OM、OMloc、LCS | 把序列当作“过程”来看 |
分布距离型 | 否 | 状态比例向量 | 欧几里得距离、CHI² | 把序列当作“构成”来看 |
Spell-based | 某种程度上 | spell 段落 | OMspell、OMtspell | 把序列当作“段落集合”来看 |
总结一句话:
OM 是时间的算法,欧几里得是结构的算法,OMspell 是段落的算法。
在社会科学中,我们可能在不同分析阶段使用不同算法:
- 初步探索 → 用状态分布(欧几里得)快速做聚类;
- 精细比对 → 用 OM 分析路径类型;
- 阶段聚合 → 用 spell-based 方法分析“生命周期段落”;
而这些路径距离之间的选择差异,其实正体现了你研究问题的侧重点:
- 你是关心“做了什么”?
- 还是“什么时候做”?
- 还是“做了多久”?
二、压缩完每个序列成为状态向量之外,我也可以用其他的距离范数,为什么一定要用 L2 距离?#
既然我们已经把每条序列压缩成了一个“状态分布向量”,那下一步的问题自然就是:
- 我们用什么距离度量两个向量之间的差异?
- 为什么偏偏是 L2(欧几里得)?有没有其他选择?
简而言之,我们之所以用 L2,不是因为它唯一,而是因为它在解释力、数学友好度、算法兼容性之间做到了最好的平衡。
它就像社会科学里的中性选项,不特别强调哪一维、不特别保护异常值、不过度扁平一切。
1. L2 是最“直观”的距离:#
当你把一个人的状态序列转换成向量,比如:
- A =
- B =
L2 就是在这个向量空间中计算它们之间的直线距离:
就像是我们上一篇教程里说的,它符合人类最本能的“空间距离直觉”。
2. L2 强调“结构性偏差”,而不是“个别极端差”#
- 每一维度的差都会被平方 → 小差异影响小,大差异影响大。所以它更强调整体结构性偏差;
- 对于社会轨迹来说,这种整体结构的“倾向”往往才是我们关心的核心。
3. L2 是大多数算法的默认选择#
如果我们打算用这些向量去做下面的:
方法 | 是否默认使用 L2 |
---|---|
K-Means 聚类 | 默认用 L2 |
PCA 主成分分析 | 基于最小平方误差(L2) |
MDS / t-SNE / UMAP 可视化 | 通常使用欧几里得作为原始度量 |
神经网络、回归预测 | 常用 L2 作为损失函数 |
换句话说,用 L2 可以让你的数据无缝地接入整个分析工具链。
4. L2 的数学性质更“友好”#
- 平滑、可导 → 易于优化、梯度分析;
- 满足三角不等式(是个合法的度量空间);
- 理论基础清晰(比如对应高斯噪声假设、最小方差原理);
那为什么不用 L1、L∞、Cosine、Jaccard?#
可以用,只是各有利弊:
距离类型 | 优点 | 缺点 | 适合什么时候 |
---|---|---|---|
L1(曼哈顿) | 稳健,对极端值不敏感 | 可能低估大偏差 | 数据中存在离群值 |
L∞(切比雪夫) | 只看最大差异,直觉清晰 | 忽略其他维度 | 只关心最“极端”的差异 |
Cosine(余弦相似度) | 只比较“方向” | 忽略幅度 | 向量总量不同但构成类似的比较(如文本) |
Jaccard | 只比较是否出现过 | 忽略次数/比例 | 适合稀疏状态、高维分类(如标签) |
如果你愿意加入更多分析深度,还可以引出这样的问题:
- 如果有某种状态特别稀有,那是否应该用“加权 L2”?
- L2 不考虑顺序,那我们是否需要进一步用 OM/LCS 等方法补充“顺序性”?
- 对于高度离散、类别变化快的数据,是否应该用 L1 或基于频率的卡方/余弦距离?
例子说明:5 个人的社会轨迹状态分布向量#
我们用一个具体的小例子来展示同一组状态分布向量在不同范数下的“距离排序”和“聚类结果”如何发生变化。
设每个个体的社会轨迹被简化成 3 个状态的分布比例:
个体 | 状态分布向量(就业, 失业, 家务) |
---|---|
A | [0.7, 0.2, 0.1] |
B | [0.6, 0.3, 0.1] |
C | [0.1, 0.7, 0.2] |
D | [0.2, 0.6, 0.2] |
E | [0.4, 0.4, 0.2] |
我们以 A 为参考对象/基准,计算 A 和其他人的距离,并看不同范数下结果有何不同。
1. L2 欧几里得距离#
与 A 的距离 | 值 |
---|---|
B | √((0.7−0.6)² + (0.2−0.3)² + 0²) = √0.02 ≈ 0.14 |
C | √((0.6)² + (−0.5)² + (−0.1)²) = √0.62 ≈ 0.79 |
D | √(0.5² + 0.4² + 0.1²) = √0.42 ≈ 0.65 |
E | √(0.3² + 0.2² + 0.1²) = √0.14 ≈ 0.37 |
排序:B < E < D < C
2. L1 曼哈顿距离#
与 A 的距离 | 值 |
---|---|
B | 0.1 + −0.1 + 0 = 0.2 |
C | 0.6 + −0.5 + −0.1 = 1.2 |
D | 0.5 + 0.4 + 0.1 = 1.0 |
E | 0.3 + 0.2 + 0.1 = 0.6 |
排序:B < E < D < C(排序一致)
3. L∞(切比雪夫距离)#
与 A 的距离 | 值 |
---|---|
B | max(0.1, 0.1, 0) = 0.1 |
C | max(0.6, 0.5, 0.1) = 0.6 |
D | max(0.5, 0.4, 0.1) = 0.5 |
E | max(0.3, 0.2, 0.1) = 0.3 |
排序:B < E < D < C(排序一致)
聚类结果可能差异
- L2 & L1 聚类:A 和 B 会被归到一个小簇;E 可能中性;C、D 明显更远。
- L∞ 聚类:更加注重“最大差值”的维度 → C 会被“单独拉远”。
结果对比表
范数类型 | 强调点 | 离 A 最近 | 离 A 最远 |
---|---|---|---|
L2 | 整体结构差异 | B | C |
L1 | 总体偏差总量 | B | C |
L∞ | 最坏维度差异 | B | C |
无论哪种范数,B 都是离 A 最近的,但不同范数强调的“远离逻辑”不同。
三、在社会序列分析中的用法:TraMineR 的 EUCLID
计算距离#
使用 TraMineR 包中的 seqdist()
函数,我们就会发现,在社会序列分析中,我们其实还有两个与“窗口”有关的概念:step
和 overlap
。
step
和 overlap
的默认值#
step
#
在 TraMineR
的 seqdist()
和内部的 CHI2()
函数中,step
的默认值是 1.
这是你在源代码中的这一行可以看到的:
seqdist <- function(..., step = 1, ...)
同时在 CHI2()
函数中也明确写了:
CHI2 <- function(seqdata, breaks = NULL, step = 1, ...)
step = 1
表示:每个时间点都被当作一个“窗口”或“段”,也就是 按每一个时间单位逐个比较状态分布。
这会导致生成的 breaks
列表是这样的(假设序列长度为 5):
breaks = list(c(1,1), c(2,2), c(3,3), c(4,4), c(5,5))
也就是说,逐点计算每个时间点的状态分布,然后逐点比较这些分布的差异,再累加起来作为总距离。
注意:如果你的序列很长,step=1
会生成很多小段。这样会让算法:
- 更敏感于微小差异
- 也更容易受到偶发状态的影响
- 可能会增加计算负担
你也可以尝试不同的 step
,比如:
d1 <- seqdist(seqdata, method = "EUCLID", step = 1) # 更细粒度d2 <- seqdist(seqdata, method = "EUCLID", step = 4) # 按阶段比较
来感受粒度变化对相似度计算的影响。
overlap = FALSE
#
你在源代码中 CHI2()
的函数定义里可以看到:
CHI2 <- function(..., overlap = FALSE, ...)
overlap
是指 时间段之间是否“重叠”,也就是窗口是否滑动。
举个简单的例子,假设你有长度为 6 的序列:
状态序列:A B C D E F
你设定:
step = 4overlap = FALSE
那么 TraMineR 会把序列划成 不重叠的段:
- 段1:位置 1-4 → A B C D
- 段2:位置 5-6 → E F
(注意:第二段短于 step=4,会有警告)
于是 TraMineR 会发出如下警告(在 CHI2()
源码中):
msg.warn("With step=", step, " last episode is shorter than the others")
为什么要警告?
因为这种“不等长分段”会造成每段的权重不一致:
- 第一段的状态分布统计来自 4 个时间点
- 第二段只有 2 个时间点 → 比较不稳定,对结果影响更小
对于距离计算,可能导致:
项目 | 影响说明 |
---|---|
欧几里得距离 | 后一段贡献的分布差异“相对减小” |
卡方距离 | 分母(频率)更小,结果可能变大或不稳定 |
标准化(norm) | 默认还是除以 √K ,但每段实际贡献不均 |
结果可解释性 | 不同段的“比较力度”不同,不利于解释 |
会影响计算结果吗?会的,但取决于 step
设置与序列长度之间的关系。
情况1:只是最后一段稍短(如 4+2)
影响有限,通常仍可使用,但要标记下来。
情况2:多个个体因为 missing 或 padding 而导致尾段不等长
可能造成更明显的偏差 → 建议修剪或调整 step
。
那么,如何处理这个问题?
方案1:手动设置 breaks
,避免不等长段
breaks = list(c(1, 3), c(4, 6)) # 自定义段落seqdist(..., breaks = breaks)
方案2:使用 norm = TRUE
,让段落数被考虑进标准化过程
seqdist(..., norm = TRUE)
方案3:开启 overlap = TRUE
,改用滑动窗口,保证窗口长度一致:
step = 4overlap = TRUE
→ 会生成重叠段,例如:[1-4], [3-6]
关于警告的这块说完了,我们又回到主逻辑里继续梳理。
如果你设置的是:
step = 4overlap = TRUE
TraMineR 会改为用 滑动窗口方式划段:
- 段1:位置 1-4 → A B C D
- 段2:位置 3-6 → C D E F
它像滑窗一样,从前面移动,每次只滑一半(step/2
),这样后续段可以捕捉前一段的延续性变化。
为什么有时候要用 overlap = TRUE
?
- 可以提高时间解析度
- 更好地捕捉序列中状态变化的“过渡期”
- 更适合分析流动型或细腻变化的社会轨迹(比如就业→失业→再就业)
但代价是:
- 会计算更多的段,增加计算量
- 某些分析可能引入冗余信息
具体这两个参数是什么意思?#
说先要说明,我们之前算的是没问题的,那些是理论上全局的欧几里得距离:
这个例子是每个人有一个“整体”状态分布向量(比如一生 70% 就业、20% 失业、10% 家务),这就像只在一个阶段计算的状态分布,那么就直接比较两个向量的欧几里得距离,是没问题的。
那 TraMineR 为啥要加个 step
和 overlap
搞得这么复杂?因为 这个算法在社会序列分析的应用中,不直接对全局分布向量比较,而是把一个人的整个序列按时间“切成几段”,每段计算一个状态分布向量,然后在这些段上分别做距离比较,再平均/合并起来。
举个例子更形象:
假设 A 的时间序列是 12 步长(比如 12 年):
A: 就业 就 就 就 就 家 家 家 失 失 失 失
现在如果 step = 4
,那么 A 会被切成 3 段:
- 第 1 段(1-4):就 就 就 就 → [1, 0, 0]
- 第 2 段(5-8):就 家 家 家 → [0.25, 0, 0.75]
- 第 3 段(9-12):失 失 失 失 → [0, 1, 0]
同理对其他个体也切成一样的段,再每段分别计算欧几里得距离,最后对所有段距离求平均或加总。
也就是说,我们计算的方式是 “1个整体分布 vs. 另一个整体分布”
而 TraMineR::seqdist(..., method="EUCLID")
的方式是:“多个子时间段分布分别比较 + 汇总”
如果要想得到我们之前的这种整体分布距离,该怎么做呢? 我们只需要这样:
# 按整段算,不分段:step = 序列总长度d <- seqdist(seqdata, method = "EUCLID", step = ncol(seqdata), overlap = FALSE)
这样 seqdist()
会只切一个段,计算的就和我们之前用手算的全局状态分布向量欧几里得距离是一样的。
总结一下
方法 | 描述 |
---|---|
我们之前算的方式 | 全局状态分布之间的欧几里得距离 |
TraMineR 默认方式 | 多个时间段的状态分布欧几里得距离,最后合并结果 |
如何设为全局分布比较 | 使用 step = ncol(seqdata) ,避免划段 |
四、总体分布距离 vs 分段分布距离#
那为什么这个算法在社会序列分析中,默认值是1,也就是要分段计算呢?
先说结论:
因为社会序列是“随时间演化”的过程,直接计算总体状态分布虽然简单,但会掩盖时间序列中的动态信息。
举个对比来解释,想象你有两个人的一生状态轨迹(简化为“就业、失业、家务”三种状态),长度都是 12:
个体 A(典型轨迹):
前期稳定就业 → 中期转向家务 → 后期失业[就 就 就 就 家 家 家 家 失 失 失 失]
个体 B(完全相同的总体比例,但顺序不同):
前期失业 → 中期就业 → 后期家务[失 失 失 失 就 就 就 就 家 家 家 家]
两人的总体状态分布是一样的:
- 就业:4/12 = 0.33
- 家务:4/12 = 0.33
- 失业:4/12 = 0.33
如果你只用总体状态分布来算欧几里得距离?那么距离 = 0(因为状态分布完全一样)。 但这显然 忽略了“时间顺序的巨大差异”。一个人前期就业,一个人前期失业,这在社会科学中可能代表完全不同的生活轨迹和含义。
那么这个算法是怎么补救这个问题的? “我们不能只看状态总量,还要看这些状态是什么时候发生的”
于是它:
- 将时间序列按时间段分段
- 每段都单独计算状态分布(一个小向量)
- 对每段之间的状态分布向量做欧几里得距离比较
- 最后将这些段落距离累加或平均
这样就能回答下面的这些问题:
- 谁在哪段时间状态变化了?
- 是否有人某一段特别像/特别不像?
- 哪些阶段差异特别大?
用表格来对比的话:
方法 | 优点 | 缺点 |
---|---|---|
总体分布距离 | 简单快速、清晰 | 无法识别时间结构变化 |
按段分布+比较(TraMineR) | 能反映状态的时间动态、变化路径 | 稍复杂,依赖 step 参数 |
也就是说:
欧几里得社会序列距离算法之所以逐段比较状态分布,而不是只算总体,是为了保留序列的“时间动态特征”。
这样才能更真实地度量社会轨迹中“什么时候发生了什么”的差异。
注意欧几里得距离的“弱顺序意识”#
在之前的教程里,我有稍微提到欧几里得距离是没有什么顺序意识的,因为如果我们算状态总体分布的话,就没有顺序可言了。
也就是说,这个评价主要指的是,在 默认的(较大 step)设置或总体分布方式下,欧几里得只关注了状态“比例”,完全忽略了状态发生的时间顺序。
比如:
step = ncol(seqdata)
(整段)step = 4
(粗粒度划段)- 或我们之前提供的那种
[0.7, 0.2, 0.1]
向量比较
在这些情况下,状态位置完全被抹平,所以才说它“忽略顺序”。
但因为我们在这个教程里学到了更多细化的东西,我们在这里可以更加精确地理解这个算法:
欧几里得距离本身不考虑状态发生的先后顺序,但在设置 step = 1
时,由于每个时间点都独立比较,它“间接地”表现出对顺序的敏感性。
换句话来说,当 step = 1
时,欧几里得确实“部分”捕捉了状态顺序的效果。
因为这时候的逻辑是:
- 每个时间点都当作一个“段”,而每段也只有一个状态
- 所以状态分布向量变成 one-hot 编码(某个状态为 1,其余为 0)
- 实际上你就是按时间逐点比较状态是否一致
举个例子,我们来对比两个序列:
序列长度 5 | A | B |
---|---|---|
[就,就,就,失,失] | [失,失,失,就,就] |
两个序列的总体状态分布是一样的(就=3,失=2),总体分布距离为 0。
但 step=1
时,我们需要针对每个时间点进行对比:
- 第 1 点:就 vs 失 → 不同
- 第 2 点:就 vs 失 → 不同
- 第 3 点:就 vs 失 → 不同
- 第 4 点:失 vs 就 → 不同
- 第 5 点:失 vs 就 → 不同
于是,5 个位置全部不同,欧几里得距离就很大。因此,step = 1
让欧几里得在形式上类似于 Hamming 距离,变得非常“结构敏感”。
五、重要参数解析#
在这里,我们主要用到了一篇论文的结论,而这篇论文非常重要,建议反复阅读(可以在 Google Scholar 上搜索文章名称,并下载 pdf 文件阅读):
文章名字:What matters in differences between life trajectories: A comparative review of sequence dissimilarity measures
作者:Matthias Studer 和 Gilbert Ritschard
期刊:Journal of the Royal Statistical Society: Series A (Statistics in Society), 2016, Vol. 179, Part 2, pp. 481–511
这篇论文也发表在了非常顶尖的期刊上,由英国皇家统计学会主办,代表了统计学界最核心的学术标准之一。 这个期刊有三个系列(A/B/C,A 是社会统计,B 是理论统计,C 是应用统计)。而这篇文章,是发表在社会统计期刊上。
1. 为什么 EUCLID
不需要 OM 的 indel
和 sm
?#
论文中的解释在第 486 页(3.1节)非常清楚:
“The dissimilarity between sequences is measured by the distance between the distribution vectors by using either the Euclidean distance or the χ²-distance. […] However, it is insensitive to the order and exact timing of the states.”
也就是说,欧几里得距离只比较状态比例向量,并没有试图“编辑”某个序列来变成另一个序列。
而这和 indel
/ sm
的本质完全不同:
概念 | 含义 | 适用于哪种方法 |
---|---|---|
indel | 插入或删除一个状态的成本 | 只适用于编辑距离(如 OM) |
sm | 替换一个状态为另一个状态的成本矩阵 | 同样是编辑距离相关参数 |
EUCLID | 对每条序列统计状态占比,变成向量后比较 | 不涉及“编辑”操作,只是向量比较 |
因此,欧几里得距离没有用到 indel 和 sm,是因为它完全不是基于编辑操作的逻辑,而是基于分布差异的几何比较。
2. Number of intervals(K)设置多少合适?#
论文第 503 页明确讨论了这个参数:
“When K increases, the sensitivity of the χ²-measure shifts from duration to timing. For K equal to the sequence length (here, 20), CHI2 receives scores that are similar to those of the Hamming family regarding timing but maintains some sensitivity to differences in durations.”
也就是说:
K 的值 | 比较的是? | 敏感性表现 |
---|---|---|
K = 1 | 整体状态分布 | 对“总量差异”敏感 |
K = T | 每个时间点(如 step=1) | 对“时序/顺序”敏感 |
中间值(如 K=5) | 分段后看分布差异 | 综合考虑时间与比例 |
- K = 1:只想知道整体暴露差异(如失业总时长是否不同)
- K = ncol(seqdata) 或
step = 1
:想捕捉细致的时间变化,就是每个时间点都会作为一个窗口,等效于 Hamming 风格 - K = 4~6:一般用于时间跨度比较长的生命历程,一年一个阶段太细,两年一个太粗时的折中
- 建议:先尝试 K = 5(论文中反复使用的值)
- 之后可通过敏感性分析(如不同 K 下的 MDS、聚类)比较效果,找到最佳分析粒度
⚠️注意:这篇论文里面的 K 和函数里面的参数 step 是有区别的!#
在这篇 Studer & Ritschard (2016) 的论文中,K 与step
参数非常相关,因为它们都控制了序列被分为多少段来分别计算状态分布距离。
你可以理解为:
K = floor(总长度 / step) ← 如果没有 overlap
术语 | 出现位置 | 实际含义 |
---|---|---|
K | 论文中使用 | 将整个序列划分为 K 个时间区间 |
step | 在 TraMineR 的 seqdist() 函数中 | 每个区间的步长(window size) |
具体什么意思呢?
首先,论文中提到的:
“Following Deville and Saporta (1983), we can overcome this limitation by considering the distribution in K successive—possibly overlapping—periods. The distance is then equal to the sum of the χ²-distances for each period.”
而在 TraMineR 源代码中:
CHI2(seqdata, step = ..., overlap = ..., euclid = TRUE/FALSE)
这段代码内部是这样构建 K
个 time intervals 的:
- 每隔
step
个时间单位划一段 - 所以总共可以划出
K = floor(ncol(seqdata) / step)
或更多(如果 overlap 为 TRUE)
举个例子,假设你有一个 12 步长的序列:
- 如果
step = 1
→ 每个时间点为一个段 →K = 12
- 如果
step = 4
→ 一段是 4 个时间单位 →K = 3
- 如果
step = 4
且overlap = TRUE
→ 你会得到更多的段,比如K = 5
所以论文说的:“We consider CHI2 with K=1, 5, 10, 20…”
在 R 包中就是:
seqdist(..., method="EUCLID", step = 12) # 相当于 K = 1seqdist(..., method="EUCLID", step = 2) # 相当于 K = 6seqdist(..., method="EUCLID", step = 1) # 相当于 K = 12
欧几里得距离在社会序列分析中的利与弊#
优点 | 描述 |
---|---|
快速 | 计算简单,可扩展到上万条轨迹 |
易解释 | 距离越小,分布越相似 |
兼容主流算法 | 可用于 MDS、聚类、tSNE |
稳定 | 不依赖于参数设置 |
局限 | 描述 |
---|---|
弱顺序意识 | 默认按整体分布比较,忽略“状态顺序”;但在设置 step = 1 时,能够间接捕捉状态序列的时间结构(类似 Hamming 距离) |
无语义意识 | 视“就业”和“照顾家庭”为平行维度 |
无结构敏感性 | 不考虑某些状态的稀有性或重要性 |
六、TraMineR 与欧几里得相关的代码逻辑梳理#
TraMineR 在欧几里得距离计算的函数实现上有一些复杂,但关键还是看下面的三个文件:
- R 文件入口:
TraMineR/R/seqdist()
,具体源代码链接点这里 - R 文件包含具体逻辑:
TraMineR/R/seqdist-CHI2.R
,具体源代码链接点这里 - 上面的这个R文件引出了 C++ 的底层计算:
src/chisq.cpp
,具体源代码链接点这里
核心结构概览#
TraMineR 中的 seqdist()
是一个统一的距离计算接口,支持多种方法,包括
- 编辑距离类(OM/HAM)、
- 分布类(CHI2/EUCLID)、
- 结构类(NMS/SVRspell)等。
它本质上做三件事:
- 参数准备与校验:检查合法性,补齐默认值
- 方法准备:对
indel/sm/refseq
等参数做预处理,并生成最终需要送入 C++ 核心函数的参数结构 - 执行计算:调用内置的 C/C++ 核心函数(或者是纯 R 实现,但比较少)完成矩阵输出
seqdist 函数的整体流程和逻辑顺序#
- 输入校验:确认输入的是序列对象
seqdata
,不是矩阵或字符;确认方法名合法(如 EUCLID) - 设置默认值和参数预处理:如
norm="auto"
、indel="auto"
、是否带缺失值等 - 特殊变量准备:如
sm
(substitution matrix)、refseq(参考序列)等 - 转换输入序列为数值形式:
seqnum(seqdata)
- 按方法不同选择不同处理:
- 如果是
EUCLID
或CHI2
→ 使用 R 中的CHI2()
函数 → 底层计算由chisq.cpp
这个文件实现 - 其他方法(OM/HAM 等)→ 准备参数后调用
.Call()
接口执行 C++ 核心代码
- 返回结果:可选输出为 pairwise 距离矩阵或参考序列的距离向量(前者占绝大多数)
为什么 EUCLID 和 CHI2 被放一起?#
CHI2()
是一个通用的“分布向量比较器”:
- 接收一个
step
参数 → 将序列分段(即论文中所说的 K 段) - 每段统计状态频率分布 → 得到频率向量(比如
[0.2, 0.7, 0.1]
) - 然后计算距离:
euclid = TRUE
→ 使用欧几里得距离euclid = FALSE
→ 使用加权卡方距离(原始 CHI2)
- 最终由 C_tmrChisq()(pairwise distance matrix)或 C_tmrChisqRef()(针对 refseq)执行
它们共用逻辑的原因是:只有最后的计算公式不同,其余都是一样的频率矩阵操作。因此,它们被纳入到了同样一个文件里面。
CHI2()
/EUCLID()
函数逻辑详细步骤#
- 构建 breaks:按 step 划分时间段
- 对每段生成频率矩阵:统计每条序列在该段内各状态出现频率
- 构建比较矩阵(dummies)
- 选择距离计算方式:
- 欧几里得:
- 卡方: (下一篇教程会讲)
- 处理参考序列(refseq) 或生成所有 pairwise 距离
- 规范化(norm)除以段数平方根(可选)
作为刚上手的开发者,可以先只实现 step=1
, overlap=False
, norm=False
的最简单版本,然后再逐步支持完整参数。
术语小词典#
术语 | 含义 |
---|---|
seqdata | 输入的状态序列数据,类似于二维数组(行=个体) |
step | 每个窗口的时间长度,控制分段粒度 |
breaks | 按 step 划分得到的时间段索引 |
dummies | 每段内状态的 one-hot 分布向量 |
refseq | 用于计算相似度的参考序列 |
norm | 是否对最终距离进行标准化 |
indel | 编辑距离中的插入删除成本,仅 OM 系列使用 |
sm | 状态替换成本矩阵(substitution matrix) |
.Call() | R 调用 C++ 接口的函数,Python 可用 Cython 对应实现 |
关于 dummies
和 norm
,我们还需要加深理解:
什么是 “构建比较矩阵(dummies)”?#
这个术语是 TraMineR 源代码里的命名,在 CHI2()
函数内部有一个 dummies(b)
函数,它的作用可以理解成把每一段时间区间内的状态序列,转成“状态分布向量”
示例:2 个个体,状态集为 [E, U, H]
,step = 2
,序列长度为 4
假设我们有如下原始状态序列:
个体 | 原始序列 |
---|---|
A | E, E, H, H |
B | E, U, U, H |
将它们按step = 2
划分为两个时间段:
-
段1(第1-2位):A =
[E, E]
,B =[E, U]
-
段2(第3-4位):A =
[H, H]
,B =[U, H]
-
dummies 矩阵(状态分布)如下:
个体 | 段 | E | U | H |
---|---|---|---|---|
A | 1 | 1.0 | 0.0 | 0.0 |
A | 2 | 0.0 | 0.0 | 1.0 |
B | 1 | 0.5 | 0.5 | 0.0 |
B | 2 | 0.0 | 0.5 | 0.5 |
也就是说
- 每段内的三个值是该段中三种状态(E/U/H)出现的频率
- 行数 = 个体数 × 时间段数
- 列数 = 状态种类数
这个矩阵会被“平铺”为一个大矩阵(例如 [E1, U1, H1, E2, U2, H2]
)作为最终用于距离计算的向量输入。
个体 | 向量结构 |
---|---|
A | 段1: [1, 0, 0] + 段2: [0, 0, 1] ⇒ [1, 0, 0, 0, 0, 1] |
B | 段1: [0.5, 0.5, 0] + 段2: [0, 0.5, 0.5] ⇒ [0.5, 0.5, 0, 0, 0.5, 0.5] |
平铺后,每个个体变成这样:
个体 | 平铺向量(6维) | 解释 |
---|---|---|
A | [1.0, 0.0, 0.0, 0.0, 0.0, 1.0] | 段1 + 段2 |
B | [0.5, 0.5, 0.0, 0.0, 0.5, 0.5] | 段1 + 段2 |
这就是 TraMineR 中 EUCLID 方法计算的核心逻辑:
把整条时间序列按段切分,每段变频率向量,然后拼起来当作一个超长向量,进行向量空间中的欧几里得的距离计算。
那如何计算欧几里得距离(L2)呢?
公式:
对 A 和 B:
- A 向量:
[1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
- B 向量:
[0.5, 0.5, 0.0, 0.0, 0.5, 0.5]
计算每一维的差值平方:
维度 | A | B | 差值 | 平方 |
---|---|---|---|---|
1 | 1.0 | 0.5 | 0.5 | 0.25 |
2 | 0.0 | 0.5 | -0.5 | 0.25 |
3 | 0.0 | 0.0 | 0 | 0 |
4 | 0.0 | 0.0 | 0 | 0 |
5 | 0.0 | 0.5 | -0.5 | 0.25 |
6 | 1.0 | 0.5 | 0.5 | 0.25 |
总和:
最终距离:
注意:
虽然叫
dummies
,但它现在其实做的是状态比例的提取,而不是传统意义上的哑变量。可以理解为:每段时间的状态分布向量,是整个欧几里得/卡方距离计算的起点。
dummies
这个词,其实是从“哑变量 dummy variable”借用来的,但 TraMineR 中这个函数的实际逻辑,和传统哑变量已经不一样了,其实不应该这么叫的,很有误导性。
在统计学中,dummy variable 一般是:一个分类变量转换为一组 0/1 向量,表示是否属于某一类。 你可以理解为,就像是一个聋哑人一样,在一些情况下只能点头(yes - 1)和摇头(no - 0)来回答问题。
那为什么 TraMineR 沿用这个词?在 TraMineR 最初的开发中,dummies()
函数的确只是对状态做 one-hot 编码(就像统计学的 dummy variable 一样),每一列表示“是否为某状态”,所以叫这个名字是合理的。 但后来它被扩展为:在一个时间段内,对状态出现频率求占比(比例),变成了 分布向量。 也就是说它从 “0/1” dummy → “频率向量” 进化了,但函数名没改,现在这个名字会让人很困惑。
因为从用户的角度看,这个函数其实做的是 “按段生成状态频率分布矩阵”,而不是所谓的 0/1 哑变量。 一个更直观的名字可能是:
get_state_distribution_by_window()
或者在 Python 里叫:
def compute_state_distribution_per_segment()
但 TraMineR 作为一个老牌 R 包,很多命名延续自早期代码传统,未做重命名。我们需要改掉这样的让人迷惑的命名方式,就像是 Python 力求的简洁清晰一样,宁愿变量名字比较长,也要让人一看就明白这个变量或者函数在做什么。
什么是 norm?为什么要做规范化?#
其实最关键要回答下面这个问题:
为什么在 method = "EUCLID"
的 TraMineR 中,如果我们分的段数不同,需要做 norm = TRUE
来归一化?否则会有什么“偏差”?
先说重点结论:因为每多一段,你就多拼接了一个向量段,整个向量维度变长,距离值也会“机械性变大”;不是因为人更不一样了,而是因为“多算了几段”。
举个例子:
- 情景 A:你把轨迹分成 2 段,每段是 3 维 → 总向量是 6 维
- 情景 B:你把轨迹分成 4 段 → 总向量是 12 维
同样两个人的状态比例差异在每段都一样,但因为你算了 4 段 vs 2 段,最终 L2 距离会是原来的 2 倍(平方和变成 4 个相加,而不是 2 个相加):
那“norm = TRUE”做了什么? 它本质上就是在最后一步把总距离除以段数的平方根:
distance = sqrt( sum of squared segment differences ) / sqrt(#segments)
换句话说:它让“距离值”只反映个体之间的真实差异,而不受“你分几段”这个人为操作的影响。
那这为什么会带来误差或偏差?
如果你没做归一化:
- 分得越细(段数越多),你会人工“放大”轨迹之间的差异;
- 比如两个人轨迹几乎一样,但你切成 10 段,他们每段差一点点,总距离就会“莫名其妙变大”;
- 那么你后续做聚类、可视化、距离排序时结果就不公平了。
换句话说,不归一化相当于“惩罚了你切得多的人”,而这并没有统计学上的合理性。
所以,norm = TRUE
是一个“算法公平性校正”机制
分析目标 | 是否建议 norm = TRUE |
---|---|
聚类分析(对比不同人) | 必须归一化,避免段数影响 |
可视化(MDS, t-SNE) | 否则距离受段数膨胀影响 |
对比不同切分方式结果 | 确保基准一致 |
分析某人内部变化(不同段) | 可不归一化,看绝对段差 |
在使用状态分布的距离计算算法中,大多数情况下都建议归一化(norm = TRUE
),因为只要你在比较多个个体之间的差异(比如做聚类、分类、相似度排序),你就希望:
- 距离值反映真实的“差异程度”,而不是受到“人为参数”(比如你切了几段)影响;
- 相同的轨迹在不同切分方式下(3 段 vs 5 段),距离值要尽可能一致;
- 不同人之间的距离才能有可比性,否则段数越多的人“看起来”差异越大,其实只是计算次数多。
总结一句话:分段越多,距离“膨胀”越严重,不是因为人更不一样了,而是“算的维度多了”。所以要用 norm = TRUE
来做“按段数归一化”,让距离反映的是“每段平均差异”,而不是“总段数乘以差异”。
具体在其他类别的序列分析距离计算中,比如在使用编辑距离(比如 OM)的时候,还需要归一化吗?
我们会在之后的教程里单开出来一篇教程,好好讲解。
加餐:Aitchison 空间 vs 普通欧几里得空间#
当我们说一个序列的状态分布像 [0.7, 0.2, 0.1]
(就业、失业、家务)
这其实就是一个典型的 组成向量(composition):
- 非负
- 加总为 1
- 只关心“相对比例”,不关心绝对量
这个结构和统计中的 Dirichlet 分布模型、Aitchison 几何空间 是一样的。但是,我们这里主要用的还是欧几里得空间,下面我们就仔细说说,社会序列分析 vs 组成数据分析(Compositional Data Analysis, CoDA)之间的理论分野。
组成数据的世界(Beta / Dirichlet)#
模型 | 通常用于什么 | 核心特征 |
---|---|---|
Beta | 两类组成比例(比如男/女,成功/失败) | 分布在 [0,1] 区间 |
Dirichlet | 多类组成比例(3个及以上类别) | 广义多元版的 Beta,建模一个 composition |
CoDA 分析 | 分析多个组成变量之间的结构关系 | 用的不是欧几里得空间,而是Aitchison 几何 |
那为啥这些模型中“没有欧几里得距离”?#
其实不是没有,而是在组成数据分析(CoDA)里,欧几里得距离不适用。
为什么?
因为组成数据遵循一个叫“封闭约束”(closure)的条件:
- 所有分量之和必须等于 1
- 改变一个分量会影响其他所有分量
- 所以普通的欧几里得空间中的度量不保持比值信息
CoDA 中用什么代替欧几里得?#
使用 Aitchison 距离 或基于 log-ratio(对数比值)变换 的方法:
- CLR(centered log-ratio)
- ILR(isometric log-ratio)
这些方法可以:
- 保持比例信息
- 比如:如果就业:失业是 3:1,和 60%:20% 是一样的方向
那为什么欧几里得能用在社会序列分析的距离对比中?#
因为社会序列分析中,我们并没有严格把序列看成是“组成数据”,而是把状态分布向量当作普通向量,拿来算欧几里得距离作为一种“粗粒度的相似性度量”。
它不考虑封闭约束、对数比率、不遵循 Aitchison 几何空间。它关注的是:“A 的状态占比和 B 的状态占比,有多不一样?”
所以它能快速、直观,但不是组成数据严格意义下的分析。
总结对比表:
维度 | TraMineR::EUCLID | CoDA / Dirichlet Models |
---|---|---|
是否 composition | ✅ yes(视为状态比例) | ✅ yes(严格封闭向量) |
用什么度量 | 欧几里得距离 | Aitchison 距离 / log-ratio |
是否考虑变换 | ❌ 否 | ✅ 对数变换如 CLR / ILR |
顺序信息 | 可选(step=1 可捕捉) | ❌ 纯静态分布,无时间概念 |
用途 | 社会序列相似性,聚类 | 建模行为偏好,成分关系等 |
如果你特别在意“比例结构”本身的相对性,比如:
- 某个群体家务占比小、就业占比高是否异常
- 某些状态是否呈现代偿关系(比如就业多→家务少)
那你可以试试 CoDaPack
、robCompositions
、compositions
包中的:
clr(X)
clr(X)
是 Centered Log-Ratio 变换。它是把组成数据(比如状态比例向量)从封闭空间(总和为 1)转换到欧几里得空间的一种方法。
为什么要用这个呢?因为组成数据有一个关键特性: 向量的每一部分都只表示“相对大小”,而不是绝对值,而且所有部分加起来一定等于 1(封闭约束)
这就导致,(1)不能直接使用欧几里得距离,(2)不能用传统的线性方法建模(因为变量之间是依赖的)。于是,clr()
变换诞生了。
然后再用 dist()
计算距离(基于 log-ratio 空间)。
接下来,我们再具体解释一下 Aitchison 空间和欧几里得空间的差别。
1. 什么是 Aitchison 空间?#
它是 组成数据(compositional data) 所处的“几何世界”。
就像欧几里得空间是长度/角度/面积所在的世界,Aitchison 空间是比例、份额、分布 所在的世界。
- 欧几里得空间关注“绝对值”
- Aitchison 空间关注“相对比例”和“比值结构”
2. Aitchison 空间的历史发展#
- 1980 年代,苏格兰统计学家 John Aitchison 发表里程碑著作 “The Statistical Analysis of Compositional Data” (1986)
- 他提出:
- 组成数据不能用普通线性方法处理
- 发明了 log-ratio 变换(CLR, ILR, ALR)
- 创立了一个新的数学空间,后来被称为 Aitchison simplex / space
他强调:
“In compositional data, the relevant information is contained in the ratios between parts, not their absolute values.”
也就是说,“30%就业 vs 60%就业”本身没什么意义,只有“就业 : 家务” 的比例才有意义。
3. 具体数学区别#
特征/操作 | 欧几里得空间 | Aitchison 空间(Simplex ) |
---|---|---|
点 | 任意实数向量 | 所有分量非负且总和为 1 的向量 |
封闭性 | 不封闭(加起来不限制) | 封闭: |
加法 | 元素逐项相加 | 闭合加法(需要保持总和为 1) |
距离 | 欧几里得距离 | Aitchison 距离(基于 log-ratio) |
标准变换 | 无需特别变换 | 使用 log-ratio 变换(如 CLR、ILR) |
内积/角度 | 普通向量点积 | 特定定义方式(涉及对数和几何均值) |
举个简单对比:
欧几里得距离:
x = [0.6, 0.3, 0.1]y = [0.5, 0.4, 0.1]sqrt((x1 - y1)^2 + (x2 - y2)^2 + (x3 - y3)^2)
Aitchison 距离(简化版):
x' = clr(x) = log(x) - mean(log(x))y' = clr(y) = log(y) - mean(log(y))distance = sqrt(sum((x' - y')^2))
你看,用了 log-ratio 变换后的向量来计算距离,才能反映出“比例结构”的差异。
4. 为什么 Aitchison 空间重要?#
它在以下领域是基础工具:
- 地质学:土壤样本中的矿物比例
- 营养学:食物中成分比例
- 微生物组:菌群丰度
- 社会科学:时间分配 / 状态占比(就业、家务、休闲)
- 市场分析:用户时间、预算、关注度分布
那为什么我们还经常用欧几里得? 因为欧几里得:
- 简单直观
- 在某些非封闭、弱组成场景中近似可用
- 很多算法只支持欧几里得空间(MDS, tSNE)
但如果你真正要处理 “比例型数据”,Aitchison 空间才是理论上正确的分析空间。
总结对比表:
对比项 | 欧几里得空间 | Aitchison 空间(组成数据专属) |
---|---|---|
适用对象 | 一般实数向量 | 比例型、封闭型组成向量 |
距离类型 | 欧几里得距离 | Aitchison 距离(基于 log-ratio) |
适用领域 | 数值/位置/图形/序列 | 成分/比例/占比 |
是否考虑比例结构 | 否 | 完全保留比例结构信息 |