mirror of
https://github.com/mudabbir-ahmad/UNI-PROG3-CW2-MLWP.git
synced 2026-10-07 20:10:20 +00:00
775 KiB
775 KiB
In [58]:
import pandas as pd
import numpy as np
from fontTools.diff import color
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, f1_score, confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
In [59]:
seed = 101In [60]:
df = pd.read_csv('data/googleplaystore_new_new.csv')In [61]:
columns_to_keep = ['Rating', 'Reviews', 'Content Rating', 'Size in bytes', 'Numeric Installs', 'Category']
df_part_a = df[columns_to_keep]In [62]:
X_raw = df_part_a.drop('Category', axis=1)
y = df_part_a['Category']In [63]:
X = pd.get_dummies(X_raw, columns=['Content Rating'])In [64]:
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=seed)In [65]:
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.25, random_state=seed)In [66]:
scaler = StandardScaler()
scaled_X_train = scaler.fit_transform(X_train)
scaled_X_val = scaler.transform(X_val)
scaled_X_test = scaler.transform(X_test)In [67]:
def eval_metrics(y_true, y_pred, label="Model"):
acc = accuracy_score(y_true, y_pred)
f1_w = f1_score(y_true, y_pred, average='weighted')
return acc, f1_w
def split_overview(train_rows, test_rows, val_rows=None):
rows = [
{'Split': 'Train', 'Rows': train_rows},
{'Split': 'Test', 'Rows': test_rows},
]
if val_rows is not None:
rows.insert(1, {'Split': 'Validation', 'Rows': val_rows})
return pd.DataFrame(rows)
def prediction_sheet(y_true, y_pred, y_prob, prefix, head_n=20):
pred_idx = np.argmax(y_prob, axis=1)
pred_conf = y_prob[np.arange(len(y_prob)), pred_idx]
sheet = pd.DataFrame({
f'True_{prefix}': pd.Series(y_true).reset_index(drop=True),
f'Predicted_{prefix}': pd.Series(y_pred).reset_index(drop=True),
'Predicted_Confidence': pred_conf
})
sheet['Correct'] = sheet[f'True_{prefix}'] == sheet[f'Predicted_{prefix}']
return sheet.head(head_n)In [68]:
split_overview(len(X_train), len(X_test), len(X_val))Out [68]:
| Split | Rows | |
|---|---|---|
| 0 | Train | 663 |
| 1 | Validation | 222 |
| 2 | Test | 222 |
In [68]:
In [69]:
# Logistic Regression (tuned a bit for better performance on imbalanced classes)
log_model = LogisticRegression(
max_iter=5000,
random_state=seed,
class_weight='balanced',
C=2.0,
solver='lbfgs'
)
log_model.fit(scaled_X_train, y_train)Out [69]:
LogisticRegression(C=2.0, class_weight='balanced', max_iter=5000,
random_state=101)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
In [70]:
y_val_pred_log = log_model.predict(scaled_X_val)
log_val_acc, log_val_f1 = eval_metrics(y_val, y_val_pred_log, "LogReg (Validation)")In [71]:
y_test_pred_log = log_model.predict(scaled_X_test)
log_test_acc, log_test_f1 = eval_metrics(y_test, y_test_pred_log, "LogReg (Test)")In [72]:
log_probs = log_model.predict_proba(scaled_X_test)
log_pred_df = prediction_sheet(y_test, y_test_pred_log, log_probs, 'Category', head_n=15)
log_pred_dfOut [72]:
| True_Category | Predicted_Category | Predicted_Confidence | Correct | |
|---|---|---|---|---|
| 0 | FINANCE | FINANCE | 0.144609 | True |
| 1 | COMMUNICATION | BUSINESS | 0.094501 | False |
| 2 | LIBRARIES_AND_DEMO | BEAUTY | 0.086577 | False |
| 3 | LIFESTYLE | EVENTS | 0.105029 | False |
| 4 | EDUCATION | EVENTS | 0.087598 | False |
| 5 | ART_AND_DESIGN | HOUSE_AND_HOME | 0.189590 | False |
| 6 | BEAUTY | ART_AND_DESIGN | 0.102613 | False |
| 7 | DATING | DATING | 0.699165 | True |
| 8 | HOUSE_AND_HOME | HOUSE_AND_HOME | 0.131807 | True |
| 9 | FINANCE | HEALTH_AND_FITNESS | 0.183727 | False |
| 10 | HEALTH_AND_FITNESS | GAME | 0.348999 | False |
| 11 | EDUCATION | ART_AND_DESIGN | 0.112001 | False |
| 12 | HEALTH_AND_FITNESS | HEALTH_AND_FITNESS | 0.118400 | True |
| 13 | FINANCE | BEAUTY | 0.088732 | False |
| 14 | FINANCE | FINANCE | 0.124280 | True |
In [73]:
best_k = 1
best_f1 = 0.0
test_error_rates = []
knn_val_acc_scores = []
knn_val_f1_scores = []
k_candidates = list(range(1, 51)) # 1..50
for k in k_candidates:
knn_model = KNeighborsClassifier(n_neighbors=k, weights='distance')
knn_model.fit(scaled_X_train, y_train)
# test error (for elbow)
y_pred_test_k = knn_model.predict(scaled_X_test)
test_error_rates.append(1 - accuracy_score(y_test, y_pred_test_k))
# validation metrics (for selection + plot)
y_pred_val_k = knn_model.predict(scaled_X_val)
val_acc_k = accuracy_score(y_val, y_pred_val_k)
val_f1_k = f1_score(y_val, y_pred_val_k, average='weighted')
knn_val_acc_scores.append(val_acc_k)
knn_val_f1_scores.append(val_f1_k)
if val_f1_k > best_f1:
best_f1 = val_f1_k
best_k = kIn [74]:
plt.figure(figsize=(10, 6), dpi=120)
plt.plot(k_candidates, test_error_rates, label='test error')
plt.xlabel('k value')
plt.title('Elbow method for KNN')
plt.legend()
plt.grid(alpha=0.3)
plt.show()In [75]:
knn_final = KNeighborsClassifier(n_neighbors=best_k, weights='distance')
knn_final.fit(scaled_X_train, y_train)Out [75]:
KNeighborsClassifier(n_neighbors=19, weights='distance')In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
In [76]:
y_val_pred_knn = knn_final.predict(scaled_X_val)
knn_val_acc, knn_val_f1 = eval_metrics(y_val, y_val_pred_knn, f"KNN k={best_k} (Validation)")In [77]:
y_test_pred_knn = knn_final.predict(scaled_X_test)
knn_test_acc, knn_test_f1 = eval_metrics(y_test, y_test_pred_knn, f"KNN k={best_k} (Test)")In [78]:
knn_probs = knn_final.predict_proba(scaled_X_test)
knn_pred_df = prediction_sheet(y_test, y_test_pred_knn, knn_probs, 'Category', head_n=15)
knn_pred_dfOut [78]:
| True_Category | Predicted_Category | Predicted_Confidence | Correct | |
|---|---|---|---|---|
| 0 | FINANCE | FINANCE | 0.322858 | True |
| 1 | COMMUNICATION | FINANCE | 0.149371 | False |
| 2 | LIBRARIES_AND_DEMO | HEALTH_AND_FITNESS | 0.675556 | False |
| 3 | LIFESTYLE | FINANCE | 0.220342 | False |
| 4 | EDUCATION | GAME | 0.982053 | False |
| 5 | ART_AND_DESIGN | LIFESTYLE | 0.696031 | False |
| 6 | BEAUTY | BEAUTY | 0.331699 | True |
| 7 | DATING | DATING | 0.921884 | True |
| 8 | HOUSE_AND_HOME | LIFESTYLE | 0.187210 | False |
| 9 | FINANCE | HEALTH_AND_FITNESS | 0.577385 | False |
| 10 | HEALTH_AND_FITNESS | GAME | 0.308132 | False |
| 11 | EDUCATION | HEALTH_AND_FITNESS | 0.346198 | False |
| 12 | HEALTH_AND_FITNESS | HEALTH_AND_FITNESS | 0.185119 | True |
| 13 | FINANCE | ART_AND_DESIGN | 0.198243 | False |
| 14 | FINANCE | FINANCE | 0.365431 | True |
In [79]:
best_acc_idx = int(np.argmax(knn_val_acc_scores))
best_f1_idx = int(np.argmax(knn_val_f1_scores))
plt.figure(figsize=(9, 5))
plt.plot(k_candidates, knn_val_acc_scores, marker='o', linewidth=2, label='Validation Accuracy')
plt.plot(k_candidates, knn_val_f1_scores, marker='s', linewidth=2, label='Validation Weighted F1')
plt.scatter(
k_candidates[best_acc_idx],
knn_val_acc_scores[best_acc_idx],
s=120, color='green', zorder=5,
label=f'Best Accuracy (k={k_candidates[best_acc_idx]})'
)
plt.scatter(
k_candidates[best_f1_idx],
knn_val_f1_scores[best_f1_idx],
s=120, color='red', zorder=5,
label=f'Best Weighted F1 (k={k_candidates[best_f1_idx]})'
)
plt.axvline(best_k, color='red', linestyle='--', alpha=0.5, label=f'Selected k = {best_k}')
plt.title('KNN Validation Performance vs K (Dynamic)')
plt.xlabel('Number of Neighbors (k)')
plt.ylabel('Score')
plt.xticks(range(1, 51, 2))
plt.grid(alpha=0.3)
plt.legend()
plt.show()In [80]:
results_df = pd.DataFrame([
{"Model": "Logistic Regression", "Split": "Validation", "Accuracy": log_val_acc, "Weighted F1": log_val_f1},
{"Model": "Logistic Regression", "Split": "Test", "Accuracy": log_test_acc, "Weighted F1": log_test_f1},
{"Model": f"KNN (k={best_k})", "Split": "Validation", "Accuracy": knn_val_acc, "Weighted F1": knn_val_f1},
{"Model": f"KNN (k={best_k})", "Split": "Test", "Accuracy": knn_test_acc, "Weighted F1": knn_test_f1},
])In [81]:
results_dfOut [81]:
| Model | Split | Accuracy | Weighted F1 | |
|---|---|---|---|---|
| 0 | Logistic Regression | Validation | 0.301802 | 0.302937 |
| 1 | Logistic Regression | Test | 0.270270 | 0.262607 |
| 2 | KNN (k=19) | Validation | 0.360360 | 0.354603 |
| 3 | KNN (k=19) | Test | 0.310811 | 0.290187 |
In [82]:
top_n = 10
top_classes = y_test.value_counts().head(top_n).index.tolist()
mask = y_test.isin(top_classes)
y_test_top = y_test[mask]
y_log_top = pd.Series(y_test_pred_log, index=y_test.index)[mask]
y_knn_top = pd.Series(y_test_pred_knn, index=y_test.index)[mask]
cm_log_top = confusion_matrix(y_test_top, y_log_top, labels=top_classes)
cm_knn_top = confusion_matrix(y_test_top, y_knn_top, labels=top_classes)
fig, ax = plt.subplots(1, 2, figsize=(15, 6))
ConfusionMatrixDisplay(confusion_matrix=cm_log_top, display_labels=top_classes).plot(
ax=ax[0],
cmap='Blues',
xticks_rotation=45,
values_format='d',
colorbar=False
)
ax[0].set_title(f'Part A: Logistic Regression CM (Top {top_n} classes)')
ConfusionMatrixDisplay(confusion_matrix=cm_knn_top, display_labels=top_classes).plot(
ax=ax[1],
cmap='Greens',
xticks_rotation=45,
values_format='d',
colorbar=False
)
ax[1].set_title(f'Part A: KNN CM (Top {top_n} classes, k={best_k})')
plt.tight_layout()
plt.show()In [83]:
actual_counts_a = y_test.value_counts().reindex(top_classes, fill_value=0)
log_pred_counts_a = pd.Series(y_test_pred_log).value_counts().reindex(top_classes, fill_value=0)
knn_pred_counts_a = pd.Series(y_test_pred_knn).value_counts().reindex(top_classes, fill_value=0)
x = np.arange(len(top_classes))
width = 0.26
fig, ax = plt.subplots(figsize=(13, 5))
ax.bar(x - width, actual_counts_a.values, width, label='Actual (Test)')
ax.bar(x, log_pred_counts_a.values, width, label='LogReg Predictions')
ax.bar(x + width, knn_pred_counts_a.values, width, label=f'KNN Predictions (k={best_k})')
ax.set_xticks(x)
ax.set_xticklabels(top_classes, rotation=45, ha='right')
ax.set_ylabel('Count')
ax.set_title(f'Part A: Actual vs Predicted Category Distribution (Top {top_n} classes)')
ax.grid(True, axis='y', alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()In [84]:
cols_part_b = [
'Rating',
'Reviews',
'Category',
'Size in bytes',
'Numeric Installs',
'Content Rating'
]
df_part_b = df[cols_part_b].dropna().copy()In [85]:
app_inputs_b = df_part_b.drop('Content Rating', axis=1)
true_content_rating = df_part_b['Content Rating']
app_inputs_b_encoded = pd.get_dummies(app_inputs_b, columns=['Category'])In [86]:
class_counts_b = true_content_rating.value_counts()
valid_mask_b = true_content_rating.map(class_counts_b) >= 2
X_b = app_inputs_b_encoded.loc[valid_mask_b]
y_b = true_content_rating.loc[valid_mask_b]
dropped_classes_b = class_counts_b[class_counts_b < 2]
X_temp_b, X_test_b, y_temp_b, y_test_b = train_test_split(
X_b, y_b, test_size=0.2, random_state=seed, stratify=y_b
)
stratify_second_b = y_temp_b if y_temp_b.value_counts().min() >= 2 else None
part_b_fallback = stratify_second_b is None
X_train_b, X_val_b, y_train_b, y_val_b = train_test_split(
X_temp_b, y_temp_b, test_size=0.25, random_state=seed, stratify=stratify_second_b
)
part_b_split_info = pd.DataFrame([
{'Split': 'Train', 'Rows': len(X_train_b)},
{'Split': 'Validation', 'Rows': len(X_val_b)},
{'Split': 'Test', 'Rows': len(X_test_b)},
])
part_b_split_infoOut [86]:
| Split | Rows | |
|---|---|---|
| 0 | Train | 663 |
| 1 | Validation | 221 |
| 2 | Test | 222 |
In [87]:
scaler_b = StandardScaler()
X_train_b_scaled = scaler_b.fit_transform(X_train_b)
X_val_b_scaled = scaler_b.transform(X_val_b)
X_test_b_scaled = scaler_b.transform(X_test_b)
display(pd.DataFrame([{
'Dropped rare classes': int(dropped_classes_b.shape[0]),
'Second split fallback used': part_b_fallback
}]))
| Dropped rare classes | Second split fallback used | |
|---|---|---|
| 0 | 1 | False |
In [88]:
logreg_b = LogisticRegression(
max_iter=5000,
random_state=seed,
class_weight='balanced',
C=2.0,
solver='lbfgs'
)
logreg_b.fit(X_train_b_scaled, y_train_b)
logreg_b_val_pred = logreg_b.predict(X_val_b_scaled)
logreg_b_test_pred = logreg_b.predict(X_test_b_scaled)
logreg_b_val_acc, logreg_b_val_f1 = eval_metrics(y_val_b, logreg_b_val_pred, "LogReg (Part B, Validation)")
logreg_b_test_acc, logreg_b_test_f1 = eval_metrics(y_test_b, logreg_b_test_pred, "LogReg (Part B, Test)")In [89]:
k_values_b = list(range(1, 51, 2))
knn_val_acc_scores_b = []
knn_val_f1_scores_b = []In [90]:
for k in k_values_b:
knn_b = KNeighborsClassifier(n_neighbors=k, weights='distance')
knn_b.fit(X_train_b_scaled, y_train_b)
y_val_pred_k = knn_b.predict(X_val_b_scaled)
knn_val_acc_scores_b.append(accuracy_score(y_val_b, y_val_pred_k))
knn_val_f1_scores_b.append(f1_score(y_val_b, y_val_pred_k, average='weighted'))
best_k_b = int(k_values_b[int(np.argmax(knn_val_f1_scores_b))])
knn_final_b = KNeighborsClassifier(n_neighbors=best_k_b, weights='distance')
knn_final_b.fit(X_train_b_scaled, y_train_b)
knn_b_val_pred = knn_final_b.predict(X_val_b_scaled)
knn_b_test_pred = knn_final_b.predict(X_test_b_scaled)
knn_b_val_acc, knn_b_val_f1 = eval_metrics(y_val_b, knn_b_val_pred, f"KNN k={best_k_b} (Part B, Validation)")
knn_b_test_acc, knn_b_test_f1 = eval_metrics(y_test_b, knn_b_test_pred, f"KNN k={best_k_b} (Part B, Test)")In [91]:
comparison_b = pd.DataFrame([
{"Model": "Logistic Regression", "Split": "Validation", "Accuracy": logreg_b_val_acc, "Weighted F1": logreg_b_val_f1},
{"Model": "Logistic Regression", "Split": "Test", "Accuracy": logreg_b_test_acc, "Weighted F1": logreg_b_test_f1},
{"Model": f"KNN (k={best_k_b})", "Split": "Validation", "Accuracy": knn_b_val_acc, "Weighted F1": knn_b_val_f1},
{"Model": f"KNN (k={best_k_b})", "Split": "Test", "Accuracy": knn_b_test_acc, "Weighted F1": knn_b_test_f1},
])
display(comparison_b)| Model | Split | Accuracy | Weighted F1 | |
|---|---|---|---|---|
| 0 | Logistic Regression | Validation | 0.583710 | 0.659486 |
| 1 | Logistic Regression | Test | 0.621622 | 0.690548 |
| 2 | KNN (k=15) | Validation | 0.873303 | 0.865222 |
| 3 | KNN (k=15) | Test | 0.864865 | 0.856732 |
In [92]:
fig, ax = plt.subplots(1, 2, figsize=(12, 4))
for i, metric in enumerate(["Accuracy", "Weighted F1"]):
for model_name in comparison_b["Model"].unique():
subset = comparison_b[comparison_b["Model"] == model_name]
ax[i].plot(subset["Split"], subset[metric], marker='o', linewidth=2, label=model_name)
ax[i].set_title(f"Part B: {metric} (Validation vs Test)")
ax[i].set_ylim(0, 1)
ax[i].grid(True, alpha=0.3)
ax[i].legend()
plt.tight_layout()
plt.show()In [93]:
fig, ax = plt.subplots(1, 2, figsize=(14, 5))
ConfusionMatrixDisplay.from_predictions(
y_test_b,
logreg_b_test_pred,
ax=ax[0],
cmap='Blues',
xticks_rotation=45,
colorbar=False
)
ax[0].set_title("Part B: Logistic Regression Confusion Matrix (Test)")
ConfusionMatrixDisplay.from_predictions(
y_test_b,
knn_b_test_pred,
ax=ax[1],
cmap='Greens',
xticks_rotation=45,
colorbar=False
)
ax[1].set_title(f"Part B: KNN Confusion Matrix (Test, k={best_k_b})")
plt.tight_layout()
plt.show()In [94]:
logreg_b_probs = logreg_b.predict_proba(X_test_b_scaled)
logreg_b_pred_df = prediction_sheet(y_test_b, logreg_b_test_pred, logreg_b_probs, 'Content_Rating', head_n=20)
logreg_b_pred_dfOut [94]:
| True_Content_Rating | Predicted_Content_Rating | Predicted_Confidence | Correct | |
|---|---|---|---|---|
| 0 | Everyone | Everyone | 0.993702 | True |
| 1 | Everyone | Everyone | 0.482609 | True |
| 2 | Everyone | Everyone | 0.994017 | True |
| 3 | Everyone | Everyone 10+ | 0.522200 | False |
| 4 | Everyone | Teen | 0.767573 | False |
| 5 | Everyone | Everyone | 0.612729 | True |
| 6 | Everyone | Everyone | 0.994448 | True |
| 7 | Everyone | Everyone | 0.993697 | True |
| 8 | Everyone | Everyone 10+ | 0.530097 | False |
| 9 | Everyone | Everyone | 0.749141 | True |
| 10 | Mature 17+ | Mature 17+ | 0.940680 | True |
| 11 | Everyone | Everyone | 0.991359 | True |
| 12 | Teen | Everyone | 0.639161 | False |
| 13 | Everyone | Everyone | 0.993842 | True |
| 14 | Everyone | Everyone | 0.993532 | True |
| 15 | Everyone | Everyone 10+ | 0.480918 | False |
| 16 | Everyone | Everyone | 0.760410 | True |
| 17 | Everyone | Everyone 10+ | 0.504585 | False |
| 18 | Everyone | Everyone | 0.460006 | True |
| 19 | Everyone | Everyone | 0.993453 | True |
In [95]:
knn_b_probs = knn_final_b.predict_proba(X_test_b_scaled)
knn_b_pred_df = prediction_sheet(y_test_b, knn_b_test_pred, knn_b_probs, 'Content_Rating', head_n=20)
knn_b_pred_dfOut [95]:
| True_Content_Rating | Predicted_Content_Rating | Predicted_Confidence | Correct | |
|---|---|---|---|---|
| 0 | Everyone | Everyone | 1.000000 | True |
| 1 | Everyone | Everyone | 1.000000 | True |
| 2 | Everyone | Everyone | 1.000000 | True |
| 3 | Everyone | Everyone | 0.564860 | True |
| 4 | Everyone | Teen | 0.640995 | False |
| 5 | Everyone | Everyone | 0.998311 | True |
| 6 | Everyone | Everyone | 1.000000 | True |
| 7 | Everyone | Everyone | 1.000000 | True |
| 8 | Everyone | Everyone | 0.875223 | True |
| 9 | Everyone | Everyone | 1.000000 | True |
| 10 | Mature 17+ | Mature 17+ | 1.000000 | True |
| 11 | Everyone | Everyone | 1.000000 | True |
| 12 | Teen | Teen | 0.998994 | True |
| 13 | Everyone | Everyone | 1.000000 | True |
| 14 | Everyone | Everyone | 1.000000 | True |
| 15 | Everyone | Everyone | 0.911639 | True |
| 16 | Everyone | Everyone | 1.000000 | True |
| 17 | Everyone | Everyone | 0.919221 | True |
| 18 | Everyone | Everyone | 0.886516 | True |
| 19 | Everyone | Everyone | 1.000000 | True |
In [96]:
actual_counts_b = pd.Series(y_test_b).value_counts().sort_index()
logreg_pred_counts_b = pd.Series(logreg_b_test_pred).value_counts().reindex(actual_counts_b.index, fill_value=0)
knn_pred_counts_b = pd.Series(knn_b_test_pred).value_counts().reindex(actual_counts_b.index, fill_value=0)
x = np.arange(len(actual_counts_b.index))
width = 0.28
fig, ax = plt.subplots(figsize=(12, 4))
ax.bar(x - width, actual_counts_b.values, width, label="Actual (Test)")
ax.bar(x, logreg_pred_counts_b.values, width, label="LogReg Predictions")
ax.bar(x + width, knn_pred_counts_b.values, width, label=f"KNN Predictions (k={best_k_b})")
ax.set_xticks(x)
ax.set_xticklabels(actual_counts_b.index, rotation=45, ha='right')
ax.set_title("Part B: Actual vs Predicted Content Rating distribution (Test set)")
ax.set_ylabel("Number of apps")
ax.grid(True, axis='y', alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()In [97]:
best_model_b = (
f"KNN (k={best_k_b})" if knn_b_test_f1 > logreg_b_test_f1 else "Logistic Regression"
)In [98]:
part_b_final_summary = pd.DataFrame([
{'Model': 'Logistic Regression', 'Accuracy': logreg_b_test_acc, 'Weighted F1': logreg_b_test_f1},
{'Model': f'KNN (k={best_k_b})', 'Accuracy': knn_b_test_acc, 'Weighted F1': knn_b_test_f1},
]).sort_values('Weighted F1', ascending=False).reset_index(drop=True)
display(part_b_final_summary)
display(pd.DataFrame([{'Part B Best Model': best_model_b}]))| Model | Accuracy | Weighted F1 | |
|---|---|---|---|
| 0 | KNN (k=15) | 0.864865 | 0.856732 |
| 1 | Logistic Regression | 0.621622 | 0.690548 |
| Part B Best Model | |
|---|---|
| 0 | KNN (k=15) |
In [99]:
cols_part_c = [
'Rating',
'Reviews',
'Category',
'Size in bytes',
'Content Rating',
'Numeric Installs'
]
df_part_c = df[cols_part_c].dropna().copy()In [100]:
app_inputs_c = df_part_c.drop('Numeric Installs', axis=1)
true_install_bucket = df_part_c['Numeric Installs'].astype(str)
app_inputs_c_encoded = pd.get_dummies(app_inputs_c, columns=['Category', 'Content Rating'])In [101]:
class_counts_c = true_install_bucket.value_counts()
valid_mask_c = true_install_bucket.map(class_counts_c) >= 2
X_c = app_inputs_c_encoded.loc[valid_mask_c]
y_c = true_install_bucket.loc[valid_mask_c]
dropped_classes_c = class_counts_c[class_counts_c < 2]
X_train_c, X_test_c, y_train_c, y_test_c = train_test_split(
X_c,
y_c,
test_size=0.2,
random_state=seed,
stratify=y_c
)
display(pd.DataFrame([
{'Split': 'Train', 'Rows': len(X_train_c)},
{'Split': 'Test', 'Rows': len(X_test_c)},
]))
display(pd.DataFrame([{
'Part C classes after filtering': int(y_c.nunique()),
'Dropped rare classes': int(dropped_classes_c.shape[0])
}]))| Split | Rows | |
|---|---|---|
| 0 | Train | 884 |
| 1 | Test | 222 |
| Part C classes after filtering | Dropped rare classes | |
|---|---|---|
| 0 | 15 | 1 |
In [102]:
scaler_c = StandardScaler()
X_train_c_scaled = scaler_c.fit_transform(X_train_c)
X_test_c_scaled = scaler_c.transform(X_test_c)
display(y_c.value_counts().head(15).rename('Class_Count').reset_index().rename(columns={'index': 'Numeric_Installs'}))| Numeric Installs | Class_Count | |
|---|---|---|
| 0 | 1000000 | 302 |
| 1 | 100000 | 191 |
| 2 | 10000000 | 134 |
| 3 | 500000 | 125 |
| 4 | 5000000 | 99 |
| 5 | 10000 | 66 |
| 6 | 50000 | 56 |
| 7 | 100000000 | 51 |
| 8 | 50000000 | 21 |
| 9 | 1000 | 16 |
| 10 | 5000 | 12 |
| 11 | 100 | 11 |
| 12 | 500000000 | 10 |
| 13 | 500 | 9 |
| 14 | 1000000000 | 3 |
In [103]:
logreg_c = LogisticRegression(
max_iter=5000,
random_state=seed,
class_weight='balanced',
C=2.0,
solver='lbfgs'
)
logreg_c.fit(X_train_c_scaled, y_train_c)
logreg_c_test_pred = logreg_c.predict(X_test_c_scaled)
logreg_c_test_acc, logreg_c_test_f1 = eval_metrics(y_test_c, logreg_c_test_pred, "LogReg (Part C, Test)")In [104]:
stratify_inner_c = y_train_c if y_train_c.value_counts().min() >= 2 else None
X_train_c_inner, X_val_c_inner, y_train_c_inner, y_val_c_inner = train_test_split(
X_train_c, y_train_c, test_size=0.25, random_state=seed, stratify=stratify_inner_c
)
display(pd.DataFrame([{'Part C inner split fallback used': stratify_inner_c is None}]))
scaler_c_inner = StandardScaler()
X_train_c_inner_scaled = scaler_c_inner.fit_transform(X_train_c_inner)
X_val_c_inner_scaled = scaler_c_inner.transform(X_val_c_inner)
X_test_c_scaled_inner = scaler_c_inner.transform(X_test_c)| Part C inner split fallback used | |
|---|---|
| 0 | False |
In [105]:
k_values_c = list(range(1, 51, 2))
knn_val_acc_scores_c = []
knn_val_f1_scores_c = []In [106]:
for k in k_values_c:
knn_c = KNeighborsClassifier(n_neighbors=k, weights='distance')
knn_c.fit(X_train_c_inner_scaled, y_train_c_inner)
y_val_pred_k = knn_c.predict(X_val_c_inner_scaled)
knn_val_acc_scores_c.append(accuracy_score(y_val_c_inner, y_val_pred_k))
knn_val_f1_scores_c.append(f1_score(y_val_c_inner, y_val_pred_k, average='weighted'))In [107]:
best_k_c = int(k_values_c[int(np.argmax(knn_val_f1_scores_c))])
knn_final_c = KNeighborsClassifier(n_neighbors=best_k_c, weights='distance')
knn_final_c.fit(X_train_c_inner_scaled, y_train_c_inner)
knn_c_test_pred = knn_final_c.predict(X_test_c_scaled_inner)
knn_c_test_acc, knn_c_test_f1 = eval_metrics(y_test_c, knn_c_test_pred, f"KNN k={best_k_c} (Part C, Test)")In [116]:
comparison_c = pd.DataFrame([
{"Model": "Logistic Regression", "Accuracy": logreg_c_test_acc, "Weighted F1": logreg_c_test_f1},
{"Model": f"KNN (k={best_k_c})", "Accuracy": knn_c_test_acc, "Weighted F1": knn_c_test_f1},
])
display(comparison_c)
fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(comparison_c["Model"], comparison_c["Weighted F1"], label="Weighted F1")
ax.plot(comparison_c["Model"], comparison_c["Accuracy"], marker='o', linewidth=2, label="Accuracy", color='orange')
ax.set_title("Part C: Model comparison on Test set")
ax.set_ylim(0, 1)
ax.grid(True, axis='y', alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()| Model | Accuracy | Weighted F1 | |
|---|---|---|---|
| 0 | Logistic Regression | 0.171171 | 0.182992 |
| 1 | KNN (k=41) | 0.324324 | 0.285951 |
In [109]:
top_n_c = 12
top_classes_c = pd.Series(y_test_c).value_counts().head(top_n_c).index.tolist()
mask_c = pd.Series(y_test_c).isin(top_classes_c)
y_test_c_top = pd.Series(y_test_c).reset_index(drop=True)[mask_c.reset_index(drop=True)]
logreg_c_pred_top = pd.Series(logreg_c_test_pred).reset_index(drop=True)[mask_c.reset_index(drop=True)]
knn_c_pred_top = pd.Series(knn_c_test_pred).reset_index(drop=True)[mask_c.reset_index(drop=True)]
fig, ax = plt.subplots(1, 2, figsize=(14, 5))
cm_log_top = confusion_matrix(y_test_c_top, logreg_c_pred_top, labels=top_classes_c)
ConfusionMatrixDisplay(confusion_matrix=cm_log_top, display_labels=top_classes_c).plot(
ax=ax[0], cmap='Blues', xticks_rotation=45, values_format='d', colorbar=False
)
ax[0].set_title(f"Part C: Logistic Regression Confusion Matrix (Top {top_n_c} classes, Test)")
cm_knn_top = confusion_matrix(y_test_c_top, knn_c_pred_top, labels=top_classes_c)
ConfusionMatrixDisplay(confusion_matrix=cm_knn_top, display_labels=top_classes_c).plot(
ax=ax[1], cmap='Greens', xticks_rotation=45, values_format='d', colorbar=False
)
ax[1].set_title(f"Part C: KNN Confusion Matrix (Top {top_n_c} classes, Test, k={best_k_c})")
plt.tight_layout()
plt.show()In [110]:
logreg_c_probs = logreg_c.predict_proba(X_test_c_scaled)
logreg_c_pred_df = prediction_sheet(y_test_c, logreg_c_test_pred, logreg_c_probs, 'Installs_Num', head_n=20)
logreg_c_pred_dfOut [110]:
| True_Installs_Num | Predicted_Installs_Num | Predicted_Confidence | Correct | |
|---|---|---|---|---|
| 0 | 1000000 | 5000 | 0.197939 | False |
| 1 | 1000000 | 50000 | 0.469158 | False |
| 2 | 10000000 | 50000000 | 0.425556 | False |
| 3 | 500000 | 1000000 | 0.145711 | False |
| 4 | 100000000 | 500000000 | 0.470446 | False |
| 5 | 100000000 | 50000000 | 0.400632 | False |
| 6 | 10000000 | 50000000 | 0.492270 | False |
| 7 | 10000000 | 5000000 | 0.250524 | False |
| 8 | 10000 | 500 | 0.647713 | False |
| 9 | 1000000 | 1000000 | 0.171517 | True |
| 10 | 100000 | 500000 | 0.334840 | False |
| 11 | 10000 | 50000 | 0.212020 | False |
| 12 | 5000000 | 100 | 0.291118 | False |
| 13 | 1000000 | 500000 | 0.241188 | False |
| 14 | 5000 | 100 | 0.207994 | False |
| 15 | 500000 | 50000 | 0.244060 | False |
| 16 | 5000000 | 10000000 | 0.237635 | False |
| 17 | 10000000 | 50000000 | 0.373763 | False |
| 18 | 1000000 | 1000000 | 0.196770 | True |
| 19 | 100000 | 5000000 | 0.161215 | False |
In [111]:
knn_c_probs = knn_final_c.predict_proba(X_test_c_scaled_inner)
knn_c_pred_df = prediction_sheet(y_test_c, knn_c_test_pred, knn_c_probs, 'Installs_Num', head_n=20)
knn_c_pred_dfOut [111]:
| True_Installs_Num | Predicted_Installs_Num | Predicted_Confidence | Correct | |
|---|---|---|---|---|
| 0 | 1000000 | 1000000 | 0.293952 | True |
| 1 | 1000000 | 1000000 | 0.446849 | True |
| 2 | 10000000 | 1000000 | 0.522130 | False |
| 3 | 500000 | 1000000 | 0.287865 | False |
| 4 | 100000000 | 1000000 | 0.376263 | False |
| 5 | 100000000 | 100000000 | 0.995326 | True |
| 6 | 10000000 | 10000000 | 0.459153 | True |
| 7 | 10000000 | 1000000 | 0.304257 | False |
| 8 | 10000 | 100000 | 0.291979 | False |
| 9 | 1000000 | 100000 | 0.427634 | False |
| 10 | 100000 | 500000 | 0.259322 | False |
| 11 | 10000 | 50000 | 0.322032 | False |
| 12 | 5000000 | 1000000 | 0.234653 | False |
| 13 | 1000000 | 500000 | 0.431720 | False |
| 14 | 5000 | 100000 | 0.377805 | False |
| 15 | 500000 | 50000 | 0.431116 | False |
| 16 | 5000000 | 1000000 | 0.443130 | False |
| 17 | 10000000 | 10000000 | 0.353770 | True |
| 18 | 1000000 | 100000 | 0.376758 | False |
| 19 | 100000 | 1000000 | 0.595938 | False |
In [112]:
dist_top_n_c = 12
most_common_classes_c = pd.Series(y_test_c).value_counts().head(dist_top_n_c).index
actual_counts_c = pd.Series(y_test_c).value_counts().reindex(most_common_classes_c)
logreg_pred_counts_c = pd.Series(logreg_c_test_pred).value_counts().reindex(most_common_classes_c, fill_value=0)
knn_pred_counts_c = pd.Series(knn_c_test_pred).value_counts().reindex(most_common_classes_c, fill_value=0)
x = np.arange(len(most_common_classes_c))
width = 0.28
fig, ax = plt.subplots(figsize=(12, 4))
ax.bar(x - width, actual_counts_c.values, width, label="Actual (Test)")
ax.bar(x, logreg_pred_counts_c.values, width, label="LogReg Predictions")
ax.bar(x + width, knn_pred_counts_c.values, width, label=f"KNN Predictions (k={best_k_c})")
ax.set_xticks(x)
ax.set_xticklabels(most_common_classes_c, rotation=45, ha='right')
ax.set_title(f"Part C: Actual vs Predicted Installs_Num distribution (Top {dist_top_n_c} classes, Test set)")
ax.set_ylabel("Number of apps")
ax.grid(True, axis='y', alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()In [113]:
best_model_c = (
f"KNN (k={best_k_c})" if knn_c_test_f1 > logreg_c_test_f1 else "Logistic Regression"
)In [114]:
part_c_final_summary = pd.DataFrame([
{'Model': 'Logistic Regression', 'Accuracy': logreg_c_test_acc, 'Weighted F1': logreg_c_test_f1},
{'Model': f'KNN (k={best_k_c})', 'Accuracy': knn_c_test_acc, 'Weighted F1': knn_c_test_f1},
]).sort_values('Weighted F1', ascending=False).reset_index(drop=True)
display(part_c_final_summary)
display(pd.DataFrame([{'Part C Best Model': best_model_c}]))| Model | Accuracy | Weighted F1 | |
|---|---|---|---|
| 0 | KNN (k=41) | 0.324324 | 0.285951 |
| 1 | Logistic Regression | 0.171171 | 0.182992 |
| Part C Best Model | |
|---|---|
| 0 | KNN (k=41) |
In [ ]: