餐厅星评排序

老王经过自己的下属小明时发现小明在办公时间玩手机,所以给他安排了为数据排序的工作。这份数据(DB)记录了10家评分机构对1000家餐厅的评价,最高的评价为5星,最低为1星。老王让小明根据每一家餐厅获得的总星数以行为单位进行升序排列,并且根据每一家评分机构给出的总星数以列为单位进行升序排列。

小明用以下的M语言代码很快地按照老王的要求完成了排序,然后继续做自己的事情去了。

let
    fnCustomSorting= 
        ( w as list ) ⇒
            List.Sort(
                List.Skip( w ),
                each 
                    Text.Length(
                        Text.Combine(
                            List.Skip(_)
                        )
                    )
            ),
    DHeaders = Table.DemoteHeaders( DB ),
    ToRows = List.Buffer( Table.ToRows( DHeaders ) ),
    CustomSort1 = fnCustomSorting(ToRows),
    Add1stList = List.Combine( { { ToRows{ 0 } }, CustomSort1 } ),
    Zipping = List.Buffer( List.Zip( Add1stList ) ),
    CustomSort2 = fnCustomSorting( Zipping ),
    Add1stCol = List.Combine( { { Zipping{ 0 } }, CustomSort2 } ),
    FromCols = Table.FromColumns( Add1stCol ),
    PHeaders = Table.PromoteHeaders( FromCols, [PromoteAllScalars=true] ),
    
    DataType=
        Table.TransformColumnTypes(
            PHeaders,
            List.Combine(
                {
                    { {"餐厅序号", Int64.Type} },
                    List.Zip(
                        {
                            List.Skip( Table.ColumnNames( PHeaders ) ),
                            List.Repeat( {type text}, 10 )
                        }
                    )
                }
            )
        )
in
    DataType

以上代码主要利用了List.Sort()可以根据以串列中的元素为自变量的函数结果进行排序的特点(详情可以回顾如何根据字符长度进行排序)定义了能够对内嵌串列的总字符数进行排序的fnCustomSorting()。要对餐厅获得的总星数进行排序,首先需要对数据以行为单位分解为多个内嵌的串列,这个任务由Table.ToRows()完成。这个步骤所得的复合串列传入fnCustomSorting()就完成了纵向的排序要求。然后要对评分机构给出的总星数进行排序,这个过程需要使用List.Zip()达到行列转换的效果(详情请回顾通过List.Zip()实现行列转换),得到的复合串列传入到自定义的fnCustomSorting()就完成了横向的排序。最后使用Table.FromColumns()把复合串列还原为表格就是老王要求的排序结果。

以下为M爱好者西瓜对同一问题精妙的解答:

let
    一排序=
        Table.Sort(
            DB,
            each
                Text.Length(
                    Text.Combine(
                        List.Skip(
                            Record.FieldValues( _ )
                        )
                    )
                )
        ),
    排序字段名=
        List.Zip(
            List.Sort(
                List.Transform(
                    List.Skip(
                        Table.ColumnNames( 一排序 )
                    ),
                    each
                        {
                            _,
                            Text.Length(
                                Text.Combine(
                                    Table.Column(
                                        DB,
                                        _
                                    )
                                )
                            )
                        }
                ),
                { each _{ 1 }, 0 }
            )
        ){0},
    自定义1 = Table.ReorderColumns( 一排序, 排序字段名 )
in
    自定义1

在西瓜的解答中,纵向的排序是通过Table.Sort()完成,而横向的排序是间接地对列进行排序,通过List.Transform构造{{列名称, 该列总星数}..{列名称, 该列总星数}}再使用List.Sort()以列的总星数为依据进行升序排序,最后使用List.Zip(){0}取出排好序的列名称,代入Table.ReorderColumns()就是需要的结果了。第一种方法和西瓜的解法折中一下,也可以达到相同的目的:

let
    HSorting=
        List.Zip(
            List.Sort(
                List.Skip(
                    Table.ToColumns(
                        Table.DemoteHeaders( DB )
                    )
                ),
                each
                    Text.Length(
                        Text.Combine(
                            List.Skip( _ )
                        )
                    )
            )
        ){ 0 },
    Reordering = Table.ReorderColumns( DB, HSorting ),
    VSorting=
        Table.Sort(
            Reordering,
            each
                Text.Length(
                    Text.Combine(
                        List.Skip(
                            Record.FieldValues( _ )
                        )
                    )
                )
        ) 
in
    VSorting

在西瓜的解答中,纵向排序那部分是目前的最优解,所以这一部分没有变动。以列为单位的排序作了一点变动,首先把标题降级为首行,然后以列为单位把表格分解为{{}..{}}形式的复合串列。在移除不参与排序的第一个元素后,通过List.Sort()依据获得的总星数进行升序排序,然后也是使用List.Zip(){ 0 }取出标题。最后,传入Table.ReorderColumns()完成对列的排序。实际上,这个问题也可以利用Table.Profile()的特点完成解答:

let
    Profiling=
        Table.Profile(
            DB,
            {
                {
                    "Asterisk",
                    each Type.Is( _, type nullable text ),
                    each Text.Length( Text.Combine( _ ) )
                }
            }
        ),
    HSorting=
        Table.ReorderColumns(
            DB,
            Table.Column(
                Table.Sort(
                    Profiling,
                    "Asterisk"
                ),
                "Column"
            )
        ),
    VSorting=
        Table.Sort(
            HSorting,
            each
                Text.Length(
                    Text.Combine(
                        List.Skip(
                            Record.FieldValues( _ )
                        )
                    )
                )
        )
in
    VSorting

与上一种办法一样,保留西瓜纵向排序的部分。Table.Profile()的主要任务是生成每一列的总星数的汇总列,然后对该列进行升序排序就刚好能得到需要的有关于排序后的列之间的相对位置,传入Table.ReorderColumns()就完成横向的排序。

附件

8 Replies to “餐厅星评排序”

  1. 我也做了一个代码.
    let
    Source = Excel.CurrentWorkbook(){[Name="Table4"]}[Content],
    一排序 = Table.Sort( Source,each Text.Length(Text.Combine(List.Skip(Record.FieldValues(_),1))) ),
    排序字段名 = List.Zip( List.Sort( List.Transform( List.Skip(Table.ColumnNames(一排序)),each {_,Text.Length(Text.Combine(Table.Column(Source,_)))} ),{each _{1},0} )){0},
    自定义1 = Table.ReorderColumns(一排序, 排序字段名)
    in
    自定义1

  2. 方法很多啊
    let
    源 = Excel.CurrentWorkbook(){[Name="Table4"]}[Content],
    自定义1 = Table.AddColumn(源,"总星数",each Text.Length(Text.Combine(List.Range(Record.ToList(_),1,1000)))),
    排序的行 = Table.Sort(自定义1,{{"总星数", Order.Ascending}})
    in
    排序的行

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注