用Python解码网易云热门歌单:数据背后的音乐偏好与创作者生态

你是否曾好奇,那些动辄收藏量破十万、播放量上亿的网易云歌单,究竟藏着怎样的秘密?为什么有些歌单能持续霸榜,而有些则默默无闻?作为一位音乐爱好者兼数据分析师,我常常被这些问题吸引。音乐平台的歌单,早已超越了简单的歌曲集合,它成为了解大众音乐品味、追踪流行趋势、甚至洞察创作者生态的绝佳窗口。今天,我们就用Python这把“手术刀”,抛开主观感受,从纯粹的数据视角,深入剖析网易云音乐的热门歌单,看看数据能告诉我们哪些关于音乐、用户和创作者的真相。

这篇文章面向那些对音乐、数据和技术交叉领域感兴趣的朋友。无论你是想为自己的音乐项目寻找数据支撑的产品经理,是希望从数据中寻找灵感的独立音乐人,还是单纯想用技术探索爱好的开发者,相信都能从中获得启发。我们将从数据采集、清洗、分析到可视化,完整走一遍流程,但重点不在于复现一个爬虫脚本,而在于如何从海量数据中提炼出有商业或人文价值的洞察。你会发现,一行行代码和一张张图表背后,是流动的音乐潮流和鲜活的用户故事。

1. 数据采集:构建你的音乐数据仓库

任何数据分析项目的第一步,都是获取高质量的数据。对于网易云音乐的歌单数据,我们需要一个稳健、高效且尊重平台规则的采集策略。直接复制粘贴网上的爬虫代码往往行不通,因为平台的反爬机制和页面结构时常更新。更重要的是,我们需要思考:到底要采集哪些数据维度,才能支撑后续的深度分析?

一个歌单的价值,远不止歌曲列表。它包含了丰富的元数据(Metadata),这些元数据正是我们分析的基石。我通常会将采集目标分为三个层次:

  1. 歌单基础信息层:歌单标题、创建者(UP主)、播放量、收藏量、评论数、分享数、创建/更新时间、歌曲数量。
  2. 歌单内容描述层:标签(如“流行”、“摇滚”、“夜晚”)、简介文字、封面图链接。
  3. 歌单社会互动层:热门评论内容、最新评论内容、收藏用户列表(若可获取)。

采集这些数据,我们需要模拟浏览器行为,并处理可能遇到的动态加载、加密参数等问题。这里分享一个经过实战检验的请求策略核心思路,它更侧重于稳定性和可维护性,而非一味追求速度。

import requests
import time
import pandas as pd
from typing import Dict, List, Optional

class NeteasePlaylistCrawler:
    def __init__(self):
        self.session = requests.Session()
        # 精心构造的请求头,模拟真实浏览器
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Accept-Encoding': 'gzip, deflate, br',
            'Referer': 'https://music.163.com/',
            'Connection': 'keep-alive',
        }
        self.session.headers.update(self.headers)

    def fetch_playlist_list(self, category: str = '全部', limit: int = 35, max_pages: int = 10) -> List[Dict]:
        """
        获取歌单列表页信息
        :param category: 歌单分类,如'华语'、'欧美'、'流行'
        :param limit: 每页歌单数量
        :param max_pages: 最大爬取页数
        :return: 歌单基础信息列表
        """
        playlists = []
        base_url = 'https://music.163.com/discover/playlist/'

        for page in range(0, max_pages):
            offset = page * limit
            params = {
                'cat': category,
                'order': 'hot',  # 按热度排序
                'limit': limit,
                'offset': offset
            }
            try:
                resp = self.session.get(base_url, params=params, timeout=10)
                resp.raise_for_status()
                # 这里需要解析HTML,提取歌单ID、标题、播放量、UP主等信息
                # 使用BeautifulSoup或lxml进行解析
                page_data = self._parse_list_page(resp.text)
                playlists.extend(page_data)
                print(f"已获取第 {page+1} 页,累计 {len(playlists)} 个歌单")
                time.sleep(1.5)  # 礼貌性延迟,避免请求过快
            except Exception as e:
                print(f"获取第 {page+1} 页失败: {e}")
                break
        return playlists

    def _parse_list_page(self, html: str) -> List[Dict]:
        """
        解析歌单列表页HTML,提取关键信息。
        这是一个示例函数骨架,实际解析规则需根据页面DOM结构调整。
        """
        # 使用BeautifulSoup解析的逻辑应在此处实现
        # 返回格式示例:[{'id': '123456', 'title':'深夜自习室', 'play_count':'1.2亿', 'creator':'某用户'}]
        pass

