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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +176 -0
  3. data/README.md +4 -2
  4. data/assets/diagram/diagram.css +331 -0
  5. data/assets/diagram/fonts/OFL.txt +93 -0
  6. data/assets/diagram/fonts/Roboto-400.ttf +0 -0
  7. data/assets/diagram/fonts/Roboto-700.ttf +0 -0
  8. data/assets/diagram/fonts/Roboto-Italic-400.ttf +0 -0
  9. data/assets/diagram/fonts/Roboto-Italic-Latin.woff2 +0 -0
  10. data/assets/diagram/fonts/Roboto-Normal-Latin.woff2 +0 -0
  11. data/assets/diagram/operators.svg +224 -0
  12. data/config/vocabularies.csv +85 -85
  13. data/docs/spec/README.md +2624 -0
  14. data/docs/spec/examples/after-operator.json +562 -0
  15. data/docs/spec/examples/any-overlap-operator.json +106 -0
  16. data/docs/spec/examples/before-operator.json +437 -0
  17. data/docs/spec/examples/co-reported-operator.json +168 -0
  18. data/docs/spec/examples/contains-operator.json +97 -0
  19. data/docs/spec/examples/date-range-operator.json +275 -0
  20. data/docs/spec/examples/death-operator.json +103 -0
  21. data/docs/spec/examples/during-operator.json +178 -0
  22. data/docs/spec/examples/episode-operator.json +80 -0
  23. data/docs/spec/examples/equal-operator.json +79 -0
  24. data/docs/spec/examples/ethnicity-operator.json +86 -0
  25. data/docs/spec/examples/except-operator.json +536 -0
  26. data/docs/spec/examples/filter-operator.json +218 -0
  27. data/docs/spec/examples/first-operator.json +148 -0
  28. data/docs/spec/examples/gender-operator.json +257 -0
  29. data/docs/spec/examples/hospice-operator.json +40 -0
  30. data/docs/spec/examples/hospitalization-operator.json +131 -0
  31. data/docs/spec/examples/information-periods-operator.json +131 -0
  32. data/docs/spec/examples/last-operator.json +148 -0
  33. data/docs/spec/examples/match-operator.json +186 -0
  34. data/docs/spec/examples/nth-occurrence-operator.json +300 -0
  35. data/docs/spec/examples/numeric-filter-operator.json +77 -0
  36. data/docs/spec/examples/one-in-two-out-operator.json +158 -0
  37. data/docs/spec/examples/person-filter-operator.json +919 -0
  38. data/docs/spec/examples/person-operator.json +274 -0
  39. data/docs/spec/examples/place-of-service-filter-operator.json +57 -0
  40. data/docs/spec/examples/provenance-operator.json +148 -0
  41. data/docs/spec/examples/provider-filter-operator.json +77 -0
  42. data/docs/spec/examples/race-operator.json +131 -0
  43. data/docs/spec/examples/recall-operator.json +289 -0
  44. data/docs/spec/examples/snf-operator.json +40 -0
  45. data/docs/spec/examples/time-window-operator.json +736 -0
  46. data/docs/spec/examples/trim-date-end-operator.json +201 -0
  47. data/docs/spec/examples/trim-date-start-operator.json +201 -0
  48. data/docs/spec/examples/union-operator.json +547 -0
  49. data/docs/spec/examples/vocabulary-operator.json +232 -0
  50. data/lib/conceptql/behaviors/metadatable.rb +68 -2
  51. data/lib/conceptql/behaviors/provenanceable.rb +4 -1
  52. data/lib/conceptql/behaviors/utilizable.rb +3 -0
  53. data/lib/conceptql/code_case_mismatch_error.rb +50 -0
  54. data/lib/conceptql/data_model/gdm.rb +23 -2
  55. data/lib/conceptql/diagram_renderer/app.rb +59 -0
  56. data/lib/conceptql/diagram_renderer/configuration.rb +87 -0
  57. data/lib/conceptql/diagram_renderer/diagram_data.rb +80 -0
  58. data/lib/conceptql/diagram_renderer/drawing.rb +255 -0
  59. data/lib/conceptql/diagram_renderer/geometry.rb +380 -0
  60. data/lib/conceptql/diagram_renderer/input_error.rb +11 -0
  61. data/lib/conceptql/diagram_renderer/js_format.rb +112 -0
  62. data/lib/conceptql/diagram_renderer/resvg.rb +120 -0
  63. data/lib/conceptql/diagram_renderer/sprite.rb +73 -0
  64. data/lib/conceptql/diagram_renderer/static.rb +70 -0
  65. data/lib/conceptql/diagram_renderer/svg.rb +73 -0
  66. data/lib/conceptql/diagram_renderer/tree_layout.rb +287 -0
  67. data/lib/conceptql/diagram_renderer.rb +82 -0
  68. data/lib/conceptql/lexicon/lexicon_strategy.rb +16 -0
  69. data/lib/conceptql/lexicon.rb +63 -11
  70. data/lib/conceptql/nodifier.rb +5 -3
  71. data/lib/conceptql/operators/after.rb +15 -1
  72. data/lib/conceptql/operators/any_overlap.rb +6 -1
  73. data/lib/conceptql/operators/before.rb +11 -1
  74. data/lib/conceptql/operators/binary_operator_operator.rb +12 -2
  75. data/lib/conceptql/operators/co_reported.rb +9 -4
  76. data/lib/conceptql/operators/contains.rb +6 -1
  77. data/lib/conceptql/operators/count.rb +2 -0
  78. data/lib/conceptql/operators/date_range.rb +15 -3
  79. data/lib/conceptql/operators/death.rb +5 -1
  80. data/lib/conceptql/operators/dedup.rb +2 -0
  81. data/lib/conceptql/operators/during.rb +6 -1
  82. data/lib/conceptql/operators/episode.rb +13 -2
  83. data/lib/conceptql/operators/equal.rb +11 -1
  84. data/lib/conceptql/operators/ethnicity.rb +19 -7
  85. data/lib/conceptql/operators/except.rb +10 -1
  86. data/lib/conceptql/operators/filter.rb +6 -1
  87. data/lib/conceptql/operators/first.rb +19 -9
  88. data/lib/conceptql/operators/from.rb +3 -0
  89. data/lib/conceptql/operators/gender.rb +10 -2
  90. data/lib/conceptql/operators/hospice.rb +7 -1
  91. data/lib/conceptql/operators/hospitalization.rb +6 -1
  92. data/lib/conceptql/operators/information_periods.rb +26 -7
  93. data/lib/conceptql/operators/invalid.rb +4 -0
  94. data/lib/conceptql/operators/last.rb +19 -9
  95. data/lib/conceptql/operators/match.rb +6 -1
  96. data/lib/conceptql/operators/multiple_vocabularies.rb +17 -3
  97. data/lib/conceptql/operators/numeric.rb +4 -0
  98. data/lib/conceptql/operators/numeric_filter.rb +13 -3
  99. data/lib/conceptql/operators/occurrence.rb +33 -12
  100. data/lib/conceptql/operators/one_in_two_out.rb +22 -5
  101. data/lib/conceptql/operators/operator.rb +8 -4
  102. data/lib/conceptql/operators/person.rb +5 -1
  103. data/lib/conceptql/operators/person_filter.rb +14 -1
  104. data/lib/conceptql/operators/place_of_service_filter.rb +14 -3
  105. data/lib/conceptql/operators/provenance.rb +17 -8
  106. data/lib/conceptql/operators/provider_filter.rb +18 -6
  107. data/lib/conceptql/operators/race.rb +14 -9
  108. data/lib/conceptql/operators/read.rb +11 -1
  109. data/lib/conceptql/operators/recall.rb +34 -2
  110. data/lib/conceptql/operators/snf.rb +7 -1
  111. data/lib/conceptql/operators/sum.rb +4 -0
  112. data/lib/conceptql/operators/temporal_operator.rb +4 -2
  113. data/lib/conceptql/operators/time_window.rb +27 -4
  114. data/lib/conceptql/operators/trim_date.rb +6 -14
  115. data/lib/conceptql/operators/trim_date_end.rb +9 -3
  116. data/lib/conceptql/operators/trim_date_start.rb +9 -3
  117. data/lib/conceptql/operators/union.rb +13 -1
  118. data/lib/conceptql/operators/vocabulary.rb +80 -6
  119. data/lib/conceptql/paths.rb +9 -1
  120. data/lib/conceptql/version.rb +1 -1
  121. data/lib/conceptql/vocabularies/drg_systems.rb +3 -2
  122. data/lib/conceptql/vocabularies/entry.rb +14 -11
  123. data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +2 -1
  124. data/lib/conceptql.rb +3 -0
  125. metadata +75 -2
  126. data/doc/spec.md +0 -2091
@@ -6,6 +6,8 @@ module ConceptQL
6
6
  module Operators
7
7
  class Count < PassThru
8
8
  register __FILE__
9
+ hidden
10
+ category "Modify Data"
9
11
 
10
12
  desc "Counts the number of records that exactly match across all columns."
11
13
  # Every stream is a set, so count returns 1 for nearly every row
@@ -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 "Creates a set of all person records with the given start_date and end_date."
22
- option :start, type: :string
23
- option :end, type: :string
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 all death records. With an upstream stream, the operator selects the death record of each person in that stream."
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 person, passes along left hand records with a start_date and end_date within a right hand record's start_date and end_date."
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 "Groups each person's records into episodes, allowing for a gap between the end_date of one record and the start_date of the next."
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: "Allowed gap in days between end date of one event and start date of the next to consider them the same episode"
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 person, passes along left hand records that have the same value_as_number as a right hand record."
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 person records that match the given set of Ethnicity codes."
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
- db.from(source_table)
28
- .where(ethnicity_concept_id: ethnicity_concept_ids)
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.map do |words|
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
- [ 0, nil ]
51
+ 0
40
52
  end
41
- end.flatten
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 all incoming records that appear in the right hand set of records."
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 that share the same person, criterion_id, criterion_table, and criterion_domain with a right hand side record."
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 treats all streams as a single, large stream. It partitions
10
- # that larget stream by person_id, then sorts within those groupings
11
- # by start_date and then select at most one row per person, regardless
12
- # of how many different types of streams enter the operator
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 order of their ranking
15
- # is arbitrary
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
- # With either option, Occurrence's timed query returns every upstream
25
- # row instead of one row per person
26
- unsupported_options :at_least, :within
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
@@ -7,6 +7,9 @@ module ConceptQL
7
7
  include ConceptQL::Behaviors::Timeless
8
8
 
9
9
  register __FILE__
10
+ # Reads a raw table. It is not a public operator.
11
+ hidden
12
+ category "Select by Property"
10
13
  basic_type :selection
11
14
  no_desc
12
15
  option :domains, type: Array
@@ -10,8 +10,16 @@ module ConceptQL
10
10
  include ConceptQL::Behaviors::Windowable
11
11
  include ConceptQL::Behaviors::Timeless
12
12
 
13
- desc "Selects all person records that match the selected gender."
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 records."
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 records."
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 all information period records, which have the observation_period domain. " \
14
- "The operator takes no upstream stream. " \
15
- "Each argument is an information type concept ID, and with arguments, " \
16
- "the operator selects only the information periods of those types."
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
- ds = ds.where(where_clause) if where_clause
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
- def where_clause
33
- dm.information_period_where_clause(arguments)
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
@@ -5,6 +5,10 @@ module ConceptQL
5
5
  class Invalid < Operator
6
6
  register __FILE__
7
7
 
8
+ # Stands in for an operator that did not resolve, so no user picks it
9
+ hidden
10
+ category "Select by Clinical Codes"
11
+
8
12
  no_desc
9
13
  default_query_columns
10
14
 
@@ -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 treats all streams as a single, large stream. It partitions
10
- # that larget stream by person_id, then sorts within those groupings
11
- # by start_date and then select at most one row per person, regardless
12
- # of how many different types of streams enter the operator
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 order of their ranking
15
- # is arbitrary
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
- # With either option, Occurrence's timed query returns every upstream
25
- # row instead of one row per person
26
- unsupported_options :at_least, :within
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 hand records that have the same values as a right hand side record. By default, every column is compared. only_columns or except_columns narrows that set, and join_columns compares only the person and the listed columns. Two NULL values count as the same value."
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 "Includes records with a value_as_number that match the given criteria."
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 treats all streams as a single, large stream. It partitions
18
- # that larget stream by person_id, then sorts within those groupings
19
- # by start_date and then select at most one row per person, regardless
20
- # of how many different types of streams enter the operator
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 number counts from the earliest record, so 2 means the second record. " \
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
- argument :occurrence, type: :integer
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
- allows_one_upstream
44
- validate_at_least_one_upstream
51
+ validate_one_upstream
45
52
 
46
53
  option :at_least, type: :string,
47
- instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date. Example: -30d = 30 days before the existing date.'
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 numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date. Example: -30d = 30 days before the existing date.'
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 ]