1414 load_and_validate_csv ,
1515 refine_activity_labels ,
1616 optimize_dataframe_memory ,
17+ winsorize_series ,
1718 create_analysis_config ,
1819 run_full_analysis ,
1920 format_business_report ,
@@ -551,12 +552,69 @@ def _run():
551552 st .warning (msg )
552553 st .stop ()
553554
555+ # ---------------------------------------------------------------------------
556+ # Winsorize revenue/price outliers - opt-in, applied right after the data is
557+ # cached (both raw_df and df_ready) and before anything downstream reads
558+ # 'price' (business insights' AOV/revenue trend/category breakdown, sampling
559+ # strata, etc.), so a handful of extreme values don't dilute those reports.
560+ # Caps values rather than dropping rows - see prox.winsorize_series.
561+ # ---------------------------------------------------------------------------
562+ st .divider ()
563+ st .header ("2. Handle Outliers" )
564+ if "price" not in raw_df .columns :
565+ st .caption ("No revenue/price column detected - nothing to winsorize." )
566+ else :
567+ winsorize_enabled = st .checkbox (
568+ "Winsorize Revenue/Price Outliers" , value = False ,
569+ help = (
570+ "Caps extreme values in the revenue/price column instead of "
571+ "removing those rows, so a handful of outlier orders don't "
572+ "dilute Average Order Value, revenue trend, or category "
573+ "revenue breakdown in Business Insights."
574+ )
575+ )
576+ if winsorize_enabled :
577+ w_col1 , w_col2 = st .columns (2 )
578+ with w_col1 :
579+ winsorize_method_label = st .radio (
580+ "Method" , ["Standard Deviation" , "Percentile" ], horizontal = True ,
581+ help = (
582+ "Standard Deviation: caps at mean +/- N standard deviations. "
583+ "Percentile: caps at the Nth/100-Nth percentile band."
584+ )
585+ )
586+ with w_col2 :
587+ if winsorize_method_label == "Standard Deviation" :
588+ winsorize_param = st .slider (
589+ "Std deviations" , 1.0 , 5.0 , 3.0 , 0.5 ,
590+ help = "Values beyond mean +/- this many standard deviations are capped." ,
591+ )
592+ else :
593+ winsorize_param = st .slider (
594+ "Percentile cutoff" , 0.5 , 10.0 , 1.0 , 0.5 ,
595+ help = "Caps at this percentile and its mirror (e.g. 1 = 1st/99th percentile)." ,
596+ )
597+
598+ winsorize_method = "std" if winsorize_method_label == "Standard Deviation" else "percentile"
599+ clipped , lower , upper = winsorize_series (raw_df ["price" ], method = winsorize_method , param = winsorize_param )
600+ n_capped = int (((raw_df ["price" ] < lower ) | (raw_df ["price" ] > upper )).sum ())
601+
602+ raw_df = raw_df .copy ()
603+ df_ready = df_ready .copy ()
604+ raw_df ["price" ] = clipped
605+ df_ready ["price" ] = df_ready ["price" ].clip (lower , upper )
606+
607+ if n_capped > 0 :
608+ st .info (f"Capped { n_capped :,} value(s) to the range [{ lower :,.2f} , { upper :,.2f} ]." )
609+ else :
610+ st .caption ("No values fell outside the winsorization bounds - nothing was capped." )
611+
554612# ---------------------------------------------------------------------------
555613# Data quality check - surfaced before filtering/analysis, so messy data is
556614# caught here instead of showing up as a confusing downstream result
557615# ---------------------------------------------------------------------------
558616st .divider ()
559- st .header ("2 . Data Quality Check" )
617+ st .header ("3 . Data Quality Check" )
560618data_quality = check_data_quality (raw_df )
561619if data_quality ["issues" ]:
562620 with st .expander (f"{ len (data_quality ['issues' ])} data quality issue(s) found" , expanded = True ):
@@ -569,7 +627,7 @@ def _run():
569627# Filter events before analysis
570628# ---------------------------------------------------------------------------
571629st .divider ()
572- st .header ("3 . Filter Events" )
630+ st .header ("4 . Filter Events" )
573631st .caption (
574632 "Remove noisy or irrelevant events before analysis, or narrow it down to "
575633 "just the events you care about. Optional - leave the list empty to "
@@ -646,7 +704,7 @@ def _run():
646704# Sampling - opt-in, with a warning above a "large" case-count threshold
647705# ---------------------------------------------------------------------------
648706st .divider ()
649- st .header ("4 . Sampling" )
707+ st .header ("5 . Sampling" )
650708enable_sampling = st .checkbox (
651709 "Enable Sampling" , value = False ,
652710 help = (
@@ -1482,5 +1540,5 @@ def _has_priority_value(series: pd.Series) -> bool:
14821540# Export: Build a Custom PDF Report
14831541# ---------------------------------------------------------------------------
14841542st .divider ()
1485- st .header ("5 . Build a Custom PDF Report" )
1543+ st .header ("6 . Build a Custom PDF Report" )
14861544render_pdf_builder (results , segment_result = st .session_state .get ("segment_result" ))
0 commit comments