📊 Статистика и аналитика

Урок 5c: Данные для принятия решений

📈 Зачем нужна аналитика в играх?

Сила данных в игровой разработке

Аналитика в играх — это не просто сбор статистики. Это инструмент для понимания поведения игроков, оптимизации игрового процесса и принятия обоснованных решений по улучшению продукта.

Ключевые преимущества аналитики:

  • Понимание игроков — как они играют, где застревают, что их мотивирует
  • Выявление проблем — узкие места в геймплее, баги, недостатки баланса
  • Оптимизация — улучшение метрик удержания, вовлеченности, монетизации
  • Проверка гипотез — A/B тестирование изменений

📊 Основные метрики

  • DAU/WAU/MAU (активные пользователи)
  • Retention (удержание)
  • Session length (длительность сессии)
  • Conversion rate (конверсия)

🎮 Игровые метрики

  • Completion rate (процент завершения)
  • Difficulty curve (кривая сложности)
  • Drop-off points (точки оттока)
  • Feature usage (использование функций)

🎛️ Создание дашборда

Дашборд — центр управления данными

Дашборд — это интерактивная панель, которая отображает ключевые метрики в реальном времени. Хороший дашборд позволяет быстро оценить состояние игры и выявить тренды.

Принципы эффективного дашборда:

  • Показывать только важные метрики
  • Использовать понятные визуализации
  • Обновляться в реальном времени
  • Быть интерактивным

Демо: Игровой дашборд

👥
1,247
Активные игроки
+12% за неделю
🎮
8,942
Игр сыграно
+8% за день
⏱️
4.2м
Средняя сессия
-5% за день
🏆
68%
Завершают игру
+3% за неделю
Класс Analytics Dashboard
class GameDashboard { constructor() { this.metrics = { activePlayers: 1247, gamesPlayed: 8942, avgSession: 4.2, completionRate: 68 }; this.updateInterval = null; this.isRealTime = false; } // Обновление метрик updateMetrics() { // Симуляция изменения данных this.metrics.activePlayers += this.randomChange(50, 0.1); this.metrics.gamesPlayed += this.randomChange(100, 0.05); this.metrics.avgSession += this.randomChange(0.5, 0.05); this.metrics.completionRate += this.randomChange(2, 0.02); // Ограничиваем значения this.metrics.completionRate = Math.max(0, Math.min(100, this.metrics.completionRate)); this.metrics.avgSession = Math.max(1, this.metrics.avgSession); this.renderMetrics(); this.logMetricsUpdate(); } // Случайное изменение с трендом randomChange(maxChange, trend = 0) { const random = (Math.random() - 0.5) * 2; // -1 до 1 const trendFactor = Math.random() < 0.7 ? 1 : -1; // 70% позитивный тренд return (random * maxChange + trend * maxChange) * trendFactor; } // Отображение метрик renderMetrics() { this.animateValue('active-players', this.metrics.activePlayers, true); this.animateValue('games-played', this.metrics.gamesPlayed, true); this.animateValue('avg-session', this.metrics.avgSession.toFixed(1) + 'м'); this.animateValue('completion-rate', Math.round(this.metrics.completionRate) + '%'); } // Анимация изменения значений animateValue(elementId, newValue, isNumber = false) { const element = document.getElementById(elementId); const startValue = isNumber ? parseInt(element.textContent.replace(/[^\d]/g, '')) : element.textContent; element.style.transform = 'scale(1.1)'; element.style.color = '#4caf50'; // Обновляем значение if (isNumber) { element.textContent = newValue.toLocaleString(); } else { element.textContent = newValue; } // Возвращаем исходное состояние setTimeout(() => { element.style.transform = 'scale(1)'; element.style.color = ''; }, 500); } // Включение режима реального времени startRealTimeUpdates() { this.isRealTime = true; this.updateInterval = setInterval(() => { this.updateMetrics(); }, 5000); // Обновление каждые 5 секунд } // Выключение режима реального времени stopRealTimeUpdates() { this.isRealTime = false; if (this.updateInterval) { clearInterval(this.updateInterval); } } // Экспорт данных exportMetrics() { const data = { timestamp: new Date().toISOString(), metrics: this.metrics, trends: this.calculateTrends() }; return JSON.stringify(data, null, 2); } // Расчет трендов calculateTrends() { // Здесь должна быть логика сравнения с предыдущими периодами return { activePlayers: { change: 12, period: 'week' }, gamesPlayed: { change: 8, period: 'day' }, avgSession: { change: -5, period: 'day' }, completionRate: { change: 3, period: 'week' } }; } logMetricsUpdate() { console.log('Метрики обновлены:', { время: new Date().toLocaleTimeString(), активные: this.metrics.activePlayers, игры: this.metrics.gamesPlayed, сессия: this.metrics.avgSession, завершение: this.metrics.completionRate }); } }

📊 Визуализация данных

Графики и диаграммы

Правильная визуализация делает данные понятными и actionable. Разные типы данных требуют разных подходов к визуализации.

Типы визуализаций:

