Exploiter le recensement 🇹🇩

Exploiter les donnĂ©es du recensement 🇹🇩

Bienvenue dans ce nouveau projet ! Nous allons explorer l’immense quantitĂ© d’informations provenant du recensement canadien fourni par Statistique Canada ! Pour traiter les donnĂ©es, nous utiliserons la librairie Simple Data Analysis (SDA), que j’ai créée (donnez-lui une Ă©toile ⭐).

Pour chaque rĂ©gion mĂ©tropolitaine, nous crĂ©erons une carte montrant les zones oĂč les revenus des mĂ©nages sont infĂ©rieurs ou supĂ©rieurs Ă  la mĂ©diane. La carte de MontrĂ©al ci-dessous est un exemple du rĂ©sultat final que nous allons obtenir ensemble.

Une carte de Montréal.

Dans ce projet, je vais vous montrer des techniques avancĂ©es pour traiter de grands ensembles de donnĂ©es avec TypeScript. Les donnĂ©es du recensement de 2021 pĂšsent environ 30 GB, mais vous aurez besoin d’environ 70 GB d’espace libre sur votre disque dur pour ce projet. Il est temps de faire un peu de mĂ©nage ! đŸ§č

Si vous ĂȘtes bloquĂ© Ă  un moment donnĂ©, il peut ĂȘtre utile de revoir les leçons prĂ©cĂ©dentes expliquant les bases de SDA :

Nous utiliserons Deno et VS Code. Consultez la leçon Installation si nécessaire.

C’est parti !

Vous voulez ĂȘtre prĂ©venu quand de nouvelles leçons sont publiĂ©es ? Abonnez-vous Ă  l'infolettre ✉ et donnez une ⭐ au cours sur GitHub pour me garder motivé ! Cliquez ici pour me contacter.

Quelle est la question ?

Pour ne pas perdre le fil, définissons la question à laquelle nous essayons de répondre :

  • Pour chaque rĂ©gion mĂ©tropolitaine canadienne, quelles sont les aires de diffusion dont le revenu des mĂ©nages est supĂ©rieur ou infĂ©rieur Ă  la mĂ©diane ?

Les régions métropolitaines sont définies ainsi dans le recensement :

Une rĂ©gion mĂ©tropolitaine de recensement (RMR) est formĂ©e d’une ou de plusieurs municipalitĂ©s adjacentes centrĂ©es sur une rĂ©gion urbaine (appelĂ©e le noyau). Une RMR doit avoir une population totale d’au moins 100 000 habitants, dont au moins 50 000 vivent dans le noyau.

Voici la définition des aires de diffusion :

Une aire de diffusion (AD) est une petite unitĂ© gĂ©ographique relativement stable avec une population moyenne de 400 Ă  700 personnes. C’est la plus petite unitĂ© gĂ©ographique pour laquelle toutes les donnĂ©es de recensement sont diffusĂ©es. Les AD couvrent tout le territoire canadien.

C’est parti pour le code !

Configuration

Pour tout configurer, utilisons setup-sda comme dans les leçons précédentes.

Créez un nouveau dossier, ouvrez-le avec VS Code, et exécutez : deno -A jsr:@nshiab/setup-sda

Ensuite, exécutez deno task sda pour surveiller main.ts et ses dépendances.

Capture d'écran de VS Code aprÚs avoir exécuté setup-sda.

💡

Pour que SDA fonctionne correctement, il est recommandĂ© d’avoir au moins la version 2.1.9 de Deno. Pour vĂ©rifier votre version, exĂ©cutez deno --version dans votre terminal. Pour la mettre Ă  jour, exĂ©cutez simplement deno upgrade.

Téléchargement des données

Pour télécharger les données du recensement avec le plus de granularité possible, cliquez sur cette page de Statistique Canada.

Cliquez sur le premier volet Fichier de téléchargement complet puis sur le bouton CSV pour Canada, provinces, territoires, divisions de recensement (DR), subdivisions de recensement (SDR) et aires de diffusion (AD).

Si vous ne le trouvez pas, voici le lien direct. Cela téléchargera un fichier zip de 2,25 GB.

Capture d'écran du site de Statistique Canada pour télécharger les données du recensement.

Parce que nous voulons travailler sur les rĂ©gions mĂ©tropolitaines, il serait utile d’avoir les noms des rĂ©gions pour chaque aire de diffusion.

Le fichier contenant ces informations se trouve ici. TĂ©lĂ©chargez-le aussi. C’est un autre fichier zip de 9,8 MB.

Capture d'écran du site de Statistique Canada pour télécharger les noms des régions.

Et enfin, comme nous voulons créer une carte, nous avons besoin des frontiÚres géospatiales des aires de diffusion. Vous les trouverez ici.

DĂ©placez tout cela dans le dossier data de votre projet et dĂ©compressez tout sauf les frontiĂšres gĂ©ospatiales dans le fichier lda_000b21a_e.zip ! C’est dĂ©compressĂ© dans mes captures d’écran, mais c’était une erreur đŸ„Č.

Surprise ! Vous avez maintenant plus de 27 GB de donnĂ©es Ă  traiter. 😅

Capture d'écran de VS Code avec les données décompressées.

Les données du recensement

Premier essai

En décompressant les données, nous avons obtenu un dossier contenant plusieurs fichiers. Les données se trouvent dans les fichiers dont le nom contient _data_.

Essayons d’ouvrir le premier pour les provinces de l’Atlantique.

main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const sdb = new SimpleDB();
 
const census = sdb.newTable("census");
await census.loadData(
  "sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_Atlantic.csv",
);
await census.logTable()
 
await sdb.done();

Hmmm
 Nous avons un problĂšme. Cette erreur signifie que les donnĂ©es n’utilisent pas l’encodage UTF-8, qui est pourtant la norme de nos jours et nĂ©cessaire pour SDA.

Capture d'écran de VS Code montrant une erreur de codage Unicode invalide.

J’ai contactĂ© Statistique Canada Ă  ce sujet, et ils m’ont dit qu’ils utilisaient l’encodage Windows-1252. Cela signifie que notre premiĂšre Ă©tape consiste Ă  rĂ©encoder les donnĂ©es


Et oui, les projets de donnĂ©es dans la vraie vie sont toujours aussi amusants ! 😬

Réencodage des données

