Démos
Neuf métiers, une seule connexion
Neuf questions de neuf métiers différents, chacune résolue par des appels aux outils de MapleStats. Voyez par vous-même ci-dessous (à voir pour le croire : les résultats sont frappants).
158
fichiers de microdonnées pour les statisticiens, gardiens des poids et des marges,
Le baccalauréat, province par province
La part des adultes de 25 à 64 ans dont le plus haut diplôme est un baccalauréat, estimée à partir du fichier de microdonnées à grande diffusion du recensement de 2021 avec ses poids d'enquête. Chaque barre est un intervalle de confiance à 95 % tiré des 16 poids de réplication du fichier : le plus étroit pour la Colombie-Britannique et le Québec, le plus large pour le Nord canadien et l'Île-du-Prince-Édouard, où l'échantillon est plus petit.
Voir les données
| Province | Estimation | IC à 95 %, borne inf. | IC à 95 %, borne sup. |
|---|---|---|---|
| Terre-Neuve-et-Labrador | 13,3 % | 12,5 % | 14,1 % |
| Île-du-Prince-Édouard | 18,2 % | 16,2 % | 20,1 % |
| Nouvelle-Écosse | 20,1 % | 19,4 % | 20,7 % |
| Nouveau-Brunswick | 16,7 % | 16,0 % | 17,3 % |
| Québec | 18,1 % | 17,9 % | 18,4 % |
| Ontario | 23,8 % | 23,6 % | 24,1 % |
| Manitoba | 20,3 % | 19,8 % | 20,9 % |
| Saskatchewan | 18,5 % | 17,8 % | 19,2 % |
| Alberta | 21,7 % | 21,3 % | 22,1 % |
| Colombie-Britannique | 22,9 % | 22,7 % | 23,1 % |
| Nord canadien | 15,1 % | 13,7 % | 16,4 % |
Les appels à l'origine de ce graphique
call_tool{"name": "statcan_pumf_tabulate", "arguments": {"url": "https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip", "rows": ["PR", "HDGREE"], "statistic": "share", "filters": {"AGEGRP": ["9", "10", "11", "12", "13", "14", "15", "16"], "HDGREE": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"]}}}R
# ============================================================
# StatCan public use microdata file
# Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
# (exact: the PUMF ZIP)
# Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
# Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
# ============================================================
# 0. Setup ----
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
download.file("https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip", "data/raw/cen21_ind_98m0001x_part_rec21.zip", mode = "wb")
zip::unzip("data/raw/cen21_ind_98m0001x_part_rec21.zip", exdir = "data/raw/cen21_ind_98m0001x_part_rec21")
file.remove("data/raw/cen21_ind_98m0001x_part_rec21.zip")
data_files <- list.files("data/raw/cen21_ind_98m0001x_part_rec21", full.names = TRUE, recursive = TRUE)Python
# ============================================================
# StatCan public use microdata file
# Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
# (exact: the PUMF ZIP)
# Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
# Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
# ============================================================
# %% 0. Setup
import zipfile
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "cen21_ind_98m0001x_part_rec21.zip"
# %% 1. Read inputs
with httpx.Client(
http2=True,
follow_redirects=True,
timeout=300,
headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"},
) as client:
response = client.get('https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip')
response.raise_for_status()
raw_path.write_bytes(response.content)
with zipfile.ZipFile(raw_path) as archive:
archive.extractall(RAW_DIR / raw_path.stem)Stata
* ============================================================
* StatCan public use microdata file
* Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
* (exact: the PUMF ZIP)
* Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
* Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/statcan_pumf_tabulate.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import json
import zipfile
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "cen21_ind_98m0001x_part_rec21.zip"
with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.get('https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip')
response.raise_for_status()
raw_path.write_bytes(response.content)
with zipfile.ZipFile(raw_path) as archive: archive.extractall(RAW_DIR / raw_path.stem)
end
log closeJulia
# ============================================================
# StatCan public use microdata file
# Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
# (exact: the PUMF ZIP)
# Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
# Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
# ============================================================
# 0. Setup
using Downloads
mkpath("data/raw")
# 1. Read inputs
Downloads.download("https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip", "data/raw/cen21_ind_98m0001x_part_rec21.zip")67
tableaux d'immigration pour les démographes, qui comptent ceux qui viennent s'établir,
Ceux qui viennent s'établir : Edmonton et Calgary
Les nouveaux résidents permanents qui ont indiqué chaque ville comme destination, par année, d'après les mises à jour mensuelles d'IRCC. *2026 ne couvre que les mois de janvier à juillet. IRCC arrondit chaque compte à un multiple de 5.
Voir les données
| Année | Edmonton | Calgary |
|---|---|---|
| 2015 | 16 745 | 21 715 |
| 2016 | 17 895 | 21 430 |
| 2017 | 15 960 | 17 875 |
| 2018 | 15 750 | 18 950 |
| 2019 | 16 425 | 19 635 |
| 2020 | 8 380 | 10 665 |
| 2021 | 14 760 | 17 870 |
| 2022 | 17 360 | 24 725 |
| 2023 | 21 665 | 27 430 |
| 2024 | 24 075 | 31 165 |
| 2025 | 18 980 | 23 560 |
| 2026* | 10 685 | 13 400 |
Les appels à l'origine de ce graphique
call_tool{"name": "ircc_monthly_query", "arguments": {"table_id": "ODP-PR-PT_CMA", "filters": {"census_metropolitan_area": "Edmonton"}, "period": "year", "year_from": 2015, "lang": "fr"}}call_tool{"name": "ircc_monthly_query", "arguments": {"table_id": "ODP-PR-PT_CMA", "filters": {"census_metropolitan_area": "Calgary"}, "period": "year", "year_from": 2015, "lang": "fr"}}R
# ============================================================
# IRCC monthly update: Canada - Permanent Residents by Province/Territory and CMA
# Purpose: Fetch the data behind MapleStats MCP's ircc_monthly_query
# (exact: the source file, then the tool's filters)
# Inputs: https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv
# Outputs: data/raw/ODP-PR-PT_CMA.csv; the prepared table as `data`
# ============================================================
# 0. Setup ----
library(dplyr)
library(janitor)
library(purrr)
library(readr)
library(stringr)
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
download.file("https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv", "data/raw/ODP-PR-PT_CMA.csv", mode = "wb")
data <- read_delim("data/raw/ODP-PR-PT_CMA.csv", delim = "\t", na = c("", "NA", "--"),
col_types = cols(.default = col_character()))
# 2. Check inputs ----
stopifnot("https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv returned no rows" = nrow(data) > 0)
# 3. Prepare data ----
# Keep the rows the MapleStats tool kept.
data <- data |>
filter(
str_to_lower(str_trim(`EN_CENSUS_METROPOLITAN_AREA`)) == "edmonton",
suppressWarnings(as.numeric(`EN_YEAR`)) >= 2015
)
data <- data |>
clean_names()
# Standard cleaning: trimmed text, empty strings as missing, and numbers
# stored as text converted to numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns <- "^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$"
data <- data |>
mutate(across(where(is.character), \(x) na_if(str_trim(x), "")))
keep_text <- names(data)[
str_detect(names(data), code_columns) |
map_lgl(
data,
\(column) is.character(column) && any(str_detect(column, "^-?0[0-9]"), na.rm = TRUE)
)
]
data <- data |>
mutate(
across(any_of(keep_text), \(x) if (is.list(x)) x else as.character(x)),
across(where(is.character) & !any_of(keep_text), parse_guess)
)Python
# ============================================================
# IRCC monthly update: Canada - Permanent Residents by Province/Territory and CMA
# Purpose: Fetch the data behind MapleStats MCP's ircc_monthly_query
# (exact: the source file, then the tool's filters)
# Inputs: https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv
# Outputs: data/raw/ODP-PR-PT_CMA.csv; the prepared table as `data`
# ============================================================
# %% 0. Setup
import re
import unicodedata
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "ODP-PR-PT_CMA.csv"
# %% 1. Read inputs
with httpx.Client(
http2=True,
follow_redirects=True,
timeout=300,
headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"},
) as client:
response = client.get('https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv')
response.raise_for_status()
raw_path.write_bytes(response.content)
data = pl.read_csv(raw_path, separator='\t', infer_schema=False, null_values=['--'])
# %% 2. Check inputs
assert data.height > 0, "https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv returned no rows"
# %% 3. Prepare data
# Keep the rows the MapleStats tool kept.
data = data.filter(
pl.col('EN_CENSUS_METROPOLITAN_AREA').cast(pl.Utf8).str.strip_chars().str.to_lowercase() == 'edmonton',
pl.col('EN_YEAR').cast(pl.Float64, strict=False) >= 2015,
)
# Standard cleaning: snake_case names as janitor's clean_names() gives them
# in R (PÉRIODE -> periode, referenceNumber -> reference_number, # -> number,
# % -> percent, apostrophes dropped, a leading digit prefixed with x),
# trimmed text, empty strings as missing. Names that clean alike are
# numbered as janitor numbers them (Indicator, indicator -> indicator,
# indicator_2).
clean_names = [
re.sub(
r"[^0-9a-z]+",
"_",
re.sub(
r"([a-z0-9])([A-Z])",
r"\1_\2",
"".join(
" " if unicodedata.category(ch)[0] in "PS" and not ch.isascii() else ch
for ch in unicodedata.normalize("NFKD", column.replace("'", ""))
)
.replace("#", " number ")
.replace("%", " percent ")
.encode("ascii", "ignore")
.decode(),
).lower(),
).strip("_")
or "x"
for column in data.columns
]
clean_names = [f"x{name}" if name[0].isdigit() else name for name in clean_names]
while len(set(clean_names)) < len(clean_names):
name_counts = {}
numbered = []
for name in clean_names:
name_counts[name] = name_counts.get(name, 0) + 1
count = name_counts[name]
numbered.append(name if count == 1 else f"{name}_{count}")
clean_names = numbered
data = data.rename(dict(zip(data.columns, clean_names)))
data = data.with_columns(pl.col(pl.Utf8).str.strip_chars().replace("", None))
# Numbers stored as text become numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns = re.compile(r"^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$")
for column in data.columns:
dtype = data.schema[column]
if code_columns.search(column) and not dtype.is_nested():
data = data.with_columns(pl.col(column).cast(pl.Utf8))
continue
if dtype != pl.Utf8 or data[column].null_count() == data.height:
continue
numbers = data[column].cast(pl.Float64, strict=False)
leading_zero = data[column].str.contains(r"^-?0[0-9]").any()
if numbers.null_count() > data[column].null_count() or leading_zero:
continue
whole = data[column].cast(pl.Int64, strict=False)
data = data.with_columns(whole if whole.null_count() == numbers.null_count() else numbers)Stata
* ============================================================
* IRCC monthly update: Canada - Permanent Residents by Province/Territory and CMA
* Purpose: Fetch the data behind MapleStats MCP's ircc_monthly_query
* (exact: the source file, then the tool's filters)
* Inputs: https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv
* Outputs: data/raw/ODP-PR-PT_CMA.csv; the prepared table as `data`
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/ircc_monthly_query.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import json
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "ODP-PR-PT_CMA.csv"
with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.get('https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv')
response.raise_for_status()
raw_path.write_bytes(response.content)
data = pl.read_csv(raw_path, separator='\t', infer_schema=False, null_values=['--'])
assert data.height > 0, "https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv returned no rows"
data = data.filter(pl.col('EN_CENSUS_METROPOLITAN_AREA').cast(pl.Utf8).str.strip_chars().str.to_lowercase() == 'edmonton', pl.col('EN_YEAR').cast(pl.Float64, strict=False) >= 2015)
nested = [name for name, dtype in data.schema.items() if dtype.is_nested()]
data = data.with_columns(pl.col(name).map_elements(lambda value: json.dumps(value.to_list() if isinstance(value, pl.Series) else value, default=str), return_dtype=pl.Utf8) for name in nested)
data.write_csv(RAW_DIR / "ODP-PR-PT_CMA_prepared.csv")
end
import delimited "data/raw/ODP-PR-PT_CMA_prepared.csv", clear varnames(1) encoding("utf-8") stringcols(_all) bindquote(strict) maxquotedrows(unlimited)
* 2. Check inputs
* The Python block checked the download for rows before its filters and
* steps, which may leave none (the tool then returns an empty table).
* 3. Prepare data
* Standard cleaning: lower-case names, trimmed text, and numbers stored as
* text converted (destring leaves genuinely non-numeric text alone). Codes
* stay text as the source wrote them: variables named like one (coordinate,
* vectorid, dguid, NAICS, postal code) and ones with a leading zero (01).
* rename *, lower stops at a clash (Indicator next to indicator), so names
* that lower-case alike are numbered as janitor numbers them (indicator,
* indicator_2), then renamed in one group rename, which allows swaps.
local names
foreach var of varlist _all {
local names `names' `=strlower("`var'")'
}
local dups : list dups names
while "`dups'" != "" {
local numbered
local before
foreach name of local names {
local count 1
foreach earlier of local before {
if "`earlier'" == "`name'" local ++count
}
local before `before' `name'
if `count' > 1 {
local name = substr("`name'", 1, 32 - strlen("_`count'")) + "_`count'"
}
local numbered `numbered' `name'
}
local names `numbered'
local dups : list dups names
}
local old_names
local new_names
local i 0
foreach var of varlist _all {
local ++i
local name : word `i' of `names'
if "`name'" != "`var'" {
local old_names `old_names' `var'
local new_names `new_names' `name'
}
}
if "`old_names'" != "" {
rename (`old_names') (`new_names')
}
quietly ds, has(type string)
local text_vars `r(varlist)'
foreach var of local text_vars {
replace `var' = strtrim(`var')
if regexm("`var'|", "^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)[|]") continue
quietly count if regexm(`var', "^-?0[0-9]")
if r(N) > 0 continue
destring `var', replace
}
log closeJulia
# ============================================================
# IRCC monthly update: Canada - Permanent Residents by Province/Territory and CMA
# Purpose: Fetch the data behind MapleStats MCP's ircc_monthly_query
# (exact: the source file, then the tool's filters)
# Inputs: https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv
# Outputs: data/raw/ODP-PR-PT_CMA.csv; the prepared table as `data`
# ============================================================
# 0. Setup
using CSV
using DataFrames
using Downloads
using TidierData
mkpath("data/raw")
# 1. Read inputs
Downloads.download("https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv", "data/raw/ODP-PR-PT_CMA.csv")
data = CSV.read("data/raw/ODP-PR-PT_CMA.csv", DataFrame, delim = "\t", types = String, missingstring = ["", "--"])
# 2. Check inputs
@assert nrow(data) > 0 "https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv returned no rows"
# 3. Prepare data
# Keep the rows the MapleStats tool kept.
data = filter(row -> begin
(lowercase(strip(coalesce(string(row["EN_CENSUS_METROPOLITAN_AREA"]), ""))) == "edmonton") &&
(!ismissing(row["EN_YEAR"]) && something(tryparse(Float64, string(row["EN_YEAR"])), NaN) >= 2015)
end, data)
# Standard cleaning: snake_case names, trimmed text, empty strings as missing.
data = @chain data begin
@clean_names
end
data = mapcols(
col -> eltype(col) <: Union{Missing, AbstractString} ?
[ismissing(x) || isempty(strip(x)) ? missing : strip(x) for x in col] : col,
data,
)
# Numbers stored as text become numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns = r"^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$"
for name in names(data)
column = data[!, name]
if occursin(code_columns, name)
data[!, name] = [ismissing(x) ? missing : string(x) for x in column]
continue
end
eltype(column) <: Union{Missing, AbstractString} || continue
values = collect(skipmissing(column))
(isempty(values) || any(v -> occursin(r"^-?0[0-9]", v), values)) && continue
any(v -> isnothing(tryparse(Float64, v)), values) && continue
whole = all(v -> !isnothing(tryparse(Int, v)), values)
data[!, name] = [
ismissing(x) ? missing : parse(whole ? Int : Float64, x) for x in column
]
endExcel (Power Query)
// ============================================================
// IRCC monthly update: Canada - Permanent Residents by Province/Territory and CMA
// Purpose: Fetch the data behind MapleStats MCP's ircc_monthly_query
// (exact: the source file, then the tool's filters)
// Inputs: https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv
// Outputs: the prepared table, loaded to a worksheet or the Data Model
// How to run: Excel > Data > Get Data > From Other Sources > Blank Query >
// Advanced Editor; paste this query, Done, then Close & Load. Answer the
// first prompt with Anonymous access; Data > Refresh All fetches it again.
// A worksheet holds 1,048,576 rows: load a larger table (a full StatCan
// table can be) with Close & Load To > Only Create Connection > Add this
// data to the Data Model.
// ============================================================
let
// 0. Setup
// Cell values as text or number, null-safe, for the filters and checks.
AsText = (value as any) as text =>
if value = null then "" else (try Text.From(value) otherwise ""),
AsNumber = (value as any) as nullable number =>
if value = null then null else (try Number.From(value, "en-US") otherwise null),
// Records to a table with every field any record has, not only the first's.
ToTable = (records as list) as table =>
Table.FromRecords(
records,
List.Distinct(List.Combine(List.Transform(records, Record.FieldNames))),
MissingField.UseNull
),
// 1. Read inputs
Raw = Web.Contents("https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv"),
Csv = Csv.Document(Raw, [Delimiter = "#(0009)", Encoding = 65001, QuoteStyle = QuoteStyle.Csv]),
Loaded = Table.PromoteHeaders(Csv, [PromoteAllScalars = true]),
// 2. Check inputs
// Stop with a clear message instead of loading an empty table.
Checked = if Table.IsEmpty(Loaded)
then error Error.Record("MapleStats", "https://www.ircc.canada.ca/opendata-donneesouvertes/data/ODP-PR-PT_CMA.csv returned no rows")
else Loaded,
// 3. Prepare data
Headers = Table.TransformColumnNames(
Checked,
(column) =>
let
name = Text.Trim(column, {Character.FromNumber(65279)})
in
name
),
// The source writes missing cells as --.
Missing = Table.TransformColumns(
Headers,
List.Transform(
Table.ColumnNames(Headers),
(name) =>
{name, (value) => if List.Contains({"--"}, value) then null else value}
)
),
// Keep the rows the MapleStats tool kept.
Filtered = Table.SelectRows(
Missing,
each Text.Lower(Text.Trim(AsText(Record.Field(_, "EN_CENSUS_METROPOLITAN_AREA")))) = "edmonton"
and (let n = AsNumber(Record.Field(_, "EN_YEAR")) in n <> null and n >= 2015)
),
// Standard cleaning, as the R script's clean_names() and type_convert():
// snake_case names without accents (PÉRIODE -> periode, referenceNumber ->
// reference_number), names that clean alike numbered (indicator,
// indicator_2), trimmed text, empty text as null, then columns whose
// every value is a number or an ISO date (YYYY-MM-DD) converted.
Accents = [#"à" = "a", #"â" = "a", #"ä" = "a", #"á" = "a", #"ç" = "c", #"é" = "e", #"è" = "e", #"ê" = "e", #"ë" = "e", #"î" = "i", #"ï" = "i", #"í" = "i", #"ô" = "o", #"ö" = "o", #"ó" = "o", #"ù" = "u", #"û" = "u", #"ü" = "u", #"ú" = "u", #"ÿ" = "y", #"ñ" = "n", #"œ" = "oe", #"æ" = "ae", #"À" = "A", #"Â" = "A", #"Ä" = "A", #"Á" = "A", #"Ç" = "C", #"É" = "E", #"È" = "E", #"Ê" = "E", #"Ë" = "E", #"Î" = "I", #"Ï" = "I", #"Í" = "I", #"Ô" = "O", #"Ö" = "O", #"Ó" = "O", #"Ù" = "U", #"Û" = "U", #"Ü" = "U", #"Ú" = "U", #"Ÿ" = "Y", #"Ñ" = "N", #"Œ" = "OE", #"Æ" = "AE"],
CleanName = (name as text) as text =>
let
Plain = Text.Combine(
List.Transform(Text.ToList(name), (ch) => Record.FieldOrDefault(Accents, ch, ch))
),
Words = Splitter.SplitTextByCharacterTransition({"a".."z", "0".."9"}, {"A".."Z"})(
Plain
),
Lower = Text.Lower(Text.Combine(Words, "_")),
Spaced = Text.Combine(
List.Transform(
Text.ToList(Lower), (ch) => if List.Contains(NameCharacters, ch) then ch else " "
)
),
Snake = Text.Combine(List.Select(Text.Split(Spaced, " "), (part) => part <> ""), "_")
in
if Snake = "" then "x" else Snake,
NameCharacters = {"a".."z", "0".."9"},
OldNames = Table.ColumnNames(Filtered),
CleanNames = List.Transform(OldNames, CleanName),
NewNames = List.Transform(
List.Positions(CleanNames),
(i) =>
let
Earlier = List.Count(
List.Select(List.FirstN(CleanNames, i), (name) => name = CleanNames{i})
)
in
if Earlier = 0 then CleanNames{i} else CleanNames{i} & "_" & Text.From(Earlier + 1)
),
Renamed = Table.RenameColumns(
Filtered,
List.Select(List.Zip({OldNames, NewNames}), (pair) => pair{0} <> pair{1})
),
Trimmed = Table.TransformColumns(
Renamed,
List.Transform(
Table.ColumnNames(Renamed),
(name) =>
{
name,
(value) =>
if value is text
then (if Text.Trim(value) = "" then null else Text.Trim(value))
else value
}
)
),
IsNumberText = (value as any) as logical =>
value is number
or (value is text and (try Number.FromText(value, "en-US") otherwise null) <> null),
IsIsoDate = (value as any) as logical =>
value is text
and Text.Length(value) = 10
and Text.Middle(value, 4, 1) = "-"
and Text.Middle(value, 7, 1) = "-"
and (try Date.FromText(value, "en-US") otherwise null) <> null,
ColumnsWhere = (table as table, test as function) as list =>
List.Select(
Table.ColumnNames(table),
(name) =>
let
Values = List.RemoveNulls(Table.Column(table, name))
in
not List.IsEmpty(Values) and List.AllTrue(List.Transform(Values, test))
),
// Codes stay text as the source wrote them: columns named like one
// (coordinate, vector_id, dguid, NAICS, postal code; StatCan's "2.1" and
// "2.10" are different coordinates) and columns with a leading zero (01).
CodeName = (name as text) as logical =>
List.Contains(
{"coordinate", "vector", "vector_id", "vectorid", "product_id", "productid", "pid",
"dguid", "postal_code", "postalcode", "fsa", "noc", "sgc"},
name
)
or Text.EndsWith(name, "_dguid")
or Text.EndsWith(name, "_postal_code")
or Text.Contains(name, "naics")
or Text.StartsWith(name, "noc_")
or Text.EndsWith(name, "_noc")
or Text.StartsWith(name, "sgc_")
or Text.EndsWith(name, "_sgc"),
HasLeadingZero = (value as any) as logical =>
value is text
and (
let
Digits = if Text.StartsWith(value, "-") then Text.Middle(value, 1) else value
in
Text.Length(Digits) > 1
and Text.Start(Digits, 1) = "0"
and List.Contains({"0".."9"}, Text.Middle(Digits, 1, 1))
),
CodeColumns = List.Select(
Table.ColumnNames(Trimmed),
(name) => CodeName(name) or List.AnyTrue(List.Transform(Table.Column(Trimmed, name), HasLeadingZero))
),
Codes = Table.TransformColumns(
Trimmed,
List.Transform(
CodeColumns,
(name) =>
{
name,
(value) =>
if value = null then null
else if value is number then Number.ToText(value, "G", "en-US")
else Text.From(value),
type text
}
)
),
NumberColumns = List.Difference(ColumnsWhere(Codes, IsNumberText), CodeColumns),
Numbers = Table.TransformColumnTypes(
Codes, List.Transform(NumberColumns, (name) => {name, type number}), "en-US"
),
DateColumns = ColumnsWhere(Numbers, IsIsoDate),
Cleaned = Table.TransformColumns(
Numbers,
List.Transform(
DateColumns,
(name) => {name, (value) => if value = null then null else Date.FromText(value, "en-US"), type date}
)
),
Data = Cleaned
in
Data54
tableaux sur le logement pour les urbanistes, qui comptent les grues avant les clés,
Le Canada construit en hauteur, pas en largeur
Les mises en chantier par année selon la SCHL, pour les maisons individuelles et les appartements, dans les centres de 10 000 habitants ou plus. En 2005, le Canada a mis en chantier 93 994 maisons individuelles et 66 404 appartements ; en 2025, 41 837 et 164 839. Les appartements sont passés de 34 % de toutes les mises en chantier à 68 %, et dépassent les maisons individuelles chaque année depuis 2011.
Voir les données
| Année | Individuelles | Appartements |
|---|---|---|
| 2005 | 93 994 | 66 404 |
| 2006 | 94 110 | 69 150 |
| 2007 | 90 855 | 69 062 |
| 2008 | 74 435 | 83 565 |
| 2009 | 60 521 | 46 388 |
| 2010 | 74 244 | 61 396 |
| 2011 | 67 089 | 77 119 |
| 2012 | 67 172 | 92 951 |
| 2013 | 63 143 | 76 009 |
| 2014 | 62 380 | 76 599 |
| 2015 | 57 739 | 92 564 |
| 2016 | 60 549 | 88 007 |
| 2017 | 63 495 | 100 365 |
| 2018 | 54 180 | 109 820 |
| 2019 | 46 909 | 115 664 |
| 2020 | 49 704 | 119 428 |
| 2021 | 63 456 | 141 684 |
| 2022 | 57 515 | 144 043 |
| 2023 | 42 924 | 147 617 |
| 2024 | 44 357 | 149 113 |
| 2025 | 41 837 | 164 839 |
Les appels à l'origine de ce graphique
call_tool{"name": "cmhc_get_table_data", "arguments": {"category_level_1": "New Housing Construction", "category_level_2": "Starts (Actual)", "column_field": "1", "row_field": "TIMESERIES"}}call_tool{"name": "cmhc_list_categories", "arguments": {}}R
# ============================================================
# Reproduce cmhc_get_table_data
# Purpose: Fetch the data behind MapleStats MCP's cmhc_get_table_data
# (exact: the request the tool made, recorded while it ran)
# Inputs: https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable
# Outputs: data/raw/ExportTable.csv; the prepared table as `data`
# ============================================================
# 0. Setup ----
library(dplyr)
library(httr2)
library(janitor)
library(purrr)
library(readr)
library(stringr)
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
request("https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable") |>
req_body_form(!!!list(`TableId` = "5.6.1", `GeographyId` = "1", `GeographyTypeId` = "1", `exportType` = "csv")) |>
req_perform(path = "data/raw/ExportTable.csv")
# The file opens with 2 title line(s) above its header; read the table between.
lines <- read_lines("data/raw/ExportTable.csv", skip = 2)
data <- read_delim(I(str_c(lines, collapse = "\n")), delim = ",",
col_types = cols(.default = col_character()))
# 2. Check inputs ----
stopifnot("https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable returned no rows" = nrow(data) > 0)
# 3. Prepare data ----
data <- data |>
clean_names()
# Standard cleaning: trimmed text, empty strings as missing, and numbers
# stored as text converted to numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns <- "^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$"
data <- data |>
mutate(across(where(is.character), \(x) na_if(str_trim(x), "")))
keep_text <- names(data)[
str_detect(names(data), code_columns) |
map_lgl(
data,
\(column) is.character(column) && any(str_detect(column, "^-?0[0-9]"), na.rm = TRUE)
)
]
data <- data |>
mutate(
across(any_of(keep_text), \(x) if (is.list(x)) x else as.character(x)),
across(where(is.character) & !any_of(keep_text), parse_guess)
)Python
# ============================================================
# Reproduce cmhc_get_table_data
# Purpose: Fetch the data behind MapleStats MCP's cmhc_get_table_data
# (exact: the request the tool made, recorded while it ran)
# Inputs: https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable
# Outputs: data/raw/ExportTable.csv; the prepared table as `data`
# ============================================================
# %% 0. Setup
import io
import re
import unicodedata
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "ExportTable.csv"
# %% 1. Read inputs
with httpx.Client(
http2=True,
follow_redirects=True,
timeout=300,
headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"},
) as client:
response = client.post('https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable', data={'TableId': '5.6.1', 'GeographyId': '1', 'GeographyTypeId': '1', 'exportType': 'csv'})
response.raise_for_status()
raw_path.write_bytes(response.content)
# The file opens with 2 title line(s) above its header; read the table between.
lines = raw_path.read_text(encoding="utf-8", errors="replace").splitlines()[2:]
data = pl.read_csv(io.StringIO("\n".join(lines)), separator=',', infer_schema=False)
# %% 2. Check inputs
assert data.height > 0, "https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable returned no rows"
# %% 3. Prepare data
# Standard cleaning: snake_case names as janitor's clean_names() gives them
# in R (PÉRIODE -> periode, referenceNumber -> reference_number, # -> number,
# % -> percent, apostrophes dropped, a leading digit prefixed with x),
# trimmed text, empty strings as missing. Names that clean alike are
# numbered as janitor numbers them (Indicator, indicator -> indicator,
# indicator_2).
clean_names = [
re.sub(
r"[^0-9a-z]+",
"_",
re.sub(
r"([a-z0-9])([A-Z])",
r"\1_\2",
"".join(
" " if unicodedata.category(ch)[0] in "PS" and not ch.isascii() else ch
for ch in unicodedata.normalize("NFKD", column.replace("'", ""))
)
.replace("#", " number ")
.replace("%", " percent ")
.encode("ascii", "ignore")
.decode(),
).lower(),
).strip("_")
or "x"
for column in data.columns
]
clean_names = [f"x{name}" if name[0].isdigit() else name for name in clean_names]
while len(set(clean_names)) < len(clean_names):
name_counts = {}
numbered = []
for name in clean_names:
name_counts[name] = name_counts.get(name, 0) + 1
count = name_counts[name]
numbered.append(name if count == 1 else f"{name}_{count}")
clean_names = numbered
data = data.rename(dict(zip(data.columns, clean_names)))
data = data.with_columns(pl.col(pl.Utf8).str.strip_chars().replace("", None))
# Numbers stored as text become numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns = re.compile(r"^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$")
for column in data.columns:
dtype = data.schema[column]
if code_columns.search(column) and not dtype.is_nested():
data = data.with_columns(pl.col(column).cast(pl.Utf8))
continue
if dtype != pl.Utf8 or data[column].null_count() == data.height:
continue
numbers = data[column].cast(pl.Float64, strict=False)
leading_zero = data[column].str.contains(r"^-?0[0-9]").any()
if numbers.null_count() > data[column].null_count() or leading_zero:
continue
whole = data[column].cast(pl.Int64, strict=False)
data = data.with_columns(whole if whole.null_count() == numbers.null_count() else numbers)Stata
* ============================================================
* Reproduce cmhc_get_table_data
* Purpose: Fetch the data behind MapleStats MCP's cmhc_get_table_data
* (exact: the request the tool made, recorded while it ran)
* Inputs: https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable
* Outputs: data/raw/ExportTable.csv; the prepared table as `data`
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/cmhc_get_table_data.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import io
import json
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "ExportTable.csv"
with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.post('https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable', data={'TableId': '5.6.1', 'GeographyId': '1', 'GeographyTypeId': '1', 'exportType': 'csv'})
response.raise_for_status()
raw_path.write_bytes(response.content)
lines = raw_path.read_text(encoding="utf-8", errors="replace").splitlines()[2:]
data = pl.read_csv(io.StringIO("\n".join(lines)), separator=',', infer_schema=False)
nested = [name for name, dtype in data.schema.items() if dtype.is_nested()]
data = data.with_columns(pl.col(name).map_elements(lambda value: json.dumps(value.to_list() if isinstance(value, pl.Series) else value, default=str), return_dtype=pl.Utf8) for name in nested)
data.write_csv(RAW_DIR / "ExportTable_prepared.csv")
end
import delimited "data/raw/ExportTable_prepared.csv", clear varnames(1) encoding("utf-8") stringcols(_all) bindquote(strict) maxquotedrows(unlimited)
* 2. Check inputs
assert _N > 0
* 3. Prepare data
* Standard cleaning: lower-case names, trimmed text, and numbers stored as
* text converted (destring leaves genuinely non-numeric text alone). Codes
* stay text as the source wrote them: variables named like one (coordinate,
* vectorid, dguid, NAICS, postal code) and ones with a leading zero (01).
* rename *, lower stops at a clash (Indicator next to indicator), so names
* that lower-case alike are numbered as janitor numbers them (indicator,
* indicator_2), then renamed in one group rename, which allows swaps.
local names
foreach var of varlist _all {
local names `names' `=strlower("`var'")'
}
local dups : list dups names
while "`dups'" != "" {
local numbered
local before
foreach name of local names {
local count 1
foreach earlier of local before {
if "`earlier'" == "`name'" local ++count
}
local before `before' `name'
if `count' > 1 {
local name = substr("`name'", 1, 32 - strlen("_`count'")) + "_`count'"
}
local numbered `numbered' `name'
}
local names `numbered'
local dups : list dups names
}
local old_names
local new_names
local i 0
foreach var of varlist _all {
local ++i
local name : word `i' of `names'
if "`name'" != "`var'" {
local old_names `old_names' `var'
local new_names `new_names' `name'
}
}
if "`old_names'" != "" {
rename (`old_names') (`new_names')
}
quietly ds, has(type string)
local text_vars `r(varlist)'
foreach var of local text_vars {
replace `var' = strtrim(`var')
if regexm("`var'|", "^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)[|]") continue
quietly count if regexm(`var', "^-?0[0-9]")
if r(N) > 0 continue
destring `var', replace
}
log closeJulia
# ============================================================
# Reproduce cmhc_get_table_data
# Purpose: Fetch the data behind MapleStats MCP's cmhc_get_table_data
# (exact: the request the tool made, recorded while it ran)
# Inputs: https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable
# Outputs: data/raw/ExportTable.csv; the prepared table as `data`
# ============================================================
# 0. Setup
using CSV
using DataFrames
using Downloads
using TidierData
mkpath("data/raw")
# 1. Read inputs
Downloads.request(
"https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable";
headers = ["Content-Type" => "application/x-www-form-urlencoded"], method = "POST", input = IOBuffer("TableId=5.6.1&GeographyId=1&GeographyTypeId=1&exportType=csv"),
output = "data/raw/ExportTable.csv",
)
# The file opens with 2 title line(s) above its header; read the table between.
lines = readlines("data/raw/ExportTable.csv")[(2 + 1):end]
data = CSV.read(IOBuffer(join(lines, "\n")), DataFrame, types = String)
# 2. Check inputs
@assert nrow(data) > 0 "https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable returned no rows"
# 3. Prepare data
# Standard cleaning: snake_case names, trimmed text, empty strings as missing.
data = @chain data begin
@clean_names
end
data = mapcols(
col -> eltype(col) <: Union{Missing, AbstractString} ?
[ismissing(x) || isempty(strip(x)) ? missing : strip(x) for x in col] : col,
data,
)
# Numbers stored as text become numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns = r"^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$"
for name in names(data)
column = data[!, name]
if occursin(code_columns, name)
data[!, name] = [ismissing(x) ? missing : string(x) for x in column]
continue
end
eltype(column) <: Union{Missing, AbstractString} || continue
values = collect(skipmissing(column))
(isempty(values) || any(v -> occursin(r"^-?0[0-9]", v), values)) && continue
any(v -> isnothing(tryparse(Float64, v)), values) && continue
whole = all(v -> !isnothing(tryparse(Int, v)), values)
data[!, name] = [
ismissing(x) ? missing : parse(whole ? Int : Float64, x) for x in column
]
endExcel (Power Query)
// ============================================================
// Reproduce cmhc_get_table_data
// Purpose: Fetch the data behind MapleStats MCP's cmhc_get_table_data
// (exact: the request the tool made, recorded while it ran)
// Inputs: https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable
// Outputs: the prepared table, loaded to a worksheet or the Data Model
// How to run: Excel > Data > Get Data > From Other Sources > Blank Query >
// Advanced Editor; paste this query, Done, then Close & Load. Answer the
// first prompt with Anonymous access; Data > Refresh All fetches it again.
// A worksheet holds 1,048,576 rows: load a larger table (a full StatCan
// table can be) with Close & Load To > Only Create Connection > Add this
// data to the Data Model.
// ============================================================
let
// 0. Setup
// Cell values as text or number, null-safe, for the filters and checks.
AsText = (value as any) as text =>
if value = null then "" else (try Text.From(value) otherwise ""),
AsNumber = (value as any) as nullable number =>
if value = null then null else (try Number.From(value, "en-US") otherwise null),
// Records to a table with every field any record has, not only the first's.
ToTable = (records as list) as table =>
Table.FromRecords(
records,
List.Distinct(List.Combine(List.Transform(records, Record.FieldNames))),
MissingField.UseNull
),
// 1. Read inputs
Raw = Web.Contents("https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable", [Headers = [#"Content-Type" = "application/x-www-form-urlencoded"], Content = Text.ToBinary(Uri.BuildQueryString([#"TableId" = "5.6.1", #"GeographyId" = "1", #"GeographyTypeId" = "1", #"exportType" = "csv"]))]),
// The file opens with 2 title line(s) above its header; read the table between.
Body = List.Skip(Lines.FromBinary(Raw, null, null, 65001), 2),
Csv = Csv.Document(Text.Combine(Body, "#(cr,lf)"), [Delimiter = ",", Encoding = 65001, QuoteStyle = QuoteStyle.Csv]),
Loaded = Table.PromoteHeaders(Csv, [PromoteAllScalars = true]),
// 2. Check inputs
// Stop with a clear message instead of loading an empty table.
Checked = if Table.IsEmpty(Loaded)
then error Error.Record("MapleStats", "https://www03.cmhc-schl.gc.ca/hmip-pimh/en/TableMapChart/ExportTable returned no rows")
else Loaded,
// 3. Prepare data
Headers = Table.TransformColumnNames(
Checked,
(column) =>
let
name = Text.Trim(column, {Character.FromNumber(65279)})
in
name
),
// Standard cleaning, as the R script's clean_names() and type_convert():
// snake_case names without accents (PÉRIODE -> periode, referenceNumber ->
// reference_number), names that clean alike numbered (indicator,
// indicator_2), trimmed text, empty text as null, then columns whose
// every value is a number or an ISO date (YYYY-MM-DD) converted.
Accents = [#"à" = "a", #"â" = "a", #"ä" = "a", #"á" = "a", #"ç" = "c", #"é" = "e", #"è" = "e", #"ê" = "e", #"ë" = "e", #"î" = "i", #"ï" = "i", #"í" = "i", #"ô" = "o", #"ö" = "o", #"ó" = "o", #"ù" = "u", #"û" = "u", #"ü" = "u", #"ú" = "u", #"ÿ" = "y", #"ñ" = "n", #"œ" = "oe", #"æ" = "ae", #"À" = "A", #"Â" = "A", #"Ä" = "A", #"Á" = "A", #"Ç" = "C", #"É" = "E", #"È" = "E", #"Ê" = "E", #"Ë" = "E", #"Î" = "I", #"Ï" = "I", #"Í" = "I", #"Ô" = "O", #"Ö" = "O", #"Ó" = "O", #"Ù" = "U", #"Û" = "U", #"Ü" = "U", #"Ú" = "U", #"Ÿ" = "Y", #"Ñ" = "N", #"Œ" = "OE", #"Æ" = "AE"],
CleanName = (name as text) as text =>
let
Plain = Text.Combine(
List.Transform(Text.ToList(name), (ch) => Record.FieldOrDefault(Accents, ch, ch))
),
Words = Splitter.SplitTextByCharacterTransition({"a".."z", "0".."9"}, {"A".."Z"})(
Plain
),
Lower = Text.Lower(Text.Combine(Words, "_")),
Spaced = Text.Combine(
List.Transform(
Text.ToList(Lower), (ch) => if List.Contains(NameCharacters, ch) then ch else " "
)
),
Snake = Text.Combine(List.Select(Text.Split(Spaced, " "), (part) => part <> ""), "_")
in
if Snake = "" then "x" else Snake,
NameCharacters = {"a".."z", "0".."9"},
OldNames = Table.ColumnNames(Headers),
CleanNames = List.Transform(OldNames, CleanName),
NewNames = List.Transform(
List.Positions(CleanNames),
(i) =>
let
Earlier = List.Count(
List.Select(List.FirstN(CleanNames, i), (name) => name = CleanNames{i})
)
in
if Earlier = 0 then CleanNames{i} else CleanNames{i} & "_" & Text.From(Earlier + 1)
),
Renamed = Table.RenameColumns(
Headers,
List.Select(List.Zip({OldNames, NewNames}), (pair) => pair{0} <> pair{1})
),
Trimmed = Table.TransformColumns(
Renamed,
List.Transform(
Table.ColumnNames(Renamed),
(name) =>
{
name,
(value) =>
if value is text
then (if Text.Trim(value) = "" then null else Text.Trim(value))
else value
}
)
),
IsNumberText = (value as any) as logical =>
value is number
or (value is text and (try Number.FromText(value, "en-US") otherwise null) <> null),
IsIsoDate = (value as any) as logical =>
value is text
and Text.Length(value) = 10
and Text.Middle(value, 4, 1) = "-"
and Text.Middle(value, 7, 1) = "-"
and (try Date.FromText(value, "en-US") otherwise null) <> null,
ColumnsWhere = (table as table, test as function) as list =>
List.Select(
Table.ColumnNames(table),
(name) =>
let
Values = List.RemoveNulls(Table.Column(table, name))
in
not List.IsEmpty(Values) and List.AllTrue(List.Transform(Values, test))
),
// Codes stay text as the source wrote them: columns named like one
// (coordinate, vector_id, dguid, NAICS, postal code; StatCan's "2.1" and
// "2.10" are different coordinates) and columns with a leading zero (01).
CodeName = (name as text) as logical =>
List.Contains(
{"coordinate", "vector", "vector_id", "vectorid", "product_id", "productid", "pid",
"dguid", "postal_code", "postalcode", "fsa", "noc", "sgc"},
name
)
or Text.EndsWith(name, "_dguid")
or Text.EndsWith(name, "_postal_code")
or Text.Contains(name, "naics")
or Text.StartsWith(name, "noc_")
or Text.EndsWith(name, "_noc")
or Text.StartsWith(name, "sgc_")
or Text.EndsWith(name, "_sgc"),
HasLeadingZero = (value as any) as logical =>
value is text
and (
let
Digits = if Text.StartsWith(value, "-") then Text.Middle(value, 1) else value
in
Text.Length(Digits) > 1
and Text.Start(Digits, 1) = "0"
and List.Contains({"0".."9"}, Text.Middle(Digits, 1, 1))
),
CodeColumns = List.Select(
Table.ColumnNames(Trimmed),
(name) => CodeName(name) or List.AnyTrue(List.Transform(Table.Column(Trimmed, name), HasLeadingZero))
),
Codes = Table.TransformColumns(
Trimmed,
List.Transform(
CodeColumns,
(name) =>
{
name,
(value) =>
if value = null then null
else if value is number then Number.ToText(value, "G", "en-US")
else Text.From(value),
type text
}
)
),
NumberColumns = List.Difference(ColumnsWhere(Codes, IsNumberText), CodeColumns),
Numbers = Table.TransformColumnTypes(
Codes, List.Transform(NumberColumns, (name) => {name, type number}), "en-US"
),
DateColumns = ColumnsWhere(Numbers, IsIsoDate),
Cleaned = Table.TransformColumns(
Numbers,
List.Transform(
DateColumns,
(name) => {name, (value) => if value = null then null else Date.FromText(value, "en-US"), type date}
)
),
Data = Cleaned
in
Data962 449
fiches du recensement pour les microéconomistes, tisseurs d'incitations, une vie à la fois,
Le faible revenu d'une génération à l'autre
La part des personnes sous la mesure de faible revenu après impôt, selon la génération, tirée des mêmes microdonnées du recensement de 2021, avec des intervalles de confiance à 95 % issus des poids de réplication. Les immigrants de première génération sont les plus susceptibles d'être à faible revenu (14,0 %). Leurs enfants, nés au Canada, le sont le moins (9,1 % et 8,8 %), moins que la troisième génération ou plus (10,3 %).
Voir les données
| Génération | Estimation | IC à 95 %, borne inf. | IC à 95 %, borne sup. |
|---|---|---|---|
| Première génération (née à l'étranger) | 14,0 % | 13,8 % | 14,1 % |
| Deuxième génération, deux parents nés à l'étranger | 9,1 % | 9,0 % | 9,3 % |
| Deuxième génération, un parent né à l'étranger | 8,8 % | 8,6 % | 9,1 % |
| Troisième génération ou plus | 10,3 % | 10,2 % | 10,4 % |
Les appels à l'origine de ce graphique
call_tool{"name": "statcan_pumf_tabulate", "arguments": {"url": "https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip", "rows": ["GENSTAT", "LOLIMA"], "statistic": "share", "filters": {"GENSTAT": ["1", "2", "3", "4"], "LOLIMA": ["1", "2"]}}}R
# ============================================================
# StatCan public use microdata file
# Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
# (exact: the PUMF ZIP)
# Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
# Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
# ============================================================
# 0. Setup ----
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
download.file("https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip", "data/raw/cen21_ind_98m0001x_part_rec21.zip", mode = "wb")
zip::unzip("data/raw/cen21_ind_98m0001x_part_rec21.zip", exdir = "data/raw/cen21_ind_98m0001x_part_rec21")
file.remove("data/raw/cen21_ind_98m0001x_part_rec21.zip")
data_files <- list.files("data/raw/cen21_ind_98m0001x_part_rec21", full.names = TRUE, recursive = TRUE)Python
# ============================================================
# StatCan public use microdata file
# Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
# (exact: the PUMF ZIP)
# Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
# Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
# ============================================================
# %% 0. Setup
import zipfile
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "cen21_ind_98m0001x_part_rec21.zip"
# %% 1. Read inputs
with httpx.Client(
http2=True,
follow_redirects=True,
timeout=300,
headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"},
) as client:
response = client.get('https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip')
response.raise_for_status()
raw_path.write_bytes(response.content)
with zipfile.ZipFile(raw_path) as archive:
archive.extractall(RAW_DIR / raw_path.stem)Stata
* ============================================================
* StatCan public use microdata file
* Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
* (exact: the PUMF ZIP)
* Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
* Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/statcan_pumf_tabulate.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import json
import zipfile
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "cen21_ind_98m0001x_part_rec21.zip"
with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.get('https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip')
response.raise_for_status()
raw_path.write_bytes(response.content)
with zipfile.ZipFile(raw_path) as archive: archive.extractall(RAW_DIR / raw_path.stem)
end
log closeJulia
# ============================================================
# StatCan public use microdata file
# Purpose: Fetch the data behind MapleStats MCP's statcan_pumf_tabulate
# (exact: the PUMF ZIP)
# Inputs: https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip
# Outputs: data/raw/cen21_ind_98m0001x_part_rec21.zip; the unzipped files
# ============================================================
# 0. Setup
using Downloads
mkpath("data/raw")
# 1. Read inputs
Downloads.download("https://www150.statcan.gc.ca/n1/pub/98m0001x/2023001/cen21_ind_98m0001x_part_rec21.zip", "data/raw/cen21_ind_98m0001x_part_rec21.zip")98
cartes de crédit pour les spécialistes du marketing, lecteurs des petits caractères de la concurrence,
Ce que coûte une carte à récompenses
Toutes les cartes répertoriées pour l'Alberta dans l'outil de comparaison des cartes de crédit de l'Agence de la consommation en matière financière du Canada (ACFC), selon leurs frais annuels et leur taux d'intérêt sur les achats, en dollars canadiens. Les cartes pour étudiants et les cartes garanties y font l'objet de recherches distinctes et ne sont pas incluses. Les 82 cartes à récompenses ont des frais médians de 99 $ et un taux médian de 21,99 % ; les 15 cartes sans récompenses, 25 $ et 13,99 %. Les gros points regroupent plusieurs cartes au même prix. Une carte prépayée, sans intérêt, est exclue.
Voir les données
| Type | Frais annuels | Taux sur les achats | Cartes |
|---|---|---|---|
| Avec récompenses | 0 $ | 19,99 % | 1 |
| Avec récompenses | 0 $ | 20,90 % | 3 |
| Avec récompenses | 0 $ | 20,95 % | 2 |
| Avec récompenses | 0 $ | 20,99 % | 7 |
| Avec récompenses | 0 $ | 21,74 % | 1 |
| Avec récompenses | 0 $ | 21,75 % | 1 |
| Avec récompenses | 0 $ | 21,90 % | 3 |
| Avec récompenses | 0 $ | 21,99 % | 15 |
| Avec récompenses | 39 $ | 20,99 % | 1 |
| Avec récompenses | 39 $ | 21,99 % | 1 |
| Avec récompenses | 50 $ | 12,99 % | 1 |
| Avec récompenses | 75 $ | 20,99 % | 1 |
| Avec récompenses | 89 $ | 21,99 % | 3 |
| Avec récompenses | 99 $ | 20,99 % | 1 |
| Avec récompenses | 99 $ | 21,99 % | 1 |
| Avec récompenses | 100 $ | 20,90 % | 1 |
| Avec récompenses | 110 $ | 20,90 % | 1 |
| Avec récompenses | 119,88 $ | 21,99 % | 1 |
| Avec récompenses | 120 $ | 20,95 % | 1 |
| Avec récompenses | 120 $ | 20,99 % | 4 |
| Avec récompenses | 120 $ | 21,99 % | 3 |
| Avec récompenses | 120 $ | 30,00 % | 1 |
| Avec récompenses | 130 $ | 20,90 % | 1 |
| Avec récompenses | 130 $ | 20,99 % | 1 |
| Avec récompenses | 139 $ | 20,99 % | 1 |
| Avec récompenses | 139 $ | 21,99 % | 7 |
| Avec récompenses | 150 $ | 20,99 % | 1 |
| Avec récompenses | 150 $ | 21,99 % | 3 |
| Avec récompenses | 165 $ | 20,50 % | 1 |
| Avec récompenses | 191,88 $ | 21,99 % | 1 |
| Avec récompenses | 199 $ | 21,99 % | 1 |
| Avec récompenses | 250 $ | 21,99 % | 1 |
| Avec récompenses | 250 $ | 30,00 % | 1 |
| Avec récompenses | 395 $ | 11,90 % | 1 |
| Avec récompenses | 399 $ | 20,99 % | 1 |
| Avec récompenses | 499 $ | 21,99 % | 1 |
| Avec récompenses | 599 $ | 21,99 % | 4 |
| Avec récompenses | 599 $ | 30,00 % | 1 |
| Avec récompenses | 799 $ | 30,00 % | 1 |
| Sans récompenses | 0 $ | 10,90 % | 1 |
| Sans récompenses | 0 $ | 12,99 % | 1 |
| Sans récompenses | 0 $ | 21,99 % | 1 |
| Sans récompenses | 0 $ | 29,90 % | 2 |
| Sans récompenses | 20 $ | 12,99 % | 1 |
| Sans récompenses | 20 $ | 13,99 % | 1 |
| Sans récompenses | 25 $ | 12,90 % | 1 |
| Sans récompenses | 25 $ | 12,99 % | 1 |
| Sans récompenses | 25 $ | 13,99 % | 1 |
| Sans récompenses | 29 $ | 13,99 % | 3 |
| Sans récompenses | 30 $ | 12,99 % | 1 |
| Sans récompenses | 39 $ | 10,99 % | 1 |
Les appels à l'origine de ce graphique
call_tool{"name": "fcac_search_credit_cards", "arguments": {"province": "AB", "limit": 100, "lang": "fr"}}reproduce_code : Il s'agit d'une page Web que l'outil analyse, pas d'un tableau de données ; un téléchargement ne reproduit donc pas le résultat. Citez l'URL et la date de consultation, ou conservez la sortie de l'outil comme donnée brute.6 350 070
cotisants à un REER pour les gestionnaires de patrimoine, qui conseillent sur les années à venir,
Qui alimente le REER
Pour l'année d'imposition 2023, 6 350 070 déclarants ont versé 55,7 G$ dans un régime enregistré d'épargne-retraite, avec une cotisation médiane de 4 000 $. Le graphique répartit ces dollars selon l'âge du cotisant. 65 % de l'argent vient de personnes de 45 ans et plus, et la plus grande part, 29 %, des personnes de 55 à 64 ans. Les chiffres viennent du tableau 11-10-0044 de Statistique Canada, tiré des dossiers fiscaux de l'Agence du revenu du Canada, et Statistique Canada publie les parts par âge arrondies au pourcentage entier. Le même tableau les détaille par province et par ville.
Voir les données
| Groupe d'âge | Part des cotisations |
|---|---|
| Moins de 25 ans | 1 % |
| 25 à 34 ans | 13 % |
| 35 à 44 ans | 21 % |
| 45 à 54 ans | 25 % |
| 55 à 64 ans | 29 % |
| 65 ans et plus | 11 % |
Les appels à l'origine de ce graphique
call_tool{"name": "wds_get_data_from_cube_coord", "arguments": {"product_id": 11100044, "coordinate": "1.4", "latest_n": 3}}8 autres appels semblables, avec d'autres arguments.
R
# ============================================================
# StatCan table 11-10-0044-01
# Purpose: Fetch the data behind MapleStats MCP's wds_get_data_from_cube_coord
# (exact: full table from the product id)
# Inputs: https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip
# Outputs: data/raw/11100044-eng.zip; the prepared table as `data`
# ============================================================
# 0. Setup ----
library(cansim)
library(dplyr)
library(janitor)
library(readr)
library(stringr)
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
data <- get_cansim("11-10-0044-01")
# 2. Check inputs ----
stopifnot("https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip returned no rows" = nrow(data) > 0)
# 3. Prepare data ----
data <- data |>
clean_names()
# cansim adds val_norm (VALUE times its scalar factor) and a Date column;
# keep STATUS and SYMBOL, which flag estimates to use with caution.
data <- data |>
filter(!is.na(val_norm))
# Standard cleaning: trimmed text, empty strings as missing, and numbers
# stored as text converted to numbers.
data <- data |>
mutate(across(where(is.character), \(x) na_if(str_trim(x), ""))) |>
type_convert()Python
# ============================================================
# StatCan table 11-10-0044-01
# Purpose: Fetch the data behind MapleStats MCP's wds_get_data_from_cube_coord
# (exact: full table from the product id)
# Inputs: https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip
# Outputs: data/raw/11100044-eng.zip; the prepared table as `data`
# ============================================================
# %% 0. Setup
import re
import unicodedata
import zipfile
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "11100044-eng.zip"
# %% 1. Read inputs
with httpx.Client(
http2=True,
follow_redirects=True,
timeout=300,
headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"},
) as client:
response = client.get('https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip')
response.raise_for_status()
raw_path.write_bytes(response.content)
# The ZIP may also hold a metadata file; read the data file.
with zipfile.ZipFile(raw_path) as archive:
data_name = next(n for n in archive.namelist() if n.lower().endswith((".csv", ".txt")) and "metadata" not in n.lower())
archive.extract(data_name, RAW_DIR)
data = pl.read_csv(RAW_DIR / data_name, separator=',', infer_schema_length=100_000)
# %% 2. Check inputs
assert data.height > 0, "https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip returned no rows"
# %% 3. Prepare data
# VALUE is in the unit named by SCALAR_FACTOR; SCALAR_ID is its power of ten.
data = data.with_columns(
(pl.col("VALUE") * 10 ** pl.col("SCALAR_ID").cast(pl.Int64)).alias(
"VALUE_NORMALIZED"
)
)
# Standard cleaning: snake_case names without accents (PÉRIODE -> periode,
# referenceNumber -> reference_number, as janitor does in R), trimmed text,
# empty strings as missing. Names that clean alike are numbered as janitor
# numbers them (Indicator, indicator -> indicator, indicator_2).
clean_names = [
re.sub(
r"[^0-9a-z]+",
"_",
re.sub(
r"([a-z0-9])([A-Z])",
r"\1_\2",
unicodedata.normalize("NFKD", column).encode("ascii", "ignore").decode(),
).lower(),
).strip("_")
for column in data.columns
]
while len(set(clean_names)) < len(clean_names):
name_counts = {}
numbered = []
for name in clean_names:
name_counts[name] = name_counts.get(name, 0) + 1
count = name_counts[name]
numbered.append(name if count == 1 else f"{name}_{count}")
clean_names = numbered
data = data.rename(dict(zip(data.columns, clean_names)))
data = data.with_columns(pl.col(pl.Utf8).str.strip_chars().replace("", None))Stata
* ============================================================
* StatCan table 11-10-0044-01
* Purpose: Fetch the data behind MapleStats MCP's wds_get_data_from_cube_coord
* (exact: full table from the product id)
* Inputs: https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip
* Outputs: data/raw/11100044-eng.zip; the prepared table as `data`
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/wds_get_data_from_cube_coord.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import json
import zipfile
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "11100044-eng.zip"
with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.get('https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip')
response.raise_for_status()
raw_path.write_bytes(response.content)
with zipfile.ZipFile(raw_path) as archive: data_name = next(n for n in archive.namelist() if n.lower().endswith((".csv", ".txt")) and "metadata" not in n.lower()); archive.extract(data_name, RAW_DIR)
data = pl.read_csv(RAW_DIR / data_name, separator=',', infer_schema_length=100_000)
nested = [name for name, dtype in data.schema.items() if dtype.is_nested()]
data = data.with_columns(pl.col(name).map_elements(lambda value: json.dumps(value.to_list() if isinstance(value, pl.Series) else value, default=str), return_dtype=pl.Utf8) for name in nested)
data.write_csv(RAW_DIR / "11100044-eng_prepared.csv")
end
import delimited "data/raw/11100044-eng_prepared.csv", clear varnames(1) encoding("utf-8")
* 2. Check inputs
assert _N > 0
* 3. Prepare data
* value is in the unit named by scalar_factor; scalar_id is its power of ten.
generate double value_normalized = value * 10^scalar_id
* Standard cleaning: lower-case names, trimmed text, and numbers stored as
* text converted (destring leaves genuinely non-numeric text alone).
* rename *, lower stops at a clash (Indicator next to indicator), so names
* that lower-case alike are numbered as janitor numbers them (indicator,
* indicator_2), then renamed in one group rename, which allows swaps.
local names
foreach var of varlist _all {
local names `names' `=strlower("`var'")'
}
local dups : list dups names
while "`dups'" != "" {
local numbered
local before
foreach name of local names {
local count 1
foreach earlier of local before {
if "`earlier'" == "`name'" local ++count
}
local before `before' `name'
if `count' > 1 {
local name = substr("`name'", 1, 32 - strlen("_`count'")) + "_`count'"
}
local numbered `numbered' `name'
}
local names `numbered'
local dups : list dups names
}
local old_names
local new_names
local i 0
foreach var of varlist _all {
local ++i
local name : word `i' of `names'
if "`name'" != "`var'" {
local old_names `old_names' `var'
local new_names `new_names' `name'
}
}
if "`old_names'" != "" {
rename (`old_names') (`new_names')
}
quietly ds, has(type string)
local text_vars `r(varlist)'
foreach var of local text_vars {
replace `var' = strtrim(`var')
}
destring, replace
log closeJulia
# ============================================================
# StatCan table 11-10-0044-01
# Purpose: Fetch the data behind MapleStats MCP's wds_get_data_from_cube_coord
# (exact: full table from the product id)
# Inputs: https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip
# Outputs: data/raw/11100044-eng.zip; the prepared table as `data`
# ============================================================
# 0. Setup
using Downloads
using TidierData
using TidierFiles
using ZipFile
mkpath("data/raw")
# 1. Read inputs
Downloads.download("https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip", "data/raw/11100044-eng.zip")
archive = ZipFile.Reader("data/raw/11100044-eng.zip")
# The ZIP may also hold a metadata file; read the data file.
data_file = first(f for f in archive.files if (endswith(lowercase(f.name), ".csv") || endswith(lowercase(f.name), ".txt")) &&
!occursin("metadata", lowercase(f.name)))
write("data/raw/table.csv", read(data_file))
close(archive)
data = read_csv("data/raw/table.csv")
# 2. Check inputs
@assert nrow(data) > 0 "https://www150.statcan.gc.ca/n1/tbl/csv/11100044-eng.zip returned no rows"
# 3. Prepare data
# VALUE is in the unit named by SCALAR_FACTOR; SCALAR_ID is its power of ten.
data.VALUE_NORMALIZED = data.VALUE .* 10.0 .^ data.SCALAR_ID
# Standard cleaning: snake_case names, trimmed text, empty strings as missing.
data = @chain data begin
@clean_names
end
data = mapcols(
col -> eltype(col) <: Union{Missing, AbstractString} ?
[ismissing(x) || isempty(strip(x)) ? missing : strip(x) for x in col] : col,
data,
)6 434
jours de rendements obligataires pour les macroéconomistes, qui lisent la courbe avant le virage,
Ce qu'attendent les marchés obligataires, en une ligne
L'écart entre les rendements des obligations de référence du gouvernement du Canada à 10 ans et à 2 ans, chaque jour ouvrable depuis 2001, d'après l'API Valet de la Banque du Canada, tracé en moyenne mensuelle. Quand le 2 ans rapporte plus que le 10 ans, la courbe est inversée : les marchés s'attendent à une baisse des taux, le plus souvent parce qu'ils prévoient un ralentissement. Elle s'est inversée en 2007, 2019 à 2020 et 2022 à 2024. Le mois le plus creux a été juillet 2023, à −123 pb ; en septembre 2026, il était en moyenne de 62 pb.
Voir les données
| Année | Moyenne | Mois le plus bas | Mois le plus haut |
|---|---|---|---|
| 2001 | 117 pb | 41 pb | 213 pb |
| 2002 | 169 pb | 139 pb | 225 pb |
| 2003 | 155 pb | 124 pb | 188 pb |
| 2004 | 165 pb | 126 pb | 199 pb |
| 2005 | 88 pb | 23 pb | 131 pb |
| 2006 | 18 pb | 6 pb | 32 pb |
| 2007 | 9 pb | −5 pb | 32 pb |
| 2008 | 95 pb | 54 pb | 171 pb |
| 2009 | 200 pb | 167 pb | 218 pb |
| 2010 | 169 pb | 139 pb | 221 pb |
| 2011 | 141 pb | 110 pb | 158 pb |
| 2012 | 76 pb | 65 pb | 100 pb |
| 2013 | 115 pb | 76 pb | 157 pb |
| 2014 | 118 pb | 84 pb | 150 pb |
| 2015 | 97 pb | 77 pb | 117 pb |
| 2016 | 69 pb | 49 pb | 96 pb |
| 2017 | 69 pb | 34 pb | 95 pb |
| 2018 | 29 pb | 8 pb | 51 pb |
| 2019 | 0 pb | −18 pb | 14 pb |
| 2020 | 24 pb | −12 pb | 49 pb |
| 2021 | 88 pb | 47 pb | 124 pb |
| 2022 | −13 pb | −87 pb | 58 pb |
| 2023 | −91 pb | −123 pb | −71 pb |
| 2024 | −33 pb | −69 pb | 17 pb |
| 2025 | 63 pb | 36 pb | 81 pb |
| 2026 | 70 pb | 61 pb | 84 pb |
Les appels à l'origine de ce graphique
call_tool{"name": "boc_get_observations", "arguments": {"series_names": ["BD.CDN.2YR.DQ.YLD", "BD.CDN.10YR.DQ.YLD"], "start_date": "2001-01-01"}}R
# ============================================================
# Bank of Canada Valet: BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD
# Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
# (exact: Valet request rebuilt from the tool's arguments)
# Inputs: https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01
# Outputs: data/raw/valet_observations.json; the prepared table as `data`
# ============================================================
# 0. Setup ----
library(dplyr)
library(httr2)
library(janitor)
library(jsonlite)
library(lubridate)
library(readr)
library(stringr)
library(tibble)
library(tidyr)
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
response <- request("https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01") |>
req_perform()
writeLines(resp_body_string(response), "data/raw/valet_observations.json")
payload <- fromJSON("data/raw/valet_observations.json", flatten = TRUE)
data <- as_tibble(payload[["observations"]])
# 2. Check inputs ----
stopifnot("https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01 returned no rows" = nrow(data) > 0)
# 3. Prepare data ----
data <- data |>
clean_names()
# Valet nests each series as <series>.v; make one row per date and series.
data <- data |>
pivot_longer(-d, names_to = "series", values_to = "value") |>
mutate(
series = str_remove(series, "_v$") |> str_to_upper(),
value = as.numeric(value),
date = ymd(d)
) |>
select(date, series, value)
# Standard cleaning: trimmed text, empty strings as missing, and numbers
# stored as text converted to numbers.
data <- data |>
mutate(across(where(is.character), \(x) na_if(str_trim(x), ""))) |>
type_convert()Python
# ============================================================
# Bank of Canada Valet: BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD
# Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
# (exact: Valet request rebuilt from the tool's arguments)
# Inputs: https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01
# Outputs: data/raw/valet_observations.json; the prepared table as `data`
# ============================================================
# %% 0. Setup
import json
import re
import unicodedata
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "valet_observations.json"
# %% 1. Read inputs
with httpx.Client(
http2=True,
follow_redirects=True,
timeout=300,
headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"},
) as client:
response = client.get('https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01')
response.raise_for_status()
raw_path.write_bytes(response.content)
payload = json.loads(raw_path.read_text(encoding="utf-8"))
records = payload['observations']
data = pl.json_normalize(records, strict=False)
# %% 2. Check inputs
assert data.height > 0, "https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01 returned no rows"
# %% 3. Prepare data
# Valet nests each series as <series>.v; make one row per date and series.
data = data.unpivot(index="d", variable_name="series", value_name="value")
data = data.with_columns(
pl.col("d").str.to_date().alias("date"),
pl.col("series").str.replace(r"\.v$", ""),
pl.col("value").cast(pl.Float64, strict=False),
).select("date", "series", "value")
# Standard cleaning: snake_case names without accents (PÉRIODE -> periode,
# referenceNumber -> reference_number, as janitor does in R), trimmed text,
# empty strings as missing. Names that clean alike are numbered as janitor
# numbers them (Indicator, indicator -> indicator, indicator_2).
clean_names = [
re.sub(
r"[^0-9a-z]+",
"_",
re.sub(
r"([a-z0-9])([A-Z])",
r"\1_\2",
unicodedata.normalize("NFKD", column).encode("ascii", "ignore").decode(),
).lower(),
).strip("_")
for column in data.columns
]
while len(set(clean_names)) < len(clean_names):
name_counts = {}
numbered = []
for name in clean_names:
name_counts[name] = name_counts.get(name, 0) + 1
count = name_counts[name]
numbered.append(name if count == 1 else f"{name}_{count}")
clean_names = numbered
data = data.rename(dict(zip(data.columns, clean_names)))
data = data.with_columns(pl.col(pl.Utf8).str.strip_chars().replace("", None))Stata
* ============================================================
* Bank of Canada Valet: BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD
* Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
* (exact: Valet request rebuilt from the tool's arguments)
* Inputs: https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01
* Outputs: data/raw/valet_observations.json; the prepared table as `data`
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/boc_get_observations.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import json
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "valet_observations.json"
with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.get('https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01')
response.raise_for_status()
raw_path.write_bytes(response.content)
payload = json.loads(raw_path.read_text(encoding="utf-8"))
records = payload['observations']
data = pl.json_normalize(records, strict=False)
nested = [name for name, dtype in data.schema.items() if dtype.is_nested()]
data = data.with_columns(pl.col(name).map_elements(lambda value: json.dumps(value.to_list() if isinstance(value, pl.Series) else value, default=str), return_dtype=pl.Utf8) for name in nested)
data.write_csv(RAW_DIR / "valet_observations_prepared.csv")
end
import delimited "data/raw/valet_observations_prepared.csv", clear varnames(1) encoding("utf-8")
* 2. Check inputs
assert _N > 0
* 3. Prepare data
* One column per series (<series>_v), one row per date.
generate date = date(d, "YMD")
format date %td
drop d
* Standard cleaning: lower-case names, trimmed text, and numbers stored as
* text converted (destring leaves genuinely non-numeric text alone).
* rename *, lower stops at a clash (Indicator next to indicator), so names
* that lower-case alike are numbered as janitor numbers them (indicator,
* indicator_2), then renamed in one group rename, which allows swaps.
local names
foreach var of varlist _all {
local names `names' `=strlower("`var'")'
}
local dups : list dups names
while "`dups'" != "" {
local numbered
local before
foreach name of local names {
local count 1
foreach earlier of local before {
if "`earlier'" == "`name'" local ++count
}
local before `before' `name'
if `count' > 1 {
local name = substr("`name'", 1, 32 - strlen("_`count'")) + "_`count'"
}
local numbered `numbered' `name'
}
local names `numbered'
local dups : list dups names
}
local old_names
local new_names
local i 0
foreach var of varlist _all {
local ++i
local name : word `i' of `names'
if "`name'" != "`var'" {
local old_names `old_names' `var'
local new_names `new_names' `name'
}
}
if "`old_names'" != "" {
rename (`old_names') (`new_names')
}
quietly ds, has(type string)
local text_vars `r(varlist)'
foreach var of local text_vars {
replace `var' = strtrim(`var')
}
destring, replace
log closeJulia
# ============================================================
# Bank of Canada Valet: BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD
# Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
# (exact: Valet request rebuilt from the tool's arguments)
# Inputs: https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01
# Outputs: data/raw/valet_observations.json; the prepared table as `data`
# ============================================================
# 0. Setup
using DataFrames
using Downloads
using JSON3
using Tables
using TidierData
mkpath("data/raw")
# 1. Read inputs
Downloads.download("https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01", "data/raw/valet_observations.json")
payload = JSON3.read(read("data/raw/valet_observations.json", String))
records = payload["observations"]
data = DataFrame(Tables.dictrowtable(records))
# 2. Check inputs
@assert nrow(data) > 0 "https://www.bankofcanada.ca/valet/observations/BD.CDN.2YR.DQ.YLD,BD.CDN.10YR.DQ.YLD/json?start_date=2001-01-01 returned no rows"
# 3. Prepare data
# Standard cleaning: snake_case names, trimmed text, empty strings as missing.
data = @chain data begin
@clean_names
end
data = mapcols(
col -> eltype(col) <: Union{Missing, AbstractString} ?
[ismissing(x) || isempty(strip(x)) ? missing : strip(x) for x in col] : col,
data,
)1 610 458
brevets pour les scientifiques, qui vérifient qui est arrivé le premier,
L'apprentissage automatique au bureau des brevets
Les demandes de brevet canadiennes dans la classe CIB G06N, où se classe l'apprentissage automatique, par année de dépôt, d'après les données Horizons PI d'ISDE. Les demandes sont passées de 24 en 2005 à un sommet de 667 en 2021. Les comptes viennent de la diffusion en bloc Horizons PI datée du 11 octobre 2024, la plus récente quand j'ai enregistré les appels. Les demandes sont publiées environ 18 mois après leur dépôt ; cette diffusion ne contient donc qu'une partie de celles déposées de 2023 à 2025 (122 pour l'instant), et ces années sont exclues.
Voir les données
| Année de dépôt | G06N |
|---|---|
| 2005 | 24 |
| 2006 | 15 |
| 2007 | 24 |
| 2008 | 39 |
| 2009 | 18 |
| 2010 | 24 |
| 2011 | 12 |
| 2012 | 26 |
| 2013 | 36 |
| 2014 | 46 |
| 2015 | 56 |
| 2016 | 77 |
| 2017 | 178 |
| 2018 | 256 |
| 2019 | 478 |
| 2020 | 529 |
| 2021 | 667 |
| 2022 | 654 |
Les appels à l'origine de ce graphique
call_tool{"name": "ised_ip_horizons_search_patents", "arguments": {"filed_from": "1800-01-01", "limit": 1}}21 autres appels semblables, avec d'autres arguments.
R
# ============================================================
# Canadian patent search (CIPO IP Horizons)
# Purpose: Fetch the data behind MapleStats MCP's ised_ip_horizons_search_patents
# (exact: the IP Horizons tables the search used, then the tool's filters)
# Inputs: https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip
# Outputs: data/raw/PT_main_1_to_2000000_2024-10-11.zip; the prepared table as `data`
# ============================================================
# 0. Setup ----
library(dplyr)
library(janitor)
library(purrr)
library(readr)
library(stringr)
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
files <- c(
main_1 = "https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip",
main_2 = "https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_2000001_to_4000000_2024-10-11.zip"
)
walk2(
files,
names(files),
\(url, name) download.file(url, str_c("data/raw/", name, ".zip"), mode = "wb")
)
# Every column as text; headers are bilingual ("Patent Number - Numéro du
# brevet"), so keep the English half in snake_case.
tables <- names(files) |>
map(
\(name) unzip(str_c("data/raw/", name, ".zip"), exdir = "data/raw") |>
read_delim(
delim = "|",
quote = "",
na = c("", "NULL"),
col_types = cols(.default = col_character()),
trim_ws = TRUE
) |>
rename_with(\(x) str_split_i(x, " - ", 1) |> make_clean_names())
) |>
set_names(names(files))
patents <- bind_rows(tables[c("main_1", "main_2")]) |>
filter(str_detect(filing_date, "^\\d{4}-\\d{2}-\\d{2}$"), filing_date >= "1800-01-01")
data <- patents |>
arrange(desc(filing_date))
# 2. Check inputs ----
stopifnot("https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip returned no rows" = nrow(data) > 0)
# 3. Prepare data ----
data <- data |>
clean_names()
# Standard cleaning: trimmed text, empty strings as missing, and numbers
# stored as text converted to numbers.
data <- data |>
mutate(across(where(is.character), \(x) na_if(str_trim(x), ""))) |>
type_convert()Python
# ============================================================
# Canadian patent search (CIPO IP Horizons)
# Purpose: Fetch the data behind MapleStats MCP's ised_ip_horizons_search_patents
# (exact: the IP Horizons tables the search used, then the tool's filters)
# Inputs: https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip
# Outputs: data/raw/PT_main_1_to_2000000_2024-10-11.zip; the prepared table as `data`
# ============================================================
# %% 0. Setup
import re
import ssl
import unicodedata
import zipfile
from pathlib import Path
import certifi
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "PT_main_1_to_2000000_2024-10-11.zip"
# %% 1. Read inputs
# CIPO's server omits its RapidSSL intermediate certificate; add it from the
# issuer on top of certifi's roots, so verification still anchors at a root.
intermediate = httpx.get("http://cacerts.rapidssl.com/RapidSSLTLSRSACAG1.crt").content
tls = ssl.create_default_context(cafile=certifi.where())
tls.load_verify_locations(cadata=ssl.DER_cert_to_PEM_cert(intermediate))
files = {
'main_1': 'https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip',
'main_2': 'https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_2000001_to_4000000_2024-10-11.zip',
}
tables = {}
with httpx.Client(verify=tls, timeout=900, follow_redirects=True) as client:
for name, url in files.items():
zip_path = RAW_DIR / f"{name}.zip"
with client.stream("GET", url) as response, zip_path.open("wb") as handle:
response.raise_for_status()
for chunk in response.iter_bytes(1 << 20):
handle.write(chunk)
with zipfile.ZipFile(zip_path) as archive:
member = archive.namelist()[0]
archive.extract(member, RAW_DIR)
# Every column as text; headers are bilingual ("Patent Number - Numéro
# du brevet"), so keep the English half in snake_case.
tables[name] = (
pl.scan_csv(
RAW_DIR / member,
separator="|",
quote_char=None,
null_values=["NULL"],
infer_schema=False,
)
.rename(
lambda column: re.sub(
r"[^a-z0-9]+", "_", column.split(" - ")[0].strip().lower()
).strip("_")
)
.with_columns(pl.all().str.strip_chars())
)
patents = pl.concat([tables[name] for name in ['main_1', 'main_2']]).filter(pl.col("filing_date").str.contains(r"^\d{4}-\d{2}-\d{2}$"), pl.col("filing_date") >= "1800-01-01")
data = patents.sort("filing_date", descending=True, nulls_last=True).collect()
# %% 2. Check inputs
assert data.height > 0, "https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip returned no rows"
# %% 3. Prepare data
# Standard cleaning: snake_case names without accents (PÉRIODE -> periode,
# referenceNumber -> reference_number, as janitor does in R), trimmed text,
# empty strings as missing. Names that clean alike are numbered as janitor
# numbers them (Indicator, indicator -> indicator, indicator_2).
clean_names = [
re.sub(
r"[^0-9a-z]+",
"_",
re.sub(
r"([a-z0-9])([A-Z])",
r"\1_\2",
unicodedata.normalize("NFKD", column).encode("ascii", "ignore").decode(),
).lower(),
).strip("_")
for column in data.columns
]
while len(set(clean_names)) < len(clean_names):
name_counts = {}
numbered = []
for name in clean_names:
name_counts[name] = name_counts.get(name, 0) + 1
count = name_counts[name]
numbered.append(name if count == 1 else f"{name}_{count}")
clean_names = numbered
data = data.rename(dict(zip(data.columns, clean_names)))
data = data.with_columns(pl.col(pl.Utf8).str.strip_chars().replace("", None))Stata
* ============================================================
* Canadian patent search (CIPO IP Horizons)
* Purpose: Fetch the data behind MapleStats MCP's ised_ip_horizons_search_patents
* (exact: the IP Horizons tables the search used, then the tool's filters)
* Inputs: https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip
* Outputs: data/raw/PT_main_1_to_2000000_2024-10-11.zip; the prepared table as `data`
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/ised_ip_horizons_search_patents.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import json
import re
import ssl
import zipfile
from pathlib import Path
import certifi
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "PT_main_1_to_2000000_2024-10-11.zip"
intermediate = httpx.get("http://cacerts.rapidssl.com/RapidSSLTLSRSACAG1.crt").content
tls = ssl.create_default_context(cafile=certifi.where())
tls.load_verify_locations(cadata=ssl.DER_cert_to_PEM_cert(intermediate))
files = {'main_1': 'https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip', 'main_2': 'https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_2000001_to_4000000_2024-10-11.zip'}
tables = {}
exec('with httpx.Client(verify=tls, timeout=900, follow_redirects=True) as client:\n for name, url in files.items():\n zip_path = RAW_DIR / f"{name}.zip"\n with client.stream("GET", url) as response, zip_path.open("wb") as handle:\n response.raise_for_status()\n for chunk in response.iter_bytes(1 << 20):\n handle.write(chunk)\n with zipfile.ZipFile(zip_path) as archive:\n member = archive.namelist()[0]\n archive.extract(member, RAW_DIR)\n tables[name] = (pl.scan_csv(RAW_DIR / member, separator="|", quote_char=None, null_values=["NULL"], infer_schema=False) .rename(lambda column: re.sub(r"[^a-z0-9]+", "_", column.split(" - ")[0].strip().lower()).strip("_")) .with_columns(pl.all().str.strip_chars()))\n')
patents = pl.concat([tables[name] for name in ['main_1', 'main_2']]).filter(pl.col("filing_date").str.contains('^\\d{4}-\\d{2}-\\d{2}\x24'), pl.col("filing_date") >= "1800-01-01")
data = patents.sort("filing_date", descending=True, nulls_last=True).collect()
nested = [name for name, dtype in data.schema.items() if dtype.is_nested()]
data = data.with_columns(pl.col(name).map_elements(lambda value: json.dumps(value.to_list() if isinstance(value, pl.Series) else value, default=str), return_dtype=pl.Utf8) for name in nested)
data.write_csv(RAW_DIR / "PT_main_1_to_2000000_202_prepared.csv")
end
import delimited "data/raw/PT_main_1_to_2000000_202_prepared.csv", clear varnames(1) encoding("utf-8")
* 2. Check inputs
assert _N > 0
* 3. Prepare data
* Standard cleaning: lower-case names, trimmed text, and numbers stored as
* text converted (destring leaves genuinely non-numeric text alone).
* rename *, lower stops at a clash (Indicator next to indicator), so names
* that lower-case alike are numbered as janitor numbers them (indicator,
* indicator_2), then renamed in one group rename, which allows swaps.
local names
foreach var of varlist _all {
local names `names' `=strlower("`var'")'
}
local dups : list dups names
while "`dups'" != "" {
local numbered
local before
foreach name of local names {
local count 1
foreach earlier of local before {
if "`earlier'" == "`name'" local ++count
}
local before `before' `name'
if `count' > 1 {
local name = substr("`name'", 1, 32 - strlen("_`count'")) + "_`count'"
}
local numbered `numbered' `name'
}
local names `numbered'
local dups : list dups names
}
local old_names
local new_names
local i 0
foreach var of varlist _all {
local ++i
local name : word `i' of `names'
if "`name'" != "`var'" {
local old_names `old_names' `var'
local new_names `new_names' `name'
}
}
if "`old_names'" != "" {
rename (`old_names') (`new_names')
}
quietly ds, has(type string)
local text_vars `r(varlist)'
foreach var of local text_vars {
replace `var' = strtrim(`var')
}
destring, replace
log closeJulia
# ============================================================
# Canadian patent search (CIPO IP Horizons)
# Purpose: Fetch the data behind MapleStats MCP's ised_ip_horizons_search_patents
# (exact: the IP Horizons tables the search used, then the tool's filters)
# Inputs: https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip
# Outputs: data/raw/PT_main_1_to_2000000_2024-10-11.zip; the prepared table as `data`
# ============================================================
# 0. Setup
using CSV
using DataFrames
using Downloads
using TidierData
using ZipFile
mkpath("data/raw")
# 1. Read inputs
files = Dict(
"main_1" => "https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip",
"main_2" => "https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_2000001_to_4000000_2024-10-11.zip",
)
tables = Dict{String, DataFrame}()
for (name, url) in files
zip_path = "data/raw/$(name).zip"
Downloads.download(url, zip_path)
archive = ZipFile.Reader(zip_path)
member = archive.files[1]
csv_path = "data/raw/$(member.name)"
write(csv_path, read(member))
close(archive)
# Every column as text; keep the English half of each bilingual header.
table = CSV.read(csv_path, DataFrame; delim = '|', quoted = false, missingstring = "NULL", types = String)
rename!(table, [lowercase(replace(strip(split(n, " - ")[1]), r"[^A-Za-z0-9]+" => "_")) for n in names(table)])
tables[name] = mapcols(col -> [ismissing(x) ? missing : strip(x) for x in col], table)
end
patents = vcat([tables[n] for n in ["main_1", "main_2"]]...)
patents = filter(row -> (!ismissing(row.filing_date) && occursin(r"^\d{4}-\d{2}-\d{2}$", row.filing_date) && row.filing_date >= "1800-01-01"), patents)
data = sort(patents, "filing_date"; rev = true)
# 2. Check inputs
@assert nrow(data) > 0 "https://opic-cipo.ca/cipo/client_downloads/Patent_CSV_2024_10_11/PT_main_1_to_2000000_2024-10-11.zip returned no rows"
# 3. Prepare data
# Standard cleaning: snake_case names, trimmed text, empty strings as missing.
data = @chain data begin
@clean_names
end
data = mapcols(
col -> eltype(col) <: Union{Missing, AbstractString} ?
[ismissing(x) || isempty(strip(x)) ? missing : strip(x) for x in col] : col,
data,
)15 937
séries de la Banque du Canada pour les analystes financiers, condamnés à expliquer le taux,
Le taux directeur depuis 2015
Le taux cible du financement à un jour de la Banque du Canada, chaque jour ouvrable depuis janvier 2015 : 29 changements, d'un creux de 0,25 % à un sommet de 5,00 %. Le 6 octobre 2026, il s'établissait à 2,25 %.
Voir les données
| Date | Taux |
|---|---|
| 1er janvier 2015 | 1,00 % |
| 21 janvier 2015 | 0,75 % |
| 15 juillet 2015 | 0,50 % |
| 12 juillet 2017 | 0,75 % |
| 6 septembre 2017 | 1,00 % |
| 17 janvier 2018 | 1,25 % |
| 11 juillet 2018 | 1,50 % |
| 24 octobre 2018 | 1,75 % |
| 4 mars 2020 | 1,25 % |
| 16 mars 2020 | 0,75 % |
| 27 mars 2020 | 0,25 % |
| 3 mars 2022 | 0,50 % |
| 14 avril 2022 | 1,00 % |
| 2 juin 2022 | 1,50 % |
| 14 juillet 2022 | 2,50 % |
| 8 septembre 2022 | 3,25 % |
| 27 octobre 2022 | 3,75 % |
| 8 décembre 2022 | 4,25 % |
| 26 janvier 2023 | 4,50 % |
| 8 juin 2023 | 4,75 % |
| 13 juillet 2023 | 5,00 % |
| 6 juin 2024 | 4,75 % |
| 25 juillet 2024 | 4,50 % |
| 5 septembre 2024 | 4,25 % |
| 24 octobre 2024 | 3,75 % |
| 12 décembre 2024 | 3,25 % |
| 30 janvier 2025 | 3,00 % |
| 13 mars 2025 | 2,75 % |
| 18 septembre 2025 | 2,50 % |
| 30 octobre 2025 | 2,25 % |
| 6 octobre 2026 | 2,25 % |
Les appels à l'origine de ce graphique
call_tool{"name": "boc_get_observations", "arguments": {"series_names": ["V39079"], "start_date": "2015-01-01", "lang": "fr"}}R
# ============================================================
# Bank of Canada Valet: V39079
# Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
# (exact: Valet request rebuilt from the tool's arguments)
# Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01
# Outputs: data/raw/valet_observations.json; the prepared table as `data`
# ============================================================
# 0. Setup ----
library(dplyr)
library(httr2)
library(janitor)
library(jsonlite)
library(lubridate)
library(purrr)
library(readr)
library(stringr)
library(tibble)
library(tidyr)
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
# 1. Read inputs ----
response <- request("https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01") |>
req_perform()
writeLines(resp_body_string(response), "data/raw/valet_observations.json")
payload <- fromJSON("data/raw/valet_observations.json", flatten = TRUE)
data <- as_tibble(payload[["observations"]])
# 2. Check inputs ----
stopifnot("https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01 returned no rows" = nrow(data) > 0)
# 3. Prepare data ----
data <- data |>
clean_names()
# Valet nests each series as <series>.v; make one row per date and series.
data <- data |>
pivot_longer(-d, names_to = "series", values_to = "value") |>
mutate(
series = str_remove(series, "_v$") |> str_to_upper(),
value = as.numeric(value),
date = ymd(d)
) |>
select(date, series, value)
# Standard cleaning: trimmed text, empty strings as missing, and numbers
# stored as text converted to numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns <- "^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$"
data <- data |>
mutate(across(where(is.character), \(x) na_if(str_trim(x), "")))
keep_text <- names(data)[
str_detect(names(data), code_columns) |
map_lgl(
data,
\(column) is.character(column) && any(str_detect(column, "^-?0[0-9]"), na.rm = TRUE)
)
]
data <- data |>
mutate(
across(any_of(keep_text), \(x) if (is.list(x)) x else as.character(x)),
across(where(is.character) & !any_of(keep_text), parse_guess)
)Python
# ============================================================
# Bank of Canada Valet: V39079
# Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
# (exact: Valet request rebuilt from the tool's arguments)
# Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01
# Outputs: data/raw/valet_observations.json; the prepared table as `data`
# ============================================================
# %% 0. Setup
import json
import re
import unicodedata
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "valet_observations.json"
# %% 1. Read inputs
with httpx.Client(
http2=True,
follow_redirects=True,
timeout=300,
headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"},
) as client:
response = client.get('https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01')
response.raise_for_status()
raw_path.write_bytes(response.content)
payload = json.loads(raw_path.read_text(encoding="utf-8"))
records = payload['observations']
data = pl.json_normalize(records, strict=False)
# %% 2. Check inputs
assert data.height > 0, "https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01 returned no rows"
# %% 3. Prepare data
# Valet nests each series as <series>.v; make one row per date and series.
data = data.unpivot(index="d", variable_name="series", value_name="value")
data = data.with_columns(
pl.col("d").str.to_date().alias("date"),
pl.col("series").str.replace(r"\.v$", ""),
pl.col("value").cast(pl.Float64, strict=False),
).select("date", "series", "value")
# Standard cleaning: snake_case names as janitor's clean_names() gives them
# in R (PÉRIODE -> periode, referenceNumber -> reference_number, # -> number,
# % -> percent, apostrophes dropped, a leading digit prefixed with x),
# trimmed text, empty strings as missing. Names that clean alike are
# numbered as janitor numbers them (Indicator, indicator -> indicator,
# indicator_2).
clean_names = [
re.sub(
r"[^0-9a-z]+",
"_",
re.sub(
r"([a-z0-9])([A-Z])",
r"\1_\2",
"".join(
" " if unicodedata.category(ch)[0] in "PS" and not ch.isascii() else ch
for ch in unicodedata.normalize("NFKD", column.replace("'", ""))
)
.replace("#", " number ")
.replace("%", " percent ")
.encode("ascii", "ignore")
.decode(),
).lower(),
).strip("_")
or "x"
for column in data.columns
]
clean_names = [f"x{name}" if name[0].isdigit() else name for name in clean_names]
while len(set(clean_names)) < len(clean_names):
name_counts = {}
numbered = []
for name in clean_names:
name_counts[name] = name_counts.get(name, 0) + 1
count = name_counts[name]
numbered.append(name if count == 1 else f"{name}_{count}")
clean_names = numbered
data = data.rename(dict(zip(data.columns, clean_names)))
data = data.with_columns(pl.col(pl.Utf8).str.strip_chars().replace("", None))
# Numbers stored as text become numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns = re.compile(r"^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$")
for column in data.columns:
dtype = data.schema[column]
if code_columns.search(column) and not dtype.is_nested():
data = data.with_columns(pl.col(column).cast(pl.Utf8))
continue
if dtype != pl.Utf8 or data[column].null_count() == data.height:
continue
numbers = data[column].cast(pl.Float64, strict=False)
leading_zero = data[column].str.contains(r"^-?0[0-9]").any()
if numbers.null_count() > data[column].null_count() or leading_zero:
continue
whole = data[column].cast(pl.Int64, strict=False)
data = data.with_columns(whole if whole.null_count() == numbers.null_count() else numbers)Stata
* ============================================================
* Bank of Canada Valet: V39079
* Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
* (exact: Valet request rebuilt from the tool's arguments)
* Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01
* Outputs: data/raw/valet_observations.json; the prepared table as `data`
* ============================================================
version 18
clear all
set more off
* 0. Setup
capture mkdir "logs"
capture log close
log using "logs/boc_get_observations.log", replace
capture mkdir "data"
capture mkdir "data/raw"
* 1. Read inputs
* Stata reads no JSON or HTML and truncates long column names, so its
* built-in Python (Stata 16+) fetches, filters and writes a CSV. Point
* Stata at a Python with these packages first: python set exec <path>.
python:
import json
from pathlib import Path
import httpx
import polars as pl
RAW_DIR = Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)
raw_path = RAW_DIR / "valet_observations.json"
with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.get('https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01')
response.raise_for_status()
raw_path.write_bytes(response.content)
payload = json.loads(raw_path.read_text(encoding="utf-8"))
records = payload['observations']
data = pl.json_normalize(records, strict=False)
nested = [name for name, dtype in data.schema.items() if dtype.is_nested()]
data = data.with_columns(pl.col(name).map_elements(lambda value: json.dumps(value.to_list() if isinstance(value, pl.Series) else value, default=str), return_dtype=pl.Utf8) for name in nested)
data.write_csv(RAW_DIR / "valet_observations_prepared.csv")
end
import delimited "data/raw/valet_observations_prepared.csv", clear varnames(1) encoding("utf-8") stringcols(_all) bindquote(strict) maxquotedrows(unlimited)
* 2. Check inputs
assert _N > 0
* 3. Prepare data
* One column per series (<series>_v), one row per date.
generate date = date(d, "YMD")
format date %td
drop d
* Standard cleaning: lower-case names, trimmed text, and numbers stored as
* text converted (destring leaves genuinely non-numeric text alone). Codes
* stay text as the source wrote them: variables named like one (coordinate,
* vectorid, dguid, NAICS, postal code) and ones with a leading zero (01).
* rename *, lower stops at a clash (Indicator next to indicator), so names
* that lower-case alike are numbered as janitor numbers them (indicator,
* indicator_2), then renamed in one group rename, which allows swaps.
local names
foreach var of varlist _all {
local names `names' `=strlower("`var'")'
}
local dups : list dups names
while "`dups'" != "" {
local numbered
local before
foreach name of local names {
local count 1
foreach earlier of local before {
if "`earlier'" == "`name'" local ++count
}
local before `before' `name'
if `count' > 1 {
local name = substr("`name'", 1, 32 - strlen("_`count'")) + "_`count'"
}
local numbered `numbered' `name'
}
local names `numbered'
local dups : list dups names
}
local old_names
local new_names
local i 0
foreach var of varlist _all {
local ++i
local name : word `i' of `names'
if "`name'" != "`var'" {
local old_names `old_names' `var'
local new_names `new_names' `name'
}
}
if "`old_names'" != "" {
rename (`old_names') (`new_names')
}
quietly ds, has(type string)
local text_vars `r(varlist)'
foreach var of local text_vars {
replace `var' = strtrim(`var')
if regexm("`var'|", "^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)[|]") continue
quietly count if regexm(`var', "^-?0[0-9]")
if r(N) > 0 continue
destring `var', replace
}
log closeJulia
# ============================================================
# Bank of Canada Valet: V39079
# Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
# (exact: Valet request rebuilt from the tool's arguments)
# Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01
# Outputs: data/raw/valet_observations.json; the prepared table as `data`
# ============================================================
# 0. Setup
using DataFrames
using Dates
using Downloads
using JSON3
using Tables
using TidierData
mkpath("data/raw")
# 1. Read inputs
Downloads.download("https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01", "data/raw/valet_observations.json")
payload = JSON3.read(read("data/raw/valet_observations.json", String))
records = payload["observations"]
data = DataFrame(Tables.dictrowtable(records))
data = mapcols(col -> [x === nothing ? missing : x for x in col], data)
# 2. Check inputs
@assert nrow(data) > 0 "https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01 returned no rows"
# 3. Prepare data
# Valet nests each series as {v: value}; make one row per date and series.
data = stack(data, Not("d"); variable_name = "series", value_name = "cell")
data.value = [
ismissing(cell) ? missing :
something(tryparse(Float64, string(get(cell, :v, ""))), missing)
for cell in data.cell
]
data.date = Date.(data.d)
data = select(data, "date", "series", "value")
# Standard cleaning: snake_case names, trimmed text, empty strings as missing.
data = @chain data begin
@clean_names
end
data = mapcols(
col -> eltype(col) <: Union{Missing, AbstractString} ?
[ismissing(x) || isempty(strip(x)) ? missing : strip(x) for x in col] : col,
data,
)
# Numbers stored as text become numbers. Codes stay text as the source
# wrote them: columns named like one (coordinate, vector_id, dguid, NAICS,
# postal code) and columns with a leading zero (01, 000000001).
code_columns = r"^(coordinate|vector|vector_?id|product_?id|pid|dguid|[a-z0-9_]*_dguid|postal_?code|[a-z0-9_]*_postal_code|fsa|[a-z0-9_]*naics[a-z0-9_]*|noc|noc_[a-z0-9_]*|[a-z0-9_]*_noc|sgc|sgc_[a-z0-9_]*|[a-z0-9_]*_sgc)$"
for name in names(data)
column = data[!, name]
if occursin(code_columns, name)
data[!, name] = [ismissing(x) ? missing : string(x) for x in column]
continue
end
eltype(column) <: Union{Missing, AbstractString} || continue
values = collect(skipmissing(column))
(isempty(values) || any(v -> occursin(r"^-?0[0-9]", v), values)) && continue
any(v -> isnothing(tryparse(Float64, v)), values) && continue
whole = all(v -> !isnothing(tryparse(Int, v)), values)
data[!, name] = [
ismissing(x) ? missing : parse(whole ? Int : Float64, x) for x in column
]
endExcel (Power Query)
// ============================================================
// Bank of Canada Valet: V39079
// Purpose: Fetch the data behind MapleStats MCP's boc_get_observations
// (exact: Valet request rebuilt from the tool's arguments)
// Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01
// Outputs: the prepared table, loaded to a worksheet or the Data Model
// How to run: Excel > Data > Get Data > From Other Sources > Blank Query >
// Advanced Editor; paste this query, Done, then Close & Load. Answer the
// first prompt with Anonymous access; Data > Refresh All fetches it again.
// A worksheet holds 1,048,576 rows: load a larger table (a full StatCan
// table can be) with Close & Load To > Only Create Connection > Add this
// data to the Data Model.
// ============================================================
let
// 0. Setup
// Cell values as text or number, null-safe, for the filters and checks.
AsText = (value as any) as text =>
if value = null then "" else (try Text.From(value) otherwise ""),
AsNumber = (value as any) as nullable number =>
if value = null then null else (try Number.From(value, "en-US") otherwise null),
// Records to a table with every field any record has, not only the first's.
ToTable = (records as list) as table =>
Table.FromRecords(
records,
List.Distinct(List.Combine(List.Transform(records, Record.FieldNames))),
MissingField.UseNull
),
// 1. Read inputs
Raw = Web.Contents("https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01"),
Payload = Json.Document(Raw),
Loaded = ToTable(Record.Field(Payload, "observations")),
// 2. Check inputs
// Stop with a clear message instead of loading an empty table.
Checked = if Table.IsEmpty(Loaded)
then error Error.Record("MapleStats", "https://www.bankofcanada.ca/valet/observations/V39079/json?start_date=2015-01-01 returned no rows")
else Loaded,
// 3. Prepare data
// Valet nests each series as <series>[v]; make one row per date and series.
Long = Table.UnpivotOtherColumns(Checked, {"d"}, "series", "cell"),
Valued = Table.AddColumn(
Long, "value", each try Number.From([cell][v], "en-US") otherwise null, type number
),
Tidy = Table.RenameColumns(Table.SelectColumns(Valued, {"d", "series", "value"}), {{"d", "date"}}),
// Standard cleaning, as the R script's clean_names() and type_convert():
// snake_case names without accents (PÉRIODE -> periode, referenceNumber ->
// reference_number), names that clean alike numbered (indicator,
// indicator_2), trimmed text, empty text as null, then columns whose
// every value is a number or an ISO date (YYYY-MM-DD) converted.
Accents = [#"à" = "a", #"â" = "a", #"ä" = "a", #"á" = "a", #"ç" = "c", #"é" = "e", #"è" = "e", #"ê" = "e", #"ë" = "e", #"î" = "i", #"ï" = "i", #"í" = "i", #"ô" = "o", #"ö" = "o", #"ó" = "o", #"ù" = "u", #"û" = "u", #"ü" = "u", #"ú" = "u", #"ÿ" = "y", #"ñ" = "n", #"œ" = "oe", #"æ" = "ae", #"À" = "A", #"Â" = "A", #"Ä" = "A", #"Á" = "A", #"Ç" = "C", #"É" = "E", #"È" = "E", #"Ê" = "E", #"Ë" = "E", #"Î" = "I", #"Ï" = "I", #"Í" = "I", #"Ô" = "O", #"Ö" = "O", #"Ó" = "O", #"Ù" = "U", #"Û" = "U", #"Ü" = "U", #"Ú" = "U", #"Ÿ" = "Y", #"Ñ" = "N", #"Œ" = "OE", #"Æ" = "AE"],
CleanName = (name as text) as text =>
let
Plain = Text.Combine(
List.Transform(Text.ToList(name), (ch) => Record.FieldOrDefault(Accents, ch, ch))
),
Words = Splitter.SplitTextByCharacterTransition({"a".."z", "0".."9"}, {"A".."Z"})(
Plain
),
Lower = Text.Lower(Text.Combine(Words, "_")),
Spaced = Text.Combine(
List.Transform(
Text.ToList(Lower), (ch) => if List.Contains(NameCharacters, ch) then ch else " "
)
),
Snake = Text.Combine(List.Select(Text.Split(Spaced, " "), (part) => part <> ""), "_")
in
if Snake = "" then "x" else Snake,
NameCharacters = {"a".."z", "0".."9"},
OldNames = Table.ColumnNames(Tidy),
CleanNames = List.Transform(OldNames, CleanName),
NewNames = List.Transform(
List.Positions(CleanNames),
(i) =>
let
Earlier = List.Count(
List.Select(List.FirstN(CleanNames, i), (name) => name = CleanNames{i})
)
in
if Earlier = 0 then CleanNames{i} else CleanNames{i} & "_" & Text.From(Earlier + 1)
),
Renamed = Table.RenameColumns(
Tidy,
List.Select(List.Zip({OldNames, NewNames}), (pair) => pair{0} <> pair{1})
),
Trimmed = Table.TransformColumns(
Renamed,
List.Transform(
Table.ColumnNames(Renamed),
(name) =>
{
name,
(value) =>
if value is text
then (if Text.Trim(value) = "" then null else Text.Trim(value))
else value
}
)
),
IsNumberText = (value as any) as logical =>
value is number
or (value is text and (try Number.FromText(value, "en-US") otherwise null) <> null),
IsIsoDate = (value as any) as logical =>
value is text
and Text.Length(value) = 10
and Text.Middle(value, 4, 1) = "-"
and Text.Middle(value, 7, 1) = "-"
and (try Date.FromText(value, "en-US") otherwise null) <> null,
ColumnsWhere = (table as table, test as function) as list =>
List.Select(
Table.ColumnNames(table),
(name) =>
let
Values = List.RemoveNulls(Table.Column(table, name))
in
not List.IsEmpty(Values) and List.AllTrue(List.Transform(Values, test))
),
// Codes stay text as the source wrote them: columns named like one
// (coordinate, vector_id, dguid, NAICS, postal code; StatCan's "2.1" and
// "2.10" are different coordinates) and columns with a leading zero (01).
CodeName = (name as text) as logical =>
List.Contains(
{"coordinate", "vector", "vector_id", "vectorid", "product_id", "productid", "pid",
"dguid", "postal_code", "postalcode", "fsa", "noc", "sgc"},
name
)
or Text.EndsWith(name, "_dguid")
or Text.EndsWith(name, "_postal_code")
or Text.Contains(name, "naics")
or Text.StartsWith(name, "noc_")
or Text.EndsWith(name, "_noc")
or Text.StartsWith(name, "sgc_")
or Text.EndsWith(name, "_sgc"),
HasLeadingZero = (value as any) as logical =>
value is text
and (
let
Digits = if Text.StartsWith(value, "-") then Text.Middle(value, 1) else value
in
Text.Length(Digits) > 1
and Text.Start(Digits, 1) = "0"
and List.Contains({"0".."9"}, Text.Middle(Digits, 1, 1))
),
CodeColumns = List.Select(
Table.ColumnNames(Trimmed),
(name) => CodeName(name) or List.AnyTrue(List.Transform(Table.Column(Trimmed, name), HasLeadingZero))
),
Codes = Table.TransformColumns(
Trimmed,
List.Transform(
CodeColumns,
(name) =>
{
name,
(value) =>
if value = null then null
else if value is number then Number.ToText(value, "G", "en-US")
else Text.From(value),
type text
}
)
),
NumberColumns = List.Difference(ColumnsWhere(Codes, IsNumberText), CodeColumns),
Numbers = Table.TransformColumnTypes(
Codes, List.Transform(NumberColumns, (name) => {name, type number}), "en-US"
),
DateColumns = ColumnsWhere(Numbers, IsIsoDate),
Cleaned = Table.TransformColumns(
Numbers,
List.Transform(
DateColumns,
(name) => {name, (value) => if value = null then null else Date.FromText(value, "en-US"), type date}
)
),
Data = Cleaned
in
DataUn MCP pour que l'Agent les trouve et les réunisse tous.
Voir les données
| Sujet | Outils |
|---|---|
| Provinces et villes : catalogues de données ouvertes, provincial, municipal | 117 |
| Statistique : statistiques et recensement | 92 |
| Terre et énergie : agriculture et alimentation, environnement et risques naturels, énergie et mines, géographie, transport et sécurité | 55 |
| Population : santé, logement, immigration | 40 |
| Argent et affaires : monnaie, prix et finances publiques, entreprises, PI et concurrence | 30 |
| Parlement : parlement, droit et élections | 21 |
L'accès aux données et leur découverte ne sont plus le goulot d'étranglement.
Ne payez plus pour des chiffres que le Canada publie déjà gratuitement. Posez la question, obtenez le chiffre avec sa source et un script qui le récupère de nouveau, et consacrez votre temps à ce que vous voulez vraiment faire : le modèle, l'argument, la décision.
MCP signifie Model Context Protocol, une norme ouverte qui relie les agents IA aux outils et aux données. Voyez-le comme un port USB-C pour l'IA : tout agent qui le parle peut se brancher sur tout serveur qui le parle aussi. MapleStats est cette prise pour les données canadiennes : 175 sources d'organismes fédéraux, de provinces, de villes et de catalogues de données ouvertes, une seule connexion.
Ce qu'une seule connexion atteint
- 1 610 458brevets
- 962 449fiches du recensement dans une totalisation
- 15 937séries de la Banque du Canada
- 8 271tableaux de Statistique Canada
- 158fichiers de microdonnées
- 98cartes de crédit
- 67tableaux d'IRCC à jour
- 54catégories de données de la SCHL