[Power BI] Table.AddRankColumnで月別ランク表を作る

（この記事は2022年8月20日の「[PBIJP Power Query 秘密特訓「虎の穴」炎の復活編 #17](https://powerbi.connpass.com/event/256575/)」で使用したものを若干アップグレードしています）

　Power BIで月別の成績をマトリックスで表示します。
　今回は、「理科年表（平成14年版），（国立天文台編））」を元に国土交通省が作成した県庁所在地の平年降水量の表を使用します。

https://www.mlit.go.jp/river/toukei_chousa/kasen/jiten/toukei/birn38p.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/a7eac709-9846-cfb2-8fdd-3c741b1490a9.png)

　この表から、月別に降水量の多い場所トップ10をマトリックスに表示させたいと思います。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/88a8770c-06e9-7609-d5ce-a57d132884ea.png"/>

# Webページのテーブル取り込み
　Webページからデータをインポートします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/8373f066-4054-0bb0-30df-af3261fb89a4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/3817bf48-c8f2-0c17-93d3-ea6148c80dc5.png)
　Webページを解析してデータを取得してくれる機能は素晴らしく使いやすいですが、これで出来上がるコードは以下のようになってしまいます。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/4b4748ca-8d76-c546-f461-fc647d3f7e71.png"/>
　このままでも問題ないのですが、もっとスマートにCSSセレクタを書きたいのと、 **Web.BrowserContents** を **Web.Contents** に書き換えたいと思います。Power BI Serviceで使用するとき、 **Web.BrowserContents** だと **On-premises data gateway** が必要になるためです。ところが、元のWebページはSJISで書かれているため、このままだと文字化けしてしまいます。そこで、 **Text.FromBinary** を使ってSJISをエンコードして読み込みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/ffebd737-a0cf-f6d2-6731-e4b69892cce5.png)

```text:テーブル取得
    Source = 
        Text.FromBinary(
            Web.Contents("https://www.mlit.go.jp/river/toukei_chousa/kasen/jiten/toukei/birn38p.html"),
            932  // sjisのページを読み込む
        ),
    ExtractedTableFromHtml = 
        Html.Table(
            Source, 
            {
                {"Column1", "TH"},
                {"Column2", "TH[colspan], TH:not([colspan]):nth-child(2)"}, 
                {"Column3", "TH[colspan] + TH:nth-child(2), TD:nth-child(3)"},
                {"Column4", "TH[colspan] ~ TH:nth-child(3), TD:nth-child(4)"},
                {"Column5", "TH[colspan] ~ TH:nth-child(4), TD:nth-child(5)"},
                {"Column6", "TH[colspan] ~ TH:nth-child(5), TD:nth-child(6)"},
                {"Column7", "TH[colspan] ~ TH:nth-child(6), TD:nth-child(7)"},
                {"Column8", "TH[colspan] ~ TH:nth-child(7), TD:nth-child(8)"},
                {"Column9", "TH[colspan] ~ TH:nth-child(8), TD:nth-child(9)"},
                {"Column10", "TH[colspan] ~ TH:nth-child(9), TD:nth-child(10)"},
                {"Column11", "TH[colspan] ~ TH:nth-child(10), TD:nth-child(11)"},
                {"Column12", "TH[colspan] ~ TH:nth-child(11), TD:nth-child(12)"},
                {"Column13", "TH[colspan] ~ TH:nth-child(12), TD:nth-child(13)"},
                {"Column14", "TH[colspan] ~ TH:nth-child(13), TD:nth-child(14)"},
                {"Column15", "TH[colspan] ~ TH:nth-child(14), TD:nth-child(15)"}
            },
            [RowSelector="TABLE:nth-child(7) > * > TR"]
        ),
```

https://qiita.com/spumoni/items/cad005c1deaa522531d8

# テーブルの整理
　最後の行にある合計行を削除 (**Table.RemoveLastN**) し、１行目をヘッダーに昇格 (**Table.PromoteHeaders**) させます。
　その後、地点名の文字の間にある空白を除去します。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/cdaba449-dedb-c97e-89f7-44c353a78670.png"/>

```text:Table.ReplaceValue構文
Table.ReplaceValue(
    table as table, 
    oldValue as any, 
    newValue as any, 
    replacer as function,   // Replacer.ReplaceText or Replacer.ReplaceValue
    columnsToSearch as list
) as table
```

