Skip to content

Commit 75609ea

Browse files
author
miranov25
committed
fix(draw): Resolve Subframe.column references in draw()
BUG_AliasDataFrame_20260324_draw_subframe_resolution draw() failed with ValueError when using Subframe.column syntax (e.g., 'Side.dy:row') because dfdraw's df.eval() interprets dots as attribute access. Also failed when subframe columns share names with main frame columns (auto_alias_subframe skips them). Fix: draw() detects Subframe.column patterns in expr, selection, and group_by, materializes them as temporary columns with underscore names (Side_dy) via join indices, rewrites expressions to match. 5 new tests, 1377 passed, 7 failed (pre-existing), no regressions.
1 parent be12a85 commit 75609ea

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

UTILS/dfextensions/AliasDataFrame/AliasDataFrame.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9978,6 +9978,50 @@ def draw(self,
99789978
df_subset = self.df
99799979
cleanup_needed = not effective_keep
99809980

9981+
# =================================================================
9982+
# Subframe column resolution for draw
9983+
# Detect 'Subframe.column' patterns in expression and selection,
9984+
# materialize them as temporary columns so dfdraw can access them.
9985+
# Column names use underscores (Sub_col) to avoid pandas eval issues.
9986+
# =================================================================
9987+
subframe_replacements = {}
9988+
if hasattr(self, '_subframes') and hasattr(self._subframes, 'subframes'):
9989+
sf_names = set(self._subframes.subframes.keys())
9990+
all_text = expr
9991+
if kwargs.get('selection'):
9992+
all_text += ' ' + kwargs['selection']
9993+
if kwargs.get('group_by'):
9994+
all_text += ' ' + str(kwargs['group_by'])
9995+
9996+
import re as _re
9997+
for match in _re.finditer(r'\b(\w+)\.(\w+)\b', all_text):
9998+
sf_name, col_name = match.group(1), match.group(2)
9999+
if sf_name in sf_names:
10000+
dot_ref = f"{sf_name}.{col_name}"
10001+
flat_ref = f"{sf_name}_{col_name}"
10002+
if flat_ref not in df_subset.columns and dot_ref not in subframe_replacements:
10003+
try:
10004+
sf = self.get_subframe(sf_name)
10005+
index_cols = self._subframes.get_entry(sf_name)['index']
10006+
if isinstance(index_cols, str):
10007+
index_cols = [index_cols]
10008+
join_idx, missing = self._compute_join_indices(sf_name, index_cols)
10009+
if col_name in sf.df.columns:
10010+
if df_subset is self.df:
10011+
df_subset = df_subset.copy()
10012+
df_subset[flat_ref] = sf.df[col_name].values[join_idx]
10013+
subframe_replacements[dot_ref] = flat_ref
10014+
except Exception:
10015+
pass
10016+
10017+
if subframe_replacements:
10018+
for dot_ref, flat_ref in subframe_replacements.items():
10019+
expr = expr.replace(dot_ref, flat_ref)
10020+
if 'selection' in kwargs and kwargs['selection']:
10021+
kwargs['selection'] = kwargs['selection'].replace(dot_ref, flat_ref)
10022+
if 'group_by' in kwargs and isinstance(kwargs.get('group_by'), str):
10023+
kwargs['group_by'] = kwargs['group_by'].replace(dot_ref, flat_ref)
10024+
998110025
# Create plotter and delegate
998210026
plotter = DFDraw(df_subset)
998310027

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Tests for BUG_AliasDataFrame_20260324_draw_subframe_resolution
3+
4+
Verifies that draw() correctly resolves Subframe.column references
5+
when the subframe has columns with names that conflict with main frame columns.
6+
"""
7+
8+
import pytest
9+
import numpy as np
10+
import pandas as pd
11+
import sys
12+
import os
13+
14+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15+
from AliasDataFrame import AliasDataFrame
16+
17+
18+
@pytest.fixture
19+
def adf_with_subframe():
20+
"""Create ADF with subframe that has conflicting column names."""
21+
df_main = pd.DataFrame({
22+
'group': [0, 0, 0, 1, 1, 1],
23+
'x': np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]),
24+
'dy': np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]),
25+
})
26+
df_sub = pd.DataFrame({
27+
'group': [0, 1],
28+
'dy': [0.2, 0.5], # conflicts with main frame
29+
'dz': [0.01, 0.02], # no conflict
30+
})
31+
adf = AliasDataFrame(df_main)
32+
adf.register_subframe('Sub', AliasDataFrame(df_sub), index_columns=['group'])
33+
return adf
34+
35+
36+
class TestDrawSubframeResolution:
37+
"""draw() should resolve Subframe.column references automatically."""
38+
39+
def test_draw_subframe_column_no_conflict(self, adf_with_subframe):
40+
"""draw('Sub.dz:x') works — dz has no name conflict."""
41+
import matplotlib
42+
matplotlib.use('Agg')
43+
adf = adf_with_subframe
44+
fig, ax, stats = adf.draw("Sub.dz:x", type='profile', bins=3)
45+
assert fig is not None
46+
assert ax is not None
47+
48+
def test_draw_subframe_column_with_conflict(self, adf_with_subframe):
49+
"""draw('Sub.dy:x') works — dy exists in both main and subframe."""
50+
import matplotlib
51+
matplotlib.use('Agg')
52+
adf = adf_with_subframe
53+
fig, ax, stats = adf.draw("Sub.dy:x", type='profile', bins=3)
54+
assert fig is not None
55+
assert ax is not None
56+
57+
def test_draw_subframe_column_values_correct(self, adf_with_subframe):
58+
"""Sub.dy values match subframe join (not main frame dy)."""
59+
import matplotlib
60+
matplotlib.use('Agg')
61+
adf = adf_with_subframe
62+
63+
fig, ax, stats = adf.draw("Sub.dy:x", type='profile', bins=2,
64+
return_data=True)
65+
profile_data = stats.get('profile_data')
66+
assert profile_data is not None
67+
68+
# Group 0 rows (x=1,2,3) should have Sub.dy = 0.2 (subframe value)
69+
# Group 1 rows (x=4,5,6) should have Sub.dy = 0.5 (subframe value)
70+
# Profile mean in first bin (x~2) should be ~0.2
71+
# Profile mean in second bin (x~5) should be ~0.5
72+
means = profile_data['y_mean'].values
73+
assert abs(means[0] - 0.2) < 0.01, f"Expected ~0.2, got {means[0]}"
74+
assert abs(means[1] - 0.5) < 0.01, f"Expected ~0.5, got {means[1]}"
75+
76+
def test_draw_subframe_in_selection(self, adf_with_subframe):
77+
"""Subframe.column works in selection parameter."""
78+
import matplotlib
79+
matplotlib.use('Agg')
80+
adf = adf_with_subframe
81+
# Use Sub.dz in selection (no conflict column)
82+
fig, ax, stats = adf.draw("dy:x", type='profile', bins=3,
83+
selection="Sub.dz<0.015")
84+
assert fig is not None
85+
# Should only have group 0 data (Sub.dz=0.01 < 0.015)
86+
87+
def test_auto_alias_subframe_conflict_skips(self, adf_with_subframe):
88+
"""auto_alias_subframe skips columns that exist in main frame."""
89+
adf = adf_with_subframe
90+
result = adf.auto_alias_subframe('Sub')
91+
# 'dy' should be skipped (exists in main frame)
92+
assert 'dy' in result['skipped_column']
93+
# 'dz' should be created (no conflict)
94+
assert any('dz' in alias for alias in result['created'])

0 commit comments

Comments
 (0)