The graduate squeeze: young graduates, unemployment and AI exposure in Africa¶

Evidence from Ghana, Nigeria, Kenya, Egypt, Senegal and South Africa

Emmanuel Edudzie, October 2026. Analysis code written with substantial help from Claude (Anthropic), under the author's direction.

The question. The first note in this series found that few young workers hold jobs exposed to generative AI, and that the exposure sits in office work. The second found that students are a large share of early AI users. This note asks about the people in between: young graduates. Do they find work, and is the work they find the kind that generative AI could change?

Graduates here means people with advanced education in the ILO's aggregate levels, which is tertiary education: ISCED 2011 levels 5–8 (short-cycle tertiary, bachelor's, master's, doctoral). Section 1 checks this against the detailed ISCED levels in the data.

Data sources¶

File What it is
UNE_DEAP_SEX_AGE_EDU_RT_A.csv ILOSTAT: unemployment rate by sex, age and education (%), annual
EMP_TEMP_SEX_AGE_EDU_NB_A.csv ILOSTAT: employment by sex, age and education (thousands), annual
EMP_TEMP_SEX_OCU_EDU_NB_A.csv ILOSTAT: employment by sex, occupation and education (thousands), annual

¶

All three ILOSTAT files were downloaded on 30 October 2026 for Egypt, Ghana, Kenya, Nigeria, Senegal and South Africa, 2015 onwards.

0. Set-up¶

In [1]:
from decimal import ROUND_HALF_UP, Decimal
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

ROOT = Path.cwd()
RAW, CLEAN, FIG = ROOT / "data" / "raw", ROOT / "data" / "clean", ROOT / "outputs" / "figures"
for folder in (CLEAN, FIG):
    folder.mkdir(parents=True, exist_ok=True)
pd.set_option("display.width", 160)

SURFACE, INK, INK_2, GRID = "#fcfcfb", "#0b0b0b", "#52514e", "#e4e3df"
BLUE, GREY, REFERENCE = "#2a78d6", "#c3c2b7", "#8a8984"
plt.rcParams.update({
    "figure.facecolor": SURFACE, "axes.facecolor": SURFACE, "savefig.facecolor": SURFACE,
    "axes.edgecolor": GRID, "axes.labelcolor": INK_2, "xtick.color": INK_2, "ytick.color": INK,
    "text.color": INK, "font.size": 10, "axes.spines.top": False, "axes.spines.right": False,
})


def frame(fig, title, subtitle, source):
    """Title and subtitle at the top left, source line at the bottom left."""
    fig.text(0.01, 0.985, title, fontsize=12, fontweight="bold", color=INK, va="top")
    fig.text(0.01, 0.925, subtitle, fontsize=9.5, color=INK_2, va="top")
    fig.text(0.01, 0.01, source, fontsize=7.5, color=INK_2, va="bottom")


def half_up(x, places=1):
    """Round halves up (12.25 -> 12.3). Python's round() can go either way."""
    return float(Decimal(str(round(float(x), 6))).quantize(Decimal(1).scaleb(-places), rounding=ROUND_HALF_UP))


# The latest survey year in each file for every country (section 1 checks the sources)
SELECTED = {"Ghana": 2025, "Nigeria": 2025, "Kenya": 2021, "Egypt": 2024, "Senegal": 2025, "South Africa": 2025}
COUNTRIES = list(SELECTED)
EDU_ORDER = ["Less than basic", "Basic", "Intermediate", "Advanced"]
GROUP_NAMES = {
    "1": "Managers", "2": "Professionals", "3": "Technicians and associate professionals",
    "4": "Clerical support workers", "5": "Service and sales workers",
    "6": "Skilled agricultural, forestry and fishery workers", "7": "Craft and related trades workers",
    "8": "Plant and machine operators, and assemblers", "9": "Elementary occupations",
}


def load(name):
    """Read an ILOSTAT file, keep the chosen country-years and the aggregate education levels."""
    d = pd.read_csv(RAW / f"{name}.csv")
    d.columns = ["country", "source", "indicator", "sex", "classif1", "edu", "time", "value", "status",
                 "note_classif", "note_indicator", "note_source"]
    keep = np.array([SELECTED.get(c) == t for c, t in zip(d["country"], d["time"])])
    d = d[keep & d["edu"].str.startswith("Education (Aggregate levels)").to_numpy()].copy()
    d["edu"] = d["edu"].str.split(": ").str[1]
    return d

1. Load the data and check it¶

1a. Sources and years¶

Every country's latest year comes from a labour force survey, except Kenya, whose latest is the 2021 Continuous Household Survey.

In [2]:
une = load("UNE_DEAP_SEX_AGE_EDU_RT_A")
emp_age = load("EMP_TEMP_SEX_AGE_EDU_NB_A")
emp_occ = load("EMP_TEMP_SEX_OCU_EDU_NB_A")
pd.concat({"unemployment": une.groupby(["country", "time"])["source"].first(),
           "employment by age": emp_age.groupby(["country", "time"])["source"].first(),
           "employment by occupation": emp_occ.groupby(["country", "time"])["source"].first()}, axis=1)
Out[2]:
unemployment employment by age employment by occupation
country time
Egypt 2024 LFS - Labour Force Sample Survey LFS - Labour Force Sample Survey LFS - Labour Force Sample Survey
Ghana 2025 LFS - Labour Force Survey LFS - Labour Force Survey LFS - Labour Force Survey
Kenya 2021 HS - Continuous household survey HS - Continuous household survey HS - Continuous household survey
Nigeria 2025 LFS - Unemployment, Under-employment Watch LFS - Unemployment, Under-employment Watch LFS - Unemployment, Under-employment Watch
Senegal 2025 LFS - National Employment Survey LFS - National Employment Survey LFS - National Employment Survey
South Africa 2025 LFS - Quarterly Labour Force Survey LFS - Quarterly Labour Force Survey LFS - Quarterly Labour Force Survey

1b. "Advanced" means tertiary education¶

The detailed ISCED 2011 levels 5–8 should add up to the "Advanced" aggregate. We check it on total employment aged 15+ in each country that reports ISCED 2011.

In [3]:
raw_age = pd.read_csv(RAW / "EMP_TEMP_SEX_AGE_EDU_NB_A.csv")
raw_age = raw_age[np.array([SELECTED.get(c) == t for c, t in zip(raw_age["ref_area.label"], raw_age["time"])])
                  & (raw_age["sex.label"] == "Total") & (raw_age["classif1.label"] == "Age (Youth, adults): 15+")]
rows = []
for country, d in raw_age.groupby("ref_area.label"):
    v = d.set_index("classif2.label")["obs_value"]
    isced = v[v.index.str.startswith("Education (ISCED-11)")]
    if len(isced):
        rows.append({"country": country, "ISCED 5-8 (000s)": isced[isced.index.str.contains(r": [5-8]\.")].sum(),
                     "Advanced (000s)": v["Education (Aggregate levels): Advanced"]})
pd.DataFrame(rows).round(1)
Out[3]:
country ISCED 5-8 (000s) Advanced (000s)
0 Egypt 6107.6 6107.6
1 Ghana 1297.4 1297.4
2 Kenya 1045.8 1045.8
3 Nigeria 7919.2 7919.2
4 Senegal 279.1 279.1
5 South Africa 3426.3 3426.3

1c. Cells ILOSTAT flags as unreliable¶

Small samples are flagged. We list the flagged cells that touch the figures used below.

In [4]:
flag_une = une[(une["status"] == "Unreliable") & une["classif1"].isin(
    ["Age (Youth, adults): 15-24", "Age (10-year bands): 25-34"]) & une["edu"].isin(["Advanced", "Total"])]
flag_occ = emp_occ[(emp_occ["status"] == "Unreliable") & (emp_occ["edu"] == "Advanced")]
display(flag_une[["country", "sex", "classif1", "edu", "value"]])
flag_occ[["country", "sex", "classif1", "value"]]
country sex classif1 edu value
10572 Kenya Male Age (Youth, adults): 15-24 Advanced 18.737
16401 Senegal Male Age (Youth, adults): 15-24 Advanced 21.998
16422 Senegal Female Age (Youth, adults): 15-24 Advanced 36.777
Out[4]:
country sex classif1 value
336 Egypt Female Occupation (ISCO-08): 6. Skilled agricultural,... 4.338
6496 Ghana Total Occupation (Skill level): X - Not elsewhere cl... 4.601
6523 Ghana Male Occupation (Skill level): X - Not elsewhere cl... 4.601
6545 Ghana Female Occupation (Skill level): Skill level 1 ~ low 4.609
6550 Ghana Female Occupation (Skill level): X - Not elsewhere cl... NaN
6762 Ghana Total Occupation (ISCO-08): 0. Armed forces occupations 4.601
6821 Ghana Male Occupation (ISCO-08): 0. Armed forces occupations 4.601
6870 Ghana Female Occupation (ISCO-08): 8. Plant and machine ope... 2.344
6875 Ghana Female Occupation (ISCO-08): 9. Elementary occupations 4.609
6880 Ghana Female Occupation (ISCO-08): 0. Armed forces occupations NaN
11980 Kenya Total Occupation (Skill level): X - Not elsewhere cl... NaN
12010 Kenya Male Occupation (Skill level): X - Not elsewhere cl... NaN
12040 Kenya Female Occupation (Skill level): X - Not elsewhere cl... NaN
12261 Kenya Total Occupation (ISCO-08): X. Not elsewhere classified NaN
12308 Kenya Male Occupation (ISCO-08): 7. Craft and related tra... 8.185
12326 Kenya Male Occupation (ISCO-08): X. Not elsewhere classified NaN
12373 Kenya Female Occupation (ISCO-08): 7. Craft and related tra... 3.993
12391 Kenya Female Occupation (ISCO-08): X. Not elsewhere classified NaN
13791 Nigeria Total Occupation (ISCO-08): 0. Armed forces occupations 33.415
13848 Nigeria Male Occupation (ISCO-08): 0. Armed forces occupations 19.481
13897 Nigeria Female Occupation (ISCO-08): 8. Plant and machine ope... 28.539
13905 Nigeria Female Occupation (ISCO-08): 0. Armed forces occupations NaN
15091 Senegal Female Occupation (Skill level): Skill level 1 ~ low 2.050
15295 Senegal Total Occupation (ISCO-08): 6. Skilled agricultural,... 2.608
15300 Senegal Total Occupation (ISCO-08): 7. Craft and related tra... 3.267
15306 Senegal Total Occupation (ISCO-08): 8. Plant and machine ope... 3.137
15316 Senegal Total Occupation (ISCO-08): 0. Armed forces occupations 4.638
15362 Senegal Male Occupation (ISCO-08): 6. Skilled agricultural,... 2.208
15367 Senegal Male Occupation (ISCO-08): 7. Craft and related tra... 2.460
15373 Senegal Male Occupation (ISCO-08): 8. Plant and machine ope... 2.968
15383 Senegal Male Occupation (ISCO-08): 0. Armed forces occupations 4.166
15401 Senegal Female Occupation (ISCO-08): 1. Managers 2.958
15424 Senegal Female Occupation (ISCO-08): 5. Service and sales wor... 5.363
15429 Senegal Female Occupation (ISCO-08): 6. Skilled agricultural,... NaN
15434 Senegal Female Occupation (ISCO-08): 7. Craft and related tra... NaN
15440 Senegal Female Occupation (ISCO-08): 8. Plant and machine ope... NaN
15445 Senegal Female Occupation (ISCO-08): 9. Elementary occupations 2.050
15450 Senegal Female Occupation (ISCO-08): 0. Armed forces occupations NaN
20727 South Africa Total Occupation (ISCO-88): 6. Skilled agricultural ... 5.601
20750 South Africa Total Occupation (ISCO-88): 0. Armed forces 3.942
20797 South Africa Male Occupation (ISCO-88): 6. Skilled agricultural ... 4.760
20820 South Africa Male Occupation (ISCO-88): 0. Armed forces 3.942
20867 South Africa Female Occupation (ISCO-88): 6. Skilled agricultural ... NaN
20890 South Africa Female Occupation (ISCO-88): 0. Armed forces NaN

None of the flagged cells is a headline total. The figures for graduate men and women aged 15–24 in Kenya and Senegal rest on flagged cells, so the sex split below uses ages 25–34.

2. Are young graduates more likely to be unemployed?¶

2a. Unemployment rate by education, ages 15–24 and 25–34¶

Few 15–24-year-olds have finished tertiary education, so ages 25–34 are the main measure for young graduates; 15–24 is shown alongside.

In [5]:
AGES = {"Age (Youth, adults): 15-24": "15–24", "Age (10-year bands): 25-34": "25–34"}
u = une[une["classif1"].isin(AGES)].assign(age=lambda d: d["classif1"].map(AGES))
rates = (u[u["sex"] == "Total"].pivot_table(index=["country", "age"], columns="edu", values="value")
         [EDU_ORDER + ["Total"]].reindex(COUNTRIES, level=0))
rates.to_csv(CLEAN / "unemployment_by_education.csv")
rates.round(1)
Out[5]:
edu Less than basic Basic Intermediate Advanced Total
country age
Ghana 15–24 3.5 4.0 7.4 13.2 5.9
25–34 1.7 2.5 4.9 10.7 4.4
Nigeria 15–24 3.8 4.6 7.2 18.5 7.0
25–34 2.8 3.7 4.5 16.9 5.9
Kenya 15–24 9.6 8.2 14.6 22.8 12.3
25–34 4.3 5.0 10.0 11.7 8.1
Egypt 15–24 5.6 6.0 13.9 42.8 15.3
25–34 2.6 3.0 6.0 17.7 8.5
Senegal 15–24 2.3 10.2 17.6 29.8 5.0
25–34 3.2 9.4 21.3 22.0 7.8
South Africa 15–24 46.3 52.4 56.2 35.8 53.2
25–34 34.2 44.2 37.7 21.4 37.3
In [6]:
gap = rates[["Advanced", "Total"]].copy()
gap["graduates ÷ all"] = gap["Advanced"] / gap["Total"]
gap.round(1)
Out[6]:
edu Advanced Total graduates ÷ all
country age
Ghana 15–24 13.2 5.9 2.2
25–34 10.7 4.4 2.5
Nigeria 15–24 18.5 7.0 2.7
25–34 16.9 5.9 2.9
Kenya 15–24 22.8 12.3 1.9
25–34 11.7 8.1 1.5
Egypt 15–24 42.8 15.3 2.8
25–34 17.7 8.5 2.1
Senegal 15–24 29.8 5.0 5.9
25–34 22.0 7.8 2.8
South Africa 15–24 35.8 53.2 0.7
25–34 21.4 37.3 0.6

Reading this. In five of the six countries, graduates are more likely than their peers to be unemployed, at both ages. South Africa is the exception: unemployment is very high for everyone, and lowest for graduates. A common explanation for the pattern elsewhere is that where most work is informal and there are no unemployment benefits, people with little education cannot afford to stay unemployed, while graduates can wait longer for the formal jobs they trained for.

2b. Graduate unemployment by sex, ages 25–34¶

In [7]:
by_sex = (u[(u["age"] == "25–34") & (u["edu"] == "Advanced")]
          .pivot_table(index="country", columns="sex", values="value")[["Female", "Male", "Total"]]
          .reindex(COUNTRIES))
by_sex.to_csv(CLEAN / "graduate_unemployment_by_sex_25_34.csv")
by_sex.round(1)
Out[7]:
sex Female Male Total
country
Ghana 11.0 10.6 10.7
Nigeria 16.9 17.0 16.9
Kenya 12.6 10.8 11.7
Egypt 31.0 10.6 17.7
Senegal 25.2 20.2 22.0
South Africa 24.7 16.9 21.4

2c. Chart: unemployment of graduates and of all young people¶

In [8]:
fig, axes = plt.subplots(1, 2, figsize=(8, 4.2), sharey=True)
fig.subplots_adjust(left=0.15, right=0.97, top=0.74, bottom=0.14, wspace=0.14)
order = rates.xs("25–34", level="age")["Advanced"].sort_values().index.tolist()   # bottom to top
for ax, age in zip(axes, ["15–24", "25–34"]):
    r = rates.xs(age, level="age").loc[order]
    for y, (country, row) in enumerate(r.iterrows()):
        ax.plot([row["Total"], row["Advanced"]], [y, y], color=GRID, linewidth=3, zorder=1)
        ax.scatter(row["Total"], y, s=60, color=GREY, zorder=2, label="All in the age group" if y == 0 else None)
        ax.scatter(row["Advanced"], y, s=60, color=BLUE, zorder=3, label="Graduates" if y == 0 else None)
        label = f"{half_up(row['Advanced'], 0):.0f}"
        if row["Advanced"] >= row["Total"]:     # graduate dot on the right: label to its right
            ax.text(row["Advanced"] + 2.2, y, label, va="center", ha="left", fontsize=8.5, color=BLUE, fontweight="bold")
        else:                                   # South Africa: graduate dot on the left
            ax.text(row["Advanced"] - 2.2, y, label, va="center", ha="right", fontsize=8.5, color=BLUE, fontweight="bold")
    ax.set_title(f"Aged {age}", fontsize=9.5, loc="left", fontweight="bold", color=INK)
    ax.set_xlim(0, 62)
    ax.set_xticks([0, 20, 40, 60], ["0", "20", "40", "60%"])
    ax.grid(axis="x", color=GRID, linewidth=0.8)
    ax.set_axisbelow(True)
    ax.spines[["left", "bottom"]].set_visible(False)
    ax.tick_params(axis="both", length=0)
    ax.set_yticks(range(len(order)), order)
axes[0].legend(frameon=False, loc="lower left", bbox_to_anchor=(0.0, 1.08), ncol=2, fontsize=8.5,
               handletextpad=0.2, columnspacing=1.2)
frame(fig, "Young graduates are more likely to be unemployed than their peers, except in South Africa",
      "Unemployment rate (%): people with tertiary education against everyone in the age group",
      "Source: ILOSTAT, unemployment rate by sex, age and education (Ghana 2025, Nigeria 2025, Kenya 2021, Egypt 2024,\n"
      "Senegal 2025, South Africa 2025). Graduates = advanced (tertiary) education, ISCED 2011 levels 5–8.")
fig.savefig(FIG / "graduate_unemployment.png", dpi=200)
plt.show()
No description has been provided for this image

3. How many young workers are graduates?¶

Share of employed people with tertiary education, by age.

