Стандартне відхилення в LINQ


80

Чи моделює LINQ сукупну функцію SQL STDDEV() (стандартне відхилення)?

Якщо ні, то який найпростіший / найкращий спосіб його обчислити?

Приклад:

  SELECT test_id, AVERAGE(result) avg, STDDEV(result) std 
    FROM tests
GROUP BY test_id


@Steven, ви можете переглянути цю прийняту відповідь тут. Є проблеми з обраним на даний момент підходом, які люди, які не прокручують сторінку вниз і не читають далі, можуть не побачити.
Дрю Ноукс

Чому хтось хоче зробити це за допомогою LINQ ?
Ant_222

Відповіді:


98

Ви можете зробити власне розширення, розрахувавши його

public static class Extensions
{
    public static double StdDev(this IEnumerable<double> values)
    {
       double ret = 0;
       int count = values.Count();
       if (count  > 1)
       {
          //Compute the Average
          double avg = values.Average();

          //Perform the Sum of (value-avg)^2
          double sum = values.Sum(d => (d - avg) * (d - avg));

          //Put it all together
          ret = Math.Sqrt(sum / count);
       }
       return ret;
    }
}

Якщо у вас є вибірка сукупності, а не всієї сукупності, тоді вам слід використовувати ret = Math.Sqrt(sum / (count - 1));.

Кріс Беннетт перетворив на розширення від додавання стандартного відхилення до LINQ .


3
Я б зробив цей тест "values.Count ()> 1", тому що якщо він рівний 1, у вас буде помилка ділення на нуль під час обчислення значення, що повертається.
duffymo

3
Math.pow (d-avg, 2)? Я пропустив виклик функції та використав (d-avg) * (d-avg)
duffymo

2
Рядок ret = Math.Sqrt ((сума) / values.Count () - 1); відсутня дужка навколо значень.Count () - 1, це має бути ret = Math.Sqrt (sum / (values.Count () - 1));
Alex Peck,

1
Я шукав це , і це мені було потрібно якийсь час , щоб зрозуміти, як використовувати розширення, але ось спосіб застосувати методи , описані вище: stdev = g.Select(o => o.number).StdDev().
Ендрю Мао,

2
@ Євген Рожков - Чому ви прибрали - 1? У відповідності з цим- 1 потрібно.
Джон Міллс

61

Відповідь Динамі працює, але робить кілька проходів через дані, щоб отримати результат. Це метод одного проходу, який обчислює стандартне відхилення вибірки :

public static double StdDev(this IEnumerable<double> values)
{
    // ref: http://warrenseen.com/blog/2006/03/13/how-to-calculate-standard-deviation/
    double mean = 0.0;
    double sum = 0.0;
    double stdDev = 0.0;
    int n = 0;
    foreach (double val in values)
    {
        n++;
        double delta = val - mean;
        mean += delta / n;
        sum += delta * (val - mean);
    }
    if (1 < n)
        stdDev = Math.Sqrt(sum / (n - 1));

    return stdDev;
}

Це стандартне відхилення вибірки, оскільки воно ділиться на n - 1. Для нормального середньоквадратичного відхилення вам потрібно розділити на n.

Тут використовується метод Уелфорда, який має вищу числову точність порівняно з Average(x^2)-Average(x)^2методом.


1
Можливо, ви не повторювали всю послідовність більше одного разу, але ваш метод все одно здійснить два виклики GetEnumerator (що може спричинити складний запит SQL). Чому б не пропустити умову і не перевірити n в кінці циклу?
Гедеон Енгельберт,

Завдяки Гедеону, також знімається рівень вкладеності. Ви маєте рацію щодо SQL, він не має відношення до того, над чим я працюю, тому я не враховував його наслідки.
Девід Кларк,

3
Вам не вистачає визначення n. Також слід зазначити, що ділення суми на (n-1) замість n робить це зразком стандартного відхилення
Ніл

3
Щоб зробити це більш ретельно відтворювати метод SQL, я змінив this IEnumerable<double?> valuesі val in values.Where(val => val != null). Крім того, я зазначу, що цей метод (метод Велфорда) є більш точним і швидшим, ніж метод вище.
Ендрю Мао,

2
Я відредагував вашу відповідь, щоб було зрозуміло, що ви обчислюєте стандартне відхилення зразка , а не звичайне стандартне відхилення .
CodesInChaos

31

Це перетворює відповідь Девіда Кларка на розширення, яке відповідає тій самій формі, що й інші сукупні функції LINQ, такі як Середнє.

Використання: var stdev = data.StdDev(o => o.number)

