Storytelling with data: A Policy Brief and a Teaching Guide¶
This document has a dual purpose. It is written as a policy brief, using real data on racism and inclusion to highlight gaps between optimism, trust, and lived experience. At the same time, it serves as a teaching guide, showing step by step how graphs and infographics can be created in Python and then transformed into storytelling devices. The combination allows readers not only to understand the findings, but also to learn the methods behind them—so that the same tools can be applied in other contexts of social decision-making.
Apart from their purpose in designing graphs for storytelling, the data that was used in the current deliverable reveal interesting insights. Firstly, they reveal an underlying optimism of the respondents with regards to optimism of institutions and programs to combat racism and discrimination, which can be attributed to the relatively young age of the majority of the respondents.
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import numpy as np
# --------------------------
# 1) Institutional trust data
# --------------------------
support_counts = {1: 2, 2: 4, 3: 29, 4: 33, 5: 69}
prog_counts_raw = {'No': 17, 'No, Not sure': 1, 'Not sure': 32, 'Yes': 86, 'Yes, No': 1}
effective_counts = {1: 1, 2: 4, 3: 26, 4: 51, 5: 55}
def likert_to_buckets(counts):
total = sum(counts.values())
neg = counts.get(1, 0) + counts.get(2, 0)
neu = counts.get(3, 0)
pos = counts.get(4, 0) + counts.get(5, 0)
return neg, neu, pos, total
def yns_to_buckets(counts):
neg = counts.get('No', 0)
pos = counts.get('Yes', 0)
neu = counts.get('Not sure', 0) + counts.get('Yes, No', 0) + counts.get('No, Not sure', 0)
total = neg + neu + pos
return neg, neu, pos, total
def to_pct(neg, neu, pos, total):
f = 100.0 / total if total else 0
return neg*f, neu*f, pos*f
sN, sU, sP = to_pct(*likert_to_buckets(support_counts))
pN, pU, pP = to_pct(*yns_to_buckets(prog_counts_raw))
eN, eU, eP = to_pct(*likert_to_buckets(effective_counts))
labels = [
"Support for integration",
"Programmes address racism",
"Effectiveness on inclusion"
]
rows = [(sN, sU, sP), (pN, pU, pP), (eN, eU, eP)]
# Consistent colors
NEG_COLOR = "#d73027" # negative (red)
NEU_COLOR = "#f7f7f7" # neutral (light gray)
POS_COLOR = "#2b8cbe" # positive (blue)
# --------------------------
# 2) Age distribution data
# --------------------------
ages = [
24,20,22,22,18,21,19,25,20,20,21,19,20,18,24,22,19,23,22,21,
21,21,20,20,19,18,19,21,20,22,21,20,20,20,20,20,20,19,20,20,
20,20,20,23,26,25,20,21,22,20,23,19,20,22,20,19,20,20,19,20,
19,20,21,20,20,21,20,18,21,19,20,19,20,20,23,22,21,22,20,20,
20,19,20,20,20,19,20,22,20,20,20,20,19,20,20,21,20,22,20,20,
19,20,22,21,21,23,21,20,20,20,20,22,19,19,21,22,24,19,19,22,
28,20,20,21,20,19,20,22,18,20,22,19,20
]
ages_arr = np.array(ages, dtype=int)
bins = np.arange(ages_arr.min()-0.5, ages_arr.max()+1.5, 1) # 1-year bins
counts, bin_edges = np.histogram(ages_arr, bins=bins)
centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0
# Smooth histogram with a simple moving average
kernel = np.array([1, 2, 1], dtype=float)
kernel = kernel / kernel.sum()
smooth = np.convolve(counts, kernel, mode='same').astype(float)
if smooth.max() > 0:
smooth = smooth / smooth.max() # normalize 0–1
# Mode age (dominant)
unique, cts = np.unique(ages_arr, return_counts=True)
mode_age = int(unique[np.argmax(cts)]) # likely 20 or 21
# --------------------------
# Plot: main chart + inset
# --------------------------
fig, ax = plt.subplots(figsize=(12, 6))
# Diverging stacked bars
ypos = list(range(len(rows)))[::-1]
for y, (neg, neu, pos) in zip(ypos, rows):
ax.barh(y, -neg, left=0, align='center', color=NEG_COLOR, edgecolor='none')
ax.barh(y, neu, left=0, align='center', color=NEU_COLOR, edgecolor='none')
ax.barh(y, pos, left=neu, align='center', color=POS_COLOR, edgecolor='none')
ax.set_yticks(ypos)
ax.set_yticklabels(labels)
ax.set_xlim(-100, 100)
ax.set_xlabel("Share of respondents (%)")
ax.axvline(0, linewidth=1, color="#999999")
ax.set_title("Institutional perceptions")
# Segment annotations
def annotate(x_left, width, y, text):
if abs(width) < 4: return
ax.text(x_left + width/2, y, text, va='center', ha='center', fontsize=9)
for y, (neg, neu, pos) in zip(ypos, rows):
annotate(-neg, neg, y, f"{neg:.0f}%")
annotate(0, neu, y, f"{neu:.0f}%")
annotate(neu, pos, y, f"{pos:.0f}%")
# Legend
handles = [
Patch(facecolor=NEG_COLOR, edgecolor='none', label="Negative (1–2 or No)"),
Patch(facecolor=NEU_COLOR, edgecolor='none', label="Neutral (3 or Not sure / mixed)"),
Patch(facecolor=POS_COLOR, edgecolor='none', label="Positive (4–5 or Yes)")
]
ax.legend(handles=handles, loc="upper center", ncol=3, bbox_to_anchor=(0.5, 1.12), frameon=False)
# --------------------------
# Inset: small age distribution on the right
# --------------------------
mini = ax.inset_axes([0.15, 0.15, 0.22, 0.55]) # x0, y0, width, height
mini.set_facecolor("white")
mini.plot(centers, smooth, color=POS_COLOR, lw=2)
# Only one x tick at dominant age
mini.set_xticks([mode_age])
mini.set_xticklabels([str(mode_age)])
# No y ticks; vertical label
mini.set_yticks([])
mini.set_ylabel("age distribution", rotation=90)
# Clean look
for spine in ["top", "right"]:
mini.spines[spine].set_visible(False)
mini.spines["bottom"].set_alpha(0.4)
mini.spines["left"].set_alpha(0.4)
mini.set_title("Age distribution", pad=6, fontsize=10)
plt.tight_layout()
plt.show()
Despite this optimism though, experiences of discrimination are not absent. In fact 1 in 5 respondents declared an experience of discrimination in various settings (that have the trust of the majority of the respondents), and only 3.6% of those that experienced discrimination asked or received assistance to deal with the ramifications of the incidence.
import matplotlib.pyplot as plt
# --- Inline survey data (exact tallies) ---
discr_experience = {"No": 109, "Yes": 26, "Yes, No": 2}
n_total = sum(discr_experience.values())
n_yes = 26 + 2 # count "Yes" + mixed
# Context percentages among the 28 "Yes"
context_info = [
"Of those who responded Yes:",
"57.1% experienced discrimination in a public space",
"46.5% experienced discrimination in an educational setting",
"39.3% experienced discrimination in Social media",
"7.1% experienced discrimination in a football setting"
]
# --- Donut chart ---
fig, ax = plt.subplots(figsize=(8, 6))
sizes = list(discr_experience.values())
labels = list(discr_experience.keys())
# Consistent color scheme: blue = No, red = Yes, gray = Yes/No
colors = ["#1f77b4", "#d62728", "#bdbdbd"]
wedges, _ = ax.pie(sizes, startangle=90, colors=colors)
# Donut hole
centre_circle = plt.Circle((0, 0), 0.60, fc="white")
ax.add_artist(centre_circle)
# Title
ax.set_title("Experienced racial discrimination?")
# Central text
pct_yes = round(100.0 * n_yes / n_total, 1)
ax.text(0, 0.07, f"{pct_yes}%", ha="center", va="center", fontsize=12, weight="bold")
ax.text(0, -0.06, "reported ‘Yes’", ha="center", va="center", fontsize=11)
ax.text(0, -0.20, "3.6% of ‘Yes’ received assistance",
ha="center", va="center", fontsize=9, color="darkred")
# Legend box positioned near the "Yes" slice (red, right side)
legend_text = "\n".join(context_info)
ax.text(1.2, 0, legend_text,
ha="left", va="center",
fontsize=10,
bbox=dict(boxstyle="round,pad=0.5", fc="white", ec="0.5"))
ax.set_aspect('equal')
plt.tight_layout()
plt.show()
Especially football and sports, which are celebrated as a tool against racism, remain as sites of active discrimination experiences.
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
# -----------------------
# Inline data (as given)
# -----------------------
program_groups = {
"Sport as tool for integration": 1, # highlighted
"Community forums & dialogue": 1,
"Support for vulnerable groups": 1,
"Awareness-raising": 1,
"Education & training": 1
}
sport_counts = {"Yes": 126, "No": 11} # survey belief in sport as integration
# Contexts among those who experienced discrimination (share within 'Yes' group)
# (pulled from your text list; written directly to avoid string parsing)
contexts_pct = {
"Public space": 57.1,
"Educational setting": 46.5,
"Social media": 39.3,
"Football setting": 7.1 # highlight this
}
# -----------------------
# Look & feel
# -----------------------
HIGHLIGHT = "#2b8cbe" # vivid blue
PALE = "#cdd9e5" # pale blue/gray for toned-down bars
NO_TONE = "#c7c7c7" # toned-down gray for "No"
PIE_PALE = "#e6e6e6" # pale for non-highlighted pie slices
PIE_HI = "#d73027" # highlight color for football slice (stands out)
# -----------------------
# Layout: 2x2 grid (bottom spans both columns)
# -----------------------
fig = plt.figure(figsize=(12.5, 8), constrained_layout=True)
gs = fig.add_gridspec(2, 2, height_ratios=[1, 1.1])
# --- Top-left: Program groups (highlight 'Sport') ---
ax0 = fig.add_subplot(gs[0, 0])
labels_pg = list(program_groups.keys())
values_pg = list(program_groups.values())
colors_pg = [HIGHLIGHT if "Sport" in lab else PALE for lab in labels_pg]
ax0.barh(labels_pg, values_pg, color=colors_pg)
ax0.set_xlim(0, 1.2)
ax0.set_xticks([])
ax0.set_title("Anti-racism program types", pad=6)
# Add inline labels on bars for a clean, infographic feel
for i, lab in enumerate(labels_pg):
ax0.text(0.05, i, lab, va="center", ha="left", fontsize=10, color="black")
# --- Top-right: Yes/No belief (Yes highlighted, No toned down) ---
ax1 = fig.add_subplot(gs[0, 1])
yn_labels = list(sport_counts.keys())
yn_vals = list(sport_counts.values())
yn_colors = [HIGHLIGHT if lab == "Yes" else NO_TONE for lab in yn_labels]
bars = ax1.bar(yn_labels, yn_vals, color=yn_colors)
ax1.set_title("Do students see sport as an effective anti-racism tool?", pad=6)
ax1.set_ylabel("Number of respondents")
# Annotate counts above bars
for b in bars:
ax1.text(b.get_x() + b.get_width()/2, b.get_height() + 0.8,
f"{int(b.get_height())}", ha="center", va="bottom", fontsize=10)
# --- Bottom: Pie (contexts) — only 'Football' highlighted ---
ax2 = fig.add_subplot(gs[1, :])
# Order slices so football can be emphasized; keep original order otherwise
ctx_items = list(contexts_pct.items())
labels_ctx = [k for k, _ in ctx_items]
values_ctx = [v for _, v in ctx_items]
colors_ctx = [PIE_HI if "Football" in lab else PIE_PALE for lab in labels_ctx]
wedges, texts = ax2.pie(values_ctx, colors=colors_ctx, startangle=90, labels=None)
# Central title for the pie
ax2.set_title("Where discrimination occurs (among those who reported it)", pad=8)
# Build a minimalist legend, showing football vs. others
legend_handles = [
Patch(facecolor=PIE_HI, label="Football setting"),
Patch(facecolor=PIE_PALE, label="Other contexts")
]
ax2.legend(handles=legend_handles, loc="center left", bbox_to_anchor=(0.02, 0.5), frameon=False)
# Add percentage labels only for the football slice; keep others unlabeled for a clean look
# Find the football wedge index
fb_idx = labels_ctx.index("Football setting")
# Compute the angle to position the label nicely
# Matplotlib returns wedges in order; place a label near the wedge edge
fb_wedge = wedges[fb_idx]
angle = (fb_wedge.theta2 + fb_wedge.theta1) / 2
# Position label slightly outside
import math
r = 1.1
x = r * math.cos(math.radians(angle))
y = r * math.sin(math.radians(angle))
ax2.text(x, y, "7.1%", ha="center", va="center", fontsize=11, weight="bold")
plt.show()
These insights reveal interesting implications. For public institutions and universities, optimism alone cannot be considered an effective policy. On the contrary, these organizations should establish mechanisms and frameworks to increase the trust of people and students, so that whenever someone experiences discrimination, they could have the institutional support to report it and mitigate the personal ramifications. In addition, sports programs have strong symbolic value, but they need as well mechanisms to transform this immense value into action that actively reduces racism and discrimination. Finally, demographic realities cannot be independent of the discussion on how to combat racism.
import matplotlib.pyplot as plt
# Values
women_share = 57.0 # inner ring (blue)
severe_poverty = 16.7 # outer ring (red)
# Style
START = 90
OUTER_R, OUTER_W = 1.3, 0.28 # outer radius, width
INNER_R, INNER_W = 0.85, 0.28 # inner radius, width
BLUE_HI, BLUE_REST = "#1f77b4", "#dbe9f6"
RED_HI, RED_REST = "#d62728", "#f8c9c9"
fig, ax = plt.subplots(figsize=(7.2, 7.2))
# --- OUTER donut: migrant women in severe poverty (16.7% highlighted) ---
outer_vals = [severe_poverty, 100 - severe_poverty]
outer_colors = [RED_HI, RED_REST]
ax.pie(outer_vals,
radius=OUTER_R,
startangle=START,
colors=outer_colors,
counterclock=False,
wedgeprops=dict(width=OUTER_W, edgecolor="white"))
# --- INNER donut: women among new nationals (57% highlighted) ---
inner_vals = [women_share, 100 - women_share]
inner_colors = [BLUE_HI, BLUE_REST]
ax.pie(inner_vals,
radius=INNER_R,
startangle=START,
colors=inner_colors,
counterclock=False,
wedgeprops=dict(width=INNER_W, edgecolor="white"))
# Cosmetics
ax.set(aspect="equal")
ax.axis("off")
ax.set_title("Demographic indicators", pad=10, fontsize=14, weight="bold")
# Legends (right side)
from matplotlib.patches import Patch
handles = [
Patch(facecolor=RED_HI, label=f"Migrant women in severe poverty (Spain): {severe_poverty:.1f}%"),
Patch(facecolor=BLUE_HI, label=f"Women among new nationals (Spain): {women_share:.0f}%"),
]
ax.legend(handles=handles, loc="center left", bbox_to_anchor=(1.05, 0.0), frameon=False)
plt.tight_layout()
plt.show()
The context must inform and guide policy design and program implementation, otherwise any type of decision is in danger of being ineffective from its inception.