In [9]:
AGE3 = {"Age (Youth, adults): 15-24": "15–24", "Age (10-year bands): 25-34": "25–34", "Age (Youth, adults): 25+": "25+"}
e = emp_age[(emp_age["sex"] == "Total") & emp_age["classif1"].isin(AGE3)].assign(age=lambda d: d["classif1"].map(AGE3))
e = e.pivot_table(index=["country", "age"], columns="edu", values="value")
grad_share = (e["Advanced"] / e["Total"] * 100).unstack("age").reindex(COUNTRIES)
grad_share.to_csv(CLEAN / "graduate_share_of_employment.csv")
grad_share.round(1)
Out[9]:
age 15–24 25+ 25–34
country
Ghana 5.6 10.0 14.1
Nigeria 4.1 9.8 11.7
Kenya 3.4 5.7 7.9
Egypt 10.3 20.1 25.6
Senegal 1.3 6.7 8.7
South Africa 8.6 19.6 18.1

4. Where do employed graduates work?¶

ILOSTAT publishes employment by occupation and education for all ages together, not by age, so this section covers all employed graduates. It uses the nine major occupation groups and the exposure tiers from the first note:

Tier Occupation groups
High 4 Clerical support
Mixed 2 Professionals, 3 Technicians (finance, ICT and media highly exposed; teaching and health not)
Partial 5 Service and sales
Low 1 Managers, 6 Farming, 7 Crafts, 8 Machine operators, 9 Elementary work

Senegal is excluded here: 65% of its workers' occupations are unclassified. South Africa reports the older ISCO-88 groups, as in the first note. Unclassified workers and the armed forces are left out of the denominators.

In [10]:
o = emp_occ[emp_occ["country"] != "Senegal"].copy()
o["scheme"] = o["classif1"].str.extract(r"\((ISCO-08|ISCO-88)\)")[0]
o = o.dropna(subset=["scheme"])
preferred = o.groupby("country")["scheme"].agg(lambda s: "ISCO-08" if "ISCO-08" in set(s) else "ISCO-88")
o = o[o["scheme"] == o["country"].map(preferred)]
o["group"] = o["classif1"].str.split(": ").str[1].str.split(".").str[0]

total = o[(o["group"] == "Total") & (o["sex"] == "Total")].set_index(["country", "edu"])["value"]
unclassified = o[(o["group"] == "X") & (o["sex"] == "Total")].set_index(["country", "edu"])["value"]
print("Unclassified occupation, % of employed:")
display((unclassified / total * 100).unstack("edu").reindex(columns=EDU_ORDER + ["Total"]).round(1))

nine = o[o["group"].isin(list("123456789"))].copy()
nine["share"] = nine["value"] / nine.groupby(["country", "sex", "edu"])["value"].transform("sum") * 100
nine.to_csv(CLEAN / "employment_shares_by_group_and_education.csv", index=False)

TIERS = {"4": "High: clerical support", "2": "Mixed: professionals and technicians",
         "3": "Mixed: professionals and technicians", "5": "Partial: service and sales"}
TIER_ORDER = ["High: clerical support", "Mixed: professionals and technicians",
              "Partial: service and sales", "Low: farming, crafts, operators, elementary, managers"]
nine["tier"] = nine["group"].map(TIERS).fillna(TIER_ORDER[3])
tiers = nine.groupby(["country", "sex", "edu", "tier"])["share"].sum().unstack("tier")[TIER_ORDER]
tiers.round(2).to_csv(CLEAN / "employment_shares_by_tier_and_education.csv")
tiers.xs("Total", level="sex").round(1)
Unclassified occupation, % of employed:
edu Less than basic Basic Intermediate Advanced Total
country
Egypt 14.5 12.8 6.0 2.6 8.4
Ghana NaN NaN NaN NaN NaN
Kenya 2.0 1.2 0.9 NaN 1.1
Nigeria 1.6 2.5 3.1 3.1 2.8
South Africa 26.5 9.3 3.2 1.0 6.2
Out[10]:
tier High: clerical support Mixed: professionals and technicians Partial: service and sales Low: farming, crafts, operators, elementary, managers
country edu
Egypt Advanced 8.8 60.3 12.8 18.1
Basic 0.6 2.1 19.6 77.6
Intermediate 6.5 14.2 20.7 58.7
Less than basic 0.2 1.0 17.5 81.3
Total 4.7 18.7 18.3 58.4
Ghana Advanced 6.8 49.9 15.0 28.3
Basic 0.4 1.2 27.7 70.7
Intermediate 3.2 6.7 32.0 58.1
Less than basic 0.0 0.4 16.3 83.3
Level not stated NaN NaN 58.5 41.5
Total 1.5 6.7 25.4 66.4
Kenya Advanced 3.1 52.3 2.8 41.8
Basic 0.1 2.1 6.2 91.6
Intermediate 1.7 12.3 12.0 74.0
Less than basic NaN 2.6 3.7 93.7
Level not stated 0.0 20.7 0.0 79.3
Total 0.9 9.1 8.2 81.7
Nigeria Advanced 4.6 32.5 27.2 35.6
Basic 0.3 1.7 30.8 67.3
Intermediate 0.7 6.4 33.8 59.1
Less than basic NaN 1.5 25.1 73.4
Total 0.9 7.1 32.0 59.9
South Africa Advanced 13.4 49.7 8.2 28.7
Basic 3.6 2.3 16.2 77.8
Intermediate 16.3 9.1 23.6 51.0
Less than basic 0.6 1.7 8.0 89.7
Level not stated 5.7 8.9 13.1 72.2
Total 10.8 14.7 17.3 57.3
In [11]:
OCC_COUNTRIES = [c for c in COUNTRIES if c != "Senegal"]
t = tiers.xs("Total", level="sex")
office = pd.DataFrame({"Graduates": t.xs("Advanced", level="edu")[TIER_ORDER[:2]].sum(axis=1),
                       "All workers": t.xs("Total", level="edu")[TIER_ORDER[:2]].sum(axis=1)}).reindex(OCC_COUNTRIES)
