ניתוח סדרות זמן בפייתון: מדריך מעשי עם pandas 3.0, matplotlib ו-statsmodels

מדריך מעשי לניתוח סדרות זמן בפייתון עם pandas 3.0. נעבור על טעינת נתונים, דגימה מחדש, חלונות מתגלגלים, פירוק עונתי, בדיקת סטציונריות ובניית מודל חיזוי ARIMA — הכל עם קוד שאפשר להריץ ישירות.

מהו ניתוח סדרות זמן ולמה זה כל כך חשוב?

סדרת זמן (Time Series) היא בעצם רצף של נקודות נתונים שמסודרות לפי ציר הזמן. מחירי מניות יומיים, טמפרטורות שעתיות, מספר כניסות לאתר בכל דקה, מכירות חודשיות — כל אלה הן סדרות זמן. והיכולת לנתח את הדפוסים שמסתתרים בתוך נתוני זמן ולחזות ערכים עתידיים? זה אחד הכלים החזקים ביותר שיש למדעני נתונים.

מה שהופך ניתוח סדרות זמן לשונה מניתוח נתונים רגיל הוא נקודה אחת פשוטה אבל מכרעת: סדר הנתונים חשוב. אי אפשר פשוט לערבב את השורות כמו שעושים עם נתונים טבלאיים רגילים. כל תצפית תלויה בתצפיות שקדמו לה, ויחסי התלות האלה — מגמות, עונתיות, מחזוריות — הם בדיוק מה שאנחנו מנסים לזהות ולנצל.

אז בואו ניגש לעניין. במדריך הזה נבצע ניתוח סדרות זמן מלא, צעד אחר צעד, עם pandas 3.0 (שיצאה בינואר 2026), matplotlib לוויזואליזציה, ו-statsmodels למודל חיזוי. כל דוגמת קוד ניתנת להרצה ישירה ב-Jupyter Notebook — אפשר להעתיק ולהריץ.

הכנת סביבת העבודה

התקנת הספריות הנדרשות

פתחו טרמינל או תא קוד ב-Jupyter והריצו:

pip install "pandas>=3.0" matplotlib statsmodels numpy pyarrow

למה pyarrow? זה שאלה שעולה הרבה. ב-pandas 3.0, טיפוס המחרוזות החדש (str) מבוסס על PyArrow מאחורי הקלעים. בנוסף, PyArrow מעניק שיפורי ביצועים רציניים לעבודה עם נתוני datetime — וזה קריטי כשעובדים עם סדרות זמן.

ייבוא הספריות

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.arima.model import ARIMA

plt.rcParams["figure.figsize"] = (12, 5)
plt.rcParams["figure.dpi"] = 100

print(f"pandas version: {pd.__version__}")

שינויי pandas 3.0 שכדאי להכיר לפני שמתחילים

לפני שצוללים לתוך העבודה המעשית, יש שלושה שינויים ב-pandas 3.0 שחייבים לדעת. מניסיון אישי, התעלמות מהם עלתה לי בכמה שעות של דיבוג מיותר.

1. רזולוציית ברירת מחדל: מיקרו-שניות במקום ננו-שניות

עד pandas 2.x, כל המרה של תאריכים יצרה אובייקטים ברזולוציית ננו-שניות (datetime64[ns]). ב-pandas 3.0, ברירת המחדל עברה למיקרו-שניות (datetime64[us]).

למה זה משנה? השינוי הזה מרחיב את טווח התאריכים הנתמך. במקום להיות מוגבלים לשנים 1678–2262 (כן, זה היה הטווח), עכשיו אפשר לעבוד עם טווחים רחבים הרבה יותר.

# pandas 3.0 — רזולוציית מיקרו-שניות כברירת מחדל
dates = pd.to_datetime(["2026-01-15", "2026-06-20", "2026-12-31"])
print(dates.dtype)  # datetime64[us]

# אם צריכים ננו-שניות — ניתן לציין במפורש
dates_ns = dates.as_unit("ns")
print(dates_ns.dtype)  # datetime64[ns]

2. Copy-on-Write כברירת מחדל

זה אולי השינוי שהכי ישפיע על הקוד היומיומי שלכם. ב-pandas 3.0, התנהגות Copy-on-Write (CoW) פעילה כברירת מחדל. בפועל, כל פעולת חיתוך על DataFrame מחזירה אובייקט שמתנהג כעותק — שינויים בתוצאת החיתוך לא ישפיעו על ה-DataFrame המקורי. זה מונע באגים נפוצים ומתסכלים בעבודה עם סדרות זמן.

# ב-pandas 3.0 — החיתוך מתנהג תמיד כעותק
df_subset = df.loc["2026-01":"2026-06"]
df_subset["value"] = 0  # לא ישנה את df המקורי

3. טיפוס str חדש למחרוזות

עמודות טקסט מזוהות כעת אוטומטית כ-str (מבוסס PyArrow) במקום object. כשטוענים CSV עם עמודות תאריך כטקסט, pandas יזהה אותן כ-str — מה שיכול להשפיע על תהליך ההמרה ל-datetime. שווה לשים לב לזה.

שלב 1: טעינת ויצירת נתוני סדרות זמן

בואו ניצור דאטהסט סינתטי של מכירות יומיות עם מגמה ועונתיות. זה מושלם ללימוד כי אנחנו שולטים בכל הרכיבים ויודעים מה לצפות לראות:

np.random.seed(42)

# יצירת טווח תאריכים של 3 שנים
dates = pd.date_range(start="2023-01-01", end="2025-12-31", freq="D")

# יצירת רכיבי הסדרה
n = len(dates)
trend = np.linspace(100, 250, n)                    # מגמה עולה
seasonality = 30 * np.sin(2 * np.pi * np.arange(n) / 365.25)  # עונתיות שנתית
noise = np.random.normal(0, 10, n)                   # רעש

sales = trend + seasonality + noise

df = pd.DataFrame({"date": dates, "sales": sales})
df = df.set_index("date")

print(df.head(10))
print(f"\nresolution: {df.index.dtype}")  # datetime64[us] ב-pandas 3.0
print(f"shape: {df.shape}")

שימו לב שב-pandas 3.0 האינדקס נוצר ברזולוציית מיקרו-שניות. אם יש לכם קוד ישן שמצפה לננו-שניות, ההמרה פשוטה:

# המרת רזולוציה אם נדרש
df.index = df.index.as_unit("ns")  # חזרה לננו-שניות

טעינת נתונים מקובץ CSV

במציאות, רוב הזמן נטען נתונים מקובץ ולא ניצור אותם. כשטוענים סדרת זמן מ-CSV, יש לדאוג לשני דברים: המרת עמודת התאריך ל-datetime והגדרתה כאינדקס.

# טעינה עם המרה אוטומטית
df = pd.read_csv(
    "sales_data.csv",
    parse_dates=["date"],
    index_col="date"
)

# לחלופין, המרה ידנית
df = pd.read_csv("sales_data.csv")
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date")
df = df.sort_index()  # תמיד חשוב לוודא מיון לפי תאריך

שלב 2: סקירה ראשונית של הנתונים

כמו בכל ניתוח נתונים, שלב ראשון — להכיר את מה שיש לנו:

# מידע כללי
print(df.info())
print(f"\nטווח תאריכים: {df.index.min()} עד {df.index.max()}")
print(f"מספר ימים: {len(df)}")
print(f"ימים חסרים: {pd.date_range(df.index.min(), df.index.max()).difference(df.index).size}")

# סטטיסטיקה תיאורית
print(df.describe())

בדיקת ימים חסרים? קריטית. פערים בנתונים יכולים להרוס לכם כל ניתוח — מ-resampling ועד מודלים סטטיסטיים. גיליתי את זה בדרך הקשה יותר מפעם אחת.

טיפול בערכי זמן חסרים

בסדרות זמן, מילוי ערכים חסרים שונה ממה שרגילים אליו בנתונים טבלאיים. אינטרפולציה היא לרוב הגישה הטובה ביותר כי היא שומרת על הרציפות הטבעית של הסדרה:

# אינטרפולציה לינארית — מתאימה לסדרות רציפות
df["sales"] = df["sales"].interpolate(method="linear")

# אינטרפולציה לפי זמן — מתחשבת ברווחי זמן לא אחידים
df["sales"] = df["sales"].interpolate(method="time")

# forward fill — מתאים לנתונים מדורגים
df["sales"] = df["sales"].ffill()

שלב 3: חיתוך וסינון לפי תאריכים

אחד הדברים שאני הכי אוהב ב-DatetimeIndex של pandas הוא היכולת לחתוך נתונים בצורה אינטואיטיבית לפי תאריכים. פשוט כותבים מחרוזת תאריך ו-pandas מבין:

# חיתוך לפי שנה
df_2024 = df.loc["2024"]

# חיתוך לפי חודש
jan_2025 = df.loc["2025-01"]

# חיתוך לפי טווח
q1_2025 = df.loc["2025-01":"2025-03"]

# סינון לפי יום בשבוע (0=שני, 6=ראשון)
weekdays_only = df[df.index.dayofweek < 5]

print(f"2024 בלבד: {len(df_2024)} שורות")
print(f"ינואר 2025: {len(jan_2025)} שורות")
print(f"רבעון 1, 2025: {len(q1_2025)} שורות")

שלב 4: דגימה מחדש (Resampling)

דגימה מחדש מאפשרת לשנות את תדירות הנתונים, וזו בהחלט אחת הפונקציות שהכי תשתמשו בהן בעבודה עם סדרות זמן.

צמצום תדירות (Downsampling)

# מיומי לשבועי — ממוצע
weekly = df.resample("W").mean()

# מיומי לחודשי — סכום
monthly_sum = df.resample("ME").sum()

# מיומי לרבעוני — חציון
quarterly = df.resample("QE").median()

print("נתונים שבועיים:")
print(weekly.head())

נקודה חשובה: ב-pandas 3.0, השתמשו ב-"ME" (Month End) במקום "M" וב-"QE" (Quarter End) במקום "Q". הסימונים הישנים הוצאו משימוש ויגרמו לאזהרות (או שגיאות).

הגדלת תדירות (Upsampling)

# מיומי לשעתי
hourly = df.resample("h").interpolate(method="linear")
print(hourly.head(24))

שלב 5: חלונות מתגלגלים וממוצעים נעים

ממוצע נע (Moving Average) הוא כנראה הטכניקה הראשונה שכדאי ללמוד לניתוח סדרות זמן. הרעיון פשוט — מחליקים את הרעש כדי לחשוף מגמות שמסתתרות מתחתיו:

# ממוצע נע של 7 ימים
df["ma_7"] = df["sales"].rolling(window=7).mean()

# ממוצע נע של 30 ימים
df["ma_30"] = df["sales"].rolling(window=30).mean()

# סטיית תקן מתגלגלת — מזהה תקופות של תנודתיות גבוהה
df["rolling_std"] = df["sales"].rolling(window=30).std()

# ממוצע נע אקספוננציאלי — נותן משקל גבוה יותר לתצפיות אחרונות
df["ema_7"] = df["sales"].ewm(span=7).mean()

ויזואליזציה של הממוצעים הנעים

fig, ax = plt.subplots(figsize=(14, 6))

ax.plot(df.index, df["sales"], alpha=0.3, label="Sales (daily)")
ax.plot(df.index, df["ma_7"], label="MA 7 days", linewidth=1.5)
ax.plot(df.index, df["ma_30"], label="MA 30 days", linewidth=2, color="red")

ax.set_title("Daily Sales with Moving Averages")
ax.set_xlabel("Date")
ax.set_ylabel("Sales")
ax.legend()
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

שלב 6: פירוק סדרת הזמן (Decomposition)

כאן הדברים מתחילים להיות באמת מעניינים. פירוק סדרת זמן לוקח את הסדרה ומפרק אותה לשלושה רכיבים נפרדים: מגמה (trend), עונתיות (seasonality), ושארית (residual). זה נותן לכם תמונה ברורה מאוד של מה בעצם מניע את השינויים בנתונים.

decomposition = seasonal_decompose(
    df["sales"],
    model="additive",
    period=365  # עונתיות שנתית
)

fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True)

decomposition.observed.plot(ax=axes[0], title="Observed")
decomposition.trend.plot(ax=axes[1], title="Trend")
decomposition.seasonal.plot(ax=axes[2], title="Seasonality")
decomposition.resid.plot(ax=axes[3], title="Residuals")

for ax in axes:
    ax.set_xlabel("")

plt.tight_layout()
plt.show()

טיפ מהניסיון: אם רכיב השארית (Residuals) נראה אקראי בלי דפוס ברור — זה סימן מצוין. זה אומר שהמגמה והעונתיות לוכדות את רוב המידע בסדרה. אם כן רואים דפוס בשארית, כנראה שחסר לכם רכיב כלשהו במודל.