Comme le rĂ©encodage de donnĂ©es est une tĂąche courante, j’ai créé la fonction reencode et je l’ai publiĂ©e dans la librairie journalism. Lorsque vous configurez votre projet avec setup-sda, journalism est automatiquement installĂ©. Cette Ă©tape sera donc trĂšs facile !

CrĂ©ons un nouveau fichier toUTF8.ts dans le dossier helpers avec le code ci-dessous. Comme nous n’avons besoin de rĂ©encoder les donnĂ©es qu’une seule fois, nous n’exportons pas de fonction. C’est simplement un script que nous allons exĂ©cuter une seule fois.

En regardant les noms des fichiers, vous remarquerez qu’ils ont tous la mĂȘme structure, sauf pour la rĂ©gion. En crĂ©ant une liste avec les rĂ©gions, nous pouvons facilement parcourir tous les fichiers.

La fonction reencode nécessite quatre arguments :

  • le fichier d’entrĂ©e
  • le fichier de sortie, qui ici a le mĂȘme nom que le fichier original mais avec _utf8 Ă  la fin
  • l’encodage d’origine
  • le nouvel encodage
toUTF8.ts
import { reencode } from "@nshiab/journalism";
 
const regions = [
  "Atlantic",
  "BritishColumbia",
  "Ontario",
  "Prairies",
  "Quebec",
  "Territories",
];
 
for (const r of regions) {
  console.log(`Processing ${r}`);
 
  const newFile =
    `sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_${r}_utf8.csv`;
  const originalFile =
    `sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_${r}.csv`;
 
  await reencode(originalFile, newFile, "windows-1252", "utf-8");
 
  console.log(`Done with ${r}`);
}

Pour exĂ©cuter ce script, nous pouvons crĂ©er une nouvelle tĂąche toUTF8 dans notre deno.json. Ne vous inquiĂ©tez pas si vous n’avez pas la mĂȘme version que moi dans les imports. Tout va bien aller !

deno.json
{
  "tasks": {
    "sda": "deno run --node-modules-dir=auto -A --watch --check sda/main.ts",
    "clean": "rm -rf .sda-cache && rm -rf .tmp",
    "toUTF8": "deno run -A sda/helpers/toUTF8.ts"
  },
  "nodeModulesDir": "auto",
  "imports": {
    "@nshiab/journalism": "jsr:@nshiab/journalism@^1.22.0",
    "@nshiab/simple-data-analysis": "jsr:@nshiab/simple-data-analysis@^4.2.0",
    "@observablehq/plot": "npm:@observablehq/plot@^0.6.17"
  }
}

ArrĂȘtez de surveiller main.ts (CTRL + C dans votre terminal) et exĂ©cutons notre nouveau script avec notre nouvelle tĂąche : deno task toUTF8

Cela prendra quelques minutes pour réencoder tous les fichiers. Voici ce que vous verrez une fois que ce sera terminé.

Capture d'écran de VS Code montrant les données réencodées.

De nouveaux fichiers sont apparus avec _utf8 dans leurs noms ! Et maintenant, votre dossier data pĂšse
 53 GB. đŸ€­

Si vous manquez d’espace de stockage, supprimez les fichiers de donnĂ©es d’origine. Nous travaillerons dĂ©sormais avec ceux qui se terminent par _utf8.csv. Gardez Ă©galement les autres fichiers, en particulier 98-401-X2021006_English_meta.txt !

Réessayons

Essayons maintenant de charger et d’afficher le fichier CSV rĂ©encodĂ© pour les provinces de l’Atlantique. Mettez Ă  jour main.ts et exĂ©cutez deno task sda dans votre terminal.

main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const sdb = new SimpleDB();
 
const census = sdb.newTable("census");
await census.loadData(
  "sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_Atlantic_utf8.csv",
);
await census.logTable();
 
await sdb.done();

Capture d'écran de VS Code montrant les données des provinces de l'Atlantique.

💡

Si la disposition du tableau s’affiche de maniĂšre Ă©trange dans votre terminal, c’est parce que la largeur du tableau est supĂ©rieure Ă  celle de votre terminal. Faites un clic droit sur le terminal et cherchez Toggle size with content width. Il y a aussi un raccourci pratique que j’utilise tout le temps pour cela : OPTION + Z sur Mac et ALT + Z sur PC.

Ça fonctionne ! đŸ„ł

Essayons un autre fichier : celui des Prairies, qui couvre les provinces de l’Alberta, de la Saskatchewan et du Manitoba.

main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const sdb = new SimpleDB();
 
const census = sdb.newTable("census");
await census.loadData(
  "sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_Prairies_utf8.csv",
);
await census.logTable();
 
await sdb.done();

Capture d'écran de VS Code montrant une erreur due à un CSV mal formaté.

Oh non ! Encore une erreur
 Il semble que ce fichier CSV soit mal formaté 

Nous pouvons ajuster les options pour rendre le chargement du CSV moins stricte et voir si cela fonctionne.

main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const sdb = new SimpleDB();
 
const census = sdb.newTable("census");
await census.loadData(
  "sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_Prairies_utf8.csv",
  { strict: false },
);
await census.logTable();
 
await sdb.done();

Capture d'écran de VS Code montrant les données des Prairies.

Magnifique ! Tout fonctionne maintenant !

Charger toutes les données

Jusqu’à prĂ©sent, nous avons chargĂ© les donnĂ©es un fichier Ă  la fois. Mais vous pouvez Ă©galement charger tous les fichiers CSV dans une seule table facilement.

Créons un nouveau fichier crunchData.ts pour cela. Cette fonction async aura un paramÚtre sdb et retournera une table census.

Lorsque vous avez des fichiers dont les noms suivent le mĂȘme modĂšle, vous pouvez utiliser des jokers *. Dans notre cas, nous voulons charger tous les fichiers CSV se terminant par _utf8.csv, donc nous les chargeons tous en utilisant *_utf8.csv, comme montrĂ© Ă  la ligne 6 ci-dessous.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv",
  {
    strict: false,
  });
 
  return census;
}

Mettons à jour main.ts pour utiliser cette nouvelle fonction. Nous réglons également cacheVerbose sur true lors de la création de notre SimpleDB. Cela enregistrera la durée totale et sera utile par la suite lorsque nous utiliserons le cache.

main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import crunchData from "./helpers/crunchData.ts";
 
const sdb = new SimpleDB({ cacheVerbose: true });
 
const census = await crunchData(sdb);
await census.logTable();
 
await sdb.done();

En fonction de la mémoire RAM disponible sur votre ordinateur, vous pourriez voir un dossier .tmp apparaßtre. Si les données sont plus volumineuses que votre RAM, ce dossier sera utilisé pour traiter toutes les données en y stockant des morceaux prétraités.

Ce dossier .tmp peut devenir assez volumineux. Sur ma machine, aprÚs la premiÚre exécution, il pÚse environ 16 GB.

💡

Si vous souhaitez nettoyer votre cache, exĂ©cutez deno task clean. Cela supprimera .tmp et .sda-cache (nous en reparlerons plus tard). Vous pouvez Ă©galement les supprimer manuellement, mais n’oubliez pas de vider votre corbeille.

Nous pouvons enfin jeter un Ɠil aux donnĂ©es. Avec 166 millions de lignes et 23 colonnes, nous avons environ 3,8 milliards de points de donnĂ©es. 🙃

Et charger tout cela a pris moins d’une minute sur mon ordinateur. Pas mal !

Capture d'écran de VS Code montrant toutes les données chargées.

Limite et cache

Pour commencer Ă  travailler sur les donnĂ©es, nous n’avons pas besoin de tout charger. Nous pouvons utiliser l’option limit pour ne charger que le premier million de lignes.

Maintenant, le chargement des données prend environ une seconde.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
    strict: false,
    limit: 1_000_000,
  });
 
  return census;
}

Capture d'écran de VS Code montrant le premier million de lignes chargées.

Nous pouvons également utiliser la méthode cache. Tout ce qui est enveloppé par cette méthode sera exécuté une seule fois et le résultat sera stocké dans le dossier .sda-cache. Si le code ne change pas dans la méthode cache, les données seront chargées depuis le cache au lieu de relancer les calculs.

Lors de la premiÚre exécution, cela prend un peu plus de temps car les données sont écrites dans le cache.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
  });
 
  return census;
}

Capture d'écran de VS Code montrant l'écriture dans le cache.

Mais lors des exĂ©cutions suivantes, les donnĂ©es sont chargĂ©es depuis le cache, ce qui est beaucoup plus rapide. Sur mon MacBook Pro, c’est 10 fois plus rapide ! đŸ˜±

Capture d'écran de VS Code montrant le cache utilisé.

Filtrage

L’une des premiĂšres choses Ă  faire lorsque l’on travaille avec de grands ensembles de donnĂ©es est de les filtrer pour ne conserver que les informations qui nous intĂ©ressent.

Notre question est :

  • Pour chaque rĂ©gion mĂ©tropolitaine canadienne, quelles sont les aires de diffusion dont le revenu des mĂ©nages est supĂ©rieur ou infĂ©rieur Ă  la mĂ©diane ?

Pour trouver le revenu total des ménages, vous pouvez consulter le fichier 98-401-X2021006_English_meta.txt. Il contient la liste de toutes les variables du recensement.

Le CHARACTERISTIC_ID pour le Median total income of household in 2020 ($) est 243.

Capture d'écran de VS Code montrant le code pour la variable du revenu total.

De plus, les fichiers de données du recensement que nous avons téléchargés contiennent différents niveaux géographiques, mais nous avons seulement besoin des aires de diffusion.

Enfin, nous n’avons besoin que de trois colonnes :

  • DGUID, qui contient l’ID gĂ©ospatial unique des aires de diffusion. Nous l’utiliserons pour trouver les bonnes frontiĂšres pour crĂ©er une carte.
  • GEO_NAME, qui contient l’ID de nommage unique des aires de diffusion. Nous l’utiliserons pour rĂ©cupĂ©rer les noms des rĂ©gions mĂ©tropolitaines.
  • C1_COUNT_TOTAL, qui contient les valeurs de la variable. Dans notre cas, il s’agit du revenu total mĂ©dian dans chaque aire de diffusion. Nous pouvons renommer cette colonne pour avoir quelque chose de plus lisible.

Mettons Ă  jour crunchData pour ne conserver que ce dont nous avons besoin.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
  });
 
  return census;
}

Capture d'écran de VS Code montrant les données filtrées.

C’est beaucoup mieux ! Nous pouvons maintenant nous concentrer sur l’ajout des noms des rĂ©gions mĂ©tropolitaines.

Les régions métropolitaines

Premier essai

Essayons de charger les noms qui se trouvent dans le fichier 2021_92-151_X.csv. Nous pouvons mettre à jour crunchData.ts. Nous continuons de travailler dans la méthode cache.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X.csv");
    await names.logTable();
  });
 
  return census;
}

Capture d'écran de VS Code montrant un problÚme d'encodage pour le fichier contenant les noms.

Nous connaissons cette erreur ! C’est encore un problùme d’encodage !

Réencodage à nouveau

Mettons Ă  jour toUTF8.ts pour convertir Ă©galement ce fichier CSV. Nous commentons le code prĂ©cĂ©dent car nous n’avons pas besoin de reconvertir les fichiers du recensement.

toUTF8.ts
import { reencode } from "@nshiab/journalism";
 
// const regions = [
//   "Atlantic",
//   "BritishColumbia",
//   "Ontario",
//   "Prairies",
//   "Quebec",
//   "Territories",
// ];
 
// for (const r of regions) {
//   console.log(`Processing ${r}`);
 
//   const newFile =
//     `sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_${r}_utf8.csv`;
//   const originalFile =
//     `sda/data/98-401-X2021006_eng_CSV/98-401-X2021006_English_CSV_data_${r}.csv`;
 
//   await reencode(originalFile, newFile, "windows-1252", "utf-8");
 
//   console.log(`Done with ${r}`);
// }
 
console.log(`Processing names data`);
await reencode(
  "sda/data/2021_92-151_X.csv",
  "sda/data/2021_92-151_X_utf8.csv",
  "windows-1252",
  "utf-8",
);
console.log("Done with names data");

ArrĂȘtez de surveiller main.ts dans votre terminal (CTRL + C) et exĂ©cutez deno task toUTF8.

