rpspanel gives researchers a documented, reproducible US state-by-year energy-economy panel (50 states + DC), with Renewable Portfolio Standard (RPS) stringency measures, in a few lines of R.
build_panel() merges EIA energy use, prices
and production (SEDS), electricity generation and retail sales, state
CO2 emissions, and BEA GDP, income and population.codebook() and cite_sources() document the panel.
Downloads are cached so vintages stay pinned.rps_stringency() returns adoption,
nominal, coverage-adjusted and incremental (Yin and Powers 2010)
requirements, plus design-flexibility indicators.as_pdata() hands the panel to plm,
fixest or csdm; export_panel() writes CSV, Stata or Parquet.Documentation: https://adebayomoses.github.io/rpspanel/
Studies of how US state energy policy affects growth and emissions usually rebuild the same state-by-year dataset by hand. Energy use and prices come from the EIA, GDP and population from the BEA, emissions from the EIA’s state CO2 data and renewable policy rules from DSIRE or Berkeley Lab. Each source has its own format, units, state codes and release schedule, and the cleaning code is rarely published. Renewable Portfolio Standards are also often reduced to a yes/no dummy, although real policies differ in targets, timelines, coverage and flexibility.
rpspanel addresses both problems: one function returns the merged panel
with a codebook and the vintage of every source, and rps_stringency()
turns hand-coded, source-cited rules for all 50 states and DC into
standardised annual stringency measures.
Several R packages give access to one of the underlying APIs: eia and EIAapi for EIA data, bea.R for BEA data, and tidycensus and censusapi for Census data. They return each source in its own format. rpspanel differs in that it harmonises these sources into a single documented state-year panel and adds RPS stringency measures, which none of them provide. In Python, the PUDL project publishes machine-readable RPS and clean energy standard data; rpspanel brings comparable policy measures to R together with the economic and energy variables needed for panel analysis, and hands the result to plm, fixest and csdm rather than re-implementing estimators.
Install the released version from CRAN:
install.packages("rpspanel")
or the development version from r-universe:
install.packages("rpspanel",
repos = c("https://adebayomoses.r-universe.dev", "https://cloud.r-project.org"))
Register for free keys at https://www.eia.gov/opendata/register.php
and https://apps.bea.gov/API/signup/, then add them to .Renviron:
EIA_API_KEY=your-eia-key
BEA_API_KEY=your-bea-key
Everything below is real output, produced by knitting this README against the live EIA and BEA APIs (28 October 2026).
library(rpspanel)
t0 <- Sys.time()
panel <- build_panel(years = 2001:2022)
#> Fetching eia_seds (9 series)
#> Fetching eia_generation (10 series)
#> Fetching eia_retail_sales (2 series)
#> Fetching eia_co2 (6 series)
#> Fetching bea_regional (6 series)
#> Fetching bea_nipa (1 series)
Sys.time() - t0
#> Time difference of 14.11745 secs
dim(panel)
#> [1] 1122 56
names(panel)
#> [1] "state" "state_name" "fips"
#> [4] "year" "energy_cons_total" "energy_cons_res"
#> [7] "energy_cons_com" "energy_cons_ind" "energy_cons_trans"
#> [10] "energy_prod_total" "renew_cons_total" "energy_price_avg"
#> [13] "energy_expend_total" "gen_total" "gen_coal"
#> [16] "gen_gas" "gen_nuclear" "gen_hydro"
#> [19] "gen_wind" "gen_solar" "gen_geothermal"
#> [22] "gen_wood" "gen_biomass_other" "elec_sales"
#> [25] "elec_price" "co2_total" "co2_elec"
#> [28] "co2_ind" "co2_trans" "co2_res"
#> [31] "co2_com" "gdp_real" "gdp_real_manuf"
#> [34] "gdp_nominal" "personal_income" "population"
#> [37] "income_pc" "gdp_deflator" "gen_renew_nonhydro"
#> [40] "re_share_nonhydro" "re_share_total" "elec_price_real"
#> [43] "energy_price_real" "energy_cons_pc" "energy_intensity"
#> [46] "co2_pc" "co2_intensity" "gdp_real_pc"
#> [49] "rps_adoption" "rps_nominal" "rps_coverage_adj"
#> [52] "rps_incremental" "rps_baseline_share" "rps_oos_rec"
#> [55] "rps_carveout" "rps_acp"
print(panel[panel$state == "CA" & panel$year >= 2018,
c("state", "year", "energy_cons_total", "gen_total", "re_share_nonhydro",
"co2_total", "gdp_real", "population", "rps_nominal")])
#> <rps_panel>: 1 state x 2018-2022 (5 rows), 7 variables
#> state year energy_cons_total gen_total re_share_nonhydro co2_total gdp_real
#> 106 CA 2018 7294353 195465.6 0.2999626 356.6808 2850609
#> 107 CA 2019 7232052 201784.2 0.2920013 356.5132 2969066
#> 108 CA 2020 6433619 193083.5 0.3152571 301.9630 2933002
#> 109 CA 2021 6778817 197165.1 0.3377095 324.0795 3167936
#> 110 CA 2022 6857539 203383.9 0.3437623 326.2054 3161153
#> population rps_nominal
#> 106 39535920 0.2900
#> 107 39547995 0.3100
#> 108 39527808 0.3300
#> 109 39152927 0.3575
#> 110 39125347 0.3850
The first build downloads every series from the seven sources. After
that the data come from the local cache, so the same vintage is used
every time until you pass refresh = TRUE.
attr(panel, "sources")[c("source", "retrieved")]
#> source retrieved
#> 1 eia_seds 2026-10-28 08:25:27
#> 2 eia_generation 2026-10-28 08:25:28
#> 3 eia_retail_sales 2026-10-28 08:25:28
#> 4 eia_co2 2026-10-28 08:25:29
#> 5 bea_regional 2026-10-28 08:25:31
#> 6 bea_nipa 2026-10-28 08:25:37
#> 7 rps_bundled 0.1.0.9000
head(codebook(panel), 8)
#> variable group label
#> 1 energy_cons_total energy Total energy consumption
#> 2 energy_cons_res energy Residential sector energy consumption
#> 3 energy_cons_com energy Commercial sector energy consumption
#> 4 energy_cons_ind energy Industrial sector energy consumption
#> 5 energy_cons_trans energy Transportation sector energy consumption
#> 6 energy_prod_total energy Total primary energy production
#> 7 renew_cons_total energy Total renewable energy consumption
#> 8 energy_price_avg energy Average price of energy, all sectors (nominal)
#> unit source series transformation
#> 1 billion Btu eia_seds TETCB As published
#> 2 billion Btu eia_seds TERCB As published
#> 3 billion Btu eia_seds TECCB As published
#> 4 billion Btu eia_seds TEICB As published
#> 5 billion Btu eia_seds TEACB As published
#> 6 billion Btu eia_seds TEPRB As published
#> 7 billion Btu eia_seds RETCB As published
#> 8 dollars per million Btu eia_seds TETCD As published
cite_sources(panel)
#> U.S. Energy Information Administration (2026). State Energy Data System (SEDS). <https://www.eia.gov/state/seds/>. Retrieved 2026-10-28 08:25:27 UTC.
#>
#> U.S. Energy Information Administration (2026). Electric Power Operational Data (Forms EIA-923, EIA-906, EIA-920). <https://www.eia.gov/electricity/data/state/>. Retrieved 2026-10-28 08:25:28 UTC.
#>
#> U.S. Energy Information Administration (2026). Electricity Retail Sales (Form EIA-861). <https://www.eia.gov/electricity/data/state/>. Retrieved 2026-10-28 08:25:28 UTC.
#>
#> U.S. Energy Information Administration (2026). State Energy-Related Carbon Dioxide Emissions. <https://www.eia.gov/environment/emissions/state/>. Retrieved 2026-10-28 08:25:29 UTC.
#>
#> U.S. Bureau of Economic Analysis (2026). Regional Economic Accounts (SAGDP, SAINC tables). <https://apps.bea.gov/regional/>. Retrieved 2026-10-28 08:25:31 UTC.
#>
#> U.S. Bureau of Economic Analysis (2026). National Income and Product Accounts, Table 1.1.9. <https://apps.bea.gov/iTable/?reqid=19>. Retrieved 2026-10-28 08:25:37 UTC.
#>
#> rpspanel authors (2026). rpspanel RPS rule coding (bundled). <https://github.com/adebayomoses/rpspanel>. Coding shipped with rpspanel 0.1.0.9000.
#>
#> Adebayo, M. (2026). rpspanel: US state energy-economy panels with renewable portfolio standards. R package version 0.1.0.9000.
snap <- as.data.frame(panel)
snap <- snap[snap$year == 2022 & snap$state %in% c("CA", "TX", "NY", "IA", "WY", "VT"),
c("state", "co2_pc", "re_share_nonhydro", "gdp_real_pc",
"elec_price_real", "rps_nominal")]
print(snap, digits = 3, row.names = FALSE)
#> state co2_pc re_share_nonhydro gdp_real_pc elec_price_real rps_nominal
#> CA 8.34 0.3438 80796 18.92 0.3850
#> IA 22.33 0.6353 63181 8.11 0.0065
#> NY 8.42 0.0658 89492 15.53 0.3622
#> TX 22.01 0.2639 66566 8.61 0.0472
#> VT 8.50 0.4733 54895 14.40 0.5900
#> WY 96.58 0.2150 64183 6.98 0.0000
National totals match the published figures (2020: about 4,570 million tonnes of energy-related CO2, 4,010 TWh of net generation, 331.6 million people):
y20 <- panel[panel$year == 2020, ]
c(co2_mt = sum(y20$co2_total), generation_twh = sum(y20$gen_total) / 1000,
population_m = sum(y20$population) / 1e6, real_gdp_tn = sum(y20$gdp_real) / 1e6)
#> co2_mt generation_twh population_m real_gdp_tn
#> 4571.9727 4009.7668 331.5781 20.1624
d <- as.data.frame(panel)
d$group <- ifelse(d$rps_adoption %in% 1, "RPS in force", "No RPS")
avg <- aggregate(re_share_nonhydro ~ year + group, data = d, FUN = mean)
par(mar = c(4, 4.5, 2.5, 1), las = 1)
plot(NA, xlim = range(avg$year), ylim = c(0, max(avg$re_share_nonhydro) * 1.1),
xlab = "", ylab = "Non-hydro renewable share", main = "Renewable generation share, state average")
cols <- c("RPS in force" = "#1b7837", "No RPS" = "#8c510a")
for (g in names(cols)) {
s <- avg[avg$group == g, ]
lines(s$year, s$re_share_nonhydro, col = cols[g], lwd = 3)
}
legend("topleft", names(cols), col = cols, lwd = 3, bty = "n")

w <- merge(d[d$year == 2005, c("state", "co2_pc")],
d[d$year == 2022, c("state", "co2_pc", "rps_nominal")],
by = "state", suffixes = c("_2005", "_2022"))
w$change <- 100 * (w$co2_pc_2022 / w$co2_pc_2005 - 1)
par(mar = c(4.5, 4.5, 2.5, 1), las = 1)
plot(w$rps_nominal, w$change, pch = 19, col = "#2166ac",
xlab = "Nominal RPS requirement, 2022 (share of sales)",
ylab = "Change in CO2 per capita, 2005-2022 (%)",
main = "CO2 per capita change vs RPS stringency")
lab <- w$rps_nominal > 0
text(w$rps_nominal[lab], w$change[lab], w$state[lab], pos = 3, cex = 0.6, col = "grey30")
abline(lm(change ~ rps_nominal, data = w), col = "#b2182b", lwd = 2)

library(fixest)
feols(log(co2_pc) ~ rps_nominal + log(gdp_real_pc) | state + year,
data = as_pdata(panel, "fixest"), cluster = ~state)
#> OLS estimation, Dep. Var.: log(co2_pc)
#> Observations: 1,122
#> Fixed-effects: state: 51, year: 22
#> Standard-errors: Clustered (state)
#> Estimate Std. Error t value Pr(>|t|)
#> rps_nominal -0.099899 0.142014 -0.703447 0.485040
#> log(gdp_real_pc) 0.194634 0.111038 1.752866 0.085755 .
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> RMSE: 0.065123 Adj. R2: 0.986657
#> Within R2: 0.027186
This two-way fixed-effects regression is only an illustration of the workflow, not a causal estimate.
f <- file.path(tempdir(), "us_state_panel.dta")
export_panel(panel, f) # Stata file + us_state_panel_codebook.csv
file.exists(f)
#> [1] TRUE
The RPS measures come from the coding bundled with the package, so they need no API key:
library(rpspanel)
rps_stringency(c(2010, 2020, 2030), states = c("CA", "WA", "TX"),
method = c("adoption", "nominal"))
#> state year rps_adoption rps_nominal
#> 1 CA 2010 1 0.20
#> 2 CA 2020 1 0.33
#> 3 CA 2030 1 0.60
#> 4 TX 2010 1 NA
#> 5 TX 2020 1 NA
#> 6 TX 2030 0 0.00
#> 7 WA 2010 1 0.00
#> 8 WA 2020 1 0.15
#> 9 WA 2030 1 0.15
s <- rps_stringency(2000:2035, states = c("CA", "NY", "OR", "IL", "NC", "WA"),
method = "nominal")
cols <- c(CA = "#1b9e77", NY = "#d95f02", OR = "#7570b3",
IL = "#e7298a", NC = "#66a61e", WA = "#e6ab02")
par(mar = c(4, 4.5, 2.5, 1), las = 1)
plot(NA, xlim = c(2000, 2035), ylim = c(0, 1), xlab = "",
ylab = "Required share of retail sales", main = "Nominal RPS requirements")
for (st in names(cols)) {
x <- s[s$state == st, ]
lines(x$year, x$rps_nominal, col = cols[st], lwd = 2.5)
}
legend("topleft", names(cols), col = cols, lwd = 2.5, bty = "n", ncol = 2)

Texas shows NA here because its standard is set in megawatts. Passing
retail sales (sales =), which build_panel() does automatically,
converts Texas’s and Iowa’s targets to a share of covered sales. The
conversion assumes a capacity factor (0.33 by default, adjustable with
capacity_factor =), so those values are approximate; they agree with
Berkeley Lab’s equivalents to within about one percentage point in every
year.
Use interpolation = "step" to follow the legal schedule exactly
instead of interpolating between target years. See
vignette("rps-stringency") for all five methods.
| Module | Functions |
|---|---|
| Fetch and cache |
fetch_eia(), fetch_bea(), fetch_census(), fetch_rps(), rps_cache_*()
|
| Harmonise |
harmonise_states(), deflate(), convert_units()
|
| Panel builder | build_panel() |
| RPS stringency |
rps_rules(), rps_stringency()
|
| Documentation |
codebook(), cite_sources()
|
| Helpers |
as_pdata(), export_panel()
|
The RPS rules bundled in inst/extdata/ code all 50 states and DC: 33
states with a mandatory RPS (including the repealed standards in KS, MT
and WV, and the ended or repealed ones in AZ, OH and TX) and 18 without
one. The RPS rows and the voluntary-goal rows have been checked against
the statutes, DSIRE program pages and the LBNL RPS & CES data (June
2026), and every row cites its sources in the source and notes
columns (verified = TRUE for all 51). Nebraska is coded as having no
RPS: Berkeley Lab counts the board-adopted clean energy commitments of
its public power utilities as a standard from 2040, but they are utility
rather than state policies. Corrections are very welcome through GitHub
issues.
US Energy Information Administration (SEDS, electricity, CO2 emissions), US Bureau of Economic Analysis (Regional and NIPA accounts) and US Census Bureau (ACS). The package downloads data on demand and does not redistribute it.
To cite rpspanel in publications, use citation("rpspanel"), or cite
the Zenodo record, which always points to the latest version:
https://doi.org/10.5281/zenodo.23480612.
Bug reports, corrections to the RPS coding and other contributions are welcome; see the contributing guide.
Please note that the rpspanel project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.