Skip to content

主流的编辑距离算法之外的社会距离计算 2.2:欧几里得距离的应用

· 58 min

前言#

在社会序列分析中,我们常常希望比较不同个体的“人生轨迹”有多相似。传统上,这类比较多采用编辑距离(如 Optimal Matching, OM),通过模拟状态的插入、删除、替换来衡量两个序列的“变形成本”。

但还有一种更直接、更结构取向的方式:我们可以先不关心状态发生的顺序,而是关心这些状态分别出现了多少次、在哪些时间段占据了多大比例。

于是,我们就可以采取一种更“分布导向”的计算方式,包含欧几里得距离法和卡方距离法。这一次,我们讲欧几里得距离法。

其核心思想是:把每一条状态序列,划分成若干时间段,每段转为一个“状态分布向量”,然后将这些段的分布拼接成一个超长向量,用欧几里得距离来度量个体间的差异。

换句话说,我们不是把整个轨迹看成一个流程(像 OM 那样),而是把它拆解成结构性片段,再把这些片段还原成向量,用最基本的欧几里得空间距离来捕捉两个轨迹在结构构成上的不同。

下面是这个计算方法的完整逻辑链条:

欧几里得序列距离计算方法的核心流程,总而言之:切段 ➜ 转向量 ➜ 拼接 ➜ 计算距离

  1. 时间段切分:例如按固定步长切成 2、3、4 段;
  2. 每段生成状态频率向量:每段中“就业/失业/家务”等状态出现的比例;
  3. 将所有段拼接:得到一个超长向量;
  4. 两两个体之间计算欧几里得距离:也就是计算向量空间中的直线距离
  5. (可选)归一化:如果 `norm = TRUE`,最后除以段数开根号(控制不同分段数带来的偏差)

目录#

一、OM vs OMspell vs 欧几里得距离计算:分析社会序列的差异,有不同的思路#

在社会序列分析中,最常见的距离度量方式,莫过于 Optimal Matching(OM)。它的核心理念是:把两条完整的状态序列视为“字符串”(就像是生物信息学家将DNA看成字符串进行分析一样),通过插入、删除、替换等操作,将一个变成另一个,计算所需的“编辑成本”。

因此,OM 是一种“顺序敏感”的距离算法:它不仅关注状态的“有无”,更关注它们出现的时间长短(duration)顺序逻辑(order/sequencing)

欧几里得距离(Euclidean Distance)则完全不同。它采取的是一种“顺序无关”的、结构取向的路径比较方式。每一条状态序列,会先被转换为一个状态分布向量,也就是问下面这个问题:

在整条轨迹中,每种状态分别占了多少比例?

这就意味着,原本的序列:

[就业, 就业, 失业, 家务, 就业, 就业, 失业, 就业, 家务, 就业]

会被压缩成:

[就业: 0.6, 失业: 0.2, 家务: 0.2]

这类向量就像是个体社会经历的压缩表达:它不再关心时间顺序,但保留了参与状态的频率结构

然后我们再用欧几里得距离,来计算得出不同个体之间这些向量的差异,就能形成一个距离矩阵(distance matrix)。因此,我们可以看到,虽然和OM一样都是最后得到距离矩阵,但是二者思路是很不一样的。

为什么这种“转向向量”的方法重要?#

这种方法的核心理念是:

OMspell、OMloc 等“混合范式”的中间地带#

在 TraMineR 的扩展方法中,也有不少方法并非传统 OM:

例如 OMspell

这种方式,其实是一种“带顺序信息的结构压缩”,介于纯粹编辑距离(OM)和纯向量距离(欧几里得)之间。

那我们可以怎么理解这些算法的差异?

方法类型顺序是否重要比较单位典型代表思维方式
编辑距离型状态(逐个比对)OM、OMloc、LCS把序列当作“过程”来看
分布距离型状态比例向量欧几里得距离、CHI²把序列当作“构成”来看
Spell-based某种程度上spell 段落OMspell、OMtspell把序列当作“段落集合”来看

总结一句话:

OM 是时间的算法,欧几里得是结构的算法,OMspell 是段落的算法。

在社会科学中,我们可能在不同分析阶段使用不同算法:

而这些路径距离之间的选择差异,其实正体现了你研究问题的侧重点:

二、压缩完每个序列成为状态向量之外,我也可以用其他的距离范数,为什么一定要用 L2 距离?#

既然我们已经把每条序列压缩成了一个“状态分布向量”,那下一步的问题自然就是:

简而言之,我们之所以用 L2,不是因为它唯一,而是因为它在解释力、数学友好度、算法兼容性之间做到了最好的平衡

它就像社会科学里的中性选项,不特别强调哪一维、不特别保护异常值、不过度扁平一切。

1. L2 是最“直观”的距离:#

当你把一个人的状态序列转换成向量,比如:

L2 就是在这个向量空间中计算它们之间的直线距离:

DL2(A,B)=(0.60.3)2+(0.30.6)2+(0.10.1)2=0.18D_{L2}(A, B) = \sqrt{(0.6 - 0.3)^2 + (0.3 - 0.6)^2 + (0.1 - 0.1)^2} = \sqrt{0.18}

就像是我们上一篇教程里说的,它符合人类最本能的“空间距离直觉”。

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只比较是否出现过忽略次数/比例适合稀疏状态、高维分类(如标签)

如果你愿意加入更多分析深度,还可以引出这样的问题:

例子说明: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 欧几里得距离#

L2(x,y)=i(xiyi)2L_2(x, y) = \sqrt{\sum_i (x_i - y_i)^2}
与 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 曼哈顿距离#

L1(x,y)=ixiyiL_1(x, y) = \sum_i |x_i - y_i|
与 A 的距离
B0.1 + −0.1 + 0 = 0.2
C0.6 + −0.5 + −0.1 = 1.2
D0.5 + 0.4 + 0.1 = 1.0
E0.3 + 0.2 + 0.1 = 0.6

排序:B < E < D < C(排序一致)

3. L∞(切比雪夫距离)#

L(x,y)=maxixiyiL_\infty(x, y) = \max_i |x_i - y_i|
与 A 的距离
Bmax(0.1, 0.1, 0) = 0.1
Cmax(0.6, 0.5, 0.1) = 0.6
Dmax(0.5, 0.4, 0.1) = 0.5
Emax(0.3, 0.2, 0.1) = 0.3

排序:B < E < D < C(排序一致)

聚类结果可能差异

结果对比表

范数类型强调点离 A 最近离 A 最远
L2整体结构差异BC
L1总体偏差总量BC
L∞最坏维度差异BC

无论哪种范数,B 都是离 A 最近的,但不同范数强调的“远离逻辑”不同。


三、在社会序列分析中的用法:TraMineR 的 EUCLID 计算距离#

使用 TraMineR 包中的 seqdist() 函数,我们就会发现,在社会序列分析中,我们其实还有两个与“窗口”有关的概念:stepoverlap

stepoverlap 的默认值#

step#

TraMineRseqdist() 和内部的 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 = 4
overlap = FALSE

那么 TraMineR 会把序列划成 不重叠的段

(注意:第二段短于 step=4,会有警告)

于是 TraMineR 会发出如下警告(在 CHI2() 源码中):

msg.warn("With step=", step, " last episode is shorter than the others")

为什么要警告?

因为这种“不等长分段”会造成每段的权重不一致

对于距离计算,可能导致:

项目影响说明
欧几里得距离后一段贡献的分布差异“相对减小”
卡方距离分母(频率)更小,结果可能变大或不稳定
标准化(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 = 4
overlap = TRUE

→ 会生成重叠段,例如:[1-4], [3-6]


关于警告的这块说完了,我们又回到主逻辑里继续梳理。

如果你设置的是:

step = 4
overlap = TRUE

TraMineR 会改为用 滑动窗口方式划段

它像滑窗一样,从前面移动,每次只滑一半(step/2),这样后续段可以捕捉前一段的延续性变化

为什么有时候要用 overlap = TRUE

但代价是:

具体这两个参数是什么意思?#

说先要说明,我们之前算的是没问题的,那些是理论上全局的欧几里得距离:

L2(x,y)=(x1y1)2+(x2y2)2++(xnyn)2L_2(x, y) = \sqrt{(x_1 - y_1)^2 + (x_2 - y_2)^2 + \dots + (x_n - y_n)^2}

这个例子是每个人有一个“整体”状态分布向量(比如一生 70% 就业、20% 失业、10% 家务),这就像只在一个阶段计算的状态分布,那么就直接比较两个向量的欧几里得距离,是没问题的。

那 TraMineR 为啥要加个 stepoverlap 搞得这么复杂?因为 这个算法在社会序列分析的应用中,不直接对全局分布向量比较,而是把一个人的整个序列按时间“切成几段”,每段计算一个状态分布向量,然后在这些段上分别做距离比较,再平均/合并起来。

举个例子更形象:

假设 A 的时间序列是 12 步长(比如 12 年):

A: 就业 就 就 就 就 家 家 家 失 失 失 失

现在如果 step = 4,那么 A 会被切成 3 段:

  1. 第 1 段(1-4):就 就 就 就 → [1, 0, 0]
  2. 第 2 段(5-8):就 家 家 家 → [0.25, 0, 0.75]
  3. 第 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(完全相同的总体比例,但顺序不同):

前期失业 → 中期就业 → 后期家务
[失 失 失 失 就 就 就 就 家 家 家 家]

两人的总体状态分布是一样的:

如果你只用总体状态分布来算欧几里得距离?那么距离 = 0(因为状态分布完全一样)。 但这显然 忽略了“时间顺序的巨大差异”。一个人前期就业,一个人前期失业,这在社会科学中可能代表完全不同的生活轨迹和含义

那么这个算法是怎么补救这个问题的? “我们不能只看状态总量,还要看这些状态是什么时候发生的

于是它:

  1. 将时间序列按时间段分段
  2. 每段都单独计算状态分布(一个小向量)
  3. 对每段之间的状态分布向量做欧几里得距离比较
  4. 最后将这些段落距离累加或平均

这样就能回答下面的这些问题:

用表格来对比的话:

方法优点缺点
总体分布距离简单快速、清晰无法识别时间结构变化
按段分布+比较(TraMineR)能反映状态的时间动态、变化路径稍复杂,依赖 step 参数

也就是说:

欧几里得社会序列距离算法之所以逐段比较状态分布,而不是只算总体,是为了保留序列的“时间动态特征”。
这样才能更真实地度量社会轨迹中“什么时候发生了什么”的差异。

注意欧几里得距离的“弱顺序意识”#

在之前的教程里,我有稍微提到欧几里得距离是没有什么顺序意识的,因为如果我们算状态总体分布的话,就没有顺序可言了。

也就是说,这个评价主要指的是,在 默认的(较大 step)设置或总体分布方式下,欧几里得只关注了状态“比例”,完全忽略了状态发生的时间顺序

比如:

在这些情况下,状态位置完全被抹平,所以才说它“忽略顺序”。

但因为我们在这个教程里学到了更多细化的东西,我们在这里可以更加精确地理解这个算法:

欧几里得距离本身不考虑状态发生的先后顺序,但在设置 step = 1 时,由于每个时间点都独立比较,它“间接地”表现出对顺序的敏感性。

换句话来说,当 step = 1 时,欧几里得确实“部分”捕捉了状态顺序的效果。

因为这时候的逻辑是:

举个例子,我们来对比两个序列:

序列长度 5AB
[就,就,就,失,失][失,失,失,就,就]

两个序列的总体状态分布是一样的(就=3,失=2),总体分布距离为 0。

step=1 时,我们需要针对每个时间点进行对比:

于是,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 的 indelsm#

论文中的解释在第 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)分段后看分布差异综合考虑时间与比例
推荐经验值:

  1. K = 1:只想知道整体暴露差异(如失业总时长是否不同)
  2. K = ncol(seqdata)step = 1:想捕捉细致的时间变化,就是每个时间点都会作为一个窗口,等效于 Hamming 风格
  3. K = 4~6:一般用于时间跨度比较长的生命历程,一年一个阶段太细,两年一个太粗时的折中
  4. 建议先尝试 K = 5(论文中反复使用的值)
  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 的:

举个例子,假设你有一个 12 步长的序列:

所以论文说的:“We consider CHI2 with K=1, 5, 10, 20…”

在 R 包中就是:

seqdist(..., method="EUCLID", step = 12) # 相当于 K = 1
seqdist(..., method="EUCLID", step = 2) # 相当于 K = 6
seqdist(..., method="EUCLID", step = 1) # 相当于 K = 12

欧几里得距离在社会序列分析中的利与弊#

优点描述
快速计算简单,可扩展到上万条轨迹
易解释距离越小,分布越相似
兼容主流算法可用于 MDS、聚类、tSNE
稳定不依赖于参数设置
局限描述
弱顺序意识默认按整体分布比较,忽略“状态顺序”;但在设置 step = 1 时,能够间接捕捉状态序列的时间结构(类似 Hamming 距离)
无语义意识视“就业”和“照顾家庭”为平行维度
无结构敏感性不考虑某些状态的稀有性或重要性

六、TraMineR 与欧几里得相关的代码逻辑梳理#

TraMineR 在欧几里得距离计算的函数实现上有一些复杂,但关键还是看下面的三个文件:

核心结构概览#

TraMineR 中的 seqdist() 是一个统一的距离计算接口,支持多种方法,包括

它本质上做三件事:

  1. 参数准备与校验:检查合法性,补齐默认值
  2. 方法准备:对 indel/sm/refseq 等参数做预处理,并生成最终需要送入 C++ 核心函数的参数结构
  3. 执行计算:调用内置的 C/C++ 核心函数(或者是纯 R 实现,但比较少)完成矩阵输出

seqdist 函数的整体流程和逻辑顺序#

  1. 输入校验:确认输入的是序列对象 seqdata,不是矩阵或字符;确认方法名合法(如 EUCLID)
  2. 设置默认值和参数预处理:如 norm="auto"indel="auto"、是否带缺失值等
  3. 特殊变量准备:如 sm(substitution matrix)、refseq(参考序列)等
  4. 转换输入序列为数值形式seqnum(seqdata)
  5. 按方法不同选择不同处理:
  1. 返回结果:可选输出为 pairwise 距离矩阵或参考序列的距离向量(前者占绝大多数)

为什么 EUCLID 和 CHI2 被放一起?#

CHI2() 是一个通用的“分布向量比较器”:

它们共用逻辑的原因是:只有最后的计算公式不同,其余都是一样的频率矩阵操作。因此,它们被纳入到了同样一个文件里面。

CHI2()/EUCLID() 函数逻辑详细步骤#

  1. 构建 breaks:按 step 划分时间段
  2. 对每段生成频率矩阵:统计每条序列在该段内各状态出现频率
  3. 构建比较矩阵(dummies)
  4. 选择距离计算方式
  1. 处理参考序列(refseq) 或生成所有 pairwise 距离
  2. 规范化(norm)除以段数平方根(可选)

作为刚上手的开发者,可以先只实现 step=1, overlap=False, norm=False 的最简单版本,然后再逐步支持完整参数。

术语小词典#

术语含义
seqdata输入的状态序列数据,类似于二维数组(行=个体)
step每个窗口的时间长度,控制分段粒度
breaksstep 划分得到的时间段索引
dummies每段内状态的 one-hot 分布向量
refseq用于计算相似度的参考序列
norm是否对最终距离进行标准化
indel编辑距离中的插入删除成本,仅 OM 系列使用
sm状态替换成本矩阵(substitution matrix)
.Call()R 调用 C++ 接口的函数,Python 可用 Cython 对应实现

