Study data¶
The studies against which the models are fitted and validated are curated in PK-DB, in a checkout of pkdb_data: its folder studies holds a folder per substance and in it a folder per study, with the publication, its figures and tables and the digitized data as hidden .tsv files (.Boulton1996a_Fig1.tsv). A model keeps the studies it uses in its own data/ folder, so that its simulations run without the checkout; pkpdsim.data copies them there.
Finding the studies¶
studies_dir() returns the folder studies of the checkout. The checkout is the path of the environment variable PKDB_DATA; without it the checkout is ../pkdb_data relative to the working directory, i.e. a clone next to the repository a script runs in. A folder which does not exist is an error which names both:
import os
from pathlib import Path
from pkpdsim.data import PKDB_DATA, studies_dir
# a checkout of pkdb_data with one study of albuterol
study = Path("checkouts/pkdb_data/studies/albuterol/Boulton1996a")
study.mkdir(parents=True)
(study / ".Boulton1996a_Fig1.tsv").write_text("time\tmean\n0.0\t0.0\n")
(study / "Boulton1996a_Fig1.png").write_bytes(b"")
os.environ[PKDB_DATA] = "checkouts/pkdb_data"
print(studies_dir().relative_to(Path.cwd()).as_posix())
os.environ[PKDB_DATA] = "missing/pkdb_data"
try:
studies_dir()
except FileNotFoundError as error:
print(type(error).__name__)
Collecting the data of a model¶
The experiments read the tables of study format 1, one hidden .tsv file per source of a study (.Boulton1996a_Fig1.tsv). pkdb_data is curated in study format 2 ("format": 2 in the study.json, with the tables subjects.tsv, interventions.tsv, timecourses_<source>.tsv, outputs_<source>.tsv, ...). collect_tsv_files(source_dir, target_dir, filter_study=None, *, studies=(), keep_files=False) writes the data of every study folder of source_dir (and of the further study folders studies) into a folder of the same name in target_dir, as the tables of format 1, so that the experiments read them unchanged:
- a study of format 2 is read with the library of PK-DB,
pkdb.prepare, with the vocabulary of the checkout (vocabulary.lock.json); this is the reading and the validation ofpkdb checkand of the upload to the database, so a study whichpkdb checkrefuses is an error. Its data become one table per source (the reported outputs and timecourses ofoutputs_<source>.tsvandtimecourses_<source>.tsv, the statistics PK-DB calculates or normalizes from them are left out) one table per source of the scatters ofscatters_<source>.tsv(a point per row with the columnsx_...andy_...of format 1, the name of the scatter aslabel,<source>Scatterwhen the source also has measurements), and the tablesTabGroups,TabIndividualsandTabInterventionsof the subjects, their characteristics and the interventions (study_tables,write_study_tables). A digitized error bar gives the statistic it names (sd,seorgsd), the missingsd,seandcvof a row are calculated as PK-DB does (cvis a fraction of the mean), an output (which has no label in format 2) gets the label of its measurement, substance, tissue, interventions and subject (cumulative_amount_change_chloride_urine_HCTZ25_sus_all), a point of a scatter has its values also inxandy, and the earlier tables of the study intarget_dirare replaced; withkeep_files=Truethe tables of the study folder intarget_dirwhich the collection does not write stay, e.g. tables a model keeps beside the ones of PK-DB (see the rows and columns); - the hidden
.tsvfiles of a study of format 1 are copied; a study without them is left out; - the figures, the tables and the publication stay in pkdb_data, and
filter_studyselects studies ofsource_dirby the name of their folder; - beside the tables of a study the record
.<study>_study.json(StudyRecord,read_study_record) is the provenance of the data the simulations run on: the day the model first collected the study (collected), kept by every later collection, and how it is known (dating:collectionfor the day the collection wrote the first record,gitwith itsdating_commitfor a day of the git history of pkdb_models,scripts/backfill_study_records.py), the day of the latest collection (updated), the release of the study in PK-DB (released, therelease.dateof itsstudy.json, andpkdb_id;Nonefor a study which is not released), the source digest of PK-DB (source_digest, format 2 only), the commit of the checkout of pkdb_data and whether the study folder had uncommitted changes (pkdb_data_commit,pkdb_data_dirty;Noneoutside git), the sha256 of every table (tables,table_digests) and the versions of pkpdsim and pkdb. A study of format 1 is recorded as well. A folder with tables of the study and no record was collected before the records existed and gets no day.StudyRecord.changed_tables(folder)names the tables which differ from the record. The temporal validation of the credibility reads the day and the assessment the provenance, see Temporal validation and Study data; studiestakes the study folders of other substances which a model uses (studies=[studies_dir() / "midazolam" / "Chung2006"]beside the studies of its own substance); two studies of the same name are an error.
What the curation should look at is reported as a CollectionWarning and the collection goes on: a curated label which is no SId (corr_creatine_cl-pravastatin_cl), the same output in two sources of a study (the same measurement, substance, tissue, subject, interventions, time and value), a curator 0 in a curve of geometric means (see below).
Every written file is reported on the console. Continuing the snippet above:
from pkpdsim.data import collect_tsv_files
os.environ[PKDB_DATA] = "checkouts/pkdb_data"
collect_tsv_files(source_dir=studies_dir() / "albuterol", target_dir=Path("data"))
print(sorted(path.as_posix() for path in Path("data").rglob("*")))
The console shows the study folder and every copy, from the absolute path of the source to the target, then the snippet prints the data folder:
A model calls it from the script data/collect_data.py of its folder:
# not executed: data/collect_data.py of a model, which needs a checkout of pkdb_data
from pkdb_models.models.albuterol import DATA_PATH_ALBUTEROL
from pkpdsim.data import collect_tsv_files, studies_dir
collect_tsv_files(
source_dir=studies_dir() / "albuterol", target_dir=DATA_PATH_ALBUTEROL
)
The rows and columns¶
The tables of a study of format 2 have the columns of format 1 (MEASUREMENT_COLUMNS, SCATTER_COLUMNS, GROUP_COLUMNS, INDIVIDUAL_COLUMNS, INTERVENTION_COLUMNS), so an experiment selects its rows as before. A missing value is NA. The ports of the models to format 2 showed what the experiments have to know about the rows:
- Geometric means. A row reported as a geometric mean has its value in
gmeanand none inmean, its spread ingsd(a factor, from a geometric error bar or calculated from thegcv) andgcv(a fraction), and thecalculation_typegeometric mean(PK-DB itself gives every measurement of a group without a calculation the calculationsample mean). An experiment reads such a curve fromgmean(yid="gmean").PKPDSimulationExperiment.load_dataframegives the geometric means their unit (add_gmean_unit, the columngmean_unit), which sbmlsim does not read fromunitfor them. A curve of geometric means has no curator 0: the mean of 0 at the time of the dose (aliskiren/Tapaninen2010 Fig1, the urine at 0 hr of apixaban/Chang2016 Tab2) cannot be a geometric mean and is left out with aCollectionWarning. The groups have thecv,gmean,gsdandgcvof their characteristics as well (the geometric mean of the renin activity of captopril/MacFadyen1991). - Values below the limit of quantification. A point below the LOQ is a row without a central value, with the LOQ in
maxand a comment: it hasNAinvalue,mean,medianandgmean, so an experiment drops it withdf.dropna(subset=["mean"])("value"for individuals,"gmean"for geometric means). It has thecalculation_typeof the other rows of its curve, a point below the LOQ of a curve of geometric means is ageometric mean. - Errors.
cvandgcvare fractions of the mean, not percent as in format 2 and in some tables of format 1 (an experiment which divides them by 100 is wrong by that factor). The missingsd,seandcvof a row are calculated from the others and the count. A table always has the columns of the errors, a curve without any error hasNAin all of them, andDataSet.from_dfof sbmlsim renamessdtomean_sd: whether a curve has errors isdf["sd"].notna().any()(dset["mean_sd"].notna().any()), not whether the column exists. Format 2 has the median withminandmaxbut no quartiles or confidence intervals, so neither do the tables. - Counts.
count(andgroup_countof the groups,dosesof the interventions) are integers, a missing count isNA(53, not53.0). A row of a group without one has the count of the group, a row of an individual counts 1. - Labels. A timecourse has the label of its curation, an output the label generated from its measurement, substance, tissue, interventions and subject. A generated label which another source of the study has as well (the trough of visit 8 of sorafenib/Fucile2015 in
outputs_Fig1.tsvand the troughs of the same patient inoutputs_Tab3.tsv) gets its source appended in every source which has it (concentration_sorafenib_plasma_SOF400_1_Fig1,..._Tab3), so a label is unique across the sources of a study and the datasets of an experiment can be keyed by it. The labels which only one source has stay as they are; a source added to a study can change the label of another source. A generated label is an SId (a leading digit gets a_); a curated label which is no SId (montelukast-M1_het) stays as curated with aCollectionWarning, an experiment keys its datasets byto_sid(label). - Interventions.
interventionholds the names of theinterventionscell of format 2, comma separated, in the natural order whichpkdb formatgives the cell (GEM,SIM,SEM0.5,SEM0.25,SEM1); a generated label joins them with_(..._GEM_SIM_...). A selection of format 1 in another order (SIM,GEM) selects nothing. - Scatters. A point of a scatter has the measurement, substance, tissue, method, value, unit, time and interventions of each axis in
x_...andy_...(x_time,x_intervention);timeis the one ofy(else ofx) andinterventionthe interventions of both axes, as before. A measurement without a substance (the eGFR of empagliflozin/Sarashina2014 Fig2) hasNAinx_substance, so a selection by the substance ofxdrops it. A point of an individual counts 1, a point of a group the count of the group (the means of the volunteers of inulin/Prescott1991 Fig4); several points of a scatter with the same group, interventions and times are anonymous subjects of the group and count 1 (Sarashina2014 Fig2). - Individuals.
TabIndividualsis long, a row per characteristic of an individual (measurement_type,choice,value,unit), andmean_of_individualscalculates the mean, SD and count of the individuals of a table at every time, e.g. for a group whose means the publication does not report. A table with a column per characteristic is a pivot:
import pandas as pd
individuals = pd.DataFrame(
{
"name": ["S1", "S1", "S2", "S2"],
"measurement_type": ["age", "sex", "age", "sex"],
"choice": [None, "M", None, "F"],
"value": [24.0, None, 31.0, None],
}
)
wide = individuals.pivot(index="name", columns="measurement_type", values="value")
wide["sex"] = individuals.pivot(
index="name", columns="measurement_type", values="choice"
)["sex"]
print(wide.reset_index().to_string(index=False))
A model whose digitized times scatter around the sampling times of the protocol moves them there with sampling_times(df, times, start=None, by="label"): within a curve the points keep their order and go to distinct sampling times, the assignment with the least total shift.
What format 2 does not store, the tables do not have either. The tables have no collection intervals: format 2 has only the time of a measurement (for a urine collection the end of the interval), PK-DB has no start and end of a collection yet (pkdb#1032), so the experiments which read tstart and tend (or time_start and time_end) of format 1 take the intervals from the publication until then. Format 2 has no digitized actual times beside the nominal ones, no SD of a single subject, no quartiles of a box plot and no confidence intervals either (pkdb#1044). Neither do the tables have the columns which the curators of format 1 added to a single table (mean_se, dose, wlow, wup, ...): an experiment which drops or reads such a column changes with the collection from format 2.
Comparing with the database¶
The models read the study folders of the checkout, which are the studies of a revision of pkdb_data. database_differences(study_folders, client=None) compares them with the studies uploaded to a PK-DB server: a study agrees when the digest of its source files (source_digest of pkdb.prepare) is the one of the server, a study the server does not have and a study with another digest are a DatabaseDifference with the reason. The server and the credentials are those of pkdb.Client (PKDB_ENDPOINT, PKDB_API_KEY); a private study needs an API key.