IRIS Makes a Grand Entrance
Let me take you back to October 11, 2024. Me, my wife, and a hospital labour room buzzing with nerves and hope. My little girl Iris was on her way, my wife was powering through contractions, and somewhere in the middle of it all we had this wild idea to build something, because apart from the contractions there wasn't much else to do. So I got my laptop out between hand-squeezes and wrote out some code to track them. It's not the slickest thing I've ever written, and by any technical criterion it's ordinary, but it is, without any competition, THE best project I've ever done, which tells you something about how badly "best" correlates with technical merit. Someday Iris might read this and get a kick out of the fact that her dad and mom were writing numbers down while she was making her grand entrance.
Here's how it started. We're in the labour room, my wife's gripping my hand, and the doctors ask us to time the contractions, how long each one lasts, how far apart they are. Which is, if you strip the situation down to what it actually is, a data acquisition task. Two variables, duration and interval, sampled irregularly, with the trend mattering more than any individual reading. I'd been jotting times into my phone, which is a terrible instrument for exactly this reason, it captures points and hides trends, and the whole clinical value of contraction timing is in the trend. So between reassuring my wife and pacing the room, I built a contraction tracker, start times, durations, frequencies, all plotted out. I'll be honest about the dual purpose here. It gave the doctors cleaner data than my phone notes ever would, and it gave a nervous father something to do with his hands. I won't pretend the second function wasn't doing at least half the work.
The raw material was a list of timestamps, 66 of them, starting at 13:03:15 on October 11, 2024 and running past 17:56, plus one outlier at 21:31 that I trimmed later, because one point four hours after a continuous series ends is noise, whatever it was I actually recorded there. Each timestamp marks the moment a contraction kicked off, captured to microsecond precision, which deserves a comment. The measurement uncertainty here is me, a human with a shaking hand deciding when a contraction "started" based on my wife's face, an error easily on the order of seconds. Recording microseconds against that is precision theatre, six decimal places of confidence describing a one-second guess. I did it anyway. It felt right at the time, and the timestamps came free from the system clock. Alongside the starts, durations, how long each one lasted in seconds, from a quick 2.4 to a hefty 244. And frequencies, the interval between consecutive starts, calculated as
where is the start time of contraction . The first entry gets None, since there's no "before" to difference against. I dumped all of this into a dictionary and turned it into a pandas DataFrame, df, with Start Time as the index, parsed as datetimes with pd.to_datetime.
There are no clever equations in this project, and that's a design decision, not a shortcut. The question the room needed answered was "is labour accelerating," and that question is answered by sums and differences, so sums and differences are what got built. Duration is straight seconds, how long my wife felt each squeeze. Frequency is the gap between starts, so for contraction ,
Raw event data with irregular sampling is hard to read by eye, the individual points carry too much scatter, so to separate the trend from the noise I grouped the contractions into 10-minute buckets, which is just flooring each timestamp to the nearest 10 minutes, if you're working in seconds from a base time, though pandas does it in one line with index.floor('10T'). Then a count per bucket:
Binning is the crudest smoothing filter there is, and it was exactly the right tool, because a rate, contractions per unit time, is far more legible to a bleary-eyed brain at 16:00 than a sequence of raw intervals.
The code itself is pandas for the wrangling and matplotlib for the graphs. Load the data into a DataFrame, set the index, plot three things:
fig, ax = plt.subplots(3, 1, figsize=(10, 10))
ax[0].plot(df.index, df['Duration (seconds)'], marker='o', linestyle='-', color='b')
ax[1].plot(df.index, df['Frequency (seconds)'], marker='o', linestyle='-', color='g')
ax[2].bar(contractions_per_10min.index, contractions_per_10min, width=0.008, color='r')
Then a fourth plot to show each contraction's actual span in time:
ax[3].scatter(df.index, [1] * len(df), color='b')
for i, row in df.iterrows():
ax[3].hlines(y=1, xmin=i, xmax=i + pd.to_timedelta(row['Duration (seconds)'], unit='s'), color='b')
It's rough, written fast between contractions, and it worked. The first three plots gave me duration over time, frequency over time, and the bucketed count. The fourth was a timeline, dots for starts, horizontal lines for durations, with the x-axis stretched via set_xlim to fit the longest event. That fourth plot is worth defending on its own, because it's the only one that shows duration and frequency in the same visual field, and the thing you're actually watching for in labour, gaps closing while events lengthen, is a relationship between the two variables, not a property of either one. Not elegant. Entirely fit for purpose.
Here's what came out:

The top plot, duration, is a blue line with dots marking each contraction's length in seconds. It opens around 49, dips to 2.4 (a blip, almost certainly my recording error rather than physiology), then climbs through the afternoon, some passing 200. Noisy, no clean monotonic rise, which is what real biological data looks like as opposed to the textbook curve. The second plot, frequency in green, is where the signal lives. Early on the intervals scatter wildly, 98 seconds, then 371, back to 51, but the envelope tightens steadily through the afternoon, with gaps as short as 25 seconds late on. The variance collapsing is as informative as the mean falling. The third plot, red bars, is the bucketed rate, 2 to 3 contractions per 10 minutes early, peaking at 5 to 6, then easing. And the bottom plot, the timeline, integrates it all at a glance, dots bunching while lines stretch.
Read together, the 66 events from 13:03 to 17:56 tell one story from four angles. Early labour, durations bouncing in the 40 to 60 second band, intervals scattered anywhere between 50 and 370 seconds. By 15:00 to 16:00, intervals dropping below 200 seconds more often, durations pushing past 100, one reaching 244. Rate peaking around 16:00 to 17:00 at 6 per bucket. Two variables moving in opposite directions at increasing speed, intervals compressing, durations extending, and when both derivatives point the same way like that, the conclusion doesn't require statistics. Iris was coming, and fast.
Could the analysis have been better? Trivially. A mean duration, , a variance, , some proper smoothing instead of crude binning. But every one of those would have answered a question nobody in the room was asking. The requirement was "is it accelerating," the plots answered it in under a second of looking, and adding rigour past the point where the decision is already made is engineering vanity, not engineering. I wrote it half panicked with my hands shaking a little, and it did precisely its job.
For Iris, someday: "hello Iris, this is your dad at one of his most stressed and happiest. It's not fancy, but it's yours. I tracked your mom's strength, and you arrived, perfect. Best project ever? Of course, because it's tied to you."
I've built a fair amount over the years, mathematical models of control systems for small satellites in geostationary orbit, aerodynamic lift capability in the Martian atmosphere, work that is objectively harder than differencing 66 timestamps. And yet ranking projects by difficulty has always been the wrong metric. The right metric is what the output was worth, and this one ran real-time, on live data, with my wife's courage as the input and Iris's first cry as the payoff, a result no orbital model of mine has ever come close to. Maybe Iris will tweak it one day, add some fancy new spin or whatever's fancy in 2040. For now, it's our little timestamped memory.