Xamarin.Formsでログをファイル出力する(iOS編)

Xamarin の場合、Visual Studio を通してデバッグ実行する場合は、System.Diagnostics.Debug か System.Diagnostics.Trace を使います。そのまま Visual Studio のデバッグ出力ウィンドウに表示されるので、手軽にプログラムの動作が確認できます。もともと、System.Diagnostics.Debug などは、.NET の Windows プログラミングで使われていたものなので、.NET であれば全般的に使えます。
つまり、ASP.NET でも、Blazor でも Azure Functions でも同じ様に使えるわけです。

同じ様に使えるということは、知識の使い廻しができる点で、

System.Diagnostics.Debug.WriteLine( message );

の使い方が、どの .NET 環境で利用できるということです。
余談ですが、Console のほうも同じで、標準出力にデータを書き出すという点は何処でも同じです。

System.Console.WriteLine( message );

なので、このような書き方をしても、どの環境であっても「標準出力」があれば出力がされます。Xamarin の場合には標準出力がないのでどこにも出力されません。ただし、別途標準出力を作ってやれば、目的の標準出力に出力されるでしょう。

デバッグ出力をファイルに書き出す

お手軽なデバッグ出力ではありますが、常に Visual Studio から起動しないといけないのはいささか面倒です。特に、スマホのアプリの場合は、スマホ単体でアプリを起動することが多く、テスト作業をするにしても Visual Studio から常に立ち上げるのは難しいでしょう。ブレークポイントを置いて何らかのチェックをしたい場合はもあるでしょうが、一連の動きをデバッグ出力としてファイルに保存しておくのがよいでしょう。

デバッグ先の出力ファイルを独自に作ってもよいのですが、ここでは System.Diagnostics.Trace のリスナーの機能を使ってみましょう。

ちなみに NuGet からライブラリを追加してよいのであれば、NLog を使う方法もあります。

NLog を使って Xamarin.Forms からログ出力する方法 – Qiita

実は、Trace には Listeners コレクションがあって出力先を追加できます。普段は Visual Studio のデバッグ出力にしか出ないのですが、これにファイルストリームを追加すると、トレース結果をファイルに出力できます。

Trace.Listeners Property (System.Diagnostics) | Microsoft Docs

var tw = System.IO.File.OpenWrite(filename);
var tr1 = new TextWriterTraceListener(tw);
System.Diagnostics.Trace.AutoFlush = true;
System.Diagnostics.Trace.Listeners.Add(tr1);

出力したいファイル名を OpenWrite 関数で開いて、TextWriterTraceListener オブジェクトを作ります。これを Listeners コレクションに Add するだけです。
AutoFlush を true にしておくのは、トレース出力(WriteLineなど)のたびにファイルに書き込むことを示しています。いちいち Flush するとスピードは遅くなるのですが、不意のアプリのクラッシュのときに、ログファイルが途中までしか書き込まれないときがあるので、AutoFlush させておいたほうが無難です。

ファイル名をどうするのかという問題がありますが、これも .NET で一般的に使われる Environment.SpecialFolder.MyDocuments あたりを使えば大丈夫です。Xamarin.Essentials でも良いのですが、所詮ファイルの作成先が作れればいいので、これでも大丈夫です。

var dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var filename = Path.Combine(dir, $"log-{DateTime.Now.ToString("yyyyMMdd-HHmm")}.txt");

ファイル名は、起動したときの時分までで作成しています。こうすると、ファイル名がユニークになります。秒まであってもいいのですが、おそらくアプリのテスト起動は1分以内に行わないだろうから、これでいいでしょう。日付毎にまとめたい場合は、別途 OpenText などを使って工夫します。

もうひとつ、Info.plist で LSSupportsOpeningDocumentsInPlace を YES にしておきます。このようにすると、アプリのフォルダが「ファイル」に表示されるようになります。

<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

こうすると、Xamarin.Forms の共通プロジェクトのほうでログ出力ができるようになります。
iPhone では次のように、ファイルを開いてログの状態が確認できます。

iOS 側のデバッグ出力をファイルに書き出す

これでテスト用のログファイル出力は十分、と思ったのですが、もうひとつ難関がありました。
Xamarin.iOS 側のプロジェクトで System.Diagnostics.Trace を使ってもデバッグ出力ができません。この理由は判らないのですが、Xamarin.iOS のほうのプロジェクトで、Trace.WriteLine としても、Xamarin.Forms 側の共通プロジェクトで出したログに合わせて出力されることはありません。多分、System.Diagnostics.Trace の実体がひとつしかないので、Xamarin.iOS 側から触れないようになっているのかもしれません。

仕方がないので、iOS側の Trace は自作します。最小限の機能で十分なので、こんな感じで WriteLine だけ作っておきます。