Maintenant, chargeons notre nouveau fichier sda/data/2021_92-151_X_utf8.csv dans crunchData.ts.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv");
    await names.logTable();
  });
 
  return census;
}

Capture d'écran de VS Code montrant un problÚme d'encodage pour le fichier contenant les noms.

Encore une erreur
 Nous devons Ă  nouveau dĂ©finir l’option strict sur false.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.logTable();
  });
 
  return census;
}

Et maintenant, ça fonctionne ! Mais ce fichier contient un impressionnant total de 63 colonnes. 😳

Filtrage

Si vous lisez la documentation (et que vous connaissez bien votre recensement đŸ„ž), vous rĂ©aliserez que vous n’avez besoin que de deux colonnes, aprĂšs avoir filtrĂ© CMATYPE_RMRGENRE pour le type B afin de ne conserver que les rĂ©gions mĂ©tropolitaines.

Comme le fichier contient des donnĂ©es pour diffĂ©rents niveaux gĂ©ographiques, nous supprimons les doublons créés en sĂ©lectionnant seulement deux colonnes. Et puisque le but est d’ajouter les noms des rĂ©gions mĂ©tropolitaines Ă  notre table census, nous renommons la colonne DADGUID_ADIDUGD en DGUID pour joindre facilement les deux tables. Nous renommons Ă©galement CMANAME_RMRNOM en CMA par souci de commoditĂ©.

Voici une version mise Ă  jour de crunchData.ts.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.keep({
      CMATYPE_RMRGENRE: "B",
    });
    await names.selectColumns(["DADGUID_ADIDUGD", "CMANAME_RMRNOM"]);
    await names.removeDuplicates();
    await names.renameColumns({
      DADGUID_ADIDUGD: "DGUID",
      CMANAME_RMRNOM: "CMA",
    });
    await census.join(names);
  });
 
  return census;
}

Capture d'écran de VS Code montrant la table du recensement avec les noms des CMA.

Victoire ! Nous avons maintenant le nom de la rĂ©gion mĂ©tropolitaine pour chaque aire de diffusion ! đŸ„ł

Les frontiĂšres des aires de diffusion

Simplification

Comme nous voulons crĂ©er une carte, nous avons besoin des frontiĂšres des aires de diffusion. Nous les avons tĂ©lĂ©chargĂ©es prĂ©cĂ©demment sous forme de fichier compressĂ© lda_000b21a_e.zip (et je vous avais dit de ne pas le dĂ©compresser 😬).

Statistique Canada fournit des donnĂ©es gĂ©ospatiales trĂšs dĂ©taillĂ©es. Mais comme nous voulons seulement dessiner des cartes, nous n’avons pas besoin d’un niveau de dĂ©tail aussi Ă©levĂ©. Une version simplifiĂ©e suffira et rendra notre code plus rapide.

Un de mes outils préférés pour simplifier les données géospatiales est mapshaper.org. Allez leur donner une ⭐ sur GitHub si vous avez un compte !

Allez sur le site et faites glisser lda_000b21a_e.zip sur la page. Notez que vous ne pouvez pas faire glisser le fichier depuis VS Code. Faites-le depuis votre dossier à l’aide du Finder ou de l’Explorateur de fichiers de votre ordinateur.

AprĂšs quelques secondes, vous verrez toutes les aires de diffusion.

Capture d'écran de mapshaper montrant les aires de diffusion.

Vous pouvez maintenant cliquer sur Simplify en haut à droite et sélectionner les options suivantes :

  • prevent shape removal
  • Visvalingam / weighted area

Cliquez sur Apply !

Capture d'écran de mapshaper montrant les aires de diffusion.

Pour l’étape suivante, je zoome gĂ©nĂ©ralement sur une zone Ă  haute densitĂ©, comme MontrĂ©al. Ensuite, Ă  l’aide du curseur en haut, je vise un seuil de simplification qui ne modifie pas les formes globales.

Ici, 10% fonctionne plutÎt bien. Notez que vous pouvez également saisir directement le pourcentage souhaité.

Capture d'écran de mapshaper montrant les options de simplification.

L’étape suivante consiste Ă  exporter les donnĂ©es simplifiĂ©es !

Cliquez sur le bouton Export en haut Ă  droite, conservez le format de fichier original Shapefile, puis cliquez sur Export.

Capture d'écran de mapshaper montrant comment exporter des calques simplifiés.

Vous pouvez maintenant renommer ce fichier en lda_000b21a_e_simplified.shp.zip (remarquez que j’ai ajoutĂ© .shp.zip Ă  l’extension pour aider SDA Ă  comprendre qu’il s’agit d’un shapefile) et le dĂ©placer dans votre dossier data.

Au lieu de 197 MB, nos données géospatiales ne pÚsent plus que 27 MB, ce qui accélérera nos calculs et le dessin des cartes !

Chargement des géométries

Pour charger les gĂ©omĂ©tries, nous pouvons utiliser la mĂ©thode loadGeoData. N’oubliez pas de modifier l’extension du fichier Shapefile simplifiĂ© en .shp.zip. Le shp est important pour SDA. Sans cela, SDA ne reconnaĂźt pas le fichier en tant que Shapefile et ne peut pas le charger correctement.

Lorsque vous chargez de nouvelles données géospatiales, il est toujours important de vérifier la projection. Ci-dessous, nous utilisons la méthode logProjections pour cela.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.keep({
      CMATYPE_RMRGENRE: "B",
    });
    await names.selectColumns(["DADGUID_ADIDUGD", "CMANAME_RMRNOM"]);
    await names.removeDuplicates();
    await names.renameColumns({
      DADGUID_ADIDUGD: "DGUID",
      CMANAME_RMRNOM: "CMA",
    });
    await census.join(names);
 
    const disseminationAreas = sdb.newTable("disseminationAreas");
    await disseminationAreas.loadGeoData(
      "sda/data/lda_000b21a_e_simplified.shp.zip",
    );
    await disseminationAreas.logProjections()
    await disseminationAreas.logTable();
  });
 
  return census;
}

Capture d'écran de VS Code chargant des données géospatiales.

Nous n’avons aucun problĂšme Ă  charger les donnĂ©es gĂ©ospatiales. Nous pouvons voir les 57 932 aires de diffusion sous forme de lignes avec leurs propriĂ©tĂ©s et leurs gĂ©omĂ©tries.