```text:地点名の空白を除去
    ReplacedValue = 
        Table.ReplaceValue(
            PromotedHeaders,
            "　",
            "",
            Replacer.ReplaceText,
            {"都道府県（地点）_1"}
        ),
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/7e3fe1a9-4a88-72da-3f90-1de271446392.png)

# 月ごとの列をUNPIVOTする
　月ごとの数値が横に並んでいるので、これを月ごとのデータに変えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/29213015-2738-9801-671a-b1e5a230c01f.png)

```text:Table.UnpivotOtherColumns構文
Table.UnpivotOtherColumns(
    table as table, 
    pivotColumns as list, 
    attributeColumn as text, 
    valueColumn as text
) as table
```

```text:月ごとのデータに分ける
UnpivotedColumns = 
    Table.UnpivotOtherColumns(
        RemovedColumns, 
        {"地点"}, 
        "属性", 
        "値"
),
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/454d94b1-364d-c934-da40-d3ce028f7ff9.png)

# Table.Groupでデータを月ごとにまとめてテーブルを作る
　順位付けをするために、 **Table.Group** を使って月ごとのテーブルを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/2d08a63b-72a0-7554-a820-d138d1e42dc2.png)

```text:Table.Group構文
Table.Group(
    table as table, 
    key as any, 
    aggregatedColumns as list, 
    optional groupKind as nullable number,  // GroupKind.Local or GroupKind.Global
    optional comparer as nullable function  // 比較関数 Comparer.Equals など
) as table
```

```text:Table.Groupで月ごとにまとめる
GroupedRows = 
    Table.Group(
        RenamedColumns2, 
        {"月"}, 
        {
            {
                "月別", 
                each _, 
                type table [地点=text, 月=nullable number, 降水量=nullable number]
            }
        }
    ),
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/9551bb94-4c06-ef69-fac1-da695b90f0ad.png)

# Table.AddRankColumnでランクを付ける

https://powerquery.microsoft.com/en-us/blog/new-rank-column-transform-in-power-query-online/

　先の **Table.Group** の式の中に、ランク項目を追加する **Table.AddRankColum** 処理を加えます。

```text:Table.AddRankColumn構文
Table.AddRankColumn(
    table as table, 
    newColumnName as text, 
    comparisonCriteria as any, 
    optional options as nullable record
) as table
```

```text:Group処理の中で月別ランク列を追加
GroupedRows = 
    Table.Group(
        RenamedColumns2, 
        {"月"}, 
        {
            {
                "月別", 
                each // ランク付け項目の追加
                    Table.AddRankColumn(
                        _,
                        "ランク",
                        {"降水量", Order.Descending},
                        [RankKind = RankKind.Ordinal]
                    ),
                type table [地点=text, 月=text, 降水量=number, ランク= Int64.Type]
            }
        }
    ),
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/e6a143cf-2bca-7779-4658-3b1a1d65fc34.png)

　**RankKind** では、値の等しいアイテムが存在するときの処理を選択でき、利用目的に応じて指定を行います。

|名前|値|説明|
|:--|:--:|:--|
|RankKind.Competition|0|等しいアイテムには同一番号が振られ、次の番号が欠番になります。<br>(ex. 1, 1, 3, 4)|
|RankKind.Dense|1|等しいアイテムには同一番号が振られ、その次には続きの番号が振られます。<br>(ex. 1, 1, 2, 3)|
|RankKind.Ordinal|2|等しいアイテムにも固有の番号が振られます。<br>(ex. 1, 2, 3, 4)|

　今回は、ランク表で同じランクが発生しないよう **RankKind.Ordinal** を使用しています。

　「月」以外の項目を展開します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/59c7aab7-d75a-233f-69c4-03430bc840e5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/b787fe48-5b6e-3f7c-b6a4-e08d7e1a5174.png)

# ゼロ幅スペース (zero width space, ZWSP)
　このままマトリックスを作成すると、以下のように月の並びがおかしくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/54888949-eba6-f3bd-ffab-f63b68dcb073.png)

　数字であればきちんと並びますが、最後の年間合計の降水量も加えたいので、文字のまま１から始まるようにするために、２文字の場合は頭に「**ゼロ幅スペース**」 を入れます。この文字は、 **Character.FromNumber(8203)** あるいは **"#(200B)"** で表すことができます。8203の16進数表記が200Bになります。

