NYC Floods: Data Analysis

Author

Mars Onyeabo

Introduction

Source: NBC News

The dataset I will be using for this project is titled, “FloodNet: Street Flooding Events Measured by FloodNet Sensors” from the NYC Open Data website1. The dataset contains information on flood events in New York City. A flood event is defined as a series of water depth measurements greater than 10 mm. There are 3,422 rows and 13 columns where each row is one flood event per sensor. Variables include sensor name, sensor ID (ID used to identify sensor location), flood event start time, flood event end time, maximum flood depth measured, the time in minutes it took for the flood to reach max flood depth, time in minute for flood to drain, total duration of flood event, and duration of flooding greater than 4, 12, and 24 inches. In this project, I want to determine which borough experiences the most severe floods.

Loading the Data and Libraries

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.0.4     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(ggplot2)
library(ggfortify)
library(RColorBrewer)

floodnet <- read_csv("FloodNet__Street_Flooding_Events_Measured_by_FloodNet_Sensors_20261012.csv")
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
  dat <- vroom(...)
  problems(dat)
Rows: 3422 Columns: 13
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (6): Sensor Name, Sensor ID, Flood Start Datetime (GMT), Flood End Datet...
dbl (5): Maximum Flood Depth (inches), Time to Maximum Flood Depth (minutes)...
num (2): Time to Drain From Peak (minutes), Total Duration (minutes)

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
floodnet
# A tibble: 3,422 × 13
   `Sensor Name`       `Sensor ID` Flood Start Datetime…¹ Flood End Datetime (…²
   <chr>               <chr>       <chr>                  <chr>                 
 1 Q - Beach 84 St     Q-beach-84… 10/30/2023 12:00:39 PM 10/30/2023 04:38:19 PM
 2 BX - 1st St/Avenue… BX-1st-st-… 04/26/2025 11:17:20 PM 04/27/2025 11:55:34 AM
 3 Q - Beach 35 St/Be… Q-beach-35… 02/09/2024 11:41:01 AM 02/09/2024 01:20:57 PM
 4 BX - Tibbett Ave/W… BX-w-234th… 08/20/2025 09:05:19 PM 08/21/2025 12:46:19 AM
 5 Q - Davenport Ct 1  Q-davenpor… 01/10/2024 12:39:25 AM 01/10/2024 02:30:40 AM
 6 BX - Ditmars St/Hu… BX-ditmars… 05/30/2024 01:28:01 AM 05/30/2024 09:18:11 AM
 7 Q - Beach 35 St/Be… Q-beach-35… 03/31/2025 01:21:39 AM 03/31/2025 02:35:35 AM
 8 BX - Tibbett Ave/W… BX-w-234th… 05/24/2026 06:59:07 AM 05/24/2026 08:19:06 PM
 9 BX - Tibbett Ave/W… BX-w-234th… 06/18/2025 09:10:48 AM 06/18/2025 02:00:47 PM
10 Q - Russell St 1    Q-russell-… 01/17/2022 12:29:33 PM 01/17/2022 04:27:05 PM
# ℹ 3,412 more rows
# ℹ abbreviated names: ¹​`Flood Start Datetime (GMT)`,
#   ²​`Flood End Datetime (GMT)`
# ℹ 9 more variables: `Maximum Flood Depth (inches)` <dbl>,
#   `Time to Maximum Flood Depth (minutes)` <dbl>,
#   `Time to Drain From Peak (minutes)` <dbl>,
#   `Total Duration (minutes)` <dbl>, …

Cleaning The Data

#excluding irrelevant columns
tidyfloods <- floodnet |>
  select(-c("Time Series Depth Values (inches)", "Time Series Depth Timestamps (seconds)", "Flood End Datetime (GMT)", "Flood Start Datetime (GMT)", "Sensor ID"))

#change column names by removing spaces
names(tidyfloods) <- c("sensor_name", "max_flood_depth_in", "time_to_max_depth_mins", "time_to_drain_mins", "total_duration_mins", "duration_4in_mins", "duration_12in_mins", "duration_24in_mins")
tidyfloods
# A tibble: 3,422 × 8
   sensor_name      max_flood_depth_in time_to_max_depth_mins time_to_drain_mins
   <chr>                         <dbl>                  <dbl>              <dbl>
 1 Q - Beach 84 St               17.7                   134.               144. 
 2 BX - 1st St/Ave…               6.18                   28.0              730. 
 3 Q - Beach 35 St…               5.31                   54.5               45.4
 4 BX - Tibbett Av…               1.18                   80                141  
 5 Q - Davenport C…               2.09                   59.8               51.4
 6 BX - Ditmars St…               9.53                   14.7              455. 
 7 Q - Beach 35 St…               3.82                   31.8               42.1
 8 BX - Tibbett Av…               1.61                  236                564. 
 9 BX - Tibbett Av…               1.34                    5                285. 
10 Q - Russell St 1               8.27                   83.1              154. 
# ℹ 3,412 more rows
# ℹ 4 more variables: total_duration_mins <dbl>, duration_4in_mins <dbl>,
#   duration_12in_mins <dbl>, duration_24in_mins <dbl>

Linear Regression

For my linear regression, the total duration in minutes will be the dependent variable and the maximum flood depth in inches will be the independent variable. I anticipate that how long a flood is highly affects how tall the flood gets.

#linear regression model with maximum flood depth vs. total flood duration
lm <- lm(max_flood_depth_in ~ total_duration_mins, data = tidyfloods)
summary(lm)

Call:
lm(formula = max_flood_depth_in ~ total_duration_mins, data = tidyfloods)

Residuals:
    Min      1Q  Median      3Q     Max 
-20.876  -3.139  -1.430   1.302  41.481 

Coefficients:
                     Estimate Std. Error t value Pr(>|t|)    
(Intercept)         4.2616520  0.1038594   41.03   <2e-16 ***
total_duration_mins 0.0053741  0.0003829   14.04   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 5.014 on 3420 degrees of freedom
Multiple R-squared:  0.05447,   Adjusted R-squared:  0.0542 
F-statistic:   197 on 1 and 3420 DF,  p-value: < 2.2e-16

Equation: 0.0005(total duration) + 4.26

For each additional minute elapsed, there is a predicted increase of 0.0005 inches in flood depth. The p-value is incredibly significant but, according to the adjusted R-squared, only 5% of the variation in observations may be explained by the linear model.

Visualization

In front of the sensor name, each row contains an acronym. ‘BK’ represents Brooklyn, ‘BX’ represents the Bronx, ‘M’ stands for Manhattan, ‘Q’ stands for Queens, and ‘SI’ stands for Staten Island.2 Here I’ve created a new column to assign each sensor to a borough to continue my analysis.

#sort the dataset by which borough the sensor is located in
tidyfloods <- tidyfloods |>
  mutate(borough =  case_when(
    startsWith(sensor_name, "SI") ~ "Staten Island",
    startsWith(sensor_name, "M") ~ "Manhattan",
    startsWith(sensor_name, "BX") ~ "Bronx",
    startsWith(sensor_name, "BK") ~ "Brooklyn",
    startsWith(sensor_name, "Q") ~ "Queens"
          )
  )

To measure which borough has the most severe floods, I will calculate the average maximum flood depth according to each borough.

#pivoting to a smaller df
avgdepth <- tidyfloods |>
  group_by(borough) |>
  summarise(avg_max_depth = mean(max_flood_depth_in))
avgdepth
# A tibble: 5 × 2
  borough       avg_max_depth
  <chr>                 <dbl>
1 Bronx                  4.60
2 Brooklyn               4.44
3 Manhattan              2.31
4 Queens                 5.68
5 Staten Island          3.91
#bar graph
avgdepth |>
  ggplot(aes(x = borough, y = avg_max_depth)) +
  geom_col(aes(fill = borough)) +
  scale_fill_brewer(palette = "Reds") +
  labs(title = "Average Flood Depth in New York City",
       fill = "",
       x = "Borough",
       y = "Avg. Max Flood Depth (inches)") +
  theme_minimal()

Conclusion

In conclusion, this bar graph shows that the average maximum measured flood depth in inches was highest in Queens and lowest in Manhattan. In other words, floods in Manhattan didn’t rise as high as they did in other areas with an average of 2.3 inches. In Queens, the calculated average is more than one inch over the other boroughs. I’m sure further research would be able to help us determine why this is. I cleaned up the dataset by making the column names simpler (removing spaces, making it all lowercase) which made it much easier to complete my data analysis. I originally wanted to visualize the difference between floods over 4 inches, floods over 12 inches, and floods over 24 inches. However, there was only a little bit of data for floods over 24 inches so it wouldn’t be a fair comparison.