This particular problem pops up in quite a few domains. (We often refer to it as "phase unwrapping" in the geosciences.) The approach here is a good one so long as your noise doesn't result in lots of mistaken "wrap-arounds".<p>However, it will fail badly in the presence of noise in many cases. It's particularly problematic when the slope changes (e.g. a polynomial) or where the slope is high and the noise is high. (Note that polynomials are still linear in the sense mentioned here: linear regression).<p>At any rate, this is definitely a nice write-up, but a bit more discussion of where the approach breaks down would be useful. It's actually a classic example of an elegant solution that breaks down frequently in practice (i.e. it's commonly used as a teaching example in various courses). A better solution is usually more complex, domain specific, and therefore out-of-scope, but failure modes for this method make for a nice set of examples.
I would have instead done like this:<p>(tldr; use a sine and cosine function regression like a linear regression. Think like solving for a free angle and a free phase instead than for a free bias and weight).<p>1. Convert the hours to an angle in degrees or in radians (a simple linear transformation).<p>2. Take the cos and sin of the angle to get the x and y position in a plane, respectively.<p>3. Introduce a time axis such that the thing doesn't draw a circle but rather an helix (like DNA).<p>4. So we now have a ton of 3D data points: (time, x, y). Create a ML model to fit a sine and a cosine to those data points to match them perfectly. Your model has only 2 free parameters to optimize for: a shared phase offset and a shared frequency. The sine uses (time, y) and the cosine (time, x).<p>5. Initialize the model with a random phase offset and a frequency ideally already close to the one you think you have. Don't initialize with a too high frequency to avoid fitting just Nyquist-frequency-close-noise.<p>6. Optimize! (With the least squares.) I guess that you might congerge only to a local minima and need to try different randon starting frequencies if you fail to converge.<p>7. The answer to your problem is the now-optimized free parameter of the frequency. It won't sit between two bins of your fft anymore.<p>Related: <a href="https://stackoverflow.com/questions/16716302/how-do-i-fit-a-sine-curve-to-my-data-with-pylab-and-numpy" rel="nofollow">https://stackoverflow.com/questions/16716302/how-do-i-fit-a-...</a><p>Note: This link contains images picturing the transformations I try to explain.<p>Disclaimer: I didn't do that yet, this is just off the top of my head. If I said something wrong, please comment. Mostly about a wrong convergence to Nyquist freq or something like that (?).<p>In the end, this way, you won't have discrete fft bins. You'll approach the problem orthogonally to that: you solve for finding the one best fft bin (frequency) directly.<p>In other words: solve for the content in the exponential of "e" as free parameters, and for one such frequency and phase offset instead of many bins.