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
fuente
@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 .

Dynami Le Savard
fuente
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.

David Clarke
fuente
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; 

    }
} 
Will Mathies
fuente
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));
Vitas
fuente
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.

Ernst Greiner
fuente
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)))
}
duc14s
fuente
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;
}
Baddack
fuente