View difference between Paste ID: XkrBRDvh and xfjBCcm9
SHOW: | | - or go back to the newest paste.
1
# Data sources:
2-
# New study permits: 40k–60k
2+
# New study permits: 40k-60k
3-
# New work permits: 280k–360k
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-
import numpy as np
19+
20
TOTAL_POP = 41_290_000
21
BIRTHS = 351_878
22-
total_population = 41_290_000
22+
DEATHS = 326_215
23-
births = 351_878
23+
OBSERVED_POP_CHANGE = 20_107
24-
deaths = 326_215
24+
NATURAL_INCREASE = BIRTHS - DEATHS
25-
observed_pop_change = 20_107
25+
26-
natural_increase = births - deaths
26+
NEW_STUDY_RANGE = (40_000, 60_000)
27
NEW_WORK_RANGE  = (280_000, 360_000)
28-
new_study_range = (40_000, 60_000)
28+
RENEWED_WORK_RANGE = (330_000, 410_000)
29-
new_work_range  = (280_000, 360_000)
29+
30-
renewed_work_range = (330_000, 410_000)
30+
STUDENT_EXPIRY_SHARE_RANGE = (0.30, 0.40)
31-
new_visas_range = (new_study_range[0] + new_work_range[0], new_study_range[1] + new_work_range[1])
31+
WORK_EXPIRY_SHARE_RANGE    = (0.60, 0.70)
32
33-
expiries_total_range = (
33+
def midpoint(lo: int, hi: int) -> float:
34-
    natural_increase + new_visas_range[0] + renewed_work_range[0] - observed_pop_change,
34+
    return (lo + hi) / 2.0
35-
    natural_increase + new_visas_range[1] + renewed_work_range[1] - observed_pop_change
35+
36
def halfspan(lo: int, hi: int) -> float:
37
    return (hi - lo) / 2.0
38-
student_share_range = (0.30, 0.40)
38+
39-
work_share_range    = (0.60, 0.70)
39+
NEW_VISAS_RANGE = (NEW_STUDY_RANGE[0] + NEW_WORK_RANGE[0],
40
                   NEW_STUDY_RANGE[1] + NEW_WORK_RANGE[1])
41-
expiring_students_range = (int(expiries_total_range[0] * student_share_range[0]),
41+
42-
                           int(expiries_total_range[1] * student_share_range[1]))
42+
EXPIRIES_TOTAL_RANGE = (
43-
expiring_work_range = (int(expiries_total_range[0] * work_share_range[0]),
43+
    NATURAL_INCREASE + NEW_VISAS_RANGE[0] + RENEWED_WORK_RANGE[0] - OBSERVED_POP_CHANGE,
44-
                       int(expiries_total_range[1] * work_share_range[1]))
44+
    NATURAL_INCREASE + NEW_VISAS_RANGE[1] + RENEWED_WORK_RANGE[1] - OBSERVED_POP_CHANGE
45
)
46-
def midpoint(lo, hi): return (lo + hi) / 2.0
46+
47-
def halfspan(lo, hi): return (hi - lo) / 2.0
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-
values = np.array([
59+
60-
    births,
60+
61-
    -deaths,
61+
62-
    midpoint(*new_visas_range),
62+
63-
    midpoint(*renewed_work_range),
63+
64-
    -midpoint(*expiring_students_range),
64+
65-
    -midpoint(*expiring_work_range),
65+
66-
    observed_pop_change
66+
values = [
67-
], dtype=float)
67+
    float(BIRTHS),
68
    -float(DEATHS),
69-
yerr = np.array([
69+
    midpoint(*NEW_VISAS_RANGE),
70-
    0,
70+
    midpoint(*RENEWED_WORK_RANGE),
71-
    0,
71+
    -midpoint(*EXPIRING_STUDENTS_RANGE),
72-
    halfspan(*new_visas_range),
72+
    -midpoint(*EXPIRING_WORK_RANGE),
73-
    halfspan(*renewed_work_range),
73+
    float(OBSERVED_POP_CHANGE),
74-
    halfspan(*expiring_students_range),
74+
75-
    halfspan(*expiring_work_range),
75+
76-
    0
76+
yerr = [
77-
], dtype=float)
77+
    0.0,
78
    0.0,
79
    halfspan(*NEW_VISAS_RANGE),
80
    halfspan(*RENEWED_WORK_RANGE),
81
    halfspan(*EXPIRING_STUDENTS_RANGE),
82
    halfspan(*EXPIRING_WORK_RANGE),
83
    0.0,
84-
plt.figure(figsize=(13,7))
84+
85
86
colors = [
87
    "#2ca02c", "#d62728", "#1f77b4",
88
    "#9467bd", "#ff7f0e", "#8c564b", "#7f7f7f"
89
]
90
91
plt.figure(figsize=(13, 7))
92
bars = plt.bar(categories, values, color=colors)
93
94-
    x = bar.get_x() + bar.get_width()/2
94+
95
    if e > 0:
96
        plt.errorbar(i, v, yerr=e, fmt='none', capsize=6, elinewidth=1.5, ecolor='black')
97
        plt.scatter([i], [v], s=40, marker='D', edgecolor='black', zorder=3)
98-
        txt = f"central estimate*: {int(round(v)):,}\nrange: {int(round(lo)):,}–{int(round(hi)):,}"
98+
99
pad = 120_000
100
for bar, v, e in zip(bars, values, yerr):
101
    x = bar.get_x() + bar.get_width() / 2
102
    va = 'bottom' if v >= 0 else 'top'
103
    if e > 0:
104
        lo, hi = v - e, v + e
105
        txt = f"central estimate*: {int(round(v)):,}\nrange: {int(round(lo)):,}-{int(round(hi)):,}"
106
    else:
107
        txt = f"{int(round(abs(v))):,}"
108
    y = v + (pad if v >= 0 else -pad)
109
    plt.text(x, y, txt, ha='center', va=va, fontsize=9)
110
111
def add_group_bracket(ax, x0, x1, y, label, invert=False):
112
    if not invert:
113
        ax.plot([x0, x0, x1, x1], [y, y+40_000, y+40_000, y], lw=1.5, color='black')
114
        ax.text((x0+x1)/2, y+55_000, label, ha='center', va='bottom', fontsize=10)
115
    else:
116
        ax.plot([x0, x0, x1, x1], [y, y-40_000, y-40_000, y], lw=1.5, color='black')
117
        ax.text((x0+x1)/2, y-55_000, label, ha='center', va='top', fontsize=10)
118
119
ax = plt.gca()
120
121
nv_idx, rw_idx = 2, 3
122
expS_idx, expW_idx = 4, 5
123
124
y_nv = values[nv_idx] + yerr[nv_idx]
125
y_rw = values[rw_idx] + yerr[rw_idx]
126
y_expS = values[expS_idx] - yerr[expS_idx]
127
y_expW = values[expW_idx] - yerr[expW_idx]
128
129
y_group_top = max(y_nv, y_rw) + 200_000
130
sum_new_renewed = int(round(values[nv_idx] + values[rw_idx]))
131
add_group_bracket(ax, nv_idx, rw_idx, y_group_top, f"New+Renewed visas (sum): {sum_new_renewed:,}")
132-
    Line2D([0], [0], marker='D', color='w', markeredgecolor='black', label='Central estimate*', markersize=7)
132+
133
y_group_bottom = min(y_expS, y_expW) - 220_000
134
sum_expiries = int(round(abs(values[expS_idx] + values[expW_idx])))
135
add_group_bracket(ax, expS_idx, expW_idx, y_group_bottom, f"All expiries (sum): {sum_expiries:,}", invert=True)
136
137
legend_elems = [
138
    Line2D([0], [0], color='black', lw=1.5, label='Range (vertical line)'),
139
    Line2D([0], [0], marker='D', color='w', markeredgecolor='black', label='Central estimate*', markersize=7),
140-
upper = max(values + yerr) * 1.7 + 200_000
140+
141-
lower = min(values - yerr) * 1.7 - 200_000
141+
142
143
plt.axhline(0, linewidth=0.9, color='black')
144
plt.title("Population Components: Canada (H1 2025)\nRanges shown with central estimate*", pad=16)
145
plt.ylabel("Count (people)")
146
147
upper = max((v + e) for v, e in zip(values, yerr)) * 1.7 + 200_000
148
lower = min((v - e) for v, e in zip(values, yerr)) * 1.7 - 200_000
149
plt.ylim(lower, upper)
150
151
plt.xticks(rotation=10)
152
plt.tight_layout()
153
154
plt.figtext(
155
    0.5, 0.01,
156
    "*Estimate based on IRCC/StatCan data and a heuristic expiry split (30-40% students, 60-70% workers).",
157
    ha="center", fontsize=9, style="italic"
158
)
159
160
plt.show()
161
162