public class IosTrace
{
    static IosTrace()
    {
        Listeners = new List<TraceListener>();
    }
    public static List<TraceListener> Listeners { get; }
    public static bool AutoFlush { get; set; } = true;
    public static void WriteLine(string message)
    {
        foreach ( var it in Listeners)
        {
            it.WriteLine(message);
            if ( AutoFlush == true ) it.Flush();
        }
    }
}

もとの System.Diagnostics.Trace と同じ様に Listeners コレクションに TextWriterTraceListener オブジェクトを追加すれば ok です。ファイル名は、Xamarin.Forms の共通プロジェクトで作ったものとは別にしておきます。

var dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var filename = Path.Combine(dir, $"ios-{DateTime.Now.ToString("yyyyMMdd-HHmm")}.txt");
var tw = System.IO.File.OpenWrite(filename);
var tr1 = new TextWriterTraceListener(tw);
IosTrace.AutoFlush = true;
IosTrace.Listeners.Add(tr1);

このようにしておくと、Xamarin.iOS 側で落ちたときのスタックトレースや微妙なコールバックの状態が判るようになります。簡易的なものなので、ファイル名や行番号などは付けていませんが、色々つけるような場合は NLog を使ったほうがいいかもしれません。

さて、iOS のほうがこれでいいのですが、Android の場合はどうなるでしょうか。というは話は、続きのブログ記事に書いておきます。

カテゴリー: 開発 | Xamarin.Formsでログをファイル出力する(iOS編) はコメントを受け付けていません

台東区ホームページの移行見積もり時間を見積もる

暫く忘れていた台東区ホームページ https://www.city.taito.lg.jp/ がリニューアルされました。この台東区のページ、去年の2月頃に「PHPはセキュリティに危ないから使ってはいけない」という要件が含まれていた案件です。ツイッター関係で、別に PHP がセキュリティ的に甘いのではなくて、作りが曖昧のだという議論があって、その後「バックエンドならば PHP をつかってもよい」という要件に変わったのですが、フロントエンドでは「動的生成をしてはいけない」というちょっと厳しめの案件でした。

入札なので、見積もり期間が短かった(2週間弱だったと思う)のと、6月スタート、12月運用開始という非常にハイスピードなため、どこかあらかじめ発注する場所が決まっているのではないか?と勘ぐってはみたいのですが、実際のところは結構堅めの会社が受注しました。台東区が出した予算枠が 5000万円だったのに対し、3000万円程度で受注した覚えがあります。

要件としては、

  • 「PHP はセキュリティに問題があるから避ける」と明言されていた
  • 実行時に DB は使わない
  • PC/スマホ共有は必須
  • ページ数は2000頁以上ある
  • 移行期間は6月から12月末までの6か月間
  • 博物館等の紹介ページも移行対象だった

というのが主なところでした。おそらくフロントエンドに WordPress を使わないようにさせるためではないか?という憶測もあり、じゃあ、バックエンドならばどうなのか、という議論もあるところです。

私が要件を見て一番のリスクだと思ったのは「博物館等の紹介ページも移行対象」のところです。当時、ざっとサイト眺めていたのですが、その手のページが無くて「多分、Flash とかなのでは?」と思ったものですが、ここがどう解決されているかは確認していません。
新しいサイトを見ると、もともと外部のサイトだったらしくリンクだけになっていますね。全てを見てないのでよくわかりません。

さて、旧台東区のホームページ トップページ 台東区ホームページ から、すべてのページが移行対象になります。台東区の担当者(あるいはコンサル?)が調べたところ 2000ページ以上あるらしいです。もともとの仕組みがどうなのかわかりませんが、すべてのページは *.html になっています。コードは、まあ、ある程度は整頓されているようでした。

要件の中に PC でもスマホでも表示できるように「レスポンシブルなページ」という用語があるので、何らかのコンサルが噛んでいる(あるいは担当者が詳しい?)ので、PHP のセキュリティが甘いという文言もそのあたりだったのでしょう。ちょうど、GMO だったかで .htaccess の不良があって wordpress がハックされるという騒ぎがあったので、それを受けての要件だと思います。

アクセス数は不明ですが、台東区民は20万人程度です。ゲームサイトのように台東区民がアクティブにサイトにアクセスする訳ではないのですが、日に10%程度の人がアクセスしたとして、2万位のアクセスになります。1時間に2000程のアクセス数しかないので、ある程度のキャッシュ用のプロキシを噛ませれば wordpress でも十分な気がしますが、要件としてはこんなところでしょう。

おそらく最終的に、

  • バックエンドで DB から静的 HTML を生成
  • フロントエンドで静的 HTML を返す
  • フロントエンドでレスポンシブルなページを作る

というのがシステム構成になっていると思います。静的 HTML を作るツールはいくつかあるのでしょうが(wordpress でも作れる)、問題となるのはシステム的なスピードよりも、移行対象となるページの多さです。最終的にどの位のページ数になったのかは不明ですが、要件段階で 2000ページ以上あることが明言されています。

作業項目(WBS)を見積もる

細々とした WBS を出す前に、大まかな作業項目を洗い出します。どうやら人海戦術になりそうな移行ページ数なので、そこが一番効いてきます。

  1. 移行前ページのクローリング&データ抽出
  2. 移行後ページの生成
  3. 移行後ページの動作確認

移行後のサーバー設定やらデータベース設定などはある程度見積やすいのですが、2000ページあると、ページ単位の作業量が問題になります。

商品販売のページとは違い、動的に変わる部分は少ない(過去の情報はそのまま変わらない)ので、単純に移行前のデータを取得して、移行後のページに整形し直すという作業になります。比率的にここが一番大きく、ひとつの作業(データ抽出→整形→動作確認)単位が 2000倍されることになります。

  • 1日で1ページならば、2000日 = 5人/半年 ペース

単純計算だとこうなります。それぞれのレイアウト込みの作業量なので、1日1ページが妥当かどうかわかりませんが、結構厳しめです。トータル予算には、前後のサーバー設定などの費用と時間がかかるので、それなりに掛かるでしょう。

開発期間は半年と決まっているので、全体の作業量を人月で割ることになるため、単純に人数を増やすしかありません。

効率化可能な場所を探す

人月商売ならば、上記の方法でページ単価を出して、ひとつず解決する。ページ単位の作業量から、全体の作業量を割り出せばいいのです。ですが、これだと作業量は変わらないので、薄利多売方式にしかなりません。

折角なので、IT 屋らしく、作業効率を高くできる場所を探します。

この場合、移行対象のページ数が多いので、ページ単位の移行作業を効率化すれば、全体の作業量がぐんと減ります。

  • ページ単位で自動化して、作業量を10分の1程度にする
  • 作業項目をひとまとめにして、作業量を10の1程度にする

分業化するあるいは自動化するのが効率化の常で、作業量は10分の1程度を目標値にします。
数パーセントの効率化では意味がないし、10分の1になる方法を考え出せれば、他社が追随できなくなります。いわゆる、社内ノウハウ、専門技術という訳です。

先の移行前/後のページ単位の作業は「手作業」を想定しています。ならば、この一連の作業を自動化させてしまうか、あるいは WBS 単位で 2000 ページの作業を圧縮させるかです。

で、考えらえるのが、

  • 移行前のページ抽出 → 移行後のページ出力 単位で自動ツールを作る
  • 移行前のクローリング → データベース保存をツール化する
  • データベースから、ページ出力を標準化する(レスポンシブル部分)
  • リンク切れなどのチェックを自動化する

移行後のページのコードを見ると「▼ヘッダーここから▼」等の作業用マークがあるので、実作業ではどこまで自動化していたのか不明ですが、「ページ出力を標準化」はきれいになされています。

以前は、大幅にカテゴリ単位でレイアウトが違ったところが標準化されています。
ただし、アイキャッチが所々入っているので、ページ出力に関してはかなり人手を使っているのではないかと想像できます。それでも小見出しやリストの表示は共通化されているので、作りやすくしてあるかなと。

所感

ともあれ、全体的には静的 HTML にしてあるので、体感的に表示が早くなっています。
実は、データベースを適切に配置させて、あまり入れ子にならないビュー専用の WordPress っぽいものを作るのと、静的 HTML 生成を動的に行えば似た感じのスピードは出せるので、静的 HTML にこだわる必要はないのですが、ここは「要件」なので仕方がない。

気象情報、緊急情報がトップページにあるので、災害時に20万人にリロードされるのは、トップページになります。いちばん重いのは jQuery 位で、初回に画像読みに少し時間が掛かるぐらいですね。災害時のメッセージ(現在では「12時間以内に配信した情報はありません。」になっているところ)は、Web API を呼び出して jQuery で埋め込んでいるようです。

カテゴリー: 開発 | 台東区ホームページの移行見積もり時間を見積もる はコメントを受け付けていません

ラズパイで adb コマンドを使う

その昔、ラズパイで adb コマンドを使うのはえらい苦労した覚えがある(ARM なのでソースコードからビルドする必要があった)のだけど、さっくりと apt-get で可能になってた。

Install ADB and Fastboot on RasPi 3 – Raspberry Pi Forums

sudo apt-get install -y android-tools-adb android-tools-fastboot

自分を plugdev グループに追加

sudo adduser $LOGNAME plugdev

adb で接続

adb devices
adb shell 

orange pi の armbian でも大丈夫でした。これで Android の自動化が捗る…かもしれない。

カテゴリー: 開発 | ラズパイで adb コマンドを使う はコメントを受け付けていません

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 モデルによる感染予測の解説 はコメントを受け付けていません