注意:在实际操作中,解析函数 _parse_list_page 需要你打开浏览器开发者工具,仔细研究目标网页的元素结构。网易云的页面结构可能变动,因此解析逻辑需要一定的灵活性和容错处理,比如使用 try...except 包裹可能缺失的字段提取。

获取到歌单ID列表后,下一步是抓取每个歌单的详情页数据。这里有一个关键技巧:优先使用官方API接口。通过浏览器开发者工具的“网络”(Network)选项卡,观察页面加载时发出的XHR或Fetch请求,常常能找到返回结构化JSON数据的API,这比解析HTML要稳定和高效得多。

假设我们找到了一个返回歌单详情的API端点,我们的采集函数可以这样设计:

    def fetch_playlist_detail(self, playlist_id: str) -> Optional[Dict]:
        """
        通过API获取歌单详情
        """
        api_url = f'https://music.163.com/api/v6/playlist/detail'
        params = {
            'id': playlist_id,
            'n': 1000,  # 获取歌单内前1000首歌曲信息
            'csrf_token': ''  # 有时需要,有时不需要,视情况而定
        }
        try:
            resp = self.session.get(api_url, params=params, timeout=10)
            data = resp.json()
            if data.get('code') == 200:
                playlist = data['playlist']
                detail_info = {
                    'id': playlist_id,
                    'name': playlist.get('name'),
                    'creator': playlist.get('creator', {}).get('nickname'),
                    'play_count': playlist.get('playCount'),
                    'subscribed_count': playlist.get('subscribedCount'), # 收藏量
                    'comment_count': playlist.get('commentCount'),
                    'share_count': playlist.get('shareCount'),
                    'track_count': playlist.get('trackCount'),
                    'tags': ','.join(playlist.get('tags', [])), # 标签合并为字符串
                    'description': playlist.get('description'),
                    'create_time': playlist.get('createTime'),
                    'update_time': playlist.get('updateTime'),
                }
                return detail_info
            else:
                print(f"歌单 {playlist_id} API返回错误: {data.get('code')}")
                return None
        except Exception as e:
            print(f"获取歌单 {playlist_id} 详情失败: {e}")
            return None

将采集到的数据持久化存储是必不可少的一步。我推荐使用 SQLite(用于快速原型)或 PostgreSQL(用于生产环境) 而非简单的CSV文件,因为关系型数据库能更好地处理后续的关联查询和复杂分析。

import sqlite3

