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
@@ -12,7 +12,15 @@ module ConceptQL
12
12
 
13
13
  register __FILE__
14
14
 
15
- desc "Identifies an event that appears either once in an inpatient setting or twice within a specified interval in an outpatient setting."
15
+ desc "For each person, passes along the earliest confirmed condition record. " \
16
+ "One inpatient record confirms a condition, and so do two outpatient records whose start dates are a given gap apart."
17
+
18
+ returns "One record per person: the earliest confirmed condition record. " \
19
+ "A returned inpatient record has the admission date as its start_date and the discharge date as its end_date, " \
20
+ "when the claim has them. Only condition records with inpatient, outpatient or carrier_claim provenance can be confirmed."
21
+
22
+ example "First diabetes diagnosis, either found once in an inpatient record, or twice in outpatient records at least 30 days apart",
23
+ [ "one_in_two_out", [ "icd9", "250.00" ], { "outpatient_minimum_gap" => "30d", "outpatient_event_to_return" => "Initial Event" } ]
16
24
 
17
25
  allows_one_upstream
18
26
  validate_one_upstream
@@ -21,13 +29,22 @@ module ConceptQL
21
29
  basic_type :temporal
22
30
 
23
31
  option :inpatient_length_of_stay, type: :integer, min: 0, default: 0,
24
- desc: "Minimum length of inpatient stay (in days) required for inpatient event to be valid", label: "Inpatient Length of Stay (Days)"
32
+ meaning: "An inpatient stay must be longer than this many days: an inpatient record counts only when its discharge date " \
33
+ "is more than this many days after its admission date. 0 or no value means no length-of-stay check.",
34
+ label: "Inpatient Length of Stay (Days)"
25
35
  option :outpatient_minimum_gap, type: :string, default: "30d",
26
- instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years".'
36
+ instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years".',
37
+ meaning: "A date adjustment, such as 30d. The confirming outpatient record must start at least this long " \
38
+ "after the initial outpatient record. Without this option, the gap is 30 days."
27
39
  option :outpatient_maximum_gap, type: :string, default: "365d",
28
- instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years".'
40
+ instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years".',
41
+ meaning: "A date adjustment, such as 365d. The confirming outpatient record must start no more than this long " \
42
+ "after the initial outpatient record. Without this option, there is no maximum gap."
29
43
  option :outpatient_event_to_return, type: :string, options: [ "Initial Event", "Confirming Event" ],
30
- default: "Initial Event", desc: "Which event to pass downstream"
44
+ default: "Initial Event",
45
+ meaning: "Which outpatient record the operator passes on when two outpatient records confirm the condition: " \
46
+ "the initial record or the confirming record. " \
47
+ "Without this option, or with any value other than Initial Event, it passes on the confirming record."
31
48
 
32
49
  validate_option DateAdjuster::VALID_INPUT, :outpatient_minimum_gap, :outpatient_maximum_gap
33
50
 
@@ -49,7 +49,8 @@ module ConceptQL
49
49
  BAD_FORMATS = [ BAD_FORMAT, NDC_BAD_FORMAT ].freeze
50
50
  UNKNOWN_CODE = "unknown code(s)"
51
51
 
52
- option :label, type: :string
52
+ option :label, type: :string,
53
+ meaning: "A short description of the operator's records. The operator writes it into the label column of each of its records, and a recall operator selects those records by it. Each label in a statement must be different."
53
54
 
54
55
  @validations = []
55
56
 
@@ -594,12 +595,15 @@ module ConceptQL
594
595
  end
595
596
  end
596
597
 
597
- # A blank value counts as not set, as in Occurrence#at_least_option
598
+ # A blank value counts as not set, as in Occurrence#at_least_option.
599
+ # So does false: the Jigsaw diagram editor writes false when a user
600
+ # clears a checkbox (seed conceptql-ufn).
598
601
  def validate_unsupported_options(*opts)
599
602
  opts.each do |opt|
600
- next if options[opt].to_s.strip.empty?
603
+ value = options[opt]
604
+ next if value == false || value.to_s.strip.empty?
601
605
 
602
- add_error("unsupported option", opt.to_s, options[opt])
606
+ add_error("unsupported option", opt.to_s, value)
603
607
  end
604
608
  end
605
609
 
@@ -10,7 +10,11 @@ module ConceptQL
10
10
 
11
11
  register __FILE__
12
12
 
13
- desc "Selects all person records. With an upstream stream, the operator selects the person record of each person in that stream."
13
+ desc "Selects every patient. With one upstream operator, it selects the patients who have a record in the upstream stream."
14
+ returns "One record for each selected patient, from the patients table. The type of the records is person, " \
15
+ "and their start_date and end_date are both the patient's birth date."
16
+ example "All patients", [ "person" ]
17
+ example "The patients with an old myocardial infarction (ICD-9-CM 412)", [ "person", [ "icd9cm", "412" ] ]
14
18
 
15
19
  allows_one_upstream
16
20
  domains :person
@@ -7,7 +7,20 @@ module ConceptQL
7
7
  class PersonFilter < BinaryOperatorOperator
8
8
  register __FILE__
9
9
 
10
- desc "Passes along left hand records with a corresponding person_id in the right hand records."
10
+ desc "Keeps each left-hand record whose patient has at least one record in the right-hand stream. The type and the dates of the right-hand records do not matter."
11
+ returns "The left-hand records of every type whose patient has at least one right-hand record. The right-hand records are not returned."
12
+ option_meaning :right, "The right-hand stream. The operator keeps the left-hand records of each patient who has a record in this stream, and does not pass these records on."
13
+
14
+ example "All MI Conditions for people who are male",
15
+ [ "person_filter", { left: [ "icd9", "412" ], right: [ "gender", "Male" ] } ]
16
+ example "All MI Conditions for people who had an office visit at some point in the data",
17
+ [ "person_filter", { left: [ "icd9", "412" ], right: [ "cpt", "99214" ] } ]
18
+ example "The same result as the example above, with the office visits converted to patients first",
19
+ [ "person_filter", { left: [ "icd9", "412" ], right: [ "person", [ "cpt", "99214" ] ] } ]
20
+ example "All MI Conditions for people who are Male OR had an office visit at some point in the data",
21
+ [ "person_filter", { left: [ "icd9", "412" ], right: [ "union", [ "cpt", "99214" ], [ "gender", "Male" ] ] } ]
22
+ example "Yields two types: all MI Conditions for people who are Male, and all office visit Procedures for people who are Male",
23
+ [ "person_filter", { left: [ "union", [ "icd9", "412" ], [ "cpt", "99214" ] ], right: [ "gender", "Male" ] } ]
11
24
  default_query_columns
12
25
 
13
26
  # PersonFilter has no `as` option, and a statement that sets one fails
@@ -31,10 +31,19 @@ module ConceptQL
31
31
  ANY = "*"
32
32
 
33
33
  desc "Passes along the records whose place of service is one of the given CMS Place of Service codes. " \
34
- "The code * matches a record that has any place of service. " \
35
34
  "A record that has no place of service is not passed along."
36
35
 
37
- argument :places_of_service, type: :codelist, vocab: VOCABULARY_ID
36
+ returns "The upstream records whose place of service is one of the given codes."
37
+
38
+ argument :places_of_service, type: :codelist, vocab: VOCABULARY_ID,
39
+ meaning: "One or more CMS Place of Service codes, such as 21 (inpatient hospital) or 22 (outpatient hospital). " \
40
+ "The code * matches a record whose place of service is any code of that vocabulary. " \
41
+ "An unknown code makes the statement invalid."
42
+
43
+ # The test data has no place of service, so the example returns no rows
44
+ example "Diabetes diagnoses (ICD-9 250.00) with the place of service outpatient hospital (22)",
45
+ [ "place_of_service_filter", 22, [ "icd9", "250.00" ] ], help: false
46
+
38
47
  category "Filter Single Stream"
39
48
  basic_type :temporal
40
49
  allows_one_upstream
@@ -51,9 +60,11 @@ module ConceptQL
51
60
  private
52
61
 
53
62
  # With no codes, the lookup returns every concept of the vocabulary.
63
+ # The codes come from the statement, whose validation reports an
64
+ # unknown code, so the lookup does not raise CodeCaseMismatchError.
54
65
  def place_of_service_concept_ids(db)
55
66
  codes = arguments.map(&:to_s)
56
- dm.concept_ids(db, VOCABULARY_ID, codes.include?(ANY) ? [] : codes)
67
+ dm.concept_ids(db, VOCABULARY_ID, codes.include?(ANY) ? [] : codes, check_case: false)
57
68
  end
58
69
 
59
70
  # With no lexicon there are no codes to check against, and every code
@@ -20,15 +20,24 @@ module ConceptQL
20
20
  register __FILE__
21
21
 
22
22
  desc "Passes along the records whose provenance matches at least one of the given keywords. " \
23
- "Each record has a file provenance, such as inpatient or outpatient, " \
24
- "and a code provenance, such as primary or first. " \
25
- "A plain keyword matches a record when either provenance matches it. " \
26
- "A keyword of the form <file keyword>:<code keyword> matches a record when its file provenance " \
27
- "matches the file keyword and its code provenance matches the code keyword. " \
28
- "The keyword * matches a record that has any file or code provenance. " \
29
- "At least one keyword is required."
23
+ "The provenance of a record says which kind of file it came from, such as inpatient, " \
24
+ "and where its code was on the claim, such as primary."
25
+
26
+ returns "The upstream records whose provenance matches at least one of the keywords."
27
+
28
+ argument :provenance_types, label: "Provenance Types", type: :codelist,
29
+ meaning: "One or more provenance keywords, in lower case. " \
30
+ "A file keyword, such as inpatient, outpatient or carrier_claim, says which kind of file a record came from. " \
31
+ "A code keyword, such as primary or first, says where the code was on the claim. " \
32
+ "A plain keyword matches a record when its file provenance or its code provenance matches it. " \
33
+ "A keyword also matches the provenances under it, so inpatient includes snf. " \
34
+ "A keyword of the form file:code, such as inpatient:primary, needs both the file and the code provenance to match. " \
35
+ "The keyword * matches a record that has any provenance. " \
36
+ "A statement with no keyword, or with an unknown keyword, is invalid."
37
+
38
+ example "Diabetes diagnoses (ICD-9 250.00) from inpatient files",
39
+ [ "provenance", "inpatient", [ "icd9", "250.00" ] ]
30
40
 
31
- argument :provenance_types, label: "Provenance Types", type: :codelist
32
41
  category "Filter Single Stream"
33
42
  basic_type :temporal
34
43
  allows_one_upstream
@@ -10,12 +10,24 @@ module ConceptQL
10
10
  register __FILE__
11
11
 
12
12
  desc "Passes along the records whose provider matches the given specialties and roles. " \
13
- "Each option is a comma-separated list of concept IDs, and * matches any value. " \
14
- "A record matches the specialties when its context specialty or its practitioner specialty " \
15
- "is in the list. " \
16
- "A record can have more than one specialty, so the operator can pass along the same record more than once."
17
- option :specialties, type: :string
18
- option :roles, type: :string
13
+ "Each option is a comma-separated list of concept IDs, and * matches any value."
14
+
15
+ returns "The upstream records that have a provider who matches both the specialties and the roles. " \
16
+ "A record can have more than one provider, so a record comes out once for each provider who matches. " \
17
+ "Each record gets the columns provider_id, context_specialty_concept_id, practitioner_specialty_concept_id " \
18
+ "and role_type_concept_id."
19
+
20
+ option :specialties, type: :string,
21
+ meaning: "A comma-separated list of specialty concept IDs, such as 38003868 (Endocrinology, Diabetes & Metabolism Physician), " \
22
+ "or * for any specialty. A provider matches when the specialty recorded for the provider on the claim, " \
23
+ "or the provider's own specialty, is in the list."
24
+ option :roles, type: :string,
25
+ meaning: "A comma-separated list of role concept IDs of the GDM_ROLE vocabulary, " \
26
+ "such as 100000067 (Attending physician) or 100176860 (Billing provider), or * for any role."
27
+
28
+ example "Diabetes diagnoses with an endocrinologist as the attending physician",
29
+ [ "provider_filter", [ "icd9", "250.00" ], { "specialties" => "38003868", "roles" => "100000067" } ]
30
+
19
31
  category "Filter Single Stream"
20
32
  basic_type :temporal
21
33
  allows_one_upstream
@@ -17,8 +17,16 @@ module ConceptQL
17
17
  include ConceptQL::Behaviors::Windowable
18
18
  include ConceptQL::Behaviors::Timeless
19
19
 
20
- desc "Selects person records that match the given set of Race codes."
21
- argument :races, type: :codelist, vocab: "Race"
20
+ desc "Selects the patients with the given races."
21
+ argument :races, type: :codelist, vocab: "Race",
22
+ meaning: "One or more race names, such as White, or race concept IDs. " \
23
+ "A name matches the concepts with that name in any vocabulary, and case does not matter. " \
24
+ "The operator also selects the more detailed races under each given race, " \
25
+ "and each concept that is a kind of one of the race concepts through an is-a relationship. " \
26
+ "A value that contains Unknown also selects the patients who have no race or race concept ID 0."
27
+ returns "One person record for each patient whose race is one of the given races. " \
28
+ "The start_date and the end_date of each record are the patient's birth date."
29
+ example "All patients recorded as white", [ "race", "White" ]
22
30
  domains :person
23
31
  category "Select by Property"
24
32
  basic_type :selection
@@ -42,17 +50,14 @@ module ConceptQL
42
50
 
43
51
  concept_ids = dm.concept_ids_by_name(db, words)
44
52
 
45
- c_ids = (concept_ids + actual_ids).map { |i| [ i, race_descendents[i] ] }.flatten.compact.uniq.sort
53
+ c_ids = (concept_ids + actual_ids.map(&:to_i)).map { |i| [ i, race_descendents[i] ] }.flatten.compact.uniq.sort
46
54
 
47
55
  c_ids = dm.related_concept_ids(db, c_ids)
48
56
 
49
- q = db.from(source_table)
50
- if words.any? { |w| w.match(/unknown/i) }
51
- c_ids += [ 0 ]
52
- q = q.where(race_concept_id: nil)
53
- end
57
+ condition = Sequel.expr(race_concept_id: c_ids)
58
+ condition = Sequel.|(condition, { race_concept_id: 0 }, { race_concept_id: nil }) if words.any? { |w| w.match(/unknown/i) }
54
59
 
55
- q.where(race_concept_id: c_ids)
60
+ db.from(source_table).where(condition)
56
61
  end
57
62
 
58
63
  def actual_ids
@@ -3,17 +3,27 @@
3
3
  require "sequel"
4
4
  require "sequel/adapters/mock"
5
5
  require_relative "../behaviors/labish"
6
+ require_relative "vocabulary"
6
7
 
7
8
  module ConceptQL
8
9
  module Operators
9
10
  class Read < Operator
10
11
  include ConceptQL::Behaviors::CodeLister
12
+ extend Vocabulary::SharedDocumentation
11
13
  register __FILE__
12
14
 
13
15
  preferred_name "READ"
16
+ # The vocabulary_name of Read in the OHDSI vocabulary table
17
+ long_name "NHS UK Read Codes Version 2 (HSCIC)"
18
+ # Metadata only: the records come from ReadGDM, which selects condition_occurrence
19
+ predominant_domains :condition_occurrence
14
20
  desc "Selects records based on the Read vocabulary."
15
- argument :read_codes, type: :codelist, vocab: "Read"
21
+ argument :read_codes, type: :codelist, vocab: "Read",
22
+ meaning: "One or more Read codes. Write each code as the Read vocabulary writes it. " \
23
+ "A code matches exactly, including case, and it does not select its child codes. " \
24
+ "A code alone of * selects every Read record."
16
25
  basic_type :selection
26
+ validate_no_upstreams
17
27
  category "Select by Clinical Codes"
18
28
  conceptql_spec_id "vocabulary"
19
29
 
@@ -14,9 +14,41 @@ module ConceptQL
14
14
  class Recall < Operator
15
15
  register __FILE__
16
16
 
17
- desc "Passes along the records of the operator in the statement that has the given label."
17
+ desc "Passes on the records of the operator in the statement that has the given label. " \
18
+ "Use it to break a complex statement into parts, or to use a stream of records more than once."
19
+ returns "The records of the operator that has the given label."
20
+ example 'Label neurological diagnoses as "neuro dxs". Recall them to remove those that were co-reported ' \
21
+ "with a nervous-system MS-DRG. Then apply the one inpatient, two outpatient rule. " \
22
+ "(The code lists are shortened.)",
23
+ [
24
+ "one_in_two_out",
25
+ [
26
+ "except",
27
+ {
28
+ "left" => [
29
+ "union",
30
+ [ "icd9", "331.0", "332.0", "340", "345.90" ],
31
+ [ "icd10cm", "G20", "G30.9", "G35", "G40.909" ],
32
+ { "label" => "neuro dxs" }
33
+ ],
34
+ "right" => [
35
+ "co_reported",
36
+ [ "recall", "neuro dxs" ],
37
+ [ "ms_drg", "056", "057", "058", "059", "060", "100", "101" ]
38
+ ]
39
+ }
40
+ ],
41
+ {
42
+ "outpatient_minimum_gap" => "30d",
43
+ "outpatient_maximum_gap" => "365d",
44
+ "outpatient_event_to_return" => "Confirming Event"
45
+ }
46
+ ]
18
47
 
19
- argument :name, type: :string
48
+ argument :name, type: :string,
49
+ meaning: "The label of the operator whose records to pass on. It must match the label exactly, " \
50
+ "including case. The labeled operator can come before or after the recall in the " \
51
+ "statement, but labeled streams that recall each other in a loop are invalid."
20
52
  category "Get Related Data"
21
53
  basic_type :selection
22
54
  validate_no_upstreams
@@ -8,7 +8,13 @@ module ConceptQL
8
8
  register __FILE__
9
9
  include ConceptQL::Behaviors::Utilizable
10
10
 
11
- desc "Selects skilled nursing facility (SNF) admission records."
11
+ desc "Selects admissions to skilled nursing facilities (SNF): one record for each admission whose claim came from an SNF file."
12
+ returns "One record for each SNF 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 SNF claims
17
+ example "All SNF admissions", [ "snf" ], help: false
12
18
 
13
19
  def collection_type
14
20
  "snf"
@@ -6,6 +6,10 @@ module ConceptQL
6
6
  module Operators
7
7
  class Sum < PassThru
8
8
  register __FILE__
9
+ # "We do not yet support numeric or sum in any meaningful way"
10
+ # (Ryan, 2026-11-03, seed conceptql-mdo.4)
11
+ hidden
12
+ category "Modify Data"
9
13
 
10
14
  def events_per_patient
11
15
  group_by_person? ? :single : :multiple
@@ -15,7 +15,8 @@ module ConceptQL
15
15
  default_query_columns
16
16
 
17
17
  option :within, type: :string,
18
- 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.'
18
+ instructions: 'Enter a positive number followed by "d", "w", "m" or "y" for days, weeks, months or years. Example: 30d.',
19
+ meaning: "A date adjustment, such as 30d, 2m or 1y-3d, that lets the compared dates be up to this amount apart. Each temporal operator applies it to its own comparison. Use a positive amount."
19
20
 
20
21
  # The temporal operators read join_columns through
21
22
  # BinaryOperatorOperator#join_columns_option, but do not declare it with
@@ -28,7 +29,8 @@ module ConceptQL
28
29
  class << self
29
30
  def allows_at_least_option
30
31
  option :at_least, type: :string,
31
- 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.'
32
+ instructions: 'Enter a positive number followed by "d", "w", "m" or "y" for days, weeks, months or years. Example: 30d.',
33
+ meaning: "A date adjustment, such as 7d or 1w, that sets the smallest gap between the compared dates. The gap must be larger than this amount. Use a positive amount."
32
34
  end
33
35
  end
34
36
 
@@ -8,8 +8,9 @@ module ConceptQL
8
8
  # A TimeWindow adjusts the start_date and end_date the incoming stream by the values specified in
9
9
  # the start and end arguments.
10
10
  #
11
- # Start and end take the format of ([+-]\d*[mdy])+. For example:
11
+ # Start and end take the format of ([+-]\d*[dwmy])+. For example:
12
12
  # 'd' => adjust date by one day. 2012-10-07 => 2012-10-08
13
+ # 'w' => adjust date by one week. 2012-10-07 => 2012-10-14
13
14
  # 'm' => adjust date by one month. 2012-10-07 => 2012-11-07
14
15
  # 'y' => adjust date by one year. 2012-10-07 => 2013-10-07
15
16
  # '1y' => adjust date by one year. 2012-10-07 => 2013-10-07
@@ -24,11 +25,33 @@ module ConceptQL
24
25
  class TimeWindow < PassThru
25
26
  register __FILE__
26
27
 
27
- desc "Adjusts start_date and end_date to create a new window of time for each record."
28
+ # The values that the start and end options take (DateAdjuster)
29
+ DATE_ADJUSTMENT_TEXT = "The value is an amount of time, made of one or more numbers, each with a unit: " \
30
+ "d (days), w (weeks), m (months) or y (years). Each part can have a - or + sign, " \
31
+ "a bare number means days, and a unit without a number means 1 of it. " \
32
+ "For example, -1m is 1 month back, and 10d3m is 3 months and 10 days. " \
33
+ "An empty value or 0 leaves the date unchanged. The value start or end uses the " \
34
+ "record's start_date or end_date, and a date in YYYY-MM-DD format sets the date to that date."
35
+
36
+ desc "Moves the start_date and the end_date of each record. " \
37
+ "Use it to shift or widen a record's dates before you compare them with other records."
28
38
  option :start, type: :string,
29
- 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.'
39
+ instructions: 'Enter a number followed by "d", "w", "m" or "y" for days, weeks, months or years, such as 30d. A negative number moves the date earlier. You can also enter start, end or a date in YYYY-MM-DD format.',
40
+ meaning: "How to change the start_date. If you omit it, the start_date does not change. #{DATE_ADJUSTMENT_TEXT}"
30
41
  option :end, type: :string,
31
- 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.'
42
+ instructions: 'Enter a number followed by "d", "w", "m" or "y" for days, weeks, months or years, such as 30d. A negative number moves the date earlier. You can also enter start, end or a date in YYYY-MM-DD format.',
43
+ meaning: "How to change the end_date. If you omit it, the end_date does not change. #{DATE_ADJUSTMENT_TEXT}"
44
+ returns "One record for each upstream record, with the start_date and the end_date that the options give. " \
45
+ "The other columns do not change."
46
+ example "All Diagnoses of Diabetes (ICD-9 250.01) within 30 days of an MI",
47
+ [ "during", { "left" => [ "icd9", "250.01" ], "right" => [ "time_window", [ "icd9", "412" ], { "start" => "-30d", "end" => "30d" } ] } ]
48
+ example "Shift the dates of all MIs back by 200 years", [ "time_window", [ "icd9", "412" ], { "start" => "-200y", "end" => "-200y" } ]
49
+ example "Widen the dates of all MIs to run from 2 months and 2 days before the MI to 1 year and 3 days after it",
50
+ [ "time_window", [ "icd9", "412" ], { "start" => "-2m-2d", "end" => "3d1y" } ]
51
+ example "Collapse the date range of all MIs to the start date: leave start_date unchanged and set end_date to start_date",
52
+ [ "time_window", [ "icd9", "412" ], { "start" => "", "end" => "start" } ]
53
+ example "Nonsensical, but allowed: swap the start_date and end_date",
54
+ [ "time_window", [ "icd9", "412" ], { "start" => "end", "end" => "start" } ]
32
55
  allows_one_upstream
33
56
  validate_one_upstream
34
57
  validate_no_arguments
@@ -4,6 +4,11 @@ require_relative "temporal_operator"
4
4
 
5
5
  module ConceptQL
6
6
  module Operators
7
+ # The base class of trim_date_end and trim_date_start. It does not
8
+ # register, so trim_date is not an operator name, and its subclasses set
9
+ # their own desc. The rest of this comment describes trim_date_end.
10
+ # trim_date_start does the same with the dates reversed.
11
+ #
7
12
  # Trims the end_date of the LHS set of records by the RHS's earliest
8
13
  # start_date (per person)
9
14
  # If a the RHS contains a start_date that comes before the LHS's start_date
@@ -27,20 +32,7 @@ module ConceptQL
27
32
  # (trim_date_start). An LHS record is discarded only when it has RHS
28
33
  # records and none of them passes.
29
34
  class TrimDate < TemporalOperator
30
- desc <<~EOF
31
- Trims the end_date of the left hand records (LHR) by the earliest
32
- start_date (per person) in the right hand records (RHR)
33
- If the RHR contains a start_date that comes before the start_date in the LHR
34
- that record in the LHR is completely discarded.
35
-
36
- If there is no record in the RHR for a record in the LHR, the record in the LHR is passed
37
- through unaffected.
38
-
39
- If the start_date of the record in the RHR is later than the end_date of the record in the LHR, the record in the LHR
40
- is passed through unaffected.
41
- EOF
42
-
43
- allows_one_upstream
35
+ no_desc
44
36
 
45
37
  def query(db)
46
38
  # If the RHS's min start date is less than the LHS start date,
@@ -17,9 +17,15 @@ module ConceptQL
17
17
  class TrimDateEnd < TrimDate
