Skip to content

Instantly share code, notes, and snippets.

@abfo
Last active September 22, 2024 21:29
Show Gist options
  • Save abfo/b89e208f837fc3b9095d3a6f68fd8d52 to your computer and use it in GitHub Desktop.
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
Display the source blob
Display the rendered blob
Raw
{
"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