Skip to content

Instantly share code, notes, and snippets.

@akira-okumura
Last active June 1, 2020 04:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save akira-okumura/4dff7e7d02c16072f8bbbcc97fa1c50b to your computer and use it in GitHub Desktop.
Save akira-okumura/4dff7e7d02c16072f8bbbcc97fa1c50b to your computer and use it in GitHub Desktop.
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