def init_database(db_path='music_data.db'):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    # 创建歌单信息表
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS playlists (
            id TEXT PRIMARY KEY,
            name TEXT,
            creator TEXT,
            play_count INTEGER,
            subscribed_count INTEGER,
            comment_count INTEGER,
            share_count INTEGER,
            track_count INTEGER,
            tags TEXT,
            description TEXT,
            create_time INTEGER,
            update_time INTEGER,
            crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    # 还可以创建歌曲表、评论表等
    conn.commit()
    conn.close()

数据采集阶段,最忌讳的就是“一把梭”。制定清晰的采集目标、设计合理的数据模型、加入足够的错误处理和日志记录,才能为后续分析打下坚实基础。我曾在初期因为没处理好网络异常,导致一晚上爬取任务中断数次,浪费了大量时间。所以,请务必让你的爬虫“健壮”起来。

2. 数据清洗与预处理:从原始数据到分析就绪

爬虫抓取回来的数据,我们称之为“原始数据”(Raw Data)。它通常是杂乱、不完整甚至包含错误的。直接用它进行分析,就像用沾满泥土的食材做菜,结果可想而知。数据清洗(Data Cleaning)和预处理(Preprocessing)的目的,就是将这些“食材”洗净、切配,变成“分析就绪”(Analysis-Ready)的状态。

针对网易云歌单数据,我们通常会遇到以下几类问题:

  • 缺失值处理:某些歌单可能没有标签、没有简介,或者收藏量、评论数等字段因为采集失败而为空。
  • 格式不一致:播放量“1.2亿”需要转换为整数120000000;时间戳可能是毫秒或秒为单位。
  • 异常值检测:是否存在播放量高得离谱(如“9999亿”)的无效数据?是否有重复的歌单记录?
  • 文本清洗:歌单简介和评论中可能包含无关字符、表情符号、HTML标签等。

让我们用Pandas来一步步解决这些问题。首先,将数据加载到DataFrame中。

import pandas as pd
import numpy as np

# 假设我们已经将爬取的数据存入了CSV文件
df_raw = pd.read_csv('raw_playlists.csv')
print(f"原始数据形状: {df_raw.shape}")
print(df_raw.info())
print(df_raw.head())

第一步:处理缺失值。 我们需要根据字段的业务含义,决定是填充、插值还是删除。

# 检查缺失值情况
missing_summary = df_raw.isnull().sum()
print("缺失值统计:\n", missing_summary[missing_summary > 0])

# 处理方案
# 1. 数值型字段(播放量、收藏量等):用中位数或0填充。用0填充需谨慎,可能扭曲分布。
df_raw['play_count'] = df_raw['play_count'].fillna(0).astype(int)
df_raw['subscribed_count'] = df_raw['subscribed_count'].fillna(0).astype(int)
# 对于播放量,如果缺失,也可以考虑用同类歌单的中位数填充,这更合理。
# median_play = df_raw['play_count'].median()
# df_raw['play_count'] = df_raw['play_count'].fillna(median_play).astype(int)

# 2. 文本型字段(标签、简介):用空字符串填充
df_raw['tags'] = df_raw['tags'].fillna('')
df_raw['description'] = df_raw['description'].fillna('')

# 3. 关键标识字段(如歌单ID、创建者)缺失,整行删除
df_clean = df_raw.dropna(subset=['id', 'creator'])
print(f"清洗后数据形状: {df_clean.shape}")

第二步:统一数据格式与类型转换。 这是最繁琐但也最关键的一步。

def parse_count(value):
    """
    将‘1.2万’、‘3.5亿’这样的字符串转换为整数。
    """
    if pd.isna(value) or value == '':
        return 0
    if isinstance(value, (int, float, np.integer)):
        return int(value)
    value = str(value).strip()
    if '亿' in value:
        return int(float(value.replace('亿', '')) * 100000000)
    elif '万' in value:
        return int(float(value.replace('万', '')) * 10000)
    else:
        try:
            return int(value)
        except:
            return 0

# 应用转换函数
df_clean['play_count'] = df_clean['play_count'].apply(parse_count)
df_clean['subscribed_count'] = df_clean['subscribed_count'].apply(parse_count)
df_clean['comment_count'] = df_clean['comment_count'].apply(parse_count)

# 时间戳转换(假设是毫秒)
df_clean['create_time'] = pd.to_datetime(df_clean['create_time'], unit='ms', errors='coerce')
df_clean['update_time'] = pd.to_datetime(df_clean['update_time'], unit='ms', errors='coerce')

第三步:检测与处理异常值。 我们可以通过描述性统计和可视化(如箱线图)来发现异常。

# 查看数值字段的统计描述
print(df_clean[['play_count', 'subscribed_count', 'comment_count']].describe())

# 定义一个基于IQR(四分位距)的异常值检测函数
def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    return (series < lower_bound) | (series > upper_bound)

# 检测播放量异常值
play_outliers = detect_outliers_iqr(df_clean['play_count'])
print(f"播放量异常值数量: {play_outliers.sum()}")
# 查看异常值
print(df_clean[play_outliers][['id', 'name', 'play_count']].head())

# 处理异常值:对于明显不合理的极高值(如超过平台理论极限),可以视为脏数据删除或截断。
# 例如,假设我们认为播放量超过100亿为异常
df_clean = df_clean[df_clean['play_count'] <= 10_000_000_000]

第四步:文本字段的深度清洗。 对于标签和简介,我们需要做分词、去停用词等处理,为后续的文本分析做准备。

import jieba
import re

def clean_text(text):
    """清洗简介文本"""
    if not text:
        return ''
    # 去除HTML标签、URL、多余空白
    text = re.sub(r'<[^>]+>', '', text)
    text = re.sub(r'http\S+', '', text)
    text = re.sub(r'\s+', ' ', text).strip()
    return text

def split_tags(tag_string):
    """将标签字符串分割为列表,并去重"""
    if not tag_string:
        return []
    # 假设标签以逗号、空格或中文顿号分隔
    tags = re.split(r'[,,\s]+', tag_string)
    # 去除空字符串和重复项
    tags = [tag.strip() for tag in tags if tag.strip()]
    return list(set(tags))

df_clean['description_clean'] = df_clean['description'].apply(clean_text)
df_clean['tags_list'] = df_clean['tags'].apply(split_tags)

# 为后续分析,可以创建一个“标签展开”的DataFrame
tags_exploded = df_clean.explode('tags_list')
tags_exploded = tags_exploded[tags_exploded['tags_list'] != '']

经过以上步骤,我们得到了一份干净、规整的数据集 df_clean。这个过程可能占用了整个项目60%以上的时间,但它的价值是决定性的。一份高质量的数据,能让后续的分析事半功倍,结论也更具说服力。记住,垃圾进,垃圾出(Garbage In, Garbage Out),在数据科学领域是铁律。

3. 核心洞察分析:音乐偏好、创作者与平台生态

数据准备就绪后,真正的探索开始了。我们将从三个核心维度切入:用户偏好(听什么)、创作者生态(谁在创作)、以及歌单本身的特征(什么样式的歌单受欢迎)。这些分析将帮助我们回答最初的问题。

3.1 解码用户音乐偏好:标签与热度的秘密

歌单标签是理解音乐风格的直接入口。我们可以通过分析标签的分布、共现关系以及与热度指标(播放、收藏、评论)的关联,来描绘一幅生动的“音乐口味地图”。

首先,让我们看看哪些音乐风格最受平台用户欢迎。

import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] # 解决中文显示问题
plt.rcParams['axes.unicode_minus'] = False