שלב 7: בדיקת סטציונריות

רוב המודלים הסטטיסטיים לסדרות זמן (כולל ARIMA שנבנה בהמשך) מניחים שהסדרה סטציונרית. מה זה אומר? שהממוצע, השונות ומבנה האוטוקורלציה לא משתנים לאורך הזמן. סדרה עם מגמה או עונתיות — פשוט לא עומדת בתנאי הזה.

מבחן Augmented Dickey-Fuller (ADF) הוא המבחן הסטנדרטי לנושא:

def test_stationarity(series, name=""):
    result = adfuller(series.dropna(), autolag="AIC")
    print(f"=== ADF Test: {name} ===")
    print(f"Test Statistic:  {result[0]:.4f}")
    print(f"p-value:         {result[1]:.4f}")
    print(f"Lags Used:       {result[2]}")
    print(f"Observations:    {result[3]}")
    if result[1] < 0.05:
        print(">> הסדרה סטציונרית (p < 0.05)")
    else:
        print(">> הסדרה לא סטציונרית (p >= 0.05)")
    print()

# בדיקה על הנתונים המקוריים
test_stationarity(df["sales"], "Original Sales")

# הפרש ראשון (differencing) — הדרך הנפוצה להפוך סדרה לסטציונרית
df["sales_diff"] = df["sales"].diff()
test_stationarity(df["sales_diff"], "First Difference")

הכלל פשוט: אם ה-p-value קטן מ-0.05, הסדרה סטציונרית. אם לא — מבצעים הפרש (differencing) ובודקים שוב. לרוב, הפרש אחד או שניים מספיקים.

שלב 8: ניתוח אוטוקורלציה

לפני שבונים מודל ARIMA, צריך להבין אילו פרמטרים לתת לו. גרפי ACF ו-PACF הם הכלי המרכזי לזה:

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

plot_acf(df["sales_diff"].dropna(), lags=40, ax=axes[0])
axes[0].set_title("ACF — Autocorrelation Function")

plot_pacf(df["sales_diff"].dropna(), lags=40, ax=axes[1])
axes[1].set_title("PACF — Partial Autocorrelation Function")

plt.tight_layout()
plt.show()

איך קוראים את הגרפים?

  • PACF קובע את p (סדר ה-AR) — חפשו את ה-lag האחרון שחורג מרווח הביטחון
  • ACF קובע את q (סדר ה-MA) — אותו רעיון
  • d — מספר ההפרשים שביצעתם עד שהסדרה הפכה לסטציונרית (בדרך כלל 1 או 2)

שלב 9: בניית מודל ARIMA לחיזוי

הגענו לחלק המרגש. ARIMA (AutoRegressive Integrated Moving Average) הוא אחד המודלים הקלאסיים לחיזוי סדרות זמן, ובכנות — הוא עדיין נקודת התחלה מצוינת גם ב-2026 לפני שקופצים לשיטות מתקדמות יותר כמו Prophet או רשתות נוירונים.

# חלוקה לאימון ומבחן
train = df["sales"][:"2025-09-30"]
test = df["sales"]["2025-10-01":]

print(f"Train: {len(train)} days")
print(f"Test: {len(test)} days")

# בניית מודל ARIMA(2,1,2)
model = ARIMA(train, order=(2, 1, 2))
model_fit = model.fit()

print(model_fit.summary())

חיזוי והשוואה לנתונים אמיתיים

# חיזוי לתקופת המבחן
forecast = model_fit.forecast(steps=len(test))

# חישוב שגיאות
from sklearn.metrics import mean_absolute_error, mean_squared_error

mae = mean_absolute_error(test, forecast)
rmse = np.sqrt(mean_squared_error(test, forecast))
print(f"MAE:  {mae:.2f}")
print(f"RMSE: {rmse:.2f}")

# ויזואליזציה
fig, ax = plt.subplots(figsize=(14, 6))

ax.plot(train.index[-90:], train[-90:], label="Train (last 90 days)")
ax.plot(test.index, test, label="Actual", linewidth=2)
ax.plot(test.index, forecast, label="Forecast", linewidth=2, linestyle="--", color="red")

ax.set_title("ARIMA Forecast vs Actual")
ax.set_xlabel("Date")
ax.set_ylabel("Sales")
ax.legend()
plt.tight_layout()
plt.show()

שלב 10: שינויי אחוז ומדדים מצטברים

בפרויקטים אמיתיים (ובמיוחד בנתונים פיננסיים), הרבה פעמים מה שמעניין הוא לא הערך עצמו אלא השינוי היחסי:

# שינוי אחוזי יומי
df["pct_change"] = df["sales"].pct_change() * 100

# שינוי אחוזי לעומת שנה קודמת (YoY)
df["yoy_change"] = df["sales"].pct_change(periods=365) * 100

# סכום מצטבר חודשי
df["cumsum_monthly"] = df.groupby(df.index.to_period("M"))["sales"].cumsum()

print(df[["sales", "pct_change", "yoy_change"]].tail(10))

טיפים מעשיים לעבודה עם סדרות זמן ב-pandas 3.0

אחרי שעברנו את כל השלבים, הנה כמה דברים שלמדתי מעבודה יומיומית עם סדרות זמן:

  • תמיד מיינו לפי תאריך — פונקציות כמו resample ו-rolling מניחות שהאינדקס ממוין. תשתמשו ב-df.sort_index() ותחסכו לעצמכם כאבי ראש
  • הגדירו freq באינדקס — הוספת תדירות (df.index.freq = "D") מונעת אזהרות ומשפרת ביצועים של מודלים סטטיסטיים
  • בדקו רזולוציה אחרי read_csv — ב-pandas 3.0, תאריכים שנקראים מ-CSV יהיו ב-datetime64[us]. צריכים ננו-שניות? השתמשו ב-.as_unit("ns")
  • היזהרו עם Timedelta — גם Timedelta עבר למיקרו-שניות כברירת מחדל. כדאי לוודא שהרזולוציות תואמות לפני חישובים בין סדרות
  • השתמשו ב-"ME" במקום "M" — סימוני תדירויות ישנים (M, Q, Y) הוצאו משימוש. עברו ל-ME, QE, YE

שאלות נפוצות

מה ההבדל בין סדרת זמן סטציונרית ללא סטציונרית?

סדרה סטציונרית היא כזו שבה הממוצע, השונות ומבנה האוטוקורלציה נשארים קבועים לאורך הזמן. סדרה עם מגמה עולה או יורדת, או עם עונתיות בולטת, אינה סטציונרית. רוב המודלים הסטטיסטיים (כמו ARIMA) דורשים סטציונריות, ולכן משתמשים בהפרש (differencing) לפני המודלינג.

מה חדש ב-pandas 3.0 לגבי תאריכים וזמנים?

השינוי המשמעותי ביותר הוא המעבר לרזולוציית מיקרו-שניות (datetime64[us]) כברירת מחדל. בנוסף, סימוני תדירויות עודכנו (כמו "ME" במקום "M"), ו-Copy-on-Write הפך לברירת מחדל — מה שמונע שינויים בלתי מכוונים בנתונים מקוריים בעת חיתוך.

ARIMA או SARIMA — מה עדיף?

תלוי בנתונים. ARIMA מתאים לסדרות ללא עונתיות ברורה, בעוד SARIMA מוסיף רכיב עונתי — מושלם לנתונים עם דפוסים חוזרים כמו מכירות שעולות כל קיץ. העצה שלי: בצעו פירוק (decomposition) וחפשו רכיב עונתי. אם הוא שם, לכו על SARIMA.

איך מטפלים בערכים חסרים בסדרת זמן?

הגישה המועדפת היא אינטרפולציה, ובמיוחד interpolate(method="time") שלוקחת בחשבון רווחים בזמן. בניגוד למילוי בממוצע, אינטרפולציה שומרת על המשכיות הסדרה. לנתונים מדורגים (כמו מחירי מניות) אפשר גם להשתמש ב-ffill().

מה ההבדל בין resample ל-rolling?

resample משנה את תדירות הנתונים — למשל, ממיומי לחודשי — ומייצר סדרה קצרה יותר. rolling מחשב ערך על חלון נע בתוך התדירות הקיימת, כמו ממוצע 7 ימים לכל יום. בקיצור: resample לצמצום תדירות, rolling להחלקה בלי לאבד נקודות.

אודות הכותב Editorial Team

Our team of expert writers and editors.