环境与版本
R==4.5.2
plotly==4.12.0
sampling==2.11
tidyverse==2.0.0Pinn Xu
2026年2月25日
本项目使用来自 Kaggle 的数据集 Most Streamed Spotify Songs 2023,包含 2023 年 Spotify 最受欢迎的 953 首歌曲,属性涵盖曲目名称、艺术家姓名、发行日期、Spotify 播放列表与榜单、播放统计、Apple Music 收录情况、Deezer 收录情况、Shazam 榜单及若干音频特征。
本项目使用的核心数值变量包括:streams(播放量)、BPM(每分钟拍数)、valence index(情绪效价指数)以及在 Spotify 和 Deezer 播放列表中的收录次数。streams 表示一首歌曲在 2023 年全年的播放次数,应为正整数;收录次数同样为计数变量(非负整数)。BPM 决定音乐的节奏与速度。情绪效价指数表示歌曲音乐内容的积极程度,编码范围为 0 到 100(0 = 完全消极,100 = 完全积极)。
类别变量包括音乐的调式(mode)与调性(key)。调式分为大调(major)和小调(minor)两类,是两种通用的和弦类别;调性涵盖 C、C#、D、D# 等,决定了歌曲所使用的基础音符与和弦组合。
总体而言,由于变量的维度与类型各异,本数据集被认为适合后续分析。
本项目旨在分析 2023 年 Spotify 最受欢迎歌曲的音乐特征的显著规律与趋势,并探究各指标之间的潜在关系。此外,将通过不同抽样方法对数据进行抽样,以验证中心极限定理(CLT)的适用性,并比较各抽样方法对总体参数的预测能力。
在正式分析前,数据集首先从 Kaggle 网站下载并存储至本地文件夹,随后使用 read.csv() 函数导入 R。以下展示了导入后数据集作为 DataFrame 对象的结构。
'data.frame': 953 obs. of 24 variables:
$ track_name : chr "Seven (feat. Latto) (Explicit Ver.)" "LALA" "vampire" "Cruel Summer" ...
$ artist.s._name : chr "Latto, Jung Kook" "Myke Towers" "Olivia Rodrigo" "Taylor Swift" ...
$ artist_count : int 2 1 1 1 1 2 2 1 1 2 ...
$ released_year : int 2023 2023 2023 2019 2023 2023 2023 2023 2023 2023 ...
$ released_month : int 7 3 6 8 5 6 3 7 5 3 ...
$ released_day : int 14 23 30 23 18 1 16 7 15 17 ...
$ in_spotify_playlists: int 553 1474 1397 7858 3133 2186 3090 714 1096 2953 ...
$ in_spotify_charts : int 147 48 113 100 50 91 50 43 83 44 ...
$ streams : chr "141381703" "133716286" "140003974" "800840817" ...
$ in_apple_playlists : int 43 48 94 116 84 67 34 25 60 49 ...
$ in_apple_charts : int 263 126 207 207 133 213 222 89 210 110 ...
$ in_deezer_playlists : chr "45" "58" "91" "125" ...
$ in_deezer_charts : int 10 14 14 12 15 17 13 13 11 13 ...
$ in_shazam_charts : chr "826" "382" "949" "548" ...
$ bpm : int 125 92 138 170 144 141 148 100 130 170 ...
$ key : chr "B" "C#" "F" "A" ...
$ mode : chr "Major" "Major" "Major" "Major" ...
$ danceability_. : int 80 71 51 55 65 92 67 67 85 81 ...
$ valence_. : int 89 61 32 58 23 66 83 26 22 56 ...
$ energy_. : int 83 74 53 72 80 58 76 71 62 48 ...
$ acousticness_. : int 31 7 17 11 14 19 48 37 12 21 ...
$ instrumentalness_. : int 0 0 0 0 63 0 0 0 0 0 ...
$ liveness_. : int 8 10 31 11 11 8 8 11 28 8 ...
$ speechiness_. : int 4 4 6 15 6 24 3 4 9 33 ...
随后,导入了本项目所使用的主要包(包括 tidyverse、plotly 和 sampling),并使用 tibble() 函数将原始数据集转换为 tibble(df),以便后续数据处理。
以下展示了数据集的初步概览,包括样本量(N = 953)及是否存在缺失值(使用 anyNA() 函数获取缺失值信息)。
Sample Size: 953
Whether there is NA: FALSE
如下所示,streams 变量的数据类型为字符型,而播放量应为数值型而非字符串,因此需要进行类型转换。
tibble [953 × 1] (S3: tbl_df/tbl/data.frame)
$ streams: chr [1:953] "141381703" "133716286" "140003974" "800840817" ...
在将数据类型转换为数值型之前,首先检查是否存在无法识别为数值的非法字符串,使用正则表达式进行数据筛选。结果显示,有 1 条观测的播放量值无效。
Invalid Streams:
# A tibble: 1 × 1
streams
<chr>
1 BPM110KeyAModeMajorDanceability53Valence75Energy69Acousticness7Instrumentalne…
随后,仅保留播放量有效的观测,并将数据类型转换为数值型。以下展示了转换后缺失值的情况及变量结构。
Whether there is NA in streams: FALSE
tibble [952 × 1] (S3: tbl_df/tbl/data.frame)
$ streams: num [1:952] 1.41e+08 1.34e+08 1.40e+08 8.01e+08 3.03e+08 ...
同样,Deezer 播放列表收录次数变量的原始类型也为字符型。再次使用 str_detect() 检查是否存在格式无效的数值。结果显示,存在若干逗号分隔格式的数值,且无其他格式的无效数值,因此在类型转换前需进行进一步处理。
Non-Number Values:
# A tibble: 79 × 1
in_deezer_playlists
<chr>
1 2,445
2 3,394
3 3,421
4 4,053
5 1,056
6 4,095
7 1,003
8 1,800
9 2,703
10 1,632
# ℹ 69 more rows
Neither Pure Number Nor Comma-Separated:
# A tibble: 0 × 1
# ℹ 1 variable: in_deezer_playlists <chr>
将所有逗号替换为空字符串后,对变量进行数值类型转换,并检查转换后是否存在缺失值。结果显示无缺失值。
Whether there is NA in in_deezer_playlists: FALSE
对于调性变量,首先检查其所有类别。结果显示存在无调性信息的空字符串,因此删除这些无调性信息的歌曲。删除后再次检查类别以确保数据有效。
Categories of Key:
[1] "B" "C#" "F" "A" "D" "F#" "" "G#" "G" "E" "A#" "D#"
Categories After Removal:
[1] "B" "C#" "F" "A" "D" "F#" "G#" "G" "E" "A#" "D#"
如下饼图所示,本数据集中两种音乐调式的占比相对均衡。具体而言,大调占比 55.3%,小调紧随其后,占比 44.7%。10.6% 的差距表明,通常与明亮情感特质相关的大调歌曲更为普遍,但小调歌曲仍占据相当比例。
# 饼图
plot_ly(
as.data.frame(table(df["mode"])),
labels = ~mode, values = ~Freq, type = "pie",
textinfo = "label+percent",
marker = list(
colors = c("khaki", "paleturquoise"),
line = list(width = 2))
) |>
layout(
title = list(y = 0.9, text = "音乐调式分布"), showlegend = FALSE,
margin = list(t = 80, l = 60, r = 60)
)在 BPM(每分钟拍数)方面,如下直方图所示,其分布在 110–130 BPM 附近达到峰值。大多数歌曲的 BPM 介于 90 到 150 之间,表明大众偏好适中节奏的音乐。
# BPM 分布
plot_ly(
df, x = ~bpm, type = "histogram",
marker = list(
color = "pink", line = list(width = 1.5, color = "white")
)
) |>
layout(
margin = list(t = 80),
title = list(y = 0.9, text = "BPM 分布"),
xaxis = list(
title = "每分钟拍数(BPM)",
linewidth = 2,
dtick = 20,
ticks = "outside",
ticklen = 5,
tickwidth = 1
),
yaxis = list(title = "频数")
)如下条形图所示,在播放量方面,C# 为最受欢迎的调性,平均播放量最高(约 6 亿次)。E 位居第二,D# 与 A# 均值相近紧随其后。D 在前五中最低。总体而言,各调性之间的差异较为适中,表明这些调性在流行度上较为均衡。
# 条形图
plot_ly(
top5_key, x = ~key, y = ~mean_streams, type = "bar",
marker = list(color = "mediumaquamarine")
) |>
layout(
title = list(y = 0.9, text = "最受欢迎的前五个调性"),
xaxis = list(title = "调性", linewidth = 2, range = c(-0.8, 4.8)),
yaxis = list(title = list(text = "平均播放量", standoff = 10)),
margin = list(t = 80, l = 80, r = 20)
)箱线图展示了不同调式类别下情绪效价(valence)的分布。小调歌曲的情绪效价中位数(54)略高于大调歌曲(48),两者的分布范围较宽,变异程度相似,四分位距存在重叠。尽管小调的平均情绪效价略高,但大量重叠表明单凭调式并不能强烈预测情绪效价。
如下散点图展示了歌曲在 Spotify 与 Deezer 播放列表中收录次数的关系,呈现出正相关趋势。随着歌曲在一个平台上播放量的增加,在另一个平台上的出现频率也随之提升。总体而言,数据最集中于 Spotify 收录次数 1 万以下、Deezer 收录次数 2 千以下的区间,少数高表现的异常值在 Spotify 超过 5 万次、Deezer 超过 1.2 万次。
# 散点图
plot_ly(df, x = ~in_spotify_playlists) |>
add_markers(
y = ~in_deezer_playlists,
marker = list(color = "salmon", opacity = 0.6)
) |>
layout(
margin = list(t = 80),
title = list(
y = 0.9,
text = "Spotify 与 Deezer 播放列表收录次数的关系"
),
xaxis = list(title = "Spotify 播放列表收录次数"),
yaxis = list(title = "Deezer 播放列表收录次数")
)中心极限定理指出,随着样本量的增加,样本均值的分布将越来越接近正态分布(对称钟形),无论总体分布的形状如何。
本节从 df 中的 bpm 变量中抽取不同样本量(n = 10、20、30、40)的样本,并重复抽样 1000 次以获取样本均值分布。下方的直方图矩阵在相同的 x 轴和 y 轴范围下展示了四种样本量的样本均值分布,以便比较其形状。矩阵之后是所有四种样本均值分布与总体统计量的对比输出。
结果表明,随着样本量的增加,样本均值的分布显著更加集中,标准差更小。同时,所有四种样本均值的均值均接近总体均值,体现了样本均值分布对总体的预测能力。此外,所有结果中的样本均值分布均呈对称形态,与正态分布形状相似。
综上,本节结果与中心极限定理的预测一致。
# 定义变量
sizes <- c(10, 20, 30, 40) # 样本量
sample_means <- numeric(1000) # 样本均值
means_sample_means <- numeric(length(sizes)) # 样本均值的均值
sds_sample_means <- numeric(length(sizes)) # 样本均值的标准差
p <- list() # 图形
# 重复抽样 1000 次
set.seed(123)
for (i in 1:length(sizes)) {
# 抽取样本并计算均值
sample_means <- replicate(
1000, mean(sample(df$bpm, size = sizes[i], replace = TRUE), na.rm = TRUE)
)
# 存储样本均值的均值与标准差
means_sample_means[i] <- mean(sample_means, na.rm = TRUE)
sds_sample_means[i] <- sd(sample_means, na.rm = TRUE)
# 存储图形信息
p[[i]] <- plot_ly(
x = sample_means, type = "histogram",
xbins = list(size = 2),
marker = list(color = "pink")
) |>
layout(
xaxis = list(
title = paste("n =", sizes[i]),
range = c(90, 150),
dtick = 15,
ticks = "outside",
ticklen = 5,
tickwidth = 1
),
yaxis = list(range = c(0, 180), dtick = 60)
)
}
# 显示图形
subplot(p, nrows = 2, margin = 0.1, titleX = TRUE) |>
layout(
title = list(y = 0.9, text = "BPM 样本均值分布"),
showlegend = FALSE,
margin = list(t = 80)
)# 总体均值与标准差
pop_mean <- mean(df$bpm, na.rm = TRUE)
pop_sd <- sd(df$bpm, na.rm = TRUE)
# 显示结果
cat(paste("Population Mean:", round(pop_mean, 2)), "\n")
cat(paste("Population SD:", round(pop_sd, 2)), "\n\n")
cat("Means of Sample Means:\n")
cat(paste0(
round(means_sample_means, 2), " (n=", seq(10, 40, 10), ")",
collapse = "\n"
))
cat("\n\n")
cat("SDs of Sample Means:\n")
cat(paste0(
round(sds_sample_means, 2), " (n=", seq(10, 40, 10), ")",
collapse = "\n"
))Population Mean: 122.84
Population SD: 28.2
Means of Sample Means:
122.99 (n=10)
123.27 (n=20)
123.09 (n=30)
122.65 (n=40)
SDs of Sample Means:
8.85 (n=10)
6.41 (n=20)
5.25 (n=30)
4.34 (n=40)
本节使用并比较三种抽样方法:有放回的简单随机抽样、以 bpm 为概率的不等概率系统抽样,以及按 mode 分层的无放回分层抽样。样本量设定为 100,并使用 sampling 包中的函数进行抽样。
# 有放回简单随机抽样
n <- 100
set.seed(123)
s <- srswr(n, nrow(df))
rows <- (1:nrow(df))[s!=0]
rows <- rep(rows, s[s!=0])
sample1 <- df[rows, ]
# 不等概率系统抽样
set.seed(123)
pik <- inclusionprobabilities(df$bpm, n) # 以 bpm 计算抽中概率
s <- UPsystematic(pik)
sample2 <- df[s!= 0, ]
# 无放回分层抽样
# 按 mode 设置层
order_index <- order(df$mode)
data <- df[order_index, ]
prop_table <- table(data$mode) / nrow(data)
sizes <- round(prop_table * n)
# 抽样
set.seed(32)
st <- sampling::strata(
data, stratanames = "mode", size = sizes, method = "srswor"
)
sample3 <- sampling::getdata(data, st)以下展示了从每个样本和总体中计算出的调式类别比例。如结果所示,分层抽样方法在预测总体调式比例方面表现最佳,因为它在抽样前根据调式类别划定层次。相比之下,简单随机抽样的表现最差,这可能是由于其随机性较强、缺乏分层抽样那样的结构性规律。此外,系统抽样的预测效果居中,优于简单随机抽样但不及分层抽样。这可能源于系统抽样按固定间隔抽取样本,样本在数据集中分布相对均匀,有助于提高类别属性比例的准确性;与此同时,以 bpm 为基础的不等概率选取也可能影响 mode 比例的分布规律。
# 调式比例
cat("Proportion of Modes in Population:")
(pct0 <- round(table(df$mode) / nrow(df), 2)); cat("\n")
cat("Proportion of Modes in Sample 1 (Simple Random):")
(pct1 <- table(sample1$mode) / nrow(sample1)); cat("\n")
cat("Proportion of Modes in Sample 2 (Systematic):")
(pct2 <- table(sample2$mode) / nrow(sample2)); cat("\n")
cat("Proportion of Modes in Sample 3 (Stratified):")
(pct3 <- table(sample3$mode) / nrow(sample3))Proportion of Modes in Population:
Major Minor
0.55 0.45
Proportion of Modes in Sample 1 (Simple Random):
Major Minor
0.63 0.37
Proportion of Modes in Sample 2 (Systematic):
Major Minor
0.46 0.54
Proportion of Modes in Sample 3 (Stratified):
Major Minor
0.55 0.45
---
title: 'Spotify 流量歌曲的音乐特征与受众趋势分析'
author: 'Pinn Xu'
date: '2026-02-25'
categories: [R, EDA, 音乐, 可视化, 抽样]
description: '对 2023 年 Spotify 播放量前列的歌曲进行探索性分析,揭示音乐流行趋势。'
image: ../../../src/img/proj/songs/cover.png
order: 5
aliases:
- ../most-streamed-songs/
## All below for Rmd
# output:
# html_document:
# fig_width: 8
# code_folding: hide
# highlight: tango
# theme: default
# toc: true
# toc_float:
# collapsed: true
---
```{r eval=FALSE, include=TRUE}
#| code-summary: '环境与版本'
R==4.5.2
plotly==4.12.0
sampling==2.11
tidyverse==2.0.0
```
```{r setup, include=FALSE}
knitr::opts_chunk$set(
fig.width=8, fig.height=5, results='hold',
echo=TRUE, warning=FALSE, message=FALSE
)
```
# 数据集概览
本项目使用来自 [Kaggle](https://www.kaggle.com/datasets/nelgiriyewithana/top-spotify-songs-2023/) 的数据集 *Most Streamed Spotify Songs 2023*,包含 2023 年 Spotify 最受欢迎的 953 首歌曲,属性涵盖*曲目名称*、*艺术家姓名*、*发行日期*、*Spotify 播放列表与榜单*、*播放统计*、*Apple Music 收录情况*、*Deezer 收录情况*、*Shazam 榜单*及若干音频特征。
本项目使用的核心数值变量包括:*streams*(播放量)、*BPM*(每分钟拍数)、*valence index*(情绪效价指数)以及*在 Spotify 和 Deezer 播放列表中的收录次数*。*streams* 表示一首歌曲在 2023 年全年的播放次数,应为正整数;*收录次数*同样为计数变量(非负整数)。*BPM* 决定音乐的节奏与速度。*情绪效价指数*表示歌曲音乐内容的积极程度,编码范围为 0 到 100(0 = 完全消极,100 = 完全积极)。
类别变量包括音乐的*调式*(mode)与*调性*(key)。*调式*分为大调(major)和小调(minor)两类,是两种通用的和弦类别;*调性*涵盖 C、C#、D、D# 等,决定了歌曲所使用的基础音符与和弦组合。
总体而言,由于变量的维度与类型各异,本数据集被认为适合后续分析。
# 分析目标
本项目旨在分析 2023 年 Spotify 最受欢迎歌曲的音乐特征的显著规律与趋势,并探究各指标之间的潜在关系。此外,将通过不同抽样方法对数据进行抽样,以验证中心极限定理(CLT)的适用性,并比较各抽样方法对总体参数的预测能力。
# 数据准备
在正式分析前,数据集首先从 Kaggle 网站下载并存储至本地文件夹,随后使用 `read.csv()` 函数导入 R。以下展示了导入后数据集作为 DataFrame 对象的结构。
```{r}
# 导入数据集
spotify_data <- read.csv("../../../src/data/spotify-2023.csv")
str(spotify_data)
```
随后,导入了本项目所使用的主要包(包括 *tidyverse*、*plotly* 和 *sampling*),并使用 `tibble()` 函数将原始数据集转换为 tibble(`df`),以便后续数据处理。
以下展示了数据集的初步概览,包括样本量(N = 953)及是否存在缺失值(使用 `anyNA()` 函数获取缺失值信息)。
```{r}
# 加载包
library(readr)
library(tidyverse)
library(sampling)
library(plotly)
# 将 dataframe 转换为 tibble
df <- tibble(spotify_data)
# 样本量
cat("Sample Size:", nrow(df), '\n')
# 检查缺失值
cat("Whether there is NA:", anyNA(df), "\n")
```
## Streams(播放量)
如下所示,*streams* 变量的数据类型为字符型,而播放量应为数值型而非字符串,因此需要进行类型转换。
```{r}
# 查看结构
str(df["streams"])
```
在将数据类型转换为数值型之前,首先检查是否存在无法识别为数值的非法字符串,使用正则表达式进行数据筛选。结果显示,有 1 条观测的播放量值无效。
```{r}
# 查看无效值
cat("Invalid Streams: \n\n")
df |>
select(streams) |>
filter(!str_detect(streams, "^(0|[1-9][0-9]*)$"))
```
随后,仅保留播放量有效的观测,并将数据类型转换为数值型。以下展示了转换后缺失值的情况及变量结构。
```{r}
# 仅保留有效值
df <- df |>
filter(str_detect(streams, "^[1-9][0-9]*"))
# 将数据类型强制转换为数值型
df <- df |>
mutate(streams = as.numeric(streams))
# 检查类型转换后 streams 中是否存在 NA
cat("Whether there is NA in streams:", anyNA(df$streams), "\n\n")
# 新结构
str(df["streams"])
```
## Deezer 播放列表收录次数
同样,Deezer 播放列表收录次数变量的原始类型也为字符型。再次使用 `str_detect()` 检查是否存在格式无效的数值。结果显示,存在若干逗号分隔格式的数值,且无其他格式的无效数值,因此在类型转换前需进行进一步处理。
```{r}
# 查看不是 0 或纯数字的值
cat("Non-Number Values:\n\n")
df |>
select(in_deezer_playlists) |>
filter(!str_detect(in_deezer_playlists, "^(0|[1-9][0-9]*)$"))
```
```{r}
# 应用新模式:逗号分隔的数字
cat("Neither Pure Number Nor Comma-Separated:\n\n")
df |>
select(in_deezer_playlists) |>
filter(!str_detect(
in_deezer_playlists, "^(0|[1-9][0-9]*|[1-9][0-9]*,[0-9]*)$")
)
```
将所有逗号替换为空字符串后,对变量进行数值类型转换,并检查转换后是否存在缺失值。结果显示无缺失值。
```{r}
# 将逗号替换为空字符串
df <- df |>
mutate(in_deezer_playlists = str_replace_all(in_deezer_playlists, ",", ""))
# 将数据类型强制转换为数值型
df <- df |>
mutate(in_deezer_playlists = as.numeric(in_deezer_playlists))
# 检查类型转换后是否存在 NA
cat("Whether there is NA in in_deezer_playlists:",
anyNA(df$in_deezer_playlists), "\n"
)
```
## 调性(Key)
对于*调性*变量,首先检查其所有类别。结果显示存在无调性信息的空字符串,因此删除这些无调性信息的歌曲。删除后再次检查类别以确保数据有效。
```{r}
# 检查 key 变量的类别
cat("Categories of Key:\n")
unique(df$key)
# 删除 key 为空的行
df <- df[df$key != "", ]
# 删除后再次检查
cat("\nCategories After Removal:\n")
unique(df$key)
```
# 单变量分析
## 调式分布
如下饼图所示,本数据集中两种音乐调式的占比相对均衡。具体而言,大调占比 55.3%,小调紧随其后,占比 44.7%。10.6% 的差距表明,通常与明亮情感特质相关的大调歌曲更为普遍,但小调歌曲仍占据相当比例。
```{r}
# 饼图
plot_ly(
as.data.frame(table(df["mode"])),
labels = ~mode, values = ~Freq, type = "pie",
textinfo = "label+percent",
marker = list(
colors = c("khaki", "paleturquoise"),
line = list(width = 2))
) |>
layout(
title = list(y = 0.9, text = "音乐调式分布"), showlegend = FALSE,
margin = list(t = 80, l = 60, r = 60)
)
```
## BPM 分布
在 BPM(每分钟拍数)方面,如下直方图所示,其分布在 110–130 BPM 附近达到峰值。大多数歌曲的 BPM 介于 90 到 150 之间,表明大众偏好适中节奏的音乐。
```{r}
# BPM 分布
plot_ly(
df, x = ~bpm, type = "histogram",
marker = list(
color = "pink", line = list(width = 1.5, color = "white")
)
) |>
layout(
margin = list(t = 80),
title = list(y = 0.9, text = "BPM 分布"),
xaxis = list(
title = "每分钟拍数(BPM)",
linewidth = 2,
dtick = 20,
ticks = "outside",
ticklen = 5,
tickwidth = 1
),
yaxis = list(title = "频数")
)
```
# 多变量分析
## 播放量最高的前五调性
如下条形图所示,在播放量方面,C# 为最受欢迎的调性,平均播放量最高(约 6 亿次)。E 位居第二,D# 与 A# 均值相近紧随其后。D 在前五中最低。总体而言,各调性之间的差异较为适中,表明这些调性在流行度上较为均衡。
```{r}
# 找出平均播放量最高的前五个调性
top5_key <- df |>
group_by(key) |>
summarize(mean_streams = mean(streams), .groups = "drop") |>
arrange(desc(mean_streams)) |>
head(5) |>
mutate(key = reorder(key, -mean_streams)) # 将 key 转换为因子
```
```{r}
# 条形图
plot_ly(
top5_key, x = ~key, y = ~mean_streams, type = "bar",
marker = list(color = "mediumaquamarine")
) |>
layout(
title = list(y = 0.9, text = "最受欢迎的前五个调性"),
xaxis = list(title = "调性", linewidth = 2, range = c(-0.8, 4.8)),
yaxis = list(title = list(text = "平均播放量", standoff = 10)),
margin = list(t = 80, l = 80, r = 20)
)
```
## 不同调式的情绪效价分布
箱线图展示了不同*调式*类别下*情绪效价*(valence)的分布。小调歌曲的情绪效价中位数(54)略高于大调歌曲(48),两者的分布范围较宽,变异程度相似,四分位距存在重叠。尽管小调的平均情绪效价略高,但大量重叠表明单凭调式并不能强烈预测情绪效价。
```{r}
# 箱线图
plot_ly(
df, x = ~mode, y = ~valence_., type = "box",
fillcolor = "thistle",
line = list(width = 2, color = "darkslateblue")
) |>
layout(
margin = list(t = 80), # 顶部边距
title = list(
y = 0.9,
text = "不同调式的情绪效价分布"
),
xaxis = list(title = "音乐调式"),
yaxis = list(title = "情绪效价")
)
```
## Spotify 与 Deezer 播放列表收录次数
如下散点图展示了歌曲在 Spotify 与 Deezer 播放列表中收录次数的关系,呈现出正相关趋势。随着歌曲在一个平台上播放量的增加,在另一个平台上的出现频率也随之提升。总体而言,数据最集中于 Spotify 收录次数 1 万以下、Deezer 收录次数 2 千以下的区间,少数高表现的异常值在 Spotify 超过 5 万次、Deezer 超过 1.2 万次。
```{r}
# 散点图
plot_ly(df, x = ~in_spotify_playlists) |>
add_markers(
y = ~in_deezer_playlists,
marker = list(color = "salmon", opacity = 0.6)
) |>
layout(
margin = list(t = 80),
title = list(
y = 0.9,
text = "Spotify 与 Deezer 播放列表收录次数的关系"
),
xaxis = list(title = "Spotify 播放列表收录次数"),
yaxis = list(title = "Deezer 播放列表收录次数")
)
```
# 中心极限定理
中心极限定理指出,随着样本量的增加,样本均值的分布将越来越接近正态分布(对称钟形),无论总体分布的形状如何。
本节从 `df` 中的 *bpm* 变量中抽取不同样本量(n = 10、20、30、40)的样本,并重复抽样 1000 次以获取样本均值分布。下方的直方图矩阵在相同的 x 轴和 y 轴范围下展示了四种样本量的样本均值分布,以便比较其形状。矩阵之后是所有四种样本均值分布与总体统计量的对比输出。
结果表明,随着样本量的增加,样本均值的分布显著更加集中,标准差更小。同时,所有四种样本均值的均值均接近总体均值,体现了样本均值分布对总体的预测能力。此外,所有结果中的样本均值分布均呈对称形态,与正态分布形状相似。
综上,本节结果与中心极限定理的预测一致。
```{r}
# 定义变量
sizes <- c(10, 20, 30, 40) # 样本量
sample_means <- numeric(1000) # 样本均值
means_sample_means <- numeric(length(sizes)) # 样本均值的均值
sds_sample_means <- numeric(length(sizes)) # 样本均值的标准差
p <- list() # 图形
# 重复抽样 1000 次
set.seed(123)
for (i in 1:length(sizes)) {
# 抽取样本并计算均值
sample_means <- replicate(
1000, mean(sample(df$bpm, size = sizes[i], replace = TRUE), na.rm = TRUE)
)
# 存储样本均值的均值与标准差
means_sample_means[i] <- mean(sample_means, na.rm = TRUE)
sds_sample_means[i] <- sd(sample_means, na.rm = TRUE)
# 存储图形信息
p[[i]] <- plot_ly(
x = sample_means, type = "histogram",
xbins = list(size = 2),
marker = list(color = "pink")
) |>
layout(
xaxis = list(
title = paste("n =", sizes[i]),
range = c(90, 150),
dtick = 15,
ticks = "outside",
ticklen = 5,
tickwidth = 1
),
yaxis = list(range = c(0, 180), dtick = 60)
)
}
# 显示图形
subplot(p, nrows = 2, margin = 0.1, titleX = TRUE) |>
layout(
title = list(y = 0.9, text = "BPM 样本均值分布"),
showlegend = FALSE,
margin = list(t = 80)
)
```
```{r}
# 总体均值与标准差
pop_mean <- mean(df$bpm, na.rm = TRUE)
pop_sd <- sd(df$bpm, na.rm = TRUE)
# 显示结果
cat(paste("Population Mean:", round(pop_mean, 2)), "\n")
cat(paste("Population SD:", round(pop_sd, 2)), "\n\n")
cat("Means of Sample Means:\n")
cat(paste0(
round(means_sample_means, 2), " (n=", seq(10, 40, 10), ")",
collapse = "\n"
))
cat("\n\n")
cat("SDs of Sample Means:\n")
cat(paste0(
round(sds_sample_means, 2), " (n=", seq(10, 40, 10), ")",
collapse = "\n"
))
```
# 抽样方法
## 抽样
本节使用并比较三种抽样方法:有放回的简单随机抽样、以 *bpm* 为概率的不等概率系统抽样,以及按 *mode* 分层的无放回分层抽样。样本量设定为 100,并使用 *sampling* 包中的函数进行抽样。
```{r}
# 有放回简单随机抽样
n <- 100
set.seed(123)
s <- srswr(n, nrow(df))
rows <- (1:nrow(df))[s!=0]
rows <- rep(rows, s[s!=0])
sample1 <- df[rows, ]
# 不等概率系统抽样
set.seed(123)
pik <- inclusionprobabilities(df$bpm, n) # 以 bpm 计算抽中概率
s <- UPsystematic(pik)
sample2 <- df[s!= 0, ]
# 无放回分层抽样
# 按 mode 设置层
order_index <- order(df$mode)
data <- df[order_index, ]
prop_table <- table(data$mode) / nrow(data)
sizes <- round(prop_table * n)
# 抽样
set.seed(32)
st <- sampling::strata(
data, stratanames = "mode", size = sizes, method = "srswor"
)
sample3 <- sampling::getdata(data, st)
```
## 方法比较
以下展示了从每个样本和总体中计算出的调式类别比例。如结果所示,分层抽样方法在预测总体调式比例方面表现最佳,因为它在抽样前根据调式类别划定层次。相比之下,简单随机抽样的表现最差,这可能是由于其随机性较强、缺乏分层抽样那样的结构性规律。此外,系统抽样的预测效果居中,优于简单随机抽样但不及分层抽样。这可能源于系统抽样按固定间隔抽取样本,样本在数据集中分布相对均匀,有助于提高类别属性比例的准确性;与此同时,以 *bpm* 为基础的不等概率选取也可能影响 *mode* 比例的分布规律。
```{r}
# 调式比例
cat("Proportion of Modes in Population:")
(pct0 <- round(table(df$mode) / nrow(df), 2)); cat("\n")
cat("Proportion of Modes in Sample 1 (Simple Random):")
(pct1 <- table(sample1$mode) / nrow(sample1)); cat("\n")
cat("Proportion of Modes in Sample 2 (Systematic):")
(pct2 <- table(sample2$mode) / nrow(sample2)); cat("\n")
cat("Proportion of Modes in Sample 3 (Stratified):")
(pct3 <- table(sample3$mode) / nrow(sample3))
```