Best Practices for Reservoir Engineers

Overview

This guide provides industry best practices for using ResSmith in production reservoir engineering workflows. These practices are based on industry standards and common pitfalls observed in real-world applications.

Data Quality and Preparation

1. Validate Input Data

Always validate your production data before analysis:

from ressmith import detect_outliers, validate_production_data

# Check for outliers
outliers = detect_outliers(data, method='iqr')
if len(outliers) > 0:
    logger.warning(f"Found {len(outliers)} outliers. Review before proceeding.")

# Validate temporal alignment
if not data.index.is_monotonic_increasing:
    data = data.sort_index()

Key Checks:

  • No negative production rates

  • Valid date ranges (no future dates, reasonable historical range)

  • Consistent time intervals (monthly, daily, etc.)

  • Handle missing values explicitly (don’t let pandas fill automatically)

2. Handle Operational Changes

Identify and account for operational events:

# Mark operational changes
data['is_operational'] = True
data.loc[shut_in_dates, 'is_operational'] = False
data.loc[workover_dates, 'is_operational'] = False

# Filter to operational periods only
operational_data = data[data['is_operational']]

Common Operational Events:

  • Shut-ins (planned or unplanned)

  • Workovers and stimulations

  • Facility constraints

  • Choke changes

  • Artificial lift installation/optimization

3. Pressure Normalization

For gas wells or variable pressure systems, always normalize rates:

from ressmith import normalize_production_with_pressure

# Gas well with pressure data
normalized_data = normalize_production_with_pressure(
    data,
    rate_col='gas',
    pressure_col='pressure',
    initial_pressure=5000.0
)

# Then fit decline model
forecast = fit_forecast(normalized_data, model_name='arps_hyperbolic')

When to Normalize:

  • Gas wells (always)

  • Oil wells with significant pressure decline

  • Variable pressure systems

  • Before comparing wells with different pressure histories

Model Selection and Validation

1. Minimum Data Requirements

Ensure sufficient data before fitting models:

# Check minimum data requirements
MIN_MONTHS = 12  # Minimum for decline analysis
if len(data) < MIN_MONTHS:
    raise ValueError(
        f"Insufficient data: {len(data)} months. "
        f"Minimum {MIN_MONTHS} months required for decline analysis."
    )

Guidelines:

  • Decline Analysis: Minimum 12 months of production data

  • Early-Time Analysis: 6-12 months (use RTA, not decline models)

  • Mature Wells: 24+ months for reliable forecasts

  • Unconventional: May need 18+ months due to complex decline behavior

2. Model Comparison

Always compare multiple models:

from ressmith import compare_models

# Compare models
comparison = compare_models(
    data,
    model_names=['arps_hyperbolic', 'power_law', 'duong', 'arps_exponential'],
    horizon=60
)

# Select best model based on multiple criteria
best_model = comparison.loc[
    (comparison['r_squared'] > 0.9) & 
    (comparison['mape'] < 15)
].sort_values('r_squared', ascending=False).iloc[0]

print(f"Best model: {best_model['model_name']}")
print(f"R²: {best_model['r_squared']:.3f}")
print(f"MAPE: {best_model['mape']:.2f}%")

Selection Criteria:

  • R² > 0.9: Good fit

  • MAPE < 15%: Reasonable forecast accuracy

  • Realistic Parameters: Check parameter ranges (see Model Selection Guide)

  • Physical Interpretation: Parameters should make engineering sense

3. Model Validation

Validate models with backtesting:

from ressmith import walk_forward_backtest

# Walk-forward validation
backtest = walk_forward_backtest(
    data,
    model_name='arps_hyperbolic',
    forecast_horizons=[12, 24, 36],
    min_train_size=12,
    step_size=3
)

# Check validation metrics
print(f"12-month forecast RMSE: {backtest[backtest['horizon']==12]['rmse'].mean():.2f}")
print(f"24-month forecast RMSE: {backtest[backtest['horizon']==24]['rmse'].mean():.2f}")

