Skip to content

EHR queries - #103

Draft
jeremyestein wants to merge 65 commits into
devfrom
ehr_queries_clean
Draft

jeremyestein wants to merge 65 commits into
devfrom
ehr_queries_clean

Conversation

@jeremyestein

Copy link
Copy Markdown
Collaborator

I have linearised the previous branches somewhat and removed all merges from dev (or janitoring), then done a big merge at the end. This takes us up to the equivalent of commit 061dca7 in the other branch.

skeating and others added 30 commits February 2, 2026 15:06
Introduces new queries for airway and sputum/secretions data, and refactors the flow sheet query to consolidate multiple values into a wider format. A new README documents the overall data extraction goal and the current set of scripts.
Transforms specific lab test results (e.g., CRP, WCC) from a long format into distinct columns using `MAX() FILTER`. This enables direct consumption of lab data in a wider format, simplifying downstream analysis.

Additionally, the query is updated to use parameters for `hospital_visit_id` and to include date range filtering, improving its flexibility.
Comment thread config.EXAMPLE/exporter.env.EXAMPLE Outdated

@jeremyestein jeremyestein left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments. I think this review is mostly a to-do list for me now :)

Comment thread src/pipeline/Snakefile
# and reruns if the underlying data for this csn/day changes.
pseudonymised_parquets = pseudonymised_parquet_files_for_date_and_hashed_csn
output:
WAVEFORM_PSEUDONYMISED_EHR / (EHR_STEM_PATTERN_HASHED + "_ehr.csv")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everywhere else we use dots to separate parts of the file so I think we should do that here too. (ie. .ehr.csv)

Also get_ehr_lookup should probably be the single source of truth for what this file looks like.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

Comment thread src/locations.py
WAVEFORM_ORIGINAL_PARQUET = WAVEFORM_EXPORT_BASE / "original-parquet"
WAVEFORM_HASH_LOOKUPS = WAVEFORM_EXPORT_BASE / "hash-lookups"
WAVEFORM_PSEUDONYMISED_PARQUET = WAVEFORM_EXPORT_BASE / "pseudonymised"
WAVEFORM_PSEUDONYMISED_EHR = WAVEFORM_EXPORT_BASE / "pseudonymised_ehr"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do_upload_multiple will refuse to upload anything that's not in the pseudonymised directory. So either that restriction would have to be expanded, or we should put this under pseudonymised. I lean to the latter, although perhaps in an EHR subdir to make it clear to the user that these files are in a different format to the waveform data? This is more a usability question than a functionality one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in a separate directory under pseudonymised would be best.

Comment thread src/db.py Outdated
Comment thread src/db_pg.py
Comment on lines +132 to +140
"DateTimeRecorded": [0],
"Units": ["None"],
"Abnormal_result": ["No"],
"Comments": ["None"],
"C-reactive protein 1": ["-"],
"CSF WCC TUBE 1": ["-"],
"CSF WCC TUBE 2": ["-"],
"CSF WCC TUBE 3": ["-"],
"C-reactive protein 2": ["-"],

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do all these values mean? "None", "No", "-" are superficially similar-sounding. Do they correspond to values in the real database?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't mean anything. I haven't seen enough real data to know what would be more representative.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that will have to be changed later, and we'll need to know types when we define a parquet scheme.

Comment thread src/db.py Outdated

def connect(self) -> None:
"""Set up connection to the database."""
self.fake_caboodle = True if settings.CABOODLE_TESTING == "TRUE" else False

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would reject "1", "True", etc, which are allowed for other variables (the ones that Snakemake processes as config?)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that because I wanted to make it as hard as possible to accidentality set CABOODLE_TESTING, as it would cause silent failures in production if used by mistake. It's probably overkill, in which case we could use pipeline.utils.config_bool to interpret other true like values.

Comment thread src/sql/lab_results.sql
ON r.lab_order_id = o.lab_order_id

WHERE
r.result_status LIKE 'FINAL'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was a wildcard intended? (otherwise this is the same as using =)

Comment thread src/csv_writer.py
filename = WAVEFORM_PSEUDONYMISED_EHR / f"{stem}_ehr.csv"
filename.parent.mkdir(exist_ok=True, parents=True)

df.to_csv(filename, index=False)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be output as parquet for consistency with the other parquet outputs, and disk space, typing, etc reasons.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have an intermediate step (i.e. to_csv, then a second process converts csv to parquet, or go straight to parquet?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we should go straight to parquet. The only purpose for the CSV intermediate for waveform data is that it's easy to append to. (Hence the desire to switch to a better appendable format #15)
But this is a batch operation so it can all be dumped in one go to parquet.

Comment on lines +69 to +86
safe_columns = [
"DateTimeRecorded",
"PlacementInstant",
"RemovalInstant",
"TubeSize",
"Repositioned",
"Position frequency",
"Temperature",
"Noradrenaline",
"Metaraminol",
"PaO2",
"PaCO2",
"Secretions",
"Sputum",
"Units",
"CRP",
"WCC",
"Comments", # Free text comments could contain sensitive information. Should we hash it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated these to match the columns in the queries, but can we rename them a bit so it's clearer which query they relate to? Eg. "Units" is not clear as to what it's the units for.

Comment thread src/db_mssql.py
) -> pd.DataFrame:
"""Retrieve airflow data from database."""

airway_query = get_sql_query_text("private/airway.sql")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DateTimeRecorded comes out as a timezoned ISO-style timestamp. This is inconsistent with the waveform files that use seconds since epoch. The zoned timestamps start run midnight to midnight local time, whereas the waveform files run midnight to midnight UTC, which will produce some massive gotchas for the user.

Shall we try and standardise on something? And check what the postgres queries do, bearing in mind that mssql and postgres handle timezones quite differently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants