Join us at FabCon Atlanta from March 16 - 20, 2026, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.
Register now!To celebrate FabCon Vienna, we are offering 50% off select exams. Ends October 3rd. Request your discount now.
Dear community,
I use Excel's PowerPivot plug-in and often found help by just reading this forum. Now it's time to ask my first question.
1) What I want to achieve:
Calculate productive minutes for each full hour. (see example at the bottom)
How could I do this in PowerPivot?
2) What data do I have?
My production team enters their production activities in a table. A timestamp is recorded. We don't work over night, only from 6am to 10pm. I cannot force the team to make an extra entry at the end of each full hour (15:00 // 16:00 // 17:00 etc), even though this would make my life much easier.
Example Table of entries with timestamps:
Timestamp | Activity |
15:00 | Packaging |
15:30 | Break |
16:30 | Packaging |
18:00 | Shift End |
… | … |
Now I need to calculate for each full hour (e.g. from 15:00 till 16:00 // 16:00 till 17:00 etc), how much time was spent packaging and how much was break or other activities?
Desired Result for this specific example:
| Packaging | Break | Other activities |
15:00-16:00 | 30min (from 15:00-15:30) | 30min | 0min |
16:00-17:00 | 30min (from 16:30-17:00) | 30min | 0min |
17:00-18:00 | 60min | 0min | 0min |
Thanks a lot!
Marcel
Wow Pete,
that is incredible! Thanks for making it work!
Is it possible to adjust the code, so that acitivities that go longer than an hour are also considered?
E.g.:
TimestampActivity
15:00 | Packaging |
15:30 | Break |
15:49 | Toilet |
16:01 | Packaging |
16:42 | Smoke |
17:17 | Toilet |
17:22 | Packaging |
19:00 | Shift End |
And that would be the desired result for the last two hours
timeRange | Packaging | Toilet | Smoke |
17:00-18:00 | 38 | 5 | 17 |
18:00-19:00 | 60 | null | null |
Thanks again!
Marcel
Hi @GotManyProblems ,
Try the updated code below. It gives me the following ouput (see new Invoicing activity):
let
Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WMjSyMjJV0lHyzCvLz0zOzEtXitUBippaGZgBRQMSk7MT05FEjQ2Aok5FqYnZMBETS6BISH5mTmoJRMjMysAQU6uZlYkRUDQ4Nz87FSJibmVojqbV3MrICFOrhZUByNbgjMy0EgXXvBSl2FgA", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Timestamp = _t, Activity = _t]),
chgTypes = Table.TransformColumnTypes(Source,{{"Timestamp", type time}, {"Activity", type text}}),
sortChronological = Table.Sort(chgTypes,{{"Timestamp", Order.Ascending}}),
addIndex1 = Table.AddIndexColumn(sortChronological, "Index1", 1, 1, Int64.Type),
addIndex0 = Table.AddIndexColumn(addIndex1, "Index0", 0, 1, Int64.Type),
mergeSelfOnIndex = Table.NestedJoin(addIndex0, {"Index1"}, addIndex0, {"Index0"}, "Added Index0", JoinKind.LeftOuter),
fixMergeSorting = Table.Sort(mergeSelfOnIndex,{{"Index1", Order.Ascending}}),
expandSelf = Table.ExpandTableColumn(fixMergeSorting, "Added Index0", {"Timestamp"}, {"endTime"}),
filterShiftEnd = Table.SelectRows(expandSelf, each ([endTime] <> null)),
remIndexes = Table.RemoveColumns(filterShiftEnd,{"Index1", "Index0"}),
addListOfHours = Table.AddColumn(remIndexes, "hourSplit", each List.Transform({Time.Hour([Timestamp])..Time.Hour([endTime])}, each Time.From(Text.Combine({Text.From(_),"00"},":")))),
expandListOfHours = Table.ExpandListColumn(addListOfHours, "hourSplit"),
addActivityDurMins = Table.AddColumn(expandListOfHours, "activityDurMins", each if Time.Hour([hourSplit]) <> Time.Hour([Timestamp]) and Time.Hour([hourSplit]) <> Time.Hour([endTime]) then 60
else if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) = Time.Hour([endTime]) then Duration.Minutes([endTime] -[Timestamp])
else if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) < Time.Hour([endTime]) then Duration.Minutes([hourSplit] - [Timestamp]) + 60
else if [Timestamp] <= [hourSplit] then Duration.Minutes([endTime] -[hourSplit])
else null),
filterZeroMins = Table.SelectRows(addActivityDurMins, each ([activityDurMins] <> 0)),
addTimeRange = Table.AddColumn(filterZeroMins, "timeRange", each let
timeFrom = Text.Combine({Text.From(Time.Hour([hourSplit])), "00"}, ":"),
timeTo = Text.Combine({Text.From(Time.Hour([hourSplit]) + 1), "00"}, ":")
in
Text.Combine({timeFrom, timeTo}, "-")),
remOthCols = Table.SelectColumns(addTimeRange,{"timeRange", "Activity", "activityDurMins"}),
pivotActivities = Table.Pivot(remOthCols, List.Distinct(remOthCols[Activity]), "Activity", "activityDurMins", List.Sum)
in
pivotActivities
Pete
Proud to be a Datanaut!
Hi Pete,
that's really impressive! Thank you so much and sorry for the delay in coming back to you.
May I make 2 more requests:
1st request:
It works really fine, but there's always a human factor... Sometimes they forget to make the last entry in the shift. E.g. for late shift (see below in the 39th line) we would miss 3 hours of production time, as there is no entry at 10pm and the next entry is from the next day:
ActivityActivityDateActivityTime
Verpackung am Arbeiten | 25/Okt/21 | 19:03:34 |
Staff Change | 26/Okt/21 | 06:21:49 |
The same can happen if we have an early shift, without a following late shift.
Is is possible to insert a "fake" line for the shift end at 2pm and 10pm, if this is the last line of this shift? I think this would need to be done before "addListOfHours".
I think to determine where to enter the 10pm shift-end, you can do something like this:
if "ActivityDate" of this line <> "ActivityDate" of the next line
Then add line with shiftend at 10pm
For the 2pm "fake" entry, maybe we could
If "endTime" of next line is >2pm
Then add line with shiftend at 2pm
2nd request:
Is it possible to calculate the duration in real time? E.g. if it is now 10:05am and the last entry was at 8:30-packaging to calculate for 8:00-9:00 30mins packaging and for 9:00-10:00 60mins packaging?
I adjusted your code so it fits to my datasource. Please find below how the data source looks like after step "remOthCol" and my complete code (excluding the SQL server's adress for safety reasons)
let
Quelle = Sql.Database("xxx.com", "ProductionDB"),
dbo_History = Quelle{[Schema="dbo",Item="History"]}[Data],
#"Gefilterte Zeilen" = Table.SelectRows(dbo_History, each ([JobID] = 19118) and ([JobStepID] = 21)),
#"Sortierte Zeilen" = Table.Sort(#"Gefilterte Zeilen",{{"JobID", Order.Ascending}, {"JobStepID", Order.Ascending}, {"ActivityDate", Order.Ascending}}),
remOthCol = Table.SelectColumns(#"Sortierte Zeilen",{"Activity", "ActivityDate"}),
#"Uhrzeit eingefügt" = Table.AddColumn(remOthCol, "ActivityTime", each DateTime.Time([ActivityDate]), type time),
#"Extrahiertes Datum" = Table.TransformColumns(#"Uhrzeit eingefügt",{{"ActivityDate", DateTime.Date, type date}}),
addIndex1 = Table.AddIndexColumn(#"Extrahiertes Datum", "Index1", 1, 1, Int64.Type),
addIndex0 = Table.AddIndexColumn(addIndex1, "Index0", 0, 1, Int64.Type),
mergeSelfOnIndex = Table.NestedJoin(addIndex0, {"Index1"}, addIndex0, {"Index0"}, "Added Index0", JoinKind.LeftOuter),
fixMergeSorting = Table.Sort(mergeSelfOnIndex,{{"Index1", Order.Ascending}}),
expandSelf = Table.ExpandTableColumn(fixMergeSorting, "Added Index0", {"ActivityTime", "ActivityDate"}, {"endTime","endDate"}),
filterShiftEnd = Table.SelectRows(expandSelf, each ([endTime] <> null)),
remIndexes = Table.RemoveColumns(filterShiftEnd,{"Index1", "Index0"}),
addListOfHours = Table.AddColumn(remIndexes, "hourSplit", each List.Transform({Time.Hour([ActivityTime])..Time.Hour([endTime])}, each Time.From(Text.Combine({Text.From(_),"00"},":")))),
expandListOfHours = Table.ExpandListColumn(addListOfHours, "hourSplit"),
addActivityDurMins = Table.AddColumn(expandListOfHours, "activityDurMins", each if Time.Hour([hourSplit]) <> Time.Hour([ActivityTime]) and Time.Hour([hourSplit]) <> Time.Hour([endTime]) then 60
else if [ActivityTime] >= [hourSplit] and Time.Hour([ActivityTime]) = Time.Hour([endTime]) then Duration.Minutes([endTime] -[ActivityTime])
else if [ActivityTime] >= [hourSplit] and Time.Hour([ActivityTime]) < Time.Hour([endTime]) then Duration.Minutes([hourSplit] - [ActivityTime]) + 60
else if [ActivityTime] <= [hourSplit] then Duration.Minutes([endTime] -[hourSplit])
else null),
filterZeroMins = Table.SelectRows(addActivityDurMins, each ([activityDurMins] <> 0)),
addTimeRange = Table.AddColumn(filterZeroMins, "timeRange", each let
timeFrom = Text.Combine({Text.From(Time.Hour([hourSplit])), "00"}, ":"),
timeTo = Text.Combine({Text.From(Time.Hour([hourSplit]) + 1), "00"}, ":")
in
Text.Combine({timeFrom, timeTo}, "-")),
remOthCols = Table.SelectColumns(addTimeRange,{"timeRange", "Activity", "activityDurMins"}),
pivotActivities = Table.Pivot(remOthCols, List.Distinct(remOthCols[Activity]), "Activity", "activityDurMins", List.Sum)
in
pivotActivities
ActivityActivityDateActivityTime
Job Setup | 21/Okt/21 | 11:27:01 |
Batch Record verfügbar (BR in Produktion) | 21/Okt/21 | 11:27:01 |
Arbeitsvorbereitung (Setup) | 22/Okt/21 | 15:13:09 |
Staff Change | 22/Okt/21 | 15:13:15 |
Arbeitsvorbereitung Pause | 22/Okt/21 | 18:14:44 |
Warten auf GrL | 25/Okt/21 | 06:22:09 |
Arbeitsvorbereitung (Setup) | 25/Okt/21 | 07:02:36 |
Warten auf QC/QA | 25/Okt/21 | 07:03:54 |
Warten auf QC/QA | 25/Okt/21 | 07:03:59 |
Auftragsfreigabe (Freigabe QC) | 25/Okt/21 | 07:18:54 |
Verpackung am Arbeiten | 25/Okt/21 | 08:37:16 |
Staff Change | 25/Okt/21 | 11:10:22 |
Staff Change | 25/Okt/21 | 11:10:26 |
Verpackung unterbrochen - Pause/Schichtende | 25/Okt/21 | 11:10:33 |
Verpackung am Arbeiten | 25/Okt/21 | 14:01:34 |
Verpackung unterbrochen - Pause/Schichtende | 25/Okt/21 | 14:01:41 |
Verpackung am Arbeiten | 25/Okt/21 | 16:13:19 |
Verpackung unterbrochen - Pause/Schichtende | 25/Okt/21 | 18:17:45 |
Verpackung am Arbeiten | 25/Okt/21 | 19:03:34 |
Staff Change | 26/Okt/21 | 06:21:49 |
Verpackung unterbrochen - Pause/Schichtende | 26/Okt/21 | 06:22:02 |
Verpackung am Arbeiten | 26/Okt/21 | 06:22:07 |
Verpackung unterbrochen - Pause/Schichtende | 26/Okt/21 | 11:09:19 |
Verpackung am Arbeiten | 26/Okt/21 | 11:58:55 |
Verpackung unterbrochen - Pause/Schichtende | 26/Okt/21 | 13:51:35 |
Verpackung am Arbeiten | 26/Okt/21 | 14:30:53 |
Warten auf Personal | 26/Okt/21 | 15:31:05 |
Verpackung am Arbeiten | 26/Okt/21 | 15:31:14 |
Verpackung unterbrochen - Pause/Schichtende | 26/Okt/21 | 18:21:49 |
Verpackung am Arbeiten | 26/Okt/21 | 19:04:16 |
Verpackung unterbrochen - Pause/Schichtende | 27/Okt/21 | 06:21:47 |
Verpackung am Arbeiten | 27/Okt/21 | 06:21:52 |
Staff Change | 27/Okt/21 | 06:22:08 |
Verpackung unterbrochen - Pause/Schichtende | 27/Okt/21 | 11:05:34 |
Verpackung am Arbeiten | 27/Okt/21 | 11:54:47 |
Verpackung unterbrochen - Pause/Schichtende | 27/Okt/21 | 13:49:52 |
Verpackung am Arbeiten | 27/Okt/21 | 14:24:37 |
Verpackung unterbrochen - Pause/Schichtende | 27/Okt/21 | 18:14:09 |
Staff Change | 28/Okt/21 | 06:22:14 |
Verpackung am Arbeiten | 28/Okt/21 | 06:22:18 |
Verpackung unterbrochen - Pause/Schichtende | 28/Okt/21 | 11:01:42 |
Verpackung am Arbeiten | 28/Okt/21 | 11:56:18 |
Verpackung unterbrochen - Pause/Schichtende | 28/Okt/21 | 13:46:58 |
Warten auf Personal | 28/Okt/21 | 14:30:45 |
Verpackung am Arbeiten | 28/Okt/21 | 14:39:02 |
Verpackung unterbrochen - Pause/Schichtende | 28/Okt/21 | 18:10:25 |
Verpackung am Arbeiten | 28/Okt/21 | 19:00:45 |
Warten auf Personal | 28/Okt/21 | 19:44:59 |
Verpackung am Arbeiten | 28/Okt/21 | 19:47:48 |
Staff Change | 29/Okt/21 | 06:22:50 |
Verpackung unterbrochen - Pause/Schichtende | 29/Okt/21 | 06:23:07 |
Verpackung am Arbeiten | 29/Okt/21 | 06:23:14 |
Verpackung unterbrochen - Pause/Schichtende | 29/Okt/21 | 11:03:18 |
Verpackung am Arbeiten | 29/Okt/21 | 11:53:31 |
Verpackung unterbrochen - Pause/Schichtende | 29/Okt/21 | 13:51:31 |
Verpackung am Arbeiten | 29/Okt/21 | 14:37:39 |
Verpackung unterbrochen - Pause/Schichtende | 29/Okt/21 | 18:11:33 |
Verpackung am Arbeiten | 29/Okt/21 | 18:57:14 |
Staff Change | 30/Okt/21 | 06:22:18 |
Verpackung unterbrochen - Pause/Schichtende | 30/Okt/21 | 09:59:24 |
Verpackung am Arbeiten | 30/Okt/21 | 10:45:31 |
Warten auf Technik | 30/Okt/21 | 11:10:13 |
Verpackung am Arbeiten | 30/Okt/21 | 11:15:38 |
Verpackung unterbrochen - Pause/Schichtende | 30/Okt/21 | 12:47:48 |
Staff Change | 01/Nov/21 | 06:21:59 |
Verpackung am Arbeiten | 01/Nov/21 | 06:22:04 |
Warten auf Druckerei | 01/Nov/21 | 10:30:24 |
Staff Change | 01/Nov/21 | 14:20:28 |
Verpackung am Arbeiten | 01/Nov/21 | 15:56:18 |
Auftragsabschluss (Bilanzierung) | 01/Nov/21 | 21:02:26 |
Kabine wird gereinigt | 01/Nov/21 | 21:02:47 |
Kabine frei | 01/Nov/21 | 21:51:01 |
Step Finished | 01/Nov/21 | 21:51:01 |
Hi @GotManyProblems ,
It's a bit of a beast, but the code below turns this:
Into this:
Create a new blank query in PQ then, in Advanced Editor, paste this over the default code. You will then be able to follow the steps I took:
let
Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WMjS1MjBQ0lEKSEzOTkzPzEtXitUBixqDRJ2KUhOzYSImlkCRkPzMnNQSiJCZlYEhplYzKxMjoGhwbn52KkTE3MrQHE2ruZWREaZWC4hbgjMy00oUXPNSlGJjAQ==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Timestamp = _t, Activity = _t]),
chgTypes = Table.TransformColumnTypes(Source,{{"Timestamp", type time}, {"Activity", type text}}),
sortChronological = Table.Sort(chgTypes,{{"Timestamp", Order.Ascending}}),
addIndex1 = Table.AddIndexColumn(sortChronological, "Index1", 1, 1, Int64.Type),
addIndex0 = Table.AddIndexColumn(addIndex1, "Index0", 0, 1, Int64.Type),
mergeSelfOnIndex = Table.NestedJoin(addIndex0, {"Index1"}, addIndex0, {"Index0"}, "Added Index0", JoinKind.LeftOuter),
fixMergeSorting = Table.Sort(mergeSelfOnIndex,{{"Index1", Order.Ascending}}),
expandSelf = Table.ExpandTableColumn(fixMergeSorting, "Added Index0", {"Timestamp"}, {"endTime"}),
filterShiftEnd = Table.SelectRows(expandSelf, each ([endTime] <> null)),
remIndexes = Table.RemoveColumns(filterShiftEnd,{"Index1", "Index0"}),
addListOfHours = Table.AddColumn(remIndexes, "hourSplit", each List.Transform({Time.Hour([Timestamp])..Time.Hour([endTime])}, each Time.From(Text.Combine({Text.From(_),"00"},":")))),
expandListOfHours = Table.ExpandListColumn(addListOfHours, "hourSplit"),
addActivityDurMins = Table.AddColumn(expandListOfHours, "activityDurMins", each if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) = Time.Hour([endTime]) then Duration.Minutes([endTime] -[Timestamp])
else if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) < Time.Hour([endTime]) then Duration.Minutes([hourSplit] - [Timestamp]) + 60
else if [Timestamp] <= [hourSplit] then Duration.Minutes([endTime] -[hourSplit])
else null),
filterZeroMins = Table.SelectRows(addActivityDurMins, each ([activityDurMins] <> 0)),
addTimeRange = Table.AddColumn(filterZeroMins, "timeRange", each let
timeFrom = Text.Combine({Text.From(Time.Hour([hourSplit])), "00"}, ":"),
timeTo = Text.Combine({Text.From(Time.Hour([hourSplit]) + 1), "00"}, ":")
in
Text.Combine({timeFrom, timeTo}, "-")),
remOthCols = Table.SelectColumns(addTimeRange,{"timeRange", "Activity", "activityDurMins"}),
pivotActivities = Table.Pivot(remOthCols, List.Distinct(remOthCols[Activity]), "Activity", "activityDurMins", List.Sum)
in
pivotActivities
Summary:
1) Sort the data chronologically on [Timestamp] and add offset indexes.
2) Merge table on itself to get activity start and end time inline.
3) Create list of hours covered by activity to duplicate rows for activities that span before end of hour/after start of hour.
4) Calculate individual activity times splitting by time hour.
5) Calculate the time range in which the activity occurred.
6) Pivot activities with activity duration as aggregate (sum) field.
Pete
Proud to be a Datanaut!