18
18
  register __FILE__
19
19
 
20
- desc "For each person, trims the end_date of all left hand records by the earliest start_date in the right hand records."
21
-
22
- allows_one_upstream
20
+ desc "For each patient, moves the end_date of each left-hand record back to the start_date of the right-hand record with the earliest start_date. A left-hand record that starts after that start_date is removed, and the left-hand records of a patient without right-hand records pass through unchanged."
21
+ returns "The left-hand records, each with its end_date set to the earlier of its own end_date and the start_date of the patient's right-hand record. Left-hand records that start after that start_date are not returned. The right-hand records are not returned."
22
+ option_meaning :left, "The left-hand stream. The operator moves the end_date of these records and passes them on."
23
+ option_meaning :right, "The right-hand stream. For each patient, the earliest start_date of these records sets the latest end_date of the left-hand records. These records are not passed on."
24
+ option_meaning :within, "A date adjustment, such as 30d, 2m or 1y-3d, that is subtracted from the start_date of the right-hand record before the comparison. The adjusted date is the one that moves the end_date of the left-hand record."
25
+
26
+ example "Each patient's span from birth to age 50 or to 1970-01-01, whichever is earlier (patients born after 1970 are dropped)",
27
+ [ "trim_date_end", { left: [ "time_window", [ "person" ], { end: "+50y" } ],
28
+ right: [ "date_range", { start: "1970-01-01", end: "1970-01-01" } ] } ]
23
29
 
24
30
  private
25
31
 
@@ -17,9 +17,15 @@ module ConceptQL
17
17
  class TrimDateStart < TrimDate
18
18
  register __FILE__
19
19
 
20
- desc "For each person, trims the start_date of all left hand records by the most recent end_date in the right hand records."
21
-
22
- allows_one_upstream
20
+ desc "For each patient, moves the start_date of each left-hand record forward to the end_date of the right-hand record with the latest start_date. A left-hand record that ends before that end_date is removed, and the left-hand records of a patient without right-hand records pass through unchanged."
21
+ returns "The left-hand records, each with its start_date set to the later of its own start_date and the end_date of the patient's right-hand record. Left-hand records that end before that end_date are not returned. The right-hand records are not returned."
22
+ option_meaning :left, "The left-hand stream. The operator moves the start_date of these records and passes them on."
23
+ option_meaning :right, "The right-hand stream. For each patient, the end_date of the record with the latest start_date sets the earliest start_date of the left-hand records. These records are not passed on."
24
+ option_meaning :within, "A date adjustment, such as 30d, 2m or 1y-3d, that is added to the end_date of the right-hand record before the comparison. The adjusted date is the one that moves the start_date of the left-hand record."
25
+
26
+ example "Each patient's span from birth to age 50, trimmed to start no earlier than 1980-01-01 (patients who were 50 before 1980 are dropped)",
27
+ [ "trim_date_start", { left: [ "time_window", [ "person" ], { end: "+50y" } ],
28
+ right: [ "date_range", { start: "1980-01-01", end: "1980-01-01" } ] } ]
23
29
 
24
30
  private
25
31
 
@@ -54,7 +54,19 @@ module ConceptQL
54
54
  vocabularies.length > 1
55
55
  end
56
56
 
57
- desc "Combines sets of incoming records into a single large set of records."
57
+ desc "Combines the records of all its upstream operators into one stream. " \
58
+ "Each record keeps its own type, so one stream can hold several types."
59
+
60
+ returns "Each record of each upstream. A record that comes from more than one upstream comes out once. " \
61
+ "Two records are the same only when every column is equal, including label."
62
+
63
+ example "Two streams of the same type (condition_occurrence) combined into one stream",
64
+ [ "union", [ "icd9", "412" ], [ "icd9", "250.01" ] ]
65
+ example "Two condition_occurrence streams combined, then combined with a procedure_occurrence stream",
66
+ [ "union", [ "union", [ "icd9", "412" ], [ "icd9", "250.01" ] ], [ "cpt", "99214" ] ]
67
+ example "Two condition_occurrence streams and a procedure_occurrence stream combined by a single union",
68
+ [ "union", [ "icd9", "412" ], [ "icd9", "250.01" ], [ "cpt", "99214" ] ]
69
+
58
70
  allows_many_upstreams
59
71
  category "Combine Streams"
60
72
  default_query_columns