Kodax

مشروع: تحليل ملف مبيعات — هياكل البيانات

مشروع: تحليل ملف مبيعات

مشروع يحاكي مهمة حقيقية يقوم بها المبرمجون يومياً: لديك ملف بيانات خام فيه أخطاء، والمطلوب تقرير واضح منه.

البيانات

ملف sales.csv بالأعمدة: date, product, category, quantity, price. بعض الصفوف معطوبة: كمية نصية، سعر فارغ، مسافات زائدة في أسماء المنتجات.

المطلوب

1. قراءة الملف بـ csv.DictReader.

2. تنظيف كل صف: حذف المسافات، تحويل الكمية لـ int والسعر لـ float.

3. تجاهل الصفوف المعطوبة دون إيقاف البرنامج، مع عدّها.

4. حساب: إجمالي المبيعات، المبيعات لكل فئة، أكثر 3 منتجات مبيعاً بالكمية.

5. كتابة التقرير في report.json.

المفاهيم المستخدمة

| الخطوة | المفهوم |

|---|---|

| القراءة | الملفات، csv، with |

| التنظيف | strip، التحويل، try/except ValueError |

| التجميع | defaultdict، Counter |

| الترتيب | sorted مع key، most_common |

| الإخراج | json.dump مع ensure_ascii=False |

| التنظيم | دوال صغيرة، if __name__ == "__main__" |

التصميم

- parse_row(row): ترجع قاموساً نظيفاً أو ترمي ValueError.

- load_sales(path): ترجع (الصفوف الصالحة، عدد المعطوبة).

- summarize(sales): ترجع قاموس التقرير.

- main(): تربط كل شيء.

لماذا ترمي parse_row خطأً بدلاً من طباعة رسالة؟ لأنها لا تعرف ماذا يريد مستدعيها: ربما يريد تجاهل الصف، أو إيقاف البرنامج، أو تسجيله. الدالة التي تكتشف المشكلة ليست دائماً التي تقرر كيف تتعامل معها.

نقطة تصميم: لا تثق بالبيانات

افترض دائماً أن البيانات القادمة من ملف أو مستخدم قد تكون خاطئة. التحقق في مكان واحد (parse_row) يجعل بقية الكود بسيطاً لأنه يتعامل مع بيانات نظيفة مضمونة.

أفكار للتوسعة

- المبيعات حسب الشهر (من عمود التاريخ).

- قراءة اسم الملف من سطر الأوامر.

- حفظ الصفوف المعطوبة في ملف منفصل للمراجعة.

ما الذي تتقنه بعد هذا المشروع؟

تحويل بيانات غير مضمونة إلى نتائج موثوقة، وهي مهارة أساسية في تحليل البيانات وتطوير الخلفيات. الخطوة التالية: دورة البرمجة الكائنية لتنظيم البرامج الأكبر.

import csv
import json
from collections import Counter, defaultdict
from pathlib import Path

SAMPLE = """date,product,category,quantity,price
2026-01-03, قهوة ,مشروبات,3,4.5
2026-01-03,شاي,مشروبات,5,1.25
2026-01-04,كرواسون,مخبوزات,2,2.0
2026-01-04,قهوة,مشروبات,abc,4.5
2026-01-05,كعكة,مخبوزات,1,
2026-01-05,شاي,مشروبات,4,1.25
2026-01-06,كرواسون,مخبوزات,6,2.0
"""

def parse_row(row):
    """ترجع صفاً نظيفاً أو ترمي ValueError إذا كان معطوباً."""
    product = row["product"].strip()
    if not product:
        raise ValueError("اسم منتج فارغ")
    return {
        "product": product,
        "category": row["category"].strip(),
        "quantity": int(row["quantity"]),
        "price": float(row["price"]),
    }

def load_sales(path):
    valid, broken = [], 0
    with open(path, newline="", encoding="utf-8") as f:
        for row in csv.DictReader(f):
            try:
                valid.append(parse_row(row))
            except (ValueError, KeyError):
                broken += 1
    return valid, broken

def summarize(sales):
    total = 0.0
    by_category = defaultdict(float)
    quantities = Counter()
    for s in sales:
        amount = s["quantity"] * s["price"]
        total += amount
        by_category[s["category"]] += amount
        quantities[s["product"]] += s["quantity"]
    return {
        "total": round(total, 2),
        "by_category": {k: round(v, 2) for k, v in sorted(by_category.items(), key=lambda kv: -kv[1])},
        "top_products": quantities.most_common(3),
    }

def main():
    path = Path("sales.csv")
    path.write_text(SAMPLE, encoding="utf-8")
    sales, broken = load_sales(path)
    report = summarize(sales)
    report["broken_rows"] = broken
    Path("report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps(report, ensure_ascii=False, indent=2))
    path.unlink()
    Path("report.json").unlink()

if __name__ == "__main__":
    main()
اختبر فهمك