From 829a6bb6d2d5a59372f2cdc0a9bd2c5724c6a08b Mon Sep 17 00:00:00 2001 From: sgosline Date: Wed, 17 Jul 2024 15:19:39 -0700 Subject: [PATCH 1/4] initial yaml file and file name changes --- dbSchema/srpAnalytics.yml | 58 +++++++++++++++++++++++++++++++ exposome/exposome_summary_stats.R | 2 +- zfExp/parseGexData.R | 2 +- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 dbSchema/srpAnalytics.yml diff --git a/dbSchema/srpAnalytics.yml b/dbSchema/srpAnalytics.yml new file mode 100644 index 0000000..e1ac694 --- /dev/null +++ b/dbSchema/srpAnalytics.yml @@ -0,0 +1,58 @@ +id: http://w3id.org/linkml/examples/srpanalyics +name: srpanalytics +prefixes: + linkml: https://w3id.org/linkml + coderdata: https://w3id.org/linkml/examples/srpanalytics + schema: http://schema.org/ +imports: + - linkml:types +default_range: string +default_prefix: srpanalytics + +slot: + chemical_ID: + description: Unique identifier for every chemical in the databaes + range: integer + identifier: true + slot_uri: schema:identifier + sample_ID: + description: Unique identifier for every sample in the database + range: integer + identifer: true + slot_uri: schema:identifier + cas_number: + description: CAS identifier for a specific chemical + + +classes: + samples: + description: List of samples collected in the Superfund study + slots: + - chemical_ID + - cas_number + attributes: + + chemicals: + description: List of chemicals measured in the Superfund study + samplesToChemicals: + description: Measurements of chemicals in samples + zebrafishSampBMDs: + description: Benchmark dose measurements of sample extracts in zebrafish + zebrafishSampDoseResponse: + description: Dose response datapoints of sample extracts in zebrafish + zebrafishSampXYCoords: + description: XY Coordinates of curve fit data for sample extracts in zebrafish + zebrafishChemBMDs: + description: Benchmark dose measurements of chemicals in zebrafish + zebrafishChemDoseResponse: + description: Dose response datapoints of chemicals in zebrafish + zebrafishChemXYCoords: + description: XY Coordinates of curve fit data for chemicals in zebrafish + allGeneEx: + description: List of all experiments that measure gene expression changes in zebrafish upon chemical treatment changes + srpDEGStats: + description: Summary statistcs + srpDEGPathways: + description: Pathways that are enriched in zebrafish genes that are differentially expressed upon treatment with a chemical + exposomeGeneStats: + description: Summary and link to exposome measurements of human genes that are differentailly expressed upon chemical treatmetn \ No newline at end of file diff --git a/exposome/exposome_summary_stats.R b/exposome/exposome_summary_stats.R index 03f24ce..ee29152 100644 --- a/exposome/exposome_summary_stats.R +++ b/exposome/exposome_summary_stats.R @@ -156,6 +156,6 @@ sg.stats <- sig.genes%>% dplyr::rename(Project=friendlyName)|> dplyr::select(Project,cas_number,Conc,Link,nGenes,Chemical_ID) -write.table(sg.stats,file=paste0(out.dir,'sigGeneStats.csv'),sep=',',row.names=F) +write.table(sg.stats,file=paste0(out.dir,'exposomeGeneStats.csv'),sep=',',row.names=F) ##not using this for now: #write.table(sig.genes,file='data/sigGeneExp.csv',sep=',',row.names=F) diff --git a/zfExp/parseGexData.R b/zfExp/parseGexData.R index 4f18595..0e9a5a0 100644 --- a/zfExp/parseGexData.R +++ b/zfExp/parseGexData.R @@ -222,7 +222,7 @@ main<-function(args=c()){ enrich<-doEnrich(allgenes) - res<-res|>left_join(enrich) + res#<-res|>left_join(enrich) ##need to get mapping to drug name readr::write_csv(enrich,file =paste0(out.dir,'srpDEGPathways.csv')) readr::write_csv(res,file =paste0(out.dir,'srpDEGstats.csv')) From 2ac87a6ec5d2b18c5ceab043212867fcb49a679e Mon Sep 17 00:00:00 2001 From: sgosline Date: Fri, 19 Jul 2024 08:23:05 -0700 Subject: [PATCH 2/4] Began changes to schema validation --- build_script.py | 19 +- dbSchema/Dockerfile | 18 -- dbSchema/ingest.py | 125 --------- dbSchema/main.py | 57 ---- dbSchema/requirements.txt | 10 - dbSchema/schemas/chemSummaryStats.json | 111 -------- dbSchema/schemas/chemXYcoords.json | 29 --- dbSchema/schemas/chemdoseResponseVals.json | 62 ----- dbSchema/schemas/chemicalByExtractSample.json | 45 ---- dbSchema/schemas/convertTojson.R | 18 -- dbSchema/schemas/databaseIngestSchema.xlsx | Bin 21573 -> 0 bytes dbSchema/schemas/envSampSummaryStats.json | 111 -------- dbSchema/schemas/envSampXYcoords.json | 29 --- dbSchema/schemas/envSampdoseResponseVals.json | 39 --- dbSchema/schemas/envSampleIntake.json | 245 ------------------ .../schemas/processingPipelineSchema.xlsx | Bin 13835 -> 0 bytes dbSchema/schemas/zebrafishDataIntake.json | 66 ----- dbSchema/setup.sh | 18 -- dbSchema/srpAnalytics.yml | 58 ----- dbSchema/validate.py | 63 ----- exposome/exposome_summary_stats.R | 5 +- 21 files changed, 20 insertions(+), 1108 deletions(-) delete mode 100644 dbSchema/Dockerfile delete mode 100644 dbSchema/ingest.py delete mode 100644 dbSchema/main.py delete mode 100644 dbSchema/requirements.txt delete mode 100644 dbSchema/schemas/chemSummaryStats.json delete mode 100644 dbSchema/schemas/chemXYcoords.json delete mode 100644 dbSchema/schemas/chemdoseResponseVals.json delete mode 100644 dbSchema/schemas/chemicalByExtractSample.json delete mode 100644 dbSchema/schemas/convertTojson.R delete mode 100644 dbSchema/schemas/databaseIngestSchema.xlsx delete mode 100644 dbSchema/schemas/envSampSummaryStats.json delete mode 100644 dbSchema/schemas/envSampXYcoords.json delete mode 100644 dbSchema/schemas/envSampdoseResponseVals.json delete mode 100644 dbSchema/schemas/envSampleIntake.json delete mode 100644 dbSchema/schemas/processingPipelineSchema.xlsx delete mode 100644 dbSchema/schemas/zebrafishDataIntake.json delete mode 100644 dbSchema/setup.sh delete mode 100644 dbSchema/srpAnalytics.yml delete mode 100644 dbSchema/validate.py diff --git a/build_script.py b/build_script.py index 942a534..6a8d750 100644 --- a/build_script.py +++ b/build_script.py @@ -70,6 +70,13 @@ def runSampMap(is_sample=False,drcfiles=[],smap='',cid='',\ print(cmd) os.system(cmd) print('ls -la .') + ##now we validate the files that came out. + dblist=['/tmp/samples.csv','/tmp/chemicals.csv','/tmp/samplesToChemicals.csv'] + for ftype in ['ChemXYCoords.csv','DoseResponse.csv','BMDs.csv']: + dblist.append('/tmp/zebrafishChem'+ftype) + dblist.append('/tmp/zebrafishSamp'+ftype) + runSchemaCheck(dblist) + def runExposome(chem_id_file): ''' @@ -78,6 +85,7 @@ def runExposome(chem_id_file): cmd = 'Rscript exposome/exposome_summary_stats.R '+chem_id_file print(cmd) os.system(cmd) + runSchemaCheck(['/tmp/exposomeGeneStats.csv']) def runExpression(gex,chem,ginfo): ''' @@ -86,13 +94,18 @@ def runExpression(gex,chem,ginfo): cmd = 'Rscript zfExp/parseGexData.R '+gex+' '+chem+' '+ginfo print(cmd) os.system(cmd) + runSchemaCheck(['/tmp/srpDEGPathways.csv','/tmp/srpDEGStats.csv','/tmp/allGeneEx.csv']) def runSchemaCheck(dbfiles=[]): ''' run schema checking ''' - ##TODO: make this work with files as arguments - cmd = 'python dbSchema/main.py' + ##TODO: make this work with internal calls + for filename in dbfiles: + classname = os.path.basename(filename).split('.')[0] + cmd = 'linkml-validate --schema srpAnalytics.yaml '+filename+' --target-class '+classname + print(cmd) + os.system(cmd) def main(): ''' @@ -106,7 +119,7 @@ def main(): # file parsing - collects all files we might need for the tool below #### ##first find the morphology and behavior pairs for chemical sources - chemdf = df.loc[df.sample_type=='chemical'] + chemdf = df.loc[df.sample_type=='chemical'] morph = chemdf.loc[chemdf.data_type=='morphology'] beh = chemdf.loc[chemdf.data_type=='behavior'] tupes =[] diff --git a/dbSchema/Dockerfile b/dbSchema/Dockerfile deleted file mode 100644 index 4ec8e61..0000000 --- a/dbSchema/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM python:3.9 -#-alpine3.18 -# To setup other dependencies -RUN apt-get update -qq && apt-get install -y net-tools \ - curl \ - unixodbc \ - unixodbc-dev - -COPY requirements.txt /requirements.txt -RUN pip3 install -r /requirements.txt - -RUN mkdir dbSchema -COPY dbSchema/* dbSchema/ -#COPY build_script.py . -WORKDIR dbSchema - -ENTRYPOINT ["python3", "main.py"] -VOLUME ["/tmp"] diff --git a/dbSchema/ingest.py b/dbSchema/ingest.py deleted file mode 100644 index 9267bc6..0000000 --- a/dbSchema/ingest.py +++ /dev/null @@ -1,125 +0,0 @@ -import pandas as pd -import numpy as np -import sqlalchemy as db - -import os -import sys - -import logging - -from validate import verify -logging.basicConfig() -logging.getLogger("sqlalchemy.engine").setLevel(logging.ERROR) -logging.getLogger("sqlalchemy.pool").setLevel(logging.ERROR) - -username=os.environ.get('DB_USER') -password=os.environ.get('DB_PASS') -db_dev_ip=os.environ.get('DB_DEV_IP') -db_prod_ip=os.environ.get('DB_PROD_IP') -db_port=os.environ.get('DB_PORT') -db_name=os.environ.get('DB_NAME') - - -def test_connection(database): - if database == "production": - engine = db.create_engine('mssql+pyodbc://{}:{}@{}:{}/{}?driver=ODBC Driver 17 for SQL Server'.format(username, password, db_prod_ip, db_port, db_name), - pool_pre_ping=True, echo=False, hide_parameters=True, connect_args={'connect_timeout': 100}, fast_executemany=True) - else: - engine = db.create_engine('mssql+pyodbc://{}:{}@{}:{}/{}?driver=ODBC Driver 17 for SQL Server'.format(username, password, db_dev_ip, db_port, db_name), - pool_pre_ping=True, echo=False, hide_parameters=True, connect_args={'connect_timeout': 100}, fast_executemany=True) - try: - engine.connect() - return True, "" - except Exception as e: - return False, e - - -def pull_raw_data(folder, if_exists, database): - """ Looks for CSV files in the provided path and then calls the read_and_save function. This function calls all of the others, this is the management function. - - Parameters: - folder (string): Path to folder where CSVs are stored - if_exists (string): Tells Pandas what to do if table already exists - Reference: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_sql.html - - Returns: - nothing - """ - if database == "production": - engine = db.create_engine('mssql+pyodbc://{}:{}@{}:{}/{}?driver=ODBC Driver 17 for SQL Server'.format(username, password, db_prod_ip, db_port, db_name), - pool_pre_ping=True, echo=False, hide_parameters=True, connect_args={'connect_timeout': 100}, fast_executemany=True) - else: - engine = db.create_engine('mssql+pyodbc://{}:{}@{}:{}/{}?driver=ODBC Driver 17 for SQL Server'.format(username, password, db_dev_ip, db_port, db_name), - pool_pre_ping=True, echo=False, hide_parameters=True, connect_args={'connect_timeout': 100}, fast_executemany=True) - for file in os.listdir(folder): - filename, file_extension = os.path.splitext(file) - if (file_extension == ".csv"): - print("Analyzing {}".format(filename)) - read_and_save(os.path.join(folder, file), filename, if_exists, engine) - print("Finished analyzing {}".format(filename)) - - -def read_and_save(csv_file, table_name, if_exists, engine): - """ Takes a CSV file, reads (using Pandas), and then saves as a Microsoft SQL Server table - - Parameters: - csv_file (string): Path to CSV file - table_name (string): The name of the table the data will be stored as. Defaults to the same name as the filename. - if_exists (string): Tells Pandas what to do if table already exists - Reference: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_sql.html - - Returns: - nothing - """ - print("\tReading csv...") - df = pd.read_csv(csv_file) - print("\t\tFinished reading csv.") - print("\t\tWriting to {}...".format(table_name)) - # If any infinite value is found, replace with a NULL value. - df.replace([np.inf, -np.inf], np.nan, inplace=True) - valid = verify(df, table_name) - if valid: - df.to_sql(table_name, engine, if_exists=if_exists, index=False) - print("\tFinished writing to {}.".format(table_name)) - else: - print("Invalid schema, not saving.") - - -def pull(table_name, engine): - """ Pulls all data from a table - - Parameters: - table_name (string): The name of the table the data will be stored as. Defaults to the same name as the filename. - - Returns: - Pandas DataFrame containing all of the rows in that table - """ - print("\t\t\tPulling from database table: {}".format(table_name)) - table_name = table_name - connection = engine.connect() - metadata = db.MetaData() - table = db.Table(table_name, metadata, autoload=True, autoload_with=engine) - query = db.select([table]) - result = connection.execute(query) - print("\t\t\tReturning Pandas dataframe of results from database table.") - return pd.DataFrame(result.fetchall()) - - -if __name__ == "__main__": - # Default Values - if_exists = 'append' - folder = './out' - database = 'dev' - - print('Usage: python3 ingest.py data_folder if_exists=["append*", "replace", "fail"] database=["development*", "production"]') - if len(sys.argv) >= 2: # includes flag for if_exists - folder = sys.argv[1] - print("Overwriting default folder value with: {}".format(folder)) - if len(sys.argv) >= 3: - if_exists = sys.argv[2] - print("Overwriting default if_exists value with: {}".format(if_exists)) - if len(sys.argv) == 4: - database = sys.argv[3] - print("Overwriting default database configuration value with: {}".format(database)) - - pull_raw_data(folder=folder, if_exists=if_exists, database=database) \ No newline at end of file diff --git a/dbSchema/main.py b/dbSchema/main.py deleted file mode 100644 index 5ad45ca..0000000 --- a/dbSchema/main.py +++ /dev/null @@ -1,57 +0,0 @@ -q#!/usr/bin/env python -# coding: utf-8 - -import os, sys, time -import argparse - -import re -from ingest import pull_raw_data, test_connection - -OUT_FOLDER = '/tmp' -IF_EXITS = 'replace' # options: "append", "replace", "fail" -DB = 'develop' # options: "develop", "production" - -parser = argparse.ArgumentParser('Take the files and moved to a database') - -parser.add_argument('--validate', dest='validate', \ - help='If this tag is added, then we validate existing files',\ - action='store_true', default=False) - -parser.add_argument('--update-db', dest='update_db', action='store_true', \ - help='Include --update-db if you want to update the database',\ - default=False) - - -def main(): - """ - main method for command line - """ - start_time = time.time() - args = parser.parse_args() - - allfiles = ['/tmp/'+a for a in os.listdir('/tmp') if 'csv' in a] - print(allfiles) - if args.validate: - print("Validating existing files for database ingest") - ##get files - for fval in allfiles: - valid.verify(pd.read_csv(fval, quotechar='"', quoting=1), re.sub('.csv', '', os.path.basename(fval))) - ##validate - if args.update_db: - print('Saving to {}...'.format(DB)) - pull_raw_data(folder=OUT_FOLDER, if_exists=IF_EXITS, database=DB) - print('Finished saving to database.') - # else: # if not saving to database, check connection to DB is okay - # print("Testing connection to database...", end='') - # okay, error = test_connection(database=DB) - # if okay: - # print('Connection OK') - # else: - # print('Connection failed, {}'.format(error)) - end_time = time.time() - time_took = str(round((end_time-start_time), 1)) + " seconds" - print ("Done, it took:" + str(time_took)) - - -if __name__ == "__main__": - main() diff --git a/dbSchema/requirements.txt b/dbSchema/requirements.txt deleted file mode 100644 index b6fde70..0000000 --- a/dbSchema/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -pandas -numpy -sqlalchemy -matplotlib -seaborn -statsmodels -astropy -scipy==1.4.1 -pyodbc -jsonschema diff --git a/dbSchema/schemas/chemSummaryStats.json b/dbSchema/schemas/chemSummaryStats.json deleted file mode 100644 index 478550c..0000000 --- a/dbSchema/schemas/chemSummaryStats.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "type": "object", - "properties": { - "Chemical_ID": { - "type": "integer", - "minimum": 0, - "Friendly Name": "", - "Description": "", - "Ontology Link": "" - }, - "Model": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "BMD10": { - "type": ["number", "null"], - "minimum": 0 - }, - "BMD50": { - "type": ["number", "null"], - "minimum": 0 - }, - "Min_Dose": { - "type": "number", - "minimum": 0 - }, - "Max_Dose": { - "type": "number", - "minimum": 0, - "maximum": 100 - }, - "AUC_Norm": { - "type": ["number", "null"], - "minimum": 0, - "maximum": 1 - }, - "cas_number": { - "type": "string", - "minLength": 5, - "maxLength": 255 - }, - "DTXSID": { - "type": "string", - "pattern": "(DTXSID)[0-9]+", - "minLength": 0, - "maxLength": 255 - }, - "PREFERRED_NAME": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "INCHIKEY": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "SMILES": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "MOLECULAR_FORMULA": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "AVERAGE_MASS": { - "type": "number", - "minimum": 0 - }, - "PUBCHEM_DATA_SOURCES": { - "type": "integer", - "minimum": 0 - }, - "zf.cid": { - "type": ["string", "null"], - "minimum": 0 - }, - "chemical_class": { - "type": "string", - "minLength": 3, - "maxLength": 255 - }, - "chemDescription": { - "type": "string", - "minLength": 0, - "maxLength": 8000 - }, - "End Point Name": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "Description": { - "type": ["string", "null"], - "minLength": 0, - "maxLength": 255 - }, - "endPointLink": { - "type": ["string", "null"], - "minLength": 0, - "maxLength": 255 - }, - "DataQC_Flag": { - "type": ["string", "null"], - "pattern": "^(Good|Moderate|Poor)" - } - } -} diff --git a/dbSchema/schemas/chemXYcoords.json b/dbSchema/schemas/chemXYcoords.json deleted file mode 100644 index b09bd86..0000000 --- a/dbSchema/schemas/chemXYcoords.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "type": "object", - "properties": { - "Chemical_ID": { - "type": "integer", - "minimum": 0 - }, - "X_vals": { - "type": "number", - "minimum": 0, - "maximum": 100 - }, - "Y_vals": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "End Point Name": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "endPointLink": { - "type": ["string", "null"], - "minLength": 0, - "maxLength": 255 - } - } -} \ No newline at end of file diff --git a/dbSchema/schemas/chemdoseResponseVals.json b/dbSchema/schemas/chemdoseResponseVals.json deleted file mode 100644 index f03dfd0..0000000 --- a/dbSchema/schemas/chemdoseResponseVals.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "type": "object", - "properties": { - "Chemical_ID": { - "type": "integer", - "minimum": 0, - "Friendly Name":"", - "Description" :"", - "Comment": "" - }, - "Dose": { - "type": "number", - "minimum": 0, - "maximum": 100, - "Friendly Name":"", - "Description" :"", - "Comment": "" - }, - "Response": { - "type": "number", - "minimum": 0, - "maximum": 1, - "Friendly Name":"", - "Description" :"", - "Comment": "" - }, - "CI_Lo": { - "type": "number", - "minimum": 0, - "maximum": 1, - "Friendly Name":"", - "Description" :"", - "Comment": "" - }, - - "CI_Hi": { - "type": "number", - "minimum": 0, - "maximum": 1, - "Friendly Name":"", - "Description" :"", - "Comment": "" - }, - "End Point Name": { - "type": "string", - "minLength": 0, - "maxLength": 255, - "pattern":"", - "Friendly Name":"", - "Description" :"", - "Comment": "" - }, - "endPointLink": { - "type": ["string", "null"], - "minLength": 0, - "maxLength": 255, - "Friendly Name":"", - "Description" :"", - "Comment": "" - } - } -} diff --git a/dbSchema/schemas/chemicalByExtractSample.json b/dbSchema/schemas/chemicalByExtractSample.json deleted file mode 100644 index 9d82eb1..0000000 --- a/dbSchema/schemas/chemicalByExtractSample.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "type": "object", - "properties": { - "Sample_ID": { - "type": "integer", - "minimum": 0 - }, - "Chemical_ID": { - "type": "integer", - "minimum": 0 - }, - "SampleNumber": { - "type": "string", - "minLength": 2, - "maxLength": 255 - }, - "SampleName": { - "type": "string", - "minLength": 2, - "maxLength": 255 - }, - "cas_number": { - "type": "string", - "minLength": 5 - }, - "measurement_value_molar": { - "type": "number", - "minimum": 0 - }, - "measurement_value_molar_unit": { - "type": "string", - "minLength": 0, - "maxLength": 255 - }, - "water_concentration_molar": { - "type": ["number", "null"], - "minimum": 0 - }, - "water_concentration_molar_unit": { - "type": ["string", "null"], - "minLength": 0, - "maxLength": 255 - } - } -} \ No newline at end of file diff --git a/dbSchema/schemas/convertTojson.R b/dbSchema/schemas/convertTojson.R deleted file mode 100644 index 5ca20ba..0000000 --- a/dbSchema/schemas/convertTojson.R +++ /dev/null @@ -1,18 +0,0 @@ -##function that converst excel to xlsx - - -##this script reads in XLSX, converts to json -library(readxl) -library(jsonlite) - -env <- readxl::read_xlsx('processingPipelineSchema.xlsx',sheet='environmentalSample') -zeb <-readxl::read_xlsx('processingPipelineSchema.xlsx',sheet='zebrafish raw data') - -if(FALSE){ - write(jsonlite::toJSON(apply(env,1,as.list)),'envSampleIntake.json') - write(jsonlite::toJSON(apply(zeb,1,as.list)),'zebrafishDataIntake.json') -} - -#also read in json and format to table - -fromJSON() \ No newline at end of file diff --git a/dbSchema/schemas/databaseIngestSchema.xlsx b/dbSchema/schemas/databaseIngestSchema.xlsx deleted file mode 100644 index 68dd2cf5da542c1c04244cec181a21b08a40087b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21573 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 LFhsEY@#%j6Wmj0s diff --git a/dbSchema/schemas/zebrafishDataIntake.json b/dbSchema/schemas/zebrafishDataIntake.json deleted file mode 100644 index c1c46c9..0000000 --- a/dbSchema/schemas/zebrafishDataIntake.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "type": "object", - "properties": { - "chemical.id": { - "type": "integer", - "minimum":0, - "Friendly Name":["Chemical ID"], - "Description":["Internal chemical identifier"], - "Comment":[null] - }, - "bottle.id": { - "type": "integer", - "minimum": 0, - "Friendly Name":["Bottle ID"], - "Description":["Internal bottle identifier"], - "Nas allowed":["no"], - "Allowable values":[null], - "Comment":[null] - }, - "conc":{ - "type": "integer", - "Friendly Name":["Concentration"], - "Description":["Dose of chemical"], - "Nas allowed":["no"], - "Comment":[null] - }, - "plate.id": { - "Friendly Name":["Plate ID"], - "Description":["Plate identifier"], - "Nas allowed":["no"], - "Allowable values":[null], - "Comment":[null] - }, - "well": { - "Friendly Name":["Well"], - "Description":["Well of plate"], - "Nas allowed":["no"], - "Allowable values":[null], - "Comment":[null] - }, - "date":{ - "Friendly Name":["Date"], - "type" : "string", - "Description":["Date assay was initiated"], - "Nas allowed":["no"], - "Allowable values":[null], - "Comment":[null] - }, - "endpoint": { - "type": "string", - "Friendly Name":["Zebrafish End point"], - "Description":["Endpoint measured"], - "Nas allowed":["no"], - "Allowable values":["listed in endpoint document"], - "Comment":[null] - }, - "value": { - "type":"number", - "Friendly Name":["Value of endpoint"], - "Description":["Individual Fish Response"], - "Nas allowed":["yes"], - "Allowable values":["1,0","null"], - "Comment":["When endpoint is DNC_ then value is NA"] - } - } -} diff --git a/dbSchema/setup.sh b/dbSchema/setup.sh deleted file mode 100644 index fe32c2a..0000000 --- a/dbSchema/setup.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -export DISABLE_AUTH=true -export ACCEPT_EULA=Y - -export HTTP_PROXY=http://proxy01.pnl.gov:3128 -export HTTPS_PROXY=http://proxy01.pnl.gov:3128 -export NO_PROXY=*.pnl.gov,*.pnnl.gov,127.0.0.1,10.120.148.170,10.120.148.153 - -echo 'Running setup' -Rscript -e "install.packages('argparse')" - -echo `which pip3` - -# To save to DB -curl https://packages.microsoft.com/ubuntu/20.10/prod/pool/main/m/msodbcsql17/msodbcsql17_17.7.2.1-1_amd64.deb --output mssql.deb -dpkg -i mssql.deb -curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/msprod.list diff --git a/dbSchema/srpAnalytics.yml b/dbSchema/srpAnalytics.yml deleted file mode 100644 index e1ac694..0000000 --- a/dbSchema/srpAnalytics.yml +++ /dev/null @@ -1,58 +0,0 @@ -id: http://w3id.org/linkml/examples/srpanalyics -name: srpanalytics -prefixes: - linkml: https://w3id.org/linkml - coderdata: https://w3id.org/linkml/examples/srpanalytics - schema: http://schema.org/ -imports: - - linkml:types -default_range: string -default_prefix: srpanalytics - -slot: - chemical_ID: - description: Unique identifier for every chemical in the databaes - range: integer - identifier: true - slot_uri: schema:identifier - sample_ID: - description: Unique identifier for every sample in the database - range: integer - identifer: true - slot_uri: schema:identifier - cas_number: - description: CAS identifier for a specific chemical - - -classes: - samples: - description: List of samples collected in the Superfund study - slots: - - chemical_ID - - cas_number - attributes: - - chemicals: - description: List of chemicals measured in the Superfund study - samplesToChemicals: - description: Measurements of chemicals in samples - zebrafishSampBMDs: - description: Benchmark dose measurements of sample extracts in zebrafish - zebrafishSampDoseResponse: - description: Dose response datapoints of sample extracts in zebrafish - zebrafishSampXYCoords: - description: XY Coordinates of curve fit data for sample extracts in zebrafish - zebrafishChemBMDs: - description: Benchmark dose measurements of chemicals in zebrafish - zebrafishChemDoseResponse: - description: Dose response datapoints of chemicals in zebrafish - zebrafishChemXYCoords: - description: XY Coordinates of curve fit data for chemicals in zebrafish - allGeneEx: - description: List of all experiments that measure gene expression changes in zebrafish upon chemical treatment changes - srpDEGStats: - description: Summary statistcs - srpDEGPathways: - description: Pathways that are enriched in zebrafish genes that are differentially expressed upon treatment with a chemical - exposomeGeneStats: - description: Summary and link to exposome measurements of human genes that are differentailly expressed upon chemical treatmetn \ No newline at end of file diff --git a/dbSchema/validate.py b/dbSchema/validate.py deleted file mode 100644 index c42c6b1..0000000 --- a/dbSchema/validate.py +++ /dev/null @@ -1,63 +0,0 @@ -from json import load -from jsonschema import Draft3Validator -import pandas as pd -import argparse - -workdir='/dbSchema/' - -schemas = { - 'chemdoseResponseVals': load(open(workdir+'/schemas/chemdoseResponseVals.json')), - 'chemicalsByExtractSample': load(open(workdir+'/schemas/chemicalByExtractSample.json')), - 'chemSummaryStats': load(open(workdir+'/schemas/chemSummaryStats.json')), - 'chemXYcoords': load(open(workdir+'/schemas/chemXYcoords.json')), - 'envSampdoseResponseVals': load(open(workdir+'/schemas/envSampdoseResponseVals.json')), - 'envSampSummaryStats': load(open(workdir+'/schemas/envSampSummaryStats.json')), - 'envSampXYcoords': load(open(workdir+'/schemas/envSampXYcoords.json')), -} - -proc_schemas = { - 'envSample': load(open(workdir+'/schemas/envSampleIntake.json')), - 'zebrafish': load(open(workdir+'/schemas/zebrafishDataIntake.json')), -} - -def verify(df, table_name): - """ Takes a Pandas DataFrame and a Microsoft SQL Server table name to compare the number of rows in each. This function only works if the to_sql function replaces the table (if it appends the number of rows will obviously be off) - - Parameters: - csv_df (Pandas Dataframe): dataframe of the original data from CSV - table_name (string): name of table that you want to compare against - - Returns: - nothing - """ - print("Verifying schema...") - df = df.where(pd.notnull(df), None) - v = Draft3Validator(schemas[table_name]) - errors = set() - for row in df.to_dict(orient='records'): - for error in sorted(v.iter_errors(row), key=str): - errors.add(str(error)) - - if errors: - print('Validation errors when running schema check on {}'.format(table_name)) - with open("/tmp/{}_validation_errors.txt".format(table_name), 'w+') as fp: - for error in errors: - fp.write("{}\n\n\n".format(error)) - return False - return True - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Validate CSVs are in proper schema') - parser.add_argument('csv_file', type=str, help='Path to CSV you want to validate') - parser.add_argument('schema', type=str, help='The schema you want to validate against. Acceptable options are: {}'.format(list(schemas.keys()))) - - args = parser.parse_args() - - if args.schema not in schemas.keys(): - print("{} is not a valid schema, please enter one of the following: {}".format(args.schema, list(schemas.keys()))) - exit(-1) - - if verify(pd.read_csv(args.csv_file, quotechar='"', quoting=1), args.schema): - print('CSV file is in expected format.') - else: - print('Schema validation failed.') diff --git a/exposome/exposome_summary_stats.R b/exposome/exposome_summary_stats.R index ee29152..77c344b 100644 --- a/exposome/exposome_summary_stats.R +++ b/exposome/exposome_summary_stats.R @@ -154,7 +154,10 @@ sg.stats <- sig.genes%>% ungroup()|> dplyr::select(-Project)|> dplyr::rename(Project=friendlyName)|> - dplyr::select(Project,cas_number,Conc,Link,nGenes,Chemical_ID) + dplyr::select(Project,cas_number,Conc,Link,nGenes,Chemical_ID)|> + mutate(concentration=as.numeric(stringr::str_replace(conc,'uM','')))|> + dplyr::select(-Conc) + write.table(sg.stats,file=paste0(out.dir,'exposomeGeneStats.csv'),sep=',',row.names=F) ##not using this for now: From 3cb8f7d732ad40adb4311c2ca4825b3ebe90eab7 Mon Sep 17 00:00:00 2001 From: sgosline Date: Fri, 19 Jul 2024 10:44:29 -0700 Subject: [PATCH 3/4] validation in place still lots of NA vals --- exposome/exposome_summary_stats.R | 2 +- sampleChemMapping/mapSamplesToChems.R | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/exposome/exposome_summary_stats.R b/exposome/exposome_summary_stats.R index 77c344b..94bc362 100644 --- a/exposome/exposome_summary_stats.R +++ b/exposome/exposome_summary_stats.R @@ -155,7 +155,7 @@ sg.stats <- sig.genes%>% dplyr::select(-Project)|> dplyr::rename(Project=friendlyName)|> dplyr::select(Project,cas_number,Conc,Link,nGenes,Chemical_ID)|> - mutate(concentration=as.numeric(stringr::str_replace(conc,'uM','')))|> + mutate(concentration=as.numeric(stringr::str_replace(Conc,'uM','')))|> dplyr::select(-Conc) diff --git a/sampleChemMapping/mapSamplesToChems.R b/sampleChemMapping/mapSamplesToChems.R index 15d88f6..0dfc1cf 100644 --- a/sampleChemMapping/mapSamplesToChems.R +++ b/sampleChemMapping/mapSamplesToChems.R @@ -26,9 +26,12 @@ required_sample_columns<-c("ClientName","SampleNumber","date_sampled","sample_ma "LocationLon","LocationName","LocationAlternateDescription", "AlternateName","cas_number","date_sample_start", "measurement_value","measurement_value_qualifier","measurement_value_unit", - "measurement_value_molar","measurement_value_molar_unit", - "water_concentration","water_concentration_qualifier","water_concentration_unit", - "water_concentration_molar","water_concentration_molar_unit") + "measurement_value_molar","measurement_value_molar_unit") + +#we need to rename the water columns +new_sample_columns=c(environment_concentration="water_concentration",environment_concentration_qualifier='water_concentration_qualifier', + environment_concentration_unit='water_concentration_unit',environment_concentration_molar='water_concentration_molar', + environment_concentration_molar_unit='water_concentration_molar_unit') ##required for comptox-derived mapping files required_comptox_columns <- c("INPUT","DTXSID","PREFERRED_NAME","INCHIKEY","SMILES","MOLECULAR_FORMULA", @@ -38,8 +41,8 @@ required_comptox_columns <- c("INPUT","DTXSID","PREFERRED_NAME","INCHIKEY","SMIL ##output tables sample_chem_columns <-c('Sample_ID','Chemical_ID',"measurement_value","measurement_value_qualifier","measurement_value_unit", "measurement_value_molar","measurement_value_molar_unit", - "water_concentration","water_concentration_qualifier","water_concentration_unit", - "water_concentration_molar","water_concentration_molar_unit") + "environment_concentration","environment_concentration_qualifier","environment_concentration_unit", + "environment_concentration_molar","environment_concentration_molar_unit") samp_columns <-c("Sample_ID","ClientName","SampleNumber","date_sampled","sample_matrix","technology", "projectName","SampleName","LocationLat","projectLink", @@ -309,19 +312,21 @@ buildSampleData<-function(fses_files, #files from barton that contain sample inf sampIds, #new ids for samples sampMapping){ ##mapping for sample names to clean up ##New data provided by michael - # print(fses_files) + # print(fses_files) + sampChem<-do.call(rbind,lapply(fses_files,function(fs){ # print(fs) # fses1<-subset(sampTab,name=='fses1')[['location']] sc <- rio::import(fs)|>#paste0(data.dir,'/fses/fses_data_for_pnnl_4-27-2021.csv'))%>% # sampChem<-read.csv(paste0(data.dir,'/pnnl_bioassay_sample_query_1-14-2021.csv'))%>% - dplyr::select(all_of(required_sample_columns))%>% + dplyr::select(all_of(c(required_sample_columns,unlist(new_sample_columns))))%>% #TODO: change original file to use new names +# dplyr::rename(new_sample_columns)|> ##REMOVE this once we have new names subset(SampleNumber!='None')%>% subset(cas_number!='NULL')%>% - mutate(water_concentration_molar=stringr::str_replace_all(water_concentration_molar,'BLOD|NULL|nc:BDL',"0"))%>% + mutate(environment_concentration_molar=stringr::str_replace_all(environment_concentration_molar,'BLOD|NULL|nc:BDL',"0"))%>% mutate(measurement_value_molar=stringr::str_replace_all(measurement_value_molar,'BLOD|NULL|BDL',"0"))%>% - mutate(water_concentration=stringr::str_replace_all(water_concentration,'BLOD|NULL|BDL',"0"))%>% - # subset(water_concentration_molar!='0.0')%>% + mutate(environment_concentration=stringr::str_replace_all(environment_concentration,'BLOD|NULL|BDL',"0"))%>% + # subset(environment_concentration_molar!='0.0')%>% subset(!measurement_value_molar%in%c('0'))%>% subset(!measurement_value%in%c("0","NULL",""))#%>% # select(-c(Sample_ID))#,Chemical_ID)) ##These two are added in the 4/27 version of the file From e6af5f05a0f603bf4dd856201d3e0e1325a72eb6 Mon Sep 17 00:00:00 2001 From: sgosline Date: Fri, 19 Jul 2024 11:47:20 -0700 Subject: [PATCH 4/4] forgot to update validation libraries --- .github/workflows/docker.yml | 19 ------------------- requirements.txt | 1 + 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a68653a..fcd39d2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -56,25 +56,6 @@ jobs: tags: sgosline/srp-exposome:latest push: true - build-push-db: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Build and push dbschema - uses: docker/build-push-action@v3 - with: - file: dbSchema/Dockerfile - tags: sgosline/srp-dbschema:latest - push: true - build-push-exp: runs-on: ubuntu-latest steps: diff --git a/requirements.txt b/requirements.txt index 85722d7..36fc2f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pandas argparse +linkml