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…
- CoursePython for Banking and Finance
- Lesson32 of 24
- Video23 min
- FormatJupyter notebook · 18 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbMoving 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))
# 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())
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))
# 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))
# 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()
# 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))
# 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))
# 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))
# 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()
# 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))
# 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))
# 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))
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)
# 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)
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))
# 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)
# 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()
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.



