conceptql 0.6.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +176 -0
- data/README.md +4 -2
- data/assets/diagram/diagram.css +331 -0
- data/assets/diagram/fonts/OFL.txt +93 -0
- data/assets/diagram/fonts/Roboto-400.ttf +0 -0
- data/assets/diagram/fonts/Roboto-700.ttf +0 -0
- data/assets/diagram/fonts/Roboto-Italic-400.ttf +0 -0
- data/assets/diagram/fonts/Roboto-Italic-Latin.woff2 +0 -0
- data/assets/diagram/fonts/Roboto-Normal-Latin.woff2 +0 -0
- data/assets/diagram/operators.svg +224 -0
- data/config/vocabularies.csv +85 -85
- data/docs/spec/README.md +2624 -0
- data/docs/spec/examples/after-operator.json +562 -0
- data/docs/spec/examples/any-overlap-operator.json +106 -0
- data/docs/spec/examples/before-operator.json +437 -0
- data/docs/spec/examples/co-reported-operator.json +168 -0
- data/docs/spec/examples/contains-operator.json +97 -0
- data/docs/spec/examples/date-range-operator.json +275 -0
- data/docs/spec/examples/death-operator.json +103 -0
- data/docs/spec/examples/during-operator.json +178 -0
- data/docs/spec/examples/episode-operator.json +80 -0
- data/docs/spec/examples/equal-operator.json +79 -0
- data/docs/spec/examples/ethnicity-operator.json +86 -0
- data/docs/spec/examples/except-operator.json +536 -0
- data/docs/spec/examples/filter-operator.json +218 -0
- data/docs/spec/examples/first-operator.json +148 -0
- data/docs/spec/examples/gender-operator.json +257 -0
- data/docs/spec/examples/hospice-operator.json +40 -0
- data/docs/spec/examples/hospitalization-operator.json +131 -0
- data/docs/spec/examples/information-periods-operator.json +131 -0
- data/docs/spec/examples/last-operator.json +148 -0
- data/docs/spec/examples/match-operator.json +186 -0
- data/docs/spec/examples/nth-occurrence-operator.json +300 -0
- data/docs/spec/examples/numeric-filter-operator.json +77 -0
- data/docs/spec/examples/one-in-two-out-operator.json +158 -0
- data/docs/spec/examples/person-filter-operator.json +919 -0
- data/docs/spec/examples/person-operator.json +274 -0
- data/docs/spec/examples/place-of-service-filter-operator.json +57 -0
- data/docs/spec/examples/provenance-operator.json +148 -0
- data/docs/spec/examples/provider-filter-operator.json +77 -0
- data/docs/spec/examples/race-operator.json +131 -0
- data/docs/spec/examples/recall-operator.json +289 -0
- data/docs/spec/examples/snf-operator.json +40 -0
- data/docs/spec/examples/time-window-operator.json +736 -0
- data/docs/spec/examples/trim-date-end-operator.json +201 -0
- data/docs/spec/examples/trim-date-start-operator.json +201 -0
- data/docs/spec/examples/union-operator.json +547 -0
- data/docs/spec/examples/vocabulary-operator.json +232 -0
- data/lib/conceptql/behaviors/metadatable.rb +68 -2
- data/lib/conceptql/behaviors/provenanceable.rb +4 -1
- data/lib/conceptql/behaviors/utilizable.rb +3 -0
- data/lib/conceptql/code_case_mismatch_error.rb +50 -0
- data/lib/conceptql/data_model/gdm.rb +23 -2
- data/lib/conceptql/diagram_renderer/app.rb +59 -0
- data/lib/conceptql/diagram_renderer/configuration.rb +87 -0
- data/lib/conceptql/diagram_renderer/diagram_data.rb +80 -0
- data/lib/conceptql/diagram_renderer/drawing.rb +255 -0
- data/lib/conceptql/diagram_renderer/geometry.rb +380 -0
- data/lib/conceptql/diagram_renderer/input_error.rb +11 -0
- data/lib/conceptql/diagram_renderer/js_format.rb +112 -0
- data/lib/conceptql/diagram_renderer/resvg.rb +120 -0
- data/lib/conceptql/diagram_renderer/sprite.rb +73 -0
- data/lib/conceptql/diagram_renderer/static.rb +70 -0
- data/lib/conceptql/diagram_renderer/svg.rb +73 -0
- data/lib/conceptql/diagram_renderer/tree_layout.rb +287 -0
- data/lib/conceptql/diagram_renderer.rb +82 -0
- data/lib/conceptql/lexicon/lexicon_strategy.rb +16 -0
- data/lib/conceptql/lexicon.rb +63 -11
- data/lib/conceptql/nodifier.rb +5 -3
- data/lib/conceptql/operators/after.rb +15 -1
- data/lib/conceptql/operators/any_overlap.rb +6 -1
- data/lib/conceptql/operators/before.rb +11 -1
- data/lib/conceptql/operators/binary_operator_operator.rb +12 -2
- data/lib/conceptql/operators/co_reported.rb +9 -4
- data/lib/conceptql/operators/contains.rb +6 -1
- data/lib/conceptql/operators/count.rb +2 -0
- data/lib/conceptql/operators/date_range.rb +15 -3
- data/lib/conceptql/operators/death.rb +5 -1
- data/lib/conceptql/operators/dedup.rb +2 -0
- data/lib/conceptql/operators/during.rb +6 -1
- data/lib/conceptql/operators/episode.rb +13 -2
- data/lib/conceptql/operators/equal.rb +11 -1
- data/lib/conceptql/operators/ethnicity.rb +19 -7
- data/lib/conceptql/operators/except.rb +10 -1
- data/lib/conceptql/operators/filter.rb +6 -1
- data/lib/conceptql/operators/first.rb +19 -9
- data/lib/conceptql/operators/from.rb +3 -0
- data/lib/conceptql/operators/gender.rb +10 -2
- data/lib/conceptql/operators/hospice.rb +7 -1
- data/lib/conceptql/operators/hospitalization.rb +6 -1
- data/lib/conceptql/operators/information_periods.rb +26 -7
- data/lib/conceptql/operators/invalid.rb +4 -0
- data/lib/conceptql/operators/last.rb +19 -9
- data/lib/conceptql/operators/match.rb +6 -1
- data/lib/conceptql/operators/multiple_vocabularies.rb +17 -3
- data/lib/conceptql/operators/numeric.rb +4 -0
- data/lib/conceptql/operators/numeric_filter.rb +13 -3
- data/lib/conceptql/operators/occurrence.rb +33 -12
- data/lib/conceptql/operators/one_in_two_out.rb +22 -5
- data/lib/conceptql/operators/operator.rb +8 -4
- data/lib/conceptql/operators/person.rb +5 -1
- data/lib/conceptql/operators/person_filter.rb +14 -1
- data/lib/conceptql/operators/place_of_service_filter.rb +14 -3
- data/lib/conceptql/operators/provenance.rb +17 -8
- data/lib/conceptql/operators/provider_filter.rb +18 -6
- data/lib/conceptql/operators/race.rb +14 -9
- data/lib/conceptql/operators/read.rb +11 -1
- data/lib/conceptql/operators/recall.rb +34 -2
- data/lib/conceptql/operators/snf.rb +7 -1
- data/lib/conceptql/operators/sum.rb +4 -0
- data/lib/conceptql/operators/temporal_operator.rb +4 -2
- data/lib/conceptql/operators/time_window.rb +27 -4
- data/lib/conceptql/operators/trim_date.rb +6 -14
- data/lib/conceptql/operators/trim_date_end.rb +9 -3
- data/lib/conceptql/operators/trim_date_start.rb +9 -3
- data/lib/conceptql/operators/union.rb +13 -1
- data/lib/conceptql/operators/vocabulary.rb +80 -6
- data/lib/conceptql/paths.rb +9 -1
- data/lib/conceptql/version.rb +1 -1
- data/lib/conceptql/vocabularies/drg_systems.rb +3 -2
- data/lib/conceptql/vocabularies/entry.rb +14 -11
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +2 -1
- data/lib/conceptql.rb +3 -0
- metadata +75 -2
- data/doc/spec.md +0 -2091
|
@@ -18,9 +18,21 @@ module ConceptQL
|
|
|
18
18
|
|
|
19
19
|
DATE_FORMAT = /\A#{Regexp.union([ /START/i, /END/i, /\d{4}-\d{2}-\d{2}/ ])}\z/.freeze
|
|
20
20
|
|
|
21
|
-
desc "
|
|
22
|
-
|
|
23
|
-
option :
|
|
21
|
+
desc "Makes one person record for each patient, with the given start_date and end_date. " \
|
|
22
|
+
"Use it to compare other records with a fixed range of dates."
|
|
23
|
+
option :start, type: :string,
|
|
24
|
+
meaning: "The start_date of each record. It is a date in YYYY-MM-DD format, START or END. " \
|
|
25
|
+
"START is the earliest start date of any information period in the database, " \
|
|
26
|
+
"and END is the latest end date. START and END are not case-sensitive."
|
|
27
|
+
option :end, type: :string,
|
|
28
|
+
meaning: "The end_date of each record. It is a date in YYYY-MM-DD format, START or END. " \
|
|
29
|
+
"START is the earliest start date of any information period in the database, " \
|
|
30
|
+
"and END is the latest end date. START and END are not case-sensitive. " \
|
|
31
|
+
"For a single day, give the same date as the start option."
|
|
32
|
+
returns "One record for each patient. The type of the records is person, " \
|
|
33
|
+
"and each record has the start_date and the end_date that the options give."
|
|
34
|
+
example "Each patient, with the dates of the year 2010", [ "date_range", { "start" => "2010-01-01", "end" => "2010-12-31" } ]
|
|
35
|
+
example "Each patient, with the dates from the start to the end of the data", [ "date_range", { "start" => "START", "end" => "END" } ]
|
|
24
36
|
category "Select by Property"
|
|
25
37
|
basic_type :selection
|
|
26
38
|
validate_no_upstreams
|
|
@@ -9,7 +9,11 @@ module ConceptQL
|
|
|
9
9
|
|
|
10
10
|
register __FILE__
|
|
11
11
|
|
|
12
|
-
desc "Selects
|
|
12
|
+
desc "Selects every death record. With one upstream operator, it selects the deaths of the patients who have a record in the upstream stream."
|
|
13
|
+
returns "One record for each selected death, from the deaths table. The type of the records is death, " \
|
|
14
|
+
"and their start_date and end_date are both the date of death."
|
|
15
|
+
example "All death records", [ "death" ]
|
|
16
|
+
example "The deaths of the patients with an old myocardial infarction (ICD-9-CM 412)", [ "death", [ "icd9cm", "412" ] ]
|
|
13
17
|
domains :death
|
|
14
18
|
allows_one_upstream
|
|
15
19
|
|
|
@@ -6,6 +6,8 @@ module ConceptQL
|
|
|
6
6
|
module Operators
|
|
7
7
|
class Dedup < PassThru
|
|
8
8
|
register __FILE__
|
|
9
|
+
hidden
|
|
10
|
+
category "Filter Single Stream"
|
|
9
11
|
|
|
10
12
|
desc "Passes along each distinct record once. Records that have the same value in every column become one record."
|
|
11
13
|
allows_one_upstream
|
|
@@ -7,7 +7,12 @@ module ConceptQL
|
|
|
7
7
|
class During < TemporalOperator
|
|
8
8
|
register __FILE__
|
|
9
9
|
|
|
10
|
-
desc "For each
|
|
10
|
+
desc "For each patient, keeps the left-hand records whose date range is inside the date range of a right-hand record. The left-hand record starts on or after the start_date of the right-hand record and ends on or before its end_date."
|
|
11
|
+
returns "The left-hand records that are inside the date range of at least one right-hand record of the same patient, each record once. The right-hand records are not returned."
|
|
12
|
+
option_meaning :within, "A date adjustment, such as 30d, 2m or 1y-3d. Before the comparison, the date range of each right-hand record is widened by this amount in each direction: its start_date moves back and its end_date moves forward. Use a positive amount."
|
|
13
|
+
|
|
14
|
+
example "All MIs for the year 2010",
|
|
15
|
+
[ "during", { left: [ "icd9", "412" ], right: [ "date_range", { start: "2010-01-01", end: "2010-12-31" } ] } ]
|
|
11
16
|
|
|
12
17
|
def where_clause
|
|
13
18
|
(within_start <= l_start_date) & (l_end_date <= within_end)
|
|
@@ -7,12 +7,23 @@ module ConceptQL
|
|
|
7
7
|
class Episode < PassThru
|
|
8
8
|
register __FILE__
|
|
9
9
|
|
|
10
|
-
desc "
|
|
10
|
+
desc "Merges each patient's records into episodes: spans of time in which the records are close together. " \
|
|
11
|
+
"For example, it can turn a set of prescriptions into the spans of time in which a patient takes a drug."
|
|
12
|
+
returns "One record for each episode. Its start_date is the earliest start_date in the episode, " \
|
|
13
|
+
"and its end_date is the latest end_date. Its criterion_table and criterion_domain are episode, " \
|
|
14
|
+
"and its criterion_id is 0, because an episode merges several records."
|
|
15
|
+
example "Episodes of diabetes diagnoses", [ "episode", [ "icd9", "250.00" ], { "gap_of" => "90" } ]
|
|
11
16
|
|
|
12
17
|
allows_one_upstream
|
|
13
18
|
validate_one_upstream
|
|
14
19
|
option :gap_of, type: :integer,
|
|
15
|
-
instructions: "
|
|
20
|
+
instructions: "Enter a number of days. A record stays in the current episode when it starts no more than this many days after the latest end date of the earlier records. The default is 0.",
|
|
21
|
+
meaning: "The largest gap in days that stays in the same episode. The operator orders each " \
|
|
22
|
+
"patient's records by start_date and then by end_date. A record stays in the current " \
|
|
23
|
+
"episode when its start_date is no more than this many days after the latest end_date " \
|
|
24
|
+
"of the earlier records, not only of the record just before. Otherwise it starts a " \
|
|
25
|
+
"new episode. A record without an end_date counts as ending on its start_date. " \
|
|
26
|
+
"The default is 0."
|
|
16
27
|
category "Modify Data"
|
|
17
28
|
basic_type :temporal
|
|
18
29
|
|
|
@@ -7,10 +7,20 @@ module ConceptQL
|
|
|
7
7
|
class Equal < TemporalOperator
|
|
8
8
|
register __FILE__
|
|
9
9
|
|
|
10
|
-
desc "For each
|
|
10
|
+
desc "For each patient, keeps the left-hand records whose value_as_number equals the value_as_number of a right-hand record. It does not compare dates."
|
|
11
|
+
returns "The left-hand records that have a right-hand record of the same patient with the same value_as_number, each record once. Left-hand records without a value_as_number (NULL) are not returned. The right-hand records are not returned."
|
|
12
|
+
|
|
13
|
+
# The test data has only one record with a value_as_number, so this
|
|
14
|
+
# example returns no rows there and stays out of the help.
|
|
15
|
+
example "HbA1c results (LOINC 4548-4) with the same value as an HbA1c result by HPLC (LOINC 17856-6) of the same patient",
|
|
16
|
+
[ "equal", { left: [ "loinc", "4548-4" ], right: [ "loinc", "17856-6" ] } ], help: false
|
|
11
17
|
|
|
12
18
|
require_column :value_as_number
|
|
13
19
|
|
|
20
|
+
# Equal compares only value_as_number, so the within option that it
|
|
21
|
+
# inherits from TemporalOperator would have no effect (seed conceptql-ufn)
|
|
22
|
+
unsupported_options :within
|
|
23
|
+
|
|
14
24
|
def where_clause
|
|
15
25
|
{ Sequel[:r][:value_as_number] => Sequel[:l][:value_as_number] }
|
|
16
26
|
end
|
|
@@ -7,11 +7,19 @@ module ConceptQL
|
|
|
7
7
|
class Ethnicity < Operator
|
|
8
8
|
register __FILE__
|
|
9
9
|
|
|
10
|
-
desc "Selects
|
|
10
|
+
desc "Selects the patients with the given ethnicities."
|
|
11
11
|
domains :person
|
|
12
12
|
category "Select by Property"
|
|
13
13
|
basic_type :selection
|
|
14
|
-
argument :ethnicity, type: :string, options: [ "Hispanic or Latino", "Not Hispanic or Latino" ]
|
|
14
|
+
argument :ethnicity, type: :string, options: [ "Hispanic or Latino", "Not Hispanic or Latino" ],
|
|
15
|
+
meaning: "One or more ethnicities. A value that starts with Hispanic selects the " \
|
|
16
|
+
"Hispanic or Latino patients, and a value that starts with Not Hispanic selects " \
|
|
17
|
+
"the patients who are not Hispanic or Latino. A value that contains Unknown " \
|
|
18
|
+
"selects the patients who have no ethnicity or ethnicity concept ID 0. Case does not matter, " \
|
|
19
|
+
"and any other value selects no patients."
|
|
20
|
+
returns "One person record for each patient whose ethnicity is one of the given ethnicities. " \
|
|
21
|
+
"The start_date and the end_date of each record are the patient's birth date."
|
|
22
|
+
example "All patients recorded as Hispanic", [ "ethnicity", "Hispanic" ]
|
|
15
23
|
validate_no_upstreams
|
|
16
24
|
validate_at_least_one_argument
|
|
17
25
|
|
|
@@ -24,21 +32,25 @@ module ConceptQL
|
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
def query(db)
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
ids = ethnicity_concept_ids
|
|
36
|
+
condition = Sequel.expr(ethnicity_concept_id: ids)
|
|
37
|
+
# Unknown (ID 0) also means no ethnicity, and IN never matches NULL.
|
|
38
|
+
condition = Sequel.|(condition, { ethnicity_concept_id: nil }) if ids.include?(0)
|
|
39
|
+
|
|
40
|
+
db.from(source_table).where(condition)
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
def ethnicity_concept_ids
|
|
32
|
-
arguments.
|
|
44
|
+
arguments.filter_map do |words|
|
|
33
45
|
case words
|
|
34
46
|
when /^Not Hispanic/i
|
|
35
47
|
38_003_564
|
|
36
48
|
when /^Hispanic/i
|
|
37
49
|
38_003_563
|
|
38
50
|
when /unknown/i
|
|
39
|
-
|
|
51
|
+
0
|
|
40
52
|
end
|
|
41
|
-
end
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
55
|
end
|
|
44
56
|
end
|
|
@@ -7,7 +7,16 @@ module ConceptQL
|
|
|
7
7
|
class Except < BinaryOperatorOperator
|
|
8
8
|
register __FILE__
|
|
9
9
|
|
|
10
|
-
desc "Removes
|
|
10
|
+
desc "Removes each left-hand record that also appears in the right-hand stream. Two records are the same when they have the same criterion_table, criterion_id, start_date and end_date."
|
|
11
|
+
returns "The left-hand records that have no right-hand record with the same criterion_table, criterion_id, start_date and end_date. A left-hand record whose table has no right-hand record passes through unchanged. The right-hand records are not returned."
|
|
12
|
+
option_meaning :right, "The right-hand stream. The operator removes each left-hand record that is also in this stream, and does not pass these records on."
|
|
13
|
+
|
|
14
|
+
example "All males who are not white",
|
|
15
|
+
[ "except", { left: [ "gender", "male" ], right: [ "race", "white" ] } ]
|
|
16
|
+
example "All Conditions that are MI",
|
|
17
|
+
[ "except", { left: [ "icd9", "412" ], right: [ "cpt", "99214" ] } ]
|
|
18
|
+
example "Passes two types downstream: Conditions that are MI (unaffected by the right-hand stream) and People that are Male but not White",
|
|
19
|
+
[ "except", { left: [ "union", [ "icd9", "412" ], [ "gender", "Male" ] ], right: [ "race", "White" ] } ]
|
|
11
20
|
default_query_columns
|
|
12
21
|
|
|
13
22
|
# An `as` option would cast both streams before the comparison. Except
|
|
@@ -7,7 +7,12 @@ module ConceptQL
|
|
|
7
7
|
class Filter < BinaryOperatorOperator
|
|
8
8
|
register __FILE__
|
|
9
9
|
|
|
10
|
-
desc "Keeps left hand records
|
|
10
|
+
desc "Keeps each left-hand record that also appears in the right-hand stream. Two records are the same when they have the same person_id, criterion_id, criterion_table, criterion_domain, start_date and end_date."
|
|
11
|
+
returns "The left-hand records that have a right-hand record with the same person_id, criterion_id, criterion_table, criterion_domain, start_date and end_date. The right-hand records are not returned."
|
|
12
|
+
option_meaning :right, "The right-hand stream. The operator keeps each left-hand record that is also in this stream, and does not pass these records on."
|
|
13
|
+
|
|
14
|
+
example "MIs that are a patient's first diagnosis of MI or diabetes",
|
|
15
|
+
[ "filter", { left: [ "icd9", "412" ], right: [ "first", [ "union", [ "icd9", "412" ], [ "icd9", "250.01" ] ] ] } ]
|
|
11
16
|
default_query_columns
|
|
12
17
|
|
|
13
18
|
# An `as` option would cast both streams before the comparison. Filter
|
|
@@ -6,13 +6,15 @@ module ConceptQL
|
|
|
6
6
|
module Operators
|
|
7
7
|
# Represents a operator that will grab the first occurrence of something
|
|
8
8
|
#
|
|
9
|
-
# The operator
|
|
10
|
-
# that
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# The operator takes exactly one upstream. It partitions the records of
|
|
10
|
+
# that upstream by person_id, sorts the records of each person by
|
|
11
|
+
# start_date, and selects at most one row per person, even when the
|
|
12
|
+
# upstream mixes records from several criterion tables
|
|
13
13
|
#
|
|
14
|
-
# If two rows have the same start_date, the
|
|
15
|
-
# is
|
|
14
|
+
# If two rows have the same start_date, the row with the lowest
|
|
15
|
+
# criterion_id comes first. criterion_id is unique within one
|
|
16
|
+
# criterion_table, so only rows from two different tables with the same
|
|
17
|
+
# id and start_date still rank in arbitrary order
|
|
16
18
|
#
|
|
17
19
|
# If we ask for the first occurrence of something and a person has no
|
|
18
20
|
# occurrences, this operator returns nothing for that person
|
|
@@ -21,9 +23,17 @@ module ConceptQL
|
|
|
21
23
|
|
|
22
24
|
desc "For each person, passes along the record with the earliest start_date."
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
returns "One record per person: the upstream record with the earliest start_date. " \
|
|
27
|
+
"When several records have that start_date, the one with the lowest criterion_id."
|
|
28
|
+
|
|
29
|
+
example "For each patient, select the Condition that represents the first occurrence of an MI",
|
|
30
|
+
[ "first", [ "icd9", "412" ] ]
|
|
31
|
+
|
|
32
|
+
# With at_least or within, Occurrence's timed query returns every
|
|
33
|
+
# upstream row instead of one row per person. group_by_date does not
|
|
34
|
+
# change the result, because first passes along one record of the
|
|
35
|
+
# earliest date either way (seed conceptql-ufn).
|
|
36
|
+
unsupported_options :at_least, :within, :group_by_date
|
|
27
37
|
|
|
28
38
|
def occurrence
|
|
29
39
|
1
|
|
@@ -10,8 +10,16 @@ module ConceptQL
|
|
|
10
10
|
include ConceptQL::Behaviors::Windowable
|
|
11
11
|
include ConceptQL::Behaviors::Timeless
|
|
12
12
|
|
|
13
|
-
desc "Selects
|
|
14
|
-
argument :gender, type: :string, options: %w[Male Female Unknown]
|
|
13
|
+
desc "Selects the patients with the given genders."
|
|
14
|
+
argument :gender, type: :string, options: %w[Male Female Unknown],
|
|
15
|
+
meaning: "One or more genders. A value that starts with M selects the male patients, " \
|
|
16
|
+
"and a value that starts with F selects the female patients. " \
|
|
17
|
+
"Any other value, such as Unknown, selects the patients whose gender is " \
|
|
18
|
+
"neither male nor female, including the patients with no gender. Case does not matter."
|
|
19
|
+
returns "One person record for each patient whose gender is one of the given genders. " \
|
|
20
|
+
"The start_date and the end_date of each record are the patient's birth date."
|
|
21
|
+
example "All female patients", [ "gender", "Female" ]
|
|
22
|
+
example "All male patients", [ "gender", "Male" ]
|
|
15
23
|
domains :person
|
|
16
24
|
category "Select by Property"
|
|
17
25
|
basic_type :selection
|
|
@@ -8,7 +8,13 @@ module ConceptQL
|
|
|
8
8
|
register __FILE__
|
|
9
9
|
include ConceptQL::Behaviors::Utilizable
|
|
10
10
|
|
|
11
|
-
desc "Selects hospice admission
|
|
11
|
+
desc "Selects hospice admissions: one record for each admission whose claim came from a hospice file."
|
|
12
|
+
returns "One record for each hospice admission. The type of the records is condition_occurrence, " \
|
|
13
|
+
"and their criterion_table is collections, the table of claims. Their start_date and end_date " \
|
|
14
|
+
"are the dates of the claim, and source_value holds the primary code of the admission when it has one. " \
|
|
15
|
+
"Each record also has admission and discharge columns, such as admission_date, discharge_date and length_of_stay."
|
|
16
|
+
# The test data has no hospice claims
|
|
17
|
+
example "All hospice admissions", [ "hospice" ], help: false
|
|
12
18
|
|
|
13
19
|
def collection_type
|
|
14
20
|
"hospice"
|
|
@@ -8,7 +8,12 @@ module ConceptQL
|
|
|
8
8
|
register __FILE__
|
|
9
9
|
include ConceptQL::Behaviors::Utilizable
|
|
10
10
|
|
|
11
|
-
desc "Selects inpatient admission
|
|
11
|
+
desc "Selects inpatient hospital admissions: one record for each admission whose claim came from an inpatient file."
|
|
12
|
+
returns "One record for each inpatient admission. The type of the records is condition_occurrence, " \
|
|
13
|
+
"and their criterion_table is collections, the table of claims. Their start_date and end_date " \
|
|
14
|
+
"are the dates of the claim, and source_value holds the primary code of the admission when it has one. " \
|
|
15
|
+
"Each record also has admission and discharge columns, such as admission_date, discharge_date and length_of_stay."
|
|
16
|
+
example "All inpatient hospital admissions", [ "hospitalization" ]
|
|
12
17
|
|
|
13
18
|
def collection_type
|
|
14
19
|
"inpatient"
|
|
@@ -10,10 +10,18 @@ module ConceptQL
|
|
|
10
10
|
include ConceptQL::Behaviors::Windowable
|
|
11
11
|
include ConceptQL::Behaviors::Timeless
|
|
12
12
|
|
|
13
|
-
desc "Selects
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
desc "Selects information periods: the spans of time in which the data has information about a patient, " \
|
|
14
|
+
"such as periods of insurance enrollment."
|
|
15
|
+
argument :information_types, label: "Information Types", type: :codelist,
|
|
16
|
+
meaning: "Zero or more information types, each as a concept ID (an integer) " \
|
|
17
|
+
"or a concept code of the GDM_INFORMATION_TYPE vocabulary, such as PARTD (Medicare Part D). " \
|
|
18
|
+
"A statement may mix both. " \
|
|
19
|
+
"With no information types, the operator selects every information period. " \
|
|
20
|
+
"With information types, it selects only the information periods whose type is one of them. " \
|
|
21
|
+
"An unknown code gets a warning and selects no period."
|
|
22
|
+
returns "One record for each selected information period, from the information_periods table. " \
|
|
23
|
+
"The type of the records is observation_period."
|
|
24
|
+
example "All information periods", [ "information_periods" ]
|
|
17
25
|
domains :observation_period
|
|
18
26
|
category "Get Related Data"
|
|
19
27
|
basic_type :selection
|
|
@@ -21,7 +29,8 @@ module ConceptQL
|
|
|
21
29
|
|
|
22
30
|
def query(db)
|
|
23
31
|
ds = db[source_table]
|
|
24
|
-
|
|
32
|
+
clause = dm.information_period_where_clause(db, arguments)
|
|
33
|
+
ds = ds.where(clause) if clause
|
|
25
34
|
ds
|
|
26
35
|
end
|
|
27
36
|
|
|
@@ -29,8 +38,18 @@ module ConceptQL
|
|
|
29
38
|
dm.table_by_domain(domain)
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# A code that names no concept selects no period. A code is known only
|
|
44
|
+
# in its own case, as the lookup finds it.
|
|
45
|
+
def additional_validation(db, opts = {})
|
|
46
|
+
return unless add_warnings?(db, opts)
|
|
47
|
+
|
|
48
|
+
codes = dm.information_type_codes(arguments)
|
|
49
|
+
return if codes.empty?
|
|
50
|
+
|
|
51
|
+
unknown = codes - dm.known_codes(db, dm.information_types_vocab, codes, check_case: false)
|
|
52
|
+
add_warning(UNKNOWN_CODE, *unknown) unless unknown.empty?
|
|
34
53
|
end
|
|
35
54
|
end
|
|
36
55
|
end
|
|
@@ -6,13 +6,15 @@ module ConceptQL
|
|
|
6
6
|
module Operators
|
|
7
7
|
# Represents a operator that will grab the last occurrence of something
|
|
8
8
|
#
|
|
9
|
-
# The operator
|
|
10
|
-
# that
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# The operator takes exactly one upstream. It partitions the records of
|
|
10
|
+
# that upstream by person_id, sorts the records of each person by
|
|
11
|
+
# start_date, and selects at most one row per person, even when the
|
|
12
|
+
# upstream mixes records from several criterion tables
|
|
13
13
|
#
|
|
14
|
-
# If two rows have the same start_date, the
|
|
15
|
-
# is
|
|
14
|
+
# If two rows have the same start_date, the row with the highest
|
|
15
|
+
# criterion_id comes first. criterion_id is unique within one
|
|
16
|
+
# criterion_table, so only rows from two different tables with the same
|
|
17
|
+
# id and start_date still rank in arbitrary order
|
|
16
18
|
#
|
|
17
19
|
# If we ask for the last occurrence of something and a person has no
|
|
18
20
|
# occurrences, this operator returns nothing for that person
|
|
@@ -21,9 +23,17 @@ module ConceptQL
|
|
|
21
23
|
|
|
22
24
|
desc "For each person, passes along the record with the most recent start_date."
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
returns "One record per person: the upstream record with the latest start_date. " \
|
|
27
|
+
"When several records have that start_date, the one with the highest criterion_id."
|
|
28
|
+
|
|
29
|
+
example "For each patient, select the Condition that represents the last occurrence of an MI",
|
|
30
|
+
[ "last", [ "icd9", "412" ] ]
|
|
31
|
+
|
|
32
|
+
# With at_least or within, Occurrence's timed query returns every
|
|
33
|
+
# upstream row instead of one row per person. group_by_date does not
|
|
34
|
+
# change the result, because last passes along one record of the
|
|
35
|
+
# latest date either way (seed conceptql-ufn).
|
|
36
|
+
unsupported_options :at_least, :within, :group_by_date
|
|
27
37
|
|
|
28
38
|
def occurrence
|
|
29
39
|
-1
|
|
@@ -9,7 +9,12 @@ module ConceptQL
|
|
|
9
9
|
class Match < BinaryOperatorOperator
|
|
10
10
|
register __FILE__
|
|
11
11
|
|
|
12
|
-
desc "Keeps left
|
|
12
|
+
desc "Keeps each left-hand record that is equal to a right-hand record in every column, including label. Two empty (NULL) values count as equal."
|
|
13
|
+
returns "The left-hand records that have a right-hand record with the same value in every column. The right-hand records are not returned."
|
|
14
|
+
option_meaning :right, "The right-hand stream. The operator keeps each left-hand record that is equal to a record of this stream, and does not pass these records on."
|
|
15
|
+
|
|
16
|
+
example "MIs that are a patient's first MI",
|
|
17
|
+
[ "match", { left: [ "icd9", "412" ], right: [ "first", [ "icd9", "412" ] ] } ]
|
|
13
18
|
default_query_columns
|
|
14
19
|
|
|
15
20
|
# match reads only_columns, except_columns, join_columns and
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "vocabulary"
|
|
4
|
+
|
|
3
5
|
module ConceptQL
|
|
4
6
|
module Operators
|
|
5
7
|
class MultipleVocabularies < Operator
|
|
6
8
|
include ConceptQL::Behaviors::Windowable
|
|
7
9
|
include ConceptQL::Behaviors::CodeLister
|
|
10
|
+
extend Vocabulary::SharedDocumentation
|
|
8
11
|
|
|
9
12
|
class << self
|
|
10
13
|
def multiple_vocabularies
|
|
@@ -26,12 +29,19 @@ module ConceptQL
|
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
|
|
32
|
+
def long_name_of(vocabulary_ids)
|
|
33
|
+
return vocabulary_ids.first if vocabulary_ids.one?
|
|
34
|
+
|
|
35
|
+
"#{vocabulary_ids[0...-1].join(', ')} and #{vocabulary_ids.last}"
|
|
36
|
+
end
|
|
37
|
+
|
|
29
38
|
def operator_symbol(word)
|
|
30
39
|
word.gsub(/\W+/, "_").downcase
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
# This will override the to_metadata method and return the preferred name
|
|
34
|
-
# based on the name listed in the file.
|
|
43
|
+
# based on the name listed in the file. The long_name names the
|
|
44
|
+
# operator's vocabularies, for example "CPT4, HCPCS and CPT4_HCPCS".
|
|
35
45
|
#
|
|
36
46
|
# This method will be called once for each vocabulary we register
|
|
37
47
|
# for this operator
|
|
@@ -39,6 +49,7 @@ module ConceptQL
|
|
|
39
49
|
h = super
|
|
40
50
|
op_info = multiple_vocabularies[name].first
|
|
41
51
|
h[:preferred_name] = op_info[:operator]
|
|
52
|
+
h[:long_name] = long_name_of(multiple_vocabularies[name].map { |mv| mv[:vocabulary_id] })
|
|
42
53
|
h[:predominant_domains] = multiple_vocabularies[name].map { |mv| mv[:domain] }.uniq.compact
|
|
43
54
|
h
|
|
44
55
|
end
|
|
@@ -47,7 +58,10 @@ module ConceptQL
|
|
|
47
58
|
register_many
|
|
48
59
|
|
|
49
60
|
desc "Selects records based on the given vocabularies."
|
|
50
|
-
argument :codes, type: :codelist
|
|
61
|
+
argument :codes, type: :codelist,
|
|
62
|
+
meaning: "One or more codes. The operator looks up each code in each of its vocabularies " \
|
|
63
|
+
"(CPT-4 and HCPCS for cpt_or_hcpcs) and selects the matching records of all of them. " \
|
|
64
|
+
"A code gets a warning only when every one of the vocabularies rejects it."
|
|
51
65
|
basic_type :selection
|
|
52
66
|
category "Select by Clinical Codes"
|
|
53
67
|
validate_no_upstreams
|
|
@@ -111,7 +125,7 @@ module ConceptQL
|
|
|
111
125
|
CODE_WARNINGS = [ *BAD_FORMATS, UNKNOWN_CODE ].freeze
|
|
112
126
|
|
|
113
127
|
def additional_validation(db, opts = {})
|
|
114
|
-
vocab_ops.each { |vo| vo.valid?(db, opts) }
|
|
128
|
+
vocab_ops.each { |vo| vo.valid?(db, opts.merge(skip_merged_vocabulary_check: true)) }
|
|
115
129
|
|
|
116
130
|
@errors += aggregate_messages(vocab_ops.map(&:errors))
|
|
117
131
|
@warnings += aggregate_messages(vocab_ops.map { |vo| vo.warnings.reject { |w| CODE_WARNINGS.include?(w.first) } })
|
|
@@ -15,6 +15,10 @@ module ConceptQL
|
|
|
15
15
|
# - An optional stream
|
|
16
16
|
class Numeric < PassThru
|
|
17
17
|
register __FILE__
|
|
18
|
+
# "We do not yet support numeric or sum in any meaningful way"
|
|
19
|
+
# (Ryan, 2026-11-03, seed conceptql-mdo.4)
|
|
20
|
+
hidden
|
|
21
|
+
category "Modify Data"
|
|
18
22
|
|
|
19
23
|
desc "Sets the value_as_number of each record of its upstream stream. " \
|
|
20
24
|
"The argument is a number, or the name of a column whose value is copied from the same record. " \
|
|
@@ -12,10 +12,20 @@ module ConceptQL
|
|
|
12
12
|
class NumericFilter < Operator
|
|
13
13
|
register __FILE__
|
|
14
14
|
|
|
15
|
-
desc "
|
|
15
|
+
desc "Passes along the records whose value_as_number is in the given range. " \
|
|
16
|
+
"Lab records, such as those of loinc, carry this value."
|
|
17
|
+
|
|
18
|
+
returns "The upstream records whose value_as_number is in the range. " \
|
|
19
|
+
"A record that has no value_as_number is not passed along."
|
|
20
|
+
|
|
21
|
+
option :greater_than_or_equal_to, type: :float,
|
|
22
|
+
meaning: "The lowest value_as_number that passes. Without this option, the range has no lower limit."
|
|
23
|
+
option :less_than_or_equal_to, type: :float,
|
|
24
|
+
meaning: "The highest value_as_number that passes. Without this option, the range has no upper limit."
|
|
25
|
+
|
|
26
|
+
example "Hemoglobin A1c results (LOINC 4548-4) with a value from 5 to 7",
|
|
27
|
+
[ "numeric_filter", [ "loinc", "4548-4" ], { "greater_than_or_equal_to" => 5, "less_than_or_equal_to" => 7 } ]
|
|
16
28
|
|
|
17
|
-
option :greater_than_or_equal_to, type: :float
|
|
18
|
-
option :less_than_or_equal_to, type: :float
|
|
19
29
|
category "Filter Single Stream"
|
|
20
30
|
basic_type :temporal
|
|
21
31
|
allows_one_upstream
|
|
@@ -14,10 +14,11 @@ module ConceptQL
|
|
|
14
14
|
# -1 => last
|
|
15
15
|
# -2 => second-to-last
|
|
16
16
|
#
|
|
17
|
-
# The operator
|
|
18
|
-
# that
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# The operator takes exactly one upstream. It partitions the records of
|
|
18
|
+
# that upstream by person_id, sorts the records of each person by
|
|
19
|
+
# start_date, and selects at most one row per person, even when the
|
|
20
|
+
# upstream mixes records from several criterion tables. With at_least or
|
|
21
|
+
# within, a person can have more than one row
|
|
21
22
|
#
|
|
22
23
|
# If two rows have the same start_date, criterion_id breaks the tie.
|
|
23
24
|
# criterion_id is unique within one criterion_table, so only rows from
|
|
@@ -34,21 +35,41 @@ module ConceptQL
|
|
|
34
35
|
preferred_name "Nth Occurrence"
|
|
35
36
|
|
|
36
37
|
desc "For each person, orders the records by start_date and passes along the record at the given position. " \
|
|
37
|
-
"A positive
|
|
38
|
-
"A negative number counts from the most recent record, so -3 means the third record from the last."
|
|
38
|
+
"A positive position counts from the earliest record and a negative one from the most recent record."
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
returns "One record per person: the record at the given position. " \
|
|
41
|
+
"A person who has fewer records than the position asks for is not passed along. " \
|
|
42
|
+
"With at_least or within, a person can have more than one record."
|
|
43
|
+
|
|
44
|
+
argument :occurrence, type: :integer,
|
|
45
|
+
meaning: "The position of the record to pass along, as an integer. " \
|
|
46
|
+
"A positive number counts from the earliest record, so 1 is the first record and 2 is the second. " \
|
|
47
|
+
"A negative number counts from the most recent record, so -1 is the last record and -3 is the third from the last. " \
|
|
48
|
+
"0 selects nothing."
|
|
41
49
|
category "Filter Single Stream"
|
|
42
50
|
basic_type :temporal
|
|
43
|
-
|
|
44
|
-
validate_at_least_one_upstream
|
|
51
|
+
validate_one_upstream
|
|
45
52
|
|
|
46
53
|
option :at_least, type: :string,
|
|
47
|
-
instructions: 'Enter a
|
|
54
|
+
instructions: 'Enter a number followed by "d", "w", "m" or "y" for days, weeks, months or years. Example: 30d.',
|
|
55
|
+
meaning: "A date adjustment, such as 30d. For each record, the operator counts that record and the later records " \
|
|
56
|
+
"that start at least this long after it ends, and passes along the record at the given position. " \
|
|
57
|
+
"So a person can have more than one record in the result."
|
|
48
58
|
option :within, type: :string,
|
|
49
|
-
instructions: 'Enter a
|
|
59
|
+
instructions: 'Enter a number followed by "d", "w", "m" or "y" for days, weeks, months or years. Example: 30d.',
|
|
60
|
+
meaning: "A date adjustment, such as 365d. For each record, the operator counts that record and the later records " \
|
|
61
|
+
"that start at most this long after it ends, and passes along the record at the given position. " \
|
|
62
|
+
"So a person can have more than one record in the result."
|
|
50
63
|
option :group_by_date, type: :boolean,
|
|
51
|
-
instructions: "Choose whether to group by date when determining the nth occurrence, treating occurrences on the same day as a single occurrence"
|
|
64
|
+
instructions: "Choose whether to group by date when determining the nth occurrence, treating occurrences on the same day as a single occurrence",
|
|
65
|
+
meaning: "Treats the records of a person on one start_date as one occurrence, so the position counts dates. " \
|
|
66
|
+
"From the date at that position, the operator passes along the record with the lowest criterion_id, " \
|
|
67
|
+
"or the highest criterion_id for a negative position."
|
|
68
|
+
|
|
69
|
+
example "For each patient, select the Condition that represents the second occurrence of an MI",
|
|
70
|
+
[ "occurrence", 2, [ "icd9", "412" ] ]
|
|
71
|
+
example "For each patient, the first record on the second day with any of these diagnoses",
|
|
72
|
+
[ "occurrence", 2, [ "icd9", "410.00", "410.01", "250.00", "250.01" ], { "group_by_date" => true } ]
|
|
52
73
|
|
|
53
74
|
def query_cols
|
|
54
75
|
dynamic_columns + [ :rn ]
|