Febrero de 2017

Volumen 32, número 2

Serie de pruebas: prueba de signos con C#

Por James McCaffrey

James McCaffreyLa prueba de signos suele usarse principalmente en situaciones en las que tiene datos de "antes y después" y quiere determinar si existen evidencias estadísticas de un efecto del mismo tipo. Se explica mejor esta idea a través de un ejemplo. Supongamos que trabaja en una empresa farmacéutica y quiere saber si un nuevo medicamento para la pérdida de peso es eficaz. Selecciona a ocho voluntarios para que usen el medicamento durante varias semanas. Observa los pesos de los ocho sujetos antes y después del experimento. Pongamos que seis de los ocho sujetos pierde peso. ¿Existen evidencias estadísticas sólidas que sugieren que el medicamento funciona?

La pérdida de peso es un ejemplo de prueba de signos típico, pero la prueba se aplica también en muchos escenarios de software y TI. Supongamos que tiene 40 equipos de servidor web y que aplica un parche de software diseñado para mejorar el rendimiento. Mide los tiempos de respuesta antes y después de aplicar el parche. ¿Cuál sería su conclusión si 32 servidores mostraran un rendimiento superior, dos ningún cambio y seis una disminución del rendimiento?

La mejor manera de ver hacia dónde se dirige este artículo es echar un vistazo al programa de demostración de la Figura 1. Después de leer este artículo, tendrá una clara comprensión del tipo de problema que resuelve la prueba de signos, sabrá exactamente cómo realizar una prueba de signos con C# y entenderá cómo se interpretan los resultados de una prueba de signos. En este artículo se presenta todo el código fuente para el programa de demostración. También puede obtener el programa de demostración completo en la descarga de código que se incluye con el artículo.

Prueba de signos con C#
Figura 1 Prueba de signos con C#

El programa de demostración configura ocho pares de datos de antes y después, con el objetivo de determinar la eficacia o ineficacia de algún régimen para perder de peso. Los datos revelaron que seis de los sujetos habían perdido peso, pero dos presentaban un aumento. El programa de demostración calcula la probabilidad de "ningún efecto" en 0,1445. Es cosa suya interpretar los resultados; por ejemplo, "Los datos muestran una indicación débil (p = 0,8555) de que el esfuerzo por perder peso haya surtido efecto".

En este artículo se supone que tiene al menos conocimientos de programación intermedios, pero no se supone que sepa nada acerca de la prueba de signos. El código de la demostración está escrito en C# y depende del espacio de nombres System.Numerics de .NET, por lo que necesitará Microsoft .NET Framework 4 (lanzado en 2010) o posterior.

Estructura del programa de demostración

Para crear el programa de demostración, inicié Visual Studio y seleccioné la plantilla de aplicación de consola de C# mediante el elemento de menú Nuevo proyecto. Asigné al proyecto el nombre SignTestUsingCSharp. Después de cargar el código de plantilla en la ventana del editor, hice clic con el botón derecho en el archivo Program.cs en el Explorador de soluciones y le cambié el nombre a SignTestProgram.cs. Luego, permití a Visual Studio cambiar el nombre de la clase Program por mí.

A continuación, hice clic con el botón derecho en el nombre del proyecto y seleccioné el elemento Agregar | Referencia. En la lista Ensamblados | Marco, seleccioné el espacio de nombres System.Numerics e hice clic en Aceptar para agregarlo a mi proyecto. En la parte superior de la ventana del editor, eliminé todas las instrucciones using, excepto la que hacía referencia al espacio de nombres System de nivel superior y, a continuación, agregué una instrucción using para hacer referencia al espacio de nombres System.Numerics.

La estructura general del programa se presenta en la Figura 2. Con fines de simplicidad, el programa usa un enfoque de método estrictamente estático en lugar de un enfoque de programación orientada a objetos (OOP). Los métodos DoCounts y ShowVector son aplicaciones auxiliares de utilidad. El trabajo de cálculo de la probabilidad de ningún efecto lo lleva a cabo el método BinomRightTail. Los métodos BinomProb y Choose son aplicaciones auxiliares de BinomRightTail.

Figura 2 Estructura del programa de demostración de la prueba de signos

using System;
using System.Numerics;
namespace SignTestUsingCSharp
{
  class SignTestProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("\nBegin Sign Test demo \n");
      // All calling statements go here
      Console.WriteLine("\n\nEnd Sign Test demo \n");
      Console.ReadLine();
    }
    static int[] DoCounts(double[] before,
      double[] after) { . . }
    static void ShowVector(string pre, double[] v,
      int dec, string post) { . . }
    static double BinomProb(int k, int n,
      double p) { . . }
    static double BinomRightTail(int k, int n,
      double p) { . . }
    static BigInteger Choose(int n, int k) { . . }
  }
}

Después de visualizar un par de mensajes de introducción, el método Main configura y muestra datos de demostración para una prueba de signos:

double[] before = new double[] { 70, 80, 75, 85, 70, 75, 50, 60 };
double[] after  = new double[] { 65, 78, 72, 87, 68, 74, 48, 63 };
Console.WriteLine("The weight data is: \n");
ShowVector("Before:  ", before, 0, "");
ShowVector("After :  ", after, 0, "\n");

En un escenario que no sea de demostración con una muestra cualquiera que supere los 30 pares de datos, tendría probablemente datos almacenados en un archivo de texto y escribiría un método de aplicación auxiliar para leer y almacenar los datos. El uso de matrices paralelas es en el enfoque más común al realizar una prueba de signos.

A continuación, la demostración usa el método DoCounts para contar el número de pares de elementos donde se produjo una pérdida de peso ("éxito"), y el número de aumentos de peso ("fracaso"):

int[] counts = DoCounts(before, after);
Console.WriteLine("Num success = " + counts[2]);
Console.WriteLine("Num failure = " + counts[0]);

El valor devuelto es una matriz donde la celda 0 contiene el recuento de fracasos (aumento de peso), la celda 1 contiene el recuento de casos que no presentaron ningún cambio y la celda 2 contiene el recuento de éxitos (pérdida de peso). Cuando aún no era tan fácil tener a su disposición un equipo, los recuentos se realizaban manualmente colocando un signo "+" junto a los éxitos y un signo "-" junto a los fracasos. De ahí el nombre que recibe la prueba de signos. Para los datos de la demostración, el enfoque manual tendría el aspecto siguiente:

Before:  70 80 75 85 70 75 50 60
After :  65 78 72 87 68 74 48 63
         +  +  +  -  +  +  +  -

Observe que la prueba de signos no tiene en cuenta la magnitud de un aumento o una pérdida de peso. A continuación, la demostración prepara la llamada a la prueba de signos de la siguiente manera:

int k = counts[2];
int n = counts[0] + counts[2];
Console.WriteLine("k = " + k + " n = " + n + " p = 0.5");

La variable k contiene el recuento de éxitos. La variable n contiene el recuento total de pares de datos. En esta situación, no existen casos en que los pesos de antes y después sean iguales. En tales situaciones, el enfoque más común es rechazar empates. No obstante, es posible que en algunas situaciones quiera incluir los empates como éxitos o como fracasos. Por ejemplo, en un programa de pérdida de peso, la ausencia de cambios en el peso se podría considerar un fracaso.

El método Main concluye de la siguiente manera:

double p_value = BinomRightTail(k, n, 0.5);
Console.WriteLine("\nProbability of 'no effect' is " + p_value.ToString("F4"));
Console.WriteLine("Probability of 'an effect' is " + (1 - p_value).ToString("F4"));

La prueba de signos es en realidad un ejemplo específico de la prueba binomial más general. La función definida por el programa BinomRightTail acepta el número de éxitos, el números de pares de datos y un valor de probabilidad, de 0,5 en este caso. Cuando una prueba binomial usa 0,5 para el parámetro de probabilidad, es una prueba de signos, como explicaré en breve.

Explicación de la función Choose

La pruebas de signos usa la distribución binomial, que a su vez usa la función Choose. La función Choose(n, k) devuelve el número de maneras de seleccionar k elementos de n elementos. Por ejemplo, Choose(5, 3) es el número de maneras en que puede seleccionar tres elementos de cinco elementos. Supongamos que los cinco elementos son (A, B, C, D, E). Existen 10 maneras de seleccionar 3 de los elementos:

(A, B, C), (A, B, D), (A, B, E), (A, C, D), (A, C, E),
(A, D, E), (B, C, D), (B, C, E), (B, D, E), (C, D, E)

La función Choose se define Choose(n, k) = n! / [k! * (n-k)!], donde el carácter "!" significa factorial. Por tanto:

Choose(5, 3) = 5! / (3! * 2!) = (5 * 4 * 3 * 2 * 1) / (3 * 2 * 1) *
  (2 * 1) = 120 / 12 = 10

La implementación de la función Choose es complicada porque el valor devuelto puede ser astronómicamente alto incluso para valores moderados de n y k. Por ejemplo:

