Baseflow Separation Algorithms

Introduction

Hydrograph analysis encompasses examination of components that contribute to streamflow for a river system. Baseflow and stormflow are the components that contribute to streamflow, and thus separation of the streamflow hydrograph into baseflow and stormflow components is useful in the estimation of groundwater flow contributions to the streamflow. Similarly, hydrograph analysis techniques are useful in numerous water resource and river management investigations to ensure sustainable use of water resources in supporting different ecosystems.

Baseflow represents is a portion of streamflow contributed through groundwater flow and delayed recharge sources while stormflow encompasses any streamflow contribution emerging from direct runoff and precipitation. During hydrograph analysis, baseflow separation approach is employed to estimate the portion of streamflow being contributed by baseflow and stormflow components which helps in the prediction of river recharge rates.

There are numerous baseflow separation techniques in literature, and application of each technique varies depending on watershed characteristics. In this article four baseflow separation techniques based on Eckhardt, 2004; Lyne and Hollick, 1979; and Sloto and Crouse, 1996 (fixed interval and sliding interval) were implemented and tested using python. It should be noted that Eckhardt, 2004 and Lyne and Hollick, 1979 techniques are rooted from techniques applied in signal processing and they are sometimes referred to as digital filters. However, baseflow flow techniques developed by Sloto and Crouse, 1996 rely on the concept of systematically constructing a connecting line between the low points of the streamflow hydrograph., and these connecting lines define the baseflow component of the streamflow hydrograph. Below is the definition of each baseflow algorithm and their implementation within python.

Baseflow algorithms

  • Lyne and Hollick, 1979 - Also known as one parameter digital filter technique.

q[t] = alpha * q[t-1] + (1 + alpha)/ 2 * (Q[t] - Q[t-1])

Where:

q[t]: Storm runoff at time step t

q[t-1]: Storm runoff at time step t-1

Q[t]: Total streamflow at time step t

Q[t-1]: Total streamflow at time step t-1

alpha : Filter parameter

Python implementation - Lyne and Hollick, 1979

"""

A function Lyne_Hollick that performs baseflow separation based on Lyne and Hollick, 1979.

Q: Time series of streamflow measurements

alpha : filter parameter

direction : Options - forward (f) or backward (b) calculation

"""

def Lyne_Hollick(Q,bflow, alpha=.925, direction = 'f'):

# Check if there has already been a run

if len(bflow) > 0:

Q = np.array(bflow)

else:

Q = np.array(Q)

f = np.zeros(len(Q))

if direction[0] == 'f':

for t in np.arange(1,len(Q)):

# algorithm

f[t] = alpha * f[t-1] + (1 + alpha)/2 * (Q[t] - Q[t-1])

# to prevent negative values

if Q[t] - f[t] > Q[t]:

f[t] = 0

elif direction[0] == 'b':

for t in np.arange(len(Q)-2, 1, -1):

f[t] = alpha * f[t+1] + (1 + alpha)/2 * (Q[t] - Q[t+1])

if Q[t] - f[t] > Q[t]:

f[t] = 0


return (f)



  • Eckhardt, 2004 - Also known as the recursive digital filter method .

b[t] = [ ( 1 - BFI) * alpha * b[ t-1 ]+ ( 1 - alpha ) * BFI * Q[t ]] / ( 1 - alpha * BFI)

Where:

b[t]: Baseflow at time step t

b[t-1]: Baseflow at time step t-1

Q[t]: Total streamflow at time step t

BFI: Baseflow Index - Fraction of baseflow to the total streamflow

alpha : Filter parameter

Python implementation - Eckhardt, 2004

"""

A function Eckhardt that separates streamflow into baseflow and stormflow.

The recursive digital filter for baseflow separation developed Based on Eckhardt (2004)

Inputs:

Qt: Time series of streamflow measurements

alpha : filter parameter

BFI : maximum baseflow index

re : number of times to run filter

"""

def Eckhardt(Qt,alpha, BFI, re):

bflow = []

# Check if bflow is empty - useful in handling multiple filtering

if len(bflow) > 0:

Qt = np.array(bflow)

else:

Qt = np.array(Qt)

# Create an array of zeros - used to collect basefloe values

f = np.zeros(len(Qt))

# Initialize baseflow at time, t = 0

f[0] = Qt[0]

# Filter out baseflow from streamflow

for t in np.arange(1,len(Qt)):

# algorithm

f[t] = ((1 - BFI) * alpha * f[t-1] + (1 - alpha) * BFI * Qt[t]) / (1 - alpha * BFI)

if f[t] > Qt[t]:

f[t] = Qt[t]

# Adds the baseflow to self variables so it can be called recursively

bflow = f

# calls method again if multiple passes are specified

if re > 1:

bflow = Eckhardt(Qt,bflow,alpha=alpha, BFI=BFI, re=re-1)

return (bflow)


The duration of surface runoff is calculated using the equation:

N = A ^0.2

where :

N : Number of days after which surface runoff ceases

A : Drainage area in square miles .

The fixed-interval method finds the lowest discharge in each interval (2N*, where N* is the integer). The sliding-interval method assigns the lowest discharge in one half the interval minus one day (0.5(2N* − 1) days).

Python implementation - Sloto and Crouse , 1996

"""

Function sliding_fixed_filter that separates streamflow into baseflow and stormflow based on

approaches proposed by Slot & Crouse, 1996.

Inputs:

flow_data - Discharge from the usgs station

station_id - USGS station Id

filter_flag - Options: sliddingfilter or fixedfilter

Drainage_area - basin drainage area. Use drain_area(station_id) function to automatically get drainage area for US stations.

"""

def sliding_fixed_filter(flow_data,station_id,filter_flag,drainage_area):

# obtaining the drainage area

#drainage_area = drain_area(station_id)

# setting window

Nact = drainage_area **0.2

N2star1 = float(str((Nact*4+1)/2).split('.')[0])

# Selecting only odd values

if (N2star1 % 2) == 0 :

N2star = int(N2star1 +1)

else:

N2star = int(N2star1)

# setting up the window

Nobs = len(flow_data)

Ngrp = math.ceil((Nobs /N2star))

# generating the sequences

f2 = np.arange(1,Ngrp+1,1) # Varying windowsizes

group_list = [np.repeat('w'+str(i),N2star) for i in f2 ]

Grps = np.concatenate(group_list,axis=0)

# creating a df of values

flow_wd = pd.DataFrame(flow_data)

flow_wd['windowsize']= Grps[0:Nobs] # Truncating the sequence to the length of the flow data

# Computing minimum baseflows for each window

min_size = flow_wd.groupby(['windowsize']).agg(min)

# Minimum baseflow values

# column header

col_hd = list(min_size.columns)[0]

min_flows = list(min_size[col_hd])

# Assigning flows to each grouping

min_bf = [np.repeat(i,N2star) for i in min_flows ]

Grpbaseflow = np.concatenate(min_bf,axis=0)

# fixed filter

if filter_flag == 'fixedfilter':

flow_wd['fixedfilter'] = Grpbaseflow[0:Nobs]

base_flow = flow_wd['fixedfilter']

base_flow.index = flow_data.index

if filter_flag == 'sliddingfilter':

# sliding filter - compute rolling minimum

flow_wd['slidingfilter'] = flow_wd[col_hd].rolling(N2star).min()

# Inserting the constant baseflow for the initial window

flow_wd.loc[flow_wd['slidingfilter'].isnull(), 'slidingfilter'] = np.array(flow_wd['slidingfilter'].dropna())[0]

base_flow = flow_wd['slidingfilter']

base_flow.index = flow_data.index

return(base_flow)



Application

The Eckhardt, 2004 baseflow technique was employed to segment streamflow into baseflow and stormflow using the python scrip explained earlier. First import the python packages and install missing packages as needed.

# Import required packages

import pandas as pd

import numpy as np

import math

After importing all the required packages, obtain the streamflow data for the gauge of interest. You can use the USGS streamflow data extraction script that I implemented in python. This python script is available here. You will need to visualize and understand your data prior to performing baseflow separation. Next, call the " Eckhardt function " described in python implementation of Eckhardt baseflow separation algorithm. At this point your script will look similar to the code snippet below. After executing the entire code, you can export the resultant data frame to a csv , text or excel file or create an interactive plot as the shown in the figure below.

# Streamflow gauge information

station_id = '03336000'

start_date = '2014-10-01'

end_date = '2020-10-19'

# Extract streamflow data using streamflow_raw function

raw_flow = streamflow_raw (station_id,start_date, end_date)

# Parameters for Eckhardt function

q = raw_flow['Flow (cfs)']

alpha = 0.95

re = 1

bfi = 0.8

# Running the Eckhardt algorithm

baseflow = Eckhardt(q,alpha, bfi, re)

# Add base flow to the streamflow data frame

raw_flow['baseflow'] = baseflow

# Compute runoff

raw_flow['runoff'] = q-baseflow

An Interactive plot showing the variability in baseflow and stormflow magnitudes obtained using the Eckhardt baseflow separation algorithm. You can select each item one by one by clicking on the item name within the legend or zoom in and out using plot interactive controls on the right side of the chart.

Conclusion

Baseflow separation techniques provide valuable information on the variability of baseflow and stormflow contributions to streamflow for a river system. However, there are factors that impact the accuracy the baseflow separation algorithms. Missing values, available period of record, temporal changes in watershed characteristics such as dam or reservoir construction, etc will impact the performance of the baseflow separation algorithms.