From 05ee8018c2b50386971a84c0be13b6c60d365bc6 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Mon, 4 Jan 2021 02:16:40 -0700 Subject: [PATCH 01/13] Remove Matlab script files --- MATLAB/beefblup.m | 341 -------------------------------------------- MATLAB/uniquecell.m | 9 -- 2 files changed, 350 deletions(-) delete mode 100644 MATLAB/beefblup.m delete mode 100644 MATLAB/uniquecell.m diff --git a/MATLAB/beefblup.m b/MATLAB/beefblup.m deleted file mode 100644 index d26f887..0000000 --- a/MATLAB/beefblup.m +++ /dev/null @@ -1,341 +0,0 @@ -% beefblup -% Main script for performing single-variate BLUP to find beef cattle -% breeding values -% Usage: beefblup -% (C) 2020 Thomas A. Christensen II -% Licensed under BSD-3-Clause License - -% Prepare the workspace for computation -clear -clc -close all - -%% Display stuff -disp('beefblup v. 0.1') -disp('(C) 2020 Thomas A. Christensen II') -disp('https://github.com/millironx/beefblup') -disp(' ') - -%% Prompt User -% Ask for an input spreadsheet -[name, path] = uigetfile('*.xlsx','Select a beefblup worksheet'); - -% Ask for an ouput text file -[savename, savepath, ~] = uiputfile('*.txt', 'Save your beefblup results', 'results'); - -% Ask for heritability -h2 = input('What is the heritability for this trait? >> '); - - -%% Import input file -tic -disp(' ') -disp('Importing Excel file...') - -% Import data from a suitable spreadsheet -fullname = [path name]; -clear name path -[~, ~, data] = xlsread(fullname); - -disp('Importing Excel file... Done!') -toc -disp(' ') - -%% Process input file -tic -disp(' ') -disp('Processing and formatting data...') -disp(' ') - -% Extract the headers into a separate array -headers = data(1,:); -data(1,:) = []; - -% Convert the string dates to numbers -data(:,2) = num2cell(datenum(data(:,2))); - -% Sort the array by date -data = sortrows(data,2); - -% Coerce all id fields to string format -strings = [1 3 4]; -data(:,strings) = cellfun(@num2str, data(:,strings), 'UniformOutput', false); - -% Define fields to hold id values for animals and their parents -ids = char(data{:,1}); -damids = char(data{:,3}); -sireids = char(data{:,4}); -numanimals = length(data(:,1)); - -% Define fields to hold the index values for animals and their parents -dam = zeros(numanimals,1); -sire = zeros(numanimals,1); - -% Find all row numbers where an animal was a parent -for i=1:numanimals - % Find all animals that this animal birthed - dammatch = ismember(damids, ids(i,:), 'rows'); - damindexes = find(dammatch == 1); - dam(damindexes) = i; - - % Find all animals that this animal sired - sirematch = ismember(sireids, ids(i,:), 'rows'); - sireindexes = find(sirematch == 1); - sire(sireindexes) = i; -end - -% Store column numbers that need to be deleted -% Column 6 contains an intermediate Excel calculation and always needs to -% be deleted -colstodelete = 6; - -% Coerce each group to string format -for i = 7:length(headers) - data(:,i) = cellfun(@num2str, data(:,i), 'UniformOutput', false); -end - -% Find any columns that need to be deleted -for i = 7:length(headers) - if length(uniquecell(data(:,i))) <= 1 - colname = headers{i}; - disp(['Column "' colname '" does not have any unique animals and will be removed']) - disp('from this analysis'); - colstodelete = [colstodelete i]; - end -end - -% Delete the appropriate columns from the datasheet and the headers -data(:,colstodelete) = []; -headers(colstodelete) = []; - -% Determine how many contemporary groups there are -numgroups = ones(1, length(headers)-5); -for i = 6:length(headers) - numgroups(i-5) = length(uniquecell(data(:,i))); -end - -% If there are more groups than animals, then the analysis cannot continue -if sum(numgroups) >= numanimals - disp('There are more contemporary groups than animals. The analysis will now abort.'); - return -end - -% Define a "normal" animal as one of the last in the groups, provided that -% all traits do not have null values -normal = cell([1 length(headers)-5]); -for i = 6:length(headers) - for j = numanimals:-1:1 - if not(cellfun(@isempty, data(j,i))) - normal(i - 5) = data(j,i); - break - end - end -end - -% Print the results of the "normal" definition -disp(' ') -disp('For the purposes of this analysis, a "normal" animal will be defined') -disp('by the following traits:') -for i = 6:length(headers) - disp([headers{i} ': ' normal{i-5}]) -end -disp(' ') -disp('If no animal matching this description exists, the results may appear') -disp('outlandish, but are still as correct as the accuracy suggests') -disp(' ') - -disp('Processing and formatting data... Done!') -toc -disp(' ') - -%% Create the fixed-effect matrix -tic -disp(' ') -disp('Creating the fixed-effect matrix...') - -% Form the fixed effect matrix -X = zeros(numanimals, sum(numgroups)-length(numgroups)+1); -X(:,1) = ones(1, numanimals); - -% Create an external counter that will increment through both loops -I = 2; - -% Store the traits in a string cell array -adjustedtraits = cell(1, sum(numgroups)-length(numgroups)); - -% Iterate through each group -for i = 1:length(normal) - % Find the traits that are present in this trait - traits = uniquecell(data(:,i+5)); - - % Remove the "normal" version from the analysis - normalindex = find(strcmp(traits, normal{i})); - traits(normalindex) = []; - - % Iterate inside of the group - for j = 1:length(traits) - matchedindex = find(strcmp(data(:,i+5), traits{j})); - X(matchedindex, I) = 1; - - % Add this trait to the string - adjustedtraits(I - 1) = traits(j); - - % Increment the big counter - I = I + 1; - end -end - -disp('Creating the fixed-effect matrix... Done!') -toc -disp(' ') - -%% Additive relationship matrix -tic -disp(' ') -disp('Creating the additive relationship matrix...') - -% Create an empty matrix for the additive relationship matrix -A = zeros(numanimals, numanimals); - -% Create the additive relationship matrix by the FORTRAN method presented -% by Henderson -for i = 1:numanimals - if dam(i) ~= 0 && sire(i) ~= 0 - for j = 1:(i-1) - A(j,i) = 0.5*(A(j,sire(i))+A(j,dam(i))); - A(i,j) = A(j,i); - end - A(i,i) = 1 + 0.5*A(sire(i),dam(i)); - elseif dam(i) ~= 0 && sire(i) == 0 - for j = 1:(i-1) - A(j,i) = 0.5*A(j,dam(i)); - A(i,j) = A(j,i); - end - A(i,i) = 1; - elseif dam(i) == 0 && sire(i) ~=0 - for j = 1:(i-1) - A(j,i) = 0.5*A(j,sire(i)); - A(i,j) = A(j,i); - end - A(i,i) = 1; - else - for j = 1:(i-1) - A(j,i) = 0; - A(i,j) = 0; - end - A(i,i) = 1; - end -end - -disp('Creating the additive relationship matrix... Done!') -toc -disp(' ') - -%% Perform BLUP -tic -disp(' ') -disp('Solving the mixed-model equations') - -% Extract the observed data -Y = cell2mat(data(:, 5)); - -% The identity matrix for random effects -Z = eye(numanimals, numanimals); - -% Remove items where there is no data -nullobs = find(isnan(Y)); -Z(nullobs, nullobs) = 0; - -% Calculate heritability -lambda = (1-h2)/h2; - -% Use the mixed-model equations -solutions = [X'*X X'*Z; Z'*X (Z'*Z)+(inv(A).*lambda)]\[X'*Y; Z'*Y]; - -% Find the accuracies -diaginv = diag(inv([X'*X X'*Z; Z'*X (Z'*Z)+(inv(A).*lambda)])); -reliability = 1 - diaginv.*lambda; - -disp('Solving the mixed-model equations... Done!') -toc -disp(' ') - -%% Output the results -tic -disp(' ') -disp('Saving results...') - -% Find how many traits we found BLUE for -numgroups = numgroups - 1; - -% Start printing results to output -fileID = fopen([savepath savename], 'w'); -fprintf(fileID, 'beefblup Results Report\n'); -fprintf(fileID, 'Produced using beefblup for MATLAB ('); -fprintf(fileID, '%s', 'https://github.com/millironx/beefblup'); -fprintf(fileID, ')\n\n'); -fprintf(fileID, 'Input:\t'); -fprintf(fileID, '%s', fullname); -fprintf(fileID, '\nAnalysis performed:\t'); -fprintf(fileID, date); -fprintf(fileID, '\nTrait examined:\t'); -fprintf(fileID, [headers{5}]); -fprintf(fileID, '\n\n'); - -% Print base population stats -fprintf(fileID, 'Base Population:\n'); -for i = 1:length(numgroups) - fprintf(fileID, '\t'); - fprintf(fileID, [headers{i+5}]); - fprintf(fileID, ':\t'); - fprintf(fileID, [normal{i}]); - fprintf(fileID, '\n'); -end -fprintf(fileID, '\tMean '); -fprintf(fileID, [headers{5}]); -fprintf(fileID, ':\t'); -fprintf(fileID, num2str(solutions(1))); -fprintf(fileID, '\n\n'); - -I = 2; - -% Contemporary group adjustments -fprintf(fileID, 'Contemporary Group Effects:\n'); -for i = 1:length(numgroups) - fprintf(fileID, '\t'); - fprintf(fileID, [headers{i+5}]); - fprintf(fileID, '\tEffect\tReliability\n'); - for j = 1:numgroups(i) - fprintf(fileID, '\t'); - fprintf(fileID, [adjustedtraits{I-1}]); - fprintf(fileID, '\t'); - fprintf(fileID, num2str(solutions(I))); - fprintf(fileID, '\t'); - fprintf(fileID, num2str(reliability(I))); - fprintf(fileID, '\n'); - - I = I + 1; - end - fprintf(fileID, '\n'); -end -fprintf(fileID, '\n'); - -% Expected breeding values -fprintf(fileID, 'Expected Breeding Values:\n'); -fprintf(fileID, '\tID\tEBV\tReliability\n'); -for i = 1:numanimals - fprintf(fileID, '\t'); - fprintf(fileID, [data{i,1}]); - fprintf(fileID, '\t'); - fprintf(fileID, num2str(solutions(i+I-1))); - fprintf(fileID, '\t'); - fprintf(fileID, num2str(reliability(i+I-1))); - fprintf(fileID, '\n'); -end - -fprintf(fileID, '\n - END REPORT -'); -fclose(fileID); - -disp('Saving results... Done!') -toc -disp(' ') \ No newline at end of file diff --git a/MATLAB/uniquecell.m b/MATLAB/uniquecell.m deleted file mode 100644 index e5beae1..0000000 --- a/MATLAB/uniquecell.m +++ /dev/null @@ -1,9 +0,0 @@ -% uniquenan -% Serves the same purpose as UNIQUE, but ensures any empty cells are not -% counted -function y = uniquecell(x) -y = unique(x); -if any(cellfun(@isempty, y)) - y(cellfun(@isempty, y)) = []; -end -end \ No newline at end of file From ec3356fcf35fc49669a2ebf79afd1cb9b1e104cf Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Mon, 4 Jan 2021 02:24:39 -0700 Subject: [PATCH 02/13] Reformat spreadsheet --- Excel/Master BLUP Worksheet.xlsx | Bin 33970 -> 0 bytes Excel/van der Werf.xlsx | Bin 15252 -> 0 bytes data/sample.csv | 8 ++++++++ 3 files changed, 8 insertions(+) delete mode 100644 Excel/Master BLUP Worksheet.xlsx delete mode 100644 Excel/van der Werf.xlsx create mode 100644 data/sample.csv diff --git a/Excel/Master BLUP Worksheet.xlsx b/Excel/Master BLUP Worksheet.xlsx deleted file mode 100644 index 41e2174ef4b9f4200b70ca6d82688b1494e43601..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33970 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
    KRPm|P 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 diff --git a/data/sample.csv b/data/sample.csv new file mode 100644 index 0000000..c3be9f3 --- /dev/null +++ b/data/sample.csv @@ -0,0 +1,8 @@ +id,birthdate,dam,sire,year,sex,weaning_weight +1,1/1/1990,,,1990,male,354 +2,1/1/1990,,,1990,female,251 +3,1/1/1991,,1,1991,male,327 +4,1/1/1991,,1,1991,female,328 +5,1/1/1991,2,1,1991,male,301 +6,1/1/1991,2,,1991,female,270 +7,1/1/1992,,,1992,male,330 From 5d7ecceef8a131557bdb4138b6c6bf004faf4695 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Mon, 4 Jan 2021 02:25:10 -0700 Subject: [PATCH 03/13] Rename 'Julia' folder to 'src' folder for consistancy with packages --- {Julia => src}/beefblup.jl | 0 {Julia => src}/install.jl | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {Julia => src}/beefblup.jl (100%) rename {Julia => src}/install.jl (100%) diff --git a/Julia/beefblup.jl b/src/beefblup.jl similarity index 100% rename from Julia/beefblup.jl rename to src/beefblup.jl diff --git a/Julia/install.jl b/src/install.jl similarity index 100% rename from Julia/install.jl rename to src/install.jl From f450295b2064743187fd8ebc80f24a065b197af7 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 10:38:49 -0500 Subject: [PATCH 04/13] Initial pass at input file change Known to produce wrong results --- src/beefblup.jl | 66 ++++++++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/src/beefblup.jl b/src/beefblup.jl index 5cf891b..79797be 100644 --- a/src/beefblup.jl +++ b/src/beefblup.jl @@ -6,7 +6,8 @@ # Licensed under BSD-3-Clause License # Import the required packages -using XLSX +using CSV +using DataFrames using LinearAlgebra using Dates using Gtk @@ -22,7 +23,7 @@ print("\n") path = open_dialog_native( "Select a beefblup worksheet", GtkNullContainer(), - ("*.xlsx", GtkFileFilter("*.xlsx", name="beefblup worksheet")) + ("*.csv", GtkFileFilter("*.csv", name="beefblup worksheet")) ) # Ask for an output text filename @@ -38,58 +39,45 @@ print("What is the heritability for this trait?> ") h2 = parse(Float64, readline(stdin)) ### Import input filename -print("[馃惍]: Importing Excel file...") +print("[馃惍]: Importing data file...") # Import data from a suitable spreadsheet -data = XLSX.readxlsx(path)[1][:] +data = CSV.File(path) |> DataFrame print("Done!\n") ### Process input file print("[馃惍]: Processing and formatting data...") -# Extract the headers into a separate array -headers = data[1,:] -data = data[2:end,:] - # Sort the array by date -data = sortslices(data, dims=1, lt=(x,y)->isless(x[2],y[2])) +sort!(data, :birthdate) # Define fields to hold id values for animals and their parents -ids = string.(data[:,1]) -damids = string.(data[:,3]) -sireids = string.(data[:,4]) -numanimals = length(ids) +numanimals = length(data.id) # Find the index values for animals and their parents -dam = indexin(damids, ids) -sire = indexin(sireids, ids) +dam = indexin(data.dam, data.id) +sire = indexin(data.sire, data.id) -# Store column numbers that need to be deleted -# Column 6 contains an intermediate Excel calculation and always need to -# be deleted -colstokeep = [1, 2, 3, 4, 5] +# Extract all of the fixed effects +fixedfx = select(data, Not([:id, :birthdate, :sire, :dam]))[:,1:end-1] # Find any columns that need to be deleted -for i in 7:length(headers) - if length(unique(data[:,i])) <= 1 - colname = headers[i] +for i in 1:ncol(fixedfx) + if length(unique(fixedfx[:,i])) <= 1 + colname = names(fixedfx)[i] print("Column '") print(colname) print("' does not have any unique animals and will be removed from this analysis\n") - else - push!(colstokeep, i) + deletecols!(fixedfx,i) end end -# Delete the appropriate columns from the datasheet and the headers -data = data[:, colstokeep] -headers = headers[colstokeep] - # Determine how many contemporary groups there are -numgroups = ones(1, length(headers)-5) -for i in 6:length(headers) - numgroups[i-5] = length(unique(data[:,i])) +numtraits = ncol(fixedfx) +numgroups = ones(1, numtraits) +for i in 1:numtraits + numgroups[i] = length(unique(fixedfx[:,i])) end # If there are more groups than animals, then the analysis cannot continue @@ -101,11 +89,11 @@ end # Define a "normal" animal as one of the last in the groups, provided that # all traits do not have null values -normal = Array{String}(undef,1,length(headers)-5) -for i in 6:length(headers) +normal = Array{String}(undef,1,numtraits) +for i in 1:numtraits for j in numanimals:-1:1 - if !ismissing(data[j,i]) - normal[i-5] = string(data[j,i]) + if !ismissing(fixedfx[j,i]) + normal[i] = string(fixedfx[j,i]) break end end @@ -129,12 +117,12 @@ Array{String}(undef,floor(Int,sum(numgroups))-length(numgroups)) # Iterate through each group for i in 1:length(normal) # Find the traits that are present in this trait - localdata = string.(data[:,i+5]) + localdata = string.(fixedfx[:,i]) traits = unique(localdata) # Remove the normal version from the analysis effecttraits = traits[findall(x -> x != normal[i], traits)] # Iterate inside of the group - for j in 1:length(effecttraits) + for j in 1:(length(effecttraits) - 1) matchedindex = findall(x -> x != effecttraits[j], localdata) X[matchedindex, counter] .= 1 # Add this trait to the string @@ -188,7 +176,7 @@ print("Done!\n") print("[馃惍]: Solving the mixed-model equations...") # Extract the observed data -Y = convert(Array{Float64}, data[:,5]) +Y = convert(Array{Float64}, data[:,end]) # The random effects matrix Z = Matrix{Int}(I, numanimals, numanimals) @@ -273,7 +261,7 @@ write(fileID, "Expected Breeding Values:\n") write(fileID, "\tID\tEBV\tReliability\n") for i in 1:numanimals write(fileID, "\t") - write(fileID, ids[i]) + write(fileID, data.id[i]) write(fileID, "\t") write(fileID, string(solutions[i+counter-1])) write(fileID, "\t") From d0b0e79b384b74d5225486e8295ac5a6df5f1f47 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 10:42:12 -0500 Subject: [PATCH 05/13] Switch to Julia's package install method --- Project.toml | 6 ++++++ src/install.jl | 13 ------------- 2 files changed, 6 insertions(+), 13 deletions(-) create mode 100644 Project.toml delete mode 100644 src/install.jl diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..d2886bb --- /dev/null +++ b/Project.toml @@ -0,0 +1,6 @@ +[deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/src/install.jl b/src/install.jl deleted file mode 100644 index 2de4ccd..0000000 --- a/src/install.jl +++ /dev/null @@ -1,13 +0,0 @@ -# beefblup install -# Prepares the Julia environment for using beefblup by installing the requisite -# packages -# Usage: julia install.jl -# (C) 2020 Thomas A. Christensen II -# Licensed under BSD-3-Clause License - -# Import the package manager -using Pkg - -# Install requisite packages -Pkg.add("XLSX") -Pkg.add("Gtk") \ No newline at end of file From 4efeb5d53a0008f9af841712707afcc37926226a Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 10:46:21 -0500 Subject: [PATCH 06/13] Add auto-project-loading shebang --- src/beefblup.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/beefblup.jl b/src/beefblup.jl index 79797be..3a63b05 100644 --- a/src/beefblup.jl +++ b/src/beefblup.jl @@ -1,3 +1,7 @@ +#!/bin/bash +#= +exec julia --project=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) "${BASH_SOURCE[0]}" "$@" +=# # beefblup # Main script for performing single-variate BLUP to find beef cattle # breeding values From de0eb6f5a9ee22d60fc3b2766216bbcb69112f03 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 12:05:55 -0500 Subject: [PATCH 07/13] Fix spell checking so I can focus on real problems --- src/beefblup.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/beefblup.jl b/src/beefblup.jl index 3a63b05..fe6750f 100644 --- a/src/beefblup.jl +++ b/src/beefblup.jl @@ -8,6 +8,8 @@ exec julia --project=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) "${BA # Usage: julia beefblup.jl # (C) 2020 Thomas A. Christensen II # Licensed under BSD-3-Clause License +# cSpell:includeRegExp #.* +# cSpell:includeRegExp ("""|''')[^\1]*\1 # Import the required packages using CSV From 82bf3cfa89767d3662271d03260400e88637de75 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 12:07:58 -0500 Subject: [PATCH 08/13] Switch to UNIX line endings --- src/beefblup.jl | 562 ++++++++++++++++++++++++------------------------ 1 file changed, 281 insertions(+), 281 deletions(-) mode change 100644 => 100755 src/beefblup.jl diff --git a/src/beefblup.jl b/src/beefblup.jl old mode 100644 new mode 100755 index fe6750f..68f8848 --- a/src/beefblup.jl +++ b/src/beefblup.jl @@ -1,281 +1,281 @@ -#!/bin/bash -#= -exec julia --project=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) "${BASH_SOURCE[0]}" "$@" -=# -# beefblup -# Main script for performing single-variate BLUP to find beef cattle -# breeding values -# Usage: julia beefblup.jl -# (C) 2020 Thomas A. Christensen II -# Licensed under BSD-3-Clause License -# cSpell:includeRegExp #.* -# cSpell:includeRegExp ("""|''')[^\1]*\1 - -# Import the required packages -using CSV -using DataFrames -using LinearAlgebra -using Dates -using Gtk - -# Display stuff -println("beefblup v 0.1") -println("(C) 2020 Thomas A. Christensen II") -println("https://github.com/millironx/beefblup") -print("\n") - -### Prompt User -# Ask for an input spreadsheet -path = open_dialog_native( - "Select a beefblup worksheet", - GtkNullContainer(), - ("*.csv", GtkFileFilter("*.csv", name="beefblup worksheet")) -) - -# Ask for an output text filename -savepath = save_dialog_native( - "Save your beefblup results", - GtkNullContainer(), - (GtkFileFilter("*.txt", name="Results file"), - "*.txt") -) - -# Ask for heritability -print("What is the heritability for this trait?> ") -h2 = parse(Float64, readline(stdin)) - -### Import input filename -print("[馃惍]: Importing data file...") - -# Import data from a suitable spreadsheet -data = CSV.File(path) |> DataFrame - -print("Done!\n") - -### Process input file -print("[馃惍]: Processing and formatting data...") - -# Sort the array by date -sort!(data, :birthdate) - -# Define fields to hold id values for animals and their parents -numanimals = length(data.id) - -# Find the index values for animals and their parents -dam = indexin(data.dam, data.id) -sire = indexin(data.sire, data.id) - -# Extract all of the fixed effects -fixedfx = select(data, Not([:id, :birthdate, :sire, :dam]))[:,1:end-1] - -# Find any columns that need to be deleted -for i in 1:ncol(fixedfx) - if length(unique(fixedfx[:,i])) <= 1 - colname = names(fixedfx)[i] - print("Column '") - print(colname) - print("' does not have any unique animals and will be removed from this analysis\n") - deletecols!(fixedfx,i) - end -end - -# Determine how many contemporary groups there are -numtraits = ncol(fixedfx) -numgroups = ones(1, numtraits) -for i in 1:numtraits - numgroups[i] = length(unique(fixedfx[:,i])) -end - -# If there are more groups than animals, then the analysis cannot continue -if sum(numgroups) >= numanimals - println("There are more contemporary groups than animals. The analysis will - now abort.") - exit() -end - -# Define a "normal" animal as one of the last in the groups, provided that -# all traits do not have null values -normal = Array{String}(undef,1,numtraits) -for i in 1:numtraits - for j in numanimals:-1:1 - if !ismissing(fixedfx[j,i]) - normal[i] = string(fixedfx[j,i]) - break - end - end -end - -print("Done!\n") - -### Create the fixed-effect matrix -print("[馃惍]: Creating the fixed-effect matrix...") - -# Form the fixed-effect matrix -X = zeros(Int8, numanimals, floor(Int,sum(numgroups))-length(numgroups)+1) -X[:,1] = ones(Int8, 1, numanimals) - -# Create an external counter that will increment through both loops -counter = 2 - -# Store the traits in a string array -adjustedtraits = -Array{String}(undef,floor(Int,sum(numgroups))-length(numgroups)) -# Iterate through each group -for i in 1:length(normal) - # Find the traits that are present in this trait - localdata = string.(fixedfx[:,i]) - traits = unique(localdata) - # Remove the normal version from the analysis - effecttraits = traits[findall(x -> x != normal[i], traits)] - # Iterate inside of the group - for j in 1:(length(effecttraits) - 1) - matchedindex = findall(x -> x != effecttraits[j], localdata) - X[matchedindex, counter] .= 1 - # Add this trait to the string - adjustedtraits[counter - 1] = traits[j] - # Increment the big counter - global counter = counter + 1 - end -end - -print("Done!\n") - -### Additive relationship matrix -print("[馃惍]: Creating additive relationship matrix...") - -# Create an empty matrix for the additive relationship matrix -A = zeros(numanimals, numanimals) - -# Create the additive relationship matrix by the FORTRAN method presented by -# Henderson -for i in 1:numanimals - if !isnothing(dam[i]) && !isnothing(sire[i]) - for j in 1:(i-1) - A[j,i] = 0.5*(A[j,sire[i]] + A[j,dam[i]]) - A[i,j] = A[j,i] - end - A[i,i] = 1 + 0.5*A[sire[i], dam[i]] - elseif !isnothing(dam[i]) && isnothing(sire[i]) - for j in 1:(i-1) - A[j,i] = 0.5*A[j,dam[i]] - A[i,j] = A[j,i] - end - A[i,i] = 1 - elseif isnothing(dam[i]) && !isnothing(sire[i]) - for j in 1:(i-1) - A[j,i] = 0.5*A[j,sire[i]] - A[i,j] = A[j,i] - end - A[i,i] = 1 - else - for j in 1:(i-1) - A[j,i] = 0 - A[i,j] = 0 - end - A[i,i] = 1 - end -end - -print("Done!\n") - -### Perform BLUP -print("[馃惍]: Solving the mixed-model equations...") - -# Extract the observed data -Y = convert(Array{Float64}, data[:,end]) - -# The random effects matrix -Z = Matrix{Int}(I, numanimals, numanimals) - -# Remove items where there is no data -nullobs = findall(isnothing, Y) -Z[nullobs, nullobs] .= 0 - -# Calculate heritability -位 = (1-h2)/h2 - -# Use the mixed-model equations -MME = [X'*X X'*Z; Z'*X (Z'*Z)+(inv(A).*位)] -MMY = [X'*Y; Z'*Y] -solutions = MME\MMY - -# Find the accuracies -diaginv = diag(inv(MME)) -reliability = ones(Float64, length(diaginv)) - diaginv.*位 - -print("Done!\n") - -### Output the results -print("[馃惍]: Saving results...") - -# Find how many traits we found BLUE for -numgroups = numgroups .- 1 - -# Start printing results to output -fileID = open(savepath, "w") -write(fileID, "beefblup Results Report\n") -write(fileID, "Produced using beefblup for Julia (") -write(fileID, "https://github.com/millironx/beefblup") -write(fileID, ")\n\n") -write(fileID, "Input:\t") -write(fileID, path) -write(fileID, "\nAnalysis performed:\t") -write(fileID, string(Dates.today())) -write(fileID, "\nTrait examined:\t") -write(fileID, headers[5]) -write(fileID, "\n\n") - -# Print base population stats -write(fileID, "Base Population:\n") -for i in 1:length(numgroups) - write(fileID, "\t") - write(fileID, headers[i+5]) - write(fileID, ":\t") - write(fileID, normal[i]) - write(fileID, "\n") -end -write(fileID, "\tMean ") -write(fileID, headers[5]) -write(fileID, ":\t") -write(fileID, string(solutions[1])) -write(fileID, "\n\n") - -# Contemporary group adjustments -counter = 2 -write(fileID, "Contemporary Group Effects:\n") -for i in 1:length(numgroups) - write(fileID, "\t") - write(fileID, headers[i+5]) - write(fileID, "\tEffect\tReliability\n") - for j in 1:numgroups[i] - write(fileID, "\t") - write(fileID, adjustedtraits[counter - 1]) - write(fileID, "\t") - write(fileID, string(solutions[counter])) - write(fileID, "\t") - write(fileID, string(reliability[counter])) - write(fileID, "\n") - - global counter = counter + 1 - end - write(fileID, "\n") -end -write(fileID, "\n") - -# Expected breeding values -write(fileID, "Expected Breeding Values:\n") -write(fileID, "\tID\tEBV\tReliability\n") -for i in 1:numanimals - write(fileID, "\t") - write(fileID, data.id[i]) - write(fileID, "\t") - write(fileID, string(solutions[i+counter-1])) - write(fileID, "\t") - write(fileID, string(reliability[i+counter-1])) - write(fileID, "\n") -end - -write(fileID, "\n - END REPORT -") -close(fileID) - -print("Done!\n") +#!/bin/bash +#= +exec julia --project=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) "${BASH_SOURCE[0]}" "$@" +=# +# beefblup +# Main script for performing single-variate BLUP to find beef cattle +# breeding values +# Usage: julia beefblup.jl +# (C) 2020 Thomas A. Christensen II +# Licensed under BSD-3-Clause License +# cSpell:includeRegExp #.* +# cSpell:includeRegExp ("""|''')[^\1]*\1 + +# Import the required packages +using CSV +using DataFrames +using LinearAlgebra +using Dates +using Gtk + +# Display stuff +println("beefblup v 0.1") +println("(C) 2020 Thomas A. Christensen II") +println("https://github.com/millironx/beefblup") +print("\n") + +### Prompt User +# Ask for an input spreadsheet +path = open_dialog_native( + "Select a beefblup worksheet", + GtkNullContainer(), + ("*.csv", GtkFileFilter("*.csv", name="beefblup worksheet")) +) + +# Ask for an output text filename +savepath = save_dialog_native( + "Save your beefblup results", + GtkNullContainer(), + (GtkFileFilter("*.txt", name="Results file"), + "*.txt") +) + +# Ask for heritability +print("What is the heritability for this trait?> ") +h2 = parse(Float64, readline(stdin)) + +### Import input filename +print("[馃惍]: Importing data file...") + +# Import data from a suitable spreadsheet +data = CSV.File(path) |> DataFrame + +print("Done!\n") + +### Process input file +print("[馃惍]: Processing and formatting data...") + +# Sort the array by date +sort!(data, :birthdate) + +# Define fields to hold id values for animals and their parents +numanimals = length(data.id) + +# Find the index values for animals and their parents +dam = indexin(data.dam, data.id) +sire = indexin(data.sire, data.id) + +# Extract all of the fixed effects +fixedfx = select(data, Not([:id, :birthdate, :sire, :dam]))[:,1:end-1] + +# Find any columns that need to be deleted +for i in 1:ncol(fixedfx) + if length(unique(fixedfx[:,i])) <= 1 + colname = names(fixedfx)[i] + print("Column '") + print(colname) + print("' does not have any unique animals and will be removed from this analysis\n") + deletecols!(fixedfx,i) + end +end + +# Determine how many contemporary groups there are +numtraits = ncol(fixedfx) +numgroups = ones(1, numtraits) +for i in 1:numtraits + numgroups[i] = length(unique(fixedfx[:,i])) +end + +# If there are more groups than animals, then the analysis cannot continue +if sum(numgroups) >= numanimals + println("There are more contemporary groups than animals. The analysis will + now abort.") + exit() +end + +# Define a "normal" animal as one of the last in the groups, provided that +# all traits do not have null values +normal = Array{String}(undef,1,numtraits) +for i in 1:numtraits + for j in numanimals:-1:1 + if !ismissing(fixedfx[j,i]) + normal[i] = string(fixedfx[j,i]) + break + end + end +end + +print("Done!\n") + +### Create the fixed-effect matrix +print("[馃惍]: Creating the fixed-effect matrix...") + +# Form the fixed-effect matrix +X = zeros(Int8, numanimals, floor(Int,sum(numgroups))-length(numgroups)+1) +X[:,1] = ones(Int8, 1, numanimals) + +# Create an external counter that will increment through both loops +counter = 2 + +# Store the traits in a string array +adjustedtraits = +Array{String}(undef,floor(Int,sum(numgroups))-length(numgroups)) +# Iterate through each group +for i in 1:length(normal) + # Find the traits that are present in this trait + localdata = string.(fixedfx[:,i]) + traits = unique(localdata) + # Remove the normal version from the analysis + effecttraits = traits[findall(x -> x != normal[i], traits)] + # Iterate inside of the group + for j in 1:(length(effecttraits) - 1) + matchedindex = findall(x -> x != effecttraits[j], localdata) + X[matchedindex, counter] .= 1 + # Add this trait to the string + adjustedtraits[counter - 1] = traits[j] + # Increment the big counter + global counter = counter + 1 + end +end + +print("Done!\n") + +### Additive relationship matrix +print("[馃惍]: Creating additive relationship matrix...") + +# Create an empty matrix for the additive relationship matrix +A = zeros(numanimals, numanimals) + +# Create the additive relationship matrix by the FORTRAN method presented by +# Henderson +for i in 1:numanimals + if !isnothing(dam[i]) && !isnothing(sire[i]) + for j in 1:(i-1) + A[j,i] = 0.5*(A[j,sire[i]] + A[j,dam[i]]) + A[i,j] = A[j,i] + end + A[i,i] = 1 + 0.5*A[sire[i], dam[i]] + elseif !isnothing(dam[i]) && isnothing(sire[i]) + for j in 1:(i-1) + A[j,i] = 0.5*A[j,dam[i]] + A[i,j] = A[j,i] + end + A[i,i] = 1 + elseif isnothing(dam[i]) && !isnothing(sire[i]) + for j in 1:(i-1) + A[j,i] = 0.5*A[j,sire[i]] + A[i,j] = A[j,i] + end + A[i,i] = 1 + else + for j in 1:(i-1) + A[j,i] = 0 + A[i,j] = 0 + end + A[i,i] = 1 + end +end + +print("Done!\n") + +### Perform BLUP +print("[馃惍]: Solving the mixed-model equations...") + +# Extract the observed data +Y = convert(Array{Float64}, data[:,end]) + +# The random effects matrix +Z = Matrix{Int}(I, numanimals, numanimals) + +# Remove items where there is no data +nullobs = findall(isnothing, Y) +Z[nullobs, nullobs] .= 0 + +# Calculate heritability +位 = (1-h2)/h2 + +# Use the mixed-model equations +MME = [X'*X X'*Z; Z'*X (Z'*Z)+(inv(A).*位)] +MMY = [X'*Y; Z'*Y] +solutions = MME\MMY + +# Find the accuracies +diaginv = diag(inv(MME)) +reliability = ones(Float64, length(diaginv)) - diaginv.*位 + +print("Done!\n") + +### Output the results +print("[馃惍]: Saving results...") + +# Find how many traits we found BLUE for +numgroups = numgroups .- 1 + +# Start printing results to output +fileID = open(savepath, "w") +write(fileID, "beefblup Results Report\n") +write(fileID, "Produced using beefblup for Julia (") +write(fileID, "https://github.com/millironx/beefblup") +write(fileID, ")\n\n") +write(fileID, "Input:\t") +write(fileID, path) +write(fileID, "\nAnalysis performed:\t") +write(fileID, string(Dates.today())) +write(fileID, "\nTrait examined:\t") +write(fileID, headers[5]) +write(fileID, "\n\n") + +# Print base population stats +write(fileID, "Base Population:\n") +for i in 1:length(numgroups) + write(fileID, "\t") + write(fileID, headers[i+5]) + write(fileID, ":\t") + write(fileID, normal[i]) + write(fileID, "\n") +end +write(fileID, "\tMean ") +write(fileID, headers[5]) +write(fileID, ":\t") +write(fileID, string(solutions[1])) +write(fileID, "\n\n") + +# Contemporary group adjustments +counter = 2 +write(fileID, "Contemporary Group Effects:\n") +for i in 1:length(numgroups) + write(fileID, "\t") + write(fileID, headers[i+5]) + write(fileID, "\tEffect\tReliability\n") + for j in 1:numgroups[i] + write(fileID, "\t") + write(fileID, adjustedtraits[counter - 1]) + write(fileID, "\t") + write(fileID, string(solutions[counter])) + write(fileID, "\t") + write(fileID, string(reliability[counter])) + write(fileID, "\n") + + global counter = counter + 1 + end + write(fileID, "\n") +end +write(fileID, "\n") + +# Expected breeding values +write(fileID, "Expected Breeding Values:\n") +write(fileID, "\tID\tEBV\tReliability\n") +for i in 1:numanimals + write(fileID, "\t") + write(fileID, data.id[i]) + write(fileID, "\t") + write(fileID, string(solutions[i+counter-1])) + write(fileID, "\t") + write(fileID, string(reliability[i+counter-1])) + write(fileID, "\n") +end + +write(fileID, "\n - END REPORT -") +close(fileID) + +print("Done!\n") From 2882f5b932f9530ee3be83d53521b28d8461eff3 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 12:36:38 -0500 Subject: [PATCH 09/13] Fix equation solving step --- src/beefblup.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/beefblup.jl b/src/beefblup.jl index 68f8848..406c0e5 100755 --- a/src/beefblup.jl +++ b/src/beefblup.jl @@ -48,7 +48,7 @@ h2 = parse(Float64, readline(stdin)) print("[馃惍]: Importing data file...") # Import data from a suitable spreadsheet -data = CSV.File(path) |> DataFrame +data = DataFrame(CSV.File(path)) print("Done!\n") @@ -128,8 +128,8 @@ for i in 1:length(normal) # Remove the normal version from the analysis effecttraits = traits[findall(x -> x != normal[i], traits)] # Iterate inside of the group - for j in 1:(length(effecttraits) - 1) - matchedindex = findall(x -> x != effecttraits[j], localdata) + for j in 1:(length(effecttraits)) + matchedindex = findall(x -> x == effecttraits[j], localdata) X[matchedindex, counter] .= 1 # Add this trait to the string adjustedtraits[counter - 1] = traits[j] From 74683f03d5fd47f5a48f8d8601cab8994067b97a Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 12:46:01 -0500 Subject: [PATCH 10/13] Fix report generation --- src/beefblup.jl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/beefblup.jl b/src/beefblup.jl index 406c0e5..c9cc600 100755 --- a/src/beefblup.jl +++ b/src/beefblup.jl @@ -211,6 +211,10 @@ print("[馃惍]: Saving results...") # Find how many traits we found BLUE for numgroups = numgroups .- 1 +# Extract the names of the traits +fixedfxnames = names(fixedfx) +traitname = names(data)[end] + # Start printing results to output fileID = open(savepath, "w") write(fileID, "beefblup Results Report\n") @@ -222,20 +226,20 @@ write(fileID, path) write(fileID, "\nAnalysis performed:\t") write(fileID, string(Dates.today())) write(fileID, "\nTrait examined:\t") -write(fileID, headers[5]) +write(fileID, traitname) write(fileID, "\n\n") # Print base population stats write(fileID, "Base Population:\n") -for i in 1:length(numgroups) +for i in 1:length(normal) write(fileID, "\t") - write(fileID, headers[i+5]) + write(fileID, fixedfxnames[i]) write(fileID, ":\t") write(fileID, normal[i]) write(fileID, "\n") end write(fileID, "\tMean ") -write(fileID, headers[5]) +write(fileID, traitname) write(fileID, ":\t") write(fileID, string(solutions[1])) write(fileID, "\n\n") @@ -245,7 +249,7 @@ counter = 2 write(fileID, "Contemporary Group Effects:\n") for i in 1:length(numgroups) write(fileID, "\t") - write(fileID, headers[i+5]) + write(fileID, fixedfxnames[i]) write(fileID, "\tEffect\tReliability\n") for j in 1:numgroups[i] write(fileID, "\t") @@ -267,7 +271,7 @@ write(fileID, "Expected Breeding Values:\n") write(fileID, "\tID\tEBV\tReliability\n") for i in 1:numanimals write(fileID, "\t") - write(fileID, data.id[i]) + write(fileID, string(data.id[i])) write(fileID, "\t") write(fileID, string(solutions[i+counter-1])) write(fileID, "\t") From 92e03a95efb08aa7056a5f6f14adfaedf7433ff1 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 12:54:55 -0500 Subject: [PATCH 11/13] Add package info to project manifest --- Project.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Project.toml b/Project.toml index d2886bb..df49e6c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,3 +1,8 @@ +name = "beefblup" +uuid = "03993faf-e476-444a-86c9-f31e8122fa24" +authors = ["Thomas A. Christensen II <25492070+MillironX@users.noreply.github.com>"] +version = "0.2.0" + [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" From 0de4b7dc0ca55bf7ed9fcdfd550ed9490f84d1d7 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 13:26:16 -0500 Subject: [PATCH 12/13] Remove package declaration from project manifest This was a test for an easier install of beefblup, but installing from the manifest makes Julia run the script instead of install the dependencies. API rewrite is slated for v0.3, so this item will be taken care of then. --- Project.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Project.toml b/Project.toml index df49e6c..d2886bb 100644 --- a/Project.toml +++ b/Project.toml @@ -1,8 +1,3 @@ -name = "beefblup" -uuid = "03993faf-e476-444a-86c9-f31e8122fa24" -authors = ["Thomas A. Christensen II <25492070+MillironX@users.noreply.github.com>"] -version = "0.2.0" - [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" From 6573492bac8f4f639eff79f511137e51618d8713 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 18 Jun 2021 13:26:35 -0500 Subject: [PATCH 13/13] Change copyright/release numbers --- README.md | 77 ++++++++++++++++++++++++------------------------- src/beefblup.jl | 8 ++--- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 6faef51..38996bd 100644 --- a/README.md +++ b/README.md @@ -13,35 +13,34 @@ Why? It's part of my effort to ## Installation 1. [Download and install Julia](https://julialang.org/downloads/platform/) -2. Open a new Julia window and type the `]` key -3. Type `add XLSX Gtk` and press **Enter** - -Alternatively, you can run the [install -script](https://github.com/MillironX/beefblup/raw/master/Julia/install.jl) from -Julia. +2. Download the [beefblup ZIP + file](https://github.com/MillironX/beefblup/archive/refs/tags/v0.2.zip) and unzip it someplace memorable +3. In your file explorer, copy the address of the "beefblup" folder +4. Launch Julia +5. Type `cd(" **Note:** beefblup and [Juno](https://junolab.org)/[Julia Pro](https://juliacomputing.com/products/juliapro.html) currently [don't get along](https://github.com/JunoLab/Juno.jl/issues/118). -> Although it's tempting to just open up beefblup in Juno and press the big play -> button, it won't work. Follow these instructions until it's fixed. If you -> don't know what Juno is: ignore this message. - -1. Download the [beefblup ZIP - file](https://github.com/MillironX/beefblup/archive/v0.1.zip) and unzip it - someplace memorable -2. Make a copy of the "Master BLUP Worksheet" and replace the sample data with your own -3. If you wish to add more contemporary group traits to your analysis, replace - or add them to the right of the Purple section -4. Save and close -5. In your file explorer, copy the address of the "Julia" folder -6. Launch Julia -7. Type `cd("