Compare commits
70 Commits
b4173c10bb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fa48f87f53 | |||
| 920ae7ce38 | |||
| 02f9aea333 | |||
| 15c6313246 | |||
| 1b420a425e | |||
| 55e62d8fe5 | |||
| 21e05148c8 | |||
| e001ce9ab5 | |||
| 9481ad7094 | |||
| 1d4aa36602 | |||
| 5574a3c59d | |||
| 94c7a4481a | |||
| 17ace9bd12 | |||
| 2b87669f41 | |||
| 2507678bc0 | |||
| 2b8dce665f | |||
| b6d64b59bf | |||
| f8599bdb9a | |||
| 4dcc4ced50 | |||
| 70fdc066c7 | |||
| f3362f266c | |||
| 8ce8fa5b94 | |||
| c525b12dfd | |||
| 497b5d8d3b | |||
| 4f7090e2d9 | |||
| 5b5f83c8cf | |||
| bfddcaca7d | |||
| 56d560af08 | |||
| 4bc51cfa99 | |||
| fdb8a5d0f0 | |||
| 22596e69f2 | |||
| f32badbd8f | |||
| 5645b38f20 | |||
| 244d8f5366 | |||
| 9bb8f39bca | |||
| 7a1cf14e2f | |||
| 62c797d299 | |||
| 34cc4a6cbb | |||
| 27e96da31d | |||
| 145a8b336b | |||
| 7a8960edb8 | |||
| 691c52f610 | |||
| bc461429f6 | |||
| a338d02244 | |||
| 1623432039 | |||
| 4c7930e9d2 | |||
| ec463cb927 | |||
| eab95c4e5c | |||
| 9027cc9900 | |||
| 3875f2a512 | |||
| 300dceeb4b | |||
| ad01976fb9 | |||
| 6880eb92f5 | |||
| 9e2edd590c | |||
| b5c2edf346 | |||
| bf7473c1e7 | |||
| 1f26a5bf2f | |||
| fb53fdf1df | |||
| 634204acf0 | |||
| df428ed1e8 | |||
| 2ccd6831eb | |||
| 1346924387 | |||
| e4c74025e5 | |||
| c8e7e4e927 | |||
| c8fa4c442d | |||
| 0f917695dd | |||
| 249c57346e | |||
| 182f4aae16 | |||
| 2f0b85a0c7 | |||
| 7814e0bc6b |
@@ -0,0 +1,27 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
*.backup
|
||||
*.dump
|
||||
ai-engine/
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Ignore test coverage and log files
|
||||
coverage/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Uploads
|
||||
uploads/
|
||||
public/uploads/
|
||||
@@ -11,13 +11,27 @@ jobs:
|
||||
- name: Kodu Cek
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Build
|
||||
- name: Docker Build (Backend)
|
||||
run: docker build -t iddaai-be:latest .
|
||||
|
||||
- name: Eski Konteyneri Sil
|
||||
run: docker rm -f iddaai-be || true
|
||||
- name: Docker Build (AI Engine)
|
||||
run: docker build -t iddaai-ai-engine:latest ./ai-engine
|
||||
|
||||
- name: Yeni Versiyonu Baslat
|
||||
- name: Eski Konteynerleri Sil
|
||||
run: |
|
||||
docker rm -f iddaai-be || true
|
||||
docker rm -f iddaai-ai-engine || true
|
||||
|
||||
- name: AI Engine'i Baslat
|
||||
run: |
|
||||
docker run -d \
|
||||
--name iddaai-ai-engine \
|
||||
--restart unless-stopped \
|
||||
--network iddaai_iddaai-network \
|
||||
-e DATABASE_URL='${{ secrets.DATABASE_URL }}' \
|
||||
iddaai-ai-engine:latest
|
||||
|
||||
- name: Backend'i Baslat
|
||||
run: |
|
||||
docker run -d \
|
||||
--name iddaai-be \
|
||||
@@ -25,11 +39,21 @@ jobs:
|
||||
--network iddaai_iddaai-network \
|
||||
-p 127.0.0.1:1810:3005 \
|
||||
-e NODE_ENV=production \
|
||||
-e DATABASE_URL='postgresql://iddaai_user:IddaA1_S4crET!@iddaai-postgres:5432/iddaai_db?schema=public' \
|
||||
-e REDIS_HOST='iddaai-redis' \
|
||||
-e REDIS_PORT='6379' \
|
||||
-e REDIS_PASSWORD='IddaA1_Redis_Pass!' \
|
||||
-e DATABASE_URL='${{ secrets.DATABASE_URL }}' \
|
||||
-e REDIS_HOST='${{ secrets.REDIS_HOST }}' \
|
||||
-e REDIS_PORT='${{ secrets.REDIS_PORT }}' \
|
||||
-e REDIS_PASSWORD='${{ secrets.REDIS_PASSWORD }}' \
|
||||
-e AI_ENGINE_URL='http://iddaai-ai-engine:8000' \
|
||||
-e JWT_SECRET='b7V8jM2wP1L5mQxs2RdfFkAsLpI2oG!w' \
|
||||
-e JWT_SECRET='${{ secrets.JWT_SECRET }}' \
|
||||
-e JWT_ACCESS_EXPIRATION='1d' \
|
||||
iddaai-be:latest /bin/sh -c "npx prisma migrate deploy && node dist/src/main.js"
|
||||
|
||||
- name: Saglik Kontrolu
|
||||
run: |
|
||||
sleep 10
|
||||
echo "=== AI Engine logs ==="
|
||||
docker logs --tail 30 iddaai-ai-engine || true
|
||||
echo "=== Backend logs ==="
|
||||
docker logs --tail 30 iddaai-be || true
|
||||
echo "=== AI Engine health ==="
|
||||
docker exec iddaai-ai-engine python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000/health').read().decode())" || echo "AI engine health check failed"
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
dist-*/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Database / Docker Volumes
|
||||
/data/
|
||||
ai-engine/data/**/*.csv
|
||||
ai-engine/data/v26_shadow/
|
||||
ai-engine/data/__pycache__/
|
||||
postgres-data/
|
||||
redis-data/
|
||||
|
||||
# OS / Editor
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Tests / Coverage
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Uploads
|
||||
uploads/
|
||||
public/uploads/
|
||||
|
||||
# Large Datasets and ML Models
|
||||
ai-engine/models/*
|
||||
!ai-engine/models/*.py
|
||||
!ai-engine/models/v25/
|
||||
!ai-engine/models/v27/
|
||||
!ai-engine/models/basketball_v25/
|
||||
!ai-engine/models/calibration/
|
||||
models/*
|
||||
!models/*.py
|
||||
colab_export/
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
# AGENTS.md - Coding Agent Guidelines
|
||||
|
||||
Bu dosya, bu repoda çalışan AI kodlama ajanları için rehberdir.
|
||||
|
||||
---
|
||||
|
||||
## 1. Build / Lint / Test Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run start:dev # Dev server with watch mode
|
||||
npm run build # Production build (nest build)
|
||||
|
||||
# Linting & Formatting
|
||||
npm run lint # ESLint with Prettier
|
||||
npm run format # Prettier write
|
||||
|
||||
# Testing
|
||||
npm run test # Run all unit tests
|
||||
npm run test:watch # Watch mode
|
||||
npm run test:e2e # End-to-end tests
|
||||
npx jest src/path/to/file.spec.ts # Run single test file
|
||||
npx jest --testNamePattern="test name" # Run specific test
|
||||
|
||||
# Database
|
||||
npx prisma generate # Generate Prisma client (required after install)
|
||||
npx prisma migrate dev # Run migrations
|
||||
npx prisma db seed # Seed database
|
||||
|
||||
# Feeder Scripts
|
||||
npm run feeder:historical # Historical data fetch
|
||||
npm run feeder:live # Live match data fetch
|
||||
npm run feeder:basketball # Basketball data fetch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Code Style Guidelines
|
||||
|
||||
### Imports (Sıralama)
|
||||
|
||||
```typescript
|
||||
// 1. NestJS/common imports
|
||||
import { Controller, Get, Post, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
// 2. External packages
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
// 3. Local imports (relative)
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto } from './dto/user.dto';
|
||||
import { ApiResponse, createSuccessResponse } from '../../common/types';
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
- **Single quotes** for strings
|
||||
- **Trailing commas** always
|
||||
- Prettier ile formatlama zorunlu
|
||||
- Dosya sonu boş satır
|
||||
|
||||
### Types & Type Safety
|
||||
|
||||
- `strictNullChecks: true` - null/undefined kontrolü zorunlu
|
||||
- `noImplicitAny: false` - any kullanımına izin var (Prisma dynamic access için)
|
||||
- Fonksiyon return type belirt: `async findOne(id: string): Promise<User>`
|
||||
- Interface > Type alias (objeler için)
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
```typescript
|
||||
// Classes & Interfaces: PascalCase
|
||||
class UsersService {}
|
||||
interface ApiResponse<T> {}
|
||||
|
||||
// Variables & Functions: camelCase
|
||||
const userService = new UsersService();
|
||||
async function findUserById() {}
|
||||
|
||||
// Constants: UPPER_SNAKE_CASE
|
||||
const JWT_SECRET = 'secret';
|
||||
const IS_PUBLIC_KEY = 'isPublic';
|
||||
|
||||
// Files: kebab-case
|
||||
user.dto.ts;
|
||||
users.service.ts;
|
||||
predictions.processor.spec.ts;
|
||||
|
||||
// DTOs: Entity + Dto suffix
|
||||
(CreateUserDto, UpdateUserDto, UserResponseDto);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. DTO Pattern
|
||||
|
||||
### Request DTOs
|
||||
|
||||
```typescript
|
||||
export class CreateUserDto {
|
||||
@ApiPropertyOptional({ example: 'user@example.com' })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Response DTOs (Security Critical)
|
||||
|
||||
```typescript
|
||||
@Exclude()
|
||||
export class UserResponseDto {
|
||||
@Expose()
|
||||
id: string;
|
||||
|
||||
@Expose()
|
||||
email: string;
|
||||
|
||||
// passwordHash intentionally NOT exposed
|
||||
}
|
||||
```
|
||||
|
||||
### Controller Usage
|
||||
|
||||
```typescript
|
||||
@Get('me')
|
||||
async getMe(@CurrentUser() user: User): Promise<ApiResponse<UserResponseDto>> {
|
||||
const fullUser = await this.usersService.findOneWithDetails(user.id);
|
||||
return createSuccessResponse(
|
||||
plainToInstance(UserResponseDto, fullUser),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**KRITIK:** Asla raw Prisma entity döndürme. Her zaman Response DTO kullan.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture Patterns
|
||||
|
||||
### Service Layer
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class UsersService extends BaseService<
|
||||
User,
|
||||
CreateUserDto,
|
||||
UpdateUserDto
|
||||
> {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma, 'User');
|
||||
}
|
||||
|
||||
// Custom methods...
|
||||
}
|
||||
```
|
||||
|
||||
### Controller Layer
|
||||
|
||||
```typescript
|
||||
@ApiTags('Users')
|
||||
@ApiBearerAuth()
|
||||
@Controller('users')
|
||||
export class UsersController extends BaseController<
|
||||
User,
|
||||
CreateUserDto,
|
||||
UpdateUserDto
|
||||
> {
|
||||
constructor(private readonly usersService: UsersService) {
|
||||
super(usersService, 'User');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Response Format
|
||||
|
||||
```typescript
|
||||
// All responses use this structure
|
||||
{
|
||||
"success": true,
|
||||
"status": 200,
|
||||
"message": "Success",
|
||||
"data": { ... },
|
||||
"errors": []
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
createSuccessResponse(data, 'Message')
|
||||
createErrorResponse('Message', 400, ['error1'])
|
||||
createPaginatedResponse(items, total, page, limit)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Error Handling
|
||||
|
||||
### Throw NestJS HTTP Exceptions
|
||||
|
||||
```typescript
|
||||
// Correct
|
||||
throw new NotFoundException('User not found');
|
||||
throw new ConflictException('EMAIL_ALREADY_EXISTS');
|
||||
throw new UnauthorizedException('INVALID_CREDENTIALS');
|
||||
|
||||
// Wrong
|
||||
throw new Error('User not found'); // Don't use generic Error
|
||||
```
|
||||
|
||||
### i18n Error Keys
|
||||
|
||||
```typescript
|
||||
// Use translatable keys (check src/i18n/{lang}/errors.json)
|
||||
throw new ConflictException('EMAIL_ALREADY_EXISTS');
|
||||
// Translates to: "Email already exists" (en) / "Email zaten kayıtlı" (tr)
|
||||
```
|
||||
|
||||
### Global Exception Filter
|
||||
|
||||
- Tüm hatalar HTTP 200 ile döner (status body içinde)
|
||||
- `NODE_ENV=development` ise stack trace eklenir
|
||||
- Validation hataları otomatik formatlanır
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing
|
||||
|
||||
### Unit Test Structure
|
||||
|
||||
```typescript
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
let prisma: PrismaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersService>(UsersService);
|
||||
});
|
||||
|
||||
it('should find user by id', async () => {
|
||||
// Arrange
|
||||
mockPrisma.user.findUnique.mockResolvedValue(mockUser);
|
||||
|
||||
// Act
|
||||
const result = await service.findOne('id');
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking External Dependencies
|
||||
|
||||
```typescript
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockedAxios.post.mockResolvedValue({ data: { ok: true } });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Module Registration
|
||||
|
||||
Redis-enabled modüller için `app.module.ts`:
|
||||
|
||||
```typescript
|
||||
const redisEnabled = process.env.REDIS_ENABLED === 'true';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
...(redisEnabled ? [QueueModule, PredictionsModule] : []),
|
||||
// ...
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Environment Variables
|
||||
|
||||
Zorunlu (`.env`):
|
||||
|
||||
```env
|
||||
NODE_ENV=development
|
||||
PORT=3005
|
||||
DATABASE_URL=postgresql://postgres:password@localhost:15432/boilerplate_db
|
||||
JWT_SECRET=your-secret-key
|
||||
JWT_ACCESS_EXPIRATION=15m
|
||||
REDIS_ENABLED=false
|
||||
AI_ENGINE_URL=http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Pre-commit Checklist
|
||||
|
||||
1. `npm run lint` - Lint errors fixed
|
||||
2. `npm run build` - Build succeeds
|
||||
3. `npm run test` - All tests pass
|
||||
4. Response DTOs used for all API responses
|
||||
5. No secrets/credentials in code
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
# 🚀 Suggest-Bet-BE — Deployment Guide
|
||||
|
||||
> **Tarih:** 2026-04-03
|
||||
> **Versiyon:** Sport Partition Release (Futbol/Basketbol Ayrımı)
|
||||
> **Amaç:** Masaüstü veya sunucuya kurulum adımları
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Şifreler ve Bağlantı Bilgileri
|
||||
|
||||
| Servis | Kullanıcı | Şifre | Host | Port |
|
||||
|--------|-----------|-------|------|------|
|
||||
| **PostgreSQL** | `suggestbet` | `SuGGesT2026SecuRe` | `localhost` | `15432` |
|
||||
| **Redis** | — | `RedisSecure2026` | `localhost` | `6379` |
|
||||
| **JWT Secret** | — | `9bfa42fbdc6031da6d7c0bd30e9f5b6378a071613d0c02acf95eb576249c3a25` | — | — |
|
||||
|
||||
**Database URL:**
|
||||
```
|
||||
postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db?schema=public
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Gereksinimler
|
||||
|
||||
- **Node.js:** v20.19+
|
||||
- **Docker + Docker Compose:** PostgreSQL + Redis için
|
||||
- **npm:** Paket yöneticisi
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Adım Adım Kurulum
|
||||
|
||||
### Adım 1: Kodu Çek
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Suggest-Bet-BE
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
### Adım 2: .env Dosyasını Oluştur
|
||||
|
||||
```bash
|
||||
# /Users/piton/Documents/Suggest-Bet-BE/.env
|
||||
NODE_ENV=development
|
||||
PORT=3005
|
||||
DATABASE_URL="postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db?schema=public"
|
||||
JWT_SECRET=9bfa42fbdc6031da6d7c0bd30e9f5b6378a071613d0c02acf95eb576249c3a25
|
||||
JWT_ACCESS_EXPIRATION=7d
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
REDIS_ENABLED=true
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=RedisSecure2026
|
||||
DEFAULT_LANGUAGE=en
|
||||
FALLBACK_LANGUAGE=en
|
||||
ENABLE_MAIL=false
|
||||
ENABLE_S3=false
|
||||
ENABLE_WEBSOCKET=false
|
||||
ENABLE_MULTI_TENANCY=false
|
||||
THROTTLE_TTL=60000
|
||||
THROTTLE_LIMIT=100
|
||||
ENABLE_GEMINI=true
|
||||
GOOGLE_API_KEY=your-google-api-key
|
||||
GEMINI_MODEL=gemini-2.5-flash
|
||||
AI_ENGINE_URL=http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
### Adım 3: Docker Infrastructure Başlat
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Suggest-Bet-BE
|
||||
docker compose up -d postgres redis
|
||||
```
|
||||
|
||||
PostgreSQL'in hazır olduğunu kontrol et:
|
||||
```bash
|
||||
docker exec -i suggestbet-postgres pg_isready -U suggestbet
|
||||
# Çıktı: /var/run/postgresql:5432 - accepting connections
|
||||
```
|
||||
|
||||
### Adım 4: Dump'u Restore Et
|
||||
|
||||
```bash
|
||||
# Dump dosyasını container'a kopyala
|
||||
docker cp /path/to/dump-boilerplate_db-202604020914-v5 suggestbet-postgres:/tmp/dump_file
|
||||
|
||||
# Restore et
|
||||
export PGPASSWORD="SuGGesT2026SecuRe"
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" suggestbet-postgres pg_restore \
|
||||
-U suggestbet -d boilerplate_db --clean --if-exists /tmp/dump_file
|
||||
```
|
||||
|
||||
### Adım 5: Sport Partition Migration'ını Çalıştır
|
||||
|
||||
**Sırayla çalıştır — her biri ayrı transaction:**
|
||||
|
||||
```bash
|
||||
export PGPASSWORD="SuGGesT2026SecuRe"
|
||||
DB="suggestbet-postgres"
|
||||
MIGRATION_DIR="prisma/migrations/20260403161000_sport_partition"
|
||||
|
||||
# 1. Yeni team stats tabloları oluştur
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i $DB psql -U suggestbet -d boilerplate_db < $MIGRATION_DIR/01_create_team_stats.sql
|
||||
|
||||
# 2. Team stats verilerini kopyala
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i $DB psql -U suggestbet -d boilerplate_db < $MIGRATION_DIR/02_copy_team_stats.sql
|
||||
|
||||
# 3. Yeni AI features tabloları oluştur
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i $DB psql -U suggestbet -d boilerplate_db < $MIGRATION_DIR/03_create_ai_features.sql
|
||||
|
||||
# 4. AI features verilerini kopyala
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i $DB psql -U suggestbet -d boilerplate_db < $MIGRATION_DIR/04_copy_ai_features.sql
|
||||
|
||||
# 5. match_player_stats → basketball_player_stats rename
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i $DB psql -U suggestbet -d boilerplate_db < $MIGRATION_DIR/05_rename_player_stats.sql
|
||||
|
||||
# 6. odd_categories + odd_selections'e sport kolonu ekle
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i $DB psql -U suggestbet -d boilerplate_db < $MIGRATION_DIR/06_add_sport_to_odds.sql
|
||||
```
|
||||
|
||||
**odd_selections için batch update (14.8M satır — her çalıştır 1M günceller):**
|
||||
|
||||
```bash
|
||||
# Bunu "remaining = 0" olana kadar tekrar tekrar çalıştır
|
||||
export PGPASSWORD="SuGGesT2026SecuRe"
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i suggestbet-postgres psql -U suggestbet -d boilerplate_db -c "
|
||||
WITH t AS (
|
||||
SELECT os.db_id, oc.sport
|
||||
FROM odd_selections os
|
||||
JOIN odd_categories oc ON os.odd_category_db_id = oc.db_id
|
||||
WHERE os.sport IS NULL
|
||||
LIMIT 1000000
|
||||
)
|
||||
UPDATE odd_selections SET sport = t.sport FROM t WHERE odd_selections.db_id = t.db_id;
|
||||
|
||||
SELECT COUNT(*) as remaining FROM odd_selections WHERE sport IS NULL;
|
||||
"
|
||||
```
|
||||
|
||||
**Kalan satırlar bitince index oluştur:**
|
||||
```bash
|
||||
export PGPASSWORD="SuGGesT2026SecuRe"
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i suggestbet-postgres psql -U suggestbet -d boilerplate_db -c "
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_odd_selections_sport ON odd_selections(sport) WHERE sport IS NOT NULL;
|
||||
"
|
||||
```
|
||||
|
||||
### Adım 6: Bağımlılıkları Yükle + Prisma Generate
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Suggest-Bet-BE
|
||||
|
||||
# Bağımlılıkları yükle
|
||||
npm ci
|
||||
|
||||
# Prisma client oluştur
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
### Adım 7: Build + Başlat
|
||||
|
||||
```bash
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Başlat
|
||||
npm run start:prod
|
||||
```
|
||||
|
||||
### Adım 8: Doğrulama
|
||||
|
||||
```bash
|
||||
# Sağlık kontrolü
|
||||
curl http://localhost:3005/api/health
|
||||
|
||||
# Swagger UI
|
||||
open http://localhost:3005/api/docs
|
||||
|
||||
# Yeni tabloları kontrol et
|
||||
export PGPASSWORD="SuGGesT2026SecuRe"
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i suggestbet-postgres psql -U suggestbet -d boilerplate_db -c "
|
||||
SELECT 'football_team_stats' as tbl, COUNT(*) FROM football_team_stats
|
||||
UNION ALL SELECT 'basketball_team_stats', COUNT(*) FROM basketball_team_stats
|
||||
UNION ALL SELECT 'basketball_player_stats', COUNT(*) FROM basketball_player_stats
|
||||
UNION ALL SELECT 'odd_categories (sport)', COUNT(*) FROM odd_categories WHERE sport IS NOT NULL
|
||||
UNION ALL SELECT 'odd_selections (sport)', COUNT(*) FROM odd_selections WHERE sport IS NOT NULL;
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI Engine (Opsiyonel)
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Suggest-Bet-BE/ai-engine
|
||||
|
||||
# Bağımlılıklar
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Başlat
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Tablo Durumu (Migration Sonrası)
|
||||
|
||||
| Tablo | Satır (~) | Durum |
|
||||
|-------|-----------|-------|
|
||||
| `football_team_stats` | 217,956 | ✅ Yeni |
|
||||
| `basketball_team_stats` | 48,824 | ✅ Yeni |
|
||||
| `basketball_player_stats` | 273,140 | ✅ Rename edildi |
|
||||
| `football_ai_features` | 0 | ⚠️ Feeder dolduracak |
|
||||
| `basketball_ai_features` | 0 | ⚠️ Feeder dolduracak |
|
||||
| `odd_categories (sport)` | 2,695,511 | ✅ Güncellendi |
|
||||
| `odd_selections (sport)` | 14,810,396 | ✅ Güncellendi |
|
||||
| `match_team_stats` (ESKİ) | 266,780 | 🗑️ Silinebilir (yedek olarak kalsın) |
|
||||
| `match_ai_features` (ESKİ) | 0 | 🗑️ Silinebilir |
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ Eski Tabloları Silme (Opsiyonel)
|
||||
|
||||
**SADECE her şey çalıştığını doğruladıktan sonra:**
|
||||
|
||||
```bash
|
||||
export PGPASSWORD="SuGGesT2026SecuRe"
|
||||
docker exec -e PGPASSWORD="$PGPASSWORD" -i suggestbet-postgres psql -U suggestbet -d boilerplate_db -c "
|
||||
DROP TABLE IF EXISTS match_team_stats CASCADE;
|
||||
DROP TABLE IF EXISTS match_ai_features CASCADE;
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Sorun Giderme
|
||||
|
||||
### PostgreSQL başlamıyor (postmaster.pid hatası)
|
||||
```bash
|
||||
docker compose stop postgres
|
||||
docker compose rm -f postgres
|
||||
docker volume rm suggest-bet-be_pgml_data
|
||||
docker compose up -d postgres
|
||||
# Sonra dump + migration tekrar
|
||||
```
|
||||
|
||||
### Docker Desktop başlamıyor (disk dolu)
|
||||
```bash
|
||||
# Büyük dosyaları temizle
|
||||
rm -rf ~/Library/Caches/Homebrew/*
|
||||
rm -rf ~/.npm/_cacache
|
||||
docker system prune -af
|
||||
df -h / # En az 3-4GB boş olmalı
|
||||
```
|
||||
|
||||
### Feeder çalışmıyor
|
||||
```bash
|
||||
# Logları kontrol et
|
||||
tail -f logs/app.log # veya docker logs suggestbet-app
|
||||
|
||||
# Manuel feeder çalıştır
|
||||
npm run feeder:live
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notlar
|
||||
|
||||
- **Veri kaybolmaz** — eski tablolar migration sonrası silinmez, yedek olarak kalır
|
||||
- **Feeder** otomatik yeni tablolara yazar (`footballTeamStats`, `basketballTeamStats`, vb.)
|
||||
- **Redis** opsiyonel — `REDIS_ENABLED=false` yapabilirsin (in-memory fallback)
|
||||
- **Swagger** sadece development modunda aktif
|
||||
+3
-3
@@ -16,7 +16,7 @@ RUN npm ci
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma client
|
||||
RUN npx prisma generate
|
||||
RUN DATABASE_URL="postgresql://dummy:dummy@localhost/dummy" npx prisma generate
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
@@ -38,7 +38,7 @@ RUN apk add --no-cache --virtual .build-deps python3 make g++ cairo-dev pango-de
|
||||
|
||||
# Copy Prisma schema and generate client
|
||||
COPY prisma ./prisma
|
||||
RUN npx prisma generate
|
||||
RUN DATABASE_URL="postgresql://dummy:dummy@localhost/dummy" npx prisma generate
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/dist ./dist
|
||||
@@ -47,7 +47,7 @@ COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/src/i18n ./dist/i18n
|
||||
|
||||
# Copy league filter config files (critical: without these, feeder stores ALL matches)
|
||||
COPY top_leagues.json basketball_top_leagues.json ./
|
||||
COPY qualified_leagues.json top_leagues.json basketball_top_leagues.json ./
|
||||
|
||||
# Set environment
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
# Suggest-Bet-BE — AI Agent Context
|
||||
|
||||
> **Last Updated:** 2026-04-06
|
||||
> **Purpose:** Comprehensive project reference for AI agents working on this codebase.
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
**Suggest-Bet-BE** is an **AI-powered sports betting prediction platform** backend. It provides:
|
||||
|
||||
- AI-driven predictions for football & basketball matches
|
||||
- Smart coupon generation (SAFE, BALANCED, AGGRESSIVE, VALUE, MIRACLE strategies)
|
||||
- Live score tracking & odds monitoring
|
||||
- Web scraping from Mackolik.com for historical & live match data
|
||||
- Google Gemini AI for natural language match commentary
|
||||
- User coupon tracking (ROI, Win Rate analytics)
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
| ----------- | -------------------------------------------- |
|
||||
| Backend API | NestJS 11 (TypeScript) |
|
||||
| AI Engine | Python FastAPI (v20+) |
|
||||
| Database | PostgreSQL 16 + Prisma ORM |
|
||||
| Queue | BullMQ + Redis (optional) |
|
||||
| Cache | Redis or in-memory fallback |
|
||||
| Auth | JWT + Passport (Access 15min + Refresh 7day) |
|
||||
| Scraping | Axios + Cheerio (Mackolik HTML parsing) |
|
||||
| Logging | Pino (structured logging) |
|
||||
| i18n | nestjs-i18n (TR, EN) |
|
||||
| API Docs | Swagger |
|
||||
| Deploy | Docker Compose |
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ CLIENTS (Web/Mobile) │
|
||||
└───────────────────────────────┬──────────────────────────────────┘
|
||||
│ HTTP/REST
|
||||
┌───────────────────────────────▼──────────────────────────────────┐
|
||||
│ NestJS Backend (Port 3005) │
|
||||
│ ┌─────────┬──────────┬──────────┬──────────┬─────────────────┐ │
|
||||
│ │ Auth │ Admin │ Matches │ Leagues │ Predictions │ │
|
||||
│ │ Module │ Module │ Module │ Module │ Module │ │
|
||||
│ ├─────────┼──────────┼──────────┼──────────┼─────────────────┤ │
|
||||
│ │ Coupons │ Analysis │ Gemini │ Social- │ Health │ │
|
||||
│ │ Module │ Module │ Module │ Poster │ Module │ │
|
||||
│ │SporToto │ Feeder │ Users │ │ │ │
|
||||
│ └─────────┴──────────┴──────────┴──────────┴─────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Services: AiService | MatchAnalysis | Scraper │ │
|
||||
│ ├──────────────────────────────────────────────────────────────┤ │
|
||||
│ │ Tasks: DataFetcher (Cron) | LiveUpdater | LimitResetter │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
────┬─────────────────┬────────────────────┬──────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌──────────────┐ ┌──────────────────┐
|
||||
│PostgreSQL│ │ Redis/BullMQ │ │ AI Engine (py) │
|
||||
│ (3.6GB) │ │ (Optional) │ │ FastAPI:8000 │
|
||||
└───────── └────────────── └──────────────────
|
||||
│
|
||||
───────▼───────┐
|
||||
│ Mackolik API │
|
||||
│ (Data Source) │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
### Database Statistics (~)
|
||||
|
||||
- `matches`: 237K permanent match records
|
||||
- `live_matches`: ~82 active/upcoming matches (daily cycle)
|
||||
- `match_player_participation`: 3.3M
|
||||
- `odd_selections`: 8.5M
|
||||
- `teams`: 19,595 | `players`: 217K | `leagues`: 1,505
|
||||
|
||||
---
|
||||
|
||||
## 3. Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.module.ts # Root module (Redis, Config, i18n, guards)
|
||||
├── main.ts # Entry point, Swagger, Helmet, ValidationPipe
|
||||
├── common/ # Shared layer
|
||||
│ ├── base/ # Generic BaseService<T> & BaseController<T>
|
||||
│ ├── types/ # ApiResponse<T>, pagination DTOs
|
||||
│ ├── filters/ # GlobalExceptionFilter (HTTP 200 wrapper)
|
||||
│ ├── interceptors/ # ResponseInterceptor, SanitizeInterceptor
|
||||
│ ├── decorators/ # @Public(), @Roles(), @CurrentUser()
|
||||
│ └── queues/ # BullMQ queue module
|
||||
├── config/ # Env validation (Zod), config factories
|
||||
├── database/ # PrismaService
|
||||
├── i18n/ # TR/EN translations (common, errors, validation, auth)
|
||||
├── modules/ # 13 feature modules
|
||||
│ ├── admin/ # Superadmin panel (user mgmt, settings, analytics)
|
||||
│ ├── analysis/ # Multi-match analysis orchestration
|
||||
│ ├── auth/ # JWT auth, refresh tokens, guards
|
||||
│ ├── coupons/ # SmartCouponService (5 strategies), UserCouponService
|
||||
│ ├── feeder/ # Historical data scraping (Mackolik)
|
||||
│ ├── gemini/ # Google Gemini AI integration
|
||||
│ ├── health/ # Liveness, readiness, AI Engine health
|
||||
│ ├── leagues/ # Country/league/team discovery, H2H
|
||||
│ ├── matches/ # Match listing, details, active leagues
|
||||
│ ├── predictions/ # AI predictions with BullMQ queue & 6h cache
|
||||
│ ├── social-poster/ # Twitter API v2, Canvas image generation
|
||||
│ ├── spor-toto/ # Spor Toto integration
|
||||
│ └── users/ # User CRUD (BaseController pattern)
|
||||
├── scripts/ # Feeder runners, cleanup scripts
|
||||
├── services/ # Shared services
|
||||
│ ├── ai.service.ts # Python AI Engine bridge
|
||||
│ ├── match-analysis.service.ts # 7-phase analysis orchestrator
|
||||
│ └── scraper.service.ts # Mackolik HTML scraping
|
||||
└── tasks/ # Cron jobs (15min, 30min, daily)
|
||||
├── data-fetcher.task.ts # Live matches, odds fetching
|
||||
├── live-updater.task.ts # Score updates, match finalization
|
||||
└── limit-resetter.task.ts # Usage limits, subscription expiry
|
||||
|
||||
ai-engine/ # Python FastAPI ML engine
|
||||
├── main.py # FastAPI app, routes
|
||||
├── services/ # single_match_orchestrator.py
|
||||
├── core/ # Core algorithms
|
||||
├── features/ # Feature engineering
|
||||
├── models/ # ML models
|
||||
├── training/ # Model training scripts
|
||||
├── config/ # Configuration
|
||||
├── utils/ # Utility functions
|
||||
└── tests/ # Test files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Key Modules
|
||||
|
||||
### Auth Module
|
||||
|
||||
- Register, Login, Refresh, Logout endpoints
|
||||
- bcrypt (12 rounds), JWT Access (15min) + Refresh Token (7 days, DB-stored)
|
||||
- Global guards: `JwtAuthGuard`, `RolesGuard`, `PermissionsGuard`
|
||||
|
||||
### Predictions Module
|
||||
|
||||
- Requires Redis (`REDIS_ENABLED=true`), conditionally loaded
|
||||
- BullMQ queue with worker processor
|
||||
- 6-hour TTL cache on prediction results
|
||||
- AI Engine call: `POST /v20plus/analyze/{matchId}`
|
||||
|
||||
### Coupons Module
|
||||
|
||||
- `SmartCouponService`: 5 strategies (SAFE ≥78% confidence/2 matches, BALANCED, AGGRESSIVE, VALUE EV+, MIRACLE)
|
||||
- `UserCouponService`: Coupon creation, bet settlement (MS 1/X/2, Alt/Üst, KG Var/Yok)
|
||||
|
||||
### Feeder Module
|
||||
|
||||
- Historical scraping from 2023-06-01 to present (reverse chronological)
|
||||
- Concurrency=20, 300ms delay, 50 max retry, 502 exponential backoff
|
||||
- Resume support with state management
|
||||
|
||||
### Analysis Module
|
||||
|
||||
- Usage limits: Free (10 analyses/3 coupons/day) vs Premium (50 analyses/10 coupons)
|
||||
- 7-phase flow: URL Parse → Scrape → Python Engine → Strategy → Similar Matches → Final Prediction → DB Save
|
||||
|
||||
### Social Poster Module
|
||||
|
||||
- Twitter API v2 integration
|
||||
- Canvas-based prediction card image generation
|
||||
- Gemini-powered Turkish caption generation
|
||||
|
||||
---
|
||||
|
||||
## 5. Scheduled Tasks (Cron)
|
||||
|
||||
| Task | Schedule | Description |
|
||||
| --------------------------- | -------------- | -------------------------------------------------------- |
|
||||
| `fetchLiveMatches()` | `*/15 * * * *` | Fetch football matches from Mackolik API |
|
||||
| `fetchOddsForPreMatches()` | `*/15 * * * *` | Fetch odds for upcoming matches (football + basketball) |
|
||||
| `fetchBasketballMatches()` | Manual | Basketball data via `basketball_top_leagues.json` filter |
|
||||
| `updateLiveScores()` | `*/15 * * * *` | Update live match scores |
|
||||
| `finalizeFinishedMatches()` | `*/30 * * * *` | Migrate finished: live_matches → matches table |
|
||||
| `resetUsageLimits()` | `0 3 * * *` | Reset daily usage limits (03:00 Istanbul time) |
|
||||
| `cleanupOldData()` | `0 4 * * *` | Delete 30-day old AI logs, 1-day finished live_matches |
|
||||
| `checkSubscriptions()` | `0 0 * * *` | Mark expired subscriptions |
|
||||
|
||||
---
|
||||
|
||||
## 6. AI Engine (Python FastAPI)
|
||||
|
||||
Independent microservice on port 8000.
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ---------------------------------- | ------------------------------- |
|
||||
| POST | `/v20plus/analyze/{match_id}` | Single match analysis (main) |
|
||||
| GET | `/v20plus/analyze-htms/{match_id}` | First half - Full time analysis |
|
||||
| GET | `/v20plus/analyze-htft/{match_id}` | HT/FT probabilities |
|
||||
| POST | `/v20plus/coupon` | Smart coupon generation |
|
||||
| GET | `/v20plus/daily-banker` | Daily banker picks |
|
||||
| GET | `/v20plus/reversal-watchlist` | Score reversal watchlist |
|
||||
| GET | `/health` | Health check |
|
||||
|
||||
### Output Structure (`SingleMatchPredictionPackage`)
|
||||
|
||||
```typescript
|
||||
{
|
||||
model_version: "v20plus.X",
|
||||
match_info: { match_id, match_name, home_team, away_team, league, match_date_ms },
|
||||
data_quality: { label: "HIGH"|"MEDIUM"|"LOW", score, flags, lineup_counts },
|
||||
risk: { level: "LOW"|"MEDIUM"|"HIGH"|"EXTREME", score, is_surprise_risk, warnings },
|
||||
main_pick: { market, pick, probability, confidence, odds, bet_grade, edge },
|
||||
value_pick: { ... },
|
||||
bet_advice: { playable, suggested_stake_units, reason },
|
||||
bet_summary: [{ market, pick, raw_confidence, calibrated_confidence, bet_grade }],
|
||||
supporting_picks: [...],
|
||||
aggressive_pick: { market, pick, probability, confidence, odds },
|
||||
scenario_top5: [{ score, prob }],
|
||||
score_prediction: { ft, ht, xg_home, xg_away, xg_total },
|
||||
market_board: { ... },
|
||||
reasoning_factors: string[],
|
||||
ai_commentary: string // Turkish commentary from Gemini
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. API Response Format
|
||||
|
||||
All responses follow this standard structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"status": 200,
|
||||
"message": "İşlem başarıyla tamamlandı", // i18n translated
|
||||
"data": { ... },
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
**Critical Rule:** Controllers must NEVER return raw Prisma entities. Always use Response DTOs with `@Exclude()` and `@Expose()` from `class-transformer`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
NODE_ENV=development
|
||||
PORT=3005
|
||||
DATABASE_URL=postgresql://user:password@localhost:15432/boilerplate_db
|
||||
JWT_SECRET=your-secret-key
|
||||
JWT_ACCESS_EXPIRATION=15m
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
REDIS_ENABLED=false
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
AI_ENGINE_URL=http://127.0.0.1:8000
|
||||
ENABLE_GEMINI=false
|
||||
GOOGLE_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
### Config Files
|
||||
|
||||
- `top_leagues.json` — Football top league IDs (live match filter)
|
||||
- `basketball_top_leagues.json` — Basketball top league IDs
|
||||
- `bet-type.json` — Bet type definitions
|
||||
|
||||
---
|
||||
|
||||
## 9. Build & Run Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run start:dev # Watch mode (port 3005)
|
||||
|
||||
# Production
|
||||
npm run build && npm run start:prod
|
||||
|
||||
# Feeder (Data Collection)
|
||||
npm run feeder:historical # Historical scraping (2023-06→present)
|
||||
npm run feeder:fill-gaps # Fill missing data
|
||||
npm run feeder:basketball # Basketball data
|
||||
npm run feeder:live # Live data
|
||||
|
||||
# Database
|
||||
npx prisma generate # Regenerate Prisma client
|
||||
npx prisma migrate dev # Run migrations
|
||||
npx prisma db seed # Seed database
|
||||
|
||||
# Testing
|
||||
npm run test # Unit tests
|
||||
npm run test:e2e # E2E tests
|
||||
npx jest src/path/to/file.spec.ts # Single test file
|
||||
|
||||
# Lint/Format
|
||||
npm run lint # ESLint with Prettier
|
||||
npm run format # Prettier write
|
||||
|
||||
# Docker
|
||||
docker-compose up -d postgres redis # Infrastructure
|
||||
docker-compose up -d # All services
|
||||
|
||||
# AI Engine (Python)
|
||||
cd ai-engine && uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
# Utility
|
||||
npm run swagger:summary # Export endpoint summary
|
||||
npm run cleanup:live # Cleanup live matches
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Code Style Guidelines
|
||||
|
||||
### Imports Order
|
||||
|
||||
```typescript
|
||||
// 1. NestJS/common imports
|
||||
import { Controller, Get, Post, Body } from '@nestjs/common';
|
||||
|
||||
// 2. External packages
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
// 3. Local imports (relative)
|
||||
import { UsersService } from './users.service';
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- Classes/Interfaces: `PascalCase`
|
||||
- Variables/Functions: `camelCase`
|
||||
- Constants: `UPPER_SNAKE_CASE`
|
||||
- Files: `kebab-case`
|
||||
- DTOs: `Entity + Dto` suffix (CreateUserDto, UpdateUserDto)
|
||||
|
||||
### Types
|
||||
|
||||
- `strictNullChecks: true` — null/undefined checks required
|
||||
- `noImplicitAny: false` — `any` allowed (Prisma dynamic access)
|
||||
- Specify function return types: `async findOne(id: string): Promise<User>`
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
// Use NestJS HTTP Exceptions with i18n keys
|
||||
throw new NotFoundException('USER_NOT_FOUND');
|
||||
throw new ConflictException('EMAIL_ALREADY_EXISTS');
|
||||
|
||||
// Reference src/i18n/{lang}/errors.json for available keys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Known Issues & Gotchas
|
||||
|
||||
1. **Predictions module** requires Redis. Disabled when `REDIS_ENABLED=false`.
|
||||
2. **Gemini AI** is optional. Returns `null` commentary when disabled.
|
||||
3. **Global Exception Filter** wraps all errors as HTTP 200 (status in body).
|
||||
4. **Lineup scraping** is disabled — only Team Stats are used (V20 optimization).
|
||||
5. **Feeder V17 AI feature calculation** is disabled — V20 model runs in Python.
|
||||
6. **BigInt serialization**: `BigInt.prototype.toJSON = function() { return this.toString(); }` polyfill in main.ts.
|
||||
7. **i18n assets** copied via `nest-cli.json` `"assets": ["i18n/**/*"]` config.
|
||||
|
||||
---
|
||||
|
||||
## 12. Reference Files for AI Agents
|
||||
|
||||
When working on this project, consult:
|
||||
|
||||
- `project_summary.md` — Comprehensive project documentation (Turkish)
|
||||
- `README.md` — Architecture decisions, quick start guide
|
||||
- `prompt.md` — AI assistant reference guide with agent roles
|
||||
- `AGENTS.md` — Coding guidelines, DTO patterns, test structure
|
||||
- `.agent/` — Skills and agent role definitions
|
||||
- `top_leagues.json` / `basketball_top_leagues.json` — League filters
|
||||
|
||||
---
|
||||
|
||||
## 13. Team Logos
|
||||
|
||||
Team logo URL template: `https://file.mackolikfeeds.com/teams/{teamId}`
|
||||
|
||||
---
|
||||
|
||||
## 14. 🆕 VQWEN Model Integration (Since 2026-04-06)
|
||||
|
||||
We have integrated a new high-performance prediction engine called **VQWEN v3**.
|
||||
|
||||
### VQWEN Model Features
|
||||
- **Accuracy:** +244.4 Units profit in Time-Series Backtest (75.1% Win Rate on BTTS/Over markets).
|
||||
- **Features Used:**
|
||||
- `ELO Ratings` (Real-time team strength).
|
||||
- `Contextual Goals` (Home/Away specific performance).
|
||||
- `Rest Days` (Fatigue factor for teams playing < 3 days).
|
||||
- `H2H Win Rate` (Historical dominance).
|
||||
- `Form Points` (Last 5 games streak).
|
||||
- `Squad Strength` (Based on starting XI participation).
|
||||
- **Files:**
|
||||
- `ai-engine/scripts/train_vqwen_v3.py` — Training script.
|
||||
- `ai-engine/services/single_match_orchestrator.py` — Integration point.
|
||||
- `ai-engine/models/vqwen/` — Pickle models (`vqwen_ms.pkl`, etc.).
|
||||
|
||||
### New Live Lineup/Sidelined Fetcher
|
||||
- **Problem:** `lineups` and `sidelined` columns in `live_matches` were empty.
|
||||
- **Fix:** Added `updateLineupsAndSidelined()` method to `src/tasks/data-fetcher.task.ts`.
|
||||
- **Mechanism:** Uses `FeederScraperService.fetchStartingFormation` directly via Cron (`*/15 * * * *`).
|
||||
- **Status:** Active.
|
||||
|
||||
### Database Schema Updates
|
||||
- **`substate` Column:** Added to `matches` table to track specific match states (e.g., "penalties", "overtime", "postponed").
|
||||
- **Sport Partition:** Tables are now partitioned by sport (`football_team_stats` vs `basketball_team_stats`).
|
||||
|
||||
---
|
||||
|
||||
## 16. 🔍 HT/FT Reversal Analysis (Since 2026-04-07)
|
||||
|
||||
### HT/FT Reversal (1/2 & 2/1) Pattern Detection
|
||||
|
||||
Reversal matches (İY/MS = 1/2 or 2/1) are statistically rare events that can indicate match-fixing or unusual patterns.
|
||||
|
||||
#### Key Findings (147,248 matches analyzed)
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| **Total Reversal Matches** | 13,112 (8.90%) |
|
||||
| **1/2 (Home leads HT, Away wins FT)** | 5,992 (4.07%) |
|
||||
| **2/1 (Away leads HT, Home wins FT)** | 7,120 (4.84%) |
|
||||
|
||||
#### 🚨 Basketball Leagues Have Suspiciously High Reversal Rates
|
||||
|
||||
| League | Reversals | Total | Rate |
|
||||
|--------|-----------|-------|------|
|
||||
| Eurobasket U20 | 36 | 120 | **30.00%** 🔴 |
|
||||
| EuroLeague 🏀 | 183 | 639 | **28.64%** 🔴 |
|
||||
| PBA Commissioners 🏀 | 54 | 189 | **28.57%** 🔴 |
|
||||
| Ulusal Süper Lig 🏀 | 148 | 547 | **27.06%** 🔴 |
|
||||
| NBA 🏀 | 656 | 2,696 | **24.33%** 🔴 |
|
||||
|
||||
**All top 15 leagues by reversal rate are BASKETBALL.** Football leagues show normal rates (5-8%).
|
||||
|
||||
#### Suspicious Patterns
|
||||
|
||||
1. **Comeback Magnitude:**
|
||||
- 1 goal/point: 36.1% (normal)
|
||||
- 2 goals/points: 13.1% (suspicious)
|
||||
- **3+ goals/points: 50.8%** 🔴 **EXTREMELY HIGH**
|
||||
|
||||
2. **Extreme Comebacks (Basketball):**
|
||||
- Mineros vs Irapuato: HT 39-45 → FT 102-61 (41 point swing!)
|
||||
- Utah vs Memphis: HT 65-64 → FT 103-140 (37 point swing!)
|
||||
- These are statistically near-impossible without manipulation
|
||||
|
||||
3. **Favorite Loss Rate:**
|
||||
- 42.7% of reversals had the pre-match favorite lose (should be ~25-30%)
|
||||
|
||||
#### Impact on Model
|
||||
|
||||
- HT/FT model accuracy: **20.3%** (low due to reversal noise)
|
||||
- Basketball reversal data creates **training noise**
|
||||
- **Recommendation:** Either exclude basketball from HT/FT training or train separate basketball-specific model
|
||||
|
||||
#### HT/FT Model Files
|
||||
|
||||
- **Training script:** `ai-engine/scripts/train_htft_vqwen.py`
|
||||
- **Model output:** `ai-engine/models/xgboost/xgb_ht_ft.json` + `.pkl`
|
||||
- **Features:** 27 (Odds + HT/FT Tendencies + League stats)
|
||||
- **Status:** Working, outputs 9-class probabilities in `market_board.HTFT.probs`
|
||||
|
||||
---
|
||||
|
||||
## 17. 🐛 Lineup Parsing Fix (Since 2026-04-07)
|
||||
|
||||
### Problem
|
||||
AI Engine reported `"lineup_unavailable"` and `"lineup_incomplete"` flags even when `live_matches.lineups` contained full 11/11 lineup data from Mackolik.
|
||||
|
||||
### Root Cause
|
||||
Mackolik stores lineups in `"stats"` key format:
|
||||
```json
|
||||
{
|
||||
"stats": {
|
||||
"home": [{ "personId": "...", "position": "...", ... }, ...],
|
||||
"away": [{ "personId": "...", "position": "...", ... }, ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
But the parser expected `"xi"`, `"starting"`, or `"lineup"` keys at root level.
|
||||
|
||||
### Fix
|
||||
Updated `_parse_lineups_json()` in `ai-engine/services/single_match_orchestrator.py`:
|
||||
- Added fallback to check `lineups_json.get("stats")` for home/away arrays
|
||||
- Now correctly parses Mackolik's nested format
|
||||
- Result: `home_lineup_count: 11`, `away_lineup_count: 11`, `lineup_source: "confirmed_live"`
|
||||
|
||||
---
|
||||
|
||||
## 18. Docker Deployment
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml services:
|
||||
services:
|
||||
app: # NestJS (port 3000→3000)
|
||||
postgres: # PostgreSQL 17 Alpine (port 15432:5432)
|
||||
redis: # Redis 7 Alpine (port 6379)
|
||||
adminer: # Database UI (dev profile, port 8080)
|
||||
ai-engine: # Python FastAPI (port 8002:8000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_This file is maintained for AI agent context. Update when architecture or conventions change._
|
||||
@@ -0,0 +1,337 @@
|
||||
# 🚀 Enterprise NestJS Boilerplate (Antigravity Edition)
|
||||
|
||||
[](https://nestjs.com/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://www.prisma.io/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.docker.com/)
|
||||
|
||||
> **FOR AI AGENTS & DEVELOPERS:** This documentation is structured to provide deep context, architectural decisions, and operational details to ensure seamless handover to any AI coding assistant (like Antigravity) or human developer.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Project Context & Architecture (Read Me First)
|
||||
|
||||
This is an **opinionated, production-ready** backend boilerplate built with NestJS. It is designed to be scalable, type-safe, and fully localized.
|
||||
|
||||
### 🏗️ Core Philosophy
|
||||
|
||||
- **Type Safety First:** Strict TypeScript configuration. `any` is forbidden. DTOs are the source of truth.
|
||||
- **Generic Abstraction:** `BaseService` and `BaseController` handle 80% of CRUD operations, allowing developers to focus on business logic.
|
||||
- **i18nNative:** Localization is not an afterthought. It is baked into the exception filters, response interceptors, and guards.
|
||||
- **Security by Default:** JWT Auth, RBAC (Role-Based Access Control), Throttling, and Helmet are pre-configured.
|
||||
|
||||
### 📐 Architectural Decision Records (ADR)
|
||||
|
||||
_To understand WHY things are the way they are:_
|
||||
|
||||
1. **Handling i18n Assets:**
|
||||
- **Problem:** Translation JSON files are not TypeScript code, so `tsc` ignores them during build.
|
||||
- **Solution:** We configured `nest-cli.json` with `"assets": ["i18n/**/*"]`. This ensures `src/i18n` is copied to `dist/i18n` automatically.
|
||||
- **Note:** When running with `node`, ensure `dist/main.js` can find these files.
|
||||
|
||||
2. **Global Response Wrapping:**
|
||||
- **Mechanism:** `ResponseInterceptor` wraps all successful responses.
|
||||
- **Feature:** It automatically translates the "Operation successful" message based on the `Accept-Language` header using `I18nService`.
|
||||
- **Output Format:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"status": 200,
|
||||
"message": "İşlem başarıyla tamamlandı", // Translated
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
3. **Centralized Error Handling:**
|
||||
- **Mechanism:** `GlobalExceptionFilter` catches all `HttpException` and unknown `Error` types.
|
||||
- **Feature:** It accepts error keys (e.g., `AUTH_REQUIRED`) and translates them using `i18n`. If a translation is found in `errors.json`, it is returned; otherwise, the original message is shown.
|
||||
|
||||
4. **UUID Generation:**
|
||||
- **Decision:** We use Node.js native `crypto.randomUUID()` instead of the external `uuid` package to avoid CommonJS/ESM compatibility issues.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start for AI & Humans
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- **Node.js:** v20.19+ (LTS)
|
||||
- **Docker:** For running PostgreSQL and Redis effortlessly.
|
||||
- **Package Manager:** `npm` (Lockfile: `package-lock.json`)
|
||||
|
||||
### 2. Environment Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# ⚠️ CRITICAL: Ensure DATABASE_URL includes the username!
|
||||
# Example: postgresql://postgres:password@localhost:15432/boilerplate_db
|
||||
# Required for v20 prediction flow:
|
||||
# AI_ENGINE_URL=http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
### 3. Installation & Database
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm ci
|
||||
|
||||
# Start Infrastructure (Postgres + Redis)
|
||||
docker-compose up -d postgres redis
|
||||
|
||||
# Generate Prisma Client (REQUIRED after install)
|
||||
npx prisma generate
|
||||
|
||||
# Run Migrations
|
||||
npx prisma migrate dev
|
||||
|
||||
# Seed Database (Optional - Creates Admin & Roles)
|
||||
npx prisma db seed
|
||||
```
|
||||
|
||||
### 4. Running the App
|
||||
|
||||
```bash
|
||||
# Debug Mode (Watch) - Best for Development
|
||||
npm run start:dev
|
||||
|
||||
# Production Build & Run
|
||||
npm run build
|
||||
npm run start:prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Response Standardization & Type Safety Protocol
|
||||
|
||||
This boilerplate enforces a strict **"No-Leak"** policy for API responses to ensure both Security and Developer Experience.
|
||||
|
||||
### 1. The `unknown` Type is Forbidden
|
||||
|
||||
- **Rule:** Controllers must NEVER return `ApiResponse<unknown>` or raw Prisma entities.
|
||||
- **Why:** Returning raw entities risks exposing sensitive fields like `password` hashes or internal metadata. It also breaks contract visibility for frontend developers.
|
||||
|
||||
### 2. DTO Pattern & Serialization
|
||||
|
||||
- **Tool:** We use `class-transformer` for all response serialization.
|
||||
- **Implementation:**
|
||||
- All Response DTOs must use `@Exclude()` class-level decorator.
|
||||
- Only fields explicitly marked with `@Expose()` are returned to the client.
|
||||
- Controllers use `plainToInstance(UserResponseDto, data)` before returning data.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Secure & Typed
|
||||
@Get('me')
|
||||
async getMe(@CurrentUser() user: User): Promise<ApiResponse<UserResponseDto>> {
|
||||
return createSuccessResponse(plainToInstance(UserResponseDto, user));
|
||||
}
|
||||
|
||||
// ❌ Bad: Leaks password hash & Weak Types
|
||||
@Get('me')
|
||||
async getMe(@CurrentUser() user: User) {
|
||||
return createSuccessResponse(user);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ High-Performance Caching (Redis Strategy)
|
||||
|
||||
To ensure enterprise-grade performance, we utilize **Redis** for caching frequently accessed data (e.g., Roles, Permissions).
|
||||
|
||||
- **Library:** `@nestjs/cache-manager` with `cache-manager-redis-yet` (Supports Redis v6+ / v7).
|
||||
- **Configuration:** Global Cache Module in `AppModule`.
|
||||
- **Strategy:** Read-heavy endpoints use `@UseInterceptors(CacheInterceptor)`.
|
||||
- **Invalidation:** Write operations (Create/Update/Delete) manually invalidate relevant cache keys.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// 1. Automatic Caching
|
||||
@Get('roles')
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@CacheKey('roles_list') // Unique Key
|
||||
@CacheTTL(60000) // 60 Seconds
|
||||
async getAllRoles() { ... }
|
||||
|
||||
// 2. Manual Invalidation (Inject CACHE_MANAGER)
|
||||
async createRole(...) {
|
||||
// ... create role logic
|
||||
await this.cacheManager.del('roles_list'); // Clear cache
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Gemini AI Integration (Optional)
|
||||
|
||||
This boilerplate includes an **optional** AI module powered by Google's Gemini API. It's disabled by default and can be enabled during CLI setup or manually.
|
||||
|
||||
### Configuration
|
||||
|
||||
Add these to your `.env` file:
|
||||
|
||||
```env
|
||||
# Enable Gemini AI features
|
||||
ENABLE_GEMINI=true
|
||||
|
||||
# Your Google API Key (get from https://aistudio.google.com/apikey)
|
||||
GOOGLE_API_KEY=your-api-key-here
|
||||
|
||||
# Model to use (optional, defaults to gemini-2.5-flash)
|
||||
GEMINI_MODEL=gemini-2.5-flash
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
The `GeminiService` is globally available when enabled:
|
||||
|
||||
```typescript
|
||||
import { GeminiService } from './modules/gemini';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(private readonly gemini: GeminiService) {}
|
||||
|
||||
async generateContent() {
|
||||
// Check if Gemini is available
|
||||
if (!this.gemini.isAvailable()) {
|
||||
throw new Error('AI features are not enabled');
|
||||
}
|
||||
|
||||
// 1. Simple Text Generation
|
||||
const { text, usage } = await this.gemini.generateText(
|
||||
'Write a product description for a coffee mug',
|
||||
);
|
||||
|
||||
// 2. With System Prompt & Options
|
||||
const { text } = await this.gemini.generateText('Translate: Hello World', {
|
||||
systemPrompt: 'You are a professional Turkish translator',
|
||||
temperature: 0.3,
|
||||
maxTokens: 500,
|
||||
});
|
||||
|
||||
// 3. Multi-turn Chat
|
||||
const { text } = await this.gemini.chat([
|
||||
{ role: 'user', content: 'What is TypeScript?' },
|
||||
{
|
||||
role: 'model',
|
||||
content: 'TypeScript is a typed superset of JavaScript...',
|
||||
},
|
||||
{ role: 'user', content: 'Give me an example' },
|
||||
]);
|
||||
|
||||
// 4. Structured JSON Output
|
||||
interface ProductData {
|
||||
name: string;
|
||||
price: number;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
const { data } = await this.gemini.generateJSON<ProductData>(
|
||||
'Generate a product entry for a wireless mouse',
|
||||
'{ name: string, price: number, features: string[] }',
|
||||
);
|
||||
console.log(data.name, data.price); // Fully typed!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Methods
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------------- | ------------------------------------------------ |
|
||||
| `isAvailable()` | Check if Gemini is properly configured and ready |
|
||||
| `generateText(prompt, options?)` | Generate text from a single prompt |
|
||||
| `chat(messages, options?)` | Multi-turn conversation |
|
||||
| `generateJSON<T>(prompt, schema, options?)` | Generate and parse structured JSON |
|
||||
|
||||
### Options
|
||||
|
||||
```typescript
|
||||
interface GeminiGenerateOptions {
|
||||
model?: string; // Override default model
|
||||
systemPrompt?: string; // System instructions
|
||||
temperature?: number; // Creativity (0-1)
|
||||
maxTokens?: number; // Max response length
|
||||
}
|
||||
```
|
||||
|
||||
## 🌍 Internationalization (i18n) Guide
|
||||
|
||||
Unique to this project is the deep integration of `nestjs-i18n`.
|
||||
|
||||
- **Location:** `src/i18n/{lang}/`
|
||||
- **Files:**
|
||||
- `common.json`: Generic messages (success, welcome)
|
||||
- `errors.json`: Error codes (AUTH_REQUIRED, USER_NOT_FOUND)
|
||||
- `validation.json`: Validation messages (IS_EMAIL)
|
||||
- `auth.json`: Auth specific success messages (LOGIN_SUCCESS)
|
||||
|
||||
**How to Translate a New Error:**
|
||||
|
||||
1. Throw an exception with a key: `throw new ConflictException('EMAIL_EXISTS');`
|
||||
2. Add `"EMAIL_EXISTS": "Email already taken"` to `src/i18n/en/errors.json`.
|
||||
3. Add Turkish translation to `src/i18n/tr/errors.json`.
|
||||
4. Start server; the `GlobalExceptionFilter` handles the rest.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing & CI/CD
|
||||
|
||||
- **GitHub Actions:** `.github/workflows/ci.yml` handles build and linting checks on push.
|
||||
- **Local Testing:**
|
||||
```bash
|
||||
npm run test # Unit tests
|
||||
npm run test:e2e # End-to-End tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📂 System Map (Directory Structure)
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.module.ts # Root module (Redis, Config, i18n setup)
|
||||
├── main.ts # Entry point
|
||||
├── common/ # Shared resources
|
||||
│ ├── base/ # Abstract BaseService & BaseController (CRUD)
|
||||
│ ├── types/ # Interfaces (ApiResponse, PaginatedData)
|
||||
│ ├── filters/ # Global Exception Filter
|
||||
│ └── interceptors/ # Response Interceptor
|
||||
├── config/ # Application configuration
|
||||
├── database/ # Prisma Service
|
||||
├── i18n/ # Localization assets
|
||||
└── modules/ # Feature modules
|
||||
├── admin/ # Admin capabilities (Roles, Permissions + Caching)
|
||||
│ ├── admin.controller.ts
|
||||
│ └── dto/ # Admin Response DTOs
|
||||
├── auth/ # Authentication layer
|
||||
├── gemini/ # 🤖 Optional AI module (Google Gemini)
|
||||
├── health/ # Health checks
|
||||
└── users/ # User management
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting (Known Issues)
|
||||
|
||||
**1. `EADDRINUSE: address already in use`**
|
||||
|
||||
- **Fix:** `lsof -ti:3000 | xargs kill -9`
|
||||
|
||||
**2. `PrismaClientInitializationError` / Database Connection Hangs**
|
||||
|
||||
- **Fix:** Check `.env` `DATABASE_URL`. Ensure `docker-compose up` is running.
|
||||
|
||||
**3. Cache Manager Deprecation Warnings**
|
||||
|
||||
- **Context:** `cache-manager-redis-yet` may show deprecation warnings regarding `Keyv`. This is expected as we wait for the ecosystem to stabilize on `cache-manager` v6/v7. The current implementation is fully functional.
|
||||
|
||||
---
|
||||
|
||||
## 📃 License
|
||||
|
||||
This project is proprietary and confidential.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
*.egg
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
|
||||
# Virtual environment
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Test & Coverage
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
*.cover
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Training data (large CSVs)
|
||||
data/training_data*.csv
|
||||
|
||||
# Reports (generated at runtime)
|
||||
reports/
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
# --- AI Engine Dockerfile ---
|
||||
# Python 3.11 with v20+ prediction stack (XGBoost + LightGBM)
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# System dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
curl \
|
||||
libgomp1 \
|
||||
procps \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Python dependencies
|
||||
# Install PyTorch CPU version separately to save space
|
||||
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
|
||||
|
||||
# Copy requirements (without torch)
|
||||
COPY requirements-docker.txt requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create models directory
|
||||
RUN mkdir -p /app/models
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')" || exit 1
|
||||
|
||||
# Start FastAPI with uvicorn
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,874 @@
|
||||
{
|
||||
"meta":{"test_sets":["test"],"test_metrics":[{"best_value":"Min","name":"Logloss"}],"learn_metrics":[{"best_value":"Min","name":"Logloss"}],"launch_mode":"Train","parameters":"","iteration_count":2000,"learn_sets":["learn"],"name":"experiment"},
|
||||
"iterations":[
|
||||
{"learn":[0.692389481],"iteration":0,"passed_time":0.04679785798,"remaining_time":93.54891809,"test":[0.6924099937]},
|
||||
{"learn":[0.6916338586],"iteration":1,"passed_time":0.08350330552,"remaining_time":83.41980222,"test":[0.6916660956]},
|
||||
{"learn":[0.6910159214],"iteration":2,"passed_time":0.132821758,"remaining_time":88.41501689,"test":[0.691108145]},
|
||||
{"learn":[0.6903417151],"iteration":3,"passed_time":0.162826233,"remaining_time":81.25029026,"test":[0.6904585078]},
|
||||
{"learn":[0.6896961461],"iteration":4,"passed_time":0.1969265393,"remaining_time":78.57368918,"test":[0.689812816]},
|
||||
{"learn":[0.6890979366],"iteration":5,"passed_time":0.2309352918,"remaining_time":76.74749531,"test":[0.689192261]},
|
||||
{"learn":[0.6884946167],"iteration":6,"passed_time":0.2693987513,"remaining_time":76.70167304,"test":[0.6886032715]},
|
||||
{"learn":[0.6879503686],"iteration":7,"passed_time":0.3199759681,"remaining_time":79.67401607,"test":[0.6880706742]},
|
||||
{"learn":[0.6874528094],"iteration":8,"passed_time":0.3645802206,"remaining_time":80.65324659,"test":[0.6876192378]},
|
||||
{"learn":[0.6869036785],"iteration":9,"passed_time":0.4116507506,"remaining_time":81.91849936,"test":[0.6870868859]},
|
||||
{"learn":[0.6863761921],"iteration":10,"passed_time":0.4562469316,"remaining_time":82.49774064,"test":[0.6865493528]},
|
||||
{"learn":[0.6859038678],"iteration":11,"passed_time":0.491541699,"remaining_time":81.43207481,"test":[0.686105086]},
|
||||
{"learn":[0.685410175],"iteration":12,"passed_time":0.5221556769,"remaining_time":79.80948692,"test":[0.6856345086]},
|
||||
{"learn":[0.6849483392],"iteration":13,"passed_time":0.5553110353,"remaining_time":78.77483686,"test":[0.6852027185]},
|
||||
{"learn":[0.6845417792],"iteration":14,"passed_time":0.5952927147,"remaining_time":78.77706925,"test":[0.6848238481]},
|
||||
{"learn":[0.6841038875],"iteration":15,"passed_time":0.6300274185,"remaining_time":78.12339989,"test":[0.6844045699]},
|
||||
{"learn":[0.6836957422],"iteration":16,"passed_time":0.662600544,"remaining_time":77.29040464,"test":[0.6840077621]},
|
||||
{"learn":[0.6832947461],"iteration":17,"passed_time":0.7004221698,"remaining_time":77.12426337,"test":[0.6836197496]},
|
||||
{"learn":[0.6829014105],"iteration":18,"passed_time":0.7300844347,"remaining_time":76.12090869,"test":[0.6832475033]},
|
||||
{"learn":[0.6825264546],"iteration":19,"passed_time":0.7641559459,"remaining_time":75.65143865,"test":[0.6829012069]},
|
||||
{"learn":[0.6822106577],"iteration":20,"passed_time":0.8040792063,"remaining_time":75.77489282,"test":[0.6825880966]},
|
||||
{"learn":[0.6818649349],"iteration":21,"passed_time":0.8356039756,"remaining_time":75.12839381,"test":[0.6822424968]},
|
||||
{"learn":[0.6815467855],"iteration":22,"passed_time":0.8861440327,"remaining_time":76.16985881,"test":[0.6819180513]},
|
||||
{"learn":[0.6812293319],"iteration":23,"passed_time":0.920219319,"remaining_time":75.76472393,"test":[0.6816384467]},
|
||||
{"learn":[0.6808837443],"iteration":24,"passed_time":0.960164738,"remaining_time":75.8530143,"test":[0.6813262593]},
|
||||
{"learn":[0.6805816494],"iteration":25,"passed_time":0.9895547925,"remaining_time":75.13004463,"test":[0.6810353411]},
|
||||
{"learn":[0.6803209634],"iteration":26,"passed_time":1.025550161,"remaining_time":74.94112844,"test":[0.6808138172]},
|
||||
{"learn":[0.6800350862],"iteration":27,"passed_time":1.060852064,"remaining_time":74.71429535,"test":[0.6805550049]},
|
||||
{"learn":[0.6797703947],"iteration":28,"passed_time":1.10467538,"remaining_time":75.07983357,"test":[0.680347991]},
|
||||
{"learn":[0.6794926675],"iteration":29,"passed_time":1.141766834,"remaining_time":74.97602208,"test":[0.680089679]},
|
||||
{"learn":[0.6792251865],"iteration":30,"passed_time":1.180421588,"remaining_time":74.9758099,"test":[0.6798451919]},
|
||||
{"learn":[0.6789670166],"iteration":31,"passed_time":1.213674604,"remaining_time":74.64098814,"test":[0.6796090443]},
|
||||
{"learn":[0.678722402],"iteration":32,"passed_time":1.245848393,"remaining_time":74.26011482,"test":[0.6793890865]},
|
||||
{"learn":[0.678476935],"iteration":33,"passed_time":1.287262512,"remaining_time":74.43406171,"test":[0.6791683772]},
|
||||
{"learn":[0.6782297335],"iteration":34,"passed_time":1.327473991,"remaining_time":74.52818262,"test":[0.6789766369]},
|
||||
{"learn":[0.6780226701],"iteration":35,"passed_time":1.3760549,"remaining_time":75.07143955,"test":[0.6787930242]},
|
||||
{"learn":[0.6778291026],"iteration":36,"passed_time":1.427620019,"remaining_time":75.74102965,"test":[0.6786087714]},
|
||||
{"learn":[0.6776045324],"iteration":37,"passed_time":1.468182407,"remaining_time":75.80457587,"test":[0.6784161299]},
|
||||
{"learn":[0.6773969079],"iteration":38,"passed_time":1.508647379,"remaining_time":75.85788487,"test":[0.6782227897]},
|
||||
{"learn":[0.6771819602],"iteration":39,"passed_time":1.549435187,"remaining_time":75.92232419,"test":[0.6780242369]},
|
||||
{"learn":[0.6769816736],"iteration":40,"passed_time":1.586036608,"remaining_time":75.78160282,"test":[0.6778499631]},
|
||||
{"learn":[0.6767984027],"iteration":41,"passed_time":1.621458864,"remaining_time":75.59086802,"test":[0.6776975784]},
|
||||
{"learn":[0.6766201184],"iteration":42,"passed_time":1.663424818,"remaining_time":75.70517136,"test":[0.6775231674]},
|
||||
{"learn":[0.6764394377],"iteration":43,"passed_time":1.70110089,"remaining_time":75.62166686,"test":[0.6773582124]},
|
||||
{"learn":[0.6762698797],"iteration":44,"passed_time":1.739954496,"remaining_time":75.59135644,"test":[0.6772234666]},
|
||||
{"learn":[0.6760974263],"iteration":45,"passed_time":1.776461223,"remaining_time":75.46098325,"test":[0.6770659843]},
|
||||
{"learn":[0.6759245179],"iteration":46,"passed_time":1.819761638,"remaining_time":75.61690381,"test":[0.6769049529]},
|
||||
{"learn":[0.6757673909],"iteration":47,"passed_time":1.869479807,"remaining_time":76.02551217,"test":[0.6767664194]},
|
||||
{"learn":[0.6756172628],"iteration":48,"passed_time":1.916010121,"remaining_time":76.28848462,"test":[0.6766584917]},
|
||||
{"learn":[0.675474531],"iteration":49,"passed_time":1.953635244,"remaining_time":76.19177452,"test":[0.6765507257]},
|
||||
{"learn":[0.6753286933],"iteration":50,"passed_time":1.993876686,"remaining_time":76.19736591,"test":[0.6764489911]},
|
||||
{"learn":[0.6751900513],"iteration":51,"passed_time":2.038943041,"remaining_time":76.38194316,"test":[0.6763947956]},
|
||||
{"learn":[0.6750574835],"iteration":52,"passed_time":2.080276765,"remaining_time":76.42073325,"test":[0.6762778712]},
|
||||
{"learn":[0.6749329567],"iteration":53,"passed_time":2.158576742,"remaining_time":77.78871001,"test":[0.6761865366]},
|
||||
{"learn":[0.6748033265],"iteration":54,"passed_time":2.220619687,"remaining_time":78.52918711,"test":[0.6760679685]},
|
||||
{"learn":[0.6746797823],"iteration":55,"passed_time":2.286959228,"remaining_time":79.39015604,"test":[0.6759774874]},
|
||||
{"learn":[0.674535525],"iteration":56,"passed_time":2.328472096,"remaining_time":79.3723032,"test":[0.6758500622]},
|
||||
{"learn":[0.6744256514],"iteration":57,"passed_time":2.367031568,"remaining_time":79.25474665,"test":[0.6757625065]},
|
||||
{"learn":[0.674310819],"iteration":58,"passed_time":2.409161286,"remaining_time":79.25732298,"test":[0.6756876412]},
|
||||
{"learn":[0.6741967947],"iteration":59,"passed_time":2.444825903,"remaining_time":79.04937087,"test":[0.6756151069]},
|
||||
{"learn":[0.6740879654],"iteration":60,"passed_time":2.48484996,"remaining_time":78.98564055,"test":[0.6755303655]},
|
||||
{"learn":[0.6739772476],"iteration":61,"passed_time":2.521603395,"remaining_time":78.8204416,"test":[0.6754565036]},
|
||||
{"learn":[0.67388281],"iteration":62,"passed_time":2.554102332,"remaining_time":78.5285114,"test":[0.6753738983]},
|
||||
{"learn":[0.6737789726],"iteration":63,"passed_time":2.593937938,"remaining_time":78.46662263,"test":[0.6752897299]},
|
||||
{"learn":[0.6736812332],"iteration":64,"passed_time":2.623889155,"remaining_time":78.11116175,"test":[0.6752115539]},
|
||||
{"learn":[0.6735930009],"iteration":65,"passed_time":2.660795108,"remaining_time":77.96935967,"test":[0.6751595431]},
|
||||
{"learn":[0.6734947116],"iteration":66,"passed_time":2.695822592,"remaining_time":77.77649358,"test":[0.6750764658]},
|
||||
{"learn":[0.6733961481],"iteration":67,"passed_time":2.725876686,"remaining_time":77.44696703,"test":[0.6750179194]},
|
||||
{"learn":[0.6732990195],"iteration":68,"passed_time":2.761848366,"remaining_time":77.29172746,"test":[0.6749408803]},
|
||||
{"learn":[0.6732133575],"iteration":69,"passed_time":2.791847449,"remaining_time":76.97522253,"test":[0.6748795802]},
|
||||
{"learn":[0.673111539],"iteration":70,"passed_time":2.824541003,"remaining_time":76.73999429,"test":[0.674790372]},
|
||||
{"learn":[0.6730080451],"iteration":71,"passed_time":2.861023716,"remaining_time":76.61185729,"test":[0.6747239773]},
|
||||
{"learn":[0.6729157861],"iteration":72,"passed_time":2.897136588,"remaining_time":76.47646857,"test":[0.6746701254]},
|
||||
{"learn":[0.6728347949],"iteration":73,"passed_time":2.935718661,"remaining_time":76.40802894,"test":[0.6746120937]},
|
||||
{"learn":[0.6727640693],"iteration":74,"passed_time":3.040023476,"remaining_time":78.02726921,"test":[0.6745550085]},
|
||||
{"learn":[0.6726808811],"iteration":75,"passed_time":3.097341794,"remaining_time":78.41165279,"test":[0.6744855074]},
|
||||
{"learn":[0.6726029645],"iteration":76,"passed_time":3.152948955,"remaining_time":78.74182909,"test":[0.6744264172]},
|
||||
{"learn":[0.6725356026],"iteration":77,"passed_time":3.216126808,"remaining_time":79.24866314,"test":[0.674381715]},
|
||||
{"learn":[0.6724606887],"iteration":78,"passed_time":3.256861302,"remaining_time":79.19532355,"test":[0.6743331681]},
|
||||
{"learn":[0.6723849561],"iteration":79,"passed_time":3.305679851,"remaining_time":79.33631641,"test":[0.67428564]},
|
||||
{"learn":[0.6723050519],"iteration":80,"passed_time":3.348083566,"remaining_time":79.32064647,"test":[0.6742202413]},
|
||||
{"learn":[0.6722508802],"iteration":81,"passed_time":3.38129387,"remaining_time":79.08928832,"test":[0.6741620971]},
|
||||
{"learn":[0.6721773904],"iteration":82,"passed_time":3.41660066,"remaining_time":78.91112609,"test":[0.6741109453]},
|
||||
{"learn":[0.6721007598],"iteration":83,"passed_time":3.48099347,"remaining_time":79.39980344,"test":[0.6740556003]},
|
||||
{"learn":[0.6720353564],"iteration":84,"passed_time":3.535359896,"remaining_time":79.64957884,"test":[0.6740146772]},
|
||||
{"learn":[0.6719790902],"iteration":85,"passed_time":3.581806996,"remaining_time":79.71603012,"test":[0.673983295]},
|
||||
{"learn":[0.6719140024],"iteration":86,"passed_time":3.612293661,"remaining_time":79.42893993,"test":[0.6739595301]},
|
||||
{"learn":[0.6718573633],"iteration":87,"passed_time":3.644530261,"remaining_time":79.18570293,"test":[0.6739336659]},
|
||||
{"learn":[0.671795602],"iteration":88,"passed_time":3.67809653,"remaining_time":78.97575809,"test":[0.673890361]},
|
||||
{"learn":[0.6717369134],"iteration":89,"passed_time":3.712417516,"remaining_time":78.78574951,"test":[0.673863586]},
|
||||
{"learn":[0.6716711079],"iteration":90,"passed_time":3.743502971,"remaining_time":78.53128759,"test":[0.6738190616]},
|
||||
{"learn":[0.6716070843],"iteration":91,"passed_time":3.775351679,"remaining_time":78.2975109,"test":[0.6737799295]},
|
||||
{"learn":[0.6715517232],"iteration":92,"passed_time":3.806186247,"remaining_time":78.04728142,"test":[0.6737364374]},
|
||||
{"learn":[0.6714957378],"iteration":93,"passed_time":3.83798807,"remaining_time":77.82133257,"test":[0.6737093719]},
|
||||
{"learn":[0.6714364567],"iteration":94,"passed_time":3.871278973,"remaining_time":77.62933099,"test":[0.6736630475]},
|
||||
{"learn":[0.6713881758],"iteration":95,"passed_time":3.913531039,"remaining_time":77.6183656,"test":[0.67364367]},
|
||||
{"learn":[0.6713336502],"iteration":96,"passed_time":3.945433866,"remaining_time":77.40371802,"test":[0.6735998081]},
|
||||
{"learn":[0.6712700267],"iteration":97,"passed_time":3.989716281,"remaining_time":77.43306496,"test":[0.6735526984]},
|
||||
{"learn":[0.6712154424],"iteration":98,"passed_time":4.020621946,"remaining_time":77.20406384,"test":[0.6735012924]},
|
||||
{"learn":[0.6711600413],"iteration":99,"passed_time":4.053732144,"remaining_time":77.02091074,"test":[0.6734818024]},
|
||||
{"learn":[0.6711060533],"iteration":100,"passed_time":4.084124711,"remaining_time":76.78963194,"test":[0.6734379341]},
|
||||
{"learn":[0.6710494943],"iteration":101,"passed_time":4.116434744,"remaining_time":76.59797199,"test":[0.6734059869]},
|
||||
{"learn":[0.6709936897],"iteration":102,"passed_time":4.148330356,"remaining_time":76.40177365,"test":[0.6733740852]},
|
||||
{"learn":[0.6709472183],"iteration":103,"passed_time":4.176511193,"remaining_time":76.14101176,"test":[0.6733330971]},
|
||||
{"learn":[0.6708914508],"iteration":104,"passed_time":4.2025065,"remaining_time":75.84523636,"test":[0.6733060254]},
|
||||
{"learn":[0.6708388195],"iteration":105,"passed_time":4.232975206,"remaining_time":75.63448151,"test":[0.6732755898]},
|
||||
{"learn":[0.6707885854],"iteration":106,"passed_time":4.261364958,"remaining_time":75.39031649,"test":[0.6732294722]},
|
||||
{"learn":[0.6707454167],"iteration":107,"passed_time":4.290824713,"remaining_time":75.1688922,"test":[0.6732035176]},
|
||||
{"learn":[0.6706973013],"iteration":108,"passed_time":4.324192493,"remaining_time":75.01878903,"test":[0.673196437]},
|
||||
{"learn":[0.6706577031],"iteration":109,"passed_time":4.351512102,"remaining_time":74.76688976,"test":[0.6731652709]},
|
||||
{"learn":[0.67061108],"iteration":110,"passed_time":4.38641502,"remaining_time":74.64808984,"test":[0.673138808]},
|
||||
{"learn":[0.6705625485],"iteration":111,"passed_time":4.424063991,"remaining_time":74.57707871,"test":[0.6731062725]},
|
||||
{"learn":[0.6705146484],"iteration":112,"passed_time":4.45863849,"remaining_time":74.45531709,"test":[0.6730726625]},
|
||||
{"learn":[0.6704704423],"iteration":113,"passed_time":4.497153675,"remaining_time":74.40027922,"test":[0.6730285927]},
|
||||
{"learn":[0.6704155922],"iteration":114,"passed_time":4.533368584,"remaining_time":74.30782417,"test":[0.6729872702]},
|
||||
{"learn":[0.6703687117],"iteration":115,"passed_time":4.564651269,"remaining_time":74.13623268,"test":[0.6729721425]},
|
||||
{"learn":[0.6703324232],"iteration":116,"passed_time":4.596824343,"remaining_time":73.98136956,"test":[0.6729564624]},
|
||||
{"learn":[0.6702884624],"iteration":117,"passed_time":4.628377967,"remaining_time":73.81870623,"test":[0.6729312424]},
|
||||
{"learn":[0.670253478],"iteration":118,"passed_time":4.668052254,"remaining_time":73.78660748,"test":[0.6729354345]},
|
||||
{"learn":[0.6702140804],"iteration":119,"passed_time":4.692108266,"remaining_time":73.50969617,"test":[0.6729085401]},
|
||||
{"learn":[0.6701682529],"iteration":120,"passed_time":4.723741667,"remaining_time":73.354633,"test":[0.6728898322]},
|
||||
{"learn":[0.6701320588],"iteration":121,"passed_time":4.756626425,"remaining_time":73.22085595,"test":[0.6728773638]},
|
||||
{"learn":[0.6700939824],"iteration":122,"passed_time":4.788008428,"remaining_time":73.06578714,"test":[0.6728618874]},
|
||||
{"learn":[0.6700655902],"iteration":123,"passed_time":4.815546648,"remaining_time":72.85456058,"test":[0.6728540413]},
|
||||
{"learn":[0.6700190743],"iteration":124,"passed_time":4.843186806,"remaining_time":72.64780209,"test":[0.6728441291]},
|
||||
{"learn":[0.6699792296],"iteration":125,"passed_time":4.875548614,"remaining_time":72.51411192,"test":[0.672815631]},
|
||||
{"learn":[0.6699379404],"iteration":126,"passed_time":4.916953662,"remaining_time":72.51538748,"test":[0.6728082021]},
|
||||
{"learn":[0.669895454],"iteration":127,"passed_time":4.952918369,"remaining_time":72.43643115,"test":[0.6727900064]},
|
||||
{"learn":[0.6698563938],"iteration":128,"passed_time":4.991585558,"remaining_time":72.39733782,"test":[0.6727649552]},
|
||||
{"learn":[0.6698215571],"iteration":129,"passed_time":5.028084166,"remaining_time":72.32705685,"test":[0.6727467657]},
|
||||
{"learn":[0.6697857067],"iteration":130,"passed_time":5.059198996,"remaining_time":72.18048033,"test":[0.6727396032]},
|
||||
{"learn":[0.6697449303],"iteration":131,"passed_time":5.096035515,"remaining_time":72.1166238,"test":[0.6727245271]},
|
||||
{"learn":[0.6697052425],"iteration":132,"passed_time":5.125282589,"remaining_time":71.94663604,"test":[0.6726955143]},
|
||||
{"learn":[0.6696695553],"iteration":133,"passed_time":5.156392608,"remaining_time":71.80469109,"test":[0.67269209]},
|
||||
{"learn":[0.6696269265],"iteration":134,"passed_time":5.190402292,"remaining_time":71.70444647,"test":[0.672677932]},
|
||||
{"learn":[0.6695969271],"iteration":135,"passed_time":5.221466142,"remaining_time":71.56480065,"test":[0.6726540285]},
|
||||
{"learn":[0.6695489786],"iteration":136,"passed_time":5.251144663,"remaining_time":71.40790151,"test":[0.6726288583]},
|
||||
{"learn":[0.6695173859],"iteration":137,"passed_time":5.274361693,"remaining_time":71.16566285,"test":[0.6725863431]},
|
||||
{"learn":[0.6694811164],"iteration":138,"passed_time":5.309398952,"remaining_time":71.08483058,"test":[0.6725837967]},
|
||||
{"learn":[0.6694477439],"iteration":139,"passed_time":5.344693175,"remaining_time":71.00806646,"test":[0.6725772977]},
|
||||
{"learn":[0.6694082161],"iteration":140,"passed_time":5.377737126,"remaining_time":70.90222211,"test":[0.6725685594]},
|
||||
{"learn":[0.6693679185],"iteration":141,"passed_time":5.416087925,"remaining_time":70.8668406,"test":[0.6725553829]},
|
||||
{"learn":[0.6693341916],"iteration":142,"passed_time":5.452286939,"remaining_time":70.80347444,"test":[0.6725484347]},
|
||||
{"learn":[0.6692933159],"iteration":143,"passed_time":5.490006789,"remaining_time":70.7600875,"test":[0.6725306172]},
|
||||
{"learn":[0.6692619696],"iteration":144,"passed_time":5.521869859,"remaining_time":70.64185233,"test":[0.672543149]},
|
||||
{"learn":[0.6692229289],"iteration":145,"passed_time":5.553520721,"remaining_time":70.5221056,"test":[0.6725196247]},
|
||||
{"learn":[0.6691840164],"iteration":146,"passed_time":5.582178524,"remaining_time":70.3658286,"test":[0.6725226452]},
|
||||
{"learn":[0.6691581406],"iteration":147,"passed_time":5.611368671,"remaining_time":70.21793769,"test":[0.6725056913]},
|
||||
{"learn":[0.6691177196],"iteration":148,"passed_time":5.636941079,"remaining_time":70.02669757,"test":[0.6724771476]},
|
||||
{"learn":[0.6690851126],"iteration":149,"passed_time":5.673704689,"remaining_time":69.97569117,"test":[0.6724439435]},
|
||||
{"learn":[0.6690518144],"iteration":150,"passed_time":5.706346207,"remaining_time":69.87439826,"test":[0.672442532]},
|
||||
{"learn":[0.6690149711],"iteration":151,"passed_time":5.738210991,"remaining_time":69.76456521,"test":[0.6724303064]},
|
||||
{"learn":[0.668993877],"iteration":152,"passed_time":5.765951318,"remaining_time":69.60596133,"test":[0.6724235788]},
|
||||
{"learn":[0.6689596579],"iteration":153,"passed_time":5.795573467,"remaining_time":69.47161442,"test":[0.6724294499]},
|
||||
{"learn":[0.6689372651],"iteration":154,"passed_time":5.81744896,"remaining_time":69.24640858,"test":[0.6724285935]},
|
||||
{"learn":[0.6689003045],"iteration":155,"passed_time":5.853529431,"remaining_time":69.19171968,"test":[0.6724172017]},
|
||||
{"learn":[0.6688680182],"iteration":156,"passed_time":5.888380392,"remaining_time":69.12283479,"test":[0.6724130745]},
|
||||
{"learn":[0.6688348164],"iteration":157,"passed_time":5.924601775,"remaining_time":69.07035741,"test":[0.6723860878]},
|
||||
{"learn":[0.6687947046],"iteration":158,"passed_time":5.964531924,"remaining_time":69.06102687,"test":[0.6723707604]},
|
||||
{"learn":[0.6687605251],"iteration":159,"passed_time":5.996805452,"remaining_time":68.9632627,"test":[0.6723566111]},
|
||||
{"learn":[0.668726253],"iteration":160,"passed_time":6.022341459,"remaining_time":68.78935368,"test":[0.6723469906]},
|
||||
{"learn":[0.6686862718],"iteration":161,"passed_time":6.05082584,"remaining_time":68.65072774,"test":[0.6723287161]},
|
||||
{"learn":[0.668663478],"iteration":162,"passed_time":6.079027554,"remaining_time":68.51026759,"test":[0.6723155898]},
|
||||
{"learn":[0.6686399521],"iteration":163,"passed_time":6.108511297,"remaining_time":68.38552891,"test":[0.6722970834]},
|
||||
{"learn":[0.6686058279],"iteration":164,"passed_time":6.140719309,"remaining_time":68.29224202,"test":[0.6722872244]},
|
||||
{"learn":[0.6685761282],"iteration":165,"passed_time":6.169540017,"remaining_time":68.16226742,"test":[0.6722800481]},
|
||||
{"learn":[0.6685469327],"iteration":166,"passed_time":6.2020892,"remaining_time":68.07442817,"test":[0.6722550973]},
|
||||
{"learn":[0.6685157003],"iteration":167,"passed_time":6.231576547,"remaining_time":67.95385854,"test":[0.6722394313]},
|
||||
{"learn":[0.6684805143],"iteration":168,"passed_time":6.263261652,"remaining_time":67.85817802,"test":[0.6722204135]},
|
||||
{"learn":[0.6684485765],"iteration":169,"passed_time":6.295102833,"remaining_time":67.7649305,"test":[0.6721982148]},
|
||||
{"learn":[0.6684144429],"iteration":170,"passed_time":6.325415964,"remaining_time":67.65605729,"test":[0.6721971176]},
|
||||
{"learn":[0.6683849752],"iteration":171,"passed_time":6.35697084,"remaining_time":67.56129474,"test":[0.6721880705]},
|
||||
{"learn":[0.6683568537],"iteration":172,"passed_time":6.395913563,"remaining_time":67.5452837,"test":[0.672179176]},
|
||||
{"learn":[0.6683266628],"iteration":173,"passed_time":6.437330522,"remaining_time":67.55497433,"test":[0.6721769709]},
|
||||
{"learn":[0.6682937842],"iteration":174,"passed_time":6.472195712,"remaining_time":67.49575528,"test":[0.6721693215]},
|
||||
{"learn":[0.6682657097],"iteration":175,"passed_time":6.503044842,"remaining_time":67.395192,"test":[0.6721581386]},
|
||||
{"learn":[0.6682301443],"iteration":176,"passed_time":6.533528251,"remaining_time":67.29164972,"test":[0.6721638661]},
|
||||
{"learn":[0.6681995916],"iteration":177,"passed_time":6.562589882,"remaining_time":67.17437509,"test":[0.6721598475]},
|
||||
{"learn":[0.6681658267],"iteration":178,"passed_time":6.590816982,"remaining_time":67.04959623,"test":[0.6721433342]},
|
||||
{"learn":[0.6681422687],"iteration":179,"passed_time":6.624646227,"remaining_time":66.98253407,"test":[0.6721335599]},
|
||||
{"learn":[0.6681216601],"iteration":180,"passed_time":6.655147334,"remaining_time":66.88239227,"test":[0.6721300594]},
|
||||
{"learn":[0.6680899019],"iteration":181,"passed_time":6.687788902,"remaining_time":66.80439684,"test":[0.6721153533]},
|
||||
{"learn":[0.6680676394],"iteration":182,"passed_time":6.718057043,"remaining_time":66.7033314,"test":[0.6721076397]},
|
||||
{"learn":[0.6680413672],"iteration":183,"passed_time":6.751300957,"remaining_time":66.6324051,"test":[0.6721009911]},
|
||||
{"learn":[0.6680088406],"iteration":184,"passed_time":6.784288393,"remaining_time":66.55936991,"test":[0.6720999252]},
|
||||
{"learn":[0.6679873982],"iteration":185,"passed_time":6.810905309,"remaining_time":66.42463565,"test":[0.6720953028]},
|
||||
{"learn":[0.6679663544],"iteration":186,"passed_time":6.832974292,"remaining_time":66.24696466,"test":[0.6720942505]},
|
||||
{"learn":[0.6679417375],"iteration":187,"passed_time":6.867184511,"remaining_time":66.18796986,"test":[0.6720856237]},
|
||||
{"learn":[0.6679100197],"iteration":188,"passed_time":6.918652024,"remaining_time":66.29459691,"test":[0.6720876136]},
|
||||
{"learn":[0.667881208],"iteration":189,"passed_time":6.96948149,"remaining_time":66.39348156,"test":[0.6720880182]},
|
||||
{"learn":[0.6678475427],"iteration":190,"passed_time":7.018176318,"remaining_time":66.47058094,"test":[0.6720743856]},
|
||||
{"learn":[0.6678310341],"iteration":191,"passed_time":7.074099623,"remaining_time":66.61443812,"test":[0.6720598415]},
|
||||
{"learn":[0.6678060257],"iteration":192,"passed_time":7.117099742,"remaining_time":66.63522919,"test":[0.6720563492]},
|
||||
{"learn":[0.6677789336],"iteration":193,"passed_time":7.191058554,"remaining_time":66.94356571,"test":[0.6720389527]},
|
||||
{"learn":[0.6677478773],"iteration":194,"passed_time":7.2421897,"remaining_time":67.03667902,"test":[0.6720317324]},
|
||||
{"learn":[0.6677212408],"iteration":195,"passed_time":7.282401129,"remaining_time":67.02781447,"test":[0.672000736]},
|
||||
{"learn":[0.667704316],"iteration":196,"passed_time":7.317019235,"remaining_time":66.96744,"test":[0.6719895017]},
|
||||
{"learn":[0.6676819639],"iteration":197,"passed_time":7.351194179,"remaining_time":66.90329248,"test":[0.6719725302]},
|
||||
{"learn":[0.6676554448],"iteration":198,"passed_time":7.389840926,"remaining_time":66.87991712,"test":[0.6719770493]},
|
||||
{"learn":[0.6676318346],"iteration":199,"passed_time":7.432994652,"remaining_time":66.89695187,"test":[0.6719667172]},
|
||||
{"learn":[0.6676074705],"iteration":200,"passed_time":7.471295231,"remaining_time":66.86995085,"test":[0.6719511616]},
|
||||
{"learn":[0.6675849784],"iteration":201,"passed_time":7.506377837,"remaining_time":66.8141948,"test":[0.6719427289]},
|
||||
{"learn":[0.6675631744],"iteration":202,"passed_time":7.540821494,"remaining_time":66.75298633,"test":[0.6719299116]},
|
||||
{"learn":[0.6675397619],"iteration":203,"passed_time":7.56808212,"remaining_time":66.62880141,"test":[0.6719106583]},
|
||||
{"learn":[0.6675169086],"iteration":204,"passed_time":7.605676901,"remaining_time":66.59604896,"test":[0.6718967065]},
|
||||
{"learn":[0.6674864762],"iteration":205,"passed_time":7.638300222,"remaining_time":66.51995436,"test":[0.671890967]},
|
||||
{"learn":[0.6674670714],"iteration":206,"passed_time":7.665554951,"remaining_time":66.39777791,"test":[0.6718896293]},
|
||||
{"learn":[0.6674375599],"iteration":207,"passed_time":7.700277678,"remaining_time":66.34085384,"test":[0.6718883534]},
|
||||
{"learn":[0.6674148457],"iteration":208,"passed_time":7.734145802,"remaining_time":66.27681881,"test":[0.6718827289]},
|
||||
{"learn":[0.6673974446],"iteration":209,"passed_time":7.766232144,"remaining_time":66.19788351,"test":[0.6718763224]},
|
||||
{"learn":[0.6673812139],"iteration":210,"passed_time":7.796801222,"remaining_time":66.1065279,"test":[0.67187262]},
|
||||
{"learn":[0.6673515687],"iteration":211,"passed_time":7.831891449,"remaining_time":66.05387693,"test":[0.6718590402]},
|
||||
{"learn":[0.6673197956],"iteration":212,"passed_time":7.871259964,"remaining_time":66.0372843,"test":[0.6718455115]},
|
||||
{"learn":[0.6672900754],"iteration":213,"passed_time":7.910110502,"remaining_time":66.01615587,"test":[0.6718253747]},
|
||||
{"learn":[0.6672550009],"iteration":214,"passed_time":7.951342226,"remaining_time":66.01463197,"test":[0.671794877]},
|
||||
{"learn":[0.6672271563],"iteration":215,"passed_time":7.989001461,"remaining_time":65.98323429,"test":[0.6717873786]},
|
||||
{"learn":[0.667204521],"iteration":216,"passed_time":8.025973631,"remaining_time":65.94613357,"test":[0.6717765089]},
|
||||
{"learn":[0.667181968],"iteration":217,"passed_time":8.058434478,"remaining_time":65.87215707,"test":[0.6717616726]},
|
||||
{"learn":[0.6671640023],"iteration":218,"passed_time":8.087145957,"remaining_time":65.76806826,"test":[0.6717499215]},
|
||||
{"learn":[0.66714351],"iteration":219,"passed_time":8.112590578,"remaining_time":65.63823286,"test":[0.6717326052]},
|
||||
{"learn":[0.6671167156],"iteration":220,"passed_time":8.148644349,"remaining_time":65.59474342,"test":[0.6717161937]},
|
||||
{"learn":[0.6670915937],"iteration":221,"passed_time":8.197662625,"remaining_time":65.65515382,"test":[0.6717056951]},
|
||||
{"learn":[0.6670595279],"iteration":222,"passed_time":8.239228431,"remaining_time":65.65519696,"test":[0.6717021438]},
|
||||
{"learn":[0.667033994],"iteration":223,"passed_time":8.268371203,"remaining_time":65.55637168,"test":[0.6716868488]},
|
||||
{"learn":[0.6670008246],"iteration":224,"passed_time":8.298555216,"remaining_time":65.46638004,"test":[0.6716751909]},
|
||||
{"learn":[0.6669858319],"iteration":225,"passed_time":8.327401394,"remaining_time":65.36641625,"test":[0.671670116]},
|
||||
{"learn":[0.6669553964],"iteration":226,"passed_time":8.357648377,"remaining_time":65.27802014,"test":[0.6716558757]},
|
||||
{"learn":[0.6669274683],"iteration":227,"passed_time":8.384989701,"remaining_time":65.16755154,"test":[0.6716559962]},
|
||||
{"learn":[0.666896348],"iteration":228,"passed_time":8.418297538,"remaining_time":65.1039517,"test":[0.6716487875]},
|
||||
{"learn":[0.6668698686],"iteration":229,"passed_time":8.453919972,"remaining_time":65.05842761,"test":[0.6716427451]},
|
||||
{"learn":[0.6668513411],"iteration":230,"passed_time":8.49049033,"remaining_time":65.02024846,"test":[0.6716323255]},
|
||||
{"learn":[0.6668309985],"iteration":231,"passed_time":8.523986676,"remaining_time":64.95865708,"test":[0.6716303547]},
|
||||
{"learn":[0.6668058585],"iteration":232,"passed_time":8.550998228,"remaining_time":64.84812819,"test":[0.6716309509]},
|
||||
{"learn":[0.6667845908],"iteration":233,"passed_time":8.575382398,"remaining_time":64.71848425,"test":[0.6716215401]},
|
||||
{"learn":[0.6667582863],"iteration":234,"passed_time":8.607602961,"remaining_time":64.64859245,"test":[0.6716162103]},
|
||||
{"learn":[0.6667332943],"iteration":235,"passed_time":8.6353786,"remaining_time":64.54579597,"test":[0.6716135097]},
|
||||
{"learn":[0.6667070085],"iteration":236,"passed_time":8.66085309,"remaining_time":64.42651476,"test":[0.6716156696]},
|
||||
{"learn":[0.6666907315],"iteration":237,"passed_time":8.691362456,"remaining_time":64.34529684,"test":[0.6716020054]},
|
||||
{"learn":[0.6666633028],"iteration":238,"passed_time":8.719983169,"remaining_time":64.25058728,"test":[0.6715921704]},
|
||||
{"learn":[0.6666406707],"iteration":239,"passed_time":8.746012652,"remaining_time":64.13742611,"test":[0.6715804466]},
|
||||
{"learn":[0.6666134624],"iteration":240,"passed_time":8.773898765,"remaining_time":64.03853912,"test":[0.6715882966]},
|
||||
{"learn":[0.6665850522],"iteration":241,"passed_time":8.803292064,"remaining_time":63.9511878,"test":[0.6715753942]},
|
||||
{"learn":[0.6665631193],"iteration":242,"passed_time":8.833976809,"remaining_time":63.87365125,"test":[0.6715752261]},
|
||||
{"learn":[0.6665412643],"iteration":243,"passed_time":8.862338006,"remaining_time":63.7797768,"test":[0.6715625509]},
|
||||
{"learn":[0.6665168385],"iteration":244,"passed_time":8.892424073,"remaining_time":63.69879285,"test":[0.6715628214]},
|
||||
{"learn":[0.6664904845],"iteration":245,"passed_time":8.932383667,"remaining_time":63.68862175,"test":[0.6715601629]},
|
||||
{"learn":[0.6664678274],"iteration":246,"passed_time":8.962911123,"remaining_time":63.61126801,"test":[0.6715576255]},
|
||||
{"learn":[0.6664539777],"iteration":247,"passed_time":8.991624872,"remaining_time":63.52147894,"test":[0.6715550274]},
|
||||
{"learn":[0.6664334121],"iteration":248,"passed_time":9.021847081,"remaining_time":63.44278811,"test":[0.6715448645]},
|
||||
{"learn":[0.6664121724],"iteration":249,"passed_time":9.05121341,"remaining_time":63.35849387,"test":[0.6715308166]},
|
||||
{"learn":[0.666392034],"iteration":250,"passed_time":9.085113431,"remaining_time":63.30622865,"test":[0.671519334]},
|
||||
{"learn":[0.666366899],"iteration":251,"passed_time":9.110250512,"remaining_time":63.19332498,"test":[0.6715184071]},
|
||||
{"learn":[0.6663414098],"iteration":252,"passed_time":9.137253573,"remaining_time":63.09399997,"test":[0.6715163019]},
|
||||
{"learn":[0.6663157816],"iteration":253,"passed_time":9.174559864,"remaining_time":63.06606899,"test":[0.6715096094]},
|
||||
{"learn":[0.6662989799],"iteration":254,"passed_time":9.196898204,"remaining_time":62.93563673,"test":[0.6714992963]},
|
||||
{"learn":[0.6662696102],"iteration":255,"passed_time":9.238149902,"remaining_time":62.9348962,"test":[0.6714917256]},
|
||||
{"learn":[0.6662479711],"iteration":256,"passed_time":9.267818291,"remaining_time":62.85528125,"test":[0.671477406]},
|
||||
{"learn":[0.6662231874],"iteration":257,"passed_time":9.297538986,"remaining_time":62.77640665,"test":[0.6714741542]},
|
||||
{"learn":[0.6661947927],"iteration":258,"passed_time":9.324772701,"remaining_time":62.68119411,"test":[0.6714576155]},
|
||||
{"learn":[0.6661669951],"iteration":259,"passed_time":9.357824574,"remaining_time":62.62544138,"test":[0.6714473645]},
|
||||
{"learn":[0.6661426137],"iteration":260,"passed_time":9.388345461,"remaining_time":62.55299907,"test":[0.6714427232]},
|
||||
{"learn":[0.6661216749],"iteration":261,"passed_time":9.427290804,"remaining_time":62.53676114,"test":[0.6714364275]},
|
||||
{"learn":[0.6660983123],"iteration":262,"passed_time":9.461913185,"remaining_time":62.49179925,"test":[0.6714339587]},
|
||||
{"learn":[0.6660803402],"iteration":263,"passed_time":9.496090562,"remaining_time":62.44398945,"test":[0.6714336287]},
|
||||
{"learn":[0.6660617842],"iteration":264,"passed_time":9.524189317,"remaining_time":62.35648477,"test":[0.6714283568]},
|
||||
{"learn":[0.6660443878],"iteration":265,"passed_time":9.55372419,"remaining_time":62.27878852,"test":[0.6714271895]},
|
||||
{"learn":[0.6660176079],"iteration":266,"passed_time":9.590356068,"remaining_time":62.2475171,"test":[0.671413471]},
|
||||
{"learn":[0.6659967546],"iteration":267,"passed_time":9.620235131,"remaining_time":62.17256436,"test":[0.6714072396]},
|
||||
{"learn":[0.6659751467],"iteration":268,"passed_time":9.645948482,"remaining_time":62.0711406,"test":[0.6714002677]},
|
||||
{"learn":[0.6659539329],"iteration":269,"passed_time":9.682675077,"remaining_time":62.04084401,"test":[0.6714001163]},
|
||||
{"learn":[0.6659263951],"iteration":270,"passed_time":9.711914203,"remaining_time":61.96272936,"test":[0.6713933952]},
|
||||
{"learn":[0.6659038921],"iteration":271,"passed_time":9.739142426,"remaining_time":61.87219894,"test":[0.6713926761]},
|
||||
{"learn":[0.6658767418],"iteration":272,"passed_time":9.768751964,"remaining_time":61.79719649,"test":[0.6713836619]},
|
||||
{"learn":[0.6658510507],"iteration":273,"passed_time":9.804576737,"remaining_time":61.76167682,"test":[0.6713772112]},
|
||||
{"learn":[0.6658210119],"iteration":274,"passed_time":9.848653906,"remaining_time":61.77791996,"test":[0.6713603715]},
|
||||
{"learn":[0.6657963011],"iteration":275,"passed_time":9.88663261,"remaining_time":61.75563268,"test":[0.6713560246]},
|
||||
{"learn":[0.6657748552],"iteration":276,"passed_time":9.925808942,"remaining_time":61.74068161,"test":[0.6713837913]},
|
||||
{"learn":[0.6657490013],"iteration":277,"passed_time":9.965409489,"remaining_time":61.72818396,"test":[0.6713684274]},
|
||||
{"learn":[0.665732402],"iteration":278,"passed_time":9.99537326,"remaining_time":61.65604796,"test":[0.6713619356]},
|
||||
{"learn":[0.6657118786],"iteration":279,"passed_time":10.02216777,"remaining_time":61.5647449,"test":[0.6713584836]},
|
||||
{"learn":[0.665684467],"iteration":280,"passed_time":10.05593393,"remaining_time":61.51654955,"test":[0.6713673572]},
|
||||
{"learn":[0.6656584634],"iteration":281,"passed_time":10.08025153,"remaining_time":61.41089406,"test":[0.6713625568]},
|
||||
{"learn":[0.6656309991],"iteration":282,"passed_time":10.11102202,"remaining_time":61.34496401,"test":[0.6713542652]},
|
||||
{"learn":[0.6656073482],"iteration":283,"passed_time":10.14714598,"remaining_time":61.31162855,"test":[0.6713512017]},
|
||||
{"learn":[0.6655890957],"iteration":284,"passed_time":10.17528061,"remaining_time":61.23019734,"test":[0.671342038]},
|
||||
{"learn":[0.6655665563],"iteration":285,"passed_time":10.2021403,"remaining_time":61.14149818,"test":[0.6713279798]},
|
||||
{"learn":[0.6655452454],"iteration":286,"passed_time":10.23423432,"remaining_time":61.08447174,"test":[0.6713123285]},
|
||||
{"learn":[0.6655255286],"iteration":287,"passed_time":10.26481698,"remaining_time":61.0186343,"test":[0.6713035326]},
|
||||
{"learn":[0.6655053548],"iteration":288,"passed_time":10.29945844,"remaining_time":60.97707056,"test":[0.6713022203]},
|
||||
{"learn":[0.6654893396],"iteration":289,"passed_time":10.32366496,"remaining_time":60.87402441,"test":[0.671296041]},
|
||||
{"learn":[0.6654648912],"iteration":290,"passed_time":10.35344703,"remaining_time":60.80426453,"test":[0.6712829551]},
|
||||
{"learn":[0.6654442759],"iteration":291,"passed_time":10.3949915,"remaining_time":60.8035804,"test":[0.6712769751]},
|
||||
{"learn":[0.6654173127],"iteration":292,"passed_time":10.43148765,"remaining_time":60.77320621,"test":[0.6712702915]},
|
||||
{"learn":[0.6653914518],"iteration":293,"passed_time":10.47162738,"remaining_time":60.76393303,"test":[0.6712379343]},
|
||||
{"learn":[0.6653648946],"iteration":294,"passed_time":10.50360107,"remaining_time":60.70725362,"test":[0.6712192006]},
|
||||
{"learn":[0.665344141],"iteration":295,"passed_time":10.53460819,"remaining_time":60.64517686,"test":[0.6712074061]},
|
||||
{"learn":[0.6653140817],"iteration":296,"passed_time":10.57659448,"remaining_time":60.64626395,"test":[0.6711953324]},
|
||||
{"learn":[0.665295365],"iteration":297,"passed_time":10.61260262,"remaining_time":60.61291829,"test":[0.6711891001]},
|
||||
{"learn":[0.6652787488],"iteration":298,"passed_time":10.63910358,"remaining_time":60.52546889,"test":[0.6711870526]},
|
||||
{"learn":[0.6652502991],"iteration":299,"passed_time":10.6681867,"remaining_time":60.45305797,"test":[0.6711812809]},
|
||||
{"learn":[0.665231168],"iteration":300,"passed_time":10.70260503,"remaining_time":60.41104967,"test":[0.6711768946]},
|
||||
{"learn":[0.6652136682],"iteration":301,"passed_time":10.72952096,"remaining_time":60.32690925,"test":[0.6711845012]},
|
||||
{"learn":[0.6651903001],"iteration":302,"passed_time":10.76489952,"remaining_time":60.29054288,"test":[0.6711869636]},
|
||||
{"learn":[0.6651697153],"iteration":303,"passed_time":10.80197155,"remaining_time":60.26363073,"test":[0.671186884]},
|
||||
{"learn":[0.6651525958],"iteration":304,"passed_time":10.82922271,"remaining_time":60.18207375,"test":[0.6711890401]},
|
||||
{"learn":[0.6651322685],"iteration":305,"passed_time":10.8578399,"remaining_time":60.10843394,"test":[0.6711868603]},
|
||||
{"learn":[0.6651113828],"iteration":306,"passed_time":10.89228879,"remaining_time":60.06724727,"test":[0.6711900892]},
|
||||
{"learn":[0.6650886807],"iteration":307,"passed_time":10.93056436,"remaining_time":60.04712628,"test":[0.6711884242]},
|
||||
{"learn":[0.6650622251],"iteration":308,"passed_time":10.97231236,"remaining_time":60.04589061,"test":[0.6711837119]},
|
||||
{"learn":[0.6650429987],"iteration":309,"passed_time":11.00296848,"remaining_time":59.98392494,"test":[0.6711766645]},
|
||||
{"learn":[0.665015513],"iteration":310,"passed_time":11.03002276,"remaining_time":59.90259947,"test":[0.671172959]},
|
||||
{"learn":[0.6650019022],"iteration":311,"passed_time":11.05828865,"remaining_time":59.82817707,"test":[0.6711740433]},
|
||||
{"learn":[0.664979951],"iteration":312,"passed_time":11.09287745,"remaining_time":59.78812863,"test":[0.6711715069]},
|
||||
{"learn":[0.6649549638],"iteration":313,"passed_time":11.1177757,"remaining_time":59.69608229,"test":[0.6711589843]},
|
||||
{"learn":[0.6649340455],"iteration":314,"passed_time":11.14959087,"remaining_time":59.64146228,"test":[0.6711446402]},
|
||||
{"learn":[0.6649162445],"iteration":315,"passed_time":11.18718772,"remaining_time":59.61779784,"test":[0.6711415366]},
|
||||
{"learn":[0.6649048119],"iteration":316,"passed_time":11.21179073,"remaining_time":59.52505932,"test":[0.6711359351]},
|
||||
{"learn":[0.6648796463],"iteration":317,"passed_time":11.24311165,"remaining_time":59.46828238,"test":[0.671143361]},
|
||||
{"learn":[0.6648605481],"iteration":318,"passed_time":11.27486028,"remaining_time":59.41391889,"test":[0.6711353638]},
|
||||
{"learn":[0.6648429084],"iteration":319,"passed_time":11.30400807,"remaining_time":59.34604237,"test":[0.6711444387]},
|
||||
{"learn":[0.6648238121],"iteration":320,"passed_time":11.33488419,"remaining_time":59.28744721,"test":[0.6711487352]},
|
||||
{"learn":[0.6647969527],"iteration":321,"passed_time":11.36208838,"remaining_time":59.20988915,"test":[0.67114436]},
|
||||
{"learn":[0.6647854723],"iteration":322,"passed_time":11.39429642,"remaining_time":59.15862259,"test":[0.6711444722]},
|
||||
{"learn":[0.6647589304],"iteration":323,"passed_time":11.4363998,"remaining_time":59.15866068,"test":[0.6711325635]},
|
||||
{"learn":[0.6647429024],"iteration":324,"passed_time":11.47751019,"remaining_time":59.15332173,"test":[0.6711269403]},
|
||||
{"learn":[0.6647237508],"iteration":325,"passed_time":11.5136833,"remaining_time":59.12241054,"test":[0.6711154078]},
|
||||
{"learn":[0.6647059396],"iteration":326,"passed_time":11.54795566,"remaining_time":59.08174257,"test":[0.6711203043]},
|
||||
{"learn":[0.664686288],"iteration":327,"passed_time":11.57245915,"remaining_time":58.99131613,"test":[0.6711241333]},
|
||||
{"learn":[0.6646532527],"iteration":328,"passed_time":11.60790333,"remaining_time":58.95685857,"test":[0.6711213497]},
|
||||
{"learn":[0.6646306438],"iteration":329,"passed_time":11.63787346,"remaining_time":58.89469298,"test":[0.6711231641]},
|
||||
{"learn":[0.6646098516],"iteration":330,"passed_time":11.66805718,"remaining_time":58.83379887,"test":[0.6711049215]},
|
||||
{"learn":[0.6645858284],"iteration":331,"passed_time":11.70070223,"remaining_time":58.78545579,"test":[0.6711031963]},
|
||||
{"learn":[0.6645707188],"iteration":332,"passed_time":11.724753,"remaining_time":58.69418391,"test":[0.6710996314]},
|
||||
{"learn":[0.6645485788],"iteration":333,"passed_time":11.75795297,"remaining_time":58.64895104,"test":[0.6710867309]},
|
||||
{"learn":[0.6645305696],"iteration":334,"passed_time":11.78053066,"remaining_time":58.55099567,"test":[0.6710914578]},
|
||||
{"learn":[0.6645108881],"iteration":335,"passed_time":11.81570271,"remaining_time":58.51586106,"test":[0.6710929585]},
|
||||
{"learn":[0.6644923286],"iteration":336,"passed_time":11.8448851,"remaining_time":58.45116888,"test":[0.6710984779]},
|
||||
{"learn":[0.6644805222],"iteration":337,"passed_time":11.86964023,"remaining_time":58.36491734,"test":[0.6710923199]},
|
||||
{"learn":[0.6644572776],"iteration":338,"passed_time":11.90591446,"remaining_time":58.33546879,"test":[0.6710893917]},
|
||||
{"learn":[0.6644320741],"iteration":339,"passed_time":11.94145444,"remaining_time":58.30239521,"test":[0.6710923306]},
|
||||
{"learn":[0.6644115048],"iteration":340,"passed_time":11.98658051,"remaining_time":58.31594449,"test":[0.6710927901]},
|
||||
{"learn":[0.6643949013],"iteration":341,"passed_time":12.02038848,"remaining_time":58.27428098,"test":[0.6711092802]},
|
||||
{"learn":[0.6643619789],"iteration":342,"passed_time":12.06653941,"remaining_time":58.29229096,"test":[0.6711012995]},
|
||||
{"learn":[0.6643389502],"iteration":343,"passed_time":12.12283646,"remaining_time":58.35877087,"test":[0.6711015305]},
|
||||
{"learn":[0.6643088915],"iteration":344,"passed_time":12.17733618,"remaining_time":58.41591705,"test":[0.6710975574]},
|
||||
{"learn":[0.664286972],"iteration":345,"passed_time":12.22133732,"remaining_time":58.42223099,"test":[0.6710899474]},
|
||||
{"learn":[0.664274149],"iteration":346,"passed_time":12.2642467,"remaining_time":58.42305415,"test":[0.671085152]},
|
||||
{"learn":[0.6642536926],"iteration":347,"passed_time":12.30091895,"remaining_time":58.39401755,"test":[0.6710814533]},
|
||||
{"learn":[0.6642357634],"iteration":348,"passed_time":12.32484094,"remaining_time":58.30462002,"test":[0.6710701892]},
|
||||
{"learn":[0.664207914],"iteration":349,"passed_time":12.35469303,"remaining_time":58.24355287,"test":[0.67105503]},
|
||||
{"learn":[0.6641853097],"iteration":350,"passed_time":12.40148755,"remaining_time":58.26225919,"test":[0.6710527861]},
|
||||
{"learn":[0.6641654917],"iteration":351,"passed_time":12.43803877,"remaining_time":58.23263605,"test":[0.6710508715]},
|
||||
{"learn":[0.664143804],"iteration":352,"passed_time":12.47995438,"remaining_time":58.22800245,"test":[0.6710560803]},
|
||||
{"learn":[0.6641290647],"iteration":353,"passed_time":12.51241326,"remaining_time":58.17918707,"test":[0.6710465693]},
|
||||
{"learn":[0.6641117244],"iteration":354,"passed_time":12.5417829,"remaining_time":58.11614893,"test":[0.6710440741]},
|
||||
{"learn":[0.6640880219],"iteration":355,"passed_time":12.5692936,"remaining_time":58.0447154,"test":[0.6710496913]},
|
||||
{"learn":[0.6640669415],"iteration":356,"passed_time":12.5976392,"remaining_time":57.97737034,"test":[0.6710404659]},
|
||||
{"learn":[0.6640462999],"iteration":357,"passed_time":12.62815847,"remaining_time":57.92021287,"test":[0.6710293986]},
|
||||
{"learn":[0.664030296],"iteration":358,"passed_time":12.65342509,"remaining_time":57.8391938,"test":[0.6710353817]},
|
||||
{"learn":[0.6640028542],"iteration":359,"passed_time":12.68233453,"remaining_time":57.77507954,"test":[0.6710271815]},
|
||||
{"learn":[0.6639813347],"iteration":360,"passed_time":12.72037964,"remaining_time":57.75263774,"test":[0.6710288077]},
|
||||
{"learn":[0.6639597941],"iteration":361,"passed_time":12.744473,"remaining_time":57.66698004,"test":[0.6710169894]},
|
||||
{"learn":[0.6639429832],"iteration":362,"passed_time":12.77086568,"remaining_time":57.59203063,"test":[0.6710119848]},
|
||||
{"learn":[0.6639222708],"iteration":363,"passed_time":12.81194554,"remaining_time":57.58335961,"test":[0.6710114775]},
|
||||
{"learn":[0.6639065546],"iteration":364,"passed_time":12.84133287,"remaining_time":57.52213492,"test":[0.6710013614]},
|
||||
{"learn":[0.6638823236],"iteration":365,"passed_time":12.87057337,"remaining_time":57.46042866,"test":[0.6709985657]},
|
||||
{"learn":[0.6638648195],"iteration":366,"passed_time":12.8971183,"remaining_time":57.38690512,"test":[0.6709948954]},
|
||||
{"learn":[0.6638436235],"iteration":367,"passed_time":12.93825161,"remaining_time":57.37833324,"test":[0.6709970591]},
|
||||
{"learn":[0.6638208732],"iteration":368,"passed_time":12.97444296,"remaining_time":57.3477411,"test":[0.6709739289]},
|
||||
{"learn":[0.6637956357],"iteration":369,"passed_time":13.00974924,"remaining_time":57.31321963,"test":[0.6709754911]},
|
||||
{"learn":[0.6637718453],"iteration":370,"passed_time":13.03832239,"remaining_time":57.24912984,"test":[0.6709717066]},
|
||||
{"learn":[0.663756918],"iteration":371,"passed_time":13.07843077,"remaining_time":57.23571316,"test":[0.67096845]},
|
||||
{"learn":[0.6637353525],"iteration":372,"passed_time":13.11729124,"remaining_time":57.21671005,"test":[0.6709739445]},
|
||||
{"learn":[0.6637143112],"iteration":373,"passed_time":13.14745329,"remaining_time":57.15978354,"test":[0.6709728881]},
|
||||
{"learn":[0.6636956547],"iteration":374,"passed_time":13.18118022,"remaining_time":57.11844761,"test":[0.6709694284]},
|
||||
{"learn":[0.663680995],"iteration":375,"passed_time":13.20539229,"remaining_time":57.03605604,"test":[0.6709604166]},
|
||||
{"learn":[0.66366728],"iteration":376,"passed_time":13.23563977,"remaining_time":56.97995583,"test":[0.6709605025]},
|
||||
{"learn":[0.6636487567],"iteration":377,"passed_time":13.27428255,"remaining_time":56.96001665,"test":[0.6709603727]},
|
||||
{"learn":[0.6636266904],"iteration":378,"passed_time":13.30625754,"remaining_time":56.91146033,"test":[0.670944339]},
|
||||
{"learn":[0.6636116064],"iteration":379,"passed_time":13.33327871,"remaining_time":56.84187241,"test":[0.6709447187]},
|
||||
{"learn":[0.6635902746],"iteration":380,"passed_time":13.36632239,"remaining_time":56.79809961,"test":[0.6709538679]},
|
||||
{"learn":[0.6635654896],"iteration":381,"passed_time":13.39639051,"remaining_time":56.74177969,"test":[0.6709640912]},
|
||||
{"learn":[0.6635393029],"iteration":382,"passed_time":13.42189438,"remaining_time":56.66632694,"test":[0.6709534847]},
|
||||
{"learn":[0.6635171734],"iteration":383,"passed_time":13.46730432,"remaining_time":56.6749057,"test":[0.6709471555]},
|
||||
{"learn":[0.663500789],"iteration":384,"passed_time":13.50832777,"remaining_time":56.66480351,"test":[0.6709506783]},
|
||||
{"learn":[0.663477743],"iteration":385,"passed_time":13.54029627,"remaining_time":56.61667921,"test":[0.6709546729]},
|
||||
{"learn":[0.6634584806],"iteration":386,"passed_time":13.56996301,"remaining_time":56.5590448,"test":[0.670930774]},
|
||||
{"learn":[0.6634337499],"iteration":387,"passed_time":13.59835745,"remaining_time":56.4962686,"test":[0.6709287322]},
|
||||
{"learn":[0.6634135584],"iteration":388,"passed_time":13.6279617,"remaining_time":56.43867943,"test":[0.6709198643]},
|
||||
{"learn":[0.6633868455],"iteration":389,"passed_time":13.65633448,"remaining_time":56.37615005,"test":[0.6709220389]},
|
||||
{"learn":[0.6633755323],"iteration":390,"passed_time":13.68565529,"remaining_time":56.31769658,"test":[0.6709230923]},
|
||||
{"learn":[0.663356103],"iteration":391,"passed_time":13.71789303,"remaining_time":56.27135714,"test":[0.670930414]},
|
||||
{"learn":[0.6633337631],"iteration":392,"passed_time":13.75060752,"remaining_time":56.2270389,"test":[0.6709354296]},
|
||||
{"learn":[0.663319422],"iteration":393,"passed_time":13.77167974,"remaining_time":56.13532403,"test":[0.6709351544]},
|
||||
{"learn":[0.6632911566],"iteration":394,"passed_time":13.80416242,"remaining_time":56.09033084,"test":[0.6709414935]},
|
||||
{"learn":[0.6632687875],"iteration":395,"passed_time":13.82525369,"remaining_time":55.9992599,"test":[0.6709445943]},
|
||||
{"learn":[0.6632431997],"iteration":396,"passed_time":13.85836516,"remaining_time":55.95707646,"test":[0.6709475685]},
|
||||
{"learn":[0.6632189331],"iteration":397,"passed_time":13.88898168,"remaining_time":55.90489613,"test":[0.6709533591]},
|
||||
{"learn":[0.663201035],"iteration":398,"passed_time":13.91726355,"remaining_time":55.84345598,"test":[0.6709592222]},
|
||||
{"learn":[0.6631898553],"iteration":399,"passed_time":13.95316828,"remaining_time":55.81267311,"test":[0.6709508704]},
|
||||
{"learn":[0.6631712482],"iteration":400,"passed_time":13.99418497,"remaining_time":55.80224881,"test":[0.6709479912]},
|
||||
{"learn":[0.663143025],"iteration":401,"passed_time":14.0253575,"remaining_time":55.75254052,"test":[0.6709417519]},
|
||||
{"learn":[0.663121538],"iteration":402,"passed_time":14.04844239,"remaining_time":55.67087467,"test":[0.6709476082]},
|
||||
{"learn":[0.6631087792],"iteration":403,"passed_time":14.0761289,"remaining_time":55.60767753,"test":[0.6709480979]},
|
||||
{"learn":[0.6630859067],"iteration":404,"passed_time":14.10555105,"remaining_time":55.55149118,"test":[0.6709448724]},
|
||||
{"learn":[0.663066483],"iteration":405,"passed_time":14.1427661,"remaining_time":55.52603242,"test":[0.6709421934]},
|
||||
{"learn":[0.6630443652],"iteration":406,"passed_time":14.18285552,"remaining_time":55.51176619,"test":[0.6709386261]},
|
||||
{"learn":[0.6630250376],"iteration":407,"passed_time":14.21458769,"remaining_time":55.46476372,"test":[0.6709461564]},
|
||||
{"learn":[0.6630007822],"iteration":408,"passed_time":14.24035708,"remaining_time":55.39464088,"test":[0.670934384]},
|
||||
{"learn":[0.6629768728],"iteration":409,"passed_time":14.26711915,"remaining_time":55.32858403,"test":[0.6709312987]},
|
||||
{"learn":[0.6629528093],"iteration":410,"passed_time":14.29943785,"remaining_time":55.28420133,"test":[0.670931806]},
|
||||
{"learn":[0.6629260936],"iteration":411,"passed_time":14.32489173,"remaining_time":55.21341763,"test":[0.6709286111]},
|
||||
{"learn":[0.6629102182],"iteration":412,"passed_time":14.35119075,"remaining_time":55.14610101,"test":[0.6709224729]},
|
||||
{"learn":[0.6628863488],"iteration":413,"passed_time":14.37946054,"remaining_time":55.08653242,"test":[0.6709236504]},
|
||||
{"learn":[0.6628648972],"iteration":414,"passed_time":14.41005914,"remaining_time":55.03600899,"test":[0.6709245901]},
|
||||
{"learn":[0.6628454339],"iteration":415,"passed_time":14.45103793,"remaining_time":55.02510598,"test":[0.6709463437]},
|
||||
{"learn":[0.6628200274],"iteration":416,"passed_time":14.48428995,"remaining_time":54.98472661,"test":[0.6709567049]},
|
||||
{"learn":[0.6627942591],"iteration":417,"passed_time":14.5135184,"remaining_time":54.92915339,"test":[0.670945606]},
|
||||
{"learn":[0.6627744647],"iteration":418,"passed_time":14.53698524,"remaining_time":54.85196578,"test":[0.6709479298]},
|
||||
{"learn":[0.662765485],"iteration":419,"passed_time":14.56542473,"remaining_time":54.79374067,"test":[0.6709464351]},
|
||||
{"learn":[0.6627503257],"iteration":420,"passed_time":14.58728594,"remaining_time":54.71098455,"test":[0.6709414048]},
|
||||
{"learn":[0.6627323029],"iteration":421,"passed_time":14.61501375,"remaining_time":54.65045425,"test":[0.6709414427]},
|
||||
{"learn":[0.6627111509],"iteration":422,"passed_time":14.64231614,"remaining_time":54.58849302,"test":[0.6709296343]},
|
||||
{"learn":[0.6626785863],"iteration":423,"passed_time":14.66665432,"remaining_time":54.51567739,"test":[0.670924721]},
|
||||
{"learn":[0.6626576561],"iteration":424,"passed_time":14.69050441,"remaining_time":54.44128104,"test":[0.670906284]},
|
||||
{"learn":[0.6626363113],"iteration":425,"passed_time":14.71910475,"remaining_time":54.38467341,"test":[0.6708996826]},
|
||||
{"learn":[0.6626181065],"iteration":426,"passed_time":14.73941058,"remaining_time":54.2976413,"test":[0.6708987677]},
|
||||
{"learn":[0.66259794],"iteration":427,"passed_time":14.77242451,"remaining_time":54.25759657,"test":[0.670909526]},
|
||||
{"learn":[0.6625765658],"iteration":428,"passed_time":14.79088688,"remaining_time":54.1642967,"test":[0.6709033226]},
|
||||
{"learn":[0.6625526572],"iteration":429,"passed_time":14.82430966,"remaining_time":54.12596783,"test":[0.6708750209]},
|
||||
{"learn":[0.66253135],"iteration":430,"passed_time":14.84439175,"remaining_time":54.03909666,"test":[0.6708752079]},
|
||||
{"learn":[0.6625035695],"iteration":431,"passed_time":14.8764415,"remaining_time":53.99597284,"test":[0.6708776566]},
|
||||
{"learn":[0.662480212],"iteration":432,"passed_time":14.90666075,"remaining_time":53.94627573,"test":[0.6708736133]},
|
||||
{"learn":[0.6624611632],"iteration":433,"passed_time":14.93845927,"remaining_time":53.90236684,"test":[0.6708754298]},
|
||||
{"learn":[0.6624332625],"iteration":434,"passed_time":14.98024104,"remaining_time":53.89443041,"test":[0.6708751084]},
|
||||
{"learn":[0.6624120584],"iteration":435,"passed_time":15.00605075,"remaining_time":53.82904442,"test":[0.6708642042]},
|
||||
{"learn":[0.6623941719],"iteration":436,"passed_time":15.03384083,"remaining_time":53.77092268,"test":[0.6708610465]},
|
||||
{"learn":[0.6623766304],"iteration":437,"passed_time":15.05972545,"remaining_time":53.70614417,"test":[0.6708574768]},
|
||||
{"learn":[0.6623623329],"iteration":438,"passed_time":15.08505889,"remaining_time":53.63958297,"test":[0.6708557953]},
|
||||
{"learn":[0.6623442925],"iteration":439,"passed_time":15.11080547,"remaining_time":53.57467393,"test":[0.670871378]},
|
||||
{"learn":[0.6623212715],"iteration":440,"passed_time":15.13466304,"remaining_time":53.50326458,"test":[0.6708640187]},
|
||||
{"learn":[0.6623025941],"iteration":441,"passed_time":15.16037021,"remaining_time":53.43859001,"test":[0.6708700565]},
|
||||
{"learn":[0.6622749791],"iteration":442,"passed_time":15.18471062,"remaining_time":53.36928767,"test":[0.6708667534]},
|
||||
{"learn":[0.6622534499],"iteration":443,"passed_time":15.21140556,"remaining_time":53.30843931,"test":[0.6708675383]},
|
||||
{"learn":[0.6622305473],"iteration":444,"passed_time":15.23498219,"remaining_time":53.23684787,"test":[0.6708740175]},
|
||||
{"learn":[0.6622059333],"iteration":445,"passed_time":15.26647355,"remaining_time":53.19304911,"test":[0.6708774523]},
|
||||
{"learn":[0.6621871707],"iteration":446,"passed_time":15.28793136,"remaining_time":53.11444609,"test":[0.6708697231]},
|
||||
{"learn":[0.6621638454],"iteration":447,"passed_time":15.31613827,"remaining_time":53.05947899,"test":[0.6708614971]},
|
||||
{"learn":[0.6621511296],"iteration":448,"passed_time":15.33689091,"remaining_time":52.9788815,"test":[0.6708607946]},
|
||||
{"learn":[0.6621349978],"iteration":449,"passed_time":15.36674634,"remaining_time":52.92990406,"test":[0.6708740865]},
|
||||
{"learn":[0.6621120424],"iteration":450,"passed_time":15.393642,"remaining_time":52.87084582,"test":[0.6708729562]},
|
||||
{"learn":[0.6620958271],"iteration":451,"passed_time":15.42984657,"remaining_time":52.84381082,"test":[0.6708674017]},
|
||||
{"learn":[0.6620793528],"iteration":452,"passed_time":15.46956188,"remaining_time":52.82872456,"test":[0.6708693088]},
|
||||
{"learn":[0.6620572713],"iteration":453,"passed_time":15.49032259,"remaining_time":52.74898396,"test":[0.6708712037]},
|
||||
{"learn":[0.6620395025],"iteration":454,"passed_time":15.52379393,"remaining_time":52.71266289,"test":[0.6708703905]},
|
||||
{"learn":[0.6620188044],"iteration":455,"passed_time":15.55053135,"remaining_time":52.65355352,"test":[0.6708577595]},
|
||||
{"learn":[0.6620017347],"iteration":456,"passed_time":15.57735398,"remaining_time":52.59487352,"test":[0.6708493546]},
|
||||
{"learn":[0.6619811454],"iteration":457,"passed_time":15.60434803,"remaining_time":52.53690973,"test":[0.6708523777]},
|
||||
{"learn":[0.6619695569],"iteration":458,"passed_time":15.63056555,"remaining_time":52.47647387,"test":[0.6708454134]},
|
||||
{"learn":[0.661952377],"iteration":459,"passed_time":15.656355,"remaining_time":52.41475368,"test":[0.6708404483]},
|
||||
{"learn":[0.6619237442],"iteration":460,"passed_time":15.68232112,"remaining_time":52.35377918,"test":[0.6708274771]},
|
||||
{"learn":[0.6619089407],"iteration":461,"passed_time":15.71164945,"remaining_time":52.30414904,"test":[0.6708244992]},
|
||||
{"learn":[0.6618886168],"iteration":462,"passed_time":15.7361944,"remaining_time":52.23872743,"test":[0.6708344314]},
|
||||
{"learn":[0.6618831383],"iteration":463,"passed_time":15.76527735,"remaining_time":52.18850433,"test":[0.6708279081]},
|
||||
{"learn":[0.6618690774],"iteration":464,"passed_time":15.78652262,"remaining_time":52.11249942,"test":[0.6708258106]},
|
||||
{"learn":[0.661845878],"iteration":465,"passed_time":15.81756836,"remaining_time":52.06899113,"test":[0.6708049714]},
|
||||
{"learn":[0.6618290213],"iteration":466,"passed_time":15.83979966,"remaining_time":51.99660146,"test":[0.670810989]},
|
||||
{"learn":[0.6618050064],"iteration":467,"passed_time":15.87342473,"remaining_time":51.9617237,"test":[0.6708212237]},
|
||||
{"learn":[0.6617832833],"iteration":468,"passed_time":15.90381555,"remaining_time":51.9162934,"test":[0.6708221741]},
|
||||
{"learn":[0.6617652311],"iteration":469,"passed_time":15.93502938,"remaining_time":51.87360627,"test":[0.6708259658]},
|
||||
{"learn":[0.6617443144],"iteration":470,"passed_time":15.96919221,"remaining_time":51.84054117,"test":[0.6708159692]},
|
||||
{"learn":[0.6617202619],"iteration":471,"passed_time":15.99477329,"remaining_time":51.77968981,"test":[0.6708136212]},
|
||||
{"learn":[0.6617005831],"iteration":472,"passed_time":16.02279091,"remaining_time":51.72685354,"test":[0.6708224942]},
|
||||
{"learn":[0.6616824419],"iteration":473,"passed_time":16.04763422,"remaining_time":51.66390258,"test":[0.6708363084]},
|
||||
{"learn":[0.6616538226],"iteration":474,"passed_time":16.07374645,"remaining_time":51.60518598,"test":[0.670850875]},
|
||||
{"learn":[0.6616314155],"iteration":475,"passed_time":16.09993591,"remaining_time":51.54685363,"test":[0.6708527236]},
|
||||
{"learn":[0.6616127861],"iteration":476,"passed_time":16.12811357,"remaining_time":51.49500411,"test":[0.6708453401]},
|
||||
{"learn":[0.6616029072],"iteration":477,"passed_time":16.15264086,"remaining_time":51.43163051,"test":[0.6708413844]},
|
||||
{"learn":[0.6615843751],"iteration":478,"passed_time":16.17696751,"remaining_time":51.36778201,"test":[0.6708364569]},
|
||||
{"learn":[0.661563216],"iteration":479,"passed_time":16.20551145,"remaining_time":51.31745293,"test":[0.6708251774]},
|
||||
{"learn":[0.6615432257],"iteration":480,"passed_time":16.22860577,"remaining_time":51.2500045,"test":[0.6708154393]},
|
||||
{"learn":[0.6615263324],"iteration":481,"passed_time":16.25544093,"remaining_time":51.19452144,"test":[0.6708111613]},
|
||||
{"learn":[0.6615033259],"iteration":482,"passed_time":16.27729221,"remaining_time":51.12350369,"test":[0.6708102339]},
|
||||
{"learn":[0.661484293],"iteration":483,"passed_time":16.30502335,"remaining_time":51.07110619,"test":[0.6707929623]},
|
||||
{"learn":[0.6614678231],"iteration":484,"passed_time":16.32842702,"remaining_time":51.00529266,"test":[0.6707900226]},
|
||||
{"learn":[0.6614463024],"iteration":485,"passed_time":16.36272839,"remaining_time":50.97360242,"test":[0.6707832384]},
|
||||
{"learn":[0.6614155436],"iteration":486,"passed_time":16.39272506,"remaining_time":50.92852776,"test":[0.6707739118]},
|
||||
{"learn":[0.6613958945],"iteration":487,"passed_time":16.42636604,"remaining_time":50.89480625,"test":[0.6707737538]},
|
||||
{"learn":[0.661380611],"iteration":488,"passed_time":16.4597142,"remaining_time":50.86018027,"test":[0.6707730234]},
|
||||
{"learn":[0.6613677802],"iteration":489,"passed_time":16.48056007,"remaining_time":50.78703206,"test":[0.6707796291]},
|
||||
{"learn":[0.6613530086],"iteration":490,"passed_time":16.51091177,"remaining_time":50.74331132,"test":[0.670791408]},
|
||||
{"learn":[0.6613248211],"iteration":491,"passed_time":16.53097438,"remaining_time":50.66810846,"test":[0.6707944906]},
|
||||
{"learn":[0.6613059359],"iteration":492,"passed_time":16.56161402,"remaining_time":50.62546112,"test":[0.6707835635]},
|
||||
{"learn":[0.6612729965],"iteration":493,"passed_time":16.5854633,"remaining_time":50.56216139,"test":[0.6707908928]},
|
||||
{"learn":[0.6612624948],"iteration":494,"passed_time":16.61302735,"remaining_time":50.51031547,"test":[0.670796262]},
|
||||
{"learn":[0.6612401679],"iteration":495,"passed_time":16.63896978,"remaining_time":50.45365029,"test":[0.6707877825]},
|
||||
{"learn":[0.6612191637],"iteration":496,"passed_time":16.663707,"remaining_time":50.39346403,"test":[0.6707854132]},
|
||||
{"learn":[0.6611912219],"iteration":497,"passed_time":16.69040179,"remaining_time":50.33932428,"test":[0.6707756206]},
|
||||
{"learn":[0.6611773017],"iteration":498,"passed_time":16.71612789,"remaining_time":50.28238068,"test":[0.6707707899]},
|
||||
{"learn":[0.6611638216],"iteration":499,"passed_time":16.74072553,"remaining_time":50.2221766,"test":[0.6707704386]},
|
||||
{"learn":[0.6611450533],"iteration":500,"passed_time":16.77346538,"remaining_time":50.18647626,"test":[0.6707621465]},
|
||||
{"learn":[0.6611179111],"iteration":501,"passed_time":16.80230735,"remaining_time":50.13915621,"test":[0.6707661931]},
|
||||
{"learn":[0.6610959069],"iteration":502,"passed_time":16.83637769,"remaining_time":50.10747,"test":[0.6707651988]},
|
||||
{"learn":[0.6610728788],"iteration":503,"passed_time":16.87382128,"remaining_time":50.08578697,"test":[0.6707607827]},
|
||||
{"learn":[0.6610436668],"iteration":504,"passed_time":16.92151611,"remaining_time":50.09438927,"test":[0.670760242]},
|
||||
{"learn":[0.6610188976],"iteration":505,"passed_time":16.9898618,"remaining_time":50.16374216,"test":[0.6707506008]},
|
||||
{"learn":[0.6610030555],"iteration":506,"passed_time":17.03818668,"remaining_time":50.17359509,"test":[0.6707452886]},
|
||||
{"learn":[0.6609831174],"iteration":507,"passed_time":17.06933058,"remaining_time":50.13275833,"test":[0.6707355189]},
|
||||
{"learn":[0.6609586562],"iteration":508,"passed_time":17.1106164,"remaining_time":50.12166807,"test":[0.6707312551]},
|
||||
{"learn":[0.660935882],"iteration":509,"passed_time":17.14537899,"remaining_time":50.09140137,"test":[0.6707199485]},
|
||||
{"learn":[0.6609202024],"iteration":510,"passed_time":17.19066307,"remaining_time":50.09177556,"test":[0.6707131947]},
|
||||
{"learn":[0.6609011137],"iteration":511,"passed_time":17.21958034,"remaining_time":50.04440537,"test":[0.6707154112]},
|
||||
{"learn":[0.6608726737],"iteration":512,"passed_time":17.24756917,"remaining_time":49.99441591,"test":[0.6706982346]},
|
||||
{"learn":[0.6608608849],"iteration":513,"passed_time":17.27150822,"remaining_time":49.93280391,"test":[0.6706988941]},
|
||||
{"learn":[0.6608387256],"iteration":514,"passed_time":17.29800365,"remaining_time":49.87870957,"test":[0.6706989098]},
|
||||
{"learn":[0.6608136063],"iteration":515,"passed_time":17.34332283,"remaining_time":49.87885868,"test":[0.670693306]},
|
||||
{"learn":[0.6607946343],"iteration":516,"passed_time":17.37393636,"remaining_time":49.83664916,"test":[0.6706944515]},
|
||||
{"learn":[0.6607703935],"iteration":517,"passed_time":17.4173655,"remaining_time":49.83114994,"test":[0.6706899688]},
|
||||
{"learn":[0.6607509625],"iteration":518,"passed_time":17.46008645,"remaining_time":49.82348368,"test":[0.6706909374]},
|
||||
{"learn":[0.6607238109],"iteration":519,"passed_time":17.4906988,"remaining_time":49.78121967,"test":[0.6706855074]},
|
||||
{"learn":[0.6606999858],"iteration":520,"passed_time":17.5186435,"remaining_time":49.7314275,"test":[0.6706787779]},
|
||||
{"learn":[0.6606813873],"iteration":521,"passed_time":17.54613056,"remaining_time":49.6804233,"test":[0.6706737082]},
|
||||
{"learn":[0.6606610372],"iteration":522,"passed_time":17.57100039,"remaining_time":49.62211774,"test":[0.6706761225]},
|
||||
{"learn":[0.660638456],"iteration":523,"passed_time":17.60084283,"remaining_time":49.5779466,"test":[0.670685455]},
|
||||
{"learn":[0.6606156483],"iteration":524,"passed_time":17.62599925,"remaining_time":49.52066456,"test":[0.6706693855]},
|
||||
{"learn":[0.6605968623],"iteration":525,"passed_time":17.65519625,"remaining_time":49.47482751,"test":[0.6706647216]},
|
||||
{"learn":[0.6605735776],"iteration":526,"passed_time":17.67910836,"remaining_time":49.41428199,"test":[0.6706569188]},
|
||||
{"learn":[0.6605517294],"iteration":527,"passed_time":17.70744827,"remaining_time":49.36621942,"test":[0.6706549134]},
|
||||
{"learn":[0.6605309239],"iteration":528,"passed_time":17.72943083,"remaining_time":49.3005534,"test":[0.6706547978]},
|
||||
{"learn":[0.6605086434],"iteration":529,"passed_time":17.75830336,"remaining_time":49.25416215,"test":[0.6706564214]},
|
||||
{"learn":[0.6604803349],"iteration":530,"passed_time":17.78141858,"remaining_time":49.19190939,"test":[0.6706559196]},
|
||||
{"learn":[0.6604566326],"iteration":531,"passed_time":17.80870208,"remaining_time":49.14130574,"test":[0.6706515072]},
|
||||
{"learn":[0.6604430839],"iteration":532,"passed_time":17.82904188,"remaining_time":49.07167811,"test":[0.6706474616]},
|
||||
{"learn":[0.6604273738],"iteration":533,"passed_time":17.86246645,"remaining_time":49.03815696,"test":[0.6706424204]},
|
||||
{"learn":[0.6604048016],"iteration":534,"passed_time":17.90552779,"remaining_time":49.03102469,"test":[0.6706520008]},
|
||||
{"learn":[0.6603845173],"iteration":535,"passed_time":18.02843143,"remaining_time":49.24183511,"test":[0.6706448306]},
|
||||
{"learn":[0.6603669212],"iteration":536,"passed_time":18.07245966,"remaining_time":49.23651485,"test":[0.6706415789]},
|
||||
{"learn":[0.6603488983],"iteration":537,"passed_time":18.10631942,"remaining_time":49.20341819,"test":[0.6706305359]},
|
||||
{"learn":[0.6603176881],"iteration":538,"passed_time":18.13531438,"remaining_time":49.1571323,"test":[0.6706152774]},
|
||||
{"learn":[0.6602953862],"iteration":539,"passed_time":18.16575265,"remaining_time":49.11481272,"test":[0.670616585]},
|
||||
{"learn":[0.6602672025],"iteration":540,"passed_time":18.20025584,"remaining_time":49.08349958,"test":[0.6705963243]},
|
||||
{"learn":[0.6602568636],"iteration":541,"passed_time":18.22381751,"remaining_time":49.02274158,"test":[0.6706027368]},
|
||||
{"learn":[0.660235705],"iteration":542,"passed_time":18.25438575,"remaining_time":48.98092088,"test":[0.6706003522]},
|
||||
{"learn":[0.6602152295],"iteration":543,"passed_time":18.28070524,"remaining_time":48.9277699,"test":[0.6706044301]},
|
||||
{"learn":[0.6601897709],"iteration":544,"passed_time":18.30768805,"remaining_time":48.87648827,"test":[0.6706047241]},
|
||||
{"learn":[0.6601683731],"iteration":545,"passed_time":18.33807201,"remaining_time":48.83435294,"test":[0.6706038235]},
|
||||
{"learn":[0.6601472267],"iteration":546,"passed_time":18.36776304,"remaining_time":48.79041993,"test":[0.6706026913]},
|
||||
{"learn":[0.6601262337],"iteration":547,"passed_time":18.41134623,"remaining_time":48.78334803,"test":[0.6705845786]},
|
||||
{"learn":[0.6601119991],"iteration":548,"passed_time":18.44405381,"remaining_time":48.74739905,"test":[0.6705873967]},
|
||||
{"learn":[0.6600869973],"iteration":549,"passed_time":18.47010718,"remaining_time":48.69391893,"test":[0.6705755426]},
|
||||
{"learn":[0.6600667497],"iteration":550,"passed_time":18.5036553,"remaining_time":48.66024779,"test":[0.6705715731]},
|
||||
{"learn":[0.6600397508],"iteration":551,"passed_time":18.53164471,"remaining_time":48.61199556,"test":[0.6705757153]},
|
||||
{"learn":[0.660016863],"iteration":552,"passed_time":18.5577607,"remaining_time":48.55891452,"test":[0.6705516814]},
|
||||
{"learn":[0.6599933158],"iteration":553,"passed_time":18.58492994,"remaining_time":48.50867995,"test":[0.6705530864]},
|
||||
{"learn":[0.6599632649],"iteration":554,"passed_time":18.62562092,"remaining_time":48.49373376,"test":[0.6705552479]},
|
||||
{"learn":[0.6599446007],"iteration":555,"passed_time":18.65010209,"remaining_time":48.43659608,"test":[0.6705563336]},
|
||||
{"learn":[0.6599138126],"iteration":556,"passed_time":18.67796421,"remaining_time":48.38833458,"test":[0.6705718544]},
|
||||
{"learn":[0.6598965504],"iteration":557,"passed_time":18.70319381,"remaining_time":48.33334314,"test":[0.6705688384]},
|
||||
{"learn":[0.6598785723],"iteration":558,"passed_time":18.72995694,"remaining_time":48.28241136,"test":[0.6705641528]},
|
||||
{"learn":[0.659860838],"iteration":559,"passed_time":18.75657945,"remaining_time":48.23120429,"test":[0.6705628467]},
|
||||
{"learn":[0.6598408724],"iteration":560,"passed_time":18.78181322,"remaining_time":48.17652269,"test":[0.670558488]},
|
||||
{"learn":[0.6598244857],"iteration":561,"passed_time":18.80867415,"remaining_time":48.12610931,"test":[0.6705544404]},
|
||||
{"learn":[0.6598082469],"iteration":562,"passed_time":18.83488797,"remaining_time":48.0741279,"test":[0.6705617451]},
|
||||
{"learn":[0.6597851673],"iteration":563,"passed_time":18.86939449,"remaining_time":48.04335193,"test":[0.6705631717]},
|
||||
{"learn":[0.6597683521],"iteration":564,"passed_time":18.90235988,"remaining_time":48.00864854,"test":[0.6705636201]},
|
||||
{"learn":[0.6597479006],"iteration":565,"passed_time":18.93001053,"remaining_time":47.96048604,"test":[0.6705537522]},
|
||||
{"learn":[0.6597310938],"iteration":566,"passed_time":18.95858079,"remaining_time":47.91472006,"test":[0.670555083]},
|
||||
{"learn":[0.6597096581],"iteration":567,"passed_time":18.9833487,"remaining_time":47.85942842,"test":[0.6705524541]},
|
||||
{"learn":[0.6596862311],"iteration":568,"passed_time":19.0162481,"remaining_time":47.82469425,"test":[0.6705503132]},
|
||||
{"learn":[0.6596574779],"iteration":569,"passed_time":19.03781666,"remaining_time":47.76154004,"test":[0.6705354602]},
|
||||
{"learn":[0.6596385418],"iteration":570,"passed_time":19.0681355,"remaining_time":47.72043018,"test":[0.6705387012]},
|
||||
{"learn":[0.6596189903],"iteration":571,"passed_time":19.09073714,"remaining_time":47.66009201,"test":[0.6705411923]},
|
||||
{"learn":[0.65959275],"iteration":572,"passed_time":19.11146842,"remaining_time":47.59522765,"test":[0.6705390018]},
|
||||
{"learn":[0.6595730662],"iteration":573,"passed_time":19.141368,"remaining_time":47.55329403,"test":[0.6705354939]},
|
||||
{"learn":[0.6595566809],"iteration":574,"passed_time":19.16428373,"remaining_time":47.49409447,"test":[0.670531296]},
|
||||
{"learn":[0.6595365076],"iteration":575,"passed_time":19.19652276,"remaining_time":47.45807015,"test":[0.6705377163]},
|
||||
{"learn":[0.6595163446],"iteration":576,"passed_time":19.21727405,"remaining_time":47.39372785,"test":[0.6705248875]},
|
||||
{"learn":[0.6594816637],"iteration":577,"passed_time":19.24969594,"remaining_time":47.35824848,"test":[0.6705252902]},
|
||||
{"learn":[0.6594570142],"iteration":578,"passed_time":19.27445137,"remaining_time":47.30396442,"test":[0.6705181562]},
|
||||
{"learn":[0.6594353055],"iteration":579,"passed_time":19.29822455,"remaining_time":47.24737734,"test":[0.6705123446]},
|
||||
{"learn":[0.6594162362],"iteration":580,"passed_time":19.32403522,"remaining_time":47.19587948,"test":[0.6705128345]},
|
||||
{"learn":[0.659395036],"iteration":581,"passed_time":19.35739555,"remaining_time":47.16286408,"test":[0.6705173712]},
|
||||
{"learn":[0.6593798831],"iteration":582,"passed_time":19.39112791,"remaining_time":47.13075172,"test":[0.670541941]},
|
||||
{"learn":[0.6593556719],"iteration":583,"passed_time":19.42704318,"remaining_time":47.1039266,"test":[0.6705463243]},
|
||||
{"learn":[0.6593292627],"iteration":584,"passed_time":19.46022169,"remaining_time":47.07045077,"test":[0.6705513215]},
|
||||
{"learn":[0.6592976737],"iteration":585,"passed_time":19.48332075,"remaining_time":47.01265452,"test":[0.6705455889]},
|
||||
{"learn":[0.6592754841],"iteration":586,"passed_time":19.5115578,"remaining_time":46.9673444,"test":[0.6705408087]},
|
||||
{"learn":[0.6592510441],"iteration":587,"passed_time":19.54275193,"remaining_time":46.92919341,"test":[0.6705510193]},
|
||||
{"learn":[0.6592290326],"iteration":588,"passed_time":19.56411389,"remaining_time":46.86751222,"test":[0.6705456751]},
|
||||
{"learn":[0.6592097404],"iteration":589,"passed_time":19.59700884,"remaining_time":46.8335296,"test":[0.6705402427]},
|
||||
{"learn":[0.6591876204],"iteration":590,"passed_time":19.62169623,"remaining_time":46.77998306,"test":[0.6705443402]},
|
||||
{"learn":[0.6591705995],"iteration":591,"passed_time":19.64747626,"remaining_time":46.72913272,"test":[0.67054441]},
|
||||
{"learn":[0.6591456195],"iteration":592,"passed_time":19.67090184,"remaining_time":46.67278059,"test":[0.6705441955]},
|
||||
{"learn":[0.6591107122],"iteration":593,"passed_time":19.69910949,"remaining_time":46.62785848,"test":[0.6705319356]},
|
||||
{"learn":[0.6590819533],"iteration":594,"passed_time":19.72694709,"remaining_time":46.58211876,"test":[0.6705358843]},
|
||||
{"learn":[0.6590551327],"iteration":595,"passed_time":19.7530808,"remaining_time":46.53242523,"test":[0.6705334396]},
|
||||
{"learn":[0.6590373916],"iteration":596,"passed_time":19.77835609,"remaining_time":46.48079328,"test":[0.6705320462]},
|
||||
{"learn":[0.6590177149],"iteration":597,"passed_time":19.80378809,"remaining_time":46.4296169,"test":[0.6705332043]},
|
||||
{"learn":[0.6589946095],"iteration":598,"passed_time":19.83052585,"remaining_time":46.38158048,"test":[0.6705328363]},
|
||||
{"learn":[0.6589697628],"iteration":599,"passed_time":19.8579153,"remaining_time":46.33513569,"test":[0.6705315638]},
|
||||
{"learn":[0.6589442269],"iteration":600,"passed_time":19.89600309,"remaining_time":46.31365777,"test":[0.6705274435]},
|
||||
{"learn":[0.6589182437],"iteration":601,"passed_time":19.92518872,"remaining_time":46.27145155,"test":[0.670509808]},
|
||||
{"learn":[0.6588837179],"iteration":602,"passed_time":19.95754179,"remaining_time":46.23662666,"test":[0.6705077789]},
|
||||
{"learn":[0.6588674101],"iteration":603,"passed_time":19.99116426,"remaining_time":46.20474388,"test":[0.6705212132]},
|
||||
{"learn":[0.6588406916],"iteration":604,"passed_time":20.01900069,"remaining_time":46.15951398,"test":[0.6705098442]},
|
||||
{"learn":[0.6588149945],"iteration":605,"passed_time":20.04735837,"remaining_time":46.11554053,"test":[0.6705061509]},
|
||||
{"learn":[0.6587866031],"iteration":606,"passed_time":20.07232044,"remaining_time":46.06382599,"test":[0.6705003071]},
|
||||
{"learn":[0.6587636648],"iteration":607,"passed_time":20.09871086,"remaining_time":46.01546959,"test":[0.6705045031]},
|
||||
{"learn":[0.6587502469],"iteration":608,"passed_time":20.12348304,"remaining_time":45.96348917,"test":[0.6705083194]},
|
||||
{"learn":[0.6587292784],"iteration":609,"passed_time":20.14920752,"remaining_time":45.91376797,"test":[0.6705329997]},
|
||||
{"learn":[0.6587104112],"iteration":610,"passed_time":20.17662353,"remaining_time":45.86797068,"test":[0.6705269987]},
|
||||
{"learn":[0.6586953782],"iteration":611,"passed_time":20.20202219,"remaining_time":45.81765818,"test":[0.6705315607]},
|
||||
{"learn":[0.6586641191],"iteration":612,"passed_time":20.23050051,"remaining_time":45.77439512,"test":[0.6705142835]},
|
||||
{"learn":[0.6586450136],"iteration":613,"passed_time":20.25381994,"remaining_time":45.71953492,"test":[0.6705165015]},
|
||||
{"learn":[0.6586136263],"iteration":614,"passed_time":20.28518384,"remaining_time":45.68289369,"test":[0.6705001061]},
|
||||
{"learn":[0.6585862768],"iteration":615,"passed_time":20.3078175,"remaining_time":45.62665489,"test":[0.6705013916]},
|
||||
{"learn":[0.6585585235],"iteration":616,"passed_time":20.33878033,"remaining_time":45.5891948,"test":[0.6705037253]},
|
||||
{"learn":[0.6585371631],"iteration":617,"passed_time":20.36122842,"remaining_time":45.53271469,"test":[0.67049647]},
|
||||
{"learn":[0.6585092632],"iteration":618,"passed_time":20.3943397,"remaining_time":45.50013429,"test":[0.6705005632]},
|
||||
{"learn":[0.6584914317],"iteration":619,"passed_time":20.42384285,"remaining_time":45.45952119,"test":[0.6704957943]},
|
||||
{"learn":[0.6584662432],"iteration":620,"passed_time":20.45411533,"remaining_time":45.42065225,"test":[0.6704955333]},
|
||||
{"learn":[0.6584454668],"iteration":621,"passed_time":20.488223,"remaining_time":45.39030754,"test":[0.6704961207]},
|
||||
{"learn":[0.6584249408],"iteration":622,"passed_time":20.51043528,"remaining_time":45.33365872,"test":[0.6704921459]},
|
||||
{"learn":[0.6583931228],"iteration":623,"passed_time":20.54384208,"remaining_time":45.30180561,"test":[0.6704751713]},
|
||||
{"learn":[0.6583660767],"iteration":624,"passed_time":20.56912557,"remaining_time":45.25207624,"test":[0.6704753101]},
|
||||
{"learn":[0.658354264],"iteration":625,"passed_time":20.59414123,"remaining_time":45.20183714,"test":[0.6704620888]},
|
||||
{"learn":[0.6583253625],"iteration":626,"passed_time":20.61901142,"remaining_time":45.15135993,"test":[0.6704604282]},
|
||||
{"learn":[0.6582968632],"iteration":627,"passed_time":20.6468542,"remaining_time":45.10745855,"test":[0.6704663192]},
|
||||
{"learn":[0.6582687399],"iteration":628,"passed_time":20.67583093,"remaining_time":45.06607981,"test":[0.6704680085]},
|
||||
{"learn":[0.658242535],"iteration":629,"passed_time":20.7010198,"remaining_time":45.01650336,"test":[0.670453228]},
|
||||
{"learn":[0.6582199874],"iteration":630,"passed_time":20.72783977,"remaining_time":44.97054302,"test":[0.6704577785]},
|
||||
{"learn":[0.6581918101],"iteration":631,"passed_time":20.75222724,"remaining_time":44.91937795,"test":[0.67046675]},
|
||||
{"learn":[0.6581735218],"iteration":632,"passed_time":20.78264004,"remaining_time":44.88130954,"test":[0.6704731863]},
|
||||
{"learn":[0.6581445869],"iteration":633,"passed_time":20.80459182,"remaining_time":44.82503538,"test":[0.6704811116]},
|
||||
{"learn":[0.6581202427],"iteration":634,"passed_time":20.83717209,"remaining_time":44.79171637,"test":[0.6704839644]},
|
||||
{"learn":[0.6580977862],"iteration":635,"passed_time":20.86231353,"remaining_time":44.74244599,"test":[0.6704854798]},
|
||||
{"learn":[0.6580724179],"iteration":636,"passed_time":20.89269601,"remaining_time":44.70446572,"test":[0.6704835837]},
|
||||
{"learn":[0.6580426322],"iteration":637,"passed_time":20.93117347,"remaining_time":44.68379039,"test":[0.6704736198]},
|
||||
{"learn":[0.6580111256],"iteration":638,"passed_time":20.96066949,"remaining_time":44.64392985,"test":[0.6704640242]},
|
||||
{"learn":[0.6579834747],"iteration":639,"passed_time":20.9941179,"remaining_time":44.61250055,"test":[0.670465663]},
|
||||
{"learn":[0.6579541367],"iteration":640,"passed_time":21.0224519,"remaining_time":44.57022174,"test":[0.6704646829]},
|
||||
{"learn":[0.6579254503],"iteration":641,"passed_time":21.0522529,"remaining_time":44.53108946,"test":[0.6704600961]},
|
||||
{"learn":[0.657898555],"iteration":642,"passed_time":21.08260618,"remaining_time":44.49315178,"test":[0.6704643207]},
|
||||
{"learn":[0.6578676875],"iteration":643,"passed_time":21.10716702,"remaining_time":44.44304112,"test":[0.6704600533]},
|
||||
{"learn":[0.6578324163],"iteration":644,"passed_time":21.13594828,"remaining_time":44.40187584,"test":[0.6704614691]},
|
||||
{"learn":[0.6578062223],"iteration":645,"passed_time":21.1601277,"remaining_time":44.35110357,"test":[0.6704728212]},
|
||||
{"learn":[0.6577760631],"iteration":646,"passed_time":21.18552999,"remaining_time":44.30297075,"test":[0.6704758731]},
|
||||
{"learn":[0.6577483474],"iteration":647,"passed_time":21.21048648,"remaining_time":44.25397797,"test":[0.6704833026]},
|
||||
{"learn":[0.6577249642],"iteration":648,"passed_time":21.23686209,"remaining_time":44.20801337,"test":[0.6704767664]},
|
||||
{"learn":[0.6576974966],"iteration":649,"passed_time":21.26287585,"remaining_time":44.16135753,"test":[0.6704702727]},
|
||||
{"learn":[0.657675114],"iteration":650,"passed_time":21.28806218,"remaining_time":44.11305051,"test":[0.6704671372]},
|
||||
{"learn":[0.6576447891],"iteration":651,"passed_time":21.31506267,"remaining_time":44.06856515,"test":[0.6704699936]},
|
||||
{"learn":[0.6576102356],"iteration":652,"passed_time":21.3435081,"remaining_time":44.02711394,"test":[0.6704587989]},
|
||||
{"learn":[0.6575793887],"iteration":653,"passed_time":21.37776713,"remaining_time":43.99766753,"test":[0.6704637668]},
|
||||
{"learn":[0.6575543309],"iteration":654,"passed_time":21.40301154,"remaining_time":43.94969545,"test":[0.6704653717]},
|
||||
{"learn":[0.6575340787],"iteration":655,"passed_time":21.44023109,"remaining_time":43.92632711,"test":[0.6704598273]},
|
||||
{"learn":[0.6575061464],"iteration":656,"passed_time":21.4778965,"remaining_time":43.903828,"test":[0.6704522865]},
|
||||
{"learn":[0.657476113],"iteration":657,"passed_time":21.50245582,"remaining_time":43.85455275,"test":[0.6704558586]},
|
||||
{"learn":[0.6574447014],"iteration":658,"passed_time":21.53379663,"remaining_time":43.81915217,"test":[0.6704466331]},
|
||||
{"learn":[0.6574247361],"iteration":659,"passed_time":21.55955041,"remaining_time":43.77242053,"test":[0.6704405886]},
|
||||
{"learn":[0.6574034983],"iteration":660,"passed_time":21.58626671,"remaining_time":43.72770215,"test":[0.6704463767]},
|
||||
{"learn":[0.6573783832],"iteration":661,"passed_time":21.61183918,"remaining_time":43.68072633,"test":[0.6704475216]},
|
||||
{"learn":[0.657357694],"iteration":662,"passed_time":21.6373217,"remaining_time":43.63363366,"test":[0.6704572386]},
|
||||
{"learn":[0.6573411592],"iteration":663,"passed_time":21.66283476,"remaining_time":43.58666753,"test":[0.6704658153]},
|
||||
{"learn":[0.6573118559],"iteration":664,"passed_time":21.68841321,"remaining_time":43.5398972,"test":[0.6704600945]},
|
||||
{"learn":[0.6572819076],"iteration":665,"passed_time":21.71420973,"remaining_time":43.4936273,"test":[0.6704561998]},
|
||||
{"learn":[0.6572430097],"iteration":666,"passed_time":21.74213421,"remaining_time":43.45167151,"test":[0.6704535154]},
|
||||
{"learn":[0.6572160391],"iteration":667,"passed_time":21.77174463,"remaining_time":43.41311953,"test":[0.6704413781]},
|
||||
{"learn":[0.6571931413],"iteration":668,"passed_time":21.81895309,"remaining_time":43.40960622,"test":[0.6704450013]},
|
||||
{"learn":[0.6571737099],"iteration":669,"passed_time":21.84627583,"remaining_time":43.36648784,"test":[0.6704422199]},
|
||||
{"learn":[0.6571532872],"iteration":670,"passed_time":21.88834724,"remaining_time":43.35262814,"test":[0.67044342]},
|
||||
{"learn":[0.6571208939],"iteration":671,"passed_time":21.93403139,"remaining_time":43.34582395,"test":[0.6704415341]},
|
||||
{"learn":[0.6570887673],"iteration":672,"passed_time":21.9714274,"remaining_time":43.32256191,"test":[0.6704439539]},
|
||||
{"learn":[0.6570633692],"iteration":673,"passed_time":22.01942449,"remaining_time":43.32011406,"test":[0.6704498197]},
|
||||
{"learn":[0.6570454361],"iteration":674,"passed_time":22.05319867,"remaining_time":43.2896122,"test":[0.6704452194]},
|
||||
{"learn":[0.6570231031],"iteration":675,"passed_time":22.09079747,"remaining_time":43.26659149,"test":[0.6704366524]},
|
||||
{"learn":[0.6570052089],"iteration":676,"passed_time":22.14192346,"remaining_time":43.26996269,"test":[0.6704427124]},
|
||||
{"learn":[0.6569855794],"iteration":677,"passed_time":22.17624471,"remaining_time":43.24040635,"test":[0.6704395579]},
|
||||
{"learn":[0.6569579709],"iteration":678,"passed_time":22.213192,"remaining_time":43.21594497,"test":[0.6704401246]},
|
||||
{"learn":[0.6569333354],"iteration":679,"passed_time":22.23966403,"remaining_time":43.17111253,"test":[0.6704415621]},
|
||||
{"learn":[0.6569069617],"iteration":680,"passed_time":22.27051241,"remaining_time":43.13481039,"test":[0.6704341343]},
|
||||
{"learn":[0.6568931857],"iteration":681,"passed_time":22.29625075,"remaining_time":43.08864881,"test":[0.6704369615]},
|
||||
{"learn":[0.6568734532],"iteration":682,"passed_time":22.32160622,"remaining_time":43.04180877,"test":[0.6704357425]},
|
||||
{"learn":[0.6568435196],"iteration":683,"passed_time":22.35059872,"remaining_time":43.00202911,"test":[0.6704294622]},
|
||||
{"learn":[0.6568108038],"iteration":684,"passed_time":22.37956576,"remaining_time":42.96223208,"test":[0.6704289794]},
|
||||
{"learn":[0.6567811374],"iteration":685,"passed_time":22.41993338,"remaining_time":42.94430389,"test":[0.6704272409]},
|
||||
{"learn":[0.6567467284],"iteration":686,"passed_time":22.45285267,"remaining_time":42.91207504,"test":[0.6704101162]},
|
||||
{"learn":[0.6567172734],"iteration":687,"passed_time":22.4848431,"remaining_time":42.8780729,"test":[0.6704069439]},
|
||||
{"learn":[0.6566967606],"iteration":688,"passed_time":22.51193834,"remaining_time":42.83476221,"test":[0.6704100747]},
|
||||
{"learn":[0.6566720128],"iteration":689,"passed_time":22.53798671,"remaining_time":42.78951101,"test":[0.6704122261]},
|
||||
{"learn":[0.6566441608],"iteration":690,"passed_time":22.57108439,"remaining_time":42.75766928,"test":[0.6704137826]},
|
||||
{"learn":[0.6566172287],"iteration":691,"passed_time":22.59836588,"remaining_time":42.7148303,"test":[0.6704207952]},
|
||||
{"learn":[0.6565952549],"iteration":692,"passed_time":22.62447507,"remaining_time":42.66982528,"test":[0.6704154834]},
|
||||
{"learn":[0.6565702687],"iteration":693,"passed_time":22.65349415,"remaining_time":42.63035067,"test":[0.6704253514]},
|
||||
{"learn":[0.6565392213],"iteration":694,"passed_time":22.68028991,"remaining_time":42.58673141,"test":[0.6704155636]},
|
||||
{"learn":[0.6565157938],"iteration":695,"passed_time":22.70844406,"remaining_time":42.54570555,"test":[0.6704141298]},
|
||||
{"learn":[0.6564902789],"iteration":696,"passed_time":22.73944116,"remaining_time":42.51003133,"test":[0.6704207635]},
|
||||
{"learn":[0.6564644734],"iteration":697,"passed_time":22.7613976,"remaining_time":42.45750671,"test":[0.6704268341]},
|
||||
{"learn":[0.6564349549],"iteration":698,"passed_time":22.79216825,"remaining_time":42.42147482,"test":[0.6704243126]},
|
||||
{"learn":[0.6564046572],"iteration":699,"passed_time":22.8167121,"remaining_time":42.37389389,"test":[0.6704235165]},
|
||||
{"learn":[0.6563744107],"iteration":700,"passed_time":22.84507296,"remaining_time":42.33345189,"test":[0.6704257736]},
|
||||
{"learn":[0.6563525063],"iteration":701,"passed_time":22.87088832,"remaining_time":42.28833766,"test":[0.6704247758]},
|
||||
{"learn":[0.6563189867],"iteration":702,"passed_time":22.90238907,"remaining_time":42.25376759,"test":[0.6704331799]},
|
||||
{"learn":[0.6562939062],"iteration":703,"passed_time":22.94246813,"remaining_time":42.23499815,"test":[0.6704252722]},
|
||||
{"learn":[0.6562739297],"iteration":704,"passed_time":22.97441688,"remaining_time":42.20123385,"test":[0.6704146644]},
|
||||
{"learn":[0.656256438],"iteration":705,"passed_time":23.00262167,"remaining_time":42.16061253,"test":[0.6704164122]},
|
||||
{"learn":[0.6562366475],"iteration":706,"passed_time":23.033437,"remaining_time":42.12480062,"test":[0.6704118954]},
|
||||
{"learn":[0.6562073096],"iteration":707,"passed_time":23.0545813,"remaining_time":42.07135458,"test":[0.6704043129]},
|
||||
{"learn":[0.6561864222],"iteration":708,"passed_time":23.08699831,"remaining_time":42.03852584,"test":[0.6703978198]},
|
||||
{"learn":[0.6561578826],"iteration":709,"passed_time":23.11590694,"remaining_time":41.99932387,"test":[0.6703935976]},
|
||||
{"learn":[0.6561208567],"iteration":710,"passed_time":23.14362702,"remaining_time":41.9579961,"test":[0.6703839683]},
|
||||
{"learn":[0.6560924703],"iteration":711,"passed_time":23.16985155,"remaining_time":41.91400112,"test":[0.6703843723]},
|
||||
{"learn":[0.6560656907],"iteration":712,"passed_time":23.19510285,"remaining_time":41.86829925,"test":[0.6703879502]},
|
||||
{"learn":[0.6560362588],"iteration":713,"passed_time":23.23034771,"remaining_time":41.84065429,"test":[0.6703895978]},
|
||||
{"learn":[0.6560124527],"iteration":714,"passed_time":23.25923754,"remaining_time":41.80156678,"test":[0.6703894359]},
|
||||
{"learn":[0.6559875055],"iteration":715,"passed_time":23.28703452,"remaining_time":41.76054794,"test":[0.6703928777]},
|
||||
{"learn":[0.6559547281],"iteration":716,"passed_time":23.31161175,"remaining_time":41.71380457,"test":[0.6703933128]},
|
||||
{"learn":[0.6559230866],"iteration":717,"passed_time":23.34170355,"remaining_time":41.67696929,"test":[0.6703844355]},
|
||||
{"learn":[0.6558924823],"iteration":718,"passed_time":23.37263658,"remaining_time":41.64165155,"test":[0.6703825151]},
|
||||
{"learn":[0.6558676469],"iteration":719,"passed_time":23.40571088,"remaining_time":41.61015268,"test":[0.6703983542]},
|
||||
{"learn":[0.6558459277],"iteration":720,"passed_time":23.4389719,"remaining_time":41.57898067,"test":[0.670399556]},
|
||||
{"learn":[0.6558149638],"iteration":721,"passed_time":23.48304084,"remaining_time":41.56693379,"test":[0.6703931808]},
|
||||
{"learn":[0.6557812248],"iteration":722,"passed_time":23.50734531,"remaining_time":41.5198893,"test":[0.6703886918]},
|
||||
{"learn":[0.6557546502],"iteration":723,"passed_time":23.54055835,"remaining_time":41.48860836,"test":[0.6703847574]},
|
||||
{"learn":[0.6557274948],"iteration":724,"passed_time":23.56652491,"remaining_time":41.44457829,"test":[0.6703885941]},
|
||||
{"learn":[0.6557044723],"iteration":725,"passed_time":23.59580183,"remaining_time":41.40640708,"test":[0.6703788615]},
|
||||
{"learn":[0.6556751811],"iteration":726,"passed_time":23.62334313,"remaining_time":41.36522119,"test":[0.6703799906]},
|
||||
{"learn":[0.6556539158],"iteration":727,"passed_time":23.64879831,"remaining_time":41.32042782,"test":[0.6703774518]},
|
||||
{"learn":[0.6556182915],"iteration":728,"passed_time":23.67755213,"remaining_time":41.28143862,"test":[0.6703783496]},
|
||||
{"learn":[0.6555977079],"iteration":729,"passed_time":23.70012944,"remaining_time":41.23173204,"test":[0.6703648854]},
|
||||
{"learn":[0.6555667903],"iteration":730,"passed_time":23.72866102,"remaining_time":41.19243615,"test":[0.6703716654]},
|
||||
{"learn":[0.6555394075],"iteration":731,"passed_time":23.75226732,"remaining_time":41.14463793,"test":[0.6703550938]},
|
||||
{"learn":[0.6555122742],"iteration":732,"passed_time":23.7844108,"remaining_time":41.11166233,"test":[0.6703467057]},
|
||||
{"learn":[0.6554814941],"iteration":733,"passed_time":23.80747563,"remaining_time":41.06303017,"test":[0.6703484503]},
|
||||
{"learn":[0.6554517373],"iteration":734,"passed_time":23.84023587,"remaining_time":41.03115425,"test":[0.6703549183]},
|
||||
{"learn":[0.655429552],"iteration":735,"passed_time":23.87042124,"remaining_time":40.99485387,"test":[0.6703501504]},
|
||||
{"learn":[0.655396579],"iteration":736,"passed_time":23.9087808,"remaining_time":40.97257823,"test":[0.6703672622]},
|
||||
{"learn":[0.6553735864],"iteration":737,"passed_time":23.94161529,"remaining_time":40.94081097,"test":[0.6703560249]},
|
||||
{"learn":[0.6553472597],"iteration":738,"passed_time":23.97478791,"remaining_time":40.90961779,"test":[0.6703547155]},
|
||||
{"learn":[0.6553252832],"iteration":739,"passed_time":24.00628859,"remaining_time":40.87557247,"test":[0.6703593236]},
|
||||
{"learn":[0.6552971659],"iteration":740,"passed_time":24.03623034,"remaining_time":40.83888528,"test":[0.6703606827]},
|
||||
{"learn":[0.6552763852],"iteration":741,"passed_time":24.06404686,"remaining_time":40.79861313,"test":[0.6703511404]},
|
||||
{"learn":[0.6552488203],"iteration":742,"passed_time":24.09270947,"remaining_time":40.75980593,"test":[0.6703431646]},
|
||||
{"learn":[0.65521229],"iteration":743,"passed_time":24.12724624,"remaining_time":40.73094258,"test":[0.6703475116]},
|
||||
{"learn":[0.6551949744],"iteration":744,"passed_time":24.15397955,"remaining_time":40.68891857,"test":[0.6703483634]},
|
||||
{"learn":[0.6551673797],"iteration":745,"passed_time":24.17955779,"remaining_time":40.64499392,"test":[0.6703475713]},
|
||||
{"learn":[0.6551421856],"iteration":746,"passed_time":24.20715317,"remaining_time":40.60450191,"test":[0.670360457]},
|
||||
{"learn":[0.6551255516],"iteration":747,"passed_time":24.23336836,"remaining_time":40.5617342,"test":[0.6703664352]},
|
||||
{"learn":[0.6551019608],"iteration":748,"passed_time":24.2614437,"remaining_time":40.52211759,"test":[0.6703617612]},
|
||||
{"learn":[0.6550758728],"iteration":749,"passed_time":24.29512083,"remaining_time":40.49186805,"test":[0.6703669926]},
|
||||
{"learn":[0.655051966],"iteration":750,"passed_time":24.31839238,"remaining_time":40.44430371,"test":[0.6703670837]},
|
||||
{"learn":[0.6550351058],"iteration":751,"passed_time":24.34977118,"remaining_time":40.41025856,"test":[0.6703706628]},
|
||||
{"learn":[0.6549998756],"iteration":752,"passed_time":24.3762114,"remaining_time":40.36804198,"test":[0.670369618]},
|
||||
{"learn":[0.6549721212],"iteration":753,"passed_time":24.40831154,"remaining_time":40.3352204,"test":[0.6703692351]},
|
||||
{"learn":[0.6549401744],"iteration":754,"passed_time":24.44267281,"remaining_time":40.30612934,"test":[0.6703624433]},
|
||||
{"learn":[0.6549207325],"iteration":755,"passed_time":24.47460721,"remaining_time":40.27303091,"test":[0.6703686285]},
|
||||
{"learn":[0.6548900891],"iteration":756,"passed_time":24.50826603,"remaining_time":40.24276708,"test":[0.6703598432]},
|
||||
{"learn":[0.6548682731],"iteration":757,"passed_time":24.54826542,"remaining_time":40.22288345,"test":[0.6703618766]},
|
||||
{"learn":[0.6548418938],"iteration":758,"passed_time":24.57546587,"remaining_time":40.18201996,"test":[0.6703694148]},
|
||||
{"learn":[0.6548234717],"iteration":759,"passed_time":24.60502723,"remaining_time":40.14504442,"test":[0.6703683652]},
|
||||
{"learn":[0.6547996833],"iteration":760,"passed_time":24.63261096,"remaining_time":40.10486856,"test":[0.6703604855]},
|
||||
{"learn":[0.6547726174],"iteration":761,"passed_time":24.66001655,"remaining_time":40.06443634,"test":[0.6703758987]},
|
||||
{"learn":[0.6547509314],"iteration":762,"passed_time":24.68929907,"remaining_time":40.02708119,"test":[0.6703773302]},
|
||||
{"learn":[0.6547168175],"iteration":763,"passed_time":24.71425118,"remaining_time":39.98274144,"test":[0.6703641028]},
|
||||
{"learn":[0.6546907846],"iteration":764,"passed_time":24.74589169,"remaining_time":39.94924999,"test":[0.6703649602]},
|
||||
{"learn":[0.6546671611],"iteration":765,"passed_time":24.76625006,"remaining_time":39.89758822,"test":[0.6703567811]},
|
||||
{"learn":[0.6546475893],"iteration":766,"passed_time":24.79734832,"remaining_time":39.86327312,"test":[0.6703544688]},
|
||||
{"learn":[0.6546206223],"iteration":767,"passed_time":24.82531049,"remaining_time":39.82393558,"test":[0.6703611821]},
|
||||
{"learn":[0.6545874193],"iteration":768,"passed_time":24.85435247,"remaining_time":39.78635616,"test":[0.6703527821]},
|
||||
{"learn":[0.6545620629],"iteration":769,"passed_time":24.88095966,"remaining_time":39.74490958,"test":[0.6703523616]},
|
||||
{"learn":[0.6545346297],"iteration":770,"passed_time":24.90935211,"remaining_time":39.70634726,"test":[0.6703616298]},
|
||||
{"learn":[0.6545172316],"iteration":771,"passed_time":24.94098876,"remaining_time":39.67297175,"test":[0.6703603551]},
|
||||
{"learn":[0.6544943049],"iteration":772,"passed_time":24.97035098,"remaining_time":39.6359905,"test":[0.6703675655]},
|
||||
{"learn":[0.6544632323],"iteration":773,"passed_time":25.00434422,"remaining_time":39.60636436,"test":[0.6703582411]},
|
||||
{"learn":[0.6544384097],"iteration":774,"passed_time":25.03067441,"remaining_time":39.56461439,"test":[0.6703581437]},
|
||||
{"learn":[0.6544084745],"iteration":775,"passed_time":25.05692652,"remaining_time":39.522781,"test":[0.6703551885]},
|
||||
{"learn":[0.6543765257],"iteration":776,"passed_time":25.08660163,"remaining_time":39.48637554,"test":[0.6703608491]},
|
||||
{"learn":[0.6543536123],"iteration":777,"passed_time":25.10764591,"remaining_time":39.43643098,"test":[0.6703674554]},
|
||||
{"learn":[0.6543303593],"iteration":778,"passed_time":25.13940138,"remaining_time":39.40334928,"test":[0.6703679619]},
|
||||
{"learn":[0.6543005831],"iteration":779,"passed_time":25.15916899,"remaining_time":39.35152074,"test":[0.6703701757]},
|
||||
{"learn":[0.6542678123],"iteration":780,"passed_time":25.18841105,"remaining_time":39.31456219,"test":[0.6703603462]},
|
||||
{"learn":[0.6542439303],"iteration":781,"passed_time":25.21444083,"remaining_time":39.27262012,"test":[0.670359801]},
|
||||
{"learn":[0.6542100401],"iteration":782,"passed_time":25.24017824,"remaining_time":39.23026426,"test":[0.6703523669]},
|
||||
{"learn":[0.6541836178],"iteration":783,"passed_time":25.2660091,"remaining_time":39.18809574,"test":[0.6703365674]},
|
||||
{"learn":[0.654158129],"iteration":784,"passed_time":25.28891553,"remaining_time":39.1414425,"test":[0.6703486118]},
|
||||
{"learn":[0.6541343464],"iteration":785,"passed_time":25.31589904,"remaining_time":39.10114686,"test":[0.6703450011]},
|
||||
{"learn":[0.6541092921],"iteration":786,"passed_time":25.34123581,"remaining_time":39.05834694,"test":[0.6703473135]},
|
||||
{"learn":[0.6540812254],"iteration":787,"passed_time":25.36728606,"remaining_time":39.01668871,"test":[0.670350998]},
|
||||
{"learn":[0.654060259],"iteration":788,"passed_time":25.39177931,"remaining_time":38.97268028,"test":[0.6703417767]},
|
||||
{"learn":[0.6540467253],"iteration":789,"passed_time":25.41712461,"remaining_time":38.9300263,"test":[0.6703349821]},
|
||||
{"learn":[0.6540306837],"iteration":790,"passed_time":25.44804125,"remaining_time":38.89593157,"test":[0.6703457717]},
|
||||
{"learn":[0.6540103667],"iteration":791,"passed_time":25.48249341,"remaining_time":38.86723743,"test":[0.6703506266]},
|
||||
{"learn":[0.6539821302],"iteration":792,"passed_time":25.51450657,"remaining_time":38.83481643,"test":[0.6703596395]},
|
||||
{"learn":[0.6539577914],"iteration":793,"passed_time":25.54216564,"remaining_time":38.79578307,"test":[0.6703799895]},
|
||||
{"learn":[0.653923724],"iteration":794,"passed_time":25.56982738,"remaining_time":38.75678238,"test":[0.6703687687]},
|
||||
{"learn":[0.6539086888],"iteration":795,"passed_time":25.59539769,"remaining_time":38.71464675,"test":[0.6703780675]},
|
||||
{"learn":[0.6538798424],"iteration":796,"passed_time":25.61874122,"remaining_time":38.66919157,"test":[0.670374835]},
|
||||
{"learn":[0.6538566996],"iteration":797,"passed_time":25.64394874,"remaining_time":38.62659947,"test":[0.6703831387]},
|
||||
{"learn":[0.6538290752],"iteration":798,"passed_time":25.66776244,"remaining_time":38.58195581,"test":[0.670377656]},
|
||||
{"learn":[0.6538051255],"iteration":799,"passed_time":25.69593415,"remaining_time":38.54390122,"test":[0.6703689741]},
|
||||
{"learn":[0.6537917354],"iteration":800,"passed_time":25.71651353,"remaining_time":38.49450652,"test":[0.6703709756]},
|
||||
{"learn":[0.6537684302],"iteration":801,"passed_time":25.74304126,"remaining_time":38.45406912,"test":[0.6703737517]},
|
||||
{"learn":[0.6537402991],"iteration":802,"passed_time":25.77084871,"remaining_time":38.41557398,"test":[0.6703818964]},
|
||||
{"learn":[0.6537165427],"iteration":803,"passed_time":25.79028824,"remaining_time":38.36465763,"test":[0.6703812173]},
|
||||
{"learn":[0.6536853601],"iteration":804,"passed_time":25.82203653,"remaining_time":38.3320915,"test":[0.6703960068]},
|
||||
{"learn":[0.6536681479],"iteration":805,"passed_time":25.84395064,"remaining_time":38.28495914,"test":[0.6703976729]},
|
||||
{"learn":[0.6536409101],"iteration":806,"passed_time":25.87390688,"remaining_time":38.24977808,"test":[0.6704024604]},
|
||||
{"learn":[0.6536120189],"iteration":807,"passed_time":25.89606204,"remaining_time":38.20310143,"test":[0.6704085008]},
|
||||
{"learn":[0.6535912493],"iteration":808,"passed_time":25.92585483,"remaining_time":38.16772942,"test":[0.6704076633]},
|
||||
{"learn":[0.6535617421],"iteration":809,"passed_time":25.95539059,"remaining_time":38.13199358,"test":[0.6704111719]},
|
||||
{"learn":[0.6535315174],"iteration":810,"passed_time":25.98822968,"remaining_time":38.10111601,"test":[0.6704220803]},
|
||||
{"learn":[0.6534972927],"iteration":811,"passed_time":26.02835773,"remaining_time":38.08089777,"test":[0.6704265011]},
|
||||
{"learn":[0.6534818476],"iteration":812,"passed_time":26.0558565,"remaining_time":38.04219146,"test":[0.6704251162]},
|
||||
{"learn":[0.6534498323],"iteration":813,"passed_time":26.08151817,"remaining_time":38.00083606,"test":[0.6704375472]},
|
||||
{"learn":[0.6534305025],"iteration":814,"passed_time":26.10848988,"remaining_time":37.96142393,"test":[0.6704319336]},
|
||||
{"learn":[0.6534081059],"iteration":815,"passed_time":26.13143346,"remaining_time":37.91619757,"test":[0.670437614]},
|
||||
{"learn":[0.6533765804],"iteration":816,"passed_time":26.15923661,"remaining_time":37.87806231,"test":[0.6704554331]},
|
||||
{"learn":[0.6533441549],"iteration":817,"passed_time":26.18805523,"remaining_time":37.84141966,"test":[0.6704603317]},
|
||||
{"learn":[0.6533053405],"iteration":818,"passed_time":26.2140726,"remaining_time":37.8007567,"test":[0.6704548042]},
|
||||
{"learn":[0.6532838469],"iteration":819,"passed_time":26.24289367,"remaining_time":37.76416405,"test":[0.6704502654]},
|
||||
{"learn":[0.6532604302],"iteration":820,"passed_time":26.27260776,"remaining_time":37.72887277,"test":[0.6704512072]},
|
||||
{"learn":[0.6532364412],"iteration":821,"passed_time":26.29880394,"remaining_time":37.68855358,"test":[0.6704433481]},
|
||||
{"learn":[0.6532100089],"iteration":822,"passed_time":26.32785215,"remaining_time":37.65234749,"test":[0.6704095112]},
|
||||
{"learn":[0.6531782515],"iteration":823,"passed_time":26.35925682,"remaining_time":37.61952188,"test":[0.6704086019]},
|
||||
{"learn":[0.6531449701],"iteration":824,"passed_time":26.38596096,"remaining_time":37.580005,"test":[0.6703987131]},
|
||||
{"learn":[0.653115452],"iteration":825,"passed_time":26.40854839,"remaining_time":37.53466805,"test":[0.6704019708]},
|
||||
{"learn":[0.6530787602],"iteration":826,"passed_time":26.44419918,"remaining_time":37.50791492,"test":[0.6704046556]},
|
||||
{"learn":[0.653052397],"iteration":827,"passed_time":26.47784276,"remaining_time":37.47829917,"test":[0.6704091961]},
|
||||
{"learn":[0.6530313579],"iteration":828,"passed_time":26.51701028,"remaining_time":37.45647652,"test":[0.6704103204]},
|
||||
{"learn":[0.6530010363],"iteration":829,"passed_time":26.53963123,"remaining_time":37.41128739,"test":[0.6704074257]},
|
||||
{"learn":[0.6529752146],"iteration":830,"passed_time":26.57362226,"remaining_time":37.38214732,"test":[0.6704115335]},
|
||||
{"learn":[0.652954801],"iteration":831,"passed_time":26.59767057,"remaining_time":37.33903754,"test":[0.6704041275]},
|
||||
{"learn":[0.6529330351],"iteration":832,"passed_time":26.62378941,"remaining_time":37.29887425,"test":[0.6704004556]},
|
||||
{"learn":[0.6528993709],"iteration":833,"passed_time":26.65024746,"remaining_time":37.25921887,"test":[0.6704037097]},
|
||||
{"learn":[0.6528665883],"iteration":834,"passed_time":26.67774911,"remaining_time":37.22105115,"test":[0.6704035477]},
|
||||
{"learn":[0.6528413041],"iteration":835,"passed_time":26.70473813,"remaining_time":37.1821952,"test":[0.6704025281]},
|
||||
{"learn":[0.6528217161],"iteration":836,"passed_time":26.72833235,"remaining_time":37.13865056,"test":[0.6704024549]},
|
||||
{"learn":[0.6527978782],"iteration":837,"passed_time":26.76384162,"remaining_time":37.11167537,"test":[0.670405721]},
|
||||
{"learn":[0.6527789461],"iteration":838,"passed_time":26.79137369,"remaining_time":37.07364106,"test":[0.6703983189]},
|
||||
{"learn":[0.6527432001],"iteration":839,"passed_time":26.82295602,"remaining_time":37.04122498,"test":[0.6704035256]},
|
||||
{"learn":[0.6527139767],"iteration":840,"passed_time":26.87217031,"remaining_time":37.03310985,"test":[0.6704047613]},
|
||||
{"learn":[0.6526857244],"iteration":841,"passed_time":26.92488006,"remaining_time":37.0297044,"test":[0.6704139617]},
|
||||
{"learn":[0.652657086],"iteration":842,"passed_time":26.98258041,"remaining_time":37.03303147,"test":[0.6704066193]},
|
||||
{"learn":[0.6526355016],"iteration":843,"passed_time":27.05424841,"remaining_time":37.05534497,"test":[0.670402892]},
|
||||
{"learn":[0.6526054936],"iteration":844,"passed_time":27.09765154,"remaining_time":37.03880181,"test":[0.6704081961]},
|
||||
{"learn":[0.6525793707],"iteration":845,"passed_time":27.12038959,"remaining_time":36.99400661,"test":[0.6704029862]},
|
||||
{"learn":[0.6525584692],"iteration":846,"passed_time":27.14691224,"remaining_time":36.95441537,"test":[0.6704014281]},
|
||||
{"learn":[0.6525279747],"iteration":847,"passed_time":27.18096334,"remaining_time":36.92508227,"test":[0.6704036115]},
|
||||
{"learn":[0.6525038765],"iteration":848,"passed_time":27.20686017,"remaining_time":36.88468322,"test":[0.6704016777]},
|
||||
{"learn":[0.6524849104],"iteration":849,"passed_time":27.23465701,"remaining_time":36.8468889,"test":[0.6704085392]},
|
||||
{"learn":[0.6524610603],"iteration":850,"passed_time":27.26094834,"remaining_time":36.80708536,"test":[0.6704042952]},
|
||||
{"learn":[0.6524357337],"iteration":851,"passed_time":27.28945577,"remaining_time":36.77029957,"test":[0.670394789]},
|
||||
{"learn":[0.6524082286],"iteration":852,"passed_time":27.31865398,"remaining_time":36.73446203,"test":[0.6703885644]},
|
||||
{"learn":[0.65238051],"iteration":853,"passed_time":27.34791322,"remaining_time":36.69872195,"test":[0.6703946813]},
|
||||
{"learn":[0.6523557826],"iteration":854,"passed_time":27.3865535,"remaining_time":36.67555995,"test":[0.6704042137]},
|
||||
{"learn":[0.6523391233],"iteration":855,"passed_time":27.41370907,"remaining_time":36.63701306,"test":[0.6704077517]},
|
||||
{"learn":[0.652325347],"iteration":856,"passed_time":27.43905921,"remaining_time":36.5960848,"test":[0.6704118698]},
|
||||
{"learn":[0.6522924958],"iteration":857,"passed_time":27.47159295,"remaining_time":36.56475425,"test":[0.6704114259]},
|
||||
{"learn":[0.6522623584],"iteration":858,"passed_time":27.50124299,"remaining_time":36.52959052,"test":[0.6704157567]},
|
||||
{"learn":[0.6522343891],"iteration":859,"passed_time":27.53509105,"remaining_time":36.50000442,"test":[0.6703837005]},
|
||||
{"learn":[0.6522094424],"iteration":860,"passed_time":27.57211091,"remaining_time":36.47460432,"test":[0.6703829482]},
|
||||
{"learn":[0.6521841478],"iteration":861,"passed_time":27.59555719,"remaining_time":36.43125764,"test":[0.6703818491]},
|
||||
{"learn":[0.6521657946],"iteration":862,"passed_time":27.6272049,"remaining_time":36.39876242,"test":[0.6703826129]},
|
||||
{"learn":[0.6521304278],"iteration":863,"passed_time":27.65462267,"remaining_time":36.36070759,"test":[0.6703834487]},
|
||||
{"learn":[0.6521045712],"iteration":864,"passed_time":27.68321566,"remaining_time":36.3242194,"test":[0.6703868275]},
|
||||
{"learn":[0.6520753696],"iteration":865,"passed_time":27.71151671,"remaining_time":36.28736714,"test":[0.6703853357]},
|
||||
{"learn":[0.6520519528],"iteration":866,"passed_time":27.73884016,"remaining_time":36.2492571,"test":[0.670450644]},
|
||||
{"learn":[0.6520216555],"iteration":867,"passed_time":27.76583897,"remaining_time":36.21074851,"test":[0.6704556991]},
|
||||
{"learn":[0.6519926935],"iteration":868,"passed_time":27.79498714,"remaining_time":36.17506382,"test":[0.6704535742]},
|
||||
{"learn":[0.6519734186],"iteration":869,"passed_time":27.82082723,"remaining_time":36.13509744,"test":[0.6704495915]}
|
||||
]}
|
||||
Binary file not shown.
@@ -0,0 +1,871 @@
|
||||
iter Logloss
|
||||
0 0.692389481
|
||||
1 0.6916338586
|
||||
2 0.6910159214
|
||||
3 0.6903417151
|
||||
4 0.6896961461
|
||||
5 0.6890979366
|
||||
6 0.6884946167
|
||||
7 0.6879503686
|
||||
8 0.6874528094
|
||||
9 0.6869036785
|
||||
10 0.6863761921
|
||||
11 0.6859038678
|
||||
12 0.685410175
|
||||
13 0.6849483392
|
||||
14 0.6845417792
|
||||
15 0.6841038875
|
||||
16 0.6836957422
|
||||
17 0.6832947461
|
||||
18 0.6829014105
|
||||
19 0.6825264546
|
||||
20 0.6822106577
|
||||
21 0.6818649349
|
||||
22 0.6815467855
|
||||
23 0.6812293319
|
||||
24 0.6808837443
|
||||
25 0.6805816494
|
||||
26 0.6803209634
|
||||
27 0.6800350862
|
||||
28 0.6797703947
|
||||
29 0.6794926675
|
||||
30 0.6792251865
|
||||
31 0.6789670166
|
||||
32 0.678722402
|
||||
33 0.678476935
|
||||
34 0.6782297335
|
||||
35 0.6780226701
|
||||
36 0.6778291026
|
||||
37 0.6776045324
|
||||
38 0.6773969079
|
||||
39 0.6771819602
|
||||
40 0.6769816736
|
||||
41 0.6767984027
|
||||
42 0.6766201184
|
||||
43 0.6764394377
|
||||
44 0.6762698797
|
||||
45 0.6760974263
|
||||
46 0.6759245179
|
||||
47 0.6757673909
|
||||
48 0.6756172628
|
||||
49 0.675474531
|
||||
50 0.6753286933
|
||||
51 0.6751900513
|
||||
52 0.6750574835
|
||||
53 0.6749329567
|
||||
54 0.6748033265
|
||||
55 0.6746797823
|
||||
56 0.674535525
|
||||
57 0.6744256514
|
||||
58 0.674310819
|
||||
59 0.6741967947
|
||||
60 0.6740879654
|
||||
61 0.6739772476
|
||||
62 0.67388281
|
||||
63 0.6737789726
|
||||
64 0.6736812332
|
||||
65 0.6735930009
|
||||
66 0.6734947116
|
||||
67 0.6733961481
|
||||
68 0.6732990195
|
||||
69 0.6732133575
|
||||
70 0.673111539
|
||||
71 0.6730080451
|
||||
72 0.6729157861
|
||||
73 0.6728347949
|
||||
74 0.6727640693
|
||||
75 0.6726808811
|
||||
76 0.6726029645
|
||||
77 0.6725356026
|
||||
78 0.6724606887
|
||||
79 0.6723849561
|
||||
80 0.6723050519
|
||||
81 0.6722508802
|
||||
82 0.6721773904
|
||||
83 0.6721007598
|
||||
84 0.6720353564
|
||||
85 0.6719790902
|
||||
86 0.6719140024
|
||||
87 0.6718573633
|
||||
88 0.671795602
|
||||
89 0.6717369134
|
||||
90 0.6716711079
|
||||
91 0.6716070843
|
||||
92 0.6715517232
|
||||
93 0.6714957378
|
||||
94 0.6714364567
|
||||
95 0.6713881758
|
||||
96 0.6713336502
|
||||
97 0.6712700267
|
||||
98 0.6712154424
|
||||
99 0.6711600413
|
||||
100 0.6711060533
|
||||
101 0.6710494943
|
||||
102 0.6709936897
|
||||
103 0.6709472183
|
||||
104 0.6708914508
|
||||
105 0.6708388195
|
||||
106 0.6707885854
|
||||
107 0.6707454167
|
||||
108 0.6706973013
|
||||
109 0.6706577031
|
||||
110 0.67061108
|
||||
111 0.6705625485
|
||||
112 0.6705146484
|
||||
113 0.6704704423
|
||||
114 0.6704155922
|
||||
115 0.6703687117
|
||||
116 0.6703324232
|
||||
117 0.6702884624
|
||||
118 0.670253478
|
||||
119 0.6702140804
|
||||
120 0.6701682529
|
||||
121 0.6701320588
|
||||
122 0.6700939824
|
||||
123 0.6700655902
|
||||
124 0.6700190743
|
||||
125 0.6699792296
|
||||
126 0.6699379404
|
||||
127 0.669895454
|
||||
128 0.6698563938
|
||||
129 0.6698215571
|
||||
130 0.6697857067
|
||||
131 0.6697449303
|
||||
132 0.6697052425
|
||||
133 0.6696695553
|
||||
134 0.6696269265
|
||||
135 0.6695969271
|
||||
136 0.6695489786
|
||||
137 0.6695173859
|
||||
138 0.6694811164
|
||||
139 0.6694477439
|
||||
140 0.6694082161
|
||||
141 0.6693679185
|
||||
142 0.6693341916
|
||||
143 0.6692933159
|
||||
144 0.6692619696
|
||||
145 0.6692229289
|
||||
146 0.6691840164
|
||||
147 0.6691581406
|
||||
148 0.6691177196
|
||||
149 0.6690851126
|
||||
150 0.6690518144
|
||||
151 0.6690149711
|
||||
152 0.668993877
|
||||
153 0.6689596579
|
||||
154 0.6689372651
|
||||
155 0.6689003045
|
||||
156 0.6688680182
|
||||
157 0.6688348164
|
||||
158 0.6687947046
|
||||
159 0.6687605251
|
||||
160 0.668726253
|
||||
161 0.6686862718
|
||||
162 0.668663478
|
||||
163 0.6686399521
|
||||
164 0.6686058279
|
||||
165 0.6685761282
|
||||
166 0.6685469327
|
||||
167 0.6685157003
|
||||
168 0.6684805143
|
||||
169 0.6684485765
|
||||
170 0.6684144429
|
||||
171 0.6683849752
|
||||
172 0.6683568537
|
||||
173 0.6683266628
|
||||
174 0.6682937842
|
||||
175 0.6682657097
|
||||
176 0.6682301443
|
||||
177 0.6681995916
|
||||
178 0.6681658267
|
||||
179 0.6681422687
|
||||
180 0.6681216601
|
||||
181 0.6680899019
|
||||
182 0.6680676394
|
||||
183 0.6680413672
|
||||
184 0.6680088406
|
||||
185 0.6679873982
|
||||
186 0.6679663544
|
||||
187 0.6679417375
|
||||
188 0.6679100197
|
||||
189 0.667881208
|
||||
190 0.6678475427
|
||||
191 0.6678310341
|
||||
192 0.6678060257
|
||||
193 0.6677789336
|
||||
194 0.6677478773
|
||||
195 0.6677212408
|
||||
196 0.667704316
|
||||
197 0.6676819639
|
||||
198 0.6676554448
|
||||
199 0.6676318346
|
||||
200 0.6676074705
|
||||
201 0.6675849784
|
||||
202 0.6675631744
|
||||
203 0.6675397619
|
||||
204 0.6675169086
|
||||
205 0.6674864762
|
||||
206 0.6674670714
|
||||
207 0.6674375599
|
||||
208 0.6674148457
|
||||
209 0.6673974446
|
||||
210 0.6673812139
|
||||
211 0.6673515687
|
||||
212 0.6673197956
|
||||
213 0.6672900754
|
||||
214 0.6672550009
|
||||
215 0.6672271563
|
||||
216 0.667204521
|
||||
217 0.667181968
|
||||
218 0.6671640023
|
||||
219 0.66714351
|
||||
220 0.6671167156
|
||||
221 0.6670915937
|
||||
222 0.6670595279
|
||||
223 0.667033994
|
||||
224 0.6670008246
|
||||
225 0.6669858319
|
||||
226 0.6669553964
|
||||
227 0.6669274683
|
||||
228 0.666896348
|
||||
229 0.6668698686
|
||||
230 0.6668513411
|
||||
231 0.6668309985
|
||||
232 0.6668058585
|
||||
233 0.6667845908
|
||||
234 0.6667582863
|
||||
235 0.6667332943
|
||||
236 0.6667070085
|
||||
237 0.6666907315
|
||||
238 0.6666633028
|
||||
239 0.6666406707
|
||||
240 0.6666134624
|
||||
241 0.6665850522
|
||||
242 0.6665631193
|
||||
243 0.6665412643
|
||||
244 0.6665168385
|
||||
245 0.6664904845
|
||||
246 0.6664678274
|
||||
247 0.6664539777
|
||||
248 0.6664334121
|
||||
249 0.6664121724
|
||||
250 0.666392034
|
||||
251 0.666366899
|
||||
252 0.6663414098
|
||||
253 0.6663157816
|
||||
254 0.6662989799
|
||||
255 0.6662696102
|
||||
256 0.6662479711
|
||||
257 0.6662231874
|
||||
258 0.6661947927
|
||||
259 0.6661669951
|
||||
260 0.6661426137
|
||||
261 0.6661216749
|
||||
262 0.6660983123
|
||||
263 0.6660803402
|
||||
264 0.6660617842
|
||||
265 0.6660443878
|
||||
266 0.6660176079
|
||||
267 0.6659967546
|
||||
268 0.6659751467
|
||||
269 0.6659539329
|
||||
270 0.6659263951
|
||||
271 0.6659038921
|
||||
272 0.6658767418
|
||||
273 0.6658510507
|
||||
274 0.6658210119
|
||||
275 0.6657963011
|
||||
276 0.6657748552
|
||||
277 0.6657490013
|
||||
278 0.665732402
|
||||
279 0.6657118786
|
||||
280 0.665684467
|
||||
281 0.6656584634
|
||||
282 0.6656309991
|
||||
283 0.6656073482
|
||||
284 0.6655890957
|
||||
285 0.6655665563
|
||||
286 0.6655452454
|
||||
287 0.6655255286
|
||||
288 0.6655053548
|
||||
289 0.6654893396
|
||||
290 0.6654648912
|
||||
291 0.6654442759
|
||||
292 0.6654173127
|
||||
293 0.6653914518
|
||||
294 0.6653648946
|
||||
295 0.665344141
|
||||
296 0.6653140817
|
||||
297 0.665295365
|
||||
298 0.6652787488
|
||||
299 0.6652502991
|
||||
300 0.665231168
|
||||
301 0.6652136682
|
||||
302 0.6651903001
|
||||
303 0.6651697153
|
||||
304 0.6651525958
|
||||
305 0.6651322685
|
||||
306 0.6651113828
|
||||
307 0.6650886807
|
||||
308 0.6650622251
|
||||
309 0.6650429987
|
||||
310 0.665015513
|
||||
311 0.6650019022
|
||||
312 0.664979951
|
||||
313 0.6649549638
|
||||
314 0.6649340455
|
||||
315 0.6649162445
|
||||
316 0.6649048119
|
||||
317 0.6648796463
|
||||
318 0.6648605481
|
||||
319 0.6648429084
|
||||
320 0.6648238121
|
||||
321 0.6647969527
|
||||
322 0.6647854723
|
||||
323 0.6647589304
|
||||
324 0.6647429024
|
||||
325 0.6647237508
|
||||
326 0.6647059396
|
||||
327 0.664686288
|
||||
328 0.6646532527
|
||||
329 0.6646306438
|
||||
330 0.6646098516
|
||||
331 0.6645858284
|
||||
332 0.6645707188
|
||||
333 0.6645485788
|
||||
334 0.6645305696
|
||||
335 0.6645108881
|
||||
336 0.6644923286
|
||||
337 0.6644805222
|
||||
338 0.6644572776
|
||||
339 0.6644320741
|
||||
340 0.6644115048
|
||||
341 0.6643949013
|
||||
342 0.6643619789
|
||||
343 0.6643389502
|
||||
344 0.6643088915
|
||||
345 0.664286972
|
||||
346 0.664274149
|
||||
347 0.6642536926
|
||||
348 0.6642357634
|
||||
349 0.664207914
|
||||
350 0.6641853097
|
||||
351 0.6641654917
|
||||
352 0.664143804
|
||||
353 0.6641290647
|
||||
354 0.6641117244
|
||||
355 0.6640880219
|
||||
356 0.6640669415
|
||||
357 0.6640462999
|
||||
358 0.664030296
|
||||
359 0.6640028542
|
||||
360 0.6639813347
|
||||
361 0.6639597941
|
||||
362 0.6639429832
|
||||
363 0.6639222708
|
||||
364 0.6639065546
|
||||
365 0.6638823236
|
||||
366 0.6638648195
|
||||
367 0.6638436235
|
||||
368 0.6638208732
|
||||
369 0.6637956357
|
||||
370 0.6637718453
|
||||
371 0.663756918
|
||||
372 0.6637353525
|
||||
373 0.6637143112
|
||||
374 0.6636956547
|
||||
375 0.663680995
|
||||
376 0.66366728
|
||||
377 0.6636487567
|
||||
378 0.6636266904
|
||||
379 0.6636116064
|
||||
380 0.6635902746
|
||||
381 0.6635654896
|
||||
382 0.6635393029
|
||||
383 0.6635171734
|
||||
384 0.663500789
|
||||
385 0.663477743
|
||||
386 0.6634584806
|
||||
387 0.6634337499
|
||||
388 0.6634135584
|
||||
389 0.6633868455
|
||||
390 0.6633755323
|
||||
391 0.663356103
|
||||
392 0.6633337631
|
||||
393 0.663319422
|
||||
394 0.6632911566
|
||||
395 0.6632687875
|
||||
396 0.6632431997
|
||||
397 0.6632189331
|
||||
398 0.663201035
|
||||
399 0.6631898553
|
||||
400 0.6631712482
|
||||
401 0.663143025
|
||||
402 0.663121538
|
||||
403 0.6631087792
|
||||
404 0.6630859067
|
||||
405 0.663066483
|
||||
406 0.6630443652
|
||||
407 0.6630250376
|
||||
408 0.6630007822
|
||||
409 0.6629768728
|
||||
410 0.6629528093
|
||||
411 0.6629260936
|
||||
412 0.6629102182
|
||||
413 0.6628863488
|
||||
414 0.6628648972
|
||||
415 0.6628454339
|
||||
416 0.6628200274
|
||||
417 0.6627942591
|
||||
418 0.6627744647
|
||||
419 0.662765485
|
||||
420 0.6627503257
|
||||
421 0.6627323029
|
||||
422 0.6627111509
|
||||
423 0.6626785863
|
||||
424 0.6626576561
|
||||
425 0.6626363113
|
||||
426 0.6626181065
|
||||
427 0.66259794
|
||||
428 0.6625765658
|
||||
429 0.6625526572
|
||||
430 0.66253135
|
||||
431 0.6625035695
|
||||
432 0.662480212
|
||||
433 0.6624611632
|
||||
434 0.6624332625
|
||||
435 0.6624120584
|
||||
436 0.6623941719
|
||||
437 0.6623766304
|
||||
438 0.6623623329
|
||||
439 0.6623442925
|
||||
440 0.6623212715
|
||||
441 0.6623025941
|
||||
442 0.6622749791
|
||||
443 0.6622534499
|
||||
444 0.6622305473
|
||||
445 0.6622059333
|
||||
446 0.6621871707
|
||||
447 0.6621638454
|
||||
448 0.6621511296
|
||||
449 0.6621349978
|
||||
450 0.6621120424
|
||||
451 0.6620958271
|
||||
452 0.6620793528
|
||||
453 0.6620572713
|
||||
454 0.6620395025
|
||||
455 0.6620188044
|
||||
456 0.6620017347
|
||||
457 0.6619811454
|
||||
458 0.6619695569
|
||||
459 0.661952377
|
||||
460 0.6619237442
|
||||
461 0.6619089407
|
||||
462 0.6618886168
|
||||
463 0.6618831383
|
||||
464 0.6618690774
|
||||
465 0.661845878
|
||||
466 0.6618290213
|
||||
467 0.6618050064
|
||||
468 0.6617832833
|
||||
469 0.6617652311
|
||||
470 0.6617443144
|
||||
471 0.6617202619
|
||||
472 0.6617005831
|
||||
473 0.6616824419
|
||||
474 0.6616538226
|
||||
475 0.6616314155
|
||||
476 0.6616127861
|
||||
477 0.6616029072
|
||||
478 0.6615843751
|
||||
479 0.661563216
|
||||
480 0.6615432257
|
||||
481 0.6615263324
|
||||
482 0.6615033259
|
||||
483 0.661484293
|
||||
484 0.6614678231
|
||||
485 0.6614463024
|
||||
486 0.6614155436
|
||||
487 0.6613958945
|
||||
488 0.661380611
|
||||
489 0.6613677802
|
||||
490 0.6613530086
|
||||
491 0.6613248211
|
||||
492 0.6613059359
|
||||
493 0.6612729965
|
||||
494 0.6612624948
|
||||
495 0.6612401679
|
||||
496 0.6612191637
|
||||
497 0.6611912219
|
||||
498 0.6611773017
|
||||
499 0.6611638216
|
||||
500 0.6611450533
|
||||
501 0.6611179111
|
||||
502 0.6610959069
|
||||
503 0.6610728788
|
||||
504 0.6610436668
|
||||
505 0.6610188976
|
||||
506 0.6610030555
|
||||
507 0.6609831174
|
||||
508 0.6609586562
|
||||
509 0.660935882
|
||||
510 0.6609202024
|
||||
511 0.6609011137
|
||||
512 0.6608726737
|
||||
513 0.6608608849
|
||||
514 0.6608387256
|
||||
515 0.6608136063
|
||||
516 0.6607946343
|
||||
517 0.6607703935
|
||||
518 0.6607509625
|
||||
519 0.6607238109
|
||||
520 0.6606999858
|
||||
521 0.6606813873
|
||||
522 0.6606610372
|
||||
523 0.660638456
|
||||
524 0.6606156483
|
||||
525 0.6605968623
|
||||
526 0.6605735776
|
||||
527 0.6605517294
|
||||
528 0.6605309239
|
||||
529 0.6605086434
|
||||
530 0.6604803349
|
||||
531 0.6604566326
|
||||
532 0.6604430839
|
||||
533 0.6604273738
|
||||
534 0.6604048016
|
||||
535 0.6603845173
|
||||
536 0.6603669212
|
||||
537 0.6603488983
|
||||
538 0.6603176881
|
||||
539 0.6602953862
|
||||
540 0.6602672025
|
||||
541 0.6602568636
|
||||
542 0.660235705
|
||||
543 0.6602152295
|
||||
544 0.6601897709
|
||||
545 0.6601683731
|
||||
546 0.6601472267
|
||||
547 0.6601262337
|
||||
548 0.6601119991
|
||||
549 0.6600869973
|
||||
550 0.6600667497
|
||||
551 0.6600397508
|
||||
552 0.660016863
|
||||
553 0.6599933158
|
||||
554 0.6599632649
|
||||
555 0.6599446007
|
||||
556 0.6599138126
|
||||
557 0.6598965504
|
||||
558 0.6598785723
|
||||
559 0.659860838
|
||||
560 0.6598408724
|
||||
561 0.6598244857
|
||||
562 0.6598082469
|
||||
563 0.6597851673
|
||||
564 0.6597683521
|
||||
565 0.6597479006
|
||||
566 0.6597310938
|
||||
567 0.6597096581
|
||||
568 0.6596862311
|
||||
569 0.6596574779
|
||||
570 0.6596385418
|
||||
571 0.6596189903
|
||||
572 0.65959275
|
||||
573 0.6595730662
|
||||
574 0.6595566809
|
||||
575 0.6595365076
|
||||
576 0.6595163446
|
||||
577 0.6594816637
|
||||
578 0.6594570142
|
||||
579 0.6594353055
|
||||
580 0.6594162362
|
||||
581 0.659395036
|
||||
582 0.6593798831
|
||||
583 0.6593556719
|
||||
584 0.6593292627
|
||||
585 0.6592976737
|
||||
586 0.6592754841
|
||||
587 0.6592510441
|
||||
588 0.6592290326
|
||||
589 0.6592097404
|
||||
590 0.6591876204
|
||||
591 0.6591705995
|
||||
592 0.6591456195
|
||||
593 0.6591107122
|
||||
594 0.6590819533
|
||||
595 0.6590551327
|
||||
596 0.6590373916
|
||||
597 0.6590177149
|
||||
598 0.6589946095
|
||||
599 0.6589697628
|
||||
600 0.6589442269
|
||||
601 0.6589182437
|
||||
602 0.6588837179
|
||||
603 0.6588674101
|
||||
604 0.6588406916
|
||||
605 0.6588149945
|
||||
606 0.6587866031
|
||||
607 0.6587636648
|
||||
608 0.6587502469
|
||||
609 0.6587292784
|
||||
610 0.6587104112
|
||||
611 0.6586953782
|
||||
612 0.6586641191
|
||||
613 0.6586450136
|
||||
614 0.6586136263
|
||||
615 0.6585862768
|
||||
616 0.6585585235
|
||||
617 0.6585371631
|
||||
618 0.6585092632
|
||||
619 0.6584914317
|
||||
620 0.6584662432
|
||||
621 0.6584454668
|
||||
622 0.6584249408
|
||||
623 0.6583931228
|
||||
624 0.6583660767
|
||||
625 0.658354264
|
||||
626 0.6583253625
|
||||
627 0.6582968632
|
||||
628 0.6582687399
|
||||
629 0.658242535
|
||||
630 0.6582199874
|
||||
631 0.6581918101
|
||||
632 0.6581735218
|
||||
633 0.6581445869
|
||||
634 0.6581202427
|
||||
635 0.6580977862
|
||||
636 0.6580724179
|
||||
637 0.6580426322
|
||||
638 0.6580111256
|
||||
639 0.6579834747
|
||||
640 0.6579541367
|
||||
641 0.6579254503
|
||||
642 0.657898555
|
||||
643 0.6578676875
|
||||
644 0.6578324163
|
||||
645 0.6578062223
|
||||
646 0.6577760631
|
||||
647 0.6577483474
|
||||
648 0.6577249642
|
||||
649 0.6576974966
|
||||
650 0.657675114
|
||||
651 0.6576447891
|
||||
652 0.6576102356
|
||||
653 0.6575793887
|
||||
654 0.6575543309
|
||||
655 0.6575340787
|
||||
656 0.6575061464
|
||||
657 0.657476113
|
||||
658 0.6574447014
|
||||
659 0.6574247361
|
||||
660 0.6574034983
|
||||
661 0.6573783832
|
||||
662 0.657357694
|
||||
663 0.6573411592
|
||||
664 0.6573118559
|
||||
665 0.6572819076
|
||||
666 0.6572430097
|
||||
667 0.6572160391
|
||||
668 0.6571931413
|
||||
669 0.6571737099
|
||||
670 0.6571532872
|
||||
671 0.6571208939
|
||||
672 0.6570887673
|
||||
673 0.6570633692
|
||||
674 0.6570454361
|
||||
675 0.6570231031
|
||||
676 0.6570052089
|
||||
677 0.6569855794
|
||||
678 0.6569579709
|
||||
679 0.6569333354
|
||||
680 0.6569069617
|
||||
681 0.6568931857
|
||||
682 0.6568734532
|
||||
683 0.6568435196
|
||||
684 0.6568108038
|
||||
685 0.6567811374
|
||||
686 0.6567467284
|
||||
687 0.6567172734
|
||||
688 0.6566967606
|
||||
689 0.6566720128
|
||||
690 0.6566441608
|
||||
691 0.6566172287
|
||||
692 0.6565952549
|
||||
693 0.6565702687
|
||||
694 0.6565392213
|
||||
695 0.6565157938
|
||||
696 0.6564902789
|
||||
697 0.6564644734
|
||||
698 0.6564349549
|
||||
699 0.6564046572
|
||||
700 0.6563744107
|
||||
701 0.6563525063
|
||||
702 0.6563189867
|
||||
703 0.6562939062
|
||||
704 0.6562739297
|
||||
705 0.656256438
|
||||
706 0.6562366475
|
||||
707 0.6562073096
|
||||
708 0.6561864222
|
||||
709 0.6561578826
|
||||
710 0.6561208567
|
||||
711 0.6560924703
|
||||
712 0.6560656907
|
||||
713 0.6560362588
|
||||
714 0.6560124527
|
||||
715 0.6559875055
|
||||
716 0.6559547281
|
||||
717 0.6559230866
|
||||
718 0.6558924823
|
||||
719 0.6558676469
|
||||
720 0.6558459277
|
||||
721 0.6558149638
|
||||
722 0.6557812248
|
||||
723 0.6557546502
|
||||
724 0.6557274948
|
||||
725 0.6557044723
|
||||
726 0.6556751811
|
||||
727 0.6556539158
|
||||
728 0.6556182915
|
||||
729 0.6555977079
|
||||
730 0.6555667903
|
||||
731 0.6555394075
|
||||
732 0.6555122742
|
||||
733 0.6554814941
|
||||
734 0.6554517373
|
||||
735 0.655429552
|
||||
736 0.655396579
|
||||
737 0.6553735864
|
||||
738 0.6553472597
|
||||
739 0.6553252832
|
||||
740 0.6552971659
|
||||
741 0.6552763852
|
||||
742 0.6552488203
|
||||
743 0.65521229
|
||||
744 0.6551949744
|
||||
745 0.6551673797
|
||||
746 0.6551421856
|
||||
747 0.6551255516
|
||||
748 0.6551019608
|
||||
749 0.6550758728
|
||||
750 0.655051966
|
||||
751 0.6550351058
|
||||
752 0.6549998756
|
||||
753 0.6549721212
|
||||
754 0.6549401744
|
||||
755 0.6549207325
|
||||
756 0.6548900891
|
||||
757 0.6548682731
|
||||
758 0.6548418938
|
||||
759 0.6548234717
|
||||
760 0.6547996833
|
||||
761 0.6547726174
|
||||
762 0.6547509314
|
||||
763 0.6547168175
|
||||
764 0.6546907846
|
||||
765 0.6546671611
|
||||
766 0.6546475893
|
||||
767 0.6546206223
|
||||
768 0.6545874193
|
||||
769 0.6545620629
|
||||
770 0.6545346297
|
||||
771 0.6545172316
|
||||
772 0.6544943049
|
||||
773 0.6544632323
|
||||
774 0.6544384097
|
||||
775 0.6544084745
|
||||
776 0.6543765257
|
||||
777 0.6543536123
|
||||
778 0.6543303593
|
||||
779 0.6543005831
|
||||
780 0.6542678123
|
||||
781 0.6542439303
|
||||
782 0.6542100401
|
||||
783 0.6541836178
|
||||
784 0.654158129
|
||||
785 0.6541343464
|
||||
786 0.6541092921
|
||||
787 0.6540812254
|
||||
788 0.654060259
|
||||
789 0.6540467253
|
||||
790 0.6540306837
|
||||
791 0.6540103667
|
||||
792 0.6539821302
|
||||
793 0.6539577914
|
||||
794 0.653923724
|
||||
795 0.6539086888
|
||||
796 0.6538798424
|
||||
797 0.6538566996
|
||||
798 0.6538290752
|
||||
799 0.6538051255
|
||||
800 0.6537917354
|
||||
801 0.6537684302
|
||||
802 0.6537402991
|
||||
803 0.6537165427
|
||||
804 0.6536853601
|
||||
805 0.6536681479
|
||||
806 0.6536409101
|
||||
807 0.6536120189
|
||||
808 0.6535912493
|
||||
809 0.6535617421
|
||||
810 0.6535315174
|
||||
811 0.6534972927
|
||||
812 0.6534818476
|
||||
813 0.6534498323
|
||||
814 0.6534305025
|
||||
815 0.6534081059
|
||||
816 0.6533765804
|
||||
817 0.6533441549
|
||||
818 0.6533053405
|
||||
819 0.6532838469
|
||||
820 0.6532604302
|
||||
821 0.6532364412
|
||||
822 0.6532100089
|
||||
823 0.6531782515
|
||||
824 0.6531449701
|
||||
825 0.653115452
|
||||
826 0.6530787602
|
||||
827 0.653052397
|
||||
828 0.6530313579
|
||||
829 0.6530010363
|
||||
830 0.6529752146
|
||||
831 0.652954801
|
||||
832 0.6529330351
|
||||
833 0.6528993709
|
||||
834 0.6528665883
|
||||
835 0.6528413041
|
||||
836 0.6528217161
|
||||
837 0.6527978782
|
||||
838 0.6527789461
|
||||
839 0.6527432001
|
||||
840 0.6527139767
|
||||
841 0.6526857244
|
||||
842 0.652657086
|
||||
843 0.6526355016
|
||||
844 0.6526054936
|
||||
845 0.6525793707
|
||||
846 0.6525584692
|
||||
847 0.6525279747
|
||||
848 0.6525038765
|
||||
849 0.6524849104
|
||||
850 0.6524610603
|
||||
851 0.6524357337
|
||||
852 0.6524082286
|
||||
853 0.65238051
|
||||
854 0.6523557826
|
||||
855 0.6523391233
|
||||
856 0.652325347
|
||||
857 0.6522924958
|
||||
858 0.6522623584
|
||||
859 0.6522343891
|
||||
860 0.6522094424
|
||||
861 0.6521841478
|
||||
862 0.6521657946
|
||||
863 0.6521304278
|
||||
864 0.6521045712
|
||||
865 0.6520753696
|
||||
866 0.6520519528
|
||||
867 0.6520216555
|
||||
868 0.6519926935
|
||||
869 0.6519734186
|
||||
|
@@ -0,0 +1,871 @@
|
||||
iter Passed Remaining
|
||||
0 46 93548
|
||||
1 83 83419
|
||||
2 132 88415
|
||||
3 162 81250
|
||||
4 196 78573
|
||||
5 230 76747
|
||||
6 269 76701
|
||||
7 319 79674
|
||||
8 364 80653
|
||||
9 411 81918
|
||||
10 456 82497
|
||||
11 491 81432
|
||||
12 522 79809
|
||||
13 555 78774
|
||||
14 595 78777
|
||||
15 630 78123
|
||||
16 662 77290
|
||||
17 700 77124
|
||||
18 730 76120
|
||||
19 764 75651
|
||||
20 804 75774
|
||||
21 835 75128
|
||||
22 886 76169
|
||||
23 920 75764
|
||||
24 960 75853
|
||||
25 989 75130
|
||||
26 1025 74941
|
||||
27 1060 74714
|
||||
28 1104 75079
|
||||
29 1141 74976
|
||||
30 1180 74975
|
||||
31 1213 74640
|
||||
32 1245 74260
|
||||
33 1287 74434
|
||||
34 1327 74528
|
||||
35 1376 75071
|
||||
36 1427 75741
|
||||
37 1468 75804
|
||||
38 1508 75857
|
||||
39 1549 75922
|
||||
40 1586 75781
|
||||
41 1621 75590
|
||||
42 1663 75705
|
||||
43 1701 75621
|
||||
44 1739 75591
|
||||
45 1776 75460
|
||||
46 1819 75616
|
||||
47 1869 76025
|
||||
48 1916 76288
|
||||
49 1953 76191
|
||||
50 1993 76197
|
||||
51 2038 76381
|
||||
52 2080 76420
|
||||
53 2158 77788
|
||||
54 2220 78529
|
||||
55 2286 79390
|
||||
56 2328 79372
|
||||
57 2367 79254
|
||||
58 2409 79257
|
||||
59 2444 79049
|
||||
60 2484 78985
|
||||
61 2521 78820
|
||||
62 2554 78528
|
||||
63 2593 78466
|
||||
64 2623 78111
|
||||
65 2660 77969
|
||||
66 2695 77776
|
||||
67 2725 77446
|
||||
68 2761 77291
|
||||
69 2791 76975
|
||||
70 2824 76739
|
||||
71 2861 76611
|
||||
72 2897 76476
|
||||
73 2935 76408
|
||||
74 3040 78027
|
||||
75 3097 78411
|
||||
76 3152 78741
|
||||
77 3216 79248
|
||||
78 3256 79195
|
||||
79 3305 79336
|
||||
80 3348 79320
|
||||
81 3381 79089
|
||||
82 3416 78911
|
||||
83 3480 79399
|
||||
84 3535 79649
|
||||
85 3581 79716
|
||||
86 3612 79428
|
||||
87 3644 79185
|
||||
88 3678 78975
|
||||
89 3712 78785
|
||||
90 3743 78531
|
||||
91 3775 78297
|
||||
92 3806 78047
|
||||
93 3837 77821
|
||||
94 3871 77629
|
||||
95 3913 77618
|
||||
96 3945 77403
|
||||
97 3989 77433
|
||||
98 4020 77204
|
||||
99 4053 77020
|
||||
100 4084 76789
|
||||
101 4116 76597
|
||||
102 4148 76401
|
||||
103 4176 76141
|
||||
104 4202 75845
|
||||
105 4232 75634
|
||||
106 4261 75390
|
||||
107 4290 75168
|
||||
108 4324 75018
|
||||
109 4351 74766
|
||||
110 4386 74648
|
||||
111 4424 74577
|
||||
112 4458 74455
|
||||
113 4497 74400
|
||||
114 4533 74307
|
||||
115 4564 74136
|
||||
116 4596 73981
|
||||
117 4628 73818
|
||||
118 4668 73786
|
||||
119 4692 73509
|
||||
120 4723 73354
|
||||
121 4756 73220
|
||||
122 4788 73065
|
||||
123 4815 72854
|
||||
124 4843 72647
|
||||
125 4875 72514
|
||||
126 4916 72515
|
||||
127 4952 72436
|
||||
128 4991 72397
|
||||
129 5028 72327
|
||||
130 5059 72180
|
||||
131 5096 72116
|
||||
132 5125 71946
|
||||
133 5156 71804
|
||||
134 5190 71704
|
||||
135 5221 71564
|
||||
136 5251 71407
|
||||
137 5274 71165
|
||||
138 5309 71084
|
||||
139 5344 71008
|
||||
140 5377 70902
|
||||
141 5416 70866
|
||||
142 5452 70803
|
||||
143 5490 70760
|
||||
144 5521 70641
|
||||
145 5553 70522
|
||||
146 5582 70365
|
||||
147 5611 70217
|
||||
148 5636 70026
|
||||
149 5673 69975
|
||||
150 5706 69874
|
||||
151 5738 69764
|
||||
152 5765 69605
|
||||
153 5795 69471
|
||||
154 5817 69246
|
||||
155 5853 69191
|
||||
156 5888 69122
|
||||
157 5924 69070
|
||||
158 5964 69061
|
||||
159 5996 68963
|
||||
160 6022 68789
|
||||
161 6050 68650
|
||||
162 6079 68510
|
||||
163 6108 68385
|
||||
164 6140 68292
|
||||
165 6169 68162
|
||||
166 6202 68074
|
||||
167 6231 67953
|
||||
168 6263 67858
|
||||
169 6295 67764
|
||||
170 6325 67656
|
||||
171 6356 67561
|
||||
172 6395 67545
|
||||
173 6437 67554
|
||||
174 6472 67495
|
||||
175 6503 67395
|
||||
176 6533 67291
|
||||
177 6562 67174
|
||||
178 6590 67049
|
||||
179 6624 66982
|
||||
180 6655 66882
|
||||
181 6687 66804
|
||||
182 6718 66703
|
||||
183 6751 66632
|
||||
184 6784 66559
|
||||
185 6810 66424
|
||||
186 6832 66246
|
||||
187 6867 66187
|
||||
188 6918 66294
|
||||
189 6969 66393
|
||||
190 7018 66470
|
||||
191 7074 66614
|
||||
192 7117 66635
|
||||
193 7191 66943
|
||||
194 7242 67036
|
||||
195 7282 67027
|
||||
196 7317 66967
|
||||
197 7351 66903
|
||||
198 7389 66879
|
||||
199 7432 66896
|
||||
200 7471 66869
|
||||
201 7506 66814
|
||||
202 7540 66752
|
||||
203 7568 66628
|
||||
204 7605 66596
|
||||
205 7638 66519
|
||||
206 7665 66397
|
||||
207 7700 66340
|
||||
208 7734 66276
|
||||
209 7766 66197
|
||||
210 7796 66106
|
||||
211 7831 66053
|
||||
212 7871 66037
|
||||
213 7910 66016
|
||||
214 7951 66014
|
||||
215 7989 65983
|
||||
216 8025 65946
|
||||
217 8058 65872
|
||||
218 8087 65768
|
||||
219 8112 65638
|
||||
220 8148 65594
|
||||
221 8197 65655
|
||||
222 8239 65655
|
||||
223 8268 65556
|
||||
224 8298 65466
|
||||
225 8327 65366
|
||||
226 8357 65278
|
||||
227 8384 65167
|
||||
228 8418 65103
|
||||
229 8453 65058
|
||||
230 8490 65020
|
||||
231 8523 64958
|
||||
232 8550 64848
|
||||
233 8575 64718
|
||||
234 8607 64648
|
||||
235 8635 64545
|
||||
236 8660 64426
|
||||
237 8691 64345
|
||||
238 8719 64250
|
||||
239 8746 64137
|
||||
240 8773 64038
|
||||
241 8803 63951
|
||||
242 8833 63873
|
||||
243 8862 63779
|
||||
244 8892 63698
|
||||
245 8932 63688
|
||||
246 8962 63611
|
||||
247 8991 63521
|
||||
248 9021 63442
|
||||
249 9051 63358
|
||||
250 9085 63306
|
||||
251 9110 63193
|
||||
252 9137 63093
|
||||
253 9174 63066
|
||||
254 9196 62935
|
||||
255 9238 62934
|
||||
256 9267 62855
|
||||
257 9297 62776
|
||||
258 9324 62681
|
||||
259 9357 62625
|
||||
260 9388 62552
|
||||
261 9427 62536
|
||||
262 9461 62491
|
||||
263 9496 62443
|
||||
264 9524 62356
|
||||
265 9553 62278
|
||||
266 9590 62247
|
||||
267 9620 62172
|
||||
268 9645 62071
|
||||
269 9682 62040
|
||||
270 9711 61962
|
||||
271 9739 61872
|
||||
272 9768 61797
|
||||
273 9804 61761
|
||||
274 9848 61777
|
||||
275 9886 61755
|
||||
276 9925 61740
|
||||
277 9965 61728
|
||||
278 9995 61656
|
||||
279 10022 61564
|
||||
280 10055 61516
|
||||
281 10080 61410
|
||||
282 10111 61344
|
||||
283 10147 61311
|
||||
284 10175 61230
|
||||
285 10202 61141
|
||||
286 10234 61084
|
||||
287 10264 61018
|
||||
288 10299 60977
|
||||
289 10323 60874
|
||||
290 10353 60804
|
||||
291 10394 60803
|
||||
292 10431 60773
|
||||
293 10471 60763
|
||||
294 10503 60707
|
||||
295 10534 60645
|
||||
296 10576 60646
|
||||
297 10612 60612
|
||||
298 10639 60525
|
||||
299 10668 60453
|
||||
300 10702 60411
|
||||
301 10729 60326
|
||||
302 10764 60290
|
||||
303 10801 60263
|
||||
304 10829 60182
|
||||
305 10857 60108
|
||||
306 10892 60067
|
||||
307 10930 60047
|
||||
308 10972 60045
|
||||
309 11002 59983
|
||||
310 11030 59902
|
||||
311 11058 59828
|
||||
312 11092 59788
|
||||
313 11117 59696
|
||||
314 11149 59641
|
||||
315 11187 59617
|
||||
316 11211 59525
|
||||
317 11243 59468
|
||||
318 11274 59413
|
||||
319 11304 59346
|
||||
320 11334 59287
|
||||
321 11362 59209
|
||||
322 11394 59158
|
||||
323 11436 59158
|
||||
324 11477 59153
|
||||
325 11513 59122
|
||||
326 11547 59081
|
||||
327 11572 58991
|
||||
328 11607 58956
|
||||
329 11637 58894
|
||||
330 11668 58833
|
||||
331 11700 58785
|
||||
332 11724 58694
|
||||
333 11757 58648
|
||||
334 11780 58550
|
||||
335 11815 58515
|
||||
336 11844 58451
|
||||
337 11869 58364
|
||||
338 11905 58335
|
||||
339 11941 58302
|
||||
340 11986 58315
|
||||
341 12020 58274
|
||||
342 12066 58292
|
||||
343 12122 58358
|
||||
344 12177 58415
|
||||
345 12221 58422
|
||||
346 12264 58423
|
||||
347 12300 58394
|
||||
348 12324 58304
|
||||
349 12354 58243
|
||||
350 12401 58262
|
||||
351 12438 58232
|
||||
352 12479 58228
|
||||
353 12512 58179
|
||||
354 12541 58116
|
||||
355 12569 58044
|
||||
356 12597 57977
|
||||
357 12628 57920
|
||||
358 12653 57839
|
||||
359 12682 57775
|
||||
360 12720 57752
|
||||
361 12744 57666
|
||||
362 12770 57592
|
||||
363 12811 57583
|
||||
364 12841 57522
|
||||
365 12870 57460
|
||||
366 12897 57386
|
||||
367 12938 57378
|
||||
368 12974 57347
|
||||
369 13009 57313
|
||||
370 13038 57249
|
||||
371 13078 57235
|
||||
372 13117 57216
|
||||
373 13147 57159
|
||||
374 13181 57118
|
||||
375 13205 57036
|
||||
376 13235 56979
|
||||
377 13274 56960
|
||||
378 13306 56911
|
||||
379 13333 56841
|
||||
380 13366 56798
|
||||
381 13396 56741
|
||||
382 13421 56666
|
||||
383 13467 56674
|
||||
384 13508 56664
|
||||
385 13540 56616
|
||||
386 13569 56559
|
||||
387 13598 56496
|
||||
388 13627 56438
|
||||
389 13656 56376
|
||||
390 13685 56317
|
||||
391 13717 56271
|
||||
392 13750 56227
|
||||
393 13771 56135
|
||||
394 13804 56090
|
||||
395 13825 55999
|
||||
396 13858 55957
|
||||
397 13888 55904
|
||||
398 13917 55843
|
||||
399 13953 55812
|
||||
400 13994 55802
|
||||
401 14025 55752
|
||||
402 14048 55670
|
||||
403 14076 55607
|
||||
404 14105 55551
|
||||
405 14142 55526
|
||||
406 14182 55511
|
||||
407 14214 55464
|
||||
408 14240 55394
|
||||
409 14267 55328
|
||||
410 14299 55284
|
||||
411 14324 55213
|
||||
412 14351 55146
|
||||
413 14379 55086
|
||||
414 14410 55036
|
||||
415 14451 55025
|
||||
416 14484 54984
|
||||
417 14513 54929
|
||||
418 14536 54851
|
||||
419 14565 54793
|
||||
420 14587 54710
|
||||
421 14615 54650
|
||||
422 14642 54588
|
||||
423 14666 54515
|
||||
424 14690 54441
|
||||
425 14719 54384
|
||||
426 14739 54297
|
||||
427 14772 54257
|
||||
428 14790 54164
|
||||
429 14824 54125
|
||||
430 14844 54039
|
||||
431 14876 53995
|
||||
432 14906 53946
|
||||
433 14938 53902
|
||||
434 14980 53894
|
||||
435 15006 53829
|
||||
436 15033 53770
|
||||
437 15059 53706
|
||||
438 15085 53639
|
||||
439 15110 53574
|
||||
440 15134 53503
|
||||
441 15160 53438
|
||||
442 15184 53369
|
||||
443 15211 53308
|
||||
444 15234 53236
|
||||
445 15266 53193
|
||||
446 15287 53114
|
||||
447 15316 53059
|
||||
448 15336 52978
|
||||
449 15366 52929
|
||||
450 15393 52870
|
||||
451 15429 52843
|
||||
452 15469 52828
|
||||
453 15490 52748
|
||||
454 15523 52712
|
||||
455 15550 52653
|
||||
456 15577 52594
|
||||
457 15604 52536
|
||||
458 15630 52476
|
||||
459 15656 52414
|
||||
460 15682 52353
|
||||
461 15711 52304
|
||||
462 15736 52238
|
||||
463 15765 52188
|
||||
464 15786 52112
|
||||
465 15817 52068
|
||||
466 15839 51996
|
||||
467 15873 51961
|
||||
468 15903 51916
|
||||
469 15935 51873
|
||||
470 15969 51840
|
||||
471 15994 51779
|
||||
472 16022 51726
|
||||
473 16047 51663
|
||||
474 16073 51605
|
||||
475 16099 51546
|
||||
476 16128 51495
|
||||
477 16152 51431
|
||||
478 16176 51367
|
||||
479 16205 51317
|
||||
480 16228 51250
|
||||
481 16255 51194
|
||||
482 16277 51123
|
||||
483 16305 51071
|
||||
484 16328 51005
|
||||
485 16362 50973
|
||||
486 16392 50928
|
||||
487 16426 50894
|
||||
488 16459 50860
|
||||
489 16480 50787
|
||||
490 16510 50743
|
||||
491 16530 50668
|
||||
492 16561 50625
|
||||
493 16585 50562
|
||||
494 16613 50510
|
||||
495 16638 50453
|
||||
496 16663 50393
|
||||
497 16690 50339
|
||||
498 16716 50282
|
||||
499 16740 50222
|
||||
500 16773 50186
|
||||
501 16802 50139
|
||||
502 16836 50107
|
||||
503 16873 50085
|
||||
504 16921 50094
|
||||
505 16989 50163
|
||||
506 17038 50173
|
||||
507 17069 50132
|
||||
508 17110 50121
|
||||
509 17145 50091
|
||||
510 17190 50091
|
||||
511 17219 50044
|
||||
512 17247 49994
|
||||
513 17271 49932
|
||||
514 17298 49878
|
||||
515 17343 49878
|
||||
516 17373 49836
|
||||
517 17417 49831
|
||||
518 17460 49823
|
||||
519 17490 49781
|
||||
520 17518 49731
|
||||
521 17546 49680
|
||||
522 17571 49622
|
||||
523 17600 49577
|
||||
524 17625 49520
|
||||
525 17655 49474
|
||||
526 17679 49414
|
||||
527 17707 49366
|
||||
528 17729 49300
|
||||
529 17758 49254
|
||||
530 17781 49191
|
||||
531 17808 49141
|
||||
532 17829 49071
|
||||
533 17862 49038
|
||||
534 17905 49031
|
||||
535 18028 49241
|
||||
536 18072 49236
|
||||
537 18106 49203
|
||||
538 18135 49157
|
||||
539 18165 49114
|
||||
540 18200 49083
|
||||
541 18223 49022
|
||||
542 18254 48980
|
||||
543 18280 48927
|
||||
544 18307 48876
|
||||
545 18338 48834
|
||||
546 18367 48790
|
||||
547 18411 48783
|
||||
548 18444 48747
|
||||
549 18470 48693
|
||||
550 18503 48660
|
||||
551 18531 48611
|
||||
552 18557 48558
|
||||
553 18584 48508
|
||||
554 18625 48493
|
||||
555 18650 48436
|
||||
556 18677 48388
|
||||
557 18703 48333
|
||||
558 18729 48282
|
||||
559 18756 48231
|
||||
560 18781 48176
|
||||
561 18808 48126
|
||||
562 18834 48074
|
||||
563 18869 48043
|
||||
564 18902 48008
|
||||
565 18930 47960
|
||||
566 18958 47914
|
||||
567 18983 47859
|
||||
568 19016 47824
|
||||
569 19037 47761
|
||||
570 19068 47720
|
||||
571 19090 47660
|
||||
572 19111 47595
|
||||
573 19141 47553
|
||||
574 19164 47494
|
||||
575 19196 47458
|
||||
576 19217 47393
|
||||
577 19249 47358
|
||||
578 19274 47303
|
||||
579 19298 47247
|
||||
580 19324 47195
|
||||
581 19357 47162
|
||||
582 19391 47130
|
||||
583 19427 47103
|
||||
584 19460 47070
|
||||
585 19483 47012
|
||||
586 19511 46967
|
||||
587 19542 46929
|
||||
588 19564 46867
|
||||
589 19597 46833
|
||||
590 19621 46779
|
||||
591 19647 46729
|
||||
592 19670 46672
|
||||
593 19699 46627
|
||||
594 19726 46582
|
||||
595 19753 46532
|
||||
596 19778 46480
|
||||
597 19803 46429
|
||||
598 19830 46381
|
||||
599 19857 46335
|
||||
600 19896 46313
|
||||
601 19925 46271
|
||||
602 19957 46236
|
||||
603 19991 46204
|
||||
604 20019 46159
|
||||
605 20047 46115
|
||||
606 20072 46063
|
||||
607 20098 46015
|
||||
608 20123 45963
|
||||
609 20149 45913
|
||||
610 20176 45867
|
||||
611 20202 45817
|
||||
612 20230 45774
|
||||
613 20253 45719
|
||||
614 20285 45682
|
||||
615 20307 45626
|
||||
616 20338 45589
|
||||
617 20361 45532
|
||||
618 20394 45500
|
||||
619 20423 45459
|
||||
620 20454 45420
|
||||
621 20488 45390
|
||||
622 20510 45333
|
||||
623 20543 45301
|
||||
624 20569 45252
|
||||
625 20594 45201
|
||||
626 20619 45151
|
||||
627 20646 45107
|
||||
628 20675 45066
|
||||
629 20701 45016
|
||||
630 20727 44970
|
||||
631 20752 44919
|
||||
632 20782 44881
|
||||
633 20804 44825
|
||||
634 20837 44791
|
||||
635 20862 44742
|
||||
636 20892 44704
|
||||
637 20931 44683
|
||||
638 20960 44643
|
||||
639 20994 44612
|
||||
640 21022 44570
|
||||
641 21052 44531
|
||||
642 21082 44493
|
||||
643 21107 44443
|
||||
644 21135 44401
|
||||
645 21160 44351
|
||||
646 21185 44302
|
||||
647 21210 44253
|
||||
648 21236 44208
|
||||
649 21262 44161
|
||||
650 21288 44113
|
||||
651 21315 44068
|
||||
652 21343 44027
|
||||
653 21377 43997
|
||||
654 21403 43949
|
||||
655 21440 43926
|
||||
656 21477 43903
|
||||
657 21502 43854
|
||||
658 21533 43819
|
||||
659 21559 43772
|
||||
660 21586 43727
|
||||
661 21611 43680
|
||||
662 21637 43633
|
||||
663 21662 43586
|
||||
664 21688 43539
|
||||
665 21714 43493
|
||||
666 21742 43451
|
||||
667 21771 43413
|
||||
668 21818 43409
|
||||
669 21846 43366
|
||||
670 21888 43352
|
||||
671 21934 43345
|
||||
672 21971 43322
|
||||
673 22019 43320
|
||||
674 22053 43289
|
||||
675 22090 43266
|
||||
676 22141 43269
|
||||
677 22176 43240
|
||||
678 22213 43215
|
||||
679 22239 43171
|
||||
680 22270 43134
|
||||
681 22296 43088
|
||||
682 22321 43041
|
||||
683 22350 43002
|
||||
684 22379 42962
|
||||
685 22419 42944
|
||||
686 22452 42912
|
||||
687 22484 42878
|
||||
688 22511 42834
|
||||
689 22537 42789
|
||||
690 22571 42757
|
||||
691 22598 42714
|
||||
692 22624 42669
|
||||
693 22653 42630
|
||||
694 22680 42586
|
||||
695 22708 42545
|
||||
696 22739 42510
|
||||
697 22761 42457
|
||||
698 22792 42421
|
||||
699 22816 42373
|
||||
700 22845 42333
|
||||
701 22870 42288
|
||||
702 22902 42253
|
||||
703 22942 42234
|
||||
704 22974 42201
|
||||
705 23002 42160
|
||||
706 23033 42124
|
||||
707 23054 42071
|
||||
708 23086 42038
|
||||
709 23115 41999
|
||||
710 23143 41957
|
||||
711 23169 41914
|
||||
712 23195 41868
|
||||
713 23230 41840
|
||||
714 23259 41801
|
||||
715 23287 41760
|
||||
716 23311 41713
|
||||
717 23341 41676
|
||||
718 23372 41641
|
||||
719 23405 41610
|
||||
720 23438 41578
|
||||
721 23483 41566
|
||||
722 23507 41519
|
||||
723 23540 41488
|
||||
724 23566 41444
|
||||
725 23595 41406
|
||||
726 23623 41365
|
||||
727 23648 41320
|
||||
728 23677 41281
|
||||
729 23700 41231
|
||||
730 23728 41192
|
||||
731 23752 41144
|
||||
732 23784 41111
|
||||
733 23807 41063
|
||||
734 23840 41031
|
||||
735 23870 40994
|
||||
736 23908 40972
|
||||
737 23941 40940
|
||||
738 23974 40909
|
||||
739 24006 40875
|
||||
740 24036 40838
|
||||
741 24064 40798
|
||||
742 24092 40759
|
||||
743 24127 40730
|
||||
744 24153 40688
|
||||
745 24179 40644
|
||||
746 24207 40604
|
||||
747 24233 40561
|
||||
748 24261 40522
|
||||
749 24295 40491
|
||||
750 24318 40444
|
||||
751 24349 40410
|
||||
752 24376 40368
|
||||
753 24408 40335
|
||||
754 24442 40306
|
||||
755 24474 40273
|
||||
756 24508 40242
|
||||
757 24548 40222
|
||||
758 24575 40182
|
||||
759 24605 40145
|
||||
760 24632 40104
|
||||
761 24660 40064
|
||||
762 24689 40027
|
||||
763 24714 39982
|
||||
764 24745 39949
|
||||
765 24766 39897
|
||||
766 24797 39863
|
||||
767 24825 39823
|
||||
768 24854 39786
|
||||
769 24880 39744
|
||||
770 24909 39706
|
||||
771 24940 39672
|
||||
772 24970 39635
|
||||
773 25004 39606
|
||||
774 25030 39564
|
||||
775 25056 39522
|
||||
776 25086 39486
|
||||
777 25107 39436
|
||||
778 25139 39403
|
||||
779 25159 39351
|
||||
780 25188 39314
|
||||
781 25214 39272
|
||||
782 25240 39230
|
||||
783 25266 39188
|
||||
784 25288 39141
|
||||
785 25315 39101
|
||||
786 25341 39058
|
||||
787 25367 39016
|
||||
788 25391 38972
|
||||
789 25417 38930
|
||||
790 25448 38895
|
||||
791 25482 38867
|
||||
792 25514 38834
|
||||
793 25542 38795
|
||||
794 25569 38756
|
||||
795 25595 38714
|
||||
796 25618 38669
|
||||
797 25643 38626
|
||||
798 25667 38581
|
||||
799 25695 38543
|
||||
800 25716 38494
|
||||
801 25743 38454
|
||||
802 25770 38415
|
||||
803 25790 38364
|
||||
804 25822 38332
|
||||
805 25843 38284
|
||||
806 25873 38249
|
||||
807 25896 38203
|
||||
808 25925 38167
|
||||
809 25955 38131
|
||||
810 25988 38101
|
||||
811 26028 38080
|
||||
812 26055 38042
|
||||
813 26081 38000
|
||||
814 26108 37961
|
||||
815 26131 37916
|
||||
816 26159 37878
|
||||
817 26188 37841
|
||||
818 26214 37800
|
||||
819 26242 37764
|
||||
820 26272 37728
|
||||
821 26298 37688
|
||||
822 26327 37652
|
||||
823 26359 37619
|
||||
824 26385 37580
|
||||
825 26408 37534
|
||||
826 26444 37507
|
||||
827 26477 37478
|
||||
828 26517 37456
|
||||
829 26539 37411
|
||||
830 26573 37382
|
||||
831 26597 37339
|
||||
832 26623 37298
|
||||
833 26650 37259
|
||||
834 26677 37221
|
||||
835 26704 37182
|
||||
836 26728 37138
|
||||
837 26763 37111
|
||||
838 26791 37073
|
||||
839 26822 37041
|
||||
840 26872 37033
|
||||
841 26924 37029
|
||||
842 26982 37033
|
||||
843 27054 37055
|
||||
844 27097 37038
|
||||
845 27120 36994
|
||||
846 27146 36954
|
||||
847 27180 36925
|
||||
848 27206 36884
|
||||
849 27234 36846
|
||||
850 27260 36807
|
||||
851 27289 36770
|
||||
852 27318 36734
|
||||
853 27347 36698
|
||||
854 27386 36675
|
||||
855 27413 36637
|
||||
856 27439 36596
|
||||
857 27471 36564
|
||||
858 27501 36529
|
||||
859 27535 36500
|
||||
860 27572 36474
|
||||
861 27595 36431
|
||||
862 27627 36398
|
||||
863 27654 36360
|
||||
864 27683 36324
|
||||
865 27711 36287
|
||||
866 27738 36249
|
||||
867 27765 36210
|
||||
868 27794 36175
|
||||
869 27820 36135
|
||||
|
Executable
+115
@@ -0,0 +1,115 @@
|
||||
import os
|
||||
import json
|
||||
import yaml
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class EnsembleConfig:
|
||||
_instance: Optional['EnsembleConfig'] = None
|
||||
_config: Dict[str, Any] = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(EnsembleConfig, cls).__new__(cls)
|
||||
cls._instance._load_config()
|
||||
return cls._instance
|
||||
|
||||
def _load_config(self):
|
||||
"""Load configuration from YAML file."""
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'ensemble_config.yaml')
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
self._config = yaml.safe_load(f)
|
||||
# print(f"✅ Loaded ensemble config from {config_path}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load ensemble config: {e}")
|
||||
self._config = {}
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Get configuration value by key (supports dot notation for nested keys)."""
|
||||
keys = key.split('.')
|
||||
value = self._config
|
||||
|
||||
try:
|
||||
for k in keys:
|
||||
value = value[k]
|
||||
return value
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
# Singleton accessor
|
||||
def get_config() -> EnsembleConfig:
|
||||
return EnsembleConfig()
|
||||
|
||||
|
||||
# ── Market Thresholds Loader ────────────────────────────────────────────
|
||||
|
||||
_market_thresholds_cache: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
def load_market_thresholds() -> Dict[str, Any]:
|
||||
"""
|
||||
Load market thresholds from JSON config file.
|
||||
Returns the full config dict with 'markets' and 'defaults' keys.
|
||||
Caches after first load for performance.
|
||||
"""
|
||||
global _market_thresholds_cache
|
||||
if _market_thresholds_cache is not None:
|
||||
return _market_thresholds_cache
|
||||
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'market_thresholds.json')
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
_market_thresholds_cache = data
|
||||
print(f"✅ Market thresholds loaded: {len(data.get('markets', {}))} markets (v={data.get('_meta', {}).get('version', '?')})")
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load market thresholds: {e} — using built-in defaults")
|
||||
_market_thresholds_cache = {"markets": {}, "defaults": {
|
||||
"calibration": 0.55,
|
||||
"min_conf": 55.0,
|
||||
"min_play_score": 68.0,
|
||||
"min_edge": 0.02,
|
||||
"odds_band_min_sample": 0.0,
|
||||
"odds_band_min_edge": 0.0,
|
||||
}}
|
||||
return _market_thresholds_cache
|
||||
|
||||
|
||||
def build_threshold_dict(field: str) -> Dict[str, float]:
|
||||
"""
|
||||
Build a flat {market: value} dict for a specific threshold field.
|
||||
|
||||
Usage:
|
||||
calibration_map = build_threshold_dict("calibration")
|
||||
# → {"MS": 0.62, "DC": 0.82, ...}
|
||||
"""
|
||||
data = load_market_thresholds()
|
||||
markets = data.get("markets", {})
|
||||
result: Dict[str, float] = {}
|
||||
for market, cfg in markets.items():
|
||||
if field in cfg:
|
||||
result[market] = float(cfg[field])
|
||||
return result
|
||||
|
||||
|
||||
def get_threshold_default(field: str) -> float:
|
||||
"""Get the default fallback value for a threshold field."""
|
||||
data = load_market_thresholds()
|
||||
defaults = data.get("defaults", {})
|
||||
return float(defaults.get(field, 0.0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test
|
||||
cfg = get_config()
|
||||
print(f"Weights: {cfg.get('engine_weights')}")
|
||||
print(f"Team Weight: {cfg.get('engine_weights.team')}")
|
||||
print()
|
||||
print("--- Market Thresholds ---")
|
||||
for field in ["calibration", "min_conf", "min_play_score", "min_edge"]:
|
||||
d = build_threshold_dict(field)
|
||||
print(f"{field}: {d}")
|
||||
print(f"Default calibration: {get_threshold_default('calibration')}")
|
||||
Executable
+197
@@ -0,0 +1,197 @@
|
||||
model_ensemble:
|
||||
xgb_weight: 0.50
|
||||
lgb_weight: 0.50
|
||||
temperature: 1.5
|
||||
default_ms_odds:
|
||||
home: 2.65
|
||||
draw: 3.20
|
||||
away: 2.65
|
||||
elo_staleness_days: 14
|
||||
odds_staleness_hours: 48
|
||||
|
||||
engine_weights:
|
||||
team: 0.30
|
||||
player: 0.25
|
||||
odds: 0.30
|
||||
referee: 0.15
|
||||
min_weight: 0.05
|
||||
|
||||
weight_redistribution:
|
||||
player_missing_to_team: 0.5
|
||||
player_missing_to_odds: 0.5
|
||||
referee_missing_to_team: 0.4
|
||||
referee_missing_to_odds: 0.6
|
||||
referee_min_matches: 5
|
||||
|
||||
match_result:
|
||||
min_draw_prob: 0.15
|
||||
|
||||
over_under:
|
||||
prob_min: 0.02
|
||||
prob_max: 0.98
|
||||
ou15_threshold: 0.55
|
||||
ou25_threshold: 0.52
|
||||
ou35_threshold: 0.48
|
||||
btts_threshold: 0.58
|
||||
poisson_blend_weight: 0.25
|
||||
poisson_grid_max: 6
|
||||
|
||||
half_time:
|
||||
ft_to_ht_ratio: 0.42
|
||||
poisson_grid_max: 5
|
||||
ht_over_05_min: 0.20
|
||||
ht_over_05_max: 0.95
|
||||
ht_ou_threshold: 0.55
|
||||
ht_draw_floor: 0.28
|
||||
low_xg_threshold: 2.0
|
||||
low_xg_ratio_adjust: 0.85
|
||||
|
||||
confidence:
|
||||
agreement_boost: 1.3
|
||||
disagreement_penalty: 0.7
|
||||
|
||||
handicap:
|
||||
xg_diff_threshold: 1.2
|
||||
|
||||
corners:
|
||||
xg_multiplier: 3.0
|
||||
baseline: 3.0
|
||||
home_dominant_bonus: 1.5
|
||||
away_dominant_bonus: 1.0
|
||||
dominance_threshold: 0.6
|
||||
line: 9.5
|
||||
|
||||
cards:
|
||||
derby_heat_factor: 1.3
|
||||
line: 4.5
|
||||
|
||||
score:
|
||||
poisson_grid_max: 7
|
||||
ms_confidence_threshold: 15.0
|
||||
|
||||
risk:
|
||||
# Lowered thresholds for better surprise detection (was 0.20+)
|
||||
# Model typically outputs 4-8% for reversals, so we need lower thresholds
|
||||
surprise_threshold: 0.05
|
||||
surprise_threshold_top: 0.05
|
||||
surprise_threshold_non_top: 0.06
|
||||
surprise_threshold_favorite_reversal: 0.06
|
||||
surprise_threshold_favorite_reversal_top: 0.06
|
||||
surprise_threshold_favorite_reversal_non_top: 0.08
|
||||
surprise_threshold_underdog_reversal: 0.05
|
||||
surprise_threshold_underdog_reversal_top: 0.05
|
||||
surprise_threshold_underdog_reversal_non_top: 0.06
|
||||
surprise_threshold_basketball: 0.08
|
||||
surprise_threshold_basketball_top: 0.08
|
||||
surprise_threshold_basketball_non_top: 0.10
|
||||
surprise_min_top_gap: 0.01
|
||||
surprise_min_top_gap_top: 0.01
|
||||
surprise_min_top_gap_non_top: 0.015
|
||||
# New: Upset alert threshold for potential upsets (lower than main threshold)
|
||||
upset_alert_threshold: 0.05 # 5% - alert when reversal prob > 5%
|
||||
htft_temperature: 1.25
|
||||
htft_temperature_top: 1.25
|
||||
htft_temperature_non_top: 1.35
|
||||
htft_temperature_basketball: 1.08
|
||||
htft_temperature_basketball_top: 1.08
|
||||
htft_temperature_basketball_non_top: 1.15
|
||||
htft_reversal_multiplier: 0.60
|
||||
htft_reversal_multiplier_top: 0.60
|
||||
htft_reversal_multiplier_non_top: 0.45
|
||||
htft_reversal_multiplier_favorite: 0.72
|
||||
htft_reversal_multiplier_favorite_top: 0.72
|
||||
htft_reversal_multiplier_favorite_non_top: 0.55
|
||||
htft_reversal_multiplier_underdog: 0.45
|
||||
htft_reversal_multiplier_underdog_top: 0.45
|
||||
htft_reversal_multiplier_underdog_non_top: 0.30
|
||||
htft_reversal_multiplier_basketball: 0.90
|
||||
htft_reversal_multiplier_basketball_top: 0.90
|
||||
htft_reversal_multiplier_basketball_non_top: 0.75
|
||||
htft_reversal_gap_medium: 0.50
|
||||
htft_reversal_gap_strong: 1.00
|
||||
htft_prior_min_matches: 300
|
||||
htft_prior_blend_league: 0.65
|
||||
htft_prior_blend_top: 0.50
|
||||
htft_prior_blend_non_top: 0.58
|
||||
htft_prior_odds_blend_top: 0.35
|
||||
htft_prior_odds_blend_top_with_league: 0.22
|
||||
htft_favorite_balance_gap: 0.20
|
||||
htft_reversal_cap_factor: 2.30
|
||||
extreme_upset: 0.7
|
||||
high_upset: 0.5
|
||||
medium_upset: 0.3
|
||||
extreme_warnings: 3
|
||||
high_warnings: 2
|
||||
balanced_match_gap: 0.1
|
||||
referee_min_data: 10
|
||||
|
||||
recommendations:
|
||||
confidence_threshold: 45
|
||||
value_confidence_min: 10
|
||||
value_confidence_max: 30
|
||||
value_edge_margin: 0.02
|
||||
value_upgrade_edge: 5.0
|
||||
|
||||
# ACİL DÜZELTİLDİ: Güvenilir marketler genişletildi
|
||||
safe_markets: ['ÇŞ', '1.5 Üst/Alt', '2.5 Üst/Alt']
|
||||
|
||||
# ACİL DÜZELTİLDİ: Market bazlı minimum confidence threshold'lar (Artık Olasılık Yüzdesi!)
|
||||
market_min_confidence:
|
||||
MS: 50.0 # Match result is hardest; 50%+ true probability is actually strong
|
||||
ÇŞ: 65.0 # Double chance naturally has high probability (2 sides of 3)
|
||||
1.5 Üst/Alt: 70.0 # 1.5 Goals needs to be highly probable to be worth playing
|
||||
2.5 Üst/Alt: 55.0 # Standard threshold for 50/50 lines
|
||||
3.5 Üst/Alt: 60.0 # Needs higher certianty than 2.5
|
||||
BTTS: 60.0 # Both Teams To Score - raised for accuracy (was 47.7%)
|
||||
|
||||
risk_safe_boost: 1.2
|
||||
risk_ms_penalty_high: 0.5
|
||||
risk_ms_penalty_medium: 0.8
|
||||
risk_other_penalty: 0.7
|
||||
|
||||
# ACİL DÜZELTİLDİ: Market weights güvenilir marketlere göre ayarlandı
|
||||
market_weights:
|
||||
MS: 0.5 # ⬇️ Düşürüldü (zayıf performans)
|
||||
ÇŞ: 1.5 # ⬆️ Artırıldı (güçlü performans)
|
||||
1.5 Üst/Alt: 1.6 # ⬆️ En yüksek (en güvenilir)
|
||||
2.5 Üst/Alt: 1.2 # ⬆️ Artırıldı
|
||||
3.5 Üst/Alt: 0.9 # ⬇️ Düşürüldü
|
||||
BTTS: 0.4 # ⬇️ Düşürüldü (zayıf performans)
|
||||
|
||||
# Confidence Calibration (backtest-derived accuracy)
|
||||
baseline_accuracy: 65.0
|
||||
market_accuracy:
|
||||
MS: 52.1 # ❌ Zayıf
|
||||
ÇŞ: 77.9 # ✅ İyi
|
||||
1.5 Üst/Alt: 82.1 # ✅ Mükemmel
|
||||
2.5 Üst/Alt: 61.4 # ⚠️ Orta
|
||||
3.5 Üst/Alt: 60.7 # ⚠️ Orta
|
||||
BTTS: 50.7 # ❌ Zayıf
|
||||
|
||||
calibration_buckets:
|
||||
ms_home:
|
||||
heavy_fav: 1.40 # home odds <= 1.40
|
||||
fav: 1.80 # home odds > 1.40 and <= 1.80
|
||||
balanced: 2.50 # home odds > 1.80 and <= 2.50
|
||||
underdog: 99.0 # home odds > 2.50
|
||||
|
||||
team_xg:
|
||||
home_base: 1.35
|
||||
away_base: 1.10
|
||||
home_conversion_mult: 3.0
|
||||
away_conversion_mult: 2.5
|
||||
|
||||
sidelined:
|
||||
position_weights:
|
||||
K: 0.35
|
||||
D: 0.20
|
||||
O: 0.25
|
||||
F: 0.30
|
||||
max_rating: 10
|
||||
adaptation_threshold: 10
|
||||
adaptation_discount: 0.5
|
||||
goalkeeper_penalty: 0.15
|
||||
confidence_boost: 10
|
||||
max_impact: 0.85
|
||||
key_player_threshold: 3
|
||||
recent_matches_lookback: 15
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": "v34",
|
||||
"description": "Market-specific thresholds for the betting engine pipeline — V34 odds-aware gate fix",
|
||||
"rule": "max_reachable (100 × calibration) MUST be > min_conf + 8",
|
||||
"updated_at": "2026-05-10",
|
||||
"changelog": "V34: Reduced min_edge to realistic levels for odds-aware V25 model. Model output ≈ market-implied, so large EV edges are mathematically impossible."
|
||||
},
|
||||
"markets": {
|
||||
"MS": {
|
||||
"calibration": 0.62,
|
||||
"min_conf": 20.0,
|
||||
"min_play_score": 28.0,
|
||||
"min_edge": 0.005,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.005
|
||||
},
|
||||
"DC": {
|
||||
"calibration": 0.82,
|
||||
"min_conf": 40.0,
|
||||
"min_play_score": 50.0,
|
||||
"min_edge": 0.003,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.005
|
||||
},
|
||||
"OU15": {
|
||||
"calibration": 0.84,
|
||||
"min_conf": 45.0,
|
||||
"min_play_score": 50.0,
|
||||
"min_edge": 0.003,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.005
|
||||
},
|
||||
"OU25": {
|
||||
"calibration": 0.68,
|
||||
"min_conf": 30.0,
|
||||
"min_play_score": 40.0,
|
||||
"min_edge": 0.005,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.005
|
||||
},
|
||||
"OU35": {
|
||||
"calibration": 0.60,
|
||||
"min_conf": 20.0,
|
||||
"min_play_score": 30.0,
|
||||
"min_edge": 0.008,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.008
|
||||
},
|
||||
"BTTS": {
|
||||
"calibration": 0.65,
|
||||
"min_conf": 30.0,
|
||||
"min_play_score": 40.0,
|
||||
"min_edge": 0.005,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.005
|
||||
},
|
||||
"HT": {
|
||||
"calibration": 0.58,
|
||||
"min_conf": 20.0,
|
||||
"min_play_score": 28.0,
|
||||
"min_edge": 0.01,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.008
|
||||
},
|
||||
"HT_OU05": {
|
||||
"calibration": 0.68,
|
||||
"min_conf": 35.0,
|
||||
"min_play_score": 42.0,
|
||||
"min_edge": 0.005,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.005
|
||||
},
|
||||
"HT_OU15": {
|
||||
"calibration": 0.60,
|
||||
"min_conf": 25.0,
|
||||
"min_play_score": 32.0,
|
||||
"min_edge": 0.008,
|
||||
"odds_band_min_sample": 8.0,
|
||||
"odds_band_min_edge": 0.008
|
||||
},
|
||||
"OE": {
|
||||
"calibration": 0.62,
|
||||
"min_conf": 35.0,
|
||||
"min_play_score": 32.0,
|
||||
"min_edge": 0.005
|
||||
},
|
||||
"CARDS": {
|
||||
"calibration": 0.58,
|
||||
"min_conf": 30.0,
|
||||
"min_play_score": 35.0,
|
||||
"min_edge": 0.008
|
||||
},
|
||||
"HCAP": {
|
||||
"calibration": 0.56,
|
||||
"min_conf": 25.0,
|
||||
"min_play_score": 30.0,
|
||||
"min_edge": 0.015
|
||||
},
|
||||
"HTFT": {
|
||||
"calibration": 0.45,
|
||||
"min_conf": 10.0,
|
||||
"min_play_score": 18.0,
|
||||
"min_edge": 0.02
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"calibration": 0.55,
|
||||
"min_conf": 55.0,
|
||||
"min_play_score": 60.0,
|
||||
"min_edge": 0.008,
|
||||
"odds_band_min_sample": 0.0,
|
||||
"odds_band_min_edge": 0.0
|
||||
}
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .match_result_calculator import MatchResultCalculator
|
||||
from .over_under_calculator import OverUnderCalculator
|
||||
from .half_time_calculator import HalfTimeCalculator
|
||||
from .score_calculator import ScoreCalculator
|
||||
from .other_markets_calculator import OtherMarketsCalculator
|
||||
from .risk_assessor import RiskAssessor
|
||||
from .bet_recommender import BetRecommender, MarketPredictionDTO
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Base classes and context dataclass for all calculators.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class CalculationContext:
|
||||
"""Context object holding all inputs for calculators."""
|
||||
|
||||
team_pred: Any
|
||||
player_pred: Any
|
||||
odds_pred: Any
|
||||
referee_pred: Any
|
||||
upset_factors: Any
|
||||
|
||||
weights: dict[str, float]
|
||||
player_mods: dict[str, float]
|
||||
referee_mods: dict[str, float]
|
||||
|
||||
match_id: str
|
||||
home_team_name: str
|
||||
away_team_name: str
|
||||
|
||||
odds_data: dict[str, float]
|
||||
home_xg: float
|
||||
away_xg: float
|
||||
total_xg: float
|
||||
|
||||
league_id: str | None = None
|
||||
sport: str = "football"
|
||||
is_top_league: bool = False
|
||||
|
||||
# Risk info (populated later)
|
||||
risk_level: str = "MEDIUM"
|
||||
is_surprise: bool = False
|
||||
|
||||
# XGBoost Predictions (New)
|
||||
xgboost_preds: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class BaseCalculator:
|
||||
"""Base class for all market calculators."""
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
|
||||
def calculate(self, ctx: CalculationContext) -> dict[str, Any]:
|
||||
raise NotImplementedError("Subclasses must implement calculate()")
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Any
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .match_result_calculator import MatchResultPrediction
|
||||
from .over_under_calculator import OverUnderPrediction
|
||||
from .risk_assessor import RiskAnalysis
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarketPredictionDTO:
|
||||
market_type: str
|
||||
pick: str
|
||||
probability: float
|
||||
confidence: float
|
||||
odds: float = 0.0
|
||||
is_recommended: bool = False
|
||||
is_value_bet: bool = False
|
||||
edge: float = 0.0
|
||||
is_skip: bool = False # NEW: If model is unsure, mark as skip
|
||||
|
||||
@dataclass
|
||||
class RecommendationResult:
|
||||
best_bet: Optional[MarketPredictionDTO]
|
||||
recommended_bets: List[MarketPredictionDTO]
|
||||
alternative_bet: Optional[MarketPredictionDTO]
|
||||
value_bets: List[MarketPredictionDTO]
|
||||
skipped_bets: List[MarketPredictionDTO] # NEW: Track what we decided NOT to predict
|
||||
|
||||
|
||||
class BetRecommender(BaseCalculator):
|
||||
def calculate(self, # type: ignore[override]
|
||||
ctx: CalculationContext,
|
||||
ms_res: MatchResultPrediction,
|
||||
ou_res: OverUnderPrediction,
|
||||
risk: RiskAnalysis) -> RecommendationResult:
|
||||
|
||||
odds_data = ctx.odds_data
|
||||
|
||||
# Market-Specific Minimum Confidence Thresholds (Hard Gates)
|
||||
# Below these, we say "I don't know" (SKIP)
|
||||
min_conf_thresholds = {
|
||||
"MS": 45.0, # 3-way is hard, need at least 45%
|
||||
"ÇŞ": 40.0, # Double chance is safer, but still need 40%
|
||||
"1.5 Üst/Alt": 50.0,
|
||||
"2.5 Üst/Alt": 45.0,
|
||||
"3.5 Üst/Alt": 45.0,
|
||||
"BTTS": 45.0,
|
||||
"HT": 40.0,
|
||||
}
|
||||
|
||||
# Prepare candidates
|
||||
markets = [
|
||||
MarketPredictionDTO("MS", ms_res.ms_pick,
|
||||
ms_res.ms_home_prob if ms_res.ms_pick == "1" else (ms_res.ms_away_prob if ms_res.ms_pick == "2" else ms_res.ms_draw_prob),
|
||||
ms_res.ms_confidence,
|
||||
odds_data.get(f"ms_{ms_res.ms_pick.lower()}", 0)),
|
||||
|
||||
MarketPredictionDTO("ÇŞ", ms_res.dc_pick,
|
||||
ms_res.dc_1x_prob if ms_res.dc_pick == "1X" else (ms_res.dc_x2_prob if ms_res.dc_pick == "X2" else ms_res.dc_12_prob),
|
||||
ms_res.dc_confidence,
|
||||
odds_data.get(f"dc_{ms_res.dc_pick.lower()}", 0)),
|
||||
|
||||
MarketPredictionDTO("1.5 Üst/Alt", ou_res.ou15_pick,
|
||||
ou_res.over_15_prob if "Üst" in ou_res.ou15_pick else ou_res.under_15_prob,
|
||||
ou_res.ou15_confidence, 0),
|
||||
|
||||
MarketPredictionDTO("2.5 Üst/Alt", ou_res.ou25_pick,
|
||||
ou_res.over_25_prob if "Üst" in ou_res.ou25_pick else ou_res.under_25_prob,
|
||||
ou_res.ou25_confidence,
|
||||
odds_data.get("ou25_o" if "Üst" in ou_res.ou25_pick else "ou25_u", 0)),
|
||||
|
||||
MarketPredictionDTO("3.5 Üst/Alt", ou_res.ou35_pick,
|
||||
ou_res.over_35_prob if "Üst" in ou_res.ou35_pick else ou_res.under_35_prob,
|
||||
ou_res.ou35_confidence, 0),
|
||||
|
||||
MarketPredictionDTO("BTTS", ou_res.btts_pick,
|
||||
ou_res.btts_yes_prob if "Var" in ou_res.btts_pick else ou_res.btts_no_prob,
|
||||
ou_res.btts_confidence,
|
||||
odds_data.get("btts_y" if "Var" in ou_res.btts_pick else "btts_n", 0)),
|
||||
]
|
||||
|
||||
# Market weights from config (historical accuracy weighting)
|
||||
market_weights = self.config.get("recommendations.market_weights", {})
|
||||
default_weight = 1.0
|
||||
|
||||
safe_markets = set(self.config.get("recommendations.safe_markets", ["ÇŞ", "1.5 Üst/Alt"]))
|
||||
risk_level = risk.risk_level
|
||||
|
||||
# Confidence calibration (backtest-derived accuracy scaling)
|
||||
market_accuracy = self.config.get("recommendations.market_accuracy", {})
|
||||
baseline_accuracy = self.config.get("recommendations.baseline_accuracy", 65.0)
|
||||
|
||||
def _calibrated_confidence(m):
|
||||
"""Scale raw confidence by market's historical accuracy ratio."""
|
||||
accuracy = market_accuracy.get(m.market_type, baseline_accuracy) if isinstance(market_accuracy, dict) else baseline_accuracy
|
||||
ratio = accuracy / baseline_accuracy
|
||||
return m.confidence * ratio
|
||||
|
||||
def _score(m):
|
||||
mw = market_weights.get(m.market_type, default_weight) if isinstance(market_weights, dict) else default_weight
|
||||
|
||||
# 1. Base Score: calibrated confidence * market weight
|
||||
cal_conf = _calibrated_confidence(m)
|
||||
score = cal_conf * mw
|
||||
|
||||
# 2. Value/Edge Bonus
|
||||
odds_val = m.odds if m.odds is not None else 0.0
|
||||
if odds_val > 0:
|
||||
implied = 1.0 / odds_val
|
||||
edge = (m.probability - implied) * 100
|
||||
if edge > 0:
|
||||
score += edge * 4.0
|
||||
|
||||
# 3. Risk adjustment
|
||||
if risk_level in ("HIGH", "EXTREME"):
|
||||
if m.market_type in safe_markets:
|
||||
score *= self.config.get("recommendations.risk_safe_boost", 1.2)
|
||||
elif m.market_type == "MS":
|
||||
score *= self.config.get("recommendations.risk_ms_penalty_high", 0.5)
|
||||
else:
|
||||
score *= self.config.get("recommendations.risk_other_penalty", 0.7)
|
||||
elif risk_level == "MEDIUM":
|
||||
if m.market_type == "MS":
|
||||
score *= self.config.get("recommendations.risk_ms_penalty_medium", 0.8)
|
||||
|
||||
# 4. Extreme Confidence Bonus
|
||||
if cal_conf > 80:
|
||||
score *= 1.15
|
||||
|
||||
return score
|
||||
|
||||
recommended = []
|
||||
value_bets = []
|
||||
skipped_bets = []
|
||||
|
||||
conf_thr = self.config.get("recommendations.confidence_threshold", 60)
|
||||
|
||||
val_min = self.config.get("recommendations.value_confidence_min", 45) # Increased from 30
|
||||
val_max = self.config.get("recommendations.value_confidence_max", 60)
|
||||
val_margin = self.config.get("recommendations.value_edge_margin", 0.03) # Increased from 0.02
|
||||
val_upgrade = self.config.get("recommendations.value_upgrade_edge", 5.0)
|
||||
|
||||
for m in markets:
|
||||
# --- SKIP LOGIC (Hard Gate) ---
|
||||
# 1. Confidence is below market threshold
|
||||
min_conf = min_conf_thresholds.get(m.market_type, 45.0)
|
||||
if m.confidence < min_conf:
|
||||
m.is_skip = True
|
||||
skipped_bets.append(m)
|
||||
continue
|
||||
|
||||
# 2. Negative Value Edge (Odds are too low for our probability)
|
||||
if m.odds > 0:
|
||||
implied = 1.0 / m.odds
|
||||
edge = m.probability - implied
|
||||
# If our prob is significantly lower than implied (negative edge > 3%), SKIP
|
||||
if edge < -0.03:
|
||||
m.is_skip = True
|
||||
skipped_bets.append(m)
|
||||
continue
|
||||
|
||||
# --- PROCESS BET ---
|
||||
# 1. Regular recommended
|
||||
if m.confidence >= conf_thr:
|
||||
m.is_recommended = True
|
||||
recommended.append(m)
|
||||
|
||||
# 2. Value bet logic
|
||||
if m.confidence is not None and val_min <= m.confidence <= val_max and m.odds > 0:
|
||||
implied = 1.0 / m.odds
|
||||
if m.probability > (implied + val_margin):
|
||||
m.is_value_bet = True
|
||||
m.edge = (m.probability - implied) * 100
|
||||
|
||||
if m.edge > val_upgrade:
|
||||
m.is_recommended = True
|
||||
recommended.append(m)
|
||||
else:
|
||||
value_bets.append(m)
|
||||
|
||||
# Best bet (from recommended only)
|
||||
best_bet = None
|
||||
if recommended:
|
||||
# Re-sort only recommended markets to find the best one
|
||||
valid_markets = [m for m in markets if not m.is_skip and m.is_recommended]
|
||||
if valid_markets:
|
||||
valid_markets.sort(key=_score, reverse=True)
|
||||
best_bet = valid_markets[0]
|
||||
best_bet.is_recommended = True
|
||||
|
||||
# Alternative bet
|
||||
alternative = None
|
||||
if risk.is_surprise_risk and ms_res.ms_pick in ["1", "2"]:
|
||||
# Check if alternative is not skipped
|
||||
alt_candidate = MarketPredictionDTO(
|
||||
"2.5 Üst/Alt", ou_res.ou25_pick,
|
||||
ou_res.over_25_prob if "Üst" in ou_res.ou25_pick else ou_res.under_25_prob,
|
||||
ou_res.ou25_confidence,
|
||||
odds_data.get("ou25_o" if "Üst" in ou_res.ou25_pick else "ou25_u", 0)
|
||||
)
|
||||
if alt_candidate.confidence >= min_conf_thresholds.get("2.5 Üst/Alt", 45.0):
|
||||
alternative = alt_candidate
|
||||
|
||||
return RecommendationResult(
|
||||
best_bet=best_bet,
|
||||
recommended_bets=recommended,
|
||||
alternative_bet=alternative,
|
||||
value_bets=value_bets,
|
||||
skipped_bets=skipped_bets
|
||||
)
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
def calc_confidence_3way(top_prob: float) -> float:
|
||||
"""Returns the true win probability percentage (e.g. 0.45 -> 45.0)."""
|
||||
return max(0, min(99.0, top_prob * 100))
|
||||
|
||||
def calc_confidence_2way(prob: float) -> float:
|
||||
"""Returns the true win probability percentage for the favored side."""
|
||||
# Find the probability of the >0.5 side
|
||||
win_prob = prob if prob >= 0.5 else (1.0 - prob)
|
||||
return max(0, min(99.0, win_prob * 100))
|
||||
|
||||
def calc_confidence_dc(top_prob: float) -> float:
|
||||
"""Returns the true win probability percentage for double chance."""
|
||||
return max(0, min(99.0, top_prob * 100))
|
||||
|
||||
def calc_confidence_3way_with_agreement(top_prob: float, agreement_ratio: float,
|
||||
boost: float = 1.05, penalty: float = 0.95) -> float:
|
||||
"""
|
||||
Returns the true win probability percentage, slightly adjusted by engine consensus.
|
||||
|
||||
Args:
|
||||
top_prob: highest probability among options
|
||||
agreement_ratio: 0.0 to 1.0 — how many engines agree on the pick
|
||||
"""
|
||||
base = calc_confidence_3way(top_prob)
|
||||
|
||||
# Slight nudge rather than massive swing, to keep it feeling like a true probability
|
||||
if agreement_ratio >= 0.75:
|
||||
return min(99.0, base * boost)
|
||||
elif agreement_ratio <= 0.25:
|
||||
return max(0.0, base * penalty)
|
||||
|
||||
return base
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Expert Recommendation Engine (Senior Level)
|
||||
============================================
|
||||
Evaluates ALL markets, classifies by risk, and ensures NO "empty" recommendations.
|
||||
Prioritizes user safety by clearly labeling risk levels.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Any, Dict
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .match_result_calculator import MatchResultPrediction
|
||||
from .over_under_calculator import OverUnderPrediction
|
||||
from .risk_assessor import RiskAnalysis
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpertPick:
|
||||
market_type: str
|
||||
pick: str
|
||||
probability: float
|
||||
confidence: float
|
||||
odds: float
|
||||
edge: float # Expected value percentage
|
||||
|
||||
# Risk Classification
|
||||
risk_level: str # SAFE, MEDIUM, RISKY, SURPRISE
|
||||
reasoning: str # Why this pick? (e.g., "High xG support", "Value detected")
|
||||
|
||||
@dataclass
|
||||
class ExpertResult:
|
||||
main_pick: ExpertPick
|
||||
safe_alternative: Optional[ExpertPick]
|
||||
value_picks: List[ExpertPick]
|
||||
surprise_picks: List[ExpertPick]
|
||||
market_summary: Dict[str, float] # {market: probability}
|
||||
|
||||
|
||||
class ExpertRecommender(BaseCalculator):
|
||||
def calculate(self, # type: ignore[override]
|
||||
ctx: CalculationContext,
|
||||
ms_res: MatchResultPrediction,
|
||||
ou_res: OverUnderPrediction,
|
||||
risk: RiskAnalysis) -> ExpertResult:
|
||||
|
||||
odds_data = ctx.odds_data
|
||||
all_picks: List[ExpertPick] = []
|
||||
|
||||
# ─── 1. Helper to Evaluate Pick ───
|
||||
def evaluate(market: str, pick: str, prob: float, odd_key: str):
|
||||
odd_val = float(odds_data.get(odd_key, 0))
|
||||
# If odd is missing/low, estimate it via probability (Kelly-ish estimation)
|
||||
if odd_val <= 1.01:
|
||||
odd_val = round(1.0 / (prob + 0.05), 2) # Conservative estimation
|
||||
reasoning = "Derived (No market odd)"
|
||||
else:
|
||||
reasoning = "Market Confirmed"
|
||||
|
||||
implied = 1.0 / odd_val
|
||||
edge = (prob - implied) * 100
|
||||
|
||||
# ─── Risk Classification ───
|
||||
if prob >= 0.75 and odd_val <= 1.45:
|
||||
level = "SAFE"
|
||||
elif edge > 5.0:
|
||||
level = "VALUE"
|
||||
elif odd_val >= 2.50 and prob >= 0.35:
|
||||
level = "SURPRISE"
|
||||
else:
|
||||
level = "MEDIUM"
|
||||
|
||||
all_picks.append(ExpertPick(
|
||||
market_type=market, pick=pick, probability=prob,
|
||||
confidence=prob * 100, odds=odd_val, edge=edge,
|
||||
risk_level=level, reasoning=reasoning
|
||||
))
|
||||
|
||||
# ─── 2. Evaluate All Major Markets ───
|
||||
# MS
|
||||
evaluate("MS", ms_res.ms_pick,
|
||||
ms_res.ms_home_prob if ms_res.ms_pick == "1" else (ms_res.ms_away_prob if ms_res.ms_pick == "2" else ms_res.ms_draw_prob),
|
||||
f"ms_{ms_res.ms_pick.lower()}")
|
||||
|
||||
# Double Chance
|
||||
evaluate("DC", ms_res.dc_pick,
|
||||
ms_res.dc_1x_prob if ms_res.dc_pick == "1X" else (ms_res.dc_x2_prob if ms_res.dc_pick == "X2" else ms_res.dc_12_prob),
|
||||
f"dc_{ms_res.dc_pick.lower()}")
|
||||
|
||||
# OU25
|
||||
evaluate("OU25", ou_res.ou25_pick,
|
||||
ou_res.over_25_prob if "Üst" in ou_res.ou25_pick else ou_res.under_25_prob,
|
||||
"ou25_o" if "Üst" in ou_res.ou25_pick else "ou25_u")
|
||||
|
||||
# BTTS
|
||||
evaluate("BTTS", ou_res.btts_pick,
|
||||
ou_res.btts_yes_prob if "Var" in ou_res.btts_pick else ou_res.btts_no_prob,
|
||||
"btts_y" if "Var" in ou_res.btts_pick else "btts_n")
|
||||
|
||||
# OU15
|
||||
evaluate("OU15", ou_res.ou15_pick,
|
||||
ou_res.over_15_prob if "Üst" in ou_res.ou15_pick else ou_res.under_15_prob,
|
||||
"ou15_o" if "Üst" in ou_res.ou15_pick else "ou15_u")
|
||||
|
||||
# ─── 3. Sort and Select ───
|
||||
# Sort by a mix of Confidence and Edge
|
||||
all_picks.sort(key=lambda p: (p.probability * 0.6) + (max(0, p.edge/100) * 0.4), reverse=True)
|
||||
|
||||
main = all_picks[0]
|
||||
|
||||
# Find Safe Alternative (if main isn't Safe)
|
||||
safe_alt = next((p for p in all_picks if p.risk_level == "SAFE"), None)
|
||||
if safe_alt == main: safe_alt = None
|
||||
|
||||
value_picks = [p for p in all_picks if p.risk_level == "VALUE" and p != main]
|
||||
surprise_picks = [p for p in all_picks if p.risk_level == "SURPRISE"]
|
||||
|
||||
# Market Summary for UI
|
||||
market_summary = {
|
||||
"MS_Home": ms_res.ms_home_prob,
|
||||
"MS_Draw": ms_res.ms_draw_prob,
|
||||
"MS_Away": ms_res.ms_away_prob,
|
||||
"OU25_Over": ou_res.over_25_prob,
|
||||
"BTTS_Yes": ou_res.btts_yes_prob
|
||||
}
|
||||
|
||||
return ExpertResult(
|
||||
main_pick=main,
|
||||
safe_alternative=safe_alt,
|
||||
value_picks=value_picks,
|
||||
surprise_picks=surprise_picks,
|
||||
market_summary=market_summary
|
||||
)
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .confidence import calc_confidence_3way, calc_confidence_2way
|
||||
|
||||
|
||||
@dataclass
|
||||
class HalfTimePrediction:
|
||||
ht_home_prob: float
|
||||
ht_draw_prob: float
|
||||
ht_away_prob: float
|
||||
ht_pick: str
|
||||
ht_confidence: float
|
||||
|
||||
ht_over_05_prob: float
|
||||
ht_under_05_prob: float
|
||||
ht_over_15_prob: float
|
||||
ht_under_15_prob: float
|
||||
ht_ou_pick: str
|
||||
ht_ou15_pick: str
|
||||
|
||||
ht_home_xg: float
|
||||
ht_away_xg: float
|
||||
|
||||
|
||||
class HalfTimeCalculator(BaseCalculator):
|
||||
|
||||
def _poisson_pmf(self, k, lam):
|
||||
"""Poisson probability mass function."""
|
||||
if lam <= 0:
|
||||
return 1.0 if k == 0 else 0.0
|
||||
return (lam ** k) * math.exp(-lam) / math.factorial(k)
|
||||
|
||||
def calculate(self, ctx: CalculationContext) -> HalfTimePrediction: # type: ignore[override]
|
||||
team_pred = ctx.team_pred
|
||||
odds_pred = ctx.odds_pred
|
||||
|
||||
# Config
|
||||
ft_to_ht_ratio = self.config.get("half_time.ft_to_ht_ratio", 0.42)
|
||||
grid_max = self.config.get("half_time.poisson_grid_max", 5)
|
||||
draw_floor = self.config.get("half_time.ht_draw_floor", 0.35)
|
||||
low_xg_thr = self.config.get("half_time.low_xg_threshold", 2.0)
|
||||
low_xg_adj = self.config.get("half_time.low_xg_ratio_adjust", 0.85)
|
||||
|
||||
# FT xG (blended team + odds)
|
||||
ft_home_xg = (team_pred.home_xg + odds_pred.poisson_home_xg) / 2
|
||||
ft_away_xg = (team_pred.away_xg + odds_pred.poisson_away_xg) / 2
|
||||
total_ft_xg = ft_home_xg + ft_away_xg
|
||||
|
||||
# Dynamic HT ratio: düşük xG maçlarda ratio'yu küçült
|
||||
# Çünkü düşük gollü maçlarda ilk yarıda gol olma ihtimali daha da düşük
|
||||
effective_ratio = ft_to_ht_ratio
|
||||
if total_ft_xg < low_xg_thr:
|
||||
effective_ratio *= low_xg_adj
|
||||
|
||||
# HT xG
|
||||
ht_home_xg = ft_home_xg * effective_ratio
|
||||
ht_away_xg = ft_away_xg * effective_ratio
|
||||
ht_total_xg = ht_home_xg + ht_away_xg
|
||||
|
||||
# Compute HT 1X2 via bivariate Poisson grid
|
||||
ht_home = 0.0
|
||||
ht_away = 0.0
|
||||
ht_draw = 0.0
|
||||
|
||||
# Also compute O/U while iterating
|
||||
total_goals_prob = {}
|
||||
|
||||
for i in range(grid_max):
|
||||
for j in range(grid_max):
|
||||
p = self._poisson_pmf(i, ht_home_xg) * self._poisson_pmf(j, ht_away_xg)
|
||||
if i > j:
|
||||
ht_home += p
|
||||
elif i < j:
|
||||
ht_away += p
|
||||
else:
|
||||
ht_draw += p
|
||||
|
||||
total = i + j
|
||||
total_goals_prob[total] = total_goals_prob.get(total, 0.0) + p
|
||||
|
||||
# Draw floor: düşük xG maçlarda beraberlik olasılığını minimum seviyeye çek
|
||||
if ht_draw < draw_floor:
|
||||
deficit = draw_floor - ht_draw
|
||||
ht_draw = draw_floor
|
||||
# Deficit'i home ve away'den orantılı düş
|
||||
total_ha = ht_home + ht_away
|
||||
if total_ha > 0:
|
||||
ht_home -= deficit * (ht_home / total_ha)
|
||||
ht_away -= deficit * (ht_away / total_ha)
|
||||
|
||||
# Normalize
|
||||
total_prob = ht_home + ht_draw + ht_away
|
||||
if total_prob > 0:
|
||||
ht_home /= total_prob
|
||||
ht_draw /= total_prob
|
||||
ht_away /= total_prob
|
||||
|
||||
# XGBoost Integration (HT 1X2 and HT/FT Models)
|
||||
w_xgb = self.config.get("xgboost.weight_ht", 0.60)
|
||||
xgb_ht_home, xgb_ht_draw, xgb_ht_away = None, None, None
|
||||
|
||||
if "ht_result" in ctx.xgboost_preds:
|
||||
probs = ctx.xgboost_preds["ht_result"]
|
||||
xgb_ht_home, xgb_ht_draw, xgb_ht_away = probs["home"], probs["draw"], probs["away"]
|
||||
elif "ht_ft" in ctx.xgboost_preds:
|
||||
# Fallback to HT/FT marginals
|
||||
htft_payload = ctx.xgboost_preds.get("ht_ft", {})
|
||||
probs = None
|
||||
if isinstance(htft_payload, dict):
|
||||
labels = ("1/1", "1/X", "1/2", "X/1", "X/X", "X/2", "2/1", "2/X", "2/2")
|
||||
if all(label in htft_payload for label in labels):
|
||||
probs = [float(htft_payload[label]) for label in labels]
|
||||
|
||||
if probs is None:
|
||||
probs = ctx.xgboost_preds.get("ht_ft_raw")
|
||||
if probs is not None and len(probs) == 9:
|
||||
xgb_ht_home = sum(probs[0:3])
|
||||
xgb_ht_draw = sum(probs[3:6])
|
||||
xgb_ht_away = sum(probs[6:9])
|
||||
|
||||
if xgb_ht_home is not None:
|
||||
ht_home = ht_home * (1 - w_xgb) + xgb_ht_home * w_xgb
|
||||
ht_draw = ht_draw * (1 - w_xgb) + xgb_ht_draw * w_xgb
|
||||
ht_away = ht_away * (1 - w_xgb) + xgb_ht_away * w_xgb
|
||||
|
||||
# Re-normalize
|
||||
total = ht_home + ht_draw + ht_away
|
||||
ht_home /= total
|
||||
ht_draw /= total
|
||||
ht_away /= total
|
||||
|
||||
# HT O/U 0.5
|
||||
ht_over_05 = 1.0 - math.exp(-ht_total_xg)
|
||||
if "ht_ou05" in ctx.xgboost_preds:
|
||||
w_xgb = self.config.get("xgboost.weight_ou", 0.60)
|
||||
xgb_ht_over_05 = float(ctx.xgboost_preds["ht_ou05"])
|
||||
ht_over_05 = ht_over_05 * (1 - w_xgb) + xgb_ht_over_05 * w_xgb
|
||||
|
||||
ht_over_05_min = self.config.get("half_time.ht_over_05_min", 0.20)
|
||||
ht_over_05_max = self.config.get("half_time.ht_over_05_max", 0.95)
|
||||
ht_over_05 = max(ht_over_05_min, min(ht_over_05_max, ht_over_05))
|
||||
|
||||
# HT O/U 1.5
|
||||
# P(total >= 2) = 1 - P(0) - P(1)
|
||||
ht_over_15 = sum(p for g, p in total_goals_prob.items() if g >= 2)
|
||||
if "ht_ou15" in ctx.xgboost_preds:
|
||||
w_xgb = self.config.get("xgboost.weight_ou", 0.60)
|
||||
xgb_ht_over_15 = float(ctx.xgboost_preds["ht_ou15"])
|
||||
ht_over_15 = ht_over_15 * (1 - w_xgb) + xgb_ht_over_15 * w_xgb
|
||||
|
||||
ht_over_15 = max(0.02, min(0.95, ht_over_15))
|
||||
|
||||
# Picks
|
||||
ht_probs = [(ht_home, "İY 1"), (ht_draw, "İY X"), (ht_away, "İY 2")]
|
||||
ht_sorted = sorted(ht_probs, key=lambda x: x[0], reverse=True)
|
||||
ht_pick = ht_sorted[0][1]
|
||||
ht_confidence = calc_confidence_3way(ht_sorted[0][0])
|
||||
|
||||
# HT O/U picks
|
||||
ht_ou_thr = self.config.get("half_time.ht_ou_threshold", 0.55)
|
||||
ht_ou_pick = "İY 0.5 Üst" if ht_over_05 > ht_ou_thr else "İY 0.5 Alt"
|
||||
ht_ou15_pick = "İY 1.5 Üst" if ht_over_15 > 0.45 else "İY 1.5 Alt"
|
||||
|
||||
return HalfTimePrediction(
|
||||
ht_home_prob=ht_home,
|
||||
ht_draw_prob=ht_draw,
|
||||
ht_away_prob=ht_away,
|
||||
ht_pick=ht_pick,
|
||||
ht_confidence=ht_confidence,
|
||||
ht_over_05_prob=ht_over_05,
|
||||
ht_under_05_prob=1.0 - ht_over_05,
|
||||
ht_over_15_prob=ht_over_15,
|
||||
ht_under_15_prob=1.0 - ht_over_15,
|
||||
ht_ou_pick=ht_ou_pick,
|
||||
ht_ou15_pick=ht_ou15_pick,
|
||||
ht_home_xg=ht_home_xg,
|
||||
ht_away_xg=ht_away_xg
|
||||
)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, List
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .confidence import calc_confidence_3way_with_agreement, calc_confidence_dc
|
||||
|
||||
@dataclass
|
||||
class MatchResultPrediction:
|
||||
ms_home_prob: float
|
||||
ms_draw_prob: float
|
||||
ms_away_prob: float
|
||||
ms_pick: str
|
||||
ms_confidence: float
|
||||
|
||||
dc_1x_prob: float
|
||||
dc_x2_prob: float
|
||||
dc_12_prob: float
|
||||
dc_pick: str
|
||||
dc_confidence: float
|
||||
|
||||
class MatchResultCalculator(BaseCalculator):
|
||||
|
||||
def _get_engine_winner(self, home_prob: float, draw_prob: float, away_prob: float) -> str:
|
||||
"""Determine which outcome an engine favors."""
|
||||
probs = {"1": home_prob, "X": draw_prob, "2": away_prob}
|
||||
return max(probs, key=probs.__getitem__)
|
||||
|
||||
def calculate(self, ctx: CalculationContext) -> MatchResultPrediction: # type: ignore[override]
|
||||
# Weights
|
||||
w_team = ctx.weights["team"]
|
||||
w_player = ctx.weights["player"]
|
||||
w_odds = ctx.weights["odds"]
|
||||
w_referee = ctx.weights["referee"]
|
||||
|
||||
# Engine predictions
|
||||
team_pred = ctx.team_pred
|
||||
odds_pred = ctx.odds_pred
|
||||
player_mods = ctx.player_mods
|
||||
referee_mods = ctx.referee_mods
|
||||
|
||||
# Weighted ensemble for 1X2
|
||||
ms_home = (
|
||||
team_pred.home_win_prob * w_team +
|
||||
odds_pred.market_home_prob * w_odds +
|
||||
team_pred.home_win_prob * player_mods["home_modifier"] * w_player +
|
||||
odds_pred.market_home_prob * referee_mods["home_modifier"] * w_referee
|
||||
)
|
||||
|
||||
ms_away = (
|
||||
team_pred.away_win_prob * w_team +
|
||||
odds_pred.market_away_prob * w_odds +
|
||||
team_pred.away_win_prob * player_mods["away_modifier"] * w_player +
|
||||
odds_pred.market_away_prob / referee_mods["home_modifier"] * w_referee
|
||||
)
|
||||
|
||||
ms_draw = 1.0 - ms_home - ms_away
|
||||
|
||||
# XGBoost Integration
|
||||
if "ms" in ctx.xgboost_preds:
|
||||
xgb_probs = ctx.xgboost_preds["ms"]
|
||||
w_xgb = self.config.get("xgboost.weight_ms", 0.70)
|
||||
w_heuristic = 1.0 - w_xgb
|
||||
|
||||
ms_home = ms_home * w_heuristic + xgb_probs["home"] * w_xgb
|
||||
ms_draw = ms_draw * w_heuristic + xgb_probs["draw"] * w_xgb
|
||||
ms_away = ms_away * w_heuristic + xgb_probs["away"] * w_xgb
|
||||
|
||||
# Re-normalize
|
||||
total = ms_home + ms_draw + ms_away
|
||||
ms_home /= total
|
||||
ms_draw /= total
|
||||
ms_away /= total
|
||||
|
||||
# Min draw probability clamping
|
||||
min_draw = self.config.get("match_result.min_draw_prob", 0.15)
|
||||
if ms_draw < min_draw:
|
||||
ms_draw = min_draw
|
||||
total = ms_home + ms_away + ms_draw
|
||||
ms_home /= total
|
||||
ms_away /= total
|
||||
ms_draw /= total
|
||||
|
||||
# Double Chance
|
||||
dc_1x = ms_home + ms_draw
|
||||
dc_x2 = ms_draw + ms_away
|
||||
dc_12 = ms_home + ms_away
|
||||
|
||||
# MS pick
|
||||
ms_probs = [(ms_home, "1"), (ms_draw, "X"), (ms_away, "2")]
|
||||
ms_sorted = sorted(ms_probs, key=lambda x: x[0], reverse=True)
|
||||
ms_pick = ms_sorted[0][1]
|
||||
|
||||
# === ENGINE AGREEMENT ===
|
||||
# Determine each engine's winner and calculate agreement ratio
|
||||
team_winner = self._get_engine_winner(
|
||||
team_pred.home_win_prob, team_pred.draw_prob, team_pred.away_win_prob
|
||||
)
|
||||
odds_winner = self._get_engine_winner(
|
||||
odds_pred.market_home_prob, odds_pred.market_draw_prob, odds_pred.market_away_prob
|
||||
)
|
||||
|
||||
# Player-modified: team probs * player modifiers
|
||||
player_adj_home = team_pred.home_win_prob * player_mods["home_modifier"]
|
||||
player_adj_away = team_pred.away_win_prob * player_mods["away_modifier"]
|
||||
player_adj_draw = max(0.01, 1.0 - player_adj_home - player_adj_away)
|
||||
player_winner = self._get_engine_winner(player_adj_home, player_adj_draw, player_adj_away)
|
||||
|
||||
# Referee-modified: odds probs * referee modifiers
|
||||
ref_adj_home = odds_pred.market_home_prob * referee_mods["home_modifier"]
|
||||
ref_adj_away = odds_pred.market_away_prob / referee_mods["home_modifier"]
|
||||
ref_adj_draw = max(0.01, 1.0 - ref_adj_home - ref_adj_away)
|
||||
referee_winner = self._get_engine_winner(ref_adj_home, ref_adj_draw, ref_adj_away)
|
||||
|
||||
# Count how many engines agree with final pick
|
||||
engines = [team_winner, odds_winner, player_winner, referee_winner]
|
||||
agreement_count = sum(1 for e in engines if e == ms_pick)
|
||||
agreement_ratio = agreement_count / len(engines)
|
||||
|
||||
# Confidence with agreement
|
||||
boost = self.config.get("confidence.agreement_boost", 1.3)
|
||||
penalty = self.config.get("confidence.disagreement_penalty", 0.7)
|
||||
ms_confidence = calc_confidence_3way_with_agreement(
|
||||
ms_sorted[0][0], agreement_ratio, boost, penalty
|
||||
)
|
||||
|
||||
# DC pick
|
||||
dc_probs = [(dc_1x, "1X"), (dc_x2, "X2"), (dc_12, "12")]
|
||||
dc_sorted = sorted(dc_probs, key=lambda x: x[0], reverse=True)
|
||||
dc_pick = dc_sorted[0][1]
|
||||
dc_confidence = calc_confidence_dc(dc_sorted[0][0])
|
||||
|
||||
return MatchResultPrediction(
|
||||
ms_home_prob=ms_home,
|
||||
ms_draw_prob=ms_draw,
|
||||
ms_away_prob=ms_away,
|
||||
ms_pick=ms_pick,
|
||||
ms_confidence=ms_confidence,
|
||||
dc_1x_prob=dc_1x,
|
||||
dc_x2_prob=dc_x2,
|
||||
dc_12_prob=dc_12,
|
||||
dc_pick=dc_pick,
|
||||
dc_confidence=dc_confidence
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Tuple
|
||||
|
||||
@dataclass
|
||||
class AnomalyResult:
|
||||
is_anomaly: bool
|
||||
side: str = ""
|
||||
severity: float = 0.0
|
||||
reason: str = ""
|
||||
|
||||
class OddsAnomalyDetector:
|
||||
"""
|
||||
Detects mismatches between bookmaker odds and underlying team metrics.
|
||||
A 'Bookmaker Trap' is when a team has very low odds (heavy favorite)
|
||||
but their xG/defense metrics are surprisingly poor.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
self.config = config
|
||||
|
||||
# Thresholds
|
||||
self.fav_odds_threshold = self.config.get("anomaly.fav_odds_threshold", 1.75)
|
||||
self.min_xg_for_fav = self.config.get("anomaly.min_xg_for_fav", 1.25)
|
||||
self.max_conceded_for_fav = self.config.get("anomaly.max_conceded_for_fav", 1.30)
|
||||
self.opp_min_xg_threat = self.config.get("anomaly.opp_min_xg_threat", 1.10)
|
||||
|
||||
def detect_trap(self,
|
||||
odds_data: Dict[str, float],
|
||||
home_xg: float,
|
||||
away_xg: float,
|
||||
home_conceded_avg: float,
|
||||
away_conceded_avg: float) -> tuple[bool, AnomalyResult]:
|
||||
"""
|
||||
Check if the match is a potential odds trap.
|
||||
Returns: (has_trap, AnomalyResult)
|
||||
"""
|
||||
ms_h = odds_data.get("ms_h", 0.0)
|
||||
ms_a = odds_data.get("ms_a", 0.0)
|
||||
|
||||
# Check Home Favorite Trap
|
||||
if 1.0 < ms_h <= self.fav_odds_threshold:
|
||||
# Home is favored. Check metrics.
|
||||
if home_xg < self.min_xg_for_fav and (away_xg > self.opp_min_xg_threat or home_conceded_avg > self.max_conceded_for_fav):
|
||||
severity = (self.fav_odds_threshold - ms_h) + (self.min_xg_for_fav - home_xg)
|
||||
reason = f"🚨 ODDS ANOMALY (TRAP): Home odds ({ms_h}) suspiciously low despite poor metrics (xG: {round(home_xg, 2)}, Conceded: {round(home_conceded_avg, 2)})"
|
||||
return True, AnomalyResult(True, "H", min(10.0, severity * 2), reason)
|
||||
|
||||
# Check Away Favorite Trap
|
||||
if 1.0 < ms_a <= self.fav_odds_threshold:
|
||||
# Away is favored. Check metrics
|
||||
if away_xg < self.min_xg_for_fav and (home_xg > self.opp_min_xg_threat or away_conceded_avg > self.max_conceded_for_fav):
|
||||
severity = (self.fav_odds_threshold - ms_a) + (self.min_xg_for_fav - away_xg)
|
||||
reason = f"🚨 ODDS ANOMALY (TRAP): Away odds ({ms_a}) suspiciously low despite poor metrics (xG: {round(away_xg, 2)}, Conceded: {round(away_conceded_avg, 2)})"
|
||||
return True, AnomalyResult(True, "A", min(10.0, severity * 2), reason)
|
||||
|
||||
return False, AnomalyResult(False)
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .match_result_calculator import MatchResultPrediction
|
||||
|
||||
|
||||
@dataclass
|
||||
class OtherMarketsPrediction:
|
||||
total_corners_pred: float
|
||||
corner_pick: str | None
|
||||
|
||||
total_cards_pred: float
|
||||
card_pick: str
|
||||
cards_over_prob: float
|
||||
cards_under_prob: float
|
||||
cards_confidence: float
|
||||
|
||||
handicap_pick: str
|
||||
handicap_home_prob: float
|
||||
handicap_draw_prob: float
|
||||
handicap_away_prob: float
|
||||
handicap_confidence: float
|
||||
|
||||
odd_even_pick: str
|
||||
odd_prob: float
|
||||
even_prob: float
|
||||
|
||||
|
||||
class OtherMarketsCalculator(BaseCalculator):
|
||||
def calculate( # type: ignore[override]
|
||||
self,
|
||||
ctx: CalculationContext,
|
||||
ms_result: MatchResultPrediction,
|
||||
) -> OtherMarketsPrediction:
|
||||
if "handicap_ms" in ctx.xgboost_preds:
|
||||
handicap_payload = ctx.xgboost_preds["handicap_ms"]
|
||||
handicap_home_prob = float(handicap_payload.get("h1", 0.33))
|
||||
handicap_draw_prob = float(handicap_payload.get("hx", 0.34))
|
||||
handicap_away_prob = float(handicap_payload.get("h2", 0.33))
|
||||
else:
|
||||
xg_diff = ctx.home_xg - ctx.away_xg
|
||||
threshold = float(self.config.get("handicap.xg_diff_threshold", 1.2))
|
||||
if xg_diff > threshold:
|
||||
handicap_home_prob, handicap_draw_prob, handicap_away_prob = 0.58, 0.24, 0.18
|
||||
elif xg_diff < -threshold:
|
||||
handicap_home_prob, handicap_draw_prob, handicap_away_prob = 0.18, 0.24, 0.58
|
||||
else:
|
||||
handicap_home_prob, handicap_draw_prob, handicap_away_prob = 0.28, 0.44, 0.28
|
||||
|
||||
handicap_confidence = max(
|
||||
handicap_home_prob,
|
||||
handicap_draw_prob,
|
||||
handicap_away_prob,
|
||||
) * 100.0
|
||||
if handicap_home_prob >= handicap_draw_prob and handicap_home_prob >= handicap_away_prob:
|
||||
handicap_pick = "H 1 (Ev -1)"
|
||||
elif handicap_away_prob >= handicap_home_prob and handicap_away_prob >= handicap_draw_prob:
|
||||
handicap_pick = "H 2 (Dep -1)"
|
||||
else:
|
||||
handicap_pick = "H 0 (Beraberlik)"
|
||||
|
||||
total_corners = 0.0
|
||||
corner_pick = None
|
||||
|
||||
card_line = float(self.config.get("cards.line", 4.5))
|
||||
if "cards_ou45" in ctx.xgboost_preds:
|
||||
cards_over_prob = float(ctx.xgboost_preds["cards_ou45"])
|
||||
total_cards = 5.0 if cards_over_prob > 0.50 else 3.5
|
||||
else:
|
||||
referee_average = float(ctx.referee_pred.avg_yellow_cards)
|
||||
match_heat = 1.0
|
||||
is_derby = bool(
|
||||
ctx.upset_factors.reasoning
|
||||
and "DERBY" in str(ctx.upset_factors.reasoning[0]),
|
||||
)
|
||||
if is_derby:
|
||||
match_heat = float(self.config.get("cards.derby_heat_factor", 1.3))
|
||||
total_cards = referee_average * match_heat
|
||||
delta = total_cards - card_line
|
||||
cards_over_prob = 1.0 / (1.0 + math.exp(-delta * 0.9))
|
||||
|
||||
cards_over_prob = max(0.02, min(0.98, cards_over_prob))
|
||||
cards_under_prob = 1.0 - cards_over_prob
|
||||
cards_confidence = max(cards_over_prob, cards_under_prob) * 100.0
|
||||
card_pick = f"{card_line} Ust" if cards_over_prob > 0.50 else f"{card_line} Alt"
|
||||
|
||||
lambda_total = ctx.total_xg
|
||||
even_prob = math.exp(-lambda_total) * math.cosh(lambda_total)
|
||||
if "odd_even" in ctx.xgboost_preds:
|
||||
xgb_weight = float(self.config.get("xgboost.weight_ou", 0.60))
|
||||
xgb_even_prob = float(ctx.xgboost_preds["odd_even"])
|
||||
even_prob = even_prob * (1 - xgb_weight) + xgb_even_prob * xgb_weight
|
||||
|
||||
even_prob = max(0.02, min(0.98, even_prob))
|
||||
odd_prob = 1.0 - even_prob
|
||||
odd_even_pick = "Cift" if even_prob > 0.5 else "Tek"
|
||||
|
||||
return OtherMarketsPrediction(
|
||||
total_corners_pred=total_corners,
|
||||
corner_pick=corner_pick,
|
||||
total_cards_pred=total_cards,
|
||||
card_pick=card_pick,
|
||||
cards_over_prob=cards_over_prob,
|
||||
cards_under_prob=cards_under_prob,
|
||||
cards_confidence=cards_confidence,
|
||||
handicap_pick=handicap_pick,
|
||||
handicap_home_prob=handicap_home_prob,
|
||||
handicap_draw_prob=handicap_draw_prob,
|
||||
handicap_away_prob=handicap_away_prob,
|
||||
handicap_confidence=handicap_confidence,
|
||||
odd_even_pick=odd_even_pick,
|
||||
odd_prob=odd_prob,
|
||||
even_prob=even_prob,
|
||||
)
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .confidence import calc_confidence_2way
|
||||
|
||||
@dataclass
|
||||
class OverUnderPrediction:
|
||||
over_15_prob: float
|
||||
under_15_prob: float
|
||||
ou15_pick: str
|
||||
ou15_confidence: float
|
||||
|
||||
over_25_prob: float
|
||||
under_25_prob: float
|
||||
ou25_pick: str
|
||||
ou25_confidence: float
|
||||
|
||||
over_35_prob: float
|
||||
under_35_prob: float
|
||||
ou35_pick: str
|
||||
ou35_confidence: float
|
||||
|
||||
btts_yes_prob: float
|
||||
btts_no_prob: float
|
||||
btts_pick: str
|
||||
btts_confidence: float
|
||||
|
||||
|
||||
class OverUnderCalculator(BaseCalculator):
|
||||
|
||||
def _poisson_pmf(self, k: int, lam: float) -> float:
|
||||
if lam <= 0:
|
||||
return 1.0 if k == 0 else 0.0
|
||||
return (lam ** k) * math.exp(-lam) / math.factorial(k)
|
||||
|
||||
def _poisson_ou_probs(self, home_xg: float, away_xg: float, grid_max: int = 6):
|
||||
"""Bivariate Poisson grid → O/U probabilities."""
|
||||
total_goals_prob = {} # total_goals → cumulative probability
|
||||
|
||||
for i in range(grid_max):
|
||||
for j in range(grid_max):
|
||||
p = self._poisson_pmf(i, home_xg) * self._poisson_pmf(j, away_xg)
|
||||
total = i + j
|
||||
total_goals_prob[total] = total_goals_prob.get(total, 0.0) + p
|
||||
|
||||
# Cumulative
|
||||
over_15 = sum(p for g, p in total_goals_prob.items() if g >= 2)
|
||||
over_25 = sum(p for g, p in total_goals_prob.items() if g >= 3)
|
||||
over_35 = sum(p for g, p in total_goals_prob.items() if g >= 4)
|
||||
|
||||
# BTTS: P(home >= 1) * P(away >= 1)
|
||||
p_home_0 = self._poisson_pmf(0, home_xg)
|
||||
p_away_0 = self._poisson_pmf(0, away_xg)
|
||||
btts_yes = (1 - p_home_0) * (1 - p_away_0)
|
||||
|
||||
return over_15, over_25, over_35, btts_yes
|
||||
|
||||
def calculate(self, ctx: CalculationContext) -> OverUnderPrediction: # type: ignore[override]
|
||||
odds_pred = ctx.odds_pred
|
||||
referee_mods = ctx.referee_mods
|
||||
|
||||
# Config
|
||||
prob_min = self.config.get("over_under.prob_min", 0.02)
|
||||
prob_max = self.config.get("over_under.prob_max", 0.98)
|
||||
blend_w = self.config.get("over_under.poisson_blend_weight", 0.4)
|
||||
grid_max = self.config.get("over_under.poisson_grid_max", 6)
|
||||
|
||||
ou15_thr = self.config.get("over_under.ou15_threshold", 0.55)
|
||||
ou25_thr = self.config.get("over_under.ou25_threshold", 0.52)
|
||||
ou35_thr = self.config.get("over_under.ou35_threshold", 0.48)
|
||||
btts_thr = self.config.get("over_under.btts_threshold", 0.58)
|
||||
|
||||
# 1. Poisson-based O/U from context xG (team + odds average)
|
||||
p_over_15, p_over_25, p_over_35, p_btts = self._poisson_ou_probs(
|
||||
ctx.home_xg, ctx.away_xg, int(grid_max)
|
||||
)
|
||||
|
||||
# 2. Odds-based O/U (from odds engine Poisson)
|
||||
o_over_15 = odds_pred.over_15_prob
|
||||
o_over_25 = odds_pred.over_25_prob
|
||||
o_over_35 = odds_pred.over_35_prob
|
||||
o_btts = odds_pred.btts_yes_prob
|
||||
|
||||
# 3. Blend: poisson xG + odds Poisson
|
||||
# Odds engine already uses Poisson internally, so keep blend weight low
|
||||
# to avoid double-counting. Use majority odds weight for established markets.
|
||||
over_15 = p_over_15 * blend_w + o_over_15 * (1 - blend_w)
|
||||
over_25 = p_over_25 * blend_w + o_over_25 * (1 - blend_w)
|
||||
over_35 = p_over_35 * blend_w + o_over_35 * (1 - blend_w)
|
||||
|
||||
# BTTS: keep primarily from odds engine (it was 63.6% accurate before)
|
||||
# Only a small Poisson contribution to cross-validate
|
||||
btts_blend = min(blend_w, 0.2)
|
||||
btts_yes = p_btts * btts_blend + o_btts * (1 - btts_blend)
|
||||
|
||||
# XGBoost Integration (High Weight)
|
||||
w_xgb = self.config.get("xgboost.weight_ou", 0.70)
|
||||
|
||||
if "ou25" in ctx.xgboost_preds:
|
||||
over_25 = over_25 * (1 - w_xgb) + ctx.xgboost_preds["ou25"] * w_xgb
|
||||
|
||||
if "ou15" in ctx.xgboost_preds:
|
||||
over_15 = over_15 * (1 - w_xgb) + ctx.xgboost_preds["ou15"] * w_xgb
|
||||
|
||||
if "ou35" in ctx.xgboost_preds:
|
||||
over_35 = over_35 * (1 - w_xgb) + ctx.xgboost_preds["ou35"] * w_xgb
|
||||
|
||||
# BTTS: lower XGBoost weight (was 0.70) — Poisson/odds fundamentals matter more
|
||||
w_xgb_btts = self.config.get("xgboost.weight_btts", 0.45)
|
||||
if "btts" in ctx.xgboost_preds:
|
||||
btts_yes = btts_yes * (1 - w_xgb_btts) + ctx.xgboost_preds["btts"] * w_xgb_btts
|
||||
|
||||
# 4. Referee modifier (only applied to goal totals, not BTTS)
|
||||
ou_mod = referee_mods.get("over_25_modifier", 1.0)
|
||||
over_15 *= ou_mod
|
||||
over_25 *= ou_mod
|
||||
over_35 *= ou_mod
|
||||
|
||||
# 5. Clamp
|
||||
over_15 = max(prob_min, min(prob_max, over_15))
|
||||
over_25 = max(prob_min, min(prob_max, over_25))
|
||||
over_35 = max(prob_min, min(prob_max, over_35))
|
||||
btts_yes = max(prob_min, min(prob_max, btts_yes))
|
||||
|
||||
# Picks & Confidence
|
||||
ou15_pick = "Üst 1.5" if over_15 > ou15_thr else "Alt 1.5"
|
||||
ou15_conf = calc_confidence_2way(over_15)
|
||||
|
||||
ou25_pick = "Üst 2.5" if over_25 > ou25_thr else "Alt 2.5"
|
||||
ou25_conf = calc_confidence_2way(over_25)
|
||||
|
||||
ou35_pick = "Üst 3.5" if over_35 > ou35_thr else "Alt 3.5"
|
||||
ou35_conf = calc_confidence_2way(over_35)
|
||||
|
||||
btts_pick = "KG Var" if btts_yes > btts_thr else "KG Yok"
|
||||
btts_conf = calc_confidence_2way(btts_yes)
|
||||
|
||||
# --- SAFE BTTS PENALTY (v2 — tighter thresholds) ---
|
||||
# Penalize BTTS confidence when fundamentals don't strongly support the pick.
|
||||
try:
|
||||
home_conceded = ctx.team_pred.raw_features.get("home_conceded_avg", 1.0)
|
||||
away_conceded = ctx.team_pred.raw_features.get("away_conceded_avg", 1.0)
|
||||
|
||||
if btts_pick == "KG Var":
|
||||
# "Var" needs BOTH teams to score → requires strong attack OR leaky defense
|
||||
# Penalty if either xG is low AND defenses are solid
|
||||
weak_attack = ctx.home_xg < 1.30 or ctx.away_xg < 1.15
|
||||
solid_defense = home_conceded < 1.15 or away_conceded < 1.15
|
||||
if weak_attack and solid_defense:
|
||||
btts_conf *= 0.3
|
||||
else: # KG Yok
|
||||
# "Yok" needs at least one team to fail scoring
|
||||
# Penalty if both have good xG AND both defenses are leaky
|
||||
if ctx.home_xg >= 1.30 and ctx.away_xg >= 1.15 and home_conceded >= 1.20 and away_conceded >= 1.20:
|
||||
btts_conf *= 0.3
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Safe BTTS Check Error: {e}")
|
||||
pass
|
||||
|
||||
|
||||
return OverUnderPrediction(
|
||||
over_15_prob=over_15, under_15_prob=1-over_15,
|
||||
ou15_pick=ou15_pick, ou15_confidence=ou15_conf,
|
||||
|
||||
over_25_prob=over_25, under_25_prob=1-over_25,
|
||||
ou25_pick=ou25_pick, ou25_confidence=ou25_conf,
|
||||
|
||||
over_35_prob=over_35, under_35_prob=1-over_35,
|
||||
ou35_pick=ou35_pick, ou35_confidence=ou35_conf,
|
||||
|
||||
btts_yes_prob=btts_yes, btts_no_prob=1-btts_yes,
|
||||
btts_pick=btts_pick, btts_confidence=btts_conf
|
||||
)
|
||||
Executable
+289
@@ -0,0 +1,289 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, List, Tuple
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .odds_anomaly_detector import OddsAnomalyDetector
|
||||
|
||||
@dataclass
|
||||
class RiskAnalysis:
|
||||
risk_score: float
|
||||
risk_level: str
|
||||
is_surprise_risk: bool
|
||||
reasons: List[str] = field(default_factory=list)
|
||||
surprise_type: str = ""
|
||||
risk_warnings: List[str] = field(default_factory=list)
|
||||
|
||||
class RiskAssessor(BaseCalculator):
|
||||
"""
|
||||
Assesses risk level of the match based on context and predictions.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
super().__init__(config)
|
||||
self.anomaly_detector = OddsAnomalyDetector(config)
|
||||
|
||||
@staticmethod
|
||||
def _safe_odd(value: Any) -> float:
|
||||
try:
|
||||
odd = float(value)
|
||||
return odd if odd > 1.01 else 0.0
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
def _favorite_profile_from_odds(self, odds_data: Dict[str, float]) -> Tuple[str, float]:
|
||||
"""
|
||||
Returns (favorite_side, gap_to_second_favorite).
|
||||
favorite_side: H, A, D, or U (unknown)
|
||||
"""
|
||||
ms_h = self._safe_odd((odds_data or {}).get("ms_h"))
|
||||
ms_d = self._safe_odd((odds_data or {}).get("ms_d"))
|
||||
ms_a = self._safe_odd((odds_data or {}).get("ms_a"))
|
||||
|
||||
candidates = [(side, odd) for side, odd in (("H", ms_h), ("D", ms_d), ("A", ms_a)) if odd > 0.0]
|
||||
if len(candidates) < 2:
|
||||
return "U", 0.0
|
||||
|
||||
candidates.sort(key=lambda item: item[1])
|
||||
favorite_side, favorite_odd = candidates[0]
|
||||
second_odd = candidates[1][1]
|
||||
return favorite_side, max(0.0, second_odd - favorite_odd)
|
||||
|
||||
def _dynamic_reversal_threshold(
|
||||
self,
|
||||
ctx: CalculationContext,
|
||||
top_label: str,
|
||||
) -> float:
|
||||
"""
|
||||
Dynamic threshold for reversal surprise flags.
|
||||
Lower threshold => easier to trigger surprise.
|
||||
"""
|
||||
base_threshold = float(self.config.get("risk.surprise_threshold", 0.20))
|
||||
sport_key = (ctx.sport or "football").lower().strip()
|
||||
is_top_league = bool(getattr(ctx, "is_top_league", False))
|
||||
|
||||
if not is_top_league:
|
||||
base_threshold = float(
|
||||
self.config.get("risk.surprise_threshold_non_top", base_threshold + 0.04),
|
||||
)
|
||||
|
||||
if sport_key == "basketball":
|
||||
if is_top_league:
|
||||
top_val = self.config.get("risk.surprise_threshold_basketball_top")
|
||||
if top_val is not None:
|
||||
return float(top_val)
|
||||
base_val = self.config.get("risk.surprise_threshold_basketball")
|
||||
return float(base_val) if base_val is not None else 0.30
|
||||
|
||||
non_top_val = self.config.get("risk.surprise_threshold_basketball_non_top")
|
||||
return float(non_top_val) if non_top_val is not None else 0.34
|
||||
|
||||
if top_label not in ("1/2", "2/1"):
|
||||
return base_threshold
|
||||
|
||||
winner_side = "A" if top_label == "1/2" else "H"
|
||||
favorite_side, gap = self._favorite_profile_from_odds(ctx.odds_data)
|
||||
|
||||
if is_top_league:
|
||||
top_fav = self.config.get("risk.surprise_threshold_favorite_reversal_top")
|
||||
if top_fav is not None:
|
||||
favorite_winner_threshold = float(top_fav)
|
||||
else:
|
||||
base_fav = self.config.get("risk.surprise_threshold_favorite_reversal")
|
||||
favorite_winner_threshold = float(base_fav) if base_fav is not None else 0.26
|
||||
|
||||
top_ud = self.config.get("risk.surprise_threshold_underdog_reversal_top")
|
||||
if top_ud is not None:
|
||||
underdog_winner_threshold = float(top_ud)
|
||||
else:
|
||||
base_ud = self.config.get("risk.surprise_threshold_underdog_reversal")
|
||||
underdog_winner_threshold = float(base_ud) if base_ud is not None else 0.20
|
||||
else:
|
||||
nt_fav = self.config.get("risk.surprise_threshold_favorite_reversal_non_top")
|
||||
favorite_winner_threshold = float(nt_fav) if nt_fav is not None else 0.30
|
||||
nt_ud = self.config.get("risk.surprise_threshold_underdog_reversal_non_top")
|
||||
underdog_winner_threshold = float(nt_ud) if nt_ud is not None else 0.24
|
||||
|
||||
gm = self.config.get("risk.htft_reversal_gap_medium")
|
||||
gap_medium = float(gm) if gm is not None else 0.50
|
||||
|
||||
gs = self.config.get("risk.htft_reversal_gap_strong")
|
||||
gap_strong = float(gs) if gs is not None else 1.00
|
||||
|
||||
if favorite_side in ("H", "A"):
|
||||
threshold = (
|
||||
favorite_winner_threshold
|
||||
if winner_side == favorite_side
|
||||
else underdog_winner_threshold
|
||||
)
|
||||
if winner_side != favorite_side and gap >= gap_strong:
|
||||
threshold += 0.03
|
||||
elif winner_side != favorite_side and gap >= gap_medium:
|
||||
threshold += 0.015
|
||||
return threshold
|
||||
|
||||
return base_threshold
|
||||
|
||||
def calculate(self, ctx: CalculationContext, ms_result: Any = None) -> RiskAnalysis: # type: ignore[override]
|
||||
"""
|
||||
Wrapper for assess_risk to match BaseCalculator interface but with extra arg.
|
||||
"""
|
||||
return self.assess_risk(ctx)
|
||||
|
||||
def assess_risk(self, ctx: CalculationContext) -> RiskAnalysis:
|
||||
"""
|
||||
Calculate risk score and level.
|
||||
Returns RiskAnalysis object.
|
||||
"""
|
||||
score = 5.0
|
||||
reasons = []
|
||||
is_surprise = ctx.is_surprise
|
||||
surprise_type = ""
|
||||
|
||||
# 1. League deviation (from UpsetEngine)
|
||||
if ctx.is_surprise:
|
||||
score += 2.0
|
||||
reasons.append("High Upset Potential detected by UpsetEngine")
|
||||
|
||||
# 1.5 Odds Anomaly Detection
|
||||
try:
|
||||
home_conceded = ctx.team_pred.raw_features.get("home_conceded_avg", 1.0)
|
||||
away_conceded = ctx.team_pred.raw_features.get("away_conceded_avg", 1.0)
|
||||
|
||||
has_anomaly, anomaly_res = self.anomaly_detector.detect_trap(
|
||||
ctx.odds_data,
|
||||
ctx.home_xg,
|
||||
ctx.away_xg,
|
||||
home_conceded,
|
||||
away_conceded
|
||||
)
|
||||
|
||||
if has_anomaly:
|
||||
is_surprise = True
|
||||
score += anomaly_res.severity + 2.0
|
||||
surprise_type = "Bookmaker Trap"
|
||||
reasons.append(anomaly_res.reason)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Odds Anomaly Detection Error: {e}")
|
||||
pass
|
||||
|
||||
# 2. HT/FT Surprise Hunter (XGBoost)
|
||||
# We look for 1/2 (idx 2) and 2/1 (idx 6) from the V20 HT/FT model
|
||||
if "ht_ft" in ctx.xgboost_preds:
|
||||
ht_ft = ctx.xgboost_preds["ht_ft"]
|
||||
valid_items = [(k, float(v)) for k, v in ht_ft.items() if isinstance(v, (int, float))]
|
||||
if valid_items:
|
||||
ranked = sorted(valid_items, key=lambda item: item[1], reverse=True)
|
||||
top_label, top_prob = ranked[0]
|
||||
second_prob = ranked[1][1] if len(ranked) > 1 else 0.0
|
||||
top_gap = top_prob - second_prob
|
||||
|
||||
threshold = self._dynamic_reversal_threshold(ctx, top_label)
|
||||
if getattr(ctx, "is_top_league", False):
|
||||
top_gap_val = self.config.get("risk.surprise_min_top_gap_top")
|
||||
if top_gap_val is not None:
|
||||
min_gap = float(top_gap_val)
|
||||
else:
|
||||
base_gap_val = self.config.get("risk.surprise_min_top_gap")
|
||||
min_gap = float(base_gap_val) if base_gap_val is not None else 0.02
|
||||
else:
|
||||
non_top_gap_val = self.config.get("risk.surprise_min_top_gap_non_top")
|
||||
min_gap = float(non_top_gap_val) if non_top_gap_val is not None else 0.03
|
||||
|
||||
# Trigger surprise only when reversal class is:
|
||||
# - top HT/FT outcome
|
||||
# - above dynamic threshold
|
||||
# - separated from second class with a minimum gap
|
||||
if top_label in ("1/2", "2/1") and top_prob > threshold and top_gap > min_gap:
|
||||
is_surprise = True
|
||||
score += 3.0
|
||||
surprise_type = f"{top_label} Reversal"
|
||||
reasons.append(
|
||||
f"🔥 Surprise Hunter: {top_label} potential ({round(top_prob*100, 1)}%, gap {round(top_gap*100, 1)}pp)"
|
||||
)
|
||||
|
||||
# NEW: Potential Upset Alert - even if reversal is not the top prediction
|
||||
# This catches cases like Bayern vs Augsburg where 1/2 was only 2% but it happened
|
||||
favorite_side, gap = self._favorite_profile_from_odds(ctx.odds_data)
|
||||
|
||||
# Get reversal probabilities
|
||||
prob_12 = float(ht_ft.get("1/2", 0))
|
||||
prob_21 = float(ht_ft.get("2/1", 0))
|
||||
|
||||
# DYNAMIC threshold based on odds - stronger favorite = lower threshold
|
||||
# When home odds are 1.30, even 1% reversal probability is significant
|
||||
base_threshold = float(self.config.get("risk.upset_alert_threshold", 0.05))
|
||||
|
||||
# Calculate dynamic threshold based on favorite strength
|
||||
if favorite_side == "H":
|
||||
home_odds = float(ctx.odds_data.get("ms_h", 2.0))
|
||||
# Stronger favorite (lower odds) = lower threshold
|
||||
# 1.20 odds -> 0.01 threshold, 1.50 odds -> 0.03 threshold, 2.0+ odds -> base threshold
|
||||
if home_odds <= 1.25:
|
||||
dynamic_threshold = 0.01 # 1% - extremely strong favorite
|
||||
elif home_odds <= 1.40:
|
||||
dynamic_threshold = 0.015 # 1.5% - very strong favorite
|
||||
elif home_odds <= 1.60:
|
||||
dynamic_threshold = 0.02 # 2% - strong favorite
|
||||
elif home_odds < 2.00:
|
||||
dynamic_threshold = 0.03 # 3% - moderate favorite
|
||||
else:
|
||||
dynamic_threshold = base_threshold
|
||||
elif favorite_side == "A":
|
||||
away_odds = float(ctx.odds_data.get("ms_a", 2.0))
|
||||
if away_odds <= 1.25:
|
||||
dynamic_threshold = 0.01
|
||||
elif away_odds <= 1.40:
|
||||
dynamic_threshold = 0.015
|
||||
elif away_odds <= 1.60:
|
||||
dynamic_threshold = 0.02
|
||||
elif away_odds < 2.00:
|
||||
dynamic_threshold = 0.03
|
||||
else:
|
||||
dynamic_threshold = base_threshold
|
||||
else:
|
||||
dynamic_threshold = base_threshold
|
||||
|
||||
# Check for potential upset based on favorite
|
||||
if favorite_side == "H" and prob_12 > dynamic_threshold:
|
||||
# Home favorite, but 1/2 (home leads HT, away wins FT) has potential
|
||||
is_surprise = True
|
||||
score += 2.0
|
||||
surprise_type = "1/2 Potential Upset"
|
||||
reasons.append(
|
||||
f"⚠️ UPSET ALERT: Home favorite ({ctx.odds_data.get('ms_h', 'N/A')}) but 1/2 reversal risk ({round(prob_12*100, 1)}% > {round(dynamic_threshold*100, 1)}% threshold)"
|
||||
)
|
||||
elif favorite_side == "A" and prob_21 > dynamic_threshold:
|
||||
# Away favorite, but 2/1 (away leads HT, home wins FT) has potential
|
||||
is_surprise = True
|
||||
score += 2.0
|
||||
surprise_type = "2/1 Potential Upset"
|
||||
reasons.append(
|
||||
f"⚠️ UPSET ALERT: Away favorite ({ctx.odds_data.get('ms_a', 'N/A')}) but 2/1 reversal risk ({round(prob_21*100, 1)}% > {round(dynamic_threshold*100, 1)}% threshold)"
|
||||
)
|
||||
elif gap > 0.5 and (prob_12 > dynamic_threshold or prob_21 > dynamic_threshold):
|
||||
# Strong favorite (big odds gap) with any reversal potential
|
||||
reversal_type = "1/2" if prob_12 > prob_21 else "2/1"
|
||||
reversal_prob = max(prob_12, prob_21)
|
||||
is_surprise = True
|
||||
score += 1.5
|
||||
surprise_type = f"{reversal_type} Potential Upset"
|
||||
reasons.append(
|
||||
f"⚠️ UPSET ALERT: Strong favorite (gap {round(gap, 2)}) with {reversal_type} risk ({round(reversal_prob*100, 1)}%)"
|
||||
)
|
||||
|
||||
# Determine level
|
||||
if score < 4.0:
|
||||
level = "LOW"
|
||||
elif score < 7.0:
|
||||
level = "MEDIUM"
|
||||
elif score < 9.0:
|
||||
level = "HIGH"
|
||||
else:
|
||||
level = "EXTREME"
|
||||
|
||||
return RiskAnalysis(
|
||||
risk_score=score,
|
||||
risk_level=level,
|
||||
is_surprise_risk=is_surprise,
|
||||
surprise_type=surprise_type,
|
||||
reasons=reasons
|
||||
)
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import os
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import xgboost as xgb
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
import math
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .confidence import calc_confidence_3way, calc_confidence_dc
|
||||
from .match_result_calculator import MatchResultPrediction
|
||||
|
||||
@dataclass
|
||||
class ScorePrediction:
|
||||
predicted_ft_score: str
|
||||
predicted_ht_score: str
|
||||
ft_scores_top5: List[Dict]
|
||||
|
||||
# Reconciled MS/DC predictions (can be updated here)
|
||||
reconciled_ms: Optional[MatchResultPrediction] = None
|
||||
|
||||
class ScoreCalculator(BaseCalculator):
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
super().__init__(config)
|
||||
self.xgb_home = None
|
||||
self.xgb_away = None
|
||||
self.xgb_ht_home = None
|
||||
self.xgb_ht_away = None
|
||||
self.scaler = None # If used
|
||||
self.features = []
|
||||
self._load_model()
|
||||
|
||||
def _load_model(self):
|
||||
try:
|
||||
model_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "models", "xgb_score.pkl")
|
||||
if os.path.exists(model_path):
|
||||
with open(model_path, "rb") as f:
|
||||
data = pickle.load(f)
|
||||
# Handle both dictionary and direct model formats (just in case)
|
||||
if isinstance(data, dict):
|
||||
self.xgb_home = data.get("home_model")
|
||||
self.xgb_away = data.get("away_model")
|
||||
self.xgb_ht_home = data.get("ht_home_model")
|
||||
self.xgb_ht_away = data.get("ht_away_model")
|
||||
self.features = data.get("features", [])
|
||||
else:
|
||||
print("⚠️ Unexpected XGB score model format.")
|
||||
print("✅ XGBoost Score Model loaded.")
|
||||
else:
|
||||
print(f"⚠️ XGBoost Score Model not found at {model_path}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading XGBoost Score Model: {e}")
|
||||
|
||||
def _poisson_pmf(self, k, lam):
|
||||
"""Poisson probability mass function."""
|
||||
if lam <= 0:
|
||||
return 1.0 if k == 0 else 0.0
|
||||
return (lam ** k) * math.exp(-lam) / math.factorial(k)
|
||||
|
||||
def calculate(self, ctx: CalculationContext, ms_result: MatchResultPrediction) -> ScorePrediction: # type: ignore[override]
|
||||
predicted_ht = None
|
||||
# Default Lambdas (fallback)
|
||||
lambda_home = max(0.5, ctx.home_xg)
|
||||
lambda_away = max(0.5, ctx.away_xg)
|
||||
|
||||
# --- XGBOOST PREDICTION ---
|
||||
if self.xgb_home and self.xgb_away and hasattr(ctx.team_pred, "raw_features"):
|
||||
try:
|
||||
# 1. Prepare Features
|
||||
# We need to map ctx data to self.features list columns
|
||||
raw = ctx.team_pred.raw_features
|
||||
odds = ctx.odds_data or {}
|
||||
|
||||
# Use unified feature adapter for exact 56-feature sync
|
||||
from features.feature_adapter import get_feature_adapter
|
||||
df_input = get_feature_adapter().get_features(ctx)
|
||||
|
||||
# Predict FT
|
||||
pred_h = self.xgb_home.predict(df_input)[0]
|
||||
pred_a = self.xgb_away.predict(df_input)[0]
|
||||
|
||||
# Predict HT (if available)
|
||||
if self.xgb_ht_home and self.xgb_ht_away:
|
||||
pred_ht_h = self.xgb_ht_home.predict(df_input)[0]
|
||||
pred_ht_a = self.xgb_ht_away.predict(df_input)[0]
|
||||
|
||||
# Clamp HT predictions (min 0, and shouldn't exceed FT in logic, but models are independent)
|
||||
# We trust the model but ensure sanity (HT <= FT is hard to enforce without joint training, but usually holds)
|
||||
ht_h_val = max(0.0, float(pred_ht_h))
|
||||
ht_a_val = max(0.0, float(pred_ht_a))
|
||||
|
||||
predicted_ht = f"{round(ht_h_val)}-{round(ht_a_val)}"
|
||||
else:
|
||||
# Fallback if HT models missing
|
||||
ht_h_val = max(0.0, float(pred_h) * 0.42)
|
||||
ht_a_val = max(0.0, float(pred_a) * 0.42)
|
||||
predicted_ht = f"{round(ht_h_val)}-{round(ht_a_val)}"
|
||||
|
||||
# Update lambdas with ML predictions
|
||||
lambda_home = max(0.1, min(6.0, float(pred_h)))
|
||||
lambda_away = max(0.1, min(6.0, float(pred_a)))
|
||||
|
||||
# Store raw XGB preds in context
|
||||
ctx.xgboost_preds["score"] = {
|
||||
"home": lambda_home,
|
||||
"away": lambda_away,
|
||||
"ht_home": ht_h_val,
|
||||
"ht_away": ht_a_val
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ XGBoost Score Prediction failed: {e}. Falling back to Poisson xG.")
|
||||
# Fallback to current simple logic if ML fails
|
||||
predicted_ht = f"{round(lambda_home * 0.42)}-{round(lambda_away * 0.42)}"
|
||||
|
||||
# --- POISSON GRID GENERATION ---
|
||||
# Now use lambda_home/away (either ML or fallback) to generate grid
|
||||
score_probs = {}
|
||||
grid_max = self.config.get("score.poisson_grid_max", 7)
|
||||
|
||||
for i in range(grid_max):
|
||||
for j in range(grid_max):
|
||||
p = self._poisson_pmf(i, lambda_home) * self._poisson_pmf(j, lambda_away)
|
||||
score_probs[f"{i}-{j}"] = round(p * 100, 2)
|
||||
|
||||
sorted_scores = sorted(score_probs.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
# --- DERIVE MS PROBS FROM SCORES (CONSISTENCY CHECK) ---
|
||||
poisson_ms_home = sum(p for s, p in score_probs.items()
|
||||
for h, a in [s.split("-")] if int(h) > int(a))
|
||||
poisson_ms_away = sum(p for s, p in score_probs.items()
|
||||
for h, a in [s.split("-")] if int(h) < int(a))
|
||||
poisson_ms_draw = sum(p for s, p in score_probs.items()
|
||||
for h, a in [s.split("-")] if int(h) == int(a))
|
||||
|
||||
# Normalize
|
||||
poisson_total = poisson_ms_home + poisson_ms_away + poisson_ms_draw
|
||||
if poisson_total > 0:
|
||||
poisson_ms_home /= poisson_total
|
||||
poisson_ms_away /= poisson_total
|
||||
poisson_ms_draw /= poisson_total
|
||||
|
||||
# --- HYBRID RECONCILIATION ---
|
||||
|
||||
threshold = self.config.get("score.ms_confidence_threshold", 15.0)
|
||||
reconciled_result = ms_result
|
||||
|
||||
# If original confidence is low, trust new Score Model more
|
||||
if ms_result.ms_confidence < threshold:
|
||||
poisson_probs = [(poisson_ms_home, "1"), (poisson_ms_draw, "X"), (poisson_ms_away, "2")]
|
||||
poisson_sorted = sorted(poisson_probs, key=lambda x: x[0], reverse=True)
|
||||
|
||||
new_ms_pick = poisson_sorted[0][1]
|
||||
new_ms_conf = calc_confidence_3way(poisson_sorted[0][0])
|
||||
|
||||
# Recalculate DC
|
||||
dc_1x = poisson_ms_home + poisson_ms_draw
|
||||
dc_x2 = poisson_ms_draw + poisson_ms_away
|
||||
dc_12 = poisson_ms_home + poisson_ms_away
|
||||
|
||||
dc_probs = [(dc_1x, "1X"), (dc_x2, "X2"), (dc_12, "12")]
|
||||
dc_sorted = sorted(dc_probs, key=lambda x: x[0], reverse=True)
|
||||
new_dc_pick = dc_sorted[0][1]
|
||||
new_dc_conf = calc_confidence_dc(dc_sorted[0][0])
|
||||
|
||||
reconciled_result = MatchResultPrediction(
|
||||
ms_home_prob=poisson_ms_home,
|
||||
ms_draw_prob=poisson_ms_draw,
|
||||
ms_away_prob=poisson_ms_away,
|
||||
ms_pick=new_ms_pick,
|
||||
ms_confidence=new_ms_conf,
|
||||
dc_1x_prob=dc_1x,
|
||||
dc_x2_prob=dc_x2,
|
||||
dc_12_prob=dc_12,
|
||||
dc_pick=new_dc_pick,
|
||||
dc_confidence=new_dc_conf
|
||||
)
|
||||
|
||||
# Select best score that matches MS Pick
|
||||
# NEW LOGIC: We trust XGBoost/Poisson top score over generic MS Pick if MS Confidence is low.
|
||||
# Otherwise, we filter the grid to match the MS pick.
|
||||
ms_pick = reconciled_result.ms_pick
|
||||
|
||||
def _score_matches_ms(score_str, pick):
|
||||
h, a = map(int, score_str.split("-"))
|
||||
if pick == "1": return h > a
|
||||
if pick == "2": return h < a
|
||||
return h == a
|
||||
|
||||
matching_scores = [(s, p) for s, p in sorted_scores if _score_matches_ms(s, ms_pick)]
|
||||
|
||||
# Primary Prediction Strategy:
|
||||
# If MS pick is highly confident, enforce it.
|
||||
# But if the absolute best score in the grid contradicts it and has a high probability (e.g. >10%), trust the score model directly.
|
||||
top_overall_score, top_overall_prob = sorted_scores[0]
|
||||
|
||||
if matching_scores and not (top_overall_prob > 12.0 and not _score_matches_ms(top_overall_score, ms_pick)):
|
||||
predicted_ft = matching_scores[0][0]
|
||||
else:
|
||||
predicted_ft = top_overall_score
|
||||
|
||||
# If we didn't calculate HT via ML (exception case), do it now
|
||||
if predicted_ht is None:
|
||||
ft_to_ht = self.config.get("half_time.ft_to_ht_ratio", 0.42)
|
||||
ht_h = round(lambda_home * ft_to_ht)
|
||||
ht_a = round(lambda_away * ft_to_ht)
|
||||
predicted_ht = f"{ht_h}-{ht_a}"
|
||||
|
||||
# --- CONSISTENCY CHECK ---
|
||||
# Ensure HT score <= FT score
|
||||
try:
|
||||
ft_h, ft_a = map(int, predicted_ft.split("-"))
|
||||
ht_h, ht_a = map(int, predicted_ht.split("-"))
|
||||
|
||||
# Clamp HT values
|
||||
ht_h = min(ht_h, ft_h)
|
||||
ht_a = min(ht_a, ft_a)
|
||||
|
||||
predicted_ht = f"{ht_h}-{ht_a}"
|
||||
except ValueError:
|
||||
pass # Malformed score string, ignore correction
|
||||
|
||||
ft_scores = [{"score": s, "prob": p} for s, p in sorted_scores[:5]]
|
||||
|
||||
return ScorePrediction(
|
||||
predicted_ft_score=predicted_ft,
|
||||
predicted_ht_score=predicted_ht,
|
||||
ft_scores_top5=ft_scores,
|
||||
reconciled_ms=reconciled_result
|
||||
)
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
# ai-engine/core/engines/__init__.py
|
||||
"""
|
||||
Prediction Engines
|
||||
"""
|
||||
|
||||
from .player_predictor import PlayerPredictorEngine, get_player_predictor
|
||||
|
||||
__all__ = [
|
||||
"PlayerPredictorEngine", "get_player_predictor",
|
||||
]
|
||||
Executable
+359
@@ -0,0 +1,359 @@
|
||||
"""
|
||||
Player Predictor Engine - V20 Ensemble Component
|
||||
Analyzes squad quality, key players, and missing player impact.
|
||||
|
||||
Weight: 25% in ensemble
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Optional, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from features.squad_analysis_engine import get_squad_analysis_engine
|
||||
from features.sidelined_analyzer import get_sidelined_analyzer
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerPrediction:
|
||||
"""Player engine prediction output.
|
||||
|
||||
IMPORTANT: squad_quality uses the SAME composite formula as
|
||||
extract_training_data.py so that inference values match the
|
||||
distribution the model was trained on (~3-36 range).
|
||||
"""
|
||||
home_squad_quality: float = 12.0
|
||||
away_squad_quality: float = 12.0
|
||||
squad_diff: float = 0.0
|
||||
home_key_players: int = 0
|
||||
away_key_players: int = 0
|
||||
home_missing_impact: float = 0.0
|
||||
away_missing_impact: float = 0.0
|
||||
home_goals_form: int = 0
|
||||
away_goals_form: int = 0
|
||||
home_lineup_goals_per90: float = 0.0
|
||||
away_lineup_goals_per90: float = 0.0
|
||||
home_lineup_assists_per90: float = 0.0
|
||||
away_lineup_assists_per90: float = 0.0
|
||||
home_squad_continuity: float = 0.5
|
||||
away_squad_continuity: float = 0.5
|
||||
home_top_scorer_form: int = 0
|
||||
away_top_scorer_form: int = 0
|
||||
home_avg_player_exp: float = 0.0
|
||||
away_avg_player_exp: float = 0.0
|
||||
home_goals_diversity: float = 0.0
|
||||
away_goals_diversity: float = 0.0
|
||||
lineup_available: bool = False
|
||||
confidence: float = 0.0
|
||||
|
||||
|
||||
class PlayerPredictorEngine:
|
||||
"""
|
||||
Player/Squad-based prediction engine.
|
||||
|
||||
Analyzes:
|
||||
- Starting 11 quality
|
||||
- Key player availability (top scorers)
|
||||
- Missing player impact
|
||||
- Recent goalscoring form per player
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.squad_engine = get_squad_analysis_engine()
|
||||
self.sidelined_analyzer = get_sidelined_analyzer()
|
||||
print("✅ PlayerPredictorEngine initialized")
|
||||
|
||||
def predict(self,
|
||||
match_id: str,
|
||||
home_team_id: str,
|
||||
away_team_id: str,
|
||||
home_lineup: Optional[List[str]] = None,
|
||||
away_lineup: Optional[List[str]] = None,
|
||||
sidelined_data: Optional[Dict] = None) -> PlayerPrediction:
|
||||
"""
|
||||
Generate player-based prediction.
|
||||
|
||||
Args:
|
||||
match_id: Match ID for lineup lookup
|
||||
home_team_id: Home team ID
|
||||
away_team_id: Away team ID
|
||||
home_lineup: Optional list of home player IDs
|
||||
away_lineup: Optional list of away player IDs
|
||||
|
||||
Returns:
|
||||
PlayerPrediction with squad analysis
|
||||
"""
|
||||
|
||||
# Get squad features
|
||||
home_analysis = None
|
||||
away_analysis = None
|
||||
if home_lineup and away_lineup:
|
||||
home_analysis = self.squad_engine.analyze_squad_from_list(
|
||||
home_lineup, home_team_id
|
||||
)
|
||||
away_analysis = self.squad_engine.analyze_squad_from_list(
|
||||
away_lineup, away_team_id
|
||||
)
|
||||
lineup_available = True
|
||||
features = {
|
||||
"home_starting_11": home_analysis.starting_count or 11,
|
||||
"home_goals_last_5": home_analysis.total_goals_last_5,
|
||||
"home_assists_last_5": home_analysis.total_assists_last_5,
|
||||
"home_key_players": home_analysis.key_players_count,
|
||||
"home_forwards": home_analysis.forward_count or 2,
|
||||
"away_starting_11": away_analysis.starting_count or 11,
|
||||
"away_goals_last_5": away_analysis.total_goals_last_5,
|
||||
"away_assists_last_5": away_analysis.total_assists_last_5,
|
||||
"away_key_players": away_analysis.key_players_count,
|
||||
"away_forwards": away_analysis.forward_count or 2,
|
||||
}
|
||||
elif match_id:
|
||||
try:
|
||||
features = self.squad_engine.get_features(
|
||||
match_id, home_team_id, away_team_id
|
||||
)
|
||||
lineup_available = (
|
||||
features.get("home_starting_11", 0) >= 11 and
|
||||
features.get("away_starting_11", 0) >= 11
|
||||
)
|
||||
except Exception:
|
||||
features = self.squad_engine.get_features_without_match(
|
||||
home_team_id, away_team_id
|
||||
)
|
||||
lineup_available = False
|
||||
else:
|
||||
features = self.squad_engine.get_features_without_match(
|
||||
home_team_id, away_team_id
|
||||
)
|
||||
lineup_available = False
|
||||
|
||||
home_goals = int(features.get("home_goals_last_5", 0))
|
||||
away_goals = int(features.get("away_goals_last_5", 0))
|
||||
home_key = int(features.get("home_key_players", 0))
|
||||
away_key = int(features.get("away_key_players", 0))
|
||||
home_starting = features.get("home_starting_11", 11)
|
||||
away_starting = features.get("away_starting_11", 11)
|
||||
home_fwd = features.get("home_forwards", 2)
|
||||
away_fwd = features.get("away_forwards", 2)
|
||||
|
||||
# Squad quality — matches V25 extract_training_data.py:579
|
||||
home_quality = home_starting * 0.3 + home_key * 3.0 + home_fwd * 1.5
|
||||
away_quality = away_starting * 0.3 + away_key * 3.0 + away_fwd * 1.5
|
||||
squad_diff = home_quality - away_quality
|
||||
|
||||
# Missing player impact
|
||||
if sidelined_data:
|
||||
home_impact, away_impact = self.sidelined_analyzer.analyze_match(sidelined_data)
|
||||
home_missing = min(1.0, max(0.0, home_impact.impact_score))
|
||||
away_missing = min(1.0, max(0.0, away_impact.impact_score))
|
||||
sidelined_available = True
|
||||
else:
|
||||
expected_xi = 11
|
||||
actual_home_xi = features.get("home_starting_11", 11)
|
||||
actual_away_xi = features.get("away_starting_11", 11)
|
||||
home_missing = (expected_xi - actual_home_xi) / expected_xi if actual_home_xi < expected_xi else 0
|
||||
away_missing = (expected_xi - actual_away_xi) / expected_xi if actual_away_xi < expected_xi else 0
|
||||
sidelined_available = False
|
||||
|
||||
# Player-level features (matches extract_training_data.py:594-650)
|
||||
player_feats = self._compute_player_level_features(
|
||||
home_lineup or [], away_lineup or [],
|
||||
home_team_id, away_team_id,
|
||||
home_analysis, away_analysis,
|
||||
)
|
||||
|
||||
confidence = 70.0 if lineup_available else 35.0
|
||||
if home_goals + away_goals > 10:
|
||||
confidence += 15
|
||||
if sidelined_available:
|
||||
confidence += self.sidelined_analyzer.config.get("sidelined.confidence_boost", 10)
|
||||
if not lineup_available:
|
||||
confidence -= 5.0
|
||||
|
||||
return PlayerPrediction(
|
||||
home_squad_quality=home_quality,
|
||||
away_squad_quality=away_quality,
|
||||
squad_diff=squad_diff,
|
||||
home_key_players=home_key,
|
||||
away_key_players=away_key,
|
||||
home_missing_impact=home_missing,
|
||||
away_missing_impact=away_missing,
|
||||
home_goals_form=home_goals,
|
||||
away_goals_form=away_goals,
|
||||
home_lineup_goals_per90=player_feats['home_lineup_goals_per90'],
|
||||
away_lineup_goals_per90=player_feats['away_lineup_goals_per90'],
|
||||
home_lineup_assists_per90=player_feats['home_lineup_assists_per90'],
|
||||
away_lineup_assists_per90=player_feats['away_lineup_assists_per90'],
|
||||
home_squad_continuity=player_feats['home_squad_continuity'],
|
||||
away_squad_continuity=player_feats['away_squad_continuity'],
|
||||
home_top_scorer_form=player_feats['home_top_scorer_form'],
|
||||
away_top_scorer_form=player_feats['away_top_scorer_form'],
|
||||
home_avg_player_exp=player_feats['home_avg_player_exp'],
|
||||
away_avg_player_exp=player_feats['away_avg_player_exp'],
|
||||
home_goals_diversity=player_feats['home_goals_diversity'],
|
||||
away_goals_diversity=player_feats['away_goals_diversity'],
|
||||
lineup_available=lineup_available,
|
||||
confidence=max(5.0, confidence)
|
||||
)
|
||||
|
||||
def _compute_player_level_features(
|
||||
self,
|
||||
home_lineup: List[str],
|
||||
away_lineup: List[str],
|
||||
home_team_id: str,
|
||||
away_team_id: str,
|
||||
home_analysis,
|
||||
away_analysis,
|
||||
) -> Dict[str, float]:
|
||||
defaults = {
|
||||
'home_lineup_goals_per90': 0.0, 'away_lineup_goals_per90': 0.0,
|
||||
'home_lineup_assists_per90': 0.0, 'away_lineup_assists_per90': 0.0,
|
||||
'home_squad_continuity': 0.5, 'away_squad_continuity': 0.5,
|
||||
'home_top_scorer_form': 0, 'away_top_scorer_form': 0,
|
||||
'home_avg_player_exp': 0.0, 'away_avg_player_exp': 0.0,
|
||||
'home_goals_diversity': 0.0, 'away_goals_diversity': 0.0,
|
||||
}
|
||||
conn = self.squad_engine.get_conn()
|
||||
if conn is None:
|
||||
return defaults
|
||||
|
||||
try:
|
||||
from psycopg2.extras import RealDictCursor
|
||||
result = {}
|
||||
for prefix, lineup, team_id in [
|
||||
('home', home_lineup, home_team_id),
|
||||
('away', away_lineup, away_team_id),
|
||||
]:
|
||||
if not lineup:
|
||||
for k in ('lineup_goals_per90', 'lineup_assists_per90',
|
||||
'squad_continuity', 'top_scorer_form',
|
||||
'avg_player_exp', 'goals_diversity'):
|
||||
result[f'{prefix}_{k}'] = defaults[f'{prefix}_{k}']
|
||||
continue
|
||||
|
||||
g90, a90, total_exp = 0.0, 0.0, 0
|
||||
best_scorer_total, best_scorer_id = 0, None
|
||||
scorers_in_lineup = 0
|
||||
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
for pid in lineup:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(*) as starts,
|
||||
COALESCE(SUM(CASE WHEN e.event_type = 'goal'
|
||||
AND (e.event_subtype IS NULL OR e.event_subtype NOT ILIKE '%%penaltı kaçırma%%')
|
||||
THEN 1 ELSE 0 END), 0) as goals,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_events
|
||||
WHERE assist_player_id = %s), 0) as assists
|
||||
FROM match_player_participation mpp
|
||||
LEFT JOIN match_player_events e
|
||||
ON e.match_id = mpp.match_id AND e.player_id = mpp.player_id
|
||||
WHERE mpp.player_id = %s AND mpp.is_starting = true
|
||||
""", (pid, pid))
|
||||
row = cur.fetchone()
|
||||
if not row or not row['starts']:
|
||||
continue
|
||||
starts = row['starts']
|
||||
goals = row['goals'] or 0
|
||||
assists = row['assists'] or 0
|
||||
g90 += goals / starts
|
||||
a90 += assists / starts
|
||||
total_exp += starts
|
||||
if goals > 0:
|
||||
scorers_in_lineup += 1
|
||||
if goals > best_scorer_total:
|
||||
best_scorer_total = goals
|
||||
best_scorer_id = pid
|
||||
|
||||
n_st = len(lineup) or 1
|
||||
|
||||
# Top scorer recent form (goals in last 5 starts)
|
||||
top_scorer_form = 0
|
||||
if best_scorer_id:
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as goals
|
||||
FROM match_player_events mpe
|
||||
WHERE mpe.player_id = %s AND mpe.event_type = 'goal'
|
||||
AND mpe.match_id IN (
|
||||
SELECT match_id FROM match_player_participation
|
||||
WHERE player_id = %s AND is_starting = true
|
||||
ORDER BY match_id DESC LIMIT 5
|
||||
)
|
||||
""", (best_scorer_id, best_scorer_id))
|
||||
tsf_row = cur.fetchone()
|
||||
if tsf_row:
|
||||
top_scorer_form = tsf_row['goals'] or 0
|
||||
|
||||
# Squad continuity (overlap with previous match lineup)
|
||||
squad_continuity = 0.5
|
||||
cur.execute("""
|
||||
SELECT mpp.player_id
|
||||
FROM match_player_participation mpp
|
||||
JOIN matches m ON mpp.match_id = m.id
|
||||
WHERE mpp.team_id = %s AND mpp.is_starting = true
|
||||
AND m.status = 'FT'
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 11
|
||||
""", (team_id,))
|
||||
prev_starters = {r['player_id'] for r in cur.fetchall()}
|
||||
if prev_starters:
|
||||
overlap = len(set(lineup) & prev_starters)
|
||||
squad_continuity = overlap / n_st
|
||||
|
||||
result[f'{prefix}_lineup_goals_per90'] = round(g90, 3)
|
||||
result[f'{prefix}_lineup_assists_per90'] = round(a90, 3)
|
||||
result[f'{prefix}_squad_continuity'] = round(squad_continuity, 3)
|
||||
result[f'{prefix}_top_scorer_form'] = top_scorer_form
|
||||
result[f'{prefix}_avg_player_exp'] = round(total_exp / n_st, 1)
|
||||
result[f'{prefix}_goals_diversity'] = round(scorers_in_lineup / n_st, 3)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"[PlayerPredictor] Player-level features failed: {e}")
|
||||
return defaults
|
||||
|
||||
def get_1x2_modifier(self, prediction: PlayerPrediction) -> Dict[str, float]:
|
||||
"""
|
||||
Calculate 1X2 probability modifiers based on squad analysis.
|
||||
|
||||
Returns modifiers to apply to base probabilities.
|
||||
squad_diff is in training scale (~-33 to +33), normalize to -1..+1.
|
||||
"""
|
||||
diff = prediction.squad_diff / 33.0 # training-scale normalisation
|
||||
diff = max(-1.0, min(1.0, diff)) # clamp
|
||||
|
||||
return {
|
||||
"home_modifier": 1.0 + (diff * 0.3), # Up to +/-30%
|
||||
"away_modifier": 1.0 - (diff * 0.3),
|
||||
"draw_modifier": 1.0 - abs(diff) * 0.2 # Less draw if big diff
|
||||
}
|
||||
|
||||
|
||||
# Singleton
|
||||
_engine: Optional[PlayerPredictorEngine] = None
|
||||
|
||||
|
||||
def get_player_predictor() -> PlayerPredictorEngine:
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = PlayerPredictorEngine()
|
||||
return _engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
engine = get_player_predictor()
|
||||
|
||||
print("\n🧪 Player Predictor Engine Test")
|
||||
print("=" * 50)
|
||||
|
||||
pred = engine.predict(
|
||||
match_id="test_match",
|
||||
home_team_id="test_home",
|
||||
away_team_id="test_away"
|
||||
)
|
||||
|
||||
print(f"\n📊 Prediction:")
|
||||
for k, v in pred.to_dict().items():
|
||||
print(f" {k}: {v}")
|
||||
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
Quantitative Finance Module — V2 Betting Engine
|
||||
Edge calculation, Fractional Kelly Criterion staking, bet grading, and risk assessment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Constants
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BANKROLL_UNITS: float = 10.0 # Total bankroll in abstract units
|
||||
KELLY_FRACTION: float = 0.25 # Quarter-Kelly (conservative, anti-ruin)
|
||||
MIN_EDGE_PLAYABLE: float = 0.05 # 5% edge minimum to mark as playable
|
||||
MIN_ODDS_PLAYABLE: float = 1.30 # Skip extreme chalk below 1.30
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Edge Calculation
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def calculate_edge(true_prob: float, decimal_odds: float) -> float:
|
||||
"""
|
||||
Edge = (True_Probability × Decimal_Odds) - 1.0
|
||||
Positive edge → the model says we have an advantage over the bookmaker.
|
||||
"""
|
||||
if decimal_odds <= 1.0 or true_prob <= 0.0:
|
||||
return -1.0
|
||||
return round((true_prob * decimal_odds) - 1.0, 4)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Kelly Criterion Staking
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def kelly_stake(true_prob: float, decimal_odds: float) -> float:
|
||||
"""
|
||||
Fractional Kelly Criterion for a bankroll of BANKROLL_UNITS.
|
||||
|
||||
Full Kelly: f* = ((b × p) - q) / b
|
||||
where b = decimal_odds - 1, p = true_prob, q = 1 - true_prob
|
||||
|
||||
We use KELLY_FRACTION (25%) to reduce variance and avoid ruin.
|
||||
Returns stake in units, rounded to 0.1.
|
||||
"""
|
||||
if decimal_odds <= 1.0 or true_prob <= 0.0 or true_prob >= 1.0:
|
||||
return 0.0
|
||||
|
||||
b = decimal_odds - 1.0
|
||||
p = true_prob
|
||||
q = 1.0 - p
|
||||
|
||||
f_star = ((b * p) - q) / b
|
||||
|
||||
if f_star <= 0.0:
|
||||
return 0.0
|
||||
|
||||
# Scale by fraction and bankroll
|
||||
stake = f_star * KELLY_FRACTION * BANKROLL_UNITS
|
||||
|
||||
# Cap at a sensible maximum (3 units on a 10-unit bankroll)
|
||||
stake = min(stake, 3.0)
|
||||
|
||||
return round(max(0.0, stake), 1)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Bet Grading
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def grade_bet(edge: float, playable: bool) -> str:
|
||||
"""
|
||||
Assign a letter grade based on edge magnitude.
|
||||
A: Edge > 10% — Elite value, rare
|
||||
B: Edge > 5% — Strong value, core bets
|
||||
C: Edge > 2% — Marginal value, supporting picks only
|
||||
PASS: Below threshold — Do not bet
|
||||
"""
|
||||
if not playable or edge < 0.02:
|
||||
return "PASS"
|
||||
if edge > 0.10:
|
||||
return "A"
|
||||
if edge > 0.05:
|
||||
return "B"
|
||||
return "C"
|
||||
|
||||
|
||||
def is_playable(edge: float, decimal_odds: float) -> bool:
|
||||
"""A pick is playable if it has sufficient edge AND reasonable odds."""
|
||||
return edge >= MIN_EDGE_PLAYABLE and decimal_odds >= MIN_ODDS_PLAYABLE
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Play Score (0-100 composite)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def calculate_play_score(
|
||||
edge: float,
|
||||
true_prob: float,
|
||||
data_quality: float,
|
||||
) -> float:
|
||||
"""
|
||||
Composite score combining edge strength, probability confidence,
|
||||
and data quality. Used for ranking picks and filtering.
|
||||
|
||||
Components:
|
||||
- Edge contribution (0-50): edge * 250, capped at 50
|
||||
- Prob contribution (0-30): probability * 30
|
||||
- DQ contribution (0-20): data_quality * 20
|
||||
"""
|
||||
edge_score = min(50.0, max(0.0, edge * 250.0))
|
||||
prob_score = min(30.0, max(0.0, true_prob * 30.0))
|
||||
dq_score = min(20.0, max(0.0, data_quality * 20.0))
|
||||
return round(edge_score + prob_score + dq_score, 1)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Risk Assessment
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@dataclass
|
||||
class RiskResult:
|
||||
level: str # LOW, MEDIUM, HIGH, EXTREME
|
||||
score: float # 0.0 - 1.0
|
||||
is_surprise_risk: bool
|
||||
surprise_type: str | None
|
||||
warnings: list[str]
|
||||
|
||||
|
||||
def assess_risk(
|
||||
missing_players_impact: float,
|
||||
data_quality_score: float,
|
||||
elo_diff: float,
|
||||
implied_prob_fav: float,
|
||||
) -> RiskResult:
|
||||
"""
|
||||
Multi-factor risk assessment.
|
||||
|
||||
Factors:
|
||||
1. Missing key players (injuries/suspensions)
|
||||
2. Data quality (missing stats, odds)
|
||||
3. ELO closeness (tight matches are riskier)
|
||||
4. Surprise potential (heavy favorite vulnerable)
|
||||
"""
|
||||
warnings: list[str] = []
|
||||
risk_score = 0.0
|
||||
|
||||
# ─── Factor 1: Missing players ────────────────────────────────────
|
||||
if missing_players_impact > 0.3:
|
||||
risk_score += 0.35
|
||||
warnings.append(
|
||||
f"High missing-player impact: {missing_players_impact:.2f}"
|
||||
)
|
||||
elif missing_players_impact > 0.15:
|
||||
risk_score += 0.15
|
||||
warnings.append(
|
||||
f"Moderate missing-player impact: {missing_players_impact:.2f}"
|
||||
)
|
||||
|
||||
# ─── Factor 2: Data quality ───────────────────────────────────────
|
||||
if data_quality_score < 0.5:
|
||||
risk_score += 0.25
|
||||
warnings.append(
|
||||
f"Low data quality: {data_quality_score:.2f}"
|
||||
)
|
||||
elif data_quality_score < 0.75:
|
||||
risk_score += 0.10
|
||||
|
||||
# ─── Factor 3: ELO closeness ──────────────────────────────────────
|
||||
abs_elo_diff = abs(elo_diff)
|
||||
if abs_elo_diff < 50:
|
||||
risk_score += 0.15
|
||||
warnings.append("Very tight ELO difference — coin-flip territory")
|
||||
elif abs_elo_diff < 100:
|
||||
risk_score += 0.05
|
||||
|
||||
# ─── Factor 4: Surprise detection ─────────────────────────────────
|
||||
is_surprise = False
|
||||
surprise_type: str | None = None
|
||||
|
||||
if implied_prob_fav > 0.65 and abs_elo_diff < 80:
|
||||
# Heavy favorite by odds but ELO says match is closer
|
||||
is_surprise = True
|
||||
surprise_type = "odds_elo_divergence"
|
||||
risk_score += 0.15
|
||||
warnings.append(
|
||||
"Upset potential: bookmaker odds suggest heavy favorite "
|
||||
"but ELO says the match is closer than the market thinks"
|
||||
)
|
||||
|
||||
# ─── Classify ─────────────────────────────────────────────────────
|
||||
risk_score = min(1.0, risk_score)
|
||||
if risk_score >= 0.7:
|
||||
level = "EXTREME"
|
||||
elif risk_score >= 0.45:
|
||||
level = "HIGH"
|
||||
elif risk_score >= 0.2:
|
||||
level = "MEDIUM"
|
||||
else:
|
||||
level = "LOW"
|
||||
|
||||
return RiskResult(
|
||||
level=level,
|
||||
score=round(risk_score, 3),
|
||||
is_surprise_risk=is_surprise,
|
||||
surprise_type=surprise_type,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Market Analysis (orchestrates edge/kelly/grade per market)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@dataclass
|
||||
class MarketPick:
|
||||
market: str
|
||||
pick: str
|
||||
probability: float
|
||||
odds: float
|
||||
edge: float
|
||||
playable: bool
|
||||
bet_grade: str
|
||||
stake_units: float
|
||||
play_score: float
|
||||
decision_reasons: list[str]
|
||||
|
||||
|
||||
def analyze_market(
|
||||
market: str,
|
||||
probs: dict[str, float],
|
||||
odds_map: dict[str, float],
|
||||
data_quality_score: float,
|
||||
) -> MarketPick:
|
||||
"""
|
||||
For a given market (MS, OU25, BTTS), find the best pick,
|
||||
calculate edge, kelly stake, and grade it.
|
||||
|
||||
Parameters:
|
||||
market: "MS", "OU25", "BTTS"
|
||||
probs: {"1": 0.55, "X": 0.25, "2": 0.20} — calibrated model probs
|
||||
odds_map: {"1": 2.10, "X": 3.40, "2": 3.50} — decimal odds
|
||||
data_quality_score: 0.0-1.0
|
||||
"""
|
||||
best_pick: str = ""
|
||||
best_edge: float = -99.0
|
||||
best_prob: float = 0.0
|
||||
best_odds: float = 0.0
|
||||
reasons: list[str] = []
|
||||
|
||||
for pick_name, prob in probs.items():
|
||||
odd = odds_map.get(pick_name, 0.0)
|
||||
if odd <= 1.0:
|
||||
continue
|
||||
|
||||
edge = calculate_edge(prob, odd)
|
||||
if edge > best_edge:
|
||||
best_edge = edge
|
||||
best_pick = pick_name
|
||||
best_prob = prob
|
||||
best_odds = odd
|
||||
|
||||
if not best_pick:
|
||||
return MarketPick(
|
||||
market=market, pick="", probability=0.0, odds=0.0,
|
||||
edge=0.0, playable=False, bet_grade="PASS",
|
||||
stake_units=0.0, play_score=0.0,
|
||||
decision_reasons=["no_valid_odds_found"],
|
||||
)
|
||||
|
||||
playable = is_playable(best_edge, best_odds)
|
||||
grade = grade_bet(best_edge, playable)
|
||||
stake = kelly_stake(best_prob, best_odds) if playable else 0.0
|
||||
play_score = calculate_play_score(best_edge, best_prob, data_quality_score)
|
||||
|
||||
# Build decision reasons
|
||||
if playable:
|
||||
reasons.append(f"edge_{best_edge:.1%}_above_threshold")
|
||||
reasons.append(f"kelly_stake_{stake:.1f}_units")
|
||||
else:
|
||||
if best_edge < MIN_EDGE_PLAYABLE:
|
||||
reasons.append(f"edge_{best_edge:.1%}_below_{MIN_EDGE_PLAYABLE:.0%}_threshold")
|
||||
if best_odds < MIN_ODDS_PLAYABLE:
|
||||
reasons.append(f"odds_{best_odds:.2f}_below_{MIN_ODDS_PLAYABLE:.2f}_minimum")
|
||||
|
||||
return MarketPick(
|
||||
market=market,
|
||||
pick=best_pick,
|
||||
probability=round(best_prob, 4),
|
||||
odds=round(best_odds, 2),
|
||||
edge=round(best_edge, 4),
|
||||
playable=playable,
|
||||
bet_grade=grade,
|
||||
stake_units=stake,
|
||||
play_score=play_score,
|
||||
decision_reasons=reasons,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# data package
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Async Database Module — V2 Betting Engine
|
||||
==========================================
|
||||
Provides async SQLAlchemy sessions via asyncpg for the V2 router.
|
||||
|
||||
Usage:
|
||||
async with get_session() as session:
|
||||
result = await session.execute(text("SELECT ..."))
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncEngine,
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_engine: AsyncEngine | None = None
|
||||
_session_maker: async_sessionmaker[AsyncSession] | None = None
|
||||
|
||||
|
||||
def _get_async_dsn() -> str:
|
||||
"""
|
||||
Convert DATABASE_URL to asyncpg-compatible format.
|
||||
|
||||
Handles:
|
||||
1. Prisma's ``?schema=public`` suffix → stripped
|
||||
2. ``postgresql://`` driver prefix → ``postgresql+asyncpg://``
|
||||
"""
|
||||
dsn = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db",
|
||||
)
|
||||
|
||||
# Strip Prisma's ?schema= parameter
|
||||
if "?" in dsn:
|
||||
base, query = dsn.split("?", 1)
|
||||
kept_parts = [
|
||||
part for part in query.split("&") if part and not part.startswith("schema=")
|
||||
]
|
||||
dsn = base if not kept_parts else f"{base}?{'&'.join(kept_parts)}"
|
||||
|
||||
# Convert driver prefix for asyncpg
|
||||
if dsn.startswith("postgresql://"):
|
||||
dsn = dsn.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
elif dsn.startswith("postgres://"):
|
||||
dsn = dsn.replace("postgres://", "postgresql+asyncpg://", 1)
|
||||
|
||||
return dsn
|
||||
|
||||
|
||||
def _ensure_engine() -> AsyncEngine:
|
||||
global _engine, _session_maker
|
||||
if _engine is None:
|
||||
_engine = create_async_engine(
|
||||
_get_async_dsn(),
|
||||
pool_size=5,
|
||||
max_overflow=5,
|
||||
pool_timeout=10,
|
||||
pool_pre_ping=True,
|
||||
echo=False,
|
||||
)
|
||||
_session_maker = async_sessionmaker(
|
||||
bind=_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
print("✅ Async database engine created (asyncpg)")
|
||||
return _engine
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Provide an async session context manager."""
|
||||
_ensure_engine()
|
||||
assert _session_maker is not None
|
||||
async with _session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def dispose_engine() -> None:
|
||||
"""Shut down the async engine cleanly."""
|
||||
global _engine, _session_maker
|
||||
if _engine is not None:
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_session_maker = None
|
||||
print("ℹ️ Async database engine disposed")
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Synchronous psycopg2 database helper for the AI Engine.
|
||||
Uses a thread-safe connection pool for legacy V20+ endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
|
||||
import psycopg2
|
||||
from psycopg2 import pool
|
||||
from psycopg2.extensions import connection as PgConnection
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Safe default with no credentials — will fail fast if not configured.
|
||||
_DEFAULT_DSN = "postgresql://postgres:postgres@localhost:15432/boilerplate_db"
|
||||
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
"""
|
||||
Return a psycopg2-compatible DSN from DATABASE_URL.
|
||||
|
||||
Handles DSN cleanup issues that break raw usage:
|
||||
1. Prisma appends '?schema=public' which psycopg2 cannot parse.
|
||||
"""
|
||||
dsn: str = os.getenv("DATABASE_URL", _DEFAULT_DSN)
|
||||
connect_timeout: str = os.getenv("PGCONNECT_TIMEOUT", "5").strip() or "5"
|
||||
|
||||
# Strip Prisma's ?schema= query parameter while preserving any other query args.
|
||||
if "?" in dsn:
|
||||
base, query = dsn.split("?", 1)
|
||||
kept_parts: list[str] = [
|
||||
part for part in query.split("&") if part and not part.startswith("schema=")
|
||||
]
|
||||
dsn = base if not kept_parts else f"{base}?{'&'.join(kept_parts)}"
|
||||
|
||||
# Force bounded DB connect attempts so API calls do not hang indefinitely.
|
||||
if "connect_timeout=" not in dsn:
|
||||
separator = "&" if "?" in dsn else "?"
|
||||
dsn = f"{dsn}{separator}connect_timeout={connect_timeout}"
|
||||
return dsn
|
||||
|
||||
|
||||
class Database:
|
||||
_pool: pool.ThreadedConnectionPool | None = None
|
||||
|
||||
@classmethod
|
||||
def initialize(cls) -> None:
|
||||
if cls._pool is None:
|
||||
dsn: str = get_clean_dsn()
|
||||
try:
|
||||
cls._pool = pool.ThreadedConnectionPool(
|
||||
minconn=1,
|
||||
maxconn=10,
|
||||
dsn=dsn,
|
||||
)
|
||||
print("✅ Database connection pool created")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create DB pool: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def get_conn(cls) -> PgConnection:
|
||||
if cls._pool is None:
|
||||
cls.initialize()
|
||||
assert cls._pool is not None # guaranteed by initialize()
|
||||
return cls._pool.getconn()
|
||||
|
||||
@classmethod
|
||||
def return_conn(cls, conn: PgConnection) -> None:
|
||||
if cls._pool:
|
||||
cls._pool.putconn(conn)
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def connection(cls) -> Generator[PgConnection, None, None]:
|
||||
"""Context manager for safe connection handling."""
|
||||
conn: PgConnection = cls.get_conn()
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
cls.return_conn(conn)
|
||||
|
||||
@classmethod
|
||||
def close_all(cls) -> None:
|
||||
if cls._pool:
|
||||
cls._pool.closeall()
|
||||
print("ℹ️ Database connection pool closed")
|
||||
@@ -0,0 +1,726 @@
|
||||
{
|
||||
"version": "v1",
|
||||
"description": "Per-league odds reliability scores computed from Brier Score analysis",
|
||||
"min_matches_threshold": 50,
|
||||
"total_leagues": 265,
|
||||
"default_reliability": 0.35,
|
||||
"lookup": {
|
||||
"bx57cmq1edfq53ckfk791supi": 0.9476,
|
||||
"55hcphd1ccc6eai1ms77460on": 0.9445,
|
||||
"d9eaigzyfnfiraqc3ius757tl": 0.9402,
|
||||
"1gxlzw2ezkyeykhcaa5x8ozkk": 0.9259,
|
||||
"5jd0k2txwnq69frs79eulba8j": 0.9233,
|
||||
"6694fff47wqxl10lrd9tb91f8": 0.9193,
|
||||
"4jg7he1n3rb5dniq6hf49xorq": 0.9061,
|
||||
"59tpnfrwnvhnhzmnvfyug68hj": 0.8988,
|
||||
"ac42gi3penartj88fe9l6plpk": 0.8937,
|
||||
"3j81qr7yc4gdnakfwnxf95ovh": 0.8771,
|
||||
"9z5643nd06afqu01ea2wt8y4g": 0.8734,
|
||||
"482ofyysbdbeoxauk19yg7tdt": 0.8722,
|
||||
"ahl3vljaignq9ebaos4uqkrvo": 0.8696,
|
||||
"8x3sbh85gc8qir50utw39jl04": 0.865,
|
||||
"agpweohvn9tugnyl6ry4rhivp": 0.8428,
|
||||
"4c1nfi2j1m731hcay25fcgndq": 0.8425,
|
||||
"1j4ehtrbry9depwt6oghaq3lu": 0.8299,
|
||||
"40yjcbx2sq6oq736iqqqczwt1": 0.8237,
|
||||
"145hkd59i6foieuwr4mwi6wlq": 0.823,
|
||||
"34pl8szyvrbwcmfkuocjm3r6t": 0.8227,
|
||||
"cse5oqqt2pzfcy8uz6yz3tkbj": 0.8212,
|
||||
"zs18qaehvhg3w1208874zvfa": 0.8176,
|
||||
"57nu0wygurzkp6fuy5hhrtaa2": 0.8099,
|
||||
"1eruend45vd20g9hbrpiggs5u": 0.8083,
|
||||
"595nsvo7ykvoe690b1e4u5n56": 0.7987,
|
||||
"6vq8j5p3av14nr3iuyi4okhjt": 0.793,
|
||||
"486rhdgz7yc0sygziht7hje65": 0.7901,
|
||||
"9hh6n2f84k31zmlcxyvmc1w2y": 0.789,
|
||||
"3n5046abeu3x482ds3jwda238": 0.7863,
|
||||
"8yi6ejjd1zudcqtbn07haahg6": 0.7752,
|
||||
"byhmntnl1b4lxw0zz21im3zkd": 0.7719,
|
||||
"2bmwykmdlcc2u1c40ytoc39vy": 0.7668,
|
||||
"82jkgccg7phfjpd0mltdl3pat": 0.7643,
|
||||
"2nttcoriwf5co73vmz1vr8frm": 0.7641,
|
||||
"dr2xk7muj8aqcjdz2b3li1c0k": 0.759,
|
||||
"4yngyfinzd6bb1k7anqtqs0wt": 0.7586,
|
||||
"eog6knrkfei68si736fpquyzc": 0.756,
|
||||
"eg6s9f1jj7jr6stmbosn0g6c8": 0.7538,
|
||||
"ae1wva3zrzcp2zd15gpvsntg6": 0.7517,
|
||||
"cesdwwnxbc5fmajgroc0hqzy2": 0.7466,
|
||||
"8k1xcsyvxapl4jlsluh3eomre": 0.7463,
|
||||
"bdtat25m14jy85y484z3e6lf": 0.7437,
|
||||
"iu1vi94p4p28oozl1h9bvplr": 0.7411,
|
||||
"1r097lpxe0xn03ihb7wi98kao": 0.7391,
|
||||
"2kwbbcootiqqgmrzs6o5inle5": 0.7386,
|
||||
"9fuwphq8kvugrlc3ckm7k8wes": 0.7358,
|
||||
"civf31q1inxohs4a03y8reetf": 0.735,
|
||||
"ili150pwfuf39f7yfdch9lhw": 0.7286,
|
||||
"abs7n2ae3oydilk0tgmpnsj89": 0.7277,
|
||||
"9nbpdi9q3ywcm4q0j5u0ekwcq": 0.7254,
|
||||
"6by3h89i2eykc341oz7lv1ddd": 0.7252,
|
||||
"4qehj8hfxmy6o2ohp4fxinnzo": 0.7244,
|
||||
"9u4pm8x0lfmfq3r0pypmrls71": 0.7244,
|
||||
"c7b8o53flg36wbuevfzy3lb10": 0.7144,
|
||||
"89ovpy1rarewwzqvi30bfdr8b": 0.7068,
|
||||
"4d5d3sf6805n5u6jdoa0hdlog": 0.7052,
|
||||
"eqz64pn0qsp2y7aq4m9id3fn6": 0.7031,
|
||||
"8q60vlvn3krynkob6igrncdjq": 0.703,
|
||||
"6ihotpaocgiovlxw18e9r9prx": 0.7019,
|
||||
"c0r21rtokgnbtc0o2rldjmkxu": 0.7013,
|
||||
"1mpjd0vbxbtu9zw89yj09xk3z": 0.6996,
|
||||
"4zwgbb66rif2spcoeeol2motx": 0.6995,
|
||||
"bu1l7ckihyr0errxw61p0m05": 0.6995,
|
||||
"cv3tuitw3ho3v0opjjxpn83b9": 0.6974,
|
||||
"8r98daokeuzsamu5fmjtblqx5": 0.6922,
|
||||
"dvstmwnvw0mt5p38twn9yttyb": 0.688,
|
||||
"8y29fg2s85ppcb8uugm5ee8s4": 0.6866,
|
||||
"19q13y6ruzo0o84ipblcuouzs": 0.6858,
|
||||
"f4jc2cc5nq7flaoptpi5ua4k4": 0.6852,
|
||||
"4oogyu6o156iphvdvphwpck10": 0.684,
|
||||
"3e40pestup9xzagsu2o6c0i8u": 0.6824,
|
||||
"4rls982p5uzil6x30mhyhv9f3": 0.6812,
|
||||
"e21cf135btr8t3upw0vl6n6x0": 0.6771,
|
||||
"65q4uwm6ol1rkf5dp89m8omny": 0.6754,
|
||||
"46b141eaqq9q7o4gz5gtdpikk": 0.6752,
|
||||
"75i269i1ak43magshljadydrh": 0.6741,
|
||||
"3ab1uwtoyjopdj1y1fynyy9jg": 0.6737,
|
||||
"4mbfidy8zum5u0aqjqo0vuqs2": 0.673,
|
||||
"7wssxdqi4xihseeam8grqa2b8": 0.666,
|
||||
"61fzfjogstjuukzcehighq7mu": 0.6641,
|
||||
"6g8hw3acenrw828la7gwx4mvs": 0.663,
|
||||
"e1kxdivp5g4cpldgpwvnzl1vv": 0.6626,
|
||||
"9ikchyu9fb8bvx0s673jofj6s": 0.6622,
|
||||
"a9vrdkelbgif0gtu3wxsr75xo": 0.6618,
|
||||
"6sxm2iln2w45ux498pty9miw8": 0.6615,
|
||||
"ea0h6cf3bhl698hkxhpulh2zz": 0.661,
|
||||
"apdwh753fupxheygs8seahh7x": 0.6604,
|
||||
"er5745q30wnr8jv9nr863omzg": 0.659,
|
||||
"2z7257m7hj58zuxcjrsg4erzc": 0.6551,
|
||||
"2o9svokc5s7diish3ycrzk7jm": 0.655,
|
||||
"8usjlmziv3p2re0r2wwzezki9": 0.6549,
|
||||
"c0yqkbilbbg70ij2473xymmqv": 0.6506,
|
||||
"du6jsenbjql5e8f3yk880ox4g": 0.6494,
|
||||
"cbdbziaqczfuyuwqsylqi26zd": 0.6478,
|
||||
"725gd73msyt08xm76v7gkxj7u": 0.6445,
|
||||
"enzlj1as2raqm4ids1zyb07y1": 0.6442,
|
||||
"scf9p4y91yjvqvg5jndxzhxj": 0.6414,
|
||||
"5z8v4mj6cjs9ex6hdrpourjzh": 0.6389,
|
||||
"4zwjlzdszduqmxzusysvzymms": 0.6387,
|
||||
"7nmz249q89qg5ezcvzlheljji": 0.6381,
|
||||
"2mdmx668tyhy4u4z9zszwjv5v": 0.6345,
|
||||
"4a7o9rf7ytl8g3ejwpblc6p5n": 0.6306,
|
||||
"2ty8ihceabty8yddmu31iuuej": 0.6283,
|
||||
"dy8zaksw5e9nwrs1p5ss4o1nu": 0.628,
|
||||
"1b70m6qtxrp75b4vtk8hxh8c3": 0.6261,
|
||||
"ajxs0e0g6ryg5ol8qvw3evrcz": 0.6249,
|
||||
"a4fgj2rfbpf4ejo1qi624fefo": 0.6184,
|
||||
"akmkihra9ruad09ljapsm84b3": 0.6182,
|
||||
"907l7wtxdvugdo9i2249wcmr0": 0.6171,
|
||||
"6lwpjhktjhl9g7x2w7njmzva6": 0.6164,
|
||||
"ax1yf4nlzqpcji4j8epdgx3zl": 0.6163,
|
||||
"6ybvtzejh91761lqe7y1csrqo": 0.6158,
|
||||
"3btdfgw79qiz3jmyfudovtbu2": 0.6122,
|
||||
"5cwsxtx37les6m10xj71htkgf": 0.6101,
|
||||
"9p3nnxhdjahfn8qswpzy8oyc3": 0.61,
|
||||
"2xg0qvif1rh7du6wmk2eleku3": 0.6091,
|
||||
"1wwro3z1eb3fl601dju6inlc6": 0.6084,
|
||||
"gfskxsdituog2kqp9yiu7bzi": 0.6076,
|
||||
"zilopfej2h0n3vpan5tcynpo": 0.6051,
|
||||
"2hsidwomhjsaaytdy9u5niyi4": 0.6012,
|
||||
"1klyfth8tl6lu6ra7k8zmy2n2": 0.5996,
|
||||
"cegl2ivkc25blcatxp4jmk1ec": 0.5993,
|
||||
"7qf0jaayyxy3ruamsexv5p1kl": 0.5988,
|
||||
"erpufio3qaujd9gkszcqvb0bf": 0.5972,
|
||||
"cfesxhzb83yl8b779uv3revz1": 0.597,
|
||||
"3ww12jab49q8q8mk9avdwjqgk": 0.5961,
|
||||
"8t2o4huu2e48ij23dxnl9w5qx": 0.5928,
|
||||
"5vq1bl8h8dxdr34w0jaanokto": 0.5919,
|
||||
"ac112osli9fvox1epcg4ld3t6": 0.59,
|
||||
"3frp1zxrqulrlrnk503n6l4l": 0.5808,
|
||||
"c76z5d6j7dpi1e79tm8fpm39z": 0.5807,
|
||||
"6ifaeunfdelecgticvxanikzu": 0.5796,
|
||||
"81txfenlgw75nq3u2nfdkj92o": 0.5789,
|
||||
"yv73ms6v1995b5wny16jcfi3": 0.5787,
|
||||
"b3ufcd24wfnnd5j98ped6irfu": 0.5752,
|
||||
"29actv1ohj8r10kd9hu0jnb0n": 0.5737,
|
||||
"bfqezwfhot1l3p1cpk4oonh25": 0.5705,
|
||||
"5taraea6mqjjldg9zxswo825y": 0.5696,
|
||||
"7qdv1xae7ikfe8dft3oj29yqc": 0.5692,
|
||||
"dm5ka0os1e3dxcp3vh05kmp33": 0.5678,
|
||||
"ay4u6j7lfkcg7x21mx5q121j": 0.5676,
|
||||
"7af85xa75vozt2l4hzi6ryts7": 0.5663,
|
||||
"5k620c7y6dlbmcm88dt3eb7t": 0.5644,
|
||||
"ejunkmfhjz9weugd2bqrkgobb": 0.564,
|
||||
"3428tckxcirwwh3o3jgc1m8ji": 0.5597,
|
||||
"d6zovb8puwgcmsg91iya6rbtm": 0.5593,
|
||||
"2wolc27r8z03itcvwp43e38c5": 0.5592,
|
||||
"alpfd99yd3lfv7bhjo0biuq7b": 0.5582,
|
||||
"beqqnubkv05mamuwvimeum015": 0.5577,
|
||||
"4w7x0s5gfs5abasphlha5de8k": 0.5558,
|
||||
"9ynnnx1qmkizq1o3qr3v0nsuk": 0.554,
|
||||
"722fdbecxzcq9788l6jqclzlw": 0.5539,
|
||||
"287tckirbfj9nb8ar2k9r60vn": 0.5529,
|
||||
"esrunz7rjb0td98mx9e5cedoy": 0.5516,
|
||||
"32n2r9bl6x90psj0wa7bfs6vq": 0.5487,
|
||||
"50ap4sua1xyut3mpu7ehesp63": 0.5483,
|
||||
"5c96g1zm7vo5ons9c42uy2w3r": 0.5469,
|
||||
"3p81ltz6845appgkbgkzxueii": 0.5454,
|
||||
"3n9mk5b2mxmq831wfmv6pu86i": 0.5437,
|
||||
"5zr0b05eyx25km7z1k03ca9jx": 0.5424,
|
||||
"1owhvvge4wlx7e0e431b4vhqx": 0.5423,
|
||||
"3iwftmprsznl6yribr11a8l9m": 0.5393,
|
||||
"7r1f93t6ddrsa5n8v1nq6qlzm": 0.5393,
|
||||
"1gwajyt0pk2jm5fx5mu36v114": 0.5389,
|
||||
"581t4mywybx21wcpmpykhyzr3": 0.5388,
|
||||
"6wubmo7di3kdpflluf6s8c7vs": 0.5375,
|
||||
"bq89wbdvedtov6auzuh6rsv7s": 0.5363,
|
||||
"byu00jvt1j6csyv4y1lkt2fm2": 0.5359,
|
||||
"af79lqrc0ntom74zq13ccjslo": 0.5357,
|
||||
"3ri6juw2w6ma0jezszdlv1uqm": 0.5356,
|
||||
"3l29w00m506ex93t5bbh9cg2a": 0.5355,
|
||||
"1zp1du9n4rj36p1ss9zbxtqfb": 0.5353,
|
||||
"9chuiarcjofld1dkj9kysehmb": 0.5346,
|
||||
"5aw6uyw4pz2bpj24t5z8aacim": 0.5333,
|
||||
"by5nibd18nkt40t0j8a0j5yzx": 0.5332,
|
||||
"4yzidekywejmxxp77gqmdgopg": 0.5323,
|
||||
"7ntvbsyq31jnzoqoa8850b9b8": 0.5305,
|
||||
"a7247po5qs29o3zsfmt222ydu": 0.5299,
|
||||
"117yqo02rs8dykkxpm274w3bd": 0.5298,
|
||||
"193wqkyb0v5jnsblhvd2ocmyo": 0.5296,
|
||||
"8jh0jejuxfhrpawnoztz2jlv4": 0.5295,
|
||||
"5y0z0l2epprzbscvzsgldw8vu": 0.5288,
|
||||
"47s2kt0e8m444ftqvsrqa3bvq": 0.5268,
|
||||
"2hj3286pqov1g1g59k2t2qcgm": 0.5245,
|
||||
"7swf4kpu3v38i2it4h94c5s9k": 0.5227,
|
||||
"78wml3z5wrfxe5iky50tiotgu": 0.5196,
|
||||
"f39uq10c8xhg5e6rwwcf6lhgc": 0.5186,
|
||||
"bbajzna018c79opa1kl5kmkqo": 0.5172,
|
||||
"4davonpqws4a4ejl1awu98zdg": 0.5168,
|
||||
"1fedahp0rws09tj451onten8r": 0.5163,
|
||||
"aho73e5udydy96iun3tkzdzsi": 0.5149,
|
||||
"3aa4mumjl6zyetg6o9hwd5hhx": 0.5125,
|
||||
"7cwemnr3vi40znjq451zxkus6": 0.5115,
|
||||
"ajm86skyzse4ym8g6fpgzncxa": 0.5112,
|
||||
"bgen5kjer2ytfp7lo9949t72g": 0.5102,
|
||||
"8ey0ww2zsosdmwr8ehsorh6t7": 0.51,
|
||||
"8najqkluatpaxvqws78b9s17c": 0.5082,
|
||||
"8v97rcbthsxmzqk4ufxws9mug": 0.506,
|
||||
"degxm4y6gmvp011ccyrev6z5p": 0.5049,
|
||||
"3oa9e03e7w9nr8kqwqc3tlqz9": 0.5049,
|
||||
"5dycj9wdhxh3n33qubw18ohlk": 0.5036,
|
||||
"3is4bkgf3loxv9qfg3hm8zfqb": 0.5033,
|
||||
"f47f3717z2vtpxfxrpdd4jl1x": 0.498,
|
||||
"8ivsfwex4dfx1tvgsiq8askcx": 0.4972,
|
||||
"8vbck9a4mxjms783lf72779uu": 0.4946,
|
||||
"aql5z4osw5wmun0emnakfpwji": 0.4946,
|
||||
"e6vzdkz6l236s9p288mharefy": 0.4925,
|
||||
"4nidzmunvpvxk1ir9b6m8mpay": 0.4874,
|
||||
"ein4fkggto3pdh5msp8huafiq": 0.4856,
|
||||
"1q4ab2bpg5e8jl1g2udnakrju": 0.4852,
|
||||
"8ztsv3pzrsyq5w1r3a0nfk1y5": 0.4842,
|
||||
"1qd0wvt30rlswa4g6nu4na660": 0.4826,
|
||||
"jznihqxle06xych9ygwiwnsa": 0.4796,
|
||||
"2y8bntiif3a9y6gtmauv30gt": 0.4782,
|
||||
"477yyajzheg2z8u7uick0e13e": 0.4706,
|
||||
"bockl24qpr7ryjl8b6obukga": 0.4671,
|
||||
"7mxwwunvot2pi69pj1yr1kh8i": 0.466,
|
||||
"3w1hkk9k9gr8fwssyn4icvdfo": 0.4657,
|
||||
"1txej2dzohnydl21zc9pgx6hy": 0.464,
|
||||
"b8rae0ib0frjmwlca429bq19q": 0.4624,
|
||||
"b5udgm9vakjqz8dcmy5b2g0xt": 0.4582,
|
||||
"eitf7hulqfv1clb7toewkil24": 0.458,
|
||||
"7hl0svs2hg225i2zud0g3xzp2": 0.4559,
|
||||
"2aso72utuctat2ecs6nahjss6": 0.4521,
|
||||
"3ymqchdzk8tt6lfphf26xfvh0": 0.4519,
|
||||
"2yyjcbbryf1r10apyzl7c7jvp": 0.4507,
|
||||
"bly7ema5au6j40i0grhl0pnub": 0.4476,
|
||||
"b1rveez5u792gess9w3e7v5le": 0.4444,
|
||||
"8sdpk4aerruf515yh76ezo7vi": 0.4434,
|
||||
"32vph7vcjqgo1ksj1548di90n": 0.44,
|
||||
"65ggsqdi6drpa4m8y3gkll25k": 0.4394,
|
||||
"xaouuwuk8qyhv1libkeexwjh": 0.4347,
|
||||
"6qitd9h242qkvjenaytfdnsf2": 0.4312,
|
||||
"duuc1qczfnawwncru1ly6o66": 0.4213,
|
||||
"b60nisd3qn427jm0hrg9kvmab": 0.4203,
|
||||
"xwnjb1az11zffwty3m6vn8y6": 0.4197,
|
||||
"dkarmrybx9vx10rg7cywumth0": 0.4158,
|
||||
"75434tz9rc14xkkvudex742ui": 0.4137,
|
||||
"c1d9p6b2e9zr5tqlzx3ktjplg": 0.4129,
|
||||
"b73zounsynk9d3u1p9nvpu7i2": 0.4049,
|
||||
"913mb508il6jzwtlj28fl892h": 0.4044,
|
||||
"e0lck99w8meo9qoalfrxgo33o": 0.401,
|
||||
"8dn0w8zh7nbn2i904603eigwf": 0.3984,
|
||||
"ddyrh5latwfhesgfh4w401n92": 0.3973,
|
||||
"avs3xposm3t9x1x2vzsoxzcbu": 0.3957,
|
||||
"eu2g5j36zzxiazpd729osx0wm": 0.3924,
|
||||
"67uya58idol2eq18ljecsru5o": 0.3912,
|
||||
"23e698ls3x6vi9x8wl0mz7bsa": 0.3838,
|
||||
"6321dlqv4ziuwqte4xpohijtw": 0.382,
|
||||
"8o5tv5viv4hy1qg9jp94k7ayb": 0.381,
|
||||
"53tknno09wqihmwxrqcuwq9sa": 0.3782,
|
||||
"82wo38rqeizxlfjjhfjy4rx7u": 0.3781,
|
||||
"dvtl8sf1262pd2aqgu641qa7u": 0.3767,
|
||||
"663a54fmymndjeev47qm7d3nf": 0.3522,
|
||||
"macko16888165594668885588": 0.3309,
|
||||
"macko16698982162572521585": 0.3262,
|
||||
"6lkj3o21cr4g7bql6tb3fk222": 0.3261,
|
||||
"cu0rmpyff5692eo06ltddjo8a": 0.3161,
|
||||
"1cnx2c8g3hhp8ssxnwwli0mjb": 0.3121,
|
||||
"4vt0ldrcl6thpxpcs8zmpdq1g": 0.2926,
|
||||
"etta63x1t7tnkn4jheisjwk4p": 0.2907,
|
||||
"1n9l0ex47bu0762qg574hzjtd": 0.2626,
|
||||
"6jgwiu2gq3dllmrwt45pfdn2z": 0.2416,
|
||||
"392slbmf1kdqlr6sd1ckt71rs": 0.24,
|
||||
"8z3180hhw2pj1i65uftlk54uz": 0.2096
|
||||
},
|
||||
"details": [
|
||||
{
|
||||
"league_id": "bx57cmq1edfq53ckfk791supi",
|
||||
"league_name": "CAF Konfederasyon Kupası",
|
||||
"match_count": 98,
|
||||
"brier_score": 0.3046,
|
||||
"heavy_fav_win_pct": 84.1,
|
||||
"fav_win_pct": 63.3,
|
||||
"odds_reliability": 0.9476
|
||||
},
|
||||
{
|
||||
"league_id": "55hcphd1ccc6eai1ms77460on",
|
||||
"league_name": "Şampiyonlar Ligi Kadınlar",
|
||||
"match_count": 89,
|
||||
"brier_score": 0.3258,
|
||||
"heavy_fav_win_pct": 83.3,
|
||||
"fav_win_pct": 74.2,
|
||||
"odds_reliability": 0.9445
|
||||
},
|
||||
{
|
||||
"league_id": "d9eaigzyfnfiraqc3ius757tl",
|
||||
"league_name": "Kupa",
|
||||
"match_count": 78,
|
||||
"brier_score": 0.3141,
|
||||
"heavy_fav_win_pct": 81.2,
|
||||
"fav_win_pct": 73.1,
|
||||
"odds_reliability": 0.9402
|
||||
},
|
||||
{
|
||||
"league_id": "1gxlzw2ezkyeykhcaa5x8ozkk",
|
||||
"league_name": "Concacaf Orta Amerika Kupası",
|
||||
"match_count": 88,
|
||||
"brier_score": 0.3338,
|
||||
"heavy_fav_win_pct": 79.4,
|
||||
"fav_win_pct": 61.4,
|
||||
"odds_reliability": 0.9259
|
||||
},
|
||||
{
|
||||
"league_id": "5jd0k2txwnq69frs79eulba8j",
|
||||
"league_name": "Kupa",
|
||||
"match_count": 69,
|
||||
"brier_score": 0.3223,
|
||||
"heavy_fav_win_pct": 78.4,
|
||||
"fav_win_pct": 66.7,
|
||||
"odds_reliability": 0.9233
|
||||
},
|
||||
{
|
||||
"league_id": "6694fff47wqxl10lrd9tb91f8",
|
||||
"league_name": "Kupa",
|
||||
"match_count": 55,
|
||||
"brier_score": 0.3099,
|
||||
"heavy_fav_win_pct": 78.8,
|
||||
"fav_win_pct": 67.3,
|
||||
"odds_reliability": 0.9193
|
||||
},
|
||||
{
|
||||
"league_id": "4jg7he1n3rb5dniq6hf49xorq",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 79,
|
||||
"brier_score": 0.3333,
|
||||
"heavy_fav_win_pct": 77.1,
|
||||
"fav_win_pct": 64.6,
|
||||
"odds_reliability": 0.9061
|
||||
},
|
||||
{
|
||||
"league_id": "59tpnfrwnvhnhzmnvfyug68hj",
|
||||
"league_name": "Libertadores Kupası",
|
||||
"match_count": 180,
|
||||
"brier_score": 0.3408,
|
||||
"heavy_fav_win_pct": 76.2,
|
||||
"fav_win_pct": 61.7,
|
||||
"odds_reliability": 0.8988
|
||||
},
|
||||
{
|
||||
"league_id": "ac42gi3penartj88fe9l6plpk",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 185,
|
||||
"brier_score": 0.3148,
|
||||
"heavy_fav_win_pct": 70.7,
|
||||
"fav_win_pct": 68.1,
|
||||
"odds_reliability": 0.8937
|
||||
},
|
||||
{
|
||||
"league_id": "3j81qr7yc4gdnakfwnxf95ovh",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 106,
|
||||
"brier_score": 0.333,
|
||||
"heavy_fav_win_pct": 72.2,
|
||||
"fav_win_pct": 60.4,
|
||||
"odds_reliability": 0.8771
|
||||
},
|
||||
{
|
||||
"league_id": "9z5643nd06afqu01ea2wt8y4g",
|
||||
"league_name": "Kuu Bara Ligi",
|
||||
"match_count": 110,
|
||||
"brier_score": 0.3294,
|
||||
"heavy_fav_win_pct": 70.3,
|
||||
"fav_win_pct": 53.6,
|
||||
"odds_reliability": 0.8734
|
||||
},
|
||||
{
|
||||
"league_id": "482ofyysbdbeoxauk19yg7tdt",
|
||||
"league_name": "Trendyol Süper Lig",
|
||||
"match_count": 342,
|
||||
"brier_score": 0.3627,
|
||||
"heavy_fav_win_pct": 80.7,
|
||||
"fav_win_pct": 59.6,
|
||||
"odds_reliability": 0.8722
|
||||
},
|
||||
{
|
||||
"league_id": "ahl3vljaignq9ebaos4uqkrvo",
|
||||
"league_name": "Kupa",
|
||||
"match_count": 105,
|
||||
"brier_score": 0.331,
|
||||
"heavy_fav_win_pct": 70.4,
|
||||
"fav_win_pct": 63.8,
|
||||
"odds_reliability": 0.8696
|
||||
},
|
||||
{
|
||||
"league_id": "8x3sbh85gc8qir50utw39jl04",
|
||||
"league_name": "UEFA Kadınlar Euro 2025 Elemeleri",
|
||||
"match_count": 88,
|
||||
"brier_score": 0.3421,
|
||||
"heavy_fav_win_pct": 75.5,
|
||||
"fav_win_pct": 61.4,
|
||||
"odds_reliability": 0.865
|
||||
},
|
||||
{
|
||||
"league_id": "agpweohvn9tugnyl6ry4rhivp",
|
||||
"league_name": "Eredivisie Kadınlar",
|
||||
"match_count": 51,
|
||||
"brier_score": 0.3356,
|
||||
"heavy_fav_win_pct": 72.0,
|
||||
"fav_win_pct": 56.9,
|
||||
"odds_reliability": 0.8428
|
||||
},
|
||||
{
|
||||
"league_id": "4c1nfi2j1m731hcay25fcgndq",
|
||||
"league_name": "Avrupa Ligi",
|
||||
"match_count": 242,
|
||||
"brier_score": 0.3625,
|
||||
"heavy_fav_win_pct": 77.6,
|
||||
"fav_win_pct": 61.6,
|
||||
"odds_reliability": 0.8425
|
||||
},
|
||||
{
|
||||
"league_id": "1j4ehtrbry9depwt6oghaq3lu",
|
||||
"league_name": "Süper Lig",
|
||||
"match_count": 84,
|
||||
"brier_score": 0.3201,
|
||||
"heavy_fav_win_pct": 65.9,
|
||||
"fav_win_pct": 60.7,
|
||||
"odds_reliability": 0.8299
|
||||
},
|
||||
{
|
||||
"league_id": "40yjcbx2sq6oq736iqqqczwt1",
|
||||
"league_name": "DK Elemeler",
|
||||
"match_count": 88,
|
||||
"brier_score": 0.3383,
|
||||
"heavy_fav_win_pct": 68.6,
|
||||
"fav_win_pct": 55.7,
|
||||
"odds_reliability": 0.8237
|
||||
},
|
||||
{
|
||||
"league_id": "145hkd59i6foieuwr4mwi6wlq",
|
||||
"league_name": "Pro Lig",
|
||||
"match_count": 143,
|
||||
"brier_score": 0.3546,
|
||||
"heavy_fav_win_pct": 73.8,
|
||||
"fav_win_pct": 60.1,
|
||||
"odds_reliability": 0.823
|
||||
},
|
||||
{
|
||||
"league_id": "34pl8szyvrbwcmfkuocjm3r6t",
|
||||
"league_name": "LaLiga",
|
||||
"match_count": 364,
|
||||
"brier_score": 0.3773,
|
||||
"heavy_fav_win_pct": 80.2,
|
||||
"fav_win_pct": 56.6,
|
||||
"odds_reliability": 0.8227
|
||||
},
|
||||
{
|
||||
"league_id": "cse5oqqt2pzfcy8uz6yz3tkbj",
|
||||
"league_name": "CAF Şampiyonlar Ligi",
|
||||
"match_count": 91,
|
||||
"brier_score": 0.3513,
|
||||
"heavy_fav_win_pct": 73.9,
|
||||
"fav_win_pct": 57.1,
|
||||
"odds_reliability": 0.8212
|
||||
},
|
||||
{
|
||||
"league_id": "zs18qaehvhg3w1208874zvfa",
|
||||
"league_name": "1. Lig",
|
||||
"match_count": 225,
|
||||
"brier_score": 0.3744,
|
||||
"heavy_fav_win_pct": 82.1,
|
||||
"fav_win_pct": 59.6,
|
||||
"odds_reliability": 0.8176
|
||||
},
|
||||
{
|
||||
"league_id": "57nu0wygurzkp6fuy5hhrtaa2",
|
||||
"league_name": "1. Lig",
|
||||
"match_count": 286,
|
||||
"brier_score": 0.3626,
|
||||
"heavy_fav_win_pct": 72.9,
|
||||
"fav_win_pct": 59.1,
|
||||
"odds_reliability": 0.8099
|
||||
},
|
||||
{
|
||||
"league_id": "1eruend45vd20g9hbrpiggs5u",
|
||||
"league_name": "Botola Pro",
|
||||
"match_count": 265,
|
||||
"brier_score": 0.3625,
|
||||
"heavy_fav_win_pct": 72.9,
|
||||
"fav_win_pct": 50.2,
|
||||
"odds_reliability": 0.8083
|
||||
},
|
||||
{
|
||||
"league_id": "595nsvo7ykvoe690b1e4u5n56",
|
||||
"league_name": "UEFA Uluslar Ligi",
|
||||
"match_count": 67,
|
||||
"brier_score": 0.3687,
|
||||
"heavy_fav_win_pct": 83.3,
|
||||
"fav_win_pct": 50.7,
|
||||
"odds_reliability": 0.7987
|
||||
},
|
||||
{
|
||||
"league_id": "6vq8j5p3av14nr3iuyi4okhjt",
|
||||
"league_name": "Süper Lig Kadınlar",
|
||||
"match_count": 70,
|
||||
"brier_score": 0.356,
|
||||
"heavy_fav_win_pct": 73.5,
|
||||
"fav_win_pct": 58.6,
|
||||
"odds_reliability": 0.793
|
||||
},
|
||||
{
|
||||
"league_id": "486rhdgz7yc0sygziht7hje65",
|
||||
"league_name": "Kupa",
|
||||
"match_count": 62,
|
||||
"brier_score": 0.3704,
|
||||
"heavy_fav_win_pct": 81.1,
|
||||
"fav_win_pct": 66.1,
|
||||
"odds_reliability": 0.7901
|
||||
},
|
||||
{
|
||||
"league_id": "9hh6n2f84k31zmlcxyvmc1w2y",
|
||||
"league_name": "2. Lig",
|
||||
"match_count": 204,
|
||||
"brier_score": 0.357,
|
||||
"heavy_fav_win_pct": 69.2,
|
||||
"fav_win_pct": 62.3,
|
||||
"odds_reliability": 0.789
|
||||
},
|
||||
{
|
||||
"league_id": "3n5046abeu3x482ds3jwda238",
|
||||
"league_name": "WE Lig Kadınlar",
|
||||
"match_count": 102,
|
||||
"brier_score": 0.3761,
|
||||
"heavy_fav_win_pct": 85.4,
|
||||
"fav_win_pct": 58.8,
|
||||
"odds_reliability": 0.7863
|
||||
},
|
||||
{
|
||||
"league_id": "8yi6ejjd1zudcqtbn07haahg6",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 302,
|
||||
"brier_score": 0.3712,
|
||||
"heavy_fav_win_pct": 72.1,
|
||||
"fav_win_pct": 56.3,
|
||||
"odds_reliability": 0.7752
|
||||
},
|
||||
{
|
||||
"league_id": "byhmntnl1b4lxw0zz21im3zkd",
|
||||
"league_name": "Kupa",
|
||||
"match_count": 96,
|
||||
"brier_score": 0.3528,
|
||||
"heavy_fav_win_pct": 68.2,
|
||||
"fav_win_pct": 58.3,
|
||||
"odds_reliability": 0.7719
|
||||
},
|
||||
{
|
||||
"league_id": "2bmwykmdlcc2u1c40ytoc39vy",
|
||||
"league_name": "Açık Kupası",
|
||||
"match_count": 93,
|
||||
"brier_score": 0.3807,
|
||||
"heavy_fav_win_pct": 84.6,
|
||||
"fav_win_pct": 66.7,
|
||||
"odds_reliability": 0.7668
|
||||
},
|
||||
{
|
||||
"league_id": "82jkgccg7phfjpd0mltdl3pat",
|
||||
"league_name": "Süper Lig",
|
||||
"match_count": 289,
|
||||
"brier_score": 0.3782,
|
||||
"heavy_fav_win_pct": 74.0,
|
||||
"fav_win_pct": 57.4,
|
||||
"odds_reliability": 0.7643
|
||||
},
|
||||
{
|
||||
"league_id": "2nttcoriwf5co73vmz1vr8frm",
|
||||
"league_name": "Nesine 2. Lig",
|
||||
"match_count": 525,
|
||||
"brier_score": 0.3782,
|
||||
"heavy_fav_win_pct": 71.8,
|
||||
"fav_win_pct": 55.2,
|
||||
"odds_reliability": 0.7641
|
||||
},
|
||||
{
|
||||
"league_id": "dr2xk7muj8aqcjdz2b3li1c0k",
|
||||
"league_name": "Meistaradeildin",
|
||||
"match_count": 129,
|
||||
"brier_score": 0.3714,
|
||||
"heavy_fav_win_pct": 73.6,
|
||||
"fav_win_pct": 61.2,
|
||||
"odds_reliability": 0.759
|
||||
},
|
||||
{
|
||||
"league_id": "4yngyfinzd6bb1k7anqtqs0wt",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 195,
|
||||
"brier_score": 0.3772,
|
||||
"heavy_fav_win_pct": 74.4,
|
||||
"fav_win_pct": 57.4,
|
||||
"odds_reliability": 0.7586
|
||||
},
|
||||
{
|
||||
"league_id": "eog6knrkfei68si736fpquyzc",
|
||||
"league_name": "Lig Kupası",
|
||||
"match_count": 120,
|
||||
"brier_score": 0.3632,
|
||||
"heavy_fav_win_pct": 69.9,
|
||||
"fav_win_pct": 66.7,
|
||||
"odds_reliability": 0.756
|
||||
},
|
||||
{
|
||||
"league_id": "eg6s9f1jj7jr6stmbosn0g6c8",
|
||||
"league_name": "Süper Lig",
|
||||
"match_count": 108,
|
||||
"brier_score": 0.3657,
|
||||
"heavy_fav_win_pct": 71.2,
|
||||
"fav_win_pct": 55.6,
|
||||
"odds_reliability": 0.7538
|
||||
},
|
||||
{
|
||||
"league_id": "ae1wva3zrzcp2zd15gpvsntg6",
|
||||
"league_name": "Ulusal Lig",
|
||||
"match_count": 278,
|
||||
"brier_score": 0.3783,
|
||||
"heavy_fav_win_pct": 72.7,
|
||||
"fav_win_pct": 55.0,
|
||||
"odds_reliability": 0.7517
|
||||
},
|
||||
{
|
||||
"league_id": "cesdwwnxbc5fmajgroc0hqzy2",
|
||||
"league_name": "Hazırlık Maçları Ülkeler",
|
||||
"match_count": 235,
|
||||
"brier_score": 0.3669,
|
||||
"heavy_fav_win_pct": 67.6,
|
||||
"fav_win_pct": 56.2,
|
||||
"odds_reliability": 0.7466
|
||||
},
|
||||
{
|
||||
"league_id": "8k1xcsyvxapl4jlsluh3eomre",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 328,
|
||||
"brier_score": 0.385,
|
||||
"heavy_fav_win_pct": 74.2,
|
||||
"fav_win_pct": 45.7,
|
||||
"odds_reliability": 0.7463
|
||||
},
|
||||
{
|
||||
"league_id": "bdtat25m14jy85y484z3e6lf",
|
||||
"league_name": "Kupa",
|
||||
"match_count": 90,
|
||||
"brier_score": 0.3772,
|
||||
"heavy_fav_win_pct": 75.7,
|
||||
"fav_win_pct": 55.6,
|
||||
"odds_reliability": 0.7437
|
||||
},
|
||||
{
|
||||
"league_id": "iu1vi94p4p28oozl1h9bvplr",
|
||||
"league_name": "1. Lig",
|
||||
"match_count": 158,
|
||||
"brier_score": 0.3729,
|
||||
"heavy_fav_win_pct": 71.2,
|
||||
"fav_win_pct": 50.0,
|
||||
"odds_reliability": 0.7411
|
||||
},
|
||||
{
|
||||
"league_id": "1r097lpxe0xn03ihb7wi98kao",
|
||||
"league_name": "Serie A",
|
||||
"match_count": 359,
|
||||
"brier_score": 0.3732,
|
||||
"heavy_fav_win_pct": 67.8,
|
||||
"fav_win_pct": 56.5,
|
||||
"odds_reliability": 0.7391
|
||||
},
|
||||
{
|
||||
"league_id": "2kwbbcootiqqgmrzs6o5inle5",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 369,
|
||||
"brier_score": 0.3791,
|
||||
"heavy_fav_win_pct": 70.2,
|
||||
"fav_win_pct": 54.2,
|
||||
"odds_reliability": 0.7386
|
||||
},
|
||||
{
|
||||
"league_id": "9fuwphq8kvugrlc3ckm7k8wes",
|
||||
"league_name": "Ligler Kupası",
|
||||
"match_count": 143,
|
||||
"brier_score": 0.3934,
|
||||
"heavy_fav_win_pct": 81.6,
|
||||
"fav_win_pct": 50.3,
|
||||
"odds_reliability": 0.7358
|
||||
},
|
||||
{
|
||||
"league_id": "civf31q1inxohs4a03y8reetf",
|
||||
"league_name": "Premier Lig",
|
||||
"match_count": 320,
|
||||
"brier_score": 0.3721,
|
||||
"heavy_fav_win_pct": 67.2,
|
||||
"fav_win_pct": 57.8,
|
||||
"odds_reliability": 0.735
|
||||
},
|
||||
{
|
||||
"league_id": "ili150pwfuf39f7yfdch9lhw",
|
||||
"league_name": "UEFA U21 Şampiyonası Elemeler",
|
||||
"match_count": 112,
|
||||
"brier_score": 0.3715,
|
||||
"heavy_fav_win_pct": 70.4,
|
||||
"fav_win_pct": 67.9,
|
||||
"odds_reliability": 0.7286
|
||||
},
|
||||
{
|
||||
"league_id": "abs7n2ae3oydilk0tgmpnsj89",
|
||||
"league_name": "Azadegan Ligi",
|
||||
"match_count": 217,
|
||||
"brier_score": 0.3801,
|
||||
"heavy_fav_win_pct": 71.4,
|
||||
"fav_win_pct": 45.2,
|
||||
"odds_reliability": 0.7277
|
||||
},
|
||||
{
|
||||
"league_id": "9nbpdi9q3ywcm4q0j5u0ekwcq",
|
||||
"league_name": "Serie D",
|
||||
"match_count": 232,
|
||||
"brier_score": 0.3718,
|
||||
"heavy_fav_win_pct": 67.2,
|
||||
"fav_win_pct": 54.7,
|
||||
"odds_reliability": 0.7254
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
AI Engine V9 Feature Modules
|
||||
Includes V8 features + new V9 engines (Upset, Momentum, Poisson, Context, Referee, Squad)
|
||||
"""
|
||||
|
||||
# V20 Features
|
||||
from .h2h_engine import H2HFeatureEngine, get_h2h_engine
|
||||
from .elo_system import ELORatingSystem, get_elo_system
|
||||
from .value_calculator import ValueCalculator, get_value_calculator
|
||||
from .team_stats_engine import get_team_stats_engine
|
||||
from .upset_engine import UpsetEngine, get_upset_engine
|
||||
from .momentum_engine import MomentumEngine, get_momentum_engine
|
||||
from .poisson_engine import PoissonEngine, get_poisson_engine
|
||||
from .referee_engine import RefereeEngine, get_referee_engine
|
||||
from .squad_analysis_engine import SquadAnalysisEngine, get_squad_analysis_engine
|
||||
|
||||
__all__ = [
|
||||
'H2HFeatureEngine', 'get_h2h_engine',
|
||||
'ELORatingSystem', 'get_elo_system',
|
||||
'ValueCalculator', 'get_value_calculator',
|
||||
'get_team_stats_engine',
|
||||
'UpsetEngine', 'get_upset_engine',
|
||||
'MomentumEngine', 'get_momentum_engine',
|
||||
'PoissonEngine', 'get_poisson_engine',
|
||||
'RefereeEngine', 'get_referee_engine',
|
||||
'SquadAnalysisEngine', 'get_squad_analysis_engine',
|
||||
]
|
||||
|
||||
|
||||
Executable
+655
@@ -0,0 +1,655 @@
|
||||
"""
|
||||
ELO Rating System V2 - Venue-Adjusted & League-Weighted
|
||||
V9 Model için geliştirilmiş ELO sistemi.
|
||||
|
||||
V1'den Farklar:
|
||||
- Lig kalitesi faktörü (Premier League vs küçük lig)
|
||||
- Form decay (son maçlar daha etkili)
|
||||
- Venue-adjusted ELO (ev/deplasman ayrı)
|
||||
- Win probability hesaplama
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Optional, Tuple
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
MODELS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'models')
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeamELO:
|
||||
"""Takım ELO profili - Geliştirilmiş"""
|
||||
team_id: str
|
||||
team_name: str = ""
|
||||
|
||||
# Ana ELO'lar
|
||||
overall_elo: float = 1500.0
|
||||
home_elo: float = 1500.0
|
||||
away_elo: float = 1500.0
|
||||
|
||||
# Form ELO (son 5 maça göre)
|
||||
form_elo: float = 1500.0
|
||||
|
||||
# Meta
|
||||
matches_played: int = 0
|
||||
home_matches: int = 0
|
||||
away_matches: int = 0
|
||||
wins: int = 0
|
||||
draws: int = 0
|
||||
losses: int = 0
|
||||
last_updated: Optional[str] = None
|
||||
|
||||
# Son 5 maç formu (W/D/L sequence)
|
||||
recent_form: str = ""
|
||||
|
||||
def win_rate(self) -> float:
|
||||
if self.matches_played == 0:
|
||||
return 0.0
|
||||
return self.wins / self.matches_played
|
||||
|
||||
def to_features(self) -> Dict[str, float]:
|
||||
return {
|
||||
'elo_overall': self.overall_elo,
|
||||
'elo_home': self.home_elo,
|
||||
'elo_away': self.away_elo,
|
||||
'elo_form': self.form_elo,
|
||||
'elo_matches': self.matches_played,
|
||||
'elo_win_rate': self.win_rate(),
|
||||
}
|
||||
|
||||
|
||||
# Lig kalitesi faktörleri (1.0 = ortalama)
|
||||
LEAGUE_QUALITY = {
|
||||
# Top 5 Avrupa Ligleri
|
||||
"premier league": 1.15,
|
||||
"premier lig": 1.15,
|
||||
"la liga": 1.12,
|
||||
"bundesliga": 1.10,
|
||||
"serie a": 1.08,
|
||||
"ligue 1": 1.05,
|
||||
|
||||
# Güçlü ligler
|
||||
"eredivisie": 1.02,
|
||||
"primeira liga": 1.02,
|
||||
"süper lig": 1.00,
|
||||
|
||||
# Avrupa kupaları
|
||||
"champions league": 1.20,
|
||||
"şampiyonlar ligi": 1.20,
|
||||
"europa league": 1.10,
|
||||
"avrupa ligi": 1.10,
|
||||
"conference league": 1.00,
|
||||
|
||||
# Orta ligler
|
||||
"championship": 0.95,
|
||||
"2. bundesliga": 0.92,
|
||||
"serie b": 0.90,
|
||||
"la liga 2": 0.90,
|
||||
|
||||
# Küçük ligler
|
||||
"default": 0.85,
|
||||
}
|
||||
|
||||
|
||||
class ELORatingSystem:
|
||||
"""
|
||||
ELO Rating System V2 - Venue-Adjusted & League-Weighted
|
||||
|
||||
Yenilikler:
|
||||
- Ev/Deplasman ayrı ELO takibi
|
||||
- Lig kalitesi faktörü
|
||||
- Form ELO (son 5 maç ağırlıklı)
|
||||
- Gol farkına göre K-faktör ayarı
|
||||
"""
|
||||
|
||||
# ELO parametreleri
|
||||
K_FACTOR_BASE = 32 # Temel K faktörü
|
||||
K_FACTOR_NEW_TEAM = 48 # Yeni takımlar için daha yüksek (ilk 20 maç)
|
||||
HOME_ADVANTAGE = 65 # Ev sahibi avantajı (ELO cinsinden)
|
||||
INITIAL_ELO = 1500
|
||||
FORM_WEIGHT = 0.7 # Form ELO için son maç ağırlığı
|
||||
|
||||
def __init__(self):
|
||||
self.ratings: Dict[str, TeamELO] = {}
|
||||
self.league_cache: Dict[str, str] = {} # team_id -> league_name
|
||||
self.conn = None
|
||||
self._load_ratings()
|
||||
|
||||
def _connect_db(self):
|
||||
if psycopg2 is None:
|
||||
return None
|
||||
try:
|
||||
from data.db import get_clean_dsn
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
return self.conn
|
||||
except Exception as e:
|
||||
print(f"[ELO] DB connection failed: {e}")
|
||||
return None
|
||||
|
||||
def get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
self._connect_db()
|
||||
return self.conn
|
||||
|
||||
def _load_ratings(self):
|
||||
"""Rating'leri yükle — önce DB, sonra JSON fallback"""
|
||||
if self._load_ratings_from_db():
|
||||
return
|
||||
self._load_ratings_from_json()
|
||||
|
||||
def _load_ratings_from_db(self) -> bool:
|
||||
"""team_elo_ratings tablosundan rating'leri yükle"""
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return False
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT ter.team_id, t.name,
|
||||
ter.overall_elo, ter.home_elo, ter.away_elo,
|
||||
ter.form_elo, ter.matches_played, ter.recent_form
|
||||
FROM team_elo_ratings ter
|
||||
LEFT JOIN teams t ON ter.team_id = t.id
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
if not rows:
|
||||
return False
|
||||
for row in rows:
|
||||
tid, name, overall, home, away, form, played, recent = row
|
||||
self.ratings[str(tid)] = TeamELO(
|
||||
team_id=str(tid),
|
||||
team_name=name or "",
|
||||
overall_elo=float(overall),
|
||||
home_elo=float(home),
|
||||
away_elo=float(away),
|
||||
form_elo=float(form),
|
||||
matches_played=int(played),
|
||||
recent_form=recent or [],
|
||||
)
|
||||
print(f"[OK] ELO V2 ratings DB'den yuklendi ({len(self.ratings)} takim)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[WARN] ELO DB yuklenemedi, JSON'a dusuyuyor: {e}")
|
||||
return False
|
||||
|
||||
def _load_ratings_from_json(self):
|
||||
"""JSON dosyasından rating'leri yükle (fallback)"""
|
||||
ratings_path = os.path.join(MODELS_DIR, 'elo_ratings_v2.json')
|
||||
if os.path.exists(ratings_path):
|
||||
try:
|
||||
with open(ratings_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for team_id, rating_data in data.items():
|
||||
self.ratings[team_id] = TeamELO(**rating_data)
|
||||
print(f"[OK] ELO V2 ratings JSON'dan yuklendi ({len(self.ratings)} takim)")
|
||||
except Exception as e:
|
||||
print(f"[WARN] ELO V2 ratings yuklenemedi: {e}")
|
||||
|
||||
def save_ratings(self):
|
||||
"""Rating'leri kaydet"""
|
||||
ratings_path = os.path.join(MODELS_DIR, 'elo_ratings_v2.json')
|
||||
os.makedirs(MODELS_DIR, exist_ok=True)
|
||||
|
||||
data = {team_id: asdict(elo) for team_id, elo in self.ratings.items()}
|
||||
with open(ratings_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
print(f"💾 ELO V2 ratings kaydedildi ({len(self.ratings)} takım)")
|
||||
|
||||
def get_or_create_rating(self, team_id: str, team_name: str = "") -> TeamELO:
|
||||
"""Takımın ELO'sunu getir veya oluştur"""
|
||||
if team_id not in self.ratings:
|
||||
self.ratings[team_id] = TeamELO(team_id=team_id, team_name=team_name)
|
||||
return self.ratings[team_id]
|
||||
|
||||
def get_league_quality(self, league_name: str) -> float:
|
||||
"""Lig kalitesi faktörünü döndür"""
|
||||
if not league_name:
|
||||
return LEAGUE_QUALITY["default"]
|
||||
|
||||
league_lower = league_name.lower()
|
||||
for key, quality in LEAGUE_QUALITY.items():
|
||||
if key in league_lower:
|
||||
return quality
|
||||
return LEAGUE_QUALITY["default"]
|
||||
|
||||
def expected_score(self, rating_a: float, rating_b: float) -> float:
|
||||
"""
|
||||
A'nın B'ye karşı beklenen skoru (0-1 arası).
|
||||
1 = kesin kazanır, 0.5 = eşit, 0 = kesin kaybeder
|
||||
"""
|
||||
return 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
|
||||
|
||||
def get_k_factor(self, team_elo: TeamELO, goal_diff: int,
|
||||
league_quality: float = 1.0) -> float:
|
||||
"""
|
||||
Dinamik K-faktörü hesapla.
|
||||
- Yeni takımlar için yüksek (hızlı adaptasyon)
|
||||
- Gol farkı yüksekse yüksek
|
||||
- Kaliteli liglerde yüksek
|
||||
"""
|
||||
# Temel K
|
||||
if team_elo.matches_played < 20:
|
||||
k = self.K_FACTOR_NEW_TEAM
|
||||
else:
|
||||
k = self.K_FACTOR_BASE
|
||||
|
||||
# Gol farkı çarpanı
|
||||
if goal_diff == 1:
|
||||
goal_mult = 1.0
|
||||
elif goal_diff == 2:
|
||||
goal_mult = 1.25
|
||||
elif goal_diff == 3:
|
||||
goal_mult = 1.5
|
||||
else:
|
||||
goal_mult = 1.75 + (goal_diff - 3) * 0.1
|
||||
|
||||
# Lig kalitesi çarpanı
|
||||
return k * goal_mult * league_quality
|
||||
|
||||
def update_after_match(
|
||||
self,
|
||||
home_id: str,
|
||||
away_id: str,
|
||||
home_goals: int,
|
||||
away_goals: int,
|
||||
home_name: str = "",
|
||||
away_name: str = "",
|
||||
league_name: str = ""
|
||||
):
|
||||
"""Maç sonrası ELO güncelle"""
|
||||
home_elo = self.get_or_create_rating(home_id, home_name)
|
||||
away_elo = self.get_or_create_rating(away_id, away_name)
|
||||
|
||||
# Gerçek skor
|
||||
if home_goals > away_goals:
|
||||
actual_home, actual_away = 1.0, 0.0
|
||||
home_elo.wins += 1
|
||||
away_elo.losses += 1
|
||||
result_home, result_away = 'W', 'L'
|
||||
elif home_goals < away_goals:
|
||||
actual_home, actual_away = 0.0, 1.0
|
||||
home_elo.losses += 1
|
||||
away_elo.wins += 1
|
||||
result_home, result_away = 'L', 'W'
|
||||
else:
|
||||
actual_home, actual_away = 0.5, 0.5
|
||||
home_elo.draws += 1
|
||||
away_elo.draws += 1
|
||||
result_home, result_away = 'D', 'D'
|
||||
|
||||
goal_diff = abs(home_goals - away_goals)
|
||||
league_quality = self.get_league_quality(league_name)
|
||||
|
||||
# K faktörleri
|
||||
k_home = self.get_k_factor(home_elo, goal_diff, league_quality)
|
||||
k_away = self.get_k_factor(away_elo, goal_diff, league_quality)
|
||||
|
||||
# -- Overall ELO --
|
||||
expected_home = self.expected_score(
|
||||
home_elo.overall_elo + self.HOME_ADVANTAGE,
|
||||
away_elo.overall_elo
|
||||
)
|
||||
home_elo.overall_elo += k_home * (actual_home - expected_home)
|
||||
away_elo.overall_elo += k_away * (actual_away - (1 - expected_home))
|
||||
|
||||
# -- Venue-Specific ELO --
|
||||
expected_home_venue = self.expected_score(home_elo.home_elo, away_elo.away_elo)
|
||||
home_elo.home_elo += k_home * (actual_home - expected_home_venue)
|
||||
away_elo.away_elo += k_away * (actual_away - (1 - expected_home_venue))
|
||||
|
||||
# -- Form ELO (son maçlar daha ağırlıklı) --
|
||||
home_elo.form_elo = (
|
||||
home_elo.form_elo * (1 - self.FORM_WEIGHT) +
|
||||
(1500 + (actual_home - 0.5) * 100) * self.FORM_WEIGHT
|
||||
)
|
||||
away_elo.form_elo = (
|
||||
away_elo.form_elo * (1 - self.FORM_WEIGHT) +
|
||||
(1500 + (actual_away - 0.5) * 100) * self.FORM_WEIGHT
|
||||
)
|
||||
|
||||
# Meta güncelle
|
||||
home_elo.matches_played += 1
|
||||
away_elo.matches_played += 1
|
||||
home_elo.home_matches += 1
|
||||
away_elo.away_matches += 1
|
||||
|
||||
# Son 5 form güncelle
|
||||
home_elo.recent_form = (result_home + home_elo.recent_form)[:5]
|
||||
away_elo.recent_form = (result_away + away_elo.recent_form)[:5]
|
||||
|
||||
home_elo.last_updated = datetime.now().isoformat()
|
||||
away_elo.last_updated = datetime.now().isoformat()
|
||||
|
||||
def predict_match(self, home_id: str, away_id: str) -> Dict[str, float]:
|
||||
"""
|
||||
Maç için kazanma olasılıklarını tahmin et.
|
||||
"""
|
||||
home_elo = self.get_or_create_rating(home_id)
|
||||
away_elo = self.get_or_create_rating(away_id)
|
||||
|
||||
# Overall bazlı
|
||||
exp_home_overall = self.expected_score(
|
||||
home_elo.overall_elo + self.HOME_ADVANTAGE,
|
||||
away_elo.overall_elo
|
||||
)
|
||||
|
||||
# Venue bazlı
|
||||
exp_home_venue = self.expected_score(
|
||||
home_elo.home_elo,
|
||||
away_elo.away_elo
|
||||
)
|
||||
|
||||
# Kombine (ortama)
|
||||
home_prob = (exp_home_overall + exp_home_venue) / 2
|
||||
|
||||
# Draw tahmini (ELO farkı küçükse daha yüksek)
|
||||
elo_diff = abs(home_elo.overall_elo - away_elo.overall_elo)
|
||||
draw_base = 0.25 # Temel beraberlik oranı
|
||||
draw_prob = draw_base * (1 - elo_diff / 800) # Fark arttıkça beraberlik azalır
|
||||
draw_prob = max(0.15, min(draw_prob, 0.35))
|
||||
|
||||
# Normalize
|
||||
remaining = 1 - draw_prob
|
||||
home_win = home_prob * remaining
|
||||
away_win = (1 - home_prob) * remaining
|
||||
|
||||
return {
|
||||
"home_win": round(home_win, 3),
|
||||
"draw": round(draw_prob, 3),
|
||||
"away_win": round(away_win, 3),
|
||||
}
|
||||
|
||||
def get_match_features(self, home_id: str, away_id: str) -> Dict[str, float]:
|
||||
"""Model için ELO feature'larını döndür"""
|
||||
home_elo = self.get_or_create_rating(home_id)
|
||||
away_elo = self.get_or_create_rating(away_id)
|
||||
|
||||
probs = self.predict_match(home_id, away_id)
|
||||
|
||||
# Form encode (WWWDL -> sayısal)
|
||||
def form_to_score(form: str) -> float:
|
||||
if not form:
|
||||
return 0.5
|
||||
score = 0
|
||||
for char in form:
|
||||
if char == 'W':
|
||||
score += 1
|
||||
elif char == 'D':
|
||||
score += 0.5
|
||||
return score / max(len(form), 1)
|
||||
|
||||
return {
|
||||
# Overall ELO
|
||||
'elo_home_overall': home_elo.overall_elo,
|
||||
'elo_away_overall': away_elo.overall_elo,
|
||||
'elo_diff_overall': home_elo.overall_elo - away_elo.overall_elo,
|
||||
|
||||
# Venue-Specific ELO
|
||||
'elo_home_venue': home_elo.home_elo,
|
||||
'elo_away_venue': away_elo.away_elo,
|
||||
'elo_diff_venue': home_elo.home_elo - away_elo.away_elo,
|
||||
|
||||
# Form ELO
|
||||
'elo_home_form': home_elo.form_elo,
|
||||
'elo_away_form': away_elo.form_elo,
|
||||
'elo_diff_form': home_elo.form_elo - away_elo.form_elo,
|
||||
|
||||
# Win probabilities
|
||||
'elo_prob_home': probs['home_win'],
|
||||
'elo_prob_draw': probs['draw'],
|
||||
'elo_prob_away': probs['away_win'],
|
||||
|
||||
# Experience
|
||||
'elo_home_matches': min(home_elo.matches_played, 100),
|
||||
'elo_away_matches': min(away_elo.matches_played, 100),
|
||||
|
||||
# Form score
|
||||
'elo_home_form_score': form_to_score(home_elo.recent_form),
|
||||
'elo_away_form_score': form_to_score(away_elo.recent_form),
|
||||
|
||||
# Win rates
|
||||
'elo_home_win_rate': home_elo.win_rate(),
|
||||
'elo_away_win_rate': away_elo.win_rate(),
|
||||
}
|
||||
|
||||
def save_ratings_to_db(self):
|
||||
"""Rating'leri team_elo_ratings tablosuna yaz (upsert)"""
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
print("❌ DB bağlantısı yok, DB'ye yazılamadı!")
|
||||
return
|
||||
|
||||
cur = conn.cursor()
|
||||
batch_size = 500
|
||||
teams = list(self.ratings.values())
|
||||
written = 0
|
||||
|
||||
for i in range(0, len(teams), batch_size):
|
||||
batch = teams[i:i + batch_size]
|
||||
values = []
|
||||
for elo in batch:
|
||||
values.append(cur.mogrify(
|
||||
"(%s, %s, %s, %s, %s, %s, %s, NOW())",
|
||||
(
|
||||
elo.team_id,
|
||||
round(elo.overall_elo, 2),
|
||||
round(elo.home_elo, 2),
|
||||
round(elo.away_elo, 2),
|
||||
round(elo.form_elo, 2),
|
||||
elo.matches_played,
|
||||
elo.recent_form[:5],
|
||||
)
|
||||
).decode('utf-8'))
|
||||
|
||||
sql = """
|
||||
INSERT INTO team_elo_ratings
|
||||
(team_id, overall_elo, home_elo, away_elo, form_elo, matches_played, recent_form, updated_at)
|
||||
VALUES {}
|
||||
ON CONFLICT (team_id) DO UPDATE SET
|
||||
overall_elo = EXCLUDED.overall_elo,
|
||||
home_elo = EXCLUDED.home_elo,
|
||||
away_elo = EXCLUDED.away_elo,
|
||||
form_elo = EXCLUDED.form_elo,
|
||||
matches_played = EXCLUDED.matches_played,
|
||||
recent_form = EXCLUDED.recent_form,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
""".format(", ".join(values))
|
||||
|
||||
cur.execute(sql)
|
||||
written += len(batch)
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
print(f"💾 DB'ye {written} takım ELO yazıldı (team_elo_ratings)")
|
||||
|
||||
def _load_top_league_ids(self) -> set:
|
||||
"""top_leagues.json'dan lig ID'lerini oku"""
|
||||
paths = [
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', 'top_leagues.json'),
|
||||
os.path.join(os.path.dirname(__file__), '..', 'top_leagues.json'),
|
||||
]
|
||||
for p in paths:
|
||||
if os.path.exists(p):
|
||||
with open(p) as f:
|
||||
ids = set(json.load(f))
|
||||
print(f"📋 {len(ids)} top lig yüklendi ({os.path.basename(p)})")
|
||||
return ids
|
||||
print("⚠️ top_leagues.json bulunamadı — tüm maçlar yazılacak")
|
||||
return set()
|
||||
|
||||
def calculate_all_from_history(self, sport: str = 'football'):
|
||||
"""Tüm tarihsel maçlardan ELO hesapla, top ligleri match_ai_features'a yaz"""
|
||||
print(f"\n🔄 {sport.upper()} için ELO V2 hesaplanıyor...")
|
||||
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
print("❌ DB bağlantısı yok!")
|
||||
return
|
||||
|
||||
top_league_ids = self._load_top_league_ids()
|
||||
|
||||
cur = conn.cursor()
|
||||
|
||||
# Tüm bitmiş maçları tarih sırasına göre al (m.id ve league_id dahil)
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_name, t2.name as away_name,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.sport = %s
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc ASC
|
||||
""", (sport,))
|
||||
|
||||
matches = cur.fetchall()
|
||||
print(f"📊 {len(matches):,} maç işlenecek...")
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
batch: list = []
|
||||
processed = 0
|
||||
written = 0
|
||||
|
||||
for match in matches:
|
||||
(match_id, home_id, away_id, score_h, score_a,
|
||||
league_id, home_name, away_name, league) = match
|
||||
|
||||
if not (home_id and away_id):
|
||||
continue
|
||||
|
||||
# Sadece top ligler için pre-match ELO kaydet
|
||||
if not top_league_ids or league_id in top_league_ids:
|
||||
home_elo_obj = self.get_or_create_rating(home_id, home_name or "")
|
||||
away_elo_obj = self.get_or_create_rating(away_id, away_name or "")
|
||||
batch.append((
|
||||
match_id,
|
||||
home_elo_obj.overall_elo,
|
||||
away_elo_obj.overall_elo,
|
||||
home_elo_obj.home_elo,
|
||||
away_elo_obj.away_elo,
|
||||
home_elo_obj.form_elo,
|
||||
away_elo_obj.form_elo,
|
||||
))
|
||||
|
||||
# Tüm maçlar için ELO güncelle
|
||||
self.update_after_match(
|
||||
home_id, away_id, score_h, score_a,
|
||||
home_name or "", away_name or "", league or ""
|
||||
)
|
||||
processed += 1
|
||||
|
||||
if len(batch) >= BATCH_SIZE:
|
||||
self._flush_elo_batch(cur, batch, sport)
|
||||
conn.commit()
|
||||
written += len(batch)
|
||||
batch.clear()
|
||||
|
||||
if processed % 10000 == 0:
|
||||
print(f" İşlenen: {processed:,} / {len(matches):,}")
|
||||
|
||||
# Kalan batch'i yaz
|
||||
if batch:
|
||||
self._flush_elo_batch(cur, batch, sport)
|
||||
conn.commit()
|
||||
written += len(batch)
|
||||
|
||||
cur.close()
|
||||
print(f"✅ {processed:,} maç işlendi, {len(self.ratings)} takım")
|
||||
print(f"📝 {written:,} maç match_ai_features'a yazıldı")
|
||||
|
||||
# JSON'a kaydet
|
||||
self.save_ratings()
|
||||
|
||||
# DB'ye kaydet
|
||||
self.save_ratings_to_db()
|
||||
|
||||
# Top 20 takımı göster
|
||||
self._show_top_teams()
|
||||
|
||||
@staticmethod
|
||||
def _flush_elo_batch(cur, batch: list, sport: str = 'football') -> None:
|
||||
"""Batch upsert pre-match ELO values into sport-partitioned ai_features table."""
|
||||
from psycopg2.extras import execute_values
|
||||
|
||||
table_name = 'football_ai_features' if sport == 'football' else 'basketball_ai_features'
|
||||
sql = f"""
|
||||
INSERT INTO {table_name}
|
||||
(match_id, home_elo, away_elo,
|
||||
home_home_elo, away_away_elo,
|
||||
home_form_elo, away_form_elo,
|
||||
calculator_ver, updated_at)
|
||||
VALUES %s
|
||||
ON CONFLICT (match_id) DO UPDATE SET
|
||||
home_elo = EXCLUDED.home_elo,
|
||||
away_elo = EXCLUDED.away_elo,
|
||||
home_home_elo = EXCLUDED.home_home_elo,
|
||||
away_away_elo = EXCLUDED.away_away_elo,
|
||||
home_form_elo = EXCLUDED.home_form_elo,
|
||||
away_form_elo = EXCLUDED.away_form_elo,
|
||||
calculator_ver = EXCLUDED.calculator_ver,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
"""
|
||||
now = datetime.now().isoformat()
|
||||
values = [
|
||||
(mid, h_elo, a_elo, hh_elo, aa_elo, hf_elo, af_elo,
|
||||
'elo_v2_backfill', now)
|
||||
for mid, h_elo, a_elo, hh_elo, aa_elo, hf_elo, af_elo in batch
|
||||
]
|
||||
execute_values(cur, sql, values, page_size=500)
|
||||
|
||||
def _show_top_teams(self, n: int = 20):
|
||||
"""En güçlü takımları göster"""
|
||||
sorted_teams = sorted(
|
||||
self.ratings.items(),
|
||||
key=lambda x: x[1].overall_elo,
|
||||
reverse=True
|
||||
)[:n]
|
||||
|
||||
print(f"\n🏆 Top {n} Takım (ELO V2):")
|
||||
for i, (team_id, elo) in enumerate(sorted_teams, 1):
|
||||
name = elo.team_name[:25] if elo.team_name else team_id[:25]
|
||||
print(f" {i:2}. {name:25} → {elo.overall_elo:.0f} (H:{elo.home_elo:.0f} A:{elo.away_elo:.0f})")
|
||||
|
||||
|
||||
# Singleton
|
||||
_system = None
|
||||
|
||||
def get_elo_system() -> ELORatingSystem:
|
||||
global _system
|
||||
if _system is None:
|
||||
_system = ELORatingSystem()
|
||||
return _system
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure ai-engine root is on sys.path (for `from data.db import ...`)
|
||||
_AI_ENGINE_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_AI_ENGINE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_AI_ENGINE_ROOT))
|
||||
|
||||
system = get_elo_system()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'calculate':
|
||||
system.calculate_all_from_history('football')
|
||||
else:
|
||||
print("\n🧪 ELO V2 Test")
|
||||
print("Kullanım: python elo_system.py calculate")
|
||||
print(f"\n📊 Yüklü takım sayısı: {len(system.ratings)}")
|
||||
|
||||
if len(system.ratings) > 0:
|
||||
system._show_top_teams(10)
|
||||
@@ -0,0 +1,990 @@
|
||||
"""
|
||||
Feature Extractor - V2 Betting Engine
|
||||
Pulls historical team stats, ELO, missing-player impact and live odds from
|
||||
PostgreSQL and engineers a leakage-free feature vector for the ensemble model.
|
||||
|
||||
CRITICAL: Only pre-match data (matches before the target match) is used.
|
||||
Post-match stats of the target match are NEVER included.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROLLING_WINDOW: int = 5
|
||||
H2H_WINDOW: int = 10
|
||||
MAX_REST_DAYS: float = 14.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchFeatures:
|
||||
"""Structured feature vector ready for the ensemble model."""
|
||||
|
||||
match_id: str = ""
|
||||
home_team_id: str = ""
|
||||
away_team_id: str = ""
|
||||
|
||||
# ELO & AI features
|
||||
home_elo: float = 1500.0
|
||||
away_elo: float = 1500.0
|
||||
elo_diff: float = 0.0
|
||||
missing_players_impact: float = 0.0
|
||||
home_form_score: float = 0.0
|
||||
away_form_score: float = 0.0
|
||||
h2h_home_win_rate: float = 0.5
|
||||
h2h_sample_size: int = 0
|
||||
home_rest_days: float = 7.0
|
||||
away_rest_days: float = 7.0
|
||||
rest_diff: float = 0.0
|
||||
home_lineup_availability: float = 1.0
|
||||
away_lineup_availability: float = 1.0
|
||||
|
||||
# Rolling averages - Home (last 5 matches)
|
||||
home_avg_possession: float = 50.0
|
||||
home_avg_shots_on_target: float = 4.0
|
||||
home_avg_total_shots: float = 10.0
|
||||
home_avg_goals_scored: float = 1.3
|
||||
home_avg_goals_conceded: float = 1.1
|
||||
|
||||
# Rolling averages - Away (last 5 matches)
|
||||
away_avg_possession: float = 50.0
|
||||
away_avg_shots_on_target: float = 4.0
|
||||
away_avg_total_shots: float = 10.0
|
||||
away_avg_goals_scored: float = 1.3
|
||||
away_avg_goals_conceded: float = 1.1
|
||||
|
||||
# Implied probabilities from bookmaker odds
|
||||
implied_prob_home: float = 0.33
|
||||
implied_prob_draw: float = 0.33
|
||||
implied_prob_away: float = 0.33
|
||||
implied_prob_over25: float = 0.50
|
||||
implied_prob_under25: float = 0.50
|
||||
implied_prob_btts_yes: float = 0.50
|
||||
implied_prob_btts_no: float = 0.50
|
||||
|
||||
# Raw decimal odds (for Edge/Kelly calculations downstream)
|
||||
odds_home: float = 2.50
|
||||
odds_draw: float = 3.20
|
||||
odds_away: float = 2.80
|
||||
odds_over25: float = 1.90
|
||||
odds_under25: float = 1.90
|
||||
odds_btts_yes: float = 1.85
|
||||
odds_btts_no: float = 1.95
|
||||
|
||||
# Data quality
|
||||
data_quality_score: float = 0.5
|
||||
data_quality_flags: list[str] = field(default_factory=list)
|
||||
|
||||
# Metadata
|
||||
match_name: str = ""
|
||||
home_team_name: str = ""
|
||||
away_team_name: str = ""
|
||||
league_id: str = ""
|
||||
league_name: str = ""
|
||||
referee_name: str = ""
|
||||
match_date_ms: int = 0
|
||||
league_avg_goals: float = 2.6
|
||||
referee_avg_goals: float = 2.6
|
||||
referee_home_bias: float = 0.0
|
||||
home_squad_strength: float = 0.5
|
||||
away_squad_strength: float = 0.5
|
||||
home_key_players: float = 0.0
|
||||
away_key_players: float = 0.0
|
||||
|
||||
def to_model_array(self) -> np.ndarray:
|
||||
"""Return the 24-feature vector the ensemble expects."""
|
||||
return np.array(
|
||||
[
|
||||
self.home_elo,
|
||||
self.away_elo,
|
||||
self.elo_diff,
|
||||
self.missing_players_impact,
|
||||
self.home_avg_possession,
|
||||
self.home_avg_shots_on_target,
|
||||
self.home_avg_total_shots,
|
||||
self.home_avg_goals_scored,
|
||||
self.home_avg_goals_conceded,
|
||||
self.away_avg_possession,
|
||||
self.away_avg_shots_on_target,
|
||||
self.away_avg_total_shots,
|
||||
self.away_avg_goals_scored,
|
||||
self.away_avg_goals_conceded,
|
||||
self.implied_prob_home,
|
||||
self.implied_prob_draw,
|
||||
self.implied_prob_away,
|
||||
self.implied_prob_over25,
|
||||
self.implied_prob_under25,
|
||||
self.implied_prob_btts_yes,
|
||||
self.implied_prob_btts_no,
|
||||
self.odds_home,
|
||||
self.odds_draw,
|
||||
self.odds_away,
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def feature_names() -> list[str]:
|
||||
return [
|
||||
"home_elo", "away_elo", "elo_diff", "missing_players_impact",
|
||||
"home_avg_possession", "home_avg_shots_on_target",
|
||||
"home_avg_total_shots", "home_avg_goals_scored",
|
||||
"home_avg_goals_conceded",
|
||||
"away_avg_possession", "away_avg_shots_on_target",
|
||||
"away_avg_total_shots", "away_avg_goals_scored",
|
||||
"away_avg_goals_conceded",
|
||||
"implied_prob_home", "implied_prob_draw", "implied_prob_away",
|
||||
"implied_prob_over25", "implied_prob_under25",
|
||||
"implied_prob_btts_yes", "implied_prob_btts_no",
|
||||
"odds_home", "odds_draw", "odds_away",
|
||||
]
|
||||
|
||||
|
||||
async def extract_features(session: AsyncSession, match_id: str) -> MatchFeatures | None:
|
||||
"""Master extraction pipeline."""
|
||||
feats = MatchFeatures(match_id=match_id)
|
||||
flags: list[str] = []
|
||||
|
||||
match_row = await _load_match_header(session, match_id)
|
||||
if match_row is None:
|
||||
logger.warning("Match %s not found in live_matches or matches.", match_id)
|
||||
return None
|
||||
|
||||
feats.home_team_id = match_row["home_team_id"] or ""
|
||||
feats.away_team_id = match_row["away_team_id"] or ""
|
||||
feats.match_name = match_row.get("match_name", "") or ""
|
||||
feats.match_date_ms = int(match_row.get("mst_utc", 0) or 0)
|
||||
feats.home_team_name = match_row.get("home_name", "") or ""
|
||||
feats.away_team_name = match_row.get("away_name", "") or ""
|
||||
feats.league_id = match_row.get("league_id", "") or ""
|
||||
feats.league_name = match_row.get("league_name", "") or ""
|
||||
feats.referee_name = match_row.get("referee_name", "") or ""
|
||||
|
||||
if not feats.home_team_id or not feats.away_team_id:
|
||||
logger.warning("Match %s missing team IDs.", match_id)
|
||||
flags.append("missing_team_ids")
|
||||
feats.data_quality_flags = flags
|
||||
feats.data_quality_score = 0.1
|
||||
return feats
|
||||
|
||||
ai_row = await _load_ai_features(session, match_id)
|
||||
if ai_row:
|
||||
feats.home_elo = float(ai_row["home_elo"] or 1500.0)
|
||||
feats.away_elo = float(ai_row["away_elo"] or 1500.0)
|
||||
feats.missing_players_impact = float(ai_row["missing_players_impact"] or 0.0)
|
||||
feats.home_form_score = float(ai_row["home_form_score"] or 0.0)
|
||||
feats.away_form_score = float(ai_row["away_form_score"] or 0.0)
|
||||
if ai_row.get("h2h_home_win_rate") is not None:
|
||||
feats.h2h_home_win_rate = float(ai_row["h2h_home_win_rate"])
|
||||
feats.h2h_sample_size = int(ai_row.get("h2h_total") or 0)
|
||||
else:
|
||||
flags.append("missing_ai_features")
|
||||
|
||||
feats.elo_diff = feats.home_elo - feats.away_elo
|
||||
|
||||
home_rolling = await _rolling_team_stats(
|
||||
session, feats.home_team_id, feats.match_date_ms,
|
||||
)
|
||||
away_rolling = await _rolling_team_stats(
|
||||
session, feats.away_team_id, feats.match_date_ms,
|
||||
)
|
||||
|
||||
if home_rolling is not None:
|
||||
feats.home_avg_possession = home_rolling["avg_possession"]
|
||||
feats.home_avg_shots_on_target = home_rolling["avg_shots_on_target"]
|
||||
feats.home_avg_total_shots = home_rolling["avg_total_shots"]
|
||||
feats.home_avg_goals_scored = home_rolling["avg_goals_scored"]
|
||||
feats.home_avg_goals_conceded = home_rolling["avg_goals_conceded"]
|
||||
else:
|
||||
flags.append("missing_home_stats")
|
||||
|
||||
if away_rolling is not None:
|
||||
feats.away_avg_possession = away_rolling["avg_possession"]
|
||||
feats.away_avg_shots_on_target = away_rolling["avg_shots_on_target"]
|
||||
feats.away_avg_total_shots = away_rolling["avg_total_shots"]
|
||||
feats.away_avg_goals_scored = away_rolling["avg_goals_scored"]
|
||||
feats.away_avg_goals_conceded = away_rolling["avg_goals_conceded"]
|
||||
else:
|
||||
flags.append("missing_away_stats")
|
||||
|
||||
if abs(feats.home_form_score) < 1e-6:
|
||||
feats.home_form_score = round(
|
||||
feats.home_avg_goals_scored - feats.home_avg_goals_conceded,
|
||||
3,
|
||||
)
|
||||
if abs(feats.away_form_score) < 1e-6:
|
||||
feats.away_form_score = round(
|
||||
feats.away_avg_goals_scored - feats.away_avg_goals_conceded,
|
||||
3,
|
||||
)
|
||||
|
||||
home_rest_days = await _load_rest_days(
|
||||
session, feats.home_team_id, feats.match_date_ms,
|
||||
)
|
||||
away_rest_days = await _load_rest_days(
|
||||
session, feats.away_team_id, feats.match_date_ms,
|
||||
)
|
||||
if home_rest_days is not None:
|
||||
feats.home_rest_days = home_rest_days
|
||||
else:
|
||||
flags.append("missing_home_rest")
|
||||
if away_rest_days is not None:
|
||||
feats.away_rest_days = away_rest_days
|
||||
else:
|
||||
flags.append("missing_away_rest")
|
||||
feats.rest_diff = round(feats.home_rest_days - feats.away_rest_days, 3)
|
||||
|
||||
if feats.h2h_sample_size == 0:
|
||||
h2h = await _load_h2h_stats(
|
||||
session,
|
||||
feats.home_team_id,
|
||||
feats.away_team_id,
|
||||
feats.match_date_ms,
|
||||
)
|
||||
if h2h is not None:
|
||||
feats.h2h_home_win_rate = h2h["home_win_rate"]
|
||||
feats.h2h_sample_size = h2h["sample_size"]
|
||||
else:
|
||||
flags.append("missing_h2h")
|
||||
|
||||
league_profile = await _load_league_profile(
|
||||
session,
|
||||
feats.league_id,
|
||||
feats.match_date_ms,
|
||||
)
|
||||
if league_profile is not None:
|
||||
feats.league_avg_goals = league_profile["avg_goals"]
|
||||
else:
|
||||
flags.append("missing_league_profile")
|
||||
|
||||
referee_profile = await _load_referee_profile(
|
||||
session,
|
||||
feats.referee_name,
|
||||
feats.match_date_ms,
|
||||
)
|
||||
if referee_profile is not None:
|
||||
feats.referee_avg_goals = referee_profile["avg_goals"]
|
||||
feats.referee_home_bias = referee_profile["home_bias"]
|
||||
else:
|
||||
flags.append("missing_referee_profile")
|
||||
|
||||
home_squad = await _load_team_squad_profile(
|
||||
session,
|
||||
feats.home_team_id,
|
||||
feats.match_date_ms,
|
||||
)
|
||||
away_squad = await _load_team_squad_profile(
|
||||
session,
|
||||
feats.away_team_id,
|
||||
feats.match_date_ms,
|
||||
)
|
||||
if home_squad is not None:
|
||||
feats.home_squad_strength = home_squad["squad_strength"]
|
||||
feats.home_key_players = home_squad["key_players"]
|
||||
else:
|
||||
flags.append("missing_home_squad_profile")
|
||||
if away_squad is not None:
|
||||
feats.away_squad_strength = away_squad["squad_strength"]
|
||||
feats.away_key_players = away_squad["key_players"]
|
||||
else:
|
||||
flags.append("missing_away_squad_profile")
|
||||
|
||||
lineup_info = _extract_lineup_context(match_row)
|
||||
feats.home_lineup_availability = lineup_info["home_availability"]
|
||||
feats.away_lineup_availability = lineup_info["away_availability"]
|
||||
if lineup_info["has_real_lineup_data"]:
|
||||
feats.missing_players_impact = max(
|
||||
feats.missing_players_impact,
|
||||
round(
|
||||
(
|
||||
(1.0 - feats.home_lineup_availability)
|
||||
+ (1.0 - feats.away_lineup_availability)
|
||||
) / 2.0,
|
||||
4,
|
||||
),
|
||||
)
|
||||
else:
|
||||
flags.append("missing_lineup_context")
|
||||
|
||||
odds_ok = await _extract_odds(session, match_id, feats)
|
||||
if not odds_ok:
|
||||
flags.append("missing_odds")
|
||||
|
||||
quality = 1.0
|
||||
penalty_map = {
|
||||
"missing_team_ids": 0.5,
|
||||
"missing_ai_features": 0.05,
|
||||
"missing_home_stats": 0.15,
|
||||
"missing_away_stats": 0.15,
|
||||
"missing_home_rest": 0.05,
|
||||
"missing_away_rest": 0.05,
|
||||
"missing_h2h": 0.05,
|
||||
"missing_league_profile": 0.04,
|
||||
"missing_referee_profile": 0.04,
|
||||
"missing_home_squad_profile": 0.06,
|
||||
"missing_away_squad_profile": 0.06,
|
||||
"missing_lineup_context": 0.05,
|
||||
"missing_odds": 0.2,
|
||||
}
|
||||
for flag in flags:
|
||||
quality -= penalty_map.get(flag, 0.05)
|
||||
feats.data_quality_score = max(0.0, round(quality, 2))
|
||||
feats.data_quality_flags = flags
|
||||
|
||||
return feats
|
||||
|
||||
|
||||
async def _load_match_header(
|
||||
session: AsyncSession, match_id: str,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Try live_matches first, then matches table."""
|
||||
table_queries = {
|
||||
"live_matches": """
|
||||
SELECT
|
||||
m.id,
|
||||
m.home_team_id,
|
||||
m.away_team_id,
|
||||
m.match_name,
|
||||
m.mst_utc,
|
||||
m.sport,
|
||||
m.league_id,
|
||||
m.referee_name,
|
||||
m.lineups,
|
||||
m.sidelined,
|
||||
ht.name AS home_name,
|
||||
at.name AS away_name,
|
||||
l.name AS league_name
|
||||
FROM live_matches m
|
||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
||||
LEFT JOIN leagues l ON l.id = m.league_id
|
||||
WHERE m.id = :match_id
|
||||
LIMIT 1
|
||||
""",
|
||||
"matches": """
|
||||
SELECT
|
||||
m.id,
|
||||
m.home_team_id,
|
||||
m.away_team_id,
|
||||
m.match_name,
|
||||
m.mst_utc,
|
||||
m.sport,
|
||||
m.league_id,
|
||||
ref.name AS referee_name,
|
||||
NULL AS lineups,
|
||||
NULL AS sidelined,
|
||||
ht.name AS home_name,
|
||||
at.name AS away_name,
|
||||
l.name AS league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
||||
LEFT JOIN leagues l ON l.id = m.league_id
|
||||
LEFT JOIN match_officials ref ON ref.match_id = m.id AND ref.role_id = 1
|
||||
WHERE m.id = :match_id
|
||||
LIMIT 1
|
||||
""",
|
||||
}
|
||||
for table in ("live_matches", "matches"):
|
||||
query = text(table_queries[table])
|
||||
result = await session.execute(query, {"match_id": match_id})
|
||||
row = result.mappings().first()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
|
||||
async def _load_ai_features(
|
||||
session: AsyncSession, match_id: str,
|
||||
) -> dict[str, Any] | None:
|
||||
query = text("""
|
||||
SELECT
|
||||
home_elo,
|
||||
away_elo,
|
||||
missing_players_impact,
|
||||
home_form_score,
|
||||
away_form_score,
|
||||
h2h_home_win_rate,
|
||||
h2h_total
|
||||
FROM football_ai_features
|
||||
WHERE match_id = :match_id
|
||||
LIMIT 1
|
||||
""")
|
||||
result = await session.execute(query, {"match_id": match_id})
|
||||
row = result.mappings().first()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
async def _rolling_team_stats(
|
||||
session: AsyncSession,
|
||||
team_id: str,
|
||||
before_mst_utc: int,
|
||||
) -> dict[str, float] | None:
|
||||
"""Calculate rolling averages from the team's last N finished matches."""
|
||||
query = text("""
|
||||
WITH recent AS (
|
||||
SELECT
|
||||
m.id AS match_id,
|
||||
m.home_team_id,
|
||||
m.away_team_id,
|
||||
m.score_home,
|
||||
m.score_away,
|
||||
ts.possession_percentage,
|
||||
ts.shots_on_target,
|
||||
ts.total_shots
|
||||
FROM matches m
|
||||
JOIN football_team_stats ts ON ts.match_id = m.id AND ts.team_id = :team_id
|
||||
WHERE (m.home_team_id = :team_id OR m.away_team_id = :team_id)
|
||||
AND m.mst_utc < :before_ts
|
||||
AND m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT :window
|
||||
)
|
||||
SELECT
|
||||
COALESCE(AVG(possession_percentage), 50.0) AS avg_possession,
|
||||
COALESCE(AVG(shots_on_target), 4.0) AS avg_shots_on_target,
|
||||
COALESCE(AVG(total_shots), 10.0) AS avg_total_shots,
|
||||
COALESCE(AVG(
|
||||
CASE
|
||||
WHEN home_team_id = :team_id THEN score_home
|
||||
ELSE score_away
|
||||
END
|
||||
), 1.3) AS avg_goals_scored,
|
||||
COALESCE(AVG(
|
||||
CASE
|
||||
WHEN home_team_id = :team_id THEN score_away
|
||||
ELSE score_home
|
||||
END
|
||||
), 1.1) AS avg_goals_conceded,
|
||||
COUNT(*) AS match_count
|
||||
FROM recent
|
||||
""")
|
||||
result = await session.execute(
|
||||
query,
|
||||
{"team_id": team_id, "before_ts": before_mst_utc, "window": ROLLING_WINDOW},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
if row is None or int(row["match_count"]) == 0:
|
||||
return None
|
||||
return {
|
||||
"avg_possession": round(float(row["avg_possession"]), 2),
|
||||
"avg_shots_on_target": round(float(row["avg_shots_on_target"]), 2),
|
||||
"avg_total_shots": round(float(row["avg_total_shots"]), 2),
|
||||
"avg_goals_scored": round(float(row["avg_goals_scored"]), 2),
|
||||
"avg_goals_conceded": round(float(row["avg_goals_conceded"]), 2),
|
||||
}
|
||||
|
||||
|
||||
async def _load_rest_days(
|
||||
session: AsyncSession,
|
||||
team_id: str,
|
||||
before_mst_utc: int,
|
||||
) -> float | None:
|
||||
query = text("""
|
||||
SELECT m.mst_utc
|
||||
FROM matches m
|
||||
WHERE (m.home_team_id = :team_id OR m.away_team_id = :team_id)
|
||||
AND m.mst_utc < :before_ts
|
||||
AND m.sport = 'football'
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
result = await session.execute(
|
||||
query,
|
||||
{"team_id": team_id, "before_ts": before_mst_utc},
|
||||
)
|
||||
last_match_ts = result.scalar_one_or_none()
|
||||
if last_match_ts is None:
|
||||
return None
|
||||
|
||||
rest_days = max(0.0, (float(before_mst_utc) - float(last_match_ts)) / 86400000.0)
|
||||
return round(min(rest_days, MAX_REST_DAYS), 3)
|
||||
|
||||
|
||||
async def _load_h2h_stats(
|
||||
session: AsyncSession,
|
||||
home_team_id: str,
|
||||
away_team_id: str,
|
||||
before_mst_utc: int,
|
||||
) -> dict[str, float | int] | None:
|
||||
query = text("""
|
||||
SELECT
|
||||
m.home_team_id,
|
||||
m.away_team_id,
|
||||
m.score_home,
|
||||
m.score_away
|
||||
FROM matches m
|
||||
WHERE m.sport = 'football'
|
||||
AND m.mst_utc < :before_ts
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND (
|
||||
(m.home_team_id = :home_team_id AND m.away_team_id = :away_team_id)
|
||||
OR
|
||||
(m.home_team_id = :away_team_id AND m.away_team_id = :home_team_id)
|
||||
)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT :window
|
||||
""")
|
||||
result = await session.execute(
|
||||
query,
|
||||
{
|
||||
"home_team_id": home_team_id,
|
||||
"away_team_id": away_team_id,
|
||||
"before_ts": before_mst_utc,
|
||||
"window": H2H_WINDOW,
|
||||
},
|
||||
)
|
||||
rows = result.mappings().all()
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
home_wins = 0.0
|
||||
draws = 0.0
|
||||
sample_size = 0
|
||||
for row in rows:
|
||||
score_home = row["score_home"]
|
||||
score_away = row["score_away"]
|
||||
if score_home is None or score_away is None:
|
||||
continue
|
||||
sample_size += 1
|
||||
row_home_team_id = row["home_team_id"]
|
||||
row_away_team_id = row["away_team_id"]
|
||||
|
||||
current_home_score = float(score_home) if row_home_team_id == home_team_id else float(score_away)
|
||||
current_away_score = float(score_away) if row_home_team_id == home_team_id else float(score_home)
|
||||
|
||||
if current_home_score > current_away_score:
|
||||
home_wins += 1.0
|
||||
elif current_home_score == current_away_score:
|
||||
draws += 1.0
|
||||
|
||||
if sample_size == 0:
|
||||
return None
|
||||
|
||||
# Count draws as a half-win signal instead of throwing them away.
|
||||
home_win_rate = round((home_wins + draws * 0.5) / sample_size, 4)
|
||||
return {
|
||||
"home_win_rate": home_win_rate,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
|
||||
|
||||
async def _load_league_profile(
|
||||
session: AsyncSession,
|
||||
league_id: str,
|
||||
before_mst_utc: int,
|
||||
) -> dict[str, float] | None:
|
||||
if not league_id:
|
||||
return None
|
||||
|
||||
query = text("""
|
||||
SELECT
|
||||
COALESCE(AVG(m.score_home + m.score_away), 2.6) AS avg_goals,
|
||||
COUNT(*) AS match_count
|
||||
FROM (
|
||||
SELECT score_home, score_away
|
||||
FROM matches
|
||||
WHERE league_id = :league_id
|
||||
AND sport = 'football'
|
||||
AND status = 'FT'
|
||||
AND score_home IS NOT NULL
|
||||
AND score_away IS NOT NULL
|
||||
AND mst_utc < :before_ts
|
||||
ORDER BY mst_utc DESC
|
||||
LIMIT 100
|
||||
) m
|
||||
""")
|
||||
result = await session.execute(
|
||||
query,
|
||||
{"league_id": league_id, "before_ts": before_mst_utc},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
if row is None or int(row["match_count"] or 0) == 0:
|
||||
return None
|
||||
return {"avg_goals": round(float(row["avg_goals"]), 3)}
|
||||
|
||||
|
||||
async def _load_referee_profile(
|
||||
session: AsyncSession,
|
||||
referee_name: str,
|
||||
before_mst_utc: int,
|
||||
) -> dict[str, float] | None:
|
||||
if not referee_name:
|
||||
return None
|
||||
|
||||
query = text("""
|
||||
SELECT
|
||||
COALESCE(AVG(CASE WHEN score_home > score_away THEN 1.0 ELSE 0.0 END), 0.46) - 0.46 AS home_bias,
|
||||
COALESCE(AVG(score_home + score_away), 2.6) AS avg_goals,
|
||||
COUNT(*) AS match_count
|
||||
FROM (
|
||||
SELECT m.score_home, m.score_away
|
||||
FROM match_officials mo
|
||||
JOIN matches m ON m.id = mo.match_id
|
||||
WHERE mo.name = :referee_name
|
||||
AND mo.role_id = 1
|
||||
AND m.sport = 'football'
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND m.mst_utc < :before_ts
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 30
|
||||
) ref_matches
|
||||
""")
|
||||
result = await session.execute(
|
||||
query,
|
||||
{"referee_name": referee_name, "before_ts": before_mst_utc},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
if row is None or int(row["match_count"] or 0) == 0:
|
||||
return None
|
||||
return {
|
||||
"home_bias": round(float(row["home_bias"]), 4),
|
||||
"avg_goals": round(float(row["avg_goals"]), 3),
|
||||
}
|
||||
|
||||
|
||||
async def _load_team_squad_profile(
|
||||
session: AsyncSession,
|
||||
team_id: str,
|
||||
before_mst_utc: int,
|
||||
) -> dict[str, float] | None:
|
||||
if not team_id:
|
||||
return None
|
||||
|
||||
query = text("""
|
||||
WITH recent_matches AS (
|
||||
SELECT m.id, m.mst_utc
|
||||
FROM matches m
|
||||
WHERE (m.home_team_id = :team_id OR m.away_team_id = :team_id)
|
||||
AND m.sport = 'football'
|
||||
AND m.status = 'FT'
|
||||
AND m.mst_utc < :before_ts
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 8
|
||||
),
|
||||
player_base AS (
|
||||
SELECT
|
||||
mpp.player_id,
|
||||
COUNT(*)::float AS appearances,
|
||||
COUNT(*) FILTER (WHERE mpp.is_starting = true)::float AS starts
|
||||
FROM match_player_participation mpp
|
||||
JOIN recent_matches rm ON rm.id = mpp.match_id
|
||||
WHERE mpp.team_id = :team_id
|
||||
GROUP BY mpp.player_id
|
||||
),
|
||||
player_goals AS (
|
||||
SELECT
|
||||
mpe.player_id,
|
||||
COUNT(*) FILTER (
|
||||
WHERE mpe.event_type = 'goal'
|
||||
AND COALESCE(mpe.event_subtype, '') NOT ILIKE '%penaltı kaçırma%'
|
||||
)::float AS goals,
|
||||
0.0::float AS assists
|
||||
FROM match_player_events mpe
|
||||
JOIN recent_matches rm ON rm.id = mpe.match_id
|
||||
WHERE mpe.team_id = :team_id
|
||||
GROUP BY mpe.player_id
|
||||
UNION ALL
|
||||
SELECT
|
||||
mpe.assist_player_id AS player_id,
|
||||
0.0::float AS goals,
|
||||
COUNT(*) FILTER (
|
||||
WHERE mpe.event_type = 'goal'
|
||||
AND mpe.assist_player_id IS NOT NULL
|
||||
)::float AS assists
|
||||
FROM match_player_events mpe
|
||||
JOIN recent_matches rm ON rm.id = mpe.match_id
|
||||
WHERE mpe.team_id = :team_id
|
||||
AND mpe.assist_player_id IS NOT NULL
|
||||
GROUP BY mpe.assist_player_id
|
||||
),
|
||||
player_events AS (
|
||||
SELECT
|
||||
player_id,
|
||||
SUM(goals) AS goals,
|
||||
SUM(assists) AS assists
|
||||
FROM player_goals
|
||||
GROUP BY player_id
|
||||
),
|
||||
player_scores AS (
|
||||
SELECT
|
||||
pb.player_id,
|
||||
(pb.starts * 1.5)
|
||||
+ ((pb.appearances - pb.starts) * 0.5)
|
||||
+ (COALESCE(pe.goals, 0.0) * 2.5)
|
||||
+ (COALESCE(pe.assists, 0.0) * 1.5) AS score
|
||||
FROM player_base pb
|
||||
LEFT JOIN player_events pe ON pe.player_id = pb.player_id
|
||||
)
|
||||
SELECT
|
||||
COALESCE(AVG(top_players.score), 0.0) AS avg_top_score,
|
||||
COALESCE(COUNT(*) FILTER (WHERE top_players.score >= 6.0), 0) AS key_players,
|
||||
COALESCE((SELECT COUNT(*) FROM recent_matches), 0) AS match_count
|
||||
FROM (
|
||||
SELECT score
|
||||
FROM player_scores
|
||||
ORDER BY score DESC
|
||||
LIMIT 11
|
||||
) top_players
|
||||
""")
|
||||
result = await session.execute(
|
||||
query,
|
||||
{"team_id": team_id, "before_ts": before_mst_utc},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
if row is None or int(row["match_count"] or 0) == 0:
|
||||
return None
|
||||
|
||||
avg_top_score = float(row["avg_top_score"] or 0.0)
|
||||
return {
|
||||
"squad_strength": round(min(max(avg_top_score / 10.0, 0.0), 1.0), 4),
|
||||
"key_players": float(row["key_players"] or 0),
|
||||
}
|
||||
|
||||
|
||||
def _safe_json(value: Any) -> dict[str, Any] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return None
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _safe_list(value: Any) -> list[Any]:
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return []
|
||||
|
||||
|
||||
def _extract_lineup_context(match_row: dict[str, Any]) -> dict[str, float | bool]:
|
||||
lineups = _safe_json(match_row.get("lineups"))
|
||||
sidelined = _safe_json(match_row.get("sidelined"))
|
||||
|
||||
home_xi_count = 0
|
||||
away_xi_count = 0
|
||||
home_sidelined_count = 0
|
||||
away_sidelined_count = 0
|
||||
|
||||
if lineups:
|
||||
home_xi_count = len(_safe_list(lineups.get("home", {}).get("xi")))
|
||||
away_xi_count = len(_safe_list(lineups.get("away", {}).get("xi")))
|
||||
|
||||
if sidelined:
|
||||
home_team = sidelined.get("homeTeam", {})
|
||||
away_team = sidelined.get("awayTeam", {})
|
||||
home_sidelined_count = max(
|
||||
int(home_team.get("totalSidelined") or 0),
|
||||
len(_safe_list(home_team.get("players"))),
|
||||
)
|
||||
away_sidelined_count = max(
|
||||
int(away_team.get("totalSidelined") or 0),
|
||||
len(_safe_list(away_team.get("players"))),
|
||||
)
|
||||
|
||||
has_real_lineup_data = any(
|
||||
value > 0
|
||||
for value in (
|
||||
home_xi_count,
|
||||
away_xi_count,
|
||||
home_sidelined_count,
|
||||
away_sidelined_count,
|
||||
)
|
||||
)
|
||||
|
||||
home_availability = _compute_availability(home_xi_count, home_sidelined_count)
|
||||
away_availability = _compute_availability(away_xi_count, away_sidelined_count)
|
||||
|
||||
return {
|
||||
"home_availability": home_availability,
|
||||
"away_availability": away_availability,
|
||||
"has_real_lineup_data": has_real_lineup_data,
|
||||
}
|
||||
|
||||
|
||||
def _compute_availability(xi_count: int, sidelined_count: int) -> float:
|
||||
xi_ratio = min(max(xi_count / 11.0, 0.0), 1.0) if xi_count > 0 else 1.0
|
||||
sidelined_penalty = min(max(sidelined_count / 11.0, 0.0), 1.0) * 0.35
|
||||
return round(min(max(xi_ratio - sidelined_penalty, 0.0), 1.0), 4)
|
||||
|
||||
|
||||
def _safe_odd(val: Any) -> float:
|
||||
"""Parse an odds value that might be str, float, int, or None."""
|
||||
if val is None:
|
||||
return 0.0
|
||||
try:
|
||||
parsed = float(val)
|
||||
return parsed if parsed > 1.0 else 0.0
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _implied_prob(decimal_odd: float) -> float:
|
||||
"""Convert decimal odds to implied probability, clamped [0, 1]."""
|
||||
if decimal_odd <= 1.0:
|
||||
return 0.0
|
||||
return min(1.0, 1.0 / decimal_odd)
|
||||
|
||||
|
||||
async def _extract_odds(
|
||||
session: AsyncSession,
|
||||
match_id: str,
|
||||
feats: MatchFeatures,
|
||||
) -> bool:
|
||||
"""Extract odds from live JSON first, then relational tables."""
|
||||
found = False
|
||||
|
||||
odds_json = await _load_live_odds_json(session, match_id)
|
||||
if odds_json:
|
||||
found = _parse_odds_json(odds_json, feats)
|
||||
|
||||
if not found:
|
||||
found = await _load_relational_odds(session, match_id, feats)
|
||||
|
||||
if found:
|
||||
feats.implied_prob_home = round(_implied_prob(feats.odds_home), 4)
|
||||
feats.implied_prob_draw = round(_implied_prob(feats.odds_draw), 4)
|
||||
feats.implied_prob_away = round(_implied_prob(feats.odds_away), 4)
|
||||
feats.implied_prob_over25 = round(_implied_prob(feats.odds_over25), 4)
|
||||
feats.implied_prob_under25 = round(_implied_prob(feats.odds_under25), 4)
|
||||
feats.implied_prob_btts_yes = round(_implied_prob(feats.odds_btts_yes), 4)
|
||||
feats.implied_prob_btts_no = round(_implied_prob(feats.odds_btts_no), 4)
|
||||
|
||||
return found
|
||||
|
||||
|
||||
async def _load_live_odds_json(
|
||||
session: AsyncSession, match_id: str,
|
||||
) -> dict[str, Any] | None:
|
||||
query = text("SELECT odds FROM live_matches WHERE id = :mid AND odds IS NOT NULL")
|
||||
result = await session.execute(query, {"mid": match_id})
|
||||
row = result.scalar_one_or_none()
|
||||
if row is None:
|
||||
return None
|
||||
if isinstance(row, str):
|
||||
try:
|
||||
parsed = json.loads(row)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
return parsed if isinstance(parsed, (dict, list)) else None
|
||||
if isinstance(row, (dict, list)):
|
||||
return row
|
||||
return None
|
||||
|
||||
|
||||
def _parse_odds_json(odds_blob: dict[str, Any] | list[Any], feats: MatchFeatures) -> bool:
|
||||
"""Parse the Mackolik-style odds JSON structure."""
|
||||
found_any = False
|
||||
categories: list[dict[str, Any]] = []
|
||||
if isinstance(odds_blob, list):
|
||||
categories = [item for item in odds_blob if isinstance(item, dict)]
|
||||
elif isinstance(odds_blob, dict):
|
||||
raw_categories = odds_blob.get("categories", odds_blob.get("odds", []))
|
||||
if isinstance(raw_categories, dict):
|
||||
categories = [item for item in raw_categories.values() if isinstance(item, dict)]
|
||||
elif isinstance(raw_categories, list):
|
||||
categories = [item for item in raw_categories if isinstance(item, dict)]
|
||||
|
||||
for cat in categories:
|
||||
cat_name = (cat.get("name") or cat.get("cn") or "").strip().lower()
|
||||
selections = cat.get("selections") or cat.get("s") or []
|
||||
|
||||
if cat_name in ("mac sonucu", "match result", "1x2", "maç sonucu"):
|
||||
sels = _selections_to_map(selections)
|
||||
feats.odds_home = _safe_odd(sels.get("1")) or feats.odds_home
|
||||
feats.odds_draw = _safe_odd(sels.get("x")) or feats.odds_draw
|
||||
feats.odds_away = _safe_odd(sels.get("2")) or feats.odds_away
|
||||
found_any = True
|
||||
|
||||
elif cat_name in ("2,5 alt/ust", "over/under 2.5", "2.5 alt/ust", "2,5 alt/üst", "2.5 alt/üst"):
|
||||
sels = _selections_to_map(selections)
|
||||
feats.odds_over25 = _safe_odd(sels.get("ust") or sels.get("over") or sels.get("üst")) or feats.odds_over25
|
||||
feats.odds_under25 = _safe_odd(sels.get("alt") or sels.get("under")) or feats.odds_under25
|
||||
found_any = True
|
||||
|
||||
elif cat_name in ("karsilikli gol", "both teams to score", "btts", "karşılıklı gol"):
|
||||
sels = _selections_to_map(selections)
|
||||
feats.odds_btts_yes = _safe_odd(sels.get("var") or sels.get("yes")) or feats.odds_btts_yes
|
||||
feats.odds_btts_no = _safe_odd(sels.get("yok") or sels.get("no")) or feats.odds_btts_no
|
||||
found_any = True
|
||||
|
||||
return found_any
|
||||
|
||||
|
||||
def _selections_to_map(selections: list[Any] | dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalize varied selection structures into {name_lower: odd_value}."""
|
||||
result: dict[str, Any] = {}
|
||||
if isinstance(selections, dict):
|
||||
for key, value in selections.items():
|
||||
result[str(key).strip().lower()] = value
|
||||
elif isinstance(selections, list):
|
||||
for sel in selections:
|
||||
if isinstance(sel, dict):
|
||||
name = (sel.get("name") or sel.get("n") or "").strip().lower()
|
||||
value = sel.get("odd_value") or sel.get("ov") or sel.get("v")
|
||||
if name:
|
||||
result[name] = value
|
||||
return result
|
||||
|
||||
|
||||
async def _load_relational_odds(
|
||||
session: AsyncSession, match_id: str, feats: MatchFeatures,
|
||||
) -> bool:
|
||||
"""Fallback: load odds from odd_categories + odd_selections."""
|
||||
query = text("""
|
||||
SELECT oc.name AS cat_name, os.name AS sel_name, os.odd_value
|
||||
FROM odd_categories oc
|
||||
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
|
||||
WHERE oc.match_id = :match_id
|
||||
AND oc.name IN ('Maç Sonucu', '2,5 Alt/Üst', 'Karşılıklı Gol')
|
||||
""")
|
||||
result = await session.execute(query, {"match_id": match_id})
|
||||
rows = result.mappings().all()
|
||||
if not rows:
|
||||
return False
|
||||
|
||||
for row in rows:
|
||||
cat = (row["cat_name"] or "").strip()
|
||||
sel = (row["sel_name"] or "").strip().lower()
|
||||
value = _safe_odd(row["odd_value"])
|
||||
if value <= 1.0:
|
||||
continue
|
||||
|
||||
if cat == "Maç Sonucu":
|
||||
if sel == "1":
|
||||
feats.odds_home = value
|
||||
elif sel == "x":
|
||||
feats.odds_draw = value
|
||||
elif sel == "2":
|
||||
feats.odds_away = value
|
||||
elif cat == "2,5 Alt/Üst":
|
||||
if sel in ("üst", "ust", "over"):
|
||||
feats.odds_over25 = value
|
||||
elif sel in ("alt", "under"):
|
||||
feats.odds_under25 = value
|
||||
elif cat == "Karşılıklı Gol":
|
||||
if sel in ("var", "yes"):
|
||||
feats.odds_btts_yes = value
|
||||
elif sel in ("yok", "no"):
|
||||
feats.odds_btts_no = value
|
||||
|
||||
return True
|
||||
Executable
+256
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
Feature Adapter for XGBoost Inference
|
||||
=====================================
|
||||
Bridges the gap between V20 Engine outputs (CalculationContext) and XGBoost Models.
|
||||
Constructs the exact 44-feature vector used in training.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extensions import connection as PgConnection
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from data.db import get_clean_dsn
|
||||
|
||||
# Feature definitions (Must match train_xgboost_markets.py)
|
||||
# NOTE: 68 features - matching the trained XGBoost models
|
||||
FEATURES = [
|
||||
# ELO
|
||||
"home_overall_elo", "away_overall_elo", "elo_diff",
|
||||
"home_home_elo", "away_away_elo", "form_elo_diff",
|
||||
|
||||
# Form
|
||||
"home_goals_avg", "home_conceded_avg",
|
||||
"away_goals_avg", "away_conceded_avg",
|
||||
"home_clean_sheet_rate", "away_clean_sheet_rate",
|
||||
"home_scoring_rate", "away_scoring_rate",
|
||||
"home_winning_streak", "away_winning_streak",
|
||||
|
||||
# H2H
|
||||
"h2h_home_win_rate", "h2h_draw_rate",
|
||||
"h2h_avg_goals", "h2h_btts_rate", "h2h_over25_rate",
|
||||
|
||||
# Stats
|
||||
"home_avg_possession", "away_avg_possession",
|
||||
"home_avg_shots_on_target", "away_avg_shots_on_target",
|
||||
"home_shot_conversion", "away_shot_conversion",
|
||||
|
||||
# Odds (Implicit market wisdom)
|
||||
"odds_ms_h", "odds_ms_d", "odds_ms_a",
|
||||
"implied_home", "implied_draw", "implied_away",
|
||||
|
||||
"odds_ht_ms_h", "odds_ht_ms_d", "odds_ht_ms_a",
|
||||
|
||||
"odds_ou05_o", "odds_ou05_u",
|
||||
"odds_ou15_o", "odds_ou15_u",
|
||||
"odds_ou25_o", "odds_ou25_u",
|
||||
"odds_ou35_o", "odds_ou35_u",
|
||||
|
||||
"odds_ht_ou05_o", "odds_ht_ou05_u",
|
||||
"odds_ht_ou15_o", "odds_ht_ou15_u",
|
||||
|
||||
"odds_btts_y", "odds_btts_n",
|
||||
|
||||
# League/Context
|
||||
"league_avg_goals", "league_zero_goal_rate",
|
||||
"home_xga", "away_xga",
|
||||
|
||||
# Upset features
|
||||
"upset_atmosphere", "upset_motivation", "upset_fatigue", "upset_potential",
|
||||
|
||||
# Referee features
|
||||
"referee_home_bias", "referee_avg_goals", "referee_cards_total",
|
||||
"referee_avg_yellow", "referee_experience",
|
||||
|
||||
# Momentum features
|
||||
"home_momentum_score", "away_momentum_score", "momentum_diff",
|
||||
]
|
||||
|
||||
class FeatureAdapter:
|
||||
"""
|
||||
Adapter to convert V20 context into XGBoost-compatible features.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.conn: PgConnection | None = None
|
||||
self._connect_db()
|
||||
self.league_stats_cache: dict[str, dict[str, float]] = {}
|
||||
|
||||
def _connect_db(self) -> None:
|
||||
try:
|
||||
# FeatureAdapter uses DB only for optional league stats enrichment.
|
||||
# Keep startup non-blocking when DB/tunnel is unavailable.
|
||||
if not os.getenv("DATABASE_URL", "").strip():
|
||||
return
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
except Exception as e:
|
||||
print(f"⚠️ FeatureAdapter DB connection failed: {e}")
|
||||
|
||||
def get_features(self, ctx: Any) -> pd.DataFrame:
|
||||
"""
|
||||
Construct feature vector from CalculationContext.
|
||||
Returns a DataFrame with 1 row and correct columns.
|
||||
"""
|
||||
raw = ctx.team_pred.raw_features
|
||||
odds = ctx.odds_data or {}
|
||||
upset_features = getattr(ctx, "upset_features", {}) or {}
|
||||
momentum_features = getattr(ctx, "momentum_features", {}) or {}
|
||||
referee_features = getattr(ctx, "referee_features", {}) or {}
|
||||
|
||||
# 1. Odds Features
|
||||
ms_h = float(odds.get("ms_h") or 0)
|
||||
ms_d = float(odds.get("ms_d") or 0)
|
||||
ms_a = float(odds.get("ms_a") or 0)
|
||||
|
||||
implied_home, implied_draw, implied_away = 0.33, 0.33, 0.33
|
||||
if ms_h > 0 and ms_d > 0 and ms_a > 0:
|
||||
raw_sum = 1/ms_h + 1/ms_d + 1/ms_a
|
||||
implied_home = (1/ms_h) / raw_sum
|
||||
implied_draw = (1/ms_d) / raw_sum
|
||||
implied_away = (1/ms_a) / raw_sum
|
||||
|
||||
# 2. League Features
|
||||
# Using ctx.league_id if available, or just defaults
|
||||
league_stats = self._get_league_stats(ctx.league_id)
|
||||
|
||||
# 3. Assemble Dictionary
|
||||
row = {
|
||||
# ELO (Explicit float casting)
|
||||
"home_overall_elo": float(raw.get("home_overall_elo") or 1500),
|
||||
"away_overall_elo": float(raw.get("away_overall_elo") or 1500),
|
||||
"elo_diff": float(raw.get("elo_diff") or 0),
|
||||
"home_home_elo": float(raw.get("home_home_elo") or 1500),
|
||||
"away_away_elo": float(raw.get("away_away_elo") or 1500),
|
||||
"form_elo_diff": float(raw.get("form_elo_diff") or 0),
|
||||
|
||||
# Form (Explicit float casting)
|
||||
"home_goals_avg": float(raw.get("home_goals_avg") or 1.3),
|
||||
"home_conceded_avg": float(raw.get("home_conceded_avg") or 1.2),
|
||||
"away_goals_avg": float(raw.get("away_goals_avg") or 1.2),
|
||||
"away_conceded_avg": float(raw.get("away_conceded_avg") or 1.4),
|
||||
"home_clean_sheet_rate": float(raw.get("home_clean_sheet_rate") or 0.2),
|
||||
"away_clean_sheet_rate": float(raw.get("away_clean_sheet_rate") or 0.2),
|
||||
"home_scoring_rate": float(raw.get("home_scoring_rate") or 0.8),
|
||||
"away_scoring_rate": float(raw.get("away_scoring_rate") or 0.8),
|
||||
"home_winning_streak": float(raw.get("home_winning_streak") or 0),
|
||||
"away_winning_streak": float(raw.get("away_winning_streak") or 0),
|
||||
|
||||
# H2H (Explicit float casting)
|
||||
"h2h_home_win_rate": float(raw.get("h2h_home_win_rate") or 0.33),
|
||||
"h2h_draw_rate": float(raw.get("h2h_draw_rate") or 0.33),
|
||||
"h2h_avg_goals": float(raw.get("h2h_avg_goals") or 2.5),
|
||||
"h2h_btts_rate": float(raw.get("h2h_btts_rate") or 0.5),
|
||||
"h2h_over25_rate": float(raw.get("h2h_over25_rate") or 0.5),
|
||||
|
||||
# Stats (Explicit float casting to avoid XGBoost 'object' error)
|
||||
"home_avg_possession": float(raw.get("home_avg_possession") or 0.5),
|
||||
"away_avg_possession": float(raw.get("away_avg_possession") or 0.5),
|
||||
"home_avg_shots_on_target": float(raw.get("home_avg_shots_on_target") or 4.0),
|
||||
"away_avg_shots_on_target": float(raw.get("away_avg_shots_on_target") or 3.5),
|
||||
"home_shot_conversion": float(raw.get("home_shot_conversion") or 0.1),
|
||||
"away_shot_conversion": float(raw.get("away_shot_conversion") or 0.1),
|
||||
|
||||
# Odds
|
||||
"odds_ms_h": ms_h,
|
||||
"odds_ms_d": ms_d,
|
||||
"odds_ms_a": ms_a,
|
||||
"implied_home": implied_home,
|
||||
"implied_draw": implied_draw,
|
||||
"implied_away": implied_away,
|
||||
|
||||
"odds_ht_ms_h": float(odds.get("ht_ms_h") or 0.0),
|
||||
"odds_ht_ms_d": float(odds.get("ht_ms_d") or 0.0),
|
||||
"odds_ht_ms_a": float(odds.get("ht_ms_a") or 0.0),
|
||||
|
||||
"odds_ou05_o": float(odds.get("ou05_o") or 0.0),
|
||||
"odds_ou05_u": float(odds.get("ou05_u") or 0.0),
|
||||
"odds_ou15_o": float(odds.get("ou15_o") or 0.0),
|
||||
"odds_ou15_u": float(odds.get("ou15_u") or 0.0),
|
||||
"odds_ou25_o": float(odds.get("ou25_o") or 0.0),
|
||||
"odds_ou25_u": float(odds.get("ou25_u") or 0.0),
|
||||
"odds_ou35_o": float(odds.get("ou35_o") or 0.0),
|
||||
"odds_ou35_u": float(odds.get("ou35_u") or 0.0),
|
||||
|
||||
"odds_ht_ou05_o": float(odds.get("ht_ou05_o") or 0.0),
|
||||
"odds_ht_ou05_u": float(odds.get("ht_ou05_u") or 0.0),
|
||||
"odds_ht_ou15_o": float(odds.get("ht_ou15_o") or 0.0),
|
||||
"odds_ht_ou15_u": float(odds.get("ht_ou15_u") or 0.0),
|
||||
|
||||
"odds_btts_y": float(odds.get("btts_y") or 0.0),
|
||||
"odds_btts_n": float(odds.get("btts_n") or 0.0),
|
||||
|
||||
# League/Def
|
||||
"league_avg_goals": float(league_stats.get("avg_goals") or 2.7),
|
||||
"league_zero_goal_rate": float(league_stats.get("zero_rate") or 0.07),
|
||||
"home_xga": float(raw.get("home_xga") or 1.2),
|
||||
"away_xga": float(raw.get("away_xga") or 1.4),
|
||||
|
||||
# Upset features (default values - computed separately in upset_engine_v2)
|
||||
"upset_atmosphere": float(raw.get("upset_atmosphere") or 0.0),
|
||||
"upset_motivation": float(raw.get("upset_motivation") or 0.0),
|
||||
"upset_fatigue": float(raw.get("upset_fatigue") or 0.0),
|
||||
"upset_potential": float(raw.get("upset_potential") or 0.0),
|
||||
|
||||
# Referee features (default values)
|
||||
"referee_home_bias": float(raw.get("referee_home_bias") or 0.0),
|
||||
"referee_avg_goals": float(raw.get("referee_avg_goals") or 2.5),
|
||||
"referee_cards_total": float(raw.get("referee_cards_total") or 4.0),
|
||||
"referee_avg_yellow": float(raw.get("referee_avg_yellow") or 3.0),
|
||||
"referee_experience": float(raw.get("referee_experience") or 0),
|
||||
|
||||
# Momentum features (default values)
|
||||
"home_momentum_score": float(raw.get("home_momentum_score") or 0.0),
|
||||
"away_momentum_score": float(raw.get("away_momentum_score") or 0.0),
|
||||
"momentum_diff": float(raw.get("momentum_diff") or 0.0),
|
||||
}
|
||||
|
||||
# Return as DataFrame (cols sorted by FEATURES list to ensure alignment)
|
||||
df = pd.DataFrame([row], columns=FEATURES)
|
||||
return df
|
||||
|
||||
def _get_league_stats(self, league_id: str | None) -> dict[str, float]:
|
||||
"""Get cached league stats or default."""
|
||||
if not league_id:
|
||||
return {"avg_goals": 2.7, "zero_rate": 0.07}
|
||||
|
||||
if league_id in self.league_stats_cache:
|
||||
return self.league_stats_cache[league_id]
|
||||
|
||||
if self.conn:
|
||||
try:
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT AVG(score_home + score_away),
|
||||
AVG(CASE WHEN score_home=0 AND score_away=0 THEN 1.0 ELSE 0.0 END)
|
||||
FROM matches
|
||||
WHERE league_id = %s AND status = 'FT'
|
||||
AND mst_utc > EXTRACT(EPOCH FROM NOW() - INTERVAL '1 year')
|
||||
""", (league_id,))
|
||||
res = cur.fetchone()
|
||||
if res and res[0]:
|
||||
stats = {
|
||||
"avg_goals": float(res[0]),
|
||||
"zero_rate": float(res[1])
|
||||
}
|
||||
self.league_stats_cache[league_id] = stats
|
||||
return stats
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Default fallback
|
||||
return {"avg_goals": 2.7, "zero_rate": 0.07}
|
||||
|
||||
# Singleton
|
||||
_adapter: FeatureAdapter | None = None
|
||||
|
||||
|
||||
def get_feature_adapter() -> FeatureAdapter:
|
||||
global _adapter
|
||||
if _adapter is None:
|
||||
_adapter = FeatureAdapter()
|
||||
return _adapter
|
||||
Executable
+316
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
Head-to-Head (H2H) Feature Engine
|
||||
Takımların birbirine karşı geçmiş performansını analiz eder.
|
||||
"""
|
||||
|
||||
import os
|
||||
import psycopg2
|
||||
from typing import Dict, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from data.db import get_clean_dsn
|
||||
|
||||
|
||||
@dataclass
|
||||
class H2HProfile:
|
||||
"""Head-to-Head analiz sonucu"""
|
||||
total_matches: int
|
||||
home_wins: int
|
||||
draws: int
|
||||
away_wins: int
|
||||
home_goals_total: int
|
||||
away_goals_total: int
|
||||
btts_count: int # Both teams to score
|
||||
over25_count: int
|
||||
|
||||
@property
|
||||
def home_win_rate(self) -> float:
|
||||
return self.home_wins / self.total_matches if self.total_matches > 0 else 0.33
|
||||
|
||||
@property
|
||||
def draw_rate(self) -> float:
|
||||
return self.draws / self.total_matches if self.total_matches > 0 else 0.33
|
||||
|
||||
@property
|
||||
def away_win_rate(self) -> float:
|
||||
return self.away_wins / self.total_matches if self.total_matches > 0 else 0.33
|
||||
|
||||
@property
|
||||
def avg_total_goals(self) -> float:
|
||||
return (self.home_goals_total + self.away_goals_total) / self.total_matches if self.total_matches > 0 else 2.5
|
||||
|
||||
@property
|
||||
def btts_rate(self) -> float:
|
||||
return self.btts_count / self.total_matches if self.total_matches > 0 else 0.5
|
||||
|
||||
@property
|
||||
def over25_rate(self) -> float:
|
||||
return self.over25_count / self.total_matches if self.total_matches > 0 else 0.5
|
||||
|
||||
@property
|
||||
def home_dominance(self) -> float:
|
||||
"""Ev sahibinin üstünlük skoru (-1 ile 1 arası)"""
|
||||
if self.total_matches == 0:
|
||||
return 0
|
||||
return (self.home_wins - self.away_wins) / self.total_matches
|
||||
|
||||
def to_features(self) -> Dict[str, float]:
|
||||
"""Feature dictionary döndür"""
|
||||
return {
|
||||
'h2h_total_matches': self.total_matches,
|
||||
'h2h_home_win_rate': self.home_win_rate,
|
||||
'h2h_draw_rate': self.draw_rate,
|
||||
'h2h_away_win_rate': self.away_win_rate,
|
||||
'h2h_avg_goals': self.avg_total_goals,
|
||||
'h2h_btts_rate': self.btts_rate,
|
||||
'h2h_over25_rate': self.over25_rate,
|
||||
'h2h_home_dominance': self.home_dominance,
|
||||
}
|
||||
|
||||
|
||||
class H2HFeatureEngine:
|
||||
"""
|
||||
Head-to-Head Feature Engine
|
||||
|
||||
İki takım arasındaki geçmiş karşılaşmaları analiz eder.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._cache: Dict[Tuple[str, str], H2HProfile] = {}
|
||||
|
||||
def get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
return self.conn
|
||||
|
||||
def get_h2h_profile(self, home_team_id: str, away_team_id: str,
|
||||
before_date: Optional[int] = None,
|
||||
limit: int = 20) -> H2HProfile:
|
||||
"""
|
||||
İki takım arasındaki geçmiş karşılaşmaları analiz et.
|
||||
|
||||
Args:
|
||||
home_team_id: Ev sahibi takım ID
|
||||
away_team_id: Deplasman takım ID
|
||||
before_date: Bu tarihten önceki maçlar (mst_utc, milliseconds)
|
||||
limit: Kaç maç geriye bakılacak
|
||||
|
||||
Returns:
|
||||
H2HProfile: Head-to-head analiz sonucu
|
||||
"""
|
||||
cache_key = (home_team_id, away_team_id)
|
||||
|
||||
# Cache kontrolü (before_date yoksa)
|
||||
if before_date is None and cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
|
||||
conn = self.get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Her iki yöndeki karşılaşmaları al
|
||||
# (A evde B deplasman + B evde A deplasman)
|
||||
query = """
|
||||
SELECT
|
||||
home_team_id, away_team_id,
|
||||
score_home, score_away
|
||||
FROM matches
|
||||
WHERE (
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
OR
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
)
|
||||
AND score_home IS NOT NULL
|
||||
AND score_away IS NOT NULL
|
||||
"""
|
||||
|
||||
params = [home_team_id, away_team_id, away_team_id, home_team_id]
|
||||
|
||||
if before_date:
|
||||
query += " AND mst_utc < %s"
|
||||
params.append(before_date)
|
||||
|
||||
query += " ORDER BY mst_utc DESC LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
cur.execute(query, params)
|
||||
matches = cur.fetchall()
|
||||
|
||||
if not matches:
|
||||
return H2HProfile(
|
||||
total_matches=0, home_wins=0, draws=0, away_wins=0,
|
||||
home_goals_total=0, away_goals_total=0,
|
||||
btts_count=0, over25_count=0
|
||||
)
|
||||
|
||||
# İstatistikleri hesapla
|
||||
home_wins = 0
|
||||
draws = 0
|
||||
away_wins = 0
|
||||
home_goals = 0
|
||||
away_goals = 0
|
||||
btts = 0
|
||||
over25 = 0
|
||||
|
||||
for match in matches:
|
||||
m_home_id, m_away_id, score_h, score_a = match
|
||||
|
||||
# Perspektifi normalize et (istenen takım açısından)
|
||||
if m_home_id == home_team_id:
|
||||
# Normal sıralama
|
||||
h_score, a_score = score_h, score_a
|
||||
else:
|
||||
# Ters sıralama (rakip evde oynamış)
|
||||
h_score, a_score = score_a, score_h
|
||||
|
||||
# Sonuç
|
||||
if h_score > a_score:
|
||||
home_wins += 1
|
||||
elif h_score < a_score:
|
||||
away_wins += 1
|
||||
else:
|
||||
draws += 1
|
||||
|
||||
# Goller
|
||||
home_goals += h_score
|
||||
away_goals += a_score
|
||||
|
||||
# BTTS
|
||||
if h_score > 0 and a_score > 0:
|
||||
btts += 1
|
||||
|
||||
# Over 2.5
|
||||
if h_score + a_score > 2.5:
|
||||
over25 += 1
|
||||
|
||||
profile = H2HProfile(
|
||||
total_matches=len(matches),
|
||||
home_wins=home_wins,
|
||||
draws=draws,
|
||||
away_wins=away_wins,
|
||||
home_goals_total=home_goals,
|
||||
away_goals_total=away_goals,
|
||||
btts_count=btts,
|
||||
over25_count=over25
|
||||
)
|
||||
|
||||
# Cache'e kaydet
|
||||
if before_date is None:
|
||||
self._cache[cache_key] = profile
|
||||
|
||||
return profile
|
||||
|
||||
def get_features(self, home_team_id: str, away_team_id: str,
|
||||
before_date: Optional[int] = None) -> Dict[str, float]:
|
||||
"""Feature dictionary döndür"""
|
||||
profile = self.get_h2h_profile(home_team_id, away_team_id, before_date)
|
||||
return profile.to_features()
|
||||
|
||||
def get_momentum(self, home_team_id: str, away_team_id: str,
|
||||
before_date: Optional[int] = None) -> Dict[str, float]:
|
||||
"""
|
||||
Son karşılaşmalardaki momentum/trend analizi.
|
||||
Son 5 maçtaki trend'e bakar.
|
||||
"""
|
||||
profile = self.get_h2h_profile(home_team_id, away_team_id, before_date, limit=5)
|
||||
|
||||
# Streak hesapla (ardışık sonuçlar)
|
||||
conn = self.get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT home_team_id, score_home, score_away
|
||||
FROM matches
|
||||
WHERE (
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
OR
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
)
|
||||
AND score_home IS NOT NULL
|
||||
"""
|
||||
params = [home_team_id, away_team_id, away_team_id, home_team_id]
|
||||
if before_date:
|
||||
query += " AND mst_utc < %s"
|
||||
params.append(before_date)
|
||||
query += " ORDER BY mst_utc DESC LIMIT 5"
|
||||
|
||||
cur.execute(query, params)
|
||||
recent = cur.fetchall()
|
||||
|
||||
streak = 0
|
||||
streak_type = None # 'home', 'away', 'draw'
|
||||
|
||||
for match in recent:
|
||||
m_home_id, score_h, score_a = match
|
||||
|
||||
# Perspektifi normalize et
|
||||
if m_home_id == home_team_id:
|
||||
result = 'home' if score_h > score_a else ('away' if score_h < score_a else 'draw')
|
||||
else:
|
||||
result = 'away' if score_h > score_a else ('home' if score_h < score_a else 'draw')
|
||||
|
||||
if streak_type is None:
|
||||
streak_type = result
|
||||
streak = 1
|
||||
elif result == streak_type:
|
||||
streak += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return {
|
||||
'h2h_recent_home_dominance': profile.home_dominance,
|
||||
'h2h_streak_length': streak,
|
||||
'h2h_streak_home': 1 if streak_type == 'home' else 0,
|
||||
'h2h_streak_away': 1 if streak_type == 'away' else 0,
|
||||
'h2h_streak_draw': 1 if streak_type == 'draw' else 0,
|
||||
}
|
||||
|
||||
|
||||
# Singleton
|
||||
_engine = None
|
||||
|
||||
def get_h2h_engine() -> H2HFeatureEngine:
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = H2HFeatureEngine()
|
||||
return _engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test
|
||||
engine = get_h2h_engine()
|
||||
|
||||
# Örnek: Fenerbahçe vs Galatasaray (ID'leri bulunmalı)
|
||||
# Test için veritabanından bir karşılaşma çekelim
|
||||
conn = engine.get_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT home_team_id, away_team_id, match_name
|
||||
FROM matches
|
||||
WHERE score_home IS NOT NULL
|
||||
LIMIT 1
|
||||
""")
|
||||
result = cur.fetchone()
|
||||
|
||||
if result:
|
||||
home_id, away_id, name = result
|
||||
print(f"\n🧪 Test: {name}")
|
||||
print(f" Home ID: {home_id}")
|
||||
print(f" Away ID: {away_id}")
|
||||
|
||||
profile = engine.get_h2h_profile(home_id, away_id)
|
||||
print(f"\n📊 H2H Profil:")
|
||||
print(f" Toplam Maç: {profile.total_matches}")
|
||||
print(f" Ev Sahibi Kazanma: {profile.home_win_rate:.1%}")
|
||||
print(f" Beraberlik: {profile.draw_rate:.1%}")
|
||||
print(f" Deplasman Kazanma: {profile.away_win_rate:.1%}")
|
||||
print(f" Ortalama Gol: {profile.avg_total_goals:.2f}")
|
||||
print(f" BTTS Oranı: {profile.btts_rate:.1%}")
|
||||
print(f" Üst 2.5 Oranı: {profile.over25_rate:.1%}")
|
||||
print(f" Ev Dominance: {profile.home_dominance:+.2f}")
|
||||
|
||||
features = engine.get_features(home_id, away_id)
|
||||
print(f"\n🔧 Features: {features}")
|
||||
@@ -0,0 +1,343 @@
|
||||
"""
|
||||
HT/FT Tendency Feature Engine
|
||||
================================
|
||||
Produces team-level HT/FT tendency features for match prediction.
|
||||
|
||||
Computes ~15 features per match based on historical data:
|
||||
- 1st half scoring/conceding rates
|
||||
- Comeback rates
|
||||
- Half-specific goal distribution
|
||||
- League-level HT/FT profiles
|
||||
|
||||
All features are computed from the `matches` table using only data
|
||||
BEFORE the match date (no future leakage).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from typing import Dict, Optional, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from data.db import get_clean_dsn
|
||||
import psycopg2
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeamHtftProfile:
|
||||
"""HT/FT tendency profile for a single team."""
|
||||
matches: int = 0
|
||||
ht_scored: int = 0 # Matches where team scored in 1st half
|
||||
ht_conceded: int = 0 # Matches where team conceded in 1st half
|
||||
ht_leading: int = 0 # Matches where team led at HT
|
||||
ht_trailing: int = 0 # Matches where team trailed at HT
|
||||
comeback_wins: int = 0 # Trailing at HT -> Won
|
||||
goals_1h: int = 0
|
||||
goals_2h: int = 0
|
||||
conceded_1h: int = 0
|
||||
conceded_2h: int = 0
|
||||
|
||||
@property
|
||||
def ht_scoring_rate(self):
|
||||
return self.ht_scored / self.matches if self.matches > 0 else 0.5
|
||||
|
||||
@property
|
||||
def ht_concede_rate(self):
|
||||
return self.ht_conceded / self.matches if self.matches > 0 else 0.5
|
||||
|
||||
@property
|
||||
def ht_win_rate(self):
|
||||
return self.ht_leading / self.matches if self.matches > 0 else 0.33
|
||||
|
||||
@property
|
||||
def comeback_rate(self):
|
||||
return self.comeback_wins / self.ht_trailing if self.ht_trailing > 0 else 0.0
|
||||
|
||||
@property
|
||||
def first_half_goal_pct(self):
|
||||
total = self.goals_1h + self.goals_2h
|
||||
return self.goals_1h / total if total > 0 else 0.5
|
||||
|
||||
@property
|
||||
def second_half_surge(self):
|
||||
"""Ratio of 2H goals vs 1H goals. >1 means more dangerous in 2nd half."""
|
||||
return self.goals_2h / self.goals_1h if self.goals_1h > 0 else 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class LeagueHtftProfile:
|
||||
"""League-level HT/FT statistics."""
|
||||
matches: int = 0
|
||||
ht_goals_total: int = 0
|
||||
ft_goals_total: int = 0
|
||||
reversals: int = 0
|
||||
htft_counts: Dict[str, int] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def avg_ht_goals(self):
|
||||
return self.ht_goals_total / self.matches if self.matches > 0 else 1.0
|
||||
|
||||
@property
|
||||
def avg_2h_goals(self):
|
||||
ft = self.ft_goals_total / self.matches if self.matches > 0 else 2.5
|
||||
return ft - self.avg_ht_goals
|
||||
|
||||
@property
|
||||
def reversal_rate(self):
|
||||
return self.reversals / self.matches if self.matches > 0 else 0.05
|
||||
|
||||
@property
|
||||
def first_half_pct(self):
|
||||
return self.ht_goals_total / self.ft_goals_total if self.ft_goals_total > 0 else 0.44
|
||||
|
||||
|
||||
class HtftTendencyEngine:
|
||||
"""
|
||||
Computes HT/FT tendency features for a given match.
|
||||
|
||||
Uses historical data from `matches` table, filtering by date to
|
||||
avoid future leakage.
|
||||
|
||||
Features are based on team-level and league-level tendencies, which
|
||||
are DIFFERENT from the existing model features (ELO, form, H2H score).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._team_cache: Dict[Tuple[str, bool], TeamHtftProfile] = {}
|
||||
self._league_cache: Dict[str, LeagueHtftProfile] = {}
|
||||
|
||||
def get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
dsn = get_clean_dsn()
|
||||
self.conn = psycopg2.connect(dsn)
|
||||
return self.conn
|
||||
|
||||
def _get_team_htft_profile(
|
||||
self,
|
||||
team_id: str,
|
||||
is_home: bool,
|
||||
before_date: Optional[int] = None,
|
||||
limit: int = 30,
|
||||
) -> TeamHtftProfile:
|
||||
"""
|
||||
Compute HT/FT profile for a team from their recent matches.
|
||||
|
||||
Args:
|
||||
team_id: Team ID
|
||||
is_home: True = only home matches, False = only away matches
|
||||
before_date: Only use matches before this timestamp (ms UTC)
|
||||
limit: Number of recent matches to consider
|
||||
"""
|
||||
cache_key = (team_id, is_home, before_date)
|
||||
if cache_key in self._team_cache:
|
||||
return self._team_cache[cache_key]
|
||||
|
||||
conn = self.get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
if is_home:
|
||||
query = """
|
||||
SELECT ht_score_home, ht_score_away, score_home, score_away
|
||||
FROM matches
|
||||
WHERE home_team_id = %s
|
||||
AND sport = 'football'
|
||||
AND status = 'FT'
|
||||
AND ht_score_home IS NOT NULL
|
||||
AND ht_score_away IS NOT NULL
|
||||
"""
|
||||
else:
|
||||
query = """
|
||||
SELECT ht_score_away, ht_score_home, score_away, score_home
|
||||
FROM matches
|
||||
WHERE away_team_id = %s
|
||||
AND sport = 'football'
|
||||
AND status = 'FT'
|
||||
AND ht_score_home IS NOT NULL
|
||||
AND ht_score_away IS NOT NULL
|
||||
"""
|
||||
|
||||
params = [team_id]
|
||||
|
||||
if before_date:
|
||||
query += " AND mst_utc < %s"
|
||||
params.append(before_date)
|
||||
|
||||
query += " ORDER BY mst_utc DESC LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
profile = TeamHtftProfile()
|
||||
profile.matches = len(rows)
|
||||
|
||||
for ht_mine, ht_opp, ft_mine, ft_opp in rows:
|
||||
# 1st half scoring
|
||||
if ht_mine > 0:
|
||||
profile.ht_scored += 1
|
||||
if ht_opp > 0:
|
||||
profile.ht_conceded += 1
|
||||
|
||||
# HT situation
|
||||
if ht_mine > ht_opp:
|
||||
profile.ht_leading += 1
|
||||
elif ht_mine < ht_opp:
|
||||
profile.ht_trailing += 1
|
||||
# Comeback
|
||||
if ft_mine > ft_opp:
|
||||
profile.comeback_wins += 1
|
||||
|
||||
# Goal distribution
|
||||
profile.goals_1h += ht_mine
|
||||
profile.goals_2h += (ft_mine - ht_mine)
|
||||
profile.conceded_1h += ht_opp
|
||||
profile.conceded_2h += (ft_opp - ht_opp)
|
||||
|
||||
self._team_cache[cache_key] = profile
|
||||
return profile
|
||||
|
||||
def _get_league_htft_profile(
|
||||
self,
|
||||
league_id: str,
|
||||
before_date: Optional[int] = None,
|
||||
) -> LeagueHtftProfile:
|
||||
"""Compute HT/FT profile for a league."""
|
||||
cache_key = (league_id, before_date)
|
||||
if cache_key in self._league_cache:
|
||||
return self._league_cache[cache_key]
|
||||
|
||||
conn = self.get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT ht_score_home, ht_score_away, score_home, score_away
|
||||
FROM matches
|
||||
WHERE league_id = %s
|
||||
AND sport = 'football'
|
||||
AND status = 'FT'
|
||||
AND ht_score_home IS NOT NULL
|
||||
AND ht_score_away IS NOT NULL
|
||||
"""
|
||||
params = [league_id]
|
||||
|
||||
if before_date:
|
||||
query += " AND mst_utc < %s"
|
||||
params.append(before_date)
|
||||
|
||||
query += " ORDER BY mst_utc DESC LIMIT 500"
|
||||
params_final = params
|
||||
|
||||
cur.execute(query, params_final)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
profile = LeagueHtftProfile()
|
||||
profile.matches = len(rows)
|
||||
|
||||
for hth, hta, sh, sa in rows:
|
||||
profile.ht_goals_total += hth + hta
|
||||
profile.ft_goals_total += sh + sa
|
||||
|
||||
# Classify HT/FT
|
||||
ht = "1" if hth > hta else ("2" if hth < hta else "X")
|
||||
ft = "1" if sh > sa else ("2" if sh < sa else "X")
|
||||
htft = f"{ht}/{ft}"
|
||||
|
||||
profile.htft_counts[htft] = profile.htft_counts.get(htft, 0) + 1
|
||||
if htft in ("1/2", "2/1"):
|
||||
profile.reversals += 1
|
||||
|
||||
self._league_cache[cache_key] = profile
|
||||
return profile
|
||||
|
||||
def get_features(
|
||||
self,
|
||||
home_team_id: str,
|
||||
away_team_id: str,
|
||||
league_id: Optional[str] = None,
|
||||
before_date: Optional[int] = None,
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Get HT/FT tendency features for a match.
|
||||
|
||||
Returns dict with ~15 features.
|
||||
"""
|
||||
# Team profiles (home side for home team, away side for away team)
|
||||
home_prof = self._get_team_htft_profile(home_team_id, is_home=True, before_date=before_date)
|
||||
away_prof = self._get_team_htft_profile(away_team_id, is_home=False, before_date=before_date)
|
||||
|
||||
# League profile
|
||||
league_prof = LeagueHtftProfile()
|
||||
if league_id:
|
||||
league_prof = self._get_league_htft_profile(league_id, before_date=before_date)
|
||||
|
||||
features = {
|
||||
# Home team HT/FT tendencies
|
||||
"htft_home_ht_scoring_rate": home_prof.ht_scoring_rate,
|
||||
"htft_home_ht_concede_rate": home_prof.ht_concede_rate,
|
||||
"htft_home_ht_win_rate": home_prof.ht_win_rate,
|
||||
"htft_home_comeback_rate": home_prof.comeback_rate,
|
||||
"htft_home_first_half_goal_pct": home_prof.first_half_goal_pct,
|
||||
"htft_home_second_half_surge": min(home_prof.second_half_surge, 3.0),
|
||||
|
||||
# Away team HT/FT tendencies
|
||||
"htft_away_ht_scoring_rate": away_prof.ht_scoring_rate,
|
||||
"htft_away_ht_concede_rate": away_prof.ht_concede_rate,
|
||||
"htft_away_ht_win_rate": away_prof.ht_win_rate,
|
||||
"htft_away_comeback_rate": away_prof.comeback_rate,
|
||||
"htft_away_first_half_goal_pct": away_prof.first_half_goal_pct,
|
||||
"htft_away_second_half_surge": min(away_prof.second_half_surge, 3.0),
|
||||
|
||||
# League-level
|
||||
"htft_league_avg_ht_goals": league_prof.avg_ht_goals,
|
||||
"htft_league_reversal_rate": league_prof.reversal_rate,
|
||||
"htft_league_first_half_pct": league_prof.first_half_pct,
|
||||
|
||||
# Data quality (how many matches we have for these features)
|
||||
"htft_home_sample_size": min(home_prof.matches / 30.0, 1.0),
|
||||
"htft_away_sample_size": min(away_prof.matches / 30.0, 1.0),
|
||||
}
|
||||
|
||||
return features
|
||||
|
||||
def clear_cache(self):
|
||||
"""Clear internal caches (useful between batches)."""
|
||||
self._team_cache.clear()
|
||||
self._league_cache.clear()
|
||||
|
||||
|
||||
# Singleton
|
||||
_engine = None
|
||||
|
||||
|
||||
def get_htft_tendency_engine() -> HtftTendencyEngine:
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = HtftTendencyEngine()
|
||||
return _engine
|
||||
|
||||
|
||||
# ── Test ─────────────────────────────────────────────────────────────────────
|
||||
if __name__ == "__main__":
|
||||
engine = get_htft_tendency_engine()
|
||||
|
||||
conn = engine.get_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT home_team_id, away_team_id, league_id, mst_utc, match_name
|
||||
FROM matches
|
||||
WHERE sport = 'football' AND status = 'FT'
|
||||
AND home_team_id IS NOT NULL AND away_team_id IS NOT NULL
|
||||
ORDER BY mst_utc DESC LIMIT 3
|
||||
""")
|
||||
matches = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
for hid, aid, lid, mst, name in matches:
|
||||
print(f"\n🏟️ {name}")
|
||||
features = engine.get_features(hid, aid, lid, mst)
|
||||
for k, v in sorted(features.items()):
|
||||
print(f" {k}: {v:.4f}")
|
||||
Executable
+434
@@ -0,0 +1,434 @@
|
||||
"""
|
||||
Momentum Engine - Son Maç Trendleri
|
||||
V9 Model için takımların anlık form trendini analiz eder.
|
||||
|
||||
Faktörler:
|
||||
1. Gol atma trendi (artan/azalan/stabil)
|
||||
2. Yenilmezlik/yenilgi serisi
|
||||
3. Son maç psikolojisi (büyük galibiyet/mağlubiyet etkisi)
|
||||
4. Ev/Deplasman momentum farkı
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MomentumData:
|
||||
"""Takım momentum verileri"""
|
||||
goals_trend: float = 0.0 # -1 (azalan) to +1 (artan)
|
||||
conceded_trend: float = 0.0 # -1 (azalan) to +1 (artan) [negatif iyi]
|
||||
unbeaten_streak: int = 0 # Yenilmezlik serisi
|
||||
losing_streak: int = 0 # Yenilgi serisi
|
||||
winning_streak: int = 0 # Galibiyet serisi
|
||||
last_match_impact: float = 0.0 # Son maç psikolojik etkisi (-1 to +1)
|
||||
momentum_score: float = 0.0 # Toplam momentum (-1 to +1)
|
||||
form_direction: str = "stable" # "improving", "declining", "stable"
|
||||
xg_underperformance: float = 0.0 # (xG_For - Real_Goals) in last matches (>0 means underperforming)
|
||||
xg_conceded_diff: float = 0.0 # (Real_Conceded - xG_Against) in last matches
|
||||
|
||||
|
||||
class MomentumEngine:
|
||||
"""
|
||||
Son maçlardaki trendi analiz eder.
|
||||
Form yükselişi/düşüşü, seriler ve psikolojik etki.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._connect_db()
|
||||
|
||||
def _connect_db(self):
|
||||
"""Veritabanına bağlan"""
|
||||
if psycopg2 is None:
|
||||
return
|
||||
|
||||
try:
|
||||
from data.db import get_clean_dsn
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
except Exception as e:
|
||||
print(f"[MomentumEngine] DB connection failed: {e}")
|
||||
self.conn = None
|
||||
|
||||
def _get_conn(self):
|
||||
"""Bağlantıyı kontrol et ve döndür"""
|
||||
if self.conn is None or self.conn.closed:
|
||||
self._connect_db()
|
||||
return self.conn
|
||||
|
||||
def get_recent_matches(
|
||||
self,
|
||||
team_id: str,
|
||||
before_date_ms: int,
|
||||
limit: int = 5,
|
||||
home_only: bool = False,
|
||||
away_only: bool = False
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Takımın son maçlarını getir.
|
||||
|
||||
Returns:
|
||||
List of matches with scores and home/away info
|
||||
"""
|
||||
conn = self._get_conn()
|
||||
if conn is None:
|
||||
return []
|
||||
|
||||
try:
|
||||
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
conditions = ["mst_utc < %s", "score_home IS NOT NULL"]
|
||||
params = [before_date_ms]
|
||||
|
||||
if home_only:
|
||||
conditions.append("home_team_id = %s")
|
||||
params.append(team_id)
|
||||
elif away_only:
|
||||
conditions.append("away_team_id = %s")
|
||||
params.append(team_id)
|
||||
else:
|
||||
conditions.append("(home_team_id = %s OR away_team_id = %s)")
|
||||
params.extend([team_id, team_id])
|
||||
query = f"""
|
||||
SELECT
|
||||
id, home_team_id, away_team_id,
|
||||
score_home, score_away, mst_utc
|
||||
FROM matches
|
||||
WHERE {' AND '.join(conditions)}
|
||||
ORDER BY mst_utc DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
params.append(limit)
|
||||
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[MomentumEngine] Query error: {e}")
|
||||
return []
|
||||
|
||||
def calculate_goals_trend(self, matches: List[Dict], team_id: str) -> Tuple[float, float]:
|
||||
"""
|
||||
Gol atma ve yeme trendini hesapla.
|
||||
Son 3 maç vs önceki 2 maç karşılaştırması.
|
||||
|
||||
Returns:
|
||||
(goals_trend, conceded_trend) - -1 to +1
|
||||
"""
|
||||
if len(matches) < 3:
|
||||
return 0.0, 0.0
|
||||
|
||||
# Her maç için gol ve yenilen gol hesapla
|
||||
goals = []
|
||||
conceded = []
|
||||
|
||||
for match in matches:
|
||||
if match['home_team_id'] == team_id:
|
||||
goals.append(match['score_home'])
|
||||
conceded.append(match['score_away'])
|
||||
else:
|
||||
goals.append(match['score_away'])
|
||||
conceded.append(match['score_home'])
|
||||
|
||||
# Son 3 vs önceki maçlar
|
||||
recent_goals = sum(goals[:3]) / 3 if len(goals) >= 3 else 0
|
||||
older_goals = sum(goals[3:]) / len(goals[3:]) if len(goals) > 3 else recent_goals
|
||||
|
||||
recent_conceded = sum(conceded[:3]) / 3 if len(conceded) >= 3 else 0
|
||||
older_conceded = sum(conceded[3:]) / len(conceded[3:]) if len(conceded) > 3 else recent_conceded
|
||||
|
||||
# Trend hesapla (-1 to +1)
|
||||
goals_trend = min(max((recent_goals - older_goals) / 2, -1), 1)
|
||||
conceded_trend = min(max((recent_conceded - older_conceded) / 2, -1), 1)
|
||||
|
||||
return goals_trend, conceded_trend
|
||||
|
||||
def calculate_streaks(self, matches: List[Dict], team_id: str) -> Tuple[int, int, int]:
|
||||
"""
|
||||
Galibiyet, yenilmezlik ve yenilgi serilerini hesapla.
|
||||
|
||||
Returns:
|
||||
(winning_streak, unbeaten_streak, losing_streak)
|
||||
"""
|
||||
winning = 0
|
||||
unbeaten = 0
|
||||
losing = 0
|
||||
|
||||
for match in matches:
|
||||
# Sonucu belirle
|
||||
if match['home_team_id'] == team_id:
|
||||
goals_for = match['score_home']
|
||||
goals_against = match['score_away']
|
||||
else:
|
||||
goals_for = match['score_away']
|
||||
goals_against = match['score_home']
|
||||
|
||||
if goals_for > goals_against: # Galibiyet
|
||||
if losing == 0: # Henüz yenilgi serisi başlamamış
|
||||
winning += 1
|
||||
unbeaten += 1
|
||||
else:
|
||||
break
|
||||
elif goals_for == goals_against: # Beraberlik
|
||||
if losing == 0:
|
||||
winning = 0 # Galibiyet serisi bitti
|
||||
unbeaten += 1
|
||||
else:
|
||||
break
|
||||
else: # Yenilgi
|
||||
if winning > 0 or unbeaten > 0:
|
||||
winning = 0
|
||||
unbeaten = 0
|
||||
losing += 1
|
||||
|
||||
return winning, unbeaten, losing
|
||||
|
||||
def calculate_last_match_impact(self, matches: List[Dict], team_id: str) -> float:
|
||||
"""
|
||||
Son maçın psikolojik etkisini hesapla.
|
||||
Büyük galibiyet = +1, büyük mağlubiyet = -1
|
||||
|
||||
Returns:
|
||||
impact score: -1 to +1
|
||||
"""
|
||||
if not matches:
|
||||
return 0.0
|
||||
|
||||
last_match = matches[0]
|
||||
|
||||
if last_match['home_team_id'] == team_id:
|
||||
goals_for = last_match['score_home']
|
||||
goals_against = last_match['score_away']
|
||||
else:
|
||||
goals_for = last_match['score_away']
|
||||
goals_against = last_match['score_home']
|
||||
|
||||
goal_diff = goals_for - goals_against
|
||||
|
||||
# Gol farkına göre etki
|
||||
if goal_diff >= 4:
|
||||
return 1.0 # Çok büyük galibiyet
|
||||
elif goal_diff >= 2:
|
||||
return 0.6
|
||||
elif goal_diff == 1:
|
||||
return 0.3
|
||||
elif goal_diff == 0:
|
||||
return 0.0
|
||||
elif goal_diff == -1:
|
||||
return -0.3
|
||||
elif goal_diff >= -3:
|
||||
return -0.6
|
||||
else:
|
||||
return -1.0 # Çok büyük mağlubiyet
|
||||
|
||||
def calculate_xg_underperformance(self, matches: List[Dict], team_id: str) -> Tuple[float, float]:
|
||||
"""
|
||||
Calculate if a team chronically underperforms its xG (Expected Goals).
|
||||
Returns:
|
||||
(xg_strike_diff, xg_defend_diff)
|
||||
xg_strike_diff: > 0 means they score LESS than expected (Bad Finishers)
|
||||
xg_defend_diff: > 0 means they concede MORE than expected (Bad Goalkeeper/Luck)
|
||||
"""
|
||||
if not matches:
|
||||
return 0.0, 0.0
|
||||
|
||||
real_scored = 0
|
||||
xg_created = 0.0
|
||||
|
||||
real_conceded = 0
|
||||
xg_conceded = 0.0
|
||||
|
||||
for m in matches:
|
||||
is_home = (m['home_team_id'] == team_id)
|
||||
if is_home:
|
||||
real_scored += m['score_home']
|
||||
real_conceded += m['score_away']
|
||||
# Create synthetic xG data (mock based on score for demo since stats table absent)
|
||||
xg_created += max(0.5, m['score_home'] * 1.5 - 0.5)
|
||||
xg_conceded += max(0.5, m['score_away'] * 1.5 - 0.5)
|
||||
else:
|
||||
real_scored += m['score_away']
|
||||
real_conceded += m['score_home']
|
||||
xg_created += max(0.5, m['score_away'] * 1.5 - 0.5)
|
||||
xg_conceded += max(0.5, m['score_home'] * 1.5 - 0.5)
|
||||
|
||||
# Calculate per match diffs
|
||||
match_count = len(matches)
|
||||
|
||||
xg_strike_diff = (xg_created - real_scored) / match_count if match_count else 0
|
||||
xg_defend_diff = (real_conceded - xg_conceded) / match_count if match_count else 0
|
||||
|
||||
return xg_strike_diff, xg_defend_diff
|
||||
|
||||
def calculate_momentum(
|
||||
self,
|
||||
team_id: str,
|
||||
before_date_ms: int,
|
||||
match_limit: int = 5
|
||||
) -> MomentumData:
|
||||
"""
|
||||
Takımın tam momentum analizini yap.
|
||||
|
||||
Returns:
|
||||
MomentumData with all metrics
|
||||
"""
|
||||
data = MomentumData()
|
||||
|
||||
matches = self.get_recent_matches(team_id, before_date_ms, match_limit)
|
||||
|
||||
if not matches:
|
||||
return data
|
||||
|
||||
# 1. Gol trendi
|
||||
data.goals_trend, data.conceded_trend = self.calculate_goals_trend(matches, team_id)
|
||||
|
||||
# 2. Seriler
|
||||
data.winning_streak, data.unbeaten_streak, data.losing_streak = \
|
||||
self.calculate_streaks(matches, team_id)
|
||||
|
||||
# 3. Son maç etkisi
|
||||
data.last_match_impact = self.calculate_last_match_impact(matches, team_id)
|
||||
|
||||
# 4. Form yönü belirleme
|
||||
if data.goals_trend > 0.3 and data.conceded_trend < 0:
|
||||
data.form_direction = "improving"
|
||||
elif data.goals_trend < -0.3 or data.conceded_trend > 0.3:
|
||||
data.form_direction = "declining"
|
||||
else:
|
||||
data.form_direction = "stable"
|
||||
|
||||
# 5. xG Underperformance (Chronik beceriksizlik)
|
||||
data.xg_underperformance, data.xg_conceded_diff = self.calculate_xg_underperformance(matches, team_id)
|
||||
|
||||
# 6. Toplam momentum skoru
|
||||
momentum = 0.0
|
||||
|
||||
# Gol trendi + savunma trendi (ters çevrilmiş)
|
||||
momentum += data.goals_trend * 0.25
|
||||
momentum += (-data.conceded_trend) * 0.20
|
||||
|
||||
# Seri bonusları
|
||||
if data.winning_streak >= 3:
|
||||
momentum += 0.25
|
||||
elif data.winning_streak >= 2:
|
||||
momentum += 0.15
|
||||
elif data.unbeaten_streak >= 5:
|
||||
momentum += 0.15
|
||||
|
||||
if data.losing_streak >= 3:
|
||||
momentum -= 0.30
|
||||
elif data.losing_streak >= 2:
|
||||
momentum -= 0.15
|
||||
|
||||
# Son maç etkisi
|
||||
momentum += data.last_match_impact * 0.20
|
||||
|
||||
# Ceza: xG Underperformance Penalty (Beceriksizlik Cezası)
|
||||
# Eğer takım attığından çok xG üretiyorsa (- puan)
|
||||
if data.xg_underperformance > 0.5: # Maç başı 0.5 gol eksik atıyor!
|
||||
momentum -= min(0.3, data.xg_underperformance * 0.2)
|
||||
|
||||
# Ceza: xG Defend Underperformance (Kötü kaleci Cezası)
|
||||
# Eğer beklenenden çok gol yiyorsa
|
||||
if data.xg_conceded_diff > 0.5:
|
||||
momentum -= min(0.3, data.xg_conceded_diff * 0.2)
|
||||
|
||||
data.momentum_score = min(max(momentum, -1), 1)
|
||||
|
||||
return data
|
||||
|
||||
def get_features(
|
||||
self,
|
||||
home_team_id: str,
|
||||
away_team_id: str,
|
||||
match_date_ms: int
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Model için feature dict döndür.
|
||||
"""
|
||||
home_momentum = self.calculate_momentum(home_team_id, match_date_ms)
|
||||
away_momentum = self.calculate_momentum(away_team_id, match_date_ms)
|
||||
|
||||
# Form direction encoding
|
||||
direction_map = {"improving": 1, "stable": 0, "declining": -1}
|
||||
|
||||
return {
|
||||
# Ev sahibi momentum
|
||||
"home_momentum_score": home_momentum.momentum_score,
|
||||
"home_goals_trend": home_momentum.goals_trend,
|
||||
"home_conceded_trend": home_momentum.conceded_trend,
|
||||
"home_winning_streak": min(home_momentum.winning_streak, 5),
|
||||
"home_unbeaten_streak": min(home_momentum.unbeaten_streak, 10),
|
||||
"home_losing_streak": min(home_momentum.losing_streak, 5),
|
||||
"home_last_impact": home_momentum.last_match_impact,
|
||||
"home_form_direction": direction_map.get(home_momentum.form_direction, 0),
|
||||
"home_xg_underperf": home_momentum.xg_underperformance,
|
||||
"home_xg_conceded_diff": home_momentum.xg_conceded_diff,
|
||||
|
||||
# Deplasman momentum
|
||||
"away_momentum_score": away_momentum.momentum_score,
|
||||
"away_goals_trend": away_momentum.goals_trend,
|
||||
"away_conceded_trend": away_momentum.conceded_trend,
|
||||
"away_winning_streak": min(away_momentum.winning_streak, 5),
|
||||
"away_unbeaten_streak": min(away_momentum.unbeaten_streak, 10),
|
||||
"away_losing_streak": min(away_momentum.losing_streak, 5),
|
||||
"away_last_impact": away_momentum.last_match_impact,
|
||||
"away_form_direction": direction_map.get(away_momentum.form_direction, 0),
|
||||
"away_xg_underperf": away_momentum.xg_underperformance,
|
||||
"away_xg_conceded_diff": away_momentum.xg_conceded_diff,
|
||||
|
||||
# Farklar
|
||||
"momentum_diff": home_momentum.momentum_score - away_momentum.momentum_score,
|
||||
"trend_diff": (home_momentum.goals_trend - home_momentum.conceded_trend) -
|
||||
(away_momentum.goals_trend - away_momentum.conceded_trend),
|
||||
"xg_underperf_diff": home_momentum.xg_underperformance - away_momentum.xg_underperformance,
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_engine_instance = None
|
||||
|
||||
def get_momentum_engine() -> MomentumEngine:
|
||||
"""Singleton pattern ile engine döndür"""
|
||||
global _engine_instance
|
||||
if _engine_instance is None:
|
||||
_engine_instance = MomentumEngine()
|
||||
return _engine_instance
|
||||
|
||||
|
||||
# Test
|
||||
if __name__ == "__main__":
|
||||
engine = get_momentum_engine()
|
||||
|
||||
# Test data
|
||||
print("=" * 60)
|
||||
print("MOMENTUM ENGINE TEST")
|
||||
print("=" * 60)
|
||||
|
||||
# Örnek hesaplama (DB olmadan)
|
||||
data = MomentumData(
|
||||
goals_trend=0.5,
|
||||
conceded_trend=-0.3,
|
||||
winning_streak=3,
|
||||
unbeaten_streak=5,
|
||||
losing_streak=0,
|
||||
last_match_impact=0.6,
|
||||
form_direction="improving"
|
||||
)
|
||||
|
||||
print(f"Goals Trend: {data.goals_trend}")
|
||||
print(f"Conceded Trend: {data.conceded_trend}")
|
||||
print(f"Winning Streak: {data.winning_streak}")
|
||||
print(f"Unbeaten Streak: {data.unbeaten_streak}")
|
||||
print(f"Form Direction: {data.form_direction}")
|
||||
print(f"Last Match Impact: {data.last_match_impact}")
|
||||
File diff suppressed because it is too large
Load Diff
Executable
+371
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
Poisson Engine - Matematiksel Gol Modeli
|
||||
V9 Model için Poisson dağılımı ile gol olasılıkları hesaplar.
|
||||
|
||||
Özellikler:
|
||||
1. Exact score olasılıkları (0-0, 1-0, 1-1, 2-1, vb.)
|
||||
2. Over/Under olasılıkları (matematiksel)
|
||||
3. BTTS (Karşılıklı Gol) olasılıkları
|
||||
4. Expected Goals (xG) tahmini
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Dict, Tuple, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
def poisson_prob(lam: float, k: int) -> float:
|
||||
"""
|
||||
Poisson olasılık formülü.
|
||||
P(X = k) = (λ^k * e^(-λ)) / k!
|
||||
"""
|
||||
if lam <= 0:
|
||||
return 1.0 if k == 0 else 0.0
|
||||
return (math.pow(lam, k) * math.exp(-lam)) / math.factorial(k)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PoissonPrediction:
|
||||
"""Poisson tahmin sonuçları"""
|
||||
home_xg: float = 0.0 # Ev sahibi beklenen gol
|
||||
away_xg: float = 0.0 # Deplasman beklenen gol
|
||||
total_xg: float = 0.0 # Toplam beklenen gol
|
||||
|
||||
# Maç sonucu olasılıkları
|
||||
home_win_prob: float = 0.0
|
||||
draw_prob: float = 0.0
|
||||
away_win_prob: float = 0.0
|
||||
|
||||
# Alt/Üst olasılıkları
|
||||
over_15_prob: float = 0.0
|
||||
over_25_prob: float = 0.0
|
||||
over_35_prob: float = 0.0
|
||||
under_15_prob: float = 0.0
|
||||
under_25_prob: float = 0.0
|
||||
under_35_prob: float = 0.0
|
||||
|
||||
# BTTS
|
||||
btts_yes_prob: float = 0.0
|
||||
btts_no_prob: float = 0.0
|
||||
|
||||
# En olası skorlar
|
||||
most_likely_scores: list = field(default_factory=list)
|
||||
|
||||
|
||||
class PoissonEngine:
|
||||
"""
|
||||
Poisson dağılımı ile gol olasılıkları hesaplar.
|
||||
İstatistiksel bir yaklaşım - machine learning'den bağımsız.
|
||||
"""
|
||||
|
||||
# Lig bazlı ortalama gol verileri (varsayılan değerler)
|
||||
DEFAULT_HOME_XG = 1.45
|
||||
DEFAULT_AWAY_XG = 1.15
|
||||
DEFAULT_LEAGUE_AVG = 2.60
|
||||
|
||||
def __init__(self):
|
||||
self.max_goals = 7 # Hesaplama için maksimum gol sayısı
|
||||
|
||||
def calculate_xg(
|
||||
self,
|
||||
home_goals_avg: float,
|
||||
home_conceded_avg: float,
|
||||
away_goals_avg: float,
|
||||
away_conceded_avg: float,
|
||||
league_home_avg: float = None,
|
||||
league_away_avg: float = None,
|
||||
league_total_avg: float = None
|
||||
) -> Tuple[float, float]:
|
||||
"""
|
||||
Beklenen gol (xG) hesapla.
|
||||
|
||||
Attack strength * Defense weakness * League average
|
||||
"""
|
||||
# Varsayılan lig ortalamaları
|
||||
if league_home_avg is None:
|
||||
league_home_avg = self.DEFAULT_HOME_XG
|
||||
if league_away_avg is None:
|
||||
league_away_avg = self.DEFAULT_AWAY_XG
|
||||
if league_total_avg is None:
|
||||
league_total_avg = self.DEFAULT_LEAGUE_AVG
|
||||
|
||||
# Güç hesaplamaları
|
||||
# Ev sahibi saldırı gücü = Ev gol ortalaması / Lig ev gol ortalaması
|
||||
home_attack = home_goals_avg / league_home_avg if league_home_avg > 0 else 1.0
|
||||
# Deplasman savunma zayıflığı = Deplasman yenilen gol / Lig deplasman yenilen
|
||||
away_defense = away_conceded_avg / league_away_avg if league_away_avg > 0 else 1.0
|
||||
|
||||
# Deplasman saldırı gücü
|
||||
away_attack = away_goals_avg / league_away_avg if league_away_avg > 0 else 1.0
|
||||
# Ev sahibi savunma zayıflığı
|
||||
home_defense = home_conceded_avg / league_home_avg if league_home_avg > 0 else 1.0
|
||||
|
||||
# Expected Goals
|
||||
home_xg = home_attack * away_defense * league_home_avg
|
||||
away_xg = away_attack * home_defense * league_away_avg
|
||||
|
||||
# Aşırı değerleri sınırla
|
||||
home_xg = max(0.3, min(home_xg, 4.0))
|
||||
away_xg = max(0.2, min(away_xg, 3.5))
|
||||
|
||||
return home_xg, away_xg
|
||||
|
||||
def calculate_score_matrix(
|
||||
self,
|
||||
home_xg: float,
|
||||
away_xg: float
|
||||
) -> Dict[Tuple[int, int], float]:
|
||||
"""
|
||||
Tüm skor kombinasyonlarının olasılıklarını hesapla.
|
||||
|
||||
Returns:
|
||||
Dict[(home_goals, away_goals)] = probability
|
||||
"""
|
||||
matrix = {}
|
||||
|
||||
for home_goals in range(self.max_goals + 1):
|
||||
for away_goals in range(self.max_goals + 1):
|
||||
prob = poisson_prob(home_xg, home_goals) * poisson_prob(away_xg, away_goals)
|
||||
matrix[(home_goals, away_goals)] = prob
|
||||
|
||||
return matrix
|
||||
|
||||
def calculate_match_odds(
|
||||
self,
|
||||
home_xg: float,
|
||||
away_xg: float
|
||||
) -> Tuple[float, float, float]:
|
||||
"""
|
||||
1X2 olasılıklarını hesapla.
|
||||
|
||||
Returns:
|
||||
(home_win, draw, away_win) probabilities
|
||||
"""
|
||||
matrix = self.calculate_score_matrix(home_xg, away_xg)
|
||||
|
||||
home_win = 0.0
|
||||
draw = 0.0
|
||||
away_win = 0.0
|
||||
|
||||
for (h, a), prob in matrix.items():
|
||||
if h > a:
|
||||
home_win += prob
|
||||
elif h == a:
|
||||
draw += prob
|
||||
else:
|
||||
away_win += prob
|
||||
|
||||
# Normalize (toplam 1 olmalı)
|
||||
total = home_win + draw + away_win
|
||||
if total > 0:
|
||||
home_win /= total
|
||||
draw /= total
|
||||
away_win /= total
|
||||
|
||||
return home_win, draw, away_win
|
||||
|
||||
def calculate_over_under(
|
||||
self,
|
||||
home_xg: float,
|
||||
away_xg: float
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Alt/Üst olasılıklarını hesapla.
|
||||
"""
|
||||
matrix = self.calculate_score_matrix(home_xg, away_xg)
|
||||
|
||||
over_15 = 0.0
|
||||
over_25 = 0.0
|
||||
over_35 = 0.0
|
||||
|
||||
for (h, a), prob in matrix.items():
|
||||
total = h + a
|
||||
if total > 1.5:
|
||||
over_15 += prob
|
||||
if total > 2.5:
|
||||
over_25 += prob
|
||||
if total > 3.5:
|
||||
over_35 += prob
|
||||
|
||||
return {
|
||||
"over_15": over_15,
|
||||
"over_25": over_25,
|
||||
"over_35": over_35,
|
||||
"under_15": 1 - over_15,
|
||||
"under_25": 1 - over_25,
|
||||
"under_35": 1 - over_35,
|
||||
}
|
||||
|
||||
def calculate_btts(
|
||||
self,
|
||||
home_xg: float,
|
||||
away_xg: float
|
||||
) -> Tuple[float, float]:
|
||||
"""
|
||||
Karşılıklı Gol (Both Teams To Score) olasılığı.
|
||||
"""
|
||||
# P(Home scores at least 1) = 1 - P(Home scores 0)
|
||||
home_scores = 1 - poisson_prob(home_xg, 0)
|
||||
# P(Away scores at least 1) = 1 - P(Away scores 0)
|
||||
away_scores = 1 - poisson_prob(away_xg, 0)
|
||||
|
||||
# P(BTTS) = P(Home scores) * P(Away scores)
|
||||
btts_yes = home_scores * away_scores
|
||||
btts_no = 1 - btts_yes
|
||||
|
||||
return btts_yes, btts_no
|
||||
|
||||
def get_most_likely_scores(
|
||||
self,
|
||||
home_xg: float,
|
||||
away_xg: float,
|
||||
top_n: int = 5
|
||||
) -> list:
|
||||
"""
|
||||
En olası skorları getir.
|
||||
"""
|
||||
matrix = self.calculate_score_matrix(home_xg, away_xg)
|
||||
|
||||
# Olasılığa göre sırala
|
||||
sorted_scores = sorted(matrix.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
return [
|
||||
{"score": f"{h}-{a}", "probability": round(prob * 100, 1)}
|
||||
for (h, a), prob in sorted_scores[:top_n]
|
||||
]
|
||||
|
||||
def predict(
|
||||
self,
|
||||
home_goals_avg: float,
|
||||
home_conceded_avg: float,
|
||||
away_goals_avg: float,
|
||||
away_conceded_avg: float,
|
||||
league_home_avg: float = None,
|
||||
league_away_avg: float = None,
|
||||
league_total_avg: float = None
|
||||
) -> PoissonPrediction:
|
||||
"""
|
||||
Tam Poisson tahmini.
|
||||
"""
|
||||
prediction = PoissonPrediction()
|
||||
|
||||
# 1. xG hesapla
|
||||
home_xg, away_xg = self.calculate_xg(
|
||||
home_goals_avg, home_conceded_avg,
|
||||
away_goals_avg, away_conceded_avg,
|
||||
league_home_avg, league_away_avg, league_total_avg
|
||||
)
|
||||
|
||||
prediction.home_xg = round(home_xg, 2)
|
||||
prediction.away_xg = round(away_xg, 2)
|
||||
prediction.total_xg = round(home_xg + away_xg, 2)
|
||||
|
||||
# 2. Maç sonucu
|
||||
hw, d, aw = self.calculate_match_odds(home_xg, away_xg)
|
||||
prediction.home_win_prob = round(hw, 3)
|
||||
prediction.draw_prob = round(d, 3)
|
||||
prediction.away_win_prob = round(aw, 3)
|
||||
|
||||
# 3. Alt/Üst
|
||||
ou = self.calculate_over_under(home_xg, away_xg)
|
||||
prediction.over_15_prob = round(ou["over_15"], 3)
|
||||
prediction.over_25_prob = round(ou["over_25"], 3)
|
||||
prediction.over_35_prob = round(ou["over_35"], 3)
|
||||
prediction.under_15_prob = round(ou["under_15"], 3)
|
||||
prediction.under_25_prob = round(ou["under_25"], 3)
|
||||
prediction.under_35_prob = round(ou["under_35"], 3)
|
||||
|
||||
# 4. BTTS
|
||||
btts_yes, btts_no = self.calculate_btts(home_xg, away_xg)
|
||||
prediction.btts_yes_prob = round(btts_yes, 3)
|
||||
prediction.btts_no_prob = round(btts_no, 3)
|
||||
|
||||
# 5. En olası skorlar
|
||||
prediction.most_likely_scores = self.get_most_likely_scores(home_xg, away_xg)
|
||||
|
||||
return prediction
|
||||
|
||||
def get_features(
|
||||
self,
|
||||
home_goals_avg: float,
|
||||
home_conceded_avg: float,
|
||||
away_goals_avg: float,
|
||||
away_conceded_avg: float,
|
||||
league_home_avg: float = None,
|
||||
league_away_avg: float = None,
|
||||
league_total_avg: float = None
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Model için feature dict.
|
||||
"""
|
||||
pred = self.predict(
|
||||
home_goals_avg, home_conceded_avg,
|
||||
away_goals_avg, away_conceded_avg,
|
||||
league_home_avg, league_away_avg, league_total_avg
|
||||
)
|
||||
|
||||
return {
|
||||
"poisson_home_xg": pred.home_xg,
|
||||
"poisson_away_xg": pred.away_xg,
|
||||
"poisson_total_xg": pred.total_xg,
|
||||
"poisson_home_win": pred.home_win_prob,
|
||||
"poisson_draw": pred.draw_prob,
|
||||
"poisson_away_win": pred.away_win_prob,
|
||||
"poisson_over_15": pred.over_15_prob,
|
||||
"poisson_over_25": pred.over_25_prob,
|
||||
"poisson_over_35": pred.over_35_prob,
|
||||
"poisson_btts_yes": pred.btts_yes_prob,
|
||||
}
|
||||
|
||||
|
||||
# Singleton
|
||||
_engine_instance = None
|
||||
|
||||
def get_poisson_engine() -> PoissonEngine:
|
||||
"""Singleton pattern"""
|
||||
global _engine_instance
|
||||
if _engine_instance is None:
|
||||
_engine_instance = PoissonEngine()
|
||||
return _engine_instance
|
||||
|
||||
|
||||
# Test
|
||||
if __name__ == "__main__":
|
||||
engine = get_poisson_engine()
|
||||
|
||||
# Örnek: Güçlü ev sahibi vs zayıf deplasman
|
||||
print("=" * 60)
|
||||
print("POISSON ENGINE TEST")
|
||||
print("Galatasaray (ev) vs Antalyaspor (deplasman)")
|
||||
print("=" * 60)
|
||||
|
||||
pred = engine.predict(
|
||||
home_goals_avg=2.1, # GS ev ortalaması
|
||||
home_conceded_avg=0.8, # GS ev yenilen
|
||||
away_goals_avg=0.9, # Antalya deplasman gol
|
||||
away_conceded_avg=1.8, # Antalya deplasman yenilen
|
||||
league_home_avg=1.5,
|
||||
league_away_avg=1.1
|
||||
)
|
||||
|
||||
print(f"\n📊 Expected Goals:")
|
||||
print(f" Ev Sahibi xG: {pred.home_xg}")
|
||||
print(f" Deplasman xG: {pred.away_xg}")
|
||||
print(f" Toplam xG: {pred.total_xg}")
|
||||
|
||||
print(f"\n🎯 Maç Sonucu:")
|
||||
print(f" 1 (Ev): {pred.home_win_prob*100:.1f}%")
|
||||
print(f" X (Beraberlik): {pred.draw_prob*100:.1f}%")
|
||||
print(f" 2 (Deplasman): {pred.away_win_prob*100:.1f}%")
|
||||
|
||||
print(f"\n⚽ Alt/Üst:")
|
||||
print(f" 2.5 Üst: {pred.over_25_prob*100:.1f}%")
|
||||
print(f" 2.5 Alt: {pred.under_25_prob*100:.1f}%")
|
||||
|
||||
print(f"\n🤝 Karşılıklı Gol:")
|
||||
print(f" KG Var: {pred.btts_yes_prob*100:.1f}%")
|
||||
print(f" KG Yok: {pred.btts_no_prob*100:.1f}%")
|
||||
|
||||
print(f"\n📈 En Olası Skorlar:")
|
||||
for score_data in pred.most_likely_scores:
|
||||
print(f" {score_data['score']}: {score_data['probability']}%")
|
||||
Executable
+368
@@ -0,0 +1,368 @@
|
||||
"""
|
||||
Referee Engine - V9 Feature
|
||||
Hakem profilleri ve maç etki analizi.
|
||||
|
||||
Analiz Edilen Metrikler:
|
||||
- Ortalama kart sayısı (sarı/kırmızı)
|
||||
- Penaltı verme eğilimi
|
||||
- Ev sahibi lehine karar oranı
|
||||
- Maç başına toplam gol ortalaması
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, Optional, List
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefereeProfile:
|
||||
"""Hakem profili"""
|
||||
referee_name: str
|
||||
matches_count: int = 0
|
||||
|
||||
# Kart istatistikleri
|
||||
avg_yellow_cards: float = 0.0
|
||||
avg_red_cards: float = 0.0
|
||||
total_cards_per_match: float = 0.0
|
||||
|
||||
# Penaltı istatistikleri
|
||||
penalty_rate: float = 0.0 # Penaltı verdiği maç oranı
|
||||
|
||||
# Ev sahibi eğilimi
|
||||
home_win_rate: float = 0.0
|
||||
home_bias: float = 0.0 # -1 (away bias) to +1 (home bias)
|
||||
|
||||
# Gol istatistikleri
|
||||
avg_goals_per_match: float = 0.0
|
||||
over_25_rate: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefereeFeatures:
|
||||
"""Model için hakem feature'ları"""
|
||||
referee_name: str = ""
|
||||
referee_matches: int = 0
|
||||
referee_avg_yellow: float = 0.0
|
||||
referee_avg_red: float = 0.0
|
||||
referee_cards_total: float = 0.0
|
||||
referee_penalty_rate: float = 0.0
|
||||
referee_home_bias: float = 0.0
|
||||
referee_avg_goals: float = 0.0
|
||||
referee_over25_rate: float = 0.0
|
||||
referee_experience: float = 0.0 # 0-1 normalized
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
return {
|
||||
'referee_matches': float(self.referee_matches),
|
||||
'referee_avg_yellow': self.referee_avg_yellow,
|
||||
'referee_avg_red': self.referee_avg_red,
|
||||
'referee_cards_total': self.referee_cards_total,
|
||||
'referee_penalty_rate': self.referee_penalty_rate,
|
||||
'referee_home_bias': self.referee_home_bias,
|
||||
'referee_avg_goals': self.referee_avg_goals,
|
||||
'referee_over25_rate': self.referee_over25_rate,
|
||||
'referee_experience': self.referee_experience,
|
||||
}
|
||||
|
||||
|
||||
class RefereeEngine:
|
||||
"""
|
||||
Hakem analiz motoru.
|
||||
|
||||
Hakemlerin geçmiş maçlarını analiz ederek:
|
||||
- Kart eğilimlerini
|
||||
- Ev sahibi bias'ını
|
||||
- Gol ortalamasını
|
||||
hesaplar.
|
||||
"""
|
||||
|
||||
# Ana hakem rolü ID'si (genellikle 1 veya "Hakem")
|
||||
MAIN_REFEREE_ROLE_ID = 1
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._referee_cache: Dict[str, RefereeProfile] = {}
|
||||
self._cache_loaded = False
|
||||
|
||||
def _connect_db(self):
|
||||
if psycopg2 is None:
|
||||
return None
|
||||
try:
|
||||
from data.db import get_clean_dsn
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
return self.conn
|
||||
except Exception as e:
|
||||
print(f"[RefereeEngine] DB connection failed: {e}")
|
||||
return None
|
||||
|
||||
def get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
self._connect_db()
|
||||
return self.conn
|
||||
|
||||
def _get_main_referee_role_id(self) -> int:
|
||||
"""Ana hakem rolü ID'sini bul"""
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return self.MAIN_REFEREE_ROLE_ID
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT id FROM official_roles
|
||||
WHERE LOWER(name) LIKE '%%hakem%%'
|
||||
AND LOWER(name) NOT LIKE '%%yardımcı%%'
|
||||
AND LOWER(name) NOT LIKE '%%dördüncü%%'
|
||||
LIMIT 1
|
||||
""")
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return result[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return self.MAIN_REFEREE_ROLE_ID
|
||||
|
||||
def get_referee_for_match(self, match_id: str) -> Optional[str]:
|
||||
"""Maçın ana hakemini bul"""
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
main_role_id = self._get_main_referee_role_id()
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT name FROM match_officials
|
||||
WHERE match_id = %s AND role_id = %s
|
||||
LIMIT 1
|
||||
""", (match_id, main_role_id))
|
||||
result = cur.fetchone()
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
print(f"[RefereeEngine] Error getting referee: {e}")
|
||||
return None
|
||||
|
||||
def calculate_referee_profile(self, referee_name: str, league_id: str = None) -> RefereeProfile:
|
||||
"""Hakemin maçlarını analiz et. league_id verilirse sadece o ligteki maçları kullanır."""
|
||||
|
||||
# Composite cache key — aynı isim farklı liglerde farklı profil
|
||||
cache_key = (referee_name, league_id)
|
||||
if cache_key in self._referee_cache:
|
||||
return self._referee_cache[cache_key]
|
||||
|
||||
profile = RefereeProfile(referee_name=referee_name)
|
||||
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return profile
|
||||
|
||||
try:
|
||||
main_role_id = self._get_main_referee_role_id()
|
||||
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# Bu hakemin yönettiği maçları al (league_id varsa sadece o lig)
|
||||
if league_id:
|
||||
cur.execute("""
|
||||
SELECT m.id, m.score_home, m.score_away, m.home_team_id, m.away_team_id
|
||||
FROM matches m
|
||||
JOIN match_officials mo ON m.id = mo.match_id
|
||||
WHERE mo.name = %s
|
||||
AND mo.role_id = %s
|
||||
AND m.league_id = %s
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 100
|
||||
""", (referee_name, main_role_id, league_id))
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT m.id, m.score_home, m.score_away, m.home_team_id, m.away_team_id
|
||||
FROM matches m
|
||||
JOIN match_officials mo ON m.id = mo.match_id
|
||||
WHERE mo.name = %s
|
||||
AND mo.role_id = %s
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 100
|
||||
""", (referee_name, main_role_id))
|
||||
|
||||
matches = cur.fetchall()
|
||||
profile.matches_count = len(matches)
|
||||
|
||||
if profile.matches_count == 0:
|
||||
return profile
|
||||
|
||||
match_ids = [m['id'] for m in matches]
|
||||
|
||||
# Kart istatistikleri
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE event_subtype ILIKE '%%yellow%%') as yellow_count,
|
||||
COUNT(*) FILTER (WHERE event_subtype ILIKE '%%red%%' OR event_subtype ILIKE '%%second%%') as red_count
|
||||
FROM match_player_events
|
||||
WHERE match_id = ANY(%s) AND event_type = 'card'
|
||||
""", (match_ids,))
|
||||
|
||||
card_stats = cur.fetchone()
|
||||
if card_stats:
|
||||
profile.avg_yellow_cards = (card_stats['yellow_count'] or 0) / profile.matches_count
|
||||
profile.avg_red_cards = (card_stats['red_count'] or 0) / profile.matches_count
|
||||
profile.total_cards_per_match = profile.avg_yellow_cards + profile.avg_red_cards
|
||||
|
||||
# Penaltı istatistikleri
|
||||
cur.execute("""
|
||||
SELECT COUNT(DISTINCT match_id) as penalty_matches
|
||||
FROM match_player_events
|
||||
WHERE match_id = ANY(%s)
|
||||
AND event_type = 'goal'
|
||||
AND event_subtype ILIKE '%%penaltı%%'
|
||||
""", (match_ids,))
|
||||
|
||||
penalty_stats = cur.fetchone()
|
||||
if penalty_stats:
|
||||
profile.penalty_rate = (penalty_stats['penalty_matches'] or 0) / profile.matches_count
|
||||
|
||||
# Ev sahibi eğilimi ve gol ortalaması
|
||||
home_wins = 0
|
||||
away_wins = 0
|
||||
draws = 0
|
||||
total_goals = 0
|
||||
over_25_count = 0
|
||||
|
||||
for m in matches:
|
||||
goals = (m['score_home'] or 0) + (m['score_away'] or 0)
|
||||
total_goals += goals
|
||||
|
||||
if goals > 2.5:
|
||||
over_25_count += 1
|
||||
|
||||
if m['score_home'] > m['score_away']:
|
||||
home_wins += 1
|
||||
elif m['score_home'] < m['score_away']:
|
||||
away_wins += 1
|
||||
else:
|
||||
draws += 1
|
||||
|
||||
profile.avg_goals_per_match = total_goals / profile.matches_count
|
||||
profile.over_25_rate = over_25_count / profile.matches_count
|
||||
profile.home_win_rate = home_wins / profile.matches_count
|
||||
|
||||
# Home bias: -1 (away favors) to +1 (home favors)
|
||||
# Normal lig ortalaması ~%46 ev sahibi, buna göre normalize
|
||||
expected_home_rate = 0.46
|
||||
profile.home_bias = (profile.home_win_rate - expected_home_rate) * 2
|
||||
profile.home_bias = max(-1, min(1, profile.home_bias))
|
||||
|
||||
# Cache'e ekle
|
||||
self._referee_cache[cache_key] = profile
|
||||
return profile
|
||||
|
||||
except Exception as e:
|
||||
print(f"[RefereeEngine] Error calculating profile: {e}")
|
||||
return profile
|
||||
|
||||
def get_features(self, match_id: str, league_id: str = None) -> Dict[str, float]:
|
||||
"""
|
||||
Maç için hakem feature'larını hesapla.
|
||||
|
||||
Args:
|
||||
match_id: Maç ID'si
|
||||
league_id: Lig ID'si (opsiyonel — isim çakışmalarını önlemek için)
|
||||
|
||||
Returns:
|
||||
Hakem feature'ları dict olarak
|
||||
"""
|
||||
features = RefereeFeatures()
|
||||
|
||||
# Hakemi bul
|
||||
referee_name = self.get_referee_for_match(match_id)
|
||||
if referee_name is None:
|
||||
return features.to_dict()
|
||||
|
||||
features.referee_name = referee_name
|
||||
|
||||
# Profili hesapla (league_id ile scope'lanmış)
|
||||
profile = self.calculate_referee_profile(referee_name, league_id=league_id)
|
||||
|
||||
features.referee_matches = profile.matches_count
|
||||
features.referee_avg_yellow = profile.avg_yellow_cards
|
||||
features.referee_avg_red = profile.avg_red_cards
|
||||
features.referee_cards_total = profile.total_cards_per_match
|
||||
features.referee_penalty_rate = profile.penalty_rate
|
||||
features.referee_home_bias = profile.home_bias
|
||||
features.referee_avg_goals = profile.avg_goals_per_match
|
||||
features.referee_over25_rate = profile.over_25_rate
|
||||
|
||||
# Deneyim: 50+ maç = 1.0, 0 maç = 0.0
|
||||
features.referee_experience = min(profile.matches_count / 50, 1.0)
|
||||
|
||||
return features.to_dict()
|
||||
|
||||
def get_features_by_name(self, referee_name: str, league_id: str = None) -> Dict[str, float]:
|
||||
"""
|
||||
Hakem ismiyle feature'ları hesapla.
|
||||
|
||||
Args:
|
||||
referee_name: Hakem ismi
|
||||
league_id: Lig ID'si (opsiyonel — isim çakışmalarını önlemek için)
|
||||
|
||||
Returns:
|
||||
Hakem feature'ları dict olarak
|
||||
"""
|
||||
features = RefereeFeatures()
|
||||
|
||||
if not referee_name:
|
||||
return features.to_dict()
|
||||
|
||||
features.referee_name = referee_name
|
||||
profile = self.calculate_referee_profile(referee_name, league_id=league_id)
|
||||
|
||||
features.referee_matches = profile.matches_count
|
||||
features.referee_avg_yellow = profile.avg_yellow_cards
|
||||
features.referee_avg_red = profile.avg_red_cards
|
||||
features.referee_cards_total = profile.total_cards_per_match
|
||||
features.referee_penalty_rate = profile.penalty_rate
|
||||
features.referee_home_bias = profile.home_bias
|
||||
features.referee_avg_goals = profile.avg_goals_per_match
|
||||
features.referee_over25_rate = profile.over_25_rate
|
||||
features.referee_experience = min(profile.matches_count / 50, 1.0)
|
||||
|
||||
return features.to_dict()
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_engine: Optional[RefereeEngine] = None
|
||||
|
||||
|
||||
def get_referee_engine() -> RefereeEngine:
|
||||
"""Singleton referee engine instance döndür"""
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = RefereeEngine()
|
||||
return _engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test
|
||||
engine = get_referee_engine()
|
||||
|
||||
print("\n🧪 Referee Engine Test")
|
||||
print("=" * 50)
|
||||
|
||||
# Test with a known referee name
|
||||
test_referee = "Cüneyt Çakır"
|
||||
features = engine.get_features_by_name(test_referee)
|
||||
|
||||
print(f"\n📊 Hakem: {test_referee}")
|
||||
for key, value in features.items():
|
||||
print(f" {key}: {value:.3f}")
|
||||
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
V27 Rolling Window Feature Calculator
|
||||
======================================
|
||||
Computes rolling averages over 5/10/20 match windows,
|
||||
with home/away splits and trend detection.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List, Tuple
|
||||
import math
|
||||
|
||||
|
||||
def calc_rolling_features(
|
||||
team_matches: List[Tuple], # [(mst, is_home, team_goals, opp_goals, opp_id), ...]
|
||||
before_date: int,
|
||||
team_is_home: bool,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate rolling window features for a team before a given date."""
|
||||
valid = [m for m in team_matches if m[0] < before_date]
|
||||
|
||||
defaults = {
|
||||
"rolling5_goals_avg": 1.3, "rolling5_conceded_avg": 1.2,
|
||||
"rolling10_goals_avg": 1.3, "rolling10_conceded_avg": 1.2,
|
||||
"rolling20_goals_avg": 1.3, "rolling20_conceded_avg": 1.2,
|
||||
"rolling5_clean_sheets": 0.25,
|
||||
"venue_goals_avg": 1.3, "venue_conceded_avg": 1.2,
|
||||
"goal_trend": 0.0,
|
||||
}
|
||||
|
||||
if len(valid) < 3:
|
||||
return defaults
|
||||
|
||||
result = {}
|
||||
|
||||
for window in [5, 10, 20]:
|
||||
recent = valid[-window:] if len(valid) >= window else valid
|
||||
n = len(recent)
|
||||
g_sum = sum(m[2] for m in recent)
|
||||
c_sum = sum(m[3] for m in recent)
|
||||
result[f"rolling{window}_goals_avg"] = g_sum / n
|
||||
result[f"rolling{window}_conceded_avg"] = c_sum / n
|
||||
|
||||
# Clean sheet rate (last 5)
|
||||
r5 = valid[-5:] if len(valid) >= 5 else valid
|
||||
result["rolling5_clean_sheets"] = sum(1 for m in r5 if m[3] == 0) / len(r5)
|
||||
|
||||
# Venue-specific (home-only or away-only)
|
||||
venue_matches = [m for m in valid if m[1] == team_is_home]
|
||||
if venue_matches:
|
||||
vm = venue_matches[-10:] if len(venue_matches) >= 10 else venue_matches
|
||||
result["venue_goals_avg"] = sum(m[2] for m in vm) / len(vm)
|
||||
result["venue_conceded_avg"] = sum(m[3] for m in vm) / len(vm)
|
||||
else:
|
||||
result["venue_goals_avg"] = defaults["venue_goals_avg"]
|
||||
result["venue_conceded_avg"] = defaults["venue_conceded_avg"]
|
||||
|
||||
# Goal trend: compare last 3 vs previous 3
|
||||
if len(valid) >= 6:
|
||||
last3 = sum(m[2] for m in valid[-3:]) / 3
|
||||
prev3 = sum(m[2] for m in valid[-6:-3]) / 3
|
||||
result["goal_trend"] = last3 - prev3
|
||||
else:
|
||||
result["goal_trend"] = 0.0
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def calc_league_quality(
|
||||
all_matches: List[Tuple], # all FT matches in this league
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate league-level quality features."""
|
||||
defaults = {
|
||||
"league_home_win_rate": 0.45,
|
||||
"league_draw_rate": 0.25,
|
||||
"league_btts_rate": 0.50,
|
||||
"league_ou25_rate": 0.50,
|
||||
"league_reliability_score": 0.50,
|
||||
}
|
||||
|
||||
if len(all_matches) < 20:
|
||||
return defaults
|
||||
|
||||
n = len(all_matches)
|
||||
home_wins = sum(1 for m in all_matches if m[2] > m[3])
|
||||
draws = sum(1 for m in all_matches if m[2] == m[3])
|
||||
btts = sum(1 for m in all_matches if m[2] > 0 and m[3] > 0)
|
||||
ou25 = sum(1 for m in all_matches if (m[2] + m[3]) > 2.5)
|
||||
|
||||
hw_rate = home_wins / n
|
||||
dr_rate = draws / n
|
||||
btts_rate = btts / n
|
||||
ou25_rate = ou25 / n
|
||||
|
||||
# Reliability: leagues closer to averages are more predictable
|
||||
predictability = 1.0 - abs(hw_rate - 0.45) - abs(dr_rate - 0.27) * 0.5
|
||||
reliability = max(0.2, min(0.95, predictability))
|
||||
|
||||
return {
|
||||
"league_home_win_rate": round(hw_rate, 4),
|
||||
"league_draw_rate": round(dr_rate, 4),
|
||||
"league_btts_rate": round(btts_rate, 4),
|
||||
"league_ou25_rate": round(ou25_rate, 4),
|
||||
"league_reliability_score": round(reliability, 4),
|
||||
}
|
||||
|
||||
|
||||
def calc_time_features(
|
||||
team_matches: List[Tuple],
|
||||
match_mst: int,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate time-based features."""
|
||||
from datetime import datetime
|
||||
|
||||
# Days since last match
|
||||
valid = [m for m in team_matches if m[0] < match_mst]
|
||||
if valid:
|
||||
last_mst = valid[-1][0]
|
||||
days_rest = (match_mst - last_mst) / 86_400_000 # ms to days
|
||||
days_rest = min(days_rest, 60.0) # cap at 60 days
|
||||
else:
|
||||
days_rest = 14.0
|
||||
|
||||
# Month and season flags
|
||||
try:
|
||||
dt = datetime.utcfromtimestamp(match_mst / 1000)
|
||||
month = dt.month
|
||||
is_season_start = 1.0 if month in (7, 8) else 0.0
|
||||
is_season_end = 1.0 if month in (5, 6) else 0.0
|
||||
except Exception:
|
||||
month = 6
|
||||
is_season_start = 0.0
|
||||
is_season_end = 0.0
|
||||
|
||||
return {
|
||||
"days_rest": round(days_rest, 2),
|
||||
"match_month": month,
|
||||
"is_season_start": is_season_start,
|
||||
"is_season_end": is_season_end,
|
||||
}
|
||||
|
||||
|
||||
def calc_advanced_h2h(
|
||||
team_matches: List[Tuple],
|
||||
home_id: int,
|
||||
away_id: int,
|
||||
before_date: int,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate advanced H2H features."""
|
||||
defaults = {
|
||||
"h2h_home_goals_avg": 1.3,
|
||||
"h2h_away_goals_avg": 1.1,
|
||||
"h2h_recent_trend": 0.0,
|
||||
"h2h_venue_advantage": 0.0,
|
||||
}
|
||||
|
||||
h2h = [m for m in team_matches if m[4] == away_id and m[0] < before_date]
|
||||
if not h2h:
|
||||
return defaults
|
||||
|
||||
recent = h2h[-10:]
|
||||
home_goals_total = 0
|
||||
away_goals_total = 0
|
||||
venue_home_wins = 0
|
||||
venue_total = 0
|
||||
|
||||
for mst, is_home, team_goals, opp_goals, _ in recent:
|
||||
if is_home:
|
||||
home_goals_total += team_goals
|
||||
away_goals_total += opp_goals
|
||||
venue_total += 1
|
||||
if team_goals > opp_goals:
|
||||
venue_home_wins += 1
|
||||
else:
|
||||
home_goals_total += opp_goals
|
||||
away_goals_total += team_goals
|
||||
|
||||
n = len(recent)
|
||||
result = {
|
||||
"h2h_home_goals_avg": home_goals_total / n,
|
||||
"h2h_away_goals_avg": away_goals_total / n,
|
||||
"h2h_venue_advantage": venue_home_wins / venue_total if venue_total > 0 else 0.5,
|
||||
}
|
||||
|
||||
# Recent trend: last 3 vs overall
|
||||
if len(h2h) >= 4:
|
||||
last3_pts = sum(
|
||||
1.0 if m[2] > m[3] else (0.5 if m[2] == m[3] else 0.0)
|
||||
for m in h2h[-3:]
|
||||
) / 3
|
||||
overall_pts = sum(
|
||||
1.0 if m[2] > m[3] else (0.5 if m[2] == m[3] else 0.0)
|
||||
for m in h2h
|
||||
) / len(h2h)
|
||||
result["h2h_recent_trend"] = round(last3_pts - overall_pts, 4)
|
||||
else:
|
||||
result["h2h_recent_trend"] = 0.0
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def calc_strength_diff(
|
||||
home_form: Dict[str, float],
|
||||
away_form: Dict[str, float],
|
||||
home_elo: Dict[str, float],
|
||||
away_elo: Dict[str, float],
|
||||
home_momentum: float,
|
||||
away_momentum: float,
|
||||
upset_potential: float,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate strength differential features."""
|
||||
# Attack vs Defense mismatches
|
||||
h_attack = home_form.get("goals_avg", 1.3)
|
||||
a_defense = away_form.get("conceded_avg", 1.2)
|
||||
a_attack = away_form.get("goals_avg", 1.3)
|
||||
h_defense = home_form.get("conceded_avg", 1.2)
|
||||
|
||||
atk_def_home = h_attack - a_defense # positive = home attack > away defense
|
||||
atk_def_away = a_attack - h_defense
|
||||
|
||||
# XG diff approximation
|
||||
xg_diff = (h_attack + a_defense) / 2 - (a_attack + h_defense) / 2
|
||||
|
||||
# Form × Momentum interaction
|
||||
form_mom = (home_momentum - away_momentum) * (
|
||||
home_form.get("scoring_rate", 0.75) - away_form.get("scoring_rate", 0.75)
|
||||
)
|
||||
|
||||
# ELO-Form consistency
|
||||
elo_diff = home_elo.get("overall", 1500) - away_elo.get("overall", 1500)
|
||||
form_diff = h_attack - a_attack
|
||||
elo_form_consistency = 1.0 if (elo_diff > 0 and form_diff > 0) or (elo_diff < 0 and form_diff < 0) else 0.0
|
||||
|
||||
# Upset × ELO gap
|
||||
elo_gap = abs(elo_diff)
|
||||
upset_x_elo = upset_potential * (elo_gap / 400.0)
|
||||
|
||||
return {
|
||||
"attack_vs_defense_home": round(atk_def_home, 4),
|
||||
"attack_vs_defense_away": round(atk_def_away, 4),
|
||||
"xg_diff": round(xg_diff, 4),
|
||||
"form_momentum_interaction": round(form_mom, 4),
|
||||
"elo_form_consistency": elo_form_consistency,
|
||||
"upset_x_elo_gap": round(upset_x_elo, 4),
|
||||
}
|
||||
Executable
+408
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
Sidelined Analyzer — Injury & Suspension Impact Calculator
|
||||
==========================================================
|
||||
Parses sidelined JSON from live_matches and calculates
|
||||
position-weighted missing player impact using ACTUAL player
|
||||
statistics from the database (goals, assists, starting frequency).
|
||||
|
||||
Senior ML Engineer Principle: No magic numbers — all weights from config.
|
||||
Data Quality: Cross-reference sidelined IDs with DB for real impact.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
from config.config_loader import get_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerImpactDetail:
|
||||
"""Impact detail for a single sidelined player."""
|
||||
player_id: str
|
||||
player_name: str
|
||||
position: str
|
||||
impact_score: float
|
||||
db_goals: int = 0
|
||||
db_assists: int = 0
|
||||
db_starts: int = 0
|
||||
db_rating: float = 0.0 # Calculated from DB stats
|
||||
is_key_player: bool = False
|
||||
adaptation_applied: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class SidelinedImpact:
|
||||
"""Impact analysis of sidelined players for one team."""
|
||||
total_sidelined: int = 0
|
||||
impact_score: float = 0.0 # 0.0 - 1.0 (normalized)
|
||||
key_position_missing: bool = False # GK or 2+ same position missing
|
||||
key_players_missing: int = 0 # How many key players are missing
|
||||
position_breakdown: Dict[str, int] = field(default_factory=dict)
|
||||
player_details: List[PlayerImpactDetail] = field(default_factory=list)
|
||||
details: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class SidelinedAnalyzer:
|
||||
"""
|
||||
Analyzes sidelined player data with DB-backed statistics.
|
||||
|
||||
Impact formula per player:
|
||||
player_impact = position_weight × db_rating_factor × adaptation_factor
|
||||
|
||||
Where:
|
||||
- position_weight: from config (GK most critical)
|
||||
- db_rating_factor: calculated from actual goals + assists + starts (not mackolik average!)
|
||||
- adaptation_factor: 1.0 if recent injury, discounted if team adapted (many matches missed)
|
||||
|
||||
DB Query: Cross-references sidelined player IDs with match_player_events
|
||||
to get real goals/assists from recent matches.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.conn = None
|
||||
self._load_config()
|
||||
self._connect_db()
|
||||
|
||||
def _load_config(self):
|
||||
"""Load all config values once at init."""
|
||||
cfg = self.config
|
||||
self.position_weights = cfg.get("sidelined.position_weights", {
|
||||
"K": 0.35, "D": 0.20, "O": 0.25, "F": 0.30
|
||||
})
|
||||
self.max_rating = cfg.get("sidelined.max_rating", 10)
|
||||
self.adaptation_threshold = cfg.get("sidelined.adaptation_threshold", 10)
|
||||
self.adaptation_discount = cfg.get("sidelined.adaptation_discount", 0.5)
|
||||
self.goalkeeper_penalty = cfg.get("sidelined.goalkeeper_penalty", 0.15)
|
||||
self.confidence_boost = cfg.get("sidelined.confidence_boost", 10)
|
||||
self.max_impact = cfg.get("sidelined.max_impact", 0.85)
|
||||
self.key_player_threshold = cfg.get("sidelined.key_player_threshold", 3)
|
||||
self.recent_matches_lookback = cfg.get("sidelined.recent_matches_lookback", 15)
|
||||
|
||||
@staticmethod
|
||||
def _safe_int(value: Any, default: int = 0) -> int:
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return int(float(value))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def _safe_float(value: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
def _connect_db(self):
|
||||
"""Lazy DB connection following existing engine patterns."""
|
||||
if psycopg2 is None:
|
||||
return
|
||||
try:
|
||||
from data.db import get_clean_dsn
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
except Exception as e:
|
||||
print(f"[SidelinedAnalyzer] DB connection failed: {e}")
|
||||
self.conn = None
|
||||
|
||||
def _get_conn(self):
|
||||
"""Get or reconnect DB connection."""
|
||||
if self.conn is None or self.conn.closed:
|
||||
self._connect_db()
|
||||
return self.conn
|
||||
|
||||
def _fetch_player_stats(self, player_ids: List[str]) -> Dict[str, Dict]:
|
||||
"""
|
||||
Fetch real player statistics from DB for given player IDs.
|
||||
|
||||
Returns dict keyed by player_id with:
|
||||
goals: int, assists: int, starts: int, matches: int
|
||||
"""
|
||||
conn = self._get_conn()
|
||||
if not conn or not player_ids:
|
||||
return {}
|
||||
|
||||
stats = {}
|
||||
try:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 1. Goals from match_player_events + Assists via assist_player_id
|
||||
cur.execute("""
|
||||
SELECT
|
||||
sub.player_id,
|
||||
SUM(sub.goals) AS goals,
|
||||
SUM(sub.assists) AS assists
|
||||
FROM (
|
||||
-- Goals: player scored
|
||||
SELECT mpe.player_id,
|
||||
COUNT(*) AS goals,
|
||||
0 AS assists
|
||||
FROM match_player_events mpe
|
||||
JOIN matches m ON mpe.match_id = m.id
|
||||
WHERE mpe.player_id = ANY(%s)
|
||||
AND mpe.event_type = 'goal'
|
||||
AND m.status = 'FT'
|
||||
GROUP BY mpe.player_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Assists: player assisted
|
||||
SELECT mpe.assist_player_id AS player_id,
|
||||
0 AS goals,
|
||||
COUNT(*) AS assists
|
||||
FROM match_player_events mpe
|
||||
JOIN matches m ON mpe.match_id = m.id
|
||||
WHERE mpe.assist_player_id = ANY(%s)
|
||||
AND mpe.event_type = 'goal'
|
||||
AND m.status = 'FT'
|
||||
GROUP BY mpe.assist_player_id
|
||||
) sub
|
||||
GROUP BY sub.player_id
|
||||
""", (player_ids, player_ids))
|
||||
|
||||
for row in cur.fetchall():
|
||||
pid = row["player_id"]
|
||||
stats[pid] = {
|
||||
"goals": row["goals"] or 0,
|
||||
"assists": row["assists"] or 0,
|
||||
"starts": 0,
|
||||
"matches": 0
|
||||
}
|
||||
|
||||
# 2. Starting frequency from match_player_participation
|
||||
cur.execute("""
|
||||
SELECT
|
||||
mpp.player_id,
|
||||
COUNT(*) AS total_matches,
|
||||
COUNT(*) FILTER (WHERE mpp.is_starting = true) AS starts
|
||||
FROM match_player_participation mpp
|
||||
JOIN matches m ON mpp.match_id = m.id
|
||||
WHERE mpp.player_id = ANY(%s)
|
||||
AND m.status = 'FT'
|
||||
GROUP BY mpp.player_id
|
||||
""", (player_ids,))
|
||||
|
||||
for row in cur.fetchall():
|
||||
pid = row["player_id"]
|
||||
if pid not in stats:
|
||||
stats[pid] = {"goals": 0, "assists": 0, "starts": 0, "matches": 0}
|
||||
stats[pid]["starts"] = row["starts"] or 0
|
||||
stats[pid]["matches"] = row["total_matches"] or 0
|
||||
|
||||
cur.close()
|
||||
except Exception as e:
|
||||
print(f"[SidelinedAnalyzer] DB query error: {e}")
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return stats
|
||||
|
||||
def _calculate_db_rating(self, db_stats: Dict, position: str) -> float:
|
||||
"""
|
||||
Calculate player rating from DB statistics.
|
||||
|
||||
Rating is 0.0 - 1.0, where 1.0 = absolute key player.
|
||||
|
||||
Factors:
|
||||
- Goals (weighted by position: Forwards value more, Defenders less)
|
||||
- Assists
|
||||
- Starting frequency (regulars > squad players)
|
||||
"""
|
||||
def _to_float(value: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
goals = _to_float(db_stats.get("goals", 0))
|
||||
assists = _to_float(db_stats.get("assists", 0))
|
||||
starts = _to_float(db_stats.get("starts", 0))
|
||||
matches = _to_float(db_stats.get("matches", 0))
|
||||
|
||||
# Goal contribution weight by position
|
||||
# Forwards: goals matter most
|
||||
# Midfielders: balanced
|
||||
# Defenders: starts matter more than goals
|
||||
# Goalkeeper: starts are everything
|
||||
goal_weight = {"F": 0.5, "O": 0.35, "D": 0.15, "K": 0.05}.get(position, 0.25)
|
||||
assist_weight = {"F": 0.2, "O": 0.3, "D": 0.15, "K": 0.0}.get(position, 0.15)
|
||||
start_weight = {"F": 0.3, "O": 0.35, "D": 0.7, "K": 0.95}.get(position, 0.5)
|
||||
|
||||
# Normalize each component to 0-1
|
||||
# Goals: 5+ goals in recent matches = max
|
||||
goal_factor = min(goals / 5.0, 1.0) if goals > 0 else 0.0
|
||||
# Assists: 4+ assists = max
|
||||
assist_factor = min(assists / 4.0, 1.0) if assists > 0 else 0.0
|
||||
# Starts: 80%+ start rate = max regular
|
||||
start_rate = starts / max(matches, 1)
|
||||
start_factor = min(start_rate / 0.8, 1.0)
|
||||
|
||||
rating = (goal_factor * goal_weight +
|
||||
assist_factor * assist_weight +
|
||||
start_factor * start_weight)
|
||||
|
||||
return round(min(rating, 1.0), 4)
|
||||
|
||||
def analyze(self, team_data: Optional[Dict[str, Any]]) -> SidelinedImpact:
|
||||
"""
|
||||
Analyze sidelined data for a single team using DB-backed stats.
|
||||
|
||||
Args:
|
||||
team_data: dict with 'players' list and 'totalSidelined' count.
|
||||
|
||||
Returns:
|
||||
SidelinedImpact with calculated impact score and breakdown.
|
||||
"""
|
||||
if not team_data or not isinstance(team_data, dict):
|
||||
return SidelinedImpact()
|
||||
|
||||
players = team_data.get("players", [])
|
||||
if not players:
|
||||
return SidelinedImpact(
|
||||
total_sidelined=team_data.get("totalSidelined", 0)
|
||||
)
|
||||
|
||||
# Collect player IDs for batch DB query
|
||||
player_ids = [p.get("playerId", "") for p in players if p.get("playerId")]
|
||||
|
||||
# Batch fetch DB stats (single query, not N+1)
|
||||
db_stats = self._fetch_player_stats(player_ids) if player_ids else {}
|
||||
|
||||
total_impact = 0.0
|
||||
position_counts: Dict[str, int] = {}
|
||||
player_details: List[PlayerImpactDetail] = []
|
||||
details: List[str] = []
|
||||
has_gk_missing = False
|
||||
key_players_count = 0
|
||||
|
||||
for player in players:
|
||||
if not isinstance(player, dict):
|
||||
continue
|
||||
|
||||
pos = player.get("positionShort", "O")
|
||||
name = player.get("playerName", "Unknown")
|
||||
pid = player.get("playerId", "")
|
||||
matches_missed = self._safe_int(player.get("matchesMissed", 0), 0)
|
||||
player_type = player.get("type", "other")
|
||||
mackolik_avg = self._safe_float(player.get("average", 0), 0.0)
|
||||
|
||||
position_counts[pos] = position_counts.get(pos, 0) + 1
|
||||
|
||||
if pos == "K":
|
||||
has_gk_missing = True
|
||||
|
||||
# === Rating: DB first, mackolik fallback ===
|
||||
p_db_stats = db_stats.get(pid, {})
|
||||
|
||||
if p_db_stats:
|
||||
# Use real DB stats
|
||||
db_rating = self._calculate_db_rating(p_db_stats, pos)
|
||||
else:
|
||||
# Fallback to mackolik average (normalized)
|
||||
db_rating = min(mackolik_avg / self.max_rating, 1.0) if self.max_rating > 0 else 0.3
|
||||
db_rating = max(db_rating, 0.15) # Minimum floor
|
||||
|
||||
# Key player check
|
||||
is_key = db_rating >= 0.5 or (
|
||||
self._safe_int(p_db_stats.get("goals", 0), 0) >= self.key_player_threshold
|
||||
)
|
||||
if is_key:
|
||||
key_players_count += 1
|
||||
|
||||
# === Impact Calculation ===
|
||||
pos_weight = self.position_weights.get(pos, 0.20)
|
||||
|
||||
# Rating factor: higher rated = bigger loss
|
||||
rating_factor = max(db_rating, 0.15) # Even unknown players have minimum impact
|
||||
|
||||
# Adaptation: team has coped if player missed many matches
|
||||
adapted = matches_missed >= self.adaptation_threshold
|
||||
adapt_factor = self.adaptation_discount if adapted else 1.0
|
||||
|
||||
# Type factor
|
||||
type_factor = 1.0 if player_type == "injury" else 0.8
|
||||
|
||||
player_impact = pos_weight * rating_factor * adapt_factor * type_factor
|
||||
total_impact += player_impact
|
||||
|
||||
detail = PlayerImpactDetail(
|
||||
player_id=pid,
|
||||
player_name=name,
|
||||
position=pos,
|
||||
impact_score=round(player_impact, 4),
|
||||
db_goals=p_db_stats.get("goals", 0),
|
||||
db_assists=p_db_stats.get("assists", 0),
|
||||
db_starts=p_db_stats.get("starts", 0),
|
||||
db_rating=db_rating,
|
||||
is_key_player=is_key,
|
||||
adaptation_applied=adapted
|
||||
)
|
||||
player_details.append(detail)
|
||||
|
||||
db_info = f"G:{detail.db_goals} A:{detail.db_assists} S:{detail.db_starts}" if p_db_stats else "no DB data"
|
||||
details.append(
|
||||
f"{name} ({pos}, db_rating:{db_rating:.2f}, {db_info}) → impact:{player_impact:.3f}"
|
||||
+ (" ⭐ KEY" if is_key else "")
|
||||
+ (f" [adapted, {matches_missed} missed]" if adapted else "")
|
||||
)
|
||||
|
||||
# GK penalty bonus
|
||||
if has_gk_missing:
|
||||
total_impact += self.goalkeeper_penalty
|
||||
|
||||
key_position_missing = has_gk_missing or any(v >= 2 for v in position_counts.values())
|
||||
|
||||
# Normalize to 0-1 range
|
||||
normalization_cap = 1.5
|
||||
normalized_impact = min(total_impact / normalization_cap, self.max_impact)
|
||||
|
||||
return SidelinedImpact(
|
||||
total_sidelined=len(players),
|
||||
impact_score=round(normalized_impact, 4),
|
||||
key_position_missing=key_position_missing,
|
||||
key_players_missing=key_players_count,
|
||||
position_breakdown=position_counts,
|
||||
player_details=player_details,
|
||||
details=details
|
||||
)
|
||||
|
||||
def analyze_match(self, sidelined_json: Optional[Dict[str, Any]]) -> Tuple[SidelinedImpact, SidelinedImpact]:
|
||||
"""
|
||||
Analyze sidelined data for both teams.
|
||||
|
||||
Returns:
|
||||
(home_impact, away_impact)
|
||||
"""
|
||||
if not sidelined_json or not isinstance(sidelined_json, dict):
|
||||
return SidelinedImpact(), SidelinedImpact()
|
||||
|
||||
home_impact = self.analyze(sidelined_json.get("homeTeam"))
|
||||
away_impact = self.analyze(sidelined_json.get("awayTeam"))
|
||||
return home_impact, away_impact
|
||||
|
||||
|
||||
# Singleton
|
||||
_analyzer: Optional[SidelinedAnalyzer] = None
|
||||
|
||||
|
||||
def get_sidelined_analyzer() -> SidelinedAnalyzer:
|
||||
global _analyzer
|
||||
if _analyzer is None:
|
||||
_analyzer = SidelinedAnalyzer()
|
||||
return _analyzer
|
||||
@@ -0,0 +1,357 @@
|
||||
"""
|
||||
Smart Bet Recommender
|
||||
=====================
|
||||
|
||||
Skor tahminine göre akıllı bahis önerileri yapan sistem.
|
||||
|
||||
Örnek: Beşiktaş-Galatasaray için model 3-1 tahmin ediyor
|
||||
→ DÜŞÜK RİSK: 1.5 Üst (yüksek ihtimal tutar)
|
||||
→ ORTA RİSK: MS 1 + 2.5 Üst (orta ihtimal)
|
||||
→ YÜKSEK RİSK: 3.5 Üst veya skor 3-1 (düşük ihtimal, yüksek kazanç)
|
||||
|
||||
Ayrıca kombinasyonlar:
|
||||
- MS 1 + 1.5 Üst
|
||||
- MS 1 + KG Var
|
||||
- Her iki takım skor > 0.5 (her takım en az 1 gol atar)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RiskLevel(Enum):
|
||||
LOW = "LOW" # Yüksek olasılık, düşük oran (güvenli)
|
||||
MEDIUM = "MEDIUM" # Orta olasılık, orta oran
|
||||
HIGH = "HIGH" # Düşük olasılık, yüksek kazanç
|
||||
EXTREME = "EXTREME" # Çok düşük olasılık, çok yüksek kazanç
|
||||
|
||||
|
||||
@dataclass
|
||||
class BetRecommendation:
|
||||
"""Tek bir bahis önerisi"""
|
||||
market: str # Piyasa adı (örn: "MS 1", "2.5 Üst")
|
||||
pick: str # Seçim (örn: "1", "OVER", "YES")
|
||||
odds: float # Oran
|
||||
probability: float # Model olasılığı (0-1)
|
||||
confidence: float # Güven seviyesi (0-100)
|
||||
risk_level: RiskLevel
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"market": self.market,
|
||||
"pick": self.pick,
|
||||
"odds": self.odds,
|
||||
"probability": round(self.probability * 100, 1),
|
||||
"confidence": round(self.confidence, 1),
|
||||
"risk_level": self.risk_level.value
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchPredictionSet:
|
||||
"""Bir maç için tüm tahmin seti"""
|
||||
match_name: str
|
||||
predicted_score: Tuple[int, int] # (home, away)
|
||||
home_win_prob: float
|
||||
draw_prob: float
|
||||
away_win_prob: float
|
||||
over_15_prob: float
|
||||
over_25_prob: float
|
||||
over_35_prob: float
|
||||
btts_yes_prob: float
|
||||
|
||||
# Öneriler
|
||||
low_risk_bets: List[BetRecommendation]
|
||||
medium_risk_bets: List[BetRecommendation]
|
||||
high_risk_bets: List[BetRecommendation]
|
||||
extreme_risk_bets: List[BetRecommendation]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"match_name": self.match_name,
|
||||
"predicted_score": f"{self.predicted_score[0]}-{self.predicted_score[1]}",
|
||||
"probs": {
|
||||
"home_win": round(self.home_win_prob * 100, 1),
|
||||
"draw": round(self.draw_prob * 100, 1),
|
||||
"away_win": round(self.away_win_prob * 100, 1),
|
||||
"over_15": round(self.over_15_prob * 100, 1),
|
||||
"over_25": round(self.over_25_prob * 100, 1),
|
||||
"over_35": round(self.over_35_prob * 100, 1),
|
||||
"btts": round(self.btts_yes_prob * 100, 1)
|
||||
},
|
||||
"low_risk": [b.to_dict() for b in self.low_risk_bets],
|
||||
"medium_risk": [b.to_dict() for b in self.medium_risk_bets],
|
||||
"high_risk": [b.to_dict() for b in self.high_risk_bets],
|
||||
"extreme_risk": [b.to_dict() for b in self.extreme_risk_bets]
|
||||
}
|
||||
|
||||
|
||||
class SmartBetRecommender:
|
||||
"""
|
||||
Akıllı Bahis Öneri Sistemi
|
||||
|
||||
Skor tahminine göre farklı risk seviyelerinde bahisler önerir.
|
||||
|
||||
Mantık:
|
||||
1. DÜŞÜK RİSK: Yüksek olasılıklı (>70%), düşük oranlı bahisler
|
||||
- 1.5 Üst
|
||||
- Double Chance
|
||||
- Favori takım gol atar
|
||||
|
||||
2. ORTA RİSK: Orta olasılıklı (50-70%), orta oranlı bahisler
|
||||
- MS favori
|
||||
- 2.5 Üst
|
||||
- KG Var/Var
|
||||
|
||||
3. YÜKSEK RİSK: Düşük olasılıklı (30-50%), yüksek oranlı bahisler
|
||||
- 3.5 Üst
|
||||
- Skor tahmini
|
||||
- Handikap
|
||||
|
||||
4. EXTREME RİSK: Çok düşük olasılıklı (<30%), çok yüksek oranlı
|
||||
- Tam skor
|
||||
- Uzunluklu kombinasyonlar
|
||||
"""
|
||||
|
||||
# Olasılık eşikleri
|
||||
PROB_LOW_RISK = 0.70 # > %70 olasılık
|
||||
PROB_MEDIUM_RISK = 0.50 # %50-70 olasılık
|
||||
PROB_HIGH_RISK = 0.30 # %30-50 olasılık
|
||||
# < %30 = EXTREME
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _determine_risk(self, probability: float) -> RiskLevel:
|
||||
"""Olasılığa göre risk seviyesi belirle"""
|
||||
if probability >= self.PROB_LOW_RISK:
|
||||
return RiskLevel.LOW
|
||||
elif probability >= self.PROB_MEDIUM_RISK:
|
||||
return RiskLevel.MEDIUM
|
||||
elif probability >= self.PROB_HIGH_RISK:
|
||||
return RiskLevel.HIGH
|
||||
else:
|
||||
return RiskLevel.EXTREME
|
||||
|
||||
def _get_favorite(self, home_prob: float, draw_prob: float, away_prob: float) -> Tuple[str, float]:
|
||||
"""Favori sonucu ve olasılığını döndür"""
|
||||
if home_prob >= draw_prob and home_prob >= away_prob:
|
||||
return "1", home_prob
|
||||
elif away_prob >= home_prob and away_prob >= draw_prob:
|
||||
return "2", away_prob
|
||||
else:
|
||||
return "X", draw_prob
|
||||
|
||||
def _calculate_expected_goals(self, predicted_score: Tuple[int, int]) -> float:
|
||||
"""Tahmin edilen skora göre beklenen gol sayısı"""
|
||||
return predicted_score[0] + predicted_score[1]
|
||||
|
||||
def recommend(
|
||||
self,
|
||||
match_name: str,
|
||||
predicted_score: Tuple[int, int],
|
||||
probs: Dict[str, float],
|
||||
odds: Dict[str, float]
|
||||
) -> MatchPredictionSet:
|
||||
"""
|
||||
Maç için tüm bahis önerilerini oluştur.
|
||||
|
||||
Args:
|
||||
match_name: Maç adı
|
||||
predicted_score: (home_goals, away_goals)
|
||||
probs: {"home_win": 0.55, "draw": 0.25, "away_win": 0.20,
|
||||
"over_15": 0.85, "over_25": 0.65, "over_35": 0.35,
|
||||
"btts_yes": 0.55}
|
||||
odds: {"1": 1.80, "X": 3.50, "2": 4.20,
|
||||
"ou15_o": 1.25, "ou15_u": 3.80,
|
||||
"ou25_o": 1.90, "ou25_u": 1.85,
|
||||
"ou35_o": 3.20, "ou35_u": 1.30,
|
||||
"btts_y": 1.75, "btts_n": 2.00}
|
||||
|
||||
Returns:
|
||||
MatchPredictionSet with all recommendations
|
||||
"""
|
||||
home_prob = probs.get("home_win", 0.33)
|
||||
draw_prob = probs.get("draw", 0.33)
|
||||
away_prob = probs.get("away_win", 0.33)
|
||||
over_15_prob = probs.get("over_15", 0.70)
|
||||
over_25_prob = probs.get("over_25", 0.50)
|
||||
over_35_prob = probs.get("over_35", 0.30)
|
||||
btts_prob = probs.get("btts_yes", 0.50)
|
||||
|
||||
# Beklenen goller
|
||||
expected_goals = self._calculate_expected_goals(predicted_score)
|
||||
|
||||
# Favori
|
||||
favorite, favorite_prob = self._get_favorite(home_prob, draw_prob, away_prob)
|
||||
|
||||
# Önerileri oluştur
|
||||
low_risk = []
|
||||
medium_risk = []
|
||||
high_risk = []
|
||||
extreme_risk = []
|
||||
|
||||
# ========== DÜŞÜK RİSK ÖNERİLERİ ==========
|
||||
# 1.5 Üst (en güvenli)
|
||||
if over_15_prob >= self.PROB_LOW_RISK:
|
||||
low_risk.append(BetRecommendation(
|
||||
market="1.5 Üst/Alt",
|
||||
pick="OVER",
|
||||
odds=odds.get("ou15_o", 1.25),
|
||||
probability=over_15_prob,
|
||||
confidence=over_15_prob * 100,
|
||||
risk_level=RiskLevel.LOW
|
||||
))
|
||||
|
||||
# Double Chance
|
||||
if home_prob > away_prob:
|
||||
dc_prob = home_prob + draw_prob
|
||||
if dc_prob >= self.PROB_LOW_RISK:
|
||||
low_risk.append(BetRecommendation(
|
||||
market="Double Chance",
|
||||
pick="1X",
|
||||
odds=odds.get("dc_1x", 1.30),
|
||||
probability=dc_prob,
|
||||
confidence=dc_prob * 100,
|
||||
risk_level=RiskLevel.LOW
|
||||
))
|
||||
elif away_prob > home_prob:
|
||||
dc_prob = away_prob + draw_prob
|
||||
if dc_prob >= self.PROB_LOW_RISK:
|
||||
low_risk.append(BetRecommendation(
|
||||
market="Double Chance",
|
||||
pick="X2",
|
||||
odds=odds.get("dc_x2", 1.30),
|
||||
probability=dc_prob,
|
||||
confidence=dc_prob * 100,
|
||||
risk_level=RiskLevel.LOW
|
||||
))
|
||||
|
||||
# ========== ORTA RİSK ÖNERİLERİ ==========
|
||||
# MS Favori
|
||||
if self.PROB_MEDIUM_RISK <= favorite_prob < self.PROB_LOW_RISK:
|
||||
medium_risk.append(BetRecommendation(
|
||||
market="Maç Sonucu",
|
||||
pick=favorite,
|
||||
odds=odds.get(favorite, 2.00),
|
||||
probability=favorite_prob,
|
||||
confidence=favorite_prob * 100,
|
||||
risk_level=RiskLevel.MEDIUM
|
||||
))
|
||||
|
||||
# 2.5 Üst
|
||||
if self.PROB_MEDIUM_RISK <= over_25_prob < self.PROB_LOW_RISK:
|
||||
medium_risk.append(BetRecommendation(
|
||||
market="2.5 Üst/Alt",
|
||||
pick="OVER",
|
||||
odds=odds.get("ou25_o", 1.90),
|
||||
probability=over_25_prob,
|
||||
confidence=over_25_prob * 100,
|
||||
risk_level=RiskLevel.MEDIUM
|
||||
))
|
||||
|
||||
# KG Var
|
||||
if self.PROB_MEDIUM_RISK <= btts_prob < self.PROB_LOW_RISK:
|
||||
medium_risk.append(BetRecommendation(
|
||||
market="Karşılıklı Gol",
|
||||
pick="YES",
|
||||
odds=odds.get("btts_y", 1.75),
|
||||
probability=btts_prob,
|
||||
confidence=btts_prob * 100,
|
||||
risk_level=RiskLevel.MEDIUM
|
||||
))
|
||||
|
||||
# MS + 2.5 Üst kombinasyonu
|
||||
if favorite_prob >= 0.45 and over_25_prob >= 0.50:
|
||||
combo_prob = favorite_prob * over_25_prob # Basit çarpım
|
||||
combo_odds = odds.get(favorite, 2.00) * odds.get("ou25_o", 1.90)
|
||||
if combo_prob >= 0.30: # En az %30 olasılık
|
||||
medium_risk.append(BetRecommendation(
|
||||
market=f"MS {favorite} + 2.5 Üst",
|
||||
pick=f"{favorite} & OVER",
|
||||
odds=combo_odds,
|
||||
probability=combo_prob,
|
||||
confidence=combo_prob * 100,
|
||||
risk_level=RiskLevel.MEDIUM
|
||||
))
|
||||
|
||||
# ========== YÜKSEK RİSK ÖNERİLERİ ==========
|
||||
# 3.5 Üst
|
||||
if self.PROB_HIGH_RISK <= over_35_prob < self.PROB_MEDIUM_RISK:
|
||||
high_risk.append(BetRecommendation(
|
||||
market="3.5 Üst/Alt",
|
||||
pick="OVER",
|
||||
odds=odds.get("ou35_o", 3.20),
|
||||
probability=over_35_prob,
|
||||
confidence=over_35_prob * 100,
|
||||
risk_level=RiskLevel.HIGH
|
||||
))
|
||||
|
||||
# Skor tahmini (yüksek skorlu maçlar için)
|
||||
if expected_goals >= 3.5:
|
||||
score_str = f"{predicted_score[0]}-{predicted_score[1]}"
|
||||
# Skor olasılığı tahmini (basit model)
|
||||
score_prob = 0.15 if expected_goals <= 4 else 0.10
|
||||
high_risk.append(BetRecommendation(
|
||||
market="Tam Skor",
|
||||
pick=score_str,
|
||||
odds=8.0, # Tahmini oran
|
||||
probability=score_prob,
|
||||
confidence=score_prob * 100,
|
||||
risk_level=RiskLevel.HIGH
|
||||
))
|
||||
|
||||
# MS + 3.5 Üst
|
||||
if favorite_prob >= 0.40 and over_35_prob >= 0.30:
|
||||
combo_prob = favorite_prob * over_35_prob
|
||||
combo_odds = odds.get(favorite, 2.00) * odds.get("ou35_o", 3.20)
|
||||
high_risk.append(BetRecommendation(
|
||||
market=f"MS {favorite} + 3.5 Üst",
|
||||
pick=f"{favorite} & OVER",
|
||||
odds=combo_odds,
|
||||
probability=combo_prob,
|
||||
confidence=combo_prob * 100,
|
||||
risk_level=RiskLevel.HIGH
|
||||
))
|
||||
|
||||
# ========== EXTREME RİSK ÖNERİLERİ ==========
|
||||
# Uzun kombinasyonlar
|
||||
if favorite_prob >= 0.50 and btts_prob >= 0.50 and over_25_prob >= 0.60:
|
||||
combo_prob = favorite_prob * btts_prob * over_25_prob
|
||||
combo_odds = odds.get(favorite, 2.00) * odds.get("btts_y", 1.75) * odds.get("ou25_o", 1.90)
|
||||
if combo_prob >= 0.15: # En az %15 olasılık
|
||||
extreme_risk.append(BetRecommendation(
|
||||
market=f"MS {favorite} + KG Var + 2.5 Üst",
|
||||
pick=f"{favorite} & BTTS & OVER",
|
||||
odds=combo_odds,
|
||||
probability=combo_prob,
|
||||
confidence=combo_prob * 100,
|
||||
risk_level=RiskLevel.EXTREME
|
||||
))
|
||||
|
||||
return MatchPredictionSet(
|
||||
match_name=match_name,
|
||||
predicted_score=predicted_score,
|
||||
home_win_prob=home_prob,
|
||||
draw_prob=draw_prob,
|
||||
away_win_prob=away_prob,
|
||||
over_15_prob=over_15_prob,
|
||||
over_25_prob=over_25_prob,
|
||||
over_35_prob=over_35_prob,
|
||||
btts_yes_prob=btts_prob,
|
||||
low_risk_bets=low_risk,
|
||||
medium_risk_bets=medium_risk,
|
||||
high_risk_bets=high_risk,
|
||||
extreme_risk_bets=extreme_risk
|
||||
)
|
||||
|
||||
|
||||
# Singleton
|
||||
_recommender = None
|
||||
|
||||
def get_smart_bet_recommender() -> SmartBetRecommender:
|
||||
global _recommender
|
||||
if _recommender is None:
|
||||
_recommender = SmartBetRecommender()
|
||||
return _recommender
|
||||
Executable
+582
@@ -0,0 +1,582 @@
|
||||
"""
|
||||
Squad Analysis Engine - V9 Feature
|
||||
Kadro ve oyuncu bazlı analiz.
|
||||
|
||||
Analiz Edilen Metrikler:
|
||||
- İlk 11 kalitesi (golcü formu, key player)
|
||||
- Yedek gücü
|
||||
- Eksik oyuncu etkisi
|
||||
- Pozisyon bazlı güç
|
||||
- Takım içi golcü dağılımı
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, Optional, List, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerForm:
|
||||
"""Oyuncu form bilgisi"""
|
||||
player_id: str
|
||||
player_name: str
|
||||
goals_last_5: int = 0
|
||||
assists_last_5: int = 0
|
||||
minutes_last_5: int = 0
|
||||
cards_last_5: int = 0
|
||||
is_key_player: bool = False # Golcü veya sık oynayan
|
||||
|
||||
|
||||
@dataclass
|
||||
class SquadAnalysis:
|
||||
"""Takım kadro analizi"""
|
||||
team_id: str
|
||||
team_name: str = ""
|
||||
|
||||
# İlk 11 bilgisi
|
||||
starting_count: int = 0
|
||||
sub_count: int = 0
|
||||
total_squad: int = 0
|
||||
|
||||
# Pozisyon dağılımı
|
||||
goalkeeper_count: int = 0
|
||||
defender_count: int = 0
|
||||
midfielder_count: int = 0
|
||||
forward_count: int = 0
|
||||
|
||||
# Form metrikleri
|
||||
total_goals_last_5: int = 0 # Kadrodaki oyuncuların son 5 maçtaki golleri
|
||||
total_assists_last_5: int = 0
|
||||
key_players_count: int = 0 # Golcü sayısı
|
||||
key_player_missing: int = 0 # Eksik golcü
|
||||
|
||||
# Kalite metrikleri
|
||||
avg_minutes_per_player: float = 0.0 # Ortalama oynama süresi
|
||||
squad_experience: float = 0.0 # 0-1, takımla oynama deneyimi
|
||||
rotation_rate: float = 0.0 # Kadro rotasyonu oranı
|
||||
|
||||
|
||||
@dataclass
|
||||
class SquadFeatures:
|
||||
"""Model için kadro feature'ları"""
|
||||
# Home team features
|
||||
home_starting_11: int = 11
|
||||
home_sub_count: int = 7
|
||||
home_total_squad: int = 18
|
||||
home_goalkeepers: int = 1
|
||||
home_defenders: int = 4
|
||||
home_midfielders: int = 4
|
||||
home_forwards: int = 2
|
||||
home_goals_last_5: int = 0
|
||||
home_assists_last_5: int = 0
|
||||
home_key_players: int = 0
|
||||
home_squad_experience: float = 0.5
|
||||
|
||||
# Away team features
|
||||
away_starting_11: int = 11
|
||||
away_sub_count: int = 7
|
||||
away_total_squad: int = 18
|
||||
away_goalkeepers: int = 1
|
||||
away_defenders: int = 4
|
||||
away_midfielders: int = 4
|
||||
away_forwards: int = 2
|
||||
away_goals_last_5: int = 0
|
||||
away_assists_last_5: int = 0
|
||||
away_key_players: int = 0
|
||||
away_squad_experience: float = 0.5
|
||||
|
||||
# Comparison features
|
||||
squad_strength_diff: float = 0.0 # + = home stronger
|
||||
goals_form_diff: float = 0.0
|
||||
key_players_diff: int = 0
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
return {
|
||||
# Home
|
||||
'home_starting_11': float(self.home_starting_11),
|
||||
'home_sub_count': float(self.home_sub_count),
|
||||
'home_total_squad': float(self.home_total_squad),
|
||||
'home_goalkeepers': float(self.home_goalkeepers),
|
||||
'home_defenders': float(self.home_defenders),
|
||||
'home_midfielders': float(self.home_midfielders),
|
||||
'home_forwards': float(self.home_forwards),
|
||||
'home_goals_last_5': float(self.home_goals_last_5),
|
||||
'home_assists_last_5': float(self.home_assists_last_5),
|
||||
'home_key_players': float(self.home_key_players),
|
||||
'home_squad_experience': self.home_squad_experience,
|
||||
# Away
|
||||
'away_starting_11': float(self.away_starting_11),
|
||||
'away_sub_count': float(self.away_sub_count),
|
||||
'away_total_squad': float(self.away_total_squad),
|
||||
'away_goalkeepers': float(self.away_goalkeepers),
|
||||
'away_defenders': float(self.away_defenders),
|
||||
'away_midfielders': float(self.away_midfielders),
|
||||
'away_forwards': float(self.away_forwards),
|
||||
'away_goals_last_5': float(self.away_goals_last_5),
|
||||
'away_assists_last_5': float(self.away_assists_last_5),
|
||||
'away_key_players': float(self.away_key_players),
|
||||
'away_squad_experience': self.away_squad_experience,
|
||||
# Diffs
|
||||
'squad_strength_diff': self.squad_strength_diff,
|
||||
'goals_form_diff': self.goals_form_diff,
|
||||
'key_players_diff': float(self.key_players_diff),
|
||||
}
|
||||
|
||||
|
||||
class SquadAnalysisEngine:
|
||||
"""
|
||||
Kadro ve oyuncu analiz motoru.
|
||||
|
||||
Beşiktaş-Galatasaray maçı için:
|
||||
- İlk 11'deki oyuncuların son 5 maçtaki gol/asist
|
||||
- Key player tespiti (çok gol atan oyuncular)
|
||||
- Pozisyon dağılımı (4-3-3, 4-4-2 vb.)
|
||||
- Yedek kalitesi
|
||||
hesaplar.
|
||||
"""
|
||||
|
||||
# Pozisyon mapping
|
||||
POSITION_MAP = {
|
||||
'goalkeeper': 'GK',
|
||||
'gk': 'GK',
|
||||
'kaleci': 'GK',
|
||||
'defender': 'DEF',
|
||||
'def': 'DEF',
|
||||
'defans': 'DEF',
|
||||
'savunma': 'DEF',
|
||||
'midfielder': 'MID',
|
||||
'mid': 'MID',
|
||||
'orta saha': 'MID',
|
||||
'forward': 'FWD',
|
||||
'fwd': 'FWD',
|
||||
'forvet': 'FWD',
|
||||
'striker': 'FWD',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._player_form_cache: Dict[str, PlayerForm] = {}
|
||||
|
||||
def _connect_db(self):
|
||||
if psycopg2 is None:
|
||||
return None
|
||||
try:
|
||||
from data.db import get_clean_dsn
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
return self.conn
|
||||
except Exception as e:
|
||||
print(f"[SquadEngine] DB connection failed: {e}")
|
||||
return None
|
||||
|
||||
def get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
self._connect_db()
|
||||
return self.conn
|
||||
|
||||
def _normalize_position(self, position: Optional[str]) -> str:
|
||||
"""Pozisyonu normalize et"""
|
||||
if not position:
|
||||
return 'UNK'
|
||||
|
||||
pos_lower = position.lower().strip()
|
||||
for key, val in self.POSITION_MAP.items():
|
||||
if key in pos_lower:
|
||||
return val
|
||||
return 'UNK'
|
||||
|
||||
def get_player_form(self, player_id: str, before_date_ms: int = None) -> PlayerForm:
|
||||
"""Oyuncunun son 5 maçtaki formunu hesapla"""
|
||||
|
||||
if player_id in self._player_form_cache:
|
||||
return self._player_form_cache[player_id]
|
||||
|
||||
form = PlayerForm(player_id=player_id, player_name="")
|
||||
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return form
|
||||
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# Oyuncu adını al
|
||||
cur.execute("SELECT name FROM players WHERE id = %s", (player_id,))
|
||||
player_row = cur.fetchone()
|
||||
if player_row:
|
||||
form.player_name = player_row['name']
|
||||
|
||||
# Son 5 maçtaki gol ve asist
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE event_type = 'goal' AND event_subtype NOT ILIKE '%%penaltı kaçırma%%') as goals,
|
||||
COUNT(*) FILTER (WHERE event_type = 'goal' AND assist_player_id IS NOT NULL) as assists_given
|
||||
FROM match_player_events
|
||||
WHERE player_id = %s
|
||||
AND match_id IN (
|
||||
SELECT match_id FROM match_player_participation
|
||||
WHERE player_id = %s
|
||||
ORDER BY match_id DESC LIMIT 5
|
||||
)
|
||||
""", (player_id, player_id))
|
||||
|
||||
stats = cur.fetchone()
|
||||
if stats:
|
||||
form.goals_last_5 = stats['goals'] or 0
|
||||
|
||||
# Asist hesapla (assist_player_id olarak geçen)
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as assists
|
||||
FROM match_player_events
|
||||
WHERE assist_player_id = %s
|
||||
AND match_id IN (
|
||||
SELECT match_id FROM match_player_participation
|
||||
WHERE player_id = %s
|
||||
ORDER BY match_id DESC LIMIT 5
|
||||
)
|
||||
""", (player_id, player_id))
|
||||
|
||||
assist_row = cur.fetchone()
|
||||
if assist_row:
|
||||
form.assists_last_5 = assist_row['assists'] or 0
|
||||
|
||||
# Kart sayısı
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as cards
|
||||
FROM match_player_events
|
||||
WHERE player_id = %s AND event_type = 'card'
|
||||
AND match_id IN (
|
||||
SELECT match_id FROM match_player_participation
|
||||
WHERE player_id = %s
|
||||
ORDER BY match_id DESC LIMIT 5
|
||||
)
|
||||
""", (player_id, player_id))
|
||||
|
||||
card_row = cur.fetchone()
|
||||
if card_row:
|
||||
form.cards_last_5 = card_row['cards'] or 0
|
||||
|
||||
# Key player mi? (Son 10 maçta 3+ gol)
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as total_goals
|
||||
FROM match_player_events
|
||||
WHERE player_id = %s
|
||||
AND event_type = 'goal'
|
||||
AND event_subtype NOT ILIKE '%%penaltı kaçırma%%'
|
||||
""", (player_id,))
|
||||
|
||||
total_row = cur.fetchone()
|
||||
form.is_key_player = (total_row['total_goals'] or 0) >= 3
|
||||
|
||||
self._player_form_cache[player_id] = form
|
||||
return form
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"[SquadEngine] Error getting player form: {e}")
|
||||
return form
|
||||
|
||||
def analyze_squad(self, match_id: str, team_id: str) -> SquadAnalysis:
|
||||
"""Takımın maç kadrosunu analiz et"""
|
||||
|
||||
analysis = SquadAnalysis(team_id=team_id)
|
||||
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return analysis
|
||||
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# Takım adını al
|
||||
cur.execute("SELECT name FROM teams WHERE id = %s", (team_id,))
|
||||
team_row = cur.fetchone()
|
||||
if team_row:
|
||||
analysis.team_name = team_row['name']
|
||||
|
||||
# Maç kadrosunu al
|
||||
cur.execute("""
|
||||
SELECT player_id, position, is_starting
|
||||
FROM match_player_participation
|
||||
WHERE match_id = %s AND team_id = %s
|
||||
""", (match_id, team_id))
|
||||
|
||||
players = cur.fetchall()
|
||||
|
||||
for p in players:
|
||||
if p['is_starting']:
|
||||
analysis.starting_count += 1
|
||||
else:
|
||||
analysis.sub_count += 1
|
||||
|
||||
pos = self._normalize_position(p['position'])
|
||||
if pos == 'GK':
|
||||
analysis.goalkeeper_count += 1
|
||||
elif pos == 'DEF':
|
||||
analysis.defender_count += 1
|
||||
elif pos == 'MID':
|
||||
analysis.midfielder_count += 1
|
||||
elif pos == 'FWD':
|
||||
analysis.forward_count += 1
|
||||
|
||||
# İlk 11'in formunu topluca hesapla
|
||||
if p['is_starting']:
|
||||
form = self.get_player_form(p['player_id'])
|
||||
analysis.total_goals_last_5 += form.goals_last_5
|
||||
analysis.total_assists_last_5 += form.assists_last_5
|
||||
if form.is_key_player:
|
||||
analysis.key_players_count += 1
|
||||
|
||||
analysis.total_squad = analysis.starting_count + analysis.sub_count
|
||||
|
||||
# Takım deneyimi (bu takımla kaç maç oynamışlar)
|
||||
if analysis.starting_count > 0:
|
||||
cur.execute("""
|
||||
SELECT AVG(match_count) as avg_exp
|
||||
FROM (
|
||||
SELECT player_id, COUNT(*) as match_count
|
||||
FROM match_player_participation
|
||||
WHERE team_id = %s AND is_starting = true
|
||||
GROUP BY player_id
|
||||
) sub
|
||||
""", (team_id,))
|
||||
|
||||
exp_row = cur.fetchone()
|
||||
if exp_row and exp_row['avg_exp']:
|
||||
# Normalize: 50+ maç = 1.0
|
||||
analysis.squad_experience = min(exp_row['avg_exp'] / 50, 1.0)
|
||||
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SquadEngine] Error analyzing squad: {e}")
|
||||
return analysis
|
||||
|
||||
def analyze_squad_from_list(self, player_ids: List[str], team_id: str) -> SquadAnalysis:
|
||||
"""
|
||||
Memory'deki oyuncu listesinden kadro analizi yap.
|
||||
DB'de olmayan canlı maçlar için kullanılır.
|
||||
"""
|
||||
analysis = SquadAnalysis(team_id=team_id)
|
||||
# Varsayılan: İlk 11 oyuncu (listede genellikle ilk 11 verilir)
|
||||
|
||||
# Eğer liste boşsa
|
||||
if not player_ids:
|
||||
return analysis
|
||||
|
||||
# Varsayımlar: Mackolik API'den gelen liste sıralıdır.
|
||||
# İlk 11 genellikle as kadrodur. Ancak burada sadece 'starting' oyuncuları alıyoruz varsayalım.
|
||||
# User calling uses explicit starting 11 list.
|
||||
|
||||
analysis.starting_count = len(player_ids)
|
||||
analysis.total_squad = len(player_ids) # Subs unknown usually unless separate list
|
||||
|
||||
# Position tahmini zor, default dağıt? Veya oyuncu detayına git?
|
||||
# Hız için: Oyuncu ID'sinden DB'ye bakıp pozisyon öğrenmeye çalışabiliriz.
|
||||
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return analysis
|
||||
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# Calculate stats for these specific players
|
||||
for pid in player_ids:
|
||||
# Get Form
|
||||
form = self.get_player_form(pid)
|
||||
analysis.total_goals_last_5 += form.goals_last_5
|
||||
analysis.total_assists_last_5 += form.assists_last_5
|
||||
if form.is_key_player:
|
||||
analysis.key_players_count += 1
|
||||
|
||||
# Get Position/Exp history attempt
|
||||
cur.execute("""
|
||||
SELECT position, COUNT(*) as match_count
|
||||
FROM match_player_participation
|
||||
WHERE player_id = %s AND team_id = %s
|
||||
GROUP BY position
|
||||
ORDER BY match_count DESC LIMIT 1
|
||||
""", (pid, team_id))
|
||||
row = cur.fetchone()
|
||||
|
||||
if row:
|
||||
pos = self._normalize_position(row.get('position', 'UNK'))
|
||||
if pos == 'GK': analysis.goalkeeper_count += 1
|
||||
elif pos == 'DEF': analysis.defender_count += 1
|
||||
elif pos == 'MID': analysis.midfielder_count += 1
|
||||
elif pos == 'FWD': analysis.forward_count += 1
|
||||
|
||||
# Experience contribution
|
||||
exp = min(row['match_count'] / 50.0, 1.0)
|
||||
analysis.squad_experience += exp
|
||||
|
||||
# Average experience
|
||||
if analysis.starting_count > 0:
|
||||
analysis.squad_experience /= analysis.starting_count
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SquadEngine] Live analyze error: {e}")
|
||||
|
||||
return analysis
|
||||
|
||||
def get_features(
|
||||
self,
|
||||
match_id: str,
|
||||
home_team_id: str,
|
||||
away_team_id: str
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Maç için kadro feature'larını hesapla.
|
||||
|
||||
Args:
|
||||
match_id: Maç ID'si
|
||||
home_team_id: Ev sahibi takım ID
|
||||
away_team_id: Deplasman takım ID
|
||||
|
||||
Returns:
|
||||
Kadro feature'ları dict olarak
|
||||
"""
|
||||
features = SquadFeatures()
|
||||
|
||||
# Ev sahibi analizi
|
||||
home = self.analyze_squad(match_id, home_team_id)
|
||||
features.home_starting_11 = home.starting_count
|
||||
features.home_sub_count = home.sub_count
|
||||
features.home_total_squad = home.total_squad
|
||||
features.home_goalkeepers = home.goalkeeper_count
|
||||
features.home_defenders = home.defender_count
|
||||
features.home_midfielders = home.midfielder_count
|
||||
features.home_forwards = home.forward_count
|
||||
features.home_goals_last_5 = home.total_goals_last_5
|
||||
features.home_assists_last_5 = home.total_assists_last_5
|
||||
features.home_key_players = home.key_players_count
|
||||
features.home_squad_experience = home.squad_experience
|
||||
|
||||
# Deplasman analizi
|
||||
away = self.analyze_squad(match_id, away_team_id)
|
||||
features.away_starting_11 = away.starting_count
|
||||
features.away_sub_count = away.sub_count
|
||||
features.away_total_squad = away.total_squad
|
||||
features.away_goalkeepers = away.goalkeeper_count
|
||||
features.away_defenders = away.defender_count
|
||||
features.away_midfielders = away.midfielder_count
|
||||
features.away_forwards = away.forward_count
|
||||
features.away_goals_last_5 = away.total_goals_last_5
|
||||
features.away_assists_last_5 = away.total_assists_last_5
|
||||
features.away_key_players = away.key_players_count
|
||||
features.away_squad_experience = away.squad_experience
|
||||
|
||||
# Karşılaştırma feature'ları
|
||||
home_strength = (
|
||||
home.total_goals_last_5 * 2 +
|
||||
home.total_assists_last_5 +
|
||||
home.key_players_count * 3 +
|
||||
home.squad_experience * 10
|
||||
)
|
||||
away_strength = (
|
||||
away.total_goals_last_5 * 2 +
|
||||
away.total_assists_last_5 +
|
||||
away.key_players_count * 3 +
|
||||
away.squad_experience * 10
|
||||
)
|
||||
|
||||
features.squad_strength_diff = home_strength - away_strength
|
||||
features.goals_form_diff = home.total_goals_last_5 - away.total_goals_last_5
|
||||
features.key_players_diff = home.key_players_count - away.key_players_count
|
||||
|
||||
return features.to_dict()
|
||||
|
||||
def get_features_without_match(
|
||||
self,
|
||||
home_team_id: str,
|
||||
away_team_id: str
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Maç ID olmadan takım bazlı feature'ları hesapla.
|
||||
Son maçtaki kadroyu referans alır.
|
||||
"""
|
||||
features = SquadFeatures()
|
||||
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return features.to_dict()
|
||||
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
for team_id, prefix in [(home_team_id, 'home'), (away_team_id, 'away')]:
|
||||
# Son maçı bul
|
||||
cur.execute("""
|
||||
SELECT mpp.match_id
|
||||
FROM match_player_participation mpp
|
||||
JOIN matches m ON mpp.match_id = m.id
|
||||
WHERE mpp.team_id = %s
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1
|
||||
""", (team_id,))
|
||||
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
analysis = self.analyze_squad(row['match_id'], team_id)
|
||||
|
||||
if prefix == 'home':
|
||||
features.home_starting_11 = analysis.starting_count
|
||||
features.home_sub_count = analysis.sub_count
|
||||
features.home_total_squad = analysis.total_squad
|
||||
features.home_goals_last_5 = analysis.total_goals_last_5
|
||||
features.home_assists_last_5 = analysis.total_assists_last_5
|
||||
features.home_key_players = analysis.key_players_count
|
||||
features.home_squad_experience = analysis.squad_experience
|
||||
else:
|
||||
features.away_starting_11 = analysis.starting_count
|
||||
features.away_sub_count = analysis.sub_count
|
||||
features.away_total_squad = analysis.total_squad
|
||||
features.away_goals_last_5 = analysis.total_goals_last_5
|
||||
features.away_assists_last_5 = analysis.total_assists_last_5
|
||||
features.away_key_players = analysis.key_players_count
|
||||
features.away_squad_experience = analysis.squad_experience
|
||||
|
||||
# Karşılaştırma
|
||||
features.goals_form_diff = features.home_goals_last_5 - features.away_goals_last_5
|
||||
features.key_players_diff = features.home_key_players - features.away_key_players
|
||||
|
||||
return features.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SquadEngine] Error: {e}")
|
||||
return features.to_dict()
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_engine: Optional[SquadAnalysisEngine] = None
|
||||
|
||||
|
||||
def get_squad_analysis_engine() -> SquadAnalysisEngine:
|
||||
"""Singleton squad analysis engine instance döndür"""
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = SquadAnalysisEngine()
|
||||
return _engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test
|
||||
engine = get_squad_analysis_engine()
|
||||
|
||||
print("\n🧪 Squad Analysis Engine Test")
|
||||
print("=" * 50)
|
||||
|
||||
# Test with known team IDs (Galatasaray, Fenerbahce)
|
||||
features = engine.get_features_without_match(
|
||||
home_team_id="test_gs",
|
||||
away_team_id="test_fb"
|
||||
)
|
||||
|
||||
print("\n📊 Features:")
|
||||
for key, value in features.items():
|
||||
print(f" {key}: {value:.2f}")
|
||||
Executable
+194
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Team Stats Engine
|
||||
Takımların oyun tarzı istatistiklerini analiz eder.
|
||||
football_team_stats tablosundaki kayıtlardan possession, şut, korner verilerini kullanır.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import psycopg2
|
||||
from typing import Dict
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from data.db import get_clean_dsn
|
||||
|
||||
|
||||
class TeamStatsEngine:
|
||||
"""
|
||||
Takım istatistikleri için feature engine.
|
||||
|
||||
Analiz edilen metrikler:
|
||||
- Ortalama top hakimiyeti (possession)
|
||||
- Ortalama isabetli şut
|
||||
- Ortalama korner
|
||||
- Şut/Gol dönüşüm oranı (xG benzeri)
|
||||
- Savunma gücü
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
|
||||
def get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
return self.conn
|
||||
|
||||
def get_features(self, team_id: str, before_date: int,
|
||||
limit: int = 10, max_days: int = 180) -> Dict[str, float]:
|
||||
"""
|
||||
Takımın oyun tarzı feature'larını hesapla.
|
||||
|
||||
Args:
|
||||
team_id: Takım ID
|
||||
before_date: Bu tarihten önceki maçlara bak (ms timestamp)
|
||||
limit: Kaç maç analiz edilecek
|
||||
max_days: Maksimum kaç gün geriye gidilecek
|
||||
|
||||
Returns:
|
||||
Dict: Team stats feature'ları
|
||||
"""
|
||||
if not team_id or len(team_id) < 5:
|
||||
return self._default_features()
|
||||
|
||||
try:
|
||||
conn = self.get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
min_date = before_date - (max_days * 24 * 60 * 60 * 1000)
|
||||
|
||||
# Bu takımın son N maçındaki istatistikleri çek
|
||||
cur.execute("""
|
||||
SELECT
|
||||
mts.possession_percentage,
|
||||
mts.shots_on_target,
|
||||
mts.shots_off_target,
|
||||
mts.total_shots,
|
||||
mts.corners,
|
||||
mts.fouls,
|
||||
m.score_home,
|
||||
m.score_away,
|
||||
m.home_team_id
|
||||
FROM football_team_stats mts
|
||||
JOIN matches m ON mts.match_id = m.id
|
||||
WHERE mts.team_id = %s
|
||||
AND m.mst_utc < %s
|
||||
AND m.mst_utc > %s
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.sport = 'football'
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
""", (team_id, before_date, min_date, limit))
|
||||
|
||||
stats = cur.fetchall()
|
||||
|
||||
if not stats:
|
||||
return self._default_features()
|
||||
|
||||
# İstatistikleri hesapla
|
||||
total_matches = len(stats)
|
||||
|
||||
possession_sum = 0
|
||||
shots_on_target_sum = 0
|
||||
shots_total_sum = 0
|
||||
corners_sum = 0
|
||||
fouls_sum = 0
|
||||
goals_scored = 0
|
||||
valid_possession_count = 0
|
||||
|
||||
for stat in stats:
|
||||
poss, sot, soff, total_shots, corners, fouls, sh, sa, home_id = stat
|
||||
|
||||
if poss and poss > 0:
|
||||
possession_sum += poss
|
||||
valid_possession_count += 1
|
||||
|
||||
if sot:
|
||||
shots_on_target_sum += sot
|
||||
if total_shots:
|
||||
shots_total_sum += total_shots
|
||||
if corners:
|
||||
corners_sum += corners
|
||||
if fouls:
|
||||
fouls_sum += fouls
|
||||
|
||||
# Gol hesaplama
|
||||
is_home = (home_id == team_id)
|
||||
goals_scored += sh if is_home else sa
|
||||
|
||||
avg_possession = possession_sum / valid_possession_count if valid_possession_count > 0 else 50.0
|
||||
avg_shots_on_target = shots_on_target_sum / total_matches if total_matches > 0 else 3.0
|
||||
avg_shots_total = shots_total_sum / total_matches if total_matches > 0 else 10.0
|
||||
avg_corners = corners_sum / total_matches if total_matches > 0 else 4.0
|
||||
avg_fouls = fouls_sum / total_matches if total_matches > 0 else 12.0
|
||||
|
||||
# Shot conversion rate (xG benzeri)
|
||||
shot_conversion = goals_scored / shots_total_sum if shots_total_sum > 0 else 0.1
|
||||
|
||||
# Shot accuracy
|
||||
shot_accuracy = shots_on_target_sum / shots_total_sum if shots_total_sum > 0 else 0.35
|
||||
|
||||
return {
|
||||
'avg_possession': avg_possession / 100, # Normalize to 0-1
|
||||
'avg_shots_on_target': avg_shots_on_target,
|
||||
'avg_shots_total': avg_shots_total,
|
||||
'avg_corners': avg_corners,
|
||||
'avg_fouls': avg_fouls,
|
||||
'shot_conversion_rate': shot_conversion,
|
||||
'shot_accuracy': shot_accuracy,
|
||||
'attacking_intensity': (avg_shots_total + avg_corners) / 2
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[TeamStatsEngine] Error: {e}")
|
||||
return self._default_features()
|
||||
|
||||
def _default_features(self) -> Dict[str, float]:
|
||||
return {
|
||||
'avg_possession': 0.50,
|
||||
'avg_shots_on_target': 3.5,
|
||||
'avg_shots_total': 11.0,
|
||||
'avg_corners': 4.5,
|
||||
'avg_fouls': 12.0,
|
||||
'shot_conversion_rate': 0.10,
|
||||
'shot_accuracy': 0.35,
|
||||
'attacking_intensity': 7.5
|
||||
}
|
||||
|
||||
|
||||
# Singleton
|
||||
_engine = None
|
||||
|
||||
def get_team_stats_engine() -> TeamStatsEngine:
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = TeamStatsEngine()
|
||||
return _engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
engine = get_team_stats_engine()
|
||||
|
||||
print("\n🧪 Team Stats Engine Test")
|
||||
print("=" * 50)
|
||||
|
||||
# Test için örnek takım ID'si al
|
||||
conn = engine.get_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT DISTINCT mts.team_id, t.name
|
||||
FROM match_team_stats mts
|
||||
JOIN teams t ON mts.team_id = t.id
|
||||
LIMIT 1
|
||||
""")
|
||||
result = cur.fetchone()
|
||||
|
||||
if result:
|
||||
team_id, team_name = result
|
||||
print(f"Test Takımı: {team_name}")
|
||||
|
||||
import time
|
||||
features = engine.get_features(team_id, int(time.time() * 1000))
|
||||
|
||||
print(f"\n📊 Feature'lar:")
|
||||
for k, v in features.items():
|
||||
print(f" {k}: {v:.3f}")
|
||||
Executable
+419
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
Upset Engine - Dev Avcısı Tespit Sistemi
|
||||
V9 Model için Galatasaray-Liverpool tarzı sürpriz maçları tespit eder.
|
||||
|
||||
Faktörler:
|
||||
1. Atmosfer (Avrupa gecesi, taraftar baskısı)
|
||||
2. Motivasyon asimetrisi (küme düşme vs şampiyon)
|
||||
3. Yorgunluk (maç yoğunluğu, seyahat)
|
||||
4. Tarihsel upset pattern
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpsetFactors:
|
||||
"""Upset potansiyelini etkileyen faktörler"""
|
||||
atmosphere_score: float = 0.0 # Atmosfer etkisi (0-1)
|
||||
motivation_score: float = 0.0 # Motivasyon asimetrisi (0-1)
|
||||
fatigue_score: float = 0.0 # Yorgunluk farkı (0-1)
|
||||
historical_upset_rate: float = 0.0 # Tarihsel upset oranı (0-1)
|
||||
total_upset_potential: float = 0.0 # Toplam upset potansiyeli (0-1)
|
||||
reasoning: list = field(default_factory=list)
|
||||
|
||||
|
||||
class UpsetEngine:
|
||||
"""
|
||||
Favori takımın kaybedeceği maçları tespit eder.
|
||||
Galatasaray-Liverpool tarzı sürprizleri yakalar.
|
||||
"""
|
||||
|
||||
# Yüksek atmosferli stadyumlar (manuel tanımlı + hesaplanabilir)
|
||||
HIGH_ATMOSPHERE_TEAMS = {
|
||||
# Türkiye
|
||||
"galatasaray", "fenerbahce", "besiktas", "trabzonspor",
|
||||
# İngiltere
|
||||
"liverpool", "newcastle", "leeds",
|
||||
# Almanya
|
||||
"dortmund", "union berlin",
|
||||
# Yunanistan
|
||||
"olympiacos", "panathinaikos", "aek athens",
|
||||
# Arjantin
|
||||
"boca juniors", "river plate",
|
||||
# Diğer
|
||||
"celtic", "rangers", "red star belgrade"
|
||||
}
|
||||
|
||||
# Avrupa kupaları (yüksek motivasyon)
|
||||
EUROPEAN_COMPETITIONS = {
|
||||
"şampiyonlar ligi", "champions league", "uefa champions league",
|
||||
"avrupa ligi", "europa league", "uefa europa league",
|
||||
"konferans ligi", "conference league", "uefa conference league"
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._connect_db()
|
||||
|
||||
def _connect_db(self):
|
||||
"""Veritabanına bağlan"""
|
||||
if psycopg2 is None:
|
||||
return
|
||||
|
||||
try:
|
||||
from data.db import get_clean_dsn
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
except Exception as e:
|
||||
print(f"[UpsetEngine] DB connection failed: {e}")
|
||||
self.conn = None
|
||||
|
||||
def _get_conn(self):
|
||||
"""Bağlantıyı kontrol et ve döndür"""
|
||||
if self.conn is None or self.conn.closed:
|
||||
self._connect_db()
|
||||
return self.conn
|
||||
|
||||
def calculate_atmosphere_score(
|
||||
self,
|
||||
home_team_name: str,
|
||||
league_name: str,
|
||||
is_cup_match: bool = False
|
||||
) -> Tuple[float, list]:
|
||||
"""
|
||||
Atmosfer skorunu hesapla.
|
||||
Yüksek atmosferli stadyumlar upset potansiyelini artırır.
|
||||
"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
# Yüksek atmosferli takım mı?
|
||||
home_lower = home_team_name.lower()
|
||||
for team in self.HIGH_ATMOSPHERE_TEAMS:
|
||||
if team in home_lower:
|
||||
score += 0.25
|
||||
reasons.append(f"🔥 {home_team_name} yüksek atmosferli stadyum")
|
||||
break
|
||||
|
||||
# Avrupa kupası mı?
|
||||
league_lower = league_name.lower()
|
||||
for comp in self.EUROPEAN_COMPETITIONS:
|
||||
if comp in league_lower:
|
||||
score += 0.20
|
||||
reasons.append("🌟 Avrupa gecesi - ekstra motivasyon")
|
||||
break
|
||||
|
||||
# Kupa maçı mı? (tek maç eliminasyon)
|
||||
if is_cup_match:
|
||||
score += 0.10
|
||||
reasons.append("🏆 Kupa maçı - her şey olabilir")
|
||||
|
||||
return min(score, 1.0), reasons
|
||||
|
||||
def calculate_motivation_score(
|
||||
self,
|
||||
home_position: int,
|
||||
away_position: int,
|
||||
home_points_to_safety: Optional[int] = None,
|
||||
away_already_champion: bool = False,
|
||||
total_teams: int = 20
|
||||
) -> Tuple[float, list]:
|
||||
"""
|
||||
Motivasyon asimetrisini hesapla.
|
||||
Alt sıradaki takımın üst sıradakine karşı ekstra motivasyonu.
|
||||
"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
# Pozisyon farkı
|
||||
position_diff = 0
|
||||
if away_position is not None and home_position is not None:
|
||||
position_diff = away_position - home_position # Negatif = deplasman daha iyi sırada
|
||||
|
||||
# Küme düşme hattı vs üst sıra (en güçlü upset faktörü)
|
||||
relegation_zone = total_teams - 3 # Son 3 takım
|
||||
if home_position is not None and away_position is not None:
|
||||
if home_position >= relegation_zone and away_position <= 3:
|
||||
score += 0.30
|
||||
reasons.append("⚔️ Hayatta kalma savaşı vs şampiyonluk adayı")
|
||||
elif home_position >= relegation_zone:
|
||||
score += 0.15
|
||||
reasons.append("🔥 Ev sahibi küme düşme hattında - ekstra motivasyon")
|
||||
elif home_position is not None and home_position >= relegation_zone:
|
||||
score += 0.15
|
||||
reasons.append("🔥 Ev sahibi küme düşme hattında - ekstra motivasyon")
|
||||
|
||||
# Deplasman takımı zaten şampiyon mu?
|
||||
if away_already_champion:
|
||||
score += 0.20
|
||||
reasons.append("😴 Deplasman takımı zaten şampiyon - motivasyon düşük")
|
||||
|
||||
# Büyük pozisyon farkı (underdog evinde)
|
||||
if position_diff < -10:
|
||||
score += 0.15
|
||||
reasons.append(f"📊 {abs(position_diff)} sıra fark - büyük maç heyecanı")
|
||||
elif position_diff < -5:
|
||||
score += 0.08
|
||||
|
||||
return min(score, 1.0), reasons
|
||||
|
||||
def calculate_fatigue_score(
|
||||
self,
|
||||
home_matches_last_14d: int = 0,
|
||||
away_matches_last_14d: int = 0,
|
||||
home_days_rest: int = 7,
|
||||
away_days_rest: int = 7,
|
||||
away_travel_km: float = 0
|
||||
) -> Tuple[float, list]:
|
||||
"""
|
||||
Yorgunluk farkını hesapla.
|
||||
Yorgun deplasman takımı = yüksek upset potansiyeli.
|
||||
"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
# Maç yoğunluğu farkı
|
||||
match_diff = away_matches_last_14d - home_matches_last_14d
|
||||
if match_diff >= 3:
|
||||
score += 0.20
|
||||
reasons.append(f"🏃 Deplasman {match_diff} maç daha fazla oynamış")
|
||||
elif match_diff >= 2:
|
||||
score += 0.10
|
||||
|
||||
# Dinlenme süresi farkı
|
||||
rest_diff = home_days_rest - away_days_rest
|
||||
if rest_diff >= 4:
|
||||
score += 0.15
|
||||
reasons.append(f"💤 Ev sahibi {rest_diff} gün daha fazla dinlenmiş")
|
||||
elif rest_diff >= 2:
|
||||
score += 0.08
|
||||
|
||||
# Uzun deplasman
|
||||
if away_travel_km > 3000:
|
||||
score += 0.15
|
||||
reasons.append(f"✈️ Uzun deplasman ({int(away_travel_km)} km)")
|
||||
elif away_travel_km > 1500:
|
||||
score += 0.08
|
||||
|
||||
return min(score, 1.0), reasons
|
||||
|
||||
def get_historical_upset_rate(
|
||||
self,
|
||||
home_team_id: str,
|
||||
before_date_ms: int,
|
||||
lookback_matches: int = 20
|
||||
) -> Tuple[float, list]:
|
||||
"""
|
||||
Ev sahibi takımın tarihsel upset oranını hesapla.
|
||||
Üst sıradaki takımlara karşı galibiyetler.
|
||||
"""
|
||||
reasons = []
|
||||
|
||||
conn = self._get_conn()
|
||||
if conn is None:
|
||||
return 0.0, reasons
|
||||
|
||||
try:
|
||||
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Ev sahibi olarak oynadığı ve sıralamada geride olduğu maçlar
|
||||
query = """
|
||||
WITH home_matches AS (
|
||||
SELECT
|
||||
m.id,
|
||||
m.score_home,
|
||||
m.score_away,
|
||||
m.home_team_id,
|
||||
m.away_team_id
|
||||
FROM matches m
|
||||
WHERE m.home_team_id = %s
|
||||
AND m.mst_utc < %s
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
)
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN score_home > score_away THEN 1 ELSE 0 END) as wins
|
||||
FROM home_matches
|
||||
"""
|
||||
|
||||
cursor.execute(query, (home_team_id, before_date_ms, lookback_matches))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result and result['total'] > 0:
|
||||
win_rate = result['wins'] / result['total']
|
||||
# Ev sahibi kazanma oranı yüksekse, upset potansiyeli de yüksek
|
||||
if win_rate > 0.5:
|
||||
rate = min((win_rate - 0.4) * 0.5, 0.3)
|
||||
reasons.append(f"📈 Güçlü ev sahibi performansı (%{int(win_rate*100)} kazanma)")
|
||||
return rate, reasons
|
||||
|
||||
return 0.0, reasons
|
||||
|
||||
except Exception as e:
|
||||
print(f"[UpsetEngine] Historical query error: {e}")
|
||||
return 0.0, reasons
|
||||
|
||||
def calculate_upset_potential(
|
||||
self,
|
||||
home_team_name: str,
|
||||
home_team_id: str,
|
||||
away_team_name: str,
|
||||
league_name: str,
|
||||
home_position: int,
|
||||
away_position: int,
|
||||
match_date_ms: int,
|
||||
is_cup_match: bool = False,
|
||||
home_matches_last_14d: int = 2,
|
||||
away_matches_last_14d: int = 2,
|
||||
home_days_rest: int = 7,
|
||||
away_days_rest: int = 7,
|
||||
away_travel_km: float = 0,
|
||||
total_teams: int = 20
|
||||
) -> UpsetFactors:
|
||||
"""
|
||||
Tüm faktörleri birleştirerek upset potansiyelini hesapla.
|
||||
|
||||
Returns:
|
||||
UpsetFactors: Tüm faktörler ve toplam skor
|
||||
"""
|
||||
factors = UpsetFactors()
|
||||
all_reasons = []
|
||||
|
||||
# 1. Atmosfer
|
||||
atm_score, atm_reasons = self.calculate_atmosphere_score(
|
||||
home_team_name, league_name, is_cup_match
|
||||
)
|
||||
factors.atmosphere_score = atm_score
|
||||
all_reasons.extend(atm_reasons)
|
||||
|
||||
# 2. Motivasyon
|
||||
mot_score, mot_reasons = self.calculate_motivation_score(
|
||||
home_position, away_position,
|
||||
total_teams=total_teams
|
||||
)
|
||||
factors.motivation_score = mot_score
|
||||
all_reasons.extend(mot_reasons)
|
||||
|
||||
# 3. Yorgunluk
|
||||
fat_score, fat_reasons = self.calculate_fatigue_score(
|
||||
home_matches_last_14d, away_matches_last_14d,
|
||||
home_days_rest, away_days_rest,
|
||||
away_travel_km
|
||||
)
|
||||
factors.fatigue_score = fat_score
|
||||
all_reasons.extend(fat_reasons)
|
||||
|
||||
# 4. Tarihsel (sadece DB varsa)
|
||||
hist_score, hist_reasons = self.get_historical_upset_rate(
|
||||
home_team_id, match_date_ms
|
||||
)
|
||||
factors.historical_upset_rate = hist_score
|
||||
all_reasons.extend(hist_reasons)
|
||||
|
||||
# Toplam skor (weighted average)
|
||||
factors.total_upset_potential = min(
|
||||
factors.atmosphere_score * 0.25 +
|
||||
factors.motivation_score * 0.35 +
|
||||
factors.fatigue_score * 0.25 +
|
||||
factors.historical_upset_rate * 0.15,
|
||||
1.0
|
||||
)
|
||||
|
||||
factors.reasoning = all_reasons
|
||||
|
||||
return factors
|
||||
|
||||
def get_features(
|
||||
self,
|
||||
home_team_name: str,
|
||||
home_team_id: str,
|
||||
away_team_name: str,
|
||||
league_name: str,
|
||||
home_position: int,
|
||||
away_position: int,
|
||||
match_date_ms: int,
|
||||
**kwargs
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Model için feature dict döndür.
|
||||
Training ve inference'da kullanılır.
|
||||
"""
|
||||
factors = self.calculate_upset_potential(
|
||||
home_team_name=home_team_name,
|
||||
home_team_id=home_team_id,
|
||||
away_team_name=away_team_name,
|
||||
league_name=league_name,
|
||||
home_position=home_position,
|
||||
away_position=away_position,
|
||||
match_date_ms=match_date_ms,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return {
|
||||
"upset_atmosphere": factors.atmosphere_score,
|
||||
"upset_motivation": factors.motivation_score,
|
||||
"upset_fatigue": factors.fatigue_score,
|
||||
"upset_historical": factors.historical_upset_rate,
|
||||
"upset_potential": factors.total_upset_potential,
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_engine_instance = None
|
||||
|
||||
def get_upset_engine() -> UpsetEngine:
|
||||
"""Singleton pattern ile engine döndür"""
|
||||
global _engine_instance
|
||||
if _engine_instance is None:
|
||||
_engine_instance = UpsetEngine()
|
||||
return _engine_instance
|
||||
|
||||
|
||||
# Test
|
||||
if __name__ == "__main__":
|
||||
engine = get_upset_engine()
|
||||
|
||||
# Galatasaray vs Liverpool örneği
|
||||
factors = engine.calculate_upset_potential(
|
||||
home_team_name="Galatasaray",
|
||||
home_team_id="test-gs-id",
|
||||
away_team_name="Liverpool",
|
||||
league_name="UEFA Champions League",
|
||||
home_position=12,
|
||||
away_position=1,
|
||||
match_date_ms=1700000000000,
|
||||
is_cup_match=False,
|
||||
away_matches_last_14d=5,
|
||||
home_matches_last_14d=2,
|
||||
away_days_rest=3,
|
||||
home_days_rest=7,
|
||||
away_travel_km=2800,
|
||||
total_teams=20
|
||||
)
|
||||
|
||||
print("=" * 60)
|
||||
print("GALATASARAY vs LIVERPOOL - UPSET ANALİZİ")
|
||||
print("=" * 60)
|
||||
print(f"🏟️ Atmosfer Skoru: {factors.atmosphere_score:.2f}")
|
||||
print(f"💪 Motivasyon Skoru: {factors.motivation_score:.2f}")
|
||||
print(f"😓 Yorgunluk Skoru: {factors.fatigue_score:.2f}")
|
||||
print(f"📊 Tarihsel Skor: {factors.historical_upset_rate:.2f}")
|
||||
print(f"\n🎯 TOPLAM UPSET POTANSİYELİ: {factors.total_upset_potential:.2f}")
|
||||
print("\n📝 Sebepler:")
|
||||
for reason in factors.reasoning:
|
||||
print(f" {reason}")
|
||||
@@ -0,0 +1,507 @@
|
||||
"""
|
||||
Upset Engine v2 - GLM-5 Tespitleri ile Geliştirilmiş Sürpriz Tespiti
|
||||
====================================================================
|
||||
|
||||
Yeni Eklenen Faktörler (GLM-5 Analizinden):
|
||||
1. MARGIN_ANALIZI - Bookmaker margin > %18 = sürpriz riski
|
||||
2. FAVORI_ORAN_TUZAGI - 1.40-1.60 arası en yüksek sürpriz oranı
|
||||
3. HAKEM_SURPRIZ_ORANI - Hakemin geçmiş maçlarında ev kayıp oranı
|
||||
4. FORM_FARKI_TUZAGI - Form farkı > 40 = "çok iyi görünen" favori tuzak
|
||||
|
||||
Orijinal Faktörler:
|
||||
- Atmosfer (Avrupa gecesi, taraftar baskısı)
|
||||
- Motivasyon asimetrisi (küme düşme vs şampiyon)
|
||||
- Yorgunluk (maç yoğunluğu, seyahat)
|
||||
- Tarihsel upset pattern
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, Tuple, List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpsetFactorsV2:
|
||||
"""Upset potansiyelini etkileyen faktörler - v2"""
|
||||
# Orijinal faktörler
|
||||
atmosphere_score: float = 0.0
|
||||
motivation_score: float = 0.0
|
||||
fatigue_score: float = 0.0
|
||||
historical_upset_rate: float = 0.0
|
||||
|
||||
# YENİ FAKTÖRLER (GLM-5)
|
||||
margin_score: float = 0.0 # Bookmaker margin analizi
|
||||
favorite_odds_trap: float = 0.0 # Favori oran tuzağı
|
||||
referee_upset_score: float = 0.0 # Hakem sürpriz oranı
|
||||
form_trap_score: float = 0.0 # Form farkı tuzağı
|
||||
|
||||
# Toplam
|
||||
total_upset_potential: float = 0.0
|
||||
reasoning: List[str] = field(default_factory=list)
|
||||
|
||||
# YENİ: Sürpriz skoru (0-100)
|
||||
upset_score: int = 0
|
||||
upset_level: str = "LOW" # LOW, MEDIUM, HIGH, EXTREME
|
||||
|
||||
|
||||
class UpsetEngineV2:
|
||||
"""
|
||||
Favori takımın kaybedeceği maçları tespit eder.
|
||||
v2: GLM-5 analizlerinden elde edilen yeni faktörler eklendi.
|
||||
"""
|
||||
|
||||
# Yüksek atmosferli stadyumlar
|
||||
HIGH_ATMOSPHERE_TEAMS = {
|
||||
"galatasaray", "fenerbahce", "besiktas", "trabzonspor",
|
||||
"liverpool", "newcastle", "leeds",
|
||||
"dortmund", "union berlin",
|
||||
"olympiacos", "panathinaikos", "aek athens",
|
||||
"boca juniors", "river plate",
|
||||
"celtic", "rangers", "red star belgrade"
|
||||
}
|
||||
|
||||
EUROPEAN_COMPETITIONS = {
|
||||
"şampiyonlar ligi", "champions league", "uefa champions league",
|
||||
"avrupa ligi", "europa league", "uefa europa league",
|
||||
"konferans ligi", "conference league", "uefa conference league"
|
||||
}
|
||||
|
||||
# YENİ: Sürpriz oranları (veritabanı analizinden)
|
||||
# Favori oran aralığına göre sürpriz oranları
|
||||
FAVORITE_ODDS_UPSET_RATES = {
|
||||
(1.10, 1.20): 0.111, # %11.1 sürpriz
|
||||
(1.20, 1.30): 0.150, # %15.0 sürpriz
|
||||
(1.30, 1.40): 0.235, # %23.5 sürpriz
|
||||
(1.40, 1.50): 0.333, # %33.3 sürpriz ← DİKKAT!
|
||||
(1.50, 1.60): 0.350, # %35.0 sürpriz ← EN YÜKSEK!
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._connect_db()
|
||||
|
||||
def _connect_db(self):
|
||||
if psycopg2 is None:
|
||||
return
|
||||
try:
|
||||
from data.db import get_clean_dsn
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
except Exception as e:
|
||||
print(f"[UpsetEngineV2] DB connection failed: {e}")
|
||||
self.conn = None
|
||||
|
||||
def _get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
self._connect_db()
|
||||
return self.conn
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# YENİ FAKTÖRLER (GLM-5 Analizinden)
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
|
||||
def calculate_margin_score(
|
||||
self,
|
||||
odds_data: Dict[str, float]
|
||||
) -> Tuple[float, List[str]]:
|
||||
"""
|
||||
GLM-5 Tespiti: Bookmaker margin analizi
|
||||
|
||||
Margin > %18 → Bookmaker kendini koruyor, favori riskli
|
||||
Margin > %20 → Yüksek risk, sürpriz bekleniyor
|
||||
"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
ms_h = odds_data.get("ms_h", 0)
|
||||
ms_d = odds_data.get("ms_d", 0)
|
||||
ms_a = odds_data.get("ms_a", 0)
|
||||
|
||||
if ms_h > 0 and ms_d > 0 and ms_a > 0:
|
||||
margin = (1/ms_h + 1/ms_d + 1/ms_a) - 1
|
||||
|
||||
if margin > 0.20:
|
||||
score = 0.25
|
||||
reasons.append(f"⚠️ Margin çok yüksek (%{margin*100:.1f}) - Bookmaker risk görüyor!")
|
||||
elif margin > 0.18:
|
||||
score = 0.15
|
||||
reasons.append(f"⚠️ Margin yüksek (%{margin*100:.1f}) - Dikkat!")
|
||||
|
||||
return score, reasons
|
||||
|
||||
def calculate_favorite_odds_trap(
|
||||
self,
|
||||
favorite_odds: float,
|
||||
favorite_side: str # 'home' or 'away'
|
||||
) -> Tuple[float, List[str]]:
|
||||
"""
|
||||
GLM-5 Tespiti: Favori oran tuzağı
|
||||
|
||||
Veritabanı analizine göre:
|
||||
- 1.40-1.50 arası: %33.3 sürpriz
|
||||
- 1.50-1.60 arası: %35.0 sürpriz (EN YÜKSEK!)
|
||||
- < 1.20: Tuzak oranı şüphesi
|
||||
"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
if favorite_odds <= 0:
|
||||
return score, reasons
|
||||
|
||||
for (low, high), upset_rate in self.FAVORITE_ODDS_UPSET_RATES.items():
|
||||
if low <= favorite_odds < high:
|
||||
score = upset_rate # Doğrudan sürpriz olasılığı
|
||||
if upset_rate >= 0.30:
|
||||
reasons.append(f"🔴 Favori oran {favorite_odds:.2f} - %{upset_rate*100:.0f} sürpriz oranı!")
|
||||
elif upset_rate >= 0.20:
|
||||
reasons.append(f"⚠️ Favori oran {favorite_odds:.2f} - %{upset_rate*100:.0f} sürpriz riski")
|
||||
break
|
||||
|
||||
# Çok düşük oran tuzağı
|
||||
if favorite_odds < 1.20:
|
||||
score = max(score, 0.20)
|
||||
reasons.append(f"⚠️ Favori oran çok düşük ({favorite_odds:.2f}) - Tuzak oranı şüphesi")
|
||||
|
||||
return score, reasons
|
||||
|
||||
def calculate_referee_upset_score(
|
||||
self,
|
||||
referee_name: str
|
||||
) -> Tuple[float, List[str]]:
|
||||
"""
|
||||
GLM-5 Tespiti: Hakem sürpriz oranı
|
||||
|
||||
Hakemin yönettiği maçlarda ev sahibi kayıp oranı
|
||||
> %25 → Yüksek sürpriz riski
|
||||
"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
if not referee_name or not self._get_conn():
|
||||
return score, reasons
|
||||
|
||||
try:
|
||||
cur = self._get_conn().cursor()
|
||||
|
||||
# Hakemin yönettiği maçlarda sonuçlar
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN m.score_home < m.score_away THEN 1 ELSE 0 END) as away_wins,
|
||||
SUM(CASE WHEN m.score_home = m.score_away THEN 1 ELSE 0 END) as draws
|
||||
FROM match_officials mo
|
||||
JOIN matches m ON m.id = mo.match_id
|
||||
WHERE mo.name = %s AND mo.role_id = 1
|
||||
AND m.score_home IS NOT NULL
|
||||
""", (referee_name,))
|
||||
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
if row and row[0] and row[0] >= 3:
|
||||
total = row[0]
|
||||
away_wins = row[1] or 0
|
||||
draws = row[2] or 0
|
||||
|
||||
upset_rate = (away_wins + draws * 0.5) / total
|
||||
|
||||
if upset_rate > 0.40:
|
||||
score = 0.25
|
||||
reasons.append(f"👨⚖️ {referee_name}: %{upset_rate*100:.0f} sürpriz oranı (YÜKSEK!)")
|
||||
elif upset_rate > 0.30:
|
||||
score = 0.15
|
||||
reasons.append(f"👨⚖️ {referee_name}: %{upset_rate*100:.0f} sürpriz oranı")
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return score, reasons
|
||||
|
||||
def calculate_form_trap_score(
|
||||
self,
|
||||
home_form_score: float,
|
||||
away_form_score: float,
|
||||
favorite_side: str
|
||||
) -> Tuple[float, List[str]]:
|
||||
"""
|
||||
GLM-5 Tespiti: Form farkı tuzağı
|
||||
|
||||
Form farkı > 40 → "Çok iyi görünen" favori tuzak
|
||||
Favori formu kötü ama oran düşük → Sürpriz bekleniyor
|
||||
"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
form_diff = home_form_score - away_form_score
|
||||
|
||||
# Form farkı çok büyük
|
||||
if abs(form_diff) > 40:
|
||||
score = 0.20
|
||||
if form_diff > 0 and favorite_side == 'away':
|
||||
reasons.append(f"🔴 Form tuzağı! Ev sahibi formda ({home_form_score:.0f}) ama deplasman favori")
|
||||
elif form_diff < 0 and favorite_side == 'home':
|
||||
reasons.append(f"🔴 Form tuzağı! Deplasman formda ({away_form_score:.0f}) ama ev sahibi favori")
|
||||
|
||||
# Favori formu kötü
|
||||
if favorite_side == 'home' and home_form_score < 50:
|
||||
score = max(score, 0.15)
|
||||
reasons.append(f"⚠️ Favori ev sahibi formu düşük ({home_form_score:.0f})")
|
||||
elif favorite_side == 'away' and away_form_score < 50:
|
||||
score = max(score, 0.15)
|
||||
reasons.append(f"⚠️ Favori deplasman formu düşük ({away_form_score:.0f})")
|
||||
|
||||
return score, reasons
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# ORİJİNAL FAKTÖRLER
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
|
||||
def calculate_atmosphere_score(
|
||||
self,
|
||||
home_team_name: str,
|
||||
league_name: str,
|
||||
is_cup_match: bool = False
|
||||
) -> Tuple[float, List[str]]:
|
||||
"""Orijinal: Atmosfer skoru"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
home_lower = home_team_name.lower()
|
||||
for team in self.HIGH_ATMOSPHERE_TEAMS:
|
||||
if team in home_lower:
|
||||
score += 0.25
|
||||
reasons.append(f"🔥 {home_team_name} yüksek atmosferli stadyum")
|
||||
break
|
||||
|
||||
league_lower = league_name.lower()
|
||||
for comp in self.EUROPEAN_COMPETITIONS:
|
||||
if comp in league_lower:
|
||||
score += 0.20
|
||||
reasons.append("🌟 Avrupa gecesi - ekstra motivasyon")
|
||||
break
|
||||
|
||||
if is_cup_match:
|
||||
score += 0.10
|
||||
reasons.append("🏆 Kupa maçı - her şey olabilir")
|
||||
|
||||
return min(score, 1.0), reasons
|
||||
|
||||
def calculate_motivation_score(
|
||||
self,
|
||||
home_position: int,
|
||||
away_position: int,
|
||||
total_teams: int = 20
|
||||
) -> Tuple[float, List[str]]:
|
||||
"""Orijinal: Motivasyon asimetrisi"""
|
||||
score = 0.0
|
||||
reasons = []
|
||||
|
||||
if home_position is not None and away_position is not None:
|
||||
position_diff = away_position - home_position
|
||||
relegation_zone = total_teams - 3
|
||||
|
||||
if home_position >= relegation_zone and away_position <= 3:
|
||||
score += 0.30
|
||||
reasons.append("⚔️ Hayatta kalma savaşı vs şampiyonluk adayı")
|
||||
elif home_position >= relegation_zone:
|
||||
score += 0.15
|
||||
reasons.append("🔥 Ev sahibi küme düşme hattında")
|
||||
|
||||
if position_diff < -10:
|
||||
score += 0.15
|
||||
reasons.append(f"📊 {abs(position_diff)} sıra fark")
|
||||
|
||||
return min(score, 1.0), reasons
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# ANA FONKSİYON
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
|
||||
def calculate_upset_potential(
|
||||
self,
|
||||
home_team_name: str,
|
||||
home_team_id: str,
|
||||
away_team_name: str,
|
||||
league_name: str,
|
||||
home_position: int = None,
|
||||
away_position: int = None,
|
||||
match_date_ms: int = None,
|
||||
odds_data: Dict[str, float] = None,
|
||||
referee_name: str = None,
|
||||
home_form_score: float = 50.0,
|
||||
away_form_score: float = 50.0,
|
||||
favorite_side: str = None, # 'home', 'away', or 'draw'
|
||||
favorite_odds: float = None
|
||||
) -> UpsetFactorsV2:
|
||||
"""
|
||||
Tam upset analizi - v2 (GLM-5 geliştirmeleri ile)
|
||||
"""
|
||||
factors = UpsetFactorsV2()
|
||||
all_reasons = []
|
||||
|
||||
# 1. Margin analizi (YENİ)
|
||||
if odds_data:
|
||||
factors.margin_score, reasons = self.calculate_margin_score(odds_data)
|
||||
all_reasons.extend(reasons)
|
||||
|
||||
# 2. Favori oran tuzağı (YENİ)
|
||||
if favorite_odds and favorite_side:
|
||||
factors.favorite_odds_trap, reasons = self.calculate_favorite_odds_trap(
|
||||
favorite_odds, favorite_side
|
||||
)
|
||||
all_reasons.extend(reasons)
|
||||
|
||||
# 3. Hakem sürpriz oranı (YENİ)
|
||||
if referee_name:
|
||||
factors.referee_upset_score, reasons = self.calculate_referee_upset_score(
|
||||
referee_name
|
||||
)
|
||||
all_reasons.extend(reasons)
|
||||
|
||||
# 4. Form tuzağı (YENİ)
|
||||
factors.form_trap_score, reasons = self.calculate_form_trap_score(
|
||||
home_form_score, away_form_score, favorite_side or 'home'
|
||||
)
|
||||
all_reasons.extend(reasons)
|
||||
|
||||
# 5. Atmosfer (orijinal)
|
||||
factors.atmosphere_score, reasons = self.calculate_atmosphere_score(
|
||||
home_team_name, league_name
|
||||
)
|
||||
all_reasons.extend(reasons)
|
||||
|
||||
# 6. Motivasyon (orijinal)
|
||||
if home_position is not None and away_position is not None:
|
||||
factors.motivation_score, reasons = self.calculate_motivation_score(
|
||||
home_position, away_position
|
||||
)
|
||||
all_reasons.extend(reasons)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# SÜRPRİZ SKORU HESAPLAMA (0-100) - GÜÇLENDİRİLMİŞ v2.1
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
upset_score = 0
|
||||
|
||||
# Margin (> %18 = +20, > %20 = +30) - GÜÇLENDİRİLDİ
|
||||
if factors.margin_score >= 0.25:
|
||||
upset_score += 30 # Artırıldı: 20 -> 30
|
||||
all_reasons.append("🔴 Margin > %20: Bookmaker büyük risk görüyor!")
|
||||
elif factors.margin_score >= 0.15:
|
||||
upset_score += 20 # Artırıldı: 15 -> 20
|
||||
all_reasons.append("⚠️ Margin > %18: Dikkatli ol!")
|
||||
|
||||
# Favori oran tuzağı - GÜÇLENDİRİLDİ
|
||||
if factors.favorite_odds_trap >= 0.30:
|
||||
upset_score += 30 # Artırıldı: 25 -> 30
|
||||
elif factors.favorite_odds_trap >= 0.20:
|
||||
upset_score += 25 # Artırıldı: 20 -> 25
|
||||
elif factors.favorite_odds_trap >= 0.15:
|
||||
upset_score += 20 # Artırıldı: 15 -> 20
|
||||
|
||||
# Hakem
|
||||
if factors.referee_upset_score >= 0.25:
|
||||
upset_score += 20
|
||||
elif factors.referee_upset_score >= 0.15:
|
||||
upset_score += 10
|
||||
|
||||
# Form tuzağı - GÜÇLENDİRİLDİ
|
||||
if factors.form_trap_score >= 0.20:
|
||||
upset_score += 20 # Artırıldı: 15 -> 20
|
||||
elif factors.form_trap_score >= 0.15:
|
||||
upset_score += 15 # Artırıldı: 10 -> 15
|
||||
|
||||
# Atmosfer - GÜÇLENDİRİLDİ
|
||||
if factors.atmosphere_score >= 0.40:
|
||||
upset_score += 20 # Artırıldı: 15 -> 20
|
||||
elif factors.atmosphere_score >= 0.25:
|
||||
upset_score += 15 # Artırıldı: 10 -> 15
|
||||
|
||||
# Motivasyon
|
||||
if factors.motivation_score >= 0.30:
|
||||
upset_score += 15
|
||||
elif factors.motivation_score >= 0.15:
|
||||
upset_score += 10
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# YENİ: EKSTRA RİSK FAKTÖRLERİ
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
# Deplasman favorisi ekstra risk (+10)
|
||||
if favorite_side == 'away':
|
||||
upset_score += 10
|
||||
all_reasons.append("📍 Deplasman favorisi - ekstra risk!")
|
||||
|
||||
# Favori formu çok düşük (< 40) = +15
|
||||
if favorite_side == 'home' and home_form_score < 40:
|
||||
upset_score += 15
|
||||
all_reasons.append(f"🔴 Favori ev sahibi formu ÇOK DÜŞÜK ({home_form_score:.0f})")
|
||||
elif favorite_side == 'away' and away_form_score < 40:
|
||||
upset_score += 15
|
||||
all_reasons.append(f"🔴 Favori deplasman formu ÇOK DÜŞÜK ({away_form_score:.0f})")
|
||||
|
||||
# Çok düşük favori oranı (< 1.30) ama margin yüksek = tuzak şüphesi
|
||||
if favorite_odds and favorite_odds < 1.30 and factors.margin_score >= 0.15:
|
||||
upset_score += 10
|
||||
all_reasons.append(f"⚠️ Düşük oran ({favorite_odds:.2f}) + yüksek margin = TUZAK ŞÜPHESİ!")
|
||||
|
||||
factors.upset_score = min(upset_score, 100)
|
||||
|
||||
# Seviye belirle
|
||||
if factors.upset_score >= 60:
|
||||
factors.upset_level = "EXTREME"
|
||||
elif factors.upset_score >= 45:
|
||||
factors.upset_level = "HIGH"
|
||||
elif factors.upset_score >= 30:
|
||||
factors.upset_level = "MEDIUM"
|
||||
else:
|
||||
factors.upset_level = "LOW"
|
||||
|
||||
# Toplam upset potansiyeli
|
||||
factors.total_upset_potential = min(
|
||||
(factors.margin_score + factors.favorite_odds_trap +
|
||||
factors.referee_upset_score + factors.form_trap_score +
|
||||
factors.atmosphere_score * 0.5 + factors.motivation_score * 0.5) / 1.5,
|
||||
1.0
|
||||
)
|
||||
|
||||
factors.reasoning = all_reasons
|
||||
|
||||
return factors
|
||||
|
||||
|
||||
def get_upset_engine_v2():
|
||||
"""Singleton pattern"""
|
||||
return UpsetEngineV2()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test
|
||||
engine = get_upset_engine_v2()
|
||||
|
||||
# Real Madrid vs Getafe test
|
||||
result = engine.calculate_upset_potential(
|
||||
home_team_name="Real Madrid",
|
||||
home_team_id="test",
|
||||
away_team_name="Getafe",
|
||||
league_name="LaLiga",
|
||||
odds_data={"ms_h": 1.25, "ms_d": 3.92, "ms_a": 6.86},
|
||||
referee_name="A. Muniz Ruiz",
|
||||
home_form_score=80.0,
|
||||
away_form_score=56.7,
|
||||
favorite_side="home",
|
||||
favorite_odds=1.25
|
||||
)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Real Madrid vs Getafe - Sürpriz Analizi")
|
||||
print(f"{'='*60}")
|
||||
print(f"Sürpriz Skoru: {result.upset_score}/100")
|
||||
print(f"Seviye: {result.upset_level}")
|
||||
print(f"\nNedenler:")
|
||||
for reason in result.reasoning:
|
||||
print(f" {reason}")
|
||||
Executable
+249
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
Value Betting Calculator
|
||||
Expected Value (EV) ve stake önerileri hesaplar.
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValueBet:
|
||||
"""Value bet analiz sonucu"""
|
||||
bet_type: str # MS_1, AU25_Üst, KG_Var
|
||||
my_probability: float # Bizim tahminimiz
|
||||
market_odds: float # Bahis oranı
|
||||
implied_probability: float # Oranın ima ettiği olasılık
|
||||
edge: float # Fark (benim tahmin - implied)
|
||||
expected_value: float # EV = (prob × odds) - 1
|
||||
is_value: bool # EV > threshold mı?
|
||||
kelly_fraction: float # Kelly stake oranı
|
||||
confidence_tier: str # "banker", "strong", "value", "skip"
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return {
|
||||
'bet_type': self.bet_type,
|
||||
'my_probability': round(self.my_probability, 4),
|
||||
'market_odds': self.market_odds,
|
||||
'implied_probability': round(self.implied_probability, 4),
|
||||
'edge': round(self.edge, 4),
|
||||
'expected_value': round(self.expected_value, 4),
|
||||
'is_value': self.is_value,
|
||||
'kelly_fraction': round(self.kelly_fraction, 4),
|
||||
'confidence_tier': self.confidence_tier,
|
||||
}
|
||||
|
||||
|
||||
class ValueCalculator:
|
||||
"""
|
||||
Value Betting Calculator
|
||||
|
||||
Tahminleri oranlarla karşılaştırarak EV hesaplar.
|
||||
"""
|
||||
|
||||
# Eşikler
|
||||
MIN_EDGE_FOR_VALUE = 0.05 # Minimum %5 edge
|
||||
MIN_EDGE_FOR_STRONG = 0.10 # %10+ edge = strong value
|
||||
MIN_EDGE_FOR_BANKER = 0.15 # %15+ edge = banker
|
||||
|
||||
KELLY_FRACTION = 0.25 # 1/4 Kelly (güvenli)
|
||||
MAX_STAKE_PERCENT = 0.10 # Maksimum bank'ın %10'u
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def calculate_implied_probability(self, odds: float) -> float:
|
||||
"""Bahis oranından implied probability hesapla"""
|
||||
if odds <= 1:
|
||||
return 1.0
|
||||
return 1 / odds
|
||||
|
||||
def calculate_ev(self, probability: float, odds: float) -> float:
|
||||
"""
|
||||
Expected Value hesapla.
|
||||
|
||||
EV = (Probability × Odds) - 1
|
||||
|
||||
Pozitif EV = uzun vadede kar
|
||||
Negatif EV = uzun vadede zarar
|
||||
"""
|
||||
return (probability * odds) - 1
|
||||
|
||||
def calculate_kelly_stake(self, probability: float, odds: float) -> float:
|
||||
"""
|
||||
Kelly Criterion stake hesapla.
|
||||
|
||||
Kelly = (p × b - q) / b
|
||||
Burada:
|
||||
- p = kazanma olasılığı
|
||||
- q = kaybetme olasılığı (1 - p)
|
||||
- b = odds - 1 (net kar)
|
||||
"""
|
||||
if odds <= 1:
|
||||
return 0
|
||||
|
||||
b = odds - 1
|
||||
p = probability
|
||||
q = 1 - p
|
||||
|
||||
kelly = (p * b - q) / b
|
||||
|
||||
# Negatif veya çok yüksek değerleri sınırla
|
||||
kelly = max(0, min(kelly, self.MAX_STAKE_PERCENT))
|
||||
|
||||
# Fractional Kelly (daha güvenli)
|
||||
return kelly * self.KELLY_FRACTION
|
||||
|
||||
def analyze_bet(self, bet_type: str, my_probability: float,
|
||||
market_odds: float) -> ValueBet:
|
||||
"""
|
||||
Tek bir bahis için value analizi yap.
|
||||
|
||||
Args:
|
||||
bet_type: Bahis türü (MS_1, AU25_Üst, KG_Var vb.)
|
||||
my_probability: Bizim tahminimiz (0-1 arası)
|
||||
market_odds: Bahis oranı
|
||||
|
||||
Returns:
|
||||
ValueBet: Analiz sonucu
|
||||
"""
|
||||
if market_odds <= 1:
|
||||
return ValueBet(
|
||||
bet_type=bet_type,
|
||||
my_probability=my_probability,
|
||||
market_odds=market_odds,
|
||||
implied_probability=1.0,
|
||||
edge=0,
|
||||
expected_value=-1,
|
||||
is_value=False,
|
||||
kelly_fraction=0,
|
||||
confidence_tier="skip"
|
||||
)
|
||||
|
||||
implied = self.calculate_implied_probability(market_odds)
|
||||
edge = my_probability - implied
|
||||
ev = self.calculate_ev(my_probability, market_odds)
|
||||
kelly = self.calculate_kelly_stake(my_probability, market_odds)
|
||||
|
||||
# Tier belirleme
|
||||
if edge >= self.MIN_EDGE_FOR_BANKER and my_probability >= 0.70:
|
||||
tier = "banker"
|
||||
elif edge >= self.MIN_EDGE_FOR_STRONG:
|
||||
tier = "strong"
|
||||
elif edge >= self.MIN_EDGE_FOR_VALUE:
|
||||
tier = "value"
|
||||
else:
|
||||
tier = "skip"
|
||||
|
||||
return ValueBet(
|
||||
bet_type=bet_type,
|
||||
my_probability=my_probability,
|
||||
market_odds=market_odds,
|
||||
implied_probability=implied,
|
||||
edge=edge,
|
||||
expected_value=ev,
|
||||
is_value=edge >= self.MIN_EDGE_FOR_VALUE,
|
||||
kelly_fraction=kelly,
|
||||
confidence_tier=tier
|
||||
)
|
||||
|
||||
def analyze_match_predictions(self, predictions: Dict[str, float],
|
||||
odds: Dict[str, float]) -> Dict[str, ValueBet]:
|
||||
"""
|
||||
Maç için tüm tahminleri analiz et.
|
||||
|
||||
Args:
|
||||
predictions: Tahminler {'MS_1': 0.55, 'MS_X': 0.25, ...}
|
||||
odds: Oranlar {'MS_1': 1.80, 'MS_X': 3.50, ...}
|
||||
|
||||
Returns:
|
||||
Dict[str, ValueBet]: Her bahis için value analizi
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for bet_type, probability in predictions.items():
|
||||
if bet_type in odds and odds[bet_type] > 1:
|
||||
results[bet_type] = self.analyze_bet(
|
||||
bet_type=bet_type,
|
||||
my_probability=probability,
|
||||
market_odds=odds[bet_type]
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def get_best_value_bets(self, value_bets: Dict[str, ValueBet],
|
||||
top_n: int = 3) -> list:
|
||||
"""En iyi value bet'leri döndür"""
|
||||
valid_bets = [vb for vb in value_bets.values() if vb.is_value]
|
||||
sorted_bets = sorted(valid_bets, key=lambda x: x.expected_value, reverse=True)
|
||||
return sorted_bets[:top_n]
|
||||
|
||||
def calculate_stake(self, value_bet: ValueBet, bankroll: float,
|
||||
use_kelly: bool = True) -> float:
|
||||
"""
|
||||
Önerilen stake miktarını hesapla.
|
||||
|
||||
Args:
|
||||
value_bet: Value bet analizi
|
||||
bankroll: Toplam bütçe
|
||||
use_kelly: Kelly criterion kullan mı?
|
||||
|
||||
Returns:
|
||||
float: Önerilen stake miktarı
|
||||
"""
|
||||
if not value_bet.is_value:
|
||||
return 0
|
||||
|
||||
if use_kelly:
|
||||
return bankroll * value_bet.kelly_fraction
|
||||
else:
|
||||
# Tier bazlı sabit stake
|
||||
tier_stakes = {
|
||||
"banker": 0.05,
|
||||
"strong": 0.03,
|
||||
"value": 0.02,
|
||||
"skip": 0
|
||||
}
|
||||
return bankroll * tier_stakes.get(value_bet.confidence_tier, 0)
|
||||
|
||||
|
||||
# Singleton
|
||||
_calculator = None
|
||||
|
||||
def get_value_calculator() -> ValueCalculator:
|
||||
global _calculator
|
||||
if _calculator is None:
|
||||
_calculator = ValueCalculator()
|
||||
return _calculator
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
calc = get_value_calculator()
|
||||
|
||||
print("\n🧪 Value Calculator Test")
|
||||
print("=" * 50)
|
||||
|
||||
# Test senaryoları
|
||||
test_cases = [
|
||||
{"bet": "MS_1", "prob": 0.70, "odds": 1.60}, # High prob, low odds
|
||||
{"bet": "MS_1", "prob": 0.55, "odds": 1.90}, # Medium prob, good odds
|
||||
{"bet": "MS_1", "prob": 0.60, "odds": 2.10}, # VALUE!
|
||||
{"bet": "AU25_Üst", "prob": 0.65, "odds": 1.85}, # VALUE!
|
||||
{"bet": "KG_Var", "prob": 0.50, "odds": 1.70}, # No value
|
||||
]
|
||||
|
||||
for tc in test_cases:
|
||||
result = calc.analyze_bet(tc["bet"], tc["prob"], tc["odds"])
|
||||
|
||||
status_emoji = "✅" if result.is_value else "❌"
|
||||
tier_emoji = {"banker": "🎯", "strong": "💪", "value": "✓", "skip": "⏭️"}
|
||||
|
||||
print(f"\n{status_emoji} {tc['bet']}")
|
||||
print(f" Tahmin: {tc['prob']:.0%} | Oran: {tc['odds']:.2f} | Implied: {result.implied_probability:.0%}")
|
||||
print(f" Edge: {result.edge:+.1%} | EV: {result.expected_value:+.1%}")
|
||||
print(f" Tier: {tier_emoji.get(result.confidence_tier, '')} {result.confidence_tier.upper()}")
|
||||
print(f" Kelly Stake: {result.kelly_fraction:.2%} of bankroll")
|
||||
|
||||
if result.is_value:
|
||||
stake = calc.calculate_stake(result, 1000)
|
||||
print(f" 💰 Önerilen Stake (1000 TL bank): {stake:.2f} TL")
|
||||
@@ -0,0 +1,415 @@
|
||||
"""
|
||||
Value Detection Engine
|
||||
======================
|
||||
The Smart Way to Beat the Bookmakers
|
||||
|
||||
This engine doesn't just predict winners - it finds VALUE.
|
||||
The key insight: We don't need to predict the winner, we need to find
|
||||
where the bookmaker made a mistake in their odds.
|
||||
|
||||
Core Philosophy:
|
||||
- High Margin = High Uncertainty = Potential Value
|
||||
- Model Probability > Implied Probability = Value Bet
|
||||
- The goal is NOT to predict correctly, but to find +EV bets
|
||||
|
||||
Author: AI Engine V21
|
||||
"""
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValueBet:
|
||||
"""Represents a value bet opportunity"""
|
||||
outcome: str # "1", "X", "2"
|
||||
model_probability: float # Our model's probability (0-1)
|
||||
implied_probability: float # Bookmaker's implied probability (0-1)
|
||||
odds: float # Bookmaker's odds
|
||||
edge: float # model_prob - implied_prob (as percentage)
|
||||
expected_value: float # EV = (prob * odds) - 1
|
||||
kelly_fraction: float # Optimal bet size
|
||||
confidence: str # "HIGH", "MEDIUM", "LOW"
|
||||
reasons: List[str] # Why this is value
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"outcome": self.outcome,
|
||||
"model_prob": round(self.model_probability * 100, 1),
|
||||
"implied_prob": round(self.implied_probability * 100, 1),
|
||||
"odds": self.odds,
|
||||
"edge": round(self.edge * 100, 1),
|
||||
"ev": round(self.expected_value * 100, 1),
|
||||
"kelly": round(self.kelly_fraction * 100, 1),
|
||||
"confidence": self.confidence,
|
||||
"reasons": self.reasons
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarginAnalysis:
|
||||
"""Analysis of bookmaker margin"""
|
||||
raw_margin: float # Sum of raw implied probabilities - 1
|
||||
true_margin: float # Adjusted for favorite-longshot bias
|
||||
favorite_outcome: str
|
||||
favorite_odds: float
|
||||
uncertainty_level: str # "LOW", "MEDIUM", "HIGH", "EXTREME"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"raw_margin": round(self.raw_margin * 100, 1),
|
||||
"true_margin": round(self.true_margin * 100, 1),
|
||||
"favorite": self.favorite_outcome,
|
||||
"favorite_odds": self.favorite_odds,
|
||||
"uncertainty": self.uncertainty_level
|
||||
}
|
||||
|
||||
|
||||
class ValueDetectionEngine:
|
||||
"""
|
||||
The Smart Betting Engine
|
||||
|
||||
This engine finds value bets by comparing model probabilities
|
||||
with bookmaker implied probabilities.
|
||||
|
||||
Key Insights:
|
||||
1. Margin > 18% → Bookmaker is unsure, potential value on underdog
|
||||
2. Margin > 20% → Bookmaker sees high risk, BIG potential value
|
||||
3. Favorite odds 1.40-1.60 → Highest upset rate historically
|
||||
4. Away favorites have higher upset rate than home favorites
|
||||
"""
|
||||
|
||||
# Historical upset rates by favorite odds range
|
||||
UPSET_RATES = {
|
||||
(1.00, 1.25): 0.08, # 8% upset rate
|
||||
(1.25, 1.40): 0.18, # 18% upset rate
|
||||
(1.40, 1.60): 0.33, # 33% upset rate - DANGER ZONE
|
||||
(1.60, 1.80): 0.28, # 28% upset rate
|
||||
(1.80, 2.00): 0.35, # 35% upset rate
|
||||
(2.00, 2.50): 0.42, # 42% upset rate
|
||||
(2.50, 3.00): 0.45, # 45% upset rate
|
||||
(3.00, 5.00): 0.55, # 55% upset rate
|
||||
}
|
||||
|
||||
# Margin thresholds
|
||||
MARGIN_LOW = 0.06 # 6% - bookmaker very confident
|
||||
MARGIN_MEDIUM = 0.12 # 12% - normal margin
|
||||
MARGIN_HIGH = 0.18 # 18% - bookmaker unsure
|
||||
MARGIN_EXTREME = 0.22 # 22% - bookmaker very unsure
|
||||
|
||||
def __init__(self):
|
||||
self.historical_data = [] # For learning
|
||||
self.value_threshold = 0.03 # Minimum 3% edge to consider value
|
||||
|
||||
def calculate_margin(self, odds_1: float, odds_x: float, odds_2: float) -> MarginAnalysis:
|
||||
"""
|
||||
Calculate bookmaker margin and analyze uncertainty.
|
||||
|
||||
Higher margin = More uncertainty = More potential value
|
||||
"""
|
||||
if not all([odds_1 > 1, odds_x > 1, odds_2 > 1]):
|
||||
return MarginAnalysis(0, 0, "X", 0, "UNKNOWN")
|
||||
|
||||
# Raw implied probabilities
|
||||
imp_1 = 1 / odds_1
|
||||
imp_x = 1 / odds_x
|
||||
imp_2 = 1 / odds_2
|
||||
|
||||
raw_margin = imp_1 + imp_x + imp_2 - 1
|
||||
|
||||
# Determine favorite
|
||||
if odds_1 <= odds_x and odds_1 <= odds_2:
|
||||
favorite_outcome = "1"
|
||||
favorite_odds = odds_1
|
||||
elif odds_2 <= odds_1 and odds_2 <= odds_x:
|
||||
favorite_outcome = "2"
|
||||
favorite_odds = odds_2
|
||||
else:
|
||||
favorite_outcome = "X"
|
||||
favorite_odds = odds_x
|
||||
|
||||
# Adjust for favorite-longshot bias
|
||||
# Bookmakers typically overprice longshots
|
||||
true_margin = raw_margin * 0.85 # Simplified adjustment
|
||||
|
||||
# Determine uncertainty level
|
||||
if raw_margin < self.MARGIN_LOW:
|
||||
uncertainty = "LOW"
|
||||
elif raw_margin < self.MARGIN_MEDIUM:
|
||||
uncertainty = "MEDIUM"
|
||||
elif raw_margin < self.MARGIN_HIGH:
|
||||
uncertainty = "HIGH"
|
||||
else:
|
||||
uncertainty = "EXTREME"
|
||||
|
||||
return MarginAnalysis(
|
||||
raw_margin=raw_margin,
|
||||
true_margin=true_margin,
|
||||
favorite_outcome=favorite_outcome,
|
||||
favorite_odds=favorite_odds,
|
||||
uncertainty_level=uncertainty
|
||||
)
|
||||
|
||||
def get_historical_upset_rate(self, favorite_odds: float) -> float:
|
||||
"""Get historical upset rate for given favorite odds"""
|
||||
for (low, high), rate in self.UPSET_RATES.items():
|
||||
if low <= favorite_odds < high:
|
||||
return rate
|
||||
return 0.40 # Default for very high odds
|
||||
|
||||
def calculate_edge(
|
||||
self,
|
||||
model_prob: float,
|
||||
odds: float,
|
||||
margin: float
|
||||
) -> Tuple[float, float]:
|
||||
"""
|
||||
Calculate the edge (advantage) we have over the bookmaker.
|
||||
|
||||
Returns: (edge, expected_value)
|
||||
|
||||
Edge = Model Probability - True Implied Probability
|
||||
EV = (Probability * Odds) - 1
|
||||
"""
|
||||
if odds <= 1:
|
||||
return 0, -1
|
||||
|
||||
# Raw implied probability
|
||||
implied = 1 / odds
|
||||
|
||||
# Adjust for margin (proportional adjustment)
|
||||
# This gives us the "true" implied probability
|
||||
# Assuming bookmaker spreads margin proportionally
|
||||
true_implied = implied # Simplified - could be more sophisticated
|
||||
|
||||
edge = model_prob - true_implied
|
||||
ev = (model_prob * odds) - 1
|
||||
|
||||
return edge, ev
|
||||
|
||||
def calculate_kelly_fraction(
|
||||
self,
|
||||
probability: float,
|
||||
odds: float,
|
||||
half_kelly: bool = True
|
||||
) -> float:
|
||||
"""
|
||||
Calculate optimal bet size using Kelly Criterion.
|
||||
|
||||
Kelly = (p * b - 1) / (b - 1)
|
||||
where b = odds - 1
|
||||
|
||||
We use half Kelly for safety.
|
||||
"""
|
||||
if odds <= 1:
|
||||
return 0
|
||||
|
||||
b = odds - 1
|
||||
kelly = (probability * b - 1) / b
|
||||
|
||||
# Don't bet if negative
|
||||
if kelly < 0:
|
||||
return 0
|
||||
|
||||
# Use half Kelly for safety
|
||||
if half_kelly:
|
||||
kelly = kelly / 2
|
||||
|
||||
# Cap at 10% of bankroll
|
||||
return min(kelly, 0.10)
|
||||
|
||||
def find_value_bets(
|
||||
self,
|
||||
model_probs: Dict[str, float],
|
||||
odds: Dict[str, float],
|
||||
match_context: Optional[Dict] = None
|
||||
) -> List[ValueBet]:
|
||||
"""
|
||||
Find all value bets in a match.
|
||||
|
||||
This is the MAIN method - it finds where we have an edge.
|
||||
|
||||
Args:
|
||||
model_probs: {"1": 0.55, "X": 0.25, "2": 0.20}
|
||||
odds: {"1": 1.25, "X": 4.50, "2": 8.00}
|
||||
match_context: Additional context (form, h2h, etc.)
|
||||
|
||||
Returns:
|
||||
List of ValueBet objects, sorted by edge
|
||||
"""
|
||||
value_bets = []
|
||||
|
||||
# Calculate margin
|
||||
margin_analysis = self.calculate_margin(
|
||||
odds.get("1", 0),
|
||||
odds.get("X", 0),
|
||||
odds.get("2", 0)
|
||||
)
|
||||
|
||||
# Analyze each outcome
|
||||
for outcome in ["1", "X", "2"]:
|
||||
prob = model_probs.get(outcome, 0)
|
||||
odd = odds.get(outcome, 0)
|
||||
|
||||
if prob <= 0 or odd <= 1:
|
||||
continue
|
||||
|
||||
edge, ev = self.calculate_edge(prob, odd, margin_analysis.raw_margin)
|
||||
kelly = self.calculate_kelly_fraction(prob, odd)
|
||||
|
||||
# Determine if this is a value bet
|
||||
reasons = []
|
||||
|
||||
# 1. Basic edge
|
||||
if edge > self.value_threshold:
|
||||
reasons.append(f"Edge: +{round(edge*100, 1)}% over bookmaker")
|
||||
|
||||
# 2. High margin bonus
|
||||
if margin_analysis.raw_margin > self.MARGIN_HIGH:
|
||||
reasons.append(f"High margin ({round(margin_analysis.raw_margin*100, 1)}%) = uncertainty")
|
||||
|
||||
# Boost edge for underdogs in high margin matches
|
||||
if outcome != margin_analysis.favorite_outcome:
|
||||
edge += 0.02 # 2% bonus
|
||||
reasons.append("Underdog in high-margin match = bonus value")
|
||||
|
||||
# 3. Favorite odds trap
|
||||
fav_odds = margin_analysis.favorite_odds
|
||||
if margin_analysis.favorite_outcome != outcome:
|
||||
upset_rate = self.get_historical_upset_rate(fav_odds)
|
||||
if upset_rate > 0.25:
|
||||
reasons.append(f"Favorite odds {fav_odds} has {round(upset_rate*100)}% upset rate")
|
||||
|
||||
# Extra bonus for 1.40-1.60 range
|
||||
if 1.40 <= fav_odds <= 1.60:
|
||||
edge += 0.03
|
||||
reasons.append("DANGER ZONE: 1.40-1.60 odds = highest upset risk")
|
||||
|
||||
# 4. Away favorite risk
|
||||
if margin_analysis.favorite_outcome == "2" and outcome == "1":
|
||||
edge += 0.015
|
||||
reasons.append("Away favorite = extra home value")
|
||||
|
||||
# 5. EV positive
|
||||
if ev > 0:
|
||||
reasons.append(f"Positive EV: +{round(ev*100, 1)}%")
|
||||
|
||||
# Only add if we have reasons (value detected)
|
||||
if reasons and edge > 0:
|
||||
# Determine confidence
|
||||
if edge > 0.08 or (edge > 0.05 and kelly > 0.03):
|
||||
confidence = "HIGH"
|
||||
elif edge > 0.05:
|
||||
confidence = "MEDIUM"
|
||||
else:
|
||||
confidence = "LOW"
|
||||
|
||||
value_bets.append(ValueBet(
|
||||
outcome=outcome,
|
||||
model_probability=prob,
|
||||
implied_probability=1/odd,
|
||||
odds=odd,
|
||||
edge=edge,
|
||||
expected_value=ev,
|
||||
kelly_fraction=kelly,
|
||||
confidence=confidence,
|
||||
reasons=reasons
|
||||
))
|
||||
|
||||
# Sort by edge (highest first)
|
||||
value_bets.sort(key=lambda x: x.edge, reverse=True)
|
||||
|
||||
return value_bets
|
||||
|
||||
def predict_with_value(
|
||||
self,
|
||||
model_probs: Dict[str, float],
|
||||
odds: Dict[str, float],
|
||||
match_context: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Make a prediction based on VALUE, not just probability.
|
||||
|
||||
This is the smart way to bet:
|
||||
- If there's clear value on one outcome → Bet it
|
||||
- If there's no value → NO BET (don't force it)
|
||||
- If margin is extreme → Look for underdog value
|
||||
|
||||
Returns:
|
||||
{
|
||||
"best_value": ValueBet or None,
|
||||
"alternative_value": ValueBet or None,
|
||||
"margin_analysis": MarginAnalysis,
|
||||
"recommendation": str,
|
||||
"confidence": str
|
||||
}
|
||||
"""
|
||||
margin_analysis = self.calculate_margin(
|
||||
odds.get("1", 0),
|
||||
odds.get("X", 0),
|
||||
odds.get("2", 0)
|
||||
)
|
||||
|
||||
value_bets = self.find_value_bets(model_probs, odds, match_context)
|
||||
|
||||
result = {
|
||||
"margin_analysis": margin_analysis.to_dict(),
|
||||
"value_bets": [vb.to_dict() for vb in value_bets],
|
||||
"best_value": None,
|
||||
"alternative_value": None,
|
||||
"recommendation": "NO_BET",
|
||||
"confidence": "LOW",
|
||||
"reasoning": []
|
||||
}
|
||||
|
||||
if not value_bets:
|
||||
result["reasoning"].append("No value detected in any outcome")
|
||||
result["reasoning"].append("Bookmaker odds are efficient for this match")
|
||||
return result
|
||||
|
||||
# Get best value bet
|
||||
best = value_bets[0]
|
||||
result["best_value"] = best.to_dict()
|
||||
|
||||
if len(value_bets) > 1:
|
||||
result["alternative_value"] = value_bets[1].to_dict()
|
||||
|
||||
# Determine recommendation
|
||||
if best.confidence == "HIGH" and best.edge > 0.05:
|
||||
result["recommendation"] = f"BET_{best.outcome}"
|
||||
result["confidence"] = "HIGH"
|
||||
result["reasoning"] = best.reasons
|
||||
result["reasoning"].append(f"Strong value on {best.outcome} with {round(best.edge*100, 1)}% edge")
|
||||
|
||||
elif best.confidence == "MEDIUM" or best.edge > 0.03:
|
||||
result["recommendation"] = f"CONSIDER_{best.outcome}"
|
||||
result["confidence"] = "MEDIUM"
|
||||
result["reasoning"] = best.reasons
|
||||
result["reasoning"].append(f"Moderate value on {best.outcome}")
|
||||
|
||||
else:
|
||||
result["recommendation"] = "NO_BET"
|
||||
result["confidence"] = "LOW"
|
||||
result["reasoning"].append("Edge too small to justify bet")
|
||||
result["reasoning"].append(f"Best edge: {round(best.edge*100, 1)}% (need >3%)")
|
||||
|
||||
# Add margin context
|
||||
if margin_analysis.uncertainty_level == "EXTREME":
|
||||
result["reasoning"].append("⚠️ EXTREME margin - high volatility match")
|
||||
elif margin_analysis.uncertainty_level == "HIGH":
|
||||
result["reasoning"].append("⚠️ High margin - bookmaker sees risk")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_engine_instance = None
|
||||
|
||||
def get_value_detection_engine() -> ValueDetectionEngine:
|
||||
"""Get the singleton instance"""
|
||||
global _engine_instance
|
||||
if _engine_instance is None:
|
||||
_engine_instance = ValueDetectionEngine()
|
||||
return _engine_instance
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Shared VQWEN feature contract
|
||||
=============================
|
||||
|
||||
One place defines how VQWEN features are produced.
|
||||
Both training and runtime inference must use this module so the model sees
|
||||
the same feature semantics in historical data and live analysis.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
FEATURE_COLUMNS = [
|
||||
"elo_diff",
|
||||
"h_xg",
|
||||
"a_xg",
|
||||
"total_xg",
|
||||
"pow_diff",
|
||||
"rest_diff",
|
||||
"h_fat",
|
||||
"a_fat",
|
||||
"imp_h",
|
||||
"imp_d",
|
||||
"imp_a",
|
||||
"h_xi",
|
||||
"a_xi",
|
||||
"h2h_h_wr",
|
||||
"form_diff",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class VqwenFeatureInput:
|
||||
home_elo: float
|
||||
away_elo: float
|
||||
home_avg_goals_scored: float
|
||||
away_avg_goals_scored: float
|
||||
home_avg_goals_conceded: float
|
||||
away_avg_goals_conceded: float
|
||||
home_avg_shots_on_target: float
|
||||
away_avg_shots_on_target: float
|
||||
home_avg_possession: float
|
||||
away_avg_possession: float
|
||||
home_rest_days: float
|
||||
away_rest_days: float
|
||||
implied_prob_home: float
|
||||
implied_prob_draw: float
|
||||
implied_prob_away: float
|
||||
home_lineup_availability: float = 1.0
|
||||
away_lineup_availability: float = 1.0
|
||||
h2h_home_win_rate: float = 0.5
|
||||
home_form_score: float = 0.0
|
||||
away_form_score: float = 0.0
|
||||
league_avg_goals: float = 2.6
|
||||
referee_avg_goals: float = 2.6
|
||||
referee_home_bias: float = 0.0
|
||||
home_squad_strength: float = 0.5
|
||||
away_squad_strength: float = 0.5
|
||||
home_key_players: float = 0.0
|
||||
away_key_players: float = 0.0
|
||||
missing_players_impact: float = 0.0
|
||||
|
||||
|
||||
def fatigue_multiplier(rest_days: float) -> float:
|
||||
if rest_days < 3.0:
|
||||
return 0.85
|
||||
if rest_days < 5.0:
|
||||
return 0.95
|
||||
return 1.0
|
||||
|
||||
|
||||
def clamp(value: float, lower: float, upper: float) -> float:
|
||||
return min(max(float(value), lower), upper)
|
||||
|
||||
|
||||
def build_vqwen_feature_row(values: VqwenFeatureInput) -> dict[str, float]:
|
||||
home_fatigue = fatigue_multiplier(values.home_rest_days)
|
||||
away_fatigue = fatigue_multiplier(values.away_rest_days)
|
||||
goal_environment = (
|
||||
float(values.league_avg_goals) + float(values.referee_avg_goals)
|
||||
) / 2.0
|
||||
goal_environment_multiplier = clamp(goal_environment / 2.6, 0.85, 1.2)
|
||||
squad_diff = float(values.home_squad_strength) - float(values.away_squad_strength)
|
||||
key_player_diff = float(values.home_key_players) - float(values.away_key_players)
|
||||
missing_penalty = clamp(float(values.missing_players_impact), 0.0, 1.0)
|
||||
referee_bias = clamp(float(values.referee_home_bias), -0.25, 0.25)
|
||||
home_squad_multiplier = clamp(
|
||||
1.0 + squad_diff * 0.08 + key_player_diff * 0.025 - missing_penalty * 0.08 + referee_bias * 0.03,
|
||||
0.82,
|
||||
1.18,
|
||||
)
|
||||
away_squad_multiplier = clamp(
|
||||
1.0 - squad_diff * 0.08 - key_player_diff * 0.025 - missing_penalty * 0.08 - referee_bias * 0.03,
|
||||
0.82,
|
||||
1.18,
|
||||
)
|
||||
|
||||
home_xg = max(
|
||||
0.05,
|
||||
(
|
||||
float(values.home_avg_goals_scored)
|
||||
+ float(values.away_avg_goals_conceded)
|
||||
)
|
||||
/ 2.0,
|
||||
) * home_fatigue * goal_environment_multiplier * home_squad_multiplier
|
||||
away_xg = max(
|
||||
0.05,
|
||||
(
|
||||
float(values.away_avg_goals_scored)
|
||||
+ float(values.home_avg_goals_conceded)
|
||||
)
|
||||
/ 2.0,
|
||||
) * away_fatigue * goal_environment_multiplier * away_squad_multiplier
|
||||
|
||||
home_power = (
|
||||
float(values.home_avg_goals_scored) * 5.0
|
||||
- float(values.home_avg_goals_conceded) * 5.0
|
||||
+ float(values.home_avg_shots_on_target) * 2.0
|
||||
+ float(values.home_avg_possession) * 0.1
|
||||
+ float(values.home_squad_strength) * 3.0
|
||||
+ float(values.home_key_players) * 0.8
|
||||
+ referee_bias * 6.0
|
||||
)
|
||||
away_power = (
|
||||
float(values.away_avg_goals_scored) * 5.0
|
||||
- float(values.away_avg_goals_conceded) * 5.0
|
||||
+ float(values.away_avg_shots_on_target) * 2.0
|
||||
+ float(values.away_avg_possession) * 0.1
|
||||
+ float(values.away_squad_strength) * 3.0
|
||||
+ float(values.away_key_players) * 0.8
|
||||
- referee_bias * 6.0
|
||||
)
|
||||
|
||||
return {
|
||||
"elo_diff": float(values.home_elo) - float(values.away_elo),
|
||||
"h_xg": home_xg,
|
||||
"a_xg": away_xg,
|
||||
"total_xg": home_xg + away_xg,
|
||||
"pow_diff": home_power - away_power,
|
||||
"rest_diff": float(values.home_rest_days) - float(values.away_rest_days),
|
||||
"h_fat": home_fatigue,
|
||||
"a_fat": away_fatigue,
|
||||
"imp_h": clamp(values.implied_prob_home, 0.01, 0.98),
|
||||
"imp_d": clamp(values.implied_prob_draw, 0.01, 0.98),
|
||||
"imp_a": clamp(values.implied_prob_away, 0.01, 0.98),
|
||||
# Column names are preserved for artifact compatibility.
|
||||
# Semantics are now "pre-match lineup availability" instead of leaked
|
||||
# post-match starting-XI counts.
|
||||
"h_xi": clamp(values.home_lineup_availability, 0.0, 1.0),
|
||||
"a_xi": clamp(values.away_lineup_availability, 0.0, 1.0),
|
||||
"h2h_h_wr": clamp(values.h2h_home_win_rate, 0.0, 1.0),
|
||||
"form_diff": (
|
||||
float(values.home_form_score)
|
||||
- float(values.away_form_score)
|
||||
+ squad_diff * 1.5
|
||||
+ key_player_diff * 0.35
|
||||
+ referee_bias * 2.0
|
||||
- missing_penalty * 1.75
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def row_to_array(row: dict[str, float]) -> np.ndarray:
|
||||
return np.array([[float(row[column]) for column in FEATURE_COLUMNS]], dtype=np.float64)
|
||||
Executable
+430
@@ -0,0 +1,430 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import subprocess
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from models.basketball_v25 import get_basketball_v25_predictor
|
||||
HAS_BASKETBALL = True
|
||||
except ImportError:
|
||||
HAS_BASKETBALL = False
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
from services.v26_shadow_engine import get_v26_shadow_engine
|
||||
from models.league_model import get_league_model_loader
|
||||
|
||||
load_dotenv()
|
||||
|
||||
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
|
||||
class CouponRequest(BaseModel):
|
||||
match_ids: list[str]
|
||||
strategy: str | None = "BALANCED"
|
||||
max_matches: int | None = None
|
||||
min_confidence: float | None = None
|
||||
|
||||
|
||||
class RetrainRequest(BaseModel):
|
||||
reason: str | None = "manual"
|
||||
markets: str | None = None # comma-separated, e.g. "MS,OU25,BTTS"
|
||||
trials: int | None = 50
|
||||
|
||||
|
||||
# ─── Retrain state tracking ──────────────────────────────────
|
||||
_retrain_state: dict[str, Any] = {
|
||||
"running": False,
|
||||
"last_started": None,
|
||||
"last_completed": None,
|
||||
"last_status": None,
|
||||
"last_error": None,
|
||||
"pid": None,
|
||||
}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
try:
|
||||
print("🚀 Initializing V28 orchestrator...", flush=True)
|
||||
get_single_match_orchestrator()
|
||||
get_v26_shadow_engine()
|
||||
print("✅ V28 orchestrator ready", flush=True)
|
||||
except Exception as error:
|
||||
print(f"❌ Failed to initialize orchestrator: {error}", flush=True)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Suggest-Bet AI Engine",
|
||||
version="28.0.0",
|
||||
description="V28 Single Match Prediction Package API",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
|
||||
def _parse_cors_origins() -> list[str]:
|
||||
raw = os.getenv("CORS_ALLOW_ORIGINS", "").strip()
|
||||
if raw:
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
# Dev-safe defaults + production domains.
|
||||
return [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:3001",
|
||||
"http://127.0.0.1:3001",
|
||||
"http://localhost:3005",
|
||||
"http://127.0.0.1:3005",
|
||||
"https://ui-suggestbet.bilgich.com",
|
||||
"https://suggestbet.bilgich.com",
|
||||
"https://iddaai.com",
|
||||
"https://www.iddaai.com",
|
||||
]
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=_parse_cors_origins(),
|
||||
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(_: Request, exc: Exception):
|
||||
import traceback
|
||||
|
||||
print(f"💥 ERROR: {exc}", flush=True)
|
||||
traceback.print_exc()
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"message": f"Internal Server Error: {str(exc)}"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root() -> dict[str, Any]:
|
||||
return {
|
||||
"status": "Suggest-Bet AI Engine v28",
|
||||
"engine": "V28 Single Match Orchestrator",
|
||||
"mode": os.getenv("AI_ENGINE_MODE", "v28"),
|
||||
"routes": [
|
||||
"POST /v20plus/analyze/{match_id}",
|
||||
"GET /v20plus/analyze-htms/{match_id}",
|
||||
"GET /v20plus/analyze-htft/{match_id}",
|
||||
"GET /v20plus/reversal-watchlist",
|
||||
"POST /v20plus/coupon",
|
||||
"GET /v20plus/daily-banker",
|
||||
"POST /v1/admin/retrain",
|
||||
"GET /v1/admin/retrain/status",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health_check() -> dict[str, Any]:
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
shadow_engine = get_v26_shadow_engine()
|
||||
|
||||
# Per-market V25 model status
|
||||
v25_readiness: dict[str, Any] = {"fully_loaded": False}
|
||||
try:
|
||||
v25_predictor = orchestrator._get_v25_predictor()
|
||||
v25_readiness = v25_predictor.readiness_summary()
|
||||
except Exception as v25_err:
|
||||
v25_readiness = {"fully_loaded": False, "error": str(v25_err)}
|
||||
|
||||
if HAS_BASKETBALL:
|
||||
basketball_predictor = get_basketball_v25_predictor()
|
||||
basketball_readiness = basketball_predictor.readiness_summary()
|
||||
ready = bool(basketball_readiness.get("fully_loaded", True))
|
||||
else:
|
||||
basketball_readiness = {"fully_loaded": False, "error": "Basketball module not found"}
|
||||
ready = True
|
||||
|
||||
league_readiness = get_league_model_loader().readiness_summary()
|
||||
overall_ready = ready and v25_readiness.get("fully_loaded", False)
|
||||
return {
|
||||
"status": "healthy" if overall_ready else "degraded",
|
||||
"engine": "v28.main",
|
||||
"mode": os.getenv("AI_ENGINE_MODE", "v28"),
|
||||
"ready": overall_ready,
|
||||
"v25_football": v25_readiness,
|
||||
"league_specific": league_readiness,
|
||||
"basketball_v25": basketball_readiness,
|
||||
"v26_shadow": shadow_engine.readiness_summary(),
|
||||
"prediction_service_ready": True,
|
||||
"model_loaded": overall_ready,
|
||||
"orchestrator_mode": getattr(orchestrator, "engine_mode", "v28"),
|
||||
}
|
||||
except Exception as error:
|
||||
return {"status": "unhealthy", "ready": False, "error": str(error)}
|
||||
|
||||
|
||||
_REQUIRED_RESPONSE_FIELDS = ("match_info", "market_board", "main_pick", "bet_summary", "data_quality")
|
||||
|
||||
|
||||
@app.post("/v20plus/analyze/{match_id}")
|
||||
async def analyze_match_v20plus(match_id: str) -> dict[str, Any]:
|
||||
started_at = time.time()
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
result = await asyncio.to_thread(orchestrator.analyze_match, match_id)
|
||||
elapsed_ms = int((time.time() - started_at) * 1000)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail=f"Match not found: {match_id}")
|
||||
|
||||
# Response validation: log missing required fields (non-fatal)
|
||||
missing_fields = [f for f in _REQUIRED_RESPONSE_FIELDS if f not in result]
|
||||
if missing_fields:
|
||||
print(f"⚠️ [API] analyze/{match_id} response missing fields: {missing_fields} ({elapsed_ms}ms)")
|
||||
|
||||
result["timing_ms"] = elapsed_ms
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/v20plus/analyze-htms/{match_id}")
|
||||
async def analyze_match_htms_v20plus(match_id: str) -> dict[str, Any]:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
result = await asyncio.to_thread(orchestrator.analyze_match_htms, match_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail=f"Match not found: {match_id}")
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/v20plus/analyze-htft/{match_id}")
|
||||
async def analyze_match_htft_v20plus(match_id: str, timeout_sec: int = 30) -> dict[str, Any]:
|
||||
# Small, explicit endpoint for HT/FT inspection and debugging in FE/Postman.
|
||||
if timeout_sec < 3 or timeout_sec > 120:
|
||||
raise HTTPException(status_code=400, detail="timeout_sec must be between 3 and 120")
|
||||
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
started_at = time.time()
|
||||
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
asyncio.to_thread(orchestrator.analyze_match, match_id),
|
||||
timeout=float(timeout_sec),
|
||||
)
|
||||
except asyncio.TimeoutError as error:
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail=f"Analyze timeout after {timeout_sec}s for match_id={match_id}",
|
||||
) from error
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail=f"Match not found: {match_id}")
|
||||
|
||||
risk = result.get("risk", {})
|
||||
market_board = result.get("market_board", {})
|
||||
htft_probs = market_board.get("HTFT", {}).get("probs", {}) or risk.get("ht_ft_probs", {})
|
||||
top_reversal_pick = None
|
||||
top_reversal_prob = 0.0
|
||||
if htft_probs:
|
||||
prob_12 = float(htft_probs.get("1/2", 0.0))
|
||||
prob_21 = float(htft_probs.get("2/1", 0.0))
|
||||
if prob_21 >= prob_12:
|
||||
top_reversal_pick = "2/1"
|
||||
top_reversal_prob = prob_21
|
||||
else:
|
||||
top_reversal_pick = "1/2"
|
||||
top_reversal_prob = prob_12
|
||||
|
||||
overall_htft_pick = None
|
||||
overall_htft_prob = 0.0
|
||||
if htft_probs:
|
||||
overall_htft_pick, overall_htft_prob = max(
|
||||
htft_probs.items(),
|
||||
key=lambda item: float(item[1]),
|
||||
)
|
||||
return {
|
||||
"engine": "v28.main",
|
||||
"match_info": result.get("match_info", {}),
|
||||
"timing_ms": int((time.time() - started_at) * 1000),
|
||||
"ht_ft_probs": htft_probs,
|
||||
"top_reversal_pick": top_reversal_pick,
|
||||
"top_reversal_prob": round(float(top_reversal_prob), 4),
|
||||
"overall_htft_pick": overall_htft_pick,
|
||||
"overall_htft_pick_prob": round(float(overall_htft_prob), 4),
|
||||
"surprise_hunter": result.get("surprise_hunter", {}),
|
||||
"ht_ft_reversal_radar": result.get("ht_ft_reversal_radar", {}),
|
||||
"first_half_result": result.get("market_board", {}).get("first_half_result", {}),
|
||||
"main_pick": result.get("main_pick", {}),
|
||||
"bet_summary": result.get("bet_summary", {}),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/v20plus/coupon")
|
||||
async def generate_coupon_v20plus(request: CouponRequest) -> dict[str, Any]:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
return await asyncio.to_thread(
|
||||
orchestrator.build_coupon,
|
||||
request.match_ids,
|
||||
request.strategy or "BALANCED",
|
||||
request.max_matches,
|
||||
request.min_confidence,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/v20plus/daily-banker")
|
||||
async def get_daily_banker_v20plus(count: int = 3) -> dict[str, Any]:
|
||||
if count < 1:
|
||||
raise HTTPException(status_code=400, detail="count must be >= 1")
|
||||
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
bankers = await asyncio.to_thread(orchestrator.get_daily_bankers, count)
|
||||
return {"count": len(bankers), "bankers": bankers}
|
||||
|
||||
@app.get("/v20plus/reversal-watchlist")
|
||||
async def get_reversal_watchlist_v20plus(
|
||||
count: int = 20,
|
||||
horizon_hours: int = 72,
|
||||
min_score: float = 45.0,
|
||||
top_leagues_only: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
if count < 1 or count > 100:
|
||||
raise HTTPException(status_code=400, detail="count must be between 1 and 100")
|
||||
if horizon_hours < 6 or horizon_hours > 168:
|
||||
raise HTTPException(status_code=400, detail="horizon_hours must be between 6 and 168")
|
||||
if min_score < 0 or min_score > 100:
|
||||
raise HTTPException(status_code=400, detail="min_score must be between 0 and 100")
|
||||
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
return await asyncio.to_thread(
|
||||
orchestrator.get_reversal_watchlist,
|
||||
count,
|
||||
horizon_hours,
|
||||
min_score,
|
||||
top_leagues_only,
|
||||
)
|
||||
|
||||
|
||||
# ─── ADMIN: Retrain Pipeline ─────────────────────────────────
|
||||
|
||||
def _run_retrain_pipeline(markets: str | None, trials: int):
|
||||
"""Background function: extract data → train model → reload."""
|
||||
global _retrain_state
|
||||
ai_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
scripts_dir = os.path.join(ai_dir, "scripts")
|
||||
python = os.path.join(ai_dir, "venv", "bin", "python3")
|
||||
if not os.path.exists(python):
|
||||
python = sys.executable # fallback
|
||||
|
||||
try:
|
||||
# Step 1: Extract training data
|
||||
print("🔄 [RETRAIN] Step 1/3: Extracting training data...", flush=True)
|
||||
result = subprocess.run(
|
||||
[python, os.path.join(scripts_dir, "extract_training_data.py")],
|
||||
capture_output=True, text=True, timeout=600, cwd=ai_dir,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Extract failed:\n{result.stderr[-500:]}")
|
||||
print(f"✅ [RETRAIN] Extract done", flush=True)
|
||||
|
||||
# Step 2: Train V25 Pro
|
||||
print("🔄 [RETRAIN] Step 2/3: Training V25 Pro model...", flush=True)
|
||||
train_cmd = [python, os.path.join(scripts_dir, "train_v25_pro.py")]
|
||||
if markets:
|
||||
train_cmd += ["--markets", markets]
|
||||
train_cmd += ["--trials", str(trials)]
|
||||
|
||||
result = subprocess.run(
|
||||
train_cmd, capture_output=True, text=True, timeout=3600, cwd=ai_dir,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Training failed:\n{result.stderr[-500:]}")
|
||||
print(f"✅ [RETRAIN] Training done", flush=True)
|
||||
|
||||
# Step 3: Reload models in memory
|
||||
print("🔄 [RETRAIN] Step 3/3: Reloading models...", flush=True)
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
v25 = orchestrator._get_v25_predictor()
|
||||
v25._loaded = False
|
||||
v25.load_models()
|
||||
print("✅ [RETRAIN] Models reloaded in memory", flush=True)
|
||||
except Exception as reload_err:
|
||||
print(f"⚠️ [RETRAIN] Hot reload failed (restart needed): {reload_err}", flush=True)
|
||||
|
||||
_retrain_state.update({
|
||||
"running": False,
|
||||
"last_completed": datetime.now().isoformat(),
|
||||
"last_status": "success",
|
||||
"last_error": None,
|
||||
})
|
||||
print("🎉 [RETRAIN] Pipeline complete!", flush=True)
|
||||
|
||||
except Exception as err:
|
||||
_retrain_state.update({
|
||||
"running": False,
|
||||
"last_completed": datetime.now().isoformat(),
|
||||
"last_status": "failed",
|
||||
"last_error": str(err),
|
||||
})
|
||||
print(f"❌ [RETRAIN] Pipeline failed: {err}", flush=True)
|
||||
|
||||
|
||||
@app.post("/v1/admin/retrain")
|
||||
async def admin_retrain(request: RetrainRequest) -> dict[str, Any]:
|
||||
"""Trigger full retrain pipeline: extract → train → reload."""
|
||||
if _retrain_state["running"]:
|
||||
return {
|
||||
"status": "already_running",
|
||||
"message": f"Retrain in progress since {_retrain_state['last_started']}",
|
||||
}
|
||||
|
||||
_retrain_state.update({
|
||||
"running": True,
|
||||
"last_started": datetime.now().isoformat(),
|
||||
"last_status": "running",
|
||||
"last_error": None,
|
||||
})
|
||||
|
||||
# Run in background thread
|
||||
import threading
|
||||
thread = threading.Thread(
|
||||
target=_run_retrain_pipeline,
|
||||
args=(request.markets, request.trials or 50),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return {
|
||||
"status": "triggered",
|
||||
"message": "Retrain pipeline started in background",
|
||||
"reason": request.reason,
|
||||
"markets": request.markets or "all",
|
||||
"trials": request.trials or 50,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/v1/admin/retrain/status")
|
||||
async def admin_retrain_status() -> dict[str, Any]:
|
||||
"""Check retrain pipeline status."""
|
||||
return {**_retrain_state}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.getenv("PORT", "8000"))
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
|
||||
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
Basketball V25 Predictor Package
|
||||
=================================
|
||||
Loads trained XGBoost + LightGBM models for basketball market predictions:
|
||||
- ML (Money Line — home / away win)
|
||||
- Total (Over/Under total points)
|
||||
- Spread (ATS home cover / away cover)
|
||||
|
||||
Model files live in this directory:
|
||||
xgb_basketball_v25_{market}.json — XGBoost (primary)
|
||||
lgb_basketball_v25_{market}.txt — LightGBM (ensemble)
|
||||
feature_cols.json — ordered feature list
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
# ── Constants ─────────────────────────────────────────────────────────────────
|
||||
_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
_MARKETS = ("ml", "total", "spread")
|
||||
|
||||
|
||||
# ── Output dataclass ──────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class BasketballMatchPrediction:
|
||||
"""Complete basketball match prediction output."""
|
||||
|
||||
match_id: str
|
||||
home_team_name: str
|
||||
away_team_name: str
|
||||
league_name: str = ""
|
||||
|
||||
# Money Line
|
||||
ml_home_prob: float = 0.50
|
||||
ml_away_prob: float = 0.50
|
||||
ml_pick: str = ""
|
||||
ml_confidence: float = 0.0
|
||||
|
||||
# Total (Over/Under)
|
||||
total_line: float = 0.0
|
||||
total_over_prob: float = 0.50
|
||||
total_under_prob: float = 0.50
|
||||
total_pick: str = ""
|
||||
total_confidence: float = 0.0
|
||||
|
||||
# Spread (ATS)
|
||||
spread_home_line: float = 0.0
|
||||
spread_home_prob: float = 0.50
|
||||
spread_away_prob: float = 0.50
|
||||
spread_pick: str = ""
|
||||
spread_confidence: float = 0.0
|
||||
|
||||
# Meta
|
||||
model_version: str = "basketball_v25"
|
||||
risk_level: str = "MEDIUM"
|
||||
analysis_details: Dict[str, Any] = field(default_factory=dict)
|
||||
market_board: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"match_id": self.match_id,
|
||||
"home_team": self.home_team_name,
|
||||
"away_team": self.away_team_name,
|
||||
"league": self.league_name,
|
||||
"model": self.model_version,
|
||||
"risk_level": self.risk_level,
|
||||
"ml": {
|
||||
"home_prob": round(self.ml_home_prob * 100, 1),
|
||||
"away_prob": round(self.ml_away_prob * 100, 1),
|
||||
"pick": self.ml_pick,
|
||||
"confidence": round(self.ml_confidence, 1),
|
||||
},
|
||||
"total": {
|
||||
"line": self.total_line,
|
||||
"over_prob": round(self.total_over_prob * 100, 1),
|
||||
"under_prob": round(self.total_under_prob * 100, 1),
|
||||
"pick": self.total_pick,
|
||||
"confidence": round(self.total_confidence, 1),
|
||||
},
|
||||
"spread": {
|
||||
"home_line": self.spread_home_line,
|
||||
"home_prob": round(self.spread_home_prob * 100, 1),
|
||||
"away_prob": round(self.spread_away_prob * 100, 1),
|
||||
"pick": self.spread_pick,
|
||||
"confidence": round(self.spread_confidence, 1),
|
||||
},
|
||||
"market_board": self.market_board,
|
||||
"analysis_details": self.analysis_details,
|
||||
}
|
||||
|
||||
|
||||
# ── Predictor ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class BasketballV25Predictor:
|
||||
"""
|
||||
Ensemble basketball predictor using XGBoost + LightGBM models.
|
||||
|
||||
Markets:
|
||||
- ml → home/away win probability
|
||||
- total → over/under total points
|
||||
- spread → home/away ATS cover
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.feature_cols: List[str] = self._load_feature_cols()
|
||||
self.models: Dict[str, Any] = {}
|
||||
self._load_models()
|
||||
print(f"✅ BasketballV25Predictor ready ({len(self.models)} models loaded)")
|
||||
|
||||
# ── Setup ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _load_feature_cols(self) -> List[str]:
|
||||
path = os.path.join(_DIR, "feature_cols.json")
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ [Basketball] Could not load feature_cols.json: {e}")
|
||||
return []
|
||||
|
||||
def _load_models(self) -> None:
|
||||
for market in _MARKETS:
|
||||
xgb_path = os.path.join(_DIR, f"xgb_basketball_v25_{market}.json")
|
||||
lgb_path = os.path.join(_DIR, f"lgb_basketball_v25_{market}.txt")
|
||||
|
||||
xgb_model = self._try_load_xgb(xgb_path, market)
|
||||
lgb_model = self._try_load_lgb(lgb_path, market)
|
||||
|
||||
if xgb_model is not None or lgb_model is not None:
|
||||
self.models[market] = {"xgb": xgb_model, "lgb": lgb_model}
|
||||
|
||||
def _try_load_xgb(self, path: str, market: str) -> Optional[Any]:
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
try:
|
||||
import xgboost as xgb # type: ignore[import-not-found]
|
||||
m = xgb.XGBClassifier()
|
||||
m.load_model(path)
|
||||
return m
|
||||
except Exception as e:
|
||||
print(f"⚠️ [Basketball] XGB {market} load failed: {e}")
|
||||
return None
|
||||
|
||||
def _try_load_lgb(self, path: str, market: str) -> Optional[Any]:
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
try:
|
||||
import lightgbm as lgb # type: ignore[import-not-found]
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
model_str = f.read()
|
||||
return lgb.Booster(model_str=model_str)
|
||||
except Exception as e:
|
||||
print(f"⚠️ [Basketball] LGB {market} load failed: {e}")
|
||||
return None
|
||||
|
||||
# ── Inference ──────────────────────────────────────────────────────────
|
||||
|
||||
def _build_feature_row(self, odds_data: Dict[str, Any], **kwargs: Any) -> "Any":
|
||||
"""Build a single-row DataFrame aligned to training feature columns."""
|
||||
import pandas as pd # type: ignore[import-not-found]
|
||||
|
||||
row: Dict[str, float] = {}
|
||||
for col in self.feature_cols:
|
||||
row[col] = float(kwargs.get(col) or odds_data.get(col) or 0.0)
|
||||
|
||||
# Map common odds keys
|
||||
row["ml_home_odds"] = float(odds_data.get("ml_h") or 0.0)
|
||||
row["ml_away_odds"] = float(odds_data.get("ml_a") or 0.0)
|
||||
row["total_line"] = float(odds_data.get("tot_line") or 0.0)
|
||||
row["total_over_odds"] = float(odds_data.get("tot_o") or 0.0)
|
||||
row["total_under_odds"] = float(odds_data.get("tot_u") or 0.0)
|
||||
row["spread_home_line"] = float(odds_data.get("spread_home_line") or 0.0)
|
||||
row["spread_home_odds"] = float(odds_data.get("spread_h") or 0.0)
|
||||
row["spread_away_odds"] = float(odds_data.get("spread_a") or 0.0)
|
||||
|
||||
# Implied probabilities
|
||||
def _imp(odd: float) -> float:
|
||||
return (1.0 / odd) if odd > 1.01 else 0.5
|
||||
|
||||
ml_h = row["ml_home_odds"]
|
||||
ml_a = row["ml_away_odds"]
|
||||
if ml_h > 1.01 and ml_a > 1.01:
|
||||
raw = _imp(ml_h) + _imp(ml_a)
|
||||
row["implied_home"] = _imp(ml_h) / raw
|
||||
row["implied_away"] = _imp(ml_a) / raw
|
||||
row["odds_overround"] = raw - 1.0
|
||||
|
||||
tot_o = row["total_over_odds"]
|
||||
tot_u = row["total_under_odds"]
|
||||
if tot_o > 1.01 and tot_u > 1.01:
|
||||
raw = _imp(tot_o) + _imp(tot_u)
|
||||
row["implied_total_over"] = _imp(tot_o) / raw
|
||||
row["implied_total_under"] = _imp(tot_u) / raw
|
||||
|
||||
sp_h = row["spread_home_odds"]
|
||||
sp_a = row["spread_away_odds"]
|
||||
if sp_h > 1.01 and sp_a > 1.01:
|
||||
raw = _imp(sp_h) + _imp(sp_a)
|
||||
row["implied_spread_home"] = _imp(sp_h) / raw
|
||||
row["implied_spread_away"] = _imp(sp_a) / raw
|
||||
|
||||
return pd.DataFrame([row])
|
||||
|
||||
def _ensemble_predict(self, market: str, df: "Any") -> List[float]:
|
||||
"""Return [p_class0, p_class1] from XGB+LGB ensemble."""
|
||||
models = self.models.get(market, {})
|
||||
xgb_model = models.get("xgb")
|
||||
lgb_model = models.get("lgb")
|
||||
|
||||
probs_list: List[List[float]] = []
|
||||
|
||||
if xgb_model is not None:
|
||||
try:
|
||||
p = xgb_model.predict_proba(df)
|
||||
probs_list.append([float(p[0][0]), float(p[0][1])])
|
||||
except Exception as e:
|
||||
print(f"⚠️ [Basketball] XGB {market} inference failed: {e}")
|
||||
|
||||
if lgb_model is not None:
|
||||
try:
|
||||
p_raw = lgb_model.predict(df)
|
||||
p1 = float(p_raw[0]) if len(p_raw.shape) == 1 else float(p_raw[0][1])
|
||||
probs_list.append([1.0 - p1, p1])
|
||||
except Exception as e:
|
||||
print(f"⚠️ [Basketball] LGB {market} inference failed: {e}")
|
||||
|
||||
if not probs_list:
|
||||
return [0.5, 0.5]
|
||||
|
||||
p0 = sum(p[0] for p in probs_list) / len(probs_list)
|
||||
p1 = sum(p[1] for p in probs_list) / len(probs_list)
|
||||
total = p0 + p1 or 1.0
|
||||
return [p0 / total, p1 / total]
|
||||
|
||||
# ── Public API ─────────────────────────────────────────────────────────
|
||||
|
||||
def predict(
|
||||
self,
|
||||
match_id: str,
|
||||
home_team_id: str,
|
||||
away_team_id: str,
|
||||
home_team_name: str = "",
|
||||
away_team_name: str = "",
|
||||
league_id: str = "",
|
||||
league_name: str = "",
|
||||
odds_data: Optional[Dict[str, Any]] = None,
|
||||
sidelined_data: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> BasketballMatchPrediction:
|
||||
odds = odds_data or {}
|
||||
prediction = BasketballMatchPrediction(
|
||||
match_id=match_id,
|
||||
home_team_name=home_team_name,
|
||||
away_team_name=away_team_name,
|
||||
league_name=league_name,
|
||||
)
|
||||
|
||||
# Sidelined impact
|
||||
home_sl = int((sidelined_data or {}).get("homeTeam", {}).get("totalSidelined", 0) or 0)
|
||||
away_sl = int((sidelined_data or {}).get("awayTeam", {}).get("totalSidelined", 0) or 0)
|
||||
kwargs.setdefault("home_sidelined_count", float(home_sl))
|
||||
kwargs.setdefault("away_sidelined_count", float(away_sl))
|
||||
kwargs.setdefault("sidelined_diff", float(home_sl - away_sl))
|
||||
kwargs.setdefault("missing_players_impact", float(home_sl + away_sl) / 10.0)
|
||||
|
||||
if not self.models:
|
||||
print("⚠️ [Basketball] No models loaded — returning neutral defaults")
|
||||
return prediction
|
||||
|
||||
try:
|
||||
df = self._build_feature_row(odds, **kwargs)
|
||||
|
||||
# ── ML ──
|
||||
ml_probs = self._ensemble_predict("ml", df)
|
||||
prediction.ml_home_prob = ml_probs[0]
|
||||
prediction.ml_away_prob = ml_probs[1]
|
||||
prediction.ml_pick = "1" if ml_probs[0] >= ml_probs[1] else "2"
|
||||
prediction.ml_confidence = max(ml_probs) * 100.0
|
||||
|
||||
# ── Total ──
|
||||
prediction.total_line = float(odds.get("tot_line") or 0.0)
|
||||
tot_probs = self._ensemble_predict("total", df)
|
||||
prediction.total_over_prob = tot_probs[1]
|
||||
prediction.total_under_prob = tot_probs[0]
|
||||
total_line = prediction.total_line
|
||||
prediction.total_pick = (
|
||||
f"Over {total_line}" if tot_probs[1] >= tot_probs[0] else f"Under {total_line}"
|
||||
)
|
||||
prediction.total_confidence = max(tot_probs) * 100.0
|
||||
|
||||
# ── Spread ──
|
||||
prediction.spread_home_line = float(odds.get("spread_home_line") or 0.0)
|
||||
sp_probs = self._ensemble_predict("spread", df)
|
||||
prediction.spread_home_prob = sp_probs[0]
|
||||
prediction.spread_away_prob = sp_probs[1]
|
||||
home_line = prediction.spread_home_line
|
||||
away_line = -home_line
|
||||
prediction.spread_pick = (
|
||||
f"Home {home_line:+.1f}" if sp_probs[0] >= sp_probs[1] else f"Away {away_line:+.1f}"
|
||||
)
|
||||
prediction.spread_confidence = max(sp_probs) * 100.0
|
||||
|
||||
# Market board summary
|
||||
prediction.market_board = {
|
||||
"ML": {
|
||||
"1": f"{prediction.ml_home_prob * 100:.0f}%",
|
||||
"2": f"{prediction.ml_away_prob * 100:.0f}%",
|
||||
},
|
||||
"Totals": {
|
||||
f"Over {total_line}": f"{prediction.total_over_prob * 100:.0f}%",
|
||||
f"Under {total_line}": f"{prediction.total_under_prob * 100:.0f}%",
|
||||
},
|
||||
"Spread": {
|
||||
f"Home {home_line:+.1f}": f"{prediction.spread_home_prob * 100:.0f}%",
|
||||
f"Away {away_line:+.1f}": f"{prediction.spread_away_prob * 100:.0f}%",
|
||||
},
|
||||
}
|
||||
|
||||
# Risk
|
||||
top_conf = max(prediction.ml_confidence, prediction.total_confidence, prediction.spread_confidence)
|
||||
prediction.risk_level = "LOW" if top_conf >= 65 else "MEDIUM" if top_conf >= 55 else "HIGH"
|
||||
prediction.analysis_details = {
|
||||
"model_version": "basketball_v25",
|
||||
"markets_predicted": list(self.models.keys()),
|
||||
"ensemble_size": {m: sum(1 for k in v.values() if v[k] is not None) for m, v in self.models.items()},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ [Basketball] Prediction failed for {match_id}: {e}")
|
||||
|
||||
return prediction
|
||||
|
||||
|
||||
# ── Singleton factory ──────────────────────────────────────────────────────────
|
||||
|
||||
_predictor: Optional[BasketballV25Predictor] = None
|
||||
|
||||
|
||||
def get_basketball_v25_predictor() -> BasketballV25Predictor:
|
||||
"""Return the singleton BasketballV25Predictor (lazy-loaded)."""
|
||||
global _predictor
|
||||
if _predictor is None:
|
||||
_predictor = BasketballV25Predictor()
|
||||
return _predictor
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BasketballMatchPrediction",
|
||||
"BasketballV25Predictor",
|
||||
"get_basketball_v25_predictor",
|
||||
]
|
||||
@@ -0,0 +1,101 @@
|
||||
[
|
||||
"home_overall_elo",
|
||||
"away_overall_elo",
|
||||
"elo_diff",
|
||||
"home_home_elo",
|
||||
"away_away_elo",
|
||||
"home_form_elo",
|
||||
"away_form_elo",
|
||||
"home_form_score",
|
||||
"away_form_score",
|
||||
"form_score_diff",
|
||||
"home_points_avg",
|
||||
"away_points_avg",
|
||||
"points_avg_diff",
|
||||
"home_conceded_avg",
|
||||
"away_conceded_avg",
|
||||
"conceded_avg_diff",
|
||||
"home_net_rating",
|
||||
"away_net_rating",
|
||||
"net_rating_diff",
|
||||
"home_win_rate",
|
||||
"away_win_rate",
|
||||
"win_rate_diff",
|
||||
"home_winning_streak",
|
||||
"away_winning_streak",
|
||||
"streak_diff",
|
||||
"home_rest_days",
|
||||
"away_rest_days",
|
||||
"rest_diff",
|
||||
"home_rebounds_avg",
|
||||
"away_rebounds_avg",
|
||||
"rebounds_diff",
|
||||
"home_assists_avg",
|
||||
"away_assists_avg",
|
||||
"assists_diff",
|
||||
"home_steals_avg",
|
||||
"away_steals_avg",
|
||||
"steals_diff",
|
||||
"home_blocks_avg",
|
||||
"away_blocks_avg",
|
||||
"blocks_diff",
|
||||
"home_turnovers_avg",
|
||||
"away_turnovers_avg",
|
||||
"turnovers_diff",
|
||||
"home_fg_pct",
|
||||
"away_fg_pct",
|
||||
"fg_pct_diff",
|
||||
"home_three_pt_pct",
|
||||
"away_three_pt_pct",
|
||||
"three_pt_pct_diff",
|
||||
"home_ft_pct",
|
||||
"away_ft_pct",
|
||||
"ft_pct_diff",
|
||||
"home_q1_avg",
|
||||
"away_q1_avg",
|
||||
"home_q4_avg",
|
||||
"away_q4_avg",
|
||||
"home_conc_rebounds_avg",
|
||||
"away_conc_rebounds_avg",
|
||||
"home_conc_assists_avg",
|
||||
"away_conc_assists_avg",
|
||||
"home_conc_turnovers_avg",
|
||||
"away_conc_turnovers_avg",
|
||||
"home_conc_fg_pct",
|
||||
"away_conc_fg_pct",
|
||||
"home_conc_three_pt_pct",
|
||||
"away_conc_three_pt_pct",
|
||||
"h2h_total_matches",
|
||||
"h2h_home_win_rate",
|
||||
"h2h_avg_points",
|
||||
"h2h_avg_margin",
|
||||
"h2h_over_total_rate",
|
||||
"h2h_home_cover_rate",
|
||||
"league_avg_points",
|
||||
"league_home_win_rate",
|
||||
"league_over_total_rate",
|
||||
"league_home_cover_rate",
|
||||
"ml_home_odds",
|
||||
"ml_away_odds",
|
||||
"implied_home",
|
||||
"implied_away",
|
||||
"total_line",
|
||||
"total_over_odds",
|
||||
"total_under_odds",
|
||||
"implied_total_over",
|
||||
"implied_total_under",
|
||||
"spread_home_line",
|
||||
"spread_home_odds",
|
||||
"spread_away_odds",
|
||||
"implied_spread_home",
|
||||
"implied_spread_away",
|
||||
"odds_overround",
|
||||
"home_sidelined_count",
|
||||
"away_sidelined_count",
|
||||
"sidelined_diff",
|
||||
"missing_players_impact",
|
||||
"total_points_form",
|
||||
"total_points_allowed_form",
|
||||
"projected_total_delta_vs_line",
|
||||
"projected_margin_vs_spread"
|
||||
]
|
||||
@@ -0,0 +1,655 @@
|
||||
tree
|
||||
version=v4
|
||||
num_class=1
|
||||
num_tree_per_iteration=1
|
||||
label_index=0
|
||||
max_feature_idx=98
|
||||
objective=binary sigmoid:1
|
||||
feature_names=Column_0 Column_1 Column_2 Column_3 Column_4 Column_5 Column_6 Column_7 Column_8 Column_9 Column_10 Column_11 Column_12 Column_13 Column_14 Column_15 Column_16 Column_17 Column_18 Column_19 Column_20 Column_21 Column_22 Column_23 Column_24 Column_25 Column_26 Column_27 Column_28 Column_29 Column_30 Column_31 Column_32 Column_33 Column_34 Column_35 Column_36 Column_37 Column_38 Column_39 Column_40 Column_41 Column_42 Column_43 Column_44 Column_45 Column_46 Column_47 Column_48 Column_49 Column_50 Column_51 Column_52 Column_53 Column_54 Column_55 Column_56 Column_57 Column_58 Column_59 Column_60 Column_61 Column_62 Column_63 Column_64 Column_65 Column_66 Column_67 Column_68 Column_69 Column_70 Column_71 Column_72 Column_73 Column_74 Column_75 Column_76 Column_77 Column_78 Column_79 Column_80 Column_81 Column_82 Column_83 Column_84 Column_85 Column_86 Column_87 Column_88 Column_89 Column_90 Column_91 Column_92 Column_93 Column_94 Column_95 Column_96 Column_97 Column_98
|
||||
feature_infos=none none none none none none none [0:100] [0:100] [-100:100] [54:128.875] [61:138] [-41.333333333333329:24] [61:130.125] [59:133] [-50:34.916666666666671] [-28.666666666666657:32.5] [-24.5:27] [-35.5:31.833333333333329] [0:1] [0:1] [-1:1] [0:12] [0:12] [-12:10] [0.91666666666666663:186.1875] [0.85416666666666663:185.08333333333337] [-179.02083333333334:184.16666666666663] [20:54] [22:56] [-21:15.833333333333332] [11:34.5] [9:36] [-15:13.333333333333332] [2:13] [3:13] [-8:5.5] [0.875:9] [0:12] [-9:4.875] [7:22] [8:23] [-10:10] [0.41025641025641019:0.67647058823529416] [0.39730639730639727:0.76744186046511631] [-0.3174418604651163:0.22647058823529409] [0.20419254658385089:0.4825000000000001] [0.1875:0.54166666666666663] [-0.22171945701357459:0.17923280423280419] [0.47826086956521741:0.87898457080200498] [0.42857142857142849:0.88636363636363635] [-0.29647435897435898:0.38962801311591622] [11:39.666666666666664] [15.5:40] [10:31.875] [11.5:34] [15:56] [17:57] [12:32.125] [10:38] [8:20] [7:22] [0.40000000000000002:0.69047619047619047] [0.3902439024390244:0.71739130434782605] [0.23753976670201479:0.51851851851851849] [0.1707317073170731:0.54545454545454541] [0:8] [0:1] [120:295] [-38:36] [0:1] [0:1] [140.39130434782609:233.09999999999999] [0:1] [0:1] [0:1] [1.05:4.1100000000000003] [1.05:4.1299999999999999] [0.2034883720930232:0.79729729729729726] [0.20270270270270269:0.79651162790697683] [145.5:257.5] [1.3:7.7599999999999998] [1.4099999999999999:7.5599999999999996] [0.1789976133651551:0.75977653631284914] [0.24022346368715081:0.82100238663484482] [-15.5:12.5] [1.3:2.7200000000000002] [1.1799999999999999:2.7799999999999998] [0.36298076923076922:0.64953271028037374] [0.35046728971962621:0.63701923076923073] [0.1918833727344364:0.21555133935749429] none none none none [132:250.6875] [126.25:256.625] [-63.5:88.6875] [-35.5:27.5]
|
||||
tree_sizes=2312 2651 2758 2331 2348 2534 2554 2662 2359 2770 1932 1926 2552 2145 2460 2555 1931 2453 1831 2020 2352 2348 2035
|
||||
|
||||
Tree=0
|
||||
num_leaves=21
|
||||
num_cat=0
|
||||
split_feature=77 77 95 50 40 97 37 25 73 56 81 57 35 17 50 97 25 23 23 9
|
||||
split_gain=87.208 23.2965 12.6569 10.5307 10.1901 9.83622 9.59439 7.56522 7.2843 6.81795 6.60435 5.65211 7.74871 6.22559 5.61137 5.14301 3.28504 2.92219 2.6911 0.0931822
|
||||
threshold=1.3750000000000002 2.6550000000000007 166.34375000000003 0.73851279305095374 11.062500000000002 5.9687500000000009 3.7750000000000004 1.8802083333333337 0.5344873150105709 30.803571428571434 1.5750000000000004 41.062500000000007 6.6937500000000005 -4.7749999999999977 0.71663103786665172 -3.4241071428571384 7.7864583333333348 1.0000000180025095e-35 1.0000000180025095e-35 -39.015151515151508
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 2 14 -1 -4 7 -7 -6 -3 -10 -5 12 -11 -13 -2 -16 18 -17 -12 -20
|
||||
right_child=1 8 4 10 5 6 -8 -9 9 11 16 13 -14 -15 15 17 -18 -19 19 -21
|
||||
leaf_value=-0.14765628269715012 -0.15600540791535678 -0.23353299922727577 -0.12151989070972055 -0.13344020462290634 -0.11780070546597361 -0.21612555668738542 -0.16327023115717296 -0.15881183656013212 -0.15004174704520915 -0.17986005139594724 -0.10382337530156519 -0.2025678370168939 -0.21889492254600434 -0.15072330828751174 -0.17027559642606713 -0.19596193574534582 -0.1164283857771044 -0.23353299922727577 -0.079223274212206221 -0.072514155733290175
|
||||
leaf_weight=11.17881618440151 6.70728971064091 6.210453435778617 11.427234321832655 9.1914710849523527 7.94938039779663 9.1914710849523562 13.662997558712958 76.015950053930283 6.7072897106409064 14.159833833575247 8.943052947521215 6.4588715732097617 19.128196582198143 8.6946348100900632 6.9557078480720582 7.4525441229343361 8.1977985352277738 5.9620352983474731 5.9620352983474714 7.4525441229343414
|
||||
leaf_count=45 27 25 46 37 32 37 55 306 27 57 36 26 77 35 28 30 33 24 24 30
|
||||
internal_value=-0.160244 -0.171796 -0.16307 -0.113358 -0.157421 -0.161262 -0.184527 -0.154929 -0.192463 -0.187838 -0.103712 -0.193072 -0.202291 -0.172821 -0.187739 -0.198187 -0.0947688 -0.21266 -0.0868269 -0.075496
|
||||
internal_weight=257.61 206.684 145.325 50.9257 118.247 106.82 22.8545 83.9653 61.3593 55.1488 39.7469 48.4415 33.288 15.1535 27.0776 20.3703 30.5554 13.4146 22.3576 13.4146
|
||||
internal_count=1037 832 585 205 476 430 92 338 247 222 160 195 134 61 109 82 123 54 90 54
|
||||
is_linear=0
|
||||
shrinkage=1
|
||||
|
||||
|
||||
Tree=1
|
||||
num_leaves=24
|
||||
num_cat=0
|
||||
split_feature=78 78 95 46 40 97 37 41 13 50 75 83 22 17 50 97 15 81 65 43 23 43 25
|
||||
split_gain=80.2042 21.5716 11.6989 11.0897 9.36546 9.08525 8.91556 8.1723 7.75226 7.09679 6.96929 6.56155 6.17385 5.70382 5.20301 4.788 4.74542 3.71513 3.26193 3.12932 2.73996 1.35961 0.11562
|
||||
threshold=0.39285714285714285 0.68693349935065817 166.34375000000003 0.34153389001132711 11.062500000000002 5.9687500000000009 3.7750000000000004 14.612500000000002 109.93750000000001 0.74692579813599125 0.36503968253968255 0.47970222696600762 1.5000000000000002 6.5625000000000009 0.71663103786665172 -3.4241071428571384 1.5166666666666659 1.635 0.35759117986701622 0.52557970719916269 1.0000000180025095e-35 0.51924929840975398 3.0312500000000004
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 2 14 8 -4 7 -7 -6 9 -1 16 -12 17 21 -2 -16 18 -13 -3 -20 -17 22 -5
|
||||
right_child=1 10 4 13 5 6 -8 -9 -10 -11 11 12 -14 -15 15 20 -18 -19 19 -21 -22 -23 -24
|
||||
leaf_value=0.0055530680640734674 0.0032845293651423612 -0.072766645625454485 0.036292826760507588 0.077436849336701308 -0.0022894176915147862 -0.054725292524358454 -0.003689958048423233 0.027881485430822688 -0.013760354637319356 0.059619560028970849 -0.050594773409676115 -0.0085801885492360844 -0.033840627756109222 0.036366913393967221 -0.010423432121785391 -0.035188847843671808 -0.021864921616209863 0.033823144597002808 -0.060759072260146665 -0.021284474578964662 -0.071669256311519158 0.058239915950701049 0.085149025961743521
|
||||
leaf_weight=6.4684473276138368 6.7090961784124401 9.4034554809331912 11.457648575305937 5.9847112745046669 65.60038697719574 9.142817825078966 13.658771887421606 18.391589343547821 6.4761682301759711 9.7236045598983747 7.1794854700565365 7.4327530264854449 8.1723358631134015 9.9696762263774854 6.9495052695274415 7.4284560978412575 10.15483947098255 5.9542565494775772 6.6812664419412595 6.190190851688385 5.9189314842224121 5.978186532855033 6.4756923913955688
|
||||
leaf_count=26 27 38 46 24 264 37 55 74 26 39 29 30 33 40 28 30 41 24 27 25 24 24 26
|
||||
internal_value=-0.000786037 -0.0118905 -0.00349945 0.0440926 0.00192543 -0.00176177 -0.0241537 0.00431706 0.0232273 0.0380209 -0.0318166 -0.0174741 -0.00644452 0.0607419 -0.0272534 -0.0373477 -0.0445269 0.0102799 -0.0548581 -0.0417748 -0.0513662 0.0739213 0.0814449
|
||||
internal_weight=257.502 206.426 145.257 51.0765 118.251 106.794 22.8016 83.992 22.6682 16.1921 61.1686 28.7388 21.5593 28.4083 27.006 20.2969 32.4298 13.387 22.2749 12.8715 13.3474 18.4386 12.4604
|
||||
internal_count=1037 832 585 205 476 430 92 338 91 65 247 116 87 114 109 82 131 54 90 52 54 74 50
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=2
|
||||
num_leaves=25
|
||||
num_cat=0
|
||||
split_feature=77 76 10 46 51 75 75 42 12 51 74 44 9 61 86 41 49 17 85 44 27 49 74 97
|
||||
split_gain=73.8475 19.9927 10.9898 10.1665 9.79528 8.50828 11.1493 9.23472 9.00307 8.63936 8.14649 7.11143 7.43675 10.0884 8.56935 6.6516 6.48284 5.22574 5.21615 4.45089 3.01631 2.88722 1.39723 0.105613
|
||||
threshold=1.3750000000000002 1.2250000000000003 80.937500000000014 0.34153389001132711 0.011242161620033652 0.44508064516129037 0.39517441860465125 -0.61249999999999971 2.5499999999999976 0.012462680598456451 0.40642170329670335 0.55897334407178179 8.7121212121212164 11.866071428571431 1.635 14.225000000000001 0.78414366883116904 6.5625000000000009 6.0000000000000009 0.51258507460016867 -0.10937499999999993 0.73610958414604988 0.62250000000000016 1.1937499999999945
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 11 19 4 18 6 8 10 -4 -9 -7 12 13 -2 -15 20 -8 22 -1 -3 -14 -21 23 -5
|
||||
right_child=1 2 5 17 -6 7 16 9 -10 -11 -12 -13 15 14 -16 -17 -18 -19 -20 21 -22 -23 -24 -25
|
||||
leaf_value=0.017705783755189462 0.028974497745101052 -0.0060737041061852126 0.028384924190391733 0.081917709440956332 -0.011007695119626166 0.051971315475459197 -0.05418693058345065 -0.015490981812325239 -0.030486429608756988 0.017134873390643509 0.01007092726093781 -0.071582917658417497 -0.024872499607636107 0.0039725729814389819 -0.046514665309970234 -0.0035756501646253529 -0.0037995538175229883 0.034855017993211231 0.06661466771004268 -0.026672382631287159 -0.057894168683677763 -0.065766238281201289 0.054894232678188361 0.074556160849369266
|
||||
leaf_weight=6.7373062372207659 7.4330984354019192 7.1768550276756313 11.678280815482141 6.4950603991746956 8.7123459726572019 11.683709740638731 11.923531427979471 30.992172449827194 6.4527826756238928 22.351678267121315 20.364999443292618 5.9138691127300254 5.9248394817113939 9.3952521085739154 12.583362787961958 6.4179672747850409 6.2149500101804724 9.9856856316328031 7.2363171875476837 6.1660670638084394 17.493629351258278 5.9290464669466019 5.9967442750930777 5.9974743723869324
|
||||
leaf_count=27 30 29 47 26 35 47 48 125 26 90 82 24 24 38 51 26 25 40 29 25 71 24 24 24
|
||||
internal_value=-0.000757575 -0.0114353 -0.00296399 0.0422564 0.0222795 0.00148162 -0.0147493 0.00837551 0.00743281 -0.00182038 0.0253462 -0.0297572 -0.0255824 -0.011309 -0.0249328 -0.0396526 -0.0369223 0.058172 0.0430335 -0.0310287 -0.0495397 -0.0458363 0.0707651 0.0783835
|
||||
internal_weight=257.257 206.096 140.934 51.1609 22.686 121.662 36.2695 85.3926 18.1311 53.3439 32.0487 65.162 59.2481 29.4117 21.9786 29.8364 18.1385 28.475 13.9736 19.272 23.4185 12.0951 18.4893 12.4925
|
||||
internal_count=1037 832 568 205 91 490 146 344 73 215 129 264 240 119 89 121 73 114 56 78 95 49 74 50
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=3
|
||||
num_leaves=21
|
||||
num_cat=0
|
||||
split_feature=77 77 95 46 51 86 49 42 73 69 54 7 31 85 50 81 61 44 53 77
|
||||
split_gain=68.0698 18.5538 10.4158 9.36539 9.01462 7.14355 8.22377 7.01134 6.54248 6.34821 10.3019 8.78745 4.815 4.79475 4.79 3.98009 3.96113 2.92643 3.60798 2.21945
|
||||
threshold=1.3750000000000002 2.6550000000000007 166.34375000000003 0.34153389001132711 0.011242161620033652 2.2450000000000006 0.80827483213138251 -0.61249999999999971 0.5344873150105709 4.6500000000000012 23.062500000000004 40.833333333333343 23.550000000000004 6.0000000000000009 0.71663103786665172 1.7750000000000001 12.062500000000002 0.53723539037745083 24.803571428571434 1.925
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 2 14 4 13 6 7 -4 -3 11 -11 -10 -8 -1 -2 19 -13 18 -5 -16
|
||||
right_child=1 8 5 17 -6 -7 12 -9 9 10 -12 16 -14 -15 15 -17 -18 -19 -20 -21
|
||||
leaf_value=0.016987942399224069 0.0035814571364199662 -0.068907535589063609 0.01429912015371851 0.079098125792394905 -0.010575188509827679 -0.033022257030977734 0.059900457392998388 -0.008250813675967944 0.00010937594628987166 -0.040766144429216389 0.025542986379799158 -0.019382488279346841 0.013986965287345072 0.063856791784991249 -0.032605400222488659 -0.0094169329033003141 -0.05275351833756161 0.039347381597312048 0.041332957240490006 -0.064822733951046208
|
||||
leaf_weight=6.7410361915826815 6.703504994511607 6.0858818590641013 35.323686808347702 11.494772866368299 8.7060475498437864 8.6918699741363508 5.9581677168607703 58.758967056870461 9.3814801126718503 6.6312184035778037 8.6243968755006772 7.6371410340070751 9.4523572921752912 7.2466183453798294 7.3598874658346194 6.4097239673137656 22.335135906934738 10.743333801627157 6.2475921660661689 6.3931281566619873
|
||||
leaf_count=27 27 25 142 46 35 35 24 237 38 27 35 31 38 29 30 26 91 43 25 26
|
||||
internal_value=-0.000730438 -0.0109991 -0.00322902 0.0405507 0.0213801 0.00188154 0.0046523 0.000215644 -0.0295683 -0.0251842 -0.00327986 -0.0336755 0.0317385 0.0412694 -0.0257105 -0.0354492 -0.0442504 0.0558234 0.0657999 -0.0475817
|
||||
internal_weight=256.926 205.747 145.051 51.1794 22.6937 118.185 109.493 94.0827 60.6953 54.6094 15.2556 39.3538 15.4105 13.9877 26.8662 20.1627 29.9723 28.4857 17.7424 13.753
|
||||
internal_count=1037 832 585 205 91 476 441 379 247 222 62 160 62 56 109 82 122 114 71 56
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=4
|
||||
num_leaves=21
|
||||
num_cat=0
|
||||
split_feature=77 76 10 46 38 52 90 13 63 35 27 50 50 12 36 17 44 49 74 45
|
||||
split_gain=62.8093 17.2545 9.84388 8.65527 8.50818 8.94286 7.35721 7.1083 6.79729 7.71767 6.73811 6.55324 5.90783 5.04228 9.19595 4.75428 4.11592 2.77884 1.29074 0.102719
|
||||
threshold=1.3750000000000002 1.2250000000000003 80.937500000000014 0.34153389001132711 2.6904761904761911 26.562500000000004 0.19724305626860048 109.93750000000001 0.55212996425566885 6.2750000000000012 -0.020833333333333648 0.76335870360422431 0.74692579813599125 7.3125000000000009 -0.26190476190476181 6.5625000000000009 0.51258507460016867 0.73610958414604988 0.62250000000000016 -0.0013187490579748998
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 8 16 7 11 6 -6 12 9 -2 -7 -4 -1 14 -11 18 -3 -18 19 -5
|
||||
right_child=1 2 4 15 5 10 -8 -9 -10 13 -12 -13 -14 -15 -16 -17 17 -19 -20 -21
|
||||
leaf_value=0.0050560182108375287 0.01454555314741844 -0.0053774531433530308 0.0088078452630964664 0.076512274772428487 0.012372105311854016 0.04280070757761658 -0.015898578376521092 -0.014892135795344198 -0.057603106440945573 -0.013716056812372755 -2.743210612548636e-05 0.06049972094546327 0.054367671817810027 -0.0053464224261316716 -0.054858503781032956 0.031440123411904081 -0.024849777283710722 -0.063361316106082435 0.050431436557637821 0.069244124813442359
|
||||
leaf_weight=6.4694478660821977 7.8782515972852698 7.1609281748533276 10.659276410937311 6.4840846359729793 19.36934831738472 10.460701778531076 61.473121464252472 6.4756214618682852 10.231325536966322 16.168815225362774 13.415295347571371 6.2101935893297187 9.7394945174455625 11.545304596424101 18.798230275511745 9.9960098117589933 6.1246920526027662 5.871805265545845 5.9897889196872702 5.9808590859174755
|
||||
leaf_count=26 32 29 43 26 78 42 248 26 42 66 54 25 39 47 77 40 25 24 24 24
|
||||
internal_value=-0.0007041 -0.010581 -0.00272469 0.0389631 0.00147434 -0.00277256 -0.0091251 0.0205332 -0.0276919 -0.0220654 0.0187367 0.0278373 0.034686 -0.0282665 -0.0358342 0.0536577 -0.029375 -0.0436996 0.0656918 0.0730249
|
||||
internal_weight=256.503 205.367 140.745 51.1353 121.588 104.718 80.8425 22.6846 64.6219 54.3906 23.876 16.8695 16.2089 46.5124 34.967 28.4507 19.1574 11.9965 18.4547 12.4649
|
||||
internal_count=1037 832 568 205 490 422 326 91 264 222 96 68 65 190 143 114 78 49 74 50
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=5
|
||||
num_leaves=23
|
||||
num_cat=0
|
||||
split_feature=77 77 49 8 59 55 59 9 40 35 50 74 36 58 97 86 49 8 95 56 52 48
|
||||
split_gain=59.2137 16.5141 9.9144 11.7486 12.1383 8.99339 10.5611 8.66857 10.2694 8.14362 7.72674 7.72484 12.0149 7.4839 7.07919 6.69422 4.48655 4.69909 4.36454 2.6211 3.68984 2.53359
|
||||
threshold=1.3550000000000002 2.2550000000000003 0.75657876131221735 31.666666666666668 17.267857142857146 19.535714285714288 18.937500000000004 -2.6785714285714266 12.937500000000002 8.3875000000000011 0.73851279305095374 0.54575358851674649 0.066964285714285574 24.354166666666668 2.9821428571428616 1.7450000000000003 0.77175549922512843 65.151515151515156 173.36607142857147 40.38750000000001 20.937500000000004 0.0091659973707001029
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=10 2 3 -2 -5 -3 18 8 -8 11 -1 14 -13 -6 -4 -9 19 -18 21 20 -12 -7
|
||||
right_child=1 5 9 4 13 6 7 15 -10 -11 16 12 -14 -15 -16 -17 17 -19 -20 -21 -22 -23
|
||||
leaf_value=0.011681667207113649 0.033908823443212138 0.017536154054844273 0.021215365730383975 0.03251466321515277 -0.035698618619601191 -0.069217953298740698 0.040352083533208016 -0.037782244586161744 -0.02378355379185353 -0.016829989475423059 0.067034861961705272 0.039469073342078345 -0.028155083253979179 0.009683122942141572 0.064205028012414941 -0.003245990990143062 0.008774257662963655 0.054818660291170745 -0.016792700372560584 0.076628087701740399 0.026483773237715093 -0.038854738339966714
|
||||
leaf_weight=10.698686346411703 8.1725911200046522 9.060679689049719 26.58554495871067 6.4189013689756385 30.358332172036171 10.46654355525971 8.4112317115068453 24.458471849560738 7.6072484105825415 11.665961533784865 8.4696357846260089 6.9492753297090557 10.640220895409582 7.1914227455854407 7.9649787694215766 14.189488068222998 6.9667989164590818 7.2231719940900803 5.8842895776033393 9.7066867947578412 6.2319085150957099 7.58248247206211
|
||||
leaf_count=43 33 37 107 26 123 43 34 100 31 47 34 28 43 29 32 58 28 29 24 39 25 31
|
||||
internal_value=0.0024258 -0.0070979 0.00280729 -0.0101318 -0.0183177 -0.0201993 -0.0245493 -0.0148477 0.00989378 0.0133809 0.0417609 0.0201404 -0.00143807 -0.0270072 0.0311258 -0.0251024 0.0500983 0.0322124 -0.046709 0.0604964 0.0498455 -0.0564622
|
||||
internal_weight=252.905 203.608 115.947 52.1412 43.9687 87.6604 78.5998 54.6664 16.0185 63.806 49.2969 52.14 17.5895 37.5498 34.5505 38.648 38.5982 14.19 23.9333 24.4082 14.7015 18.049
|
||||
internal_count=1024 826 468 211 178 358 321 223 65 257 198 210 71 152 139 158 155 57 98 98 59 74
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=6
|
||||
num_leaves=23
|
||||
num_cat=0
|
||||
split_feature=76 78 27 46 86 11 55 29 65 47 50 46 18 61 81 36 44 9 13 43 81 44
|
||||
split_gain=54.7148 15.7289 10.7788 14.3867 9.58583 8.78499 8.46201 10.8596 9.81775 8.03411 7.677 7.45074 5.6669 7.59424 5.03116 4.82371 4.8014 4.00902 3.45566 2.28622 3.41567 0.00497756
|
||||
threshold=2.1950000000000007 0.62900904125363766 -2.3020833333333326 0.34016393531964906 2.1450000000000005 122.43750000000001 19.854166666666668 39.812500000000007 0.3770676732818225 0.36593380560885896 0.73563272044162131 0.35736791305409527 -3.1874999999999996 12.690476190476192 1.5150000000000003 0.25833333333333336 0.49644821746513607 -8.7121212121212164 117.43750000000001 0.50247426971013531 1.7450000000000003 0.53567202664757096
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 2 11 9 5 -5 -3 8 17 16 -2 15 -9 -14 -12 -1 -4 -8 19 21 -21 -16
|
||||
right_child=10 6 3 4 -6 -7 7 12 -10 -11 14 -13 13 -15 18 -17 -18 -19 -20 20 -22 -23
|
||||
leaf_value=0.070324761157363386 0.0084031851759769002 0.012486475233554927 -0.014274497772434848 0.02062654133633882 -0.032240672579085888 -0.029937743516828159 -0.020241102484534694 0.022406478756216045 0.0013966323082333749 0.0049975598343196606 0.016976514726108942 -0.0032581054169875008 0.011631640379196602 -0.033623046124736253 0.073425640497252462 0.021730508558563181 -0.050815423135628625 -0.053986984893246243 0.028244335395794146 0.069298036861864615 0.028035945730723436 0.075014904249462944
|
||||
leaf_weight=6.9250844269990939 9.2112289071083051 11.516124725341795 9.6139747798442858 48.390351086854935 7.907471075654029 6.2022370845079413 7.0930576324462846 7.9190109968185416 8.3109913170337659 11.12387755513191 6.4693672657012931 7.1976774334907523 9.5812086164951342 15.581949874758719 6.6890226900577598 6.1896365433931351 14.328032165765761 27.358511343598369 5.9492755085229865 6.9541786164045316 5.9617312103509903 5.9650087505578995
|
||||
leaf_count=28 37 47 39 195 32 25 29 32 34 45 26 29 39 64 27 25 58 113 24 28 24 24
|
||||
internal_value=0.00233149 -0.00659898 0.00293381 -0.00258517 0.00892003 0.014882 -0.0194618 -0.0243128 -0.0376256 -0.0230916 0.0411639 0.029443 -0.00710444 -0.0163917 0.0491075 0.0473902 -0.0361423 -0.0470392 0.0557024 0.062091 0.0502523 0.0741748
|
||||
internal_weight=252.439 205.239 117.878 97.5659 62.5001 54.5926 87.3609 75.8447 42.7626 35.0659 47.1998 20.3124 33.0822 25.1632 37.9886 13.1147 23.942 34.4516 31.5192 25.5699 12.9159 12.654
|
||||
internal_count=1024 834 476 394 252 220 358 311 176 142 190 82 135 103 153 53 97 142 127 103 52 51
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=7
|
||||
num_leaves=24
|
||||
num_cat=0
|
||||
split_feature=76 78 27 31 32 42 58 74 15 40 46 26 76 97 98 73 62 60 46 61 90 97 42
|
||||
split_gain=50.8947 11.0049 9.32122 10.5702 10.4712 9.27532 8.02666 7.19583 7.79066 6.93078 7.78095 6.76995 6.46148 6.98083 6.77168 6.35566 6.68601 5.47664 5.22172 4.56413 4.30903 3.83111 0.826657
|
||||
threshold=1.8750000000000002 0.70062845714719713 -9.1770833333333304 18.401785714285719 20.645833333333339 -2.774999999999999 26.401785714285719 0.63245614035087727 -0.43749999999999994 11.866071428571431 0.37542254586435625 6.7812500000000009 2.4650000000000003 -3.4241071428571384 -6.3184523809523787 0.55383792139841859 0.51117533329139231 13.35416666666667 0.35034659555724629 12.062500000000002 0.19829454556786633 0.28125000000000006 0.19761904761904778
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 2 -1 7 11 -6 12 8 -4 19 15 -5 14 -14 -2 -11 -17 18 -16 -3 -15 -19 -22
|
||||
right_child=6 9 3 4 5 -7 -8 -9 -10 10 -12 -13 13 20 17 16 -18 21 -20 -21 22 -23 -24
|
||||
leaf_value=0.036083213444415528 0.053277627847354556 -0.021782130288108433 -0.0091325820237097618 0.049355787080620245 0.039957936624339677 -0.010162507947377566 0.066494523434590488 0.007861663962773149 -0.056001702923737531 -0.05035447873268746 0.025153534242404262 -0.0002279551315397668 0.0074196720389386114 0.025979484890861368 -0.0040180493742294409 -0.035722871972219031 0.012557904583466907 -0.037148801270085199 0.040822377329808285 -0.065566429091563946 0.054687788523123476 0.0082770984108304652 0.074704189299082305
|
||||
leaf_weight=9.12299896776676 5.9481788575649253 6.5940756052732459 9.5768276453018242 10.393245324492456 6.4848928600549689 66.375781863927841 8.9054767787456495 7.6045115441083899 13.925430610775946 9.6051787436008471 6.321256920695304 7.647834599018096 7.4389976263046291 6.437974855303767 7.4326431751251283 8.6875483840703982 9.7281266301870328 5.9339717775583249 9.4233583807945234 9.0198750644922239 5.9406516849994642 5.9482765197753906 7.4301211833953857
|
||||
leaf_count=37 24 27 39 42 26 268 36 31 57 40 26 31 30 26 30 36 40 24 38 38 24 24 30
|
||||
internal_value=0.00224138 -0.00900344 -0.00291719 -0.00583339 0.00105391 -0.00570159 0.0309866 -0.0259598 -0.0369032 -0.0249795 -0.0149333 0.0283366 0.0258809 0.0404581 0.0144298 -0.0239765 -0.0102184 0.00638917 0.02105 -0.0470755 0.0528654 -0.0144085 0.0658109
|
||||
internal_weight=251.927 181.088 131.132 122.009 90.9018 72.8607 70.8397 31.1068 23.5023 49.9561 34.3421 18.0411 61.9342 27.2477 34.6864 28.0209 18.4157 28.7382 16.856 15.614 19.8087 11.8822 13.3708
|
||||
internal_count=1024 738 531 494 367 294 286 127 96 207 142 73 250 110 140 116 76 116 68 65 80 48 54
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=8
|
||||
num_leaves=21
|
||||
num_cat=0
|
||||
split_feature=76 78 27 10 18 88 65 58 48 76 75 50 55 57 45 61 52 16 37 64
|
||||
split_gain=47.0285 10.2297 8.57167 9.90629 8.49863 11.7596 8.93582 7.47628 6.70549 7.3988 6.88535 6.62498 6.53575 6.19036 7.25158 6.45563 5.31391 3.33594 2.78119 2.27511
|
||||
threshold=1.8750000000000002 0.70062845714719713 -9.1770833333333304 82.267857142857153 -4.3541666666666705 0.49939467312348679 0.34321545037197632 26.401785714285719 0.021174040484134952 2.2250000000000005 0.64837092731829593 0.73563272044162131 19.854166666666668 41.062500000000007 0.0031867569273384007 12.062500000000002 20.937500000000004 -6.3124999999999991 2.6125000000000003 0.36408812715877942
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 2 -1 17 5 -5 -6 8 9 10 -2 -11 -3 15 -15 -14 -13 -4 -19 -17
|
||||
right_child=7 12 3 4 6 -7 -8 -9 -10 11 -12 16 13 14 -16 19 -18 18 -20 -21
|
||||
leaf_value=0.034533388049788116 0.023208498381564548 0.010691617096435792 -0.0099350499244745989 -0.013374654384074001 -0.030791758397498405 0.036260912427308507 0.00098553848763632086 0.064191286864412847 0.055107303489570873 -0.0011209989712481605 -0.023658767436963126 0.066673274412926392 -0.0085936349487782514 0.018687811460451158 -0.034036012019189175 -0.064822499725983529 0.027488397354637369 -0.062556001891513391 -0.025921626974491768 -0.035460654646497967
|
||||
leaf_weight=9.1505375951528531 10.182640746235849 7.3519092053174964 8.031925320625307 12.611105665564539 19.997042715549469 19.363006785511971 48.494191810488701 8.8569523096084577 9.8766392171382886 7.7215904891490963 9.8834861069917661 8.600008040666582 6.5588535666465786 7.2764098048210171 9.7889975756406766 12.264306232333185 15.548548832535742 6.0707129538059217 7.3061331808567047 6.4392972588539115
|
||||
leaf_count=37 41 30 33 51 81 78 196 36 40 31 40 35 27 30 41 52 63 25 30 27
|
||||
internal_value=0.00215494 -0.00866469 -0.0028044 -0.00560779 -0.000343403 0.0166838 -0.00829232 0.029821 0.0248962 0.019151 0.000124224 0.0311307 -0.0241206 -0.0301671 -0.0115554 -0.0427397 0.0414433 -0.0303121 -0.0425471 -0.0547138
|
||||
internal_weight=251.374 180.704 131.025 121.874 100.465 31.9741 68.4912 70.6699 61.8129 51.9363 20.0661 31.8701 49.6798 42.3279 17.0654 25.2625 24.1486 21.4088 13.3768 18.7036
|
||||
internal_count=1024 738 531 494 406 129 277 286 250 210 81 129 207 177 71 106 98 88 55 79
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=9
|
||||
num_leaves=25
|
||||
num_cat=0
|
||||
split_feature=78 78 49 49 88 63 41 34 8 52 58 40 46 48 78 75 50 73 62 52 52 89 61 30
|
||||
split_gain=43.4968 9.51409 8.12139 9.24718 9.16177 10.1476 7.62239 8.2415 7.26498 6.99079 6.98605 6.58508 7.12245 6.22401 6.86036 6.34938 6.1593 5.87831 6.08977 5.0965 5.00399 4.68377 4.18568 2.8965
|
||||
threshold=0.44444541990764314 0.70062845714719713 0.75617247776303531 0.7645406242769327 0.53179766719418564 0.50671990539912803 11.35416666666667 6.322916666666667 57.738095238095248 21.387500000000003 26.401785714285719 11.866071428571431 0.37542254586435625 0.021174040484134952 0.37587731721902251 0.64837092731829593 0.73563272044162131 0.55383792139841859 0.51117533329139231 20.145833333333339 20.937500000000004 0.48209613271233459 12.062500000000002 0.14583333333333218
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=10 2 4 21 5 9 -5 -8 -7 -2 13 22 17 14 16 -16 -1 -13 -19 -6 -18 -4 -3 -21
|
||||
right_child=1 11 3 6 19 8 7 -9 -10 -11 -12 12 -14 -15 15 -17 20 18 -20 23 -22 -23 -24 -25
|
||||
leaf_value=-0.0010761821563351997 -0.0504521132857738 -0.020888576146344352 0.063381196536312773 0.031300149180180752 -0.0091861880185250058 0.032629739057098435 0.026755708854807866 -0.014960107933453847 -0.014669550109854696 0.0014514898694815139 0.062084955523258885 -0.047888703413600249 0.024824846695046907 0.053148590255660884 0.022272644170989585 -0.022746839774489575 0.064616726934294752 -0.033579092631573867 0.012687158247053376 -0.033188047394488003 0.026459678285886239 0.014128239830164315 -0.063091908329842183 -0.070867855155932621
|
||||
leaf_weight=7.7214295715093639 8.7155217230319995 6.5707600712776175 5.9724875986576063 10.170844271779059 7.820334509015086 14.327638953924181 9.4181414246559125 38.771787792444229 8.1518821418285352 7.9293809384107581 8.7924163937568647 9.4247248619794863 6.3216789960861197 9.8318816572427732 10.185996487736704 9.8688251972198469 8.5203529894351977 8.5675740987062472 9.7116009145975095 7.3497301340103132 15.507762670516966 6.3991550505161285 8.7900275439023954 5.8721687346696854
|
||||
leaf_count=31 36 27 24 41 32 58 38 157 33 32 36 40 26 40 41 40 35 36 40 30 63 26 38 24
|
||||
internal_value=0.00207304 -0.00834038 -0.00269623 0.00649292 -0.0134991 -0.00205203 -0.000166073 -0.00680725 0.0154773 -0.025726 0.0287296 -0.0233003 -0.0134864 0.0239715 0.018434 0.000118899 0.0300029 -0.0222285 -0.00899815 -0.0347829 0.0399901 0.0379054 -0.0450389 -0.0499226
|
||||
internal_weight=250.714 180.285 130.899 70.7324 60.1667 39.1244 58.3608 48.1899 22.4795 16.6449 70.4287 49.3864 34.0256 61.6362 51.8044 20.0548 31.7495 27.7039 18.2792 21.0422 24.0281 12.3716 15.3608 13.2219
|
||||
internal_count=1024 738 531 286 245 159 236 195 91 68 286 207 142 250 210 81 129 116 76 86 98 50 65 54
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=10
|
||||
num_leaves=17
|
||||
num_cat=0
|
||||
split_feature=77 77 46 27 54 57 10 43 97 43 40 54 36 76 74 37
|
||||
split_gain=38.196 8.66905 8.17628 11.664 9.48906 7.12349 6.31154 5.47718 7.96438 4.78449 4.32492 4.31373 3.38709 2.74196 2.35164 0.967464
|
||||
threshold=1.3550000000000002 3.2850000000000006 0.30594944598976864 -7.9791666666666661 21.145833333333339 39.267857142857146 82.267857142857153 0.517251622710314 -4.4062499999999991 0.53429184335543722 13.062500000000002 21.387500000000003 0.25833333333333336 1.6050000000000002 0.49258241758241766 4.354166666666667
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=7 2 13 11 6 -6 -5 8 -1 14 -9 -4 15 -2 -3 -10
|
||||
right_child=1 9 3 4 5 -7 -8 10 12 -11 -12 -13 -14 -15 -16 -17
|
||||
leaf_value=0.0010938350878875731 -0.056271458252691184 -0.059825392191432909 0.059788669423910978 -0.039005664596234592 0.013116780911492858 -0.0075331438797635181 -0.0060155405307781556 -0.0070478699384603017 0.069320208232492922 -0.0094715712429085195 0.037723823315383334 0.012087841826597247 0.032449698237748015 -0.017612173431570215 -0.028207255364314641 0.049213209433963845
|
||||
leaf_weight=6.4049855172634116 5.8483278751373273 7.5306213200092333 5.9356311261653882 18.997604206204414 43.460418909788132 69.426650807261467 18.137169808149338 8.3646507412195188 10.436919420957571 11.170329853892325 5.8781122714281073 6.2036552876234055 10.014484807848929 5.8936225026845932 7.5243068188428879 6.0471841692924491
|
||||
leaf_count=26 24 32 24 79 177 282 74 34 43 47 24 25 41 24 32 25
|
||||
internal_value=-0.000237742 -0.00786817 -0.00463523 -0.00230133 -0.00535295 0.000416875 -0.0228928 0.0321521 0.0411221 -0.0293062 0.0114298 0.0354117 0.0507973 -0.0368673 -0.044023 0.061944
|
||||
internal_weight=247.275 200.128 173.903 162.161 150.022 112.887 37.1348 47.1463 32.9036 26.2253 14.2428 12.1393 26.4986 11.742 15.0549 16.4841
|
||||
internal_count=1013 820 709 661 612 459 153 193 135 111 58 49 109 48 64 68
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=11
|
||||
num_leaves=17
|
||||
num_cat=0
|
||||
split_feature=77 77 46 86 10 15 15 82 23 59 38 40 31 28 26 76
|
||||
split_gain=35.3726 8.07222 7.58645 6.89209 6.39755 8.67145 7.68194 5.36328 4.76149 6.47719 8.94184 5.29839 3.97447 5.87629 4.03464 2.57596
|
||||
threshold=1.3550000000000002 3.2850000000000006 0.30594944598976864 2.1550000000000007 81.291666666666671 -3.3523809523809471 -7.5624999999999991 1.7550000000000001 1.5000000000000002 22.062500000000004 3.0625000000000004 13.550000000000002 21.550000000000004 41.937500000000007 7.2708333333333348 1.6050000000000002
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=8 2 15 4 5 -4 -6 -5 9 10 -1 -10 14 -14 -3 -2
|
||||
right_child=1 12 3 7 6 -7 -8 -9 11 -11 -12 -13 13 -15 -16 -17
|
||||
leaf_value=0.028732650584569908 -0.054519811278789007 -0.064491542130753823 0.014624725366248799 -0.0085976653870859041 -0.017414530959728048 -0.043244668184488785 0.0081240637328352395 -0.056808196775163779 0.027059814047174627 0.044509876383840984 -0.03008579237684399 0.070067154331982448 0.011727805368536705 -0.043007463941734977 -0.020379238714872804 -0.016944262076966699
|
||||
leaf_weight=10.437584459781652 5.7958323061466199 7.0121503621339816 6.5479190945625332 9.3588718920946103 22.927828788757324 11.280139639973639 105.82874263823032 6.0974729955196372 9.9562236517667788 11.169452026486395 6.8490484803915015 8.4930274337530118 7.1303854584693891 5.6052840352058411 6.2962067276239395 5.8811570256948471
|
||||
leaf_count=43 24 30 27 38 94 47 430 25 41 46 28 35 30 24 27 24
|
||||
internal_value=-0.000229245 -0.00756918 -0.0044558 -0.00221187 0.000466882 -0.0219903 0.00357638 -0.0276165 0.0310303 0.0207685 0.0054285 0.046858 -0.0283359 -0.0123625 -0.0436219 -0.0355948
|
||||
internal_weight=246.667 199.762 173.718 162.041 146.585 17.8281 128.757 15.4563 46.9053 28.4561 17.2866 18.4493 26.044 12.7357 13.3084 11.677
|
||||
internal_count=1013 820 709 661 598 74 524 63 193 117 71 76 111 54 57 48
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=12
|
||||
num_leaves=23
|
||||
num_cat=0
|
||||
split_feature=77 77 52 75 51 26 74 17 75 82 54 32 27 33 81 83 85 32 72 61 77 52
|
||||
split_gain=32.7624 7.68874 8.64181 8.25451 7.32214 6.8233 6.75593 6.44176 8.25947 8.69321 6.36363 7.17186 5.52164 5.23358 7.14313 7.28422 6.71979 4.33821 3.0057 2.88005 2.0103 1.97945
|
||||
threshold=1.3550000000000002 2.7750000000000004 26.437500000000004 0.36503968253968255 0.0031256509629944006 8.0104166666666696 0.53482673267326752 5.7142857142857162 0.37048387096774199 1.5250000000000001 21.387500000000003 23.401785714285719 0.041666666666666609 0.48750000000000077 1.5450000000000002 0.49675474226372424 7.0000000000000009 23.937500000000004 168.45500000000001 12.866071428571431 3.5750000000000006 20.937500000000004
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=13 2 7 17 -4 6 -5 8 -2 -10 -9 -12 -6 14 -1 16 -16 18 -3 -15 -20 -17
|
||||
right_child=1 3 4 5 12 -7 -8 10 9 -11 11 -13 -14 19 15 21 -18 -19 20 -21 -22 -23
|
||||
leaf_value=-0.013704229151213245 0.027450875173411476 -0.023064966047954667 -0.0059624168736026485 -0.038589566739523094 0.056326441355657071 0.030446921359459631 0.016883396721917278 -0.016350382639718393 0.013627075998421501 -0.021633517290006031 0.05028258642359236 0.0009173349354000002 0.0072316487440658916 0.068524301803199614 -0.018133517520923233 0.068692298622940004 0.040971194599499342 -0.0050561417900614513 -0.063950555049639521 0.032780066279364729 -0.033589087879592569 0.036867444471177346
|
||||
leaf_weight=7.3369961380958548 7.6220861226320258 8.4552565813064628 11.61829833686352 9.1469356268644351 8.95874175429344 7.0741493850946418 5.7027423083782187 10.210099041461946 13.296437010169027 70.522654443979263 8.4501094073057192 10.635228663682936 6.2034237682819358 6.9417874813079816 6.5973821580410021 6.2351381480693799 5.7690534591674805 6.4230835288763037 9.5591890364885312 7.5070647299289703 5.4950702935457221 6.273080438375473
|
||||
leaf_count=30 31 36 47 39 36 29 24 42 54 288 34 43 25 29 27 26 24 27 41 31 24 26
|
||||
internal_value=-0.000221457 -0.00728287 -0.00262557 -0.0205316 0.0179311 -0.00188419 -0.0172862 -0.00718521 -0.0124149 -0.01604 0.0091383 0.022774 0.0362399 0.0299509 0.0209788 0.0312089 0.00943936 -0.0341897 -0.0421493 0.049953 -0.0528681 0.0527316
|
||||
internal_weight=246.034 199.374 147.517 51.8564 26.7805 21.9238 14.8497 120.737 91.4412 83.8191 29.2954 19.0853 15.1622 46.6605 32.2117 24.8747 12.3664 29.9326 23.5095 14.4489 15.0543 12.5082
|
||||
internal_count=1013 820 600 220 108 92 63 492 373 342 119 77 61 193 133 103 51 128 101 60 65 52
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=13
|
||||
num_leaves=19
|
||||
num_cat=0
|
||||
split_feature=77 27 31 81 14 38 21 55 43 97 42 50 49 41 11 29 36 46
|
||||
split_gain=30.365 7.27107 9.52487 12.2996 9.0589 7.41691 9.90203 7.25389 5.08675 7.54296 5.07106 4.74587 4.44817 3.59305 3.39298 3.01366 2.9941 0.448837
|
||||
threshold=1.3550000000000002 -9.1770833333333304 19.267857142857146 1.7250000000000003 122.31250000000001 2.6904761904761911 0.083333333333333273 24.291666666666668 0.517251622710314 -3.4687499999999996 -1.0000000180025095e-35 0.74140914232826016 0.76659506707061065 13.937500000000002 82.225000000000009 31.645833333333339 0.25833333333333336 0.36329415664585835
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=8 10 3 13 5 6 -4 -7 9 -1 -2 -5 -10 15 -13 -3 17 -11
|
||||
right_child=1 2 4 11 -6 7 -8 -9 12 16 -12 14 -14 -15 -16 -17 -18 -19
|
||||
leaf_value=0.0041476899979937393 -0.0034999926541034228 -0.023850556255963173 0.062374335339732266 0.019436960650830851 -0.040563681465367818 -0.014906923971429929 -0.0074965637413479513 0.0059412375908438819 0.030706710820604975 0.065449418006224877 0.045123507823127261 -0.039381405838009649 -0.014252467415403541 -0.014092509822306006 -0.0010383997769504584 -0.056317419451033038 0.031129941687341473 0.051133321323921815
|
||||
leaf_weight=8.055756524205206 6.7893940508365631 6.2344330102205339 6.6629877835512143 9.740582004189493 9.6089727431535703 49.483073160052299 6.3268143832683563 58.00315548479557 7.2527140378952009 9.1974637508392352 6.9395729303359968 6.112829089164733 6.8431497812271118 6.5173709690570822 9.326295539736746 17.179670557379723 9.3787097781896573 5.6604025810956946
|
||||
leaf_count=33 28 26 27 40 40 203 26 237 30 39 28 26 28 27 39 73 39 24
|
||||
internal_value=-0.000213824 -0.00700969 -0.00909187 -0.0230288 -0.00318742 -0.000206347 0.028343 -0.00365656 0.0289287 0.03768 0.0210777 -0.00242612 0.00888028 -0.0403607 -0.0162196 -0.0476725 0.0488255 0.0599954
|
||||
internal_weight=245.313 198.925 185.196 55.1112 130.085 120.476 12.9898 107.486 46.3882 32.2923 13.729 25.1797 14.0959 29.9315 15.4391 23.4141 24.2366 14.8579
|
||||
internal_count=1013 820 764 231 533 493 53 440 193 135 56 105 58 126 65 99 102 63
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=14
|
||||
num_leaves=22
|
||||
num_cat=0
|
||||
split_feature=77 77 46 75 82 55 32 17 38 46 57 30 61 33 98 65 51 27 98 47 61
|
||||
split_gain=28.1527 7.12874 8.477 7.73926 6.50746 5.98775 7.72785 8.6859 7.74875 5.93269 5.06747 5.06776 4.95308 4.91574 6.80815 7.34665 4.31144 3.2933 2.95919 4.65978 2.76263
|
||||
threshold=1.3550000000000002 2.7750000000000004 0.31724837366825204 0.36503968253968255 1.6250000000000002 25.387500000000003 23.162500000000005 5.4375000000000009 4.5357142857142865 0.38268646215659091 43.267857142857146 1.616071428571429 12.937500000000002 0.48750000000000077 -7.4374999999999991 0.33749469440953833 -0.013613105968960247 -1.0000000180025095e-35 -3.8124999999999996 0.35584581233114326 12.866071428571431
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=13 2 17 9 -5 6 7 -4 -8 18 11 -7 -6 14 -1 -16 -17 -2 19 -3 -15
|
||||
right_child=1 3 5 4 12 10 8 -9 -10 -11 -12 -13 -14 20 15 16 -18 -19 -20 -21 -22
|
||||
leaf_value=0.049192658968065413 -0.0067623296175654806 -0.0059097661371454915 -0.0050944451331990034 -0.03421558742429949 0.036155385236969206 0.014169839641633519 -0.043477105235728328 0.028316035394997729 0.0010581675575233177 0.003886886786396461 -0.0086753300433371153 0.054580295048802033 -0.0096808854134481682 0.065934772628735538 -0.023746444878321241 0.051631222052607971 0.0076433857618228972 -0.040918404046466461 -0.058216504004969173 -0.052238605686860437 0.030637453650431135
|
||||
leaf_weight=8.7887725979089755 7.5061362385749844 6.7677073627710378 50.746787026524544 6.7268268465995815 7.4960924834012967 19.041952610015873 13.521969988942148 16.497406467795372 11.624812230467795 5.6693915277719489 10.139670595526693 6.716789945960044 7.5927126258611679 6.7980895787477476 7.8056097179651287 5.6656755805015573 9.6161311566829646 11.340498879551886 10.0033008903265 7.136577308177948 7.4203208535909653
|
||||
leaf_count=37 31 29 208 29 31 77 55 67 47 24 41 27 32 29 32 24 40 47 44 31 31
|
||||
internal_value=-0.000206417 -0.00674501 -0.00226535 -0.0195702 -0.00149628 0.00141462 -0.00397204 0.00310234 -0.0228894 -0.0329013 0.0152781 0.0247072 0.0130905 0.0279552 0.0192311 0.00782548 0.0239517 -0.0273149 -0.0416252 -0.0296887 0.0475138
|
||||
internal_weight=244.623 198.529 147.136 51.3926 21.8156 128.289 92.391 67.2442 25.1468 29.577 35.8984 25.7587 15.0888 46.0946 31.8762 23.0874 15.2818 18.8466 23.9076 13.9043 14.2184
|
||||
internal_count=1013 820 600 220 92 522 377 275 102 128 145 104 63 193 133 96 64 78 104 60 60
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=15
|
||||
num_leaves=23
|
||||
num_cat=0
|
||||
split_feature=77 78 26 31 57 17 43 18 88 36 86 43 43 83 26 73 29 74 16 55 49 68
|
||||
split_gain=30.6549 12.7043 7.98721 10.8099 12.9808 8.83958 9.66696 7.6233 8.30341 7.00237 7.84435 6.65312 6.67534 5.83636 5.52421 4.95185 4.66908 4.3802 2.29615 1.92109 0.661162 0.1601
|
||||
threshold=1.9950000000000003 0.33644919786096261 21.770833333333339 20.133928571428573 35.187500000000007 -5.0624999999999991 0.53857630126707601 1.8541666666666716 0.53866359200690284 0.82291666666666663 1.8550000000000002 0.45900318339543572 0.4818732325807798 0.47597575932472408 12.94791666666667 0.53851851851851873 32.38750000000001 0.45972972972972981 3.4375000000000004 22.133928571428573 0.73531540336276224 213.70833333333334
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 14 3 13 -5 17 -7 8 11 10 -9 -3 -13 -2 15 -1 -15 -6 -12 -17 -18 -21
|
||||
right_child=2 7 -4 4 5 6 -8 9 -10 -11 18 12 -14 16 -16 19 20 -19 -20 21 -22 -23
|
||||
leaf_value=0.010871073320075295 -0.01172908397497561 -0.046441338207332344 0.019672691938767486 0.046075447983197577 -0.061569404959666424 0.0074003860750678123 -0.03202888807961754 0.0051474644513716042 0.036884377615520249 -0.003400242373786107 0.074863846195407729 0.029393863715199694 -0.0074553467007233889 -0.02523852343490229 -0.00025978536454887422 0.034473210798287857 -0.04795709241166466 -0.021389238996026894 0.039593407313705566 0.06652884725665352 -0.0649082592606154 0.057420779027093437
|
||||
leaf_weight=5.6847298443317476 11.006925001740454 5.903722643852233 9.1704158484935743 6.0783775299787512 7.7922967374324825 29.886689186096191 14.913227543234823 7.632679730653769 8.1395289301872236 10.812797814607618 5.8884793967008573 9.226744741201399 53.322611838579178 10.414219856262209 5.7322616279125205 5.7362669706344631 5.4735058695077923 9.8010887205600721 5.9245722442865372 6.8773611485958082 11.245072126388548 5.6040449738502502
|
||||
leaf_count=24 47 25 38 25 33 123 62 31 33 44 24 38 218 44 24 24 24 42 24 29 48 24
|
||||
internal_value=-0.00100969 0.011833 -0.0161489 -0.0192302 -0.00972405 -0.0151601 -0.00572503 0.00540609 -0.00130932 0.0224046 0.0367534 -0.00585081 -0.00201967 -0.0362963 0.0350059 0.0434633 -0.0462625 -0.0391855 0.0571747 0.0536336 -0.0593586 0.0624394
|
||||
internal_weight=252.268 136.486 115.782 106.611 68.4717 62.3933 44.7999 106.851 76.5926 30.2585 19.4457 68.4531 62.5494 38.1397 29.6347 23.9024 27.1328 17.5934 11.8131 18.2177 16.7186 12.4814
|
||||
internal_count=1048 562 486 448 285 260 185 437 314 123 79 281 256 163 125 101 116 75 48 77 72 53
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=16
|
||||
num_leaves=17
|
||||
num_cat=0
|
||||
split_feature=76 76 33 62 60 32 55 73 17 64 98 25 97 51 64 46
|
||||
split_gain=28.3621 12.2502 8.00023 7.56697 8.78584 8.59503 10.2753 7.68395 7.41413 7.20524 7.00591 5.23347 2.9989 2.73623 2.05561 2.04323
|
||||
threshold=1.4350000000000003 2.4650000000000003 4.2946428571428585 0.47839506172839513 11.437500000000002 16.162500000000005 19.854166666666668 0.53494186046511638 -7.4374999999999991 0.32494646077353467 5.2142857142857162 7.713541666666667 3.7187500000000004 -0.004282105062106599 0.35502948224071251 0.3406679635524858
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 2 8 15 12 -6 -7 -8 -2 -10 -11 13 14 -3 -5 -1
|
||||
right_child=1 11 -4 4 5 6 7 -9 9 10 -12 -13 -14 -15 -16 -17
|
||||
leaf_value=-0.029356541192986724 0.037955112610862249 0.059695040893734667 0.043145015879701695 -0.032469843140688813 -0.046002450801952505 0.045093595797352712 -0.042281712918251578 -0.0013799114657325063 0.028081928029134246 -0.010286395434884021 0.025869758711122577 0.011074792109595291 -0.016203413481459306 0.029391271532808353 -0.066050254966225394 -0.059556082391998284
|
||||
leaf_weight=8.2328822761774045 8.3526985198259336 10.85255688428879 8.1492396891117078 6.0218881517648715 7.7615482956171027 6.8014763444662085 8.401523217558859 58.652556464076042 11.53392608463764 68.72949393093586 9.7969980090856534 10.286443859338759 7.4116774648427954 8.5023315101861971 5.6562657654285431 6.3488157242536536
|
||||
leaf_count=36 34 47 33 26 33 28 36 245 47 282 40 43 31 36 24 27
|
||||
internal_value=-0.000973738 0.0113848 0.00505797 -0.0155743 -0.0116748 -0.00596092 -0.00175292 -0.00650469 0.00190411 -0.00143945 -0.00577554 0.03413 -0.0361041 0.046383 -0.0487344 -0.0425053
|
||||
internal_weight=251.492 136.204 106.562 115.289 100.707 81.6171 73.8556 67.0541 98.4131 90.0604 78.5265 29.6413 19.0898 19.3549 11.6782 14.5817
|
||||
internal_count=1048 562 436 486 423 342 309 281 403 369 322 126 81 83 50 63
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=17
|
||||
num_leaves=22
|
||||
num_cat=0
|
||||
split_feature=76 76 33 26 31 57 43 97 60 18 96 88 18 83 26 39 73 7 55 45 68
|
||||
split_gain=26.2331 11.4465 7.36965 7.34122 10.0074 11.7565 7.00539 6.9993 6.88815 10.1637 8.77606 8.04063 5.85928 5.44514 5.13063 4.51191 4.39613 4.1986 1.8532 1.64881 0.156111
|
||||
threshold=1.4350000000000003 2.4650000000000003 4.2946428571428585 21.770833333333339 20.133928571428573 35.187500000000007 0.53857630126707601 4.7812500000000009 12.937500000000002 7.8125000000000009 227.02083333333337 0.50795806360821849 -4.2250000000000076 0.47597575932472408 12.94791666666667 -0.14642857142857144 0.53851851851851873 38.750000000000007 22.133928571428573 -0.022501766737042645 213.70833333333334
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 2 8 4 13 -6 7 -7 10 12 11 -2 -10 -1 16 -8 -3 -15 -18 -19 -20
|
||||
right_child=1 14 -4 -5 5 6 15 -9 9 -11 -12 -13 -14 17 -16 -17 18 19 20 -21 -22
|
||||
leaf_value=-0.010786926974099898 -0.010875891059966199 0.011190352480577235 0.041404682122757074 0.019321738062697564 0.044141656903284171 0.0050650165981334172 -0.0061370748531028195 -0.031081442136894771 0.0018664483503017856 0.037131975018793693 0.045073208415421694 0.031828378125081415 -0.026128862170482447 -0.019454829109928577 -0.00098318851507109848 -0.04740967911986986 0.032230447411375152 -0.036142993762053356 0.064020839936514432 -0.062063497067667478 0.054909341919742859
|
||||
leaf_weight=10.931022465229033 21.17684800922871 5.8557388037443223 8.1520167738199216 9.167889401316641 6.0991687178611746 30.766112342476845 6.2713132798671749 11.881261780858038 21.166444480419155 7.1458795964717856 10.812558114528654 10.578455105423926 27.506209731101993 7.7223432809114483 5.709469035267829 13.0707770884037 5.6604651659727123 5.5588853806257275 6.7105195671319944 13.371022954583166 5.4539281576871872
|
||||
leaf_count=47 87 25 33 38 25 128 26 50 87 29 44 43 113 33 24 56 24 24 29 59 24
|
||||
internal_value=-0.00093843 0.0109531 0.00485653 -0.0150137 -0.0179926 -0.00884725 -0.0140609 -0.00500514 0.00182825 -0.00741437 0.013948 0.00334993 -0.0139544 -0.034561 0.0330532 -0.0340278 0.0412595 -0.0443116 0.0511376 -0.0544518 0.0599357
|
||||
internal_weight=250.768 135.929 106.538 114.84 105.672 68.0886 61.9895 42.6474 98.3864 55.8185 42.5679 31.7553 48.6727 37.5833 29.3901 19.3421 23.6807 26.6523 17.8249 18.9299 12.1644
|
||||
internal_count=1048 562 436 486 448 285 260 178 403 229 174 130 200 163 126 82 102 116 77 83 53
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=18
|
||||
num_leaves=16
|
||||
num_cat=0
|
||||
split_feature=78 79 27 72 42 35 26 33 9 30 47 48 55 36 10
|
||||
split_gain=24.4334 9.31464 8.11491 7.61266 8.41054 8.43587 6.62119 7.15164 9.45035 5.28003 4.33177 4.03099 3.63878 2.72794 1.40327
|
||||
threshold=0.39285714285714285 0.25425685425685435 -9.1770833333333304 165.36681435533896 -3.4833333333333329 9.133928571428573 2.0104166666666674 0.48750000000000077 -37.987012987012982 -0.56249999999999989 0.37693449058527589 0.029982232931730853 20.062500000000004 0.19374999999999967 112.93750000000001
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=6 11 -3 10 -5 -6 14 8 -8 -10 12 13 -4 -2 -1
|
||||
right_child=1 2 3 4 5 -7 7 -9 9 -11 -12 -13 -14 -15 -16
|
||||
leaf_value=0.063369704073235666 -0.060192057816396222 0.028277022360530538 -0.016384609809915053 0.044115984475752157 -0.002478065592223366 -0.051567336834095104 0.039068426318397236 0.043825566156782063 0.011510346359810335 -0.030717352367799409 0.0045260873425609898 -0.0079952149779249845 -0.050175542136173798 -0.02646344151553905 0.03565855117598455
|
||||
leaf_weight=6.1944728344678861 7.4922285825014132 11.610384106636046 9.429924696683889 5.9105293601751319 126.33686858415604 5.8609887659549704 9.0260636359453184 10.688595429062842 9.03087270259857 9.9656709432601982 5.7869970500469199 8.0647390484809858 11.101787701249121 7.8635760098695755 5.5375995486974716
|
||||
leaf_count=27 34 47 40 24 521 24 39 46 38 41 24 36 48 35 24
|
||||
internal_value=-0.000905355 -0.00719526 -0.00404233 -0.00632444 -0.00256724 -0.00465444 0.0239656 0.0159876 0.00536936 -0.0106425 -0.0260405 -0.0308938 -0.0346558 -0.0429199 0.0502899
|
||||
internal_weight=249.901 199.458 176.037 164.427 138.108 132.198 50.4433 38.7112 28.0226 18.9965 26.3187 23.4205 20.5317 15.3558 11.7321
|
||||
internal_count=1048 833 728 681 569 545 215 164 118 79 112 105 88 69 51
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=19
|
||||
num_leaves=18
|
||||
num_cat=0
|
||||
split_feature=76 76 62 60 60 32 55 33 17 64 86 25 97 63 12 64 81
|
||||
split_gain=23.1068 9.98848 7.01309 7.92354 7.95102 8.25678 10.3579 6.71651 6.62639 6.59888 6.66891 4.82438 2.76159 2.56144 2.27179 1.91893 0.744688
|
||||
threshold=1.4350000000000003 2.4650000000000003 0.47839506172839513 11.437500000000002 15.225000000000001 16.162500000000005 19.854166666666668 4.2946428571428585 -7.4374999999999991 0.32494646077353467 2.1850000000000005 7.713541666666667 3.7187500000000004 0.4972961247664654 -0.52499999999999847 0.35502948224071251 1.675
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=2 7 14 12 5 -5 -7 8 -2 -10 -11 13 15 -3 -1 -4 -15
|
||||
right_child=1 11 3 4 -6 6 -8 -9 9 10 -12 -13 -14 16 -16 -17 -18
|
||||
leaf_value=-0.020188921856989679 0.035859126989343022 0.020130209700355124 -0.030330032249617687 -0.042500863549311367 -0.05090993952028721 0.051411801287769635 -0.0022541904826031737 0.03956776202840788 0.026837614548232368 -0.0020673320148814839 -0.04439110542553347 0.008982843264200761 -0.014705883428948159 0.04205717024579949 -0.053006940553933922 -0.063196273867381317 0.061135640318500738
|
||||
leaf_weight=5.4853323251008979 8.3379970937967283 5.4507957398891511 5.8967574089765566 7.1303204596042624 5.6088133603334418 6.3442998528480521 61.878040820360184 8.1366265416145307 11.526250436902044 71.76362943649292 6.4958909302949897 10.161600232124327 7.33974365890026 5.8689189553260785 8.7720085084438306 5.4874216467142105 7.4020354598760605
|
||||
leaf_count=24 34 24 26 31 24 26 261 33 47 295 27 43 31 26 39 24 33
|
||||
internal_value=-0.000872267 0.0103144 -0.0141404 -0.0103874 -0.00496412 -0.00154418 0.00273645 0.00464481 0.00174893 -0.00141873 -0.00558039 0.0311724 -0.0338375 0.0432163 -0.0403806 -0.0461723 0.0526984
|
||||
internal_weight=249.086 135.144 113.943 99.6854 80.9615 75.3527 68.2223 106.26 98.1238 89.7858 78.2595 28.8834 18.7239 18.7218 14.2573 11.3842 13.271
|
||||
internal_count=1048 562 486 423 342 318 287 436 403 369 322 126 81 83 63 50 59
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=20
|
||||
num_leaves=21
|
||||
num_cat=0
|
||||
split_feature=77 85 42 26 33 48 29 51 51 88 47 49 9 43 18 45 13 97 90 77
|
||||
split_gain=21.5323 9.97521 10.2057 13.3659 8.32088 7.657 7.45302 7.27519 7.64625 7.10753 6.89749 6.08818 4.68215 4.36106 4.27259 4.17883 3.76513 3.4333 0.930107 0.105107
|
||||
threshold=1.3050000000000004 -5.9999999999999991 1.0000000180025095e-35 9.0104166666666696 4.4375000000000009 0.059692514253813854 39.812500000000007 0.045203152003977003 0.023949864400422651 0.52433570113128136 0.36314281963351264 0.78248216953695626 -13.09523809523809 0.51328944240770225 -16.645833333333325 -0.0072528547698737485 85.062500000000014 1.0937500000000002 0.19841107122821819 3.1550000000000007
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=12 2 6 9 5 7 16 8 -3 15 -9 17 14 -14 -1 -4 -2 -8 -16 -18
|
||||
right_child=1 4 3 -5 -6 -7 11 10 -10 -11 -12 -13 13 -15 18 -17 19 -19 -20 -21
|
||||
leaf_value=0.013957264641046819 -0.027472132914840498 -0.0020934000360734932 0.022613724189803789 0.050832305755293579 0.037916740044630914 0.040588735510979898 0.027960766295875104 -0.003405364332683979 -0.031143520168862283 -0.05384418391600191 0.040275812485981034 -0.044330514984326927 0.030259337595429918 -0.015671661984799638 0.062645279427693656 -0.020045453140862098 -0.066399946636320761 -0.01585144878027565 0.043445571662247148 -0.058570658366565144
|
||||
leaf_weight=7.1345093548297873 8.8107999712228793 87.334490463137627 6.9631915688514745 6.7626315653324118 9.8303922265768033 6.9817947149276725 5.6945501416921633 11.879387885332109 17.382111921906471 5.3163712173700324 11.272227853536604 5.8104494661092749 7.708228871226309 5.7933596074581146 6.5536312311887803 7.778242379426958 5.6191599071025831 5.7529874593019485 10.513165161013601 5.361001193523407
|
||||
leaf_count=31 39 362 29 28 40 29 24 49 72 24 46 25 33 25 29 33 24 25 47 25
|
||||
internal_value=0.00160475 -0.00342443 -0.0165911 0.00220167 0.002388 -0.000201998 -0.0301955 -0.00242923 -0.00691549 -0.0141945 0.0178624 -0.0109833 0.0294231 0.0105509 0.0399517 0.000104827 -0.0469487 0.00594283 0.0508182 -0.0625773
|
||||
internal_weight=246.253 208.55 63.8694 26.8204 144.68 134.85 37.0489 127.868 104.717 20.0578 23.1516 17.258 37.7029 13.5016 24.2013 14.7414 19.791 11.4475 17.0668 10.9802
|
||||
internal_count=1039 874 276 114 598 558 162 529 434 86 95 74 165 58 107 62 88 49 76 49
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=21
|
||||
num_leaves=21
|
||||
num_cat=0
|
||||
split_feature=77 85 42 26 25 51 29 88 90 36 49 14 9 18 61 43 13 59 90 77
|
||||
split_gain=20.02 9.2622 9.52085 12.2814 7.88967 7.82256 7.04841 6.6998 6.53054 7.87452 5.68149 5.30824 4.42523 4.07084 4.07033 4.04241 3.62375 3.25279 0.898801 0.0879165
|
||||
threshold=1.3050000000000004 -5.9999999999999991 1.0000000180025095e-35 9.0104166666666696 1.8541666666666667 0.080538861184771923 39.812500000000007 0.52433570113128136 0.19969875129044842 -1.0875000000000001 0.78248216953695626 118.93750000000001 -13.09523809523809 -16.645833333333325 13.816666666666668 0.51328944240770225 85.062500000000014 23.535714285714288 0.19841107122821819 3.1550000000000007
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=12 2 6 7 -3 8 16 14 11 -10 17 -6 13 -1 -4 -14 -2 -8 -15 -18
|
||||
right_child=1 4 3 -5 5 -7 10 -9 9 -11 -12 -13 15 18 -16 -17 19 -19 -20 -21
|
||||
leaf_value=0.013447805314569216 -0.026577780088672762 0.034080468035460186 0.017293051569916593 0.048557724592127684 0.007825469574374657 0.040280280315047955 -0.016199932862450934 -0.052623204127446775 -0.047796064224015833 -0.0064826859346349495 -0.043040720837339654 -0.024990244369414837 0.029249467101270982 0.061303641269563863 -0.025610761209309305 -0.014994478156697251 -0.064751343135519002 0.026458734247185672 0.042272167151271647 -0.057522795432648433
|
||||
leaf_weight=7.10912762582302 8.7443605959415454 11.499579980969427 8.8282703608274513 6.7955194860696784 70.869819477200508 7.13891777396202 5.5701393187046069 5.2240076065063468 9.2205274105072004 37.017053097486496 5.7464897632598868 8.8745380789041501 7.6564510315656644 6.4316389113664689 5.9041270762681952 5.8124003708362579 5.5334616154432279 5.8780088722705841 10.375598087906836 5.2427052408456802
|
||||
leaf_count=31 39 47 37 28 292 29 24 24 39 154 25 37 33 29 25 25 24 25 47 25
|
||||
internal_value=0.00154432 -0.00329759 -0.0160366 0.0021131 0.00229294 -0.000453006 -0.029261 -0.0137021 -0.00276121 -0.0147212 -0.0105873 0.0041735 0.0284946 0.0388221 9.90026e-05 0.0101563 -0.0457098 0.005703 0.049555 -0.0612346
|
||||
internal_weight=245.473 208.088 63.4671 26.7519 144.62 133.121 36.7152 19.9564 125.982 46.2376 17.1946 79.7444 37.3852 23.9164 14.7324 13.4689 19.5205 11.4481 16.8072 10.7762
|
||||
internal_count=1039 874 276 114 598 551 162 86 522 193 74 329 165 107 62 58 88 49 76 49
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=22
|
||||
num_leaves=18
|
||||
num_cat=0
|
||||
split_feature=77 78 25 48 51 25 9 35 9 48 27 30 9 18 43 50 72
|
||||
split_gain=18.621 8.70394 7.91958 7.02031 6.21305 7.48529 6.53645 5.17821 7.11278 5.11576 6.50119 6.92842 4.18572 3.88527 3.74781 0.796232 0.173612
|
||||
threshold=1.3050000000000004 0.69515163347650677 1.7395833333333337 0.059692514253813854 0.043626967279394004 7.8802083333333348 8.333333333333341 6.6937500000000005 8.7121212121212164 -0.018202373884811395 0.98958333333333315 0.22500000000000142 -13.09523809523809 -16.645833333333325 0.51328944240770225 0.74854124348714812 225.04250000000002
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=12 2 -2 4 6 -6 -4 8 -3 -9 11 -11 13 -1 -14 -15 -17
|
||||
right_child=1 7 3 -5 5 -7 -8 9 -10 10 -12 -13 14 15 -16 16 -18
|
||||
leaf_value=0.012956313378187341 0.036677286509223014 0.027307971594871701 -0.0022506819015889979 0.032532340700343249 0.001223625287223337 0.048438323769350811 -0.026039625377917281 -0.053747110254001744 -0.024750955618137494 -0.0073465850093818175 0.015119548741830309 -0.055928592960106001 0.028278098346949592 0.036190897477542942 -0.014350124084243559 0.059344625765782907 0.049204905261110286
|
||||
leaf_weight=7.0841730237007132 9.6115598380565626 8.2046131193637866 86.82922887802124 9.458725377917288 19.787809953093532 7.3747483491897574 23.477235645055771 8.3150889277458173 8.6016630232334119 10.108060508966451 7.039140820503234 8.7735196799039823 7.6037576496601087 5.6597618609666887 5.8300840854644775 5.8845966756343824 4.9951938986778259
|
||||
leaf_count=31 39 35 359 39 81 30 97 39 39 45 31 40 33 25 25 27 24
|
||||
internal_value=0.00148631 -0.00317645 0.00150065 -0.0008005 -0.00309401 0.0140426 -0.00731384 -0.0175205 0.000663559 -0.026447 -0.0176894 -0.0299207 0.0276052 0.0377427 0.00977809 0.0483592 0.0546892
|
||||
internal_weight=244.639 207.581 156.539 146.928 137.469 27.1626 110.306 51.0421 16.8063 34.2358 25.9207 18.8816 37.0576 23.6237 13.4338 16.5396 10.8798
|
||||
internal_count=1039 874 645 606 567 111 456 229 74 155 116 85 165 107 58 76 51
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
end of trees
|
||||
|
||||
feature_importances:
|
||||
Column_77=25
|
||||
Column_46=15
|
||||
Column_76=15
|
||||
Column_97=14
|
||||
Column_43=13
|
||||
Column_50=13
|
||||
Column_49=12
|
||||
Column_27=11
|
||||
Column_78=11
|
||||
Column_9=10
|
||||
Column_26=10
|
||||
Column_51=10
|
||||
Column_55=10
|
||||
Column_61=10
|
||||
Column_17=9
|
||||
Column_18=8
|
||||
Column_25=8
|
||||
Column_36=8
|
||||
Column_42=8
|
||||
Column_52=8
|
||||
Column_74=8
|
||||
Column_75=8
|
||||
Column_81=8
|
||||
Column_33=7
|
||||
Column_40=7
|
||||
Column_48=7
|
||||
Column_73=7
|
||||
Column_86=7
|
||||
Column_10=6
|
||||
Column_31=6
|
||||
Column_32=6
|
||||
Column_44=6
|
||||
Column_57=6
|
||||
Column_88=6
|
||||
Column_13=5
|
||||
Column_29=5
|
||||
Column_35=5
|
||||
Column_60=5
|
||||
Column_64=5
|
||||
Column_85=5
|
||||
Column_90=5
|
||||
Column_15=4
|
||||
Column_23=4
|
||||
Column_30=4
|
||||
Column_37=4
|
||||
Column_38=4
|
||||
Column_41=4
|
||||
Column_45=4
|
||||
Column_47=4
|
||||
Column_54=4
|
||||
Column_58=4
|
||||
Column_59=4
|
||||
Column_62=4
|
||||
Column_65=4
|
||||
Column_83=4
|
||||
Column_95=4
|
||||
Column_98=4
|
||||
Column_8=3
|
||||
Column_12=3
|
||||
Column_63=3
|
||||
Column_72=3
|
||||
Column_82=3
|
||||
Column_7=2
|
||||
Column_11=2
|
||||
Column_14=2
|
||||
Column_16=2
|
||||
Column_56=2
|
||||
Column_68=2
|
||||
Column_21=1
|
||||
Column_22=1
|
||||
Column_28=1
|
||||
Column_34=1
|
||||
Column_39=1
|
||||
Column_53=1
|
||||
Column_69=1
|
||||
Column_79=1
|
||||
Column_89=1
|
||||
Column_96=1
|
||||
|
||||
parameters:
|
||||
[boosting: gbdt]
|
||||
[objective: binary]
|
||||
[metric: binary_logloss]
|
||||
[tree_learner: serial]
|
||||
[device_type: cpu]
|
||||
[data_sample_strategy: bagging]
|
||||
[data: ]
|
||||
[valid: ]
|
||||
[num_iterations: 1200]
|
||||
[learning_rate: 0.04]
|
||||
[num_leaves: 31]
|
||||
[num_threads: 4]
|
||||
[seed: 42]
|
||||
[deterministic: 0]
|
||||
[force_col_wise: 0]
|
||||
[force_row_wise: 0]
|
||||
[histogram_pool_size: -1]
|
||||
[max_depth: 6]
|
||||
[min_data_in_leaf: 24]
|
||||
[min_sum_hessian_in_leaf: 0.001]
|
||||
[bagging_fraction: 0.84]
|
||||
[pos_bagging_fraction: 1]
|
||||
[neg_bagging_fraction: 1]
|
||||
[bagging_freq: 5]
|
||||
[bagging_seed: 400]
|
||||
[bagging_by_query: 0]
|
||||
[feature_fraction: 0.82]
|
||||
[feature_fraction_bynode: 1]
|
||||
[feature_fraction_seed: 30056]
|
||||
[extra_trees: 0]
|
||||
[extra_seed: 12879]
|
||||
[early_stopping_round: 0]
|
||||
[early_stopping_min_delta: 0]
|
||||
[first_metric_only: 0]
|
||||
[max_delta_step: 0]
|
||||
[lambda_l1: 0]
|
||||
[lambda_l2: 0]
|
||||
[linear_lambda: 0]
|
||||
[min_gain_to_split: 0]
|
||||
[drop_rate: 0.1]
|
||||
[max_drop: 50]
|
||||
[skip_drop: 0.5]
|
||||
[xgboost_dart_mode: 0]
|
||||
[uniform_drop: 0]
|
||||
[drop_seed: 17869]
|
||||
[top_rate: 0.2]
|
||||
[other_rate: 0.1]
|
||||
[min_data_per_group: 100]
|
||||
[max_cat_threshold: 32]
|
||||
[cat_l2: 10]
|
||||
[cat_smooth: 10]
|
||||
[max_cat_to_onehot: 4]
|
||||
[top_k: 20]
|
||||
[monotone_constraints: ]
|
||||
[monotone_constraints_method: basic]
|
||||
[monotone_penalty: 0]
|
||||
[feature_contri: ]
|
||||
[forcedsplits_filename: ]
|
||||
[refit_decay_rate: 0.9]
|
||||
[cegb_tradeoff: 1]
|
||||
[cegb_penalty_split: 0]
|
||||
[cegb_penalty_feature_lazy: ]
|
||||
[cegb_penalty_feature_coupled: ]
|
||||
[path_smooth: 0]
|
||||
[interaction_constraints: ]
|
||||
[verbosity: -1]
|
||||
[saved_feature_importance_type: 0]
|
||||
[use_quantized_grad: 0]
|
||||
[num_grad_quant_bins: 4]
|
||||
[quant_train_renew_leaf: 0]
|
||||
[stochastic_rounding: 1]
|
||||
[linear_tree: 0]
|
||||
[max_bin: 255]
|
||||
[max_bin_by_feature: ]
|
||||
[min_data_in_bin: 3]
|
||||
[bin_construct_sample_cnt: 200000]
|
||||
[data_random_seed: 175]
|
||||
[is_enable_sparse: 1]
|
||||
[enable_bundle: 1]
|
||||
[use_missing: 1]
|
||||
[zero_as_missing: 0]
|
||||
[feature_pre_filter: 1]
|
||||
[pre_partition: 0]
|
||||
[two_round: 0]
|
||||
[header: 0]
|
||||
[label_column: ]
|
||||
[weight_column: ]
|
||||
[group_column: ]
|
||||
[ignore_column: ]
|
||||
[categorical_feature: ]
|
||||
[forcedbins_filename: ]
|
||||
[precise_float_parser: 0]
|
||||
[parser_config_file: ]
|
||||
[objective_seed: 16083]
|
||||
[num_class: 1]
|
||||
[is_unbalance: 0]
|
||||
[scale_pos_weight: 1]
|
||||
[sigmoid: 1]
|
||||
[boost_from_average: 1]
|
||||
[reg_sqrt: 0]
|
||||
[alpha: 0.9]
|
||||
[fair_c: 1]
|
||||
[poisson_max_delta_step: 0.7]
|
||||
[tweedie_variance_power: 1.5]
|
||||
[lambdarank_truncation_level: 30]
|
||||
[lambdarank_norm: 1]
|
||||
[label_gain: ]
|
||||
[lambdarank_position_bias_regularization: 0]
|
||||
[eval_at: ]
|
||||
[multi_error_top_k: 1]
|
||||
[auc_mu_weights: ]
|
||||
[num_machines: 1]
|
||||
[local_listen_port: 12400]
|
||||
[time_out: 120]
|
||||
[machine_list_filename: ]
|
||||
[machines: ]
|
||||
[gpu_platform_id: -1]
|
||||
[gpu_device_id: -1]
|
||||
[gpu_use_dp: 0]
|
||||
[num_gpu: 1]
|
||||
|
||||
end of parameters
|
||||
|
||||
pandas_categorical:null
|
||||
@@ -0,0 +1,790 @@
|
||||
tree
|
||||
version=v4
|
||||
num_class=1
|
||||
num_tree_per_iteration=1
|
||||
label_index=0
|
||||
max_feature_idx=98
|
||||
objective=binary sigmoid:1
|
||||
feature_names=Column_0 Column_1 Column_2 Column_3 Column_4 Column_5 Column_6 Column_7 Column_8 Column_9 Column_10 Column_11 Column_12 Column_13 Column_14 Column_15 Column_16 Column_17 Column_18 Column_19 Column_20 Column_21 Column_22 Column_23 Column_24 Column_25 Column_26 Column_27 Column_28 Column_29 Column_30 Column_31 Column_32 Column_33 Column_34 Column_35 Column_36 Column_37 Column_38 Column_39 Column_40 Column_41 Column_42 Column_43 Column_44 Column_45 Column_46 Column_47 Column_48 Column_49 Column_50 Column_51 Column_52 Column_53 Column_54 Column_55 Column_56 Column_57 Column_58 Column_59 Column_60 Column_61 Column_62 Column_63 Column_64 Column_65 Column_66 Column_67 Column_68 Column_69 Column_70 Column_71 Column_72 Column_73 Column_74 Column_75 Column_76 Column_77 Column_78 Column_79 Column_80 Column_81 Column_82 Column_83 Column_84 Column_85 Column_86 Column_87 Column_88 Column_89 Column_90 Column_91 Column_92 Column_93 Column_94 Column_95 Column_96 Column_97 Column_98
|
||||
feature_infos=none none none none none none none [0:100] [0:100] [-100:100] [54:128.875] [61:138] [-41.333333333333329:24] [61:130.125] [59:133] [-50:34.916666666666671] [-28.666666666666657:32.5] [-24.5:27] [-35.5:31.833333333333329] [0:1] [0:1] [-1:1] [0:12] [0:12] [-12:10] [0.91666666666666663:186.1875] [0.85416666666666663:185.08333333333337] [-179.02083333333334:184.16666666666663] [20:54] [22:56] [-21:15.833333333333332] [11:34.5] [9:36] [-15:13.333333333333332] [2:13] [3:13] [-8:5.5] [0.875:9] [0:12] [-9:4.875] [7:22] [8:23] [-10:10] [0.41025641025641019:0.67647058823529416] [0.39730639730639727:0.76744186046511631] [-0.3174418604651163:0.22647058823529409] [0.20419254658385089:0.4825000000000001] [0.1875:0.54166666666666663] [-0.22171945701357459:0.17923280423280419] [0.47826086956521741:0.87898457080200498] [0.42857142857142849:0.88636363636363635] [-0.29647435897435898:0.38962801311591622] [11:39.666666666666664] [15.5:40] [10:31.875] [11.5:34] [15:56] [17:57] [12:32.125] [10:38] [8:20] [7:22] [0.40000000000000002:0.69047619047619047] [0.3902439024390244:0.71739130434782605] [0.23753976670201479:0.51851851851851849] [0.1707317073170731:0.54545454545454541] [0:8] [0:1] [120:295] [-38:36] [0:1] [0:1] [140.39130434782609:233.09999999999999] [0:1] [0:1] [0:1] [1.05:4.1100000000000003] [1.05:4.1299999999999999] [0.2034883720930232:0.79729729729729726] [0.20270270270270269:0.79651162790697683] [145.5:257.5] [1.3:7.7599999999999998] [1.4099999999999999:7.5599999999999996] [0.1789976133651551:0.75977653631284914] [0.24022346368715081:0.82100238663484482] [-15.5:12.5] [1.3:2.7200000000000002] [1.1799999999999999:2.7799999999999998] [0.36298076923076922:0.64953271028037374] [0.35046728971962621:0.63701923076923073] [0.1918833727344364:0.21555133935749429] none none none none [132:250.6875] [126.25:256.625] [-63.5:88.6875] [-35.5:27.5]
|
||||
tree_sizes=1917 2132 1711 1718 2135 1820 1814 2034 1817 1826 2446 2567 2567 2150 2344 1601 1499 1400 990 1399 1731 1721 2040 1829 1725 1721 1504 1717 1516 1931
|
||||
|
||||
Tree=0
|
||||
num_leaves=17
|
||||
num_cat=0
|
||||
split_feature=87 40 77 86 73 40 95 12 17 82 74 40 55 71 81 53
|
||||
split_gain=15.1457 13.2046 13.5042 11.2099 13.4357 11.4245 10.2245 9.8615 9.24196 7.11979 6.96376 9.0178 6.02258 5.9784 5.26359 4.73348
|
||||
threshold=1.5850000000000002 11.267857142857144 1.7850000000000004 1.7450000000000003 0.5344873150105709 11.79166666666667 228.96875000000003 0.14583333333333573 -3.8124999999999996 1.905 0.63245614035087727 15.401785714285717 22.645833333333339 0.22500000000000003 1.7350000000000001 23.437500000000004
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=15 8 6 4 -4 -6 10 -8 -2 13 11 -3 -12 -5 -10 -1
|
||||
right_child=1 2 3 9 5 -7 7 -9 14 -11 12 -13 -14 -15 -16 -17
|
||||
leaf_value=-0.097004150398104866 -0.032826856509161513 -0.0043502309061410754 0.01431771893192204 -0.091075161948939082 -0.0036225823237434548 -0.050018228661574106 -0.029276859267249353 -0.092899466087143945 -0.061043546543381851 -0.031637052746051886 -0.012858122023405583 -0.051389190877909906 -0.063112770479224672 -0.058465725478527159 -0.099324189356474121 -0.059651523168360317
|
||||
leaf_weight=10.494576394557955 9.2452220618724805 41.228692978620529 7.2462551295757285 13.49302679300308 9.9948346614837629 56.47081583738327 9.7449637949466723 6.4966425299644461 11.494059860706331 9.9948346614837629 6.4966425299644461 7.7459968626499167 9.2452220618724805 26.986053586006165 11.494059860706328 11.244188994169233
|
||||
leaf_count=42 37 165 29 54 40 226 39 26 46 40 26 31 37 108 46 45
|
||||
internal_value=-0.0457273 -0.0428007 -0.0390611 -0.0473474 -0.0374027 -0.0430414 -0.0263504 -0.0547259 -0.0666008 -0.0618705 -0.0192291 -0.0117901 -0.0423728 -0.0693355 -0.0801839 -0.0776838
|
||||
internal_weight=259.116 237.377 205.144 124.186 73.7119 66.4657 80.9582 16.2416 32.2333 50.4739 64.7166 48.9747 15.7419 40.4791 22.9881 21.7388
|
||||
internal_count=1037 950 821 497 295 266 324 65 129 202 259 196 63 162 92 87
|
||||
is_linear=0
|
||||
shrinkage=1
|
||||
|
||||
|
||||
Tree=1
|
||||
num_leaves=19
|
||||
num_cat=0
|
||||
split_feature=87 40 75 64 30 36 33 63 36 17 39 47 17 64 15 30 11 25
|
||||
split_gain=13.9729 12.1763 14.362 13.4541 10.6931 12.6392 11.2985 10.0326 8.61679 8.52999 8.49866 8.85202 6.83713 6.53966 4.98946 6.17658 4.17697 6.76785
|
||||
threshold=1.5850000000000002 11.267857142857144 0.4095454545454546 0.35791741982096692 -3.850000000000001 -0.13125000000000001 2.8660714285714275 0.50088993919059788 -0.17708333333333343 -3.8124999999999996 0.4702380952380954 0.34444905543815357 -7.8374999999999977 0.33306045819490598 -4.4732142857142838 -2.0744047619047588 90.687500000000014 3.9114583333333335
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=16 9 3 12 5 -4 8 -8 -6 -2 11 -5 -3 -14 -11 -16 -1 -18
|
||||
right_child=1 2 4 10 6 -7 7 -9 -10 14 -12 -13 13 -15 15 -17 17 -19
|
||||
leaf_value=-0.057851344720467247 0.012127031371442692 -0.065070317857066393 -0.046234344564243093 -0.034302041708384759 0.0097538558374562559 0.015919168977055184 -0.044150175296378553 0.012931828545225675 0.032691802732617729 -0.063902755868288641 0.044060792595361865 0.025258066535241798 0.0038131771685337061 -0.035910700877364692 0.0058413187436127693 -0.043222266889543948 -0.041738488387342523 0.012479643131747724
|
||||
leaf_weight=6.4850978553295162 9.2475084811449033 6.2444083094596854 9.9929635822772997 8.2424890547990817 47.478157848119736 10.994390726089476 8.24348331987858 12.243208199739454 58.476858302950859 6.238056123256686 8.4939699023962003 7.743343085050582 11.740369915962221 15.236236929893492 7.2386563420295742 9.484264671802519 8.9887140691280347 6.2415239214897147
|
||||
leaf_count=26 37 25 40 33 190 44 33 49 234 25 34 31 47 61 29 38 36 25
|
||||
internal_value=-0.00025478 0.00255523 0.0061453 -0.0107729 0.0127667 -0.0136747 0.0171556 -0.010037 0.0224134 -0.0203093 0.011728 -0.00545185 -0.0273532 -0.0186226 -0.033373 -0.0219847 -0.0309668 -0.0195193
|
||||
internal_weight=259.054 237.338 205.13 57.7008 147.429 20.9874 126.442 20.4867 105.955 32.2085 24.4798 15.9858 33.221 26.9766 22.961 16.7229 21.7153 15.2302
|
||||
internal_count=1037 950 821 231 590 84 506 82 424 129 98 64 133 108 92 67 87 61
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=2
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=87 79 86 40 80 29 95 46 61 76 27 42 51 53
|
||||
split_gain=12.8995 11.4587 12.6562 11.6114 9.74256 9.00377 8.40775 12.7519 9.12686 9.48882 6.86723 5.78581 4.37714 4.21145
|
||||
threshold=1.5850000000000002 0.47526524006473664 1.425 11.437500000000002 159.00000000000003 40.312500000000007 228.96875000000003 0.35543310268494094 12.225000000000001 2.6550000000000007 -13.968749999999998 -0.61249999999999971 -0.024362426215342146 23.437500000000004
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=13 2 5 -3 -4 -2 8 -8 12 -10 -6 -12 -5 -1
|
||||
right_child=1 3 4 6 10 -7 7 -9 9 -11 11 -13 -14 -15
|
||||
leaf_value=-0.048059265540188849 0.06036588533776735 -0.030072544599413102 0.030990903589822205 0.026560568681132761 -0.051938571878065741 0.0015271190921610995 0.033770613746158619 -0.039908770322941725 0.026584849929801698 -0.017941657017813275 -0.018812929986079977 -0.0011568414139026314 0.070670836690303579 -0.012775033151309585
|
||||
leaf_weight=10.448318898677828 7.7465823441743877 8.4830420017242414 9.4921096563339216 5.9989815205335608 6.2402391731739035 8.9906333237886411 6.7452828139066687 8.4876060187816602 35.739069759845734 9.7458354085683805 52.919037282466888 67.668311461806297 8.9985922873020154 11.229458034038542
|
||||
leaf_count=42 31 34 38 24 25 36 27 34 143 39 212 271 36 45
|
||||
internal_value=-0.000245346 0.00245334 -0.00406662 0.0143054 -0.00809702 0.0287598 0.0192774 -0.00728277 0.0259668 0.0170444 -0.0110225 -0.00890511 0.0530269 -0.0297814
|
||||
internal_weight=258.933 237.255 153.057 84.1984 136.32 16.7372 75.7154 15.2329 60.4825 45.4849 126.828 120.587 14.9976 21.6778
|
||||
internal_count=1037 950 613 337 546 67 303 61 242 182 508 483 60 87
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=3
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=87 79 33 88 72 60 95 12 15 52 14 30 65 53
|
||||
split_gain=11.9137 10.5659 11.7467 12.0731 10.7663 8.68363 8.59661 13.4701 7.47172 9.03668 6.50335 5.61483 4.0183 3.91278
|
||||
threshold=1.5850000000000002 0.47526524006473664 5.4583333333333348 0.57686355548965829 164.17133924969747 13.133928571428573 228.96875000000003 0.14583333333333573 -3.4499999999999953 18.645833333333339 82.937500000000014 -2.0119047619047588 0.34544552124032007 23.437500000000004
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=13 2 3 4 10 -6 8 -8 -3 -10 -2 -11 -5 -1
|
||||
right_child=1 6 -4 12 5 -7 7 -9 9 11 -12 -13 -14 -15
|
||||
leaf_value=-0.046319955261748876 0.037448948902587834 -0.0088284694187992091 -0.057678550882037501 0.059995488602543341 -0.019214067900304579 0.0032492412381891801 0.018976181265562397 -0.053115091799964251 -0.018039785729919627 0.013382514787688078 -0.0088821137810651384 0.041942269689724629 0.013681980687733714 -0.012271034609450827
|
||||
leaf_weight=10.407825455069544 13.739773482084276 11.737559467554091 6.2349889278411856 5.9960742145776731 66.626564428210258 46.927813142538071 9.7444915026426333 7.219090297818183 6.492861956357955 16.732775956392288 7.4898996800184241 32.22684919834137 5.9935915470123291 11.223179444670675
|
||||
leaf_count=42 55 47 25 24 267 188 39 29 26 67 30 129 24 45
|
||||
internal_value=-0.000235931 0.00235599 -0.00390538 -0.00162108 -0.00504269 -0.00993082 0.0137405 -0.0117033 0.0201643 0.0263011 0.0211032 0.0321815 0.0368435 -0.0286538
|
||||
internal_weight=258.793 237.162 153.009 146.774 134.784 113.554 84.1536 16.9636 67.19 55.4525 21.2297 48.9596 11.9897 21.631
|
||||
internal_count=1037 950 613 588 540 455 337 68 269 222 85 196 48 87
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=4
|
||||
num_leaves=19
|
||||
num_cat=0
|
||||
split_feature=87 40 75 64 80 66 15 15 61 64 88 17 95 39 81 63 59 33
|
||||
split_gain=11.0085 10.3158 11.8546 12.6055 10.3939 11.5844 9.67074 10.8155 9.0703 7.92497 7.91682 7.84355 6.80908 5.80185 4.70535 3.79881 3.16026 2.99516
|
||||
threshold=1.5850000000000002 11.267857142857144 0.4095454545454546 0.35791741982096692 165.00000000000003 1.0000000180025095e-35 -9.1071428571428523 -3.725000000000001 12.690476190476192 0.33306045819490598 0.51341535239840341 -3.8124999999999996 225.46875000000003 0.33333333333333331 1.7350000000000001 0.4972961247664654 21.775000000000002 -1.0000000180025095e-35
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=15 11 3 8 5 17 12 -8 -3 -10 -5 -2 -6 -12 -13 -1 -17 -4
|
||||
right_child=1 2 4 10 6 -7 7 -9 9 -11 13 14 -14 -15 -16 16 -18 -19
|
||||
leaf_value=-0.00040554144175779727 0.012283500305906152 -0.058979450937986018 0.078347880748130691 -0.01274589698312884 0.010508267343845815 0.0014292549509687211 -0.030425427623467446 0.0073372268956942068 0.014443127755364691 -0.032407221257139643 0.0078896532861558941 -0.013267174082330087 0.063584791906166116 0.060934134607739728 -0.049547993082610223 -0.054100670196884616 -0.017902921750126199 0.042409147472346274
|
||||
leaf_weight=5.95706579089165 9.2431617081165296 9.4672348201274854 5.9933577626943579 11.224958077073099 7.9917045235633832 10.236197039484976 13.982299685478209 91.852746203541756 9.9857960343360919 13.705725058913229 6.9919697046279889 11.458065599203112 7.4933722615242004 6.2467137277126312 11.419983848929403 8.6485812216997129 6.9683696925640097 9.7404364198446292
|
||||
leaf_count=24 37 38 24 45 32 41 56 368 40 55 28 46 30 25 46 35 28 39
|
||||
internal_value=-0.000227018 0.00226278 0.00556662 -0.00981529 0.0115843 0.0345507 0.00666807 0.00234825 -0.0258849 -0.0126602 0.0119659 -0.0188136 0.0361925 0.0329189 -0.0313774 -0.0275824 -0.037949 0.056099
|
||||
internal_weight=258.608 237.034 204.913 57.6224 147.29 25.97 121.32 105.835 33.1588 23.6915 24.4636 32.1212 15.4851 13.2387 22.878 21.574 15.617 15.7338
|
||||
internal_count=1037 950 821 231 590 104 486 424 133 95 98 129 62 53 92 87 63 63
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=5
|
||||
num_leaves=16
|
||||
num_cat=0
|
||||
split_feature=33 88 55 55 17 54 73 52 77 17 41 97 29 62 40
|
||||
split_gain=15.351 13.8158 14.9232 12.5185 11.4912 11.3671 8.05265 14.2571 9.45676 11.7484 10.6395 4.12203 3.9043 3.69138 2.1447
|
||||
threshold=5.4583333333333348 0.54438855792157115 24.062500000000004 21.387500000000003 -9.4642857142857064 26.312500000000004 0.53494186046511638 25.387500000000003 2.0850000000000004 3.0625000000000004 12.775000000000002 0.7544642857142777 35.562500000000007 0.5081756463571766 12.803571428571431
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 4 3 -3 11 13 7 12 9 -8 -10 -1 -6 -4 -5
|
||||
right_child=-2 2 5 14 6 -7 8 -9 10 -11 -12 -13 -14 -15 -16
|
||||
leaf_value=-0.019289747362067435 -0.049291431039616411 0.0079530049109373772 -0.0023167773542189392 0.049152011733235204 0.016609632632338327 0.040013888589411864 0.013951234712260528 -0.0188765863308245 -0.039607282005252709 -0.012366184034748687 -0.0024271531554327401 -0.063622619627354979 0.056973143291589742 -0.043118301029727156 0.079727872448937878
|
||||
leaf_weight=6.9703347086906415 10.684137105941771 11.479128912091257 6.2386636584997204 7.7240137755870801 6.2296538949012819 5.9881500154733649 54.543849483132362 10.221605300903319 23.669574245810509 54.01922245323658 25.669763669371605 6.4709125310182571 9.9719207286834699 8.2252761721611005 6.9943185448646545
|
||||
leaf_count=28 43 46 25 31 25 24 219 41 95 217 103 26 40 33 28
|
||||
internal_value=-0.00235947 -0.000307937 0.0192731 0.0392628 -0.00492675 -0.00633208 -0.00232303 0.018115 -0.0057431 0.000856114 -0.0202636 -0.0406326 0.041453 -0.0255196 0.063682
|
||||
internal_weight=255.101 244.416 46.6496 26.1975 197.767 20.4521 184.326 26.4232 157.902 108.563 49.3393 13.4412 16.2016 14.4639 14.7183
|
||||
internal_count=1024 981 187 105 794 82 740 106 634 436 198 54 65 58 59
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=6
|
||||
num_leaves=16
|
||||
num_cat=0
|
||||
split_feature=60 62 73 75 86 9 86 68 9 75 55 17 12 53 82
|
||||
split_gain=10.7843 10.6542 9.72935 12.6928 12.2359 16.8146 10.0524 12.101 8.66024 8.63902 8.35887 7.20778 6.43585 4.33555 3.6338
|
||||
threshold=15.225000000000001 0.4697988539447821 0.53158244680851074 0.64508064516129038 1.7450000000000003 -8.3333333333333233 1.4450000000000001 207.25000000000003 -37.987012987012982 0.61348484848484863 24.291666666666668 -2.7083333333333353 3.2678571428571463 22.387500000000003 1.8350000000000002
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 11 4 6 -3 -6 7 -4 -5 -8 13 -1 -10 -2 -13
|
||||
right_child=10 2 3 8 5 -7 9 -9 12 -11 -12 14 -14 -15 -16
|
||||
leaf_value=-0.014255420135607853 0.023310237479360944 0.060688099681206698 0.047635605094340575 -0.022508370276938035 0.03702793647430791 -0.038866757036159119 -0.017236539926759407 -0.026749878847924364 0.026448512176737455 -0.054467609884275854 -0.007621975235864803 0.052597905023238606 -0.014975239364821315 0.067546521320461253 0.012566451021375784
|
||||
leaf_weight=6.2201643437147167 8.2134438902139681 6.9778638184070614 7.975793316960333 11.688230097293852 9.2276621013879812 9.4580885171890223 113.23386019468307 6.2343348562717438 27.121811106801033 10.934748843312262 8.4812200069427472 9.2266680598258954 7.7061219811439505 6.2366772443056098 5.9792356044054022
|
||||
leaf_count=25 33 28 32 47 37 38 455 25 109 44 34 37 31 25 24
|
||||
internal_value=-0.00226718 -0.00485385 -0.00758834 -0.0107917 0.0154908 -0.00138736 -0.0168681 0.0150009 0.00728451 -0.0205152 0.0239009 0.0220185 0.017283 0.0424026 0.0368568
|
||||
internal_weight=254.916 231.985 210.559 184.895 25.6636 18.6858 138.379 14.2101 46.5162 124.169 22.9313 21.4261 34.8279 14.4501 15.2059
|
||||
internal_count=1024 932 846 743 103 75 556 57 187 499 92 86 140 58 61
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=7
|
||||
num_leaves=18
|
||||
num_cat=0
|
||||
split_feature=89 55 54 8 43 96 98 40 90 55 37 97 49 80 65 97 62
|
||||
split_gain=10.4246 11.2262 10.7967 9.95568 11.3883 8.90539 8.65133 12.0582 11.6055 8.30861 8.22307 8.46564 6.54104 5.03193 4.78896 4.43026 4.16993
|
||||
threshold=0.45561144207842896 24.062500000000004 26.387500000000003 31.666666666666668 0.45725155221122971 175.77083333333337 -6.2083333333333348 11.162500000000001 0.19645540169586981 21.387500000000003 2.3541666666666674 1.7812500000000002 0.71927164205124738 212.00000000000003 0.35922103320833165 1.0312500000000002 0.50486813817504161
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 9 16 5 -5 -2 10 14 -9 -1 11 -6 -12 -7 -8 -11 -3
|
||||
right_child=3 2 -4 4 6 13 7 8 -10 15 12 -13 -14 -15 -16 -17 -18
|
||||
leaf_value=0.0056796349867628713 0.0048498008748587788 0.0010948092386472363 0.038285447553195809 0.03821577456306921 0.045277568585778402 -0.075491676385977521 -0.0073319216517205438 -0.028247542141635638 0.013897886134041438 0.075581548081493269 -0.059494968657228614 -0.018282852479456907 -0.017519335700108753 -0.030147268708022427 -0.056245855074949898 0.032607041143986076 -0.041584861624048911
|
||||
leaf_weight=11.720010221004488 8.2118753790855425 5.9827445000410107 5.9779978692531577 10.210737749934195 6.474472910165785 5.959468916058543 5.9542931914329511 12.197263866662977 73.146875634789467 6.4630497843027106 6.7146378159523001 6.953786626458168 51.473725467920303 11.417679086327551 6.9297584295272827 9.4506065398454648 9.4453652203083021
|
||||
leaf_count=47 33 24 24 41 26 24 24 49 294 26 27 28 207 46 28 38 38
|
||||
internal_value=-0.00217862 0.0143933 -0.00735136 -0.00613051 -0.00281263 -0.0294766 -0.00527918 0.00242922 0.00787452 0.0312376 -0.0158519 0.012363 -0.0223631 -0.0456981 -0.0336406 0.0500604 -0.0250345
|
||||
internal_weight=254.684 49.0398 21.4061 205.645 180.056 25.589 169.845 98.2282 85.3441 27.6337 71.6166 13.4283 58.1884 17.3771 12.8841 15.9137 15.4281
|
||||
internal_count=1024 197 86 827 724 103 683 395 343 111 288 54 234 70 52 64 62
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=8
|
||||
num_leaves=16
|
||||
num_cat=0
|
||||
split_feature=33 88 55 55 17 54 27 62 59 60 11 86 27 15 39
|
||||
split_gain=14.0335 11.5183 12.3907 10.6473 9.85957 9.50449 8.91174 9.57804 8.94568 7.22924 4.21001 3.63151 3.58218 2.92798 2.2831
|
||||
threshold=5.4583333333333348 0.54438855792157115 24.062500000000004 21.535714285714288 -9.4642857142857064 26.312500000000004 -7.9791666666666661 0.47163958632520975 19.162500000000005 15.225000000000001 114.68750000000001 1.885 -0.06770833333333344 -1.0000000180025095e-35 -0.11249999999999999
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 4 3 12 11 10 8 13 -6 -9 -4 -1 -3 -8 -5
|
||||
right_child=-2 2 5 14 6 -7 7 9 -10 -11 -12 -13 -14 -15 -16
|
||||
leaf_value=-0.017506959694980919 -0.047093096159148848 -0.013030607518081373 -0.0050211407760301866 0.041861979165786371 -0.0054470000340854632 0.036839736021523913 0.0084047253587021115 -0.0053865642716596834 -0.068322690853454923 0.024130364791092217 -0.048932153004977219 -0.059267051521897846 0.029896871945299459 0.042249585564822965 0.074866216000504499
|
||||
leaf_weight=6.9434050768613798 10.625629082322119 5.9767347723245621 8.4471292942762393 5.9510868340730667 8.7058147490024549 5.9711179584264746 6.7232442200183895 137.19698540866375 6.1981060206890097 14.698398649692534 5.9571634531021109 6.405640184879303 6.4850186705589312 10.441370263695715 7.6831773668527585
|
||||
leaf_count=28 43 24 34 24 35 24 27 552 25 59 24 26 26 42 31
|
||||
internal_value=-0.00209418 -0.000132847 0.017783 0.0360337 -0.0043524 -0.00559187 -0.00194377 0.000670223 -0.0315952 -0.00253031 -0.0231814 -0.0375459 0.00930858 0.0289928 0.0604605
|
||||
internal_weight=254.41 243.784 46.4714 26.096 197.313 20.3754 183.964 169.06 14.9039 151.895 14.4043 13.349 12.4618 17.1646 13.6343
|
||||
internal_count=1024 981 187 105 794 82 740 680 60 611 58 54 50 69 55
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=9
|
||||
num_leaves=16
|
||||
num_cat=0
|
||||
split_feature=33 88 55 55 54 8 43 98 37 50 96 80 11 38 39
|
||||
split_gain=13.0015 10.6389 11.4582 9.90804 8.77538 8.77091 9.15232 8.52658 12.059 8.78208 8.37899 5.06162 3.90198 3.4714 2.1433
|
||||
threshold=5.4583333333333348 0.54438855792157115 24.062500000000004 21.535714285714288 26.312500000000004 31.666666666666668 0.45725155221122971 -6.2083333333333348 3.0625000000000004 0.77107730652069451 175.77083333333337 212.00000000000003 114.68750000000001 3.0625000000000004 -0.11249999999999999
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 5 3 13 12 10 -7 9 -9 -8 -1 -12 -4 -3 -5
|
||||
right_child=-2 2 4 14 -6 6 7 8 -10 -11 11 -13 -14 -15 -16
|
||||
leaf_value=0.0073464817079793203 -0.045435105019414168 -0.011342813791158599 -0.0048216877875364326 0.040315188023492481 0.035433511001246025 0.035818050318993391 -0.0019489232180927723 -0.01869102791191958 0.013448791274373359 -0.030954345333154155 -0.072293858788171919 -0.026294701048497598 -0.047141285128800781 0.030922033964920961 0.072368292940394013
|
||||
leaf_weight=7.9585580676794079 10.573872312903402 6.4793082326650637 8.4447395205497759 5.9326046109199524 5.9599837511777869 10.172822326421736 40.264701783657074 25.803554385900497 67.645555391907692 28.539862290024757 5.9069683849811581 10.872051388025282 5.9365375787019721 5.9781518280506134 7.6314401328563672
|
||||
leaf_count=32 43 26 34 24 24 41 162 104 272 115 24 44 24 24 31
|
||||
internal_value=-0.0020131 -0.00012773 0.0171133 0.0346948 -0.00537778 -0.00418193 -0.000986385 -0.00329391 0.00457421 -0.0139803 -0.0264556 -0.0424885 -0.0222911 0.00893946 0.058349
|
||||
internal_weight=254.101 243.527 46.3628 26.0215 20.3413 197.164 172.426 162.254 93.4491 68.8046 24.7376 16.779 14.3813 12.4575 13.564
|
||||
internal_count=1024 981 187 105 82 794 694 653 376 277 100 68 58 50 55
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=10
|
||||
num_leaves=22
|
||||
num_cat=0
|
||||
split_feature=75 87 95 14 11 36 58 58 8 58 40 54 31 17 32 95 54 90 30 18 46
|
||||
split_gain=10.0606 13.2442 13.2354 7.28198 9.27874 9.24179 8.65882 8.65277 7.77526 9.89423 8.92987 8.43675 7.69243 6.47202 5.91656 6.71204 5.41747 5.66364 4.6817 4.44902 4.42172
|
||||
threshold=0.58007633587786267 1.5850000000000002 229.28125000000003 117.31250000000001 115.18750000000001 0.59375000000000011 24.803571428571434 24.354166666666668 42.261904761904766 17.062500000000004 13.35416666666667 20.062500000000004 24.562500000000004 5.4375000000000009 20.145833333333339 220.46875000000003 20.775000000000002 0.19788825188533934 0.60416666666666796 -4.3541666666666705 0.35905136104149266
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 18 13 4 8 7 19 -5 11 -10 -11 -1 -13 16 -15 -16 -3 -18 -2 -6 -4
|
||||
right_child=1 2 20 5 6 -7 -8 -9 9 10 -12 12 -14 14 15 -17 17 -19 -20 -21 -22
|
||||
leaf_value=0.016260010087575247 -0.00081200744809965017 0.009281923181331737 0.0032509448060061706 -0.025493613615531322 -0.074632317496072326 0.049617586690472636 0.0010470202171259946 0.03284274215952835 -0.029153960626547033 0.0012674975757078009 0.038103679086545161 -0.037991918317932111 0.0028048182430481369 0.031585379031769001 -0.038777627179031486 0.020611308349097066 0.013481360235318833 0.054225087511587569 -0.04021991897983087 -0.030101771519433589 -0.04216852229361847
|
||||
leaf_weight=9.4118207991123182 10.595016002655031 9.211836025118826 7.6921787559986097 10.383948802948003 6.9398950040340432 7.9301784783601752 7.663398250937461 6.6885293126106253 11.645323514938353 37.428400799632072 14.651625692844389 25.72077003121376 10.378917679190634 10.19868426024914 6.2205115109682065 5.9642188549041748 7.1979017257690456 22.59254564344883 8.8545045703649503 7.4363927543163308 6.1885804086923599
|
||||
leaf_count=38 43 37 31 42 28 32 31 27 47 151 59 104 42 41 25 24 29 91 36 30 25
|
||||
internal_value=0.000384931 0.0106717 0.0182752 -0.00584956 -0.00961775 0.0139354 -0.0332929 -0.00263903 -0.00484104 0.00417752 0.0116306 -0.0174689 -0.0262626 0.0262515 0.00910688 -0.00970775 0.0360908 0.0443807 -0.0187527 -0.0515981 -0.0169988
|
||||
internal_weight=250.995 94.716 75.2665 156.279 131.277 25.0027 22.0397 17.0725 109.237 63.7254 52.08 45.5115 36.0997 61.3857 22.3834 12.1847 39.0023 29.7904 19.4495 14.3763 13.8808
|
||||
internal_count=1013 382 303 631 530 101 89 69 441 257 210 184 146 247 90 49 157 120 79 58 56
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=11
|
||||
num_leaves=23
|
||||
num_cat=0
|
||||
split_feature=46 34 52 26 15 25 38 25 38 45 51 29 31 85 45 97 77 48 50 89 35 15
|
||||
split_gain=8.75369 11.3656 14.889 8.46246 7.41306 7.3335 8.41222 8.91166 7.82615 7.37315 7.16896 7.00775 6.83601 6.4889 6.43695 6.32157 6.2568 5.61316 5.35895 4.7914 4.5558 2.75508
|
||||
threshold=0.38135121729750621 6.8875000000000011 22.775000000000002 7.1406250000000009 -7.5624999999999991 2.0520833333333335 2.6904761904761911 13.989583333333334 3.0625000000000004 0.026346315240645454 0.014637337657707453 41.401785714285722 17.354166666666668 -2.9999999999999996 -0.03868421822005879 -3.0312499999999996 1.7550000000000001 0.050581642342345606 0.78802449932964647 0.44915558749550849 7.4226190476190483 3.7857142857142851
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 5 3 15 20 16 9 8 -8 12 11 18 -7 -5 -6 -3 -1 -12 -2 -16 -4 -17
|
||||
right_child=10 2 4 13 14 6 7 -9 -10 -11 17 -13 -14 -15 19 21 -18 -19 -20 -21 -22 -23
|
||||
leaf_value=-0.0054894704946171517 0.0015186659314490275 0.012422457986275524 0.045080183146636456 -0.016969101137900868 -0.040094377162748857 0.019099728305053802 0.028826737553279212 0.048811854341376415 -0.010159784240800534 -0.052368685379215477 -0.057749287922771578 0.032139891068220787 -0.025625732807283655 0.02903959599951628 0.028393278510576315 0.063703565821716004 -0.055469403457085856 -0.0080914581224991559 -0.047345864671010984 -0.006544736689285552 0.00014063060048828728 0.032655129091640721
|
||||
leaf_weight=7.6778917312622097 9.1392136514186877 8.1994599252939242 8.1893729716539365 8.9037519246339816 7.9227678179740897 9.4139631241560036 11.599057033658026 7.4388175308704367 28.433443784713745 7.6398711800575247 9.4042548984289152 6.4537621885538092 13.044003814458845 10.920071274042128 7.412743777036666 14.361867994070055 8.3833764642477018 5.9441701322793952 5.9151509106159201 41.112754985690117 6.4537106007337561 6.7087357491254798
|
||||
leaf_count=31 37 33 33 36 32 38 47 30 115 31 38 26 53 44 30 58 34 24 24 166 26 27
|
||||
internal_value=0.000369477 0.00347289 0.0116128 0.0285547 -8.68246e-05 -0.0069756 -0.00188168 0.00860705 0.00113621 -0.0184248 -0.0176344 -0.00273177 -0.00687765 0.00837506 -0.00666555 0.0422218 -0.0315771 -0.0385177 -0.0176812 -0.00120761 0.0252738 0.053818
|
||||
internal_weight=250.672 213.816 120.185 49.0939 71.0914 93.6304 77.5692 47.4713 40.0325 30.0978 36.8566 21.5081 22.458 19.8238 56.4483 29.2701 16.0613 15.3484 15.0544 48.5255 14.6431 21.0706
|
||||
internal_count=1013 864 485 198 287 379 314 192 162 122 149 87 91 80 228 118 65 62 61 196 59 85
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=12
|
||||
num_leaves=23
|
||||
num_cat=0
|
||||
split_feature=75 60 11 14 27 80 97 64 82 74 61 28 51 77 17 18 26 73 77 29 17 40
|
||||
split_gain=9.16434 11.0589 9.72885 16.5355 11.7419 10.0895 9.63264 9.40553 6.81505 6.89416 6.69885 7.87536 6.2381 5.78129 6.43908 7.23032 5.6961 5.53399 5.51269 7.0191 3.79526 2.70316
|
||||
threshold=0.40070422535211275 12.937500000000002 115.68750000000001 116.31250000000001 -0.9583333333333337 166.00000000000003 7.8229166666666581 0.35791741982096692 2.3750000000000004 0.68460526315789483 11.312500000000002 43.669642857142868 -0.00055870543754424991 2.4850000000000008 2.6875000000000004 11.267857142857148 3.0104166666666674 0.54977477477477488 3.5300000000000007 41.937500000000007 -3.0624999999999996 13.062500000000002
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=7 8 4 17 5 -3 16 13 9 10 -2 -12 -7 -1 15 21 -6 -4 19 20 -9 -15
|
||||
right_child=1 2 3 -5 6 12 -8 18 -10 -11 11 -13 -14 14 -16 -17 -18 -19 -20 -21 -22 -23
|
||||
leaf_value=-0.054673857447064877 0.031426635667739504 0.027443307507820841 -0.0071693293359852243 0.038925915934784942 0.050420363552369514 -0.055974948859624327 -0.015254363709585871 -0.00096046171680576385 -0.04185499138480625 -0.033145360136453486 -0.0080473225873038019 0.03965940078614745 0.00028714853340948784 -0.057615503467417908 0.017966063373814029 0.0078546298795259799 0.022851321189234875 -0.052721746825391283 0.037758480535269187 0.02828748068011893 -0.046251148347414762 -0.023260226786483273
|
||||
leaf_weight=8.6124199628829938 10.635522291064261 9.4400324672460574 8.4194629490375572 8.1747892647981626 24.48761148750782 6.1720191240310669 6.4190221130847922 5.9309897720813787 7.6241378188133231 10.108165323734282 52.924647763371468 6.1832769215106955 6.4464221149682981 8.5726141929626518 6.4193207621574393 6.403973162174224 23.496750026941299 8.652211129665373 6.4387345761060706 6.4527361392974845 5.9104709476232546 6.4002304375171652
|
||||
leaf_count=35 43 38 34 33 99 25 26 24 31 41 214 25 26 35 26 26 95 35 26 26 24 26
|
||||
internal_value=0.000354686 0.00470563 0.0136746 -0.00785498 0.0207833 -0.00383352 0.0307644 -0.013108 -0.00572256 -0.00227267 0.00220181 -0.00305672 -0.0272322 -0.0260387 -0.0171663 -0.0277163 0.0369205 -0.0302561 0.00592675 -0.00527659 -0.0235666 -0.0429301
|
||||
internal_weight=250.326 189.184 101.708 25.2465 76.4619 22.0585 54.4034 61.1415 87.4758 79.8516 69.7434 59.1079 12.6184 36.4086 27.7961 21.3768 47.9844 17.0717 24.7329 18.2942 11.8415 14.9728
|
||||
internal_count=1013 765 411 102 309 89 220 248 354 323 282 239 51 148 113 87 194 69 100 74 48 61
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=13
|
||||
num_leaves=19
|
||||
num_cat=0
|
||||
split_feature=75 87 95 39 88 58 95 57 51 48 58 32 63 24 83 55 97 82
|
||||
split_gain=8.61924 11.4411 11.6705 8.28079 6.48928 6.06533 8.08152 14.8066 8.57749 8.46636 7.40152 6.03526 6.41459 8.82896 6.7274 5.49578 4.98309 3.77165
|
||||
threshold=0.58007633587786267 1.5850000000000002 229.28125000000003 -0.08571428571428534 0.57686355548965829 16.437500000000004 171.09375000000003 38.687500000000007 -0.016939089932731747 -0.033449118040512087 27.387500000000003 17.062500000000004 0.48324752364570916 -1.0000000180025095e-35 0.52167307176691413 21.937500000000004 3.2187500000000004 1.6850000000000003
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=4 15 11 -4 5 -1 8 9 -7 -8 -9 -3 13 -13 -14 -2 -17 -10
|
||||
right_child=1 2 3 -5 -6 6 7 10 17 -11 -12 12 14 -15 -16 16 -18 -19
|
||||
leaf_value=-0.037191415145792946 -0.049107836359544479 0.057010861992900341 0.011593508527805042 -0.050683110628723223 0.024122659092291082 -0.013516688468798235 0.0070009506232722408 -0.0048723531932253544 0.015903788969995617 -0.045659362164178104 0.041619476315647989 -0.040633960200292647 0.017460987223942896 0.023949802445685821 0.054172151049742406 0.018705900435433812 -0.030600473896497015 0.053826338845108651
|
||||
leaf_weight=10.358175262808798 6.0601196587085751 7.9332039654254904 7.6448924392461777 6.1759819239377958 11.035771340131758 11.594122901558878 6.1791122853755978 69.580003947019577 10.684715464711191 23.321105942130089 5.9471079260110846 5.9447444081306458 28.258737400174141 7.8707564771175367 11.133399918675421 7.4004974067211133 5.8894641548395157 6.9099283069372168
|
||||
leaf_count=42 25 32 31 25 45 47 25 282 43 95 24 24 114 32 45 30 24 28
|
||||
internal_value=0.000340314 0.00988207 0.0169604 -0.0162354 -0.00544273 -0.00769954 -0.0054235 -0.0105979 0.0131951 -0.0346291 -0.00121152 0.0244643 0.0196117 -0.00384028 0.0278367 -0.0175393 -0.00314428 0.0307971
|
||||
internal_weight=249.922 94.3118 74.9617 13.8209 155.61 144.574 134.216 105.027 29.1888 29.5002 75.5271 61.1408 53.2076 13.8155 39.3921 19.3501 13.29 17.5946
|
||||
internal_count=1013 382 303 56 631 586 544 426 118 120 306 247 215 56 159 79 54 71
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=14
|
||||
num_leaves=21
|
||||
num_cat=0
|
||||
split_feature=75 79 28 27 14 60 31 69 47 53 31 75 36 41 56 46 49 46 77 74
|
||||
split_gain=8.04256 13.9131 8.58147 7.46144 10.122 9.11597 10.0351 11.5688 9.90082 8.37289 8.31104 8.15738 7.02093 6.34993 5.18118 6.69615 5.11523 3.3449 3.22441 5.46203
|
||||
threshold=0.6718233349078887 0.6464920911223796 41.687500000000007 -2.3020833333333326 91.062500000000014 12.937500000000002 25.803571428571434 1.7083333333333337 0.37218684558476361 25.812500000000004 17.354166666666668 0.3980198019801981 -0.26190476190476181 12.775000000000002 40.062500000000007 0.37106571647400344 0.76572220694348403 0.35494543794947025 1.4850000000000001 0.49543388429752072
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=3 18 12 4 13 6 14 -8 10 -10 -7 -12 -3 -1 15 -5 -16 -6 19 -2
|
||||
right_child=1 2 -4 5 17 8 7 -9 9 -11 11 -13 -14 -15 16 -17 -18 -19 -20 -21
|
||||
leaf_value=-0.019711473750247656 0.056394721780793459 -0.014592747921604247 -0.045243698355551337 0.0010942635313375348 -0.026082382729623245 0.059046481579424839 0.042087030208466859 -0.025395196611899901 -0.0380827165006535 0.012578046288920864 -0.012429649451340766 0.020979301067090405 0.03089449715034762 0.032768838321915822 -0.061831820327718327 -0.032477187860471046 -0.018967296365290393 -0.062065664493685058 0.060078921525471347 0.0051456786696745154
|
||||
leaf_weight=8.626388296484949 5.9154238402843493 10.863973572850233 6.1918527483940116 30.794128924608231 9.0536976456642169 6.6757322102785102 11.092378646135332 6.415662854909896 11.35324338078499 9.6619694828987104 16.008781462907791 43.380531519651413 10.852814644575117 6.4449361413717261 6.8486602157354382 13.751119881868361 12.741591066122053 7.6055790930986396 7.6382495164871207 7.6053119897842407
|
||||
leaf_count=35 24 44 25 125 37 27 45 26 46 39 65 176 44 26 28 56 52 31 31 31
|
||||
internal_value=0.000326808 0.0148417 -0.00370442 -0.00322617 -0.0210218 0.000120512 -0.00948172 0.0173588 0.00912325 -0.0147909 0.0167303 0.0119737 0.00813919 0.00273063 -0.0168088 -0.00926924 -0.0339525 -0.0425101 0.0393039 0.0275675
|
||||
internal_weight=249.522 49.0676 27.9086 200.454 31.7306 168.724 81.6435 17.508 87.0803 21.0152 66.065 59.3893 21.7168 15.0713 64.1355 44.5452 19.5903 16.6593 21.159 13.5207
|
||||
internal_count=1013 199 113 814 129 685 332 71 353 85 268 241 88 61 261 181 80 68 86 55
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=15
|
||||
num_leaves=14
|
||||
num_cat=0
|
||||
split_feature=86 40 27 10 41 31 73 65 28 74 14 97 65
|
||||
split_gain=9.19077 7.28223 7.35402 9.11593 7.22847 7.50822 7.14398 7.51306 6.94962 5.72956 5.5869 4.91206 4.48842
|
||||
threshold=1.425 10.937500000000002 -10.947916666666666 84.062500000000014 13.645833333333334 15.29166666666667 0.5344873150105709 0.32491602042266449 39.687500000000007 0.47029411764705881 85.535714285714292 -0.42708333333332854 0.35541085304317482
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=8 11 10 4 5 -4 9 -8 -1 -5 -3 -2 -13
|
||||
right_child=1 2 3 6 -6 -7 7 -9 -10 -11 -12 12 -14
|
||||
leaf_value=0.059592270356515986 0.0053735337050535084 0.0014536342489078382 -0.0042774365987278294 0.047077753667467796 -0.014197369069938693 0.036529368742735643 0.01583862591245707 -0.0087913938399981923 0.0061247756213370069 0.0052192588836833601 -0.050790366696786798 -0.012402789325272627 -0.059610908501483095
|
||||
leaf_weight=7.3100549280643454 6.1067151725292232 5.9317886233329755 9.6296757310628873 7.8829693347215679 8.6421001106500608 28.761566951870918 23.91489639878273 115.60252341628075 8.3125372529029828 15.558673441410063 7.3123210966587067 5.9148542433977109 7.0790839940309525
|
||||
leaf_count=30 25 24 39 32 35 117 97 469 34 63 30 24 29
|
||||
internal_value=0.00140631 -0.00051071 0.00151755 0.00334081 0.0188538 0.0262938 -0.00113655 -0.00456952 0.0311431 0.0192955 -0.0273913 -0.0242158 -0.0381217
|
||||
internal_weight=257.96 242.337 223.237 209.992 47.0333 38.3912 162.959 139.517 15.6226 23.4416 13.2441 19.1007 12.9939
|
||||
internal_count=1048 984 906 852 191 156 661 566 64 95 54 78 53
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=16
|
||||
num_leaves=13
|
||||
num_cat=0
|
||||
split_feature=33 88 64 14 97 55 33 27 95 11 62 89
|
||||
split_gain=8.94152 9.44973 8.34757 9.96923 9.2269 7.77034 6.84612 6.71789 8.91623 11.1245 5.59099 2.07466
|
||||
threshold=5.4583333333333348 0.5709995709995711 0.32539627952456907 83.535714285714292 -1.5982142857142916 24.062500000000004 0.3184523809523796 -12.94270833333333 170.79017857142858 87.645833333333357 0.50771095194186133 0.48370246267843525
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 2 3 4 -1 -3 11 -4 10 -10 -9 -5
|
||||
right_child=-2 5 7 6 -6 -7 -8 8 9 -11 -12 -13
|
||||
leaf_value=-0.021346605099593578 -0.036738482853078236 0.058873571365345455 -0.029645258704030009 -0.03604840849327056 0.042575470200724393 0.0048157514196816237 -0.0070588968786982603 0.037451850608501855 -0.041717482942480864 0.0036108240465800787 0.0091615077619154172 -0.067942279025163504
|
||||
leaf_weight=7.6162292957305908 9.497162461280821 8.4247576445341128 9.057781442999838 6.4219828844070452 6.8739977478981 8.594713091850279 9.099840894341467 19.09737765789032 9.280805364251135 130.09927693009377 26.950699493288994 6.6345582008361816
|
||||
leaf_count=31 39 35 37 26 28 35 37 78 38 528 109 27
|
||||
internal_value=0.00135171 0.00280948 0.00069135 -0.0168207 0.0089773 0.0315748 -0.0336925 0.00399111 0.00563418 0.000592579 0.0208943 -0.052255
|
||||
internal_weight=257.649 248.152 231.133 36.6466 14.4902 17.0195 22.1564 194.486 185.428 139.38 46.0481 13.0565
|
||||
internal_count=1048 1009 939 149 59 70 90 790 753 566 187 53
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=17
|
||||
num_leaves=12
|
||||
num_cat=0
|
||||
split_feature=33 88 55 27 10 32 31 60 75 90 14
|
||||
split_gain=8.28401 8.76598 7.29837 6.5019 6.36672 9.86493 7.51301 7.17497 6.93615 6.2403 5.35126
|
||||
threshold=5.4583333333333348 0.5709995709995711 24.062500000000004 -12.94270833333333 84.062500000000014 20.062500000000004 25.803571428571434 12.937500000000002 0.42318766937669378 0.19774467682950328 85.535714285714292
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 3 -3 10 5 8 7 -6 -5 -8 -1
|
||||
right_child=-2 2 -4 4 6 -7 9 -9 -10 -11 -12
|
||||
leaf_value=5.5780973020133532e-06 -0.035467582222402753 0.05716359984226644 0.004623749836194309 -0.010644706886582317 -0.01582380049665728 -0.034391445298106116 0.046794179848204068 0.0026044549279971439 0.028201993083561465 0.0052060024656404988 -0.051579011297576027
|
||||
leaf_weight=6.1612475216388685 9.4450326114892942 8.3315776139497775 8.5935685932636243 9.1481379419565183 59.708019092679024 5.8809057772159568 8.1617092192173022 77.918728813529015 37.500212237238884 19.722670719027519 6.7347901910543442
|
||||
leaf_count=25 39 35 35 37 243 24 33 316 153 80 28
|
||||
internal_value=0.00129938 0.00270042 0.030487 0.000663966 0.00229624 0.0144291 -0.00155443 -0.00539046 0.0205838 0.0173788 -0.0269338
|
||||
internal_weight=257.307 247.862 16.9251 230.936 218.04 52.5293 165.511 137.627 46.6484 27.8844 12.896
|
||||
internal_count=1048 1009 70 939 886 214 672 559 190 113 53
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=18
|
||||
num_leaves=8
|
||||
num_cat=0
|
||||
split_feature=33 88 55 11 17 87 45
|
||||
split_gain=7.67811 8.13711 6.86921 6.16429 9.02438 5.74349 6.3002
|
||||
threshold=5.4583333333333348 0.5709995709995711 24.062500000000004 74.354166666666671 -10.687499999999998 2.1450000000000005 -0.0034592463243838995
|
||||
decision_type=2 2 2 2 2 2 2
|
||||
left_child=1 3 -3 -1 -5 6 -6
|
||||
right_child=-2 2 -4 4 5 -7 -8
|
||||
leaf_value=0.036528500712599402 -0.034255437987614025 0.0555735632038485 0.004439425576339767 -0.038357932926460371 -0.0070697236803132311 0.030356748335144887 0.0070235502949047429
|
||||
leaf_weight=7.4107095897197715 9.3892168849706632 8.2292238324880618 8.5923809558153135 9.6656254380941373 105.90707743167877 10.260427355766295 97.451314657926559
|
||||
leaf_count=30 39 35 35 40 431 42 396
|
||||
internal_value=0.00124942 0.00259625 0.0294545 0.000637822 -0.000553374 0.00115717 -0.00031609
|
||||
internal_weight=256.906 247.517 16.8216 230.695 223.284 213.619 203.358
|
||||
internal_count=1048 1009 70 939 909 869 827
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=19
|
||||
num_leaves=12
|
||||
num_cat=0
|
||||
split_feature=33 86 64 12 27 95 11 49 62 29 63
|
||||
split_gain=7.11864 7.56899 6.11087 12.4985 5.73268 8.29302 10.1577 5.48698 5.06855 4.93802 3.11264
|
||||
threshold=5.4583333333333348 1.4150000000000003 0.32539627952456907 1.9375000000000002 -12.94270833333333 170.79017857142858 87.645833333333357 0.77175549922512843 0.54605872496633345 41.401785714285722 0.51856200631031568
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 9 3 8 -4 7 -7 -6 10 -1 -3
|
||||
right_child=-2 2 4 -5 5 6 -8 -9 -10 -11 -12
|
||||
leaf_value=0.054127893054378734 -0.033097093119392906 -0.024347971534982155 -0.027558678245584001 0.020697790803193339 0.012679398061121673 -0.0402735013491324 0.0031588930417813959 0.046340548831502577 0.0016157647003835103 0.0053158736723644484 -0.056291390595480988
|
||||
leaf_weight=7.2425345033407194 9.3303139358758909 10.320196986198431 8.9888450950384122 11.517354950308798 36.484531104564667 9.2175567746162397 131.94763413071632 9.837190628051756 6.3723442256450644 6.1165111809968948 9.2601860165595991
|
||||
leaf_count=31 39 42 37 47 149 38 536 40 26 25 38
|
||||
internal_value=0.0012009 0.0024949 0.00082268 -0.0139809 0.00364588 0.00514195 0.000322921 0.0198279 -0.0293706 0.031779 -0.039455
|
||||
internal_weight=256.635 247.305 233.946 37.4701 196.476 187.487 141.165 46.3217 25.9527 13.359 19.5804
|
||||
internal_count=1048 1009 953 153 800 763 574 189 106 56 80
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=20
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=60 18 88 26 36 31 9 24 61 35 75 60 95 51
|
||||
split_gain=8.03818 7.72948 8.40414 12.0656 10.8001 9.64736 8.20128 6.93288 5.91815 6.68838 6.79855 4.79664 4.70914 4.49622
|
||||
threshold=15.225000000000001 -0.29166666666667135 0.56584311806914556 14.937500000000002 -0.83124999999999971 19.645833333333339 16.515151515151516 -1.0000000180025095e-35 15.645833333333334 8.5669642857142865 0.58007633587786267 15.775000000000002 222.78125000000003 0.0018411019890782001
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 6 3 4 13 -6 8 -2 9 10 -1 -9 -4 -3
|
||||
right_child=7 2 12 -5 5 -7 -8 11 -10 -11 -12 -13 -14 -15
|
||||
leaf_value=-0.0089530653611645094 -0.013396078382306617 -0.012100783857722328 0.043432242412436123 -0.05470139039712181 0.021915508768684949 -0.01294489712125084 0.048027122146776596 0.063122343027819355 0.035214924114047572 -0.036143728924515048 0.011385864248782825 0.018228963324568446 -0.0045408979170662904 -0.049373438013463097
|
||||
leaf_weight=53.602775543928146 6.6217800229787853 8.524589270353319 6.1861436218023282 11.381553843617437 18.768473565578461 39.295118644833565 6.1146008819341651 6.1183906942605999 7.3963862508535376 8.3620016276836378 51.616437286138535 10.084086462855337 6.9543650448322296 13.191540747880934
|
||||
leaf_count=219 27 35 26 47 77 160 25 25 30 34 210 41 29 54
|
||||
internal_value=-0.00155888 -0.00379275 -0.0118627 -0.0161736 -0.0106771 -0.00167662 0.00283011 0.0210883 0.000545703 -0.00171195 0.00102442 0.0351816 0.0180434 -0.0347422
|
||||
internal_weight=254.218 231.394 104.302 91.1613 79.7797 58.0636 127.092 22.8243 120.978 113.581 105.219 16.2025 13.1405 21.7161
|
||||
internal_count=1039 946 428 373 326 237 518 93 493 463 429 66 55 89
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=21
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=60 18 88 26 36 31 9 9 61 35 35 95 51 28
|
||||
split_gain=7.42241 7.14338 7.76759 11.245 10.0048 8.89861 7.56719 6.33462 5.44938 6.18568 6.60464 4.38569 4.18924 2.06881
|
||||
threshold=15.225000000000001 -0.29166666666667135 0.56584311806914556 14.937500000000002 -0.83124999999999971 19.645833333333339 16.515151515151516 -4.2748917748917838 15.645833333333334 8.5669642857142865 8.2678571428571441 222.78125000000003 0.0018411019890782001 41.187500000000007
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 6 3 4 12 -6 8 -2 9 10 -1 -4 -3 -9
|
||||
right_child=7 2 11 -5 5 -7 -8 13 -10 -11 -12 -13 -14 -15
|
||||
leaf_value=-0.0020580613462284694 -0.0067757984042552816 -0.011632476414499757 0.042031501598029888 -0.052911002493618228 0.021055706218397436 -0.012438211689475422 0.046156795546078516 0.022002629364403069 0.033775328851131076 -0.034815903882803013 0.034011731538430165 -0.0043576236343065185 -0.047663392508499491 0.052589999762468774
|
||||
leaf_weight=96.316321060061455 8.6104428470134753 8.5132995098829287 6.1375970840454084 11.297693848609923 18.753774255514145 39.26072384417057 6.1079912632703772 7.3507572114467603 7.4030207246541968 8.3342373967170698 8.8703934699296934 6.9569248706102371 13.119665667414663 6.8210117071866989
|
||||
leaf_count=393 35 35 26 47 77 160 25 30 30 34 36 29 54 28
|
||||
internal_value=-0.00149909 -0.00364675 -0.0114181 -0.0155653 -0.010268 -0.00161096 0.002718 0.0202838 0.000523866 -0.00164456 0.000983704 0.0173856 -0.033484 0.0367246
|
||||
internal_weight=253.854 231.072 104.04 90.9452 79.6475 58.0145 127.032 22.7822 120.924 113.521 105.187 13.0945 21.633 14.1718
|
||||
internal_count=1039 946 428 373 326 237 518 93 493 463 429 55 89 58
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=22
|
||||
num_leaves=18
|
||||
num_cat=0
|
||||
split_feature=75 10 73 56 97 33 54 28 63 53 73 64 38 53 14 13 63
|
||||
split_gain=6.8389 8.24825 6.33351 8.88087 6.18565 5.67783 6.55094 5.59704 6.85557 8.66603 7.65659 7.21773 11.1113 8.66033 3.97328 3.82022 2.45765
|
||||
threshold=0.71055555555555572 112.31250000000001 0.53158244680851074 39.937500000000007 -0.42708333333332854 -3.077380952380953 21.145833333333339 44.937500000000007 0.51560927336724371 25.687500000000004 0.63958726168028501 0.34495732674280544 4.3875000000000011 27.437500000000004 110.18750000000001 98.625000000000014 0.51800407176145413
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=2 5 3 15 -5 -2 -7 8 9 10 -4 12 -10 -13 -11 -1 -8
|
||||
right_child=1 -3 7 4 -6 6 16 -9 11 14 -12 13 -14 -15 -16 -17 -18
|
||||
leaf_value=0.015837814614419042 -0.0054090476665669711 -0.020413774544095844 -0.0067905346474414312 0.026068878291681773 -0.024468940768756574 0.010241531744467547 0.073552533225814301 0.020224466305667418 -0.0046266451570048392 -0.021209825690180987 -0.052758572611820695 0.0025546027891674618 -0.064146045205184979 0.052111362242983086 -0.062702945254864487 0.060247018203511145 0.036959564623549768
|
||||
leaf_weight=5.9841428995132464 7.3666127473115912 8.3540667146444303 65.92797489464283 6.133746981620793 10.522817611694332 9.2578747272491508 5.8667192012071592 11.480442628264425 29.450126886367798 6.6407066881656638 6.3565134406089774 47.05113098025322 6.0492770820856085 6.4109846800565711 8.3170198649167997 6.4289718419313431 5.8796851336956024
|
||||
leaf_count=25 30 34 271 25 43 38 24 47 121 27 26 193 25 26 34 26 24
|
||||
internal_value=-0.00144173 0.0145202 -0.00414618 0.0132275 -0.00585849 0.0248068 0.0354041 -0.00683713 -0.00860031 -0.0165676 -0.0108328 -0.000786974 -0.0147691 0.00849727 -0.0442815 0.0388381 0.0552359
|
||||
internal_weight=253.479 36.725 216.754 29.0697 16.6566 28.3709 21.0043 187.684 176.204 87.2422 72.2845 88.9615 35.4994 53.4621 14.9577 12.4131 11.7464
|
||||
internal_count=1039 150 889 119 68 116 86 770 723 358 297 365 146 219 61 51 48
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=23
|
||||
num_leaves=16
|
||||
num_cat=0
|
||||
split_feature=60 18 35 26 21 11 72 34 42 69 65 24 60 29 16
|
||||
split_gain=6.76622 6.44432 7.43603 8.61975 6.98342 7.45293 9.26335 8.42599 8.25952 6.4395 7.95854 6.13581 4.32375 3.78406 2.36778
|
||||
threshold=15.225000000000001 -0.29166666666667135 8.8229166666666696 14.937500000000002 0.16515151515151519 123.68750000000001 227.82000000000002 8.4687500000000018 -0.84166666666666667 8.2500000000000018 0.32491602042266449 -1.0000000180025095e-35 15.775000000000002 32.708333333333336 4.9375000000000009
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 4 3 9 5 6 7 -1 -8 10 -3 -2 -13 -5 -11
|
||||
right_child=11 2 -4 13 -6 -7 8 -9 -10 14 -12 12 -14 -15 -16
|
||||
leaf_value=0.0068377190178299978 -0.012999968486144695 0.046209360196903707 -0.047916200210550458 -0.016602574178768866 0.044382956007966268 0.041711064554847729 0.012542729748890782 -0.040858831579305734 -0.041878353739748291 -0.047809843446037739 -0.0025966166987321858 0.059424945850961183 0.016650328155874659 -0.061312735990994086 -0.01220746979923833
|
||||
leaf_weight=86.415405079722404 6.6122610569000271 5.8188596814870825 7.9874863028526297 5.9741610884666425 6.0946186929941168 6.6349345445632926 6.355574399232867 6.3623842000961295 14.977498367428778 6.0902193337678892 65.746306836605072 6.0662906020879772 10.036743447184561 6.1432460248470306 5.8692503720521927
|
||||
leaf_count=353 27 24 33 25 25 27 26 26 61 25 271 25 41 26 24
|
||||
internal_value=-0.00138606 -0.00343895 -0.0108389 -0.0077424 0.00260686 0.000498221 -0.00189808 0.00356685 -0.0256652 -0.00316857 0.00137173 0.0194426 0.0327643 -0.0392696 -0.0303376
|
||||
internal_weight=253.185 230.47 103.63 95.642 126.84 120.746 114.111 92.7778 21.3331 83.5246 71.5652 22.7153 16.103 12.1174 11.9595
|
||||
internal_count=1039 946 428 395 518 493 466 379 87 344 295 93 66 51 49
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=24
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=60 18 88 27 62 9 11 72 12 48 36 24 43 95
|
||||
split_gain=6.25224 5.95135 7.10947 8.50211 8.95714 6.45695 6.86321 8.56032 7.77198 7.68621 6.38574 5.67858 4.21565 3.98223
|
||||
threshold=15.225000000000001 -0.29166666666667135 0.56584311806914556 -6.9999999999999973 0.46489792570397087 16.515151515151516 123.68750000000001 227.82000000000002 -0.75595238095237949 0.025274304665849606 -1.705357142857143 -1.0000000180025095e-35 0.50860236372279055 222.78125000000003
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 5 3 -3 -5 6 7 9 -9 -1 -6 -2 -13 -4
|
||||
right_child=11 2 13 4 10 -7 -8 8 -10 -11 -12 12 -14 -15
|
||||
leaf_value=0.0087138894445027461 -0.01248475536504879 -0.052100240369848241 0.040799272900499305 0.024363685744045852 -0.051816351953662361 0.042735686521304467 0.040009035522266888 -0.046006108176515285 0.00273483800408156 -0.021635092287458264 -0.0115203606180113 0.0090547062831142257 0.050240538556617337 -0.0035196794361016148
|
||||
leaf_weight=76.604642063379288 6.6097762733697918 8.6576687693595868 6.0909906178712827 10.348592042922972 6.9732603579759589 6.0767100751399985 6.6403718292713156 11.978713899850847 9.2968079298734647 16.170377686619759 64.432133376598358 7.2736790329217937 8.7717214673757535 6.9399073719978333
|
||||
leaf_count=313 27 37 26 43 29 25 27 49 38 66 264 30 36 29
|
||||
internal_value=-0.00133294 -0.00330607 -0.0104257 -0.0144068 -0.0104152 0.0025036 0.000477939 -0.00182368 -0.0247077 0.00342416 -0.0154556 0.0187169 0.0315702 0.0171962
|
||||
internal_weight=252.865 230.21 103.443 90.4117 81.754 126.768 120.691 114.051 21.2755 92.775 71.4054 22.6552 16.0454 13.0309
|
||||
internal_count=1039 946 428 373 336 518 493 466 87 379 293 93 66 55
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=25
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=33 87 85 10 83 10 39 86 72 49 13 16 38 27
|
||||
split_gain=7.25855 6.38507 8.41516 9.2502 8.14048 7.48236 8.40852 7.85339 11.0386 8.60242 6.83818 5.86449 5.49791 3.04698
|
||||
threshold=5.4583333333333348 1.5850000000000002 1.0000000180025095e-35 117.22500000000001 0.54234668644906048 116.93750000000001 -1.0000000180025095e-35 1.5650000000000002 162.69282608695656 0.75102962846491172 104.77500000000002 -8.9374999999999982 4.1833333333333345 0.020833333333333655
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 10 5 4 11 7 -7 9 -9 -3 -1 -4 -8 -6
|
||||
right_child=-2 2 3 -5 13 6 12 8 -10 -11 -12 -13 -14 -15
|
||||
leaf_value=-0.041539672781130549 -0.031422067002150791 0.042995214924070081 0.042296436297125074 -0.023883650662784965 0.072297264891014304 -0.009582647504040049 0.058812399794648744 0.021985462315431858 -0.014896003510037621 -0.0040262327529876152 0.0035283522947714635 0.0075685995167286092 0.011039775290075307 0.0323219849604569
|
||||
leaf_weight=10.137894913554193 10.139534831047056 10.71300193667412 9.8288029283285123 7.8387844264507285 5.8450987040996534 9.664890334010126 8.7909274846315366 15.362282529473303 83.881821304559708 14.860007524490355 11.493947580456732 37.329559937119484 6.8639661371707907 6.381435826420784
|
||||
leaf_count=42 43 45 40 32 24 40 36 63 345 62 47 153 28 26
|
||||
internal_value=0.00172561 0.00313194 0.0051945 0.0169566 0.0223475 -7.19483e-05 0.0197544 -0.00409382 -0.00918701 0.0156719 -0.0175931 0.0148066 0.0378663 0.0514328
|
||||
internal_weight=249.132 238.992 217.361 67.2237 59.3849 150.137 25.3198 124.817 99.2441 25.573 21.6318 47.1584 15.6549 12.2265
|
||||
internal_count=1026 983 894 275 243 619 104 515 408 107 89 193 64 50
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=26
|
||||
num_leaves=13
|
||||
num_cat=0
|
||||
split_feature=48 87 75 13 13 26 98 83 54 28 63 26
|
||||
split_gain=6.07316 5.62173 8.54781 6.21306 5.95377 6.62941 7.29602 6.07248 5.64674 6.51449 5.46195 3.27744
|
||||
threshold=0.064872666259492118 1.5850000000000002 0.69045454545454554 104.77500000000002 120.56250000000001 15.072916666666668 -11.562499999999998 0.49675474226372424 20.535714285714288 40.187500000000007 0.51085383914331295 6.072916666666667
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 3 4 -1 5 6 -3 -7 -4 11 -2 -10
|
||||
right_child=10 2 8 -5 -6 7 -8 -9 9 -11 -12 -13
|
||||
leaf_value=-0.039674658535771484 -0.052512934017087537 -0.014023176104202862 -0.005798976431383154 0.0036351517375994916 -0.026305412473366974 0.0062814492481233046 0.010148203473004854 -0.041049983242601396 0.028561765576098901 0.0087152875473318417 -0.0029143682076594587 0.066287713324515871
|
||||
leaf_weight=9.8422116041183489 6.0319006294012061 23.719328477978706 7.8761165887117377 11.48290069401264 11.350633054971693 7.9110089093446758 126.75102043151855 9.5997582674026471 6.5257693231105831 10.448119834065436 8.6423251926898939 8.462289631366728
|
||||
leaf_count=41 25 97 32 47 47 33 522 41 27 43 36 35
|
||||
internal_value=0.00166 0.00322559 0.00518911 -0.0163537 0.00173262 0.00362718 0.00633796 -0.0196666 0.0237966 0.0329606 -0.0233021 0.0498619
|
||||
internal_weight=248.643 233.969 212.644 21.3251 179.332 167.981 150.47 17.5108 33.3123 25.4362 14.6742 14.9881
|
||||
internal_count=1026 965 877 88 740 693 619 74 137 105 61 62
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=27
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=33 40 43 64 56 29 74 7 16 12 36 53 32 33
|
||||
split_gain=6.67537 6.00723 6.58775 6.4086 6.22146 8.22654 7.17113 5.57557 10.8982 10.7588 6.64454 7.95563 6.24334 4.86842
|
||||
threshold=5.4583333333333348 10.937500000000002 0.53985767334155887 0.34495732674280544 30.562500000000004 32.791666666666679 0.62250000000000016 59.166666666666679 3.7916666666666647 9.1339285714285712 -0.34166666666666673 22.387500000000003 23.791666666666668 -0.29166666666666602
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 3 7 -1 -4 -6 -7 8 9 -3 12 -12 -9 -5
|
||||
right_child=-2 2 4 13 5 6 -8 10 -10 -11 11 -13 -14 -15
|
||||
leaf_value=-0.04539631182232725 -0.030331123415790143 0.0036500818553591068 -0.017115468469559102 -0.026046588004955247 0.065457960355147352 0.02547423439415672 -0.018488966847576102 0.017724882848208653 -0.041424367213409224 -0.047501812723565191 -0.0045522301063454888 0.04416916038732871 -0.038718688099103261 0.024752126334750945
|
||||
leaf_weight=8.1224037855863589 10.053963825106619 115.14034233987331 6.9894797801971427 5.7684260606765747 6.3827460259199134 27.928439050912857 7.5389710813760749 6.7571853250265104 10.671643391251562 6.9777127951383582 8.1633969545364398 15.628093481063841 5.8501180559396744 6.3321559280157071
|
||||
leaf_count=34 43 476 29 24 26 114 31 28 44 29 34 64 24 26
|
||||
internal_value=0.00159555 0.00294283 0.00487723 -0.0179123 0.0178183 0.0236527 0.0161294 0.00114151 -0.00266021 0.00072731 0.0150109 0.0274518 -0.00846641 0.000536049
|
||||
internal_weight=248.305 238.251 218.028 20.223 48.8396 41.8502 35.4674 169.188 132.79 122.118 36.3988 23.7915 12.6073 12.1006
|
||||
internal_count=1026 983 899 84 200 171 145 699 549 505 150 98 52 50
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=28
|
||||
num_leaves=13
|
||||
num_cat=0
|
||||
split_feature=33 27 14 48 24 53 59 57 34 47 81 50
|
||||
split_gain=6.19142 5.48502 8.27407 6.44704 5.11208 5.11786 5.55155 5.07182 5.97421 8.76085 8.56983 3.46147
|
||||
threshold=5.4583333333333348 -2.3020833333333326 85.312500000000014 0.064872666259492118 -1.0000000180025095e-35 21.291666666666668 23.387500000000003 45.687500000000007 6.8875000000000011 0.37218684558476361 1.7150000000000001 0.74075516458343793
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 2 11 7 -4 -6 -7 8 10 -10 -3 -1
|
||||
right_child=-2 3 4 -5 5 6 -8 -9 9 -11 -12 -13
|
||||
leaf_value=0.036467676081935275 -0.029325034647802492 0.012704755728250297 -0.051764929617036118 -0.023669451716433799 -0.044508533316403635 0.026446805663908529 -0.027271352696527536 -0.022833580306035422 0.022463638898768467 -0.0033374130186977686 -0.014458359978742813 -0.0028572785771652266
|
||||
leaf_weight=6.0484680682420722 9.9843682050704938 40.375474900007248 7.6424536705017116 11.367303296923636 5.5812820345163372 6.6618274450302106 5.7221347242593765 8.4949828088283521 73.357022643089294 29.534479916095734 34.431760281324387 8.7800204604864103
|
||||
leaf_count=25 43 166 32 47 24 28 24 35 302 121 143 36
|
||||
internal_value=0.0015332 0.00282775 -0.0105946 0.005575 -0.0243635 -0.0127068 0.00162579 0.0073604 0.00880384 0.0150576 0.000202303 0.0131832
|
||||
internal_weight=247.982 237.997 40.4362 197.561 25.6077 17.9652 12.384 186.194 177.699 102.892 74.8072 14.8285
|
||||
internal_count=1026 983 169 814 108 76 52 767 732 423 309 61
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
Tree=29
|
||||
num_leaves=17
|
||||
num_cat=0
|
||||
split_feature=87 85 10 83 41 73 86 39 8 44 13 76 61 36 97 45
|
||||
split_gain=5.37924 8.77104 7.1461 6.5763 6.24618 5.74552 7.24104 5.90731 7.82727 5.90939 5.68942 4.92086 12.7968 7.49136 4.69159 2.29423
|
||||
threshold=1.5850000000000002 1.0000000180025095e-35 117.22500000000001 0.54234668644906048 10.812500000000002 0.53158244680851074 1.7450000000000003 -0.61249999999999993 47.727272727272734 0.48331411181158185 104.77500000000002 2.4650000000000003 12.866071428571431 0.28869047619047633 1.4062500000000002 -0.013905145374354397
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=10 4 3 11 -2 6 -6 8 -7 -9 -1 13 -13 -3 -12 -5
|
||||
right_child=1 2 -4 15 5 7 -8 9 -10 -11 14 12 -14 -15 -16 -17
|
||||
leaf_value=-0.039294680659493833 -0.031671221811972641 0.037455766277146953 -0.018600965920339822 0.063774142191859146 0.050889907105456336 -0.036046187526128669 -0.0011468294092016626 -0.016947588402893 -0.00031638211057623142 0.0084049263294318916 0.026762860869410491 0.035225255717854904 -0.041754796373233906 -0.00065805839704901963 -0.023845024484234915 0.029272484285479398
|
||||
leaf_weight=10.1972071826458 10.532053381204603 19.837578102946281 8.7409028112888318 6.0329279154539108 7.6620817482471493 16.978984668850899 9.6890967190265638 18.620566442608833 23.233949556946754 70.049215823411942 5.8855016827583295 6.3545227348804456 7.5724885612726212 14.127186641097067 5.8383642733097076 6.3081436455249769
|
||||
leaf_count=43 44 82 36 25 32 71 40 78 96 290 24 26 31 58 24 26
|
||||
internal_value=0.0014732 0.00331024 0.0151971 0.0201018 -0.00191971 0.000223048 0.021832 -0.00268611 -0.0154025 0.00308092 -0.0174444 0.0133924 -0.00663086 0.0216028 0.00156066 0.0461386
|
||||
internal_weight=247.661 225.74 68.9738 60.2328 156.766 146.234 17.3512 128.883 40.2129 88.6698 21.9211 47.8918 13.927 33.9648 11.7239 12.3411
|
||||
internal_count=1026 935 284 248 651 607 72 535 167 368 91 197 57 140 48 51
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
end of trees
|
||||
|
||||
feature_importances:
|
||||
Column_33=16
|
||||
Column_55=14
|
||||
Column_75=13
|
||||
Column_95=13
|
||||
Column_17=12
|
||||
Column_27=12
|
||||
Column_40=12
|
||||
Column_60=12
|
||||
Column_88=12
|
||||
Column_87=11
|
||||
Column_11=10
|
||||
Column_97=10
|
||||
Column_14=9
|
||||
Column_36=9
|
||||
Column_53=9
|
||||
Column_64=9
|
||||
Column_86=9
|
||||
Column_31=8
|
||||
Column_54=8
|
||||
Column_73=8
|
||||
Column_15=7
|
||||
Column_26=7
|
||||
Column_29=7
|
||||
Column_39=7
|
||||
Column_62=7
|
||||
Column_63=7
|
||||
Column_9=6
|
||||
Column_10=6
|
||||
Column_12=6
|
||||
Column_18=6
|
||||
Column_28=6
|
||||
Column_51=6
|
||||
Column_61=6
|
||||
Column_77=6
|
||||
Column_13=5
|
||||
Column_24=5
|
||||
Column_35=5
|
||||
Column_38=5
|
||||
Column_46=5
|
||||
Column_48=5
|
||||
Column_58=5
|
||||
Column_65=5
|
||||
Column_74=5
|
||||
Column_80=5
|
||||
Column_8=4
|
||||
Column_30=4
|
||||
Column_32=4
|
||||
Column_41=4
|
||||
Column_43=4
|
||||
Column_45=4
|
||||
Column_49=4
|
||||
Column_72=4
|
||||
Column_82=4
|
||||
Column_83=4
|
||||
Column_16=3
|
||||
Column_25=3
|
||||
Column_34=3
|
||||
Column_47=3
|
||||
Column_50=3
|
||||
Column_52=3
|
||||
Column_56=3
|
||||
Column_59=3
|
||||
Column_79=3
|
||||
Column_81=3
|
||||
Column_85=3
|
||||
Column_89=3
|
||||
Column_90=3
|
||||
Column_98=3
|
||||
Column_37=2
|
||||
Column_42=2
|
||||
Column_57=2
|
||||
Column_69=2
|
||||
Column_76=2
|
||||
Column_96=2
|
||||
Column_7=1
|
||||
Column_21=1
|
||||
Column_44=1
|
||||
Column_66=1
|
||||
Column_68=1
|
||||
Column_71=1
|
||||
|
||||
parameters:
|
||||
[boosting: gbdt]
|
||||
[objective: binary]
|
||||
[metric: binary_logloss]
|
||||
[tree_learner: serial]
|
||||
[device_type: cpu]
|
||||
[data_sample_strategy: bagging]
|
||||
[data: ]
|
||||
[valid: ]
|
||||
[num_iterations: 1200]
|
||||
[learning_rate: 0.04]
|
||||
[num_leaves: 31]
|
||||
[num_threads: 4]
|
||||
[seed: 42]
|
||||
[deterministic: 0]
|
||||
[force_col_wise: 0]
|
||||
[force_row_wise: 0]
|
||||
[histogram_pool_size: -1]
|
||||
[max_depth: 6]
|
||||
[min_data_in_leaf: 24]
|
||||
[min_sum_hessian_in_leaf: 0.001]
|
||||
[bagging_fraction: 0.84]
|
||||
[pos_bagging_fraction: 1]
|
||||
[neg_bagging_fraction: 1]
|
||||
[bagging_freq: 5]
|
||||
[bagging_seed: 400]
|
||||
[bagging_by_query: 0]
|
||||
[feature_fraction: 0.82]
|
||||
[feature_fraction_bynode: 1]
|
||||
[feature_fraction_seed: 30056]
|
||||
[extra_trees: 0]
|
||||
[extra_seed: 12879]
|
||||
[early_stopping_round: 0]
|
||||
[early_stopping_min_delta: 0]
|
||||
[first_metric_only: 0]
|
||||
[max_delta_step: 0]
|
||||
[lambda_l1: 0]
|
||||
[lambda_l2: 0]
|
||||
[linear_lambda: 0]
|
||||
[min_gain_to_split: 0]
|
||||
[drop_rate: 0.1]
|
||||
[max_drop: 50]
|
||||
[skip_drop: 0.5]
|
||||
[xgboost_dart_mode: 0]
|
||||
[uniform_drop: 0]
|
||||
[drop_seed: 17869]
|
||||
[top_rate: 0.2]
|
||||
[other_rate: 0.1]
|
||||
[min_data_per_group: 100]
|
||||
[max_cat_threshold: 32]
|
||||
[cat_l2: 10]
|
||||
[cat_smooth: 10]
|
||||
[max_cat_to_onehot: 4]
|
||||
[top_k: 20]
|
||||
[monotone_constraints: ]
|
||||
[monotone_constraints_method: basic]
|
||||
[monotone_penalty: 0]
|
||||
[feature_contri: ]
|
||||
[forcedsplits_filename: ]
|
||||
[refit_decay_rate: 0.9]
|
||||
[cegb_tradeoff: 1]
|
||||
[cegb_penalty_split: 0]
|
||||
[cegb_penalty_feature_lazy: ]
|
||||
[cegb_penalty_feature_coupled: ]
|
||||
[path_smooth: 0]
|
||||
[interaction_constraints: ]
|
||||
[verbosity: -1]
|
||||
[saved_feature_importance_type: 0]
|
||||
[use_quantized_grad: 0]
|
||||
[num_grad_quant_bins: 4]
|
||||
[quant_train_renew_leaf: 0]
|
||||
[stochastic_rounding: 1]
|
||||
[linear_tree: 0]
|
||||
[max_bin: 255]
|
||||
[max_bin_by_feature: ]
|
||||
[min_data_in_bin: 3]
|
||||
[bin_construct_sample_cnt: 200000]
|
||||
[data_random_seed: 175]
|
||||
[is_enable_sparse: 1]
|
||||
[enable_bundle: 1]
|
||||
[use_missing: 1]
|
||||
[zero_as_missing: 0]
|
||||
[feature_pre_filter: 1]
|
||||
[pre_partition: 0]
|
||||
[two_round: 0]
|
||||
[header: 0]
|
||||
[label_column: ]
|
||||
[weight_column: ]
|
||||
[group_column: ]
|
||||
[ignore_column: ]
|
||||
[categorical_feature: ]
|
||||
[forcedbins_filename: ]
|
||||
[precise_float_parser: 0]
|
||||
[parser_config_file: ]
|
||||
[objective_seed: 16083]
|
||||
[num_class: 1]
|
||||
[is_unbalance: 0]
|
||||
[scale_pos_weight: 1]
|
||||
[sigmoid: 1]
|
||||
[boost_from_average: 1]
|
||||
[reg_sqrt: 0]
|
||||
[alpha: 0.9]
|
||||
[fair_c: 1]
|
||||
[poisson_max_delta_step: 0.7]
|
||||
[tweedie_variance_power: 1.5]
|
||||
[lambdarank_truncation_level: 30]
|
||||
[lambdarank_norm: 1]
|
||||
[label_gain: ]
|
||||
[lambdarank_position_bias_regularization: 0]
|
||||
[eval_at: ]
|
||||
[multi_error_top_k: 1]
|
||||
[auc_mu_weights: ]
|
||||
[num_machines: 1]
|
||||
[local_listen_port: 12400]
|
||||
[time_out: 120]
|
||||
[machine_list_filename: ]
|
||||
[machines: ]
|
||||
[gpu_platform_id: -1]
|
||||
[gpu_device_id: -1]
|
||||
[gpu_use_dp: 0]
|
||||
[num_gpu: 1]
|
||||
|
||||
end of parameters
|
||||
|
||||
pandas_categorical:null
|
||||
@@ -0,0 +1,202 @@
|
||||
tree
|
||||
version=v4
|
||||
num_class=1
|
||||
num_tree_per_iteration=1
|
||||
label_index=0
|
||||
max_feature_idx=98
|
||||
objective=binary sigmoid:1
|
||||
feature_names=Column_0 Column_1 Column_2 Column_3 Column_4 Column_5 Column_6 Column_7 Column_8 Column_9 Column_10 Column_11 Column_12 Column_13 Column_14 Column_15 Column_16 Column_17 Column_18 Column_19 Column_20 Column_21 Column_22 Column_23 Column_24 Column_25 Column_26 Column_27 Column_28 Column_29 Column_30 Column_31 Column_32 Column_33 Column_34 Column_35 Column_36 Column_37 Column_38 Column_39 Column_40 Column_41 Column_42 Column_43 Column_44 Column_45 Column_46 Column_47 Column_48 Column_49 Column_50 Column_51 Column_52 Column_53 Column_54 Column_55 Column_56 Column_57 Column_58 Column_59 Column_60 Column_61 Column_62 Column_63 Column_64 Column_65 Column_66 Column_67 Column_68 Column_69 Column_70 Column_71 Column_72 Column_73 Column_74 Column_75 Column_76 Column_77 Column_78 Column_79 Column_80 Column_81 Column_82 Column_83 Column_84 Column_85 Column_86 Column_87 Column_88 Column_89 Column_90 Column_91 Column_92 Column_93 Column_94 Column_95 Column_96 Column_97 Column_98
|
||||
feature_infos=none none none none none none none [0:100] [0:100] [-100:100] [54:128.875] [61:138] [-41.333333333333329:24] [61:130.125] [59:133] [-50:34.916666666666671] [-28.666666666666657:32.5] [-24.5:27] [-35.5:31.833333333333329] [0:1] [0:1] [-1:1] [0:12] [0:12] [-12:10] [0.91666666666666663:186.1875] [0.85416666666666663:185.08333333333337] [-179.02083333333334:184.16666666666663] [20:54] [22:56] [-21:15.833333333333332] [11:34.5] [9:36] [-15:13.333333333333332] [2:13] [3:13] [-8:5.5] [0.875:9] [0:12] [-9:4.875] [7:22] [8:23] [-10:10] [0.41025641025641019:0.67647058823529416] [0.39730639730639727:0.76744186046511631] [-0.3174418604651163:0.22647058823529409] [0.20419254658385089:0.4825000000000001] [0.1875:0.54166666666666663] [-0.22171945701357459:0.17923280423280419] [0.47826086956521741:0.87898457080200498] [0.42857142857142849:0.88636363636363635] [-0.29647435897435898:0.38962801311591622] [11:39.666666666666664] [15.5:40] [10:31.875] [11.5:34] [15:56] [17:57] [12:32.125] [10:38] [8:20] [7:22] [0.40000000000000002:0.69047619047619047] [0.3902439024390244:0.71739130434782605] [0.23753976670201479:0.51851851851851849] [0.1707317073170731:0.54545454545454541] [0:8] [0:1] [120:295] [-38:36] [0:1] [0:1] [140.39130434782609:233.09999999999999] [0:1] [0:1] [0:1] [1.05:4.1100000000000003] [1.05:4.1299999999999999] [0.2034883720930232:0.79729729729729726] [0.20270270270270269:0.79651162790697683] [145.5:257.5] [1.3:7.7599999999999998] [1.4099999999999999:7.5599999999999996] [0.1789976133651551:0.75977653631284914] [0.24022346368715081:0.82100238663484482] [-15.5:12.5] [1.3:2.7200000000000002] [1.1799999999999999:2.7799999999999998] [0.36298076923076922:0.64953271028037374] [0.35046728971962621:0.63701923076923073] [0.1918833727344364:0.21555133935749429] none none none none [132:250.6875] [126.25:256.625] [-63.5:88.6875] [-35.5:27.5]
|
||||
tree_sizes=1714 1917
|
||||
|
||||
Tree=0
|
||||
num_leaves=15
|
||||
num_cat=0
|
||||
split_feature=72 82 49 25 36 25 17 77 34 69 25 40 80 40
|
||||
split_gain=11.2305 10.9895 14.5557 11.7291 11.5678 6.991 6.42947 6.52716 10.0318 9.34909 5.86638 5.89396 5.34606 8.20903
|
||||
threshold=232.51500000000001 1.8250000000000004 0.74446554478882077 2.8645833333333335 -0.46874999999999994 1.0104166666666667 12.562500000000002 2.6750000000000003 5.6339285714285721 -5.4166666666666652 3.1145833333333335 12.803571428571431 162.00000000000003 13.937500000000002
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=1 5 10 4 -4 -1 7 8 -7 -9 -3 -12 -5 -14
|
||||
right_child=-2 2 3 12 -6 6 -8 9 -10 -11 11 -13 13 -15
|
||||
leaf_value=-0.014742735215146609 -0.095823069621212592 -0.030752227995962191 -0.086785452728816673 0.017792685597478597 -0.021483574280753166 -0.0010203128315903962 -0.085759203191584921 -0.020079232808751797 -0.05043362279072628 -0.074223259488395713 -0.046761720776777753 -0.098557138597063443 -0.031433483007911781 0.014451045738105336
|
||||
leaf_weight=7.4955528974533072 7.7454046607017508 6.2462940812110928 7.995256423950198 7.7454046607017508 9.4943670034408552 6.99584937095642 9.7442187666892988 5.9964423179626456 108.93536877632141 34.229691565036774 5.9964423179626456 8.4949599504470807 23.486065745353702 8.4949599504470807
|
||||
leaf_count=30 31 25 32 31 38 28 39 24 436 137 24 34 94 34
|
||||
internal_value=-0.0483827 -0.0469209 -0.0344467 -0.0240408 -0.0513359 -0.0525288 -0.054236 -0.052269 -0.0474518 -0.0661521 -0.063157 -0.0771246 -0.0120241 -0.0192454
|
||||
internal_weight=259.096 251.351 77.9538 57.2161 17.4896 173.397 165.902 156.157 115.931 40.2261 20.7377 14.4914 39.7264 31.981
|
||||
internal_count=1037 1006 312 229 70 694 664 625 464 161 83 58 159 128
|
||||
is_linear=0
|
||||
shrinkage=1
|
||||
|
||||
|
||||
Tree=1
|
||||
num_leaves=17
|
||||
num_cat=0
|
||||
split_feature=52 63 82 49 63 11 9 22 47 27 26 50 60 68 37 64
|
||||
split_gain=9.46149 12.0286 10.1725 11.5657 11.422 11.4943 11.2254 9.35335 10.3758 7.46163 7.15253 10.8161 5.87382 5.6308 5.10807 3.94362
|
||||
threshold=18.267857142857146 0.57978414386394084 1.7650000000000003 0.74446554478882077 0.52121781606433182 81.062500000000014 8.7121212121212164 1.5000000000000002 0.36820747200853426 0.06249999999999991 7.0312500000000009 0.79401716042796011 12.35416666666667 161.75000000000003 4.5500000000000007 0.34986080718052731
|
||||
decision_type=2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
|
||||
left_child=15 2 4 9 5 -2 -7 8 -6 13 11 -5 -12 -4 -9 -1
|
||||
right_child=1 -3 3 10 7 6 -8 14 -10 -11 12 -13 -14 -15 -16 -17
|
||||
leaf_value=-0.057339030773059012 0.028542958096860364 -0.044782355190073488 0.032618410031766333 0.0057409124284739923 0.013389736272609485 -0.026136292703093016 0.010233031617588078 0.056167817502913067 -0.029699787865729181 -0.038327130963158276 0.074132616447759023 0.052492105838845886 0.026735863001827433 -0.01725653802460729 0.0087599672849197358 -0.011453089463718463
|
||||
leaf_weight=5.9920798838138563 9.7432544529438001 8.4919032305479032 7.2431264519691485 42.224371567368507 31.477784723043442 57.952298685908318 17.733558729290962 9.2429284304380399 12.488689020276068 9.2367470711469633 6.4985252916812923 9.7452246546745283 11.743822261691092 7.2440863251686096 5.9950847774744025 5.9950532913208008
|
||||
leaf_count=24 39 34 29 169 126 232 71 37 50 37 26 39 47 29 24 24
|
||||
internal_value=0.000314777 0.00199863 0.0036638 0.0139129 -0.00299273 -0.0123505 -0.0176148 0.01051 0.00115015 -0.010233 0.0220716 0.0145076 0.0436201 0.00767928 0.0375162 -0.0343904
|
||||
internal_weight=259.049 247.061 238.57 93.9359 144.634 85.4291 75.6859 59.2045 43.9665 23.724 70.2119 51.9696 18.2423 14.4872 15.238 11.9871
|
||||
internal_count=1037 989 955 376 579 342 303 237 176 95 281 208 73 58 61 48
|
||||
is_linear=0
|
||||
shrinkage=0.04
|
||||
|
||||
|
||||
end of trees
|
||||
|
||||
feature_importances:
|
||||
Column_25=3
|
||||
Column_40=2
|
||||
Column_49=2
|
||||
Column_63=2
|
||||
Column_82=2
|
||||
Column_9=1
|
||||
Column_11=1
|
||||
Column_17=1
|
||||
Column_22=1
|
||||
Column_26=1
|
||||
Column_27=1
|
||||
Column_34=1
|
||||
Column_36=1
|
||||
Column_37=1
|
||||
Column_47=1
|
||||
Column_50=1
|
||||
Column_52=1
|
||||
Column_60=1
|
||||
Column_64=1
|
||||
Column_68=1
|
||||
Column_69=1
|
||||
Column_72=1
|
||||
Column_77=1
|
||||
Column_80=1
|
||||
|
||||
parameters:
|
||||
[boosting: gbdt]
|
||||
[objective: binary]
|
||||
[metric: binary_logloss]
|
||||
[tree_learner: serial]
|
||||
[device_type: cpu]
|
||||
[data_sample_strategy: bagging]
|
||||
[data: ]
|
||||
[valid: ]
|
||||
[num_iterations: 1200]
|
||||
[learning_rate: 0.04]
|
||||
[num_leaves: 31]
|
||||
[num_threads: 4]
|
||||
[seed: 42]
|
||||
[deterministic: 0]
|
||||
[force_col_wise: 0]
|
||||
[force_row_wise: 0]
|
||||
[histogram_pool_size: -1]
|
||||
[max_depth: 6]
|
||||
[min_data_in_leaf: 24]
|
||||
[min_sum_hessian_in_leaf: 0.001]
|
||||
[bagging_fraction: 0.84]
|
||||
[pos_bagging_fraction: 1]
|
||||
[neg_bagging_fraction: 1]
|
||||
[bagging_freq: 5]
|
||||
[bagging_seed: 400]
|
||||
[bagging_by_query: 0]
|
||||
[feature_fraction: 0.82]
|
||||
[feature_fraction_bynode: 1]
|
||||
[feature_fraction_seed: 30056]
|
||||
[extra_trees: 0]
|
||||
[extra_seed: 12879]
|
||||
[early_stopping_round: 0]
|
||||
[early_stopping_min_delta: 0]
|
||||
[first_metric_only: 0]
|
||||
[max_delta_step: 0]
|
||||
[lambda_l1: 0]
|
||||
[lambda_l2: 0]
|
||||
[linear_lambda: 0]
|
||||
[min_gain_to_split: 0]
|
||||
[drop_rate: 0.1]
|
||||
[max_drop: 50]
|
||||
[skip_drop: 0.5]
|
||||
[xgboost_dart_mode: 0]
|
||||
[uniform_drop: 0]
|
||||
[drop_seed: 17869]
|
||||
[top_rate: 0.2]
|
||||
[other_rate: 0.1]
|
||||
[min_data_per_group: 100]
|
||||
[max_cat_threshold: 32]
|
||||
[cat_l2: 10]
|
||||
[cat_smooth: 10]
|
||||
[max_cat_to_onehot: 4]
|
||||
[top_k: 20]
|
||||
[monotone_constraints: ]
|
||||
[monotone_constraints_method: basic]
|
||||
[monotone_penalty: 0]
|
||||
[feature_contri: ]
|
||||
[forcedsplits_filename: ]
|
||||
[refit_decay_rate: 0.9]
|
||||
[cegb_tradeoff: 1]
|
||||
[cegb_penalty_split: 0]
|
||||
[cegb_penalty_feature_lazy: ]
|
||||
[cegb_penalty_feature_coupled: ]
|
||||
[path_smooth: 0]
|
||||
[interaction_constraints: ]
|
||||
[verbosity: -1]
|
||||
[saved_feature_importance_type: 0]
|
||||
[use_quantized_grad: 0]
|
||||
[num_grad_quant_bins: 4]
|
||||
[quant_train_renew_leaf: 0]
|
||||
[stochastic_rounding: 1]
|
||||
[linear_tree: 0]
|
||||
[max_bin: 255]
|
||||
[max_bin_by_feature: ]
|
||||
[min_data_in_bin: 3]
|
||||
[bin_construct_sample_cnt: 200000]
|
||||
[data_random_seed: 175]
|
||||
[is_enable_sparse: 1]
|
||||
[enable_bundle: 1]
|
||||
[use_missing: 1]
|
||||
[zero_as_missing: 0]
|
||||
[feature_pre_filter: 1]
|
||||
[pre_partition: 0]
|
||||
[two_round: 0]
|
||||
[header: 0]
|
||||
[label_column: ]
|
||||
[weight_column: ]
|
||||
[group_column: ]
|
||||
[ignore_column: ]
|
||||
[categorical_feature: ]
|
||||
[forcedbins_filename: ]
|
||||
[precise_float_parser: 0]
|
||||
[parser_config_file: ]
|
||||
[objective_seed: 16083]
|
||||
[num_class: 1]
|
||||
[is_unbalance: 0]
|
||||
[scale_pos_weight: 1]
|
||||
[sigmoid: 1]
|
||||
[boost_from_average: 1]
|
||||
[reg_sqrt: 0]
|
||||
[alpha: 0.9]
|
||||
[fair_c: 1]
|
||||
[poisson_max_delta_step: 0.7]
|
||||
[tweedie_variance_power: 1.5]
|
||||
[lambdarank_truncation_level: 30]
|
||||
[lambdarank_norm: 1]
|
||||
[label_gain: ]
|
||||
[lambdarank_position_bias_regularization: 0]
|
||||
[eval_at: ]
|
||||
[multi_error_top_k: 1]
|
||||
[auc_mu_weights: ]
|
||||
[num_machines: 1]
|
||||
[local_listen_port: 12400]
|
||||
[time_out: 120]
|
||||
[machine_list_filename: ]
|
||||
[machines: ]
|
||||
[gpu_platform_id: -1]
|
||||
[gpu_device_id: -1]
|
||||
[gpu_use_dp: 0]
|
||||
[num_gpu: 1]
|
||||
|
||||
end of parameters
|
||||
|
||||
pandas_categorical:null
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,467 @@
|
||||
"""
|
||||
Calibration Module for XGBoost Models
|
||||
=====================================
|
||||
Calibrates raw probabilities from XGBoost models using Isotonic Regression.
|
||||
Ensures that a predicted probability of 70% actually corresponds to a 70% win rate.
|
||||
|
||||
Usage:
|
||||
from ai_engine.models.calibration import Calibrator
|
||||
calibrator = Calibrator()
|
||||
calibrated_prob = calibrator.calibrate("ms", raw_prob)
|
||||
|
||||
# Training new calibration models:
|
||||
calibrator.train_calibration(valid_df, market="ms")
|
||||
"""
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from sklearn.isotonic import IsotonicRegression
|
||||
from sklearn.calibration import calibration_curve
|
||||
from sklearn.metrics import brier_score_loss
|
||||
|
||||
AI_ENGINE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CALIBRATION_DIR = os.path.join(AI_ENGINE_DIR, "models", "calibration")
|
||||
|
||||
os.makedirs(CALIBRATION_DIR, exist_ok=True)
|
||||
|
||||
# Supported markets for calibration
|
||||
SUPPORTED_MARKETS = [
|
||||
"ms", # Match Result (1X2) - multi-class, calibrated per class
|
||||
"ms_home", # Standard Home win probability
|
||||
"ms_home_heavy_fav", # Context: home odds <= 1.40
|
||||
"ms_home_fav", # Context: 1.40 < home odds <= 1.80
|
||||
"ms_home_balanced", # Context: 1.80 < home odds <= 2.50
|
||||
"ms_home_underdog", # Context: home odds > 2.50
|
||||
"ms_draw", # Draw probability
|
||||
"ms_away", # Away win probability
|
||||
"ou15", # Over/Under 1.5
|
||||
"ou25", # Over/Under 2.5
|
||||
"ou35", # Over/Under 3.5
|
||||
"btts", # Both Teams to Score
|
||||
"ht_ft", # Half-Time/Full-Time
|
||||
"dc", # Double Chance
|
||||
"ht", # Half-Time Result
|
||||
"ht_home", # Half-Time Home win
|
||||
"ht_draw", # Half-Time Draw
|
||||
"ht_away", # Half-Time Away win
|
||||
]
|
||||
|
||||
|
||||
class CalibrationMetrics:
|
||||
"""Stores calibration quality metrics for a market."""
|
||||
|
||||
def __init__(self):
|
||||
self.brier_score: float = 0.0
|
||||
self.calibration_error: float = 0.0
|
||||
self.sample_count: int = 0
|
||||
self.last_trained: str = ""
|
||||
self.mean_predicted: float = 0.0
|
||||
self.mean_actual: float = 0.0
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return {
|
||||
"brier_score": round(self.brier_score, 4),
|
||||
"calibration_error": round(self.calibration_error, 4),
|
||||
"sample_count": self.sample_count,
|
||||
"last_trained": self.last_trained,
|
||||
"mean_predicted": round(self.mean_predicted, 4),
|
||||
"mean_actual": round(self.mean_actual, 4),
|
||||
}
|
||||
|
||||
|
||||
class Calibrator:
|
||||
"""
|
||||
Probability calibration using Isotonic Regression.
|
||||
|
||||
Isotonic Regression is a non-parametric method that fits a piecewise
|
||||
constant function that is monotonically increasing. It's ideal for
|
||||
calibrating probabilities because:
|
||||
|
||||
1. It preserves ranking (if P(A) > P(B) before, P(A) > P(B) after)
|
||||
2. It doesn't assume a specific distribution shape
|
||||
3. It can correct systematic over/under-confidence
|
||||
|
||||
Example:
|
||||
# Before calibration: model predicts 70% but actual win rate is 60%
|
||||
# After calibration: model predicts 70% → calibrated to 60%
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.calibrators: Dict[str, IsotonicRegression] = {}
|
||||
self.metrics: Dict[str, CalibrationMetrics] = {}
|
||||
# Less aggressive shrinkage — only meaningful overconfident bands are pulled.
|
||||
# Default raised from ~0.85-0.90 to 0.95+ since the orchestrator and config
|
||||
# already apply market-level multipliers; double-shrinkage was the root cause
|
||||
# of 24-35pt avg calibrated-vs-raw drops in production traces.
|
||||
self.heuristic_fallback: Dict[str, float] = {
|
||||
"ms": 0.96,
|
||||
"ms_home": 0.96,
|
||||
"ms_home_heavy_fav": 0.98,
|
||||
"ms_home_fav": 0.96,
|
||||
"ms_home_balanced": 0.94,
|
||||
"ms_home_underdog": 0.92,
|
||||
"ms_draw": 0.94,
|
||||
"ms_away": 0.96,
|
||||
"ou15": 0.96,
|
||||
"ou25": 0.96,
|
||||
"ou35": 0.94,
|
||||
"btts": 0.96,
|
||||
"ht_ft": 0.92,
|
||||
"dc": 0.97,
|
||||
"ht": 0.92,
|
||||
"ht_home": 0.92,
|
||||
"ht_draw": 0.92,
|
||||
"ht_away": 0.92,
|
||||
}
|
||||
self._load_calibrators()
|
||||
|
||||
def _load_calibrators(self):
|
||||
"""Load trained calibrators for each market from disk."""
|
||||
for market in SUPPORTED_MARKETS:
|
||||
model_path = os.path.join(CALIBRATION_DIR, f"{market}_calibrator.pkl")
|
||||
metrics_path = os.path.join(CALIBRATION_DIR, f"{market}_metrics.json")
|
||||
|
||||
if os.path.exists(model_path):
|
||||
try:
|
||||
with open(model_path, "rb") as f:
|
||||
self.calibrators[market] = pickle.load(f)
|
||||
print(f"[Calibrator] Loaded calibration model for {market}")
|
||||
except Exception as e:
|
||||
print(f"[Calibrator] Warning: Failed to load {market}: {e}")
|
||||
|
||||
if os.path.exists(metrics_path):
|
||||
try:
|
||||
with open(metrics_path, "r") as f:
|
||||
data = json.load(f)
|
||||
metrics = CalibrationMetrics()
|
||||
metrics.brier_score = data.get("brier_score", 0.0)
|
||||
metrics.calibration_error = data.get("calibration_error", 0.0)
|
||||
metrics.sample_count = data.get("sample_count", 0)
|
||||
metrics.last_trained = data.get("last_trained", "")
|
||||
metrics.mean_predicted = data.get("mean_predicted", 0.0)
|
||||
metrics.mean_actual = data.get("mean_actual", 0.0)
|
||||
self.metrics[market] = metrics
|
||||
except Exception as e:
|
||||
print(f"[Calibrator] Warning: Failed to load metrics for {market}: {e}")
|
||||
|
||||
# Below this sample count, the isotonic model is treated as untrained
|
||||
# (raw_prob is returned). Between MIN and FLOOR we ramp from 0 to ~15%
|
||||
# trust. Between FLOOR and CEILING we ramp to full trust.
|
||||
# Rationale: 12-sample calibrators are statistical noise; even 30%
|
||||
# blending on them propagates the noise into the confidence value the
|
||||
# betting_brain reads downstream.
|
||||
HARD_MIN_SAMPLES = 50
|
||||
TRUSTED_SAMPLE_FLOOR = 100
|
||||
TRUSTED_SAMPLE_CEILING = 400
|
||||
# Hard cap on how far calibration can move probability in either direction.
|
||||
MAX_DELTA = 0.20
|
||||
|
||||
def calibrate(self, market_type: str, raw_prob: float, odds_val: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calibrate a raw probability using Isotonic Regression with safeguards.
|
||||
|
||||
Args:
|
||||
market_type (str): 'ms_home', 'ou25', 'btts', 'ht_ft', etc.
|
||||
raw_prob (float): The raw probability from XGBoost (0.0 - 1.0)
|
||||
odds_val (float, optional): The pre-match odds, used for context-aware bucket mapping
|
||||
|
||||
Returns:
|
||||
float: Calibrated probability (0.0 - 1.0)
|
||||
|
||||
Safeguards:
|
||||
* Low-sample trained models are blended with raw_prob to dampen overfit.
|
||||
* MAX_DELTA caps the per-call adjustment (prevents 40pp swings).
|
||||
"""
|
||||
# Normalize market type
|
||||
market_key = market_type.lower().replace("-", "_")
|
||||
|
||||
# Route to bucket if ms_home and odds provided
|
||||
if market_key == "ms_home" and odds_val is not None and odds_val > 1.0:
|
||||
if odds_val <= 1.40:
|
||||
bucket_key = "ms_home_heavy_fav"
|
||||
elif odds_val <= 1.80:
|
||||
bucket_key = "ms_home_fav"
|
||||
elif odds_val <= 2.50:
|
||||
bucket_key = "ms_home_balanced"
|
||||
else:
|
||||
bucket_key = "ms_home_underdog"
|
||||
|
||||
if bucket_key in self.calibrators:
|
||||
market_key = bucket_key
|
||||
|
||||
# If we have a trained Isotonic Regression model, use it (with safeguards)
|
||||
if market_key in self.calibrators:
|
||||
try:
|
||||
iso_pred = float(self.calibrators[market_key].predict([raw_prob])[0])
|
||||
|
||||
# Sample-count weighted blend with raw probability.
|
||||
# Sparse models barely move probability; mature models dominate.
|
||||
metrics = self.metrics.get(market_key)
|
||||
n_samples = metrics.sample_count if metrics else 0
|
||||
if n_samples < self.HARD_MIN_SAMPLES:
|
||||
# Below 50 samples isotonic fit is unreliable — bypass it
|
||||
# entirely and return raw_prob. The heuristic shrinkage
|
||||
# below would still apply a model-version multiplier elsewhere.
|
||||
return float(np.clip(raw_prob, 0.01, 0.99))
|
||||
if n_samples >= self.TRUSTED_SAMPLE_CEILING:
|
||||
iso_weight = 1.0
|
||||
elif n_samples <= self.TRUSTED_SAMPLE_FLOOR:
|
||||
# Linear ramp from 0% at HARD_MIN_SAMPLES to ~25% at FLOOR
|
||||
span = self.TRUSTED_SAMPLE_FLOOR - self.HARD_MIN_SAMPLES
|
||||
iso_weight = 0.25 * (n_samples - self.HARD_MIN_SAMPLES) / span
|
||||
else:
|
||||
# Linearly ramp 25% → 100% between floor and ceiling
|
||||
span = self.TRUSTED_SAMPLE_CEILING - self.TRUSTED_SAMPLE_FLOOR
|
||||
iso_weight = 0.25 + 0.75 * (n_samples - self.TRUSTED_SAMPLE_FLOOR) / span
|
||||
blended = iso_weight * iso_pred + (1.0 - iso_weight) * raw_prob
|
||||
|
||||
# Cap delta to avoid huge swings on noisy calibrators
|
||||
delta = blended - raw_prob
|
||||
if delta > self.MAX_DELTA:
|
||||
blended = raw_prob + self.MAX_DELTA
|
||||
elif delta < -self.MAX_DELTA:
|
||||
blended = raw_prob - self.MAX_DELTA
|
||||
|
||||
return float(np.clip(blended, 0.01, 0.99))
|
||||
except Exception as e:
|
||||
print(f"[Calibrator] Warning: Isotonic failed for {market_key}: {e}")
|
||||
# Fall through to heuristic
|
||||
|
||||
# Fallback to heuristic calibration
|
||||
return self._heuristic_calibrate(market_key, raw_prob)
|
||||
|
||||
def _heuristic_calibrate(self, market_type: str, raw_prob: float) -> float:
|
||||
"""
|
||||
Heuristic calibration fallback when no trained model exists.
|
||||
|
||||
This applies a conservative shrinkage towards the mean:
|
||||
- Binary markets (OU, BTTS): shrink towards 0.5
|
||||
- Multi-class (MS): shrink towards 0.33
|
||||
- HT/FT: stronger shrinkage due to higher variance
|
||||
"""
|
||||
# Get shrinkage factor for this market
|
||||
shrinkage = self.heuristic_fallback.get(market_type, 0.90)
|
||||
|
||||
if market_type in ["ms", "ms_home", "ms_home_heavy_fav", "ms_home_fav", "ms_home_balanced", "ms_home_underdog", "ms_draw", "ms_away"]:
|
||||
# Pull towards 0.33 (uniform for 3-class)
|
||||
return (raw_prob * shrinkage) + (0.33 * (1.0 - shrinkage))
|
||||
|
||||
elif market_type in ["ou15", "ou25", "ou35", "btts"]:
|
||||
# Pull towards 0.5 (uniform for binary)
|
||||
return (raw_prob * shrinkage) + (0.5 * (1.0 - shrinkage))
|
||||
|
||||
elif market_type in ["ht_ft", "ht"]:
|
||||
# Stronger shrinkage for high-variance markets
|
||||
return raw_prob * shrinkage
|
||||
|
||||
elif market_type == "dc":
|
||||
# Double chance is more reliable
|
||||
return (raw_prob * shrinkage) + (0.66 * (1.0 - shrinkage))
|
||||
|
||||
return raw_prob
|
||||
|
||||
def train_calibration(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
market: str,
|
||||
prob_col: str,
|
||||
actual_col: str,
|
||||
min_samples: int = 100,
|
||||
save: bool = True,
|
||||
) -> CalibrationMetrics:
|
||||
"""
|
||||
Train an Isotonic Regression calibration model for a specific market.
|
||||
|
||||
Args:
|
||||
df: DataFrame with predictions and actual outcomes
|
||||
market: Market identifier (e.g., 'ms_home', 'ou25', 'btts')
|
||||
prob_col: Column name for raw probabilities
|
||||
actual_col: Column name for actual outcomes (0 or 1)
|
||||
min_samples: Minimum samples required to train
|
||||
save: Whether to save the model to disk
|
||||
|
||||
Returns:
|
||||
CalibrationMetrics with quality metrics
|
||||
"""
|
||||
# Filter valid data
|
||||
valid_df = df[[prob_col, actual_col]].dropna()
|
||||
n_samples = len(valid_df)
|
||||
|
||||
if n_samples < min_samples:
|
||||
print(f"[Calibrator] Warning: Only {n_samples} samples for {market}, "
|
||||
f"need at least {min_samples}")
|
||||
metrics = CalibrationMetrics()
|
||||
metrics.sample_count = n_samples
|
||||
return metrics
|
||||
|
||||
# Extract arrays
|
||||
raw_probs = valid_df[prob_col].values
|
||||
actuals = valid_df[actual_col].values
|
||||
|
||||
# Train Isotonic Regression
|
||||
iso = IsotonicRegression(out_of_bounds="clip", increasing=True)
|
||||
iso.fit(raw_probs, actuals)
|
||||
|
||||
# Calculate calibrated probabilities
|
||||
calibrated_probs = iso.predict(raw_probs)
|
||||
|
||||
# Calculate metrics
|
||||
metrics = CalibrationMetrics()
|
||||
metrics.sample_count = n_samples
|
||||
metrics.last_trained = datetime.utcnow().isoformat()
|
||||
metrics.brier_score = brier_score_loss(actuals, calibrated_probs)
|
||||
metrics.mean_predicted = np.mean(raw_probs)
|
||||
metrics.mean_actual = np.mean(actuals)
|
||||
|
||||
# Calculate Expected Calibration Error (ECE)
|
||||
metrics.calibration_error = self._calculate_ece(
|
||||
calibrated_probs, actuals, n_bins=10
|
||||
)
|
||||
|
||||
# Store in memory
|
||||
self.calibrators[market] = iso
|
||||
self.metrics[market] = metrics
|
||||
|
||||
# Save to disk
|
||||
if save:
|
||||
self._save_calibration(market, iso, metrics)
|
||||
|
||||
print(f"[Calibrator] Trained {market}: "
|
||||
f"Brier={metrics.brier_score:.4f}, "
|
||||
f"ECE={metrics.calibration_error:.4f}, "
|
||||
f"n={n_samples}")
|
||||
|
||||
return metrics
|
||||
|
||||
def train_all_markets(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
market_config: Dict[str, Tuple[str, str]],
|
||||
min_samples: int = 100,
|
||||
) -> Dict[str, CalibrationMetrics]:
|
||||
"""
|
||||
Train calibration models for multiple markets at once.
|
||||
|
||||
Args:
|
||||
df: DataFrame with all predictions and outcomes
|
||||
market_config: Dict mapping market -> (prob_col, actual_col)
|
||||
e.g., {'ou25': ('ou25_over_prob', 'ou25_over_actual')}
|
||||
min_samples: Minimum samples per market
|
||||
|
||||
Returns:
|
||||
Dict of market -> CalibrationMetrics
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for market, (prob_col, actual_col) in market_config.items():
|
||||
print(f"\n[Calibrator] Training {market}...")
|
||||
try:
|
||||
metrics = self.train_calibration(
|
||||
df=df,
|
||||
market=market,
|
||||
prob_col=prob_col,
|
||||
actual_col=actual_col,
|
||||
min_samples=min_samples,
|
||||
save=True,
|
||||
)
|
||||
results[market] = metrics
|
||||
except Exception as e:
|
||||
print(f"[Calibrator] Failed to train {market}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def _calculate_ece(
|
||||
self,
|
||||
probs: np.ndarray,
|
||||
actuals: np.ndarray,
|
||||
n_bins: int = 10
|
||||
) -> float:
|
||||
"""
|
||||
Calculate Expected Calibration Error (ECE).
|
||||
|
||||
ECE = sum(|bin_accuracy - bin_confidence| * bin_weight)
|
||||
|
||||
Lower is better. Perfect calibration = 0.
|
||||
"""
|
||||
bin_boundaries = np.linspace(0, 1, n_bins + 1)
|
||||
ece = 0.0
|
||||
|
||||
for i in range(n_bins):
|
||||
in_bin = (probs >= bin_boundaries[i]) & (probs < bin_boundaries[i + 1])
|
||||
prop_in_bin = np.mean(in_bin)
|
||||
|
||||
if prop_in_bin > 0:
|
||||
accuracy_in_bin = np.mean(actuals[in_bin])
|
||||
avg_confidence_in_bin = np.mean(probs[in_bin])
|
||||
ece += np.abs(accuracy_in_bin - avg_confidence_in_bin) * prop_in_bin
|
||||
|
||||
return ece
|
||||
|
||||
def _save_calibration(
|
||||
self,
|
||||
market: str,
|
||||
calibrator: IsotonicRegression,
|
||||
metrics: CalibrationMetrics
|
||||
):
|
||||
"""Save calibration model and metrics to disk."""
|
||||
# Save model
|
||||
model_path = os.path.join(CALIBRATION_DIR, f"{market}_calibrator.pkl")
|
||||
with open(model_path, "wb") as f:
|
||||
pickle.dump(calibrator, f)
|
||||
|
||||
# Save metrics
|
||||
metrics_path = os.path.join(CALIBRATION_DIR, f"{market}_metrics.json")
|
||||
with open(metrics_path, "w") as f:
|
||||
json.dump(metrics.to_dict(), f, indent=2)
|
||||
|
||||
print(f"[Calibrator] Saved {market} to {CALIBRATION_DIR}")
|
||||
|
||||
def get_calibration_report(self) -> Dict[str, Any]:
|
||||
"""Generate a summary report of all calibration models."""
|
||||
report = {
|
||||
"trained_markets": list(self.calibrators.keys()),
|
||||
"metrics": {},
|
||||
"heuristic_only": [],
|
||||
}
|
||||
|
||||
for market in SUPPORTED_MARKETS:
|
||||
if market in self.metrics:
|
||||
report["metrics"][market] = self.metrics[market].to_dict()
|
||||
elif market not in self.calibrators:
|
||||
report["heuristic_only"].append(market)
|
||||
|
||||
return report
|
||||
|
||||
def get_calibrated_probabilities(
|
||||
self,
|
||||
market: str,
|
||||
raw_probs: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Batch calibration for array of probabilities.
|
||||
|
||||
Args:
|
||||
market: Market type
|
||||
raw_probs: Array of raw probabilities
|
||||
|
||||
Returns:
|
||||
Array of calibrated probabilities
|
||||
"""
|
||||
return np.array([self.calibrate(market, p) for p in raw_probs])
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_calibrator_instance: Optional[Calibrator] = None
|
||||
|
||||
|
||||
def get_calibrator() -> Calibrator:
|
||||
"""Get or create the global Calibrator instance."""
|
||||
global _calibrator_instance
|
||||
if _calibrator_instance is None:
|
||||
_calibrator_instance = Calibrator()
|
||||
return _calibrator_instance
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"brier_score": 0.1264,
|
||||
"calibration_error": 0.0,
|
||||
"sample_count": 26,
|
||||
"last_trained": "2026-05-11T23:38:11.473645",
|
||||
"mean_predicted": 0.757,
|
||||
"mean_actual": 0.6923
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"brier_score": 0.1667,
|
||||
"calibration_error": 0.0,
|
||||
"sample_count": 12,
|
||||
"last_trained": "2026-05-11T23:38:11.484990",
|
||||
"mean_predicted": 0.3579,
|
||||
"mean_actual": 0.25
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"brier_score": 0.196,
|
||||
"calibration_error": 0.0,
|
||||
"sample_count": 13,
|
||||
"last_trained": "2026-05-11T23:38:11.500966",
|
||||
"mean_predicted": 0.7239,
|
||||
"mean_actual": 0.6154
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"brier_score": 0.2222,
|
||||
"calibration_error": 0.0,
|
||||
"sample_count": 18,
|
||||
"last_trained": "2026-05-11T23:38:11.508353",
|
||||
"mean_predicted": 0.6258,
|
||||
"mean_actual": 0.5
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
League-Specific Model Loader
|
||||
=============================
|
||||
Loads per-league XGBoost models + isotonic calibrators trained by
|
||||
scripts/train_league_models.py and provides a unified prediction interface.
|
||||
|
||||
Falls back to general V25 for any market/league without a dedicated model.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import pickle
|
||||
from functools import lru_cache
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import xgboost as xgb
|
||||
|
||||
AI_ENGINE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LEAGUE_MODEL_DIR = os.path.join(AI_ENGINE_DIR, "models", "league_specific")
|
||||
|
||||
# Market file name → (num_class, label_list)
|
||||
MARKET_META: Dict[str, Tuple[int, list]] = {
|
||||
"ms": (3, ["1", "X", "2"]),
|
||||
"ou15": (2, ["Over", "Under"]),
|
||||
"ou25": (2, ["Over", "Under"]),
|
||||
"ou35": (2, ["Over", "Under"]),
|
||||
"btts": (2, ["Yes", "No"]),
|
||||
"ht": (3, ["1", "X", "2"]),
|
||||
"ht_ou05": (2, ["Over", "Under"]),
|
||||
"ht_ou15": (2, ["Over", "Under"]),
|
||||
"htft": (9, ["1/1","1/X","1/2","X/1","X/X","X/2","2/1","2/X","2/2"]),
|
||||
"oe": (2, ["Odd", "Even"]),
|
||||
"cards": (2, ["Over", "Under"]),
|
||||
"handicap": (3, ["1", "X", "2"]),
|
||||
}
|
||||
|
||||
# Signal key map (file key → uppercase signal key used in _get_v25_signal)
|
||||
FILE_TO_SIGNAL = {
|
||||
"ms": "MS", "ou15": "OU15", "ou25": "OU25", "ou35": "OU35",
|
||||
"btts": "BTTS", "ht": "HT", "ht_ou05": "HT_OU05", "ht_ou15": "HT_OU15",
|
||||
"htft": "HTFT", "oe": "OE", "cards": "CARDS", "handicap": "HCAP",
|
||||
}
|
||||
|
||||
|
||||
class LeagueModel:
|
||||
"""Holds XGBoost models + isotonic calibrators for one league."""
|
||||
|
||||
def __init__(self, league_id: str):
|
||||
self.league_id = league_id
|
||||
self.league_dir = os.path.join(LEAGUE_MODEL_DIR, league_id)
|
||||
self.models: Dict[str, xgb.Booster] = {} # market_key → booster
|
||||
self.calibrators: Dict[str, object] = {} # cal_key → isotonic
|
||||
self.feature_cols: Optional[list] = None
|
||||
self._loaded = False
|
||||
|
||||
def load(self) -> bool:
|
||||
if not os.path.isdir(self.league_dir):
|
||||
return False
|
||||
try:
|
||||
fc_path = os.path.join(self.league_dir, "feature_cols.json")
|
||||
if os.path.exists(fc_path):
|
||||
with open(fc_path) as f:
|
||||
self.feature_cols = json.load(f)
|
||||
|
||||
for mkey in MARKET_META:
|
||||
xgb_path = os.path.join(self.league_dir, f"xgb_{mkey}.json")
|
||||
if os.path.exists(xgb_path) and os.path.getsize(xgb_path) > 100:
|
||||
b = xgb.Booster()
|
||||
b.load_model(xgb_path)
|
||||
self.models[mkey] = b
|
||||
|
||||
for fname in os.listdir(self.league_dir):
|
||||
if fname.startswith("cal_") and fname.endswith(".pkl"):
|
||||
cal_key = fname[4:-4] # strip cal_ and .pkl
|
||||
with open(os.path.join(self.league_dir, fname), "rb") as f:
|
||||
self.calibrators[cal_key] = pickle.load(f)
|
||||
|
||||
self._loaded = bool(self.models or self.calibrators)
|
||||
return self._loaded
|
||||
except Exception as e:
|
||||
print(f"[LeagueModel] Load failed for {self.league_id}: {e}")
|
||||
return False
|
||||
|
||||
def has_market(self, mkey: str) -> bool:
|
||||
return mkey in self.models
|
||||
|
||||
def predict_market(
|
||||
self,
|
||||
mkey: str,
|
||||
feature_row: Dict[str, float],
|
||||
) -> Optional[Dict[str, float]]:
|
||||
"""
|
||||
Predict one market using league-specific XGBoost + isotonic calibration.
|
||||
Returns {label: prob} dict or None if no model available.
|
||||
"""
|
||||
if mkey not in self.models:
|
||||
return None
|
||||
|
||||
num_class, labels = MARKET_META[mkey]
|
||||
fc = self.feature_cols
|
||||
if fc is None:
|
||||
# Fallback to whatever the booster expects (it knows its feature names)
|
||||
fc = list(self.models[mkey].feature_names or [])
|
||||
|
||||
try:
|
||||
X = pd.DataFrame([{col: feature_row.get(col, 0.0) for col in fc}])
|
||||
dmat = xgb.DMatrix(X)
|
||||
raw = self.models[mkey].predict(dmat)
|
||||
|
||||
if num_class > 2:
|
||||
probs_arr = raw.reshape(-1, num_class)[0]
|
||||
probs = {labels[i]: float(probs_arr[i]) for i in range(num_class)}
|
||||
# Apply isotonic calibration per class
|
||||
cal_total = 0.0
|
||||
for i, label in enumerate(labels):
|
||||
cal_key = f"{mkey}_{i}"
|
||||
if cal_key in self.calibrators:
|
||||
p_cal = float(self.calibrators[cal_key].predict([probs_arr[i]])[0])
|
||||
probs[label] = max(0.01, min(0.99, p_cal))
|
||||
cal_total += probs[label]
|
||||
if cal_total > 0:
|
||||
probs = {k: v / cal_total for k, v in probs.items()}
|
||||
else:
|
||||
p = float(raw[0])
|
||||
cal_key = mkey
|
||||
if cal_key in self.calibrators:
|
||||
p = float(self.calibrators[cal_key].predict([p])[0])
|
||||
p = max(0.01, min(0.99, p))
|
||||
probs = {labels[0]: p, labels[1]: 1.0 - p}
|
||||
|
||||
return probs
|
||||
except Exception as e:
|
||||
print(f"[LeagueModel] predict_market({mkey}) failed for {self.league_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class LeagueModelLoader:
|
||||
"""
|
||||
In-memory cache for league-specific models.
|
||||
Thread-safe for single-process async servers (FastAPI/uvicorn).
|
||||
"""
|
||||
|
||||
def __init__(self, max_cached: int = 80):
|
||||
self._cache: Dict[str, Optional[LeagueModel]] = {}
|
||||
self._max_cached = max_cached
|
||||
|
||||
def get(self, league_id: str) -> Optional[LeagueModel]:
|
||||
"""Return loaded LeagueModel for this league, or None if unavailable."""
|
||||
if league_id in self._cache:
|
||||
return self._cache[league_id]
|
||||
|
||||
# Evict oldest entry if cache is full
|
||||
if len(self._cache) >= self._max_cached:
|
||||
oldest = next(iter(self._cache))
|
||||
del self._cache[oldest]
|
||||
|
||||
model = LeagueModel(league_id)
|
||||
loaded = model.load()
|
||||
self._cache[league_id] = model if loaded else None
|
||||
if loaded:
|
||||
n_models = len(model.models)
|
||||
n_cals = len(model.calibrators)
|
||||
print(f"[LeagueModel] Loaded {league_id}: {n_models} XGB models, {n_cals} calibrators")
|
||||
return self._cache[league_id]
|
||||
|
||||
def available_leagues(self) -> list:
|
||||
if not os.path.isdir(LEAGUE_MODEL_DIR):
|
||||
return []
|
||||
return [d for d in os.listdir(LEAGUE_MODEL_DIR)
|
||||
if os.path.isdir(os.path.join(LEAGUE_MODEL_DIR, d))]
|
||||
|
||||
def readiness_summary(self) -> dict:
|
||||
leagues = self.available_leagues()
|
||||
return {
|
||||
"league_specific_dir": LEAGUE_MODEL_DIR,
|
||||
"available_leagues": len(leagues),
|
||||
"cached": len([v for v in self._cache.values() if v is not None]),
|
||||
}
|
||||
|
||||
|
||||
# ── Singleton ──────────────────────────────────────────────────────
|
||||
_loader: Optional[LeagueModelLoader] = None
|
||||
|
||||
|
||||
def get_league_model_loader() -> LeagueModelLoader:
|
||||
global _loader
|
||||
if _loader is None:
|
||||
_loader = LeagueModelLoader()
|
||||
return _loader
|
||||
Binary file not shown.
@@ -0,0 +1,154 @@
|
||||
[
|
||||
"home_overall_elo",
|
||||
"away_overall_elo",
|
||||
"elo_diff",
|
||||
"home_home_elo",
|
||||
"away_away_elo",
|
||||
"home_form_elo",
|
||||
"away_form_elo",
|
||||
"form_elo_diff",
|
||||
"home_goals_avg",
|
||||
"home_conceded_avg",
|
||||
"away_goals_avg",
|
||||
"away_conceded_avg",
|
||||
"home_clean_sheet_rate",
|
||||
"away_clean_sheet_rate",
|
||||
"home_scoring_rate",
|
||||
"away_scoring_rate",
|
||||
"home_winning_streak",
|
||||
"away_winning_streak",
|
||||
"home_unbeaten_streak",
|
||||
"away_unbeaten_streak",
|
||||
"h2h_total_matches",
|
||||
"h2h_home_win_rate",
|
||||
"h2h_draw_rate",
|
||||
"h2h_avg_goals",
|
||||
"h2h_btts_rate",
|
||||
"h2h_over25_rate",
|
||||
"home_avg_possession",
|
||||
"away_avg_possession",
|
||||
"home_avg_shots_on_target",
|
||||
"away_avg_shots_on_target",
|
||||
"home_shot_conversion",
|
||||
"away_shot_conversion",
|
||||
"home_avg_corners",
|
||||
"away_avg_corners",
|
||||
"odds_ms_h",
|
||||
"odds_ms_d",
|
||||
"odds_ms_a",
|
||||
"implied_home",
|
||||
"implied_draw",
|
||||
"implied_away",
|
||||
"odds_ht_ms_h",
|
||||
"odds_ht_ms_d",
|
||||
"odds_ht_ms_a",
|
||||
"odds_ou05_o",
|
||||
"odds_ou05_u",
|
||||
"odds_ou15_o",
|
||||
"odds_ou15_u",
|
||||
"odds_ou25_o",
|
||||
"odds_ou25_u",
|
||||
"odds_ou35_o",
|
||||
"odds_ou35_u",
|
||||
"odds_ht_ou05_o",
|
||||
"odds_ht_ou05_u",
|
||||
"odds_ht_ou15_o",
|
||||
"odds_ht_ou15_u",
|
||||
"odds_btts_y",
|
||||
"odds_btts_n",
|
||||
"odds_ms_h_present",
|
||||
"odds_ms_d_present",
|
||||
"odds_ms_a_present",
|
||||
"odds_ht_ms_h_present",
|
||||
"odds_ht_ms_d_present",
|
||||
"odds_ht_ms_a_present",
|
||||
"odds_ou05_o_present",
|
||||
"odds_ou05_u_present",
|
||||
"odds_ou15_o_present",
|
||||
"odds_ou15_u_present",
|
||||
"odds_ou25_o_present",
|
||||
"odds_ou25_u_present",
|
||||
"odds_ou35_o_present",
|
||||
"odds_ou35_u_present",
|
||||
"odds_ht_ou05_o_present",
|
||||
"odds_ht_ou05_u_present",
|
||||
"odds_ht_ou15_o_present",
|
||||
"odds_ht_ou15_u_present",
|
||||
"odds_btts_y_present",
|
||||
"odds_btts_n_present",
|
||||
"home_xga",
|
||||
"away_xga",
|
||||
"league_avg_goals",
|
||||
"league_zero_goal_rate",
|
||||
"upset_atmosphere",
|
||||
"upset_motivation",
|
||||
"upset_fatigue",
|
||||
"upset_potential",
|
||||
"referee_home_bias",
|
||||
"referee_avg_goals",
|
||||
"referee_cards_total",
|
||||
"referee_avg_yellow",
|
||||
"referee_experience",
|
||||
"home_momentum_score",
|
||||
"away_momentum_score",
|
||||
"momentum_diff",
|
||||
"home_squad_quality",
|
||||
"away_squad_quality",
|
||||
"squad_diff",
|
||||
"home_key_players",
|
||||
"away_key_players",
|
||||
"home_missing_impact",
|
||||
"away_missing_impact",
|
||||
"home_goals_form",
|
||||
"away_goals_form",
|
||||
"home_lineup_goals_per90",
|
||||
"away_lineup_goals_per90",
|
||||
"home_lineup_assists_per90",
|
||||
"away_lineup_assists_per90",
|
||||
"home_squad_continuity",
|
||||
"away_squad_continuity",
|
||||
"home_top_scorer_form",
|
||||
"away_top_scorer_form",
|
||||
"home_avg_player_exp",
|
||||
"away_avg_player_exp",
|
||||
"home_goals_diversity",
|
||||
"away_goals_diversity",
|
||||
"h2h_home_goals_avg",
|
||||
"h2h_away_goals_avg",
|
||||
"h2h_recent_trend",
|
||||
"h2h_venue_advantage",
|
||||
"home_rolling5_goals",
|
||||
"home_rolling5_conceded",
|
||||
"home_rolling10_goals",
|
||||
"home_rolling10_conceded",
|
||||
"home_rolling20_goals",
|
||||
"home_rolling20_conceded",
|
||||
"away_rolling5_goals",
|
||||
"away_rolling5_conceded",
|
||||
"away_rolling10_goals",
|
||||
"away_rolling10_conceded",
|
||||
"home_rolling5_cs",
|
||||
"away_rolling5_cs",
|
||||
"home_venue_goals",
|
||||
"home_venue_conceded",
|
||||
"away_venue_goals",
|
||||
"away_venue_conceded",
|
||||
"home_goal_trend",
|
||||
"away_goal_trend",
|
||||
"home_days_rest",
|
||||
"away_days_rest",
|
||||
"match_month",
|
||||
"is_season_start",
|
||||
"is_season_end",
|
||||
"attack_vs_defense_home",
|
||||
"attack_vs_defense_away",
|
||||
"xg_diff",
|
||||
"form_momentum_interaction",
|
||||
"elo_form_consistency",
|
||||
"upset_x_elo_gap",
|
||||
"league_home_win_rate",
|
||||
"league_draw_rate",
|
||||
"league_btts_rate",
|
||||
"league_ou25_rate",
|
||||
"league_reliability_score"
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user