In [ ]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV, learning_curve
from sklearn.linear_model import LinearRegression, Lasso, Ridge, ElasticNet
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.metrics import mean_squared_error, r2_score
import folium
from folium.plugins import HeatMap
Exploratory Data Analysis
In [ ]:
# 1.1 Descriptive Statistics for Numerical Features
df = pd.read_csv("AB_NYC_2019.csv")
# get numerical columns
num_cols = df.select_dtypes(include=['number'])
print("Descriptive Statistics for Numerical Features:")
print(num_cols.describe())
Descriptive Statistics for Numerical Features:
id host_id latitude longitude price \
count 4.889500e+04 4.889500e+04 48895.000000 48895.000000 48895.000000
mean 1.901714e+07 6.762001e+07 40.728949 -73.952170 152.720687
std 1.098311e+07 7.861097e+07 0.054530 0.046157 240.154170
min 2.539000e+03 2.438000e+03 40.499790 -74.244420 0.000000
25% 9.471945e+06 7.822033e+06 40.690100 -73.983070 69.000000
50% 1.967728e+07 3.079382e+07 40.723070 -73.955680 106.000000
75% 2.915218e+07 1.074344e+08 40.763115 -73.936275 175.000000
max 3.648724e+07 2.743213e+08 40.913060 -73.712990 10000.000000
minimum_nights number_of_reviews reviews_per_month \
count 48895.000000 48895.000000 38843.000000
mean 7.029962 23.274466 1.373221
std 20.510550 44.550582 1.680442
min 1.000000 0.000000 0.010000
25% 1.000000 1.000000 0.190000
50% 3.000000 5.000000 0.720000
75% 5.000000 24.000000 2.020000
max 1250.000000 629.000000 58.500000
calculated_host_listings_count availability_365
count 48895.000000 48895.000000
mean 7.143982 112.781327
std 32.952519 131.622289
min 1.000000 0.000000
25% 1.000000 0.000000
50% 1.000000 45.000000
75% 2.000000 227.000000
max 327.000000 365.000000
In [ ]:
# 1.2 Handle Missing Values
print("\nMissing values before cleaning:")
print(df.isnull().sum())
# drop rows with missing reviews_per_month
df = df.dropna(subset=['reviews_per_month'])
# fill missing last_review with 'Unknown'
df['last_review'] = df['last_review'].fillna('Unknown')
# fill numeric columns with median
for col in df.select_dtypes(include='number'):
if df[col].isnull().sum() > 0:
df[col] = df[col].fillna(df[col].median())
# fill categorical columns with most frequent value
for col in df.select_dtypes(include='object'):
if df[col].isnull().sum() > 0:
df[col] = df[col].fillna(df[col].mode()[0])
print("\nMissing values after cleaning:")
print(df.isnull().sum())
Missing values before cleaning: id 0 name 16 host_id 0 host_name 21 neighbourhood_group 0 neighbourhood 0 latitude 0 longitude 0 room_type 0 price 0 minimum_nights 0 number_of_reviews 0 last_review 10052 reviews_per_month 10052 calculated_host_listings_count 0 availability_365 0 dtype: int64 Missing values after cleaning: id 0 name 0 host_id 0 host_name 0 neighbourhood_group 0 neighbourhood 0 latitude 0 longitude 0 room_type 0 price 0 minimum_nights 0 number_of_reviews 0 last_review 0 reviews_per_month 0 calculated_host_listings_count 0 availability_365 0 dtype: int64
In [ ]:
# 1.3 Detect and Remove Outliers in Price Column (IQR)
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
low = Q1 - 1.5 * IQR
high = Q3 + 1.5 * IQR
print(f"\nPrice outlier bounds: [{low:.2f}, {high:.2f}]")
print("Outliers detected:", len(df[(df['price'] < low) | (df['price'] > high)]))
df_clean = df[(df['price'] >= low) & (df['price'] <= high)]
print("Dataset shape after removing outliers:", df_clean.shape)
Price outlier bounds: [-82.50, 321.50] Outliers detected: 2077 Dataset shape after removing outliers: (36766, 16)
In [ ]:
# 1.4 Visualizations
fig, ax = plt.subplots(2, 2, figsize=(15, 12))
# histogram of price
ax[0,0].hist(df_clean['price'], bins=50, edgecolor='black')
ax[0,0].set_title('Price Histogram')
ax[0,0].set_xlabel('Price')
ax[0,0].set_ylabel('Count')
# scatter plot price vs reviews
ax[0,1].scatter(df_clean['number_of_reviews'], df_clean['price'], alpha=0.5)
ax[0,1].set_title('Price vs Reviews')
ax[0,1].set_xlabel('Reviews')
ax[0,1].set_ylabel('Price')
# boxplot by room type
sns.boxplot(x='room_type', y='price', data=df_clean, ax=ax[1,0])
ax[1,0].set_title('Room Type vs Price')
ax[1,0].tick_params(axis='x', rotation=45)
# boxplot by neighbourhood group
sns.boxplot(x='neighbourhood_group', y='price', data=df_clean, ax=ax[1,1])
ax[1,1].set_title('Neighbourhood vs Price')
ax[1,1].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()
In [ ]:
# 1.5 Correlation Heatmap
cols = ['price', 'minimum_nights', 'number_of_reviews',
'reviews_per_month', 'calculated_host_listings_count', 'availability_365']
corr = df_clean[cols].corr()
plt.figure(figsize=(8,6))
sns.heatmap(corr, annot=True, cmap='coolwarm')
plt.title('Correlation Heatmap')
plt.show()
print("\nCorrelation Analysis (|r| > 0.5):")
for i in range(len(corr.columns)):
for j in range(i+1, len(corr.columns)):
val = corr.iloc[i, j]
if abs(val) > 0.5:
print(f"{corr.columns[i]} - {corr.columns[j]}: {val:.3f}")
Correlation Analysis (|r| > 0.5): number_of_reviews - reviews_per_month: 0.557
2. Feature Engineering
In [ ]:
# 2.1 One-Hot Encoding
df_features = df_clean.copy()
cat_cols = ['neighbourhood_group', 'room_type']
df_encoded = pd.get_dummies(df_features, columns=cat_cols, drop_first=True)
print("Shape after encoding:", df_encoded.shape)
print("\nNew columns:")
print([c for c in df_encoded.columns if c not in df_features.columns])
Shape after encoding: (36766, 20) New columns: ['neighbourhood_group_Brooklyn', 'neighbourhood_group_Manhattan', 'neighbourhood_group_Queens', 'neighbourhood_group_Staten Island', 'room_type_Private room', 'room_type_Shared room']
In [ ]:
# 2.2 Create Derived Feature
# Create price_per_accommodates feature
df_encoded['price_per_accommodates'] = df_encoded['price'] / df_encoded['minimum_nights']
# Handle any division by zero or infinity
df_encoded['price_per_accommodates'] = df_encoded['price_per_accommodates'].replace([np.inf, -np.inf], np.nan)
df_encoded['price_per_accommodates'] = df_encoded['price_per_accommodates'].fillna(df_encoded['price_per_accommodates'].median())
print("Created derived feature: price_per_accommodates")
Created derived feature: price_per_accommodates
In [ ]:
# 2.3 Scale Numerical Features
# Define features to scale
features_to_scale = ['minimum_nights', 'number_of_reviews', 'reviews_per_month',
'calculated_host_listings_count', 'availability_365', 'price_per_accommodates']
# Initialize scaler
scaler = StandardScaler()
# Scale the features
df_encoded[features_to_scale] = scaler.fit_transform(df_encoded[features_to_scale])
print("Scaled numerical features:")
print(df_encoded[features_to_scale].describe())
Scaled numerical features:
minimum_nights number_of_reviews reviews_per_month \
count 3.676600e+04 3.676600e+04 3.676600e+04
mean 2.164521e-17 2.473739e-17 -1.360556e-16
std 1.000014e+00 1.000014e+00 1.000014e+00
min -2.791706e-01 -5.900912e-01 -8.082006e-01
25% -2.791706e-01 -5.490458e-01 -7.017768e-01
50% -2.219194e-01 -4.053870e-01 -3.943303e-01
75% -1.074171e-01 8.715751e-02 3.801984e-01
max 7.122751e+01 1.229816e+01 3.377362e+01
calculated_host_listings_count availability_365 \
count 3.676600e+04 3.676600e+04
mean 1.855304e-17 -6.493564e-17
std 1.000014e+00 1.000014e+00
min -1.575128e-01 -8.695402e-01
25% -1.575128e-01 -8.695402e-01
50% -1.575128e-01 -4.807688e-01
75% -1.165801e-01 8.410542e-01
max 1.318654e+01 1.968491e+00
price_per_accommodates
count 3.676600e+04
mean 9.894954e-17
std 1.000014e+00
min -1.135773e+00
25% -6.932034e-01
50% -2.580097e-01
75% 4.132213e-01
max 5.473270e+00
3. Baseline Regression Models
In [ ]:
# 3.1 Prepare Features and Target
# select numeric features
feature_cols = ['minimum_nights', 'number_of_reviews', 'reviews_per_month',
'calculated_host_listings_count', 'availability_365']
# add one-hot encoded categorical features
feature_cols += [c for c in df_encoded.columns if c.startswith('neighbourhood_group_')
or c.startswith('room_type_')]
# define feature matrix and target vector
X = df_encoded[feature_cols]
y = df_encoded['price']
print("Feature columns:")
print(feature_cols)
print("\nX shape:", X.shape)
print("y shape:", y.shape)
Feature columns: ['minimum_nights', 'number_of_reviews', 'reviews_per_month', 'calculated_host_listings_count', 'availability_365', 'neighbourhood_group_Brooklyn', 'neighbourhood_group_Manhattan', 'neighbourhood_group_Queens', 'neighbourhood_group_Staten Island', 'room_type_Private room', 'room_type_Shared room'] X shape: (36766, 11) y shape: (36766,)
In [ ]:
3.2 # split data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print("Training set size:", X_train.shape[0])
print("Test set size:", X_test.shape[0])
# train linear regression model
lr_model = LinearRegression()
lr_model.fit(X_train, y_train)
# make predictions
y_train_pred = lr_model.predict(X_train)
y_test_pred = lr_model.predict(X_test)
Training set size: 29412 Test set size: 7354
In [ ]:
3.3 # function to calculate RMSE, MSE, R2
def calculate_metrics(y_true, y_pred):
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_true, y_pred)
return rmse, mse, r2
# calculate metrics for train and test sets
train_rmse, train_mse, train_r2 = calculate_metrics(y_train, y_train_pred)
test_rmse, test_mse, test_r2 = calculate_metrics(y_test, y_test_pred)
# print results
print("Baseline Linear Regression Results")
print("-" * 40)
print(f"Training RMSE: {train_rmse:.2f}")
print(f"Training MSE: {train_mse:.2f}")
print(f"Training R²: {train_r2:.4f}")
print(f"Test RMSE: {test_rmse:.2f}")
print(f"Test MSE: {test_mse:.2f}")
print(f"Test R²: {test_r2:.4f}")
Baseline Linear Regression Results ---------------------------------------- Training RMSE: 47.29 Training MSE: 2236.38 Training R²: 0.4733 Test RMSE: 47.26 Test MSE: 2233.97 Test R²: 0.4645
4. Lasso, Ridge, and ElasticNet Regression
In [ ]:
# 4.1 Define Hyperparameter Grids
# possible alpha values
alpha_values = [0.01, 0.1, 1, 10, 100]
# possible l1 ratios for ElasticNet
l1_ratios = [0.1, 0.5, 0.9]
# create parameter grids for models
lasso_params = {'alpha': alpha_values}
ridge_params = {'alpha': alpha_values}
elasticnet_params = {'alpha': alpha_values, 'l1_ratio': l1_ratios}
In [ ]:
# 4.2 Grid Search with Cross-Validation
# initialize models
lasso = Lasso(random_state=42)
ridge = Ridge(random_state=42)
elasticnet = ElasticNet(random_state=42)
print("Running Grid Search...")
# grid search with 5-fold CV
lasso_grid = GridSearchCV(lasso, lasso_params, cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
ridge_grid = GridSearchCV(ridge, ridge_params, cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
elasticnet_grid = GridSearchCV(elasticnet, elasticnet_params, cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
# fit the grid searches
lasso_grid.fit(X_train, y_train)
ridge_grid.fit(X_train, y_train)
elasticnet_grid.fit(X_train, y_train)
# show best parameters
print("Best Parameters:")
print("Lasso:", lasso_grid.best_params_)
print("Ridge:", ridge_grid.best_params_)
print("ElasticNet:", elasticnet_grid.best_params_)
Running Grid Search...
Best Parameters:
Lasso: {'alpha': 0.01}
Ridge: {'alpha': 1}
ElasticNet: {'alpha': 0.01, 'l1_ratio': 0.9}
In [ ]:
# 4.3 Evaluate Regularized Models
# get best estimators from grid search
best_lasso = lasso_grid.best_estimator_
best_ridge = ridge_grid.best_estimator_
best_elasticnet = elasticnet_grid.best_estimator_
# make predictions on test set
lasso_pred = best_lasso.predict(X_test)
ridge_pred = best_ridge.predict(X_test)
elasticnet_pred = best_elasticnet.predict(X_test)
# calculate metrics
lasso_rmse, lasso_mse, lasso_r2 = calculate_metrics(y_test, lasso_pred)
ridge_rmse, ridge_mse, ridge_r2 = calculate_metrics(y_test, ridge_pred)
elasticnet_rmse, elasticnet_mse, elasticnet_r2 = calculate_metrics(y_test, elasticnet_pred)
# create results table
results = pd.DataFrame({
'Model': ['Linear Regression', 'Lasso', 'Ridge', 'ElasticNet'],
'RMSE': [test_rmse, lasso_rmse, ridge_rmse, elasticnet_rmse],
'MSE': [test_mse, lasso_mse, ridge_mse, elasticnet_mse],
'R²': [test_r2, lasso_r2, ridge_r2, elasticnet_r2]
})
print("\nModel Comparison Results")
print("=" * 50)
print(results.round(4))
Model Comparison Results
==================================================
Model RMSE MSE R²
0 Linear Regression 47.2649 2233.9703 0.4645
1 Lasso 47.2613 2233.6308 0.4646
2 Ridge 47.2642 2233.9073 0.4645
3 ElasticNet 47.2549 2233.0283 0.4647
Bias-Variance Tradeoff and Model Complexity
In [ ]:
# 5.1 Polynomial Features Experiment
degrees = [1, 2, 3, 4]
poly_results = []
print("Testing Polynomial Features...")
for degree in degrees:
print(f"\nDegree {degree}:")
# create polynomial features
poly = PolynomialFeatures(degree=degree, include_bias=False)
X_train_poly = poly.fit_transform(X_train)
X_test_poly = poly.transform(X_test)
# train linear regression model
model = LinearRegression()
model.fit(X_train_poly, y_train)
# make predictions
train_pred = model.predict(X_train_poly)
test_pred = model.predict(X_test_poly)
# calculate metrics
train_rmse, train_mse, train_r2 = calculate_metrics(y_train, train_pred)
test_rmse, test_mse, test_r2 = calculate_metrics(y_test, test_pred)
# store results
poly_results.append({
'Degree': degree,
'Train_RMSE': train_rmse,
'Test_RMSE': test_rmse,
'Train_R2': train_r2,
'Test_R2': test_r2,
'Features': X_train_poly.shape[1]
})
# print summary for this degree
print("Features:", X_train_poly.shape[1])
print(f"Train RMSE: {train_rmse:.2f}, Test RMSE: {test_rmse:.2f}")
print(f"Train R²: {train_r2:.4f}, Test R²: {test_r2:.4f}")
# convert results to DataFrame
poly_df = pd.DataFrame(poly_results)
print("\nPolynomial Features Results:")
print(poly_df)
Testing Polynomial Features... Degree 1: Features: 11 Train RMSE: 47.29, Test RMSE: 47.26 Train R²: 0.4733, Test R²: 0.4645 Degree 2: Features: 77 Train RMSE: 46.61, Test RMSE: 46.86 Train R²: 0.4883, Test R²: 0.4735 Degree 3: Features: 363 Train RMSE: 45.83, Test RMSE: 47.66 Train R²: 0.5053, Test R²: 0.4555 Degree 4: Features: 1364 Train RMSE: 45.32, Test RMSE: 54.79 Train R²: 0.5163, Test R²: 0.2805 Polynomial Features Results: Degree Train_RMSE Test_RMSE Train_R2 Test_R2 Features 0 1 47.290345 47.264895 0.473338 0.464484 11 1 2 46.612766 46.863423 0.488322 0.473543 77 2 3 45.831506 47.658349 0.505330 0.455531 363 3 4 45.322078 54.786213 0.516266 0.280489 1364
In [87]:
### 5.2 Learning Curves
# Import learning_curve function
from sklearn.model_selection import learning_curve
# Plot learning curves for different degrees
plt.figure(figsize=(14, 10))
for i, degree in enumerate([1, 2, 3, 4], 1):
plt.subplot(2, 2, i)
# Create polynomial features
poly = PolynomialFeatures(degree=degree, include_bias=False)
# Use smaller subset for higher degrees to avoid memory issues
data_size = min(1500, len(X)) if degree > 2 else min(3000, len(X))
X_subset = X.iloc[:data_size]
y_subset = y.iloc[:data_size]
X_poly = poly.fit_transform(X_subset)
# Create learning curve with more reasonable train sizes
train_sizes, train_scores, val_scores = learning_curve(
LinearRegression(), X_poly, y_subset,
train_sizes=np.linspace(0.2, 1.0, 8), # Start from 20% to avoid tiny samples
cv=3, scoring='neg_mean_squared_error', random_state=42)
# Convert to RMSE
train_rmse_mean = np.sqrt(-train_scores.mean(axis=1))
train_rmse_std = np.sqrt(-train_scores).std(axis=1)
val_rmse_mean = np.sqrt(-val_scores.mean(axis=1))
val_rmse_std = np.sqrt(-val_scores).std(axis=1)
# Plot learning curves
plt.plot(train_sizes, train_rmse_mean, 'o-', color='blue',
label='Training RMSE', linewidth=2, markersize=4)
plt.fill_between(train_sizes, train_rmse_mean - train_rmse_std,
train_rmse_mean + train_rmse_std, alpha=0.1, color='blue')
plt.plot(train_sizes, val_rmse_mean, 's-', color='red',
label='Validation RMSE', linewidth=2, markersize=4)
plt.fill_between(train_sizes, val_rmse_mean - val_rmse_std,
val_rmse_mean + val_rmse_std, alpha=0.1, color='red')
plt.xlabel('Training Set Size')
plt.ylabel('RMSE')
plt.title(f'Learning Curve - Degree {degree}')
plt.legend()
plt.grid(True, alpha=0.3)
print(f"Degree {degree}: {X_poly.shape[1]} features")
plt.suptitle('Learning Curves: Bias-Variance Tradeoff Analysis', fontsize=16)
plt.tight_layout()
plt.show()
# Plot complexity vs performance
plt.figure(figsize=(10, 6))
plt.plot(poly_df['Degree'], poly_df['Train_RMSE'], 'o-', label='Training RMSE', linewidth=2)
plt.plot(poly_df['Degree'], poly_df['Test_RMSE'], 's-', label='Test RMSE', linewidth=2)
plt.xlabel('Polynomial Degree (Model Complexity)')
plt.ylabel('RMSE')
plt.title('Model Complexity vs Performance')
plt.legend()
plt.grid(True, alpha=0.3)
plt.xticks(degrees)
plt.show()
Degree 1: 11 features Degree 2: 77 features Degree 3: 363 features Degree 4: 1364 features
In [ ]:
# 5.3 Discussion of Bias-Variance Tradeoff
print("\n" + "="*60)
print("BIAS-VARIANCE TRADEOFF ANALYSIS")
print("="*60)
print("\nObservations from the polynomial features experiment:")
print("-" * 50)
# Bias observations
print("1. BIAS:")
print(" - Degree 1 (Linear): High bias, underfitting")
print(" - As degree increases: Bias decreases")
# Variance observations
print("\n2. VARIANCE:")
print(" - Degree 1: Low variance, stable predictions")
print(" - Higher degrees: Increased variance, overfitting risk")
# Overfitting/Underfitting
print("\n3. OVERFITTING/UNDERFITTING:")
for _, row in poly_df.iterrows():
degree = int(row['Degree'])
train_rmse = row['Train_RMSE']
test_rmse = row['Test_RMSE']
gap = test_rmse - train_rmse
if gap < 5:
status = "Good fit"
elif gap < 15:
status = "Slight overfitting"
else:
status = "Overfitting"
print(f" Degree {degree}: Gap = {gap:.2f} -> {status}")
# Recommendations
print("\n4. RECOMMENDATIONS:")
best_degree = int(poly_df.loc[poly_df['Test_RMSE'].idxmin(), 'Degree'])
print(f" - Best polynomial degree: {best_degree}")
print(" - Balances bias and variance effectively")
# Final model summary
print("\nFinal Model Performance Summary:")
print("-" * 40)
best_idx = results['R²'].idxmax()
best_model = results.iloc[best_idx]
print(f"Best performing model: {best_model['Model']}")
print(f"Test RMSE: {best_model['RMSE']:.2f}")
print(f"Test R²: {best_model['R²']:.4f}")
============================================================ BIAS-VARIANCE TRADEOFF ANALYSIS ============================================================ Observations from the polynomial features experiment: -------------------------------------------------- 1. BIAS: - Degree 1 (Linear): High bias, underfitting - As degree increases: Bias decreases 2. VARIANCE: - Degree 1: Low variance, stable predictions - Higher degrees: Increased variance, overfitting risk 3. OVERFITTING/UNDERFITTING: Degree 1: Gap = -0.03 -> Good fit Degree 2: Gap = 0.25 -> Good fit Degree 3: Gap = 1.83 -> Good fit Degree 4: Gap = 9.46 -> Slight overfitting 4. RECOMMENDATIONS: - Best polynomial degree: 2 - Balances bias and variance effectively Final Model Performance Summary: ---------------------------------------- Best performing model: ElasticNet Test RMSE: 47.25 Test R²: 0.4647
Advanced Visualization to Interpret the Results
In [ ]:
# 6.1 Geographic Heatmap of Airbnb Prices
# check if lat/lon columns exist
if 'latitude' in df_clean.columns and 'longitude' in df_clean.columns:
print("Creating geographic heatmap of Airbnb prices...")
# sample data to avoid performance issues
sample_size = min(3000, len(df_clean))
df_sample = df_clean.sample(n=sample_size, random_state=42).reset_index(drop=True)
df_sample = df_sample.dropna(subset=['latitude', 'longitude']) # drop missing coords
# create base map centered on NYC
nyc_lat, nyc_lon = df_sample['latitude'].mean(), df_sample['longitude'].mean()
m = folium.Map(location=[nyc_lat, nyc_lon], zoom_start=11)
# add heatmap layer
heat_data = df_sample[['latitude', 'longitude', 'price']].values.tolist()
HeatMap(heat_data, radius=15, blur=20).add_to(m)
# save map
m.save('nyc_airbnb_heatmap.html')
print("Geographic heatmap saved as 'nyc_airbnb_heatmap.html'")
# scatter plot for reference
plt.figure(figsize=(10, 8))
scatter = plt.scatter(df_sample['longitude'], df_sample['latitude'],
c=df_sample['price'], cmap='YlOrRd', alpha=0.6, s=15)
plt.colorbar(scatter, label='Price ($)')
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.title('NYC Airbnb Price Distribution by Location')
plt.tight_layout()
plt.show()
else:
print("Latitude and longitude columns not found in dataset.")
Creating geographic heatmap of Airbnb prices... Geographic heatmap saved as 'nyc_airbnb_heatmap.html'
In [88]:
# 6.2 Predicted vs Actual Prices Visualization
# get best performing model
best_idx = results['R²'].idxmax()
best_model_name = results.iloc[best_idx]['Model']
print(f"Best performing model: {best_model_name}")
# get predictions from the best model
if best_model_name == 'Linear Regression':
best_pred = y_test_pred
elif best_model_name == 'Lasso':
best_pred = lasso_pred
elif best_model_name == 'Ridge':
best_pred = ridge_pred
else: # ElasticNet
best_pred = elasticnet_pred
# scatter plot: predicted vs actual
plt.figure(figsize=(10, 8))
plt.scatter(y_test, best_pred, alpha=0.6, s=20)
# perfect prediction line
min_val, max_val = min(y_test.min(), best_pred.min()), max(y_test.max(), best_pred.max())
plt.plot([min_val, max_val], [min_val, max_val], 'r--', label='Perfect Prediction')
# calculate metrics
rmse = np.sqrt(mean_squared_error(y_test, best_pred))
r2 = r2_score(y_test, best_pred)
# show metrics on plot
plt.text(0.05, 0.95, f'{best_model_name}\nR² = {r2:.3f}\nRMSE = {rmse:.2f}',
transform=plt.gca().transAxes, bbox=dict(boxstyle='round', facecolor='wheat'))
plt.xlabel('Actual Price ($)')
plt.ylabel('Predicted Price ($)')
plt.title('Predicted vs Actual Prices')
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Best performing model: ElasticNet