Анализ целевой аудитории сообщества VK

Материал из Поле цифровой дидактики

Анализ аудитории сообществ ВКонтакте

Автор: Сабитова Алина

Группа: АДЭУ-221

Дисциплина: Работа с API социальных сетей и облачных сервисов

Статус проекта: Выполнен

Параметр Описание
Описание Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.
Область знаний Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.
Близкие понятия SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.
Среда разработки Python 3.8+, Flask, vk_api, matplotlib, pandas

Цель проекта

Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.

Задачи

  1. Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.
  2. Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.
  3. Визуализация — построить 4 графика:
    • Распределение по полу (круговая диаграмма)
    • Возрастное распределение (гистограмма)
    • Топ-5 городов (горизонтальная столбчатая диаграмма)
    • Вовлечённость по типам контента (столбчатая диаграмма)
  4. Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.
  5. Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.

Диаграмма работы приложения

Структура проекта

Структура проекта в VS Code:

📄 app.py
основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.
📁 templates/index.html
HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.
📁 static/style.css
файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.

Код приложения

app.py

Показать код
from flask import Flask, render_template, request, jsonify
import vk_api
import matplotlib
matplotlib.use('Agg')  
import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter
from datetime import datetime
import os
import base64
import io
import pandas as pd
import time

app = Flask(__name__)

# Папка для статики
os.makedirs('static', exist_ok=True)

# Настройка стиля seaborn
sns.set_style("whitegrid")
sns.set_palette("husl")

# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА
TOKEN = "ваш_токен_сюда"

def get_community_data(group_id):
    """Собирает данные о сообществе"""
    try:
        vk_session = vk_api.VkApi(token=TOKEN)
        vk = vk_session.get_api()
        
        # Определяем ID сообщества
        if str(group_id).lstrip('-').isdigit():
            owner_id = int(group_id)
            group_id_for_api = owner_id
        else:
            group_id_for_api = group_id
        
        # Получаем информацию о сообществе
        try:
            group = vk.groups.getById(group_id=group_id_for_api, fields='members_count,description')[0]
            community_name = group['name']
            members_count = group['members_count']
        except:
            group = vk.groups.getById(group_id=str(group_id_for_api), fields='members_count,description')[0]
            community_name = group['name']
            members_count = group['members_count']
        
        # Собираем подписчиков с пагинацией (до 2000)
        members_data = []
        try:
            total_to_collect = min(2000, members_count)
            offset = 0
            batch_size = 1000
            
            while offset < total_to_collect:
                members = vk.groups.getMembers(
                    group_id=group_id_for_api, 
                    fields='sex,bdate,city', 
                    count=batch_size,
                    offset=offset
                )
                
                for user in members['items']:
                    user_info = {}
                    
                    if user.get('sex') == 1:
                        user_info['sex'] = 'Женщины'
                    elif user.get('sex') == 2:
                        user_info['sex'] = 'Мужчины'
                    else:
                        user_info['sex'] = 'Не указан'
                    
                    if user.get('bdate'):
                        bdate = user['bdate']
                        if len(bdate.split('.')) == 3:
                            try:
                                year = int(bdate.split('.')[2])
                                age = datetime.now().year - year
                                if 0 < age < 100:
                                    user_info['age'] = age
                            except:
                                pass
                    
                    if user.get('city') and user['city'].get('title'):
                        user_info['city'] = user['city']['title']
                    
                    members_data.append(user_info)
                
                offset += len(members['items'])
                time.sleep(0.34)
                
        except Exception as e:
            print(f"Ошибка при сборе подписчиков: {e}")
        
        # Собираем посты
        posts_data = []
        try:
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip('-').isdigit() else group_id, count=20, filter='owner')
            for post in wall['items']:
                post_info = {
                    'text': post.get('text', '')[:100],
                    'likes': post['likes']['count'],
                    'comments': post['comments']['count'],
                    'reposts': post['reposts']['count'],
                    'views': post.get('views', {}).get('count', 0)
                }
                
                if post.get('attachments'):
                    attach_type = post['attachments'][0]['type']
                    if attach_type == 'photo':
                        post_info['type'] = 'Фото'
                    elif attach_type == 'video':
                        post_info['type'] = 'Видео'
                    elif attach_type == 'link':
                        post_info['type'] = 'Ссылка'
                    else:
                        post_info['type'] = 'Текст'
                else:
                    post_info['type'] = 'Текст'
                
                if post_info['views'] > 0:
                    post_info['er'] = (post_info['likes'] + post_info['comments'] + post_info['reposts']) / post_info['views'] * 1000
                else:
                    post_info['er'] = 0
                
                posts_data.append(post_info)
        except Exception as e:
            print(f"Ошибка при сборе постов: {e}")
        
        return {
            'success': True,
            'community_name': community_name,
            'members_count': members_count,
            'members': members_data,
            'posts': posts_data
        }
        
    except Exception as e:
        return {'success': False, 'error': str(e)}

def generate_charts(members_data, posts_data):
    """Генерирует графики с помощью seaborn"""
    charts = {}
    
    plt.rcParams['font.family'] = 'sans-serif'
    plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
    
    # 1. Распределение по полу
    if members_data:
        fig, ax = plt.subplots(figsize=(7, 5))
        sex_counts = Counter([m.get('sex', 'Не указан') for m in members_data])
        colors = ['#ff6b6b', '#4ecdc4', '#95a5a6']
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct='%1.1f%%',
               colors=colors[:len(sex_counts)], startangle=90,
               explode=[0.02] * len(sex_counts), shadow=True)
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['sex'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    # 2. Возрастное распределение
    ages = [m['age'] for m in members_data if 'age' in m]
    if ages:
        fig, ax = plt.subplots(figsize=(8, 5))
        sns.histplot(ages, bins=15, kde=True, color='#3498db', edgecolor='white', linewidth=1.5, alpha=0.7)
        ax.set_xlabel('Возраст')
        ax.set_ylabel('Количество подписчиков')
        avg_age = sum(ages) / len(ages)
        ax.axvline(avg_age, color='#e74c3c', linestyle='--', linewidth=2, label=f'Средний: {avg_age:.1f} лет')
        ax.legend()
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['age'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    # 3. Топ-5 городов
    cities = [m['city'] for m in members_data if 'city' in m and m['city'] != 'Не указан']
    if cities:
        fig, ax = plt.subplots(figsize=(9, 6))
        city_counts = Counter(cities).most_common(5)
        city_names, city_values = zip(*city_counts)
        
        df = pd.DataFrame({'Город': city_names, 'Количество': city_values})
        df = df.sort_values('Количество', ascending=True)
        
        sns.barplot(data=df, y='Город', x='Количество', hue='Город', palette='rocket', legend=False, ax=ax)
        ax.set_xlabel('Количество подписчиков')
        ax.set_ylabel('Город')
        plt.tight_layout()
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['cities'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    # 4. Вовлечённость по типам контента
    if posts_data:
        fig, ax = plt.subplots(figsize=(10, 6))
        type_er = {}
        for post in posts_data:
            if post['type'] not in type_er:
                type_er[post['type']] = []
            type_er[post['type']].append(post['er'])
        
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}
        df_er = pd.DataFrame({'Тип контента': list(avg_er.keys()), 'ER': list(avg_er.values())})
        df_er = df_er.sort_values('ER', ascending=False)
        
        bars = sns.barplot(data=df_er, x='Тип контента', y='ER', hue='Тип контента', palette='viridis', legend=False, ax=ax)
        ax.set_ylabel('ER (активность на 1000 просмотров)')
        ax.set_xlabel('Тип контента')
        
        for bar, val in zip(bars.patches, df_er['ER'].values):
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, 
                   f'{val:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
        
        plt.xticks(rotation=45)
        plt.tight_layout()
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['engagement'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    return charts

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/analyze', methods=['POST'])
def analyze():
    group_url = request.json.get('group_url', '').strip()
    
    if not group_url:
        return jsonify({'success': False, 'error': 'Введите ссылку на сообщество'})
    
    if 'vk.com/' in group_url:
        group_id = group_url.split('vk.com/')[-1].split('/')[0]
    else:
        group_id = group_url
    
    data = get_community_data(group_id)
    
    if not data['success']:
        return jsonify({'success': False, 'error': data.get('error', 'Ошибка при получении данных')})
    
    charts = generate_charts(data['members'], data['posts'])
    
    stats = {}
    
    if data['members']:
        sex_counts = Counter([m.get('sex', 'Не указан') for m in data['members']])
        stats['sex'] = dict(sex_counts)
    
    ages = [m['age'] for m in data['members'] if 'age' in m]
    if ages:
        stats['avg_age'] = sum(ages) / len(ages)
    
    cities = [m['city'] for m in data['members'] if 'city' in m and m['city'] != 'Не указан']
    if cities:
        stats['top_cities'] = Counter(cities).most_common(5)
    
    if data['posts']:
        avg_er = sum(p['er'] for p in data['posts']) / len(data['posts'])
        stats['avg_er'] = avg_er
        type_er = {}
        for post in data['posts']:
            if post['type'] not in type_er:
                type_er[post['type']] = []
            type_er[post['type']].append(post['er'])
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))
        stats['best_post_type'] = best_type
    
    recommendations = []
    if 'avg_age' in stats:
        if stats['avg_age'] < 20:
            recommendations.append("🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы")
        elif stats['avg_age'] < 30:
            recommendations.append("🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент")
        else:
            recommendations.append("🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент")
    
    if stats.get('best_post_type'):
        recommendations.append(f"📈 Лучший формат контента: {stats['best_post_type']} → публикуйте чаще именно его")
    
    if data['posts'] and stats.get('avg_er'):
        if stats['avg_er'] < 50:
            recommendations.append("⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив")
        elif stats['avg_er'] > 150:
            recommendations.append("🔥 Отличная вовлечённость! Продолжайте в том же духе")
    
    if stats.get('top_cities'):
        main_city = stats['top_cities'][0][0]
        recommendations.append(f"📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия")
    
    return jsonify({
        'success': True,
        'community_name': data['community_name'],
        'members_count': data['members_count'],
        'stats': stats,
        'charts': charts,
        'recommendations': recommendations
    })

if __name__ == '__main__':
    app.run(debug=True)

templates/index.html

Показать код
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Анализ аудитории сообществ VK</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <div class="card">
            <h1>📱 Анализ аудитории сообществ VK</h1>
            <p class="subtitle">Анализ аудитории сообщества и рекомендации по контенту</p>
            
            <div class="input-group">
                <input type="text" id="groupUrl" placeholder="Введите ссылку или ID сообщества">
                <button onclick="analyze()" id="analyzeBtn">🔍 Анализировать</button>
            </div>
            
            <div class="loader" id="loader">
                <div class="loader-spinner"></div>
                <p>Анализируем сообщество... Это может занять до 30 секунд</p>
            </div>
            
            <div class="results" id="results">
                <div class="stats" id="stats"></div>
                <div class="chart-grid" id="charts"></div>
                <div class="recommendations" id="recommendations"></div>
            </div>
        </div>
    </div>
    
    <script>
        async function analyze() {
            const groupUrl = document.getElementById('groupUrl').value;
            if (!groupUrl) {
                alert('Введите ссылку или ID сообщества');
                return;
            }
            
            const btn = document.getElementById('analyzeBtn');
            const loader = document.getElementById('loader');
            const results = document.getElementById('results');
            
            btn.disabled = true;
            loader.style.display = 'block';
            results.style.display = 'none';
            
            try {
                const response = await fetch('/analyze', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ group_url: groupUrl })
                });
                
                const data = await response.json();
                
                if (data.success) {
                    displayResults(data);
                } else {
                    showError(data.error);
                }
            } catch (error) {
                showError('Ошибка соединения с сервером');
            } finally {
                btn.disabled = false;
                loader.style.display = 'none';
            }
        }
        
        function displayResults(data) {
            const statsHtml = `
                <div class="stat">
                    <div class="stat-value">${data.community_name}</div>
                    <div class="stat-label">Название сообщества</div>
                </div>
                <div class="stat">
                    <div class="stat-value">${data.members_count.toLocaleString()}</div>
                    <div class="stat-label">Подписчиков</div>
                </div>
                ${data.stats.avg_age ? `
                <div class="stat">
                    <div class="stat-value">${data.stats.avg_age.toFixed(1)} лет</div>
                    <div class="stat-label">Средний возраст</div>
                </div>
                ` : ''}
                ${data.stats.avg_er ? `
                <div class="stat">
                    <div class="stat-value">${data.stats.avg_er.toFixed(2)}</div>
                    <div class="stat-label">Средняя вовлечённость (ER)</div>
                </div>
                ` : ''}
            `;
            document.getElementById('stats').innerHTML = statsHtml;
            
            let chartsHtml = '';
            if (data.charts.sex) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>👥 Распределение по полу</h3>
                        <img src="data:image/png;base64,${data.charts.sex}" alt="Распределение по полу">
                    </div>
                `;
            }
            if (data.charts.age) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>🎂 Возрастное распределение</h3>
                        <img src="data:image/png;base64,${data.charts.age}" alt="Возраст">
                    </div>
                `;
            }
            if (data.charts.cities) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>🏙️ Топ-5 городов</h3>
                        <img src="data:image/png;base64,${data.charts.cities}" alt="Города">
                    </div>
                `;
            }
            if (data.charts.engagement) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>📈 Вовлечённость по типам контента</h3>
                        <img src="data:image/png;base64,${data.charts.engagement}" alt="ER">
                    </div>
                `;
            }
            document.getElementById('charts').innerHTML = chartsHtml;
            
            let recHtml = '<h3>💡 Рекомендации по контент-стратегии</h3><ul>';
            data.recommendations.forEach(rec => {
                recHtml += `<li>${rec}</li>`;
            });
            recHtml += '</ul>';
            document.getElementById('recommendations').innerHTML = recHtml;
            
            document.getElementById('results').style.display = 'block';
        }
        
        function showError(message) {
            const results = document.getElementById('results');
            results.style.display = 'block';
            results.innerHTML = `<div class="error">❌ Ошибка: ${message}</div>`;
        }
    </script>
