Trend Analysis¶
Time is one of the most important dimensions in business data. Revenue grows or falls. Customer behaviour shifts by season. Products have lifecycle patterns. Trend analysis finds the signal in the noise — separating genuine growth from seasonal effects and random variation.
Learning Objectives¶
- Plot time series data and identify trends
- Calculate period-over-period changes (MoM, YoY)
- Apply rolling averages to smooth noise
- Decompose seasonality from trend
- Identify anomalous periods
Setting Up Time Series in Pandas¶
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
df = pd.read_csv("orders_clean.csv", parse_dates=["order_date"])
df["total"] = df["quantity"] * df["unit_price"]
completed = df[df["status"] == "completed"].copy()
# Aggregate to daily
daily = completed.groupby("order_date")["total"].sum()
# Aggregate to monthly
monthly = completed.set_index("order_date")["total"].resample("ME").sum()
monthly.index = monthly.index.to_period("M").astype(str) # "2024-01", "2024-02", ...
print(monthly)
Trend Line — Daily Revenue¶
fig, ax = plt.subplots(figsize=(14, 5))
ax.plot(daily.index, daily.values, alpha=0.3, color="#94A3B8", linewidth=1, label="Daily")
# 7-day moving average
ma7 = daily.rolling(7).mean()
ax.plot(ma7.index, ma7.values, color="#0D9488", linewidth=2, label="7-day MA")
# 28-day moving average
ma28 = daily.rolling(28).mean()
ax.plot(ma28.index, ma28.values, color="#F59E0B", linewidth=2, label="28-day MA")
ax.set_title("Daily Revenue with Moving Averages")
ax.set_xlabel("Date")
ax.set_ylabel("Revenue (£)")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Period-over-Period Changes¶
# Month-over-month change
monthly_df = pd.DataFrame({"revenue": monthly})
monthly_df["prev_month"] = monthly_df["revenue"].shift(1)
monthly_df["mom_change"] = monthly_df["revenue"] - monthly_df["prev_month"]
monthly_df["mom_pct"] = (monthly_df["mom_change"] / monthly_df["prev_month"] * 100).round(1)
print(monthly_df[["revenue", "mom_pct"]].to_string())
# Plot MoM growth
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
ax1.bar(monthly_df.index, monthly_df["revenue"] / 1000, color="#0D9488", alpha=0.8)
ax1.set_title("Monthly Revenue")
ax1.set_ylabel("Revenue (£k)")
colors = ["#0D9488" if x >= 0 else "#EF4444" for x in monthly_df["mom_pct"].fillna(0)]
ax2.bar(monthly_df.index, monthly_df["mom_pct"].fillna(0), color=colors, alpha=0.8)
ax2.axhline(y=0, color="black", linewidth=0.8)
ax2.set_title("Month-over-Month Growth (%)")
ax2.set_ylabel("Growth (%)")
ax2.tick_params(axis="x", rotation=45)
plt.tight_layout()
plt.show()
Day-of-Week Patterns¶
completed["day_of_week"] = completed["order_date"].dt.day_name()
day_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
dow_revenue = (
completed
.groupby("day_of_week")["total"]
.agg(["sum", "mean", "count"])
.reindex(day_order)
)
print(dow_revenue)
fig, ax = plt.subplots(figsize=(10, 5))
ax.bar(dow_revenue.index, dow_revenue["mean"], color="#0D9488", alpha=0.8)
ax.axhline(dow_revenue["mean"].mean(), color="red", linestyle="--", label="Weekly avg")
ax.set_title("Average Order Value by Day of Week")
ax.set_ylabel("Avg Order Value (£)")
ax.legend()
plt.tight_layout()
plt.show()
Identifying Anomalous Periods¶
# Flag months where revenue was >2 std below the rolling mean
rolling_mean = monthly.rolling(3).mean()
rolling_std = monthly.rolling(3).std()
lower_bound = rolling_mean - 2 * rolling_std
anomalies = monthly[monthly < lower_bound]
print(f"Anomalous months: {len(anomalies)}")
print(anomalies)
# Highlight anomalies on the chart
fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(monthly.index, monthly.values, color="#0D9488", linewidth=2)
ax.fill_between(monthly.index, lower_bound, monthly.max(), alpha=0.1, color="#0D9488")
for period, value in anomalies.items():
ax.scatter(period, value, color="#EF4444", zorder=5, s=100, label="Anomaly")
ax.set_title("Monthly Revenue with Anomaly Detection")
ax.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Practice Exercises¶
Warm-up 1. Calculate monthly revenue for completed orders. Print the results. 2. Calculate month-over-month percentage change for each month. 3. Plot a bar chart of monthly revenue.
Main 4. Plot daily revenue with 7-day and 28-day moving averages. 5. Calculate and plot average order value by day of week. Which days are slowest? 6. Find the top 3 and bottom 3 months by revenue.
Stretch
7. Decompose the time series into trend and seasonality using seasonal_decompose from statsmodels.
8. Calculate year-over-year revenue growth for each month (compare Jan 2024 to Jan 2023, etc.).
Interview Questions¶
[Beginner] What is a moving average and why is it useful for trend analysis?
[Mid-level] Revenue in November is always much higher than other months. How do you separate the seasonal effect from the underlying growth trend?
[Senior] Your dashboard shows an 18% MoM revenue increase. A stakeholder is excited. What questions would you ask before presenting this as good news?