[C#]拡張メソッドで、クロス表を作る

# クロス表

LINQをつかってサクサク集計処理が書けるC#ですが、標準にあるLINQでは、集計できるのですが、うまく表示してくれないものに、クロス表があります。

クロス表とはこういうものね。

|        |       要素α　|    要素β    |
|:------|:-------:|:-----:|
| 要素A             |              100 |        200        |
| 要素B           |            40 |       24       |
| 要素C             |              30 |        34        |

縦と横の要素を掛け合わせた値が入るやつです。
エクセルではピポットテーブルをつかってやる、あれです。
二つの変数間の関係を見るうえで、基本かつ最強であるものです。

こういうクロス集計は、クエリ的には簡単です。

```
list.GroupBy(n=> new {n.Item1,n.Item2 })
 .Select(n=> new {n.Key.Item1,n.Key.Item2,Count= n.Count()});

```

二つの要素をGroupByのKeyにすればいいだけです。

でも、上のクエリでの出力(JSON)は、

```
[
 { Item1 : "要素A" ,Item2 : "要素α" ,Count : 100},
 { Item1 : "要素A" ,Item2 : "要素β" ,Count : 200},
....
]

```

という形で、二つの値のペアと、カウントという形になります。
これは、可読性が良くありません。クロス表がほしい。

# 前提

以降、昔に書いた記事、
[TestProjectはC#のスクリプト実行環境](https://qiita.com/kiichi54321/items/33238ec7e8a5c95b5026)
で書いたように、テストプロジェクトで使うことを前提としています。

テストプロジェクトは、メソッド一つを簡単に実行できるわ、時間計測もしてくれるわ、デバッグもできるわで、超便利です。

そして、このような拡張メソッドを定義しています。

```
        public static string ConsoleWriteLine(this string text)
        {
            System.Console.WriteLine(text);
            return text;
        }
```

実行結果で、コンソール出力をみれるので、テストプロジェクトは便利です。

また、拡張メソッドで、Nullチェックを本来しないとだめですが、記事の可読性のため飛ばしています。

# クロス表を作る拡張メソッド

こういう拡張メソッドを作りました。TSV形式のテキストに変換します。タプル構文を使います。
タプル構文とDictionaryの相性はとてもいいです。

```csharp
        /// <summary>
        /// Dictionary<(T,T1),T2> をクロス表に変換する。
        /// </summary>
        /// <returns></returns>
        public static string ToCrossTable<T, T1, T2>(this Dictionary<(T row, T1 colm), T2> dic, 
　　　　　　　　　　　　　　     Func<T, string> row_label ,
　　　　　　　　　　　　　　     Func<T1, string> colm_label)
        {
            HashSet<T> hash_row = new HashSet<T>();
            HashSet<T1> hash_colm = new HashSet<T1>();

            foreach (var item in dic.Keys)
            {
                hash_row.Add(item.row);
                hash_colm.Add(item.colm);
            }

            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("\t");
            foreach (var item in hash_colm.OrderBy(n => n))
            {
                stringBuilder.Append($"{colm_label(item)}\t");
            }
            stringBuilder.AppendLine();

            foreach (var row in hash_row.OrderBy(n => n))
            {
                stringBuilder.Append($"{row_label(row)}\t");
                foreach (var colm in hash_colm.OrderBy(n => n))
                {
                    if (dic.ContainsKey((row, colm)))
                    {
                        stringBuilder.Append(dic[(row, colm)]);
                    }
                    stringBuilder.Append("\t");
                }
                stringBuilder.AppendLine();
            }
            return stringBuilder.ToString();
        }

        public static string ToCrossTable<T,T1,T2>(this Dictionary<(T,T1),T2> dic)
        {
            return ToCrossTable(dic, n => n.ToString(), n => n.ToString());
        }
```


# 基本的な使い方

```csharp
list.GroupBy(n=> (n.Item1,n.Item2))
　.ToDictionary(n=>n.Key,n=> n.Count())
　.ToCrossTable()
　.ConsoleWriteLine();
```

これで、クロス表がコンソール出力できます。
TSVなので、結果をエクセルに張り付ければ、エクセルで見れます。

初めは、これで充分とか思っていたのですが、
文字列のソートでは、10以上の数値を含むと適切にソートをしてくれない問題がありました。

```
1月,10月,11月,12月,2月,3月,4月,5月,6月,7月,8月,9月
```

文字列あるあるです。そのため、

```csharp
list.GroupBy(n=> (n.Month,n.Day))
　.ToDictionary(n=>n.Key,n=> n.Count())
　.ToCrossTable(n=>$"{n}月",n=> $"{n}日" )
　.ConsoleWriteLine();
```

という形で、ソート可能な数値で、GroupByをしてから、文字列をあとから作る、というモードも用意しました。
これで、さくっと、クロス表が作れます。

# 応用編　Dictionary<(row,col),T>　へ変換する

Dictionary<(row,col),T> の形を作れば、クロス表を作れるのが分かりました。
つまり、Dictionary<(row,col),T> の形に変形すれば、なんでもクロス表が作れるということです。

```csharp
var results = list.GroupBy(n=>n.Key)
               .Select(x => new{
                  x.Key,
                  ItemCountDic = x.Select(n=>n.Item).GroupBy(n=>n).ToDictonary(n=>n.Key,n=>n.Count()),
                  UserCountDic = x.Select(n=>n.UserName).GroupBy(n=>n).ToDictonary(n=>n.Key,n=>n.Count()),
               }).ToArray();
```

こういう形で、一つの変数でまずGroupByをして、そのあとで、対象を変えながら、GroupByをするということはよくやります。
形式的には、オブジェクトの中に、Dictionaryがあるパターンです。

これも、クロス表にしたいですよね。
このような拡張メソッドを定義しておきます。

```csharp
        public static Dictionary<(T1,T2),T3> ToTupleDic<T,T1,T2,T3>(this IEnumerable<T> list, 
                                  Func<T,T1> row_label,
                                  Func<T,Dictionary<T2,T3>> colm_dic)
        {
            return list.SelectMany(n => colm_dic(n).ToDictionary(m => (row_label(n), m.Key), m => m.Value))
                .ToDictionary(n => n.Key, n => n.Value);
        }
```


SelectManyの中で、ペアのDicを展開することで、Dictionary<(T1,T2),T3>を作ります。
これは、SelectManyの使い方のちょいテクですが、毎回考えるのが面倒なので、拡張メソッド化しておくと便利です。

使い方はこんな感じです。

```csharp
results.ToTupleDic(n=>n.Key,n=>n.ItemCountDic)
  .ToCrossTable()
　.ConsoleWriteLine();
```

楽々クロス表出力。

# 応用編　オブジェクトをDictionaryに変換する拡張メソッド

オブジェクトの中に、Dictionaryがある形でもクロス表が作れることが分かりました。
Dictionaryさえ作ってしまえば、クロス表が作れるということです。

```csharp
        public static Dictionary<string,object> ToPropertiesDic(this object obj)
        {
            return obj.GetType().GetProperties().ToDictionary(n=>n.Name,n=> n.GetValue(obj));
        }
```

このような形で、オブジェクトをDictionaryに変換する拡張メソッドを作ると、

```csharp
results.ToTupleDic(n=>n.Key,n=>n.Item.ToPropertiesDic())
  .ToCrossTable()
　.ConsoleWriteLine();
```

このような形でクロス表が作れます。

# 応用編　割合に変換する拡張メソッド

実数を見るより、割合を見た方が分かりやすいことはあります。
こういう拡張メソッドをサクッと定義しましょう。

```
        public static Dictionary<T, double> ToRate<T>(this Dictionary<T, int> dic)
        {
            var all_count = dic.Sum(n => n.Value);
            return dic.ToDictionary(n => n.Key, n => n.Value / (double)all_count);
        }
```

使い方

```csharp
list.GroupBy(n=> (n.Item1,n.Item2))
　.ToDictionary(n=>n.Key,n=> n.Count())
  .ToRate()
　.ToCrossTable()
　.ConsoleWriteLine();
```

とToRateを入れるだけで、割合に変換できます。

# おまけ。Tsvを出力する拡張メソッド

```csharp

        public static string ToTsv<T>(this IEnumerable<T> list)
        {
            System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
            stringBuilder.AppendLine(string.Join("\t", typeof(T).GetProperties().Select(n => n.Name).ToArray()));

            foreach (var item in list)
            {
                stringBuilder.AppendLine(string.Join("\t", typeof(T).GetProperties().Select(n => n.GetValue(item)?.ToString())));
            }
            return stringBuilder.ToString();
        }
```

クロス表ではないのですが、この拡張メソッドもとても便利です。

使い方

```csharp
list.ToTsv().ConsoleWriteLine();
```
楽ちん出力。オブジェクト内オブジェクトがなければ、なんでも出力できます。
オブジェクト内オブジェクトがあるものだと、ToJSON() を定義して使うといいです。

# まとめ

テストプロジェクトは、スクリプト実行環境で、とても便利。
メソッドチェーン楽しい。
さくさくクロス表が作れてデータ集計、超はかどる。