Vue.jsとChart.jsでCovid19の観測サイトを作る

Vue.js に慣れるため、という名目で Covid 19 の観測サイトを作成してみます。

実のところ、Covid 19 の観測サイトは既にたくさんあります。

ただ、データ集計をして表示するだけならば色々あるのですが、

  • 観測データを動的に比較する
  • 観測データを使って、何かシミュレーションする

ことができません。以前 プログラマにもわかる SEIR モデルシミュレーション – Qiita ということで、SEIR モデルで Excel を使って予測値を出していたのですが、これは毎日手作業でやっていました。データ整形がちょっと面倒だったのと、予測 Rt 値を恣意的に(数日の平均など)を使っていくつかシミュレーションしていたのです。

で、先日 NHK サイトで二次利用可能なオープンデータとして使えることが分かったので、これを使って自動化してみます。

CSV 形式を加工する

Vue.js(JavaScript) で NHK の CSV 形式のデータをパースしてもよいのですが、既に JSON 形式になっていたほうが楽なので、中間の Web API を作成します。

Azure Function のタイマートリガー

タイマートリガーで1時間単位でCSV形式のファイルをダウンロードします。
Covid 19 のデータは1日単位でしか更新されないので、もっとスパンが長くてもよいのですが、何時頃公開されるか判らないので1時間単位。

プログラムを最初に書いたときは、いちいちCSV形式のデータをダウンロードしていたのですが、結構遅いので、ダウンロードして JSON 形式にパースしたら BLOB に保存しています。

[FunctionName("NHKCovidTimer")]
public static async Task RunTimer([TimerTrigger("0 5 * * * *")] TimerInfo myTimer,
    [Blob("covid/japan.json", FileAccess.Write)] Stream jsonfile,
    ILogger log)

{
    log.LogInformation("called NHKCovidTimer");
    var url = "https://www3.nhk.or.jp/n-data/opendata/coronavirus/nhk_news_covid19_prefectures_daily_data.csv";
    var cl = new HttpClient();
    // 1行ずつ読み込み JSON 形式に変換
    var res = await cl.GetAsync(url);
    var data = new List<Covid>();
    using (var st = new StreamReader(await res.Content.ReadAsStreamAsync()))
    {
        // タイトルは読み飛ばし
        st.ReadLine();
        while (true)
        {
            string line = st.ReadLine();
            if (string.IsNullOrEmpty(line)) break;
            var items = line.Split(",");
            if (items.Length >= 7)
            {
                var it = new Covid()
                {
                    Date = DateTime.Parse(items[0]),
                    LocationId = int.Parse(items[1]),
                    Location = items[2],
                    Cases = int.Parse(items[3]),
                    CasesTotal = int.Parse(items[3]),
                    Deaths = int.Parse(items[3]),
                    DeathsTotal = int.Parse(items[3]),
                };
                data.Add(it);
            }
        }
        // ソートしておく
        data = data.OrderBy(t => t.LocationId).ThenBy(t => t.Date).ToList();
        // 週平均を計算
        calcCasesAve(data);
        // 週単位Rt値を計算
        calcCasesRt(data);
        // 週単位Rt平均値を計算
        calcCasesRtAve(data);
    }
    var json = JsonConvert.SerializeObject(new { result = data });
    var writer = new StreamWriter(jsonfile);
    writer.Write(json);
    writer.Close();
    // return new OkObjectResult("save json " + DateTime.Now.ToString());
}

CSV 形式をパースするだけでなく、あらかじめ

  • 週平均
  • 週単位のRt値
  • 週単位のRt平均値

などを計算しておきます。

保存する JSON の形式は Covid クラスに定義しています。大文字をわざわざ小文字に変えているのは、Vue.js の読み取りに合わせたためです。

public class Covid
{
    [JsonProperty("date")]
    public DateTime Date { get; set; }
    [JsonProperty("locationId")]
    public int LocationId { get; set; }
    [JsonProperty("location")]
    public string Location { get; set; }
    [JsonProperty("cases")]
    public int Cases { get; set; }
    [JsonProperty("casesTotal")]
    public int CasesTotal { get; set; }
    [JsonProperty("deaths")]
    public int Deaths { get; set; }
    [JsonProperty("deathsTotal")]
    public int DeathsTotal { get; set; }

    [JsonProperty("casesAverage")]
    public float CasesAverage { get; set; }   // 週移動平均
    [JsonProperty("casesRt")]
    public float CasesRt { get; set; }        // 週単位Rt値 = 続く1週間の感染者数平均 / 当日感染者数  
    [JsonProperty("casesRtAverage")]
    public float CasesRtAverage { get; set; }     // Rt値の週移動平均
}

HTTP トリガーを定義する

Web API は非常に簡単で、HttpTrigger で JSON ファイルの中味を返すだけです。データ量が 3M 程度になって大き目になってしまったので、後で期間や都道府県で絞れるように修正します。

[FunctionName("NHKCovid")]
public static async Task<IActionResult> RunRead(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    [Blob("covid/japan.json", FileAccess.Read)] Stream jsonfile,
    ILogger log)
{
    log.LogInformation("called NHKCovid");
    var sr = new StreamReader(jsonfile);
    var json = await sr.ReadToEndAsync();
    return new OkObjectResult(json);
}

Vue.js + Chart.js 側のコード

例えば、陽性者数のグラフは、Web APIの中から cases の値を羅列するだけの簡単なものです。
都道府県を複数選択できる(this.valueにある)ので、これの分だけ datasets を作ります。Chart.js は datasets の中に線グラフの色などが入っているので、その部分は二重に定義することになります。ここでは、最低限の定義しかしていません。

Chart.js は vue-chartjs を使っています。

/**
 * 感染者数のグラフを作成
 */
makeCases(res,start_date,end_date,locations) {

    var sdate = Date.parse(start_date)
    var edate = Date.parse(end_date)
    var datasets = []
    var labels = []

    var i = 0;
    locations.forEach(location => {
    var data = [];
    var data2 = [];
    labels = [];
    res.data.result.forEach(el => {
        if ( el.location == location ) {
        var dt = Date.parse( el.date )
        if ( sdate <= dt && dt <= edate ) {
            dt = new Date(dt)
            dt = dt.getFullYear() + "/" + (dt.getMonth()+1) + "/" + dt.getDate() 
            labels.push( dt )
            data.push( el.cases )
            data2.push( el.casesAverage )
        }
        }
    });
    var dataset = 
        {
        label: location,
        fill: false,
        borderColor: i >= this.colors.length? "rgba(200,200,200,0.5)": this.colors[i].n,
        data: data
        }
    var dataset2 = 
        {
        label: location + "(週平均)",
        fill: false,
        borderColor: i >= this.colors.length? "rgba(100,100,100,0.5)": this.colors[i].ave,
        data: data2
        }
    datasets.push( dataset )
    datasets.push( dataset2 )
    i++;
    })
    return { labels, datasets };
},

都道府県を再選択したときにグラフを再描画させます。しかし、Chart.js がデータの更新による再表示に対応していないので、ごっそりデータの中味を書き替えて更新を通知するという方式をとっています。クローンは JSON.parse(JSON.stringify(…)) で作ると安全にできます。

async getData() {
    var url = process.env.VUE_APP_NHK_COVID_API_URL
    console.log( url )
    var res = await axios.get(url);

    var { labels, datasets } = this.makeCases( res, "2020-10-01", "2021-12-31", this.value )
    var { labels2, datasets2 } = this.makeCasesRt( res, "2020-10-01", "2021-12-31", this.value )
    this.datax.labels = labels ;
    this.datax.datasets = datasets ;
    this.datart.labels = labels2 ;
    this.datart.datasets = datasets2 ;

    var { labels3, datasets3 } = this.makeCasesFuture( res, "2020-12-01", "2021-12-31", this.value )
    this.datafu.labels = labels3 ;
    this.datafu.datasets = datasets3 ;

    var data = this.makeCases2( res, "2020-10-01", "2021-12-31", this.value )
    this.datax2.labels = data.labels ;
    this.datax2.datasets = data.datasets ;
    // 再描画の代わり
    this.datax = JSON.parse(JSON.stringify(this.datax));
    this.datax2 = JSON.parse(JSON.stringify(this.datax2));
    this.datart = JSON.parse(JSON.stringify(this.datart));
    this.datafu = JSON.parse(JSON.stringify(this.datafu));
},

本来ならば、この this.datax まわりを Vuex の Store に詰め込めばいいのですが、これも後で変えましょう。4つのグラフが並んでいると、さすがに面倒臭いので。

予測値を計算する

予測の計算が試行錯誤がやりやすいように、JavaScript 側で計算しています。

  • 感染期間を7日間として Rt を計算する
  • Rt 値から週平均 Rt 値を計算する
  • 週平均 Rt 値から、未来の日の陽性者数を計算する

確定した週平均 Rt 値(精度上、1週間前の値が確定値になる)を使って、前進的に予測します。

/**
 * 最新の実効再生産数から今後1か月の感染者数を予測
 */
makeCasesFuture(res,start_date,end_date,locations) {

    var sdate = Date.parse(start_date)
    var edate = Date.parse(end_date)
    var datasets = []
    var labels = []
    var i = 0;

    console.log( edate )

    locations.forEach(location => {
    // 最終日を取得
    var lastdate = null 
    var last = null
    res.data.result.forEach(el => {
        if ( el.location == location ) {
        var dt = Date.parse( el.date )
        if ( el.casesRt > 0.0 ) {
            if ( lastdate < dt ) {
            lastdate = dt 
            last = el
            }
            if ( sdate <= dt ) {
            dt = new Date(dt)
            dt = dt.getFullYear() + "/" + (dt.getMonth()+1) + "/" + dt.getDate() 
            if ( i == 0 ) labels.push( dt )
            }
        }
        }
    })
    console.log( last );

    var data = [];
    var data2 = [];
    // 実測値を集計
    res.data.result.forEach(el => {
        if ( el.location == location ) {
        var dt = Date.parse( el.date )
        if ( el.casesRt > 0.0 ) {
            if ( sdate <= dt ) {
            data.push( el.cases )
            data2.push( el.casesRtAverage )
            }
        }
        }
    })
    // 予測値を計算
    var rt = last.casesRtAverage ;
    for ( var j=1; j<=40; j++ ) {
        // 過去7日間の cases と Rt から予測 cases を計算する
        var len = data.length ;
        var cases = (
        data[ len-7 ] * data2[ len-7 ] + 
        data[ len-6 ] * data2[ len-6 ] +
        data[ len-5 ] * data2[ len-5 ] + 
        data[ len-4 ] * data2[ len-4 ] + 
        data[ len-3 ] * data2[ len-3 ] + 
        data[ len-2 ] * data2[ len-2 ] + 
        data[ len-1 ] * data2[ len-1 ] ) / 7.0 ;
        data.push( Math.floor(cases))
        data2.push( rt );

        var dt = new Date(Date.parse( last.date ))
        dt.setDate(dt.getDate() + j);
        dt = dt.getFullYear() + "/" + (dt.getMonth()+1) + "/" + dt.getDate() 
        if ( i == 0 ) labels.push( dt )
    }

    var dataset = 
        {
        label: location,
        fill: false,
        borderColor: i >= this.colors.length? "rgba(200,200,200,0.5)": this.colors[i].n,
        data: data,
        yAxisID: "y-axis-1", 
        }
    var dataset2 = 
        {
        label: location + "週平均Rt",
        fill: false,
        borderColor: i >= this.colors.length? "rgba(100,100,100,0.5)": this.colors[i].ave,
        data: data2,
        yAxisID: "y-axis-2", 
        }
    datasets.push( dataset )
    datasets.push( dataset2 )
    i++;
    })
    return { labels3: labels, datasets3: datasets };
},

実行

https://moonmile.net/nhkcovid/

陽性者数

陽性者数予測

コード

moonmile/NHKCovid: Covid 19 の観測サイト

カテゴリー: 開発 | Vue.jsとChart.jsでCovid19の観測サイトを作る はコメントを受け付けていません

具体的に Vuex と MVVM を比較する

MVVM を習得している人に Flux な Vuex をどう使ったら学習効率が良さそうなのか?

まずは Blazor で

Redmine の projects テーブルの一覧を表示する画面を作成します。

MVVM パターンに分離せず、ひとつの Projects.razor ファイルに作成しています。

@page "/project"
@inject HttpClient Http

<h1>プロジェクト一覧</h1>
<table class="table">
    <thead>
        <tr>
            <th>ID</th>
            <th>名前</th>
            <th>説明</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in projects)
        {
            <tr>
                <td>@item.Id</td>
                <td>@item.Name</td>
                <td>@item.Description</td>
            </tr>
        }
    </tbody>
</table>

@code {
    private Project[] projects = new Project[] { };

    protected override async Task OnInitializedAsync()
    {
        var url = "http://localhost:8000/api/project";
        this.projects = await Http.GetFromJsonAsync<Project[]>(url);
    }

    public class Project {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
    }
}
  1. ルーティングは @page で指定します。これは Vue Router よりちょっと便利。
  2. データは、this.projects で参照できるように。Vue で言えば data に保持している感じ
  3. Web API の呼び出しは HttpClient をインジェクションして利用します。HttpClient は、連続して何度も呼び出すとタイムアウトが重なっていまってメモリを喰うという欠点があるのです。このため、Web 絡みではひとつの HttpClient を使いまわします。

.NET5 の HttpClient には、GetFromJsonAsync という拡張メソッドが入って、Newtonsoft の Json ライブラリがいらなくなりました。取り込んだのか、別途実装したのかは不明ですが、単純に JSON をクラスに取り込むだけならばこれでいけます。

Web API で取得できる JSON の中味はさておき、取り出したいキーだけをプロパティとして設定しておきます。実は、日付とかブール値とかは自動で変換してくれないので、別途コンバータか自前変換が必要なのが、いまいちなところです。まあ、単純なデシリアライズならばこれで十分かも。

Newtonsoft.Json から System.Text.Json に移行する – .NET | Microsoft Docs

データストア的には、プロジェクトの一覧を保持している projects だけなので、Vuex を使うと冗長すぎて比較としてはフェアではないのですが、これを Vue/Vuex を使って書き直します。
(実際は、Vue.js で書いた後に Blazor で書いていますが)

次に Vuex で

実のところ、Vue 側は、

  • Vue
  • Vuex
  • Vue Router
  • Vuetify

を使っています。

なので、Blazor 版よりも構造化されているのですが、ひとまず、標準的(と思われる)Vuex の使い方です。

Web API は axios を使っています。直接 store.js のほうに突っ込んでもいいのですが、Web API なライブライ的に使い廻せるところは外部に題しておきたいところです。

JavaScript の場合、ライブラリをどう作るのか不明(昔の prototype しか知らないのであった)なので、こんな感じに作って置きます。Web API の呼び出しの場合、

  • Web API 自身のエラーをどうするのか?
  • 認証系のエラーをどうするのか?
  • パラメータ不正の場合はどうするのか?

というややこしい話がありますが、大抵の場合は正常に返ってくるものと想定して作ります。そして、不正になった場合は、そのまま例外が飛ぶので、UI 側でエラーになります。

本来ならば、フェールセーフ的に axios の例外が発生したときは、しれっと projects の空配列を返したいところです。

webapi/redmine.js

import axios from 'axios'

export default {
    project: {
        async all() {
            var res = await axios.get('http://localhost:8000/api/project');
            var items = res.data ;
            return items ;
        },
    },
}

データストアは、こんな感じで書きます。
state, mutations, getters のところは普段通りの定番。getters は使わなくてもよいのですが、統一的に map できるように作っておきます。
そして、actions の中から定義済みの Web API を呼び出します。

store.js

import Vue from 'vue'
import Vuex from 'vuex'
import redmine from '@/webapi/Redmine'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        projects: [],
    },
    mutations: {
        projects(state, payload) {
            state.projects = payload.projects
        },
    },
    getters: {
        projects(state) {
            return state.projects
        },
    },
    actions: {
        async projects({commit}) {
            var items = await redmine.project.all();
            commit('projects', { projects: items })
        },
    },
})

それぞれの名前を "projects" に統一させてしまっているのは、MVVM パターンに似せるためです。state に対する get/set が異なる名前になるとプロパティの設定/取得としては違和感があります。逆に言えば、get/set を統一的に使いたくない場合(非対称な場合)は、この projects の部分は名前を変えたほうがいいでしょう。

おそらく、検索結果をフィルターする場合には、getters の名前を変えることになるかな、と。実際、Vuex のサンプルではそうなっています。

この store.js を活用するのが Project.vue です。

Project.vue

<template>
  <v-container>
      <h2>プロジェクト一覧</h2>

      <v-data-table
        :headers="headers"
        :items="items"
        :items-per-page="5"
        class="elevation-1"
      ></v-data-table>

  </v-container>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  name: 'Project',
  data: function() {
    return {
      headers: [
        { text: 'id', value: 'id' },
        { text: 'プロジェクト名', value: 'name' },
        { text: '詳細', value: 'description' },
      ],
    }
  },
  computed: {
    ...mapGetters({
      items: 'projects',
    })
  },
  mounted() {
      this.getProjects()
  },
  methods: {
    ...mapActions({
      getProjects: 'projects', 
    })
  }
}
</script>

テーブル表示に Vuetify の v-data-table を使っています。
getters と actions の dispatch は直接呼び出したくないので、mapGetters と mapActions でマッピングさせます。

store.js 内では projects として統一した名前にしていましたが、マッピングするときに利用しやすように名前を変えます。Web API を呼び出すだけであれば、Project.vue のコード内には this.$store は出てきません。

この画面の状態を data に持たせるのか、store.js のほうに書くのかは悩むところではありますが、MVVM パターンのように記述するのであれば、UI の状態も store.js の方に記述するのがベターでしょう。MVVM の View(Viewのバックエンドのコード)に状態を持たせてしまうと ViewModel との整合性があわなくなる、かつ再帰テストがやりづらくなるので、この手の状態値も ViewModel の方に押し込んでしまいます。

その形にならうならば、Vuex のほうも .vue のほう記述するよりも store.js に押し込んでしまったほうがよいでしょうね。
ただし、いくつか書いてみる判るのですが、View である .vue から store.js は比較的遠い位置にあります。上記の headers 配列やダイアログの表示フラグのような View のみに関するところは、.vue の data で十分なような気がします。いや、むしろ View のほうにいれておかないと、部品的なコンポーネントを作るたびに store.js を改修せねばならず、かなり面倒です。「コンポーネント」として分離されていません。

Vue の画面単位で Vuex のモジュール分けをしたほうがよいのか?

Microsoft 的な MVVM パターンの使い方で言えば、Vue のひとつのコンポーネントに対して Vuex のデータストアがあることになります。Vuex で言えば、モジュール分けになります。

Vuex でモジュール分けをすると名前空間で分けられるのはいいけど、相互に値をやり取りするたびに rootState が必要になって、これまたちょっと変な構造になります。
この部分は、もうちょっと考察が必要。

カテゴリー: 開発 | 具体的に Vuex と MVVM を比較する はコメントを受け付けていません

Flux を Blazor に活用してみる

.NETで作れるブラウザ上のシングルアプリケーション Blazor は、仮想 DOM を使っていて、内部的には(多分)Vue.js とよく似ています。

Blazor の内部動作は Razor なので(ASP.NET の Razor)、SPA内部でのデータ構造は自由になります。動作的には MVVM パターンな View との双方向バインドになっていますが、たまに StateHasChanged を呼び出して画面を更新しないといけないのが、いまいちイケていないところです。

通常の Blazor の場合

Web API を呼び出して、その結果を表示させる場合、Blazor ではこんな感じになります。

@page "/normal"
@inject HttpClient Http
<h3>Normal Sample</h3>

<button class="btn btn-primary" @onclick="save">Click me</button>
<div>
    @Result
</div>

@code {
    private string Result { get; set; }

    private async void save()
    {
        // Web API 呼び出し
        var url = "http://localhost:8000/api/project";
        var res = await Http.GetAsync(url);
        // 結果を mutations へ commit する
        this.Result = await res.Content.ReadAsStringAsync();
        this.StateHasChanged();
    }
}

Vue.js ならば data で result で保持するところを、Blazor ではプロパティ(実はフィールドでも良い)で持ちます。@code な部分は、Vue.js で言えば script なところです。Blazor では暗黙的に Blazor コンポーネントのクラスに割り当てられるので、このコードは、Normal クラスの内部メソッドやプロパティになります。

「Click me」なボタンを押したときに、save メソッドに割り当てる部分も、Vue.js と同じです。

Blazor の元ネタである Razor の場合(それ以前でいれば Web フォーム)も、こんな形でメソッドに割り当てることができます。Web フォームの場合は、JavaScript を駆使して該当するメソッド(実際はサーバーで動作する)に割り当てていたのですが、Blazor の場合はブラウザ上で動作する wasm 内で動くので完全にクライアントサイドで動作する save メソッドになります。

StateHasChanged メソッドは、仮想 DOM が更新されたのでレンダラーを動かす印なのですが、これが必要な場合と必要がない場合があります。大抵の場合は大丈夫なのですが、HttpClient で非同期に動いたときは必要っぽいです。

この部分は MVVM パターンで言えば OnPropertyChanged なのですが、Blazor の StateHasChanged はコンポーネント単位(この場合は、Normal.razor)への通知になるので、微妙なところです。たくさんのプロパティがあるときに、StateHasChanged により動作が遅くなるのかどうかは不明です。ちなみに、SVG でライフゲームを作ったとき 100 x 100 のプロパティを変更させていますが、それほど重くはありません。1万個のプロパティがあっても大丈夫なので、大抵の場合はスピード的に問題はないのでしょう。

動作させるとこんな感じになります。


Vuex っぽく書き替える

これを Vuex っぽく書き直してみます。Blazor 上のいくつかの Flux 実装をみると、dispatch や commit などの名前を使っていますが、名前自体に意味はない(アーキテクチャなので)ので C# のプロパティとメソッドを使って構築します。実は Vuex の書き方って Command パターンの悪癖の典型ような気がするので、これは型付の言語にはなじまないですよね。Blazor の場合は、コンパイルが間に挟まるので、プロパティ名やメソッド名にきちんと意味を持たせた方が、開発的に安全です。

namespace FluxBlazor
{
    public class Store
    {
        // state:
        private string _result = "";

        public string Result
        {
            // getters 
            get => _result;
            // mutations 
            set
            {
                _result = value;
                OnStateHasChanged?.Invoke();
            }
        }
        public event Action OnStateHasChanged;

        HttpClient Http = new HttpClient();
        // actions: 
        public Task Save()
        {
            var task = new Task(async () =>
            {
                // Web API 呼び出し
                var url = "http://localhost:8000/api/project";
                var res = await Http.GetAsync(url);
                // 結果を mutations へ commit する
                this.Result = await res.Content.ReadAsStringAsync();
            });
            task.Start();
            return task;
        }
    }
}

Store.cs という新しいクラスを作って、そこに Flux の各種の値とメソッドを書いていきます。

  • state は、外部から見えないようにするため、private で定義
  • getters は、プロパティの get を使う
  • mutations は、別のメソッドにしようと思ったけど、意味としてはプロパティと set と同じなので、ここで定義する
  • actions は非同期処理になるので、常に Task を返す。

Vue.js の場合は、mutate による state の変化が、そのまま仮想DOMに伝わります。
しかし、Blazor の場合は、いちいち仮想DOMを更新しないといけないので、OnStateHasChanged イベントを発生させて、その中で StateHasChanged を呼び出して貰います。StateHasChanged の処理自体は、コンポーネントに紐づいているので、現在描画しているコンポーネントの StateHasChanged メソッドを呼び出す必要があるのです。

これ App クラス内に作って呼び出せば大丈夫かと思ったけど、駄目でした。あくまで、コンポーネント単位で StateHasChanged を呼ばないと駄目みたい。

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    public static Store store { get; set; } = new Store();
}

Store の実体(オブジェクト)を何処におくか悩んだのですが、App.razor に書きます。Vuex の場合は、App.vue 内になるので、ちょうどそれと同じです。
Store オブジェクトは、どのコンポーネントからも参照ができるように static にしておきます。これで、App.store として参照できます。Vuex の場合は this.$store になるので、これも同じ感じです。

Flux.razor を新しく作って、Store クラスにある Web API を呼び出すようにします。

@page "/flux"

<h1>flux blazor sample</h1>

<button class="btn btn-primary" @onclick="save">Click me</button>
<div>
    @Result
</div>

@code {

    private string Result => App.store.Result;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        // 変更イベントを受け取る
        App.store.OnStateHasChanged += () => { this.StateHasChanged(); };
    }

    private void save()
    {
        Console.WriteLine("click save");
        // dispatch 
        App.store.Save();
    }
}

Web API の結果を表示する Result プロパティは、App.store.Result 自身を返します。これは、ちょうど Vue.js で算出プロパティを使って Vuex の getters を呼び出すところと同じです。

画面の初期化時には、OnStateHasChanged イベント内で StateHasChanged メソッドを呼び出すようにします。これで、Store 内の mutations の更新で自動で画面が更新されます。

ボタンを押したときの save メソッドでは、Store クラスの Save メソッドを呼び出します。これは Vuex の dispatch にあたる処理です。

これを実行してみると、Normal.razor と同じ動作になります。


まるで MVVM パターンのようだ

実際に書いてみるとわかりますが、実質 MVVM パターンと変わりません。Flux の場合は、state に対する読み取り(getters)と書き込み(mutations)を分けていますが、C# の場合はプロパティの get/set で十分です。引数がある場合は、別途メソッドを作ることになるでしょうが、値の出し入れだけなればプロパティで十分です。

MVVM と Flux/Vuex の大きな違いは、actions のところでしょう。
最初のノーマルな Blazor のコードの場合は、Web API 呼び出しの後に await を使ってレスポンスを取得、そして取得結果を表示しています。
Flux 風の場合は、非同期な save メソッドを用意しておいて、そこで Task オブジェクトを作って呼び出しています。Flux 風の場合は、Save メソッドを呼び出した直後では Reuslt の値は変更されません。あくまで、Save メソッドは Web API を呼び出すだけで、結果待ちはしていないのです。これは複数の Web API を actions 内に記述したときも同じです。このため Web API のレスポンスを受けて Result プロパティが変更されるタイミングを知るためには、通常の MVVM パターンとは違った方法が必要です。Save メソッドの戻り値ではないので、Vuex の then にあたる ContinueWith でメソッドチェーンをするか、await で同期待ちをします。

Blazor の場合、まだ始まったばかりなので、データの保持が MVVM がよいのか Flux がよいのか判然としませんが、Blazor のコンポーネント単位で ViewModel を持たせるよりも、App クラスに一括して持って持っておいたほうが画面遷移があるときは便利かもしれません。デスクトップアプリの WPF や UWP の場合でもたびたび画面遷移で ViewModel をどう引き継ぐのかというのが問題になるので、似たような議論は Blazor 界隈でおこるでしょう。

ちなみに、デスクトップアプリの ViewModel の取り回しは、子の ViewModel を作るときに、親の ViewModel を引き渡してしまうのが簡単です。

ViewModel _vm ;

var v = new ChildView( _vm );
v.ShowDialog();

こんな感じで、子ウィンドウを開くときに、自分の ViewModel を渡してしまいます。このようにすると、子のダイアログ内で親の変数を扱うときにも便利なので(親画面で表示しているリストの変更やラベルの変更など)、定型的に使うとよいです。

カテゴリー: 開発 | Flux を Blazor に活用してみる はコメントを受け付けていません

MVVM パターンと Vuex を比較して理解する

とあるところで、Laravel + Vue.js で社内ツールを作ることになりました…と仮定しましょう。自分としては WPF とかで作ったほうがノウハウがあるので手軽に作れるのですが、先方の要望といういうか、既にあるツールが Vue.js の electron で作ってあるのと、サーバーサイドが Larevel で Web API で作ってあるのでそれに準じましょうという具合です。

ちなみに、私としては Vue.js は半年前に始めたばかりで、それも Laravel の View 上に Vue.js のスクリプトを読み込ませる方式で作っていました。なので、プログラミングの作法としては、Laravel 寄り(PHP寄り)になるので、いまいち Vue.js は判らないのです。JavaScript が苦手というのもありますが。

漠然と Vue.js オンリーで作っている場合は、適当な data を作ってため込むという VB や Windows フォームな作りで十分な訳ですが、ある程度の規模が大きくなると(どの位が「ある程度」というのか疑問はありますが、10画面ぐらいあれば「ある程度」と言えるでしょう)、なんらかの形でデータ層を用意しておいたほうがベターです。

そのような場合、Windows のデスクトップならば WPF で作る、Android や iOS アプリならば Xamarin.Forms を使うという形で MVVM パターンを使うのが常です。View 側に XAML を使って、ロジックに ViewModel を使う。データベースのアクセスなどに Model クラスや Entity Framework を使えばよいので、(自分の中では)かなりノウハウが蓄積しています。Prism を使うという手もあるのですが、私の場合それほど複雑な形にしたくないので毎度 INotifyPropertyChanged を継承したクラスを自作(実際には使いまわし)しています。ICommand を使わないからこれで十分なのです。

MVVM パターンとは何か?

WPF や Xmarin.Forms や、今や瀕死状態の UWP(何処にいちゃったんでしょうね?.NET5 の UI ラインナップにも無かった気が…)でおなじみの MVVM パターンです。実は、Laravel や CakePHP や、以前の mac のように MVC パターンも一緒に覚えておくといいです。Objective-C で mac アプリを使うときは MVC パターンだし。でも今は、Swift か。

画面の View は、大抵の場合は画面専用のマークアップ言語で書きます。WPF であれば XAML、XAML 以外に MVVM パターンの View があるのか?と言えば、探せばあるんじゃないだろうか?な感じですが、MVVM パターンとしては、View 自体は XAML に限るという訳ではありません。ただ、現状の実装としては XAML のようなスタイルにしないと(INotifyPropertyChangedの連携があるので)、うまく実装できないというところです。

Model は、いわゆる値クラスでもあり、データベースから取ってきた Entitiy Framework でもあり、データを保持するところです。

この View と Model の間を取り持つのが ViewModel クラスな訳ですが、一般的な Microsoft 社のサンプルコードだと、ひとつの View に対してひとつの ViewModel が律儀に割り付けられていますが、そうしないといけない訳ではありません。見通し的には、1対1対応のほうが判りやすいという面もあるのですが、共通なデータを持たせる場合(特に画面間でのデータが共通である場合)View – ViewModel の対応が冗長になってしまうので、ひとつの ViewModel に複数の View を持たせるという、Document-View 方式でも良いわけです。

MVVM パターンも MVC パターンも使われ始めた頃(20年前位?)はいろいろと学習しないと訳が分からない感じでしたが(オブジェクト指向とかオブジェクト脳とかUMLとか諸々の波もあったので)、いまとなっては一般的なアプリケーションの View と Model をうまく分離&組み合わえるパターンのひとつです。私的には「MV* パターン」とまとめて呼ぶこともあります。

Vuex とは何か?

Vue.js でコンポーネントを作るときに v-model というのがあるので、MVVM パターンでええやん!と思うわけですが、Vuex というものを使います。正確には Flux アーキテクチャを使って Vue.js に作られた Vuex を使います。

データの取り回しとか、Vue.js であれこれやるのに精いっぱいなのに(初学者的には!)、更に Vuex の方式を覚えないといけないという混乱状態です。実は Vue Router も思えないといけないのと、Vuetify も使わないといけない。

自分としてはできるだけ既知のもの、あるいは「これはいけそう」と思えるものを使いたいので、既に MVC パターンや MVVM パターンがあるところに Vuex という発想は必要なのか?という疑問がでてきます。

Vuex ではおなじみのこのぐるぐるの図ですが、遷移が一方向にしか回らないので理解が簡単!ということなのですが、本当なのでしょうか? MVVM パターンのほうが相互にやり取りする(two way方式)ので、簡単ではないでしょうか?

それにいちいち、dispatch や commit を呼び出すというのはどういうことなのか?メソッド呼び出しでいいんじゃないだろうか?という不信が湧き出るわけです(今でも、ちょっと湧き出るけど)。

でもって、google で「Vuex 使わない」で検索すると、ぽろぽろと出てきますね。疑似的に MVVM パターに則ったライブラリを自作してもよいのですが、Vuex は公式から出ているということで、それなりの理由があるのでしょう。では「それなりの理由」とは何ぞや?というところから、理解したいし、それをした上で使うのか使わないのかを判断したいわけです。

シーケンス図で理解する Vuex

実は、さっきのぐるぐるの図には、ひとつ大きな見落としがあります。
主に Actions からの呼び出しは Web API に対して非同期に行われています。

デスクトップアプリや、サーバーサイドでのデータベースアクセスは、基本は同期的に行われています。現在の C# だとファイルアクセスするときに async/await を使い「非同期」として呼び出すことができますが、本質的に await で処理待ちをしていれば同期的に操作をしているし、通常の場合は印刷や重たい画像処理以外は同期的に書いていてもアプリケーションのレスポンスはCPUのスピードに関係するところが多いので、それほど問題にはなりません。

しかし、Vue.js のように、基本何かのデータを保存したり読み込んだりするのに常に Web APIを呼び出す必要があるときには、何らかの処理のほとんどが非同期的におこなわれます。実装としては、axios クラスを使って処理待ちの then を使うわけです。

なので、コンポーネント(View)でユーザーがボタンクリックなどのアクションを起した後に、Web APIを呼び出して、応答されたデータがリストに表示されるまでにはそれなりの時間が掛かり、この部分はCPU的には連続ではないのです。

Vuex の解説で使われるステートチャート(遷移図)だけでは、時系列が判りづらいので、具体的にシーケンス図で書いたのがこれです。

画面を表示したときの mounted のときに、Web API の api/Rooms を呼び出して、画面に会議室の一覧を表示する画面を想定したものです。

  1. コンポーネントから dispatch する
  2. actions から Web API を axios で呼び出す。
  3. レスポンスは、非同期になり Promise を使う。
  4. ここでいったん画面に制御が戻る
  5. レスポンスが返ってきたときに、結果を commit する
  6. 書き込み専用の mutations で state に書き込む
  7. 読み込み専用の getters から stete の変更がコンポーネントに通知される
  8. コンポーネント側の rooms が更新されて、画面にリストが表示される。

という具合です。なるほど、dispatch が必ず Promise を返す=非同期である、というのがここに意味があります。内部で、Web API を呼び出さない同期的な処理であっても、Promise を返すので、画面のコンポーネント側では常に「非同期呼び出し」として処理が統一できます。

つまり、内部動作が非同期処理か同期処理かを区別せずに、(乱暴ですが)すべて非同期処理とみなす、という方法を Vuex は取っています。

同じものを MVVM パターンで書いてみましょう。画面を表示させたときに、Web API を呼び出してリストの初期状態を取得する、というパターンはよくあります。

View がロードしたときに ViewModel の呼び出し自体は同期的に行われます。しかし、ViewModel 内では、Web API を呼び出す(HttpClientなど)ので、これの戻り値の取得は非同期になります。
実際は Task オブジェクトが返されるのですが、コード上では実行待ちのように記述したいので await を使います。

Web API からレスポンスが返ってきたときは、Model クラスに同期的に書き込みが行われます。Model クラスの変更に伴い、ViewModel が View に対して OnProertyChange イベントを発生させます。このイベントを受けて View の該当箇所が更新されます。

シーケンス図を書く Vuex と MVVM パターンの類似点がよくわかります。
少なくとも「非同期で Web API を呼び出す必要がある」場合には、Vuex も MVVM もまったく同じシーケンス図になります。

違いとしては、

  • Vuex の場合は、Web API が頻繁に呼び出されるので、非同期処理が前提になっている
  • MVVM の場合は、Model の変更を View に伝える、同期的な通知が主になっている

ということでしょう。

Vuex で複数の Web API を呼び出す

View 側のアクションで、複数の Web API を呼び出すパターンを考えてみましょう。さきの Vuex と MVVM パターンの類似点に注目すれば、非同期的に Web API を呼び出す場所は、Actions の中になります。

この例では、api/Rooms と api/Price の2つの Web API を呼び出しています。画面では rooms と prices を独立で表示させているわけですが、画面からは1回の GetRooms を dispatch で呼び出します。その中の Actions の中で2回の Web APIが呼び出されるわけです。

View 側の次のアクションとしては、2つの Web APIが戻ってきた後(あるいは、呼び出している途中)に何かをやりたいわけですから、Actions の中で axios の await か then を使ったほうが都合がよいわけです。

