From 300ef28bdcb124fdf1aec52e832bfe5f5330c8d3 Mon Sep 17 00:00:00 2001 From: Vildan Pirpiroglu Date: Sun, 19 Jul 2026 22:44:40 +0200 Subject: [PATCH] Complete hypothesis testing lab --- .gitignore | 4 + lab-hypothesis-testing.ipynb | 498 +++++++++++++++++++++++++++++++++-- 2 files changed, 485 insertions(+), 17 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35dfbee --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.ipynb_checkpoints/ +.jupyter/ +*.csv diff --git a/lab-hypothesis-testing.ipynb b/lab-hypothesis-testing.ipynb index 0cc26d5..a5022e6 100644 --- a/lab-hypothesis-testing.ipynb +++ b/lab-hypothesis-testing.ipynb @@ -51,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -278,7 +278,7 @@ "[800 rows x 11 columns]" ] }, - "execution_count": 3, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -297,11 +297,71 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dragon: n = 32, mean HP = 83.31, std = 23.80\n", + "Grass : n = 70, mean HP = 67.27, std = 19.52\n", + "\n", + "Levene's test p-value: 0.1748\n", + "-> Using Welch correction (equal_var=False), safe either way.\n", + "\n", + "t-statistic : 3.3350\n", + "p-value : 0.000799\n", + "\n", + "p < 0.05 -> REJECT H0\n", + "\n", + "Mean difference: 16.04 HP\n", + "Cohen's d : 0.766 (0.2 small / 0.5 medium / 0.8 large)\n" + ] + } + ], "source": [ - "#code here" + "# TEST CHOICE: independent two-sample t-test (Welch), one-tailed.\n", + "# Two independent groups + directional claim (\"more HP\") -> alternative='greater'.\n", + "# H0: mean HP Dragon <= mean HP Grass\n", + "# H1: mean HP Dragon > mean HP Grass\n", + "# alpha = 0.05\n", + "\n", + "dragon = df.loc[df['Type 1'] == 'Dragon', 'HP']\n", + "grass = df.loc[df['Type 1'] == 'Grass', 'HP']\n", + "\n", + "print(f'Dragon: n = {len(dragon)}, mean HP = {dragon.mean():.2f}, std = {dragon.std():.2f}')\n", + "print(f'Grass : n = {len(grass)}, mean HP = {grass.mean():.2f}, std = {grass.std():.2f}')\n", + "\n", + "# Check the equal-variance assumption\n", + "lev_stat, lev_p = st.levene(dragon, grass)\n", + "print(f\"\\nLevene's test p-value: {lev_p:.4f}\")\n", + "print('-> Using Welch correction (equal_var=False), safe either way.')\n", + "\n", + "# Run the test\n", + "t_stat, p_value = st.ttest_ind(dragon, grass, equal_var=False, alternative='greater')\n", + "\n", + "print(f'\\nt-statistic : {t_stat:.4f}')\n", + "print(f'p-value : {p_value:.6f}')\n", + "\n", + "alpha = 0.05\n", + "if p_value < alpha:\n", + " print(f'\\np < {alpha} -> REJECT H0')\n", + "else:\n", + " print(f'\\np >= {alpha} -> FAIL TO REJECT H0')\n", + "\n", + "# Effect size\n", + "n1, n2 = len(dragon), len(grass)\n", + "pooled_std = np.sqrt(((n1-1)*dragon.var(ddof=1) + (n2-1)*grass.var(ddof=1)) / (n1+n2-2))\n", + "cohens_d = (dragon.mean() - grass.mean()) / pooled_std\n", + "print(f\"\\nMean difference: {dragon.mean() - grass.mean():.2f} HP\")\n", + "print(f\"Cohen's d : {cohens_d:.3f} (0.2 small / 0.5 medium / 0.8 large)\")\n", + "\n", + "# FINDINGS:\n", + "# At 5% significance we reject H0. Dragon type Pokemon have significantly higher\n", + "# average HP than Grass type. The difference is not just statistically significant\n", + "# but also practically meaningful: Cohen's d is close to 0.8, a medium-to-large effect.\n", + "# Caveat: the Dragon group has only 32 observations, which limits statistical power." ] }, { @@ -313,11 +373,213 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Legendary : 65\n", + "Non-Legendary : 735\n", + "\n", + "Standard threshold : 0.05\n", + "Bonferroni threshold : 0.00833\n", + "\n", + "HP | p = 1.003e-13 | d = 1.040 | REJECT H0\n", + "Attack | p = 2.520e-16 | d = 1.345 | REJECT H0\n", + "Defense | p = 4.827e-11 | d = 0.929 | REJECT H0\n", + "Sp. Atk | p = 1.551e-21 | d = 1.836 | REJECT H0\n", + "Sp. Def | p = 2.295e-15 | d = 1.428 | REJECT H0\n", + "Speed | p = 1.049e-18 | d = 1.264 | REJECT H0\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
legendary_meannon_legendary_meant_statisticp_valuesignificant_005significant_bonferronicohens_d
stat
HP92.7467.188.9811.002691e-13TrueTrue1.040
Attack116.6875.6710.4382.520372e-16TrueTrue1.345
Defense99.6671.567.6374.826998e-11TrueTrue0.929
Sp. Atk122.1868.4513.4171.551461e-21TrueTrue1.836
Sp. Def105.9468.8910.0162.294933e-15TrueTrue1.428
Speed100.1865.4611.4751.049016e-18TrueTrue1.264
\n", + "
" + ], + "text/plain": [ + " legendary_mean non_legendary_mean t_statistic p_value \\\n", + "stat \n", + "HP 92.74 67.18 8.981 1.002691e-13 \n", + "Attack 116.68 75.67 10.438 2.520372e-16 \n", + "Defense 99.66 71.56 7.637 4.826998e-11 \n", + "Sp. Atk 122.18 68.45 13.417 1.551461e-21 \n", + "Sp. Def 105.94 68.89 10.016 2.294933e-15 \n", + "Speed 100.18 65.46 11.475 1.049016e-18 \n", + "\n", + " significant_005 significant_bonferroni cohens_d \n", + "stat \n", + "HP True True 1.040 \n", + "Attack True True 1.345 \n", + "Defense True True 0.929 \n", + "Sp. Atk True True 1.836 \n", + "Sp. Def True True 1.428 \n", + "Speed True True 1.264 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "#code here" + "# TEST CHOICE: independent two-sample t-test (Welch) for each stat, two-tailed.\n", + "# The claim is \"different\" (not higher/lower) -> alternative='two-sided'.\n", + "# Group sizes are very unbalanced (65 vs 735) -> equal_var=False.\n", + "# H0: mean stat Legendary == mean stat Non-Legendary\n", + "# H1: mean stat Legendary != mean stat Non-Legendary\n", + "# alpha = 0.05 (also reported with Bonferroni correction, since we run 6 tests)\n", + "\n", + "stats_cols = ['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']\n", + "\n", + "legendary = df[df['Legendary'] == True]\n", + "non_legendary = df[df['Legendary'] == False]\n", + "\n", + "print(f'Legendary : {len(legendary)}')\n", + "print(f'Non-Legendary : {len(non_legendary)}\\n')\n", + "\n", + "alpha = 0.05\n", + "alpha_bonf = alpha / len(stats_cols)\n", + "\n", + "results = []\n", + "for col in stats_cols:\n", + " t_stat, p_val = st.ttest_ind(legendary[col], non_legendary[col],\n", + " equal_var=False, alternative='two-sided')\n", + " a, b = legendary[col], non_legendary[col]\n", + " pooled = np.sqrt(((len(a)-1)*a.var(ddof=1) + (len(b)-1)*b.var(ddof=1)) / (len(a)+len(b)-2))\n", + " d = (a.mean() - b.mean()) / pooled\n", + "\n", + " results.append({\n", + " 'stat': col,\n", + " 'legendary_mean': round(a.mean(), 2),\n", + " 'non_legendary_mean': round(b.mean(), 2),\n", + " 't_statistic': round(t_stat, 3),\n", + " 'p_value': p_val,\n", + " 'significant_005': p_val < alpha,\n", + " 'significant_bonferroni': p_val < alpha_bonf,\n", + " 'cohens_d': round(d, 3)\n", + " })\n", + "\n", + "results_df = pd.DataFrame(results).set_index('stat')\n", + "print(f'Standard threshold : {alpha}')\n", + "print(f'Bonferroni threshold : {alpha_bonf:.5f}\\n')\n", + "\n", + "for col in stats_cols:\n", + " row = results_df.loc[col]\n", + " verdict = 'REJECT H0' if row['significant_005'] else 'fail to reject H0'\n", + " print(f\"{col:9s} | p = {row['p_value']:.3e} | d = {row['cohens_d']:6.3f} | {verdict}\")\n", + "\n", + "# FINDINGS:\n", + "# All six stats return p-values far below 0.05 and stay significant even after\n", + "# Bonferroni correction. Legendary Pokemon are significantly stronger than\n", + "# Non-Legendary Pokemon on every stat measured.\n", + "# Cohen's d shows where the gap is widest, which is more informative for game\n", + "# balance than the p-values themselves.\n", + "# Caveat: only 65 Legendary Pokemon, so estimates for that group carry more uncertainty.\n", + "\n", + "results_df" ] }, { @@ -483,22 +745,224 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Close : n = 6,829 (40.2%), mean = 246,951.98, median = 218,200.00\n", + "Far : n = 10,171 (59.8%), mean = 180,678.44, median = 149,800.00\n", + "\n", + "Difference in means: 66,273.54\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
longitudelatitudedist_schooldist_hospitalis_closemedian_house_value
0-114.3134.193.6948888.187319False66900.0
1-114.4734.403.5525917.966235False80100.0
2-114.5633.693.4539408.143077False85700.0
3-114.5733.643.4488408.154416False73400.0
4-114.5733.573.4568488.183508False65500.0
\n", + "
" + ], + "text/plain": [ + " longitude latitude dist_school dist_hospital is_close \\\n", + "0 -114.31 34.19 3.694888 8.187319 False \n", + "1 -114.47 34.40 3.552591 7.966235 False \n", + "2 -114.56 33.69 3.453940 8.143077 False \n", + "3 -114.57 33.64 3.448840 8.154416 False \n", + "4 -114.57 33.57 3.456848 8.183508 False \n", + "\n", + " median_house_value \n", + "0 66900.0 \n", + "1 80100.0 \n", + "2 85700.0 \n", + "3 73400.0 \n", + "4 65500.0 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Euclidean distance from each neighborhood to the school and the hospital\n", + "\n", + "def euclidean_distance(lon1, lat1, lon2, lat2):\n", + " \"\"\"Euclidean distance between two coordinate pairs.\"\"\"\n", + " return np.sqrt((lon1 - lon2)**2 + (lat1 - lat2)**2)\n", + "\n", + "SCHOOL = (-118, 34)\n", + "HOSPITAL = (-122, 37)\n", + "THRESHOLD = 0.50\n", + "\n", + "df['dist_school'] = euclidean_distance(df['longitude'], df['latitude'], *SCHOOL)\n", + "df['dist_hospital'] = euclidean_distance(df['longitude'], df['latitude'], *HOSPITAL)\n", + "\n", + "# \"Close\" means close to EITHER facility\n", + "df['is_close'] = (df['dist_school'] < THRESHOLD) | (df['dist_hospital'] < THRESHOLD)\n", + "\n", + "close = df.loc[df['is_close'], 'median_house_value']\n", + "far = df.loc[~df['is_close'], 'median_house_value']\n", + "\n", + "print(f'Close : n = {len(close):,} ({len(close)/len(df):.1%}), mean = {close.mean():,.2f}, median = {close.median():,.2f}')\n", + "print(f'Far : n = {len(far):,} ({len(far)/len(df):.1%}), mean = {far.mean():,.2f}, median = {far.median():,.2f}')\n", + "print(f'\\nDifference in means: {close.mean() - far.mean():,.2f}')\n", + "\n", + "df[['longitude', 'latitude', 'dist_school', 'dist_hospital', 'is_close', 'median_house_value']].head()" + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Levene's test p-value: 0.0208\n", + "-> Variances differ, Welch correction needed.\n", + "\n", + "t-statistic : 37.9923\n", + "p-value : 1.50325e-301\n", + "\n", + "p < 0.05 -> REJECT H0\n", + "\n", + "Mean difference : 66,273.54\n", + "Cohen's d : 0.595\n", + "\n", + "Mann-Whitney U p-value: 0\n", + "-> Non-parametric test agrees with the t-test.\n" + ] + } + ], + "source": [ + "# TEST CHOICE: independent two-sample t-test (Welch), one-tailed.\n", + "# Two independent groups (close vs far) + directional claim (\"more expensive\").\n", + "# H0: mean value close <= mean value far\n", + "# H1: mean value close > mean value far\n", + "# alpha = 0.05\n", + "\n", + "# Check the equal-variance assumption\n", + "lev_stat, lev_p = st.levene(close, far)\n", + "print(f\"Levene's test p-value: {lev_p:.4g}\")\n", + "print('-> Variances differ, Welch correction needed.\\n' if lev_p < 0.05 else '-> Variances comparable.\\n')\n", + "\n", + "t_stat, p_value = st.ttest_ind(close, far, equal_var=False, alternative='greater')\n", + "\n", + "print(f't-statistic : {t_stat:.4f}')\n", + "print(f'p-value : {p_value:.6g}')\n", + "\n", + "alpha = 0.05\n", + "if p_value < alpha:\n", + " print(f'\\np < {alpha} -> REJECT H0')\n", + "else:\n", + " print(f'\\np >= {alpha} -> FAIL TO REJECT H0')\n", + "\n", + "# Effect size\n", + "n1, n2 = len(close), len(far)\n", + "pooled_std = np.sqrt(((n1-1)*close.var(ddof=1) + (n2-1)*far.var(ddof=1)) / (n1+n2-2))\n", + "cohens_d = (close.mean() - far.mean()) / pooled_std\n", + "print(f\"\\nMean difference : {close.mean() - far.mean():,.2f}\")\n", + "print(f\"Cohen's d : {cohens_d:.3f}\")\n", + "\n", + "# Non-parametric cross-check: house values are right-skewed and capped at the top\n", + "u_stat, u_p = st.mannwhitneyu(close, far, alternative='greater')\n", + "print(f'\\nMann-Whitney U p-value: {u_p:.6g}')\n", + "print('-> Non-parametric test agrees with the t-test.')\n", + "\n", + "# FINDINGS:\n", + "# At 5% significance we reject H0. Neighborhoods within 0.50 units of either the\n", + "# school or the hospital have significantly higher median house values than those\n", + "# farther away. The Mann-Whitney U test confirms this without assuming normality,\n", + "# which matters because house values here are right-skewed and capped.\n", + "#\n", + "# IMPORTANT CAVEAT: this is correlation, not causation. The two reference\n", + "# coordinates fall near Los Angeles and San Francisco, so \"close to a school or\n", + "# hospital\" is effectively a proxy for \"close to a major metropolitan area\".\n", + "# The price gap most likely reflects urban location rather than the facilities\n", + "# themselves. A causal claim would require controlling for city, income and other\n", + "# neighborhood characteristics." + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -512,9 +976,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.12.9" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 }