مشروع: تحليل ملف مبيعات — هياكل البيانات
مشروع: تحليل ملف مبيعات
مشروع يحاكي مهمة حقيقية يقوم بها المبرمجون يومياً: لديك ملف بيانات خام فيه أخطاء، والمطلوب تقرير واضح منه.
البيانات
ملف 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()
اختبر فهمك