Skip to content

Commit d89b4c3

Browse files
committed
feat: ninagen scripts
1 parent a8f42a1 commit d89b4c3

7 files changed

Lines changed: 347 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ dependencies = [
1919
"pygeometa>=0.19.0",
2020
"orjson>=3.11.4",
2121
"duckdb>=1.4.2",
22-
"lxml>=6.0.2"
22+
"lxml>=6.0.2",
23+
"openpyxl>=3.1.5",
24+
"python-calamine>=0.6.1"
2325
]
2426
description = ""
2527
license = "GPL-3.0+"

src/datasync/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
import typer
66

7-
from . import dms, nva, ubw
7+
from . import dms, ninagen, nva, ubw
88

99
app = typer.Typer()
1010
app.add_typer(nva.app, name="nva")
1111
app.add_typer(ubw.app, name="ubw")
1212
app.add_typer(dms.app, name="dms")
13+
app.add_typer(ninagen.app, name="ninagen")
1314

1415
if __name__ == "__main__":
1516
app()

src/datasync/ninagen/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# load the files containg the scripts
2+
# this allows to attach the scripts to the app
3+
from . import snp_analysis, snp_database # noqa: F401
4+
5+
# then export the app
6+
from .app import app
7+
8+
__all__ = ["app"]

src/datasync/ninagen/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import typer
2+
3+
app = typer.Typer()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import io
2+
import pathlib
3+
4+
import duckdb
5+
6+
from .app import app
7+
8+
9+
@app.command()
10+
def snp_analysis_to_parquet(file: str) -> None:
11+
"""
12+
Convert the csv to a parquet file
13+
14+
:param file: Path to the file csv containing the resut of a SNP analysis
15+
:type file: str
16+
"""
17+
db = duckdb.connect()
18+
19+
table = None
20+
filepath = pathlib.Path(file)
21+
22+
with filepath.open(encoding="utf-8") as f:
23+
table = db.read_csv(
24+
io.BytesIO(
25+
"\n".join(
26+
f.read().replace("\r", "").split("\n\n")[2].split("\n")[2:]
27+
).encode()
28+
)
29+
)
30+
31+
table.select("* rename (column00 as position, column01 as genlab_id)").query(
32+
virtual_table_name="sample_positions",
33+
sql_query="""
34+
unpivot sample_positions
35+
on * exclude (position, genlab_id)
36+
""",
37+
).filter("genlab_id != 'NTC'").select("""
38+
position,
39+
genlab_id,
40+
name as gene,
41+
case
42+
when lower(value) in ('no call') then 0
43+
when value[1] = 'T' then 4
44+
when value[1] = 'G' then 3
45+
when value[1] = 'C' then 2
46+
when value[1] = 'A' then 1
47+
else try_cast(value[1] as int)
48+
end as alle1,
49+
case
50+
when lower(value) in ('no call') then 0
51+
when value[3] = 'T' then 4
52+
when value[3] = 'G' then 3
53+
when value[3] = 'C' then 2
54+
when value[3] = 'A' then 1
55+
else try_cast(value[3] as int)
56+
end as alle2
57+
""").write_parquet(str(filepath.with_suffix(".parquet")))
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import duckdb
2+
from openpyxl.utils.cell import get_column_letter
3+
from python_calamine import CalamineWorkbook
4+
5+
from ..settings import log
6+
from .app import app
7+
8+
9+
@app.command()
10+
def snp_database_normalize(
11+
file: str, sheet: str = "Sheet1", start_row: int = 3
12+
) -> None:
13+
"""
14+
Convert the excel spreadsheet containing SNP data used by genetists
15+
The convertion will cleanup the alleles and make sure they are numbers
16+
17+
:param file: Path to the file to open
18+
:type file: str
19+
:param sheet: Name of the sheet containing the data
20+
:type sheet: str
21+
:param start_row: First row to process
22+
:type start_row: int
23+
"""
24+
db = duckdb.connect()
25+
26+
# The header has some columns with empty names, as they depend on the previous
27+
wb = CalamineWorkbook.from_path(file)
28+
header = wb.get_sheet_by_name(sheet).to_python(skip_empty_area=False)[0]
29+
fixed_header = []
30+
for i, v in enumerate(header):
31+
if not v:
32+
fixed_header.append(header[i - 1] + "_Alle2")
33+
else:
34+
fixed_header.append(v + "_Alle1" if i > 4 else v)
35+
36+
table = db.sql(f"""install excel; load excel;
37+
select *
38+
from read_xlsx('{file}', range = 'A{start_row}:{get_column_letter(len(fixed_header))}', header=false, all_varchar = true)
39+
""").to_arrow_table() # noqa: E501, S608
40+
41+
rel = db.from_arrow(table.rename_columns(fixed_header))
42+
row_numbers = rel.select("""row_number() OVER () as row_number,
43+
* rename ("Fluidigm#" as fluidigm,
44+
"NINA Genlab id" as fish_id,
45+
"Vdr#" as river_id,
46+
"Pop id" as pop_id
47+
)
48+
""")
49+
50+
unpivoted = row_numbers.query(
51+
virtual_table_name="analysis",
52+
sql_query="""
53+
unpivot analysis
54+
on columns(* exclude(
55+
'row_number',
56+
'fluidigm',
57+
'fish_id',
58+
'GUID',
59+
'pop_id',
60+
'river_id'
61+
))
62+
into
63+
name alle
64+
value alle_value
65+
""",
66+
)
67+
68+
grouped = unpivoted.query(
69+
virtual_table_name="alleles",
70+
sql_query=r"""
71+
from alleles
72+
select
73+
row_number,
74+
fluidigm,
75+
fish_id,
76+
GUID,
77+
pop_id,
78+
river_id,
79+
first(regexp_replace(alle, '_Alle\d', '')) as gene,
80+
first(alle_value order by alle) as alle1,
81+
last(alle_value order by alle) as alle2
82+
group by
83+
row_number,
84+
fluidigm,
85+
fish_id,
86+
GUID,
87+
pop_id,
88+
river_id,
89+
regexp_replace(alle, '_Alle\d', '')
90+
""",
91+
)
92+
93+
fixed = grouped.query(
94+
virtual_table_name="genes",
95+
sql_query="""from genes
96+
select * replace (
97+
case
98+
when alle1 = 'T' then 4
99+
when alle1 = 'G' then 3
100+
when alle1 = 'C' then 2
101+
when alle1 = 'A' then 1
102+
when alle1 in ('-', 'N') then 0
103+
else try_cast(alle1 as int)
104+
end as alle1,
105+
case
106+
when alle2 = 'T' then 4
107+
when alle2 = 'G' then 3
108+
when alle2 = 'C' then 2
109+
when alle2 = 'A' then 1
110+
when alle2 in ('-', 'N') then 0
111+
else try_cast(alle2 as int)
112+
end as alle2
113+
)
114+
where alle1 is not null and alle2 is not null
115+
order by row_number
116+
""",
117+
)
118+
119+
fixed.to_parquet("genes.parquet")
120+
# NOTE: it's necessary to materialize first,
121+
# duckdb cannot unpivot and pivot in the same query
122+
# NOTE: all the operations are lazy, so everything up to this point
123+
# will be executed as a single query
124+
fixed_genes = db.read_parquet("genes.parquet") # noqa: F841
125+
log.debug(fixed_genes)
126+
127+
db.sql(
128+
"""
129+
pivot fixed_genes
130+
on gene
131+
using first(alle1) as Alle1, first(alle2) as Alle2
132+
group by row_number, fluidigm, fish_id, GUID, pop_id, river_id
133+
order by row_number
134+
""",
135+
).to_parquet("pivoted.parquet")
136+
137+
# TODO: use original columns order

0 commit comments

Comments
 (0)