fhir_scorecard 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +56 -0
- data/.simplecov +16 -0
- data/.travis.yml +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +201 -0
- data/README.md +94 -0
- data/Rakefile +25 -0
- data/fhir_scorecard.gemspec +15 -0
- data/lib/fhir_scorecard.rb +15 -0
- data/lib/rubrics/codes_exist.rb +72 -0
- data/lib/rubrics/codes_umls.rb +105 -0
- data/lib/rubrics/codes_umls_preferred_descriptions.rb +105 -0
- data/lib/rubrics/completeness.rb +61 -0
- data/lib/rubrics/cvx_immunizations.rb +32 -0
- data/lib/rubrics/cvx_meds.rb +73 -0
- data/lib/rubrics/datetimes_iso8601.rb +73 -0
- data/lib/rubrics/labs_loinc.rb +38 -0
- data/lib/rubrics/quantities_ucum.rb +69 -0
- data/lib/rubrics/references_resolve.rb +86 -0
- data/lib/rubrics/rxnorm_meds.rb +73 -0
- data/lib/rubrics/smoking_status.rb +48 -0
- data/lib/rubrics/snomed_conditions.rb +32 -0
- data/lib/rubrics/vital_signs.rb +64 -0
- data/lib/rubrics.rb +23 -0
- data/lib/scorecard.rb +46 -0
- data/lib/tasks/tasks.rake +207 -0
- data/lib/terminology.rb +135 -0
- data/logs/.keep +0 -0
- data/terminology/.keep +0 -0
- data/test/test_helper.rb +9 -0
- data/test/unit/basic_test.rb +52 -0
- data/test/unit/codes_exist_test.rb +169 -0
- data/test/unit/terminology_test.rb +91 -0
- metadata +90 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
module FHIR
|
2
|
+
class RxNormMeds < FHIR::Rubrics
|
3
|
+
|
4
|
+
# Medications should be coded with RxNorm
|
5
|
+
rubric :rxnorm_meds do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:validated_fields => 0
|
9
|
+
}
|
10
|
+
# Medication.code (CodeableConcept)
|
11
|
+
# MedicationAdministration.medicationCodeableConcept / medicationReference
|
12
|
+
# MedicationDispense.medicationCodeableConcept / medicationReference
|
13
|
+
# MedicationOrder.medicationCodeableConcept / medicationReference
|
14
|
+
# MedicationStatement.medicationCodeableConcept / medicationReference
|
15
|
+
|
16
|
+
resources = record.entry.map{|e|e.resource}
|
17
|
+
resources.each do |resource|
|
18
|
+
if resource.is_a?(FHIR::Medication)
|
19
|
+
results[:eligible_fields] += 1
|
20
|
+
results[:validated_fields] += 1 if rxnorm?(resource.code)
|
21
|
+
elsif (
|
22
|
+
resource.is_a?(FHIR::MedicationOrder) ||
|
23
|
+
resource.is_a?(FHIR::MedicationDispense) ||
|
24
|
+
resource.is_a?(FHIR::MedicationAdministration) ||
|
25
|
+
resource.is_a?(FHIR::MedicationStatement) )
|
26
|
+
if resource.medicationCodeableConcept
|
27
|
+
results[:eligible_fields] += 1
|
28
|
+
results[:validated_fields] += 1 if rxnorm?(resource.medicationCodeableConcept)
|
29
|
+
elsif resource.medicationReference
|
30
|
+
results[:eligible_fields] += 1
|
31
|
+
results[:validated_fields] += 1 if local_rxnorm_reference?(resource.medicationReference,record,resource.contained)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
37
|
+
percentage = 0.0 if percentage.nan?
|
38
|
+
points = 10.0 * percentage
|
39
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of Medication[x] Resource codes use RxNorm. Maximum of 10 points."
|
40
|
+
response(points.to_i,message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.rxnorm?(codeableconcept)
|
44
|
+
return false if codeableconcept.nil? || codeableconcept.coding.nil?
|
45
|
+
codeableconcept.coding.any?{|x| x.system=='http://www.nlm.nih.gov/research/umls/rxnorm' && FHIR::Terminology.get_description('RXNORM',x.code)}
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.local_rxnorm_reference?(reference,record,contained)
|
49
|
+
if contained && reference.reference && reference.reference.start_with?('#')
|
50
|
+
contained.each do |resource|
|
51
|
+
return true if resource.is_a?(FHIR::Medication) && reference.reference[1..-1]==resource.id
|
52
|
+
end
|
53
|
+
end
|
54
|
+
record.entry.each do |entry|
|
55
|
+
if entry.resource.is_a?(FHIR::Medication) && reference_matchs?(reference,entry)
|
56
|
+
return true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.reference_matchs?(reference,entry)
|
63
|
+
if reference.reference.start_with?('urn:uuid:')
|
64
|
+
(reference.reference == entry.fullUrl)
|
65
|
+
elsif reference.reference.include?('Medication/')
|
66
|
+
(reference.reference.split('Medication/').last.split('/').first == entry.id)
|
67
|
+
else
|
68
|
+
false # unable to verify reference points to RXNORM
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module FHIR
|
2
|
+
class SmokingStatus < FHIR::Rubrics
|
3
|
+
|
4
|
+
SMOKING_CODES = [
|
5
|
+
'449868002', #Current every day smoker
|
6
|
+
'428041000124106', #Current some day smoker
|
7
|
+
'8517006', #Former smoker
|
8
|
+
'266919005', #Never smoker
|
9
|
+
'77176002', #Smoker, current status unknown
|
10
|
+
'266927001', #Unknown if ever smoked
|
11
|
+
'428071000124103', #Current Heavy tobacco smoker
|
12
|
+
'428061000124105' #Current Light tobacco smoker
|
13
|
+
]
|
14
|
+
|
15
|
+
# The Patient Record should include Smoking Status
|
16
|
+
rubric :smoking_status do |record|
|
17
|
+
|
18
|
+
resources = record.entry.map{|e|e.resource}
|
19
|
+
has_smoking_status = resources.any? do |resource|
|
20
|
+
resource.is_a?(FHIR::Observation) && smoking_observation?(resource)
|
21
|
+
end
|
22
|
+
|
23
|
+
if has_smoking_status
|
24
|
+
points = 10
|
25
|
+
else
|
26
|
+
points = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
message = "The Patient Record should include Smoking Status (DAF-core-smokingstatus profile on Observation)."
|
30
|
+
response(points.to_i,message)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.smoking_observation?(resource)
|
34
|
+
smoking_code?(resource.code) && smoking_value?(resource.valueCodeableConcept)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.smoking_code?(codeableconcept)
|
38
|
+
return false if codeableconcept.nil? || codeableconcept.coding.nil?
|
39
|
+
codeableconcept.coding.any?{|x| x.system=='http://loinc.org' && x.code=='72166-2'}
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.smoking_value?(codeableconcept)
|
43
|
+
return false if codeableconcept.nil? || codeableconcept.coding.nil?
|
44
|
+
codeableconcept.coding.any?{|x| x.system=='http://snomed.info/sct' && SMOKING_CODES.include?(x.code)}
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module FHIR
|
2
|
+
class SnomedConditions < FHIR::Rubrics
|
3
|
+
|
4
|
+
# Conditions should be coded with the SNOMED Core Subset
|
5
|
+
rubric :snomed_core do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:validated_fields => 0
|
9
|
+
}
|
10
|
+
|
11
|
+
resources = record.entry.map{|e|e.resource}
|
12
|
+
resources.each do |resource|
|
13
|
+
if resource.is_a?(FHIR::Condition)
|
14
|
+
results[:eligible_fields] += 1
|
15
|
+
results[:validated_fields] += 1 if snomed_core?(resource.code)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
20
|
+
percentage = 0.0 if percentage.nan?
|
21
|
+
points = 10.0 * percentage
|
22
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of Condition Resource codes use the SNOMED Core Subset. Maximum of 10 points."
|
23
|
+
response(points.to_i,message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.snomed_core?(codeableconcept)
|
27
|
+
return false if codeableconcept.nil? || codeableconcept.coding.nil?
|
28
|
+
codeableconcept.coding.any?{|x| x.system=='http://snomed.info/sct' && FHIR::Terminology.is_core_snomed?(x.code)}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module FHIR
|
2
|
+
class VitalSigns < FHIR::Rubrics
|
3
|
+
|
4
|
+
# http://hl7.org/fhir/2016Sep/observation-vitalsigns.html
|
5
|
+
VITAL_SIGNS = {
|
6
|
+
# '8716-3' => '', # vital signs group
|
7
|
+
'9279-1' => [], #'/min', # respitory rate
|
8
|
+
'8867-4' => [], #'/min', # heart rate
|
9
|
+
'59408-5' => [], #'%', # Oxygen saturation
|
10
|
+
'8310-5' => [], #'Cel', # Body temperature
|
11
|
+
'8302-2' => [], #'cm', # Body height
|
12
|
+
'9843-4' => [], #'cm', # Head circumference
|
13
|
+
'29463-7' => [], #'kg', # Body weight
|
14
|
+
'39156-5' => [], #'kg/m2', # Body mass index
|
15
|
+
'55284-4' => ['8480-6','8462-4'] # Blood pressure systolic and diastolic group
|
16
|
+
# '8480-6' => 'mm[Hg]', # Systolic blood pressure
|
17
|
+
# '8462-4' => 'mm[Hg]', # Diastolic blood pressure
|
18
|
+
# '8478-0' => 'mm[Hg]' # Mean blood pressure
|
19
|
+
}
|
20
|
+
|
21
|
+
# Vital Signs should be present with specific LOINC codes
|
22
|
+
rubric :vital_signs do |record|
|
23
|
+
|
24
|
+
not_found = VITAL_SIGNS.clone
|
25
|
+
|
26
|
+
resources = record.entry.map{|e|e.resource}
|
27
|
+
resources.each do |resource|
|
28
|
+
if resource.is_a?(FHIR::Observation)
|
29
|
+
found_code = get_vital_code(resource.code)
|
30
|
+
components = not_found[ found_code ]
|
31
|
+
if components.nil?
|
32
|
+
components = []
|
33
|
+
else
|
34
|
+
components = components.clone
|
35
|
+
end
|
36
|
+
if components.empty?
|
37
|
+
not_found.delete(found_code)
|
38
|
+
elsif resource.component
|
39
|
+
resource.component.each do |component|
|
40
|
+
sub_code = get_vital_code(component.code)
|
41
|
+
components.delete(sub_code)
|
42
|
+
end
|
43
|
+
not_found.delete(found_code) if components.empty?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
percentage = ( (VITAL_SIGNS.length.to_f - not_found.length.to_f) / (VITAL_SIGNS.length.to_f) )
|
49
|
+
percentage = 0.0 if percentage.nan?
|
50
|
+
points = 10.0 * percentage
|
51
|
+
message = "#{(100 * percentage).to_i}% (#{VITAL_SIGNS.length - not_found.length}/#{VITAL_SIGNS.length}) of Vital Signs had at least one recorded Observation. Maximum of 10 points."
|
52
|
+
response(points.to_i,message)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.get_vital_code(codeableconcept)
|
56
|
+
return nil if codeableconcept.nil? || codeableconcept.coding.nil?
|
57
|
+
|
58
|
+
coding = codeableconcept.coding.find{|x| x.system=='http://loinc.org' && VITAL_SIGNS.has_key?(x.code)}
|
59
|
+
code = coding.code if coding
|
60
|
+
code
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/rubrics.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module FHIR
|
2
|
+
class Rubrics
|
3
|
+
|
4
|
+
@@rubrics = {}
|
5
|
+
|
6
|
+
def self.apply(record)
|
7
|
+
report = {}
|
8
|
+
@@rubrics.each do |key,rubric|
|
9
|
+
report[key] = rubric.call(record)
|
10
|
+
end
|
11
|
+
report
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.rubric(key,&block)
|
15
|
+
@@rubrics[key] = block
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.response(points,message)
|
19
|
+
{:points=>points,:message=>message}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
data/lib/scorecard.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module FHIR
|
2
|
+
class Scorecard
|
3
|
+
|
4
|
+
attr_accessor :report
|
5
|
+
attr_accessor :points
|
6
|
+
|
7
|
+
def score(bundle_raw)
|
8
|
+
@report = {}
|
9
|
+
@points = 0
|
10
|
+
|
11
|
+
# Check that the patient record is a FHIR Bundle.
|
12
|
+
bundle = FHIR.from_contents(bundle_raw)
|
13
|
+
if bundle.is_a?(FHIR::Bundle)
|
14
|
+
@points += 10
|
15
|
+
@report[:bundle] = { :points=>10, :message=>'Patient Record is a FHIR Bundle.'}
|
16
|
+
else
|
17
|
+
@report[:bundle] = { :points=>0, :message=>'Patient Record must be a FHIR Bundle.'}
|
18
|
+
end
|
19
|
+
|
20
|
+
if bundle.is_a?(FHIR::Bundle)
|
21
|
+
|
22
|
+
# Check that the patient record contains a FHIR Patient.
|
23
|
+
@patient = nil
|
24
|
+
count = 0
|
25
|
+
bundle.entry.each do |entry|
|
26
|
+
if entry.resource && entry.resource.is_a?(FHIR::Patient)
|
27
|
+
@patient = entry.resource
|
28
|
+
count += 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
if @patient && count==1
|
32
|
+
@points += 10
|
33
|
+
@report[:patient] = { :points=>10, :message=>'Patient Record contains one FHIR Patient.'}
|
34
|
+
else
|
35
|
+
@report[:patient] = { :points=>0, :message=>'Patient Record must contain one FHIR Patient.'}
|
36
|
+
end
|
37
|
+
|
38
|
+
report.merge!(FHIR::Rubrics.apply(bundle))
|
39
|
+
end
|
40
|
+
|
41
|
+
@report[:points] = @report.values.inject(0){|sum,section| sum+=section[:points]}
|
42
|
+
@report
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
namespace :fhir do
|
2
|
+
|
3
|
+
desc 'console'
|
4
|
+
task :console, [] do |t, args|
|
5
|
+
binding.pry
|
6
|
+
end
|
7
|
+
|
8
|
+
desc 'score a FHIR Bundle'
|
9
|
+
task :score, [:bundle_path] do |t, args|
|
10
|
+
bundle_path = args[:bundle_path]
|
11
|
+
if bundle_path.nil?
|
12
|
+
puts 'A path to FHIR Bundle is required!'
|
13
|
+
else
|
14
|
+
contents = File.open(bundle_path,'r:UTF-8',&:read)
|
15
|
+
scorecard = FHIR::Scorecard.new
|
16
|
+
report = scorecard.score(contents)
|
17
|
+
puts
|
18
|
+
puts " POINTS CATEGORY MESSAGE"
|
19
|
+
puts " ------ -------- -------"
|
20
|
+
report.each do |key,value|
|
21
|
+
next if key==:points
|
22
|
+
printf(" %3d %20s %s\n", value[:points], key, value[:message])
|
23
|
+
end
|
24
|
+
puts " ------"
|
25
|
+
printf(" %3d %20s\n", report[:points], 'TOTAL')
|
26
|
+
puts
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'post-process LOINC Top 2000 common lab results CSV'
|
31
|
+
task :process_loinc, [] do |t, args|
|
32
|
+
require 'find'
|
33
|
+
require 'csv'
|
34
|
+
puts 'Looking for `./terminology/LOINC*.csv`...'
|
35
|
+
loinc_file = Find.find('terminology').find{|f| /LOINC.*\.csv$/ =~f }
|
36
|
+
if loinc_file
|
37
|
+
output_filename = 'terminology/scorecard_loinc_2000.txt'
|
38
|
+
puts "Writing to #{output_filename}..."
|
39
|
+
output = File.open(output_filename,'w:UTF-8')
|
40
|
+
line = 0
|
41
|
+
begin
|
42
|
+
CSV.foreach(loinc_file, encoding: 'iso-8859-1:utf-8', headers: true) do |row|
|
43
|
+
line += 1
|
44
|
+
next if row.length <=1 || row[1].nil? # skip the categories
|
45
|
+
# CODE | DESC | UCUM UNITS
|
46
|
+
output.write("#{row[1]}|#{row[2]}|#{row[6]}\n")
|
47
|
+
end
|
48
|
+
rescue Exception => e
|
49
|
+
puts "Error at line #{line}"
|
50
|
+
puts e.message
|
51
|
+
end
|
52
|
+
output.close
|
53
|
+
puts 'Done.'
|
54
|
+
else
|
55
|
+
puts 'LOINC file not found.'
|
56
|
+
puts 'Download the LOINC Top 2000 Common Lab Results file'
|
57
|
+
puts ' -> http://loinc.org/usage/obs/loinc-top-2000-plus-loinc-lab-observations-us.csv'
|
58
|
+
puts 'copy it into your `./terminology` folder, and rerun this task.'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
desc 'post-process SNOMED Core Subset file'
|
63
|
+
task :process_snomed, [] do |t, args|
|
64
|
+
require 'find'
|
65
|
+
puts 'Looking for `./terminology/SNOMEDCT_CORE_SUBSET*.txt`...'
|
66
|
+
snomed_file = Find.find('terminology').find{|f| /SNOMEDCT_CORE_SUBSET.*\.txt$/ =~f }
|
67
|
+
if snomed_file
|
68
|
+
output_filename = 'terminology/scorecard_snomed_core.txt'
|
69
|
+
output = File.open(output_filename,'w:UTF-8')
|
70
|
+
line = 0
|
71
|
+
begin
|
72
|
+
entire_file = File.read(snomed_file)
|
73
|
+
puts "Writing to #{output_filename}..."
|
74
|
+
entire_file.split("\n").each do |l|
|
75
|
+
row = l.split('|')
|
76
|
+
line += 1
|
77
|
+
next if line==1 # skip the headers
|
78
|
+
# CODE | DESC
|
79
|
+
output.write("#{row[0]}|#{row[1]}\n")
|
80
|
+
end
|
81
|
+
rescue Exception => e
|
82
|
+
puts "Error at line #{line}"
|
83
|
+
puts e.message
|
84
|
+
end
|
85
|
+
output.close
|
86
|
+
puts 'Done.'
|
87
|
+
else
|
88
|
+
puts 'SNOMEDCT file not found.'
|
89
|
+
puts 'Download the SNOMEDCT Core Subset file'
|
90
|
+
puts ' -> https://www.nlm.nih.gov/research/umls/Snomed/core_subset.html'
|
91
|
+
puts 'copy it into your `./terminology` folder, and rerun this task.'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
desc 'post-process common UCUM codes'
|
96
|
+
task :process_ucum, [] do |t, args|
|
97
|
+
require 'find'
|
98
|
+
puts 'Looking for `./terminology/concepts.tsv`...'
|
99
|
+
ucum_file = Find.find('terminology').find{|f| /concepts.tsv$/ =~f }
|
100
|
+
if ucum_file
|
101
|
+
output_filename = 'terminology/scorecard_ucum.txt'
|
102
|
+
output = File.open(output_filename,'w:UTF-8')
|
103
|
+
line = 0
|
104
|
+
begin
|
105
|
+
entire_file = File.read(ucum_file)
|
106
|
+
puts "Writing to #{output_filename}..."
|
107
|
+
entire_file.split("\n").each do |l|
|
108
|
+
row = l.split("\t")
|
109
|
+
line += 1
|
110
|
+
next if line==1 # skip the headers
|
111
|
+
output.write("#{row[0]}\n") # code
|
112
|
+
output.write("#{row[5]}\n") if row[0]!=row[5] # synonym
|
113
|
+
end
|
114
|
+
rescue Exception => e
|
115
|
+
puts "Error at line #{line}"
|
116
|
+
puts e.message
|
117
|
+
end
|
118
|
+
output.close
|
119
|
+
puts 'Done.'
|
120
|
+
else
|
121
|
+
puts 'UCUM concepts file not found.'
|
122
|
+
puts 'Download the UCUM concepts file'
|
123
|
+
puts ' -> http://download.hl7.de/documents/ucum/concepts.tsv'
|
124
|
+
puts 'copy it into your `./terminology` folder, and rerun this task.'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
desc 'post-process UMLS terminology file'
|
129
|
+
task :process_umls, [] do |t, args|
|
130
|
+
require 'find'
|
131
|
+
puts 'Looking for `./terminology/MRCONSO.RRF`...'
|
132
|
+
input_file = Find.find('terminology').find{|f| f=='terminology/MRCONSO.RRF' }
|
133
|
+
if input_file
|
134
|
+
start = Time.now
|
135
|
+
output_filename = 'terminology/scorecard_umls.txt'
|
136
|
+
output = File.open(output_filename,'w:UTF-8')
|
137
|
+
line = 0
|
138
|
+
excluded = 0
|
139
|
+
excluded_systems = Hash.new(0)
|
140
|
+
begin
|
141
|
+
entire_file = File.read(input_file)
|
142
|
+
puts "Writing to #{output_filename}..."
|
143
|
+
entire_file.split("\n").each do |l|
|
144
|
+
row = l.split('|')
|
145
|
+
line += 1
|
146
|
+
include_code = false
|
147
|
+
codeSystem = row[11]
|
148
|
+
code = row[13]
|
149
|
+
description = row[14]
|
150
|
+
case codeSystem
|
151
|
+
when 'SNOMEDCT_US'
|
152
|
+
codeSystem = 'SNOMED'
|
153
|
+
include_code = (row[4]=='PF' && ['FN','OAF'].include?(row[12]))
|
154
|
+
when 'LNC'
|
155
|
+
codeSystem = 'LOINC'
|
156
|
+
include_code = true
|
157
|
+
when 'ICD10CM'
|
158
|
+
codeSystem = 'ICD10'
|
159
|
+
include_code = (row[12]=='PT')
|
160
|
+
when 'ICD10PCS'
|
161
|
+
codeSystem = 'ICD10'
|
162
|
+
include_code = (row[12]=='PT')
|
163
|
+
when 'ICD9CM'
|
164
|
+
codeSystem = 'ICD9'
|
165
|
+
include_code = (row[12]=='PT')
|
166
|
+
when 'MTHICD9'
|
167
|
+
codeSystem = 'ICD9'
|
168
|
+
include_code = true
|
169
|
+
when 'RXNORM'
|
170
|
+
include_code = true
|
171
|
+
when 'CVX'
|
172
|
+
include_code = (['PT','OP'].include?(row[12]))
|
173
|
+
when 'SRC'
|
174
|
+
# 'SRC' rows define the data sources in the file
|
175
|
+
include_code = false
|
176
|
+
else
|
177
|
+
include_code = false
|
178
|
+
excluded_systems[codeSystem] += 1
|
179
|
+
end
|
180
|
+
if include_code
|
181
|
+
output.write("#{codeSystem}|#{code}|#{description}\n")
|
182
|
+
else
|
183
|
+
excluded += 1
|
184
|
+
end
|
185
|
+
end
|
186
|
+
rescue Exception => e
|
187
|
+
puts "Error at line #{line}"
|
188
|
+
puts e.message
|
189
|
+
end
|
190
|
+
output.close
|
191
|
+
puts "Processed #{line} lines, excluding #{excluded} redundant entries."
|
192
|
+
puts "Excluded code systems: #{excluded_systems}" if !excluded_systems.empty?
|
193
|
+
finish = Time.now
|
194
|
+
minutes = ((finish-start)/60)
|
195
|
+
seconds = (minutes - minutes.floor) * 60
|
196
|
+
puts "Completed in #{minutes.floor} minute(s) #{seconds.floor} second(s)."
|
197
|
+
puts 'Done.'
|
198
|
+
else
|
199
|
+
puts 'UMLS file not found.'
|
200
|
+
puts 'Download the US National Library of Medicine (NLM) Unified Medical Language System (UMLS) Full Release files'
|
201
|
+
puts ' -> https://www.nlm.nih.gov/research/umls/licensedcontent/umlsknowledgesources.html'
|
202
|
+
puts 'Install the metathesaurus with the following data sources:'
|
203
|
+
puts ' CVX|CVX;ICD10CM|ICD10CM;ICD10PCS|ICD10PCS;ICD9CM|ICD9CM;LNC|LNC;MTHICD9|ICD9CM;RXNORM|RXNORM;SNOMEDCT_US|SNOMEDCT'
|
204
|
+
puts 'After installation, copy `{install path}/META/MRCONSO.RRF` into your `./terminology` folder, and rerun this task.'
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/terminology.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
module FHIR
|
2
|
+
class Terminology
|
3
|
+
|
4
|
+
CODE_SYSTEMS = {
|
5
|
+
'http://snomed.info/sct'=>'SNOMED',
|
6
|
+
'http://loinc.org'=>'LOINC',
|
7
|
+
'http://www.nlm.nih.gov/research/umls/rxnorm'=>'RXNORM',
|
8
|
+
'http://hl7.org/fhir/sid/icd-10'=>'ICD10',
|
9
|
+
'http://hl7.org/fhir/sid/icd-10-de'=>'ICD10',
|
10
|
+
'http://hl7.org/fhir/sid/icd-10-nl'=>'ICD10',
|
11
|
+
'http://hl7.org/fhir/sid/icd-10-us'=>'ICD10',
|
12
|
+
'http://www.icd10data.com/icd10pcs'=>'ICD10',
|
13
|
+
'http://hl7.org/fhir/sid/icd-9-cm'=>'ICD9',
|
14
|
+
'http://hl7.org/fhir/sid/icd-9-cm/diagnosis'=>'ICD9',
|
15
|
+
'http://hl7.org/fhir/sid/icd-9-cm/procedure'=>'ICD9',
|
16
|
+
'http://hl7.org/fhir/sid/cvx'=>'CVX'
|
17
|
+
}
|
18
|
+
|
19
|
+
@@term_root = File.expand_path '../terminology',File.dirname(File.absolute_path(__FILE__))
|
20
|
+
|
21
|
+
@@loaded = false
|
22
|
+
@@top_lab_code_units = {}
|
23
|
+
@@top_lab_code_descriptions = {}
|
24
|
+
@@known_codes = {}
|
25
|
+
@@core_snomed = {}
|
26
|
+
@@common_ucum = []
|
27
|
+
|
28
|
+
def self.set_terminology_root(root)
|
29
|
+
@@term_root = root
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.load_terminology
|
33
|
+
if !@@loaded
|
34
|
+
begin
|
35
|
+
# load the top lab codes
|
36
|
+
filename = File.join(@@term_root,'scorecard_loinc_2000.txt')
|
37
|
+
raw = File.open(filename,'r:UTF-8',&:read)
|
38
|
+
raw.split("\n").each do |line|
|
39
|
+
row = line.split('|')
|
40
|
+
@@top_lab_code_descriptions[row[0]] = row[1] if !row[1].nil?
|
41
|
+
@@top_lab_code_units[row[0]] = row[2] if !row[2].nil?
|
42
|
+
end
|
43
|
+
rescue Exception => error
|
44
|
+
FHIR.logger.error error
|
45
|
+
end
|
46
|
+
|
47
|
+
begin
|
48
|
+
# load the known codes
|
49
|
+
filename = File.join(@@term_root,'scorecard_umls.txt')
|
50
|
+
raw = File.open(filename,'r:UTF-8',&:read)
|
51
|
+
raw.split("\n").each do |line|
|
52
|
+
row = line.split('|')
|
53
|
+
codeSystem = row[0]
|
54
|
+
code = row[1]
|
55
|
+
description = row[2]
|
56
|
+
if @@known_codes[codeSystem]
|
57
|
+
codeSystemHash = @@known_codes[codeSystem]
|
58
|
+
else
|
59
|
+
codeSystemHash = {}
|
60
|
+
@@known_codes[codeSystem] = codeSystemHash
|
61
|
+
end
|
62
|
+
codeSystemHash[code] = description
|
63
|
+
end
|
64
|
+
rescue Exception => error
|
65
|
+
FHIR.logger.error error
|
66
|
+
end
|
67
|
+
|
68
|
+
begin
|
69
|
+
# load the core snomed codes
|
70
|
+
@@known_codes['SNOMED'] = {} if @@known_codes['SNOMED'].nil?
|
71
|
+
codeSystemHash = @@known_codes['SNOMED']
|
72
|
+
filename = File.join(@@term_root,'scorecard_snomed_core.txt')
|
73
|
+
raw = File.open(filename,'r:UTF-8',&:read)
|
74
|
+
raw.split("\n").each do |line|
|
75
|
+
row = line.split('|')
|
76
|
+
code = row[0]
|
77
|
+
description = row[1]
|
78
|
+
codeSystemHash[code] = description if codeSystemHash[code].nil?
|
79
|
+
@@core_snomed[code] = description
|
80
|
+
end
|
81
|
+
rescue Exception => error
|
82
|
+
FHIR.logger.error error
|
83
|
+
end
|
84
|
+
|
85
|
+
begin
|
86
|
+
# load common UCUM codes
|
87
|
+
filename = File.join(@@term_root,'scorecard_ucum.txt')
|
88
|
+
raw = File.open(filename,'r:UTF-8',&:read)
|
89
|
+
raw.split("\n").each do |code|
|
90
|
+
@@common_ucum << code
|
91
|
+
end
|
92
|
+
@@common_ucum.uniq!
|
93
|
+
rescue Exception => error
|
94
|
+
FHIR.logger.error error
|
95
|
+
end
|
96
|
+
|
97
|
+
@@loaded = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.get_description(system,code)
|
102
|
+
load_terminology
|
103
|
+
if @@known_codes[system]
|
104
|
+
@@known_codes[system][code]
|
105
|
+
else
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.is_core_snomed?(code)
|
111
|
+
load_terminology
|
112
|
+
!@@core_snomed[code].nil?
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.is_top_lab_code?(code)
|
116
|
+
load_terminology
|
117
|
+
!@@top_lab_code_units[code].nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.lab_units(code)
|
121
|
+
load_terminology
|
122
|
+
@@top_lab_code_units[code]
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.is_known_ucum?(units)
|
126
|
+
load_terminology
|
127
|
+
@@top_lab_code_units.values.include?(units) || @@common_ucum.include?(units)
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.lab_description(code)
|
131
|
+
load_terminology
|
132
|
+
@@top_lab_code_descriptions[code]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/logs/.keep
ADDED
File without changes
|
data/terminology/.keep
ADDED
File without changes
|