# 统计标签频率
tag_counts = tags_exploded['tags_list'].value_counts().head(20)

# 绘制水平条形图
plt.figure(figsize=(12, 8))
bars = plt.barh(tag_counts.index[::-1], tag_counts.values[::-1], color=sns.color_palette("husl", 20))
plt.xlabel('歌单数量')
plt.title('网易云音乐热门歌单标签TOP20')
# 在条形末端添加数据标签
for i, (value, bar) in enumerate(zip(tag_counts.values[::-1], bars)):
    plt.text(value + 5, bar.get_y() + bar.get_height()/2, f'{value}', va='center')
plt.tight_layout()
plt.show()

仅仅知道“流行”标签最多还不够。我们需要知道哪些标签组合(即歌单的标签集合)更容易打造出爆款歌单。这可以通过计算标签与热度指标的相关系数,或者构建“标签-热度”矩阵来实现。

标签 平均播放量(万) 平均收藏量 平均评论数 出现频次
欧美 1250 4.5万 3200 高频
纯音乐 850 3.2万 1800 中频
学习 2200 8.1万 5500 中频
摇滚 980 3.8万 2500 中频
ACG 1500 5.5万 4200 中频
治愈 760 2.9万 1400 高频

提示:上表为模拟数据,用于展示分析思路。实际分析中,“学习”、“工作”等场景化标签的歌单,其平均互动数据(收藏、评论)往往显著高于纯风格标签,这说明功能性歌单(如助眠、专注、运动)的用户粘性和社交属性更强

另一个有趣的角度是分析歌单简介的文本情感和主题。利用简单的文本分析技术,我们可以从海量简介中提炼出用户创建歌单的动机和情感倾向。

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# 使用TF-IDF提取简介文本的关键词
vectorizer = TfidfVectorizer(max_features=1000, stop_words=['的', '了', '在', '是', '我', '有', '和', '就', '都', '而', '及', '与', '等'])
tfidf_matrix = vectorizer.fit_transform(df_clean['description_clean'].fillna(''))

# 使用LDA模型进行主题聚类(假设我们想找出5个主要主题)
lda = LatentDirichletAllocation(n_components=5, random_state=42)
lda.fit(tfidf_matrix)

# 查看每个主题下的关键词
feature_names = vectorizer.get_feature_names_out()
for topic_idx, topic in enumerate(lda.components_):
    print(f"\n主题 #{topic_idx + 1}:")
    top_words_idx = topic.argsort()[:-10 - 1:-1] # 取权重最高的10个词
    top_words = [feature_names[i] for i in top_words_idx]
    print(", ".join(top_words))

运行上述代码,你可能会得到类似“夜晚、安静、睡眠”、“回忆、青春、经典”、“节奏、运动、能量”这样的主题词群。这比单纯看标签更能深入理解歌单的情感内核和使用场景

3.2 剖析创作者生态:谁是真正的“歌单之王”?

