production decline curve with exponential fit
Petroleum Engineering
Example from the compendium of canonical charts
Python Code
"""Petroleum Engineering — production decline curve with exponential fit."""
import numpy as np
import pandas as pd
import plotly.graph_objects as go
URL = "https://raw.githubusercontent.com/Jeffalltogether/well_decline_curve_analysis/master/data/Production_Time_Series.CSV"
def fit_exponential(t, q):
"""Fit q = qi * exp(-Di * t) via log-linear regression."""
mask = q > 0
if mask.sum() < 3:
return None, None
log_q = np.log(q[mask])
t_fit = t[mask]
coeffs = np.polyfit(t_fit, log_q, 1)
Di = -coeffs[0]
qi = np.exp(coeffs[1])
return qi, Di
def generate():
try:
print("fetching production time series …")
df = fetch_csv(URL)
# Normalize column names
df.columns = [c.strip() for c in df.columns]
# Find the liquid/oil column
liq_col = None
for c in df.columns:
if "liquid" in c.lower() or "oil" in c.lower() or "bbl" in c.lower():
liq_col = c
break
if liq_col is None:
raise ValueError(f"No liquid column found. Columns: {list(df.columns)}")
# Find date and well ID columns
date_col = None
for c in df.columns:
if "date" in c.lower():
date_col = c
break
well_col = None
for c in df.columns:
if "api" in c.lower() or "uwi" in c.lower() or "well" in c.lower():
well_col = c
break
df[date_col] = pd.to_datetime(df[date_col])
df[liq_col] = pd.to_numeric(df[liq_col], errors="coerce")
# Pick the most productive well
if well_col:
best_well = df.groupby(well_col)[liq_col].sum().idxmax()
wdf = df[df[well_col] == best_well].copy()
else:
wdf = df.copy()
wdf = wdf.dropna(subset=[liq_col]).sort_values(date_col)
wdf = wdf[wdf[liq_col] > 0]
# Skip ramp-up: start from peak month
peak_idx = wdf[liq_col].idxmax()
wdf = wdf.loc[peak_idx:].reset_index(drop=True)
t = np.arange(len(wdf), dtype=float)
q = wdf[liq_col].values.astype(float)
qi, Di = fit_exponential(t, q)
except Exception as e:
print(f" data fetch failed ({e}); using synthetic fallback")
# Synthetic Arps exponential decline
np.random.seed(42)
t = np.arange(60, dtype=float)
qi_true, Di_true = 800.0, 0.06
q = qi_true * np.exp(-Di_true * t) * (1 + np.random.normal(0, 0.05, len(t)))
q = np.maximum(q, 10)
dates = pd.date_range("2020-01", periods=60, freq="MS")
wdf = pd.DataFrame({date_col if 'date_col' in dir() else "date": dates, "Liquid (bbl)": q})
date_col = list(wdf.columns)[0]
liq_col = "Liquid (bbl)"
qi, Di = qi_true, Di_true
t_fit = np.arange(len(wdf), dtype=float)
q_fit = qi * np.exp(-Di * t_fit) if (qi is not None and Di is not None) else None
dates_arr = wdf[date_col]
fig = go.Figure()
# Actual production
fig.add_trace(go.Scatter(
x=dates_arr,
y=wdf[liq_col],
mode="lines+markers",
name="Actual production",
marker=dict(size=5, color=VIOLET),
line=dict(color=VIOLET, width=1.5),
hovertemplate="%{x|%b %Y}: %{y:,.0f} bbl<extra></extra>",
))
# Exponential fit
if q_fit is not None:
fig.add_trace(go.Scatter(
x=dates_arr,
y=q_fit,
mode="lines",
name=f"Exp. fit (Dᵢ={Di:.3f}/mo)",
line=dict(color=TEAL, width=2, dash="dash"),
hovertemplate="%{x|%b %Y}: %{y:,.0f} bbl<extra>Fit</extra>",
))
fig.update_layout(
xaxis=dict(title=""),
yaxis=dict(title="Liquid Production (bbl)", type="log"),
legend=dict(x=0.98, y=0.98, xanchor="right", yanchor="top"),
margin=dict(t=50, b=50, l=70, r=60),
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly