Due: Tuesday, October 27 at 11:59 PM EDT
In this homework you will implement a Naive Bayes classifier for categorical data and investigate how a protected attribute can be encoded in other features. Using a cleaned version of the 1994 Adult Census Income dataset, you will predict the recorded sex attribute from the remaining columns and consider what this reveals about the limits of simply removing protected attributes from a model’s inputs.
By the end of the homework, you should be able to:
Credit: Adapted materials from Thao Nguyen and Sara Mathieson, modified from materials created by Ameet Soni and Allison Gong.
Start with the HW05 programming tips for help working with the starter’s data structures, CSV/ARFF readers, probability tables, log scores, and tests.
Download the Homework 5 starter files from Piazza. Your working folder should contain:
NaiveBayes.py
Partition.py
run_NB.py
tests.py
README.md
HW05_written_problems.pdf
HW05_written_problems.tex
data/
tennis_train.arff
tennis_test.arff
zoo_train.arff
zoo_test.arff
1994_census_cleaned_train.csv
1994_census_cleaned_test.csv
1994_census_cleaned_corrected_train.csv
1994_census_cleaned_corrected_test.csv
Use Python 3.9 or newer. No third-party packages are required.
Implement the classifier yourself rather than using a library implementation
of Naive Bayes. You may reuse your argument parsing and ARFF-reading code from
HW04, adapting it to the interfaces and multiclass labels described below.
The command-line parser, file-format dispatch, output formatting, and main
program are provided. Complete the NotImplementedError placeholders and
preserve the supplied type annotations and interfaces.
Keep the model in NaiveBayes.py. Use run_NB.py to read data, train the
model, make predictions, and report results. Importing either module must not
run the analysis; call main() only beneath an
if __name__ == "__main__": guard.
Complete the included HW05_written_problems.pdf worksheet. Its six sections
cover joint and conditional probabilities, Bayes’ rule and base rates, Laplace
smoothing, Naive Bayes predictions and log scores, conditional independence,
and multiclass prediction and runtime analysis.
You may print and complete the worksheet by hand or typeset your answers in
the supplied HW05_written_problems.tex. Show your calculations and clearly
label final answers. A calculator is allowed, but do not use Python or another
programming language to solve the written problems. Keep exact fractions until
the final step or round final decimals to four places.
Submit one legible PDF named HW05_written_problems.pdf, either scanned from
your handwritten work or compiled from your completed LaTeX source. These
problems are separate from the experiment analysis in README.md.
The provided parse_args(argv=None) in run_NB.py accepts:
-r/--train_filename: the training-data path; and-e/--test_filename: the test-data path.Both arguments are required. The provided read_data() selects the reader
based on the input file extension. For example:
python3 run_NB.py \
-r data/1994_census_cleaned_train.csv \
-e data/1994_census_cleaned_test.csv
python3 run_NB.py -r data/zoo_train.arff -e data/zoo_test.arff
The starter code defines Example and Partition in Partition.py and
imports them into run_NB.py:
Example.features is a dictionary mapping feature names to values.Example.label is an integer in {0, 1, ..., K - 1}.Partition.data is a list of examples, and Partition.n is its length.Partition.F maps each feature name to its possible values.Partition.K is the number of classes.All input features are discrete and unordered. Treat even numeric-looking feature values as categories. Your model must support any number of classes, including the seven classes in the zoo dataset.
Complete read_arff(filename) so that it returns a Partition. Read feature
names and possible values from the attribute declarations and examples from
the data section. The final attribute is the class label.
Unlike HW04, labels must be integers from 0 through K - 1, rather than
-1 and +1. Use the declared class values to determine K, and keep the
class mapping consistent between training and test data. Test this reader
with both the tennis and zoo datasets.
Complete read_csv(filename) so that it also returns a Partition. The first
row contains column names. Collect the observed values for each feature in
F; CSV files do not provide the attribute declarations found in ARFF files.
Python’s csv module is useful here.
For the census files, use sex as the label, with Male represented by 0
and Female by 1. Exclude sex from both Example.features and
Partition.F. These are the categories recorded in the supplied dataset.
The income column remains an input feature, even though income was the
prediction target in the original dataset.
Preserve a consistent feature order when reading the files. A regular Python
dictionary preserves insertion order; you may also use OrderedDict.
For the original census files, verify that your reader produces:
num train = 28998, num classes = 2
num test = 7419, num classes = 2
Fit the model using only the training partition. Do not use test examples to estimate probabilities or choose the majority class.
Before implementing Naive Bayes, answer these questions in README.md:
For every confusion matrix, use rows for actual labels and columns for predicted labels. Clearly identify the label order.
Complete the NaiveBayes class in NaiveBayes.py. Preserve these interfaces:
from Partition import FeatureValues, Partition
class NaiveBayes:
def __init__(self, partition: Partition) -> None:
...
def predict_log_scores(self, x_test: FeatureValues) -> list[float]:
...
def classify(self, x_test: FeatureValues) -> int:
...
def predict(self, examples: list[FeatureValues]) -> list[int]:
...
The constructor receives the training Partition. The classify() method
receives one example’s feature dictionary and returns an integer class label.
Here FeatureValues means dict[str, str]. Complete the supplied counting
and log-probability helper methods as well. You may add other helpers.
Naive Bayes assumes that the features are conditionally independent given
the class. For each candidate class k, its score is proportional to:
P(y = k) × product over features j of P(x_j | y = k)
Let n be the number of training examples, K the number of classes,
N_k the number of training examples in class k, and N_k,j,v the number
of training examples in class k whose feature j has value v. Let
|F_j| be the number of possible values of feature j.
Use add-one Laplace smoothing for both priors and likelihoods:
P(y = k) = (N_k + 1) / (n + K)
P(x_j = v | y = k) = (N_k,j,v + 1) / (N_k + |F_j|)
Include every possible value of each feature, even when its count within a particular class is zero.
Compute and store all model probabilities in the constructor, using natural logarithms. Do not recount training examples or recompute probabilities each time you classify a test example.
Store the log priors in log_class[k]. Store likelihoods in
log_feature[feature_name][feature_value][k], a nested dictionary whose
innermost values are lists indexed by class. These named attributes allow
individual training calculations to be tested independently.
Taking a logarithm turns a ratio into a difference of logarithms:
log P(y = k) = log(N_k + 1) - log(n + K)
log P(x_j = v | y = k) = log(N_k,j,v + 1) - log(N_k + |F_j|)
For the original census training data, the smoothed log priors should be approximately:
[-0.39003307271614496, -1.1302097192251246]
In predict_log_scores(), compute a score for every class:
score(k) = log P(y = k) + sum over features j of log P(x_j | y = k)
Return a list of scores in class order. In classify(), return the class
with the largest score, choosing the smallest class label in a tie. You do
not need to exponentiate the scores or normalize them into posterior
probabilities. Implement predict() to return predictions for a list of
feature dictionaries, preserving input order.
Missing required features and values outside the training domain must raise
KeyError. Ignore extra feature keys. Do not expand feature domains using
test data. The supplied census test values all occur in their corresponding
training domains.
The supplied main program trains one model and classifies the test examples. An individual prediction can also be tested directly:
nb_model = NaiveBayes(train_partition)
y_hat = nb_model.classify(example.features)
Complete these independently testable functions in run_NB.py:
def confusion_matrix(y_true: list[int], y_pred: list[int], K: int) -> list[list[int]]:
...
def accuracy(y_true: list[int], y_pred: list[int]) -> float:
...
def balanced_error_rate(matrix: list[list[int]]) -> float:
...
The confusion matrix must contain integer counts in a K by K list of
lists. Compute accuracy as the fraction of correct predictions. BER is the
mean of the class-specific error rates; see Part 4 for the binary formula.
Follow each function’s docstring for empty inputs and invalid arguments.
These functions and the model methods must return values without printing or mutating their inputs. The provided main program prints the confusion matrix, accuracy, correct prediction count, and BER when it is defined.
Run the public tests from your homework folder:
python3 tests.py
The eight sample tests check readers, smoothing, scores, multiclass predictions, tie breaking, and metrics on small examples. They fail until you complete the relevant methods. Passing them does not guarantee full credit; also test cases of your own and run the complete experiments.
Run the small datasets before the census experiments:
python3 run_NB.py -r data/tennis_train.arff -e data/tennis_test.arff
python3 run_NB.py -r data/zoo_train.arff -e data/zoo_test.arff
The reference results from the original lab are:
| Dataset | Correct predictions | Accuracy |
|---|---|---|
| Tennis | 13 out of 14 | 0.928571 |
| Zoo | 30 out of 35 | 0.857143 |
For tennis, the confusion matrix is:
predicted
0 1
actual 0 4 1
1 0 9
For zoo, the confusion matrix is:
predicted
0 1 2 3 4 5 6
actual 0 13 0 0 1 0 0 0
1 0 7 0 0 0 0 0
2 0 1 0 0 1 0 0
3 0 0 0 4 0 0 0
4 0 0 0 0 1 0 0
5 0 0 0 0 0 3 0
6 0 0 0 0 0 2 2
Run your model on the original census files, then train a new model and evaluate it using the corrected files:
python3 run_NB.py \
-r data/1994_census_cleaned_corrected_train.csv \
-e data/1994_census_cleaned_corrected_test.csv
Answer the following questions in README.md. Include the confusion matrix
and accuracy for each census experiment.
Compute the balanced error rate (BER) on the corrected census test data.
For this binary problem, BER is the mean of the two class-specific error
rates. Taking class 1 as positive:
BER = 0.5 × (FP / (FP + TN) + FN / (FN + TP))
Show your calculation and interpret the result. What does BER tell you that overall accuracy alone may obscure when class sizes differ?
Complete the short, ungraded questionnaire at the end of README.md as well.
Document any extension in README.md and keep the required categorical
classifier usable with the supplied datasets.
Submit one .zip file to the HW05 assignment on Gradescope containing:
NaiveBayes.py
Partition.py
run_NB.py
README.md
HW05_written_problems.pdf
Include any additional helper modules needed to run your code. You do not
need to include data/ unless Gradescope’s submission instructions explicitly
request it.
Before submitting:
README.md;HW05_written_problems.pdf; andFollow the course collaboration and attribution policies. You may discuss approaches at the level permitted by the course policy, but submitted code and analysis must be your own unless the assignment is explicitly designated as collaborative. Credit any permitted sources or assistance.