Desviación estándar en LINQ


80

¿LINQ modela la función SQL agregada? STDDEV() (desviación estándar)?

Si no es así, ¿cuál es la forma más sencilla o con las mejores prácticas de calcularlo?

Ejemplo:

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


@Steven, es posible que desee volver a visitar la respuesta aceptada aquí. Hay problemas con el enfoque seleccionado actualmente que las personas que no se desplazan hacia abajo y leen más podrían no ver.
— Drew Noakes

¿Por qué alguien querría hacer esto usando LINQ ?
— Ant_222

Respuestas:


98

Puedes hacer tu propia extensión calculándola

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

Si tiene una muestra de la población en lugar de toda la población, debe usar ret = Math.Sqrt(sum / (count - 1));.

Transformado en extensión de Agregar desviación estándar a LINQ por Chris Bennett .


3
Haría esa prueba "values.Count ()> 1", porque si es exactamente 1, tendrá un error de división por cero cuando calcule el valor de retorno.
— duffymo

3
Math.pow (d-avg, 2)? Omitiría la llamada a la función y usaría (d-avg) * (d-avg)
— duffymo

2
La línea ret = Math.Sqrt ((suma) / values.Count () - 1); faltan paréntesis alrededor de values.Count () - 1, debería ser ret = Math.Sqrt (sum / (values.Count () - 1));
— Alex Peck

1
Yo estaba buscando esto y me tomó un tiempo para averiguar cómo utilizar la extensión, pero aquí es la forma de aplicar los métodos indicados anteriormente: stdev = g.Select(o => o.number).StdDev().
— Andrew Mao

2
@Yevgeniy Rozhkov - ¿Por qué eliminaste el - 1? De acuerdo con esto el - 1que se requiere.
— John Mills

61

La respuesta de Dynami funciona, pero hace varios pases a través de los datos para obtener un resultado. Este es un método de un solo paso que calcula la desviación estándar de la muestra :

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

Ésta es la desviación estándar de la muestra, ya que divide por n - 1. Para la desviación estándar normal, debe dividir porn .

Esto utiliza el método de Welford, que tiene una mayor precisión numérica en comparación con el Average(x^2)-Average(x)^2método.


1
Es posible que no haya iterado la secuencia completa más de una vez, pero su método seguirá haciendo dos llamadas a GetEnumerator (lo que podría desencadenar una consulta SQL compleja). ¿Por qué no omitir la condición y marcar n al final del ciclo?
— Gideon Engelberth

Gracias Gideon, también elimina un nivel de anidación. Tiene razón sobre el SQL, no es relevante para lo que estoy trabajando, así que no había considerado la implicación.
— David Clarke

3
Falta una definición de n. También debe tenerse en cuenta que dividir la suma por (n-1) en lugar de n hace que esto sea una desviación estándar de muestra
— Neil

3
Para hacer esto replicar con más cuidado el método SQL, cambié this IEnumerable<double?> valuesy val in values.Where(val => val != null). Además, señalaré que este método (método de Welford) es más preciso y más rápido que el método anterior.
— Andrew Mao

2
Edité su respuesta para dejar en claro que está calculando la desviación estándar de la muestra , no la desviación estándar normal .
— CodesInChaos

31

Esto convierte la respuesta de David Clarke en una extensión que sigue la misma forma que las otras funciones LINQ agregadas como Promedio.

El uso sería: 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
Tenga en cuenta que Average/ Min/ Max/ etc tienen sobrecargas con y sin funciones de selector. También tienen sobrecargas para tipos integrales, flotación, etc.
— Drew Noakes

5
var stddev = Math.Sqrt(data.Average(z=>z*z)-Math.Pow(data.Average(),2));

2

Directamente al grano (y C #> 6.0), la respuesta de Dynamis se convierte en esta:

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

Editar 2020-08-27:

Tomé los comentarios de @David Clarke para hacer algunas pruebas de rendimiento y estos son los resultados:

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

Probé esto con una lista de un millón de dobles aleatorios;
la implementación original tenía un tiempo de ejecución de ~ 48 ms;
la implementación optimizada para el rendimiento, 2-3 ms
por lo que esta es una mejora significativa.

Algunos detalles interesantes: ¡
deshacerse de Math.Pow trae un impulso de 33ms!
List en lugar de IEnumerable 6ms
manualmente Cálculo promedio 4ms
For-loops en lugar de ForEach-loops 2ms
Array en lugar de List trae solo una mejora de ~ 2%, así que omití esto
usando single en lugar de double no trae nada

Bajar aún más el código y usar goto (sí, GOTO ... no he usado esto desde el ensamblador de los 90 ...) en lugar de bucles for no paga, ¡Gracias a Dios!

También he probado el cálculo paralelo, esto tiene sentido en la lista> 200.000 elementos. Parece que el hardware y el software necesitan inicializarse mucho y esto es contraproducente para listas pequeñas.

Todas las pruebas se ejecutaron dos veces seguidas para eliminar el tiempo de calentamiento.


Se consciente de que pasa varias veces a través de los datos cuando se evalúa Count(), Average()y Sum(). Eso está bien para valores pequeños de, countpero tiene potencial para afectar el rendimiento si countes grande.
— David Clarke

@ David, entonces la solución más simple en mi opinión sería reemplazar la firma con (this IList<double> values), las pruebas de rendimiento mostrarían el impacto y cuántos elementos marcan una diferencia significativa
— Ernst Greiner

Si eso no resuelve el problema - los métodos de extensión ( Count, Average, Sum) cada uno iterar la colección por lo que aún tiene tres iteraciones completos para producir un resultado.
— 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)))
}

Tenga en cuenta que esto todavía está realizando múltiples pasadas a través de los datos; está bien si es un conjunto de datos pequeño pero no es bueno para valores grandes de count.
— David Clarke

0

4 líneas simples, utilicé una lista de dobles pero se podría usar 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;
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.