用于记录分析的滑动时间窗口

时间:2011-12-02 09:23:11

标签: c# .net algorithm

我有电话的数据结构。对于此问题,有两个字段CallTimeNumberDialled

我想要执行的分析是“在10秒窗口中有多于两次调用相同的数字”该集合已经按CallTime排序,并且是List<Cdr>

我的解决方案是

List<Cdr> records = GetRecordsSortedByCallTime();
for (int i = 0; i < records.Count; i++)
{
    var baseRecord = records[i];
    for (int j = i; j < records.Count; j++)
    {
        var comparisonRec = records[j];

        if (comparisonRec.CallTime.Subtract(baseRecord.CallTime).TotalSeconds < 20)
        {
            if (comparisonRec.NumberDialled == baseRecord.NumberDialled)
                ReportProblem(baseRecord, comparisonRec);
        }
        else
        {
            // We're more than 20 seconds away from the base record.  Break out of the inner loop
            break; 
        }
    }
}

至少可以说这是丑陋的。是否有更好,更清洁,更快捷的方法?

虽然我没有在大型数据集上对此进行测试,但我将以每小时约100,000条记录运行它,因此每条记录都会有大量的比较。

更新数据按时间排序,而不是像早期版本的问题那样排序

6 个答案:

答案 0 :(得分:5)

如果电话呼叫已按呼叫时间排序,您可以执行以下操作:

  • 初始化一个哈希表,其中包含每个电话号码的计数器(哈希表可以先为空,然后随时添加元素)
  • 有两个指向你的链接列表的指针,我们称之为'左'和'右'
  • 每当“左”和“右”呼叫之间的时间戳小于10秒时,向右移动“向右”,并将新遇到的电话号码的数量增加一个
  • 每当差异超过10秒时,将“向左”向前移动一个,并将“左”指针留下的电话号码的数量递减一个
  • 在任何时候,如果有一个电话号码,其哈希表中的计数器为3或更多,你就会发现一个电话号码在10秒钟内有超过2个电话

这是一个线性时间算法,并行处理所有数字。

答案 1 :(得分:2)

我不知道你确切的结构,所以我为这个演示创建了自己的结构:

class CallRecord
{
    public long NumberDialled { get; set; }
    public DateTime Stamp { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var calls = new List<CallRecord>()
        {
            new CallRecord { NumberDialled=123, Stamp=new DateTime(2011,01,01,10,10,0) },
            new CallRecord { NumberDialled=123, Stamp=new DateTime(2011,01,01,10,10,9) },
            new CallRecord { NumberDialled=123, Stamp=new DateTime(2011,01,01,10,10,18) },
        };

        var dupCalls = calls.Where(x => calls.Any(y => y.NumberDialled == x.NumberDialled && (x.Stamp - y.Stamp).Seconds > 0 && (x.Stamp - y.Stamp).Seconds <= 10)).Select(x => x.NumberDialled).Distinct();

        foreach (var dupCall in dupCalls)
        {
            Console.WriteLine(dupCall);
        }

        Console.ReadKey();
    }
}

LINQ表达式遍历所有记录并查找位于当前记录(.Seconds > 0)之前且在时间限制(.Seconds <= 10)内的记​​录。由于Any方法不断遍历整个列表,这可能会有点性能损失,但至少代码更清晰:)

答案 2 :(得分:1)

我建议您使用Rx ExtensionInterval方法。

  

Reactive Extensions(Rx)是一个库,用于使用可观察序列和LINQ样式查询运算符组合异步和基于事件的程序。使用Rx,开发人员使用Observables表示异步数据流,使用LINQ运算符查询异步数据流,并使用调度程序参数化异步数据流中的并发性

Interval 方法返回一个可观察的序列,该序列在每个句点后生成一个值

这是一个简单的例子:

    var callsPer10Seconds = Observable.Interval(TimeSpan.FromSeconds(10));

    from x in callsPer10Seconds 
           group x by x into g 
           let count = g.Count() 
           orderby count descending 
           select new {Value = g.Key, Count = count}; 

    foreach (var x in q) 
    { 
        Console.WriteLine("Value: " + x.Value + " Count: " + x.Count); 
    } 

答案 3 :(得分:0)

records.OrderBy(p => p.CallTime)
    .GroupBy(p => p.NumberDialled)
    .Select(p => new { number = p.Key, cdr = p.ToList() })
    .Select(p => new
    {
        number = p.number,
        cdr =
            p.cdr.Select((value, index) => index == 0 ? null : (TimeSpan?)(value.CallTime - p.cdr[index - 1].CallTime))
            .FirstOrDefault(q => q.HasValue && q.Value.TotalSeconds < 10)
    }).Where(p => p.cdr != null);

答案 4 :(得分:0)

分两步:

  1. 使用调用本身和有趣范围内的所有调用生成枚举
  2. 过滤此列表以查找连续的来电
  3. 使用AsParallel扩展方法在每条记录上并行完成计算。

    也可以不在最后调用ToArray并让计算完成,而其他代码可以在线程上执行,而不是强迫它等待并行计算完成。

    var records = new [] {
        new { CallTime= DateTime.Now, NumberDialled = 1 },
        new { CallTime= DateTime.Now.AddSeconds(1), NumberDialled = 1 }
    };
    var span = TimeSpan.FromSeconds(10);
    
    // Select for each call itself and all other calls in the next 'span' seconds
    var callInfos = records.AsParallel()
        .Select((r, i) =>
            new
            {
                Record = r,
                Following = records.Skip(i+1)
                                .TakeWhile(r2 => r2.CallTime - r.CallTime < span)
            }
        );
    
    // Filter the calls that interest us
    var problematic = (from callinfo in callInfos 
                    where callinfo.Following.Any(r => callinfo.Record.NumberDialled == r.NumberDialled)
                    select callinfo.Record)
                    .ToArray();
    

答案 5 :(得分:0)

如果性能可以接受(我认为它应该是,因为100k记录不是特别多),这种方法(我认为)很好而且干净:

首先,我们按编号对记录进行分组:

var byNumber = 
    from cdr in calls
    group cdr by cdr.NumberDialled into g
    select new 
             {
                 NumberDialled = g.Key,
                 Calls = g.OrderBy(cdr => cdr.CallTime)
             };

我们现在所做的是Zip(.NET 4)每个调用集合本身 - 一个一个地移位,将调用时间列表转换为间隙列表调用。然后我们寻找最多10秒的间隔数字:

var interestingNumbers =
    from g in byNumber
    let callGaps = g.Calls.Zip(g.Calls.Skip(1), 
        (cdr1, cdr2) => cdr2.CallTime - cdr1.CallTime)
    where callGaps.Any(ts => ts.TotalSeconds <= 10)
    select g.NumberDialled;

现在interestingNumbers是一系列感兴趣的数字。