Classifying Reported Crime Categories in San Francisco with Python

This updated tutorial classifies the eight most frequent reported crime categories in a deterministic 23,831-row San Francisco Open Data extract. It uses a fixed stratified holdout, train-only one-hot encoding, a class-balanced random forest, and a most-frequent baseline.
Scope and data
The target is the category assigned to an already reported incident. The model does not predict whether a crime will occur. Features are day of week, police district, hour, month, address pattern, longitude, and latitude.

Pipeline
preprocessor = ColumnTransformer([
('categorical', OneHotEncoder(handle_unknown='ignore'), categorical),
('numeric', 'passthrough', numeric),
])
model = Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(
n_estimators=180, min_samples_leaf=3,
class_weight='balanced_subsample', random_state=42
)),
])
The split is stratified across all 8 classes. On the holdout, balanced accuracy was 0.343 and macro F1 was 0.329. The most-frequent baseline macro F1 was 0.045. Macro metrics give every category equal weight and reveal weak minority-class performance.

Limitations and responsible use
Reports reflect exposure, reporting behavior, policing practices, and historical policy. District and coordinates can encode socioeconomic proxies. Random splitting lets nearby incidents from similar periods enter both partitions. This model must not be used to target people, infer individual risk, or justify enforcement. A serious study needs temporal and geographic validation, uncertainty estimates, drift monitoring, and community oversight.