```text:
ReplacedValue2 = 
    Table.TransformColumns(
        ChangedType2,
        {
            {
                "月",
                each 
                    if Text.Contains(_, "月") 
                    then Text.End(Text.Format("#(200B)#{0}", {_}), 3)   // 1月から9月にはゼロ幅スペースを頭につける 
                    else "年間"  // 「合計」は「年間」に置き換える
            }
        }
    )
```

https://ja.wikipedia.org/wiki/%E3%82%BC%E3%83%AD%E5%B9%85%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9

# マトリックスを作成する
　以上で作成したデータをマトリックスに書き出します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/98d09e1c-40f1-98ab-0b65-b3a89cf16614.png)

# 全コード

<details><summary>全コード</summary><div>

```text:全コード
let
    Source = 
        Text.FromBinary(
            Web.Contents("https://www.mlit.go.jp/river/toukei_chousa/kasen/jiten/toukei/birn38p.html"),
            932  // sjis
        ),
    ExtractedTableFromHtml = 
        Html.Table(
            Source, 
            {
                {"Column1", "TH"},
                {"Column2", "TH[colspan], TH:not([colspan]):nth-child(2)"}, 
                {"Column3", "TH[colspan] + TH:nth-child(2), TD:nth-child(3)"},
                {"Column4", "TH[colspan] ~ TH:nth-child(3), TD:nth-child(4)"},
                {"Column5", "TH[colspan] ~ TH:nth-child(4), TD:nth-child(5)"},
                {"Column6", "TH[colspan] ~ TH:nth-child(5), TD:nth-child(6)"},
                {"Column7", "TH[colspan] ~ TH:nth-child(6), TD:nth-child(7)"},
                {"Column8", "TH[colspan] ~ TH:nth-child(7), TD:nth-child(8)"},
                {"Column9", "TH[colspan] ~ TH:nth-child(8), TD:nth-child(9)"},
                {"Column10", "TH[colspan] ~ TH:nth-child(9), TD:nth-child(10)"},
                {"Column11", "TH[colspan] ~ TH:nth-child(10), TD:nth-child(11)"},
                {"Column12", "TH[colspan] ~ TH:nth-child(11), TD:nth-child(12)"},
                {"Column13", "TH[colspan] ~ TH:nth-child(12), TD:nth-child(13)"},
                {"Column14", "TH[colspan] ~ TH:nth-child(13), TD:nth-child(14)"},
                {"Column15", "TH[colspan] ~ TH:nth-child(14), TD:nth-child(15)"}
            },
            [RowSelector="TABLE:nth-child(7) > * > TR"]
        ),
    RemovedBottomRows = 
        Table.RemoveLastN(
            ExtractedTableFromHtml,
            1
        ),
    PromotedHeaders = Table.PromoteHeaders(RemovedBottomRows, [PromoteAllScalars=true]),
    ReplacedValue = Table.ReplaceValue(PromotedHeaders,"　","",Replacer.ReplaceText,{"都道府県（地点）_1"}),
    RenamedColumns = Table.RenameColumns(ReplacedValue,{{"都道府県（地点）_1", "地点"}}),
    RemovedColumns = Table.RemoveColumns(RenamedColumns,{"都道府県（地点）"}),
    UnpivotedColumns = Table.UnpivotOtherColumns(RemovedColumns, {"地点"}, "属性", "値"),
    ChangedType = Table.TransformColumnTypes(UnpivotedColumns,{{"値", type number}}),
    RenamedColumns2 = Table.RenameColumns(ChangedType,{{"属性", "月"}, {"値", "降水量"}}),
    GroupedRows = 
        Table.Group(
            RenamedColumns2, 
            {"月"}, 
            {
                {
                    "月別", 
                    each 
                        Table.AddRankColumn(
                            _,
                            "ランク",
                            {"降水量", Order.Descending},
                            [RankKind = RankKind.Ordinal]
                        ),
                    type table [地点=text, 月=text, 降水量=number, ランク= Int64.Type]
                }
            }
        ),
    Expanded = Table.ExpandTableColumn(GroupedRows, "月別", {"地点", "降水量", "ランク"}, {"地点", "降水量", "ランク"}),
    ReplacedValue2 = 
        Table.TransformColumns(
            Expanded,
            {
                {
                    "月",
                    each 
                        if Text.Contains(_, "月") 
                        then Text.End(Text.Format("#(200B)#{0}", {_}), 3) 
                        else "年間",
                    type text
                }
            }
        )
in
    ReplacedValue2
```
</div></details>