# What to Watch?: Official Solution

*English translation by SOTA – AI Community of the Russian original. Organisers who would like this translation removed can email sota.ai.community@gmail.com.*

All-Russian School Olympiad in Informatics 2025–2026, Final Stage<br>
“Artificial Intelligence” profile, Tour 2, Moscow, 25 March 2026<br>
Task A

*Translator's note: the official solution is the authors' Jupyter notebook exported to PDF. Code cells are marked “In [n]”, their results “Out [n]”, and printed output follows the cell. Lines that the PDF wrapped are joined again. Comments, docstrings and messages are translated; the code itself is unchanged. Figures are described in words; see the original PDF for the images.*

## 1.1 Summary

This notebook **reads the data from files in the same folder**, solves 3 subtasks and saves the answers to CSV:

- **A1** `favorite_genre` → `answer_a1.csv`
- **A2** `actor_match` → `answer_a2.csv`
- **A4** `next_in_session` → `answer_a3.csv`

## 1.2 Files that the notebook must see

Input (in the same folder as the notebook): - `queries_A.csv`: queries (5 candidates each, `c1..c5`) - `items_A.csv`: film metadata (`genre`, `duration`) - `events_A.csv`: user events (`open/finish/like`) - `item_meta_A.json`: film metadata (lists of actors `actors`) - `sessions_A.json`: sessions (inside `sessions[*].path`)

Output (created next to the notebook):

| File | Relative path | Purpose |
|---|---|---|
| `answer_a1.csv` | `./answer_a1.csv` | answers for `favorite_genre` |
| `answer_a2.csv` | `./answer_a2.csv` | answers for `actor_match` |
| `answer_a4.csv` | `./answer_a3.csv` | answers for `next_in_session` |

## 1.3 A universal solution pattern

The same skeleton is used in all three subtasks:

1) `melt` converts the candidates `c1..c5` from wide to long format (one row per candidate).

2) `merge(..., how='left')` attaches the features/score **without losing candidates**.

3) If there are lists (actors, transition pairs), we use `explode`. Important: **empty lists give** a `NaN` row (this is convenient for keeping the candidate and getting a contribution of 0).

4) The winner is chosen by sorting with `sort_values` + `drop_duplicates('query_id')` (the first in sorted order).

5) Saving: `to_csv(index=False)`.

Links to the pandas documentation (official):

- `pandas.melt`:
  https://pandas.pydata.org/docs/reference/api/pandas.melt.html
- `DataFrame.explode`:
  https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.explode.html
- `pandas.merge`:
  https://pandas.pydata.org/docs/reference/api/pandas.merge.html
- `DataFrame.sort_values`:
  https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.sort_values.html
- `DataFrame.drop_duplicates`:
  https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.drop_duplicates.html
- `DataFrame.to_csv`:
  https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_csv.html

In [1]:

```python
import pandas as pd
import matplotlib.pyplot as plt
```

### 1.3.1 Quick check of data loading

Below, we read all the files once and look at the table sizes and the distribution of `query_type`.

In [2]:

```python
Q = pd.read_csv('queries_A.csv')
I = pd.read_csv('items_A.csv')
E = pd.read_csv('events_A.csv')
M = pd.read_json('item_meta_A.json')[['item_id','actors']]
S = pd.read_json('sessions_A.json')

print('Q', Q.shape, 'I', I.shape, 'E', E.shape, 'M', M.shape, 'S', S.shape)
Q.query_type.value_counts()
```

```text
Q (800, 8) I (260, 3) E (5334, 3) M (260, 2) S (180, 2)
```

Out [2]:

```text
query_type
favorite_genre     400
actor_match        200
next_in_session    200
Name: count, dtype: int64
```

In [3]:

```python
vc = Q.query_type.value_counts().sort_index()
ax = vc.plot(kind='bar', title='Number of queries by type')
ax.set_xlabel('query_type')
ax.set_ylabel('count')
plt.tight_layout()
plt.show()
```

