Files
BostonHousing/bostonhousing.py
T
2024-06-08 20:34:26 +02:00

521 lines
20 KiB
Python

# %% [markdown]
# # The question
# How can the analysis of various socio-economic and environmental factors, like crime rate, land zoning, business proportion, pollution levels, housing features, and educational resources, accurately predict the market values of houses in Boston?
#
# This question seeks to understand the impact of a wide range of factors on house values, aiming to develop a comprehensive model that accounts for the multifaceted nature of real estate valuation.
# %%
# Import necessary libraries
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
from sklearn.utils import resample
from numpy.linalg import LinAlgError
# %%
# Set a seed for reproducibility of random operations like train-test split
seed = 22980254
# Load the dataset into a pandas DataFrame
df = pd.read_csv('housing_data.csv')
# %% [markdown]
#
# # Perform Exploratory Data Analysis (EDA)
#
# Perform an exploratory analysis on a broader set of variables relevant to the research question.
# %%
# Basic information about the dataset
data_info = df.info()
# Initial exploration of specified variables
exploration = df[['medv', 'rm', 'rad']].describe()
# Checking for missing values
missing_values = df[['medv', 'rm', 'rad']].isnull().sum()
# %% [markdown]
# # Visualizing the data
# %%
# Histograms
plt.figure(figsize=(15, 5))
plt.subplot(1, 3, 1)
sns.histplot(df['medv'], kde=True)
plt.title('Distribution of MEDV')
plt.subplot(1, 3, 2)
sns.histplot(df['rm'], kde=True)
plt.title('Distribution of RM')
plt.subplot(1, 3, 3)
sns.histplot(df['rad'], kde=True, bins=24)
plt.title('Distribution of RAD')
plt.tight_layout()
plt.show()
# %%
# Scatter plots
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
sns.scatterplot(x='rm', y='medv', data=df)
plt.title('MEDV vs. RM')
plt.subplot(1, 2, 2)
sns.scatterplot(x='rad', y='medv', data=df)
plt.title('MEDV vs. RAD')
plt.tight_layout()
plt.show()
# %%
# Generate a correlation matrix heatmap to visualize the correlations between all variables
plt.figure(figsize=(14, 10))
correlation_matrix = df[['medv', 'rm', 'rad']].corr()
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Matrix Heatmap')
print(correlation_matrix)
# %%
print(exploration)
print(missing_values)
# %% [markdown]
# ## Some comments and findings
# The whole dataset is composed by 506 entries spread alongside 12 columns, in this dataset there are no missing values.
#
# ### The Statistics
# 1) MEDV (Median Value of Houses):
# - Range: $5,000 - $50,000
# - Mean: $22,533
# - Standard Deviation: $9,197
# 2) RM (Average Number of Rooms per Dwelling):
# - Range: 3.56 - 8.78 rooms
# - Mean: 6.28 rooms
# - Standard Deviation: 0.70 rooms
# 3) RAD (Index of Accessibility to Radial Highways):
# - Range: 1 - 24
# - Mean: 9.55
# - Standard Deviation: 8.71
#
# ### Visualizations
# 1. Histograms:
# - MEDV: Appears to have a semi-normal distribution with a peak at around the values $20,000 - $25,000.
# - RM: Shows a normal distribution centered around 6 rooms.
# - RAD: Exhibits a bimodal distribution, indicating two groups of areas based on highway accessibility.
#
# 2. Scatter Plots:
# MEDV vs. RM:
# - Shows a positive correlation, suggesting that houses with more rooms tend to have higher median values.
# MEDV vs. RAD:
# - The relationship is not as clear, but there seems to be a trend
# - where higher accessibility to highways correlates to lower house values, possibly due to noise or other factors.
#
# 3. Correlation Matrix:
# - MEDV and RM: Strong positive correlation (0.70).
# - MEDV and RAD: Negative correlation (-0.38).
#
# ## Initial Findings
# • The number of rooms (RM) has a significant positive impact on the median value of houses.
#
# • Accessibility to highways (RAD) appears to negatively impact house values, though this relationship is less clear and might be influenced by other factors like noise or pollution.
# %% [markdown]
# # Formal Analysis
# The proces is the folowwing:
#
# 1. *Data Splitting*:
# - Create a function to split the dataset into a 90% training set and a 10% test set.
# 2. *Model Fitting*:
# - Develop a function to fit a linear regression model using the training set.
# 3. *Coefficient Interpretation*:
# - Interpret the coefficients for rad and crim.
# 4. *Bootstrap Analysis*:
# - Implement a bootstrap method to test the significance of coefficients.
# 5. *Hypothesis Testing and Model Refinement*:
# - Use hypothesis testing to identify significant variables.
# - Refine the model based on significant variables.
# - Evaluate the refined model on the test set using Mean Squared Error (MSE)
#
# %%
# Define a custom function to split the data into training and testing sets
# This function shuffles the indices and splits the data accordingly, ensuring reproducibility by setting a random seed
import numpy as np
import pandas as pd
def custom_split(data, target_column, test_size=0.1, student_id=seed):
# Validate inputs
if len(data) == 0 or test_size <= 0 or test_size >= 1:
raise ValueError("Invalid data size or test size")
if target_column not in data.columns:
raise ValueError("Target column not found in data")
# Set random seed using student ID for reproducibility
np.random.seed(student_id)
# Shuffle the dataset more thoroughly
data_shuffled = data.sample(frac=1, random_state=student_id).reset_index(drop=True)
# Determine the size of the test set
test_set_size = int(len(data) * test_size)
# Ensure test_set_size is not larger than the dataset
test_set_size = min(test_set_size, len(data) - 1)
# Split the indices for the test and training sets
test_indices = np.arange(test_set_size)
train_indices = np.arange(test_set_size, len(data))
# Split the data into training and test sets
test_set = data_shuffled.iloc[test_indices.tolist()]
train_set = data_shuffled.iloc[train_indices.tolist()]
# Split features and target variable
X_train = train_set.drop(columns=target_column)
y_train = train_set[target_column]
X_test = test_set.drop(columns=target_column)
y_test = test_set[target_column]
return X_train, X_test, y_train, y_test
# %%
# Custom function to fit a linear regression model using the normal equation
def custom_lr(X, y):
X_b = np.c_[np.ones((X.shape[0], 1)), X] # Adding a column of ones for the intercept term
theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) # Calculating best fit parameters
return theta_best
# Custom function to predict using the linear regression model
def custom_predict(X, theta):
X_b = np.c_[np.ones((X.shape[0], 1)), X]
return X_b.dot(theta)
# Custom function to calculate Mean Squared Error
def custom_mean_squared_error(y_true, y_pred):
mse = np.mean((y_true - y_pred) ** 2)
return mse
# %%
# Splitting the data
X_train, X_test, y_train, y_test = custom_split(df, 'medv')
# %%
# Fitting the model using the custom function
theta_best = custom_lr(X_train, y_train)
# %%
# Coefficient Interpretation
coefficients = pd.DataFrame([theta_best[1:]], columns=X_train.columns, index=['Coefficient']).T
# %%
# Bootstrap Analysis
def bootstrap_analysis(data, n_bootstrap=1000):
bootstrap_coefs = []
for _ in range(n_bootstrap):
sample_data = data.sample(n=len(data), replace=True)
X_sample, y_sample = sample_data.drop(columns='medv'), sample_data['medv']
theta_sample = custom_lr(X_sample, y_sample)
bootstrap_coefs.append(theta_sample[1:]) # Exclude the intercept
return np.array(bootstrap_coefs)
bootstrap_coefs = bootstrap_analysis(df)
# %%
# Calculating statistics for bootstrap coefficients
coef_std = np.std(bootstrap_coefs, axis=0)
confidence_intervals = np.percentile(bootstrap_coefs, [2.5, 97.5], axis=0)
# %%
# Preparing results for display
coef_analysis = pd.DataFrame({
'Coefficient Mean': np.mean(bootstrap_coefs, axis=0),
'Std Dev': coef_std,
'95% CI Lower': confidence_intervals[0],
'95% CI Upper': confidence_intervals[1]
}, index=X_train.columns)
# %%
# Check for significance
coef_analysis['Significant'] = (coef_analysis['95% CI Lower'] > 0) | (coef_analysis['95% CI Upper'] < 0)
# %%
# Predicting and calculating MSE
y_pred_custom = custom_predict(X_test, theta_best)
mse_custom = custom_mean_squared_error(y_test, y_pred_custom)
# %%
# Displaying the results
print("Custom Coefficients:", coefficients)
print("Custom Mean Squared Error on Test Set:", mse_custom)
# %%
for i, feature in enumerate(X_train.columns):
print(f"{feature}: {confidence_intervals[:, i]}")
# %% [markdown]
# In our confindence list of the intervals the data that is considered statistically significant are: CRIM, ZN, NOX, RM, DIS, RAD, TAX, PTRATIO and LSTAT
# %% [markdown]
# # Model Coefficients
# - crim -0.131907
# - zn 0.048926
# - indus 0.024283
# - nox -18.026122
# - rm 3.520948
# - age 0.003638
# - dis -1.535510
# - rad 0.334010
# - tax -0.013442
# - ptratio -1.004608
# - lstat -0.583388
#
# ## Interpreting the coefficients
# The coefficients of the linear regression model, which predicts the median value of houses (medv) using various features, are as follows:
# - CRIM: -0.132 (per capita crime rate by town)
# - ZN: 0.049 (proportion of residential land zoned for lots over 25,000 sq.ft.)
# - INDUS: 0.024 (proportion of non-retail business acres per town)
# - NOX: -18.03 (nitric oxides concentration)
# - RM: 3.52 (average number of rooms per dwelling)
# - AGE: 0.004 (proportion of owner-occupied units built prior to 1940)
# - DIS: -1.54 (weighted distances to five Boston employment centers)
# - RAD: 0.334 (index of accessibility to radial highways)
# - TAX: -0.013 (full-value property-tax rate per $10,000)
# - PTRATIO: -1.005 (pupil-teacher ratio by town)
# - LSTAT: -0.583 (% lower status of the population)
#
# _Values can vary because of the manual splits function_
#
# *Interpretation of Key Coefficients*:
# - RM (Rooms): A coefficient of 3.52 suggests a strong positive relationship between the
# number of rooms and house value. Each additional room is associated with an increase in
# the median value by approximately $3521.
# - RAD (Accessibility to Highways): The coefficient of 0.334 indicates a positive
# relationship, meaning that increased accessibility to highways is associated with a higher
# median house value. However, the impact is relatively smaller compared to other
# variables like rm.
# *Mean Squared Error (MSE) on Test Set*:
# - The MSE of the model on the test set is 21.58. This value represents the average
# squared difference between the actual and predicted house values, providing a measure
# of the model's accuracy.
# %%
# Perform bootstrap evaluation
def custom_bootstrap_evaluation(X_train, y_train, X_test, y_test, n_iterations=1000):
mse_values = []
for _ in range(n_iterations):
boot_x, boot_y = resample(X_train, y_train)
theta_boot = custom_lr(boot_x, boot_y)
y_pred_boot = custom_predict(X_test, theta_boot)
mse_values.append(custom_mean_squared_error(y_test, y_pred_boot))
return np.mean(mse_values), np.std(mse_values)
bootstrap_mse_mean, bootstrap_mse_std = custom_bootstrap_evaluation(X_train, y_train, X_test, y_test)
print(f"Custom Model - Bootstrap MSE Mean: {bootstrap_mse_mean}, Std: {bootstrap_mse_std}")
# %% [markdown]
# ## Bootstrap Analysis:
# - The bootstrap analysis performed on the linear regression model provides insightful statistics about the stability and significance of each coefficient. By calculating the standard deviation of coefficients across multiple bootstrap samples, we gain an understanding of their variability. This variability measurement is crucial for assessing the reliability of each feature's impact on the model.
#
# - Additionally, the 95% confidence intervals for each coefficient are calculated. These intervals give a range in which the true value of the coefficient is likely to fall, offering a degree of certainty about the estimations. This approach is particularly useful in determining the robustness of each feature's influence on the target variable.
#
# - Lastly, significance testing is conducted to determine whether the coefficients are significantly different from zero. A coefficient significantly different from zero suggests that the corresponding feature plays a meaningful role in predicting the target variable. This step is vital for feature selection and model refinement, as it helps in identifying the most impactful predictors among the available features.
# -Overall, the bootstrap analysis deepens our understanding of the model's behavior by providing a comprehensive view of the importance and reliability of each feature in the dataset
# %% [markdown]
# # Refined model
# %%
# Splitting the data using custom_split
X_train, X_test, y_train, y_test = custom_split(df, 'medv')
# Fitting the original model with all features
theta_best = custom_lr(X_train, y_train)
# Predicting on the test set with the original model
y_pred_original = custom_predict(X_test, theta_best)
mse_original = custom_mean_squared_error(y_test, y_pred_original)
# Refining the model to include only 'rm' and 'rad'
X_train_refined = X_train[['crim', 'zn', 'nox', 'rm', 'dis', 'rad', 'tax', 'ptratio', 'lstat']]
X_test_refined = X_test[['crim', 'zn', 'nox', 'rm', 'dis', 'rad', 'tax', 'ptratio', 'lstat']]
theta_refined = custom_lr(X_train_refined, y_train)
# Predicting on the test set with the refined model
y_pred_refined = custom_predict(X_test_refined, theta_refined)
mse_refined = custom_mean_squared_error(y_test, y_pred_refined)
# Coefficients of the refined model
refined_coefficients = pd.DataFrame([theta_refined[1:]], columns=X_train_refined.columns, index=['Coefficient']).T
print("Original Model MSE:", mse_original)
print("Refined Model MSE:", mse_refined)
# Plotting the results
plt.figure(figsize=(10, 5))
plt.bar(['Original Model', 'Refined Model'], [mse_original, mse_refined], color=['blue', 'red'])
plt.ylabel('Mean Squared Error')
plt.title('Original vs Refined Model MSE Comparison')
plt.show()
plt.figure(figsize=(10, 5))
plt.bar(refined_coefficients.index, refined_coefficients['Coefficient'], color='blue')
plt.ylabel('Coefficient Value')
plt.title('Coefficients of the Refined Model')
plt.show()
# %% [markdown]
# Bar charts are used to compare the MSE of the original and refined models.
# %%
# Splitting the data
X_train, X_test, y_train, y_test = custom_split(df, 'medv')
# Fitting the original model
theta_best = custom_lr(X_train, y_train)
# Predicting on the test set with the original model
y_pred_original = custom_predict(X_test, theta_best)
# Refining the model to include only 'rm' and 'rad'
X_train_refined = X_train[['rm', 'rad']]
X_test_refined = X_test[['rm', 'rad']]
theta_refined = custom_lr(X_train_refined, y_train)
# Predicting on the test set with the refined model
y_pred_refined = custom_predict(X_test_refined, theta_refined)
# Preparing data for violin plot
plot_data = pd.DataFrame({
'Actual Values': y_test,
'Original Model Predictions': y_pred_original,
'Refined Model Predictions': y_pred_refined
})
# Melting the DataFrame for use with seaborn
plot_data_melted = plot_data.melt(var_name='Group', value_name='MEDV')
# Plotting violin plots
plt.figure(figsize=(12, 6))
sns.violinplot(x='Group', y='MEDV', data=plot_data_melted)
plt.title('Comparison of Prediction Distributions')
plt.show()
# %% [markdown]
# Violin plots are drawn to display the distribution of predictions from both models against the actual values, offering a comprehensive view of the model's performance.
# %% [markdown]
# ## Results of the refined model
#
# *Mean Squared Error (MSE) Comparison*:
# - The MSE of the original model was 21.576.
# - After refining the model to focus on the variables RM (number of rooms) and RAD
# (accessibility to highways), the MSE increased to 21.69. This increase suggests that while these two variables are important, other variables might be needed to be included to have a better model and predictions
#
# *Coefficients of the Refined Model*:
# - RM (Rooms): Coefficient of 8.341. This larger coefficient in the refined model
# underscores the strong positive impact of the number of rooms on house value.
# - RAD (Accessibility to Highways): Coefficient of -0.266. In the refined model, this variable shows a negative impact, contrary to the original model. This suggests that when considered alone with RM, highway accessibility might be inversely related to house values.
#
# *Visualizations*:
# 1) MSE Comparison: The bar chart compares the MSEs of the original and refined models, illustrating the change in prediction accuracy.
# 2) Coefficients of the Refined Model: The bar chart displays the coefficients of RM and RAD in the refined model, highlighting their relative impacts on house value.
# %% [markdown]
# ## Some Considerations
# Since the model was so little and the split function made by myself was not randomized enough it means that the MSE and the impact of the coefficients are not as accurate as the pre defined one like from the 'sklearn' library or the 'xgboost' ones below we can see how close or far we got from these
# %%
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from xgboost import XGBRegressor
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt
# Assuming df is your DataFrame and 'medv' is the target column
# Splitting the data using custom_split
X_train_custom, X_test_custom, y_train_custom, y_test_custom = custom_split(df, 'medv')
# Splitting the data using sklearn's train_test_split
X_train_sklearn, X_test_sklearn, y_train_sklearn, y_test_sklearn = train_test_split(df.drop(columns='medv'), df['medv'], test_size=0.1, random_state=42)
# Initialize lists to store MSEs
mse_custom_split = []
mse_sklearn_split = []
# Custom Linear Regression Model
theta_custom = custom_lr(X_train_custom, y_train_custom)
y_pred_custom = custom_predict(X_test_custom, theta_custom)
mse_custom_split.append(custom_mean_squared_error(y_test_custom, y_pred_custom))
theta_sklearn = custom_lr(X_train_sklearn, y_train_sklearn)
y_pred_sklearn = custom_predict(X_test_sklearn, theta_sklearn)
mse_sklearn_split.append(custom_mean_squared_error(y_test_sklearn, y_pred_sklearn))
# Sklearn Linear Regression Model
lr_model_custom = LinearRegression()
lr_model_custom.fit(X_train_custom, y_train_custom)
y_pred_lr_custom = lr_model_custom.predict(X_test_custom)
mse_custom_split.append(mean_squared_error(y_test_custom, y_pred_lr_custom))
lr_model_sklearn = LinearRegression()
lr_model_sklearn.fit(X_train_sklearn, y_train_sklearn)
y_pred_lr_sklearn = lr_model_sklearn.predict(X_test_sklearn)
mse_sklearn_split.append(mean_squared_error(y_test_sklearn, y_pred_lr_sklearn))
# XGBoost Model
xgb_model_custom = XGBRegressor()
xgb_model_custom.fit(X_train_custom, y_train_custom)
y_pred_xgb_custom = xgb_model_custom.predict(X_test_custom)
mse_custom_split.append(mean_squared_error(y_test_custom, y_pred_xgb_custom))
xgb_model_sklearn = XGBRegressor()
xgb_model_sklearn.fit(X_train_sklearn, y_train_sklearn)
y_pred_xgb_sklearn = xgb_model_sklearn.predict(X_test_sklearn)
mse_sklearn_split.append(mean_squared_error(y_test_sklearn, y_pred_xgb_sklearn))
# Plotting the results
plt.figure(figsize=(12, 6))
model_names = ['Custom Linear Regression', 'Sklearn Linear Regression', 'XGBoost']
bar_width = 0.35
index = np.arange(len(model_names))
plt.bar(index, mse_custom_split, bar_width, label='Custom Split')
plt.bar(index + bar_width, mse_sklearn_split, bar_width, label='Sklearn Split')
plt.xlabel('Model')
plt.ylabel('Mean Squared Error')
plt.title('MSE Comparison by Split Method')
plt.xticks(index + bar_width / 2, model_names)
plt.legend()
plt.tight_layout()
plt.show()
# %% [markdown]
# As we can see if we had used the already existing split functions the model would have been much better
# %% [markdown]
# # The Conclusion
#
# In conclusion, this analysis employs a data-driven approach to unravel the intricate factors influencing house values in Boston. By considering a broad spectrum of socio-economic and environmental variables, the study provides valuable insights into the housing market, highlighting the significance of certain features while also emphasizing the complexity and multifaceted nature of real estate valuation.
#
#