Exerciseschevron_rightChapter 7chevron_right7.5
fitness_center

Exercise 7.5

Net Pay Cutoff Sensitivity

Level 2
Chapter 7: Well Log Analysis
descriptionProblem

Cutoffs are where petrophysics meets the bank. On OD-003 (OML 58) the full evaluation is already done for you: every sample in logs now carries VSHALE, PHIE, and SW. What's left is the decision: which samples count as pay, and what that decision is worth.

A sample is net pay when it clears all three cutoffs at once:

> VSHALE < vsh_max and PHIE > phie_min and SW < sw_max

Each qualifying sample is step_ft thick (here 0.5 ft), so net pay in feet is just count(passing) × step_ft.

Write two functions:

  1. net_pay_ft(logs, vsh_max, phie_min, sw_max, step_ft=STEP_FT): return the

net pay (feet) for one set of cutoffs.

  1. pay_value_usd(net_pay_ft, per_ft=2_000_000): at $2 million per foot

of recoverable reserves, return the dollar value of that pay.

Then build a summary dict mapping each cutoff name to its net pay, running all three sets:

Setvsh_maxphie_minsw_max
Tight0.300.120.50
Base0.400.080.60
Relaxed0.500.050.70
summary = {
    "Tight":   net_pay_ft(logs, 0.30, 0.12, 0.50),
    "Base":    net_pay_ft(logs, 0.40, 0.08, 0.60),
    "Relaxed": net_pay_ft(logs, 0.50, 0.05, 0.70),
}

Looser cutoffs can only add footage, never remove it, so the numbers must climb Tight ≤ Base ≤ Relaxed. The gap between them, multiplied out, is the reserves you're betting on a cutoff call.

lightbulbHints (0/3)

Stuck? Reveal hints one at a time — they progress from nudge to near-solution.

codeYour solution
main.py
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

np.random.seed(7)
depth = np.arange(7000.0, 7850.0, 0.5)
_cond = [depth < 7200, depth < 7400, depth < 7600, depth < 7700, depth >= 7700]
_gr   = np.select(_cond, [105.0, 40.0, 32.0, 110.0, 45.0])
_rt   = np.select(_cond, [4.0,   2.2,  35.0, 4.0,   2.5])
_rhob = np.select(_cond, [2.54,  2.31, 2.26, 2.55,  2.33])
_nphi = np.select(_cond, [0.33,  0.21, 0.17, 0.34,  0.22])
logs = pd.DataFrame({
    "DEPTH": depth,
    "GR":   _gr   + np.random.normal(0, 4.0,  len(depth)),
    "RT":   _rt   * np.exp(np.random.normal(0, 0.10, len(depth))),
    "RHOB": _rhob + np.random.normal(0, 0.02, len(depth)),
    "NPHI": np.clip(_nphi + np.random.normal(0, 0.015, len(depth)), 0.0, 1.0),
})
STEP_FT = 0.5

# --- Petrophysical evaluation (done for you on OD-003) -----------------------
# Adds VSHALE, PHIE, SW columns to `logs`. You only decide the cutoffs.
A, M, N, RW = 0.81, 2.0, 2.0, 0.04
RHO_MA, RHO_FL = 2.65, 1.0

_gr_clean = logs["GR"].quantile(0.05)
_gr_shale = logs["GR"].quantile(0.95)
_igr = (logs["GR"] - _gr_clean) / (_gr_shale - _gr_clean)
logs["VSHALE"] = _igr.clip(0.0, 1.0)

_phid = ((RHO_MA - logs["RHOB"]) / (RHO_MA - RHO_FL)).clip(0.0, 0.45)
_phia = (_phid + logs["NPHI"]) / 2.0
logs["PHIE"] = (_phia * (1.0 - logs["VSHALE"])).clip(0.0, 0.40)

_sw = (((A * RW) / (logs["PHIE"] ** M * logs["RT"])) ** (1.0 / N)).clip(0.0, 1.0)
_sw[logs["PHIE"] <= 0.01] = 1.0
logs["SW"] = _sw
# -----------------------------------------------------------------------------


def net_pay_ft(logs, vsh_max, phie_min, sw_max, step_ft=STEP_FT):
    """Feet of net pay: samples passing VSHALE<vsh_max AND PHIE>phie_min AND SW<sw_max."""
    pay = (logs["VSHALE"] < vsh_max) & (logs["PHIE"] > phie_min) & (logs["SW"] < sw_max)
    return int(pay.sum()) * step_ft


def pay_value_usd(net_pay_ft, per_ft=2_000_000):
    """Dollar value of net pay at `per_ft` USD per foot of recoverable reserves."""
    return net_pay_ft * per_ft


summary = {
    "Tight":   net_pay_ft(logs, 0.30, 0.12, 0.50),
    "Base":    net_pay_ft(logs, 0.40, 0.08, 0.60),
    "Relaxed": net_pay_ft(logs, 0.50, 0.05, 0.70),
}

print(summary)
print(f"Base net pay value: ${pay_value_usd(summary['Base']):,.0f}")
print(f"Tight -> Relaxed swing: ${pay_value_usd(summary['Relaxed'] - summary['Tight']):,.0f}")

lockCopying code is a Full Access feature.