基于公共子序列的轨迹聚类(c#)
前言
如今的世界中,想要研究人们的出行活动,可以利用智能手机或智能手环等个人设备生成的 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。注意,兄弟姐妹相似度总是比后代相似度的值要大。根据这两个阈值,我们可以进行轨迹族谱的生成。如果相似度rateIbf
和rateJaf
都大于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#)的更多相关文章
- 基于DP的LCS(最长公共子序列)问题
最长公共子序列,即给出两个序列,给出最长的公共序列,例如: 序列1 understand 序列2 underground 最长公共序列undernd,长度为7 一般这类问题很适合使用动态规划,其动态规 ...
- LCSS最长公共子序列算法
0.论文基本介绍以及相关内容 分析移动用户位置的相似性,提取移动用户的相似路径在出行路径预测.兴趣区域发现.轨迹聚类.个性化路径推荐等领域具有广泛的应用. 重点:利用移动用户定位数据找到合适轨迹的表示 ...
- [Data Structure] LCSs——最长公共子序列和最长公共子串
1. 什么是 LCSs? 什么是 LCSs? 好多博友看到这几个字母可能比较困惑,因为这是我自己对两个常见问题的统称,它们分别为最长公共子序列问题(Longest-Common-Subsequence ...
- ACM/ICPC 之 最长公共子序列计数及其回溯算法(51Nod-1006(最长公共子序列))
这道题被51Nod定为基础题(这要求有点高啊),我感觉应该可以算作一级或者二级题目,主要原因不是动态规划的状态转移方程的问题,而是需要理解最后的回溯算法. 题目大意:找到两个字符串中最长的子序列,子序 ...
- 最长公共子序列(LCS问题)
先简单介绍下什么是最长公共子序列问题,其实问题很直白,假设两个序列X,Y,X的值是ACBDDCB,Y的值是BBDC,那么XY的最长公共子序列就是BDC.这里解决的问题就是需要一种算法可以快速的计算出这 ...
- C++版 - Lintcode 77-Longest Common Subsequence最长公共子序列(LCS) - 题解
版权声明:本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C++版 - L ...
- 【ZH奶酪】如何用Python计算最长公共子序列和最长公共子串
1. 什么是最长公共子序列?什么是最长公共子串? 1.1. 最长公共子序列(Longest-Common-Subsequences,LCS) 最长公共子序列(Longest-Common-Subseq ...
- 用Python计算最长公共子序列和最长公共子串
如何用Python计算最长公共子序列和最长公共子串 1. 什么是最长公共子序列?什么是最长公共子串? 1.1. 最长公共子序列(Longest-Common-Subsequences,LCS) 最长公 ...
- HDU 1243 反恐训练营 (动态规划求最长公共子序列)
反恐训练营 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Subm ...
随机推荐
- POJ 1390 Blocks (区间DP) 题解
题意 t组数据,每组数据有n个方块,给出它们的颜色,每次消去的得分为相同颜色块个数的平方(要求连续),求最大得分. 首先看到这题我们发现我们要把大块尽可能放在一起才会有最大收益,我们要将相同颜色块合在 ...
- cve_2019_0708_bluekeep复现采坑
0X01 简介 Microsoft Windows是美国微软公司发布的视窗操作系统.远程桌面连接是微软从Windows 2000 Server开始提供的功能组件. 2019年5月14日,微软发布了月度 ...
- 【Offer】[35] 【复杂链表的复制】
题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的he ...
- SpringCloud学习笔记(3):使用Feign实现声明式服务调用
简介 Feign是一个声明式的Web Service客户端,它简化了Web服务客户端的编写操作,相对于Ribbon+RestTemplate的方式,开发者只需通过简单的接口和注解来调用HTTP API ...
- 010 深入理解Python语言
目录 一.概述 二.计算机技术的演进 2.1 计算机技术的演进过程 三.编程语言的多样初心 3.1 编程语言有哪些? 3.2 不同编程语言的初心和适用对象 3.3 2018年以后的计算环境- 四.Py ...
- hadoop2.7.3启动报错问题
在日志里面可以看出是没有对应的权限,因此在hdfs_site.xml中添加 <property> <name>dfs.permissions</name> < ...
- 我的JS文件
{ "squadName" : "林宥嘉", "homeTown" : "1987年7月1日(农历六月初六)", ...
- 学生管理系统——ArrayList集合
此学生管理系统利用了集合ArrayList实现了对学生的增删改查:利用数组中的方法实现分数排序:运用String类的equals实现登录功能的字符串比较. 管理员类: package data; pu ...
- Spring Boot 入门之单元测试篇(五)
博客地址:http://www.moonxy.com 一.前言 JUnit 是一个由 Java 语言编写的开源的回归测试(回归测试是指重复以前全部或部分的相同测试)框架,由Erich Gamma 和 ...
- C# 10分钟入门基于WebOffice实现在线编辑文档,实时保存到服务器(所有office,兼容WPS)
今天,他来了(weboffice在线编辑文档). 上次写了一个在线预览的博,当然,效果并不是太理想,但是紧急解决了当时的问题. 后来,小编重新查找资料,求助大牛,终于使用新的方式替换了之前的low方法 ...