Mathew K Analytics

Lesson 32 · Python for Banking and Finance

Applying Moving Averages and Smoothing Techniques to Financial Time Series in Python

In this lesson, we will learn how to detect trends and fluctuations in financial data using moving averages. Moving averages are important in banking for…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Moving Averages and Smoothing in Banking Transactions#

  • In this lesson, we will learn how to detect trends and fluctuations in financial data using moving averages.
  • Moving averages are important in banking for tasks like detecting spending patterns, smoothing out noise, and preventing false alerts.
  • You will work with synthetic banking transaction data to master different moving average and smoothing techniques.
  • You will build practical skills to summarize transaction flows, highlight seasonality, and prepare data for real-world risk alerts.
  • By the end, you will be able to use Python to calculate and visualize a variety of smoothing techniques on banking data.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
np.random.seed(42)

Understanding the Banking Transactions Data#

  • We will use a synthetic transactions dataset, representing customers, their transactions, and relevant details.
  • Each row is one transaction: with columns for customer ID, amount, type (Debit/Credit), channel, and timestamp.
  • Beginners often forget that financial data is typically not evenly spaced, and may have outliers.
  • Always check the time intervals and outliers before smoothing.
n_transactions = 1000
n_customers = 200
df = pd.DataFrame({
    'transaction_id': range(1, n_transactions + 1),
    'customer_id': np.random.choice([f'CUST_{i:04d}' for i in range(1, n_customers + 1)], n_transactions),
    'amount': np.round(np.random.normal(150, 60, n_transactions), 2),
    'transaction_type': np.random.choice(['Debit', 'Credit'], n_transactions),
    'channel': np.random.choice(['ATM', 'Online', 'Branch', 'POS'], n_transactions),
    'date': pd.date_range(start='2024-01-01', periods=n_transactions, freq='h')
})
print(df.shape)
print(df.head(3))
(1000, 6)
   transaction_id customer_id  amount transaction_type channel  \
0               1   CUST_0103  238.77           Credit     ATM   
1               2   CUST_0180  269.17           Credit     POS   
2               3   CUST_0093   58.62           Credit  Online   

                 date  
0 2024-01-01 00:00:00  
1 2024-01-01 01:00:00  
2 2024-01-01 02:00:00  
# Basic aggregation: sum transaction amounts per day
df['date_day'] = df['date'].dt.date
daily_amounts = df.groupby('date_day')['amount'].sum().reset_index()
print(daily_amounts.head())
     date_day   amount
0  2024-01-01  3771.76
1  2024-01-02  3993.85
2  2024-01-03  3777.92
3  2024-01-04  3946.69
4  2024-01-05  3489.53

What is a Moving Average?#

  • A moving average smooths out short-term jumps in amounts by averaging over a sliding window.
  • In banking, it helps identify trends, spot unusual surges, or inform credit limits.
  • Beginners often make the window too small or large; each use-case needs the right period.
  • Always plot the results to see if smoothing adds real insight.
# Beginner Example 1: Simple 3-day moving average
daily_amounts['MA_3'] = daily_amounts['amount'].rolling(window=3).mean()
print(daily_amounts[['amount','MA_3']].head(5))
    amount         MA_3
0  3771.76          NaN
1  3993.85          NaN
2  3777.92  3847.843333
3  3946.69  3906.153333
4  3489.53  3738.046667
# Beginner Example 2: 7-day moving average for weekly smoothing
daily_amounts['MA_7'] = daily_amounts['amount'].rolling(window=7).mean()
print(daily_amounts[['amount','MA_7']].head(10))
    amount         MA_7
0  3771.76          NaN
1  3993.85          NaN
2  3777.92          NaN
3  3946.69          NaN
4  3489.53          NaN
5  3632.14          NaN
6  3481.98  3727.695714
7  3553.62  3696.532857
8  3941.06  3688.991429
9  3450.38  3642.200000
# Beginner Example 3: Visualizing daily amounts vs 7-day moving average
plt.figure(figsize=(10,5))
plt.plot(daily_amounts['date_day'], daily_amounts['amount'], label='Daily Total', alpha=0.7)
plt.plot(daily_amounts['date_day'], daily_amounts['MA_7'], label='7-day MA', color='red', linewidth=2)
plt.xlabel('Date')
plt.ylabel('Amount')
plt.title('Daily Transaction Totals and 7-day Moving Average')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image
# Intermediate Example 1: 14-day moving average for trend analysis
daily_amounts['MA_14'] = daily_amounts['amount'].rolling(window=14).mean()
print(daily_amounts[['amount','MA_14']].tail(10))
     amount        MA_14
