Skip to content

Commit b7b3ed5

Browse files
authored
Merge pull request #51 from advanced-computing/change1
changes to the website
2 parents 1fe9d34 + 5e33f5b commit b7b3ed5

3 files changed

Lines changed: 732 additions & 100 deletions

File tree

Homepage.py

Lines changed: 222 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,25 @@
1111
start_time = time.time()
1212

1313
st.set_page_config(page_title="Weekly U.S. Petroleum Supply", layout="wide")
14-
st.title("U.S. Petroleum & WTI Weekly Monitor")
14+
15+
# =========================
16+
# Sidebar title
17+
# =========================
18+
st.sidebar.markdown(
19+
"""
20+
<h1 style="font-size: 1.5rem; line-height: 1.2; margin-bottom: 0.2rem;">
21+
U.S. Petroleum & WTI Weekly Monitor
22+
</h1>
23+
""",
24+
unsafe_allow_html=True,
25+
)
26+
st.sidebar.caption("Source: EIA")
27+
st.sidebar.divider()
28+
29+
# =========================
30+
# Main page header
31+
# =========================
32+
st.title("Weekly U.S. Petroleum Supply")
1533
st.subheader("Team Members: Irina, Indra")
1634
st.caption("Source: U.S. Energy Information Administration (EIA)")
1735

@@ -79,7 +97,13 @@
7997
PROJECT_ID = "sipa-adv-c-giggling-wombat"
8098
TOTAL_SUPPLY_TABLE_ID = f"{PROJECT_ID}.petroleum_supply.weekly_supply"
8199
PRODUCT_SUPPLY_TABLE_ID = f"{PROJECT_ID}.petroleum_supply.weekly_supply_by_product"
100+
WTI_TABLE_ID = f"{PROJECT_ID}.petroleum_supply.weekly_wti"
101+
82102
DEFAULT_PRODUCT_COUNT = 3
103+
MIN_CORRELATION_POINTS = 12
104+
TOP_ANALYSIS_COUNT = 10
105+
TWO_COLUMN_LAYOUT = 2
106+
THREE_COLUMN_LAYOUT = 3
83107

84108

85109
@st.cache_resource
@@ -101,7 +125,7 @@ def load_supply_data() -> pd.DataFrame:
101125
FROM `{TOTAL_SUPPLY_TABLE_ID}`
102126
ORDER BY week
103127
"""
104-
df = client.query(query).to_dataframe()
128+
df = client.query(query).to_dataframe(create_bqstorage_client=False)
105129
df["week"] = pd.to_datetime(df["week"])
106130
df["total_supply"] = pd.to_numeric(df["total_supply"], errors="coerce")
107131
df = df.dropna(subset=["week", "total_supply"])
@@ -116,13 +140,84 @@ def load_supply_product_data() -> pd.DataFrame:
116140
FROM `{PRODUCT_SUPPLY_TABLE_ID}`
117141
ORDER BY week
118142
"""
119-
df = client.query(query).to_dataframe()
143+
df = client.query(query).to_dataframe(create_bqstorage_client=False)
120144
df["week"] = pd.to_datetime(df["week"])
121145
df["product_supplied"] = pd.to_numeric(df["product_supplied"], errors="coerce")
122146
df = df.dropna(subset=["week", "product", "product_name", "product_supplied"])
123147
return df
124148

125149

150+
@st.cache_data(ttl=60 * 60)
151+
def load_wti_data() -> pd.DataFrame:
152+
client = get_bq_client()
153+
query = f"""
154+
SELECT week, wti_price
155+
FROM `{WTI_TABLE_ID}`
156+
ORDER BY week
157+
"""
158+
df = client.query(query).to_dataframe(create_bqstorage_client=False)
159+
df["week"] = pd.to_datetime(df["week"])
160+
df["wti_price"] = pd.to_numeric(df["wti_price"], errors="coerce")
161+
df = df.dropna(subset=["week", "wti_price"])
162+
return df
163+
164+
165+
def compute_product_price_sensitivity(
166+
product_df: pd.DataFrame,
167+
wti_df: pd.DataFrame,
168+
) -> pd.DataFrame:
169+
merged = product_df.merge(wti_df, on="week", how="inner")
170+
171+
if merged.empty:
172+
return pd.DataFrame()
173+
174+
results = []
175+
176+
grouped = merged.groupby(["product", "product_name"], dropna=False)
177+
178+
for (product_code, product_name), group in grouped:
179+
group = group.dropna(subset=["product_supplied", "wti_price"]).copy()
180+
181+
if len(group) < MIN_CORRELATION_POINTS:
182+
continue
183+
184+
if group["product_supplied"].nunique() < TWO_COLUMN_LAYOUT:
185+
continue
186+
187+
if group["wti_price"].nunique() < TWO_COLUMN_LAYOUT:
188+
continue
189+
190+
correlation = group["product_supplied"].corr(group["wti_price"])
191+
192+
if pd.isna(correlation):
193+
continue
194+
195+
direction = "Positive" if correlation >= 0 else "Negative"
196+
197+
results.append(
198+
{
199+
"product": product_code,
200+
"product_name": product_name,
201+
"correlation_with_wti": correlation,
202+
"abs_correlation": abs(correlation),
203+
"direction": direction,
204+
"weeks_used": len(group),
205+
}
206+
)
207+
208+
if not results:
209+
return pd.DataFrame()
210+
211+
result_df = (
212+
pd.DataFrame(results).sort_values("abs_correlation", ascending=False).reset_index(drop=True)
213+
)
214+
215+
return result_df
216+
217+
218+
# =========================
219+
# Load data
220+
# =========================
126221
try:
127222
weekly_total = load_supply_data()
128223
except Exception as e:
@@ -133,8 +228,28 @@ def load_supply_product_data() -> pd.DataFrame:
133228
st.error("No supply data found in BigQuery.")
134229
st.stop()
135230

