EduFlow
Python لتحليل البيانات
⁦0%⁩

الوحدة 4 · التحليل: التجميع والدمج وإعادة التشكيل

السلاسل الزمنية: resample والـ accessor المسمّى dt

20 دقراءة 4 دتمرين Python

الدرس في لمحة

حوّل الطلبات ذات الطوابع الزمنية إلى سلاسل شهرية وفصلية وأسبوعية باستخدام resample، وقارن الفترات بـ pct_change، وخفّف الضوضاء بـ rolling، واستخرج أجزاء التقويم بالـ accessor المسمّى dt.

  1. 1resample: حاويات من الزمن
  2. 2مقارنة الفترات
  3. 3الـ accessor المسمّى dt: أجزاء التقويم

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

الأرقام الشهرية التي استخدمتها منذ الدرس 1.3 جاءت من مكان ما: بضعة آلاف من الطلبات ذات الطوابع الزمنية، موزّعة بحسب الشهر التقويمي. الزمن هو محور كل سؤال عمل تقريبًا (“هل ينمو الإيراد؟”، “هل نوفمبر هذا العام طبيعي؟”، “متى يتسوّق الناس؟”)، ولدى pandas أدوات مبنية له. وقبل استخدامها، تحتاج صفًا لكل طلب مع إيراده وطابعه الزمني.

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
order_revenue = items.groupby("order_id", as_index=False)["revenue"].sum()

orders = orders.merge(order_revenue, on="order_id", how="left", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
print(orders[["order_id", "order_date", "revenue"]].head(3))

كل كتلة Run فيما يلي تبدأ بهذه الأسطر، مختصرة لتبقى مقروءة. وفي الـ notebook الخاص بك كنت ستشغّلها مرة واحدة.

resample: حاويات من الزمن

تجمّع resample الصفوف في فترات زمنية منتظمة، وهو ما نسمّيه إعادة التجميع الزمني. وتحتاج أن تكون التواريخ في الفهرس:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]

by_time = orders.set_index("order_date")
monthly = by_time.resample("ME")["revenue"].sum()
quarterly = by_time.resample("QE")["revenue"].sum()
print(len(monthly), "months")
print(monthly.tail(4).round(0))
print(quarterly.round(0))

النص اسم مستعار للتكرار (frequency alias): "D" للأيام، و"W" للأسابيع المنتهية يوم الأحد، و"ME" لنهايات الأشهر، و"QE" لنهايات الأرباع، و"YE" لنهايات السنوات. أعادت pandas 2.2 تسمية "M" و"Q" و"Y" إلى نسخ الـ E لتوضّح أن التسميات هي نهايات الفترات، والأسماء القديمة تُطلق الآن تحذير إهمال. أما الفترات (periods) فكائن مختلف يحتفظ بالأسماء القصيرة: to_period("M") وto_period("Q") صحيحتان، لأن الفترة تمثّل الشهر أو الربع كله لا آخر يوم فيه. والسلسلة الفصلية هي التي يحدّق فيها رئيس النمو: 80,523 دولارًا في الربع الثالث من 2025، ثم 146,825 في الربع الرابع.

الفرق بين resample و to_period

تستطيع أيضًا التجميع بحسب فترة: orders.groupby(orders["order_date"].dt.to_period("M"))["revenue"].sum(). تبدو النتيجة هنا متطابقة، بتسميات مثل 2025-11 بدلًا من 2025-11-30. ويظهر الفرق حين لا تكون في فترة ما بيانات: تشملها resample بمجموع 0، بينما يتخطّاها التجميع تمامًا. وللرسوم البيانية ومعدّلات النمو تريد كل فترة حاضرة، فتكون resample الخيار الافتراضي الأكثر أمانًا.

خط زمني من أكتوبر إلى ديسمبر عليه نقاط طلبات متفرّقة. خطوط عمودية تقسمه إلى ثلاث حاويات شهرية. كل حاوية مسمّاة بآخر يوم فيها: 2025-10-31 و 2025-11-30 و 2025-12-31، ومجموع الإيراد تحت كل تسمية. أكتوبر نوفمبر ديسمبر 2025-10-31 2025-11-30 2025-12-31 30,339 75,334 41,153
تضع resample كل طابع زمني في حاوية ثابتة وتسمّي الحاوية بنهايتها

مقارنة الفترات

السلاسل الخام تجيب عن “كم”؛ والتغيّرات تجيب عن “بأيّ سرعة”:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
by_time = orders.set_index("order_date")

quarterly = by_time.resample("QE")["revenue"].sum()
print(pd.DataFrame({
    "revenue": quarterly.round(0),
    "vs_prev_quarter": quarterly.pct_change().round(3),
    "vs_year_ago": quarterly.pct_change(4).round(3),
}))
monthly = by_time.resample("ME")["revenue"].sum()
print(monthly.rolling(3).mean().tail(3).round(0))

تقسم pct_change() كل قيمة على القيمة التي قبلها وتطرح 1. الربع الرابع من 2025 أعلى بنسبة 82% من الثالث، وهي القفزة التي لاحظها الجميع. لكن pct_change(4) تقارن كل ربع بالربع نفسه قبل سنة، وهذا العمود يحكي قصة أهدأ: الربع الرابع من 2025 أعلى بنسبة 180% من الربع الرابع من 2024، بينما نما الأول والثاني والثالث بنسب 200% و145% و165%. نما الربع الرابع بسرعة تقارب بقية السنة. جزء كبير من “القفزة” نمط موسمي كان موجودًا في 2024 أيضًا: كان الربع الرابع من 2024 أعلى بنسبة 73% من الربع الثالث من 2024.

تحسب rolling(3).mean() متوسط كل شهر مع الشهرين اللذين قبله، أي المتوسط المتحرك، فتنعّم القفزات المفردة مثل نوفمبر. استخدمها لترى الاتجاه؛ واستخدم السلسلة الخام لترى الأحداث.

الـ accessor المسمّى dt: أجزاء التقويم

للأنماط داخل الفترة، مثل أيام الأسبوع أو ساعات اليوم، استخرج الجزء بـ .dt وجمّع بحسبه:

Python
import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
orders = orders[orders["status"] != "cancelled"]
d = orders["order_date"]
print(orders.groupby(d.dt.day_name()).size().sort_values())
print(orders.groupby(d.dt.hour).size().head(4))
print(d.dt.to_period("Q").value_counts().sort_index().tail(2))

الطلبات موزّعة بالتساوي تقريبًا على أيام الأسبوع، من 241 يوم الاثنين إلى 298 يومي الثلاثاء والسبت، فلا يوجد أثر قوي ليوم الأسبوع يحتاج تفسيرًا. أجزاء مفيدة أخرى: dt.year وdt.quarter وdt.month وdt.isocalendar().week وdt.date وdt.normalize() (منتصف ليل اليوم نفسه، وقد استخدمتها في الدرس 3.4).

إعادة التجميع اليومية هي المكان الذي تُختبر فيه الافتراضات. تعطي by_time.resample("D").size() عددًا لكل يوم تقويمي، 729 يومًا من أول طلب إلى آخره، وتظهر حقيقتان دفعة واحدة: 119 يومًا لم تشهد أي طلب (وفي سلسلة مبنية بـ groupby كانت تلك الأيام ستغيب ببساطة)، وأكثر يوم ازدحامًا كان 10 نوفمبر 2025 بـ 16 طلبًا، قبل الجمعة السوداء (28 نوفمبر) بنحو ثلاثة أسابيع. والأيام الأعلى الأخرى، 6 و8 و26 نوفمبر، موزّعة على الشهر أيضًا. أيًّا كان ما دفع نوفمبر، فلم يكن يومًا ترويجيًا كبيرًا واحدًا؛ تذكّر ذلك في دراسة الحالة.

في التمرين تبني السلسلة الشهرية ونظرة “سنة مقابل سنة” بنفسك. وفي الدرس التالي ترتّب العملاء وتقسّمهم إلى فئات، فتحوّل مقياسًا متصلًا مثل إجمالي الإنفاق إلى شرائح.

أهم النقاط

  • df.set_index("order_date").resample("ME")["revenue"].sum() توزّع الصفوف على الأشهر التقويمية؛ و"QE" و"W" و"D" تعطي الأرباع والأسابيع والأيام.
  • في pandas 2.2 وما بعدها، تكتب resample نهايات الأشهر والأرباع "ME" و"QE"؛ أما الاسمان القديمان "M" و"Q" فمُهمَلان (deprecated) هناك، بينما تحتفظ الفترات مثل to_period("M") بالأسماء القصيرة.
  • تشمل resample الفترات الفارغة (الشهر الذي بلا طلبات يظهر بـ 0)، بينما التجميع بحسب dt.to_period("M") لا يسرد إلا الفترات التي فيها بيانات.
  • تقارن pct_change() كل فترة بالتي قبلها، وpct_change(4) على الأرباع تقارن بالربع نفسه قبل سنة.
  • تسميات resample تحدّد نهاية كل حاوية زمنية، فقد تعرض سلسلة أسبوعية تسمية من 2026 لطلبات قُدّمت في ديسمبر 2025؛ افحص الفترتين الأولى والأخيرة بحثًا عن بيانات جزئية.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

الإيراد الفصلي موجود في q، الأقدم أولًا. أيّ تعبير يعطي نمو كل ربع مقارنةً بالربع نفسه قبل سنة؟

السؤال 2 من 3

عدد الطلبات الأسبوعي المحسوب بـ resample("W") ينتهي بأسبوع تسميته 2026-01-04 فيه 11 طلبًا فقط. ما الذي يحدث؟

السؤال 3 من 3

أيّ كود يعطي عدد الطلبات لكل اسم يوم من أيام الأسبوع؟

تدريب

تمرين Python

شهريًا، وفصليًا، وسنة مقابل سنة

في orders صف لكل طلب غير ملغى، مع order_date وrevenue. ابنِ monthly (الإيراد لكل شهر تقويمي، بتسميات نهاية الشهر) وmonthly_orders (عدد الطلبات لكل شهر) باستخدام resample. وابنِ quarterly (الإيراد لكل ربع) وyoy، نموّه مقارنةً بالربع نفسه قبل سنة ككسر (1.0 تعني ‎+100%). وأخيرًا اجعل best_weekday اسم يوم الأسبوع صاحب أعلى إيراد إجمالي.

ملفات البيانات: order_items.csv, orders.csv

import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
order_revenue = items.groupby("order_id", as_index=False)["revenue"].sum()
orders = orders.merge(order_revenue, on="order_id", how="left", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]

# 1. Monthly revenue (Series, one value per calendar month, month-end labels)
monthly = None

# 2. Monthly number of orders
monthly_orders = None

# 3. Quarterly revenue, and its growth versus the same quarter a year earlier
quarterly = None
yoy = None

# 4. Weekday name with the highest total revenue, e.g. "Monday"
best_weekday = None
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

(لا مخرجات)

الفحوص

  • في monthly 24 شهرًا، ونوفمبر 2025 نحو 75,334
  • تعرض monthly_orders 325 طلبًا في نوفمبر 2025
  • في quarterly 8 أرباع، تنتهي عند نحو 146,825
  • yoy تساوي 1.795 للربع الرابع من 2025 وفارغة لعام 2024
  • best_weekday هو يوم الأسبوع الأعلى إيرادًا