浅谈分组统计

在现实生活中,分组统计是很常用的。例如人民银行要求商业银行报送的反洗钱报表中就有一个项目是当月发生的大额交易的笔数和金额,其中大额交易定义为某个客户的当日累计发生额在人民币20万元或者外币等值1万美元以上。这样就要从大量的交易流水账中按交易日期进行分组统计。

让我们来生成要统计的数据,如下所示:

IEnumerable<Tuple<int, double>> GetTuples(int n)
{
  var tuples = new Tuple<int, double>[n];
  var rand = new Random();
  for (int k = 1, i = 0; i < n; i++)
  {
    var r = rand.Next(n);
    k += (r >= n - 3) ? 2 : ((r >= n - 9) ? 1 : 0);
    tuples[i] = new Tuple<int, double>(k, rand.NextDouble());
  }
  return tuples;
}

该方法生成 n 项已经排好序的数据。

现在,让我们来按关键字分组,并统计每组的个数和平均值。

首先,使用 C# 的 foreach 循环,如下所示:

IEnumerable<Tuple<int, int, double>> ForEach(IEnumerable<Tuple<int, double>> tuples)
{
  var result = new List<Tuple<int, int, double>>();
  var count = 0;
  var sum = 0.0;
  int? key = null;
  foreach (var v in tuples)
  {
    if (key != v.Item1)
    {
      if (key != null) result.Add(new Tuple<int, int, double>(key.Value, count, sum / count));
      sum = count = 0;
      key = v.Item1;
    }
    count++;
    sum += v.Item2;
  }
  if (key != null) result.Add(new Tuple<int, int, double>(key.Value, count, sum / count));
  return result;
}

这种方法有个最大的缺点就是在 foreach 循环结束之后还要进行一次统计,闻到了代码的“坏味道”。

那么,就让我们来重构吧,这次,使用迭代器进行循环:

IEnumerable<Tuple<int, int, double>> Iterate(IEnumerable<Tuple<int, double>> tuples)
{
  var result = new List<Tuple<int, int, double>>();
  var count = 0;
  var sum = 0.0;
  int? key = null;
  for (var iter = tuples.GetEnumerator(); ; count++, sum += iter.Current.Item2)
  {
    var hasValue = iter.MoveNext();
    if (!hasValue || key != iter.Current.Item1)
    {
      if (key != null) result.Add(new Tuple<int, int, double>(key.Value, count, sum / count));
      if (!hasValue) break;
      sum = count = 0;
      key = iter.Current.Item1;
    }
  }
  return result;
}

这样,就消灭了“坏味道”。

注意,以上两种方法都假设输入数据已经排好序。如若不然,就要先对输入数据进行一次排序。

最后,如果使用 Linq 的话,还可以更简单:

IEnumerable<Tuple<int, int, double>> Linq(IEnumerable<Tuple<int, double>> tuples)
{
  var result = new List<Tuple<int, int, double>>();
  var q = from k in tuples group k by k.Item1;
  foreach (var g in q) result.Add(new Tuple<int, int, double>(g.Key, g.Count(), g.Average(v => v.Item2)));
  return result;
}

要注意 Linq 方法无论是运行时间还是占用的内存都更大。

我们来看看 Main 方法:

static void Main(string[] args)
{
  try
  {
    new Program().Run(Console.Out, int.Parse(args[0]));
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex);
  }
}

void Run(TextWriter writer, int n)
{
  var tuples = GetTuples(n * 1024 * 1024);
  Write("ForEach", writer, ForEach(tuples));
  Write("Iterate", writer, Iterate(tuples));
  Write(" Linq  ", writer, Linq(tuples));
}

其中的 Write 方法如下所示:

void Write(string title, TextWriter writer, IEnumerable<Tuple<int, int, double>> tuples)
{
  writer.WriteLine("==========> " + title + " <============");
  writer.WriteLine("Key ------Count Average----------");
  var count = 0;
  var sum = 0.0;
  foreach (var t in tuples)
  {
    writer.WriteLine("{0,3} {1,11:N0} {2}", t.Item1, t.Item2, t.Item3);
    count += t.Item2;
    sum += t.Item2 * t.Item3;
  }
  writer.WriteLine("--- ----------- -----------------");
  writer.WriteLine("{0,3} {1,11:N0} {2}", tuples.Count(), count, sum / count);
  writer.WriteLine();
}

最后,这个程序的输出如下所示:

==========> ForEach <============
Key ------Count Average----------
  1      10,476 0.492122426354162
  2   1,633,289 0.499917991099794
  3     981,345 0.500446307804579
  5   1,542,377 0.500567888024527
  6     478,158 0.499376479287702
  8      62,325 0.501552373474687
  9   1,463,104 0.500270067230854
 11     802,680 0.500518684820775
 13     367,798 0.499572390413821
 14     492,947 0.500767958524
 16   2,403,053 0.500023199420802
 17     248,208 0.499988049057847
--- ----------- -----------------
 12  10,485,760 0.50018897689056

==========> Iterate <============
Key ------Count Average----------
  1      10,476 0.492122426354162
  2   1,633,289 0.499917991099794
  3     981,345 0.500446307804579
  5   1,542,377 0.500567888024527
  6     478,158 0.499376479287702
  8      62,325 0.501552373474687
  9   1,463,104 0.500270067230854
 11     802,680 0.500518684820775
 13     367,798 0.499572390413821
 14     492,947 0.500767958524
 16   2,403,053 0.500023199420802
 17     248,208 0.499988049057847
--- ----------- -----------------
 12  10,485,760 0.50018897689056

==========>  Linq   <============
Key ------Count Average----------
  1      10,476 0.492122426354162
  2   1,633,289 0.499917991099794
  3     981,345 0.500446307804579
  5   1,542,377 0.500567888024527
  6     478,158 0.499376479287702
  8      62,325 0.501552373474687
  9   1,463,104 0.500270067230854
 11     802,680 0.500518684820775
 13     367,798 0.499572390413821
 14     492,947 0.500767958524
 16   2,403,053 0.500023199420802
 17     248,208 0.499988049057847
--- ----------- -----------------
 12  10,485,760 0.50018897689056

这个程序中用到的 Tuple 类如下所示:

class Tuple<T1, T2>
{
  public T1 Item1 { get; private set; }
  public T2 Item2 { get; private set; }
  public Tuple(T1 item1, T2 item2) { Item1 = item1; Item2 = item2; }
}

class Tuple<T1, T2, T3> : Tuple<T1, T2>
{
  public T3 Item3 { get; private set; }
  public Tuple(T1 item1, T2 item2, T3 item3) : base(item1, item2) { Item3 = item3;  }
}

其实 .NET Framework 4.0 Base Class Library 中已经有 Tuple 类了。

本文中的全部源程序代码可以在这里下载。

版权声明:本文为博主http://www.zuiniusn.com 原创文章,未经博主允许不得转载。

时间: 2024-09-30 01:30:40

浅谈分组统计的相关文章

浅谈正则表达式中的分组和引用

问题 我的答案 说明 由正则表达式如何匹配相同字符出发,讲讲正则表达式中的选择.分组和引用. 问题 在外刊君读者群中看到有人提出这样的一个需求: 把字符串切成连续相同字符的正则怎么写?比如abbcccdddd切成a,bb,ccc,dddd 之前我对正则表达式也是略有研究,想尝试一下.其实我对正则表达式的学习基本完全来源于犀牛书的第10章,真正看懂这一章,我觉得操作正则表达式应该不在话下. 我的答案 先给出我的答案吧: 'abbccddd'.match(/(w)1*/g) // ["a"

Linux的文本处理工具浅谈-awk sed grep

Linux的文本处理工具浅谈 awk   老大 [功能说明] 用于文本处理的语言(取行,过滤),支持正则 NR代表行数,$n取某一列,$NF最后一列 NR==20,NR==30 从20行到30行 FS竖着切,列的分隔符 RS横着切,行的分隔符 [语法格式] awk [–F] [“[分隔符]”] [’{print$1,$NF}’] [目标文件] awk 'BEGIN{FS="[列分隔符]+";RS="[行分隔符]+";print "-GEGIN-"

.net中对象序列化技术浅谈

