7.8.3. The Chi-Square Statistic

Time: 00:08:36 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | chi2 (Intro) (PDF)

At the end of the semester, quarter, or block, students can evaluate their courses, expressing their opinions about its various aspects. The evaluations typically include Likert-scale questions consisting of a statement and a ranked list of discrete responses (similar to a multiple-choice question). For example, "Did the instructor dress appropriately for class?" The evaluation device might give students four choices: (a) always, (b) most of the time, (c) seldom, and (d) never. Evaluations help institutions select and train qualified instructors and help instructors improve courses, but only if the data is valid. If students rush through the evaluation, making random selections, the evaluation data is meaningless.

The chi-square statistic (chi sounds like the beginning of kite) is the favored analytic tool for assessing this kind of data. The example has one question with four categories, "bins" or possible answers, called the degrees of freedom. To simplify the example, we assume that 100 students respond. If the responses are random, we expect students to select each choice about the same number of times. So, the expected frequency, fe, is 100/4 or 25 per bin. If the observed frequency, fo, is close to the expected frequency, that question is not significant (it does not demonstrate any useful information). But if the data are truly random, the observed and expected frequencies are unlikely to be exactly equal. The chi-square statistic attempts to distinguish between random and meaningful data with some degree of confidence.

\[f_e = {N \over k} \]
The expected frequency. If the events occur randomly with equal probability, then the expected frequency is the total number of responses or observations (100) divided by the number of categories or bins (4). For example, if we roll a fair die, the probability of a face showing is 1/6; if we roll it 100 times, the expected frequency of each possible value is 100/6.
\[\chi^2 = \sum { (f_o - f_e)^2 \over f_e } = {1 \over f_e} \sum (f_o - f_e)^2 \]
The chi-square statistic. In the chi-square (χ2) formula, fo, is the observed frequency (the count of how many students chose each category), one for each bin or category; fe, the expected frequency, is the same for all categories. The Σ operator means to sum the results of the formula for every observed frequency.

The above formulas might look intimidating depending on your mathematical background. However, it will become clear once you see what each term means, how the problem relates to Chapter 7, and how we translate the formula into C++. The key is recognizing that fo is an array whose size is the same as the number of categories or bins in the problem (four in this example). Now that we know we are working with an array let's write the first version of the program.

Chi-square, Version 1

#include <iostream>
#include <cmath>
using namespace std;

int main()
{							// (a)
	int	fo[100];				// observed frequencies
	int	N = 0;					// number of students completing the eval
	int	k = 0;					// number of categories (the degrees of freedom)

	cout << "Enter the observed frequencies for each category, -1 to end:" << endl;

	int	f;					// (b)
	cin >> f;
	while (f != -1 && k < 100)
	{
		fo[k++] = f;
		N += f;
		cin >> f;
	}

	double	fe = (double)N / k;			// (c)

	double	sum = 0;				// (d)
	for (int i = 0; i < k; i++)
		sum += pow(fo[i] - fe, 2);

	cout << "Chi-square = " << sum / fe << endl;	// (e)

	return 0;
}
The chi-square solution as a single function. The first version lets us focus on converting the formula, especially the Σ operator, to C++. The chi-square statistic is appropriate whenever we need to compare observed and expected frequencies, so we make the observed-frequency array accommodatingly large: fo[100] (but I would dislike manually entering that much data).
  1. The variable names follow the formula as closely as possible by writing the subscripts in regular text. The program initializes two variables, using them as accumulators.
  2. A count is always ≥ 0, helping us design the program input. Users enter the observed frequencies (the counts for each category) or a -1 to end the input. k++ counts the number of categories or degrees of freedom. The program "primes the pump" for the first input loop with the initial cin statement above the loop. The loop continues while the input is not -1 and the degrees of freedom are < 100.
  3. Calculates the expected frequency, fe. The typecast prevents a truncation error.
  4. Together, sum and the for-loop implement the Σ or summation operation. It's more efficient, and mathematically equivalent in this case, to move the division operation outside the summation.
  5. The program calculates the final chi-square statistic in the cout statement. We must compare the calculated statistic with a value from a table to determine if the question is likely meaningful or meaningless.

When we run the program with the input: 40, 30, 20, and 10, the output is 20. To answer the ultimate question, "Are the observed frequencies significant?" we compare the chi-square to a critical value found in a table. If the calculated value is greater than the critical table value, the observed frequencies are significant and probably not just a set of random values.

Chi-square, Version 2

The previous solution is short enough to fit in a single function easily. Nevertheless, separating the code into a client and supplier architecture has two advantages. First, it makes the chi-square calculation (the supplier) easier to reuse in other programs - perhaps unrelated to Likert scale questionnaires. Second, it leaves the client to determine the data's source (console, file, etc.) and statistic's ultimate use (output or further calculations).

lickert.cpp (Client) chi2.cpp (Supplier)
#include <iostream>
using namespace std;

double	chi2(int* fo, int k, double N);

int main()
{
    int fo[15];
    int N = 0;
    int k = 0;

    cout << "Enter the observed frequencies, -1 to end:" << endl;

    while (k < 15)						// (a)
    {
        int f;
        cin >> f;
        if (f == -1) break;
        fo[k++] = f;
        N += f;
    }

    cout << "Chi-square = " << chi2(fo, k, N) << endl;		// (b)

    return 0;
}
#include <cmath>
using namespace std;

double chi2(int* fo, int k, double N)	// (c)
{
    double fe =  N / k;			// (d)

    double sum = 0;			// (e)
    for (int i = 0; i < k; i++)
        sum += pow(fo[i] - fe, 2);

    return sum / fe;			// (f)
}
 
 
 
 
 
 
 
 
 
 
 
 
 
Client-supplier chi-square solution. Likert-scale questionnaires typically have five or seven categories per question. We've renamed the application or client likert.cpp, suggesting a narrower use of the chi-square program. Accordingly, we reduce the size of the observed frequency array to a more realistic size.
  1. This variation of the input loop moves f into the loop, reducing its scope and eliminating the initial cin statement. On balance, this version requires an if-statement to detect the -1 and end the loop.
  2. The program calls the chi2 function embedded in the cout statement. The function call converts argument N to a double, matching the corresponding parameter's type in the function prototype (see Function definition, declaration, and prototype examples).
  3. Defining parameter N as a double effectively implements the cast needed to avoid a truncation error when calculating the observed frequencies (d).
  4. Calculates the observed frequencies, fo, without casting.
  5. sum and the for-loop implement the Σ or summation operation.
  6. Calculates and returns the chi-square statistic.