At the end of the semester, quarter, or block, students can evaluate each course, expressing their opinions about its various aspects. The evaluations typically include Likert-scale questions with a statement and a ranked list of discrete responses (similar to multiple-choice questions). 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. To simplify the example, we assume that 100 students respond, with 40 choosing (a), 30 choosing (b), 20 choosing (c), and 10 choosing (d).
The chi-square statistic (kī, like kite) is appropriate for evaluating discrete, categorical data, comparing observed and expected frequencies. It calculates a value that, when compared with a reference value from a contingency table, indicates with some level of confidence whether the difference between the calculated and reference values is significant. The example has one question with four categories, "bins" or possible answers, called the degrees of freedom. 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.
|
|
|
The formulas might look intimidating depending on your mathematical background. However, once you understand what the arcane symbols mean, you'll see that they translate directly to corresponding C++ features related to arrays. The formulas relate two frequencies, one expected and the other observed, and distinguish them with subscripts. The expected frequency is the same for each category: the total number of observations or participants (100). The observed frequencies are the number of times a participant selected each category: 40, 30, 20, and 10. In this context, subscripts are a typographic device that C++ can't replicate, so it's customary to form variable names by "hoisting" the subscript up to "regular" text: fe and fo become fe and fo, respectively. fe is a single value and fo is an array of size k, the number of categories or bins (4 in this example).
int k; ... int N; ... double fe = (double)N / k; |
double sum = 0;
for (int i = 0; i < k; i++)
sum += pow(fo[i] - fe, 2);
double chi2 = sum / fe;
|
Of all the symbols appearing in the chi-square formula (Figure 2), the Σ operator condenses the most meaning into the least amount of space. It implies iterating over the elements in the fo array, calculating a term during each iteration, and summing the terms. Consequently, programmers translate Σ to C++ with a for-loop and the addition with assignment operator: +=.
#include <iostream>
#include <cmath>
using namespace std;
int main()
{
cout << "Number of categories (0 < k <= 100):"; // the degrees of freedom, k
int k;
cin >> k;
if (k < 1 || > 100) // validate k to avoid an out-of-bounds error
{
cerr << "The number of categories must be 0 < k <= 100";
exit(1);
}
int fo[100]; // prepare for data input
int N = 0;
for (int i = 0; i < k;) // input the observed frequencies;
{ // the loop doesn't increment i
cout << "Frequency for category " << i+1 << ": "; // in case the frequency is invalid
int f;
cin >> f;
if (f < 0) // validate each frequency
{
cerr << "Negative frequencies are not allowed";
continue;
}
fo[i++] = f; // store valid frequencies and increment i
N += f;
}
double fe = (double)N / k; // calculate chi-square
double sum = 0;
for (int i = 0; i < k; i++)
sum += pow(fo[i] - fe, 2);
cout << "Chi-square = " << sum / fe << endl;
return 0;
}
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 statistic to a critical value from a contingency table. If the calculated value exceeds the critical value in the table, the observed frequencies are significant and unlikely to be random.
The previous solution is short enough to fit in a single function. Nevertheless, separating the code into a client-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 allows developers to tailor the client to a specific problem and determine the data's source (console, file, etc.) and the statistic's ultimate use (output or further calculations).
| lickert.cpp (Client) | chi2.cpp (Supplier) |
|---|---|
#include <iostream>
#include <cmath>
using namespace std;
double chi2(int* fo, int k, double N);
int main()
{
cout << "Number of categories (0 < k <= 9):";
int k = 0;
cin >> k;
if (k < 1 || k > 9)
{
cerr << "The number of categories is out of bounds";
exit(1);
}
int fo[9];
int N = 0;
for (int i = 0; i < k;)
{
cout << "Frequency for category " << i+1 << ": ";
int f;
cin >> f;
if (f < 0)
{
cerr << "Negative frequencies are not allowed";
continue;
}
fo[i++] = f;
N += f;
}
cout << "Chi-square = " << chi2(fo, k, N) << endl;
return 0;
} |
#include <cmath>
using namespace std;
double chi2(int* fo, int k, double N)
{
double fe = N / k;
double sum = 0;
for (int i = 0; i < k; i++)
sum += pow(fo[i] - fe, 2);
return sum / fe;
} |
(double)N, is now implicitly implemented by the function call.