Mais la projection proj=lcc avec ses units=m pose problÚme. Statistique Canada utilise la projection conforme conique de Lambert avec des coordonnées en mÚtres. Pour que de nombreuses méthodes de SDA fonctionnent correctement, nous avons besoin des coordonnées avec la projection WGS84 utilisant la latitude et la longitude.

Mais ne vous inquiĂ©tez pas, SDA gĂšre cela pour vous. Il vous suffit de passer l’option { toWGS84: true } pour convertir vos donnĂ©es gĂ©ospatiales au bon format.

Pendant qu’on y est, sĂ©lectionnons uniquement les colonnes qui nous intĂ©ressent :

  • DGUID, qui est l’ID unique pour les aires de diffusion. Nous l’utiliserons pour joindre les gĂ©omĂ©tries aux donnĂ©es du recensement.
  • geom, qui contient les gĂ©omĂ©tries.

Et joignons la table disseminationAreas Ă  la table census ! Comme nous avons un DGUID dans chaque table, SDA l’utilisera pour faire correspondre les donnĂ©es du recensement des aires de diffusion avec les bonnes frontiĂšres.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
      limit: 1_000_000,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.keep({
      CMATYPE_RMRGENRE: "B",
    });
    await names.selectColumns(["DADGUID_ADIDUGD", "CMANAME_RMRNOM"]);
    await names.removeDuplicates();
    await names.renameColumns({
      DADGUID_ADIDUGD: "DGUID",
      CMANAME_RMRNOM: "CMA",
    });
    await census.join(names);
 
    const disseminationAreas = sdb.newTable("disseminationAreas");
    await disseminationAreas.loadGeoData(
      "sda/data/lda_000b21a_e_simplified.shp.zip",
      { toWGS84: true },
    );
    await disseminationAreas.selectColumns(["DGUID", "geom"]);
    await census.join(disseminationAreas);
  });
 
  return census;
}

Capture d'écran de VS Code montrant les données complÚtes des aires de diffusion.

Nos données sont enfin complÚtes ! Nous avons nos aires de diffusion avec leur revenu total médian des ménages, le nom de leur région métropolitaine et leurs frontiÚres !

Nous pouvons supprimer l’option limit Ă  la ligne 9 et sĂ©lectionner uniquement les trois colonnes que nous utiliserons par la suite. Nous pouvons Ă©galement supprimer les lignes avec des valeurs manquantes.

Traitons toutes les données maintenant !

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.keep({
      CMATYPE_RMRGENRE: "B",
    });
    await names.selectColumns(["DADGUID_ADIDUGD", "CMANAME_RMRNOM"]);
    await names.removeDuplicates();
    await names.renameColumns({
      DADGUID_ADIDUGD: "DGUID",
      CMANAME_RMRNOM: "CMA",
    });
    await census.join(names);
 
    const disseminationAreas = sdb.newTable("disseminationAreas");
    await disseminationAreas.loadGeoData(
      "sda/data/lda_000b21a_e_simplified.shp.zip",
      { toWGS84: true },
    );
    await disseminationAreas.selectColumns(["DGUID", "geom"]);
    await census.join(disseminationAreas);
    await census.selectColumns(["medianIncome", "CMA", "geom"]);
    await census.removeMissing();
  });
 
  return census;
}

Capture d'écran de VS Code montrant les données complÚtes des aires de diffusion.

Nous avons maintenant nos données pour environ 37 000 aires de diffusion situées dans des régions métropolitaines.

Et comme tout ce code est dans la mĂ©thode cache, il ne s’exĂ©cutera qu’une seule fois, ce qui nous permettra de travailler rapidement sur les prochaines Ă©tapes ! Comme indiquĂ© ci-dessus, lors de la premiĂšre exĂ©cution, le code a mis 1 min 31 s Ă  s’exĂ©cuter sur mon ordinateur. Mais lors de la seconde, il n’a fallu que 97 ms. 😏

Capture d'écran de VS Code montrant les données complÚtes chargées depuis le cache.

La structuration, le formatage, le filtrage et le nettoyage des donnĂ©es sont souvent les Ă©tapes les plus longues dans l’analyse et la visualisation de donnĂ©es. đŸ«  Mais il est Ă©galement extrĂȘmement important de bien les faire pour Ă©viter les erreurs dans votre analyse et vos visualisations.

Prenez toujours le temps de lire la documentation des donnĂ©es. Cela peut sembler une perte de temps au dĂ©but, mais cela vous fera en rĂ©alitĂ© gagner beaucoup de temps par la suite. J’ai dĂ©jĂ  vĂ©cu ça. Faites-moi confiance. đŸ«Ł

Répondre à la question

Variation par rapport à la médiane

La question à laquelle nous voulons répondre est :

  • Pour chaque rĂ©gion mĂ©tropolitaine canadienne, quelles sont les aires de diffusion dont le revenu des mĂ©nages est supĂ©rieur ou infĂ©rieur Ă  la mĂ©diane ?

Nous devons donc trouver le revenu total mĂ©dian des mĂ©nages pour chaque rĂ©gion mĂ©tropolitaine. C’est facile Ă  faire avec la mĂ©thode summarize.

Notez que nous pouvons maintenant travailler en dehors de la méthode cache. Les données étant nettoyées et filtrées, cela devient beaucoup plus léger à traiter.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.keep({
      CMATYPE_RMRGENRE: "B",
    });
    await names.selectColumns(["DADGUID_ADIDUGD", "CMANAME_RMRNOM"]);
    await names.removeDuplicates();
    await names.renameColumns({
      DADGUID_ADIDUGD: "DGUID",
      CMANAME_RMRNOM: "CMA",
    });
    await census.join(names);
 
    const disseminationAreas = sdb.newTable("disseminationAreas");
    await disseminationAreas.loadGeoData(
      "sda/data/lda_000b21a_e_simplified.shp.zip",
      { toWGS84: true },
    );
    await disseminationAreas.selectColumns(["DGUID", "geom"]);
    await census.join(disseminationAreas);
    await census.selectColumns(["medianIncome", "CMA", "geom"]);
    await census.removeMissing();
  });
 
  const medians = await census.summarize({
    values: "medianIncome",
    categories: "CMA",
    summaries: "median",
    outputTable: "medians",
  });
  await medians.logTable();
 
  return census;
}

Capture d'écran de VS Code montrant le revenu total médian des ménages.

Maintenant que nous avons la médiane pour chaque CMA dans la table medians, nous pouvons joindre la table medians à la table census. Comme les deux tables ont la colonne CMA, SDA pourra facilement faire correspondre les lignes. Par commodité, nous pouvons supprimer la colonne value de medians avant de faire la jointure.

Nous pouvons maintenant ajouter une nouvelle colonne varPerc avec la variation en pourcentage par rapport Ă  la mĂ©diane pour chaque aire de diffusion. Nous pouvons Ă©galement arrondir les valeurs. C’est ce que nous utiliserons pour colorer nos cartes.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.keep({
      CMATYPE_RMRGENRE: "B",
    });
    await names.selectColumns(["DADGUID_ADIDUGD", "CMANAME_RMRNOM"]);
    await names.removeDuplicates();
    await names.renameColumns({
      DADGUID_ADIDUGD: "DGUID",
      CMANAME_RMRNOM: "CMA",
    });
    await census.join(names);
 
    const disseminationAreas = sdb.newTable("disseminationAreas");
    await disseminationAreas.loadGeoData(
      "sda/data/lda_000b21a_e_simplified.shp.zip",
      { toWGS84: true },
    );
    await disseminationAreas.selectColumns(["DGUID", "geom"]);
    await census.join(disseminationAreas);
    await census.selectColumns(["medianIncome", "CMA", "geom"]);
    await census.removeMissing();
  });
 
  const medians = await census.summarize({
    values: "medianIncome",
    categories: "CMA",
    summaries: "median",
    outputTable: "medians",
  });
  await medians.removeColumns("value");
 
  await census.join(medians);
  await census.addColumn(
    "varPerc",
    "number",
    `(medianIncome - median) / median * 100`,
  );
  await census.round("varPerc");
 
  return census;
}

Capture d'écran de VS Code montrant la variation par rapport au revenu médian dans chaque région métropolitaine.

Nous avons maintenant la réponse à notre question : la variation par rapport à la médiane dans chaque aire de diffusion pour toutes les régions métropolitaines.

Il est temps de visualiser tout ça !

Création des cartes

Pour garder notre code organisĂ© et pour rester sain d’esprit, crĂ©ons un nouveau fichier visualizeData.ts dans le dossier helpers.

La nouvelle fonction aura besoin de la table census et, pour l’instant, elle se contentera de l’afficher dans la console.

visualizeData.ts
import { SimpleTable } from "@nshiab/simple-data-analysis";
 
export default async function visualizeData(census: SimpleTable) {
  await census.logTable();
}

Et mettons Ă  jour main.ts pour appeler cette nouvelle fonction.

main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import crunchData from "./helpers/crunchData.ts";
import visualizeData from "./helpers/visualizeData.ts";
 
const sdb = new SimpleDB({ cacheVerbose: true });
 
const census = await crunchData(sdb);
await visualizeData(census);
 
await sdb.done();

Nous voulons dessiner une carte pour chaque région métropolitaine, alors commençons par récupérer toutes les régions métropolitaines uniques dans nos données.

Ensuite, nous pouvons les parcourir dans une boucle, cloner la table census et ne garder que les lignes correspondant Ă  la bonne CMA.

Comme vous pouvez le voir dans la capture d’écran ci-dessous, lorsque vous ne spĂ©cifiez pas de nom pour les tables, SDA les nomme automatiquement table0, table1, etc. Ici, nous voyons que la derniĂšre table est table42, ce qui signifie que nous parcourons 43 rĂ©gions mĂ©tropolitaines.

Pour connaĂźtre le nombre de rĂ©gions mĂ©tropolitaines, vous pouvez Ă©galement vĂ©rifier la propriĂ©tĂ© .length de allCMAs, puisque getUniques renvoie toutes les valeurs uniques d’une colonne sous forme de liste.

visualizeData.ts
import { SimpleTable } from "@nshiab/simple-data-analysis";
 
export default async function visualizeData(census: SimpleTable) {
  const allCMAs = await census.getUniques("CMA");
 
  for (const CMA of allCMAs) {
    const tableCMA = await census.cloneTable();
    await tableCMA.keep({ CMA });
    await tableCMA.logTable();
  }
}

Capture d'écran de VS Code aprÚs avoir exécuté setup-sda.

💡

Comme la table census possÚde une colonne CMA et que la boucle utilise également la variable CMA, nous pouvons écrire { CMA } comme raccourci pour { CMA: CMA }.

Nous pouvons maintenant crĂ©er une fonction pour dessiner nos cartes. Lorsque nous avons tout configurĂ© avec setup-sda, nous avons automatiquement installĂ© Plot. Cette puissante librairie de visualisation de donnĂ©es fonctionne trĂšs bien avec SDA et c’est ce que nous allons utiliser pour dessiner nos cartes.

Comme c’est la mĂȘme fonction pour dessiner toutes les cartes, nous pouvons la crĂ©er en dehors de la boucle. Cette fonction attendra les donnĂ©es au format GeoJSON, avec une liste de features contenant des propriĂ©tĂ©s. Pour commencer, dessinons les polygones des aires de diffusion avec le marqueur geo et colorons le fill et le stroke avec les valeurs calculĂ©es dans la colonne varPerc.

Dans la boucle, nous pouvons passer cette fonction Ă  la mĂ©thode writeMap, qui lui transmettra les donnĂ©es de la table au format GeoJSON. Nous devons Ă©galement spĂ©cifier l’emplacement oĂč nous voulons enregistrer la carte, et nous utilisons le nom de la rĂ©gion mĂ©tropolitaine pour crĂ©er des fichiers uniques dans le dossier output.

Comme nous sommes encore en phase d’exploration pour la crĂ©ation des cartes, limitons la boucle pour ne travailler que sur la premiĂšre rĂ©gion mĂ©tropolitaine pour l’instant. Cela nous permettra d’itĂ©rer plus rapidement et d’amĂ©liorer notre visualisation des donnĂ©es.

visualizeData.ts
import { SimpleTable } from "@nshiab/simple-data-analysis";
import { geo, plot } from "@observablehq/plot";
 
