With the beginning of Q2 2020 the global economy finds itself adapting to an unprecedented situation brought on by the Covid-19 pandemic. Oil demand and supply has already been severely impacted across the world. In this notebook, we will see how Vortexa's Python SDK can help analysts, traders and data scientists identify the earliest flow trends related to China, in its capacity as a major crude importer and clean products exporter.
For the purposes of this analysis we will take advantage of the SDK's CargoTimeSeries endpoint. This endpoint allows SDK users to find aggregate flows between regions and provinces, for various products, vessels or commercial entities such as charterers and vessel owners. To see more details about Endpoints check our Docs page.
PS1: This notebook was generated on 15 April 2020. Vortexa is constantly improving the quality of our data and models, and consequently some historical data points may change causing future runs of the notebook to yield different results.
PS2: The following packages were used:
Please note that matplotlib
is not part of the default requirements of the SDK and therefore it needs to be installed prior to running the notebook. To install the library simply run pip install -U matplotlib
or if you are using conda environment conda install -c conda-forge matplotlib
.
import pandas as pd
from datetime import datetime
import matplotlib.pyplot as plt
from vortexasdk import CargoTimeSeries, Products, Geographies
pd.options.display.max_columns = None
pd.options.display.max_colwidth = 200
pd.options.display.max_rows = 100
We will study China flows starting from January 2018 until the end of March 2020. Note that the SDK provides access to Vortexa data since 2016-01-01 with a maximum date range of 4 years per query.
# Define filter limits, i.e. start and end date for this analysis. We will consider only historical (i.e. closed)
# movements. Given that the notebook was populated at mid April, we will then set our max date
# to the end of March 2020
START_DATE = datetime(2018, 1, 1)
# Make sure you include the whole day, i.e. since we want to include movements up to the end of March we should
# specify end date either as midnight of March 31st or 1st of April (if we used datetime(2020,3,31)) we would
# lose essentially one whole day of movements
END_DATE = datetime(2020, 3, 31, 23, 59, 59)
# Define cargo unit for the timeseries (will be constant over the analysis)
TS_UNIT = 'bpd'
# Define the granularity of the timeseries (will be constant over the analysis)
TS_FREQ = 'month'
print('Start date: {}'.format(START_DATE))
print('End date: {}'.format(END_DATE))
Start date: 2018-01-01 00:00:00 End date: 2020-03-31 23:59:59
# We want to search the ID that corresponds to the origin `China`.
china = [g.id for g in Geographies().search('china').to_list() if 'country' in g.layer]
# Check we've only got one ID for China
assert len(china) == 1
print('China polygon id: {}'.format(china[0]))
2020-04-16 11:14:51,901 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['china']} 2020-04-16 11:14:51,901 vortexasdk.client — INFO — Creating new VortexaClient 2020-04-16 11:14:52,049 vortexasdk.client — INFO — 13 Results to retreive. Sending 1 post requests in parallel using 6 threads. China polygon id: 934c47f36c16a58d68ef5e007e62a23f5f036ee3f3d1f5f85a48c572b90ad8b2
At this point it is worth paying some attention to the .search
method. Given a term argument the query will return all Geographies that match this term. Given the fact that one term can return multiple results we need to make sure we retrieve only the ID(s) we are interested for. In the case above we use the layer
attribute to keep only the Geographies that are of layer
country. To understand the different layers in Geographies check the Geography Entries Docs. We will also illustrate with an example how the results would look if we didn't filter on country layer.
PS: As you can see everytime an API query is executed, logs are printed to the console. The SDK uses a LOG_LEVEL=INFO
as the default option, but users can configure the level of detail of logs with the use of environment variables.
# This is just for illustration purposes
china_all = [(g.id, g.name, g.layer) for g in Geographies().search('china').to_list()]
china_all_df = pd.DataFrame(data = china_all, columns = ['id', 'name', 'layer'])
china_all_df
2020-04-16 11:14:52,411 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['china']} 2020-04-16 11:14:52,565 vortexasdk.client — INFO — 13 Results to retreive. Sending 1 post requests in parallel using 6 threads.
id | name | layer | |
---|---|---|---|
0 | 934c47f36c16a58d68ef5e007e62a23f5f036ee3f3d1f5f85a48c572b90ad8b2 | China | [country] |
1 | 781cacc7033f877caa4b4106d096b74afe006a96391bf5a56a4f55b849359a42 | South China | [shipping_region] |
2 | a63890260e29d859390fd1a23c690181afd4bd152943a04c00cd6a5ecf3f7d1e | North China | [shipping_region] |
3 | b5fafce6e20de2dc307fb7e0b89978ee91a49a7b6ec6f5461daf2633f3c56674 | China (excl. HK & Macau) | [shipping_region] |
4 | 9a021f43c397b175ddfff7a91d46ee6e6e16d37e9f9d52398ac6895656109d86 | China Steel Chemical | [terminal] |
5 | 4f5f0b6f433d65718e62b663e802d9e9f0ff9eba2a2b8dd4558ee94dfbf4d8a6 | China Energy Services Ningbo | [terminal] |
6 | f192fa0cb55da8cdc2e658e9102af9ce632e0be358b0519aa75f0cb3b52fb739 | East China Sea STS | [sts_zone] |
7 | 404fd9f15f7b9bd3e4b2a838307b04dd991813fc9afc9e500d4af03c88fcb7ec | Multipurpose (China Merchants) Terminal | [terminal] |
8 | 4988ec0894dd49ddc0e838325906013aa69125c8a93c04262456900a716e3137 | China Union, Freeport Of Monrovia | [terminal] |
9 | 46bb424e5c7c951b4a85b38c747769ff8d3f1656ddfd51176f696b7e1941cc50 | China Resources Chemical Holding Terminal | [terminal] |
10 | cc637abe27aabcf5665c59ddc7549afedf32702b10fdec9eef2cae45ed736db0 | China General Terminal Distribution Corp | [terminal] |
11 | 9579af63401ee4a876768a77e5c0d34bd0a7cfe89bacd4b0e8b8b3a1bfe703bf | China Bay(Ioc Terminal) (Ex-Trincomalee) | [terminal] |
12 | 662a45e342d9394ce1e4595195f5fbcf3fbb0b1975be2798c745d9d5e2f8642e | Wenzhou China Petroleum Fuel Bitumen Co., Ltd. | [terminal] |
We will now search for the ID that corresponds to Crude/Condensates product. In the same manner as above we will use the product layer to ensure we retrieve the desired ID (although not necessary in this specific case since there is only one product with that name). To understand the different layers in Products check the Product Entries Docs.
# Find Crude/Condensates ID
crude = [p.id for p in Products().search('Crude/Condensates').to_list() if p.layer[0] == 'group']
# Check we've only got one Crude ID
assert len(crude) == 1
print('Crude id: {}'.format(crude[0]))
2020-04-16 11:14:52,961 vortexasdk.operations — INFO — Searching Products with params: {'term': ['Crude/Condensates'], 'ids': [], 'product_parent': [], 'allowTopLevelProducts': True} Crude id: 54af755a090118dcf9b0724c9a4e9f14745c26165385ffa7f1445bc768f06f11
For all the following flow aggregations, intra-movements will be excluded from analysis. Intra-movements are defined as movements having their origin and destination to the same geographic area (e.g. if origin is a country intra-movements are defined as movements starting and finishing at the same country and the same goes for any other layer such as geographic region, trading region etc.). The .search()
method of CargoTimeSeries
object accepts an argument called disable_geographic_exclusion_rules
which is set to None and by default excludes all intra-movements. To include intra-movements to aggregations use disable_geographic_exclusion_rules=True
.
# Query API
df = CargoTimeSeries().search(
# Filter on cargo arrival date (i.e. the date that the unloading operation started)
filter_activity = 'unloading_start',
# We are interested in movements into China
filter_destinations = china,
# Keep only Crude/Condensate movements
filter_products = crude,
# Quantity unit to use
timeseries_unit = TS_UNIT,
# Look on monthly imports
timeseries_frequency = TS_FREQ,
# Uncomment to INCLUDE intra-movements to aggregations
# disable_geographic_exclusion_rules = True,
# Set the date range of analysis
filter_time_min = START_DATE,
filter_time_max = END_DATE).\
to_df()
# Convert key column to datetime and set as index
df['key'] = pd.to_datetime(df['key']).dt.date
df = df.rename(columns = {'key': 'date', 'value': 'bpd'})
df = df.set_index('date')
df.tail()
2020-04-16 11:14:55,447 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'unloading_start', 'filter_activity': 'unloading_start', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['54af755a090118dcf9b0724c9a4e9f14745c26165385ffa7f1445bc768f06f11'], 'filter_vessels': [], 'filter_destinations': ['934c47f36c16a58d68ef5e007e62a23f5f036ee3f3d1f5f85a48c572b90ad8b2'], 'filter_origins': [], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None}
bpd | count | |
---|---|---|
date | ||
2019-11-01 | 9.768183e+06 | 354 |
2019-12-01 | 9.717057e+06 | 384 |
2020-01-01 | 9.739571e+06 | 368 |
2020-02-01 | 8.763636e+06 | 308 |
2020-03-01 | 8.909193e+06 | 335 |
# Plot the data
(df['bpd'] / 10**6).plot(kind='bar', figsize = (10,5))
plt.xticks(rotation = 60)
plt.title('China Crude Imports \n 2-Year Average Import Volume: {:,}M bpd'.
format(round(df['bpd'][:-3].mean() / 10**6, 2)), fontsize=13)
plt.ylabel('Quantity (Millions bpd)')
plt.xlabel('Month')
Text(0.5, 0, 'Month')
March seaborne crude imports into China seem steady at 8.91M bpd, very close to the 8.76M bpd of February and to the 2-year historical average (Jan 2018 - Dec 2019) of 8.71M bpd (note that China also imports crude via pipeline). The data makes sense intuitively, since February/March China arrivals are cargoes that have been bought and loaded at least one or two months before, a period when the coronovirus pandemic hadn't yet escalated at its full extent. The real impact of the pandemic is expected to be seen from April onward.
Let’s also look at the main crude suppliers of China, and how their exports have fared. We will focus on MEG/AG, West Africa, South America East Coast and Russia Far East trading regions. Please note that MEG exports include the port of Ceyhan due to the Kirkuk Iraqi exports and exclude movements loading from Saudi Arabia Red Sea region.
# Find region IDs
meg = [g.id for g in Geographies().search('MEG').to_list() if 'shipping_region' in g.layer]
waf = [g.id for g in Geographies().search('West Africa').to_list() if 'shipping_region' in g.layer]
saec = [g.id for g in Geographies().search('South America East').to_list() if 'trading_region' in g.layer]
russia = [g.id for g in Geographies().search('Russia Far').to_list() if 'trading_region' in g.layer]
suppliers = meg + waf + saec + russia
# Ensure we've only got one ID for the desired regions
assert len(meg) == 1
assert len(waf) == 1
assert len(saec) == 1
assert len(russia) == 1
# Create a dictionary to map region ids to names. This will be useful when we will combine the results from
# different queries to a single DataFrame
suppliers_dict = {meg[0]: 'MEG/AG',
waf[0]: 'West Africa',
saec[0]: 'South America East Coast',
russia[0]: 'Russia Far East'}
2020-04-16 11:14:56,125 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['MEG']} 2020-04-16 11:14:56,240 vortexasdk.client — INFO — 4 Results to retreive. Sending 1 post requests in parallel using 6 threads. 2020-04-16 11:14:56,593 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['West Africa']} 2020-04-16 11:14:56,728 vortexasdk.client — INFO — 3 Results to retreive. Sending 1 post requests in parallel using 6 threads. 2020-04-16 11:14:57,092 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['South America East']} 2020-04-16 11:14:57,360 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['Russia Far']}
We will now make separate API calls for each origin (i.e. China supplier) and store the results for all suppliers to a single DataFrame.
# Create an empty list. After each API call the resulting DataFrame will be appended to this list. At the end,
# the DataFrames of the list will be concatenated to create a single DataFrame
df_list = []
# Iterate through China crude suppliers
for p in suppliers:
print('Loading China crude imports from {}'.format(suppliers_dict[p]))
dfp = CargoTimeSeries().search(
# Filter on cargo arrival date (i.e. the date that the unloading operation started)
filter_activity = 'unloading_start',
# At each iteration use a different origin
filter_origins = p,
# We are only interested in movements into China
filter_destinations = china,
# Keep only Crude/Condensate movements
filter_products = crude,
# Quantity unit to use
timeseries_unit = TS_UNIT,
# Look on monthly imports
timeseries_frequency = TS_FREQ,
# Set the date range of analysis
filter_time_min = START_DATE,
filter_time_max = END_DATE).\
to_df()
dfp['key'] = pd.to_datetime(dfp['key']).dt.date
dfp = dfp.drop(columns = 'count')
dfp = dfp.rename(columns = {'key': 'date', 'value': '{}'.format(suppliers_dict[p])})
dfp = dfp.set_index('date')
df_list.append(dfp)
print('-----------------------------------------------------------------------------------')
# Concatenate DataFrames
df_supl = pd.concat(df_list, axis=1)
df_supl = round(df_supl).astype(int)
df_supl.tail()
Loading China crude imports from MEG/AG 2020-04-16 11:14:57,689 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'unloading_start', 'filter_activity': 'unloading_start', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['54af755a090118dcf9b0724c9a4e9f14745c26165385ffa7f1445bc768f06f11'], 'filter_vessels': [], 'filter_destinations': ['934c47f36c16a58d68ef5e007e62a23f5f036ee3f3d1f5f85a48c572b90ad8b2'], 'filter_origins': ['0899599f74faadb7ba7eb65205ee5c20cb434367a6e7203bc274d310cdb54754'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} ----------------------------------------------------------------------------------- Loading China crude imports from West Africa 2020-04-16 11:14:58,047 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'unloading_start', 'filter_activity': 'unloading_start', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['54af755a090118dcf9b0724c9a4e9f14745c26165385ffa7f1445bc768f06f11'], 'filter_vessels': [], 'filter_destinations': ['934c47f36c16a58d68ef5e007e62a23f5f036ee3f3d1f5f85a48c572b90ad8b2'], 'filter_origins': ['5e0cb0b66780a00576309aecff1b65934fc580d775bab29ac2a79397d8544e04'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} ----------------------------------------------------------------------------------- Loading China crude imports from South America East Coast 2020-04-16 11:14:58,358 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'unloading_start', 'filter_activity': 'unloading_start', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['54af755a090118dcf9b0724c9a4e9f14745c26165385ffa7f1445bc768f06f11'], 'filter_vessels': [], 'filter_destinations': ['934c47f36c16a58d68ef5e007e62a23f5f036ee3f3d1f5f85a48c572b90ad8b2'], 'filter_origins': ['44bbc77b3eb859ff3135f669b152afb76b56434e7faae835e601d8cc6af110eb'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} ----------------------------------------------------------------------------------- Loading China crude imports from Russia Far East 2020-04-16 11:14:58,667 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'unloading_start', 'filter_activity': 'unloading_start', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['54af755a090118dcf9b0724c9a4e9f14745c26165385ffa7f1445bc768f06f11'], 'filter_vessels': [], 'filter_destinations': ['934c47f36c16a58d68ef5e007e62a23f5f036ee3f3d1f5f85a48c572b90ad8b2'], 'filter_origins': ['4bd22474bec28e0d2576ff0ad204966df7f082d85af4f062763c49c4b1cac65d'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} -----------------------------------------------------------------------------------
MEG/AG | West Africa | South America East Coast | Russia Far East | |
---|---|---|---|---|
date | ||||
2019-11-01 | 5020461 | 1440984 | 1042541 | 532488 |
2019-12-01 | 4145689 | 1747375 | 865083 | 628027 |
2020-01-01 | 4659045 | 1235428 | 1212141 | 756810 |
2020-02-01 | 4308542 | 1297303 | 1088467 | 488932 |
2020-03-01 | 4355388 | 1487335 | 800464 | 513588 |
# Calculate 2-year (Jan 2018 to Dec 2019) historical average exports volume per origin
round(df_supl[:-3].mean().to_frame().rename(columns={0: 'avg_bpd'})).astype(int)
avg_bpd | |
---|---|
MEG/AG | 4072638 |
West Africa | 1480763 |
South America East Coast | 988728 |
Russia Far East | 568111 |
# Plot the data
(df_supl / 10**6).plot(figsize=(10,5))
plt.title('China Crude Main Suppliers', fontsize=14)
plt.ylabel('Quantity (Millions bpd)')
plt.xlabel('Month')
plt.grid()
# Alternative way to visualize data: Create a stacked barplot
(df_supl[list(suppliers_dict.values())] / 10**6).plot.bar(stacked=True, figsize = (14,7))
plt.xticks(rotation = 60)
plt.title('China Crude Main Suppliers', fontsize=14)
plt.ylabel('Quantity (Millions bpd)')
plt.xlabel('Month')
Text(0.5, 0, 'Month')
MEG, West African and Russian Far East seaborne crude arrivals to China also look steady and close to their historical averages of approximately 4.1M bpd, 1.48M bpd and 570k bpd respectively. On the other hand, arrivals from South America East Coast stood at ~1.1M bpd on February, a 10% decrease compared to January numbers, to be followed by a further 26.5% decrease in March, standing at 800k bpd.
Let's move on now to explore China's exports on clean products. To make the export analysis compatible with the most commonly used benchmark data, instead of using China
as origin, we will use China (excl. HK & Macau)
. We will focus our analysis on the following products: Gasoline/Blending Components, Jet/Kero and Diesel/Gasoil.
# Find China exl HK & Macau ID
china_excl = [g.id for g in Geographies().search('China (excl. HK & Macau)').to_list()]
# Check we've only got one ID
assert len(china_excl) == 1
2020-04-16 11:14:59,490 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['China (excl. HK & Macau)']}
# Find product IDs
gasoline = [p.id for p in Products().search('Gasoline/Blending Components').to_list() if p.layer[0] == 'group_product']
jet_kero = [p.id for p in Products().search('Jet/Kero').to_list() if p.layer[0] == 'group_product']
diesel_gasoil = [p.id for p in Products().search('Diesel/Gasoil').to_list() if p.layer[0] == 'group_product']
clean_products = gasoline + jet_kero + diesel_gasoil
# Ensure we've only got one ID for the desired clean products
assert len(gasoline) == 1
assert len(jet_kero) == 1
assert len(diesel_gasoil) == 1
# Create a dictionary to map product ids to names
products_dict = {gasoline[0]: 'Gasoline/Blending Components',
jet_kero[0]: 'Jet/Kero',
diesel_gasoil[0]: 'Diesel/Gasoil'}
2020-04-16 11:14:59,775 vortexasdk.operations — INFO — Searching Products with params: {'term': ['Gasoline/Blending Components'], 'ids': [], 'product_parent': [], 'allowTopLevelProducts': True} 2020-04-16 11:15:00,076 vortexasdk.operations — INFO — Searching Products with params: {'term': ['Jet/Kero'], 'ids': [], 'product_parent': [], 'allowTopLevelProducts': True} 2020-04-16 11:15:00,226 vortexasdk.client — INFO — 3 Results to retreive. Sending 1 post requests in parallel using 6 threads. 2020-04-16 11:15:00,776 vortexasdk.operations — INFO — Searching Products with params: {'term': ['Diesel/Gasoil'], 'ids': [], 'product_parent': [], 'allowTopLevelProducts': True} 2020-04-16 11:15:01,057 vortexasdk.client — INFO — 2 Results to retreive. Sending 1 post requests in parallel using 6 threads.
Now we will apply a similar logic and make consecutive API calls, but this time origin and destination will be fixed and product will change at each iteration.
# Apply same logic as with Crude Suppliers
df_list_2 = []
# Iterate through all products
for p in clean_products:
print('Loading China {} exports'.format(products_dict[p]))
dfc = CargoTimeSeries().search(
# We look on exports, therefore we will filter on cargo deparure date (i.e. the date where the loading operation was completed)
filter_activity = 'loading_end',
# Will now use China exl HK & Macau instead of China
filter_origins = china_excl,
# At each iteration an API call for a different product will be made
filter_products = p,
# Keep same unit as before
timeseries_unit = TS_UNIT,
# Look on monthly exports
timeseries_frequency = TS_FREQ,
# Keep same date range
filter_time_min = START_DATE,
filter_time_max = END_DATE).\
to_df()
dfc['key'] = pd.to_datetime(dfc['key']).dt.date
dfc = dfc.drop(columns = 'count')
dfc = dfc.rename(columns = {'key': 'date', 'value': '{}'.format(products_dict[p])})
dfc = dfc.set_index('date')
df_list_2.append(dfc)
print('-----------------------------------------------------------------------------------')
# Concatenate results
df_clean = pd.concat(df_list_2, axis=1)
# Calculate total clean exports
df_clean['total'] = df_clean.sum(axis=1)
# Round results
df_clean = round(df_clean).astype(int)
df_clean.tail()
Loading China Gasoline/Blending Components exports 2020-04-16 11:15:01,436 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'loading_end', 'filter_activity': 'loading_end', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['9256907ba7e4ed11ff03aa297a7e62e14484ce5a85c8118c7495b9120ad0e268'], 'filter_vessels': [], 'filter_destinations': [], 'filter_origins': ['b5fafce6e20de2dc307fb7e0b89978ee91a49a7b6ec6f5461daf2633f3c56674'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} ----------------------------------------------------------------------------------- Loading China Jet/Kero exports 2020-04-16 11:15:01,745 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'loading_end', 'filter_activity': 'loading_end', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['d72edbab2b5e2e249d8c216909945922a02eb017dd200f90876fee6fa180c743'], 'filter_vessels': [], 'filter_destinations': [], 'filter_origins': ['b5fafce6e20de2dc307fb7e0b89978ee91a49a7b6ec6f5461daf2633f3c56674'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} ----------------------------------------------------------------------------------- Loading China Diesel/Gasoil exports 2020-04-16 11:15:02,041 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'loading_end', 'filter_activity': 'loading_end', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['deda35eb9ca56b54e74f0ff370423f9a8c61cf6a3796fcb18eaeeb32a8c290bb'], 'filter_vessels': [], 'filter_destinations': [], 'filter_origins': ['b5fafce6e20de2dc307fb7e0b89978ee91a49a7b6ec6f5461daf2633f3c56674'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} -----------------------------------------------------------------------------------
Gasoline/Blending Components | Jet/Kero | Diesel/Gasoil | total | |
---|---|---|---|---|
date | ||||
2019-11-01 | 492356 | 268515 | 376318 | 1137189 |
2019-12-01 | 435618 | 332784 | 412341 | 1180742 |
2020-01-01 | 317788 | 290461 | 453342 | 1061591 |
2020-02-01 | 419214 | 259163 | 507211 | 1185588 |
2020-03-01 | 391827 | 292484 | 513760 | 1198071 |
# Calculate 2-year (Jan 2018 to Dec 2019) historical average exports volume per product
round(df_clean[:-3].mean().to_frame().rename(columns={0: 'avg_bpd'})).astype(int)
avg_bpd | |
---|---|
Gasoline/Blending Components | 341510 |
Jet/Kero | 200624 |
Diesel/Gasoil | 357895 |
total | 900029 |
# Plot the data
(df_clean / 10**6).plot(figsize=(10,5))
plt.title('China Clean Exports', fontsize=14)
plt.ylabel('Quantity (Millions bpd)')
plt.xlabel('Month')
plt.grid()
# Alternative way to visualize data: Create a stacked barplot
(df_clean[list(products_dict.values())] / 10**6).plot.bar(stacked=True, figsize = (14,6))
plt.xticks(rotation = 60)
plt.title('China Clean Exports (Breakdown by Product)', fontsize=14)
plt.ylabel('Quantity (Millions bpd)')
plt.xlabel('Month')
Text(0.5, 0, 'Month')
While we see a lag on the coronavirus impact on China's crude imports, the impact on clean product exports is more immediate. Diesel exports showed an uptick from Oct 2019 reaching a 2-year high at ~515k bpd on March, a ~30% increase compared to the (2-year) historical average of 358k bpd. March Gasoline and Jet/Kero exports held steady at 390k bpd and 290k bpd respectively, both matching February levels.
Finally, to finish our analysis, let's look at the historical trend of the main destinations for the Chinese Diesel/Gasoil exports. This time we won't look at regions, but we will explore results on a country level. More specifically, we will focus on exports to the Phillipines, Australia and Singapore.
# Find country IDs
philli = [g.id for g in Geographies().search('Philippines').to_list() if 'country' in g.layer]
australia = [g.id for g in Geographies().search('Australia').to_list() if 'country' in g.layer]
singapore = [g.id for g in Geographies().search('Singapore').to_list() if 'country' in g.layer]
receivers = philli + australia + singapore
# Ensure we've only got one ID for the desired countries
assert len(philli) == 1
assert len(australia) == 1
assert len(singapore) == 1
# Create a dictionary to map country ids to names. This will be useful when plotting the results
receivers_dict = {philli[0]: 'Philippines',
australia[0]: 'Australia',
singapore[0]: 'Singapore'}
2020-04-16 11:15:02,792 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['Philippines']} 2020-04-16 11:15:02,918 vortexasdk.client — INFO — 3 Results to retreive. Sending 1 post requests in parallel using 6 threads. 2020-04-16 11:15:03,273 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['Australia']} 2020-04-16 11:15:03,387 vortexasdk.client — INFO — 5 Results to retreive. Sending 1 post requests in parallel using 6 threads. 2020-04-16 11:15:03,738 vortexasdk.operations — INFO — Searching Geographies with params: {'term': ['Singapore']} 2020-04-16 11:15:03,872 vortexasdk.client — INFO — 24 Results to retreive. Sending 1 post requests in parallel using 6 threads.
# Create an empty list. After each API call the resulting DataFrame will be appended to this list. At the end
# the DataFrames of the list will just be concatenated to create a single DataFrame
df_list_3 = []
# Iterate through all destinations
for p in receivers:
print('Loading China diesel/gasoil exports to {}'.format(receivers_dict[p]))
dfp = CargoTimeSeries().search(
# Filter on cargo deparure date
filter_activity = 'loading_end',
# Will again use China exl HK & Macau instead of China
filter_origins = china_excl,
# At each iteration an API call for a different destination will be made
filter_destinations = p,
# Look only at Diesel/F=Gasoil exports
filter_products = diesel_gasoil,
# Keep same quantity as before
timeseries_unit = TS_UNIT,
# Look at monthly exports
timeseries_frequency = TS_FREQ,
# Keep same date range as before
filter_time_min = START_DATE,
filter_time_max = END_DATE).\
to_df()
dfp['key'] = pd.to_datetime(dfp['key']).dt.date
dfp = dfp.drop(columns = 'count')
dfp = dfp.rename(columns = {'key': 'date', 'value': '{}'.format(receivers_dict[p])})
dfp = dfp.set_index('date')
df_list_3.append(dfp)
print('-----------------------------------------------------------------------------------')
# Concatenate DataFrames
df_receiv = pd.concat(df_list_3, axis=1)
df_receiv = round(df_receiv).astype(int)
df_receiv.tail()
Loading China diesel/gasoil exports to Philippines 2020-04-16 11:15:04,258 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'loading_end', 'filter_activity': 'loading_end', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['deda35eb9ca56b54e74f0ff370423f9a8c61cf6a3796fcb18eaeeb32a8c290bb'], 'filter_vessels': [], 'filter_destinations': ['065aab207e6fe875caf93419bd6cfedcbb0933098c75e52a6702b75bdfe71c53'], 'filter_origins': ['b5fafce6e20de2dc307fb7e0b89978ee91a49a7b6ec6f5461daf2633f3c56674'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} ----------------------------------------------------------------------------------- Loading China diesel/gasoil exports to Australia 2020-04-16 11:15:04,806 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'loading_end', 'filter_activity': 'loading_end', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['deda35eb9ca56b54e74f0ff370423f9a8c61cf6a3796fcb18eaeeb32a8c290bb'], 'filter_vessels': [], 'filter_destinations': ['24075b270335ddb25b5f3f0fa8d9657f7a39df829ce9fa262f40a41a8758d21c'], 'filter_origins': ['b5fafce6e20de2dc307fb7e0b89978ee91a49a7b6ec6f5461daf2633f3c56674'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} ----------------------------------------------------------------------------------- Loading China diesel/gasoil exports to Singapore 2020-04-16 11:15:04,988 vortexasdk.operations — INFO — Searching CargoTimeSeries with params: {'timeseries_frequency': 'month', 'timeseries_unit': 'bpd', 'timeseries_activity': 'loading_end', 'filter_activity': 'loading_end', 'filter_time_min': '2018-01-01T00:00:00.000Z', 'filter_time_max': '2020-03-31T23:59:59.000Z', 'size': 500, 'filter_charterers': [], 'filter_owners': [], 'filter_products': ['deda35eb9ca56b54e74f0ff370423f9a8c61cf6a3796fcb18eaeeb32a8c290bb'], 'filter_vessels': [], 'filter_destinations': ['7fed43c640957555ddac588be64822538078409a0acdaf22126623203ef9954a'], 'filter_origins': ['b5fafce6e20de2dc307fb7e0b89978ee91a49a7b6ec6f5461daf2633f3c56674'], 'filter_storage_locations': [], 'filter_ship_to_ship_locations': [], 'filter_waypoints': [], 'disable_geographic_exclusion_rules': None, 'timeseries_activity_time_span_min': None, 'timeseries_activity_time_span_max': None} -----------------------------------------------------------------------------------
Philippines | Australia | Singapore | |
---|---|---|---|
date | |||
2019-11-01 | 62138 | 74625 | 46324 |
2019-12-01 | 108409 | 60698 | 25452 |
2020-01-01 | 39039 | 26275 | 85208 |
2020-02-01 | 45258 | 54056 | 101426 |
2020-03-01 | 62362 | 57715 | 101959 |
# Calculate 2-year (Jan 2018 to Dec 2019) historical average Diesel/Gasoil exports volume per country destination
round(df_receiv[:-3].mean().to_frame().rename(columns={0: 'avg_bpd'})).astype(int)
avg_bpd | |
---|---|
Philippines | 64707 |
Australia | 48511 |
Singapore | 29293 |
# Plot the data
df_receiv.plot(figsize=(10,5))
plt.title('China Diesel/Gasoil Exports', fontsize=14)
plt.ylabel('Quantity (bpd)')
plt.xlabel('Month')
plt.grid()
From the above graph, we see a large increase in China's Diesel/Gasoil exports heading towards Singapore, cargoes that will be used for storage purposes. Exports to Singapore have been steadily rising since January reaching a two-year high of close to 100k bpd in February and March, more than 3-times higher compared to the historical (2-year) average of 29k bpd and a 300% increase compared to the 25k bpd of December 2019.
That’s a very quick overview of how coronavirus is currently impacting China’s crude and clean products flows. As noted above, there are many important signals already evident in the data that tells us about how the situation could unfold in the coming months. Especially on the crude side, the datasets can additionally be blended with analysis of floating storage data – another leading indicator that will be watched given the current shape of the oil forward curve. Stay tuned and keep an eye on our Floating Storage notebook and the upcoming Predicting Flows, Floating Storage Trends, and Covid-19 Effects with Python SDK webinar.