Amsterdam Airbnb dataset: An End-to-End Project | by Rob | Towards Data Science

A Data Science portfolio project is like studying for your driving license practical exam, you are not learning to drive, you are learning how to pass the exam.
When preparing your portfolio it’s important to have projects that cover different fields, techniques and are able to tell a story.
In this article, my main goal is to show how I would do a data science portfolio project that covers visualization, data preprocessing, modeling and final considerations along with production suggestions.
I am using the Amsterdam Airbnb dataset for predicting the price of a flat given a list of predictors.
The dataset once imported into Python is analyzed using pandas_profiling a very useful tool that extends the df.info() functionality in pandas.
As described in the report, the dataset contains 14 variables, 10 are numeric and 2 categorical (we will probably need to get dummy variables for those when modeling).
Moreover, according to the report variable host_listings_count and calculated_host_listings_count are highly correlated with a Pearson score of 0.94, hence we will discard the former to avoid multicollinearity problems.
We can see that our target variable price is not a number, let’s have a look to the longest one, in order to understand if there are any formatting applied that needs to be removed before converting:
max(df[‘price’].values, key = len)>>> '$1,305.00'
First of all, we can see our target variable has 2 different characters we need to get rid of, the $ symbol and the comma identifying the thousands. let’s get rid of them by using the df.apply().
df[‘price’] = df[‘price’].apply(lambda x: x.replace(‘$’, ‘’))df[‘price’] = df[‘price’].apply(lambda x: x.replace(‘,’, ‘’))df[‘price’] = pd.to_numeric(df[‘price’])
The dataset has two columns containing pieces of information about coordinates where the flat is located, plus we have our target variable. Hence we can create a heatmap to better understand where flats are located and how the price is affected by the location
To achieve we will use gmaps , a python package to create interactive maps using Google Maps.
You can use the free version, without an API key, however, you will get maps with an ugly ‘for development purposes only’ watermark, if you want to get rid of those watermarks you can register (by adding a credit card) to Google Cloud Platform and claim free credits. More info here.
Please, be careful with API keys, especially if you want to share your project online. (i disabled my key before pushing the notebook to GitHub 🙃)
You can install gmaps on Jupyter Notebook by first enabling via terminal ipywidgets extensions:
$ jupyter nbextension enable — py — sys-prefix widgetsnbextension
and then:
$ pip install gmaps
and finally, load the extension with:
$ jupyter nbextension enable — py — sys-prefix gmaps
Creating heatmaps with gmaps is simple, we specify a Map object and then pass our dataframe with coordinates and weights.
fig = gmaps.Map(layout={‘width’: ‘1000px’, ‘height’: ‘500px’, ‘padding’: ‘10px’})fig.add_layer(gmaps.heatmap_layer(df[[‘latitude’, ‘longitude’]], weights=df[‘price’]))fig
The map shows that locations in the city center are more expensive, while the outskirts are cheaper (a pattern that probably does not only exists in Amsterdam). In addition, the city center seems to have its own pattern.
In order to capture some geographical pattern we need to apply some feature engineering, a nice approach is to find a list of point of interest (POI) and calculate the distance between each observation and the POI.
If we know that a specific location is very close to a place we consider expensive most probably the whole sorrounding area will be expensive.
To calculate the distance in KM I am using a function that retrieves the haversine distance, AKA the distance between two points on a sphere.
This kind of metric has its own pros and cons: it gives an easy way to calculate the distance between two points, but it does not take into account obstacles such as buildings, lakes, rivers, borders, and so on.
To get a list of POI, I searched on Google and the, for each of them I searched the geographical coordinates.
here are the results:
It is now possible to define a function to calculate distances of one house to each of the POI:
from math import radians, cos, sin, asin, sqrtdef haversine(lon1, lat1, lon2, lat2): “”” Calculate the great circle distance between two points  on the earth (specified in decimal degrees) “”” # convert decimal degrees to radians  lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) # haversine formula  dlon = lon2 — lon1  dlat = lat2 — lat1  a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 c = 2 * asin(sqrt(a))  km = 6367 * c return km
Now we can iterate each row of the dataset, and for each of them we iterate through our POI dataframe and calculate the distance for every POI (Did I say too many times “each” or “POI”?)
Let’s now plot again our map with POIs, we can do this by iterating over each row of the poi dataframe and creating a tuple using list comprehension:
fig.add_layer(gmaps.symbol_layer([tuple(x) for x in poi.to_numpy()] , fill_color=’green’, stroke_color=’green’))fig
From the visualization, it can be possible to see that near Willemspark there are much fewer flats than compared to areas around, in addition, most of the POIs are located in ‘expensive’ areas, especially around Dam Square's district.
Let’s now start the modeling section, we will prepare our dataset by encoding the categorical variable room_type and by splitting it into train and test
df = pd.get_dummies(df)X = df.drop([‘price’], axis=1)y = df[‘price’]from sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=1)
The dataset is split in 80–20 % between train and test.
The first model used will be used as a baseline, as we need a benchmark to assess the performances of other models and compare results.
It consists of a classical Linear Regression, evaluated with cross-validation using the GridSearchCV class on r2 and MAE metrics.
let’s fit it:
from sklearn.linear_model import LinearRegressionlin_reg = LinearRegression()lin_reg.fit(X_train, y_train)y_pred = lin_reg.predict(X_test)
We can now calculate our r2 and MAE errors by creating a dummy dataframe to store results for each model:
from sklearn import metricsr2 = metrics.r2_score(y_test, y_pred)mae = metrics.mean_absolute_error(y_test, y_pred)scores = pd.DataFrame({‘Baseline (regression)’ : [r2, mae]}, index=[‘R2’, ‘MAE’])scores
The mae tells us that our predictions are on average 40$ off, while the R2 tells us our data are pretty sparsed.
An interesting plot to graphically assess the results of the regression is the difference of test set and predicted value against our test set:
The closest the values are to 0 the better since the delta y_test — y_pred should be ideally 0.
The next model has been chosen according to the sklearn map, and it consists of a Support Vector Machine. However, since playing with parameters its not always easy, and it might require specific knowledge GridSearchCV will come in help:
if 'svr_gridsearch_cv.pkl' in os.listdir():        svr_grid_search = joblib.load('svr_gridsearch_cv.pkl')    else:        from sklearn.svm import SVRsvr = SVR()param_grid = [      {'C': [1, 10, 100, 1000], 'kernel': ['linear']},      {'C': [1, 10, 100, 1000], 'gamma': [0.01, 0.001, 0.0001], 'kernel': ['rbf']}]svr_grid_search = GridSearchCV(svr, param_grid=param_grid,                                    n_jobs=-1,                                    scoring=['r2', 'neg_mean_squared_error'],                                  refit='neg_mean_squared_error', verbose=100)svr_grid_search.fit(X_train, y_train)joblib.dump(svr_grid_search.best_estimator_, 'svr_gridsearch_cv.pkl')
Be careful, this task can take more than 40 minutes, that’s why before fitting I check if the model already exists from a previous fit, and if exists, I load it.
We will then, again, predict and calculate our metrics based on the best parameters from GridSearchCV
Compared to our baseline model the mae decreased by almost 4$ on average.
Let’s also for this model plot predicted values vs the delta of the errors:
The third model we will test is based on Stochastic Gradient Descent, I will use LightGBM, a library by Microsoft which is widely used in the industry and is one of the most used libraries to win Kaggle competitions.
if 'gbm_gridsearch_cv.pkl' in os.listdir():        gbm_grid_search = joblib.load('gbm_gridsearch_cv.pkl')    else:        from lightgbm import LGBMRegressorgbm = LGBMRegressor()param_grid = {    'learning_rate': [0.01, 0.1, 1],    'n_estimators': [50, 100, 150],    'boosting_type': ['gbdt', 'dart'],    'num_leaves': [15, 31, 50]}gbm_grid_search = GridSearchCV(gbm, param_grid=param_grid,                                    n_jobs=-1,                                    scoring=['r2', 'neg_mean_squared_error'],                                  refit='neg_mean_squared_error', verbose=100)gbm_grid_search.fit(X_train, y_train)joblib.dump(gbm_grid_search.best_estimator_, 'gbm_gridsearch_cv.pkl')
The model train pretty fast and the results aren’t bad at all:
So far the best performing model is the GBM, which increases the R2 by ~ 6% while the mae is slightly worse.
Thinking about a way to further improve our regressor, the first thing that bumped into my mind was neural networks obviously! So I decided to implement a simple one:
def build_model(): model = keras.Sequential([ tf.keras.layers.Dense(64, activation=’relu’, input_shape=(25,)),  tf.keras.layers.Dropout(0.2),  tf.keras.layers.Dense(128, activation=’relu’), tf.keras.layers.Dropout(0.2),  tf.keras.layers.Dense(1) ])optimizer = tf.keras.optimizers.RMSprop(0.001)model.compile(loss=’mean_squared_error’, optimizer=optimizer, metrics=[‘mae’, r2_keras]) return model
However, after playing a bit and running it for 100 epochs results aren’t particularly surprising:
Results are basically an average between the GBM and the SVR, and also plotting the errors give a very similar plot to the previous ones.
Despite having already metrics on the performances of the models, in order to give a final evaluation on which model could be considered the best, it is necessary to add other evaluation metrics such as the resources needed to implement the model and the time needed to train it.
Considering the second model SVR: it performed exceptionally, achieving the best MAE, however the training time of a grid search to implement took over 40 minutes, meaning that every time something wants to be checked or changed it cost at least 40 minutes.
The third model (Gradient boosting tree), took a few seconds to fit and the results were pretty good, practically achieving the best results overall.
Considering the last model, the neural network, it took as well not so much time to train, a couple of minutes, however, the results weren’t radically better than the previous models, actually, it performed more or less in the same way, maybe because I didn’t choose the right hyperparameters, maybe because the amount of data, maybe because multiple other reasons, however, it didn't perform radically better than previous models.
In addition, it is a less explainable model, meaning that it is difficult for us to explain how a prediction was decided, while for example, for a linear regression we can have all the data such as intercept and coefficients:
coefficients = pd.concat([pd.DataFrame(X.columns, columns=['variable']), pd.DataFrame(np.transpose(lin_reg.coef_), columns ['coefficients'])], axis = 1)coefficients
Taking into account the previously mentioned considerations, and stating that very often compromises are a good approximation, we can conclude that, given our metrics, the best model was the Gradient Boosting Tree (LightGBM), it trained in a blink of an eye and results were the best among other candidates.
Moreover, opting for a machine learning model offers an advantage: a decision tree is an explainable model, which is possible to decompose it and find why and how it calculated specific results instead of another, calling the following method on the tree regressor it is possible to have a look at the tree’s diagram:
import lightgbmlightgbm.create_tree_digraph(gbm_grid_search.best_estimator_)
XAI, or Explainable AI is a very important aspect of modern Data Science, which focuses on how a particular prediction was achieved, instead of seeing model as black boxes.
Citing a paper* from the University of Bonn:
A prerequisite for obtaining a scientific outcome is domain knowledge, which is needed to gain explainability, but also to enhance scientific consistency.
[*] Ribana Roscher, Bastian Bohn, Marco F. Duarte, and Jochen Garcke, Explainable Machine Learning for Scientific Insights and Discoveries (2019).
Thanks for reading this story! I hope it was interesting for you. If yes, please consider following my profile, it means a lot for me 😃