@@ -73,8 +73,10 @@ def plot_event_study(
7373 periods : list, optional
7474 List of periods to plot. If None, uses all periods from results.
7575 reference_period : any, optional
76- The reference period (normalized to effect=0). Will be shown as a
77- hollow marker. If None, tries to infer from results.
76+ The reference period to highlight. When explicitly provided, effects
77+ are normalized (ref effect subtracted) and ref SE is set to NaN.
78+ When None and auto-inferred from results, only hollow marker styling
79+ is applied (no normalization). If None, tries to infer from results.
7880 pre_periods : list, optional
7981 List of pre-treatment periods. Used for shading.
8082 post_periods : list, optional
@@ -151,8 +153,9 @@ def plot_event_study(
151153 trends holds. Large pre-treatment effects suggest the assumption may
152154 be violated.
153155
154- 2. **Reference period**: Usually the last pre-treatment period (t=-1),
155- normalized to zero. This is the omitted category.
156+ 2. **Reference period**: Usually the last pre-treatment period (t=-1).
157+ When explicitly specified via ``reference_period``, effects are normalized
158+ to zero at this period. When auto-inferred, shown with hollow marker only.
156159
157160 3. **Post-treatment periods**: The treatment effects of interest. These
158161 show how the outcome evolved after treatment.
@@ -170,10 +173,18 @@ def plot_event_study(
170173
171174 from scipy import stats as scipy_stats
172175
176+ # Track if reference_period was explicitly provided by user
177+ reference_period_explicit = reference_period is not None
178+
173179 # Extract data from results if provided
174180 if results is not None :
175- effects , se , periods , pre_periods , post_periods , reference_period = \
176- _extract_plot_data (results , periods , pre_periods , post_periods , reference_period )
181+ extracted = _extract_plot_data (
182+ results , periods , pre_periods , post_periods , reference_period
183+ )
184+ effects , se , periods , pre_periods , post_periods , reference_period , reference_inferred = extracted
185+ # If reference was inferred from results, it was NOT explicitly provided
186+ if reference_inferred :
187+ reference_period_explicit = False
177188 elif effects is None or se is None :
178189 raise ValueError (
179190 "Must provide either 'results' or both 'effects' and 'se'"
@@ -192,6 +203,19 @@ def plot_event_study(
192203 # Compute confidence intervals
193204 critical_value = scipy_stats .norm .ppf (1 - alpha / 2 )
194205
206+ # Normalize effects to reference period ONLY if explicitly specified by user
207+ # Auto-inferred reference periods (from CallawaySantAnna) just get hollow marker styling,
208+ # NO normalization. This prevents unintended normalization when the reference period
209+ # isn't a true identifying constraint (e.g., CallawaySantAnna with base_period="varying").
210+ if (reference_period is not None and reference_period in effects and
211+ reference_period_explicit ):
212+ ref_effect = effects [reference_period ]
213+ if np .isfinite (ref_effect ):
214+ effects = {p : e - ref_effect for p , e in effects .items ()}
215+ # Set reference SE to NaN (it's now a constraint, not an estimate)
216+ # This follows fixest convention where the omitted category has no SE/CI
217+ se = {p : (np .nan if p == reference_period else s ) for p , s in se .items ()}
218+
195219 plot_data = []
196220 for period in periods :
197221 effect = effects .get (period , np .nan )
@@ -304,14 +328,17 @@ def _extract_plot_data(
304328 pre_periods : Optional [List [Any ]],
305329 post_periods : Optional [List [Any ]],
306330 reference_period : Optional [Any ],
307- ) -> Tuple [Dict , Dict , List , List , List , Any ]:
331+ ) -> Tuple [Dict , Dict , List , List , List , Any , bool ]:
308332 """
309333 Extract plotting data from various result types.
310334
311335 Returns
312336 -------
313337 tuple
314- (effects, se, periods, pre_periods, post_periods, reference_period)
338+ (effects, se, periods, pre_periods, post_periods, reference_period, reference_inferred)
339+
340+ reference_inferred is True if reference_period was auto-detected from results
341+ rather than explicitly provided by the user.
315342 """
316343 # Handle DataFrame input
317344 if isinstance (results , pd .DataFrame ):
@@ -328,7 +355,8 @@ def _extract_plot_data(
328355 if periods is None :
329356 periods = list (results ['period' ])
330357
331- return effects , se , periods , pre_periods , post_periods , reference_period
358+ # DataFrame input: reference_period was already set by caller, never inferred here
359+ return effects , se , periods , pre_periods , post_periods , reference_period , False
332360
333361 # Handle MultiPeriodDiDResults
334362 if hasattr (results , 'period_effects' ):
@@ -348,7 +376,8 @@ def _extract_plot_data(
348376 if periods is None :
349377 periods = post_periods
350378
351- return effects , se , periods , pre_periods , post_periods , reference_period
379+ # MultiPeriodDiDResults: reference_period was already set by caller, never inferred here
380+ return effects , se , periods , pre_periods , post_periods , reference_period , False
352381
353382 # Handle CallawaySantAnnaResults (event study aggregation)
354383 if hasattr (results , 'event_study_effects' ) and results .event_study_effects is not None :
@@ -362,8 +391,12 @@ def _extract_plot_data(
362391 if periods is None :
363392 periods = sorted (effects .keys ())
364393
394+ # Track if reference_period was explicitly provided vs auto-inferred
395+ reference_inferred = False
396+
365397 # Reference period is typically -1 for event study
366398 if reference_period is None :
399+ reference_inferred = True # We're about to infer it
367400 # Detect reference period from n_groups=0 marker (normalization constraint)
368401 # This handles anticipation > 0 where reference is at e = -1 - anticipation
369402 for period , effect_data in results .event_study_effects .items ():
@@ -380,7 +413,7 @@ def _extract_plot_data(
380413 if post_periods is None :
381414 post_periods = [p for p in periods if p >= 0 ]
382415
383- return effects , se , periods , pre_periods , post_periods , reference_period
416+ return effects , se , periods , pre_periods , post_periods , reference_period , reference_inferred
384417
385418 raise TypeError (
386419 f"Cannot extract plot data from { type (results ).__name__ } . "
0 commit comments