---
title: "Airbnb in San Francisco"
output:
flexdashboard::flex_dashboard:
orientation: columns
vertical_layout: fill
theme:
version: 4
bootswatch: lux
source_code: embed
---
```{r setup, include=FALSE}
#***Set Up***
library(flexdashboard)
library(tidyverse)
library(sf)
library(plotly)
library(leaflet)
library(flexdashboard)
library(gt)
library(dplyr)
library(ggplot2)
library(bslib)
#***Load the Data***
airbnb <- read_csv("data/listings.csv")
boundary <- st_read("data/neighborhoods.geojson")
airbnbsf <- read.csv("data/listings_sf.csv", header=TRUE)
boundary <- st_transform(boundary, crs = 4326)
st_crs(boundary)
#***Chart A***
#*Function-Based Color Mapping***
#Define pal using color mapping functions
pal <- colorFactor(palette = "RdYlGn", domain= airbnbsf$room_type)
#*Creating a Leaflet map ***
#*Popup Format*
popup_format <-
paste0("<b>Name:</b>", airbnbsf$name, "<br>",
"<b>Host Name: </b>", airbnbsf$host_name, "<br>",
"<b>Price: </b>", airbnbsf$price, "<br>")
#***Chart B***
#New Entire Home Data Table
entirehomessf <- airbnbsf |>
filter(room_type == "Entire home/apt") |>
filter(minimum_nights < 30)
#*Create Groups for Neighbourhoods
neighbourhoodcountsf <- airbnbsf |>
count(neighbourhood, sort = TRUE) |>
rename(count = n)
#Count qualifying Entire Home/Apt listings per host
hostnumsfperhost <- entirehomessf |>
count(host_id, name = "n_entire_homes") |>
mutate(bucket = cut(n_entire_homes,
breaks = c(0, 1, 2, 3, Inf),
labels = c("1", "2", "3", "4+")))
#Summary Table
hostnumsfperhosttable <- hostnumsfperhost |>
group_by(bucket) |>
summarise(count = n()) |>
mutate(percentages = round(count / sum(count) * 100, 1))
#***CHART C: PRICE PER NEIGHBORHOOD EXPLORATION***
#Create Summary Chart with Listing Count per Neighborhood to help filter in Bar Chart Later
neighbourhoodcountsf <- airbnbsf |>
count(neighbourhood, sort = TRUE) |>
rename(count = n)
#Create Summary Table with Median Price per Neighborhood
priceperneighbourhoodsf <- airbnbsf |>
group_by(neighbourhood) |>
summarise(
medianprice = median(price, na.rm=TRUE))
#Join Median Price per Neighborhood and Number of Listings
priceperneighbourhoodsf <- left_join(priceperneighbourhoodsf, neighbourhoodcountsf, by = "neighbourhood")
#Filter Joined Summary Table
priceperneighbourhoodsf <- priceperneighbourhoodsf |>
filter(count > 100)
#Outisde of R studio, download crime data (Source https://www.sfchronicle.com/projects/2026/san-francisco-crime-neighborhood/).
#Clean data by renaming columns to match AirBnB Neighbourhood name taxonomy. Delete columns where neighbourhoods are not present in AirBnB taxonomy.
#Bring in Crime Data
crimesf<- read.csv("data/crime_by_airbnb_neighborhood.csv", header=TRUE)
#Add Crime Buckets
crimebucket <- crimesf |>
mutate(bucket = cut(Rate.per.1.000,
breaks = c(0, 5, 10, Inf),
labels = c("Low", "Medium", "High")))
#Join Crime with Existing Data
priceperneighbourhoodsf <- left_join(priceperneighbourhoodsf, crimebucket, by = c("neighbourhood" = "Neighborhood..Airbnb.naming."))
#Filter out NA
priceperneighbourhoodsffiltered <- priceperneighbourhoodsf |>
filter(!is.na(bucket))
```
Column {data-width=650}
-----------------------------------------------------------------------
### Overview
```{r}
leaflet() |>
addProviderTiles(providers$Stadia.AlidadeSmooth) |>
addProviderTiles(providers$Esri.WorldGrayCanvas,
group = "ESRI World Gray") |>
addProviderTiles(providers$Esri.WorldImagery,
group = "ESRI World Imagery") |>
setView(lng = -122.4190, lat = 37.7749, zoom = 12) |>
addCircles(data = airbnbsf,
fillColor = ~pal(room_type),
fill = TRUE,
fillOpacity = 0.7,
stroke = FALSE,
radius = 1,
popup = popup_format) |>
addPolygons(data= boundary,
color = "blue",
fill = FALSE,
weight = 1) |>
addLegend(
position = 'topright',
pal = pal,
values = airbnbsf$room_type,
title = "Room Type") |>
addLayersControl(
baseGroups = c("ESRI World Gray",
"ESRI World Imagery",
"Sadia Dark"),
overlayGroups = c("Airbnb Listings", "Neighborhood Boundary")
)
```
Column {data-width=350}
-----------------------------------------------------------------------
### Analysis {.scroll-panel}
```{r, fig.width=5, fig.height=4.5, out.width="100%", fig.align="center"}
#***Chart B****
#Bar Chart
hostnumsfperhosttable |>
ggplot(aes(x = bucket, y = count, fill = bucket)) +
geom_col(show.legend = FALSE) +
scale_fill_manual(values = c("1" = "grey70", "2" = "#c0392b",
"3" = "#c0392b", "4+" = "#c0392b")) +
labs(
x = "Number of Properties Owned by Same Host",
y = "Number of Hosts",
title = "How Many Properties Does Each Host With an Entire
Home/Apt Airbnb Have in SF?",
subtitle = "SF regulation says hosts may only have one
Entire Home/Apt Property"
) +
geom_text(aes(label = paste0(percentages, "%")),
vjust = -0.5,
size = 3.5) +
geom_vline(xintercept = 1.5, linetype = "dashed") +
annotate(
"text",
x = 1.6,
y = max(hostnumsfperhosttable$count) * 0.6,
angle = 90,
label = "Hosts with more than one property") +
theme_minimal()
#Please note changes to Chart B from Lab 2. Lab two erroneously construed the "Calculated_Host_Listing_Count" that was built into the "airbnbsf" dataset as the number of entire homes
###***Chart C***
priceperneighbourhoodsffiltered |>
ggplot(aes(x = Rate.per.1.000,
y = medianprice)) +
geom_point(position="dodge") +
scale_fill_brewer(palette = "YlOrRd")+
labs(x = "Crimes per 1000 People",
y = "AirBnB Price per Night",
title= "Violent Crime Rates and AirBnB Median Prices",
subtitle = "There is no clear correlation between neighborhoods with
high violent crime rates and price per night",
caption = "Filtered for neighborhoods with more than 100 listings.
Downtown/Civic Center, Outer Sunset, and Parkside filtered- no crime data available.
Sources: AirBnB Data- Inside AirBnB, Crime Data- SF Chronicle"
) +
theme(axis.text.x = element_text(angle = 90))
```
```{css, echo=FALSE}
.scroll-panel .chart-stage {
overflow-y: auto !important;
overflow-x: hidden !important;
}
.scroll-panel .chart-stage img {
width: 100% !important;
height: auto !important;
display: block;
margin-bottom: 16px;
}
```