rguides

filter

filter applies linear filtering to a time series. It computes moving averages via convolution or autoregressive filters via recursion. This is the stats package’s filter(), not dplyr’s filter(), they’re completely different functions for different tasks.

Signature

filter(x, filter, method = c("convolution", "recursive"),
       sides = 2, circular = FALSE, init)

Arguments:

  • x, a univariate or multivariate time series
  • filter, filter coefficients in reverse time order
  • method, "convolution" (moving average) or "recursive" (autoregression)
  • sides, 1 for past values only, 2 for centered around lag 0
  • circular, TRUE wraps filter around series ends
  • init, initial values for recursive filters (default: zeros)

Returns: A time series object.

Convolution filters (Moving averages)

The default method is convolution, which computes a weighted moving average. A 3-point moving average:

x <- 1:10
filter(x, rep(1, 3))
# Time Series:
# Start = 1
# End = 10
# Frequency = 1
#  [1]  NA  2  3  4  5  6  7  8  9 10

Notice the first two values are NA — the centered convolution needs values on both sides of the current point, so the series boundary leaves the endpoints undefined. This behaviour is inherent to symmetric windows: a window of width k produces (k-1)/2 missing values at each end. You can control which end gets the missing values by adjusting the sides argument, or switch to a one-sided window if you cannot tolerate gaps.

The sides argument

sides = 2 (default) centers the window around lag 0, using values from both sides of the current position — this produces a symmetric smoothing effect but leaves NA at both ends of the series. sides = 1 uses only past values, which is essential for real-time processing where future data is unavailable. With single-sided filtering, the first length(filter) - 1 values are NA because there is not enough history yet.

# Centered (sides=2): current value plus one on each side
filter(1:10, rep(1, 3), sides = 2)
#> [1]  NA  2  3  4  5  6  7  8  9  NA

# Past values only (sides=1): common for real-time applications
filter(1:10, rep(1, 3), sides = 1)
#> [1] NA NA  2  3  4  5  6  7  8  9

The sides = 1 output shows two leading NA values because the window needs three past points before it can produce a result. This one-sided approach is the standard choice for streaming data, control systems, and any scenario where latency matters more than symmetry. Once you have settled on a side orientation, the next decision is how the window handles the series boundaries — whether it wraps around or transitions to a recursive formulation.

Circular and recursive filtering

Setting circular = TRUE wraps the filter around the series ends, useful for cyclical data like angles or day-of-week values where there is no natural beginning or end. The "recursive" method applies autoregressive filtering — each output value depends on previous outputs, making it suitable for exponential smoothing and signal tracking.

# Circular: wraps around the ends
filter(1:10, rep(1, 3), sides = 1, circular = TRUE)

# Recursive: y[t] = x[t] + 0.5*y[t-1], simple exponential smoothing
filter(1:10, 0.5, method = "recursive")
#> [1]  1.0  2.5  4.2  6.1  8.0 10.0 12.0 14.0 16.0 18.0

The recursive output grows steadily because each value feeds into the next: the initial 1.0 gets augmented by half of itself, then half of that accumulation, and so on. This compounding effect is what makes recursive filtering a natural fit for exponential decay, signal tracking, and any process where the current state depends on its own history. Choosing between convolution and recursion usually comes down to whether your smoothing window is fixed-width or stateful. The examples below show how these building blocks combine to solve real analysis tasks.

Practical examples

A 5-point moving average smooths noisy data by averaging each point with its two neighbours on each side. For financial time series, an exponentially-weighted moving average can be approximated with recursive filters, and a seasonal pattern in monthly data can be removed with a 12-month centered moving average.

# Smooth a noisy signal with a 5-point moving average
signal <- sin(seq(0, 4 * pi, length.out = 100))
noise <- rnorm(100, sd = 0.3)
smoothed <- filter(signal + noise, rep(1, 5) / 5)

# 20-day simple moving average for financial data
filter(prices, rep(1, 20) / 20)

# 12-month centered MA to remove annual seasonality
filter(monthly_data, rep(1, 12) / 12, sides = 2)

Each of these calls produces a time series object that preserves the original time index, so you can plot the smoothed series directly on top of the raw data without realigning timestamps. The rep(1, k) / k pattern works for any odd window width and is the most common smoothing idiom in base R. When you write these coefficient vectors manually for asymmetric windows, the order of the coefficients matters — and the convention R uses is not the one most people expect on first encounter.

Filter coefficients in reverse time order

The filter coefficients are specified in reverse time order. This matches the convention for AR and MA coefficients in time series analysis:

# For y[i] = 0.5*x[i] + 0.3*x[i-1] + 0.2*x[i-2]
# Write coefficients in reverse: c(0.2, 0.3, 0.5)
filter(x, c(0.2, 0.3, 0.5))

This trips people up. A symmetric 3-point average is rep(1, 3), already symmetric, so reverse order doesn’t matter. But for asymmetric filters, remember: first coefficient is the oldest lag. This reverse-time convention matches how AR and MA models are specified in the stats literature, so it is consistent even though it feels backwards on first use. Another behaviour worth knowing before you apply these functions to real data is how they respond to gaps in the input — missing observations propagate through the window in a way that can silently corrupt results if you are not watching for it.

Missing values

filter allows NA values in the input series:

x <- c(1, 2, NA, 4, 5)
filter(x, rep(1, 3))
#  [1] NA NA NA  3  4
# NA propagates through the filter window

Missing values in the filter itself cause the output to be missing everywhere. When NA appears inside the coefficient vector, every output position that would have used that coefficient becomes NA, which typically means the entire output collapses to missing values. This is rarely what you want, so always check your coefficient vector for unintended NA values before calling the function. Beyond missing-data behaviour, there are a handful of other edge cases that trip up even experienced R users — the following sections cover the most frequent ones.

Common pitfalls

Assuming centered filter length must be odd

With sides = 2, an even-length filter is allowed but asymmetrically positioned, more of the filter extends forward in time than backward:

# Even-length centered filter: more forward than backward
filter(1:10, rep(1, 4), sides = 2)
# Position: uses x[i-1], x[i], x[i+1], x[i+2]

The asymmetry in an even-width centered window is subtle but real: with four points, one neighbour behind and two ahead are used, so the smoothed value is slightly forward-biased. This bias matters when you are aligning filtered output with timestamps for publication-quality plots. A different kind of confusion — and a far more common one — happens when another package exports a function with the same name.

Confusing filter() with dplyr::filter()

These are completely different functions:

# stats::filter — time series linear filtering
filter(AirPassengers, rep(1, 12))

# dplyr::filter — row selection from data frames
filter(df, age > 18, status == "active")

Loading dplyr masks stats::filter. Use stats::filter() explicitly if both are loaded.

Filter vs convolve

convolve() with type = "filter" uses the FFT and can be faster for long filters on long series, but it doesn’t return a time series and doesn’t handle NA values properly. Use filter() when you need proper time series semantics.

See also

  • dplyr::filter() — Row selection from data frames (the dplyr version, not time-series filtering).
  • purrr::keep() — Keep elements of a list that satisfy a condition.
  • apply() — Apply functions over array margins for matrix operations.