  • Линейные графики — тренды во времени
  • Столбчатые диаграммы — сравнение категорий
  • Круговые диаграммы — части от целого
  • Heatmap — интенсивность по двум осям

Демо: Интерактивный график

Количество игр по дням недели

ПН ВТ СР ЧТ ПТ СБ ВС

🔥 Heat Map активности

Карта активности игроков по часам

Низкая активность Средняя активность Высокая активность Пиковая активность
Создание интерактивных графиков
class DataVisualization { constructor() { this.chartData = [45, 67, 89, 23, 56, 78, 91]; this.heatmapData = this.generateHeatmapData(); } // Создание столбчатого графика renderChart() { const container = document.getElementById('games-chart'); container.innerHTML = ''; const maxValue = Math.max(...this.chartData); this.chartData.forEach((value, index) => { const bar = document.createElement('div'); bar.className = 'chart-bar'; bar.style.height = `${(value / maxValue) * 100}%`; bar.setAttribute('data-value', value); // Анимация появления bar.style.transform = 'scaleY(0)'; container.appendChild(bar); setTimeout(() => { bar.style.transform = 'scaleY(1)'; }, index * 100); }); } // Обновление данных графика updateChart() { this.chartData = this.chartData.map(value => Math.max(1, value + (Math.random() - 0.5) * 20) ); this.renderChart(); } // Генерация случайных данных randomizeChart() { this.chartData = Array.from({length: 7}, () => Math.floor(Math.random() * 100) + 10 ); this.renderChart(); } // Генерация данных для heatmap generateHeatmapData() { const data = []; // 7 дней × 24 часа = 168 ячеек for (let day = 0; day < 7; day++) { for (let hour = 0; hour < 24; hour++) { const baseActivity = this.getBaseActivity(day, hour); const randomFactor = Math.random() * 0.3 + 0.85; // 85-115% const activity = Math.round(baseActivity * randomFactor); data.push({ day, hour, activity, level: this.getActivityLevel(activity) }); } } return data; } // Базовая активность по времени getBaseActivity(day, hour) { // Выходные более активны const dayMultiplier = (day === 5 || day === 6) ? 1.3 : 1; // Пиковые часы: утро (8-10), обед (12-14), вечер (18-22) let hourMultiplier = 1; if (hour >= 8 && hour <= 10) hourMultiplier = 1.4; else if (hour >= 12 && hour <= 14) hourMultiplier = 1.2; else if (hour >= 18 && hour <= 22) hourMultiplier = 1.6; else if (hour >= 0 && hour <= 6) hourMultiplier = 0.3; return Math.round(50 * dayMultiplier * hourMultiplier); } // Уровень активности getActivityLevel(activity) { if (activity < 25) return 'low'; if (activity < 50) return 'medium'; if (activity < 75) return 'high'; return 'very-high'; } // Отображение heatmap renderHeatmap() { const container = document.getElementById('activity-heatmap'); container.innerHTML = ''; this.heatmapData.forEach(cell => { const cellElement = document.createElement('div'); cellElement.className = `heatmap-cell ${cell.level}`; cellElement.textContent = cell.activity; cellElement.title = `День ${cell.day + 1}, ${cell.hour}:00 - ${cell.activity} игроков`; container.appendChild(cellElement); }); } }

🧪 A/B тестирование

Научный подход к улучшению игры

A/B тестирование — это метод сравнения двух версий игры, чтобы понять, какая работает лучше. Это позволяет принимать решения на основе данных, а не предположений.

Что можно A/B тестировать:

  • Уровни сложности — какой баланс лучше удерживает игроков
  • UI элементы — кнопки, цвета, расположение
  • Награды — размер и частота выдачи
  • Обучение — разные способы объяснения механик

Демо: A/B тест кнопки "Играть"

Вариант A (Контроль)
Обычная синяя кнопка
3.2%
CTR (Click Through Rate)
Базовая версия
Вариант B (Тест)
Яркая градиентная кнопка
4.7%
CTR (Click Through Rate)
95% уверенность

Результат теста

Вариант B показал на +46.9% лучший результат

Рекомендуется внедрить в основную версию

Система A/B тестирования
class ABTestSystem { constructor() { this.tests = new Map(); this.userAssignments = new Map(); } // Создание нового теста createTest(testId, variants, trafficSplit = [50, 50]) { const test = { id: testId, variants, trafficSplit, results: variants.reduce((acc, variant) => { acc[variant.id] = { impressions: 0, conversions: 0, conversionRate: 0 }; return acc; }, {}), isActive: true, createdAt: Date.now() }; this.tests.set(testId, test); return test; } // Назначение пользователя в вариант assignUser(userId, testId) { const test = this.tests.get(testId); if (!test || !test.isActive) return null; // Проверяем, не назначен ли уже пользователь const userKey = `${userId}_${testId}`; if (this.userAssignments.has(userKey)) { return this.userAssignments.get(userKey); } // Назначаем в вариант на основе хеша ID пользователя const hash = this.hashUserId(userId); let cumulativeWeight = 0; let assignedVariant = null; for (let i = 0; i < test.variants.length; i++) { cumulativeWeight += test.trafficSplit[i]; if (hash <= cumulativeWeight) { assignedVariant = test.variants[i]; break; } } this.userAssignments.set(userKey, assignedVariant); this.recordImpression(testId, assignedVariant.id); return assignedVariant; } // Хеширование ID пользователя для консистентного назначения hashUserId(userId) { let hash = 0; for (let i = 0; i < userId.length; i++) { const char = userId.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Конвертируем в 32-битное число } return Math.abs(hash) % 100; // 0-99 } // Запись показа recordImpression(testId, variantId) { const test = this.tests.get(testId); if (test && test.results[variantId]) { test.results[variantId].impressions++; } } // Запись конверсии recordConversion(testId, variantId) { const test = this.tests.get(testId); if (test && test.results[variantId]) { test.results[variantId].conversions++; this.calculateConversionRate(testId, variantId); } } // Расчет конверсии calculateConversionRate(testId, variantId) { const test = this.tests.get(testId); const result = test.results[variantId]; if (result.impressions > 0) { result.conversionRate = (result.conversions / result.impressions) * 100; } } // Статистическая значимость calculateSignificance(testId) { const test = this.tests.get(testId); const variants = Object.keys(test.results); if (variants.length !== 2) return null; // Поддерживаем только A/B тесты const [variantA, variantB] = variants; const resultA = test.results[variantA]; const resultB = test.results[variantB]; // Упрощенная формула z-теста для пропорций const p1 = resultA.conversions / resultA.impressions; const p2 = resultB.conversions / resultB.impressions; const pooledP = (resultA.conversions + resultB.conversions) / (resultA.impressions + resultB.impressions); const se = Math.sqrt(pooledP * (1 - pooledP) * (1/resultA.impressions + 1/resultB.impressions)); const zScore = Math.abs(p1 - p2) / se; // Примерная конверсия z-score в p-value const pValue = 2 * (1 - this.normalCDF(Math.abs(zScore))); return { zScore, pValue, isSignificant: pValue < 0.05, confidence: (1 - pValue) * 100 }; } // Приблизительная CDF нормального распределения normalCDF(x) { return 0.5 * (1 + this.erf(x / Math.sqrt(2))); } // Приблизительная функция ошибок erf(x) { const a1 = 0.254829592; const a2 = -0.284496736; const a3 = 1.421413741; const a4 = -1.453152027; const a5 = 1.061405429; const p = 0.3275911; const sign = x < 0 ? -1 : 1; x = Math.abs(x); const t = 1.0 / (1.0 + p * x); const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); return sign * y; } // Получение результатов теста getTestResults(testId) { const test = this.tests.get(testId); if (!test) return null; const significance = this.calculateSignificance(testId); return { test, significance, duration: Date.now() - test.createdAt, winner: this.determineWinner(testId) }; } // Определение победителя determineWinner(testId) { const test = this.tests.get(testId); const variants = Object.keys(test.results); let winner = variants[0]; let maxRate = test.results[winner].conversionRate; variants.forEach(variantId => { const rate = test.results[variantId].conversionRate; if (rate > maxRate) { maxRate = rate; winner = variantId; } }); return winner; } }

🎯 Практическое задание

Создайте комплексную систему аналитики:

  1. Добавьте трекинг событий игрока (клики, время на уровне, ошибки)
  2. Создайте воронку конверсии от первого запуска до завершения игры
  3. Реализуйте cohort analysis (анализ когорт пользователей)
  4. Добавьте систему алертов при аномальных изменениях метрик
Подсказка: Event Tracking
class GameAnalytics { constructor() { this.events = []; this.sessionId = this.generateSessionId(); this.userId = this.getUserId(); } // Трекинг события trackEvent(eventType, properties = {}) { const event = { timestamp: Date.now(), sessionId: this.sessionId, userId: this.userId, type: eventType, properties: { ...properties, userAgent: navigator.userAgent, url: window.location.href } }; this.events.push(event); this.sendToAnalytics(event); } // Трекинг времени на экране trackScreenTime(screenName) { const startTime = Date.now(); return () => { const duration = Date.now() - startTime; this.trackEvent('screen_time', { screen: screenName, duration: duration, durationSeconds: Math.round(duration / 1000) }); }; } }
Финальный урок: Мотивация и удержание игроков →