This notebook expands upon the concepts used in the tutorial notebook and can be utilized for analysis of rounds in the Electricity Strategy Game. Any of the following code can be changed to create new or different visualizations, but as is, running through the cells will allow you to visualize and calculate the profit that your plants have generated in a given hour.
Note: This notebook does not currently take into account any loans, interest payments, or carbon credits, and is simply an evaluation of the individual hour you are examining.
First on our agenda is to import dependencies -- packages in Python that add to the basic functions in Python -- same as before.
from datascience import *
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import pandas as pd
plt.style.use('fivethirtyeight')
Next, let's select your portfolio. In the cell below, assign section to the section number that corresponds to your own according to the following table.
Number | Section Time |
---|---|
1 | Wednesday 8am |
2 | Friday 2pm |
3 | Wednesday 9am |
4 | Friday 4pm |
Assign YOUR_PORTFOLIO to the name of your portfolio from the following choices:
'Bay Views', 'Beachfront', 'Big Coal', 'Big Gas', 'East Bay', 'Fossil Light', 'Old Timers'
The variable, demand, should be set to the quantity demanded by the market for the given hour, and finally set hour to the hour (1-4) that you want to be examining. You can change these at any time, just be sure to rerun all cells after you make changes here.
section = 1
YOUR_PORTFOLIO = "East Bay"
period = 2
hour = 3
Next we import the demand realizations (for past rounds) and forecasts (for future rounds) and assign the current demand.
demand_table = Table.read_table('demand.csv')
demand = demand_table.where("round", period).where("hour", hour)["load"].item()
print(demand)
19500.0
Next we import our tables, one for each discussion section, and one with individualized information about our plants.
bids_0 = Table.read_table('MC_bids.csv').sort('PORTFOLIO')
bids_1 = Table.read_table('S1_bids_' + str(period) + '.csv').sort('PORTFOLIO')
bids_2 = Table.read_table('S2_bids_' + str(period) + '.csv').sort('PORTFOLIO')
bids_3 = Table.read_table('S3_bids_' + str(period) + '.csv').sort('PORTFOLIO')
bids_4 = Table.read_table('S4_bids_' + str(period) + '.csv').sort('PORTFOLIO')
ESG = Table.read_table('ESGPorfolios_forcsv.csv')
The table below should now display the names and bids that your team and others in your discussion section assigned for each individual plant.
bids = globals()['bids_' + str(section)]
if section > 0:
bids = bids.where("PERIOD", period)
bids.show()
TEAM | TEAM_ID | PORTFOLIO | PORTFOLIO_ID | PLANT | PLANT_ID | PERIOD | PRICE1 | PRICE2 | PRICE3 | PRICE4 |
---|---|---|---|---|---|---|---|---|---|---|
Debreu | 4 | Bay_Views | 3 | MORRO_BAY_1-2 | 31 | 2 | 40 | 38.78 | 38.78 | 38.78 |
Debreu | 4 | Bay_Views | 3 | MORRO_BAY_3-4 | 32 | 2 | 40 | 36.61 | 36.61 | 36.61 |
Debreu | 4 | Bay_Views | 3 | MOSS_LANDING_6 | 33 | 2 | 40 | 32.56 | 32.56 | 32.56 |
Debreu | 4 | Bay_Views | 3 | MOSS_LANDING_7 | 34 | 2 | 40 | 32.56 | 32.56 | 32.56 |
Debreu | 4 | Bay_Views | 3 | OAKLAND | 35 | 2 | 61.17 | 61.17 | 61.17 | 61.17 |
Arrow | 1 | Beachfront | 4 | COOLWATER | 41 | 2 | 42.39 | 42.39 | 42.39 | 42.39 |
Arrow | 1 | Beachfront | 4 | ETIWANDA_1-4 | 42 | 2 | 42.67 | 42.67 | 42.67 | 42.67 |
Arrow | 1 | Beachfront | 4 | ETIWANDA_5 | 43 | 2 | 62.89 | 62.89 | 62.89 | 62.89 |
Arrow | 1 | Beachfront | 4 | ELLWOOD | 44 | 2 | 75.61 | 75.61 | 75.61 | 75.61 |
Arrow | 1 | Beachfront | 4 | MANDALAY_1-2 | 45 | 2 | 39.06 | 39.06 | 39.06 | 39.06 |
Arrow | 1 | Beachfront | 4 | MANDALAY_3 | 46 | 2 | 52.06 | 52.06 | 52.06 | 52.06 |
Arrow | 1 | Beachfront | 4 | ORMOND_BEACH_1 | 47 | 2 | 38.06 | 38.06 | 38.06 | 38.06 |
Arrow | 1 | Beachfront | 4 | ORMOND_BEACH_2 | 48 | 2 | 38.06 | 38.06 | 38.06 | 38.06 |
Becker | 2 | Big_Coal | 1 | Four Corners | 11 | 2 | 40.93 | 42.38 | 58.27 | 40 |
Becker | 2 | Big_Coal | 1 | ALAMITOS_7 | 12 | 2 | 73.73 | 73.73 | 73.73 | 73.73 |
Becker | 2 | Big_Coal | 1 | HUNTINGTON_BEACH_1-2 | 13 | 2 | 40.93 | 42.39 | 58.28 | 42.38 |
Becker | 2 | Big_Coal | 1 | HUNTINGTON_BEACH_5 | 14 | 2 | 66.51 | 66.51 | 66.51 | 66.51 |
Becker | 2 | Big_Coal | 1 | REDONDO_5-6 | 15 | 2 | 41.95 | 42.38 | 59.71 | 42.38 |
Becker | 2 | Big_Coal | 1 | REDONDO_7-8 | 16 | 2 | 41.95 | 42.38 | 58.28 | 42.38 |
Coase | 3 | Big_Gas | 2 | EL_SEGUNDO_1-2 | 21 | 2 | 44.83 | 47.33 | 399.99 | 47.33 |
Coase | 3 | Big_Gas | 2 | EL_SEGUNDO_3-4 | 22 | 2 | 41.22 | 47.33 | 250 | 47.33 |
Coase | 3 | Big_Gas | 2 | LONG_BEACH | 23 | 2 | 52.5 | 52.93 | 449.99 | 52.93 |
Coase | 3 | Big_Gas | 2 | NORTH_ISLAND | 24 | 2 | 65.5 | 65.5 | 499.99 | 65.5 |
Coase | 3 | Big_Gas | 2 | ENCINA | 25 | 2 | 41.67 | 47.33 | 300 | 47.33 |
Coase | 3 | Big_Gas | 2 | KEARNY | 26 | 2 | 500 | 500 | 499.99 | 500 |
Coase | 3 | Big_Gas | 2 | SOUTH_BAY | 27 | 2 | 43.83 | 47.33 | 349.99 | 47.33 |
Friedman | 5 | East_Bay | 5 | PITTSBURGH_1-4 | 51 | 2 | 41 | 45 | 57 | 43 |
Friedman | 5 | East_Bay | 5 | PITTSBURGH_5-6 | 52 | 2 | 40.93 | 45 | 57 | 43 |
Friedman | 5 | East_Bay | 5 | PITTSBURG_7 | 53 | 2 | 60 | 60 | 60 | 60 |
Friedman | 5 | East_Bay | 5 | CONTRA_COSTA_4-5 | 54 | 2 | 58.29 | 58.29 | 58.28 | 58.29 |
Friedman | 5 | East_Bay | 5 | CONTRA_COSTA_6-7 | 55 | 2 | 40.93 | 44.5 | 56 | 42 |
Friedman | 5 | East_Bay | 5 | POTRERO_HILL | 56 | 2 | 69.9 | 69.9 | 69.9 | 69.9 |
Krugman | 7 | Low_Fossil | 7 | HUMBOLDT | 71 | 2 | 47.44 | 47.44 | 47.44 | 47.44 |
Krugman | 7 | Low_Fossil | 7 | HELMS | 72 | 2 | 0.5 | 0.5 | 0.5 | 0.5 |
Krugman | 7 | Low_Fossil | 7 | HUNTERS_POINT_1-2 | 73 | 2 | 49.17 | 49.17 | 49.17 | 49.17 |
Krugman | 7 | Low_Fossil | 7 | HUNTERS_POINT_4 | 74 | 2 | 75.89 | 75.89 | 75.89 | 75.89 |
Krugman | 7 | Low_Fossil | 7 | DIABLO_CANYON_1 | 75 | 2 | 11.5 | 11.5 | 11.5 | 11.5 |
Heckman | 6 | Old_Timers | 6 | BIG_CREEK | 61 | 2 | 0 | 0 | 0 | 0 |
Heckman | 6 | Old_Timers | 6 | MOHAVE_1 | 62 | 2 | 34.5 | 34.5 | 34.5 | 34.5 |
Heckman | 6 | Old_Timers | 6 | MOHAVE_2 | 63 | 2 | 34.5 | 34.5 | 34.5 | 34.5 |
Heckman | 6 | Old_Timers | 6 | HIGHGROVE | 64 | 2 | 52.06 | 52.06 | 80 | 52.06 |
Heckman | 6 | Old_Timers | 6 | SAN_BERNARDINO | 65 | 2 | 58.28 | 58.28 | 100 | 58.28 |
Recall the table ESG, which we used in the introductory notebook. The first few rows are shown below.
ESG.show(5)
Group | Group_num | UNIT NAME | Capacity_MW | Heat_Rate_MMBTUperMWh | Fuel_Price_USDperMMBTU | Fuel_Cost_USDperMWH | Var_OandM_USDperMWH | Total_Var_Cost_USDperMWH | Carbon_tonsperMWH | FixedCst_OandM_perDay | Plant_ID |
---|---|---|---|---|---|---|---|---|---|---|---|
Big Coal | 1 | FOUR CORNERS | 1900 | 11.67 | 3 | 35 | 1.5 | 36.5 | 1.1 | $8,000 | 11 |
Big Coal | 1 | ALAMITOS 7 | 250 | 16.05 | 4.5 | 72.22 | 1.5 | 73.72 | 0.85 | $0 | 12 |
Big Coal | 1 | HUNTINGTON BEACH 1&2 | 300 | 8.67 | 4.5 | 39 | 1.5 | 40.5 | 0.46 | $2,000 | 13 |
Big Coal | 1 | HUNTINGTON BEACH 5 | 150 | 14.44 | 4.5 | 65 | 1.5 | 66.5 | 0.77 | $2,000 | 14 |
Big Coal | 1 | REDONDO 5&6 | 350 | 8.99 | 4.5 | 40.44 | 1.5 | 41.94 | 0.48 | $3,000 | 15 |
... (37 rows omitted)
We will need both of these tables to generate an analysis of how our round went, so in the following cell we will join the tables based on the column Plant_ID:
sorted_joined_table = bids.join("PLANT_ID", ESG, "Plant_ID").sort("PRICE" + str(hour), descending = False)
sorted_joined_table.show(5)
PLANT_ID | TEAM | TEAM_ID | PORTFOLIO | PORTFOLIO_ID | PLANT | PERIOD | PRICE1 | PRICE2 | PRICE3 | PRICE4 | Group | Group_num | UNIT NAME | Capacity_MW | Heat_Rate_MMBTUperMWh | Fuel_Price_USDperMMBTU | Fuel_Cost_USDperMWH | Var_OandM_USDperMWH | Total_Var_Cost_USDperMWH | Carbon_tonsperMWH | FixedCst_OandM_perDay |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
61 | Heckman | 6 | Old_Timers | 6 | BIG_CREEK | 2 | 0 | 0 | 0 | 0 | Old Timers | 7 | BIG CREEK | 1000 | nan | 0 | 0 | 0 | 0 | 0 | $15,000 |
72 | Krugman | 7 | Low_Fossil | 7 | HELMS | 2 | 0.5 | 0.5 | 0.5 | 0.5 | Fossil Light | 8 | HELMS | 800 | nan | 0 | 0 | 0.5 | 0.5 | 0 | $15,000 |
75 | Krugman | 7 | Low_Fossil | 7 | DIABLO_CANYON_1 | 2 | 11.5 | 11.5 | 11.5 | 11.5 | Fossil Light | 8 | DIABLO CANYON 1 | 1000 | 1 | 7.5 | 7.5 | 4 | 11.5 | 0 | $20,000 |
33 | Debreu | 4 | Bay_Views | 3 | MOSS_LANDING_6 | 2 | 40 | 32.56 | 32.56 | 32.56 | Bay Views | 4 | MOSS LANDING 6 | 750 | 6.9 | 4.5 | 31.06 | 1.5 | 32.56 | 0.37 | $8,000 |
34 | Debreu | 4 | Bay_Views | 3 | MOSS_LANDING_7 | 2 | 40 | 32.56 | 32.56 | 32.56 | Bay Views | 4 | MOSS LANDING 7 | 750 | 6.9 | 4.5 | 31.06 | 1.5 | 32.56 | 0.37 | $8,000 |
... (37 rows omitted)
Great! Now we have a table that has our bids for this round as well as information about the capacity of our plants as well as their marginal cost of production. Now we can continue and make plots similar to those in the introductory notebook, that will allow us to examine how we performed in this given hour. Run the following series of cells to generate a legend as well as a plot of plant capacity versus plant bid price, with bids ordered from least to greatest.
def find_x_pos(widths):
cumulative_widths = [0]
cumulative_widths.extend(np.cumsum(widths))
half_widths = [i/2 for i in widths]
x_pos = []
for i in range(0, len(half_widths)):
x_pos.append(half_widths[i] + cumulative_widths[i])
return x_pos
width = sorted_joined_table.column("Capacity_MW")
width
height = sorted_joined_table.column('PRICE' + str(hour))
height
new_x = find_x_pos(width)
energy_colors_dict = {}
count = 0
colors = ['#EC5F67', '#F29056', '#F9C863', '#99C794', '#5FB3B3', '#6699CC', '#C594C5']
for i in set(sorted_joined_table['Group']):
energy_colors_dict[i] = colors[count]
count += 1
colors_mapped = list(pd.Series(sorted_joined_table['Group']).map(energy_colors_dict))
sorted_joined_table = sorted_joined_table.with_column('Color', colors_mapped)
plt.figure(figsize=(5,1))
plt.bar(energy_colors_dict.keys(), 1, color = energy_colors_dict.values())
plt.xticks(rotation=60)
plt.title('Legend')
plt.show()
# Make the plot
plt.figure(figsize=(9,6))
plt.bar(new_x, height, width=width, color=sorted_joined_table['Color'], edgecolor = "black")
#plt.xticks(y_pos, bars)
# Add title and axis names
plt.title('All Energy Sources')
plt.xlabel('Capacity_MW')
plt.ylabel('Variable Cost')
plt.show()
plt.figure(figsize=(5,1))
plt.bar(energy_colors_dict.keys(), 1, color = energy_colors_dict.values())
plt.xticks(rotation=60)
plt.title('Legend')
plt.show()
Now we can examine the plot above to determine our market price using the capacity demanded that we entered at the top of this notebook.
def price_calc(demand, sorted_table):
price = 0
sum_cap = 0
for i in range(0,len(sorted_table['Capacity_MW'])):
if sum_cap + sorted_table['Capacity_MW'][i] > demand:
price = sorted_table['PRICE' + str(hour)][i]
break
else:
sum_cap += sorted_table['Capacity_MW'][i]
price = sorted_table['PRICE' + str(hour)][i]
return price
def price_line_plot(price):
plt.axhline(y=price, color='r', linewidth = 2)
print("Price: " + str(price))
def demand_plot(demand):
plt.axvline(x=demand, color='r', linewidth = 2)
print("Capacity: " + str(demand))
# Make the plot
price = price_calc(demand, sorted_joined_table)
plt.figure(figsize=(9,6))
plt.bar(new_x, height, width=width, color=sorted_joined_table['Color'], edgecolor = "black")
plt.title('All Energy Sources')
plt.xlabel('Capacity_MW')
plt.ylabel('Variable Cost')
print('Hour: ' + str(hour))
price_line_plot(price)
demand_plot(demand)
plt.show()
plt.figure(figsize=(5,1))
plt.bar(energy_colors_dict.keys(), 1, color = energy_colors_dict.values())
plt.xticks(rotation=60)
plt.title('Legend')
plt.show()
Hour: 3 Price: 300.0 Capacity: 19500.0
Finally, we have a market price. Let us construct the same plot, but this time only considering the plants that are part of our portfolio.
your_source = sorted_joined_table.where("Group", YOUR_PORTFOLIO)
width_yours = your_source.column("Capacity_MW")
height_yours = your_source.column('PRICE' + str(hour))
height_yours_marginal_cost = your_source.column("Total_Var_Cost_USDperMWH")
new_x_yours = find_x_pos(width_yours)
label_yours = your_source.column("PLANT")
# Make the plot
plt.figure(figsize=(9,6))
plt.bar(new_x_yours, height_yours, width=width_yours, color = energy_colors_dict[YOUR_PORTFOLIO], edgecolor = "black")
plt.title(YOUR_PORTFOLIO)
plt.xlabel('Capacity_MW')
plt.ylabel('Price')
for new_x_i, height_i, label_i in zip(new_x_yours, height_yours, label_yours):
plt.text(new_x_i, height_i, label_i,
ha='center', va='bottom', fontsize=8)
price_line_plot(price)
plt.show()
Price: 300.0
Let's reduce our problem further to only include the plants that are actually operational under the new market price. Run the following cell to see the plants that will operate given our market price, and then we can begin the process of calculating our profit.
nonmarg_capacity = sum(sorted_joined_table.where('PRICE' + str(hour), are.below(price))["Capacity_MW"])
marg_capacity = sum(sorted_joined_table.where('PRICE' + str(hour), are.equal_to(price))["Capacity_MW"])
marg_demand = demand - nonmarg_capacity
marg_proportion = marg_demand / marg_capacity
marginal_plants = np.where(height_yours == price)[0]
width_yours2 = width_yours.copy()
width_yours2[marginal_plants] = width_yours2[marginal_plants] * marg_proportion
new_x_yours2 = find_x_pos(width_yours2)
plt.figure(figsize=(9,6))
num_x = len(your_source.where('PRICE' + str(hour), are.below_or_equal_to(price))[1])
plt.bar(new_x_yours2[:num_x], height_yours[:num_x], width=width_yours2[:num_x],
color = energy_colors_dict[YOUR_PORTFOLIO],
edgecolor = "black")
plt.title(YOUR_PORTFOLIO)
plt.xlabel('Capacity_MW')
plt.ylabel('Price')
for new_x_i, height_i, label_i in zip(new_x_yours2[:num_x], height_yours[:num_x], label_yours[:num_x]):
plt.text(new_x_i, height_i, label_i,
ha='center', va='bottom', fontsize=8)
price_line_plot(price)
plt.show()
Price: 300.0
The plot above allows us to see how the prices we have set correspond to the market price. However, in order to look at the profits that we are making, we should look at our marginal cost rather than our bid prices. We do this in the plot below, allowing us to compare the market price of energy in this hour to the marginal costs for running the plants that have a bid price that was below the market price.
plt.figure(figsize=(9,6))
num_x = len(your_source.where('PRICE' + str(hour), are.below_or_equal_to(price))[1])
plt.bar(new_x_yours2[:num_x], height_yours_marginal_cost[:num_x], width=width_yours2[:num_x],
color = energy_colors_dict[YOUR_PORTFOLIO],
edgecolor = "black")
plt.title(YOUR_PORTFOLIO)
plt.xlabel('Capacity_MW')
plt.ylabel('Marginal Cost')
for new_x_i, height_i, label_i in zip(new_x_yours2[:num_x], height_yours_marginal_cost[:num_x], label_yours[:num_x]):
plt.text(new_x_i, height_i, label_i,
ha='center', va='bottom', fontsize=8)
price_line_plot(price)
plt.show()
Price: 300.0
Now we can finally calculate profit. The graphic above should show the market price line along with the marginal costs for operating our plants. Thus we can calculate profit by calculating the area between the red line and the blue boxes, as that will give us the total revenue - total cost. The function defined below is designed to perform that calculation.
def profit(sorted_table, price):
capacity_subset = sum(sorted_table.where('PRICE' + str(hour), are.below(price))["Capacity_MW"])
capacity_subset += sum(sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Capacity_MW"] * marg_proportion)
revenue = capacity_subset * price
cost = 0
for i in range(len(sorted_table.where('PRICE' + str(hour), are.below(price))["Total_Var_Cost_USDperMWH"])):
cost += sorted_table.where('PRICE' + str(hour), are.below(price))["Total_Var_Cost_USDperMWH"][i]\
* sorted_table.where('PRICE' + str(hour), are.below(price))["Capacity_MW"][i]
for i in range(len(sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Total_Var_Cost_USDperMWH"])):
cost += sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Total_Var_Cost_USDperMWH"][i]\
* (sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Capacity_MW"][i] * marg_proportion)
return revenue - cost
def profit_pab(sorted_table, price):
revenue = 0
for i in range(len(sorted_table.where('PRICE' + str(hour), are.below(price))['PRICE' + str(hour)])):
revenue += sorted_table.where('PRICE' + str(hour), are.below(price))['PRICE' + str(hour)][i]\
* sorted_table.where('PRICE' + str(hour), are.below(price))["Capacity_MW"][i]
for i in range(len(sorted_table.where('PRICE' + str(hour), are.equal_to(price))['PRICE' + str(hour)])):
revenue += sorted_table.where('PRICE' + str(hour), are.equal_to(price))['PRICE' + str(hour)][i]\
* (sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Capacity_MW"][i] * marg_proportion)
cost = 0
for i in range(len(sorted_table.where('PRICE' + str(hour), are.below(price))["Total_Var_Cost_USDperMWH"])):
cost += sorted_table.where('PRICE' + str(hour), are.below(price))["Total_Var_Cost_USDperMWH"][i]\
* sorted_table.where('PRICE' + str(hour), are.below(price))["Capacity_MW"][i]
for i in range(len(sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Total_Var_Cost_USDperMWH"])):
cost += sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Total_Var_Cost_USDperMWH"][i]\
* (sorted_table.where('PRICE' + str(hour), are.equal_to(price))["Capacity_MW"][i] * marg_proportion)
return revenue - cost
profit(your_source, price)
760922.0
We now have the ability to estimate the amount of profit our plants will generate based on a given amount of demand! Again, this does not include considerations of credit or carbon emmissions, but it should give you a general sens of how your plants performed for this hour. Try going back to the start of the notebook and changing the hour you are examining, or add more cells to do a deeper analysis of your performance on your own to make better decisions for your team in the future.
Congratulations! You have completed your Jupyter Notebook for the ESG. If you have questions, please do not hesitate to post them on the dedicated Piazza thread (https://piazza.com/class/jr2lknh4q311x1?cid=41).
Module Developers: Alec Kan, Alma Pineda, Aarish Irfan, Elaine Chien, and Octavian Sima.
Data Science Modules: http://data.berkeley.edu/education/modules