test-patient-generator 1.0.2 → 1.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.
- data/Gemfile +6 -8
- data/lib/test-patient-generator.rb +1 -6
- data/lib/tpg/ext/coded.rb +9 -3
- data/lib/tpg/ext/data_criteria.rb +150 -142
- data/lib/tpg/ext/range.rb +2 -132
- data/lib/tpg/ext/value.rb +21 -82
- data/lib/tpg/generation/exporter.rb +14 -32
- data/lib/tpg/generation/generator.rb +158 -84
- data/lib/tpg/generation/randomizer.rb +17 -30
- metadata +83 -9
- data/lib/tpg/ext/conjunction.rb +0 -23
- data/lib/tpg/ext/derivation_operator.rb +0 -49
- data/lib/tpg/ext/population_criteria.rb +0 -12
- data/lib/tpg/ext/precondition.rb +0 -23
- data/lib/tpg/ext/subset_operator.rb +0 -7
- data/lib/tpg/ext/temporal_reference.rb +0 -66
@@ -1,11 +1,5 @@
|
|
1
|
-
require 'set'
|
2
|
-
|
3
1
|
module HQMF
|
4
|
-
# Contains functions that can be used to randomly generate fields for patients.
|
5
|
-
# Also includes utility functions for randomly generating numbers and dates or choosing options within a range.
|
6
2
|
class Randomizer
|
7
|
-
UNIQUE_PATIENT_IDS = Set.new # Used to make sure we don't duplicate randomly generated patient IDs
|
8
|
-
|
9
3
|
# Add trivial demographics info to a Record. Trivial fields are ones that identify a patient as unique to a human eye
|
10
4
|
# but have no impact on CQM, e.g. address. This is essentially an upsert, so any preexisting info will be wiped.
|
11
5
|
#
|
@@ -21,7 +15,7 @@ module HQMF
|
|
21
15
|
patient.languages << randomize_language
|
22
16
|
patient.first = randomize_first_name(patient.gender) if patient.gender
|
23
17
|
patient.last = randomize_last_name
|
24
|
-
patient.medical_record_number =
|
18
|
+
patient.medical_record_number = Digest::MD5.hexdigest("#{patient.first} #{patient.last}")
|
25
19
|
|
26
20
|
patient
|
27
21
|
end
|
@@ -34,10 +28,10 @@ module HQMF
|
|
34
28
|
# Black persons 12.6%
|
35
29
|
# Hispanic 16.3%
|
36
30
|
# White 63.7%
|
37
|
-
def self.randomize_race_and_ethnicity
|
38
|
-
|
31
|
+
def self.randomize_race_and_ethnicity(percent = nil)
|
32
|
+
percent ||= rand(999)
|
39
33
|
|
40
|
-
case
|
34
|
+
case percent
|
41
35
|
when 0..1
|
42
36
|
{race: '2076-8', ethnicity: '2186-5'} # pacific islander
|
43
37
|
when 2..10
|
@@ -73,11 +67,11 @@ module HQMF
|
|
73
67
|
# 00.1% persian
|
74
68
|
# 00.1% us sign
|
75
69
|
# 03.0% other
|
76
|
-
def self.randomize_language
|
77
|
-
|
70
|
+
def self.randomize_language(percent = nil)
|
71
|
+
percent ||= rand(999)
|
78
72
|
|
79
|
-
case
|
80
|
-
when 0..802
|
73
|
+
case percent
|
74
|
+
when 0..802
|
81
75
|
'en-US' # english
|
82
76
|
when 802..925
|
83
77
|
'es-US' # spanish
|
@@ -175,26 +169,17 @@ module HQMF
|
|
175
169
|
'postalCode' => zip
|
176
170
|
}.to_json
|
177
171
|
end
|
178
|
-
|
179
|
-
# Randomize patient IDs for a given patients. We ensure that this ID is unique.
|
180
|
-
#
|
181
|
-
# @return The same patient with a random, non-duplicate ID assigned.
|
182
|
-
def self.randomize_patient_id
|
183
|
-
# Keep trying to add a new ID to the set. Return the ID when we find one that's unique.
|
184
|
-
loop do
|
185
|
-
id = (0...10).map{ ('0'..'9').to_a[rand(10)] }.join.to_s
|
186
|
-
break id if UNIQUE_PATIENT_IDS.size < UNIQUE_PATIENT_IDS.add(id).size
|
187
|
-
end
|
188
|
-
end
|
189
172
|
|
190
173
|
# More accurately, randomize a believable birthdate. Given a patient, find all coded entries that
|
191
174
|
# have age-related implications and make sure the patient is at least that old. Since that is complicated,
|
192
|
-
# for now let's just make them
|
175
|
+
# for now let's just make them 40
|
193
176
|
#
|
194
177
|
# @param A patient with coded entries that dictate potential birthdates
|
195
178
|
# @return A realistic birthdate for the given patient
|
196
179
|
def self.randomize_birthdate(patient)
|
197
|
-
Time.now
|
180
|
+
now = Time.now
|
181
|
+
range = randomize_range(now.advance(years: -40), now.advance(years: -35))
|
182
|
+
range.low.to_time_object
|
198
183
|
end
|
199
184
|
|
200
185
|
# Randomly generate a Range object that is within a lower and upper bounds. It is guaranteed that the high of the
|
@@ -202,12 +187,14 @@ module HQMF
|
|
202
187
|
#
|
203
188
|
# @param [Time] earliest_time The lowest possible value for the low in this Range. nil implies unbounded.
|
204
189
|
# @param [Time] latest_time The highest possible value for the high in this Range. nil implies unbounded.
|
190
|
+
# @param [Hash] maximum_length Optionally used to bound the length of a range. This is a hash that can be passed as a parameter to Time.advance.
|
205
191
|
# @return A Range that represents a start time and end time within the acceptable bounds supplied.
|
206
|
-
def self.randomize_range(earliest_time, latest_time)
|
207
|
-
earliest_time ||= Time.now.advance(years: -
|
208
|
-
latest_time ||= Time.now.
|
192
|
+
def self.randomize_range(earliest_time, latest_time, maximum_length = nil)
|
193
|
+
earliest_time ||= Time.now.advance(years: -35)
|
194
|
+
latest_time ||= Time.now.advance(days: -1)
|
209
195
|
|
210
196
|
low = Time.at(Randomizer.between(earliest_time.to_i, latest_time.to_i))
|
197
|
+
latest_time = low.advance(maximum_length) if maximum_length
|
211
198
|
high = Time.at(Randomizer.between(low, latest_time.to_i))
|
212
199
|
|
213
200
|
low = Value.new("TS", nil, Value.time_to_ts(low), true, false, false)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: test-patient-generator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,88 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
13
|
-
dependencies:
|
12
|
+
date: 2012-12-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: health-data-standards
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.2.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: hquery-patient-api
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.0.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.0.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: hqmf-parser
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.1.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: hqmf2js
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.1.0
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.1.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: qrda_generator
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 1.0.1
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.0.1
|
14
94
|
description: A utility to generate patients for unit testing clinical quality measure
|
15
95
|
logic. The instructions for generation are guided by HQMF documents and exported
|
16
96
|
into various health standards, e.g. C32, CCR.
|
@@ -21,15 +101,9 @@ extra_rdoc_files: []
|
|
21
101
|
files:
|
22
102
|
- lib/test-patient-generator.rb
|
23
103
|
- lib/tpg/ext/coded.rb
|
24
|
-
- lib/tpg/ext/conjunction.rb
|
25
104
|
- lib/tpg/ext/data_criteria.rb
|
26
|
-
- lib/tpg/ext/derivation_operator.rb
|
27
|
-
- lib/tpg/ext/population_criteria.rb
|
28
|
-
- lib/tpg/ext/precondition.rb
|
29
105
|
- lib/tpg/ext/range.rb
|
30
106
|
- lib/tpg/ext/record.rb
|
31
|
-
- lib/tpg/ext/subset_operator.rb
|
32
|
-
- lib/tpg/ext/temporal_reference.rb
|
33
107
|
- lib/tpg/ext/value.rb
|
34
108
|
- lib/tpg/generation/exporter.rb
|
35
109
|
- lib/tpg/generation/generator.rb
|
data/lib/tpg/ext/conjunction.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
module Conjunction
|
2
|
-
module AllTrue
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# @param [Array] base_patients
|
6
|
-
# @return
|
7
|
-
def generate(base_patients)
|
8
|
-
preconditions.each do |precondition|
|
9
|
-
precondition.generate(base_patients)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# @param [Array] base_patients
|
17
|
-
# @return
|
18
|
-
module AtLeastOneTrue
|
19
|
-
def generate(base_patients)
|
20
|
-
preconditions.sample.generate(base_patients)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
module HQMF
|
2
|
-
class DerivationOperator
|
3
|
-
# Perform an intersection between two sets of Ranges (assuming these are timestamps).
|
4
|
-
#
|
5
|
-
# @param [Array] set1 One array of Ranges to be intersected.
|
6
|
-
# @param [Array] set2 The other array of Ranges to be intersected.
|
7
|
-
# @return A new array that contains the shared Ranges between set1 and set2.
|
8
|
-
def self.intersection(set1, set2)
|
9
|
-
# Special cases to account for emptiness
|
10
|
-
return [] if set1.empty? && set2.empty?
|
11
|
-
return set1 if set2.empty?
|
12
|
-
return set2 if set1.empty?
|
13
|
-
|
14
|
-
# Merge each element of the two sets together
|
15
|
-
result = []
|
16
|
-
set1.each do |range1|
|
17
|
-
set2.each do |range2|
|
18
|
-
intersect = range1.intersection(range2)
|
19
|
-
result << intersect unless intersect.nil?
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
result
|
24
|
-
end
|
25
|
-
|
26
|
-
# Perform a union between two sets of Ranges (assuming these are timestamps)
|
27
|
-
#
|
28
|
-
# @param [Array] set1 One array of Ranges to be unioned.
|
29
|
-
# @param [Array] set2 The other array of Ranges to be unioned.
|
30
|
-
# @return A new array tha contains the union of Ranges between set1 and set2.
|
31
|
-
def self.union(set1, set2)
|
32
|
-
# Special cases to account for emptiness
|
33
|
-
return [] if set1.empty? && set2.empty?
|
34
|
-
return set1 if set2.empty?
|
35
|
-
return set2 if set1.empty?
|
36
|
-
|
37
|
-
# Join each element of the two sets together
|
38
|
-
result = []
|
39
|
-
set1.each do |range1|
|
40
|
-
set2.each do |range2|
|
41
|
-
union = range1.union(range2)
|
42
|
-
result.concat!(union)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
result
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
module HQMF
|
2
|
-
class PopulationCriteria
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# @param [Array] base_patients
|
6
|
-
# @return
|
7
|
-
def generate(base_patients)
|
8
|
-
# All population criteria begin with a single conjunction precondition
|
9
|
-
preconditions.first.generate(base_patients)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
data/lib/tpg/ext/precondition.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
module HQMF
|
2
|
-
class Precondition
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# @param [Array] base_patients
|
6
|
-
# @return
|
7
|
-
def generate(base_patients)
|
8
|
-
# d = HQMF::Generator.hqmf.data_criteria(preconditions[2].preconditions[1].preconditions.first.reference.id)
|
9
|
-
|
10
|
-
if conjunction?
|
11
|
-
# Include the matching module to override our generation functions
|
12
|
-
conjunction_module = "Conjunction::#{self.conjunction_code.classify}"
|
13
|
-
conjunction_module = conjunction_module.split('::').inject(Kernel) {|scope, name| scope.const_get(name)}
|
14
|
-
|
15
|
-
extend conjunction_module
|
16
|
-
generate(base_patients)
|
17
|
-
elsif reference
|
18
|
-
data_criteria = HQMF::Generator.hqmf.data_criteria(reference.id)
|
19
|
-
data_criteria.generate(base_patients)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
module HQMF
|
2
|
-
# Generates a Range to define the timing of a data_criteria
|
3
|
-
class TemporalReference
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# @param [Array] base_patients
|
7
|
-
# @return
|
8
|
-
def generate(base_patients)
|
9
|
-
if reference.id == "MeasurePeriod"
|
10
|
-
matching_time = Generator::hqmf.measure_period.clone
|
11
|
-
else
|
12
|
-
# First generate patients for the data criteria that this temporal reference points to
|
13
|
-
data_criteria = Generator::hqmf.data_criteria(reference.id)
|
14
|
-
base_patients = data_criteria.generate(base_patients)
|
15
|
-
|
16
|
-
# Now that the data criteria is defined, we can set our relative time to those generated results
|
17
|
-
matching_time = data_criteria.generation_range.first.clone
|
18
|
-
end
|
19
|
-
|
20
|
-
# TODO add nils where necessary. Ranges should be unbounded, despite the relative time's potential bounds (unless the type specifies)
|
21
|
-
if range
|
22
|
-
offset = range.try(:clone)
|
23
|
-
|
24
|
-
case type
|
25
|
-
when "DURING"
|
26
|
-
# TODO differentiate between this and CONCURRENT
|
27
|
-
when "SBS" # Starts before start
|
28
|
-
offset.low.value.insert(0, "-")
|
29
|
-
when "SAS" # Starts after start
|
30
|
-
offset.low = offset.high
|
31
|
-
offset.high = nil
|
32
|
-
when "SBE" # Starts before end
|
33
|
-
offset.low = offset.high
|
34
|
-
offset.high = nil
|
35
|
-
offset.low.value.insert(0, "-")
|
36
|
-
matching_time.low = matching_time.high
|
37
|
-
when "SAE" # Starts after end
|
38
|
-
|
39
|
-
when "EBS" # Ends before start
|
40
|
-
|
41
|
-
when "EAS" # Ends after start
|
42
|
-
|
43
|
-
when "EBE" # Ends before end
|
44
|
-
|
45
|
-
when "EAE" # Ends after end
|
46
|
-
|
47
|
-
when "SDU" # Starts during
|
48
|
-
matching_time.high.value = nil
|
49
|
-
when "EDU" # Ends during
|
50
|
-
|
51
|
-
when "ECW" # Ends concurrent with
|
52
|
-
|
53
|
-
when "SCW" # Starts concurrent with
|
54
|
-
|
55
|
-
when "CONCURRENT"
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
matching_time = Range.merge_ranges(offset, matching_time)
|
60
|
-
end
|
61
|
-
|
62
|
-
# Note we return the possible times to the calling data criteria, not patients
|
63
|
-
[matching_time]
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|