0% found this document useful (0 votes)
3 views48 pages

Business Cycle Analysis R Code Explained

Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views48 pages

Business Cycle Analysis R Code Explained

Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Business Cycle Analysis in R

A beginner-friendly but rigorous explanation of the uploaded spectral-cycle


script

Source script
This report explains the file structuralCycleAnalysis.R, which implements a spectral cycle extraction
and expansion/contraction detection workflow in R. The code uses HP or linear detrending, a
periodogram, a Butterworth or FFT bandpass filter, and ggplot outputs.

Prepared as a 30+ page technical walkthrough. The goal is to help a beginner understand what every
major line is doing, while also warning about methodological and coding limitations that matter for
serious time-series work.

Date: 6 May 2026

Business cycle analysis R code - beginner explanation Page 1 of 48


How this guide is organised
The document follows the same order as the R script. It first explains the economic objective, then the
input parameters, then the seven internal stages of the function, and finally the example section. The
last part gives caveats, corrections, a safer code template, a glossary, and an annotated code
appendix.
1. What the code is trying to do

2. Business-cycle language in plain English

3. The packages and setup block

4. The function signature and inputs

5. Frequency, dates, and observation units

6. Detrending: why the trend must be removed

7. The HP filter branch

8. The linear trend branch

9. Spectral density and the periodogram

10. From frequency to cycle length

11. Selecting the dominant period

12. Designing the bandpass window

13. Butterworth filtering and filtfilt

14. FFT fallback filtering

15. Building the output data set

16. Expansion and contraction classification

17. Spell summaries and the duration issue

18. Peak and trough detection

19. The three ggplot outputs

20. What the returned object contains

21. The non-food credit example

22. The simulated-data block

23. How to interpret the results

24. Diagnostics before trusting the answer

25. Economic checks for Indian credit data

26. Limitations and risks

27. Corrections I recommend

28. A safer revised code skeleton

29. Beginner glossary

30. Practical checklist

31. Annotated code appendix

32. References

Business cycle analysis R code - beginner explanation Page 2 of 48


Central warning
In this script, an expansion means the extracted cycle is above zero, meaning the observed series
is above its estimated trend after filtering. A contraction means the extracted cycle is zero or
below. This is not automatically the same as an official recession, a decline in GDP, or negative
growth.

Business cycle analysis R code - beginner explanation Page 3 of 48


1. What the code is trying to do
The uploaded R script is a business-cycle extraction routine. It takes a single numeric time series - for
example, monthly non-food credit to industry - and tries to separate the short-to-medium-term
cyclical movement from the longer-term trend. After reconstructing a cyclical component, it labels
dates as expansion or contraction and creates plots to show the results.

In macroeconomics and applied finance, this kind of work is useful when the raw series has a strong
trend. Credit, output, prices, tax collections, deposits, and many other economic series often grow
over time. If we look only at the level, we may confuse long-run growth with cyclical acceleration and
deceleration. The code therefore asks: after removing the trend, is the series above or below its
normal path, and what cycle length seems to dominate?

The important point is that this script uses a mechanical statistical definition. It does not know the
institutional details of the Indian economy. It does not know policy changes, pandemic disruptions,
reporting breaks, banking-sector reforms, or structural breaks unless these show up numerically in
the series. For that reason, the output should be read as an empirical diagnostic, not as a final
economic narrative.

Beginner translation
The script tries to answer: where is the series relative to its estimated normal path, and does it
seem to move in a repeated wave-like pattern?

- Input: one numeric vector x and an optional date vector.

- Core output: trend, reconstructed cycle, expansion/contraction spells, peaks and troughs, and plots.

- Main methods: HP or linear detrending, spectral peak selection, and narrow bandpass filtering.

- Main risk: a clean-looking cycle can still be misleading if the data are seasonal, non-stationary in the
wrong way, structurally broken, or measured in nominal levels.

Business cycle analysis R code - beginner explanation Page 4 of 48


2. Business-cycle language in plain English
A business cycle is a repeated pattern of expansion and slowdown around a longer-run path. In
real-world macroeconomics, cycles are irregular. They do not arrive exactly every 36 months or 48
months. The script simplifies this reality by searching for a dominant frequency within a user-chosen
range, such as 3 to 5 years.

A trend is the slow-moving component of a series. For example, if bank credit rises for many years
because the economy is larger, inflation is higher, and banking penetration improves, that persistent
rise is mostly trend. A cycle is the temporary deviation around that trend. If credit is higher than the
trend, the extracted cycle is positive. If credit is lower than the trend, the extracted cycle is negative.

The code uses the zero line of the extracted cycle as the dividing line between expansion and
contraction. This is simple and transparent, but it must be interpreted carefully. A negative cycle does
not necessarily mean the original series is falling; it may mean the series is growing more slowly than
its estimated trend.

Term Plain-English meaning in this script Important caution

Trend The smooth, long-run path of the series. Different trend filters can give different
trends.

Cycle The temporary movement after removing It is an estimated object, not directly
trend and filtering for a chosen frequency observed data.
band.

Expansion Cycle is above zero. Not necessarily positive growth or an


official expansion.

Contraction Cycle is zero or below. Not necessarily a recession.

Peak A local high point in the cycle. Small noisy wiggles can create too
many peaks.

Trough A local low point in the cycle. Needs economic judgement and
censoring rules.

Business cycle analysis R code - beginner explanation Page 5 of 48


3. The packages and setup block
The first block defines the R packages the script needs. It checks which packages are missing and
installs them. Then it loads the libraries into the R session. This makes the script convenient for a first
run, because the user does not need to install each package manually.

For serious research work, automatic installation inside an analysis script has disadvantages. It makes
the script less reproducible because package versions can change over time. A better research
practice is to record package versions using a project environment, for example with renv, or to put
installation commands in a separate setup script.

Each package has a specific role. mFilter supplies the HP filter. signal supplies the Butterworth filter
and filtfilt. tidyverse supplies tibbles, dplyr verbs, and ggplot. pracma and zoo are loaded, but in the
current script they are not actually used.

Package Used for in this script Comment

mFilter hpfilter() Essential if detrend = "hp".

signal butter(), filtfilt() Essential for the main bandpass filter.

pracma hilbert if needed Loaded but not used in the current


code.

zoo Time-series utilities Loaded but not used in the current


code.

tidyverse tibble, mutate, group_by, summarize, ggplot Essential for data handling and plots.

# Spectral-cycle + expansion/contraction detection in R


# Requires: mFilter, signal, pracma, zoo, tidyverse
# Install missing packages automatically (only if needed)
pkgs <- c("mFilter","signal","pracma","zoo","tidyverse")
to_install <- pkgs[!pkgs %in% [Link]()[,"Package"]]
if(length(to_install)) [Link](to_install, dependencies = TRUE)

library(mFilter) # hp filter
library(signal) # Butterworth and filtfilt
library(pracma) # hilbert (if needed)
library(zoo)
library(tidyverse)

Business cycle analysis R code - beginner explanation Page 6 of 48


4. The function signature and inputs
The main function is called spectral_cycle. It is designed to be reusable: you pass a numeric series, a
date vector, and settings such as monthly or quarterly frequency, the cycle length you want to search
over, and whether you want HP or linear detrending.

The argument x is the core data. It should be a clean numeric vector with no missing values. The
function does not currently check for missing values, non-numeric values, duplicated dates,
seasonality, outliers, or data breaks. That means data preparation must happen before this function is
called.

The arguments cyc_min and cyc_max define the search window. If cyc_min = 3 and cyc_max = 5, the
code searches for a cycle whose full wave length lies between 3 and 5 years. A full wave means
peak-to-peak or trough-to-trough, not the length of one expansion phase only.

Argument Meaning Typical value

x Numeric observations of the series. Credit, IIP, GDP, etc.

dates Dates attached to x. Monthly Date vector.

freq Number of observations per year. 12 monthly, 4 quarterly.

cyc_min, cyc_max Shortest and longest cycle period to search for, in 3 and 5 in the NFC example.
years.

detrend Method used to remove trend. "hp" or "linear".

butter_order Order of the Butterworth filter. 4 in this script.

bandwidth_pct Width around the dominant period. 0.25 means plus/minus 25


