Guest User

Untitled

a guest
Aug 24th, 2025
15
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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)",
  60.     "Renewed Work Permits",
  61.     "Expiring Student Visas",
  62.     "Expiring Work Permits",
  63.     "Population Change",
  64. ]
  65.  
  66. values = [
  67.     float(BIRTHS),
  68.     -float(DEATHS),
  69.     midpoint(*NEW_VISAS_RANGE),
  70.     midpoint(*RENEWED_WORK_RANGE),
  71.     -midpoint(*EXPIRING_STUDENTS_RANGE),
  72.     -midpoint(*EXPIRING_WORK_RANGE),
  73.     float(OBSERVED_POP_CHANGE),
  74. ]
  75.  
  76. yerr = [
  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. ]
  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. for i, (v, e) in enumerate(zip(values, yerr)):
  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.  
  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.  
  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. ]
  141. plt.legend(handles=legend_elems, loc='upper right')
  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.  
Advertisement
Add Comment
Please, Sign In to add comment