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-squared 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-squared statistic attempts to distinguish between random and meaningful data with some degree of confidence.
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-squared, Version 1
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-squared 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-squared, 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-squared 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-squared = " << 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-squared 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-squared program. Accordingly, we reduce the size of the observed frequency array to a more realistic size.
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.
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).
Defining parameter N as a double effectively implements the cast needed to avoid a truncation error when calculating the observed frequencies (d).
Calculates the observed frequencies, fo, without casting.
sum and the for-loop implement the Σ or summation operation.