Spaces:
Sleeping
Sleeping
add top products section
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import zipfile
|
| 2 |
from pathlib import Path
|
| 3 |
|
|
@@ -19,6 +20,21 @@ SMA_COLOR = "#ff4b4b"
|
|
| 19 |
SMA_LINE_WIDTH = 4
|
| 20 |
TOOLTIP_FONT_SIZE = 16
|
| 21 |
TARGET_X_TICKS = 24
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
@st.cache_data
|
|
@@ -85,9 +101,9 @@ items_slot = c4.empty()
|
|
| 85 |
|
| 86 |
chart_slot = st.empty()
|
| 87 |
|
| 88 |
-
month_options = [d.strftime(
|
| 89 |
-
default_start_month = pd.Timestamp(default_start).to_period("M").strftime(
|
| 90 |
-
default_end_month = pd.Timestamp(max_date).to_period("M").strftime(
|
| 91 |
|
| 92 |
start_label, end_label = st.select_slider(
|
| 93 |
"Date range",
|
|
@@ -124,14 +140,14 @@ sma_v = sma.loc[(sma.index >= start_month) & (sma.index <= end_month)]
|
|
| 124 |
# categorical — otherwise the 28–31-day variation between month-starts shows
|
| 125 |
# up as uneven gaps on a continuous datetime axis.
|
| 126 |
monthly = full_net.loc[start_month:end_month].rename_axis("Month").reset_index(name="Net")
|
| 127 |
-
monthly["Label"] = monthly["Month"].dt.strftime(
|
| 128 |
sma_df = sma_v.rename_axis("Month").reset_index(name="Avg")
|
| 129 |
-
sma_df["Label"] = sma_df["Month"].dt.strftime(
|
| 130 |
|
| 131 |
fig = px.bar(monthly, x="Label", y="Net", custom_data=["Month"])
|
| 132 |
fig.update_traces(
|
| 133 |
marker_color=BAR_COLOR,
|
| 134 |
-
hovertemplate="<b>$%{y:,.2f}</b><br>%{customdata[0]|
|
| 135 |
)
|
| 136 |
fig.add_scatter(
|
| 137 |
x=sma_df["Label"],
|
|
@@ -141,7 +157,7 @@ fig.add_scatter(
|
|
| 141 |
line=dict(color=SMA_COLOR, width=SMA_LINE_WIDTH),
|
| 142 |
customdata=sma_df[["Month"]],
|
| 143 |
hovertemplate=(
|
| 144 |
-
f"<b>$%{{y:,.2f}}</b><br>%{{customdata[0]|
|
| 145 |
f"{SMA_WINDOW_MONTHS}-mo average<extra></extra>"
|
| 146 |
),
|
| 147 |
)
|
|
@@ -151,7 +167,7 @@ tick_angle = -45 if len(tick_labels) > 12 else 0
|
|
| 151 |
fig.update_layout(
|
| 152 |
xaxis_title=None,
|
| 153 |
yaxis_title="USD",
|
| 154 |
-
margin=
|
| 155 |
showlegend=False,
|
| 156 |
hoverlabel=dict(font_size=TOOLTIP_FONT_SIZE),
|
| 157 |
)
|
|
@@ -161,3 +177,48 @@ fig.update_xaxes(
|
|
| 161 |
tickangle=tick_angle,
|
| 162 |
)
|
| 163 |
chart_slot.plotly_chart(fig, width="stretch")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import textwrap
|
| 2 |
import zipfile
|
| 3 |
from pathlib import Path
|
| 4 |
|
|
|
|
| 20 |
SMA_LINE_WIDTH = 4
|
| 21 |
TOOLTIP_FONT_SIZE = 16
|
| 22 |
TARGET_X_TICKS = 24
|
| 23 |
+
CHART_MARGIN = dict(l=0, r=0, t=10, b=0)
|
| 24 |
+
MONTH_KEY_FORMAT = "%Y-%m"
|
| 25 |
+
MONTH_LABEL_FORMAT = "%b %Y"
|
| 26 |
+
MONTH_FULL_FORMAT = "%B %Y"
|
| 27 |
+
DATE_LONG_FORMAT = "%d %B %Y"
|
| 28 |
+
|
| 29 |
+
TOP_N_PRODUCTS = 20
|
| 30 |
+
PRODUCT_LABEL_MAX = 32
|
| 31 |
+
PRODUCT_WRAP_WIDTH = 64
|
| 32 |
+
PRODUCT_ROW_HEIGHT = 24
|
| 33 |
+
PRODUCT_CHART_PAD = 60
|
| 34 |
+
PRODUCT_BAR_COLOR = "#a78bfa"
|
| 35 |
+
PRODUCT_BAR_GAP = 0.6
|
| 36 |
+
PRODUCT_LABEL_FONT_SIZE = 14
|
| 37 |
+
PRODUCT_TOOLTIP_FONT_SIZE = 16
|
| 38 |
|
| 39 |
|
| 40 |
@st.cache_data
|
|
|
|
| 101 |
|
| 102 |
chart_slot = st.empty()
|
| 103 |
|
| 104 |
+
month_options = [d.strftime(MONTH_KEY_FORMAT) for d in full_idx]
|
| 105 |
+
default_start_month = pd.Timestamp(default_start).to_period("M").strftime(MONTH_KEY_FORMAT)
|
| 106 |
+
default_end_month = pd.Timestamp(max_date).to_period("M").strftime(MONTH_KEY_FORMAT)
|
| 107 |
|
| 108 |
start_label, end_label = st.select_slider(
|
| 109 |
"Date range",
|
|
|
|
| 140 |
# categorical — otherwise the 28–31-day variation between month-starts shows
|
| 141 |
# up as uneven gaps on a continuous datetime axis.
|
| 142 |
monthly = full_net.loc[start_month:end_month].rename_axis("Month").reset_index(name="Net")
|
| 143 |
+
monthly["Label"] = monthly["Month"].dt.strftime(MONTH_LABEL_FORMAT)
|
| 144 |
sma_df = sma_v.rename_axis("Month").reset_index(name="Avg")
|
| 145 |
+
sma_df["Label"] = sma_df["Month"].dt.strftime(MONTH_LABEL_FORMAT)
|
| 146 |
|
| 147 |
fig = px.bar(monthly, x="Label", y="Net", custom_data=["Month"])
|
| 148 |
fig.update_traces(
|
| 149 |
marker_color=BAR_COLOR,
|
| 150 |
+
hovertemplate=(f"<b>$%{{y:,.2f}}</b><br>%{{customdata[0]|{MONTH_FULL_FORMAT}}}<extra></extra>"),
|
| 151 |
)
|
| 152 |
fig.add_scatter(
|
| 153 |
x=sma_df["Label"],
|
|
|
|
| 157 |
line=dict(color=SMA_COLOR, width=SMA_LINE_WIDTH),
|
| 158 |
customdata=sma_df[["Month"]],
|
| 159 |
hovertemplate=(
|
| 160 |
+
f"<b>$%{{y:,.2f}}</b><br>%{{customdata[0]|{MONTH_FULL_FORMAT}}}<br>"
|
| 161 |
f"{SMA_WINDOW_MONTHS}-mo average<extra></extra>"
|
| 162 |
),
|
| 163 |
)
|
|
|
|
| 167 |
fig.update_layout(
|
| 168 |
xaxis_title=None,
|
| 169 |
yaxis_title="USD",
|
| 170 |
+
margin=CHART_MARGIN,
|
| 171 |
showlegend=False,
|
| 172 |
hoverlabel=dict(font_size=TOOLTIP_FONT_SIZE),
|
| 173 |
)
|
|
|
|
| 177 |
tickangle=tick_angle,
|
| 178 |
)
|
| 179 |
chart_slot.plotly_chart(fig, width="stretch")
|
| 180 |
+
|
| 181 |
+
# Top products: gross spend per ASIN over the selected range. Refunds aren't
|
| 182 |
+
# netted here because Refund Details.csv has Order ID but no ASIN, so refunds
|
| 183 |
+
# on multi-item orders can't be cleanly attributed to a product.
|
| 184 |
+
with st.expander("Top products", expanded=False):
|
| 185 |
+
top = (
|
| 186 |
+
orders_v.groupby("ASIN", as_index=False)
|
| 187 |
+
.agg(
|
| 188 |
+
Spent=("Total Amount", "sum"),
|
| 189 |
+
Product=("Product Name", "last"),
|
| 190 |
+
LastDate=("Order Date", "max"),
|
| 191 |
+
)
|
| 192 |
+
.nlargest(TOP_N_PRODUCTS, "Spent")
|
| 193 |
+
.iloc[::-1]
|
| 194 |
+
)
|
| 195 |
+
top["Label"] = top["Product"].where(
|
| 196 |
+
top["Product"].str.len() <= PRODUCT_LABEL_MAX,
|
| 197 |
+
top["Product"].str.slice(0, PRODUCT_LABEL_MAX - 1) + "…",
|
| 198 |
+
)
|
| 199 |
+
top["Wrapped"] = top["Product"].apply(
|
| 200 |
+
lambda p: textwrap.fill(p, width=PRODUCT_WRAP_WIDTH).replace("\n", "<br>")
|
| 201 |
+
)
|
| 202 |
+
top["DateFmt"] = top["LastDate"].dt.strftime(DATE_LONG_FORMAT)
|
| 203 |
+
top_fig = px.bar(top, x="Spent", y="ASIN", orientation="h", custom_data=["Wrapped", "DateFmt"])
|
| 204 |
+
top_fig.update_traces(
|
| 205 |
+
marker_color=PRODUCT_BAR_COLOR,
|
| 206 |
+
hovertemplate=(
|
| 207 |
+
"<b>$%{x:,.2f}</b>" "<br><i>%{customdata[1]}</i>" "<br>%{customdata[0]}<extra></extra>"
|
| 208 |
+
),
|
| 209 |
+
)
|
| 210 |
+
top_fig.update_layout(
|
| 211 |
+
xaxis_title="USD",
|
| 212 |
+
yaxis_title=None,
|
| 213 |
+
margin=CHART_MARGIN,
|
| 214 |
+
hoverlabel=dict(font_size=PRODUCT_TOOLTIP_FONT_SIZE, align="left"),
|
| 215 |
+
height=len(top) * PRODUCT_ROW_HEIGHT + PRODUCT_CHART_PAD,
|
| 216 |
+
bargap=PRODUCT_BAR_GAP,
|
| 217 |
+
)
|
| 218 |
+
top_fig.update_yaxes(
|
| 219 |
+
tickmode="array",
|
| 220 |
+
tickvals=top["ASIN"],
|
| 221 |
+
ticktext=top["Label"],
|
| 222 |
+
tickfont=dict(size=PRODUCT_LABEL_FONT_SIZE),
|
| 223 |
+
)
|
| 224 |
+
st.plotly_chart(top_fig, width="stretch")
|