diff --git a/.gitignore b/.gitignore index b067edd..09d56b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /Manifest.toml +/test/result \ No newline at end of file diff --git a/Project.toml b/Project.toml index 7cd47ac..49ecc38 100644 --- a/Project.toml +++ b/Project.toml @@ -1,20 +1,24 @@ -name = "Validation" +name = "AnalyticalMethodValidation" uuid = "b401a7b7-5953-49f1-86ca-9d186498acee" authors = ["Yu-Fong Peng "] -version = "0.2.1" +version = "0.1.0" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +Chain = "8be319e6-bccf-4806-a6f7-6fae938471bc" +ChemistryQuantitativeAnalysis = "3ed48883-6809-43ec-b77a-d23b0650d8c4" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataPipes = "02685ad9-2d12-40c3-9f73-c6aeda6a7ff5" -SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" +Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -TypedTables = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9" [compat] CSV = "0.10" +Chain = "0.5" +ChemistryQuantitativeAnalysis = "0.7" +Dictionaries = "0.3" +DataFrames = "1.5.0" DataPipes = "0.3" -SplitApplyCombine = "1" -TypedTables = "1" julia = "1" [extras] diff --git a/README.md b/README.md index eaa5d13..fe13410 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,59 @@ -# Validation +# AnalyticalMethodValidation |CI status|Coverage| |:-------:|:------:| [![][ci-img]][ci-url]| [![][codecov-img]][codecov-url]| -[ci-img]: https://github.com/yufongpeng/Validation.jl/actions/workflows/CI.yml/badge.svg?branch=main -[ci-url]: https://github.com/yufongpeng/Validation.jl/actions/workflows/CI.yml?query=branch%3Amain -[codecov-img]: https://codecov.io/gh/yufongpeng/Validation.jl/branch/main/graph/badge.svg -[codecov-url]: https://codecov.io/gh/yufongpeng/Validation.jl +[ci-img]: https://github.com/yufongpeng/AnalyticalMethodValidation.jl/actions/workflows/CI.yml/badge.svg?branch=main +[ci-url]: https://github.com/yufongpeng/AnalyticalMethodValidation.jl/actions/workflows/CI.yml?query=branch%3Amain +[codecov-img]: https://codecov.io/gh/yufongpeng/AnalyticalMethodValidation.jl/branch/main/graph/badge.svg +[codecov-url]: https://codecov.io/gh/yufongpeng/AnalyticalMethodValidation.jl -A small package for analyzing method validation data. It only accepts csv data from Agilent MassHunter Quantitative analysis and the table needs to be flat. +A small package for analytical method validation, and sample analysis. + +For command line interfaces, see [`juliaquant`](https://github.com/yufongpeng/juliaquant). ## Function -1. `read_data`: read the data with some transformation. -2. `QCReport`: calculate accuracy and rsd of QC samples. -3. `APData`: calculate accuracy, repeatability and reproducibility. -4. `RecoveryData`: calculate recovery by prespiked/postspiked. -5. `MEData`: calculate matrix effect by with_matrix/std_solution. -6. `StabilityData`: calculate accuracy and rsd of QC samples in different tempearture and restoration days. -7. `SampleReport`: average each sample. -8. `Report`: flatten nested `Data` object for CSV output. - -## Scripts -See "/scipt" for command line interfaces. +1. `read`: read csv file(s) into `AnalysisTable` (See [`ChemistryQuantitativeAnalysis.jl`](https://github.com/yufongpeng/ChemistryQuantitativeAnalysis.jl)). Currently, only data from MassHunter Software in wide format is supported. + +### Report functions +These function accept `AnalysisTable` or `Batch`. +1. `qc_report`: calculate accuracy and rsd of QC samples. +2. `ap_report`: calculate accuracy, repeatability and reproducibility. +3. `recovery_report`: calculate recovery by prespiked/postspiked. +4. `me_report`: calculate matrix effect by with_matrix/std_solution. +5. `stability_report`: calculate accuracy and rsd of QC samples in different tempearture and restoration days. +6. `sample_report`: average each sample. + +### Util functions +1. `pivot`: transform dataframe into wide format. +2. `unpivot`: transform dataframe into long format. +3. `selectby`: select values by specific column, and apply `select!` as if the values are columns. This function is useful to merge multiple statistical values into specific formats. +4. `mean_plus_minus_std`: round and merge mean values and standard deviations with "±". +5. `add_percentage`: add "%". +6. `normalize`: normalize dataframe by the given normalizer. +7. `qualify`: replace data out of acceptable range. +8. `qualify!`: replace data out of acceptable range. ## Computation ### Intra-day -$$\mu_{d} = \sum_{j=1}^{n_d} \dfrac{c_{d, j}}{n_d}$$ +$$a_{d,j } = \dfrac{c_{d, j}}{conc.}$$ + +$$\mu_{d} = \sum_{j=1}^{n_d} \dfrac{a_{d, j}}{n_d}$$ -$$s_{intra}^2 = \dfrac{1}{p}\sum_{i = 1}^{p}\sum_{j = 1}^{n_i} \dfrac{(c_{i, j} - \mu_i)^2}{n_i - 1}$$ +$$s_{intra}^2 = \dfrac{1}{p}\sum_{i = 1}^{p}\sum_{j = 1}^{n_i} \dfrac{(a_{i, j} - \mu_i)^2}{n_i - 1}$$ -$$accuracy_{intra, d} = \dfrac{\mu_{d}}{conc.}$$ +$$accuracy_{intra, d} = \mu_{d}$$ $$rsd_{intra, d} = \dfrac{s_{intra}}{accuracy_{intra, d}}$$ $p$: number of days, $n_i$: number of repeats of $i$ th day, $c_{i, j}$: measured concentration of $i$ th day and $j$ th repeat, $conc.$: reference concentration ### Inter-day -$$\mu = \dfrac{1}{p}\sum_{i = 1}^{p}\sum_{j = 1}^{n_i} \dfrac{c_{i, j}}{n_i}$$ -$$accuracy_{inter} = \sum_{i = 1}^{p} \dfrac{accuracy_{intra, i}}{p} = \dfrac{\mu}{conc.}$$ +$$\mu = \dfrac{1}{p}\sum_{i = 1}^{p}\sum_{j = 1}^{n_i} \dfrac{a_{i, j}}{n_i}$$ + +$$accuracy_{inter} = \sum_{i = 1}^{p} \dfrac{accuracy_{intra, i}}{p} = \mu$$ $$repeatability = rsd_{intra} = \dfrac{s_{intra}}{accuracy_{inter}}$$ @@ -49,4 +63,10 @@ $$s_{inter}^2 = max\ \{0, s_{between}^2 - \dfrac{s_{intra}^2}{\hat{n}}\ \}$$ $$\hat{n}=\dfrac{p}{\sum_{i=1}^p\dfrac{1}{n_i}}$$ -$$reproducibility = rsd_{total} = \dfrac{\sqrt{s_{inter}^2+s_{intra}^2}}{accuracy_{inter}}$$ \ No newline at end of file +$$reproducibility = rsd_{total} = \dfrac{\sqrt{s_{inter}^2+s_{intra}^2}}{accuracy_{inter}}$$ + +## Reference +[Guidelines and Recommendations of the GTFCh](https://www.gtfch.org/cms/index.php/en/guidelines) + +[Appendix B - Requirements for the validation of analytical methods](https://www.gtfch.org/cms/images/stories/files/Appendix%20B%20GTFCh%2020090601.pdf) +p.21~p.22 \ No newline at end of file diff --git a/script/ap_main.jl b/script/ap_main.jl deleted file mode 100644 index c414dec..0000000 --- a/script/ap_main.jl +++ /dev/null @@ -1,78 +0,0 @@ -using Validation, CSV - -global input = String[] -global id = r"Pre.*_(.*)_.*" -global type = "Accuracy" -global ouput = "accuracy_precision.csv" -global help = false - -let i = 0 - while i < length(ARGS) - i += 1 - if ARGS[i] == "-h" - global help = true - break - elseif ARGS[i] == "-i" - i += 1 - global id = Regex(ARGS[i]) - elseif ARGS == "-t" - i += 1 - global type = ARGS[i] - elseif ARGS[i] == "-s" - i += 1 - global output = ARGS[i] - else - any(x -> occursin("-", x), ARGS[i:end]) && throw(ArgumentError("Invalid switches position")) - global input = ARGS[i:end] - break - end - end -end - -function main() - if help - println(stdout, - """ - - julia [julia switches] -- ap_main.jl [swithes] [input files] - - Swithces (a '*' marks the default value) - -h Print this message - -i {"Pre.*_(.*)_.*"*} Set the identifier for the AP experiment samples; this will be wrapped in `Regex`. - -t {Accuracy*|"Final Conc."|Area} Set the quantification value type - -s {accuracy_precision.csv*} Set the ouput file - """) - return - end - ap = APData(read_data.(input)...; id, type) - level = keys(ap.summary) - printstyled("Daily: ", color = :blue) - println() - for k in level - printstyled("Level ", k, "\n", color = :green) - display(ap.daily.accuracy[k]) - display(ap.daily.rsd[k]) - println() - end - printstyled("Final: ", color = :blue) - println() - for k in level - printstyled("Level ", k, "\n", color = :green) - display(ap.summary[k]) - println() - end - i = 0 - file = output - name = split(basename(output), ".csv")[1] - dir = dirname(file) - dir = isempty(dir) ? pwd() : dir - filename = basename(file) - while filename in readdir(dir) - i += 1 - filename = join([name, "($i).csv"], "") - file = joinpath(dir, filename) - end - CSV.write(file, Report(ap)) -end - -(@__MODULE__() == Main) && main() diff --git a/script/me_main.jl b/script/me_main.jl deleted file mode 100644 index 2440d25..0000000 --- a/script/me_main.jl +++ /dev/null @@ -1,72 +0,0 @@ -using Validation, CSV - -global input = String[] -global matrix = r"Pre.*_(.*)_.*" -global stds = r"Post.*_(.*)_.*" -global type = "Final Conc." -global output = "me.csv" -global help = false - -let i = 0 - while i < length(ARGS) - i += 1 - if ARGS[i] == "-h" - global help = true - break - elseif ARGS[i] == "-matrix" - i += 1 - global matrix = Regex(ARGS[i]) - elseif ARGS[i] == "-stds" - i += 1 - global stds = Regex(ARGS[i]) - elseif ARGS == "-t" - global type = ARGS[i] - elseif ARGS[i] == "-s" - i += 1 - global output = ARGS[i] - else - any(x -> occursin("-", x), ARGS[i:end]) && throw(ArgumentError("Invalid switches position")) - global input = ARGS[i:end] - break - end - end -end - -function main() - if help - println(stdout, - """ - - julia [julia switches] -- me_main.jl [swithes] [input files] - - Swithces (a '*' marks the default value) - -h Print this message - -matrix {"Pre.*_(.*)_.*"*} Set the identifier for the samples; this will be wrapped in `Regex`. - -stds {"Post.*_(.*)_.*"*} Set the identifier for the standard solutions; this will be wrapped in `Regex`. - -t {Accuracy|"Final Conc."*|Area} Set the quantification value type - -s {me.csv*} Set the ouput file - """) - return - end - me = MEData(reduce(append!, read_data.(input)); matrix, stds, type) - level = keys(me.data) - for k in level - printstyled("Level ", k, "\n", color = :green) - display(me.data[k]) - println() - end - i = 0 - file = output - name = split(basename(output), ".csv")[1] - dir = dirname(file) - dir = isempty(dir) ? pwd() : dir - filename = basename(file) - while filename in readdir(dir) - i += 1 - filename = join([name, "($i).csv"], "") - file = joinpath(dir, filename) - end - CSV.write(file, Report(me.data)) -end - -(@__MODULE__() == Main) && main() \ No newline at end of file diff --git a/script/qc_main.jl b/script/qc_main.jl deleted file mode 100644 index e9e613d..0000000 --- a/script/qc_main.jl +++ /dev/null @@ -1,60 +0,0 @@ -using Validation, CSV - -global input = String[] -global id = r"PooledQC" -global type = "Final Conc." -global output = "qc.csv" -global help = false - -let i = 0 - while i < length(ARGS) - i += 1 - if ARGS[i] == "-h" - global help = true - break - elseif ARGS[i] == "-i" - i += 1 - global id = Regex(ARGS[i]) - elseif ARGS == "-t" - global type = ARGS[i] - else - any(x -> occursin("-", x), ARGS[i:end]) && throw(ArgumentError("Invalid switches position")) - global input = ARGS[i:end] - break - end - end -end - -function main() - if help - println(stdout, - """ - - julia [julia switches] -- qc_main.jl [swithes] [input files] - - Swithces (a '*' marks the default value) - -h Print this message - -i {PooledQC*} Set the identifier for the AP experiment samples; this will be wrapped in `Regex`. - -t {Accuracy|"Final Conc."*|Area} Set the quantification value type - -s {accuracy_precision.csv*} Set the ouput file - """) - return - end - qc = QCReport(reduce(append!, read_data.(input)); id, type) - display(qc) - println() - i = 0 - file = output - name = split(basename(output), ".csv")[1] - dir = dirname(file) - dir = isempty(dir) ? pwd() : dir - filename = basename(file) - while filename in readdir(dir) - i += 1 - filename = join([name, "($i).csv"], "") - file = joinpath(dir, filename) - end - CSV.write(file, qc.report) -end - -(@__MODULE__() == Main) && main() diff --git a/script/recovery_main.jl b/script/recovery_main.jl deleted file mode 100644 index 864bd50..0000000 --- a/script/recovery_main.jl +++ /dev/null @@ -1,72 +0,0 @@ -using Validation, CSV - -global input = String[] -global pre = r"Pre.*_(.*)_.*" -global post = r"Post.*_(.*)_.*" -global type = "Final Conc." -global output = "recovery.csv" -global help = false - -let i = 0 - while i < length(ARGS) - i += 1 - if ARGS[i] == "-h" - global help = true - break - elseif ARGS[i] == "-pre" - i += 1 - global pre = Regex(ARGS[i]) - elseif ARGS[i] == "-post" - i += 1 - global post = Regex(ARGS[i]) - elseif ARGS == "-t" - global type = ARGS[i] - elseif ARGS[i] == "-s" - i += 1 - global output = ARGS[i] - else - any(x -> occursin("-", x), ARGS[i:end]) && throw(ArgumentError("Invalid switches position")) - global input = ARGS[i:end] - break - end - end -end - -function main() - if help - println(stdout, - """ - - julia [julia switches] -- recovery_main.jl [swithes] [input files] - - Swithces (a '*' marks the default value) - -h Print this message - -pre {"Pre.*_(.*)_.*"*} Set the identifier for the prespiked samples; this will be wrapped in `Regex`. - -post {"Post.*_(.*)_.*"*} Set the identifier for the postspiked samples; this will be wrapped in `Regex`. - -t {Accuracy|"Final Conc."*|Area} Set the quantification value type - -s {recovery.csv*} Set the ouput file - """) - return - end - recovery = RecoveryData(reduce(append!, read_data.(input)); pre, post, type) - level = keys(recovery.data) - for k in level - printstyled("Level ", k, "\n", color = :green) - display(recovery.data[k]) - println() - end - i = 0 - file = output - name = split(basename(output), ".csv")[1] - dir = dirname(file) - dir = isempty(dir) ? pwd() : dir - filename = basename(file) - while filename in readdir(dir) - i += 1 - filename = join([name, "($i).csv"], "") - file = joinpath(dir, filename) - end - CSV.write(file, Report(recovery.data)) -end - -(@__MODULE__() == Main) && main() \ No newline at end of file diff --git a/script/stability_main.jl b/script/stability_main.jl deleted file mode 100644 index 50265e8..0000000 --- a/script/stability_main.jl +++ /dev/null @@ -1,82 +0,0 @@ -using Validation, CSV - -global input = String[] -global d0 = r"S.*_(.*)_.*" #r"Pre.*_(.*)_.*" -global days = r"S.*_(.*)_(.*)_(.*)_.*" -global order = "TDL" -global type = "Accuracy" -global output = "stability.csv" -global help = false - -let i = 0 - while i < length(ARGS) - i += 1 - if ARGS[i] == "-h" - global help = true - break - elseif ARGS[i] == "-d0" - i += 1 - global d0 = Regex(ARGS[i]) - elseif ARGS[i] == "-d" - i += 1 - global days = Regex(ARGS[i]) - elseif ARGS[i] == "-o" - i += 1 - global days = ARGS[i] - elseif ARGS == "-t" - i += 1 - global type = ARGS[i] - elseif ARGS[i] == "-s" - i += 1 - global output = ARGS[i] - else - any(x -> occursin("-", x), ARGS[i:end]) && throw(ArgumentError("Invalid switches position")) - global input = ARGS[i:end] - break - end - end -end - -function main() - if help - println(stdout, - """ - - julia [julia switches] -- stability_main.jl [swithes] [input files] - - Swithces (a '*' marks the default value) - -h Print this message - -d0 {"S.*_(.*)_.*"*} Set the identifier for the day0 samples; this will be wrapped in `Regex`. The concentration level is captured in the identifier - -d {"S.*_(.*)_(.*)_(.*)_.*"*} Set the identifier for the stability samples; this will be wrapped in `Regex`. The storage condition, concentration level, and storage days are captured in the identifier; the order can be set by -o - -o {TDL*} Set the order of captured values from -d identifiers; T is temperature (storage condition); D is storage days; L is concentration level - -t {Accuracy|"Final Conc."*|Area} Set the quantification value type - -s {stability.csv*} Set the ouput file - """) - return - end - stability = StabilityData(reduce(append!, read_data.(input)); d0, days, order, type) - for temp in keys(stability.accuracy) - printstyled("Temperature ", temp, color = :blue) - println() - for k in keys(stability.accuracy[temp]) - printstyled("Level ", k, "\n", color = :green) - display(stability.accuracy[temp][k]) - display(stability.rsd[temp][k]) - println() - end - end - i = 0 - file = output - name = split(basename(output), ".csv")[1] - dir = dirname(file) - dir = isempty(dir) ? pwd() : dir - filename = basename(file) - while filename in readdir(dir) - i += 1 - filename = join([name, "($i).csv"], "") - file = joinpath(dir, filename) - end - CSV.write(file, Report(stability)) -end - -(@__MODULE__() == Main) && main() diff --git a/src/AnalyticalMethodValidation.jl b/src/AnalyticalMethodValidation.jl new file mode 100644 index 0000000..73832e5 --- /dev/null +++ b/src/AnalyticalMethodValidation.jl @@ -0,0 +1,104 @@ +module AnalyticalMethodValidation +using DataPipes, Statistics, CSV, DataFrames, Chain, ChemistryQuantitativeAnalysis, Dictionaries +export read_masshunter, qc_report, sample_report, ap_report, me_report, recovery_report, stability_report, + pivot, unpivot, + mean_plus_minus_std, add_percentage, selectby, normalize, qualify!, qualify +const CQA = ChemistryQuantitativeAnalysis + +include("utils.jl") + +""" + read(file, source = :mh) + +Read data into `AnalysisTable` from various sourece. + +Currently, only data from Agilent MassHunter Quantitative analysis is implemented. The table needs to be flat. There must be a column whose name contains \"Data File\" as id for each file. + +The returned `AnalysisTable` contains multiple `SampleDataTable` to repressent different data types which `samplecol` is `:File`. +""" +function read(file, source = :mh; datatype = Dictionary{String, Symbol}(), numtype = Float64) + if source == :mh || source == :MassHunter + read_masshunter(file; datatype, numtype) + end +end + +function read_masshunter(file; datatype = Dictionary{String, Symbol}(), numtype) + get!(datatype, "Area", :area) + get!(datatype, "Height", :height) + get!(datatype, "ISTD Resp. Ratio", :relative_signal) + get!(datatype, "Final Conc.", :estimated_concentration) + get!(datatype, "Accuracy", :accuracy) + t = Vector{Vector{String}}(undef, 2) + for (i, l) in enumerate(eachline(file)) + i > 2 && break + t[i] = split(l, ",") + end + #t[2] = replace.(t[2], r".*Con.*" => "Concentration") + ic = @p t[1] findall(occursin("Results", _)) + #id_info = @p t[1] findfirst(occursin("Results", _)) + cname = @p ic map(replace(t[1][_], " Results" => "")) + n_datatype = round(Int, (length(t[1]) - ic[1] + 1) / length(ic)) - 1 + dname = t[2][ic[1]:ic[1] + n_datatype] + datafile = findfirst(x -> occursin("Data File", string(x)), t[2]) + tbl_id = CSV.read(file, DataFrame; select = [datafile], skipto = 3) + at = analysistable(get(datatype, dname[i + 1], Symbol(dname[i + 1])) => begin + tbl = CSV.read(file, DataFrame; select = ic .+ i, skipto = 3) + for col in eachcol(tbl) + replace!(col, missing => 0.0) + end + SampleDataTable(:File, DataFrame("File" => getproperty(tbl_id, propertynames(tbl_id)[1]), (cname .=> convert.(Vector{numtype}, eachcol(tbl)))...)) + end for i in 0:n_datatype) + if "Accuracy" in dname + dt = getproperty(at, get(datatype, "Accuracy", :accuracy)) + for a in eachanalyte(dt) + a ./= 100 + end + end + at +end + +function read_masshunter(files::AbstractVector; datatype = Dictionary{String, Symbol}(), numtype) + get!(datatype, "Area", :area) + get!(datatype, "Height", :height) + get!(datatype, "ISTD Resp. Ratio", :relative_signal) + get!(datatype, "Final Conc.", :estimated_concentration) + get!(datatype, "Accuracy", :accuracy) + dict = map(files) do file + t = Vector{Vector{String}}(undef, 2) + for (i, l) in enumerate(eachline(file)) + i > 2 && break + t[i] = split(l, ",") + end + #t[2] = replace.(t[2], r".*Con.*" => "Concentration") + ic = @p t[1] findall(occursin("Results", _)) + #id_info = @p t[1] findfirst(occursin("Results", _)) + cname = @p ic map(replace(t[1][_], " Results" => "")) + n_datatype = round(Int, (length(t[1]) - ic[1] + 1) / length(ic)) - 1 + dname = t[2][ic[1]:ic[1] + n_datatype] + datafile = findfirst(x -> occursin("Data File", string(x)), t[2]) + tbl_id = CSV.read(file, DataFrame; select = [datafile], skipto = 3) + dictionary(get(datatype, dname[i + 1], Symbol(dname[i + 1])) => begin + tbl = CSV.read(file, DataFrame; select = ic .+ i, skipto = 3) + for col in eachcol(tbl) + replace!(col, missing => 0.0) + end + DataFrame("File" => getproperty(tbl_id, propertynames(tbl_id)[1]), (cname .=> convert.(Vector{numtype}, eachcol(tbl)))...) + end for i in 0:n_datatype) + end + datatype = reduce(intersect!, map(keys, dict)) + at = analysistable(dt => begin + SampleDataTable(:File, vcat(get.(dict, dt, nothing)...)) + end for dt in datatype) + acc = get(datatype, "Accuracy", :accuracy) + if acc in propertynames(at) + dt = getproperty(at, get(datatype, "Accuracy", :accuracy)) + for a in eachanalyte(dt) + a ./= 100 + end + end + at +end + +include("report.jl") + +end \ No newline at end of file diff --git a/src/Validation.jl b/src/Validation.jl deleted file mode 100644 index a93d1cb..0000000 --- a/src/Validation.jl +++ /dev/null @@ -1,159 +0,0 @@ -module Validation -using DataPipes, SplitApplyCombine, TypedTables, Statistics, CSV -import Base: sort, show -export read_data, Report, Data, QCReport, SampleReport, APData, MEData, RecoveryData, StabilityData - -include("utils.jl") - -""" - Data - -Abstract type for data. -""" -abstract type Data end - -""" - Report{T} - -Report type. - -# Fields -* `data`: `Data` object. -* `report`: `Table` object. -""" -mutable struct Report{T} - data::T - report::Table -end - -""" - QCReport - -A wrapper of `Table` containing QC results. - -# Fields -* `report`: QC table. -""" -struct QCReport - report::Table - function QCReport(tbl::Table; id = r"PooledQC", type = "Final Conc.", stats = [mean, std, rsd]) - cols = propertynames(tbl) - qc = @p tbl filter(occursin(id, getproperty(_, cols[1]))) filter(==(type, getproperty(_, cols[2]))) Table - cols = propertynames(qc)[3:end] - report = @p getproperties(qc, cols) columns map(apply(stats, _)) - new(Table((Stats = stats, ), report)) - end -end - -""" - SampleReport - -A wrapper of `Table` containing sample results. - -# Fields -* `report`: sample table. -""" -struct SampleReport - report::Table - function SampleReport(tbl::Table; id = r"Sample_(\d*).*", type = "Final Conc.") - cols = propertynames(tbl) - sample = @p tbl filter(occursin(id, getproperty(_, cols[1]))) filter(==(type, getproperty(_, cols[2]))) Table - getproperty(sample, cols[1]) .= map(x -> match(id, x)[1], getproperty(sample, cols[1])) - @p sample group(getproperty(cols[1])) map(getproperties(_, cols[3:end])) map(map(mean, columns(_))) reduce(vcat) Table Table((sample = unique(getproperty(sample, cols[1])),); (cols[3:end] .=> collect(columns(__)))...) new - end -end - -""" - APData <: Data - -A type for accuracy and precision results. - -# Fields -* `daily`: a `Table` containing daily accuracy, standard deviation, and relative standard deviation. -* `summary`: a `Table` containing overall accuracy, intraday variation, interday variation, repeatability, and reproducalbility. -""" -struct APData <: Data - daily::NamedTuple - summary::TypedTables.Dictionary -end - -""" - MEData <: Data - -A type for matrix effect results. - -# Fields -* `data`: a `Dictionary`. The keys are concentration levels, and the values are `Table`s. -""" -struct MEData <: Data - data::TypedTables.Dictionary -end - -""" - RecoveryData <: Data - -A type for recovery results. - -# Fields -* `data`: a `Dictionary`. The keys are concentration levels, and the values are `Table`s. -""" -struct RecoveryData <: Data - data::TypedTables.Dictionary -end - -""" - StabilityData <: Data - -A type for stability results. - -# Fields -* `accuracy`: a `Dictionary`. The keys are storage conditions, and the values are `Dictionary`s whose keys are concentration levels, and values are `Table`s. -* `rsd`: a `Dictionary`. The keys are storage conditions, and the values are `Dictionary`s whose keys are concentration levels, and values are `Table`s. -""" -struct StabilityData <: Data - accuracy::TypedTables.Dictionary - std::TypedTables.Dictionary -end - -""" - read_data(file) - -Read csv data from Agilent MassHunter Quantitative analysis. The table needs to be flat. -""" -function read_data(file) - t = Vector{Vector{String}}(undef, 2) - for (i, l) in enumerate(eachline(file)) - i > 2 && break - t[i] = split(l, ",") - end - #t[2] = replace.(t[2], r".*Con.*" => "Concentration") - ic = @p t[1] findall(occursin("Results", _)) - #id_info = @p t[1] findfirst(occursin("Results", _)) - cname = @p ic map(Symbol(replace(t[1][_], " Results" => ""))) - n_datatype = round(Int, (length(t[1]) - ic[1] + 1) / length(ic)) - 1 - dname = t[2][ic[1]:ic[1] + n_datatype] - tbl_id = CSV.read(file, Table; select = [2], skipto = 3) - mapreduce(vcat, 0:n_datatype) do i - tbl = CSV.read(file, Table; select = ic .+ i, skipto = 3) - Table(; Symbol(t[2][2]) => tbl_id.Column2, var"Data Type" = repeat([dname[i + 1]], size(tbl_id, 1)), (cname .=> collect(columns(tbl)))...) - end -end - -include("report.jl") - -function show(io::IO, ::MIME"text/plain", report::Report{T}) where T - println(io, "Report of ", replace(repr(T), "Data" => "")) - show(io, MIME("text/plain"), report.report) -end - -function show(io::IO, ::MIME"text/plain", report::QCReport) - print(io, "QC Report\n") - show(io, MIME("text/plain"), report.report) -end - -function show(io::IO, ::MIME"text/plain",report::SampleReport) - print(io, "Sample Report\n") - show(io, MIME("text/plain"), report.report) -end - -end \ No newline at end of file diff --git a/src/report.jl b/src/report.jl index d6fb8a1..1307225 100644 --- a/src/report.jl +++ b/src/report.jl @@ -1,192 +1,405 @@ """ - APData(tbls::Table...; id = r"Pre.*_(.*)_.*", type = "Accuracy") + qc_report(at::AnalysisTable; + id = r"PooledQC", + type = :estimated_concentration, + pct = true, + stats = [mean, std, pct ? rsd_pct : rsd], + names = ["Mean", "Standard Deviation", "Relative Standard Deviation" * (pct ? "(%)" : "")], + colanalyte = :Analyte, + colstats = :Stats + ) -Create `APData` from multiple tables. +Compute statistics of QC data. # Arguments -* `tbls`: each table should contain data of one day. -* `id`: `Regex` identifier for the AP experiment samples. The concentration level is captured in the identifier. -* `type`: quantification value type. +* `at`: `AnalysisTable`. +* `id`: `Regex` identifier for the QC samples. +* `pct`: whether converting ratio data into percentage (*100). +* `type`: data type for calculation. +* `stats`: statistics functions. +* `names`: names of statistics. When `nothing` is given, `stats` is served as `names`. +* `colanalyte`: column name of analytes. +* `colstats`: column name of statistics. """ -function APData(tbls::Table...; id = r"Pre.*_(.*)_.*", type = "Accuracy") - gtbls = map(tbls) do tbl - cols = propertynames(tbl) - ap = @p tbl filter(occursin(id, getproperty(_, cols[1]))) filter(==(type, getproperty(_, cols[2]))) Table - level = cols[1] - getproperty(ap, level) .= getindex.(match.(id, getproperty(ap, level)), 1) - ap = @p ap group(getproperty(level)) map(getproperties(_, propertynames(ap)[3:end])) map(columns) - end - ns = @p gtbls mapreduce(map(x -> map(y -> 1 / length(y), x), _), fmap(vcat)) map(Table) map(map(mean, columns(_))) - accuracies = @p gtbls mapreduce(map(x -> map(mean, x), _), fmap(vcat)) map(Table) - vars = @p gtbls mapreduce(map(x -> map(var, x), _), fmap(vcat)) map(Table) - accuracy = @p accuracies map(map(mean, columns(_))) - var_intra = @p vars map(map(mean, columns(_))) - std_intra = @p var_intra map(map(sqrt, _)) - rsds = fmap(Table ∘ (fmap ∘ fmap)(f_rsd))(vars, accuracies) - stds = fmap(Table ∘ (fmap ∘ fmap)(f_std))(vars) - var_bet = @p accuracies map(map(var, columns(_))) - std_bet = @p var_bet map(map(sqrt, _)) - var_inter = (fmap ∘ fmap)(f_var_inter)(var_bet, var_intra, ns) - std_inter = @p var_inter map(map(sqrt, _)) - repeatability = (fmap ∘ fmap)(f_rsd)(var_intra, accuracy) - reproducibility = (fmap ∘ fmap)(f_rsd)(var_inter, var_intra, accuracy) - stats = ["Accuracy", "Intraday standard deviation", "Intraday variance", "\"Betweenday\" standard deviation", "\"Betweenday\" variance", "Interday standard deviation", "Interday variance", "Repeatability", "Reproducibility"] - APData( - (accuracy = accuracies, std = stds, rsd = rsds, var = vars), - fmap((x -> Table((Stats = stats, ), x)) ∘ fmap(vcat))(accuracy, std_intra, var_intra, std_bet, var_bet, std_inter, var_inter, repeatability, reproducibility) - ) +function qc_report(at::AnalysisTable; + id = r"PooledQC", + type = :estimated_concentration, + pct = true, + stats = [mean, std, pct ? rsd_pct : rsd], + names = ["Mean", "Standard Deviation", "Relative Standard Deviation" * (pct ? "(%)" : "")], + colanalyte = :Analyte, + colstats = :Stats + ) + colanalyte = Symbol(colanalyte) + colstats = Symbol(colstats) + dt = getproperty(at, Symbol(type)) + df = CQA.table(dt) + if !isa(df, DataFrame) + df = DataFrame(df) + end + analytes = analytename(at) + scol = samplecol(dt) + names = isnothing(names) ? stats : names + @chain df begin + filter(scol => Base.Fix1(occursin, id), _) + stack(analytes, scol; variable_name = colanalyte, value_name = :Data) + select!([colanalyte, :Data]) + groupby(colanalyte) + combine(:Data => x -> vcat(apply(stats, x)...); renamecols = false) + insertcols!(_, 2, colstats => repeat(string.(names); outer = size(_, 1) ÷ length(stats))) + sort!(colanalyte) + end end """ - MEData(tbl::Table; matrix = r"Post.*_(.*)_.*", stds = r"STD.*_(.*)_.*", type = "Area") + sample_report(at::AnalysisTable; id = r"Sample_(\\d*).*", type = :estimated_concentration, colanalyte = :Analyte) -Create `MEData` from a table. +Compute mean of sample data. # Arguments -* `tbl`: a `Table`. -* `matrix`: `Regex` identifier for samples with matrix. The concentration level is captured in the identifier. -* `stds`: `Regex` identifier for standard solution. The concentration level is captured in the identifier. -* `type`: quantification value type. +* `at`: `AnalysisTable`. +* `id`: `Regex` identifier for the QC samples. +* `type`: data type for calculation. +* `colanalyte`: column name of analytes. """ -MEData(tbl::Table; matrix = r"Post.*_(.*)_.*", stds = r"STD.*_(.*)_.*", type = "Area") = ratio_data(tbl; type, pre = matrix, post = stds) |> MEData +function sample_report(at::AnalysisTable; id = r"Sample_(\d*).*", type = :estimated_concentration, colanalyte = :Analyte) + colanalyte = Symbol(colanalyte) + dt = getproperty(at, Symbol(type)) + df = CQA.table(dt) + if !isa(df, DataFrame) + df = DataFrame(df) + end + analytes = analytename(at) + scol = samplecol(dt) + sample = @p df filter(scol => Base.Fix1(occursin, id)) + sample[:, scol] = map(x -> match(id, x)[1], sample[!, scol]) + @chain sample begin + stack(analytes, scol; variable_name = colanalyte, value_name = :Data) + select!([scol, colanalyte, :Data]) + groupby([scol, colanalyte]) + combine(:Data => mean; renamecols = false) + end +end """ - RecoveryData(tbl::Table; pre = r"Pre.*_(.*)_.*", post = r"Post.*_(.*)_.*", type = "Final Conc.") + ap_report(at::AnalysisTable; + id = r"Pre.*_(.*)_.*", + type = :accuracy, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + colday = :Day, + collevel = :Level + ) -Create `RecoveryData` from a table. +Compute accuracy and precision. A `NamedTuple` is returned with two elements: `daily` is a `DataFrame` containing accuracy and standard deviation for each day, and `summary` is a `DataFrame` containing overall accuracy, repeatability and reproducibility. # Arguments -* `tbl`: a `Table`. -* `pre`: `Regex` identifier for prespiked samples. The concentration level is captured in the identifier. -* `post`: `Regex` identifier for postspiked samples. The concentration level is captured in the identifier. -* `type`: quantification value type. +* `at`: `AnalysisTable`. +* `id`: `Regex` identifier for the AP experiment samples. The day and concentration level is captured in the identifier; the order can be set by `order`. +* `order`: a string for setting the order of captured values from `id`; D is day; L is concentration level. +* `type`: data type for calculation. +* `pct`: whether converting ratio data into percentage (*100). +* `colanalyte`: column name of analytes. +* `colstats`: column name of statistics. +* `colday`: column name of validation day. +* `collevel`: column name of level. """ -RecoveryData(tbl::Table; pre = r"Pre.*_(.*)_.*", post = r"Post.*_(.*)_.*", type = "Final Conc.") = ratio_data(tbl; type, pre, post) |> RecoveryData - -function ratio_data(tbl::Table; pre = r"Pre.*_(.*)_.*", post = r"Post.*_(.*)_.*", type = "Final Conc.") - cols = propertynames(tbl) - df = @p tbl filter(==(type, getproperty(_, cols[2]))) - pre_tbl = @p df filter(occursin(pre, getproperty(_, cols[1]))) Table - post_tbl = @p df filter(occursin(post, getproperty(_, cols[1]))) Table - level = cols[1] - getproperty(pre_tbl, level) .= getindex.(match.(pre, getproperty(pre_tbl, level)), 1) - getproperty(post_tbl, level) .= getindex.(match.(post, getproperty(post_tbl, level)), 1) - pre_tbls = @p pre_tbl group(getproperty(level)) map(getproperties(_, propertynames(pre_tbl)[3:end])) map(columns) - post_tbls = @p post_tbl group(getproperty(level)) map(getproperties(_, propertynames(post_tbl)[3:end])) map(columns) - pre_report = @p pre_tbls map(Table ∘ fmap(x -> apply([mean, rsd], x))) - post_report = @p post_tbls map(Table ∘ fmap(x -> apply([mean, rsd], x))) - report = map((x, y) -> Table((Stats = ["Recovery", "RSD"], ), Table(fmap(map)([pct_ratio, std_sum], x, y))), pre_report, post_report) - for level in report - drugs = @p level propertynames collect - insert!(getproperty(level, drugs[1]), 2, "standard deviation") - popfirst!(drugs) - for drug in drugs - dt = getproperty(level, drug) - insert!(dt, 2, dt[1] * dt[2]) +function ap_report(at::AnalysisTable; + id = r"Pre(.*)_(.*)_.*", + order = "DL", + type = :accuracy, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + colday = :Day, + collevel = :Level + ) + colanalyte = Symbol(colanalyte) + colstats = Symbol(colstats) + colday = Symbol(colday) + collevel = Symbol(collevel) + col = ["Accuracy", "Standard Deviation", "Accuracy", "Intraday Standard Deviation", "Betweenday Standard Deviation", "Interday Standard Deviation", "Repeatability", "Reproducibility"] + if pct + f_var_sum_rsd_ = f_var_sum_rsd_pct + ratio_ = ratio_pct + col .*= "(%)" + else + f_var_sum_rsd_ = f_var_sum_rsd + ratio_ = / + end + dt = getproperty(at, Symbol(type)) + df = CQA.table(dt) + if !isa(df, DataFrame) + df = DataFrame(df) + end + analytes = analytename(at) + level = samplecol(dt) + ap_df = @chain df begin + filter(level => Base.Fix1(occursin, id), _) + insertcols!(_, 1, (Symbol.("__", split(order, "")) .=> map(1:length(order)) do i + getindex.(match.(id, _[!, level]), i) end + )...) + select!(Not([level])) end - report + rename!(ap_df, :__D => colday, :__L => collevel) + ap_df[!, colday] .= @p getproperty(ap_df, colday) map(parse(Int, _)) + ap_df = stack(ap_df, analytes, [collevel, colday]; variable_name = colanalyte, value_name = :Data) + pct && (ap_df[!, colday] .*= 100) + gdf = groupby(ap_df, [colanalyte, collevel, colday]) + ns = @chain gdf begin + combine(:Data .=> inv ∘ length; renamecols = false) + groupby([colanalyte, collevel]) + combine(:Data => mean; renamecols = false) + end + accuracies = combine(gdf, :Data => mean; renamecols = false) + stds = combine(gdf, :Data => std; renamecols = false) + vars = combine(gdf, :Data => var; renamecols = false) + accuracy, var_bet = @chain accuracies begin + groupby([colanalyte, collevel]) + (combine(_, :Data => mean; renamecols = false), combine(_, :Data => var; renamecols = false)) + end + var_intra = combine(groupby(vars, [colanalyte, collevel]), :Data => mean; renamecols = false) + var_inter = deepcopy(accuracy) + var_inter[:, :Data] = @. f_var_inter(getproperty([var_bet, var_intra, ns], :Data)...) + reproducibility = deepcopy(accuracy) + reproducibility[:, :Data] = @. f_var_sum_rsd_(getproperty([var_intra, var_inter, reproducibility], :Data)...) + std_intra = transform!(var_intra, :Data => ByRow(sqrt); renamecols = false) + std_bet = transform!(var_bet, :Data => ByRow(sqrt); renamecols = false) + std_inter = transform!(var_inter, :Data => ByRow(sqrt); renamecols = false) + repeatability = deepcopy(accuracy) + repeatability[:, :Data] = @. ratio_(getproperty([std_intra, repeatability], :Data)...) + for (st, nm) in zip([accuracies, stds, accuracy, std_intra, std_bet, std_inter, repeatability, reproducibility], col) + insertcols!(st, colstats => nm) + end + daily = @chain vcat(accuracies, stds) begin + select!([colanalyte, collevel, colday, colstats, :Data]) + sort!([colanalyte, collevel, colday]) + end + summary = @chain vcat(std_intra, accuracy, std_bet, std_inter, repeatability, reproducibility) begin + select!([colanalyte, collevel, colstats, :Data]) + sort!([colanalyte, collevel]) + end + (; daily, summary) end """ - StabilityData(tbl::Table; d0 = r"S.*_(.*)_.*", days = r"S.*_(.*)_(.*)_(.*)_.*", order = "TDL", type = "Accuracy") + me_report(at::AnalysisTable; + matrix = r"Post.*_(.*)_.*", + stds = r"STD.*_(.*)_.*", + type = :area, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + collevel = :Level + ) -Create `StabilityData` from a table. +Compute matrix effects. # Arguments -* `tbl`: a `Table`. -* `d0`: `Regex` identifier for day0 samples. The concentration level is captured in the identifier. -* `days`: `Regex` identifier for the stability samples. The storage condition, concentration level, and storage days are captured in the identifier; the order can be set by `order`. -* `order`: a string for setting the order of captured values from `days`; T is temperature (storage condition); D is storage days; L is concentration level -* `type`: quantification value type. +* `at`: `AnalysisTable`. +* `matrix`: `Regex` identifier for samples with matrix. The concentration level is captured in the identifier. +* `stds`: `Regex` identifier for standard solution. The concentration level is captured in the identifier. +* `type`: data type for calculation. +* `pct`: whether converting ratio data into percentage (*100). +* `colanalyte`: column name of analytes. +* `colstats`: column name of statistics. +* `collevel`: column name of level. """ -function StabilityData(tbl::Table; d0 = r"S.*_(.*)_.*", days = r"S.*_(.*)_(.*)_(.*)_.*", order = "TDL", type = "Accuracy") - cols = propertynames(tbl) - df = @p tbl filter(==(type, getproperty(_, cols[2]))) - d0_tbl = @p df filter(occursin(d0, getproperty(_, cols[1]))) Table - level = cols[1] - getproperty(d0_tbl, level) .= getindex.(match.(d0, getproperty(d0_tbl, level)), 1) - stability_tbl = @p df filter(occursin(days, getproperty(_, cols[1]))) Table - stability_tbl = Table(stability_tbl; (Symbol.(split(order, "")) .=> map(1:length(order)) do i - getindex.(match.(days, getproperty(stability_tbl, level)), i) - end - )...) - stability_tbl = Table(stability_tbl; D = (@p stability_tbl.D map(parse(Int, replace(_, "D" => ""))))) - stability_tbl = sort(stability_tbl, :D) - ls = @p stability_tbl.L unique - ndays = @p stability_tbl.D unique - pushfirst!(ndays, 0) - d0_gtbl = @p d0_tbl filter(in(getproperty(_, level), ls)) group(getproperty(level)) map((columns ∘ getproperties)(_, cols[3:end])) - gtbl = @p stability_tbl group(getproperty(:T)) map(group(getproperty(:L), _)) map(map(x -> group(getproperty(:D), x), _)) map((fmap ∘ fmap)(getproperties(propertynames(stability_tbl)[3:end - 3]))) - d0_accuracy = @p d0_gtbl map(fmap(mean)) - d0_std = @p d0_gtbl map(fmap(std)) - accuracy = @p gtbl map(fmap((x -> Table((Days = ndays, ), x)) ∘ vcat_fmap2_table_skip1(mean))(d0_accuracy , _)) - stds = @p gtbl map(fmap((x -> Table((Days = ndays, ), x)) ∘ vcat_fmap2_table_skip1(std))(d0_std , _)) - StabilityData(accuracy, stds) +function me_report(at::AnalysisTable; + matrix = r"Post.*_(.*)_.*", + stds = r"STD.*_(.*)_.*", + type = :area, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + collevel = :Level + ) + df = ratio_data(at; type, pre = matrix, post = stds, pct, colanalyte, colstats, collevel) + replace!(getproperty(df, colstats), pct ? "Ratio(%)" => "Matrix Effect(%)" : "Ratio" => "Matrix Effect") + df end """ - Report(::Data) + recovery_report(at::AnalysisTable; + pre = r"Pre.*_(.*)_.*", + post = r"Post.*_(.*)_.*", + type = :area, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + collevel = :Level + ) -Create `Report` from `Data` +Compute recovery. + +# Arguments +* `at`: `AnalysisTable`. +* `pre`: `Regex` identifier for prespiked samples. The concentration level is captured in the identifier. +* `post`: `Regex` identifier for postspiked samples. The concentration level is captured in the identifier. +* `type`: data type for calculation. +* `pct`: whether converting ratio data into percentage (*100). +* `colanalyte`: column name of analytes. +* `colstats`: column name of statistics. +* `collevel`: column name of level. """ -function Report(data::StabilityData) - nt = map((accuracy = data.accuracy, std = data.std)) do dt - mapreduce(vcat, pairs(dt)) do (temp, tbl) - levels = @p tbl keys collect - new = Pair{Symbol, Any}[:Days => getindex(tbl, levels[1]).Days] - drugs = @p getindex(tbl, levels[1]) propertynames collect - popfirst!(drugs) - for drug in drugs - for level in levels - push!(new, Symbol(string(drug) * "_" * level) => getproperty(getindex(tbl, level), drug)) - end - end - Table((Temp = repeat([temp], length(new[1][2])), ); new...) - end - end - cols = @p nt.accuracy propertynames collect - new = Pair{Symbol, Any}[:Temp => nt.accuracy.Temp, :Days => nt.accuracy.Days] - cols = cols[3:end] - for col in cols - push!(new, Symbol(string(col) * "_" * "acc") => getproperty(nt.accuracy, col)) - push!(new, Symbol(string(col) * "_" * "std") => getproperty(nt.std, col)) - end - Report(data, Table(; new...)) +function recovery_report(at::AnalysisTable; + pre = r"Pre.*_(.*)_.*", + post = r"Post.*_(.*)_.*", + type = :area, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + collevel = :Level + ) + df = ratio_data(at; type, pre, post, pct, colanalyte, colstats, collevel) + replace!(getproperty(df, colstats), pct ? "Ratio(%)" => "Recovery(%)" : "Ratio" => "Recovery") + df end -function Report(data::APData) - levels = @p data.summary keys collect - ndays = size(getindex(data.daily.accuracy, levels[1]), 1) - stats = vcat([["Accuracy($i)", "standard deviation($i)", "RSD($i)"] for i in 1:ndays]..., getindex(data.summary, levels[1]).Stats) - new = Pair{Symbol, Any}[:Stats => stats] - drugs = @p getindex(data.summary, levels[1]) propertynames collect - popfirst!(drugs) - for drug in drugs - for level in levels - daily_acc = getproperty(data.daily.accuracy[level], drug) - daily_std = getproperty(data.daily.std[level], drug) - daily_rsd = getproperty(data.daily.rsd[level], drug) - stat = vcat([[daily_acc[i], daily_std[i], daily_rsd[i]] for i in 1:ndays]..., getproperty(getindex(data.summary, level), drug)) - push!(new, Symbol(string(drug) * "_" * level) => stat) - end +function ratio_data(at::AnalysisTable; + pre = r"Pre.*_(.*)_.*", + post = r"Post.*_(.*)_.*", + type = :estimated_concentration, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + collevel = :Level + ) + colanalyte = Symbol(colanalyte) + colstats = Symbol(colstats) + collevel = Symbol(collevel) + col = ["Ratio", "Standard Deviation"] + if pct + ratio_ = ratio_pct + col .*= "(%)" + else + ratio_ = / end - Report(data, Table(; new...)) -end - -Report(data::RecoveryData) = Report(data, ratio_report(data.data)) -Report(data::MEData) = Report(data, ratio_report(data.data)) - -function ratio_report(data::TypedTables.Dictionary) - levels = @p data keys collect - new = Pair{Symbol, Any}[:Stats => getindex(data, levels[1]).Stats] - drugs = @p getindex(data, levels[1]) propertynames collect - popfirst!(drugs) - for drug in drugs - for level in levels - push!(new, Symbol(string(drug) * "_" * level) => getproperty(getindex(data, level), drug)) - end + dt = getproperty(at, Symbol(type)) + df = CQA.table(dt) + if !isa(df, DataFrame) + df = DataFrame(df) + end + analytes = analytename(at) + level = samplecol(dt) + df = @chain df begin + stack(analytes, level; variable_name = colanalyte, value_name = :Data) + rename!(Dict(level => collevel)) + end + pre_df = filter(collevel => Base.Fix1(occursin, pre), df) + post_df = filter(collevel => Base.Fix1(occursin, post), df) + pre_df[:, collevel] = getindex.(match.(pre, pre_df[!, collevel]), 1) + post_df[:, collevel] = getindex.(match.(post, post_df[!, collevel]), 1) + sort!(pre_df, collevel) + sort!(post_df, collevel) + pre_gdf = groupby(pre_df, [colanalyte, collevel]) + post_gdf = groupby(post_df, [colanalyte, collevel]) + ratio = combine(pre_gdf, :Data => mean; renamecols = false) + stds = combine(pre_gdf, :Data => rsd; renamecols = false) + post_ratio = combine(post_gdf, :Data => mean; renamecols = false) + post_rsd = combine(post_gdf, :Data => rsd; renamecols = false) + ratio[:, :Data] = @. ratio_(ratio[!, :Data], post_ratio[!, :Data]) + stds[:, :Data] = @. f_rsd_sum_std(stds[!, :Data], post_rsd[!, :Data], ratio[!, :Data]) + insertcols!(ratio, colstats => col[1]) + insertcols!(stds, colstats => col[2]) + @chain vcat(ratio, stds) begin + select!([colanalyte, collevel, colstats, :Data]) + sort!([colanalyte, collevel]) end - Table(; new...) end +""" + stability_report(at::AnalysisTable; + d0 = r"S.*_(.*)_.*", + id = r"S.*_(.*)_(.*)_(.*)_.*", + order = "CDL", + type = :accuracy, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + colcondition = :Condition, + colday = :Day, + collevel = :Level + ) +Compute stability. A `NamedTuple` is returned with two elements: `day0` is a `DataFrame` conataing day0 data, and `result` is a `DataFrame` conataing data of other days. + +# Arguments +* `at`: `AnalysisTable`. +* `d0`: `Regex` identifier for day0 samples. The concentration level is captured in the identifier. +* `id`: `Regex` identifier for the stability samples. The storage condition, concentration level, and storage days are captured in the identifier; the order can be set by `order`. +* `order`: a string for setting the order of captured values from `id`; C is storage condition; D is storage days; L is concentration level +* `type`: data type for calculation. +* `pct`: whether converting ratio data into percentage (*100). +* `colanalyte`: column name of analytes. +* `colstats`: column name of statistics. +* `colcondition`: column name of storage condition. +* `colday`: column name of validation day. +* `collevel`: column name of level. +""" +function stability_report(at::AnalysisTable; + d0 = r"S.*_(.*)_.*", + id = r"S.*_(.*)_(.*)_(.*)_.*", + order = "CDL", + type = :accuracy, + pct = true, + colanalyte = :Analyte, + colstats = :Stats, + colcondition = :Condition, + colday = :Day, + collevel = :Level + ) + colanalyte = Symbol(colanalyte) + colstats = Symbol(colstats) + colcondition = Symbol(colcondition) + colday = Symbol(colday) + collevel = Symbol(collevel) + col = ["Accuracy", "Standard Deviation"] + if pct + col .*= "(%)" + end + dt = getproperty(at, Symbol(type)) + df = CQA.table(dt) + if !isa(df, DataFrame) + df = DataFrame(df) + end + analytes = analytename(at) + level = samplecol(dt) + stability_df = filter(level => Base.Fix1(occursin, d0), df) + d0_data = Dict(:__C => "", :__D => 0, :__L => getindex.(match.(d0, getproperty(stability_df, level)), 1)) + insertcols!(stability_df, 1, map(Symbol.("__", split(order, ""))) do p + p => d0_data[p] + end...) + rename!(stability_df, :__C => colcondition, :__D => colday, :__L => collevel) + select!(stability_df, Not([level])) + ulevel = unique(getproperty(stability_df, collevel)) + stability_df2 = filter(level => Base.Fix1(occursin, id), df) + insertcols!(stability_df2, 1, (Symbol.("__", split(order, "")) .=> map(1:length(order)) do i + getindex.(match.(id, stability_df2[!, level]), i) + end + )...) + rename!(stability_df2, :__C => colcondition, :__D => colday, :__L => collevel) + select!(stability_df2, Not([level])) + stability_df2[!, colday] .= @p getproperty(stability_df2, colday) map(parse(Int, _)) + intersect!(ulevel, unique(getproperty(stability_df2, collevel))) + stability_df = @chain stability_df begin + append!(stability_df2) + filter!(collevel => in(ulevel), _) + sort!(colday) + stack(analytes, [colcondition, colday, collevel]; variable_name = colanalyte, value_name = :Data) + end + pct && (stability_df.Data .*= 100) + gdf = groupby(stability_df, [colanalyte, colcondition, colday, collevel]) + accuracy = combine(gdf, :Data => mean, renamecols = false) + stds = combine(gdf, :Data => std, renamecols = false) + insertcols!(accuracy, colstats => col[1]) + insertcols!(stds, colstats => col[2]) + result = @chain vcat(accuracy, stds) begin + select!([colanalyte, colcondition, colday, collevel, colstats, :Data]) + sort!([colanalyte, colcondition, colday, collevel]) + end + day0 = filter(colday => ==(0), result) + filter!(colday => >(0), result) + (; day0, result) +end \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl index 332e40f..10dab8f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,21 +1,329 @@ -rsd(v) = std(v) / mean(v) * 100 +rsd(v) = std(v) / mean(v) +rsd_pct(v) = 100 * rsd(v) apply(fs, x...) = [f(x...) for f in fs] -vcat_fmap2_table_skip1(f) = (x, y) -> vcat([x], (collect ∘ fmap(fmap(f) ∘ columns))(y)) +apply(f::Function, x...) = f(x...) -#islessD(x, y) = isless(parse(Int, replace(x, "D" => "")), parse(Int, replace(y, "D" => ""))) -function sort(tbl::Table, sym::Symbol; kwargs...) - ord = sortperm(getproperty(tbl, sym); kwargs...) - Table(; (propertynames(tbl) .=> getindex.(collect(columns(tbl)), Ref(ord)))...) +f_var_inter(var_bet, var_intra, inv_n) = max(var_bet - var_intra * inv_n, 0) +f_var_sum_rsd(var1, var2, means) = sqrt(var1 + var2) / means +f_var_sum_rsd_pct(var1, var2, means) = f_var_sum_rsd(var1, var2, means) * 100 +ratio_pct(x, y) = x / y * 100 +f_rsd_sum_std(x, y, m) = sqrt(x ^ 2 + y ^ 2) * m +f_rsd_sum_std_pct(x, y, m) = f_rsd_sum_std(x, y, m) * 100 + +""" + pivot(df::DataFrame, col; rows = [], prefix = true, notsort = ["Stats", "File"], drop = []) + pivot(df::DataFrame, cols::AbstractVector; rows = [], prefix = true, notsort = ["Stats", "File"], drop = []) + +Transform `DataFrame` into wide format. + +# Arguments +* `df`: target `DataFrame`. +* `col`: the column (Symbol or String) holding the column names in wide format. +* `cols`: the column(s) (Vector) holding the column names in wide format. + +# Keyword Arguments +* `rows`: the column(s) (Symbol, String, or Vector) preserving as row keys in wide format. +* `prefix`: whether preserving `col` or `cols` in column names. +* `notsort`: columns (Vector); do not sort by these columns. +* `drop`: columns (Vector); drop these columns. +""" +function pivot(df::DataFrame, cols; rows = [], prefix = true, notsort = ["Stats", "File"], drop = []) + df = _pivot(df, cols; rows, prefix, drop) + ord = filter(!startswith("Data"), names(df)) + notsort = string.(notsort) + filter!(x -> !in(x, notsort), ord) + isempty(ord) ? df : sort!(df, ord) +end +function _pivot(df::DataFrame, col; rows = [], prefix = true, drop = []) + cols = names(df) + value_id = findall(startswith("Data"), cols) + row_id = setdiff(eachindex(cols), value_id) + col_id = findfirst(==(String(col)), cols) + setdiff!(row_id, col_id) + dfs = prefix ? [unstack(df, row_id, col_id, v, renamecols = x -> Symbol(cols[v], :|, cols[col_id], :(=), x)) for v in value_id] : + [unstack(df, row_id, col_id, v, renamecols = x -> Symbol(cols[v], :|, x)) for v in value_id] + df = length(dfs) == 1 ? only(dfs) : outerjoin(dfs...; on = cols[row_id]) + select!(df, rows, Not(drop)) end -reducer(f) = (x...) -> reduce(f, x) -applyer(f) = (x...) -> f(x) -fmap(f) = (x...) -> map(f, x...) +function _pivot(df::DataFrame, cols::AbstractVector; rows = [], prefix = true, drop = []) + length(cols) == 1 && return _pivot(df, only(cols); rows, prefix) + dfs = [df] + row_id = Int[] + colss = names(df) + for col in cols + df = first(dfs) + colss = names(df) + value_id = findall(startswith("Data"), colss) + row_id = setdiff(eachindex(colss), value_id) + col_id = findfirst(==(String(col)), colss) + setdiff!(row_id, col_id) + dfs = mapreduce(append!, dfs) do df + colss = names(df) + prefix ? [unstack(df, row_id, col_id, v, renamecols = x -> Symbol(colss[v], :|, colss[col_id], :(=), x)) for v in value_id] : + [unstack(df, row_id, col_id, v, renamecols = x -> Symbol(colss[v], :|, x)) for v in value_id] + end + end + df = length(dfs) == 1 ? only(dfs) : outerjoin(dfs...; on = colss[row_id]) + select!(df, rows, Not(drop)) +end -f_var_inter(var_bet, var_intra, inv_n) = max(var_bet - var_intra * inv_n, 0) -f_std(vars) = sqrt(vars) -f_rsd(vars, means) = sqrt(vars) / means * 100 -f_rsd(var1, var2, means) = sqrt(var1 + var2) / means * 100 -std_sum(x, y) = sqrt(x^2 + y^2) -pct_ratio(x, y) = x / y * 100 \ No newline at end of file +""" + unpivot(df::DataFrame, col; rows = [], notsort = ["Stats", "File"], drop = []) + unpivot(df::DataFrame, cols::AbstractVector; rows = [], notsort = ["Stats", "File"], drop = []) + +Transform `DataFrame` into wide format. + +# Arguments +* `df`: target `DataFrame`. +* `col`: the column name (Symbol or String) in long format. +* `cols`: the column(s) (Vector) in long format. + +# Keyword Arguments +* `rows`: the column(s) (Symbol, String, or Vector) preserving as row keys in long format. +* `notsort`: columns (Vector); do not sort by these columns. +* `drop`: columns (Vector); drop these columns. +""" +function unpivot(df::DataFrame, col; rows = [], notsort = ["Stats", "File"], drop = []) + df = _unpivot(df, col) + select!(df, rows, Not(drop)) + ord = filter(!startswith("Data"), names(df)) + notsort = string.(notsort) + filter!(x -> !in(x, notsort), ord) + isempty(ord) ? df : sort!(df, ord) +end + +function unpivot(df::DataFrame, cols::AbstractVector; rows = [], notsort = ["Stats", "File"], drop = []) + for col in cols + df = _unpivot(df, col) + end + select!(df, rows, Not(drop)) + ord = filter(!startswith("Data"), names(df)) + notsort = string.(notsort) + filter!(x -> !in(x, notsort), ord) + isempty(ord) ? df : sort!(df, ord) +end + +function _unpivot(df::DataFrame, col) + col = string(col) + cols = names(df) + value_id = findall(startswith("Data"), cols) + row_id = setdiff(eachindex(cols), value_id) + valuecols = map(value_id) do i + replace(cols[i], Regex("\\|" * col * "=[^\\|]*") => "") + end + newcols = map(value_id) do i + only(match(Regex("\\|" * col * "=([^\\|]*)"), cols[i])) + end + colmap = Dictionary{String, Vector{Int}}() + for (i, v) in enumerate(valuecols) + push!(get!(colmap, v, Int[]), i) + end + dfs = [stack(select(df, row_id, value_id[v] .=> identity .=> newcols[v]), newcols[v]; variable_name = col, value_name = k) for (k, v) in pairs(colmap)] + df = length(dfs) == 1 ? only(dfs) : outerjoin(dfs...; on = vcat(cols[row_id], col)) +end + +""" + mean_plus_minus_std(m, s; digits = 2) + +Round and merge mean values and standard deviations with "±". + +# Arguments +* `m`: mean values. +* `s`: standard deviations. +* `digits`: rounds to the specified number of digits after the decimal place. +""" +mean_plus_minus_std(m, s; digits = 2) = @. string(round(m; digits), "±", round(s; digits)) + +""" + add_percentage(s; ispct = false, digits = 2) + +Add "%" to `s`. + +# Arguments +* `s`: numbers. +* `ispct`: whether the orinal data is percentage; if not, the value will time 100. +* `digits`: rounds to the specified number of digits after the decimal place. +""" +add_percentage(s; ispct = false, digits = 2) = ispct ? (@. string(round(s; digits), "%")) : (@. string(round(s * 100; digits), "%")) + +""" + selectby(df::DataFrame, col, col_pairs...; + pivot = false, + rows = [], + notsort = ["Stats", "File"], + prefix = true, + drop = [], + kwargs...) + +Select values by `col`, and apply `select!` as if the values are columns. + +# Arguments +* `df`: target `DataFrame`. +* `col`: column name. +* `col_pairs`: `DataFrames.jl` syntax to manipulate columns. They will be put in internal `select!` function. + +# Keyword Arguments +* `rows`: the column(s) (Symbol, String, or Vector) preserving as row keys. +* `pivot`: whether pivot the dataframe by `col`. +* `notsort`: columns (Vector); do not sort by these columns. +* `drop`: columns (Vector); drop these columns. +* `kwargs`: keyword arguments for internal `select!` function. +""" +function selectby(df::DataFrame, col, col_pairs...; + pivot = nothing, + rows = [], + notsort = ["Stats", "File"], + prefix = true, + drop = [], + kwargs...) + col = string(col) + cols = names(df) + col_id = findfirst(==(col), cols) + dfs = if isnothing(col_id) + pivot = isnothing(pivot) ? true : pivot + value_id = findall(startswith("Data"), cols) + row_id = setdiff(eachindex(cols), value_id) + valuecols = map(value_id) do i + replace(cols[i], Regex("\\|" * col * "=[^\\|]*") => "") + end + newcols = map(value_id) do i + only(match(Regex("\\|" * col * "=([^\\|]*)"), cols[i])) + end + colmap = Dictionary{String, Vector{Int}}() + for (i, v) in enumerate(valuecols) + push!(get!(colmap, v, Int[]), i) + end + map(pairs(colmap)) do (k, v) + _select_pivot_unpivot!(select(df, row_id, value_id[v] .=> identity .=> newcols[v]), row_id, cols[row_id], col, k, col_pairs...; pivot, prefix, kwargs...) + end + else + pivot = isnothing(pivot) ? false : pivot + value_ids = findall(startswith("Data"), cols) + row_id = setdiff(eachindex(cols), value_ids) + setdiff!(row_id, col_id) + map(value_ids) do value_id + _select_pivot_unpivot!(unstack(df, row_id, col_id, value_id), row_id, cols[row_id], col, cols[value_id], col_pairs...; pivot, prefix, kwargs...) + end + end + df = if length(dfs) == 1 + only(dfs) + else + cols = names(last(dfs)) + value_ids = findall(startswith("Data"), cols) + row_id = setdiff(eachindex(cols), value_ids) + col_id = findfirst(==(colstats), cols) + setdiff!(row_id, col_id) + outerjoin(dfs...; on = cols[row_id]) + end + select!(df, rows, Not(drop)) + ord = filter(!startswith("Data"), names(df)) + notsort = string.(notsort) + filter!(x -> !in(x, notsort), ord) + isempty(ord) ? df : sort!(df, ord) +end + +function _select_pivot_unpivot!(df, row_id, row_cols, col, value_name, col_pairs...; pivot = false, prefix = true, kwargs...) + select!(df, row_id, col_pairs...; kwargs...) + cols = names(df) + row_id = findall(in(row_cols), cols) + col_id = setdiff(eachindex(cols), row_id) + if pivot && prefix + rename!(df, cols[col_id] .=> string(value_name, "|", col, "=") .* cols[col_id]) + elseif pivot + rename!(df, cols[col_id] .=> string(value_name, "|") .* cols[col_id]) + else + stack(df, col_id, row_id; variable_name = col, value_name = value_name) + end +end + +""" + normalize(df::DataFrame, normalizer::DataFrame; id = [:Analyte, :L], stats = (All(), "Accuracy"), colstats = :Stats) + +Normalize `DataFrame` by the given normalizer. + +# Arguments +* `normalizer`: the `DataFrame` to normalize `df`. +* `df`: the `DataFrame` to be normalized. +* `id`: the column(s) (Symbol, string or integer) with a unique key for each row. +* `stats`: a `Tuple` represented as statistics involved in normalization. The first argument applies to `df`, and the second applies to `normalizer`. `All()` indicates including all statistics. +* `colstats`: column name of statistics. +""" +function normalize(df::DataFrame, normalizer::DataFrame; id = [:Analyte, :Level], stats = (All(), "Accuracy"), colstats = :Stats) + normalizer = filter(colstats => ==(stats[1]), normalizer) + df = deepcopy(df) + ngdf = groupby(normalizer, id) + tgdf = groupby(df, id) + if stats[1] isa All + stats = (unique(getproperty(df, colstats)), stats[2]) + end + for (i, j) in zip(eachindex(ngdf), eachindex(tgdf)) + tgdf[j].Data[in.(getproperty(tgdf[j], colstats), Ref(stats[1]))] ./= ngdf[i].Data + end + df +end + +""" + qualify(df::DataFrame; kwargs...) + +Replace data out of acceptable range. See `qualify!` for details. +""" +qualify(df::DataFrame; kwargs...) = qualify!(deepcopy(df); kwargs...) + +""" + qualify!(df::DataFrame; + lod = nothing, + loq = nothing, + lloq = nothing, + uloq = nothing, + lodsub = "][p] + cols = filter!(startswith("Data"), names(df)) + bk = zeros(length(cols)) + fb = [identity for i in cols] + th = mapreduce(hcat, th) do t + bk .+ t + end + sub = mapreduce(hcat, sub) do s + apply.(fb, s) + end + for (col, ts, ss) in zip(cols, eachrow(th), eachrow(sub)) + df[!, col] = map(df[!, col]) do x + for (t, s, c) in zip(ts, ss, comp) + c(x, t) && return s + end + x + end + end + df +end \ No newline at end of file diff --git a/test/Manifest.toml b/test/Manifest.toml new file mode 100644 index 0000000..0c4368e --- /dev/null +++ b/test/Manifest.toml @@ -0,0 +1,746 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.10.2" +manifest_format = "2.0" +project_hash = "4fc291937c44f592e8028bc65ef130f3f12f6ebf" + +[[deps.Accessors]] +deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "LinearAlgebra", "MacroTools", "Markdown", "Test"] +git-tree-sha1 = "c0d491ef0b135fd7d63cbc6404286bc633329425" +uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" +version = "0.1.36" + + [deps.Accessors.extensions] + AccessorsAxisKeysExt = "AxisKeys" + AccessorsIntervalSetsExt = "IntervalSets" + AccessorsStaticArraysExt = "StaticArrays" + AccessorsStructArraysExt = "StructArrays" + AccessorsUnitfulExt = "Unitful" + + [deps.Accessors.weakdeps] + AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + Requires = "ae029012-a4dd-5104-9daa-d747884805df" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[[deps.Adapt]] +deps = ["LinearAlgebra", "Requires"] +git-tree-sha1 = "6a55b747d1812e699320963ffde36f1ebdda4099" +uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +version = "4.0.4" + + [deps.Adapt.extensions] + AdaptStaticArraysExt = "StaticArrays" + + [deps.Adapt.weakdeps] + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[[deps.ArgCheck]] +git-tree-sha1 = "a3a402a35a2f7e0b87828ccabbd5ebfbebe356b4" +uuid = "dce04be8-c92d-5529-be00-80e4d2c0e197" +version = "2.3.0" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.1" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + +[[deps.BangBang]] +deps = ["Accessors", "Compat", "ConstructionBase", "InitialValues", "LinearAlgebra", "Requires"] +git-tree-sha1 = "490e739172eb18f762e68dc3b928cad2a077983a" +uuid = "198e06fe-97b7-11e9-32a5-e1d131e6ad66" +version = "0.4.1" + + [deps.BangBang.extensions] + BangBangChainRulesCoreExt = "ChainRulesCore" + BangBangDataFramesExt = "DataFrames" + BangBangStaticArraysExt = "StaticArrays" + BangBangStructArraysExt = "StructArrays" + BangBangTablesExt = "Tables" + BangBangTypedTablesExt = "TypedTables" + + [deps.BangBang.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + TypedTables = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[deps.Baselet]] +git-tree-sha1 = "aebf55e6d7795e02ca500a689d326ac979aaf89e" +uuid = "9718e550-a3fa-408a-8086-8db961cd8217" +version = "0.1.1" + +[[deps.CSV]] +deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] +git-tree-sha1 = "6c834533dc1fabd820c1db03c839bf97e45a3fab" +uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +version = "0.10.14" + +[[deps.Calculus]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad" +uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +version = "0.5.1" + +[[deps.Chain]] +git-tree-sha1 = "9ae9be75ad8ad9d26395bf625dea9beac6d519f1" +uuid = "8be319e6-bccf-4806-a6f7-6fae938471bc" +version = "0.6.0" + +[[deps.ChemistryQuantitativeAnalysis]] +deps = ["CSV", "Dictionaries", "GLM", "LinearAlgebra", "Pkg", "Tables", "ThreadsX", "TypedTables"] +git-tree-sha1 = "ced2c838aea3f156e5812c23697c0ac1e8253f5b" +uuid = "3ed48883-6809-43ec-b77a-d23b0650d8c4" +version = "0.7.1" + +[[deps.CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "59939d8a997469ee05c4b4944560a820f9ba0d73" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.4" + +[[deps.Compat]] +deps = ["TOML", "UUIDs"] +git-tree-sha1 = "c955881e3c981181362ae4088b35995446298b80" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "4.14.0" +weakdeps = ["Dates", "LinearAlgebra"] + + [deps.Compat.extensions] + CompatLinearAlgebraExt = "LinearAlgebra" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.1.0+0" + +[[deps.CompositionsBase]] +git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" +uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b" +version = "0.1.2" +weakdeps = ["InverseFunctions"] + + [deps.CompositionsBase.extensions] + CompositionsBaseInverseFunctionsExt = "InverseFunctions" + +[[deps.ConstructionBase]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "260fd2400ed2dab602a7c15cf10c1933c59930a2" +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.5.5" + + [deps.ConstructionBase.extensions] + ConstructionBaseIntervalSetsExt = "IntervalSets" + ConstructionBaseStaticArraysExt = "StaticArrays" + + [deps.ConstructionBase.weakdeps] + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[[deps.Crayons]] +git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" +uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" +version = "4.1.1" + +[[deps.DataAPI]] +git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" +uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +version = "1.16.0" + +[[deps.DataFrames]] +deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "REPL", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] +git-tree-sha1 = "04c738083f29f86e62c8afc341f0967d8717bdb8" +uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +version = "1.6.1" + +[[deps.DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "97d79461925cdb635ee32116978fc735b9463a39" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.19" + +[[deps.DataValueInterfaces]] +git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" +uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" +version = "1.0.0" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[deps.DefineSingletons]] +git-tree-sha1 = "0fba8b706d0178b4dc7fd44a96a92382c9065c2c" +uuid = "244e2a9f-e319-4986-a169-4d1fe445cd52" +version = "0.1.2" + +[[deps.Dictionaries]] +deps = ["Indexing", "Random", "Serialization"] +git-tree-sha1 = "573c92ef22ee0783bfaa4007c732b044c791bc6d" +uuid = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" +version = "0.4.1" + +[[deps.Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[deps.Distributions]] +deps = ["FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] +git-tree-sha1 = "7c302d7a5fec5214eb8a5a4c466dcf7a51fcf169" +uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" +version = "0.25.107" + + [deps.Distributions.extensions] + DistributionsChainRulesCoreExt = "ChainRulesCore" + DistributionsDensityInterfaceExt = "DensityInterface" + DistributionsTestExt = "Test" + + [deps.Distributions.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[deps.DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.9.3" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.6.0" + +[[deps.DualNumbers]] +deps = ["Calculus", "NaNMath", "SpecialFunctions"] +git-tree-sha1 = "5837a837389fccf076445fce071c8ddaea35a566" +uuid = "fa6b7ba4-c1ee-5f82-b5fc-ecf0adba8f74" +version = "0.6.8" + +[[deps.FilePathsBase]] +deps = ["Compat", "Dates", "Mmap", "Printf", "Test", "UUIDs"] +git-tree-sha1 = "9f00e42f8d99fdde64d40c8ea5d14269a2e2c1aa" +uuid = "48062228-2e41-5def-b9a4-89aafe57970f" +version = "0.9.21" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[deps.FillArrays]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "bfe82a708416cf00b73a3198db0859c82f741558" +uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" +version = "1.10.0" +weakdeps = ["PDMats", "SparseArrays", "Statistics"] + + [deps.FillArrays.extensions] + FillArraysPDMatsExt = "PDMats" + FillArraysSparseArraysExt = "SparseArrays" + FillArraysStatisticsExt = "Statistics" + +[[deps.Future]] +deps = ["Random"] +uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" + +[[deps.GLM]] +deps = ["Distributions", "LinearAlgebra", "Printf", "Reexport", "SparseArrays", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns", "StatsModels"] +git-tree-sha1 = "273bd1cd30768a2fddfa3fd63bbc746ed7249e5f" +uuid = "38e38edf-8417-5370-95a0-9cbb8c7f171a" +version = "1.9.0" + +[[deps.HypergeometricFunctions]] +deps = ["DualNumbers", "LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] +git-tree-sha1 = "f218fe3736ddf977e0e772bc9a586b2383da2685" +uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" +version = "0.3.23" + +[[deps.Indexing]] +git-tree-sha1 = "ce1566720fd6b19ff3411404d4b977acd4814f9f" +uuid = "313cdc1a-70c2-5d6a-ae34-0150d3930a38" +version = "1.1.1" + +[[deps.InitialValues]] +git-tree-sha1 = "4da0f88e9a39111c2fa3add390ab15f3a44f3ca3" +uuid = "22cec73e-a1b8-11e9-2c92-598750a2cf9c" +version = "0.3.1" + +[[deps.InlineStrings]] +deps = ["Parsers"] +git-tree-sha1 = "9cc2baf75c6d09f9da536ddf58eb2f29dedaf461" +uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" +version = "1.4.0" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[deps.InverseFunctions]] +deps = ["Test"] +git-tree-sha1 = "896385798a8d49a255c398bd49162062e4a4c435" +uuid = "3587e190-3f89-42d0-90ee-14403ec27112" +version = "0.1.13" +weakdeps = ["Dates"] + + [deps.InverseFunctions.extensions] + DatesExt = "Dates" + +[[deps.InvertedIndices]] +git-tree-sha1 = "0dc7b50b8d436461be01300fd8cd45aa0274b038" +uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" +version = "1.3.0" + +[[deps.IrrationalConstants]] +git-tree-sha1 = "630b497eafcc20001bba38a4651b327dcfc491d2" +uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" +version = "0.2.2" + +[[deps.IteratorInterfaceExtensions]] +git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" +uuid = "82899510-4779-5014-852e-03e436cf321d" +version = "1.0.0" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "7e5d6779a1e09a36db2a7b6cff50942a0a7d0fca" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.5.0" + +[[deps.LaTeXStrings]] +git-tree-sha1 = "50901ebc375ed41dbf8058da26f9de442febbbec" +uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +version = "1.3.1" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.4.0+0" + +[[deps.LibGit2]] +deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.6.4+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.0+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[deps.LogExpFunctions]] +deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] +git-tree-sha1 = "18144f3e9cbe9b15b070288eef858f71b291ce37" +uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" +version = "0.3.27" + + [deps.LogExpFunctions.extensions] + LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" + LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables" + LogExpFunctionsInverseFunctionsExt = "InverseFunctions" + + [deps.LogExpFunctions.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + ChangesOfVariables = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" + InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[deps.MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "2fa9ee3e63fd3a4f7a9a4f4744a52f4856de82df" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.13" + +[[deps.Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[deps.MbedTLS_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.28.2+1" + +[[deps.MicroCollections]] +deps = ["Accessors", "BangBang", "InitialValues"] +git-tree-sha1 = "44d32db644e84c75dab479f1bc15ee76a1a3618f" +uuid = "128add7d-3638-4c79-886c-908ea0c25c34" +version = "0.2.0" + +[[deps.Missings]] +deps = ["DataAPI"] +git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" +uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +version = "1.2.0" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2023.1.10" + +[[deps.NaNMath]] +deps = ["OpenLibm_jll"] +git-tree-sha1 = "0877504529a3e5c3343c6f8b4c0381e57e4387e4" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "1.0.2" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.2.0" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.23+4" + +[[deps.OpenLibm_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "05823500-19ac-5b8b-9628-191a04bc5112" +version = "0.8.1+2" + +[[deps.OpenSpecFun_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.5+0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "dfdf5519f235516220579f949664f1bf44e741c5" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.6.3" + +[[deps.PDMats]] +deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] +git-tree-sha1 = "949347156c25054de2db3b166c52ac4728cbad65" +uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" +version = "0.11.31" + +[[deps.Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.8.1" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.10.0" + +[[deps.PooledArrays]] +deps = ["DataAPI", "Future"] +git-tree-sha1 = "36d8b4b899628fb92c2749eb488d884a926614d3" +uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" +version = "1.4.3" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.2.1" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.4.3" + +[[deps.PrettyTables]] +deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "Reexport", "StringManipulation", "Tables"] +git-tree-sha1 = "88b895d13d53b5577fd53379d913b9ab9ac82660" +uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" +version = "2.3.1" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.QuadGK]] +deps = ["DataStructures", "LinearAlgebra"] +git-tree-sha1 = "9b23c31e76e333e6fb4c1595ae6afa74966a729e" +uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" +version = "2.9.4" + +[[deps.REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[deps.Random]] +deps = ["SHA"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + +[[deps.Referenceables]] +deps = ["Adapt"] +git-tree-sha1 = "02d31ad62838181c1a3a5fd23a1ce5914a643601" +uuid = "42d2dcc6-99eb-4e98-b66c-637b7d73030e" +version = "0.1.3" + +[[deps.Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.3.0" + +[[deps.Rmath]] +deps = ["Random", "Rmath_jll"] +git-tree-sha1 = "f65dcb5fa46aee0cf9ed6274ccbd597adc49aa7b" +uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" +version = "0.7.1" + +[[deps.Rmath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "6ed52fdd3382cf21947b15e8870ac0ddbff736da" +uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" +version = "0.4.0+0" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.SentinelArrays]] +deps = ["Dates", "Random"] +git-tree-sha1 = "0e7508ff27ba32f26cd459474ca2ede1bc10991f" +uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" +version = "1.4.1" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[deps.Setfield]] +deps = ["ConstructionBase", "Future", "MacroTools", "StaticArraysCore"] +git-tree-sha1 = "e2cc6d8c88613c05e1defb55170bf5ff211fbeac" +uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" +version = "1.1.1" + +[[deps.ShiftedArrays]] +git-tree-sha1 = "503688b59397b3307443af35cd953a13e8005c16" +uuid = "1277b4bf-5013-50f5-be3d-901d8477a67a" +version = "2.0.0" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[deps.SortingAlgorithms]] +deps = ["DataStructures"] +git-tree-sha1 = "66e0a8e672a0bdfca2c3f5937efb8538b9ddc085" +uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" +version = "1.2.1" + +[[deps.SparseArrays]] +deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +version = "1.10.0" + +[[deps.SpecialFunctions]] +deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] +git-tree-sha1 = "e2cfc4012a19088254b3950b85c3c1d8882d864d" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "2.3.1" + + [deps.SpecialFunctions.extensions] + SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" + + [deps.SpecialFunctions.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + +[[deps.SplitApplyCombine]] +deps = ["Dictionaries", "Indexing"] +git-tree-sha1 = "c06d695d51cfb2187e6848e98d6252df9101c588" +uuid = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" +version = "1.2.3" + +[[deps.SplittablesBase]] +deps = ["Setfield", "Test"] +git-tree-sha1 = "e08a62abc517eb79667d0a29dc08a3b589516bb5" +uuid = "171d559e-b47b-412a-8079-5efa626c420e" +version = "0.1.15" + +[[deps.StaticArraysCore]] +git-tree-sha1 = "36b3d696ce6366023a0ea192b4cd442268995a0d" +uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" +version = "1.4.2" + +[[deps.Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.10.0" + +[[deps.StatsAPI]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "1ff449ad350c9c4cbc756624d6f8a8c3ef56d3ed" +uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +version = "1.7.0" + +[[deps.StatsBase]] +deps = ["DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "5cf7606d6cef84b543b483848d4ae08ad9832b21" +uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +version = "0.34.3" + +[[deps.StatsFuns]] +deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] +git-tree-sha1 = "cef0472124fab0695b58ca35a77c6fb942fdab8a" +uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" +version = "1.3.1" + + [deps.StatsFuns.extensions] + StatsFunsChainRulesCoreExt = "ChainRulesCore" + StatsFunsInverseFunctionsExt = "InverseFunctions" + + [deps.StatsFuns.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" + +[[deps.StatsModels]] +deps = ["DataAPI", "DataStructures", "LinearAlgebra", "Printf", "REPL", "ShiftedArrays", "SparseArrays", "StatsAPI", "StatsBase", "StatsFuns", "Tables"] +git-tree-sha1 = "5cf6c4583533ee38639f73b880f35fc85f2941e0" +uuid = "3eaba693-59b7-5ba5-a881-562e759f1c8d" +version = "0.7.3" + +[[deps.StringManipulation]] +deps = ["PrecompileTools"] +git-tree-sha1 = "a04cabe79c5f01f4d723cc6704070ada0b9d46d5" +uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" +version = "0.3.4" + +[[deps.SuiteSparse]] +deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"] +uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" + +[[deps.SuiteSparse_jll]] +deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] +uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" +version = "7.2.1+1" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.TableTraits]] +deps = ["IteratorInterfaceExtensions"] +git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" +uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" +version = "1.0.1" + +[[deps.Tables]] +deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "OrderedCollections", "TableTraits"] +git-tree-sha1 = "cb76cf677714c095e535e3501ac7954732aeea2d" +uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +version = "1.11.1" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[deps.ThreadsX]] +deps = ["Accessors", "ArgCheck", "BangBang", "ConstructionBase", "InitialValues", "MicroCollections", "Referenceables", "SplittablesBase", "Transducers"] +git-tree-sha1 = "70bd8244f4834d46c3d68bd09e7792d8f571ef04" +uuid = "ac1d9e8a-700a-412c-b207-f0111f4b6c0d" +version = "0.1.12" + +[[deps.TranscodingStreams]] +git-tree-sha1 = "71509f04d045ec714c4748c785a59045c3736349" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.10.7" +weakdeps = ["Random", "Test"] + + [deps.TranscodingStreams.extensions] + TestExt = ["Test", "Random"] + +[[deps.Transducers]] +deps = ["Accessors", "Adapt", "ArgCheck", "BangBang", "Baselet", "CompositionsBase", "ConstructionBase", "DefineSingletons", "Distributed", "InitialValues", "Logging", "Markdown", "MicroCollections", "Requires", "SplittablesBase", "Tables"] +git-tree-sha1 = "47e516e2eabd0cf1304cd67839d9a85d52dd659d" +uuid = "28d57a85-8fef-5791-bfe6-a80928e7c999" +version = "0.4.81" + + [deps.Transducers.extensions] + TransducersBlockArraysExt = "BlockArrays" + TransducersDataFramesExt = "DataFrames" + TransducersLazyArraysExt = "LazyArrays" + TransducersOnlineStatsBaseExt = "OnlineStatsBase" + TransducersReferenceablesExt = "Referenceables" + + [deps.Transducers.weakdeps] + BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" + DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" + LazyArrays = "5078a376-72f3-5289-bfd5-ec5146d43c02" + OnlineStatsBase = "925886fa-5bf2-5e8e-b522-a9147a512338" + Referenceables = "42d2dcc6-99eb-4e98-b66c-637b7d73030e" + +[[deps.TypedTables]] +deps = ["Adapt", "Dictionaries", "Indexing", "SplitApplyCombine", "Tables", "Unicode"] +git-tree-sha1 = "84fd7dadde577e01eb4323b7e7b9cb51c62c60d4" +uuid = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9" +version = "1.4.6" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[deps.WeakRefStrings]] +deps = ["DataAPI", "InlineStrings", "Parsers"] +git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23" +uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" +version = "1.4.2" + +[[deps.WorkerUtilities]] +git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" +uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" +version = "1.6.1" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.13+1" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.8.0+1" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.52.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.4.0+2" diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..0392994 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,6 @@ +[deps] +Chain = "8be319e6-bccf-4806-a6f7-6fae938471bc" +ChemistryQuantitativeAnalysis = "3ed48883-6809-43ec-b77a-d23b0650d8c4" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/data/D1.csv b/test/data/D1.csv index 6fcd715..5118869 100644 --- a/test/data/D1.csv +++ b/test/data/D1.csv @@ -6,23 +6,23 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, !,PooledQC_1-r001.d,QC,112/4/19 15:58,,FALSE,6,1.3,74.7,2942451,260.1851,81.5,505438,270.1382,72.8,9950021,267.3499 !,PooledQC_1-r002.d,QC,112/4/19 16:05,,FALSE,6,1.3,88.6,2869754,270.865,84.5,506542,263.3587,76.7,8939474,260.0799 !,PooledQC_1-r003.d,QC,112/4/19 16:11,,FALSE,6,1.3,84.6,2777212,258.0662,83.9,434072,264.7634,83,9428951,268.0131 -,Cal1_1_01-r001.d,Cal,112/4/19 16:17,,FALSE,1,1,113.8,47581,5.2411,103,8374,4.8978,111.8,196644,5.2914 -,Cal1_1_01-r002.d,Cal,112/4/19 16:24,,FALSE,1,1,101.1,51554,5.2555,116.1,8723,5.4034,95.2,176142,5.1596 -,Cal1_1_01-r003.d,Cal,112/4/19 16:30,,FALSE,1,1,107.6,52732,5.1791,104.2,8879,5.4113,100.6,186207,5.0792 -,Cal1_1_01-r004.d,Cal,112/4/19 16:36,,FALSE,1,1,99.7,46202,4.9356,114.9,8272,5.2432,97.4,187500,5.2176 -,Cal1_1_01-r005.d,Cal,112/4/19 16:43,,FALSE,1,1,100.6,56874,5.1325,91.7,8584,4.8341,112.7,188926,5.185 -,Cal1_2_02-r001.d,Cal,112/4/19 16:49,,FALSE,2,1,82.9,108841,9.0873,94.1,17397,9.5148,86.2,354726,9.1202 -!,Cal1_2_02-r002.d,Cal,112/4/19 16:56,,FALSE,2,1,97.5,110738,9.046,81.1,15679,8.4076,91.3,331514,9.1264 -,Cal1_2_02-r003.d,Cal,112/4/19 17:02,,FALSE,2,1,88.9,98885,9.1913,83.2,15900,9.0173,100.5,331947,9.154 -,Cal1_3_05-r001.d,Cal,112/4/19 17:08,,FALSE,3,1,93.1,299738,23.7663,101.3,52804,23.8296,91.6,1024400,26.1587 -,Cal1_3_05-r002.d,Cal,112/4/19 17:15,,FALSE,3,1,93.9,306013,27.2157,91.7,52466,26.1756,98.4,1134634,24.1097 -,Cal1_3_05-r003.d,Cal,112/4/19 17:21,,FALSE,3,1,95,342786,26.9883,100.2,46749,26.2953,103.9,1189592,22.2283 -,Cal1_4_1-r001.d,Cal,112/4/19 17:27,,FALSE,4,1,97,590879,43.4935,98,94543,45.5004,94.1,2143721,41.0343 -,Cal1_4_1-r002.d,Cal,112/4/19 17:34,,FALSE,4,1,94.1,663043,48.0714,92.7,101242,49.3269,81.1,2044941,42.0745 -,Cal1_4_1-r003.d,Cal,112/4/19 17:40,,FALSE,4,1,97,587054,48.0058,87,100106,42.9881,78.4,1980359,40.1985 -,Cal1_5_2-r001.d,Cal,112/4/19 17:46,,FALSE,5,1,103.4,1399847,118.3681,103.2,246959,102.1661,98.8,4488502,96.7995 -,Cal1_5_2-r002.d,Cal,112/4/19 17:53,,FALSE,5,1,115.5,1511726,113.4829,103.9,253875,97.8707,108.3,4925858,100.2591 -,Cal1_5_2-r003.d,Cal,112/4/19 17:59,,FALSE,5,1,116.5,1577724,108.4983,115,232398,111.9959,97.9,5039822,111.8649 +,Cal1_1_0-1-r001.d,Cal,112/4/19 16:17,,FALSE,1,1,113.8,47581,5.2411,103,8374,4.8978,111.8,196644,5.2914 +,Cal1_1_0-1-r002.d,Cal,112/4/19 16:24,,FALSE,1,1,101.1,51554,5.2555,116.1,8723,5.4034,95.2,176142,5.1596 +,Cal1_1_0-1-r003.d,Cal,112/4/19 16:30,,FALSE,1,1,107.6,52732,5.1791,104.2,8879,5.4113,100.6,186207,5.0792 +,Cal1_1_0-1-r004.d,Cal,112/4/19 16:36,,FALSE,1,1,99.7,46202,4.9356,114.9,8272,5.2432,97.4,187500,5.2176 +,Cal1_1_0-1-r005.d,Cal,112/4/19 16:43,,FALSE,1,1,100.6,56874,5.1325,91.7,8584,4.8341,112.7,188926,5.185 +,Cal1_2_0-2-r001.d,Cal,112/4/19 16:49,,FALSE,2,1,82.9,108841,9.0873,94.1,17397,9.5148,86.2,354726,9.1202 +!,Cal1_2_0-2-r002.d,Cal,112/4/19 16:56,,FALSE,2,1,97.5,110738,9.046,81.1,15679,8.4076,91.3,331514,9.1264 +,Cal1_2_0-2-r003.d,Cal,112/4/19 17:02,,FALSE,2,1,88.9,98885,9.1913,83.2,15900,9.0173,100.5,331947,9.154 +,Cal1_3_0-5-r001.d,Cal,112/4/19 17:08,,FALSE,3,1,93.1,299738,23.7663,101.3,52804,23.8296,91.6,1024400,26.1587 +,Cal1_3_0-5-r002.d,Cal,112/4/19 17:15,,FALSE,3,1,93.9,306013,27.2157,91.7,52466,26.1756,98.4,1134634,24.1097 +,Cal1_3_0-5-r003.d,Cal,112/4/19 17:21,,FALSE,3,1,95,342786,26.9883,100.2,46749,26.2953,103.9,1189592,22.2283 +,Cal1_4_01-r001.d,Cal,112/4/19 17:27,,FALSE,4,1,97,590879,43.4935,98,94543,45.5004,94.1,2143721,41.0343 +,Cal1_4_01-r002.d,Cal,112/4/19 17:34,,FALSE,4,1,94.1,663043,48.0714,92.7,101242,49.3269,81.1,2044941,42.0745 +,Cal1_4_01-r003.d,Cal,112/4/19 17:40,,FALSE,4,1,97,587054,48.0058,87,100106,42.9881,78.4,1980359,40.1985 +,Cal1_05_02-r001.d,Cal,112/4/19 17:46,,FALSE,5,1,103.4,1399847,118.3681,103.2,246959,102.1661,98.8,4488502,96.7995 +,Cal1_05_02-r002.d,Cal,112/4/19 17:53,,FALSE,5,1,115.5,1511726,113.4829,103.9,253875,97.8707,108.3,4925858,100.2591 +,Cal1_05_02-r003.d,Cal,112/4/19 17:59,,FALSE,5,1,116.5,1577724,108.4983,115,232398,111.9959,97.9,5039822,111.8649 ,Cal1_6_5-r001.d,Cal,112/4/19 18:06,,FALSE,6,1,93,3308525,261.559,96.7,546875,234.1456,91.1,12108113,249.3414 ,Cal1_6_5-r002.d,Cal,112/4/19 18:12,,FALSE,6,1,98.8,2980722,241.4244,109.7,492156,254.8143,92.6,11797564,240.3896 ,Cal1_6_5-r003.d,Cal,112/4/19 18:18,,FALSE,6,1,100,3196610,212.1061,91.3,494828,260.1321,85.2,10995122,214.1201 @@ -37,23 +37,23 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, ,Cal1_9_20-r003.d,Cal,112/4/19 19:16,,FALSE,9,1,120.8,13141432,1175.4979,117.6,2340113,1101.9954,110.7,47930119,1066.4392 !,STDQC1_2.d,CC,112/4/19 19:28,,FALSE,6,1.3,146074.5,1635253,406435.5641,45562.8,240343,164788.6259,51622.5,5882611,167173.7743 !,PooledQC1_2.d,QC,112/4/19 19:35,,FALSE,6,1.3,82.6,2898526,257.068,88.9,424443,255.4324,72.3,9841836,255.9983 -,Pre1_01_1.d,QC,112/4/19 19:47,,FALSE,1,1,111.7,64725,5.6827,107.8,9542,5.3378,127.1,203959,5.905 -!,Pre1_01_2.d,QC,112/4/19 19:54,,FALSE,1,1,109.9,56971,5.7943,117.8,8117,5.4887,111.9,227463,6.1944 -!,Pre1_01_3.d,QC,112/4/19 20:00,,FALSE,1,1,111.4,60564,5.8717,114.7,9827,5.7351,121,225960,6.2515 -!,Pre1_01_4.d,QC,112/4/19 20:06,,FALSE,1,1,103.6,57028,5.6319,114.8,10223,6.0421,121.4,183655,5.9217 -!,Pre1_01_5.d,QC,112/4/19 20:13,,FALSE,1,1,125.3,65694,5.8631,118.5,8943,5.6767,112.5,223814,6.0746 -,Pre1_02_1.d,QC,112/4/19 20:19,,FALSE,2,1,119.3,119529,10.1279,108.7,19419,9.967,110.8,486178,10.2788 -,Pre1_02_2.d,QC,112/4/19 20:25,,FALSE,2,1,100.7,104961,9.7685,100.3,18457,9.7327,108.1,391266,9.912 -,Pre1_02_3.d,QC,112/4/19 20:32,,FALSE,2,1,103,127925,10.8957,108.1,17688,9.8061,116.3,384412,11.6285 -,Pre1_02_4.d,QC,112/4/19 20:38,,FALSE,2,1,105.4,124913,10.4366,110.3,18433,9.4291,106.2,460750,10.7226 -,Pre1_02_5.d,QC,112/4/19 20:44,,FALSE,2,1,101.7,128674,9.6702,111.4,19298,11.1425,114.8,419954,9.6819 +,Pre1_0-1_1.d,QC,112/4/19 19:47,,FALSE,1,1,86.009,64725,5.6827,107.8,9542,5.3378,127.1,203959,5.905 +!,Pre1_0-1_2.d,QC,112/4/19 19:54,,FALSE,1,1,84.623,56971,5.7943,117.8,8117,5.4887,111.9,227463,6.1944 +!,Pre1_0-1_3.d,QC,112/4/19 20:00,,FALSE,1,1,85.778,60564,5.8717,114.7,9827,5.7351,121,225960,6.2515 +!,Pre1_0-1_4.d,QC,112/4/19 20:06,,FALSE,1,1,79.772,57028,5.6319,114.8,10223,6.0421,121.4,183655,5.9217 +!,Pre1_0-1_5.d,QC,112/4/19 20:13,,FALSE,1,1,96.481,65694,5.8631,118.5,8943,5.6767,112.5,223814,6.0746 +,Pre1_0-2_1.d,QC,112/4/19 20:19,,FALSE,2,1,91.861,119529,10.1279,108.7,19419,9.967,110.8,486178,10.2788 +,Pre1_0-2_2.d,QC,112/4/19 20:25,,FALSE,2,1,77.539,104961,9.7685,100.3,18457,9.7327,108.1,391266,9.912 +,Pre1_0-2_3.d,QC,112/4/19 20:32,,FALSE,2,1,79.31,127925,10.8957,108.1,17688,9.8061,116.3,384412,11.6285 +,Pre1_0-2_4.d,QC,112/4/19 20:38,,FALSE,2,1,81.158,124913,10.4366,110.3,18433,9.4291,106.2,460750,10.7226 +,Pre1_0-2_5.d,QC,112/4/19 20:44,,FALSE,2,1,78.309,128674,9.6702,111.4,19298,11.1425,114.8,419954,9.6819 !,STDQC1_3.d,CC,112/4/19 20:57,,FALSE,6,1.3,155592,1666939,494496.0975,137805.2,256446,490830.8331,63248.9,6180853,176847.6923 !,PooledQC1_3.d,QC,112/4/19 21:03,,FALSE,6,1.3,84.7,2528635,277.3304,88.2,467445,241.2606,82.2,10495986,257.4329 -!,Pre1_2_1.d,QC,112/4/19 21:16,,FALSE,5,1,105.7,1568431,121.7126,115.3,261934,118.2864,105.4,4928757,111.3734 -!,Pre1_2_2.d,QC,112/4/19 21:23,,FALSE,5,1,127.8,1680413,130.7787,130.7,228989,127.6987,123.9,4818120,123.8758 -!,Pre1_2_3.d,QC,112/4/19 21:29,,FALSE,5,1,124.2,1628416,119.1527,132.2,252817,133.1594,103.2,5150929,108.1756 -!,Pre1_2_4.d,QC,112/4/19 21:35,,FALSE,5,1,124.8,1559704,130.7777,131.3,272689,113.2519,111.3,5542151,122.3173 -,Pre1_2_5.d,QC,112/4/19 21:42,,FALSE,5,1,99.9,1634011,106.9209,102.9,231136,110.9165,96.4,4605706,97.3768 +!,Pre1_02_1.d,QC,112/4/19 21:16,,FALSE,5,1,105.7,1568431,121.7126,115.3,261934,118.2864,105.4,4928757,111.3734 +!,Pre1_02_02.d,QC,112/4/19 21:23,,FALSE,5,1,127.8,1680413,130.7787,130.7,228989,127.6987,123.9,4818120,123.8758 +!,Pre1_02_3.d,QC,112/4/19 21:29,,FALSE,5,1,124.2,1628416,119.1527,132.2,252817,133.1594,103.2,5150929,108.1756 +!,Pre1_02_4.d,QC,112/4/19 21:35,,FALSE,5,1,124.8,1559704,130.7777,131.3,272689,113.2519,111.3,5542151,122.3173 +,Pre1_02_5.d,QC,112/4/19 21:42,,FALSE,5,1,99.9,1634011,106.9209,102.9,231136,110.9165,96.4,4605706,97.3768 !,Pre1_15_1.d,QC,112/4/19 21:48,,FALSE,8,1,108.3,9449235,805.4354,91.5,1522560,709.5458,111.2,35575674,785.1635 ,Pre1_15_2.d,QC,112/4/19 21:54,,FALSE,8,1,102.5,10348765,809.7129,102,1630984,709.2966,117.2,40137372,797.3282 ,Pre1_15_3.d,QC,112/4/19 22:01,,FALSE,8,1,101.1,10252595,734.9857,104.4,1440314,754.556,120,39381460,845.289 @@ -61,15 +61,15 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, ,Pre1_15_5.d,QC,112/4/19 22:13,,FALSE,8,1,91.7,9332232,735.0488,90.9,1411389,779.9373,113.8,35437925,751.2354 !,STDQC1_4.d,CC,112/4/19 22:26,,FALSE,6,1.3,100654.3,1628635,339762.631,1130037.3,223471,3151808.995,49421.5,5311687,141107.6794 !,PooledQC1_4.d,QC,112/4/19 22:32,,FALSE,6,1.3,82,2652071,281.1996,77.2,405106,238.8151,80.5,9746444,256.5709 -,Post_01_1.d,QC,112/4/19 22:45,,FALSE,1,1,109.9,49544,5.3957,106.7,9749,5.8838,103.2,165173,5.5616 -,Post_01_2.d,QC,112/4/19 22:51,,FALSE,1,1,99.7,52108,5.2367,106.5,8073,5.3732,110.1,202400,5.6532 -,Post_01_3.d,QC,112/4/19 22:58,,FALSE,1,1,111.1,49674,5.4073,110.2,8316,5.3093,100.4,176221,5.5725 -,Post_02_1.d,QC,112/4/19 23:04,,FALSE,2,1,96.4,122300,9.9364,105.9,18477,9.8852,101,377932,9.9032 -,Post_02_2.d,QC,112/4/19 23:10,,FALSE,2,1,91.2,105570,9.016,110,16648,9.995,91,360173,9.7001 -,Post_02_3.d,QC,112/4/19 23:17,,FALSE,2,1,103.5,98617,9.8477,98.5,17240,9.2466,93.7,412361,9.8693 -,Post_2_1.d,QC,112/4/19 23:23,,FALSE,5,1,105,1209916,106.0271,106.5,223501,123.5191,98,4243184,93.0059 -,Post_2_2.d,QC,112/4/19 23:30,,FALSE,5,1,113.9,1215047,102.8634,116.3,199585,98.2603,103.3,4184436,103.3244 -,Post_2_3.d,QC,112/4/19 23:36,,FALSE,5,1,109,1360237,112.0024,110.6,211699,106.554,101.7,4294658,100.7243 +,Post_0-1_1.d,QC,112/4/19 22:45,,FALSE,1,1,109.9,49544,5.3957,106.7,9749,5.8838,103.2,165173,5.5616 +,Post_0-1_2.d,QC,112/4/19 22:51,,FALSE,1,1,99.7,52108,5.2367,106.5,8073,5.3732,110.1,202400,5.6532 +,Post_0-1_3.d,QC,112/4/19 22:58,,FALSE,1,1,111.1,49674,5.4073,110.2,8316,5.3093,100.4,176221,5.5725 +,Post_0-2_1.d,QC,112/4/19 23:04,,FALSE,2,1,96.4,122300,9.9364,105.9,18477,9.8852,101,377932,9.9032 +,Post_0-2_2.d,QC,112/4/19 23:10,,FALSE,2,1,91.2,105570,9.016,110,16648,9.995,91,360173,9.7001 +,Post_0-2_3.d,QC,112/4/19 23:17,,FALSE,2,1,103.5,98617,9.8477,98.5,17240,9.2466,93.7,412361,9.8693 +,Post_02_1.d,QC,112/4/19 23:23,,FALSE,5,1,105,1209916,106.0271,106.5,223501,123.5191,98,4243184,93.0059 +,Post_02_02.d,QC,112/4/19 23:30,,FALSE,5,1,113.9,1215047,102.8634,116.3,199585,98.2603,103.3,4184436,103.3244 +,Post_02_3.d,QC,112/4/19 23:36,,FALSE,5,1,109,1360237,112.0024,110.6,211699,106.554,101.7,4294658,100.7243 ,Post_15_1.d,QC,112/4/19 23:42,,FALSE,8,1,116.7,8937483,810.0292,106.4,1440183,840.2626,110.6,36181690,839.2939 !,Post_15_2.d,QC,112/4/19 23:49,,FALSE,8,1,106,8925869,813.916,111.4,1373695,790.0946,106.4,34594228,857.4285 ,Post_15_3.d,QC,112/4/19 23:55,,FALSE,8,1,109.1,8647520,716.376,113.4,1581770,793.415,115.2,34933347,931.8385 diff --git a/test/data/D2S0S7.csv b/test/data/D2S0S7.csv index ef557ae..220a2a4 100644 --- a/test/data/D2S0S7.csv +++ b/test/data/D2S0S7.csv @@ -3,32 +3,32 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, !,PooledQC2_1-r001.d,QC,112/4/20 02:09,,FALSE,6,1.3,81.5,2833806,259.7312,87.6,420510,244.4125,77,9732073,266.8167 !,PooledQC2_1-r002.d,QC,112/4/20 02:15,,FALSE,6,1.3,88.4,2508480,249.248,83.7,421392,246.9807,80.4,9992729,258.2189 !,PooledQC2_1-r003.d,QC,112/4/20 02:21,,FALSE,6,1.3,85.7,2523780,232.1686,90.5,453200,268.9367,79.1,9827728,256.1977 -,Cal2_1_01.d,Cal,112/4/20 02:34,,FALSE,1,1,99.8,55899,5.2387,112.7,8012,5.1368,113.6,181971,5.3278 -,Cal2_2_02.d,Cal,112/4/20 02:40,,FALSE,2,1,92.6,95829,9.0611,97.2,15142,9.5172,96,371367,9.1037 -,Cal2_3_05.d,Cal,112/4/20 02:47,,FALSE,3,1,93.7,311909,23.4272,106.3,48738,26.5838,103,1128200,22.997 -,Cal2_4_1.d,Cal,112/4/20 02:53,,FALSE,4,1,93.9,635995,49.9316,84.4,92403,47.6775,80,2173257,45.9944 -,Cal2_5_2.d,Cal,112/4/20 02:59,,FALSE,5,1,114.5,1440395,113.4555,108.5,228200,101.4739,104.5,5236067,93.4781 -,Cal2_6_5.d,Cal,112/4/20 03:06,,FALSE,6,1,98.6,3353698,228.9356,99.7,494005,270.2441,92.2,11910526,217.4459 +,Cal2_1_0-1.d,Cal,112/4/20 02:34,,FALSE,1,1,99.8,55899,5.2387,112.7,8012,5.1368,113.6,181971,5.3278 +,Cal2_2_0-2.d,Cal,112/4/20 02:40,,FALSE,2,1,92.6,95829,9.0611,97.2,15142,9.5172,96,371367,9.1037 +,Cal2_3_0-5.d,Cal,112/4/20 02:47,,FALSE,3,1,93.7,311909,23.4272,106.3,48738,26.5838,103,1128200,22.997 +,Cal2_4_01.d,Cal,112/4/20 02:53,,FALSE,4,1,93.9,635995,49.9316,84.4,92403,47.6775,80,2173257,45.9944 +,Cal2_5_02.d,Cal,112/4/20 02:59,,FALSE,5,1,114.5,1440395,113.4555,108.5,228200,101.4739,104.5,5236067,93.4781 +,Cal2_6_05.d,Cal,112/4/20 03:06,,FALSE,6,1,98.6,3353698,228.9356,99.7,494005,270.2441,92.2,11910526,217.4459 ,Cal2_7_10.d,Cal,112/4/20 03:12,,FALSE,7,1,102.7,6585960,443.5252,90,981472,525.2427,105.8,27439933,537.9522 ,Cal2_8_15.d,Cal,112/4/20 03:18,,FALSE,8,1,100.6,8523162,718.752,103.6,1420673,737.2413,110.7,33649340,878.7764 ,Cal2_9_20.d,Cal,112/4/20 03:25,,FALSE,9,1,103.7,14959542,1023.7433,97.5,1979822,1038.4905,117.3,52765978,1130.7841 !,PooledQC2_2.d,QC,112/4/20 03:44,,FALSE,6,1.3,75.3,2744233,245.033,90.2,457719,280.7727,84.8,8954696,242.4197 -,Pre2_01_1.d,QC,112/4/20 03:56,,FALSE,1,1,123.8,58779,5.9385,104.4,8362,5.3709,127.3,183304,5.9674 -!,Pre2_01_2.d,QC,112/4/20 04:03,,FALSE,1,1,122.7,59716,5.9366,127.5,10516,6.277,120.8,206845,5.9419 -!,Pre2_01_3.d,QC,112/4/20 04:09,,FALSE,1,1,125.3,63079,5.9174,127.3,8743,5.9135,127.4,208080,6.1225 -!,Pre2_01_4.d,QC,112/4/20 04:15,,FALSE,1,1,123.5,57727,5.9261,113.7,8698,5.6849,124.1,182370,6.0071 -,Pre2_01_5.d,QC,112/4/20 04:22,,FALSE,1,1,122.2,56445,5.8583,110,8474,5.4521,119.4,216471,5.9221 -,Pre2_02_1.d,QC,112/4/20 04:28,,FALSE,2,1,104.9,127610,10.1918,102.6,16653,9.9624,110.2,443010,10.2225 -,Pre2_02_2.d,QC,112/4/20 04:34,,FALSE,2,1,109.8,120864,9.68,93.8,16760,9.8752,110.1,404109,10.8068 -,Pre2_02_3.d,QC,112/4/20 04:41,,FALSE,2,1,102.1,107122,10.1083,101.5,15924,9.3505,97.9,416467,9.3879 -,Pre2_02_4.d,QC,112/4/20 04:47,,FALSE,2,1,98.6,103882,9.9585,98.1,17804,11.6055,109.8,422887,9.0751 -,Pre2_02_5.d,QC,112/4/20 04:54,,FALSE,2,1,109.2,126732,9.7161,93.1,19205,9.8079,98.2,439139,10.5227 +,Pre2_0-1_1.d,QC,112/4/20 03:56,,FALSE,1,1,123.8,58779,5.9385,104.4,8362,5.3709,127.3,183304,5.9674 +!,Pre2_0-1_2.d,QC,112/4/20 04:03,,FALSE,1,1,122.7,59716,5.9366,127.5,10516,6.277,120.8,206845,5.9419 +!,Pre2_0-1_3.d,QC,112/4/20 04:09,,FALSE,1,1,125.3,63079,5.9174,127.3,8743,5.9135,127.4,208080,6.1225 +!,Pre2_0-1_4.d,QC,112/4/20 04:15,,FALSE,1,1,123.5,57727,5.9261,113.7,8698,5.6849,124.1,182370,6.0071 +,Pre2_0-1_5.d,QC,112/4/20 04:22,,FALSE,1,1,122.2,56445,5.8583,110,8474,5.4521,119.4,216471,5.9221 +,Pre2_0-2_1.d,QC,112/4/20 04:28,,FALSE,2,1,104.9,127610,10.1918,102.6,16653,9.9624,110.2,443010,10.2225 +,Pre2_0-2_2.d,QC,112/4/20 04:34,,FALSE,2,1,109.8,120864,9.68,93.8,16760,9.8752,110.1,404109,10.8068 +,Pre2_0-2_3.d,QC,112/4/20 04:41,,FALSE,2,1,102.1,107122,10.1083,101.5,15924,9.3505,97.9,416467,9.3879 +,Pre2_0-2_4.d,QC,112/4/20 04:47,,FALSE,2,1,98.6,103882,9.9585,98.1,17804,11.6055,109.8,422887,9.0751 +,Pre2_0-2_5.d,QC,112/4/20 04:54,,FALSE,2,1,109.2,126732,9.7161,93.1,19205,9.8079,98.2,439139,10.5227 !,PooledQC2_3.d,QC,112/4/20 05:13,,FALSE,6,1.3,74.4,2637284,254.384,94.4,430729,288.1989,80,9670143,245.9001 -!,Pre2_2_1.d,QC,112/4/20 05:25,,FALSE,5,1,131.4,1657490,125.3901,116.5,220852,130.5294,109.3,5402356,105.2948 -!,Pre2_2_2.d,QC,112/4/20 05:32,,FALSE,5,1,123.9,1508372,103.9472,125.1,256370,123.1166,112.2,4890396,99.1598 -!,Pre2_2_3.d,QC,112/4/20 05:38,,FALSE,5,1,124.9,1428638,106.8898,128.1,217432,132.1262,117,4855891,116.0477 -!,Pre2_2_4.d,QC,112/4/20 05:44,,FALSE,5,1,116.4,1421586,108.4191,111.4,224164,111.4408,110.9,4983859,114.913 -!,Pre2_2_5.d,QC,112/4/20 05:51,,FALSE,5,1,109.1,1603414,124.1117,119.4,217439,110.4444,109.9,5403209,115.9304 +!,Pre2_02_1.d,QC,112/4/20 05:25,,FALSE,5,1,131.4,1657490,125.3901,116.5,220852,130.5294,109.3,5402356,105.2948 +!,Pre2_02_02.d,QC,112/4/20 05:32,,FALSE,5,1,123.9,1508372,103.9472,125.1,256370,123.1166,112.2,4890396,99.1598 +!,Pre2_02_3.d,QC,112/4/20 05:38,,FALSE,5,1,124.9,1428638,106.8898,128.1,217432,132.1262,117,4855891,116.0477 +!,Pre2_02_4.d,QC,112/4/20 05:44,,FALSE,5,1,116.4,1421586,108.4191,111.4,224164,111.4408,110.9,4983859,114.913 +!,Pre2_02_5.d,QC,112/4/20 05:51,,FALSE,5,1,109.1,1603414,124.1117,119.4,217439,110.4444,109.9,5403209,115.9304 ,Pre2_15_1.d,QC,112/4/20 05:57,,FALSE,8,1,88.2,9884274,781.5914,103.2,1445277,786.3646,106.8,39608371,778.6928 ,Pre2_15_2.d,QC,112/4/20 06:03,,FALSE,8,1,107.5,9587396,683.9141,102.4,1613551,685.5874,119.8,35663308,882.3544 !,Pre2_15_3.d,QC,112/4/20 06:10,,FALSE,8,1,103.5,9237923,838.0701,113.2,1603037,841.2154,128.2,35955793,899.2427 @@ -36,15 +36,15 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, !,Pre2_15_5.d,QC,112/4/20 06:22,,FALSE,8,1,112.1,10177836,872.8416,108.4,1468580,819.7479,110,37866828,865.1262 !,PooledQC2_4.d,QC,112/4/20 06:41,,FALSE,6,1.3,88.8,2844200,252.7461,80.9,425537,244.1796,77.6,9755083,236.6466 !,PooledQCD7_4.d,QC,112/4/20 06:48,,FALSE,6,0.7,126.1,3974217,249.9569,143.7,648917,267.8917,145.6,13872983,249.0319 -,S_RT_D7_02_1.d,QC,112/4/20 07:01,,FALSE,2,1,100.3,105817,9.3299,91.7,16268,9.1715,109.7,361582,10.5729 -,S_RT_D7_02_2.d,QC,112/4/20 07:07,,FALSE,2,1,101.4,102392,9.4356,101.8,14802,10.3795,110,333792,11.299 -,S_RT_D7_02_3.d,QC,112/4/20 07:13,,FALSE,2,1,92.3,86615,9.9322,84.7,13509,8.9721,98.2,319452,9.6193 -!,S_4C_D7_02_1.d,QC,112/4/20 07:20,,FALSE,2,1,98.1,112637,9.6119,108.4,18286,9.9362,106.5,383878,11.9495 -,S_4C_D7_02_2.d,QC,112/4/20 07:26,,FALSE,2,1,99.8,123172,9.9836,108.5,16950,11.146,102.5,434991,9.252 -,S_4C_D7_02_3.d,QC,112/4/20 07:32,,FALSE,2,1,102,99123,9.1999,102.9,17559,11.7896,98.2,357711,11.6177 -,S_N20C_D7_02_1.d,QC,112/4/20 07:39,,FALSE,2,1,108.8,95106,9.1817,111.6,15023,10.3617,99.7,387207,10.0663 +,S_RT_D7_0-2_1.d,QC,112/4/20 07:01,,FALSE,2,1,100.3,105817,9.3299,91.7,16268,9.1715,109.7,361582,10.5729 +,S_RT_D7_0-2_2.d,QC,112/4/20 07:07,,FALSE,2,1,101.4,102392,9.4356,101.8,14802,10.3795,110,333792,11.299 +,S_RT_D7_0-2_3.d,QC,112/4/20 07:13,,FALSE,2,1,92.3,86615,9.9322,84.7,13509,8.9721,98.2,319452,9.6193 +!,S_4C_D7_0-2_1.d,QC,112/4/20 07:20,,FALSE,2,1,98.1,112637,9.6119,108.4,18286,9.9362,106.5,383878,11.9495 +,S_4C_D7_0-2_2.d,QC,112/4/20 07:26,,FALSE,2,1,99.8,123172,9.9836,108.5,16950,11.146,102.5,434991,9.252 +,S_4C_D7_0-2_3.d,QC,112/4/20 07:32,,FALSE,2,1,102,99123,9.1999,102.9,17559,11.7896,98.2,357711,11.6177 +,S_N20C_D7_0-2_1.d,QC,112/4/20 07:39,,FALSE,2,1,108.8,95106,9.1817,111.6,15023,10.3617,99.7,387207,10.0663 ,S_N20C_D7_15_3.d,QC,112/4/20 07:45,,FALSE,8,1,93.7,8215739,819.4076,107.7,1138928,724.4008,101.6,32842032,792.3401 -,S_N20C_D7_02_3.d,QC,112/4/20 07:51,,FALSE,2,1,115.3,90321,9.5268,115.4,14928,9.6353,108.1,368689,9.8067 +,S_N20C_D7_0-2_3.d,QC,112/4/20 07:51,,FALSE,2,1,115.3,90321,9.5268,115.4,14928,9.6353,108.1,368689,9.8067 !,PooledQCD7_5.d,QC,112/4/20 08:10,,FALSE,6,0.7,143,4099047,242.7301,145.9,624484,222.3677,131.2,15747567,268.5578 ,S_RT_D7_15_1.d,QC,112/4/20 08:23,,FALSE,8,1,94.4,8697442,670.9784,91.8,1255136,751.9573,121.4,32015383,796.1522 ,S_RT_D7_15_2.d,QC,112/4/20 08:30,,FALSE,8,1,89.4,10783237,677.1012,97.1,1712251,696.974,110.3,39371159,856.5624 @@ -54,5 +54,5 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, ,S_4C_D7_15_3.d,QC,112/4/20 08:55,,FALSE,8,1,101.7,8941728,754.1687,108.5,1234954,810.1721,114.6,37240862,865.9058 !,S_N20C_D7_15_1.d,QC,112/4/20 09:01,,FALSE,8,1,104.1,8270489,757.932,103.4,1350459,781.5978,123,37944224,861.9695 !,S_N20C_D7_15_2.d,QC,112/4/20 09:08,,FALSE,8,1,100.1,8612939,780.4876,104.6,1270617,780.4909,110.2,35372025,962.6454 -!,S_N20C_D7_02_2.d,QC,112/4/20 09:14,,FALSE,2,1,103.5,92822,10.2452,101.6,16963,9.859,112.2,344688,10.8151 +!,S_N20C_D7_0-2_2.d,QC,112/4/20 09:14,,FALSE,2,1,103.5,92822,10.2452,101.6,16963,9.859,112.2,344688,10.8151 !,PooledQCD7_6.d,QC,112/4/20 09:33,,FALSE,6,0.7,137.5,3793274,231.3495,139.5,645021,256.1432,127.7,15674599,243.0561 diff --git a/test/data/D3.csv b/test/data/D3.csv index a2dbeab..132e92a 100644 --- a/test/data/D3.csv +++ b/test/data/D3.csv @@ -3,32 +3,32 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, !,PooledQC3_1-r001.d,QC,112/4/20 11:28,NOAC_QT_9.m,FALSE,6,1.3,88.2,2722459,250.0871,89.6,443326,286.5663,73.2,9132321,255.3682 !,PooledQC3_1-r002.d,QC,112/4/20 11:34,NOAC_QT_9.m,FALSE,6,1.3,82.1,2972909,252.6744,82.9,443704,288.5804,76.9,9977421,253.6303 !,PooledQC3_1-r003.d,QC,112/4/20 11:40,NOAC_QT_9.m,FALSE,6,1.3,89.5,2865708,266.6603,86.7,411107,272.905,80.4,9327722,245.2769 -,Cal3_1_01.d,Cal,112/4/20 11:53,NOAC_QT_9.m,FALSE,1,1,112.7,57281,5.2371,112.9,8478,5.3964,113.7,161850,5.284 -!,Cal3_2_02.d,Cal,112/4/20 11:59,NOAC_QT_9.m,FALSE,2,1,96.3,97283,9.1321,84.3,13465,8.4258,85.4,371478,9.241 -,Cal3_3_05.d,Cal,112/4/20 12:06,NOAC_QT_9.m,FALSE,3,1,105.1,336183,25.0133,93.6,47106,25.1579,88.7,1079661,22.1795 -,Cal3_4_1.d,Cal,112/4/20 12:12,NOAC_QT_9.m,FALSE,4,1,87.1,579756,48.0329,92.3,84463,44.1295,80.4,2130643,42.2053 -,Cal3_5_2.d,Cal,112/4/20 12:18,NOAC_QT_9.m,FALSE,5,1,114.2,1583806,107.2056,112,228569,121.0412,100.8,5205277,94.7514 -,Cal3_6_5.d,Cal,112/4/20 12:25,NOAC_QT_9.m,FALSE,6,1,97.4,3198675,245.3812,102.5,490255,223.3554,85.1,9916160,256.3737 +,Cal3_1_0-1.d,Cal,112/4/20 11:53,NOAC_QT_9.m,FALSE,1,1,112.7,57281,5.2371,112.9,8478,5.3964,113.7,161850,5.284 +!,Cal3_2_0-2.d,Cal,112/4/20 11:59,NOAC_QT_9.m,FALSE,2,1,96.3,97283,9.1321,84.3,13465,8.4258,85.4,371478,9.241 +,Cal3_3_0-5.d,Cal,112/4/20 12:06,NOAC_QT_9.m,FALSE,3,1,105.1,336183,25.0133,93.6,47106,25.1579,88.7,1079661,22.1795 +,Cal3_4_01.d,Cal,112/4/20 12:12,NOAC_QT_9.m,FALSE,4,1,87.1,579756,48.0329,92.3,84463,44.1295,80.4,2130643,42.2053 +,Cal3_5_02.d,Cal,112/4/20 12:18,NOAC_QT_9.m,FALSE,5,1,114.2,1583806,107.2056,112,228569,121.0412,100.8,5205277,94.7514 +,Cal3_6_05.d,Cal,112/4/20 12:25,NOAC_QT_9.m,FALSE,6,1,97.4,3198675,245.3812,102.5,490255,223.3554,85.1,9916160,256.3737 ,Cal3_7_10.d,Cal,112/4/20 12:31,NOAC_QT_9.m,FALSE,7,1,105.2,6898902,527.1849,109.1,967901,462.3404,116.5,23171693,511.33 ,Cal3_8_15.d,Cal,112/4/20 12:38,NOAC_QT_9.m,FALSE,8,1,99.5,8895203,760.0809,101,1339213,753.1637,100.4,37082346,743.7311 ,Cal3_9_20.d,Cal,112/4/20 12:44,NOAC_QT_9.m,FALSE,9,1,109.5,13359535,1238.4571,112.3,2057118,1126.1784,107.1,51489843,1019.5125 !,PooledQC3_2.d,QC,112/4/20 13:03,NOAC_QT_9.m,FALSE,6,1.3,74.2,2559449,232.6459,82.8,410604,274.1859,72.4,9428294,270.1426 -!,Pre3_01_1.d,QC,112/4/20 13:16,NOAC_QT_9.m,FALSE,1,1,119,65369,6.1478,124.8,9133,5.7892,127.7,200692,6.2847 -!,Pre3_01_2.d,QC,112/4/20 13:22,NOAC_QT_9.m,FALSE,1,1,105.4,50559,5.5677,143.9,10209,6.5973,123.6,198581,6.179 -!,Pre3_01_3.d,QC,112/4/20 13:28,NOAC_QT_9.m,FALSE,1,1,99.8,54944,5.4394,114.5,9747,6.027,125,204370,5.8001 -!,Pre3_01_4.d,QC,112/4/20 13:35,NOAC_QT_9.m,FALSE,1,1,118.9,63946,5.9972,121.6,9907,6.2807,122.2,221895,6.3085 -!,Pre3_01_5.d,QC,112/4/20 13:41,NOAC_QT_9.m,FALSE,1,1,113.6,60660,5.7295,134.8,9000,6.5399,114,208433,6.1007 -,Pre3_02_1.d,QC,112/4/20 13:47,NOAC_QT_9.m,FALSE,2,1,98.8,123019,10.3836,111.2,16844,9.3179,115.7,430410,9.7712 -,Pre3_02_2.d,QC,112/4/20 13:54,NOAC_QT_9.m,FALSE,2,1,115.9,137909,11.7935,105,21295,10.1962,104.7,455161,12.1742 -!,Pre3_02_3.d,QC,112/4/20 14:00,NOAC_QT_9.m,FALSE,2,1,118.6,136396,11.0605,110.1,22338,12.8133,110.1,425828,12.2092 -!,Pre3_02_4.d,QC,112/4/20 14:07,NOAC_QT_9.m,FALSE,2,1,131.6,146727,12.2555,130,24443,11.8988,128.4,478738,11.3379 -!,Pre3_02_5.d,QC,112/4/20 14:13,NOAC_QT_9.m,FALSE,2,1,123.6,156033,13.2554,136.7,20677,11.5665,136.6,510693,13.461 +!,Pre3_0-1_1.d,QC,112/4/20 13:16,NOAC_QT_9.m,FALSE,1,1,119,65369,6.1478,124.8,9133,5.7892,127.7,200692,6.2847 +!,Pre3_0-1_2.d,QC,112/4/20 13:22,NOAC_QT_9.m,FALSE,1,1,105.4,50559,5.5677,143.9,10209,6.5973,123.6,198581,6.179 +!,Pre3_0-1_3.d,QC,112/4/20 13:28,NOAC_QT_9.m,FALSE,1,1,99.8,54944,5.4394,114.5,9747,6.027,125,204370,5.8001 +!,Pre3_0-1_4.d,QC,112/4/20 13:35,NOAC_QT_9.m,FALSE,1,1,118.9,63946,5.9972,121.6,9907,6.2807,122.2,221895,6.3085 +!,Pre3_0-1_5.d,QC,112/4/20 13:41,NOAC_QT_9.m,FALSE,1,1,113.6,60660,5.7295,134.8,9000,6.5399,114,208433,6.1007 +,Pre3_0-2_1.d,QC,112/4/20 13:47,NOAC_QT_9.m,FALSE,2,1,98.8,123019,10.3836,111.2,16844,9.3179,115.7,430410,9.7712 +,Pre3_0-2_2.d,QC,112/4/20 13:54,NOAC_QT_9.m,FALSE,2,1,115.9,137909,11.7935,105,21295,10.1962,104.7,455161,12.1742 +!,Pre3_0-2_3.d,QC,112/4/20 14:00,NOAC_QT_9.m,FALSE,2,1,118.6,136396,11.0605,110.1,22338,12.8133,110.1,425828,12.2092 +!,Pre3_0-2_4.d,QC,112/4/20 14:07,NOAC_QT_9.m,FALSE,2,1,131.6,146727,12.2555,130,24443,11.8988,128.4,478738,11.3379 +!,Pre3_0-2_5.d,QC,112/4/20 14:13,NOAC_QT_9.m,FALSE,2,1,123.6,156033,13.2554,136.7,20677,11.5665,136.6,510693,13.461 !,PooledQC3_3.d,QC,112/4/20 14:32,NOAC_QT_9.m,FALSE,6,1.3,80.6,2847901,244.9455,90.2,467702,265.1718,80.6,9195266,241.9663 -!,Pre3_2_1.d,QC,112/4/20 14:45,NOAC_QT_9.m,FALSE,5,1,123.1,1547460,108.106,113.1,210445,117.1391,111.1,5597464,122.1489 -!,Pre3_2_2.d,QC,112/4/20 14:51,NOAC_QT_9.m,FALSE,5,1,125,1464151,122.9693,131.9,215679,131.8853,127.9,5350250,110.9158 -!,Pre3_2_3.d,QC,112/4/20 14:57,NOAC_QT_9.m,FALSE,5,1,115.8,1429671,121.773,129.7,217339,126.7027,116.5,5200804,112.4787 -!,Pre3_2_4.d,QC,112/4/20 15:04,NOAC_QT_9.m,FALSE,5,1,113.8,1626304,121.7748,132.1,252498,122.1083,119.6,5530175,125.6272 -!,Pre3_2_5.d,QC,112/4/20 15:10,NOAC_QT_9.m,FALSE,5,1,127.7,1545554,116.6806,118.3,235137,129.3,119.8,5008579,109.8221 +!,Pre3_02_1.d,QC,112/4/20 14:45,NOAC_QT_9.m,FALSE,5,1,123.1,1547460,108.106,113.1,210445,117.1391,111.1,5597464,122.1489 +!,Pre3_02_2.d,QC,112/4/20 14:51,NOAC_QT_9.m,FALSE,5,1,125,1464151,122.9693,131.9,215679,131.8853,127.9,5350250,110.9158 +!,Pre3_02_3.d,QC,112/4/20 14:57,NOAC_QT_9.m,FALSE,5,1,115.8,1429671,121.773,129.7,217339,126.7027,116.5,5200804,112.4787 +!,Pre3_02_4.d,QC,112/4/20 15:04,NOAC_QT_9.m,FALSE,5,1,113.8,1626304,121.7748,132.1,252498,122.1083,119.6,5530175,125.6272 +!,Pre3_02_5.d,QC,112/4/20 15:10,NOAC_QT_9.m,FALSE,5,1,127.7,1545554,116.6806,118.3,235137,129.3,119.8,5008579,109.8221 ,Pre3_15_1.d,QC,112/4/20 15:17,NOAC_QT_9.m,FALSE,8,1,106.4,9878635,707.2258,96.1,1534192,781.938,118.9,40082100,792.0211 ,Pre3_15_2.d,QC,112/4/20 15:23,NOAC_QT_9.m,FALSE,8,1,98.5,10165585,802.8786,103.7,1449028,830.5609,111.9,38018114,836.1698 ,Pre3_15_3.d,QC,112/4/20 15:29,NOAC_QT_9.m,FALSE,8,1,97.7,9846125,740.0453,103.3,1551069,744.0335,97.8,39501687,771.3509 diff --git a/test/data/S30.csv b/test/data/S30.csv index c333aa1..d714c80 100644 --- a/test/data/S30.csv +++ b/test/data/S30.csv @@ -1,25 +1,25 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, ,Data File,Type,Acq. Date-Time,DA Method File,Approved,Level,Dil.,Accuracy,Area,Final Conc.,Accuracy,Area,Final Conc.,Accuracy,Area,Final Conc. -,Calo_1_0_1-r001.d,Cal,112/5/13 19:50,NOACs_blood_new.m,FALSE,1,1,97.9,69480,5.1948,106,10889,5.4502,105.3,275242,5.713 -,Calo_1_0_1-r002.d,Cal,112/5/13 19:57,NOACs_blood_new.m,FALSE,1,1,100.9,70173,4.9971,101,10523,5.4476,117.2,264879,5.6092 -,Calo_1_0_1-r003.d,Cal,112/5/13 20:03,NOACs_blood_new.m,FALSE,1,1,104.8,71584,5.1422,99.1,9670,5.1542,104.3,287783,5.7627 -,Calo_1_0_1-r004.d,Cal,112/5/13 20:09,NOACs_blood_new.m,FALSE,1,1,96.2,64602,4.9592,103,10512,5.2004,122.4,257910,5.6201 +,Calo_1_0-1-r001.d,Cal,112/5/13 19:50,NOACs_blood_new.m,FALSE,1,1,97.9,69480,5.1948,106,10889,5.4502,105.3,275242,5.713 +,Calo_1_0-1-r002.d,Cal,112/5/13 19:57,NOACs_blood_new.m,FALSE,1,1,100.9,70173,4.9971,101,10523,5.4476,117.2,264879,5.6092 +,Calo_1_0-1-r003.d,Cal,112/5/13 20:03,NOACs_blood_new.m,FALSE,1,1,104.8,71584,5.1422,99.1,9670,5.1542,104.3,287783,5.7627 +,Calo_1_0-1-r004.d,Cal,112/5/13 20:09,NOACs_blood_new.m,FALSE,1,1,96.2,64602,4.9592,103,10512,5.2004,122.4,257910,5.6201 ,Calo_1_0_1-r005.d,Cal,112/5/13 20:16,NOACs_blood_new.m,FALSE,1,1,99.4,61783,5.2715,99.3,10346,5.3665,110.8,287625,5.6877 -,Calo_2_0_2-r001.d,Cal,112/5/13 20:22,NOACs_blood_new.m,FALSE,2,1,100.9,144467,9.4893,87.4,23550,9.0379,92.1,548414,9.1076 -,Calo_2_0_2-r002.d,Cal,112/5/13 20:29,NOACs_blood_new.m,FALSE,2,1,85.5,149327,9.1511,99,24817,9.195,93.6,539676,9.0605 -,Calo_2_0_2-r003.d,Cal,112/5/13 20:35,NOACs_blood_new.m,FALSE,2,1,84.9,126507,8.9873,87.4,22245,9.5397,97.9,619018,9.2851 -,Calo_3_0_5-r001.d,Cal,112/5/13 20:41,NOACs_blood_new.m,FALSE,3,1,93.2,408195,23.0574,100.2,69000,25.0515,90.8,1719644,23.7109 -,Calo_3_0_5-r002.d,Cal,112/5/13 20:48,NOACs_blood_new.m,FALSE,3,1,97.2,451115,26.5622,107.1,70534,24.0277,87.8,1774829,24.9433 -,Calo_3_0_5-r003.d,Cal,112/5/13 20:54,NOACs_blood_new.m,FALSE,3,1,100.2,392569,23.8082,93.9,69784,23.7191,85.3,1729268,20.8343 -,Calo_4_1-r001.d,Cal,112/5/13 21:00,NOACs_blood_new.m,FALSE,4,1,90.2,760346,45.6203,93.4,145114,40.1876,78.7,3335385,37.3748 -,Calo_4_1-r002.d,Cal,112/5/13 21:07,NOACs_blood_new.m,FALSE,4,1,85,809714,43.9789,81.4,139255,48.7221,72.1,3231046,38.5634 -,Calo_4_1-r003.d,Cal,112/5/13 21:13,NOACs_blood_new.m,FALSE,4,1,99.8,837163,46.4156,86.5,135287,44.7682,78.1,3290205,38.056 -,Calo_5_2-r001.d,Cal,112/5/13 21:19,NOACs_blood_new.m,FALSE,5,1,104.4,1833935,121.4145,111.1,326851,97.1096,98.3,6998017,96.3166 -,Calo_5_2-r002.d,Cal,112/5/13 21:26,NOACs_blood_new.m,FALSE,5,1,107.7,1840135,101.6865,101.3,367631,101.2662,88.2,8085822,95.164 -!,Calo_5_2-r003.d,Cal,112/5/13 21:32,NOACs_blood_new.m,FALSE,5,1,103.3,1842810,104.251,116.8,369450,121.8452,102.1,7132915,92.084 -,Calo_6_5-r001.d,Cal,112/5/13 21:38,NOACs_blood_new.m,FALSE,6,1,97.7,4194649,237.2207,98.5,764214,264.7015,96.3,18523166,254.36 -,Calo_6_5-r002.d,Cal,112/5/13 21:45,NOACs_blood_new.m,FALSE,6,1,103,3896643,242.9498,93,804764,267.5185,89.3,18730692,247.2952 -,Calo_6_5-r003.d,Cal,112/5/13 21:51,NOACs_blood_new.m,FALSE,6,1,98.1,3480057,268.1289,101.6,693794,250.8962,88.3,18162122,216.8556 +,Calo_2_0-2-r001.d,Cal,112/5/13 20:22,NOACs_blood_new.m,FALSE,2,1,100.9,144467,9.4893,87.4,23550,9.0379,92.1,548414,9.1076 +,Calo_2_0-2-r002.d,Cal,112/5/13 20:29,NOACs_blood_new.m,FALSE,2,1,85.5,149327,9.1511,99,24817,9.195,93.6,539676,9.0605 +,Calo_2_0-2-r003.d,Cal,112/5/13 20:35,NOACs_blood_new.m,FALSE,2,1,84.9,126507,8.9873,87.4,22245,9.5397,97.9,619018,9.2851 +,Calo_3_0-5-r001.d,Cal,112/5/13 20:41,NOACs_blood_new.m,FALSE,3,1,93.2,408195,23.0574,100.2,69000,25.0515,90.8,1719644,23.7109 +,Calo_3_0-5-r002.d,Cal,112/5/13 20:48,NOACs_blood_new.m,FALSE,3,1,97.2,451115,26.5622,107.1,70534,24.0277,87.8,1774829,24.9433 +,Calo_3_0-5-r003.d,Cal,112/5/13 20:54,NOACs_blood_new.m,FALSE,3,1,100.2,392569,23.8082,93.9,69784,23.7191,85.3,1729268,20.8343 +,Calo_4_01-r001.d,Cal,112/5/13 21:00,NOACs_blood_new.m,FALSE,4,1,90.2,760346,45.6203,93.4,145114,40.1876,78.7,3335385,37.3748 +,Calo_4_01-r002.d,Cal,112/5/13 21:07,NOACs_blood_new.m,FALSE,4,1,85,809714,43.9789,81.4,139255,48.7221,72.1,3231046,38.5634 +,Calo_4_01-r003.d,Cal,112/5/13 21:13,NOACs_blood_new.m,FALSE,4,1,99.8,837163,46.4156,86.5,135287,44.7682,78.1,3290205,38.056 +,Calo_5_02-r001.d,Cal,112/5/13 21:19,NOACs_blood_new.m,FALSE,5,1,104.4,1833935,121.4145,111.1,326851,97.1096,98.3,6998017,96.3166 +,Calo_5_02-r002.d,Cal,112/5/13 21:26,NOACs_blood_new.m,FALSE,5,1,107.7,1840135,101.6865,101.3,367631,101.2662,88.2,8085822,95.164 +!,Calo_5_02-r003.d,Cal,112/5/13 21:32,NOACs_blood_new.m,FALSE,5,1,103.3,1842810,104.251,116.8,369450,121.8452,102.1,7132915,92.084 +,Calo_6_05-r001.d,Cal,112/5/13 21:38,NOACs_blood_new.m,FALSE,6,1,97.7,4194649,237.2207,98.5,764214,264.7015,96.3,18523166,254.36 +,Calo_6_05-r002.d,Cal,112/5/13 21:45,NOACs_blood_new.m,FALSE,6,1,103,3896643,242.9498,93,804764,267.5185,89.3,18730692,247.2952 +,Calo_6_05-r003.d,Cal,112/5/13 21:51,NOACs_blood_new.m,FALSE,6,1,98.1,3480057,268.1289,101.6,693794,250.8962,88.3,18162122,216.8556 ,Calo_7_10-r001.d,Cal,112/5/13 21:58,NOACs_blood_new.m,FALSE,7,1,102.1,8353504,463.4004,97.1,1420023,463.3124,100.8,41224458,539.2087 ,Calo_7_10-r002.d,Cal,112/5/13 22:04,NOACs_blood_new.m,FALSE,7,1,102.9,7982358,454.5508,93.5,1593631,447.3312,114.5,40439307,552.3802 ,Calo_7_10-r003.d,Cal,112/5/13 22:10,NOACs_blood_new.m,FALSE,7,1,103.9,7273849,486.3703,99,1327195,486.7952,107.8,36617996,533.1631 @@ -29,15 +29,15 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, ,Calo_9_20-r001.d,Cal,112/5/13 22:36,NOACs_blood_new.m,FALSE,9,1,128.9,16146896,1161.4904,101.7,3175323,1093.8687,101.2,59465490,944.0397 ,Calo_9_20-r002.d,Cal,112/5/13 22:42,NOACs_blood_new.m,FALSE,9,1,132.2,16882064,1267.6702,113.7,3050175,1128.4927,89.2,55691498,900.9912 ,Calo_9_20-r003.d,Cal,112/5/13 22:48,NOACs_blood_new.m,FALSE,9,1,123.1,16415123,1284.769,97.7,2949944,1060.4572,93.8,63355066,930.8714 -,S_RT_D30_02_1.d,QC,112/5/14 00:11,NOACs_blood_new.m,FALSE,2,1,96.9,150284,9.1923,99.3,27968,9.4295,92.2,580793,10.0248 -,S_RT_D30_02_2.d,QC,112/5/14 00:17,NOACs_blood_new.m,FALSE,2,1,108.5,127644,9.2481,95.2,28654,9.9172,94.3,651042,9.4266 -,S_RT_D30_02_3.d,QC,112/5/14 00:24,NOACs_blood_new.m,FALSE,2,1,106.8,142219,9.9783,99.9,27056,10.8873,115.3,613030,11.7271 -,S_4C_D30_02_1.d,QC,112/5/14 00:30,NOACs_blood_new.m,FALSE,2,1,109.5,141880,11.5467,114.2,28137,11.8185,101.8,567664,9.3756 -,S_4C_D30_02_2.d,QC,112/5/14 00:36,NOACs_blood_new.m,FALSE,2,1,96.6,119009,9.9619,98.3,23769,10.2309,103.9,554081,9.8941 -,S_4C_D30_02_3.d,QC,112/5/14 00:43,NOACs_blood_new.m,FALSE,2,1,117.5,136662,10.7476,110.1,26233,10.3072,108.7,677485,9.6654 -,S_N20C_D30_02_1.d,QC,112/5/14 00:49,NOACs_blood_new.m,FALSE,2,1,106.2,128376,9.9181,100.7,23124,9.7683,94.8,506155,9.8797 -,S_N20C_D30_02_2.d,QC,112/5/14 00:55,NOACs_blood_new.m,FALSE,2,1,104.4,138872,9.5424,106.5,24001,9.7509,106.2,611836,10.2163 -,S_N20C_D30_02_3.d,QC,112/5/14 01:02,NOACs_blood_new.m,FALSE,2,1,94.3,126140,10.4288,104.4,27139,10.3408,109.1,596296,10.513 +,S_RT_D30_0-2-1.d,QC,112/5/14 00:11,NOACs_blood_new.m,FALSE,2,1,96.9,150284,9.1923,99.3,27968,9.4295,92.2,580793,10.0248 +,S_RT_D30_0-2-2.d,QC,112/5/14 00:17,NOACs_blood_new.m,FALSE,2,1,108.5,127644,9.2481,95.2,28654,9.9172,94.3,651042,9.4266 +,S_RT_D30_0-2-3.d,QC,112/5/14 00:24,NOACs_blood_new.m,FALSE,2,1,106.8,142219,9.9783,99.9,27056,10.8873,115.3,613030,11.7271 +,S_4C_D30_0-2-1.d,QC,112/5/14 00:30,NOACs_blood_new.m,FALSE,2,1,109.5,141880,11.5467,114.2,28137,11.8185,101.8,567664,9.3756 +,S_4C_D30_0-2-2.d,QC,112/5/14 00:36,NOACs_blood_new.m,FALSE,2,1,96.6,119009,9.9619,98.3,23769,10.2309,103.9,554081,9.8941 +,S_4C_D30_0-2-3.d,QC,112/5/14 00:43,NOACs_blood_new.m,FALSE,2,1,117.5,136662,10.7476,110.1,26233,10.3072,108.7,677485,9.6654 +,S_N20C_D30_0-2-1.d,QC,112/5/14 00:49,NOACs_blood_new.m,FALSE,2,1,106.2,128376,9.9181,100.7,23124,9.7683,94.8,506155,9.8797 +,S_N20C_D30_0-2-2.d,QC,112/5/14 00:55,NOACs_blood_new.m,FALSE,2,1,104.4,138872,9.5424,106.5,24001,9.7509,106.2,611836,10.2163 +,S_N20C_D30_0-2-3.d,QC,112/5/14 01:02,NOACs_blood_new.m,FALSE,2,1,94.3,126140,10.4288,104.4,27139,10.3408,109.1,596296,10.513 ,S_RT_D30_15_1.d,QC,112/5/14 01:21,NOACs_blood_new.m,FALSE,8,1,101.8,9307831,708.1421,81.6,1836603,690.1744,106.4,50319764,714.0863 !,S_RT_D30_15_2.d,QC,112/5/14 01:27,NOACs_blood_new.m,FALSE,8,1,103.2,10247017,791.1318,101.4,2265932,679.3441,126.2,52360922,896.711 !,S_RT_D30_15_3.d,QC,112/5/14 01:34,NOACs_blood_new.m,FALSE,8,1,113.6,11179147,891.0744,99.2,2467054,776.4027,108.8,51514070,856.1605 diff --git a/test/data/sample.csv b/test/data/sample.csv index c403af9..0460560 100644 --- a/test/data/sample.csv +++ b/test/data/sample.csv @@ -1,11 +1,11 @@ Sample,,,,,,,,A Results,,,B Results,,,C Results,, ,Data File,Type,Acq. Date-Time,DA Method File,Approved,Level,Dil.,Accuracy,Area,Final Conc.,Accuracy,Area,Final Conc.,Accuracy,Area,Final Conc. -,Cal2_1_0_1.d,Cal,112/5/14 04:13,NOACs_blood_new.m,FALSE,1,1,113.6,58187,5.4281,99.6,13585,4.8324,96.8,277255,4.8391 -,Cal2_2_0_2.d,Cal,112/5/14 04:19,NOACs_blood_new.m,FALSE,2,1,106.6,129737,9.5565,112.4,30813,12.0409,104.2,604598,10.9232 -,Cal2_3_0_5.d,Cal,112/5/14 04:25,NOACs_blood_new.m,FALSE,3,1,96.4,311309,24.3406,90,59652,22.2516,93.8,1338648,22.9462 -,Cal2_4_1.d,Cal,112/5/14 04:32,NOACs_blood_new.m,FALSE,4,1,82.2,640455,42.598,87.8,133383,44.92,95.3,2554341,48.633 -,Cal2_5_2.d,Cal,112/5/14 04:38,NOACs_blood_new.m,FALSE,5,1,104.5,1628568,117.4529,131.3,338902,122.3304,111.1,5994739,124.0801 -,Cal2_6_5.d,Cal,112/5/14 04:44,NOACs_blood_new.m,FALSE,6,1,81.4,2634833,184.5182,93.2,639561,253.4512,93.5,13064586,245.87 +,Cal2_1_0-1.d,Cal,112/5/14 04:13,NOACs_blood_new.m,FALSE,1,1,113.6,58187,5.4281,99.6,13585,4.8324,96.8,277255,4.8391 +,Cal2_2_0-2.d,Cal,112/5/14 04:19,NOACs_blood_new.m,FALSE,2,1,106.6,129737,9.5565,112.4,30813,12.0409,104.2,604598,10.9232 +,Cal2_3_0-5.d,Cal,112/5/14 04:25,NOACs_blood_new.m,FALSE,3,1,96.4,311309,24.3406,90,59652,22.2516,93.8,1338648,22.9462 +,Cal2_4_01.d,Cal,112/5/14 04:32,NOACs_blood_new.m,FALSE,4,1,82.2,640455,42.598,87.8,133383,44.92,95.3,2554341,48.633 +,Cal2_5_02.d,Cal,112/5/14 04:38,NOACs_blood_new.m,FALSE,5,1,104.5,1628568,117.4529,131.3,338902,122.3304,111.1,5994739,124.0801 +,Cal2_6_05.d,Cal,112/5/14 04:44,NOACs_blood_new.m,FALSE,6,1,81.4,2634833,184.5182,93.2,639561,253.4512,93.5,13064586,245.87 ,Cal2_7_10.d,Cal,112/5/14 04:51,NOACs_blood_new.m,FALSE,7,1,111.2,6994911,498.0678,125.2,1828155,633.0192,159.2,37840578,782.1555 ,Cal2_8_15.d,Cal,112/5/14 04:57,NOACs_blood_new.m,FALSE,8,1,62,6261175,488.9898,80.3,1637246,622.4147,89.4,33259481,692.5357 ,Cal2_9_20.d,Cal,112/5/14 05:04,NOACs_blood_new.m,FALSE,9,1,104.9,11670932,1016.6239,113.9,2800467,989.1726,116.4,50302148,1190.8672 diff --git a/test/runtests.jl b/test/runtests.jl index 91124db..e9d029a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,23 +1,40 @@ -using Validation, TypedTables, DataPipes, Statistics +using AnalyticalMethodValidation, ChemistryQuantitativeAnalysis, DataFrames, Statistics, Chain using Test +const AMA = AnalyticalMethodValidation +const CQA = ChemistryQuantitativeAnalysis -@testset "Validation.jl" begin - tbls = read_data.(["data/D1.csv", "data/D2S0S7.csv", "data/D3.csv", "data/S30.csv"]) - qc_t = @p tbls[1] filter(occursin(r"PooledQC", _.var"Data File")) filter(==("Final Conc.", _.var"Data Type")) Table - qc = QCReport(tbls[1]) - @test isapprox(qc.report.A[1], mean(qc_t.A)) - ap = APData(tbls[1:3]...) - re = RecoveryData(tbls[1]) - me = MEData(tbls[1]; matrix = r"Pre.*_(.*)_.*", stds = r"Post.*_(.*)_.*", type = "Area") - st = StabilityData(reduce(append!, tbls[[2, 4]])) - rap = Report(ap) - @test isapprox(var(rap.report.A_01[[1,4,7]]), rap.report.A_01[14]) - @test isapprox(rap.report.A_01[14] - rap.report.A_01[12] / 5, rap.report.A_01[16]) - @test isapprox(rap.report.A_01[16] + rap.report.A_01[12], (rap.report.A_01[18] * rap.report.A_01[10] / 100) ^ 2) - rre = Report(re) - rme = Report(me) - rst = Report(st) - sample = read_data("data/sample.csv") - sp = SampleReport(sample) - @test isapprox(sp.report.A[2], mean(sample.A[270:271])) +@testset "AnalyticalMethodValidation.jl" begin + df1 = AMA.read(joinpath("data", "D1.csv")) + df2 = AMA.read(joinpath("data", "D2S0S7.csv")) + dfs = AMA.read(joinpath.("data", ["D1.csv", "D2S0S7.csv", "D3.csv"])) + qc_t = filter("File" => Base.Fix1(occursin, r"PooledQC"), CQA.table(df1.estimated_concentration)) + qc = qc_report(df1; pct = false) + @test isapprox(qc.Data[1], mean(qc_t.A)) + ap = ap_report(dfs) + re = recovery_report(df1) + me = me_report(df1; matrix = r"Pre.*_(.*)_.*", stds = r"Post.*_(.*)_.*") + st = stability_report(df2; d0 = r"Pre.*_(.*)_.*", id = r"S.*_(.*)_D(.*)_(.*)_.*") + @test isapprox(std(ap.daily.Data[1:2:5]), ap.summary.Data[3]) + @test isapprox(ap.summary.Data[3] ^ 2 - ap.summary.Data[1] ^ 2 / 5, ap.summary.Data[4] ^ 2) + @test isapprox(sqrt(ap.summary.Data[4] ^ 2 + ap.summary.Data[1] ^ 2) * 100 / ap.summary.Data[2], ap.summary.Data[6]) + sample = AMA.read(joinpath("data", "sample.csv")) + sp = @chain sample begin + sample_report + pivot(:Analyte) + qualify(; lod = [1.5, 0.03, 0.3], loq = [5, 0.1, 1], lodsub = missing, loqsub = [1.5, 0.03, 0.3]) + end + @test collect(sp[1, [2, 4]]) == [1.5, 0.3] + @test ismissing(sp[4, 4]) + m = selectby(st.result, :Stats, ["Accuracy(%)", "Standard Deviation(%)"] => mean_plus_minus_std => "Accuracy(%)") + pv = pivot(m, [:Analyte, :Level]; drop = :Stats) + @test m[all.(zip(m.Analyte .== "A", m.Condition .== "4C", m.Level .== "0-2")), "Data"][begin] == pv[pv.Condition .== "4C", "Data|Analyte=A|Level=0-2"][begin] + @test unpivot(pv, "Level") == pivot(m, "Analyte"; drop = :Stats) + @test m.Data == unpivot(pv, ["Level", "Analyte"]; rows = :Analyte).Data + qc1 = selectby(qc, :Stats, ["Mean", "Standard Deviation"] => mean_plus_minus_std => "Mean", "Relative Standard Deviation" => add_percentage => "RSD"; pivot = true, prefix = false) + qc2 = @chain qc begin + pivot(:Stats) + selectby(:Stats, ["Mean", "Standard Deviation"] => mean_plus_minus_std => "Mean", "Relative Standard Deviation" => add_percentage => "RSD") + end + @test qc1[1, "Data|RSD"] == "4.37%" + @test qc2[2, "Data|Stats=RSD"] == "5.06%" end \ No newline at end of file