aamir.consulting logo
aamir.consulting math that moves the world in service of others
Piece 01

Anomaly Detection
for Space Agriculture

Sending anything into space, and losing it, is very expensive. Here is a machine learning model that tells you if a plant is stressed in space, in time to act quickly to save it. It also determines which metrics best predict that stress, so we only send up a lightweight set of hardware that best detects that stress.

Work done for the Plant Processing Area (PPA) at NASA’s Kennedy Space Center (KSC)

A crop sensor on the Moon On the lunar surface a sensor watches a tray of wilting plants and sets off a red alarm. An astronaut walks over and waters them, the plants recover, and the alarm clears. SCANNING STRESS HEALTHY
Sensor · alarm · water · recovered
Problem

The Problem

How do you spot a failing crop early, with hardware light enough to fly?

Losing a crop growout in orbit is expensive, and so is every gram of sensor you fly to see it coming.

Response

The Response

Dissect the machine learning model trained to detect stress to figure out which sensors to fly.

Train a model to spot a stressed plant from measurements of biomarkers, then read which biomarkers the model relied on most to classify a plant as stressed, and fly only the sensors that best detect those biomarkers.

Sneak peek From the 149 metrics measured, we select the six that most signal stress.
The metrics that best signal an anomaly: all 52 measurements ranked by importance, each with a 95 percent confidence interval; the six whose whole interval clears the line are drawn in teal, the rest in grey
Method

The Method

Adapt a machine learning model designed for cancer detection and direct it to detect plant stress.

The worked example below is a published cancer study. It works for space agriculture as well, because both problems need the same mathematical structure that the machine learning model implements to detect anomalies and explain the underlying biological mechanisms that produce those anomalies.

Framework
Metabolomics Workflow Tutorial 1 from the Centre for Integrative Metabolomics & Computational Biology (CIMCB), by Mendez, Broadhurst et al., 2019
Worked example
140 urine samples, 149 measurements each, one yes-or-no answer to find

This work was applied in the Plant Processing Area at Kennedy Space Center. The worked example on this page is not that data, it is the openly published CIMCB tutorial.

Step 01

Import the packages

Choosing the right tools to do the analysis

Four imports carry the whole workflow: numpy and pandas for the arrays and tables, train_test_split from scikit-learn to hold data back from the model, and cimcb_lite, which supplies the metabolomics-specific plots and the Partial Least Squares Discriminant Analysis (PLS-DA) model wrapper.

import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split

import cimcb_lite as cb

print('All packages successfully loaded')
All packages successfully loaded
Cell 01 If this cell prints, the tools are in place.
Step 02

Load the data sheet and the peak sheet

Organising the data so every measurement can be judged on its own merits

The data are nuclear magnetic resonance (NMR) spectroscopy measurements of urine, stored in an Excel workbook. That workbook must carry two sheets, one named Data and one named Peak.

# The path to the input file (Excel spreadsheet)
filename = 'GastricCancer_NMR.xlsx'

# Load Peak and Data tables into two variables
dataTable, peakTable = cb.utils.load_dataXL(filename, DataSheet='Data', PeakSheet='Peak')
Data Table & Peak Table is suitable.
TOTAL SAMPLES: 140    TOTAL PEAKS: 149
Cell 02 Both sheets are read in one call, which is why the workbook's sheet names are not negotiable.

2.1The data sheet

Each row represents a urine sample. The columns record the outcome for that sample (whether it is a quality-control (QC) sample, has gastric cancer, is a benign tumour, or is healthy), along with the measured concentration of every metabolite, the small molecules the body's chemistry produces. One hundred and forty rows: 43 gastric cancer, 40 benign, 40 healthy, and 17 pooled QC samples. A QC sample is the same pooled material measured again and again, so any change in its readings comes from the instrument, not the biology.

2.2The peak sheet

The peak sheet's rows describe the metabolites themselves, rather than the samples: their names, the percentage of samples for which a measurement is missing, and how much the readings of that metabolite vary across the repeated QC samples.

Step 03

Clean the peak sheet

Dropping the measurements too unreliable to justify a sensor

We clean the peak sheet by imposing two conditions on every metabolite:

A metabolite that fails either test is not evidence, it is noise with a name, and it is dropped before it can influence anything downstream.

# Create a clean peak table
rsd      = peakTable['QC_RSD']
percMiss = peakTable['Perc_missing']
peakTableClean = peakTable[(rsd < 20) & (percMiss < 10)]

