2015 年 11 月

第 30 卷,第 12 期

测试运行 - 使用 C# 执行 t-检验

作者 James McCaffrey

James McCaffreyt-检验是最基本的统计分析形式之一。它目标是当您只有两个样本集时,确定两组数字的均值(平均值)是否相等。通过举例可以很好地解释这个原理。假设您要调查一大型学区内高中男生和女生的数学能力。能力测试的成本高昂且非常耗时,所以您不可能对所有学生都进行测试。您可以改为随机选择一个包含 10 个男生和 10 个女生的样本,然后对这些学生进行数学测试。通过样本结果,您可以执行 t-检验,以此来推断所有男生的真实平均得分是否等于所有女生的真实平均得分。

许多独立的工具(包括 Excel)都可以执行 t-检验。不过,如果您想直接将 t-检验功能集成到软件系统中,那么使用独立的工具可能并不合适或者根本做不到,并且可能涉及到版权或其他法律问题。本文介绍了如何使用原始(无外部库)C# 代码执行 t-检验。

若要了解 t-检验的定义以及本文要讨论的问题,最好的方法是查看图 1 中的演示程序。第一个数据集是 { 88, 77, 78, 85, 90, 82, 88, 98, 90 }。您可以假设这些数字是 10 个男生的测试得分,其中一个男生由于某种原因没有参加测试,只剩下九个男生的得分。

使用 C# 执行 t-检验的演示
图 1:使用 C# 执行 t-检验的演示

第二个数据集是 { 81, 72, 67, 81, 71, 70, 82, 81 }。您可以假设这些数字是 10 个女生的测试得分,其中两个女生由于某种原因没有参加测试,只剩下八个女生的得分。第一个数据集的平均值是 86.22,第二个数据集的平均值是 75.63。也就是说,两个群组的平均值不一样,因为两者相差接近 11。不过,这两个群组(所有男生和所有女生)的总平均得分其实是相同的,因为我们只使用了样本,样本平均值出现差值可能是偶然事件。

演示程序使用这两个样本数据集计算出的“t-统计量” (t) 值为 3.4233,以及“自由度”(通常缩写为 df,或者通常用希腊小写字母 nu (ν) 表示)值为 14.937。然后,使用 t 值和 df 值计算出的概率值(p-值)为 0.00379。t-检验分为几种形式。最常见的形式也许是学生 t-检验。本演示使用的是经过改进的变体,称为 Welch t-检验。

p-值是指两个群体(所有男生和所有女生)的真实平均值实际相同的概率,以及鉴于样本得分,观察到的差值 11 属于偶然事件的概率。在此示例中,p-值非常小,所以您可以得出结论,所有男生和所有女生的真实平均值并不相等。在大多数问题中,用于与计算得出的 p-值作对比的临界 p-值被任意定义为 0.01 或 0.05。

换言之,如果所有男生和所有女生的真实平均得分相同,那么您在两个样本(分别包括九个数字和八个数字)的平均值中观察到接近 11 的差值的概率仅有 0.00379,这种概率是极低的。

要理解本文的内容,您至少需要拥有中级编程技巧,但无需了解 t-检验的一切。虽然本演示使用 C# 进行编码,但您也应该能够顺利地将代码重构为其他语言(如 Visual Basic、.NET 或 JavaScript)。

理解 t-分布

t-检验基于 t-分布。t-分布与正态(也称为高斯或钟形)分布密切相关。正态分布数据集的形状同时取决于数据的均值和标准偏差。标准偏差是衡量数据如何分布和变化的值。一种特殊的情况是,均值(通常用希腊字母 mu (µ) 表示)为 0,标准偏差(英文缩写通常为 sd,或者通常用希腊字母 sigma (σ) 表示)为 1。我们将均值为 0 且 sd 为 1 的正态分布称为标准正态分布。图形如图 2 所示。

标准正态分布
图 2:标准正态分布

图 2 中,定义标准正态分布的等式被称为概率密度函数。t-分布与正态分布十分相似。t-分布的形状取决于一个值,即“自由度”。 df 为 5 时的 t-分布如图 3 所示。

图 3 中,定义 t-分布的等式涉及了 Gamma 函数,通常用希腊大写字母 gamma (Γ) 表示该函数。为了执行 t-检验,您需要对 t-分布曲线下两个完全相同的面积进行计算和求和。合并面积就是 p-值。例如,在图 3 中,如果 t 值是 2.0,那么您需要计算曲线下负无穷大到 -2.0 之间,以及 +2.0 到正无穷大之间的合并面积。在此示例中,合并面积(即 p-值)为 0.101939。对于演示程序来说,当 t = 3.4233 时,合并面积为 0.00379。

t-分布
图 3:t-分布

那么,该如何计算 t-分布下的面积呢? 虽然解决这个问题的方法有几种,但最常用的技巧是计算标准正态分布曲线下的一个相关面积,然后用它来计算 p-值。例如,在图 2 中,如果 z(t 的正态等值)值为 -2.0,则您可以计算从负无穷大到 -2.0 之间的面积,计算结果为 0.02275。然后,您可以使用正态曲线下的这一面积来计算 t-分布下的相应面积。

