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()
No description has been provided for this image
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}")
No description has been provided for this image
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
No description has been provided for this image
No description has been provided for this image
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'
No description has been provided for this image
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
No description has been provided for this image