65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (30投票s)

2008年6月28日

CPOL

3分钟阅读

viewsIcon

92744

downloadIcon

2941

一篇关于最常用的描述性统计的文章,包括标准差、偏度、峰度、百分位数、四分位数等。

引言

80-20 规则适用:即使统计学取得了进步,我们的大部分工作也只需要单变量描述性统计 – 这些涉及均值、标准差、范围、偏度、峰度、百分位数、四分位数等的计算。 本文描述了一种在 C# 中实现描述性统计的简单方法。 重点在于用户端的易用性。

要求

要运行代码,您需要具备以下条件

  • .NET Framework 2.0 及以上版本
  • 如果您想打开下载项目中包含的项目文件,则需要 Microsoft Visual Studio 2005
  • 如果您想运行下载项目中包含的单元测试,则需要 Nunit 2.4

本文中包含的下载作为类库实现。 您需要引用该项目才能使用其功能。

下载还包括一个 NUnit 测试,以防您想更改代码并运行自己的单元测试。

代码

代码设计的目标是简化用法。 我们设想用户将执行以下代码以获得所需的结果。 这涉及一个简单的 3 步流程

  1. 实例化一个 Descriptive 对象
  2. 调用其 .Analyze() 方法
  3. 从其 .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 进行编程。
职业:资深软件架构师
地点:马来西亚

© . All rights reserved.