Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- Churn oluyor mu kontrol.xlsx +3 -0
- Dockerfile +21 -0
- README.md +2 -8
- Telco-Customer-Churn.csv +0 -0
- app.py +91 -0
- churn_model.pkl +3 -0
- churn_model.py +110 -0
- deneme.py +73 -0
- gradio_app.py +237 -0
- model_features.pkl +3 -0
- requirements.txt +10 -0
- templates/index.html +149 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
Churn[[:space:]]oluyor[[:space:]]mu[[:space:]]kontrol.xlsx filter=lfs diff=lfs merge=lfs -text
|
Churn oluyor mu kontrol.xlsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:cf0d3660e7db1da712247599d74c880a8f80a727b000b90ec5cdbfdd98950564
|
3 |
+
size 783308
|
Dockerfile
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use official Python image
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
# Set working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy requirements and source code
|
8 |
+
COPY requirements.txt ./
|
9 |
+
COPY gradio_app.py ./
|
10 |
+
COPY churn_model.pkl ./
|
11 |
+
COPY model_features.pkl ./
|
12 |
+
COPY Telco-Customer-Churn.csv ./
|
13 |
+
|
14 |
+
# Install dependencies
|
15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
16 |
+
|
17 |
+
# Expose Gradio default port
|
18 |
+
EXPOSE 7860
|
19 |
+
|
20 |
+
# Run the Gradio app
|
21 |
+
CMD ["python", "gradio_app.py"]
|
README.md
CHANGED
@@ -1,12 +1,6 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
|
4 |
-
colorFrom: pink
|
5 |
-
colorTo: yellow
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.31.0
|
8 |
-
app_file: app.py
|
9 |
-
pinned: false
|
10 |
---
|
11 |
-
|
12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: gradio-app
|
3 |
+
app_file: gradio_app.py
|
|
|
|
|
4 |
sdk: gradio
|
5 |
sdk_version: 5.31.0
|
|
|
|
|
6 |
---
|
|
|
|
Telco-Customer-Churn.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
app.py
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request
|
2 |
+
import joblib
|
3 |
+
import pandas as pd
|
4 |
+
|
5 |
+
app = Flask(__name__)
|
6 |
+
|
7 |
+
model = joblib.load("churn_model.pkl")
|
8 |
+
model_features = joblib.load("model_features.pkl")
|
9 |
+
|
10 |
+
value_map = {
|
11 |
+
"Aydan aya": "Month-to-month",
|
12 |
+
"1 yıllık": "One year",
|
13 |
+
"2 yıllık": "Two year",
|
14 |
+
"Elektronik çek": "Electronic check",
|
15 |
+
"Posta çeki": "Mailed check",
|
16 |
+
"Banka havalesi (otomatik)": "Bank transfer (automatic)",
|
17 |
+
"Kredi kartı (otomatik)": "Credit card (automatic)",
|
18 |
+
"Hayır": "No",
|
19 |
+
"Evet": "Yes",
|
20 |
+
"Yok": "No internet service",
|
21 |
+
"Telefon hizmeti yok": "No phone service",
|
22 |
+
"Fiber optik": "Fiber optic"
|
23 |
+
}
|
24 |
+
|
25 |
+
@app.route("/", methods=["GET", "POST"])
|
26 |
+
def index():
|
27 |
+
result = None
|
28 |
+
|
29 |
+
if request.method == "POST":
|
30 |
+
form = request.form
|
31 |
+
input_dict = {}
|
32 |
+
|
33 |
+
# Sayısal alanlar
|
34 |
+
tenure = float(form.get("tenure"))
|
35 |
+
monthly = float(form.get("MonthlyCharges"))
|
36 |
+
total = float(form.get("TotalCharges"))
|
37 |
+
|
38 |
+
# Temel sayısal değişkenler
|
39 |
+
input_dict["tenure"] = tenure
|
40 |
+
input_dict["PhoneService"] = form.get("PhoneService") == "Evet"
|
41 |
+
input_dict["avg_charge_per_month"] = total / tenure if tenure > 0 else 0
|
42 |
+
input_dict["charge_ratio"] = total / (monthly * tenure) if monthly > 0 and tenure > 0 else 1
|
43 |
+
|
44 |
+
# tenure_bin
|
45 |
+
tenure_label = "0-12" if tenure <= 12 else "12-24" if tenure <= 24 else "24+"
|
46 |
+
for bin_label in ["0-12", "12-24", "24+"]:
|
47 |
+
input_dict[f"tenure_bin_{bin_label}"] = (tenure_label == bin_label)
|
48 |
+
|
49 |
+
# is_long_term_contract
|
50 |
+
contract_value = value_map.get(form.get("Contract"), form.get("Contract"))
|
51 |
+
input_dict["is_long_term_contract"] = contract_value in ["One year", "Two year"]
|
52 |
+
|
53 |
+
# One-hot kategorik değişkenler
|
54 |
+
categorical_fields = [
|
55 |
+
"gender", "SeniorCitizen", "Partner", "Dependents", "PaperlessBilling",
|
56 |
+
"MultipleLines", "InternetService", "OnlineSecurity", "OnlineBackup",
|
57 |
+
"DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies",
|
58 |
+
"Contract", "PaymentMethod"
|
59 |
+
]
|
60 |
+
|
61 |
+
for field in categorical_fields:
|
62 |
+
raw_value = form.get(field)
|
63 |
+
mapped_value = value_map.get(raw_value, raw_value)
|
64 |
+
for col in model_features:
|
65 |
+
if col.startswith(f"{field}_"):
|
66 |
+
input_dict[col] = (col == f"{field}_{mapped_value}")
|
67 |
+
|
68 |
+
# Eksik kalan tüm feature'lar tamamlanır
|
69 |
+
for col in model_features:
|
70 |
+
if col not in input_dict:
|
71 |
+
input_dict[col] = 0 if col == "tenure" or "charge" in col or "avg" in col else False
|
72 |
+
|
73 |
+
# DataFrame oluştur ve tahmin yap
|
74 |
+
input_df = pd.DataFrame([[input_dict[col] for col in model_features]], columns=model_features)
|
75 |
+
|
76 |
+
print("💬 MODELE GİDEN VERİLER:", flush=True)
|
77 |
+
print(input_df.to_dict(orient="records")[0], flush=True)
|
78 |
+
|
79 |
+
prediction = model.predict_proba(input_df)[0][1]
|
80 |
+
score = round(prediction * 100, 2)
|
81 |
+
if score >= 50:
|
82 |
+
comment = "Müşteri Kaybedilebilir."
|
83 |
+
else:
|
84 |
+
comment = "Müşteri Kayıp Riski Taşımıyor."
|
85 |
+
result = f"Churn Riski: %{score} — {comment}"
|
86 |
+
|
87 |
+
|
88 |
+
return render_template("index.html", result=result)
|
89 |
+
|
90 |
+
if __name__ == "__main__":
|
91 |
+
app.run(debug=False)
|
churn_model.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e13f104be0f634de27e6721dd5fea34b64f29d9863684ddcdc8aa6a8bccbfa4c
|
3 |
+
size 640263
|
churn_model.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import pandas as pd
|
3 |
+
import matplotlib.pyplot as plt
|
4 |
+
import seaborn as sns
|
5 |
+
|
6 |
+
from xgboost import XGBClassifier
|
7 |
+
from sklearn.model_selection import StratifiedKFold, cross_val_predict
|
8 |
+
from sklearn.preprocessing import MinMaxScaler
|
9 |
+
from imblearn.over_sampling import SMOTE
|
10 |
+
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
|
11 |
+
from imblearn.pipeline import Pipeline
|
12 |
+
import joblib
|
13 |
+
|
14 |
+
sns.set(style='whitegrid')
|
15 |
+
|
16 |
+
# Veri setini oku
|
17 |
+
telco = pd.read_csv('Telco-Customer-Churn.csv')
|
18 |
+
pd.set_option('display.max_columns', None)
|
19 |
+
|
20 |
+
# TotalCharges sayısallaştırma ve eksik verileri çıkarma
|
21 |
+
telco.TotalCharges = pd.to_numeric(telco.TotalCharges, errors='coerce')
|
22 |
+
telco.dropna(inplace=True)
|
23 |
+
|
24 |
+
# CustomerID'yi çıkar
|
25 |
+
df2 = telco.iloc[:, 1:]
|
26 |
+
|
27 |
+
# Binary sütunları True/False yap
|
28 |
+
bool_map = {'Yes': True, 'No': False}
|
29 |
+
binary_columns = ['Churn', 'Partner', 'Dependents', 'PhoneService', 'PaperlessBilling']
|
30 |
+
for col in binary_columns:
|
31 |
+
df2[col] = df2[col].map(bool_map)
|
32 |
+
|
33 |
+
df2['SeniorCitizen'].replace({1: True, 0: False}, inplace=True)
|
34 |
+
df2['gender'].replace({'Female': True, 'Male': False}, inplace=True)
|
35 |
+
|
36 |
+
# Yeni özellikler
|
37 |
+
# Ortalama aylık ödeme
|
38 |
+
df2['avg_charge_per_month'] = df2['TotalCharges'] / df2['tenure'].replace(0, 1)
|
39 |
+
# Toplam ödeme ile (aylık * süre) oranı
|
40 |
+
df2['charge_ratio'] = df2.apply(
|
41 |
+
lambda row: row['TotalCharges'] / (row['MonthlyCharges'] * row['tenure'])
|
42 |
+
if row['MonthlyCharges'] > 0 and row['tenure'] > 0 else 1, axis=1)
|
43 |
+
|
44 |
+
# Süre kategorisi
|
45 |
+
df2['tenure_bin'] = pd.cut(df2['tenure'], bins=[0, 12, 24, df2['tenure'].max()], labels=['0-12', '12-24', '24+'])
|
46 |
+
|
47 |
+
# One-hot encoding
|
48 |
+
multi_cat_cols = ['MultipleLines', 'InternetService', 'OnlineSecurity', 'OnlineBackup',
|
49 |
+
'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies',
|
50 |
+
'Contract', 'PaymentMethod', 'tenure_bin']
|
51 |
+
df_dummies = pd.get_dummies(df2, columns=multi_cat_cols, drop_first=False)
|
52 |
+
|
53 |
+
# Uzun vadeli sözleşme özelliği
|
54 |
+
df_dummies['is_long_term_contract'] = (
|
55 |
+
df_dummies.get('Contract_One year', False) | df_dummies.get('Contract_Two year', False)
|
56 |
+
)
|
57 |
+
|
58 |
+
# Hedef ve bağımsız değişkenler
|
59 |
+
y = df_dummies['Churn'].values
|
60 |
+
X = df_dummies.drop(columns=['Churn'])
|
61 |
+
|
62 |
+
# En önemli 15 özelliği seçmek için XGB fit
|
63 |
+
temp_model = XGBClassifier(random_state=42)
|
64 |
+
temp_model.fit(X, y)
|
65 |
+
feature_importances = pd.Series(temp_model.feature_importances_, index=X.columns).sort_values(ascending=False)
|
66 |
+
top_15_features = feature_importances.head(15).index.tolist()
|
67 |
+
X_selected = X[top_15_features]
|
68 |
+
|
69 |
+
# Pipeline: Ölçekleme + SMOTE + XGBoost
|
70 |
+
pipe = Pipeline([
|
71 |
+
("scaler", MinMaxScaler()),
|
72 |
+
("smote", SMOTE(sampling_strategy=1.0, random_state=42)),
|
73 |
+
("xgb", XGBClassifier(
|
74 |
+
n_estimators=100,
|
75 |
+
max_depth=4,
|
76 |
+
learning_rate=0.1,
|
77 |
+
subsample=1.0,
|
78 |
+
colsample_bytree=0.7,
|
79 |
+
scale_pos_weight=1,
|
80 |
+
eval_metric='logloss',
|
81 |
+
random_state=42
|
82 |
+
))
|
83 |
+
])
|
84 |
+
|
85 |
+
# 5-fold stratified cross-validation
|
86 |
+
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
|
87 |
+
y_pred = cross_val_predict(pipe, X_selected, y, cv=cv, method="predict")
|
88 |
+
y_proba = cross_val_predict(pipe, X_selected, y, cv=cv, method="predict_proba")[:, 1]
|
89 |
+
|
90 |
+
# Performans metrikleri
|
91 |
+
print("XGBoost Cross-Validation Sonuçları (Binary):")
|
92 |
+
print("--------------------------------------------")
|
93 |
+
print("Accuracy: {:.4f}".format(accuracy_score(y, y_pred)))
|
94 |
+
print("Precision: {:.4f}".format(precision_score(y, y_pred)))
|
95 |
+
print("Recall: {:.4f}".format(recall_score(y, y_pred)))
|
96 |
+
print("F1 Score: {:.4f}".format(f1_score(y, y_pred)))
|
97 |
+
print("ROC-AUC: {:.4f}".format(roc_auc_score(y, y_proba)))
|
98 |
+
print("")
|
99 |
+
print("XGBoost Cross-Validation Sonuçları (Macro):")
|
100 |
+
print("--------------------------------------------")
|
101 |
+
print("Accuracy: {:.4f}".format(accuracy_score(y, y_pred)))
|
102 |
+
print("Precision: {:.4f}".format(precision_score(y, y_pred, average='macro')))
|
103 |
+
print("Recall: {:.4f}".format(recall_score(y, y_pred, average='macro')))
|
104 |
+
print("F1 Score: {:.4f}".format(f1_score(y, y_pred, average='macro')))
|
105 |
+
print("ROC-AUC: {:.4f}".format(roc_auc_score(y, y_proba)))
|
106 |
+
|
107 |
+
# Modeli eğit ve kaydet
|
108 |
+
pipe.fit(X_selected, y)
|
109 |
+
joblib.dump(pipe, 'churn_model.pkl')
|
110 |
+
joblib.dump(top_15_features, "model_features.pkl")
|
deneme.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import joblib
|
2 |
+
import pandas as pd
|
3 |
+
|
4 |
+
# 🔁 Kaydedilen modeli ve özellik listesini yükle
|
5 |
+
model = joblib.load("churn_model.pkl")
|
6 |
+
model_features = joblib.load("model_features.pkl")
|
7 |
+
|
8 |
+
# 🔍 Müşteri verisi (güncel örneğe göre)
|
9 |
+
customer = {
|
10 |
+
'gender': True, # Female → True
|
11 |
+
'SeniorCitizen': False,
|
12 |
+
'Partner': True,
|
13 |
+
'Dependents': False,
|
14 |
+
'PhoneService': True,
|
15 |
+
'PaperlessBilling': True,
|
16 |
+
'tenure': 28,
|
17 |
+
'MonthlyCharges': 104.8,
|
18 |
+
'TotalCharges': 3046.05,
|
19 |
+
|
20 |
+
'MultipleLines_No': False,
|
21 |
+
'MultipleLines_No phone service': False,
|
22 |
+
'MultipleLines_Yes': True,
|
23 |
+
|
24 |
+
'InternetService_DSL': False,
|
25 |
+
'InternetService_Fiber optic': True,
|
26 |
+
'InternetService_No': False,
|
27 |
+
|
28 |
+
'OnlineSecurity_No': True,
|
29 |
+
'OnlineSecurity_Yes': False,
|
30 |
+
'OnlineSecurity_No internet service': False,
|
31 |
+
|
32 |
+
'OnlineBackup_No': True,
|
33 |
+
'OnlineBackup_Yes': False,
|
34 |
+
'OnlineBackup_No internet service': False,
|
35 |
+
|
36 |
+
'DeviceProtection_No': False,
|
37 |
+
'DeviceProtection_Yes': True,
|
38 |
+
'DeviceProtection_No internet service': False,
|
39 |
+
|
40 |
+
'TechSupport_No': False,
|
41 |
+
'TechSupport_Yes': True,
|
42 |
+
'TechSupport_No internet service': False,
|
43 |
+
|
44 |
+
'StreamingTV_No': False,
|
45 |
+
'StreamingTV_Yes': True,
|
46 |
+
'StreamingTV_No internet service': False,
|
47 |
+
|
48 |
+
'StreamingMovies_No': False,
|
49 |
+
'StreamingMovies_Yes': True,
|
50 |
+
'StreamingMovies_No internet service': False,
|
51 |
+
|
52 |
+
'Contract_Month-to-month': True,
|
53 |
+
'Contract_One year': False,
|
54 |
+
'Contract_Two year': False,
|
55 |
+
|
56 |
+
'PaymentMethod_Bank transfer (automatic)': False,
|
57 |
+
'PaymentMethod_Credit card (automatic)': False,
|
58 |
+
'PaymentMethod_Electronic check': True,
|
59 |
+
'PaymentMethod_Mailed check': False
|
60 |
+
}
|
61 |
+
|
62 |
+
# Eksik kalan tüm özellikleri sıfırla
|
63 |
+
full_input = {col: customer.get(col, 0) for col in model_features}
|
64 |
+
X_test = pd.DataFrame([full_input])
|
65 |
+
|
66 |
+
# 🎯 Tahmin ve olasılık
|
67 |
+
prediction = model.predict(X_test)[0]
|
68 |
+
proba = model.predict_proba(X_test)[0][1]
|
69 |
+
|
70 |
+
# 📝 Sonuç
|
71 |
+
label = "CHURN edecek" if prediction == 1 else "Kalacak"
|
72 |
+
print(f"📊 Tahmin: {label}")
|
73 |
+
print(f"🎯 Churn olasılığı: %{proba * 100:.2f}")
|
gradio_app.py
ADDED
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import joblib
|
3 |
+
import pandas as pd
|
4 |
+
import numpy as np
|
5 |
+
from sklearn.metrics.pairwise import euclidean_distances
|
6 |
+
import random
|
7 |
+
|
8 |
+
model = joblib.load("churn_model.pkl")
|
9 |
+
model_features = joblib.load("model_features.pkl")
|
10 |
+
|
11 |
+
# Load the customer data
|
12 |
+
customer_df = pd.read_csv("Telco-Customer-Churn.csv")
|
13 |
+
|
14 |
+
customer_df['MonthlyCharges'] = pd.to_numeric(customer_df['MonthlyCharges'], errors='coerce').fillna(0)
|
15 |
+
customer_df['TotalCharges'] = pd.to_numeric(customer_df['TotalCharges'], errors='coerce').fillna(0)
|
16 |
+
customer_df['tenure'] = pd.to_numeric(customer_df['tenure'], errors='coerce').fillna(0)
|
17 |
+
|
18 |
+
value_map = {
|
19 |
+
"Aydan aya": "Month-to-month",
|
20 |
+
"1 yıllık": "One year",
|
21 |
+
"2 yıllık": "Two year",
|
22 |
+
"Elektronik çek": "Electronic check",
|
23 |
+
"Posta çeki": "Mailed check",
|
24 |
+
"Banka havalesi (otomatik)": "Bank transfer (automatic)",
|
25 |
+
"Kredi kartı (otomatik)": "Credit card (automatic)",
|
26 |
+
"Hayır": "No",
|
27 |
+
"Evet": "Yes",
|
28 |
+
"Yok": "No internet service",
|
29 |
+
"Telefon hizmeti yok": "No phone service",
|
30 |
+
"Fiber optik": "Fiber optic",
|
31 |
+
"Erkek": "Male",
|
32 |
+
"Kadın": "Female"
|
33 |
+
}
|
34 |
+
|
35 |
+
def customer_to_features(row):
|
36 |
+
# Build a feature dict for a customer row, using the same logic as predict_churn
|
37 |
+
input_dict = {}
|
38 |
+
tenure = row['tenure']
|
39 |
+
monthly = row['MonthlyCharges']
|
40 |
+
total = row['TotalCharges']
|
41 |
+
input_dict["tenure"] = tenure
|
42 |
+
input_dict["PhoneService"] = row['PhoneService'] == "Yes"
|
43 |
+
input_dict["avg_charge_per_month"] = total / tenure if tenure > 0 else 0
|
44 |
+
input_dict["charge_ratio"] = total / (monthly * tenure) if monthly > 0 and tenure > 0 else 1
|
45 |
+
tenure_label = "0-12" if tenure <= 12 else "12-24" if tenure <= 24 else "24+"
|
46 |
+
for bin_label in ["0-12", "12-24", "24+"]:
|
47 |
+
input_dict[f"tenure_bin_{bin_label}"] = (tenure_label == bin_label)
|
48 |
+
contract_value = row['Contract']
|
49 |
+
input_dict["is_long_term_contract"] = contract_value in ["One year", "Two year"]
|
50 |
+
categorical_fields = [
|
51 |
+
"gender", "SeniorCitizen", "Partner", "Dependents", "PaperlessBilling",
|
52 |
+
"MultipleLines", "InternetService", "OnlineSecurity", "OnlineBackup",
|
53 |
+
"DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies",
|
54 |
+
"Contract", "PaymentMethod"
|
55 |
+
]
|
56 |
+
for field in categorical_fields:
|
57 |
+
raw_value = row[field]
|
58 |
+
mapped_value = value_map.get(raw_value, raw_value)
|
59 |
+
for col in model_features:
|
60 |
+
if col.startswith(f"{field}_"):
|
61 |
+
input_dict[col] = (col == f"{field}_{mapped_value}")
|
62 |
+
for col in model_features:
|
63 |
+
if col not in input_dict:
|
64 |
+
input_dict[col] = 0 if col == "tenure" or "charge" in col or "avg" in col else False
|
65 |
+
return [input_dict[col] for col in model_features]
|
66 |
+
|
67 |
+
# Precompute all customer feature vectors
|
68 |
+
customer_feature_matrix = np.vstack([customer_to_features(row) for _, row in customer_df.iterrows()])
|
69 |
+
|
70 |
+
def autofill_random_customer():
|
71 |
+
row = customer_df.sample(1).iloc[0]
|
72 |
+
# Map English values back to Turkish for dropdowns
|
73 |
+
reverse_map = {v: k for k, v in value_map.items()}
|
74 |
+
def rev(val):
|
75 |
+
return reverse_map.get(val, val)
|
76 |
+
# Ensure dropdown values are valid
|
77 |
+
def safe(val, allowed):
|
78 |
+
v = rev(val)
|
79 |
+
return v if v in allowed else allowed[0]
|
80 |
+
return [
|
81 |
+
float(row['tenure']),
|
82 |
+
float(row['MonthlyCharges']),
|
83 |
+
float(row['TotalCharges']),
|
84 |
+
safe(row['PhoneService'], phone_service_options),
|
85 |
+
safe(row['gender'], gender_options),
|
86 |
+
'Evet' if row['SeniorCitizen'] == 1 else 'Hayır',
|
87 |
+
'Evet' if row['Partner'] == 'Yes' else 'Hayır',
|
88 |
+
'Evet' if row['Dependents'] == 'Yes' else 'Hayır',
|
89 |
+
'Evet' if row['PaperlessBilling'] == 'Yes' else 'Hayır',
|
90 |
+
safe(row['MultipleLines'], multiple_lines_options),
|
91 |
+
safe(row['InternetService'], internet_service_options),
|
92 |
+
safe(row['OnlineSecurity'], online_security_options),
|
93 |
+
safe(row['OnlineBackup'], online_backup_options),
|
94 |
+
safe(row['DeviceProtection'], device_protection_options),
|
95 |
+
safe(row['TechSupport'], tech_support_options),
|
96 |
+
safe(row['StreamingTV'], streaming_tv_options),
|
97 |
+
safe(row['StreamingMovies'], streaming_movies_options),
|
98 |
+
safe(row['Contract'], contract_options),
|
99 |
+
safe(row['PaymentMethod'], payment_method_options)
|
100 |
+
]
|
101 |
+
|
102 |
+
def find_similar_customers_vector(input_vector, n=5):
|
103 |
+
dists = euclidean_distances(customer_feature_matrix, input_vector.reshape(1, -1)).flatten()
|
104 |
+
top_idx = np.argsort(dists)[:n]
|
105 |
+
print("Top distances:", dists[top_idx])
|
106 |
+
print("Top indices:", top_idx)
|
107 |
+
return customer_df.iloc[top_idx][['customerID','gender','SeniorCitizen','Partner','Dependents','tenure','Contract','PaymentMethod','MonthlyCharges','TotalCharges','Churn']]
|
108 |
+
|
109 |
+
def predict_churn(
|
110 |
+
tenure, monthly, total, PhoneService, gender, SeniorCitizen, Partner, Dependents, PaperlessBilling,
|
111 |
+
MultipleLines, InternetService, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport,
|
112 |
+
StreamingTV, StreamingMovies, Contract, PaymentMethod
|
113 |
+
):
|
114 |
+
# Ensure numeric types
|
115 |
+
tenure = float(tenure)
|
116 |
+
monthly = float(monthly)
|
117 |
+
total = float(total)
|
118 |
+
input_dict = {}
|
119 |
+
input_dict["tenure"] = tenure
|
120 |
+
input_dict["PhoneService"] = PhoneService == "Evet"
|
121 |
+
input_dict["avg_charge_per_month"] = total / tenure if tenure > 0 else 0
|
122 |
+
input_dict["charge_ratio"] = total / (monthly * tenure) if monthly > 0 and tenure > 0 else 1
|
123 |
+
tenure_label = "0-12" if tenure <= 12 else "12-24" if tenure <= 24 else "24+"
|
124 |
+
for bin_label in ["0-12", "12-24", "24+"]:
|
125 |
+
input_dict[f"tenure_bin_{bin_label}"] = (tenure_label == bin_label)
|
126 |
+
contract_value = value_map.get(Contract, Contract)
|
127 |
+
input_dict["is_long_term_contract"] = contract_value in ["One year", "Two year"]
|
128 |
+
categorical_fields = [
|
129 |
+
"gender", "SeniorCitizen", "Partner", "Dependents", "PaperlessBilling",
|
130 |
+
"MultipleLines", "InternetService", "OnlineSecurity", "OnlineBackup",
|
131 |
+
"DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies",
|
132 |
+
"Contract", "PaymentMethod"
|
133 |
+
]
|
134 |
+
form = {
|
135 |
+
"gender": gender,
|
136 |
+
"SeniorCitizen": SeniorCitizen,
|
137 |
+
"Partner": Partner,
|
138 |
+
"Dependents": Dependents,
|
139 |
+
"PaperlessBilling": PaperlessBilling,
|
140 |
+
"MultipleLines": MultipleLines,
|
141 |
+
"InternetService": InternetService,
|
142 |
+
"OnlineSecurity": OnlineSecurity,
|
143 |
+
"OnlineBackup": OnlineBackup,
|
144 |
+
"DeviceProtection": DeviceProtection,
|
145 |
+
"TechSupport": TechSupport,
|
146 |
+
"StreamingTV": StreamingTV,
|
147 |
+
"StreamingMovies": StreamingMovies,
|
148 |
+
"Contract": Contract,
|
149 |
+
"PaymentMethod": PaymentMethod
|
150 |
+
}
|
151 |
+
for field in categorical_fields:
|
152 |
+
raw_value = form[field]
|
153 |
+
mapped_value = value_map.get(raw_value, raw_value)
|
154 |
+
for col in model_features:
|
155 |
+
if col.startswith(f"{field}_"):
|
156 |
+
input_dict[col] = (col == f"{field}_{mapped_value}")
|
157 |
+
for col in model_features:
|
158 |
+
if col not in input_dict:
|
159 |
+
input_dict[col] = 0 if col == "tenure" or "charge" in col or "avg" in col else False
|
160 |
+
input_df = pd.DataFrame([[input_dict[col] for col in model_features]], columns=model_features)
|
161 |
+
prediction = model.predict_proba(input_df)[0][1]
|
162 |
+
score = round(prediction * 100, 2)
|
163 |
+
if score >= 50:
|
164 |
+
comment = "Müşteri Kaybedilebilir."
|
165 |
+
else:
|
166 |
+
comment = "Müşteri Kayıp Riski Taşımıyor."
|
167 |
+
result = f"Churn Riski: %{score} — {comment}"
|
168 |
+
# Vector similarity
|
169 |
+
similar_customers = find_similar_customers_vector(input_df.values[0], n=5)
|
170 |
+
return result, similar_customers
|
171 |
+
|
172 |
+
# Define options for dropdowns (Turkish values)
|
173 |
+
phone_service_options = ["Evet", "Hayır"]
|
174 |
+
gender_options = ["Erkek", "Kadın"]
|
175 |
+
senior_citizen_options = ["Evet", "Hayır"]
|
176 |
+
partner_options = ["Evet", "Hayır"]
|
177 |
+
dependents_options = ["Evet", "Hayır"]
|
178 |
+
paperless_billing_options = ["Evet", "Hayır"]
|
179 |
+
multiple_lines_options = ["Hayır", "Evet", "Telefon hizmeti yok"]
|
180 |
+
internet_service_options = ["DSL", "Fiber optik", "Yok"]
|
181 |
+
online_security_options = ["Hayır", "Evet", "Yok"]
|
182 |
+
online_backup_options = ["Hayır", "Evet", "Yok"]
|
183 |
+
device_protection_options = ["Hayır", "Evet", "Yok"]
|
184 |
+
tech_support_options = ["Hayır", "Evet", "Yok"]
|
185 |
+
streaming_tv_options = ["Hayır", "Evet", "Yok"]
|
186 |
+
streaming_movies_options = ["Hayır", "Evet", "Yok"]
|
187 |
+
contract_options = ["Aydan aya", "1 yıllık", "2 yıllık"]
|
188 |
+
payment_method_options = [
|
189 |
+
"Elektronik çek", "Posta çeki", "Banka havalesi (otomatik)", "Kredi kartı (otomatik)"
|
190 |
+
]
|
191 |
+
|
192 |
+
with gr.Blocks() as demo:
|
193 |
+
gr.Markdown("# Müşteri Churn Tahmini")
|
194 |
+
with gr.Row():
|
195 |
+
tenure = gr.Number(label="Kullanım Süresi (tenure)", value=1)
|
196 |
+
monthly = gr.Number(label="Aylık Ücret (MonthlyCharges)", value=1)
|
197 |
+
total = gr.Number(label="Toplam Ücret (TotalCharges)", value=1)
|
198 |
+
with gr.Row():
|
199 |
+
PhoneService = gr.Dropdown(phone_service_options, label="Telefon Hizmeti (PhoneService)")
|
200 |
+
gender = gr.Dropdown(gender_options, label="Cinsiyet (gender)")
|
201 |
+
SeniorCitizen = gr.Dropdown(senior_citizen_options, label="Kıdemli Vatandaş (SeniorCitizen)")
|
202 |
+
Partner = gr.Dropdown(partner_options, label="Partner")
|
203 |
+
Dependents = gr.Dropdown(dependents_options, label="Bağımlılar (Dependents)")
|
204 |
+
PaperlessBilling = gr.Dropdown(paperless_billing_options, label="Kağıtsız Fatura (PaperlessBilling)")
|
205 |
+
with gr.Row():
|
206 |
+
MultipleLines = gr.Dropdown(multiple_lines_options, label="Çoklu Hat (MultipleLines)")
|
207 |
+
InternetService = gr.Dropdown(internet_service_options, label="İnternet Servisi (InternetService)")
|
208 |
+
OnlineSecurity = gr.Dropdown(online_security_options, label="Online Güvenlik (OnlineSecurity)")
|
209 |
+
OnlineBackup = gr.Dropdown(online_backup_options, label="Online Yedekleme (OnlineBackup)")
|
210 |
+
DeviceProtection = gr.Dropdown(device_protection_options, label="Cihaz Koruma (DeviceProtection)")
|
211 |
+
TechSupport = gr.Dropdown(tech_support_options, label="Teknik Destek (TechSupport)")
|
212 |
+
StreamingTV = gr.Dropdown(streaming_tv_options, label="TV Yayını (StreamingTV)")
|
213 |
+
StreamingMovies = gr.Dropdown(streaming_movies_options, label="Film Yayını (StreamingMovies)")
|
214 |
+
with gr.Row():
|
215 |
+
Contract = gr.Dropdown(contract_options, label="Sözleşme (Contract)")
|
216 |
+
PaymentMethod = gr.Dropdown(payment_method_options, label="Ödeme Yöntemi (PaymentMethod)")
|
217 |
+
autofill_btn = gr.Button("Rastgele Müşteri ile Doldur")
|
218 |
+
submit_btn = gr.Button("Tahmin Et")
|
219 |
+
output = gr.Textbox(label="Sonuç")
|
220 |
+
similar_customers_table = gr.Dataframe(label="Benzer Müşteriler (İlk 5)")
|
221 |
+
autofill_btn.click(
|
222 |
+
autofill_random_customer,
|
223 |
+
inputs=[],
|
224 |
+
outputs=[tenure, monthly, total, PhoneService, gender, SeniorCitizen, Partner, Dependents, PaperlessBilling,
|
225 |
+
MultipleLines, InternetService, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport,
|
226 |
+
StreamingTV, StreamingMovies, Contract, PaymentMethod]
|
227 |
+
)
|
228 |
+
submit_btn.click(
|
229 |
+
predict_churn,
|
230 |
+
inputs=[tenure, monthly, total, PhoneService, gender, SeniorCitizen, Partner, Dependents, PaperlessBilling,
|
231 |
+
MultipleLines, InternetService, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport,
|
232 |
+
StreamingTV, StreamingMovies, Contract, PaymentMethod],
|
233 |
+
outputs=[output, similar_customers_table]
|
234 |
+
)
|
235 |
+
|
236 |
+
if __name__ == "__main__":
|
237 |
+
demo.launch()
|
model_features.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:fc7c1d59e2bd2dfce4329ee3949ad58f3e2723bc32b8263cc1a81bec702d4b6b
|
3 |
+
size 324
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio
|
2 |
+
Flask
|
3 |
+
pandas
|
4 |
+
joblib
|
5 |
+
scikit-learn
|
6 |
+
xgboost
|
7 |
+
imbalanced-learn
|
8 |
+
matplotlib
|
9 |
+
seaborn
|
10 |
+
numpy
|
templates/index.html
ADDED
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="tr">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<title>Müşteri Churn Tahmini</title>
|
6 |
+
<style>
|
7 |
+
body {
|
8 |
+
background-color: #d9dafa;
|
9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
10 |
+
color: #564dbf;
|
11 |
+
padding: 40px;
|
12 |
+
}
|
13 |
+
|
14 |
+
h2 {
|
15 |
+
color: #1a1636;
|
16 |
+
}
|
17 |
+
|
18 |
+
form {
|
19 |
+
background-color: #c2baff;
|
20 |
+
border: 2px solid #8780bf;
|
21 |
+
border-radius: 12px;
|
22 |
+
padding: 30px;
|
23 |
+
width: 450px;
|
24 |
+
box-shadow: 2px 2px 10px #3c30e3;
|
25 |
+
}
|
26 |
+
|
27 |
+
label {
|
28 |
+
display: block;
|
29 |
+
margin-top: 12px;
|
30 |
+
font-weight: bold;
|
31 |
+
}
|
32 |
+
|
33 |
+
input, select {
|
34 |
+
width: 100%;
|
35 |
+
padding: 8px;
|
36 |
+
margin-top: 5px;
|
37 |
+
border: 1px solid #564dbf;
|
38 |
+
border-radius: 6px;
|
39 |
+
}
|
40 |
+
|
41 |
+
input[type="submit"] {
|
42 |
+
background-color: #7791d9;
|
43 |
+
color: white;
|
44 |
+
font-weight: bold;
|
45 |
+
cursor: pointer;
|
46 |
+
margin-top: 20px;
|
47 |
+
}
|
48 |
+
|
49 |
+
input[type="submit"]:hover {
|
50 |
+
background-color: #1a1636;
|
51 |
+
}
|
52 |
+
|
53 |
+
h3 {
|
54 |
+
margin-top: 30px;
|
55 |
+
color: #1a1636;
|
56 |
+
}
|
57 |
+
</style>
|
58 |
+
</head>
|
59 |
+
<body>
|
60 |
+
<h2>Müşteri Bilgileri</h2>
|
61 |
+
<form method="POST">
|
62 |
+
<label>Cinsiyet:</label>
|
63 |
+
<select name="gender"><option>Kadın</option><option>Erkek</option></select>
|
64 |
+
|
65 |
+
<label>65 yaş ve üstü mü?</label>
|
66 |
+
<select name="SeniorCitizen"><option>Hayır</option><option>Evet</option></select>
|
67 |
+
|
68 |
+
<label>Evli mi?</label>
|
69 |
+
<select name="Partner"><option>Hayır</option><option>Evet</option></select>
|
70 |
+
|
71 |
+
<label>Bakmakla yükümlü olduğu kişi var mı?</label>
|
72 |
+
<select name="Dependents"><option>Hayır</option><option>Evet</option></select>
|
73 |
+
|
74 |
+
<label>Telefon hizmeti var mı?</label>
|
75 |
+
<select name="PhoneService"><option>Hayır</option><option>Evet</option></select>
|
76 |
+
|
77 |
+
<label>Kağıtsız fatura kullanıyor mu?</label>
|
78 |
+
<select name="PaperlessBilling"><option>Hayır</option><option>Evet</option></select>
|
79 |
+
|
80 |
+
<label>Abonelik süresi (ay):</label>
|
81 |
+
<input type="number" step="1" name="tenure" value="0" required>
|
82 |
+
|
83 |
+
<label>Aylık ücret ($):</label>
|
84 |
+
<input type="number" step="0.01" name="MonthlyCharges" value="0.00" required>
|
85 |
+
|
86 |
+
<label>Toplam ücret ($):</label>
|
87 |
+
<input type="number" step="0.01" name="TotalCharges" value="0.00" required>
|
88 |
+
|
89 |
+
<label>Çoklu hat durumu:</label>
|
90 |
+
<select name="MultipleLines">
|
91 |
+
<option>Hayır</option><option>Evet</option><option>Telefon hizmeti yok</option>
|
92 |
+
</select>
|
93 |
+
|
94 |
+
<label>İnternet hizmeti:</label>
|
95 |
+
<select name="InternetService">
|
96 |
+
<option>DSL</option><option>Fiber optik</option><option>Yok</option>
|
97 |
+
</select>
|
98 |
+
|
99 |
+
<label>Online güvenlik:</label>
|
100 |
+
<select name="OnlineSecurity">
|
101 |
+
<option>Hayır</option><option>Evet</option><option>İnternet hizmeti yok</option>
|
102 |
+
</select>
|
103 |
+
|
104 |
+
<label>Online yedekleme:</label>
|
105 |
+
<select name="OnlineBackup">
|
106 |
+
<option>Hayır</option><option>Evet</option><option>İnternet hizmeti yok</option>
|
107 |
+
</select>
|
108 |
+
|
109 |
+
<label>Cihaz koruma:</label>
|
110 |
+
<select name="DeviceProtection">
|
111 |
+
<option>Hayır</option><option>Evet</option><option>İnternet hizmeti yok</option>
|
112 |
+
</select>
|
113 |
+
|
114 |
+
<label>Teknik destek:</label>
|
115 |
+
<select name="TechSupport">
|
116 |
+
<option>Hayır</option><option>Evet</option><option>İnternet hizmeti yok</option>
|
117 |
+
</select>
|
118 |
+
|
119 |
+
<label>TV yayını:</label>
|
120 |
+
<select name="StreamingTV">
|
121 |
+
<option>Hayır</option><option>Evet</option><option>İnternet hizmeti yok</option>
|
122 |
+
</select>
|
123 |
+
|
124 |
+
<label>Film yayını:</label>
|
125 |
+
<select name="StreamingMovies">
|
126 |
+
<option>Hayır</option><option>Evet</option><option>İnternet hizmeti yok</option>
|
127 |
+
</select>
|
128 |
+
|
129 |
+
<label>Abonelik türü:</label>
|
130 |
+
<select name="Contract">
|
131 |
+
<option>Aydan aya</option><option>1 yıllık</option><option>2 yıllık</option>
|
132 |
+
</select>
|
133 |
+
|
134 |
+
<label>Ödeme yöntemi:</label>
|
135 |
+
<select name="PaymentMethod">
|
136 |
+
<option>Elektronik çek</option>
|
137 |
+
<option>Posta çeki</option>
|
138 |
+
<option>Banka havalesi (otomatik)</option>
|
139 |
+
<option>Kredi kartı (otomatik)</option>
|
140 |
+
</select>
|
141 |
+
|
142 |
+
<input type="submit" value="Tahmin Et">
|
143 |
+
</form>
|
144 |
+
|
145 |
+
{% if result %}
|
146 |
+
<h3>{{ result }}</h3>
|
147 |
+
{% endif %}
|
148 |
+
</body>
|
149 |
+
</html>
|