- The correct way to bin a
pandas.DataFrameis to usepandas.cut - Verify the date column is in a
datetimeformat withpandas.to_datetime. - Use
.dt.hourto extract the hour, for use in the.cutmethod. - Tested in
python 3.8.11andpandas 1.3.1
How to bin the data
import pandas as pd
import numpy as np # for test data
import random # for test data
# setup a sample dataframe; creates 1.5 months of hourly observations
np.random.seed(365)
random.seed(365)
data = {'date': pd.bdate_range('2020-09-21', freq='h', periods=1100).tolist(),
'x': np.random.randint(10, size=(1100))}
df = pd.DataFrame(data)
# the date column of the sample data is already in a datetime format
# if the date column is not a datetime, then uncomment the following line
# df.date= pd.to_datetime(df.date)
# define the bins
bins = [0, 6, 12, 18, 24]
# add custom labels if desired
labels = ['00:00-05:59', '06:00-11:59', '12:00-17:59', '18:00-23:59']
# add the bins to the dataframe
df['Time Bin'] = pd.cut(df.date.dt.hour, bins, labels=labels, right=False)
df.head()
date x Time Bin
0 2020-09-21 00:00:00 2 00:00-05:59
1 2020-09-21 01:00:00 4 00:00-05:59
2 2020-09-21 02:00:00 1 00:00-05:59
3 2020-09-21 03:00:00 5 00:00-05:59
4 2020-09-21 04:00:00 2 00:00-05:59
df.tail()
date x Time Bin
1095 2020-11-05 15:00:00 2 12:00-17:59
1096 2020-11-05 16:00:00 3 12:00-17:59
1097 2020-11-05 17:00:00 1 12:00-17:59
1098 2020-11-05 18:00:00 2 18:00-23:59
1099 2020-11-05 19:00:00 2 18:00-23:59
Groupby 'Time Bin'
- Use
pandas.DataFrame.groupbyon'Time Bin', and then aggregate'x'into alistandmean.
# groupby Time Bin and aggregate a list for the observations, and mean
dfg = df.groupby('Time Bin', as_index=False)['x'].agg([list, 'mean'])
# change the column names, if desired
dfg.columns = ['X Observations', 'X mean']
dfg
X Observations X mean
Time Bin
00:00-05:59 [2, 4, 1, 5, 2, 2, ...] 4.416667
06:00-11:59 [9, 8, 4, 0, 3, 3, ...] 4.760870
12:00-17:59 [7, 7, 7, 0, 8, 4, ...] 4.384058
18:00-23:59 [3, 2, 6, 2, 6, 8, ...] 4.459559
Answer from Trenton McKinney on Stack Overflow- The correct way to bin a
pandas.DataFrameis to usepandas.cut - Verify the date column is in a
datetimeformat withpandas.to_datetime. - Use
.dt.hourto extract the hour, for use in the.cutmethod. - Tested in
python 3.8.11andpandas 1.3.1
How to bin the data
import pandas as pd
import numpy as np # for test data
import random # for test data
# setup a sample dataframe; creates 1.5 months of hourly observations
np.random.seed(365)
random.seed(365)
data = {'date': pd.bdate_range('2020-09-21', freq='h', periods=1100).tolist(),
'x': np.random.randint(10, size=(1100))}
df = pd.DataFrame(data)
# the date column of the sample data is already in a datetime format
# if the date column is not a datetime, then uncomment the following line
# df.date= pd.to_datetime(df.date)
# define the bins
bins = [0, 6, 12, 18, 24]
# add custom labels if desired
labels = ['00:00-05:59', '06:00-11:59', '12:00-17:59', '18:00-23:59']
# add the bins to the dataframe
df['Time Bin'] = pd.cut(df.date.dt.hour, bins, labels=labels, right=False)
df.head()
date x Time Bin
0 2020-09-21 00:00:00 2 00:00-05:59
1 2020-09-21 01:00:00 4 00:00-05:59
2 2020-09-21 02:00:00 1 00:00-05:59
3 2020-09-21 03:00:00 5 00:00-05:59
4 2020-09-21 04:00:00 2 00:00-05:59
df.tail()
date x Time Bin
1095 2020-11-05 15:00:00 2 12:00-17:59
1096 2020-11-05 16:00:00 3 12:00-17:59
1097 2020-11-05 17:00:00 1 12:00-17:59
1098 2020-11-05 18:00:00 2 18:00-23:59
1099 2020-11-05 19:00:00 2 18:00-23:59
Groupby 'Time Bin'
- Use
pandas.DataFrame.groupbyon'Time Bin', and then aggregate'x'into alistandmean.
# groupby Time Bin and aggregate a list for the observations, and mean
dfg = df.groupby('Time Bin', as_index=False)['x'].agg([list, 'mean'])
# change the column names, if desired
dfg.columns = ['X Observations', 'X mean']
dfg
X Observations X mean
Time Bin
00:00-05:59 [2, 4, 1, 5, 2, 2, ...] 4.416667
06:00-11:59 [9, 8, 4, 0, 3, 3, ...] 4.760870
12:00-17:59 [7, 7, 7, 0, 8, 4, ...] 4.384058
18:00-23:59 [3, 2, 6, 2, 6, 8, ...] 4.459559
Whenever I bin time series data by a time range, which seems to be what you are doing here, I just create an "hour of day" column and slice over that. Also, I normally set the index as datetime values...though that is not necessary here.
# assuming your "timestamp" column is labeled ts:
df['hod'] = [r.hour for r in df.ts]
# now you can calculate stats for each bin
ave = df[ (df.hod>=0) & (df.hod<6) ].mean()
I would think there is a method of using df.resample here, but with the poorly defined starting/ending points in your time series I think this may require more attention than the above method.
Is this along the lines of what you were wanting?
Convert Time column to hours by Series.dt.hour and use cut for binning:
rng = pd.date_range('2017-04-03', periods=30, freq='H').strftime('%H:%M:%S')
df = pd.DataFrame({'Time': rng})
hours = pd.to_datetime(df['Time'], format='%H:%M:%S').dt.hour
df['cats'] = pd.cut(hours,
bins=[0,6,12,18,24],
include_lowest=True,
labels=['cat1','cat2','cat3','cat4'])
print (df)
Time cats
0 00:00:00 cat1
1 01:00:00 cat1
2 02:00:00 cat1
3 03:00:00 cat1
4 04:00:00 cat1
5 05:00:00 cat1
6 06:00:00 cat1
7 07:00:00 cat2
8 08:00:00 cat2
9 09:00:00 cat2
10 10:00:00 cat2
11 11:00:00 cat2
12 12:00:00 cat2
13 13:00:00 cat3
14 14:00:00 cat3
15 15:00:00 cat3
16 16:00:00 cat3
17 17:00:00 cat3
18 18:00:00 cat3
19 19:00:00 cat4
20 20:00:00 cat4
21 21:00:00 cat4
22 22:00:00 cat4
23 23:00:00 cat4
24 00:00:00 cat1
25 01:00:00 cat1
26 02:00:00 cat1
27 03:00:00 cat1
28 04:00:00 cat1
29 05:00:00 cat1
- Convert date to unix timestamp
def convert_to_unix(s):
return time.mktime(datetime.strptime(s, "%Y-%m-%d %H:%M:%S").timetuple())
- Then convert the timestamp to hours from seconds (60*60) and divide it by the time interval(6 hours, in this case)
df['bins'] = np.array( [ int ( convert_to_unix(i) / 60 * 60 * 6) for i in df['Time']] )
You can change the category after that.
I think @DrV's is the correct answer, but I've prepared an example trying to show how something similar could be achieved using Pandas:
import numpy
import pandas
import datetime
import time
# Binning delta
delta = datetime.timedelta(hours=1)
# Sample data
sample = [
['2014-08-09 16:30:00', 'label1'],
['2014-08-09 15:30:00', 'label2'],
['2014-08-09 14:30:00', 'label3'],
['2014-08-09 14:00:00', 'label4']
]
# Create dataframe and append UNIX timestamp column
df = pandas.DataFrame(sample)
df.columns = ['Datetime', 'Label']
df['Datetime'] = pandas.to_datetime(df['Datetime'])
df['UnixStamp'] = df['Datetime'].apply(lambda d: time.mktime(d.timetuple()))
df = df.set_index('Datetime')
# Calculate bins
bins = numpy.arange(min(df['UnixStamp']), max(df['UnixStamp']) + delta.seconds, delta.seconds)
# Group columns by datetime bin
def bin_from_tstamp(tstamp):
diffs = [abs(tstamp - bin) for bin in bins]
return bins[diffs.index(min(diffs))]
grouped = df.groupby(df['UnixStamp'].map(
lambda t: datetime.datetime.fromtimestamp(bin_from_tstamp(t))
))
At this point grouped contains the dataset grouped by datetime bins.
The following is the result of printing grouped.groups (where the keys are the datetime bins and the values are the grouped datetimes):
{
numpy.datetime64('2014-08-09T18:00:00.000000000+0200'): [
Timestamp('2014-08-09 16:30:00')
],
numpy.datetime64('2014-08-09T17:00:00.000000000+0200'): [
Timestamp('2014-08-09 15:30:00')
],
numpy.datetime64('2014-08-09T16:00:00.000000000+0200'): [
Timestamp('2014-08-09 14:30:00'),
Timestamp('2014-08-09 14:00:00'
]
}
Something along these lines should do:
# data: a lists of lists (length 2) of measurements
# res: resulting list of lists
# delta: time delta
# output list (will be a list of lists, as in the question
res = []
# end of first bin:
binstart = data[0][0]
res.append([binstart, []])
# iterate through the data item
for d in data:
# if the data item belongs to this bin, append it into the bin
if d[0] < binstart + delta:
res[-1][1].append(d[1])
continue
# otherwise, create new empty bins until this data fits into a bin
binstart += delta
while d[0] > binstart + delta:
res.append([binstart, [])
binstart += delta
# create a bin with the data
res.append([binstart, [d[1]]])
Converting to datetime and using pandas.Grouper + Offset Aliases:
df['date'] = pd.to_datetime(df.date)
df.groupby(pd.Grouper(key='date', freq='30min')).mean().dropna()
speed
date
2018-09-20 01:30:00 47.040000
2018-09-20 07:30:00 26.311429
2018-09-20 10:30:00 39.947500
2018-09-20 11:00:00 32.298000
Since your date column isn't really a date, it's probably more sensible to convert it to a timedelta that way you don't have a date attached to it.
Then, you can use dt.floor to group into 30 minute bins.
import pandas as pd
df['date'] = pd.to_timedelta(df.date)
df.groupby(df.date.dt.floor('30min')).mean()
Output:
speed
date
01:30:00 47.040000
07:30:00 26.311429
10:30:00 39.947500
11:00:00 32.298000
I know it's late. But better late than never. I also came across a similar requirement and done by using pandas library.
First, Load data in pandas data-frame
Second, check TIME column must be datetime object and not object type (like string or whatever). You can check it by
df.info()
for example, in my case TIME column was initially of object type i.e. string type
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17640 entries, 0 to 17639
Data columns (total 3 columns):
TIME 17640 non-null object
value 17640 non-null int64
dtypes: int64(1), object(2)
memory usage: 413.5+ KB
if that is the case, then convert it to pandas datetime object by using this command
df['TIME'] = pd.to_datetime(df['TIME'])ignore this if already in datetime format
df.info() now gives updated format
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17640 entries, 0 to 17639
Data columns (total 3 columns):
TIME 17640 non-null datetime64[ns]
value 17640 non-null int64
dtypes: datetime64ns, int64(1)
memory usage: 413.5 KB
Now our dataframe is ready for magic :)
counts = pd.Series(index=df.TIME, data=np.array(df.count)).resample('15T').count() print(counts[:3])TIME 2017-07-01 00:00:00 3 2017-07-01 00:15:00 3 2017-07-01 00:30:00 3 Freq: 15T, dtype: int64in above command
15Tmeans 15minutes bucket, you can replace it withDfor day bucket,2Dfor 2 days bucket,Mfor month bucket,2Mfor 2 months bucket and so on. You can read the detail of these notations on this linknow, our buckets data is done as you can see above. for time range use this command. Use the same time range as of data. In my case, my data was 3 months so I am creating time-range of 3 months.
r = pd.date_range('2017-07', '2017-09', freq='15T')
x = np.repeat(np.array(r), 2, axis=0)[1:-1]
# now reshape data to fit in Dataframe
x = np.array(x)[:].reshape(-1, 2)
# now fit in dataframe and print it
final_df = pd.DataFrame(x, columns=['start', 'end'])
print(final_df[:3])
start end
0 2017-07-01 00:00:00 2017-07-01 00:15:00
1 2017-07-01 00:15:00 2017-07-01 00:30:00
2 2017-07-01 00:30:00 2017-07-01 00:45:00
date ranges also done
Now append count and dateranges to get final outcome
final_df['count'] = np.array(means) print(final_df[:3])
start end count
0 2017-07-01 00:00:00 2017-07-01 00:15:00 3
1 2017-07-01 00:15:00 2017-07-01 00:30:00 3
2 2017-07-01 00:30:00 2017-07-01 00:45:00 3
Hope anyone find it useful.
Well, I'm not sure that this is what you asked for. If it's not, I would recommend you to improve your question, because it's very hard to understand your problem. In particular, it would be nice to see what you've already tried to do.
from __future__ import division, print_function
from collections import namedtuple
from itertools import product
from datetime import time
from StringIO import StringIO
MAX_HOURS = 23
MAX_MINUTES = 59
def process_data_file(data_file):
"""
The data_file is supposed to be an opened file object
"""
time_entry = namedtuple("time_entry", ["time", "count"])
data_to_bin = []
for line in data_file:
t, count = line.rstrip().split("\t")
t = map(int, t.split()[-1].split(":")[:2])
data_to_bin.append(time_entry(time(*t), int(count)))
return data_to_bin
def make_milestones(min_hour=0, max_hour=MAX_HOURS, interval=15):
minutes = [minutes for minutes in xrange(MAX_MINUTES+1) if not minutes % interval]
hours = range(min_hour, max_hour+1)
return [time(*milestone) for milestone in list(product(hours, minutes))]
def bin_time(data_to_bin, milestones):
time_entry = namedtuple("time_entry", ["time", "count"])
data_to_bin = sorted(data_to_bin, key=lambda time_entry: time_entry.time, reverse=True)
binned_data = []
current_count = 0
upper = milestones.pop()
lower = milestones.pop()
for entry in data_to_bin:
while not lower <= entry.time <= upper:
if current_count:
binned_data.append(time_entry("{}-{}".format(str(lower)[:-3], str(upper)[:-3]), current_count))
current_count = 0
upper, lower = lower, milestones.pop()
current_count += entry.count
return binned_data
data_file = StringIO("""1-1-1900 10:41:00\t1
3-1-1900 09:54:00\t1
4-1-1900 15:45:00\t1
5-1-1900 18:41:00\t1
4-1-1900 15:45:00\t1""")
binned_time = bin_time(process_data_file(data_file), make_milestones())
for entry in binned_time:
print(entry.time, entry.count, sep="\t")
The output:
18:30-18:45 1
15:45-16:00 2
10:30-10:45 1
You may want to take a look at pd.cut.
# toy data
df = pd.DataFrame(pd.date_range('2020-01-01', '2022-01-01'), columns = ['date'])
date
0 2020-01-01
1 2020-01-02
2 2020-01-03
3 2020-01-04
4 2020-01-05
.. ...
You can generate the labels and boundaries for the bins.
from numpy import datetime64
bin_labels = [1, 2, 3, 4]
cut_bins = [datetime64('2019-12-31'), datetime64('2020-04-01'), datetime64('2020-12-31'), datetime64('2021-09-01'), datetime64('2022-01-01')]
And save the bins into a new column.
df['cut'] = pd.cut(df['date'], bins = cut_bins, labels = bin_labels]
date cut
0 2020-01-01 1
1 2020-01-02 1
2 2020-01-03 1
3 2020-01-04 1
4 2020-01-05 1
.. ... ..
727 2021-12-28 4
728 2021-12-29 4
729 2021-12-30 4
730 2021-12-31 4
731 2022-01-01 4
Hope it helps.
I have found a way which I think works (for those who may be interested in binning date-time values in the future) - assume the data is the same as given in the question description:
from dateutil.relativedelta import relativedelta
import numpy as np
dates = []
start = df["date"].min().date()
dates.append(np.datetime64(start))
while start <= df["date"].max().date():
start = start + relativedetla(months = 21)
dates.append(np.datetime64(start))
df["Group"] = pd.cut(
df["date"], bins = dates,
labels = ["A", "B", "C", "D"],
right = False #right = False ensures no group overlap in date values
)
It works for me with pandas 0.23.4
import pandas as pd
import numpy as np
df = pd.DataFrame({
'userID': ['DSm7ysk', 'no51CdJ', 'foo', 'bar'],
'duration': [pd.Timedelta('3 hours 8 minutes 49 seconds'), pd.Timedelta('35 minutes 50 seconds'), pd.Timedelta('1 minutes 13 seconds'), pd.Timedelta('6 minutes 43 seconds')]
})
bins = [
pd.Timedelta(minutes = 0),
pd.Timedelta(minutes = 5),
pd.Timedelta(minutes = 10),
pd.Timedelta(minutes = 20),
pd.Timedelta(minutes = 30),
pd.Timedelta(hours = 4)
]
labels = ['0-5min', '5-10min', '10-20min', '20-30min', '30min+']
df['bins'] = pd.cut(df['duration'], bins, labels = labels)
Result:

You can normalize to seconds before binning. This reduces the problem to binning integers.
df = pd.DataFrame({'userID': ['A', 'B'],
'duration': pd.to_timedelta(['00:08:49', '00:35:50'])})
L = ['00:00:00', '00:05:00', '00:10:00', '00:20:00', '00:30:00', '04:00:00']
bins = pd.to_timedelta(L).total_seconds()
cats = ['0-5min', '5-10min', '10-20min', '20-30min', '30min+']
df['bins'] = pd.cut(df['duration'].dt.total_seconds(), bins, labels=cats)
print(df)
# duration userID bins
# 0 00:08:49 A 5-10min
# 1 00:35:50 B 30min+