Global price of carbon emissions and assessment of comparative carbon prices (Figure 2.26)¶
Notebook sr15_2.5_carbon_price_analysis¶
This notebook is based on the Release 1.1 of the IAMC 1.5C Scenario Explorer and Data and refers to the published version of the IPCC Special Report on Global Warming of 1.5C (SR15).
The notebook is run with pyam release 0.5.0.
The source code of this notebook is available on GitHub (release 2.0.2).
IPCC SR15 scenario assessment¶
Assessment of carbon prices by warming category¶
This notebook contains the carbon price assessment of the scenario ensemble
in Section 2.5.2 and for Figure 2.26 of the IPCC's "Special Report on Global Warming of 1.5°C".
The scenario data used in this analysis can be accessed and downloaded at https://data.ene.iiasa.ac.at/iamc-1.5c-explorer.
Load pyam
package and other dependencies¶
import pandas as pd
import numpy as np
import io
import yaml
import math
import matplotlib.pyplot as plt
plt.style.use('style_sr15.mplstyle')
%matplotlib inline
import pyam
from utils import boxplot_by_cat
Import scenario data, categorization and specifications files¶
The metadata file with scenario categorisation and quantitative indicators can be downloaded at https://data.ene.iiasa.ac.at/iamc-1.5c-explorer.
Alternatively, it can be re-created using the notebook sr15_2.0_categories_indicators
.
The last cell of this section loads and assigns a number of auxiliary lists as defined in the categorization notebook.
sr1p5 = pyam.IamDataFrame(data='../data/iamc15_scenario_data_world_r2.0.xlsx')
sr1p5.load_meta('sr15_metadata_indicators.xlsx')
with open("sr15_specs.yaml", 'r') as stream:
specs = yaml.load(stream, Loader=yaml.FullLoader)
rc = pyam.run_control()
for item in specs.pop('run_control').items():
rc.update({item[0]: item[1]})
cats = specs.pop('cats')
marker= specs.pop('marker')
Downselect scenario ensemble to categories of interest for this assessment¶
Only scenarios that limit warming to at most 2°C at the end of the century are included in this assessment.
cats.remove('Above 2C')
Set specifications for figures and statistics¶
First, set the list of years included in the plots. Then, define an auxiliary dictionary and function for easier display.
years = [2030, 2050, 2070, 2100]
filter_args = dict(df=sr1p5, category=cats, marker=None, join_meta=True)
def plotting_args():
return {'categories': cats, 'column': 'category',
'add_marker': marker, 'ar5_format': True}
Rename aggregate indicator from metadata¶
This simplifies including the aggregate indicator 'Annual compounded Net Present Value' in the plots and assessment below.
sr1p5.meta.rename(columns={'carbon price|AC NPV (2030-2100)': 'ac_npv'}, inplace=True)
Check for misreported carbon prices and exclude these from analysis and figues¶
In some cases, models were not able to report carbon prices throughout the century due to the scenario protocol and policy implementation. Carbon prices below 5 USD/tCO2 in 2030 or reported as nan
were, after consultation with the modeling teams, assessed to be misreported entries. For consistency, these scenarios are excluded throughout this notebook.
df = sr1p5.filter(category=cats, variable='Price|Carbon', year=range(2030, 2101, 10))
misc = df.validate({'Price|Carbon': {'lo': 5, 'year': 2030}},
exclude_on_fail=True).set_index(pyam.META_IDX)
Check for nan
entries in the carbon price timeseries and remove scenarios.
carbon_price = df.timeseries()
carbon_price.index = carbon_price.index.droplevel([2, 3, 4])
has_na = carbon_price.apply(lambda x: np.isnan(max(x)), axis=1)
na = carbon_price[has_na]
df.set_meta(meta=True, name='exclude', index=na.index)
Show all carbon-price trajectories excluded from the analysis and export to xlsx
for archiving¶
all_excluded = carbon_price.loc[misc.index].append(na)
all_excluded
sr1p5.set_meta(True, 'exclude', all_excluded.index)
df.filter(exclude=False, inplace=True)
Compute net-present value carbon price timeseries¶
Reload carbon-price timeseries after excluding non-valid timeseries data.
carbon_price = df.timeseries()
carbon_price.index = carbon_price.index.droplevel([2, 3, 4])
carbon_price_npv = carbon_price.copy()
r = 0.05
baseyear = 2020
for y in carbon_price_npv.columns:
carbon_price_npv[y] = carbon_price_npv[y] / math.pow(1 + r, y - baseyear)
Statistical assessment of the carbon price development¶
The following assessment is the basis of Section 2.5.2.1.
stats = pyam.Statistics(df=sr1p5, groupby={'category': cats})
stats.add(carbon_price, header='Carbon price')
summary = stats.summarize()
summary
summary.to_excel('output/sec_2.5_carbon_price_summary_statistics.xlsx')
Descriptive statistics of the 'Higher 2C' pathways¶
stats.summarize().loc[('category', 'Higher 2C')]
Descriptive statistics of the 'Below 1.5C' pathways¶
stats.summarize().loc[('category', 'Below 1.5C')]
Plot the average carbon price by category¶
This section produces panel b of Figure 2.26.
filtered_npv = pyam.filter_by_meta(carbon_price_npv, df, category=None, marker=None, ac_npv=None, join_meta=True)
price_threshold = 690
cols = ['ac_npv']
boxplot_by_cat(filtered_npv, **plotting_args(), years=cols,
xlabel='Annual compounded net-present-value carbon price from 2030 until 2100',
ylabel='Price of carbon (USD/tCO2)',
ymax=price_threshold, ymin=0, save='output/fig2.26b_carbon_price_npv.png')
Summary of outliers in the figure¶
Display the timeseries data of scenarios exceeding the upper threshold of the figure (the number of data points exceeding the threshold per category is marked above the panel).
above_threshold = filtered_npv[cols].apply(lambda x: max(x) > price_threshold, axis=1)
filtered_npv[above_threshold]
Plot price trajectory by category over time using a log scale¶
This section produces panel a of Figure 2.26.
filtered_data = pyam.filter_by_meta(carbon_price, df, category=None, marker=None, join_meta=True)
price_threshold = 15000
hlines = (
[i for i in range(10, 91, 10)]
+ [i for i in range(100, 901, 100)]
+ [i for i in range(1000, 10001, 1000)]
)
boxplot_by_cat(filtered_data, **plotting_args(), ylabel='Price of carbon (USD/tCO2)',
log_scale=True, years=years, ymax=price_threshold,
hlines=hlines, legend=False, save='output/fig2.26a_carbon_price_over_time.png')
Summary of outliers in the figure¶
Display the timeseries data of scenarios exceeding the upper threshold of the figure (the number of data points exceeding the threshold per category is marked above the panel).
above_threshold = filtered_data[years].apply(lambda x: max(x) > price_threshold, axis=1)
filtered_data[above_threshold]
Export timeseries data to xlsx
¶
writer = pd.ExcelWriter('output/sec2.5_carbon_price_timeseries.xlsx')
pyam.utils.write_sheet(writer, 'real', pyam.filter_by_meta(carbon_price, **filter_args),
index=True)
pyam.utils.write_sheet(writer, 'npv', pyam.filter_by_meta(carbon_price_npv, **filter_args),
index=True)
writer.save()
Assessment of relative difference between 1.5°C and 2°C scenarios¶
The following assessment is the basis of Section 2.5.2.1.
We first define a mapping between corresponding pairs of scenarios, then assign this mapping to a number of dictionaries.
mapping = [
('SSP1-19', 'SSP1-26', 'SSP1'),
('SSP2-19', 'SSP2-26', 'SSP2'),
('SSP5-19', 'SSP5-26', 'SSP5'),
('ADVANCE_2020_1.5C-2100', 'ADVANCE_2020_WB2C', 'ADVANCE_2020'),
('CD-LINKS_NPi2020_400', 'CD-LINKS_NPi2020_1000', 'CD-LINKS_NPi2020'),
('DAC15_50', 'DAC2_66', 'DAC'),
('EMF33_1.5C_full', 'EMF33_WB2C_full', 'EMF33_full'),
('EMF33_1.5C_limbio', 'EMF33_WB2C_limbio', 'EMF33_limbio'),
('EMF33_1.5C_nofuel', 'EMF33_WB2C_nofuel', 'EMF33_nofuel'),
('PEP_1p5C_full_eff', 'PEP_2C_full_eff', 'PEP_full_eff'),
('PEP_1p5C_full_goodpractice', 'PEP_2C_full_goodpractice', 'PEP_full_goodpractice'),
('PEP_1p5C_full_NDC', 'PEP_2C_full_NDC', 'PEP_full_NDC'),
('PEP_1p5C_full_netzero', 'PEP_2C_full_netzero', 'PEP_full_netzero'),
('PEP_1p5C_red_eff', 'PEP_2C_red_eff', 'PEP_red_eff'),
('SMP_1p5C_Def', 'SMP_2C_Def', 'SMP_Def'),
('SMP_1p5C_early', 'SMP_2C_early', 'SMP_early'),
('SMP_1p5C_lifesty', 'SMP_2C_lifesty', 'SMP_lifesty'),
('SMP_1p5C_regul', 'SMP_2C_regul', 'SMP_regul'),
('SMP_1p5C_Sust', 'SMP_2C_Sust', 'SMP_Sust'),
('SFCM_SSP2_Bio_1p5Degree', 'SFCM_SSP2_Bio_2Degree', 'SFCM_SSP2_Bio'),
('SFCM_SSP2_combined_1p5Degree', 'SFCM_SSP2_combined_2Degree', 'SFCM_SSP2_combined'),
('SFCM_SSP2_EEEI_1p5Degree', 'SFCM_SSP2_EEEI_2Degree', 'SFCM_SSP2_EEEI'),
('SFCM_SSP2_LifeStyle_1p5Degree', 'SFCM_SSP2_LifeStyle_2Degree', 'SFCM_SSP2_LifeStyle'),
('SFCM_SSP2_Ref_1p5Degree', 'SFCM_SSP2_Ref_2Degree', 'SFCM_SSP2_Ref'),
('SFCM_SSP2_ST_bio_1p5Degree', 'SFCM_SSP2_ST_bio_2Degree', 'SFCM_SSP2_ST_bio'),
('SFCM_SSP2_ST_CCS_1p5Degree', 'SFCM_SSP2_ST_CCS_2Degree', 'SFCM_SSP2_ST_CCS'),
('SFCM_SSP2_ST_nuclear_1p5Degree', 'SFCM_SSP2_ST_nuclear_2Degree', 'SFCM_SSP2_ST_nuclear'),
('SFCM_SSP2_ST_solar_1p5Degree', 'SFCM_SSP2_ST_solar_2Degree', 'SFCM_SSP2_ST_solar'),
('SFCM_SSP2_ST_wind_1p5Degree', 'SFCM_SSP2_ST_wind_2Degree', 'SFCM_SSP2_ST_wind'),
('SFCM_SSP2_SupTech_1p5Degree', 'SFCM_SSP2_SupTech_2Degree', 'SFCM_SSP2_SupTech'),
('TERL_15D_LowCarbonTransportPolicy', 'TERL_2D_LowCarbonTransportPolicy', 'TERL_LowCarbonTransportPolicy'),
('TERL_15D_NoTransportPolicy', 'TERL_2D_NoTransportPolicy', 'TERL_NoTransportPolicy')
]
rename_1p5 = {}
rename_1p5_reverse = {}
rename_2 = {}
rename_2_reverse = {}
for (scen_1p5, scen_2, scen) in mapping:
rename_1p5[scen_1p5] = scen
rename_1p5_reverse[scen] = scen_1p5
rename_2[scen_2] = scen
rename_2_reverse[scen] = scen_2
def apply_rename_mapping(data, rename):
return data.loc[(slice(None), rename), :].copy().rename(rename, level=1)
carbon_price_all = df.filter(variable='Price|Carbon', exclude=False,
year=range(2030, 2101, 10)).timeseries()
carbon_price_all = pyam.filter_by_meta(carbon_price_all, sr1p5, ac_npv=None, join_meta=True)
carbon_price_1p5 = apply_rename_mapping(carbon_price_all, rename_1p5)
carbon_price_2 = apply_rename_mapping(carbon_price_all, rename_2)
Show pairs where only one of the corresponding scenario is available¶
The reason for the corresponding scenario not being available could be one of the following:
- not reported by modeling team
- excluded due misreported carbon prices (see section above)
- warming outcome not in line with comparison of 1.5°C and 2°C scenarios
pd.DataFrame(index=carbon_price_1p5.index.difference(carbon_price_2.index))
pd.DataFrame(index=carbon_price_2.index.difference(carbon_price_1p5.index))
Compute the relative difference per scenario pair over time¶
carbon_price_rel = carbon_price_1p5 / carbon_price_2
Remove scenarios where the relative difference is not defined
carbon_price_rel = carbon_price_rel[~np.isnan(carbon_price_rel[2030])]
Define an auxiliary function to generate descriptive statistics
def describe_by_cat(data, category=None):
return (
apply_rename_mapping(
pyam.filter_by_meta(apply_rename_mapping(data, rename_1p5_reverse),
sr1p5, category=category),
rename_1p5)
.describe()
.reindex(index=['count', 'mean', '25%', '75%'])
)
describe_by_cat(carbon_price_rel, ['Below 1.5C', '1.5C low overshoot', '1.5C high overshoot'])
Show the descriptive statistics across scenario pairs excluding high overshoot scebaruis¶
describe_by_cat(carbon_price_rel, ['Below 1.5C', '1.5C low overshoot'])
Descriptive statistics only for scenarios submitted from model intercomparison projects¶
projects = ['ADVANCE*', 'SSP*', 'CD-LINKS*', 'EMF33*']
carbon_price_rel_mip = (
carbon_price_rel[pyam.pattern_match(carbon_price_rel.reset_index().scenario, projects).values]
)
Descriptive statistics across all scenario pairs¶
describe_by_cat(carbon_price_rel_mip, ['Below 1.5C', '1.5C low overshoot', '1.5C high overshoot'])
Only scenarios that remain below 1.5°C or exhibit low overshoot¶
describe_by_cat(carbon_price_rel_mip, ['Below 1.5C', '1.5C low overshoot'])
Export data for relative carbon price assessment to xlsx
¶
def add_cats(data, df, col_suffix, mapping, mapping_reverse):
ret = apply_rename_mapping(data, mapping_reverse)
ret['scenario_{}'.format(col_suffix)] = ret.reset_index().scenario.values
ret = pyam.filter_by_meta(ret, df, category=None, join_meta=True)
ret.rename(columns={'category': 'subcategory_{}'.format(col_suffix)}, inplace=True)
return apply_rename_mapping(ret, mapping)
carbon_price_rel = add_cats(carbon_price_rel, df, '1.5', rename_1p5, rename_1p5_reverse)
carbon_price_rel = add_cats(carbon_price_rel, df, '2', rename_2, rename_2_reverse)
carbon_price_rel.to_excel('output/sec2.5_relative_carbon_prices.xlsx')