DarScope rent model

A Ridge regression that suggests the yearly rent, in SAR, of a villa or duplex in Riyadh, Jeddah, Dammam or Al Khobar. It learned from 2021 rental ads, so its answer is a suggestion based on the 2021 market, not a valuation.

Made by alm7zm, and used for the rent suggestion in DarScope, a team web app for managing rental houses. Published on Hugging Face and Kaggle.

Results

Measured once, on 304 houses held out from training:

Model Average error (MAPE) Average error (SAR) Within 15%
This model (Ridge) 23.1% 22,998 46.1%
Ridge and gradient boosting averaged 23.1% 22,871 46.4%
Baseline: city and size only 38.8% 34,562 24.7%

Boosting gained nothing over Ridge, so the simpler model was kept: it is one weighted sum that fits in a 10 KB JSON file.

What makes this model different

About 60% of the rows in this dataset are copies of the same ad. With a plain random split, the same houses land in both the training and the test set, so the score measures how well a model remembers houses, not how well it prices new ones. This model was built to avoid that trap and a few others:

A quick approach This model
Copies Kept, so copies of one house sit in both train and test Removed first: 3,718 rows became 1,513 houses
Test set One random 80/20 split 1 house in 5 held out by a hash of its facts, so copies can never cross sides. Used once, at the very end
Choosing settings By hand, checked on one split 5-fold grouped cross-validation on the training houses only
Bad rows Only extreme prices, cut by z-score Six rules found by reading the ads: monthly, daily and weekly prices, apartments and single floors, offices and hotel villas, sale ads, impossible sizes and prices (95 rows)
Descriptions Ignored Arabic text normalised, then read for an annex and internal stairs, and to find the bad rows
Districts Raw text: spelling variants split one district, and same-named districts in two cities merge Normalised names, kept apart by city, and target-encoded so a rarely seen district leans towards the average
Score RMSE and R² on the log scale Error in SAR and in %, and the share within 15%, on houses the model never saw
Ready to use A notebook A 10 KB JSON with a library-free predict.py, plus the scikit-learn pipeline

A leaky split would show a better-looking number. This one holds for a house the model has never seen: 23.1% average error, against 38.8% for a guess from city and size alone.

Inputs

City, district, street front, type (Villa or Duplex), size in m², age in years, bedrooms, bathrooms, living rooms, 13 amenities (kitchen, garage, driver's room, maid's room, furnished, AC, roof, pool, front yard, basement, stairs, elevator, fireplace) and the ad's description, from which two words are read: an annex (ملحق) and internal stairs (درج داخلي).

From these come 29 model inputs: the city, front and type one-hot encoded, the district (within its city) target-encoded, and 25 scaled numbers such as log size, size per bedroom and the amenity count. The model predicts the log of the rent.

How to use it

Without any library, from rent-model.json:

from predict import predict

predict("Riyadh", "حي الملقا", "North", "Villa", size=400, age=0, bedrooms=5, bathrooms=5,
        livingrooms=2, amenities=["kitchen", "garage", "maid_room", "ac"],
        description="فيلا جديدة مع ملحق")
# about 123,468 SAR a year

With scikit-learn 1.7.2, from rent-model.joblib. Its input is a table with the 29 columns that clean.py produces (see Training):

import joblib
import pandas as pd

model = joblib.load("rent-model.joblib")  # a pickle: it can run code, so load only files you trust
houses = pd.read_csv("aqar-clean.csv")
model.predict(houses[model.feature_names_in_])

Both give the same rents, to within 0.01%.

Limits

  • 2021 asking rents. The data is ads, not signed contracts, and rents have moved since.
  • Villas and duplexes in four cities only, 100 to 1,652 m². predict returns None outside that size range. The score holds for yearly rents between 10,000 and 500,000 SAR.
  • Unknown districts. The model knows 203 districts. For any other one it uses the average district, and its error rises to about 32%.
  • Dammam and Al Khobar have far fewer ads than Riyadh and Jeddah, so their districts are learned from fewer houses.

Training

  • Data: Saudi Arabia Real Estate (AQAR) on Kaggle: 3,718 rows, of which 1,418 houses remain after cleaning (about 60% of the rows were copies). The data itself is not included here. clean.py rebuilds the clean table from the original file: it is on Hugging Face and runs as a Kaggle notebook.
  • Split: 1 house in 5 is held out by a hash of its facts, so copies of a house never sit on both sides. Every choice was made with 5-fold grouped cross-validation on the other 1,114.
  • Model: TransformedTargetRegressor(log/exp) around OneHotEncoder + TargetEncoder + StandardScaler, then Ridge(alpha=1). After the test, the final model was fitted on all 1,418 houses; the scores above are from before that.
  • The full story, from the first data checks to every model tried and its scores, is in prediction-model.md. The code is prepare.ipynb, saved without its outputs, which would show rows of the data.

Files

File What it is
rent-model.json The weights: a base, one number per city, front, type and district, and 25 input weights
predict.py Turns a house into a rent from the JSON, with no libraries. python predict.py checks it
rent-model.joblib The scikit-learn pipeline (pickle; needs scikit-learn 1.7.2)
prepare.ipynb The notebook that explored and cleaned the data, compared the models, took the test and exported the model
prediction-model.md The write-up of every step, with the numbers each one found
requirements.txt The library versions the notebook ran with

Licence

The model, code and write-up are under CC BY 4.0: credit alm7zm. They hold no rows of the training data, whose licence reads "Data files © Original Authors".

Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support