Skip to content

Commit f4f197d

Browse files
author
VaitaR
committed
cashing
1 parent 01e8f08 commit f4f197d

17 files changed

Lines changed: 167 additions & 65 deletions

FALLBACK_REPORT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@
5353
| unordered | optimized_reentry | unique_users | time_to_convert | ✗ No | |
5454
| unordered | optimized_reentry | unique_users | cohort_analysis | ✗ No | |
5555

56-
## Recommendations
56+
## Recommendations

app.py

Lines changed: 136 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,98 @@
125125

126126
# Performance monitoring decorators
127127

128+
# Cached Data Loading Functions
129+
@st.cache_data
130+
def load_sample_data_cached() -> pd.DataFrame:
131+
"""Cached wrapper for loading sample data to prevent regeneration on every UI interaction"""
132+
from core import DataSourceManager
133+
manager = DataSourceManager()
134+
return manager.get_sample_data()
135+
136+
@st.cache_data
137+
def load_file_data_cached(file_name: str, file_size: int, file_type: str, file_content: bytes) -> pd.DataFrame:
138+
"""Cached wrapper for loading file data based on file properties to avoid re-processing same files"""
139+
import os
140+
import tempfile
141+
142+
from core import DataSourceManager
143+
144+
manager = DataSourceManager()
145+
146+
# Create a temporary file from the uploaded content
147+
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file_type}") as tmp_file:
148+
tmp_file.write(file_content)
149+
tmp_file.flush()
150+
temp_path = tmp_file.name
151+
152+
try:
153+
# Create a mock uploaded file object for the manager
154+
class MockUploadedFile:
155+
def __init__(self, name, content):
156+
self.name = name
157+
self._content = content
158+
159+
def getvalue(self):
160+
return self._content
161+
162+
mock_file = MockUploadedFile(file_name, file_content)
163+
return manager.load_from_file(mock_file)
164+
finally:
165+
# Clean up temporary file
166+
try:
167+
os.unlink(temp_path)
168+
except OSError:
169+
pass
170+
171+
@st.cache_data
172+
def load_clickhouse_data_cached(query: str, connection_hash: str) -> pd.DataFrame:
173+
"""Cached wrapper for ClickHouse data loading based on query and connection"""
174+
# Note: This assumes the connection is already established in session state
175+
if hasattr(st.session_state, "data_source_manager") and st.session_state.data_source_manager.clickhouse_client:
176+
return st.session_state.data_source_manager.load_from_clickhouse(query)
177+
return pd.DataFrame()
178+
179+
@st.cache_data
180+
def get_segmentation_properties_cached(events_data: pd.DataFrame) -> dict[str, list[str]]:
181+
"""Cached wrapper for getting segmentation properties to avoid repeated JSON parsing"""
182+
from core import DataSourceManager
183+
manager = DataSourceManager()
184+
return manager.get_segmentation_properties(events_data)
185+
186+
@st.cache_data
187+
def get_property_values_cached(events_data: pd.DataFrame, prop_name: str, prop_type: str) -> list[str]:
188+
"""Cached wrapper for getting property values to avoid repeated filtering"""
189+
from core import DataSourceManager
190+
manager = DataSourceManager()
191+
return manager.get_property_values(events_data, prop_name, prop_type)
192+
193+
@st.cache_data
194+
def get_sorted_event_names_cached(events_data: pd.DataFrame) -> list[str]:
195+
"""Cached wrapper for getting sorted event names to avoid repeated sorting"""
196+
return sorted(events_data["event_name"].unique())
197+
198+
@st.cache_data
199+
def calculate_timeseries_metrics_cached(
200+
events_data: pd.DataFrame,
201+
funnel_steps: tuple[str, ...],
202+
polars_period: str,
203+
config_dict: dict[str, Any],
204+
use_polars: bool = True
205+
) -> pd.DataFrame:
206+
"""
207+
Cached wrapper for time series calculation to prevent recalculation during tab switching.
208+
Uses tuple for funnel_steps and config dict for proper hashing.
209+
"""
210+
from core import FunnelCalculator
211+
212+
# Reconstruct config from dict
213+
config = FunnelConfig.from_dict(config_dict)
214+
calculator = FunnelCalculator(config, use_polars=use_polars)
215+
216+
# Convert tuple back to list for the calculator
217+
steps_list = list(funnel_steps)
218+
219+
return calculator.calculate_timeseries_metrics(events_data, steps_list, polars_period)
128220

