Widelong - #1
Conversation
|
Rewrote the mapping spec document to focus on the mapping operations, rather than the database formats. specs/mapping-operations.qmd is the main mapping operations spec |
|
@martinwellman Can you let me know when this PR is good to review again? |
|
This PR is ready to review again. @yulric |
…rst column name Problem: On Windows machines if you do not specify the file encoding to be UTF-8-BOM when reading in a CSV file a i.. is appended to the first column header name. For example, if the column name is measures it will become i..measures. Solution: Add the fileEncoding argument when using the read.csv function.
yulric
left a comment
There was a problem hiding this comment.
The document itself looks pretty good! Just commend on the code and organization.
- I recommend using a library to handle pretty printing CSV files. Its a pretty common piece of functionality and there are established libraries that have been doing it for some time. For example, DT
- Recommend using renv to track all your dependencies in your project. Its like the requirements.txt file in Python.
- We try to put all asset files (like CSV files, images etc.) in a folder called assets.
| group_by: [ date, compartment ] | ||
| ``` | ||
|
|
||
| ## Usage: Grouping |
There was a problem hiding this comment.
I think its worthwhile to provide a quick grouping example here, one that's used in the other operations.
| @@ -0,0 +1,59 @@ | |||
| #' This file contains functions for converting various R types to | |||
There was a problem hiding this comment.
An annoying thing about R is that you're technically not allowed to have sub-directories within the R folder. I would move this file to the root of the R folder.
| # pipe characters, 2) add optional word breaks to strings (<wbr>), | ||
| # 3) concatenate strings in row with pipe separator. | ||
| row <- row %>% | ||
| sapply(function(x) str_replace_all(x, "\\|", "\\\\|")) %>% |
There was a problem hiding this comment.
When using a function from a library can you call it using the library name? So this would be stringr::str_replace_all().
With R its hard to know whether a function is from base R or from a library, so we always use the above syntax to call a function from a library.
| # 3) concatenate strings in row with pipe separator. | ||
| row <- row %>% | ||
| sapply(function(x) str_replace_all(x, "\\|", "\\\\|")) %>% | ||
| sapply(function(x) str_replace_all(x, "_", "_<wbr>")) %>% |
There was a problem hiding this comment.
Why are you replacing _ with _<wbr> here?
|
I updated this PR with all the requested changes. Let me know if everything looks good. I replace _ with _ for the headers in a markdown table to allow optional wrapping of the headers at each underscore. Since the ODM wide format can have very long names this helps the tables take up less space horizontally and a bit easier to read. I've commented the code to explain this. |
mathew-thomson
left a comment
There was a problem hiding this comment.
I think this looks great ! A fantastic break down, and really lays things out clearly. I think we may need to talk about ID generation when we move from wide to long, but otherwise I think this is in fantastic shape.
|
|
||
| ## Grouping | ||
|
|
||
| No grouping is required for wide-to-long, as we process each row in isolation (ie. row-by-row). |
There was a problem hiding this comment.
We may (forgive my ignorance here) need to do some grouping, however, in the sense that each wide row probably is a "measure set" and so would need a generated "measure set ID" to be common across all the new long rows.
| |---------------|-------------|----------|-------------------|---------------|-------------------|-----------------|-------|-------| | ||
| | Sept 18, 2023 | water | sample | liquid | SARS-CoV-2-N1 | gene copies per L | arithmetic mean | 1 | 40 | | ||
| | Sept 18, 2023 | water | sample | liquid | PMMoV | gene copies per L | arithmetic mean | 1 | 45 | | ||
| | Sept 18, 2023 | water | sample | liquid | pH | unitless | arithmetic mean | 1 | 6.1 | |
There was a problem hiding this comment.
As alluded to in my other comment, I think we may also need an operation for generating IDs for some of the new rows as well move from wide to long format. Measure sets, as I mention above, is one example. But quality IDs would be another example. I can try to generate an exhaustive list, if that would be helpful.
| df <- df[rows, ] | ||
|
|
||
| # Add optional word breaks after underscores, allowing headers to wrap | ||
| # and take up less room horizontally. |
There was a problem hiding this comment.
| # and take up less room horizontally. | |
| # and take up less room horizontally. Effectively, if a table gets too long this will result in the addition of a horizontal scrollbar instead of trying to squish all the columns. Squishing all the columns resulted in word-wrapping occurring that would look weird. |
| (water, sample, liquid, SARS-CoV-2-N1, gene copies per L, arithmetic mean, 1) | ||
| ``` | ||
|
|
||
| We can apply some string maps or custom transforms to clean these values: |
There was a problem hiding this comment.
I'd like to suggest describing this as a separate operation. I'd plan for each operation to be an individual function. These operations could be grouped into a method. So, pivoting wider and longer could be a 'method' (a collection of operations), but it seems more like an operation.
DougManuel
left a comment
There was a problem hiding this comment.
The PR looks good.
The pivot wider and longer works well. These are common concepts and practices with analysts, and the description works well from that analyst’s perspective.
Two late coming comments:
- I suggest splitting the pivot wide into more operations. Specifically, this could be a
recodeor similar.
We can apply some string maps, or custom transforms to clean these values:
- We should be clear or explicit that variable mapping involves getting or putting into specific tables. The table information is all available in the ODM, but it is worth reviewing.
|
|
||
| # Operation: Pivot Wider | ||
|
|
||
| Pivoting wider converts long format rows to wide format columns. It is based on R's [pivot_wider()](https://tidyr.tidyverse.org/reference/pivot_wider.html) function in the tidyverse and is the inverse of the [Pivot Longer](#operation-pivot-longer) operation. When pivoting wider we have (in the source table) name columns and value columns. The name columns will determine the wide format column name while the value columns will determine the new column's value. Often there is just one value column, but more than one can be provided, in which case we may transform the multiple values into a single value (eg. we can concatenate the values '24' and '12' to get the single value '24.12') |
There was a problem hiding this comment.
Is the aggregation operation something the user can set? E.g., maybe a user wants the values to be concatenated into a string, while others might want to take the arithmetic or geometric mean of the values
| mdtable_csv_file("../assets/examples/odm/measure-singledate-odmlong.csv", rows = 1) | ||
| ``` | ||
|
|
||
| Applying this same rule to the remaining two wide-name columns in the example, we get two more rows: |
There was a problem hiding this comment.
Depending on how the value aggregation is handled in long-to-wide, the number of resulting rows from this wide-to-long operation could change. If the long-to-wide yields a list of values, Each item would be mapped to its own row, whereas if the values were aggregated, it'll result in one row where there originally might have been several. Maybe that should be noted somehow. For measure reports, the value if the aggregation column could be changed to reflect what happened, or a note could be appended to the notes field.
jeandavidt
left a comment
There was a problem hiding this comment.
It all looks good.
The only questions I have revolve around how many "long" rows for the same type of measurement are collapsed into a single "wide" value. Something to discuss next meeting :)
|
Updated the specs and ready for re-review. See specs/mapping-configs.qmd and specs/mapping-operations.qmd. In mapping-configs.qmd I'd like to rethink the sections "Creating New Output Rows" and "Multiple Input and Output Rows" to make it simpler, suggestions are welcome. |
yulric
left a comment
There was a problem hiding this comment.
The main comment is about the organization of the documents. Right now the specification of the mapping config is in two locations, mapping-configs.qmd and mapping-operations.qmd. I think we should move all of that information into the mapping-configs.qmd file and focus the mapping-operations.qmd file into a high level document that goes over the different features that the implementation should support, showing real world use cases/examples for why a feature is needed. This will decouple the implementation specifications from the high level specifications so that if we decide for example to use the linkml transformer schema instead, the high level specifications do not need to change.
| # Quarto documentation | ||
| specs/*_files/ | ||
| specs/*.html | ||
| README.html |
There was a problem hiding this comment.
I recommend creating a Quarto project file. It has the following advantages:
- You get one single website that has all your documentation files. You don't have to run different render commands, one for each quarto file.
- You can specify an output directory where the entire website is built and stored. This way you have only entry in your .gitignore for the website rather than multiple.
| # Introduction | ||
|
|
||
| This document describes the various operations required for mapping between different database formats, such as between ODM long format to ODM wide format (and vice versa) and PHA4GE to ODM. Each section describes the operation with examples, and provides a list of database conversions that require the operation. | ||
| This section lists all operations allowable in a mapping configuration file. For details on the mapping configuration file see [Mapping Configuration Files](mapping-configs.qmd). With these operations, the source or input tables only contain the rows for the current group being processed. If the `group_by` key is not specified for the `main_inputs` table, then each source/input table processed by the operations will only have one row. |
There was a problem hiding this comment.
I'm not sure what you mean by the second sentence. Do you mind expanding or giving an example?
There was a problem hiding this comment.
I think I'm in the same boat as Yulric here - also unclear as to what is meant by "With these operations, the source or input tables only contain the rows for the current group being processed."
| ```{r, echo=FALSE} | ||
| mdtable_csv_file("../assets/examples/odm/measure-multidate-odmlong.csv") | ||
| ``` | ||
| The `copy` operation will copy one or more columns from the source table to the target table, optionally format the value(s), and optionally cast it to a specified type. The `method` key specifies which source rows are used for copying. For a method of `use_source_rows`, if an intermediate output table is already created, it will use the [source row numbers](mapping-behavior.qmd#assigning-source-table-and-source-row-numbers) assigned to each of the output table rows. If an intermediate output table is not already created, it will copy all the rows from the current source table. For a method of `exact`, all source rows are copied in order, populating the output table from top to bottom and creating new rows if required. |
There was a problem hiding this comment.
Is there a use case for the exact method?
| operation: copy | ||
| operation_config: | ||
| target_column: mr_reportDate | ||
| target_value: "The report date is {reportDate}" |
There was a problem hiding this comment.
Will there be a situation where they will need to specify the table within the curly braces? For example is two tables have the same column?
There was a problem hiding this comment.
Is the source_table key what determines what table the column is coming from?
| The `order_columns` operation reorders the columns in the output table. The list of strings `column_order_regex` specifies the order of the columns. These are regular expressions that are applied to all columns. Full matches must occur in order for a regular expression to match a column name. | ||
|
|
||
| For example, using the following long table group, resulting from [grouping](#operation-grouping) by date: | ||
| If an exact match occurs (eg. the column `mr_reportDate` matching the regular expression `mr_reportDate`) then the column is placed in the position of that match. Otherwise, if no exact match occurs then the position of a wildcard match is used. If multiple columns match a wildcard then those columns are sorted alphabetically for consistency. If no match occurs then the column is placed at the end. For example, if a table has a column `mr_reportDate`, `mr_specimen`, `mr_aggregation`, and `sm_sampleMat`, and the following configuration is used: |
There was a problem hiding this comment.
Not sure what you mean by the position of a wildcard match is used?
| The `pivot_longer` operation pivots individual input columns into individual output rows. It is the inverse of the `pivot_wider` operation. | ||
|
|
||
| We can apply some string maps or custom transforms to clean these values: | ||
| From the input rows (of the input table specified by `source_table`) `pivot_longer` finds any column that fully matches the regular expression specified in `match_column_values`. The regular expression must match the full column name, not just a part of it. It then takes the captures in the regular expression and assigns them to the columns (in order) specified in `target_columns`. In the `target_columns` field a value of `NULL` can be used if that capture should be ignored. It also takes the value in the input table found in the matched column and assigns it to the column specified in `target_value_column`. Using the input row below as an example: |
There was a problem hiding this comment.
Are we comfortable that all our needs can be met with a regular expression?
| ```{r, echo=FALSE} | ||
| mdtable_csv_file("../assets/examples/odm/measure-singledate-odmwide.csv") | ||
| ``` | ||
| The `method` field specifies how the output table is populated. If it is `new_rows` then each pivot will result in a new row. If it is `existing_rows` then no new rows are created (with one exception, see below), instead the values are pivoted into the existing rows (possibly overwriting values in those rows). With `existing_rows`, the rows used for pivoting are the [source rows](mapping-behavior.qmd#assigning-source-table-and-source-row-numbers) assigned to each existing output row. The one exception for `existing_rows` is if the output table is currently empty, in which case pivoting longer behaves as `new_rows` until a single pivot is complete. |
There was a problem hiding this comment.
Could you add an example for how this would work?
| ``` | ||
|
|
||
| - ODM long format to ODM wide format | ||
| The `pivot_wider` operation pivots individual input rows into individual output columns. It is the inverse of the `pivot_longer` operation. When mapping a single input row to a column we take the values in various columns of the input row to form a new column name (specified by `target_column`). We then set the target value to the value found in the `source_column` column. |
There was a problem hiding this comment.
Add text about the difference between text in the target_column surrounded by {} and not?
| The `pivot_wider` operation pivots individual input rows into individual output columns. It is the inverse of the `pivot_longer` operation. When mapping a single input row to a column we take the values in various columns of the input row to form a new column name (specified by `target_column`). We then set the target value to the value found in the `source_column` column. | ||
|
|
||
| # Operation: Pivot Longer | ||
| The `method` key specifies how new or existing rows are created or populated in the output. `stack` fills the column in the output table from top to bottom, starting with the first empty row, and adding new rows if required. `stack_no_new_rows` also fills the column in the output table from top to bottom, starting with the first empty row, but if there are more values to stack than available existing output rows we end and do not create new rows. `existing_rows` will only populate existing rows, starting from top to bottom, and will also use the [source rows](mapping-behavior.qmd#assigning-source-table-and-source-row-numbers) attached to each output row as the source of data. |
There was a problem hiding this comment.
Could you give an example for each method using the same input table?
| ```txt | ||
| (water, sample, liquid, SARS-CoV-2-N1, gene copies per L, arithmetic mean, 1) | ||
| ``` | ||
| The `set` operation assigns predefined values to columns in the output rows. If the output currently has more than one row the row index to copy the value to can be set with the `target_index` key, which is a 0-based index. `target_index` can also be an array of integers to specify multiple row indices to set. If `target_index` is not specified, or `all` is specified, then all rows are set. The default value is `all`. |
There was a problem hiding this comment.
I wonder if more than the target_index a would be better, since people would not really know what the target index would be. Like, set the organizatioId column values to 1 for rows with sampleId equal to 2.
There was a problem hiding this comment.
I agree that having some ability to match on filtering rules would be helpful
mathew-thomson
left a comment
There was a problem hiding this comment.
Looking good! Curious to see responses to Yulric's comment. Some of the documentation is a little over my head, unfortunately, but that's maybe to be expected.
| # PHES-ODM-Map | ||
| Transform data from various database formats to ODM. | ||
|
|
||
| Transform data from various database formats to ODM, and ODM wide format to/from ODM long format. We are currently developing the specs for this project, please view the documentation below: |
There was a problem hiding this comment.
May wish to specify that eventually to hope is to be able to move to and from the ODM.
There was a problem hiding this comment.
Love these diagram files - super clean, clear and helpful.
There was a problem hiding this comment.
Really like these example images - really clean and clear. Nice work!
There was a problem hiding this comment.
Really detailed summary, well explained, and makes a lot of sense. Covers a lot of bases.
| # Introduction | ||
|
|
||
| This document describes the various operations required for mapping between different database formats, such as between ODM long format to ODM wide format (and vice versa) and PHA4GE to ODM. Each section describes the operation with examples, and provides a list of database conversions that require the operation. | ||
| This section lists all operations allowable in a mapping configuration file. For details on the mapping configuration file see [Mapping Configuration Files](mapping-configs.qmd). With these operations, the source or input tables only contain the rows for the current group being processed. If the `group_by` key is not specified for the `main_inputs` table, then each source/input table processed by the operations will only have one row. |
There was a problem hiding this comment.
I think I'm in the same boat as Yulric here - also unclear as to what is meant by "With these operations, the source or input tables only contain the rows for the current group being processed."
There was a problem hiding this comment.
Some of the jargon/terms in this document flew over my head a bit, but from what I was able to follow I think it's looking quite good. I'm interested to see the responses to Yulric's questions/comments as well.
jeandavidt
left a comment
There was a problem hiding this comment.
Great work Martin! The documentation reads really well and the figures are very clear.
| To avoid having to enter many separate `copy` operations, multiple `copy` operations can be specified using arrays for `target_column`, `source_table`, and `source_column`. The example below copies `samples["siteID"]` to the output's `sm_siteID` column, `samples["saMaterial"]` to the output's `sm_sampleMat` column, and `contacts["contactID"]` to the output's `co_contactID` column: | ||
|
|
||
| ```yaml | ||
| operation: copy |
There was a problem hiding this comment.
Could one set derived values (like in the example below) here as well?
| operation: copy | ||
| operation_config: | ||
| target_column: mr_reportDate | ||
| target_value: "The report date is {reportDate}" |
There was a problem hiding this comment.
Is the source_table key what determines what table the column is coming from?
| ```txt | ||
| (water, sample, liquid, SARS-CoV-2-N1, gene copies per L, arithmetic mean, 1) | ||
| ``` | ||
| The `set` operation assigns predefined values to columns in the output rows. If the output currently has more than one row the row index to copy the value to can be set with the `target_index` key, which is a 0-based index. `target_index` can also be an array of integers to specify multiple row indices to set. If `target_index` is not specified, or `all` is specified, then all rows are set. The default value is `all`. |
There was a problem hiding this comment.
I agree that having some ability to match on filtering rules would be helpful
|
|
||
| ```yaml | ||
| metadata: | ||
| title: ODM 2.0 Long to Wide |
There was a problem hiding this comment.
I expect we will eventually want more metadata, such as description: maybe map_fromandmap_to`.
The authoring approach for YAML (in Quarto and other similar approaches) is:
author:
- name: "Author Name"
affiliation: "Author Affiliation"
orcid: "Author ORCID"
email: "Author Email"
url: "Author URL"
```
|
|
||
| {fig-align="center"} | ||
|
|
||
| `main_inputs` is the main input table for the current output table. We will iterate over the rows of this table one at a time. For each input row, we will apply all mapping operations one at a time to generate the new row(s) for the output table. When proceeding to each subsequent mapping operation, we carry forward the resulting output rows from the previous operation. This will construct the new row(s) iteratively, growing the row(s) as we proceed. Once all operations have been applied, we save the output rows and proceed to the next input row and repeat the process to generate other new rows. |
There was a problem hiding this comment.
I wondered if we just want inputs, rather than main_inputs and other_inputs. I think you are right that folks need to start somewhere
| ```{r, echo=FALSE, eval=TRUE, file="../R/doc_mdtables.R"} | ||
| ``` | ||
|
|
||
| # Introduction |
DougManuel
left a comment
There was a problem hiding this comment.
The document looks comprehensive. I have a feeling that there additional operations, but I can't think of any.
The folks over at linkML have been active in the last few week on the transformer. https://github.com/linkml/linkml-transformer
There are links to a meeting last spring, which has a presentation and good review of various mapping libraries. I see they are using the pydantic library. https://pydantic.dev

This is the first attempt at the ODM wide-to-long-to-wide mapping. Can you go over it?