在python中,什么是最有效的识别n次事件集群的方法?

时间:2017-06-07 16:35:46

标签: python

例如,如果我有以下事件数据,并且想要找到彼此相差1分钟的至少2个事件的集群,其中id_1,id_2和id_3都是相同的。作为参考,除了日期时间时间戳之外,我还有纪元时间戳(以微秒为单位)。

event_id       timestamp          id_1 id_2  id_3
9442823 Jun 15, 2015 10:22 PM PDT   A   1   34567
9442821 Jun 15, 2015 10:22 PM PDT   A   2   12345
9442817 Jun 15, 2015 10:22 PM PDT   A   3   34567
9442814 Jun 15, 2015 10:22 PM PDT   A   1   12345
9442813 Jun 15, 2015 10:22 PM PDT   A   2   34567
9442810 Jun 15, 2015 10:22 PM PDT   A   3   12345
9442805 Jun 15, 2015 10:22 PM PDT   A   1   34567
9442876 Jun 15, 2015 10:23 PM PDT   A   2   12345
9442866 Jun 15, 2015 10:23 PM PDT   A   3   34567
9442858 Jun 15, 2015 10:23 PM PDT   A   1   12345
9442845 Jun 15, 2015 10:23 PM PDT   C   2   34567
9442840 Jun 15, 2015 10:23 PM PDT   C   3   12345
9442839 Jun 15, 2015 10:23 PM PDT   C   1   34567
9442838 Jun 15, 2015 10:23 PM PDT   C   2   12345
9442907 Jun 15, 2015 10:24 PM PDT   C   3   34567
9442886 Jun 15, 2015 10:24 PM PDT   C   1   12345
9442949 Jun 15, 2015 10:25 PM PDT   C   2   34567
9442934 Jun 15, 2015 10:25 PM PDT   C   3   12345

对于找到的每个群集,我想返回一组(id_1,id_2,id_3,[event_ids列表],min_timestamp_of_cluster,max_timestamp_of_cluster)。另外,如果有一个包含(例如)6个事件的集群,我只希望返回包含所有事件的单个结果,而不是每个事件的3个事件返回一个结果。

2 个答案:

答案 0 :(得分:0)

如果我正确理解您的问题,您可以使用scikit-learn的DBSCAN聚类算法和自定义距离(或度量)功能。如果id_1,id_2或id_3中的任何一个不同,则距离函数应返回非常大的数字。否则应该返回时差。

但是使用这种方法,簇的数量由算法确定,而不是作为算法的输入。如果您决定将簇数作为输入传递,则k-means是您可能需要查看的聚类算法。

答案 1 :(得分:0)

在纯python中,使用一个“滑动窗口”,它包含1分钟范围内的所有事件。

前提很简单:维护一个子序列的事件队列 按顺序排列总清单。 “窗口”(队列)应该是您关心的所有事件。在这种情况下,这取决于60秒的时间间隔要求。

在处理事件时,将一个事件添加到队列的末尾。如果队列中的第一个事件距离新添加的最后一个事件超过60秒,则通过从队列前面删除第一个事件来向前滑动窗口。

这是python3:

import collections
import operator
import itertools

from datetime import datetime

#### FROM HERE: vvv is just faking events. Delete or replace.

class Event(collections.namedtuple('Event', 'event_id timestamp id_1 id_2 id_3 epoch_ts')):
    def __str__(self):
        return ('{e.event_id} {e.timestamp}  {e.id_1}  {e.id_2}  {e.id_3}'
                .format(e=self))

def get_events():
    event_list = map(operator.methodcaller('strip'), '''
        9442823 Jun 15, 2015 10:22 PM PDT   A   1   34567
        9442821 Jun 15, 2015 10:22 PM PDT   A   2   12345
        9442817 Jun 15, 2015 10:22 PM PDT   A   3   34567
        9442814 Jun 15, 2015 10:22 PM PDT   A   1   12345
        9442813 Jun 15, 2015 10:22 PM PDT   A   2   34567
        9442810 Jun 15, 2015 10:22 PM PDT   A   3   12345
        9442805 Jun 15, 2015 10:22 PM PDT   A   1   34567
        9442876 Jun 15, 2015 10:23 PM PDT   A   2   12345
        9442866 Jun 15, 2015 10:23 PM PDT   A   3   34567
        9442858 Jun 15, 2015 10:23 PM PDT   A   1   12345
        9442845 Jun 15, 2015 10:23 PM PDT   C   2   34567
        9442840 Jun 15, 2015 10:23 PM PDT   C   3   12345
        9442839 Jun 15, 2015 10:23 PM PDT   C   1   34567
        9442838 Jun 15, 2015 10:23 PM PDT   C   2   12345
        9442907 Jun 15, 2015 10:24 PM PDT   C   3   34567
        9442886 Jun 15, 2015 10:24 PM PDT   C   1   12345
        9442949 Jun 15, 2015 10:25 PM PDT   C   2   34567
        9442934 Jun 15, 2015 10:25 PM PDT   C   3   12345
    '''.strip().splitlines())

    for line in event_list:
        idev, *rest = line.split()
        ts = rest[:6]
        id1, id2, id3 = rest[6:]
        id2 = int(id2)  # faster when sorting (see find_clustered_events)
        id3 = int(id3)  # faster when sorting (see find_clustered_events)
        ts_str = ' '.join(ts)
        dt = datetime.strptime(ts_str.replace('PDT', '-0700'), '%b %d, %Y %I:%M %p %z')
        epoch = dt.timestamp()
        ev = Event(idev, ts_str, id1, id2, id3, epoch)
        yield ev

