Downloads · 30 days
0
Noam12345/apartments-rent-prediction-assignment-2
apartments-rent-prediction-assignment-2 is a tabular regression model from Noam12345. Use it for the tabular regression task on the model card, and read the license before you ship it in a product. It is set up for scikit-learn. The card lists the license as mit.
Assignment 2 · Classification, Regression, Clustering, Evaluation · May 2026
Downloads · 30 days
0
Access
Public
Updated May 10, 2026
Repo size
483 MB
Likes
0
Public
Click a slice to open those files.
.mp4193 MB · 65%
From the Hugging Face model README
Assignment #2 · Classification, Regression, Clustering, Evaluation · May 2026
<video src="https://huggingface.co/Noam12345/apartments-rent-prediction-assignment-2/resolve/main/presentation.mp4" controls="controls" style="max-width: 720px;"></video>
Bonus: there's a live interactive predictor at the end of this README. jump to the demo.
This project takes ~99,000 US apartment listings and tries to answer two questions:
1. Given an apartment's location, size, and amenities, can we predict its monthly rent?
2. Can we classify a listing as Cheap / Mid / Premium for its city's market? Tiers are defined relative to each city's own price distribution, so "Premium" means high-end for that local market rather than just expensive in absolute dollars.
The pipeline goes from raw scraped data to two trained Decision Tree pipelines: a regressor that predicts monthly rent at RMSE = $426 (R² = 0.72), and a tier classifier at 62% accuracy / 0.62 F1 macro on a balanced three-class problem.
| Property | Details |
|---|---|
| Source | UCI Apartments for Rent Classified |
| Original format | .7z archive containing a single CSV (UCI compresses large datasets) |
| CSV format | semicolon separated (sep=';'), Latin 1 encoded (encoding='cp1252'), handled in the notebook |
| Raw Size | 99,492 rows × 22 features |
| Final Size | 99,384 rows × 32 features (after cleaning and engineering) |
| Target Variable | price (monthly rent in USD) |
| Tier Definition (Part 7) | Quantile binning into 3 classes, computed per city (33rd / 66th percentiles within each city), fit on y_train only. |
The extracted CSV (apartments_for_rent_classified_100K.csv) is bundled in this repo so it's easy to reproduce. To regenerate from source: download the .7z from UCI and extract it with 7zip, p7zip, or Keka.
| Type | Columns |
|---|---|
| Numeric | bathrooms, bedrooms, square_feet, latitude, longitude, time |
| Categorical | state, cityname, source, has_photo, pets_allowed, fee, category, price_type |
| Textual | title, body, amenities (engineered into numeric counts) |
| Excluded | id (identifier), currency (constant: USD), price_display (string of price), address (high cardinality free text) |
import pandas as pd, numpy as np
import matplotlib.pyplot as plt, seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.linear_model import LinearRegression, Ridge, LogisticRegression
from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
SEED = 42
Built in Google Colab with the dataset uploaded via the Files sidebar.
Encoding: UCI's CSV is semicolon separated and Latin 1 encoded, so I read it with pd.read_csv(sep=';', encoding='cp1252').
Missing values:
bathrooms, bedrooms, square_feet, latitude, longitude): filled with the column median (robust to outliers).cityname, state, pets_allowed, amenities, body, title): filled with the string 'Unknown'.Date parsing: the time column is a Unix timestamp (seconds since 1970). I parsed it with pd.to_datetime(unit='s') and pulled out listing_year and listing_month as features.
Duplicates: Exact duplicate rows removed.
Constant columns: currency was 100% USD, so I dropped it.
Impossible values dropped (23 listings):
price < $200 or price > $20,000: likely placeholder errors or extreme luxury outliers.square_feet < 100 or > 10,000: 0 sqft entries and unrealistic mansions.bedrooms > 8 or bathrooms > 8: data entry errors.Final shape after cleaning: (99,384, 23)

Box plots of price, square_feet, and bedrooms show clear extreme tails. I filtered outliers using explicit business rule thresholds (see the Cleaning section), rather than blindly removing them by IQR.

A few takeaways:
square_feet, bedrooms, and bathrooms all correlate with each other, and modestly with price.latitude/longitude correlations with price are close to zero. Geography matters, but not linearly: NYC and SF are far apart on the map but both expensive.
Raw price is very right skewed, while log(price) is much closer to normal. This is also why I added log_sqft later on.
Median rent is around $1,400. The mean is dragged up by the luxury tail.

