2010-03-05 25 views
7

Necesito generar contenedores para el cálculo de un histograma. El lenguaje es C#. Básicamente, necesito tomar una matriz de números decimales y generar un diagrama de histogramas.Buscando un histograma Algoritmo de agrupamiento para datos decimales

No he podido encontrar una biblioteca decente para hacer esto directamente, así que ahora solo estoy buscando una biblioteca o un algoritmo para ayudarme a hacer el agrupamiento de los datos.

Entonces ...

  • ¿Hay bibliotecas de C# que hay que tomar en una matriz de datos decimales y salida de un histograma desechado?
  • ¿Existe un algoritmo genérico para construir los contenedores que se utilizarán en un histograma generado?

Respuesta

13

Aquí hay una función de cuchara simple que yo uso. Lamentablemente, los genéricos .NET no admite un tipo contraint numérica por lo que tendrá que poner en práctica una versión diferente de la siguiente función para decimal, int, double, etc.

public static List<int> Bucketize(this IEnumerable<decimal> source, int totalBuckets) 
{ 
    var min = source.Min(); 
    var max = source.Max(); 
    var buckets = new List<int>(); 

    var bucketSize = (max - min)/totalBuckets; 
    foreach (var value in source) 
    { 
     int bucketIndex = 0; 
     if (bucketSize > 0.0) 
     { 
      bucketIndex = (int)((value - min)/bucketSize); 
      if (bucketIndex == totalBuckets) 
      { 
       bucketIndex--; 
      } 
     } 
     buckets[bucketIndex]++; 
    } 
    return buckets; 
} 
+0

Escribí un algoritmo casi idéntico utilizando el lenguaje SAS e iba a tener que hacer que mi desarrollador lo tradujera a C#. Gracias por esto. –

+1

@Jake Pearson: si importa el espacio de nombre System.Linq, no necesitará su primer bucle foreach para encontrar los valores mínimo y máximo. En su lugar, simplemente escriba: min = source.Min(); y max = source.Max(). No estoy seguro de que sea mucho más eficiente en la CPU, pero es un poco menos de lectura. –

+0

Buena llamada, actualizada. –

4

me impares resultados utilizando @JakePearson aceptadas responder. Tiene que ver con una caja de borde.

Aquí está el código que usé para probar su método. Cambié muy poco el método de extensión, devolviendo un int[] y aceptando double en lugar de decimal.

public partial class Form1 : Form 
{ 
    public Form1() 
    { 
     InitializeComponent(); 

     Random rand = new Random(1325165); 

     int maxValue = 100; 
     int numberOfBuckets = 100; 

     List<double> values = new List<double>(); 
     for (int i = 0; i < 10000000; i++) 
     { 
      double value = rand.NextDouble() * (maxValue+1);    
      values.Add(value); 
     } 

     int[] bins = values.Bucketize(numberOfBuckets); 

     PointPairList points = new PointPairList(); 
     for (int i = 0; i < numberOfBuckets; i++) 
     { 
      points.Add(i, bins[i]); 
     } 

     zedGraphControl1.GraphPane.AddBar("Random Points", points,Color.Black); 
     zedGraphControl1.GraphPane.YAxis.Title.Text = "Count"; 
     zedGraphControl1.GraphPane.XAxis.Title.Text = "Value"; 


     zedGraphControl1.AxisChange(); 
     zedGraphControl1.Refresh(); 

    } 
} 

public static class Extension 
{ 
    public static int[] Bucketize(this IEnumerable<double> source, int totalBuckets) 
    { 
     var min = source.Min(); 
     var max = source.Max(); 
     var buckets = new int[totalBuckets]; 

     var bucketSize = (max - min)/totalBuckets; 
     foreach (var value in source) 
     { 
      int bucketIndex = 0; 
      if (bucketSize > 0.0) 
      { 
       bucketIndex = (int)((value - min)/bucketSize); 
       if (bucketIndex == totalBuckets) 
       { 
        bucketIndex--; 
       } 
      } 
      buckets[bucketIndex]++; 
     } 
     return buckets; 
    } 
} 

Todo funciona bien cuando se usan 10,000,000 valores dobles aleatorios entre 0 y 100 (exclusivo). Cada segmento tiene aproximadamente el mismo número de valores, lo que tiene sentido dado que Random devuelve una distribución normal.

Good Result

Pero cuando cambié la línea de generación de valor a partir de

double value = rand.NextDouble() * (maxValue+1);    

a

double value = rand.Next(0, maxValue + 1); 

y se obtiene el resultado siguiente, que cuenta doble el último cubo.

Odd Result

Parece ser que cuando un valor es igual a uno de los límites de un cubo, el código como está escrito pone el valor en el cubo incorrecta. Este artefacto no parece ocurrir con valores aleatorios double, ya que la probabilidad de que un número aleatorio sea igual a un límite de un depósito es raro y no sería obvio.

La forma en que corregí esto es definir qué lado del límite del contenedor es inclusivo versus exclusivo.

Piense en

0< x <=11< x <=2 ... 99< x <=100

vs

0<= x <11<= x <2 ...99<= x <100

No puede tener ambos límites incluidos, ya que el método no sabría en qué depósito ponerlo si tiene un valor que es exactamente igual a un límite.

public enum BucketizeDirectionEnum 
    { 
     LowerBoundInclusive, 
     UpperBoundInclusive 
    } 

    public static int[] Bucketize(this IList<double> source, int totalBuckets, BucketizeDirectionEnum inclusivity = BucketizeDirectionEnum.UpperBoundInclusive) 
    { 
     var min = source.Min(); 
     var max = source.Max(); 
     var buckets = new int[totalBuckets]; 
     var bucketSize = (max - min)/totalBuckets; 

     if (inclusivity == BucketizeDirectionEnum.LowerBoundInclusive) 
     { 
      foreach (var value in source) 
      { 
       int bucketIndex = (int)((value - min)/bucketSize); 
       if (bucketIndex == totalBuckets) 
        continue; 
       buckets[bucketIndex]++; 
      } 
     } 
     else 
     { 
      foreach (var value in source) 
      { 
       int bucketIndex = (int)Math.Ceiling((value - min)/bucketSize) - 1; 
       if (bucketIndex < 0) 
        continue; 
       buckets[bucketIndex]++; 
      } 
     } 

     return buckets; 
    } 

El único problema ahora es si el conjunto de datos de entrada tiene una gran cantidad de valores mínimo y máximo, el método de intervalos excluirá muchos de esos valores y la gráfica resultante se falsificar el conjunto de datos.

Cuestiones relacionadas