"""README, file index and column dictionary for 05_Predictions.""" import glob, os, shutil, pandas as pd from pcommon import * IDX = [ ('00_KPI_TILES.csv', 'All', 'Every headline tile', 'screen x tile', 'mixed', 'Headline numbers for every screen, with display text, unit, label and a note'), ('01_reconciliation/reconciliation_checks.csv', '1 Reconciliation', 'Source file vs loaded', 'measure', 'Actual / Corrected', 'Binzagr file total vs loaded total per measure, difference and status'), ('03_forecast/forecast_metrics.csv', '3 Forecast proof', 'KPI tiles', 'level x metric', 'Actual', 'Hold-out error vs repeat-last-week, forecast value added, band coverage, safety stock, validation scores of every candidate'), ('03_forecast/forecast_series.csv', '3 Forecast proof', 'Item picker (SKU / Van / Principal)', 'series', 'Actual', 'Every forecast series with name, volume class, model used, selling status (active / slowing / dormant), hold-out error and a promotion-spike flag'), ('03_forecast/forecast_backtest_weekly.csv', '3 Forecast proof', '1 Forecast band vs actual', 'series x week', 'Actual (history) / Forecast (hold-out)', 'Weekly actuals Mar-Oct 2026; for the 4 hold-out weeks also p10 / p50 / p90 forecast and the repeat-last-week benchmark'), ('03_forecast/forecast_model_race.csv', '3 Forecast proof', '2 Model race', 'level x model', 'Actual', 'Series won on validation and on the hold-out, error if one model were used for everything; foundation models listed as not run'), ('03_forecast/forecast_next_12_weeks.csv', '3 Forecast proof', '3 Next 12 weeks', 'series x week', 'Forecast', 'Weeks ending 27 Oct 2026 - 12 Jan 2027: p10 / p50 / p90 and selling status; SKU level adds safety stock, a suggested order quantity and the order rule'), ('04_warehouse_labour/warehouse_cost_per_activity.csv', '4 Warehouse labour', '1 Cost per activity', 'activity', 'Corrected / Simulated', 'Operations, hours, SAR per 100 ops, seconds per op vs the median and best-quartile person-month, saving at those rates'), ('04_warehouse_labour/warehouse_badge_sharing_shifts.csv', '4 Warehouse labour', '2 Badge-sharing fix', 'shift', 'Corrected', 'Every high / medium-risk or over-12-hour shift: recorded hours, corrected hours for two people, reasons; drill to scans by person_session_id'), ('04_warehouse_labour/warehouse_heads_profile_by_hour.csv', '4 Warehouse labour', '3 Heads needed vs on shift', 'site x weekday x hour', 'Corrected', 'Average day, Aug-Sep 2026: heads on shift vs heads the workload needs at median and top-quartile rates'), ('04_warehouse_labour/warehouse_heads_hourly_last_4_weeks.csv', '4 Warehouse labour', '3 Heads needed vs on shift (actual days)', 'site x hour', 'Corrected', 'Same measures for each hour of 3-30 Sep 2026'), ('04_warehouse_labour/warehouse_workload_forecast_daily.csv', '4 Warehouse labour', '3 Workload forecast', 'site x day', 'Forecast', 'September back-test (made on 31 Aug) and October 2026 forecast of workload, with heads needed per 8-hour shift'), ('04_warehouse_labour/warehouse_heads_forecast_hourly_oct.csv', '4 Warehouse labour', '3 October plan by hour', 'site x day x hour', 'Forecast', 'October workload spread over each site\'s hour-of-day profile: heads needed by hour vs typical heads on shift'), ('04_warehouse_labour/warehouse_role_activity_hours.csv', '4 Warehouse labour', '4 Role x activity hours', 'WMS role x activity family', 'Actual', 'Scan work hours by WMS menu level and activity; flags storekeepers and managers on floor work (no job titles in the data)'), ('05_van_rep_day/van_list.csv', '5 Van rep day', 'Van selector, 4 Van productivity', 'van', 'Actual', 'Activity, outlets per day, sales, outlets served and overlap with other vans, active flag'), ('05_van_rep_day/van_visit_plan_next_week.csv', '5 Van rep day', '1 Today\'s outlets, at-risk first', 'van x day x outlet', 'Forecast', '21-27 Oct 2026: outlets each van is likely to visit (probability >= 0.15), at-risk first, with churn risk and the 5 offers'), ('05_van_rep_day/next_best_sku_by_outlet.csv', '5 Van rep day + 6 Outlets', 'Offer next', 'outlet x rank (1-5)', 'Forecast', 'Top 5 SKUs each outlet has not bought yet: chance of buying in 8 weeks, typical spend, expected value, plain reason'), ('05_van_rep_day/van_load_suggestion_next_week.csv', '5 Van rep day', '2 Suggested load (cases)', 'van x SKU', 'Forecast (Simulated until van load data)', 'Next week p10 / p50 / p90 cases per van and SKU, last week sold, returns of the last 4 weeks'), ('05_van_rep_day/next_best_sku_backtest.csv', '5 Van rep day', '3 Are the picks real?', 'method', 'Actual', 'Share of picks bought unprompted in the 8 unseen weeks after 25 Aug: our picks vs best-sellers vs random'), ('05_van_rep_day/next_best_sku_hit_rate_by_rank.csv', '5 Van rep day', '3 detail', 'pick rank', 'Actual', 'Share of picks bought, by position 1-5'), ('05_van_rep_day/van_metrics.csv', '5 Van rep day', 'Model checks', 'area x metric', 'Actual', 'Visit-plan, van-load and productivity metrics'), ('06_outlets/outlets.csv', '6 Outlets', '2 Outlet card, 3 Win-back, map colours', 'outlet', 'Actual / Forecast', 'Every outlet that bought Mar-Oct 2026: names, credit / cash, spend band, status, churn risk, dates, value, balance, vans, GPS, last SKUs, offers, nearest active van'), ('06_outlets/outlet_map_points.csv', '6 Outlets', '1 Riyadh outlet map', 'customer', 'Actual / Forecast', 'All 18,232 customers with GPS: status, churn risk, spend band, main van, lapsed ring'), ('06_outlets/outlet_monthly_sales.csv', '6 Outlets', '2 Card sparkline', 'outlet x month', 'Actual', 'Monthly net sales per outlet, Mar-Oct 2026'), ('06_outlets/winback_list.csv', '6 Outlets', '3 Win-back list', 'lapsed outlet', 'Actual / Forecast', 'Lapsed outlets by annual value: last van, days quiet, nearest active van and distance, next-best SKUs'), ('06_outlets/churn_model_metrics.csv', '6 Outlets', 'KPI tiles detail', 'metric', 'Actual', 'AUC vs recency rule, top-20% capture, calibration by quintile, top features'), ('07_receivables/receivables_ageing_buckets.csv', '7 Receivables', '1 Ageing buckets', 'bucket', 'Corrected', 'Credit book by due status (FIFO ageing rebuilt from the balance snapshot and invoices)'), ('07_receivables/receivables_by_term.csv', '7 Receivables', '2 Credit vs cash', 'customer type x term', 'Actual', 'Customers, buying customers, balances, placeholder vs real credit limits by term code'), ('07_receivables/receivables_collection_list.csv', '7 Receivables', '3 Collection list', 'credit customer with a balance', 'Corrected', 'Priority P1-P4, who can collect (van rep or credit control), balance, past due, unmatched, exposure, days late, plain reasons'), ('07_receivables/receivables_cash_due_by_week.csv', '7 Receivables', 'Cash due schedule (extra)', 'week', 'Corrected', 'Open invoice amounts by due week, and what is overdue now'), ('08_value_rollup/value_levers.csv', '2 Impact tree + 8 Value roll-up', 'Tree levels, roll-up table', 'lever', 'Mixed', 'Each lever: area / function / lever, driver, formula, low / base / high SAR a year, one-off cash, labels, proof screen, overlap note, total'), ('08_value_rollup/value_assumptions.csv', '8 Value roll-up', '2 Assumption sliders', 'assumption', 'Researched / Assumption', 'Slider values (low / base / high), unit, source, and which levers use each'), ('08_value_rollup/value_data_asks.csv', '8 Value roll-up', '3 Data that turns Simulated into Actual', 'data item', '-', 'Each data ask and the levers it moves to Actual'), ] D = { 'screen': 'Wireframe screen number', 'screen_name': 'Wireframe screen name', 'tile': 'Tile caption', 'value': 'Numeric value', 'display': 'Text to show on the tile', 'unit': 'Unit of the value', 'label': 'Actual / Corrected / Simulated / Forecast (see README)', 'note': 'Context line for the tile or row', 'measure': 'What is being reconciled', 'binzagr_file': 'Total recomputed from Binzagr\'s raw export', 'loaded': 'Total after cleaning (02_Cleaned)', 'difference': 'loaded - binzagr_file', 'status': 'Match, or why the totals differ (outlets: active / slowing / lapsed)', 'source': 'Where the number comes from', 'level': 'Forecast level: sku (cases), van (SAR) or principal (SAR)', 'metric': 'Metric name', 'series_id': 'SKU code, van code or principal name', 'series_name': 'SKU description, van code or principal name', 'unit': 'Unit of the values', 'volume_class': 'A = SKUs making 80% of training volume, B = next 15%, C = rest', 'training_total': 'Total volume in the training weeks (to 22 Sep 2026)', 'model_used': 'Model behind the hold-out and forward forecast for this series', 'race_winner_validation': 'Model with the lowest error on this series in the validation weeks (1-22 Sep)', 'principal': 'Principal (product hierarchy level 1)', 'units_per_case': 'Units (EA) per selling unit (case)', 'holdout_actual': 'Actual volume in the 4 hold-out weeks', 'holdout_wape_model': 'Weekly error of our forecast on the hold-out (WAPE); blank when the series sold nothing in the hold-out', 'holdout_wape_naive': 'Weekly error of repeat-last-week on the hold-out (WAPE); blank when the series sold nothing in the hold-out', 'holdout_spike': 'A hold-out week was over 5x the series\' own 13-week maximum (promotion-like, unforecastable)', 'week_start': 'First day of the Wednesday-Tuesday week', 'week_end': 'Last day (Tuesday) of the week', 'split': 'history / holdout (hold-out = 23 Sep - 20 Oct 2026, never used for fitting or model choice)', 'actual': 'Actual value in the week', 'forecast_p10': 'Forecast, 10th percentile (lower band)', 'forecast_p50': 'Forecast, median (point forecast)', 'forecast_p90': 'Forecast, 90th percentile (upper band)', 'naive_repeat_last_week': 'Benchmark: last week\'s actual', 'model': 'Model name', 'family': 'Model family', 'series_won_validation': 'Series where this model had the lowest validation error', 'series_won_holdout': 'Series where this model had the lowest hold-out error (after the fact)', 'share_of_volume_won_holdout': 'Share of training volume in the series it won on the hold-out', 'validation_wape_if_used_for_all': 'Validation error if this model forecast every series', 'holdout_wape_if_used_for_all': 'Hold-out error if this model forecast every series', 'horizon_weeks': 'Weeks ahead of the 20 Oct 2026 data end', 'safety_stock_cases': 'Safety stock for 95% service with one-week cover (1.645 x error sigma of the last 16 weeks); 0 for slowing or dormant SKUs', 'suggested_order_qty_cases': 'Order-up-to quantity for the week: p50 + safety stock (active), p50 only (slowing), 0 (dormant). Stock on hand is not in the data', 'last_sale_week': 'Last week (ending Tuesday) with a sale', 'weeks_since_last_sale': 'Weeks from that week to 20 Oct 2026 (0 = sold in the last week)', 'usual_gap_weeks': 'Median weeks between weeks with a sale (1 = sells every week)', 'selling_status': 'active / slowing (no sale for max(4, 2x usual gap) weeks) / dormant (no sale for max(8, 3x usual gap) weeks)', 'order_rule': 'How the suggested order was set for this selling status', 'collect_via': 'Who can collect: the van rep on route (van still active), or credit control (van no longer active, or no purchase since March)', 'activity': 'Deck activity (slide 6), built from WMS transaction types', 'operations': 'Primary operations (e.g. receipts, pick lines, pallets loaded)', 'scan_work_hours': 'Work time seen in the scans (cycle time, idle gaps over 30 min excluded)', 'paid_hours_posted': 'Paid hours posted to the activity (Simulated, scan-day basis)', 'labour_cost_sar_8m': 'Labour cost posted to the activity, Feb-Sep 2026 (Simulated wages)', 'sar_per_100_ops': 'Labour SAR per 100 operations', 'seconds_per_op_actual': 'Average work seconds per operation', 'seconds_per_op_median_person_month': 'Median person-month rate', 'seconds_per_op_best_quartile': 'Best-quartile (25th percentile) person-month rate', 'rate_vs_best_quartile_pct': 'How much slower the average is than the best quartile (%)', 'saving_at_median_rate_sar_yr': 'SAR a year if the average ran at the median rate', 'saving_at_best_quartile_rate_sar_yr': 'SAR a year if the average ran at the best-quartile rate', 'person_months_benchmarked': 'Person-months with 30+ operations used for the rates', 'in_deck_slide_6': 'Activity is one of the six on deck slide 6', 'person_session_id': 'Person work session id (joins 02_Cleaned jeddah_wms_transactions.person_session_id and 03_Simulated sim_session_attendance.session_id)', 'person_id': 'P + payroll number, or B + badge for logins not in the user list', 'payroll_number': 'Payroll number from Binzagr\'s user list (no names)', 'badges': 'Badges used in the session', 'n_badges': 'Number of badges in the session', 'wms_role': 'WMS menu level (tally clerk / storekeeper / warehouse manager / not in user list)', 'main_wh': 'Site with most scans in the session', 'session_start': 'First scan of the session', 'session_end': 'Last scan of the session', 'recorded_hours_on_badge': 'Span from first to last scan, as recorded', 'corrected_hours_person_1': 'Corrected hours, first person (sessions over 12 h are split into two people)', 'corrected_hours_person_2': 'Corrected hours, second person (0 if not split)', 'paid_shifts': 'Paid shifts in the simulation (2 for split sessions)', 'concurrent_badge_h': 'Hours two badges of one person were active at the same time', 'badge_sharing_risk': 'high / medium / low / none (rules + anomaly model)', 'why_flagged': 'Plain reasons for the flag', 'scans': 'Scans in the session', 'date': 'Date', 'drill_to_scans': 'How to fetch the scans behind the row', 'site': 'Jeddah site (WH_ID)', 'weekday': 'Day of week', 'weekday_order': 'Saturday = 1 ... Friday = 7', 'hour_of_day': 'Hour of day (0-23)', 'heads_on_shift': 'People on shift in the hour (share of the hour inside their scanned session span)', 'heads_needed_median_rate': 'Heads the workload needs if people worked at their role\'s median rate', 'heads_needed_top_quartile_rate': 'Heads the workload needs at the top-quartile rate', 'workload_standard_hours': 'Workload in standard hours (P50 standard times of the scans)', 'n_days': 'Days averaged', 'period': 'Period averaged', 'hour': 'Clock hour', 'actual_workload_standard_hours': 'Actual workload in standard hours', 'forecast_workload_standard_hours': 'Forecast workload in standard hours', 'heads_needed_median_rate_8h': 'People needed for an 8-hour day at the median rate', 'heads_needed_top_quartile_rate_8h': 'People needed for an 8-hour day at the top-quartile rate', 'method': 'Method used', 'typical_heads_on_shift': 'Average heads on shift at that weekday and hour (Aug-Sep)', 'wms_role_menu_level': 'WMS menu level', 'activity_family': 'WMS activity family', 'share_of_role_hours': 'Share of the role\'s scan work hours', 'flag_senior_role_on_floor_work': 'Storekeeper or manager hours on loading, picking, moves, consolidation or replenishment', 'van': 'Van / sales rep code (no names)', 'active_days': 'Days with sales, Mar-Oct 2026', 'first_day': 'First sales day', 'last_day': 'Last sales day', 'median_outlets_per_day': 'Median outlets served per active day', 'sales_sar': 'Net sales, Mar-Oct 2026', 'median_sales_per_day_sar': 'Median net sales per active day', 'active_days_8w': 'Active days in the last 8 weeks', 'median_outlets_per_day_8w': 'Median outlets per day, last 8 weeks', 'sales_8w_sar': 'Net sales, last 8 weeks', 'outlets_served_8w': 'Distinct outlets served, last 8 weeks', 'share_outlets_also_served_by_other_vans_8w': 'Share of its outlets also served by another van (last 8 weeks)', 'active_now': 'Sold in the last 14 days', 'visit_rank_in_day': 'Order in the day\'s list: high churn risk first, then visit probability', 'outlet_id': 'dist_cd-cust_cd (joins every outlet file)', 'outlet_name': 'Outlet name (customer master)', 'outlet_name_ar': 'Outlet name in Arabic', 'visit_probability': 'Chance the van visits the outlet that day (model tested on 14-20 Oct)', 'expected_visits_van_day': 'Sum of visit probabilities for the van that day', 'days_since_last_visit_by_this_van': 'Days since this van last served the outlet', 'churn_risk_tier': 'High (>= 25% chance of no purchase in 8 weeks), Medium (10-25%), Low, Lapsed, or not scored', 'churn_risk_8w': 'Chance of no purchase in the next 8 weeks', 'spend_band': 'A top 10% / B next 20% / C next 30% / D bottom 40% of outlets by annual value', 'customer_type': 'Credit (Q) or Cash (C)', 'balance_sar': 'Outstanding balance, snapshot 20 Oct 2026', 'offer_1': 'Next-best SKU 1 (name)', 'offer_2': 'Next-best SKU 2', 'offer_3': 'Next-best SKU 3', 'offer_4': 'Next-best SKU 4', 'offer_5': 'Next-best SKU 5', 'offer_value_8w_sar': 'Expected value of the 5 offers over 8 weeks', 'week': 'Week covered', 'sku': 'Product code', 'sku_name': 'Product description', 'forecast_p10_cases': 'Forecast cases, 10th percentile', 'forecast_p50_cases': 'Forecast cases, median', 'suggested_load_p90_cases': 'Suggested load: 90th percentile of demand (covers 9 weeks in 10)', 'suggested_load_cases_rounded': 'Suggested load in whole cases after the SKU status rule: p90 (active or new SKUs), p50 (slowing), 0 (dormant)', 'sku_selling_status': 'Company-wide selling status of the SKU (as in forecast_series.csv); new = first sold after 22 Sep', 'load_rule': 'How the load was set for this SKU status', 'last_week_sold_cases': 'Cases sold in the week ending 20 Oct', 'avg_weekly_cases_8w': 'Average weekly cases, last 8 weeks', 'returns_last_4w_cases': 'Cases returned in the last 4 weeks', 'outlet_status': 'active / slowing / lapsed', 'main_van': 'Van with most visits in the last 13 weeks (else the last van)', 'rank': 'Pick position 1-5', 'p_buy_8w': 'Chance the outlet buys it within 8 weeks unprompted (measured hit rate at that position)', 'typical_8w_spend_sar': 'Typical spend in the first 8 weeks after an outlet starts buying this SKU', 'expected_value_8w_sar': 'p_buy_8w x typical_8w_spend_sar', 'why': 'Plain reason for the pick', 'picks_made_on': 'Data cut-off for the picks', 'precision_at_5': 'Share of the 5 picks bought in the 8 unseen weeks', 'outlets_with_at_least_one_pick_bought': 'Share of outlets that bought at least one pick', 'lift_vs_best_sellers': 'Precision / best-sellers precision', 'lift_vs_random': 'Precision / random precision', 'test_window': 'Test window', 'pick_rank': 'Pick position', 'share_bought_8w': 'Share of picks at that position bought within 8 weeks', 'area': 'Model or measure group', 'payment_term': 'Payment term code (ZC01 cash/cheque, ZC04 30 days end of month ...)', 'channel_code': 'Customer hierarchy level 2 code', 'channel_inferred': 'Channel inferred from outlet names (Inferred)', 'status': 'Outlet status at 20 Oct: active / slowing (> 30 days) / lapsed (> max(60 days, 3x usual gap))', 'first_purchase': 'First purchase in the window', 'last_purchase': 'Last purchase', 'days_since_last': 'Days since the last purchase (at 20 Oct)', 'usual_gap_days': 'Median days between purchase days', 'purchase_days': 'Days with a purchase', 'invoices': 'Invoices', 'sales_window_sar': 'Net sales Mar-Oct 2026', 'annual_value_sar': 'Average sales per year since the first purchase (conservative)', 'last_van': 'Van of the last purchase', 'vans_in_window': 'Distinct vans that served the outlet', 'lat': 'Latitude', 'lon': 'Longitude', 'last_sku_1': 'Top SKU of the last purchase day', 'last_sku_2': 'Second SKU of the last purchase day', 'last_sku_3': 'Third SKU of the last purchase day', 'nearest_active_van': 'Van serving the nearest outlet that bought in the last 4 weeks', 'nearest_active_km': 'Straight-line km to that outlet', 'ring_lapsed': 'Draw a ring: outlet is lapsed', 'month': 'Month (YYYY-MM)', 'net_sales_sar': 'Net sales in the month', 'days_quiet': 'Days since the last purchase', 'next_best_sku_1': 'Next-best SKU 1', 'next_best_sku_2': 'Next-best SKU 2', 'next_best_sku_3': 'Next-best SKU 3', 'next_best_sku_4': 'Next-best SKU 4', 'next_best_sku_5': 'Next-best SKU 5', 'priority': 'P1-P4 (risk points x exposure)', 'past_due_sar': 'Past due (FIFO ageing, Corrected)', 'past_due_over_60_sar': 'More than 60 days past due', 'unmatched_sar': 'Balance not matched to van-sales invoices since March (older debt or other channels)', 'exposure_sar': 'past_due_sar + unmatched_sar', 'days_late': 'Days past due of the oldest past-due invoice', 'monthly_buying_sar': 'Average monthly purchases, last 90 days', 'days_since_last_purchase': 'Days since the last purchase (blank = nothing bought since March)', 'return_rate_13w': 'Returns / sales, last 13 weeks', 'credit_limit_sar': 'Credit limit (blank when it is the 9,999,999 placeholder)', 'credit_limit_is_placeholder': 'Limit is the 9,999,999 placeholder', 'over_limit': 'Balance above a real credit limit', 'risk_points': 'Sum of rule points behind the priority', 'not_due_sar': 'Not yet due', 'due_1_30_sar': '1-30 days past due', 'due_31_60_sar': '31-60 days past due', 'due_61_90_sar': '61-90 days past due', 'due_90_plus_sar': 'Over 90 days past due', 'bucket': 'Ageing bucket', 'bucket_order': 'Sort order', 'amount_sar': 'Amount', 'customers': 'Customers', 'share_of_book': 'Share of the credit book', 'term': 'Payment term', 'buying_customers': 'Customers who bought Mar-Oct 2026', 'customers_with_balance': 'Customers with a positive balance', 'placeholder_limits': 'Credit limit = 9,999,999 placeholder', 'real_limits': 'Credit customers with a real limit', 'due_week_ending': 'Week (ending Tuesday) the amount falls due, or "overdue now"', 'lever_id': 'Lever id', 'level_2_area': 'Impact tree level 2 (business area)', 'level_3_function': 'Impact tree level 3 (function)', 'level_4_lever': 'Impact tree level 4 (lever)', 'driver': 'What drives the value', 'driver_value': 'Driver amount', 'driver_unit': 'Driver unit', 'formula': 'How the driver becomes SAR (assumption ids)', 'low_sar_yr': 'Low case, SAR a year', 'base_sar_yr': 'Base case, SAR a year', 'high_sar_yr': 'High case, SAR a year', 'one_off_cash_low_sar': 'One-off cash released, low', 'one_off_cash_base_sar': 'One-off cash released, base', 'one_off_cash_high_sar': 'One-off cash released, high', 'value_label': 'Label of the SAR value', 'evidence_label': 'Label of the evidence behind the driver', 'proof_screen': 'Screen that proves the driver', 'include_in_total': 'Added to the total (False = overlaps or proxy, shown only)', 'overlap_note': 'Caveat or overlap', 'assumption_id': 'Assumption id (used in formulas)', 'assumption': 'Assumption (slider)', 'low': 'Low value', 'base': 'Base value', 'high': 'High value', 'used_by': 'Levers that use it', 'data_needed': 'Data Binzagr would send', 'levers_moved_to_actual': 'Levers that become Actual with it', } rows, crow = [], [] for f, scr, wid, grain, lab, what in IDX: p = os.path.join(OUT, f); d = pd.read_csv(p, dtype=str, keep_default_na=False) rows.append({'file': f.replace('/', '\\'), 'screen': scr, 'widget': wid, 'grain': grain, 'rows': len(d), 'columns': d.shape[1], 'label': lab, 'what_it_holds': what}) for c in d.columns: desc = D.get(c, '') if c == 'status': desc = {'01_reconciliation/reconciliation_checks.csv': 'Match, or why the two totals differ', '03_forecast/forecast_model_race.csv': 'Whether the model was run and how it was used'}.get(f, desc) crow.append({'file': f.replace('/', '\\'), 'column': c, 'description': desc}) missing = sorted({r['column'] for r in crow if not r['description']}) print('columns without description:', missing) pd.DataFrame(rows).to_csv(os.path.join(OUT, '00_INDEX.csv'), index=False, encoding='utf-8-sig') pd.DataFrame(crow).to_csv(os.path.join(OUT, '00_COLUMNS.csv'), index=False, encoding='utf-8-sig') # scripts sd = os.path.join(OUT, 'scripts'); os.makedirs(sd, exist_ok=True) for s in sorted(glob.glob('/home/claude/binzagr/pred/*.py')): shutil.copy(s, sd) print('index rows', len(rows), 'column rows', len(crow))