Exerciseschevron_rightChapter 4chevron_right4.10
fitness_center

Exercise 4.10

Field Summary Dashboard

Level 3
Chapter 4: NumPy & Pandas
descriptionProblem

The capstone: the single-page field summary a production manager presents at the monthly review. The starter builds a 24-month, five-well history df (well, month, oil_bopd, water_bwpd, gas_mscfd).

Write two functions, then assemble the figure.

  1. latest_summary(df): one row per well for the latest month, with

columns: well, oil_bopd, water_bwpd, gas_mscfd, water_cut (water / (oil + water)), gor (gas × 1000 / oil), and mom_oil_change (latest month's oil minus the previous month's). Return a DataFrame.

  1. field_total(summary): a dict with the field-wide oil_bopd,

water_bwpd, and gas_mscfd (the column sums, as ints).

Then build a 2×3 matplotlib figure (plt.subplots(2, 3, ...)) that includes at least: a stacked bar of each well's oil contribution, a line of field water cut over the 24 months, and a scatter of oil rate vs. water cut for the latest month. Add a fig.suptitle. This is the deliverable that separates a data-literate engineer from one chained to a spreadsheet.

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 pandas as pd
import matplotlib.pyplot as plt

_specs = [
    ("OD-001", 2000, 0.015, 100, 12, 1000),
    ("OD-002", 1200, 0.020, 250, 25,  700),
    ("OD-005", 2600, 0.012,  90, 10, 1600),
    ("OD-007", 1500, 0.025, 180, 20,  850),
    ("OD-009", 1800, 0.018, 130, 15,  950),
]
_rows = []
for _name, _oil0, _doil, _w0, _dw, _gas0 in _specs:
    for i in range(24):
        _rows.append({
            "well": _name,
            "month": i + 1,
            "oil_bopd": round(_oil0 * (1 - _doil) ** i),
            "water_bwpd": _w0 + _dw * i,
            "gas_mscfd": round(_gas0 * (1 - 0.008) ** i),
        })
df = pd.DataFrame(_rows)


def latest_summary(df):
    last = df["month"].max()
    prev = last - 1
    rows = []
    for well, g in df.groupby("well"):
        cur = g[g["month"] == last].iloc[0]
        was = g[g["month"] == prev].iloc[0]
        rows.append({
            "well": well,
            "oil_bopd": int(cur["oil_bopd"]),
            "water_bwpd": int(cur["water_bwpd"]),
            "gas_mscfd": int(cur["gas_mscfd"]),
            "water_cut": cur["water_bwpd"] / (cur["oil_bopd"] + cur["water_bwpd"]),
            "gor": cur["gas_mscfd"] * 1000 / cur["oil_bopd"],
            "mom_oil_change": int(cur["oil_bopd"] - was["oil_bopd"]),
        })
    return pd.DataFrame(rows)


def field_total(summary):
    return {
        "oil_bopd": int(summary["oil_bopd"].sum()),
        "water_bwpd": int(summary["water_bwpd"].sum()),
        "gas_mscfd": int(summary["gas_mscfd"].sum()),
    }


summary = latest_summary(df)
totals = field_total(summary)

fig, axes = plt.subplots(2, 3, figsize=(16, 9))

# 1. Stacked bar - each well's oil contribution to the field total.
bottom = 0
for _, row in summary.iterrows():
    axes[0, 0].bar(["Field"], [row["oil_bopd"]], bottom=bottom, label=row["well"])
    bottom += row["oil_bopd"]
axes[0, 0].set_title("Oil contribution by well")
axes[0, 0].set_ylabel("Oil rate (bopd)")
axes[0, 0].legend(fontsize=8)

# 2. Field water-cut trend over 24 months.
monthly = df.groupby("month").agg(oil=("oil_bopd", "sum"), water=("water_bwpd", "sum"))
field_wc = monthly["water"] / (monthly["oil"] + monthly["water"])
axes[0, 1].plot(field_wc.index, field_wc.values, color="#CC4444", linewidth=2)
axes[0, 1].set_title("Field water cut trend")
axes[0, 1].set_xlabel("Month"); axes[0, 1].set_ylabel("Water cut")

# 3. Oil rate vs. water cut (latest month).
axes[0, 2].scatter(summary["oil_bopd"], summary["water_cut"], s=80, color="#4682B4")
for _, row in summary.iterrows():
    axes[0, 2].annotate(row["well"], (row["oil_bopd"], row["water_cut"]), fontsize=8)
axes[0, 2].set_title("Oil rate vs. water cut (current)")
axes[0, 2].set_xlabel("Oil rate (bopd)"); axes[0, 2].set_ylabel("Water cut")

# 4. Oil rate per well (latest month).
axes[1, 0].bar(summary["well"], summary["oil_bopd"], color="#2E8B57")
axes[1, 0].set_title("Current oil rate by well"); axes[1, 0].set_ylabel("bopd")

# 5. GOR per well.
axes[1, 1].bar(summary["well"], summary["gor"], color="#DAA520")
axes[1, 1].set_title("Current GOR by well"); axes[1, 1].set_ylabel("scf/bbl")

# 6. Text call-outs.
top = summary.loc[summary["oil_bopd"].idxmax()]
worst = summary.loc[summary["water_cut"].idxmax()]
axes[1, 2].axis("off")
axes[1, 2].text(0.02, 0.85, f"Field oil:   {totals['oil_bopd']:,} bopd", fontsize=11)
axes[1, 2].text(0.02, 0.65, f"Top performer: {top['well']} ({top['oil_bopd']:,} bopd)", fontsize=11)
axes[1, 2].text(0.02, 0.45, f"Watch: {worst['well']} (WC {worst['water_cut']:.0%})", fontsize=11)

fig.suptitle("OML 58 - monthly field summary", fontweight="bold", fontsize=14)
plt.tight_layout()
plt.show()

lockCopying code is a Full Access feature.