fields_b = fields.copy()
fields_b["family"] = fields_b["field"].map(family)
short = {"Data Science & ML": "Data Science & ML", "Data Engineering": "Data Engineering",
"Data Analytics & BI": "Data Analytics & BI", "Software Engineering": "Software Eng.",
"IT & Cloud Infrastructure": "IT & Cloud", "Cybersecurity": "Cybersecurity",
"Product & Project Management": "Product & Project Mgmt", "Business Analysis & Consulting": "Business Analysis",
"Finance & Accounting": "Finance & Accounting", "Marketing & Sales": "Marketing & Sales",
"All other occupations": "All other occupations"}
label_pos = {"Business Analysis & Consulting": "middle left", "Finance & Accounting": "bottom center",
"Software Engineering": "bottom center", "Data Analytics & BI": "bottom center",
"Cybersecurity": "middle left"}
fig = go.Figure()
for fam, color in family_color.items():
sub = fields_b[fields_b["family"] == fam]
fig.add_trace(go.Scatter(
x=sub["postings"], y=sub["median"], mode="markers+text", name=fam,
text=sub["field"].map(short), textposition=[label_pos.get(f_, "top center") for f_ in sub["field"]],
textfont=dict(size=11),
marker=dict(color=color, opacity=0.85, size=14 + 70 * np.sqrt(sub["ai_share_postings"]), line=dict(color="white", width=1.5)),
customdata=np.stack([sub["ai_share_postings"] * 100, sub["salary_postings"], sub["remote_or_hybrid_share_stated"] * 100], axis=1),
hovertemplate="<b>%{text}</b><br>Postings: %{x:,.0f}<br>Median salary: $%{y:,.0f}<br>AI-related share: %{customdata[0]:.0f}%<br>Remote or hybrid (of those that state it): %{customdata[2]:.0f}%<extra></extra>"))
fig.update_xaxes(title="Valid postings (log scale)", type="log", range=[np.log10(1800), np.log10(1000000)],
tickvals=[2000, 5000, 10000, 20000, 50000, 100000, 500000],
ticktext=["2,000", "5,000", "10,000", "20,000", "50,000", "100,000", "500,000"])
fig.update_yaxes(title="Median annual salary (USD)", tickprefix="$", tickformat=",.0f", range=[20000, 230000])
style(fig, "Demand vs. Pay by Occupation Field",
"Bubble size = AI-related share of postings | top = better paid, right = more openings", height=620, legend=True)
fig.update_layout(legend=dict(orientation="h", x=0.5, xanchor="center", y=-0.14, yanchor="top"), margin=dict(b=120))
show(fig)