Choose(100, 25) = 242,519,269,720,337,121,015,504

Para devolver los altísimos valores que puede generar la prueba de signos, el programa de demostración usa el tipo BigInteger en el espacio de nombres System.Numerics. La implementación de demostración de Choose usa dos problemas matemáticos para mejorar la eficacia. Primero, tal como se demuestra, Choose(n, k) = Choose(n, n-k). Por ejemplo:

Choose(10, 7) = Choose(10, 3)

Al usar el valor más bajo de k, puede realizar menos cálculos. Segundo, existe una definición alternativa de Choose que se explica mejor con un ejemplo:

Choose(10, 3) = (10 * 9 * 8) / (3 * 2 * 1)

En palabras, el denominador es k! y el numerador usa solo los primeros términos k de la ecuación n!, y muchos términos se anulan. Si reunimos estas ideas, la implementación de demostración de Choose se presenta en la Figura 3.

Figura 3 Función Choose

static BigInteger Choose(int n, int k)
{
  if (n == k) return 1; // Required special case
  int delta, iMax;
  if (k < n - k) { // Ex: Choose(100,3)
    delta = n - k;
    iMax = k;
  }
  else { // Ex: Choose(100,97)
    delta = k;
    iMax = n - k;
  }
  BigInteger ans = delta + 1;
  for (int i = 2; i <= iMax; ++i)
    ans = (ans * (delta + i)) / i;
  return ans;
}

Explicación de la distribución binomial

La clave para comprender cómo implementar e interpretar la prueba de signos es entender la distribución binomial. Esta se explica mejor a través de un ejemplo. Imagine que tiene una moneda sesgada y que, al girarla, la probabilidad de obtener cara es 0,6 y la probabilidad de obtener cruz es 0,4, y supongamos que define el éxito como la obtención de cara. Si gira la moneda n = 8 veces, la distribución binomial le ofrece la probabilidad de obtener exactamente k éxitos en n pruebas, donde la probabilidad de un éxito en una sola prueba es p = 0,6 en este ejemplo.

La probabilidad de obtener exactamente ocho caras y cero cruces en ocho lanzamientos es la probabilidad de obtener ocho caras consecutivas, en este caso:

Pr(X = 8) = 0.6 * 0.6 * 0.6 * 0.6 * 0.6 * 0.6 * 0.6 * 0.6 = (0.6)^8 * (0.4)^0 = 0.0168

Para obtener exactamente siete caras en ocho lanzamientos, puede obtener siete caras y una cruz en cualquier de los ocho lanzamientos. Existen ocho combinaciones:

Pr(X = 7) = Choose(8, 1) * [ (0.6)^7 * (0.4)^1 ] = 8 * 0.0280 * 0.4 = 0.0896

La ecuación general de la probabilidad de obtener exactamente k éxitos en n pruebas, donde p es la probabilidad de un éxito en una sola prueba, es la siguiente:

P(X = k) = Choose(n, k) * p^k * (1-p)^n-k

En el caso de la prueba de signos, p es siempre 0,5, de modo que 1-p también es 0,5 y la ecuación se simplifica de la siguiente manera:

P(X = k) = Choose(n, k) * (0.5)^n

De este modo, para los datos de la demostración, existen n = 8 pruebas (pares de datos) y existen k = 6 éxitos (pérdidas de peso), por lo que la probabilidad de obtener exactamente seis éxitos es:

P(X = 6) = Choose(8, 6) * (0.5)^8 = 28 * 0.0039 = 0.1094

Las probabilidades de obtener exactamente de cero a ocho éxitos en ocho pruebas si p = 0,5 se muestran en el gráfico de la Figura 4.

La implementación de una función que devuelva la probabilidad binomial es directa:

static double BinomProb(int k, int n, double p)
{
  // Probability of k "successes" in n trials
  // if p is prob of success on a single trial
  BigInteger c = Choose(n, k);
  double left = Math.Pow(p, k);
  double right = Math.Pow(1.0 - p, n - k);
  return (double)c * left * right;
}

La distribución binomial de n = 8 y de p = 0,5
Figura 4 La distribución binomial de n = 8 y de p = 0,5

La demostración define una función binomial general que acepta p como un parámetro. Una alternativa es definir una versión que suponga que p = 0,5 y simplificar el cálculo como hemos descrito anteriormente. La demostración no incluye una comprobación de errores. Por ejemplo, en un entorno de producción, probablemente querría asegurarse de que k <= n; de que ni k ni n son negativos; y de que p está entre 0,0 y 1,0.

Implementación de la prueba de signos