简而言之,若要执行 t-检验,您必须对 t-分布下的两个(相等)面积进行计算和求和。我们将这一面积称为 p-值。为此,您可以计算标准正态分布下的一个面积,然后用此面积计算出 p-值。

计算标准正态分布下的面积

计算标准正态分布曲线下的面积的方法有很多种。这就是计算机科学界最久远的问题之一。我首选的方法是使用所谓的 ACM 算法 #209。美国计算机协会 (ACM) 已经发布了很多基本算法,用于数值计算和统计计算。

算法 #209 的 C# 实现在图 4 中呈现为高斯函数。该函数接受介于负无穷大和正无穷大之间的 z 值,并且返回标准正态分布下从负无穷大到 z 的面积的近似值。

图 4:计算标准正态分布下的面积

public static double Gauss(double z)
{
  // input = z-value (-inf to +inf)
  // output = p under Standard Normal curve from -inf to z
  // e.g., if z = 0.0, function returns 0.5000
  // ACM Algorithm #209
  double y; // 209 scratch variable
  double p; // result. called 'z' in 209
  double w; // 209 scratch variable
  if (z == 0.0)
    p = 0.0;
  else
  {
    y = Math.Abs(z) / 2;
    if (y >= 3.0)
    {
      p = 1.0;
    }
    else if (y < 1.0)
    {
      w = y * y;
      p = ((((((((0.000124818987 * w
        - 0.001075204047) * w + 0.005198775019) * w
        - 0.019198292004) * w + 0.059054035642) * w
        - 0.151968751364) * w + 0.319152932694) * w
        - 0.531923007300) * w + 0.797884560593) * y * 2.0;
    }
    else
    {
      y = y - 2.0;
      p = (((((((((((((-0.000045255659 * y
        + 0.000152529290) * y - 0.000019538132) * y
        - 0.000676904986) * y + 0.001390604284) * y
        - 0.000794620820) * y - 0.002034254874) * y
        + 0.006549791214) * y - 0.010557625006) * y
        + 0.011630447319) * y - 0.009279453341) * y
        + 0.005353579108) * y - 0.002141268741) * y
        + 0.000535310849) * y + 0.999936657524;
    }
  }
  if (z > 0.0)
    return (p + 1.0) / 2;
  else
    return (1.0 - p) / 2;
}

只需快速浏览一下图 4 中的代码,您就应该确信使用现有算法(如 ACM #209)比您自己从头开始编码实现要容易得多。ACM #209 的替代方法是使用稍作修改的 7.1.26 等式,该等式取自 Milton Abramowitz 和 Irene A. Stegun 编写的“数学函数手册”(Dover Publications 出版,1965 年)。

计算 t-分布下的面积

在实现了手头的高斯函数之后,您可以使用 ACM 算法 #395 计算 t-分布下的面积。算法 #395 的 C# 实现在图 5 中呈现为学生函数。该函数接受 t 值和 df 值,并返回从负无穷大到 t 以及从 t 到正无穷大的合并面积。

图 5:计算 t-分布下的面积

public static double Student(double t, double df)
{
  // for large integer df or double df
  // adapted from ACM algorithm 395
  // returns 2-tail p-value
  double n = df; // to sync with ACM parameter name
  double a, b, y;
  t = t * t;
  y = t / n;
  b = y + 1.0;
  if (y > 1.0E-6) y = Math.Log(b);
  a = n - 0.5;
  b = 48.0 * a * a;
  y = a * y;
  y = (((((-0.4 * y - 3.3) * y - 24.0) * y - 85.5) /
    (0.8 * y * y + 100.0 + b) + y + 3.0) / b + 1.0) *
    Math.Sqrt(y);
  return 2.0 * Gauss(-y); // ACM algorithm 209
}

算法 #395 有两种形式。一种形式接受整数值的 df 参数,另一种形式接受双精度类型值的 df 参数。在大多数统计问题中,自由度是整数值,但 Welch t-检验使用双精度类型值。

演示程序

为了创建演示程序,我启动了 Visual Studio,并新建了一个名为 TTest 的 C# 控制台应用程序。此演示并不十分依赖 .NET 版本,因此,任何版本的 Visual Studio 都可以正常运行。在将模板代码加载到编辑器中之后,我删除了所有使用的语句,对顶层系统命名空间的单次引用除外。在“解决方案资源管理器”窗口中,我将文件 Program.cs 重命名为 TTestProgram.cs,并允许 Visual Studio 自动为我重命名类 Program。

虽然演示程序有点过长而无法全部展示,但您可以在本文随附的文件下载中找到完整的源代码。Main 方法从设置和显示两个样本数据集着手:

Console.WriteLine("\nBegin Welch's t-test using C# demo\n");
var x = new double[] { 88, 77, 78, 85, 90, 82, 88, 98, 90 };
var y = new double[] { 81, 72, 67, 81, 71, 70, 82, 81 };
Console.WriteLine("\nThe first data set (x) is:\n");
ShowVector(x, 0);
Console.WriteLine("\nThe second data set (y) is:\n");
ShowVector(y, 0);

所有的工作都由名为 TTest 的方法执行:

Console.WriteLine("\nStarting Welch's t-test using C#\n");
TTest(x, y);
Console.WriteLine("\nEnd t-test demo\n");
Console.ReadLine();

通过对每个数据集中的值进行求和,开始定义 TTest 方法:

public static void TTest(double[] x, double[] y)
{
  double sumX = 0.0;
  double sumY = 0.0;
  for (int i = 0; i < x.Length; ++i)
    sumX += x[i];
  for (int i = 0; i < y.Length; ++i)
    sumY += y[i];
...

接下来,使用这些和来计算两个样本的均值:

int n1 = x.Length;
int n2 = y.Length;
double meanX = sumX / n1;
double meanY = sumY / n2;

接下来,使用两个均值计算两个样本的方差:

double sumXminusMeanSquared = 0.0; // Calculate variances
double sumYminusMeanSquared = 0.0;
for (int i = 0; i < n1; ++i)
  sumXminusMeanSquared += (x[i] - meanX) * (x[i] - meanX);
for (int i = 0; i < n2; ++i)
  sumYminusMeanSquared += (y[i] - meanY) * (y[i] - meanY);
double varX = sumXminusMeanSquared / (n1 - 1);
double varY = sumYminusMeanSquared / (n2 - 1);

数据集的方差是标准偏差的平方,所以标准偏差是方差的平方根,且 t-检验支持方差。接下来,计算 t 统计量:

double top = (meanX - meanY);
double bot = Math.Sqrt((varX / n1) + (varY / n2));
double t = top / bot;

简而言之,t 统计量就是用两个样本均值之间的差值,除以方差和的平方根,再除以相关的样本大小。接下来,计算自由度:

double num = ((varX / n1) + (varY / n2)) *
  ((varX / n1) + (varY / n2));
double denomLeft = ((varX / n1) * (varX / n1)) / (n1 - 1);
double denomRight = ((varY / n2) * (varY / n2)) / (n2 - 1);
double denom = denomLeft + denomRight;
double df = num / denom;

计算 Welch t-检验的自由度稍微有些棘手,并且等式根本不容易看出来。幸运的是,您永远无需修改此计算。TTest 方法最后会计算 p-值,并显示所有计算出的值:

...
  double p = Student(t, df); // Cumulative two-tail density
  Console.WriteLine("mean of x = " + meanX.ToString("F2"));
  Console.WriteLine("mean of y = " + meanY.ToString("F2"));
  Console.WriteLine("t = " + t.ToString("F4"));
  Console.WriteLine("df = " + df.ToString("F3"));
  Console.WriteLine("p-value = " + p.ToString("F5"));
  Explain();
}

名为 Explain 的由项目定义的方法显示了对 p-值的解释,如图 1 所示。

几点注释

涉及到 t-检验的统计问题其实分为几种不同的种类。本文中描述的问题类型有时被称为未配对 t-检验,因为各个样本数据集的数据值之间没有概念上的关联。另一种类型的 t-检验称为配对样本检验。当您拥有某种事前和事后数据(如实施某种指令之前的测试得分,以及随后实施某种指令之后的测试得分)时,可以使用这种检验。在这种情况下,每对得分在概念上是相关的。

在大多数情况下,此处提供的 Welch t-检验优于更常见的学生 t-检验。学生 t-检验通常要求两个样本数据集中都拥有等量的数据点,并且要求两个样本的方差应大致相等。Welch t-检验可以用于样本大小不等的数据集,即使样本方差有差异,这种检验也很可靠。

本文中介绍的 t-检验类型称为双尾检验。我们差不多可以将其理解为,出现需要确定两个群组的均值是否相同的问题。如果目标是确定第一组的均值是否大于第二组的均值,则可以使用单尾 t-检验。当执行单尾 t-检验时,您可以将双尾 p-值除以 2。

在解释 t-检验的结果时,您应该非常保守。类似“根据计算得出的 t-检验 p-值 0.008,我得出结论,男生和女生的真实群体均值不太可能相同”的结论明显优于“p-值为 0.008 表示男生的平均得分明显高于女生的平均得分”。

t-检验的替代方法称为 Mann-Whitney U 检验。两种技巧都根据样本来推断两个群体的均值是否相等,但 Mann-Whitney U 检验作出的统计假设更少,从而使结论更加保守(您不太可能得出以下结论:调查中的均值不同)。

t-检验仅适用于有两个群组的情况。有关检查三个或三个以上群组均值的问题,您应该使用 F-检验这一分析方法。


Dr.James McCaffrey供职于华盛顿地区雷蒙德市沃什湾的 Microsoft Research。他参与过多个 Microsoft 产品的工作,包括 Internet Explorer 和 Bing。Scripto可通过 jammc@microsoft.com 与 McCaffrey 取得联系。

衷心感谢以下 Microsoft Research 技术专家对本文的审阅: Kirk Olynyk