November 2015

Volume 30 Number 12

Тесты - T-тест на C#

By Джеймс Маккафри | November 2015 |

James McCaffreyT-тест (t-test) — одна из самых фундаментальных форм статистического анализа. Его цель — определить, равны ли средние из двух наборов чисел, когда имеются лишь выборки из этих двух наборов. Суть лучше пояснить на примере. Допустим, вы исследуете математические способности юношей и девушек в средних школах в большом округе. Тест на определение таких способностей дорогостоящий и требует много времени, поэтому вы не в состоянии раздать его всем школьникам. Вместо этого вы случайным образом делаете выборку 10 юношей и 10 девушек и даете им математический тест. Используя результаты выборки, вы можете выполнить t-тест, чтобы логически определить, равен ли истинный средний (true average) бал всех юношей истинному среднему балу всех девушек.

Есть много автономных инструментов, включая Excel, способных выполнять t-тест. Но, если вы хотите интегрировать функциональность такой проверки непосредственно в какую-либо программную систему, применение автономных инструментов может оказаться затруднительным или невозможным, а также заставит решать проблемы с авторскими правами или другие юридические вопросы. В этой статье объясняется, как выполнять t-тест, используя чистый код на C# (без внешних библиотек).

Лучший способ прочувствовать, что такое t-тест, и понять, куда я клоню в этой статье, — взглянуть на демонстрационную программу на рис. 1. Первый набор данных — { 88, 77, 78, 85, 90, 82, 88, 98, 90 }. Вы можете представить его как баллы по результатам теста 10 юношей, где результат одного из юношей по каким-то причинам выпал, оставив вам всего девять результатов.

Демонстрация t-теста на C#
Рис. 1. Демонстрация t-теста на C#

Второй набор данных — { 81, 72, 67, 81, 71, 70, 82, 81 }. Вы можете представить его как баллы по результатам теста 10 девушек, где результаты двух девушек по каким-то причинам выпали, оставив вам всего восемь результатов. Среднее первого набора — 86.22, а среднее второго — 75.63; это предполагает, что средние двух групп не одинаковы, поскольку разница составляет почти 11 баллов. Но даже если бы общие баллы двух групп (всех юношей и девушек) были одинаковы, то, поскольку используются лишь выборки, разница между средними выборок могла бы быть делом случая.

{Для верстки: далее будут встречаться греческие буквы}

Используя наборы данных двух выборок, демонстрационная программа вычисляет t-статистику (t-statistic) (t) со значением 3.4233 и степени свободы (degrees of freedom) (часто сокращают до df или обозначают греческой буквой нижнего регистра «ню», ν) со значением 14.937. Затем, используя значения t и df, вычисляется вероятность (probability value, p-value) со значением 0.00379. Существует несколько форм t-теста. Возможно, наиболее распространенной является проверка по t-критерию Стьюдента (Student t-test). В демонстрации применяется улучшенная вариация проверки по t-критерию Уэлча (Welch t-test).

P-значение — это вероятность того, что истинные средние двух популяций (всех юношей и девушек) действительно одинаковы, учитывая баллы выборок, а значит, наблюдавшаяся разница примерно в 11 баллов является случайной. В этом случае p-значение очень мало, поэтому вы заключили бы, что истинные средние для всех юношей и всех девушек не равны. В большинстве задач критическое p-значение для сравнения с вычисленным p-значением произвольно определяется равным 0.01 или 0.05.

Говоря несколько иначе, если истинные средние баллы для всех юношей и девушек были бы одинаковы, вероятность того, что вы увидели бы наблюдавшуюся разницу почти в 11 баллов между средними в двух выборках размером по девять и восемь элементов составляет всего 0.00379 — это крайне маловероятно.

В этой статье предполагается, что вы умеете программировать хотя бы на среднем уровне, но ничего не знаете о t-тесте. Демонстрационная программа написана на C#, но у вас не должно возникнуть особых трудностей, если вы захотите переработать код под другой язык, например Visual Basic .NET или JavaScript.

Понимание t-распределения

T-test основан на t-распределении. А t-распределение тесно связано с нормальным распределением (также называемым гауссовым или колоколообразным [bell-shaped]). Форма нормально распределенного набора зависит как от среднего отклонения, так и от среднего квадратичного отклонения (standard deviation) данных. Среднее квадратичное отклонение — это значение, которое служит мерой разброса данных. Особый случай — среднее отклонение (часто обозначаемое греческой буквой «мю», µ) равно 0, а среднее квадратичное отклонение (часто обозначаемое сокращением на английском sd или греческой буквой «сигма», σ) равно 1. Нормальное распределение при среднем = 0 и sd = 1 называют стандартным нормальным распределением. Его график показан на рис. 2.

Стандартное нормальное распределение
Рис. 2. Стандартное нормальное распределение

Normal Probability Density Function, mean = 0, sd = 1 Функция плотности нормального распределения, среднее = 0, sd = 1
prob вероятность
Gauss(-2.0) = 0.02275 Gauss(–2.0) = 0.02275

На рис. 2 уравнение, которое определяет стандартное нормальное распределение, называют функцией плотности вероятностей (probability density function). T-распределение сильно напоминает нормальное распределение. Форма t-распределения зависит от единственного значения — степени свободы. T-распределение при df = 5 представлено на рис. 3.

T-распределение
Рис. 3. T-распределение

t-Distribution Probability Density Function, df = 5 Функция плотности t-распределения вероятности, df = 5
prob вероятность
Student(2.0, 5) = 0.05097 + 0.05097 =0.101939 Student(2.0, 5) = 0.05097 + 0.05097 = 0.101939

На рис. 3 уравнение, которое определяет t-распределение, включает функцию Gamma, которая обозначается греческой прописной буквой «гамма» (Γ). Чтобы выполнить t-тест, вы должны вычислить и суммировать две идентичные области под кривой t-распределения. Эта объединенная область и есть p-значение. Например, на рис. 3, если значение t равно 2.0, нужные вам области под кривой размещаются от –бесконечности до –2.0 и от +2.0 до +бесконечности. В данном случае комбинированная область, т. е. p-значение, — 0.101939. В демонстрационной программе, когда t = 3.4233, комбинированная область равна 0.00379.

Хорошо, но как вычислить область под t-распределением? Есть несколько подходов к решению этой задачи, но самый популярный метод — вычисление одной связанной области под кривой стандартного нормального распределения и ее использование для вычисления p-значения. Например, на рис. 2, если z (нормальный эквивалент t) имеет значение –2.0, вы можете вычислить область от –бесконечности до –2.0, получив 0.02275. Затем эту область под нормальной кривой можно задействовать для вычисления соответствующей области под t-распределением.

Итак, чтобы выполнить t-тест, вы должны вычислить, а потом суммировать две (равные) области под t-распределением. Эта область называется p-значением. Для этого можно вычислить одну область под стандартным нормальным распределением и использовать ее для получения p-значения.

Вычисление области под стандартным нормальным распределением

Область под кривой стандартного нормального распределения вычисляется многими способами. Это одна из старейших задач в компьютерной науке. Я предпочитаю метод, где используется ACM-алгоритм #209. Association for Computing Machinery (ACM) опубликовала множество фундаментальных алгоритмов для численных и статистических расчетов.

C#-реализация алгоритма #209 представлена на рис. 4 как функция Gauss. Эта функция принимает значение z, лежащее в интервале от –бесконечности до +бесконечности, и возвращает хорошую аппроксимацию области под стандартным нормальным распределением от –бесконечности до z.

Рис. 4. Расчет области под стандартным нормальным распределением

public static double Gauss(double z)
{
  // ввод = z-value (от -inf до +inf)
  // вывод = p под кривой стандартного нормального
  // распределения от -inf до z, например,
  // если z = 0.0, функция возвращает 0.5000

  // ACM-алгоритм #209
  double y; // случайная переменная (scratch variable) в 209
  double p; // результат, называемый z в 209
  double w; // случайная переменная в 209

  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) «Handbook of Mathematical Functions» (Dover Publications, 1965).

Вычисление области под t-распределением

При наличии реализации функции Gauss область под t-распределением можно вычислить по ACM-алгоритму #395. C#-реализация этого алгоритма представлена на рис. 5 как функция Student. Эта функция принимает значения t и df и возвращает объединенную область от –бесконечности до t плюс от t до +бесконечности.

