Creating time series for demos or testing purposes can be challenging. The timelorde
library solves this problem for you. Using Luxon
dates as native units of the x
axis makes it easy to produce signals that work over arbitrary intervals of time. The library makes it convenient to produce various time series with trends
, seasonality
, and noise
.
It is easy to get up and running. You must add it into your javascript
or typescript
project.
pnpm add timelorde
yarn install timelorde
npm install timelorde
This should install timelorde
into your project.
This library makes use of the Luxon
time and date library. This excellent library has three primary types that we use in this project: the DateTime
type, the Duration
type, and the Interval
type. The DateTime
is an immutable data structure that represents a specific date and accompanying methods. The Duration
type represents a period of time such as 2 months or 1 day. The Interval
type is an object representing a half-open interval of time where each endpoint is a DateTime
object. The Luxon
library also includes various types to help with the complexity of time zones. Please check out their documentation here
If you do not already use the luxon
library you can import the needed types from timelorde/luxon
.
import { Interval, DateTime, Duration, } from "timelorde/luxon";
With a bit of understanding how to create the basic Luxon
types we can start using our library. We have three major types of signals:
- trend
- seasonality
- noise
Each of the signal types implement a sample
method that will sample over an Interval
instance and a fixed Duration
that represents the granularity of the sampling. For example, using the type Linear
with the formal constructor
Linear(gradient: number, duration: Duration, intercept = 0.0)
we can create a linear trend that will climb at the rate of the gradient
over the duration
of the trend starting at the intercept
given by the client.
With the following code we will sample the linear signal over the span of a month with the granularity of 4 days and starting with an intercept of 100. Along with this signal we also include two different seasonal signals and some red noise to make it look realistic. We can also observe that we can add
two signals together to get a composite signal.
import { Interval, DateTime, Duration } from "timelorde/luxon";
import { Linear, Sinusoidal, Red } from "timelorde";
const trend = new Linear(2, Duration.fromObject({ days: 4 }), 100);
const seasonality = new Sinusoidal(20, Duration.fromObject({ days: 7 })).add(
new Sinusoidal(4, Duration.fromObject({ days: 1 }))
);
const noise = new Red(0, 3, 0.5);
const timeseries = trend.add(seasonality).add(noise);
const start = DateTime.fromISO("2022-02-10");
const end = DateTime.fromISO("2022-03-10");
const interval = Interval.fromDateTimes(start, end);
const series = timeseries.sample(interval, Duration.fromObject({ hours: 1 }));
The return type of Signal.sample
is an array of type Sample[]
. The Sample
type represents the relationship between a DateTime
instance and the value
given for that instance as given for the interval and granularity of the sample.
Using our series we can transform the Sample
type into something our graphing software can recognize. The one I use expects a CVS input. I achieve this using a reduction on the series below.
series.reduce((acc, { date, value }) => {
return acc + `${date.toISODate()},${value.toFixed(2)}\n`;
}, "date,values\n");
Taking the values for this series and putting it into our favorite graphing software we get the following graph.
For the rest of the README we will only show the resulting graph of the series and not the data of the series.
The trend signals represent a class of signals that help you define the trend of your time series. We currently have three trends in the project:
Flat
Linear
Exponential
The noise signals represent a class of signals that help you define the noise your time series. We currently have two noise signals in the project:
Gaussian
Red
The seasonality signals represent the signals that have a period to them. We currently have only one signal that represents seasonality.
Sinusoidal
Along with amplitude and frequency, we can also set the offset of the start of the signal. A negative offset will push the signal to the left that many units and a positive offset will shift the signal to the right.
We currently have two types of signal composition in timelorde
. They are the add
and mcl
methods of the Signal
type. The add
method takes two signals and adds the signals underlying values together while the mul
method takes two signals and multiplies their values together.
For example,
import { Interval, DateTime, Duration } from "timelorde/luxon";
import { Linear, Sinusoidal } from "timelorde";
const trend = new Linear(2, Duration.fromObject({ days: 1 }));
const seasonality = new Sinusoidal(2, Duration.fromObject({ hours: 12 }));
const timeseries = trend.add(seasonality);
const start = DateTime.fromISO("2022-03-03");
const end = DateTime.fromISO("2022-03-10");
const interval = Interval.fromDateTimes(start, end);
const series = timeseries.sample(interval, Duration.fromObject({ hours: 1 }));
will produce the following graph.
whereas multiplying the trend
and seasonality
in the following example
import { Interval, DateTime, Duration } from "timelorde/luxon";
import { Linear, Sinusoidal } from "timelorde";
const trend = new Linear(2, Duration.fromObject({ days: 1 }));
const seasonality = new Sinusoidal(2, Duration.fromObject({ hours: 12 }));
const timeseries = trend.mul(seasonality);
const start = DateTime.fromISO("2022-03-03");
const end = DateTime.fromISO("2022-03-10");
const interval = Interval.fromDateTimes(start, end);
const series = timeseries.sample(interval, Duration.fromObject({ hours: 1 }));
will give us the following graph.
Assume we are tasked with simulating seasonal temperatures here on earth with a slight trend towards a warmer world. This is a working example from the excellent mockseries library.
We will assume the following conditions for our simulated series:
- The temperature has an average value of 12°C
- The temperature is slowly rising by 0.1°C over a year
- The approximate max is 25°C and the average min is -1°C
- The yearly seasonalities are impacted by the warming trend of temperatures and results in bigger yearly temperature swings
- The noise of the series increases as the temperature increases.
- The series sample must be four years long
First we import all the necessary types
import { Interval, DateTime, Duration } from "timelorde/luxon";
import { Flat, Linear, Sinusoidal, Gaussian } from "timelorde";
And we will use a variable temperature
to represent our current working temperature signal.
let temperature;
and then we start constructing the signals that we need. We model our average constraint by creating a Flat
signal with the value 12.
const average = new Flat(12);
We model the warming constraint with a Linear
signal that grows by 0.1°C over a period of one year.
const warming = new Linear(0.1, Duration.fromObject({ years: 1 }));
Adding these two signals together
temperature = average.add(warming);
and sampling over four years with the granularity of one day
temperature.sample(interval, Duration.fromObject({ days: 1 }));
gives us a graph that demonstrates a 0.4°C growth from 12°C to 12.4°C over the span of four years.
Our seasonal changes include both yearly and daily. For our yearly seasonal change we want to show the progression of temperatures through winter, spring, summer, and fall. To calculate the amplitude we need we take the difference between the max and min temperatures and divide it by two. In our example the difference between 25°C and -1°C is 26°C. Dividing this value by 2 and using it as our amplitude we can model the yearly seasonality with creating a new Sinusoidal
instance with an amplitude of 13 and a duration over one year.
const yearly = new Sinusoidal(13, Duration.fromObject({ years: 1 }));
If we add the yearly signal to the average and warming signal
temperature = average.add(warming).add(yearly);
we have a sinusoidal wave over four years that has a constant amplitude of 13 but with the average increase of 0.4°C.
However, our constraints require us to increase the amplitude of the sinusoidal wave as the temperature gets warmer. This is a common pattern in modeling signals and worth pointing out. We can adjust the growth of our warming signal by multiplying it by the yearly seasonal signal.
const growth = warming.mul(yearly);
Sampling over four years with the growth signal gives us the graph
which gives a signal for a warming that varies more over the four year growth of the yearly seasonal temperatures. The growth only varies between 5 and -5 degrees at the end of four years which is what we are looking for.
Using the growth
signal we can replace the warming
signal in our temperature signal
temperature = average.add(growth).add(yearly);
Graphing temperature
over four years with a 1 day granularity gives us the graph
in which we can now see the growth of the amplitude over the four years.
Naturally occurring temperature series have a certain amount of variability of temperature changes over time that is not defined by trend or the seasonality changes. This type of variability is called noise and is needed to make any time series look realistic. To make our working example look more realistic we will add some noise.
We construct some noise by creating a new Gaussian
instance and a reasonable number of 1 for the mean and 0.5 for the standard deviation. These values made our series look good and felt reasonable. For your project you must play with the noise to make it look good to you and your domain.
let noise = new Gaussian(1, 0.5);
Our requirements state that the noise of the signal must grow as the signal amplitude grows. The variable growth
holds the value of this growth and we will use it to increase the standard deviation of the noise over time. We multiply noise
with growth
to get our new noise
noise = noise.mul(growth);
which will scale the noise depending on the growth of the amplitude of the signal. Graphing the noise over a span of four years gives us the graph
where we can see that the noise varies between 1.5 and -1.5 for the first year and between 9.5 to -9.5 at the fourth year. Adding this noise signal to the current signal for temperature
temperature = average.add(growth).add(yearly).add(noise);
will give us the graph over a four year span
which is the model we are looking for.
You may need to adjust the offset of sinusoidal signal depending on the exact dates of your series. The signal for our working example starts out at 12°C, grows to its max of 25°C at 3 months, comes back down to 12°C at 6 months, down to the minimum -1°C at 9 months and returns at 12°C by the end of the year. For most of us in Northern America it would be awkward if you started your series in August and the hottest time of the year was at the end of November.
When constructing a sinusoidal signal we can adjust the offset of the signal give us an even more realistic sampling of the temperatures between two dates. In our working example, if we adjusted the offset of our yearly
variable to be -3 months
const yearly = new Sinusoidal(13, Duration.fromObject({ years: 1 }), Duration.fromObject({months: -3}));
we can see that we have pushed the curve over to the left giving us a starting signal that more matches what the temperatures would be like in August.
This library will help you make amazing time series models that will wow and impress your friends and family. With the various types of signals you can create series that show trends and seasonality, while adding realism through noise signals. This library though impressive is pretty small and is looking to grow.
We are hoping to add the following:
- Partial Intervals -- Do not produce results for example, weekends.
- Glitches -- All series have glitches, we should include them
There are a few other time series libraries out there. If you are using python or Java they will have you covered.
"The code in this project is licensed under MIT license."