View difference between Paste ID: QJribsip and XkrBRDvh
SHOW: | | - or go back to the newest paste.
1
# Data sources:
2
# New study permits: 40k-60k
3
# New work permits: 280k-360k
4
# Study permits (new): capped at 437k for 2025. H1 likely 40k-60k new approvals (cap + seasonality).
5
# Work permits (new): ~825k finalized Jan-Jul, but 40-50% are renewals. H1 ~280k-360k new approvals.
6
# Combined H1 2025 “new” study+work ≈ 320k-420k.
7
# StatsCan shows NPR stock fell in Q1 2025, meaning exits/extensions outweighed new arrivals. 
8
# https://www.canada.ca/en/immigration-refugees-citizenship/corporate/mandate/corporate-initiatives/levels/inventories-backlogs.html
9
# https://www.canada.ca/en/immigration-refugees-citizenship/news/notices/2025-provincial-territorial-allocations-under-international-student-cap.html https://open.canada.ca/data/en/dataset/9b34e712-513f-44e9-babf-9df4f7256550
10
# https://open.canada.ca/data/en/dataset/90115b00-f9b8-49e8-afa3-b4cff8facaee
11
# https://open.canada.ca/data/en/dataset/360024f2-17e9-4558-bfc1-3616485d65b9
12
# https://www150.statcan.gc.ca/t1/tbl1/en/tv.action?pid=1710012101
13
# https://www150.statcan.gc.ca/n1/daily-quotidien/250618/dq250618a-eng.htm
14
# Births: https://www150.statcan.gc.ca/t1/tbl1/en/dtl!downloadDbLoadingData-nonTraduit.action?pid=1310041501&latestN=0&startDate=20000101&endDate=99990101&csvLocale=en&selectedMembers=%5B%5B1%5D%2C%5B%5D%2C%5B1%5D%5D&checkedLevels=1D1%2C1D2 (https://www150.statcan.gc.ca/t1/tbl1/en/tv.action?pid=1310041501) - 351,878
15
# Deaths: https://www150.statcan.gc.ca/t1/tbl1/en/dtl!downloadDbLoadingData-nonTraduit.action?pid=1310070801&latestN=0&startDate=20000101&endDate=99990101&csvLocale=en&selectedMembers=%5B%5B1%5D%2C%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%5D%2C%5B1%2C2%5D%5D&checkedLevels= (https://www150.statcan.gc.ca/t1/tbl1/en/tv.action?pid=1310070801) - 326,215 
16
17
import matplotlib.pyplot as plt
18
from matplotlib.lines import Line2D
19
20
TOTAL_POP = 41_290_000
21
BIRTHS = 351_878
22
DEATHS = 326_215
23
OBSERVED_POP_CHANGE = 20_107
24
NATURAL_INCREASE = BIRTHS - DEATHS
25
26
NEW_STUDY_RANGE = (40_000, 60_000)
27
NEW_WORK_RANGE  = (280_000, 360_000)
28
RENEWED_WORK_RANGE = (330_000, 410_000)
29
30
STUDENT_EXPIRY_SHARE_RANGE = (0.30, 0.40)
31
WORK_EXPIRY_SHARE_RANGE    = (0.60, 0.70)
32
33
def midpoint(lo: int, hi: int) -> float:
34
    return (lo + hi) / 2.0
35
36
def halfspan(lo: int, hi: int) -> float:
37
    return (hi - lo) / 2.0
38
39
NEW_VISAS_RANGE = (NEW_STUDY_RANGE[0] + NEW_WORK_RANGE[0],
40
                   NEW_STUDY_RANGE[1] + NEW_WORK_RANGE[1])
41
42
EXPIRIES_TOTAL_RANGE = (
43
    NATURAL_INCREASE + NEW_VISAS_RANGE[0] + RENEWED_WORK_RANGE[0] - OBSERVED_POP_CHANGE,
44
    NATURAL_INCREASE + NEW_VISAS_RANGE[1] + RENEWED_WORK_RANGE[1] - OBSERVED_POP_CHANGE
45
)
46
47
EXPIRING_STUDENTS_RANGE = (
48
    int(EXPIRIES_TOTAL_RANGE[0] * STUDENT_EXPIRY_SHARE_RANGE[0]),
49
    int(EXPIRIES_TOTAL_RANGE[1] * STUDENT_EXPIRY_SHARE_RANGE[1]),
50
)
51
EXPIRING_WORK_RANGE = (
52
    int(EXPIRIES_TOTAL_RANGE[0] * WORK_EXPIRY_SHARE_RANGE[0]),
53
    int(EXPIRIES_TOTAL_RANGE[1] * WORK_EXPIRY_SHARE_RANGE[1]),
54
)
55
56
categories = [
57
    "Births",
58
    "Deaths",
59-
    "New Visas (study+work)",
59+
    "New Study Visas",
60
    "New Work Visas",
61
    "Renewed Work Permits",
62
    "Expiring Student Visas",
63
    "Expiring Work Permits",
64
    "Population Change",
65
]
66
67
values = [
68
    float(BIRTHS),
69-
    midpoint(*NEW_VISAS_RANGE),
69+
70
    midpoint(*NEW_STUDY_RANGE),
71
    midpoint(*NEW_WORK_RANGE),
72
    midpoint(*RENEWED_WORK_RANGE),
73
    -midpoint(*EXPIRING_STUDENTS_RANGE),
74
    -midpoint(*EXPIRING_WORK_RANGE),
75
    float(OBSERVED_POP_CHANGE),
76
]
77
78
yerr = [
79-
    halfspan(*NEW_VISAS_RANGE),
79+
80
    0.0,
81
    halfspan(*NEW_STUDY_RANGE),
82
    halfspan(*NEW_WORK_RANGE),
83
    halfspan(*RENEWED_WORK_RANGE),
84
    halfspan(*EXPIRING_STUDENTS_RANGE),
85
    halfspan(*EXPIRING_WORK_RANGE),
86
    0.0,
87-
    "#2ca02c", "#d62728", "#1f77b4",
87+
88-
    "#9467bd", "#ff7f0e", "#8c564b", "#7f7f7f"
88+
89
colors = [
90
    "#2ca02c",  # Births
91-
plt.figure(figsize=(13, 7))
91+
    "#d62728",  # Deaths
92
    "#17becf",  # New Study (teal)
93
    "#1f77b4",  # New Work (blue)
94
    "#9467bd",  # Renewed Work (purple)
95
    "#ff7f0e",  # Expiring Student (orange)
96
    "#8c564b",  # Expiring Work (brown)
97
    "#7f7f7f",  # Pop change (grey)
98
]
99
100
plt.figure(figsize=(14, 7.5))
101
bars = plt.bar(categories, values, color=colors)
102
103
for i, (v, e) in enumerate(zip(values, yerr)):
104
    if e > 0:
105
        plt.errorbar(i, v, yerr=e, fmt='none', capsize=6, elinewidth=1.5, ecolor='black')
106
        plt.scatter([i], [v], s=40, marker='D', edgecolor='black', zorder=3)
107
108
pad = 120_000
109
for bar, v, e in zip(bars, values, yerr):
110
    x = bar.get_x() + bar.get_width() / 2
111
    va = 'bottom' if v >= 0 else 'top'
112
    if e > 0:
113
        lo, hi = v - e, v + e
114
        txt = f"central estimate*: {int(round(v)):,}\nrange: {int(round(lo)):,}-{int(round(hi)):,}"
115
    else:
116
        txt = f"{int(round(abs(v))):,}"
117
    y = v + (pad if v >= 0 else -pad)
118
    plt.text(x, y, txt, ha='center', va=va, fontsize=9)
119
120
def add_group_bracket(ax, x0, x1, y, label, invert=False):
121-
nv_idx, rw_idx = 2, 3
121+
122-
expS_idx, expW_idx = 4, 5
122+
123
        ax.text((x0+x1)/2, y+55_000, label, ha='center', va='bottom', fontsize=10)
124-
y_nv = values[nv_idx] + yerr[nv_idx]
124+
125
        ax.plot([x0, x0, x1, x1], [y, y-40_000, y-40_000, y], lw=1.5, color='black')
126-
y_expS = values[expS_idx] - yerr[expS_idx]
126+
127
128
ax = plt.gca()
129-
y_group_top = max(y_nv, y_rw) + 200_000
129+
130-
sum_new_renewed = int(round(values[nv_idx] + values[rw_idx]))
130+
ns_idx, nw_idx, rw_idx = 2, 3, 4
131-
add_group_bracket(ax, nv_idx, rw_idx, y_group_top, f"New+Renewed visas (sum): {sum_new_renewed:,}")
131+
expS_idx, expW_idx = 5, 6
132
133-
y_group_bottom = min(y_expS, y_expW) - 220_000
133+
y_ns = values[ns_idx] + yerr[ns_idx]
134-
sum_expiries = int(round(abs(values[expS_idx] + values[expW_idx])))
134+
y_nw = values[nw_idx] + yerr[nw_idx]
135-
add_group_bracket(ax, expS_idx, expW_idx, y_group_bottom, f"All expiries (sum): {sum_expiries:,}", invert=True)
135+
136
y_expS = values[expS_idx] - yerr[expS_idx]  # negative
137
y_expW = values[expW_idx] - yerr[expW_idx]
138
139
y_group_new = max(y_ns, y_nw) + 180_000
140
sum_new = int(round(values[ns_idx] + values[nw_idx]))
141
add_group_bracket(ax, ns_idx, nw_idx, y_group_new, f"New visas (Study+Work sum): {sum_new:,}")
142
143
y_group_new_renewed = max(y_group_new + 220_000, max(y_ns, y_nw, y_rw) + 260_000)
144-
plt.title("Population Components: Canada (H1 2025)\nRanges shown with central estimate*", pad=16)
144+
sum_new_renewed = int(round(values[ns_idx] + values[nw_idx] + values[rw_idx]))
145
add_group_bracket(ax, ns_idx, rw_idx, y_group_new_renewed, f"New (Study+Work) + Renewed Work (sum): {sum_new_renewed:,}")
146
147-
upper = max((v + e) for v, e in zip(values, yerr)) * 1.7 + 200_000
147+
y_group_exp = min(y_expS, y_expW) - 220_000
148-
lower = min((v - e) for v, e in zip(values, yerr)) * 1.7 - 200_000
148+
sum_exp = int(round(abs(values[expS_idx] + values[expW_idx])))
149
add_group_bracket(ax, expS_idx, expW_idx, y_group_exp, f"All expiries (sum): {sum_exp:,}", invert=True)
150
151
legend_elems = [
152
    Line2D([0], [0], color='black', lw=1.5, label='Range (vertical line)'),
153
    Line2D([0], [0], marker='D', color='w', markeredgecolor='black', label='Central estimate*', markersize=7),
154
]
155
plt.legend(handles=legend_elems, loc='upper right')
156
157
plt.axhline(0, linewidth=0.9, color='black')
158
plt.title("Population Components: Canada (H1 2025)\nNew visas split into Study vs Work; ranges shown with central estimate*", pad=16)
159
plt.ylabel("Count (people)")
160-
plt.show()
160+
161
upper = max((v + e) for v, e in zip(values, yerr)) * 1.75 + 200_000
162-
162+
lower = min((v - e) for v, e in zip(values, yerr)) * 1.75 - 250_000
163
plt.ylim(lower, upper)
164
165
plt.xticks(rotation=10)
166
plt.tight_layout()
167
168
plt.figtext(
169
    0.5, 0.01,
170
    "*Estimate based on IRCC/StatCan data and a heuristic expiry split (30-40% students, 60-70% workers).",
171
    ha="center", fontsize=9, style="italic"
172
)
173
174
plt.show()