Header Ads

Header ADS

Rolling Average

 

Rolling Average — সম্পূর্ণ ব্যাখ্যা 📖


১. আগে একটা Real Story দিয়ে শুরু করি

🌡️ তোমার নিজের জীবনের Example

ধরো তুমি প্রতিদিন পড়াশোনা করো। এই মাসের প্রথম ৭ দিন:

সোমবার    → ৩ ঘণ্টা
মঙ্গলবার  → ১ ঘণ্টা  (অসুস্থ ছিলে)
বুধবার    → ৫ ঘণ্টা
বৃহস্পতিবার → ২ ঘণ্টা
শুক্রবার  → ৮ ঘণ্টা  (exam এর আগের দিন)
শনিবার    → ১ ঘণ্টা  (exam দিলে)
রবিবার    → ৪ ঘণ্টা

এখন তোমার মা জিজ্ঞেস করলো —

"তুমি প্রতিদিন গড়ে কতক্ষণ পড়ো?"

তুমি যদি শুধু শুক্রবার এর কথা বলো → ৮ ঘণ্টা (সত্য না) তুমি যদি শুধু মঙ্গলবার এর কথা বলো → ১ ঘণ্টা (সত্য না)

সত্যিকারের picture পেতে হলে কয়েকদিনের average দেখতে হবে।

এই "কয়েকদিনের চলমান average" টাই হলো Rolling Average।


২. Rolling Average কী? (Definition)

Rolling Average হলো — একটা নির্দিষ্ট সংখ্যক দিনের data এর average, যেটা প্রতিদিন এগিয়ে যায় (roll করে)।

সহজ ভাষায়:

আজকের Rolling Average (৭ দিনের)
= আজ সহ গত ৭ দিনের average

কালকের Rolling Average (৭ দিনের)
= কাল সহ গত ৭ দিনের average
(মানে আজ থেকে ৮ দিন আগেরটা বাদ যাবে, কালকেরটা ঢুকবে)

এভাবে প্রতিদিন "roll" করে এগিয়ে যায়।

৩. Rolling Average এর প্রকারভেদ

Rolling Average কত দিনের হবে সেটা
use case এর উপর নির্ভর করে।

সবচেয়ে Common:

7-Day Rolling Average   → সাপ্তাহিক trend দেখতে
14-Day Rolling Average  → দুই সপ্তাহের trend দেখতে
30-Day Rolling Average  → মাসিক trend দেখতে

৪. Step by Step Calculate করি

FoodBD App এর Daily Orders এর Data:

তারিখ        Orders
──────────────────
Jan 1   →    100
Jan 2   →    120
Jan 3   →     80   (বৃষ্টির দিন, কম order)
Jan 4   →    200   (বিশেষ offer ছিলো)
Jan 5   →    150
Jan 6   →    130
Jan 7   →    110
Jan 8   →    140
Jan 9   →    160
Jan 10  →    170

3-Day Rolling Average Calculate করি:

Jan 1  → শুধু ১ দিনের data আছে, rolling avg নেই
Jan 2  → শুধু ২ দিনের data আছে, rolling avg নেই
Jan 3  → (100 + 120 + 80)  ÷ 3 = 100.0
Jan 4  → (120 + 80 + 200)  ÷ 3 = 133.3
Jan 5  → (80 + 200 + 150)  ÷ 3 = 143.3
Jan 6  → (200 + 150 + 130) ÷ 3 = 160.0
Jan 7  → (150 + 130 + 110) ÷ 3 = 130.0
Jan 8  → (130 + 110 + 140) ÷ 3 = 126.7
Jan 9  → (110 + 140 + 160) ÷ 3 = 136.7
Jan 10 → (140 + 160 + 170) ÷ 3 = 156.7

লক্ষ্য করো:

Jan 4 এ actual orders ছিলো ২০০ (হঠাৎ বেশি)
কিন্তু Jan 4 এর Rolling Average = 133.3

মানে Rolling Average এই হঠাৎ বাড়াটাকে
smooth করে দিয়েছে।
এটাই Rolling Average এর কাজ।

৫. কেন Rolling Average দরকার?

সমস্যা — Raw Data তে Noise থাকে:

Raw Daily Orders দেখলে:

400 ┤                    ╭╮
350 ┤                   ╭╯╰╮
300 ┤          ╭╮       ╯   ╰╮
250 ┤         ╭╯╰╮           ╰╮    ╭╮
200 ┤    ╭╮  ╭╯   ╰╮          ╰╮  ╭╯╰
150 ┤   ╭╯╰╮╭╯     ╰╮          ╰╮╭╯
100 ┤  ╭╯  ╰╯        ╰╮         ╰╯
 50 ┤ ╭╯               ╰╮
  0 ┼─────────────────────────────────
    Jan1                          Jan30

খুব উঁচু-নিচু (Noisy) — trend বোঝা কঠিন।

সমাধান — Rolling Average দিয়ে Smooth করো:

7-Day Rolling Average দেখলে:

400 ┤
350 ┤
300 ┤                        ╭───
250 ┤              ╭─────────╯
200 ┤        ╭─────╯
150 ┤  ╭─────╯
100 ┤──╯
 50 ┤
  0 ┼─────────────────────────────────
    Jan1                          Jan30

মসৃণ line — trend clearly বোঝা যাচ্ছে।
Orders ধীরে ধীরে বাড়ছে।

৬. Real Business Example — FoodBD App

Scenario:

Manager জিজ্ঞেস করলো:
"জানুয়ারিতে আমাদের daily orders এর
trend কেমন ছিলো?"

Raw data দেখলে বোঝা কঠিন কারণ:
- Weekend এ বেশি order
- বৃষ্টির দিনে কম order
- Offer এর দিনে হঠাৎ বেশি order

7-Day Rolling Average দেখলে:
এই সব fluctuation smooth হয়ে
actual trend বের হয়ে আসে।

৭. SQL দিয়ে Rolling Average Calculate করা

Query 1 — 7-Day Rolling Average (Basic)

SELECT
    order_date,
    daily_orders,

    -- 7-Day Rolling Average
    ROUND(
        AVG(daily_orders) OVER (
            ORDER BY order_date
            ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
        )
    , 2) AS rolling_avg_7day

FROM (
    -- প্রতিদিনের total orders count করো
    SELECT
        DATE(created_at)    AS order_date,
        COUNT(*)            AS daily_orders
    FROM orders
    WHERE created_at >= NOW() - INTERVAL '30 days'
    GROUP BY DATE(created_at)
) daily_data

ORDER BY order_date;

Output:

order_date  | daily_orders | rolling_avg_7day
------------|--------------|------------------
2024-01-01  |    100       |   100.00
2024-01-02  |    120       |   110.00
2024-01-03  |     80       |   100.00
2024-01-04  |    200       |   125.00
2024-01-05  |    150       |   130.00
2024-01-06  |    130       |   130.00
2024-01-07  |    110       |   127.14   ← এখন থেকে পুরো ৭ দিনের avg
2024-01-08  |    140       |   132.86
2024-01-09  |    160       |   138.57
2024-01-10  |    170       |   151.43

Query 2 — Multiple Rolling Averages একসাথে

SELECT
    order_date,
    daily_orders,

    -- 7-Day Rolling Average
    ROUND(AVG(daily_orders) OVER (
        ORDER BY order_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ), 2)  AS rolling_7day,

    -- 14-Day Rolling Average
    ROUND(AVG(daily_orders) OVER (
        ORDER BY order_date
        ROWS BETWEEN 13 PRECEDING AND CURRENT ROW
    ), 2)  AS rolling_14day,

    -- 30-Day Rolling Average
    ROUND(AVG(daily_orders) OVER (
        ORDER BY order_date
        ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
    ), 2)  AS rolling_30day

FROM (
    SELECT
        DATE(created_at)  AS order_date,
        COUNT(*)          AS daily_orders
    FROM orders
    GROUP BY DATE(created_at)
) daily_data

ORDER BY order_date;

Output:

order_date  | daily_orders | rolling_7day | rolling_14day | rolling_30day
------------|--------------|--------------|---------------|---------------
2024-01-01  |    100       |   100.00     |    100.00     |    100.00
2024-01-07  |    110       |   127.14     |    127.14     |    127.14
2024-01-14  |    180       |   155.71     |    141.43     |    141.43
2024-01-21  |    220       |   198.57     |    177.14     |    163.33
2024-01-30  |    260       |   238.57     |    215.71     |    178.00

Query 3 — Rolling Average দিয়ে Anomaly ধরা

-- যেদিন actual orders, rolling average এর চেয়ে
-- ৫০% বেশি বা কম সেদিন alert করো

SELECT
    order_date,
    daily_orders,
    rolling_7day,

    -- Difference percentage
    ROUND(
        (daily_orders - rolling_7day) * 100.0
        / NULLIF(rolling_7day, 0)
    , 2) AS diff_percentage,

    -- Flag করো
    CASE
        WHEN daily_orders > rolling_7day * 1.5
            THEN '🔺 Unusually HIGH'
        WHEN daily_orders < rolling_7day * 0.5
            THEN '🔻 Unusually LOW'
        ELSE '✅ Normal'
    END AS status

