iurbinah commited on
Commit
fc7f7d3
·
1 Parent(s): b9cbea7

Styled UI: cleaner cards, compact layout, embed-ready

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +301 -246
src/streamlit_app.py CHANGED
@@ -1,5 +1,5 @@
1
  # mean_inference_app.py
2
- # Streamlit ≥1.32
3
 
4
  import streamlit as st
5
  import pandas as pd
@@ -7,268 +7,323 @@ import numpy as np
7
  from scipy.stats import norm, t
8
  import io
9
 
10
- # ---------- Page ----------
11
- st.title("Inference for Means (Continous Variables): Confidence Interval & Hypothesis Test")
12
-
13
- st.markdown(r"""
14
- This app performs inference on population means:
15
-
16
- 1. **One-Sample Mean**
17
- 2. **Difference Between Two Independent Means**
18
 
19
- For either case you may construct a **confidence interval** or run a **hypothesis test**,
20
- choosing a *z* (large-sample approximation) or *t* (σ unknown; normal population) approach.
21
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  # ---------- INPUTS ----------
24
- st.header("Inputs")
25
-
26
- # Inference type (one- vs two-sample)
27
- inf_type = st.radio(
28
- "Choose inference:",
29
- ["One-Sample Mean", "Two-Sample Mean (independent)"],
30
- key="inf_type"
31
- )
32
 
33
- # Analysis type
34
- analysis_type = st.radio(
35
- "Choose analysis:",
36
- ["Confidence Interval", "Hypothesis Test"],
37
- key="analysis_type"
38
- )
39
 
40
  # Distribution choice
41
  dist_choice = st.radio(
42
  "Sampling distribution:",
43
- ["Large sample Approximation → *z*", "Small sample (σ unknown and normal population) → *t*"],
44
- key="dist_choice"
 
45
  )
 
 
 
46
 
47
  # ---------- Sample Data Inputs ----------
48
- if inf_type == "One-Sample Mean":
49
- st.subheader("Sample")
50
- n = st.number_input("Sample size (n)", min_value=2, step=1, value=30, key="n")
51
- xbar = st.number_input("Sample mean (x̄)", format="%.4f", value=0.0, key="xbar")
52
- s = st.number_input("Sample standard deviation (s) • use σ if known", min_value=0.0001,
53
- format="%.4f", value=1.0, key="s")
54
- else:
 
 
 
 
 
 
55
  col1, col2 = st.columns(2)
56
  with col1:
57
- st.subheader("Group 1")
58
- n1 = st.number_input("Sample size n₁", min_value=2, step=1, value=30, key="n1")
59
- xbar1 = st.number_input("Sample mean x̄₁", format="%.4f", value=0.0, key="xbar1")
60
- s1 = st.number_input("Sample standard deviation s₁", min_value=0.0001,
61
- format="%.4f", value=1.0, key="s1")
 
 
 
 
62
  with col2:
63
- st.subheader("Group 2")
64
- n2 = st.number_input("Sample size n₂", min_value=2, step=1, value=30, key="n2")
65
- xbar2 = st.number_input("Sample mean x̄₂", format="%.4f", value=0.0, key="xbar2")
66
- s2 = st.number_input("Sample standard deviation s₂", min_value=0.0001,
67
- format="%.4f", value=1.0, key="s2")
 
 
 
 
 
68
 
69
  # ---------- CI / HT specific controls ----------
70
  if analysis_type == "Confidence Interval":
71
- conf_level = st.slider("Confidence level", 0.80, 0.99, 0.95, step=0.01,
72
- key="conf_level")
73
- else: # Hypothesis Test
74
- alpha = st.slider("Significance level (α)", 0.01, 0.10, 0.05, step=0.01, key="alpha")
75
- if inf_type == "One-Sample Mean":
76
- mu0 = st.number_input("Null mean (μ₀)", format="%.4f", value=0.0, key="mu0")
77
- alt = st.radio("Alternative hypothesis (H₁):",
78
- ["μ ≠ μ₀ (Two-sided)",
79
- > μ₀ (Right-sided)",
80
- "μ < μ₀ (Left-sided)"],
81
- key="alt_one")
82
- else:
83
- alt = st.radio("Alternative hypothesis (H₁):",
84
- ["μ₁ ≠ μ₂ (Two-sided)",
85
- "μ₁ > μ₂ (Right-sided)",
86
- "μ₁ < μ₂ (Left-sided)"],
87
- key="alt_two")
88
-
89
- # ---------- RUN ----------
90
- run = st.button("Run Analysis")
91
-
92
- st.header("Results")
93
-
94
- # ===== ONE-SAMPLE =====
95
- if run and inf_type == "One-Sample Mean" and n >= 2:
96
- se = s / np.sqrt(n)
97
- df = n - 1
98
- dist_is_z = dist_choice.startswith("Large")
99
-
100
- if analysis_type == "Confidence Interval":
101
- # critical value
102
- if dist_is_z:
103
- crit = norm.ppf(1 - (1 - conf_level) / 2)
104
- else:
105
- crit = t.ppf(1 - (1 - conf_level) / 2, df)
106
- margin = crit * se
107
- lower, upper = xbar - margin, xbar + margin
108
-
109
- # --- Display ---
110
- st.subheader("Confidence Interval")
111
- st.metric("Sample mean (x̄)", f"{xbar:.4f}")
112
- st.metric(f"{conf_level*100:.1f}% CI",
113
- f"[{lower:.4f}, {upper:.4f}]")
114
- st.write(f"Standard Error: **{se:.4f}**")
115
- st.write(f"Critical value ({'z' if dist_is_z else 't'}): **{crit:.4f}**")
116
- st.write(f"Margin of Error: **{margin:.4f}**")
117
-
118
- results = {
119
- "Analysis": ["CI – One-Sample"],
120
- "n": [n], "x̄": [xbar], "s": [s],
121
- "SE": [se], "Dist": ["z" if dist_is_z else "t"],
122
- "Conf Level": [conf_level],
123
- "Lower": [lower], "Upper": [upper]
124
- }
125
-
126
- else: # Hypothesis Test
127
- if dist_is_z:
128
- zt = (xbar - mu0) / se
129
- p_val = (2 if "≠" in alt else 1) * \
130
- (1 - norm.cdf(abs(zt))) if "≠" in alt else \
131
- (1 - norm.cdf(zt) if ">" in alt else norm.cdf(zt))
132
- crit = norm.ppf(1 - alpha/2) if "≠" in alt else norm.ppf(1 - alpha)
133
- else:
134
- zt = (xbar - mu0) / se # same formula, but follows t-df
135
- p_val = (2 if "≠" in alt else 1) * \
136
- (1 - t.cdf(abs(zt), df)) if "≠" in alt else \
137
- (1 - t.cdf(zt, df) if ">" in alt else t.cdf(zt, df))
138
- crit = t.ppf(1 - alpha/2, df) if "≠" in alt else t.ppf(1 - alpha, df)
139
-
140
- decision = ("Reject H₀", "Fail to reject H₀")[p_val > alpha]
141
-
142
- # --- Display ---
143
- st.subheader("Hypothesis Test")
144
- st.metric(f"{'z' if dist_is_z else 't'}-statistic", f"{zt:.4f}")
145
- st.metric("p-value", f"{p_val:.4g}")
146
- st.write(f"Standard Error: **{se:.4f}**")
147
- st.write(f"α = {alpha:.2f} • Alternative: **{alt}**")
148
- st.write(f"Critical value(s): **{('±' if '≠' in alt else '')}{crit:.4f}**")
149
- st.subheader("Conclusion")
150
- if decision == "Reject H₀":
151
- st.success(f"Reject H₀ at α = {alpha:.2f}.")
152
- else:
153
- st.info(f"Fail to reject H₀ at α = {alpha:.2f}.")
154
-
155
- results = {
156
- "Analysis": ["HT – One-Sample"],
157
- "n": [n], "x̄": [xbar], "s": [s], "μ₀": [mu0],
158
- "SE": [se], "Dist": ["z" if dist_is_z else "t"],
159
- "α": [alpha], "Alt": [alt],
160
- "Stat": [zt], "p": [p_val],
161
- "Reject_H0": [decision.startswith("Reject")]
162
- }
163
-
164
- # ===== TWO-SAMPLE =====
165
- elif run and inf_type.startswith("Two-Sample") and n1 >= 2 and n2 >= 2:
166
- se = np.sqrt(s1**2/n1 + s2**2/n2)
167
- df_num = (s1**2/n1 + s2**2/n2)**2
168
- df_den = (s1**2/n1)**2/(n1-1) + (s2**2/n2)**2/(n2-1)
169
- df = df_num / df_den
170
- diff = xbar1 - xbar2
171
- dist_is_z = dist_choice.startswith("Large")
172
-
173
- if analysis_type == "Confidence Interval":
174
- crit = norm.ppf(1 - (1 - conf_level) / 2) if dist_is_z else t.ppf(
175
- 1 - (1 - conf_level) / 2, df)
176
- margin = crit * se
177
- lower, upper = diff - margin, diff + margin
178
-
179
- st.subheader("Confidence Interval")
180
- st.metric("Difference (x̄₁ − x̄₂)", f"{diff:.4f}")
181
- st.metric(f"{conf_level*100:.1f}% CI",
182
- f"[{lower:.4f}, {upper:.4f}]")
183
- st.write(f"Standard Error: **{se:.4f}**")
184
- st.write(f"df (Welch): **{df:.1f}**")
185
- st.write(f"Critical value ({'z' if dist_is_z else 't'}): **{crit:.4f}**")
186
-
187
- results = {
188
- "Analysis": ["CI – Two-Sample"],
189
- "n₁": [n1], "x̄₁": [xbar1], "s₁": [s1],
190
- "n₂": [n2], "x̄₂": [xbar2], "s₂": [s2],
191
- "SE": [se], "df": [df],
192
- "Dist": ["z" if dist_is_z else "t"],
193
- "Conf Level": [conf_level],
194
- "Lower": [lower], "Upper": [upper]
195
- }
196
-
197
- else: # Hypothesis Test
198
- zt = diff / se
199
- if dist_is_z:
200
- p_val = (2 if "≠" in alt else 1) * \
201
- (1 - norm.cdf(abs(zt))) if "≠" in alt else \
202
- (1 - norm.cdf(zt) if ">" in alt else norm.cdf(zt))
203
- crit = norm.ppf(1 - alpha/2) if "≠" in alt else norm.ppf(1 - alpha)
204
- else:
205
- p_val = (2 if "≠" in alt else 1) * \
206
- (1 - t.cdf(abs(zt), df)) if "≠" in alt else \
207
- (1 - t.cdf(zt, df) if ">" in alt else t.cdf(zt, df))
208
- crit = t.ppf(1 - alpha/2, df) if "≠" in alt else t.ppf(1 - alpha, df)
209
-
210
- decision = ("Reject H₀", "Fail to reject H₀")[p_val > alpha]
211
-
212
- st.subheader("Hypothesis Test")
213
- st.metric(f"{'z' if dist_is_z else 't'}-statistic", f"{zt:.4f}")
214
- st.metric("p-value", f"{p_val:.4g}")
215
- st.write(f"Standard Error: **{se:.4f}**")
216
- st.write(f"df (Welch): **{df:.1f}**")
217
- st.write(f"α = {alpha:.2f} • Alternative: **{alt}**")
218
- st.write(f"Critical value(s): **{('±' if '≠' in alt else '')}{crit:.4f}**")
219
- st.subheader("Conclusion")
220
- if decision == "Reject H₀":
221
- st.success(f"Reject H₀ at α = {alpha:.2f}.")
222
  else:
223
- st.info(f"Fail to reject H₀ at α = {alpha:.2f}.")
224
-
225
- results = {
226
- "Analysis": ["HT – Two-Sample"],
227
- "n₁": [n1], "x̄₁": [xbar1], "s₁": [s1],
228
- "n₂": [n2], "x̄₂": [xbar2], "s₂": [s2],
229
- "SE": [se], "df": [df],
230
- "Dist": ["z" if dist_is_z else "t"],
231
- "α": [alpha], "Alt": [alt],
232
- "Stat": [zt], "p": [p_val],
233
- "Reject_H0": [decision.startswith("Reject")]
234
- }
235
 
236
- # Warn if inputs invalid
237
- elif run:
238
- st.warning("Please ensure sample sizes are ≥ 2 and standard deviations are > 0.")
239
-
240
- # ---------- DOWNLOAD ----------
241
- if run and 'results' in locals():
242
- df_out = pd.DataFrame(results)
243
- buff = io.BytesIO()
244
- with pd.ExcelWriter(buff, engine="xlsxwriter") as writer:
245
- df_out.to_excel(writer, index=False, sheet_name="Summary")
246
- st.download_button(
247
- label="Download Summary (.xlsx)",
248
- data=buff.getvalue(),
249
- file_name=f"mean_inference_{analysis_type.lower().replace(' ', '_')}.xlsx",
250
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
251
- )
252
-
253
- # ---------- THEORETICAL NOTES ----------
254
- st.header("Theoretical Notes")
255
-
256
- st.markdown(r"""
257
- ### One-Sample Mean
258
- * **Assumptions:** simple random sample; normal population *or* large $n$.
259
- * **Standard error:** $SE = \dfrac{s}{\sqrt{n}}$
260
- * **Confidence interval:**
261
- $ \displaystyle \bar x \;\pm\; z_{1-\alpha/2} \,SE $ *(or)*
262
- $ \displaystyle \bar x \;\pm\; t_{1-\alpha/2,\;df=n-1}\,SE $
263
- * **Test statistic:**
264
- $ \displaystyle z = \frac{\bar x-\mu_0}{SE} $ *(or)*
265
- $ \displaystyle t_{df} = \frac{\bar x-\mu_0}{SE} $
266
-
267
- ### Two Independent Means
268
- * **Standard error (Welch):**
269
- $ \displaystyle SE = \sqrt{\frac{s_1^{2}}{n_1}+\frac{s_2^{2}}{n_2}} $
270
- * **df (Welch–Satterthwaite):**
271
- $ \displaystyle
272
- df = \frac{(s_1^{2}/n_1 + s_2^{2}/n_2)^2}
273
- {(s_1^{2}/n_1)^2/(n_1-1) + (s_2^{2}/n_2)^2/(n_2-1)} $
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  """)
 
1
  # mean_inference_app.py
2
+ # Streamlit ≥1.32 — Styled for embedding
3
 
4
  import streamlit as st
5
  import pandas as pd
 
7
  from scipy.stats import norm, t
8
  import io
9
 
10
+ # ---------- Page Config ----------
11
+ st.set_page_config(
12
+ page_title="Inference for Means",
13
+ page_icon="📈",
14
+ layout="centered",
15
+ initial_sidebar_state="collapsed"
16
+ )
 
17
 
18
+ # ---------- Custom CSS for Clean Embed Look ----------
19
+ st.markdown("""
20
+ <style>
21
+ /* Compact header */
22
+ .main h1 { font-size: 1.6rem; margin-bottom: 0.5rem; }
23
+
24
+ /* Tighter spacing */
25
+ .block-container { padding-top: 1.5rem; padding-bottom: 1rem; }
26
+
27
+ /* Styled result cards */
28
+ .result-card {
29
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
30
+ padding: 1.2rem;
31
+ border-radius: 10px;
32
+ color: white;
33
+ text-align: center;
34
+ margin: 0.5rem 0;
35
+ }
36
+ .result-card h3 {
37
+ margin: 0;
38
+ font-size: 0.85rem;
39
+ font-weight: 400;
40
+ opacity: 0.9;
41
+ }
42
+ .result-card p {
43
+ margin: 0.3rem 0 0 0;
44
+ font-size: 1.5rem;
45
+ font-weight: 600;
46
+ }
47
+
48
+ /* CI card */
49
+ .ci-card { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
50
+
51
+ /* Reject card */
52
+ .reject-card { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); }
53
+
54
+ /* Fail to reject card */
55
+ .accept-card { background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); }
56
+
57
+ /* Compact inputs */
58
+ .stNumberInput > div > div > input { padding: 0.4rem; }
59
+
60
+ /* Subtle divider */
61
+ hr { margin: 1rem 0; border: none; border-top: 1px solid #e0e0e0; }
62
+
63
+ /* Info boxes */
64
+ .info-box {
65
+ background: #f8f9fa;
66
+ border-left: 3px solid #667eea;
67
+ padding: 0.7rem 1rem;
68
+ border-radius: 0 8px 8px 0;
69
+ margin: 0.5rem 0;
70
+ font-size: 0.9rem;
71
+ }
72
+
73
+ /* Hide Streamlit branding for cleaner embed */
74
+ #MainMenu {visibility: hidden;}
75
+ footer {visibility: hidden;}
76
+ header {visibility: hidden;}
77
+ </style>
78
+ """, unsafe_allow_html=True)
79
+
80
+ # ---------- Title ----------
81
+ st.markdown("## 📈 Inference for Means")
82
+ st.caption("Confidence intervals & hypothesis tests for one or two means")
83
 
84
  # ---------- INPUTS ----------
85
+ with st.container():
86
+ col1, col2 = st.columns(2)
87
+ with col1:
88
+ inf_type = st.radio("Inference type:", ["One-Sample", "Two-Sample"],
89
+ key="inf_type", horizontal=True)
90
+ with col2:
91
+ analysis_type = st.radio("Analysis:", ["Confidence Interval", "Hypothesis Test"],
92
+ key="analysis_type", horizontal=True)
93
 
94
+ st.markdown("<hr>", unsafe_allow_html=True)
 
 
 
 
 
95
 
96
  # Distribution choice
97
  dist_choice = st.radio(
98
  "Sampling distribution:",
99
+ ["z (large sample)", "t (small sample, σ unknown)"],
100
+ key="dist_choice",
101
+ horizontal=True
102
  )
103
+ dist_is_z = dist_choice.startswith("z")
104
+
105
+ st.markdown("<hr>", unsafe_allow_html=True)
106
 
107
  # ---------- Sample Data Inputs ----------
108
+ if inf_type == "One-Sample":
109
+ cols = st.columns(3)
110
+ with cols[0]:
111
+ n = st.number_input("n", min_value=2, step=1, value=30, key="n")
112
+ with cols[1]:
113
+ xbar = st.number_input("x̄", format="%.4f", value=0.0, key="xbar")
114
+ with cols[2]:
115
+ s = st.number_input("s (or σ)", min_value=0.0001, format="%.4f", value=1.0, key="s")
116
+
117
+ st.markdown(f'<div class="info-box">SE = s/√n = <strong>{s/np.sqrt(n):.4f}</strong></div>',
118
+ unsafe_allow_html=True)
119
+
120
+ else: # Two-Sample
121
  col1, col2 = st.columns(2)
122
  with col1:
123
+ st.markdown("**Group 1**")
124
+ c1, c2, c3 = st.columns(3)
125
+ with c1:
126
+ n1 = st.number_input("n₁", min_value=2, step=1, value=30, key="n1")
127
+ with c2:
128
+ xbar1 = st.number_input("x̄₁", format="%.4f", value=0.0, key="xbar1")
129
+ with c3:
130
+ s1 = st.number_input("s₁", min_value=0.0001, format="%.4f", value=1.0, key="s1")
131
+
132
  with col2:
133
+ st.markdown("**Group 2**")
134
+ c1, c2, c3 = st.columns(3)
135
+ with c1:
136
+ n2 = st.number_input("n₂", min_value=2, step=1, value=30, key="n2")
137
+ with c2:
138
+ xbar2 = st.number_input("x̄₂", format="%.4f", value=0.0, key="xbar2")
139
+ with c3:
140
+ s2 = st.number_input("s₂", min_value=0.0001, format="%.4f", value=1.0, key="s2")
141
+
142
+ st.markdown("<hr>", unsafe_allow_html=True)
143
 
144
  # ---------- CI / HT specific controls ----------
145
  if analysis_type == "Confidence Interval":
146
+ conf_level = st.select_slider("Confidence level:",
147
+ options=[0.90, 0.95, 0.99], value=0.95,
148
+ format_func=lambda x: f"{x*100:.0f}%", key="conf_level")
149
+ else:
150
+ cols = st.columns([1, 2])
151
+ with cols[0]:
152
+ alpha = st.select_slider("α level:", options=[0.01, 0.05, 0.10], value=0.05, key="alpha")
153
+ with cols[1]:
154
+ if inf_type == "One-Sample":
155
+ mu0 = st.number_input("Null mean (μ₀):", format="%.4f", value=0.0, key="mu0")
156
+ alt = st.radio("H₁:", ["μ ≠ μ₀", "μ > μ₀", "μ < μ₀"], horizontal=True, key="alt_one")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  else:
158
+ alt = st.radio("H₁:", ["μ₁ μ₂", "μ₁ > μ₂", "μ₁ < μ₂"], horizontal=True, key="alt_two")
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ # ---------- RUN ----------
161
+ run = st.button("▶ Calculate", type="primary", use_container_width=True)
162
+
163
+ # ---------- Helper function ----------
164
+ def result_card(label, value, card_class="result-card"):
165
+ return f'<div class="{card_class}"><h3>{label}</h3><p>{value}</p></div>'
166
+
167
+ # ===== RESULTS =====
168
+ if run:
169
+ st.markdown("<hr>", unsafe_allow_html=True)
170
+
171
+ # ===== ONE-SAMPLE =====
172
+ if inf_type == "One-Sample" and n >= 2:
173
+ se = s / np.sqrt(n)
174
+ df = n - 1
175
+
176
+ if analysis_type == "Confidence Interval":
177
+ crit = norm.ppf(1 - (1 - conf_level)/2) if dist_is_z else t.ppf(1 - (1 - conf_level)/2, df)
178
+ margin = crit * se
179
+ lower, upper = xbar - margin, xbar + margin
180
+
181
+ cols = st.columns(2)
182
+ with cols[0]:
183
+ st.markdown(result_card("Sample Mean (x̄)", f"{xbar:.4f}"), unsafe_allow_html=True)
184
+ with cols[1]:
185
+ st.markdown(result_card(f"{conf_level*100:.0f}% Confidence Interval",
186
+ f"[{lower:.4f}, {upper:.4f}]", "result-card ci-card"), unsafe_allow_html=True)
187
+
188
+ with st.expander("📋 Details", expanded=False):
189
+ st.write(f"**Standard Error:** {se:.4f}")
190
+ st.write(f"**Critical value ({'z' if dist_is_z else f't(df={df})'}):** ±{crit:.4f}")
191
+ st.write(f"**Margin of Error:** {margin:.4f}")
192
+
193
+ results = {"Analysis": ["CI"], "n": [n], "x̄": [xbar], "s": [s],
194
+ "SE": [se], "Lower": [lower], "Upper": [upper]}
195
+
196
+ else: # Hypothesis Test
197
+ stat = (xbar - mu0) / se
198
+
199
+ if "≠" in alt:
200
+ p_val = 2 * (1 - (norm.cdf(abs(stat)) if dist_is_z else t.cdf(abs(stat), df)))
201
+ crit = norm.ppf(1 - alpha/2) if dist_is_z else t.ppf(1 - alpha/2, df)
202
+ elif ">" in alt:
203
+ p_val = 1 - (norm.cdf(stat) if dist_is_z else t.cdf(stat, df))
204
+ crit = norm.ppf(1 - alpha) if dist_is_z else t.ppf(1 - alpha, df)
205
+ else:
206
+ p_val = norm.cdf(stat) if dist_is_z else t.cdf(stat, df)
207
+ crit = -(norm.ppf(1 - alpha) if dist_is_z else t.ppf(1 - alpha, df))
208
+
209
+ reject = p_val <= alpha
210
+
211
+ cols = st.columns(2)
212
+ with cols[0]:
213
+ st.markdown(result_card(f"{'z' if dist_is_z else 't'}-statistic", f"{stat:.4f}"),
214
+ unsafe_allow_html=True)
215
+ with cols[1]:
216
+ st.markdown(result_card("p-value", f"{p_val:.4g}"), unsafe_allow_html=True)
217
+
218
+ if reject:
219
+ st.markdown(result_card("Decision", f"Reject H₀ at α = {alpha}", "result-card reject-card"),
220
+ unsafe_allow_html=True)
221
+ else:
222
+ st.markdown(result_card("Decision", f"Fail to reject H₀ at α = {alpha}", "result-card accept-card"),
223
+ unsafe_allow_html=True)
224
+
225
+ with st.expander("📋 Details", expanded=False):
226
+ st.write(f"**x̄ = {xbar:.4f}** vs **μ₀ = {mu0:.4f}**")
227
+ st.write(f"**SE:** {se:.4f}")
228
+ st.write(f"**df:** {df}" if not dist_is_z else "**Distribution:** Normal (z)")
229
+ st.write(f"**Critical value:** {'±' if '≠' in alt else ''}{abs(crit):.4f}")
230
+
231
+ results = {"Analysis": ["HT"], "n": [n], "x̄": [xbar], "μ₀": [mu0],
232
+ "stat": [stat], "p-value": [p_val], "Reject": [reject]}
233
+
234
+ # ===== TWO-SAMPLE =====
235
+ elif inf_type == "Two-Sample" and n1 >= 2 and n2 >= 2:
236
+ se = np.sqrt(s1**2/n1 + s2**2/n2)
237
+ diff = xbar1 - xbar2
238
+
239
+ # Welch df
240
+ df_num = (s1**2/n1 + s2**2/n2)**2
241
+ df_den = (s1**2/n1)**2/(n1-1) + (s2**2/n2)**2/(n2-1)
242
+ df = df_num / df_den
243
+
244
+ if analysis_type == "Confidence Interval":
245
+ crit = norm.ppf(1 - (1 - conf_level)/2) if dist_is_z else t.ppf(1 - (1 - conf_level)/2, df)
246
+ margin = crit * se
247
+ lower, upper = diff - margin, diff + margin
248
+
249
+ cols = st.columns(2)
250
+ with cols[0]:
251
+ st.markdown(result_card("Difference (x̄₁ − x̄₂)", f"{diff:.4f}"), unsafe_allow_html=True)
252
+ with cols[1]:
253
+ st.markdown(result_card(f"{conf_level*100:.0f}% Confidence Interval",
254
+ f"[{lower:.4f}, {upper:.4f}]", "result-card ci-card"), unsafe_allow_html=True)
255
+
256
+ with st.expander("📋 Details", expanded=False):
257
+ st.write(f"**x̄₁ = {xbar1:.4f}**, **x̄₂ = {xbar2:.4f}**")
258
+ st.write(f"**Standard Error (Welch):** {se:.4f}")
259
+ st.write(f"**df (Welch):** {df:.1f}")
260
+ st.write(f"**Margin of Error:** {margin:.4f}")
261
+
262
+ results = {"Analysis": ["CI-2"], "x̄₁": [xbar1], "x̄₂": [xbar2],
263
+ "Diff": [diff], "Lower": [lower], "Upper": [upper]}
264
+
265
+ else: # Hypothesis Test
266
+ stat = diff / se
267
+
268
+ if "≠" in alt:
269
+ p_val = 2 * (1 - (norm.cdf(abs(stat)) if dist_is_z else t.cdf(abs(stat), df)))
270
+ crit = norm.ppf(1 - alpha/2) if dist_is_z else t.ppf(1 - alpha/2, df)
271
+ elif ">" in alt:
272
+ p_val = 1 - (norm.cdf(stat) if dist_is_z else t.cdf(stat, df))
273
+ crit = norm.ppf(1 - alpha) if dist_is_z else t.ppf(1 - alpha, df)
274
+ else:
275
+ p_val = norm.cdf(stat) if dist_is_z else t.cdf(stat, df)
276
+ crit = -(norm.ppf(1 - alpha) if dist_is_z else t.ppf(1 - alpha, df))
277
+
278
+ reject = p_val <= alpha
279
+
280
+ cols = st.columns(2)
281
+ with cols[0]:
282
+ st.markdown(result_card(f"{'z' if dist_is_z else 't'}-statistic", f"{stat:.4f}"),
283
+ unsafe_allow_html=True)
284
+ with cols[1]:
285
+ st.markdown(result_card("p-value", f"{p_val:.4g}"), unsafe_allow_html=True)
286
+
287
+ if reject:
288
+ st.markdown(result_card("Decision", f"Reject H₀ at α = {alpha}", "result-card reject-card"),
289
+ unsafe_allow_html=True)
290
+ else:
291
+ st.markdown(result_card("Decision", f"Fail to reject H₀ at α = {alpha}", "result-card accept-card"),
292
+ unsafe_allow_html=True)
293
+
294
+ with st.expander("📋 Details", expanded=False):
295
+ st.write(f"**x̄₁ = {xbar1:.4f}**, **x̄₂ = {xbar2:.4f}**")
296
+ st.write(f"**SE (Welch):** {se:.4f}")
297
+ st.write(f"**df (Welch):** {df:.1f}")
298
+
299
+ results = {"Analysis": ["HT-2"], "x̄₁": [xbar1], "x̄₂": [xbar2],
300
+ "stat": [stat], "p-value": [p_val], "Reject": [reject]}
301
+
302
+ # Download
303
+ if 'results' in locals():
304
+ df_out = pd.DataFrame(results)
305
+ buff = io.BytesIO()
306
+ with pd.ExcelWriter(buff, engine="xlsxwriter") as writer:
307
+ df_out.to_excel(writer, index=False)
308
+ st.download_button("📥 Download Results", data=buff.getvalue(),
309
+ file_name="mean_inference.xlsx",
310
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
311
+
312
+ # ---------- Formulas (collapsed) ----------
313
+ with st.expander("📚 Formulas & Theory", expanded=False):
314
+ st.markdown(r"""
315
+ **One-Sample Mean**
316
+ - SE: $s/\sqrt{n}$
317
+ - CI: $\bar x \pm t_{\alpha/2, df} \cdot SE$
318
+ - Test stat: $t = (\bar x - \mu_0)/SE$, $df = n-1$
319
+
320
+ **Two Independent Means (Welch)**
321
+ - SE: $\sqrt{s_1^2/n_1 + s_2^2/n_2}$
322
+ - Welch df: $\dfrac{(s_1^2/n_1 + s_2^2/n_2)^2}{(s_1^2/n_1)^2/(n_1-1) + (s_2^2/n_2)^2/(n_2-1)}$
323
+ - CI: $(\bar x_1 - \bar x_2) \pm t_{\alpha/2, df} \cdot SE$
324
+ - Test stat: $t = (\bar x_1 - \bar x_2)/SE$
325
+
326
+ **When to use z vs t:**
327
+ - **z**: Large sample (n ≥ 30) or σ known
328
+ - **t**: Small sample with σ unknown (assumes normal population)
329
  """)