Choke Point Notebook#

The objective of this analysis is to examine the impact of the Red Sea Conflict on maritime trade statistics derived from AIS data.

We process transit calls and estimated trade volume from the IMF’s PortWatch platform for key areas of interest. We then produce charts to inspect trends and calculate percentage changes from a historical baseline.

Setup#

Import libraries and define output paths to save graphs and tables.

import os
from os.path import join, exists

import pandas as pd
import geopandas as gpd
from shapely.geometry import Point

import git
import time

git_repo = git.Repo(os.getcwd(), search_parent_directories=True)
git_root = git_repo.git.rev_parse("--show-toplevel")
from red_sea_monitoring.utils import *

# For plotting
from plotnine import *
from mizani.breaks import date_breaks
from mizani.formatters import date_format, percent_format
import plotnine

# plotnine.options.figure_size = (10, 8)
plotnine.options.figure_size = (5, 4)

output_dir = (
    r"C:\Users\WB514197\WBG\Development Data Partnership - Red Sea Maritime Monitoring"
)
charts_dir = join(git_root, "reports")

Data#

Locations#

Retrieve chokopoints locations dataset and select the three areas of interest.

chokepoints = get_chokepoints()

# List areas of interest
aois = ["Bab el-Mandeb Strait", "Cape of Good Hope", "Suez Canal", "Strait of Hormuz"]

chokepoints_sel = chokepoints.loc[chokepoints.portname.isin(aois)].copy()
chokepoints_sel
portid portname country ISO3 continent fullname lat lon vessel_count_total vessel_count_container ... vessel_count_tanker industry_top1 industry_top2 industry_top3 share_country_maritime_import share_country_maritime_export LOCODE pageid countrynoaccents ObjectId
0 chokepoint1 Suez Canal None None None Suez Canal 30.593346 32.436882 22217 6455 ... 6919 Mineral Products Vegetable Products Chemical & Allied Industries None None None c57c79bf612b4372b08a9c6ea9c97ef0 None 1
3 chokepoint4 Bab el-Mandeb Strait None None None Bab el-Mandeb Strait 12.788597 43.349545 22519 6280 ... 7386 Mineral Products Chemical & Allied Industries Vegetable Products None None None 6b1814d64903461b98144a6cc25eb79c None 4
5 chokepoint6 Strait of Hormuz None None None Strait of Hormuz 26.296853 56.859848 34177 5140 ... 21148 Mineral Products Chemical & Allied Industries Vegetable Products None None None cb5856222a5b4105adc6ee7e880a1730 None 6
6 chokepoint7 Cape of Good Hope None None None Cape of Good Hope -34.927286 20.882737 17332 2018 ... 3973 Mineral Products Vegetable Products Prepared Foodstuffs & Beverages None None None edf18f455a2b4637a3632b6af201abe9 None 7

4 rows × 23 columns

chokepoints_sel.loc[:, "geometry"] = chokepoints_sel.apply(
    lambda x: Point(x.lon, x.lat), axis=1
)
chokepoints_sel = gpd.GeoDataFrame(
    chokepoints_sel, geometry="geometry", crs="EPSG:4326"
)
# chokepoints_sel.to_file(join(output_dir, "chokepoints.shp"), driver='ESRI Shapefile')

Map of Choke Points#

chokepoints_sel[
    [
        "geometry",
        "portname",
        "vessel_count_total",
        "vessel_count_container",
        "vessel_count_dry_bulk",
        "vessel_count_general_cargo",
        "vessel_count_RoRo",
        "vessel_count_tanker",
        "industry_top1",
        "industry_top2",
        "industry_top3",
    ]
].explore(
    column="portname",
    cmap="Dark2",
    marker_kwds={"radius": 15},
    tiles="Esri Ocean Basemap",
    legend_kwds={"loc": "upper right", "caption": "Choke Points"},
)
Make this Notebook Trusted to load map: File -> Trust Notebook

AIS Data#

Pull AIS data for selected locations.

output_dir
'C:\\Users\\WB514197\\WBG\\Development Data Partnership - Red Sea Maritime Monitoring'
ids = list(chokepoints_sel.portid)
df_chokepoints = get_chokepoint_data(ids)
df_chokepoints.loc[:, "ymd"] = df_chokepoints.date.dt.strftime("%Y-%m-%d")
df_chokepoints.to_csv(
    join(
        output_dir,
        "data",
        "PortWatch-data",
        f'chokepoints_data_{time.strftime("%Y_%m_%d")}.csv',
    ),
    index=False,
)

