用聚集算法计算轨迹中的滞留区域

  需求:在采集完用户轨迹后,根据轨迹中的坐标点统计滞留区域。并对滞留区域在半径和时间上有要求,即多长时间以内不计算在内,聚集的半径小于多少。
  说明:这里规定聚集半径上限为ClusterRadius、聚集时间下限为ClusterTime,并且这两个值是预先给定的。
一,聚类分析基本概念


聚类分析仅根据在数据中发现的描述对象及其关系的信息,将数据对象分组。其目标是,组内的对象相互之间是相似的(相关的),而不同组中的对象是不同的(不相关的)。组内的相似性(同质性)越大,组间差别越大,聚类就越好。

简而言之就是根据一定的规则将一组对象进行分组。例如,人们将日常生活中的事物分成了衣、食、住、行等不同的类别(簇);当然按照不同特征,人们还可以将这些事物继续分组为不同的子类,衣可以分为衣服和裤子或者是男装和女装等。聚类分析的过程就是发现数据对象之间的关系的过程;而聚类算法就是模拟分组的过程。(这里个人感觉算法像是在模拟大脑思考的过程,类似于仿生学)
常用聚集算法的分类:

  • K均值:K均值是基于原型的、划分的聚类技术。它试图发现用户指定个数(K)的簇(由质心代表)。
  • 凝聚的层次聚类:这种聚类方法涉及一组密切相关的聚类技术他们通过如下步骤产生。
    层次聚类:开始,每个点作为一个单点簇;然后,重复的合并两个靠近的簇,直到产生单个、包含所有点的簇。
  • DBSCAN:这是一种产生划分聚类的基于密度聚类的算法,簇的个数由算法自动的确定,低密度区域中的点被视为噪声而忽略,因此,DBSCAN不产生完全聚类。

二,算法设计

1,距离聚集

  在拿到上面的需求的时候,简直是一脸懵逼,我靠怎么根据轨迹点计算出滞留点呀,完全没有思路,想了很久才想到通过计算距离和时间的方式来统计。这样就需要构造聚集实体类,该实体类中包一个起始的中心点和一个聚集点的集合,并根据这个中心点及时间先后顺序来遍历整个轨迹,将到该中心点的距离小于ClusterRadius的点加入到实体的聚集集合中,这样在遍历完整个列表后就可以得到一个聚集实体的列表,这样就将原始的点根据距离聚合成了一个个的聚集实体(其实就将一条轨迹切割成了一条条的线段),最后根据ClusterTime因素遍历一遍聚集实体列表,将时间短于预定值的实体去除,剩下的结果就是符合要求的滞留区域。用伪代码表示如下:

  1. 定义聚集实体;
  2. 选择起始点作为第一个聚集实体的中心;
  3. 遍历轨迹点的集合,将到聚集实体中心的距离小于ClusterRadius的点添加到该聚集实体的聚集列表中;
  4. 直到与上一个聚集实体中心的距离大于ClusterRadius,则以该点为中心重新构造一个聚集实体;
  5. 遍历所有聚集实体,舍弃时间小于ClusterTime的实体;
  6. 最后剩余的实体就是所需的结果。

  用代码实现上面思路后发现其实聚集的结果并不是十分的理想,因为每个聚集实体的中心点都不是根据计算得来的,也不是最终的滞留区域的质心,就会有可能产生如下图2-1所示的聚集结果,其中绿色圆圈表示最终的滞留区域,但很明显圆点并不是质心,真正的质心应该往左偏移了,并且这个聚集结果显然也不是最理想的,比较理想的应该是图2-2这样的。
图2-1,不理想时的结果
图2-2,比较理想的结果

  另外,该算法的时间复杂度和空间复杂度比较高,并且需要不断的计算距离,计算比较复杂,最终导致效率比较低。

2,网格和密度聚集

  在距离聚集结果不理想的情况下,了解到数据挖掘中聚集算法能够比较有效的处理地图上标注的聚集问题。便看了《数据挖掘导论》这本书中相关内容以及网上的一些资料(具体的已经不记得咯,或许这就是潜移默化!),最终选择了网格算法和密度算法。其中网格算法能够有效的避免频繁的平方开方计算;对于密度算法感觉就像是仿生一样,人眼一看到一条轨迹上的点,第一判断就是哪儿的点密集、乱就应该是停留时间比较长的地方咯,之所以选择这个算法也是因为这个原因,事实证明这样聚集的结果要好于第一种。下面简要的描述下这两种算法的思想:
  网格算法:将地图分成一个个的格子,然后所有的点都投射到对应区域的格子中,每个格子就可以称之为一个聚集对象,每个格子中点的质心可以成为该区域的聚集中心(类似的在地图上显示的各个区的区名位置都是区政府的位置)。除了这种最原始的粗糙的计算方法,还衍生了一些优化后的网格算法。
  对网格构造过程的优化:动态的构造网格,上面叙述的这种方式在点分布不均匀的情况下,其实会产生许多空的网格(网格中没有点),如图2-3所示,所以就有了这种动态构造的策略。首先,要明确的一点是在地图范围确定之后,其实对应的每个网格的位置都是确定的,产生的结果如图2-4所示。总结一句话就是,上面的算法是先切好豆腐后撒了点糖,后面这种是边切边撒糖咯(总结的好尴尬啊,没错,我就是肚子饿了,想吃油炸豆腐拌白糖,还不能炸太老哦。。。。。。)

图2-3,原始方法会产生空网格
图2-4,动态构造只会产生深色表示的网格
  对网格构造结果的优化:对构造的网格再重新构造一遍。从图2-4可以看出中间那三个网格中点靠的比较近,其实应该放到一个网格中才比较合理。因此,对于这种网格的质心在网格边缘与其相邻的其它网格的质心靠的比较近(这种距离可以用网格的边长来量化)的情况,应该将网格作为元素重新构造一遍,如果有质心相距较近的网格就合并为一个。这种合并操作其实可以循环合并多次直到没有网格的质心聚集较近,但一般合并一次就足够了,这个根据需求来就好了。注意:这种方式需要定义网格相邻是4个(上、下、左、右)还是8个(上、下、左、右、左上、右上、左下、右下)。最终结果应该是图2-5这样的。

图2-5,蓝色框表示最终结果

  密度算法:这种方法在处理需求时是在网格算法的基础上进行的,首先需要定义密度的临界值(随实际需求而定,往往临界值越大最后的网格越少),将密度(点的个数)低于临界值的网格称之为离散网格并去掉。最后就只剩下高密度的网格啦,这里假设临界值为2,对应图2-5最终的结果应该为图2-6所示。

图2-6,根据密度筛选后的结果
  这样就得到了想要的结果,最终的效果图如下:
图2-7,最终结果的局部图
图2-8,ClusterRadius=50m,ClusterTime=5min
图2-9,ClusterRadius=200m,ClusterTime=5min

三,具体代码实现


网格对象的定义:

class ClusterRect
{
    public ClusterRect(double MinX, double MinY, double MaxX, double MaxY)
    {
        this.MinX = MinX;
        this.MinY = MinY;
        this.MaxX = MaxX;
        this.MaxY = MaxY;

        this.IndexList = new List<int>();
    }

    // 质心
    public double CenterX { get; set; }
    public double CenterY { get; set; }
    // 网格范围
    public double MinX { get; set; }
    public double MinY { get; set; }
    public double MaxX { get; set; }
    public double MaxY { get; set; }
    // 包含的数据在集合中的索引
    public List<int> IndexList { get; set; }

    public void AddIndex(int index)
    {
        this.IndexList.Add(index);
    }

    public void AddIndex(int i, double x, double y)
    {
        // 更新质心
        CenterX = (CenterX * this.IndexList.Count + x) / (this.IndexList.Count + 1);
        CenterY = (CenterY * this.IndexList.Count + y) / (this.IndexList.Count + 1);

        this.AddIndex(i);
    }

    /// <summary>
    /// 判断坐标x,y是否在矩形区域内
    /// </summary>
    /// <param name="x">x坐标</param>
    /// <param name="y">y坐标</param>
    /// <returns>true在,false不在</returns>
    public bool ContainPoint(double x, double y)
    {
        return (x >= MinX && x <= MaxX && y >= MinY && y <= MaxY);
    }

