الوحدة 4 · التحليل: التجميع والدمج وإعادة التشكيل
السلاسل الزمنية: resample والـ accessor المسمّى dt
20 دقراءة 4 دتمرين Python
الدرس في لمحة
حوّل الطلبات ذات الطوابع الزمنية إلى سلاسل شهرية وفصلية وأسبوعية باستخدام resample، وقارن الفترات بـ pct_change، وخفّف الضوضاء بـ rolling، واستخرج أجزاء التقويم بالـ accessor المسمّى dt.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
الأرقام الشهرية التي استخدمتها منذ الدرس 1.3 جاءت من مكان ما: بضعة آلاف من الطلبات ذات الطوابع الزمنية، موزّعة بحسب الشهر التقويمي. الزمن هو محور كل سؤال عمل تقريبًا (“هل ينمو الإيراد؟”، “هل نوفمبر هذا العام طبيعي؟”، “متى يتسوّق الناس؟”)، ولدى pandas أدوات مبنية له. وقبل استخدامها، تحتاج صفًا لكل طلب مع إيراده وطابعه الزمني.
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 الصفوف في فترات زمنية منتظمة، وهو ما نسمّيه إعادة التجميع الزمني. وتحتاج أن تكون التواريخ في الفهرس:
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 الخيار الافتراضي الأكثر أمانًا.
مقارنة الفترات
السلاسل الخام تجيب عن “كم”؛ والتغيّرات تجيب عن “بأيّ سرعة”:
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 وجمّع بحسبه:
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؛ افحص الفترتين الأولى والأخيرة بحثًا عن بيانات جزئية.