-
Notifications
You must be signed in to change notification settings - Fork 0
/
streamlit_app.py
191 lines (173 loc) · 8.75 KB
/
streamlit_app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
from graphviz import Digraph
import pandas as pd
from ast import literal_eval
from itertools import permutations
import streamlit as st
from course_path import node_str, has_edge, has_solid_path, add_prereq
import requests, io
from SessionState import get
def remove_unnecessary_edges(graph: Digraph, courses: pd.DataFrame, course_list: list, styles=None):
"""
`graph`から余分なエッジを取り除く。余分なエッジとは、コースAからコースBへ最低1つの他コースを経由する予習必須の
パスが存在するときにある、コースAからコースBへのエッジのこと。
"""
for p in permutations(course_list, r=2):
if has_edge(graph, p[0], p[1], styles):
for prereq in courses[courses['course_number'] == p[1]].iloc[0]['course_prepare_must']:
if has_solid_path(graph, courses, p[0], prereq):
try:
graph.body.remove(graph._edge % (p[0], p[1], ''))
except ValueError:
for style in styles:
try:
graph.body.remove(graph._edge % (p[0], p[1], ' [style={}]'.format(style)))
except ValueError:
continue
break
def main():
st.title('あなたにぴったりなコースをお届け!')
token = st.secrets['token'] # aidemy-contentsから読み取りができるトークン
owner = 'TeamAidemy'
repo = 'aidemy-contents'
path = 'contents-automator/all.csv'
branch = 'master'
# GitHubから最新のCSVをダウンロードしてDataFrameに変換する
r = requests.get('https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={branch}'.format(
owner=owner, repo=repo, path=path, branch=branch), headers={
'accept': 'application/vnd.github.v3.raw',
'authorization': 'token {}'.format(token)
}
)
courses = pd.read_csv(io.StringIO(r.text),
converters={'course_prepare_must': literal_eval, 'course_prepare_suggestion': literal_eval, 'course_suggestion': literal_eval})
# 可視化に必要な変数を定義
name = 'custom'
fontname = 'Noto Sans CJK JP'
path = Digraph(name=name, strict=True)
path.attr('graph', fontname=fontname)
path.attr('node', fontname=fontname)
path.attr('edge', fontname=fontname)
aidemy = 'https://aidemy.aidemy.jp/courses/'
node_colors = ['green', 'yellow', 'orange', 'red']
styles = ['dashed']
# コース間関係の全体像を表示する
if st.checkbox('全体像を表示'):
# 全体像のグラフを初期化する
dot = Digraph(name='all', strict=True)
dot.attr('graph', rankdir='LR')
dot.attr('graph', fontname=fontname)
dot.attr('node', fontname=fontname)
dot.attr('edge', fontname=fontname)
# 全てのコースをグラフに追加する
for _, row in courses.iterrows():
dot.node(str(row['course_number']), row['course_title'], color=node_colors[row['course_level'] - 1],
href=aidemy+str(row['course_number']))
edges = st.multiselect('表示したい関係性を選択してください', ['予習必須', 'できればやろう', '次におすすめ'])
# エッジを追加する
if '予習必須' in edges:
for _, row in courses.iterrows():
for prereq in row['course_prepare_must']:
dot.edge(str(prereq), str(row['course_number']))
if 'できればやろう' in edges:
for _, row in courses.iterrows():
for prereq in row['course_prepare_suggestion']:
dot.edge(str(prereq), str(row['course_number']), style='dashed')
if '次におすすめ' in edges:
for _, row in courses.iterrows():
for next in row['course_suggestion']:
if not has_edge(dot, str(row['course_number']), str(next), ['dashed']):
dot.edge(str(row['course_number']), str(next), style='dotted', color='purple')
# 余分なエッジを取り除く
remove_unnecessary_edges(dot, courses, courses['course_number'], styles)
# グラフを表示
st.graphviz_chart(dot)
st.markdown('$\\rightarrow$: 予習必須 $\dashrightarrow$: できればやろう')
# キーワードで目標となるコースを検索して、ユーザーに提示する
search_keys = st.text_input('キーワードで目標にしたいコースを検索')
if not search_keys:
st.stop()
search_keys = search_keys.replace(' ', ' ').split() # ORで検索
candidate_ids = set()
for key in search_keys:
# コースタイトルと概要とキーワードの中を検索
candidates = courses[courses['course_title'].str.contains(key, case=False) | courses['course_description'].str.contains(key, case=False) |
courses['keywords'].str.contains(key, case=False)]
candidate_ids.update(set(candidates['course_number']))
options = []
name_id_dict = {}
name_description_dict = {}
for candidate in candidate_ids:
row = courses[courses['course_number'] == candidate].iloc[0]
options.append(row['course_title'])
name_id_dict[row['course_title']] = candidate
name_description_dict[row['course_title']] = row['course_description']
selected = st.multiselect('受講したいコースを選んでください', options)
# 選択したコースを記録しつつ選択が終わるまで待機
target_courses = set()
target_course_names = set()
for s in selected:
target_course_names.add(s)
target_courses.add(name_id_dict[s])
st.text('選択済みのコース')
for c in target_course_names:
st.markdown('[{}]({}):{}'.format(c, aidemy + str(name_id_dict[c]), name_description_dict[c]))
if not st.button('選択完了'):
st.stop()
# 目標としたいコースと、その予習必須・おすすめコースをグラフに追加する
course_list = [] # グラフ内にある全てのコースのリスト
for target in target_courses:
row = courses[courses['course_number'] == target].iloc[0]
if node_str(str(target), row['course_title'], color=node_colors[row['course_level'] - 1],
href=aidemy+str(target), fontname=fontname) not in path.body:
course_list.append(target)
path.node(str(target), row['course_title'], color=node_colors[row['course_level'] - 1],
href=aidemy+str(target), fontname=fontname)
add_prereq(path, courses, target, node_colors, aidemy, course_list)
# 余分なエッジを取り除く
remove_unnecessary_edges(path, courses, course_list, styles)
# グラフ内にある全てのコースのリストから受講必須とおすすめのコースを判別する
required = set()
encouraged = set()
for course in course_list:
if course in target_courses:
continue
for target in target_courses:
if has_solid_path(path, courses, course, target):
required.add(course)
break
if course not in required:
encouraged.add(course)
# 受講必須とおすすめのコースをリストアップする
st.subheader('受講必須のコース')
if len(required) == 0:
st.text('なし')
for r in required:
row = courses[courses['course_number'] == r].iloc[0]
st.markdown('[{}]({})'.format(row['course_title'], aidemy + str(r)))
st.subheader('受講おすすめのコース')
if len(encouraged) == 0:
st.text('なし')
for e in encouraged:
row = courses[courses['course_number'] == e].iloc[0]
st.markdown('[{}]({})'.format(row['course_title'], aidemy + str(e)))
# 受講順序を示すグラフを表示する
st.subheader('おすすめの受講順序')
st.graphviz_chart(path)
st.markdown('$\\rightarrow$: 予習必須 $\dashrightarrow$: できればやろう')
if __name__ == "__main__":
# パスワードで認証する
session_state = get(password='')
authorized = False
if session_state.password != st.secrets['password']:
with st.form("パスワードを入力してください"):
session_state.password = st.text_input("パスワード", type='password')
submitted = st.form_submit_button("ログイン")
if submitted:
if session_state.password == st.secrets['password']:
authorized = True
else:
st.error('パスワードが間違っています')
if authorized:
main()
else:
main()