Pergunta

Preciso gerar caixas para fins de cálculo de um histograma. A linguagem é C#. Basicamente, preciso pegar uma variedade de números decimais e gerar um gráfico de histograma a partir deles.

Não consegui encontrar uma biblioteca decente para fazer isso imediatamente, então agora estou apenas procurando uma biblioteca ou um algoritmo para me ajudar a fazer o binning dos dados.

Então...

  • Existem bibliotecas C# que receberão uma variedade de dados decimais e produzirão um histograma em binned?
  • Existe um algoritmo genérico para a construção das caixas a serem usadas em um histograma gerado?
Foi útil?

Solução

Aqui está uma função de balde simples que eu uso. Infelizmente, o .NET Generics não suporta uma contragem numérica, então você terá que implementar uma versão diferente da seguinte função para decimal, int, dupla, 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;
}

Outras dicas

Recebi resultados estranhos usando a resposta aceita @jakepearson. Tem a ver com um caso de borda.

Aqui está o código que eu usei para testar o método dele. Eu mudei o método de extensão ligeiramente, retornando um int[] e aceitar double ao invés 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;
    }
}

Tudo funciona bem ao usar 10.000.000 valores duplos aleatórios entre 0 e 100 (exclusivos). Cada balde tem aproximadamente o mesmo número de valores, o que faz sentido, dado que Random Retorna uma distribuição normal.

Good Result

Mas quando mudei a linha de geração de valor de

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

para

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

E você obtém o seguinte resultado, que conta o último balde.

Odd Result

Parece que, quando um valor é o mesmo que um dos limites de um balde, o código como está escrito coloca o valor no balde incorreto. Este artefato não parece acontecer com aleatório double Os valores como a chance de um número aleatório ser igual a um limite de um balde é raro e não seria óbvio.

A maneira como corrigi isso é definir qual lado do limite do balde é inclusivo versus exclusivo.

Imagine

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

vs.

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

Você não pode ter os dois limites inclusivos, pois o método não saberia qual balde para colocá -lo se você tiver um valor exatamente igual a um limite.

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

O único problema agora é que, se o conjunto de dados de entrada tiver muitos valores mínimo e máximo, o método de binning excluirá muitos desses valores e o gráfico resultante deturpará o conjunto de dados.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top