Data Analysis#

Data Smoothening#

# df = pd.read_csv(join(output_dir, f'chokepoints_data_{time.strftime("%m_%d_%Y")}.csv'))
df = pd.read_csv(
    join(
        output_dir,
        "data",
        "PortWatch-data",
        f'chokepoints_data_{time.strftime("%Y_%m_%d")}.csv',
    )
)
df.date = pd.to_datetime(df.date)
df = df.loc[df.date >= "2019-01-01"].copy()
df.tail(2)
date year month day portid portname n_container n_dry_bulk n_general_cargo n_roro ... n_total capacity_container capacity_dry_bulk capacity_general_cargo capacity_roro capacity_tanker capacity_cargo capacity ObjectId ymd
8398 2024-09-28 20:00:00 2024 9 29 chokepoint7 Cape of Good Hope 4 29 1 0 ... 50 670484.409484 2.489614e+06 5845.416104 0.0 1.278569e+06 3.165944e+06 4.444513e+06 46205 2024-09-28
8399 2024-09-29 20:00:00 2024 9 30 chokepoint7 Cape of Good Hope 2 17 1 0 ... 30 148947.124463 1.596359e+06 8962.110837 0.0 9.289129e+05 1.754269e+06 2.683182e+06 46206 2024-09-29

2 rows × 22 columns

Calculate 7-day rolling average and create month-day attribute.

df = (
    df.groupby("portname")[["n_tanker", "n_cargo", "n_total", "capacity", "date"]]
    .rolling(7, center=True, min_periods=1, on="date")
    .mean()
)
df.reset_index(inplace=True)
df.drop("level_1", axis=1, inplace=True)
df.loc[:, "ymd"] = df.date.dt.strftime("%Y-%m-%d")
df.loc[:, "md"] = df.date.dt.strftime("%m-%d")
df.head(2)
portname capacity n_cargo n_tanker n_total date ymd md
0 Bab el-Mandeb Strait 3.888476e+06 42.0 18.25 60.25 2019-01-01 19:00:00 2019-01-01 01-01
1 Bab el-Mandeb Strait 3.866908e+06 40.6 18.20 58.80 2019-01-02 19:00:00 2019-01-02 01-02

Figure: AIS Transit Calls in Key Areas, Historical#

conflict_date = "2023-10-07"
crisis_date = "2023-11-17"
if not exists(join(charts_dir, "chokepoints")):
    os.makedirs(join(charts_dir, "chokepoints"), mode=0o777)
plotnine.options.figure_size = (8, 6)  # 10, 8
p0 = (
    ggplot(df, aes(x="date", y="n_total", group="portname", color="portname"))  #
    + geom_line(alpha=1, size=0.4)
    + geom_vline(xintercept=conflict_date, linetype="dashed", color="red")
    + geom_vline(xintercept=crisis_date, linetype="dashed", color="red")
    + labs(
        x="",
        y="",
        subtitle="Number of Vessels",
        title="AIS Transit Calls in Key Areas",
        color="Area of Interest",
    )
    + theme_minimal()
    + scale_x_datetime(breaks=date_breaks("6 month"), labels=date_format("%Y-%m"))
    + scale_color_brewer(type="qual", palette=2)
    + theme(
        text=element_text(family="Roboto", size=13),
        plot_title=element_text(family="Roboto", size=16, weight="bold"),
        axis_text_x=element_text(rotation=45, hjust=1),
        legend_position="none",
    )
    + facet_wrap("~portname", scales="fixed", ncol=1)
)
display(p0)
p0.save(
    filename=join(
        charts_dir, "chokepoints", "transit-calls-chokepoints-historical.jpeg"
    ),
    dpi=300,
)
../../_images/89df27917252368bea98224eefe00a8f9cd1e2d397b2dba1c164b4d9b3a93a17.png
<Figure Size: (800 x 600)>
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:587: PlotnineWarning: Saving 8 x 6 in image.
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:588: PlotnineWarning: Filename: C:/Users/WB514197/Repos/red-sea-monitoring\reports\chokepoints\transit-calls-chokepoints-historical.jpeg

Calculate Reference Values#

Periods

  • Baseline: 2021, 2022, 2023 (January 1st – October 6th)

  • Middle East Conflict: 2023 (October 7th - November 16th)

  • Red Sea Crisis: November 17th - January 31st, 2024