231+
try:
232+
weekly_by_product = load_supply_product_data()
233+
except Exception as e:
234+
st.error(f"Failed to load product-level supply data from BigQuery: {e}")
235+
st.stop()
236+
237+
try:
238+
weekly_wti = load_wti_data()
239+
except Exception as e:
240+
st.error(f"Failed to load WTI data from BigQuery: {e}")
241+
st.stop()
242+
243+
if weekly_by_product.empty:
244+
st.error("No product-level supply data found in BigQuery.")
245+
st.stop()
246+
247+
if weekly_wti.empty:
248+
st.error("No WTI data found in BigQuery.")
249+
st.stop()
250+
136251
# =========================
137-
# Interactive Filters
252+
# Sidebar Filters
138253
# =========================
139254
st.sidebar.header("Filters")
140255

@@ -170,17 +285,16 @@ def load_supply_product_data() -> pd.DataFrame:
170285
st.warning("No data available for the selected date range.")
171286
st.stop()
172287

173-
try:
174-
weekly_by_product = load_supply_product_data()
175-
except Exception as e:
176-
st.error(f"Failed to load product-level supply data from BigQuery: {e}")
177-
st.stop()
178-
179288
filtered_product = weekly_by_product[
180289
(weekly_by_product["week"] >= pd.to_datetime(start_week))
181290
& (weekly_by_product["week"] <= pd.to_datetime(end_week))
182291
].copy()
183292

293+
filtered_wti = weekly_wti[
294+
(weekly_wti["week"] >= pd.to_datetime(start_week))
295+
& (weekly_wti["week"] <= pd.to_datetime(end_week))
296+
].copy()
297+
184298
product_options = sorted(filtered_product["product_name"].dropna().unique().tolist())
185299

