import%20marimo%0A%0A__generated_with%20%3D%20%220.23.9%22%0Aapp%20%3D%20marimo.App(width%3D%22full%22)%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%20Imports%2C%20Globally%20Useful%20Functions%2C%20Connection%20handler%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%20%23%20Always%20use%20these%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20import%20pandas%20as%20pd%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20from%20matplotlib.patches%20import%20Ellipse%0A%0A%20%20%20%20%23%20db%20layer%0A%20%20%20%20from%20sqlalchemy%20import%20text%0A%20%20%20%20from%20db%20import%20pg%0A%0A%20%20%20%20%23%20notebook%20specific%20stuff%0A%20%20%20%20from%20sklearn.preprocessing%20import%20StandardScaler%0A%20%20%20%20from%20sklearn.decomposition%20import%20PCA%0A%20%20%20%20from%20sklearn.cluster%20import%20KMeans%0A%0A%20%20%20%20import%20statsmodels.api%20as%20sm%0A%20%20%20%20from%20statsmodels.stats.outliers_influence%20import%20variance_inflation_factor%0A%0A%20%20%20%20from%20xgboost%20import%20XGBRegressor%0A%20%20%20%20from%20sklearn.model_selection%20import%20train_test_split%0A%20%20%20%20from%20sklearn.metrics%20import%20mean_absolute_error%0A%20%20%20%20from%20xgboost%20import%20plot_importance%0A%20%20%20%20from%20sklearn.model_selection%20import%20GridSearchCV%0A%0A%20%20%20%20from%20skopt%20import%20BayesSearchCV%0A%20%20%20%20from%20skopt.space%20import%20Real%2C%20Integer%0A%20%20%20%20from%20xgboost%20import%20XGBRegressor%0A%20%20%20%20from%20sklearn.model_selection%20import%20train_test_split%0A%20%20%20%20from%20sklearn.metrics%20import%20mean_absolute_error%0A%0A%20%20%20%20from%20sklearn.neural_network%20import%20MLPRegressor%0A%0A%20%20%20%20def%20plot_ols_importance(ols_model%2C%20top_n%3D15)%3A%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20Plots%20the%20top%20N%20most%20important%20features%20from%20a%20statsmodels%20OLS%20model.%0A%20%20%20%20%20%20%20%20Sorted%20by%20Absolute%20t-statistic%20(Signal-to-Noise).%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20%23%20Extract%20the%20data%20(Dropping%20'const'%20because%20the%20intercept%20ruins%20the%20visual%20scale)%0A%20%20%20%20%20%20%20%20t_stats%20%3D%20ols_model.tvalues.drop('const'%2C%20errors%3D'ignore')%0A%20%20%20%20%20%20%20%20coefs%20%3D%20ols_model.params.drop('const'%2C%20errors%3D'ignore')%0A%20%20%20%20%20%20%20%20conf_int%20%3D%20ols_model.conf_int().drop('const'%2C%20errors%3D'ignore')%0A%0A%20%20%20%20%20%20%20%20%23%20Calculate%20the%20error%20whisker%20length%20(distance%20from%20coefficient%20to%20the%20lower%20CI%20bound)%0A%20%20%20%20%20%20%20%20errors%20%3D%20coefs%20-%20conf_int%5B0%5D%20%0A%0A%20%20%20%20%20%20%20%20%23%20Create%20a%20DataFrame%20and%20sort%20by%20Absolute%20t-statistic%0A%20%20%20%20%20%20%20%20summary_df%20%3D%20pd.DataFrame(%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20'coef'%3A%20coefs%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20'error'%3A%20errors%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20't_stat_abs'%3A%20t_stats.abs()%0A%20%20%20%20%20%20%20%20%7D)%0A%0A%20%20%20%20%20%20%20%20%23%20Sort%20ascending%20and%20take%20the%20tail%20so%20the%20most%20important%20is%20at%20the%20top%20of%20the%20plot%0A%20%20%20%20%20%20%20%20summary_df%20%3D%20summary_df.sort_values(by%3D't_stat_abs'%2C%20ascending%3DTrue).tail(top_n)%0A%0A%20%20%20%20%20%20%20%20fig%2C%20ax%20%3D%20plt.subplots(figsize%3D(10%2C%208))%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%20the%20dots%20and%20whiskers%0A%20%20%20%20%20%20%20%20ax.errorbar(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dsummary_df%5B'coef'%5D%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dsummary_df.index%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20xerr%3Dsummary_df%5B'error'%5D%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20fmt%3D'o'%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20'o'%20makes%20it%20a%20dot%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3D'%232E86C1'%2C%20%20%20%20%20%20%20%20%23%20A%20nice%20modern%20blue%0A%20%20%20%20%20%20%20%20%20%20%20%20ecolor%3D'lightgray'%2C%20%20%20%20%20%23%20Whisker%20color%0A%20%20%20%20%20%20%20%20%20%20%20%20%23capsize%3D5%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Add%20caps%20to%20the%20whiskers%0A%20%20%20%20%20%20%20%20%20%20%20%20%23capthick%3D2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20markersize%3D6%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%23%20Draw%20a%20vertical%20line%20at%200%20(The%20%22No%20Effect%22%20line)%0A%20%20%20%20%20%20%20%20ax.axvline(x%3D0%2C%20color%3D'%23E74C3C'%2C%20linestyle%3D'--'%2C%20alpha%3D0.7)%0A%0A%20%20%20%20%20%20%20%20%23%20Clean%20up%20the%20styling%0A%20%20%20%20%20%20%20%20ax.set_title(f%22Top%20%7Btop_n%7D%20OLS%20Features%20by%20Importance%20(t-statistic)%22%2C%20fontsize%3D16%2C%20pad%3D15)%0A%20%20%20%20%20%20%20%20ax.set_xlabel(%22Impact%20(Wait%20Time%20in%20Minutes)%22%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20ax.grid(axis%3D'x'%2C%20linestyle%3D'--'%2C%20alpha%3D0.3)%0A%20%20%20%20%20%20%20%20ax.spines%5B'top'%5D.set_visible(False)%0A%20%20%20%20%20%20%20%20ax.spines%5B'right'%5D.set_visible(False)%0A%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20plt.show()%0A%0A%20%20%20%20engine%20%3D%20pg(%22gp0%22)%20%20%20%23%20Knoebels%20ride%20data%20lives%20in%20the%20gp0%20database%20(schema%3A%20knoebels)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20BayesSearchCV%2C%0A%20%20%20%20%20%20%20%20GridSearchCV%2C%0A%20%20%20%20%20%20%20%20Integer%2C%0A%20%20%20%20%20%20%20%20MLPRegressor%2C%0A%20%20%20%20%20%20%20%20Real%2C%0A%20%20%20%20%20%20%20%20StandardScaler%2C%0A%20%20%20%20%20%20%20%20XGBRegressor%2C%0A%20%20%20%20%20%20%20%20engine%2C%0A%20%20%20%20%20%20%20%20mean_absolute_error%2C%0A%20%20%20%20%20%20%20%20mo%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20pd%2C%0A%20%20%20%20%20%20%20%20pg%2C%0A%20%20%20%20%20%20%20%20plot_importance%2C%0A%20%20%20%20%20%20%20%20plot_ols_importance%2C%0A%20%20%20%20%20%20%20%20plt%2C%0A%20%20%20%20%20%20%20%20sm%2C%0A%20%20%20%20%20%20%20%20text%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%20Pull%20Data%0A%0A%20%20%20%20There%20isn't%20much%20going%20on%20in%20this%20cell%20because%20I%20offloaded%20a%20lot%20of%20the%20cleanup%20work%20to%20a%20VIEW%20in%20my%20postgres%20db.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(engine%2C%20pd%2C%20text)%3A%0A%20%20%20%20%23%20LZ_attractions_io%20carries%20one%20row%20per%20(timestamp%2C%20ride)%20and%20uses%20the%0A%20%20%20%20%23%20Item%20_id%20that%20matches%20LZ_attractions_io_poi.%0A%20%20%20%20%23%20QUERY_OLD%20%3D%20text(%0A%20%20%20%20%23%20%20%20%20%20%22%22%22%0A%20%20%20%20%23%20%20%20%20%20SELECT%20a.time_stamp%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20a._id%20%20%20%20%20%20%20%20%20%20%20%20AS%20ride_id%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20TRIM(r.name)%20%20%20%20%20%20%20%20%20%20%20AS%20ride_name%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20TRIM(c.%22name%22)%20%20%20%20%20%20%20%20%20AS%20category%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20ROUND((r.minimum_height_requirement%20*%2039.37)%3A%3Anumeric%2C%200)%20as%20minimum_height_requirement_inches%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20ROUND((r.minimum_unaccompanied_height_requirement%20*%2039.37)%3A%3Anumeric%2C%200)%20as%20minimum_unaccompanied_height_requirement_inches%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20ROUND((r.maximum_height_requirement%20*%2039.37)%3A%3Anumeric%2C%200)%20as%20maximum_height_requirement_inches%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20r.%22location%22%20%09AS%20coordinates%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20a.queue_time%20%20%20%20%20AS%20queue_time_sec%0A%20%20%20%20%23%20%20%20%20%20%20%20FROM%20knoebels.%22LZ_attractions_io%22%20a%0A%20%20%20%20%23%20%20%20%20%20%20%20JOIN%20knoebels.%22LZ_attractions_io_poi%22%20r%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20ON%20r._id%20%3D%20a._id%0A%20%20%20%20%23%20%20%20%20%20%20%20JOIN%20knoebels.%22LZ_attractions_io_categories%22%20c%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20on%20r.category%20%3D%20c._id%20%0A%20%20%20%20%23%20%20%20%20%20%20WHERE%20a.queue_time%20IS%20NOT%20NULL%0A%20%20%20%20%23%20%20%20%20%20%20ORDER%20BY%20a.time_stamp%0A%20%20%20%20%23%20%20%20%20%20%22%22%22%0A%20%20%20%20%23%20)%0A%0A%20%20%20%20QUERY%20%3D%20text(%22SELECT%20*%20FROM%20knoebels.dm_knb_queue_weather%20ORDER%20BY%20time_stamp%22)%20%23%20new%20fancier%20view%20I%20created%20in%20postgres%0A%0A%20%20%20%20df%20%3D%20pd.read_sql(QUERY%2C%20engine)%0A%0A%20%20%20%20%23%20Scraper%20writes%20naive%20timestamps%20that%20represent%20America%2FNew_York%20wall%20clock.%0A%20%20%20%20%23%20Localize%20so%20downstream%20time-of-day%20%2F%20day-of-week%20features%20are%20unambiguous.%0A%20%20%20%20df%5B%22time_stamp%22%5D%20%3D%20(%0A%20%20%20%20%20%20%20%20pd.to_datetime(df%5B%22time_stamp%22%5D)%0A%20%20%20%20%20%20%20%20.dt.tz_localize(%22America%2FNew_York%22%2C%20ambiguous%3D%22NaT%22%2C%20nonexistent%3D%22NaT%22)%0A%20%20%20%20)%0A%20%20%20%20df_raw%20%3D%20df.dropna(subset%3D%5B%22time_stamp%22%5D).reset_index(drop%3DTrue)%20%23%20drop%20any%20rows%20without%20a%20timestamp%2C%20shouldn't%20be%20possible%0A%0A%20%20%20%20print(f%22Loaded%20%7Blen(df)%3A%2C%7D%20rows%20across%20%7Bdf%5B'ride_name'%5D.nunique()%7D%20rides%22)%0A%20%20%20%20print(f%22Time%20range%3A%20%7Bdf%5B'time_stamp'%5D.min()%7D%20%E2%86%92%20%7Bdf%5B'time_stamp'%5D.max()%7D%22)%0A%20%20%20%20return%20(df_raw%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%20Park-State%20PCA%20Features%20(lagged)%0A%0A%20%20%20%20The%20same%20PCA%20from%20the%20%60writeup.ipynb%60%20notebook%2C%20but%20turned%20into%20*lagged*%0A%20%20%20%20predictors%20of%20an%20individual%20ride's%20current%20wait.%0A%0A%20%20%20%20**What%20the%20three%20axes%20mean**%20(interpreted%20in%20the%20writeup)%3A%0A%0A%20%20%20%20-%20**PC1**%20%E2%80%94%20how%20busy%20the%20whole%20park%20is%20(%E2%89%8840%25%20of%20variance)%0A%20%20%20%20-%20**PC2**%20%E2%80%94%20kiddieland%20corner%20%E2%86%94%20main%20midway%20(%E2%89%8813%25)%0A%20%20%20%20-%20**PC3**%20%E2%80%94%20fast%20spinning%2Fwhirling%20flats%20%E2%86%94%20slow%20transport%20%26%20water%20rides%0A%20%20%20%20%20%20(%E2%89%887%25%3B%20weak%20and%20uncertain%20%E2%80%94%20see%20the%20capstone%2C%20where%20we%20test%20and%20reject%20the%0A%20%20%20%20%20%20earlier%20%22daypart%22%20reading)%0A%0A%20%20%20%20**Why%20these%20are%20*per-snapshot*%2C%20not%20per-ride.**%20Each%20PC%20is%20one%20number%20for%20the%0A%20%20%20%20whole%20park%20at%20a%20given%205-min%20bin%20(a%20weighted%20blend%20of%20every%20ride's%20wait).%20So%0A%20%20%20%20they%20join%20onto%20the%20feature%20matrix%20on%20the%20**timestamp**%20%E2%80%94%20every%20ride%20row%20in%20a%0A%20%20%20%20bin%20gets%20the%20same%20PC1%2FPC2%2FPC3.%0A%0A%20%20%20%20**Why%20lag%20them.**%20The%20*un-lagged*%20PC%20at%20time%20T%20is%20built%20from%20every%20ride's%20wait%0A%20%20%20%20at%20T%20%E2%80%94%20including%20the%20ride%20we're%20predicting.%20Feeding%20that%20to%20the%20model%20is%0A%20%20%20%20target%20leakage.%20Lagging%20by%20%E2%89%A530%20min%20turns%20it%20into%20%22the%20state%20of%20the%20park%20half%0A%20%20%20%20an%20hour%20ago%2C%22%20which%20we%20genuinely%20know%20at%20predict%20time.%20Using%2030%2F60%2F90%20also%0A%20%20%20%20lets%20the%20model%20read%20the%20*trend*%20(filling%20up%20vs.%20emptying%20out)%2C%20not%20just%20the%0A%20%20%20%20level.%0A%0A%20%20%20%20**Snapping%2C%20handled%20in%20two%20places%3A**%0A%0A%20%20%20%201.%20%60build_pc_scores()%60%20floors%20%60df_raw%60's%20**tz-aware**%20timestamps%20to%205-min%0A%20%20%20%20%20%20%20bins%2C%20so%20the%20PCA%20index%20sits%20in%20the%20same%20%60America%2FNew_York%60%20tz%20as%20the%0A%20%20%20%20%20%20%20feature%20matrix.%20(The%20writeup%20uses%20*naive*%20timestamps%20%E2%80%94%20importing%20its%0A%20%20%20%20%20%20%20%60scores%60%20directly%20would%20silently%20NaN%20the%20whole%20join%20on%20the%20tz%20mismatch.)%0A%20%20%20%202.%20%60make_lagged_pcs()%60%20shifts%20the%20score%20index%20**forward**%20by%20the%20lag%2C%20and%20we%0A%20%20%20%20%20%20%20merge%20on%20the%20exact%20bin.%20A%20score%20measured%20at%20%60S%60%20is%20relabeled%20%60S%20%2B%20lag%60%20so%0A%20%20%20%20%20%20%20it%20lines%20up%20with%20the%20future%20row%20it's%20allowed%20to%20predict.%20Because%20we%20match%0A%20%20%20%20%20%20%20an%20explicit%20timestamp%20(not%20a%20row%20offset)%2C%20the%20overnight%20close%20never%20bleeds%0A%20%20%20%20%20%20%20yesterday's%20last%20reading%20into%20this%20morning%20%E2%80%94%20a%20missing%20source%20bin%20simply%0A%20%20%20%20%20%20%20yields%20%60NaN%60.%0A%0A%20%20%20%20The%20opening-hour%20rows%20(first%2030%2F60%2F90%20min%2C%20where%20%60T%20%E2%88%92%20lag%60%20doesn't%20exist)%20get%0A%20%20%20%20filled%20with%20%600%60%2C%20which%20is%20principled%20here%3A%20PCA%20scores%20are%20mean-centered%2C%20so%0A%20%20%20%20%600%60%20literally%20means%20%22average%20park%20state.%22%20For%20XGBoost%20we%20pass%20%60fill_value%3DNone%60%0A%20%20%20%20instead%20and%20let%20the%20model%20handle%20the%20%60NaN%60s%20natively.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(df_raw%2C%20pd)%3A%0A%20%20%20%20from%20sklearn.preprocessing%20import%20StandardScaler%20as%20_StandardScaler%0A%20%20%20%20from%20sklearn.decomposition%20import%20PCA%20as%20_PCA%0A%0A%20%20%20%20def%20build_pc_scores(data%2C%20freq%3D%225min%22%2C%20n_components%3D3%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20drop_like%3D(%22powersurge%22%2C%20%22power%20surge%22))%3A%0A%20%20%20%20%20%20%20%20%22%22%22Long%20(timestamp%2C%20ride)%20table%20-%3E%20per-snapshot%20PC%20scores.%0A%0A%20%20%20%20%20%20%20%20Returns%20a%20DataFrame%20indexed%20by%205-min%20bin%20(tz-aware%2C%20same%20tz%20as%20df_raw)%0A%20%20%20%20%20%20%20%20with%20columns%20PC1..PCn%20%E2%80%94%20one%20row%20per%20whole-park%20snapshot.%20Mirrors%0A%20%20%20%20%20%20%20%20writeup.ipynb%20step-for-step%2C%20but%20off%20df_raw%20so%20the%20tz%20lines%20up.%22%22%22%0A%20%20%20%20%20%20%20%20wide%20%3D%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20data.assign(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bin_ts%3Ddata%5B%22time_stamp%22%5D.dt.floor(freq)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20queue_time_min%3Ddata%5B%22queue_time_sec%22%5D%20%2F%2060.0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pivot_table(index%3D%22bin_ts%22%2C%20columns%3D%22ride_name%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20values%3D%22queue_time_min%22%2C%20aggfunc%3D%22mean%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.sort_index()%0A%20%20%20%20%20%20%20%20%20%20%20%20.ffill()%20%20%20%20%20%20%23%20carry%20last%20known%20wait%20forward%20(line%20didn't%20teleport)%0A%20%20%20%20%20%20%20%20%20%20%20%20.fillna(0)%20%20%20%20%23%20still-missing%20%3D%20pre-open%20%3D%20no%20line%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%23%20Drop%20dead%2Ferratic%20rides%20(~90%25%20imputed%20zeros%20in%20writeup).%20Your%20ride_name%0A%20%20%20%20%20%20%20%20%23%20is%20the%20pretty%20name%20(%22Power%20Surge%22)%3B%20match%20case-insensitively%20so%20either%0A%20%20%20%20%20%20%20%20%23%20that%20or%20the%20writeup's%20snake_case%20spelling%20is%20caught.%0A%20%20%20%20%20%20%20%20drop_cols%20%3D%20%5Bc%20for%20c%20in%20wide.columns%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20any(s%20in%20str(c).lower()%20for%20s%20in%20drop_like)%5D%0A%20%20%20%20%20%20%20%20wide%20%3D%20wide.drop(columns%3Ddrop_cols%2C%20errors%3D%22ignore%22)%0A%0A%20%20%20%20%20%20%20%20pca%20%3D%20_PCA(n_components%3Dn_components)%0A%20%20%20%20%20%20%20%20scores%20%3D%20pca.fit_transform(_StandardScaler().fit_transform(wide))%0A%20%20%20%20%20%20%20%20cols%20%3D%20%5Bf%22PC%7Bi%20%2B%201%7D%22%20for%20i%20in%20range(n_components)%5D%0A%20%20%20%20%20%20%20%20print(%22park-state%20PCA%3A%22%2C%20%22%2C%20%22.join(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7Bc%7D%20%7Bv%3A.1%25%7D%22%20for%20c%2C%20v%20in%20zip(cols%2C%20pca.explained_variance_ratio_))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22(sum%20%7Bpca.explained_variance_ratio_.sum()%3A.1%25%7D%20of%20%7Bwide.shape%5B1%5D%7D%20rides)%22)%0A%20%20%20%20%20%20%20%20return%20pd.DataFrame(scores%2C%20index%3Dwide.index%2C%20columns%3Dcols)%0A%0A%20%20%20%20def%20make_lagged_pcs(pc_scores%2C%20lags%3D(30%2C%2060%2C%2090))%3A%0A%20%20%20%20%20%20%20%20%22%22%22Relabel%20the%20score%20index%20S%20-%3E%20S%2Blag%20so%20a%20score%20measured%20at%20S%20aligns%0A%20%20%20%20%20%20%20%20with%20the%20row%20at%20T%20%3D%20S%2Blag%20(row%20T%20sees%20the%20park%20as%20it%20was%20at%20T-lag).%0A%20%20%20%20%20%20%20%20Returns%20one%20frame%3A%20PC1_lag30%2C%20PC2_lag30%2C%20...%2C%20PC3_lag90.%22%22%22%0A%20%20%20%20%20%20%20%20frames%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20lag%20in%20lags%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20pc_scores.add_suffix(f%22_lag%7Blag%7D%22).copy()%0A%20%20%20%20%20%20%20%20%20%20%20%20f.index%20%3D%20f.index%20%2B%20pd.Timedelta(minutes%3Dlag)%0A%20%20%20%20%20%20%20%20%20%20%20%20frames.append(f)%0A%20%20%20%20%20%20%20%20out%20%3D%20pd.concat(frames%2C%20axis%3D1)%20%20%20%20%20%20%20%20%20%23%20aligns%20on%20the%20shared%205-min%20grid%0A%20%20%20%20%20%20%20%20out.index.name%20%3D%20pc_scores.index.name%20%20%20%23%20'bin_ts'%0A%20%20%20%20%20%20%20%20return%20out%0A%0A%20%20%20%20def%20add_lagged_pc_features(data%2C%20lagged_pcs%2C%20freq%3D%225min%22%2C%20fill_value%3D0.0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20keep_lags%3DNone)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Pipe%20step%3A%20join%20lagged%20park-state%20PCs%20onto%20each%20row%20by%20its%205-min%20bin.%0A%0A%20%20%20%20%20%20%20%20keep_lags%3D(30%2C)%20keeps%20only%20the%2030-min%20lag%20%E2%80%94%20for%20OLS%2FNN%2C%20where%20lag60%2F90%0A%20%20%20%20%20%20%20%20are%20too%20autocorrelated%20with%20lag30%20to%20add%20partial%20signal%20(fat%20VIFs).%20None%0A%20%20%20%20%20%20%20%20keeps%20all%20lags%2C%20which%20XGB%20happily%20exploits%20as%20a%20trend%20(filling%20vs.%0A%20%20%20%20%20%20%20%20emptying).%20fill_value%3D0%20%3D%3D%20'average%20park%20state'%20(PCA%20scores%20are%0A%20%20%20%20%20%20%20%20mean-centered)%3B%20pass%20None%20for%20XGB%20to%20keep%20NaNs%20it%20handles%20natively.%22%22%22%0A%20%20%20%20%20%20%20%20pcs%20%3D%20lagged_pcs%0A%20%20%20%20%20%20%20%20if%20keep_lags%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20suffixes%20%3D%20tuple(f%22_lag%7Blag%7D%22%20for%20lag%20in%20keep_lags)%0A%20%20%20%20%20%20%20%20%20%20%20%20pcs%20%3D%20lagged_pcs%5B%5Bc%20for%20c%20in%20lagged_pcs.columns%20if%20c.endswith(suffixes)%5D%5D%0A%20%20%20%20%20%20%20%20out%20%3D%20data.assign(_bin_ts%3Ddata%5B%22time_stamp%22%5D.dt.floor(freq))%0A%20%20%20%20%20%20%20%20out%20%3D%20out.merge(pcs%2C%20how%3D%22left%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20left_on%3D%22_bin_ts%22%2C%20right_index%3DTrue).drop(columns%3D%22_bin_ts%22)%0A%20%20%20%20%20%20%20%20if%20fill_value%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%20%3D%20list(pcs.columns)%0A%20%20%20%20%20%20%20%20%20%20%20%20out%5Bcols%5D%20%3D%20out%5Bcols%5D.fillna(fill_value)%0A%20%20%20%20%20%20%20%20return%20out%0A%0A%20%20%20%20lagged_pcs%20%3D%20make_lagged_pcs(build_pc_scores(df_raw)%2C%20lags%3D(30%2C%2060%2C%2090))%0A%20%20%20%20return%20add_lagged_pc_features%2C%20lagged_pcs%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%20Feature%20Engineering%20Pipeline%0A%0A%20%20%20%20This%20pipeline%20transforms%20raw%20ride%20queue%20data%20into%20a%20model-ready%20feature%20matrix%20via%20a%0A%20%20%20%20chain%20of%20composable%20functions%2C%20all%20wired%20together%20at%20the%20end%20by%20%60get_features()%60.%0A%0A%20%20%20%20The%20pipeline%20supports%20two%20axes%20of%20configuration%3A%0A%20%20%20%20-%20**%60model_flavor%60**%20%E2%80%94%20%60'rides'%60%20(A%20dummy%20column%20for%20each%20ride%20per%20row)%20or%20%60'categories'%60%0A%20%20%20%20%20%20(ride_id%20and%20ride_name%20dropped%2C%20ride_catagory%20dummied)%0A%20%20%20%20-%20**%60model_type%60**%20%E2%80%94%20%60'ols'%60%20(linear%20regression)%2C%20%60'xgb'%60%20(XGBoost)%2C%20or%20%60'nn'%60%0A%20%20%20%20%20%20(MLPRegressor)%0A%0A%20%20%20%20%23%23%23%20Steps%0A%0A%20%20%20%201.%20**%60fill_height_nulls%60**%20*(categories%20only)*%20%E2%80%94%20Fills%20missing%20height%20requirement%20fields%3A%0A%20%20%20%20%20%20%20min%2Funaccompanied%20heights%20default%20to%200%20in.%2C%20max%20height%20defaults%20to%20120%20in.%20This%20is%20categories%20only%20because%20the%20height%20requirements%20were%20so%20highly%20correlated%20with%20specific%20ride%20dummies%20that%20it%20actually%20blew%20up%20the%20OLS%20algorithm%0A%0A%20%20%20%202.%20**%60add_time_features%60**%20%E2%80%94%20Derives%20%60queue_time_min%60%20(because%20queue_time%20is%20returned%20from%20the%20api%20in%20seconds%2C%20despite%20only%20ever%20being%20provided%20in%205%20minute%20increments).%20%60hour%60%2C%20%60month%60%2C%20and%20%60day_name%60%0A%20%20%20%20%20%20%20from%20the%20raw%20timestamp%20of%20the%20scrape.%0A%0A%20%20%20%203.%20**%60create_dummies%60**%20%E2%80%94%20One-hot%20encodes%20%60day_name%60%2C%20%60month%60%2C%20%60weather_description%60%2C%0A%20%20%20%20%20%20%20and%20either%20%60ride_name%60%20or%20%60category%60%20depending%20on%20flavor.%0A%0A%20%20%20%204.%20**%60add_promo_features%60**%20%E2%80%94%20Adds%20binary%20indicators%20for%20park%20promotions%3A%0A%20%20%20%20%20%20%20-%20%60bargain_night%60%3A%20Friday%20evenings%205%E2%80%939%20PM%0A%20%20%20%20%20%20%20-%20%60sundown_special%60%3A%20Thursday%2FFriday%204%E2%80%939%20PM%0A%20%20%20%20%20%20%20-%20%60bargain_thrill_interaction%60%3A%20interaction%20term%20for%20thrill%20rides%20(or%20the%20Thrill%0A%20%20%20%20%20%20%20%20%20category)%20during%20Bargain%20Night%2C%20capturing%20the%20expected%20demand%20spike.%0A%0A%20%20%20%205.%20**%60standardize_numerics%60**%20%E2%80%94%20Z-score%20standardizes%20continuous%20columns%20for%20OLS%20and%0A%20%20%20%20%20%20%20the%20neural%20network.%20Skipped%20entirely%20for%20XGBoost%2C%20which%20is%20scale-invariant.%0A%0A%20%20%20%206.%20**%60finalize_df%60**%20%E2%80%94%20Drops%20bookkeeping%20columns%20(%60time_stamp%60%2C%20%60coordinates%60%2C%20etc.)%0A%20%20%20%20%20%20%20and%2C%20for%20OLS%2C%20removes%20one%20dummy%20per%20group%20to%20establish%20reference%20baselines%0A%20%20%20%20%20%20%20(%60day_name_Sunday%60%2C%20%60month_5%60%2C%20%60weather_description_Clear%20sky%60%2C%20and%20either%0A%20%20%20%20%20%20%20%60ride_name_Grand%20Carousel%60%20or%20%60category_Family%60).%0A%0A%20%20%20%20%23%23%23%20How%20the%20Output%20Matrix%20Varies%20by%20Configuration%0A%0A%20%20%20%20The%20shape%20and%20content%20of%20the%20final%20feature%20matrix%20depend%20heavily%20on%20which%20combination%0A%20%20%20%20of%20%60model_flavor%60%20and%20%60model_type%60%20is%20requested.%20The%20%60flavor%60%20parameter%20controls%0A%20%20%20%20*which%20entity%20is%20being%20modeled*%3A%20the%20%60'rides'%60%20flavor%20retains%20individual%20ride%20identity%0A%20%20%20%20columns%20(one%20dummy%20per%20ride)%20and%20drops%20height%20requirements%2C%20since%20those%20are%20constant%0A%20%20%20%20per%20ride%20and%20would%20be%20redundant%3B%20the%20%60'categories'%60%20flavor%20instead%20retains%20height%0A%20%20%20%20requirement%20columns%20%E2%80%94%20which%20now%20vary%20meaningfully%20across%20grouped%20observations%20%E2%80%94%20and%0A%20%20%20%20encodes%20the%20broader%20ride%20category%20(Thrill%2C%20Family%2C%20etc.)%20rather%20than%20individual%20ride%0A%20%20%20%20names.%20The%20%60model_type%60%20parameter%20then%20determines%20*how*%20the%20matrix%20is%20numerically%0A%20%20%20%20prepared.%20XGBoost%20is%20indifferent%20to%20feature%20scale%20and%20has%20no%20problem%20with%20a%20full%20set%0A%20%20%20%20of%20dummy%20variables%2C%20so%20the%20matrix%20is%20passed%20through%20largely%20as-is.%20OLS%20and%20the%0A%20%20%20%20MLPRegressor%2C%20however%2C%20share%20the%20same%20preparation%20pathway%2C%20since%20both%20are%20fundamentally%0A%20%20%20%20rooted%20in%20linear%20algebra%3A%20weights%20and%20coefficients%20are%20learned%20by%20minimizing%20loss%20over%0A%20%20%20%20dot%20products%2C%20which%20means%20a%20feature%20measured%20in%20hundreds%20(e.g.%20precipitation%20in%0A%20%20%20%20thousandths%20of%20an%20inch)%20can%20dominate%20a%20feature%20measured%20in%20single%20digits%20(e.g.%20hour%20of%0A%20%20%20%20day)%20simply%20due%20to%20scale%20rather%20than%20predictive%20signal.%20For%20this%20reason%2C%20both%20%60'ols'%60%0A%20%20%20%20and%20%60'nn'%60%20receive%20Z-score%20standardized%20continuous%20features.%20OLS%20additionally%20requires%0A%20%20%20%20one%20dummy%20variable%20dropped%20from%20each%20categorical%20group%20to%20avoid%20the%20dummy%20variable%20trap%0A%20%20%20%20%E2%80%94%20where%20columns%20within%20a%20group%20always%20sum%20to%201%2C%20making%20the%20design%20matrix%20singular%20and%0A%20%20%20%20OLS%20insoluble.%20The%20dropped%20column%20becomes%20the%20*reference%20category*%3A%20the%20implicit%0A%20%20%20%20baseline%20all%20other%20coefficients%20in%20that%20group%20are%20interpreted%20against.%20For%20example%2C%0A%20%20%20%20with%20%60day_name_Sunday%60%20dropped%2C%20the%20coefficient%20on%20%60day_name_Monday%60%20represents%20the%0A%20%20%20%20*difference*%20in%20expected%20queue%20time%20on%20Mondays%20relative%20to%20Sundays%2C%20holding%20everything%0A%20%20%20%20else%20constant.%20The%20MLPRegressor%20doesn't%20carry%20this%20requirement%2C%20but%20benefits%20from%20the%0A%20%20%20%20same%20standardization%20for%20the%20same%20underlying%20reason.%0A%0A%20%20%20%20%23%23%23%20Usage%0A%0A%20%20%20%20%60%60%60python%0A%20%20%20%20df_features%20%3D%20get_features(df_raw%2C%20f%3D'rides'%2C%20t%3D'xgb')%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20All%20steps%20flow%20through%20pandas%20%60.pipe()%60%2C%20keeping%20each%20transformation%20isolated%2C%0A%20%20%20%20testable%2C%20and%20easy%20to%20swap%20out.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(StandardScaler%2C%20add_lagged_pc_features%2C%20pd)%3A%0A%20%20%20%20def%20add_time_features(data)%3A%0A%20%20%20%20%20%20%20%20return%20data.assign(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20queue_time_min%20%3D%20data%5B%22queue_time_sec%22%5D%20%2F%2060.0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20hour%20%3D%20data%5B'time_stamp'%5D.dt.hour%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20month%20%3D%20data%5B'time_stamp'%5D.dt.month%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20day_name%20%3D%20data%5B'time_stamp'%5D.dt.day_name()%20%23%20'Monday'%2C%20'Tuesday'%2C%20etc.%20going%20to%20generate%20dummies%20from%20these%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20def%20create_dummies(data%2C%20model_flavor%3D'rides')%3A%0A%20%20%20%20%20%20%20%20data%20%3D%20pd.get_dummies(data%2C%20columns%3D%5B'day_name'%5D%2C%20dtype%3Dint)%20%23%20drop%20'day_name_Sunday'%20for%20OLS%0A%20%20%20%20%20%20%20%20data%20%3D%20pd.get_dummies(data%2C%20columns%3D%5B'month'%5D%2C%20dtype%3Dint)%20%23%20drop%20'month_5'%20for%20OLS%0A%20%20%20%20%20%20%20%20data%20%3D%20pd.get_dummies(data%2C%20columns%3D%5B'weather_description'%5D%2C%20dtype%3Dint)%20%23%20drop%20'weather_description_Clear%20sky'%20for%20OLS%0A%0A%20%20%20%20%20%20%20%20if%20model_flavor%20%3D%3D%20'rides'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20pd.get_dummies(data%2C%20columns%3D%5B'ride_name'%5D%2C%20dtype%3Dint)%20%23%20drop%20'ride_name_Grand%20Carousel'%20for%20OLS%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20pd.get_dummies(data%2C%20columns%3D%5B'category'%5D%2C%20dtype%3Dint)%20%23%20drop%20family%20category%20for%20OLS%2C%20need%20to%20get%20name%0A%0A%20%20%20%20def%20encode_weather(data%2C%20model_type%3D'ols')%3A%0A%20%20%20%20%20%20%20%20%22%22%22Weather%20encoding%20differs%20by%20model.%0A%0A%20%20%20%20%20%20%20%20-%20xgb%3A%20keep%20the%20ordinal%20%60wmo_weather_code%60%20(WMO%20codes%20are%20intensity-%0A%20%20%20%20%20%20%20%20%20%20ordered%20ints%20%E2%80%94%200-3%20clear%2Fcloud%2C%2045%2F48%20fog%2C%2051%2F53%2F55%20drizzle%2C%2061%2F63%2F65%0A%20%20%20%20%20%20%20%20%20%20rain...)%20and%20drop%20the%20sparse%20one-hot%20description%20dummies.%20Trees%20split%20on%0A%20%20%20%20%20%20%20%20%20%20thresholds%2C%20so%20they%20recover%20the%20non-linear%20severity%20steps%20a%20linear%20term%0A%20%20%20%20%20%20%20%20%20%20can't%2C%20and%20we%20shed%20~10%20sparse%20columns.%20(WMO%20codes%20are%20only%20ordered%0A%20%20%20%20%20%20%20%20%20%20*within*%20a%20weather%20family%2C%20not%20across%2C%20but%20trees%20isolate%20ranges%20fine.)%0A%20%20%20%20%20%20%20%20-%20ols%2Fnn%3A%20a%20single%20linear%20term%20on%20WMO%20codes%20would%20be%20nonsense%20(a%0A%20%20%20%20%20%20%20%20%20%20thunderstorm%20code%2095%20!%3D%2095x%20a%20drizzle)%2C%20so%20keep%20the%20description%20dummies%0A%20%20%20%20%20%20%20%20%20%20BUT%20drop%20the%20rain%2Fdrizzle%20family%20%E2%80%94%20those%20levels%20are%20rare%20(Heavy%20rain%0A%20%20%20%20%20%20%20%20%20%20std%20err%20~0.5)%20and%20collinear%20with%20the%20continuous%20precipitation_in%20%2F%0A%20%20%20%20%20%20%20%20%20%20cloud_cover_pct%2C%20so%20they%20only%20add%20noise.%20Fog%20stays%20(strong%2C%20distinct%0A%20%20%20%20%20%20%20%20%20%20visibility%20signal)%3B%20the%20cloud-cover%20descriptors%20stay%20(significant).%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20desc_cols%20%3D%20list(data.filter(like%3D'weather_description_').columns)%0A%0A%20%20%20%20%20%20%20%20if%20model_type%20%3D%3D%20'xgb'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3Ddesc_cols)%0A%20%20%20%20%20%20%20%20%20%20%20%20data%5B'wmo_weather_code'%5D%20%3D%20pd.to_numeric(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20data%5B'wmo_weather_code'%5D%2C%20errors%3D'coerce')%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20data%0A%0A%20%20%20%20%20%20%20%20%23%20ols%20%2F%20nn%3A%20drop%20only%20the%20rain%20%2B%20drizzle%20dummies%3B%20keep%20Fog%20%2B%20cloud%20terms%0A%20%20%20%20%20%20%20%20rain_drizzle%20%3D%20%5Bc%20for%20c%20in%20desc_cols%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20'rain'%20in%20c.lower()%20or%20'drizzle'%20in%20c.lower()%5D%0A%20%20%20%20%20%20%20%20return%20data.drop(columns%3Drain_drizzle)%0A%0A%20%20%20%20%23%20only%20used%20for%20category%20flavor%20models%0A%20%20%20%20def%20fill_height_nulls(data)%3A%0A%20%20%20%20%20%20%20%20return%20data.assign(%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Fill%20missing%20min%20heights%20with%200%20inches%0A%20%20%20%20%20%20%20%20%20%20%20%20minimum_height_requirement_inches%20%3D%20lambda%20x%3A%20x%5B'minimum_height_requirement_inches'%5D.fillna(0)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20minimum_unaccompanied_height_requirement_inches%20%3D%20lambda%20x%3A%20x%5B'minimum_unaccompanied_height_requirement_inches'%5D.fillna(0)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Fill%20missing%20max%20heights%20with%20120%20inches%0A%20%20%20%20%20%20%20%20%20%20%20%20maximum_height_requirement_inches%20%3D%20lambda%20x%3A%20x%5B'maximum_height_requirement_inches'%5D.fillna(120)%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%23%20this%20relies%20on%20columns%20added%20by%20creat_dummies()%0A%20%20%20%20def%20add_promo_features(data%2C%20model_flavor%3D'rides')%3A%0A%20%20%20%20%20%20%20%20%23%20Define%20universal%20features%0A%20%20%20%20%20%20%20%20feature_dict%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20'bargain_night'%3A%20lambda%20x%3A%20((x%5B'day_name_Friday'%5D%20%3D%3D%201)%20%26%20(x%5B'hour'%5D%20%3E%3D%2017)%20%26%20(x%5B'hour'%5D%20%3C%2021)).astype(int)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20'sundown_special'%3A%20lambda%20x%3A%20(((x%5B'day_name_Thursday'%5D%20%3D%3D%201)%20%7C%20(x%5B'day_name_Friday'%5D%20%3D%3D%201))%20%26%20(x%5B'hour'%5D%20%3E%3D%2016)%20%26%20(x%5B'hour'%5D%20%3C%2021)).astype(int)%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%23%20We%20are%20building%20the%20dictionary%20outside%20of%20the%20assign()%20call%20because%0A%20%20%20%20%20%20%20%20%23%20of%20this%20conditional%20logic%20depending%20on%20the%20flavor%20of%20model%20we're%20after%0A%20%20%20%20%20%20%20%20if%20model_flavor%20%3D%3D%20'categories'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20feature_dict%5B'bargain_thrill_interaction'%5D%20%3D%20lambda%20x%3A%20x%5B'bargain_night'%5D%20*%20x%5B'category_Thrill'%5D%0A%0A%20%20%20%20%20%20%20%20elif%20model_flavor%20%3D%3D%20'rides'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20feature_dict%5B'bargain_thrill_interaction'%5D%20%3D%20lambda%20x%3A%20x%5B'bargain_night'%5D%20*%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%5B'ride_name_Impulse'%5D%20%7C%20x%5B'ride_name_Phoenix'%5D%20%7C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%5B'ride_name_Twister'%5D%20%7C%20x%5B'ride_name_Flying%20Turns'%5D%20%7C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%5B'ride_name_Haunted%20Mansion'%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%23%20Unpack%20the%20dictionary%20directly%20into%20.assign()%0A%20%20%20%20%20%20%20%20return%20data.assign(**feature_dict)%0A%0A%20%20%20%20def%20standardize_numerics(data%2C%20model_type%3D'ols')%3A%0A%20%20%20%20%20%20%20%20if%20model_type%20%3D%3D%20'xgb'%3A%20%23%20aligning%20function%20signature%20with%20the%20rest%20to%20make%20generalizing%20the%20pipeline%20easier%20below%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20data%20%20%20%20%20%20%20%20%20%23%20do%20nothing%20if%20desired%20model%20type%20is%20xgb%0A%0A%20%20%20%20%20%20%20%20%23%20Create%20a%20copy%20so%20as%20not%20to%20mutate%20the%20original%20dataframe%20in%20place%0A%20%20%20%20%20%20%20%20df%20%3D%20data.copy()%0A%0A%20%20%20%20%20%20%20%20%23%20define%20ONLY%20the%20continuous%20columns%0A%20%20%20%20%20%20%20%20%23%20no%20dummies%20allowed%0A%20%20%20%20%20%20%20%20continuous_cols%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20'temperature_f'%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20'humidity_pct'%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20'precipitation_in'%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20'cloud_cover_pct'%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20'wind_mph'%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20'wind_gust_mph'%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20'hour'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20'minimum_height_requirement_inches'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20'minimum_unaccompanied_height_requirement_inches'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20'maximum_height_requirement_inches'%0A%20%20%20%20%20%20%20%20%5D%0A%0A%20%20%20%20%20%20%20%20%23%20z-score%20the%20lagged%20PCs%20too.%20Raw%20PCA%20scores%20are%20mean-centered%20but%20not%0A%20%20%20%20%20%20%20%20%23%20unit-variance%2C%20so%20without%20this%20their%20coefficients%20aren't%20comparable%20to%0A%20%20%20%20%20%20%20%20%23%20the%20other%20(standardized)%20continuous%20features.%20Names%20are%20dynamic%0A%20%20%20%20%20%20%20%20%23%20(PC1_lag30%2C%20...)%2C%20so%20match%20by%20pattern%20rather%20than%20hard-coding.%0A%20%20%20%20%20%20%20%20continuous_cols%20%2B%3D%20%5Bc%20for%20c%20in%20df.columns%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20c.startswith(%22PC%22)%20and%20%22_lag%22%20in%20c%5D%0A%0A%20%20%20%20%20%20%20%20cols_to_scale%20%3D%20%5Bcol%20for%20col%20in%20continuous_cols%20if%20col%20in%20df.columns%5D%0A%0A%20%20%20%20%20%20%20%20scaler%20%3D%20StandardScaler()%0A%0A%20%20%20%20%20%20%20%20%23%20replace%20the%20raw%20numbers%20with%20their%20Z-scores%3A%20mean%3D0%2C%20std%3D1%0A%20%20%20%20%20%20%20%20df%5Bcols_to_scale%5D%20%3D%20scaler.fit_transform(df%5Bcols_to_scale%5D)%0A%0A%20%20%20%20%20%20%20%20return%20df%0A%0A%20%20%20%20def%20finalize_df(data%2C%20model_flavor%3D'rides'%2C%20model_type%3D'ols')%3A%0A%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3D%5B'time_stamp'%2C%20'coordinates'%2C%20'rain_in'%2C%20'feels_like_f'%5D)%0A%20%20%20%20%20%20%20%20if%20model_type%20!%3D%20'xgb'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20xgb%20keeps%20wmo_weather_code%20as%20its%20ordinal%20weather%20feature%20(see%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20encode_weather)%3B%20ols%2Fnn%20use%20the%20description%20dummies%20instead.%0A%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3D%5B'wmo_weather_code'%5D)%0A%0A%20%20%20%20%20%20%20%20if%20model_type%20in%5B'ols'%2C%20'nn'%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3D%5B'weather_description_Clear%20sky'%2C%20'day_name_Sunday'%2C%20'month_5'%5D)%20%23%20create%20our%20linreg%20baselines%20from%20dummied%20variables%0A%0A%20%20%20%20%20%20%20%20if%20model_flavor%20%3D%3D%20'rides'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3D%5B'ride_id'%2C%20'category'%2C%20'minimum_height_requirement_inches'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'minimum_unaccompanied_height_requirement_inches'%2C%20'maximum_height_requirement_inches'%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3Ddata.filter(like%3D'category_').columns)%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20model_type%20in%5B'ols'%2C%20'nn'%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3D%5B'ride_name_Grand%20Carousel'%5D)%0A%20%20%20%20%20%20%20%20else%3A%20%23%20categories%0A%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3D%5B'ride_name'%2C%20'ride_id'%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3Ddata.filter(like%3D'ride_name_').columns)%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20model_type%20in%5B'ols'%2C%20'nn'%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20data%20%3D%20data.drop(columns%3D%5B'category_Family'%5D)%20%20%20%20%0A%0A%20%20%20%20%20%20%20%20return%20data%0A%0A%20%20%20%20def%20get_features(data%2C%20lagged_pcs%2C%20f%3D'rides'%2C%20t%3D'xgb')%3A%0A%20%20%20%20%20%20%20%20%23%20eventualy%20decide%20whether%20to%20load%20cached%20featureset%20from%20parquet%20files%0A%20%20%20%20%20%20%20%20%23%20or%20re-generate%20here.%0A%0A%20%20%20%20%20%20%20%20print(f%22Creating%20matrix%20for%20%7Bf%7D-flavored%20model%20for%20use%20with%20%7Bt%7D%22)%0A%20%20%20%20%20%20%20%20return%20(data%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(fill_height_nulls)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(add_time_features)%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20join%20the%20lagged%20park-state%20PCs%20while%20time_stamp%20still%20exists%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20(finalize_df%20drops%20it).%20XGB%20keeps%20all%20lags%20%2B%20NaNs%3B%20ols%2Fnn%20get%20only%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20the%20lag30%20(others%20too%20collinear)%20and%200-fill%20(%3D%20park%20mean).%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(add_lagged_pc_features%2C%20lagged_pcs%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fill_value%3D(None%20if%20t%20%3D%3D%20'xgb'%20else%200.0)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20keep_lags%3D(None%20if%20t%20%3D%3D%20'xgb'%20else%20(30%2C)))%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(create_dummies%2C%20model_flavor%3Df)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(encode_weather%2C%20model_type%3Dt)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(add_promo_features%2C%20model_flavor%3Df)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(standardize_numerics%2C%20model_type%3Dt)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(finalize_df%2C%20model_flavor%3Df%2C%20model_type%3Dt)%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%23%20df_features_rides%20%3D%20get_features(df_raw%2C%20t%3D'xgb')%0A%20%20%20%20return%20(get_features%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%20Model%20Training%20Pipeline%0A%0A%20%20%20%20This%20module%20contains%20the%20training%20logic%20for%20all%20three%20model%20types%2C%20dispatched%20through%0A%20%20%20%20a%20single%20entry%20point%2C%20%60do_train()%60.%20Calling%20%60do_train()%60%20with%20the%20desired%20%60f%60%20(flavor)%0A%20%20%20%20and%20%60t%60%20(type)%20parameters%20handles%20feature%20matrix%20construction%20via%20%60get_features()%60%2C%0A%20%20%20%20target%2Ffeature%20separation%2C%20and%20routing%20to%20the%20appropriate%20model%20function%20%E2%80%94%20keeping%20the%0A%20%20%20%20entire%20workflow%20launchable%20from%20a%20single%20line.%0A%0A%20%20%20%20%23%23%23%20Model%20Functions%0A%0A%20%20%20%20**%60do_ols(y%2C%20X)%60**%20%E2%80%94%20Fits%20an%20Ordinary%20Least%20Squares%20regression%20using%20%60statsmodels%60.%0A%20%20%20%20Note%20that%20%60statsmodels%60%20does%20not%20add%20an%20intercept%20term%20by%20default%2C%20so%20a%20constant%20column%0A%20%20%20%20is%20prepended%20to%20%60X%60%20manually%20via%20%60sm.add_constant()%60%20before%20fitting.%20Returns%20the%20fitted%0A%20%20%20%20model%20and%20prints%20a%20full%20summary%20table%20alongside%20a%20feature%20importance%20plot.%0A%0A%20%20%20%20**%60do_nn(y%2C%20X%2C%20split_method)%60**%20%E2%80%94%20Trains%20a%20%60MLPRegressor%60%20with%20two%20hidden%20layers%20(64%0A%20%20%20%20nodes%2C%20then%2032)%2C%20ReLU%20activation%2C%20and%20the%20Adam%20solver.%20Architecture%20comments%20are%0A%20%20%20%20left%20inline%20intentionally%20%E2%80%94%20the%20hyperparameter%20choices%20here%20are%20worth%20understanding%2C%0A%20%20%20%20not%20just%20running.%20Supports%20both%20chronological%20and%20random%20train%2Ftest%20splitting.%0A%0A%20%20%20%20**%60do_xgb(y%2C%20X%2C%20xgb_split_method%2C%20xgb_tuning)%60**%20%E2%80%94%20Trains%20an%20%60XGBRegressor%60%20with%0A%20%20%20%20optional%20hyperparameter%20search.%20Supports%20two%20tuning%20strategies%20%E2%80%94%20%60'grid'%60%20and%0A%20%20%20%20%60'bayes'%60%20%E2%80%94%20and%20two%20split%20strategies.%20Returns%20the%20best%20estimator%20and%20plots%20the%20top%2015%0A%20%20%20%20most%20important%20features%20by%20weight.%0A%0A%20%20%20%20%23%23%23%20Train%2FTest%20Split%20Strategy%0A%0A%20%20%20%20Both%20%60do_nn()%60%20and%20%60do_xgb()%60%20support%20two%20splitting%20methods%3A%20%60'chrono'%60%0A%20%20%20%20(chronological)%20and%20a%20random%20%60train_test_split()%60.%20For%20this%20dataset%20the%20chronological%0A%20%20%20%20split%20is%20the%20correct%20choice.%20Because%20we're%20forecasting%20wait%20times%20from%20timestamped%0A%20%20%20%20observations%2C%20a%20random%20split%20would%20allow%20the%20model%20to%20train%20on%20future%20data%20and%20test%20on%0A%20%20%20%20past%20data%20%E2%80%94%20information%20it%20couldn't%20possibly%20have%20in%20a%20real%20deployment%20scenario.%20This%0A%20%20%20%20is%20a%20form%20of%20**data%20leakage**%20that%20produces%20unrealistically%20optimistic%20evaluation%0A%20%20%20%20metrics.%20The%20chronological%20split%20preserves%20temporal%20order%20by%20using%20the%20first%2080%25%20of%0A%20%20%20%20observations%20for%20training%20and%20the%20final%2020%25%20for%20testing%2C%20honestly%20simulating%20what%20the%0A%20%20%20%20model%20would%20see%20in%20production.%20The%20random%20split%20option%20is%20retained%20purely%20for%0A%20%20%20%20comparison%20purposes.%0A%0A%20%20%20%20%23%23%23%20Hyperparameter%20Tuning%20in%20%60do_xgb()%60%0A%0A%20%20%20%20XGBoost%20exposes%20several%20hyperparameters%20that%20significantly%20affect%20model%20performance%20%E2%80%94%0A%20%20%20%20most%20notably%20%60max_depth%60%20(how%20deep%20each%20decision%20tree%20grows)%2C%20%60n_estimators%60%20(how%20many%0A%20%20%20%20trees%20to%20build)%2C%20and%20%60learning_rate%60%20(how%20aggressively%20the%20model%20corrects%20itself%20with%0A%20%20%20%20each%20new%20tree).%20Finding%20good%20values%20requires%20searching%20across%20combinations%20of%20these%2C%0A%20%20%20%20which%20is%20where%20the%20two%20tuning%20strategies%20diverge.%20**Grid%20search**%20exhaustively%20tests%0A%20%20%20%20every%20combination%20from%20a%20manually%20defined%20grid%20%E2%80%94%20predictable%2C%20but%20the%20number%20of%0A%20%20%20%20combinations%20multiplies%20fast.%20**Bayesian%20search**%20takes%20a%20smarter%20approach%3A%20it%20treats%0A%20%20%20%20the%20hyperparameter%20space%20as%20a%20probability%20landscape%20and%20uses%20the%20results%20of%20prior%20trials%0A%20%20%20%20to%20decide%20*where%20to%20look%20next*%2C%20covering%20far%20more%20ground%20with%20far%20fewer%20evaluations.%20At%0A%20%20%20%20%60n_iter%3D30%60%2C%20Bayesian%20search%20tests%20only%2030%20combinations%20while%20still%20converging%20on%0A%20%20%20%20strong%20hyperparameters%20%E2%80%94%20something%20a%20comparably%20sized%20grid%20search%20could%20not%20guarantee.%0A%0A%20%20%20%20%23%23%23%20Usage%0A%0A%20%20%20%20%60%60%60python%0A%20%20%20%20%23%20OLS%20on%20ride-level%20data%0A%20%20%20%20model_ols%20%3D%20do_train(f%3D'rides'%2C%20t%3D'ols')%0A%0A%20%20%20%20%23%20Bayesian-tuned%20XGBoost%20on%20category-level%20data%2C%20chronological%20split%0A%20%20%20%20model_xgb%20%3D%20do_train(f%3D'categories'%2C%20t%3D'xgb'%2C%20xgb_split_method%3D'chrono'%2C%20xgb_tuning%3D'bayes')%0A%0A%20%20%20%20%23%20Neural%20network%20on%20ride-level%20data%0A%20%20%20%20model_nn%20%20%3D%20do_train(f%3D'rides'%2C%20t%3D'nn')%0A%20%20%20%20%60%60%60%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20BayesSearchCV%2C%0A%20%20%20%20GridSearchCV%2C%0A%20%20%20%20Integer%2C%0A%20%20%20%20MLPRegressor%2C%0A%20%20%20%20Real%2C%0A%20%20%20%20XGBRegressor%2C%0A%20%20%20%20df_raw%2C%0A%20%20%20%20get_features%2C%0A%20%20%20%20lagged_pcs%2C%0A%20%20%20%20mean_absolute_error%2C%0A%20%20%20%20np%2C%0A%20%20%20%20plot_importance%2C%0A%20%20%20%20plot_ols_importance%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20sm%2C%0A%20%20%20%20train_test_split%2C%0A)%3A%0A%20%20%20%20import%20json%0A%20%20%20%20from%20pathlib%20import%20Path%0A%20%20%20%20from%20sklearn.pipeline%20import%20make_pipeline%0A%20%20%20%20from%20sklearn.compose%20import%20TransformedTargetRegressor%0A%20%20%20%20from%20sklearn.preprocessing%20import%20StandardScaler%20as%20_StdScaler%0A%0A%20%20%20%20XGB_PARAM_CACHE%20%3D%20Path(%22xgb_param_cache.json%22)%0A%0A%20%20%20%20def%20_load_param_cache()%3A%0A%20%20%20%20%20%20%20%20if%20XGB_PARAM_CACHE.exists()%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20json.loads(XGB_PARAM_CACHE.read_text())%0A%20%20%20%20%20%20%20%20return%20%7B%7D%0A%0A%20%20%20%20def%20_save_param_cache(cache)%3A%0A%20%20%20%20%20%20%20%20XGB_PARAM_CACHE.write_text(json.dumps(cache%2C%20indent%3D2%2C%20sort_keys%3DTrue))%0A%0A%20%20%20%20def%20_fit_xgb_with_params(params%2C%20X_fit%2C%20y_fit)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Build%2Ffit%20an%20XGBRegressor%20from%20a%20plain%20params%20dict%2C%20falling%20back%20to%0A%20%20%20%20%20%20%20%20CPU%20if%20this%20machine%20has%20no%20usable%20CUDA%20device%20(e.g.%20the%20server).%22%22%22%0A%20%20%20%20%20%20%20%20base%20%3D%20dict(random_state%3D42%2C%20missing%3Dnp.nan)%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20XGBRegressor(device%3D'cuda'%2C%20**base%2C%20**params).fit(X_fit%2C%20y_fit)%0A%20%20%20%20%20%20%20%20except%20Exception%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20XGBRegressor(**base%2C%20**params).fit(X_fit%2C%20y_fit)%0A%0A%20%20%20%20def%20do_nn(y%2C%20X%2C%20split_method%3D'chrono')%3A%0A%20%20%20%20%20%20%20%20%23%201.%20Chronological%20Split%20(Keep%20our%20time-series%20forecasting%20honest!)%0A%20%20%20%20%20%20%20%20if%20split_method%20%3D%3D%20'chrono'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20split_index%20%3D%20int(len(X)%20*%200.8)%0A%20%20%20%20%20%20%20%20%20%20%20%20X_train%2C%20X_test%20%3D%20X.iloc%5B%3Asplit_index%5D%2C%20X.iloc%5Bsplit_index%3A%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20y_train%2C%20y_test%20%3D%20y.iloc%5B%3Asplit_index%5D%2C%20y.iloc%5Bsplit_index%3A%5D%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20X_train%2C%20X_test%2C%20y_train%2C%20y_test%20%3D%20train_test_split(X%2C%20y%2C%20test_size%3D0.2%2C%20random_state%3D42)%0A%0A%20%20%20%20%20%20%20%20%23%202.%20Define%20the%20Neural%20Network%20Architecture.%0A%20%20%20%20%20%20%20%20%23%20%20%20%20Two%20wrappers%20around%20the%20MLP%2C%20both%20fit%20on%20the%20TRAIN%20fold%20only%3A%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20-%20StandardScaler%3A%20the%200%2F1%20ride%20dummies%20(our%20strongest%20signal)%20are%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20%20%20low-variance%20next%20to%20the%20standardized%20continuous%20block%3B%20scaling%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20%20%20lifts%20them%20to%20parity.%20Mainly%20buys%20run-to-run%20STABILITY%20here%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20%20%20(multi-seed%20std%200.03%20vs%200.14%20unscaled).%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20-%20log1p%20target%3A%20queue_time_min%20is%20heavily%20right-skewed%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20%20%20(skew%20~3.4)%2C%20which%20wrecks%20the%20MSE%20landscape.%20log1p%20is%20the%20big%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20%20%20lever%20%E2%80%94%20~28%25%20lower%20MAE%20and%20~10x%20faster%20convergence%20%E2%80%94%20and%20is%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20%20%20principled%20for%20the%20MAE%2Fmedian%20we%20report.%20expm1%20inverts%20it%20so%0A%20%20%20%20%20%20%20%20%23%20%20%20%20%20%20%20%20.predict()%20still%20returns%20minutes%20(bake-off%20cell%20unchanged).%0A%20%20%20%20%20%20%20%20nn_model%20%3D%20TransformedTargetRegressor(%0A%20%20%20%20%20%20%20%20%20%20%20%20regressor%3Dmake_pipeline(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_StdScaler()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20MLPRegressor(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20hidden_layer_sizes%3D(16%2C%208)%2C%20%20%23%20tuned%20via%20multi-seed%20sweep%20(see%20md%20cell%20above%20the%20nn%20call)%3B%20beat%20(64%2C32)%20by%20~7%25%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20activation%3D'relu'%2C%20%20%20%20%20%20%20%20%20%20%20%23%20The%20non-linear%20%22switch%22%20that%20turns%20nodes%20on%2Foff%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20solver%3D'adam'%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20The%20specific%20Gradient%20Descent%20engine%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20learning_rate_init%3D0.001%2C%20%20%20%20%23%20Step%20size%20down%20the%20loss%20landscape%20mountain%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20max_iter%3D500%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Maximum%20trips%20down%20the%20mountain%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20random_state%3D42%2C%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)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20func%3Dnp.log1p%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20inverse_func%3Dnp.expm1%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20print(%22Training%20Neural%20Network...%20%22)%0A%20%20%20%20%20%20%20%20nn_model.fit(X_train%2C%20y_train)%0A%0A%20%20%20%20%20%20%20%20predictions%20%3D%20nn_model.predict(X_test)%0A%20%20%20%20%20%20%20%20print(f%22Neural%20Net%20MAE%3A%20%7Bmean_absolute_error(y_test%2C%20predictions)%3A.2f%7D%20minutes%22)%0A%0A%20%20%20%20%20%20%20%20return%20nn_model%0A%0A%20%20%20%20def%20do_ols(y%2C%20X)%3A%0A%20%20%20%20%20%20%20%20X%20%3D%20sm.add_constant(X)%20%23%20statsmodels%20doesn't%20add%20this%20by%20default%2C%20add%20a%20y%20intercept%0A%20%20%20%20%20%20%20%20model%20%3D%20sm.OLS(y%2C%20X).fit()%0A%20%20%20%20%20%20%20%20print(model.summary())%0A%20%20%20%20%20%20%20%20plot_ols_importance(model)%0A%20%20%20%20%20%20%20%20return%20model%0A%0A%20%20%20%20def%20do_xgb(y%2C%20X%2C%20xgb_split_method%2C%20xgb_tuning%2C%20cache_key%3D'default'%2C%20force_search%3DFalse)%3A%0A%20%20%20%20%20%20%20%20%23%20prep%20train%2Ftest%20splits%0A%0A%20%20%20%20%20%20%20%20if%20xgb_split_method%20%3D%3D%20'chrono'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Calculate%20where%20the%2080%25%20mark%20is%0A%20%20%20%20%20%20%20%20%20%20%20%20split_index%20%3D%20int(len(X)%20*%200.8)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Slicing%20chronologically%0A%20%20%20%20%20%20%20%20%20%20%20%20X_train%20%3D%20X.iloc%5B%3Asplit_index%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20y_train%20%3D%20y.iloc%5B%3Asplit_index%5D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20X_test%20%3D%20X.iloc%5Bsplit_index%3A%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20y_test%20%3D%20y.iloc%5Bsplit_index%3A%5D%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20This%20is%20not%20appropriate%20for%20this%20dataset%2C%20leaving%20in%20option%20to%20run%20this%20way%20fo%20comparison%20sake%0A%20%20%20%20%20%20%20%20%20%20%20%20X_train%2C%20X_test%2C%20y_train%2C%20y_test%20%3D%20train_test_split(X%2C%20y%2C%20test_size%3D0.2%2C%20random_state%3D42)%0A%0A%20%20%20%20%20%20%20%20if%20xgb_tuning%20%3D%3D%20'bayes'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20cache%20%3D%20_load_param_cache()%0A%20%20%20%20%20%20%20%20%20%20%20%20cache_id%20%3D%20f%22%7Bcache_key%7D%7C%7Bxgb_split_method%7D%22%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20force_search%20and%20cache_id%20in%20cache%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Reuse%20the%20winning%20hyperparameters%20from%20a%20previous%20search%20%E2%80%94%20skip%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20the%20~30-iteration%20hunt%20entirely%20and%20just%20refit%20on%20this%20data.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20params%20%3D%20cache%5Bcache_id%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Using%20cached%20hyperparameters%20for%20'%7Bcache_id%7D'%3A%20%7Bparams%7D%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(%22(pass%20force_search%3DTrue%20to%20re-run%20the%20Bayesian%20search)%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20best_xgb%20%3D%20_fit_xgb_with_params(params%2C%20X_train%2C%20y_train)%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Define%20the%20Search%20Space%2C%20BayesianSearch%20accepts%20ranges%20of%20values%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20search_spaces%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'max_depth'%3A%20Integer(3%2C%2012)%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Search%20any%20whole%20number%20between%203%20and%2012%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'n_estimators'%3A%20Integer(100%2C%201000)%2C%20%20%20%20%20%20%20%23%20Search%20any%20whole%20number%20between%20100%20and%201000%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'learning_rate'%3A%20Real(0.01%2C%200.3%2C%20'log-uniform')%20%23%20Search%20decimals%2C%20prioritizing%20smaller%20numbers%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bayes_search%20%3D%20BayesSearchCV(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20estimator%3DXGBRegressor(random_state%3D42%2Cdevice%3D'cuda'%2Cmissing%3Dnp.nan)%2C%20%23%20for%20extra%20horsepower%20on%20my%20desktop%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23estimator%3DXGBRegressor(random_state%3D42)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20search_spaces%3Dsearch_spaces%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20n_iter%3D30%2C%20%20%20%20%20%20%20%20%20%20%20%23%20It%20will%20ONLY%20test%2030%20combinations%20total!%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20cv%3D3%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%203-fold%20cross%20validation%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20scoring%3D'neg_mean_absolute_error'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20random_state%3D42%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20verbose%3D1%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Fit%20the%20model%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20you%20could%20in%20theory%20find%20the%20same%20parameters%20using%20GridSearch%20with%20a%20sufficiently%20large%20gridspace%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20but%20it%20would%20take%20forever.%20We%20have%20the%20power%20of%20Thomas%20Bayes%20and%20vector%20calculus%20on%20our%20side.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(%22Initiating%20Bayesian%20Hunt...%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bayes_search.fit(X_train%2C%20y_train)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Best%20Hyperparameters%20Found%3A%20%7Bbayes_search.best_params_%7D%22)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Persist%20the%20winners%20so%20future%20runs%20can%20skip%20the%20search.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Coerce%20numpy%20scalars%20-%3E%20native%20python%20so%20json%20can%20serialize%20them.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20params%20%3D%20%7Bk%3A%20(v.item()%20if%20hasattr(v%2C%20'item')%20else%20v)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20k%2C%20v%20in%20bayes_search.best_params_.items()%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20cache%5Bcache_id%5D%20%3D%20params%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_save_param_cache(cache)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Cached%20to%20%7BXGB_PARAM_CACHE%7D%22)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20best_xgb%20%3D%20bayes_search.best_estimator_%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20best_predictions%20%3D%20best_xgb.predict(X_test)%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Bayesian%20Tuned%20MAE%3A%20%7Bmean_absolute_error(y_test%2C%20best_predictions)%3A.2f%7D%20minutes%22)%0A%0A%20%20%20%20%20%20%20%20elif%20xgb_tuning%20%3D%3D%20'grid'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Define%20the%20grid%20of%20hyperparams%20to%20test%0A%20%20%20%20%20%20%20%20%20%20%20%20param_grid%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'max_depth'%3A%20%5B3%2C%205%2C%208%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'n_estimators'%3A%20%5B100%2C%20300%2C%20500%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'learning_rate'%3A%20%5B0.05%2C%200.1%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Set%20up%20the%20search%20(cv%3D3%20%3D%203-fold%20cross%20validation)%0A%20%20%20%20%20%20%20%20%20%20%20%20grid_search%20%3D%20GridSearchCV(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20estimator%3DXGBRegressor(random_state%3D42%2Cdevice%3D'cuda'%2Cmissing%3Dnp.nan)%2C%20%23%20for%20extra%20horsepower%20on%20my%20desktop%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23estimator%3DXGBRegressor(random_state%3D42)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20param_grid%3Dparam_grid%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20scoring%3D'neg_mean_absolute_error'%2C%20%23%20the%20lowest%20MAE%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20cv%3D3%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20verbose%3D2%20%23%20print%20progress%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20print(%22Executing%20grid%20search...%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20grid_search.fit(X_train%2C%20y_train)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20View%20the%20winning%20combination%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f%22Best%20Settings%20Found%3A%20%7Bgrid_search.best_params_%7D%22)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Evaluate%20the%20best%20model%0A%20%20%20%20%20%20%20%20%20%20%20%20best_xgb%20%3D%20grid_search.best_estimator_%0A%20%20%20%20%20%20%20%20%20%20%20%20best_predictions%20%3D%20best_xgb.predict(X_test)%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f%22New%20Tuned%20MAE%3A%20%7Bmean_absolute_error(y_test%2C%20best_predictions)%3A.2f%7D%20minutes%22)%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%20the%20top%2015%20most%20important%20features%0A%20%20%20%20%20%20%20%20fig%2C%20ax%20%3D%20plt.subplots(figsize%3D(10%2C%208))%0A%20%20%20%20%20%20%20%20plot_importance(best_xgb%2C%20max_num_features%3D15%2C%20ax%3Dax%2C%20importance_type%3D'weight')%0A%20%20%20%20%20%20%20%20plt.show()%0A%20%20%20%20%20%20%20%20return%20best_xgb%20%20%20%20%0A%0A%20%20%20%20def%20do_train(t%20%3D%20'xgb'%2C%20f%20%3D%20'rides'%2C%20xgb_split_method%3D'chrono'%2C%20xgb_tuning%3D'bayes'%2C%20force_search%3DFalse)%3A%0A%20%20%20%20%20%20%20%20df_train%20%3D%20get_features(df_raw%2C%20lagged_pcs%2C%20f%3Df%2C%20t%3Dt)%0A%20%20%20%20%20%20%20%20%23df_train.info()%0A%0A%20%20%20%20%20%20%20%20y%20%3D%20df_train%5B'queue_time_min'%5D%20%23%20define%20target%20vector%20(y)%2C%20we%20want%20to%20predict%20queue%20times%20in%20minutes%0A%20%20%20%20%20%20%20%20X%20%3D%20df_train.drop(columns%3D%5B'queue_time_min'%2C%20'queue_time_sec'%5D)%20%23%20X%20is%20the%20feature%20matrix%2C%20make%20sure%20to%20drop%20all%20forms%20of%20the%20target%20columns%0A%0A%20%20%20%20%20%20%20%20if%20t%20%3D%3D%20'ols'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20do_ols(y%2C%20X)%0A%20%20%20%20%20%20%20%20elif%20t%20%3D%3D%20'xgb'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20do_xgb(y%2C%20X%2C%20xgb_split_method%2C%20xgb_tuning%2C%20cache_key%3Df%2C%20force_search%3Dforce_search)%0A%20%20%20%20%20%20%20%20elif%20t%20%3D%3D%20'nn'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20do_nn(y%2C%20X)%0A%0A%20%20%20%20return%20(do_train%2C)%0A%0A%0A%40app.cell%0Adef%20_(do_train)%3A%0A%20%20%20%20do_train(f%3D'rides'%2C%20t%3D'ols')%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(do_train)%3A%0A%20%20%20%20do_train(f%3D'categories'%2C%20t%3D'ols')%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(do_train)%3A%0A%20%20%20%20rides_xgb%20%3D%20do_train(f%3D'rides'%2C%20t%3D'xgb')%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(do_train)%3A%0A%20%20%20%20cats_xgb%20%3D%20do_train(f%3D'categories'%2C%20t%3D'xgb')%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%20Tuning%20%60do_nn%60%20%E2%80%94%20empirical%2C%20not%20hand-waved%0A%0A%20%20%20%20Every%20choice%20in%20the%20network%20below%20was%20*measured*%2C%20not%20guessed%20%E2%80%94%20all%20on%20the%0A%20%20%20%20**same%20chronological%2080%2F20%20split**%2C%20averaged%20over%20**4%E2%80%935%20random%20seeds**.%0A%20%20%20%20(Seeds%20matter%3A%20there's%20~0.1-min%20run-to-run%20jitter%20even%20at%20a%20*fixed*%20seed%2C%0A%20%20%20%20from%20nondeterministic%20threaded%20BLAS%20reductions%2C%20so%20single-run%20comparisons%0A%20%20%20%20are%20unreliable.)%0A%0A%20%20%20%20**1.%20Two%20transforms%20do%20the%20heavy%20lifting**%20(holdout%20MAE%2C%20minutes)%3A%0A%0A%20%20%20%20%7C%20input%20X%20%7C%20target%20y%20%7C%20mean%20MAE%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20raw%20(as-is)%20%7C%20raw%20%7C%203.04%20%7C%0A%20%20%20%20%7C%20StandardScaler%20%7C%20raw%20%7C%202.54%20%7C%0A%20%20%20%20%7C%20raw%20%7C%20**log1p**%20%7C%202.42%20%7C%0A%20%20%20%20%7C%20**StandardScaler**%20%7C%20**log1p**%20%7C%20**2.17**%20%7C%0A%0A%20%20%20%20-%20**%60log1p%60%20target%20is%20the%20big%20lever.**%20%60queue_time_min%60%20is%20heavily%0A%20%20%20%20%20%20right-skewed%20(skew%20%E2%89%88%203.4)%2C%20which%20wrecks%20the%20MSE%20landscape%3B%20%60log1p%60%20cuts%0A%20%20%20%20%20%20MAE%20~28%25%20and%20converges%20~10%C3%97%20faster.%20It's%20principled%20for%20the%20**MAE%2Fmedian**%0A%20%20%20%20%20%20we%20report%20(the%20median%20passes%20cleanly%20through%20a%20monotonic%20transform)%2C%20and%0A%20%20%20%20%20%20%60expm1%60%20inverts%20it%20so%20%60.predict()%60%20still%20returns%20minutes.%0A%20%20%20%20-%20**%60StandardScaler%60%20mostly%20buys%20stability**%20(seed%20std%200.03%20vs%200.14)%3A%20the%0A%20%20%20%20%20%200%2F1%20ride%20dummies%20are%20our%20strongest%20signal%20but%20low-variance%20next%20to%20the%0A%20%20%20%20%20%20already-standardized%20continuous%20block%2C%20so%20scaling%20lifts%20them%20to%20parity.%0A%0A%20%20%20%20**2.%20Smaller%20network%20is%20better%20%E2%80%94%20capacity%20was%20never%20the%20constraint.**%20With%0A%20%20%20%20~600k%20training%20rows%20even%20a%2020k-param%20net%20can't%20overfit%20(~30%20rows%2Fparam)%2C%20so%0A%20%20%20%20extra%20neurons%20just%20give%20Adam%20more%20ways%20to%20settle%20into%20a%20worse%20minimum.%0A%20%20%20%20Sweeping%20%60hidden_layer_sizes%60%3A%0A%0A%20%20%20%20%7C%20arch%20%7C%20params%20%7C%20mean%20MAE%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20(128%2C%2064)%20%7C%2019%2C841%20%7C%202.40%20%7C%0A%20%20%20%20%7C%20(64%2C%2064%2C%2032)%20%7C%2012%2C033%20%7C%202.15%20%7C%0A%20%20%20%20%7C%20(64%2C%2032)%20%E2%86%90%20original%20guess%20%7C%207%2C873%20%7C%202.12%20%7C%0A%20%20%20%20%7C%20(32%2C%2016)%20%7C%203%2C425%20%7C%202.00%20%7C%0A%20%20%20%20%7C%20**(16%2C%208)**%20%7C%20**1%2C585**%20%7C%20**1.96**%20%7C%0A%20%20%20%20%7C%20(8%2C%204)%20%7C%20761%20%7C%201.98%20%7C%0A%0A%20%20%20%20MAE%20falls%20steadily%20as%20the%20net%20shrinks%2C%20then%20**bottoms%20around%20%60(16%2C%208)%60**%0A%20%20%20%20(going%20smaller%20stops%20helping%20and%20gets%20noisier).%20%60(16%2C%208)%60%20has%20the%20best%20mean%0A%20%20%20%20*and*%20the%20tightest%20seed%20spread%20*and*%20trains%20~3%C3%97%20faster%20than%20the%20original%0A%20%20%20%20%60(64%2C%2032)%60%20%E2%80%94%20**~7%25%20lower%20MAE%20for%20~5%C3%97%20fewer%20parameters**.%20That's%20what%20%60do_nn%60%0A%20%20%20%20uses%20below.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(do_train)%3A%0A%20%20%20%20rides_nn%20%3D%20do_train(t%3D'nn'%2C%20f%3D'rides')%0A%20%20%20%20return%20(rides_nn%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%20Visualizing%20the%20Trained%20Neural%20Network%0A%0A%20%20%20%20A%20look%20*inside*%20the%20%60MLPRegressor%60%20from%20the%20cell%20above.%20Two%20panels%3A%0A%0A%20%20%20%20-%20**Architecture%20graph**%20%E2%80%94%20the%20actual%20learned%20network%3A%20input%20layer%20%E2%86%92%0A%20%20%20%20%20%20Hidden%201%20(16)%20%E2%86%92%20Hidden%202%20(8)%20%E2%86%92%20output.%20Each%20edge%20is%20a%20weight%20from%0A%20%20%20%20%20%20%60model.coefs_%60%3B%20**red%20%3D%20positive%2C%20blue%20%3D%20negative**%2C%20and%20**opacity%20scales%0A%20%20%20%20%20%20with%20%7Cweight%7C**%20(per-layer%20normalized)%2C%20so%20the%20connections%20the%20network%0A%20%20%20%20%20%20actually%20leans%20on%20stand%20out%20from%20the%20noise.%20The%20input%20layer%20and%20hidden%0A%20%20%20%20%20%20layers%20are%20large%2C%20so%20each%20column%20is%20capped%20at%20a%20handful%20of%20nodes%20for%0A%20%20%20%20%20%20legibility%20(a%20%60%E2%8B%AE%60%20marks%20truncation)%3B%20the%20parenthetical%20is%20the%20*true*%20layer%0A%20%20%20%20%20%20width.%0A%20%20%20%20-%20**Training%20loss%20curve**%20%E2%80%94%20%60model.loss_curve_%60%2C%20the%20optimizer's%20loss%20(%C2%BD%C2%B7MSE)%0A%20%20%20%20%20%20at%20each%20Adam%20iteration%2C%20showing%20how%20the%20fit%20converged.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(np%2C%20plt%2C%20rides_nn)%3A%0A%20%20%20%20def%20draw_neural_net(model%2C%20max_show%3D14)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Render%20an%20MLPRegressor%20as%20a%20node%2Fedge%20architecture%20diagram%20%2B%20its%0A%20%20%20%20%20%20%20%20training%20loss%20curve.%20Big%20layers%20are%20capped%20at%20%60max_show%60%20nodes%20(with%20a%0A%20%20%20%20%20%20%20%20%E2%8B%AE%20truncation%20marker)%3B%20the%20real%20width%20is%20printed%20under%20each%20column.%22%22%22%0A%20%20%20%20%20%20%20%20%23%20Unwrap%20to%20the%20underlying%20MLPRegressor%3A%20do_nn%20wraps%20it%20in%20a%0A%20%20%20%20%20%20%20%20%23%20TransformedTargetRegressor(make_pipeline(StandardScaler%2C%20MLP))%2C%20so%20the%0A%20%20%20%20%20%20%20%20%23%20coefs_%2Floss_curve_%20live%20a%20couple%20layers%20down.%20Works%20on%20a%20bare%20MLP%20too.%0A%20%20%20%20%20%20%20%20if%20hasattr(model%2C%20%22regressor_%22)%3A%20%20%20%23%20TransformedTargetRegressor%20-%3E%20fitted%20inner%0A%20%20%20%20%20%20%20%20%20%20%20%20model%20%3D%20model.regressor_%0A%20%20%20%20%20%20%20%20if%20hasattr(model%2C%20%22steps%22)%3A%20%20%20%20%20%20%20%20%23%20Pipeline%20-%3E%20final%20estimator%0A%20%20%20%20%20%20%20%20%20%20%20%20model%20%3D%20model.steps%5B-1%5D%5B1%5D%0A%20%20%20%20%20%20%20%20%23%20Full%20layer%20widths%3A%20%5Bn_inputs%2C%20hidden_1%2C%20...%2C%20hidden_k%2C%20n_outputs%5D%0A%20%20%20%20%20%20%20%20full_sizes%20%3D%20%5Bmodel.coefs_%5B0%5D.shape%5B0%5D%5D%20%2B%20%5Bc.shape%5B1%5D%20for%20c%20in%20model.coefs_%5D%0A%20%20%20%20%20%20%20%20n_layers%20%3D%20len(full_sizes)%0A%20%20%20%20%20%20%20%20show_sizes%20%3D%20%5Bmin(s%2C%20max_show)%20for%20s%20in%20full_sizes%5D%0A%0A%20%20%20%20%20%20%20%20fig%2C%20(ax%2C%20ax2)%20%3D%20plt.subplots(%0A%20%20%20%20%20%20%20%20%20%20%20%201%2C%202%2C%20figsize%3D(16%2C%209)%2C%20gridspec_kw%3D%7B%22width_ratios%22%3A%20%5B3%2C%202%5D%7D%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%23%20---%20left%20panel%3A%20the%20network%20graph%20---%0A%20%20%20%20%20%20%20%20x_pos%20%3D%20np.linspace(0.05%2C%200.95%2C%20n_layers)%0A%20%20%20%20%20%20%20%20%23%20vertically%20centered%20node%20y-coords%20per%20layer%0A%20%20%20%20%20%20%20%20node_y%20%3D%20%5Bnp.linspace(0%2C%201%2C%20s)%20if%20s%20%3E%201%20else%20np.array(%5B0.5%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20s%20in%20show_sizes%5D%0A%0A%20%20%20%20%20%20%20%20%23%20edges%2C%20colored%20by%20weight%20sign%2C%20opacity%20by%20%7Cweight%7C%20(normalized%20per%20layer)%0A%20%20%20%20%20%20%20%20for%20li%2C%20W%20in%20enumerate(model.coefs_)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20sub%20%3D%20W%5B%3Ashow_sizes%5Bli%5D%2C%20%3Ashow_sizes%5Bli%20%2B%201%5D%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20amax%20%3D%20np.abs(W).max()%20or%201.0%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20i%20in%20range(show_sizes%5Bli%5D)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20j%20in%20range(show_sizes%5Bli%20%2B%201%5D)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20w%20%3D%20sub%5Bi%2C%20j%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ax.plot(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5Bx_pos%5Bli%5D%2C%20x_pos%5Bli%20%2B%201%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5Bnode_y%5Bli%5D%5Bi%5D%2C%20node_y%5Bli%20%2B%201%5D%5Bj%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3D%22%23C0392B%22%20if%20w%20%3E%3D%200%20else%20%22%232471A3%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alpha%3Dmin(0.9%2C%20abs(w)%20%2F%20amax%20*%200.85%20%2B%200.02)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20linewidth%3D0.6%2C%20zorder%3D1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20labels%20%3D%20(%5B%22Input%22%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2B%20%5Bf%22Hidden%20%7Bk%20%2B%201%7D%22%20for%20k%20in%20range(n_layers%20-%202)%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2B%20%5B%22Output%22%5D)%0A%20%20%20%20%20%20%20%20for%20x%2C%20ys%2C%20full_s%2C%20show_s%2C%20lbl%20in%20zip(%0A%20%20%20%20%20%20%20%20%20%20%20%20x_pos%2C%20node_y%2C%20full_sizes%2C%20show_sizes%2C%20labels%0A%20%20%20%20%20%20%20%20)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ax.scatter(%5Bx%5D%20*%20len(ys)%2C%20ys%2C%20s%3D190%2C%20color%3D%22white%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20edgecolors%3D%22%232C3E50%22%2C%20linewidths%3D1.6%2C%20zorder%3D2)%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20full_s%20%3E%20show_s%3A%20%20%23%20mark%20that%20we%20truncated%20this%20column%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ax.text(x%2C%20-0.05%2C%20%22%E2%8B%AE%22%2C%20ha%3D%22center%22%2C%20va%3D%22top%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D20%2C%20color%3D%22%232C3E50%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20ax.text(x%2C%201.08%2C%20f%22%7Blbl%7D%5Cn(%7Bfull_s%7D)%22%2C%20ha%3D%22center%22%2C%20va%3D%22bottom%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D11%2C%20fontweight%3D%22bold%22)%0A%0A%20%20%20%20%20%20%20%20ax.set_xlim(0%2C%201)%0A%20%20%20%20%20%20%20%20ax.set_ylim(-0.14%2C%201.24)%0A%20%20%20%20%20%20%20%20ax.axis(%22off%22)%0A%20%20%20%20%20%20%20%20ax.set_title(%22MLPRegressor%20architecture%5Cnedge%20color%20%3D%20weight%20sign%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22(red%20%2B%2C%20blue%20%E2%88%92)%2C%20opacity%20%3D%20%7Cweight%7C%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D12%2C%20pad%3D16)%0A%0A%20%20%20%20%20%20%20%20%23%20---%20right%20panel%3A%20training%20loss%20curve%20---%0A%20%20%20%20%20%20%20%20ax2.plot(model.loss_curve_%2C%20color%3D%22%23117A65%22%2C%20linewidth%3D2)%0A%20%20%20%20%20%20%20%20ax2.set_title(f%22Training%20loss%20%E2%80%94%20%7Blen(model.loss_curve_)%7D%20iters%2C%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22final%20%7Bmodel.loss_curve_%5B-1%5D%3A.3f%7D%22%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20ax2.set_xlabel(%22Adam%20iteration%22)%0A%20%20%20%20%20%20%20%20ax2.set_ylabel(%22Loss%20(%C2%BD%20%C2%B7%20MSE)%22)%0A%20%20%20%20%20%20%20%20ax2.grid(alpha%3D0.3)%0A%20%20%20%20%20%20%20%20ax2.spines%5B%22top%22%5D.set_visible(False)%0A%20%20%20%20%20%20%20%20ax2.spines%5B%22right%22%5D.set_visible(False)%0A%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20return%20fig%0A%0A%20%20%20%20draw_neural_net(rides_nn)%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%20What%20didn't%20work%3A%20autoencoder-compressed%20features%0A%0A%20%20%20%20One%20honest%20negative%20result%20before%20the%20capstone.%20I%20tried%20using%20a%20neural%20net%0A%20%20%20%20not%20as%20the%20predictor%20but%20as%20an%20**unsupervised%20feature%20compressor**%20%E2%80%94%20a%20deep%0A%20%20%20%20%60tanh%60%20**autoencoder**%20squeezing%20all%20~89%20features%20through%20a%20narrow%20bottleneck%0A%20%20%20%20(%22PCA%2C%20but%20it%20can%20bend%22)%2C%20then%20handing%20only%20those%20latent%20codes%20to%20XGBoost.%20The%0A%20%20%20%20bet%3A%20maybe%20the%20nonlinear%20codes%20surface%20an%20interaction%20the%20trees%20couldn't%20already%0A%20%20%20%20find.%0A%0A%20%20%20%20They%20didn't.%20On%20the%20same%20chronological%20split%2C%20**XGB%20on%20full%20%2B%20autoencoder%20codes%0A%20%20%20%20(2.03%20min)%20was%20no%20better%20than%20XGB%20on%20the%20raw%20matrix%20(2.01)**%20%E2%80%94%20the%20learned%20codes%0A%20%20%20%20added%20nothing.%20(And%20once%20both%20models%20get%20the%20%60log1p%60%20target%2C%20NN%20and%20XGB%0A%20%20%20%20effectively%20*tie*%20at%20~1.97%2C%20so%20there%20was%20no%20NN-vs-XGB%20gap%20for%20the%20autoencoder%20to%0A%20%20%20%20bridge%20in%20the%20first%20place.)%20So%20the%20autoencoder%20is%20retired%3B%20the%20real%20capstone%20is%0A%20%20%20%20below.%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%20Capstone%20%E2%80%94%20did%20the%20lagged%20park-state%20PCs%20%22take%20over%2C%22%20and%20do%20they%20mean%20what%20we%20think%3F%0A%0A%20%20%20%20Adding%20the%20three%20lagged%20park-state%20PCs%20visibly%20*dominated*%20the%20models.%20This%0A%20%20%20%20section%20turns%20that%20observation%20into%20a%20**test%20of%20the%20interpretations**%20we%20gave%0A%20%20%20%20PC1%2FPC2%2FPC3%2C%20via%20a%20simple%20**mediation%20A%2FB**%3A%20fit%20the%20rides%20OLS%20**without**%20the%0A%20%20%20%20lagged%20PCs%2C%20then%20**with**%20them%20(same%20rows%2C%20same%20other%20features)%2C%20and%20watch%20what%0A%20%20%20%20moves.%0A%0A%20%20%20%20The%20PCs%20are%20*lagged%20park%20state*%20%E2%80%94%20they%20sit%20causally%20**downstream**%20of%0A%20%20%20%20hour%20%2F%20day%20%2F%20weather%20(those%20conditions%20drive%20the%20crowd%2030%20min%20later).%20So%20if%0A%20%20%20%20they're%20a%20good%20summary%20of%20%22how%20busy%20is%20it%2C%22%20adding%20them%20should%20**absorb**%20the%0A%20%20%20%20coefficients%20of%20whatever%20was%20merely%20*proxying*%20for%20crowd%20state%20(calendar%2C%0A%20%20%20%20weather)%2C%20while%20leaving%20the%20**ride-identity**%20coefficients%20alone%20%E2%80%94%20each%20PC%20is%20one%0A%20%20%20%20number%20per%20snapshot%2C%20identical%20for%20every%20ride%20in%20a%20bin%2C%20so%20it's%20orthogonal%20to%0A%20%20%20%20*which*%20ride%20you%20ask%20about.%0A%0A%20%20%20%20%7C%20PC%20%7C%20interpretation%20%7C%20should%20absorb%E2%80%A6%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20**PC1**%20%7C%20overall%20park%20busyness%20%7C%20weekends%2C%20peak%20months%2C%20broad%20time%20signal%20%7C%0A%20%20%20%20%7C%20**PC2**%20%7C%20kiddieland%20%E2%86%94%20main%20midway%20(spatial)%20%7C%20little%20here%20%E2%80%94%20its%20meaning%20lives%20in%20ride-space%20%7C%0A%20%20%20%20%7C%20**PC3**%20%7C%20evening%20%E2%86%94%20daytime%20daypart%20%7C%20%60hour%60%2C%20evening%20promos%20%7C%0A%0A%20%20%20%20**Two%20panels%20below%3A**%20(A)%20which%20coefficients%20the%20PCs%20absorbed%2C%20and%20(B)%20what%20each%0A%20%20%20%20PC%20actually%20correlates%20with%20%E2%80%94%20i.e.%2C%20does%20the%20displacement%20match%20the%20story%3F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(df_raw%2C%20get_features%2C%20lagged_pcs%2C%20mo%2C%20np%2C%20pd%2C%20plt%2C%20sm)%3A%0A%20%20%20%20%23%20Mediation%20A%2FB%20on%20the%20rides%20'ols'%20matrix%3A%20fit%20OLS%20WITHOUT%20the%20lagged%20PCs%2C%0A%20%20%20%20%23%20then%20WITH%20them%20(same%20rows%2Ffeatures%20otherwise)%2C%20and%20read%20what%20the%20PCs%0A%20%20%20%20%23%20displaced.%20OLS%20twice%20on%20~750k%20rows%20is%20a%20couple%20of%20seconds.%0A%20%20%20%20_df%20%3D%20get_features(df_raw%2C%20lagged_pcs%2C%20f%3D%22rides%22%2C%20t%3D%22ols%22)%0A%20%20%20%20_y%20%3D%20_df%5B%22queue_time_min%22%5D%0A%20%20%20%20_X_full%20%3D%20_df.drop(columns%3D%5B%22queue_time_min%22%2C%20%22queue_time_sec%22%5D)%0A%20%20%20%20_pc%20%3D%20%5Bc%20for%20c%20in%20_X_full.columns%20if%20c.startswith(%22PC%22)%5D%0A%0A%20%20%20%20_before%20%3D%20sm.OLS(_y%2C%20sm.add_constant(_X_full.drop(columns%3D_pc))).fit()%0A%20%20%20%20_after%20%3D%20sm.OLS(_y%2C%20sm.add_constant(_X_full)).fit()%0A%0A%20%20%20%20%23%20(A)%20coefficient%20displacement%20on%20the%20shared%20(non-PC)%20features%0A%20%20%20%20_delta%20%3D%20_after.params.reindex(_before.params.index)%20-%20_before.params%0A%20%20%20%20_disp%20%3D%20pd.DataFrame(%7B%22without_PCs%22%3A%20_before.params%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22with_PCs%22%3A%20_after.params.reindex(_before.params.index)%7D)%0A%20%20%20%20_disp%5B%22shrink_to_0%22%5D%20%3D%20_disp%5B%22without_PCs%22%5D.abs()%20-%20_disp%5B%22with_PCs%22%5D.abs()%0A%20%20%20%20_ride%20%3D%20_disp.index.str.startswith(%22ride_name_%22)%0A%20%20%20%20_absorbed%20%3D%20(_disp%5B~_ride%5D.sort_values(%22shrink_to_0%22%2C%20ascending%3DFalse)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.head(12)%5B%5B%22without_PCs%22%2C%20%22with_PCs%22%5D%5D.round(3))%0A%20%20%20%20_ride_drift%2C%20_non_drift%20%3D%20_delta%5B_ride%5D.abs().mean()%2C%20_delta%5B~_ride%5D.abs().mean()%0A%0A%20%20%20%20%23%20(B)%20what%20each%20PC%20correlates%20with%20among%20interpretable%20(non-ride)%20features%0A%20%20%20%20_interp%20%3D%20%5Bc%20for%20c%20in%20_X_full.columns%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20c.startswith(%22ride_name_%22)%20and%20c%20not%20in%20_pc%5D%0A%20%20%20%20_corr%20%3D%20_X_full%5B_pc%20%2B%20_interp%5D.corr().loc%5B_interp%2C%20_pc%5D%0A%20%20%20%20_corr%20%3D%20_corr.reindex(_corr%5B_pc%5B0%5D%5D.abs().sort_values(ascending%3DFalse).index).round(3)%0A%0A%20%20%20%20%23%20viz%3A%20before%20-%3E%20after%20%22collapse%22%20for%20the%20most-absorbed%20features%0A%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(9%2C%206))%0A%20%20%20%20_o%20%3D%20_absorbed.iloc%5B%3A%3A-1%5D%0A%20%20%20%20_yp%20%3D%20np.arange(len(_o))%0A%20%20%20%20_ax.hlines(_yp%2C%20_o%5B%22without_PCs%22%5D%2C%20_o%5B%22with_PCs%22%5D%2C%20color%3D%22%23B0B0B0%22%2C%20lw%3D2%2C%20zorder%3D1)%0A%20%20%20%20_ax.scatter(_o%5B%22without_PCs%22%5D%2C%20_yp%2C%20color%3D%22%23C0392B%22%2C%20s%3D55%2C%20zorder%3D2%2C%20label%3D%22without%20PCs%22)%0A%20%20%20%20_ax.scatter(_o%5B%22with_PCs%22%5D%2C%20_yp%2C%20color%3D%22%232471A3%22%2C%20s%3D55%2C%20zorder%3D2%2C%20label%3D%22with%20lagged%20PCs%22)%0A%20%20%20%20_ax.axvline(0%2C%20color%3D%22black%22%2C%20lw%3D0.8%2C%20ls%3D%22--%22%2C%20alpha%3D0.6)%0A%20%20%20%20_ax.set_yticks(_yp)%0A%20%20%20%20_ax.set_yticklabels(_o.index)%0A%20%20%20%20_ax.set_xlabel(%22OLS%20coefficient%20(minutes)%22)%0A%20%20%20%20_ax.set_title(%22Lagged%20PCs%20collapse%20the%20calendar%2Fweather%20coefficients%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22toward%20zero%20%E2%80%94%20they%20absorb%20them%22)%0A%20%20%20%20_ax.legend(loc%3D%22lower%20right%22)%0A%20%20%20%20_ax.grid(axis%3D%22x%22%2C%20alpha%3D0.3)%0A%20%20%20%20plt.tight_layout()%0A%0A%20%20%20%20mo.vstack(%5B%0A%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%23%23%23%20Fit%20quality%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22R%C2%B2%20**%7B_before.rsquared%3A.3f%7D%20%E2%86%92%20%7B_after.rsquared%3A.3f%7D**%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22(%2B%7B_after.rsquared%20-%20_before.rsquared%3A.3f%7D)%20from%20just%203%20columns%20%E2%80%94%20the%20PCs%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22really%20do%20take%20over.%20After-model%20PC%20coefficients%3A%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22**PC1%20%7B_after.params%5B_pc%5B0%5D%5D%3A%2B.2f%7D**%20(t%3D%7B_after.tvalues%5B_pc%5B0%5D%5D%3A.0f%7D)%2C%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22PC2%20%7B_after.params%5B_pc%5B1%5D%5D%3A%2B.2f%7D%2C%20PC3%20%7B_after.params%5B_pc%5B2%5D%5D%3A%2B.2f%7D.%22%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%23%23%23%20(A)%20What%20the%20PCs%20absorbed%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22Ride-identity%20coefficients%20barely%20move%20(mean%20%7C%CE%94%7C%20**%7B_ride_drift%3A.3f%7D**)%2C%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22everything%20else%20shifts%20~13%C3%97%20more%20(**%7B_non_drift%3A.3f%7D**)%3A%20the%20PCs%20soak%20up%20the%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22*calendar%2Fweather*%20proxies%20for%20crowd%20state%2C%20not%20the%20*ride*%20signal.%20(Month%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22still%20matters%20%E2%80%94%20its%20effect%20now%20flows%20*through*%20measured%20park%20state.)%22%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20_fig%2C%0A%20%20%20%20%20%20%20%20_absorbed%2C%0A%20%20%20%20%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%23%23%23%20(B)%20What%20each%20PC%20proxies%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Correlations%20of%20each%20lagged%20PC%20with%20the%20interpretable%20features%20(read%20the%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22**rankings**%2C%20not%20magnitudes%20%E2%80%94%20a%20binary%20dummy%20can't%20correlate%20hard%20with%20a%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22continuous%20axis).%20**PC1%20%3D%20busyness**%20holds%20up%20(Saturdays%2C%20October%3B%20busiest%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22when%20*cooler*%20%E2%86%92%20fall%20weekends%2C%20not%20hot%20summer).%20**PC2**%20is%20weak%20here%20because%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22it's%20a%20*spatial*%20contrast%20living%20in%20ride-space.%20**PC3**%20tracks%20weather%20more%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22than%20%60hour%60%20%E2%80%94%20see%20the%20ride-description%20deep-dive%20below%20for%20its%20real%20meaning.%22%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20_corr%2C%0A%20%20%20%20%5D)%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%23%20PC3%20%E2%80%94%20an%20open%20question%20(map%20it%20yourself)%0A%0A%20%20%20%20I%20threw%20a%20lot%20at%20PC3%3A%20ride%20descriptions%2C%20GPS%2C%20water-vs-dry%2C%20ride%20height%2C%0A%20%20%20%20handstamp%2Fbargain%20inclusion%2C%20install%20year%2C%20duration%2C%20intensity%2C%20speed%0A%20%20%20%20(scraped%20from%20Knoebels'%20own%20WP%20REST%20API).%20The%20cleanest%20read%20is%20**fast%0A%20%20%20%20spinning%2Fwhirling%20flats%20%E2%86%94%20slow%20transport%20%26%20water%20rides**%20(best%20numeric%20proxy%3A%0A%20%20%20%20ride%20*intensity*%2C%20r%20%E2%89%88%200.35)%20%E2%80%94%20and%20we%20explicitly%20**ruled%20out**%20daypart%2C%20age%2C%0A%20%20%20%20water%2C%20and%20pure%20geography.%20But%20at%20~7%25%20of%20variance%20it%20stays%20a%20weak%2C%0A%20%20%20%20only-loosely-interpretable%20axis.%0A%0A%20%20%20%20The%20map%20below%20places%20each%20ride%20at%20its%20real%20lat%2Flong%2C%20colored%20by%20PC3%20loading.%0A%20%20%20%20PC2%20was%20a%20clean%20north%E2%80%93south%20split%20(kiddieland)%3B%20PC3's%20reds%20(the%20spinning%0A%20%20%20%20flats)%20cluster%20loosely%20in%20the%20central%20midway%20while%20the%20blues%20ring%20the%0A%20%20%20%20periphery%20%E2%80%94%20but%20there's%20no%20crisp%20rule.%20**If%20you%20know%20Knoebels%20and%20spot%20the%0A%20%20%20%20pattern%20I%20missed%2C%20I'd%20genuinely%20love%20to%20hear%20it.**%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(df_raw%2C%20pd%2C%20pg%2C%20plt%2C%20text)%3A%0A%20%20%20%20from%20sklearn.preprocessing%20import%20StandardScaler%20as%20_SS%0A%20%20%20%20from%20sklearn.decomposition%20import%20PCA%20as%20_PCA%0A%0A%20%20%20%20%23%20recompute%20the%20park-state%20PCA%20but%20keep%20the%20loadings%20(which%20rides%20co-move)%0A%20%20%20%20_wide%20%3D%20(df_raw.assign(_b%3Ddf_raw%5B%22time_stamp%22%5D.dt.floor(%225min%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_q%3Ddf_raw%5B%22queue_time_sec%22%5D%20%2F%2060.0)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20.pivot_table(index%3D%22_b%22%2C%20columns%3D%22ride_name%22%2C%20values%3D%22_q%22%2C%20aggfunc%3D%22mean%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20.sort_index().ffill().fillna(0))%0A%20%20%20%20_wide%20%3D%20_wide.drop(columns%3D%5Bc%20for%20c%20in%20_wide.columns%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20any(s%20in%20str(c).lower()%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20s%20in%20(%22powersurge%22%2C%20%22power%20surge%22))%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20errors%3D%22ignore%22)%0A%20%20%20%20_pca%20%3D%20_PCA(n_components%3D3).fit(_SS().fit_transform(_wide))%0A%20%20%20%20_load%20%3D%20pd.DataFrame(_pca.components_.T%2C%20index%3D_wide.columns%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20columns%3D%5B%22PC1%22%2C%20%22PC2%22%2C%20%22PC3%22%5D).reset_index(names%3D%22name%22)%0A%0A%20%20%20%20%23%20ride%20GPS%20from%20the%20attractions%20feed%3B%20join%20on%20a%20normalized%20name%20key%0A%20%20%20%20_gps%20%3D%20pd.read_sql(text(%22SELECT%20name%2C%20lattitude%20AS%20lat%2C%20longitude%20AS%20lon%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22FROM%20knoebels.dm_latest_ride_info%22)%2C%20pg(%22gp0%22))%0A%20%20%20%20_norm%20%3D%20lambda%20s%3A%20%22%22.join(ch%20for%20ch%20in%20str(s).lower()%20if%20ch.isalnum())%0A%20%20%20%20_load%5B%22k%22%5D%20%3D%20_load%5B%22name%22%5D.map(_norm)%0A%20%20%20%20_gps%5B%22k%22%5D%20%3D%20_gps%5B%22name%22%5D.map(_norm)%0A%20%20%20%20_m%20%3D%20_load.merge(_gps.drop(columns%3D%22name%22)%2C%20on%3D%22k%22).dropna(subset%3D%5B%22lat%22%2C%20%22lon%22%5D)%0A%0A%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(11%2C%209))%0A%20%20%20%20_lim%20%3D%20_m%5B%22PC3%22%5D.abs().max()%0A%20%20%20%20_sc%20%3D%20_ax.scatter(_m%5B%22lon%22%5D%2C%20_m%5B%22lat%22%5D%2C%20c%3D_m%5B%22PC3%22%5D%2C%20cmap%3D%22RdBu_r%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20vmin%3D-_lim%2C%20vmax%3D_lim%2C%20s%3D60%20%2B%20700%20*%20_m%5B%22PC3%22%5D.abs()%20%2F%20_lim%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20edgecolor%3D%22k%22%2C%20linewidth%3D0.5)%0A%20%20%20%20for%20_%2C%20_r%20in%20_m.iterrows()%3A%0A%20%20%20%20%20%20%20%20_ax.annotate(_r%5B%22name%22%5D%2C%20(_r%5B%22lon%22%5D%2C%20_r%5B%22lat%22%5D)%2C%20fontsize%3D7%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20xytext%3D(3%2C%203)%2C%20textcoords%3D%22offset%20points%22)%0A%20%20%20%20_ax.set_title(%22PC3%20loading%20by%20ride%20location%20%20(red%20%3D%20%2B%2C%20blue%20%3D%20%E2%88%92%2C%20size%20%3D%20%7Cloading%7C)%22)%0A%20%20%20%20_ax.set_xlabel(%22longitude%20(W%20%E2%86%92)%22)%0A%20%20%20%20_ax.set_ylabel(%22latitude%20(N%20%E2%86%91)%22)%0A%20%20%20%20_fig.colorbar(_sc%2C%20ax%3D_ax%2C%20label%3D%22PC3%20loading%22)%0A%20%20%20%20_ax.grid(alpha%3D0.25)%0A%20%20%20%20plt.tight_layout()%0A%20%20%20%20_fig%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%20Bonus%20%E2%80%94%20scrying%20the%20neural%20net%0A%0A%20%20%20%20The%20NN%20and%20a%20tuned%20XGBoost%20effectively%20**tie**%20(~1.97%20MAE)%20%E2%80%94%20but%20only%20one%20of%0A%20%20%20%20them%20lets%20you%20open%20it%20up%20and%20read%20what%20it%20learned.%20Each%20of%20the%2016%20first-layer%0A%20%20%20%20neurons%20is%20a%20*learned%20weighted%20blend%20of%20the%2089%20inputs*%20%E2%80%94%20a%20loading%20vector%2C%0A%20%20%20%20exactly%20like%20a%20PCA%20component.%20So%20we%20can%20read%20them%20the%20same%20way%20we%20read%20PC1%2F2%2F3%3A%0A%20%20%20%20inspect%20the%20weights%2C%20correlate%20each%20neuron's%20activations%20with%20interpretable%0A%20%20%20%20features%2C%20and%20**ablate**%20each%20one%20(switch%20it%20off%2C%20measure%20the%20MAE%20hit)%20to%20see%0A%20%20%20%20which%20actually%20matter.%20The%20question%20that%20ties%20the%20whole%20notebook%20together%3A%0A%20%20%20%20**did%20the%20network%20independently%20rediscover%20the%20park-busyness%20signal%3F**%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(%0A%20%20%20%20df_raw%2C%0A%20%20%20%20get_features%2C%0A%20%20%20%20lagged_pcs%2C%0A%20%20%20%20mean_absolute_error%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20np%2C%0A%20%20%20%20pd%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20rides_nn%2C%0A)%3A%0A%20%20%20%20%23%20forward%20pass%20through%20the%20trained%20(16%2C8)%20net%20on%20the%20holdout%20fold%0A%20%20%20%20_mlp%20%3D%20rides_nn.regressor_.steps%5B-1%5D%5B1%5D%0A%20%20%20%20_scaler%20%3D%20rides_nn.regressor_.steps%5B0%5D%5B1%5D%0A%20%20%20%20_df%20%3D%20get_features(df_raw%2C%20lagged_pcs%2C%20f%3D%22rides%22%2C%20t%3D%22nn%22)%0A%20%20%20%20_y%20%3D%20_df%5B%22queue_time_min%22%5D%0A%20%20%20%20_X%20%3D%20_df.drop(columns%3D%5B%22queue_time_min%22%2C%20%22queue_time_sec%22%5D)%0A%20%20%20%20_feat%20%3D%20list(_X.columns)%0A%20%20%20%20_Xte%20%3D%20_X.iloc%5Bint(len(_X)%20*%200.8)%3A%5D.reset_index(drop%3DTrue)%0A%20%20%20%20_yte%20%3D%20_y.iloc%5Bint(len(_X)%20*%200.8)%3A%5D%0A%20%20%20%20_W0%2C%20_b0%20%3D%20_mlp.coefs_%5B0%5D%2C%20_mlp.intercepts_%5B0%5D%0A%20%20%20%20_W1%2C%20_b1%20%3D%20_mlp.coefs_%5B1%5D%2C%20_mlp.intercepts_%5B1%5D%0A%20%20%20%20_W2%2C%20_b2%20%3D%20_mlp.coefs_%5B2%5D%2C%20_mlp.intercepts_%5B2%5D%0A%20%20%20%20_H1%20%3D%20np.maximum(0%2C%20_scaler.transform(_Xte)%20%40%20_W0%20%2B%20_b0)%20%20%20%23%20n%20x%2016%20(relu)%0A%0A%20%20%20%20def%20_pred(H)%3A%0A%20%20%20%20%20%20%20%20return%20np.expm1(np.maximum(0%2C%20H%20%40%20_W1%20%2B%20_b1)%20%40%20_W2%20%2B%20_b2).ravel()%0A%20%20%20%20_base%20%3D%20mean_absolute_error(_yte%2C%20_pred(_H1))%0A%0A%20%20%20%20%23%20ablation%20importance%20per%20neuron%0A%20%20%20%20_imp%20%3D%20np.array(%5Bmean_absolute_error(_yte%2C%20_pred(np.where(np.arange(16)%20%3D%3D%20_j%2C%200.0%2C%201.0)%20*%20_H1))%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%20_base%20for%20_j%20in%20range(16)%5D)%0A%20%20%20%20_order%20%3D%20np.argsort(-_imp)%0A%0A%20%20%20%20%23%20neuron%20x%20interpretable-feature%20correlation%20matrix%0A%20%20%20%20_concepts%20%3D%20%5B%22PC1_lag30%22%2C%20%22PC2_lag30%22%2C%20%22PC3_lag30%22%2C%20%22hour%22%2C%20%22temperature_f%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22day_name_Saturday%22%2C%20%22month_10%22%2C%20%22bargain_thrill_interaction%22%5D%0A%20%20%20%20_C%20%3D%20np.zeros((16%2C%20len(_concepts)))%0A%20%20%20%20with%20np.errstate(invalid%3D%22ignore%22%2C%20divide%3D%22ignore%22)%3A%0A%20%20%20%20%20%20%20%20for%20_j%20in%20range(16)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20_H1%5B%3A%2C%20_j%5D.std()%20%3E%201e-9%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_C%5B_j%5D%20%3D%20%5Bnp.corrcoef(_H1%5B%3A%2C%20_j%5D%2C%20_Xte%5B_c%5D.values)%5B0%2C%201%5D%20for%20_c%20in%20_concepts%5D%0A%20%20%20%20_C%20%3D%20np.nan_to_num(_C)%0A%0A%20%20%20%20%23%20heatmap%3A%20neurons%20sorted%20by%20importance%20(top%20%3D%20most)%20x%20what%20they%20detect%0A%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(11%2C%207))%0A%20%20%20%20_im%20%3D%20_ax.imshow(_C%5B_order%5D%2C%20cmap%3D%22RdBu_r%22%2C%20vmin%3D-0.6%2C%20vmax%3D0.6%2C%20aspect%3D%22auto%22)%0A%20%20%20%20_ax.set_xticks(range(len(_concepts)))%0A%20%20%20%20_ax.set_xticklabels(%5Bc.replace(%22_lag30%22%2C%20%22%22).replace(%22day_name_%22%2C%20%22%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.replace(%22bargain_thrill_interaction%22%2C%20%22bargain%C3%97thrill%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20c%20in%20_concepts%5D%2C%20rotation%3D40%2C%20ha%3D%22right%22%2C%20fontsize%3D9)%0A%20%20%20%20_ax.set_yticks(range(16))%0A%20%20%20%20_ax.set_yticklabels(%5Bf%22n%7B_j%7D%20%20(%CE%94%7B_imp%5B_j%5D%3A%2B.2f%7D)%22%20for%20_j%20in%20_order%5D%2C%20fontsize%3D9)%0A%20%20%20%20for%20_r%20in%20range(16)%3A%0A%20%20%20%20%20%20%20%20for%20_k%20in%20range(len(_concepts))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20abs(_C%5B_order%5B_r%5D%2C%20_k%5D)%20%3E%3D%200.25%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.text(_k%2C%20_r%2C%20f%22%7B_C%5B_order%5B_r%5D%2C%20_k%5D%3A%2B.2f%7D%22%2C%20ha%3D%22center%22%2C%20va%3D%22center%22%2C%20fontsize%3D7)%0A%20%20%20%20_ax.set_title(%22Hidden-1%20neurons%20(top%20%E2%86%92%20bottom%20%3D%20most%20%E2%86%92%20least%20important)%20%C3%97%20what%20they%20detect%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22color%20%3D%20corr(neuron%20activation%2C%20feature)%22)%0A%20%20%20%20_fig.colorbar(_im%2C%20ax%3D_ax%2C%20label%3D%22correlation%22%2C%20shrink%3D0.8)%0A%20%20%20%20plt.tight_layout()%0A%0A%20%20%20%20%23%20dynamic%20narrative%20(neuron%20indices%20shift%20run-to-run%2C%20so%20compute%2C%20don't%20hardcode)%0A%20%20%20%20_pc1i%20%3D%20_feat.index(%22PC1_lag30%22)%0A%20%20%20%20_busy%20%3D%20int(np.argmax(np.abs(_C%5B%3A%2C%200%5D)))%0A%20%20%20%20_top%20%3D%20int(_order%5B0%5D)%0A%20%20%20%20_busy_rank%20%3D%20int(np.where(_order%20%3D%3D%20_busy)%5B0%5D%5B0%5D)%20%2B%201%0A%20%20%20%20_is_ride%20%3D%20np.array(%5Bf.startswith(%22ride_name_%22)%20for%20f%20in%20_feat%5D)%0A%20%20%20%20_rshare%20%3D%20np.abs(_W0%5B_is_ride%5D).sum(0)%20%2F%20(np.abs(_W0).sum(0)%20%2B%201e-9)%0A%20%20%20%20_mem%20%3D%20int(np.argmax(_rshare))%0A%20%20%20%20_same%20%3D%20(%22%E2%80%94%20and%20it's%20also%20the%20**single%20most%20important**%20neuron%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20if%20_busy%20%3D%3D%20_top%20else%20f%22%E2%80%94%20ranked%20**%23%7B_busy_rank%7D%20of%2016**%20by%20importance%22)%0A%0A%20%20%20%20_tbl%20%3D%20pd.DataFrame(%7B%0A%20%20%20%20%20%20%20%20%22neuron%22%3A%20%5Bf%22n%7B_j%7D%22%20for%20_j%20in%20_order%5D%2C%0A%20%20%20%20%20%20%20%20%22ablation%20%CE%94MAE%22%3A%20_imp%5B_order%5D.round(3)%2C%0A%20%20%20%20%20%20%20%20%22top%20input%20weights%22%3A%20%5B%22%2C%20%22.join(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7B_feat%5Bi%5D.replace('ride_name_'%2C%20'R%3A').replace('day_name_'%2C%20'')%7D%3A%7B_W0%5Bi%2C%20_j%5D%3A%2B.2f%7D%22%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20i%20in%20np.argsort(-np.abs(_W0%5B%3A%2C%20_j%5D))%5B%3A3%5D)%20for%20_j%20in%20_order%5D%2C%0A%20%20%20%20%7D)%0A%0A%20%20%20%20mo.vstack(%5B%0A%20%20%20%20%20%20%20%20mo.md(f%22%22%22%23%23%23%20Reading%20the%2016%20neurons%20%20(holdout%20MAE%20%7B_base%3A.2f%7D)%0A%0A%20%20%20%20**The%20punchline.**%20The%20neuron%20that%20most%20tracks%20park%20busyness%20is%20**n%7B_busy%7D**%0A%20%20%20%20(corr%20**%7B_C%5B_busy%2C%200%5D%3A%2B.2f%7D**%20with%20%60PC1_lag30%60%2C%20input%20weight%20**%7B_W0%5B_pc1i%2C%20_busy%5D%3A%2B.2f%7D**)%0A%20%20%20%20%7B_same%7D.%20With%20zero%20knowledge%20of%20our%20PCA%2C%20the%20network%20independently%20elevated%0A%20%20%20%20*%22how%20busy%20was%20the%20park%2030%20minutes%20ago%22*%20to%20a%20top%20internal%20feature%20%E2%80%94%20the%20very%0A%20%20%20%20signal%20**PC1**%20dominated%20the%20OLS%20with%20(coef%20%2B2.72%2C%20t%20%3D%20358).%20Two%20completely%0A%20%20%20%20different%20model%20families%2C%20one%20discovery.%0A%0A%20%20%20%20**Division%20of%20labor.**%20Neuron%20**n%7B_mem%7D**%20is%20a%20*ride-memorizer*%3A%20**%7B_rshare%5B_mem%5D%3A.0%25%7D**%0A%20%20%20%20of%20its%20input%20weight%20sits%20on%20specific%20ride%20dummies%20and%20its%20heatmap%20row%20is%20nearly%0A%20%20%20%20flat%20(it%20ignores%20park-state%20entirely).%20The%20net%20split%20its%2016%20neurons%20between%20the%0A%20%20%20%20global%20crowd%20signal%20(the%20PC-tracking%20rows)%20and%20the%20ride-identity%20lookup.%0A%0A%20%20%20%20**Honest%20caveat.**%20Because%20%60PC1_lag30%60%20is%20itself%20an%20input%2C%20a%20neuron%20leaning%20on%20it%0A%20%20%20%20isn't%20a%20*from-scratch*%20rediscovery%20%E2%80%94%20the%20non-trivial%20result%20is%20that%20ablation%20ranks%0A%20%20%20%20those%20park-state%20neurons%20at%20the%20**top**.%20And%20unlike%20PCA's%20clean%20orthogonal%20axes%2C%0A%20%20%20%20the%20net%20**smears**%20busyness%20across%20several%20neurons%20rather%20than%20isolating%20one%20%E2%80%94%0A%20%20%20%20fuzzier%20than%20PCA%2C%20but%20unmistakably%20the%20same%20concept%20the%20linear%20model%20found.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20_fig%2C%0A%20%20%20%20%20%20%20%20_tbl%2C%0A%20%20%20%20%5D)%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
231da9d4e64cc50e4b58afb04ee1130e