Skip to content

Commit 8917d1e

Browse files
committed
Update database to insert aggregated data #88
1 parent 3d2489e commit 8917d1e

1 file changed

Lines changed: 176 additions & 18 deletions

File tree

populate_gromstole_db.py

Lines changed: 176 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import sys
44
import glob
55
import csv
6+
from datetime import datetime
7+
from epiweeks import Week
8+
import json
69
from scripts.progress_utils import Callback
710
import psycopg2
811
import psycopg2.extras
@@ -15,6 +18,8 @@ def parse_args():
1518

1619
parser.add_argument('--indir', type=str, default="/home/wastewater/results/gromstole",
1720
help="Path to the gromstole results directory")
21+
parser.add_argument('--upload_dir', type=str, default="/home/wastewater/uploads",
22+
help="Path to the uploads directory")
1823
parser.add_argument('--dbname', type=str, default=os.environ.get("POSTGRES_DB", "gromstole_db"),
1924
help="Postgresql database name")
2025
parser.add_argument('--dbhost', type=str, default=os.environ.get("POSTGRES_HOST", "localhost"),
@@ -43,42 +48,111 @@ def open_connection(connection_parameters):
4348
LAB VARCHAR(255),
4449
RUN VARCHAR(255),
4550
SAMPLE VARCHAR(255),
46-
POSITION VARCHAR(255),
51+
COLDATE DATE,
52+
REGION VARCHAR(255),
53+
LATITUDE FLOAT NULL,
54+
LONGITUDE FLOAT NULL,
4755
LABEL VARCHAR(255),
56+
MUTATION VARCHAR(255),
4857
FREQUENCY VARCHAR(255),
4958
COVERAGE VARCHAR(255),
5059
PATH VARCHAR(255))'''
5160
cur.execute(results_table)
5261

5362
cur.execute('''CREATE INDEX IF NOT EXISTS frequency_index ON RESULTS (frequency)''')
5463

64+
aggregate_table = '''CREATE TABLE IF NOT EXISTS AGGREGATE_MAPPED (
65+
ID SERIAL PRIMARY KEY,
66+
REGION VARCHAR(255),
67+
YEAR VARCHAR(255),
68+
EPIWEEK VARCHAR(255),
69+
NUC VARCHAR(255),
70+
AMINO VARCHAR(255),
71+
NSAMPLES INTEGER,
72+
COUNT INTEGER,
73+
COVERAGE INTEGER )'''
74+
cur.execute(aggregate_table)
75+
5576
conn.commit()
5677
return cur, conn
5778

5879

59-
def insert_files(cur, files, callback=None):
80+
def parse_date(dt, formats=('%y-%m-%d', '%d-%m-%y', '%d-%m-%Y', '%m-%d-%Y', '%Y-%m-%d', '%d-%b-%y', '%m/%d/%Y', '%d/%m/%y')):
81+
""" Try multiple date formats """
82+
if dt in ['UNK', '', ' ', 'null', None]:
83+
return None
84+
for fmt in formats:
85+
try:
86+
return datetime.strptime(dt, fmt)
87+
except ValueError:
88+
pass
89+
raise ValueError(f"No supported format detected for date string '{dt}'")
90+
91+
92+
def retrieve_metadata(runs, upload_dir, callback=None):
93+
"""
94+
Retrieve metadata from the metadata.csv file
95+
96+
:param runs: set, runs to retrieve metadata for
97+
:param upload_dir: str, path to the upload directory
98+
:return: dict, metadata
99+
"""
100+
metadata = {}
101+
for lab, run in runs:
102+
if lab not in metadata:
103+
metadata.update({lab: {}})
104+
if run not in metadata[lab]:
105+
metadata[lab].update({run: {}})
106+
107+
upload_path = os.path.join(upload_dir, lab, run, 'metadata.csv')
108+
if not os.path.exists(upload_path):
109+
callback('Metadata file not found: {}'.format(upload_path))
110+
continue
111+
112+
with open(upload_path, encoding='latin-1') as handle:
113+
for row in csv.DictReader(handle):
114+
sample_key = 'r1 fastq filename'
115+
try:
116+
if len(row) == 1 or row[sample_key] == '':
117+
continue
118+
except:
119+
callback('Empty metadata file: {}'.format(row))
120+
raise
121+
122+
sample = row[sample_key].split('_')[0]
123+
124+
if lab == 'western':
125+
sample = sample.replace('_', '-')
126+
try:
127+
metadata[lab][run].update({sample: {
128+
'coldate': row['sample collection date'],
129+
'region': row['geolocation name (region)'],
130+
'latitude': row['geolocation latitude'],
131+
'longitude': row['geolocation longitude']
132+
}})
133+
except:
134+
callback('Column names incorrect in {}'.format(os.path.join(upload_path)))
135+
continue
136+
return metadata
137+
138+
139+
def new_mapped_files(cur, files, callback=None):
60140
"""
61-
Inserts the file name, file creation date, path and checksum of the file into the database
141+
Get the set of new mapped files to process
62142
63143
:param curr: object, cursor object
64144
:param filepath: str, path to the file
65-
:return: None
145+
:return: set, new files, new runs
66146
"""
147+
new_files = set()
148+
new_runs = set()
67149
for lab, run, sample, path in files:
68150
cur.execute('SELECT * FROM RESULTS WHERE LAB = %s AND RUN = %s AND SAMPLE = %s', (lab, run, sample))
69151
if cur.fetchone() is not None:
70152
continue
71-
callback("Inserting results from file: {}".format(path))
72-
with open(path, 'r') as f:
73-
mapped = csv.DictReader(f)
74-
for line in mapped:
75-
if float(line['coverage']) * float(line['frequency']) == 1:
76-
continue
77-
78-
cur.execute("INSERT INTO RESULTS (LAB, RUN, SAMPLE, POSITION, LABEL, FREQUENCY, COVERAGE, PATH)"
79-
" VALUES(%s, %s, %s, %s, %s, %s, %s, %s)"
80-
" ON CONFLICT DO NOTHING",
81-
(lab, run, sample, line['position'], line['label'], line['frequency'], line['coverage'], path))
153+
new_files.add((lab, run, sample, path))
154+
new_runs.add((lab, run))
155+
return new_files, new_runs
82156

83157

84158
def get_files(files):
@@ -93,10 +167,92 @@ def get_files(files):
93167
runpath, sample = os.path.split(normfile)
94168
run = os.path.basename(runpath)
95169
lab = found_keywords[0]
96-
f.add((lab, run, sample, normfile))
170+
f.add((lab, run, sample.split('.')[0], normfile))
97171
return f
98172

99173

174+
def insert_files(cur, files, metadata, callback=None):
175+
"""
176+
Insert files into the database
177+
178+
:param curr: object, cursor object
179+
:param files: set, files to insert
180+
:param metadata: dict, metadata
181+
:return: None
182+
"""
183+
184+
# import location to region map as a dict
185+
# TODO: Need a better way to map locations to regions
186+
regions = json.load(open("data/regions.json"))
187+
188+
for lab, run, sample, path in files:
189+
if lab not in metadata:
190+
callback("Lab {} not in metadata".format(lab))
191+
continue
192+
if run not in metadata[lab]:
193+
callback("Run {} not in metadata".format(run))
194+
continue
195+
if sample not in metadata[lab][run]:
196+
callback("Sample {} from {}/{} not in metadata".format(sample, lab, run))
197+
continue
198+
199+
md = metadata[lab][run][sample]
200+
callback("Inserting results from file: {}".format(path))
201+
with open(path, 'r') as f:
202+
mapped = csv.DictReader(f)
203+
last_fail = None
204+
failed_latlong = set()
205+
for line in mapped:
206+
coverage = float(line['coverage'])
207+
frequency = float(line['frequency'])
208+
if coverage < 10 or frequency < 1e-3:
209+
continue
210+
211+
coldate = parse_date(md['coldate'].strip())
212+
if coldate is None:
213+
continue
214+
215+
try:
216+
latitude = float(md['latitude'])
217+
longitude = float(md['longitude'])
218+
except ValueError:
219+
if (lab, run, sample) not in failed_latlong:
220+
failed_latlong.add((lab, run, sample))
221+
callback(f"Setting latitude and longitude to None for {lab}/{run}/{sample}")
222+
latitude = None
223+
longitude = None
224+
225+
cur.execute("INSERT INTO RESULTS (LAB, RUN, SAMPLE, COLDATE, REGION, LATITUDE, LONGITUDE, LABEL, MUTATION, FREQUENCY, COVERAGE, PATH)"
226+
" VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
227+
" ON CONFLICT DO NOTHING",
228+
(lab, run, sample, coldate, md['region'], latitude, longitude,
229+
line['position'], line['label'], line['frequency'], line['coverage'], path))
230+
231+
232+
# Aggregate Data
233+
location = md['region'].strip()
234+
region = regions.get(location, None)
235+
if region is None and location != last_fail:
236+
callback(f"Failed to map location {location} to region")
237+
last_fail = location
238+
continue
239+
240+
epiweek = Week.fromdate(coldate.date())
241+
mutation = (line['label'], line['mutation'])
242+
243+
# Check to see if the value exists in database
244+
cur.execute("SELECT * FROM AGGREGATE_MAPPED WHERE REGION = %s AND YEAR = '%s' AND EPIWEEK = '%s' AND NUC = %s AND AMINO = %s",
245+
(region, epiweek.year, epiweek.week, mutation[0], mutation[1]))
246+
if cur.fetchone() is None:
247+
cur.execute("INSERT INTO AGGREGATE_MAPPED (REGION, YEAR, EPIWEEK, NUC, AMINO, NSAMPLES, COUNT, COVERAGE)"
248+
" VALUES(%s, %s, %s, %s, %s, %s, %s, %s)",
249+
(region, epiweek.year, epiweek.week, mutation[0], mutation[1], 1, frequency*coverage, coverage))
250+
else:
251+
cur.execute("UPDATE AGGREGATE_MAPPED SET NSAMPLES = NSAMPLES + 1, COUNT = COUNT + %s, COVERAGE = COVERAGE + %s"
252+
" WHERE REGION = %s AND YEAR = '%s' AND EPIWEEK = '%s' AND NUC = %s AND AMINO = %s",
253+
(frequency*coverage, coverage, region, epiweek.year, epiweek.week, mutation[0], mutation[1]))
254+
255+
100256
if __name__ == "__main__":
101257
args = parse_args()
102258
cb = Callback()
@@ -130,7 +286,9 @@ def get_files(files):
130286
connection_parameters['dbname'] = args.dbname
131287
cur, conn = open_connection(connection_parameters)
132288
files = glob.glob("{}/**/*.mapped.csv".format(args.indir), recursive=True)
133-
clean_files = get_files(files)
134-
insert_files(cur, clean_files, callback=cb.callback)
289+
mapped_files = get_files(files)
290+
unprocessed_mapped_files, unprocessed_runs = new_mapped_files(cur, mapped_files, callback=cb.callback)
291+
metadata = retrieve_metadata(unprocessed_runs, args.upload_dir, callback=cb.callback)
292+
insert_files(cur, unprocessed_mapped_files, metadata, callback=cb.callback)
135293
conn.commit()
136294
conn.close()

0 commit comments

Comments
 (0)