HW05 Programming Tips
This guide focuses on how to work with the HW05 starter code. It assumes you are comfortable writing Python functions, loops, lists, and dictionaries. No third-party libraries are required. The command-line parser, file-format selection, and output formatting are already implemented.
Understand the data passed between functions
The readers return a Partition, the model trains on that partition, and
prediction operates on feature dictionaries. These are different inputs:
| Expression | Type | Meaning |
|---|---|---|
partition.data |
list[Example] |
The examples in a dataset. |
example.features |
dict[str, str] |
One example’s feature names and values. |
example.label |
int |
Its class, from 0 through K - 1. |
partition.F |
dict[str, list[str]] |
Each feature’s possible values. |
partition.K |
int |
The number of possible classes. |
For example, a single example might have
features = {"color": "blue", "shape": "round"} and label 1.
The corresponding domains might include
F = {"color": ["blue", "red"], "shape": ["round", "square"]}.
A domain lists possibilities, including values absent from that example.
The aliases FeatureValues and FeatureDomains in Partition.py name the
two dictionary types above. They do not change how dictionaries behave.
When calling model.classify(...), pass example.features, not example
or the entire partition. Keep feature values as strings, even when they look
like numbers; only class labels become integers.
Read CSV and ARFF without losing their structure
For CSV, csv.reader handles delimiters and quoted fields. Read the header
once, then process the remaining rows:
import csv
from io import StringIO
# StringIO lets you practice on a tiny CSV without creating a file.
with StringIO("color,shape\nblue,round\nred,square\n") as stream:
reader = csv.reader(stream)
names = next(reader)
for row in reader:
record = dict(zip(names, row))
print(record)
For a real file, replace StringIO(...) with
open(filename, newline="", encoding="utf-8"). next(reader) consumes the
header; the loop starts at the first data row. Check row lengths before
using zip, which otherwise silently stops at the shorter input.
In the census reader, separate sex from the feature dictionary and collect
first-seen values for each remaining column in F. Strip surrounding
whitespace so "Private" and " Private" do not become different categories.
ARFF requires two phases. Before @data, collect attribute declarations;
after it, read examples. A Boolean such as in_data can track the phase.
Skip blank lines and full-line % comments. Compare keywords without regard
to case, but preserve the feature values themselves.
The header supplies domains, so retain declared values even if no data row
uses them. Map the final attribute’s class values to integers in declaration
order. Do not infer K solely from labels observed in the rows: a declared
class might have no examples. The reader docstring specifies the supported
format; re is available if useful, but regular expressions are not required.
Check before moving on: inspect one returned example, its label,
partition.F, and the partition’s size. A reader bug can otherwise look
like a probability or classification bug later.
Build probability tables that prediction can use directly
The model’s required storage layout is:
log_class[k] → one class's log prior
log_feature[feature][value][k] → one log likelihood
For example, log_feature["color"]["blue"][1] represents
log P(color = blue | class = 1). Work through the indexing order when
constructing and debugging the tables.
Training belongs in __init__. Store the resulting tables as attributes
such as self.log_class so later methods can retrieve them. Prediction
should look up stored probabilities, not scan the training examples again.
A useful implementation sequence is to check class counts, then feature-value counts within each class, then the smoothed log probabilities. Include every declared feature value for every class, including zero-count combinations. This is where Laplace smoothing matters.
Avoid shared mutable containers when initializing counts or matrices:
rows = [[0, 0]] * 2 # Both entries refer to the same row.
rows[0][1] += 1
print(rows) # [[0, 1], [0, 1]]
rows = [[0, 0] for _ in range(2)] # Separate rows.
rows[0][1] += 1
print(rows) # [[0, 1], [0, 0]]
The same issue applies to lists stored under different dictionary keys: create a fresh count list for each feature value.
Use log scores consistently
math.log computes the natural logarithm. Apply it to the smoothed
probabilities, or use the equivalent difference of logarithms:
import math
numerator, denominator = 3, 8
log_probability = math.log(numerator) - math.log(denominator)
assert math.isclose(log_probability, math.log(numerator / denominator))
Add log probabilities when combining features. Do not take the log of a
product of many small probabilities: the product can already have rounded
to zero before math.log sees it. Negative log scores are expected, and
-2.0 is a higher score than -10.0.
predict_log_scores() returns one score per class, in class order.
classify() returns the index of the highest score, not the score itself.
Remember the required smallest-label tie break. No exponentiation or
posterior normalization is needed to select the highest-scoring class.
A value with zero observations within a class still has a smoothed
probability if it belongs to the feature’s domain. A test value outside
the training domain is a different case: the HW05 interface requires
KeyError. Do not silently expand domains using test data.
Test each stage independently
The computational methods return results without printing or changing their
inputs. This lets you test a reader, score, or metric without running the
whole experiment. The provided main() handles presentation.
Start with a tiny partition whose counts you can calculate by hand. Check
that the class probabilities sum to one and that, for a fixed feature and
class, its value probabilities sum to one. Since the tables store logs,
use math.exp to recover probabilities for this debugging check only.
Use approximate comparisons for floating-point results.
For metrics, use unequal class sizes and some mistakes in both directions.
Remember that confusion-matrix rows are actual labels and columns are
predictions. Accuracy weights examples equally; BER averages the error
rates of the actual classes equally. A class with no actual examples has an
undefined error rate, which the supplied BER docstring requires you to
handle with ValueError.
Run a focused public test while implementing a particular stage:
python3 -m unittest tests.TestDataAndMetrics.test_csv_label_is_excluded -v
python3 -m unittest tests.TestNaiveBayes.test_smoothed_probabilities_and_absent_class -v
Then run python3 tests.py, followed by the tennis and zoo experiments,
before using the larger census datasets. These commands should be run from
the homework folder so that local imports and relative data paths resolve.
If a test fails, inspect the smallest intermediate result that could explain
it: a parsed category, a count, one stored likelihood, or one class score.
For unexpected KeyErrors, repr(value) helps reveal whitespace, and
examining the dictionary at each nesting level helps locate an incorrect
lookup. Remove temporary debugging prints from computational helpers before
submitting.