Skip to content

Forecast rollups (monthly → quarterly)

Aggregate monthly actuals/forecast into QTD/YTD and fiscal quarters.

Inputs

  • entity, dept (text)
  • month (date-like; EOM recommended)
  • amount (numeric)

Pandas recipe (calendar & fiscal)

import pandas as pd

df = pd.read_csv("docs/examples/forecast_monthly.csv", parse_dates=["month"])

# Calendar quarter totals
df["quarter"] = df["month"].dt.to_period("Q")
q = df.groupby(["entity","dept","quarter"], as_index=False)["amount"].sum()

# QTD (within-quarter running total)
df["qtd"] = df.groupby(
    ["entity","dept", df["month"].dt.to_period("Q")]
)["amount"].cumsum()

# YTD
df["year"] = df["month"].dt.year
df["ytd"] = df.groupby(["entity","dept","year"])["amount"].cumsum()

# Fiscal quarters starting July (Q-JUN)
df["fquarter"] = df["month"].dt.to_period("Q-JUN")
fq = df.groupby(["entity","dept","fquarter"], as_index=False)["amount"].sum()
print(q.head(), fq.head())

SQL sketch (Postgres)

SELECT
  entity,
  dept,
  DATE_TRUNC('quarter', month)::date AS quarter_start,
  SUM(amount) AS amount_q
FROM fact_monthly
GROUP BY 1,2,3;
-- Fiscal quarters: shift by 6 months then DATE_TRUNC('quarter'), shift back.

Checks

  • Totals tie to monthly source
  • Partial quarters handled (missing months = 0)
  • Rounding rules documented

Examples: see forecast_monthly.csv in Examples

SQL recipes

Postgres

-- Calendar quarter totals
SELECT
  entity,
  dept,
  date_trunc('quarter', month)::date AS quarter_start,
  SUM(amount) AS amount_q
FROM fact_monthly
GROUP BY 1,2,3;

-- QTD / YTD running totals
SELECT
  entity, dept, month::date,
  SUM(amount) OVER (
    PARTITION BY entity, dept, date_trunc('quarter', month)
    ORDER BY month
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS amount_qtd,
  SUM(amount) OVER (
    PARTITION BY entity, dept, date_trunc('year', month)
    ORDER BY month
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS amount_ytd
FROM fact_monthly;

-- Fiscal quarters starting July (shift 6 months)
SELECT
  entity,
  dept,
  (date_trunc('quarter', month - INTERVAL '6 months') + INTERVAL '6 months')::date AS fiscal_q_start,
  SUM(amount) AS amount_fq
FROM fact_monthly
GROUP BY 1,2,3;

SQL Server

-- Calendar quarter totals
SELECT
  entity,
  dept,
  DATEADD(QUARTER, DATEDIFF(QUARTER, 0, [month]), 0) AS quarter_start,
  SUM(amount) AS amount_q
FROM fact_monthly
GROUP BY entity, dept, DATEADD(QUARTER, DATEDIFF(QUARTER, 0, [month]), 0);

-- QTD / YTD running totals
SELECT
  entity,
  dept,
  [month],
  SUM(amount) OVER (
    PARTITION BY entity, dept, YEAR([month]), DATEPART(QUARTER, [month])
    ORDER BY [month]
    ROWS UNBOUNDED PRECEDING
  ) AS amount_qtd,
  SUM(amount) OVER (
    PARTITION BY entity, dept, YEAR([month])
    ORDER BY [month]
    ROWS UNBOUNDED PRECEDING
  ) AS amount_ytd
FROM fact_monthly;

-- Fiscal quarters starting July (shift 6 months)
SELECT
  entity,
  dept,
  DATEADD(MONTH, 6,
    DATEADD(QUARTER, DATEDIFF(QUARTER, 0, DATEADD(MONTH, -6, [month])), 0)
  ) AS fiscal_q_start,
  SUM(amount) AS amount_fq
FROM fact_monthly
GROUP BY
  entity, dept,
  DATEADD(MONTH, 6,
    DATEADD(QUARTER, DATEDIFF(QUARTER, 0, DATEADD(MONTH, -6, [month])), 0)
  );