Column

Overview

Column

Analysis

---
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;
}
```