|
a |
|
b/MED277_bot.ipynb |
|
|
1 |
{ |
|
|
2 |
"cells": [ |
|
|
3 |
{ |
|
|
4 |
"cell_type": "code", |
|
|
5 |
"execution_count": 1, |
|
|
6 |
"metadata": { |
|
|
7 |
"ExecuteTime": { |
|
|
8 |
"end_time": "2018-06-11T22:22:28.421703Z", |
|
|
9 |
"start_time": "2018-06-11T22:22:26.327228Z" |
|
|
10 |
} |
|
|
11 |
}, |
|
|
12 |
"outputs": [], |
|
|
13 |
"source": [ |
|
|
14 |
"import pandas as pd\n", |
|
|
15 |
"from sklearn.externals import joblib\n", |
|
|
16 |
"import re\n", |
|
|
17 |
"from nltk.stem.snowball import SnowballStemmer\n", |
|
|
18 |
"from collections import defaultdict\n", |
|
|
19 |
"import operator\n", |
|
|
20 |
"import numpy as np\n", |
|
|
21 |
"import sklearn.feature_extraction.text as text\n", |
|
|
22 |
"from sklearn import decomposition\n", |
|
|
23 |
"from nltk.stem import PorterStemmer, WordNetLemmatizer\n", |
|
|
24 |
"from sklearn.decomposition import PCA\n", |
|
|
25 |
"from numpy.linalg import norm" |
|
|
26 |
] |
|
|
27 |
}, |
|
|
28 |
{ |
|
|
29 |
"cell_type": "code", |
|
|
30 |
"execution_count": 2, |
|
|
31 |
"metadata": { |
|
|
32 |
"ExecuteTime": { |
|
|
33 |
"end_time": "2018-06-11T22:22:30.024471Z", |
|
|
34 |
"start_time": "2018-06-11T22:22:30.000549Z" |
|
|
35 |
} |
|
|
36 |
}, |
|
|
37 |
"outputs": [], |
|
|
38 |
"source": [ |
|
|
39 |
"'''This function loads discharge summary data from MIMIC-III dataset NOTEEVENTS.csv.gz file.\n", |
|
|
40 |
"update the base_path to be the location of this zipped '''\n", |
|
|
41 |
"def load_data():\n", |
|
|
42 |
" ## Intitializing data paths\n", |
|
|
43 |
" base_path = r'D:\\ORGANIZATION\\UCSD_Life\\Work\\4. Quarter-3\\Subjects\\MED 277\\Project\\DATA\\\\'\n", |
|
|
44 |
" data_file = base_path+\"NOTEEVENTS.csv.gz\"\n", |
|
|
45 |
" \n", |
|
|
46 |
" ## Loading data frames from CSV file\n", |
|
|
47 |
" df = pd.read_csv(data_file, compression='gzip')\n", |
|
|
48 |
" \n", |
|
|
49 |
" ## Uncomment this to slice the size of dataset\n", |
|
|
50 |
" #df = df[:10000]\n", |
|
|
51 |
" \n", |
|
|
52 |
" ## Uncomment this to save processed data to the memory\n", |
|
|
53 |
" #joblib.dump(df,base_path+'data10.pkl')\n", |
|
|
54 |
" ## loading data frames from PKL memory\n", |
|
|
55 |
" #df1 = joblib.load(base_path+'data10.pkl')\n", |
|
|
56 |
" \n", |
|
|
57 |
" ## Filtering dataframe for \"Discharge summaries\" and \"TEXT\"\n", |
|
|
58 |
" df = df.loc[df['CATEGORY'] == 'Discharge summary'] #Extracting only discharge summaries\n", |
|
|
59 |
" df_text = df['TEXT']\n", |
|
|
60 |
" return df_text" |
|
|
61 |
] |
|
|
62 |
}, |
|
|
63 |
{ |
|
|
64 |
"cell_type": "markdown", |
|
|
65 |
"metadata": {}, |
|
|
66 |
"source": [ |
|
|
67 |
"## EXTRACT ALL THE TOPICS" |
|
|
68 |
] |
|
|
69 |
}, |
|
|
70 |
{ |
|
|
71 |
"cell_type": "code", |
|
|
72 |
"execution_count": 3, |
|
|
73 |
"metadata": { |
|
|
74 |
"ExecuteTime": { |
|
|
75 |
"end_time": "2018-06-11T22:22:32.608559Z", |
|
|
76 |
"start_time": "2018-06-11T22:22:32.603573Z" |
|
|
77 |
} |
|
|
78 |
}, |
|
|
79 |
"outputs": [], |
|
|
80 |
"source": [ |
|
|
81 |
"'''Method that processes the entire document string'''\n", |
|
|
82 |
"def process_text(txt):\n", |
|
|
83 |
" txt1 = re.sub('[\\n]',\" \",txt)\n", |
|
|
84 |
" txt1 = re.sub('[^A-Za-z \\.]+', '', txt1)\n", |
|
|
85 |
" \n", |
|
|
86 |
" return txt1" |
|
|
87 |
] |
|
|
88 |
}, |
|
|
89 |
{ |
|
|
90 |
"cell_type": "code", |
|
|
91 |
"execution_count": 4, |
|
|
92 |
"metadata": { |
|
|
93 |
"ExecuteTime": { |
|
|
94 |
"end_time": "2018-06-11T22:22:33.596917Z", |
|
|
95 |
"start_time": "2018-06-11T22:22:33.586970Z" |
|
|
96 |
} |
|
|
97 |
}, |
|
|
98 |
"outputs": [], |
|
|
99 |
"source": [ |
|
|
100 |
"'''Method that processes the document string not considering separate lines'''\n", |
|
|
101 |
"def process(txt):\n", |
|
|
102 |
" txt1 = re.sub('[\\n]',\" \",txt)\n", |
|
|
103 |
" txt1 = re.sub('[^A-Za-z ]+', '', txt1)\n", |
|
|
104 |
" \n", |
|
|
105 |
" _wrds = txt1.split()\n", |
|
|
106 |
" stemmer = SnowballStemmer(\"english\") ## May use porter stemmer\n", |
|
|
107 |
" wrds = [stemmer.stem(wrd) for wrd in _wrds]\n", |
|
|
108 |
" return wrds" |
|
|
109 |
] |
|
|
110 |
}, |
|
|
111 |
{ |
|
|
112 |
"cell_type": "code", |
|
|
113 |
"execution_count": 5, |
|
|
114 |
"metadata": { |
|
|
115 |
"ExecuteTime": { |
|
|
116 |
"end_time": "2018-06-11T22:22:34.412734Z", |
|
|
117 |
"start_time": "2018-06-11T22:22:34.402790Z" |
|
|
118 |
} |
|
|
119 |
}, |
|
|
120 |
"outputs": [], |
|
|
121 |
"source": [ |
|
|
122 |
"'''Method that processes raw string and gets a processes list containing lines'''\n", |
|
|
123 |
"def get_processed_sentences(snt_txt):\n", |
|
|
124 |
" snt_list = []\n", |
|
|
125 |
" for line in snt_txt.split('.'):\n", |
|
|
126 |
" line = line.strip()\n", |
|
|
127 |
" if len(line.split()) >= 5:\n", |
|
|
128 |
" snt_list.append(line)\n", |
|
|
129 |
" return snt_list" |
|
|
130 |
] |
|
|
131 |
}, |
|
|
132 |
{ |
|
|
133 |
"cell_type": "code", |
|
|
134 |
"execution_count": 6, |
|
|
135 |
"metadata": { |
|
|
136 |
"ExecuteTime": { |
|
|
137 |
"end_time": "2018-06-11T22:22:35.078078Z", |
|
|
138 |
"start_time": "2018-06-11T22:22:35.051176Z" |
|
|
139 |
} |
|
|
140 |
}, |
|
|
141 |
"outputs": [], |
|
|
142 |
"source": [ |
|
|
143 |
"'''This method extracts topic from sentence'''\n", |
|
|
144 |
"def extract_topic(str_arg, num_topics = 1, num_top_words = 3):\n", |
|
|
145 |
" vectorizer = text.CountVectorizer(input='content', analyzer='word', lowercase=True, stop_words='english')\n", |
|
|
146 |
" try:\n", |
|
|
147 |
" dtm = vectorizer.fit_transform(str_arg.split())\n", |
|
|
148 |
" vocab = np.array(vectorizer.get_feature_names())\n", |
|
|
149 |
" \n", |
|
|
150 |
" #clf = decomposition.NMF(n_components=num_topics, random_state=1) ## topic extraction\n", |
|
|
151 |
" clf = decomposition.LatentDirichletAllocation(n_components=num_topics, learning_method='online')\n", |
|
|
152 |
" clf.fit_transform(dtm)\n", |
|
|
153 |
"\n", |
|
|
154 |
" topic_words = []\n", |
|
|
155 |
" for topic in clf.components_:\n", |
|
|
156 |
" word_idx = np.argsort(topic)[::-1][0:num_top_words] ##[::-1] reverses the list\n", |
|
|
157 |
" topic_words.append([vocab[i] for i in word_idx])\n", |
|
|
158 |
" return topic_words\n", |
|
|
159 |
" except:\n", |
|
|
160 |
" return None" |
|
|
161 |
] |
|
|
162 |
}, |
|
|
163 |
{ |
|
|
164 |
"cell_type": "code", |
|
|
165 |
"execution_count": 7, |
|
|
166 |
"metadata": { |
|
|
167 |
"ExecuteTime": { |
|
|
168 |
"end_time": "2018-06-11T22:22:35.804137Z", |
|
|
169 |
"start_time": "2018-06-11T22:22:35.794166Z" |
|
|
170 |
} |
|
|
171 |
}, |
|
|
172 |
"outputs": [], |
|
|
173 |
"source": [ |
|
|
174 |
"'''This method extracts topics of each sentence and returns a list'''\n", |
|
|
175 |
"def extract_topics_all(doc_string):\n", |
|
|
176 |
" #One entry per sentence in list\n", |
|
|
177 |
" doc_str = process_text(doc_string)\n", |
|
|
178 |
" doc_str = get_processed_sentences(doc_str)\n", |
|
|
179 |
" \n", |
|
|
180 |
" res = []\n", |
|
|
181 |
" for i in range (0, len(doc_str)):\n", |
|
|
182 |
" snd_str = doc_str[i].lower()\n", |
|
|
183 |
" #print(\"Sending ----------------------------\",snd_str,\"==========\",len(snd_str))\n", |
|
|
184 |
" tmp_topic = extract_topic(snd_str, num_topics = 2, num_top_words = 1)\n", |
|
|
185 |
" for top in tmp_topic:\n", |
|
|
186 |
" for wrd in top:\n", |
|
|
187 |
" res.append(wrd)\n", |
|
|
188 |
" return res" |
|
|
189 |
] |
|
|
190 |
}, |
|
|
191 |
{ |
|
|
192 |
"cell_type": "code", |
|
|
193 |
"execution_count": 8, |
|
|
194 |
"metadata": { |
|
|
195 |
"ExecuteTime": { |
|
|
196 |
"end_time": "2018-06-11T22:22:36.470386Z", |
|
|
197 |
"start_time": "2018-06-11T22:22:36.462381Z" |
|
|
198 |
} |
|
|
199 |
}, |
|
|
200 |
"outputs": [], |
|
|
201 |
"source": [ |
|
|
202 |
"'''This function takes a dataframe and returns all the topics in the entire corpus'''\n", |
|
|
203 |
"def extract_corpus_topics(arg_df):\n", |
|
|
204 |
" all_topics = set()\n", |
|
|
205 |
" cnt = 1\n", |
|
|
206 |
" for txt in arg_df:\n", |
|
|
207 |
" all_topics = all_topics.union(extract_topics_all(txt))\n", |
|
|
208 |
" print(\"Processed \",cnt,\" records\")\n", |
|
|
209 |
" cnt += 1\n", |
|
|
210 |
" all_topics = list(all_topics)\n", |
|
|
211 |
" return all_topics" |
|
|
212 |
] |
|
|
213 |
}, |
|
|
214 |
{ |
|
|
215 |
"cell_type": "markdown", |
|
|
216 |
"metadata": {}, |
|
|
217 |
"source": [ |
|
|
218 |
"## GET A VECTORIZED REPRESENTATION OF ALL THE TOPICS" |
|
|
219 |
] |
|
|
220 |
}, |
|
|
221 |
{ |
|
|
222 |
"cell_type": "code", |
|
|
223 |
"execution_count": 9, |
|
|
224 |
"metadata": { |
|
|
225 |
"ExecuteTime": { |
|
|
226 |
"end_time": "2018-06-11T22:22:38.161868Z", |
|
|
227 |
"start_time": "2018-06-11T22:22:38.140924Z" |
|
|
228 |
} |
|
|
229 |
}, |
|
|
230 |
"outputs": [], |
|
|
231 |
"source": [ |
|
|
232 |
"'''data_set = words list per document.\n", |
|
|
233 |
" vocabulary = list of all the words present\n", |
|
|
234 |
" _vocab = dict of word counts for words in vocabulary'''\n", |
|
|
235 |
"def get_vocab_wrd_map(df_text):\n", |
|
|
236 |
" data_set = []\n", |
|
|
237 |
" vocabulary = []\n", |
|
|
238 |
" _vocab = defaultdict(int)\n", |
|
|
239 |
" for i in range(0,df_text.size):\n", |
|
|
240 |
" txt = process(df_text[i])\n", |
|
|
241 |
" data_set.append(txt)\n", |
|
|
242 |
"\n", |
|
|
243 |
" for wrd in txt:\n", |
|
|
244 |
" _vocab[wrd] += 1\n", |
|
|
245 |
"\n", |
|
|
246 |
" vocabulary = vocabulary + txt\n", |
|
|
247 |
" vocabulary = list(set(vocabulary))\n", |
|
|
248 |
"\n", |
|
|
249 |
" if(i%100 == 0):\n", |
|
|
250 |
" print(\"%5d records processed\"%(i))\n", |
|
|
251 |
" return data_set, vocabulary, _vocab" |
|
|
252 |
] |
|
|
253 |
}, |
|
|
254 |
{ |
|
|
255 |
"cell_type": "code", |
|
|
256 |
"execution_count": 10, |
|
|
257 |
"metadata": { |
|
|
258 |
"ExecuteTime": { |
|
|
259 |
"end_time": "2018-06-11T22:22:39.105476Z", |
|
|
260 |
"start_time": "2018-06-11T22:22:39.099498Z" |
|
|
261 |
} |
|
|
262 |
}, |
|
|
263 |
"outputs": [], |
|
|
264 |
"source": [ |
|
|
265 |
"'''vocab = return sorted list of most common words in vocabulary'''\n", |
|
|
266 |
"def get_common_vocab(num_arg, vocab):\n", |
|
|
267 |
" vocab = sorted(vocab.items(), key=operator.itemgetter(1), reverse=True)\n", |
|
|
268 |
" vocab = vocab[:num_arg]\n", |
|
|
269 |
" return vocab" |
|
|
270 |
] |
|
|
271 |
}, |
|
|
272 |
{ |
|
|
273 |
"cell_type": "code", |
|
|
274 |
"execution_count": 11, |
|
|
275 |
"metadata": { |
|
|
276 |
"ExecuteTime": { |
|
|
277 |
"end_time": "2018-06-11T22:22:39.851482Z", |
|
|
278 |
"start_time": "2018-06-11T22:22:39.839514Z" |
|
|
279 |
} |
|
|
280 |
}, |
|
|
281 |
"outputs": [], |
|
|
282 |
"source": [ |
|
|
283 |
"'''Convert vocabulary and most common words to map for faster access'''\n", |
|
|
284 |
"def get_vocab_map(vocabulary, vocab):\n", |
|
|
285 |
" vocab_map = {}\n", |
|
|
286 |
" for i in range(0,len(vocab)):\n", |
|
|
287 |
" vocab_map[vocab[i][0]] = i \n", |
|
|
288 |
" \n", |
|
|
289 |
" vocabulary_map = {}\n", |
|
|
290 |
" for i in range(0,len(vocabulary)):\n", |
|
|
291 |
" vocabulary_map[vocabulary[i]] = i\n", |
|
|
292 |
" \n", |
|
|
293 |
" return vocabulary_map, vocab_map" |
|
|
294 |
] |
|
|
295 |
}, |
|
|
296 |
{ |
|
|
297 |
"cell_type": "code", |
|
|
298 |
"execution_count": 12, |
|
|
299 |
"metadata": { |
|
|
300 |
"ExecuteTime": { |
|
|
301 |
"end_time": "2018-06-11T22:22:40.626409Z", |
|
|
302 |
"start_time": "2018-06-11T22:22:40.609455Z" |
|
|
303 |
} |
|
|
304 |
}, |
|
|
305 |
"outputs": [], |
|
|
306 |
"source": [ |
|
|
307 |
"'''This function returns n-gram context embedding for each word'''\n", |
|
|
308 |
"def get_embedding(word, data_set, vocab_map, wdw_size):\n", |
|
|
309 |
" embedding = [0]*len(vocab_map)\n", |
|
|
310 |
" for docs in data_set:\n", |
|
|
311 |
" for i in range(wdw_size, len(docs)-wdw_size):\n", |
|
|
312 |
" if docs[i] == word:\n", |
|
|
313 |
" for j in range(i-wdw_size, i-1):\n", |
|
|
314 |
" if docs[j] in vocab_map:\n", |
|
|
315 |
" embedding[vocab_map[docs[j]]] += 1\n", |
|
|
316 |
" for j in range(i+1, i+wdw_size):\n", |
|
|
317 |
" if docs[j] in vocab_map:\n", |
|
|
318 |
" embedding[vocab_map[docs[j]]] += 1\n", |
|
|
319 |
" total_words = sum(embedding)\n", |
|
|
320 |
" if total_words != 0:\n", |
|
|
321 |
" embedding[:] = [e/total_words for e in embedding]\n", |
|
|
322 |
" return embedding" |
|
|
323 |
] |
|
|
324 |
}, |
|
|
325 |
{ |
|
|
326 |
"cell_type": "code", |
|
|
327 |
"execution_count": 13, |
|
|
328 |
"metadata": { |
|
|
329 |
"ExecuteTime": { |
|
|
330 |
"end_time": "2018-06-11T22:22:41.413308Z", |
|
|
331 |
"start_time": "2018-06-11T22:22:41.405327Z" |
|
|
332 |
} |
|
|
333 |
}, |
|
|
334 |
"outputs": [], |
|
|
335 |
"source": [ |
|
|
336 |
"'''This is a helper function that returns n-gram embedding for all the topics in the corpus'''\n", |
|
|
337 |
"def get_embedding_all(all_topics, data_set, vocab_map, wdw_size):\n", |
|
|
338 |
" embeddings = []\n", |
|
|
339 |
" for i in range(0, len(all_topics)):\n", |
|
|
340 |
" embeddings.append(get_embedding(all_topics[i], data_set, vocab_map, wdw_size))\n", |
|
|
341 |
" return embeddings" |
|
|
342 |
] |
|
|
343 |
}, |
|
|
344 |
{ |
|
|
345 |
"cell_type": "markdown", |
|
|
346 |
"metadata": {}, |
|
|
347 |
"source": [ |
|
|
348 |
"## Get similarity function" |
|
|
349 |
] |
|
|
350 |
}, |
|
|
351 |
{ |
|
|
352 |
"cell_type": "code", |
|
|
353 |
"execution_count": 14, |
|
|
354 |
"metadata": { |
|
|
355 |
"ExecuteTime": { |
|
|
356 |
"end_time": "2018-06-11T22:22:42.876508Z", |
|
|
357 |
"start_time": "2018-06-11T22:22:42.868529Z" |
|
|
358 |
} |
|
|
359 |
}, |
|
|
360 |
"outputs": [], |
|
|
361 |
"source": [ |
|
|
362 |
"def cos_matrix_multiplication(matrix, vector):\n", |
|
|
363 |
" \"\"\"\n", |
|
|
364 |
" Calculating pairwise cosine distance using matrix vector multiplication.\n", |
|
|
365 |
" \"\"\"\n", |
|
|
366 |
" dotted = matrix.dot(vector)\n", |
|
|
367 |
" matrix_norms = np.linalg.norm(matrix, axis=1)\n", |
|
|
368 |
" vector_norm = np.linalg.norm(vector)\n", |
|
|
369 |
" matrix_vector_norms = np.multiply(matrix_norms, vector_norm)\n", |
|
|
370 |
" neighbors = np.divide(dotted, matrix_vector_norms)\n", |
|
|
371 |
" return neighbors" |
|
|
372 |
] |
|
|
373 |
}, |
|
|
374 |
{ |
|
|
375 |
"cell_type": "code", |
|
|
376 |
"execution_count": 15, |
|
|
377 |
"metadata": { |
|
|
378 |
"ExecuteTime": { |
|
|
379 |
"end_time": "2018-06-11T22:22:43.710277Z", |
|
|
380 |
"start_time": "2018-06-11T22:22:43.695318Z" |
|
|
381 |
} |
|
|
382 |
}, |
|
|
383 |
"outputs": [], |
|
|
384 |
"source": [ |
|
|
385 |
"'''This function generates most similar topic to a given embedding'''\n", |
|
|
386 |
"def get_most_similar_topics(embd, embeddings, all_topics, num_wrd=10):\n", |
|
|
387 |
" sim_top = []\n", |
|
|
388 |
" cos_sim = cos_matrix_multiplication(np.array(embeddings), embd)\n", |
|
|
389 |
" #closest_match = cos_sim.argsort()[-num_wrd:][::-1] ## This sorts all matches in order\n", |
|
|
390 |
" \n", |
|
|
391 |
" ## This just takes 80% and above similar matches\n", |
|
|
392 |
" idx = list(np.where(cos_sim > 0.9)[0])\n", |
|
|
393 |
" val = list(cos_sim[np.where(cos_sim > 0.9)])\n", |
|
|
394 |
" closest_match, list2 = (list(t) for t in zip(*sorted(zip(idx, val), reverse=True)))\n", |
|
|
395 |
" closest_match = np.array(closest_match)\n", |
|
|
396 |
" \n", |
|
|
397 |
" for i in range(0, closest_match.shape[0]):\n", |
|
|
398 |
" sim_top.append(all_topics[closest_match[i]])\n", |
|
|
399 |
" return sim_top" |
|
|
400 |
] |
|
|
401 |
}, |
|
|
402 |
{ |
|
|
403 |
"cell_type": "markdown", |
|
|
404 |
"metadata": {}, |
|
|
405 |
"source": [ |
|
|
406 |
"## Topic Modelling" |
|
|
407 |
] |
|
|
408 |
}, |
|
|
409 |
{ |
|
|
410 |
"cell_type": "code", |
|
|
411 |
"execution_count": 16, |
|
|
412 |
"metadata": { |
|
|
413 |
"ExecuteTime": { |
|
|
414 |
"end_time": "2018-06-11T22:22:44.978887Z", |
|
|
415 |
"start_time": "2018-06-11T22:22:44.972904Z" |
|
|
416 |
} |
|
|
417 |
}, |
|
|
418 |
"outputs": [], |
|
|
419 |
"source": [ |
|
|
420 |
"'''This function extracts matches for a regular expression in the text'''\n", |
|
|
421 |
"def get_regex_match(regex, str_arg):\n", |
|
|
422 |
" srch = re.search(regex,str_arg)\n", |
|
|
423 |
" if srch is not None:\n", |
|
|
424 |
" return srch.group(0).strip()\n", |
|
|
425 |
" else:\n", |
|
|
426 |
" return \"Not found\"" |
|
|
427 |
] |
|
|
428 |
}, |
|
|
429 |
{ |
|
|
430 |
"cell_type": "code", |
|
|
431 |
"execution_count": 17, |
|
|
432 |
"metadata": { |
|
|
433 |
"ExecuteTime": { |
|
|
434 |
"end_time": "2018-06-11T22:22:45.895464Z", |
|
|
435 |
"start_time": "2018-06-11T22:22:45.878481Z" |
|
|
436 |
} |
|
|
437 |
}, |
|
|
438 |
"outputs": [], |
|
|
439 |
"source": [ |
|
|
440 |
"'''This is a helper function that helps extracting answer to extraction type questions'''\n", |
|
|
441 |
"def extract(key,str_arg):\n", |
|
|
442 |
" if key == 'dob':\n", |
|
|
443 |
" return get_regex_match('Date of Birth:(.*)] ', str_arg)\n", |
|
|
444 |
" elif key == 'a_date':\n", |
|
|
445 |
" return get_regex_match('Admission Date:(.*)] ', str_arg)\n", |
|
|
446 |
" elif key == 'd_date':\n", |
|
|
447 |
" return get_regex_match('Discharge Date:(.*)]\\n', str_arg)\n", |
|
|
448 |
" elif key == 'sex':\n", |
|
|
449 |
" return get_regex_match('Sex:(.*)\\n', str_arg)\n", |
|
|
450 |
" elif key == 'service':\n", |
|
|
451 |
" return get_regex_match('Service:(.*)\\n', str_arg)\n", |
|
|
452 |
" elif key == 'allergy':\n", |
|
|
453 |
" return get_regex_match('Allergies:(.*)\\n(.*)\\n', str_arg)\n", |
|
|
454 |
" elif key == 'attdng':\n", |
|
|
455 |
" return get_regex_match('Attending:(.*)]\\n', str_arg)\n", |
|
|
456 |
" else:\n", |
|
|
457 |
" return \"I Don't know\"" |
|
|
458 |
] |
|
|
459 |
}, |
|
|
460 |
{ |
|
|
461 |
"cell_type": "code", |
|
|
462 |
"execution_count": 18, |
|
|
463 |
"metadata": { |
|
|
464 |
"ExecuteTime": { |
|
|
465 |
"end_time": "2018-06-11T22:22:46.788047Z", |
|
|
466 |
"start_time": "2018-06-11T22:22:46.771119Z" |
|
|
467 |
} |
|
|
468 |
}, |
|
|
469 |
"outputs": [], |
|
|
470 |
"source": [ |
|
|
471 |
"'''This method extracts topic from sentence'''\n", |
|
|
472 |
"def extract_topic(str_arg, num_topics = 1, num_top_words = 3):\n", |
|
|
473 |
" vectorizer = text.CountVectorizer(input='content', analyzer='word', lowercase=True, stop_words='english')\n", |
|
|
474 |
" dtm = vectorizer.fit_transform(str_arg.split())\n", |
|
|
475 |
" vocab = np.array(vectorizer.get_feature_names())\n", |
|
|
476 |
" \n", |
|
|
477 |
" #clf = decomposition.NMF(n_components=num_topics, random_state=1) ## topic extraction\n", |
|
|
478 |
" clf = decomposition.LatentDirichletAllocation(n_components=num_topics, learning_method='online')\n", |
|
|
479 |
" clf.fit_transform(dtm)\n", |
|
|
480 |
" \n", |
|
|
481 |
" topic_words = []\n", |
|
|
482 |
" for topic in clf.components_:\n", |
|
|
483 |
" word_idx = np.argsort(topic)[::-1][0:num_top_words] ##[::-1] reverses the list\n", |
|
|
484 |
" topic_words.append([vocab[i] for i in word_idx])\n", |
|
|
485 |
" return topic_words" |
|
|
486 |
] |
|
|
487 |
}, |
|
|
488 |
{ |
|
|
489 |
"cell_type": "code", |
|
|
490 |
"execution_count": 19, |
|
|
491 |
"metadata": { |
|
|
492 |
"ExecuteTime": { |
|
|
493 |
"end_time": "2018-06-11T22:22:47.747483Z", |
|
|
494 |
"start_time": "2018-06-11T22:22:47.740500Z" |
|
|
495 |
} |
|
|
496 |
}, |
|
|
497 |
"outputs": [], |
|
|
498 |
"source": [ |
|
|
499 |
"'''This method extracts topics in a question'''\n", |
|
|
500 |
"def extract_Q_topic(str_arg):\n", |
|
|
501 |
" try:\n", |
|
|
502 |
" return extract_topic(str_arg)\n", |
|
|
503 |
" except:\n", |
|
|
504 |
" return None\n", |
|
|
505 |
" ## Future Scope fix later for more comprehensive results" |
|
|
506 |
] |
|
|
507 |
}, |
|
|
508 |
{ |
|
|
509 |
"cell_type": "code", |
|
|
510 |
"execution_count": 20, |
|
|
511 |
"metadata": { |
|
|
512 |
"ExecuteTime": { |
|
|
513 |
"end_time": "2018-06-11T22:22:48.590228Z", |
|
|
514 |
"start_time": "2018-06-11T22:22:48.578259Z" |
|
|
515 |
} |
|
|
516 |
}, |
|
|
517 |
"outputs": [], |
|
|
518 |
"source": [ |
|
|
519 |
"def get_extract_map(key_wrd):\n", |
|
|
520 |
" ## A Stemmed mapping for simple extractions\n", |
|
|
521 |
" extract_map = {'birth':'dob', 'dob':'dob',\n", |
|
|
522 |
" 'admiss':'a_date', 'discharg':'d_date',\n", |
|
|
523 |
" 'sex':'sex', 'gender':'sex', 'servic':'service',\n", |
|
|
524 |
" 'allergi':'allergy', 'attend':'attdng'}\n", |
|
|
525 |
" if key_wrd in extract_map.keys():\n", |
|
|
526 |
" return extract_map[key_wrd]\n", |
|
|
527 |
" else:\n", |
|
|
528 |
" return None" |
|
|
529 |
] |
|
|
530 |
}, |
|
|
531 |
{ |
|
|
532 |
"cell_type": "code", |
|
|
533 |
"execution_count": 21, |
|
|
534 |
"metadata": { |
|
|
535 |
"ExecuteTime": { |
|
|
536 |
"end_time": "2018-06-11T22:22:49.521736Z", |
|
|
537 |
"start_time": "2018-06-11T22:22:49.504781Z" |
|
|
538 |
} |
|
|
539 |
}, |
|
|
540 |
"outputs": [], |
|
|
541 |
"source": [ |
|
|
542 |
"'''Method that generates the answer for text extraction questions'''\n", |
|
|
543 |
"def get_extracted_answer(topic_str, text):\n", |
|
|
544 |
" port = PorterStemmer()\n", |
|
|
545 |
" for i in range(0, len(topic_str)):\n", |
|
|
546 |
" rel_wrd = topic_str[i]\n", |
|
|
547 |
" for wrd in rel_wrd:\n", |
|
|
548 |
" key = get_extract_map(port.stem(wrd))\n", |
|
|
549 |
" if key is not None:\n", |
|
|
550 |
" return extract(key, text)\n", |
|
|
551 |
" return None" |
|
|
552 |
] |
|
|
553 |
}, |
|
|
554 |
{ |
|
|
555 |
"cell_type": "code", |
|
|
556 |
"execution_count": 22, |
|
|
557 |
"metadata": { |
|
|
558 |
"ExecuteTime": { |
|
|
559 |
"end_time": "2018-06-11T22:22:50.506103Z", |
|
|
560 |
"start_time": "2018-06-11T22:22:50.494136Z" |
|
|
561 |
} |
|
|
562 |
}, |
|
|
563 |
"outputs": [], |
|
|
564 |
"source": [ |
|
|
565 |
"'''This method extracts topics of each sentence and returns a list'''\n", |
|
|
566 |
"def get_topic_mapping(doc_string):\n", |
|
|
567 |
" #One entry per sentence in list\n", |
|
|
568 |
" doc_str = process_text(doc_string)\n", |
|
|
569 |
" doc_str = get_processed_sentences(doc_str)\n", |
|
|
570 |
" \n", |
|
|
571 |
" res = defaultdict(list)\n", |
|
|
572 |
" for i in range (0, len(doc_str)):\n", |
|
|
573 |
" snd_str = doc_str[i].lower()\n", |
|
|
574 |
" #print(\"Sending ----------------------------\",snd_str,\"==========\",len(snd_str))\n", |
|
|
575 |
" tmp_topic = extract_topic(snd_str, num_topics = 2, num_top_words = 1)\n", |
|
|
576 |
" for top in tmp_topic:\n", |
|
|
577 |
" for wrd in top:\n", |
|
|
578 |
" res[wrd].append(doc_str[i])\n", |
|
|
579 |
" return res" |
|
|
580 |
] |
|
|
581 |
}, |
|
|
582 |
{ |
|
|
583 |
"cell_type": "code", |
|
|
584 |
"execution_count": 23, |
|
|
585 |
"metadata": { |
|
|
586 |
"ExecuteTime": { |
|
|
587 |
"end_time": "2018-06-11T22:22:51.287014Z", |
|
|
588 |
"start_time": "2018-06-11T22:22:51.280036Z" |
|
|
589 |
} |
|
|
590 |
}, |
|
|
591 |
"outputs": [], |
|
|
592 |
"source": [ |
|
|
593 |
"def get_direct_answer(topic_str, topic_map):\n", |
|
|
594 |
" ## Maybe apply lemmatizer here\n", |
|
|
595 |
" for i in range(0, len(topic_str)):\n", |
|
|
596 |
" rel_wrd = topic_str[i]\n", |
|
|
597 |
" for wrd in rel_wrd:\n", |
|
|
598 |
" if wrd in topic_map.keys():\n", |
|
|
599 |
" return topic_map[wrd]\n", |
|
|
600 |
" return None" |
|
|
601 |
] |
|
|
602 |
}, |
|
|
603 |
{ |
|
|
604 |
"cell_type": "code", |
|
|
605 |
"execution_count": 24, |
|
|
606 |
"metadata": { |
|
|
607 |
"ExecuteTime": { |
|
|
608 |
"end_time": "2018-06-11T22:22:52.353164Z", |
|
|
609 |
"start_time": "2018-06-11T22:22:52.343190Z" |
|
|
610 |
} |
|
|
611 |
}, |
|
|
612 |
"outputs": [], |
|
|
613 |
"source": [ |
|
|
614 |
"def get_answer(topic, topic_map, embedding_short, all_topics, data_set, vocab_map, pca, wdw_size=5):\n", |
|
|
615 |
" ## Get most similar topics\n", |
|
|
616 |
" tpc_embedding = get_embedding(topic, data_set, vocab_map, wdw_size)\n", |
|
|
617 |
" tpc_embedding = pca.transform([tpc_embedding])\n", |
|
|
618 |
" sim_topics = get_most_similar_topics(tpc_embedding[0], embedding_short, all_topics, num_wrd = len(all_topics))\n", |
|
|
619 |
" for topic in sim_topics:\n", |
|
|
620 |
" if topic in topic_map.keys():\n", |
|
|
621 |
" return topic_map[topic]\n", |
|
|
622 |
" return None" |
|
|
623 |
] |
|
|
624 |
}, |
|
|
625 |
{ |
|
|
626 |
"cell_type": "code", |
|
|
627 |
"execution_count": 25, |
|
|
628 |
"metadata": { |
|
|
629 |
"ExecuteTime": { |
|
|
630 |
"end_time": "2018-06-11T22:22:53.157013Z", |
|
|
631 |
"start_time": "2018-06-11T22:22:53.150059Z" |
|
|
632 |
} |
|
|
633 |
}, |
|
|
634 |
"outputs": [], |
|
|
635 |
"source": [ |
|
|
636 |
"'''This function checks if the user input text is an instruction allowed in chatbot or not'''\n", |
|
|
637 |
"def is_instruction_option(str_arg):\n", |
|
|
638 |
" if str_arg == \"exit\" or str_arg == \"summary\" or str_arg == \"reveal\":\n", |
|
|
639 |
" return True\n", |
|
|
640 |
" else:\n", |
|
|
641 |
" return False" |
|
|
642 |
] |
|
|
643 |
}, |
|
|
644 |
{ |
|
|
645 |
"cell_type": "code", |
|
|
646 |
"execution_count": 26, |
|
|
647 |
"metadata": { |
|
|
648 |
"ExecuteTime": { |
|
|
649 |
"end_time": "2018-06-11T22:22:53.953950Z", |
|
|
650 |
"start_time": "2018-06-11T22:22:53.941983Z" |
|
|
651 |
} |
|
|
652 |
}, |
|
|
653 |
"outputs": [], |
|
|
654 |
"source": [ |
|
|
655 |
"def print_bot():\n", |
|
|
656 |
"\tprint(r\" _ _ _\")\n", |
|
|
657 |
"\tprint(r\" | o o |\")\n", |
|
|
658 |
"\tprint(r\" \\| = |/\")\n", |
|
|
659 |
"\tprint(r\" -------\")\n", |
|
|
660 |
"\tprint(r\" |||||||\")\n", |
|
|
661 |
"\tprint(r\" // \\\\\")\n", |
|
|
662 |
"\t\n", |
|
|
663 |
"def print_caption():\n", |
|
|
664 |
"\tprint(r\"\t||\\\\ || || ||= =||\")\n", |
|
|
665 |
"\tprint(r\"\t|| \\\\ || || ||= =||\")\n", |
|
|
666 |
"\tprint(r\"\t|| \\\\ || || ||\")\n", |
|
|
667 |
"\tprint(r\"\t|| \\\\|| ||_ _ _ ||\")" |
|
|
668 |
] |
|
|
669 |
}, |
|
|
670 |
{ |
|
|
671 |
"cell_type": "code", |
|
|
672 |
"execution_count": null, |
|
|
673 |
"metadata": { |
|
|
674 |
"ExecuteTime": { |
|
|
675 |
"start_time": "2018-06-11T22:24:04.067Z" |
|
|
676 |
} |
|
|
677 |
}, |
|
|
678 |
"outputs": [ |
|
|
679 |
{ |
|
|
680 |
"name": "stdout", |
|
|
681 |
"output_type": "stream", |
|
|
682 |
"text": [ |
|
|
683 |
"Loading data ... \n", |
|
|
684 |
"\n", |
|
|
685 |
"Getting Vocabulary ...\n", |
|
|
686 |
" 0 records processed\n", |
|
|
687 |
"Creating context ...\n", |
|
|
688 |
"Learning topics ...\n", |
|
|
689 |
"Processed 1 records\n", |
|
|
690 |
"Processed 2 records\n", |
|
|
691 |
"Processed 3 records\n", |
|
|
692 |
"Processed 4 records\n", |
|
|
693 |
"Processed 5 records\n", |
|
|
694 |
"Processed 6 records\n", |
|
|
695 |
"Processed 7 records\n", |
|
|
696 |
"Processed 8 records\n", |
|
|
697 |
"Processed 9 records\n", |
|
|
698 |
"Processed 10 records\n", |
|
|
699 |
"Processed 11 records\n", |
|
|
700 |
"Processed 12 records\n", |
|
|
701 |
"Processed 13 records\n", |
|
|
702 |
"Processed 14 records\n", |
|
|
703 |
"Processed 15 records\n", |
|
|
704 |
"Processed 16 records\n", |
|
|
705 |
"Processed 17 records\n", |
|
|
706 |
"Processed 18 records\n", |
|
|
707 |
"Processed 19 records\n", |
|
|
708 |
"Processed 20 records\n", |
|
|
709 |
"Processed 21 records\n", |
|
|
710 |
"Processed 22 records\n", |
|
|
711 |
"Processed 23 records\n", |
|
|
712 |
"Processed 24 records\n", |
|
|
713 |
"Processed 25 records\n", |
|
|
714 |
"Processed 26 records\n", |
|
|
715 |
"Processed 27 records\n", |
|
|
716 |
"Processed 28 records\n", |
|
|
717 |
"Processed 29 records\n", |
|
|
718 |
"Processed 30 records\n", |
|
|
719 |
"Processed 31 records\n", |
|
|
720 |
"Processed 32 records\n", |
|
|
721 |
"Processed 33 records\n", |
|
|
722 |
"Processed 34 records\n", |
|
|
723 |
"Processed 35 records\n", |
|
|
724 |
"Processed 36 records\n", |
|
|
725 |
"Processed 37 records\n", |
|
|
726 |
"Processed 38 records\n", |
|
|
727 |
"Processed 39 records\n", |
|
|
728 |
"Processed 40 records\n", |
|
|
729 |
"Processed 41 records\n", |
|
|
730 |
"Processed 42 records\n", |
|
|
731 |
"Processed 43 records\n", |
|
|
732 |
"Processed 44 records\n", |
|
|
733 |
"Processed 45 records\n", |
|
|
734 |
"Processed 46 records\n", |
|
|
735 |
"Processed 47 records\n", |
|
|
736 |
"Processed 48 records\n", |
|
|
737 |
"Processed 49 records\n", |
|
|
738 |
"Processed 50 records\n", |
|
|
739 |
"Getting Embeddings\n", |
|
|
740 |
"\t||\\\\ || || ||= =||\n", |
|
|
741 |
"\t|| \\\\ || || ||= =||\n", |
|
|
742 |
"\t|| \\\\ || || ||\n", |
|
|
743 |
"\t|| \\\\|| ||_ _ _ ||\n", |
|
|
744 |
" _ _ _\n", |
|
|
745 |
" | o o |\n", |
|
|
746 |
" \\| = |/\n", |
|
|
747 |
" -------\n", |
|
|
748 |
" |||||||\n", |
|
|
749 |
" // \\\\\n", |
|
|
750 |
"Bot:> I am online!\n", |
|
|
751 |
"Bot:> Type \"exit\" to switch to end a patient's session\n", |
|
|
752 |
"Bot:> Type \"summary\" to view patient's discharge summary\n", |
|
|
753 |
"Bot:> What is your Patient Id [0 to 49?]5\n", |
|
|
754 |
"Bot:> Reading Discharge Summary for Patient Id: 5\n", |
|
|
755 |
"Bot:> How can I help ?\n", |
|
|
756 |
"Person:>What is my date of birth?\n", |
|
|
757 |
"Bot:> Date of Birth: [**2109-10-8**]\n", |
|
|
758 |
"Bot:> How can I help ?\n", |
|
|
759 |
"Person:>When was I discharged?\n", |
|
|
760 |
"Bot:> Discharge Date: [**2172-3-8**]\n", |
|
|
761 |
"Bot:> How can I help ?\n", |
|
|
762 |
"Person:>What is my gender?\n", |
|
|
763 |
"Bot:> Sex: F\n", |
|
|
764 |
"Bot:> How can I help ?\n", |
|
|
765 |
"Person:>What are the services I had?\n", |
|
|
766 |
"Bot:> Service: NEUROSURGERY\n", |
|
|
767 |
"Bot:> How can I help ?\n", |
|
|
768 |
"Person:>Am I married?\n", |
|
|
769 |
"Bot:> ['Social History She is married']\n", |
|
|
770 |
"Bot:> How can I help ?\n", |
|
|
771 |
"Person:>How was my MRI?\n", |
|
|
772 |
"Bot:> ['Pertinent Results MRI Right middle cranial fossa mass likely represents a meningioma and is stable since MRI of', 'The previously seen midline nasopharyngeal mass has decreased in size since MRI of']\n", |
|
|
773 |
"Bot:> How can I help ?\n", |
|
|
774 |
"Person:>How can I make an appointment?\n", |
|
|
775 |
"Bot:> ['Make sure to take your steroid medication with meals or a glass of milk']\n", |
|
|
776 |
"Bot:> How can I help ?\n", |
|
|
777 |
"Person:>Do I have sinus?\n", |
|
|
778 |
"Bot:> ['She was found to hve a right cavernous sinus and nasopharyngeal mass', 'A gadoliniumenhanced head MRI performed at Hospital on showed a bright mass involving the cavernous sinus']\n", |
|
|
779 |
"Bot:> How can I help ?\n", |
|
|
780 |
"Person:>How should I take my medication?\n", |
|
|
781 |
"Bot:> ['If you are being sent home on steroid medication make sure you are taking a medication to protect your stomach Prilosec Protonix or Pepcid as these medications can cause stomach irritation', 'Pain or headache that is continually increasing or not relieved by pain medication']\n" |
|
|
782 |
] |
|
|
783 |
} |
|
|
784 |
], |
|
|
785 |
"source": [ |
|
|
786 |
"if __name__ == \"__main__\":\n", |
|
|
787 |
" print(\"Loading data ...\",\"\\n\")\n", |
|
|
788 |
" df_text = load_data()\n", |
|
|
789 |
" \n", |
|
|
790 |
" print(\"Getting Vocabulary ...\")\n", |
|
|
791 |
" data_set, vocabulary, _vocab = get_vocab_wrd_map(df_text)\n", |
|
|
792 |
" \n", |
|
|
793 |
" print(\"Creating context ...\")\n", |
|
|
794 |
" vocab = get_common_vocab(1000, _vocab)\n", |
|
|
795 |
" vocabulary_map, vocab_map = get_vocab_map(vocabulary, vocab)\n", |
|
|
796 |
" \n", |
|
|
797 |
" print(\"Learning topics ...\")\n", |
|
|
798 |
" all_topics = extract_corpus_topics(df_text)\n", |
|
|
799 |
" \n", |
|
|
800 |
" print(\"Getting Embeddings\")\n", |
|
|
801 |
" embeddings = get_embedding_all(all_topics, data_set, vocab_map, 5)\n", |
|
|
802 |
" \n", |
|
|
803 |
" pca = PCA(n_components=10)\n", |
|
|
804 |
" embedding_short = pca.fit_transform(embeddings)\n", |
|
|
805 |
" \n", |
|
|
806 |
" print_caption()\n", |
|
|
807 |
" print_bot()\n", |
|
|
808 |
" print(\"Bot:> I am online!\")\n", |
|
|
809 |
" print(\"Bot:> Type \\\"exit\\\" to switch to end a patient's session\")\n", |
|
|
810 |
" print(\"Bot:> Type \\\"summary\\\" to view patient's discharge summary\")\n", |
|
|
811 |
" while(True):\n", |
|
|
812 |
" while(True):\n", |
|
|
813 |
" try:\n", |
|
|
814 |
" pid = int(input(\"Bot:> What is your Patient Id [0 to \"+str(df_text.shape[0]-1)+\"?]\"))\n", |
|
|
815 |
" except:\n", |
|
|
816 |
" continue\n", |
|
|
817 |
" if pid < 0 or pid > df_text.shape[0]-1:\n", |
|
|
818 |
" print(\"Bot:> Patient Id out or range!\")\n", |
|
|
819 |
" continue\n", |
|
|
820 |
" else:\n", |
|
|
821 |
" print(\"Bot:> Reading Discharge Summary for Patient Id: \",pid)\n", |
|
|
822 |
" break\n", |
|
|
823 |
"\n", |
|
|
824 |
" personal_topics = extract_topics_all(df_text[pid])\n", |
|
|
825 |
" topic_mapping = get_topic_mapping(df_text[pid])\n", |
|
|
826 |
" \n", |
|
|
827 |
" ques = \"random starter\"\n", |
|
|
828 |
" while(ques != \"exit\"):\n", |
|
|
829 |
" ## Read Question\n", |
|
|
830 |
" ques = input(\"Bot:> How can I help ?\\nPerson:>\")\n", |
|
|
831 |
" \n", |
|
|
832 |
" ## Check if it is an instructional question\n", |
|
|
833 |
" if is_instruction_option(ques):\n", |
|
|
834 |
" if ques == \"summary\":\n", |
|
|
835 |
" print(\"Bot:> ================= Discharge Summary for Patient Id \",pid,\"\\n\")\n", |
|
|
836 |
" print(df_text[pid])\n", |
|
|
837 |
" elif ques == \"reveal\":\n", |
|
|
838 |
" print(topic_mapping, topic_mapping.keys())\n", |
|
|
839 |
" continue\n", |
|
|
840 |
" \n", |
|
|
841 |
" ## Extract Question topic\n", |
|
|
842 |
" topic_q = extract_Q_topic(ques)\n", |
|
|
843 |
" if topic_q is None:\n", |
|
|
844 |
" print(\"Bot:> I am a specialized NLP bot, please as a more specific question for me!\")\n", |
|
|
845 |
" continue\n", |
|
|
846 |
" ans = get_extracted_answer(topic_q, df_text[pid])\n", |
|
|
847 |
" if ans is not None:\n", |
|
|
848 |
" print(\"Bot:> \",ans)\n", |
|
|
849 |
" else:\n", |
|
|
850 |
" ans = get_direct_answer(topic_q, topic_mapping)\n", |
|
|
851 |
" if ans is not None:\n", |
|
|
852 |
" print(\"Bot:> \",ans)\n", |
|
|
853 |
" else:\n", |
|
|
854 |
" ans = get_answer(topic_q, topic_mapping, embedding_short, all_topics, data_set, vocab_map, pca, 5)\n", |
|
|
855 |
" if ans is not None:\n", |
|
|
856 |
" print(\"Bot:> \",ans)\n", |
|
|
857 |
" else:\n", |
|
|
858 |
" print(\"Bot:> Sorry but, I have no information on this topic!\")" |
|
|
859 |
] |
|
|
860 |
} |
|
|
861 |
], |
|
|
862 |
"metadata": { |
|
|
863 |
"kernelspec": { |
|
|
864 |
"display_name": "Python 3", |
|
|
865 |
"language": "python", |
|
|
866 |
"name": "python3" |
|
|
867 |
}, |
|
|
868 |
"language_info": { |
|
|
869 |
"codemirror_mode": { |
|
|
870 |
"name": "ipython", |
|
|
871 |
"version": 3 |
|
|
872 |
}, |
|
|
873 |
"file_extension": ".py", |
|
|
874 |
"mimetype": "text/x-python", |
|
|
875 |
"name": "python", |
|
|
876 |
"nbconvert_exporter": "python", |
|
|
877 |
"pygments_lexer": "ipython3", |
|
|
878 |
"version": "3.6.4" |
|
|
879 |
} |
|
|
880 |
}, |
|
|
881 |
"nbformat": 4, |
|
|
882 |
"nbformat_minor": 2 |
|
|
883 |
} |