Click here for the Visualization on Tableau Public

Project Overview

Data Gathering and Cleaning

Electricity Rate Data:

  • Data source
  • Files were divided into Investor-Owned and Non-Investor-Owned Utilities, combined each year into a single sheet within a Google Sheets document.
  • Copied to a new file to preserve original data, then created a script to delete extra columns so the data only has the basics needed: zip code, state, and average residential rate.
  • Downloaded as a .csv file for use in RStudio
  • While combining data so each zip code only has a single row, discovered the data does not have all 41,704 zip codes, the resulting tables have varying numbers of rows, a little over 39,000. Some remote zip codes may not have power generation or utilities may not have provided data to the federal database. It is a relatively small percentage missing, so will continue with available data.
  • Created a list with one entry per zip to analyze changes year over year, but after further review of the data found some zips have large discrepancies in rates between 2 different electrical utility providers. Will use 2023 data for current rates and use multiple rates per zip to get accurate analysis of highest electrical rates rather than using mean rates per zip for the main analysis.

Sunroof Data:

  • Data Source
  • Created a simplified table (removing extraneous columns) with an SQL query and downloaded as a .csv file for use in RStudio.
  • Kaggle also had the same data, including state-level which may be useful for additional analysis. Downloaded that and simplified columns in Sheets.
  • Much of the Sunroof data specifically involves roof potential like the area of rooftops, which is not relevant for the scope of this project, especially because it seems the numbers include commercial rooftops, causing some zip codes with large commercial buildings to have significant outliers in solar potential of median rooftops. This project is concerned specifically with average sunlight potential. This relates to the “yearly_sunlight_kwh_kw_threshold_avg” data field, which has the average annual kWh output PER kw of solar panels installed based on average solar exposure, taking into account historical weather patterns for the area.
  • This data has 11,516 rows, so about 1/4th of ZIPs, which limits analysis but likely applies to the most populous zip codes, will check that using some code in R with population data from US census by ZIP.

Solar Install Cost Data:

  • Data Source
  • Removed tax credit column because those could change soon and are not fully relevant to the current project
  • Added column for State abbreviations because currently Rate data uses abbreviations and Sunroof data has full state names. Including a data frame into the RStudio project with state names and abbreviations to standardize the datasets.

Other:

  • Electric rates by state: Data Source - most recent update was 5/19/25 when downloaded. Main analysis is by zip but some state-level analysis will also be useful.

  • ZIP population data: from 2020 US census. Data Source - used to verify limited ZIPs of Sunroof data covers sufficient population:

sr_pop_check <- inner_join(sunroof, zip_pop_data, by = join_by(region_name == ZIP))
sum(sr_pop_check$Population)
## [1] 254732187
  • This is approximately 77% of total US population.

Processing and Analysis

x <- reduce(rateDFs, inner_join, by = "zip")

rate_changes <- data.frame(zip = str_pad(x$zip, 5, pad= "0"), state = x$state.x, rate1 = x$res_rate.x, rate2 = x$res_rate.y, rate3 = x$res_rate.x.x, rate4 = x$res_rate.y.y, rate5 = x$res_rate.x.x.x, rate6 = x$res_rate.y.y.y, rate7 = x$res_rate.x.x.x.x, rate8 = x$res_rate.y.y.y.y, rate9 = x$res_rate.x.x.x.x.x, rate10 = x$res_rate.y.y.y.y.y)

rate_changes <- rate_changes %>% mutate(avg_percent_change = 
+                          ( ((rate10 - rate9) / rate9) + 
+                           ((rate9 - rate8) / rate8) + 
+                           ((rate8 - rate7) / rate7) + 
+                           ((rate7 - rate6) / rate6) + 
+                           ((rate6 - rate5) / rate5) + 
+                           ((rate5 - rate4) / rate4) + 
+                           ((rate4 - rate3) / rate3) + 
+                           ((rate3 - rate2) / rate2) + 
+                           ((rate2 - rate1) / rate1)) / 9)
rate_changes <- rate_changes[!(rate_changes$rate1 == 0),] 

highest_increase <- rate_changes[(rate_changes$avg_percent_change > 0.1),] %>% arrange(desc(avg_percent_change)) %>% mutate(avg_percent_change = formattable::percent(avg_percent_change))
zip state avg_percent_change
99841 AK 21.01%
94130 CA 18.97%
99801 AK 15.71%
99824 AK 15.71%
92025 CA 15.43%
92026 CA 15.43%
92027 CA 15.43%
92029 CA 15.43%
92069 CA 15.43%
92078 CA 15.43%
state_data <- inner_join(solar_costs, inner_join(sunroof_by_state, state_elec_rates, by = join_by(state, state_abbrev)), join_by(State == state, 'State Code' == state_abbrev)) %>% mutate(avg_roof_annual_value = yearly_sunlight_kwh_median * res_rate, solar_value = res_rate * yearly_sunlight_kwh_kw_threshold_avg) %>% arrange(desc(solar_value))
State Avg Res Elec Rate Solar Value
Hawaii 42.34 53869.67
California 30.55 39006.64
Massachusetts 31.22 30571.40
Connecticut 28.16 28017.08
Maine 26.29 25939.08
Rhode Island 25.31 25194.57
New York 24.37 23977.64
New Hampshire 23.62 23046.08
Vermont 22.29 21182.19
Arizona 15.20 20948.97
state_data <- state_data %>% mutate(cost_adjusted_value = solar_value / as.numeric(str_remove_all(state_data$`Average Cost per Watt`, "\\$"))) %>% arrange(desc(cost_adjusted_value))
State Cost Per Watt Adjusted Solar Value
Hawaii $3.13 17210.757
California $3.33 11713.706
Massachusetts $3.12 9798.527
Connecticut $2.93 9562.142
Maine $3.10 8367.445
Rhode Island $3.04 8287.688
New Hampshire $2.97 7759.624
Vermont $2.79 7592.182
Arizona $2.79 7508.592
New York $3.33 7200.493
sv_by_utility <- inner_join(sunroof, rates_2023, join_by(region_name == zip), relationship = "many-to-many") %>% mutate(solar_value = res_rate * yearly_sunlight_kwh_kw_threshold_avg) %>% group_by(utility_name) %>% summarise(avg_solar_value = mean(solar_value), avg_rate = mean(res_rate)) %>% arrange(desc(avg_solar_value)) %>% left_join(utility_states, by="utility_name")
Utility Avg Solar Value Avg Rate State
Maui Electric Co Ltd 575.5062 0.4376639 HI
Hawaii Electric Light Co Inc 549.2299 0.4651928 HI
Hawaiian Electric Co Inc 548.9109 0.4322474 HI
City of Moreno Valley - (CA) 501.1074 0.3651722 CA
San Diego Gas & Electric Co 468.7397 0.3625885 CA
Bear Valley Electric Service 467.4137 0.3251913 CA
City of Glendale - (CA) 352.3287 0.2507589 CA
City of Pasadena - (CA) 339.1705 0.2413939 CA
Pacific Gas & Electric Co.  331.9175 0.2708457 CA
Southern California Edison Co 327.9508 0.2412293 CA
City of Colton - (CA) 324.7716 0.2286579 CA
Los Angeles Department of Water & Power 322.9479 0.2298631 CA
Merced Irrigation District 302.9477 0.2452918 CA
City of Azusa 282.6303 0.2011532 CA
Otero County Electric Coop Inc 271.3404 0.1914961 NM
Fitchburg Gas & Elec Light Co 268.7910 0.2754571 MA
Alameda Municipal Power 257.9562 0.2189595 CA
City of Norwich - (CT) 257.1614 0.2544515 CT
City of Lodi - (CA) 254.4226 0.2077174 CA
Consolidated Edison Co-NY Inc 251.3198 0.2548815 NY
sv_by_zip <- inner_join(sunroof, rates_2023, join_by(region_name == zip), relationship = "many-to-many") %>% group_by(region_name) %>% summarise(avg_res_rate = mean(res_rate), avg_sunlight = mean(yearly_sunlight_kwh_kw_threshold_avg), state=first(state)) %>% mutate(solar_value = avg_res_rate * avg_sunlight) %>% arrange(desc(solar_value))
ZIP State Solar Value
96732 HI 575.5062
96753 HI 575.5062
96761 HI 575.5062
96779 HI 575.5062
96793 HI 575.5062

sv_by_zip <- left_join(sv_by_zip, rate_changes[,c(1,13)],join_by(region_name == zip)) %>% arrange(desc(avg_percent_change))
ZIP State Solar Value Avg Rate Change
94130 CA 235.3584 0.1896504
92025 CA 312.9260 0.1543100
92026 CA 312.9260 0.1543100
92027 CA 312.9260 0.1543100
92029 CA 312.9260 0.1543100

Insights