Last active
September 22, 2024 21:29
-
-
Save abfo/b89e208f837fc3b9095d3a6f68fd8d52 to your computer and use it in GitHub Desktop.
Python to simulate different PG&E rate plans with an electric vehicle. Why? See https://ithoughthecamewithyou.com/post/which-pge-rate-plan-works-best-for-ev-charging
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Best PG&E Electric Rate Plan for EV\n", | |
"\n", | |
"Actual rate plan data in [this](https://www.pge.com/assets/rates/tariffs/Res_Inclu_TOU_Current.xlsx) Excel file. [This](https://www.pge.com/en/account/rate-plans/find-your-best-rate-plan/tiered-rate-plan.html) page has some details on the E1 tiered rate plan. TOU holidays are defined [here](https://www.pge.com/tariffs/en/rate-information/tou-holidays.html).\n", | |
"\n", | |
"Most plans have some minimum daily delivery, ignoring this as they are similar and not driven by usage. The tiered plans (E-1) etc just start charing more once you use more and are designed for people who can't control when power is used so not analyzing these. The remaining plans have a discount based on when you draw power, so looking at E-TOU-B, E-TOU-C and E-TOU-D which all have subtle differences. Also EVA, EVB, EV2 and E-ELEC which are targeted at EV / other electrical technology usage.\n", | |
"\n", | |
"Baseline is need for E-TOU-C. Information is on page 3 of the bill. I'm in baseline territory T, and my heat source is B - Not Electric (makes sense, heating is gas). For the current month my baseline is 208 kWh which is 32 days at 6.5 kWh / day. It's 32 because the billing cycle is 7/19/24 to 8/19/24 for some reason. And baseline switches from summer to winter based on month, but the billing periods are parts of months... For now, I'm using a simplified model for this. \n", | |
"\n", | |
"I'm also ignoring the very strange California Climate Credit ([previously](https://ithoughthecamewithyou.com/post/california-climate-credit)) which is in some plans but not others. It's a one off discount and not usuage driven. " | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 27, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"import pandas as pd\n", | |
"import calendar\n", | |
"from datetime import datetime" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Plan Calculations\n", | |
"\n", | |
"Functions to calculate total dollars spent based on day of year, hour of day and an assumption for EV charging. " | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 136, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# assumption for daily charging - this will just be added at 4am each day as that's always going to be of peak\n", | |
"dailyEvKwh = 17.8\n", | |
"\n", | |
"\n", | |
"# E-TOU-C has a baseline discount, the months and billing periods are complex and so will simmplify to calendar months...\n", | |
"# These figure are for territory T with gas heating.\n", | |
"etoucSummerDailyBaseline = 6.5\n", | |
"etoucWinterDailyBaseline = 7.5\n", | |
"etoucCurrentMonth = 99\n", | |
"etoucRemainingBaseline = 0;\n", | |
"\n", | |
"def isTouHoliday(date):\n", | |
" # off peak weekends\n", | |
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n", | |
" if dateobj.weekday() in [5, 6]:\n", | |
" return True\n", | |
" \n", | |
" # off peak holidays \n", | |
" holidays = ['2024-01-01', '2024-02-19', '2024-03-11', '2024-05-01', '2024-05-27', '2024-07-04', '2024-09-02', '2024-11-01', '2024-11-04', '2024-11-11', '2024-11-28', '2024-12-25',\n", | |
" '2023-01-03', '2023-02-20', '2023-03-13', '2023-05-01', '2023-05-29', '2023-07-04', '2023-09-02', '2023-11-01', '2023-11-06', '2023-11-11', '2023-11-23', '2023-12-25']\n", | |
" return date in holidays\n", | |
"\n", | |
"def days_in_month(date):\n", | |
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n", | |
" # Get the number of days in the month\n", | |
" _, num_days = calendar.monthrange(dateobj.year, dateobj.month)\n", | |
" return num_days\n", | |
"\n", | |
"def evChargeKwh(hour):\n", | |
" startHour = int(hour.split(':')[0])\n", | |
" if (startHour == 4):\n", | |
" return dailyEvKwh\n", | |
" else:\n", | |
" return 0\n", | |
" \n", | |
"def priceLookupThreeBand(date, hour, startPeak, endPeak, startPartPeak, endPartPeak, startSummer, endSummer, summerPeak, summerPartPeak, summerOffPeak, winterPeak, winterPartPeak, winterOffPeak):\n", | |
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n", | |
" month = dateobj.month\n", | |
" startHour = int(hour.split(':')[0])\n", | |
"\n", | |
" price = 0\n", | |
"\n", | |
" if month in range(startSummer, endSummer):\n", | |
" if startHour in range(startPeak, endPeak):\n", | |
" price = summerPeak\n", | |
" elif startHour in range(startPartPeak, endPartPeak):\n", | |
" price = summerPartPeak\n", | |
" else:\n", | |
" price = summerOffPeak\n", | |
" else:\n", | |
" if startHour in range(startPeak, endPeak):\n", | |
" price = winterPeak\n", | |
" elif startHour in range(startPartPeak, endPartPeak):\n", | |
" price = winterPartPeak\n", | |
" else:\n", | |
" price = winterOffPeak\n", | |
"\n", | |
" return price\n", | |
"\n", | |
"def priceLookupTwoBand(date, hour, startPeak, endPeak, summerPeak, summerOffPeak, winterPeak, winterOffPeak, forceOffPeak):\n", | |
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n", | |
" month = dateobj.month\n", | |
" startHour = int(hour.split(':')[0])\n", | |
"\n", | |
" # summer is June-September, Winter is October-May\n", | |
" price = 0\n", | |
" if startHour not in range(startPeak, endPeak) or forceOffPeak:\n", | |
" if month in range(6, 9):\n", | |
" price = summerOffPeak\n", | |
" else:\n", | |
" price = winterOffPeak\n", | |
" else:\n", | |
" if month in range(6, 9):\n", | |
" price = summerPeak\n", | |
" else:\n", | |
" price = winterPeak\n", | |
"\n", | |
" return price\n", | |
"\n", | |
"def etoub(date, hour, kwh):\n", | |
" price = priceLookupTwoBand(date, hour, 16, 20, 0.56943, 0.44637, 0.4328, 0.394, False)\n", | |
" return (kwh + evChargeKwh(hour)) * price\n", | |
"\n", | |
"def etouc(date, hour, kwh):\n", | |
" global etoucCurrentMonth\n", | |
" global etoucRemainingBaseline\n", | |
"\n", | |
" # reset baseline each month\n", | |
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n", | |
" month = dateobj.month\n", | |
" if (month != etoucCurrentMonth):\n", | |
" # calculate days in month\n", | |
" daysInMonth = days_in_month(date)\n", | |
" if month in range(6, 9):\n", | |
" etoucRemainingBaseline = etoucSummerDailyBaseline * daysInMonth\n", | |
" else:\n", | |
" etoucRemainingBaseline = etoucWinterDailyBaseline * daysInMonth\n", | |
" etoucCurrentMonth = month\n", | |
"\n", | |
" # calculate discount\n", | |
" dicountedKwh = 0\n", | |
" if (kwh <= etoucRemainingBaseline):\n", | |
" dicountedKwh = kwh\n", | |
" etoucRemainingBaseline = etoucRemainingBaseline - kwh\n", | |
" else:\n", | |
" dicountedKwh = etoucRemainingBaseline\n", | |
" etoucRemainingBaseline = 0\n", | |
" discount = dicountedKwh * 0.09837\n", | |
"\n", | |
" price = priceLookupTwoBand(date, hour, 16, 20, 0.59342, 0.49042, 0.47926, 0.44926, False)\n", | |
" cost = (kwh + evChargeKwh(hour)) * price\n", | |
"\n", | |
" # less discount\n", | |
" cost = cost - discount\n", | |
"\n", | |
" # there is a daily meter fee on this plan, add this at 4am each day\n", | |
" startHour = int(hour.split(':')[0])\n", | |
" if (startHour == 4):\n", | |
" cost = cost + 0.25298\n", | |
"\n", | |
" return cost\n", | |
"\n", | |
"def etoud(date, hour, kwh):\n", | |
" forceOffPeak = isTouHoliday(date)\n", | |
" price = priceLookupTwoBand(date, hour, 17, 19, 0.55447, 0.41951, 0.46486, 0.42625, forceOffPeak)\n", | |
" return (kwh + evChargeKwh(hour)) * price\n", | |
"\n", | |
"def eva(date, hour, kwh):\n", | |
" price = 0\n", | |
" if isTouHoliday(date):\n", | |
" price = priceLookupThreeBand(date, hour, 15, 18, 15, 18, 5, 10, 0.69132, 0.44721, 0.33466, 0.50872, 0.37671, 0.30498)\n", | |
" else:\n", | |
" price = priceLookupThreeBand(date, hour, 14, 20, 7, 22, 5, 10, 0.69132, 0.44721, 0.33466, 0.50872, 0.37671, 0.30498)\n", | |
" return (kwh + evChargeKwh(hour)) * price\n", | |
"\n", | |
"def evb(date, hour, kwh):\n", | |
" price = 0\n", | |
" if isTouHoliday(date):\n", | |
" price = priceLookupThreeBand(date, hour, 15, 18, 15, 18, 5, 10, 0.68841, 0.4443, 0.33175, 0.50587, 0.37386, 0.30213)\n", | |
" else:\n", | |
" price = priceLookupThreeBand(date, hour, 14, 20, 7, 22, 5, 10, 0.68841, 0.4443, 0.33175, 0.50587, 0.37386, 0.30213)\n", | |
" cost = (kwh + evChargeKwh(hour)) * price\n", | |
" # there is a daily meter fee on this plan, add this at 4am each day\n", | |
" startHour = int(hour.split(':')[0])\n", | |
" if (startHour == 4):\n", | |
" cost = cost + 0.04928\n", | |
" return cost\n", | |
"\n", | |
"def ev2(date, hour, kwh):\n", | |
" price = priceLookupThreeBand(date, hour, 16, 21, 15, 23, 6, 9, 0.62402, 0.51353, 0.31151, 0.49691, 0.48021, 0.31151)\n", | |
" return (kwh + evChargeKwh(hour)) * price\n", | |
"\n", | |
"def eelec(date, hour, kwh):\n", | |
" price = priceLookupThreeBand(date, hour, 16, 21, 15, 23, 6, 9, 0.6028, 0.44092, 0.38424, 0.37129, 0.3492, 0.33534)\n", | |
" cost = (kwh + evChargeKwh(hour)) * price\n", | |
" # there is a daily meter fee on this plan, add this at 4am each day\n", | |
" startHour = int(hour.split(':')[0])\n", | |
" if (startHour == 4):\n", | |
" cost = cost + 0.49281\n", | |
" return cost" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"etoucCurrentMonth = 99 # reset to make sure we calculate the first month\n", | |
"\n", | |
"print(etoub('2024-09-01', '23:00', 0.67))\n", | |
"print(etouc('2024-09-01', '23:00', 0.67))\n", | |
"print(etoud('2024-09-01', '23:00', 0.67))\n", | |
"print(eva('2024-09-01', '23:00', 0.67))\n", | |
"print(evb('2024-09-01', '23:00', 0.67))\n", | |
"print(ev2('2024-09-01', '23:00', 0.67))\n", | |
"print(eelec('2024-09-01', '23:00', 0.67))" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Simulate Bill\n", | |
"\n", | |
"Use real data to simulate possible bills. This uses a PG&E expert for the 12 months before I started charging an EV. So the simulation should show how the average existing usage will change cost based on the various plan rules while adding an off-peak EV charge each day." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"df = pd.read_csv('pge1yr.csv', skiprows=6)\n", | |
"df.head()" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"etoucCurrentMonth = 99 # reset to make sure we calculate the first month\n", | |
"\n", | |
"# possible bills\n", | |
"etoubTotal = 0\n", | |
"etoucTotal = 0\n", | |
"etoudTotal = 0\n", | |
"evaTotal = 0\n", | |
"evbTotal = 0\n", | |
"ev2Total = 0\n", | |
"eelecTotal = 0\n", | |
"\n", | |
"# process historical data\n", | |
"for index, row in df.iterrows():\n", | |
" etoubTotal = etoubTotal + etoub(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n", | |
" etoucTotal = etoucTotal + etouc(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n", | |
" etoudTotal = etoudTotal + etoud(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n", | |
" evaTotal = evaTotal + eva(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n", | |
" evbTotal = evbTotal + evb(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n", | |
" ev2Total = ev2Total + ev2(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n", | |
" eelecTotal = eelecTotal + eelec(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n", | |
" \n", | |
"print(f\"E-TOU-B Total: ${etoubTotal:,.2f}\")\n", | |
"print(f\"E-TOU-C Total: ${etoucTotal:,.2f}\")\n", | |
"print(f\"E-TOU-D Total: ${etoudTotal:,.2f}\")\n", | |
"print(f\"EV Rate A Total: ${evaTotal:,.2f}\")\n", | |
"print(f\"EV Rate B Total: ${evbTotal:,.2f}\")\n", | |
"print(f\"EV-2 Total: ${ev2Total:,.2f}\")\n", | |
"print(f\"E-ELEC Total: ${eelecTotal:,.2f}\")" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": ".venv", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.12.5" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment