June 16, 2024

Background

  • This project utilizes the House Sales in King County, USA dataset, which is found on Kaggle.

  • The House Sales in King County, USA dataset contains data on house sales in King County, Washington.

  • King County is home to the city of Seattle and is one of the most populous regions in the state of Washington.

Problem Definition

The goal of this project is to build a predictive model to estimate the sale price of houses based on various features such as the number of bedrooms, bathrooms, square footage, and other attributes that we will explore in this project. Accurate predictions of house prices can help potential buyers, real estate agents, and policymakers make informed decisions.

To accurately predict the House Prices, we would need to first extensively analyze the Kings County Housing Dataset, identify general trends, carry out any preprocessing if required, and then proceed towards model training and testing.

Loading the Data and First Look

scroll to see more….

# load the data
kc_data <- read.csv("kc_house_data.csv")

# first look at our data
str(kc_data)
'data.frame':   21613 obs. of  21 variables:
 $ id           : num  7.13e+09 6.41e+09 5.63e+09 2.49e+09 1.95e+09 ...
 $ date         : chr  "20141013T000000" "20141209T000000" "20150225T000000" "20141209T000000" ...
 $ price        : num  221900 538000 180000 604000 510000 ...
 $ bedrooms     : int  3 3 2 4 3 4 3 3 3 3 ...
 $ bathrooms    : num  1 2.25 1 3 2 4.5 2.25 1.5 1 2.5 ...
 $ sqft_living  : int  1180 2570 770 1960 1680 5420 1715 1060 1780 1890 ...
 $ sqft_lot     : int  5650 7242 10000 5000 8080 101930 6819 9711 7470 6560 ...
 $ floors       : num  1 2 1 1 1 1 2 1 1 2 ...
 $ waterfront   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ view         : int  0 0 0 0 0 0 0 0 0 0 ...
 $ condition    : int  3 3 3 5 3 3 3 3 3 3 ...
 $ grade        : int  7 7 6 7 8 11 7 7 7 7 ...
 $ sqft_above   : int  1180 2170 770 1050 1680 3890 1715 1060 1050 1890 ...
 $ sqft_basement: int  0 400 0 910 0 1530 0 0 730 0 ...
 $ yr_built     : int  1955 1951 1933 1965 1987 2001 1995 1963 1960 2003 ...
 $ yr_renovated : int  0 1991 0 0 0 0 0 0 0 0 ...
 $ zipcode      : int  98178 98125 98028 98136 98074 98053 98003 98198 98146 98038 ...
 $ lat          : num  47.5 47.7 47.7 47.5 47.6 ...
 $ long         : num  -122 -122 -122 -122 -122 ...
 $ sqft_living15: int  1340 1690 2720 1360 1800 4760 2238 1650 1780 2390 ...
 $ sqft_lot15   : int  5650 7639 8062 5000 7503 101930 6819 9711 8113 7570 ...
head(kc_data)
          id            date   price bedrooms bathrooms sqft_living sqft_lot
1 7129300520 20141013T000000  221900        3      1.00        1180     5650
2 6414100192 20141209T000000  538000        3      2.25        2570     7242
3 5631500400 20150225T000000  180000        2      1.00         770    10000
4 2487200875 20141209T000000  604000        4      3.00        1960     5000
5 1954400510 20150218T000000  510000        3      2.00        1680     8080
6 7237550310 20140512T000000 1225000        4      4.50        5420   101930
  floors waterfront view condition grade sqft_above sqft_basement yr_built
1      1          0    0         3     7       1180             0     1955
2      2          0    0         3     7       2170           400     1951
3      1          0    0         3     6        770             0     1933
4      1          0    0         5     7       1050           910     1965
5      1          0    0         3     8       1680             0     1987
6      1          0    0         3    11       3890          1530     2001
  yr_renovated zipcode     lat     long sqft_living15 sqft_lot15
1            0   98178 47.5112 -122.257          1340       5650
2         1991   98125 47.7210 -122.319          1690       7639
3            0   98028 47.7379 -122.233          2720       8062
4            0   98136 47.5208 -122.393          1360       5000
5            0   98074 47.6168 -122.045          1800       7503
6            0   98053 47.6561 -122.005          4760     101930
# the number of NA values in each column
colSums(is.na(kc_data))
           id          date         price      bedrooms     bathrooms 
            0             0             0             0             0 
  sqft_living      sqft_lot        floors    waterfront          view 
            0             0             0             0             0 
    condition         grade    sqft_above sqft_basement      yr_built 
            0             0             0             0             0 
 yr_renovated       zipcode           lat          long sqft_living15 
            0             0             0             0             0 
   sqft_lot15 
            0 

Initial Observations

  • The dataset comprises over 20 different features, including variables such as the number of bedrooms, bathrooms, square footage of living space, and more.

  • The target variable we aim to predict is the house price (price).

  • The dataset contains over 20,000 observations (rows), providing a substantial amount of data for analysis and model training.

  • All features in our dataset are numeric, except for the date feature. This simplifies the preprocessing step as there is no need to convert categorical data into one-hot encoded data.

  • We also found that there are no missing values (NAs) in the dataset. This eliminates the need for handling missing data.

EDA: Distribution of House Prices

The price of houses in our dataset have a right-skew distribution, meaning that the median price of houses would be a better indicator of central tendancy than the mean.

EDA: Price v/s Living Space

There is generally a positive correlation between the price and sqft_living features, indicating that larger houses tend to be more expensive. However, this relationship is not perfectly linear, suggesting the presence of some outliers. Additionally, other factors such as location, number of bedrooms and bathrooms, and view quality are also likely influencing house prices.

EDA: Price v/s Other Sq ft. Features

When plotting price against various square footage variables, it is observed that sqft_basement and sqft_lot do not show a distinctive relationship with the price variable. The presence of numerous outliers further suggests that these variables may be less predictive of house prices and could potentially be excluded from model training.

EDA: Number of ‘0’ values in features

It is essential to identify rows with zero values in certain features to prevent them from becoming outliers and hindering our model’s performance.

We observe that bathrooms and bedrooms have very few zeros, allowing us to simply exclude rows where these features are zero. The yr_renovated feature has almost all zeros, making it meaningless for model training. Conversely, yr_built has no zeros, indicating it is worth exploring its effect on the price.

EDA: Median Price v/s Number of Bedrooms

Since the price of houses is roughly increasing with the bedrooms count, the number of bedrooms can be considered one of the strong indicators of price .

EDA: Median Price v/s Number of Bathrooms

Since the price of houses is roughly increasing with the bathrooms count, the number of bathrooms can be considered one of the strong indicators of price .

EDA: Median Price v/s Other Features

The price of houses seems to be roughly increasing with an increase in view, grade, waterfront, floors, and condition values which was totally expected. These features would also serve as good indicators for our target variable price.

EDA: Median Price v/s Year Built

The plot of yr_built v/s price does not give out any significant information.

EDA: Correlation Plot

A correlation plot helps visualize the relationships between different features in our dataset and helps us identify patterns and dependencies among them.

We can see that variables such as waterfront and view are highly correlated and variables such as yr_renovated and sqft_lot are not correlated. This gives us a basic picture of interdependencies between features which might be helpful for further model development.

Data Cleaning

# Removing 0's from bedrooms and bathrooms
kc_data_clean <- kc_data %>%
  filter(bedrooms != 0 & bathrooms != 0)

# Remove columns sqft_lot, sqft_lot15, sqft_living15, sqft_basement
kc_data_clean <- kc_data_clean %>%
  select(-sqft_lot, -sqft_lot15, -sqft_living15, -sqft_basement)

# Removing column yr_renovated, date and id
kc_data_clean <- kc_data_clean %>%
  select(-yr_renovated, -date, -id)

We discarded rows with zero bedrooms and bathrooms since these instances were few and had minimal impact on the dataset size.

Columns such as sqft_lot, sqft_lot15, sqft_living15, and sqft_basement were removed due to numerous outliers and a lack of significant correlation with the target variable, price. sqft_living15 was also highly correlated with sqft_living, making it redundant.

