import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import warnings
import seaborn as sns
import folium

warnings.filterwarnings("ignore")

df = pd.read_csv("C:/Users/mosop/Downloads/python files/airbnb data.csv")

Introduction

Airbnb is a global marketplace, but it does not look the same in every city. This report uses data on roughly 51,700 listings across 10 European cities to answer these questions:

  1. Where is the market concentrated?
  2. What types of places are being listed?
  3. How does price change with the size of a place?
  4. How satisfied are guests, and does a bigger market mean happier guests?

Findings

This section walks through the five charts.

Where the market is

London, Paris, Lisbon, and Rome each get their own slice, and the other six cities are grouped together as “Other Cities”. London has the largest share of listings among the four named cities.

city_counts = df['city'].value_counts()
top4 = city_counts.head(4)
other = city_counts.iloc[4:].sum()

pie_df = pd.DataFrame({'City': list(top4.index) + ['Other Cities'],
                       'Listings': list(top4.values) + [other]})

number_outside_colors = len(pie_df.City.unique())
outside_color_ref_number = np.arange(number_outside_colors)*4

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1, 1, 1)

colormap = plt.get_cmap("tab20c")
outer_colors = colormap(outside_color_ref_number)

all_listings = pie_df.Listings.sum()

donut = pie_df.set_index('City')['Listings'].plot(
    kind='pie', radius=1, colors=outer_colors, pctdistance=0.85, labeldistance=1.1,
    wedgeprops=dict(edgecolor='w'), textprops={'fontsize':18},
    autopct=lambda p: '{:.1f}%\n({:,.0f})'.format(p, (p/100)*all_listings),
    startangle=90)
hole = plt.Circle((0,0), 0.3, fc='white')
fig1 = plt.gcf()
artist = fig1.gca().add_artist(hole)
ax.yaxis.set_visible(False)
title = plt.title('Share of Airbnb listings by city', fontsize=18)
center_text = ax.text(0, 0, 'Total Listings\n' + '{:,}'.format(all_listings), ha='center', va='center', fontsize=14)
limits = ax.axis('equal')
plt.tight_layout()
plt.show()

Types of places being listed

Every city has a different mix of places. Each bar is a city, sorted from the most listings to the fewest, and the colors split its listings into entire homes, private rooms, and shared rooms. Some cities are mostly entire homes, while others have as many or more private rooms. The shared-room section is so small that it barely shows on any bar.

from matplotlib.ticker import FuncFormatter
stacked_df = df.groupby(['city', 'room_type']).size().unstack(fill_value=0)
stacked_df = stacked_df.loc[stacked_df.sum(axis=1).sort_values(ascending=False).index]
fig = plt.figure(figsize=(18, 10))
ax = fig.add_subplot(1, 1, 1)
stacked_plot = stacked_df.plot(kind='bar', stacked=True, ax=ax)
y_label = plt.ylabel('Number of Listings', fontsize=18, labelpad=10)
chart_title = plt.title('Airbnb listings by city and room type', fontsize=18)
x_ticks = plt.xticks(rotation=0, horizontalalignment = 'center', fontsize=14)
y_ticks = plt.yticks(fontsize=14)
x_label = ax.set_xlabel('City', fontsize=18)
handles, labels = ax.get_legend_handles_labels()
handles = [ handles[2], handles[1], handles[0] ]
labels  = [ labels[2],  labels[1],  labels[0] ]
chart_legend = plt.legend(handles, labels, loc='best', fontsize=14)
ax.yaxis.set_major_formatter(FuncFormatter( lambda x, pos:('%1.0fK')%(x*1e-3)))
plt.show()

City of London

Zooming in on London shows where each type of place sits. Each circle is one listing from a random sample of 1500. Green circles are entire homes, red circles are private rooms, and blue circles are shared rooms.

map_df = df[df['city'] == 'London'].sample(n=1500, random_state=42).reset_index(drop=True)
center_of_map = [map_df.latitude.mean(), map_df.longitude.mean()]
my_map = folium.Map(location = center_of_map,
                    zoom_start = 12,
                    width = '90%',
                    height = '100%',
                    left = '5%',
                    right = '5%',
                    top = '0%')

tiles = ['openstreetmap']
for tile in tiles:
    tile_layer = folium.TileLayer(tile).add_to(my_map)

layer_control = folium.LayerControl().add_to(my_map)

for i in range(0, len(map_df)):
    room = map_df.loc[i, 'room_type']
    if room == 'Entire home/apt':
        color = 'green'
    elif room == 'Private room':
        color = 'red'
    elif room == 'Shared room':
        color = 'blue'
    else:
        color = 'black'

    try:
        if color != 'black':
            circle = folium.Circle(location = [ map_df.loc[i, 'latitude'], map_df.loc[i, 'longitude']],
                          tooltip = map_df.loc[i, 'room_type'],
                          popup = 'Price: ${:.0f} \n Satisfaction: {}'.format(map_df.loc[i, 'price_total'], map_df.loc[i, 'guest_satisfaction_score']),
                          radius = 50,
                          color = color,
                          fill = True,
                          fill_color = color,
                          fill_opacity = 0.5).add_to(my_map)
    except:
        pass
print(my_map._repr_html_())
Make this Notebook Trusted to load map: File -> Trust Notebook

Size and price

Most listings are small, and price rises with size. The gray bars show how many listings there are for each size, from studios to four bedrooms, and the green bars show the average price. One-bedroom places are by far the most common, and the green bars generally climb as the places get bigger.

y= df.groupby('num_bedrooms').agg(Count=('price_total', 'size'),
avgPrice=('price_total', 'mean')).reset_index()
y.columns = ['Bedrooms', 'Count', 'avgPrice']
d2 = y.loc[0:4]
d2['Label'] = ['Studio', '1 Bed', '2 Beds', '3 Beds', '4 Beds']

def autolabel(these_bars, this_ax, place_of_decimals, symbol):
    for each_bar in these_bars:
        height = each_bar.get_height()
        this_ax.text(each_bar.get_x()+each_bar.get_width()/2, height*1.01, symbol+format(height, place_of_decimals),
                     fontsize=11, color='black', ha='center', va='bottom')

fig = plt.figure(figsize=(18, 10))
ax1 = fig.add_subplot(1, 1, 1)
ax2 = ax1.twinx()
bar_width = 0.4

x_pos = np.arange(5)
count_bars = ax1.bar(x_pos-(0.5*bar_width), d2.Count, bar_width, color='gray', edgecolor='black', label='amount of listings')
avg_price_bars = ax2.bar(x_pos+(0.5*bar_width), d2.avgPrice, bar_width, color='green', edgecolor='black', label='Average Price')

x_label = ax1.set_xlabel('Number of Bedrooms', fontsize=18)
y_label1 = ax1.set_ylabel('Count of Listings', fontsize=18, labelpad=20)
y_label2 = ax2.set_ylabel('Average Price', fontsize=18, rotation=270, labelpad=20)
ax1.tick_params(axis='y', labelsize=14)
ax2.tick_params(axis='y', labelsize=14)

chart_title = plt.title('Airbnb listings and average price by size', fontsize=18)
x_tickpos = ax1.set_xticks(x_pos)
x_ticklabels = ax1.set_xticklabels(d2.Label, fontsize=14)

count_color, count_label = ax1.get_legend_handles_labels()
price_color, price_label = ax2.get_legend_handles_labels()
legend = ax1.legend(count_color + price_color, count_label + price_label, loc='upper right', frameon=True, ncol=1, shadow=True,
                    borderpad=1, fontsize=14)
y_lim1 = ax1.set_ylim(0, d2.Count.max()*1.50)
y_lim2 = ax2.set_ylim(0, d2.avgPrice.max()*1.50)

autolabel(count_bars, ax1, '.0f', '')
autolabel(avg_price_bars, ax2, '.2f', '$')

plt.show()

Guest satisfaction

Guests rated every city highly, but not equally. London has the lowest score and Athens has the highest.

x = df.groupby('city')['guest_satisfaction_score'].mean().round(1).reset_index()
x.columns = ['Tag', 'Count']
x = x.sort_values('Count', ascending=False).reset_index(drop=True)
def pick_colors_according_to_mean_count(d):
    colors = []
    avg = d.Count.mean()
    for value in d.Count:
        if value > avg * 1.01:
            colors.append('green')
        elif value < avg * 0.99:
            colors.append('lightcoral')
        else:
            colors.append('black')
    return colors

bottom3 = 0
top3 = 10
d1 = x.loc[bottom3:top3 - 1]
d1 = d1.sort_values('Count', ascending=True)
d1.reset_index(inplace=True, drop=True)
my_colors1= pick_colors_according_to_mean_count(d1)

Above = mpatches.Patch(color='green', label='Above Average')
At    = mpatches.Patch(color='black', label='Within 1% of the Average')
Below = mpatches.Patch(color='lightcoral', label='Below Average')

fig = plt.figure(figsize=(18, 12))
ax1 = fig.add_subplot(1, 1, 1)
bars = ax1.barh(d1.Tag, d1.Count, color=my_colors1)

for row_counter, value_at_row_counter in enumerate(d1.Count):
    bar_label = ax1.text(value_at_row_counter + 0.1, row_counter,
             str(value_at_row_counter), color='black', size=12,
             ha='left', va='center', backgroundcolor='white')

x_limits = plt.xlim(85, d1.Count.max() + 1.5)

chart_legend = ax1.legend(loc='lower right', handles=[Above, At, Below], fontsize=14)
mean_line = plt.axvline(d1.Count.mean(), color='black', linestyle='dashed')
mean_text = ax1.text(d1.Count.mean() + 0.1, 0, 'Mean = ' + str(round(d1.Count.mean(), 1)),
         rotation=0, fontsize=14)

chart_title = ax1.set_title('Average satisfaction by city', size=20)
x_label = ax1.set_xlabel('Average guest satisfaction score', fontsize=16)
y_label = ax1.set_ylabel('City', fontsize=16)
x_ticks = plt.xticks(fontsize=14)
y_ticks = plt.yticks(fontsize=14)
plt.show()

Conclusion

Bigger and pricier does not mean better rated. The largest market London has the lowest satisfaction score, and one of the cheapest cities Athens has the highest. Most listings are small 1 bedroom places, price rises quickly with size, and the mix of entire homes versus private rooms varies a lot by city.