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",
+ " legendary_mean | \n",
+ " non_legendary_mean | \n",
+ " t_statistic | \n",
+ " p_value | \n",
+ " significant_005 | \n",
+ " significant_bonferroni | \n",
+ " cohens_d | \n",
+ "
\n",
+ " \n",
+ " | stat | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | HP | \n",
+ " 92.74 | \n",
+ " 67.18 | \n",
+ " 8.981 | \n",
+ " 1.002691e-13 | \n",
+ " True | \n",
+ " True | \n",
+ " 1.040 | \n",
+ "
\n",
+ " \n",
+ " | Attack | \n",
+ " 116.68 | \n",
+ " 75.67 | \n",
+ " 10.438 | \n",
+ " 2.520372e-16 | \n",
+ " True | \n",
+ " True | \n",
+ " 1.345 | \n",
+ "
\n",
+ " \n",
+ " | Defense | \n",
+ " 99.66 | \n",
+ " 71.56 | \n",
+ " 7.637 | \n",
+ " 4.826998e-11 | \n",
+ " True | \n",
+ " True | \n",
+ " 0.929 | \n",
+ "
\n",
+ " \n",
+ " | Sp. Atk | \n",
+ " 122.18 | \n",
+ " 68.45 | \n",
+ " 13.417 | \n",
+ " 1.551461e-21 | \n",
+ " True | \n",
+ " True | \n",
+ " 1.836 | \n",
+ "
\n",
+ " \n",
+ " | Sp. Def | \n",
+ " 105.94 | \n",
+ " 68.89 | \n",
+ " 10.016 | \n",
+ " 2.294933e-15 | \n",
+ " True | \n",
+ " True | \n",
+ " 1.428 | \n",
+ "
\n",
+ " \n",
+ " | Speed | \n",
+ " 100.18 | \n",
+ " 65.46 | \n",
+ " 11.475 | \n",
+ " 1.049016e-18 | \n",
+ " True | \n",
+ " True | \n",
+ " 1.264 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " longitude | \n",
+ " latitude | \n",
+ " dist_school | \n",
+ " dist_hospital | \n",
+ " is_close | \n",
+ " median_house_value | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " -114.31 | \n",
+ " 34.19 | \n",
+ " 3.694888 | \n",
+ " 8.187319 | \n",
+ " False | \n",
+ " 66900.0 | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " -114.47 | \n",
+ " 34.40 | \n",
+ " 3.552591 | \n",
+ " 7.966235 | \n",
+ " False | \n",
+ " 80100.0 | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " -114.56 | \n",
+ " 33.69 | \n",
+ " 3.453940 | \n",
+ " 8.143077 | \n",
+ " False | \n",
+ " 85700.0 | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " -114.57 | \n",
+ " 33.64 | \n",
+ " 3.448840 | \n",
+ " 8.154416 | \n",
+ " False | \n",
+ " 73400.0 | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " -114.57 | \n",
+ " 33.57 | \n",
+ " 3.456848 | \n",
+ " 8.183508 | \n",
+ " False | \n",
+ " 65500.0 | \n",
+ "
\n",
+ " \n",
+ "
\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
}