Procurando um algoritmo de binning de histograma para dados decimais
-
24-09-2019 - |
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?
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.
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.
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.