Data Cleaning
# raw data
df <- read.csv("Stormwater_overflow_events.csv")
# data cleaning
df <- df %>%
drop_na() %>%
distinct()
df <- df %>%
dplyr::select(overflow_events, rain_mm, impervious_pct, combined_sewer, maintenance_overdue, upstream_construction, catchment_type, green_infra_index)
Summary Statistics of Response Variable:
## Range: 0 13
##
## Mean: 2.616162
##
## Variance: 4.392711
Distribution: The response variable is count data, meaning a Poisson distribution would be a good fit. However, there is overdispersion present in the response variable, so it actually follows the Negative Binomial distribution. There are no rows of data with repeated or missing data, but rain_c was taken out as rain_mm was preferred as a more interpretable predictor.
# descriptive statistics for overflow_events
df %>%
summarise(
Mean = mean(df$overflow_events),
SD = sd(df$overflow_events),
Median = median(df$overflow_events),
IQR = IQR(df$overflow_events),
Min = min(df$overflow_events),
Max = max(df$overflow_events),
Variance = var(df$overflow_events)
)
## Mean SD Median IQR Min Max Variance
## 1 2.616162 2.09588 2 3 0 13 4.392711
# descriptive statistics for rain_mm
df %>%
summarise(
Mean = mean(df$rain_mm),
SD = sd(df$rain_mm),
Median = median(df$rain_mm),
IQR = IQR(df$rain_mm),
Min = min(df$rain_mm),
Max = max(df$rain_mm),
Variance = var(df$rain_mm)
)
## Mean SD Median IQR Min Max Variance
## 1 27.9596 14.77619 24.8 19.4 3.3 87.6 218.3359
# descriptive statistics for impervious_pct
df %>%
summarise(
Mean = mean(df$impervious_pct),
SD = sd(df$impervious_pct),
Median = median(df$impervious_pct),
IQR = IQR(df$impervious_pct),
Min = min(df$impervious_pct),
Max = max(df$impervious_pct),
Variance = var(df$impervious_pct)
)
## Mean SD Median IQR Min Max Variance
## 1 40.63401 15.76052 40.3 21.3 5 89.9 248.3941
# descriptive statistics for green_infra_index
df %>%
summarise(
Mean = mean(df$green_infra_index),
SD = sd(df$green_infra_index),
Median = median(df$green_infra_index),
IQR = IQR(df$green_infra_index),
Min = min(df$green_infra_index),
Max = max(df$green_infra_index),
Variance = var(df$green_infra_index)
)
## Mean SD Median IQR Min Max Variance
## 1 49.76162 5.072855 49.6 7.1 36.6 64.2 25.73386
# descriptive statistics for combined_sewer
prop.table(table(df$combined_sewer))
##
## 0 1
## 0.5319865 0.4680135
# descriptive statistics for maintenance_overdue
prop.table(table(df$maintenance_overdue))
##
## 0 1
## 0.6498316 0.3501684
# descriptive statistics for upstream_construction
prop.table(table(df$upstream_construction))
##
## 0 1
## 0.7340067 0.2659933
# descriptive statistics for catchment_types
prop.table(table(df$catchment_type))
##
## Commercial Industrial Mixed Residential
## 0.2558923 0.1178451 0.2121212 0.4141414
# find mean overflow for binary variables
df %>%
group_by(combined_sewer) %>%
summarise(mean_overflow = mean(overflow_events),
sd_overflow = sd(overflow_events),
n = n())
## # A tibble: 2 × 4
## combined_sewer mean_overflow sd_overflow n
## <int> <dbl> <dbl> <int>
## 1 0 2.18 1.69 158
## 2 1 3.11 2.39 139
df %>%
group_by(maintenance_overdue) %>%
summarise(mean_overflow = mean(overflow_events),
sd_overflow = sd(overflow_events),
n = n())
## # A tibble: 2 × 4
## maintenance_overdue mean_overflow sd_overflow n
## <int> <dbl> <dbl> <int>
## 1 0 2.30 1.62 193
## 2 1 3.21 2.68 104
df %>%
group_by(catchment_type) %>%
summarise(mean_overflow = mean(overflow_events),
sd_overflow = sd(overflow_events),
n = n())
## # A tibble: 4 × 4
## catchment_type mean_overflow sd_overflow n
## <chr> <dbl> <dbl> <int>
## 1 Commercial 2.49 1.81 76
## 2 Industrial 2.97 2.51 35
## 3 Mixed 2.60 2.36 63
## 4 Residential 2.60 2.00 123
# check correlations between numeric variables and overflow_events
cor(df$overflow_events, df$rain_mm)
## [1] 0.3490611
cor(df$overflow_events, df$impervious_pct)
## [1] 0.2265991
cor(df$overflow_events, df$green_infra_index)
## [1] 0.01818317
model <- glm.nb(overflow_events ~ rain_mm + impervious_pct + green_infra_index + combined_sewer + maintenance_overdue + upstream_construction + catchment_type,
data = df)
summary(model)
##
## Call:
## glm.nb(formula = overflow_events ~ rain_mm + impervious_pct +
## green_infra_index + combined_sewer + maintenance_overdue +
## upstream_construction + catchment_type, data = df, init.theta = 16.66146599,
## link = log)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.105244 0.409053 -0.257 0.79696
## rain_mm 0.017673 0.002428 7.278 3.38e-13 ***
## impervious_pct 0.011296 0.002554 4.424 9.70e-06 ***
## green_infra_index -0.003929 0.007817 -0.503 0.61520
## combined_sewer 0.364351 0.079989 4.555 5.24e-06 ***
## maintenance_overdue 0.259273 0.080690 3.213 0.00131 **
## upstream_construction -0.038045 0.088794 -0.428 0.66831
## catchment_typeIndustrial 0.202087 0.135149 1.495 0.13484
## catchment_typeMixed -0.068650 0.117650 -0.584 0.55955
## catchment_typeResidential -0.070436 0.100747 -0.699 0.48447
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for Negative Binomial(16.6615) family taken to be 1)
##
## Null deviance: 431.19 on 296 degrees of freedom
## Residual deviance: 323.10 on 287 degrees of freedom
## AIC: 1122.3
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 16.66
## Std. Err.: 9.41
##
## 2 x log-likelihood: -1100.285
# check for multicollinearity
vif(model)
## GVIF Df GVIF^(1/(2*Df))
## rain_mm 1.033424 1 1.016575
## impervious_pct 1.049283 1 1.024345
## green_infra_index 1.021748 1 1.010815
## combined_sewer 1.045263 1 1.022381
## maintenance_overdue 1.040516 1 1.020057
## upstream_construction 1.055161 1 1.027211
## catchment_type 1.091805 3 1.014746
numeric_variables <- df[, c("overflow_events", "rain_mm", "impervious_pct", "green_infra_index")]
ggpairs(numeric_variables)
Significant predictors:
Non-significant predictors:
There appears to be more overflow events when there was higher amounts of rain. The amount of overflow events seems to be somewhat evenly distributed amongst the catchment types.
There appears to be more data for residential catchment type. The distribution of count of impervious percentage per each catchment type appears to be pretty similar.
There appears to be a moderately even distribution of green infra index scores per each catchment type, with the distribution for each one being pretty similar regardless of maintenance overdue.
There appears to be less upstream construction overall, with a higher amount of no upstream construction if it is not a combined sewer.
There appears to be a higher amount of events where it was not a combined sewer and there was no upstream construction. There is a fairly even amount of this combination for there being and not being maintenance overdue. There appears to be a higher amount of events overall if there was maintenance overdue. Data seems spread evenly when there is less rain, but there is a high count of events when rain increases and maintenance is overdue. Outliers when this is true also do have a combined sewer and upstream construction.
Most of the data is between overflow count of 0 to 7. There is pretty even distribution of events per impervious percentage, except for a few outliers when it is 50%. Most of the rain amounts are low but the high overflow counts correspond with heavy rain.
First Order Model
model_full <- glm.nb(overflow_events ~ ., data = training_data)
summary(model_full)
##
## Call:
## glm.nb(formula = overflow_events ~ ., data = training_data, init.theta = 17.18612833,
## link = log)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.4118147 0.4888816 -0.842 0.399586
## rain_mm 0.0209775 0.0030541 6.869 6.48e-12 ***
## impervious_pct 0.0115499 0.0029782 3.878 0.000105 ***
## combined_sewer 0.3767579 0.0965302 3.903 9.50e-05 ***
## maintenance_overdue1 0.3020182 0.0974090 3.101 0.001932 **
## upstream_construction1 -0.0663958 0.1090332 -0.609 0.542557
## catchment_typeIndustrial 0.1223575 0.1590551 0.769 0.441729
## catchment_typeMixed -0.1797131 0.1405208 -1.279 0.200930
## catchment_typeResidential -0.1132117 0.1211230 -0.935 0.349951
## green_infra_index -0.0004957 0.0094968 -0.052 0.958371
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for Negative Binomial(17.1861) family taken to be 1)
##
## Null deviance: 310.85 on 206 degrees of freedom
## Residual deviance: 220.27 on 197 degrees of freedom
## AIC: 777.17
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 17.2
## Std. Err.: 12.0
##
## 2 x log-likelihood: -755.168
# Perform wald test
wald.test(Sigma = vcov(model_full), b = coef(model_full), Terms = 1:9)
## Wald test:
## ----------
##
## Chi-squared test:
## X2 = 87.6, df = 9, P(> X2) = 5e-15
# Evaluates the null hypothesis that all of the first 9 coefficients are equal to zero
par(mfrow=c(2,2))
plot(model_full)
Model with Added Interactions
model_int <- glm.nb(overflow_events ~ rain_mm*impervious_pct + rain_mm*combined_sewer + maintenance_overdue + upstream_construction + catchment_type + green_infra_index, data = training_data)
summary(model_int)
##
## Call:
## glm.nb(formula = overflow_events ~ rain_mm * impervious_pct +
## rain_mm * combined_sewer + maintenance_overdue + upstream_construction +
## catchment_type + green_infra_index, data = training_data,
## init.theta = 20.24909611, link = log)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.0369885 0.5795251 0.064 0.94911
## rain_mm 0.0071365 0.0100476 0.710 0.47754
## impervious_pct 0.0029633 0.0069937 0.424 0.67177
## combined_sewer 0.2896734 0.2179430 1.329 0.18381
## maintenance_overdue1 0.2947343 0.0964392 3.056 0.00224 **
## upstream_construction1 -0.0779627 0.1083289 -0.720 0.47172
## catchment_typeIndustrial 0.1296796 0.1576730 0.822 0.41082
## catchment_typeMixed -0.1670980 0.1395593 -1.197 0.23118
## catchment_typeResidential -0.0997509 0.1202935 -0.829 0.40697
## green_infra_index -0.0011955 0.0095920 -0.125 0.90081
## rain_mm:impervious_pct 0.0002854 0.0002104 1.356 0.17495
## rain_mm:combined_sewer 0.0026983 0.0060476 0.446 0.65547
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for Negative Binomial(20.2491) family taken to be 1)
##
## Null deviance: 316.80 on 206 degrees of freedom
## Residual deviance: 222.16 on 195 degrees of freedom
## AIC: 779.16
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 20.2
## Std. Err.: 16.5
##
## 2 x log-likelihood: -753.156
par(mfrow=c(2,2))
plot(model_int)
Model With Quadratic + Interaction Terms
model_quad <- glm.nb(overflow_events ~ poly(rain_mm, 2) + rain_mm*impervious_pct + rain_mm*combined_sewer + maintenance_overdue + upstream_construction + catchment_type + green_infra_index, data = training_data)
summary(model_quad)
##
## Call:
## glm.nb(formula = overflow_events ~ poly(rain_mm, 2) + rain_mm *
## impervious_pct + rain_mm * combined_sewer + maintenance_overdue +
## upstream_construction + catchment_type + green_infra_index,
## data = training_data, init.theta = 20.02502277, link = log)
##
## Coefficients: (1 not defined because of singularities)
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.2465092 0.4946702 0.498 0.61825
## poly(rain_mm, 2)1 1.8082376 2.2027456 0.821 0.41170
## poly(rain_mm, 2)2 -0.4790037 0.6674549 -0.718 0.47297
## rain_mm NA NA NA NA
## impervious_pct 0.0032095 0.0071042 0.452 0.65143
## combined_sewer 0.3033036 0.2219330 1.367 0.17174
## maintenance_overdue1 0.2985620 0.0965706 3.092 0.00199 **
## upstream_construction1 -0.0774548 0.1083617 -0.715 0.47474
## catchment_typeIndustrial 0.1377592 0.1581391 0.871 0.38368
## catchment_typeMixed -0.1715280 0.1399186 -1.226 0.22023
## catchment_typeResidential -0.1023701 0.1205642 -0.849 0.39583
## green_infra_index -0.0012642 0.0095658 -0.132 0.89486
## rain_mm:impervious_pct 0.0002722 0.0002140 1.272 0.20339
## rain_mm:combined_sewer 0.0022081 0.0061901 0.357 0.72131
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for Negative Binomial(20.025) family taken to be 1)
##
## Null deviance: 316.42 on 206 degrees of freedom
## Residual deviance: 221.39 on 194 degrees of freedom
## AIC: 780.63
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 20.0
## Std. Err.: 16.0
##
## 2 x log-likelihood: -752.634
par(mfrow=c(2,2))
plot(model_quad)
Model with only Interactions
model_int_only <- glm.nb(overflow_events ~ rain_mm*impervious_pct + rain_mm*combined_sewer, data = training_data)
summary(model_int_only)
##
## Call:
## glm.nb(formula = overflow_events ~ rain_mm * impervious_pct +
## rain_mm * combined_sewer, data = training_data, init.theta = 12.51779913,
## link = log)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.0693922 0.3435850 0.202 0.840
## rain_mm 0.0043287 0.0103112 0.420 0.675
## impervious_pct 0.0028085 0.0071276 0.394 0.694
## combined_sewer 0.2661240 0.2223116 1.197 0.231
## rain_mm:impervious_pct 0.0003169 0.0002155 1.471 0.141
## rain_mm:combined_sewer 0.0034290 0.0061956 0.553 0.580
##
## (Dispersion parameter for Negative Binomial(12.5178) family taken to be 1)
##
## Null deviance: 297.32 on 206 degrees of freedom
## Residual deviance: 221.35 on 201 degrees of freedom
## AIC: 779.46
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 12.52
## Std. Err.: 6.91
##
## 2 x log-likelihood: -765.463
par(mfrow=c(2,2))
plot(model_int_only)
Reduced Model
model_reduced <- glm.nb(overflow_events ~ rain_mm + impervious_pct + combined_sewer, data = training_data)
summary(model_reduced)
##
## Call:
## glm.nb(formula = overflow_events ~ rain_mm + impervious_pct +
## combined_sewer, data = training_data, init.theta = 11.16512861,
## link = log)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.403296 0.183172 -2.202 0.027684 *
## rain_mm 0.019973 0.003118 6.406 1.50e-10 ***
## impervious_pct 0.012357 0.002995 4.126 3.69e-05 ***
## combined_sewer 0.377227 0.098550 3.828 0.000129 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for Negative Binomial(11.1651) family taken to be 1)
##
## Null deviance: 291.75 on 206 degrees of freedom
## Residual deviance: 219.85 on 203 degrees of freedom
## AIC: 777.9
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 11.17
## Std. Err.: 5.59
##
## 2 x log-likelihood: -767.901
par(mfrow=c(2,2))
plot(model_reduced)
Final Model
# The final test model is a variation of the reduced model with the addition of maintenance_overdue variable.
final_model <- glm.nb(
overflow_events ~ rain_mm + impervious_pct + combined_sewer +
maintenance_overdue, data = training_data )
summary(final_model)
##
## Call:
## glm.nb(formula = overflow_events ~ rain_mm + impervious_pct +
## combined_sewer + maintenance_overdue, data = training_data,
## init.theta = 15.94943497, link = log)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.480086 0.180369 -2.662 0.007775 **
## rain_mm 0.019945 0.003009 6.629 3.38e-11 ***
## impervious_pct 0.011828 0.002910 4.064 4.82e-05 ***
## combined_sewer 0.349652 0.095937 3.645 0.000268 ***
## maintenance_overdue1 0.283574 0.095959 2.955 0.003125 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for Negative Binomial(15.9494) family taken to be 1)
##
## Null deviance: 307.9 on 206 degrees of freedom
## Residual deviance: 222.8 on 202 degrees of freedom
## AIC: 771.66
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 15.9
## Std. Err.: 10.7
##
## 2 x log-likelihood: -759.658
par(mfrow=c(2,2))
plot(final_model)
AIC Test
# All models' AIC values are outputted for comparison
AIC(model_full, model_int, model_quad, model_int_only, model_reduced, final_model)
## df AIC
## model_full 11 777.1676
## model_int 13 779.1564
## model_quad 14 780.6336
## model_int_only 7 779.4635
## model_reduced 5 777.9010
## final_model 6 771.6579
Likelihood Ratio Tests
# LRT for Full Model vs Full Model w/ Interaction Terms
full_int <- lrtest(model_full, model_int)[2, "Pr(>Chisq)"]
cat("Full vs Interactions:", full_int)
## Full vs Interactions: 0.3658393
# LRT for Full Model vs Full Model w/ Quadratic & Interaction Terms
full_quad <- lrtest(model_full, model_quad)[2, "Pr(>Chisq)"]
cat("\nFull vs Quadratic + Interactions:", full_quad)
##
## Full vs Quadratic + Interactions: 0.4691799
# LRT for Full Model vs Interaction Terms Only Model
full_int_only <- lrtest(model_full, model_int_only)[2, "Pr(>Chisq)"]
cat("\nFull vs Interactions Only:", full_int_only)
##
## Full vs Interactions Only: 0.0357273
# LRT for Full Model vs Reduced Model (w/o maintenance_overdue variable)
full_reduced <- lrtest(model_full, model_reduced)[2, "$Pr(>Chisq)"]
cat("\nFull vs Reduced:", full_reduced)
##
## Full vs Reduced:
# LRT for Interaction Terms Only Model vs Reduced Model (w/o maintenance_overdue variable)
int_only_reduced <- lrtest(model_int_only, model_reduced)[2, "Pr(>Chisq)"]
cat("\nInteractions Only vs Reduced:", int_only_reduced)
##
## Interactions Only vs Reduced: 0.2955961
# LRT for Final Model (Reduced Model w/ maintenance_overdue) vs Reduced Model (w/o maintenance_overdue)
final_reduced <- lrtest(final_model, model_reduced)[2, "Pr(>Chisq)"]
cat("\nFinal vs Reduced:", final_reduced)
##
## Final vs Reduced: 0.004090648
Testing Model
test_model <- glm.nb(
overflow_events ~ rain_mm + impervious_pct + combined_sewer +
maintenance_overdue, data = testing_data)
## Warning in theta.ml(Y, mu, sum(w), w, limit = control$maxit, trace =
## control$trace > : iteration limit reached
## Warning in theta.ml(Y, mu, sum(w), w, limit = control$maxit, trace =
## control$trace > : iteration limit reached
summary(test_model)
##
## Call:
## glm.nb(formula = overflow_events ~ rain_mm + impervious_pct +
## combined_sewer + maintenance_overdue, data = testing_data,
## init.theta = 21443.66227, link = log)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.197909 0.262670 -0.753 0.45118
## rain_mm 0.017334 0.004347 3.987 6.68e-05 ***
## impervious_pct 0.011097 0.004691 2.366 0.01799 *
## combined_sewer 0.441172 0.135650 3.252 0.00114 **
## maintenance_overdue1 0.226136 0.138066 1.638 0.10144
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for Negative Binomial(21443.66) family taken to be 1)
##
## Null deviance: 137.95 on 88 degrees of freedom
## Residual deviance: 103.65 on 84 degrees of freedom
## AIC: 341.19
##
## Number of Fisher Scoring iterations: 1
##
##
## Theta: 21444
## Std. Err.: 492504
## Warning while fitting theta: iteration limit reached
##
## 2 x log-likelihood: -329.193
# Checking dispersion in training/testing data
cat("Training Dispersion:", var(training_data$overflow_events) / mean(training_data$overflow_events))
## Training Dispersion: 1.810762
cat("\nTesting Dispersion:", var(testing_data$overflow_events) / mean(testing_data$overflow_events))
##
## Testing Dispersion: 1.413655
# ratio based off training data
cat("\nTheta vs SE:", summary(final_model)$theta / summary(final_model)$SE)
##
## Theta vs SE: 1.489214
Evaluating model
# obtaining predicted values
pred_test <- predict(final_model, newdata = testing_data)
# calculating residual mean squared error
RMSE <- sqrt(mean((testing_data$overflow_events - pred_test)^2))
cat("RMSE:", RMSE)
## RMSE: 2.685286
# obtaining predicted values
df_pred <- testing_data %>%
mutate(predicted = predict(test_model, type = "response"),
actual = overflow_events)
# plot to compare actual vs predicted overflow events
ggplot(df_pred, aes(x = actual, y = predicted)) +
geom_point() +
geom_abline(color = "red", linetype = "dashed") +
labs(title = "Predicted vs Actual Overflow Events",
x = "Predicted", y = "Observed",
caption = paste("RMSE =", round(RMSE, 2)))