もうひとつ、データを更新した上で、もう一度リストを更新させるという update と getlist の2つを順番で呼び出すパターンを考えてみましょう。

順序的には、Web API で update の API を呼び出した後に、リストの更新をしないと駄目です。何らかの原因でupdate に失敗したときはリストを更新しても意味は無いし、update が完全に終わるまでリストを取得しても意味ありません。サーバー側の update 処理完了まえにリストの取得をしても意味がありませんからね。

本来ならば、サーバー側でトランザクションを取る方法も多いのですが、呼び出し側で順番を守って Web API を呼び出しても ok な訳です。この「順番を守って呼び出す」部分を Actions 側に記述しておけば、View 側の手違いがなくなります。

Vuex はルールを守って使う

MVVMer な私としては、Vuex は「ルールを守って使えば ok」という結論です。MVVM パターンであっても、ViewModel 内で頻繁に HttpClient を非同期で呼び出すならば、同じようなシーケンスになるのならば、Vuex のスタイルに合わせたほうが無難です。

actions: 
  store.commit('project/projects', items )

getters:
  store.getters['project/projects']

dispatch:
  store.dispatch('project/projects')

mutatins:
  projects( state, payload ) {
      state.projects = payload.projects
  }

state: 
  projects: []

ただ、なんといいますか、commit や dispatch を呼び出すときに、メソッド名(タイプとも言う)を文字列になっているのはなんとかならなかったんでしょうか。ここは慣れそうにないですが、まあ、適度に map を使うということで。

カテゴリー: 開発 | MVVM パターンと Vuex を比較して理解する はコメントを受け付けていません

接触確認アプリ(COCOA)の実機デバッグ環境を整えようとするができなかった話

最初に書いておきますが、iPhoneでの接触確認アプリの実機環境は整えることができません。もともと暴露通知(Exposure Notification)が、ひとつの国でひとつのアプリでしか使えないという制限のためという理由もあるのですが、Exposure Notification を有効にしたまま実機(iPhone)にインストールすることができません。

目的としては、

  • 暴露通知(Exposure Notification)の動作を把握する
  • COCOA の不具合っぽいものを確認しておく(直すことはできないので、確認のみ)

とするので、内部的なところには踏み込まないようにします。

ソースコードをダウンロードする

cocoa の元ネタのソースコードを github からダウンロードします。

現在の cocoa は ver.1.1.2 なのですが、ここにあるソースコードは 1.1.1 のままになっています。このため、実際の cocoa とは違うので「動作チェック」という訳にはいかないのですが、Exposure Notification の仕組みを知るには良い材料でしょう。

プログラムが Xamarin.Forms を使っているので、Visual Studio 2019 と Visual Studio for Mac を使います。

Visual Studio 2019 のほうは、Windows 上で Android アプリのビルドができます。iPhone アプリの開発は Windows から mac を通しても可能なのですが、mac のほうに Visual Studio for Mac を入れたほうが便利です。

ソースコードを開く

Android と iOS のアプリは、Covid19Radar.sln を Visual Studio で開きます。

  • Covid19Radar.sln
  • Covid19Radar.Functions.sln

Covid19Radar.Functions.sln のほうは、サーバー側のプログラムです。きちんと設定してやれば、ローカルの Azure エミュレータと Cosmos DB を使ってサーバーサイドも構築することも可能なのですが、これはまだ試していません。

  • Covid19Radar
  • Covid19Radar.Android
  • Covid19Radar.iOS

という3つのプロジェクトがあります。Xamarin.Forms で Android/iOS のアプリを作るとき、共通コードとなる Covid19Radar プロジェクトがあって、それぞれ機種特有のコードを Covid19Radar.Android や Covid19Radar.iOS に書くことになっています。

肝心の Exposure Notification のコードは、内部的に Google(Android) と Apple(iOS) では別々のコードになるところなのですが、XamarinComponents/XPlat/ExposureNotification at master · xamarin/XamarinComponents という形で、Android と iOS の動作をひとつにまとめて扱えるようになっています。

NuGet の場合は、以下からダウンロードができます。

setting.json を書き替える

Exposure Notification の仕組みで優れているところは、

  • 自分が陽性と解ったときだけ、サーバーに TEK(Temporary Exposure Key)を送る
  • 大勢の人は、定期的に陽性者の TEK をダウンロードして、自分のスマホ内で照合する

ところです。個人データと思われる TEK の情報を、「陽性情報の登録」のときのみサーバーにアップロードします。HER-SYS 番号との連係はさておき、自発的に「陽性情報の登録」をしたときだけしか、サーバーにアクセスしにいかないので、個人データの漏洩リスクが減ります。

もうひとつ、定期的にサーバーから TEK をダウンロードしますが、この中にある RPI(Rolling Proximity Identifier)だけでは、個人を特定しにくいです。実は、全くできないわけではなくて、別途 Beacon を使って接触通知アプリを使って広域的に探っていけばできないこともないのですが、現在のところ難しい、というところです。

Covid19Radar プロジェクトの中に、setting.json というファイルがあります。

これを開くといくつかの設定があります。実際の cocoa では、APP_VERSION などが設定されているはずです。

{
  "appVersion": "APP_VERSION",
  "apiSecret": "API_SECRET",
  "apiUrlBase": "https://API_URL_BASE/api",
  "supportedRegions": "440",
  "blobStorageContainerName": "c19r",
  "androidSafetyNetApiKey": "ANDROID_SAFETYNETKEY",
  "cdnUrlBase": "https://CDN_URL_BASE/",
  "licenseUrl": "https://covid19radarjpnprod.z11.web.core.windows.net/license.html",
  "appStoreUrl": "https://itunes.apple.com/jp/app/id1516764458?mt=8",
  "googlePlayUrl": "https://play.google.com/store/apps/details?id=jp.go.mhlw.covid19radar",
  "supportEmail": "SUPPORT_EMAIL"
}

apiUrlBase は、「陽性情報の登録」をしたときの呼び出し先で、Azure Functions が使われています。このアドレスと秘密キーは本番の cocoa だけのもなので、このままにしておきます。逆に言えば、、間違って「陽性情報の登録」を押しても、勝手にサーバーに接続できないということですね。

陽性者の TEK をダウンロードするための URL は、cdnUrlBase となるので、ここは書き替えます。

  "cdnUrlBase": "https://covid19radar-jpn-prod.azureedge.net/",

デバッグページ(DebugPage)を表示させる

もうひとつ、動作確認用のデバッグページを表示できるようにします。

ViewModels/MenuPageViewModel.cs を開いて、以下のコードを追加しておきます。

#if DEBUG
            MenuItems.Add(new MainMenuModel()
            {
                Icon = "\uf0c0",
                PageName = nameof(DebugPage),
                Title = nameof(DebugPage)
            });
#endif

Debug_Mock と Debug の違い

プロジェクトをビルドする前に、Debug_Mock と Debug を確認しておきます。

  • Debug は、Exposure Notification API を使ったデバッグモード
  • Debug_Mock は、Exposure Notification API を使わずにエミュレート

の違いがあります。

Nearby.EXPOSURE_NOTIFICATION_API エラーが出る

EN api を使ったまま、実機 Android にインストール(Visual Studio からのデバッグ実行)はできるのですが、何かのタイミングで、次のようなエラーが出ます。

**Android.Gms.Common.Apis.ApiException:** '17: API: Nearby.EXPOSURE_NOTIFICATION_API is not available on this device. Connection failed with: ConnectionResult{statusCode=UNKNOWN_ERROR_CODE(39507), resolution=null, message=null}'

前後のログ出力はこんな感じ。

08-22 09:26:56.703 I/MonoDroid(23458): UNHANDLED EXCEPTION:
08-22 09:26:56.713 I/MonoDroid(23458): Android.Gms.Common.Apis.ApiException: 17: API: Nearby.EXPOSURE_NOTIFICATION_API is not available on this device. Connection failed with: ConnectionResult{statusCode=UNKNOWN_ERROR_CODE(39507), resolution=null, message=null}
08-22 09:26:56.713 I/MonoDroid(23458):   at Android.Gms.Nearby.ExposureNotification.IExposureNotificationClient.IsEnabledAsync () [0x00067] in <a915b6bc332b428d88e3e93dc94335a6>:0 
08-22 09:26:56.713 I/MonoDroid(23458):   at Xamarin.ExposureNotifications.ExposureNotification.PlatformGetStatusAsync () [0x00074] in <e42d902759884cb2b9bcb6b7e1a5859c>:0 
08-22 09:26:56.713 I/MonoDroid(23458):   at Covid19Radar.Services.ExposureNotificationService.UpdateStatusMessageAsync () [0x00025] in D:\git\cocoa\Covid19Radar\Covid19Radar\Covid19Radar\Services\ExposureNotificationService.cs:88 
08-22 09:26:56.713 I/MonoDroid(23458):   at Covid19Radar.Services.ExposureNotificationService.OnUserDataChanged (System.Object sender, Covid19Radar.Model.UserDataModel userData) [0x00057] in D:\git\cocoa\Covid19Radar\Covid19Radar\Covid19Radar\Services\ExposureNotificationService.cs:73 
08-22 09:26:56.713 I/MonoDroid(23458):   at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__7_0 (System.Object state) [0x00000] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/AsyncMethodBuilder.cs:1021 
08-22 09:26:56.714 I/MonoDroid(23458):   at Android.App.SyncContext+<>c__DisplayClass2_0.<Post>b__0 () [0x00000] in <eaa205f580954a64824b74a79fa87c62>:0 
08-22 09:26:56.714 I/MonoDroid(23458):   at Java.Lang.Thread+RunnableImplementor.Run () [0x00008] in <eaa205f580954a64824b74a79fa87c62>:0 
08-22 09:26:56.714 I/MonoDroid(23458):   at Java.Lang.IRunnableInvoker.n_Run (System.IntPtr jnienv, System.IntPtr native__this) [0x00008] in <eaa205f580954a64824b74a79fa87c62>:0 
08-22 09:26:56.714 I/MonoDroid(23458):   at (wrapper dynamic-method) Android.Runtime.DynamicMethodNameCounter.1(intptr,intptr)
08-22 09:26:56.714 I/MonoDroid(23458):   --- End of managed Android.Gms.Common.Apis.ApiException stack trace ---
08-22 09:26:56.714 I/MonoDroid(23458): com.google.android.gms.common.api.ApiException: 17: API: Nearby.EXPOSURE_NOTIFICATION_API is not available on this device. Connection failed with: ConnectionResult{statusCode=UNKNOWN_ERROR_CODE(39507), resolution=null, message=null}
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.internal.ApiExceptionUtil.fromStatus(com.google.android.gms:play-services-base@@17.1.0:4)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.api.internal.ApiExceptionMapper.getException(com.google.android.gms:play-services-base@@17.1.0:2)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.api.internal.zaf.zaa(com.google.android.gms:play-services-base@@17.1.0:15)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.zac(com.google.android.gms:play-services-base@@17.1.0:175)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.onConnectionFailed(com.google.android.gms:play-services-base@@17.1.0:95)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.internal.zag.onConnectionFailed(com.google.android.gms:play-services-base@@17.1.0:2)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.internal.BaseGmsClient$zzf.zza(com.google.android.gms:play-services-basement@@17.2.1:6)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.internal.BaseGmsClient$zza.zza(com.google.android.gms:play-services-basement@@17.2.1:25)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.internal.BaseGmsClient$zzc.zzo(com.google.android.gms:play-services-basement@@17.2.1:11)
08-22 09:26:56.714 I/MonoDroid(23458): 	at com.google.android.gms.common.internal.BaseGmsClient$zzb.handleMessage(com.google.android.gms:play-services-basement@@17.2.1:49)
08-22 09:26:56.714 I/MonoDroid(23458): 	at android.os.Handler.dispatchMessage(Handler.java:107)

要は、EN api を利用するときには、アプリが正式に Google Play に登録しておく必要があり、アプリが EN api を使うときに Google Play Service が呼び出しのチェックをしているそうです。

このあたりのデバッグのしにくさは、ドイツ版の接触確認アプリ(corona-warn-app)や Google の exposure-notifications-android でも話題になっています。

ちなみに、corona-warn-app のほうでは、Frida を使った Google Play Service の偽装の方法が提案されているので、これだと上手くいくかもしれません(ちょっと試してない)。

Google Play 開発者サービスでガードが掛かる

つまり、Android 実機の場合は、

  • EN api を有効したままインストールはできるが、実行時に EN api を呼び出したときに Google Play Service でガードがかかる。

という訳で、UI 周りのチェックをしたいときは Debug_Mock で動かし、EN api の挙動をを調べる場合は今のところ手がないというパターンです。

iOS の場合は、インストール時にガードが掛かる

iPhone 版の場合は、Entitlements.plist に EN api の記述があります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.developer.exposure-notification</key>
	<true/>
	<key>aps-environment</key>
	<string>development</string>
</dict>
</plist>

この「com.apple.developer.exposure-notification」の値が true になることで、iPhone で EN api を利用できるようになります。

ただし、ビルドして実機に iPhone にインストールするときにプロビジョニングでこの値をチェックするのですが、はねられてしまいます。この値を有効にするには、あらかじめ申請が必要で
Exposure Notification Entitlement Request – Contact Us – Apple Developer で送ります。これはい各国の保健省(日本では厚生労働省)の認可が必要なので、日本の場合は cocoa の開発サイドのみになります。

カテゴリー: 開発 | 接触確認アプリ(COCOA)の実機デバッグ環境を整えようとするができなかった話 はコメントを受け付けていません

PCR検査の偽陽性問題を具体値で考える

PCR検査の拡充に対して偽陽性の問題がたびたび出てくる。8月現在で陽性率の高さもあって、再びPCR検査を増やすあるいは増やさないの意見が飛び交うわけだが、具体的に既にPCR検査を行っている国の場合、どの程度の「偽陽性」を許容しているのかを計算してみることにしよう。

感度と特異度

まずは用語を明確にしておく。

検査/東京大学 保健センター

ベイズだと感度も特異度も同等に扱うのだけど、疫学では別々に扱う。

  • 感度が、検査によって真の陽性者が陽性と判明する確率
  • 特異度が、検査によって真の陰性者が陰性と判明する確率

を表す。東大保健センターの図をそのまま借用すると以下のようになる。

ここで問題になるのは、

  • 検査によって真の陽性者ではあるが陰性として判別される「偽陰性」
  • 検査によって真の陰性者ではあるが陽性として判断される「偽陽性」

の2種類がある。この数値が増加することによる問題はそれぞれ別になる。

  • 偽陰性が増加すると、市中に陰性と思っている陽性患者を放つことになる → 二次感染の増加
  • 偽陽性が増加すると、陽性と思っている陰性者(正常者)が病院に収容することになり病床を圧迫する → 医療崩壊の可能性

「陽性的中率」は 70/(70+9) = 検出した真の陽性者数 / 検査での陽性者数 となる。陽性的中率が悪ければ、罹患していない人を患者として病院に収容することになる。これも病床が埋まり、医療崩壊の原因となる。

新型コロナウィルスのPCR検査では、どの程度の感度と特異度なのかが問題なのであるが、医療機関の解説を見ると、以下のようになる。

  • 感度 70%
  • 特異度 99%

特異度が極めて高いのは、偽陽性を多くして余計な治療を行わないように注意するためだ。通常の医療であっても、薬の投与には副作用があり健康体に害が多い。ゆえに、無闇に薬を出さないようにするため(正常体を間違って副作用で病気にさせないため)に、特異度を極めて高く=偽陽性を極めて低くなるように慎重に調査する。

感度が特異度よりも低いのは、病気の疑いがある場合も患者を拾うためだ(と思う)。PCR検査の場合は、ウィルスの数を敷居とするため、無闇に高い閾値にする≒感度を上げると本来のウィルスで病気になる人を見落としてしまう恐れがある。それよりも、まずは閾値を少し下げて≒感度を下げて病気の疑いがあることを示す。それから再検査という手順になる。よくある人間ドックの精密検査がその方式で、人間ドックでは大まかな病気疑いを血液検査やバリウム検査などで異常値を取得する。このときに、ちょっと疑いありの状態では精密検査として後日再調査をする。
ときには、精密検査をしても何も出ずに健康体であったことがわかる。しかし、何分の一の確率で実際に病気であることがわかる。このとき、最初の人間ドックの検査が無駄であったという訳ではなく、精密検査をするためのよい閾値となっていることがわかる。
この方式を「スクリーニング」と呼ぶ。

スクリーニングの手法は、医療関係だけでなくテスト手法や製造工程の以上検出などでよく使われる。製造工程が異常であること(不良品を出さないという意味で)を素早く検知したいが、無闇に工程を止めるのも時間と資金の無駄であるのでできるだけ工程を止めたくないというジレンマがある。検出器のスピードや精度にも関わってくる。この場合、不良品検出のための閾値を低めに設定しておき、不良品疑いのものをもう一度検品するという方法をとる。最初の不良品検出はざっと行うスクリーニングになる。苦情受付の電話窓口と裏に控えている部長の関係もスクリーニングになる。

このように不良であること、この場合は陽性として検出して患者として治療にあたりたいという目的に沿う場合、二段階の検査が有効に働く。

日本のPCR検査の場合、CTスキャンによる検査や医者の問診が良いスクリーニングになっている。CTスキャンによる肺炎疑いや、専門医よる病状の早く、あるいは濃厚接触者というスクリーニングを経て、PCR検査という手順を踏む。当然、PCR検査自体もできるだけ陽性患者を拾う(隔離という意味でも)たい意図があるので、感度はあまり上げていないのだと思われる。
この感度が90%だったり、特異度を99%近くに上げることが妥当かどうかはよくわからない。ただ、ウィルスの数を閾値としているのだから、ある程度の幅を持たせないと意味がない。少しのウィルスでも病気になるのか、それとも大量のウィルスでないと病気にならないのかという違いがある。単純に言えば、1個だけの新型コロナウィルスが肺に入っても病気にはならないだろう。1個だけなのに急激に増殖するとは考えにくい。ならば10個ならばどうか、100個ならばどうか、という形で閾値を決めることになる。そこには確率的に罹患するかどうかの境目があるので、明確な数があるわけではない。

これにより、感度と特異度は、凡その値として以下を使ってもよいのだろうと思う。

  • 感度 70%
  • 特異度 99%

このスクリーニングが結構重要な役割をしていることが解る。ベイズで言えばこれが「事前条件」となる。スクリーニングがあるなしでは、この事前条件が異なるため、その後の確率の見方が異なる。

一方で陽性率の計算として

  • 陽性率 = 陽性者数 / PCR検査数

がよく使われる。陽性率が高いと危険でPCR検査の数が足りていない、というのがPCR検査を多くする場合の理由の一つとなるのだが、

  • スクリーニングの精度が良く、ほどよく感度を上げている場合には、陽性率は高くなる
  • スクリーニングの精度が悪く、患者疑いの人を大量に組み上げている場合には、陽性率は低くなる

という特徴がある。例としては、前者がCTスキャン等でスクリーニングを行った場合、後者が自己診断によるPCR検査の希望、という形になる。
ただし、「スクリーニングの精度」にも問題があり、例えば「確実に、新型コロナウィルスに罹った患者しか通さないスクリーニング」を施した場合でも、陽性率は高くなる。4月頃に言われていた、37度が4日間継続というのはそれに近いところがある。
故に、陽性率の高い低いだけでは、PCR検査の数が妥当であるかどうかは解らないということが、解るのである。

ニューヨーク市の偽陽性を推測する

さて、ほどんどCTスキャン検査をしていない(と思う)状態で、PCR検査を最大化している(と思われる)ニューヨーク市の状態で偽陽性の数を計算してみよう。


ニューヨーク州の人口は1945万人で、抗体検査によりニューヨーク市は12%程度は既に感染済みであることがわかっている。本来は市のほうで計算したかったのだが、検査済み検査数が州のほうにしかないので「ニューヨーク州」で計算する。

8/1 時点

  • 感染者数 754 人
  • 実施済み検査数 82,737 人

ほんとうは実際に検査した中から感染者数を割り出さないといけないのだが、今回は概算なのでこの数値を使う。

同様に東京都の8/1では

  • 感染者数 472 人
  • 検査数 4324 人

となっている。

これを先の表に埋め込む

  • 検査で陽性の計に「感染者数」を入れる
  • 検査で陰性の計に「検査数 – 感染者数」を入れる。
  • 合計値が「検査数」になる。

このとき、真の罹患している/罹患していない人数を計算したい。
ここで、初期値として、検査が100%正しいと仮定して、

  • 罹患している人 = 感染者数
  • 罹患していない人 = 検査数 – 感染者数

を入れる。当然のことながら、合計値は同じになる。

ここで、検査の仮定である「感度」と「特異度」を入れる。

  • 感度 70%
  • 特異度 99%

罹患している人の数から、感度の割合で、検査の陽性/陰性を割り振る。
罹患していない人の数から、特異度の割合で、検査の陽性/陰性を割り振る。

検査の陽性の合計を、「推測」の列に入れる。
同じように検査の陰性を「推測」の列に入れる。

ここで、罹患している/していない人数が、正しいければ、計の列と推測の列は一致するはずなのだが、当然のことながら一致しない。これは、感度と特異度があるから、当然のことなのだ。検査は実際の真の値とはズレる。

これを違いとして列をつくっておく。

さて、初期値である罹患している人数が違っていることが分かったので、これを真の値に近づけてみよう。
本来ならば、最小二乗法などを使いながら近づけるのだけど、ぽちぽちと「違い」を見ながら値を近づけていくだけでも可能だ。

東京都の場合は、最初の472から620まで増やしていくと「違い」の列がほぼゼロになることがわかる。つまり、この検査では、

  • 偽陰性は186人ほど取りこぼし、
  • 偽陽性で37人ほど間違って隔離している

ことがわかる。

同じことをニューヨーク州で実験してみよう。
東京都の場合と違い、罹患している患者(オレンジのセル)を少しずつ下げていくと違いが減少していくことがわかる。この違いが「0」になれば、真の罹患している患者数がわかるはずだ。
しかし、ニューヨーク州の場合は、罹患している人数を 10 人にしても、違いが0にならない。
これはどういうことだろうか?

  • PCR検査の感度や特異度の仮定が間違っているため、計算があわない。
  • PCR検査が多すぎるため、偽陽性多すぎて、正しく検査できていない。

のどちらかだろう。

このシミュレーションでは、ニューヨーク州でのPCR検査では、罹患している人を正しく検出できていないことが判明する(感度や特異度の仮定が正しければ)。つまり、感度が70%、特異度が99%程度では、真の罹患している患者が0人であっても、検査で陽性となる患者が700人以上で出てしまうということだ。

特異度を 99.9% と仮定する

実際の特異度はもっと高くて、ほぼ100%に近いという記事もある。

RIETI – PCR検査体制の拡充と偽陽性の問題

では、特異度を10倍精度良くして、99.9%と99.99%で考えてみよう。

この位の特異度になると、PCR検査は有効に働く。

  • 特異度が 99.9% の場合は、真の罹患者が 970 人程度
  • 特異度が 99.99% の場合は、真の罹患者が 1070 人程度

と推測される。つまり、偽陽性が極端に少ない状態=特異度の精度が高い状態の場合には、大量なPCR検査は有効に働くということだ。

所感

実際の感度がどの位なのかは解らないが、特異度が極めて高い(偽陽性が限りなく低い)状態でないと、大量PCR検査は有効に働かない。
また、感度の関係から、検査をしても一定量の真の感染患者を取りこぼしてしまう。取りこぼしをどうするのかという問題が残ってしまう。

対策としては、「検査で陰性であっても、偽陰性を疑い一定期間隔離する」ことになるのだが、この表に出てくる「検査で陰性」かつ「罹患している」患者というのは具体的に知ることはできない。「検査で陰性」の人数に含まれてしまっている。このため、偽陰性を疑い、すべての人を隔離してしまうとそれだけで病床が埋まってしまい医療崩壊を起してしまう。難しいところだ。

このため、偽陰性自体を減らす方法として、

  • 検査の総数自体を減らして、偽陽性となる人数を減らす

ことになるので、「検査の総数」を減らすために事前のスクリーニング(事前条件となるCTスキャンや医師による判断、濃厚接触者の判断)が必要になると思われる。このあたりのシミュレーションは別途行いたい。

参考文献

カテゴリー: 開発 | PCR検査の偽陽性問題を具体値で考える はコメントを受け付けていません

隠れ SIR モデルによる感染予測の解説

日頃ツイッターで更新しているこの図の説明がまとまっていなかったので、記録しておきます。

過去の値から感染者を推測する

発想元は西浦教授のこの論文 感染症流行の予測:感染症数理モデルにおける定量的課題 からです。現在分かっている新規感染者の数から、過去に遡って感染者数と感染率を推測し、再び現在と未来の感染者数を予測するという方法です。正確な計算では、過去の感染率(あるいはRt値)は分布を持つので、未来の推測も幅を持った分布になるわけですが、そこまで計算するのは大変なので、簡略的にExcelで可能なように書き直します。

計算方法

  1. 新規感染者dI_nを7日間(潜伏+発症期間)遡って、隠れ新規dI’_n-7と定義する
  2. 隠れ患者I’nを続く7日間の合計値と定義する。
  3. 隠れ新規dI’n/隠れ患者I’nを隠れ感染率rと定義する。
  4. 隠れ実効再生産数Rt’を隠れ患者と続く7日間の隠れ新規の合計値の比と定義する。

なお、潜伏+発症期間を7日間とする理由は、現在の新型コロナウィルスでは感染期間が6.7日間と発表されていることが理由です。

特別な行動変容(都市封鎖や自粛要請など)がない限り、感染率や実効再生産数は変わらないと考えられる。この仮定から、過去7日間の隠れ感染率rをその前日の感染率とほぼ同じと推測できる。

これにより、過去7日間の隠れ新規dI’nが推測できる。
隠れ新規dI’nは7日後に新規患者数dInとして現れる関係があるため、過去7日間の隠れ新規n’から未来の日にあたる新規患者数nが推測できる。

未来においては、隠れ患者I’nの値を、新規患者dInと隠れ新規dI’nの値から計算でき推測が可能である。

数式にすると何やらややこしいのですが、要するに、

  • 潜伏期間+発症期間を7日間とする
  • この期間で、二次感染が発生する
  • 現在の新規患者は7日前に何処かで感染している。
  • どこかで感染した後に、自覚症状が出て新規感染として隔離される前に、どこかで二次感染を発生させる。

というように、感染した直後から新規感染者としてカウントされる前に「滞留」する現象に注目します。実際、SIR や SEIR モデルでは回復の期間に二次感染を発生させるというモデルです。

Excel で計算する

これを Excel で計算できるようにします。感染率の計算が循環関数にならないように1日ずらしているのがミソです。循環関数になるとイテレーションが発生して、計算がややこしくなってしまうため、こうして1日ずらしています。

実際にはそのままプロットすると、土日の検査がないときに凹んでしまうので、7日間の移動平均でプロットします。

7日前よりも後にある感染率rは、推測値になるため手動で設定しています。先に解説した通り、特別な行動変容の変化がない限り、実効再生産数や感染率は大きく変わらないので、直前の値の平均などを使います。

実験として、都市閉鎖を行った場合を想定し、感染率rを 0.100 程度に変化させることで予想グラフが作成できます。

現在の感染率のままの場合

都市封鎖をして 7/13 より感染率を 0.100 に変えた場合

オレンジ色の新規患者のラインが変わることがわかります。

東京都以外でも計算してみる

日次データがあれば、他の都市の様子を観察できます。

ロサンゼルスの場合

テキサス州のダラスの場合

ドイツのベルリンの場合

灰色のラインが隠れ患者数です。テキサス州のダラスでは単調増加となり全く抑え込めていません。ロサンゼルスでは、7/1頃に少し抑え込むことに成功しましたが、再び増加中です。

ドイツのベルリンでは、直近では増加していますが、隠れ患者の数が上下に揺れています。これが抑え込めている状態です。直近のデータが6/30までしかないので、最近のものは不明です。

この推測計算の欠点

この概算では計算できないものを上げておきます。

  • 院内感染数を考慮しない。
  • 外部からの流入を考慮しない。
  • 未来予測では、新規感染者を一定率で取り除けることと仮定している。
  • 自然回復が考慮されていない。

もともと分布のある推測値をそのままプロットしてしまうので、推測される新規患者数には範囲があります。ただし、将来において同じ感染率とは限らない(自粛や PCR検査強化、何らかの検査の偏りなど)ので、もともと実際の新規患者数とは一致しません。なので、ひとまずの増加傾向と対処を行ったときの遅延(滞留する患者による二次感染のため)を確認するためという点では使えると思います。

参考データ

計算式が入った Excel は以下でダウンロードができます。

各国のデータは以下から取得しています。

カテゴリー: 開発 | 隠れ SIR モデルによる感染予測の解説 はコメントを受け付けていません

Vue.js で子コンポーネントから親に値を送る(SVG編)

この手の記事は結構あるのだけど、毎度忘れてしまうので備忘録的に。

やりたいこと

こんな風にSVGにピンを置きます。Vue.js を使って、SVG画像のピンの位置を親の App.vue に伝えます。

App.vue

親コンポーネント(App.vue)のデータで持つのは map_x と map_y で、data() の戻り値で保持します。これは、そのまま App.vue 内で {{map_x}} で参照が可能です。

方眼紙とピンで表示は Hello.vue で指定できるようにします。
Hello.vue 側は max_x と max_y プロパティ、変更したときのイベントを updatePos で返せるようにしてあります。

<template>
  <div id="app">
    <div>map_x: {{map_x}}</div>
    <div>map_y: {{map_y}}</div>
    <Hello 
     :map_x="map_x" 
     :map_y="map_y" 
     @updatePos="updatePos" />
  </div>
</template>

<script>
import Hello from './components/Hello.vue'

export default {
  name: 'App',
  components: {
    Hello,
  },
  data() {
    return {
      map_x: 100,
      map_y: 100,
    };
  },
  methods: {
    updatePos(x,y) {
      this.map_x = x ;
      this.map_y = y ;
    }
  }
}
</script>

Hello.vue

親の App.vue から max_x と map_y を受け取るのがプロパティで props に記述します。ピンはマウスのクリック(v-on:mousedown)で動かせるようにしてあるので、別途データとして pin_x と pin_y を用意します。
プロパティの max_x と max_y は親から引き渡される読み取り専用のパラメータなので別途代入しています。

実際は、pin_x と pin_y は、ピンの先端に座標が合うように少し補正してあります。

<template>
  <div>
    <h1>Hello SVG</h1>
    <div>
      <svg
           ref="pic"
           viewBox="0 0 600 400"
           v-on:mousedown="mousedown()"
      >
            <image 
              href="@/assets/images/hogan.jpg" />
            <image
               :x="pin_x"
               :y="pin_y"
               width="20px"
               height="58px"
               href="@/assets/images/marker.svg"
            />
      </svg>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Hello',
  props: {
    map_x: Number,
    map_y: Number,
  },
  data() {
    return {
      pin_x: this.map_x - 10 ,
      pin_y: this.map_y - 40 ,
    }
  },
  methods: {
    mousedown() {
        const vbox = this.$refs.pic.viewBox ;
        const pic_w = vbox.baseVal.width ;
        const pic_h = vbox.baseVal.height ;
        var r = this.$refs.pic.getBoundingClientRect();
        var x = Math.round(Math.round(event.clientX-r.left) * pic_w/r.width) ;
        var y = Math.round(Math.round(event.clientY-r.top)  * pic_h/r.height);
        this.pin_x = x - 10 ;
        this.pin_y = y - 40 ;
        this.$emit('updatePos', x, y );
    },
  },
}
</script>

マウスをクリックしたときの mousedown 関数では、クリックした座標を補正するためにちょっとややこしいことになっています。

クリックしたときの座標を返すために、this.$emit で updatePos イベントを呼び出します。

Vue 親子コンポーネントの関係は MVVM パターンではない

つまりは、

  • 親から子コンポーネントに渡すときは max_x, max_y のようにプロパティ経由で渡す
  • 子から親コンポーネントに渡すときは updatePos イベントのように this.$emit を通して渡す

のようになるので、MVVM パターンのように対称ではありません。Vue.js が MVVM パターンなのは、ひとつのコンポーネント内での仮想 DOM とのやり取りの話であって、親子コンポーネントは oneway のプロパティ経由なのだ、と。

という訳で、勘違いして迷っていたのでメモ書き。

カテゴリー: 開発 | Vue.js で子コンポーネントから親に値を送る(SVG編) はコメントを受け付けていません

各国の接触確認アプリを探索してみよう

プログラムを書く時に、どのような目的でそのアプリあるいはシステムが使われるのか?を定義しないとあらぬところに労力を掛け過ぎてしまいがちです。労力も時間も有限なのだから(時には無限の場合もあるけど)、注力するべきところに注力して仕上げをしたいところです。

規模の大きいITプロジェクトの場合、なかなか全容を把握するのは難しく、新人の頃は各工程(要件定義や設計工程)の全容が見ないままプロジェクトが終わることも度々あります。
今回の「接触確認アプリ」ですが、実はひと通りITプロジェクトの工程が見えてくる「好例」です。

  • 要件定義&概要設計
  • 必要な API
  • クライアント(Android, iOSアプリ)
  • サーバーサイド

という組み合わせでがあり、しかも各国のソースコードが github に公開されています。実装の違いも含めて、比較できることは珍しいことなので、ざっとでも情報を見ておくと今後に役立つでしょう。という訳で、いくつか見た範囲内での実例をリンクを記述しておきましょう。

接触確認アプリの目的

「接書確認アプリ」の目的は中国などを含めると様々な理由が含められますが、日本版のものは、5/9の有志気概者会議で明確に定義されています。

第1回 接触確認アプリに関する有識者検討会合 開催 | 政府CIOポータル

スマートフォンを活用して、

  1. 自らの行動変容を確認できる、
  2. 自分が感染者と分かったときに、プライバシー保護と本人同意を前提に、濃厚接触者に通知し、濃厚接触者自ら国の新型コロナウイルス感染者等把握・管理支援システム(仮称)に登録できるようにすることで、健康観察への円滑な移行等も期待できる。

これは「定義」なので、これ以上でもこれ以下でもありません。実際にはこの目的から提案依頼書(RFP)や要件定義書に落として「契約」を結ぶわけですが、今回は政府筋なので必要はないでしょう。発注先とは結ばれているとは思うのですが、ここでは問いません。

接触確認(暴露通知)の仕様

接触確認アプリの仕様は有識者会議と厚生労働省が作っています。

第1回 接触確認アプリに関する有識者検討会合 開催 | 政府CIOポータル
第2回 接触確認アプリに関する有識者検討会合 開催 | 政府CIOポータル
接触確認アプリに関する仕様書等の公表 | 政府CIOポータル

「接触確認アプリ及び関連システム仕様書」を見ると、詳細設計まで書いてあるのはさておき、官庁系のものとしては丁寧に書かれているので、これをもとに接触確認アプリとサーバーサイドを作ります。

このシーケンスと内部構造は、Google/Apple の仕様書にも書かれているものなので、そこは一致してますね。陽性患者の登録部分は日本独自なので、ここが作成時の肝となるところです。設計書や実装サイドとしては、この「仕様書」に従ってシステムを構築します。

いくつか興味深い「非機能要件」が定義されてます。

運用に関しては72時間以内(3日間)の復旧程度なので、即時復旧は意味しませんが、それなりに運用体制が必要です。あと、データは「クラウドを利用」しリージョンが「日本国内あること」の記述があります。この辺りも踏まえて受注と運用を検討します。

接触確認アプリの仕様

接触確認アプリは当初は GPS を利用した行動追跡が必須になっていたのですが、Google と Apple が共同で仕様を合わせた Exposure Notification(暴露通知)を使うことで、Bluetooth/BLE を利用したものに置き得られています。

アップルとグーグルが新型コロナ暴露通知アプリのサンプルコードやUI、詳細ポリシーを公開 | TechCrunch Japan

仕様の詳細は、Google/Apple の各サイトを見て貰うことにして、いくつか限定条件があります。

  • 暴露通知アプリは1国1アプリとする
  • 暴露通知とGPSを共有してはいけない
  • 暴露通知を他のアプリにバンドルしてはいけない