# basic or less: recombine the two lowest levels by numbers employed, not by averaging shares
low_edu = nine[(nine["sex"] == "Total") & nine["edu"].isin(["Less than basic", "Basic"])]
low_edu = low_edu.assign(office=low_edu["tier"].isin(TIER_ORDER[:2]))
office["Basic or less"] = (low_edu[low_edu["office"]].groupby("country")["value"].sum()
                           / low_edu.groupby("country")["value"].sum() * 100)
office["graduates ÷ all"] = office["Graduates"] / office["All workers"]
office.round(1)
Out[11]:
Graduates All workers Basic or less graduates ÷ all
country
Ghana 56.7 8.2 1.3 6.9
Nigeria 37.1 8.0 1.8 4.6
Kenya 55.4 10.1 2.3 5.5
Egypt 69.1 23.4 1.8 3.0
South Africa 63.0 25.5 5.5 2.5

Graduates are far more likely than other workers to hold clerical, professional and technical jobs, the tiers where generative AI exposure is concentrated. Part of the "mixed" tier is teaching and health, which are not exposed, so this is an upper bound on graduates' exposure.

4b. Chart: exposure tiers of graduates' jobs and of all jobs¶

In [12]:
TIER_STYLE = {TIER_ORDER[0]: (BLUE, 1.0), TIER_ORDER[1]: (BLUE, 0.62), TIER_ORDER[2]: (BLUE, 0.3), TIER_ORDER[3]: (GREY, 1.0)}
SHORT = {TIER_ORDER[0]: "High: clerical", TIER_ORDER[1]: "Mixed: professionals, technicians",
         TIER_ORDER[2]: "Partial: service, sales", TIER_ORDER[3]: "Low: other"}
fig, ax = plt.subplots(figsize=(8, 5.4))
fig.subplots_adjust(left=0.2, right=0.86, top=0.8, bottom=0.1)
labels, ypos, data = [], [], []
for i, country in enumerate(OCC_COUNTRIES[::-1]):
    for j, (edu, name) in enumerate([("Total", "All workers"), ("Advanced", "Graduates")]):
        y = i * 2.6 + j
        ypos.append(y)
        labels.append(f"{country}  {name}" if j == 1 else name)
        data.append(t.loc[(country, edu)])
data = pd.DataFrame(data)
left = np.zeros(len(data))
for tier in TIER_ORDER:
    colour, alpha = TIER_STYLE[tier]
    ax.barh(ypos, data[tier], left=left, color=colour, alpha=alpha, edgecolor=SURFACE, linewidth=1.2, height=0.8,
            label=SHORT[tier])
    left += data[tier].to_numpy()
for y, v in zip(ypos, data[TIER_ORDER[:2]].sum(axis=1)):
    ax.text(103, y, f"{half_up(v, 0):.0f}%", va="center", fontsize=9, fontweight="bold", color=BLUE)
ax.text(103, max(ypos) + 1.1, "Office-based\n(high + mixed)", fontsize=8, color=INK_2, va="bottom")
ax.set_yticks(ypos, [lab.replace("  ", "\n") if "Graduates" in lab else lab for lab in labels], fontsize=8.5)
ax.set_xlim(0, 100)
ax.set_xticks([0, 25, 50, 75, 100])
ax.tick_params(axis="both", length=0)
ax.spines[["left", "bottom"]].set_visible(False)
ax.legend(frameon=False, loc="lower left", bbox_to_anchor=(-0.22, 1.0), ncol=4, fontsize=8, handlelength=1.2,
          columnspacing=1.0)
frame(fig, "Graduates hold the office jobs where generative AI exposure is concentrated",
      "Share of employed people with a known occupation, by exposure tier of their occupation group (%)",
      "Source: ILOSTAT, employment by sex, occupation and education (Ghana 2025, Nigeria 2025, Kenya 2021, Egypt 2024,\n"
      "South Africa 2025); tiers from ILO Working Paper 140. All ages. South Africa uses ISCO-88 major groups.")
fig.savefig(FIG / "graduate_exposure_tiers.png", dpi=200)
plt.show()
No description has been provided for this image

5. How many graduates work below graduate level?¶

The ILO's standard (normative) measure links occupation groups to skill levels: groups 1–3 (managers, professionals, technicians) are skill levels 3–4, which usually call for tertiary education; groups 4–9 are skill levels 1–2. A graduate in groups 4–9 is counted as working below graduate level ("overeducated").

In [13]:
grads = nine[(nine["sex"] == "Total") & (nine["edu"] == "Advanced")].pivot_table(
    index="country", columns="group", values="share").reindex(OCC_COUNTRIES)
below = pd.DataFrame({
    "Clerical (group 4)": grads["4"],
    "Service and sales (group 5)": grads["5"],
    "Farming, crafts, operators, elementary (6–9)": grads[["6", "7", "8", "9"]].sum(axis=1),
})
below["Below graduate level, total"] = below.sum(axis=1)
below.to_csv(CLEAN / "graduates_below_graduate_level.csv")
below.round(1)
Out[13]:
Clerical (group 4) Service and sales (group 5) Farming, crafts, operators, elementary (6–9) Below graduate level, total
country
Ghana 6.8 15.0 18.3 40.1
Nigeria 4.6 27.2 31.2 63.0
Kenya 3.1 2.8 18.0 23.9
Egypt 8.8 12.8 10.3 31.9
South Africa 13.4 8.2 7.8 29.4
In [14]:
by_sex_below = (nine[(nine["edu"] == "Advanced") & nine["group"].isin(list("456789"))]
                .groupby(["country", "sex"])["share"].sum().unstack("sex")[["Female", "Male"]].reindex(OCC_COUNTRIES))
by_sex_below.round(1)
Out[14]:
sex Female Male
country
Ghana 42.3 38.5
Nigeria 65.1 61.8
Kenya 17.2 28.4
Egypt 16.3 38.3
South Africa 31.6 26.6
In [15]:
fig, ax = plt.subplots(figsize=(8, 3.9))
fig.subplots_adjust(left=0.16, right=0.95, top=0.72, bottom=0.12)
b = below.sort_values("Below graduate level, total")
parts = [("Clerical (group 4)", BLUE, 1.0, "Clerical (high exposure)"),
         ("Service and sales (group 5)", BLUE, 0.35, "Service and sales"),
         ("Farming, crafts, operators, elementary (6–9)", GREY, 1.0, "Farming, crafts, operators, elementary")]
left = np.zeros(len(b))
for col, colour, alpha, name in parts:
    ax.barh(b.index, b[col], left=left, color=colour, alpha=alpha, edgecolor=SURFACE, linewidth=1.2, height=0.65,
            label=name)
    left += b[col].to_numpy()
for y, v in enumerate(b["Below graduate level, total"]):
    ax.text(v + 1, y, f"{half_up(v, 0):.0f}%", va="center", fontsize=9.5, fontweight="bold", color=INK)
ax.set_xlim(0, 75)
ax.set_xticks([])
ax.spines[["left", "bottom"]].set_visible(False)
ax.tick_params(axis="y", length=0, labelsize=10)
ax.legend(frameon=False, loc="lower left", bbox_to_anchor=(-0.2, 1.0), ncol=3, fontsize=8, handlelength=1.2)
frame(fig, "In Nigeria, nearly two in three employed graduates work below graduate level",
      "Share of employed graduates in occupations that do not usually require tertiary education (%), by occupation",
      "Source: ILOSTAT, employment by sex, occupation and education (Ghana 2025, Nigeria 2025, Kenya 2021, Egypt 2024,\n"
      "South Africa 2025). All ages. Below graduate level = ISCO major groups 4–9 (skill levels 1–2), the ILO's normative measure.")
fig.savefig(FIG / "graduates_below_level.png", dpi=200)
plt.show()
No description has been provided for this image

6. Summary numbers for the brief¶

In [16]:
r25, r15 = rates.xs("25–34", level="age"), rates.xs("15–24", level="age")
summary = {
    **{f"{c}: unemployment 25–34, graduates (%)": half_up(r25.loc[c, "Advanced"]) for c in COUNTRIES},
    **{f"{c}: unemployment 25–34, all (%)": half_up(r25.loc[c, "Total"]) for c in COUNTRIES},
    **{f"{c}: unemployment 15–24, graduates (%)": half_up(r15.loc[c, "Advanced"]) for c in COUNTRIES},
    **{f"{c}: unemployment 15–24, all (%)": half_up(r15.loc[c, "Total"]) for c in COUNTRIES},
    "Egypt: graduate women 25–34 unemployed (%)": half_up(by_sex.loc["Egypt", "Female"]),
    "Egypt: graduate men 25–34 unemployed (%)": half_up(by_sex.loc["Egypt", "Male"]),
    **{f"{c}: graduates' share of employed 25–34 (%)": half_up(grad_share.loc[c, "25–34"]) for c in COUNTRIES},
    **{f"{c}: graduates in office-based jobs (%)": half_up(office.loc[c, "Graduates"]) for c in OCC_COUNTRIES},
    **{f"{c}: all workers in office-based jobs (%)": half_up(office.loc[c, "All workers"]) for c in OCC_COUNTRIES},
    **{f"{c}: graduates in clerical jobs (%)": half_up(grads.loc[c, "4"]) for c in OCC_COUNTRIES},
    **{f"{c}: graduates below graduate level (%)": half_up(below.loc[c, "Below graduate level, total"]) for c in OCC_COUNTRIES},
}
summary = pd.Series(summary)
summary.to_csv(ROOT / "outputs" / "summary_numbers.csv", header=["value"])
summary
Out[16]:
Ghana: unemployment 25–34, graduates (%)                10.7
Nigeria: unemployment 25–34, graduates (%)              16.9
Kenya: unemployment 25–34, graduates (%)                11.7
Egypt: unemployment 25–34, graduates (%)                17.7
Senegal: unemployment 25–34, graduates (%)              22.0
South Africa: unemployment 25–34, graduates (%)         21.4
Ghana: unemployment 25–34, all (%)                       4.4
Nigeria: unemployment 25–34, all (%)                     5.9
Kenya: unemployment 25–34, all (%)                       8.1
Egypt: unemployment 25–34, all (%)                       8.5
Senegal: unemployment 25–34, all (%)                     7.8
South Africa: unemployment 25–34, all (%)               37.3
Ghana: unemployment 15–24, graduates (%)                13.2
Nigeria: unemployment 15–24, graduates (%)              18.5
Kenya: unemployment 15–24, graduates (%)                22.8
Egypt: unemployment 15–24, graduates (%)                42.8
Senegal: unemployment 15–24, graduates (%)              29.8
South Africa: unemployment 15–24, graduates (%)         35.8
Ghana: unemployment 15–24, all (%)                       5.9
Nigeria: unemployment 15–24, all (%)                     7.0
Kenya: unemployment 15–24, all (%)                      12.3
Egypt: unemployment 15–24, all (%)                      15.3
Senegal: unemployment 15–24, all (%)                     5.0
South Africa: unemployment 15–24, all (%)               53.2
Egypt: graduate women 25–34 unemployed (%)              31.0
Egypt: graduate men 25–34 unemployed (%)                10.6
Ghana: graduates' share of employed 25–34 (%)           14.1
Nigeria: graduates' share of employed 25–34 (%)         11.7
Kenya: graduates' share of employed 25–34 (%)            7.9
Egypt: graduates' share of employed 25–34 (%)           25.6
Senegal: graduates' share of employed 25–34 (%)          8.7
South Africa: graduates' share of employed 25–34 (%)    18.1
Ghana: graduates in office-based jobs (%)               56.7
Nigeria: graduates in office-based jobs (%)             37.1
Kenya: graduates in office-based jobs (%)               55.4
Egypt: graduates in office-based jobs (%)               69.1
South Africa: graduates in office-based jobs (%)        63.0
Ghana: all workers in office-based jobs (%)              8.2
Nigeria: all workers in office-based jobs (%)            8.0
Kenya: all workers in office-based jobs (%)             10.1
Egypt: all workers in office-based jobs (%)             23.4
South Africa: all workers in office-based jobs (%)      25.5
Ghana: graduates in clerical jobs (%)                    6.8
Nigeria: graduates in clerical jobs (%)                  4.6
Kenya: graduates in clerical jobs (%)                    3.1
Egypt: graduates in clerical jobs (%)                    8.8
South Africa: graduates in clerical jobs (%)            13.4
Ghana: graduates below graduate level (%)               40.1
Nigeria: graduates below graduate level (%)             63.0
Kenya: graduates below graduate level (%)               23.9
Egypt: graduates below graduate level (%)               31.9
South Africa: graduates below graduate level (%)        29.4
dtype: float64

7. Limitations¶

  1. Occupation by education covers all ages. ILOSTAT does not publish it by age, so section 4 describes all employed graduates, not only young ones.
  2. Major groups only. Exposure varies within groups; many graduate professionals are teachers and health workers, whose jobs are not exposed. The office-based share is an upper bound.
  3. Exposure is not job loss. The tiers measure what generative AI could do to tasks, not what it will do.
  4. Unemployment is a narrow measure. It leaves out graduates who have stopped looking for work and those in informal or part-time work below their skills.
  5. Survey differences. Years, surveys and classifications differ (Kenya's data are from 2021; South Africa uses ISCO-88), so cross-country comparisons are approximate.
  6. Small samples. Few people aged 15–24 have tertiary education, so rates for that group, and especially by sex, are less precise.