La idea de la prueba de signos es calcular la probabilidad de que no se haya producido ningún efecto. Conceptualmente, esto significa que cualquier diferencia entre un valor de antes y un valor de después se debe meramente al azar. Matemáticamente, esto significa que la probabilidad de un aumento o una pérdida de peso es de 0,5.

La prueba de signos supone que no se produce ningún efecto y, a continuación, calcula la probabilidad de que el número de éxitos observado podría haberse generado con esta suposición. Para el caso de los datos de la demostración, donde hubo seis éxitos (pérdidas de peso) en ocho pruebas, en lugar de calcular la probabilidad de exactamente seis éxitos como podría suponer, se calcula la probabilidad de seis éxitos o más. Esta idea es bastante sutil.

El cálculo de la probabilidad de k éxitos o más se denomina en ocasiones una prueba de cruz derecha. Por tanto, para implementar la prueba de signos, se calcula la probabilidad de k éxitos o más mediante el cálculo de la probabilidad de exactamente k éxitos más k+1 éxitos, más k+2 éxitos y así sucesivamente. La demostración lo implementa de la siguiente manera:

static double BinomRightTail(int k, int n, double p)
{
  // Probability of k or more successes in n trials
  double sum = 0.0;
  for (int i = k; i <= n; ++i)
    sum += BinomProb(i, n, p);
  return sum;
}

Todo lo necesario para completar una prueba de signos son funciones opcionales para contar el número de éxitos y mostrar los valores. La demostración define el método de recuento como:

static int[] DoCounts(double[] before, double[] after)
{
  int[] result = new int[3];
  for (int i = 0; i < before.Length; ++i) {
    if (after[i] > before[i])
      ++result[0];  // Fail
    else if (after[i] < before[i])
      ++result[2]; // Success
    else
      ++result[0]; // Neither
  }
  return result;
}

El método de visualización de la aplicación auxiliar es:

static void ShowVector(string pre, double[] v, int dec, string post)
{
  Console.Write(pre);
  for (int i = 0; i < v.Length; ++i)
    Console.Write(v[i].ToString("F" + dec) + " ");
  Console.WriteLine(post);
}

Un diseño alternativo es combinar el recuento de éxitos-fracasos y los cálculos binomiales en un metamétodo más grande.

Resumen

Siempre debe interpretar los resultados de una prueba de signos con cautela. Es mejor decir "la prueba de signos sugiere que existe un efecto" que decir "existe un efecto".

El problema del ejemplo se denomina prueba unilateral o de una cruz. Dado que este ejemplo implicaba un experimento de pérdida de peso, un efecto sería más pérdidas de peso (éxitos) de las que obtendría por probabilidad. También puede realizar una prueba de signos bilateral, también denominada de doble cruz. Por ejemplo, supongamos que está llevando a cabo un experimento con algún tipo de analgésico. Como parte de su experimento, pesa a los sujetos de la prueba antes y después del experimento. No tiene ningún motivo para creer que el analgésico afectará al peso. En otras palabras, un efecto podría ser tanto una pérdida como un aumento de peso.

La parte más delicada de la prueba de signos es mantener claras las definiciones. Existe una posible confusión, ya que se dan múltiples simetrías en cada problema. Puede definir un éxito como un aumento o una reducción en un valor posterior. Por ejemplo, en el ejemplo de la pérdida de peso, un éxito sería una reducción del valor posterior. No obstante, si sus datos representan puntuaciones de prueba en algún tipo de examen antes y después de estudiar, un éxito se definiría probablemente como un aumento en el valor posterior.

Esta prueba de signos es un ejemplo de lo que se conoce como una prueba estadística no paramétrica. Esto significa, en parte, que la prueba de signos no realiza ninguna suposición sobre la distribución de los datos sometidos a estudio. En lugar de usar una prueba de signos, es posible usar lo que se conoce como una prueba t emparejada. No obstante, la prueba t supone que los datos de la población tienen una distribución normal (gausiana, con forma de campana), que sería casi imposible de comprobar con un conjunto de datos de tamaño reducido. Por este motivo, cuando quiero investigar datos de antes y después, suelo usar una prueba de signos en lugar de una prueba t emparejada.


El Dr. James McCaffrey trabaja para Microsoft Research en Redmond, Washington. Ha colaborado en el desarrollo de varios productos de Microsoft como, por ejemplo, Internet Explorer y Bing. Puede ponerse en contacto con James en jammc@microsoft.com.

Gracias a los expertos técnicos de Microsoft por revisar su artículo: Chris Lee y Kirk Olynyk