歌单的创建者(UP主)是平台内容生态的基石。分析他们,能让我们了解优质内容的来源。我们可以从几个维度对UP主进行“画像”:

  1. 生产力:创建的歌单总数。
  2. 影响力:所创歌单的总播放量、平均播放量、总收藏量。
  3. 专业性/垂直度:歌单标签的集中度(是专注于某一风格,还是涉猎广泛)。
  4. 互动能力:所创歌单的平均评论数。

让我们计算一下UP主的影响力排行榜。这里我们定义一个简单的“影响力指数”,综合播放、收藏和评论。

# 按创建者分组聚合
creator_stats = df_clean.groupby('creator').agg(
    playlist_count=('id', 'count'),
    total_play=('play_count', 'sum'),
    avg_play=('play_count', 'mean'),
    total_subscribed=('subscribed_count', 'sum'),
    avg_comment=('comment_count', 'mean')
).reset_index()

# 计算一个综合影响力分数(需归一化处理)
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
# 选择要加权的指标
metrics_to_scale = ['total_play', 'total_subscribed', 'avg_comment']
creator_stats[['scaled_'+m for m in metrics_to_scale]] = scaler.fit_transform(creator_stats[metrics_to_scale])
# 假设权重:总播放量0.5,总收藏量0.3,平均评论0.2
creator_stats['influence_score'] = (creator_stats['scaled_total_play'] * 0.5 +
                                    creator_stats['scaled_total_subscribed'] * 0.3 +
                                    creator_stats['scaled_avg_comment'] * 0.2)

# 按影响力分数排序
top_creators = creator_stats.sort_values('influence_score', ascending=False).head(10)
print(top_creators[['creator', 'playlist_count', 'total_play', 'influence_score']])

分析结果可能会让你惊讶:高产的UP主不一定影响力最大。有些用户只创建了少数几个歌单,但每个都是精品,播放量惊人。而有些UP主虽然创建了大量歌单,但平均质量平平。这启示我们,在运营或合作时,应更关注UP主的“爆款率”和“平均质量”,而非单纯的数量

进一步,我们可以对UP主进行聚类分析,看看平台上存在哪些不同类型的创作者。例如,是否存在“爆款专业户”、“垂直领域深耕者”、“情怀收藏家”、“热门搬运工”等不同类型?这可以通过对UP主的各项指标进行聚类算法(如K-Means)来实现。

3.3 歌单成功要素分析:什么样的歌单能火?

这是所有分析中最具商业价值的部分。我们试图建立一个简单的模型,来预测或解释一个歌单的成功(以播放量或收藏量为衡量标准)。我们可以将歌单的特征作为自变量(X),热度指标作为因变量(y),进行探索性分析。

潜在的特征包括:

  • 基础特征:歌曲数量、标签数量、简介文本长度、是否存在封面图。
  • 内容特征:主要音乐风格(通过标签推断)、情感倾向(通过简介文本分析得出)。
  • 创作者特征:UP主的历史影响力分数、专注度。
  • 时间特征:创建时长、最近更新时间。

我们可以先进行相关性分析,看看哪些特征与热度指标关系最密切。

# 构建特征DataFrame
df_features = df_clean.copy()
df_features['tag_count'] = df_features['tags_list'].apply(len)
df_features['desc_length'] = df_features['description_clean'].apply(len)
df_features['existence_days'] = (pd.Timestamp.now() - df_features['create_time']).dt.days
# 合并UP主影响力分数
df_features = df_features.merge(creator_stats[['creator', 'influence_score']], on='creator', how='left')
df_features.rename(columns={'influence_score': 'creator_influence'}, inplace=True)

# 选择数值型特征计算相关性
numeric_features = ['track_count', 'tag_count', 'desc_length', 'existence_days', 'creator_influence']
target = 'play_count' # 或 'subscribed_count'

correlation_matrix = df_features[numeric_features + [target]].corr()
print(f"特征与'{target}'的相关性:\n", correlation_matrix[target].sort_values(ascending=False))

在我的某次分析中,发现 creator_influence(UP主影响力)和 existence_days(存在天数)与播放量的正相关性最强,而 tag_count(标签数量)呈现微弱的负相关(标签过多可能意味着定位不清晰)。desc_length(简介长度)则呈现倒U型关系,适中的简介最好。

当然,这只是线性相关的初步观察。更深入的分析可以尝试使用决策树或随机森林等模型,它们不仅能给出特征重要性排序,还能捕捉特征间的非线性交互作用。

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

# 准备数据
X = df_features[numeric_features].fillna(0)
y = df_features[target]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 训练一个简单的随机森林模型
rf_model = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)

# 查看特征重要性
feature_importance = pd.DataFrame({
    'feature': numeric_features,
    'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)
print("随机森林特征重要性:\n", feature_importance)

通过这样的分析,我们或许能总结出一些“爆款歌单公式”的雏形,例如:“由高影响力UP主创建,聚焦1-3个核心标签,拥有简洁有力(约50-150字)的场景化简介,并持续更新维护的歌单,更容易获得成功”。这些洞察对于平台运营、UP主内容创作乃至音乐人的推广,都具有参考价值。

4. 高级可视化与故事讲述:让数据自己说话

数据分析的最终目的是为了交流和决策。再深刻的洞察,如果无法清晰有效地传达,价值也会大打折扣。在这一部分,我们将超越基础的柱状图和饼图,使用更高级的可视化技术来讲述数据故事,并分享一些让图表既专业又美观的实用技巧。

技巧一:使用Seaborn增强图表表现力。 Matplotlib是基础,但Seaborn在统计图表和美化上更胜一筹。

import seaborn as sns
import matplotlib.pyplot as plt

# 示例:绘制播放量与收藏量的联合分布与回归线
plt.figure(figsize=(10, 6))
# 取对数处理,使分布更集中,便于观察
df_plot = df_clean[(df_clean['play_count']>0) & (df_clean['subscribed_count']>0)].copy()
df_plot['log_play'] = np.log10(df_plot['play_count'] + 1)
df_plot['log_sub'] = np.log10(df_plot['subscribed_count'] + 1)

scatter = sns.jointplot(data=df_plot, x='log_play', y='log_sub',
                        kind='reg',  # 同时显示散点和回归线
                        height=8,
                        ratio=4,
                        marginal_kws={'bins': 30, 'kde': True},
                        joint_kws={'scatter_kws': {'s': 10, 'alpha': 0.5},
                                   'line_kws': {'color': 'red'}})
scatter.ax_joint.set_xlabel('播放量 (log10)')
scatter.ax_joint.set_ylabel('收藏量 (log10)')
scatter.ax_joint.set_title('歌单播放量与收藏量关系散点图(含回归线)')
plt.tight_layout()
plt.show()

这张图可以直观地展示播放量和收藏量之间强烈的正相关关系,同时边缘的直方图展示了两个变量的分布情况。

技巧二:用热力图揭示标签共现关系。 哪些标签经常一起出现?这能帮助我们理解音乐风格的组合趋势。

from itertools import combinations
from collections import Counter

# 生成标签共现矩阵
cooccurrence = Counter()
for tags in df_clean['tags_list']:
    # 对每个歌单的标签列表,生成所有两两组合
    for tag1, tag2 in combinations(sorted(set(tags)), 2):
        cooccurrence[(tag1, tag2)] += 1

# 转换为DataFrame,便于绘制热力图
top_tags = tag_counts.head(15).index.tolist() # 取前15个热门标签
cooccurrence_matrix = pd.DataFrame(0, index=top_tags, columns=top_tags)
for (tag1, tag2), count in cooccurrence.items():
    if tag1 in top_tags and tag2 in top_tags:
        cooccurrence_matrix.loc[tag1, tag2] = count
        cooccurrence_matrix.loc[tag2, tag1] = count # 矩阵是对称的

# 绘制热力图
plt.figure(figsize=(12, 10))
sns.heatmap(cooccurrence_matrix, annot=True, fmt='d', cmap='YlOrRd',
            square=True, linewidths=.5, cbar_kws={"shrink": .8})
plt.title('热门歌单标签共现关系热力图')
plt.tight_layout()
plt.show()

从热力图中,你可能会发现“学习”和“纯音乐”高度共现,“运动”和“摇滚”、“电子”关联紧密。这为创建组合标签歌单提供了数据支持。

技巧三:用时间序列图追踪趋势。 音乐潮流是变化的。我们可以分析不同时间段创建的歌单,其主流标签或平均热度有何变化。

# 按歌单创建年份-月份分组,分析标签趋势
df_clean['create_ym'] = df_clean['create_time'].dt.to_period('M')
# 计算每个月最热门的标签
trend_data = []
for period, group in df_clean.groupby('create_ym'):
    # 展开该月所有歌单的标签
    monthly_tags = group.explode('tags_list')
    top_tag = monthly_tags['tags_list'].value_counts().index[0] if not monthly_tags.empty else 'N/A'
    avg_play = group['play_count'].mean()
    trend_data.append({'period': period, 'top_tag': top_tag, 'avg_play': avg_play})

df_trend = pd.DataFrame(trend_data).sort_values('period')
# 选取最近24个月的数据绘图
df_trend_last2y = df_trend[df_trend['period'] >= df_trend['period'].max() - 24]

fig, ax1 = plt.subplots(figsize=(14, 7))
# 绘制平均播放量折线
color = 'tab:blue'
ax1.set_xlabel('创建时间 (年-月)')
ax1.set_ylabel('平均播放量', color=color)
line1 = ax1.plot(df_trend_last2y['period'].astype(str), df_trend_last2y['avg_play'], color=color, marker='o', label='平均播放量')
ax1.tick_params(axis='y', labelcolor=color)
ax1.set_xticklabels(df_trend_last2y['period'].astype(str), rotation=45)

# 创建第二个y轴,用于标注月度最热标签
ax2 = ax1.twinx()
color = 'tab:red'
ax2.set_ylabel('月度最热标签', color=color)
# 在折线图对应位置标注标签名
for i, (period, tag) in enumerate(zip(df_trend_last2y['period'], df_trend_last2y['top_tag'])):
    ax2.text(i, df_trend_last2y['avg_play'].iloc[i] * 1.02, tag,
             ha='center', va='bottom', fontsize=9, rotation=30, color=color)
ax2.tick_params(axis='y', labelcolor=color)

fig.tight_layout()
plt.title('近两年歌单月度趋势:平均播放量与最热标签')
plt.show()

这样的图表能生动地展示音乐风潮的起落,比如“City Pop”、“Lo-fi”等标签可能在特定时间段突然崛起。

技巧四:构建交互式仪表盘。 对于需要深度探索或汇报的场景,静态图表可能不够用。使用 Plotly Dash 或 Streamlit 可以快速构建一个交互式数据分析仪表盘。

# 这是一个Streamlit应用的极简示例框架
import streamlit as st
import pandas as pd
import plotly.express as px

st.set_page_config(page_title="网易云歌单分析仪表盘", layout="wide")
st.title("🎵 网易云热门歌单数据分析仪表盘")

# 加载数据
df = pd.read_csv('cleaned_playlists.csv')
# 侧边栏过滤器
selected_tag = st.sidebar.selectbox('选择音乐标签', options=['全部'] + df['tags_list'].explode().unique().tolist())
min_play = st.sidebar.slider('最小播放量(万)', 0, int(df['play_count'].max()/10000), 0)

# 根据筛选条件过滤数据
if selected_tag != '全部':
    df_filtered = df[df['tags_list'].apply(lambda x: selected_tag in x)]
else:
    df_filtered = df
df_filtered = df_filtered[df_filtered['play_count'] >= min_play * 10000]

# 主显示区
col1, col2 = st.columns(2)
with col1:
    # 绘制播放量分布直方图
    fig1 = px.histogram(df_filtered, x='play_count', nbins=50, title='播放量分布')
    st.plotly_chart(fig1, use_container_width=True)
with col2:
    # 绘制标签云或条形图
    tag_counts = df_filtered.explode('tags_list')['tags_list'].value_counts().head(15)
    fig2 = px.bar(x=tag_counts.values, y=tag_counts.index, orientation='h', title='热门标签TOP15')
    st.plotly_chart(fig2, use_container_width=True)

# 显示数据表格
st.subheader("歌单数据详情")
st.dataframe(df_filtered[['name', 'creator', 'play_count', 'subscribed_count', 'tags']].head(20))

将这个脚本保存为 app.py,在终端运行 streamlit run app.py,一个包含筛选、图表交互和数据预览的网页应用就启动了。这能让你的分析结果瞬间变得生动和可操作。

可视化的精髓在于用最合适的图表讲最清晰的故事。避免为了炫技而使用复杂的图表,始终记住你的受众和想要传达的核心信息。颜色搭配、字体选择、图表注释等细节,共同决定了最终呈现的专业程度。在我自己的项目中,我通常会准备两套可视化方案:一套用于快速探索和调试(简单直接),另一套用于最终报告和展示(精美、信息完整、有故事线)。

更多推荐