5
5
import re
6
6
import typing
7
7
from collections import Counter
8
+ from dateutil import parser as date_parser
8
9
from pathlib import Path
9
10
from typing import Any , Callable , Dict , List , Optional , Set , TextIO , Tuple , Union , cast
10
11
from urllib .request import urlopen
26
27
add_built_in_prefixes_to_prefix_map ,
27
28
get_default_metadata ,
28
29
)
29
- from .sssom_datamodel import Mapping , MappingSet
30
+ from .sssom_datamodel import Mapping , MappingSet , MatchTypeEnum
30
31
from .sssom_document import MappingSetDocument
31
32
from .typehints import Metadata , MetadataType , PrefixMap
32
33
from .util import (
@@ -141,6 +142,24 @@ def read_obographs_json(
141
142
)
142
143
143
144
145
+ def read_snomed_icd10cm_map_tsv (
146
+ file_path : str ,
147
+ prefix_map : Dict [str , str ] = None ,
148
+ meta : Dict [str , str ] = None ,
149
+ ) -> MappingSetDataFrame :
150
+ """Parse special SNOMED ICD10CM mapping file and translates it into a MappingSetDataFrame.
151
+
152
+ :param file_path: The path to the obographs file
153
+ :param prefix_map: an optional prefix map
154
+ :param meta: an optional dictionary of metadata elements
155
+ :return: A SSSOM MappingSetDataFrame
156
+ """
157
+ raise_for_bad_path (file_path )
158
+ df = read_pandas (file_path )
159
+ df2 = from_snomed_icd10cm_map_tsv (df , prefix_map = prefix_map , meta = meta )
160
+ return df2
161
+
162
+
144
163
def _get_prefix_map_and_metadata (
145
164
prefix_map : Optional [PrefixMap ] = None , meta : Optional [MetadataType ] = None
146
165
) -> Metadata :
@@ -501,6 +520,144 @@ def from_obographs(
501
520
return to_mapping_set_dataframe (mdoc )
502
521
503
522
523
+ def from_snomed_icd10cm_map_tsv (
524
+ df : pd .DataFrame ,
525
+ prefix_map : Optional [PrefixMap ] = None ,
526
+ meta : Optional [MetadataType ] = None ,
527
+ ) -> MappingSetDataFrame :
528
+ """Convert a snomed_icd10cm_map dataframe to a MappingSetDataFrame.
529
+
530
+ :param df: A mappings dataframe
531
+ :param prefix_map: A prefix map
532
+ :param meta: A metadata dictionary
533
+ :return: MappingSetDataFrame
534
+
535
+ # Field descriptions
536
+ # - Taken from: doc_Icd10cmMapReleaseNotes_Current-en-US_US1000124_20210901.pdf
537
+ FIELD,DATA_TYPE,PURPOSE,Joe's comments
538
+ - id,UUID,A 128 bit unsigned integer, uniquely identifying the map record,
539
+ - effectiveTime,Time,Specifies the inclusive date at which this change becomes effective.,
540
+ - active,Boolean,Specifies whether the member’s state was active (=1) or inactive (=0) from the nominal release date
541
+ specified by the effectiveTime field.,
542
+ - moduleId,SctId,Identifies the member version’s module. Set to a child of 900000000000443000|Module| within the
543
+ metadata hierarchy.,The only value in the entire set is '5991000124107', which has label 'SNOMED CT to ICD-10-CM
544
+ rule-based mapping module' (
545
+ https://www.findacode.com/snomed/5991000124107--snomed-ct-to-icd-10-cm-rule-based-mapping-module.html).
546
+ - refSetId,SctId,Set to one of the children of the |Complex map type| concept in the metadata hierarchy.,The only
547
+ value in the entire set is '5991000124107', which has label 'ICD-10-CM complex map reference set' (
548
+ https://www.findacode.com/snomed/6011000124106--icd-10-cm-complex-map-reference-set.html).
549
+ - referencedComponentId,SctId,The SNOMED CT source concept ID that is the subject of the map record.,
550
+ - mapGroup,Integer,An integer identifying a grouping of complex map records which will designate one map target at
551
+ the time of map rule evaluation. Source concepts that require two map targets for classification will have two sets
552
+ of map groups.,
553
+ - mapPriority,Integer,Within a map group, the mapPriority specifies the order in which complex map records should be
554
+ evaluated to determine the correct map target.,
555
+ - mapRule,String,A machine-readable rule, (evaluating to either ‘true’ or ‘false’ at run-time) that indicates
556
+ whether this map record should be selected within its map group.,
557
+ - mapAdvice,String,Human-readable advice that may be employed by the software vendor to give an end-user advice on
558
+ selection of the appropriate target code. This includes a) a summary statement of the map rule logic, b) a statement
559
+ of any limitations of the map record and c) additional classification guidance for the coding professional.,
560
+ - mapTarget,String,The target ICD-10 classification code of the map record.,
561
+ - correlationId,SctId,A child of |Map correlation value| in the metadata hierarchy, identifying the correlation
562
+ between the SNOMED CT concept and the target code.,
563
+ - mapCategoryId,SctId,Identifies the SNOMED CT concept in the metadata hierarchy which is the MapCategory for the
564
+ associated map record. This is a subtype of 447634004 |ICD-10 Map Category value|.,
565
+ """
566
+ # https://www.findacode.com/snomed/447561005--snomed-ct-source-code-to-target-map-correlation-not-specified.html
567
+ match_type_snomed_unspecified_id = 447561005
568
+ prefix_map = _ensure_prefix_map (prefix_map )
569
+ ms = _init_mapping_set (meta )
570
+
571
+ mlist : List [Mapping ] = []
572
+ for _ , row in df .iterrows ():
573
+ mdict = {
574
+ 'subject_id' : f'SNOMED:{ row ["referencedComponentId" ]} ' ,
575
+ 'subject_label' : row ['referencedComponentName' ],
576
+
577
+ # 'predicate_id': 'skos:exactMatch',
578
+ # - mapCategoryId: can use for mapping predicate? Or is correlationId more suitable?
579
+ # or is there a SKOS predicate I can map to in case where predicate is unknown? I think most of these
580
+ # mappings are attempts at exact matches, but I can't be sure (at least not without using these fields
581
+ # to determine: mapGroup, mapPriority, mapRule, mapAdvice).
582
+ # mapCategoryId,mapCategoryName: Only these in set: 447637006 "MAP SOURCE CONCEPT IS PROPERLY CLASSIFIED",
583
+ # 447638001 "MAP SOURCE CONCEPT CANNOT BE CLASSIFIED WITH AVAILABLE DATA",
584
+ # 447639009 "MAP OF SOURCE CONCEPT IS CONTEXT DEPENDENT"
585
+ # 'predicate_modifier': '???',
586
+ # Description: Modifier for negating the prediate. See https://github.com/mapping-commons/sssom/issues/40
587
+ # Range: PredicateModifierEnum: (joe: only lists 'Not' as an option)
588
+ # Example: Not Negates the predicate, see documentation of predicate_modifier_enum
589
+ # - predicate_id <- mapAdvice?
590
+ # - predicate_modifier <- mapAdvice?
591
+ # mapAdvice: Pipe-delimited qualifiers. Ex:
592
+ # "ALWAYS Q71.30 | CONSIDER LATERALITY SPECIFICATION"
593
+ # "IF LISSENCEPHALY TYPE 3 FAMILIAL FETAL AKINESIA SEQUENCE SYNDROME CHOOSE Q04.3 | MAP OF SOURCE CONCEPT
594
+ # IS CONTEXT DEPENDENT"
595
+ # "MAP SOURCE CONCEPT CANNOT BE CLASSIFIED WITH AVAILABLE DATA"
596
+ 'predicate_id' : f'SNOMED:{ row ["mapCategoryId" ]} ' ,
597
+ 'predicate_label' : row ['mapCategoryName' ],
598
+
599
+ 'object_id' : f'ICD10CM:{ row ["mapTarget" ]} ' ,
600
+ 'object_label' : row ['mapTargetName' ],
601
+
602
+ # match_type <- mapRule?
603
+ # ex: TRUE: when "ALWAYS <code>" is in pipe-delimited list in mapAdvice, this always shows TRUE. Does this
604
+ # mean I could use skos:exactMatch in these cases?
605
+ # match_type <- correlationId?: This may look redundant, but I want to be explicit. In officially downloaded
606
+ # SNOMED mappings, all of them had correlationId of 447561005, which also happens to be 'unspecified'.
607
+ # If correlationId is indeed more appropriate for predicate_id, then I don't think there is a representative
608
+ # field for 'match_type'.
609
+ 'match_type' : MatchTypeEnum ('Unspecified' ) if row ['correlationId' ] == match_type_snomed_unspecified_id \
610
+ else MatchTypeEnum ('Unspecified' ),
611
+
612
+ 'mapping_date' : date_parser .parse (str (row ['effectiveTime' ])).date (),
613
+ 'other' : '|' .join ([f'{ k } ={ str (row [k ])} ' for k in [
614
+ 'id' ,
615
+ 'active' ,
616
+ 'moduleId' ,
617
+ 'refsetId' ,
618
+ 'mapGroup' ,
619
+ 'mapPriority' ,
620
+ 'mapRule' ,
621
+ 'mapAdvice' ,
622
+ ]]),
623
+
624
+ # More fields (https://mapping-commons.github.io/sssom/Mapping/):
625
+ # - subject_category: absent
626
+ # - author_id: can this be "SNOMED"?
627
+ # - author_label: can this be "SNOMED"?
628
+ # - reviewer_id: can this be "SNOMED"?
629
+ # - reviewer_label: can this be "SNOMED"?
630
+ # - creator_id: can this be "SNOMED"?
631
+ # - creator_label: can this be "SNOMED"?
632
+ # - license: Is this something that can be determined?
633
+ # - subject_source: URL of some official page for SNOMED version used?
634
+ # - subject_source_version: Is this knowable?
635
+ # - objectCategory <= mapRule?
636
+ # mapRule: ex: TRUE: when "ALWAYS <code>" is in pipe-delimited list in mapAdvice, this always shows TRUE.
637
+ # Does this mean I could use skos:exactMatch in these cases?
638
+ # object_category:
639
+ # objectCategory:
640
+ # Description: The conceptual category to which the subject belongs to. This can be a string denoting
641
+ # the category or a term from a controlled vocabulary.
642
+ # Example: UBERON:0001062 (The CURIE of the Uberon term for "anatomical entity".)
643
+ # - object_source: URL of some official page for ICD10CM version used?
644
+ # - object_source_version: would this be "10CM" as in "ICD10CM"? Or something else? Or nothing?
645
+ # - mapping_provider: can this be "SNOMED"?
646
+ # - mapping_cardinality: Could I determine 1:1 or 1:many or many:1 based on:
647
+ # mapGroup, mapPriority, mapRule, mapAdvice?
648
+ # - match_term_type: What is this?
649
+ # - see_also: Should this be a URL to the SNOMED term?
650
+ # - comment: Description: Free text field containing either curator notes or text generated by tool providing
651
+ # additional informative information.
652
+ }
653
+ mlist .append (_prepare_mapping (Mapping (** mdict )))
654
+
655
+ ms .mappings = mlist
656
+ _set_metadata_in_mapping_set (mapping_set = ms , metadata = meta )
657
+ doc = MappingSetDocument (mapping_set = ms , prefix_map = prefix_map )
658
+ return to_mapping_set_dataframe (doc )
659
+
660
+
504
661
# All from_* take as an input a python object (data frame, json, etc) and return a MappingSetDataFrame
505
662
# All read_* take as an input a a file handle and return a MappingSetDataFrame (usually wrapping a from_* method)
506
663
@@ -525,6 +682,9 @@ def get_parsing_function(input_format: Optional[str], filename: str) -> Callable
525
682
return read_alignment_xml
526
683
elif input_format == "obographs-json" :
527
684
return read_obographs_json
685
+ elif input_format == "snomed-icd10cm-map-tsv" :
686
+ return read_snomed_icd10cm_map_tsv
687
+
528
688
else :
529
689
raise Exception (f"Unknown input format: { input_format } " )
530
690
0 commit comments