🏡 AI-Driven Agents for Fair and Efficient Property Valuation¶
- Advisor: Dr. Ashraf Elsayed
- Student Name: Fahad Abubaker Bahashwan
- Institution: MidOcean University
- Date: 30, July 2025
What we'll do:
- Load and explore data
- Create geographic features
- Train different ML models
- Compare which model works best
📚 Load Required Libraries¶
Import all the essential Python libraries needed for data processing, analysis, visualization, and modeling.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from geopy.distance import geodesic
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import Ridge, Lasso, LinearRegression
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
import xgboost as xgb
⚙️ Initialize Default Values¶
Define default configuration settings, constants, or initial parameters to be used throughout the notebook.
city_center_coords = {
'Riyadh': (24.702157462388044, 46.74276616894144),
'Jeddah': (21.55406527981118, 39.18189665824677),
'Mecca': (21.42330879427541, 39.82553438746125),
'Medina': (24.45808931439141, 39.62228459678812),
'Dammam': (26.40491223507235, 50.082472821265576),
'Khobar': (26.271817530349022, 50.204861924141625),
'Qatif': (26.5656, 50.0089),
'Al-Ahsa': (26.567036126182284, 50.005786609509734),
'Jubail': (27.003708651215092, 49.629486676571645),
'Khamis Mushait': (18.30250384919938, 42.74398498622415),
'Buraidah': (26.356200460399695, 43.968039364588286),
'Hafar Al-Batin': (28.41245379479697, 45.96700913116033),
'Diriyah': (24.75019708336476, 46.53413641074703),
'Huraymila': (25.112716354278646, 46.09974748404961),
'Nairyah': (27.473500934006946, 48.48032843080484),
'Jumum': (21.622696847405454, 39.69755012299724)
}
# Mapping Arabic province names to English
province_map = {
'الرياض': 'Riyadh',
'جدة': 'Jeddah',
'مكة المكرمة': 'Mecca',
'المدينة المنورة': 'Medina',
'الدمام': 'Dammam',
'الخبر': 'Khobar',
'القطيف': 'Qatif',
'الاحساء': 'Al-Ahsa',
'الجبيل': 'Jubail',
'خميس مشيط': 'Khamis Mushait',
'بريدة': 'Buraidah',
'حفر الباطن': 'Hafar Al-Batin',
'الدرعية': 'Diriyah',
'حـريملاء': 'Huraymila',
'النعيرية': 'Nairyah',
'الجموم': 'Jumum'
}
property_type = {
'أرض سكني': 'Residential Land',
'فلل': 'Villas',
'شقق': 'Apartments',
'مرافق': 'Facilities',
'زراعي': 'Agricultural',
'أرض تجاري': 'Commercial Land',
'مصانع ومستودعات': 'Factories & Warehouses',
'أرض تجاري سكني': 'Mixed-use Land',
'اخرى': 'Other',
'مبنى تجاري سكني': 'Mixed-use Building',
'مبنى سكني/تجاري': 'Mixed-use Building',
'استراحات': 'Rest Houses',
'أرض فضاء': 'Vacant Land',
'مبنى تجاري': 'Commercial Building',
'شقة': 'Apartments',
'مرفق صناعي': 'Industrial Facility',
'استراحة': 'Rest House',
'مبنى سكني': 'Residential Building',
'شاليه': 'Chalet'
}
pd.set_option('display.max_columns', None)
📂 Load Original Dataset and Create a Working Copy¶
Import the original dataset and create a duplicate to preserve the raw data for reference and ensure safe preprocessing.
The dataset has been successfully loaded and contains 66,005 records
orginal_df = pd.read_excel("AllTransa_66006.xlsx")
df = orginal_df.copy()
df.shape
(66005, 41)
🛠️ Clean Column Names and Standardize Formats¶
Refactor column names for consistency, convert Arabic labels to English, and ensure proper data formatting for analysis.
# Convert to datetime
df['transactionDate'] = pd.to_datetime(df['transactionDate'], errors='coerce')
df['transactionDate'] = df['transactionDate'].dt.strftime('%Y-%m-%d')
# convert from Arabic to English names
df['metricsType'] = df['metricsType'].map(property_type)
df['provinceName'] = df['provinceName'].map(province_map)
df['provinceName'].value_counts()
provinceName Riyadh 40332 Jeddah 6442 Mecca 4151 Dammam 3980 Buraidah 2862 Khobar 2195 Al-Ahsa 1615 Medina 1166 Huraymila 1043 Qatif 756 Jubail 636 Khamis Mushait 319 Hafar Al-Batin 239 Jumum 119 Diriyah 81 Nairyah 69 Name: count, dtype: int64
🧹 Remove Unnecessary Columns and Reorder Features¶
Drop irrelevant or redundant columns and rearrange the remaining features into a logical, consistent order.
# Define the columns to drop as a variable
columns_to_drop = [
'main_key', '_priceOfMeter', 'transactionPrice', 'transactionNumber', 'type', 'subdivisionId', 'polygonData', 'noOfProperties', 'zoningId',
'parcelId', 'parcelObjectId', 'blockNo', 'area', 'geometry','totalArea',
'parcelImageURL', 'projectName', 'landUsageGroup', 'sellingType', 'landUseaDetailed',
'centroid', 'propertyType', 'totalArea', 'details', 'landUseGroup',
'orignalTransactionNum', 'propertyNumber'
]
#'isLowValueTransaction','transactionSource',
#,neighborhood', 'neighborhoodId', 'region', 'regionId', 'provinceId'
# ,'subdivisionNo' , 'parcelNo',
# Drop the columns using the variable
df = df.drop(columns=columns_to_drop)
#order columns
desired_order = [
'transactionDate', 'centroidX', 'centroidY', 'metricsType', 'provinceName',
'isLowValueTransaction', 'transactionSource', 'neighborhood', 'neighborhoodId',
'region', 'regionId', 'provinceId', 'subdivisionNo', 'priceOfMeter'
]
# Filter only the columns that exist in df
existing_cols = [col for col in desired_order if col in df.columns]
# Reorder
df = df[existing_cols]
df['metricsType'].value_counts()
metricsType Residential Land 27737 Apartments 16854 Mixed-use Land 6880 Villas 4701 Facilities 4565 Agricultural 1682 Commercial Land 1430 Factories & Warehouses 1249 Mixed-use Building 602 Other 91 Vacant Land 81 Commercial Building 68 Rest Houses 51 Residential Building 10 Rest House 2 Industrial Facility 1 Chalet 1 Name: count, dtype: int64
df.shape
(66005, 14)
values_to_remove = ['Facilities','Agricultural','Factories & Warehouses','Other','Vacant Land','Commercial Building',
'Rest Houses','Residential Building','Rest House','Industrial Facility','Chalet','Mixed-use Building']
df = df[~df['metricsType'].isin(values_to_remove)]
df.shape
(57602, 14)
df = df[df['isLowValueTransaction'] != True] ## Remove unreal values
df.shape
(56955, 14)
df['metricsType'].value_counts()
metricsType Residential Land 27232 Apartments 16807 Mixed-use Land 6854 Villas 4637 Commercial Land 1425 Name: count, dtype: int64
df.describe()
| centroidX | centroidY | neighborhoodId | regionId | provinceId | priceOfMeter | |
|---|---|---|---|---|---|---|
| count | 56955.000000 | 56955.000000 | 5.695500e+04 | 56955.000000 | 56955.000000 | 56955.000000 |
| mean | 45.686881 | 24.402443 | 1.479388e+08 | 7.671074 | 77711.200176 | 3671.090370 |
| std | 3.389673 | 1.574042 | 2.394722e+08 | 3.338664 | 33386.180241 | 4496.727586 |
| min | 39.039992 | 18.219735 | 2.100030e+05 | 2.000000 | 21000.000000 | 11.000000 |
| 25% | 46.461224 | 24.509398 | 1.010007e+07 | 5.000000 | 51000.000000 | 1345.000000 |
| 50% | 46.725390 | 24.774094 | 1.010001e+08 | 10.000000 | 101000.000000 | 2600.000000 |
| 75% | 46.923978 | 25.000986 | 1.010002e+08 | 10.000000 | 101000.000000 | 4444.000000 |
| max | 50.236258 | 28.460183 | 1.310005e+09 | 13.000000 | 131000.000000 | 207012.000000 |
🧼 Clean Price Data: Remove Zero Values and Outliers¶
- Remove zero values from the
priceOfMetercolumn to eliminate invalid entries. - Apply IQR filtering (Interquartile Range) to remove extreme outliers and retain data within a statistically reasonable range.
- Outliers are defined as values outside
Q1 - 1.5 * IQRandQ3 + 1.5 * IQR.
- Outliers are defined as values outside
# 1. Remove zero prices
df2 = df[df['priceOfMeter'] > 0]
# 2. Remove outliers using IQR (Interquartile Range)
Q1 = df2['priceOfMeter'].quantile(0.25)
Q3 = df2['priceOfMeter'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# Filter rows within bounds
df2 = df2[(df2['priceOfMeter'] >= lower_bound) & (df2['priceOfMeter'] <= upper_bound)]
df2.describe()
df2.shape
(52953, 14)
# Plot the boxplot for the column 'priceofmeter'
plt.figure(figsize=(8, 5))
sns.boxplot(data=df2, x='priceOfMeter')
plt.title('Boxplot of Price per Meter')
plt.xlabel('Price per Meter')
plt.grid(True)
plt.show()
➕ Feature Engineering: Add New Features¶
Create additional meaningful features to enhance model performance and capture deeper insights from the data.
# Calculate distance to city center using haversine Formula - https://en.wikipedia.org/wiki/Haversine_formula
def calculate_distance(row):
lat, lon = row['centroidY'], row['centroidX']
city = row['provinceName']
if city in city_center_coords:
center_lat, center_lon = city_center_coords[city]
return geodesic((lat, lon), (center_lat, center_lon)).km
return None # fallback for unknown city
# Apply the function to each row to create the new feature
df2['dist_to_city_center_km'] = df2.apply(calculate_distance, axis=1)
# Calculate Nearby Schools Count ( 1KM / 3KM )
df_schools = pd.read_excel("schools.xlsx")
# Define haversine (vectorized) - https://en.wikipedia.org/wiki/Haversine_formula
def haversine_np(lat1, lon1, lat2, lon2):
R = 6371 # Earth radius in KM
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = np.sin(dlat/2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2.0)**2
c = 2 * np.arcsin(np.sqrt(a))
return R * c
# Function to count schools within 3km for a target point
def count_schools_near_target(row, schools_df, radius_km=1):
distances = haversine_np(row['centroidY'], row['centroidX'],
schools_df['latitude'].values, schools_df['longitude'].values)
return int(np.sum(distances <= radius_km))
# Apply to each row in
df2['schools_within_1km'] = df2.apply(
lambda row: count_schools_near_target(row, df_schools), axis=1
)
# Convert `metricsType` into one-hot encoded columns and standardize column names for consistency.
df2 = pd.get_dummies(df2, columns=['metricsType'], prefix='', prefix_sep='')
# Clean column names
df2.columns = (
df2.columns
.str.strip() # Remove leading/trailing whitespace
.str.replace(' ', '_', regex=False) # Replace spaces with _
.str.replace('-', '_', regex=False) # Replace dashes with _
.str.lower() # Convert to lowercase
)
CHECK Check¶
df2.shape
(52953, 20)
df2.dropna(inplace=True) # if exist
df2.shape
(52086, 20)
df2.duplicated().sum()
np.int64(698)
df2.drop_duplicates(inplace=True)
df2.shape
(51388, 20)
# Clean `priceofmeter` values for each property type using IQR filtering to remove outliers.
# ✅ Define one-hot encoded type columns
type_cols = ['apartments', 'commercial_land', 'residential_land', 'mixed_use_land', 'villas'] #'mixed_use_building',
# ✅ Label the original data for plotting
df2_long = []
for col in type_cols:
temp = df2[df2[col] == 1].copy()
temp['type'] = col
temp['source'] = 'Before'
df2_long.append(temp)
df2_before = pd.concat(df2_long)
# ✅ Clean the data using IQR per type
cleaned_dfs = []
for col in type_cols:
type_df = df2[df2[col] == 1]
Q1 = type_df['priceofmeter'].quantile(0.25)
Q3 = type_df['priceofmeter'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
filtered = type_df[(type_df['priceofmeter'] >= lower) & (type_df['priceofmeter'] <= upper)].copy()
filtered['type'] = col
filtered['source'] = 'After'
cleaned_dfs.append(filtered)
df2_after = pd.concat(cleaned_dfs)
# ✅ Combine before and after for plotting
df2_plot = pd.concat([df2_before, df2_after])
# ✅ Create the boxplot
plt.figure(figsize=(14, 6))
sns.boxplot(data=df2_plot, x='type', y='priceofmeter', hue='source')
plt.title('Price of Meter: Before vs After Outlier Removal (per Property Type)')
plt.xlabel('Property Type')
plt.ylabel('Price per Meter')
plt.xticks(rotation=15)
plt.grid(True)
plt.tight_layout()
plt.show()
#df2_before.shape
df2_after.shape
(49308, 22)
🔥 Correlation Heatmap of Numerical Features¶
Visualize the pairwise correlations between all numerical features to identify strong linear relationships and potential multicollinearity.
selected_cols = ['priceofmeter', 'centroidx', 'centroidy', 'dist_to_city_center_km','schools_within_1km'] # Replace with your desired columns
numeric_df = df2_after[selected_cols]
plt.figure(figsize=(10, 8))
sns.heatmap(numeric_df.corr(), annot=True, cmap='coolwarm')
plt.title("Correlation Heatmap (Selected Columns)")
plt.show()
🏗️ Feature Selection and Model Training Setup¶
Select key features to train the model and define the target variable priceofmeter.
features = ['centroidy', 'centroidx','schools_within_1km', 'dist_to_city_center_km', 'apartments',
'commercial_land', 'residential_land', 'mixed_use_land','villas']
target = 'priceofmeter'
X = df2_after[features]
y = df2_after[target]
🤖 Train Multiple Models and Evaluate Performance¶
Split the data into training and testing sets, then apply preprocessing using pipelines:
- Numerical features: Imputation and standard scaling
- Categorical features: Passed through as-is
Train and evaluate multiple regression models, including:
LinearRegression, Ridge, Lasso, RandomForest, GradientBoosting, KNN, DecisionTree, and XGBoost.
Performance is assessed using R² and RMSE on both training and test sets.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) #42
numeric_features = ['centroidy', 'centroidx','schools_within_1km', 'dist_to_city_center_km']
categorical_features = ['apartments', 'commercial_land', 'residential_land',
'mixed_use_land', 'villas']
numeric_transformer = Pipeline([
('imputer', SimpleImputer(strategy="mean")),
('scaler', StandardScaler())
])
preprocessor = ColumnTransformer([
('num', numeric_transformer, numeric_features),
('cat', 'passthrough', categorical_features)
])
models = {
'LinearRegression': LinearRegression(),
'Ridge': Ridge(),
'Lasso': Lasso(),
'RandomForest': RandomForestRegressor(n_estimators=50, random_state=42),
'GradientBoosting': GradientBoostingRegressor(n_estimators=50, random_state=42),
'KNN': KNeighborsRegressor(n_neighbors=5),
'DecisionTree': DecisionTreeRegressor(random_state=42),
'XGBoost': xgb.XGBRegressor(n_estimators=50, random_state=42)
}
📊 Model Performance Summary¶
Display a comparison of all trained regression models based on key evaluation metrics:
- R² Score (Train/Test)
- RMSE (Train/Test)
results = []
for name, model in models.items():
pipe = Pipeline([
('preprocessor', preprocessor),
('regressor', model)
])
pipe.fit(X_train, y_train)
train_pred = pipe.predict(X_train)
test_pred = pipe.predict(X_test)
train_r2 = r2_score(y_train, train_pred)
test_r2 = r2_score(y_test, test_pred)
train_rmse = np.sqrt(mean_squared_error(y_train, train_pred))
test_rmse = np.sqrt(mean_squared_error(y_test, test_pred))
train_mae = mean_absolute_error(y_train, train_pred)
test_mae = mean_absolute_error(y_test, test_pred)
r2_gap = train_r2 - test_r2
rmse_gap = test_rmse - train_rmse
# Determine Fit Status
if test_r2 < 0.5:
Fit_Status = '🔴 Weak / Underfitting'
elif r2_gap > 0.20 or rmse_gap > 250:
Fit_Status = '❌ Overfitting'
elif r2_gap > 0.10 or rmse_gap > 150:
Fit_Status = '⚠️ Mild Overfitting'
elif test_r2 >= 0.75 and r2_gap < 0.05:
Fit_Status = '✅ Excellent Fit'
elif test_r2 >= 0.65:
Fit_Status = '🟢 Good Fit'
else:
Fit_Status = '🟡 Moderate Fit'
results.append({
'Model': name,
'Train_R2': train_r2,
'Test_R2': test_r2,
'R2_Gap': r2_gap,
'Train_RMSE': train_rmse,
'Test_RMSE': test_rmse,
'RMSE_Gap': rmse_gap,
'Fit Status': Fit_Status
})
# Convert to DataFrame
result_df = pd.DataFrame(results)
# Sort by Test_R2 descending
result_df = result_df.sort_values(by='Test_R2', ascending=False).reset_index(drop=True)
# Show result
result_df.head(100)
| Model | Train_R2 | Test_R2 | R2_Gap | Train_RMSE | Test_RMSE | RMSE_Gap | Fit Status | |
|---|---|---|---|---|---|---|---|---|
| 0 | RandomForest | 0.948291 | 0.851186 | 0.097106 | 434.145018 | 740.577084 | 306.432066 | ❌ Overfitting |
| 1 | XGBoost | 0.862631 | 0.836539 | 0.026092 | 707.616355 | 776.167830 | 68.551475 | ✅ Excellent Fit |
| 2 | KNN | 0.877867 | 0.817072 | 0.060795 | 667.220181 | 821.084897 | 153.864716 | ⚠️ Mild Overfitting |
| 3 | DecisionTree | 0.961352 | 0.799362 | 0.161990 | 375.332776 | 859.912353 | 484.579577 | ❌ Overfitting |
| 4 | GradientBoosting | 0.768029 | 0.752637 | 0.015392 | 919.539787 | 954.807021 | 35.267234 | ✅ Excellent Fit |
| 5 | LinearRegression | 0.452374 | 0.449537 | 0.002837 | 1412.847848 | 1424.334747 | 11.486899 | 🔴 Weak / Underfitting |
| 6 | Ridge | 0.452374 | 0.449535 | 0.002839 | 1412.847860 | 1424.336800 | 11.488940 | 🔴 Weak / Underfitting |
| 7 | Lasso | 0.452362 | 0.449356 | 0.003006 | 1412.863857 | 1424.568885 | 11.705029 | 🔴 Weak / Underfitting |
🏆 Identify the Best Performing Model¶
Select the best model based on:
- Highest R² on Test Set → Indicates best explanatory power
- Lowest RMSE on Test Set → Indicates best prediction accuracy
This helps determine the most reliable model for real-world application.
# Filter out models flagged as overfitting
filtered_df = result_df[~result_df['Fit Status'].str.contains('Overfitting')]
if not filtered_df.empty:
best_model_by_r2 = filtered_df.loc[filtered_df['Test_R2'].idxmax(), 'Model']
best_model_by_rmse = filtered_df.loc[filtered_df['Test_RMSE'].idxmin(), 'Model']
else:
# If all overfit, fallback to best Test_R2 ignoring Fit_Status
best_model_by_r2 = result_df.loc[result_df['Test_R2'].idxmax(), 'Model']
best_model_by_rmse = result_df.loc[result_df['Test_RMSE'].idxmin(), 'Model']
print("🏆 Best Model by R2 (filtered):", best_model_by_r2)
print("🏆 Best Model by RMSE (filtered):", best_model_by_rmse)
🏆 Best Model by R2 (filtered): XGBoost 🏆 Best Model by RMSE (filtered): XGBoost
filtered_df.head(10)
| Model | Train_R2 | Test_R2 | R2_Gap | Train_RMSE | Test_RMSE | RMSE_Gap | Fit Status | |
|---|---|---|---|---|---|---|---|---|
| 1 | XGBoost | 0.862631 | 0.836539 | 0.026092 | 707.616355 | 776.167830 | 68.551475 | ✅ Excellent Fit |
| 4 | GradientBoosting | 0.768029 | 0.752637 | 0.015392 | 919.539787 | 954.807021 | 35.267234 | ✅ Excellent Fit |
| 5 | LinearRegression | 0.452374 | 0.449537 | 0.002837 | 1412.847848 | 1424.334747 | 11.486899 | 🔴 Weak / Underfitting |
| 6 | Ridge | 0.452374 | 0.449535 | 0.002839 | 1412.847860 | 1424.336800 | 11.488940 | 🔴 Weak / Underfitting |
| 7 | Lasso | 0.452362 | 0.449356 | 0.003006 | 1412.863857 | 1424.568885 | 11.705029 | 🔴 Weak / Underfitting |
best_model_name = 'XGBoost'
# Get the best model from the dictionary (previously selected based on evaluation metrics)
best_model = models[best_model_name]
# Build a pipeline that includes preprocessing and the chosen regression model
pipe = Pipeline([
('preprocessor', preprocessor), # Apply preprocessing steps (e.g., scaling, imputing, encoding)
('regressor', best_model) # Use the best regression model
])
# Train (fit) the pipeline on the training data
pipe.fit(X_train, y_train)
# Make predictions on the test set
y_pred = pipe.predict(X_test)
# Calculate the residuals (errors)
residuals = y_pred - y_test
residuals_pct = (residuals / y_test) * 100 # Percentage error
# Create a DataFrame to compare actual vs predicted values
results_df = pd.DataFrame({
'Actual': y_test.values,
'Predicted': y_pred,
'Difference ': residuals,
'Error%': residuals_pct
})
# Show the first 5 rows of results
results_df.head()
| Actual | Predicted | Difference | Error% | |
|---|---|---|---|---|
| 55522 | 2138 | 2019.766602 | -118.233398 | -5.530093 |
| 26792 | 8019 | 7544.817383 | -474.182617 | -5.913239 |
| 35045 | 2333 | 3591.302490 | 1258.302490 | 53.934955 |
| 51137 | 3928 | 3293.339111 | -634.660889 | -16.157355 |
| 2857 | 3263 | 5231.555664 | 1968.555664 | 60.329625 |
#results_df.to_excel("results.xlsx", index=False)
results_df.describe()
| Actual | Predicted | Difference | Error% | |
|---|---|---|---|---|
| count | 9862.000000 | 9862.000000 | 9862.000000 | 9862.000000 |
| mean | 2719.976678 | 2726.398438 | 6.421726 | 22.886855 |
| std | 1919.861830 | 1751.865601 | 776.180642 | 223.335764 |
| min | 17.000000 | -83.895554 | -5426.311768 | -379.651845 |
| 25% | 1227.000000 | 1384.957092 | -262.784485 | -10.377111 |
| 50% | 2261.000000 | 2433.297607 | 7.467590 | 0.407463 |
| 75% | 3700.000000 | 3632.885986 | 282.035736 | 14.356199 |
| max | 9083.000000 | 8579.887695 | 6912.509766 | 8946.600932 |
🎯 Actual vs Predicted Plot¶
Visualize how closely the model's predictions align with the actual values.
plt.figure(figsize=(15, 7))
plt.scatter(results_df['Actual'], results_df['Predicted'], alpha=0.5, color='blue', label='Predictions')
# Ideal line
plt.plot(results_df['Actual'], results_df['Actual'], 'r--', label='Perfect Prediction')
plt.xlabel("Actual Value")
plt.ylabel("Predicted Value")
plt.title("Actual vs Predicted")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
📊 Filtered Percentage Error Distribution (±100%)¶
Display a histogram of prediction errors limited to the ±100% range.
This focuses the analysis on typical prediction behavior by excluding extreme outliers.
The red dashed line at 0% represents perfect prediction.
filtered = results_df[(results_df['Error%'] >= -100) & (results_df['Error%'] <= 100)]
plt.figure(figsize=(10, 6))
plt.hist(filtered['Error%'], bins=100, color='green', edgecolor='black', alpha=0.7)
plt.axvline(0, color='red', linestyle='--', linewidth=2)
plt.xlabel("Percentage Error (%)", fontsize=12)
plt.ylabel("Count", fontsize=12)
plt.title("Distribution of % Errors (Filtered ±100%)", fontsize=14)
plt.grid(True)
plt.tight_layout()
plt.show()
📊 Distribution of Percentage Errors (Filtered ±20%)¶
Visualize prediction errors within the ±20% range to exclude extreme outliers.
This highlights the model’s typical performance with a clearer and more focused error distribution.
filtered = results_df[(results_df['Error%'] >= -20) & (results_df['Error%'] <= 20)]
plt.figure(figsize=(10, 6))
plt.hist(filtered['Error%'], bins=100, color='green', edgecolor='black', alpha=0.7)
plt.axvline(0, color='red', linestyle='--', linewidth=2)
plt.xlabel("Percentage Error (%)", fontsize=12)
plt.ylabel("Count", fontsize=12)
plt.title("Distribution of % Errors (Filtered ±20%)", fontsize=14)
plt.grid(True)
plt.tight_layout()
plt.show()
📈 Evaluate Model Accuracy: MAPE & MAE¶
- MAPE (Mean Absolute Percentage Error): Measures average percentage difference between actual and predicted values.
- MAE (Mean Absolute Error): Measures average absolute difference in the same units as the target variable.
mape = np.mean(np.abs((y_test - y_pred) / y_test)) * 100
print(f"📉 MAPE (متوسط نسبة الخطأ): {mape:.2f}%")
mae = np.mean(np.abs(y_test - y_pred))
print(f"📏 MAE (متوسط الخطأ المطلق): {mae:.2f}")
📉 MAPE (متوسط نسبة الخطأ): 36.54% 📏 MAE (متوسط الخطأ المطلق): 487.40
results_df.shape
(9862, 4)
📉 Line Plot: Actual vs Predicted Values (Sorted)¶
Display actual and predicted values on a sorted line plot to assess overall prediction trends.
This helps visually compare how closely the predictions follow the true values across samples.
# Sort by actual or index for a clean line (optional)
results_sorted = results_df.sort_values(by='Actual').reset_index(drop=True) # row index order
plt.figure(figsize=(12, 6))
# Plot actual values
plt.plot(results_sorted['Actual'], label='Actual', color='black', linewidth=2)
# Plot predicted values
plt.plot(results_sorted['Predicted'], label='Predicted', color='green', alpha=0.7)
# Labels and styling
plt.title('Actual vs Predicted Values (Line Plot)', fontsize=14)
plt.xlabel('Sample Index (sorted)', fontsize=12)
plt.ylabel('Value', fontsize=12)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
📊 R² Score Comparison: Train vs Test¶
Compare the R² scores of different models on both training and testing sets.
plt.figure(figsize=(14, 6))
bar_width = 0.35
index = np.arange(len(result_df))
# Custom colors
train_color = '#1f77b4' # Blue
test_color = '#aec7e8' # Light blue
plt.bar(index, result_df["Train_R2"], bar_width, label='Train R²',color=train_color)
plt.bar(index + bar_width, result_df["Test_R2"], bar_width, label='Test R²',color=test_color)
plt.xlabel("Model")
plt.ylabel("R² Score")
plt.title("R² Comparison (Train vs Test)")
plt.xticks(index + bar_width / 2, result_df["Model"], rotation=45)
plt.legend()
plt.tight_layout()
plt.show()
📉 RMSE Comparison: Train vs Test¶
Visualize and compare the Root Mean Squared Error (RMSE) across models for both training and testing sets.
Lower RMSE values indicate better prediction accuracy.
plt.figure(figsize=(14, 6))
bar_width = 0.35
index = np.arange(len(result_df))
# Custom colors
train_color = '#1f77b4' # Blue
test_color = '#aec7e8' # Light blue
plt.bar(index, result_df["Train_RMSE"], bar_width, label='Train RMSE',color=train_color)
plt.bar(index + bar_width, result_df["Test_RMSE"], bar_width, label='Test RMSE',color=test_color)
plt.xlabel("Model")
plt.ylabel("RMSE")
plt.title("RMSE Comparison (Train vs Test)")
plt.xticks(index + bar_width / 2, result_df["Model"], rotation=45)
plt.legend()
plt.tight_layout()
plt.show()