跳至主要內容

    用表格模型預測時間資料:Lag 與 Rolling 特徵

    把每一列的可用歷史寫清楚,再比較一次預測十四天的結果

    sunny 桑尼· · 42 MIN READ · 233 次瀏覽

    用表格模型預測時間資料:Lag 與 Rolling 特徵
    本文目錄

    每天一筆訂單量,原本只有日期和數字。若想把它交給 Random Forest,模型卻需要一張特徵表:每列一個要預測的目標,旁邊放可以幫忙的資訊。

    最自然的資訊就是最近發生的事:昨天多少、上週同一天多少、最近七天平均多少。但只要視窗往右多放一天,模型可能把今天的答案當成今天的特徵。另一個更隱蔽的問題是:訓練表每列都有昨天的真值,實際一次預測十四天時,第二天開始的「昨天」卻還沒發生。

    上一篇先建立時間切分、季節基線與 ARIMA 的觀念。本篇把這條時間邊界落到資料表與程式:先核每列特徵,再實際訓練森林,最後逐步把自己的預測接回去。讀者會看見驗證選出的模型,到了下一段未必仍勝過簡單基線。

    一列資料,要先決定是在預測哪一天

    以每日資料為例,假設我們在第 t 天開始前預測當天的訂單,昨日觀測已經結算可用。最基本的監督式資料列可以寫成:

    target(t)   = y(t)
    lag1(t)     = y(t-1)
    lag7(t)     = y(t-7)
    rolling7(t) = [y(t-7) + ... + y(t-1)] / 7
    

    target 是答案;其餘三欄都在這個答案發生以前。lag 代表滯後多少筆觀測,rolling 則把一段歷史壓成統計量。前者保留指定位置,後者提供最近水準的摘要。它們不是必須搭配特定模型的專用欄位,同一張表也能交給線性回歸或梯度提升樹。

    圖 1|scikit-learn developers 的官方小時租車範例,BSD 3-Clause;資料另署名 Hadi Fanaee-T/UCI CC BY 4.0,OpenML v2 整理見文末。上方是 Polars 特徵程式,下方是 tail(10) 的十四欄輸出,這十列已有足夠歷史,不是缺值頭列。i64/f64 是欄位型別,不是額外資料列。原頁橫向捲動的截圖依原欄順序接合;手機先看全圖,再將五組完整來源欄位各等比放大至 620px 寬,以 LANCZOS 縮放後直排。重複的 count 是 target,只用來對齊同一列,不放入模型輸入;沒有新增樣本。來源:原例。

    官方範例使用小時資料,所以除了前一小時、前一天,還會放前一週等欄位。本文自己的實作則是每日人工資料,只使用 lag1、lag7、rolling7,加上七個星期位置指示欄。不要把外部圖的小時 lag 直接照搬成每日序列的天數。

    第一個可訓練的目標在 index 7,因為它之前剛好有七個觀測。以下來自本文實際輸出,省略七個日曆指示欄,數值只在表中四捨五入:

    target index當天 targetlag1lag7rolling7
    745.758.643.052.442857
    847.845.747.152.828571
    949.947.851.252.928571

    例如 index 7 的平均只取 index 0–6,不包含 45.7 這個當天答案。到了 index 8,昨天的 45.7 已經成為歷史,因此可以出現在 lag1。相同數字能不能使用,取決於預測發生的時間,而不是它曾經被叫作 target 就永遠不能再用。

    shift 移的是數值對齊,不能只改日期標籤

    在 pandas,建立昨天值的常見寫法是把序列向後移一列:

    lag1 = y.shift(1)
    lag7 = y.shift(7)
    

    不指定 freq 時,正的 periods 會讓較早的數值對齊到較晚的列,開頭留下缺值。原本 index 0 的觀測,現在成為 index 1 這列的歷史特徵。

    圖 2|pandas documentation contributors,2.2.3 DataFrame.shift 原頁程式/輸出截圖,BSD 3-Clause。三欄分別是 periods 0、1、2;period 0 是當期原值,不能當作預測當期的合法 lag。例如 2020-01-03 的原值 15,同列的一期滯後是 20、兩期滯後是 10。最前面的 NaN 表示沒有更早數值。這張截圖沒有展示 freq 移動日期的例子,兩種語意另見正文。截取原例程式與表格並裁去右側頁面空白,保留完整標頭、日期、NaN 與五列輸出。來源:shift API。

    要分清的是,shift(freq="D") 主要移動時間索引標籤,並不是同一個「把昨日數值接到今天 target」的操作。日期看起來晚了一天,不代表特徵與答案就已正確配對。合併回目標表以前,仍要核對索引、頻率與真正可用的歷史。

    負的 shift 常用來建立未來目標,例如想把 t+7 的答案放到 t 這列;如果把它錯放進特徵,就直接看到了未來。是否安全要看欄位用途:未來值可以是訓練 label,卻不能是當時拿不到的輸入,而且訓練截止日之後的 label 也不能混進 fit。

    rolling 的右邊界,決定有沒有把答案平均進去

    直接寫 y.rolling(7).mean(),預設會包含當列的 y(t)。若這列要預測的正是 y(t),就已把未知答案放進特徵。本文先 shift,再 rolling:

    rolling7 = y.shift(1).rolling(7).mean()
    

    這樣 index t 的右邊界停在 t−1,往左共七筆。先把公式寫出來,再核程式,會比只記住某一串 pandas 語法可靠。

    圖 3|pandas documentation contributors,2.2.3 rolling API 原頁程式/輸出截圖,BSD 3-Clause。原輸入 B 為 [0,1,2,NaN,4],固定兩筆的 sum 得到 NaN、1、3、NaN、NaN;整數 window 預設要求兩個有效觀測。這張未 shift 的原例包含當列,只用來說明 window/min_periods,不是本文安全七日 mean 特徵的實跑;裁去右側頁面空白,保留完整程式、標頭與五列輸出。來源:rolling API。
    圖 4|同一 pandas API 原例,BSD 3-Clause,輸入數值相同,時間是 09:00:00、02、03、05、06。改成兩秒視窗後,原例的 sum 是 0、1、3、NaN、4。預設右閉的時間範圍與 min_periods,使它和「固定兩筆」不同;不是每日 2D 視窗,也不是另一組 [0,1,2,3,4] 的資料。此原例同樣未 shift、包含當列,只說明視窗行為;截取程式與表格、裁去右側空白,完整日期、NaN 與五列仍保留。來源同 rolling API。

    固定筆數視窗與時間長度視窗也不同。rolling(7) 看七筆;有 datetime index 時,rolling("7D") 看七天範圍,若有缺日或同一天多筆,包含的筆數就可能不同。整數視窗預設需要完整 window 數量的有效觀測才輸出;時間長度視窗的 min_periods 預設則不同,不能看到同樣寫「七」就當成相同特徵。

    本文的每日資料沒有缺日,rolling 的 min_periods 沿用整數視窗預設七筆,因此前七列沒有完整 lag7/rolling7,直接移除。這是在承認歷史不足,不是把缺值解釋成零需求。若真實資料的中段有缺值,還要另決定缺值是漏報、停業還是真正零值,不能直接補零或從未來回填。

    center=True 或向前看的 window 尤其要小心:它們可用於事後平滑與描述,卻可能把預測時間點後的觀測放進統計量。整張表算得出數字,不代表當時算得出來。

    程式另放了一個可檢查的反例:只把 y[10] 增加 70,index 10 的安全特徵完全不變,因為它們最多看到 index 9。未 shift 的七日平均卻增加 10,正好是 70/7。這比單看最終分數,更直接暴露 target 是否進入視窗。

    排好時間還不夠:要排對群組與可用時間

    單一序列以時間排序後 shift,通常很直觀。若資料變成多家門市,直接對整欄 shift,某家店的第一天可能接到上一家店最後一天的數字。應先按「門市、時間」排序,再在各門市內形成自己的歷史;rolling 同樣不能跨越群組邊界。

    還要分清「觀測日期」與「拿到資料的日期」。假設昨日訂單要到今天中午才結算,早上九點預測時就不能用昨日最終值。此時合法的 lag 可能必須更長,或改用當時可取得的速報版本。後來修正的歷史資料,也不能冒充當年當時看到的版本。

    日曆則是另一種欄位。星期、已知假日通常可以預先取得;未來真實雨量或未來實際價格調整則要看它們何時確定。本文七個 weekday 指示欄只是 index 對七取餘數的人造週期位置,沒有對應真實公曆日期,也沒有為真實資料處理假日。

    因此,每新增一欄,都可以問同一個具體問題:如果把時間停在這次預測的起點,這個值真的已存在嗎?不只答案欄需要問,日後的聚合、填補、標準化與特徵選擇也一樣。

    訓練時看過昨日真值,十四天後的昨日卻可能是預測

    假設最後知道的是第 T 天,要一次交付 T+1 到 T+14。第一天的 lag1 可以用真實 y(T),但第二天的 lag1 就需要 y(T+1),而它此時還不知道。

    最直接的處理方式是 recursive:先用一個單步模型預測第一天,再把這個預測當成歷史接回去,產生第二天的特徵。七日平均與 lag7 也要跟著這條更新後的歷史走;不能只有 lag1 接預測,其他欄位仍偷用完整測試真值。

    起點已知:... y(T-1), y(T)
    預測一步:ŷ(T+1)
    接回歷史:... y(T), ŷ(T+1)
    再算特徵 → 預測 ŷ(T+2)
    重複,直到 ŷ(T+14)
    

    這也帶來訓練與部署的落差:訓練列裡的 lag 多半是真值,遞迴後段則混入模型自己的預測,誤差可能透過特徵繼續傳遞。但不是每一步都必然更差,也不是誤差一定單調增加;要按實際 horizon 評估。

    其他常見策略解決的方式不同:

    策略怎麼產生未來十四點主要取捨

    Recursive
    怎麼產生未來十四點
    單步模型反覆預測,接回自己的輸出
    主要取捨
    模型可共用;需要逐步計算特徵,可能受回饋誤差影響
    Direct
    怎麼產生未來十四點
    分別訓練第 1、2、…、14 步模型
    主要取捨
    不必把前一步預測當輸入;多個模型與各 horizon 的標籤要管理
    Multi-output
    怎麼產生未來十四點
    一個模型同時輸出十四個目標
    主要取捨
    可一起建模多步關係;模型需支援向量輸出,訓練樣本要有完整目標範圍

    Direct 的十四個模型不必分享完全相同的誤差結構;multi-output 也不是把 recursive 的結果換個格式輸出。兩者建立訓練 label 時,都要確保 label 的最晚時間沒有超過訓練截止日。模型數量、單次模型複雜度、資料量與服務方式一起決定成本,不能只用「一個模型」就斷言最便宜。

    本文只實作 recursive 與另一個明確分開的每日 one-step 協定,沒有訓練 direct 或 multi-output 模型,也不宣稱哪一種策略普遍較準。

    先讓候選接受同一個十四天預測任務

    把序列變成表格,不代表它就變成可以任意隨機切的獨立資料。相鄰列有大量重疊歷史,隨機切分還可能讓較晚的資料參與訓練,再回頭評估較早的時段。

    圖 5|scikit-learn developers,1.5.2 TimeSeriesSplit 官方圖,BSD 3-Clause,原 PNG 不改像素。這是人工索引 0–99 的四折拓樸,藍色 train、橘色 test;下方 class/group 是原例附帶標記,不表示此切法自動分層或隔離群組。本文實作是後面的兩個驗證起點,沒有冒充這張圖的四折。來源:完整範例與程式。

    本文使用兩個擴張起點驗證,然後留下最後一段。索引從零開始,lag7 使第一個訓練 target 是 index 7:

    區段fit 的 target 索引從起點一次預測
    驗證一7–125126–139,共十四點
    驗證二7–139140–153,共十四點
    最後評估7–153154–167,共十四點

    兩個候選是先定週期七的季節 naive,以及固定參數森林。季節 naive 把最後七天重複兩輪;森林每一步則重算 lag1、lag7、rolling7 和七個日曆指示欄。兩者都只有起點之前的真實歷史,段內不取得新觀測。

    森林用 40 棵樹、最大深度 6、每葉至少 3 筆、seed 7、單一 job。沒有用最後區段調這些值。樹可以利用非線性與特徵交互作用,但本例森林的每棵樹在葉節點仍輸出訓練目標的平均,不能因此期待它自動把持續成長向訓練範圍外延伸。

    完整資料是 168 點人工訊號,包含每步 0.1 的成長、七步週期形狀與五步確定擾動,沒有抽樣真實門市資料。資料形狀、週期與候選都是教學預設,不是盲測或隨機搜尋的勝出組合。

    完整程式與實際輸出

    這支程式已在下列既有環境實際執行,輸出包含完整人工序列、前幾列特徵、每折十四點預測、最後選擇與 one-step 協定:

    Python 3.10.12
    numpy 1.26.3
    pandas 2.2.2
    scikit-learn 1.5.1
    

    把下方完整程式存成檔案,已有對應套件的環境可執行:

    python3 lag_rolling_forecasts.py > stdout.json
    

    依賴版本可記成以下 requirements。本文核對的是現有環境,不宣稱在所有平台全新安裝都已驗證。外部文件使用 scikit-learn 1.5.2、pandas 2.2.3,與本次執行版本分開列出。

    numpy==1.26.3
    pandas==2.2.2
    scikit-learn==1.5.1
    
    """Original synthetic teaching example: safe lag features and recursive forecasts.
    
    Daily observations arrive without delay. Fixed-origin 14-day forecasts never
    receive the intermediate actuals. A separately labelled one-step protocol does.
    """
    import json
    import platform
    
    import numpy as np
    import pandas as pd
    import sklearn
    from sklearn.ensemble import RandomForestRegressor
    
    
    PERIOD = 7
    HORIZON = 14
    PATTERN = (-6, -3, 0, 2, 5, 11, 8)
    DISTURBANCE = (-1, 0, 1, 0, -1)
    SERIES = [50 + 0.1 * t + PATTERN[t % PERIOD] + DISTURBANCE[t % 5]
              for t in range(168)]
    FEATURES = ["lag1", "lag7", "rolling7"] + [f"weekday{k}" for k in range(7)]
    CANDIDATES = ("seasonal_naive", "random_forest")
    
    
    def feature_table(history):
        """Row t predicts y[t]; every target-derived feature ends at t-1."""
        y = pd.Series(history, dtype=float)
        table = pd.DataFrame({"target": y, "lag1": y.shift(1),
                              "lag7": y.shift(7),
                              "rolling7": y.shift(1).rolling(7).mean()})
        for k in range(7):
            table[f"weekday{k}"] = (table.index % 7 == k).astype(int)
        return table.dropna()
    
    
    def next_features(history):
        """History may contain previous predictions during recursive deployment."""
        t = len(history)
        assert t >= 7
        return [history[-1], history[-7], float(np.mean(history[-7:]))] + [
            int(t % 7 == k) for k in range(7)]
    
    
    def fit_forest(history):
        table = feature_table(history)
        model = RandomForestRegressor(n_estimators=40, max_depth=6,
                                      min_samples_leaf=3, random_state=7, n_jobs=1)
        model.fit(table[FEATURES].to_numpy(), table["target"].to_numpy())
        return model
    
    
    def forecast(history, name, model=None):
        """Only the observed prefix is supplied; recursively append own predictions."""
        prefix = list(history)
        if name == "seasonal_naive":
            return [prefix[-PERIOD + h % PERIOD] for h in range(HORIZON)]
        assert name == "random_forest" and model is not None
        predicted = []
        for _ in range(HORIZON):
            value = float(model.predict([next_features(prefix)])[0])
            predicted.append(value)
            prefix.append(value)
        return predicted
    
    
    def mae(truth, predicted):
        return float(np.mean(np.abs(np.asarray(truth) - np.asarray(predicted))))
    
    
    def rounded(values):
        return np.round(values, 8).tolist()
    
    
    def main():
        validation = []
        for origin in (126, 140):
            history = SERIES[:origin]
            model = fit_forest(history)
            for name in CANDIDATES:
                predicted = forecast(history, name, model)
                validation.append({"origin": origin, "training_target_indices": [7, origin - 1],
                    "forecast_indices": [origin, origin + HORIZON - 1], "model": name,
                    "prediction": rounded(predicted), "truth": rounded(SERIES[origin:origin + HORIZON]),
                    "MAE": mae(SERIES[origin:origin + HORIZON], predicted)})
        scores = {name: float(np.mean([row["MAE"] for row in validation
                                      if row["model"] == name])) for name in CANDIDATES}
        selected = min(CANDIDATES, key=lambda name: (scores[name], CANDIDATES.index(name)))
        # Freeze selection before reading final-block errors.
        origin = 154
        model = fit_forest(SERIES[:origin])
        final = []
        for name in CANDIDATES:
            prediction = forecast(SERIES[:origin], name, model)
            final.append({"model": name, "selected_using_validation_only": name == selected,
                "prediction": rounded(prediction), "truth": rounded(SERIES[origin:]),
                "MAE": mae(SERIES[origin:], prediction)})
    
        # Different information contract: true past is revealed before each next day.
        # Same already-fitted forest, no refits; this is not a fixed-origin result.
        one_step = [float(model.predict([next_features(SERIES[:t])])[0])
                    for t in range(origin, len(SERIES))]
    
        table = feature_table(SERIES[:origin])
        alignment_error = max(abs(a - b) for t in table.index
                              for a, b in zip(table.loc[t, FEATURES], next_features(SERIES[:t])))
        assert alignment_error < 1e-12
        # Perturb y[10] itself: correctly shifted features for target 10 stay fixed;
        # the unshifted rolling mean includes the unavailable target and changes.
        changed = SERIES.copy()
        changed[10] += 70
        safe = feature_table(SERIES).loc[10, FEATURES].to_numpy()
        safe_changed = feature_table(changed).loc[10, FEATURES].to_numpy()
        assert np.array_equal(safe, safe_changed)
        unsafe_delta = float(pd.Series(changed).rolling(7).mean()[10]
                             - pd.Series(SERIES).rolling(7).mean()[10])
        assert abs(unsafe_delta - 10) < 1e-12
        print(json.dumps({"scope": "Hand-set 168-point daily teaching signal; actual fitted forest, "
            "not an external benchmark. Calendar known, observations available without delay.",
            "versions": {"python": platform.python_version(), "numpy": np.__version__,
                "pandas": pd.__version__, "sklearn": sklearn.__version__},
            "series": rounded(SERIES), "features": FEATURES,
            "first_three_training_rows": table.head(3).reset_index(names="target_index").to_dict("records"),
            "feature_alignment_max_error": alignment_error,
            "target_perturbation_check": {"index": 10, "target_increase": 70,
                "shifted_features_unchanged": True, "unshifted_rolling_mean_change": unsafe_delta},
            "validation": validation, "mean_validation_MAE": scores,
            "selected_before_final_evaluation": selected,
            "final_fixed_origin": {"origin": origin, "training_target_indices": [7, 153],
                "forecast_indices": [154, 167], "horizon": HORIZON, "results": final},
            "different_one_step_observed_history_protocol": {
                "refit": False, "predictions": rounded(one_step), "MAE": mae(SERIES[origin:], one_step),
                "scope": "Each prediction gets actual observations through t-1. "
                         "Not available to the fixed-origin 14-day forecast; not a rival model score."},
            "limits": ["The pattern, candidate hyperparameters and folds were fixed for teaching.",
                "Validation training prefixes overlap; folds are not independent samples.",
                "No random-split comparison, prediction intervals or general model ranking.",
                "Tree forecasts can fail under extrapolation or distribution shift.",
                "Real panels require grouping; delayed values require availability-time handling."]},
            ensure_ascii=False, indent=2))
    
    
    if __name__ == "__main__":
        main()
    

    保存輸出中的驗證平均如下。它是在兩個驗證起點的相同十四步 MAE 上選模型,最後區段不參與選擇:

    {
      "mean_validation_MAE": {
        "seasonal_naive": 1.514285714285713,
        "random_forest": 1.4619162286126577
      },
      "selected_before_final_evaluation": "random_forest"
    }
    
    候選驗證一 MAE驗證二 MAE驗證平均最後十四點 MAE
    季節 naive1.5642861.4642861.5142861.292857
    Random Forest1.3709791.5528541.4619161.448743

    驗證選了森林,最後卻是季節基線較好。這個結果應該保留,不能看完最後分數再宣稱自己原本選的是基線。兩個驗證起點也有大量重疊訓練資料,不是兩份獨立資料集,不能僅憑這一點小差異做顯著性或一般優劣推論。

    最後一段前兩步能看見遞迴的差異:起點預測 index 154 約 61.262047,接著 index 155 約 61.388879。第二步用的 lag1 是前一步預測 61.262047,而不是後來看到的真值 58.4。到第二週,lag7 也開始包含第一週的預測;程式用一份只含已知歷史與自身預測的 prefix,讓所有視窗一起遵守這個邊界。

    每天拿到新真值,是另一份預測契約

    如果實際工作是每天早上重新預測今天,昨日真值在那之前已經到達,就可以使用更近期的實際歷史。這是合法的 one-step observed-history 任務;它與年初一次交付未來十四天,是不同服務需求。

    程式最後另用同一個已擬合森林、不重新 fit,每天取得截至 t−1 的真實觀測後預測 t。它的 MAE 約 1.522394,本例反而比十四步 recursive 的 1.448743 略差。因此不能把「能拿到更多真歷史」改寫成「對任何固定模型一定更準」。模型如何切分特徵空間、預測偏差怎麼累積,都會影響這一段的實際結果。

    圖 6|scikit-learn developers,BSD 3-Clause;底層 Bike Sharing 資料另署名 Hadi Fanaee-T/UCI CC BY 4.0,OpenML v2 由 Joaquin Vanschoren 整理。灰色是實際租車量,藍線是 GBRT 中位數、橘線是 Poisson-loss 平均預測,藍色面積來自第 5/95 分位模型。來源顯示第一個時間切分折(all_splits[0])三千個測試列中的最後 96 列,每列特徵使用實際滯後歷史,不是從左端一次遞迴預測整段。它不是本文森林的輸出,也不能單憑陰影名稱保證 90% 覆蓋。原 PNG 保留;封面將完整同圖等比置於白底 1600×900,不加文字或曲線。來源:完整官方範例。

    對這類圖先問清楚資訊契約,再看線條貼不貼近。若模型在每個測試列都取得實際的前一小時值,就不能把一整段圖當成「從最左端起點一次預測到最右端」的成果。圖上的區間也需要另做實證覆蓋檢查,不能只因名稱寫 90% 就認為已校準。

    對一個新的門市任務,最有用的檢查是沿著一筆具體預測往回追:這列答案是哪天?每欄的最後觀測時間是哪天?十四步的後段是拿預測還是真值重算?然後才比較模型。只要這些時間角色清楚,lag 與 rolling 才是在把歷史交給模型,而不是把答案藏進表格。

    來源、資料脈絡與圖像授權

    本文的原創部分是人工 168 點序列、十個特徵的逐列檢查、target 擾動反例、森林固定起點遞迴與 one-step 的分開實作,以及兩次驗證後保留最後排名反轉的結果。外部圖為來源文件輸出,沒有冒充本站模型或資料重跑。

    以下保留選用圖的完整署名、變更及兩份 BSD 授權全文。資料來源 CC BY 4.0 與軟體/文件 BSD 的範圍分開說明。

    # DS61 source image credits and reuse notices
    
    The following source-derived images accompany this article. The credits, changes and applicable license notices are retained below. The article source list provides clickable links to the original documentation and dataset records.
    
    ## scikit-learn 1.5.2 documentation figures and output
    
    - `fig-01-timeseries-split.png`: scikit-learn developers, generated TimeSeriesSplit example, [documentation page](https://scikit-learn.org/1.5/modules/cross_validation.html#time-series-split), [source PNG](https://scikit-learn.org/1.5/_images/sphx_glr_plot_cv_indices_013.png). Synthetic generated indices; four expanding splits. Reused without pixel changes.
    - `fig-02-lagged-feature-table.png` and `fig-02-mobile-lagged-feature-table.png`: scikit-learn developers, [Lagged features for time series forecasting](https://scikit-learn.org/1.5/auto_examples/applications/plot_time_series_lagged_features.html), v1.5.2. The source table is rendered from the example's `lagged_df.tail(10)` output. The full table had to be captured in two horizontal browser-scroll states because the official page clips its 14-column output in the content viewport. The source code and table are retained; the two captures are stitched in original column order, and the mobile derivative first shows the complete image, then stacks five complete original column groups, each proportionally enlarged to 620 pixels wide with LANCZOS. The target-count column is repeated only for row identification and is not a predictor input. No table values, labels or row order are changed.
    - `fig-06-official-forecast.png` and `cover.png`: same scikit-learn example and authors. `fig-06` is the original source PNG, reused without pixel changes. `cover.png` is the complete source plot fitted proportionally on a white 1600×900 canvas without added text or marks. It reports the last 96 rows of the first TimeSeriesSplit fold (all_splits[0], 3,000 test rows); it is not the DS61 practice output or a calibration guarantee.
    
    The Bike Sharing data underlying the scikit-learn example is credited separately from the scikit-learn documentation: UCI record [Bike Sharing Dataset](https://archive.ics.uci.edu/dataset/275/bike+sharing+dataset), creator Hadi Fanaee-T, [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The associated paper is Hadi Fanaee-T and João Gama, “Event labeling combining ensemble detectors and background knowledge,” DOI [10.1007/s13748-013-0040-3](https://doi.org/10.1007/s13748-013-0040-3); João Gama is an associated-paper coauthor, not a name in the UCI record's Creators field. The example retrieves OpenML `Bike_Sharing_Demand` version 2 (data id 42712). OpenML metadata identifies the UCI source and credits Joaquin Vanschoren for cleanup: replacing category labels, reversing temperature/wind normalization, and changing feature names. No row-by-row byte comparison was performed, and no byte-exact UCI identity is claimed. The OpenML `Public` field is not represented as a license.
    
    ## pandas 2.2.3 documentation outputs
    
    - `fig-03-shift-lag-table.png`: pandas documentation contributors, [`DataFrame.shift`](https://pandas.pydata.org/pandas-docs/version/2.2.3/reference/api/pandas.DataFrame.shift.html), version 2.2.3; source code/output example screenshot, not an independent original figure. Right-side page whitespace is cropped; complete code, headers, dates, NaN and all five output rows are retained.
    - `fig-04-rolling-row-window.png` and `fig-05-rolling-time-window.png`: pandas documentation contributors, [`DataFrame.rolling`](https://pandas.pydata.org/pandas-docs/version/2.2.3/reference/api/pandas.DataFrame.rolling.html), version 2.2.3; source code/output example screenshots, not independent original figures. Right-side page whitespace is cropped; complete code, headers, dates where present, NaN and all five output rows are retained. The examples use synthetic data. These unshifted rolling examples include the current row and illustrate window/min_periods behavior, not the shifted forecasting features in this article.
    
    ## Full license texts
    
    The following notices are copied verbatim from scikit-learn 1.5.2 and pandas 2.2.3 project license files.
    
    ### scikit-learn 1.5.2 BSD 3-Clause License
    
    BSD 3-Clause License
    
    Copyright (c) 2007-2024 The scikit-learn developers.
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    
    * Neither the name of the copyright holder nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    
    ### pandas 2.2.3 BSD 3-Clause License
    
    BSD 3-Clause License
    
    Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
    All rights reserved.
    
    Copyright (c) 2011-2023, Open source contributors.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    
    * Neither the name of the copyright holder nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →