Exercise 6.9
Multi-Well Production Analysis
Once the data is clean, the first real question is: how is the field doing? You roll the wells up to a field profile, find the peak, total the production, and put a number on the decline.
The starter builds a year of monthly oil for three OML 58 wells (df with well_id, date, oil_bopd, days_on). Write five functions and a plot:
well_date_ranges(df): dictwell → (first_date, last_date).field_monthly_oil(df): total field oil rate per month (a Series
indexed by date).
peak_month(df): the month with the highest field rate.field_cum_oil_bbl(df): total field oil volume = Σoil_bopd × days_on.avg_decline_rate(df): the secant exponential decline from peak to the
final month: ln(q_peak / q_final) / months_elapsed (per month).
Then plot field monthly oil rate over time. The secant decline is exactly the back-of-envelope number an engineer quotes in a review ("the field's declining about 5% a month").
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
import matplotlib.pyplot as plt
_specs = [("OD-001", 2400, 0.05), ("OD-002", 1800, 0.07), ("OD-003", 3100, 0.04)]
_dates = pd.date_range("2023-01-01", periods=12, freq="MS")
_rows = []
for _name, _qi, _di in _specs:
for _i, _d in enumerate(_dates):
_rows.append({"well_id": _name, "date": _d,
"oil_bopd": round(_qi * np.exp(-_di * _i)), "days_on": 30})
df = pd.DataFrame(_rows)
def well_date_ranges(df):
return {well: (g["date"].min(), g["date"].max()) for well, g in df.groupby("well_id")}
def field_monthly_oil(df):
return df.groupby("date")["oil_bopd"].sum().sort_index()
def peak_month(df):
return field_monthly_oil(df).idxmax()
def field_cum_oil_bbl(df):
return float((df["oil_bopd"] * df["days_on"]).sum())
def avg_decline_rate(df):
fm = field_monthly_oil(df)
pm = fm.idxmax()
q_peak = float(fm.loc[pm])
q_final = float(fm.iloc[-1])
months = (fm.index[-1].year - pm.year) * 12 + (fm.index[-1].month - pm.month)
if months <= 0 or q_final <= 0:
return 0.0
return (np.log(q_peak) - np.log(q_final)) / months
fm = field_monthly_oil(df)
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(fm.index, fm.values, marker="o", color="#2E8B57", linewidth=2)
ax.set_xlabel("Month")
ax.set_ylabel("Field oil rate (bopd)")
ax.set_title("OML 58 - field oil rate")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"Peak {peak_month(df).strftime('%Y-%m')}, "
f"cum {field_cum_oil_bbl(df):,.0f} bbl, "
f"decline {avg_decline_rate(df)*100:.1f}%/month")
lockCopying code is a Full Access feature.