LoginSignup
5
5

More than 5 years have passed since last update.

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

Posted at

クロス表

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#のスクリプト実行環境
で書いたように、テストプロジェクトで使うことを前提としています。

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

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

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

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

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

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

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

        /// <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());
        }

基本的な使い方

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月

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

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> の形に変形すれば、なんでもクロス表が作れるということです。

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があるパターンです。

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

        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の使い方のちょいテクですが、毎回考えるのが面倒なので、拡張メソッド化しておくと便利です。

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

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

楽々クロス表出力。

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

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

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

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

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);
        }

使い方

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

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

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


        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();
        }

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

使い方

list.ToTsv().ConsoleWriteLine();

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

まとめ

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

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5