Validation Guidelines:

  • Use walk-forward backtesting for time series

  • Require RMSE < 20% of mean production rate

  • Check forecast accuracy at multiple horizons

  • Validate on out-of-sample data (not used for fitting)

Forecasting Best Practices

1. Forecast Horizon

Use appropriate forecast horizons:

# Calculate appropriate horizon
historical_length = len(data)
forecast_horizon = min(
    historical_length * 2,  # 2x historical data
    120  # Maximum 10 years
)

forecast = fit_forecast(
    data,
    model_name='arps_hyperbolic',
    horizon=forecast_horizon
)

Guidelines:

  • Short-term (1-2 years): Use 2x historical data length

  • Long-term (5-10 years): Maximum 10 years, use probabilistic methods

  • Reserves Estimation: Use economic limits, not fixed horizons

2. Economic Limits

Always apply economic limits:

from ressmith import estimate_eur

eur_result = estimate_eur(
    data,
    model_name='arps_hyperbolic',
    t_max=360,  # 30 years maximum
    econ_limit=10.0  # Stop at 10 STB/day
)

print(f"EUR: {eur_result['eur']:.0f} STB")
print(f"Economic limit reached at: {eur_result.get('t_econ_limit', 'N/A')} days")

Economic Limit Guidelines:

  • Oil Wells: 5-15 STB/day (depends on operating costs)

  • Gas Wells: 50-200 MCF/day (depends on processing costs)

  • Unconventional: May be lower (5-10 STB/day) due to higher costs

3. Probabilistic Forecasting

Always use probabilistic methods for reserves:

from ressmith import probabilistic_forecast

# Probabilistic forecast
prob = probabilistic_forecast(
    data,
    model_name='arps_hyperbolic',
    horizon=120,
    n_samples=1000,
    param_uncertainty={
        'qi': (0.1, 'relative'),  # ±10% uncertainty
        'di': (0.2, 'relative'),  # ±20% uncertainty
        'b': (0.1, 'absolute')    # ±0.1 absolute uncertainty
    }
)

# Report P10/P50/P90
print(f"P10 EUR: {prob['p10'].sum():.0f} STB")
print(f"P50 EUR: {prob['p50'].sum():.0f} STB")
print(f"P90 EUR: {prob['p90'].sum():.0f} STB")

Uncertainty Guidelines:

  • P10 (Optimistic): 10% probability of exceeding

  • P50 (Deterministic): 50% probability (most likely)

  • P90 (Conservative): 90% probability of exceeding

  • Use P90 for reserves booking (conservative estimate)

Reservoir Engineering Integration

1. Combine with RTA

Use RTA to validate decline models:

from ressmith import analyze_production_data, identify_flow_regime

# Check flow regime
regime = identify_flow_regime(time, rate)

if regime == 'transient':
    # Use RTA, not decline models
    rta_result = analyze_production_data(time, rate)
    print(f"Permeability: {rta_result['permeability']:.2f} md")
    print(f"Flow regime: {rta_result['flow_regime']}")
else:
    # Safe to use decline models
    forecast = fit_forecast(data, model_name='arps_hyperbolic')

2. Material Balance Validation

Validate forecasts with material balance:

from ressmith.primitives import solution_gas_drive_material_balance

# Material balance check
mb_result = solution_gas_drive_material_balance(
    pressure=current_pressure,
    cumulative_production=current_cumulative,
    params=mb_params
)

# Compare with forecast
forecast_cumulative = forecast.yhat.cumsum()
if abs(mb_result['Np_calculated'] - forecast_cumulative.iloc[-1]) > 0.1 * forecast_cumulative.iloc[-1]:
    logger.warning("Material balance and forecast don't match. Review assumptions.")

3. Well Interference

Account for well interference in multi-well fields:

from ressmith import analyze_interference_with_production_history

# Analyze interference
interference = analyze_interference_with_production_history(
    well_data={
        'well_1': data1,
        'well_2': data2
    },
    well_locations={
        'well_1': (lat1, lon1),
        'well_2': (lat2, lon2)
    }
)

# Adjust forecasts for interference
if interference['interference_factor'] > 0.1:
    logger.warning(
        f"Significant interference detected: "
        f"{interference['interference_factor']*100:.1f}%"
    )

Economics Best Practices

1. Scenario Analysis

Always run multiple price scenarios:

from ressmith import evaluate_scenarios
from ressmith.objects import EconSpec

base_spec = EconSpec(
    price_assumptions={'oil': 70.0},
    opex=15.0,
    discount_rate=0.1
)

scenarios = {
    'low': {'prices': {'oil': 50.0}, 'opex': 18.0},
    'base': {},
    'high': {'prices': {'oil': 90.0}, 'opex': 12.0}
}

results = evaluate_scenarios(forecast, base_spec, scenarios)

for scenario, result in results.items():
    print(f"{scenario}: NPV = ${result.npv:,.0f}")

2. Discount Rate Selection

Use appropriate discount rates:

  • Corporate Hurdle: Typically 10-15%

  • Risk-Adjusted: Higher for riskier projects (15-20%)

  • Real Options: Lower for flexible projects (8-12%)

  • Reserves Reporting: Follow SEC/SPE guidelines

3. Operating Costs

Include all relevant costs:

econ_spec = EconSpec(
    price_assumptions={'oil': 70.0, 'gas': 3.0},
    opex=15.0,  # $/STB operating costs
    capex=100000.0,  # Initial capital
    discount_rate=0.1,
    taxes=0.35  # Tax rate
)

Cost Components:

  • OPEX: Lifting costs, processing, transportation

  • CAPEX: Drilling, completion, facilities

  • Taxes: Severance, ad valorem, income taxes

  • Abandonment: End-of-life costs

Portfolio Analysis

1. Consistent Methodology

Use consistent methods across portfolio:

from ressmith import analyze_portfolio

# Analyze entire portfolio with same method
portfolio = analyze_portfolio(
    well_data,
    model_name='arps_hyperbolic',  # Consistent model
    horizon=60,  # Consistent horizon
    econ_spec=econ_spec  # Consistent economics
)

# Rank by value
ranked = portfolio.sort_values('npv', ascending=False)

2. Risk Reporting

Report risk metrics:

# Portfolio risk metrics
portfolio_stats = {
    'total_eur_p50': portfolio['eur'].sum(),
    'total_npv': portfolio['npv'].sum(),
    'wells_count': len(portfolio),
    'avg_eur': portfolio['eur'].mean(),
    'std_eur': portfolio['eur'].std()
}

print(f"Portfolio P50 EUR: {portfolio_stats['total_eur_p50']:,.0f} STB")
print(f"Portfolio NPV: ${portfolio_stats['total_npv']:,.0f}")

Documentation and Reproducibility

1. Document Assumptions

Always document key assumptions:

# Document assumptions
assumptions = {
    'model': 'arps_hyperbolic',
    'horizon': 60,
    'econ_limit': 10.0,
    'oil_price': 70.0,
    'discount_rate': 0.1,
    'data_period': '2019-01 to 2023-12',
    'operational_events': 'Shut-in 2020-03 to 2020-05'
}

# Save with results
results['assumptions'] = assumptions

2. Version Control

Track code and data versions:

import ressmith
import pandas as pd

# Document versions
metadata = {
    'ressmith_version': ressmith.__version__,
    'pandas_version': pd.__version__,
    'analysis_date': pd.Timestamp.now(),
    'analyst': 'Engineer Name'
}

Summary Checklist

Before finalizing any analysis:

  • [ ] Data validated (outliers, missing values, temporal alignment)

  • [ ] Operational events accounted for

  • [ ] Pressure normalized (if applicable)

  • [ ] Multiple models compared

  • [ ] Model validated with backtesting

  • [ ] Forecast horizon appropriate

  • [ ] Economic limits applied

  • [ ] Probabilistic forecast generated (P10/P50/P90)

  • [ ] RTA validation (if early-time data)

  • [ ] Material balance check (if applicable)

  • [ ] Well interference considered (if multi-well)

  • [ ] Multiple price scenarios evaluated

  • [ ] Assumptions documented

  • [ ] Results reproducible

For more detailed guidance, see: