Собесов

Valiotti Analytics — пивот, retention и дашборд индустрии видеоигр

SQLSQL fundamentals + dashboardЛёгкаяJunior

Условие

Четыре мини-задачи:

  1. SQL: из city_population(city, population) найти город с минимальным населением.
  2. SQL: из таблицы (ID, Name, Val) сделать пивот → (ID, A, B, C) (значение Val).
  3. SQL: Retention 1/3/7 дней по пользователям с группировкой по месяцу установки (с января 2022). Таблицы: user(user_id, installed_at), client_session(user_id, created_at).
  4. Dash (Plotly): дашборд по games.csv с фильтрами жанров и рейтингов, stacked area по платформам и годам, scatter по оценкам.

Решение

1. Город с минимальным населением

SELECT city, population
FROM city_population
ORDER BY population ASC
LIMIT 1;

Альтернатива:

SELECT city, population FROM city_population
WHERE population = (SELECT MIN(population) FROM city_population);

2. Pivot

SELECT ID,
       MAX(CASE WHEN Name = 'A' THEN Val END) AS A,
       MAX(CASE WHEN Name = 'B' THEN Val END) AS B,
       MAX(CASE WHEN Name = 'C' THEN Val END) AS C
FROM tbl
GROUP BY ID
ORDER BY ID;

В Postgres есть crosstab (extension tablefunc).

3. Retention 1/3/7 по месяцам

WITH base AS (
  SELECT u.user_id,
         u.installed_at::date AS install_date,
         DATE_TRUNC('month', u.installed_at)::date AS install_month
  FROM "user" u
  WHERE u.installed_at >= DATE '2022-01-01'
),
ret AS (
  SELECT b.user_id, b.install_month, b.install_date,
         MAX(CASE WHEN s.created_at::date = b.install_date + INTERVAL '1 day' THEN 1 ELSE 0 END) AS r1,
         MAX(CASE WHEN s.created_at::date = b.install_date + INTERVAL '3 day' THEN 1 ELSE 0 END) AS r3,
         MAX(CASE WHEN s.created_at::date = b.install_date + INTERVAL '7 day' THEN 1 ELSE 0 END) AS r7
  FROM base b
  LEFT JOIN client_session s ON s.user_id = b.user_id
  GROUP BY b.user_id, b.install_month, b.install_date
)
SELECT install_month,
       COUNT(*)                        AS users,
       AVG(r1) AS retention_1d,
       AVG(r3) AS retention_3d,
       AVG(r7) AS retention_7d
FROM ret
GROUP BY install_month
ORDER BY install_month;

4. Dash dashboard (Plotly)

import dash
import plotly.express as px
import pandas as pd
from dash import dcc, html, Input, Output
 
df = pd.read_csv('games.csv')
df = df.dropna()
df = df[df['Year_of_Release'] >= 2000]
 
app = dash.Dash(__name__)
 
app.layout = html.Div([
    html.H1('Games market dashboard'),
    html.P('Описание: дашборд индустрии видеоигр. Фильтры: жанр, рейтинг, годы.'),
    dcc.Dropdown(id='genre',  options=[{'label': g, 'value': g} for g in sorted(df['Genre'].unique())],  multi=True),
    dcc.Dropdown(id='rating', options=[{'label': r, 'value': r} for r in sorted(df['Rating'].unique())], multi=True),
    dcc.RangeSlider(id='years', min=2000, max=int(df['Year_of_Release'].max()),
                    step=1, value=[2000, int(df['Year_of_Release'].max())]),
    html.Div(id='cnt'),
    dcc.Graph(id='area'),
    dcc.Graph(id='scatter'),
])
 
@app.callback(
    [Output('cnt', 'children'),
     Output('area', 'figure'),
     Output('scatter', 'figure')],
    [Input('genre','value'), Input('rating','value'), Input('years','value')]
)
def update(genres, ratings, yr):
    f = df.copy()
    if genres:  f = f[f['Genre'].isin(genres)]
    if ratings: f = f[f['Rating'].isin(ratings)]
    f = f[f['Year_of_Release'].between(yr[0], yr[1])]
 
    cnt   = f'Выбрано игр: {len(f)}'
    area  = px.area(f.groupby(['Year_of_Release', 'Platform']).size().reset_index(name='n'),
                    x='Year_of_Release', y='n', color='Platform')
    sct   = px.scatter(f, x='User_Score', y='Critic_Score', color='Genre')
    return cnt, area, sct
 
if __name__ == '__main__':
    app.run_server()

Подводные камни

  1. MIN(population) с одинаковыми минимумамиLIMIT 1 вернёт один. Если нужны все — WHERE population = (SELECT MIN ...).
  2. Pivot через CASE WHEN: если для одного (ID, Name) несколько строк — MAX выберет один.
  3. Retention: по дням ровно (D1 = install + 1 day)? Или интервал (D1 = «активен в первые 24h»)? Уточняйте.
  4. LEFT JOIN обязателен — иначе пользователь без сессий выпадет, что искажает retention.
  5. User_Score строкой в games.csv ('tbd', NaN) — нужна конвертация.
  6. Dash callbacks: каждое изменение фильтра передаёт ВСЕ значения; не делайте отдельный callback на каждый фильтр.

Эталонный ответ

(1) ORDER + LIMIT 1 или MIN-subquery. (2) Conditional aggregation MAX(CASE WHEN Name = 'X' THEN Val END). (3) Pre-period installed_at после 2022-01-01, MAX(CASE) для D1/D3/D7, GROUP BY install month, AVG для retention. (4) Plotly Dash с 3 фильтрами и обновлением 3 outputs.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти