Data
import pandas as pd
start_times = pd.DatetimeIndex(['2021-01-01 10:30:00', '2021-01-01 09:00:00', '2021-01-01 22:00:00'])
end_times = pd.DatetimeIndex(['2021-01-03 16:30:00', '2021-01-01 15:30:00', '2021-01-01 23:00:00'])
categories = ['A', 'B', 'B']
df = pd.DataFrame({'datetime_start': start_times, 'datetime_end': end_times, 'event_category': categories})
Answer
First we groupby event_category so that the apply works per category. The concatenation of the two series represents the changes in the events, that is, the beginnings and ends of events. The groupby and sum inside the apply are needed in case there are multiple events which start or end at the same time in the same category. The cumulative sum (cumsum) gives the total number of events at the times that there were changes, that is, at the times when one or more event started or ended. Next we upsample with asfreq to the desired frequency. This should be at least equal to the time granularity of the data. Finally we resample again (implemented with groupby and Grouper objects) and sum.
Essentially we are counting the number of periods occupied by all the events in each category and multiplying by the length of a period (half hour in the example) and then grouping by day. The DateOffset object is used to parametrize the period.
step = pd.DateOffset(hours=0.5) # Half hour steps
df.groupby('event_category') \
.apply(lambda x: pd.concat([pd.Series(1, x['datetime_start']),
pd.Series(-1, x['datetime_end'])]) \
.groupby(level=0) \
.sum() \
.cumsum() \
.asfreq(step, method='ffill')
) \
.groupby([pd.Grouper(level=0), pd.Grouper(level=1, freq='D')]) \
.sum() * step.hours
This will work for overlapping events in the same category.
Results
event_category
A 2021-01-01 13.5
2021-01-02 24.0
2021-01-03 16.5
B 2021-01-01 7.5
dtype: float64