%23%20%2F%2F%2F%20script%0A%23%20requires-python%20%3D%20%22%3E%3D3.11%22%0A%23%20dependencies%20%3D%20%5B%0A%23%20%20%20%20%20%22numpy%22%2C%0A%23%20%20%20%20%20%22scikit-learn%22%2C%0A%23%20%20%20%20%20%22yohou%5Bplotting%5D%22%2C%0A%23%20%5D%0A%23%20%2F%2F%2F%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.23.8%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Custom%20Function-Based%20Transforms%0A%0A%20%20%20%20%5B%60FunctionTransformer%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.preprocessing.function.FunctionTransformer%2F)%20wraps%20arbitrary%20polars%20operations%20as%20a%20full%0A%20%20%20%20sklearn-compatible%20transformer.%20This%20gives%20you%20%60fit%60%20%2F%20%60transform%60%20%2F%0A%20%20%20%20%60inverse_transform%60%20lifecycle%20methods%2C%20automatic%20statefulness%20detection%2C%0A%20%20%20%20and%20seamless%20composition%20with%20forecasters%20and%20pipelines.%0A%0A%20%20%20%20**Prerequisites%3A**%20Working%20knowledge%20of%20polars%20expressions%20and%20%5B%60PointReductionForecaster%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.point.reduction.PointReductionForecaster%2F)%0A%20%20%20%20(see%20%60examples%2Fquickstart.py%60).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_()%3A%0A%20%20%20%20from%20copy%20import%20deepcopy%0A%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20polars%20as%20pl%0A%20%20%20%20from%20sklearn.linear_model%20import%20Ridge%0A%0A%20%20%20%20from%20yohou.datasets%20import%20fetch_tourism_monthly%0A%20%20%20%20from%20yohou.metrics%20import%20MeanAbsoluteError%0A%20%20%20%20from%20yohou.model_selection%20import%20train_test_split%0A%20%20%20%20from%20yohou.plotting%20import%20(%0A%20%20%20%20%20%20%20%20plot_forecast%2C%0A%20%20%20%20%20%20%20%20plot_score_time_series%2C%0A%20%20%20%20%20%20%20%20plot_time_series%2C%0A%20%20%20%20)%0A%20%20%20%20from%20yohou.point%20import%20PointReductionForecaster%0A%20%20%20%20from%20yohou.preprocessing%20import%20FunctionTransformer%2C%20LagTransformer%0A%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20FunctionTransformer%2C%0A%20%20%20%20%20%20%20%20LagTransformer%2C%0A%20%20%20%20%20%20%20%20MeanAbsoluteError%2C%0A%20%20%20%20%20%20%20%20PointReductionForecaster%2C%0A%20%20%20%20%20%20%20%20Ridge%2C%0A%20%20%20%20%20%20%20%20deepcopy%2C%0A%20%20%20%20%20%20%20%20fetch_tourism_monthly%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20pl%2C%0A%20%20%20%20%20%20%20%20plot_forecast%2C%0A%20%20%20%20%20%20%20%20plot_score_time_series%2C%0A%20%20%20%20%20%20%20%20plot_time_series%2C%0A%20%20%20%20%20%20%20%20train_test_split%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%201.%20Load%20Data%0A%0A%20%20%20%20We%20load%20the%20monthly%20tourism%20series%20and%20split%20it%20into%20train%20and%20test%20sets.%0A%20%20%20%20%5B%60train_test_split%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.model_selection.split.train_test_split%2F)%20preserves%20temporal%20order%2C%20which%20is%0A%20%20%20%20essential%20for%20time%20series%20work.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fetch_tourism_monthly%2C%20plot_time_series%2C%20train_test_split)%3A%0A%20%20%20%20df%20%3D%20fetch_tourism_monthly().frame.select(%22time%22%2C%20%22T1__tourists%22).drop_nulls().rename(%7B%22T1__tourists%22%3A%20%22tourists%22%7D)%0A%20%20%20%20y_train%2C%20y_test%20%3D%20train_test_split(df%2C%20test_size%3D0.15)%0A%20%20%20%20plot_time_series(y_train%2C%20title%3D%22Training%20Data%22)%0A%20%20%20%20return%20y_test%2C%20y_train%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%202.%20Stateless%20Transform%3A%20Log%20Scale%0A%0A%20%20%20%20A%20simple%20log%20transform%20stabilises%20variance%20in%20multiplicative%20time%20series.%0A%20%20%20%20The%20function%20receives%20a%20polars%20%5B%60DataFrame%60%5D(https%3A%2F%2Fdocs.pola.rs%2Fapi%2Fpython%2Fstable%2Freference%2Fdataframe%2Findex.html)%20**without%20the%20%60%22time%22%60%20column**%0A%20%20%20%20and%20must%20return%20a%20DataFrame%20(or%20array)%20with%20the%20same%20number%20of%20rows.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FunctionTransformer%2C%20np%2C%20plot_time_series%2C%20y_train)%3A%0A%20%20%20%20log_transformer%20%3D%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20func%3Dnp.log%2C%0A%20%20%20%20%20%20%20%20inverse_func%3Dnp.exp%2C%0A%20%20%20%20%20%20%20%20check_inverse%3DFalse%2C%0A%20%20%20%20)%0A%20%20%20%20y_log%20%3D%20log_transformer.fit_transform(y_train)%0A%20%20%20%20_combined%20%3D%20y_train.rename(%7B%22tourists%22%3A%20%22original%22%7D).join(%0A%20%20%20%20%20%20%20%20y_log.rename(%7B%22tourists%22%3A%20%22log-scaled%22%7D)%2C%0A%20%20%20%20%20%20%20%20on%3D%22time%22%2C%0A%20%20%20%20)%0A%20%20%20%20plot_time_series(_combined%2C%20title%3D%22Log%20Transform%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20We%20set%20%60check_inverse%3DFalse%60%20because%20the%20tourism%20dataset%20is%20monthly%20data%0A%20%20%20%20(variable%20day%20counts).%20With%20daily%20or%20sub-daily%20data%20you%20can%20use%0A%20%20%20%20%60check_inverse%3DTrue%60%20(default)%20to%20verify%20%60exp(log(x))%20%E2%89%88%20x%60%20at%20%60fit()%60%20time.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%203.%20Stateful%20Transform%3A%20Differencing%0A%0A%20%20%20%20When%20%60func%60%20produces%20leading%20NaN%20rows%20(e.g.%2C%20%60.diff()%60)%2C%20yohou%0A%20%20%20%20auto-detects%20the%20**warmup**%20and%20sets%20%60observation_horizon%60%20accordingly.%0A%20%20%20%20The%20first%20%60observation_horizon%60%20rows%20are%20stored%20as%20memory%20so%20that%0A%20%20%20%20%60inverse_transform%60%20can%20recover%20the%20original%20values.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FunctionTransformer%2C%20pl%2C%20plot_time_series%2C%20y_train)%3A%0A%20%20%20%20def%20_diff(df%3A%20pl.DataFrame)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20return%20df.select(pl.all().diff())%0A%0A%20%20%20%20def%20_cumsum(df%3A%20pl.DataFrame%2C%20past%3A%20pl.DataFrame%20%7C%20None%20%3D%20None)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20if%20past%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Prepend%20past%20observation%20so%20cumsum%20starts%20from%20the%20original%20level%0A%20%20%20%20%20%20%20%20%20%20%20%20combined%20%3D%20pl.concat(%5Bpast%2C%20df%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20combined.select(pl.all().cum_sum()).tail(len(df))%0A%20%20%20%20%20%20%20%20return%20df.select(pl.all().cum_sum())%0A%0A%20%20%20%20diff_transformer%20%3D%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20func%3D_diff%2C%0A%20%20%20%20%20%20%20%20inverse_func%3D_cumsum%2C%0A%20%20%20%20%20%20%20%20check_inverse%3DFalse%2C%0A%20%20%20%20)%0A%20%20%20%20y_diff%20%3D%20diff_transformer.fit_transform(y_train)%0A%0A%20%20%20%20plot_time_series(y_diff%2C%20title%3D%22After%20Differencing%22)%0A%20%20%20%20return%20diff_transformer%2C%20y_diff%0A%0A%0A%40app.cell%0Adef%20_(diff_transformer%2C%20mo%2C%20y_diff%2C%20y_train)%3A%0A%20%20%20%20%23%20inverse_transform%20needs%20X_p%20(warmup%20data)%20when%20observation_horizon%20%3E%200%0A%20%20%20%20_y_recovered%20%3D%20diff_transformer.inverse_transform(y_diff%2C%20y_train.head(diff_transformer._observation_horizon))%0A%20%20%20%20%23%20Compare%20recovered%20vs.%20original%20(minus%20warmup)%0A%20%20%20%20_y_orig_tail%20%3D%20y_train.tail(len(y_diff))%0A%20%20%20%20_max_diff%20%3D%20(_y_recovered.select(%22tourists%22).to_series()%20-%20_y_orig_tail.select(%22tourists%22).to_series()).abs().max()%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20f%22**Max%20absolute%20reconstruction%20error**%3A%20%7B_max_diff%7D%5Cn%5Cn%22%0A%20%20%20%20%20%20%20%20f%22The%20round-trip%20is%20exact%20because%20%60inverse_transform%60%20uses%20stored%20%22%0A%20%20%20%20%20%20%20%20f%22observation%20memory%20to%20recover%20the%20initial%20level.%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%204.%20Custom%20Feature%20Names%0A%0A%20%20%20%20By%20default%2C%20output%20column%20names%20match%20the%20input.%20Use%20%60feature_names_out%60%0A%20%20%20%20to%20rename%3A%0A%20%20%20%20-%20%60%22one-to-one%22%60%3A%20keep%20original%20names%20(same%20as%20default)%0A%20%20%20%20-%20A%20**callable**%20%60(transformer%2C%20input_features)%20-%3E%20list%5Bstr%5D%60%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FunctionTransformer%2C%20mo%2C%20pl%2C%20y_train)%3A%0A%20%20%20%20def%20_pct_change(df%3A%20pl.DataFrame)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20return%20df.select(pl.all().pct_change())%0A%0A%20%20%20%20pct_transformer%20%3D%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20func%3D_pct_change%2C%0A%20%20%20%20%20%20%20%20feature_names_out%3Dlambda%20self%2C%20names%3A%20%5Bf%22%7Bn%7D_pct%22%20for%20n%20in%20names%5D%2C%0A%20%20%20%20)%0A%20%20%20%20_y_pct%20%3D%20pct_transformer.fit_transform(y_train)%0A%20%20%20%20_names%20%3D%20pct_transformer.get_feature_names_out()%0A%20%20%20%20mo.md(f%22**Output%20feature%20names**%3A%20%7B_names%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%205.%20Keyword%20Arguments%0A%0A%20%20%20%20Pass%20extra%20arguments%20to%20%60func%60%20and%20%60inverse_func%60%20via%20%60kw_args%60%20and%0A%20%20%20%20%60inv_kw_args%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FunctionTransformer%2C%20pl%2C%20plot_time_series%2C%20y_train)%3A%0A%20%20%20%20def%20_clip(df%3A%20pl.DataFrame%2C%20*%2C%20lower%3A%20float%2C%20upper%3A%20float)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20return%20df.select(pl.all().clip(lower%2C%20upper))%0A%0A%20%20%20%20clip_transformer%20%3D%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20func%3D_clip%2C%0A%20%20%20%20%20%20%20%20kw_args%3D%7B%22lower%22%3A%20100.0%2C%20%22upper%22%3A%20500.0%7D%2C%0A%20%20%20%20)%0A%20%20%20%20_y_clipped%20%3D%20clip_transformer.fit_transform(y_train)%0A%20%20%20%20_combined%20%3D%20_y_clipped.rename(%7B%22tourists%22%3A%20%22clipped%22%7D).join(%0A%20%20%20%20%20%20%20%20y_train.rename(%7B%22tourists%22%3A%20%22original%22%7D)%2C%0A%20%20%20%20%20%20%20%20on%3D%22time%22%2C%0A%20%20%20%20)%0A%20%20%20%20plot_time_series(_combined%2C%20title%3D%22Clip%20to%20%5B100%2C%20500%5D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%206.%20Inside%20a%20Forecaster%0A%0A%20%20%20%20Use%20%5B%60FunctionTransformer%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.preprocessing.function.FunctionTransformer%2F)%20as%20%60target_transformer%60%20to%20log-transform%20the%0A%20%20%20%20target%20before%20fitting%20and%20automatically%20back-transform%20predictions.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20FunctionTransformer%2C%0A%20%20%20%20LagTransformer%2C%0A%20%20%20%20MeanAbsoluteError%2C%0A%20%20%20%20PointReductionForecaster%2C%0A%20%20%20%20Ridge%2C%0A%20%20%20%20np%2C%0A%20%20%20%20y_test%2C%0A%20%20%20%20y_train%2C%0A)%3A%0A%20%20%20%20forecaster%20%3D%20PointReductionForecaster(%0A%20%20%20%20%20%20%20%20estimator%3DRidge(alpha%3D1.0)%2C%0A%20%20%20%20%20%20%20%20target_transformer%3DFunctionTransformer(%0A%20%20%20%20%20%20%20%20%20%20%20%20func%3Dnp.log%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20inverse_func%3Dnp.exp%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20check_inverse%3DFalse%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20feature_transformer%3DLagTransformer(lag%3D%5B1%2C%206%2C%2012%5D)%2C%0A%20%20%20%20)%0A%0A%20%20%20%20_horizon%20%3D%20len(y_test)%0A%20%20%20%20forecaster.fit(y_train%2C%20forecasting_horizon%3D_horizon)%0A%20%20%20%20y_pred%20%3D%20forecaster.predict(forecasting_horizon%3D_horizon)%0A%0A%20%20%20%20scorer%20%3D%20MeanAbsoluteError()%0A%20%20%20%20scorer.fit(y_train)%0A%20%20%20%20_score%20%3D%20scorer.score(y_test%2C%20y_pred)%0A%20%20%20%20return%20forecaster%2C%20y_pred%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%5B%60plot_forecast%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.plotting.forecasting.plot_forecast%2F)%20overlays%20the%20predicted%20values%20against%20the%20test%20actuals%2C%0A%20%20%20%20optionally%20showing%20a%20tail%20of%20the%20training%20history%20for%20context.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(plot_forecast%2C%20y_pred%2C%20y_test%2C%20y_train)%3A%0A%20%20%20%20plot_forecast(%0A%20%20%20%20%20%20%20%20y_test%2C%0A%20%20%20%20%20%20%20%20y_pred%2C%0A%20%20%20%20%20%20%20%20y_train%3Dy_train%2C%0A%20%20%20%20%20%20%20%20n_history%3D36%2C%0A%20%20%20%20%20%20%20%20title%3D%22Forecast%20with%20Log%20Target%20Transform%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Multi-vintage%20Scoring%0A%0A%20%20%20%20The%20%60observe_predict%60%20method%20with%20%60stride%3D1%60%20produces%20one%20forecast%20per%0A%20%20%20%20observation%20point%2C%20creating%20multiple%20*vintages*.%20Each%20vintage%20represents%0A%20%20%20%20a%20different%20forecast%20origin%2C%20so%20you%20can%20analyse%20how%20accuracy%20evolves%20as%0A%20%20%20%20the%20model%20absorbs%20more%20data.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(deepcopy%2C%20forecaster%2C%20y_test)%3A%0A%20%20%20%20_vintage_model%20%3D%20deepcopy(forecaster)%0A%20%20%20%20y_pred_vintages%20%3D%20_vintage_model.observe_predict(%0A%20%20%20%20%20%20%20%20y%3Dy_test%2C%0A%20%20%20%20%20%20%20%20stride%3D1%2C%0A%20%20%20%20%20%20%20%20forecasting_horizon%3Dlen(y_test)%2C%0A%20%20%20%20)%0A%20%20%20%20print(f%22Vintages%3A%20%7By_pred_vintages%5B'vintage_time'%5D.n_unique()%7D%22)%0A%20%20%20%20y_pred_vintages.head(10)%0A%20%20%20%20return%20(y_pred_vintages%2C)%0A%0A%0A%40app.cell%0Adef%20_(MeanAbsoluteError%2C%20y_train)%3A%0A%20%20%20%20vintage_scorer%20%3D%20MeanAbsoluteError()%0A%20%20%20%20vintage_scorer.fit(y_train)%0A%20%20%20%20return%20(vintage_scorer%2C)%0A%0A%0A%40app.cell%0Adef%20_(plot_score_time_series%2C%20vintage_scorer%2C%20y_pred_vintages%2C%20y_test)%3A%0A%20%20%20%20plot_score_time_series(%0A%20%20%20%20%20%20%20%20vintage_scorer%2C%0A%20%20%20%20%20%20%20%20y_test%2C%0A%20%20%20%20%20%20%20%20y_pred_vintages%2C%0A%20%20%20%20%20%20%20%20title%3D%22MAE%20over%20Time%22%2C%0A%20%20%20%20%20%20%20%20y_label%3D%22MAE%22%2C%0A%20%20%20%20%20%20%20%20height%3D380%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Next%20Steps%0A%0A%20%20%20%20-%20**Sklearn%20wrappers**%3A%20See%20%5B%60examples%2Fpreprocessing%2Fsklearn_wrappers.py%60%5D(%2Fexamples%2Fdata-features%2Fsklearn_wrappers%2F)%20for%20built-in%20StandardScaler%2C%20MinMaxScaler%2C%20etc.%0A%20%20%20%20-%20**Window%20transforms**%3A%20See%20%60examples%2Fpreprocessing%2Fwindow_transforms.py%60%20for%20rolling%20and%20expanding%20windows%0A%20%20%20%20-%20**Signal%20processing**%3A%20See%20%5B%60examples%2Fpreprocessing%2Fsignal_processing.py%60%5D(%2Fexamples%2Fdata-features%2Fsignal_processing%2F)%20for%20numerical%20filters%20and%20differentiators%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
abe300038d402e99113cc4c0fbbe004b