Exercise 14.4
Stick-Slip Severity Index - Torque CV & ROP Loss
Extend the stick-slip detection function to return a severity index (0 to 1) based on the ratio of torque standard deviation to mean torque. Plot severity vs. depth and correlate with ROP. By how much does severe stick-slip reduce drilling rate?
---
The chapter's detect_stick_slip(...) only gives you a yes/no flag. A driller wants to know how bad the dysfunction is and what it costs in penetration rate. You will turn the boolean detector into a continuous severity index and then quantify the ROP loss it causes on an OML well section.
The verified detect_stick_slip(torque, rpm, window=50, torque_cv_threshold=0.3) is embedded for you (read it; you are extending the same idea) and a self-contained well DataFrame is built for you with np.random.seed(99): 600 rows over Depth 8000 → 9200 ft, baseline Torque ~6000 ft-lbs, RPM ~120, ROP ~40 ft/hr, with a clear injected stick-slip zone from 8400 to 8800 ft where the torque swings violently (6000 + 4000*sin(...)) and the ROP is depressed by 18 ft/hr.
Your tasks:
- Write
stick_slip_severity(torque, window=30)returning apandas.Series
the same length as torque. For each rolling window it is the torque coefficient of variation: rolling_std / rolling_mean (use pandas' default ddof=1 std, and .replace(0, np.nan) on the mean to avoid divide-by-zero), then clipped to [0, 1] so it reads as a 0-to-1 severity index. Leading rows are NaN (the window has not filled yet). That is expected.
- Write
rop_reduction(df, severity_threshold=0.2, window=30)that splits the
rows by severity and returns a dict:
severe= rows whereseverity > severity_thresholdmild= rows whereseverity <= severity_threshold'rop_severe'= meandf["ROP"]over the severe rows'rop_mild'= meandf["ROP"]over the mild rows'reduction_pct'=100 * (1 - rop_severe / rop_mild)
- Assign the output variables exactly:
severity = stick_slip_severity(well["Torque"])max_severity = float(severity.max())reduction = rop_reduction(well)reduction_pct = float(reduction["reduction_pct"])
> Think about it: severity is a ratio (std ÷ mean), so it is dimensionless > and scale-invariant: doubling every torque reading leaves it unchanged. That is > exactly why a CV beats raw standard deviation for comparing wells with different > baseline torque. By how much does the severe zone drop the ROP versus the calm > rock around it?
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 stick-slip detector from the chapter (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
# ── Self-contained OML well section (do not edit) ────────────────────────
def build_well():
"""600-row well with a clear injected stick-slip zone (8400-8800 ft)."""
np.random.seed(99)
n = 600
depth = np.linspace(8000, 9200, n)
torque = 6000 + np.random.normal(0, 150, n)
rop = 40 + np.random.normal(0, 3, n)
rpm = 120 + np.random.normal(0, 5, n)
ss = (depth >= 8400) & (depth <= 8800) # the stick-slip zone
torque[ss] = 6000 + 4000 * np.sin(np.linspace(0, 80, ss.sum()))
rop[ss] = rop[ss] - 18 # ROP depressed by dysfunction
return pd.DataFrame({"Depth": depth, "Torque": torque, "ROP": rop, "RPM": rpm})
well = build_well()
def stick_slip_severity(torque, window=30):
"""Rolling torque coefficient of variation, clipped to [0, 1].
severity = rolling_std(ddof=1) / rolling_mean (mean 0 -> NaN), clip [0, 1]
Returns a pandas.Series the same length as torque (leading rows are NaN).
"""
tq = pd.Series(torque)
cv = tq.rolling(window).std() / tq.rolling(window).mean().replace(0, np.nan)
return cv.clip(0, 1)
def rop_reduction(df, severity_threshold=0.2, window=30):
"""Split rows by severity and quantify the ROP penalty.
Returns {'rop_severe', 'rop_mild', 'reduction_pct'} where
severe = severity > severity_threshold
mild = severity <= severity_threshold
reduction_pct = 100 * (1 - rop_severe / rop_mild)
"""
sev = stick_slip_severity(df["Torque"], window=window)
severe = sev > severity_threshold
mild = sev <= severity_threshold
rop_severe = df["ROP"][severe].mean()
rop_mild = df["ROP"][mild].mean()
reduction_pct = 100 * (1 - rop_severe / rop_mild)
return {"rop_severe": rop_severe, "rop_mild": rop_mild,
"reduction_pct": reduction_pct}
severity = stick_slip_severity(well["Torque"])
max_severity = float(severity.max())
reduction = rop_reduction(well)
reduction_pct = float(reduction["reduction_pct"])
print("max severity:", round(max_severity, 4))
print("ROP severe (ft/hr):", round(reduction["rop_severe"], 2))
print("ROP mild (ft/hr):", round(reduction["rop_mild"], 2))
print("ROP loss in severe stick-slip (%):", round(reduction_pct, 2))
lockCopying code is a Full Access feature.