Calculating The Average Time Of Day Using The Circular Mean And Mellifera
A few weeks ago I was wondering what the average time of day was when making Git commits on my personal programming projects. Some of these projects, such as Sunder, have been primarily developed in my free time during the evenings after working hours at my then full-time job, while other projects, such as Mellifera, have been primarily developed during the day while working at my current part-time job with more flexible hours. It would be interesting to see if the actual commit times to these projects matched my intuition about when I felt like I was working on those projects, so I wrote a Mellifera program to calculate just that.
Every time that you make a commit with
git commit,
the current time is stored as the author date within the commit. You can check
this author date with
git log
using:
~/sources/mellifera$ git log -1 --format=fuller | head -n 5
commit 1e6f5cb68ac3a06b3572f15a1f47fd201d2a6da3
Author: ashn <me@ashn.dev>
AuthorDate: Tue Jun 30 09:44:30 2026 -0400
Commit: ashn <me@ashn.dev>
CommitDate: Tue Jun 30 09:44:30 2026 -0400
In this case, the most recent commit to Mellifera occurred on Tuesday June 30th
at 9:44 AM. This is kind of a nightmare to parse, but using the
%aI format specifier, we can have git log
spit out this time in an ISO 8601 date and time with UTC offset:
~/sources/mellifera$ git log -1 --format='%aI'
2026-06-30T09:44:30-04:00
And if we do that for the full log, we can get a list of these ISO 8601 date and times as a list with one datetime per line:
~/sources/mellifera$ git log --format='%aI' | head -n10 # 10 commits for brevity
2026-06-30T09:44:30-04:00
2026-06-29T02:02:34-04:00
2026-06-25T15:30:42-04:00
2026-06-24T20:37:15-04:00
2026-06-24T18:19:18-04:00
2026-06-22T11:27:11-04:00
2026-06-22T10:35:51-04:00
2026-06-19T07:16:22-04:00
2026-06-17T14:21:44-04:00
2026-06-11T11:33:28-04:00
Normalizing these times to UTC is also relatively easy, since the UTC offset is included in the datetime string:
Okay, so we have all of these datetimes normalized and just need to average them. Simple right? Well not so fast, as there is a hidden gotcha here. Imagine for a moment that we only have two datetimes to average: one at 11 PM on some day and another at 1 AM the next day. Intuitively, we know that the average time between 11 PM and 1 AM is midnight (00:00:00 in 24 hour time), as midnight sits right between 11 and 1 on a clock face.

However, if we use the normal formula for average, average =
sum(values)/count(values), then we will end up with an average time of
12 o'clock noon (12:00:00) instead of 12 o'clock midnight (00:00:00)!
This occurs because the normal formula for average, known as the arithmetic mean, only works for numbers in a non-modular number system, such as the real numbers. Our measurements, the time of day, occur as part of a repeating 24 hour cycle, which makes the arithmetic mean practically meaningless (pun-intended).
Thankfully, this is already a solved problem. For modular number systems, such as a 24 hour clock, we can use the circular mean, which works as an average for this kind of cyclic data. The algorithm for the circular mean is as follows:
- Scale HMS data into the number of seconds since midnight.
- Map the seconds since midnight data onto the range
[0, 2π]. - Take the
cosineandsineof each measurement to get thexandyCartesian coordinates. - Find the arithmetic mean of the
xandycoordinates for all those measurements. - Transform the average
xandycoordinates back into a radial value withatan2. - Map the radial value in the range
[0, 2π]back into the seconds since midnight.
As code, this algorithm takes the following form, which we can see correctly produces the midnight (00:00:00 UTC) result for the circular mean of 11 PM and 1 AM:
And if we plug back in the original 10 time measurements from the Mellifera
repository's git log, then we get a convincing looking 16:30
UTC (i.e. 12:30 EDT) for the average commit time.
I keep a version of this script with the debugging lines commented out in my
personal ~/bin directory as
average-commit-time.mf.
I also keep a driver shell script in that same directory as
average-commit-time.
Looking at some of my existing projects, we can see that Mellifera has an average commit time of ~18:30 UTC (2:30 PM EDT), Sunder has an average commit time of almost midnight UTC (8:00 PM EDT), and this website has an average commit time of ~20:00 UTC (4:00 PM EDT).
$ average-commit-time ~/sources/mellifera
18:26:59 UTC
$ average-commit-time ~/sources/sunder
23:50:33 UTC
$ average-commit-time ~/sources/site
20:01:18 UTC
This matches my original hypothesis that most Mellifera commits happened mid-day, and that most Sunder commits happened in my free time after work. It is also neat to see that updates to this website's repository average around 4:00 PM EDT. That repository contains a bunch of extra junk, including miscellaneous ramblings, TODO notes, and a bunch of random one-off scripts, all of which I chuck into the repo at seemingly arbitrary times of the day. So it is neat to see that this averages out to mid-afternoon for commits. 🤷
This was a fun exercise, but I should note that this script does have some limitations. For one, a repository with a bimodal distribution of commit times will suffer the same averaging smoothing with the circular mean as bimodal non-modular data does with the arithmetic mean, smearing the average to somewhere in between the two clusters of times. This might actually be what is going on with the average time for this website's repository, as a lot of updates tend to happen in the early morning and late at night! Additionally, this script normalizes times to UTC, but for someone like me who lives in a region that changes UTC offset during daylight savings, the "perceived local time" can be jiggled around by an hour. Since commits to a project repository can come from different committers in different time zones all over the world, I wanted to use UTC for this script, but one could imagine dropping the UTC offset here to get an average local time instead.
In any case, it was cool getting to use Mellifera for some ad-hoc scripting, and I enjoyed learning about the circular mean. While doing research on this project, I found most of the information I needed on the Wikipedia page for the circular mean, but I also happened to find this succinct answer from user starblue on StackOverflow, “Compute unit vectors from the angles and take the angle of their average.” which I thought was really intuitive. Finally, there is also this really detailed answer on the Math Stack Exchange with an accompanying video which goes into way more depth than I have the capacity to understand, but is still really interesting to watch!