*[Figure: see page 3 of the [original solutions PDF](https://vos.olimpiada.ru/upload/files/Arhive_tasks/2025-26/final/ai/sol-ai-9-11-tur2-final-25-26.pdf). A bar chart titled "Number of queries by type": x axis `query_type`, y axis `count`; actor_match = 200, favorite_genre = 400, next_in_session = 200.]*

### 1.3.2 **A1.** `favorite_genre`

In [4]:

```python
q = Q[Q.query_type == 'favorite_genre'][['query_id','user_id','c1','c2','c3','c4','c5']]

e = E[['user_id','item_id','event_type']].copy()
e['w'] = e.event_type.map({'open': 1, 'finish': 2, 'like': 3})
e = e.merge(I[['item_id','genre']], on='item_id', how='left')
g = e.groupby(['user_id','genre'], as_index=False)['w'].sum()

c = q.melt(['query_id','user_id'], ['c1','c2','c3','c4','c5'], value_name='item_id')[['query_id','user_id','item_id']]
c = c.merge(I[['item_id','genre','duration']], on='item_id', how='left')
c = c.merge(g, on=['user_id','genre'], how='left')
c['w'] = c.w.fillna(0)

c = c.sort_values(['query_id','w','duration','item_id'], ascending=[1,0,1,1])
a1 = c.drop_duplicates('query_id')[['query_id','item_id']]

a1.to_csv('answer_a1.csv', index=False)
a1.head()
```

Out [4]:

```text
      query_id  item_id
400          1     1199
1201         2     1065
1602         3     1211
403          4     1086
4            5     1163
```

### 1.3.3 **A2.** `actor_match`

In [5]:

```python
q = Q[Q.query_type == 'actor_match'][['query_id','user_id','c1','c2','c3','c4','c5']]
e = E[E.event_type.isin(['finish','like'])][['user_id','item_id']]

m = M[['item_id','actors']].copy()
m['actors'] = m.actors.apply(lambda x: x if isinstance(x, list) else [])

ua = e.merge(m, on='item_id', how='left')
ua = ua.explode('actors').dropna(subset=['actors'])
ua = ua.drop_duplicates(['user_id','actors'])
ua['hit'] = 1
ua = ua[['user_id','actors','hit']]

c = q.melt(['query_id','user_id'], ['c1','c2','c3','c4','c5'], value_name='item_id')[['query_id','user_id','item_id']]
c = c.merge(m, on='item_id', how='left')
c = c.explode('actors')
c = c.drop_duplicates(['query_id','item_id','actors'])

c = c.merge(ua, on=['user_id','actors'], how='left')
c['hit'] = c.hit.fillna(0)

s = c.groupby(['query_id','item_id'], as_index=False)['hit'].sum()
s = s.sort_values(['query_id','hit','item_id'], ascending=[1,0,1])
a2 = s.drop_duplicates('query_id')[['query_id','item_id']]

a2.to_csv('answer_a2.csv', index=False)
a2.head()
```

Out [5]:

```text
    query_id  item_id
4        401     1252
9        402     1255
14       403     1227
19       404     1216
24       405     1232
```

### 1.3.4 **A3.** `next_in_session`

In [6]:

```python
q = Q[Q.query_type == 'next_in_session'][['query_id','user_id','c1','c2','c3','c4','c5']]

fin = E[E.event_type == 'finish'][['user_id','item_id']].drop_duplicates()
fin = fin.rename(columns={'item_id': 'from_item'})

t = S.explode('sessions').dropna(subset=['sessions'])
t['path'] = t.sessions.apply(lambda x: x.get('path', []))
t = t[['user_id','path']]
t['pair'] = t.path.apply(lambda p: list(zip(p[:-1], p[1:])))
t = t.explode('pair').dropna(subset=['pair'])
t['from_item'] = t.pair.str[0]
t['item_id'] = t.pair.str[1]
t['hit'] = 1
tr = t.groupby(['user_id','from_item','item_id'], as_index=False)['hit'].sum()

c = q.melt(['query_id','user_id'], ['c1','c2','c3','c4','c5'], value_name='item_id')[['query_id','user_id','item_id']]
c = c.merge(fin, on='user_id', how='left')
c = c.merge(tr, on=['user_id','from_item','item_id'], how='left')
c['hit'] = c.hit.fillna(0)

s = c.groupby(['query_id','item_id'], as_index=False)['hit'].sum()
s = s.sort_values(['query_id','hit','item_id'], ascending=[1,0,1])
a3 = s.drop_duplicates('query_id')[['query_id','item_id']]

a3.to_csv('answer_a3.csv', index=False)
a3.head()
```

Out [6]:

```text
    query_id  item_id
2        601     1028
5        602     1071
11       603     1049
17       604     1101
20       605     1004
```