public static class Extensions
{
    public static double StdDev<T>(this IEnumerable<T> list, Func<T, double> values)
    {
        // ref: /programming/2253874/linq-equivalent-for-standard-deviation
        // ref: http://warrenseen.com/blog/2006/03/13/how-to-calculate-standard-deviation/ 
        var mean = 0.0;
        var sum = 0.0;
        var stdDev = 0.0;
        var n = 0;
        foreach (var value in list.Select(values))
        {
            n++;
            var delta = value - mean;
            mean += delta / n;
            sum += delta * (value - mean);
        }
        if (1 < n)
            stdDev = Math.Sqrt(sum / (n - 1));

        return stdDev; 

    }
} 

1
Зверніть увагу, що Average/ Min/ Max/ etc мають перевантаження з функціями селектора та без них. У них також є перевантаження для цілісних типів, поплавків тощо.
Дрю Ноукс,


2

Прямо до точки (і C #> 6.0) відповідь "Динамісу" стає такою:

    public static double StdDev(this IEnumerable<double> values)
    {
        var count = values?.Count() ?? 0;
        if (count <= 1) return 0;

        var avg = values.Average();
        var sum = values.Sum(d => Math.Pow(d - avg, 2));

        return Math.Sqrt(sum / count);
    }

Редагувати 2020-08-27:

Я взяв коментарі @David Clarke, щоб зробити кілька тестів продуктивності, і ось такі результати:

    public static (double stdDev, double avg) StdDevFast(this List<double> values)
    {
        var count = values?.Count ?? 0;
        if (count <= 1) return (0, 0);

        var avg = GetAverage(values);
        var sum = GetSumOfSquareDiff(values, avg);

        return (Math.Sqrt(sum / count), avg);
    }

    private static double GetAverage(List<double> values)
    {
        double sum = 0.0;
        for (int i = 0; i < values.Count; i++) 
            sum += values[i];
        
        return sum / values.Count;
    }
    private static double GetSumOfSquareDiff(List<double> values, double avg)
    {
        double sum = 0.0;
        for (int i = 0; i < values.Count; i++)
        {
            var diff = values[i] - avg;
            sum += diff * diff;
        }
        return sum;
    }

Я перевірив це зі списком в один мільйон випадкових
подвоєнь, вихідна реалізація мала час роботи ~ 48 мс,
оптимізована продуктивність реалізації 2-3 мс,
тому це суттєве покращення.

Кілька цікавих деталей:
позбавлення від Math.Pow приносить приріст на 33 мс!
Список замість IEnumerable 6ms
вручну Середній розрахунок 4ms
For-Loops замість ForEach-Loops 2ms
Array замість List приносить лише поліпшення на ~ 2%, тому я пропустив це,
використовуючи single замість double не приносить нічого

Подальше зниження коду та використання goto (так GOTO ... не використовував це з 90-х років асемблер ...) замість for-loops не платить, дякую Богу!

Я протестував також паралельний розрахунок, це має сенс у списку> 200 000 пунктів. Здається, що апаратне та програмне забезпечення має багато ініціалізувати, і це для невеликих списків контрапродуктивно.

Всі тести проводились два рази поспіль, щоб позбутися часу розминки.


Врахуйте , що робить кілька проходів через дані при оцінці Count(), Average()і Sum(). Це нормально для малих значень, countале може мати вплив на ефективність, якщо countвона велика.
Девід Кларк,

@ david, тому найпростішим рішенням, на мій погляд, було б замінити підпис (this IList<double> values), тести продуктивності показали б вплив і те, скільки предметів суттєво впливає
Ернст Грейнер

Так , це не вирішує проблему - ці методи розширення ( Count, Average, Sum) кожен ітерацію колекції , так що ви все ще є три повних ітерацій для отримання результату.
Девід Кларк,

0
public static double StdDev(this IEnumerable<int> values, bool as_sample = false)
{
    var count = values.Count();
    if (count > 0) // check for divide by zero
    // Get the mean.
    double mean = values.Sum() / count;

    // Get the sum of the squares of the differences
    // between the values and the mean.
    var squares_query =
        from int value in values
        select (value - mean) * (value - mean);
    double sum_of_squares = squares_query.Sum();
    return Math.Sqrt(sum_of_squares / (count - (as_sample ? 1 : 0)))
}

Зауважте, це все ще робить кілька проходів через дані - добре, якщо невеликий набір даних, але не підходить для великих значень count.
Девід Кларк,

0

Прості 4 рядки, я використовував список пар, але один міг використовувати IEnumerable<int> values

public static double GetStandardDeviation(List<double> values)
{
    double avg = values.Average();
    double sum = values.Sum(v => (v - avg) * (v - avg));
    double denominator = values.Count - 1;
    return denominator > 0.0 ? Math.Sqrt(sum / denominator) : -1;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.