Odchylenie standardowe w LINQ

80

Czy LINQ modeluje zagregowaną funkcję SQL STDDEV() (odchylenie standardowe)?

Jeśli nie, jaki jest najprostszy / sprawdzony sposób obliczenia tego?

Przykład:

  SELECT test_id, AVERAGE(result) avg, STDDEV(result) std 
    FROM tests
GROUP BY test_id
Steven
źródło
@Steven, możesz ponownie odwiedzić zaakceptowaną odpowiedź tutaj. Występują problemy z aktualnie wybranym podejściem, których osoby, które nie przewijają w dół i nie czytają dalej, mogą nie widzieć.
Drew Noakes
Dlaczego ktoś miałby chcieć to zrobić za pomocą LINQ ?
Ant_222,

Odpowiedzi:

98

Możesz stworzyć własne rozszerzenie obliczające to

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

Jeśli masz próbkę populacji, a nie całą populację, powinieneś użyć ret = Math.Sqrt(sum / (count - 1));.

Przekształcony w rozszerzenie z dodawania odchylenia standardowego do LINQ przez Chrisa Bennetta .

Dynami Le Savard
źródło
3
Zrobiłbym ten test „values.Count ()> 1”, ponieważ jeśli jest to dokładnie 1, podczas obliczania zwracanej wartości wystąpi błąd dzielenia przez zero.
duffymo
3
Math.pow (d-avg, 2)? Pominąłbym wywołanie funkcji i użył (d-avg) * (d-avg)
duffymo
2
Wiersz ret = Math.Sqrt ((sum) / values.Count () - 1); brakuje nawiasów wokół value.Count () - 1, powinno to być ret = Math.Sqrt (sum / (values.Count () - 1));
Alex Peck
1
Szukałem tego i zajęło mi trochę czasu, aby dowiedzieć się, jak używać rozszerzenia, ale tutaj jest sposób, aby zastosować metody podane powyżej: stdev = g.Select(o => o.number).StdDev().
Andrew Mao,
2
@Yevgeniy Rozhkov - Dlaczego usunąłeś - 1? Według tego- 1 jest wymagane.
John Mills,
61

Odpowiedź Dynami działa, ale dokonuje wielu przejść przez dane, aby uzyskać wynik. Jest to metoda jednoprzebiegowa, która oblicza odchylenie standardowe próbki :

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

Jest to odchylenie standardowe próbki, ponieważ dzieli się przez n - 1. W przypadku normalnego odchylenia standardowego należy nzamiast tego podzielić przez .

Wykorzystuje to metodę Welforda, która ma większą dokładność numeryczną w porównaniu z tą Average(x^2)-Average(x)^2metodą.

David Clarke
źródło
1
Być może nie wykonałeś iteracji całej sekwencji więcej niż raz, ale Twoja metoda nadal będzie wykonywać dwa wywołania GetEnumerator (co może wyzwalać złożone zapytanie SQL). Dlaczego nie pominąć warunku i sprawdzić n na końcu pętli?
Gideon Engelber,
Dzięki Gideon, usuwa również poziom zagnieżdżenia. Masz rację co do języka SQL, nie ma to związku z tym, nad czym pracuję, więc nie rozważałem konsekwencji.
David Clarke
3
Brakuje Ci definicji n. Należy również zauważyć, że podzielenie sumy przez (n-1) zamiast n daje przykładowe odchylenie standardowe
Neil
3
Aby dokładniej odtworzyć metodę SQL, zmieniłem this IEnumerable<double?> valuesi val in values.Where(val => val != null). Zaznaczę również, że ta metoda (metoda Welforda) jest dokładniejsza i szybsza niż metoda powyżej.
Andrew Mao
2
Zredagowałem twoją odpowiedź, aby było jasne, że obliczasz odchylenie standardowe próbki , a nie normalne odchylenie standardowe .
CodesInChaos
31

To konwertuje odpowiedź Davida Clarke'a na rozszerzenie, które ma tę samą postać, co inne funkcje agregujące LINQ, takie jak Średnia.

Sposób użycia będzie: 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; 

    }
} 
Will Mathies
źródło
1
Zauważ, że Average/ Min/ Max/ etc mają przeciążenia zi bez funkcji selektora. Mają też przeciążenia dla typów całkowitych, zmiennoprzecinkowych itp.
Drew Noakes,
5
var stddev = Math.Sqrt(data.Average(z=>z*z)-Math.Pow(data.Average(),2));
Vitas
źródło
2

Prosto do rzeczy (i C #> 6,0), odpowiedź Dynamis wygląda tak:

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

Edycja 2020-08-27:

Wziąłem komentarze @David Clarke, aby wykonać testy wydajności i oto wyniki:

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

Przetestowałem to z listą miliona losowych dubli
. Oryginalna implementacja miała czas działania ~ 48 ms,
a implementacja zoptymalizowana pod kątem wydajności 2-3 ms,
więc jest to znacząca poprawa.

Kilka interesujących szczegółów:
pozbycie się Math.Pow zapewnia przyspieszenie o 33 ms!
List zamiast IEnumerable 6ms
ręcznie Obliczanie średniej 4ms
For-loop zamiast ForEach-loop 2ms
Array zamiast List przynosi tylko poprawę o ~ 2%, więc pominąłem to
używając single zamiast double nic nie daje

Dalsze obniżanie kodu i używanie goto (tak, GOTO ... nie używałem tego od asemblera z lat 90 ...) zamiast pętli for nie opłaca się, dzięki Bogu!

Przetestowałem również obliczenia równoległe, ma to sens na liście> 200.000 pozycji. Wygląda na to, że sprzęt i oprogramowanie wymagają dużo inicjalizacji i jest to dla małych list nieproduktywne.

Wszystkie testy zostały wykonane dwa razy z rzędu, aby pozbyć się czasu rozgrzewki.

Ernst Greiner
źródło
Bądź świadomy tego sprawia, przechodzi przez wiele danych przy ocenie Count(), Average()oraz Sum(). To jest w porządku w przypadku małych wartości, countale może wpłynąć na wydajność, jeśli countjest duże.
David Clarke
@ david, więc moim zdaniem najprostszym rozwiązaniem byłoby zastąpienie sygnatury (this IList<double> values), testy wydajności pokazałyby wpływ, a ile elementów robi znaczącą różnicę
Ernst Greiner
Tak, że nie rozwiązuje problemu - te metody rozszerzenie ( Count, Average, Sum) każda iteracyjne kolekcji więc jeszcze trzy pełne powtórzeń do wytworzenia rezultatu.
David Clarke
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)))
}
duc14s
źródło
Zwróć uwagę, że nadal powoduje to wielokrotne przepuszczanie danych - w porządku, jeśli jest to mały zestaw danych, ale nie jest dobry w przypadku dużych wartości count.
David Clarke
0

Proste 4 wiersze, użyłem listy podwójnych, ale można było użyć 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;
}
Baddack
źródło