用户友好的 C# 描述性统计类






4.86/5 (30投票s)
一篇关于最常用的描述性统计的文章,包括标准差、偏度、峰度、百分位数、四分位数等。
引言
80-20 规则适用:即使统计学取得了进步,我们的大部分工作也只需要单变量描述性统计 – 这些涉及均值、标准差、范围、偏度、峰度、百分位数、四分位数等的计算。 本文描述了一种在 C# 中实现描述性统计的简单方法。 重点在于用户端的易用性。
要求
要运行代码,您需要具备以下条件
- .NET Framework 2.0 及以上版本
- 如果您想打开下载项目中包含的项目文件,则需要 Microsoft Visual Studio 2005
- 如果您想运行下载项目中包含的单元测试,则需要 Nunit 2.4
本文中包含的下载作为类库实现。 您需要引用该项目才能使用其功能。
下载还包括一个 NUnit 测试,以防您想更改代码并运行自己的单元测试。
代码
代码设计的目标是简化用法。 我们设想用户将执行以下代码以获得所需的结果。 这涉及一个简单的 3 步流程
- 实例化一个
Descriptive
对象 - 调用其
.Analyze()
方法 - 从其
.Result
对象检索结果
这是一个典型的用户代码
double[] x = {1, 2, 4, 7, 8, 9, 10, 12};
Descriptive desp = new Descriptive(x);
desp.Analyze(); // analyze the data
Console.WriteLine("Result is: " + desp.Result.FirstQuartile.ToString());
实现了两个类
DescriptiveResult
Descriptive
DescriptiveResult
是一个派生结果对象的类,它保存分析结果。 在我们的实现中,.Result
成员变量定义如下
/// <summary>
/// The result class the holds the analysis results
/// </summary>
public class DescriptiveResult
{
// sortedData is used to calculate percentiles
internal double[] sortedData;
/// <summary>
/// DescriptiveResult default constructor
/// </summary>
public DescriptiveResult() { }
/// <summary>
/// Count
/// </summary>
public uint Count;
/// <summary>
/// Sum
/// </summary>
public double Sum;
/// <summary>
/// Arithmetic mean
/// </summary>
public double Mean;
/// <summary>
/// Geometric mean
/// </summary>
public double GeometricMean;
/// <summary>
/// Harmonic mean
/// </summary>
public double HarmonicMean;
/// <summary>
/// Minimum value
/// </summary>
public double Min;
/// <summary>
/// Maximum value
/// </summary>
public double Max;
/// <summary>
/// The range of the values
/// </summary>
public double Range;
/// <summary>
/// Sample variance
/// </summary>
public double Variance;
/// <summary>
/// Sample standard deviation
/// </summary>
public double StdDev;
/// <summary>
/// Skewness of the data distribution
/// </summary>
public double Skewness;
/// <summary>
/// Kurtosis of the data distribution
/// </summary>
public double Kurtosis;
/// <summary>
/// Interquartile range
/// </summary>
public double IQR;
/// <summary>
/// Median, or second quartile, or at 50 percentile
/// </summary>
public double Median;
/// <summary>
/// First quartile, at 25 percentile
/// </summary>
public double FirstQuartile;
/// <summary>
/// Third quartile, at 75 percentile
/// </summary>
public double ThirdQuartile;
/// <summary>
/// Sum of Error
/// </summary>
internal double SumOfError;
/// <summary>
/// The sum of the squares of errors
/// </summary>
internal double SumOfErrorSquare;
/// <summary>
/// Percentile
/// </summary>
/// <param name="percent">Pecentile, between 0 to 100</param>
/// <returns>Percentile<returns>
为了简单起见,大多数成员变量都实现为 public
变量。 唯一的成员函数 - Percentile
- 允许用户传递参数(以百分比表示,例如,30 表示 30%)并接收百分位数结果。
下表列出了可用的结果(假设您使用的 Descriptive
对象名称是 desp
结果 | 存储在变量中的结果 |
数据点数 | desp.Result.Count |
最小值 | desp.Result.Min |
最大值 | desp.Result.Max |
值的范围 | desp.Result.Range |
值的总和 | desp.Result.Sum |
算术平均值 | desp.Result.Mean |
几何平均值 | desp.Result.GeometricMean |
调和平均值 | desp.Result.HarmonicMean |
样本方差 | desp.Result.Variance |
样本标准差 | desp.Result.StdDev |
分布的偏度 | desp.Result.Skewness |
分布的峰度 | desp.Result.Kurtosis |
四分位距 | desp.Result.IQR |
中位数(50% 百分位数) | desp.Result.Median |
第一四分位数:25% 百分位数 | desp.Result.FirstQuartile |
第三四分位数:75% 百分位数 | desp.Result.ThirdQuartile |
百分位数 | desp.Result.Percentile() * |
* 百分位数的参数是从 0 到 100 的值,表示所需的百分位数。
Descriptive 类
Descriptive
类执行所有分析,其实现如下
/// <summary>
/// Descriptive class
/// </summary>
public class Descriptive
{
private double[] data;
private double[] sortedData;
/// <summary>
/// Descriptive results
/// </summary>
public DescriptiveResult Result = new DescriptiveResult();
#region Constructors
/// <summary>
/// Descriptive analysis default constructor
/// </summary>
public Descriptive() { } // default empty constructor
/// <summary>
/// Descriptive analysis constructor
/// </summary>
/// <param name="dataVariable">Data array</param>
public Descriptive(double[] dataVariable)
{
data = dataVariable;
}
#endregion // Constructors
请注意,我们需要一个 sortedData
类来促进百分位数和四分位数相关的统计。 它存储用户数据的排序版本。
Descriptive
类的构造函数允许用户在对象实例化期间分配数据数组
double[] x = {1, 2, 4, 7, 8, 9, 10, 12};
Descriptive desp = new Descriptive(x);
一旦 Descriptive
对象被实例化,用户只需要调用 .Analyze()
方法来执行分析。 随后,用户可以从 Descriptive
对象中的 .Result
对象中检索分析结果。
Analyze()
方法的实现如下
/// <summary>
/// Run the analysis to obtain descriptive information of the data
/// </summary>
public void Analyze()
{
// initializations
Result.Count = 0;
Result.Min = Result.Max = Result.Range = Result.Mean =
Result.Sum = Result.StdDev = Result.Variance = 0.0d;
double sumOfSquare = 0.0d;
double sumOfESquare = 0.0d; // must initialize
double[] squares = new double[data.Length];
double cumProduct = 1.0d; // to calculate geometric mean
double cumReciprocal = 0.0d; // to calculate harmonic mean
// First iteration
for (int i = 0; i < data.Length; i++)
{
if (i==0) // first data point
{
Result.Min = data[i];
Result.Max = data[i];
Result.Mean = data[i];
Result.Range = 0.0d;
}
else
{ // not the first data point
if (data[i] < Result.Min) Result.Min = data[i];
if (data[i] > Result.Max) Result.Max = data[i];
}
Result.Sum += data[i];
squares[i] = Math.Pow(data[i], 2); //TODO: may not be necessary
sumOfSquare += squares[i];
cumProduct *= data[i];
cumReciprocal += 1.0d / data[i];
}
Result.Count = (uint)data.Length;
double n = (double)Result.Count; // use a shorter variable in double type
Result.Mean = Result.Sum / n;
Result.GeometricMean = Math.Pow(cumProduct, 1.0 / n);
// see https://mathworld.net.cn/HarmonicMean.html
Result.HarmonicMean = 1.0d / (cumReciprocal / n);
Result.Range = Result.Max - Result.Min;
// second loop, calculate Stdev, sum of errors
//double[] eSquares = new double[data.Length];
double m1 = 0.0d;
double m2 = 0.0d;
double m3 = 0.0d; // for skewness calculation
double m4 = 0.0d; // for kurtosis calculation
// for skewness
for (int i = 0; i < data.Length; i++)
{
double m = data[i] - Result.Mean;
double mPow2 = m * m;
double mPow3 = mPow2 * m;
double mPow4 = mPow3 * m;
m1 += Math.Abs(m);
m2 += mPow2;
// calculate skewness
m3 += mPow3; // Math.Pow((data[i] - mean), 3);
// calculate skewness
m4 += mPow4; // Math.Pow((data[i] - mean), 4);
}
Result.SumOfError = m1;
Result.SumOfErrorSquare = m2; // Added for Excel function DEVSQ
sumOfESquare = m2;
// var and standard deviation
Result.Variance = sumOfESquare / ((double)Result.Count - 1);
Result.StdDev = Math.Sqrt(Result.Variance);
// using Excel approach
double skewCum = 0.0d; // the cum part of SKEW formula
for (int i = 0; i < data.Length; i++)
{
skewCum += Math.Pow((data[i] - Result.Mean) / Result.StdDev, 3);
}
Result.Skewness = n / (n - 1) / (n - 2) * skewCum;
// kurtosis: see http://en.wikipedia.org/wiki/Kurtosis (heading: Sample Kurtosis)
double m2_2 = Math.Pow(sumOfESquare, 2);
Result.Kurtosis = ((n + 1) * n * (n - 1)) / ((n - 2) * (n - 3)) *
(m4 / m2_2) -
3 * Math.Pow(n - 1, 2) / ((n - 2) * (n - 3)); // second last formula for G2
// calculate quartiles
sortedData = new double[data.Length];
data.CopyTo(sortedData, 0);
Array.Sort(sortedData);
// copy the sorted data to result object so that
// user can calculate percentile easily
Result.sortedData = new double[data.Length];
sortedData.CopyTo(Result.sortedData, 0);
Result.FirstQuartile = percentile(sortedData, 25);
Result.ThirdQuartile = percentile(sortedData, 75);
Result.Median = percentile(sortedData, 50);
Result.IQR = percentile(sortedData, 75) - percentile(sortedData, 25);
} // end of method Analyze
描述性统计的计算非常简单,除了百分位数函数(以及随后的四分位数计算)有点棘手。 因此,我有一个单独的函数来处理它,如下所示
/// <summary>
/// Calculate percentile of a sorted data set
/// </summary>
/// <param name="sortedData">array of double values</param>
/// <param name="p">percentile, value 0-100</param>
/// <returns></returns>
internal static double percentile(double[] sortedData, double p)
{
// algo derived from Aczel pg 15 bottom
if (p >= 100.0d) return sortedData[sortedData.Length - 1];
double position = (double)(sortedData.Length + 1) * p / 100.0;
double leftNumber = 0.0d, rightNumber = 0.0d;
double n = p / 100.0d * (sortedData.Length - 1) + 1.0d;
if (position >= 1)
{
leftNumber = sortedData[(int)System.Math.Floor(n) - 1];
rightNumber = sortedData[(int)System.Math.Floor(n)];
}
else
{
leftNumber = sortedData[0]; // first data
rightNumber = sortedData[1]; // first data
}
if (leftNumber == rightNumber)
return leftNumber;
else
{
double part = n - System.Math.Floor(n);
return leftNumber + part * (rightNumber - leftNumber);
}
} // end of internal function percentile
百分位数算法源自 Amir Aczel 的著作 "Complete Business Statistics"。
结论
此处介绍的描述性统计程序提供了一种获取常用描述性统计的简单方法,包括标准差、偏度、峰度、百分位数、四分位数等。
历史
- 2008 年 6 月 28 日:首次发布
关于作者
Jan Low, 博士,是 Foundasoft.com(马来西亚)的资深软件架构师。 他还是各种文本分析软件、统计库、图像处理库和安全加密组件的作者。 他主要使用 C#、C++ 和 VB.NET 进行编程。
职业:资深软件架构师
地点:马来西亚