Coverage for application/qaqc/vars/vars_qaqc_processor.py: 89%

422 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-17 17:51 +0000

1import requests 

2import sys 

3 

4from application.util.functions import * 

5from application.image_review.vars.vars_annotation_processor import VarsAnnotationProcessor 

6 

7 

8class VarsQaqcProcessor(VarsAnnotationProcessor): 

9 """ 

10 Filters and formats annotations for the various DARC QA/QC checks. 

11 """ 

12 

13 def __init__(self, sequence_names: list, vars_dive_url: str, vars_phylogeny_url: str): 

14 super().__init__(sequence_names, vars_dive_url, vars_phylogeny_url) 

15 self.videos = [] 

16 self.load_phylogeny() 

17 

18 def fetch_annotations(self, seq_name): 

19 """ 

20 Fetches annotations for a given sequence name from VARS 

21 """ 

22 print(f'Fetching annotations for sequence {seq_name} from VARS...', end='') 

23 sys.stdout.flush() 

24 

25 res = requests.get(url=f'{self.vars_dive_url}/{seq_name.replace(" ", "%20")}') 

26 dive_json = res.json() 

27 print('fetched!') 

28 

29 for video in dive_json['media']: 

30 if 'urn:imagecollection:org' not in video['uri']: 

31 self.videos.append({ 

32 'start_timestamp': parse_datetime(video['start_timestamp']), 

33 'uri': video['uri'].replace('http://hurlstor.soest.hawaii.edu/videoarchive', 'https://hurlvideo.soest.hawaii.edu'), 

34 'sequence_name': video['video_sequence_name'], 

35 'video_reference_uuid': video['video_reference_uuid'], 

36 }) 

37 return dive_json['annotations'] 

38 

39 def find_duplicate_associations(self): 

40 """ 

41 Finds annotations that have more than one of the same association besides s2 

42 """ 

43 for name in self.sequence_names: 

44 for annotation in self.fetch_annotations(name): 

45 if annotation.get('group') == 'localization': 

46 continue 

47 # get list of associations 

48 association_set = set() 

49 duplicate_associations = False 

50 for association in annotation['associations']: 

51 name = association['link_name'] 

52 if name not in association_set: 

53 if name != 's2': 

54 association_set.add(name) 

55 else: 

56 duplicate_associations = True 

57 break 

58 if duplicate_associations: 

59 self.working_records.append(annotation) 

60 self.sort_records(self.process_working_records(self.videos)) 

61 

62 def find_missing_s1(self): 

63 """ 

64 Finds annotations that are missing s1 (ignores 'none' records) 

65 """ 

66 for name in self.sequence_names: 

67 for annotation in self.fetch_annotations(name): 

68 if annotation['concept'] == 'none' or annotation.get('group') == 'localization': 

69 continue 

70 s1 = get_association(annotation, 's1') 

71 if not s1: 

72 self.working_records.append(annotation) 

73 self.sort_records(self.process_working_records(self.videos)) 

74 

75 def find_identical_s1_s2(self): 

76 """ 

77 Finds annotations that have an s2 association that is the same as its s1 association 

78 """ 

79 for name in self.sequence_names: 

80 for annotation in self.fetch_annotations(name): 

81 if annotation.get('group') == 'localization': 

82 continue 

83 s2s = [] 

84 s1 = '' 

85 for association in annotation['associations']: 

86 if association['link_name'] == 's1': 

87 s1 = association['to_concept'] 

88 elif association['link_name'] == 's2': 

89 s2s.append(association['to_concept']) 

90 if s1 in s2s: 

91 self.working_records.append(annotation) 

92 self.sort_records(self.process_working_records(self.videos)) 

93 

94 def find_duplicate_s2(self): 

95 """ 

96 Finds annotations that have multiple s2 associations with the same value 

97 """ 

98 for name in self.sequence_names: 

99 for annotation in self.fetch_annotations(name): 

100 if annotation.get('group') == 'localization': 

101 continue 

102 duplicate_s2s = False 

103 s2_set = set() 

104 for association in annotation['associations']: 

105 if association['link_name'] == 's2': 

106 if association['to_concept'] in s2_set: 

107 duplicate_s2s = True 

108 break 

109 else: 

110 s2_set.add(association['to_concept']) 

111 if duplicate_s2s: 

112 self.working_records.append(annotation) 

113 self.sort_records(self.process_working_records(self.videos)) 

114 

115 def find_missing_upon_substrate(self): 

116 """ 

117 Finds annotations that have an upon association that is not an organism, but the 'upon' is not present in s1 or 

118 any s2 

119 """ 

120 for name in self.sequence_names: 

121 for annotation in self.fetch_annotations(name): 

122 if annotation.get('group') == 'localization': 

123 continue 

124 upon = None 

125 missing_upon = False 

126 for association in annotation['associations']: 

127 if association['link_name'] == 'upon': 

128 if (association['to_concept'] and association['to_concept'][0].isupper()) \ 

129 or association['to_concept'].startswith('dead'): 

130 # 'upon' is an organism, don't need it to be in s1/s2 

131 pass 

132 else: 

133 # 'upon' should be in s1 or s2 

134 upon = association['to_concept'] 

135 break 

136 if upon: 

137 missing_upon = True 

138 for association in annotation['associations']: 

139 if (association['link_name'] == 's1' or association['link_name'] == 's2') \ 

140 and association['to_concept'] == upon: 

141 missing_upon = False 

142 break 

143 if missing_upon: 

144 self.working_records.append(annotation) 

145 self.sort_records(self.process_working_records(self.videos)) 

146 

147 def find_mismatched_substrates(self): 

148 """ 

149 Finds annotations that occur at the same timestamp (same second) but have different substrates 

150 """ 

151 for name in self.sequence_names: 

152 annotations_with_same_timestamp = {} 

153 sorted_annotations = sorted(self.fetch_annotations(name), key=lambda d: d['recorded_timestamp']) 

154 # loop through all annotations, add ones with same timestamp to dict 

155 i = 0 

156 while i < len(sorted_annotations) - 2: 

157 if sorted_annotations[i].get('group') == 'localization': 

158 i += 1 

159 continue 

160 base_timestamp = sorted_annotations[i]['recorded_timestamp'][:19] 

161 base_annotation = sorted_annotations[i] 

162 i += 1 

163 while sorted_annotations[i]['recorded_timestamp'][:19] == base_timestamp: 

164 if sorted_annotations[i].get('group') != 'localization': 

165 if base_timestamp not in annotations_with_same_timestamp.keys(): 

166 annotations_with_same_timestamp[base_timestamp] = [base_annotation] 

167 annotations_with_same_timestamp[base_timestamp].append(sorted_annotations[i]) 

168 i += 1 

169 # loop through each annotation that shares the same timestamp, compare substrates 

170 for timestamp_key in annotations_with_same_timestamp.keys(): 

171 base_substrates = {'s2': set()} 

172 check_substrates = {'s2': set()} 

173 for association in annotations_with_same_timestamp[timestamp_key][0]['associations']: 

174 if association['link_name'] == 's1': 

175 base_substrates['s1'] = association['to_concept'] 

176 if association['link_name'] == 's2': 

177 base_substrates['s2'].add(association['to_concept']) 

178 for i in range(1, len(annotations_with_same_timestamp[timestamp_key])): 

179 for association in annotations_with_same_timestamp[timestamp_key][i]['associations']: 

180 if association['link_name'] == 's1': 

181 check_substrates['s1'] = association['to_concept'] 

182 if association['link_name'] == 's2': 

183 check_substrates['s2'].add(association['to_concept']) 

184 if base_substrates != check_substrates: 

185 for annotation in annotations_with_same_timestamp[timestamp_key]: 

186 self.working_records.append(annotation) 

187 self.sort_records(self.process_working_records(self.videos)) 

188 

189 def find_missing_upon(self): 

190 """ 

191 Finds annotations that are missing upon (ignores 'none' records) 

192 """ 

193 for name in self.sequence_names: 

194 for annotation in self.fetch_annotations(name): 

195 if annotation['concept'] == 'none' or annotation.get('group') == 'localization': 

196 continue 

197 if not get_association(annotation, 'upon'): 

198 self.working_records.append(annotation) 

199 self.sort_records(self.process_working_records(self.videos)) 

200 

201 def get_num_records_missing_ancillary_data(self): 

202 """ 

203 Finds number of annotations that are missing ancillary data 

204 """ 

205 num_records_missing = 0 

206 for name in self.sequence_names: 

207 for annotation in self.fetch_annotations(name): 

208 if 'ancillary_data' not in annotation.keys(): 

209 num_records_missing += 1 

210 return num_records_missing 

211 

212 def find_missing_ancillary_data(self): 

213 """ 

214 Finds annotations that are missing ancillary data (can be very slow) 

215 """ 

216 for name in self.sequence_names: 

217 for annotation in self.fetch_annotations(name): 

218 if 'ancillary_data' not in annotation.keys(): 

219 self.working_records.append(annotation) 

220 self.sort_records(self.process_working_records(self.videos)) 

221 

222 def find_id_refs_different_concept_name(self): 

223 """ 

224 Finds annotations with the same ID reference that have different concept names 

225 """ 

226 for name in self.sequence_names: 

227 id_ref_names = {} # dict of {id_ref: {name_1, name_2}} to check for more than one name 

228 id_ref_annotations = {} # dict of all annotations per id_ref: {id_ref: [annotation_1, annotation_2]} 

229 for annotation in self.fetch_annotations(name): 

230 if annotation.get('group') == 'localization': 

231 continue 

232 for association in annotation['associations']: 

233 if association['link_name'] == 'identity-reference': 

234 if association['link_value'] not in id_ref_names.keys(): 

235 id_ref_names[association['link_value']] = set() 

236 id_ref_annotations[association['link_value']] = [] 

237 id_ref_names[association['link_value']].add(annotation['concept']) 

238 id_ref_annotations[association['link_value']].append(annotation) 

239 break 

240 for id_ref, name_set in id_ref_names.items(): 

241 if len(name_set) > 1: 

242 for annotation in id_ref_annotations[id_ref]: 

243 self.working_records.append(annotation) 

244 self.sort_records(self.process_working_records(self.videos)) 

245 

246 def find_id_refs_conflicting_associations(self): 

247 """ 

248 Finds annotations with the same ID reference that have conflicting associations 

249 """ 

250 to_concepts = ['s1', 's2', 'upon', 'size', 'habitat', 'megahabitat', 'sampled-by'] 

251 for name in self.sequence_names: 

252 id_ref_associations = {} # dict of {id_ref: {ass_1_name: ass_1_val, ass_2_name: ass_2_val}} 

253 id_ref_annotations = {} # dict of all annotations per id_ref: {id_ref: [annotation_1, annotation_2]} 

254 for annotation in self.fetch_annotations(name): 

255 if annotation.get('group') == 'localization': 

256 continue 

257 id_ref = get_association(annotation, 'identity-reference') 

258 if id_ref: 

259 current_id_ref = id_ref['link_value'] 

260 if current_id_ref not in id_ref_associations.keys(): 

261 id_ref_associations[current_id_ref] = { 

262 'flag': False, # we'll set this to true if we find any conflicting associations 

263 's2': set(), # s2, sampled-by, and sample-reference are allowed to have 

264 'sampled-by': set(), # more than one association 

265 'sample-reference': set(), 

266 } 

267 id_ref_annotations[current_id_ref] = [annotation] 

268 # populate id_ref dict with all associations 

269 for ass in annotation['associations']: 

270 if ass['link_name'] == 'guide-photo': 

271 pass 

272 elif ass['link_name'] == 's2' or ass['link_name'] == 'sampled-by': 

273 id_ref_associations[current_id_ref][ass['link_name']].add(ass['to_concept']) 

274 elif ass['link_name'] == 'sample-reference': 

275 id_ref_associations[current_id_ref][ass['link_name']].add(ass['link_value']) 

276 else: 

277 id_ref_associations[current_id_ref][ass['link_name']] = \ 

278 ass['link_value'] if ass['link_name'] not in to_concepts else ass['to_concept'] 

279 else: 

280 # check current association values vs those saved 

281 id_ref_annotations[current_id_ref].append(annotation) 

282 temp_s2_set = set() 

283 temp_sampled_by_set = set() 

284 temp_sample_ref_set = set() 

285 for ass in annotation['associations']: 

286 if ass['link_name'] == 'guide-photo': 

287 pass 

288 elif ass['link_name'] == 's2': 

289 temp_s2_set.add(ass['to_concept']) 

290 elif ass['link_name'] == 'sampled-by': 

291 temp_sampled_by_set.add(ass['to_concept']) 

292 elif ass['link_name'] == 'sample-reference': 

293 temp_sample_ref_set.add(ass['link_value']) 

294 else: 

295 if ass['link_name'] in to_concepts: 

296 if ass['link_name'] in id_ref_associations[current_id_ref].keys(): 

297 # cases like 'guide-photo' will only be present on one record 

298 if id_ref_associations[current_id_ref][ass['link_name']] != ass['to_concept']: 

