基于公共子序列的轨迹聚类(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 ...
随机推荐
- codeforces 816 E. Karen and Supermarket(树形dp)
题目链接:http://codeforces.com/contest/816/problem/E 题意:有n件商品,每件有价格ci,优惠券di,对于i>=2,使用di的条件为:xi的优惠券需要被 ...
- Codeforces Round #381 (Div. 2) C. Alyona and mex(无语)
题目链接 http://codeforces.com/contest/740/problem/C 题意:有一串数字,给你m个区间求每一个区间内不含有的最小的数,输出全部中最小的那个尽量使得这个最小值最 ...
- C、C++格式化字符串
引言 在C和C++开发中,我们经常会用到printf来进行字符串的格式化,例如printf("format string %d, %d", 1, 2);,这样的格式化只是用于打印调 ...
- 【Redis】缓存穿透与缓存雪崩
一.缓存雪崩 1.1 缓存雪崩产生的原因 1.2 解决方案 1.3 锁的方式 1.4 消息中间件 1.5 一级和二级缓存 1.6 均摊分配redis key 失效时间 二.缓存穿透 一.缓存雪崩 1. ...
- 深入Go的错误处理机制使用
开篇词 程序运行过程中不可避免的发生各种错误,要想让自己的程序保持较高的健壮性,那么异常,错误处理是需要考虑周全的,每个编程语言提供了一套自己的异常错误处理机制,在Go中,你知道了吗?接下来我们一起看 ...
- 062 Python必备库-从Web解析到网络空间
目录 一.概述 二.Python库之网络爬虫 2.1 Requests 2.2 Scrapy 2.3 pyspider 三.Python库之Web信息提取 3.1 Beautiful Soup 3.2 ...
- git修改user.name 和user.email
今天刚刚入门了下git,,然后初始化的时候将用户名弄错了...就很气啊.然后网上找了半天都找不出一个可靠的修改全局用户名和邮箱的方法.. 最后还是自己摸索出来了..其实也很简单. 首先进入gi ...
- group by语法
group by 用法解析 group by语法可以根据给定数据列的每个成员对查询结果进行分组统计,最终得到一个分组汇总表. SELECT子句中的列名必须为分组列或列函数.列函数对于GROUP BY子 ...
- Hbase 统计表行数的3种方式总结
有些时候需要我们去统计某一个Hbase表的行数,由于hbase本身不支持SQL语言,只能通过其他方式实现.可以通过一下几种方式实现hbase表的行数统计工作: 1.count命令 最直接的方式是在hb ...
- 磁盘告警之---神奇的魔法(Sparse file)
一.问题来源 半夜钉钉接到告警,某台机器的磁盘使用率少于20%,于是迷糊中爬起来,咔咔咔 find / -size +1G,咔咔咔,把几个只有4-5G的日志文件echo空值了一下,然后吓蒙了,刚刚 ...