Deedle


Working with series and time series data in F#

In this section, we look at F# data frame library features that are useful when working with time series data or, more generally, any ordered series. Although we mainly look at operations on the Series type, many of the operations can be applied to data frame Frame containing multiple series. Furthermore, data frame provides an elegant way for aligning and joining series.

You can also get this page as an F# script file from GitHub and run the samples interactively.

Generating input data

For the purpose of this tutorial, we'll need some input data. For simplicitly, we use the following function which generates random prices using the geometric Brownian motion. The code is adapted from the financial tutorial on Try F#.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
// Use Math.NET for probability distributions
#r "MathNet.Numerics.dll"
open MathNet.Numerics.Distributions

/// Generates price using geometric Brownian motion
///  - 'seed' specifies the seed for random number generator
///  - 'drift' and 'volatility' set properties of the price movement
///  - 'initial' and 'start' specify the initial price and date
///  - 'span' specifies time span between individual observations
///  - 'count' is the number of required values to generate
let randomPrice seed drift volatility initial start span count = 
  (Implementation omitted)

// 12:00 AM today, in current time zone
let today = DateTimeOffset(DateTime.Today)
let stock1 = randomPrice 1 0.1 3.0 20.0 today 
let stock2 = randomPrice 2 0.2 1.5 22.0 today

The implementation of the function is not particularly important for the purpose of this page, but you can find it in the script file with full source. Once we have the function, we define a date today (representing today's midnight) and two helper functions that set basic properties for the randomPrice function.

To get random prices, we now only need to call stock1 or stock2 with TimeSpan and the required number of prices:

1: 
2: 
3: 
Chart.Combine
  [ stock1 (TimeSpan(0, 1, 0)) 1000 |> Chart.FastLine
    stock2 (TimeSpan(0, 1, 0)) 1000 |> Chart.FastLine ]

The above snippet generates 1k of prices in one minute intervals and plots them using the F# Charting library. When you run the code and tweak the chart look, you should see something like this:

Data alignment and zipping

One of the key features of the data frame library for working with time series data is automatic alignment based on the keys. When we have multiple time series with date as the key (here, we use DateTimeOffset, but any type of date will do), we can combine multiple series and align them automatically to specified date keys.

To demonstrate this feature, we generate random prices in 60 minute, 30 minute and 65 minute intervals:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
let s1 = stock1 (TimeSpan(1, 0, 0)) 6 |> series
val s1 : Series<DateTimeOffset,float> =
  series [ 12:00:00 AM => 20.76; 1:00:00 AM => 21.11; 2:00:00 AM => 22.51 
            3:00:00 AM => 23.88; 4:00:00 AM => 23.23; 5:00:00 AM => 22.68 ] 

let s2 =stock2 (TimeSpan(0, 30, 0)) 12 |> series
val s2 : Series<DateTimeOffset,float> =
  series [ 12:00:00 AM => 21.61; 12:30:00 AM => 21.64; 1:00:00 AM => 21.86 
            1:30:00 AM => 22.22;  2:00:00 AM => 22.35; 2:30:00 AM => 22.76 
            3:00:00 AM => 22.68;  3:30:00 AM => 22.64; 4:00:00 AM => 22.90 
            4:30:00 AM => 23.40;  5:00:00 AM => 23.33; 5:30:00 AM => 23.43] 

let s3 = stock1 (TimeSpan(1, 5, 0)) 6 |> series
val s3 : Series<DateTimeOffset,float> =
  series [ 12:00:00 AM => 21.37; 1:05:00 AM => 22.73; 2:10:00 AM => 22.08 
            3:15:00 AM => 23.92; 4:20:00 AM => 22.72; 5:25:00 AM => 22.79 

Zipping time series

Let's first look at operations that are available on the Series<K, V> type. A series exposes Zip operation that can combine multiple series into a single series of pairs. This is not as convenient as working with data frames (which we'll see later), but it is useful if you only need to work with one or two columns without missing values:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
// Match values from right series to keys of the left one
// (this creates series with no missing values)
s1.Zip(s2, JoinKind.Left)
val it : Series<DateTimeOffset,float opt * float opt>
  12:00:00 AM -> (21.32, 21.61) 
   1:00:00 AM -> (22.62, 21.86) 
   2:00:00 AM -> (22.00, 22.35)  
  (...)

// Match values from the left series to keys of the right one
// (right has higher resolution, so half of left values are missing)
s1.Zip(s2, JoinKind.Right)
val it : Series<DateTimeOffset,float opt * float opt>
  12:00:00 AM -> (21.32,     21.61) 
  12:30:00 AM -> (<missing>, 21.64)  
   1:00:00 AM -> (22.62,     21.86) 
  (...)

// Use left series key and find the nearest previous
// (smaller) value from the right series
s1.Zip(s2, JoinKind.Left, Lookup.ExactOrSmaller)
val it : Series<DateTimeOffset,float opt * float opt>
  12:00:00 AM -04:00 -> (21.32, 21.61) 
   1:00:00 AM -04:00 -> (22.62, 21.86) 
   2:00:00 AM -04:00 -> (22.00, 22.35)  
  (...)

Using Zip on series is somewhat complicated. The result is a series of tuples, but each component of the tuple may be missing. To represent this, the library uses the T opt type (a type alias for OptionalValue<T>). This is not necessary when we use data frame to work with multiple columns.

Joining data frames

When we store data in data frames, we do not need to use tuples to represent combined values. Instead, we can simply use data frame with multiple columns. To see how this works, let's first create three data frames containing the three series from the previous section:

1: 
2: 
3: 
4: 
5: 
6: 
// Contains value for each hour
let f1 = Frame.ofColumns ["S1" => s1]
// Contains value every 30 minutes
let f2 = Frame.ofColumns ["S2" => s2]
// Contains values with 65 minute offsets
let f3 = Frame.ofColumns ["S3" => s3]

Similarly to Series<K, V>, the type Frame<R, C> has an instance method Join that can be used for joining (for unordered) or aligning (for ordered) data. The same operation is also exposed as Frame.join and Frame.joinAlign functions, but it is usually more convenient to use the member syntax in this case:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
// Union keys from both frames and align corresponding values
f1.Join(f2, JoinKind.Outer)
val it : Frame<DateTimeOffset,string> =
                 S1        S2               
  12:00:00 AM -> 21.32     21.61 
  12:30:00 AM -> <missing> 21.64 
   1:00:00 AM -> 22.62     21.86 
  (...)

// Take only keys where both frames contain all values
// (We get only a single row, because 'f3' is off by 5 minutes)
f2.Join(f3, JoinKind.Inner)
val it : Frame<DateTimeOffset,string> =
                 S2      S3               
  12:00:00 AM -> 21.61   21.37 

// Take keys from the left frame and find corresponding values
// from the right frame, or value for a nearest smaller date
// ($21.37 is repeated for all values between 12:00 and 1:05)
f2.Join(f3, JoinKind.Left, Lookup.ExactOrSmaller)
val it : Frame<DateTimeOffset,string> =
                 S2      S3               
  12:00:00 AM -> 21.61   21.37 
  12:30:00 AM -> 21.64   21.37 
   1:00:00 AM -> 21.86   21.37 
   1:30:00 AM -> 22.22   22.73 
  (...)

// If we perform left join as previously, but specify exact 
// matching, then most of the values are missing
f2.Join(f3, JoinKind.Left, Lookup.Exact)
val it : Frame<DateTimeOffset,string> =
                 S2      S3               
  12:00:00 AM -> 21.61   21.37
  12:30:00 AM -> 21.64   <missing>        
   1:00:00 AM -> 21.86   <missing>        
  (...)

// Equivalent to line 2, using function syntax 
Frame.join JoinKind.Outer f1 f2

// Equivalent to line 20, using function syntax
Frame.joinAlign JoinKind.Left Lookup.ExactOrSmaller f1 f2

The automatic alignment is extremely useful when you have multiple data series with different offsets between individual observations. You can choose your set of keys (dates) and then easily align other data to match the keys. Another alternative to using Join explicitly is to create a new frame with just keys that you are interested in (using Frame.ofRowKeys) and then use the AddSeries member (or the df?New <- s syntax) to add series. This will automatically left join the new series to match the current row keys.

When aligning data, you may or may not want to create data frame with missing values. If your observations do not happen at exact time, then using Lookup.ExactOrSmaller or Lookup.ExactOrGreater is a great way to avoid mismatch.

If you have observations that happen e.g. at two times faster rate (one series is hourly and another is half-hourly), then you can create data frame with missing values using Lookup.Exact (the default value) and then handle missing values explicitly (as discussed here).

Windowing, chunking and pairwise

Windowing and chunking are two operations on ordered series that allow aggregating the values of series into groups. Both of these operations work on consecutive elements, which contrast with grouping that does not use order.

Sliding windows

Sliding window creates windows of certain size (or certain condition). The window "slides" over the input series and provides a view on a part of the series. The key thing is that a single element will typically appear in multiple windows.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
// Create input series with 6 observations
let lf = stock1 (TimeSpan(0, 1, 0)) 6 |> series

// Create series of series representing individual windows
lf |> Series.window 4
// Aggregate each window using 'Stats.mean'
lf |> Series.windowInto 4 Stats.mean
// Get first value in each window
lf |> Series.windowInto 4 Series.firstValue

The functions used above create window of size 4 that moves from the left to right. Given input [1,2,3,4,5,6] the this produces the following three windows: [1,2,3,4], [2,3,4,5] and [3,4,5,6]. By default, the Series.window function automatically chooses the key of the last element of the window as the key for the whole window (we'll see how to change this soon):

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
// Calculate means for sliding windows
let lfm1 = lf |> Series.windowInto 4 Stats.mean
// Construct dataframe to show aligned results
Frame.ofColumns [ "Orig" => lf; "Means" => lfm1 ]
val it : Frame<DateTimeOffset,string> =
                 Means      Orig        
  12:00:00 AM -> <missing>  20.16
  12:01:00 AM -> <missing>  20.32
  12:02:00 AM -> <missing>  20.25
  12:03:00 AM -> 20.30      20.45
  12:04:00 AM -> 20.34      20.32
  12:05:00 AM -> 20.34      20.33

What if we want to avoid creating <missing> values? One approach is to specify that we want to generate windows of smaller sizes at the beginning or at the end of the beginning. This way, we get incomplete windows that look as [1], [1,2], [1,2,3] followed by the three complete windows shown above:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
let lfm2 = 
  // Create sliding windows with incomplete windows at the beginning
  lf |> Series.windowSizeInto (4, Boundary.AtBeginning) (fun ds ->
    Stats.mean ds.Data)

Frame.ofColumns [ "Orig" => lf; "Means" => lfm2 ]
val it : Frame<DateTimeOffset,string> =
                 Means  Orig        
  12:00:00 AM -> 20.16  20.16
  12:01:00 AM -> 20.24  20.32
  12:02:00 AM -> 20.24  20.25
  12:03:00 AM -> 20.30  20.45
  12:04:00 AM -> 20.34  20.32
  12:05:00 AM -> 20.34  20.33

As you can see, the values in the first column are equal, because the first Mean value is just the average of singleton series.

When you specify Boundary.AtBeginning (this example) or Boundary.Skip (default value used in the previous example), the function uses the last key of the window as the key of the aggregated value. When you specify Boundary.AtEnding, the first key is used, so the values can be nicely aligned with original values. When you want to specify custom key selector, you can use a more general function Series.aggregate.

In the previous sample, the code that performs aggregation is no longer just a simple function like Stats.mean, but a lambda that takes ds, which is of type DataSegment<T>. This type informs us whether the window is complete or not. For example:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
// Simple series with characters
let st = Series.ofValues [ 'a' .. 'e' ]
st |> Series.windowSizeInto (3, Boundary.AtEnding) (function
  | DataSegment.Complete(ser) -> 
      // Return complete windows as uppercase strings
      String(ser |> Series.values |> Array.ofSeq).ToUpper()
  | DataSegment.Incomplete(ser) -> 
      // Return incomplete windows as padded lowercase strings
      String(ser |> Series.values |> Array.ofSeq).PadRight(3, '-') )  
val it : Series<int,string> =
  0 -> ABC 
  1 -> BCD 
  2 -> CDE 
  3 -> de- 
  4 -> e-- 

Window size conditions

The previous examples generated windows of fixed size. However, there are two other options for specifying when a window ends.

  • The first option is to specify the maximal distance between the first and the last key
  • The second option is to specify a function that is called with the first and the last key; a window ends when the function returns false.

The two functions are Series.windowDist and Series.windowWhile (together with versions suffixed with Into that call a provided function to aggregate each window):

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
// Generate prices for each hour over 30 days
let hourly = stock1 (TimeSpan(1, 0, 0)) (30*24) |> series

// Generate windows of size 1 day (if the source was
// irregular, windows would have varying size)
hourly |> Series.windowDist (TimeSpan(24, 0, 0))

// Generate windows such that date in each window is the same
// (windows start every hour and end at the end of the day)
hourly |> Series.windowWhile (fun d1 d2 -> d1.Date = d2.Date)

Chunking series

Chunking is similar to windowing, but it creates non-overlapping chunks, rather than (overlapping) sliding windows. The size of chunk can be specified in the same three ways as for sliding windows (fixed size, distance on keys and condition):

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
// Generate per-second observations over 10 minutes
let hf = stock1 (TimeSpan(0, 0, 1)) 600 |> series

// Create 10 second chunks with (possible) incomplete
// chunk of smaller size at the end.
hf |> Series.chunkSize (10, Boundary.AtEnding) 

// Create 10 second chunks using time span and get
// the first observation for each chunk (downsample)
hf |> Series.chunkDistInto (TimeSpan(0, 0, 10)) Series.firstValue

// Create chunks where hh:mm component is the same
// (containing observations for all seconds in the minute)
hf |> Series.chunkWhile (fun k1 k2 -> 
  (k1.Hour, k1.Minute) = (k2.Hour, k2.Minute))

The above examples use various chunking functions in a very similar way, mainly because the randomly generated input is very uniform. However, they all behave differently for inputs with non-uniform keys.

Using chunkSize means that the chunks have the same size, but may correspond to time series of different time spans. Using chunkDist guarantees that there is a maximal time span over each chunk, but it does not guarantee when a chunk starts. That is something which can be achieved using chunkWhile.

Finally, all of the aggregations discussed so far are just special cases of Series.aggregate which takes a discriminated union that specifies the kind of aggregation (see API reference). However, in practice it is more convenient to use the helpers presented here - in some rare cases, you might need to use Series.aggregate as it provides a few other options.

Pairwise

A special form of windowing is building a series of pairs containing a current and previous value from the input series (in other words, the key for each pair is the key of the later element). For example:

1: 
2: 
3: 
4: 
5: 
// Create a series of pairs from earlier 'hf' input
hf |> Series.pairwise 

// Calculate differences between the current and previous values
hf |> Series.pairwiseWith (fun k (v1, v2) -> v2 - v1)

The pairwise operation always returns a series that has no value for the first key in the input series. If you want more complex behavior, you will usually need to replace pairwise with window. For example, you might want to get a series that contains the first value as the first element, followed by differences. This has the nice property that summing rows, starting from the first one gives you the current price:

1: 
2: 
3: 
4: 
5: 
6: 
// Sliding window with incomplete segment at the beginning 
hf |> Series.windowSizeInto (2, Boundary.AtBeginning) (function
  // Return the first value for the first segment
  | DataSegment.Incomplete s -> s.GetAt(0)
  // Calculate difference for all later segments
  | DataSegment.Complete s -> s.GetAt(1) - s.GetAt(0))

Sampling and resampling time series

Given a time series with high-frequency prices, sampling or resampling makes it possible to get time series with representative values at lower frequency. The library uses the following terminology:

  • Lookup means that we find values at specified key; if a key is not available, we can look for value associated with the nearest smaller or the nearest greater key.

  • Resampling means that we aggregate values values into chunks based on a specified collection of keys (e.g. explicitly provided times), or based on some relation between keys (e.g. date times having the same date).

  • Uniform resampling is similar to resampling, but we specify keys by providing functions that generate a uniform sequence of keys (e.g. days), the operation also fills value for days that have no corresponding observations in the input sequence.

Finally, the library also provides a few helper functions that are specifically desinged for series with keys of types DateTime and DateTimeOffset.

Lookup

Given a series hf, you can get a value at a specified key using hf.Get(key) or using hf |> Series.get key. However, it is also possible to find values for larger number of keys at once. The instance member for doing this is hf.GetItems(..). Moreover, both Get and GetItems take an optional parameter that specifies the behavior when the exact key is not found.

Using the function syntax, you can use Series.getAll for exact key lookup and Series.lookupAll when you want more flexible lookup:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
// Generate a bit less than 24 hours of data with 13.7sec offsets
let mf = stock1 (TimeSpan.FromSeconds(13.7)) 6300 |> series
// Generate keys for all minutes in 24 hours
let keys = [ for m in 0.0 .. 24.0*60.0-1.0 -> today.AddMinutes(m) ]

// Find value for a given key, or nearest greater key with value
mf |> Series.lookupAll keys Lookup.ExactOrGreater
val it : Series<DateTimeOffset,float> =
  12:00:00 AM -> 20.07 
  12:01:00 AM -> 19.98 
  ...         -> ...   
  11:58:00 PM -> 19.03 
  11:59:00 PM -> <missing>        

// Find value for nearest smaller key
// (This returns value for 11:59:00 PM as well)
mf |> Series.lookupAll keys Lookup.ExactOrSmaller

// Find values for exact key 
// (This only works for the first key)
mf |> Series.lookupAll keys Lookup.Exact

Lookup operations only return one value for each key, so they are useful for quick sampling of large (or high-frequency) data. When we want to calculate a new value based on multiple values, we need to use resampling.

Resampling

Series supports two kinds of resamplings. The first kind is similar to lookup in that we have to explicitly specify keys. The difference is that resampling does not find just the nearest key, but all smaller or greater keys. For example:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
// For each key, collect values for greater keys until the 
// next one (chunk for 11:59:00 PM is empty)
mf |> Series.resample keys Direction.Forward

// For each key, collect values for smaller keys until the 
// previous one (the first chunk will be singleton series)
mf |> Series.resample keys Direction.Backward

// Aggregate each chunk of preceding values using mean
mf |> Series.resampleInto keys Direction.Backward 
  (fun k s -> Stats.mean s)

// Resampling is also available via the member syntax
mf.Resample(keys, Direction.Forward)

The second kind of resampling is based on a projection from existing keys in the series. The operation then collects chunks such that the projection returns equal keys. This is very similar to Series.groupBy, but resampling assumes that the projection preserves the ordering of the keys, and so it only aggregates consequent keys.

The typical scenario is when you have time series with date time information (here DateTimeOffset) and want to get information for each day (we use DateTime with empty time to represent dates):

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
// Generate 2.5 months of data in 1.7 hour offsets
let ds = stock1 (TimeSpan.FromHours(1.7)) 1000 |> series

// Sample by day (of type 'DateTime')
ds |> Series.resampleEquiv (fun d -> d.Date)

// Sample by day (of type 'DateTime')
ds.ResampleEquivalence(fun d -> d.Date)

The same operation can be easily implemented using Series.chunkWhile, but as it is often used in the context of sampling, it is included in the library as a primitive. Moreover, we'll see that it is closely related to uniform resampling.

Note that the resulting series has different type of keys than the source. The source has keys DateTimeOffset (representing date with time) while the resulting keys are of the type returned by the projection (here, DateTime representing just dates).

Uniform resampling

In the previous section, we looked at resampleEquiv, which is useful if you want to sample time series by keys with "lower resolution" - for example, sample date time observations by date. However, the function discussed in the previous section only generates values for which there are keys in the input sequence - if there is no observation for an entire day, then the day will not be included in the result.

If you want to create sampling that assigns value to each key in the range specified by the input sequence, then you can use uniform resampling.

The idea is that uniform resampling applies the key projection to the smallest and greatest key of the input (e.g. gets date of the first and last observation) and then it generates all keys in the projected space (e.g. all dates). Then it picks the best value for each of the generated key.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
// Create input data with non-uniformly distributed keys
// (1 value for 10/3, three for 10/4 and two for 10/6)
let days =
  [ "10/3/2013 12:00:00"; "10/4/2013 15:00:00" 
    "10/4/2013 18:00:00"; "10/4/2013 19:00:00"
    "10/6/2013 15:00:00"; "10/6/2013 21:00:00" ]
let nu = 
  stock1 (TimeSpan(24,0,0)) 10 |> series
  |> Series.indexWith days |> Series.mapKeys DateTimeOffset.Parse

// Generate uniform resampling based on dates. Fill
// missing chunks with nearest smaller observations.
let sampled =
  nu |> Series.resampleUniform Lookup.ExactOrSmaller 
    (fun dt -> dt.Date) (fun dt -> dt.AddDays(1.0))

// Same thing using the C#-friendly member syntax
// (Lookup.ExactOrSmaller is the default value)
nu.ResampleUniform((fun dt -> dt.Date), (fun dt -> dt.AddDays(1.0)))

// Turn into frame with multiple columns for each day
// (to format the result in a readable way)
sampled 
|> Series.mapValues Series.indexOrdinally
|> Frame.ofRows
val it : Frame<DateTime,int> =
             0      1          2                
10/3/2013 -> 21.45  <missing>  <missing>        
10/4/2013 -> 21.63  19.83      17.51
10/5/2013 -> 17.51  <missing>  <missing>        
10/6/2013 -> 18.80  20.93      <missing>        

To perform the uniform resampling, we need to specify how to project (resampled) keys from original keys (we return the Date), how to calculate the next key (add 1 day) and how to fill missing values.

After performing the resampling, we turn the data into a data frame, so that we can nicely see the results. The individual chunks have the actual observation times as keys, so we replace those with just integers (using Series.indexOrdinal). The result contains a simple ordered row of observations for each day.

The important thing is that there is an observation for each day - even for for 10/5/2013 which does not have any corresponding observations in the input. We call the resampling function with Lookup.ExactOrSmaller, so the value 17.51 is picked from the last observation of the previous day (Lookup.ExactOrGreater would pick 18.80 and Lookup.Exact would give us an empty series for that date).

Sampling time series

Perhaps the most common sampling operation that you might want to do is to sample time series by a specified TimeSpan. Although this can be easily done by using some of the functions above, the library provides helper functions exactly for this purpose:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
// Generate 1k observations with 1.7 hour offsets
let pr = stock1 (TimeSpan.FromHours(1.7)) 1000 |> series

// Sample at 2 hour intervals; 'Backward' specifies that
// we collect all previous values into a chunk.
pr |> Series.sampleTime (TimeSpan(2, 0, 0)) Direction.Backward

// Same thing using member syntax - 'Backward' is the dafult
pr.Sample(TimeSpan(2, 0, 0))

// Get the most recent value, sampled at 2 hour intervals
pr |> Series.sampleTimeInto
  (TimeSpan(2, 0, 0)) Direction.Backward Series.lastValue

Calculations and statistics

In the final section of this tutorial, we look at writing some calculations over time series. Many of the functions demonstrated here can be also used on unordered data frames and series.

Shifting and differences

First of all, let's look at functions that we need when we need to compare subsequent values in the series. We already demonstrated how to do this using Series.pairwise. In many cases, the same thing can be done using an operation that operates over the entire series.

The two useful functions here are:

  • Series.diff calcualtes the difference between current and n-th previous element
  • Series.shift shifts the values of a series by a specified offset

The following snippet illustrates how both functions work:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
// Generate sample data with 1.7 hour offsets
let sample = stock1 (TimeSpan.FromHours(1.7)) 6 |> series

// Calculates: new[i] = s[i] - s[i-1]
let diff1 = sample |> Series.diff 1
// Diff in the opposite direction
let diffM1 = sample |> Series.diff -1

// Shift series values by 1
let shift1 = sample |> Series.shift 1

// Align all results in a frame to see the results
let df = 
  [ "Shift +1" => shift1 
    "Diff +1" => diff1 
    "Diff" => sample - shift1 
    "Orig" => sample ] |> Frame.ofColumns 
val it : Frame<DateTimeOffset,string> =
                 Diff       Diff +1    Orig   Shift +1         
  12:00:00 AM -> <missing>  <missing>  21.73  <missing>        
   1:42:00 AM ->  1.73       1.73      23.47  21.73 
   3:24:00 AM -> -0.83      -0.83      22.63  23.47 
   5:06:00 AM ->  2.37       2.37      25.01  22.63 
   6:48:00 AM -> -1.57      -1.57      23.43  25.01 
   8:30:00 AM ->  0.09       0.09      23.52  23.43 

In the above snippet, we first calcluate difference using the Series.diff function. Then we also show how to do that using Series.shift and binary operator applied to two series (sample - shift). The following section provides more details. So far, we also used the functional notation (e.g. sample |> Series.diff 1), but all operations can be called using the member syntax - very often, this gives you a shorter syntax. This is also shown in the next few snippets.

Operators and functions

Time series also supports a large number of standard F# functions such as log and abs. You can also use standard numerical operators to apply some operation to all elements of the series.

Because series are indexed, we can also apply binary operators to two series. This automatically aligns the series and then applies the operation on corresponding elements.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
// Subtract previous value from the current value
sample - sample.Shift(1)

// Calculate logarithm of such differences
log (sample - sample.Shift(1))

// Calculate square of differences
sample.Diff(1) ** 2.0

// Calculate average of value and two immediate neighbors
(sample.Shift(-1) + sample + sample.Shift(2)) / 3.0

// Get absolute value of differences
abs (sample - sample.Shift(1))

// Get absolute value of distance from the mean
abs (sample - (Stats.mean sample))

The time series library provides a large number of functions that can be applied in this way. These include trigonometric functions (sin, cos, ...), rounding functions (round, floor, ceil), exponentials and logarithms (exp, log, log10) and more. In general, whenever there is a built-in numerical F# function that can be used on standard types, the time series library should support it too.

However, what can you do when you write a custom function to do some calculation and want to apply it to all series elements? Let's have a look:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
// Truncate value to interval [-1.0, +1.0]
let adjust v = min 1.0 (max -1.0 v)

// Apply adjustment to all function
adjust $ sample.Diff(1)

// The $ operator is a shorthand for
sample.Diff(1) |> Series.mapValues adjust

In general, the best way to apply custom functions to all values in a series is to align the series (using either Series.join or Series.joinAlign) into a single series containing tuples and then apply Series.mapValues. The library also provides the $ operator that simplifies the last step - f $ s applies the function f to all values of the series s.

Data frame operations

Finally, many of the time series operations demonstrated above can be applied to entire data frames as well. This is particularly useful if you have data frame that contains multiple aligned time series of similar structure (for example, if you have multiple stock prices or open-high-low-close values for a given stock).

The following snippet is a quick overview of what you can do:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
/// Multiply all numeric columns by a given constant
df * 0.65

// Apply function to all columns in all series
let conv x = min x 20.0
df |> Frame.mapRowValues (fun os -> conv $ os.As<float>())
   |> Frame.ofRows

// Sum each column and divide results by a constant
Stats.sum df / 6.0
// Divide sum by mean of each frame column
Stats.sum df / Stats.mean df
namespace System
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Data

--------------------
namespace Microsoft.FSharp.Data
namespace Deedle
namespace FSharp.Charting
val root : string
namespace MathNet
namespace MathNet.Numerics
namespace MathNet.Numerics.Distributions
val randomPrice : seed:int -> drift:float -> volatility:float -> initial:float -> start:DateTimeOffset -> span:TimeSpan -> count:int -> seq<DateTimeOffset * float>


 Generates price using geometric Brownian motion
  - 'seed' specifies the seed for random number generator
  - 'drift' and 'volatility' set properties of the price movement
  - 'initial' and 'start' specify the initial price and date
  - 'span' specifies time span between individual observations
  - 'count' is the number of required values to generate
val seed : int
val drift : float
val volatility : float
val initial : float
val start : DateTimeOffset
val span : TimeSpan
val count : int
let dist = Normal(0.0, 1.0, RandomSource=Random(seed))
  let dt = (span:TimeSpan).TotalDays / 250.0
  let driftExp = (drift - 0.5 * pown volatility 2) * dt
  let randExp = volatility * (sqrt dt)
  ((start:DateTimeOffset), initial) |> Seq.unfold (fun (dt, price) ->
    let price = price * exp (driftExp + randExp * dist.Sample())
    Some((dt, price), (dt + span, price))) |> Seq.take count
val today : DateTimeOffset
Multiple items
type DateTimeOffset =
  struct
    new : dateTime:DateTime -> DateTimeOffset + 5 overloads
    member Add : timeSpan:TimeSpan -> DateTimeOffset
    member AddDays : days:float -> DateTimeOffset
    member AddHours : hours:float -> DateTimeOffset
    member AddMilliseconds : milliseconds:float -> DateTimeOffset
    member AddMinutes : minutes:float -> DateTimeOffset
    member AddMonths : months:int -> DateTimeOffset
    member AddSeconds : seconds:float -> DateTimeOffset
    member AddTicks : ticks:int64 -> DateTimeOffset
    member AddYears : years:int -> DateTimeOffset
    ...
  end

--------------------
DateTimeOffset ()
DateTimeOffset(dateTime: DateTime) : DateTimeOffset
DateTimeOffset(ticks: int64, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(dateTime: DateTime, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 10 overloads
    member Add : value:TimeSpan -> DateTime
    member AddDays : value:float -> DateTime
    member AddHours : value:float -> DateTime
    member AddMilliseconds : value:float -> DateTime
    member AddMinutes : value:float -> DateTime
    member AddMonths : months:int -> DateTime
    member AddSeconds : value:float -> DateTime
    member AddTicks : value:int64 -> DateTime
    member AddYears : value:int -> DateTime
    ...
  end

--------------------
DateTime ()
   (+0 other overloads)
DateTime(ticks: int64) : DateTime
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
property DateTime.Today: DateTime
val stock1 : (TimeSpan -> int -> seq<DateTimeOffset * float>)
val stock2 : (TimeSpan -> int -> seq<DateTimeOffset * float>)
type Chart =
  static member Area : data:seq<#value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member Area : data:seq<#key * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member Bar : data:seq<#value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member Bar : data:seq<#key * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member BoxPlotFromData : data:seq<#key * #seq<'a2>> * ?Name:string * ?Title:string * ?Color:Color * ?XTitle:string * ?YTitle:string * ?Percentile:int * ?ShowAverage:bool * ?ShowMedian:bool * ?ShowUnusualValues:bool * ?WhiskerPercentile:int -> GenericChart (requires 'a2 :> value)
  static member BoxPlotFromStatistics : data:seq<#key * #value * #value * #value * #value * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string * ?Percentile:int * ?ShowAverage:bool * ?ShowMedian:bool * ?ShowUnusualValues:bool * ?WhiskerPercentile:int -> GenericChart
  static member Bubble : data:seq<#value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string * ?BubbleMaxSize:int * ?BubbleMinSize:int * ?BubbleScaleMax:float * ?BubbleScaleMin:float * ?UseSizeForLabel:bool -> GenericChart
  static member Bubble : data:seq<#key * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string * ?BubbleMaxSize:int * ?BubbleMinSize:int * ?BubbleScaleMax:float * ?BubbleScaleMin:float * ?UseSizeForLabel:bool -> GenericChart
  static member Candlestick : data:seq<#value * #value * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> CandlestickChart
  static member Candlestick : data:seq<#key * #value * #value * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> CandlestickChart
  ...
static member Chart.Combine : charts:seq<ChartTypes.GenericChart> -> ChartTypes.GenericChart
Multiple items
type TimeSpan =
  struct
    new : ticks:int64 -> TimeSpan + 3 overloads
    member Add : ts:TimeSpan -> TimeSpan
    member CompareTo : value:obj -> int + 1 overload
    member Days : int
    member Duration : unit -> TimeSpan
    member Equals : value:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member Hours : int
    member Milliseconds : int
    member Minutes : int
    ...
  end

--------------------
TimeSpan ()
TimeSpan(ticks: int64) : TimeSpan
TimeSpan(hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : TimeSpan
static member Chart.FastLine : data:seq<#value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Drawing.Color * ?XTitle:string * ?YTitle:string -> ChartTypes.GenericChart
static member Chart.FastLine : data:seq<#key * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Drawing.Color * ?XTitle:string * ?YTitle:string -> ChartTypes.GenericChart
val s1 : Series<DateTimeOffset,float>
val series : observations:seq<'a * 'b> -> Series<'a,'b> (requires equality)
val s2 : Series<DateTimeOffset,float>
val s3 : Series<DateTimeOffset,float>
member Series.Zip : otherSeries:Series<'K,'V2> -> Series<'K,('V opt * 'V2 opt)>
member Series.Zip : otherSeries:Series<'K,'V2> * kind:JoinKind -> Series<'K,('V opt * 'V2 opt)>
member Series.Zip : otherSeries:Series<'K,'V2> * kind:JoinKind * lookup:Lookup -> Series<'K,('V opt * 'V2 opt)>
type JoinKind =
  | Outer = 0
  | Inner = 1
  | Left = 2
  | Right = 3
JoinKind.Left: JoinKind = 2
JoinKind.Right: JoinKind = 3
type Lookup =
  | Exact = 1
  | ExactOrGreater = 3
  | ExactOrSmaller = 5
  | Greater = 2
  | Smaller = 4
Lookup.ExactOrSmaller: Lookup = 5
val f1 : Frame<DateTimeOffset,string>
Multiple items
module Frame

from Deedle

--------------------
type Frame =
  static member ReadCsv : stream:Stream * hasHeaders:Nullable<bool> * inferTypes:Nullable<bool> * inferRows:Nullable<int> * schema:string * separators:string * culture:string * maxRows:Nullable<int> * missingValues:string [] * preferOptions:Nullable<bool> -> Frame<int,string>
  static member ReadCsv : location:string * hasHeaders:Nullable<bool> * inferTypes:Nullable<bool> * inferRows:Nullable<int> * schema:string * separators:string * culture:string * maxRows:Nullable<int> * missingValues:string [] * preferOptions:bool -> Frame<int,string>
  static member ReadReader : reader:IDataReader -> Frame<int,string>
  static member CustomExpanders : Dictionary<Type,Func<obj,seq<string * Type * obj>>>
  static member NonExpandableInterfaces : ResizeArray<Type>
  static member NonExpandableTypes : HashSet<Type>

--------------------
type Frame<'TRowKey,'TColumnKey (requires equality and equality)> =
  interface IDynamicMetaObjectProvider
  interface INotifyCollectionChanged
  interface IFsiFormattable
  interface IFrame
  new : names:seq<'TColumnKey> * columns:seq<ISeries<'TRowKey>> -> Frame<'TRowKey,'TColumnKey>
  new : rowIndex:IIndex<'TRowKey> * columnIndex:IIndex<'TColumnKey> * data:IVector<IVector> * indexBuilder:IIndexBuilder * vectorBuilder:IVectorBuilder -> Frame<'TRowKey,'TColumnKey>
  member AddColumn : column:'TColumnKey * series:ISeries<'TRowKey> -> unit
  member AddColumn : column:'TColumnKey * series:seq<'V> -> unit
  member AddColumn : column:'TColumnKey * series:ISeries<'TRowKey> * lookup:Lookup -> unit
  member AddColumn : column:'TColumnKey * series:seq<'V> * lookup:Lookup -> unit
  ...

--------------------
new : names:seq<'TColumnKey> * columns:seq<ISeries<'TRowKey>> -> Frame<'TRowKey,'TColumnKey>
new : rowIndex:Indices.IIndex<'TRowKey> * columnIndex:Indices.IIndex<'TColumnKey> * data:IVector<IVector> * indexBuilder:Indices.IIndexBuilder * vectorBuilder:Vectors.IVectorBuilder -> Frame<'TRowKey,'TColumnKey>
static member Frame.ofColumns : cols:Series<'C,#ISeries<'R>> -> Frame<'R,'C> (requires equality and equality)
static member Frame.ofColumns : cols:seq<'C * #ISeries<'R>> -> Frame<'R,'C> (requires equality and equality)
val f2 : Frame<DateTimeOffset,string>
val f3 : Frame<DateTimeOffset,string>
member Frame.Join : otherFrame:Frame<'TRowKey,'TColumnKey> -> Frame<'TRowKey,'TColumnKey>
member Frame.Join : colKey:'TColumnKey * series:Series<'TRowKey,'V> -> Frame<'TRowKey,'TColumnKey>
member Frame.Join : otherFrame:Frame<'TRowKey,'TColumnKey> * kind:JoinKind -> Frame<'TRowKey,'TColumnKey>
member Frame.Join : colKey:'TColumnKey * series:Series<'TRowKey,'V> * kind:JoinKind -> Frame<'TRowKey,'TColumnKey>
member Frame.Join : otherFrame:Frame<'TRowKey,'TColumnKey> * kind:JoinKind * lookup:Lookup -> Frame<'TRowKey,'TColumnKey>
member Frame.Join : colKey:'TColumnKey * series:Series<'TRowKey,'V> * kind:JoinKind * lookup:Lookup -> Frame<'TRowKey,'TColumnKey>
JoinKind.Outer: JoinKind = 0
JoinKind.Inner: JoinKind = 1
Lookup.Exact: Lookup = 1
val join : kind:JoinKind -> frame1:Frame<'R,'C> -> frame2:Frame<'R,'C> -> Frame<'R,'C> (requires equality and equality)
val joinAlign : kind:JoinKind -> lookup:Lookup -> frame1:Frame<'R,'C> -> frame2:Frame<'R,'C> -> Frame<'R,'C> (requires equality and equality)
val lf : Series<DateTimeOffset,float>
Multiple items
module Series

from Deedle

--------------------
type Series =
  static member ofNullables : values:seq<Nullable<'a0>> -> Series<int,'a0> (requires default constructor and value type and 'a0 :> ValueType)
  static member ofObservations : observations:seq<'c * 'd> -> Series<'c,'d> (requires equality)
  static member ofOptionalObservations : observations:seq<'K * 'a1 option> -> Series<'K,'a1> (requires equality)
  static member ofValues : values:seq<'a> -> Series<int,'a>

--------------------
type Series<'K,'V (requires equality)> =
  interface IFsiFormattable
  interface ISeries<'K>
  new : pairs:seq<KeyValuePair<'K,'V>> -> Series<'K,'V>
  new : keys:'K [] * values:'V [] -> Series<'K,'V>
  new : keys:seq<'K> * values:seq<'V> -> Series<'K,'V>
  new : index:IIndex<'K> * vector:IVector<'V> * vectorBuilder:IVectorBuilder * indexBuilder:IIndexBuilder -> Series<'K,'V>
  member After : lowerExclusive:'K -> Series<'K,'V>
  member Aggregate : aggregation:Aggregation<'K> * observationSelector:Func<DataSegment<Series<'K,'V>>,KeyValuePair<'TNewKey,OptionalValue<'R>>> -> Series<'TNewKey,'R> (requires equality)
  member Aggregate : aggregation:Aggregation<'K> * keySelector:Func<DataSegment<Series<'K,'V>>,'TNewKey> * valueSelector:Func<DataSegment<Series<'K,'V>>,OptionalValue<'R>> -> Series<'TNewKey,'R> (requires equality)
  member AsyncMaterialize : unit -> Async<Series<'K,'V>>
  ...

--------------------
new : pairs:seq<Collections.Generic.KeyValuePair<'K,'V>> -> Series<'K,'V>
new : keys:seq<'K> * values:seq<'V> -> Series<'K,'V>
new : keys:'K [] * values:'V [] -> Series<'K,'V>
new : index:Indices.IIndex<'K> * vector:IVector<'V> * vectorBuilder:Vectors.IVectorBuilder * indexBuilder:Indices.IIndexBuilder -> Series<'K,'V>
val window : size:int -> series:Series<'K,'T> -> Series<'K,Series<'K,'T>> (requires equality)
val windowInto : size:int -> f:(Series<'K,'T> -> 'R) -> series:Series<'K,'T> -> Series<'K,'R> (requires equality)
type Stats =
  static member count : frame:Frame<'R,'C> -> Series<'C,int> (requires equality and equality)
  static member count : series:Series<'K,'V> -> int (requires equality)
  static member describe : series:Series<'K,'V> -> Series<string,float> (requires equality and equality)
  static member expandingCount : series:Series<'K,'V> -> Series<'K,float> (requires equality)
  static member expandingKurt : series:Series<'K,'V> -> Series<'K,float> (requires equality)
  static member expandingMax : series:Series<'K,'V> -> Series<'K,float> (requires equality)
  static member expandingMean : series:Series<'K,'V> -> Series<'K,float> (requires equality)
  static member expandingMin : series:Series<'K,'V> -> Series<'K,float> (requires equality)
  static member expandingSkew : series:Series<'K,'V> -> Series<'K,float> (requires equality)
  static member expandingStdDev : series:Series<'K,'V> -> Series<'K,float> (requires equality)
  ...
static member Stats.mean : frame:Frame<'R,'C> -> Series<'C,float> (requires equality and equality)
static member Stats.mean : series:Series<'K,'V> -> float (requires equality)
val firstValue : series:Series<'K,'V> -> 'V (requires equality)
val lfm1 : Series<DateTimeOffset,float>
val lfm2 : Series<DateTimeOffset,float>
val windowSizeInto : int * Boundary -> f:(DataSegment<Series<'K,'T>> -> 'R) -> series:Series<'K,'T> -> Series<'K,'R> (requires equality)
type Boundary =
  | AtBeginning = 1
  | AtEnding = 2
  | Skip = 4
Boundary.AtBeginning: Boundary = 1
val ds : DataSegment<Series<DateTimeOffset,float>>
property DataSegment.Data: Series<DateTimeOffset,float>
val st : Series<int,char>
static member Series.ofValues : values:seq<'a> -> Series<int,'a>
Boundary.AtEnding: Boundary = 2
Multiple items
union case DataSegment.DataSegment: DataSegmentKind * 'T -> DataSegment<'T>

--------------------
module DataSegment

from Deedle

--------------------
type DataSegment<'T> =
  | DataSegment of DataSegmentKind * 'T
    override ToString : unit -> string
    member Data : 'T
    member Kind : DataSegmentKind
active recognizer Complete: DataSegment<'a> -> Choice<'a,'a>
val ser : Series<int,char>
Multiple items
type String =
  new : value:char -> string + 7 overloads
  member Chars : int -> char
  member Clone : unit -> obj
  member CompareTo : value:obj -> int + 1 overload
  member Contains : value:string -> bool
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EndsWith : value:string -> bool + 2 overloads
  member Equals : obj:obj -> bool + 2 overloads
  member GetEnumerator : unit -> CharEnumerator
  member GetHashCode : unit -> int
  ...

--------------------
String(value: nativeptr<char>) : String
String(value: nativeptr<sbyte>) : String
String(value: char []) : String
String(c: char, count: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: char [], startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : String
val values : series:Series<'K,'T> -> seq<'T> (requires equality)
type Array =
  member Clone : unit -> obj
  member CopyTo : array:Array * index:int -> unit + 1 overload
  member GetEnumerator : unit -> IEnumerator
  member GetLength : dimension:int -> int
  member GetLongLength : dimension:int -> int64
  member GetLowerBound : dimension:int -> int
  member GetUpperBound : dimension:int -> int
  member GetValue : [<ParamArray>] indices:int[] -> obj + 7 overloads
  member Initialize : unit -> unit
  member IsFixedSize : bool
  ...
val ofSeq : source:seq<'T> -> 'T []
active recognizer Incomplete: DataSegment<'a> -> Choice<'a,'a>
val hourly : Series<DateTimeOffset,float>
val windowDist : distance:'D -> series:Series<'K,'T> -> Series<'K,Series<'K,'T>> (requires comparison and equality and member ( - ))
val windowWhile : cond:('K -> 'K -> bool) -> series:Series<'K,'T> -> Series<'K,Series<'K,'T>> (requires equality)
val d1 : DateTimeOffset
val d2 : DateTimeOffset
property DateTimeOffset.Date: DateTime
val hf : Series<DateTimeOffset,float>
val chunkSize : int * Boundary -> series:Series<'K,'T> -> Series<'K,Series<'K,'T>> (requires equality)
val chunkDistInto : distance:'D -> f:(Series<'K,'T> -> 'R) -> series:Series<'K,'T> -> Series<'K,'R> (requires comparison and equality and member ( - ))
val chunkWhile : cond:('K -> 'K -> bool) -> series:Series<'K,'T> -> Series<'K,Series<'K,'T>> (requires equality)
val k1 : DateTimeOffset
val k2 : DateTimeOffset
property DateTimeOffset.Hour: int
property DateTimeOffset.Minute: int
val pairwise : series:Series<'K,'T> -> Series<'K,('T * 'T)> (requires equality)
val pairwiseWith : f:('K -> 'T * 'T -> 'a) -> series:Series<'K,'T> -> Series<'K,'a> (requires equality)
val k : DateTimeOffset
val v1 : float
val v2 : float
val s : Series<DateTimeOffset,float>
member Series.GetAt : index:int -> 'V
val mf : Series<DateTimeOffset,float>
TimeSpan.FromSeconds(value: float) : TimeSpan
val keys : DateTimeOffset list
val m : float
DateTimeOffset.AddMinutes(minutes: float) : DateTimeOffset
val lookupAll : keys:seq<'K> -> lookup:Lookup -> series:Series<'K,'T> -> Series<'K,'T> (requires equality)
Lookup.ExactOrGreater: Lookup = 3
val resample : keys:seq<'K> -> dir:Direction -> series:Series<'K,'V> -> Series<'K,Series<'K,'V>> (requires equality)
type Direction =
  | Backward = 0
  | Forward = 1
Direction.Forward: Direction = 1
Direction.Backward: Direction = 0
val resampleInto : keys:seq<'K> -> dir:Direction -> f:('K -> Series<'K,'V> -> 'a) -> series:Series<'K,'V> -> Series<'K,'a> (requires equality)
member Series.Resample : keys:seq<'K> * direction:Direction -> Series<'K,Series<'K,'V>>
member Series.Resample : keys:seq<'K> * direction:Direction * valueSelector:Func<'K,Series<'K,'V>,'c> -> Series<'K,'c>
member Series.Resample : keys:seq<'K> * direction:Direction * valueSelector:Func<'TNewKey,Series<'K,'V>,'R> * keySelector:Func<'K,Series<'K,'V>,'TNewKey> -> Series<'TNewKey,'R> (requires equality)
val ds : Series<DateTimeOffset,float>
TimeSpan.FromHours(value: float) : TimeSpan
val resampleEquiv : keyProj:('K1 -> 'K2) -> series:Series<'K1,'V1> -> Series<'K2,Series<'K1,'V1>> (requires equality and equality)
val d : DateTimeOffset
static member SeriesExtensions.ResampleEquivalence : series:Series<'K,'V> * keyProj:Func<'K,'a> -> Series<'a,Series<'K,'V>> (requires equality and equality)
static member SeriesExtensions.ResampleEquivalence : series:Series<'K,'V> * keyProj:Func<'K,'a2> * aggregate:Func<Series<'K,'V>,'a3> -> Series<'a2,'a3> (requires equality and equality)
val days : string list
val nu : Series<DateTimeOffset,float>
val indexWith : keys:seq<'K2> -> series:Series<'K1,'T> -> Series<'K2,'T> (requires equality and equality)
val mapKeys : f:('K -> 'R) -> series:Series<'K,'T> -> Series<'R,'T> (requires equality and equality)
DateTimeOffset.Parse(input: string) : DateTimeOffset
DateTimeOffset.Parse(input: string, formatProvider: IFormatProvider) : DateTimeOffset
DateTimeOffset.Parse(input: string, formatProvider: IFormatProvider, styles: Globalization.DateTimeStyles) : DateTimeOffset
val sampled : Series<DateTime,Series<DateTimeOffset,float>>
val resampleUniform : fillMode:Lookup -> keyProj:('K1 -> 'K2) -> nextKey:('K2 -> 'K2) -> series:Series<'K1,'V> -> Series<'K2,Series<'K1,'V>> (requires equality and comparison)
val dt : DateTimeOffset
val dt : DateTime
DateTime.AddDays(value: float) : DateTime
static member SeriesExtensions.ResampleUniform : series:Series<'K,'V> * keyProj:Func<'K,'a> * nextKey:Func<'a,'a> -> Series<'a,'V> (requires equality and comparison)
static member SeriesExtensions.ResampleUniform : series:Series<'K1,'V> * keyProj:Func<'K1,'K2> * nextKey:Func<'K2,'K2> * fillMode:Lookup -> Series<'K2,'V> (requires equality and comparison)
val mapValues : f:('T -> 'R) -> series:Series<'K,'T> -> Series<'K,'R> (requires equality)
val indexOrdinally : series:Series<'K,'T> -> Series<int,'T> (requires equality)
static member Frame.ofRows : rows:seq<'R * #ISeries<'C>> -> Frame<'R,'C> (requires equality and equality)
static member Frame.ofRows : rows:Series<'R,#ISeries<'C>> -> Frame<'R,'C> (requires equality and equality)
val pr : Series<DateTimeOffset,float>
val sampleTime : interval:TimeSpan -> dir:Direction -> series:Series<'a,'b> -> Series<'a,Series<'a,'b>> (requires equality and member ( + ))
static member SeriesExtensions.Sample : series:Series<'K,'V> * keys:seq<'K> -> Series<'K,'V> (requires equality)
static member SeriesExtensions.Sample : series:Series<DateTimeOffset,'V> * interval:TimeSpan -> Series<DateTimeOffset,'V>
static member SeriesExtensions.Sample : series:Series<DateTimeOffset,'V> * interval:TimeSpan * dir:Direction -> Series<DateTimeOffset,'V>
static member SeriesExtensions.Sample : series:Series<DateTimeOffset,'V> * start:DateTimeOffset * interval:TimeSpan * dir:Direction -> Series<DateTimeOffset,'V>
val sampleTimeInto : interval:TimeSpan -> dir:Direction -> f:(Series<'K,'V> -> 'a) -> series:Series<'K,'V> -> Series<'K,'a> (requires equality and member ( + ))
val lastValue : series:Series<'K,'V> -> 'V (requires equality)
val sample : Series<DateTimeOffset,float>
val diff1 : Series<DateTimeOffset,float>
val diff : offset:int -> series:Series<'K,'T> -> Series<'K,'T> (requires equality and member ( - ))
val diffM1 : Series<DateTimeOffset,float>
val shift1 : Series<DateTimeOffset,float>
val shift : offset:int -> series:Series<'K,'T> -> Series<'K,'T> (requires equality)
val df : Frame<DateTimeOffset,string>
static member SeriesExtensions.Shift : series:Series<'K,'V> * offset:int -> Series<'K,'V> (requires equality)
val log : value:'T -> 'T (requires member Log)
static member SeriesExtensions.Diff : series:Series<'K,float> * offset:int -> Series<'K,float> (requires equality)
val abs : value:'T -> 'T (requires member Abs)
val adjust : v:float -> float
val v : float
val min : e1:'T -> e2:'T -> 'T (requires comparison)
val max : e1:'T -> e2:'T -> 'T (requires comparison)
val conv : x:float -> float


 Multiply all numeric columns by a given constant
val x : float
val mapRowValues : f:(ObjectSeries<'C> -> 'V) -> frame:Frame<'R,'C> -> Series<'R,'V> (requires equality and equality)
val os : ObjectSeries<string>
member ObjectSeries.As : unit -> Series<'K,'R>
Multiple items
val float : value:'T -> float (requires member op_Explicit)

--------------------
type float = Double

--------------------
type float<'Measure> = float
static member Stats.sum : frame:Frame<'R,'C> -> Series<'C,float> (requires equality and equality)
static member Stats.sum : series:Series<'K,'V> -> float (requires equality)
Fork me on GitHub