LoginSignup
1

More than 3 years have passed since last update.

LINQでMedian(メディアン)

Last updated at Posted at 2020-03-28

なぜこのメソッドがない?

LINQを使っていてMax、Averageなどの統計値を出したい、という場面がよくありますが、
なぜこれが標準で計算できないんだ!
と個人的に思う処理に

  1. メディアン (Median)
  2. 四分位点
  3. 標準偏差 (Std)

等がありました。
なので、まずは最も使用頻度が高いメディアンを実装してみました。

やりたいこと

その1:シンプルかつ違和感のない処理
例えば和を求めたい場合は、下のコードのように標準ライブラリのSum()を使って

List<int> iList = new List<int> { 1, 2, 3, 4, 5 };
int sum = iList.Sum()

のような形で実現可能です。
これに合わせて

List<int> iList = new List<int> { 1, 2, 3, 4, 5 };
int median = iList.Median()

のようにシンプルに算出したいです。

その2:型依存性なし
入力がint型のとき、double型のとき、long型のとき・・
と全て実装すると可読性が著しく落ちるので、
色んな型を一括で処理できるようにしたいです。

実現するには?

その1を実現するためには「拡張メソッド」
その2を実現するためには「ジェネリック」
を使用すれば良いらしいです
(恥ずかしながら今回調べて初めて知りました)

こちらを参考にさせて頂きました

コード

アルゴリズム自体はこちらを参照させて頂きました。

本体(DateTime型とそれ以外で処理を分けています)

public static class LinQCustomMethods
    {
        // メディアン算出メソッド(Generics)
        public static T Median<T>(this IEnumerable<T> src)
        {
            //ジェネリックの四則演算用クラス
            var ao = new ArithmeticOperation<T>();
            //昇順ソート
            var sorted = src.OrderBy(a => a).ToArray();
            if (!sorted.Any())
            {
                throw new InvalidOperationException("Cannot compute median for an empty set.");
            }
            int medianIndex = sorted.Length / 2;
            //要素数が偶数のとき、真ん中の2要素の平均を出力
            if (sorted.Length % 2 == 0)
            {
                //四則演算可能な時のみ算出
                if (ao.ArithmeticOperatable(typeof(T)))
                {
                    return ao.Divide(ao.Add(sorted[medianIndex], sorted[medianIndex - 1]), (T)(object)2.0);
                }
                else throw new InvalidOperationException("Cannot compute arithmetic operation");
            }
            //奇数のときは、真ん中の値を出力
            else
            {
                return sorted[medianIndex];
            }
        }

        // メディアン算出(DateTime型のみ別メソッド)
        public static DateTime Median(this IEnumerable<DateTime> src)
        {
            //昇順ソート
            var sorted = src.OrderBy(a => a).ToArray();
            if (!sorted.Any())
            {
                throw new InvalidOperationException("Cannot compute median for an empty set.");
            }
            int medianIndex = sorted.Length / 2;
            //要素数が偶数のとき、真ん中の2要素の平均を出力
            if (sorted.Length % 2 == 0)
            {
                return sorted[medianIndex] + new TimeSpan((sorted[medianIndex - 1] - sorted[medianIndex]).Ticks / 2);
            }
            //奇数のときは、真ん中の値を出力
            else
            {
                return sorted[medianIndex];
            }
        }
    }

ジェネリックの四則演算用クラス
こちらを参考にさせて頂きました

//ジェネリック四則演算用クラス
    public class ArithmeticOperation<T>
    {
        /// <summary>
        /// 四則演算適用可能かを判定
        /// </summary>
        /// <param name="src">判定したいタイプ</param>
        /// <returns></returns>
        public bool ArithmeticOperatable(Type srcType)
        {
            //四則演算可能な型の一覧
            var availableT = new Type[]
            {
            typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(long), typeof(ulong), typeof(byte),
            typeof(decimal), typeof(double)
            };
            if (availableT.Contains(srcType)) return true;
            else return false;
        }

        /// <summary>
        /// 四則演算可能なクラスに対しての処理
        /// </summary>
        public ArithmeticOperation()
        {
            var availableT = new Type[]
            {
            typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(long), typeof(ulong), typeof(byte),
            typeof(decimal), typeof(double)
            };
            if (!availableT.Contains(typeof(T)))
            {
                throw new NotSupportedException();
            }
            var p1 = Expression.Parameter(typeof(T));
            var p2 = Expression.Parameter(typeof(T));
            Add = Expression.Lambda<Func<T, T, T>>(Expression.Add(p1, p2), p1, p2).Compile();
            Subtract = Expression.Lambda<Func<T, T, T>>(Expression.Subtract(p1, p2), p1, p2).Compile();
            Multiply = Expression.Lambda<Func<T, T, T>>(Expression.Multiply(p1, p2), p1, p2).Compile();
            Divide = Expression.Lambda<Func<T, T, T>>(Expression.Divide(p1, p2), p1, p2).Compile();
            Modulo = Expression.Lambda<Func<T, T, T>>(Expression.Modulo(p1, p2), p1, p2).Compile();
            Equal = Expression.Lambda<Func<T, T, bool>>(Expression.Equal(p1, p2), p1, p2).Compile();
            GreaterThan = Expression.Lambda<Func<T, T, bool>>(Expression.GreaterThan(p1, p2), p1, p2).Compile();
            GreaterThanOrEqual = Expression.Lambda<Func<T, T, bool>>(Expression.GreaterThanOrEqual(p1, p2), p1, p2).Compile();
            LessThan = Expression.Lambda<Func<T, T, bool>>(Expression.LessThan(p1, p2), p1, p2).Compile();
            LessThanOrEqual = Expression.Lambda<Func<T, T, bool>>(Expression.LessThanOrEqual(p1, p2), p1, p2).Compile();
        }
        public Func<T, T, T> Add { get; private set; }
        public Func<T, T, T> Subtract { get; private set; }
        public Func<T, T, T> Multiply { get; private set; }
        public Func<T, T, T> Divide { get; private set; }
        public Func<T, T, T> Modulo { get; private set; }
        public Func<T, T, bool> Equal { get; private set; }
        public Func<T, T, bool> GreaterThan { get; private set; }
        public Func<T, T, bool> GreaterThanOrEqual { get; private set; }
        public Func<T, T, bool> LessThan { get; private set; }
        public Func<T, T, bool> LessThanOrEqual { get; private set; }
    }

結果

List<int> iList = new List<int> { 1, 2, 3, 4, 5 };
List<double> dList = new List<double> { 3.2, 3.5, 3.6, 4 };
List<DateTime> dtList = new List<DateTime> { new DateTime(2020, 3, 24), new DateTime(2020, 3, 25), new DateTime(2020, 3, 26), new DateTime(2020, 3, 27) };
//メディアン(int)
Console.WriteLine(iList.Median().ToString());
//メディアン(double)
Console.WriteLine(dList.Median().ToString());
//メディアン(DateTime)
Console.WriteLine(dtList.Median().ToString());
3
3.55
2020/03/25 12:00:00

正常にメディアンが出せていそうです

苦労したところ

その1. Generics(T)型の足し算
上のコード中の「要素数が偶数のとき、真ん中の2要素の平均を出力」とありますが、
平均を出すためには足し算、割り算等の四則演算が必要です。
Generics型は「a + b」みたいな形で四則演算ができないので、
こちらを参考に、四則演算クラスを追加しました。

※Average()メソッドを使えばGenerics型のまま平均を出せますが、
今後Generic型の処理で四則演算を使いたい場面が多そうなので、クラスとして作成しました。

その2. DateTime型の扱い
DateTime型は上記四則演算クラスが適用できないので、別途メソッドを作りました。
DateTime型はMath.Maxクラスも使えなかったりと、色々不便なところが多いですね。

その3. Genericsのキャスト
平均を出す際に「2」で割る必要がありますが、
そのままだとGenerics(T)型をint型で割れずにエラーが出るため、
「2」をGenerics(T)型にキャストする必要があります。
こちらを参考に、
「2.0」としてdouble型で宣言 → object型にキャスト → Generics(T)型にキャスト
の順で、Generics(T)型へのキャストが実現できました。

そして

上記を実装したあとに、こんなものがあることに気付きました(泣)
リンク

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
1