Exercise 4.10
Field Summary Dashboard
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.
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.
field_total(summary): a dict with the field-wideoil_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.
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 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.