虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

コロナウイルス(COVID-19)のデータを地図上に可視化してみる

皆さんこんにちは。虎の穴ラボのY.Fです。
ちょっと前から、虎の穴ラボも原則リモートになったのでこのブログも自宅から書いてます。

(リモートワークの様子はこちら) toranoana-lab.hatenablog.com

さて、今回はリモートワーク実施の発端である、COVID-19(コロナウィルス)に関するデータを地図上に表示してみたいと思います。

今回作るもの

leaflet.jsと公開されているCOVID-19のデータを使って、どこの地域でどのくらいの感染者が出ているかなどを表示してみようと思います。

leafletjs.com

以下のようなものが出来上がります。以下の例は、アメリカあたりの情報を表示したスクリーンショットになります。

f:id:toranoana-lab:20200427151958p:plain
地域毎の亡くなった人数

使うもの

今回利用する環境及びデータは以下のようになります。

また、依存ライブラリは以下の package.json を参照下さい。

{
  "name": "covid-leaflet",
  "version": "1.0.0",
  "description": "COVID-19 Data description to leaflet(OpenStreetMap)",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --inline --progress",
    "start": "npm run dev",
    "build": "webpack"
  },
  "keywords": [
    "leaflet","COVID-19"
  ],
  "author": "y.f",
  "license": "MIT",
  "dependencies": {
    "leaflet": "^1.6.0",
    "papaparse": "^5.2.0",
    "typescript": "^3.8.3"
  },
  "devDependencies": {
    "@types/leaflet": "^1.5.12",
    "@typescript-eslint/eslint-plugin": "^2.29.0",
    "@typescript-eslint/parser": "^2.29.0",
    "css-loader": "^3.5.2",
    "eslint": "^6.8.0",
    "eslint-config-prettier": "^6.10.1",
    "eslint-plugin-prettier": "^3.1.3",
    "file-loader": "^6.0.0",
    "html-webpack-plugin": "^4.2.0",
    "moment": "^2.24.0",
    "prettier": "^2.0.4",
    "style-loader": "^1.1.4",
    "ts-loader": "^7.0.1",
    "webpack": "^4.42.1",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.10.3"
  }
}

COVID-19のデータ

さて、まずはじめにCOVID-19のデータを取得して行きたいと思います。
データ自体はCSVとして上記リポジトリに保存されています。地域ごとの確認数、亡くなった人数、回復者数が取得できます。

(日毎のデータ) github.com

このままだと使いにくいので、テキストデータを以下のURLから取得します。 https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/04-26-2020.csv

上記データを利用する場合、ライセンスや利用規約には注意して下さい。
Reliance on the Website for medical guidance or use of the Website in commerce is strictly prohibited. とあるので、商用目的では利用できません。

データ取得の実装

では、上記CSVを取得していきます。CSVを手でパースするのは面倒なので、今回は PaPaParse というライブラリを利用します。

www.papaparse.com

このライブラリを使うことで、少しオプションを設定するだけで簡単にURLからCSVを読み込めます。

import moment, { Moment } from 'moment';
import { parse } from "papaparse";

const PROVINCE_STATE = 2;
const COUNTRY_REGION = 3;
const LAST_UPDATE = 4;
const LAT = 5;
const LNG = 6;
const CONFIRMED = 7;
const DEATHS = 8;
const RECOVERED = 9;
const ACTIVE = 10;

export interface CovidResponse {
  provinceState: string;
  countryRegion: string;
  lastUpdate: Moment;
  lat: number;
  lng: number;
  confirmedNumber: number;
  deathNumber: number;
  recoverdNumber: number;
  // 上記全部の合計
  activeAmount: number;
}

/**
 * デイリーのコロナデータを取得するメソッド
 */
export const getCovidData = async (): Promise<CovidResponse[]> => {
  let subDay = 1;
  let link = getLink(subDay);
  let data = await loadCsv(link).catch(err => err);
  // 時間帯的にデータがない場合があるのでその場合は一日前のデータを取る
  if (data instanceof Error && data.message === "Not Found") {
    subDay += 1;
    console.info(`CSV Data is 'Not Found'. Try to get ${subDay} ago data.`)
    link = getLink(subDay);
    data = await loadCsv(link).catch(err => console.error(err));
  } else if (data instanceof Error) {
    console.error(data);
    return [];
  }
  if (!data) return [];
  // ヘッダ行を除くのでsliceする
  data = data.slice(1);
  const result = data.filter(csvRow => csvRow[LAT] && csvRow[LNG]).map(csvRow => convertCsvToObject(csvRow));
  return result;
};

const getLink = (subDay: number) => `https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/${nowStr(subDay)}.csv`;

const nowStr = (subDay: number) => moment().subtract(subDay, "d").format("MM-DD-YYYY");

// PaPaParseを使ってCSVを読み込むが、Promiseを返してはくれないのでラップする
const loadCsv = (url: string): Promise<string[][]> => {
  return new Promise<string[][]>((resolve, reject) => {
    parse(url, {
      download: true,
      complete: (results: any) => {
        resolve(results.data);
      },
      error: (err: Error) => {
        reject(err);
      }
    });
  });
};

// CSVをパースした結果をインターフェースに落とし込む
const convertCsvToObject = (csvData: string[]): CovidResponse => {
  return {
    provinceState: csvData[PROVINCE_STATE],
    countryRegion: csvData[COUNTRY_REGION],
    lastUpdate: moment(csvData[LAST_UPDATE]),
    lat: parseFloat(csvData[LAT]),
    lng: parseFloat(csvData[LNG]),
    confirmedNumber: parseInt(csvData[CONFIRMED]),
    deathNumber: parseInt(csvData[DEATHS]),
    recoverdNumber: parseInt(csvData[RECOVERED]),
    activeAmount: parseInt(csvData[ACTIVE])
  }
};