32  2858.19  3605.938571
33  3980.31  3647.354286
34  3113.85  3559.196429
35  3589.21  3536.608571
36  3773.28  3511.585000
37  3174.86  3498.551429
38  3541.97  3484.867857
39  3390.59  3499.635714
40  3650.13  3489.317143
41  2947.83  3412.750000
# Intermediate Example 2: Centered moving average
daily_amounts['MA_7_centered'] = daily_amounts['amount'].rolling(window=7, center=True).mean()
print(daily_amounts[['amount','MA_7','MA_7_centered']].head(10))
    amount         MA_7  MA_7_centered
0  3771.76          NaN            NaN
1  3993.85          NaN            NaN
2  3777.92          NaN            NaN
3  3946.69          NaN    3727.695714
4  3489.53          NaN    3696.532857
5  3632.14          NaN    3688.991429
6  3481.98  3727.695714    3642.200000
7  3553.62  3696.532857    3627.931429
8  3941.06  3688.991429    3660.662857
9  3450.38  3642.200000    3667.007143
# Intermediate Example 3: Exponential Weighted Moving Average (EWMA)
daily_amounts['EWMA_7'] = daily_amounts['amount'].ewm(span=7, adjust=False).mean()
print(daily_amounts[['amount','EWMA_7']].head(10))
    amount       EWMA_7
0  3771.76  3771.760000
1  3993.85  3827.282500
2  3777.92  3814.941875
3  3946.69  3847.878906
4  3489.53  3758.291680
5  3632.14  3726.753760
6  3481.98  3665.560320
7  3553.62  3637.575240
8  3941.06  3713.446430
9  3450.38  3647.679822
# Intermediate Example 4: Plot all types of smoothing
plt.figure(figsize=(12,6))
plt.plot(daily_amounts['date_day'], daily_amounts['amount'], label='Raw', alpha=0.5)
plt.plot(daily_amounts['date_day'], daily_amounts['MA_7'], label='7d MA', color='red')
plt.plot(daily_amounts['date_day'], daily_amounts['MA_14'], label='14d MA', color='green', linestyle='--')
plt.plot(daily_amounts['date_day'], daily_amounts['EWMA_7'], label='7d EWMA', color='orange')
plt.xlabel('Date')
plt.ylabel('Amount')
plt.title('Comparison of Moving Averages - Banking Transactions')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image
# Advanced Example 1: Handling outliers in banking data
daily_amounts['amount_no_outlier'] = np.where(
    (daily_amounts['amount'] > daily_amounts['amount'].quantile(0.99)) |
    (daily_amounts['amount'] < daily_amounts['amount'].quantile(0.01)),
    np.nan, daily_amounts['amount']
)
daily_amounts['MA_7_no_outlier'] = daily_amounts['amount_no_outlier'].rolling(window=7).mean()
print(daily_amounts[['amount','MA_7','MA_7_no_outlier']].tail(10))
     amount         MA_7  MA_7_no_outlier
32  2858.19  3490.118571              NaN
33  3980.31  3516.650000              NaN
34  3113.85  3387.232857              NaN
35  3589.21  3400.388571              NaN
36  3773.28  3394.085714              NaN
37  3174.86  3382.580000              NaN
38  3541.97  3433.095714              NaN
39  3390.59  3509.152857      3509.152857
40  3650.13  3461.984286      3461.984286
41  2947.83  3438.267143      3438.267143
# Advanced Example 2: Using groupby for customer-level smoothing
df['date_day'] = df['date'].dt.date
cust_daily = df.groupby(['customer_id','date_day'])['amount'].sum().reset_index()
cust_daily['MA_3_by_cust'] = cust_daily.groupby('customer_id')['amount'].rolling(window=3).mean().reset_index(0,drop=True)
print(cust_daily[cust_daily['customer_id']=='CUST_0001'].head(6))
  customer_id    date_day  amount  MA_3_by_cust