关于 dummiesnorm,我们还需要加深理解:

什么是 “构建比较矩阵(dummies)”?#

这个术语是 TraMineR 源代码里的命名,在 CHI2() 函数内部有一个 dummies(b) 函数,它的作用可以理解成把每一段时间区间内的状态序列,转成“状态分布向量”

示例:2 个个体,状态集为 [E, U, H]step = 2,序列长度为 4

假设我们有如下原始状态序列:

个体原始序列
AE, E, H, H
BE, U, U, H

将它们按step = 2划分为两个时间段:

个体EUH
A11.00.00.0
A20.00.01.0
B10.50.50.0
B20.00.50.5

也就是说

这个矩阵会被“平铺”为一个大矩阵(例如 [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)呢?

公式:

D(x,y)=i=1n(xiyi)2D(x, y) = \sqrt{ \sum_{i=1}^{n} (x_i - y_i)^2 }

对 A 和 B:

计算每一维的差值平方:

维度AB差值平方
11.00.50.50.25
20.00.5-0.50.25
30.00.000
40.00.000
50.00.5-0.50.25
61.00.50.50.25

总和:

0.25+0.25+0+0+0.25+0.25=1.00.25 + 0.25 + 0 + 0 + 0.25 + 0.25 = 1.0

最终距离:

1.0=1.0\sqrt{1.0} = 1.0

注意:

虽然叫 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 来归一化?否则会有什么“偏差”?

先说重点结论:因为每多一段,你就多拼接了一个向量段,整个向量维度变长,距离值也会“机械性变大”;不是因为人更不一样了,而是因为“多算了几段”。

举个例子:

同样两个人的状态比例差异在每段都一样,但因为你算了 4 段 vs 2 段,最终 L2 距离会是原来的 2 倍(平方和变成 4 个相加,而不是 2 个相加):

L2_总=k=1nL2_每段k2n\text{L2\_总} = \sqrt{\sum_{k=1}^{n} \text{L2\_每段}_k^2} \propto \sqrt{n}

那“norm = TRUE”做了什么? 它本质上就是在最后一步把总距离除以段数的平方根

distance = sqrt( sum of squared segment differences ) / sqrt(#segments)

换句话说:它让“距离值”只反映个体之间的真实差异,而不受“你分几段”这个人为操作的影响。

那这为什么会带来误差或偏差?

如果你没做归一化:

换句话说,不归一化相当于“惩罚了你切得多的人”,而这并没有统计学上的合理性。

所以,norm = TRUE 是一个“算法公平性校正”机制

分析目标是否建议 norm = TRUE
聚类分析(对比不同人)必须归一化,避免段数影响
可视化(MDS, t-SNE)否则距离受段数膨胀影响
对比不同切分方式结果确保基准一致
分析某人内部变化(不同段)可不归一化,看绝对段差

在使用状态分布的距离计算算法中,大多数情况下都建议归一化(norm = TRUE),因为只要你在比较多个个体之间的差异(比如做聚类、分类、相似度排序),你就希望:

总结一句话:分段越多,距离“膨胀”越严重,不是因为人更不一样了,而是“算的维度多了”。所以要用 norm = TRUE 来做“按段数归一化”,让距离反映的是“每段平均差异”,而不是“总段数乘以差异”。

具体在其他类别的序列分析距离计算中,比如在使用编辑距离(比如 OM)的时候,还需要归一化吗?

我们会在之后的教程里单开出来一篇教程,好好讲解。


加餐:Aitchison 空间 vs 普通欧几里得空间#

当我们说一个序列的状态分布像 [0.7, 0.2, 0.1](就业、失业、家务)
这其实就是一个典型的 组成向量(composition)

这个结构和统计中的 Dirichlet 分布模型Aitchison 几何空间 是一样的。但是,我们这里主要用的还是欧几里得空间,下面我们就仔细说说,社会序列分析 vs 组成数据分析(Compositional Data Analysis, CoDA)之间的理论分野。

组成数据的世界(Beta / Dirichlet)#

模型通常用于什么核心特征
Beta两类组成比例(比如男/女,成功/失败)分布在 [0,1] 区间
Dirichlet多类组成比例(3个及以上类别)广义多元版的 Beta,建模一个 composition
CoDA 分析分析多个组成变量之间的结构关系用的不是欧几里得空间,而是Aitchison 几何

那为啥这些模型中“没有欧几里得距离”?#

其实不是没有,而是在组成数据分析(CoDA)里,欧几里得距离不适用

为什么?

因为组成数据遵循一个叫“封闭约束”(closure)的条件:

CoDA 中用什么代替欧几里得?#

使用 Aitchison 距离 或基于 log-ratio(对数比值)变换 的方法:

这些方法可以:

那为什么欧几里得能用在社会序列分析的距离对比中?#

因为社会序列分析中,我们并没有严格把序列看成是“组成数据”,而是把状态分布向量当作普通向量,拿来算欧几里得距离作为一种“粗粒度的相似性度量”。

它不考虑封闭约束、对数比率、不遵循 Aitchison 几何空间。它关注的是:“A 的状态占比和 B 的状态占比,有多不一样?”

所以它能快速、直观,但不是组成数据严格意义下的分析

总结对比表:

维度TraMineR::EUCLIDCoDA / Dirichlet Models
是否 composition✅ yes(视为状态比例)✅ yes(严格封闭向量)
用什么度量欧几里得距离Aitchison 距离 / log-ratio
是否考虑变换❌ 否✅ 对数变换如 CLR / ILR
顺序信息可选(step=1 可捕捉)❌ 纯静态分布,无时间概念
用途社会序列相似性,聚类建模行为偏好,成分关系等

如果你特别在意“比例结构”本身的相对性,比如:

那你可以试试 CoDaPackrobCompositionscompositions 包中的:

clr(X)

clr(X) 是 Centered Log-Ratio 变换。它是把组成数据(比如状态比例向量)从封闭空间(总和为 1)转换到欧几里得空间的一种方法。

为什么要用这个呢?因为组成数据有一个关键特性: 向量的每一部分都只表示“相对大小”,而不是绝对值,而且所有部分加起来一定等于 1(封闭约束)

这就导致,(1)不能直接使用欧几里得距离,(2)不能用传统的线性方法建模(因为变量之间是依赖的)。于是,clr() 变换诞生了。

然后再用 dist() 计算距离(基于 log-ratio 空间)。

接下来,我们再具体解释一下 Aitchison 空间和欧几里得空间的差别。

1. 什么是 Aitchison 空间?#

它是 组成数据(compositional data) 所处的“几何世界”。

就像欧几里得空间是长度/角度/面积所在的世界,Aitchison 空间是比例、份额、分布 所在的世界。

2. Aitchison 空间的历史发展#

他强调:

“In compositional data, the relevant information is contained in the ratios between parts, not their absolute values.”

也就是说,“30%就业 vs 60%就业”本身没什么意义,只有“就业 : 家务” 的比例才有意义。

3. 具体数学区别#

特征/操作欧几里得空间 Rn\mathbb{R}^nAitchison 空间(Simplex SD\mathcal{S}^D
任意实数向量所有分量非负且总和为 1 的向量
封闭性不封闭(加起来不限制)封闭:xi=1\sum x_i = 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 空间重要?#

它在以下领域是基础工具:

那为什么我们还经常用欧几里得? 因为欧几里得:

但如果你真正要处理 “比例型数据”,Aitchison 空间才是理论上正确的分析空间。

总结对比表:

对比项欧几里得空间Aitchison 空间(组成数据专属)
适用对象一般实数向量比例型、封闭型组成向量
距离类型欧几里得距离Aitchison 距离(基于 log-ratio)
适用领域数值/位置/图形/序列成分/比例/占比
是否考虑比例结构完全保留比例结构信息