Рис. 5. Вычисление области под t-распределением

public static double Student(double t, double df)
{
  // Для df с очень большими целыми значениями или
  // двойной точности. Адаптировано из ACM-алгоритма 395.
  // Возвращает двухстороннее (2-tail) p-значение.
  double n = df; // для синхронизации с ACM-именем параметра
  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-алгоритм 209
}

Алгоритм #395 имеет две формы. Одна форма принимает параметр df как целое значение, а вторая — как значение типа double. В большинстве задач статистики степень свободы является целым значением, но в проверке по t-критерию Уэлча используется значение типа double.

Демонстрационная программа

Чтобы создать демонстрационную программу, я запустил Visual Studio и выбрал шаблон C# Console Application. Я назвал проект TTest. В этой программе нет значимых зависимостей от .NET Framework, поэтому подойдет любая версия Visual Studio. После загрузки кода шаблона я удалил все выражения using, кроме одной ссылки на пространство имен верхнего уровня System на переименовал в окне Solution Explorer файл 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;

Потом две средние применяются для вычисления дисперсий (variances) в двух выборках:

double sumXminusMeanSquared = 0.0; // расчет дисперсии
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;

Расчет степеней свободы для проверки по t-критерию Уэлча довольно сложен, а уравнение вовсе не очевидно. К счастью, вам никогда не понадобится модифицировать это вычисление. Метод TTest завершает подсчетом p-значения и отображением всех вычисленных значений:

...
  double p = Student(t, df); // накопленная двухсторонняя
                             // плотность
  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-тестом (unpaired t-test), поскольку в наборах данных каждой выборки между значениями нет концептуальной связи. Другой тип t-теста называют парной проверкой выборки; его можно использовать, когда у вас есть какие-то данные до и после, например баллы по тесту до инструктажа и баллы по тесту после инструктажа. Здесь каждая пара баллов концептуально связана.

Проверка по t-критерию Уэлча, представленная здесь, в большинстве сценариев превосходит более популярную проверку по t-критерию Стьюдента. Последняя, как правило, требует равного количества точек данных в каждом наборе данных двух выборок и того, чтобы дисперсия двух выборок была приблизительно равной. Проверка по t-критерию Уэлча способна работать с неравными по размеру выборками и надежна, даже когда дисперсии выборок различаются.

Тип t-теста, объясняемый в этой статье, называется двухсторонним (two-tailed test). Это более-менее синонимично задаче, где цель — определить, одинаковы ли средние двух групп. Односторонний t-тест (one-tailed t-test) можно применять в ситуациях, когда цель — выяснить, больше ли среднее одной группы, чем среднее другой. При выполнении одностороннего t-теста вы делите двухстороннее p-значение пополам.

Интерпретируя результаты t-теста, следует быть очень осторожным. Выводы вроде «исходя из вычисленного в ходе t-теста p-значения 0.008, я счел маловероятным, что истинные средние популяций юношей и девушек одинаковы» гораздо лучше, чем «p-значение 0.008 означает, что средние баллы юношей выше таковых у девушек».

Альтернатива t-тесту называется проверка по U-критерию Манна–Уитни (Mann–Whitney U test). Оба метода логически определяют, равны средние двух популяций или нет на основе выборок, но в проверке по U-критерию Манна–Уитни делается меньше статистических допущений, что ведет к более консервативным заключениям (вы с меньшей вероятностью сочтете, что исследуемые средние различаются).

T-тест ограничен ситуациями, где имеется две группы. Для задач, изучающих средние трех и более групп, вам пришлось бы использовать аналитический метод, называемый F-тестом, или критерием Фишера.

Исходный код можно скачать по ссылке msdn.com/magazine/msdnmag1115.


Джеймс Маккафри (Dr. James McCaffrey) работает на Microsoft Research в Редмонде (штат Вашингтон). Принимал участие в создании нескольких продуктов Microsoft, в том числе Internet Explorer и Bing. С ним можно связаться по адресу jammc@microsoft.com.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Research Кирку Олинику (Kirk Olynyk).