文本数据的分分合合
对于数据分析工作来说,可能最希望的是面对规整、一致的数据,例如定义清晰明确的关系数据库,或者可以自由钻取、切片的OLAP 数据仓库。但不幸的是,很多时候,我们不得不面对来自方方面面的零散数据,亲自操刀煎炒烹炸,这其中,文本数据的合并、分拆就有点像基本的刀工,既要做的漂亮,但又不能用太多的时间。这时候一把趁手好刀的作用就不容忽视了——我们这里介绍的,集算器,就可以说是这个领域的双立人、王麻子了。
仔细想一下,处理文本数据时需要特别关注的问题,不外乎是否包含标题行、数据之间如何分隔、过滤空行、是否特别判断数据类型等等,不过别忘了,还有一个很重要的问题,那就是对文件标题的处理。事实上,很多情况下,输出文件的时候都会利用文件标题传递一些信息,最常见的是时间或者某种分类,这一方面能够用来标识和区分文件,另一方面也可以增加可读性,提高检索效率。
集算器在处理文本数据的时候,通过灵活利用各种函数和数据结构,能够简洁、方便地应对上述各种问题。
首先,我们来看一个合并文本数据的例子:
某公司要求各地分支机构每天上报销售数据,要求(简化后)包括三项,销售人员编码、产品编码、总数,各项之间用制表符分隔,上报的文件名由分支机构1位代码和上报时间组成,具体样例如下:
北京分公司:B20180325.txt
销售人员 产品 总数
BJ003 RPTv5.2 15
BJ012 esProc2.0 3
上海分公司:S20180325.txt
销售人员 产品 总数
SH001 RPTv4.9 2
广东分公司:G20180325.txt
销售人员 产品 总数
GZ005 RPTv5.2 21
SZ001 esProc2.0 5
GZ001 RPTv4.9 3
期望导入后的数据库表sales_summary结果为:
销售人员 产品 总数 销售日期
BJ003 RPTv5.2 15 20180325
BJ012 esProc2.0 3 20180325
SH001 RPTv4.9 2 20180325
GZ005 RPTv5.2 21 20180325
SZ001 esProc2.0 5 20180325
GZ001 RPTv4.9 3 20180325
首先,把上报的文件都保存到一个目录中,例如是Windows系统的E盘根目录下,然后执行下面集算器脚本就行了!
A | |
1 | [a,b,c] |
2 | =A1.(file("E:\\"+~+string(ddate)+".txt").import@t()).conj().derive(ddate:d_date) |
3 | >db.update@i(A2, sales_summary,salesId,productId,amount,d_date) |
我们来逐行解释一下:
A1:简单声明了一个“序列”,其中包含三项a、b、c,这个就是我们知道的上报文件的结构;
A2:一句话解决了所有文件处理,所以得说的详细些:
首先,file("E:\\"+~+string(ddate)+".txt")匹配了E盘根目录下所有“.txt”后缀,且包含了销售日期ddate字符串的文本文件,其中ddate通过参数方式指定;
然后,A1.(file(……).import@t()),每一个匹配上面规则的文件(E盘根目录下的文本文件)都通过import@t()函数导入进来,并自动填充到A1声明的三项中,这里@t指明了导入文件时,自动忽略第一行标题;
继续,A1.(……).conj(),按照A1这个序列的结构导入每个文件后,将所有内容通过conj()函数串联合并成一个“大”序列;
最后A1.(……).conj().derive(ddate:d_date),在合并好的“大”序列中加上一列ddate日期。
这时,我们期望的合并文本的工作就已经完成了。
最后一行A3是一个我们经常会做的后续操作,就是将合并后的完整数据A2用函数update@i()更新到数据表sales_summary中,其中@i表示的是只进行插入。
这样,我们实际上用一句脚本就完成了多个文本文件数据的导入与合并工作。当然,要求高一些,还可以通过循环逻辑来控制参数ddate按照日期类型进行递增,从而导入好几天的数据,或者通过系统调度功能实现脚本的每天定时运行。这些就不是这篇文章关注的话题了。
接下来,我们从数据接收者的角色转变一下,作为数据提供者,我们如何从已有的数据,尤其是海量数据中,按约定的格式输出多个文本文件呢?
同样来看例子:
某品牌需要将各地销售过程中发展的会员信息按照产品线进行划分,进而分析策划相应的推广活动,由于客户可能会在不同的门店购买不同商品时进行重复注册,然后通过前面例子的方式进行汇总上报后合并,因此会出现用户分属不同分公司、或者产品线的情况,同时导致这个表的数据量较大,有400余万条记录。
样例数据如下:
源表(member)数据:
MONTH
(汇总上报时间) |
ID
(会员编号) |
TYPE
(类型) |
COMPANY
(分公司编号) |
DEPARTMENT
(所属部门) |
JOINTIME
(注册时间) |
PROGROUP
(产品线) |
201305 | 1009 | 1 | A | A1 | 201108 | DG |
201305 | 1009 | 1 | B | B1 | 201207 | PG |
201305 | 1009 | 1 | C | C1 | 201301 | SG |
201305 | 1009 | 1 | D | D1 | 201109 | SEG |
201305 | 1013 | 2 | C | C2 | 201302 | SG |
201305 | 1027 | 2 | A | A3 | 201007 | DG |
我们期望把表里的数据按产品线PROGROUP列的不同值导出多个TXT文件,并用产品线来命名,针对上述样例述数据,期望结果为:
DG.txt
MONTH ID TYPE COMPANY DEPARTMENT JOINTIME PROGROUP
201305 1009 1 A A1 201108 DG
201305 1027 2 A A3 201007 DG
PG.txt
MONTH ID TYPE COMPANY DEPARTMENT JOINTIME PROGROUP
201305 1009 1 B B1 201207 PG
SEG.txt
MONTH ID TYPE COMPANY DEPARTMENT JOINTIME PROGROUP
201305 1009 1 D D1 201109 SEG
SG.txt
MONTH ID TYPE COMPANY DEPARTMENT JOINTIME PROGROUP
201305 1009 1 C C1 201301 SG
201305 1013 2 C C2 201302 SG
虽然数据表是存在关系数据库中,可以用SQL语言进行访问呢和计算。但是,由于SQL语言中对数据分组后必须要有相应的聚合操作,因此,实现起来相对复杂,思路不是很直接。同时,由于数据量大,SQL中还需要使用游标分批向TXT文件进行追加写入,同样非常麻烦。
而对于集算器来说,利用它提供的分组游标功能,完成这个计算只需要三句代码,脚本如下:
A | B | |
1 | =db.cursor("SELECT * FROM TB1 ORDER BY PROGROUP ASC") | |
2 | for A1;PROGROUP | >file("E:\\"+A2.PROGROUP+".txt").export@t(A2) |
依旧是简洁明快的节奏!我们还是来分步说明一下:
A1:通过db.cursor()函数用一句SQL 创建一个数据库游标,其中是按照PROGROUP排序的。
A2:按照A1创建的游标中的PROGROUP值进行循环,事实上实现了分组功能
B2:A2每次循环执行一次,也就是针对每个产品线执行一次,读取一组PROGROUP记录,写到按照PROGROUP命名的TXT中。其中 file()函数按照A2中每次循环对应的PROGROUP值在E盘根目录下创建一个文本文件,export@t()函数将A2的值(就是每次循环读取的所有这个产品线的数据)连同字段名称作为标题(标题功能由@t指定),一起写入文件。
处理文本数据,只是集算器丰富能力中的一小部分,其中最重要的是体现了集算器设计思路的“直观”特点,通过丰富的预置函数和选项,让数据分析人员能够真正做到“所想即所得”,也就是用自然的方式表述需求,最后得到自然的解决方式。
最后,需要额外说明的一点是,上面例子中使用的都是文本文件,而对于同样普遍使用的Excel电子表格,集算器也提供了对等的支持,无非就是把import换成importxls,export换成exportxls就成了。