COCOAデータを読む

[2022-08-28] COCOA通知が来ました(2022-08-24に「合計51分間の接触」)。その時点のデータまで追記してあります(exposure_data.json も)。

COCOA 2.0.0 になって、MatchCount がログに記録されないようになりました。

COCOA 2.0.1 になって、COCOA アプリの「陽性登録者との接触結果を確認」→「情報を保存」で exposure_data.json という詳細ログが保存できるようになりました。これを見れば接触日などがわかります。私の iPhone のデータ例を置いておきます。中を見るための Python プログラムの一例です(同様なことをするためのサイト COCOAログチェッカー2.0 (β)COCOAログ.jp ができました):

import json
import pandas as pd

with open("exposure_data.json") as f:
    exposure = json.load(f)

print("Date Infectiousness  TypicalAttenuationDb MinAttenuationDb SecondsSinceLastScan")
for x in exposure["exposure_windows"]:
    t = pd.Timestamp(x['DateMillisSinceEpoch'], unit='ms')
    print(str(t)[:10], x["Infectiousness"], end="")
    for y in x["ScanInstances"]:
        print(" ", y["TypicalAttenuationDb"], y["MinAttenuationDb"],
              y["SecondsSinceLastScan"], end="")
    print()

出力:

2022-03-25 2  97 92 120
2022-03-31 2  91 91 60
2022-04-01 2  93 93 60
2022-04-14 2  93 88 60
2022-07-04 2  91 87 60
2022-07-04 2  91 91 60
2022-07-18 2  91 84 120  90 90 60
2022-07-19 2  89 89 60
2022-07-22 2  91 91 60
2022-07-23 2  87 87 60  84 78 120
2022-07-28 2  94 94 120  95 95 60
2022-07-29 2  89 89 60
2022-08-01 2  88 80 60
2022-08-04 2  100 100 60
2022-08-05 2  98 98 60  90 89 60  98 98 60
2022-08-13 2  89 89 60
2022-08-19 2  83 83 60
2022-08-19 2  80 69 120  85 79 60
2022-08-19 2  78 76 60
2022-08-19 2  86 86 60
2022-08-19 2  87 87 60
2022-08-19 2  91 91 60
2022-08-19 2  88 82 60
2022-08-20 2  96 96 60
2022-08-20 2  90 89 60
2022-08-21 2  101 101 60
2022-08-21 2  98 98 60
2022-08-21 2  86 86 60
2022-08-21 2  87 87 60
2022-08-21 2  89 87 60  87 83 120
2022-08-22 2  86 84 60
2022-08-22 2  94 94 60
2022-08-22 2  90 90 60
2022-08-22 2  97 97 60
2022-08-23 2  87 82 120  97 97 60
2022-08-23 2  85 85 60
2022-08-24 2  80 75 120  74 71 60
2022-08-24 2  92 92 60
2022-08-24 2  88 86 60
2022-08-24 2  53 51 180  55 49 120  51 46 120  55 54 180  58 58 60  58 56 180  58 50 240  68 64 180  57 56 60  63 56 120  63 63 60  58 55 180
2022-08-24 2  59 58 180  62 59 240  64 64 60  59 55 300  56 56 60  66 66 60  61 59 120  58 58 60

表示される日付の9:00から丸一日の間に接触があったことがわかります。日付以外の項目の意味は Google のドキュメント に説明されています(いまいちよくわからない説明ですが)。鈴木健治さんのCOCOAの接触時間が長い理由と、COCOAログチェッカー件数の意味がたいへんわかりやすい解説です。

一つの exposure_windows の要素はたかだか30分で、これ以上の接触は2回と数えられます。ですから、上の例の 2022-07-04 は、2人に会ったか、1人に2回会ったか、1人に30分を超えて会ったか、ということのようです。TypicalAttenuationDb と MinAttenuationDb はそれぞれ BLE(Bluetooth Low Energy)電界強度の dB(デシベル)値の平均値・最小値です。dB 値が小さいほど電界強度が強いことになります。野上大介さんの解説によれば 5m 離れていれば 65〜75dB 程度のようです。SecondsSinceLastScan は秒数を 60 の倍数に丸めたものです。

ちなみに、iPhone の「設定」→「接触通知」→「使用中」→「接触チェックの記録」に出る「一致したキーの数」は次のような不思議な変化をします:

一致したキーの数

このあとずっと 0 でしたが、2022-07-08 に 1 に転じました。変化のあったところだけを書き出すと次のようになります:

2022-03-30 15:38,1
2022-04-03 17:20,2
2022-04-07 17:29,3
2022-04-16 15:53,1
2022-04-17 00:28,0
2022-04-18 16:08,1
2022-05-14 15:54,0
2022-07-08 14:37,1
2022-07-21 02:54,0
2022-07-23 00:20,1
2022-07-23 12:58,2
2022-07-23 21:00,3
2022-07-25 16:31,4
2022-08-04 02:53,5
2022-08-05 22:00,4
2022-08-06 17:43,5
2022-08-07 10:50,4
2022-08-08 21:21,5
2022-08-13 04:22,4
2022-08-14 04:23,3
2022-08-19 19:57,2
2022-08-20 23:59,1
2022-08-22 04:00,2
2022-08-22 19:56,4
2022-08-22 23:57,5
2022-08-23 15:58,6
2022-08-23 19:57,7
2022-08-23 23:58,9
2022-08-24 13:00,10
2022-08-24 17:02,11
2022-08-25 00:00,13
2022-08-25 04:01,14
2022-08-26 03:54,15
2022-08-26 22:32,19
2022-08-27 03:30,20
2022-08-27 13:24,21
2022-08-27 19:26,23
2022-08-28 00:23,25
2022-08-28 05:20,26

どうやらこの「一致したキーの数」は積算値で、何かのタイミングでリセットされるようですが、詳細はわかりません。おそらく「一致したキーの数」が変化したタイミングで exposure_data.json も書き変わるのだと思います。


以下は古い記述です。

接触チェックの記録

iPhoneにCOCOAをインストールしている。接触チェックの記録は,「設定」→「接触通知」→「接触のログ記録の状況」→「接触チェックの記録」→「接触チェックの記録を書き出す」でAirDropなどの手段で書き出すことができる。書き出されるのは ExposureChecks-YYYY-MM-DD.json のようなJSONファイルである。これを読んでみよう。

ExportVersion という項目があり,古いものは1,新しいものは2になっている。次のようにすれば接触情報(MatchCount が0以外)および毎日のキー数を表示できる:

#! /usr/bin/env python3

import sys
import json

def show(d):
    print()
    for k in ["Hash", "MatchCount", "Timestamp"]:
        print(k, d[k])

keycount = {}

for arg in sys.argv[1:]:
    with open(arg) as f:
        d = json.load(f)
    print(arg, "ExportVersion:", d["ExportVersion"])
    if d["ExportVersion"] == 1:
        for i in d["ExposureChecks"]:
            keycount[i["Timestamp"]] = keycount.get(i["Timestamp"], 0) + i["RandomIDCount"]
            if i["MatchCount"] != 0:
                show(i)
    elif d["ExportVersion"] == 2:
        for i in d["ExposureChecks"]:
            for j in i["Files"]:
                keycount[i["Timestamp"]] = keycount.get(i["Timestamp"], 0) + j["KeyCount"]
                if "MatchCount" in j and j["MatchCount"] != 0: # ENv2ではMatchCountはない
                    show(j)
    else:
        print(arg, "Unknown ExportVersion:", d["ExportVersion"])

print()
for k in sorted(keycount):
    print(k, keycount[k])

出力例:

ExposureChecks-2020-09-12.json ExportVersion: 2

Hash 6CBAA717FA2D584F2E8A77731C4B7D5294ED4B7E899EC3D4E54598BDCDE0400B
MatchCount 1
Timestamp 2020-09-12 01:15:24 +0900

2020-08-29 00:38:00 +0900 408
2020-08-30 00:41:07 +0900 178
2020-08-31 01:08:33 +0900 162
2020-09-01 00:48:51 +0900 422
2020-09-02 01:46:07 +0900 207
2020-09-03 03:01:08 +0900 328
2020-09-04 03:20:12 +0900 289
2020-09-05 03:50:32 +0900 315
2020-09-06 00:13:21 +0900 121
2020-09-07 00:22:34 +0900 58
2020-09-08 00:45:24 +0900 276
2020-09-09 00:54:57 +0900 209
2020-09-10 01:04:33 +0900 236
2020-09-11 03:08:06 +0900 481
2020-09-12 01:15:24 +0900 412

MatchCount が 0 でないハッシュ値を 鈴木健治さんの接触日シート別冊 または https://cacaotest.sakura.ne.jp で検索すれば接触日がわかる。同じことが COCOAログチェッカー でできる。

[COCOA 2] COCOA 2.0.0 beta の段階で ExposureChecks-*.json から MatchCount の欄が消えた。これは ENv2 を採用したためである。

COCOAの動作ログ

COCOAの動作ログについては COCOA 突如として初期化される問題(UserDataが壊れている) #16 に詳しい。

COCOAアプリを立ち上げて,左上の三本線メニュー→「アプリに関するお問い合わせ」→「動作情報を送信」→「動作情報を確認する」→「保存完了OK」,いったんCOCOAを閉じて「ファイル」アプリを立ち上げ,「接触確認アプリ」フォルダを開き,cocoa_log_*.zip というファイルを長押しし,「共有」でAirDropなどでパソコンに送る(zipファイルを長押ししないでタップするとiPhoneの中で展開される)。

このzipファイルを展開すると2週間分のcsvファイルが出てくる。中はCOCOAの動作ログである。

次のようなスクリプトでエラー行だけ表示することができる:

#! /usr/bin/env python3

import re
import sys
from zipfile import ZipFile

for arg in sys.argv[1:]:
    with ZipFile(arg) as z:
        for x in z.namelist():
            with z.open(x) as f:
                for r in f:
                    line = r.decode('utf-8')
                    if re.search('^.*?,"Error",', line):
                        print(line)

出力例:

"2021/02/01 13:09:59","Error","Fail to download files, ...(後略)...
"2021/02/01 17:12:55","Error","Failed to get terms update info., ...(後略)...
"2021/01/31 21:14:08","Error","Failed to check version., ...(後略)...