12. Trend Detection
Chapter 12 of 18 · 20 min
Trend detection identifies long-term directional movement in data, filtering out noise and seasonality to reveal underlying patterns.
Moving Average Trend Lines
Simple moving averages reveal direction:
# Multiple timeframe trends
df['trend_7d'] = df['value'].rolling(window=7).mean()
df['trend_30d'] = df['value'].rolling(window=30).mean()
df['trend_90d'] = df['value'].rolling(window=90).mean()
# Trend direction indicator
df['trend_direction'] = np.where(df['trend_30d'] > df['trend_30d'].shift(30), 'upward', 'downward')
Linear Regression for Trend Slope
Calculate the slope of the trend line:
from scipy import stats
def calculate_trend(series):
x = np.arange(len(series))
slope, intercept, r_value, p_value, std_err = stats.linregress(x, series)
return {
'slope': slope,
'r_squared': r_value**2,
'p_value': p_value,
'trend_per_period': slope / series.mean() * 100 # % change per period
}
trend_metrics = calculate_trend(df['revenue'].dropna())
print(f"Daily trend: {trend_metrics['slope']:.2f}")
print(f"R²: {trend_metrics['r_squared']:.3f}")
print(f"p-value: {trend_metrics['p_value']:.4f}")
Detecting Change Points
Significant shifts in underlying patterns:
# Cumulative sum (CUSUM) for shift detection
def cusum_detect(data, threshold=5):
mean = data.mean()
cusum_pos = []
cusum_neg = []
for val in data:
cusum_pos.append(max(0, cusum_pos[-1] + val - mean if cusum_pos else val - mean))
cusum_neg.append(min(0, cusum_neg[-1] + val - mean if cusum_neg else val - mean))
return cusum_pos, cusum_neg
cusum_pos, cusum_neg = cusum_detect(df['metric'])
# Find change points where CUSUM exceeds threshold
change_points = [i for i, (p, n) in enumerate(zip(cusum_pos, cusum_neg))
if abs(p) > threshold or abs(n) > threshold]
Local Trend with Polynomial Fitting
from numpy.polynomial import polynomial as P
# Fit quadratic trend (captures acceleration/deceleration)
degree = 2
x = np.arange(len(df))
coeffs = np.polyfit(x, df['value'].values, degree)
poly = np.poly1d(coeffs)
df['poly_trend'] = poly(x)
# Detect inflection points where second derivative changes sign
second_derivative = np.diff(np.diff(poly(x)))
inflection_indices = np.where(np.diff(np.sign(second_derivative)) != 0)[0] + 2
Visualization with Confidence Bands
plt.figure(figsize=(12, 6))
plt.plot(df.index, df['value'], alpha=0.4, label='Actual')
plt.plot(df.index, df['trend_30d'], linewidth=2, label='30-day Trend')
# Confidence band using rolling standard deviation
upper_band = df['trend_30d'] + 2 * df['value'].rolling(30).std()
lower_band = df['trend_30d'] - 2 * df['value'].rolling(30).std()
plt.fill_between(df.index, lower_band, upper_band, alpha=0.2, label='95% CI')
plt.legend()
plt.title('Trend with Confidence Band')
EXERCISE
Calculate 90-day rolling trend for your data, identify change points using CUSUM with threshold=3, and plot the result with confidence bands.