export default async function visualizeData(census: SimpleTable) {
  const allCMAs = await census.getUniques("CMA");
 
  function drawMap(
    data: { features: { properties: { [key: string]: unknown } }[] },
  ) {
    return plot({
      marks: [
        geo(data, { fill: "varPerc", stroke: "varPearc" }),
      ],
    });
  }
 
  for (const CMA of allCMAs) {
    const tableCMA = await census.cloneTable();
    await tableCMA.keep({ CMA });
    await tableCMA.writeMap(drawMap, `./sda/output/${CMA}.png`);
    break;
  }
}

Capture d'écran de VS Code montrant une premiÚre itération de la carte.

C’est moche, mais ça fonctionne !

Nous pouvons maintenant ajouter un titre, un sous-titre et une légende. Nous pouvons également utiliser une échelle divergente pour les couleurs et restreindre le domaine de -100 % à +100 %. Nous pouvons aussi ajuster la projection. Si vous voulez en savoir plus sur ces personnalisations, consultez la leçon Visualiser des données.

Une chose que vous vous demandez peut-ĂȘtre est pourquoi nous utilisons data.features[0].properties.CMA pour le titre.

Lorsque cette fonction sera exĂ©cutĂ©e, elle ne connaĂźtra pas la variable CMA. Nous devons donc rĂ©cupĂ©rer le nom de la rĂ©gion mĂ©tropolitaine directement Ă  partir des donnĂ©es. Comme nous utilisons writeMap, SDA transmet les donnĂ©es sous forme de GeoJSON Ă  cette fonction. Dans les features GeoJSON, les donnĂ©es sont stockĂ©es dans l’objet properties. Nous rĂ©cupĂ©rons donc le nom de la rĂ©gion mĂ©tropolitaine en prenant le premier feature et en trouvant le nom CMA dans ses propriĂ©tĂ©s.

visualizeData.ts
import { SimpleTable } from "@nshiab/simple-data-analysis";
import { geo, plot } from "@observablehq/plot";
 
export default async function visualizeData(census: SimpleTable) {
  const allCMAs = await census.getUniques("CMA");
 
  function drawMap(
    data: { features: { properties: { [key: string]: unknown } }[] },
  ) {
    return plot({
      title: data.features[0].properties.CMA,
      subtitle:
        "Variation from the median household total income at the dissemination area level.",
      caption: "2021 Census, Statistics Canada",
      inset: 10,
      projection: {
        type: "mercator",
        domain: data,
      },
      color: {
        legend: true,
        type: "diverging",
        domain: [-100, 100],
        label: null,
        tickFormat: (d) => {
          if (d > 0) {
            return `+${d}%`;
          } else {
            return `${d}%`;
          }
        },
      },
      marks: [
        geo(data, { fill: "varPerc", stroke: "varPerc" }),
      ],
    });
  }
 
  for (const CMA of allCMAs) {
    const tableCMA = await census.cloneTable();
    await tableCMA.keep({ CMA });
    await tableCMA.writeMap(drawMap, `./sda/output/${CMA}.png`);
    break;
  }
}

Capture d'écran de VS Code montrant une carte stylisée.

Ça a bien meilleure allure.

Une derniùre chose que j’aime faire sur mes cartes est d’ajouter un contour.

Dans la boucle, nous pouvons cloner la tableCMA et fusionner toutes les gĂ©omĂ©tries des aires de diffusion en utilisant la mĂ©thode aggregateGeo. Comme cela peut ĂȘtre assez coĂ»teux en termes de calcul lorsqu’il y a beaucoup d’aires de diffusion, nous mettons le rĂ©sultat en cache.

Nous pouvons ensuite insérer cette géométrie nouvellement créée dans la tableCMA.

Dans la fonction drawMap, nous pouvons ajouter un nouveau marqueur geo pour dessiner le contour. Mais nous devons filtrer sur le varPerc pour nous assurer de ne dessiner que le contour et non toutes les aires de diffusion Ă  nouveau.

visualizeData.ts
import { SimpleTable } from "@nshiab/simple-data-analysis";
import { geo, plot } from "@observablehq/plot";
 
export default async function visualizeData(census: SimpleTable) {
  const allCMAs = await census.getUniques("CMA");
 
  function drawMap(
    data: { features: { properties: { [key: string]: unknown } }[] },
  ) {
    return plot({
      title: data.features[0].properties.CMA,
      subtitle:
        "Variation from the median household total income at the dissemination area level.",
      caption: "2021 Census, Statistics Canada",
      inset: 10,
      projection: {
        type: "mercator",
        domain: data,
      },
      color: {
        legend: true,
        type: "diverging",
        domain: [-100, 100],
        label: null,
        tickFormat: (d) => {
          if (d > 0) {
            return `+${d}%`;
          } else {
            return `${d}%`;
          }
        },
      },
      marks: [
        geo(
          data.features.filter((d) => typeof d.properties.varPerc === "number"),
          { fill: "varPerc", stroke: "varPerc" },
        ),
        geo(data.features.filter((d) => d.properties.varPerc === null), {
          stroke: "black",
          opacity: 0.5,
        }),
      ],
    });
  }
 
  for (const CMA of allCMAs) {
    const tableCMA = await census.cloneTable();
    await tableCMA.keep({ CMA });
 
    const outline = await tableCMA.cloneTable();
    await outline.cache(async () => {
      await outline.aggregateGeo("union");
    });
    await tableCMA.insertTables(outline);
 
    await tableCMA.writeMap(drawMap, `./sda/output/${CMA}.png`);
    break;
  }
}

Capture d'écran de VS Code montrant une carte stylisée avec un contour.

La différence est subtile, mais il est important de voir les trous dans la géométrie de la région métropolitaine.

Maintenant que nous sommes satisfaits de l’apparence de notre carte, il est temps de supprimer l’instruction break Ă  la ligne 57 ! CrĂ©ons 43 cartes ! ExĂ©cutez tout le code !

Si vous examinez les cartes aprÚs avoir exécuté votre code, vous remarquerez quelques problÚmes.

Capture d'écran montrant des cartes problématiques.

Tout d’abord, vous pouvez voir que certains dossiers ont Ă©tĂ© créés pour certaines rĂ©gions mĂ©tropolitaines dans le dossier output. C’est parce que certains noms contiennent le caractĂšre / ! Nous devons remplacer ce caractĂšre par autre chose avant de le transmettre Ă  writeMap.

