ehrdata 0.5.0 adds readers for event tables, MEDS and FHIR R4, annotation and aggregation of medical codes, rebin for coarser time intervals, and five real, openly downloadable datasets. It also makes the PhysioNet loaders return analysis-ready data and fixes many bugs in OMOP setup and in writing to and reading from .h5ed and .zarr files.
Warning
Breaking changes
ehrdata.dt.physionet2012andehrdata.dt.physionet2019return analysis-ready data, which changes the dataset contents (see Changed).- ehrdata requires
anndata>=0.13.1. ehrdata.dt.ehrdata_blobsstores the time series in the 3D.Xby default instead of.layers["tem_data"]. Passlayer="tem_data"to keep the old behavior (see Fixed).
Added
ehrdata.rebinaggregates the time axis of anEHRDatainto coarser intervals, for instance hourly into 6-hourly ones, with theaggregation_strategyof the loaders. It applies to.Xand every layer with a time axis and updates the interval offsets in.tem. (#316) @Zethsonehrdata.io.from_eventsbins a table of events, one row per code recorded for a subject at a time as in the Medical Event Data Standard (MEDS), into anEHRDatawith a time axis. It reads pandas, pyarrow, DuckDB relations and parquet files, starts each subject's intervals at an anchor time fromobsor at the subject's first event, counts the events of codes without numeric values, and returns a dense array or asparse.COO. (#313) @Zethsonehrdata.io.read_medsreads a MEDS dataset: it bins the events withehrdata.io.from_events, selects subjects bysplit, stores the subject splits and the static events inobsand the code descriptions invar.ehrdata.io.write_medswrites anEHRDataas a MEDS dataset, with each value as an event at the start of its interval. (#314) @Zethsonehrdata.annotate_codesannotates variables named by codes likeICD10CM/I21.0,ICD9CM/410.01,ATC/C07AB02, or MEDS MIMIC-IV codes likeDIAGNOSIS//ICD//10//I210with their vocabulary, code, description, ICD-9-CM and ICD-10 chapter, category and diagnosis codes, and ATC levels, without any licensed tables. With an OMOP CDM database, it takes the descriptions, the ICD source codes that map to SNOMED CT and other standard concepts, and the ATC levels of drugs in other vocabularies, such as RxNorm, from theconcept,concept_relationshipandconcept_ancestortables.ehrdata.aggregate_codesaggregates the variables to one of these levels, for 2D and 3D data. (#317) @Zethsonehrdata.io.read_fhirreads FHIR R4 Bulk Data NDJSON exports, optionally compressed, and Bundles: it bins Observations, Conditions, medications, Procedures, Encounters and Immunizations withehrdata.io.from_events, codes them asLOINC/8480-6like OMOP vocabularies, stores times in UTC, and stores the Patient resources inobsand the code descriptions and units invar. (#315) @Zethson- The Reading EHR data tutorial reads small synthetic examples with every reader,
ehrdata.io.from_pandas,ehrdata.io.from_events,ehrdata.io.read_meds,ehrdata.io.read_fhir,ehrdata.io.read_h5ed, andehrdata.io.read_zarr, and annotates the codes withehrdata.annotate_codes. (#319) @Zethson ehrdata.dt.eicu_crd,ehrdata.dt.mimic_iv_meds,ehrdata.dt.pbcseq,ehrdata.dt.heart_failure, andehrdata.dt.heart_diseaseload real, openly downloadable clinical datasets.
The demo of the eICU Collaborative Research Database holds vital signs and laboratory measurements of 2520 ICU stays since ICU admission, the MIMIC-IV demo in MEDS all events of 100 patients since their first hospital admission, and the Mayo Clinic primary biliary cholangitis trial the repeated visits of 312 patients.
The UCI heart failure and heart disease datasets are static cohorts of 299 and 920 patients.
Likeehrdata.dt.physionet2012, they hold the labels of documented codes as categories, documented placeholders as missing values, binary outcomes as 0 or 1, and the start of every interval intem["time_value"]. (#322) @Zethsonehrdata.io.read_medstakesanchor_codeto start the intervals of each subject at its first event of this code, such as a hospital admission, instead of at its first event, which in MEDS datasets is often the birth. (#322) @Zethsonehrdata.harmonize_missing_valuesandehrdata.infer_feature_typessupportsparse.COOarrays. For a numericsparse.COOlayer,harmonize_missing_valuesturns the implicit fill value0intonp.nan, except for the variables passed asvars, whose0values stay real values. (#306) @sueoglu
Changed
- Breaking:
ehrdata.dt.physionet2012andehrdata.dt.physionet2019now return analysis-ready data, which changes the dataset contents.Gender(andICUTypeof PhysioNet 2012) hold the labels of their documented codes as categories, such as"female"and"male", and the -1 that PhysioNet 2012 uses for missing values, for instance inHeight,Survival,DiasABP,NIDiasABPandWeight, are missing values. Outcomes such asIn-hospital_deathandSepsisLabelstay 0 or 1. A numerictime_valuecolumn in.temholds the start of every interval ininterval_length_unitfor plotting, andehrdata.rebinkeeps it. (#318) @Zethson - ehrdata now requires
anndata>=0.13.1, the first release providing everythingEHRDatabuilds on: a 3D.X,.Xunified intolayers[None], and pydata-sparseCOOarrays in memory. The last ehrdata still toleratedanndata<0.13, while already supporting it. (#277) @eroell ehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variableswithaggregation_strategy="last"(or"first") no longer return a missing value when the last (or first) record of an interval has no value in the kept field. The aggregation now considers only the records that do carry a value, so the last (or first) observed value is kept. Consequently, records without a value no longer contribute a unit to the unit report, andis_presentof the long format table marks the intervals in which a value was observed. (#298) @eroell
Fixed
ehrdata.io.write_h5edandehrdata.io.write_zarrnow write the datetime, timedelta andobjectcolumns of.obs,.varand.tem: datetimes and timedeltas as ISO 8601 strings, andobjectcolumns as a numeric dtype where their values allow it and as strings otherwise, with missing values staying missing. Integers and booleans among them stay exact, in a nullable dtype if values are missing, and strings next to other values stay strings, keeping codes like007intact. Previously,ehrdata.io.write_h5edfailed on such columns, so that the output ofehrdata.io.omop.setup_obscould not be written at all, as the OMOP tables bring datetime columns, andobjectcolumns for the fields a database read finds empty;ehrdata.io.write_zarrfailed on datetime columns and wroteobjectcolumns as strings, with"None"for missing values. Nullable float columns are now written too. (#303) @eroellehrdata.io.write_zarrwithconvert_strings_to_categoricals=Trueno longer converts the string columns of the written object's own.temtocategoricaldtype; the conversion now happens on the copy that is written. (#303) @eroellehrdata.io.write_h5edandehrdata.io.write_zarrnow cast everyobjectlayer to a numeric dtype, or to strings if that fails, also whenXis not ofobjectdtype or isNone, as for the layersehrdata.io.omop.setup_variablesbuilds. Previously,ehrdata.io.write_h5edfailed on such a layer if it held anything but strings, andehrdata.io.write_zarrwrote it as strings, with"None"for missing values. Missing values of anobjectXor layer cast to strings are now written as"nan", which reading harmonizes tonp.nan. The arrays are cast while writing, without copying theEHRData, so that the layers of one read in backed mode can be written. (#304) @eroellehrdata.io.read_h5edandehrdata.io.read_zarrwithcast_variables_to_float=True, the default, now cast the variables of each non-numeric layer to float. Previously, they silently replaced every non-numeric layer by a copy ofX, and failed ifXwasNoneor a scipy sparse array. (#304) @eroellehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variableswithaggregation_strategy="last"(or"first") are now deterministic when several records share the same person, concept and datetime. (#330) @eroellehrdata.annotate_codesno longer reads names likeBMI (kg/m2)as the codem2)of the vocabularyBMI (kg: only names whose part before the/has no spaces or parentheses are codes. (#324) @Zethsonehrdata.dt.mimic_iv_medsdrops the codes without any event in the intervals, which were all-missing variables: with the defaults, it now returns 4253 instead of 7033 codes. (#325) @Zethsonehrdata.io.write_h5ednow writes the nullablestringcolumns thatehrdata.annotate_codesadds to.var, asehrdata.io.write_zarralready did. It previously failed whenever such a column was not converted to a categorical because its values are unique, for instance on subsets of a few variables. (#326) @Zethsonehrdata.infer_feature_typesskips features without any value with a warning and leaves their feature type missing instead of raising aValueError, so that it, and every function that infers feature types on the fly, works on data with all-missing variables. (#327) @Zethson- Subsetting an
EHRDatawith arrays or lists on more than one axis, such asedata[obs_indices, var_names]oredata[:, var_names, time_indices], now selects every combination of the indices, likenumpy.ix_, for every layer with a time axis and for a 3D.X, also forsparse.COOand dask arrays. Previously, the time axis was applied to the observations, or the indices were paired up, so that these layers got the wrong shape andEHRData.copyfailed. (#323) @Zethson ehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variablesnow restrict the variables toconcept_ids, which they previously ignored. (#312) @Zethsonehrdata.io.omop.setup_variableswithenrich_var_with_unit_info=Truenow merges the unit information on the concept id of each variable instead of its row position, which madeunit_concept_idcome out as 0, 1, 2, etc. (#312) @Zethsonehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variablesno longer bin data points after the inclusive end date of the observation table chosen inehrdata.io.omop.setup_obs, such asobservation_period_end_date, which changes the results wherever intervals extended past it. (#312) @Zethsonehrdata.io.omop.setup_connection, and with it dataset loaders such asehrdata.dt.gibleed_omop, now replaces existing tables instead of failing when run twice on the same connection. (#312) @Zethsonehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variablesnow make every interval exactlyinterval_length_numberinterval_length_unitlong, where DuckDB previously turned offsets of 30 days or more into calendar months, so that e.g. 30-day intervals from 2100-01-01 had their second one start on 2100-02-01 instead of 2100-01-31. (#312) @Zethsonehrdata.io.to_pandaswithformat="long"no longer fails for a 3Dsparse.COOarray, or for an index ofobsortemthat has a name, such as thetemofehrdata.dt.physionet2012andehrdata.dt.physionet2019. (#314) @Zethsonehrdata.infer_feature_typesno longer fails on 3D data when printing its overview, andehrdata.feature_type_overviewtakeslayerand counts categories across observations and timepoints. (#311) @Zethson- In
ehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variables, the units are now carried along the aggregation: from the very data point that is kept for"last"/"first", and shared by the data points that are combined otherwise. Since combining values of different units is not meaningful, anaggregation_strategyother than"last","first"and"count"now raises aNotImplementedErrorif a feature has more than oneunit_concept_id. Data tables without aunit_concept_idare unaffected, which insetup_interval_variablesis all of them butdevice_exposureanddose_era. (#300) @eroell ehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variablesnow markis_presentof the long format table for the intervals in which a value was observed with everyaggregation_strategy, not only with"last"/"first". (#300) @eroellehrdata.io.omop.setup_variablesandehrdata.io.omop.setup_interval_variableswithaggregation_strategy="std"no longer fail withCatalog Error: Scalar Function with name std does not exist!. The strategy is now mapped to DuckDB'sstddev_samp. (#300) @eroell- ehrdata no longer imports the storage type aliases (
H5Array,H5Group,ZarrArray,ZarrGroup) from the privateanndata.compat, which dropped them in anndata 0.13.3. (#293) @eroell ehrdata.dt.mimic_2now downloads the raw data from the scverse example data mirror (https://exampledata.scverse.org/ehrapy/full_cohort_data.csv) instead of physionet, which is unreachable from some sandboxed environments. The data is unchanged. (#256) @eroell @Zethsonehrdata.dt.ehrdata_blobsnow defaultslayertoNoneand stores the generated time series tensor in.X. Previously it always wrote to.layers["tem_data"]regardless of thelayerargument, so code that omittedlayersilently analyzed a static 2D snapshot instead of the intended 3D time series. Passlayer=explicitly to keep the tensor in a named layer instead. (#284) @sueoglu
Maintenance
Full Changelog: v0.4.0...v0.5.0