1国1アプリの理由は提供元が各国の保険省となるためです。複数出したり、保険省(日本では厚生労働省)以外が出すと審査で落ちるでしょう。
暴露通知とGPSとを共有しないのは、もともとGPSを使わないためなので、それが理由です。逆に言えば、組み合わせて詳細な行動追跡アプリを作ることも可能(昔の彼ログのような)なのですが、これに関しては駄目ですね。
暴露通知は、主に単機能で扱います。おそらくできるだけ許可を少なくする目的と、変な形で企業がバンドルできないようにするためでしょう。実質、厚生労働省以外は出せないので、ここは厚生労働省の要件定義次第になります。1国1アプリとのことなので、自治体が独自に使うことできません。大阪で予定してクーポン付きとかも駄目ですね。

Exposure Notifications: Helping fight COVID-19 – Google

ExposureNotification | Apple Developer Documentation

Googleのほうには、Android アプリの(Kotlin)とサーバーサイド(Java)の例があります。Apple のほうは API リファレンスだけなのですが、各国それなりに Swift で作ったコードがあるので参考にしやすいでしょう。

google/exposure-notifications-android: Exposure Notifications Android Reference Design

試してみるならば、Android のコードを使うのが便利です。Android Studio を使うと簡単にビルドができます。

google/exposure-notifications-server: Exposure Notification Reference Server | Covid-19 Exposure Notifications

サーバーサイドは試してはいませんが docker で動くようです。多分、ローカル環境で Android アプリとサーバーの動作環境を作れると思います。

ドイツの場合

ドイツの接触確認アプリが GitHub で公開されています。

Corona-Warn-App

いくつかのプロジェクトがありますが、Android, iOS, Server を見ておけばよいでしょう。

ドイツの corona-warn-app はドキュメントが綺麗に整備されていて、こんな風にシーケンス図やユースケースが残されています。

cwa-documentation/solution_architecture.md at master · corona-warn-app/cwa-documentation

この手のものが残されていると、ソースコードを修正するときに何が目的なのかが分かりやすいですね。

ちなみに、サーバーサイドは Java + PostgreSQL なので、日本の要件定義に合致しません。

フランスの場合

フランス版は GitLib で公開されています。

StopCovid sources · GitLab

ちょっと、興味深いのは「ROBERT」の文字が入っているところです。

これ、ロベルト・コッホの「ROBERT」なんですね。

ロベルト・コッホ研究所 – Wikipedia
RKI – Startseite

シンガポールの場合

シンガポールですが、コードは公開されてません(多分)。
シンガポール版は、実はハードウェアで実装されていて、不必要になったら捨てることができます。

なぜ新型コロナの追跡はアプリではなくハードウェアで行うべきなのか? – GIGAZINE

子供や高齢者にも利用してもらうことを考えると、こんな形でハードを配布するのがいいんですけどね。今回は、要件定義的に「スマホアプリ」となっているので割愛します。

日本の場合(Covid19Radar)

日本版のベースは Github で公開されています。
基本は Xamrin.Forms(C#) を利用していて、サーバーでは Azure Functions+CosomsDB になります。

Covid-19Radar/Covid19Radar: Open Source / Internationalization/ iOS Android Cross Platform Contact Tracing App by exposure notification framework Xamarin App and Server Side Code

settings.json でアップロード先のURL(API_URL_BASE)やAPI_SECRETが書き替えになっているので、これをローカル環境で動かすようにすれば、ローカルでの実験が可能です。

{
  "appVersion": "APP_VERSION",
  "apiSecret": "API_SECRET",
  "apiUrlBase": "https://API_URL_BASE/api",
  "supportedRegions": "440",
  "blobStorageContainerName": "c19r",
  "androidSafetyNetApiKey": "ANDROID_SAFETYNETKEY",
  "cdnUrlBase": "https://CDN_URL_BASE/",
  "licenseUrl": "https://covid19radarjpnprod.z11.web.core.windows.net/license.html",
  "appStoreUrl": "https://itunes.apple.com/jp/app/id1516764458?mt=8",
  "googlePlayUrl": "https://play.google.com/store/apps/details?id=jp.go.mhlw.covid19radar",
  "supportEmail": "SUPPORT_EMAIL"
}

サーバーサイドの Azure Functions と CosmosDB はローカル版もあるので、ローカル環境を構築して運用試験をすることも可能です。

実際には陽性者番号との突き合わせがサーバーサイドで発生するので、そこは自前で補ってやらないといけないのですが、複数の Android 機を使って一律通知のテスト位はできるかなと思います。

あと電力消費の問題も、複数台の Android に入れて1週間位実験すれば実情がわかるんじゃないでしょうか。

日本の場合(まもりあいJapan)

1国1アプリということで、開発は止まってしまったのですが、まもりあいJapan版も GitHub で公開されています。

まもりあいJapan

もともと Google/Appleの暴露通知ができる前からスタートしているのでGPS利用だった訳ですが、Exposure Notifications版に直して、通知先を Azure Functions に直せばローカルでは動かせるようになるかなと思っています。ちょっと試してみたいところですね。
サーバーサイドは nodejs + Firebase が使われています。

おまけ

接触確認アプリは BLE の近接距離を測っている(実際は電波強度)だけなので、もともと BLE を使うと実装できる話なのです。

接触確認アプリが有効になっているかを調べるアプリです。数十センチ程度まで近づくと素早く点滅します。M5 Atom 用です。https://twitter.com/ksasao/status/1274385507565178885 参照。

だから、本当はアップロードはフリーWiFiなどを利用すれば、ハードウェアでもいいはずなんですよね。とりあえず、皆様がBTを常時有効にし始めるので、データ調査が捗ってしまうという面もあったりなかったり。

すれちがいフレームワークのためのBLEを用いた近接検知機構の実装と評価

BLEはその名の通り低電力で動くのでボタン電池で1年間とか動いたりします。それを利用した近接検知はBLEが出た当初から研究されているので、色々調べると面白いですよ。

カテゴリー: 開発 | 各国の接触確認アプリを探索してみよう はコメントを受け付けていません

プログラマにもわかる SEIR モデルシミュレーション

最初に「SEIRモデル」を知ったのは2月末の頃です。まだダイヤモンド・プリンセス号で新型コロナウィルスが流行っていた頃で東京都にも他の都市もヨーロッパもアメリカもそれほど問題にはしていませんでした。

中国の武漢の状態から「感染力がインフルエンザ並みであるが、クラスター(当時はスーパースプレッダーという用語が使われていました)があり、条件が揃うと十数倍の感染力になってしまう」ことが指摘されていました。

さて、私自身は一介のプログラマなので疫学の専門ではありません。しかし、SEIRモデルが示す通り、数理としての疫学があり専門家会議の西浦教授が「数理」を通して現象を解いているところに共感を覚えます。同時に、コンピュータシミュレーションや統計学の分野として、疫学の「SEIRモデル」を解くことができます。

タイトルに「プログラマにもわかる」という言葉を入れましたが、敢えて言えば「プログラマだから分かる SEIRモデル入門」というところです。SEIRモデル自体は大学の1年ぐらいで学べるような疫学の入門レベルのものです。実際のシミュレーションには数々の現実のパラメータが含まれることになりますが、統計学の主要因分析のように主な要因(原因)特定して、現象を概略を把握しておくとは重要なことです。

というわけで、ちょっとしたプログラム(JS)を利用しながら、SEIRモデルの動作を実験してみましょうというのがこの記事の主旨です。

以下、書くときの効率を重んじて口調が教科書風になりますがご容赦を。

SEIRモデルとは何か?

SEIRモデルというのは、今回のような疫病の場合に患者の状態を4つに分けたモデルである。

  • 未感染者(S:Susceptible)
  • 潜伏期間中(E:Exposed)
  • 発症中(I:Infectious)
  • 免疫獲得者(R:Recoverd)

SEIRモデルは、4つの状態に分かれて、それぞれが直線的に遷移する。

UMLで使われるステートチャート(状態遷移図)と同じだ。4つの状態があって、遷移は、以下の3つの線だけになる。

  • 未感染者から潜伏期間中
  • 潜伏期間中から発症中
  • 発症中から免疫獲得者

未感染者は初期状態で、健康な状態。インフルエンザで言えば、今年の流行に罹っていない人だ。
この未感染者が誰からインフルエンザのウィルスを貰うと、まず潜伏期間中になる。潜伏期間中の場合は、誰にうつす訳ではない期間のことを言う。
潜伏期間が過ぎると、発症期間中に移行して誰かを感染させる状態になる。インフルエンザの場合は、体が動かなくなるので家に必然的に家に籠ることが多い。
発症期間が過ぎると何らかの免疫ができて、免疫獲得者となり回復する。縁起が悪いが、発症期間中に死亡しても免疫獲得者の枠に入れる。免疫を獲得すると誰かにウィルスをうつすこともないし、同時に誰かからウィルスをうつされることもない。

現実には、未感染者から一気に免疫獲得者になる道筋があって、これがワクチンの接種、つまりインフルエンザの予防接種だ。予防接種をすることによって、潜伏期間と発症期間を一気にすっ飛ばして(疫学的には素早く移動するということになるが)、免疫獲得者の枠に入る。

世の中で言われる「集団免疫」という手法は、この「未感染者から一気に免疫獲得者」になることを示している。インフルエンザなどのワクチンが開発された状態(インフルエンザは毎年変わるので、毎年予防接種を受けないといけない)の場合には、途中の発症期間中の死亡率がきわめて低い(実は予防接種でもゼロではない)ので、この「集団免疫」という手段を取る。麻疹とか風疹も同じだ。
しかし、新型コロナウィルスのように決定的なワクチンがない状態で集団免疫の手法を取ると、発症期間に死亡する確率が高くなるため手段として妥当ではない。しかしながら、国内死亡率というのは、単純に新型コロナウィルス由来だけのものではなく、経済的な自殺者も含めるものなので、そのバランスを鑑みて国ごとに方策がを決めることになる。かなりシビアな話だ。

遷移割合を入れる

UMLのステートチャート(状態遷移図)の場合は、100%次の状態に遷移するのだが、疫学の場合は遷移する「割合」が入ってくる。例えば、3000人の未感染者に1人の感染者が含まれていたら、新たに潜伏期間中に遷移する人数はどのくらいだろう?という具合だ。

これを学術的には「摂動論」という言う。解析学が微分/積分を使って数式を解析的に解くのだが、摂動論は状態の差分を逐一計算する。今で言えば、コンピューターシミュレーションのはしりのようなものだ。機械学習で使われる畳み込み計算も摂動論に一種である。一気に解析的に解いてしまうのでなく、状態を1手1手(この場合は1日1日)進めながら最終的な値に近づけていく計算手法である。

数理計算の場合、この手のコンピューターの計算力を使う方法が良く使われる。完全に予測をするためには、「遷移する確率分布」を求める(いわゆるベイズ)を使うことになるが、ここでは単純化させて「遷移する割合」を使う。
ここで、割合をつかっても良い理由がいくつかあって、

  • 都市閉鎖、自粛などで途中の「実効再生産数 Rt」が変化する
  • クラスターの発生などで、遷移確率は正規分布ではない(偏りがある、条件による)

このような理由があって、確率を正確に計算しても意味がない。後述するがダイヤモンド・プリンセス号のような「理想的な状態」の場合には、この SEIR モデルがよくマッチする。逆に言えば、数々の現実の条件が入ったときには、SEIR モデルはマッチしないと言える。

遷移する割合は差分として扱えるので次のように dS/dt としてΔ(差分)として表せる。

それぞれの状態をS,E,I,Rとして表して、n-1番目の状態からn番目の状態に遷移する、というのが次の式の意味になる。n番目というのは、このような1手1手のステップを使うときに良く利用される帰納法的な手法だ。

最初のSとRの式が他と異なるのは、S(未感染者)の場合は入り(プラス)がない。R(免疫獲得者)の場合は出(マイナス)がないことを示している。
つまり、Sは単調減少、Rは単調増加になる。途中のEとIは、増加と現象が前後の状態によって異なってくることが分かる。

このままだと、dS/dt 自体の計算が少しやりづらいので、dS/dt を中心にして式を書き直してみる。

それぞれの差分(dS/dt)には、何らかの割合(パラメータ)が加えられている。これを示すために、

  • S(未感染者)には、β と N(総人数)という割合
  • E(潜伏期間)には、α という割合
  • I(発症期間)には、γ という割合

を掛けることにする。N というのは全体の総人数(S+E+I+R)で全て足した数になる。

SEIR モデルとしては、

  • β : 感染率
  • α : 潜伏率
  • γ : 回復率

という呼び方をする。これは丁度、Compartmental models in epidemiology – Wikipedia の SEIR model の式と同じになる。英語版の SEIR モデルの場合、自然死亡率である μ が含まれているが、ここで扱う SEIR モデルは短期間となるので、μ = 0 とする。

潜伏率(α)や回復率(γ)というのをどのように定義するのかというと、次のように疫学の潜伏期間(lp)と発症期間(ip)によって表せる。

潜伏期間というのは、誰からウィルスを貰って発病(症状が出る)までの期間である。通常の場合、潜伏期間中にはウィルスを外に出さない、つまり誰にもうつさない状態となる。インフルエンザで言えば、2,3日ということになる。
発症期間というのは、症状が出てから回復して免疫を獲得するまでの期間である。モデル的には発症期間中の死亡もここに含まれるので症状が重い場合には、実は発症期間は短く換算される。インフルエンザで言えば1週間ということになる。小学生がインフルエンザに罹って学校に復帰できるまでの期間が1週間なので、これにあたる。

さて、肝心の感染率(β)はどのように計算すればよいだろうか。
今回の新型コロナウィルスで一気に有名なった「基本再生産数 R0」や「実効再生産数 Rt」がこの感染率に関係してくる。
「再生産数」というのは、疫学だけの用語ではなくて「ひとつの個体がどれだけの別の個体をうみだすか」の割合(数)になる。例えば、女性が一生のうちに子どもを生むという再生産数は、2.3程度が理想的と言える。2.0よりちょっと多いのは、途中で病気や事故などで死亡するからだ。現在の少子化により1.6位になっているので、ぐんぐん人口が減ることが分かる。

私自身の専門になるけど、炉物理の分野でも再生産数という意味で「断面積 σ」が使われる。ひとつの中性子が U235 に当たって、いくつの中性子を生み出すかという計算に断面積を使う。この断面積は、核種固有ではなく燃料棒や制御棒などを含めた値でもあり、最近のコンピューターシミュレーションでは、燃料棒、制御棒、一次冷却水を別々の断面積で扱い、再生産数を計算し臨界を保つことになる。ここは余談だが。

  • 「基本」再生産数は環境に依存しない、ウィルス固有の値
  • 「実効」再生産数は環境に依存する割合

という違いがある。「基本」(basic)と「実効」(effective)の違いは他の分野でも出てくる。
さて、ここでは基本再生産数 R0 を使ってみよう。先の英語の SEIR model の中に次のような式がある。

基本再生産数 R0 は、感染率、潜伏率、回復率、死亡率(μ)によって、計算されていることがわかる。ここで、潜伏率(α)と回復率(γ)は既知であり、死亡率(μ)は 0 とするので、次のように式が書き替えられる。

つまり、感染率は、基本生産数に回復率を掛けたものになる。回復率(γ)は、発症期間(ip)の逆数となるので、次の形になる。

ところで、基本再生産数 R0 の定義を思い出してみよう。ひとつの個体がその生存期間中に他の個体を出す割合である。疫学の教科書には、ひとりの感染者が他の感染者(二次感染者)を生み出す数、となっているが、つまりウィルスが生きている間(回復期間に入る前、つまり発症期間I)にどれだけの患者を作るか?という数になる。さきに書いた、少子化の問題になれば発症期間はイコール女性の一生の間という期間になる。

R0 は発症期間に依存していることになる。発症期間に依存している R0 を発症期間(ip)で割るというのが感染率(β)の意味になる。
つまり、感染率というのは単位あたり(この場合は1日あたり)に感染者を増やす割合を示している。
この部分、単位日あたりの感染率と基本再生産数を別に扱っているのは不思議な感じがするが、ともかく同じことを示していることが分かるだろう。多分、「倍加期間」というように、核種の半減期(指数関数)にあわせるための工夫だと思う。

何故、指数関数になるのか

SEIR モデルなど実測の経緯を示すために指数関数(片対数表)が良く使われる。急激に患者数が上がってしまうために指数関数を使っているように見えるが、実はこの SEIR モデル自体が指数関数的に増加するようになっているために、グラフがそうなる。

モデルと現実とが逆転しているようにみえるが、「現実の主要因要素を抜き出して関したものがモデル」であるので、モデルは現実の一部をうまく表せるようになっている。逆に、現実のパラメータがモデルに合わなくなると、そのモデルは捨てなくてはいけない。新しいモデルを適用する必要がある。

これは、パターンランゲージなど活用してプログラムを組むときに似ている。業務モデルを作ってパターンに当てはめたり、パターンをうまく使えるプログラム言語を活用すると、システムは効率よく作れる。しかし、現実をうまく業務モデルに落とし込めなかったり、業務モデルが確定した後に現実のほうが変わってしまった(業務モデル自身によって改善された場合も含む)場合でも、業務モデルから乖離してしまう。
この場合、業務モデルを新しく作り直すのは常だ。
同じ現象は、SEIR モデルにも言える。

指数関数というと自然対数 e の乗数を使ったものが多いが(これは数学的に理由がある。e で指数関数を使うと微分しても、e に戻るので計算が楽になるのだ。ガウス分布がこれにあたる)、なにも疫病の現実が指数関数にマッチしいるのではない。指数関数になる SEIR モデルを使っているから、シミュレーション結果が指数関数的になるのだ。
この因果関係がよくわかっていないと、指数関数にフィッテイングするように現実の感染者数をマッチさせてみたり、S字カーブにフィッティングする、という謎理論が出てくる。いや、謎理論は別にいいのだが、現実に即していないので「科学的」とは言えないだろう。

SEIR モデルの場合、感染して潜伏期間に入る、潜伏してから発症する、発症してから免疫を獲得するというステップがある。これは現実のウィルスも同じである。新型コロナウィルスの場合、潜伏期間中に誰かに移すという報告もあるので発症期間が少しずれるが、これは「誰かに移す期間を発症期間とする」とう定義に合わせて、2日間ほど発症期間に加えてしまえばよい。

あらためて、未感染者 S の差分の式をみていこう。感染率(β)が、未感染者(S)に掛け合わさって感染していくことがわかる。総数(N)で割ってあるのは、これらが単位のない割合となるからだ。

  • 未感染者(S)から一定の割合で感染して潜伏期間中(E)の移る。
  • 潜伏期間から発症期間があるが、潜伏期間中に滞留する。
  • 同じ理由で、発症期間にも滞留する。

これにより、それぞれの「滞留」数が、患者数の累計が指数関数的に増加するという意味あいになる。これは解析的に解いてもいいのだが、現実のパラメーター(実効再生産数 Rt)が日よって異なってくるので解析的に解くのは妥当ではない。日単位でシミュレーション解析をしたほうがよい。

これをシミュレーションしたのが、下の図になる。

オレンジ色の潜伏期間数が、急激に上がっている(線形的ではない)ではことが分かるだろう。特に立ち上がりのところは、指数関数的にあがる。実際は e にフィットする訳ではないが指数関数的に爆発的に増加する(いわゆるオーバーシュート)と言われるのはこの現象である。

SEIRモデルを JavaScript でシミュレートする

統計学的に解析が必要となるので、R を使ったり Python で既存のライブラリを使ったシミュレーションが多いのだが、実は Javascript でも簡単に作れる。

特に、基本再生産数 R0 を色々変えてみたり、潜伏期間や発症期間を変えて現実にフィットするかをチェックするためには、試行錯誤が簡単にできる Javascript がよいだろう。Python で GUI を使って作ることも可能なのだが、Vue.js を使い c3.js のようなグラフツールを作れば、JS でも十分使える。

        seir_eq: function(v,t,alpha,beta,gamma,N) {
            S = v[0]
            E = v[1]
            I = v[2]
            R = v[3]
            ds = - beta * I / N * S             // dS/dt = -βI/N*S
            de = beta * I / N * S - alpha * E   // dE/dt = βI/N*S-αE
            di = alpha * E - gamma * I          // dI/dt = αE - γI
            dr = gamma * I                      // dR/dt = γI 

            return [ds,de,di,dr];
        },

肝は、先の差分の式を JS に直すだけだ。
ここでは、seir_eq という関数を使って差分の部分を計算している。S,E,I,R が配列になっているのは、参考にした python プログラムの名残りである。

これを日単位でステップ実行さると先のようなグラフができる。
シミュレーターは、SEIR モデル シミュレータ で実行ができるようにしてある。

コードは、moonmile/seir-model: SEIR model simulator で公開しているので、詳しくは index.html の中味を見て欲しい。それほど難しいことはやっていないのが分かるだろう。シンプルな SEIR モデル程度ならば、それほど難しくはない。

考察

試しに、以下の数値で計算を実行してみてほしい。

  • 未感染者(S)が3,000人
  • 初期の感染者が5人
  • 基本再生産数 R0 = 10.0

基本再生産数 R0 が 10.0 というのは「理想的なクラスター」の値である。
1か月後には、潜伏期間の患者が1,500人になることが分かる。この数値は、実際にダイヤモンド・プリンセス号の患者数にうまくフィットしている。閉鎖空間における新型コロナウィルスの感染率は非常に高いと言える。逆に言えば、閉鎖空間ではない場合の Rt は小さい。感染させない人が8割ぐらいいるという根拠である。

東京都の都市閉鎖についても、場所によっては同じ現象が起こると考えられる。一概に閉鎖がよい訳ではないのは、経済活動的な理由もあるが、人の行き来を少なくしてしまうために場所によっては「感染に理想的な空間」が生まれやすいという理由でもある。

また、新型コロナウィルスでは以下の恐れがある(まだ事実とは言えない)。

  • 無症状の保菌者がいる。
  • 無症状の保菌者が感染させている可能性が高い

これは今後のニューヨークでの抗体検査、日本での抗体検査によって明らかになっていくことだろう。

新規患者数の遅延について

以下は、隠れ患者を想定した SIR モデルのグラフである。私的に計算したものなので、学術的な正しさは脇に置いておく。

新型コロナウィルスでは、潜伏期間が長い且つ無症状患者が多いと感がられるので「隠れ患者」を試算してみた結果である。

  • 新規患者数を2週間前に遡って、隠れ新規患者数とする
  • 2週間前の隠れ患者数の累計を出し、その時点での新規患者を回復者として取り除く
  • 感染率を推測し、現時点に当てはめ、2週間後の患者数を予測する

という手法を取っている。このグラフは Excel で集計し、以下でダウンロードが可能である。

他にも新規患者数について様々な予測グラフがあるが、以下の点を指摘しておきたい。

  • 現実をモデルにフィッティングさせてはいけない(特にSカーブ)。常に、現実のほうが正しい。
  • 全国一律で予測してはいけない。「クラスター発生」である通り、漠然と発生しているのではなく各地に偏在する、分散がある。よって、最低限、閉鎖する都市単位(東京都、大阪府、札幌市など)で傾向をみる必要がある。

完全な予測は難しいし、実際のところは完全に予測するのは不可能だ。IT 屋なら分かるだろうけど、ウォーターフォール方式でプロジェクトが盲目的に進んでしまえば、そこに待っているのはデスマーチしかない。偶然にもプロジェクトが成功するかもしれないが、それは現実が変わらないという前提で綿密な計画を立てたときに限る。
ならば、その時々にハンドリングをするアジャイル方式で取らねばならない。専門者会議の意見が週単位で変わってしまうのも仕方がない。長期的なスローガンは今回の場合は悪手に陥りやすい。手間暇はかかるが、状況を見据えてその場その場で状況に合わせて短期的な計画を立て、実行して結果を見直すというサイクルが必要だ。

時として、長期的な安心を求めるかもしれないが、それは無駄だ。しかし、暗中模索というわけではない。今回紹介する SEIR モデルのように、ある程度の先行きの予測は「シミュレーション」により可能である。シミュレーション結果がズレていれば、清くパラメータを変えて再びシミュレーションをすればよい。隠れSIRモデルを使って、独自に日々更新しているのはそれの意味もある。

と言うわけで、プログラマならばちょっと手を動かして JS で組んでみてください。Python でも組めるし、これならば C# でもいけるでしょう。Excel VBA でも可能です。是非ためしてみてください。

参考文献

カテゴリー: 開発 | プログラマにもわかる SEIR モデルシミュレーション はコメントを受け付けていません