#### TO HERE: ^^^  was just faking up your events. Delete or replace.


def add_cluster(key, group):
    '''Do whatever you want with the clusters. I'll print them.'''

    print('Cluster:', key)
    print('    ','\n    '.join(map(str, group)), sep='')


def find_clustered_events(events, cluster=3, gap_secs=60):
    '''Call add_cluster on clusters of events within a maximum time gap.

    Args:
        events (iterable): series of events, in chronological order
        cluster (int): minimum number of events in a cluster
        gap_secs (float): maximum time-gap from start to end of cluster

    Returns:
        None.

    '''
    window = collections.deque()
    evkey = lambda e: (e.id_1, e.id_2, e.id_3)

    for ev in events:
        window.append(ev)

        t0 = window[0].epoch_ts
        tn = window[-1].epoch_ts

        if tn - t0 < gap_secs:
            continue

        window.pop()

        for k, g in itertools.groupby(sorted(window, key=evkey), key=evkey):
            group = tuple(g)
            if len(group) >= cluster:
                add_cluster(k, group)

        window.append(ev)
        window.popleft()

# Call find_clustered with event generator, cluster args. 
# Note that your data doesn't have any 3-clusters without time seconds. :-(

find_clustered_events(get_events(), cluster=2)

输出如下:

$ python test.py
Cluster: ('A', 1, 34567)
    9442823 Jun 15, 2015 10:22 PM PDT  A  1  34567
    9442805 Jun 15, 2015 10:22 PM PDT  A  1  34567
Cluster: ('A', 2, 12345)
    9442821 Jun 15, 2015 10:22 PM PDT  A  2  12345
    9442876 Jun 15, 2015 10:23 PM PDT  A  2  12345
Cluster: ('A', 3, 34567)
    9442817 Jun 15, 2015 10:22 PM PDT  A  3  34567
    9442866 Jun 15, 2015 10:23 PM PDT  A  3  34567
Cluster: ('A', 1, 12345)
    9442814 Jun 15, 2015 10:22 PM PDT  A  1  12345
    9442858 Jun 15, 2015 10:23 PM PDT  A  1  12345
Cluster: ('C', 2, 34567)
    9442845 Jun 15, 2015 10:23 PM PDT  C  2  34567
    9442949 Jun 15, 2015 10:25 PM PDT  C  2  34567

请注意:上面的代码不会尝试跟踪群集中已有的事件。因此,如果你有一个每十五秒发生一次的事件类型,你将得到一个这样的序列:

 event1   t=0:00
 event2   t=0:15
 event3   t=0:30
 event4   t=0:45
 event5   t=1:00

你会得到重叠的群集:

event1, event2, event3 (t=0:00 .. 0:30)
event2, event3, event4 (t=0:15 .. 0:45)
event3, event4, event5 (t=0:30 .. 1:00)

从技术上讲,这些是有效的集群,每个集群都略有不同。但是,如果您希望事件仅出现在单个群集中,您可能希望从窗口中删除以前的群集事件。

或者,如果聚类和重复的可能性很低,它可能会提高性能以在add_cluster()函数中实现重复检查,以减少主循环所做的工作。

最后一点:这是 LOT 的排序。排序效率不高,因为每次出现新事件时都会重复排序。如果您有大型数据集,性能可能会很差。如果你的事件键相对较少 - 也就是说,如果id1,2,3值倾向于一遍又一遍地重复 - 你最好动态地为每个不同的键(id1 + id2 + id3)创建单独的deques并调度将事件发送到适当的双端队列,应用相同的窗口逻辑,然后检查双端队列的长度。

另一方面,如果您正在处理类似Web服务器日志的事情,请求者总是在更改,这可能会导致所有无用的deques产生内存问题。所以这是一个记忆与时间的权衡,你必须要注意。