</body>
</html>

static/style.css

Показать код
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
    min-height: 100vh;
    padding: 40px 20px;
}

.container {
    max-width: 1400px;
    margin: 0 auto;
}

.card {
    background: rgba(255, 255, 255, 0.98);
    backdrop-filter: blur(10px);
    border-radius: 32px;
    padding: 40px;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

h1 {
    font-size: 2.5rem;
    font-weight: 700;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    margin-bottom: 12px;
}

.subtitle {
    font-size: 1.1rem;
    color: #6b7280;
    margin-bottom: 35px;
    border-left: 4px solid #667eea;
    padding-left: 16px;
}

.input-group {
    display: flex;
    gap: 12px;
    margin-bottom: 30px;
    flex-wrap: wrap;
}

input {
    flex: 1;
    padding: 16px 20px;
    border: 2px solid #e5e7eb;
    border-radius: 20px;
    font-size: 16px;
    background: #f9fafb;
}

input:focus {
    outline: none;
    border-color: #667eea;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}

button {
    padding: 16px 32px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 20px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
}

button:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}

.loader {
    display: none;
    text-align: center;
    padding: 50px;
}

.loader-spinner {
    width: 50px;
    height: 50px;
    border: 4px solid #e5e7eb;
    border-top-color: #667eea;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin: 0 auto 20px;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

.results {
    display: none;
    animation: fadeInUp 0.5s ease;
}

@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 20px;
    margin-bottom: 40px;
}

.stat {
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    padding: 24px 20px;
    border-radius: 24px;
    text-align: center;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.stat-value {
    font-size: 32px;
    font-weight: 800;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.stat-label {
    color: #6b7280;
    font-size: 14px;
    font-weight: 500;
}

.chart-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
    gap: 30px;
    margin: 40px 0;
}

.chart-card {
    background: #ffffff;
    border-radius: 24px;
    padding: 24px;
    text-align: center;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}

.chart-card h3 {
    font-size: 1.3rem;
    font-weight: 600;
    margin-bottom: 20px;
    color: #1f2937;
}

.chart-card img {
    max-width: 100%;
    height: auto;
    border-radius: 16px;
}

.recommendations {
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
    border-left: 5px solid #22c55e;
    padding: 28px;
    border-radius: 24px;
    margin-top: 30px;
}

.recommendations h3 {
    color: #15803d;
    margin-bottom: 20px;
    font-size: 1.3rem;
    font-weight: 700;
}

.recommendations ul {
    list-style: none;
    padding-left: 0;
}

.recommendations li {
    padding: 12px 0;
    color: #166534;
    border-bottom: 1px solid #bbf7d0;
}

.recommendations li:last-child {
    border-bottom: none;
}

.error {
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
    color: #dc2626;
    padding: 20px;
    border-radius: 20px;
    text-align: center;
}

@media (max-width: 768px) {
    body { padding: 20px 15px; }
    .card { padding: 24px; }
    h1 { font-size: 1.8rem; }
    .input-group { flex-direction: column; }
    .chart-grid { grid-template-columns: 1fr; }
    .stats { grid-template-columns: 1fr; }
}

Ход выполнения

Устанавливаем необходимые библиотеки:

Запускаем основной скрипт приложения:

После запуска приложение доступно по адресу: http://127.0.0.1:5000

Результат

Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:

Введем ссылку на сообщество "Афиша" и посмотрим результат:

В приложении выводятся:

Ключевые метрики

Ключевые метрики сообщества

График, отображающий распределение подписчиков сообщества по полу

Распределение подписчиков по полу

График, показывающий распределение подписчиков по возрасту с указанием среднего значения

Возрастное распределение подписчиков

График, выводящий 5 городов с наибольшим количеством подписчиков

Топ-5 городов по количеству подписчиков

График сравнения вовлеченности подписчиков в зависимости от типа контента

Вовлечённость по типам контента

Рекомендации

По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.

Рекомендации по контент-стратегии

Выводы

  1. Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.
  2. Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).
  3. Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:
    • Распределение по полу (круговая диаграмма)
    • Возрастное распределение (гистограмма)
    • Топ-5 городов (горизонтальная столбчатая диаграмма)
    • Вовлечённость по типам контента (столбчатая диаграмма)
  4. Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.
  5. Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.