print("Number of peaks remaining: {}".format(len(peakTableClean)))
Number of peaks remaining: 52
Cell 03 The two thresholds are explicit and in one place, so a programme can argue about them and change them without touching the rest of the pipeline.
Scatter of all 149 metabolites, variability across the quality-control repeats on the horizontal axis against percentage of missing values on the vertical; the 52 inside both thresholds are teal, the 97 outside are grey
Fig 01 The same two conditions, drawn. Every measurement in the study is a point; the shaded corner is the only place a measurement is allowed to survive. 149 measured, 52 kept. Of the 97 discarded, 70 failed on variability alone, one on missing values alone, and 26 on both. Everything after this runs on 52 measurements, not 149.
Step 04

Quality assessment by principal component analysis (PCA)

Preparing a data set worthy of training a machine learning model, and confirming the instrument can be trusted

We build the data matrix X by removing from the data sheet the metabolite columns that failed the cleaning conditions. A series of transformations then puts every measurement on a comparable footing, ready for PCA:

# Extract and scale the metabolite data from the dataTable
peaklist = peakTableClean['Name']
X        = dataTable[peaklist].values
Xlog     = np.log10(X)
Xscale   = cb.utils.scale(Xlog, method='auto')
Xknn     = cb.utils.knnimpute(Xscale, k=3)

print("Xknn: {} rows & {} columns".format(*Xknn.shape))
Xknn: 140 rows & 52 columns
Cell 04 Four named tables rather than one overwritten variable, so each stage can be inspected when a later plot looks wrong. 129 individual readings were missing and are filled in here.

PCA condenses the 52 measurements into a few summary directions called principal components (PCs). The first, PC1, captures the most variation between samples; PC2 captures the next most. Plotting every sample against PC1 and PC2 gives the score plot, and the first thing to look for in it is the QC samples.

Score plot of the first two principal components, with patient samples spread widely and quality-control pools clustered tightly, each with a 95 percent confidence ellipse
Fig 02 Score plot, PC1 against PC2. PC1 carries 39.4% of the variation and PC2 a further 7.3%. The QC pools are the same material measured repeatedly, so they should land on top of each other. Here they do, which means the spread among the patient samples is real and not the instrument wandering.

The score plot

To explain the score plot we need PCA more generally. Each sample is described by the 52 metabolite concentrations that survived cleaning. Plotting a sample therefore means placing a point in a coordinate system with 52 axes. Having plotted every sample there, we would like a simpler two-dimensional picture for analysis.

That simpler picture is what PCA provides. Once the data sits in 52 dimensions we can calculate its PCs: directions that account for the variation in the data, ordered by how much each explains: PC1 the most, PC2 the second most, and so on.

Taking PC1 and PC2 out of the full 52-axis system gives a plane. Projecting every sample point onto that plane produces the score plot.

X 140 samples × 52 measurements × w one weight per column = t one score per sample 52 numbers become one number
Fig AOne direction through the data, one number per sample.
52 dimensions of measured data PC 1 PC 2 the plane worth looking at
Fig BTwo of those directions span a plane inside the larger space.
PC 1 PC 2 one sample, 52 measurements the same sample, 2 numbers what is discarded
Fig CEvery sample drops onto that plane along the shortest path. What the arrow measures is what you agreed to lose.
The same move, in three dimensions Fifty-two axes cannot be drawn, but the operation can. A plane is chosen inside the larger space, and each data point drops onto it along the shortest path. Everything PCA does here is that picture, with 52 axes instead of three.

The loadings plot

PC1 can be viewed as the X axis and PC2 as the Y axis of a plane inside the full 52-axis system. The loadings plot shows what those two new axes are made of. Each point is one of the measured metabolites: its position on the X axis shows how much weight it carries on PC1, and its position on the Y axis how much it carries on PC2. The further a point sits from the origin, the more that measurement drives the differences between samples.

Loadings plot with each of the 52 metabolites as a point, the six furthest from the origin labelled by name
Fig 03 Where the variation comes from. The six measurements carrying the most weight are named outright; five of them sit in one tight group, so their labels are pulled out to the side. These are the first hints of which sensors will matter, before any model is trained.
Step 05

Univariate statistics, before anything is learned

Checking whether any single measurement already does the job on its own

This section compares gastric cancer against healthy controls one metabolite at a time. It runs before the machine learning deliberately: if a difference is visible in a single measurement, that is worth knowing before a model gets the credit for finding it.

Eleven of the 52 separate the two groups on a t-test. After correcting for the fact that we ran 52 tests and some will look significant by luck alone, seven survive. That is the bar the model has to beat to be worth its complexity.

Step 06

Machine learning: partial least squares discriminant analysis (PLS-DA)

Training a model that learns instead of memorising, and proving it is not luck

PLS-DA looks for the few directions through the 52 measurements that best separate two groups: here cancer and healthy; on a crop tray, stressed and healthy.

6.1Split the data into training and test sets

Before creating a model we split the data set into a training set and a test set, to avoid overfitting. Overfitting is when a model learns one particular data set too closely: its predictions fit that data well and fall apart when it meets anything new.

We split so that three quarters makes up the training set and one quarter the test set. The makeup of each is determined by stratified random selection, which means both sets end up with the same proportion of healthy samples to samples with gastric cancer.

# Split dataTable2 and Y into train and test (with stratification)
dataTrain, dataTest, Ytrain, Ytest = train_test_split(
    dataTable2, Y, test_size=0.25, stratify=Y, random_state=10)

print("DataTrain = {} samples with {} positive cases.".format(len(Ytrain), sum(Ytrain)))
print("DataTest  = {} samples with {} positive cases.".format(len(Ytest),  sum(Ytest)))
DataTrain = 62 samples with 32 positive cases.
DataTest  = 21 samples with 11 positive cases.
Cell 05 The 83 gastric cancer and healthy samples, split three to one. The random seed is fixed, so this split, and every figure after it, reproduces exactly.

6.2Find the optimal number of components

Now we need the right number of components (the directions the model builds) to train with. Too few and the model misses the signal; too many and it memorises the training data. To find the balance, we run k-fold cross-validation.

First we take the entire training set and have the program predict a value for each sample in it. Those predictions are compared against the known values, and the coefficient of determination (R²) measures how closely they agree. We calculate R² for a range of component counts, up to six here.

Second, we split the training rows into k subsets of equal size, each called a fold. Then we calculate Q², the same measure computed on data held out from training: k−1 folds train the model and the remaining fold is the one predictions are recorded on. The folds rotate, so every fold is held out exactly once. Here k = 5, so four folds train and one is predicted on.

Rather than assert the answer, here is the dial. Each key refits the model with that many components and redraws what it predicts. The readout also reports the area under the curve (AUC): a score from 0.5, which is guessing, to 1, which is perfect, for how well the model ranks one group above the other. Section 6.3 shows where it comes from.

Fig 04 · live Left, every training sample's predicted score against the 0.5 cut-off. Right, the fitted score (R²) in amber and the held-out score (Q²) in teal across all six counts. Turn it up and the left panel separates cleanly while the teal line falls away. That gap is the model memorising these particular 62 samples. Held-out performance actually peaks at three components; the tutorial takes two, because the gap between the two lines is narrower there. Both are defensible, which is the point.
Fitted and held-out coefficient of determination plotted against number of components, each with a bootstrap confidence band, and the gap between them marked at six components
Fig 05 The same decision as a static figure, with 95% bootstrap bands. R² keeps climbing because more components can always fit the training data better. Q² is the honest one, and it turns over: past that point, extra complexity buys memory, not insight.

6.3Train and evaluate the model

The program now trains the model on two components, which are called latent variables (LVs) once they are being used this way, and tests the accuracy of its predictions. The following visuals are how that performance is judged.

Three evaluation panels: predicted score by class as a violin plot, the two probability densities overlaid, and the receiver operating characteristic curve with a readout of the summary statistics
Fig 06 Evaluation on the training set at a cut-off of 0.5. AUC 0.97, accuracy 0.89, sensitivity 0.88, specificity 0.90.

Class vs predicted score. The distribution of the model's predictions for the two possible outcomes, 0 for healthy and 1 for cancerous. The line at 0.5 is the cut-off: a score above it is called a 1, below it a 0. That cut-off is set in one line of code and can be changed: 0.75, for instance, if a false alarm is more expensive than a missed case.

Predicted score vs density. How the predictions are distributed. One curve is the scores the model gave samples that were actually healthy; the other, samples that were actually cancerous. Where the curves overlap is where the model is unsure.

1−specificity vs sensitivity. Sensitivity is the share of true cases the model catches; specificity is the share of healthy samples it correctly clears. The receiver operating characteristic (ROC) curve plots one against the other for every possible cut-off between 0 and 1, and the AUC is the area under that curve.

6.4Permutation test

Now we attack the result. A permutation test shuffles which samples are labelled cancerous and which healthy, then builds, trains and tests a new model on the shuffled labels. The number of each label always matches the original; only which sample carries which label changes. If a model trained on nonsense labels scores as well as the real one, the real one learned nothing.

We run 100 shuffles and record R² and Q² for each. This test uses eight folds rather than five, so the real model's Q² reads slightly differently here than in 6.2.

One hundred shuffled relabellings plotted by their correlation with the true labels against the variance they explain, with the real model marked by stars far to the right and above the cloud
Fig 07 The test that decides whether the model learned the biology or learned the data set. The best that any shuffled relabelling managed on held-out data was 0.13, against the real model's 0.51. None of the hundred came close, so the p-value is below 0.01: less than a one-in-a-hundred chance that luck produced this result.

The real model should sit isolated, high and to the right, with the shuffled models clustered lower and to the left, where their labels are least like the real ones. That is what this graph shows. If shuffled models scored just as well, the measurements we trained on would not be meaningful for prediction in the first place.

6.5Latent variable projections

These graphs show how the two LVs work together to tell cancerous and healthy samples apart. Suppose LV1 accounted for 50% of the variation in the data and LV2 also accounted for 50%. That would not mean they account for 100% together, because the variation each one explains can overlap. This is how that relationship becomes visible.

Scatter of the training samples on the first two latent variables, healthy and cancerous drawn in different colours and separating along the diagonal
Fig 08 The two directions the model built, with the samples plotted on them. Separation runs along the diagonal rather than along either axis alone, which is why the model needs both and why neither one on its own would do.
Step 07

Which measurements are doing the work

Naming the measurements worth flying a sensor for

Everything up to here established that the model is real. This is what it was for: the model is now asked which of the 52 measurements it actually leaned on, and how sure we can be of that answer. Two independent checks answer it: the regression coefficient, which says how strongly each measurement pushes the prediction, and the variable importance in projection (VIP) score, which says how much each one contributes to the directions the model built. Each comes with a 95% confidence interval from 200 bootstrap resamples.

Every measurement's regression coefficient with a 95 percent bootstrap interval, sorted by magnitude, those whose interval crosses zero drawn in grey
Fig 09 The shortlist. A measurement whose whole interval sits on one side of zero reliably moves the prediction, and is drawn in amber. Twenty of 52 qualify. The grey ones cross zero: the data cannot even say which way they push, so they have not earned a place on anything.
The metrics that best signal an anomaly: variable importance in projection for every measurement with a 95 percent bootstrap interval, sorted by magnitude; the six whose whole interval sits above one are teal, the rest grey
Fig 10 The final cut. A VIP score above 1 marks a measurement as more important than average, and here we keep only those whose whole interval clears that line. Six of 52 qualify, and all six are also on the shortlist in Fig 09.
Why both Two criteria, applied independently, are stronger than either alone. A measurement that survives the coefficient test and VIP is a candidate worth spending bench time on; one that survives only a single test is a lead, not a finding. Here the two agree on all six.
Step 08

Test the model on data it has never seen

Proving the model holds up on samples it has never met

The model has not seen dataTest, which holds the measurements for the samples kept back during training, and it has not seen Ytest, the 0 or 1 classification for each of them. Testing on those samples returns the same visuals produced in 6.3, now with the held-out results alongside. The table adds the F1 score, a single number that balances how many true cases are caught against how many alarms are false.

Held-out samples by predicted score, the training and test receiver operating characteristic curves on the same axes, and a table comparing every summary statistic between train and test
Fig 11 Train and test on the same axes. Reading the gap between them is the point of the whole exercise: AUC holds up at 0.95 against 0.97, while accuracy drops from 0.89 to 0.76 on 21 samples. The ranking is sound; the precise accuracy figure is not something to quote without saying how few samples it rests on.
Sources

Workflow and further reading

the pipeline is published; so is the reasoning behind it

Next

Facing a costly decision with more data than clarity?

Bring me the decision and the data behind it. I will tell you what matters, what you can cut, and how confident you can be before you commit.

Start a conversation