অধ্যায় 6 · প্রজেক্ট ও পরের ধাপ
প্রজেক্ট: BitByte Shop-এর বিক্রয় বিশ্লেষণ রিপোর্ট
- পৃষ্ঠা 21 / 22
- 18 মিনিট পড়া
এবার সব একসাথে কাজে লাগানোর পালা। BitByte Shop-এর ম্যানেজার বিক্রির একটা রিপোর্ট চান: প্রতি মাসে কত বিক্রি হলো, কোন প্রোডাক্ট আর কোন কাস্টমার সবচেয়ে গুরুত্বপূর্ণ, কাস্টমাররা কীভাবে টাকা দেন, আর কোন অর্ডারগুলোতে নজর দেওয়া দরকার? প্রতিটা প্রশ্নের উত্তর আপনি দেবেন একটা করে কোয়েরি দিয়ে, সংখ্যাগুলো একটার সাথে আরেকটা মিলিয়ে দেখবেন, আর শেষে একটা Python স্ক্রিপ্ট লিখবেন, যা পুরো রিপোর্ট চালিয়ে প্রতিটা টেবিল CSV ফাইলে সেভ করে — চার্ট বা স্প্রেডশিটে ব্যবহারের জন্য তৈরি।
এটা একজন ডেটা অ্যানালিস্টের রোজকার কাজ, আর বেশিরভাগ ML প্রজেক্টের প্রথম ধাপও: চার্ন (churn) মডেল বা রেকমেন্ডার ট্রেন করার আগে কাউকে না কাউকে অর্ডারের কাঁচা সারিগুলো থেকে মাস, প্রোডাক্ট আর কাস্টমার ধরে ভরসাযোগ্য সংখ্যা বের করতে হয়।
যা শিখবেন
- ব্যবসার একটা অস্পষ্ট প্রশ্নকে নির্দিষ্ট নিয়মে বদলানো (আয় বলতে ঠিক কী গোনা হবে?) আর সেই নিয়ম সব জায়গায় একভাবে প্রয়োগ করা।
- জয়েন,
GROUP BY,CASE,LEFT JOINআর সাবকোয়েরি মিলিয়ে রিপোর্টের কোয়েরি লেখা। - রিপোর্ট যাচাই করা: কোন টোটালগুলো মিলতেই হবে, কোন সারি হারিয়ে যাওয়া চলবে না।
- pandas দিয়ে Python থেকে রিপোর্ট চালানো আর CSV-তে এক্সপোর্ট করা।
কাজের বিবরণ আর মূল নিয়ম
প্রতিটা রিপোর্ট শুরু হয় সংজ্ঞা দিয়ে। দুজন মানুষ যদি "আয়" দুইভাবে গোনেন, তাঁদের সংখ্যা কখনো মিলবে না, আর কেউই কারও সংখ্যায় ভরসা করবে না। তাই প্রথম কোয়েরির আগেই নিয়মগুলো লিখে ফেলুন:
| পরিভাষা | এই রিপোর্টে যে নিয়ম |
|---|---|
| বিক্রি হওয়া অর্ডার | যে অর্ডারের status delivered বা shipped। বাতিল (cancelled) আর অপেক্ষমাণ (pending) অর্ডার আলাদাভাবে দেখানো হবে। |
| আয় | বিক্রি হওয়া অর্ডারের order_items থেকে quantity * unit_price: আসলে যে দামে বিক্রি হয়েছে, ছাড়সহ। |
| মাস | order_date-এর মাস, YYYY-MM আকারে। |
| গড় অর্ডার মূল্য | আয় ÷ বিক্রি হওয়া অর্ডারের সংখ্যা, প্রতি অর্ডারের একটা করে টোটাল থেকে হিসাব করা। |
প্রথমে ডেটার আকার আর অর্ডারের status-গুলোতে একবার চোখ বুলিয়ে নিন, যাতে জানেন আপনি কী গুনছেন:
SELECT 'orders' AS table_name, COUNT(*) AS n_rows FROM orders
UNION ALL
SELECT 'order_items', COUNT(*) FROM order_items
UNION ALL
SELECT 'payments', COUNT(*) FROM payments;
SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC, status;+-------------+--------+
| table_name | n_rows |
+-------------+--------+
| orders | 14 |
| order_items | 20 |
| payments | 13 |
+-------------+--------+
+-----------+--------+
| status | orders |
+-----------+--------+
| delivered | 11 |
| cancelled | 1 |
| pending | 1 |
| shipped | 1 |
+-----------+--------+মোট ১৪টা অর্ডার, যার ১২টা বিক্রি হিসেবে গোনা হবে। সংখ্যাগুলো মনে রাখুন; পরের প্রতিটা ফলাফলকে এদের সাথে মিলতে হবে।
ধাপ ১: মূল সংখ্যাগুলো
গড় অর্ডার মূল্যের জন্য আগে প্রতি অর্ডারের একটা টোটাল লাগবে, তারপর সেই টোটালগুলোর গড়। আইটেমের সারিগুলোর সরাসরি গড় নিলে পাবেন গড় আইটেমের মূল্য, অর্ডারের নয়। তাই প্রতি অর্ডারের টোটাল একটা সাবকোয়েরিতে বানান, আর বাইরের কোয়েরিতে সেগুলোর সারাংশ নিন:
SELECT COUNT(*) AS orders,
COUNT(DISTINCT customer_id) AS customers,
SUM(total) AS revenue,
ROUND(AVG(total), 2) AS avg_order_value,
MAX(total) AS largest_order
FROM (
SELECT o.order_id, o.customer_id,
SUM(oi.quantity * oi.unit_price) AS total
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY o.order_id, o.customer_id
) AS t;+--------+-----------+---------+-----------------+---------------+
| orders | customers | revenue | avg_order_value | largest_order |
+--------+-----------+---------+-----------------+---------------+
| 12 | 6 | 70050 | 5837.5 | 18500 |
+--------+-----------+---------+-----------------+---------------+৬ জন কাস্টমারের ১২টা বিক্রি হওয়া অর্ডার, মোট ৭০,০৫০ টাকা। গড় (প্রায় ৫,৮০০) আর সবচেয়ে বড় অর্ডারের (১৮,৫০০) ফারাকটা খেয়াল করুন: অল্প কয়েকটা বড় অর্ডার — একটা মনিটর আর একটা বুটক্যাম্প — থেকেই আয়ের বড় একটা অংশ এসেছে।
ধাপ ২: মাসভিত্তিক আয়
SELECT strftime('%Y-%m', o.order_date) AS month,
COUNT(DISTINCT o.order_id) AS orders,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY month
ORDER BY month;+---------+--------+---------+
| month | orders | revenue |
+---------+--------+---------+
| 2026-01 | 2 | 6600 |
| 2026-02 | 2 | 7000 |
| 2026-03 | 3 | 21400 |
| 2026-04 | 2 | 23450 |
| 2026-05 | 2 | 8500 |
| 2026-06 | 1 | 3100 |
+---------+--------+---------+যা খেয়াল করবেন:
COUNT(*)নয়,COUNT(DISTINCT o.order_id): জয়েনের পর প্রতি আইটেমে একটা সারি থাকে, তাই দুই আইটেমের অর্ডার দুবার গোনা হয়ে যেত।- ছয় মাসের সংখ্যা যোগ করলে ৭০,০৫০, অর্থাৎ মূল টোটাল। এটাই আপনার প্রথম ক্রস-চেক।
- মার্চ আর এপ্রিল বড় দেখাচ্ছে দুটো দামি অর্ডারের (বুটক্যাম্প আর মনিটর) কারণে, বেশি অর্ডারের কারণে নয়। যোগফলের পাশে সবসময় অর্ডারের সংখ্যাটাও দেখুন।
strftimeSQLite-এর। MySQL-এ লিখুনDATE_FORMAT(order_date, '%Y-%m'), PostgreSQL-এto_char(order_date, 'YYYY-MM')(দেখুন টেক্সট, সংখ্যা ও তারিখের ফাংশন, CAST আর COALESCE)।
কোনো মাসে বিক্রি না হলে সেই মাসটা এই ফলাফল থেকে স্রেফ বাদ পড়বে, আর চার্টও চুপচাপ তাকে টপকে যাবে। অ্যাডভান্সড টিউটোরিয়ালের প্রথম পাতা CTE আর রিকার্সিভ কোয়েরি-তে রিকার্সিভ CTE দিয়ে একটা ক্যালেন্ডার বানানো হবে, যাতে খালি মাসগুলোও ০ হিসেবে দেখা যায়।
ধাপ ৩: সেরা প্রোডাক্ট, আর যেগুলো কখনো বিক্রি হয়নি
SELECT p.name,
SUM(oi.quantity) AS units,
SUM(oi.quantity * oi.unit_price) AS revenue,
SUM(oi.quantity * (p.price - oi.unit_price)) AS discount
FROM order_items AS oi
JOIN orders AS o ON o.order_id = oi.order_id
JOIN products AS p ON p.product_id = oi.product_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY p.product_id, p.name
ORDER BY revenue DESC, p.product_id
LIMIT 5;+---------------------------+-------+---------+----------+
| name | units | revenue | discount |
+---------------------------+-------+---------+----------+
| 27-inch Monitor | 1 | 18500 | 0 |
| AI Engineering Bootcamp | 1 | 15000 | 0 |
| Mechanical Keyboard | 2 | 8550 | 450 |
| SQL Masterclass | 2 | 6000 | 0 |
| Hands-On Machine Learning | 2 | 5000 | 0 |
+---------------------------+-------+---------+----------+discount কলাম তালিকার দাম (list price) আর আসলে যে দামে বিক্রি হয়েছে, তা তুলনা করে: কিবোর্ডটা একবার ৪,৫০০-র বদলে ৪,০৫০ টাকায় বিক্রি হয়েছিল। এজন্যই আয় সবসময় order_items.unit_price থেকে, কখনো products.price থেকে নয়: প্রোডাক্ট টেবিলে থাকে আজকের দাম, অর্ডার আইটেমে থাকে কাস্টমার আসলে কত দিয়েছেন।
যে প্রোডাক্টগুলো কোনো অর্ডারেই নেই, সেগুলো খুঁজতে লাগবে LEFT JOIN আর অন্য পাশটা ফাঁকা কিনা তার পরীক্ষা (দেখুন LEFT, RIGHT, FULL, CROSS আর সেলফ জয়েন):
SELECT p.product_id, p.name, p.stock
FROM products AS p
LEFT JOIN order_items AS oi ON oi.product_id = p.product_id
WHERE oi.product_id IS NULL
ORDER BY p.product_id;+------------+-----------------------------+-------+
| product_id | name | stock |
+------------+-----------------------------+-------+
| 10 | Noise-Cancelling Headphones | 8 |
| 11 | Gift Card | NULL |
+------------+-----------------------------+-------+৮টা হেডফোন স্টকে পড়ে আছে, একটাও বিক্রি হয়নি: ম্যানেজারের জন্য কাজের একটা তথ্য।
ধাপ ৪: সেরা কাস্টমার
SELECT c.customer_id,
c.name,
COALESCE(c.city, 'Unknown') AS city,
COUNT(DISTINCT o.order_id) AS orders,
SUM(oi.quantity * oi.unit_price) AS revenue,
MIN(o.order_date) AS first_order,
MAX(o.order_date) AS last_order
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY c.customer_id, c.name, c.city
ORDER BY revenue DESC, c.customer_id
LIMIT 5;+-------------+---------------+------------+--------+---------+-------------+------------+
| customer_id | name | city | orders | revenue | first_order | last_order |
+-------------+---------------+------------+--------+---------+-------------+------------+
| 1 | Nadia Rahman | Dhaka | 3 | 23600 | 2026-01-12 | 2026-04-19 |
| 2 | Tanvir Ahmed | Chattogram | 3 | 22500 | 2026-01-20 | 2026-05-21 |
| 3 | Farhana Akter | Dhaka | 3 | 9500 | 2026-02-14 | 2026-06-09 |
| 7 | Mitu Das | Dhaka | 1 | 5500 | 2026-05-06 | 2026-05-06 |
| 6 | Imran Hossain | Khulna | 1 | 4950 | 2026-04-02 | 2026-04-02 |
+-------------+---------------+------------+--------+---------+-------------+------------+প্রতি কাস্টমারের অর্ডারের সংখ্যা, আয় আর শেষ অর্ডারের তারিখ: এই তিনটা সংখ্যা (কত ঘন ঘন, কত টাকার, কত সাম্প্রতিক) হুবহু RFM ফিচার (recency, frequency, monetary), যার ওপর চার্ন আর LTV (lifetime value) মডেল দাঁড়িয়ে থাকে। অ্যাডভান্সড টিউটোরিয়ালে এগুলো ML ফিচার হিসেবে বানাবেন।
কাদের কাছ থেকে কোনো আয়ই আসেনি? প্রতি কাস্টমারের আয় একটা সাবকোয়েরিতে রাখুন, আর সব কাস্টমারকে তার সাথে LEFT JOIN করুন:
SELECT c.customer_id, c.name, COALESCE(r.revenue, 0) AS revenue
FROM customers AS c
LEFT JOIN (
SELECT o.customer_id, SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY o.customer_id
) AS r ON r.customer_id = c.customer_id
WHERE r.customer_id IS NULL
ORDER BY c.customer_id;+-------------+-------------+---------+
| customer_id | name | revenue |
+-------------+-------------+---------+
| 4 | Rafiq Islam | 0 |
| 8 | Karim Uddin | 0 |
+-------------+-------------+---------+দুটো আলাদা গল্প: Karim কখনো অর্ডারই করেননি, আর Rafiq একবার অর্ডার করে বাতিল করেছেন। শুধু orders টেবিল দিয়ে "অর্ডারবিহীন কাস্টমার" খুঁজলে পাওয়া যেত শুধু Karim-কে। কোন তালিকা চাই, তা প্রশ্নের ওপর নির্ভর করে, তাই কোনটা রিপোর্ট করছেন তা স্পষ্ট বলুন।
ধাপ ৫: ক্যাটাগরির পারফরম্যান্স
প্রতিটা ক্যাটাগরি দেখাতে হবে, প্রোডাক্টহীন Furniture-সহ। তাই শুরু করুন categories থেকে, প্রোডাক্টগুলো LEFT JOIN করুন, তারপর প্রতি প্রোডাক্টের বিক্রির একটা সাবকোয়েরি LEFT JOIN করুন। আয়ের শতাংশ বের করতে ভাগ করা হয় সর্বমোট আয় দিয়ে, যা আসে একটা স্কেলার সাবকোয়েরি থেকে:
SELECT c.name AS category,
COUNT(p.product_id) AS products,
COALESCE(SUM(s.units), 0) AS units,
COALESCE(SUM(s.revenue), 0) AS revenue,
ROUND(100.0 * COALESCE(SUM(s.revenue), 0) / (
SELECT SUM(oi.quantity * oi.unit_price)
FROM order_items AS oi
JOIN orders AS o ON o.order_id = oi.order_id
WHERE o.status IN ('delivered', 'shipped')
), 1) AS pct_of_revenue
FROM categories AS c
LEFT JOIN products AS p ON p.category_id = c.category_id
LEFT JOIN (
SELECT oi.product_id,
SUM(oi.quantity) AS units,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items AS oi
JOIN orders AS o ON o.order_id = oi.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY oi.product_id
) AS s ON s.product_id = p.product_id
GROUP BY c.category_id, c.name
ORDER BY revenue DESC, c.category_id;+-------------+----------+-------+---------+----------------+
| category | products | units | revenue | pct_of_revenue |
+-------------+----------+-------+---------+----------------+
| Electronics | 4 | 8 | 31550 | 45.0 |
| Courses | 2 | 3 | 21000 | 30.0 |
| Accessories | 2 | 5 | 8900 | 12.7 |
| Books | 2 | 5 | 8600 | 12.3 |
| Furniture | 0 | 0 | 0 | 0.0 |
+-------------+----------+-------+---------+----------------+মিলিয়ে দেখুন: ৩১,৫৫০ + ২১,০০০ + ৮,৯০০ + ৮,৬০০ = ৭০,০৫০, আর শতাংশগুলো যোগ করলে ১০০। Gift Card-এর কোনো ক্যাটাগরি নেই, তাই এই তালিকায় সেটা আসতেই পারে না; এখানে সমস্যা হয়নি শুধু এজন্য যে সেটা কখনো বিক্রি হয়নি। হলে ক্যাটাগরির টোটাল মূল টোটালের চেয়ে কম হতো — ঠিক এ কারণেই দুটো মিলিয়ে দেখতে হয়।
খেয়াল করুন, বিক্রির নিয়ম status IN ('delivered', 'shipped') এখন একই কোয়েরিতে দুবার লেখা। অ্যাডভান্সড টিউটোরিয়ালে একটা CTE (WITH sold AS (…)) দিয়ে নিয়মটা একবার লিখে বারবার ব্যবহার করবেন।
ধাপ ৬: পেমেন্টের ধরন
SELECT method,
COUNT(*) AS payments,
COUNT(DISTINCT order_id) AS orders,
SUM(amount) AS amount,
ROUND(100.0 * SUM(amount) / (SELECT SUM(amount) FROM payments), 1) AS pct
FROM payments
GROUP BY method
ORDER BY amount DESC, method;+--------+----------+--------+--------+------+
| method | payments | orders | amount | pct |
+--------+----------+--------+--------+------+
| card | 6 | 6 | 44950 | 64.2 |
| bkash | 5 | 5 | 18000 | 25.7 |
| cash | 2 | 2 | 7100 | 10.1 |
+--------+----------+--------+--------+------+টাকার প্রায় দুই-তৃতীয়াংশ আসে কার্ডে। এবার orders কলামটা যোগ করুন: ৬ + ৫ + ২ = ১৩, অথচ টাকা দেওয়া হয়েছে ১২টা অর্ডারে। অর্ডার ৬-এর টাকা এসেছে দুই ভাগে, bKash আর কার্ডে, তাই দুই মেথডেই সেটা একবার করে গোনা হয়েছে। কোনো সংখ্যাই ভুল নয়; তারা আলাদা প্রশ্নের উত্তর দেয়। পেমেন্ট গুনলে "পেমেন্ট" বলুন, "অর্ডার" নয়।
ধাপ ৭: যে অর্ডারগুলোতে নজর দরকার
শেষ যাচাইটা মেলায় প্রতিটা অর্ডারের মূল্য আর তার জন্য পাওয়া টাকা। আইটেম আর পেমেন্ট আলাদাভাবে অ্যাগ্রিগেট করুন, তারপর দুটো টোটাল জয়েন করুন; কাঁচা সারিগুলো সরাসরি জয়েন করলে অর্ডার ৬-এর আইটেম তার দুটো পেমেন্ট দিয়ে গুণ হয়ে যেত (অনেক টেবিল জয়েন, আর ডুপ্লিকেট সারির ফাঁদ পাতার সেই ফাঁদ):
SELECT o.order_id,
o.status,
t.total,
COALESCE(pay.paid, 0) AS paid,
t.total - COALESCE(pay.paid, 0) AS unpaid
FROM orders AS o
JOIN (
SELECT order_id, SUM(quantity * unit_price) AS total
FROM order_items
GROUP BY order_id
) AS t ON t.order_id = o.order_id
LEFT JOIN (
SELECT order_id, SUM(amount) AS paid
FROM payments
GROUP BY order_id
) AS pay ON pay.order_id = o.order_id
WHERE t.total <> COALESCE(pay.paid, 0)
ORDER BY o.order_id;+----------+-----------+-------+------+--------+
| order_id | status | total | paid | unpaid |
+----------+-----------+-------+------+--------+
| 5 | cancelled | 18500 | 0 | 18500 |
| 13 | pending | 15000 | 0 | 15000 |
+----------+-----------+-------+------+--------+মাত্র দুটো অর্ডারের হিসাব মেলে না, আর দুটোই প্রত্যাশিত: বাতিল অর্ডারের টাকা কখনো আসেনি, আর pending অর্ডারটা এখনো টাকার অপেক্ষায় (১৫,০০০ টাকা ঝুঁকিতে)। বিক্রি হওয়া প্রতিটা অর্ডারের টাকা পুরো পাওয়া গেছে। আসল দোকানে এই কোয়েরি প্রতি রাতে চলত, আর এতে অপ্রত্যাশিত কিছু এলেই অ্যালার্ট বাজত।
ধাপ ৮: Python স্ক্রিপ্ট হিসেবে রিপোর্ট
নোটবুকে ছড়ানো কোয়েরি এখনো রিপোর্ট নয়। এগুলো একটা স্ক্রিপ্টে রাখুন, যাতে এক কমান্ডে পুরো রিপোর্ট আবার চলে, আর প্রতিটা ফলাফল এমন CSV-তে সংরক্ষণ করুন, যা স্প্রেডশিট, চার্ট বা ড্যাশবোর্ড পড়তে পারে। shop.db-এর পাশে এটা report.py নামে সংরক্ষণ করুন:
"""BitByte Shop sales report: run the analysis queries and save each result as CSV."""
import sqlite3
from pathlib import Path
import pandas as pd
SOLD = "o.status IN ('delivered', 'shipped')" # ব্যবসার নিয়ম, একবারই লেখা
QUERIES = {
"monthly_revenue": f"""
SELECT strftime('%Y-%m', o.order_date) AS month,
COUNT(DISTINCT o.order_id) AS orders,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE {SOLD}
GROUP BY month
ORDER BY month""",
"top_products": f"""
SELECT p.name, SUM(oi.quantity) AS units,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items AS oi
JOIN orders AS o ON o.order_id = oi.order_id
JOIN products AS p ON p.product_id = oi.product_id
WHERE {SOLD}
GROUP BY p.product_id, p.name
ORDER BY revenue DESC, p.product_id
LIMIT 5""",
"best_customers": f"""
SELECT c.name, COUNT(DISTINCT o.order_id) AS orders,
SUM(oi.quantity * oi.unit_price) AS revenue,
MAX(o.order_date) AS last_order
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE {SOLD}
GROUP BY c.customer_id, c.name
ORDER BY revenue DESC, c.customer_id""",
"payment_mix": """
SELECT method, COUNT(*) AS payments, SUM(amount) AS amount
FROM payments
GROUP BY method
ORDER BY amount DESC, method""",
}
def run_report(db_path="shop.db", out_dir="report"):
out = Path(out_dir)
out.mkdir(exist_ok=True)
con = sqlite3.connect(db_path)
try:
results = {}
for name, sql in QUERIES.items():
df = pd.read_sql_query(sql, con)
df.to_csv(out / f"{name}.csv", index=False)
results[name] = df
print(f"{name:16} {len(df):2} rows -> {out / name}.csv")
# মিলিয়ে দেখা: আইটেম থেকে পাওয়া আয় = বিক্রি হওয়া অর্ডারে পাওয়া টাকা
paid = pd.read_sql_query(
f"SELECT SUM(p.amount) AS paid FROM payments AS p "
f"JOIN orders AS o ON o.order_id = p.order_id WHERE {SOLD}", con
)["paid"].iloc[0]
revenue = results["monthly_revenue"]["revenue"].sum()
assert revenue == paid, f"revenue {revenue} does not match payments {paid}"
print(f"check passed: revenue {revenue} = payments {paid}")
return results
finally:
con.close()
if __name__ == "__main__":
report = run_report()
print()
print(report["top_products"].to_string(index=False))monthly_revenue 6 rows -> report/monthly_revenue.csv
top_products 5 rows -> report/top_products.csv
best_customers 6 rows -> report/best_customers.csv
payment_mix 3 rows -> report/payment_mix.csv
check passed: revenue 70050 = payments 70050
name units revenue
27-inch Monitor 1 18500
AI Engineering Bootcamp 1 15000
Mechanical Keyboard 2 8550
SQL Masterclass 2 6000
Hands-On Machine Learning 2 5000চালান এভাবে:
python report.pyযে সিদ্ধান্তগুলো নিজের কোডেও নেওয়ার মতো:
- নিয়ম এক জায়গায়।
SOLDএকবারই লেখা, তাই বিক্রির সংজ্ঞা বদলালে সব টেবিল একসাথে বদলায়। এটা আপনার নিজের লেখা, কখনো না বদলানো একটা SQL টুকরো, তাই এখানে f-string চলে; কিন্তু ব্যবহারকারীর টাইপ করা মান সবসময় প্যারামিটার হিসেবে পাঠাতে হবে, কখনো SQL-এর ভেতর বসিয়ে নয় (কেন, তা অ্যাডভান্সড টিউটোরিয়ালে হাতে-কলমে দেখবেন)। - রিপোর্ট নিজেই নিজেকে যাচাই করে। আয় আর পেমেন্ট কখনো না মিললে
assertস্ক্রিপ্ট থামিয়ে দেয়, তাই ভাঙা সংখ্যা চুপচাপ ম্যানেজারের কাছে পৌঁছায় না। - সাধারণ CSV আউটপুট। সব টুল এটা পড়তে পারে: Excel, Google Sheets, pandas, BI ড্যাশবোর্ড, ট্রেনিং পাইপলাইন।
- কানেকশন বন্ধ হয়
finally-তে, কোনো কোয়েরি ব্যর্থ হলেও।
কী লেখা হলো, দেখুন:
print(Path("report/monthly_revenue.csv").read_text())month,orders,revenue
2026-01,2,6600
2026-02,2,7000
2026-03,3,21400
2026-04,2,23450
2026-05,2,8500
2026-06,1,3100CSV থেকে চার্ট
চটজলদি একটা টেক্সট চার্ট বানাতে কোনো লাইব্রেরিই লাগে না — টার্মিনালে বা লগে এটা বেশ কাজে আসে:
monthly = pd.read_csv("report/monthly_revenue.csv")
for row in monthly.itertuples():
bar = "#" * round(row.revenue / 1000) # প্রতি 1,000 টাকায় একটা #
print(f"{row.month} {bar:24} {row.revenue:>6}")2026-01 ####### 6600
2026-02 ####### 7000
2026-03 ##################### 21400
2026-04 ####################### 23450
2026-05 ######## 8500
2026-06 ### 3100আসল চার্টের জন্য Matplotlib ব্যবহার করুন, যেমনটা Python for AI-এর Matplotlib দিয়ে চার্ট পাতায় দেখেছেন; নিচের কোড CSV ফাইলগুলোর পাশে একটা বার চার্ট PNG হিসেবে সেভ করে:
import matplotlib.pyplot as plt
monthly = pd.read_csv("report/monthly_revenue.csv")
ax = monthly.plot.bar(x="month", y="revenue", legend=False, rot=0)
ax.set_title("BitByte Shop revenue by month (taka)")
plt.tight_layout()
plt.savefig("report/monthly_revenue.png")বাস্তবে কোথায় দেখবেন
- ড্যাশবোর্ড (Metabase, Power BI, Looker Studio) মানে ঠিক এই কোয়েরিগুলো, নির্দিষ্ট সময় পরপর চালানো আর চার্টে আঁকা।
- ML ফিচার: ধাপ ৪-এর প্রতি-কাস্টমার টেবিল দিয়েই চার্ন বা LTV ডেটাসেটের শুরু; প্রতি-প্রোডাক্ট টেবিল চাহিদার পূর্বাভাস আর রেকমেন্ডেশনে কাজে লাগে।
- AI প্রোডাক্টের অ্যানালিটিক্স: অর্ডারের জায়গায় LLM কল আর আয়ের জায়গায় টোকেন বা খরচ বসান — একই মাসভিত্তিক, মডেলভিত্তিক আর ব্যবহারকারীভিত্তিক কোয়েরি বলে দেবে একটা AI ফিচার কোথায় টাকা খরচ করছে।
সাধারণ ভুল
- বাতিল অর্ডারকে আয় ধরা। status ফিল্টার না দিলে ফেব্রুয়ারি ১৮,৫০০ টাকা (বাতিল মনিটর) আর জুন ১৫,০০০ টাকা (টাকা না পাওয়া pending অর্ডার) বেশি দেখায়। সমাধান: আগে নিয়মে একমত হন, তারপর প্রতিটা কোয়েরিতে তা প্রয়োগ করুন (কোডে একটাই
SOLD)। - আয়ের জন্য
products.priceব্যবহার। এতে ছাড় আর দামের পরিবর্তন হারিয়ে যায়; কিবোর্ড বিক্রি হয়েছে ৪,০৫০-এ, ৪,৫০০-তে নয়। সমাধান:SUM(oi.quantity * oi.unit_price)। - আইটেম আর পেমেন্ট একসাথে জয়েন করা। অর্ডার ৬-এ একটা আইটেম আর দুটো পেমেন্ট, তাই আইটেমের মূল্য দুবার গোনা হয়। সমাধান: প্রতিটা পাশ নিজের সাবকোয়েরিতে অ্যাগ্রিগেট করুন, তারপর টোটালগুলো জয়েন করুন।
- INNER JOIN-এ সারি হারানো। ক্যাটাগরি টেবিল থেকে Furniture আর কাস্টমার তালিকা থেকে Karim উধাও হয়ে যান। সমাধান: যে টেবিলের সব সারি দেখাতেই হবে, সেখান থেকে শুরু করুন আর বাকিগুলো
LEFT JOINকরুন, সাথেCOALESCE(…, 0)। - মিলিয়ে না দেখে সংখ্যায় ভরসা করা। সমাধান: টোটালগুলো মেলান — মাসের যোগফল = ক্যাটাগরির যোগফল = মূল আয় = বিক্রি হওয়া অর্ডারের পেমেন্ট।
নিজে চেষ্টা করুন
- সহজ: বিক্রি হওয়া অর্ডারের শহরভিত্তিক আয়, যেসব কাস্টমারের শহর নেই তাঁদের
Unknownহিসেবে দেখিয়ে, বড় থেকে ছোট। - সহজ: একটা বিক্রি হওয়া অর্ডারে গড়ে কয়টা আইটেম (quantity-র যোগফল) থাকে? দশমিকের পর ২ ঘর পর্যন্ত রাউন্ড করুন।
- মাঝারি: মাস × ক্যাটাগরির একটা টেবিল: প্রতি মাসে একটা সারি, আর Books, Electronics, Courses ও Accessories-এর আয়ের জন্য একটা করে কলাম (শর্তসাপেক্ষ অ্যাগ্রিগেশন, দেখুন CASE WHEN, শর্তসাপেক্ষ গণনা আর ব্যবসার প্রশ্ন)।
- মাঝারি:
report.py-এরQUERIES-এ একটাcategory_revenueকোয়েরি যোগ করে রিপোর্টটা আবার চালান। - কঠিন: বিক্রি হওয়া প্রতিটা অর্ডারের জন্য অর্ডারের তারিখ থেকে পুরো টাকা পাওয়ার তারিখ (শেষ পেমেন্ট) পর্যন্ত কত দিন লেগেছে। শুধু সেই অর্ডারগুলো দেখান, যেগুলোর টাকা অর্ডারের দিনের পরে এসেছে।
উত্তর
-- 1. শহরভিত্তিক আয়
SELECT COALESCE(c.city, 'Unknown') AS city,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY COALESCE(c.city, 'Unknown')
ORDER BY revenue DESC, city;
-- 2. বিক্রি হওয়া অর্ডারে গড় আইটেম
SELECT ROUND(AVG(items), 2) AS avg_items
FROM (
SELECT oi.order_id, SUM(oi.quantity) AS items
FROM order_items AS oi
JOIN orders AS o ON o.order_id = oi.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY oi.order_id
) AS t;
-- 3. মাস x ক্যাটাগরি আয়
SELECT strftime('%Y-%m', o.order_date) AS month,
SUM(CASE WHEN p.category_id = 1 THEN oi.quantity * oi.unit_price ELSE 0 END) AS books,
SUM(CASE WHEN p.category_id = 2 THEN oi.quantity * oi.unit_price ELSE 0 END) AS electronics,
SUM(CASE WHEN p.category_id = 3 THEN oi.quantity * oi.unit_price ELSE 0 END) AS courses,
SUM(CASE WHEN p.category_id = 4 THEN oi.quantity * oi.unit_price ELSE 0 END) AS accessories
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN products AS p ON p.product_id = oi.product_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY month
ORDER BY month;
-- 5. অর্ডার থেকে পুরো টাকা পাওয়া পর্যন্ত দিন
SELECT o.order_id, o.order_date, MAX(pay.paid_on) AS fully_paid,
CAST(julianday(MAX(pay.paid_on)) - julianday(o.order_date) AS INTEGER) AS days
FROM orders AS o
JOIN payments AS pay ON pay.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY o.order_id, o.order_date
HAVING MAX(pay.paid_on) > o.order_date
ORDER BY days DESC, o.order_id;# 4. QUERIES-এ আরেকটা এন্ট্রি, তারপর রিপোর্ট আবার চালান
QUERIES["category_revenue"] = f"""
SELECT COALESCE(cat.name, 'Uncategorised') AS category,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items AS oi
JOIN orders AS o ON o.order_id = oi.order_id
JOIN products AS p ON p.product_id = oi.product_id
LEFT JOIN categories AS cat ON cat.category_id = p.category_id
WHERE {SOLD}
GROUP BY category
ORDER BY revenue DESC, category"""
report = run_report()
print(report["category_revenue"].to_string(index=False))সারসংক্ষেপ
- রিপোর্ট শুরু হয় লিখিত নিয়ম দিয়ে: কোনটা বিক্রি, আয় মানে কী, কোন তারিখ দিয়ে মাস ঠিক হবে।
- আয় হলো বিক্রি হওয়া অর্ডারের
SUM(quantity * unit_price); জয়েনের পর অর্ডার গুনুনCOUNT(DISTINCT order_id)দিয়ে। - যে টেবিলের সব সারি দেখাতে হবে সেখান থেকে শুরু করে বাকিগুলো
LEFT JOINকরুন; one-to-many দিকের টেবিলগুলো জয়েনের আগে আলাদা আলাদা অ্যাগ্রিগেট করুন। - টোটাল মিলিয়ে দেখুন: মাস, ক্যাটাগরি, মূল সংখ্যা আর পেমেন্ট — সব মিলতে হবে।
- কোয়েরির একটা ডিকশনারি,
pd.read_sql_queryআরto_csv— এই তিনে মিলে একটা Python স্ক্রিপ্ট কোয়েরিগুলোকে এমন রিপোর্টে পরিণত করে, যা যতবার খুশি চালানো যায় আর যা নিজেই নিজের হিসাব যাচাই করে।
এরপর: সারসংক্ষেপ, চিট শিট আর এরপর কী পাতায় পুরো বিগিনার টিউটোরিয়ালের সিনট্যাক্স এক জায়গায় পাবেন, আর দেখবেন অ্যাডভান্সড টিউটোরিয়াল আপনাকে কোথায় নিয়ে যায় — শুরুতেই CTE, যা এই পাতার লম্বা কোয়েরিগুলোকে অনেক সহজে পড়ার মতো করে তোলে।