129221
# Data Source Management
130222
def initialize_session_state():
@@ -201,6 +293,7 @@ def create_enhanced_event_selector_DISABLED():
201293
"""Create enhanced event selector with search, filters, and categorized display - DISABLED in simplified version"""
202294

203295

296+
@st.cache_data
204297
def get_comprehensive_performance_analysis() -> dict[str, Any]:
205298
"""
206299
Get comprehensive performance analysis from all monitored components
@@ -254,13 +347,14 @@ def get_comprehensive_performance_analysis() -> dict[str, Any]:
254347
return analysis
255348

256349

350+
@st.cache_data
257351
def get_event_statistics(events_data: pd.DataFrame) -> dict[str, dict[str, Any]]:
258352
"""Get comprehensive statistics for each event in the dataset"""
259353
if events_data is None or events_data.empty:
260354
return {}
261355

262356
event_stats = {}
263-
event_counts = events_data["event_name"].value_counts()
357+
events_data["event_name"].value_counts()
264358
total_events = len(events_data)
265359
unique_users = events_data["user_id"].nunique()
266360

@@ -368,10 +462,10 @@ def clear_all_steps():
368462
if "event_statistics" not in st.session_state:
369463
st.session_state.event_statistics = get_event_statistics(st.session_state.events_data)
370464

371-
# Cache the sorted list of unique event names for better UI performance
465+
# Use cached sorted event names for better UI performance
372466
if "sorted_event_names" not in st.session_state:
373-
st.session_state.sorted_event_names = sorted(
374-
st.session_state.events_data["event_name"].unique()
467+
st.session_state.sorted_event_names = get_sorted_event_names_cached(
468+
st.session_state.events_data
375469
)
376470

377471
available_events = st.session_state.sorted_event_names
@@ -401,7 +495,7 @@ def clear_all_steps():
401495

402496
with event_col1:
403497
# Checkbox с улучшенным дизайном
404-
selected = st.checkbox(
498+
st.checkbox(
405499
f"**{event}**",
406500
value=is_selected,
407501
key=f"event_cb_{event.replace(' ', '_').replace('-', '_')}",
@@ -541,9 +635,9 @@ def main():
541635
if data_source == "Sample Data":
542636
if st.button("Load Sample Data", key="load_sample_data_button"):
543637
with st.spinner("Loading sample data..."):
544-
st.session_state.events_data = (
545-
st.session_state.data_source_manager.get_sample_data()
546-
)
638+
# Use cached sample data loading
639+
st.session_state.events_data = load_sample_data_cached()
640+
547641
# Clear cached event names when new data is loaded
548642
if "sorted_event_names" in st.session_state:
549643
del st.session_state.sorted_event_names
@@ -562,9 +656,17 @@ def main():
562656

563657
if uploaded_file is not None:
564658
with st.spinner("Processing file..."):
565-
st.session_state.events_data = (
566-
st.session_state.data_source_manager.load_from_file(uploaded_file)
659+
# Use cached file loading based on file properties
660+
file_content = uploaded_file.getvalue()
661+
file_type = uploaded_file.name.split('.')[-1].lower()
662+
663+
st.session_state.events_data = load_file_data_cached(
664+
uploaded_file.name,
665+
uploaded_file.size,
666+
file_type,
667+
file_content
567668
)
669+
568670
if not st.session_state.events_data.empty:
569671
# Clear cached event names when new data is loaded
570672
if "sorted_event_names" in st.session_state:
@@ -612,9 +714,14 @@ def main():
612714

613715
if st.button("Execute Query"):
614716
with st.spinner("Executing query..."):
615-
st.session_state.events_data = (
616-
st.session_state.data_source_manager.load_from_clickhouse(ch_query)
717+
# Create a connection hash for caching
718+
connection_hash = f"{ch_host}:{ch_port}:{ch_database}:{ch_username}"
719+
720+
# Use cached ClickHouse loading
721+
st.session_state.events_data = load_clickhouse_data_cached(
722+
ch_query, connection_hash
617723
)
724+
618725
if not st.session_state.events_data.empty:
619726
# Clear cached event names when new data is loaded
620727
if "sorted_event_names" in st.session_state:
@@ -640,7 +747,7 @@ def main():
640747
st.markdown("### ⚡ Performance")
641748
if "performance_history" in st.session_state and st.session_state.performance_history:
642749
latest_performance = st.session_state.performance_history[-1]
643-
performance_analysis = get_comprehensive_performance_analysis()
750+
get_comprehensive_performance_analysis()
644751

645752
# Display current performance
646753
st.markdown("**Latest Analysis:**")
@@ -773,11 +880,9 @@ def main():
773880
selected_values = []
774881

775882
if st.session_state.events_data is not None and not st.session_state.events_data.empty:
776-
# Update available properties
777-
st.session_state.available_properties = (
778-
st.session_state.data_source_manager.get_segmentation_properties(
779-
st.session_state.events_data
780-
)
883+
# Update available properties using cached function
884+
st.session_state.available_properties = get_segmentation_properties_cached(
885+
st.session_state.events_data
781886
)
782887

783888
if st.session_state.available_properties:
@@ -801,11 +906,9 @@ def main():
801906
if selected_property != "None":
802907
prop_type, prop_name = selected_property.split("_", 1)
803908

804-
# Get available values for this property
805-
prop_values = (
806-
st.session_state.data_source_manager.get_property_values(
807-
st.session_state.events_data, prop_name, prop_type
808-
)
909+
# Get available values for this property using cached function
910+
prop_values = get_property_values_cached(
911+
st.session_state.events_data, prop_name, prop_type
809912
)
810913

811914
if prop_values:
@@ -1451,8 +1554,16 @@ def format_time(minutes):
14511554
if hasattr(st.session_state.data_source_manager, "_last_lazy_df"):
14521555
lazy_df = st.session_state.data_source_manager.get_lazy_frame()
14531556

1454-
timeseries_data = calculator.calculate_timeseries_metrics(
1455-
st.session_state.events_data, results.steps, polars_period, lazy_df
1557+
# Use cached time series calculation to prevent recalculation during tab switching
1558+
config_dict = st.session_state.funnel_config.to_dict()
1559+
funnel_steps_tuple = tuple(results.steps)
1560+
1561+
timeseries_data = calculate_timeseries_metrics_cached(
1562+
st.session_state.events_data,
1563+
funnel_steps_tuple,
1564+
polars_period,
1565+
config_dict,
1566+
use_polars
14561567
)
14571568

14581569
if not timeseries_data.empty:

core/calculator.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ def calculate_funnel_metrics(
10061006
Returns:
10071007
FunnelResults object with all calculated metrics
10081008
"""
1009-
start_time = time.time()
1009+
time.time()
10101010

10111011
if len(funnel_steps) < 2:
10121012
return FunnelResults(
@@ -1088,7 +1088,7 @@ def _calculate_funnel_metrics_polars(
10881088
existing_events_in_data = set(
10891089
polars_df.select("event_name").unique().to_series().to_list()
10901090
)
1091-
funnel_steps_in_data = set(funnel_steps) & existing_events_in_data
1091+
set(funnel_steps) & existing_events_in_data
10921092

10931093
zero_counts = [0] * len(funnel_steps)
10941094
drop_offs = [0] * len(funnel_steps)
@@ -1167,7 +1167,6 @@ def _calculate_funnel_metrics_polars(
11671167

11681168
# Ensure consistent data types between DataFrames
11691169
# Get the schema of segment_polars_df
1170-
segment_schema = segment_polars_df.schema
11711170

11721171
# Cast user_id in both DataFrames to string to ensure consistent types
11731172
segment_polars_df = segment_polars_df.with_columns(
@@ -1272,7 +1271,7 @@ def _calculate_funnel_metrics_pandas(
12721271
return FunnelResults([], [], [], [], [])
12731272
# Events exist but none match funnel steps - check if any of the funnel steps exist in the data at all
12741273
existing_events_in_data = set(events_df["event_name"].unique())
1275-
funnel_steps_in_data = set(funnel_steps) & existing_events_in_data
1274+
set(funnel_steps) & existing_events_in_data
12761275

12771276
zero_counts = [0] * len(funnel_steps)
12781277
drop_offs = [0] * len(funnel_steps)
@@ -2013,7 +2012,7 @@ def _calculate_timeseries_metrics_pandas(
20132012

20142013
# Define first and last steps
20152014
first_step = funnel_steps[0]
2016-
last_step = funnel_steps[-1]
2015+
funnel_steps[-1]
20172016
conversion_window_hours = self.config.conversion_window_hours
20182017

20192018
try:
@@ -3250,7 +3249,7 @@ def _calculate_path_analysis_polars_optimized(
32503249
# Fully vectorized approach for between-steps analysis
32513250
try:
32523251
# Get unique user IDs with valid conversion pairs
3253-
user_ids = conversion_pairs.select("user_id").unique()
3252+
conversion_pairs.select("user_id").unique()
32543253

32553254
# Create a lazy frame with step pairs information
32563255
step_pairs_lazy = conversion_pairs.lazy().select(
@@ -3785,7 +3784,7 @@ def _analyze_between_steps_polars(
37853784
continue
37863785

37873786
step_A_time = user_A[0, "step_A_time"]
3788-
conversion_window = timedelta(hours=self.config.conversion_window_hours)
3787+
timedelta(hours=self.config.conversion_window_hours)
37893788

37903789
# Find first B after A within conversion window
37913790
potential_Bs = user_B.filter(
@@ -4576,7 +4575,7 @@ def _calculate_unique_pairs_funnel_polars(
45764575
users_count.append(count)
45774576

45784577
# For unique pairs, conversion rate is step-to-step
4579-
step_conversion_rate = (
4578+
(
45804579
(count / len(prev_step_users) * 100) if len(prev_step_users) > 0 else 0
45814580
)
45824581
# But we also track overall conversion rate from first step for consistency
@@ -5694,7 +5693,7 @@ def _user_did_later_steps_before_current(
56945693
try:
56955694
# Get the funnel sequence from the order that steps appear in the overall dataset
56965695
# This is a heuristic but works for most cases
5697-
all_funnel_events = all_events_df["event_name"].unique()
5696+
all_events_df["event_name"].unique()
56985697

56995698
# For the test case, we know the sequence should be: Sign Up -> Email Verification -> First Login
57005699
# When checking Email Verification after Sign Up, we should see if First Login happened before Email Verification
@@ -5888,7 +5887,7 @@ def _calculate_unique_pairs_funnel(
58885887
users_count.append(count)
58895888

58905889
# For unique pairs, conversion rate is step-to-step
5891-
step_conversion_rate = (
5890+
(
58925891
(count / len(prev_step_users) * 100) if len(prev_step_users) > 0 else 0
58935892
)
58945893
# But we also track overall conversion rate from first step
@@ -5929,7 +5928,7 @@ def _user_did_later_steps_before_current_polars(
59295928
try:
59305929
# Find the indices of steps in the funnel
59315930
try:
5932-
prev_step_idx = funnel_steps.index(prev_step)
5931+
funnel_steps.index(prev_step)
59335932
current_step_idx = funnel_steps.index(current_step)
59345933
except ValueError:
59355934
# If the steps aren't in the funnel, we can't determine order

core/data_source.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ def _load_from_file_pandas_fallback(self, uploaded_file) -> pd.DataFrame:
322322

323323
return df
324324

325-
except Exception as e:
325+
except Exception:
326326
return pd.DataFrame()
327327

328328
def connect_clickhouse(
@@ -338,7 +338,7 @@ def connect_clickhouse(
338338
database=database,
339339
)
340340
# Test connection
341-
result = self.clickhouse_client.query("SELECT 1")
341+
self.clickhouse_client.query("SELECT 1")
342342
return True
343343
except Exception as e:
344344
st.error(f"ClickHouse connection failed: {str(e)}")

0 commit comments

Comments
 (0)