FROM (
    SELECT
        order_date,
        daily_orders,
        ROUND(AVG(daily_orders) OVER (
            ORDER BY order_date
            ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
        ), 2) AS rolling_7day
    FROM (
        SELECT
            DATE(created_at) AS order_date,
            COUNT(*)         AS daily_orders
        FROM orders
        GROUP BY DATE(created_at)
    ) d
) final_data

ORDER BY order_date;

Output:

order_date  | daily_orders | rolling_7day | diff_pct | status
------------|--------------|--------------|----------|-------------------
2024-01-04  |    200       |   125.00     |  +60.0%  | 🔺 Unusually HIGH
2024-01-11  |     50       |   130.00     |  -61.5%  | 🔻 Unusually LOW
2024-01-18  |    150       |   145.00     |   +3.4%  | ✅ Normal

Jan 4 — কোনো special offer ছিলো? Jan 11 — কোনো সমস্যা হয়েছিলো? App down ছিলো?


৮. Python দিয়ে Rolling Average Calculate করা

import pandas as pd
import psycopg2

conn = psycopg2.connect(
    host="your-host",
    database="your_db",
    user="your_user",
    password="your_password"
)

query = """
    SELECT
        DATE(created_at)  AS order_date,
        COUNT(*)          AS daily_orders
    FROM orders
    WHERE created_at >= NOW() - INTERVAL '30 days'
    GROUP BY DATE(created_at)
    ORDER BY order_date
"""

df = pd.read_sql(query, conn)
conn.close()

# Date column কে datetime এ convert করো
df['order_date'] = pd.to_datetime(df['order_date'])
df = df.sort_values('order_date').reset_index(drop=True)

# =============================================
# ROLLING AVERAGE CALCULATE
# =============================================

# 7-Day Rolling Average
df['rolling_7day'] = (
    df['daily_orders']
    .rolling(window=7, min_periods=1)
    .mean()
    .round(2)
)

# 14-Day Rolling Average
df['rolling_14day'] = (
    df['daily_orders']
    .rolling(window=14, min_periods=1)
    .mean()
    .round(2)
)

# 30-Day Rolling Average
df['rolling_30day'] = (
    df['daily_orders']
    .rolling(window=30, min_periods=1)
    .mean()
    .round(2)
)

print("============================================")
print("      ROLLING AVERAGE REPORT               ")
print("============================================")
print(df[[
    'order_date',
    'daily_orders',
    'rolling_7day',
    'rolling_14day',
    'rolling_30day'
]].to_string(index=False))

Output:

============================================
      ROLLING AVERAGE REPORT
============================================
order_date  daily_orders  rolling_7day  rolling_14day  rolling_30day
2024-01-01           100        100.00         100.00         100.00
2024-01-02           120        110.00         110.00         110.00
2024-01-03            80        100.00         100.00         100.00
2024-01-04           200        125.00         125.00         125.00
2024-01-05           150        130.00         130.00         130.00
2024-01-06           130        130.00         130.00         130.00
2024-01-07           110        127.14         127.14         127.14
2024-01-08           140        132.86         132.86         132.86
2024-01-09           160        138.57         138.57         138.57
2024-01-10           170        151.43         151.43         151.43

# =============================================
# ANOMALY DETECTION
# =============================================
df['diff_pct'] = (
    (df['daily_orders'] - df['rolling_7day'])
    * 100 / df['rolling_7day']
).round(2)

def get_status(diff):
    if diff > 50:
        return '🔺 Unusually HIGH'
    elif diff < -50:
        return '🔻 Unusually LOW'
    else:
        return '✅ Normal'

df['status'] = df['diff_pct'].apply(get_status)

# শুধু Anomaly গুলো দেখাও
anomalies = df[df['status'] != '✅ Normal']

print("\n===== Anomaly Report =====")
if len(anomalies) == 0:
    print("কোনো Anomaly নেই। সব Normal ✅")
else:
    print(anomalies[[
        'order_date',
        'daily_orders',
        'rolling_7day',
        'diff_pct',
        'status'
    ]].to_string(index=False))

Output:

===== Anomaly Report =====
order_date  daily_orders  rolling_7day  diff_pct  status
2024-01-04           200        125.00     60.00   🔺 Unusually HIGH
2024-01-11            50        130.00    -61.54   🔻 Unusually LOW

৯. ETL — Daily Rolling Average Save করা

# etl_rolling_average.py
# প্রতিদিন সকাল ৭টায় CRON এ run হবে

import psycopg2
from datetime import datetime

def calculate_and_save_rolling_avg():
    conn = psycopg2.connect(
        host="your-host",
        database="your_db",
        user="your_user",
        password="your_password"
    )
    cursor = conn.cursor()

    # Calculate করো এবং summary table এ save করো
    upsert_query = """
        INSERT INTO order_rolling_avg_summary (
            summary_date,
            daily_orders,
            rolling_avg_7day,
            rolling_avg_14day,
            rolling_avg_30day
        )
        SELECT
            order_date,
            daily_orders,

            ROUND(AVG(daily_orders) OVER (
                ORDER BY order_date
                ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
            ), 2),

            ROUND(AVG(daily_orders) OVER (
                ORDER BY order_date
                ROWS BETWEEN 13 PRECEDING AND CURRENT ROW
            ), 2),

            ROUND(AVG(daily_orders) OVER (
                ORDER BY order_date
                ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
            ), 2)

        FROM (
            SELECT
                DATE(created_at)  AS order_date,
                COUNT(*)          AS daily_orders
            FROM orders
            WHERE created_at >= NOW() - INTERVAL '30 days'
            GROUP BY DATE(created_at)
        ) daily_data

        ON CONFLICT (summary_date)
        DO UPDATE SET
            daily_orders      = EXCLUDED.daily_orders,
            rolling_avg_7day  = EXCLUDED.rolling_avg_7day,
            rolling_avg_14day = EXCLUDED.rolling_avg_14day,
            rolling_avg_30day = EXCLUDED.rolling_avg_30day,
            updated_at        = NOW()
    """

    cursor.execute(upsert_query)
    conn.commit()

    print(f"[{datetime.now()}] Rolling Average ETL Complete.")
    cursor.close()
    conn.close()

if __name__ == "__main__":
    calculate_and_save_rolling_avg()

Crontab:

# প্রতিদিন সকাল ৭টায় চলবে
0 7 * * * /usr/bin/python3 /home/etl/etl_rolling_average.py

১০. Superset এ Rolling Average দেখানো

-- Superset Line Chart এর জন্য
-- Raw data এবং Rolling Average একসাথে দেখাও

SELECT
    summary_date        AS date,
    daily_orders        AS "Daily Orders",
    rolling_avg_7day    AS "7-Day Rolling Avg",
    rolling_avg_30day   AS "30-Day Rolling Avg"
FROM order_rolling_avg_summary
WHERE summary_date >= NOW() - INTERVAL '30 days'
ORDER BY summary_date;

Superset এ Line Chart বানাও। তিনটা line একসাথে দেখাবে। Raw data উঁচু-নিচু হবে। Rolling Average smooth line হবে।


১১. Window Size কত রাখবো?

Window ছোট (3-7 দিন):
→ Recent change দ্রুত ধরা যায়
→ কিন্তু এখনো একটু noisy থাকে
→ কখন use করবে: Short-term trend দেখতে

Window বড় (14-30 দিন):
→ অনেক smooth হয়
→ কিন্তু recent change দেরিতে দেখা যায়
→ কখন use করবে: Long-term trend দেখতে

Production এ সাধারণত:
Daily dashboard → 7-Day Rolling Average
Monthly report  → 30-Day Rolling Average

১২. Common Mistakes ⚠️

Mistake সমস্যা সমাধান
min_periods না দেওয়া প্রথম কয়েকদিন NULL আসে min_periods=1 দাও
Window size ঠিক না করা অনেক বড় window দিলে সব smooth হয়ে যায় Use case অনুযায়ী window বেছে নাও
Missing dates handle না করা কোনো দিনের data না থাকলে skip হয় সব dates fill করো, missing = 0
Timezone ignore করা ভুল দিনে data পড়ে সবসময় timezone specify করো

সারসংক্ষেপ 🎯

বিষয় মূল কথা
Rolling Average কী  নির্দিষ্ট কয়েকদিনের চলমান average
কেন দরকার Raw data এর noise কমিয়ে actual trend দেখাতে
Common Windows       7-day, 14-day, 30-day
SQL এ AVG() OVER (ROWS BETWEEN N PRECEDING AND CURRENT ROW)
Python এ df['col'].rolling(window=7).mean()
Superset এ Line chart এ raw + rolling একসাথে দেখাও


Powered by Blogger.