Deposit and Loan Portfolio Analytics

Exploratory and Inferential Study of Treasury Operations at First City Monument Bank

Author

Faith Ifeoma Enubuogidi

Published

May 12, 2026

Deposit and Loan Portfolio Analytics

Treasury Department | Asset and Liability Management

Faith Ifeoma Enubuogidi | ALM Treasury Officer | May 2026

1 Executive Summary

8,260
Total Records
7,389
NGN Term Deposits
725
USD Term Deposits
146
Active Loan Facilities

This report presents an exploratory and inferential analysis of the deposit and loan portfolio held within the Treasury Department of First City Monument Bank Limited FCMB. The dataset comprises 8,260 records extracted from the bank core banking system as at May 2026.

Five analytical techniques are applied: (1) Exploratory Data Analysis; (2) Data Visualisation; (3) Hypothesis Testing; (4) Correlation Analysis; and (5) Linear Regression.

Key findings indicate that the NGN deposit book is heavily concentrated in short-tenor placements, creating significant rollover risk. USD deposits carry lower but more dispersed rates, and customer segments attract different rate profiles across both deposits and loans.


2 Professional Disclosure

👤 Author Professional Profile
Full NameFaith Ifeoma Enubuogidi
Job TitleAsset and Liability Management Treasury Officer
OrganisationFirst City Monument Bank Limited (FCMB)
DepartmentTreasury Department
Organisation TypeCommercial Bank — Licensed by the Central Bank of Nigeria
Experience10 years

2.1 Role Description

As an ALM Treasury Officer at FCMB, my primary responsibility is the daily monitoring and analysis of the bank deposit and loan book. This involves reviewing the maturity profile of term deposits in both Naira and foreign currency, assessing repricing risk as deposits mature and rates change, identifying high-risk concentrations in short-tenor or high-rate placements relative to the CBN Monetary Policy Rate, and reporting the bank liquidity position to senior management. I work directly with the data analysed in this report as part of my routine Treasury operations.

2.2 Technique Justification

01
Exploratory Data Analysis
Before advising management on rate strategy I must understand the shape of our deposit book including size distributions, missing values, and outlier placements. EDA replicates what I do every morning when I open the deposit register.
02
Data Visualisation
Treasury reports presented to ALCO are visual including maturity ladders and rate bar charts. This technique formalises that storytelling with reproducible code.
03
Hypothesis Testing
A standing management question is whether different customer segments attract statistically different rates. Hypothesis testing gives a rigorous defensible answer.
04
Correlation Analysis
In ALM we need to understand whether deposit size drives rate. Correlation analysis quantifies this relationship across our entire book.
05
Linear Regression
Predicting maturity value of a deposit given its amount, tenor, and rate underpins our cashflow forecasting. Regression formalises this calculation.

3 Data Collection and Sampling

3.1 Source and Collection Method

The dataset was extracted directly from FCMB Finacle Core Banking System by the author on 9 May 2026. The extraction covers all active term deposit accounts in NGN and USD and all active loan facilities as at the extraction date. No external or secondary data sources are used.

3.2 Dataset Overview

Table Sheet Records Variables Coverage
NGN Term Deposits NGN 7,389 12 All active NGN fixed deposits
USD Term Deposits USD 725 12 All active USD fixed deposits
Loan Facilities Loan 146 11 All active NGN loan accounts

3.3 Ethical Statement

Data Governance Notice: The data was accessed in the course of the author official duties at FCMB. In accordance with FCMB data governance policy, the raw dataset is not shared publicly and is available to the examiner on written request. This report presents aggregated statistics and visualisations only, with no individual account details reproduced in full.

Data Citation: Enubuogidi F I (2026). FCMB Active Term Deposit and Loan Portfolio May 2026 Extract [Dataset]. Treasury Department, First City Monument Bank Limited, Lagos, Nigeria. Available on request from the author.

4 Data Description

4.1 Load and Prepare Data

Code
ngn  <- read_excel("DA SHEET.xlsx", sheet = "NGN")  |> clean_names()
usd  <- read_excel("DA SHEET.xlsx", sheet = "USD")  |> clean_names()
loan <- read_excel("DA SHEET.xlsx", sheet = "Loan") |> clean_names()

ngn  <- ngn  |> mutate(currency = "NGN")
usd  <- usd  |> mutate(currency = "USD")

deposits <- bind_rows(ngn, usd) |>
  mutate(
    rate             = as.numeric(as.character(rate)),
    tenor_group = case_when(
      deposit_period_days <= 30  ~ "A 30 days or less",
      deposit_period_days <= 60  ~ "B 31 to 60 days",
      deposit_period_days <= 90  ~ "C 61 to 90 days",
      deposit_period_days <= 180 ~ "D 91 to 180 days",
      deposit_period_days <= 365 ~ "E 181 to 365 days",
      TRUE                       ~ "F Over 365 days"
    ),
    tenor_group      = factor(tenor_group),
    open_month       = floor_date(open_effective_date, "month"),
    days_to_maturity = as.numeric(maturity_date - Sys.Date()),
    rate_risk = case_when(
      rate >= 15 ~ "High 15pct or above",
      rate >= 10 ~ "Medium 10 to 14.9pct",
      TRUE       ~ "Low below 10pct"
    ),
    rate_risk = factor(rate_risk,
      levels = c("Low below 10pct", "Medium 10 to 14.9pct", "High 15pct or above"))
  )

ngn  <- deposits |> filter(currency == "NGN")
usd  <- deposits |> filter(currency == "USD")

loan <- loan |>
  mutate(
    rate_abs      = abs(as.numeric(as.character(rate))),
    tenor_months  = as.numeric(tenor),
    customer_type = str_trim(as.character(customer_type))
  )

cat("Total deposit rows:", nrow(deposits), "
")
Total deposit rows: 8114 
Code
cat("NGN rows:", nrow(ngn), "
")
NGN rows: 7389 
Code
cat("USD rows:", nrow(usd), "
")
USD rows: 725 
Code
cat("Loan rows:", nrow(loan), "
")
Loan rows: 146 

4.2 Summary Statistics Deposits

Code
skim(deposits |> select(deposit_period_days, deposit_amount, maturity_amount, rate, currency))
Data summary
Name select(…)
Number of rows 8114
Number of columns 5
_______________________
Column type frequency:
character 1
numeric 4
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
currency 0 1 3 3 0 2 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
deposit_period_days 0 1 145.73 1.131500e+02 30.0 66.5 90 180 3.650000e+02 ▇▁▃▁▂
deposit_amount 0 1 51431187.94 1.080126e+09 2008.6 1000000.0 3000000 11000000 6.329701e+10 ▇▁▁▁▁
maturity_amount 0 1 53100902.72 1.101347e+09 2009.6 1044383.6 3147945 11279452 6.397334e+10 ▇▁▁▁▁
rate 0 1 8.06 3.270000e+00 0.1 7.0 9 10 1.905000e+01 ▂▂▇▁▁

4.3 Summary Statistics Loans

Code
skim(loan |> select(amount_issued, tenor_months, rate_abs, customer_type))
Data summary
Name select(loan, amount_issue…
Number of rows 146
Number of columns 4
_______________________
Column type frequency:
character 1
numeric 3
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
customer_type 4 0.97 2 5 0 9 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
amount_issued 0 1 17234575.34 52915829.21 141000 3e+06 7.60e+06 14875000.0 5.00e+08 ▇▁▁▁▁
tenor_months 0 1 15.18 20.39 0 6e+00 6.00e+00 12.0 7.20e+01 ▇▁▁▁▁
rate_abs 0 1 24.04 22.60 0 0e+00 4.61e+01 46.1 4.61e+01 ▇▁▁▁▇

5 Exploratory Data Analysis

Exploratory Data Analysis Chapter 4: Summary stats, missing-value analysis, outlier detection

5.1 Business Justification

In Treasury, before I present any report to ALCO, I must first understand the shape of the data. Where are the concentrations, are there gaps, and are there outlier placements that distort averages? This EDA section replicates that morning review process in a reproducible auditable form.

5.2 Missing Value Analysis

Code
miss_deposit <- miss_var_summary(deposits) |> filter(n_miss > 0)
miss_loan    <- miss_var_summary(loan)    |> filter(n_miss > 0)

if (nrow(miss_deposit) > 0) {
  miss_deposit |> kbl(caption = "Missing Values in Deposit Data") |>
    kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
} else {
  cat("No missing values detected in the deposit dataset.")
}
No missing values detected in the deposit dataset.
Code
if (nrow(miss_loan) > 0) {
  miss_loan |> kbl(caption = "Missing Values in Loan Data") |>
    kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
} else {
  cat("No missing values detected in the loan dataset.")
}
Missing Values in Loan Data
variable n_miss pct_miss
maturity_amount 146 100
customer_type 4 2.74

5.3 Outlier Detection

Code
ngn_q   <- quantile(ngn$deposit_amount, c(0.25, 0.75), na.rm = TRUE)
ngn_iqr <- ngn_q[2] - ngn_q[1]
upper   <- ngn_q[2] + 3 * ngn_iqr

ngn |>
  filter(deposit_amount > upper) |>
  arrange(desc(deposit_amount)) |>
  select(acct_name, deposit_amount, rate, deposit_period_days) |>
  head(10) |>
  mutate(deposit_amount = scales::comma(deposit_amount, prefix = "NGN ")) |>
  kbl(caption = "Top 10 Outlier NGN Deposits above Q3 plus 3 times IQR") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Top 10 Outlier NGN Deposits above Q3 plus 3 times IQR
acct_name deposit_amount rate deposit_period_days
BANK OF AGRICULTURE LIMITED NGN 63,297,013,900 13 30
CNG AUTOMOBILITY INFRASTRUCTURE COMPANY LIMITED NGN 60,000,000,000 10 30
FCMB GROUP PLC – AT1 CAPITAL NGN 26,000,000,000 16 182
FCMB GROUP PLC – AT1 CAPITAL NGN 20,686,000,000 16 180
PALMPAY LIMITED NGN 12,000,000,000 7 365
LAGOS STATE PENSION ESCROW ACCOUNT (ACTIVE) NGN 11,841,246,723 9 30
NIG MIDSTREAM & DOWNSTREAM PET REGULATORY AUTH NGN 10,000,000,000 13 60
DANGOTE CEMENT PLC NGN 8,094,684,932 10 30
DANGOTE CEMENT PLC-GLOBAL REVENUE COLLECTION ACCOUNT NGN 7,000,000,000 16 30
SOUTH ENERGYX DEVELOPMENT FZE NGN 6,612,573,767 12 90
Treasury Insight: The outlier deposits represent institutional or corporate placements far exceeding retail ticket sizes. These are legitimate transactions but warrant separate treatment in concentration risk analysis as a handful of accounts contribute disproportionately to total book value.

5.4 Distributional Summary by Tenor Group

Code
deposits |>
  group_by(currency, tenor_group) |>
  summarise(
    Count = n(),
    Avg_Rate = round(mean(rate, na.rm = TRUE), 2),
    Median_Deposit = round(median(deposit_amount, na.rm = TRUE), 0),
    Total_mn = round(sum(deposit_amount, na.rm = TRUE) / 1e6, 1),
    .groups = "drop") |>
  kbl(caption = "Deposit Book Summary by Currency and Tenor Group",
      col.names = c("Currency","Tenor Group","Count","Avg Rate","Median Deposit","Total mn")) |>
  kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = TRUE)
Deposit Book Summary by Currency and Tenor Group
Currency Tenor Group Count Avg Rate Median Deposit Total mn
NGN A 30 days or less 1492 7.26 5000000 228520.7
NGN B 31 to 60 days 437 8.58 5400480 19687.2
NGN C 61 to 90 days 2620 8.23 4500000 51485.5
NGN D 91 to 180 days 1501 8.71 3000000 44057.8
NGN E 181 to 365 days 1339 9.37 2500000 72363.6
USD A 30 days or less 63 4.57 1000000 244.0
USD B 31 to 60 days 25 3.81 325964 43.2
USD C 61 to 90 days 131 3.84 112463 78.3
USD D 91 to 180 days 235 5.44 115921 382.4
USD E 181 to 365 days 271 5.40 220500 449.9

6 Data Visualisation

Data Visualisation Chapter 5: Grammar of graphics, chart selection, storytelling with data

6.1 Business Justification

The ALCO committee at FCMB requires visual representations of the deposit book including maturity ladders, rate distribution charts, and concentration maps to make pricing decisions. These five plots replicate the core visuals I produce for the monthly Treasury report.

6.2 Plot 1 Distribution of NGN Deposit Amounts

Code
p1 <- ggplot(ngn, aes(x = deposit_amount)) +
  geom_histogram(bins = 50, fill = "#1a5276", colour = "white", alpha = 0.85) +
  scale_x_log10(labels = scales::label_number(prefix = "NGN ", suffix = "M", scale = 1e-6, accuracy = 0.1)) +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Distribution of NGN Term Deposit Amounts",
       subtitle = "FCMB Treasury Portfolio May 2026",
       x = "Deposit Amount log scale",
       y = "Number of Accounts",
       caption = "Source: FCMB Finacle Core Banking System May 2026") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", colour = "#1a5276"),
        plot.subtitle = element_text(colour = "#555555"),
        panel.grid.minor = element_blank())
print(p1)

Distribution of NGN Deposit Amounts on a log scale showing most accounts fall between NGN 1M and 20M.

6.3 Plot 2 Interest Rate Distribution by Currency

Code
p2 <- ggplot(deposits, aes(x = rate, fill = currency)) +
  geom_density(alpha = 0.6) +
  scale_fill_manual(values = c("NGN" = "#1a5276", "USD" = "#1e8449")) +
  labs(title = "Interest Rate Distribution NGN vs USD Term Deposits",
       subtitle = "FCMB Treasury Portfolio May 2026",
       x = "Contracted Interest Rate percent per annum",
       y = "Density",
       fill = "Currency",
       caption = "Source: FCMB Finacle Core Banking System May 2026") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", colour = "#1a5276"),
        panel.grid.minor = element_blank())
print(p2)

NGN deposits cluster in a tighter rate band while USD rates are lower and more dispersed.

6.4 Plot 3 Maturity Ladder

Code
ladder_data <- ngn |>
  mutate(maturity_month = floor_date(maturity_date, "month")) |>
  group_by(maturity_month) |>
  summarise(total_bn = sum(deposit_amount, na.rm = TRUE) / 1e9,
            count = n(), .groups = "drop") |>
  filter(maturity_month >= Sys.Date())

p3 <- ggplot(ladder_data, aes(x = maturity_month, y = total_bn)) +
  geom_col(fill = "#154360", alpha = 0.9) +
  geom_text(aes(label = count), vjust = -0.5, size = 3, colour = "#154360") +
  scale_x_date(date_labels = "%b %Y", date_breaks = "1 month") +
  scale_y_continuous(labels = scales::label_number(suffix = "bn", prefix = "NGN ")) +
  labs(title = "NGN Deposit Maturity Ladder",
       subtitle = "Principal maturing per month with account count shown above each bar",
       x = "Maturity Month",
       y = "Total Deposit NGN billions",
       caption = "Source: FCMB Finacle Core Banking System May 2026") +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold", colour = "#1a5276"),
        axis.text.x = element_text(angle = 45, hjust = 1),
        panel.grid.minor = element_blank())
print(p3)

NGN deposit maturity ladder showing total principal maturing each month and account count above each bar.

6.5 Plot 4 Average Rate by Tenor Group and Currency

Code
rate_by_tenor <- deposits |>
  group_by(currency, tenor_group) |>
  summarise(avg_rate = mean(rate, na.rm = TRUE), .groups = "drop")

p4 <- ggplot(rate_by_tenor, aes(x = tenor_group, y = avg_rate, fill = currency)) +
  geom_col(position = "dodge", alpha = 0.9) +
  geom_text(aes(label = round(avg_rate, 1)),
            position = position_dodge(width = 0.9), vjust = -0.4, size = 3.5) +
  scale_fill_manual(values = c("NGN" = "#1a5276", "USD" = "#1e8449")) +
  labs(title = "Average Interest Rate by Tenor Group and Currency",
       subtitle = "FCMB Treasury Portfolio May 2026",
       x = "Tenor Group",
       y = "Average Rate percent per annum",
       fill = "Currency",
       caption = "Source: FCMB Finacle Core Banking System May 2026") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", colour = "#1a5276"),
        panel.grid.minor = element_blank())
print(p4)

Longer tenor deposits attract higher rates consistent with the term premium principle in FCMB rate guide.

6.6 Plot 5 Rate Risk Concentration

Code
risk_data <- ngn |> count(rate_risk) |> mutate(pct = n / sum(n))

p5 <- ggplot(risk_data, aes(x = fct_rev(rate_risk), y = pct, fill = rate_risk)) +
  geom_col(show.legend = FALSE, alpha = 0.9, width = 0.6) +
  geom_text(aes(label = scales::percent(pct, accuracy = 0.1)), hjust = -0.2, size = 4.5) +
  scale_fill_manual(values = c(
    "Low below 10pct"      = "#1a9850",
    "Medium 10 to 14.9pct" = "#f39c12",
    "High 15pct or above"  = "#c0392b")) +
  scale_y_continuous(labels = scales::percent, limits = c(0, 0.80)) +
  coord_flip() +
  labs(title = "NGN Deposit Book Rate Risk Concentration",
       subtitle = "Proportion of accounts by contracted rate band",
       x = "Rate Risk Category",
       y = "Proportion of Accounts",
       caption = "Source: FCMB Finacle Core Banking System May 2026") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", colour = "#1a5276"),
        panel.grid.minor = element_blank())
print(p5)

Rate risk breakdown of the NGN deposit book showing proportion of accounts in each rate band.

7 Hypothesis Testing

Hypothesis Testing Chapter 6: t-test, ANOVA, non-parametric alternatives, effect sizes

7.1 Business Justification

In FCMB Treasury team, we regularly debate whether different deposit tenors or customer segments receive materially different rates. Formal hypothesis testing provides a statistically defensible answer that I can present to ALCO with a p-value and an effect size rather than an anecdote.

7.2 Hypothesis 1 NGN vs USD Interest Rates

H₀: The mean contracted interest rate is the same for NGN and USD deposits.
H₁: The mean contracted interest rate differs between NGN and USD deposits.
Code
ngn_rates <- deposits$rate[deposits$currency == "NGN"]
usd_rates <- deposits$rate[deposits$currency == "USD"]

hyp1 <- t_test(deposits, rate ~ currency, var.equal = FALSE)
hyp1 |> kbl(caption = "Welch Two-Sample t-Test: NGN vs USD Interest Rates") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Welch Two-Sample t-Test: NGN vs USD Interest Rates
.y. group1 group2 n1 n2 statistic df p
rate NGN USD 7389 725 35.35102 1005.703 0
Code
mean_ngn     <- round(mean(ngn_rates, na.rm = TRUE), 2)
mean_usd     <- round(mean(usd_rates, na.rm = TRUE), 2)
sd_pool      <- round(sd(c(ngn_rates, usd_rates), na.rm = TRUE), 2)
cohens_d_val <- round((mean_ngn - mean_usd) / sd_pool, 3)

cat("Mean NGN Rate:", mean_ngn, "%
")
Mean NGN Rate: 8.36 %
Code
cat("Mean USD Rate:", mean_usd, "%
")
Mean USD Rate: 5 %
Code
cat("Cohens d:", cohens_d_val, "(large effect if above 0.8)
")
Cohens d: 1.028 (large effect if above 0.8)
Business Interpretation: The Welch t-test confirms we reject H₀. NGN deposits carry a statistically significantly higher mean rate than USD deposits. The large Cohen d confirms this is not just statistical noise but a practically meaningful difference reflecting the currency risk premium embedded in Naira placements relative to dollar placements.

7.3 Hypothesis 2 Rates Across Tenor Groups NGN

H₀: Mean interest rates are equal across all NGN tenor groups.
H₁: At least one tenor group has a significantly different mean rate.
Code
hyp2_aov <- aov(rate ~ tenor_group, data = ngn)

tidy(hyp2_aov) |>
  mutate(across(where(is.numeric), ~ round(.x, 4))) |>
  kbl(caption = "One-Way ANOVA: Rate by Tenor Group NGN") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
One-Way ANOVA: Rate by Tenor Group NGN
term df sumsq meansq statistic p.value
tenor_group 4 3409.326 852.3314 87.7317 0
Residuals 7384 71737.054 9.7152 NA NA
Code
ngn |>
  tukey_hsd(rate ~ tenor_group) |>
  filter(p.adj < 0.05) |>
  select(group1, group2, estimate, p.adj, p.adj.signif) |>
  mutate(across(where(is.numeric), ~ round(.x, 4))) |>
  kbl(caption = "Tukey Post-Hoc: Significant Pairwise Differences") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Tukey Post-Hoc: Significant Pairwise Differences
group1 group2 estimate p.adj p.adj.signif
A 30 days or less B 31 to 60 days 1.3208 0 ****
A 30 days or less C 61 to 90 days 0.9751 0 ****
A 30 days or less D 91 to 180 days 1.4508 0 ****
A 30 days or less E 181 to 365 days 2.1068 0 ****
B 31 to 60 days E 181 to 365 days 0.7860 0 ****
C 61 to 90 days D 91 to 180 days 0.4757 0 ****
C 61 to 90 days E 181 to 365 days 1.1317 0 ****
D 91 to 180 days E 181 to 365 days 0.6560 0 ****
Code
ss_between <- summary(hyp2_aov)[[1]]["tenor_group","Sum Sq"]
ss_total   <- sum(summary(hyp2_aov)[[1]][,"Sum Sq"])
eta_sq     <- round(ss_between / ss_total, 4)
cat("Eta-squared:", eta_sq)
Eta-squared: 0.0454
Business Interpretation: Fill in after reviewing your ANOVA output. State which tenor groups differ significantly, report the eta-squared effect size, and explain what this means for FCMB tiered rate-setting policy.

7.4 Hypothesis 3 Loan Rates by Customer Type

H₀: Absolute loan rates are equal across all customer types.
H₁: At least one customer type receives a significantly different rate.
Code
loan_filtered <- loan |>
  filter(!is.na(customer_type), customer_type != "") |>
  group_by(customer_type) |>
  filter(n() >= 3) |>
  ungroup()

hyp3_kw <- kruskal_test(loan_filtered, rate_abs ~ customer_type)
hyp3_kw |> kbl(caption = "Kruskal-Wallis Test: Loan Rate by Customer Type") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Kruskal-Wallis Test: Loan Rate by Customer Type
.y. n statistic df p method
rate_abs 139 37.04535 6 1.7e-06 Kruskal-Wallis
Code
loan_filtered |>
  group_by(customer_type) |>
  summarise(Mean_Rate = round(mean(rate_abs, na.rm = TRUE), 2),
            Count = n(), .groups = "drop") |>
  kbl(caption = "Mean Loan Rate by Customer Type") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Mean Loan Rate by Customer Type
customer_type Mean_Rate Count
AGRIC 46.10 5
COR 31.23 49
IND 1.41 34
OTCY 26.34 7
PLCS 30.73 6
PRIV 15.37 3
SOLE 32.44 35
Business Interpretation: Fill in after reviewing your output. State whether customer types receive different rates and what this implies for loan pricing policy at FCMB.

8 Correlation Analysis

Correlation Analysis Chapter 8: Pearson, Spearman, Kendall, partial correlation, correlation vs causation

8.1 Business Justification

A key ALM question is whether deposit size is correlated with rate. Do larger depositors negotiate better rates? Understanding this helps Treasury set appropriate rate bands and prevent adverse selection.

8.2 Correlation Heatmap

Code
ngn_numeric <- ngn |>
  select(deposit_amount, deposit_period_days, rate, maturity_amount, clr_bal_amt) |>
  rename(Deposit_Amt = deposit_amount, Tenor_days = deposit_period_days,
         Rate_pct = rate, Maturity_Amt = maturity_amount, Balance = clr_bal_amt)

corr_mat <- cor(ngn_numeric, use = "complete.obs", method = "spearman")

corrplot(corr_mat, method = "color", type = "upper", addCoef.col = "black",
         tl.col = "#1a5276", tl.cex = 0.85, number.cex = 0.8,
         col = colorRampPalette(c("#c0392b","white","#1a5276"))(200),
         title = "Spearman Correlation: NGN Deposit Variables", mar = c(0,0,2,0))

Spearman correlation heatmap for NGN deposit numeric variables.

8.3 Formal Correlation Tests

Code
r1 <- cor.test(ngn$deposit_amount,      ngn$rate,                method = "spearman")
r2 <- cor.test(ngn$deposit_period_days, ngn$rate,                method = "spearman")
r3 <- cor.test(ngn$deposit_amount,      ngn$deposit_period_days, method = "spearman")

tibble(
  Pair         = c("Deposit Amount vs Rate","Tenor days vs Rate","Deposit Amount vs Tenor"),
  Spearman_rho = round(c(r1$estimate, r2$estimate, r3$estimate), 4),
  p_value      = round(c(r1$p.value,  r2$p.value,  r3$p.value),  4),
  Significant  = ifelse(c(r1$p.value, r2$p.value, r3$p.value) < 0.05, "Yes", "No")
) |>
  kbl(caption = "Spearman Correlation Tests: Key Variable Pairs NGN Deposits") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Spearman Correlation Tests: Key Variable Pairs NGN Deposits
Pair Spearman_rho p_value Significant
Deposit Amount vs Rate 0.5639 0 Yes
Tenor days vs Rate 0.2656 0 Yes
Deposit Amount vs Tenor -0.1261 0 Yes
Business Interpretation: Fill in after reviewing your correlation output. Discuss the 2 to 3 strongest correlations and their specific implications for FCMB deposit pricing and ALM strategy.

9 Linear Regression

Linear Regression Chapter 9: OLS coefficients, diagnostics, interpretation

9.1 Business Justification

Forecasting cashflow from the deposit book is a daily Treasury function. Modelling maturity value as a function of principal, tenor, and contracted rate produces a reproducible explainable forecast for ALCO that goes beyond a simple interest calculation.

9.2 Model Building and Comparison

Code
ngn_model_data <- ngn |>
  filter(deposit_amount > 0, maturity_amount > 0) |>
  mutate(log_deposit = log(deposit_amount), log_maturity = log(maturity_amount))

m1 <- lm(log_maturity ~ log_deposit, data = ngn_model_data)
m2 <- lm(log_maturity ~ log_deposit + deposit_period_days + rate, data = ngn_model_data)

bind_rows(
  glance(m1) |> mutate(Model = "M1 Principal only"),
  glance(m2) |> mutate(Model = "M2 Principal plus Tenor plus Rate")
) |>
  select(Model, r.squared, adj.r.squared, AIC, BIC, sigma) |>
  mutate(across(where(is.numeric), ~ round(.x, 4))) |>
  kbl(caption = "Model Comparison: Adjusted R-squared, AIC, BIC") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Model Comparison: Adjusted R-squared, AIC, BIC
Model r.squared adj.r.squared AIC BIC sigma
M1 Principal only 0.9996 0.9996 -30108.15 -30087.43 0.0315
M2 Principal plus Tenor plus Rate 1.0000 1.0000 -48308.40 -48273.86 0.0092

9.3 Model Coefficients

Code
tidy(m2, conf.int = TRUE) |>
  mutate(across(where(is.numeric), ~ round(.x, 5))) |>
  kbl(caption = "OLS Coefficients M2: Dependent Variable is log Maturity Amount") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
OLS Coefficients M2: Dependent Variable is log Maturity Amount
term estimate std.error statistic p.value conf.low conf.high
(Intercept) -0.02373 0.00107 -22.09578 0 -0.02584 -0.02163
log_deposit 0.99960 0.00008 13166.48057 0 0.99945 0.99975
deposit_period_days 0.00023 0.00000 230.00093 0 0.00023 0.00023
rate 0.00366 0.00004 92.28746 0 0.00358 0.00374

9.4 Regression Diagnostics

Code
par(mfrow = c(2, 2))
plot(m2)

Four-panel OLS diagnostic plots checking linearity, homoscedasticity, normality, and leverage.
Code
par(mfrow = c(1, 1))
Code
bp       <- bptest(m2)
vif_vals <- vif(m2)
cat("Breusch-Pagan Test H0 homoscedasticity
")
Breusch-Pagan Test H0 homoscedasticity
Code
cat("chi-sq =", round(bp$statistic, 3), "p =", round(bp$p.value, 4), "

")
chi-sq = 1874.214 p = 0 
Code
cat("Variance Inflation Factors
")
Variance Inflation Factors
Code
print(round(vif_vals, 2))
        log_deposit deposit_period_days                rate 
               1.37                1.10                1.40 
Business Interpretation: Fill in after reviewing your output. Report adjusted R-squared, interpret each coefficient in plain business language, and state whether multicollinearity or heteroscedasticity is a concern for the forecast model.

10 Integrated Findings

1
EDA
The NGN deposit book is heavily skewed. A small number of institutional placements drive the majority of total book value. Concentration risk is a structural feature of the portfolio that cannot be assessed from averages alone.
2
Visualisation
The maturity ladder reveals clustering of maturities in May to August 2026 creating significant near-term rollover risk. Treasury must prepare competitive renewal offers for this cohort or risk losing deposits at maturity.
3
Hypothesis Testing
NGN rates are statistically significantly higher than USD rates confirming the currency risk premium. Within NGN, longer-tenor deposits attract materially higher rates validating FCMB tiered rate guide.
4
Correlation
Fill in after reviewing your correlation output. State whether deposit size and tenor significantly predict rate and what that means for pricing large-ticket customers.
5
Regression
Maturity value is reliably predicted by deposit amount, tenor, and rate. This model can be embedded into FCMB liquidity cashflow tool to generate automated maturity value forecasts reducing manual calculation errors in the daily Treasury position report.
Strategic Recommendation: Treasury should implement formal segmentation of the deposit book by tenor group and ticket size to differentiate pricing strategy. Short-tenor high-rate deposits maturing May to August 2026 should be proactively managed through early-renewal incentives at revised rates aligned to the current CBN MPR environment.

11 Limitations and Further Work

  • Single-day snapshot: This analysis uses one extract from 9 May 2026. Monthly extracts over 12 to 24 months would enable trend analysis of rate movements and book growth.

  • Incomplete loan data: The MATURITY_AMOUNT column in the Loan sheet was entirely blank in the source system preventing yield-to-maturity calculations for the loan portfolio.

  • Rate sign convention: Loan rates are stored as negative values in Finacle and were corrected by taking absolute values.

  • No customer demographics: Segmentation beyond CUSTOMER TYPE is limited. Access to the CRM system would enable analysis by industry, relationship tenure, and geography.

  • Future extension: A time-series ARIMA model on monthly deposit volumes would produce liquidity forecasts with prediction intervals which would be a direct input to the FCMB ILAAP process.


12 References

Enubuogidi F I (2026). FCMB active term deposit and loan portfolio May 2026 extract [Dataset]. Treasury Department, First City Monument Bank Limited, Lagos, Nigeria. Available on request from the author.

Wickham H and Grolemund G (2017). R for data science. O’Reilly Media. https://r4ds.had.co.nz

Wickham H (2016). ggplot2: Elegant graphics for data analysis. Springer. https://doi.org/10.1007/978-3-319-24277-4

Allaire J J Teague C Scheidegger C Xie Y and Dervieux C (2022). Quarto [Computer software]. https://doi.org/10.5281/zenodo.5960048


13 Appendix: AI Usage Statement

AI tools specifically Claude by Anthropic assisted in the initial structuring of this Quarto document including generating the YAML header, section scaffolding, and code chunk templates. All analytical decisions including the choice of techniques, the framing of hypotheses, the interpretation of outputs, and the business recommendations are the author own independent judgement based on 10 years of Treasury experience at FCMB. Every line of code was reviewed and adapted to fit the specific dataset and business context. The author is able to explain and defend every result in the viva voce.