percent.

spec_taper Taper used in periodogram estimation. 0.1.

spectral_cycle <- function(x, dates = NULL, freq = 12,


cyc_min = 2, cyc_max = 8, # cycle bounds in years
detrend = c("hp","linear"),
butter_order = 4,
bandwidth_pct = 0.25, # +/- pct around dominant period
spec_taper = 0.1) {
# x: numeric vector (observations)
# dates: vector of Date or POSIX or character convertible to Date; length same as x
# freq: observations per year (12 monthly, 4 quarterly, 1 yearly)
# cyc_min, cyc_max: min and max cycle period (in years) to search for peak
# returns: list with cycle series, dominant_period (years), data frame of spells, and plots

Business cycle analysis R code - beginner explanation Page 7 of 48


5. Frequency, dates, and observation units
The frequency argument is crucial. It tells the function how many observations occur in one year. For
monthly data, freq = 12. For quarterly data, freq = 4. This matters because spectral frequencies are
initially measured per observation, and the code converts them into cycles per year by multiplying by
freq.

If the user does not supply dates, the function creates artificial dates beginning on 1 January 2000.
For monthly data, this will create a monthly sequence. For quarterly data, round(12/freq) becomes 3,
so the sequence advances every 3 months. For annual data, it advances every 12 months.

If the user supplies dates, the code converts them to Date format and checks that the length of dates
equals the length of x. This length check is good. However, it does not check whether the dates are
actually equally spaced. Spectral methods assume regular spacing, so this should be checked before
using the function.

Rule
Before running spectral analysis, confirm that the data are clean, regularly spaced, and have the
same length as the date vector.

- A monthly series with missing months is dangerous unless missing months are explicitly handled.

- A quarterly series should have one observation per quarter with no gaps.

- Dates must match the data after any trimming, such as dropping the first 12 rows.

- The function treats the series as regularly sampled even if the supplied dates are irregular.

n <- length(x)
if([Link](dates)) {
dates <- [Link](from = [Link]("2000-01-01"), by = paste0(round(12/freq)," months"),
[Link] = n)
} else {
dates <- [Link](dates)
}
stopifnot(length(dates) == n)

# 1. Detrend (HP or linear)


if(detrend == "hp") {

Business cycle analysis R code - beginner explanation Page 8 of 48


6. Detrending: why the trend must be removed
The first major operation is detrending. Detrending means removing a slow-moving component so
that the remaining data are closer to a stationary cyclical component. In a trending macroeconomic
series, spectral analysis on the raw level can be dominated by very low frequencies. That would make
the code pick up long-run growth rather than a business cycle.

The script allows two detrending methods: HP filtering and a simple linear time trend. HP filtering is
flexible because the trend can bend over time. Linear detrending is simpler and assumes the trend is
a straight line. The choice matters. Two different trend methods can produce different cycles, spell
dates, and turning points.

A beginner should think of detrending as drawing a smooth baseline through the data and then
studying the wiggles around that baseline. But the baseline is not a fact. It is estimated. Therefore,
the cycle is also estimated.

Key idea
The cycle is calculated as observed series minus trend, then further filtered around a selected
frequency band.

Choice What it assumes When it may be reasonable Risk

HP trend Trend is smooth but can Macro series with changing Endpoint bias and
bend. growth rates. sensitivity to lambda.

Linear trend Trend is a straight line. Short samples or simple Too rigid for many
teaching examples. economic series.

Business cycle analysis R code - beginner explanation Page 9 of 48


7. The HP filter branch
When detrend = "hp", the code uses the Hodrick-Prescott filter through hpfilter(). The HP filter
decomposes a series into a smooth trend and a residual cycle. The smoothing parameter, called
lambda, controls how smooth the trend is. A larger lambda makes the trend smoother and pushes
more movement into the cycle.

The code uses common macroeconomic choices: lambda = 1600 for quarterly data and lambda =
129600 for monthly data. More generally it applies the Ravn-Uhlig scaling rule, 1600 times
(freq/4)^4. This is a standard frequency adjustment used to adapt quarterly lambda to other
sampling frequencies.

After estimating the HP trend, the script computes cycle_input = x - trend. This is the detrended
series that will be passed into the spectral analysis. Notice that this HP cycle is not yet the final cycle.
The final cycle is reconstructed later with a bandpass filter around the dominant period.

- HP filtering is popular because it is easy to apply and gives a smooth trend.

- It can be sensitive at the beginning and end of the sample, which matters when interpreting recent
observations.

- It can create cycles mechanically even when the underlying data-generating process is not truly
cyclical.

- For credit data, using nominal levels instead of logs or real values may change the meaning of the
extracted cycle.

hp <- hpfilter(x, freq = lambda, type = "lambda")


trend <- hp$trend
cycle_input <- x - trend
} else {
fit <- lm(x ~ seq_along(x))
trend <- fitted(fit)
cycle_input <- residuals(fit)
}

Business cycle analysis R code - beginner explanation Page 10 of 48


8. The linear trend branch
When detrend = "linear", the code fits an ordinary least squares regression of x on a simple time
index: 1, 2, 3, and so on. The fitted values are treated as the trend. The residuals are treated as the
detrended input.

This is very easy to understand. Imagine drawing the best straight line through the series. The
residual is the vertical distance between the observed value and that straight line. Positive residuals
mean the observation is above the line; negative residuals mean it is below the line.

The disadvantage is that many economic series do not follow a straight-line trend. Credit can
accelerate, slow down, or be affected by structural reforms. A linear trend may mistake changes in
long-run growth for cycles. The method is therefore useful as a comparison case, but should not be
accepted blindly.

Line of code Plain-language explanation

fit <- lm(x ~ seq_along(x)) Fit a straight-line trend against time.

trend <- fitted(fit) Save the fitted straight-line values as the trend.

cycle_input <- residuals(fit) Save the deviations from that line as the detrended input.

# 2. Spectral density estimate to find dominant frequency in target band


# [Link] expects a ts or numeric; we'll use [Link]
sp <- [Link](cycle_input, taper = spec_taper, detrend = TRUE, log = "no", plot = FALSE)
freqs <- sp$freq # frequency in cycles per unit (unit = observation frequency)

Business cycle analysis R code - beginner explanation Page 11 of 48


9. Spectral density and the periodogram
After detrending, the script estimates the spectrum of the detrended series using [Link](). A
spectrum tells us how much variation in a time series is associated with different frequencies. A low
frequency corresponds to a slow cycle; a high frequency corresponds to a fast cycle.

A periodogram is a simple estimate of the spectral density. In beginner language, it asks: if the series
is made up of many waves of different lengths, which wave lengths seem to explain more of the
movement? The script then restricts attention to cycle lengths chosen by the user, such as 3 to 5
years.

The argument taper = spec_taper applies tapering before estimating the spectrum. Tapering gently
reduces the weight of observations near the ends of the sample to reduce spectral leakage. The
argument detrend = TRUE in [Link] also removes a linear trend before spectrum estimation.
Because the series was already detrended, this is an extra precaution, but it may slightly change the
spectral estimate.

Important distinction
The periodogram is used to choose the dominant cycle length. The final cycle is then reconstructed
using a bandpass filter.

cy_per_year <- freqs * freq


# Allowed band in cycles per year from cyc_min..cyc_max (period in years)
allowed <- (1/cyc_max) <= cy_per_year & cy_per_year <= (1/cyc_min)
if(!any(allowed)) stop("No spectral power in requested cycle band. Expand cyc_min/cyc_max.")

# pick max within allowed band


idx_dom <- [Link](sp$spec * allowed) # multiply by logical to zero out others

Business cycle analysis R code - beginner explanation Page 12 of 48


10. From frequency to cycle length
The object sp$freq contains frequencies in cycles per observation. If the data are monthly, one
observation is one month. A frequency must therefore be converted into cycles per year to be
economically interpretable. The script does this by multiplying by freq, where freq is the number of
observations per year.

For example, if a monthly series has a spectral frequency of 0.02 cycles per month, multiplying by 12
gives 0.24 cycles per year. A frequency of 0.24 cycles per year means the period is 1 / 0.24 = about
4.17 years. That is the full length of the wave.

The code then builds the allowed search band. If cyc_min = 3 and cyc_max = 5, the period range is 3
to 5 years. In frequency terms, that becomes 1/5 to 1/3 cycles per year. The conversion is inverse
because long periods correspond to low frequencies.

Period in years Frequency in cycles per year Interpretation

3 years 1/3 = 0.333 One full wave every 3 years.

4 years 1/4 = 0.250 One full wave every 4 years.

5 years 1/5 = 0.200 One full wave every 5 years.

dom_freq_year <- cy_per_year[idx_dom] # cycles per year


dom_period_years <- 1 / dom_freq_year # years per cycle

# 3. Bandpass design around the identified dominant frequency


# define lower/upper period bounds as +/- bandwidth_pct

Business cycle analysis R code - beginner explanation Page 13 of 48


11. Selecting the dominant period
The script chooses the frequency with the highest spectral power inside the allowed band. This is the
empirical centre of the cycle extraction. If the strongest spectral peak between 3 and 5 years is near
4 years, the dominant period is reported as about 4 years.

The line idx_dom <- [Link](sp$spec * allowed) works by multiplying spectral values outside the
allowed band by zero. Because spectral density values are non-negative, this usually makes the
maximum inside the allowed band win. A cleaner and safer implementation would subset first:
allowed_idx <- which(allowed); idx_dom <- allowed_idx[[Link](sp$spec[allowed])].

The dominant period is calculated as 1 divided by the frequency in cycles per year. This is a core
spectral identity: frequency tells how many waves occur per year; period tells how many years one
wave takes.

- The selected period depends on the chosen cyc_min and cyc_max range.

- A short sample gives poor frequency resolution, so the selected period can be unstable.

- Outliers and breaks can create misleading spectral peaks.

- You should test sensitivity to different bands, such as 2-8 years and 3-7 years.

upper_period <- dom_period_years * (1 + bandwidth_pct)


# convert to normalized frequency for butter (Nyquist = 0.5 * sampling_rate)
# sampling_rate in observations per year = freq
nyq <- 0.5 * freq
# convert period (years) -> freq (cycles per year)

Business cycle analysis R code - beginner explanation Page 14 of 48


12. Designing the bandpass window
Once the dominant period is found, the script builds a band around it. With bandwidth_pct = 0.25, it
keeps periods roughly within plus or minus 25 percent of the dominant period. If the dominant period
is 4 years, the lower period is 3 years and the upper period is 5 years.

The names lower_period and upper_period refer to the lower and upper period length. But frequency
moves in the opposite direction. The lower frequency is 1 / upper_period, while the upper frequency is
1 / lower_period. This inverse relation is a common source of confusion for beginners.

The code then normalises these frequencies for the signal::butter() function. In digital filtering,
normalised frequency is measured relative to the Nyquist frequency, which is half the sampling rate.
For monthly data, the sampling rate is 12 observations per year, so the Nyquist frequency is 6 cycles
per year.

Object Meaning

lower_period Dominant period times (1 - bandwidth_pct). This is the shorter period


boundary.

upper_period Dominant period times (1 + bandwidth_pct). This is the longer period


boundary.

low_f_cpy Lower frequency boundary, equal to 1 / upper_period.

high_f_cpy Upper frequency boundary, equal to 1 / lower_period.

low_norm, high_norm Frequency boundaries divided by the Nyquist frequency for butter().

high_f_cpy <- 1 / lower_period # upper frequency


# normalized (0..1) where 1 -> Nyquist
low_norm <- low_f_cpy / nyq
high_norm <- high_f_cpy / nyq
if(low_norm <= 0) low_norm <- 1e-6
if(high_norm >= 1) high_norm <- 0.9999
if(low_norm >= high_norm) {
warning("Computed filter band invalid; falling back to fft bandpass.")
use_butter <- FALSE
} else {
use_butter <- TRUE
}

# 4. Filter the detrended series to reconstruct the cycle


if(use_butter) {
bf <- butter(butter_order, c(low_norm, high_norm), type = "pass")

Business cycle analysis R code - beginner explanation Page 15 of 48


13. Butterworth filtering and filtfilt
If the computed normalised frequency range is valid, the script designs a Butterworth bandpass filter.
A bandpass filter removes movements that are too slow and movements that are too fast, retaining
only oscillations in the desired frequency band. Here, the desired band is centred around the
dominant period found from the periodogram.

The Butterworth filter is a smooth filter. The butter_order parameter controls how sharp the filter is. A
higher order makes the passband sharper, but may also introduce more sensitivity. The script uses
order 4, a common moderate choice.

The code applies filtfilt rather than a one-sided filter. filtfilt applies filtering forward and backward,
which removes phase shift. Phase shift matters because a one-sided filter can move peaks and
troughs in time. For dating cycles, moving a peak by even a few months can be a serious problem.

- Benefit: filtfilt avoids phase distortion, so timing is more credible than with a one-pass filter.

- Risk: filtering can create endpoint artefacts, especially at the start and end of the sample.

- Risk: a narrow band can make the cycle look smoother and more regular than the true economy.

- Research practice: check whether turning points remain similar under alternative filters.

# fallback: simple FFT bandpass


X <- fft(cycle_input)
freqs_fft <- (0:(n-1)) / n * freq # cycles per year associated with each FFT bin
# create mask for allowed frequencies up to Nyquist
mask <- rep(0, n)
# handle symmetry
allowed_idx <- which((freqs_fft >= low_f_cpy & freqs_fft <= high_f_cpy) |
(freqs_fft >= (freq - high_f_cpy) & freqs_fft <= (freq - low_f_cpy)))

Business cycle analysis R code - beginner explanation Page 16 of 48


14. FFT fallback filtering
If the Butterworth frequency band is invalid, the script falls back to a simple FFT bandpass filter. FFT
means Fast Fourier Transform. It converts the time series from the time domain into the frequency
domain, zeroes out frequencies outside the desired band, and then converts the filtered
frequency-domain object back into a time-domain series.

The code creates a vector called mask. Frequencies inside the allowed band receive mask value 1,
while frequencies outside receive 0. Multiplying the FFT coefficients by this mask removes unwanted
frequencies. The inverse FFT then reconstructs a filtered cycle.

This fallback is useful but fairly crude. A hard frequency cutoff can create ringing artefacts, and the
treatment of endpoints can be delicate. In practice, if the Butterworth band is invalid, one should first
ask why the requested band is invalid. It may indicate a bad parameter choice or too low a sampling
frequency.

Beginner analogy
The FFT fallback is like decomposing music into notes, muting all notes outside a chosen range, and
reconstructing the music from the remaining notes.

X_filtered <- X * mask


cycle_recon <- Re(fft(X_filtered, inverse = TRUE) / n)
}

# 5. Identify expansion (cycle_recon > 0) and contraction (<=0) spells


sign_series <- ifelse(cycle_recon > 0, 1, 0) # 1 = expansion, 0 = contraction
# find spell boundaries
df <- tibble(date = dates, observed = x, cycle = cycle_recon, trend = trend, expansion =
sign_series)
df <- df %>% mutate(exp_shift = lag(expansion, default = first(expansion)))
# start when expansion != previous
df <- df %>%
mutate(change = expansion != exp_shift,
spell_id = cumsum(change))
# summarize spells

Business cycle analysis R code - beginner explanation Page 17 of 48


15. Building the output data set
After reconstructing the cycle, the script creates a tidy data frame called df. This data frame contains
the date, original observed series, reconstructed cycle, estimated trend, and expansion indicator. This
is the main object needed for plotting and later analysis.

The expansion indicator is created using ifelse(cycle_recon > 0, 1, 0). If the cycle is positive, the
observation is labelled 1. Otherwise it is labelled 0. This is mathematically simple and transparent.

Because this classification is based on the filtered cycle, any issue in detrending or filtering will feed
directly into the spell chronology. The labels should not be treated as independent facts. They are
consequences of the full chain of modelling choices.

Column Meaning

date Date attached to each observation.

observed Original input series x.

cycle Reconstructed bandpass cycle.

trend Trend estimated by HP or linear detrending.

expansion 1 when cycle > 0; 0 otherwise.

change TRUE when the expansion state changes from previous observation.

spell_id Cumulative count of state changes, used to group spells.

summarize(start = first(date), end = last(date),


duration = [Link](end - start) + 1,
start_index = first(row_number()),
end_index = last(row_number()), .groups = "drop") %>%
arrange(start)
spells <- spells %>%
mutate(type = ifelse(expansion==1, "Expansion", "Contraction")) %>%

Business cycle analysis R code - beginner explanation Page 18 of 48


16. Expansion and contraction classification
The code defines expansion mechanically as cycle_recon > 0. This means the cyclical component is
above its own zero line. In many macroeconomic applications, that can be interpreted as above-trend
activity. Contraction is cycle_recon <= 0, which means below-trend activity.

This approach is common in exploratory cycle analysis, but it is not the same as recession dating.
Official business-cycle dating often considers multiple indicators, depth, diffusion, and duration. This
script considers only one series and one zero threshold.

For credit data, the interpretation should be even more careful. Credit may remain positive and rising
even when the extracted cycle is negative. A negative credit cycle means credit is below its
estimated cyclical norm, not necessarily that credit outstanding is shrinking.

Suggested wording
For presentations, say: the extracted credit cycle is above/below zero, rather than saying the
economy is in expansion/contraction.

- Expansion in this script: above-trend filtered component.

- Contraction in this script: at-trend or below-trend filtered component.

- Not captured: severity, breadth across sectors, policy causes, or official recession criteria.

- Recommended label for plots: "Above-trend phase" and "Below-trend phase" may be clearer than
expansion and contraction.

Business cycle analysis R code - beginner explanation Page 19 of 48


17. Spell summaries and the duration issue
The script groups consecutive expansion and contraction observations into spells. It does this by
comparing each observation to the previous state, marking a change when the state flips, and using
cumsum(change) to create spell identifiers. This is a clever and common tidyverse pattern.

However, there is an important bug or at least a serious interpretation issue. The duration is
calculated as [Link](end - start) + 1. Because start and end are Date objects, this computes
calendar days, not the number of monthly or quarterly observations. For monthly data, a spell from 1
January to 1 March is counted as about 60 days, not 3 observations.

If the intended duration is the number of months or quarters, the code should use n() inside
summarize. A better output would report both observation_count and approximate duration_years =
observation_count / freq. This is one of the most important corrections to make before using the spell
table in a report.

Correction
Replace the duration calculation with duration_obs = n() and duration_years = duration_obs
/ freq. Do not interpret the current duration column as months.

select(type, start, end, duration)

# 6. Also find cycle turning points (peaks & troughs) using local maxima/minima
# use simple sign of first difference of cycle to find local maxima/minima
dm <- diff(sign(diff(df$cycle))) # 2nd difference sign trick
# local max: dm == -1, local min: dm == 1 (positions shifted)
peaks_idx <- which(dm == -1) + 1
troughs_idx <- which(dm == 1) + 1
peaks <- tibble(type = "Peak", date = df$date[peaks_idx], value = df$cycle[peaks_idx])
troughs <- tibble(type = "Trough", date = df$date[troughs_idx], value = df$cycle[troughs_idx])
turns <- bind_rows(peaks, troughs) %>% arrange(date)

# 7. Plots
p1 <- ggplot(df, aes(x = date)) +

Business cycle analysis R code - beginner explanation Page 20 of 48


18. Peak and trough detection
The script detects turning points using the sign of first differences. First it computes diff(df$cycle),
which tells whether the cycle is rising or falling between adjacent observations. Then sign() converts
these differences into positive, zero, or negative movement. A second diff() identifies changes from
rising to falling or falling to rising.

When dm == -1, the series has moved from rising to falling, so the middle point is a local peak. When
dm == 1, the series has moved from falling to rising, so the middle point is a local trough. The +1
adjusts the index because differencing shortens the vector.

This method is simple, but it can find too many turning points if the cycle is noisy or flat. A serious
business-cycle dating routine usually adds censoring rules: minimum phase length, minimum
complete-cycle length, and sometimes an amplitude threshold. Without these rules, tiny wiggles can
become labelled peaks and troughs.

- Good feature: easy to understand and transparent.

- Weakness: no minimum duration rule between peak and trough.

- Weakness: flat segments and small numerical changes can cause ambiguous signs.

- Improvement: combine local extrema with a Bry-Boschan style censoring rule or use domain-specific
thresholds.

labs(title = "Observed series and trend", y = "Observed") +


theme_minimal()

p2 <- ggplot(df, aes(x = date)) +


geom_line(aes(y = cycle), size = 0.8) +
geom_hline(yintercept = 0, color = "black", linetype = "dotted") +
labs(title = paste0("Reconstructed cycle (dominant period = ",
round(dom_period_years,2)," years)"),

Business cycle analysis R code - beginner explanation Page 21 of 48


19. The three ggplot outputs
The function creates three ggplot objects. It does not save them to disk; it returns them in a list. The
user later prints them to display the charts. This is a good design because the user can customise the
plots after receiving them.

The first plot shows the observed series and the estimated trend. This is the most important
diagnostic plot. Before interpreting the cycle, you should ask whether the trend looks plausible. If the
trend is implausible, the cycle will also be implausible.

The second plot shows the reconstructed cycle with a zero line. This plot is useful for seeing the
timing and amplitude of the cycle. The third plot shows an area chart with fill determined by
expansion state. It visually separates positive and negative phases.

Plot object What it shows Diagnostic question

series_trend Observed series and estimated trend. Does the trend look economically plausible?

cycle Reconstructed cycle and zero line. Are the cycles regular, too smooth, or too
noisy?

cycle_state Area chart with expansion state. Do above/below trend phases make sense?

p3 <- ggplot(df, aes(x = date, y = cycle, fill = factor(expansion))) +


geom_area(alpha = 0.4) +
geom_hline(yintercept = 0, linetype = "dotted") +
labs(title = "Cycle with expansion (above 0) and contraction (below 0)",
fill = "State") +
theme_minimal()

return(list(
data = df,
spells = spells,
turns = turns,
dominant_period_years = dom_period_years,
dominant_freq_per_year = dom_freq_year,
plots = list(series_trend = p1, cycle = p2, cycle_state = p3)
))
}

# -----------------------
# Example usage with simulated monthly series (trend + cycle + noise)
[Link](123)
n <- 220
dates <- seq([Link]("2007-01-01"), by = "month", [Link] = n)

Business cycle analysis R code - beginner explanation Page 22 of 48


20. What the returned object contains
At the end of the function, the code returns a list. This is common in R when a function needs to
return several related outputs. The list contains the data frame, spell table, turning-point table,
dominant period, dominant frequency, and the three plots.

A beginner can access the components using the dollar sign. For example, res$data gives the full
data frame, res$spells gives the spell table, and res$plots$cycle gives the cycle plot. This design
keeps the analysis compact and reusable.

For research use, it would be helpful to also return the filter settings, the allowed spectral band, the
lower and upper filter frequencies, the detrending method, lambda, and maybe the spectrum object.
That would make the output more auditable and easier to reproduce.

Component Use

data Main time-series data set with observed, trend, cycle, and state.

spells Expansion and contraction spell table. Duration currently needs


correction.

turns Peak and trough dates and values.

dominant_period_years Estimated full cycle length in years.

dominant_freq_per_year Estimated frequency in cycles per year.

plots List of ggplot objects.

x_sim <- [Link]("../DATA/[Link]", sep = "|", skip = 1)


x_sim <- x_sim[-1:-12, 3]/1e6
res <- spectral_cycle(x = x_sim, dates = dates, freq = 12,
cyc_min = 3, cyc_max = 5,
detrend = "hp")

# View results
print(paste("Dominant period (years):", round(res$dominant_period_years,3)))

Business cycle analysis R code - beginner explanation Page 23 of 48


21. The non-food credit example
After defining the function, the script sets a seed, creates n = 220, and builds monthly dates starting
from January 2007. Then it reads a data file called ../DATA/[Link] using a pipe separator and skipping
the first row. The code then takes column 3, removes the first 12 rows, and divides by 1e6.

The comment says this is non-food credit in the industrial sector. The call to spectral_cycle uses
monthly frequency, searches for a 3-to-5-year cycle, and uses HP detrending. It then prints the
dominant period, spells, and turning points, and displays the plots.

There are two practical concerns. First, the date vector is fixed at length 220, but the imported data
after dropping 12 rows may not have length 220. If the lengths differ, stopifnot(length(dates) == n)
inside the function will fail. Second, the expression x_sim[-1:-12, 3] works in R as a negative
sequence, but -(1:12) is clearer and safer for readers.

- [Link](..., sep = "|", skip = 1) means the file is pipe-delimited and the first line is skipped.

- x_sim[-1:-12, 3] drops rows 1 to 12 and selects the third column.

- Dividing by 1e6 rescales the series, probably to millions or a larger reporting unit.

- The code assumes the dates and the cleaned series have identical length.

print("Turning points (first 6):")


print(res$turns)

# Display plots
print(res$plots$series_trend)
print(res$plots$cycle)
print(res$plots$cycle_state)

trend <- 0.05 * (1:n) # linear-ish


true_cycle <- 2.5 * sin(2*pi*(1/n)*(1:n) * (n/(2.5*12))) # roughly 2.5-year cycle
# simpler: build a cycle with period 2.5 years = 30 months
period_months <- 2.5 * 12
t <- 1:n
true_cycle <- 1.5 * sin(2*pi * t / period_months)
x_sim <- trend + true_cycle + rnorm(n, sd = 0.8)

#### Find Structural Breaks in the NFC

Business cycle analysis R code - beginner explanation Page 24 of 48


22. The simulated-data block
At the end of the script, there is a block that creates a linear trend, a sine-wave cycle, and random
noise. This is a standard way to simulate a time series for testing. However, in the current script this
simulated x_sim is created after the real-data analysis and is not passed again into spectral_cycle.

This creates a small mismatch between the comment and the actual execution. The comment says
"Example usage with simulated monthly series", but the active example reads non-food credit data.
Then the simulation is constructed only after the plots from the real-data run. A beginner might think
the plots are based on the simulation, but they are not.

If the purpose is to teach or test the function, the simulated-data block should be placed before a
second function call, such as res_sim <- spectral_cycle(x_sim, dates = dates, freq = 12, cyc_min = 2,
cyc_max = 4). Then one can check whether the function recovers the known 2.5-year cycle.

Recommended teaching use


First simulate a series where the true cycle is known. Run the function on it. Only after that, move
to real data where the true cycle is unknown.

Business cycle analysis R code - beginner explanation Page 25 of 48


23. How to interpret the results
The printed dominant period tells you the estimated full cycle length. If the output says 4.1 years, it
means the strongest spectral component within the selected band corresponds to a wave that takes
about 4.1 years from peak to peak. It does not mean expansions last 4.1 years. A complete cycle
includes both expansion and contraction phases.

The spells table tells you consecutive periods for which the filtered cycle is positive or non-positive.
Because the current duration field is in days, you should not report it as months. After correcting it to
observation counts, you can interpret spell length in months or quarters depending on the data
frequency.

The turning-points table gives local peaks and troughs of the cycle. You should compare these dates
with known economic events: credit booms, policy tightening, demonetisation, COVID-19,
banking-sector stress, changes in RBI classification, or data revisions. Statistical dates become
meaningful only when interpreted economically.

Output Correct interpretation Incorrect interpretation

dominant_period_years Full wave length of dominant cycle in Length of each expansion.


selected band.

cycle > 0 Above estimated cyclical zero line. The economy is definitely expanding.

cycle <= 0 At or below estimated cyclical zero line. The original series is falling.

turns Local extrema in the filtered cycle. Official peak and trough dates.

Business cycle analysis R code - beginner explanation Page 26 of 48


24. Diagnostics before trusting the answer
A rigorous time-series workflow should not stop after producing a nice plot. The researcher should run
diagnostics. The first diagnostic is data integrity: no missing observations, no duplicated dates, no
irregular intervals, and no obvious input error. The second diagnostic is transformation: decide
whether the series should be logged, deflated, seasonally adjusted, or converted to growth rates.

The third diagnostic is sensitivity. Rerun the function with different detrending choices, different cycle
bands, and different bandwidth percentages. If the turning points move dramatically, the chronology
is not robust. The fourth diagnostic is endpoint sensitivity: be especially cautious about the last few
observations because filters often behave poorly near sample endpoints.

The fifth diagnostic is economic validation. A cycle extracted from industrial credit should be
compared with industrial production, capacity utilisation, lending rates, credit policy changes,
corporate investment, and sector-specific shocks. A purely univariate cycle can be informative, but it
is incomplete.

- Check missing values: anyNA(x).

- Check regular dates: all date gaps are expected monthly or quarterly gaps.

- Check transformations: nominal vs real, level vs log, seasonal adjustment.

- Check robustness: HP vs linear, 2-8 years vs 3-5 years, different bandwidths.

- Check endpoints: avoid over-interpreting the latest peak/trough without confirmation.

- Check economics: match dates with known events and related indicators.

Business cycle analysis R code - beginner explanation Page 27 of 48


25. Economic checks for Indian credit data
The script appears to analyse non-food credit in the industrial sector. For such a series, the raw level
may be affected by inflation, growth in the financial system, classification changes, one-off shocks,
and changes in reporting. A serious analysis should decide whether the appropriate object is nominal
credit, real credit, log credit, credit-to-GDP, credit growth, or deviation from trend.

If the question is about the credit cycle, log real credit or credit-to-GDP may be more interpretable
than nominal levels. If the question is about the burden of credit or financial deepening, ratios may be
better. If the question is about short-run momentum, growth rates may be useful. The best
transformation depends on the research question.

Seasonality also matters. Monthly financial data can have seasonal patterns due to financial year-end
effects, reporting behaviour, festival demand, and policy calendar effects. If the data are not
seasonally adjusted, a spectral method may find seasonal or near-seasonal components instead of a
true business cycle. The code searches a 3-to-5-year band in the example, which avoids pure annual
seasonality, but seasonality can still contaminate trend and filter estimates.

Research question Possible transformation

Is credit above its long-run path? Log real credit minus trend.

Is credit deepening relative to the Credit-to-GDP or sectoral credit-to-output ratio.


economy?

Is momentum accelerating? Year-on-year or annualised growth rate.

Is there a medium-term financial Bandpass-filtered log real credit or credit gap.


cycle?

Business cycle analysis R code - beginner explanation Page 28 of 48


26. Limitations and risks
The code is useful, but it should not be treated as a final business-cycle dating system. It is univariate,
meaning it uses only one series. It assumes regular observations. It does not handle missing values. It
does not impose minimum phase or cycle lengths. It does not test for structural breaks, even though
the final comment says "Find Structural Breaks in the NFC".

The HP filter has well-known limitations. It can distort dynamics, suffer from endpoint problems, and
produce cycles that depend on the smoothing parameter. Spectral methods also require care: the
periodogram can be noisy, the selected peak can be sample-dependent, and structural breaks can
look like low-frequency cycles.

The line of analysis is still valuable if framed correctly. It is a disciplined exploratory tool. It can help
detect whether a series has medium-term cyclical behaviour and when its above-trend and
below-trend phases occur. But the final interpretation must combine statistical evidence with
institutional knowledge and robustness checks.

- No NA handling.

- No outlier treatment.

- No seasonal adjustment.

- No structural-break estimation despite the final section heading.

- No minimum phase/cycle duration rules for turning points.

- Duration field is currently calendar days, not observations.

- No automatic audit of whether dates and data are aligned after trimming.

Business cycle analysis R code - beginner explanation Page 29 of 48


27. Corrections I recommend
The most urgent correction is the spell duration calculation. For monthly data, report duration_obs =
n(), duration_months = n(), and duration_years = n()/12. For quarterly data, report duration_quarters
= n() and duration_years = n()/4. This avoids accidental interpretation of calendar-day differences as
months.

The second correction is input validation. The function should stop early if x is not numeric, contains
missing values, or has fewer observations than required for the target cycle band. It should check that
dates are sorted, unique, and regularly spaced. These checks protect the user from silent mistakes.

The third correction is reproducibility. Do not install packages automatically inside the analysis script.
Use a separate setup file or project lockfile. The fourth correction is reporting. Return the filter
settings and spectral object so that the result is auditable. Finally, add a structural-break section if
that is part of the research agenda.

- Use -(1:12) instead of -1:-12 for clearer row dropping.

- Rename x_sim to x_nfc when using real non-food credit data.

- Create dates after reading and trimming the data, or check the imported length explicitly.

- Return lambda, lower/upper frequency bounds, and chosen bandwidth in the result list.

- Add warnings for endpoint interpretation and for very short samples.

# Safer spell duration inside summarize()


summarize(
start = first(date),
end = last(date),
duration_obs = n(),
duration_years = duration_obs / freq,
.groups = "drop"
)

Business cycle analysis R code - beginner explanation Page 30 of 48


28. A safer revised code skeleton
The following skeleton shows the kind of validation and reporting I would add before using this in a
formal empirical note. It is not a complete replacement for the original script, but it illustrates the
direction of improvement. The main idea is to fail early when inputs are unsafe and to return enough
metadata to audit the result.

A beginner should not worry if every line is not immediately clear. The important lesson is that good
time-series code checks the data before estimating the model, and it reports enough information for
someone else to reproduce the result.

Research principle
A function for serious empirical work should not only produce results. It should also protect the user
from common mistakes.

spectral_cycle_safe <- function(x, dates, freq = 12, cyc_min = 2, cyc_max = 8,


detrend = c("hp", "linear"), bandwidth_pct = 0.25) {
detrend <- [Link](detrend)
if(![Link](x)) stop("x must be numeric.")
if(anyNA(x)) stop("x contains missing values. Clean or impute first.")
if(length(x) != length(dates)) stop("x and dates must have same length.")
dates <- [Link](dates)
if(any(duplicated(dates))) stop("dates contain duplicates.")
if([Link](dates)) stop("dates must be sorted.")
# Then run the same detrend, spectrum, filter, spells, and plots.
# Return both estimates and metadata: lambda, band, frequency bounds, and settings.
}

Business cycle analysis R code - beginner explanation Page 31 of 48


29. Beginner glossary
This glossary translates the main time-series vocabulary used by the script into beginner-friendly
language. Keep this page near you when reading the annotated code appendix.

The most important distinction is between observed data and estimated components. The observed
series is what came from the data source. The trend, cycle, dominant period, spells, and turning
points are all estimated by the code. They depend on the method and settings.

Word Meaning

Observation One data point in the series, such as one month.

Frequency How often the data are observed per year. Monthly = 12, quarterly = 4.

Trend Slow-moving baseline path of the series.

Cycle Temporary deviation around the trend, usually medium-term.

Spectrum Breakdown of variance by frequency.

Frequency How many waves occur per year.

Period How many years one full wave takes. Period = 1/frequency.

Bandpass filter A filter that keeps only movements within a chosen frequency range.

Nyquist frequency Half the sampling rate; the highest frequency digital data can identify without
aliasing.

Peak Local maximum of the cycle.

Trough Local minimum of the cycle.

Spell A continuous run of expansion or contraction labels.

Business cycle analysis R code - beginner explanation Page 32 of 48


30. Practical checklist
Use this checklist before presenting results from the script. It is deliberately practical. Most mistakes
in applied time-series work come not from advanced mathematics, but from misaligned dates,
unhandled missing values, poor transformations, and over-interpretation of mechanically generated
cycles.

A good final report should show the raw series, the trend, the cycle, the assumptions, and the
robustness checks. It should clearly say that the dates are estimated using a particular filter and a
particular cycle band. It should also explain why that band is economically appropriate.

- Confirm the data source and variable definition.

- Check whether the data are nominal or real, seasonally adjusted or not, stock or flow.

- Check length after trimming rows.

- Build dates after final cleaning, not before, unless the length is known.

- Plot the raw series before filtering.

- Decide whether to log-transform the series.

- Run HP and linear detrending as a sensitivity comparison.

- Try alternative cycle bands and compare dominant periods.

- Correct spell duration to observations, months, quarters, or years.

- Do not call the results official recessions or official expansions.

- Compare turning points with external macroeconomic evidence.

- Document all parameter choices in the appendix.

Business cycle analysis R code - beginner explanation Page 33 of 48


31. Reading the code as a pipeline
The best way to read the function is not line by line at first, but stage by stage. The function has
seven conceptual stages. First it prepares arguments and dates. Second it detrends the data. Third it
estimates the spectrum and chooses the dominant period. Fourth it designs a filter around that
period. Fifth it reconstructs the cycle. Sixth it classifies states and turning points. Seventh it returns
plots and tables.

This pipeline structure is good programming practice because each stage has a clear purpose.
However, in future versions, the code could make this structure even clearer by separating tasks into
smaller helper functions: validate_inputs(), detrend_series(), estimate_dominant_period(),
reconstruct_cycle(), date_spells(), and plot_results().

Breaking the code into helper functions would make debugging easier. For example, if the dominant
period looks wrong, one can test only the spectrum function. If the spell dates look wrong, one can
test only the spell-dating function. This is how production-quality empirical code is usually organised.

Stage Function block Output

1 Argument matching and dates Clean dates and n.

2 Detrending trend and cycle_input.

3 Periodogram dominant period and frequency.

4 Band design low/high filter bounds.

5 Filtering cycle_recon.

6 Dating spells and turns.

7 Plotting and return plots and result list.

Business cycle analysis R code - beginner explanation Page 34 of 48


32. How a beginner should debug this script
When a script is complex, beginners often run the whole file and then feel lost when an error appears.
A better approach is to run one block at a time. First load packages. Then define the function without
running the example. Then read the data and inspect its dimensions. Then call the function. Then
inspect each returned object.

For this script, the most likely beginner errors are package installation problems, wrong working
directory, missing ../DATA/[Link] file, mismatch between x_sim length and dates length, missing
values in the data, or a cycle band that does not produce an allowed spectral region. Each of these
errors has a different solution.

A useful debugging line is length(x_sim) right after creating x_sim. Compare it with length(dates).
Another useful line is summary(x_sim), which tells whether there are missing values or strange
magnitudes. Also run head(x_sim) and tail(x_sim) to ensure the selected column is the intended
series.

- If the data file is not found, check getwd() and the relative path ../DATA/[Link].

- If the date length does not match, create dates after reading the cleaned data.

- If there are missing values, clean them before hpfilter or [Link].

- If the cycle band fails, widen cyc_min and cyc_max or check freq.

# Debugging checks after reading the data


length(x_sim)
length(dates)
summary(x_sim)
anyNA(x_sim)
head(x_sim)
tail(x_sim)

Business cycle analysis R code - beginner explanation Page 35 of 48


33. Structural breaks: what is missing
The final comment in the script says "Find Structural Breaks in the NFC", but no structural-break code
follows. Therefore, the current script does not estimate structural breaks. This matters because
structural breaks can distort trend and spectral estimates. A banking-sector regime change or a
reporting change can look like a cycle if it is not modelled separately.

A structural break is a change in the underlying relationship or data-generating process. For example,
the trend growth rate of credit may change after a financial crisis, a regulatory change, or a major
shock. If the sample contains such a break, a single HP trend or a single spectral pattern may be too
simple.

To add structural-break analysis in R, one common route is the strucchange package. For multiple
unknown breaks in a regression trend, Bai-Perron style breakpoints can be used. For a beginner, the
first step is visual: plot the log series and growth rate, then mark known policy and crisis dates. Only
after that should formal break tests be added.

- Current status: structural-break analysis is not implemented.

- Why it matters: breaks can be mistaken for cycles.

- Simple first check: plot series and growth rates with known event markers.

- Possible R direction: strucchange::breakpoints() for formal break dating.

- Interpretation rule: do not mix structural breaks and business cycles without explaining the difference.

Business cycle analysis R code - beginner explanation Page 36 of 48


34. Suggested reporting language
When using this script in a paper, presentation, or policy note, wording matters. Overclaiming is easy.
Instead of saying "the industrial credit cycle entered recession", say "the extracted medium-term
component of industrial non-food credit moved below zero". This tells the reader exactly what was
measured.

Also report parameter choices. A reader should know that the series was monthly, HP-detrended with
lambda 129600, searched over a 3-to-5-year band, and filtered using a 25 percent bandwidth around
the dominant spectral period. Without these details, the results are not reproducible.

Finally, separate statistical findings from economic interpretation. The statistical finding is the
estimated cycle and turning-point chronology. The economic interpretation explains why those dates
make sense, using external evidence such as monetary policy, sectoral investment, bank
balance-sheet conditions, and macro shocks.

Professional standard
Always tell the reader the transformation, detrending method, frequency band, filter, and dating
rule.

Example wording:
Using monthly data, I HP-detrend the industrial non-food credit series
and identify the dominant spectral component within a 3-to-5-year band.
The reconstructed medium-term component is then classified as above-trend
when positive and below-trend when non-positive. The resulting dates are
therefore statistical phase dates, not official recession dates.

Business cycle analysis R code - beginner explanation Page 37 of 48


35. Annotated mini-walkthrough of the most
important lines
This page highlights the lines that carry the most methodological weight. If a beginner understands
these lines, the rest of the script becomes much easier to follow. The first important line is
[Link](detrend), which forces the user to choose one of the allowed detrending methods. The
second is hpfilter() or lm(), where the trend is estimated.

The third important line is [Link](), which estimates the spectrum. The fourth is the allowed
band condition, where periods in years are converted into frequencies per year. The fifth is the
selection of idx_dom, which chooses the dominant frequency. The sixth is butter() plus filtfilt(), where
the final cycle is reconstructed.

The seventh is ifelse(cycle_recon > 0, 1, 0), where the above-trend and below-trend states are
defined. The eighth is the spells summarise block, where the duration correction is needed. The ninth
is the turning-point block using diff(sign(diff())).

Line idea Why it matters

[Link](detrend) Protects against invalid detrending choices.

hpfilter or lm Defines the trend and therefore the detrended input.

[Link] Finds where spectral power is concentrated.

allowed band Restricts search to economically relevant cycle lengths.

[Link] Chooses the dominant frequency.

butter and filtfilt Reconstructs the cycle without phase shift.

ifelse(cycle > 0) Defines expansion/contraction states.

summarize(duration) Currently reports days, so it needs correction.

Business cycle analysis R code - beginner explanation Page 38 of 48


36. References for the methods
The script combines common tools from applied macroeconomic time-series analysis and digital
signal processing. The following references are useful if you want to move from beginner
understanding to deeper methodological knowledge. They are included for context; the PDF itself
explains the uploaded code rather than reproducing any one paper.

How to use these references


Read HP and bandpass-filter papers to understand why filters are useful but controversial. Read
business-cycle dating work to understand why official phase dating usually uses more than one
indicator.

- Burns, A. F. and Mitchell, W. C. (1946). Measuring Business Cycles. National Bureau of Economic
Research.

- Hodrick, R. J. and Prescott, E. C. (1997). Postwar U.S. Business Cycles: An Empirical Investigation.
Journal of Money, Credit and Banking.

- Ravn, M. O. and Uhlig, H. (2002). On Adjusting the Hodrick-Prescott Filter for the Frequency of
Observations. Review of Economics and Statistics.

- Baxter, M. and King, R. G. (1999). Measuring Business Cycles: Approximate Band-Pass Filters for
Economic Time Series. Review of Economics and Statistics.

- Christiano, L. J. and Fitzgerald, T. J. (2003). The Band Pass Filter. International Economic Review.

- Hamilton, J. D. (2018). Why You Should Never Use the Hodrick-Prescott Filter. Review of Economics
and Statistics.

- Shumway, R. H. and Stoffer, D. S. Time Series Analysis and Its Applications, for general time-series
foundations.

- R documentation for stats::[Link], mFilter::hpfilter, signal::butter, signal::filtfilt, and ggplot2.

Business cycle analysis R code - beginner explanation Page 39 of 48


37. Annotated code appendix: complete script
with line numbers
This appendix reproduces the uploaded R script with line numbers. It is included so that a beginner
can connect the explanations above to the exact code. Long lines are wrapped visually, but the line
numbers follow the original file.

1 # Spectral-cycle + expansion/contraction detection in R


2 # Requires: mFilter, signal, pracma, zoo, tidyverse
3 # Install missing packages automatically (only if needed)
4 pkgs <- c("mFilter","signal","pracma","zoo","tidyverse")
5 to_install <- pkgs[!pkgs %in% [Link]()[,"Package"]]
6 if(length(to_install)) [Link](to_install, dependencies = TRUE)
7
8 library(mFilter) # hp filter
9 library(signal) # Butterworth and filtfilt
10 library(pracma) # hilbert (if needed)
11 library(zoo)
12 library(tidyverse)
13
14 spectral_cycle <- function(x, dates = NULL, freq = 12,
15 cyc_min = 2, cyc_max = 8, # cycle bounds in years
16 detrend = c("hp","linear"),
17 butter_order = 4,
18 bandwidth_pct = 0.25, # +/- pct around dominant period
19 spec_taper = 0.1) {
20 # x: numeric vector (observations)
21 # dates: vector of Date or POSIX or character convertible to Date; length same as x
22 # freq: observations per year (12 monthly, 4 quarterly, 1 yearly)
23 # cyc_min, cyc_max: min and max cycle period (in years) to search for peak
24 # returns: list with cycle series, dominant_period (years), data frame of spells, and
plots
25
26 detrend <- [Link](detrend)
27 n <- length(x)
28 if([Link](dates)) {
29 dates <- [Link](from = [Link]("2000-01-01"), by = paste0(round(12/freq)," months"),
[Link] = n)
30 } else {
31 dates <- [Link](dates)
32 }

Business cycle analysis R code - beginner explanation Page 40 of 48


37. Annotated code appendix: complete script
with line numbers continued
33 stopifnot(length(dates) == n)
34
35 # 1. Detrend (HP or linear)
36 if(detrend == "hp") {
37 # lambda rule of thumb: 1600 for quarterly, 129600 for monthly? common choices:
38 lambda <- ifelse(freq==4,1600, ifelse(freq==12,129600, 1600*(freq/4)^4))
39 hp <- hpfilter(x, freq = lambda, type = "lambda")
40 trend <- hp$trend
41 cycle_input <- x - trend
42 } else {
43 fit <- lm(x ~ seq_along(x))
44 trend <- fitted(fit)
45 cycle_input <- residuals(fit)
46 }
47
48 # 2. Spectral density estimate to find dominant frequency in target band
49 # [Link] expects a ts or numeric; we'll use [Link]
50 sp <- [Link](cycle_input, taper = spec_taper, detrend = TRUE, log = "no", plot =
FALSE)
51 freqs <- sp$freq # frequency in cycles per unit (unit = observation frequency)
52 # Convert [Link] frequency to cycles per year: spec$freq * freq
53 cy_per_year <- freqs * freq
54 # Allowed band in cycles per year from cyc_min..cyc_max (period in years)
55 allowed <- (1/cyc_max) <= cy_per_year & cy_per_year <= (1/cyc_min)
56 if(!any(allowed)) stop("No spectral power in requested cycle band. Expand
cyc_min/cyc_max.")
57
58 # pick max within allowed band
59 idx_dom <- [Link](sp$spec * allowed) # multiply by logical to zero out others
60 dom_freq_year <- cy_per_year[idx_dom] # cycles per year
61 dom_period_years <- 1 / dom_freq_year # years per cycle
62
63 # 3. Bandpass design around the identified dominant frequency
64 # define lower/upper period bounds as +/- bandwidth_pct

Business cycle analysis R code - beginner explanation Page 41 of 48


37. Annotated code appendix: complete script
with line numbers continued
65 lower_period <- dom_period_years * (1 - bandwidth_pct)
66 upper_period <- dom_period_years * (1 + bandwidth_pct)
67 # convert to normalized frequency for butter (Nyquist = 0.5 * sampling_rate)
68 # sampling_rate in observations per year = freq
69 nyq <- 0.5 * freq
70 # convert period (years) -> freq (cycles per year)
71 low_f_cpy <- 1 / upper_period # lower frequency (cycles per year)
72 high_f_cpy <- 1 / lower_period # upper frequency
73 # normalized (0..1) where 1 -> Nyquist
74 low_norm <- low_f_cpy / nyq
75 high_norm <- high_f_cpy / nyq
76 if(low_norm <= 0) low_norm <- 1e-6
77 if(high_norm >= 1) high_norm <- 0.9999
78 if(low_norm >= high_norm) {
79 warning("Computed filter band invalid; falling back to fft bandpass.")
80 use_butter <- FALSE
81 } else {
82 use_butter <- TRUE
83 }
84
85 # 4. Filter the detrended series to reconstruct the cycle
86 if(use_butter) {
87 bf <- butter(butter_order, c(low_norm, high_norm), type = "pass")
88 cycle_recon <- filtfilt(bf, cycle_input) # zero phase
89 } else {
90 # fallback: simple FFT bandpass
91 X <- fft(cycle_input)
92 freqs_fft <- (0:(n-1)) / n * freq # cycles per year associated with each FFT bin
93 # create mask for allowed frequencies up to Nyquist
94 mask <- rep(0, n)
95 # handle symmetry
96 allowed_idx <- which((freqs_fft >= low_f_cpy & freqs_fft <= high_f_cpy) |

Business cycle analysis R code - beginner explanation Page 42 of 48


37. Annotated code appendix: complete script
with line numbers continued
97 (freqs_fft >= (freq - high_f_cpy) & freqs_fft <= (freq -
low_f_cpy)))
98 mask[allowed_idx] <- 1
99 X_filtered <- X * mask
100 cycle_recon <- Re(fft(X_filtered, inverse = TRUE) / n)
101 }
102
103 # 5. Identify expansion (cycle_recon > 0) and contraction (<=0) spells
104 sign_series <- ifelse(cycle_recon > 0, 1, 0) # 1 = expansion, 0 = contraction
105 # find spell boundaries
106 df <- tibble(date = dates, observed = x, cycle = cycle_recon, trend = trend, expansion =
sign_series)
107 df <- df %>% mutate(exp_shift = lag(expansion, default = first(expansion)))
108 # start when expansion != previous
109 df <- df %>%
110 mutate(change = expansion != exp_shift,
111 spell_id = cumsum(change))
112 # summarize spells
113 spells <- df %>%
114 group_by(spell_id, expansion) %>%
115 summarize(start = first(date), end = last(date),
116 duration = [Link](end - start) + 1,
117 start_index = first(row_number()),
118 end_index = last(row_number()), .groups = "drop") %>%
119 arrange(start)
120 spells <- spells %>%
121 mutate(type = ifelse(expansion==1, "Expansion", "Contraction")) %>%
122 select(type, start, end, duration)
123
124 # 6. Also find cycle turning points (peaks & troughs) using local maxima/minima
125 # use simple sign of first difference of cycle to find local maxima/minima
126 dm <- diff(sign(diff(df$cycle))) # 2nd difference sign trick
127 # local max: dm == -1, local min: dm == 1 (positions shifted)
128 peaks_idx <- which(dm == -1) + 1

Business cycle analysis R code - beginner explanation Page 43 of 48


37. Annotated code appendix: complete script
with line numbers continued
129 troughs_idx <- which(dm == 1) + 1
130 peaks <- tibble(type = "Peak", date = df$date[peaks_idx], value = df$cycle[peaks_idx])
131 troughs <- tibble(type = "Trough", date = df$date[troughs_idx], value =
df$cycle[troughs_idx])
132 turns <- bind_rows(peaks, troughs) %>% arrange(date)
133
134 # 7. Plots
135 p1 <- ggplot(df, aes(x = date)) +
136 geom_line(aes(y = observed), alpha = 0.4) +
137 geom_line(aes(y = trend), linetype = "dashed") +
138 labs(title = "Observed series and trend", y = "Observed") +
139 theme_minimal()
140
141 p2 <- ggplot(df, aes(x = date)) +
142 geom_line(aes(y = cycle), size = 0.8) +
143 geom_hline(yintercept = 0, color = "black", linetype = "dotted") +
144 labs(title = paste0("Reconstructed cycle (dominant period = ",
145 round(dom_period_years,2)," years)"),
146 y = "Cycle") +
147 theme_minimal()
148
149 p3 <- ggplot(df, aes(x = date, y = cycle, fill = factor(expansion))) +
150 geom_area(alpha = 0.4) +
151 geom_hline(yintercept = 0, linetype = "dotted") +
152 labs(title = "Cycle with expansion (above 0) and contraction (below 0)",
153 fill = "State") +
154 theme_minimal()
155
156 return(list(
157 data = df,
158 spells = spells,
159 turns = turns,
160 dominant_period_years = dom_period_years,

Business cycle analysis R code - beginner explanation Page 44 of 48


37. Annotated code appendix: complete script
with line numbers continued
161 dominant_freq_per_year = dom_freq_year,
162 plots = list(series_trend = p1, cycle = p2, cycle_state = p3)
163 ))
164 }
165
166 # -----------------------
167 # Example usage with simulated monthly series (trend + cycle + noise)
168 [Link](123)
169 n <- 220
170 dates <- seq([Link]("2007-01-01"), by = "month", [Link] = n)
171
172 ### Reading Non-food credit in Industrial sector
173 x_sim <- [Link]("../DATA/[Link]", sep = "|", skip = 1)
174 x_sim <- x_sim[-1:-12, 3]/1e6
175 res <- spectral_cycle(x = x_sim, dates = dates, freq = 12,
176 cyc_min = 3, cyc_max = 5,
177 detrend = "hp")
178
179 # View results
180 print(paste("Dominant period (years):", round(res$dominant_period_years,3)))
181 print("First 6 expansion/contraction spells:")
182 print(res$spells, 6)
183 print("Turning points (first 6):")
184 print(res$turns)
185
186 # Display plots
187 print(res$plots$series_trend)
188 print(res$plots$cycle)
189 print(res$plots$cycle_state)
190
191 trend <- 0.05 * (1:n) # linear-ish
192 true_cycle <- 2.5 * sin(2*pi*(1/n)*(1:n) * (n/(2.5*12))) # roughly 2.5-year cycle

Business cycle analysis R code - beginner explanation Page 45 of 48


37. Annotated code appendix: complete script
with line numbers continued
193 # simpler: build a cycle with period 2.5 years = 30 months
194 period_months <- 2.5 * 12
195 t <- 1:n
196 true_cycle <- 1.5 * sin(2*pi * t / period_months)
197 x_sim <- trend + true_cycle + rnorm(n, sd = 0.8)
198
199
200 #### Find Structural Breaks in the NFC
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224

Business cycle analysis R code - beginner explanation Page 46 of 48


37. Annotated code appendix: complete script
with line numbers continued
225
226
227
228
229
230
231

Business cycle analysis R code - beginner explanation Page 47 of 48


38. Final one-page summary
The uploaded code is a strong starting point for exploratory business-cycle analysis. Its main strength
is that it gives a complete workflow: remove trend, find a dominant frequency in a user-defined
business-cycle band, reconstruct a cycle around that frequency, label above-trend and below-trend
phases, detect local turning points, and plot the results.

The main methodological lesson is that every output depends on modelling choices. The trend
depends on HP or linear detrending. The dominant period depends on the periodogram and search
band. The reconstructed cycle depends on the bandpass filter and bandwidth. The
expansion/contraction labels depend on the zero threshold. The turning points depend on local
extrema without censoring rules.

The main coding correction is the duration calculation in the spells table: it currently returns calendar
days, not months or quarters. The main research correction is to add data validation, transformation
decisions, robustness checks, and structural-break analysis if the objective is a formal empirical note.
Used carefully, the script can be a useful diagnostic tool for identifying medium-term cyclical
movement in a macro-financial series such as industrial non-food credit.

Bottom line
Use the output as estimated above-trend and below-trend phases of one series. Do not overstate it
as official business-cycle dating unless you add stronger dating rules, robustness checks, and
broader economic evidence.

Business cycle analysis R code - beginner explanation Page 48 of 48

You might also like