start_reference_date = "2022-01-01"
conflict_date = "2023-10-07"
crisis_date = "2023-11-17"
df_ref = df.loc[(df.date >= start_reference_date) & (df.date < conflict_date)].copy()
df_ref = df_ref.groupby(["portname", "md"])[
    ["n_tanker", "n_cargo", "n_total", "capacity"]
].mean()
df_ref.reset_index(inplace=True)
df_ref.rename(
    columns={
        "n_tanker": "n_tanker_ref",
        "n_cargo": "n_cargo_ref",
        "n_total": "n_total_ref",
        "capacity": "capacity_ref",
    },
    inplace=True,
)

Filter recent data (2023 onwards) and merge reference values.

df_filt = df.loc[(df.date >= "2023-01-01")].copy()
df_filt = df_filt.merge(df_ref, on=["portname", "md"], how="left", validate="m:1")

Calculate percentage change.

df_filt.loc[:, "n_total_pct_ch"] = df_filt.apply(
    lambda x: (x.n_total - x.n_total_ref) / (x.n_total_ref), axis=1
)
df_filt = df_filt.loc[~(df_filt.ymd == "2024-02-29")].copy()

Figure: AIS Transit Calls in Key Areas#

p1 = (
    ggplot(df_filt, aes(x="ymd", y="n_total", group="portname", color="portname"))  #
    + geom_line(alpha=1)
    + geom_vline(xintercept=conflict_date, linetype="dashed", color="red")
    + geom_vline(xintercept=crisis_date, linetype="dashed", color="red")
    + labs(
        x="",
        y="",
        subtitle="Number of Vessels",
        title="AIS Transit Calls in Key Areas",
        color="Area of Interest",
    )
    + theme_minimal()
    + theme(
        text=element_text(family="Roboto", size=13),
        plot_title=element_text(family="Roboto", size=16, weight="bold"),
        axis_text_x=element_text(rotation=45, hjust=1),
        legend_position="none",
    )
    + scale_x_datetime(breaks=date_breaks("2 month"), labels=date_format("%Y-%m"))
    + scale_color_brewer(type="qual", palette=2)
    + theme(legend_position="bottom")
)
display(p1)
p1.save(
    filename=join(charts_dir, "chokepoints", "transit-calls-chokepoints.jpeg"), dpi=300
)
../../_images/be2ef067ce24cdffd639563c861c652970ef466c44516ea0c271903c8a6976bb.png
<Figure Size: (800 x 600)>
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:587: PlotnineWarning: Saving 8 x 6 in image.
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:588: PlotnineWarning: Filename: C:/Users/WB514197/Repos/red-sea-monitoring\reports\chokepoints\transit-calls-chokepoints.jpeg

Figure: AIS Transit Calls Relative to Historical Average#

p2 = (
    ggplot(df_filt, aes(x="date", y="n_total", group="portname", color="portname"))  #
    + geom_line(size=1)
    + geom_vline(xintercept=conflict_date, linetype="dashed", color="red")
    + geom_vline(xintercept=crisis_date, linetype="dashed", color="red")
    + geom_line(
        aes(x="date", y="n_total_ref", group="portname"),
        color="black",
        size=0.75,
        alpha=3 / 4,
    )
    + labs(
        x="",
        y="",
        subtitle="Number of Vessels",
        title="AIS Transit Calls Relative to Historical Average",
        color="Area of Interest",
    )
    + theme_minimal()
    + theme(
        text=element_text(family="Roboto", size=13),
        plot_title=element_text(family="Roboto", size=16, weight="bold"),
        axis_text_x=element_text(rotation=45, hjust=1),
        legend_position="none",
    )
    + scale_x_datetime(breaks=date_breaks("2 month"), labels=date_format("%Y-%m"))
    + scale_color_brewer(type="qual", palette=2)
    + facet_wrap("~portname", scales="fixed", ncol=1)
)
display(p2)
p2.save(
    filename=join(charts_dir, "chokepoints", "transit-calls-chokepoints-ref.jpeg"),
    dpi=300,
)
../../_images/b326cdf6440245a90128671eaef60210ee4402a8d0f2bb1f3dc485d3942c26e2.png
<Figure Size: (800 x 600)>
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:587: PlotnineWarning: Saving 8 x 6 in image.
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:588: PlotnineWarning: Filename: C:/Users/WB514197/Repos/red-sea-monitoring\reports\chokepoints\transit-calls-chokepoints-ref.jpeg

Figure: AIS Transit Calls % Change from Historical Average#

p3 = (
    ggplot(
        df_filt.loc[df_filt.date >= "2023-08-01"],
        aes(x="ymd", y="n_total_pct_ch", group="portname", color="portname"),
    )  #
    + geom_line(size=0.8)
    + geom_vline(xintercept=conflict_date, linetype="dashed", color="red")
    + geom_vline(xintercept=crisis_date, linetype="dashed", color="red")
    + geom_hline(yintercept=0, color="black", alpha=3 / 4, size=0.3)
    + labs(
        x="",
        y="",
        subtitle="% Change",
        title="AIS Transit Calls in Key Areas",
        color="Area of Interest",
    )
    + theme_minimal()
    + theme(
        text=element_text(family="Roboto", size=13),
        plot_title=element_text(family="Roboto", size=16, weight="bold"),
        axis_text_x=element_text(rotation=45, hjust=1),
        legend_position="none",
    )
    + scale_x_datetime(breaks=date_breaks("1 month"), labels=date_format("%Y-%m"))
    + scale_color_brewer(type="qual", palette=2)
    + scale_y_continuous(labels=percent_format())
    + facet_wrap("~portname", scales="fixed", ncol=1)
)
display(p3)
p3.save(
    filename=join(charts_dir, "chokepoints", "transit-calls-chokepoints-pct.jpeg"),
    dpi=300,
)
../../_images/035a9ef539a98be3821b01897e6981bebba3ad3a91d45515da6f9083fb500eb8.png
<Figure Size: (800 x 600)>
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:587: PlotnineWarning: Saving 8 x 6 in image.
c:\WBG\Anaconda3\envs\rtmis\Lib\site-packages\plotnine\ggplot.py:588: PlotnineWarning: Filename: C:/Users/WB514197/Repos/red-sea-monitoring\reports\chokepoints\transit-calls-chokepoints-pct.jpeg

Summary Statistics#

Calculate average values by location and time period.

conflict_date, crisis_date
('2023-10-07', '2023-11-17')
df.loc[:, "period"] = ""
df.loc[
    (df.date >= start_reference_date) & (df.date < crisis_date), "period"
] = "Reference"
df.loc[
    (df.date >= conflict_date) & (df.date < crisis_date), "period"
] = "Middle East Conflict"
df.loc[(df.date >= crisis_date), "period"] = "Red Sea Crisis"
df_agg = (
    df.loc[df.period != ""]
    .groupby(["portname", "period"])[["n_tanker", "n_cargo", "n_total", "capacity"]]
    .mean()
)

# change order of rows
df_agg = df_agg.reindex(
    ["Reference", "Middle East Conflict", "Red Sea Crisis"], level=1
)

Table: Daily Average Values by Time Period#

table = df_agg.copy()
# format column numbers to 2 decimal places only for first three columns
table.iloc[:, :3] = table.iloc[:, :3].applymap(lambda x: "{:.2f}".format(x))

# format last column numbers to thousands
table.loc[:, "capacity"] = table.capacity.apply(lambda x: "{:,.0f}".format(x))

table.rename(
    columns={
        "n_tanker": "Tankers",
        "n_cargo": "Cargo",
        "n_total": "Total",
        "capacity": "Capacity",
    },
    inplace=True,
)
table.index.names = ["Area of Interest", "Period"]
display(table)
C:\Users\WB514197\AppData\Local\Temp\ipykernel_32892\36468859.py:3: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
Tankers Cargo Total Capacity
Area of Interest Period
Bab el-Mandeb Strait Reference 23.91 46.86 70.77 4,823,326
Middle East Conflict 25.95 51.58 77.53 5,305,603
Red Sea Crisis 15.34 27.61 42.95 2,585,144
Cape of Good Hope Reference 9.34 36.68 46.01 4,380,133
Middle East Conflict 10.03 39.20 49.23 4,813,646
Red Sea Crisis 15.03 53.92 68.95 6,224,209
Suez Canal Reference 23.09 46.98 70.07 4,737,291
Middle East Conflict 25.20 51.64 76.83 5,246,677
Red Sea Crisis 16.06 32.79 48.85 2,947,435
print(table.reset_index().to_markdown(tablefmt="github", index=False))
| Area of Interest     | Period               |   Tankers |   Cargo |   Total | Capacity   |
|----------------------|----------------------|-----------|---------|---------|------------|
| Bab el-Mandeb Strait | Reference            |     23.91 |   46.86 |   70.77 | 4,823,326  |
| Bab el-Mandeb Strait | Middle East Conflict |     25.95 |   51.58 |   77.53 | 5,305,603  |
| Bab el-Mandeb Strait | Red Sea Crisis       |     15.34 |   27.61 |   42.95 | 2,585,144  |
| Cape of Good Hope    | Reference            |      9.34 |   36.68 |   46.01 | 4,380,133  |
| Cape of Good Hope    | Middle East Conflict |     10.03 |   39.2  |   49.23 | 4,813,646  |
| Cape of Good Hope    | Red Sea Crisis       |     15.03 |   53.92 |   68.95 | 6,224,209  |
| Suez Canal           | Reference            |     23.09 |   46.98 |   70.07 | 4,737,291  |
| Suez Canal           | Middle East Conflict |     25.2  |   51.64 |   76.83 | 5,246,677  |
| Suez Canal           | Red Sea Crisis       |     16.06 |   32.79 |   48.85 | 2,947,435  |
df_agg_copy = df_agg.copy()
res = []
for aoi in aois:
    df_sub = df_agg_copy.loc[(aoi), :].transpose().copy()
    df_sub.loc[:, "Middle East Conflict"] = (
        df_sub.loc[:, "Middle East Conflict"] - df_sub.loc[:, "Reference"]
    ) / df_sub.loc[:, "Reference"]
    df_sub.loc[:, "Red Sea Crisis"] = (
        df_sub.loc[:, "Red Sea Crisis"] - df_sub.loc[:, "Reference"]
    ) / df_sub.loc[:, "Reference"]
    df_sub2 = df_sub.transpose()
    df_sub2.drop("Reference", inplace=True)
    df_sub2.loc[:, "portname"] = aoi
    res.append(df_sub2)
df_agg_pct = pd.concat(res)

Table: Daily Average Values by Time Period, % Change from Baseline#

df_agg_pct.reset_index(inplace=True)
df_agg_pct.set_index(["portname", "period"], inplace=True)

# format columns as pct
df_agg_pct = df_agg_pct.applymap(lambda x: "{:.2%}".format(x))
df_agg_pct.rename(
    columns={
        "n_tanker": "Tankers",
        "n_cargo": "Cargo",
        "n_total": "Total",
        "capacity": "Capacity",
    },
    inplace=True,
)
df_agg_pct.index.names = ["Area of Interest", "Period"]
display(df_agg_pct)
C:\Users\WB514197\AppData\Local\Temp\ipykernel_32892\3184510119.py:5: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
Tankers Cargo Total Capacity
Area of Interest Period
Bab el-Mandeb Strait Middle East Conflict 8.56% 10.08% 9.56% 10.00%
Red Sea Crisis -35.83% -41.07% -39.30% -46.40%
Cape of Good Hope Middle East Conflict 7.42% 6.87% 6.98% 9.90%
Red Sea Crisis 61.03% 47.01% 49.85% 42.10%
Suez Canal Middle East Conflict 9.12% 9.91% 9.65% 10.75%
Red Sea Crisis -30.46% -30.20% -30.29% -37.78%
print(df_agg_pct.reset_index().to_markdown(tablefmt="github", index=False))
| Area of Interest     | Period               | Tankers   | Cargo   | Total   | Capacity   |
|----------------------|----------------------|-----------|---------|---------|------------|
| Bab el-Mandeb Strait | Middle East Conflict | 8.56%     | 10.08%  | 9.56%   | 10.00%     |
| Bab el-Mandeb Strait | Red Sea Crisis       | -35.83%   | -41.07% | -39.30% | -46.40%    |
| Cape of Good Hope    | Middle East Conflict | 7.42%     | 6.87%   | 6.98%   | 9.90%      |
| Cape of Good Hope    | Red Sea Crisis       | 61.03%    | 47.01%  | 49.85%  | 42.10%     |
| Suez Canal           | Middle East Conflict | 9.12%     | 9.91%   | 9.65%   | 10.75%     |
| Suez Canal           | Red Sea Crisis       | -30.46%   | -30.20% | -30.29% | -37.78%    |

Save tables to excel

with pd.ExcelWriter(
    join(
        output_dir,
        "tables",
        f'summary-tables-chokepoints-{time.strftime("%Y_%m_%d")}.xlsx',
    )
) as writer:
    table.to_excel(writer, sheet_name="Chokepoints Summary")
    df_agg_pct.to_excel(writer, sheet_name="Chokepoints % Change")