EUA analysis¶
In this notebook, we will show a use case of estimating the number of EUAs needed for a fleet for the current year.¶
We will choose our fleet as a list of vessels, get their historical voyage emissions using Signal Ocean's Vessel Emissions APIn and use linear extrapolation to estimate the needed EUAs at the end of the year.¶
Setup¶
Install the Signal Ocean SDK:
pip install signal-ocean
Set your subscription key acquired here: https://apis.signalocean.com/profile
In [ ]:
Copied!
pip install signal-ocean
pip install signal-ocean
Initialize Vessel Emissions API¶
In [1]:
Copied!
signal_ocean_api_key = '' #replace with your subscription key
from signal_ocean import Connection
from signal_ocean.vessel_emissions import VesselEmissionsAPI
connection = Connection(signal_ocean_api_key)
emissions_api = VesselEmissionsAPI(connection)
signal_ocean_api_key = '' #replace with your subscription key
from signal_ocean import Connection
from signal_ocean.vessel_emissions import VesselEmissionsAPI
connection = Connection(signal_ocean_api_key)
emissions_api = VesselEmissionsAPI(connection)
Import helpful modules¶
In [2]:
Copied!
from datetime import date, timedelta
from tqdm import tqdm
import pandas as pd
import datetime as dt
import numpy as np
from datetime import date, timedelta
from tqdm import tqdm
import pandas as pd
import datetime as dt
import numpy as np
Input Data¶
Choose a fleet as list of IMOs.¶
In [3]:
Copied!
vessel_imos = [ 9299111, 9314193, 9411197, 9425526, 9440473, 9459096, 9461659, 9462354, 9486922, 9486934, 9487469, 9487471, 9487483, 9505819, 9524982, 9543536, 9580405,
9580417, 9688336, 9688348, 9723007, 9726619, 9767340, 9773947, 9780251, 9783992, 9833723 ]
vessel_imos = [ 9299111, 9314193, 9411197, 9425526, 9440473, 9459096, 9461659, 9462354, 9486922, 9486934, 9487469, 9487471, 9487483, 9505819, 9524982, 9543536, 9580405,
9580417, 9688336, 9688348, 9723007, 9726619, 9767340, 9773947, 9780251, 9783992, 9833723 ]
Use the emissions api to get historical emissions data for the list of selected vessels, including EU emissions. We will store the data as pandas dataframe.¶
In [4]:
Copied!
emissions_list = []
for imo in tqdm(vessel_imos):
vessel_emissions = emissions_api.get_emissions_by_imo(imo, include_eu_emissions=True)
vessel_emissions = [emissions.to_dict() for emissions in vessel_emissions]
emissions_list.extend(vessel_emissions)
emissions_df = pd.json_normalize(emissions_list, sep='')
emissions_df.describe()
emissions_list = []
for imo in tqdm(vessel_imos):
vessel_emissions = emissions_api.get_emissions_by_imo(imo, include_eu_emissions=True)
vessel_emissions = [emissions.to_dict() for emissions in vessel_emissions]
emissions_list.extend(vessel_emissions)
emissions_df = pd.json_normalize(emissions_list, sep='')
emissions_df.describe()
100%|███████████████████████████████████████████| 27/27 [00:15<00:00, 1.78it/s]
Out[4]:
IMO | VoyageNumber | VesselTypeID | VesselClassID | Deadweight | Quantity | TransportWorkInMillionTonneMiles | TransportWorkInMillionDwtMiles | EmissionsVoyageCO2InTons | EmissionsVoyageCOInTons | ... | EuropeanUnionRegulatedEmissionsPortCallSOxInTons | EuropeanUnionRegulatedEmissionsPortCallPmInTons | EuropeanUnionRegulatedEmissionsStopCO2InTons | EuropeanUnionRegulatedEmissionsStopCOInTons | EuropeanUnionRegulatedEmissionsStopCh4InTons | EuropeanUnionRegulatedEmissionsStopN2OInTons | EuropeanUnionRegulatedEmissionsStopNMVOCInTons | EuropeanUnionRegulatedEmissionsStopNOxInTons | EuropeanUnionRegulatedEmissionsStopSOxInTons | EuropeanUnionRegulatedEmissionsStopPmInTons | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 2.929000e+03 | 2929.000000 | 2929.0 | 2929.000000 | 2929.000000 | 2929.000000 | 2929.000000 | 2929.000000 | 2929.000000 | 2929.000000 | ... | 572.000000 | 572.000000 | 626.000000 | 626.000000 | 626.000000 | 626.000000 | 626.000000 | 626.000000 | 626.000000 | 626.000000 |
mean | 9.562796e+06 | 126.784227 | 1.0 | 87.118129 | 75800.533971 | 56108.910891 | 163.953845 | 217.178833 | 1122.464010 | 0.988094 | ... | 0.102479 | 0.048364 | 72.304583 | 0.062734 | 0.001359 | 0.003442 | 0.069755 | 1.813836 | 0.113059 | 0.036320 |
std | 1.399511e+05 | 58.187661 | 0.0 | 1.004282 | 28939.074146 | 23301.761359 | 236.048850 | 306.676472 | 1062.966930 | 0.935962 | ... | 0.114249 | 0.030793 | 202.221402 | 0.174937 | 0.003789 | 0.009510 | 0.194515 | 5.099325 | 0.496942 | 0.095695 |
min | 9.299111e+06 | 1.000000 | 1.0 | 85.000000 | 37887.000000 | 24000.000000 | 0.118400 | 0.210267 | 192.858353 | 0.169537 | ... | 0.000880 | 0.006338 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
25% | 9.462354e+06 | 86.000000 | 1.0 | 86.000000 | 49998.000000 | 35000.000000 | 28.452220 | 40.057118 | 430.472571 | 0.378935 | ... | 0.029326 | 0.014085 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
50% | 9.505819e+06 | 123.000000 | 1.0 | 87.000000 | 74103.000000 | 55000.000000 | 58.804550 | 82.312392 | 688.415628 | 0.606231 | ... | 0.127079 | 0.061035 | 15.980352 | 0.013846 | 0.000300 | 0.000782 | 0.015396 | 0.422041 | 0.011419 | 0.009049 |
75% | 9.688348e+06 | 163.000000 | 1.0 | 88.000000 | 104649.000000 | 80000.000000 | 218.748960 | 289.765112 | 1422.189508 | 1.249819 | ... | 0.127079 | 0.061035 | 60.219080 | 0.052393 | 0.001135 | 0.002851 | 0.058257 | 1.508939 | 0.056936 | 0.029599 |
max | 9.833723e+06 | 277.000000 | 1.0 | 89.000000 | 149989.000000 | 148000.000000 | 3027.411400 | 3492.910834 | 10094.160506 | 8.861095 | ... | 2.443825 | 0.348220 | 2584.554489 | 2.233068 | 0.048370 | 0.120924 | 2.482978 | 70.337610 | 9.462714 | 1.366754 |
8 rows × 88 columns
We will convert the date columns (StartDate, EndDate) to datetime for easier manipulation. Since we want a yearly analysis, we will assign a "Year" label to each voyage, which we will get from the EndDate.¶
In [5]:
Copied!
emissions_df['StartDate'] = pd.to_datetime(emissions_df['StartDate'], format='ISO8601')
emissions_df['EndDate'] = pd.to_datetime(emissions_df['EndDate'], format='ISO8601')
emissions_df['Year'] = emissions_df['EndDate'].dt.year
emissions_df['StartDate'] = pd.to_datetime(emissions_df['StartDate'], format='ISO8601')
emissions_df['EndDate'] = pd.to_datetime(emissions_df['EndDate'], format='ISO8601')
emissions_df['Year'] = emissions_df['EndDate'].dt.year
To perform our analysis, we group our data per IMO and Year sum the values that we need, i.e. Total CO2 Emissions and EU - regulated CO2 emissions.¶
In [6]:
Copied!
imos = emissions_df.IMO.unique()
grouped_df = emissions_df.groupby(['IMO', 'Year']).agg(
VesselName=("VesselName", "first"),
VesselClass=("VesselClass", "first"),
Total_CO2_In_Tonnes=("EmissionsVoyageCO2InTons", "sum"),
EU_CO2_In_Tonnes=("EuropeanUnionRegulatedEmissionsVoyageCO2InTons", "sum"))#.reset_index()
imos = emissions_df.IMO.unique()
grouped_df = emissions_df.groupby(['IMO', 'Year']).agg(
VesselName=("VesselName", "first"),
VesselClass=("VesselClass", "first"),
Total_CO2_In_Tonnes=("EmissionsVoyageCO2InTons", "sum"),
EU_CO2_In_Tonnes=("EuropeanUnionRegulatedEmissionsVoyageCO2InTons", "sum"))#.reset_index()
Before we proceed, we will calculate the percentage of EU - regulated emissions per year, as well as the EUAs for the available data.¶
In [7]:
Copied!
grouped_df['EU_Emissions_Percentage'] = grouped_df['EU_CO2_In_Tonnes']/grouped_df['Total_CO2_In_Tonnes']
grouped_df['EUAs (40%)'] = grouped_df['EU_CO2_In_Tonnes'] * 0.4
grouped_df.head(5)
grouped_df['EU_Emissions_Percentage'] = grouped_df['EU_CO2_In_Tonnes']/grouped_df['Total_CO2_In_Tonnes']
grouped_df['EUAs (40%)'] = grouped_df['EU_CO2_In_Tonnes'] * 0.4
grouped_df.head(5)
Out[7]:
VesselName | VesselClass | Total_CO2_In_Tonnes | EU_CO2_In_Tonnes | EU_Emissions_Percentage | EUAs (40%) | ||
---|---|---|---|---|---|---|---|
IMO | Year | ||||||
9299111 | 2018 | Elka Apollon | Aframax | 24487.237390 | 5356.425477 | 0.218744 | 2142.570191 |
2019 | Elka Apollon | Aframax | 12882.690209 | 3804.605036 | 0.295327 | 1521.842015 | |
2020 | Elka Apollon | Aframax | 15466.783677 | 4384.380411 | 0.283471 | 1753.752165 | |
2021 | Elka Apollon | Aframax | 12755.502182 | 7164.812591 | 0.561704 | 2865.925037 | |
2022 | Elka Apollon | Aframax | 22431.696629 | 2893.248478 | 0.128980 | 1157.299391 |
Finally, we will calculate our estimations using extrapolations from the historical data. We calculate the year to date percentage for the date of the analysis, as well as the the average yearly percentage of EU emissions. Using these two we can estimate the total and EU emissions at the end of the current year, and the number of EUAs needed for the estimated emissions.¶
In [8]:
Copied!
current_year = 2024
year_to_date_percentage = (dt.datetime.now() - dt.datetime(dt.datetime.now().year,1,1)).days/365
grouped_df['Estimated_end_of_year_CO2'] = np.nan
grouped_df['Estimated_end_of_year_EU_CO2'] = np.nan
grouped_df['Estimated_end_of_year_EUAs'] = 0
for imo in imos:
average_co2_across_years = grouped_df['Total_CO2_In_Tonnes'][imo].mean()
average_eu_co2_across_years = grouped_df['EU_CO2_In_Tonnes'][imo].mean()
percentage_of_avg_eu_co2 = average_eu_co2_across_years/average_co2_across_years
projection_total = grouped_df['Total_CO2_In_Tonnes'][imo][current_year] / year_to_date_percentage
projection_eu = projection_total * percentage_of_avg_eu_co2
grouped_df.loc[(imo, 2024), 'Estimated_end_of_year_CO2'] = projection_total
grouped_df.loc[(imo, 2024), 'Estimated_end_of_year_EU_CO2'] = projection_eu
grouped_df.loc[(imo, 2024), 'Estimated_end_of_year_EUAs'] = round(projection_eu * 0.4)
current_year = 2024
year_to_date_percentage = (dt.datetime.now() - dt.datetime(dt.datetime.now().year,1,1)).days/365
grouped_df['Estimated_end_of_year_CO2'] = np.nan
grouped_df['Estimated_end_of_year_EU_CO2'] = np.nan
grouped_df['Estimated_end_of_year_EUAs'] = 0
for imo in imos:
average_co2_across_years = grouped_df['Total_CO2_In_Tonnes'][imo].mean()
average_eu_co2_across_years = grouped_df['EU_CO2_In_Tonnes'][imo].mean()
percentage_of_avg_eu_co2 = average_eu_co2_across_years/average_co2_across_years
projection_total = grouped_df['Total_CO2_In_Tonnes'][imo][current_year] / year_to_date_percentage
projection_eu = projection_total * percentage_of_avg_eu_co2
grouped_df.loc[(imo, 2024), 'Estimated_end_of_year_CO2'] = projection_total
grouped_df.loc[(imo, 2024), 'Estimated_end_of_year_EU_CO2'] = projection_eu
grouped_df.loc[(imo, 2024), 'Estimated_end_of_year_EUAs'] = round(projection_eu * 0.4)
In [9]:
Copied!
projected_emissions_for_current_year = grouped_df.loc[grouped_df.index.get_level_values(1)==current_year]
projected_emissions_for_current_year.head(10)
projected_emissions_for_current_year = grouped_df.loc[grouped_df.index.get_level_values(1)==current_year]
projected_emissions_for_current_year.head(10)
Out[9]:
VesselName | VesselClass | Total_CO2_In_Tonnes | EU_CO2_In_Tonnes | EU_Emissions_Percentage | EUAs (40%) | Estimated_end_of_year_CO2 | Estimated_end_of_year_EU_CO2 | Estimated_end_of_year_EUAs | ||
---|---|---|---|---|---|---|---|---|---|---|
IMO | Year | |||||||||
9299111 | 2024 | Elka Apollon | Aframax | 11856.572240 | 0.000000 | 0.000000 | 0.000000 | 17105.331492 | 3432.097933 | 1373 |
9314193 | 2024 | Dubai Gold | Panamax | 13900.358315 | 817.786967 | 0.058832 | 327.114787 | 20053.876621 | 1230.526045 | 492 |
9411197 | 2024 | Asahi Princess | Aframax | 18612.543976 | 2740.002717 | 0.147213 | 1096.001087 | 26852.089136 | 2321.580055 | 929 |
9425526 | 2024 | MP MR Tanker 2 | MR2 | 16446.783554 | 0.000000 | 0.000000 | 0.000000 | 23727.573112 | 1043.193350 | 417 |
9440473 | 2024 | Altesse | Panamax | 10742.458029 | 0.000000 | 0.000000 | 0.000000 | 15498.012571 | 1324.737525 | 530 |
9459096 | 2024 | Nave Orion | MR2 | 9165.163124 | 0.000000 | 0.000000 | 0.000000 | 13222.468538 | 187.828246 | 75 |
9461659 | 2024 | Hafnia Pegasus | MR2 | 14512.583470 | 0.000000 | 0.000000 | 0.000000 | 20937.126351 | 1185.199828 | 474 |
9462354 | 2024 | Koi | Panamax | 9862.682237 | 0.000000 | 0.000000 | 0.000000 | 14228.770816 | 0.000000 | 0 |
9486922 | 2024 | Alberta | Aframax | 11998.141262 | 2148.311226 | 0.179054 | 859.324490 | 17309.571385 | 1293.840240 | 518 |
9486934 | 2024 | Samos | Aframax | 15615.181956 | 0.000000 | 0.000000 | 0.000000 | 22527.831676 | 1909.014500 | 764 |
To get the total number of EUAs, we just sum the last column.¶
In [10]:
Copied!
sum(projected_emissions_for_current_year.Estimated_end_of_year_EUAs)
sum(projected_emissions_for_current_year.Estimated_end_of_year_EUAs)
Out[10]:
30229