%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.14%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%20How%20to%20Transform%20Features%20on%20the%20Forecast%20Channel%0A%0A%20%20%20%20This%20notebook%20derives%20features%20from%20an%20%60X_forecast%60%20frame%20using%0A%20%20%20%20%5B%60PerVintageActualTransformer%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.compose.per_vintage.PerVintageActualTransformer%2F)%2C%0A%20%20%20%20composes%20several%20of%20them%20with%0A%20%20%20%20%5B%60FeatureUnion%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.compose.feature_union.FeatureUnion%2F)%2C%0A%20%20%20%20and%20passes%20the%20result%20to%20a%20forecaster's%20%60X_forecast%60%20channel.%0A%0A%20%20%20%20**Prerequisites%3A**%20Familiarity%20with%20the%20three%20exogenous%20types%0A%20%20%20%20(%5BExogenous%20Features%5D(%2Fpages%2Ftutorials%2Fexogenous-features%2F))%20and%0A%20%20%20%20forecast%20vintages%20(%5BWork%20with%20Forecast%20Vintages%5D(%2Fpages%2Fhow-to%2Fforecast-vintages%2F)).%0A%20%20%20%20For%20why%20the%20two%20transformer%20kinds%20exist%2C%20see%0A%20%20%20%20%5BTransformer%20Kinds%5D(%2Fpages%2Fexplanation%2Ftransformer-kinds%2F).%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%201.%20An%20X_forecast%20Frame%20Has%20Two%20Time%20Axes%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20polars%20as%20pl%0A%0A%20%20%20%20from%20yohou.datasets%20import%20make_exogenous_regression%0A%0A%20%20%20%20H%20%3D%206%0A%20%20%20%20data%20%3D%20make_exogenous_regression(n_samples%3D120%2C%20forecasting_horizon%3DH)%0A%20%20%20%20y%2C%20X_actual%2C%20X_forecast%20%3D%20data.y%2C%20data.X_actual%2C%20data.X_forecast%0A%0A%20%20%20%20print(%22X_forecast%20columns%3A%22%2C%20X_forecast.columns)%0A%20%20%20%20print(%22vintages%3A%22%2C%20X_forecast%5B%22vintage_time%22%5D.n_unique())%0A%20%20%20%20print(X_forecast.head(4))%0A%20%20%20%20return%20H%2C%20X_actual%2C%20X_forecast%2C%20pl%2C%20y%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%20Each%20row%20is%20identified%20by%20%60vintage_time%60%20(when%20the%20forecast%20was%20issued)%0A%20%20%20%20*and*%20%60time%60%20(what%20it%20forecasts).%20Several%20vintages%20say%20something%20about%20the%0A%20%20%20%20same%20%60time%60%2C%20so%20%60time%60%20alone%20does%20not%20identify%20a%20row.%20That%20is%20why%20an%0A%20%20%20%20ordinary%20single-axis%20transformer%20cannot%20consume%20this%20frame.%0A%0A%20%20%20%20%23%23%202.%20Lift%20a%20Stateless%20Transformer%20onto%20the%20Vintage%20Axis%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(X_forecast%2C%20pl)%3A%0A%20%20%20%20from%20yohou.compose%20import%20PerVintageActualTransformer%0A%20%20%20%20from%20yohou.preprocessing%20import%20FunctionTransformer%0A%0A%20%20%20%20anomaly%20%3D%20PerVintageActualTransformer(%0A%20%20%20%20%20%20%20%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20%20%20%20%20func%3Dlambda%20df%3A%20df.select(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22wx_temp%22)%20-%20pl.col(%22wx_temp%22).mean()).alias(%22wx_anomaly%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20feature_names_out%3Dlambda%20self%2C%20names%3A%20%5B%22wx_anomaly%22%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%0A%20%20%20%20X_forecast_t%20%3D%20anomaly.fit_transform(X_forecast)%0A%0A%20%20%20%20print(%22out%20columns%3A%22%2C%20X_forecast_t.columns)%0A%20%20%20%20print(X_forecast_t.head(4))%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20FunctionTransformer%2C%0A%20%20%20%20%20%20%20%20PerVintageActualTransformer%2C%0A%20%20%20%20%20%20%20%20X_forecast_t%2C%0A%20%20%20%20%20%20%20%20anomaly%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%20The%20%60vintage_time%60%20and%20%60time%60%20index%20columns%20survive%2C%20and%20each%20vintage's%0A%20%20%20%20anomaly%20is%20computed%20against%20that%20vintage's%20own%20mean.%20Nothing%20is%20borrowed%0A%20%20%20%20from%20a%20neighbouring%20vintage.%0A%0A%20%20%20%20%23%23%203.%20Each%20Vintage%20Is%20Transformed%20Independently%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(X_forecast_t%2C%20pl)%3A%0A%20%20%20%20%23%20Each%20vintage's%20anomalies%20are%20centred%20on%20that%20vintage%20alone%2C%20so%20they%20sum%20to%0A%20%20%20%20%23%20roughly%20zero%20within%20every%20vintage%20rather%20than%20only%20across%20the%20frame.%0A%20%20%20%20per_vintage%20%3D%20(%0A%20%20%20%20%20%20%20%20X_forecast_t.group_by(%22vintage_time%22)%0A%20%20%20%20%20%20%20%20.agg(pl.col(%22wx_anomaly%22).mean().alias(%22mean_anomaly%22))%0A%20%20%20%20%20%20%20%20.sort(%22vintage_time%22)%0A%20%20%20%20)%0A%0A%20%20%20%20print(per_vintage.head(4))%0A%20%20%20%20print(%22max%20%7Cmean%7C%20across%20vintages%3A%22%2C%20per_vintage%5B%22mean_anomaly%22%5D.abs().max())%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.%20Fit%20a%20Transformer%20per%20Vintage%0A%0A%20%20%20%20The%20anomaly%20above%20computed%20its%20mean%20inline%20with%20a%20%60FunctionTransformer%60.%0A%20%20%20%20The%20wrapper's%20real%20value%20is%20lifting%20a%20transformer%20that%20*fits*%20from%20data%3A%20a%0A%20%20%20%20%60SimpleImputer%60%20fills%20each%20vintage's%20missing%20values%20from%20that%20vintage's%20own%0A%20%20%20%20mean.%20It%20is%20leakage-free%20because%20a%20vintage%20is%20fully%20known%20at%20its%0A%20%20%20%20%60vintage_time%60.%20(To%20normalize%20against%20the%20*training*%20distribution%20instead%2C%0A%20%20%20%20put%20the%20scaler%20in%20the%20estimator%20pipeline%2C%20not%20here.)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(PerVintageActualTransformer%2C%20X_forecast%2C%20pl)%3A%0A%20%20%20%20from%20yohou.preprocessing%20import%20SimpleImputer%0A%0A%20%20%20%20%23%20Knock%20a%20gap%20into%20the%20second%20row%20of%20every%20vintage%2C%20then%20impute%20per%20vintage.%0A%20%20%20%20X_gappy%20%3D%20X_forecast.with_columns(%0A%20%20%20%20%20%20%20%20pl.when(pl.int_range(pl.len()).over(%22vintage_time%22)%20%3D%3D%201)%0A%20%20%20%20%20%20%20%20.then(None)%0A%20%20%20%20%20%20%20%20.otherwise(pl.col(%22wx_temp%22))%0A%20%20%20%20%20%20%20%20.alias(%22wx_temp%22)%0A%20%20%20%20)%0A%20%20%20%20imputed%20%3D%20PerVintageActualTransformer(SimpleImputer(strategy%3D%22mean%22)).fit_transform(X_gappy)%0A%0A%20%20%20%20print(%22gaps%20introduced%3A%22%2C%20X_gappy%5B%22wx_temp%22%5D.null_count())%0A%20%20%20%20print(%22gaps%20remaining%20after%20per-vintage%20impute%3A%22%2C%20imputed%5B%22wx_temp%22%5D.null_count())%0A%20%20%20%20print(imputed.head(4))%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.%20Compose%20Forecast%20Transformers%0A%0A%20%20%20%20A%20%5B%60FeatureUnion%60%5D(%2Fpages%2Fapi%2Fgenerated%2Fyohou.compose.feature_union.FeatureUnion%2F)%0A%20%20%20%20of%20forecast-kind%20branches%20is%20itself%20forecast-kind%2C%20and%20it%20aligns%20its%20branches%0A%20%20%20%20on%20%60vintage_time%60%20and%20%60time%60%20together%20rather%20than%20on%20%60time%60%20alone.%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%20PerVintageActualTransformer%2C%0A%20%20%20%20X_forecast%2C%0A%20%20%20%20anomaly%2C%0A%20%20%20%20pl%2C%0A)%3A%0A%20%20%20%20from%20yohou.compose%20import%20FeatureUnion%0A%0A%20%20%20%20squared%20%3D%20PerVintageActualTransformer(%0A%20%20%20%20%20%20%20%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20%20%20%20%20func%3Dlambda%20df%3A%20df.select((pl.col(%22wx_temp%22)%20**%202).alias(%22wx_sq%22))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20feature_names_out%3Dlambda%20self%2C%20names%3A%20%5B%22wx_sq%22%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%0A%20%20%20%20features%20%3D%20FeatureUnion(%0A%20%20%20%20%20%20%20%20transformer_list%3D%5B(%22anom%22%2C%20anomaly)%2C%20(%22sq%22%2C%20squared)%5D%2C%0A%20%20%20%20)%0A%20%20%20%20X_forecast_u%20%3D%20features.fit_transform(X_forecast)%0A%0A%20%20%20%20print(%22union%20kind%3A%22%2C%20features.__sklearn_tags__().transformer_tags.kind)%0A%20%20%20%20print(%22union%20columns%3A%22%2C%20X_forecast_u.columns)%0A%20%20%20%20return%20(FeatureUnion%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%23%206.%20Stateful%20Inners%2C%20and%20the%20One%20Rule%20That%20Is%20Enforced%0A%0A%20%20%20%20A%20wrapped%20transformer%20may%20be%20**stateful**.%20A%20vintage%20is%20internally%0A%20%20%20%20contiguous%2C%20so%20a%20lag%20or%20a%20ramp%20is%20well%20defined%20inside%20one%2C%20and%20a%20fresh%0A%20%20%20%20clone%20per%20vintage%20means%20its%20history%20never%20reaches%20across%20a%20boundary.%20The%0A%20%20%20%20cost%20is%20rows%3A%20it%20consumes%20its%20%60observation_horizon%60%20from%20each%20vintage's%0A%20%20%20%20earliest%20steps.%0A%0A%20%20%20%20What%20*is*%20enforced%20is%20that%20a%20composition%20must%20be%20**homogeneous%20in%20kind**.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20FeatureUnion%2C%0A%20%20%20%20FunctionTransformer%2C%0A%20%20%20%20PerVintageActualTransformer%2C%0A%20%20%20%20X_forecast%2C%0A%20%20%20%20anomaly%2C%0A%20%20%20%20pl%2C%0A)%3A%0A%20%20%20%20from%20yohou.preprocessing%20import%20LagTransformer%0A%0A%20%20%20%20%23%20A%20stateful%20inner%20IS%20lifted%3A%20the%20ramp%20is%20computed%20within%20each%20vintage.%0A%20%20%20%20ramp%20%3D%20PerVintageActualTransformer(%0A%20%20%20%20%20%20%20%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20%20%20%20%20func%3Dlambda%20df%3A%20df.select(pl.col(%22wx_temp%22).diff().alias(%22ramp%22))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20feature_names_out%3Dlambda%20self%2C%20names%3A%20%5B%22ramp%22%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20ramped%20%3D%20ramp.fit_transform(X_forecast)%0A%20%20%20%20print(%22stateful%20inner%20lifted%20-%3E%20%22%2C%20ramped.columns)%0A%20%20%20%20print(%22rows%3A%22%2C%20len(X_forecast)%2C%20%22-%3E%22%2C%20len(ramped)%2C%20%22(each%20vintage%20loses%20its%20first%20step%20to%20the%20diff)%22)%0A%0A%20%20%20%20%23%20Mixing%20kinds%20in%20one%20container%20has%20no%20correct%20alignment%2C%20so%20it%20is%20rejected.%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20FeatureUnion(%0A%20%20%20%20%20%20%20%20%20%20%20%20transformer_list%3D%5B(%22anom%22%2C%20anomaly)%2C%20(%22lag%22%2C%20LagTransformer(%5B1%5D))%5D%2C%0A%20%20%20%20%20%20%20%20).fit(X_forecast)%0A%20%20%20%20except%20ValueError%20as%20err%3A%0A%20%20%20%20%20%20%20%20print(%22%5Cnhomogeneity%20rule%3A%22%2C%20err)%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%207.%20Feed%20the%20Result%20to%20a%20Forecaster%0A%0A%20%20%20%20Derived%20forecast%20features%20enter%20through%20the%20%60X_forecast%60%20channel.%20They%20cannot%0A%20%20%20%20be%20passed%20as%20%60actual_transformer%60%2C%20which%20processes%20single-axis%20%60X_actual%60%20data%0A%20%20%20%20and%20accepts%20actual-kind%20transformers%20only.%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%20H%2C%0A%20%20%20%20PerVintageActualTransformer%2C%0A%20%20%20%20X_actual%2C%0A%20%20%20%20X_forecast%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20y%2C%0A)%3A%0A%20%20%20%20from%20sklearn.ensemble%20import%20HistGradientBoostingRegressor%0A%0A%20%20%20%20from%20yohou.model_selection%20import%20train_test_split%0A%20%20%20%20from%20yohou.point%20import%20PointReductionForecaster%0A%0A%20%20%20%20y_train%2C%20_y_test%2C%20X_actual_train%2C%20_X_actual_test%20%3D%20train_test_split(%0A%20%20%20%20%20%20%20%20y%2C%20X_actual%2C%20test_size%3D20%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Put%20the%20transformer%20in%20the%20slot%20and%20pass%20the%20raw%20frame.%20The%20forecaster%20fits%0A%20%20%20%20%23%20it%20on%20X_forecast%20and%20applies%20it%20before%20deriving%20step%20columns%2C%20so%20the%20columns%0A%20%20%20%20%23%20reaching%20the%20estimator%20are%20built%20from%20wx_anomaly%20rather%20than%20wx_temp.%0A%20%20%20%20forecaster%20%3D%20PointReductionForecaster(%0A%20%20%20%20%20%20%20%20estimator%3DHistGradientBoostingRegressor(max_iter%3D30%2C%20max_depth%3D3%2C%20random_state%3D0)%2C%0A%20%20%20%20%20%20%20%20reduction_strategy%3D%22direct%22%2C%0A%20%20%20%20%20%20%20%20forecast_transformer%3DPerVintageActualTransformer(%0A%20%20%20%20%20%20%20%20%20%20%20%20FunctionTransformer(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20func%3Dlambda%20df%3A%20df.select(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22wx_temp%22)%20-%20pl.col(%22wx_temp%22).mean()).alias(%22wx_anomaly%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20feature_names_out%3Dlambda%20self%2C%20names%3A%20%5B%22wx_anomaly%22%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20)%0A%20%20%20%20forecaster.fit(%0A%20%20%20%20%20%20%20%20y%3Dy_train%2C%0A%20%20%20%20%20%20%20%20X_actual%3DX_actual_train%2C%0A%20%20%20%20%20%20%20%20forecasting_horizon%3DH%2C%0A%20%20%20%20%20%20%20%20X_forecast%3DX_forecast%2C%0A%20%20%20%20)%0A%20%20%20%20%23%20predict%20takes%20the%20raw%20frame%20too%3A%20the%20slot%20transforms%20it%20on%20the%20way%20in.%0A%20%20%20%20y_pred%20%3D%20forecaster.predict(forecasting_horizon%3DH%2C%20X_forecast%3DX_forecast)%0A%0A%20%20%20%20print(%22step%20columns%20built%20from%3A%22%2C%20sorted(forecaster._step_column_names_)%5B%3A3%5D)%0A%20%20%20%20print(y_pred.head())%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%5BTransform%20Forecast%20Features%5D(%2Fpages%2Fhow-to%2Ftransform-forecast-features%2F)%20for%20the%20full%20guide%0A%20%20%20%20-%20%5BTransformer%20Kinds%5D(%2Fpages%2Fexplanation%2Ftransformer-kinds%2F)%20for%20why%20the%20vintage%20axis%20rules%20out%20statefulness%0A%20%20%20%20-%20%5BFeature%20Pipelines%5D(%2Fpages%2Fexplanation%2Ffeature-pipelines%2F)%20for%20how%20composition%20derives%20its%20kind%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
9267ad8334678f08ca9ef329a4a74a55