Last active
June 1, 2020 04:38
-
-
Save akira-okumura/4dff7e7d02c16072f8bbbcc97fa1c50b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import graphviz | |
import datetime | |
def reformat(line): | |
values = line.split('\t') | |
case_number = int(values[0]) | |
if values[3] == '春日市': | |
# 春日市 format e.g., case 659 | |
# 例目 年代 性別 居住地 職業 発症日 現在の症状 濃厚接触者 | |
sex = values[2].replace('性', '') | |
age = int(values[1].replace('歳代', '').replace('代', '').replace('10歳未満', '0')) | |
note = [] | |
job = values[4] | |
elif values[3].find('福岡市') == 0: | |
# 福岡市 format e.g., case 663 | |
# 例目 年代 性別 居住地 家族構成 職業 行動歴 発症日 症状 特記事項 | |
sex = values[2].replace('性', '') | |
age = int(values[1].replace('歳代', '').replace('代', '').replace('10歳未満', '0')) | |
note = values[9].split(',') | |
job = values[5] | |
else: | |
# 北九州市 format | |
# 例目 行政区 年代 性別 職業等 渡航歴 濃厚接触者 備考 | |
sex = values[3].replace('性', '') | |
if values[2] == '調査中': | |
age = None | |
else: | |
age = int(values[2].replace('歳代', '').replace('代', '').replace('10歳未満', '0')) | |
note = values[7].split(',') | |
job = values[4] | |
return [case_number, sex, age, note, job] | |
data = (('2020-05-23', ('659 60代 女性 春日市 無職 なし 症状なし 家族:1名、その他:調査中', | |
'660 小倉北区 30歳代 男 小倉北特別支援学校講師 なし 同僚、医療スタッフ ・入院中', | |
'661 若松区 50歳代 女 上下水道局西部工事事務所 なし 家族、同僚 ・入院中', | |
'662 八幡西区 20歳代 女 大学生 なし 家族、友人 ・入院中')), | |
('2020-05-24', ('663 50代 女 福岡市博多区 単身 自営業 調査中 調査中 倦怠感 ・496例目と同じ人物(再陽性者),・感染症指定医療機関に入院中', | |
'664 門司区 60歳代 女 無職 なし 家族 ・入院中', | |
'665 八幡西区 80歳代 男 無職 なし 家族 ・死亡', | |
'666 行橋市 80歳代 女 無職 なし 調査中 ・入院中')), | |
('2020-05-25', ('667 門司区 80歳代 女 無職 なし 家族、医療スタッフ ・入院中', | |
'668 門司区 60歳代 女 無職 なし 家族、訪問看護スタッフ、介護サービス利用者・スタッフ ・入院中', | |
'669 門司区 70歳代 男 無職 なし 家族 ・入院中', | |
'670 小倉北区 80歳代 男 無職 なし 家族 ・入院中', | |
'671 小倉南区 10歳代 男 高校生 なし 調査中 ・自宅待機中', | |
'672 門司区 50歳代 女 小倉北特別支援学校教諭 なし 調査中 ・660例目の濃厚接触者,・自宅待機中')), | |
('2020-05-26', ('673 小倉南区 80歳代 男 無職 なし 家族、医療スタッフ ・入院中', | |
'674 戸畑区 20歳代 女 会社員 なし 同居人、同僚 ・入院中')), | |
('2020-05-27', ('675 若松区 70歳代 男 無職 なし 家族 ・入院中', | |
'676 小倉南区 70歳代 女 無職 なし 家族 ・自宅待機中', | |
'677 行橋市 80歳代 男 無職 なし 家族 ・入院中', | |
'678 小倉南区 20歳代 女 医療スタッフ なし 調査中 ・670例目の濃厚接触者,・入院中', | |
'679 小倉北区 20歳代 女 医療スタッフ なし エステサロン従業員 ・670例目の濃厚接触者,・入院中', | |
'680 八幡西区 80歳代 女 無職 なし 調査中 ・入院中', | |
'681 八幡西区 40歳代 女 医療スタッフ 調査中 家族 ・666例目の濃厚接触者', | |
'682 小倉南区 30歳代 女 医療スタッフ 調査中 無し ・666例目の濃厚接触者')), | |
('2020-05-28', ('683 八幡西区 30歳代 男 自営業 なし 家族、同僚、友人、同僚の家族、友人の家族 ・入院中', | |
'684 小倉南区 30歳代 女 医療スタッフ なし 家族、友人 ・入院中', | |
'685 中間市 70歳代 男 無職 なし 家族、医療スタッフ ・入院中', | |
'686 小倉南区 80歳代 男 無職 なし 家族、医療スタッフ ・679例目の濃厚接触者,・入院中', | |
'687 行橋市 20歳代 女 医療スタッフ 調査中 調査中 ・678例目の濃厚接触者,・入院中', | |
'688 小倉南区 10歳代 男 中学生 なし 家族、友人とその家族 ・自宅待機中', | |
'689 直方市 30歳代 男 介護施設職員 調査中 調査中 ・680例目の濃厚接触者,・入院中', | |
'690 小倉南区 10歳代 女 小学生 なし 家族、友人、先生 ・684例目の濃厚接触者,・入院中', | |
'691 小倉北区 30歳代 男 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・自宅待機中', | |
'692 小倉北区 30歳代 女 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・自宅待機中', | |
'693 小倉南区 30歳代 女 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・自宅待機中', | |
'694 下関市 50歳代 女 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・入院中', | |
'695 門司区 40歳代 女 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・自宅待機中', | |
'696 門司区 30歳代 女 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・自宅待機中', | |
'697 小倉北区 20歳代 女 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・自宅待機中', | |
'698 下関市 40歳代 女 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・入院中', | |
'699 小倉南区 30歳代 男 医療スタッフ 調査中 調査中 ・668例目の濃厚接触者,・自宅待機中', | |
'700 八幡東区 30歳代 男 介護施設職員 なし 家族 ・668例目の濃厚接触者,・自宅待機中', | |
'701 小倉北区 50歳代 男 北九州市消防職員 なし 家族 ・670例目の濃厚接触者,・入院中', | |
'702 若松区 20歳代 女 エステサロン従業員 なし 家族、エステサロン利用者・スタッフ ・679例目の濃厚接触者,・入院中', | |
'703 八幡西区 70歳代 女 会社員 なし 同僚 ・681例目の濃厚接触者,・入院中')), | |
('2020-05-29', ('704 八幡西区 30歳代 男 無職 なし 家族、親戚 ', | |
'705 小倉北区 10歳代 男 中学生 なし 家族、友人 ', | |
'706 戸畑区 70歳代 男 会社員 なし 家族、同僚 ', | |
'707 門司区 60歳代 男 介護施設職員 調査中 家族 ・668例目の濃厚接触者', | |
'708 戸畑区 90歳代 男 調査中 調査中 調査中 ', | |
'709 小倉南区 80歳代 女 調査中 調査中 調査中 ', | |
'710 八幡西区 90歳代 女 調査中 調査中 調査中 ', | |
'711 小倉南区 30歳代 男 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'712 小倉南区 30歳代 女 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'713 小倉北区 20歳代 女 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'714 小倉北区 20歳代 男 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'715 小倉北区 70歳代 男 調査中 調査中 調査中 ・687例目の濃厚接触者', | |
'716 小倉北区 80歳代 女 調査中 調査中 調査中 ・687例目の濃厚接触者', | |
'717 小倉南区 30歳代 男 医療スタッフ 調査中 調査中 ・687例目の濃厚接触者', | |
'718 小倉北区 20歳代 男 医療スタッフ 調査中 調査中 ・687例目の濃厚接触者', | |
'719 築上郡 40歳代 女 医療スタッフ 調査中 調査中 ・687例目の濃厚接触者', | |
'720 遠賀郡 60歳代 女 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'721 八幡西区 20歳代 男 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'722 八幡西区 60歳代 女 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'723 八幡西区 90歳代 女 調査中 調査中 調査中 ・680、689例目の濃厚接触者', | |
'724 八幡西区 40歳代 女 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'725 若松区 50歳代 男 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'726 八幡西区 40歳代 女 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'727 八幡西区 20歳代 男 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'728 八幡東区 40歳代 女 介護施設職員 調査中 調査中 ・680、689例目の濃厚接触者', | |
'729 八幡西区 80歳代 女 調査中 調査中 調査中 ・680、689例目の濃厚接触者')), | |
('2020-05-30', ('730 70代 男 福岡市南区 - - 入所中 調査中 調査中 ・609例目と同じ人物(再陽性者),・感染症指定医療機関に入院中', | |
'731 小倉北区 20歳代 男 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'732 小倉北区 30歳代 女 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'733 小倉南区 20歳代 男 医療スタッフ 調査中 調査中 686例目の濃厚接触者', | |
'734 小倉南区 30歳代 女 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'735 小倉南区 20歳代 男 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'736 小倉北区 20歳代 男 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'737 小倉南区 20歳代 男 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'738 小倉南区 20歳代 女 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'739 小倉南区 40歳代 女 医療スタッフ 調査中 調査中 ・686例目の濃厚接触者', | |
'740 戸畑区 40歳代 女 無職 なし 調査中 ', | |
'741 八幡東区 30歳代 男 小倉北特別支援学校教諭 なし なし ・660例目、672例目の同僚', | |
'742 八幡西区 30歳代 男 会社員 なし 家族 ', | |
'743 小倉南区 10歳未満 男 小学生 なし 家族、友人 ・683例目の濃厚接触者', | |
'744 門司区 70歳代 男 無職 なし 調査中 ・668例目の濃厚接触者', | |
'745 門司区 30歳代 女 調査中 なし 調査中 ', | |
'746 八幡西区 80歳代 男 調査中 なし 調査中 ')), | |
('2020-05-31', ('747 豊前市 20歳代 女 医療スタッフ なし 家族 ・686例目の濃厚接触者', | |
'748 福岡市 40歳代 男 医療スタッフ 調査中 調査中 ・714例目の濃厚接触者', | |
'749 小倉南区 10歳代 男 中学生 なし 家族 ・688例目の濃厚接触者', | |
'750 小倉南区 30歳代 男 医療スタッフ 調査中 調査中 ・715例目の濃厚接触者', | |
'751 小倉北区 20歳代 女 医療スタッフ 調査中 調査中 ・入院中', | |
'752 八幡西区 80歳代 男 調査中 調査中 調査中 ・入院中', | |
'753 八幡西区 調査中 女 調査中 調査中 調査中 ・710例目の濃厚接触者', | |
'754 小倉北区 10歳代 男 中学生 調査中 調査中 ・705例目の濃厚接触者', | |
'755 小倉南区 10歳代 女 小学生 調査中 調査中 ・690例目の濃厚接触者', | |
'756 小倉南区 10歳代 女 小学生 調査中 調査中 ・690例目の濃厚接触者', | |
'757 小倉南区 10歳代 男 小学生 調査中 調査中 ・690例目の濃厚接触者', | |
'758 小倉南区 10歳代 女 小学生 調査中 調査中 ・690例目の濃厚接触者')), | |
('2020-06-01', ())) | |
cases = {} | |
def register_cases(label_mode=0): | |
for daily_data in data: | |
date = datetime.date.fromisoformat(daily_data[0]) | |
for line in daily_data[1]: | |
idx, sex, age, note, job = reformat(line) | |
node_name = 'F%d' % idx | |
source_idx = [] | |
# find a cluster | |
for note_item in note: | |
if note_item.find('例目の濃厚接触者') >= 0: # cluster | |
sources = [int(x) for x in note_item.replace('・', '').split('例目の濃厚接触者')[0].split('、')] | |
for source in sources: | |
source_idx.append('F%d' % source) | |
elif note_item.find('例目の同僚') >= 0: # cluster | |
sources = [int(x.replace('例目', '')) for x in note_item.replace('・', '').split('例目の同僚')[0].split('、')] | |
for source in sources: | |
source_idx.append('F%d' % source) | |
cases['F%d' % idx] = {'node_name':node_name, 'date':date, 'note':note, 'sex':sex, 'age':age, | |
'job':job, 'source_idx':source_idx, 'linked':False} | |
def register_sources(): | |
for case in cases.values(): | |
for source_idx in case['source_idx']: | |
cases[source_idx]['linked'] = True | |
def make_date_ranks(): | |
date_ranks = {} | |
for case in sorted(cases.values(), key=lambda x:x['date']): | |
date = case['date'] | |
if date not in date_ranks.keys(): | |
date_ranks[date] = [case,] | |
else: | |
date_ranks[date].append(case) | |
return date_ranks | |
def make_date_nodes(date_ranks, label_mode): | |
date_nodes = [] | |
for date in date_ranks.keys(): | |
if str(date) == '2020-05-23': | |
with graph.subgraph() as s: | |
s.attr('node', fixedsize='1', width='4', fontsize='36') | |
s.node('date', label='陽性発表日', shape='plaintext', fontsize='36') | |
with graph.subgraph() as s: | |
s.attr(rank='same') | |
# default not to be seen | |
s.attr('node', fixedsize='1', width='0.5') | |
m, d = map(int, str(date).split('-')[1:]) | |
if (m, d) == (5, 23): | |
label = '%d/%d' % (m, d) | |
else: | |
label = '%d' % d | |
if d == 1: | |
s.node('date%s' % date, label=label, shape='plaintext', fontsize='24') | |
else: | |
s.node('date%s' % date, label=label, shape='plaintext', fontsize='20') | |
date_nodes.append('date%s' % date) | |
tmp1 = sorted([x for x in date_ranks[date] if x['node_name'].find('F') >= 0], key=lambda x:x['linked'], reverse=True) | |
date_ranks[date] = tmp1 | |
for case in date_ranks[date]: | |
s.attr('node', shape='octagon', color='black', style='diagonals', fontcolor='black') | |
if case['sex'].find('男') >= 0: | |
color = '#0000ff' # blue | |
elif case['sex'].find('女') >= 0: | |
color = '#ff0000' # red | |
else: | |
color='#000000' # black | |
if len(case['note']) > 0 and case['note'][0].find('例目と同じ人物(再陽性者)') >= 0: | |
s.attr('node', shape='doublecircle', style='', color=color, fontcolor='black') | |
elif len(case['note']) > 0 and case['note'][0].find('例目') >= 0: | |
s.attr('node', shape='square', style='', color=color, fontcolor='black') | |
else: | |
# probably OK to be categorized into '感染経路不明' | |
s.attr('node', shape='circle', style='filled', color=color+'AA', fontcolor='white') | |
print(case['node_name'], case['note']) | |
if label_mode == 0: # case number | |
s.node(case['node_name'], label=case['node_name'], fontname='Myriad Pro') | |
elif label_mode == 1: # age | |
if case['age'] == None: | |
label = '' | |
elif case['age'] == 0: | |
label = '<10歳' | |
else: | |
label = '%d代' % case['age'] | |
s.node(case['node_name'], label=label, fontname='Myriad Pro') | |
elif label_mode == 2: # job | |
label = case['job'] | |
n = len(label) | |
if n >= 4: | |
label = label[:n//2] + '\n' + label[n//2:] | |
s.node(case['node_name'], label=label, fontname='Myriad Pro', fontsize='10') | |
graph.edge('date', date_nodes[0], style='invis') | |
return date_nodes | |
def link_date_nodes(): | |
for i in range(len(date_nodes) - 1): | |
graph.edge(date_nodes[i], date_nodes[i + 1], style='invis') | |
def add_cluster_labels(): | |
label_edge = lambda a, b, c : graph.edge(a, b, label=c, style='') | |
graph = graphviz.Graph(engine='dot') | |
graph.attr('node', fontname='Hiragino UD Sans F StdN', fontsize='14') | |
graph.attr('edge', arrowhead='vee', arrowsize='0.5', dir='both') | |
graph.attr(nodesep='0.1', ranksep='0.12') | |
register_cases() | |
register_sources() | |
add_cluster_labels() | |
date_ranks = make_date_ranks() | |
date_nodes = make_date_nodes(date_ranks, label_mode=2) | |
for case in cases.values(): | |
if len(case['source_idx']) > 0: | |
source_idx = case['source_idx'] | |
node_name = case['node_name'] | |
for source in source_idx: | |
source_name = cases[source]['node_name'] | |
graph.edge(source_name, node_name) | |
link_date_nodes() | |
graph.graph_attr['rankdir'] = 'LR' | |
graph.view() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment