Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
256 lines (216 loc) Β· 9.98 KB
/
Copy pathapp.py
File metadata and controls
256 lines (216 loc) Β· 9.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
import streamlit as st
import pandas as pd
import os
from dashboard.viz import (
load_distance_matrix,
load_preprocessed_numeric,
compute_pca_embedding,
plot_embedding_2d,
plot_embedding_3d,
plot_distance_histogram,
plot_neighbor_bar,
get_available_numeric_features,
compute_raw_feature_view,
plot_raw_feature_scatter_2d,
)
st.set_page_config(page_title="Strategic Product Optimizer", layout="wide")
st.title("Product Portfolio Analysis")
# Sidebar for category selection
category = st.sidebar.selectbox(
"Select Product Category",
["Smartphones", "Tractors", "User Upload"]
)
folder_name = category.lower().replace(" ", "_")
base_path = os.path.join("outputs", folder_name)
nn_path = os.path.join(base_path, "nearest_neighbors.csv")
summary_path = os.path.join(base_path, "distance_summary.txt")
dm_path = os.path.join(base_path, "distance_matrix.csv")
pre_path = os.path.join(base_path, "preprocessed_output.csv")
# --- DISPLAY SUMMARY STATS ---
if os.path.exists(summary_path):
with open(summary_path, 'r') as f:
summary_text = f.read()
st.sidebar.info(f"**Category Summary:**\n\n{summary_text}")
# --- PRE-LOAD NUMERIC DATA (needed for feature comparison + PCA) ---
pre = None
dm = None
if os.path.exists(pre_path):
pre = load_preprocessed_numeric(pre_path)
if os.path.exists(dm_path):
dm = load_distance_matrix(dm_path)
if pre is not None and dm is not None and len(pre) == len(dm):
pre.index = dm.index
# --- MAIN ANALYSIS ---
if os.path.exists(nn_path):
nn_df = pd.read_csv(nn_path)
st.session_state['df'] = nn_df
if dm is not None:
st.session_state['dist_matrix'] = dm.values
# Selection UI
product_list = sorted(nn_df.iloc[:, 0].unique())
selected_product = st.selectbox("Select a Product to find neighbors", product_list)
# Filter Results
results = nn_df[nn_df.iloc[:, 0] == selected_product].sort_values('distance')
# Layout with Columns
col1, col2 = st.columns([2, 1])
with col1:
st.subheader(f"Top Nearest Neighbors for {selected_product}")
display_results = results.iloc[:, [2, -1]].head(5)
display_results.columns = ['Neighbor Name', 'Distance Score']
st.table(display_results)
with col2:
st.subheader("Market Impact Analysis")
top_neighbor_dist = results['distance'].iloc[0]
closest_neighbor_name = results.iloc[0, 2]
# Compare specs feature-by-feature to explain WHY
diffs = None
if pre is not None and selected_product in pre.index and closest_neighbor_name in pre.index:
p1_specs = pre.loc[selected_product]
p2_specs = pre.loc[closest_neighbor_name]
diffs = (p1_specs - p2_specs).abs().sort_values()
# Display Alerts with Strategic Business Insights
if top_neighbor_dist < 0.05:
st.error(f"π΄ Cannibalization Risk (Dist: {top_neighbor_dist:.4f})")
st.markdown(
f"**{selected_product}** and **{closest_neighbor_name}** are near-identical in the market. "
"Keeping both creates internal competition and dilutes market share."
)
if diffs is not None:
overlap_features = ", ".join(diffs.index[:5])
st.markdown(f"**Overlapping specs:** {overlap_features}")
st.markdown("**π‘ Recommendation:** Consider rationalizing/removing one product from the portfolio. "
"Consolidating into a single SKU can reduce costs and strengthen positioning.")
elif top_neighbor_dist < 0.50:
st.warning(f"π‘ Significant Overlap (Dist: {top_neighbor_dist:.4f})")
st.markdown(
f"**{selected_product}** competes closely with **{closest_neighbor_name}**. "
"Consumers may struggle to distinguish between the two."
)
if diffs is not None:
shared_list = diffs.index[:5].tolist()
shared_features = ", ".join(shared_list)
diff_candidates = [f for f in diffs.tail(5).index[::-1] if f not in shared_list]
diff_features = ", ".join(diff_candidates[:3])
st.markdown(f"**Shared ground:** {shared_features}")
st.markdown(f"**Key differentiators to leverage:** {diff_features}")
st.markdown("**π‘ Recommendation:** Sharpen the value proposition β emphasize the differentiating "
"features in marketing to carve out distinct customer segments.")
else:
st.success(f"π’ Unique Positioning (Dist: {top_neighbor_dist:.4f})")
st.markdown(
f"**{selected_product}** is well-differentiated from its nearest competitor "
f"**{closest_neighbor_name}**. It occupies a distinct market position."
)
if diffs is not None:
strengths = ", ".join(diffs.tail(5).index[::-1])
st.markdown(f"**Competitive advantages:** {strengths}")
st.markdown("**π‘ Recommendation:** Invest in distribution and marketing to maximize reach. "
"This product fills a unique gap in the portfolio β protect and develop it.")
st.caption("Alert Logic: π΄ Cannibalization < 0.05 | π‘ Overlap < 0.50 | π’ Unique > 0.50")
# --- Task 2 Visualizations ---
if dm is not None and pre is not None:
st.subheader("Visual Analytics")
# 1) Neighbor bar
topk = results.sort_values("distance").head(10)
nb_df = topk.iloc[:, [2, -1]].copy()
nb_df.columns = ["neighbor", "distance"]
st.plotly_chart(plot_neighbor_bar(nb_df, selected_product), use_container_width=True)
# 2) Distance histogram
st.plotly_chart(plot_distance_histogram(dm), use_container_width=True)
# 3) Feature selector + 2D / 3D PCA
st.markdown("### PCA Feature Selection")
available_features = get_available_numeric_features(pre_path)
selected_features = st.multiselect(
"Select numeric features to include in PCA",
options=available_features,
default=available_features,
key=f"pca_features_{folder_name}"
)
if len(selected_features) < 2:
st.warning("Please select at least 2 numeric features to generate a 2D PCA view.")
else:
pre_pca = load_preprocessed_numeric(pre_path, selected_features=selected_features)
if len(pre_pca) == len(dm):
pre_pca.index = dm.index
# 2D PCA
emb_df_2d = compute_pca_embedding(pre_pca, list(dm.index), n_components=2)
st.plotly_chart(
plot_embedding_2d(
emb_df_2d,
selected=selected_product,
neighbors=nb_df["neighbor"].tolist()
),
use_container_width=True
)
# 3D PCA
st.markdown("### 3D PCA View")
if len(selected_features) >= 3:
emb_df_3d = compute_pca_embedding(pre_pca, list(dm.index), n_components=3)
st.plotly_chart(
plot_embedding_3d(
emb_df_3d,
selected=selected_product,
neighbors=nb_df["neighbor"].tolist()
),
use_container_width=True
)
else:
st.info("Select at least 3 numeric features to display the 3D PCA view.")
# 4) Raw feature scatter with user-defined axes
st.markdown("### 2D Raw Feature Comparison")
raw_x = st.selectbox(
"Select x-axis feature",
options=available_features,
index=0,
key=f"raw_x_{folder_name}"
)
default_y_index = 1 if len(available_features) > 1 else 0
raw_y = st.selectbox(
"Select y-axis feature",
options=available_features,
index=default_y_index,
key=f"raw_y_{folder_name}"
)
if raw_x == raw_y:
st.info("Please choose two different features for the raw 2D scatter.")
else:
raw_df = compute_raw_feature_view(
pre,
list(dm.index),
x_feature=raw_x,
y_feature=raw_y,
)
st.plotly_chart(
plot_raw_feature_scatter_2d(
raw_df,
selected=selected_product,
neighbors=nb_df["neighbor"].tolist()
),
use_container_width=True
)
else:
st.warning("Missing distance_matrix.csv or preprocessed_output.csv for visualizations.")
else:
st.error(f"No analysis data found for {category}. Please ensure the distance matrix script has been run.")
# --- Task 3: Product Removal Simulation ---
st.markdown("---")
st.subheader("Simulation: Product Removal")
from dashboard.simulation import simulate_removal
if os.path.exists(nn_path):
products_to_remove = st.multiselect("Select Products to Remove", product_list)
threshold = st.number_input("Optional Gap Threshold", min_value=0.0, value=0.0)
if st.button("Simulate Removal"):
if dm is not None:
result = simulate_removal(dm, st.session_state['dist_matrix'], products_to_remove, threshold)
if 'error' in result:
st.error(result['error'])
else:
st.write(f"Mean Substitute Distance: {result['mean_dist']:.2f}")
st.write(f"Max Substitute Distance: {result['max_dist']:.2f}")
st.subheader("Substitutes")
for removed, info in result['substitutes'].items():
st.write(f"- Removed {removed} β Substitute {info['substitute']} (dist {info['distance']:.2f})")
if threshold:
st.write("Gaps:", ", ".join(result.get('gaps', [])))
else:
st.error("Distance matrix not available. Cannot run simulation.")