Exercise 19.5
Find the Real Underperformer - Difficulty-Normalised Drilling Cost
In Project 3, change well E's formation hardness to match well D's (make it a hard hole too) and re-rank. Does E stay the worst once it is no longer the easiest hole? Then add a seventh well of your own design that is fast but in soft rock: where does it rank raw vs. difficulty-normalised, and what does that teach about benchmarking new crews?
---
The fastest way to teach a drilling crew the wrong lesson is to rank them by raw cost-per-foot: the team that drilled the deepest, hardest hole looks worst, and the team that drilled a shallow, soft one slowly looks fine. Project 3's fix was to divide difficulty out first, then rank what is left: the part the crew actually controls.
The six-well SPEC and the DAYRATE are in the do-not-edit block. Each well is (id, TD, hardness, efficiency, npt). Write one function:
def benchmark(spec):
'''Cost-benchmark wells two ways and name the worst by each measure.
Returns (rows, raw_worst, norm_worst).'''Exact procedure (mirror the chapter):
- For each well,
rop = 110 / hard * eff;days = (td / rop / 24) / (1 - npt);
cost = days * DAYRATE.
rows[wid] = {'cpf': cost / td, 'cpf_norm': (cost / td) / hard, 'cost': cost}
cpf is raw cost-per-foot, cpf_norm divides hardness back out.
raw_worst= the well with the highestcpf;norm_worst= the well with the
highest cpf_norm (use max(rows, key=...)).
return rows, raw_worst, norm_worst.
Then call it once at module level into the names the tests read:
rows, raw_worst, norm_worst = benchmark(SPEC)> Think about it: raw cost-per-foot flags well B, but B only looks bad > because it drilled the second-deepest, second-hardest hole. Divide difficulty > out and the real laggard is well E: a shallow hole drilled at 70% > efficiency with 52% non-productive time. If you re-ran this after making E's > rock as hard as the deepest well's, would E still be the underperformer? And > what does that tell you about which number to put in the lessons-learned report?
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.
# ── Verified Chapter 19 drilling-benchmark spec + cost model (do not edit) ─
# Each well: (id, TD ft, formation hardness, drilling efficiency 0-1, non-productive-time fraction)
SPEC = [("A", 9000, 1.0, 0.92, 0.32), ("B", 11000, 1.6, 0.78, 0.44), ("C", 9500, 1.1, 0.95, 0.30),
("D", 12500, 1.9, 0.97, 0.28), ("E", 8800, 0.9, 0.70, 0.52), ("F", 10200, 1.4, 0.85, 0.38)]
DAYRATE = 280000.0 # $/day full rig spread
# ── end do-not-edit ──────────────────────────────────────────────────────
def benchmark(spec):
"""Cost-benchmark a list of wells two ways: raw cost-per-foot, and a
DIFFICULTY-NORMALISED cost-per-foot that divides formation hardness out.
Returns (rows, raw_worst, norm_worst):
rows[wid] = {'cpf': float, 'cpf_norm': float, 'cost': float}
raw_worst = well with the highest RAW cpf (penalises the hardest hole)
norm_worst = well with the highest NORMALISED cpf (the true underperformer)
"""
rows = {}
for wid, td, hard, eff, npt in spec:
rop = 110 / hard * eff # on-bottom ROP: rock AND practices
days = (td / rop / 24) / (1 - npt) # non-productive time inflates calendar days
cost = days * DAYRATE
rows[wid] = dict(cpf=cost / td, cpf_norm=(cost / td) / hard, cost=cost) # divide difficulty OUT
raw_worst = max(rows, key=lambda w: rows[w]["cpf"])
norm_worst = max(rows, key=lambda w: rows[w]["cpf_norm"])
return rows, raw_worst, norm_worst
rows, raw_worst, norm_worst = benchmark(SPEC)
for w in sorted(rows, key=lambda x: rows[x]["cpf_norm"]):
print(f" {w}: raw $/ft {rows[w]['cpf']:6.0f} normalised $/ft {rows[w]['cpf_norm']:6.1f}")
print(f"raw cost/ft would flag well {raw_worst} (just the hardest hole)")
print(f"difficulty-normalised, the real underperformer is well {norm_worst}")
lockCopying code is a Full Access feature.