tslib-0.1.4: src/Data/TimeSeries/UTime.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE BangPatterns #-}
module Data.TimeSeries.UTime (
-- * Simple accessors
tsLength,
tsRange,
tsTraversed,
tsTraversedWithIndex,
toPairList,
-- * Lookup at time
tsSearch,
firstAfter,
lastBefore,
-- * Construction
fromSortedPairList,
fromUnsortedPairList,
fromPeriodicData,
-- * Lookup with interpolation
TSInterpolate(..),
interpolateAt,
linearBetween,
tsiExtend,
tsiNoExtend,
interpolateLinear,
extendInterpolateLinear,
tsGet,
-- * Slicing
tsSlice,
justAfter,
tsSliceByCount,
tsSplitAt,
-- * Merging time series
TSMerge(..),
tsMerge,
tsMergeWith,
tsMergeEnhance,
-- * Resampling time series
tsResampleLocal,
extendForward,
extendBackward,
tsResampleGlobal,
tsResampleMoving,
-- * Shifting time series in time
tsOffsetGeneral,
tsOffsetByPeriod,
) where
import Control.Lens
import Data.Bits (shiftR)
import Data.Ord (comparing)
import Data.Maybe (catMaybes, fromJust)
import Data.TimeSeries.Class
import Data.TimeSeries.Periodic
import Data.UTime (UTime(..))
import qualified Data.Vector.Generic as G
import Statistics.Function (sortBy)
tsLength :: TSeries ts a => ts a -> Int
tsLength = G.length . toVector
{-# INLINE tsLength #-}
-- | Returns the first and last time stamp of a time series.
tsRange :: TSeries ts a => ts a -> Maybe (UTime, UTime)
tsRange ts = case tsLength ts of
0 -> Nothing
_ -> Just (G.head v, G.last v)
where
v = tsTimes ts
-- TODO(klao): Going through lists is probably inefficient! Check, and
-- go through streams it that's the case.
-- | Traversal of the values of a time series with access to the time stamp as index.
tsTraversed :: (TSeries ts a, TSeries ts b) => IndexedTraversal UTime (ts a) (ts b) a b
tsTraversed f ts
= fromVector . G.fromListN (G.length v) <$> traverse (itraversed f) (G.toList v)
where
v = toVector ts
-- | Traversal of ('UTime', @value@) pairs of a time series with
-- access to a positional index.
--
-- The user of this traversal should guarantee to not reorder the
-- events (ie. that the time stamps are modified in a monotonic way).
tsTraversedWithIndex :: (TSeries ts a, TSeries ts b)
=> IndexedTraversal Int (ts a) (ts b) (UTime, a) (UTime, b)
tsTraversedWithIndex f ts
= fromVector . G.fromListN (G.length v) <$> itraversed f (G.toList v)
where
v = toVector ts
-- | Construct a time series from a list of (time stamp, value) pairs
--
-- Precondition: the list /have to/ be sorted by time stamps.
fromSortedPairList :: TSeries ts a => [(UTime, a)] -> ts a
fromSortedPairList = fromVector . G.fromList
-- | Construct a time series from a list of (time stamp, value) pairs
--
-- The list is sorted by time stamps on construction.
fromUnsortedPairList :: TSeries ts a => [(UTime, a)] -> ts a
fromUnsortedPairList = fromVector . sortBy (comparing fst) . G.fromList
-- | Returns a list of (time stamp, value) pairs of a time series
-- sorted by time stamps.
toPairList :: TSeries ts a => ts a -> [(UTime, a)]
toPairList = G.toList . toVector
-- | Zip a 'PeriodicSequence' with a list of values.
fromPeriodicData :: TSeries ts a => PeriodicSequence -> [a] -> ts a
fromPeriodicData ps as = fromSortedPairList $ zip (psToUTimeList ps) as
--------------------------------------------------------------------------------
-- Lookup
-- | Returns the lowest index @i@ such that all events after @i@ have time
-- stamp at least t.
tsSearch :: TSeries ts a => ts a
-> UTime -- ^ the reference time stamp @t@
-> Int -- ^ the returned index @i@
tsSearch ts t | n == 0 = 0
| otherwise = t `seq` go 0 n
where
v = tsTimes ts
n = G.length v
go !l !u | l >= u = l
| G.unsafeIndex v k < t = go (k+1) u
| otherwise = go l k
where
k = (l + u) `shiftR` 1
-- | Returns the first event that has time stamp not earlier than @t@
-- or 'Nothing' if there's no such event in the series.
firstAfter :: TSeries ts a => ts a
-> UTime -- ^ the refenrece time stmap @t@
-> Maybe (UTime, a)
firstAfter ts t | k < tsLength ts = Just (toVector ts G.! k)
| otherwise = Nothing
where
k = tsSearch ts t
-- | Returns the last event that has time stamp not later than @t@
-- or 'Nothing' if there's no such event in the series.
lastBefore :: TSeries ts a => ts a
-> UTime -- ^ the reference time stamp @t@
-> Maybe (UTime, a)
lastBefore ts t | k < n, t == t2 = Just e2
| k > 0 = Just e1
| otherwise = Nothing
where
k = tsSearch ts t
v = toVector ts
n = tsLength ts
e2@(t2, _) = v G.! k
e1 = v G.! (k-1)
--------------------------------------------------------------------------------
-- Slicing
-- | Returns the slice of the time series within the provided time interval.
--
-- The time interval is interpreted as half-open, ie. the start time
-- is inclusive, but the end time is not. If you need different
-- behavior use the provided convenience function: 'justAfter' (for
-- start time, to make it exclusive; for end time, to make it
-- inclusive).
tsSlice :: TSeries ts a => ts a
-> UTime -- ^ start time (inclusive)
-> UTime -- ^ end time (exclusive)
-> ts a
tsSlice ts tStart tEnd
| tStart > tEnd = error "tsSlice: start time later than end time"
| otherwise = fromVector $ G.slice start (end - start) $ toVector ts
where
start = tsSearch ts tStart
end = tsSearch ts tEnd
-- | Returns the next representable time value.
justAfter :: UTime -> UTime
justAfter = succ
-- | Returns the slice of the time series within the provided index interval.
--
-- The index interval is half-open: start index is inclusive, end
-- index is exclusive.
tsSliceByCount :: TSeries ts a => ts a
-> Int -- ^ start index (inclusive)
-> Int -- ^ end index (exclusive)
-> ts a
tsSliceByCount ts start end = fromVector $ G.slice start (end - start) $ toVector ts
-- | Split the time series into two parts: everything strictly before
-- the given time stamp, and everything after the given time stamp
-- (including it).
tsSplitAt :: TSeries ts a => UTime -> ts a -> (ts a, ts a)
tsSplitAt t ts = (before, after)
where
i = tsSearch ts t
v = toVector ts
(before, after) = G.splitAt i v & both %~ fromVector
--------------------------------------------------------------------------------
-- Interpolating
-- | Data type used to set up interpolation functions like
-- 'interpolateAt', 'tsMergeEnhance' and 'tsResampleLocal'.
data TSInterpolate a =
TSInterpolate
{ tsiBefore :: UTime -> (UTime, a) -> Maybe (UTime, a)
-- ^ Function to interpolate for time stamps that are before the first item in the time series.
, tsiAfter :: UTime -> (UTime, a) -> Maybe (UTime, a)
-- ^ Function to interpolate for time stamps that are after the last item in the time series.
, tsiBetween :: UTime -> (UTime, a) -> (UTime, a) -> Maybe (UTime, a)
-- ^ Function to interpolate for time stamps in the middle of the time series.
}
interpolateAt :: TSeries ts a => TSInterpolate a -- ^ Parameters to use during the interpolation.
-> ts a -- ^ Input time series to interpolate from.
-> UTime -- ^ Time stamp to interpolate at.
-> Maybe (UTime, a) -- ^ The result of interpolation.
interpolateAt inter ts t | k < n, t == t2 = Just e2
| k < n, k > 0 = tsiBetween inter t e1 e2
| n > 0, k == 0 = tsiBefore inter t e2
| n > 0, k == n = tsiAfter inter t e1
| otherwise = Nothing
where
k = tsSearch ts t
v = toVector ts
n = tsLength ts
e2@(t2, _) = v G.! k
e1 = v G.! (k-1)
-- | Helper function to linearly interpolate a numeric value between two events.
--
-- Assumes that the two events are in order and the interpolating time
-- is strictly between the two time stamps.
--
-- Useful as the 'tsiBetween' field of a 'TSInterpolate'.
linearBetween :: Fractional a => UTime -> (UTime, a) -> (UTime, a)
-> Maybe (UTime, a)
linearBetween ut (ut0, x0) (ut1, x1) = Just (ut, wx / (t1 - t0))
where
f (UTime us) = fromIntegral us
t = f ut
t0 = f ut0
t1 = f ut1
wx = x0 * (t1 - t) + x1 * (t - t0)
-- | Defines trivial extending of a time series: uses the the same
-- value as the first/last item.
--
-- To be used as 'tsiBefore' or 'tsiAfter' field of a 'TSInterpolate'.
tsiExtend :: UTime -> (UTime, a) -> Maybe (UTime, a)
tsiExtend t (_, x) = Just (t, x)
-- | Defines non-extending time series.
--
-- To be used as 'tsiBefore' or 'tsiAfter' field of a 'TSInterpolate'.
tsiNoExtend :: UTime -> (UTime, a) -> Maybe (UTime, a)
tsiNoExtend = const $ const Nothing
-- | Linearly interpolates within time series; does not extend.
interpolateLinear :: Fractional a => TSInterpolate a
interpolateLinear = TSInterpolate tsiNoExtend tsiNoExtend linearBetween
-- | Linearly interpolates within time series; extends at the ends.
extendInterpolateLinear :: Fractional a => TSInterpolate a
extendInterpolateLinear = TSInterpolate tsiExtend tsiExtend linearBetween
-- | Access any time stamp in the given time series, with linear
-- interpolation and trivial extension at the ends if needed.
tsGet :: (Fractional a, TSeries ts a) => ts a -> UTime -> a
tsGet ts = snd . fromJust . interpolateAt extendInterpolateLinear ts
--------------------------------------------------------------------------------
-- Time series merging
-- | Structure describing a recipe for a generic merge.
data TSMerge a b c =
TSMerge
{ tsmLeft :: UTime -> a -> Maybe c
, tsmRight :: UTime -> b -> Maybe c
, tsmBoth :: UTime -> a -> b -> Maybe c
}
-- | Generic (non-interpolating) merge.
--
-- Every time stamp considered independently from all the
-- other. Conversion or combination of values is made according to the
-- provided recipe, based on whether the value is present in one or
-- both time series.
tsMerge :: (TSeries ts a, TSeries ts b, TSeries ts c)
=> TSMerge a b c -> ts a -> ts b -> ts c
tsMerge (TSMerge mleft mright mboth) tsa tsb = fromSortedPairList . catMaybes $ go as0 bs0
where
as0 = toPairList tsa
bs0 = toPairList tsb
fleft (t,a) = (t,) <$> mleft t a
fright (t,b) = (t,) <$> mright t b
go as [] = map fleft as
go [] bs = map fright bs
go as@((ta, a) : as') bs@((tb, b) : bs')
| ta < tb = fleft (ta, a) : go as' bs
| ta > tb = fright (tb, b) : go as bs'
| otherwise = ((ta,) <$> mboth ta a b) : go as' bs'
-- | Simple (non-interpolating) merge, similar to zipWith.
--
-- Only the time stamps for which value is present in both series are
-- considered. Values are combined by the user-supplied function.
tsMergeWith :: (TSeries ts a, TSeries ts b, TSeries ts c)
=> (UTime -> a -> b -> c) -> ts a -> ts b -> ts c
tsMergeWith fboth tsa tsb = tsMerge merger tsa tsb
where
merger = TSMerge (const $ const Nothing) (const $ const Nothing) mboth
mboth t a b = Just $ fboth t a b
-- | Merges two time series by extending/resampling them to match each other.
--
-- The two time series are extended/resampled with 'tsResampleLocal'
-- using the provided interpolators; and then merged with
-- 'tsMergeWith'.
tsMergeEnhance :: (TSeries ts a, TSeries ts b, TSeries ts c)
=> (Bool, TSInterpolate a)
-> (Bool, TSInterpolate b)
-> (UTime -> a -> b -> c)
-> ts a -> ts b -> ts c
tsMergeEnhance aInterp bInterp fboth tsa tsb = tsMergeWith fboth tsaEnhanced tsbEnhanced
where
tsaEnhanced = tsResampleLocal (fst aInterp) (snd aInterp) (G.toList $ tsTimes tsb) tsa
tsbEnhanced = tsResampleLocal (fst bInterp) (snd bInterp) (G.toList $ tsTimes tsa) tsb
--------------------------------------------------------------------------------
-- Resampling
-- | Resample or extend a time series to have values at the provided time stamps.
--
-- Resampling is done by locally interpolating between the two
-- neighboring events (using the provided @interpolator@).
--
-- If the @extend?@ argument is @True@, the original time series is
-- extended; otherwise the result will have values only at the new
-- time stamps (and the elements of the original time series are
-- discarded if they don't appear in the provided time stamp
-- sequence).
tsResampleLocal :: TSeries ts a
=> Bool -- ^ extend?
-> TSInterpolate a -- ^ interpolator
-> [UTime] -- ^ time stamp sequence
-> ts a -> ts a
tsResampleLocal keepOriginal interp times series
= fromSortedPairList $ catMaybes $ go False (toPairList series) times
where
go _ [] _ = []
go eLast (p : ps) []
| not keepOriginal = []
| otherwise = map Just $ if eLast then ps else p : ps
go eLast ps@(p1@(t1, _) : ps') ts@(t : ts')
| t1 < t, keepOriginal, not eLast = Just p1 : go True ps ts
| t < t1 = tsiBefore interp t p1 : go eLast ps ts'
| t == t1 = Just p1 : go False ps' ts'
| otherwise = case ps' of
[] -> map (flip (tsiAfter interp) p1) ts
(p2@(t2, _) : _)
| t < t2 -> tsiBetween interp t p1 p2 : go eLast ps ts'
| otherwise -> go False ps' ts
-- | Extend/resample a time series by copying values forward.
extendForward :: TSeries ts a => Bool -> [UTime] -> ts a -> ts a
extendForward keepOriginal = tsResampleLocal keepOriginal interpForward
where
interpForward = TSInterpolate tsiNoExtend tsiExtend keepLeft
keepLeft t (_,x) _ = Just (t, x)
-- | Extend/resample a time series by copying values backward.
extendBackward :: TSeries ts a => Bool -> [UTime] -> ts a -> ts a
extendBackward keepOriginal = tsResampleLocal keepOriginal interpBackward
where
interpBackward = TSInterpolate tsiExtend tsiNoExtend keepRight
keepRight t _ (_,x) = Just (t, x)
-- | Resample a time series to have values at provided time stamps.
--
-- For every new time stamp a user supplied function is evaluated with
-- that time stamp and two sub-series: one containing every event
-- strictly before the time stamp, and one containing events at or
-- after the time stamp. The results are collected to create the
-- resulting time series.
tsResampleGlobal :: TSeries ts a
=> (UTime -> ts a -> ts a -> Maybe (UTime, a))
-> [UTime]
-> ts a -> ts a
tsResampleGlobal sample times series = fromSortedPairList $ catMaybes $ map sliceOn times
where
sliceOn t = sample t before after
where
(before, after) = tsSplitAt t series
-- | Resamples a time series by calculating aggregates over a moving
-- window of a given duration at the given time stamps.
--
-- If the time stamps for the window positions should be periodic too, you can use
-- @'psOver' ('tsRange' series)@ as the @times@ argument.
tsResampleMoving :: TSeries ts a
=> (UTime -> ts a -> Maybe a)
-> Period -- ^ window duration
-> [UTime] -- ^ @times@: time stamps for the window ends
-> ts a -> ts a
tsResampleMoving sample p = tsResampleGlobal gSample
where
gSample t before _ = (t,) <$> sample t window
where
(_, window) = tsSplitAt (periodStepBackUTime p t) before
--------------------------------------------------------------------------------
-- Shift time series in time.
-- | Shift time series in time by applying the user provided function
-- to all the time stamps.
--
-- Prerequisite: the provided time-modifying function has to be monotone.
tsOffsetGeneral :: TSeries ts a
=> (UTime -> UTime)
-> ts a -> ts a
tsOffsetGeneral f = tsTraversedWithIndex . _1 %~ f
tsOffsetByPeriod :: TSeries ts a
=> Period
-> ts a -> ts a
tsOffsetByPeriod p = tsOffsetGeneral $ periodStepUTime p