Binning custom data based on a unix timestamp - python

I have a dict of data entries with a UNIX epoch timestamp as the key, and some value (this could be Boolean, int, float, enumerated string). I'm trying to set up a method that takes a start time, end time, and bin size (x minutes, x hours or x days), puts the values in the dict into the array of one of the bins between these times.
Essentially, I'm trying to convert data from the real world measured at a certain time to data occurring on a time-step, starting at time=0 and going until time=T, where the length of the time step can be set when calling the method.
I'm trying to make something along the lines of:
def binTimeSeries(dict, startTime, endTime, timeStep):
bins = []
#floor begin time to a timeStep increment
#ciel end time to a timeStep increment
for key in dict.keys():
if key > floorStartTime and key < cielEndTime:
timeDiff = (key - floorStartTime)
binIndex = floor(timeDiff/timeStep)
bins[binIndex].append(dict[key])
I'm having trouble working out what time format is suitable to do the conversion from UNIX epoch timestamp to, that can handle the floor, ciel and modulo operations given a variable timeStep interval, and then how to actually perform those operations. I've searched for this, but am getting confused with the formalisms for datetime, pandas, and which might be more suitable for this.

Maybe something like this? Instead of asking for a bin size (the interval of each bin), I think it makes more sense to ask how many bins you'd like instead. That way you're guaranteed each bin will be the same size (cover the same interval).
In my example below, I generated some fake data, which I called data. The start- and end-timestamps I picked arbitrarily, as well as the number of bins. I calculate the difference between the end- and start-timestamps, which I'm calling the duration - this yields the total duration between the two timestamps (I realize it's a bit silly to recalculate this value, seeing as how I hardcoded it earlier in the end_time_stamp definition, but it's just there for completeness). The bin_interval (in seconds) can be calculated by dividing the duration by the number of bins.
I ended up doing everything just using plain old UNIX / POSIX timestamps, without any conversion. However, I will mention that datetime.datetime has a method called fromtimestamp, which accepts a POSIX timestamp and returns a datetime object populated with the year, month, seconds, etc.
In addition, in my example, all I end up adding to the bins are the keys - just for demonstration - you'll have to modify it to suit your needs.
def main():
import time
values = ["A", "B", "C", "D", "E", "F", "G"]
data = {time.time() + (offset * 32): value for offset, value in enumerate(values)}
start_time_stamp = time.time() + 60
end_time_stamp = start_time_stamp + 75
number_of_bins = 12
assert end_time_stamp > start_time_stamp
duration = end_time_stamp - start_time_stamp
bin_interval = duration / number_of_bins
bins = [[] for _ in range(number_of_bins)]
for key, value in data.items():
if not (start_time_stamp <= key <= end_time_stamp):
continue
for bin_index, current_bin in enumerate(bins):
if start_time_stamp + (bin_index * bin_interval) <= key < start_time_stamp + ((bin_index + 1) * bin_interval):
current_bin.append(key)
break
print("Original data:")
for key, value in data.items():
print(key, value)
print(f"\nStart time stamp: {start_time_stamp}")
print(f"End time stamp: {end_time_stamp}\n")
print(f"Bin interval: {bin_interval}")
print("Bins:")
for current_bin in bins:
print(current_bin)
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
Output:
Original data:
1573170895.1871762 A
1573170927.1871762 B
1573170959.1871762 C
1573170991.1871762 D
1573171023.1871762 E
1573171055.1871762 F
1573171087.1871762 G
Start time stamp: 1573170955.1871762
End time stamp: 1573171030.1871762
Bin interval: 6.25
Bins:
[1573170959.1871762]
[]
[]
[]
[]
[1573170991.1871762]
[]
[]
[]
[]
[1573171023.1871762]
[]

Related

Using the datetime timedelta function

I'm wondering how I can make these three statements a single statement that I loop through:
minute_dt_array = np.arange(start, end, dt.timedelta(minutes=1)).astype(dt.datetime)
hour_dt_array = np.arange(start, end, dt.timedelta(hours=1)).astype(dt.datetime)
day_dt_array = np.arange(start, end, dt.timedelta(days=1)).astype(dt.datetime)
If I want to create a list that is [minutes, days, hours] so I can iterate through a single statement as opposed to writing it three times. How do I do that?
for example, I'm looking to write a loop that does something like this:
timeunits = ['day','hour','minute']
for interval in timeunits:
arrays['%s_array' % interval] = np.arange(start, end, dt.timedelta(**interval**=1)).astype(dt.datetime)
But I don't know what to put in the time delta function.
If you want to be able to call the array by name, what about a zip to fill a dict?
from datetime import datetime, timedelta
import numpy as np
start, end = datetime(2020,11,20), datetime(2020,11,22)
arrays = dict()
for k, i in zip(('days','hours','minutes'), (1, 1/24, 1/1440)):
arrays[k] = np.arange(start, end, timedelta(i)).astype(datetime)
# one-liner:
# arrays = {k: np.arange(start, end, timedelta(i)).astype(datetime) for k, i in zip(('days','hours','minutes'), (1, 1/24, 1/1440))}
Or if it is sufficient that arrays is a list, simply iterate the time intervals as
arrays = []
for i in (1, 1/24, 1/1440):
arrays.append(np.arange(start, end, timedelta(i)).astype(datetime))
There is no need to do that as with your intervals:
1d = 24h = 1440 minutes
So, every 60minutes you have 1h, every 24hrs you get 1d.
When you combine intervals as you suggest you get duplicate data points for for 1h timedelta resolution, and even triples for daily resolution. So, it should be enough to use the first granularity and just check if you got a round number of minutes, hours:
minute_dt_array = np.arange(start, end, dt.timedelta(minutes=1)).astype(dt.datetime)
the_list = []
for n, dtt in enumerate(minute_dt_array):
the_list.append([n // 1440, n // 60, n]) # days, hours, minutes
the_list is what you need.

How to get a period of time with Numpy?

If a np.datetime64 type data is given, how to get a period of time around the time?
For example, if np.datetime64('2020-04-01T21:32') is given to a function, I want the function to return np.datetime64('2020-04-01T21:30') and np.datetime64('2020-04-01T21:39') - 10 minutes around the given time.
Is there any way to do this with numpy?
Numpy does not have a built in time period like Pandas does.
If all you want are two time stamps the following function should work.
def ten_minutes_around(t):
t = ((t.astype('<M8[m]') # Truncate time to integer minute representation
.astype('int') # Convert to integer representation
// 10) * 10 # Remove any sub 10 minute minutes
).astype('<M8[m]') # convert back to minute timestamp
return np.array([t, t + np.timedelta64(10, 'm')]).T
For example:
for t in [np.datetime64('2020-04-01T21:32'), np.datetime64('2052-02-03T13:56:03.172')]:
s, e = ten_minutes_around(t)
print(s, t, e)
gives:
2020-04-01T21:30 2020-04-01T21:32 2020-04-01T21:40
2652-02-03T13:50 2652-02-03T13:56:03.172 2652-02-03T14:00
and
ten_minutes_around(np.array([
np.datetime64('2020-04-01T21:32'),
np.datetime64('2652-02-03T13:56:03.172'),
np.datetime64('1970-04-01'),
]))
gives
array([['2020-04-01T21:30', '2020-04-01T21:40'],
['2652-02-03T13:50', '2652-02-03T14:00'],
['1970-04-01T00:00', '1970-04-01T00:10']], dtype='datetime64[m]')
To do so we can get the minute from the given time and subtract it from the given time to get the starting of the period and add 9 minutes to get the ending time of the period.
import numpy as np
time = '2020-04-01T21:32'
dt = np.datetime64(time)
base = (dt.tolist().time().minute) % 10 // base would be 3 in this case
start = dt - np.timedelta64(base,'m')
end = start + np.timedelta64(9,'m')
print(start,end,sep='\n')
I hope this helps.

Modifying the date index of pandas dataframe

I am trying to write a highly efficient function that would take an average size dataframe (~5000 rows) and return a dataframe with column of the latest year (and same index) such that for each date index of the original dataframe the month containing that date is between some pre-specified start date (st_d) and end date (end_d). I wrote a code where the year is decremented till the month for a particular dateindex is within the desired range. However, it is really slow. For the dataframe with only 366 entries it takes ~0.2s. I need to make it at least an order of magnitude faster so that I can repeatedly apply it to tens of thousands of dataframes. I would very much appreciate any suggestions for this.
import pandas as pd
import numpy as np
import time
from pandas.tseries.offsets import MonthEnd
def year_replace(st_d, end_d, x):
tmp = time.perf_counter()
def prior_year(d):
# 100 is number of the years back, more than enough.
for i_t in range(100):
#The month should have been fully seen in one of the data years.
t_start = pd.to_datetime(str(d.month) + '/' + str(end_d.year - i_t), format="%m/%Y")
t_end = t_start + MonthEnd(1)
if t_start <= end_d and t_start >= st_d and t_end <= end_d and t_end >= st_d:
break
if i_t < 99:
return t_start.year
else:
raise BadDataException("Not enough data for Gradient Boosted tree.")
output = pd.Series(index = x.index, data = x.index.map(lambda tt: prior_year(tt)), name = 'year')
print("time for single dataframe replacement = ", time.perf_counter() - tmp)
return output
i = pd.date_range('01-01-2019', '01-01-2020')
x = pd.DataFrame(index = i, data=np.full(len(i), 0))
st_d = pd.to_datetime('01/2016', format="%m/%Y")
end_d = pd.to_datetime('01/2018', format="%m/%Y")
year_replace(st_d, end_d, x)
My advice is: avoid loop whenever you can and check out if an easier way is available.
If I do understand what you aim to do is:
For given start and stop timestamps, find the latest (higher) timestamp t where month is given from index and start <= t <= stop
I believe this can be formalized as follow (I kept your function signature for conveniance):
def f(start, stop, x):
assert start < stop
tmp = time.perf_counter()
def y(d):
# Check current year:
if start <= d.replace(day=1, year=stop.year) <= stop:
return stop.year
# Check previous year:
if start <= d.replace(day=1, year=stop.year-1) <= stop:
return stop.year-1
# Otherwise fail:
raise TypeError("Ooops")
# Apply to index:
df = pd.Series(index=x.index, data=x.index.map(lambda t: y(t)), name='year')
print("Tick: ", time.perf_counter() - tmp)
return df
It seems to execute faster as requested (almost two decades, we should benchmark to be sure, eg.: with timeit):
Tick: 0.004744200000004639
There is no need to iterate, you can just check current and previous year. If it fails, it cannot exist a timestamp fulfilling your requirements.
If the day must be kept, then just remove the day=1 in replace method. If you require cut criteria not being equal then modify inequalities accordingly. The following function:
def y(d):
if start < d.replace(year=stop.year) < stop:
return stop.year
if start < d.replace(year=stop.year-1) < stop:
return stop.year-1
raise TypeError("Ooops")
Returns the same dataframe as yours.

Verifying timestamps in a time series

I am working with time series data and I would like to know if there is a efficient & pythonic way to verify if the sequence of timestamps associated to the series is valid. In other words, I would like to know if the sequence of time stamps is in the correct ascending order without missing or duplicated values.
I suppose that verifying the correct order and the presence of duplicated values should be fairly straightforward but I am not so sure about the detection of missing timestamps.
numpy.diff can be used to find the difference between subsequent time stamps. These diffs can then be evaluated to determine if the timestamps look as expected:
import numpy as np
import datetime as dt
def errant_timestamps(ts, expected_time_step=None, tolerance=0.02):
# get the time delta between subsequent time stamps
ts_diffs = np.array([tsd.total_seconds() for tsd in np.diff(ts)])
# get the expected delta
if expected_time_step is None:
expected_time_step = np.median(ts_diffs)
# find the index of timestamps that don't match the spacing of the rest
ts_slow_idx = np.where(ts_diffs < expected_time_step * (1-tolerance))[0] + 1
ts_fast_idx = np.where(ts_diffs > expected_time_step * (1+tolerance))[0] + 1
# find the errant timestamps
ts_slow = ts[ts_slow_idx]
ts_fast = ts[ts_fast_idx]
# if the timestamps appear valid, return None
if len(ts_slow) == 0 and len(ts_fast) == 0:
return None
# return any errant timestamps
return ts_slow, ts_fast
sample_timestamps = np.array(
[dt.datetime.strptime(sts, "%d%b%Y %H:%M:%S") for sts in (
"05Jan2017 12:45:00",
"05Jan2017 12:50:00",
"05Jan2017 12:55:00",
"05Jan2017 13:05:00",
"05Jan2017 13:10:00",
"05Jan2017 13:00:00",
)]
)
print errant_timestamps(sample_timestamps)

pandas.date_range accurate freq parameter

I'm trying to generate a pandas.DateTimeIndex with a samplefrequency of 5120 Hz. That gives a period of increment=0.0001953125 seconds.
If you try to use pandas.date_range(), you need to specify the frequency (parameter freq) as str or as pandas.DateOffset. The first one can only handle an accuracy up to 1 ns, the latter has a terrible performance compared to the str and has even a worse error.
When using the string, I construct is as follows:
freq=str(int(increment*1e9))+'N')
which performs my 270 Mb file in less than 2 seconds, but I have an error (in the DateTimeIndex) after 3 million records of about 1500 µs.
When using the pandas.DateOffset, like this
freq=pd.DateOffset(seconds=increment)
it parses the file in 1 minute and 14 seconds, but has an error of about a second.
I also tried constructing the DateTimeIndex using
starttime + pd.to_timedelta(cumulativeTimes, unit='s')
This sum takes also ages to complete, but is the only one which doesn't have the error in the resulting DateTimeIndex.
How can I achieve a performant generation of the DateTimeIndex, keeping my accuracy?
I used a pure numpy implementation to fix this:
accuracy = 'ns'
relativeTime = np.linspace(
offset,
offset + (periods - 1) * increment,
periods)
def unit_correction(u):
if u is 's':
return 1e0
elif u is 'ms':
return 1e3
elif u is 'us':
return 1e6
elif u is 'ns':
return 1e9
# Because numpy only knows ints as its date datatype,
# convert to accuracy.
return (np.datetime64(starttime)
+ (relativeTime*unit_correction(accuracy)).astype(
"timedelta64["+accuracy+"]"
)
)
(this is the github pull request for people interested: https://github.com/adamreeve/npTDMS/pull/31)
I think I reach a similar result with the function below (although it uses only nanosecond precision):
def date_range_fs(duration, fs, start=0):
""" Create a DatetimeIndex based on sampling frequency and duration
Args:
duration: number of seconds contained in the DatetimeIndex
fs: sampling frequency
start: Timestamp at which de DatetimeIndex starts (defaults to POSIX
epoch)
Returns: the corresponding DatetimeIndex
"""
return pd.to_datetime(
np.linspace(0, 1e9*duration, num=fs*duration, endpoint=False),
unit='ns',
origin=start)

Categories