299 id_ref_associations[current_id_ref]['flag'] = True 

300 break 

301 else: 

302 id_ref_associations[current_id_ref][ass['link_name']] = ass['to_concept'] 

303 else: 

304 if ass['link_name'] in id_ref_associations[current_id_ref].keys(): 

305 if id_ref_associations[current_id_ref][ass['link_name']] != ass['link_value']: 

306 id_ref_associations[current_id_ref]['flag'] = True 

307 break 

308 else: 

309 id_ref_associations[current_id_ref][ass['link_name']] = ass['link_value'] 

310 if temp_s2_set != id_ref_associations[current_id_ref]['s2'] \ 

311 or temp_sampled_by_set != id_ref_associations[current_id_ref]['sampled-by'] \ 

312 or temp_sample_ref_set != id_ref_associations[current_id_ref]['sample-reference']: 

313 id_ref_associations[current_id_ref]['flag'] = True 

314 for id_ref in id_ref_associations.keys(): 

315 if id_ref_associations[id_ref]['flag']: 

316 for annotation in id_ref_annotations[id_ref]: 

317 self.working_records.append(annotation) 

318 self.sort_records(self.process_working_records(self.videos)) 

319 

320 def find_blank_associations(self): 

321 """ 

322 Finds all records that have associations with a link value of "" 

323 """ 

324 for name in self.sequence_names: 

325 for annotation in self.fetch_annotations(name): 

326 if annotation.get('group') == 'localization': 

327 continue 

328 for association in annotation['associations']: 

329 if association['link_value'] == '' and association['to_concept'] == 'self': 

330 self.working_records.append(annotation) 

331 self.sort_records(self.process_working_records(self.videos)) 

332 

333 def find_suspicious_hosts(self): 

334 """ 

335 Finds annotations that have an upon that is the same concept as itself 

336 """ 

337 for name in self.sequence_names: 

338 for annotation in self.fetch_annotations(name): 

339 if annotation.get('group') == 'localization': 

340 continue 

341 upon = get_association(annotation, 'upon') 

342 if upon and upon['to_concept'] == annotation['concept']: 

343 self.working_records.append(annotation) 

344 self.sort_records(self.process_working_records(self.videos)) 

345 

346 def find_missing_expected_association(self): 

347 """ 

348 Finds annotations that are expected to be upon another organism, but are not. This is a very slow test because 

349 before it can begin, we must retrieve the taxa from VARS for every record (unlike the other tests, we can't 

350 filter beforehand). 

351 

352 If more concepts need to be added for this check, simply add them to the appropriate list below: 

353 

354 Example: To add the order 'order123' to the list, change the declaration below from: 

355 

356 orders = ['Comatulida'] 

357 

358 to: 

359 

360 orders = ['Comatulida', 'order123'] 

361 

362 If a list does not exist, declare a new list and add it to the conditional: 

363 

364 Example: To add the subfamily 'subfam123' to the check, add a new list named 'subfamilies': 

365 

366 subfamilies = ['subfam123'] 

367 

368 Then add the new list to the conditional: 

369 

370 ... 

371 or ('family' in record.keys() and record['family'] in families) 

372 or ('subfamily' in record.keys() and record['subfamily'] in subfamilies) <<< ADD THIS LINE 

373 or ('genus' in record.keys() and record['genus'] in genera) 

374 ... 

375 

376 If you want the new addition to be highlighted in the table on the webpage, add the name to the ranksToHighlight 

377 list in vars/qaqc.js, at ~line 340 

378 """ 

379 classes = ['Ophiuroidea'] 

380 orders = ['Comatulida'] 

381 infraorders = ['Anomura', 'Caridea'] 

382 families = ['Goniasteridae', 'Poecilasmatidae', 'Parazoanthidae', 'Tubulariidae', 'Amphianthidae', 'Actinoscyphiidae'] 

383 genera = ['Henricia'] 

384 concepts = ['Hydroidolina'] 

385 for name in self.sequence_names: 

386 for annotation in self.fetch_annotations(name): 

387 if annotation.get('group') == 'localization': 

388 continue 

389 self.working_records.append(annotation) 

390 self.sort_records(self.process_working_records(self.videos)) 

391 temp_records = self.final_records 

392 self.final_records = [] 

393 for record in temp_records: 

394 if record.get('class') in classes \ 

395 or record.get('order') in orders \ 

396 or record.get('infraorder') in infraorders \ 

397 or record.get('family') in families \ 

398 or record.get('genus') in genera \ 

