Skip to content

Combine replicates with a mean

In this lesson you will record two more FTIR measurements of the same film and combine all three replicates into one mean. By the end the mean will carry its own list of exact input versions, so you will not have to reconstruct its membership from a notebook. Allow about five minutes.

This carries on from Find a result and fix a mistake: the corrected first replicate is already in your session, and it becomes the mean’s first member. You need only the base package.

A replicate is a repeat measurement of the same sample under comparable conditions. A mean is a useful summary, but it cannot replace the measurements that went into it. Here each record call commits one run and its initial checkpoint. Two more recordings join the corrected first replicate, giving you three separate results before you combine anything.

A small helper builds each synthetic replicate on the shared axis, with a slightly different peak height. That makes it easy to see what is being averaged: absorbance values at corresponding wavenumbers, not the coordinates themselves.

Tell the mean which fields have which roles

Section titled “Tell the mean which fields have which roles”

sample.aggregate needs both axis and value. These arguments make your intention explicit: wavenumber locates each point, and absorbance supplies the values to average. MTRL does not infer those roles from a column’s spelling.

The members list is equally deliberate. It holds the three handles — corrected from lesson 5 and the two new ones — in the order you want recorded as inputs. Passing that list to aggregate commits the mean for you; there is no detached candidate to add afterwards.

units = {"wavenumber": "cm^-1", "absorbance": "a.u."}
def replicate_spectrum(scale):
data = np.empty(axis.size, dtype=[("wavenumber", "<f8"), ("absorbance", "<f8")])
data["wavenumber"] = axis
data["absorbance"] = 0.015 * (axis - 2000.0) / 1000.0
data["absorbance"] += scale * 0.6 * np.exp(-((axis - 2450.0) / 60.0) ** 2)
return data
replicate_02 = sample.record(replicate_spectrum(0.95), name="ftir-replicate-02", units=units)
replicate_03 = sample.record(replicate_spectrum(1.05), name="ftir-replicate-03", units=units)
members = [corrected, replicate_02, replicate_03]
before = len(sample.core.revisions)
mean = sample.aggregate(
members,
axis="wavenumber",
value="absorbance",
method="mean",
name="mean-spectrum",
)
print(f"mean peak: {round(float(mean.to_numpy()['absorbance'].max()), 5)}")
print(f"exact inputs: {len(mean.checkpoint.lineage.input_refs)}")
print(f"revisions added: {len(sample.core.revisions) - before}")
print(f"second replicate peak: {round(float(replicate_02.to_numpy()['absorbance'].max()), 5)}")
mean peak: 0.60675
exact inputs: 3
revisions added: 1
second replicate peak: 0.57675

You have a mean peak, three exact input references and one new revision. The final value comes from the second replicate, which remains unchanged by the combination.

Before using this route with real measurements, check that a point-by-point combination is what you intend. The inputs must be active handles from this facade, with compatible units and axes that match exactly after the core’s shared-range trimming. The facade accepts two-field records here; it refuses unrelated extra columns rather than silently copying them from the first member.

Two recording calls added two measurement revisions. One aggregate call added a result revision and a new aggregated run. The mean’s lineage names the exact member records in your supplied order, while the measured values stay intact.

That list is more precise than “the mean of the film spectra”. If you later correct a member, this already committed mean still refers to the version it actually used. You can make a new mean from the corrected inputs without changing the old result’s story.

Get your data out exports this mean for other tools while keeping the complete archive for anyone who needs to inspect how you obtained it.