The 2022 wheat shock¶
The Russian Federation and Ukraine are two of the largest wheat exporters, and the war in Ukraine began in February 2022. This notebook looks at what the FAOSTAT Detailed Trade Matrix shows for their wheat trade in 2021, 2022 and 2023, using only the sample bundled with netviz-tools, so it runs offline.
It covers:
- Russian and Ukrainian export totals and their main buyers.
- Why the reporting perspective changes the answer for Russia.
- World volume and supplier concentration, 2010 to 2024.
- Which exporters rose and fell from 2021 to 2022.
- The largest exporters over time, and a caveat about reverse PageRank.
- A Sankey diagram, a flow map and networks for 2022 and over time.
All quantities are in tonnes as reported in FAOSTAT. Unless stated otherwise, flows are importer-reported (the default of faostat.load_sample). The data show what changed; they do not say why.
import pandas as pd
import plotly.io as pio
import netviz_tools as nv
from netviz_tools.datasets import faostat
pio.renderers.default = "notebook_connected"
RU, UA = "Russian Federation", "Ukraine"
EXPORTERS = [RU, UA, "United States of America", "Canada", "Australia"]
wheat = faostat.load_sample(items="Wheat")
countries = faostat.countries()
print(
f"{len(wheat):,} flows, {wheat['time'].min()} to {wheat['time'].max()}, unit {wheat['unit'].unique()}"
)
22,633 flows, 2010 to 2024, unit ['t']
Russian and Ukrainian exports¶
Total exports per year in million tonnes, as reported by the importing countries:
exports = (
wheat[wheat["source"].isin([RU, UA])]
.pivot_table(index="time", columns="source", values="weight", aggfunc="sum")
.div(1e6)
)
exports.loc[2019:2024].round(2)
| source | Russian Federation | Ukraine |
|---|---|---|
| time | ||
| 2019 | 24.82 | 15.22 |
| 2020 | 26.82 | 14.63 |
| 2021 | 22.39 | 14.58 |
| 2022 | 22.44 | 8.36 |
| 2023 | 33.96 | 13.35 |
| 2024 | 27.90 | 15.00 |
Ukraine's exports fell from 14.58 million tonnes in 2021 to 8.36 in 2022, a drop of about 43%, and recovered to 13.35 in 2023. Russia's importer-reported exports hardly moved in 2022 (22.39 to 22.44) and rose to 33.96 in 2023.
Who bought from them¶
nv.partners ranks a country's buyers for one year. The share is of that country's total exports.
def top_buyers(country, years=(2021, 2022, 2023), n=5):
cols = {}
for y in years:
p = nv.partners(wheat, country, role="out", time=y, top_n=n)
cols[y] = [f"{name} ({share:.0%})" for name, share in p["share"].items()]
return pd.DataFrame(cols, index=pd.RangeIndex(1, n + 1, name="rank"))
top_buyers(UA)
| 2021 | 2022 | 2023 | |
|---|---|---|---|
| rank | |||
| 1 | Indonesia (21%) | Türkiye (25%) | Spain (31%) |
| 2 | Egypt (12%) | Spain (12%) | Türkiye (19%) |
| 3 | Türkiye (11%) | Poland (6%) | Egypt (7%) |
| 4 | Pakistan (9%) | Romania (6%) | Indonesia (5%) |
| 5 | Morocco (6%) | Egypt (6%) | Italy (4%) |
top_buyers(RU)
| 2021 | 2022 | 2023 | |
|---|---|---|---|
| rank | |||
| 1 | Türkiye (28%) | Türkiye (28%) | Türkiye (26%) |
| 2 | Egypt (12%) | Egypt (18%) | Egypt (16%) |
| 3 | Iran (Islamic Republic of) (11%) | Iran (Islamic Republic of) (8%) | Kazakhstan (8%) |
| 4 | Kazakhstan (6%) | Saudi Arabia (8%) | Pakistan (6%) |
| 5 | Azerbaijan (5%) | Kazakhstan (7%) | Saudi Arabia (5%) |
Ukraine's buyers changed. Indonesia, its largest buyer in 2021, is not in the 2022 top five, while Spain, Poland and Romania are. In 2023 Spain took 31% of Ukraine's wheat. Russia's buyers stayed much the same: Türkiye and Egypt together took 40% to 46% of its exports in all three years.
nv.plot.time_series with focus= follows one country per year, straight from the flow table. role="out" keeps its exports; hover shows each year's change from the year before:
nv.plot.time_series(wheat, focus=UA, role="out", time=range(2015, 2025)).show()
The reporting perspective¶
Each flow can be reported twice: by the importer and by the exporter. The Russian Federation stops reporting its trade after 2021. With exporter-reported data, its wheat exports disappear from 2022:
by_reporter = pd.DataFrame(
{
rep: faostat.load_sample(items="Wheat", reporter=rep)
.query("source == @RU")
.groupby("time")["weight"]
.sum()
.div(1e6)
for rep in ("importer", "exporter", "combined")
}
)
by_reporter.loc[2019:2024].round(2)
| importer | exporter | combined | |
|---|---|---|---|
| time | |||
| 2019 | 24.82 | 31.87 | 30.49 |
| 2020 | 26.82 | 37.27 | 32.87 |
| 2021 | 22.39 | 27.26 | 26.62 |
| 2022 | 22.44 | NaN | 22.44 |
| 2023 | 33.96 | NaN | 33.96 |
| 2024 | 27.90 | NaN | 27.90 |
Three things follow:
- Exporter-reported data have no Russian wheat exports in 2022, 2023 or 2024. An analysis on that perspective would show the largest supplier of 2022 by some measures vanishing from the market.
- Importer-reported data (the default) still see Russia, because its buyers report what they received: 22.44 million tonnes in 2022.
- The two perspectives disagree even when both exist. In 2021 Russia reported 27.26 million tonnes of exports; its partners reported receiving 22.39. The combined view takes the importer's report where one exists and fills the rest from the exporter's.
Russia also stops reporting its imports. Importer-reported data have Russian wheat imports up to 2021 and none after (NaN means no rows):
ru_imports = (
wheat[wheat["target"] == RU]
.groupby("time")["weight"]
.sum()
.div(1e3)
.rename("Russia imports, kt")
)
ru_imports.reindex(range(2019, 2025)).round(1)
time 2019 191.6 2020 190.0 2021 123.2 2022 NaN 2023 NaN 2024 NaN Name: Russia imports, kt, dtype: float64
This matters again below, when a centrality measure depends on Russia's imports.
Which exporters rose and fell¶
nv.plot.ranking with two years and change=True ranks countries by the change between them. size_by="out_strength" is a country's total exports. The plot takes the flow table and builds the two yearly graphs itself.
nv.plot.ranking(wheat, time=[2021, 2022], size_by="out_strength", change=True).show()
change_21_22 = (
wheat[wheat["time"].isin([2021, 2022])]
.pivot_table(index="source", columns="time", values="weight", aggfunc="sum", fill_value=0)
.div(1e6)
.assign(change=lambda d: d[2022] - d[2021])
.sort_values("change")
)
pd.concat([change_21_22.head(3), change_21_22.tail(3)]).round(2)
| time | 2021 | 2022 | change |
|---|---|---|---|
| source | |||
| Ukraine | 14.58 | 8.36 | -6.22 |
| Canada | 19.16 | 14.99 | -4.18 |
| United States of America | 20.68 | 17.35 | -3.32 |
| India | 2.02 | 4.31 | 2.29 |
| Argentina | 9.73 | 12.79 | 3.06 |
| France | 13.82 | 17.66 | 3.84 |
Ukraine has the largest drop (14.58 to 8.36 million tonnes, 6.22 million less), followed by Canada (4.18 million less) and the United States (3.32 million less). France has the largest increase (13.82 to 17.66 million tonnes, 3.84 million more), then Argentina (3.06 million more) and India (2.29 million more).
World volume and supplier concentration¶
nv.graphs_by(..., by="time") builds one graph per year. nv.temporal.metric_series then computes graph-level metrics for each. out_strength_hhi is the Herfindahl-Hirschman index of exporters' shares of world volume: 1 would mean a single supplier.
graphs = nv.graphs_by(wheat, by="time", node_attrs=countries)
world = nv.temporal.metric_series(
graphs, ["n_nodes", "n_edges", "total_weight", "out_strength_hhi"], index_name="year"
)
(
world.loc[2019:2024]
.astype({"n_nodes": int, "n_edges": int})
.assign(total_weight=lambda d: (d["total_weight"] / 1e6).round(1))
.round({"out_strength_hhi": 4})
.rename(columns={"total_weight": "total_weight (Mt)"})
)
| n_nodes | n_edges | total_weight (Mt) | out_strength_hhi | |
|---|---|---|---|---|
| year | ||||
| 2019 | 162 | 1625 | 156.2 | 0.0952 |
| 2020 | 166 | 1605 | 163.4 | 0.0932 |
| 2021 | 166 | 1741 | 170.7 | 0.0855 |
| 2022 | 166 | 1646 | 166.6 | 0.0841 |
| 2023 | 164 | 1650 | 175.3 | 0.0991 |
| 2024 | 146 | 1417 | 161.9 | 0.0881 |
fig = nv.plot.time_series(
world.assign(total_weight=world["total_weight"] / 1e6).rename(
columns={"total_weight": "world volume (Mt)", "out_strength_hhi": "supplier HHI"}
),
["world volume (Mt)", "supplier HHI"],
facet=True,
title="World wheat trade, importer-reported",
)
fig.show()
change = world.loc[2022, "total_weight"] / world.loc[2021, "total_weight"] - 1
print(f"World volume 2021 -> 2022: {change:+.1%}")
World volume 2021 -> 2022: -2.4%
World volume fell by 2.4% in 2022, from 170.7 to 166.6 million tonnes, and reached 175.3 million tonnes in 2023. Supplier concentration barely changed in 2022 (HHI 0.0855 to 0.0841). The HHI then rose to 0.0991 in 2023, when Russia's importer-reported exports reached 34 million tonnes.
The 2024 row has fewer countries (146 against 164 in 2023). Reporting for the latest year may still be incomplete in this release. Treat the last year with care.
The largest exporters over time¶
nv.temporal.centrality_series tracks one centrality measure per country across the yearly graphs. Dividing out-strength by world volume gives each exporter's share:
out_strength = nv.temporal.centrality_series(
graphs, "out_strength", nodes=EXPORTERS, index_name="year"
)
share = out_strength.div(world["total_weight"], axis=0).mul(100)
share.loc[2019:2024].round(2)
| node | Russian Federation | Ukraine | United States of America | Canada | Australia |
|---|---|---|---|---|---|
| year | |||||
| 2019 | 15.89 | 9.75 | 14.68 | 12.98 | 5.69 |
| 2020 | 16.42 | 8.95 | 13.60 | 13.82 | 4.83 |
| 2021 | 13.12 | 8.54 | 12.11 | 11.22 | 14.03 |
| 2022 | 13.47 | 5.02 | 10.42 | 9.00 | 14.99 |
| 2023 | 19.37 | 7.62 | 8.77 | 12.20 | 14.74 |
| 2024 | 17.23 | 9.26 | 12.11 | 11.44 | 9.30 |
fig = nv.plot.time_series(
share,
title="Share of world wheat exports, importer-reported",
y_title="% of world volume",
)
fig.show()
Ukraine's share fell from 8.5% in 2021 to 5.0% in 2022. Australia was the largest exporter in 2022 at 15.0% (25.0 million tonnes), ahead of Russia at 13.5%.
A caveat about reverse PageRank¶
reverse_pagerank is PageRank on the reversed graph: a country scores high when it supplies a large share of the imports of countries that themselves score high. It measures position, not volume, and it reacts to small flows between exporters.
rpr = nv.temporal.centrality_series(graphs, "reverse_pagerank", nodes=EXPORTERS, index_name="year")
rpr.loc[2019:2024].round(3)
| node | Russian Federation | Ukraine | United States of America | Canada | Australia |
|---|---|---|---|---|---|
| year | |||||
| 2019 | 0.098 | 0.010 | 0.133 | 0.134 | 0.012 |
| 2020 | 0.113 | 0.008 | 0.134 | 0.131 | 0.011 |
| 2021 | 0.105 | 0.008 | 0.113 | 0.107 | 0.064 |
| 2022 | 0.024 | 0.084 | 0.128 | 0.119 | 0.061 |
| 2023 | 0.035 | 0.088 | 0.083 | 0.080 | 0.023 |
| 2024 | 0.024 | 0.028 | 0.093 | 0.085 | 0.025 |
Russia's score fell from 0.105 in 2021 to 0.024 in 2022 while its exports stayed at 22.4 million tonnes. The decomposition below shows why. On the reversed graph, each buyer passes on its own score in proportion to the share of its imports that came from Russia (times the damping factor 0.85):
def contributions(g, country, alpha=0.85):
rp = nv.metrics.centrality(g, ["reverse_pagerank"], alpha=alpha)["reverse_pagerank"]
rows = {
buyer: alpha * rp[buyer] * g[country][buyer]["weight"] / g.in_degree(buyer, weight="weight")
for buyer in g.successors(country)
}
return pd.Series(rows, name="contribution").sort_values(ascending=False)
for year in (2021, 2022):
c = contributions(graphs[year], RU)
total = nv.metrics.centrality(graphs[year], ["reverse_pagerank"]).loc[RU, "reverse_pagerank"]
print(f"{year}: Russia {total:.3f}; top buyers' contributions:")
print(c.head(3).round(4).to_string(), "\n")
2021: Russia 0.105; top buyers' contributions: Kazakhstan 0.0763 Belarus 0.0084 Türkiye 0.0022 2022: Russia 0.024; top buyers' contributions: Türkiye 0.0049 Kazakhstan 0.0042 Armenia 0.0010
In 2021, most of Russia's score (0.076 of 0.105) came through Kazakhstan, which bought almost all of its small wheat imports from Russia. Kazakhstan in turn scored high because Russia, also a small importer, bought Kazakh wheat. The two exporters fed each other's score. In 2022 Russia no longer reports its imports, so the Kazakhstan-to-Russia edge disappears from importer-reported data, the loop breaks, and both scores drop. The same mechanism works the other way for Ukraine: its score rose from 0.008 to 0.084 in 2022, the year Poland and Romania, both wheat exporters themselves, entered its top five buyers.
So the fall in Russia's reverse PageRank is a reporting artifact, not a change in its exports. Use out-strength or shares for volume questions, and check any centrality result against the reporter perspective.
2022 in three figures¶
The plots below take the 2022 graph, which carries continent attributes from faostat.countries().
g22 = graphs[2022]
print(
f"{g22.number_of_nodes()} countries, {g22.number_of_edges():,} flows, "
f"{world.loc[2022, 'total_weight'] / 1e6:.1f} Mt"
)
166 countries, 1,646 flows, 166.6 Mt
The Sankey diagram shows flows from the ten largest exporters (left) to the ten largest importers (right). All other countries are grouped as "Other".
nv.plot.sankey(g22, top_n=10).show()
The flow map draws the 60 largest flows between the bundled country label points.
nv.plot.flow_map(g22, top_n=60).show()
The network shows the 40 countries with the largest total trade, coloured by continent (without color_by, nodes are coloured by trade community). Edges below the median weight among those countries are dropped to reduce clutter, and the 15 largest countries are labelled. Hover over a node or an edge midpoint for exact amounts.
nv.plot.network(g22, top_n=40, color_by="continent", min_weight_quantile=0.5, show_labels=15).show()
Passing the mapping of yearly graphs animates the network, one frame per year, with node positions fixed so that movement means change. Here are the 30 largest countries from 2020 to 2023:
nv.plot.network(
{year: graphs[year] for year in range(2020, 2024)},
top_n=30,
color_by="continent",
min_weight_quantile=0.5,
show_labels=10,
title="Wheat trade, 2020 to 2023",
).show()
Summary¶
- Ukraine's importer-reported wheat exports fell from 14.6 million tonnes in 2021 to 8.4 in 2022, the largest drop of any exporter, and its largest buyers changed: Spain, Poland and Romania entered its top five.
- World wheat trade fell by only 2.4% in 2022, and supplier concentration did not rise until 2023.
- The Russian Federation stops reporting its trade after 2021. Exporter-reported data lose its exports, and any metric that uses its imports changes for reasons that have nothing to do with the market.
Data: FAO. 2025. FAOSTAT: Detailed trade matrix. Accessed on 27 September 2026. https://www.fao.org/faostat/en/#data/TM. Licence: CC-BY-4.0.