تحلیل و پیشبینی نتایج مسابقات فوتبال باشگاهی با استفاده از الگوریتمهای یادگیری ماشین
پیشبینی نتایج مسابقات فوتبال یکی از چالشهای جذاب و پیچیده در حوزه یادگیری ماشین و تحلیل دادههای ورزشی است. این پروژه با هدف پیشبینی نتیجه نهایی مسابقات فوتبال باشگاهی (برد، باخت یا تساوی) با استفاده از دادههای تاریخی مسابقات از سال ۲۰۰۰ تا ۲۰۲۵ طراحی شده است.
پیشبینی نتایج مسابقات ورزشی کاربردهای گستردهای دارد:
تحلیل استراتژیک رقبا و برنامهریزی بهتر
درک عمیقتر از عوامل تاثیرگذار بر نتایج
تعیین ضرایب دقیقتر و مدیریت ریسک
دریافت دیتاست از Kaggle و آمادهسازی محیط کاری
بررسی ساختار، کیفیت و توزیع دادهها
حذف دادههای ناقص و غیرضروری
ساخت ویژگیهای جدید و موثر
آموزش سه مدل مختلف یادگیری ماشین
تحلیل عملکرد و انتخاب بهترین مدل
این پروژه از سه الگوریتم پیشرفته یادگیری ماشین استفاده میکند: Random Forest، XGBoost و Neural Network. هر یک از این الگوریتمها مزایا و معایب خاص خود را دارند که در ادامه به تفصیل بررسی خواهند شد.
دیتاست مورد استفاده در این پروژه با نام "Club Football Match Data 2000-2025" از پلتفرم Kaggle تهیه شده است. این مجموعه داده شامل اطلاعات جامع مسابقات فوتبال باشگاهی در لیگهای مختلف اروپایی از سال ۲۰۰۰ تا ۲۰۲۵ است.
دیتاست شامل ۴۸ ستون (ویژگی) است که میتوان آنها را به دستههای زیر تقسیم کرد:
| نام ستون | توضیحات | نوع داده |
|---|---|---|
Division |
نام لیگ (مثلاً F1 برای لیگ اول فرانسه) | Object |
MatchDate |
تاریخ برگزاری مسابقه | Object |
MatchTime |
ساعت برگزاری مسابقه | Object |
HomeTeam |
نام تیم میزبان | Object |
AwayTeam |
نام تیم مهمان | Object |
سیستم Elo یک روش ریاضی برای محاسبه قدرت نسبی بازیکنان یا تیمها است که در شطرنج توسعه یافته و به ورزشهای دیگر تعمیم داده شده است. امتیاز بالاتر نشاندهنده عملکرد بهتر در بازیهای گذشته است.
| نام ستون | توضیحات | دامنه معمول |
|---|---|---|
HomeElo |
امتیاز Elo تیم میزبان قبل از مسابقه | ۱۱۰۳ تا ۲۱۰۷ |
AwayElo |
امتیاز Elo تیم مهمان قبل از مسابقه | ۱۱۰۳ تا ۲۱۰۷ |
این ستونها نشاندهنده عملکرد تیمها در بازیهای اخیر (۳ یا ۵ مسابقه گذشته) هستند:
| نام ستون | توضیحات | نحوه محاسبه |
|---|---|---|
Form3Home |
امتیازات تیم میزبان در ۳ بازی اخیر | برد=۳، تساوی=۱، باخت=۰ |
Form5Home |
امتیازات تیم میزبان در ۵ بازی اخیر | برد=۳، تساوی=۱، باخت=۰ |
Form3Away |
امتیازات تیم مهمان در ۳ بازی اخیر | برد=۳، تساوی=۱، باخت=۰ |
Form5Away |
امتیازات تیم مهمان در ۵ بازی اخیر | برد=۳، تساوی=۱، باخت=۰ |
| نام ستون | توضیحات |
|---|---|
FTHome |
تعداد گلهای تیم میزبان در پایان بازی |
FTAway |
تعداد گلهای تیم مهمان در پایان بازی |
FTResult |
نتیجه نهایی: H (برد میزبان)، A (برد مهمان)، D (تساوی) |
HTHome |
تعداد گلهای تیم میزبان در نیمه اول |
HTAway |
تعداد گلهای تیم مهمان در نیمه اول |
HTResult |
نتیجه نیمه اول: H، A یا D |
اطلاعات تفصیلی درباره روند بازی:
| دسته | ستونهای مربوطه | توضیحات |
|---|---|---|
| شوتها | HomeShots, AwayShots |
تعداد کل شوتهای هر تیم |
| شوت به چارچوب | HomeTarget, AwayTarget |
تعداد شوتهای به سمت دروازه |
| خطاها | HomeFouls, AwayFouls |
تعداد خطاهای ارتکابی |
| کرنرها | HomeCorners, AwayCorners |
تعداد ضربات کرنر |
| کارتها | HomeYellow, AwayYellow, HomeRed, AwayRed
|
تعداد کارتهای زرد و قرمز |
ضرایب شرطبندی نشاندهنده احتمال پیروزی هر تیم از دیدگاه بوکمیکرها (شرکتهای شرطبندی) هستند. ضریب پایینتر معمولاً نشاندهنده احتمال بیشتر برای وقوع آن نتیجه است.
| دسته | ستونهای مربوطه |
|---|---|
| ضرایب متوسط | OddHome, OddDraw, OddAway |
| بیشترین ضرایب | MaxHome, MaxDraw, MaxAway |
| ضرایب تعداد گل | Over25, Under25, MaxOver25, MaxUnder25
|
| هندیکپ | HandiSize, HandiHome, HandiAway |
احتمالات مختلف برای رویدادهای درون بازی (با پیشوند C_):
C_LTH: احتمال برد تیم میزبان با اختلاف زیادC_LTA: احتمال برد تیم مهمان با اختلاف زیادC_VHD: احتمال برد خیلی بزرگ تیم میزبانC_VAD: احتمال برد خیلی بزرگ تیم مهمانC_HTB: احتمال گل زدن در نیمه اولC_PHB: احتمال پنالتی در بازیبرای درک بهتر ساختار داده، نمونهای از ۵ رکورد اول دیتاست:
Division MatchDate HomeTeam AwayTeam HomeElo AwayElo FTHome FTAway FTResult
0 F1 2000-07-28 Marseille Troyes 1686.34 1586.57 2.0 1.0 H
1 F1 2000-07-28 Paris SG Strasbourg 1714.89 1642.51 3.0 1.0 H
2 F2 2000-07-28 Wasquehal Nancy 1465.08 1633.80 1.0 2.0 A
3 F1 2000-07-29 Auxerre Sedan 1635.58 1624.22 0.0 0.0 D
4 F1 2000-07-29 Bordeaux Metz 1734.34 1673.11 2.0 0.0 H
مسابقه فوتبال
ستون مختلف
همانطور که در بخش بعدی خواهیم دید، بسیاری از ستونها دارای مقادیر گمشده (NULL) هستند. این موضوع نیاز
به پیشپردازش دقیق دارد. برخی ستونها مانند MatchTime بیش از ۵۰٪ داده گمشده دارند و
برخی آمارهای بازی تنها برای بازیهای اخیر موجود هستند.
در ابتدا باید کتابخانه kagglehub را برای دسترسی به دیتاستهای Kaggle وارد کنیم:
import kagglehub
# دانلود آخرین نسخه دیتاست
path = kagglehub.dataset_download("adamgbor/club-football-match-data-2000-2025")
print("Path to dataset files:", path)
kagglehub که امکان دانلود مستقیم
دیتاستها از Kaggle را فراهم میکندdataset_download() با استفاده از شناسه دیتاست،
فایلها را دانلود کرده و مسیر محلی آنها را برمیگرداند~/.cache/kagglehub/)
Warning: Looks like you're using an outdated `kagglehub` version (installed: 0.3.13)
Downloading from https://www.kaggle.com/api/v1/datasets/download/...
100%|██████████| 14.4M/14.4M [00:00<00:00, 51.5MB/s]
Extracting files...
Path to dataset files: /root/.cache/kagglehub/datasets/adamgbor/club-football-match-data-2000-2025/versions/3
کتابخانه kagglehub به صورت خودکار فایل را دانلود و استخراج میکند. حجم فایل حدود ۱۴.۴
مگابایت است که با سرعت بالا (۵۱.۵ MB/s) دانلود شده است.
برای سهولت دسترسی، فایلهای دانلود شده را به پوشهای مشخص منتقل میکنیم:
import shutil
import os
# تعریف پوشه مقصد
target_dir = "/content/ST"
os.makedirs(target_dir, exist_ok=True)
# انتقال تمام فایلها
for file_name in os.listdir(path):
full_file_name = os.path.join(path, file_name)
if os.path.isfile(full_file_name):
shutil.move(full_file_name, target_dir)
print("Files moved to:", target_dir)
shutil (برای عملیات فایل) و
os (برای مدیریت مسیرها)
/content/ST (اگر وجود ندارد) برای ذخیره فایلها.
پارامتر exist_ok=True از بروز خطا در صورت وجود پوشه جلوگیری میکندos.path.isfile(): اطمینان میدهد که فقط فایلها (نه پوشهها)
منتقل شوندپس از آمادهسازی فایلها، با استفاده از کتابخانه Pandas فایل CSV را میخوانیم:
import pandas as pd
# خواندن فایل CSV
match_data = pd.read_csv("/content/ST/Matches.csv")
# نمایش ۵ رکورد اول
match_data.head()
read_csv() فایل CSV را خوانده و به یک DataFrame تبدیل
میکندhead() به صورت پیشفرض ۵ رکورد اول را نمایش میدهد
با اجرای دستور match_data.head()، ساختار اولیه دیتاست به صورت زیر نمایش داده میشود:
| Index | Division | MatchDate | HomeTeam | AwayTeam | HomeElo | AwayElo | FTHome | FTAway | FTResult |
|---|---|---|---|---|---|---|---|---|---|
| 0 | F1 | 2000-07-28 | Marseille | Troyes | 1686.34 | 1586.57 | 2.0 | 1.0 | H |
| 1 | F1 | 2000-07-28 | Paris SG | Strasbourg | 1714.89 | 1642.51 | 3.0 | 1.0 | H |
| 2 | F2 | 2000-07-28 | Wasquehal | Nancy | 1465.08 | 1633.80 | 1.0 | 2.0 | A |
| 3 | F1 | 2000-07-29 | Auxerre | Sedan | 1635.58 | 1624.22 | 0.0 | 0.0 | D |
| 4 | F1 | 2000-07-29 | Bordeaux | Metz | 1734.34 | 1673.11 | 2.0 | 0.0 | H |
FTResult شامل سه مقدار است: H (برد میزبان)، A (برد مهمان)، D (تساوی)در این مرحله تنها چند ستون از کل ۴۸ ستون نمایش داده شده است. برای مشاهده تمام ستونها باید از دستورات
دیگری مانند match_data.columns استفاده کنیم که در بخش بعدی بررسی خواهد شد.
پس از بارگذاری موفقیتآمیز دیتاست، مرحله بعدی کاوش و درک عمیق دادههاست. این مرحله شامل بررسی ساختار، کیفیت، توزیع و ویژگیهای مختلف دادهها است.
برای دریافت اطلاعات جامع درباره دیتاست از متد info() استفاده میکنیم:
match_data.info()
'pandas.core.frame.DataFrame'>
RangeIndex: 230557 entries, 0 to 230556
Data columns (total 48 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Division 230557 non-null object
1 MatchDate 230557 non-null object
2 MatchTime 99072 non-null object
3 HomeTeam 230557 non-null object
4 AwayTeam 230557 non-null object
5 HomeElo 141597 non-null float64
6 AwayElo 141528 non-null float64
7 Form3Home 229057 non-null float64
8 Form5Home 229057 non-null float64
9 Form3Away 229057 non-null float64
10 Form5Away 229057 non-null float64
11 FTHome 230554 non-null float64
12 FTAway 230554 non-null float64
13 FTResult 230554 non-null object
14 HTHome 175977 non-null float64
15 HTAway 175977 non-null float64
16 HTResult 175977 non-null object
17 HomeShots 114735 non-null float64
18 AwayShots 114738 non-null float64
19 HomeTarget 113929 non-null float64
20 AwayTarget 113932 non-null float64
21 HomeFouls 113973 non-null float64
22 AwayFouls 113973 non-null float64
23 HomeCorners 114363 non-null float64
24 AwayCorners 114363 non-null float64
25 HomeYellow 119298 non-null float64
26 AwayYellow 119299 non-null float64
27 HomeRed 119299 non-null float64
28 AwayRed 119297 non-null float64
29 OddHome 227527 non-null float64
30 OddDraw 227527 non-null float64
31 OddAway 227527 non-null float64
32 MaxHome 202922 non-null float64
33 MaxDraw 202922 non-null float64
34 MaxAway 202922 non-null float64
35 Over25 148398 non-null float64
36 Under25 148397 non-null float64
37 MaxOver25 148398 non-null float64
38 MaxUnder25 148397 non-null float64
39 HandiSize 156733 non-null float64
40 HandiHome 156475 non-null float64
41 HandiAway 156451 non-null float64
42 C_LTH 112602 non-null float64
43 C_LTA 112602 non-null float64
44 C_VHD 112602 non-null float64
45 C_VAD 112602 non-null float64
46 C_HTB 112602 non-null float64
47 C_PHB 112602 non-null float64
dtypes: float64(41), object(7)
memory usage: 84.4+ MB
float64 (اعداد اعشاری)object (رشتههای متنی)یکی از مهمترین بخشهای کاوش داده، شناسایی مقادیر گمشده (NULL) است:
match_data.isnull().sum()
| نام ستون | تعداد مقادیر گمشده | درصد گمشدگی | وضعیت |
|---|---|---|---|
| Division | 0 | 0% | کامل ✓ |
| MatchDate | 0 | 0% | کامل ✓ |
| MatchTime | 131,485 | 57% | نیمه گمشده |
| HomeTeam | 0 | 0% | کامل ✓ |
| AwayTeam | 0 | 0% | کامل ✓ |
| HomeElo | 88,960 | 39% | قابل قبول |
| AwayElo | 89,029 | 39% | قابل قبول |
| Form3Home/Away | 1,500 | 0.7% | عالی ✓ |
| FTHome/Away/Result | 3 | 0.001% | تقریباً کامل ✓ |
| HTHome/Away/Result | 54,580 | 24% | قابل قبول |
| HomeShots/AwayShots | ~115,822 | 50% | نیمه گمشده |
| ضرایب شرطبندی | 3,030 - 82,160 | 1-36% | متفاوت |
دستهبندی ستونها بر اساس میزان گمشدگی:
دلیل گمشدگی: برخی آمارها (مانند شوتها) فقط برای سالهای اخیر ثبت شدهاند و برای بازیهای قدیمیتر موجود نیستند.
match_data.shape
(230557, 48)
این خروجی نشان میدهد که دیتاست دارای ۲۳۰,۵۵۷ سطر (مسابقه) و ۴۸ ستون (ویژگی) است.
match_data.columns
Index(['Division', 'MatchDate', 'MatchTime', 'HomeTeam', 'AwayTeam', 'HomeElo',
'AwayElo', 'Form3Home', 'Form5Home', 'Form3Away', 'Form5Away', 'FTHome',
'FTAway', 'FTResult', 'HTHome', 'HTAway', 'HTResult', 'HomeShots',
'AwayShots', 'HomeTarget', 'AwayTarget', 'HomeFouls', 'AwayFouls',
'HomeCorners', 'AwayCorners', 'HomeYellow', 'AwayYellow', 'HomeRed',
'AwayRed', 'OddHome', 'OddDraw', 'OddAway', 'MaxHome', 'MaxDraw',
'MaxAway', 'Over25', 'Under25', 'MaxOver25', 'MaxUnder25', 'HandiSize',
'HandiHome', 'HandiAway', 'C_LTH', 'C_LTA', 'C_VHD', 'C_VAD', 'C_HTB',
'C_PHB'],
dtype='object')
برای درک بهتر توزیع دادههای عددی، از متد describe() استفاده میکنیم:
match_data.describe()
بر اساس یافتههای مرحله کاوش، اکنون باید دادهها را برای مدلسازی آماده کنیم. این فرآیند شامل حذف ستونهای غیرضروری، مدیریت دادههای گمشده، و تبدیل دادهها به فرمت مناسب است.
با توجه به تحلیل دادههای گمشده و هدف پروژه، تصمیم میگیریم ستونهای زیر را حذف کنیم:
match_data.drop([
'HTHome', 'HTAway', 'HTResult', # نتایج نیمه اول
'HomeShots', 'AwayShots', # شوتها
'HomeTarget', 'AwayTarget', # شوت به چارچوب
'HomeFouls', 'AwayFouls', # خطاها
'HomeCorners', 'AwayCorners', # کرنرها
'HomeYellow', 'AwayYellow', # کارت زرد
'HomeRed', 'AwayRed', # کارت قرمز
'OddHome', 'OddDraw', 'OddAway', # ضرایب متوسط
'MaxHome', 'MaxDraw', 'MaxAway', # بیشترین ضرایب
'Over25', 'Under25', # ضرایب تعداد گل
'MaxOver25', 'MaxUnder25', # بیشترین ضرایب گل
'HandiSize', 'HandiHome', 'HandiAway' # هندیکپ
], axis=1, inplace=True)
| دسته | دلیل حذف |
|---|---|
| نتایج نیمه اول | ۲۴٪ داده گمشده + اطلاعات مشابه با نتیجه نهایی |
| آمار بازی | ۵۰٪ داده گمشده + فقط برای بازیهای اخیر موجود است |
| ضرایب شرطبندی | تا ۳۶٪ داده گمشده + همبستگی بالا با سایر ویژگیها |
| هندیکپ | ۳۲٪ داده گمشده + اطلاعات تکراری |
پارامتر inplace=True تغییرات را مستقیماً روی DataFrame اصلی اعمال میکند و نیازی به
ایجاد کپی جدید نیست. پارامتر axis=1 مشخص میکند که حذف باید روی ستونها (نه سطرها)
انجام شود.
match_data.columns
پس از حذف، ستونهای باقیمانده عبارتند از:
Index(['Division', 'MatchDate', 'MatchTime', 'HomeTeam', 'AwayTeam', 'HomeElo',
'AwayElo', 'Form3Home', 'Form5Home', 'Form3Away', 'Form5Away', 'FTHome',
'FTAway', 'FTResult', 'C_LTH', 'C_LTA', 'C_VHD', 'C_VAD', 'C_HTB', 'C_PHB'],
dtype='object')
ستون غیرضروری
ویژگی مفید
در ادامه فرآیند پیشپردازش، رکوردهایی که دارای مقادیر گمشده در ستونهای حیاتی هستند را حذف میکنیم. این کار در بخش بعدی (مهندسی ویژگی) و همراه با آمادهسازی نهایی داده برای مدلسازی انجام خواهد شد.
به جای حذف همه رکوردهای دارای هر نوع داده گمشده، تنها رکوردهایی را حذف میکنیم که:
FTResult) آنها NULL استاین رویکرد به ما امکان میدهد حداکثر داده معتبر را حفظ کنیم.
مهندسی ویژگی فرآیند ساخت ویژگیهای جدید از دادههای موجود است که میتواند قدرت پیشبینی مدل را به طور قابل توجهی افزایش دهد. در این بخش، سه ویژگی جدید و قدرتمند میسازیم.
به جای استفاده از امتیازات Elo هر تیم به صورت جداگانه، اختلاف امتیازات را محاسبه میکنیم. این عدد نشاندهنده قدرت نسبی تیم میزبان نسبت به مهمان است.
# ساخت ویژگی EloDiff
match_data['EloDiff'] = match_data['HomeElo'] - match_data['AwayElo']
مثال: اگر تیم میزبان Elo برابر ۱۸۰۰ و مهمان ۱۶۰۰ داشته باشد، EloDiff = +۲۰۰ خواهد بود که نشاندهنده برتری قابل توجه میزبان است.
دو ویژگی (HomeElo, AwayElo) را به یک ویژگی تبدیل میکند
مدل را بر روی قدرت نسبی تیمها متمرکز میکند
معمولاً عملکرد مدل را در پیشبینی بهبود میبخشد
پس از ساخت EloDiff، دیگر نیازی به ستونهای اصلی نداریم:
match_data.drop(['HomeElo', 'AwayElo'], axis=1, inplace=True)
مشابه EloDiff، اختلاف امتیازات فرم ۵ بازی اخیر را محاسبه میکنیم:
# بررسی وجود دادههای گمشده در Form5Home
print(match_data['Form5Home'].isna().sum()) # خروجی: 1500
# ساخت ویژگی Form5Diff
match_data['Form5Diff'] = match_data['Form5Home'] - match_data['Form5Away']
این ویژگی نشان میدهد کدام تیم در ۵ بازی اخیر عملکرد بهتری داشته است:
مشابه Form5Diff اما برای ۳ بازی اخیر:
# ساخت ویژگی Form3Diff
match_data['Form3Diff'] = match_data['Form3Home'] - match_data['Form3Away']
استفاده از هر دو ویژگی اطلاعات متفاوتی ارائه میدهد:
ترکیب این دو به مدل امکان میدهد هم روندهای کوتاهمدت و هم بلندمدت را در نظر بگیرد.
match_data.columns
خروجی نشان میدهد که حالا ۳ ویژگی جدید داریم:
Index(['Division', 'MatchDate', 'MatchTime', 'HomeTeam', 'AwayTeam',
'Form3Home', 'Form5Home', 'Form3Away', 'Form5Away', 'FTHome',
'FTAway', 'FTResult', 'C_LTH', 'C_LTA', 'C_VHD', 'C_VAD', 'C_HTB',
'C_PHB', 'EloDiff', 'Form5Diff', 'Form3Diff'],
dtype='object')
در کد اصلی، دستور زیر اجرا شده است:
match_data.drop(columns=['Form3Home', 'Form5Home', 'Form3Away', 'Form5Away'])
دقت کنید که این دستور بدون پارامتر inplace=True نوشته شده، بنابراین تغییرات روی
DataFrame اصلی اعمال نمیشود. در عمل، این ستونها همچنان در دیتاست باقی ماندهاند و در مرحله بعد
(آمادهسازی نهایی برای مدلسازی) استفاده میشوند.
| نام ویژگی | فرمول | کاربرد | دامنه مقادیر |
|---|---|---|---|
EloDiff |
HomeElo - AwayElo | قدرت نسبی تیمها | حدود -1000 تا +1000 |
Form5Diff |
Form5Home - Form5Away | فرم میانمدت | -15 تا +15 |
Form3Diff |
Form3Home - Form3Away | فرم کوتاهمدت | -9 تا +9 |
در این بخش، سه الگوریتم پیشرفته یادگیری ماشین را برای پیشبینی نتایج مسابقات پیادهسازی میکنیم. قبل از شروع، باید دادهها را برای آموزش آماده کنیم.
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
pandas و numpy: پردازش و محاسبات عددیtrain_test_split: تقسیم داده به آموزش و تستLabelEncoder: تبدیل برچسبهای متنی به عددیStandardScaler: نرمالسازی دادههاmatplotlib و seaborn: رسم نمودارها
# کپی دیتاست
df = match_data.copy()
print(f"\nRows: {df.shape[0]}, Columns: {df.shape[1]}")
# تعریف ویژگیهای مورد استفاده
feature_cols = ['Form3Home', 'Form5Home', 'Form3Away', 'Form5Away',
'C_LTH', 'C_LTA', 'C_VHD', 'C_VAD', 'C_HTB', 'C_PHB',
'EloDiff', 'Form5Diff', 'Form3Diff']
۱۳ ویژگی انتخاب شده به ۳ دسته تقسیم میشوند:
# حذف رکوردهای بدون برچسب
df_clean = df.dropna(subset=['FTResult'])
# حذف رکوردهای بدون ویژگیهای ضروری
df_clean = df_clean.dropna(subset=feature_cols)
print(f"After cleaning: {df_clean.shape[0]} rows")
نتیجه: بعد از پاکسازی، تعداد رکوردهای معتبر حدود ۱۱۲,۶۰۲ مسابقه باقی میماند.
# ویژگیها (X) و برچسبها (y)
X = df_clean[feature_cols].copy()
y = df_clean['FTResult'].copy()
print(f"\nFeatures: {len(feature_cols)}")
چون برچسبهای ما متنی هستند (H, A, D)، باید آنها را به عدد تبدیل کنیم:
# رمزگذاری برچسبها
le = LabelEncoder()
y_encoded = le.fit_transform(y)
print(f"\nTarget classes: {le.classes_}")
# خروجی: ['A' 'D' 'H']
# تقسیم 80% آموزش، 20% تست
X_train, X_test, y_train, y_test = train_test_split(
X, y_encoded,
test_size=0.2, # 20% برای تست
random_state=42, # برای تکرارپذیری
stratify=y_encoded # حفظ نسبت کلاسها
)
print(f"\nTrain: {X_train.shape[0]}, Test: {X_test.shape[0]}")
# خروجی: Train: 90081, Test: 22521
پارامتر stratify=y_encoded تضمین میکند که نسبت کلاسها (برد میزبان، مهمان، تساوی) در هر
دو مجموعه آموزش و تست یکسان باشد. این کار از ایجاد تورش (bias) در مدل جلوگیری میکند.
# نرمالسازی با StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
ویژگیهای مختلف دامنههای متفاوتی دارند:
EloDiff: از -۱۰۰۰ تا +۱۰۰۰Form3Diff: از -۹ تا +۹C_LTH: از ۰ تا ۱StandardScaler همه ویژگیها را به میانگین ۰ و انحراف معیار ۱ تبدیل میکند تا هیچ ویژگیای به دلیل مقیاس بزرگتر، تاثیر بیشتری نداشته باشد.
# ذخیره دادهها برای استفاده در مدلهای بعدی
np.save('X_train_scaled.npy', X_train_scaled)
np.save('X_test_scaled.npy', X_test_scaled)
np.save('y_train.npy', y_train)
np.save('y_test.npy', y_test)
print("\n" + "="*60)
print("Data preprocessing complete!")
print("="*60)
80% از کل داده
20% از کل داده
ویژگی نرمالشده
Random Forest یک الگوریتم ensemble است که از ترکیب چندین درخت تصمیمگیری برای پیشبینی دقیقتر استفاده میکند. هر درخت روی یک زیرمجموعه تصادفی از دادهها آموزش میبیند و نتیجه نهایی با رایگیری اکثریت تعیین میشود.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
from sklearn.model_selection import cross_val_score
import warnings
warnings.filterwarnings('ignore')
print("="*60)
print("Random Forest Model")
print("="*60)
# بارگذاری دادهها
X_train = np.load('X_train_scaled.npy')
X_test = np.load('X_test_scaled.npy')
y_train = np.load('y_train.npy')
y_test = np.load('y_test.npy')
print("\nTraining Random Forest...")
# ساخت مدل با پارامترهای بهینه
rf_model = RandomForestClassifier(
n_estimators=200, # تعداد درختها
max_depth=15, # عمق هر درخت
min_samples_split=10, # حداقل نمونه برای split
min_samples_leaf=4, # حداقل نمونه در برگ
random_state=42,
n_jobs=-1 # استفاده از تمام هستههای CPU
)
# آموزش مدل
rf_model.fit(X_train, y_train)
| پارامتر | مقدار | توضیح |
|---|---|---|
n_estimators |
200 | تعداد درختها در جنگل - هرچه بیشتر، دقت بالاتر اما زمان بیشتر |
max_depth |
15 | حداکثر عمق درخت - جلوگیری از overfitting |
min_samples_split |
10 | حداقل تعداد نمونه برای تقسیم یک گره |
min_samples_leaf |
4 | حداقل تعداد نمونه در هر برگ نهایی |
n_jobs |
-1 | استفاده از تمام پردازندهها برای سرعت بیشتر |
# پیشبینی
y_pred = rf_model.predict(X_test)
y_train_pred = rf_model.predict(X_train)
# محاسبه دقت
train_acc = accuracy_score(y_train, y_train_pred)
test_acc = accuracy_score(y_test, y_pred)
print(f"\nTrain Accuracy: {train_acc:.4f} ({train_acc*100:.2f}%)")
print(f"Test Accuracy: {test_acc:.4f} ({test_acc*100:.2f}%)")
# اعتبارسنجی متقابل 5-fold
cv_scores = cross_val_score(rf_model, X_train, y_train, cv=5)
print(f"\nCross Validation (5-fold):")
print(f"Mean: {cv_scores.mean():.4f}, Std: {cv_scores.std():.4f}")
در این روش، دادههای آموزش را به ۵ قسمت تقسیم میکنیم. هر بار ۴ قسمت برای آموزش و ۱ قسمت برای اعتبارسنجی استفاده میشود. این کار ۵ بار تکرار میشود و میانگین نتایج گزارش میگردد. این روش تخمین بهتری از عملکرد واقعی مدل ارائه میدهد.
print("\n" + "="*60)
print("Classification Report:")
print("="*60)
print(classification_report(y_test, y_pred))
گزارش شامل معیارهای زیر برای هر کلاس است:
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# ماتریس درهمریختگی
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0])
axes[0].set_title('Confusion Matrix - Random Forest')
axes[0].set_ylabel('Actual')
axes[0].set_xlabel('Predicted')
# اهمیت ویژگیها
feature_names = ['Form3Home', 'Form5Home', 'Form3Away', 'Form5Away',
'C_LTH', 'C_LTA', 'C_VHD', 'C_VAD', 'C_HTB', 'C_PHB',
'EloDiff', 'Form5Diff', 'Form3Diff']
importances = rf_model.feature_importances_
indices = np.argsort(importances)[::-1]
axes[1].bar(range(len(importances)), importances[indices])
axes[1].set_title('Feature Importance - Random Forest')
axes[1].set_xticks(range(len(importances)))
axes[1].set_xticklabels([feature_names[i] for i in indices],
rotation=45, ha='right')
plt.tight_layout()
plt.savefig('rf_results.png', dpi=300, bbox_inches='tight')
plt.show()
Random Forest به ما میگوید کدام ویژگیها برای پیشبینی مهمتر هستند. این اطلاعات میتواند برای:
XGBoost (Extreme Gradient Boosting) یک الگوریتم پیشرفته boosting است که از ترکیب متوالی درختهای ضعیف برای ساخت یک مدل قوی استفاده میکند. هر درخت جدید سعی میکند خطاهای درختهای قبلی را اصلاح کند.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
from sklearn.model_selection import cross_val_score
import warnings
warnings.filterwarnings('ignore')
# نصب XGBoost در صورت نیاز
try:
import xgboost as xgb
except:
print("Installing XGBoost...")
!pip install xgboost -q
import xgboost as xgb
print("="*60)
print("XGBoost Model")
print("="*60)
# بارگذاری دادهها
X_train = np.load('X_train_scaled.npy')
X_test = np.load('X_test_scaled.npy')
y_train = np.load('y_train.npy')
y_test = np.load('y_test.npy')
print("\nTraining XGBoost...")
# ساخت مدل
xgb_model = xgb.XGBClassifier(
n_estimators=200, # تعداد boosting rounds
max_depth=6, # عمق درخت
learning_rate=0.1, # نرخ یادگیری
subsample=0.8, # نسبت نمونهبرداری
colsample_bytree=0.8, # نسبت انتخاب ویژگیها
random_state=42,
eval_metric='mlogloss', # معیار ارزیابی
use_label_encoder=False
)
# آموزش مدل
xgb_model.fit(X_train, y_train)
| پارامتر | مقدار | توضیح |
|---|---|---|
learning_rate |
0.1 | وزن هر درخت جدید - مقدار کمتر = یادگیری محتاطانهتر |
subsample |
0.8 | ۸۰٪ از داده برای هر درخت - جلوگیری از overfitting |
colsample_bytree |
0.8 | ۸۰٪ از ویژگیها برای هر درخت - تنوع بیشتر |
eval_metric |
mlogloss | multi-class logloss برای طبقهبندی چندکلاسه |
# پیشبینی
y_pred = xgb_model.predict(X_test)
y_train_pred = xgb_model.predict(X_train)
# محاسبه دقت
train_acc = accuracy_score(y_train, y_train_pred)
test_acc = accuracy_score(y_test, y_pred)
print(f"\nTrain Accuracy: {train_acc:.4f} ({train_acc*100:.2f}%)")
print(f"Test Accuracy: {test_acc:.4f} ({test_acc*100:.2f}%)")
# اعتبارسنجی متقابل
cv_scores = cross_val_score(xgb_model, X_train, y_train, cv=5)
print(f"\nCross Validation (5-fold):")
print(f"Mean: {cv_scores.mean():.4f}, Std: {cv_scores.std():.4f}")
# گزارش طبقهبندی
print("\n" + "="*60)
print("Classification Report:")
print("="*60)
print(classification_report(y_test, y_pred))
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# ماتریس درهمریختگی
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Greens', ax=axes[0])
axes[0].set_title('Confusion Matrix - XGBoost')
axes[0].set_ylabel('Actual')
axes[0].set_xlabel('Predicted')
# اهمیت ویژگیها
feature_names = ['Form3Home', 'Form5Home', 'Form3Away', 'Form5Away',
'C_LTH', 'C_LTA', 'C_VHD', 'C_VAD', 'C_HTB', 'C_PHB',
'EloDiff', 'Form5Diff', 'Form3Diff']
importances = xgb_model.feature_importances_
indices = np.argsort(importances)[::-1]
axes[1].bar(range(len(importances)), importances[indices],
color='green', alpha=0.7)
axes[1].set_title('Feature Importance - XGBoost')
axes[1].set_xticks(range(len(importances)))
axes[1].set_xticklabels([feature_names[i] for i in indices],
rotation=45, ha='right')
plt.tight_layout()
plt.savefig('xgb_results.png', dpi=300, bbox_inches='tight')
plt.show()
شبکههای عصبی از لایههای متعدد نورونهای مصنوعی تشکیل شدهاند که با یادگیری از داده، الگوهای پیچیده را کشف میکنند. این مدل از کتابخانه TensorFlow/Keras استفاده میکند.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
import warnings
warnings.filterwarnings('ignore')
# نصب TensorFlow در صورت نیاز
try:
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
except:
print("Installing TensorFlow...")
!pip install tensorflow -q
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
print("="*60)
print("Neural Network Model")
print("="*60)
# بارگذاری دادهها
X_train = np.load('X_train_scaled.npy')
X_test = np.load('X_test_scaled.npy')
y_train = np.load('y_train.npy')
y_test = np.load('y_test.npy')
num_classes = len(np.unique(y_train)) # 3 کلاس
input_dim = X_train.shape[1] # 13 ویژگی
print(f"\nFeatures: {input_dim}, Classes: {num_classes}")
print("\nBuilding Neural Network...")
# ساخت مدل Sequential
model = keras.Sequential([
# لایه اول: 128 نورون + Dropout
layers.Dense(128, activation='relu', input_shape=(input_dim,)),
layers.Dropout(0.3),
# لایه دوم: 64 نورون + Dropout
layers.Dense(64, activation='relu'),
layers.Dropout(0.3),
# لایه سوم: 32 نورون + Dropout
layers.Dense(32, activation='relu'),
layers.Dropout(0.2),
# لایه خروجی: 3 نورون (یک برای هر کلاس)
layers.Dense(num_classes, activation='softmax')
])
| لایه | تعداد نورون | تابع فعالسازی | Dropout |
|---|---|---|---|
| ورودی | 128 | ReLU | 30% |
| مخفی ۱ | 64 | ReLU | 30% |
| مخفی ۲ | 32 | ReLU | 20% |
| خروجی | 3 | Softmax | - |
# تنظیمات آموزش
model.compile(
optimizer='adam', # الگوریتم بهینهسازی
loss='sparse_categorical_crossentropy', # تابع خطا
metrics=['accuracy'] # معیار ارزیابی
)
print("\nTraining Neural Network...")
# آموزش با 100 epoch
history = model.fit(
X_train, y_train,
epochs=100, # تعداد دور کامل آموزش
batch_size=32, # تعداد نمونه در هر batch
validation_split=0.2, # 20% از train برای اعتبارسنجی
verbose=0 # عدم نمایش جزئیات
)
# پیشبینی
y_pred_proba = model.predict(X_test, verbose=0)
y_pred = np.argmax(y_pred_proba, axis=1)
y_train_pred_proba = model.predict(X_train, verbose=0)
y_train_pred = np.argmax(y_train_pred_proba, axis=1)
# محاسبه دقت
train_acc = accuracy_score(y_train, y_train_pred)
test_acc = accuracy_score(y_test, y_pred)
print(f"\nTrain Accuracy: {train_acc:.4f} ({train_acc*100:.2f}%)")
print(f"Test Accuracy: {test_acc:.4f} ({test_acc*100:.2f}%)")
# گزارش طبقهبندی
print("\n" + "="*60)
print("Classification Report:")
print("="*60)
print(classification_report(y_test, y_pred))
fig = plt.figure(figsize=(15, 5))
# ماتریس درهمریختگی
ax1 = plt.subplot(1, 3, 1)
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Purples', ax=ax1)
ax1.set_title('Confusion Matrix - Neural Network')
ax1.set_ylabel('Actual')
ax1.set_xlabel('Predicted')
# نمودار Loss
ax2 = plt.subplot(1, 3, 2)
ax2.plot(history.history['loss'], label='Train Loss', linewidth=2)
ax2.plot(history.history['val_loss'], label='Validation Loss', linewidth=2)
ax2.set_title('Model Loss')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Loss')
ax2.legend()
ax2.grid(True, alpha=0.3)
# نمودار Accuracy
ax3 = plt.subplot(1, 3, 3)
ax3.plot(history.history['accuracy'], label='Train Accuracy', linewidth=2)
ax3.plot(history.history['val_accuracy'], label='Validation Accuracy', linewidth=2)
ax3.set_title('Model Accuracy')
ax3.set_xlabel('Epoch')
ax3.set_ylabel('Accuracy')
ax3.legend()
ax3.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('nn_results.png', dpi=300, bbox_inches='tight')
plt.show()
در این بخش، هر سه مدل را با یکدیگر مقایسه میکنیم تا بهترین الگوریتم را برای پیشبینی نتایج مسابقات فوتبال مشخص کنیم.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import warnings
warnings.filterwarnings('ignore')
print("=" * 60)
print("Part 5: Model Comparison")
print("=" * 60)
# بارگذاری دادهها
X_train = np.load('X_train_scaled.npy')
X_test = np.load('X_test_scaled.npy')
y_train = np.load('y_train.npy')
y_test = np.load('y_test.npy')
num_classes = len(np.unique(y_train))
print("\nTraining all models...")
print("1. Random Forest...")
from sklearn.ensemble import RandomForestClassifier
rf_model = RandomForestClassifier(
n_estimators=200,
max_depth=15,
random_state=42,
n_jobs=-1
)
rf_model.fit(X_train, y_train)
rf_pred = rf_model.predict(X_test)
rf_train_pred = rf_model.predict(X_train)
print("2. XGBoost...")
try:
import xgboost as xgb
except:
!pip install xgboost -q
import xgboost as xgb
xgb_model = xgb.XGBClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
random_state=42,
eval_metric='mlogloss',
use_label_encoder=False
)
xgb_model.fit(X_train, y_train)
xgb_pred = xgb_model.predict(X_test)
xgb_train_pred = xgb_model.predict(X_train)
print("3. Neural Network...")
try:
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
except:
!pip install tensorflow -q
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
nn_model = keras.Sequential([
layers.Dense(128, activation='relu', input_shape=(X_train.shape[1],)),
layers.Dropout(0.3),
layers.Dense(64, activation='relu'),
layers.Dropout(0.3),
layers.Dense(32, activation='relu'),
layers.Dropout(0.2),
layers.Dense(num_classes, activation='softmax')
])
nn_model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
nn_model.fit(X_train, y_train, epochs=100, batch_size=32,
validation_split=0.2, verbose=0)
nn_pred = np.argmax(nn_model.predict(X_test, verbose=0), axis=1)
nn_train_pred = np.argmax(nn_model.predict(X_train, verbose=0), axis=1)
# تابع محاسبه معیارها
def calc_metrics(y_true, y_pred, name):
return {
'Model': name,
'Accuracy': accuracy_score(y_true, y_pred),
'Precision': precision_score(y_true, y_pred, average='weighted'),
'Recall': recall_score(y_true, y_pred, average='weighted'),
'F1-Score': f1_score(y_true, y_pred, average='weighted')
}
# محاسبه برای داده تست
test_results = [
calc_metrics(y_test, rf_pred, 'Random Forest'),
calc_metrics(y_test, xgb_pred, 'XGBoost'),
calc_metrics(y_test, nn_pred, 'Neural Network')
]
# محاسبه برای داده آموزش
train_results = [
calc_metrics(y_train, rf_train_pred, 'Random Forest'),
calc_metrics(y_train, xgb_train_pred, 'XGBoost'),
calc_metrics(y_train, nn_train_pred, 'Neural Network')
]
test_df = pd.DataFrame(test_results)
train_df = pd.DataFrame(train_results)
print("\n" + "=" * 80)
print("Test Set Results:")
print("=" * 80)
print(test_df.to_string(index=False))
print("\n" + "=" * 80)
print("Train Set Results:")
print("=" * 80)
print(train_df.to_string(index=False))
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
metrics = ['Accuracy', 'Precision', 'Recall', 'F1-Score']
for idx, metric in enumerate(metrics):
ax = axes[idx // 2, idx % 2]
x = np.arange(len(test_df))
width = 0.35
train_vals = train_df[metric].values
test_vals = test_df[metric].values
# رسم نمودار میلهای
ax.bar(x - width/2, train_vals, width, label='Train',
alpha=0.8, color='skyblue')
ax.bar(x + width/2, test_vals, width, label='Test',
alpha=0.8, color='orange')
ax.set_xlabel('Models')
ax.set_ylabel(metric)
ax.set_title(f'{metric} Comparison')
ax.set_xticks(x)
ax.set_xticklabels(test_df['Model'], rotation=15, ha='right')
ax.legend()
ax.grid(True, alpha=0.3, axis='y')
# اضافه کردن مقادیر روی میلهها
for i, v in enumerate(train_vals):
ax.text(i - width/2, v + 0.01, f'{v:.3f}',
ha='center', va='bottom', fontsize=8)
for i, v in enumerate(test_vals):
ax.text(i + width/2, v + 0.01, f'{v:.3f}',
ha='center', va='bottom', fontsize=8)
plt.tight_layout()
plt.savefig('model_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
best_idx = test_df['Accuracy'].idxmax()
best_model = test_df.loc[best_idx, 'Model']
best_acc = test_df.loc[best_idx, 'Accuracy']
print("\n" + "=" * 80)
print(f"Best Model: {best_model}")
print(f"Test Accuracy: {best_acc:.4f} ({best_acc * 100:.2f}%)")
print("=" * 80)
summary_data = {
'Metric': ['Train Accuracy', 'Test Accuracy', 'Overfit Gap'],
'Random Forest': [
f"{train_df.loc[0, 'Accuracy']:.4f}",
f"{test_df.loc[0, 'Accuracy']:.4f}",
f"{(train_df.loc[0, 'Accuracy'] - test_df.loc[0, 'Accuracy']):.4f}"
],
'XGBoost': [
f"{train_df.loc[1, 'Accuracy']:.4f}",
f"{test_df.loc[1, 'Accuracy']:.4f}",
f"{(train_df.loc[1, 'Accuracy'] - test_df.loc[1, 'Accuracy']):.4f}"
],
'Neural Network': [
f"{train_df.loc[2, 'Accuracy']:.4f}",
f"{test_df.loc[2, 'Accuracy']:.4f}",
f"{(train_df.loc[2, 'Accuracy'] - test_df.loc[2, 'Accuracy']):.4f}"
]
}
summary_df = pd.DataFrame(summary_data)
print("\nSummary:")
print(summary_df.to_string(index=False))
Overfit Gap تفاوت بین دقت آموزش و تست است:
بر اساس معیارهای مختلف، نتایج مقایسه به صورت زیر خواهد بود:
| مدل | Test Accuracy | Train Accuracy | Overfit Gap | سرعت آموزش | رتبه کلی |
|---|---|---|---|---|---|
| Random Forest | ~0.54 | ~0.75 | ~0.21 | متوسط | سوم |
| XGBoost | ~0.55 | ~0.62 | ~0.07 | سریع | اول |
| Neural Network | ~0.54 | ~0.58 | ~0.04 | کند | دوم |
در این پروژه، یک سیستم جامع برای پیشبینی نتایج مسابقات فوتبال با استفاده از یادگیری ماشین توسعه دادیم. مراحل اصلی پروژه شامل موارد زیر بود:
دریافت دیتاست ۲۳۰,۵۵۷ مسابقه از Kaggle
بررسی ساختار، کیفیت و توزیع ۴۸ ویژگی
حذف ۲۷ ستون غیرضروری و پاکسازی دادههای ناقص
ساخت ۳ ویژگی قدرتمند: EloDiff، Form5Diff، Form3Diff
آموزش Random Forest، XGBoost و Neural Network
انتخاب XGBoost به عنوان بهترین مدل
| مدل | نقاط قوت | نقاط ضعف | کاربرد پیشنهادی |
|---|---|---|---|
| Random Forest | قابل تفسیر، اهمیت ویژگیها، سریع | Overfitting بالا در این داده | تحلیل اولیه و Feature Importance |
| XGBoost | بهترین دقت، تعادل خوب، سریع | نیاز به تنظیم پارامترها | پیشبینی production |
| Neural Network | کمترین overfitting، انعطافپذیر | زمان آموزش بالا، black box | دادههای بسیار زیاد |
بر اساس تحلیل Feature Importance، مهمترین عوامل تاثیرگذار بر نتیجه مسابقه:
اختلاف قدرت تیمها - مهمترین فاکتور
فرم اخیر تیمها - تاثیر قابل توجه
احتمالات محاسبهشده - اطلاعات تکمیلی
مشکل: بیش از ۵۰٪ دادههای ناقص در برخی ستونها
راهحل: حذف ستونهای با گمشدگی بالا و استفاده از ویژگیهای کاملتر
مشکل: تفاوت زیاد بین دقت train و test
راهحل: استفاده از Regularization، Dropout و Cross Validation
مشکل: تعداد بردهای میزبان بیشتر از سایر کلاسها
راهحل: استفاده از stratified split و weighted metrics
مشکل: عوامل غیرقابل اندازهگیری زیاد (روحیه، تاکتیک، شانس)
راهحل: پذیرش محدودیتهای ذاتی و تمرکز بر الگوهای آماری
این پروژه نشان داد که:
از استاد گرامی دکتر فدیشهای بابت راهنماییها و آموزشهای ارزشمند در درس مباحث ویژه ۲ صمیمانه سپاسگزاریم.