shtota commited on
Commit
8f81517
·
verified ·
1 Parent(s): e4c4919

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +295 -0
app.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import plotly.graph_objects as go
3
+ from plotly.subplots import make_subplots
4
+ import gradio as gr
5
+ import ast
6
+ from functools import lru_cache
7
+ from collections import Counter
8
+
9
+ # --- Constants and Mappings (Unchanged) ---
10
+ BODY_ORDER = ['Very light-bodied', 'Light-bodied', 'Medium-bodied', 'Full-bodied', 'Very full-bodied']
11
+ ACIDITY_ORDER = ['Low', 'Medium', 'High']
12
+ BODY_MAPPING = {'Very light-bodied': 1, 'Light-bodied': 2, 'Medium-bodied': 3, 'Full-bodied': 4, 'Very full-bodied': 5}
13
+ WINE_TYPE_ORDER = {'Red': 2, 'Rosé': 1, 'White': 0}
14
+ SAMPLE_THRESHOLDS = {'Common (100+)': 100, 'Uncommon (50+)': 50, 'Rare (20+)': 20}
15
+ COUNTRY_FLAGS = {
16
+ 'United States': '🇺🇸', 'France': '🇫🇷', 'Italy': '🇮🇹', 'Spain': '🇪🇸', 'Germany': '🇩🇪', 'Australia': '🇦🇺',
17
+ 'Chile': '🇨🇱', 'Argentina': '🇦🇷', 'Portugal': '🇵🇹', 'South Africa': '🇿🇦', 'New Zealand': '🇳🇿',
18
+ 'Austria': '🇦🇹', 'Greece': '🇬🇷', 'Hungary': '🇭🇺', 'Croatia': '🇭🇷', 'Slovenia': '🇸🇮', 'Canada': '🇨🇦',
19
+ 'Brazil': '🇧🇷', 'Uruguay': '🇺🇾', 'Israel': '🇮🇱', 'Lebanon': '🇱🇧', 'Turkey': '🇹🇷', 'Bulgaria': '🇧🇬',
20
+ 'Romania': '🇷🇴', 'Georgia': '🇬🇪', 'Moldova': '🇲🇩', 'Switzerland': '🇨🇭', 'England': '🏴'
21
+ }
22
+ FOOD_EMOJIS = {
23
+ 'Beef': '🥩', 'Pork': '🍖', 'Lamb': '🍖', 'Poultry': '🍗', 'Seafood': '🐟', 'Rich Fish': '🐠',
24
+ 'Shellfish': '🦐', 'Cheese': '🧀', 'Pasta': '🍝', 'Pizza': '🍕', 'Vegetables': '🥬', 'Vegetarian': '🥬',
25
+ 'Veal': '🥩', 'Game Meat': '🦌', 'Barbecue': '🍖', 'Codfish': '🐟', 'Sweet Dessert': '🍰', 'Dessert': '🍰',
26
+ 'Appetizers': '🍾', 'Fruit': '🍇', 'Nuts': '🥜', 'Chocolate': '🍫'
27
+ }
28
+
29
+
30
+ # --- OPTIMIZATION 1: Data Loading & Pre-processing ---
31
+ @lru_cache(maxsize=1)
32
+ def load_and_preprocess_data():
33
+ """Loads and performs expensive one-time preprocessing on the dataset."""
34
+ try:
35
+ df = pd.read_csv('XWines_Full_100K_wines.csv')
36
+ except FileNotFoundError:
37
+ raise FileNotFoundError("CSV file 'XWines_Full_100K_wines.csv' not found.")
38
+
39
+ def parse_list_string(s):
40
+ try:
41
+ return ast.literal_eval(s) if isinstance(s, str) else []
42
+ except (ValueError, SyntaxError):
43
+ return []
44
+
45
+ df['grapes_list'] = df['Grapes'].apply(parse_list_string)
46
+ df['harmonize_list'] = df['Harmonize'].apply(parse_list_string)
47
+ df['main_grape'] = df['grapes_list'].apply(lambda x: x[0] if x else 'Unknown')
48
+ df['num_grapes'] = df['grapes_list'].apply(len)
49
+ df['body_numeric'] = df['Body'].map(BODY_MAPPING)
50
+ return df
51
+
52
+
53
+ # --- OPTIMIZATION 2: Vectorized Data Aggregation ---
54
+ def get_top_food_pairings(series, top_n=3):
55
+ """Get top N food pairings with emojis and names."""
56
+ all_pairings = [item for sublist in series for item in sublist]
57
+ if not all_pairings:
58
+ return {'emojis': '🍽️', 'names': 'General'}
59
+
60
+ top_items = Counter(all_pairings).most_common(top_n)
61
+ emojis = ''.join([FOOD_EMOJIS.get(item[0], '🍽️') for item in top_items])
62
+ names = ', '.join([item[0] for item in top_items])
63
+ return {'emojis': emojis, 'names': names}
64
+
65
+
66
+ def aggregate_wine_data(df, wine_types, max_grape_count, min_samples_choice, regional_grouping):
67
+ """Filters and aggregates wine data using efficient, vectorized pandas operations."""
68
+ filtered_df = df.copy()
69
+
70
+ if wine_types and 'All' not in wine_types:
71
+ filtered_df = filtered_df[filtered_df['Type'].isin(wine_types)]
72
+ if max_grape_count < 5:
73
+ filtered_df = filtered_df[filtered_df['num_grapes'] <= max_grape_count]
74
+
75
+ group_by_cols = ['main_grape', 'Type']
76
+ if regional_grouping:
77
+ group_by_cols.append('Country')
78
+
79
+ agg_df = filtered_df.groupby(group_by_cols).agg(
80
+ count=('ABV', 'size'),
81
+ avg_fullness=('body_numeric', 'mean'),
82
+ abv_list=('ABV', list),
83
+ body_list=('Body', list),
84
+ acidity_list=('Acidity', list),
85
+ harmonize_list=('harmonize_list', list),
86
+ region_count=('RegionName', 'nunique'),
87
+ winery_count=('WineryName', 'nunique')
88
+ ).reset_index()
89
+
90
+ min_samples = SAMPLE_THRESHOLDS[min_samples_choice]
91
+ agg_df = agg_df[agg_df['count'] >= min_samples].copy()
92
+
93
+ if agg_df.empty:
94
+ return agg_df
95
+
96
+ # --- THE FIX ---
97
+ agg_df['body_dist'] = agg_df['body_list'].apply(
98
+ lambda x: (pd.Series(x).value_counts(normalize=True) * 100).to_dict())
99
+ agg_df['acid_dist'] = agg_df['acidity_list'].apply(
100
+ lambda x: (pd.Series(x).value_counts(normalize=True) * 100).to_dict())
101
+ # --- END OF FIX ---
102
+
103
+ agg_df['pairing_data'] = agg_df['harmonize_list'].apply(get_top_food_pairings)
104
+ agg_df['pairing_emoji'] = agg_df['pairing_data'].apply(lambda x: x['emojis'])
105
+ agg_df['pairing_names'] = agg_df['pairing_data'].apply(lambda x: x['names'])
106
+ agg_df['wine_type_order'] = agg_df['Type'].map(WINE_TYPE_ORDER)
107
+ agg_df = agg_df.sort_values(by=['wine_type_order', 'avg_fullness'], ascending=[False, True])
108
+
109
+ return agg_df
110
+
111
+
112
+ # --- OPTIMIZATION 3: Efficient & Clean Chart Creation ---
113
+ def create_wine_chart(chart_data, regional_grouping):
114
+ """Creates the Plotly figure with optimized traces and layout."""
115
+ if chart_data.empty:
116
+ fig = go.Figure()
117
+ fig.add_annotation(text="No data available with current filters.", xref="paper", yref="paper", x=0.5, y=0.5,
118
+ showarrow=False)
119
+ return fig
120
+
121
+ num_rows = len(chart_data)
122
+
123
+ # Add wine type emoji based on type
124
+ wine_type_emojis = {'Red': '🍷', 'White': '🥂', 'Rosé': '🌸', 'Sparkling': '🍾'}
125
+ chart_data['wine_emoji'] = chart_data['Type'].map(wine_type_emojis).fillna('🍷')
126
+
127
+ if regional_grouping:
128
+ chart_data['flag'] = chart_data['Country'].map(COUNTRY_FLAGS).fillna('🌍')
129
+ chart_data['grape_label'] = chart_data.apply(lambda row: f"{row['wine_emoji']} {row['main_grape']} {row['flag']}", axis=1)
130
+ else:
131
+ chart_data['grape_label'] = chart_data.apply(lambda row: f"{row['wine_emoji']} {row['main_grape']}", axis=1)
132
+
133
+ y_labels = chart_data['grape_label'].tolist()
134
+
135
+ fig = make_subplots(
136
+ rows=1, cols=5,
137
+ specs=[[{}, {"type": "bar"}, {"type": "bar"}, {"type": "box"}, {}]],
138
+ column_widths=[0.30, 0.25, 0.25, 0.15, 0.05],
139
+ horizontal_spacing=0.02,
140
+ shared_yaxes=True
141
+ )
142
+
143
+ hover_texts = chart_data.apply(
144
+ lambda row: (
145
+ f"<b>{row['main_grape']} ({row.get('Country', 'Global') if regional_grouping else 'Global'})</b><br>"
146
+ f"Wineries: {row['winery_count']}<br>"
147
+ f"Regions: {row['region_count']}<br>"
148
+ f"Total Wines: {row['count']:,}"),
149
+ axis=1
150
+ )
151
+ fig.add_trace(go.Bar(
152
+ y=y_labels, x=[1] * num_rows, orientation='h',
153
+ marker_color='rgba(0,0,0,0)', showlegend=False,
154
+ hoverinfo='text', hovertext=hover_texts
155
+ ), row=1, col=1)
156
+ fig.add_trace(go.Scatter(
157
+ y=y_labels, x=[0.03] * num_rows, mode='text',
158
+ text=y_labels, textposition='middle right',
159
+ textfont={'size': 14, 'color': '#2F2F2F'},
160
+ hoverinfo='none', showlegend=False
161
+ ), row=1, col=1)
162
+
163
+ body_colors = {'Very light-bodied': '#FFB6C1', 'Light-bodied': '#CD5C5C', 'Medium-bodied': '#C13636',
164
+ 'Full-bodied': '#8B0000', 'Very full-bodied': '#4B0000'}
165
+ for body_type in BODY_ORDER:
166
+ values = chart_data['body_dist'].apply(lambda d: d.get(body_type, 0))
167
+ fig.add_trace(go.Bar(
168
+ y=y_labels, x=values, name=body_type, orientation='h',
169
+ marker_color=body_colors.get(body_type), showlegend=False,
170
+ hovertemplate=f"{body_type}: %{{x:.1f}}%<extra></extra>"
171
+ ), row=1, col=2)
172
+
173
+ acid_colors = {'Low': '#F5F5DC', 'Medium': '#DAA520', 'High': '#B8860B'}
174
+ for acid_type in ACIDITY_ORDER:
175
+ values = chart_data['acid_dist'].apply(lambda d: d.get(acid_type, 0))
176
+ fig.add_trace(go.Bar(
177
+ y=y_labels, x=values, name=acid_type, orientation='h',
178
+ marker_color=acid_colors.get(acid_type), showlegend=False,
179
+ hovertemplate=f"{acid_type} acidity: %{{x:.1f}}%<extra></extra>"
180
+ ), row=1, col=3)
181
+
182
+ # Color box plots by wine type
183
+ box_colors = {'Red': '#8B0000', 'White': '#DAA520', 'Rosé': '#CD5C5C', 'Sparkling': '#9370DB'}
184
+ for idx, (i, row) in enumerate(chart_data.iterrows()):
185
+ abv_values = row['abv_list']
186
+ color = box_colors.get(row['Type'], '#6A5ACD')
187
+ fig.add_trace(go.Box(
188
+ y=[y_labels[idx]] * len(abv_values), x=abv_values, name=row['Type'], orientation='h',
189
+ showlegend=False, marker_color=color, line_color=color,
190
+ hovertemplate=f"ABV: %{{x:.1f}}%<extra></extra>"
191
+ ), row=1, col=4)
192
+
193
+ fig.add_trace(go.Scatter(
194
+ y=y_labels, x=[0.5] * num_rows, mode='text',
195
+ text=chart_data['pairing_emoji'], textposition='middle center',
196
+ textfont={'size': 22}, showlegend=False,
197
+ hoverinfo='text', hovertext=chart_data['pairing_names']
198
+ ), row=1, col=5)
199
+
200
+ fig.update_layout(
201
+ title={
202
+ 'text': "Wine Characteristics by Grape Variety",
203
+ 'x': 0.5,
204
+ 'font': {'size': 24, 'color': '#2F2F2F'}
205
+ },
206
+ height=max(500, num_rows * 40),
207
+ barmode='stack', showlegend=False, plot_bgcolor='#FFFFFF', paper_bgcolor='#F5F5F5',
208
+ margin=dict(l=10, r=10, t=120, b=20), boxgap=0.5, bargap=0.4
209
+ )
210
+
211
+ column_titles = ["Wine / Hover for Info", "Body Profile (%)", "Acidity Profile (%)", "Alcohol (ABV %)", "Food Pairing"]
212
+ for i, title in enumerate(column_titles, 1):
213
+ domain = fig.layout[f'xaxis{i if i > 1 else ""}'].domain
214
+ fig.add_annotation(
215
+ x=(domain[0] + domain[1]) / 2, y=1.05,
216
+ xref="paper", yref="paper", text=f"<b>{title}</b>",
217
+ xanchor='center', showarrow=False, font={'size': 14, 'color': '#2F2F2F'}
218
+ )
219
+
220
+ for i in range(1, 6):
221
+ fig.update_yaxes(showticklabels=False, showgrid=False, zeroline=False, row=1, col=i)
222
+ fig.update_xaxes(showticklabels=False, showgrid=False, zeroline=False, title_text="", row=1, col=i)
223
+
224
+ fig.update_yaxes(categoryorder="array", categoryarray=y_labels, autorange=False, range=[-0.5, num_rows - 0.5],
225
+ row=1, col=1)
226
+
227
+ for i in range(num_rows):
228
+ if i % 2 == 1:
229
+ fig.add_hrect(y0=i - 0.5, y1=i + 0.5, fillcolor="#F0F0F0", layer="below", line_width=0, row=1, col="all")
230
+
231
+ return fig
232
+
233
+
234
+ # --- Gradio Interface Logic ---
235
+ def update_dashboard(wine_types, max_grape_count, min_samples_choice, regional_grouping,
236
+ progress=gr.Progress(track_tqdm=True)):
237
+ """Main function to update dashboard."""
238
+ progress(0, desc="Loading and processing data...")
239
+ df = load_and_preprocess_data()
240
+
241
+ progress(0.5, desc="Filtering and aggregating...")
242
+ chart_data = aggregate_wine_data(df, wine_types, max_grape_count, min_samples_choice, regional_grouping)
243
+
244
+ progress(0.8, desc="Creating chart...")
245
+ fig = create_wine_chart(chart_data, regional_grouping)
246
+
247
+ total_combinations = len(chart_data)
248
+ total_wines = chart_data['count'].sum() if not chart_data.empty else 0
249
+ min_samples = SAMPLE_THRESHOLDS[min_samples_choice]
250
+ grouping_type = "grape+region" if regional_grouping else "grape+type"
251
+ summary = f"📊 Showing **{total_combinations}** {grouping_type} combinations from **{total_wines:,}** wines (min {min_samples} samples each)"
252
+
253
+ return fig, summary
254
+
255
+
256
+ # Create Gradio interface
257
+ def create_interface():
258
+ with gr.Blocks(title="Wine Analysis Dashboard", theme=gr.themes.Soft()) as demo:
259
+ gr.Markdown("# 🍷 Wine Characteristics Dashboard")
260
+
261
+ with gr.Row():
262
+ wine_type_filter = gr.CheckboxGroup(
263
+ choices=['Red', 'White', 'Rosé', 'Sparkling', 'Dessert', 'Dessert/Port'],
264
+ value=['Red'], label="🍷 Wine Types"
265
+ )
266
+ max_grape_slider = gr.Slider(
267
+ minimum=1, maximum=5, step=1, value=1, label="🍇 Max Grapes per Wine",
268
+ info="1: Varietals, 5: All Blends"
269
+ )
270
+ min_samples_choice = gr.Radio(
271
+ choices=list(SAMPLE_THRESHOLDS.keys()), value='Common (100+)',
272
+ label="Minimum Sample Size"
273
+ )
274
+ regional_grouping = gr.Checkbox(
275
+ value=True, label="Split By Country"
276
+ )
277
+
278
+ summary_text = gr.Markdown()
279
+ wine_plot = gr.Plot()
280
+
281
+ inputs = [wine_type_filter, max_grape_slider, min_samples_choice, regional_grouping]
282
+ outputs = [wine_plot, summary_text]
283
+
284
+ # Auto-update when any input changes
285
+ for input_component in inputs:
286
+ input_component.change(update_dashboard, inputs=inputs, outputs=outputs)
287
+
288
+ demo.load(fn=update_dashboard, inputs=inputs, outputs=outputs)
289
+
290
+ return demo
291
+
292
+
293
+ if __name__ == "__main__":
294
+ app_interface = create_interface()
295
+ app_interface.launch()