Exerciseschevron_rightChapter 6chevron_right6.9
fitness_center

Exercise 6.9

Multi-Well Production Analysis

Level 3
Chapter 6: Petroleum Data Sources
descriptionProblem

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:

  1. well_date_ranges(df): dict well → (first_date, last_date).
  2. field_monthly_oil(df): total field oil rate per month (a Series

indexed by date).

  1. peak_month(df): the month with the highest field rate.
  2. field_cum_oil_bbl(df): total field oil volume = Σ oil_bopd × days_on.
  3. 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").

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
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.