Nous pouvons mettre Ă  jour visualizeData.ts pour corriger cela.

visualizeData.ts
import { SimpleTable } from "@nshiab/simple-data-analysis";
import { geo, plot } from "@observablehq/plot";
 
export default async function visualizeData(census: SimpleTable) {
  const allCMAs = await census.getUniques("CMA");
 
  function drawMap(
    data: { features: { properties: { [key: string]: unknown } }[] },
  ) {
    return plot({
      title: data.features[0].properties.CMA,
      subtitle:
        "Variation from the median household total income at the dissemination area level.",
      caption: "2021 Census, Statistics Canada",
      inset: 10,
      projection: {
        type: "mercator",
        domain: data,
      },
      color: {
        legend: true,
        type: "diverging",
        domain: [-100, 100],
        label: null,
        tickFormat: (d) => {
          if (d > 0) {
            return `+${d}%`;
          } else {
            return `${d}%`;
          }
        },
      },
      marks: [
        geo(
          data.features.filter((d) => typeof d.properties.varPerc === "number"),
          { fill: "varPerc", stroke: "varPerc" },
        ),
        geo(data.features.filter((d) => d.properties.varPerc === null), {
          stroke: "black",
          opacity: 0.5,
        }),
      ],
    });
  }
 
  for (const CMA of allCMAs) {
    const tableCMA = await census.cloneTable();
    await tableCMA.keep({ CMA });
 
    const outline = await tableCMA.cloneTable();
    await outline.cache(async () => {
      await outline.aggregateGeo("union");
    });
    await tableCMA.insertTables(outline);
 
    await tableCMA.writeMap(
      drawMap,
      `./sda/output/${(CMA as string).replaceAll("/", "-")}.png`,
    );
  }
}

Nous pouvons Ă©galement constater que le contour de certaines villes n’est pas correct et que la carte de MontrĂ©al n’est qu’un Ă©norme polygone. Cela se produit gĂ©nĂ©ralement lorsque certaines gĂ©omĂ©tries sont invalides. La simplification crĂ©e souvent des gĂ©omĂ©tries invalides. Heureusement, nous pouvons utiliser la mĂ©thode fixGeo pour rĂ©soudre ce problĂšme !

Nous pouvons mettre à jour crunchData.ts, dans la méthode cache, pour corriger les géométries aprÚs le chargement des données géospatiales.

crunchData.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
export default async function crunchData(sdb: SimpleDB) {
  const census = sdb.newTable("census");
 
  await census.cache(async () => {
    await census.loadData("sda/data/98-401-X2021006_eng_CSV/*_utf8.csv", {
      strict: false,
    });
    await census.keep({
      GEO_LEVEL: "Dissemination area",
      CHARACTERISTIC_ID: [243], // Median total income of household in 2020 ($)
    });
    await census.selectColumns([
      "DGUID",
      "GEO_NAME",
      "C1_COUNT_TOTAL",
    ]);
    await census.renameColumns({ C1_COUNT_TOTAL: "medianIncome" });
 
    const names = sdb.newTable("names");
    await names.loadData("sda/data/2021_92-151_X_utf8.csv", { strict: false });
    await names.keep({
      CMATYPE_RMRGENRE: "B",
    });
    await names.selectColumns(["DADGUID_ADIDUGD", "CMANAME_RMRNOM"]);
    await names.removeDuplicates();
    await names.renameColumns({
      DADGUID_ADIDUGD: "DGUID",
      CMANAME_RMRNOM: "CMA",
    });
    await census.join(names);
 
    const disseminationAreas = sdb.newTable("disseminationAreas");
    await disseminationAreas.loadGeoData(
      "sda/data/lda_000b21a_e_simplified.shp.zip",
      { toWGS84: true },
    );
    await disseminationAreas.fixGeo();
    await disseminationAreas.selectColumns(["DGUID", "geom"]);
    await census.join(disseminationAreas);
    await census.selectColumns(["medianIncome", "CMA", "geom"]);
    await census.removeMissing();
  });
 
  const medians = await census.summarize({
    values: "medianIncome",
    categories: "CMA",
    summaries: "median",
    outputTable: "medians",
  });
  await medians.removeColumns("value");
 
  await census.join(medians);
  await census.addColumn(
    "varPerc",
    "number",
    `(medianIncome - median) / median * 100`,
  );
  await census.round("varPerc");
 
  return census;
}

ArrĂȘtez de surveiller main.ts (CTRL + C dans votre terminal) et supprimez .temp et .sda-cache manuellement ou en exĂ©cutant deno task clean. Supprimez Ă©galement tout le contenu du dossier output.

Et relançons tout depuis le début !

Capture d'écran montrant les cartes corrigées.

Tout fonctionne parfaitement et tous nos problùmes ont disparu. 😁

Sur mon ordinateur, notre code a pu traiter toutes les donnĂ©es et dessiner toutes les cartes en 2 minutes et 46 secondes. Mais grĂące Ă  notre systĂšme de mise en cache astucieux, si je veux ajuster l’apparence des cartes, cela prend seulement 32 secondes pour réécrire les 43 cartes.

Au fait, ici, nous enregistrons nos cartes sous forme d’images, mais si vous voulez des vecteurs pour les modifier dans Illustrator ou d’autres outils de conception, il vous suffit de remplacer .png par .svg dans writeMap.

Conclusion

Que de chemin parcouru ! Traiter de grands ensembles de donnĂ©es avec plusieurs tables stockant des donnĂ©es tabulaires et gĂ©ospatiales n’est pas une tĂąche facile.

Mais j’espĂšre que cet exemple concret vous a montrĂ© comment vous pouvez couper au travers de gigaoctets de donnĂ©es comme dans du beurre avec SDA. 🧈

Amusez-vous bien avec votre prochain projet de donnĂ©es et n’hĂ©sitez pas Ă  me contacter si vous souhaitez partager ce que vous crĂ©ez avec SDA ou si vous avez des questions ! 😊

Vous avez aimé ? Vous voulez ĂȘtre prĂ©venu quand de nouvelles leçons sont publiĂ©es ? Abonnez-vous Ă  l'infolettre ✉ et donnez une ⭐ au cours sur GitHub pour me garder motivé ! Contactez-moi si vous avez des questions.