1661. Average Time of Process per Machine
On LeetCode ->Problem¶
Given an Activity table with one start and one end row per (machine_id, process_id), return each machine's average process duration rounded to 3 decimals.
Input:
# Activity
-----------+------------+---------------+----------
machine_id | process_id | activity_type | timestamp
-----------+------------+---------------+----------
0 | 0 | 'start' | 0.712
0 | 0 | 'end' | 1.520
0 | 1 | 'start' | 3.140
0 | 1 | 'end' | 4.120
1 | 0 | 'start' | 0.550
1 | 0 | 'end' | 1.550
Output:
-----------+----------------
machine_id | processing_time
-----------+----------------
0 | 0.894
1 | 1.000
Key trick¶
Convert each process into one duration row by summing signed timestamps:
endcontributes+timestampstartcontributes-timestamp
So each (machine_id, process_id) duration is \(\text{duration} = \text{end} - \text{start}\).
Trap¶
- Joining only on
process_idand forgettingmachine_id. - Averaging raw timestamps instead of durations.
- Dividing by row count instead of process count.
- In PostgreSQL,
ROUND(double precision, integer)needs a cast tonumeric.
Why is it interesting?¶
It tests whether you can transform paired event rows into per-entity metrics, a common pattern in logs, sessions, jobs, and lifecycle tables.
SQL solution¶
SQLite¶
WITH process_duration AS (
SELECT
machine_id,
process_id,
-- Each process has exactly one start and one end.
-- start is negative, end is positive, so SUM gives end - start.
SUM(
CASE
WHEN activity_type = 'end' THEN timestamp
ELSE -timestamp
END
) AS duration
FROM Activity
GROUP BY machine_id, process_id
)
SELECT
machine_id,
ROUND(AVG(duration), 3) AS processing_time
FROM process_duration
GROUP BY machine_id;
PostgreSQL¶
WITH process_duration AS (
SELECT
machine_id,
process_id,
-- Same logic as SQLite.
SUM(
CASE
WHEN activity_type = 'end' THEN timestamp
ELSE -timestamp
END
) AS duration
FROM Activity
GROUP BY machine_id, process_id
)
SELECT
machine_id,
-- PostgreSQL needs numeric for ROUND(value, decimals).
ROUND(AVG(duration)::numeric, 3) AS processing_time
FROM process_duration
GROUP BY machine_id;
Pandas solution¶
import pandas as pd
def get_average_time(activity: pd.DataFrame) -> pd.DataFrame:
df = activity.copy()
# Use signed timestamps so each grouped process sums to end - start.
df["signed_timestamp"] = df["timestamp"].where(
df["activity_type"].eq("end"),
-df["timestamp"],
)
process_duration = (
df.groupby(["machine_id", "process_id"], as_index=False)
.agg(duration=("signed_timestamp", "sum"))
)
result = (
process_duration.groupby("machine_id", as_index=False)
.agg(processing_time=("duration", "mean"))
)
result["processing_time"] = result["processing_time"].round(3)
return result[["machine_id", "processing_time"]]
Pytest test¶
import sqlite3
import pandas as pd
import pytest
from pandas.testing import assert_frame_equal
SQLITE_QUERY = """
WITH process_duration AS (
SELECT
machine_id,
process_id,
SUM(
CASE
WHEN activity_type = 'end' THEN timestamp
ELSE -timestamp
END
) AS duration
FROM Activity
GROUP BY machine_id, process_id
)
SELECT
machine_id,
ROUND(AVG(duration), 3) AS processing_time
FROM process_duration
GROUP BY machine_id
ORDER BY machine_id;
"""
def get_average_time(activity: pd.DataFrame) -> pd.DataFrame:
df = activity.copy()
df["signed_timestamp"] = df["timestamp"].where(
df["activity_type"].eq("end"),
-df["timestamp"],
)
process_duration = (
df.groupby(["machine_id", "process_id"], as_index=False)
.agg(duration=("signed_timestamp", "sum"))
)
result = (
process_duration.groupby("machine_id", as_index=False)
.agg(processing_time=("duration", "mean"))
)
result["processing_time"] = result["processing_time"].round(3)
return result[["machine_id", "processing_time"]].sort_values("machine_id").reset_index(drop=True)
def run_sqlite(activity: pd.DataFrame) -> pd.DataFrame:
with sqlite3.connect(":memory:") as conn:
activity.to_sql("Activity", conn, index=False)
return pd.read_sql_query(SQLITE_QUERY, conn)
@pytest.mark.parametrize(
"rows, expected_rows",
[
(
[
[0, 0, "start", 0.712],
[0, 0, "end", 1.520],
[0, 1, "start", 3.140],
[0, 1, "end", 4.120],
[1, 0, "start", 0.550],
[1, 0, "end", 1.550],
[1, 1, "start", 0.430],
[1, 1, "end", 1.420],
[2, 0, "start", 4.100],
[2, 0, "end", 4.512],
[2, 1, "start", 2.500],
[2, 1, "end", 5.000],
],
[
[0, 0.894],
[1, 0.995],
[2, 1.456],
],
),
(
[
[7, 0, "start", 10.0],
[7, 0, "end", 13.0],
],
[
[7, 3.000],
],
),
(
[
[1, 10, "start", 0.0],
[1, 10, "end", 2.0],
[1, 11, "start", 5.0],
[1, 11, "end", 8.0],
[2, 10, "start", 1.0],
[2, 10, "end", 5.0],
[2, 11, "start", 10.0],
[2, 11, "end", 12.0],
],
[
[1, 2.500],
[2, 3.000],
],
),
],
)
def test_average_time_sqlite_and_pandas(rows, expected_rows):
activity = pd.DataFrame(
rows,
columns=["machine_id", "process_id", "activity_type", "timestamp"],
)
expected = pd.DataFrame(
expected_rows,
columns=["machine_id", "processing_time"],
)
sql_result = run_sqlite(activity)
pandas_result = get_average_time(activity)
assert_frame_equal(sql_result, expected, check_dtype=False, atol=1e-9)
assert_frame_equal(pandas_result, expected, check_dtype=False, atol=1e-9)
Comment on my solution¶
- Your SQL solution is correct and readable.
- It is PostgreSQL-specific because of
::numeric; SQLite would useROUND(AVG(duration), 3). - The self-join approach is a good interview answer, though conditional aggregation is shorter.
- Your Pandas solution is correct and clear.
- The merge keys are correct because you join on both
machine_idandprocess_id.
-- WORKS
WITH StartActivity AS (
SELECT
machine_id,
process_id,
timestamp AS started_at
FROM Activity
WHERE activity_type = 'start'
),
EndActivity AS (
SELECT
machine_id,
process_id,
timestamp AS ended_at
FROM Activity
WHERE activity_type = 'end'
),
StartEndActivity AS (
SELECT
s.machine_id,
s.process_id,
e.ended_at - s.started_at AS duration
FROM StartActivity AS s
JOIN EndActivity AS e
ON e.machine_id = s.machine_id
AND e.process_id = s.process_id
)
SELECT
machine_id,
ROUND(AVG(duration)::numeric, 3) AS processing_time
FROM StartEndActivity
GROUP BY machine_id;
import pandas as pd
def get_average_time(activity: pd.DataFrame) -> pd.DataFrame:
start_activity = activity.loc[activity["activity_type"] == "start"]
end_activity = activity.loc[activity["activity_type"] == "end"]
result = start_activity.merge(
end_activity,
on=["machine_id", "process_id"],
how="inner",
suffixes=("", "_end")
)
result["duration"] = result["timestamp_end"] - result["timestamp"]
result = (
result.groupby("machine_id", as_index=False)
.agg(processing_time=("duration", "mean"))
)
result["processing_time"] = result["processing_time"].round(3)
return result[["machine_id", "processing_time"]]