The yr_renovated column primarily contained ‘0’ values, indicating minimal renovation activity, thus irrelevant for predictive modeling. The date and id features were also removed as id is just a unique identifier, and date in character form is not useful for predicting price.

While further data cleaning steps could be undertaken, only these actions were executed for simplicity.

Pre-processing

set.seed(142)

# Perform train-test split (80% train, 20% test)
train_index <- createDataPartition(kc_data$price, p = 0.8, list = FALSE)
train_data <- kc_data_clean[train_index, ]
test_data <- kc_data_clean[-train_index, ]


predictors <- setdiff(names(train_data), "price")
target <- "price"

# Create x_train, y_train, x_test, y_test
x_train <- train_data[, predictors]   # training features
y_train <- train_data[, target]       # training target (price)
x_test <- test_data[, predictors]     # testing features
y_test <- test_data[, target]         # testing target (price)

# Remove NA values (if any)
complete_rows <- complete.cases(x_train, y_train)
x_train <- x_train[complete_rows, ]
y_train <- y_train[complete_rows]

We split our dataset into training and testing sets to ensure fair evaluation. The model will be trained on the training data and tested on the unseen test data. Additionally, the features (x_train, x_test) are separated from the target variable price (y_train, y_test).

Simple Linear Regression Model

# Train the linear regression model
lm_model <- lm(y_train ~ ., data = x_train)

# Make predictions on the test set
predictions <- predict(lm_model, newdata = x_test)

# Calculate evaluation metrics
rmse_lr <- sqrt(mean((predictions - y_test)^2))
r_squared_lr <- cor(predictions, y_test)^2

results_lr <- data.frame(Actual = y_test, Predicted = predictions)

# Plot using ggplot
ggplot(results_lr, aes(x = Actual, y = Predicted)) +
  geom_point(alpha = 0.6, color = "green") +
  geom_abline(intercept = 0, slope = 1, color = "red", linetype = "dashed") +
  ggtitle("Actual vs. Predicted Prices (Linear Regression: Test Data)") +
  xlab("Actual Prices (in $)") +
  ylab("Predicted Prices (in $)") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.title.x = element_text(face = "bold"),
    axis.title.y = element_text(face = "bold")
  )

Our initial Linear Regression model exhibits less promising results on the Test Data since the slope of the dotted red line is noticeably greater than 1. To delve deeper, we’ll explore residual plots and training plots to understand the underlying dynamics.

From the training plot, it’s apparent that our model isn’t adequately capturing the underlying pattern of our training data. This discrepancy could stem from the fact that our data doesn’t conform to a linear relationship.

Analyzing the scatter plot for residuals reveals a distinct pattern: there’s a noticeable curve where residuals tend to be positive for lower predicted values and negative for higher predicted values. This suggests that our model might be underestimating the prices of lower-valued houses while overestimating those of higher-valued ones.

Random Forest Model

# Train the Random Forest Regression model
rf_model <- randomForest(y_train ~ ., data = x_train)

# Make predictions on the test set
predictions <- predict(rf_model, newdata = x_test)

# Calculate evaluation metrics
rmse_rf <- sqrt(mean((predictions - y_test)^2))
r_squared_rf <- cor(predictions, y_test)^2

results_rf <- data.frame(Actual = y_test, Predicted = predictions)

# Plot using ggplot
ggplot(results_rf, aes(x = Actual, y = Predicted)) +
  geom_point(alpha = 0.6, color = "lightblue") +
  geom_abline(intercept = 0, slope = 1, color = "red", linetype = "dashed") +
  ggtitle("Actual vs. Predicted Prices (Random Forest)") +
  xlab("Actual Prices") +
  ylab("Predicted Prices") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.title.x = element_text(face = "bold"),
    axis.title.y = element_text(face = "bold")
  )

The Random Forest Model exhibits superior performance compared to the Simple Linear Regression Model, as indicated by the closer alignment of the scatter plot points to the diagonal red dashed line, which represents perfect prediction (slope = 1). This proximity suggests that our Random Forest model’s predictions are more accurate, particularly for houses with lower to moderate prices.