    /// <summary>
    /// 矩形区域中包含的坐标数
    /// </summary>
    /// <returns></returns>
    public int PointCount()
    {
        return IndexList.Count;
    }

    public int First()
    {
        return IndexList.First();
    }

    public int Last()
    {
        return IndexList.Last();
    }

    /// <summary>
    /// 判断两个矩形是否相邻
    /// </summary>
    /// <param name="clusterRect"></param>
    /// <returns></returns>
    public bool CloseRegion(ClusterRect clusterRect)
    {
        if (
            Math.Abs(this.MinX - clusterRect.MinX) < 0.000001 ||    // 左边
            Math.Abs(this.MaxX - clusterRect.MaxX) < 0.000001 ||    // 右边
            Math.Abs(this.MinY - clusterRect.MinY) < 0.000001 ||    // 上边
            Math.Abs(this.MaxY - clusterRect.MaxY) < 0.000001 ||    // 下边
            (Math.Abs(this.MinX - clusterRect.MaxX) < 0.000001 && Math.Abs(this.MinY - clusterRect.MaxY) < 0.000001) || // 左上角
            (Math.Abs(this.MinX - clusterRect.MaxX) < 0.000001 && Math.Abs(this.MinY - clusterRect.MaxY) < 0.000001) || // 右上角
            (Math.Abs(this.MinX - clusterRect.MaxX) < 0.000001 && Math.Abs(this.MaxY - clusterRect.MinY) < 0.000001) || // 左下角
            (Math.Abs(this.MaxX - clusterRect.MinY) < 0.000001 && Math.Abs(this.MaxY - clusterRect.MinY) < 0.000001))   // 右下角
        {
            return true;
        }
        return false;
    }

    /// <summary>
    /// 合并ClusterRect对象,且索引集合中的索引必须要连续
    /// </summary>
    /// <param name="tempRect"></param>
    public void AbsorbRect(ClusterRect rect)
    {
        if (rect == null) return;

        this.CenterX = (this.CenterX * this.IndexList.Count + rect.CenterX * rect.PointCount()) 
            / (this.IndexList.Count + rect.PointCount());
        this.CenterY = (this.CenterY * this.IndexList.Count + rect.CenterY * rect.PointCount()) 
            / (this.IndexList.Count + rect.PointCount());

        if (rect.First() - this.Last() == 1)
        {
            this.IndexList.AddRange(rect.IndexList);
        }
        else
        {
            return;
        }
        // 重新计算矩形的边界
        this.MinX = Math.Min(this.MinX, rect.MinX);
        this.MinY = Math.Min(this.MinY, rect.MinY);
        this.MaxX = Math.Max(this.MaxX, rect.MaxX);
        this.MaxY = Math.Max(this.MaxY, rect.MaxY);
    }
}

具体的聚集算法关键代码:

{
    .......// 省略若干行代码

    // 这里自动脑补为自己的初始化,配置文件或常量都行
    double maxDist = ClusterRadius;
    double maxDuration = ClusterTime;
    
    double maxRadius = maxDist / 2;
    List<ClusterRect> rects = new List<ClusterRect>();
    double total = 0;
    
    // 以开始点作为第一个矩形的中心
    double firstX = Convert.ToDouble(points[0][1]), firstY = Convert.ToDouble(points[0][2]);
    ClusterRect firstRect = new ClusterRect(firstX - maxRadius
                                            , firstY - maxRadius
                                            , firstX + maxRadius
                                            , firstY + maxRadius);
    firstRect.AddIndex(0);
    firstRect.CenterX = firstX; firstRect.CenterY = firstY;
    rects.Add(firstRect);
    
    // 1,构造网格矩阵
    bool existRect = false;
    double x, y;
    for (int i = 1; i < points.Count; i++)
    {
        x = Convert.ToDouble(points[i][1]);
        y = Convert.ToDouble(points[i][2]);
    
        for (int j = 0; j < rects.Count; j++)
        {
            ClusterRect tempRect = rects[j];
            if (tempRect.ContainPoint(x, y) && tempRect.Last() == i - 1) //
            {
                // 在区域内,添加到这个矩形
                tempRect.AddIndex(i, x, y);
                existRect = true;
                break;
            }
        }
    
        if (!existRect)
        {
            double minX = Math.Floor((x - firstRect.MinX) / maxDist) * maxDist + firstRect.MinX;
            double minY = Math.Floor((y - firstRect.MinY) / maxDist) * maxDist + firstRect.MinY;
            double maxX = minX + maxDist;
            double maxY = minY + maxDist;
            // 创建网格对象
            ClusterRect rect = new ClusterRect(minX, minY, maxX, maxY);
            rect.AddIndex(i, x, y);
            rects.Add(rect);
        }
        existRect = false;
    }
    
    // 2,过滤密度低于3的离散网格,这里集合可以复用的,但是便于理解没有这样做
    List<ClusterRect> filterRect = new List<ClusterRect>();
    foreach (ClusterRect rect in rects)
    {
        if (rect.PointCount() < 3)
        {
            filterRect.Add(rect);
        }
    }
    // 这里集合可以复用的,但是便于理解没有这样做
    List<ClusterRect> rectList = new List<ClusterRect>();
    // 3,合并临近网格,并去掉低于临界值的网格
    if (rects.Count > 0)
    {
        rectList.Add(rects[0]);
        for (int i = 1; i < rects.Count; i++)
        {
            ClusterRect tempRect = rects[i];
            ClusterRect preRect = rectList.Last();
    
            //double preX = Convert.ToDouble(points[preRect.Last()][1]);
            //double preY = Convert.ToDouble(points[preRect.Last()][2]);
            //double tempX = Convert.ToDouble(points[tempRect.First()][1]);
            //double tempY = Convert.ToDouble(points[tempRect.First()][2]);
    
            // 合并临近的网格,并且要求质心距离小于最大聚集半径
            // 这里网格包含8个方向,合并到上一个网格中
            if (tempRect.CloseRegion(preRect)
                && Math.Sqrt((Math.Pow(tempRect.CenterX - preRect.CenterX, 2)
                              + Math.Pow(tempRect.CenterY - preRect.CenterY, 2))) <= maxDist)
            {
                // 是临近网格,合并网格
                preRect.AbsorbRect(tempRect);
            }
            else
            {
                rectList.Add(tempRect);
            }
        }
    }

    .......// 省略若干行代码
}

四,总结

  该算法在合并网格之后形成的大网格可能会比ClusterRadius大,但是合并的依据是质心间的距离,而质心在这里能表示两个点的分布都比较靠近且大部分的点间的距离都小于ClusterRadius。因此实际中能够接受这个问题。

Math.Sqrt((Math.Pow(tempRect.CenterX - preRect.CenterX, 2)
            + Math.Pow(tempRect.CenterY - preRect.CenterY, 2))) 
    <= maxDist

  另外,因为轨迹中的点是有先后顺序的,不像搜索地图上周围的美食时那种聚集不需要顾及先后顺序只需将距离相近的点聚集就行了,所以在这里控制了每个ClusterRect中的点是一个连续的序列,即一条完整的线段,这样也能避免来回两条线的聚集。
  最后的最后,上面应用的是二维空间下,如果是n维的数据结构或其他的应用场景还是需要灵活的运用,选择合适的聚集算法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • 本篇结构 简介 聚类算法的分类 K-Means聚类算法 DBSCAN聚类算法 本篇介绍了聚类算法的种类,重点关注K...
    w1992wishes阅读 7,460评论 0 14
  • 题记:最近的一个月,天气逐渐转冷,天亮的时间慢慢的变晚,墨墨同学同学的起床时间也随之推后。推后的结果是一日活动全部...
    雨墨妈妈阅读 322评论 0 2
  • 今晚,慧敏同学带读《发展心理学》第一章内容---毕生发展绪论。本章有以下四个内容:1、毕生发展的取向,2、关键...
    章志桂阅读 255评论 0 0
  • 一 我叫莫忆。 于小望是我的病友。 她是一个奇怪的女孩,整个病房里她只和我说话。 我喜欢于小望。只是因为她爱读童话...
    暴跳橙子阅读 538评论 0 2
  • 二十一 这两人正说着话呢,李袈澜已经拉过风铃来挡着躲到了后面,风铃只得拦了凤舞:“好大家,别跟这小姑娘一般见识了,...
    笔间流年阅读 190评论 2 1