前言

如今的世界中,想要研究人们的出行活动,可以利用智能手机或智能手环等个人设备生成的 GPS 数据进行研究。而在众多的研究方向中,出行的热点路线或者说经常出行的路线也比较受欢迎。采用热力图的方式对其进行研究具有许多优点。热力图给使用者的感觉就是特别直观,一眼便看出来哪些路径属于热迹(我们把热点路线,也就是重复度高的路线称为热迹)。如下图所示:

(图片来自网上,侵删)

从图中我们一眼便能够找出两条粗壮的热迹。这表示有某种物体经常沿着两条路线运动。但对于计算机来说,要从图中找出这两条热迹,并加以区分形成两条完整的路线可不是一件容易的事。所以我们只能直接从轨迹上入手。接下来将介绍利用公共子序列进行轨迹聚类的方法。

该聚类方法的核心思想是相似的轨迹在地理空间中占有的位置基本一致,轨迹越相似其共有的位置占原轨迹空间的比重越大,并且随着我们划分轨迹的精度降低而提高。

数据准备

Gps 数据准备

研究轨迹聚类,最基本的要求就是拥有大量的轨迹数据。幸运的,我从网上的项目中找到了公开的 Gps 数据。为什么说是幸运的?在此之前,我曾写过一个 App,以期收集自己的出行轨迹进行研究。该应用的确达到了预期,但困难的是手机要开启 Gps 才能得到比较精准的轨迹数据,这显然提高了手机电量的要求。除此之外,收集众多的轨迹需要大量时间、上班族轨迹相对固定等一系列因素导致收集自己的轨迹计划夭折。而在网上找到的数据完全满足研究要求。这些数据来自于微软亚洲研究院的项目 Geolife,是每个人都能够获取的。该数据以 plt 为文件后缀存储,但实际上就是普通的文本文件,并且除了前六行外就是 csv 格式的。

轨迹数据模型类

这是本项目中用到的数据类模型

//轨迹数据点通用模型(以接口定义)
public interface IGeoInfoModel
{
float Latitude { get; set; }
float Longitude { get; set; }
} //轨迹类
public class Trajectory
{
public Guid Id { get; }
public Trajectory()
{
Id = Guid.NewGuid();
}
public string Name { get; set; }
public List<IGeoInfoModel> GeoPoints { get; set; }//轨迹点集
public List<string> GeoCodes { get; set; }//轨迹编码集
public Trajectory Parent { get; set; }//轨迹的父亲
//轨迹的兄弟姐妹,该集合中的轨迹与该轨迹相似度极高(线程安全集合)
public ConcurrentBag<Trajectory> Siblings { get; } = new ConcurrentBag<Trajectory>();
//轨迹的后辈,该集合中的轨迹与该轨迹具有一定相似性,但低于兄弟姐妹(线程安全集合)
public ConcurrentBag<Trajectory> Children { get; } = new ConcurrentBag<Trajectory>();
public float MinLat => GeoPoints.Min(t => t.Latitude);
public float MinLon => GeoPoints.Min(t => t.Longitude);
public float MaxLat => GeoPoints.Max(t => t.Latitude);
public float MaxLon => GeoPoints.Max(t => t.Longitude);
public int Level { get; set; }//轨迹在族谱中的代数/层数
public string LevelTag { get; set; }//代数标签,用于打印
}

轨迹序列产生(轨迹的低分辨率/低精度描述)

要找轨迹间的公共子序列,首先得有可以描述轨迹的序列的方法。该序列具有比原始轨迹低的精度,但基本可以描述一条轨迹的空间位置。下面介绍两种该项目中用到的编码方法。

保留高位小数法

最简单的就是仅保留经纬度的高位小数来进行编码。这种方式的产生的数据的精度调节有限,但也能满足一般需求。以坐标(1.2222,33.44444)来说保留三位小数后可得“1.222_33.444”,中间添加下划线是保留反解码的需求。

var digits = 3;
var dig = $"F{digits}";
var code = $"{t.Latitude.ToString(dig)}_{t.Longitude.ToString(dig)}"

GeoHash 法

GeoHash 是公认的计算经纬度编码的有效方法,并且精度调节能力较强。读者可以从这里进行了解Geohash 精度和原理。除此之外,GeoHash 可以得到较短的字符串,还是以坐标(1.2222,33.44444)来说,得到 7 位的字符编码“s8pycn3”。该hash码精度和直接取上面取三位小数的方式上精度接近,但字符长却缩减了 5 位。这利于高效的查找公共子序列。在 c#中我们直接 nuget 命令添加 nupkg 包NGeoHash.DotNetCore即可获得 GeoHash 的计算方法。

var code = GeoHash.Encode(1.2222,33.44444, 7);

子序列的最终形式

上述两种方法皆可得到描述轨迹的序列。对于我们的公共子序列聚类来说,我们使用 GeoHash 得到轨迹序列能够得到较高的效率(花费时间基本为保留高位小数法的 60%)。此外需要注意的就是:1.该聚类方法并不关心轨迹点的先后顺序,也就是时间无关性;2.产生的轨迹序列会有重复值,但我们只关心轨迹是否在某一空间,而不关心在某一空间出现的次数,也就是重复无用性。基于以上两点,初步产生的轨迹序列需要去重,这对于查找公共子序列来说也是一种效率优化。

这对于 c#来说是极其简单的:

//并行迭代轨迹获得轨迹序列
Parallel.ForEach(tracks, t =>
{
// t.GeoCodes = GeoHelper.GetGeoCodes(t.GeoPoints, 3).ToList();//保留高位小数法
t.GeoCodes = t.GeoPoints
.Select(tt => GeoHash.Encode(tt.Latitude, tt.Longitude, 7))
.Distinct()
.ToList();//GeoHash法
});

轨迹相似度的计算

在得到每条轨迹的序列基础上,我们可以进行轨迹的相似度计算。而轨迹相似度可以归结为轨迹序列间的公共子序列占原始序列的比率。

//array为轨迹列表,ibf为索引1,jaf为索引2,计算GeoCodes的交集
var intersect = array[ibf].GeoCodes.Intersect(array[jaf].GeoCodes).ToList();
//公共子序列的个数转为浮点型,避免下一步计算为整形时结果直接归零
var intersectCount = (float) intersect.Count;
//公共轨迹(子序列)的与轨迹1的相似度
var rateIbf = intersectCount / array[ibf].GeoCodes.Count;
//公共轨迹(子序列)的与轨迹2的相似度
var rateJaf = intersectCount / array[jaf].GeoCodes.Count;

轨迹族谱的生成

得到轨迹相似度后,我们定义了两个阈值:一个为兄弟姐妹相似度scaleSimilar,一个为后代相似度scaleBlood。注意,兄弟姐妹相似度总是比后代相似度的值要大。根据这两个阈值,我们可以进行轨迹族谱的生成。如果相似度rateIbfrateJaf都大于scaleSimilar,说明两条轨迹极为相似,被定义为兄弟姐妹。如果都大于scaleBlood,并且rateIbf大于rateJaf,则轨迹 2 是 1 的后代,反之 1 是 2 的后代。

以下是这个算法的核心:

//构建轨迹树(族谱)
public IEnumerable<Trajectory> BuildClusterTree(IEnumerable<Trajectory> trajectories, float scaleSimilar = 0.8f,
float scaleBlood = 0.6f)
{
var array = trajectories.OrderByDescending(t => t.GeoCodes.Count).ToArray();
for (var ibf = 0; ibf < array.Length; ibf++)
{
Parallel.For(ibf + 1, array.Length, jaf =>
{
if (array[jaf].Level == 1) return;
var intersect = array[ibf].GeoCodes.Intersect(array[jaf].GeoCodes).ToList();
var intersectCount = (float) intersect.Count;
var rateIbf = intersectCount / array[ibf].GeoCodes.Count;
var rateJaf = intersectCount / array[jaf].GeoCodes.Count;
if (rateIbf >= scaleSimilar && rateJaf >= scaleSimilar)
{
array[jaf].Level = 1;
array[ibf].Siblings.Add(array[jaf]);
array[jaf].Siblings.Add(array[ibf]);
}
else if (rateJaf > rateIbf && rateIbf >= scaleBlood)
{
array[jaf].Level = 1;
array[jaf].Parent = array[ibf];
array[ibf].Children.Add(array[jaf]);
}
else if (rateIbf > rateJaf && rateJaf >= scaleBlood)
{
array[ibf].Level = 1;
array[ibf].Parent = array[jaf];
array[jaf].Children.Add(array[ibf]);
}
});
} var root = array.Where(t => t.Level == 0 ).ToList();
SetClusterTreeLevelInfo(root);
return root;
}
//设置代数及标签信息
private void SetClusterTreeLevelInfo(IEnumerable<Trajectory> tree,int level = 0, string levelTag = "")
{
Parallel.For(0, tree.Count(), i =>
{
var node = tree.ElementAt(i);
node.Level = level;
node.LevelTag = $"{levelTag}{i}";
for (var j = 0; j < node.Siblings.Count; j++)
{
var sib = node.Siblings.ElementAt(j);
sib.Level = level;
sib.LevelTag = $"{node.LevelTag}__{j}_sim";
}
SetClusterTreeLevelInfo(node.Children, level + 1, $"{node.LevelTag}_");
});
}

结果展示及说明

我们直接上最终的轨迹族谱图的展示

读者放大后应该能够看清图片的文件名,文件名是代数标签。以“3.png”和“3_0.png”来说,“3.png”是“3_0.png”的父亲。对应的,后者是前者的孩子(所有轨迹对于都是自适应固定大小的图片画的,所以缩放比例和整体偏移会导致看起来有差别。读者仔细观察能够发现 3_0 只是 3 的左下角一部分)。而对于“3_0_4_8.png”和“3_0_4_8__sim_0.png”来说则互为兄弟姐妹(缩略图与自适应的确影响了结果的查看)。

此外,我们能够看出第 3 和第 6 是两个大家族。如果利用所有轨迹的数据生成热力图,那么 3 族和 6 族所经过的地方热力值相对来说要高很多。把一个大家族的所有成员轨迹叠加后可以得到我们想要的热迹。

总结

该算法如果不用并行计算是一个时间复杂度为:O(n^2)的算法“n(n+1)/2”。而改为并行计算后,效率有很大提升(由于直接采用并行计算,不知道提升了多少)。示例中71条轨迹生成族谱树仅需25毫秒左右,而1988条轨迹需要7.5秒左右。从效率和结果上来说都达到了令人满意的程度。另外,该项目已经上传github,访问请点击QianMo,欢迎大家提出修改意见。

基于公共子序列的轨迹聚类(c#)的更多相关文章

  1. 基于DP的LCS(最长公共子序列)问题

    最长公共子序列,即给出两个序列,给出最长的公共序列,例如: 序列1 understand 序列2 underground 最长公共序列undernd,长度为7 一般这类问题很适合使用动态规划,其动态规 ...

  2. LCSS最长公共子序列算法

    0.论文基本介绍以及相关内容 分析移动用户位置的相似性,提取移动用户的相似路径在出行路径预测.兴趣区域发现.轨迹聚类.个性化路径推荐等领域具有广泛的应用. 重点:利用移动用户定位数据找到合适轨迹的表示 ...

  3. [Data Structure] LCSs——最长公共子序列和最长公共子串

    1. 什么是 LCSs? 什么是 LCSs? 好多博友看到这几个字母可能比较困惑,因为这是我自己对两个常见问题的统称,它们分别为最长公共子序列问题(Longest-Common-Subsequence ...

  4. ACM/ICPC 之 最长公共子序列计数及其回溯算法(51Nod-1006(最长公共子序列))

    这道题被51Nod定为基础题(这要求有点高啊),我感觉应该可以算作一级或者二级题目,主要原因不是动态规划的状态转移方程的问题,而是需要理解最后的回溯算法. 题目大意:找到两个字符串中最长的子序列,子序 ...

  5. 最长公共子序列(LCS问题)

    先简单介绍下什么是最长公共子序列问题,其实问题很直白,假设两个序列X,Y,X的值是ACBDDCB,Y的值是BBDC,那么XY的最长公共子序列就是BDC.这里解决的问题就是需要一种算法可以快速的计算出这 ...

  6. C++版 - Lintcode 77-Longest Common Subsequence最长公共子序列(LCS) - 题解

    版权声明:本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C++版 - L ...

  7. 【ZH奶酪】如何用Python计算最长公共子序列和最长公共子串

    1. 什么是最长公共子序列?什么是最长公共子串? 1.1. 最长公共子序列(Longest-Common-Subsequences,LCS) 最长公共子序列(Longest-Common-Subseq ...

  8. 用Python计算最长公共子序列和最长公共子串

    如何用Python计算最长公共子序列和最长公共子串 1. 什么是最长公共子序列?什么是最长公共子串? 1.1. 最长公共子序列(Longest-Common-Subsequences,LCS) 最长公 ...

  9. HDU 1243 反恐训练营 (动态规划求最长公共子序列)

    反恐训练营 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Subm ...

随机推荐

  1. CF - 652F Ants on a Circle

    题目传送门 题解: 先观察蚂蚁相撞, 可以发现, 如果我们将相撞的2个蚂蚁互换位置的话,蚂蚁相当于没有碰撞体积,直接穿过去了.所以我们可以直接计算出最终哪些位置上会有蚂蚁. 接下来就需要知道蚂蚁们的最 ...

  2. ASP.NET Core结合Nacos来完成配置管理和服务发现

    目录 前言 Nacos的简介 启动Nacos 配置管理 服务发现 写在最后 前言 今年4月份的时候,和平台组的同事一起调研了一下Nacos,也就在那个时候写了.net core版本的非官方版的SDK. ...

  3. Python中字符编码及转码

    python 字符编码及转码 python 默认编码 python 2.X 默认的字符编码是ASCII, 默认的文件编码也是ASCII python 3.X 默认的字符编码是unicode,默认的文件 ...

  4. java架构师视频教程 内含activemq+jvm+netty+dubbo

    目录: 架构师视频教程包含activemq jvm netty dubbo 0分布式项目实战所有视频(分布式项目视频)互联网架构师第二期-视频部分互联网架构师第二期-资料部分1.Netty快速入门教程 ...

  5. Sublime Text 实用方法

    代码比对 安装Sublimerge插件 打开Sublime Text后,接着按Ctrl+Shift+P,并输入Install Package 待其加载完成,再输入Sublimerge Pro 当安装完 ...

  6. cssrelative

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  7. 数据可视化之3D中国

    本文链接:https://blog.csdn.net/zhai_865327/article/details/82983489 其实一般情况下2D平面地图就够用了,但是为了更加美观及突出效果,就需要3 ...

  8. Widget 中的 State 解析

    StatefulWidget 应对有交互.需要动态变化视觉效果的场景 StatelessWidget 则用于处理静态的.无状态的视图展示 那么,StatelessWidget 是否有存在的必要?Sta ...

  9. vscode Springboot 启动debug报错:Build failed, do you want to continue?

    一,前言 vscode我感觉是一个特别好用的开发工具,我根据文章https://www.cnblogs.com/WangBoBlog/p/9464281.html去搭建一个简单的springboot工 ...

  10. 用git提交新项目到github

    1.(先进入项目文件夹)通过命令 git init 把这个目录变成git可以管理的仓库 git init 2.把文件添加到版本库中,使用命令 git add .添加到暂存区里面去,不要忘记后面的小数点 ...