However, as the price of houses increases beyond $2,000,000, the model’s accuracy diminishes, evident from the widening spread of data points away from the ideal line. This discrepancy indicates that our model struggles to accurately predict the prices of higher-value houses, potentially due to the complexity of factors influencing their pricing beyond what our model captures.

The Random Forest Feature Importance Plot displays the relative significance of different features in predicting house prices. Features with higher importance contribute more to the model’s predictive accuracy, while those with lower importance have less impact. This visualization helps identify which features are most influential in determining house prices.


The Random Forest Feature Importance Plot reveals that sqft_living and grade are the two most influential features in predicting house price. Interestingly, location-related features such as lat, long, and zipcode also rank among the top 10 most important features, underscoring the significant role of a property’s location in determining its value.

Gradient Boosting (xgboost) Model

# Convert training and testing data to DMatrix format
dtrain <- xgb.DMatrix(data = as.matrix(x_train), label = y_train)
dtest <- xgb.DMatrix(data = as.matrix(x_test), label = y_test)

params <- list(
  objective = "reg:squarederror",
  eval_metric = "rmse",
  max_depth = 6,
  eta = 0.3,
  subsample = 0.7,
  colsample_bytree = 0.7
)

# Train the xgboost model
xgb_model <- xgb.train(params = params, data = dtrain, nrounds = 100)

# Make predictions on the test set
predictions_xgb <- predict(xgb_model, newdata = dtest)

# Calculate evaluation metrics
rmse_xgb <- sqrt(mean((predictions_xgb - y_test)^2))
r_squared_xgb <- cor(predictions_xgb, y_test)^2

# Create a data frame for actual vs predicted values
results_xgb <- data.frame(Actual = y_test, Predicted = predictions_xgb)

# Plot using ggplot
ggplot(results_xgb, aes(x = Actual, y = Predicted)) +
  geom_point(alpha = 0.6, color = "purple") +
  geom_abline(intercept = 0, slope = 1, color = "red", linetype = "dashed") +
  ggtitle("Actual vs. Predicted Prices (Gradient Boosting)") +
  xlab("Actual Prices (in $)") +
  ylab("Predicted Prices (in $)") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.title.x = element_text(face = "bold"),
    axis.title.y = element_text(face = "bold")
  )

The Gradient Boosting model demonstrates superior performance compared to the Linear Regression model. The data points cluster closely around the ideal y=x line, suggesting that our model’s predictions closely align with actual house prices. However, challenges still persist in accurately predicting the values of higher-priced houses, as indicated by the scattered data points deviating from the line.

Combined Model Results

The RMSE (Root Mean Squared Error) is a metric used to measure the difference between the predicted values from a model and the actual values. A lower RMSE indicates a better fit for the model on the data used to create the graph. The Linear regression model has the highest RMSE, while the random forest model and XGBoost model have similar RMSEs (Random Forest model having a sightly lower RMSE).

Linear Regression has the lowest R-squared: This indicates that the linear regression model explains the least amount of variance in the target variable compared to the other two models. In other words, a significant portion of the variation in the data is not explained by the linear model.

Random Forest likely has the best fit: This is because Random Forest has the highest R-squared value among the three models. A higher R-squared signifies a better fit for the model, meaning it explains a larger proportion of the variance in the target variable.

Further Improvements

  • The major concern we notice is that our models struggle to predict the prices of high-value houses accurately, possibly due to outliers influencing predictions. Handling outliers is crucial to improve model accuracy.

  • Additionally, the Linear Regression Model performs notably worse than the other two models. Linear regression models are sensitive to feature scales, so normalizing or standardizing features is advisable to ensure equal contribution to predictions.

  • Feature Engineering, such as combining location-related features like latitude, longitude, and zipcode, could enhance model performance by capturing location-specific patterns. Also removing unnecessary features could improve model results

  • Hyperparameter Tuning for the XGBoost model is essential. Experimenting with different parameter sets and selecting the one yielding the best results can significantly improve model performance.

  • More Advanced Algorithms, such as gradient boosting machines (GBM), support vector machines (SVM), or neural networks, can be done to leverage their potential for capturing complex patterns in the data.