需求:在采集完用户轨迹后,根据轨迹中的坐标点统计滞留区域。并对滞留区域在半径和时间上有要求,即多长时间以内不计算在内,聚集的半径小于多少。
  说明:这里规定聚集半径上限为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维的数据结构或其他的应用场景还是需要灵活的运用,选择合适的聚集算法。