これでCSVデータを取得し、その結果を CovidResponse インターフェースに落とし込む事ができます。

地図を扱う

今回は、leaflet.jsというライブラリを使ってOpenStreetMapを扱います。
利用できる地図はOpenStreetMap以外にも国土地理院が出しているものがいくつかあります。当然、日本周辺しか表示できません。
maps.gsi.go.jp

地図データを扱う場合にも、COVID-19のデータと同様にライセンスは注意が必要です。

www.openstreetmap.org

OpenStreetMapは上記のようにライセンス表記が必要になります。

地図上にCOVID-19のデータを表示する

背景地図をOpenStreetMapにし、いわゆる主題図を上記で取得したCOVID-19のデータとして表示します。

import 'leaflet/dist/leaflet.css';
import "./main.css";
import "./images/layers-2x.png";
import "./images/layers.png";

import L from 'leaflet';
import { getCovidData, CovidResponse } from './covidApi';

const TORA_BUILDINGS: [number, number] = [35.7025938, 139.772022];
const LICENSES: { [key: string]: string } = {
  OSM:
    '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
};

const covidDataStr = (covidData: CovidResponse) => {
  return `
  <table>
    <tbody>
      <tr><td>国</td><td>${covidData.countryRegion}</td></tr>
      <tr><td>地域</td><td>${covidData.provinceState}</td></tr>
      <tr><td>更新日時</td><td>${covidData.lastUpdate.format("YYYY/MM/DD hh:mm:ss")}</td></tr>
      <tr><td>累計確認数</td><td>${covidData.confirmedNumber}</td></tr>
      <tr><td>累計死亡者数</td><td>${covidData.deathNumber}</td></tr>
      <tr><td>累計回復者数</td><td>${covidData.recoverdNumber}</td></tr>
    </tbody>
  </table>
  `;
};

(async function() {
  const covidData = await getCovidData();

  const customCovidLayerConfirm = covidData.map(data => {
    return  L.circle([data.lat, data.lng], {
      color: 'yellow',
      fillColor: '#ffe200',
      fillOpacity: 0.5,
      radius: data.confirmedNumber * 2
    }).bindPopup(covidDataStr(data));
  });
  const confirmLayer = L.layerGroup(customCovidLayerConfirm);

  const customCovidLayerDeath = covidData.map(data => {
    return L.circle([data.lat, data.lng], {
      color: 'red',
      fillColor: '#f03',
      fillOpacity: 0.5,
      radius: data.deathNumber * 2
    }).bindPopup(covidDataStr(data));
  })
  const deathLayer = L.layerGroup(customCovidLayerDeath);

  const customCovidLayerRecover = covidData.map(data => {
    return L.circle([data.lat, data.lng], {
      color: 'green',
      fillColor: '#78f4ad',
      fillOpacity: 0.5,
      radius: data.recoverdNumber * 2
    }).bindPopup(covidDataStr(data));
  })
  const recoverLayer = L.layerGroup(customCovidLayerRecover);

  // レイヤ追加(OSM)
  const osm = L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
    { attribution: LICENSES['OSM'], maxZoom: 10 }
  );
  let mymap = new L.Map('map').setView(TORA_BUILDINGS, 5);
  const baseMaps = {
    "OSM": osm
  };
  const overlayMaps = {
    "Confirm": confirmLayer,
    "Deaths": deathLayer,
    "Recover": recoverLayer
  };

  osm.addTo(mymap);
  L.control.layers(baseMaps, overlayMaps).addTo(mymap);
})();

各主題図となりうるデータをカスタムレイヤとして登録しています。(confirmLayer, deathLayer, recoverLayer)
また、表示しようとしているデータに従って地図上に円ポリゴンを描画しています。

まとめ

今回はCOVID-19のデータを可視化してみました。
用意されている様々なライブラリを利用することで、個人でも簡単に地図データの可視化などができることが理解していただけたのでは無いかと思います。
また、もう一歩高度なことをしたい場合は、自分で地図のタイル画像を作るという方法があります。
よく知られているのがPostgreSQL + PostGIS + QGISという組み合わせなので興味がある方はぜひお試しください。

qgis.org

P.S.

虎の穴ラボでの開発に少しでも興味を持っていただけた方は、採用説明会やカジュアル面談という場でもっと深くお話しすることもできます。ぜひお気軽に申し込みいただければ幸いです。 虎の穴ラボのエンジニアが、開発プロセスの内容であったり、「今季何見ました?」といったオタクトークであったり、何でもお応えします。
※現在はオンラインカジュアル面談のみ受け付けております。(2020/4/28時点)

WantedlyLAPLASでの採用も行っております)

yumenosora.co.jp

news.toranoana.jp

とらのあなラボでは、ツイッターで情報発信しています。ぜひフォローしてください! twitter.com

5月14日に「とらのあな採用説明会 5/14 オタク企業で働くエンジニアの魅力について」オンライン会社説明会を開催します。

yumenosora.connpass.com

また、5月27日には、「【オンライン開催】リモートワークノウハウLT【とらのあなLT】」を開催します。定例LT会ですが、今回はリモートワークに的を絞った内容となります。人数制限はございませんのでぜひご参加ください!

yumenosora.connpass.com