399 or record.get('concept') in concepts: 

400 upon = get_association(record, 'upon') 

401 if upon and upon['to_concept'][0].islower() and 'dead' not in upon['to_concept']: 

402 self.final_records.append(record) 

403 

404 def find_long_host_associate_time_diff(self): 

405 greater_than_one_min = {} 

406 greater_than_five_mins = {} 

407 not_found = [] 

408 for name in self.sequence_names: 

409 sorted_annotations = sorted(self.fetch_annotations(name), key=lambda d: d['recorded_timestamp']) 

410 for i in range(len(sorted_annotations)): 

411 associate_record = sorted_annotations[i] 

412 upon = get_association(sorted_annotations[i], 'upon') 

413 if upon and upon['to_concept'] and upon['to_concept'][0].isupper(): 

414 # the associate's 'upon' is an organism 

415 host_concept_name = upon['to_concept'] 

416 observation_time = extract_recorded_datetime(associate_record) 

417 found = False 

418 for j in range(i + 10, -1, -1): 

419 """  

420 Checks backward, looking for the most recent host w/ matching name. We start at i + 10 because  

421 there can be multiple records with the exact same timestamp, and one of those records could be  

422 the 'upon' 

423 """ 

424 # to catch index out of range exception 

425 while j >= len(sorted_annotations): 

426 j -= 1 

427 host_record = sorted_annotations[j] 

428 host_time = extract_recorded_datetime(host_record) 

429 if host_time > observation_time or i == j: 

430 # host record won't be recorded after associate record, ignore this record 

431 # i == j: record shouldn't be associated with itself, ignore 

432 pass 

433 else: 

434 if host_record['concept'] == host_concept_name: 

435 # the host record's name is equal to the host concept name (associate's 'upon' name) 

436 found = True 

437 time_diff = observation_time - host_time 

438 if time_diff.seconds > 300: 

439 greater_than_five_mins[associate_record['observation_uuid']] = time_diff 

440 self.working_records.append(associate_record) 

441 elif time_diff.seconds > 60: 

442 greater_than_one_min[associate_record['observation_uuid']] = time_diff 

443 self.working_records.append(associate_record) 

444 break 

445 if not found: 

446 not_found.append(associate_record['observation_uuid']) 

447 self.working_records.append(associate_record) 

448 self.sort_records(self.process_working_records(self.videos)) 

449 for uuid in greater_than_one_min.keys(): 

450 next((x for x in self.final_records if x['observation_uuid'] == uuid), None)['status'] = \ 

451 'Time between record and closest previous matching host record greater than one minute ' \ 

452 f'({greater_than_one_min[uuid].seconds} seconds)' 

453 for uuid in greater_than_five_mins.keys(): 

454 next((x for x in self.final_records if x['observation_uuid'] == uuid), None)['status'] = \ 

455 'Time between record and closest previous matching host record greater than five minutes ' \ 

456 f'({greater_than_five_mins[uuid].seconds // 60 % 60} mins, {greater_than_five_mins[uuid].seconds % 60} seconds)' 

457 for uuid in not_found: 

458 next((x for x in self.final_records if x['observation_uuid'] == uuid), None)['status'] = \ 

459 f'Host not found in previous records' 

460 

461 def find_num_bounding_boxes(self): 

462 bounding_box_counts = {} 

463 total_count_annos = 0 

464 total_count_boxes = 0 

465 for name in self.sequence_names: 

466 for annotation in self.fetch_annotations(name): 

467 total_count_annos += 1 

468 if annotation['concept'] not in bounding_box_counts.keys(): 

469 bounding_box_counts[annotation['concept']] = { 

470 'boxes': 0, 

471 'annos': 0, 

472 } 

473 bounding_box_counts[annotation['concept']]['annos'] += 1 

474 if get_association(annotation, 'bounding box'): 

475 total_count_boxes += 1 

476 bounding_box_counts[annotation['concept']]['boxes'] += 1 

477 sorted_box_counts = dict(sorted(bounding_box_counts.items())) 

478 self.final_records.append({ 

479 'total_count_annos': total_count_annos, 

480 'total_count_boxes': total_count_boxes, 

481 'bounding_box_counts': sorted_box_counts, 

482 }) 

483 

484 def find_localizations_without_bounding_boxes(self): 

485 """ 

486 Finds records that are in the "localization" group but do not contain a bounding box association. Also finds 

487 records that have a bounding box association but are not in the "localization" group. 

488 """ 

489 for name in self.sequence_names: 

490 for annotation in self.fetch_annotations(name): 

491 has_box = False 

492 for association in annotation['associations']: 

493 if association['link_name'] == 'bounding box': 

494 has_box = True 

495 break 

496 if annotation.get('group') == 'localization': 

497 if not has_box: 

498 self.working_records.append(annotation) 

499 elif has_box: 

500 self.working_records.append(annotation) 

501 self.sort_records(self.process_working_records(self.videos)) 

502 

503 def find_unique_fields(self): 

504 def load_dict(field_name, unique_dict, individual_count): 

505 if field_name not in unique_dict.keys(): 

506 unique_dict[field_name] = {} 

507 unique_dict[field_name]['records'] = 1 

508 unique_dict[field_name]['individuals'] = individual_count 

509 else: 

510 unique_dict[field_name]['records'] += 1 

511 unique_dict[field_name]['individuals'] += individual_count 

512 

513 unique_concept_names = {} 

514 unique_concept_upons = {} 

515 unique_substrate_combinations = {} 

516 unique_comments = {} 

517 unique_condition_comments = {} 

518 unique_megahabitats = {} 

519 unique_habitats = {} 

520 unique_habitat_comments = {} 

521 unique_id_certainty = {} 

522 unique_occurrence_remarks = {} 

523 

524 for name in self.sequence_names: 

525 for annotation in self.fetch_annotations(name): 

526 substrates = [] 

527 upon = None 

528 comment = None 

529 condition_comment = None 

530 megahabitat = None 

531 habitat = None 

532 habitat_comment = None 

533 id_certainty = None 

534 occurrence_remark = None 

535 individual_count = 1 

536 

537 for association in annotation['associations']: 

538 match association['link_name']: 

539 case 's1' | 's2': 

540 substrates.append(association['to_concept']) 

541 case 'upon': 

542 upon = association['to_concept'] 

543 case 'comment': 

544 comment = association['link_value'] 

545 case 'condition-comment': 

546 condition_comment = association['link_value'] 

547 case 'megahabitat': 

548 megahabitat = association['to_concept'] 

549 case 'habitat': 

550 habitat = association['to_concept'] 

551 case 'habitat-comment': 

552 habitat_comment = association['link_value'] 

553 case 'identity-certainty': 

554 id_certainty = association['link_value'] 

555 case 'occurrence-remark': 

556 occurrence_remark = association['link_value'] 

557 case 'population-quantity': 

558 if association['link_value'] != '': 

559 individual_count = int(association['link_value']) 

560 case 'categorical-abundance': 

561 match association['link_value']: 

562 case '11-20': 

563 individual_count = 15 

564 case '21-50': 

565 individual_count = 35 

566 case '51-100': 

567 individual_count = 75 

568 case '\u003e100': 

569 individual_count = 100 

570 

571 if substrates is not None: 

572 substrates.sort() 

573 substrates = ', '.join(substrates) 

574 

575 load_dict(annotation['concept'], unique_concept_names, individual_count) 

576 load_dict(f'{annotation["concept"]}:{upon}', unique_concept_upons, individual_count) 

577 load_dict(substrates, unique_substrate_combinations, individual_count) 

578 load_dict(comment, unique_comments, individual_count) 

579 load_dict(condition_comment, unique_condition_comments, individual_count) 

580 load_dict(megahabitat, unique_megahabitats, individual_count) 

581 load_dict(habitat, unique_habitats, individual_count) 

582 load_dict(habitat_comment, unique_habitat_comments, individual_count) 

583 load_dict(id_certainty, unique_id_certainty, individual_count) 

584 load_dict(occurrence_remark, unique_occurrence_remarks, individual_count) 

585 

586 self.final_records.append({'concept-names': unique_concept_names}) 

587 self.final_records.append({'concept-upon-combinations': unique_concept_upons}) 

588 self.final_records.append({'substrate-combinations': unique_substrate_combinations}) 

589 self.final_records.append({'comments': unique_comments}) 

590 self.final_records.append({'condition-comments': unique_condition_comments}) 

591 self.final_records.append({'megahabitats': unique_megahabitats}) 

592 self.final_records.append({'habitats': unique_habitats}) 

593 self.final_records.append({'habitat-comments': unique_habitat_comments}) 

594 self.final_records.append({'identity-certainty': unique_id_certainty}) 

595 self.final_records.append({'occurrence-remarks': unique_occurrence_remarks})