conceptql 0.5.0 → 0.6.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 +454 -0
- data/config/vocabularies.csv +85 -87
- data/config/vocabulary_formats.sha +1 -0
- data/config/vocabulary_formats.tsv +39 -0
- data/doc/spec.md +207 -67
- data/lib/conceptql/behaviors/metadatable.rb +15 -3
- data/lib/conceptql/behaviors/provenanceable.rb +82 -8
- data/lib/conceptql/behaviors/utilizable.rb +5 -1
- data/lib/conceptql/data_model/base.rb +2 -4
- data/lib/conceptql/data_model/gdm.rb +3 -1
- data/lib/conceptql/database.rb +7 -2
- data/lib/conceptql/date_adjuster.rb +12 -5
- data/lib/conceptql/lexicon/lexicon_gdm.rb +4 -1
- data/lib/conceptql/lexicon/lexicon_ohdsi.rb +19 -1
- data/lib/conceptql/lexicon/lexicon_strategy.rb +35 -1
- data/lib/conceptql/lexicon.rb +210 -2
- data/lib/conceptql/nodifier.rb +7 -1
- data/lib/conceptql/one_query_counts.rb +120 -0
- data/lib/conceptql/operators/count.rb +4 -0
- data/lib/conceptql/operators/death.rb +1 -1
- data/lib/conceptql/operators/dedup.rb +1 -1
- data/lib/conceptql/operators/episode.rb +10 -2
- data/lib/conceptql/operators/ethnicity.rb +0 -4
- data/lib/conceptql/operators/except.rb +5 -0
- data/lib/conceptql/operators/filter.rb +12 -2
- data/lib/conceptql/operators/hospice.rb +1 -1
- data/lib/conceptql/operators/hospitalization.rb +1 -1
- data/lib/conceptql/operators/information_periods.rb +4 -1
- data/lib/conceptql/operators/invalid.rb +7 -1
- data/lib/conceptql/operators/multiple_vocabularies.rb +19 -6
- data/lib/conceptql/operators/numeric.rb +51 -14
- data/lib/conceptql/operators/occurrence.rb +16 -4
- data/lib/conceptql/operators/operator.rb +63 -19
- data/lib/conceptql/operators/person.rb +1 -1
- data/lib/conceptql/operators/person_filter.rb +5 -0
- data/lib/conceptql/operators/place_of_service_filter.rb +38 -10
- data/lib/conceptql/operators/provenance.rb +12 -2
- data/lib/conceptql/operators/provider_filter.rb +5 -1
- data/lib/conceptql/operators/race.rb +0 -4
- data/lib/conceptql/operators/recall.rb +1 -1
- data/lib/conceptql/operators/snf.rb +2 -2
- data/lib/conceptql/operators/sum.rb +46 -5
- data/lib/conceptql/operators/temporal_operator.rb +6 -2
- data/lib/conceptql/operators/trim_date.rb +52 -6
- data/lib/conceptql/operators/trim_date_end.rb +11 -1
- data/lib/conceptql/operators/trim_date_start.rb +11 -1
- data/lib/conceptql/operators/union.rb +45 -1
- data/lib/conceptql/operators/vocabulary.rb +7 -3
- data/lib/conceptql/paths.rb +10 -0
- data/lib/conceptql/query.rb +78 -24
- data/lib/conceptql/query_modifiers/gdm/provider_query_modifier.rb +7 -7
- data/lib/conceptql/rdbms/generic.rb +21 -1
- data/lib/conceptql/rdbms/spark.rb +12 -0
- data/lib/conceptql/scope.rb +187 -24
- data/lib/conceptql/version.rb +1 -1
- data/lib/conceptql/vocabularies/drg_systems.rb +118 -0
- data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +10 -4
- data/lib/conceptql/vocabularies/entry.rb +15 -3
- data/lib/conceptql/vocabularies/formats.rb +59 -0
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +6 -1
- data/lib/conceptql/window/table.rb +12 -3
- metadata +8 -4
- data/lib/conceptql/operators/concurrent_within.rb +0 -69
data/doc/spec.md
CHANGED
|
@@ -33,7 +33,7 @@ GDM keeps the data in a small number of tables:
|
|
|
33
33
|
- deaths
|
|
34
34
|
- One row for each recorded death
|
|
35
35
|
|
|
36
|
-
ConceptQL supports two variants of GDM. The data model `gdm` reads clinical codes from clinical_codes. The data model `gdm_wide` reads them from observations, a wide table that holds the same rows as clinical_codes with the columns of their contexts and collections joined on. ConceptQL looks up codes, such as ICD-9 412, in a vocabulary database to find their concept IDs.
|
|
36
|
+
ConceptQL supports two variants of GDM. The data model `gdm` reads clinical codes from clinical_codes. The data model `gdm_wide` reads them from observations, a wide table that holds the same rows as clinical_codes with the columns of their contexts and collections joined on. ConceptQL looks up codes, such as ICD-9 412, in a vocabulary database to find their concept IDs. When the data database is a real database, and neither its search path nor the database at LEXICON_URL has the vocabulary tables, ConceptQL raises an error instead of running, because without a vocabulary every code would match no rows.
|
|
37
37
|
|
|
38
38
|
For instance, using ConceptQL we can take a statement that looks like this:
|
|
39
39
|
|
|
@@ -256,6 +256,43 @@ Selection operators are the parts of a ConceptQL query that search for specific
|
|
|
256
256
|
|
|
257
257
|
There are _many_ selection operators. A list of currently implemented operators is available in Appendix A.
|
|
258
258
|
|
|
259
|
+
### DRG Codes
|
|
260
|
+
|
|
261
|
+
CMS renumbered the Diagnosis-Related Group (DRG) codes on 2007-10-01, when the MS-DRG system replaced the CMS-DRG system. Most codes got a new meaning: DRG 205 meant a liver disorder in CMS-DRG and means other respiratory system diagnoses in MS-DRG. One vocabulary, DRG, holds the codes of both systems, and a clinical code does not record which meaning applied. So a statement names the DRG system, and ConceptQL selects by the code and by the start_date of the clinical code:
|
|
262
|
+
|
|
263
|
+
- `ms_drg` selects the DRG codes whose start_date is on or after 2007-10-01.
|
|
264
|
+
- `cms_drg` selects the DRG codes whose start_date is before 2007-10-01.
|
|
265
|
+
|
|
266
|
+
```YAML
|
|
267
|
+
---
|
|
268
|
+
- ms_drg
|
|
269
|
+
- '205'
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+

|
|
274
|
+
|
|
275
|
+
| person_id | criterion_id | criterion_table | criterion_domain | start_date | end_date | source_value |
|
|
276
|
+
| --------- | ------------ | --------------- | -------------------- | ---------- | ---------- | ------------ |
|
|
277
|
+
| 17 | 1365 | observations | procedure_occurrence | 2010-06-02 | 2010-06-06 | 205 |
|
|
278
|
+
| 68 | 18 | observations | procedure_occurrence | 2008-04-19 | 2008-04-23 | 205 |
|
|
279
|
+
| 95 | 603 | observations | procedure_occurrence | 2008-08-27 | 2008-09-01 | 205 |
|
|
280
|
+
|
|
281
|
+
```YAML
|
|
282
|
+
---
|
|
283
|
+
- cms_drg
|
|
284
|
+
- '205'
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+

|
|
289
|
+
|
|
290
|
+
`No Results found.`
|
|
291
|
+
|
|
292
|
+
Both operators check codes as other vocabulary operators do: a DRG code has 3 digits, and a code that the DRG vocabulary does not have gets a warning. When the vocabulary records the system of each meaning (the OHDSI concept class MS-DRG or DRG), the code list of a statement names each code by its meaning in the operator's system. Otherwise it names the code by all of its meanings.
|
|
293
|
+
|
|
294
|
+
The `drg` operator is deprecated. It selects the DRG codes of both systems, whatever the date. It still runs, but annotation shows a warning on it, and a later release will make it an error.
|
|
295
|
+
|
|
259
296
|
## All Other Operators i.e. Mutation Operators
|
|
260
297
|
|
|
261
298
|
Virtually all other operators add, remove, filter, or otherwise alter streams of results. They are discussed in this section.
|
|
@@ -269,7 +306,9 @@ Because streams represent sets of results, its makes sense to include a operator
|
|
|
269
306
|
- Takes any number of upstream operators and aggregates their streams
|
|
270
307
|
- Unions together streams with identical types
|
|
271
308
|
- Think of streams with the same type flowing together into a single stream
|
|
272
|
-
- Union
|
|
309
|
+
- Union removes duplicates: a result that arrives from more than one upstream operator appears once
|
|
310
|
+
- Every stream is a set, so no operator passes along two identical results
|
|
311
|
+
- Two results are identical when they have the same value in every column, label included
|
|
273
312
|
- Streams with the different types flow along together concurrently without interacting
|
|
274
313
|
- It does not make sense to union, say, condition_occurrence results with procedure_occurrence results, so streams with different types won't mingle together, but will continue to flow downstream in parallel
|
|
275
314
|
|
|
@@ -353,9 +392,35 @@ Because streams represent sets of results, its makes sense to include a operator
|
|
|
353
392
|
| 17 | 31480 | observations | condition_occurrence | 2009-05-21 | 2009-05-21 | 486 |
|
|
354
393
|
| 17 | 53417 | observations | procedure_occurrence | 2008-04-05 | 2008-04-05 | 99231 |
|
|
355
394
|
|
|
395
|
+
```YAML
|
|
396
|
+
---
|
|
397
|
+
- union
|
|
398
|
+
- - first
|
|
399
|
+
- - icd9
|
|
400
|
+
- '412'
|
|
401
|
+
- - icd9
|
|
402
|
+
- '412'
|
|
403
|
+
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+

|
|
407
|
+
|
|
408
|
+
| person_id | criterion_id | criterion_table | criterion_domain | start_date | end_date | source_value |
|
|
409
|
+
| --------- | ------------ | --------------- | -------------------- | ---------- | ---------- | ------------ |
|
|
410
|
+
| 11 | 82614 | observations | condition_occurrence | 2008-10-29 | 2008-10-29 | 412 |
|
|
411
|
+
| 17 | 27269 | observations | condition_occurrence | 2008-08-25 | 2008-08-25 | 412 |
|
|
412
|
+
| 17 | 46165 | observations | condition_occurrence | 2009-04-30 | 2009-04-30 | 412 |
|
|
413
|
+
| 38 | 40556 | observations | condition_occurrence | 2010-02-12 | 2010-02-12 | 412 |
|
|
414
|
+
| 54 | 74606 | observations | condition_occurrence | 2008-06-05 | 2008-06-05 | 412 |
|
|
415
|
+
| 60 | 986 | observations | condition_occurrence | 2009-07-19 | 2009-07-22 | 412 |
|
|
416
|
+
| 66 | 16171 | observations | condition_occurrence | 2009-07-25 | 2009-07-25 | 412 |
|
|
417
|
+
| 73 | 52843 | observations | condition_occurrence | 2008-11-16 | 2008-11-16 | 412 |
|
|
418
|
+
| 77 | 58165 | observations | condition_occurrence | 2010-10-06 | 2010-10-06 | 412 |
|
|
419
|
+
| 81 | 1405 | observations | condition_occurrence | 2009-01-28 | 2009-01-30 | 412 |
|
|
420
|
+
|
|
356
421
|
### Filter
|
|
357
422
|
|
|
358
|
-
This operator takes two sets of incoming streams, a left-hand stream and a right-hand stream. It passes along each left-hand result that also appears in the right-hand stream. Two results are the same when they have the same person_id, criterion_id, criterion_domain, start_date, and end_date. The operator discards all results in the right-hand stream. Use filter to keep only the results that two streams have in common, as an intersection would.
|
|
423
|
+
This operator takes two sets of incoming streams, a left-hand stream and a right-hand stream. It passes along each left-hand result that also appears in the right-hand stream. Two results are the same when they have the same person_id, criterion_id, criterion_table, criterion_domain, start_date, and end_date. A criterion_id identifies a result only within its criterion_table, so results from two tables never match, even when their ids are equal. The operator discards all results in the right-hand stream. Use filter to keep only the results that two streams have in common, as an intersection would.
|
|
359
424
|
|
|
360
425
|
```YAML
|
|
361
426
|
---
|
|
@@ -524,6 +589,62 @@ And just to show how multiple streams behave:
|
|
|
524
589
|
| 66 | 16171 | observations | condition_occurrence | 2009-07-25 | 2009-07-25 | 412 |
|
|
525
590
|
| 73 | 52843 | observations | condition_occurrence | 2008-11-16 | 2008-11-16 | 412 |
|
|
526
591
|
|
|
592
|
+
## Filtering by Provenance
|
|
593
|
+
|
|
594
|
+
### provenance
|
|
595
|
+
|
|
596
|
+
`provenance` passes along the events of its upstream stream that come from a given kind of source record. Each event has two provenances. Its file provenance tells which kind of file the record came from, such as `inpatient`, `outpatient` or `carrier_claim`. Its code provenance tells the position of the code in the record, such as `primary`, `first` or `admitting`. Each argument of `provenance` is a keyword from the file or the code provenance vocabulary, and the operator keeps an event when its provenance is that keyword's concept or one of its descendants.
|
|
597
|
+
|
|
598
|
+
A plain keyword, such as `inpatient` or `primary_or_first`, keeps an event when either provenance matches it. A keyword of the form `<file keyword>:<code keyword>` keeps an event when its file provenance matches the file keyword AND its code provenance matches the code keyword. The left half must be a file provenance keyword and the right half a code provenance keyword. Any other keyword with a `:` is an error.
|
|
599
|
+
|
|
600
|
+
When `provenance` has several keywords, it keeps the events that match any one of them. So `["inpatient:primary", "outpatient:first"]` keeps the inpatient events with a primary code and the outpatient events with a first code.
|
|
601
|
+
|
|
602
|
+
```YAML
|
|
603
|
+
---
|
|
604
|
+
- provenance
|
|
605
|
+
- - icd9
|
|
606
|
+
- '250.00'
|
|
607
|
+
- inpatient:primary_or_first
|
|
608
|
+
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+

|
|
612
|
+
|
|
613
|
+
| person_id | criterion_id | criterion_table | criterion_domain | start_date | end_date | source_value |
|
|
614
|
+
| --------- | ------------ | --------------- | -------------------- | ---------- | ---------- | ------------ |
|
|
615
|
+
| 37 | 310 | observations | condition_occurrence | 2009-01-19 | 2009-01-20 | 25000 |
|
|
616
|
+
| 68 | 19 | observations | condition_occurrence | 2008-04-19 | 2008-04-23 | 25000 |
|
|
617
|
+
| 68 | 1596 | observations | condition_occurrence | 2008-11-10 | 2008-11-11 | 25000 |
|
|
618
|
+
| 131 | 676 | observations | condition_occurrence | 2008-03-21 | 2008-03-24 | 25000 |
|
|
619
|
+
| 207 | 106 | observations | condition_occurrence | 2010-04-28 | 2010-05-02 | 25000 |
|
|
620
|
+
| 237 | 918 | observations | condition_occurrence | 2010-07-16 | 2010-07-19 | 25000 |
|
|
621
|
+
|
|
622
|
+
The keyword `*` keeps an event that has any file or code provenance. It drops only the events whose two provenances are both empty. Several keywords are ORed, so `*` with other keywords keeps the same events as `*` alone. A `*` in a `<file keyword>:<code keyword>` keyword is an error. `provenance` needs at least one keyword: with none, it is the error `no provenance keywords; use "*" for any provenance`.
|
|
623
|
+
|
|
624
|
+
```YAML
|
|
625
|
+
---
|
|
626
|
+
- provenance
|
|
627
|
+
- - icd9
|
|
628
|
+
- '250.00'
|
|
629
|
+
- "*"
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+

|
|
634
|
+
|
|
635
|
+
| person_id | criterion_id | criterion_table | criterion_domain | start_date | end_date | source_value |
|
|
636
|
+
| --------- | ------------ | --------------- | -------------------- | ---------- | ---------- | ------------ |
|
|
637
|
+
| 2 | 89752 | observations | condition_occurrence | 2009-05-11 | 2009-05-11 | 25000 |
|
|
638
|
+
| 2 | 57897 | observations | condition_occurrence | 2009-05-16 | 2009-05-16 | 25000 |
|
|
639
|
+
| 2 | 76985 | observations | condition_occurrence | 2009-07-31 | 2009-07-31 | 25000 |
|
|
640
|
+
| 3 | 93400 | observations | condition_occurrence | 2009-11-17 | 2009-11-17 | 25000 |
|
|
641
|
+
| 5 | 89717 | observations | condition_occurrence | 2009-06-28 | 2009-06-28 | 25000 |
|
|
642
|
+
| 7 | 15942 | observations | condition_occurrence | 2008-04-23 | 2008-04-23 | 25000 |
|
|
643
|
+
| 7 | 69706 | observations | condition_occurrence | 2008-05-12 | 2008-05-12 | 25000 |
|
|
644
|
+
| 7 | 38732 | observations | condition_occurrence | 2008-06-09 | 2008-06-09 | 25000 |
|
|
645
|
+
| 7 | 38733 | observations | condition_occurrence | 2008-06-09 | 2008-06-09 | 25000 |
|
|
646
|
+
| 7 | 70454 | observations | condition_occurrence | 2008-10-02 | 2008-10-02 | 25000 |
|
|
647
|
+
|
|
527
648
|
## Co-reported Events
|
|
528
649
|
|
|
529
650
|
### co_reported
|
|
@@ -856,16 +977,25 @@ There are situations when the date columns associated with a result should have
|
|
|
856
977
|
|
|
857
978
|
- Takes 2 arguments
|
|
858
979
|
- First argument is the stream on which to operate
|
|
859
|
-
- Second argument is a hash with two keys: [:start, :end] each with a value in the following format: "(
|
|
980
|
+
- Second argument is a hash with two keys: [:start, :end] each with a value in the following format: "[res]\*([-+]?\\d\*[dwmy])+" or "[res]\*[-+]?\\d+"
|
|
981
|
+
- d is days, w is weeks (7 days), m is months and y is years
|
|
982
|
+
- A unit without a number means 1 of that unit
|
|
860
983
|
- Both start and end must be defined, even if you are only adjusting one of the dates
|
|
861
984
|
- Some examples
|
|
862
985
|
- 30d => 30 days
|
|
863
986
|
- 20 => 20 days
|
|
864
987
|
- d => 1 day
|
|
988
|
+
- 2w => 2 weeks (14 days)
|
|
865
989
|
- 1y => 1 year
|
|
866
990
|
- -1m => -1 month
|
|
867
991
|
- 10d3m => 3 months and 10 days
|
|
868
992
|
- -2y10m-3d => -2 years, +10 months, -3 days
|
|
993
|
+
- An optional prefix of the letters r, e and s comes before the amounts
|
|
994
|
+
- r reverses the adjustment: each amount counts back instead of forward, so R365d means 365 days before the date and R-1m means 1 month after it
|
|
995
|
+
- e adjusts from the result's end_date, so E30d means end_date + 30 days
|
|
996
|
+
- s adjusts from the result's start_date, so S7d means start_date + 7 days
|
|
997
|
+
- Without e or s, the start value adjusts start_date and the end value adjusts end_date
|
|
998
|
+
- A bare number means days, also after an r, e or s prefix: R365, E30 and S7 mean the same as R365d, E30d and S7d
|
|
869
999
|
- The start or end value can also be '', '0', or nil
|
|
870
1000
|
- This will leave the date unaffected
|
|
871
1001
|
- The start or end value can also be the string 'start' or 'end'
|
|
@@ -1036,6 +1166,27 @@ Person streams carry a patient's date of birth in their date columns. This makes
|
|
|
1036
1166
|
| 152 | 82988 | observations | condition_occurrence | 2008-08-16 | 2008-08-16 | 412 |
|
|
1037
1167
|
| 152 | 84298 | observations | condition_occurrence | 2010-11-22 | 2010-11-22 | 412 |
|
|
1038
1168
|
|
|
1169
|
+
### trim_date_end and trim_date_start
|
|
1170
|
+
|
|
1171
|
+
- trim_date_end takes a left stream and a right stream. For each person, it trims the end_date of each left record to the earliest start_date of the person's right records.
|
|
1172
|
+
- A left record whose start_date comes after that right start_date is discarded.
|
|
1173
|
+
- A left record passes through unchanged when its person has no right records, or when that right start_date comes after its end_date.
|
|
1174
|
+
- trim_date_start trims the other end. For each person, it trims the start_date of each left record to the latest end_date of the person's right records.
|
|
1175
|
+
- A left record whose end_date comes before that right end_date is discarded.
|
|
1176
|
+
- A left record passes through unchanged when its person has no right records, or when that right end_date comes before its start_date.
|
|
1177
|
+
- With join_columns, the person's right records are the right records of the person with the same join_columns values as the left record.
|
|
1178
|
+
- With compare_all: true, trim_date_end and trim_date_start compare each left record with every right record of the person (and of the join_columns), not only with the person's first or last right record, and return each left record once.
|
|
1179
|
+
- trim_date_end trims its end_date to the earliest right start_date on or after its start_date.
|
|
1180
|
+
- trim_date_start trims its start_date to the latest right end_date on or before its end_date.
|
|
1181
|
+
- A left record is discarded only when it has right records and none of them qualifies.
|
|
1182
|
+
|
|
1183
|
+
### Window tables
|
|
1184
|
+
|
|
1185
|
+
- The `window` statement runs a statement inside windows: `["window", <statement>, {"window_table": <window table>}]`.
|
|
1186
|
+
- The window table has the columns person_id, start_date and end_date. It is the name of a table or a ConceptQL statement.
|
|
1187
|
+
- Each selection operator keeps only the results whose person has a window and whose start_date falls in that window. The column window_id of a result identifies its window.
|
|
1188
|
+
- If the window table is a ConceptQL statement with errors, annotate reports them under window_table and counts nothing. Running the statement raises InvalidStatementError.
|
|
1189
|
+
|
|
1039
1190
|
## Type Conversion
|
|
1040
1191
|
|
|
1041
1192
|
There are situations where it is appropriate to convert the type of a stream of results into a different type. In programmer parlance, we say "typecasting" or "casting", which is the terminology we'll use here. A good analogy and mnemonic for casting is to think of taking a piece of metal, say a candle holder, melting it down, and recasting it into, say, a lamp. We'll do something similar with streams. We'll take, for example, a condition_occurrence stream and recast it into a stream of person.
|
|
@@ -1518,64 +1669,37 @@ With a `numeric` operator defined, we can use the `sum` operator to sum the valu
|
|
|
1518
1669
|
|
|
1519
1670
|
### Counting
|
|
1520
1671
|
|
|
1521
|
-
The `count` operator groups identical results and stores the number of results in each group in the value_as_number column. Two results are identical only when all of their columns match, including criterion_id.
|
|
1672
|
+
The `count` operator is deprecated. It still runs, but annotation shows a warning on it, and a later release will make it an error. `count` groups identical results and stores the number of results in each group in the value_as_number column. Two results are identical only when all of their columns match, including criterion_id. Every stream is a set, so `count` returns 1 for nearly every result. It does not count the events of each person.
|
|
1522
1673
|
|
|
1523
|
-
To count the events of each person, set value_as_number to 1 with `numeric`, and then use `sum
|
|
1674
|
+
To count the events of each person, set value_as_number to 1 with `numeric`, and then use `sum` with its `group_by_person` option. `sum` gives one person result for each person, and value_as_number holds the number of events. Without `group_by_person`, `sum` puts the results of one person in several groups when they have different source_values, so the count of the person is split over several results.
|
|
1524
1675
|
|
|
1525
1676
|
```YAML
|
|
1526
1677
|
---
|
|
1527
1678
|
- sum
|
|
1528
1679
|
- - numeric
|
|
1529
1680
|
- 1
|
|
1530
|
-
- - icd9
|
|
1531
|
-
- '486'
|
|
1532
|
-
|
|
1533
|
-
```
|
|
1534
|
-
|
|
1535
|
-

|
|
1536
|
-
|
|
1537
|
-
| person_id | criterion_id | criterion_table | criterion_domain | start_date | end_date | source_value | value_as_number |
|
|
1538
|
-
| --------- | ------------ | --------------- | -------------------- | ---------- | ---------- | ------------ | --------------- |
|
|
1539
|
-
| 14 | 0 | observations | condition_occurrence | 2008-09-12 | 2009-04-05 | 486 | 2.0 |
|
|
1540
|
-
| 17 | 0 | observations | condition_occurrence | 2009-05-21 | 2009-05-21 | 486 | 1.0 |
|
|
1541
|
-
| 19 | 0 | observations | condition_occurrence | 2008-02-02 | 2008-02-03 | 486 | 1.0 |
|
|
1542
|
-
| 23 | 0 | observations | condition_occurrence | 2009-08-18 | 2010-02-17 | 486 | 3.0 |
|
|
1543
|
-
| 25 | 0 | observations | condition_occurrence | 2009-07-08 | 2009-07-08 | 486 | 1.0 |
|
|
1544
|
-
| 27 | 0 | observations | condition_occurrence | 2010-09-30 | 2010-09-30 | 486 | 1.0 |
|
|
1545
|
-
| 28 | 0 | observations | condition_occurrence | 2009-09-12 | 2009-09-12 | 486 | 1.0 |
|
|
1546
|
-
| 38 | 0 | observations | condition_occurrence | 2010-02-24 | 2010-02-24 | 486 | 3.0 |
|
|
1547
|
-
| 44 | 0 | observations | condition_occurrence | 2009-09-07 | 2010-08-30 | 486 | 6.0 |
|
|
1548
|
-
| 51 | 0 | observations | condition_occurrence | 2008-03-08 | 2008-10-18 | 486 | 2.0 |
|
|
1549
|
-
|
|
1550
|
-
We could do dumb things like count the number of times a row shows up in a union:
|
|
1551
|
-
|
|
1552
|
-
```YAML
|
|
1553
|
-
---
|
|
1554
|
-
- count
|
|
1555
|
-
- - union
|
|
1556
1681
|
- - icd9
|
|
1557
1682
|
- '412'
|
|
1558
|
-
|
|
1559
|
-
-
|
|
1560
|
-
|
|
1561
|
-
- inpatient
|
|
1683
|
+
- '414.01'
|
|
1684
|
+
- '250.00'
|
|
1685
|
+
- :group_by_person: true
|
|
1562
1686
|
|
|
1563
1687
|
```
|
|
1564
1688
|
|
|
1565
|
-

|
|
1690
|
+
|
|
1691
|
+
| person_id | criterion_id | criterion_table | criterion_domain | start_date | end_date | source_value | value_as_number |
|
|
1692
|
+
| --------- | ------------ | --------------- | ---------------- | ---------- | ---------- | ------------ | --------------- |
|
|
1693
|
+
| 2 | 0 | patients | person | 2009-05-11 | 2009-07-31 | | 3.0 |
|
|
1694
|
+
| 3 | 0 | patients | person | 2009-11-17 | 2009-11-17 | | 1.0 |
|
|
1695
|
+
| 5 | 0 | patients | person | 2009-06-28 | 2009-06-28 | | 1.0 |
|
|
1696
|
+
| 6 | 0 | patients | person | 2010-09-26 | 2010-09-26 | | 1.0 |
|
|
1697
|
+
| 7 | 0 | patients | person | 2008-04-23 | 2010-07-24 | | 30.0 |
|
|
1698
|
+
| 8 | 0 | patients | person | 2008-10-17 | 2009-07-16 | | 3.0 |
|
|
1699
|
+
| 9 | 0 | patients | person | 2010-08-01 | 2010-09-12 | | 2.0 |
|
|
1700
|
+
| 11 | 0 | patients | person | 2008-06-17 | 2009-11-12 | | 4.0 |
|
|
1701
|
+
| 12 | 0 | patients | person | 2009-04-20 | 2009-05-21 | | 3.0 |
|
|
1702
|
+
| 13 | 0 | patients | person | 2008-03-05 | 2010-09-08 | | 9.0 |
|
|
1579
1703
|
|
|
1580
1704
|
#### Numeric Value Comparison
|
|
1581
1705
|
|
|
@@ -1593,11 +1717,12 @@ The `numeric_filter` operator takes one stream. It passes along the results whos
|
|
|
1593
1717
|
- 1
|
|
1594
1718
|
- - icd9
|
|
1595
1719
|
- '412'
|
|
1720
|
+
- :group_by_person: true
|
|
1596
1721
|
- :greater_than_or_equal_to: 2
|
|
1597
1722
|
|
|
1598
1723
|
```
|
|
1599
1724
|
|
|
1600
|
-

|
|
1601
1726
|
|
|
1602
1727
|
| person_id | criterion_id | criterion_table | criterion_domain | start_date | end_date | source_value | value_as_number |
|
|
1603
1728
|
| --------- | ------------ | --------------- | ---------------- | ---------- | ---------- | ---------------- | --------------- |
|
|
@@ -1646,27 +1771,42 @@ Numeric doesn't have to take a stream. If it doesn't have a stream as an argumen
|
|
|
1646
1771
|
- Sets start_date to the earliest start_date in the group
|
|
1647
1772
|
- Sets the end_date to the most recent end_date in the group
|
|
1648
1773
|
- Sets criterion_id to 0 since there is no particular single row that the result refers to anymore
|
|
1774
|
+
- Takes one option, `group_by_person`, which is true or false. When it is true, `sum` puts all of the results of each person in one group, whatever their type and source_value. It gives one person result for each person:
|
|
1775
|
+
- person_id is the person
|
|
1776
|
+
- criterion_domain is person and criterion_table is the person table, as a cast to `person` gives them
|
|
1777
|
+
- criterion_id is 0
|
|
1778
|
+
- start_date is the earliest start_date and end_date is the most recent end_date of the person's results
|
|
1779
|
+
- value_as_number is the sum of the value_as_number column of the person's results
|
|
1780
|
+
- The other columns, such as source_value, are NULL
|
|
1781
|
+
- In a window table, `sum` gives one result for each person and window
|
|
1649
1782
|
|
|
1650
1783
|
## Appendix A - Selection Operators
|
|
1651
1784
|
|
|
1652
1785
|
ConceptQL creates a vocabulary operator for each vocabulary in its vocabulary list (`config/vocabularies.csv`) and in its vocabulary database. This table lists the most common selection operators.
|
|
1653
1786
|
|
|
1654
|
-
| Operator Name | Stream Type | Arguments | Returns
|
|
1655
|
-
| ------------------- | -------------------- | ---------------------------------------- |
|
|
1656
|
-
| cpt | procedure_occurrence | 1 or more CPT codes | All clinical codes that match any of the CPT codes
|
|
1657
|
-
| icd9 | condition_occurrence | 1 or more ICD-9-CM diagnosis codes | All clinical codes that match any of the ICD-9 codes
|
|
1658
|
-
| icd9_procedure | procedure_occurrence | 1 or more ICD-9-CM procedure codes | All clinical codes that match any of the ICD-9 procedure codes
|
|
1659
|
-
| icd10cm | condition_occurrence | 1 or more ICD-10-CM codes | All clinical codes that match any of the ICD-10-CM codes
|
|
1660
|
-
| hcpcs | procedure_occurrence | 1 or more HCPCS codes | All clinical codes that match any of the HCPCS codes
|
|
1661
|
-
|
|
|
1662
|
-
|
|
|
1663
|
-
|
|
|
1664
|
-
|
|
|
1665
|
-
|
|
|
1666
|
-
|
|
|
1667
|
-
|
|
|
1668
|
-
|
|
|
1669
|
-
|
|
|
1787
|
+
| Operator Name | Stream Type | Arguments | Returns |
|
|
1788
|
+
| ------------------- | -------------------- | ---------------------------------------- | -------------------------------------------------------------------- |
|
|
1789
|
+
| cpt | procedure_occurrence | 1 or more CPT codes | All clinical codes that match any of the CPT codes |
|
|
1790
|
+
| icd9 | condition_occurrence | 1 or more ICD-9-CM diagnosis codes | All clinical codes that match any of the ICD-9 codes |
|
|
1791
|
+
| icd9_procedure | procedure_occurrence | 1 or more ICD-9-CM procedure codes | All clinical codes that match any of the ICD-9 procedure codes |
|
|
1792
|
+
| icd10cm | condition_occurrence | 1 or more ICD-10-CM codes | All clinical codes that match any of the ICD-10-CM codes |
|
|
1793
|
+
| hcpcs | procedure_occurrence | 1 or more HCPCS codes | All clinical codes that match any of the HCPCS codes |
|
|
1794
|
+
| ms_drg | procedure_occurrence | 1 or more DRG codes | All clinical codes from 2007-10-01 that match any of the DRG codes |
|
|
1795
|
+
| cms_drg | procedure_occurrence | 1 or more DRG codes | All clinical codes before 2007-10-01 that match any of the DRG codes |
|
|
1796
|
+
| loinc | observation | 1 or more LOINC codes | All clinical codes that match any of the LOINC codes |
|
|
1797
|
+
| ndc | drug_exposure | 1 or more NDC codes | All clinical codes that match any of the NDC codes |
|
|
1798
|
+
| rxnorm | drug_exposure | 1 or more RxNorm codes | All clinical codes that match any of the RxNorm codes |
|
|
1799
|
+
| snomed | condition_occurrence | 1 or more SNOMED codes | All clinical codes that match any of the SNOMED codes |
|
|
1800
|
+
| gender | person | 1 or more of Male, Female, and Unknown | All patients whose gender matches any of the arguments |
|
|
1801
|
+
| race | person | 1 or more race names or race concept IDs | All patients whose race matches any of the arguments |
|
|
1802
|
+
| death | death | None | All death records |
|
|
1803
|
+
| information_periods | observation_period | None | All information periods |
|
|
1804
|
+
| date_range | person | A start date and an end date | One result for each patient, with the given dates |
|
|
1805
|
+
| hospitalization | condition_occurrence | None | All inpatient admissions |
|
|
1806
|
+
| hospice | condition_occurrence | None | All hospice admissions |
|
|
1807
|
+
| snf | condition_occurrence | None | All skilled nursing facility admissions |
|
|
1808
|
+
|
|
1809
|
+
hospitalization, hospice and snf select admissions. On gdm, they read collections and admission_details. On gdm_wide, they read observations, but their results still have criterion_table collections. Their results have criterion_domain condition_occurrence, because GDM has no visit or admission domain. primary_concept_id, source_value and source_vocabulary_id hold one code of the admission that has primary provenance. That code is usually a diagnosis, but it can be a procedure or a DRG. When the admission has no code with primary provenance, these three columns are NULL. Because the results have the condition_occurrence type, union combines them with the results of condition operators such as icd9 into one stream.
|
|
1670
1810
|
|
|
1671
1811
|
## Appendix B - Algorithm Showcase
|
|
1672
1812
|
|
|
@@ -38,6 +38,15 @@ module ConceptQL
|
|
|
38
38
|
@conceptql_spec_id = value
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Marks an operator as deprecated. It still runs. Operator#validate adds
|
|
42
|
+
# the message to its annotation as a warning, and its metadata says
|
|
43
|
+
# deprecated: true. A subclass does not inherit the mark.
|
|
44
|
+
def deprecated(message = nil)
|
|
45
|
+
return @deprecated unless message
|
|
46
|
+
|
|
47
|
+
@deprecated = message
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def predominant_domains(*values)
|
|
42
51
|
return @predominant_domains if values.empty?
|
|
43
52
|
|
|
@@ -150,11 +159,12 @@ module ConceptQL
|
|
|
150
159
|
max_upstreams: @max_upstreams || 0,
|
|
151
160
|
arguments: @arguments || [],
|
|
152
161
|
options: process_options(@options),
|
|
153
|
-
|
|
162
|
+
# Strings, as JSON gives them: domains (for example death) are symbols
|
|
163
|
+
predominant_domains: (@domains || @predominant_domains || []).map(&:to_s),
|
|
154
164
|
desc: get_desc,
|
|
155
165
|
categories: @categories || [],
|
|
156
166
|
basic_type: @basic_type,
|
|
157
|
-
deprecated:
|
|
167
|
+
deprecated: (true if deprecated), # nil for an operator that is not deprecated
|
|
158
168
|
arity: arity,
|
|
159
169
|
aliases: @aliases,
|
|
160
170
|
spec_id: spec_id
|
|
@@ -176,8 +186,10 @@ module ConceptQL
|
|
|
176
186
|
end
|
|
177
187
|
end
|
|
178
188
|
|
|
189
|
+
# The desc that the operator set (no_desc sets ""), else the standard
|
|
190
|
+
# vocabulary text. A subclass does not inherit its parent's desc.
|
|
179
191
|
def get_desc
|
|
180
|
-
@
|
|
192
|
+
@desc || standard_description
|
|
181
193
|
end
|
|
182
194
|
|
|
183
195
|
def process_options(options)
|
|
@@ -1,26 +1,100 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
+
# Provenance keywords select records by their file provenance
|
|
5
|
+
# (file_provenance_type) and their code provenance (code_provenance_type).
|
|
6
|
+
#
|
|
7
|
+
# A plain keyword, such as "inpatient" or "primary", names a concept in the
|
|
8
|
+
# file or the code provenance vocabulary. It keeps a record when either
|
|
9
|
+
# column is in the descendants of that concept.
|
|
10
|
+
#
|
|
11
|
+
# A keyword "<file keyword>:<code keyword>", such as
|
|
12
|
+
# "inpatient:primary_or_first", keeps a record when its file provenance is
|
|
13
|
+
# in the descendants of the file keyword AND its code provenance is in the
|
|
14
|
+
# descendants of the code keyword.
|
|
15
|
+
#
|
|
16
|
+
# Several keywords are ORed: a record is kept when any keyword keeps it.
|
|
17
|
+
#
|
|
18
|
+
# The keyword "*" keeps a record when either column is any provenance
|
|
19
|
+
# concept. Because keywords are ORed, "*" with other keywords is "*".
|
|
4
20
|
module Provenanceable
|
|
21
|
+
PAIR_SEPARATOR = ":"
|
|
22
|
+
ANY = "*"
|
|
23
|
+
|
|
5
24
|
def self.included(base)
|
|
6
25
|
base.require_column :file_provenance_type
|
|
7
26
|
base.require_column :code_provenance_type
|
|
8
27
|
end
|
|
9
28
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
# No keyword at all is a validation error of Provenance. It still builds
|
|
30
|
+
# the condition of "*", as an invalid pair still builds a condition.
|
|
31
|
+
def build_where_from_codes(db, codes)
|
|
32
|
+
return plain_condition(db, []) if codes.empty? || codes.include?(ANY)
|
|
33
|
+
|
|
34
|
+
pairs, plain = codes.partition { |code| pair_keyword?(code) }
|
|
35
|
+
conditions = pairs.map { |pair| pair_condition(db, pair) }
|
|
36
|
+
conditions.unshift(plain_condition(db, plain)) if plain.any?
|
|
37
|
+
return conditions.first if conditions.one?
|
|
38
|
+
|
|
39
|
+
Sequel.|(*conditions)
|
|
14
40
|
end
|
|
15
41
|
|
|
16
|
-
|
|
42
|
+
# The keywords as written that name no provenance concept. A pair is
|
|
43
|
+
# recognized only when it has one separator, a file keyword on the left
|
|
44
|
+
# and a code keyword on the right. Case is compared exactly, as the lookup
|
|
45
|
+
# compares it: the provenance vocabularies have no vocabulary_formats row
|
|
46
|
+
# that declares case-insensitivity (seed conceptql-oji ruling "b"). So
|
|
47
|
+
# "Inpatient" names no concept, and the concept codes are in lower case.
|
|
48
|
+
# "*" is known. It is not a half of a pair.
|
|
49
|
+
def find_bad_keywords(db, codes)
|
|
50
|
+
pairs, plain = (codes - [ ANY ]).partition { |code| pair_keyword?(code) }
|
|
51
|
+
known = known_keywords(db, all_provenance_types_vocab)
|
|
52
|
+
bad = plain.reject { |code| known.include?(code.to_s) }
|
|
53
|
+
bad += pairs.reject { |pair| known_pair?(db, pair) }
|
|
54
|
+
codes & bad
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def pair_keyword?(code)
|
|
60
|
+
code.to_s.include?(PAIR_SEPARATOR)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def known_pair?(db, pair)
|
|
64
|
+
file, code, *rest = pair.split(PAIR_SEPARATOR, -1)
|
|
65
|
+
rest.empty? &&
|
|
66
|
+
known_keywords(db, dm.file_provenance_types_vocab).include?(file) &&
|
|
67
|
+
known_keywords(db, dm.code_provenance_types_vocab).include?(code)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The concept codes of the vocabularies.
|
|
71
|
+
def known_keywords(db, vocabularies)
|
|
72
|
+
dm.concept_codes(db, vocabularies)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def all_provenance_types_vocab
|
|
76
|
+
[ dm.file_provenance_types_vocab, dm.code_provenance_types_vocab ].flatten
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def prov_of(db, vocabularies, ancestors)
|
|
80
|
+
dm.descendants_of(db, dm.concept_ids(db, vocabularies, ancestors))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def plain_condition(db, codes)
|
|
17
84
|
Sequel.|(
|
|
18
|
-
{ file_provenance_type: prov_of(db,
|
|
85
|
+
{ file_provenance_type: prov_of(db, all_provenance_types_vocab, codes) },
|
|
86
|
+
{ code_provenance_type: prov_of(db, all_provenance_types_vocab, codes) }
|
|
19
87
|
)
|
|
20
88
|
end
|
|
21
89
|
|
|
22
|
-
|
|
23
|
-
|
|
90
|
+
# An invalid pair (validation reports it) still builds a condition. A
|
|
91
|
+
# half with no concept matches nothing, so the pair keeps no record.
|
|
92
|
+
def pair_condition(db, pair)
|
|
93
|
+
file, code = pair.split(PAIR_SEPARATOR, 2)
|
|
94
|
+
Sequel.&(
|
|
95
|
+
{ file_provenance_type: prov_of(db, dm.file_provenance_types_vocab, [ file ]) },
|
|
96
|
+
{ code_provenance_type: prov_of(db, dm.code_provenance_types_vocab, [ code ]) }
|
|
97
|
+
)
|
|
24
98
|
end
|
|
25
99
|
end
|
|
26
100
|
end
|
|
@@ -41,13 +41,17 @@ module ConceptQL
|
|
|
41
41
|
# With the Database's share_utilization option, the list is read from a
|
|
42
42
|
# table that every statement on the Database shares. The WITH entry only
|
|
43
43
|
# carries the list to Scope#with_ctes, which takes it out of the SQL and
|
|
44
|
-
# creates the table when the query first reads rows.
|
|
44
|
+
# creates the table when the query first reads rows. With
|
|
45
|
+
# CONCEPTQL_AVOID_CTES, with_ctes extracts nothing, so the scope keeps
|
|
46
|
+
# the table instead.
|
|
45
47
|
def evaluate(db)
|
|
46
48
|
cdb = nodifier.cdb
|
|
47
49
|
return super unless cdb.opts[:share_utilization]
|
|
48
50
|
|
|
49
51
|
list = select_it(query(db))
|
|
50
52
|
name = cdb.shared_utilization_table(list, scope)
|
|
53
|
+
return scope.add_shared_table(db, name, list) if ConceptQL.avoid_ctes?
|
|
54
|
+
|
|
51
55
|
db[name].with(name, list, Scope::SHARED_TABLE => true)
|
|
52
56
|
end
|
|
53
57
|
|
|
@@ -19,6 +19,7 @@ module ConceptQL
|
|
|
19
19
|
:concepts_by_name,
|
|
20
20
|
:concepts_ds,
|
|
21
21
|
:concepts_to_codes,
|
|
22
|
+
:concepts_to_codes_in_class,
|
|
22
23
|
:descendants_of,
|
|
23
24
|
:known_codes,
|
|
24
25
|
:related_concept_ids
|
|
@@ -258,10 +259,7 @@ module ConceptQL
|
|
|
258
259
|
end
|
|
259
260
|
|
|
260
261
|
def table_columns(*tables)
|
|
261
|
-
tables.
|
|
262
|
-
p o
|
|
263
|
-
p tables
|
|
264
|
-
end
|
|
262
|
+
tables.flat_map { |t| table_cols(t) }
|
|
265
263
|
end
|
|
266
264
|
|
|
267
265
|
def table_source_value(table)
|
|
@@ -115,8 +115,10 @@ module ConceptQL
|
|
|
115
115
|
:context_id
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# The schema of a data model does not change, so the answer is kept, as
|
|
119
|
+
# for start_date_columns and the other column lookups.
|
|
118
120
|
def source_vocabulary_ids
|
|
119
|
-
@source_vocabulary_ids
|
|
121
|
+
@source_vocabulary_ids ||= assign_column_to_table do |table, columns|
|
|
120
122
|
next if %w[patients death].any? { |tn| table.to_s =~ /#{tn}/ }
|
|
121
123
|
|
|
122
124
|
reggy = /#{table.to_s.split('_').first}_vocabulary_id/
|
data/lib/conceptql/database.rb
CHANGED
|
@@ -95,8 +95,10 @@ module ConceptQL
|
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# The LEXICON_URL database, or nil when it is unset. Lexicon tries the
|
|
98
|
-
# data db first
|
|
99
|
-
#
|
|
98
|
+
# data db first. When neither has the vocabulary tables, a real data db
|
|
99
|
+
# raises Lexicon::NoVocabularyError, and a db-less Database (db nil or a
|
|
100
|
+
# Sequel mock) falls back to LexiconNoDB, so no database (and no sqlite3
|
|
101
|
+
# gem) is needed here.
|
|
100
102
|
def make_lexicon_db
|
|
101
103
|
return unless ENV["LEXICON_URL"]
|
|
102
104
|
|
|
@@ -126,6 +128,9 @@ module ConceptQL
|
|
|
126
128
|
@source_tables.fetch(key) { db.table_exists?(table) && (@source_tables[key] = true) }
|
|
127
129
|
end
|
|
128
130
|
|
|
131
|
+
# The Lexicon passed as lexicon:, or one built from the data db and the
|
|
132
|
+
# LEXICON_URL database. Database.new calls it, so a real data db without
|
|
133
|
+
# vocabulary tables raises Lexicon::NoVocabularyError there.
|
|
129
134
|
def lexicon
|
|
130
135
|
@lexicon ||= Lexicon.new(self.class.lexicon_db, db, cache: @opts[:cache_lookups])
|
|
131
136
|
end
|