0   CUST_0001  2024-01-06  166.63           NaN
1   CUST_0001  2024-01-21  162.63           NaN
2   CUST_0001  2024-01-23  125.43    151.563333
3   CUST_0001  2024-01-30   86.50    124.853333
4   CUST_0001  2024-01-31  165.09    125.673333
5   CUST_0001  2024-02-04  158.52    136.703333
# Advanced Example 3: Detecting anomalies: flagging points far from the moving average
cust_daily['deviation'] = cust_daily['amount'] - cust_daily['MA_3_by_cust']
cust_daily['anomaly_flag'] = np.abs(cust_daily['deviation']) > 2 * cust_daily.groupby('customer_id')['amount'].transform('std')
anomalies = cust_daily[cust_daily['anomaly_flag']]
print(anomalies.head(5))
Empty DataFrame
Columns: [customer_id, date_day, amount, MA_3_by_cust, deviation, anomaly_flag]
Index: []

Error Handling and Debugging Smoothing Operations#

  • Sometimes, rolling means fail with missing data or the wrong window size.
  • You should always monitor for NaNs, mismatches in alignment, and time gaps.
  • Printing descriptive errors helps you fix pipeline issues quickly.
# Handling NaNs from insufficient data
try:
    test_df = pd.DataFrame({'val':[1,2,np.nan,4,5]})
    test_df['MA_3'] = test_df['val'].rolling(window=3).mean()
    print(test_df)
except Exception as e:
    print('Error:', e)
   val  MA_3
0  1.0   NaN
1  2.0   NaN
2  NaN   NaN
3  4.0   NaN
4  5.0   NaN
# Defensive smoothing: fill NaNs and proceed
safe_df = test_df.copy()
safe_df['val_filled'] = safe_df['val'].fillna(method='ffill')
safe_df['MA_3_filled'] = safe_df['val_filled'].rolling(window=3).mean()
print(safe_df)
   val  MA_3  val_filled  MA_3_filled
0  1.0   NaN         1.0          NaN
1  2.0   NaN         2.0          NaN
2  NaN   NaN         2.0     1.666667
3  4.0   NaN         4.0     2.666667
4  5.0   NaN         5.0     3.666667

Best Practices When Applying Moving Averages#

  • Always check your data for NaNs and gaps before smoothing.
  • Communicate the meaning of your smoothing window to business stakeholders.
  • Compare multiple windows to find the most business-relevant trend.
  • In critical systems, log or monitor if smoothing hides sudden and important changes.
# Common pattern: wrapper function for moving average
def compute_moving_average(series, window, center=False):
    return series.rolling(window=window, center=center).mean()
 
daily_amounts['MA_func'] = compute_moving_average(daily_amounts['amount'], 5)
print(daily_amounts[['amount','MA_func']].head(10))
    amount   MA_func
0  3771.76       NaN
1  3993.85       NaN
2  3777.92       NaN
3  3946.69       NaN
4  3489.53  3795.950
5  3632.14  3768.026
6  3481.98  3665.652
7  3553.62  3620.792
8  3941.06  3619.666
9  3450.38  3611.836
# End-to-end Example: Monthly cash flow smoothing for operational planning
df['month'] = df['date'].dt.to_period('M')
monthly_amounts = df.groupby('month')['amount'].sum().reset_index()
monthly_amounts['MA_3_month'] = monthly_amounts['amount'].rolling(window=3).mean()
monthly_amounts['EWMA_3_month'] = monthly_amounts['amount'].ewm(span=3, adjust=False).mean()
print(monthly_amounts)
     month     amount  MA_3_month  EWMA_3_month
0  2024-01  114493.50         NaN     114493.50
1  2024-02   37208.58         NaN      75851.04
# Visualizing monthly smoothing results
plt.figure(figsize=(8,4))
plt.plot(monthly_amounts['month'].astype(str), monthly_amounts['amount'], label='Monthly Total', marker='o')
plt.plot(monthly_amounts['month'].astype(str), monthly_amounts['MA_3_month'], label='3-MA', marker='s')
plt.plot(monthly_amounts['month'].astype(str), monthly_amounts['EWMA_3_month'], label='3-EWMA', marker='^')
plt.xlabel('Month')
plt.ylabel('Amount')
plt.title('Monthly Transaction Totals and Smoothing')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image

Thanks and Next Steps#

  • You now know how to use moving averages and exponential smoothing to make sense of raw transaction data.
  • Use these tools to spot trends, prepare forecasts, and flag possible risk events.
  • For more banking analytics and practical code, check out my YouTube and subscribe for weekly lessons!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.