Table.Combine()可以抽象地概括为function(table as list, optional columns as any) as table,意思为该函数的第一个参数为串列,串列中的每一个元素都必须为表格,第二个参数是可选的并且数据的类型是没有限制的。Table.Combine()与SQL中的Union All很相似,都是纵向合并表格,当其中一个表格含有其他表格没有的列时,其他表格会被自动添加这一列,所有的值为null。Table.Combine()一般用于纵向合并Table.Group()和Table.Partition()分组的结果。
在实际工作经常会遇到纵向放置的表格,大体的情形如下:
上图中的表格(Case1)由上至下分别记录了商铺1至商铺5关于4种商品的销售记录,现在需要进行纵向合并,并注明是哪一间商铺的销售记录,即要求得到以下表格:
由原表格转化至上表的代码为:
let Grouping= Table.Group( Case1, "Product", { "GTable", each Table.TransformColumns( Table.AddIndexColumn( _, "StoreName", 1 ), { "StoreName", each Text.Combine( { "Store", Text.From( _ ) } ) } ) } ), UnoinAll= Table.Combine( Table.Column( Table.SelectRows( Grouping, each List.Contains( {"A".."D"}, [Product] ) ), "GTable" ), type table [ Product = text, Price = number, Quantity = Int64.Type, StoreName = text ] ) in UnoinAll
以上代码首先通过全域分组,把相同的产品分为一组,然后组内注明商铺的名称。由于按商品的名称进行全域分组会使空行和重复的列名称分别被分为一组,所以在使用Table.Combine()进行纵向合并前需要使用Table.SelectRows()过滤无关的行。
假如情况稍微复杂一点, 每一行会随机出现缺失值但是一定不会全部为空值,并且每一间商铺出售的商品的种类数目仍然相等。比如,销售记录的表格变成如下图所示的表格(Case2):
由于第五间商铺的第四行缺失了产品名称,如果仍然沿用之前的方法,这一行将不会出现在结果中。为了解决这一问题,需要使用以下的代码:
let Buffering = List.Buffer( Table.ColumnNames( Case2 ) ), RemoveRows= Table.RemoveMatchingRows( Case2, List.TransformMany( { List.Repeat( {null}, Table.ColumnCount( Case2 ) ), Buffering }, each { Buffering }, ( x, y ) ⇒ Record.FromList( x, y ) ), Buffering ), AddIndex = Table.AddIndexColumn( RemoveRows, "StoreName", 0 ), GroupCount = List.NonNullCount( List.Distinct( AddIndex[Product] ) ), Partition= Table.Partition( AddIndex, "StoreName", GroupCount, each _ ), UnionAll= Table.Combine( List.Transform( Partition, each Table.TransformColumns( _, { "StoreName", each Text.Combine( { "Store", Text.From( Number.IntegerDivide( _, GroupCount ) + 1 ) } ) } ) ), type table [ Product = text, Price = number, Quantity = Int64.Type, StoreName = text ] ) in UnionAll
为了移除三列都为空值的行以及含有标题的行,以上代码使用了List.TransformMany()构造了Table.RemoveMatchingRows()的第二个参数所需要的串列套记录的复合串列:{ [Product=null, Price=null, Quantity=null],[Product="Product", Price="Price", Quantity="Quantity"]}。移除了不需要的行后,每一间商铺的销售记录都会占据4行,所以为表格添加了指数列之后就能使用Table.Partition()把表格分为四组,每一种商品分为一组。接着,通过Table.TransformColumns()把每一组内的指数列转化为商铺名称。完成以上所有转化后,就可以使用Table.Combine()进行纵向合并。值得一提的是,以上Partition()步骤可以使用另一种相对没有那么简洁的方法实现,以下为该方法对应的代码:
Partition= List.Accumulate( List.Numbers( 1, GroupCount ), {}, ( x, y ) ⇒ List.Combine( { x, { Table.AlternateRows( Table.RemoveMatchingRows( AddIndex, Table.ToRecords( Table.Combine( x ) ), Buffering ), 1, GroupCount - y, 1 ) } } ) )
第二种情形仍然假设每一间商铺所卖的商品的种类的数量相等,假如每一间商铺所卖的商品不相同,以上的解法就失效了,这时就需要借助局部分组。比如,除了第五间商铺出现了缺失值,以下表格(Case3)的第四间商铺没有关于商品D的销售记录:
由于现在每一间商铺占据的行数不等,需要使用以下代码进行转换:
let Buffering = List.Buffer( Table.ColumnNames( Case3 ) ), Grouping= Table.Group( Case3, "Product", { "GTable", each Table.RemoveMatchingRows( _, List.TransformMany( { List.Repeat( {null}, Table.ColumnCount( Case3 ) ), Buffering }, each { Buffering }, ( x, y ) ⇒ Record.FromList( x, y ) ), Buffering ) }, GroupKind.Local, ( x, y ) ⇒ Number.From( y = null ) ), UnionAll= Table.Combine( List.TransformMany( { List.RemoveItems( Grouping[GTable], { #table( {}, {} ) } ) }, each List.Numbers( 0, List.Count( _ ) ), ( x, y ) ⇒ Table.AddColumn( x{y}, "StoreName", each Text.Combine( { "Store", Text.From( y + 1 ) } ) ) ), type table [ Product = text, Price = number, Quantity = Int64.Type, StoreName = text ] ) in UnionAll
由于局部分组后的结果并不完美,需要使用Table.RemoveMatchingRows()把三列都是null的行与含有标题的行同时移除。在这之后会出现一些空表格,以上代码选择了使用List.RemoveMatchingItems( 串列, { #table( {}, {} ) } )的组合移除了这些空表格。为分组后的每一个表格添加商铺的名称后,就可以使用Table.Combine()进行纵向的表格合并。
老师讲得很精彩!但从该案例上看,有点简单问题复杂化了。
向老师致敬。
我的思路先把标题降级,局部分组条件y="Product",提升标题,删除空行,合并
怎么看着比VBA要复杂呢
物有所长寸有所短
老师是为了运用Table.Combine才这么讲的,如果仅仅是为了解题实际很简单。1.删空行 2.降标题 3.用Table.Split将表格按每5行拆分成Table 4.将每个Table的第一行升为标题,5.添加索引列。6.将Table展开。
实际工作中我发现Table.Combine 当出现有多个表时会非常慢,比如第一参数的List中有30张表,每张表有1000行,合并后即3万行,那么速度就是大约需要30分钟以上.