The usual suspects show up at the top: CA, NY, MA, DC, HI, often 2 to 3 times the cheapest states. Geography is clearly a big price driver, which is part of the motivation for the K Means clustering on lat/long below.

Pearson correlation is about 0.4. Size matters, but with a lot of variance. A 1,000 sqft apartment in NYC and one in Kansas have very different rents, which suggests interactions matter more than any single feature.

Honestly, the signal here is weaker than I expected:
Bottom line: photo presence and pet policy are weak signals on their own. I left them in the model as one hot features, but didn't expect them to dominate, and they don't show up in the top feature importances later on.

Clear monotonic relationship: each extra bedroom adds to median rent, though the extra premium shrinks past 3 bedrooms.
I added several types of features: structural, text derived, polynomial/interaction, date derived, and cluster based.
| Category | Features |
|---|---|
| Structural | rooms_total = bedrooms + bathrooms, log_sqft |
| Polynomial / interactions | sqft_x_bedrooms, sqft_x_bathrooms, beds_x_baths |
| Text derived | title_length, body_length, amenities_count |
| Date derived | listing_year, listing_month |
| Amenity flag | has_pets (binary) |
| Geographic reduction | state_top (top 15 + Other), city_top (top 30 + Other) |
| Cluster features | cluster_id (categorical), dist_to_centroid, cluster_prob_0..3 (soft membership) |
Cluster feature utility check (I wanted to verify the cluster features actually help before keeping them): Linear R² without cluster features: 0.531 · with cluster features: 0.535 (Δ +0.004). Small but positive, so I kept them.
Methodology (avoiding data leakage):
random_state=42).K chosen via Elbow + Silhouette on a training sample.
I tested K from 2 to 8. Silhouette peaks at K = 4, so that's the value I went with.

Four apartment archetypes come out of (latitude, longitude, log_sqft, bedrooms, bathrooms):
cluster_id ends up being a useful feature.
A plain LinearRegression on bathrooms, bedrooms, square_feet, latitude, longitude, plus has_photo, fee, and source one hots.
Result: R² = 0.219, RMSE = $712, MAE = $489.
This is the baseline I want every later model to improve on.

square_feet and bathrooms dominate the linear model's coefficients. The basics matter most when there's nothing else to work with.

| Model | R² (test) | RMSE | MAE |
|---|---|---|---|
| Baseline Linear (raw features) | 0.219 | $712 | $489 |
| Linear Regression (engineered) | 0.535 | $550 | $341 |
| Ridge Regression | 0.535 | $550 | $341 |
| Decision Tree Regressor | 0.721 | $426 | $252 |
Engineered features more than doubled the baseline R² (0.22 to 0.53), and the Decision Tree pushed it further to 0.72. The tree captures interactions like location × size that the linear models simply cannot express on their own.

Predictions track the diagonal a lot more tightly than the baseline. The residuals are roughly centered, with a tail of noisier luxury listings (top 1%).

Top drivers in the winning tree:
square_feet: the single biggest predictor.state_top one hots (CA, NY, etc.): geography is a big one.city_top one hots: neighborhood level signal.cluster_id: the engineered cluster feature, which made it into the top 5.bathrooms: extra bathrooms read as a premium amenity.To verify the Decision Tree's defaults were close to optimal, I ran a small GridSearchCV with 3 fold cross validation, sweeping max_depth ∈ {10, 15, 20, None} and min_samples_leaf ∈ {10, 20, 50}, scoring on R².
| Default Tree | Tuned Tree | |
|---|---|---|
| Test R² | 0.721 | 0.736 |
| Test RMSE | $426 | $414 |
| Best params | n/a | max_depth=None, min_samples_leaf=20 |
The tuning bought a small additional improvement (~0.015 R², ~$12 RMSE), confirming the chosen defaults were already close to the sweet spot. The comparison table above reports un tuned results so all four models sit on the same playing field. No model gets a tuning advantage the others didn't get.
The assignment offered three example strategies (median split, quantile binning, business-rule threshold). I chose quantile binning into 3 classes (Cheap / Mid / Premium), defined per city: each listing is labelled relative to the 33rd and 66th percentiles of price within its own city, with state-level and global fallbacks for thinly-listed cities.
Why this rather than the alternatives:
All thresholds are computed on y_train only and looked up at predict time by cityname / state, so there's no leakage from the test set.

Quantile binning gives roughly 35 / 32 / 32 per class on the training set. Classes are balanced enough that accuracy is a reasonable primary metric; I also report F1 macro to catch per-class weakness.
Q1: Precision or recall? Precision. When the model labels a listing "Premium", we want it to actually be premium for its market — wrong tags erode trust faster than missed ones.
Q2: False Positive or False Negative? False Positives, especially on the Premium class. A false-positive Premium tag is visible and deceptive; a false-negative is recoverable because the listing still surfaces in any price-sorted view.
| Logistic Regression | Decision Tree | k-NN |
|---|---|---|
![]() | ![]() | ![]() |
All three classifiers share the same error pattern: mistakes concentrate on adjacent tiers (Cheap↔Mid, Mid↔Premium) and almost never jump across to far ones (Cheap↔Premium). That's the expected shape under quantile binning — the cutoffs are arbitrary fences in a continuous distribution, and a listing close to a percentile boundary can plausibly fall on either side. Each model also visibly handles the Mid class worst, since Mid is squeezed between two such boundaries inside every city. Logistic Regression in particular under-predicts Mid (lots of Mid listings get pushed into Cheap or Premium); the Decision Tree is meaningfully more balanced across the diagonal.

| Classifier | Accuracy | F1 (macro) |
|---|---|---|
| Logistic Regression | 0.580 | 0.562 |
| Decision Tree Classifier | 0.619 | 0.616 |
| k-NN (k=15) | 0.603 | 0.596 |
The Decision Tree wins, the same pattern as in the regression head: rent depends on feature interactions (size × location, etc.), and trees can express those automatically while linear models cannot. Logistic Regression sits in last place because the relationship between features and tier is non-linear and the model can only fit linear decision boundaries. k-NN lands in the middle — distance-based methods do work here, but the high-dimensional one-hot space dilutes their signal.
| Class | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| Cheap | 0.645 | 0.720 | 0.680 | 6,963 |
| Mid | 0.497 | 0.483 | 0.490 | 6,398 |
| Premium | 0.713 | 0.645 | 0.677 | 6,516 |
Two takeaways:
The Decision Tree wins both heads — regression and classification — because rent depends on interactions between features (location × size, size × bathrooms, etc.) that linear models simply cannot express. The other lift came from feature engineering: log-transformed size, cluster archetypes, and top-N geography together more than doubled the baseline regression R² before any model change. On the negative side, Ridge added nothing over plain Linear (the dataset is large enough not to need regularisation), k-NN was hurt by the high-dimensional one-hot space, and luxury listings (top 1% by price) remained noisy in every model — they depend on features that aren't in the dataset, like views, brand, or exclusivity.
A few caveats worth flagging:
<a id="demo"></a>
Live Gradio app on Hugging Face Spaces. Type in apartment features, get a rent prediction and a tier classification from the trained pipelines:
<iframe src="https://noam12345-apartments-rent-predictor.hf.space" frameborder="0" width="100%" height="800"></iframe>The Space loads regression_winner.pkl and classification_winner.pkl directly, so you're hitting the same Decision Tree pipelines reported in the metrics tables above. Cluster features (cluster_id, dist_to_centroid, cluster_prob_*) are recomputed at predict time using the original training time KMeans.
| File | Description |
|---|---|
Assignment_2_NoamFuchs.ipynb | Full code notebook (all cells run, outputs embedded) |
apartments_for_rent_classified_100K.csv | Extracted dataset (~100 MB), bundled for reproducibility |
regression_winner.pkl | Trained Decision Tree regression pipeline (preprocessing + model) |
classification_winner.pkl | Trained Decision Tree classification pipeline |
presentation.mp4 | 4 to 6 minute video walkthrough |
plots/ | All visualizations referenced above |
README.md | This file |
import pickle
# Regression: predicts monthly rent in USD
with open('regression_winner.pkl', 'rb') as f:
reg_pipe = pickle.load(f)
predicted_rent = reg_pipe.predict(new_listings_df)
# Classification: predicts 'Cheap' | 'Mid' | 'Premium'
with open('classification_winner.pkl', 'rb') as f:
clf_pipe = pickle.load(f)
predicted_tier = clf_pipe.predict(new_listings_df)
Both pipelines already include preprocessing (scaling, one hot encoding), so you can pass in raw feature columns rather than pre transformed values.
Noam Fuchs · Introduction to Data Science · Reichman University (IDC) · Spring 2026