186300
selected_products = st.sidebar.multiselect(
@@ -203,57 +317,121 @@ def load_supply_product_data() -> pd.DataFrame:
203317
except Exception:
204318
latest_total = None
205319

206-
c1, c2 = st.columns(2)
320+
c1, c2 = st.columns(TWO_COLUMN_LAYOUT)
207321
c1.metric("Weeks in selected range", f"{filtered_total.shape[0]:,}")
208322
c2.metric(
209323
"Latest total (sum of products)",
210324
f"{latest_total:,.0f}" if latest_total is not None else "—",
211325
)
212326

213-
st.divider()
214-
st.subheader("Total Product Supplied (Weekly, All Products Summed)")
215-
216-
fig, ax = plt.subplots(figsize=(8, 4))
217-
ax.plot(filtered_total["week"], filtered_total["total_supply"])
218-
ax.set_xlabel("Week")
219-
ax.set_ylabel("Total Product Supplied")
220-
st.pyplot(fig)
221-
222-
with st.expander("Show total supply data table"):
223-
st.dataframe(
224-
filtered_total.sort_values("week", ascending=False),
225-
use_container_width=True,
226-
)
227-
228327
st.caption(
229328
"Note: 'Product supplied' is often used as a proxy for consumption. "
230-
"This visualization is descriptive (not causal)."
329+
"This dashboard is descriptive rather than causal."
231330
)
232331

233332
st.divider()
234-
st.subheader("Product-Level Weekly Supply")
235333

236-
if not selected_products:
237-
st.warning("Please select at least one product from the sidebar.")
334+
# =========================
335+
# Two side-by-side charts
336+
# =========================
337+
left_col, right_col = st.columns(TWO_COLUMN_LAYOUT)
338+
339+
with left_col:
340+
st.subheader("Total Product Supplied")
341+
342+
fig, ax = plt.subplots(figsize=(7, 4))
343+
ax.plot(filtered_total["week"], filtered_total["total_supply"])
344+
ax.set_xlabel("Week")
345+
ax.set_ylabel("Total Product Supplied")
346+
st.pyplot(fig)
347+
348+
with st.expander("Show total supply data table"):
349+
total_display = filtered_total.sort_values("week", ascending=False).copy()
350+
total_display["week"] = total_display["week"].dt.strftime("%Y-%m-%d")
351+
st.dataframe(total_display, width="stretch")
352+
353+
with right_col:
354+
st.subheader("Product-Level Weekly Supply")
355+
356+
if not selected_products:
357+
st.warning("Please select at least one product from the sidebar.")
358+
else:
359+
product_plot_df = filtered_product[
360+
filtered_product["product_name"].isin(selected_products)
361+
].copy()
362+
363+
fig2, ax2 = plt.subplots(figsize=(7, 4))
364+
for product_name in selected_products:
365+
temp = product_plot_df[product_plot_df["product_name"] == product_name]
366+
ax2.plot(temp["week"], temp["product_supplied"], label=product_name)
367+
368+
ax2.set_xlabel("Week")
369+
ax2.set_ylabel("Product Supplied")
370+
ax2.legend()
371+
st.pyplot(fig2)
372+
373+
with st.expander("Show product-level data table"):
374+
product_display = product_plot_df.sort_values(
375+
["product_name", "week"], ascending=[True, False]
376+
).copy()
377+
product_display["week"] = product_display["week"].dt.strftime("%Y-%m-%d")
378+
st.dataframe(product_display, width="stretch")
379+
380+
st.divider()
381+
382+
# =========================
383+
# Product sensitivity section
384+
# =========================
385+
st.subheader("Product Sensitivity to WTI Price")
386+
387+
sensitivity_df = compute_product_price_sensitivity(filtered_product, filtered_wti)
388+
389+
if sensitivity_df.empty:
390+
st.warning("Not enough overlapping weekly data to evaluate product sensitivity to WTI price.")
238391
else:
239-
product_plot_df = filtered_product[
240-
filtered_product["product_name"].isin(selected_products)
241-
].copy()
392+
top_product = sensitivity_df.iloc[0]
242393

243-
fig2, ax2 = plt.subplots(figsize=(8, 4))
244-
for product_name in selected_products:
245-
temp = product_plot_df[product_plot_df["product_name"] == product_name]
246-
ax2.plot(temp["week"], temp["product_supplied"], label=product_name)
394+
m1, m2, m3 = st.columns(THREE_COLUMN_LAYOUT)
395+
m1.metric("Most price-sensitive product", top_product["product_name"])
396+
m2.metric(
397+
"Correlation with WTI",
398+
f"{top_product['correlation_with_wti']:.2f}",
399+
)
400+
m3.metric("Weeks used", f"{int(top_product['weeks_used'])}")
247401

248-
ax2.set_xlabel("Week")
249-
ax2.set_ylabel("Product Supplied")
250-
ax2.legend()
251-
st.pyplot(fig2)
402+
st.caption(
403+
"Products are ranked by the absolute correlation between weekly product "
404+
"supplied and weekly WTI price over the selected date range. This is a "
405+
"descriptive measure, not a causal estimate."
406+
)
407+
408+
chart_df = sensitivity_df.head(TOP_ANALYSIS_COUNT).sort_values(
409+
"abs_correlation", ascending=True
410+
)
411+
412+
fig3, ax3 = plt.subplots(figsize=(6, 3.5))
413+
ax3.barh(
414+
chart_df["product_name"],
415+
chart_df["abs_correlation"],
416+
color="darkorange",
417+
)
418+
ax3.set_xlabel("Absolute correlation with WTI price")
419+
ax3.set_ylabel("Product")
420+
st.pyplot(fig3)
252421

253-
with st.expander("Show product-level data table"):
422+
with st.expander("Show product sensitivity table"):
254423
st.dataframe(
255-
product_plot_df.sort_values(["product_name", "week"], ascending=[True, False]),
256-
use_container_width=True,
424+
sensitivity_df[
425+
[
426+
"product",
427+
"product_name",
428+
"correlation_with_wti",
429+
"abs_correlation",
430+
"direction",
431+
"weeks_used",
432+
]
433+
],
434+
width="stretch",
257435
)
258436

259437
elapsed = time.time() - start_time

0 commit comments

Comments
 (0)