@@ -171,6 +171,37 @@ def _total_emissions_process(start_time, end_time, cache, filter_regions=None,
171
171
return result
172
172
173
173
174
+ def _adjust_ef_with_mlf (co2_factors_df , cache ):
175
+ """ Divides emissions factors by MLF for each plant """
176
+ # Get MLF data for DUIDs
177
+ duid_mlfs = download_dudetailsummary (cache , include_mlfs = True )
178
+ duid_mlfs ['FILE_DATE' ] = pd .to_datetime (duid_mlfs ['START_DATE' ], format = "%Y-%m-%d %H:%M:%S" )
179
+
180
+ df = co2_factors_df .copy ()
181
+ df .insert (0 ,'FILE_DATE' , pd .to_datetime (df .file_year .astype (str ) \
182
+ + '/' + df .file_month .astype (str ) + '/01' , \
183
+ format = "%Y-%m-%d" ))
184
+
185
+ df .sort_values (['FILE_DATE' , 'DUID' ], inplace = True )
186
+ duid_mlfs .sort_values (['FILE_DATE' , 'DUID' ], inplace = True )
187
+ adj_factors = pd .merge_asof (left = df ,
188
+ right = duid_mlfs ,
189
+ on = ['FILE_DATE' ],
190
+ by = ['DUID' ],
191
+ direction = 'backward' )
192
+
193
+ # Save raw EF data
194
+ adj_factors ['unadjusted_CO2E_EF' ] = adj_factors ['CO2E_EMISSIONS_FACTOR' ]
195
+
196
+ # Keep emissions factors where mlf is not given, else adjust by dividing by MLF
197
+ adj_factors ['CO2E_EMISSIONS_FACTOR' ] = np .where (adj_factors ['TRANSMISSIONLOSSFACTOR' ].isna (), \
198
+ adj_factors ['CO2E_EMISSIONS_FACTOR' ], \
199
+ adj_factors ['CO2E_EMISSIONS_FACTOR' ] / adj_factors ['TRANSMISSIONLOSSFACTOR' ])
200
+
201
+ return adj_factors [['file_year' , 'file_month' , 'DUID' , 'CO2E_EMISSIONS_FACTOR' , \
202
+ 'CO2E_ENERGY_SOURCE' , 'CO2E_DATA_SOURCE' , 'unadjusted_CO2E_EF' ]]
203
+
204
+
174
205
def _get_duid_emissions_intensities (start_time , end_time , cache ):
175
206
"""Merges emissions factors from GENSETID to DUID and cleans data"""
176
207
co2factors_df = download_plant_emissions_factors (start_time , end_time , cache )
@@ -261,9 +292,9 @@ def _calculate_sent_out(energy_df):
261
292
262
293
def get_marginal_emitter (start_time , end_time , cache ):
263
294
"""Retrieves the marginal emissions intensity for each dispatch interval and region. This factor being the weighted
264
- sum of the generators contributing to price-setting. Although not necessarily common, there may be times where
265
- multiple technology types contribute to the marginal emissions - note however that the 'DUID' and 'CO2E_ENERGY_SOURCE'
266
- returned will reflect only the plant which makes the greatest contribution towards price-setting .
295
+ sum of the generators contributing to price-setting. Note also the emissions factors for each plant are adjusted by
296
+ their corresponding MLF given that the weighted contribution of a generator to price setting is indicative of the
297
+ generator's MW contribution as seen at the RRN, not generator site .
267
298
268
299
Parameters
269
300
----------
@@ -283,9 +314,8 @@ def get_marginal_emitter(start_time, end_time, cache):
283
314
Columns: Type: Description:
284
315
Time datetime Timestamp reported as end of dispatch interval.
285
316
Region str The NEM region corresponding to the marginal emitter data.
286
- Intensity_Index float The intensity index [tCO2e/MWh] (as by weighted contributions) of the price-setting generators.
287
- DUID str Unit identifier of the generator with the largest contribution on the margin for that Time-Region.
288
- CO2E_ENERGY_SOURCE str Unit energy source with the largest contribution on the margin for that Time-Region.
317
+ Intensity_Index float The intensity index [tCO2e/MWh] (as by weighted contributions and adjusted for MLFs) of the price-setting generators.
318
+ Energy source str Combined string of energy sources which are the marginal generators for that Time-Region.
289
319
================== ======== ==================================================================================================
290
320
291
321
"""
@@ -296,6 +326,11 @@ def get_marginal_emitter(start_time, end_time, cache):
296
326
## gen_info = download_generators_info(cache)
297
327
logger .warning ('Warning: Gen_info table only has most recent NEM registration and exemption list. Does not account for retired generators' )
298
328
co2_factors = _get_duid_emissions_intensities (start_time , end_time , cache )
329
+
330
+ # Adjust EF by MLF for each plant
331
+ co2_factors = _adjust_ef_with_mlf (co2_factors , cache )
332
+
333
+ # Get Price Setter data (marginal generators)
299
334
price_setters = download_pricesetter_files (start_time , end_time , cache )
300
335
301
336
# Drop Basslink
@@ -313,14 +348,38 @@ def get_marginal_emitter(start_time, end_time, cache):
313
348
# Weigh CO2 intensity by 'Increase' contributions
314
349
filt_df ['weighted_co2_factor' ] = filt_df ['Increase' ] * filt_df ['CO2E_EMISSIONS_FACTOR' ]
315
350
316
- # Aggregate sum of weighted CO2 intensities
317
- values = filt_df .groupby (by = ['Time' ,'Region' ], axis = 0 ).sum ()
318
- values = values .reset_index ()[['Time' ,'Region' ,'weighted_co2_factor' ]]
319
- values .rename (columns = {'weighted_co2_factor' : 'Intensity_Index' }, inplace = True )
320
-
321
- # Identify Emissions tech/DUID with the largest contribution (increase) value for Time-Region
322
- source = filt_df .sort_values (['Increase' ]).drop_duplicates (['Time' ,'Region' ], keep = "last" )[['Time' , 'Region' , 'DUID' , 'CO2E_ENERGY_SOURCE' ]]
323
- result = values .merge (source , on = ['Time' ,'Region' ], how = 'left' )
351
+ # Count the number of price setters per Time-Region
352
+ filt_df = filt_df .merge (right = filt_df .groupby (["Time" , "Region" ],as_index = False ).size (),
353
+ how = 'left' ,
354
+ on = ["Time" , "Region" ])
355
+
356
+ # Filter out price setter increase factors less than 0.05MW, and Hydro 0 tCO2/e instances where the Hydro generator is not the main contributor to price setting
357
+ filt_df_adj = filt_df [filt_df ['Increase' ]> 0.05 ]
358
+ filt_df_adj = filt_df_adj [~ ((filt_df_adj .duplicated (['Time' ,'Region' ])) & \
359
+ (filt_df_adj ['weighted_co2_factor' ]== 0 ) & \
360
+ (filt_df_adj ['CO2E_ENERGY_SOURCE' ].isin (['Hydro' , 'Battery Storage' ])) & \
361
+ (filt_df_adj ['Increase' ] < (1 / filt_df_adj ['size' ])))]
362
+
363
+ # Aggregate CO2 intensities by weighted sum
364
+ values = filt_df_adj .groupby (by = ['Time' ,'Region' ], axis = 0 ).sum ()
365
+ values .insert (0 ,'Intensity_Index' , values ['weighted_co2_factor' ] / values ['Increase' ])
366
+ values = values .reset_index ()[['Time' , 'Region' , 'Intensity_Index' ]]
367
+
368
+ # Collate all energy source technologies per Time-Region
369
+ source = filt_df_adj [["Time" , "Region" , "CO2E_ENERGY_SOURCE" ]]
370
+ for tech in source ["CO2E_ENERGY_SOURCE" ].unique ():
371
+ source .insert (3 , tech , np .where (source ['CO2E_ENERGY_SOURCE' ]== tech , 1 , np .nan ))
372
+ source_count = source .groupby (by = ['Time' ,'Region' ], axis = 0 ).sum ()
373
+
374
+ # Concatenate string of all tech types contributing to marginal emissions
375
+ for tech in source_count .columns :
376
+ source_count .loc [:, tech ] = np .where (source_count [tech ]> 0 , tech + " + " , "" )
377
+ source_count .reset_index (inplace = True )
378
+ source_count ["Energy Source" ] = source_count [source_count .columns [~ source_count .columns .isin (['Time' ,'Region' ])]].agg ('' .join , axis = 1 )
379
+ source_count ["Energy Source" ] = source_count ["Energy Source" ].str .rstrip (' + ' )
380
+
381
+ # Merge to marginal data
382
+ result = values .merge (source_count [["Time" , "Region" , "Energy Source" ]], on = ["Time" , "Region" ], how = "left" )
324
383
325
384
return result
326
385
0 commit comments