Exercise 14.10
Drilling Performance Dashboard - One-Page Well Summary
Build a comprehensive drilling dashboard for a single well that displays: time-depth curve vs. plan and offset, ROP vs. depth with formation tops, MSE and efficiency vs. depth, stick-slip severity vs. depth, and a cost summary table. Format it as a one-page report.
---
This is the capstone for the drilling-analytics chapter. The exercise text asks for a full one-page report with plots; here we build the numeric engine behind that report: a single aggregator that reduces a whole drilling dataset to the scalar KPIs a drilling supervisor reads at the top of the morning report. (Plots are not gradable; the summary numbers are.)
The verified compute_mse and detect_stick_slip functions from the chapter are embedded for you, along with the exact OML drilling-data generator (np.random.seed(42), 5000 rows) and these constants:
BIT_DIAMETER = 8.5(inches)TIME_INTERVAL_HR = 2 / 60(2-minute samples, in hours)SPREAD_RATE_PER_HR = 280000 / 24(a /hr)ccs_estimate(depth) = 5000 + 1.5 * depth(confined compressive strength, psi)
Do not modify the embedded functions, generator, or constants. Use them as given.
Your task is to write the aggregator:
def build_dashboard(df, db=BIT_DIAMETER, interval_hr=TIME_INTERVAL_HR,
spread_rate=SPREAD_RATE_PER_HR):
...It returns a single dict of scalar summary metrics with exactly these 7 keys (all plain Python floats):
| key | meaning |
|---|---|
total_footage | Depth (ft) max minus min (total interval drilled) |
total_hours | len(df) * interval_hr (hours on bottom) |
avg_rop | mean of ROP (ft/hr) |
avg_mse | mean MSE via compute_mse (pass WOB (klbf) * 1000 as lbf) |
avg_efficiency_pct | mean of clip(ccs_estimate(depth) / mse * 100, 0, 100) |
stick_slip_pct | 100 * detect_stick_slip(Torque, RPM).mean() |
total_cost_usd | spread_rate * total_hours |
Then run it on the embedded seed-42 drilling_data and expose these output variables for the grader:
dashboard: the full dict returned bybuild_dashboard(drilling_data)total_footage,avg_rop,avg_mse,stick_slip_pct,total_cost_usd:
the individual floats pulled out of dashboard
> Think about it: total_cost_usd must equal spread_rate total_hours > exactly, and total_hours must equal len(df) interval_hr exactly; these > are definitions, not approximations. Why does avg_efficiency_pct have to sit > inside [0, 100] no matter how rough the data is, and what does an avg_mse > many times larger than the rock's ccs_estimate tell you about how the well > was drilled?
Stuck? Reveal hints one at a time — they progress from nudge to near-solution.
visibilityReveal reference solutionexpand_more
Try solving it yourself first — the hints walk you through it. The solution below is one valid approach; yours may differ and still be correct.
import numpy as np
import pandas as pd
# ── Verified compute_mse (chapter 14, do not edit) ───────────────────────
def compute_mse(torque_ftlbs, rpm, wob_lbf, rop_fthr, bit_diameter_in):
"""
Calculate Mechanical Specific Energy.
Parameters
----------
torque_ftlbs : array-like
Torque (ft-lbs).
rpm : array-like
Rotary speed (rev/min).
wob_lbf : array-like
Weight on bit (lbf). Note: input in lbf, not klbf.
rop_fthr : array-like
Rate of penetration (ft/hr).
bit_diameter_in : float
Bit diameter (inches).
Returns
-------
numpy.ndarray
MSE values (psi).
"""
torque = np.asarray(torque_ftlbs, dtype=float)
r = np.asarray(rpm, dtype=float)
w = np.asarray(wob_lbf, dtype=float)
rop = np.asarray(rop_fthr, dtype=float)
db = bit_diameter_in
# Avoid division by zero
rop_safe = np.where(rop > 0, rop, 1e-6)
rotary_term = 480 * torque * r / (db**2 * rop_safe)
wob_term = 4 * w / (np.pi * db**2)
return rotary_term + wob_term
# ── Verified detect_stick_slip (chapter 14, do not edit) ─────────────────
def detect_stick_slip(torque, rpm, window=50, torque_cv_threshold=0.3):
"""
Detect stick-slip vibration from surface measurements.
Stick-slip is indicated by high coefficient of variation
in torque at relatively stable surface RPM.
Parameters
----------
torque : array-like
Torque measurements (ft-lbs).
rpm : array-like
Surface RPM measurements.
window : int
Rolling window size.
torque_cv_threshold : float
Coefficient of variation threshold for flagging.
Returns
-------
pandas.Series
Boolean mask where True indicates stick-slip.
"""
tq = pd.Series(torque)
r = pd.Series(rpm)
tq_mean = tq.rolling(window).mean()
tq_std = tq.rolling(window).std()
rpm_std = r.rolling(window).std()
# Coefficient of variation in torque
tq_cv = tq_std / tq_mean.replace(0, np.nan)
# RPM should be relatively stable (driller holding steady)
rpm_stable = rpm_std < 15
return (tq_cv > torque_cv_threshold) & rpm_stable
# ── Verified OML drilling-data generator (chapter 14, do not edit) ────────
np.random.seed(42)
n_points = 5000
depth_start = 500
rop_base = np.piecewise(
np.linspace(0, 1, n_points),
[np.linspace(0, 1, n_points) < 0.3,
(np.linspace(0, 1, n_points) >= 0.3) & (np.linspace(0, 1, n_points) < 0.6),
np.linspace(0, 1, n_points) >= 0.6],
[lambda x: 120 - 80*x, # soft formation, fast drilling
lambda x: 45 + 10*np.sin(20*x), # medium formation, variable
lambda x: 25 - 10*(x-0.6)] # hard formation, slow
)
rop = rop_base + np.random.normal(0, 5, n_points)
rop = np.clip(rop, 5, 200)
time_interval_hr = 2 / 60 # 2 minutes in hours
depth = depth_start + np.cumsum(rop * time_interval_hr)
wob = np.where(depth < 4000, 15 + np.random.normal(0, 2, n_points),
np.where(depth < 7000, 25 + np.random.normal(0, 3, n_points),
35 + np.random.normal(0, 3, n_points)))
wob = np.clip(wob, 5, 50)
rpm = np.where(depth < 4000, 140 + np.random.normal(0, 10, n_points),
np.where(depth < 7000, 120 + np.random.normal(0, 8, n_points),
100 + np.random.normal(0, 8, n_points)))
rpm = np.clip(rpm, 40, 180)
bit_diameter_in = 8.5
torque = 0.125 * (wob * 1000.0) * (bit_diameter_in / 12.0) + np.random.normal(0, 250, n_points)
torque = np.clip(torque, 800, 30000)
spp = 2000 + 0.15 * depth + np.random.normal(0, 100, n_points)
stick_slip_mask = (depth > 6000) & (depth < 6500)
torque[stick_slip_mask] = (
torque[stick_slip_mask] + 3500 * np.sin(np.linspace(0, 60, stick_slip_mask.sum()))
)
drilling_data = pd.DataFrame({
"Depth (ft)": np.round(depth, 1),
"ROP (ft/hr)": np.round(rop, 1),
"WOB (klbf)": np.round(wob, 1),
"RPM": np.round(rpm, 0).astype(int),
"Torque (ft-lbs)": np.round(torque, 0).astype(int),
"SPP (psi)": np.round(spp, 0).astype(int),
})
# ── Dashboard constants (do not edit) ────────────────────────────────────
BIT_DIAMETER = 8.5 # bit diameter, inches
TIME_INTERVAL_HR = 2 / 60 # 2-minute sample interval, hours
SPREAD_RATE_PER_HR = 280000 / 24 # $280k/day rig spread, $/hr
def ccs_estimate(depth):
"""Confined compressive strength of the rock (psi), rising with depth."""
return 5000 + 1.5 * depth
def build_dashboard(df, db=BIT_DIAMETER, interval_hr=TIME_INTERVAL_HR,
spread_rate=SPREAD_RATE_PER_HR):
"""Reduce a drilling dataset to a dict of scalar summary KPIs.
Returns a dict with exactly these 7 float keys:
total_footage = Depth max - Depth min (ft)
total_hours = len(df) * interval_hr (hr)
avg_rop = mean ROP (ft/hr)
avg_mse = mean MSE (psi), via compute_mse with WOB in lbf
avg_efficiency_pct = mean of clip(ccs/mse*100, 0, 100) (%)
stick_slip_pct = 100 * detect_stick_slip(Torque, RPM).mean() (%)
total_cost_usd = spread_rate * total_hours ($)
"""
depth = df["Depth (ft)"]
rop = df["ROP (ft/hr)"]
wob = df["WOB (klbf)"]
rpm = df["RPM"]
torque = df["Torque (ft-lbs)"]
n = len(df)
total_footage = float(depth.max() - depth.min())
total_hours = float(n * interval_hr)
avg_rop = float(rop.mean())
mse = compute_mse(torque, rpm, wob * 1000.0, rop, db)
avg_mse = float(np.mean(mse))
ccs = ccs_estimate(depth)
efficiency = np.clip(ccs / mse * 100.0, 0, 100)
avg_efficiency_pct = float(np.mean(efficiency))
stick_slip_pct = float(100.0 * detect_stick_slip(torque, rpm).mean())
total_cost_usd = float(spread_rate * total_hours)
return {
"total_footage": total_footage,
"total_hours": total_hours,
"avg_rop": avg_rop,
"avg_mse": avg_mse,
"avg_efficiency_pct": avg_efficiency_pct,
"stick_slip_pct": stick_slip_pct,
"total_cost_usd": total_cost_usd,
}
dashboard = build_dashboard(drilling_data)
total_footage = dashboard["total_footage"]
avg_rop = dashboard["avg_rop"]
avg_mse = dashboard["avg_mse"]
stick_slip_pct = dashboard["stick_slip_pct"]
total_cost_usd = dashboard["total_cost_usd"]
print("DRILLING PERFORMANCE DASHBOARD")
print("=" * 45)
print(f" Total footage: {dashboard['total_footage']:,.1f} ft")
print(f" Total hours: {dashboard['total_hours']:,.1f} hr")
print(f" Avg ROP: {dashboard['avg_rop']:.2f} ft/hr")
print(f" Avg MSE: {dashboard['avg_mse']:,.0f} psi")
print(f" Avg efficiency: {dashboard['avg_efficiency_pct']:.1f}%")
print(f" Stick-slip: {dashboard['stick_slip_pct']:.1f}% of time")
print(f" Total cost: ${dashboard['total_cost_usd']/1e6:.2f}M")
lockCopying code is a Full Access feature.