I'm writing a program that is going to analyse some data imported from an Excel spreadsheet. It is currently running quite slow (couple seconds on an average machine) and I was wondering whether it could be sped up in any way.
There are three classes containing the data.
This first class contains experimental runs. The data may be excluded in the calculation of sample mean, which is why it has the isIncludedInMean
property.
namespace PCR_Analysis
{
/// <summary>
/// Contains a single experimental run
/// </summary>
public class Experiment
{
public double cT; // cT measured value
public bool isIncludedInMean; // included in mean calculations?
public Experiment(string cTinput)
{
// Try render input string as double, otherwise set as NaN
if (!double.TryParse(cTinput, out cT))
{
cT = double.NaN;
isIncludedInMean = false; // ignore non-values
}
else
{
isIncludedInMean = true;
}
}
// Flip isIncludedInMean if cT is a number
public void flipIsIncludedInMean()
{
if (!double.IsNaN(cT))
{
isIncludedInMean = !isIncludedInMean;
}
}
}
}
Multiple (usually three) experiments are included in a single sample, which also contains the mean of the experiments, as well as the sample's name:
using System.Collections.Generic;
using System.Linq;
namespace PCR_Analysis.Classes
{
public class Sample
{
public string name { get; } // sample name
public double cTmean; // sample mean
public List<Experiment> experimentRepeats = new List<Experiment>();
// Set name when generated
public Sample(string inputName)
{
name = inputName;
cTmean = double.NaN;
}
// Add Experiment to list
public void AddExperiment(string cTinput)
{
Experiment tempExperiment = new Experiment(cTinput);
experimentRepeats.Add(tempExperiment);
}
// Calculate mean
public void CalculateCTMean()
{
// Define list of non-ignored values
List<Experiment> meanExperiments = experimentRepeats.
Where(x => x.isIncludedInMean).
ToList();
// Calculate mean if list is non-empty
if (meanExperiments.Count > 0)
{
double sum = meanExperiments.Sum(x => x.cT);
cTmean = sum / meanExperiments.Count;
}
}
}
}
There will be multiple samples contained within a particular gene experimental run:
using System.Collections.Generic;
namespace PCR_Analysis.Classes
{
public class Gene
{
public string name { get; } // Gene name
public List<Sample> samples = new List<Sample>(); // Sample list
// Set name when generated
public Gene(string inputName)
{
name = inputName;
}
// Add sample to list if it doesn't exist already
public void AddSample(string sampleName)
{
if (!samples.Exists(x => x.name == sampleName))
{
Sample tempSample = new Sample(sampleName);
samples.Add(tempSample);
}
}
}
}
Finally, there is a class to import the data from an excel spreadsheet into these structures. There are some magic numbers at the beginning responsible for the input format, but these will stay constant (the output from the experimental machine is the same all the time).
using System;
using System.Linq;
using Excel = Microsoft.Office.Interop.Excel;
using System.Collections.Generic;
namespace PCR_Analysis.Classes
{
public class DataImport
{
// Global variables
// Corner cells of the inspected range containing values
private static string topLeftCell = "B9";
private static string bottomRightCell = "G1000";
// Columns for particular data types w.r.t above range
// i.e. topLeftCell is column 1
private static int geneColumn = 2;
private static int sampleColumn = 1;
private static int cTcolumn = 6;
/// <summary>
/// Opens an Excel workbook as read only. Updates links automatically
/// </summary>
static public Excel.Workbook OpenWorksheetAsReadOnly(string filePath)
{
// Initialise application
Excel.Application excelInstance = null;
// Open workbook as readOnly, update links
excelInstance = new Excel.Application();
Excel.Workbook excelWorkbook = excelInstance.Workbooks.
Open(filePath, true, true);
// Return opened workbook
return excelWorkbook;
}
/// <summary>
/// Imports data from Excel workbook into a two-dimensional array
/// </summary>
static public object[,] ImportExcelIntoArray(string filePath)
{
// Initialise and open file
Excel.Workbook source = null;
source = OpenWorksheetAsReadOnly(filePath);
// Load range and convert values to object
string importRange = topLeftCell + ":" + bottomRightCell;
Excel.Range sourceRange = source.Sheets[1].Range(importRange);
object[,] sourceValues = (object[,])sourceRange.Value2;
// Return
return sourceValues;
}
static public List<Gene> ConvertToGeneList(string filePath)
{
// Load file, initialise list
object[,] input = ImportExcelIntoArray(filePath);
List<Gene> output = new List<Gene>();
// Initialise content variables
string sampleName, geneName, cTstring;
// Count the number of non-(empty or null) gene cells
int rowCount = Enumerable.Range(1, input.GetLength(0))
.Count(row => !String.IsNullOrEmpty(
Convert.ToString(input[row, 1])));
// Iterate through all rows
for (int i = 1; i <= rowCount; i++)
{
// Assign variables
sampleName = Convert.ToString(input[i, sampleColumn]);
geneName = Convert.ToString(input[i, geneColumn]);
cTstring = Convert.ToString(input[i, cTcolumn]);
// Add a gene to list if it doesn't exist and return its index
if (!output.Exists(g => g.name == geneName))
{
output.Add(new Gene(geneName));
}
int geneIndex = output.FindIndex(g => g.name == geneName);
// Add a sample to gene if it doesn't exist and return its index
output[geneIndex].AddSample(sampleName);
int sampleIndex = output[geneIndex].samples
.FindIndex(s => s.name == sampleName);
// Add cT value to sample
output[geneIndex].samples[sampleIndex].AddExperiment(cTstring);
}
return output;
}
}
}
To me it feels like the import could be improved somehow - the files take a while to open and the "check gene, check sample" iterates over the gene list and the sample list within that gene twice for every row analysed (first to see if it exists, then to return its index) - but I don't know how a speed-up/tidying up could be achieved.