Analyse des données de la plateforme bilibili à grande échelle

Cet article présente une méthode d'analyse des données vidéo de la plateforme Bilibili en utilisant des technologies de big data, aboutissant à des résultats visualisés.

1. Analyse globale des vidéos

Approche d'analyse : On comemnce par une vue d'ensemble, puis on examine les 100 vidéos les mieux classées par catégorie.

Vue d'ensemble inclut :

  1. Le volume de lecture par section.
  2. Les interactions « triple-action » (pièces, favoris, likes) par section.
  3. Les données de commentaires, barrage (danmaku) et partages.
  4. Génération d'un nuage de mots-clés synthétique.

Top 100 des vidéos inclut :

  1. Répartition des catégories dans le top 100.
  2. Volume de lecture du top 100.
  3. Distribution moyenne des pièces, favoris et likes.
  4. Moyenne des commentaires, barrages et partages par section.

1.1 Prétraitement des données

On examine les informations, les valeurs manquantes, les doublons et les types de données. Comme les données sont assez complètes, le traitement supplémentaire est limité.

On effectue des opérations de séparation et d'agrégation pour faciliter l'analyse. La catégorie « Toutes sections » contient des vidéos en tête de classement dans chaque catégorie, ce qui génère des doublons ; ces entrées sont donc exclues.

df.info()
df.isnull().count()
df.nunique().count()
df.dtypes
# Exclusion du classement global
df_filtered = df[df['section_type'] != 'global']
df_filtered['section_type'].value_counts()
# Tri par score (ordre ascendant)
df_top100 = df_filtered.sort_values(by='score', ascending=False)[:100]
df_by_type = df_filtered.drop(['author', 'video_id', 'tag_name', 'video_title', 'rank'], axis=1)
gp_by_type = df_by_type.groupby('section_type').sum().astype('int')
type_list = gp_by_type.index.tolist()

1.2 Visualisation des données

Volume de lecture par section

play_volumes = [round(x/100000000, 2) for x in gp_by_type['play_count'].tolist()]

pie_chart = (
    Pie()
    .add(
        "",
        [list(pair) for pair in zip(type_list, play_volumes)],
        radius=["40%", "75%"],
    )
    .set_global_opts(
        title_opts=opts.TitleOpts(title="Volume de lecture par section (unité : cent millions)"),
        legend_opts=opts.LegendOpts(orient="vertical", pos_top="15%", pos_left="2%"),
    )
    .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}"))
)
pie_chart.render_notebook()

Les trois premières catégories en termes de lecture sont : Vie quotidienne, Animation et Musicalité étrange. Ces deux dernières sont des points forts distinctifs de la plateforme.

Interactions « triple-action » par section

coin_values = [round(x/1000000, 2) for x in gp_by_type['coin_count'].tolist()]
like_values = [round(x/1000000, 2) for x in gp_by_type['like_count'].tolist()]
fav_values = [round(x/1000000, 2) for x in gp_by_type['favorite_count'].tolist()]

def create_bar_chart() -> Bar:
    chart = (
        Bar()
        .add_xaxis(type_list)
        .add_yaxis("Pièces", coin_values)
        .add_yaxis("Likes", like_values)
        .add_yaxis("Favoris", fav_values)
        .set_global_opts(
            title_opts=opts.TitleOpts(title="Interactions triple-action par section"),
            yaxis_opts=opts.AxisOpts(name="Unité : million"),
            xaxis_opts=opts.AxisOpts(name="Section", axislabel_opts={"rotate": 45})
        )
    )
    return chart

create_bar_chart().render_notebook()

La section Vie quotidienne domine pour les pièces et les likes, mais les favoris sont plus élevés pour la section Animation. La section Technologie occupe la quatrième place pour les favoris.

Barragse, commentaires et partages

barrage_values = [round(x/100000, 2) for x in gp_by_type['barrage_count'].tolist()]
comment_values = [round(x/100000, 2) for x in gp_by_type['comment_count'].tolist()]
share_values = [round(x/100000, 2) for x in gp_by_type['share_count'].tolist()]

line_chart = (
    Line()
    .add_xaxis(type_list)
    .add_yaxis("Barrages", barrage_values, label_opts=opts.LabelOpts(is_show=False))
    .add_yaxis("Commentaires", comment_values, label_opts=opts.LabelOpts(is_show=False))
    .add_yaxis("Partages", share_values, label_opts=opts.LabelOpts(is_show=False))
    .set_global_opts(
        title_opts=opts.TitleOpts(title="Barrages, commentaires et partages"),
        tooltip_opts=opts.TooltipOpts(trigger="axis", axis_pointer_type="cross"),
        yaxis_opts=opts.AxisOpts(name="Unité : cent mille"),
        xaxis_opts=opts.AxisOpts(name="Date", axislabel_opts={"rotate": 45})
    )
)
line_chart.render_notebook()

Nuage de tags populaires

tags_concatenated = ','.join(df_filtered['tag_name']).split(',')
tags_frequency = pd.Series(tags_concatenated).value_counts()

word_cloud = (
    WordCloud()
    .add("", [list(pair) for pair in zip(tags_frequency.index, tags_frequency)],
         word_size_range=[10, 100])
    .set_global_opts(title_opts=opts.TitleOpts(title="Tags populaires"))
)
word_cloud.render_notebook()

Distribution moyenne des interactions

gp_quality = df_top100.groupby('section_type')[['coin_count', 'favorite_count', 'like_count']].mean().astype('int')
section_names = gp_quality.index.tolist()
coin_means = gp_quality['coin_count'].values.tolist()
fav_means = gp_quality['favorite_count'].values.tolist()
like_means = gp_quality['like_count'].values.tolist()

def create_radar_chart() -> Radar:
    chart = (
        Radar()
        .add_schema(
            schema=[opts.RadarIndicatorItem(name=name, max_=600000) for name in section_names]
        )
        .add("Pièces", [coin_means], color='#40e0d0')
        .add("Favoris", [fav_means], color='#1e90ff')
        .add("Likes", [like_means], color='#b8860b')
        .set_series_opts(
            label_opts=opts.LabelOpts(is_show=False),
            linestyle_opts=opts.LineStyleOpts(width=3, type_='dotted'),
        )
        .set_global_opts(title_opts=opts.TitleOpts(title="Distribution moyenne des interactions"))
    )
    return chart

create_radar_chart().render_notebook()

La section Vie quotidienne conserve une moyenne de pièces et de likes supérieure à celle de l'Animation. Les sections avec les moyennes les plus élevées sont : Vie quotidienne, Cinéma/TV et Mode. La section Mode se distingue par un nombre de favoris nettement supérieur aux pièces et likes.

1.3 Résultats de l'analyse

Les visualisations montrent que les trois catégories les plus lues sont Vie quotidienne, Animation et Musicalité étrange. Fait surprenant : la catégorie la plus regardée sur une plateforme historiquement centrée sur l'animation est la vie quotidienne.

La répartition des catégories dans le top 100 est cohérente avec l'analyse globale. La moyenne des pièces et likes pour la vie quotidienne dépasse celle de l'animation.

2. Analyse d'une vidéo unique

2.1 Prétraitement des données

Démonstration d'un script de collecte de données

import requests, csv, time
import sys
from bs4 import BeautifulSoup

def fetch_barrage_data(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
        'Cookie': 'votre_cookie_ici'
    }
    response = requests.get(url=url, headers=headers)
    soup = BeautifulSoup(response.content, 'lxml')
    barrage_elements = soup.find_all('d')
    if not barrage_elements:
        return []
    
    processed_data = []
    for element in barrage_elements:
        parts = element.get('p').split(',')
        parts.append(element.string)
        parts[4] = time.ctime(float(parts[4]))
        processed_data.append(parts)
    return processed_data

def convert_seconds(seconds):
    seconds = float(seconds)
    m, s = divmod(seconds, 60)
    h, m = divmod(m, 60)
    return "%02d:%02d:%02d" % (h, m, s)

def main():
    video_ids = [16980576, 16980597, 16548432]
    output_header = ['time', 'mode', 'font', 'color', 'timestamp', 'pool', 'user_id', 'row_id', 'text']
    
    for idx, vid in enumerate(video_ids):
        url = f"https://comment.bilibili.com/{vid}.xml"
        file_name = f"barrage_{idx}.csv"
        data = fetch_barrage_data(url)
        if data:
            with open(file_name, 'w', newline='', encoding='utf-8') as f:
                writer = csv.writer(f)
                writer.writerow(output_header)
                writer.writerows(data)

if __name__ == "__main__":
    main()

2.2 Nettoyage des données

Importation des bibliothèques

import numpy as np
import pandas as pd
import glob
import re
import jieba
import matplotlib.pyplot as plt
import seaborn as sns
from pyecharts.charts import *
from pyecharts import options as opts
from snownlp import SnowNLP
from gensim import corpora, models

Fusion des fichiers de données

csv_files = glob.glob('./data/barrage/*.csv')
print(f'Découverte de {len(csv_files)} fichiers CSV.')

with open('merged_barrages.csv', 'w') as outfile:
    for fname in csv_files:
        with open(fname, 'r') as infile:
            outfile.write(infile.read())
        outfile.write('\n')
print('Fusion terminée.')

Traitement des doublons et valeurs manquantes

df = pd.read_csv("./merged_barrages.csv", header=None, on_bad_lines='skip')
df = df.iloc[:, [1, 2]]
df = df.drop_duplicates()
df = df.dropna()
df.columns = ["user", "barrage_text"]

Compression des séquences répétées

def compress_repeats(text):
    if len(text) < 2:
        return text
    compressed = text[0]
    for char in text[1:]:
        if char != compressed[-1]:
            compressed += char
    return compressed

df["barrage_text"] = df["barrage_text"].apply(compress_repeats)

Filtrage des caractères spéciaux

df['barrage_text'] = df['barrage_text'].str.extract(r"([\u4e00-\u9fa5]+)")
df = df.dropna()
df = df[df["barrage_text"].str.len() >= 4]

2.3 Visualisation des données

Les analyses visuelles révèlent une reconnaissance positive pour la série « Le Silence de la Vérité », avec une mention fréquente du personnage de l'avocat Jiang Yang, incarné par l'acteur Bai Yu.

3. Traitement du langage naturel (NLP)

3.1 Analyse des sentiments

L'analyse des sentiments permet d'évaluer la polarité émotionnelle des textes. On utilise ici la bibliothèque SnowNLP pour attribuer un score entre 0 (négatif) et 1 (positif) à chaque barrage.

df['sentiment_score'] = df["barrage_text"].apply(lambda x: SnowNLP(x).sentiments)

Distribution des sentiments globaux

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

plt.figure(figsize=(12, 6))
sns.histplot(df['sentiment_score'], kde=True, bins=20, color='green')
plt.title("Distribution des sentiments dans les barrages")
plt.xlabel("Score de sentiment")
plt.ylabel("Densité")
plt.show()

Sentiments envers les acteurs principaux

actor_map = {
    'jiangyang': 'Bai Yu|Jiang Yang',
    'yanliang': 'Liao Fan|Yan Liang',
    'zhangchao': 'Ning Li|Zhang Chao'
}

for actor_key, actor_pattern in actor_map.items():
    df[actor_key] = df['barrage_text'].str.contains(actor_pattern)

actor_sentiment = pd.Series({
    key: df.loc[df[key], 'sentiment_score'].mean() 
    for key in actor_map.keys()
})
print(actor_sentiment.sort_values())

Les scores moyens indiquent une perception positive de tous les acteurs. Jiang Yang obtient un score légèrement inférieur, possiblement en raison de l'empathie des spectateurs face aux injustices subies par son personnage.

3.2 Analyse thématique

On segmente les barrages en deux groupes : positifs (score > 0.8) et négatifs (score < 0.3), puis on applique une modélisation thématique LDA pour identifier les thèmes principaux dans chaque groupe.

Prétraitement textuel

# Segmentation des mots
positive_text = df['barrage_text'][df['sentiment_score'] >= 0.8]
negative_text = df['barrage_text'][df['sentiment_score'] < 0.3]

def segment_text(text):
    return ' '.join(jieba.cut(text))

positive_segmented = positive_text.apply(segment_text)
negative_segmented = negative_text.apply(segment_text)

# Suppression des mots vides
stopwords = open('./stopwords.txt', 'r', encoding='utf-8').read().splitlines()

def remove_stopwords(text_list):
    return [word for word in text_list if word not in stopwords and len(word) > 1]

positive_tokens = positive_segmented.apply(lambda x: remove_stopwords(x.split()))
negative_tokens = negative_segmented.apply(lambda x: remove_stopwords(x.split()))

Modélisation thématique sur les commentaires positifs

positive_dict = corpora.Dictionary(positive_tokens)
positive_corpus = [positive_dict.doc2bow(text) for text in positive_tokens]
positive_lda = models.LdaModel(positive_corpus, num_topics=5, id2word=positive_dict, passes=10)

print("Thèmes positifs :")
for idx, topic in positive_lda.print_topics():
    print(f"Thème {idx}: {topic}")

Modélisation thématique sur les commentaires négatifs

negative_dict = corpora.Dictionary(negative_tokens)
negative_corpus = [negative_dict.doc2bow(text) for text in negative_tokens]
negative_lda = models.LdaModel(negative_corpus, num_topics=5, id2word=negative_dict, passes=10)

print("Thèmes négatifs :")
for idx, topic in negative_lda.print_topics():
    print(f"Thème {idx}: {topic}")

L'analyse thématique permet d'identifier les raisons sous-jacentes des réactions positives et négatives du public, offrant ainsi des insights appprofondis sur la perception de la série.

Étiquettes: Python Bilibili

Publié le 27 juin à 06h10