Guest User

Untitled

a guest
Aug 24th, 2025
52
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 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.     -float(DEATHS),
  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.     0.0,
  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. ]
  88.  
  89. colors = [
  90.     "#2ca02c",  # Births
  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.     if not invert:
  122.         ax.plot([x0, x0, x1, x1], [y, y+40_000, y+40_000, y], lw=1.5, color='black')
  123.         ax.text((x0+x1)/2, y+55_000, label, ha='center', va='bottom', fontsize=10)
  124.     else:
  125.         ax.plot([x0, x0, x1, x1], [y, y-40_000, y-40_000, y], lw=1.5, color='black')
  126.         ax.text((x0+x1)/2, y-55_000, label, ha='center', va='top', fontsize=10)
  127.  
  128. ax = plt.gca()
  129.  
  130. ns_idx, nw_idx, rw_idx = 2, 3, 4
  131. expS_idx, expW_idx = 5, 6
  132.  
  133. y_ns = values[ns_idx] + yerr[ns_idx]
  134. y_nw = values[nw_idx] + yerr[nw_idx]
  135. y_rw = values[rw_idx] + yerr[rw_idx]
  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. 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. y_group_exp = min(y_expS, y_expW) - 220_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.  
  161. upper = max((v + e) for v, e in zip(values, yerr)) * 1.75 + 200_000
  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()
Advertisement
Add Comment
Please, Sign In to add comment