.net中对象序列化技术浅谈 2009-03-11 阅读2756评论2 序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储和传输数 据.例如,可以序列化一个对象,然后使用 HTTP 通过 Internet 在客户端和服务器之间传输该对象.反之,反序列化根据流重新构造对象.此外还可以将对象序列化后保存到本地,再次运行的时候可以从本地文件 中“恢复”对象到序列化之前的状态.在.net中有提供了几种序列化的方式:二进制序列化

浅谈RAII&智能指针

关于RAII,官方给出的解释是这样的"资源获取就是初始化".听起来貌似不是很懂的哈,其实说的通俗点的话就是它是一种管理资源,避免内存泄漏的一种方法.它可以保证在各种情况下,当你对对象进行使用时先通过构造函数来进行资源的分配和初始化,最后通过析构函数来进行清理,有效的保证了资源的正确分配和释放.(特别是在异常中,因为异常往往会改变代码正确的执行顺序,这就很容易引起资源管理的混乱和内存的泄漏) 其中智能指针就是RAII的一种实现模式,所谓的智能就是它可以自动化的来管理它所指向那份空间的资源

浅谈软件项目的需求管理

软件项目区别于其它项目的最显著的特征是其不可见性,它不像硬件购销.建筑工程,都是实实在在可见的东西.而软件项目在系统交付之前很长一段时间,客户是无法感知自己想要的系统究竟是什么样子.因此,需求管理就显得十分重要,据相关统计数据分析,软件项目90%以上失败的原因都在于没有重视需求或者需求管理方面做的不到位导致的. 需求管理作为软件项目管理的一个重要内容,贯穿项目实施的全生命周期.俗话说:万事开头难.需求作为软件开发的第一个环节,其重要性不言而喻.市面上关于需求管理的相关理论和书籍很多,但多数停留在

递归算法浅谈

递归算法 程序调用自身的编程技巧称为递归( recursion). 一个过程或函数在其定义或说明中又直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题类似的规模较小的问题来求解,递归策略仅仅需少量的程序就可描写叙述出解题过程所须要的多次反复计算,大大地降低了程序的代码量. 注意: (1) 递归就是在过程或函数里调用自身; (2) 在使用递增归策略时,必须有一个明白的递归结束条件,称为递归出口. 一个比較经典的描写叙述是老和尚讲故事,他说从前有座山,山上有座庙,庙里有个

浅谈自然语言处理基础(下)

命名实体识别 命名实体的提出源自信息抽取问题,即从报章等非结构化文本中抽取关于公司活动和国防相关活动的结构化信息,而人名.地名.组织机构名.时间和数字表达式结构化信息的关键内容,所以需要从文本中去识别这些实体指称及其类别,即命名实体识别和分类. 21世纪以后,基于大规模语料库的统计方法成为自然语言处理的主流,以下是基于统计模型的命名实体识别方法归纳: 基于CRF的命名实体识别方法 基于CRF的命名实体识别方法简便易行,而且可以获得较好的性能,广泛地应用于人名.地名和组织机构等各种类型命名实体的识

关系型数据库表结构设计规范-浅谈(转)

数据库表结构设计规范-浅谈,为啥是浅谈呢,因为主要的观点还是来自原微信公共账号的一篇文章,稍微加了一些自己的看法. 谁来进行数据库的设计? 肯定是具体的开发工程师来进行,开发同学的话,第一业务熟悉度比较高,第二结合OO和ORM的思想,能有比较好的运用关系型数据库的特性.如果是DBA同学的话,虽然对于数据库本身了解比较多,但是对于业务了解较少,很难有比较客观的设计.但是业务上线或者运行期间,需要DBA同学能够重度的加入进来,针对一些性能点和不合理的点进行优化,同事也可以在上线前,针对SQL进行re

浅谈文本的相似度问题

今天要研究的问题是如何计算两个文本的相似度.正如上篇文章描述,计算文本的相似度在工程中有着重要的应用, 比如文本去重,搜索引擎网页判重,论文的反抄袭,ACM竞赛中反作弊等等. 上篇文章介绍的SimHash算法是比较优秀的文档判重算法,它能处理海量文本的判重,Google搜索引擎也正是用这 个算法来处理网页的重复问题.实际上,仅拿文本的相似度计算来说,有很多算法都能解决这个问题,并且都达到比 较满意的效果.最常见的几种方法如下 (1)基于最长公共子串 (2)基于最长公共子序列 (3)基于最少编辑距