Анализ целевой аудитории сообщества VK: различия между версиями
Нет описания правки |
Нет описания правки |
||
| Строка 45: | Строка 45: | ||
Структура проекта в VS Code: | Структура проекта в VS Code: | ||
[[Файл:Vk api1.png|600px]] | [[Файл:Vk api1.png|600px]] | ||
Основной python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask: | |||
``` | |||
from flask import Flask, render_template, request, jsonify | |||
import vk_api | |||
import matplotlib | |||
matplotlib.use('Agg') | |||
import matplotlib.pyplot as plt | |||
from collections import Counter | |||
from datetime import datetime | |||
import os | |||
import base64 | |||
import io | |||
app = Flask(__name__) | |||
# Папка для статики | |||
os.makedirs('static', exist_ok=True) | |||
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: | |||
# Если не получилось, пробуем как числовой ID | |||
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'] | |||
# Собираем подписчиков | |||
members_data = [] | |||
try: | |||
members = vk.groups.getMembers(group_id=group_id_for_api, fields='sex,bdate,city', count=500) | |||
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) | |||
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'] = 'Текст' | |||
# ER | |||
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): | |||
"""Генерирует графики и возвращает их в base64""" | |||
charts = {} | |||
# Настройка шрифтов для русских букв | |||
plt.rcParams['font.family'] = 'sans-serif' | |||
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans'] | |||
# 1. Распределение по полу | |||
if members_data: | |||
fig, ax = plt.subplots(figsize=(6, 4)) | |||
sex_counts = Counter([m.get('sex', 'Не указан') for m in members_data]) | |||
ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct='%1.1f%%') | |||
buf = io.BytesIO() | |||
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100) | |||
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=(6, 4)) | |||
ax.hist(ages, bins=15, color='skyblue', edgecolor='black') | |||
ax.set_xlabel('Возраст') | |||
ax.set_ylabel('Количество') | |||
buf = io.BytesIO() | |||
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100) | |||
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=(8, 5)) | |||
city_counts = Counter(cities).most_common(5) | |||
city_names, city_values = zip(*city_counts) | |||
# Создаём горизонтальную столбчатую диаграмму | |||
bars = ax.barh(city_names, city_values, color='lightcoral', edgecolor='darkred') | |||
ax.set_title('Топ-5 городов аудитории', fontsize=14, fontweight='bold') | |||
ax.set_xlabel('Количество подписчиков', fontsize=11) | |||
ax.set_ylabel('Город', fontsize=11) | |||
plt.tight_layout() | |||
buf = io.BytesIO() | |||
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100) | |||
buf.seek(0) | |||
charts['cities'] = base64.b64encode(buf.getvalue()).decode() | |||
plt.close(fig) | |||
# 4. Вовлечённость по типам контента | |||
if posts_data: | |||
fig, ax = plt.subplots(figsize=(8, 5)) | |||
type_er = {} | |||
for post in posts_data: | |||
post_type = post['type'] | |||
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()} | |||
bars = ax.bar(avg_er.keys(), avg_er.values(), color='lightgreen', edgecolor='darkgreen') | |||
ax.set_ylabel('ER (на 1000 просмотров)', fontsize=11) | |||
plt.xticks(rotation=45) | |||
# Добавляем значения на столбцы | |||
for bar, val in zip(bars, avg_er.values()): | |||
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, | |||
f'{val:.1f}', ha='center', va='bottom', fontsize=9) | |||
plt.tight_layout() | |||
buf = io.BytesIO() | |||
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100) | |||
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': 'Введите ссылку на сообщество'}) | |||
# Извлекаем ID сообщества из ссылки | |||
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) | |||
stats['min_age'] = min(ages) | |||
stats['max_age'] = max(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 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход") | |||
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] | |||
main_city_count = stats['top_cities'][0][1] | |||
total_with_city = sum(count for _, count in stats['top_cities']) | |||
if len(stats['top_cities']) > 1: | |||
recommendations.append(f"🏙️ Топ-5 городов: {', '.join([f'{city} ({cnt})' for city, cnt in stats['top_cities'][:3]])}") | |||
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) | |||
``` | |||
После запуска приложение доступно по адресу: '''http://127.0.0.1:5000''' | После запуска приложение доступно по адресу: '''http://127.0.0.1:5000''' | ||
Версия от 22:07, 26 марта 2026
| Параметр | Описание |
|---|---|
| Описание | Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию. |
| Область знаний | Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование. |
| Близкие понятия | SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение. |
| Среда разработки | Python 3.8+, Flask, vk_api, matplotlib, pandas |
Цель проекта
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.
Задачи
- Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.
- Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.
- Визуализация — построить 4 графика:
- Распределение по полу (круговая диаграмма)
- Возрастное распределение (гистограмма)
- Топ-5 городов (горизонтальная столбчатая диаграмма)
- Вовлечённость по типам контента (столбчатая диаграмма)
- Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.
- Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.
Диаграмма работы приложения
Структура проекта
Структура проекта в VS Code:
Основной python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask: ``` from flask import Flask, render_template, request, jsonify import vk_api import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from collections import Counter from datetime import datetime import os import base64 import io
app = Flask(__name__)
- Папка для статики
os.makedirs('static', exist_ok=True)
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:
# Если не получилось, пробуем как числовой ID
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']
# Собираем подписчиков
members_data = []
try:
members = vk.groups.getMembers(group_id=group_id_for_api, fields='sex,bdate,city', count=500)
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)
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'] = 'Текст'
# ER
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):
"""Генерирует графики и возвращает их в base64"""
charts = {}
# Настройка шрифтов для русских букв
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
# 1. Распределение по полу
if members_data:
fig, ax = plt.subplots(figsize=(6, 4))
sex_counts = Counter([m.get('sex', 'Не указан') for m in members_data])
ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct='%1.1f%%')
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
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=(6, 4))
ax.hist(ages, bins=15, color='skyblue', edgecolor='black')
ax.set_xlabel('Возраст')
ax.set_ylabel('Количество')
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
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=(8, 5))
city_counts = Counter(cities).most_common(5)
city_names, city_values = zip(*city_counts)
# Создаём горизонтальную столбчатую диаграмму
bars = ax.barh(city_names, city_values, color='lightcoral', edgecolor='darkred')
ax.set_title('Топ-5 городов аудитории', fontsize=14, fontweight='bold')
ax.set_xlabel('Количество подписчиков', fontsize=11)
ax.set_ylabel('Город', fontsize=11)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
buf.seek(0)
charts['cities'] = base64.b64encode(buf.getvalue()).decode()
plt.close(fig)
# 4. Вовлечённость по типам контента
if posts_data:
fig, ax = plt.subplots(figsize=(8, 5))
type_er = {}
for post in posts_data:
post_type = post['type']
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()}
bars = ax.bar(avg_er.keys(), avg_er.values(), color='lightgreen', edgecolor='darkgreen')
ax.set_ylabel('ER (на 1000 просмотров)', fontsize=11)
plt.xticks(rotation=45)
# Добавляем значения на столбцы
for bar, val in zip(bars, avg_er.values()):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f'{val:.1f}', ha='center', va='bottom', fontsize=9)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
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': 'Введите ссылку на сообщество'})
# Извлекаем ID сообщества из ссылки
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)
stats['min_age'] = min(ages)
stats['max_age'] = max(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 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход")
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]
main_city_count = stats['top_cities'][0][1]
total_with_city = sum(count for _, count in stats['top_cities'])
if len(stats['top_cities']) > 1:
recommendations.append(f"🏙️ Топ-5 городов: {', '.join([f'{city} ({cnt})' for city, cnt in stats['top_cities'][:3]])}")
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)
```
После запуска приложение доступно по адресу: http://127.0.0.1:5000
Выводы
- Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.
- Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).
- Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:
- Распределение по полу (круговая диаграмма)
- Возрастное распределение (гистограмма)
- Топ-5 городов (горизонтальная столбчатая диаграмма)
- Вовлечённость по типам контента (столбчатая диаграмма)
- Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.
- Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.

