数据可视化:使用 Quil 绘制二维直方图
1. 数据可视化概述
在数据科学中,可视化是理解和传达数据信息的重要手段。数字背后蕴含着重要的故事,需要我们为其赋予清晰且有说服力的表达。在探索数据集时,我们通常会进行探索性数据分析(Exploratory Data Analysis),关注数值数据的分布、分类数据的计数以及数据属性之间的相关性。
常用的可视化工具如 Incanter 能生成多种图表,但有时可能无法满足特定数据的可视化需求。此时,其他 Clojure 库如 clojurewerkz/envision 和 Karsten Schmidt 的 thi-ng/geom 可以提供更多探索性数据可视化的能力。
2. 数据获取
本次可视化使用了 2011 年俄罗斯选举数据以及美国财富分配数据。美国财富分配数据量较小,可直接在源代码中输入;俄罗斯选举数据的源代码可从 https://github.com/clojuredatascience/ch10-visualization 下载。
下载源代码后,可在项目根目录下运行以下命令下载选举数据:
script/download-data.sh
若之前已下载过该数据,也可将数据文件直接移动到本章的数据目录。
3. 探索性数据可视化问题
在之前的分析中,我们使用带有透明度的散点图来可视化选民投票率与获胜者得票比例之间的关系,但该图表并非理想选择,因为我们主要关注特定区域内点的密度,而散点图的透明度虽能揭示数据结构,但表示不够明确,部分点过淡难以看清,或过于密集而重叠。
为解决这些问题,我们可以使用二维直方图。二维直方图通过颜色来表示二维空间中高低密度区域,将图表划分为网格,每个网格单元代表两个维度上的一个范围,落入该单元的点越多,该范围内的密度就越大。
4. 二维直方图的表示
直方图是将连续分布表示为一系列区间(bins)的方式。以下是用于将连续数据划分为离散区间的 bin 函数:
(defn bin [n-bins xs]
(let [min-x (apply min xs)
range-x (- (apply max xs) min-x)
max-bin (dec n-bins)
bin-fn (fn [x]
(-> (- x min-x)
(/ range-x)
(* n-bins)
(int)
(min max-bin)))]
(map bin-fn xs)))
例如,将 0 到 19 的范围划分为 5 个区间,可得到如下序列:
(defn ex-1-1 []
(bin 5 (range 20)))
;;(0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4)
bin 函数返回每个数据点所在的区间索引,而非计数。我们可以使用 Clojure 的 frequencies 函数来确定落入每个区间的点的数量:
(defn ex-1-2 []
(frequencies (bin 5 (range 20))))
;;{0 4, 1 4, 2 4, 3 4, 4 4}
这是一维直方图的合理表示方式。对于二维直方图,我们可以对 x 和 y 数据分别执行相同的计算,并使用 vector 函数将区间索引组合成 [x-bin y-bin] 的形式:
(defn histogram-2d [xs ys n-bins]
(-> (map vector
(bin n-bins xs)
(bin n-bins ys))
(frequencies)))
以下是一个示例,展示如何使用该函数:
(defn ex-10-3 []
(histogram-2d (range 20)
(reverse (range 20)) 5))
;;{[0 4] 4, [1 3] 4, [2 2] 4, [3 1] 4, [4 0] 4}
若要绘制真实数据的二维直方图,可加载俄罗斯选举数据:
(defn ex-10-4 []
(let [data (load-data :ru-victors)]
(histogram-2d (i/$ :turnout data)
(i/$ :victors-share data) 5)))
运行上述代码后,可得到直方图区间的计数值,这些计数值将作为我们在直方图上绘制的密度值。
5. 使用 Quil 进行可视化
Quil 是一个 Clojure 库,它封装了 Processing 框架,提供了极大的灵活性来创建自定义可视化。使用 Quil 进行可视化需要创建一个 sketch,这是 Processing 中对包含绘图指令的程序的称呼。大部分 API 函数可从 quil.core 命名空间获取,我们在代码中以 q 表示。
以下是一个简单的示例,创建一个 250x250 像素的窗口:
(q/sketch :size [250 250])
Quil 提供了标准的二维图形绘制原语,如点、线、弧、三角形、四边形、矩形和椭圆。若要绘制矩形,可使用 q/rect 函数,并指定位置(x 和 y 坐标)以及宽度和高度。
以下代码在原点绘制一个边长为 50 像素的正方形:
(defn ex-10-5 []
(let [setup #(q/rect 0 0 50 50)]
(q/sketch :setup setup
:size [250 250])))
6. Quil 的坐标系统
Quil 使用的坐标系统与大多数计算机图形程序相同,原点位于显示屏的左上角,y 轴向下,x 轴向右。这与大多数图表中的 y 轴方向不同,因此在绘制时通常需要翻转 y 坐标。
常见的做法是用草图的高度减去所需的 y 值(从草图底部测量),这样 y 为 0 时对应草图的底部,y 值越大对应草图中越高的位置。
7. 绘制网格
以下函数可用于绘制简单的网格,它接受区间数量 n-bins 和大小参数 [width height]:
(defn draw-grid [{:keys [n-bins size]}]
(let [[width height] size
x-scale (/ width n-bins)
y-scale (/ height n-bins)
setup (fn []
(doseq [x (range n-bins)
y (range n-bins)
:let [x-pos (* x x-scale)
y-pos (- height
(* (inc y) y-scale))]]
(q/rect x-pos y-pos x-scale y-scale)))]
(q/sketch :setup setup :size size)))
该函数通过计算 x 和 y 方向的缩放因子,在每个区间位置绘制一个矩形。需要注意的是,绘图指令在 doseq 中执行,以避免 Clojure 的惰性求值导致无绘制结果。
8. 指定填充颜色
在 Quil 中,使用 q/fill 函数来填充颜色,指定的填充颜色将一直使用,直到指定新的填充颜色。许多 Quil 函数会影响当前绘图上下文,具有状态性,如 fill、stroke、scale 和 font 等。
以下是 draw-grid 函数的改进版本 draw-filled-grid,增加了 fill-fn 参数,用于确定网格中每个点的矩形填充颜色:
(defn draw-filled-grid [{:keys [n-bins size fill-fn]}]
(let [[width height] size
x-scale (/ width n-bins)
y-scale (/ height n-bins)
setup (fn []
(doseq [x (range n-bins)
y (range n-bins)
:let [x-pos (* x x-scale)
y-pos (- height
(* (inc y) y-scale))]]
(q/fill (fill-fn x y))
(q/rect x-pos y-pos x-scale y-scale)))]
(q/sketch :setup setup :size size)))
Quil 的 fill 函数接受多种参数形式:
| 参数数量 | 描述 |
| ---- | ---- |
| 1 个 | RGB 值(可以是数字或 q/color 表示) |
| 2 个 | RGB 值加上透明度 |
| 3 个 | 红、绿、蓝分量,取值范围为 0 到 255 |
| 4 个 | 红、绿、蓝和透明度分量 |
我们可以将直方图区间的计数值除以最大值,再乘以 255,得到一个 0 到 255 之间的灰度值,用于填充矩形:
(defn ex-10-6 []
(let [data (load-data :ru-victors)
n-bins 5
hist (histogram-2d (i/$ :turnout data)
(i/$ :victors-share data)
n-bins)
max-val (apply max (vals hist))
fill-fn (fn [x y]
(-> (get hist [x y] 0)
(/ max-val)
(* 255)))]
(draw-filled-grid {:n-bins n-bins
:size [250 250]
:fill-fn fill-fn})))
为了提高直方图的分辨率,我们可以增加区间数量:
(defn ex-10-7 []
(let [data (load-data :ru-victors)
n-bins 25
hist (histogram-2d (i/$ :turnout data)
(i/$ :victors-share data)
n-bins)
max-val (apply max (vals hist))
fill-fn (fn [x y]
(-> (get hist [x y] 0)
(/ max-val)
(* 255)))]
(draw-filled-grid {:n-bins n-bins
:size [250 250]
:fill-fn fill-fn})))
然而,增加区间数量后,直方图中的大部分单元格变得很暗,难以分辨细节,尤其是右上角的值过高,导致中心区域也变得模糊。为解决这个问题,我们可以采取以下两种方法:
- 绘制 z-score 而非实际值,以减轻异常值的影响。
- 使用更多颜色来丰富视觉信息。
以下是绘制 z-score 的代码:
(defn ex-10-8 []
(let [data (load-data :ru-victors)
n-bins 25
hist (histogram-2d (i/$ :turnout data)
(i/$ :victors-share data)
n-bins)
mean (s/mean (vals hist))
sd (s/sd (vals hist))
fill-fn (fn [x y]
(-> (get hist [x y] 0)
(- mean)
(/ sd)
(q/map-range -1 3 0 255)))]
(draw-filled-grid {:n-bins n-bins
:size [250 250]
:fill-fn fill-fn})))
Quil 的 map-range 函数可将一个范围的值映射到另一个范围的值。通过绘制 z-score,我们可以得到一个更清晰的灰度表示,但区分不同灰度仍然具有挑战性。
9. 颜色与填充
为了进一步改善可视化效果,我们可以使用颜色来表示每个单元格,使直方图更像热图。“较冷”的颜色(如蓝色和绿色)表示低值,“较热”的颜色(如橙色和红色)表示高密度区域。
以下是将 z-score 映射到颜色的函数:
(defn z-score->heat [z-score]
(let [colors [(q/color 0 0 255) ;; Blue
(q/color 0 255 255) ;; Turquoise
(q/color 0 255 0) ;; Green
(q/color 255 255 0) ;; Yellow
(q/color 255 0 0)] ;; Red
offset (-> (q/map-range z-score -1 3 0 3.999)
(max 0)
(min 3.999))]
(q/lerp-color (nth colors offset)
(nth colors (inc offset))
(rem offset 1))))
我们将该函数传递给 draw-filled-grid 函数:
(defn ex-10-9 []
(let [data (load-data :ru-victors)
n-bins 25
hist (histogram-2d (i/$ :turnout data)
(i/$ :victors-share data)
n-bins)
mean (s/mean (vals hist))
sd (s/sd (vals hist))
fill-fn (fn [x y]
(-> (get hist [x y] 0)
(- mean)
(/ sd)
(z-score->heat)))]
(draw-filled-grid {:n-bins n-bins
:size [250 250]
:fill-fn fill-fn})))
热图能够更清晰地展示数据的内部结构,使之前难以确定的细节变得更加明显。
通过以上步骤,我们可以使用 Quil 库创建自定义的二维直方图和热图,有效地可视化数据并传达信息。
graph LR
A[获取数据] --> B[探索性数据分析]
B --> C[选择可视化方式]
C --> D[二维直方图]
D --> E[使用Quil绘制]
E --> F[绘制网格]
F --> G[指定填充颜色]
G --> H[绘制灰度直方图]
H --> I[增加区间数量]
I --> J[解决细节问题]
J --> K[绘制z-score]
K --> L[使用颜色填充]
L --> M[生成热图]
数据可视化:使用 Quil 绘制二维直方图
10. 热图的优势与效果分析
使用热图进行数据可视化具有显著的优势。从之前的灰度直方图到热图的转变,我们可以更清晰地观察到数据的内部结构。在灰度直方图中,由于区分不同灰度的难度较大,一些数据细节难以展现。而热图通过使用丰富的颜色,将数据的密度信息以更直观的方式呈现出来。
例如,在之前的俄罗斯选举数据可视化中,热图能够让我们更清楚地看到数据中的强对角线形状,同时也能观察到该形状内部的更多变化。原本由于区域过密或过疏而难以确定的细节,在热图中变得更加明显。这有助于我们更深入地分析数据,发现潜在的规律和趋势。
11. 总结与拓展
通过上述的步骤和代码示例,我们详细介绍了如何使用 Quil 库创建自定义的二维直方图和热图。整个过程可以总结为以下几个关键步骤:
1.
数据获取
:从指定的数据源下载或直接输入所需的数据。
2.
数据处理
:使用
bin
函数将连续数据划分为离散区间,并通过
histogram-2d
函数生成二维直方图的计数值。
3.
可视化准备
:使用 Quil 创建 sketch 窗口,并了解其坐标系统。
4.
绘制基础图形
:绘制网格,为后续的填充颜色做准备。
5.
颜色填充
:从简单的灰度填充到使用 z-score 映射颜色,再到最终生成热图。
在实际应用中,我们可以根据具体的数据特点和分析需求,对这些步骤进行拓展和优化。例如:
-
数据方面
:可以尝试使用不同的数据集进行可视化,或者对现有数据进行进一步的清洗和预处理,以提高可视化的效果。
-
可视化方面
:可以调整热图的颜色映射范围、区间数量等参数,以满足不同的视觉需求。还可以添加更多的图形元素,如坐标轴、标签等,使可视化结果更加完整和专业。
以下是一个简单的拓展示例,为热图添加坐标轴标签:
(defn draw-heatmap-with-labels [{:keys [n-bins size fill-fn]}]
(let [[width height] size
x-scale (/ width n-bins)
y-scale (/ height n-bins)
setup (fn []
;; 绘制热图
(doseq [x (range n-bins)
y (range n-bins)
:let [x-pos (* x x-scale)
y-pos (- height
(* (inc y) y-scale))]]
(q/fill (fill-fn x y))
(q/rect x-pos y-pos x-scale y-scale))
;; 添加 x 轴标签
(q/fill 0)
(q/text-align q/CENTER q/TOP)
(doseq [x (range n-bins)]
(let [x-pos (+ (* x x-scale) (/ x-scale 2))
y-pos height]
(q/text (str x) x-pos y-pos)))
;; 添加 y 轴标签
(q/text-align q/RIGHT q/CENTER)
(doseq [y (range n-bins)]
(let [x-pos 0
y-pos (- height (* (inc y) y-scale) (/ y-scale 2))]
(q/text (str y) x-pos y-pos))))]
(q/sketch :setup setup :size size)))
(defn ex-10-10 []
(let [data (load-data :ru-victors)
n-bins 25
hist (histogram-2d (i/$ :turnout data)
(i/$ :victors-share data)
n-bins)
mean (s/mean (vals hist))
sd (s/sd (vals hist))
fill-fn (fn [x y]
(-> (get hist [x y] 0)
(- mean)
(/ sd)
(z-score->heat)))]
(draw-heatmap-with-labels {:n-bins n-bins
:size [250 250]
:fill-fn fill-fn})))
通过这个拓展示例,我们可以看到如何在热图的基础上添加坐标轴标签,使可视化结果更加清晰和易于理解。
12. 注意事项
在使用 Quil 进行数据可视化时,还需要注意以下几点:
-
坐标系统
:Quil 的坐标系统原点位于左上角,y 轴向下,与常规的数学坐标系统不同。在绘制图形时,需要根据这个特点进行相应的坐标转换。
-
状态性函数
:Quil 中的许多函数具有状态性,如
fill
、
stroke
等。在使用这些函数时,要注意其对后续绘图指令的影响,避免出现意外的绘图结果。
-
性能问题
:当处理大量数据或增加区间数量时,可能会导致绘图性能下降。在这种情况下,可以考虑对数据进行采样或优化算法,以提高绘图效率。
13. 总结表格
| 步骤 | 操作 | 代码示例 |
|---|---|---|
| 数据获取 | 从指定数据源下载或输入数据 |
(load-data :ru-victors)
|
| 数据处理 | 划分区间并生成二维直方图计数值 |
(histogram-2d (i/$ :turnout data) (i/$ :victors-share data) n-bins)
|
| 可视化准备 | 创建 sketch 窗口 |
(q/sketch :size [250 250])
|
| 绘制基础图形 | 绘制网格 |
(draw-grid {:n-bins n-bins :size [250 250]})
|
| 颜色填充 | 灰度填充 |
(draw-filled-grid {... :fill-fn (fn [x y] (-> (get hist [x y] 0) (/ max-val) (* 255)))})
|
| 颜色填充 | z-score 映射颜色 |
(draw-filled-grid {... :fill-fn (fn [x y] (-> (get hist [x y] 0) (- mean) (/ sd) (q/map-range -1 3 0 255)))})
|
| 颜色填充 | 生成热图 |
(draw-filled-grid {... :fill-fn (fn [x y] (-> (get hist [x y] 0) (- mean) (/ sd) (z-score->heat)))})
|
| 拓展 | 添加坐标轴标签 |
(draw-heatmap-with-labels {...})
|
graph LR
A[数据获取] --> B[数据处理]
B --> C[可视化准备]
C --> D[绘制基础图形]
D --> E[颜色填充]
E --> F[灰度填充]
E --> G[z-score映射颜色]
E --> H[生成热图]
H --> I[拓展优化]
I --> J[添加坐标轴标签]
通过以上内容,我们全面介绍了使用 Quil 进行数据可视化的方法,从基础的二维直方图绘制到热图的生成,再到拓展和优化。希望这些内容能够帮助你更好地进行数据可视化工作,发现数据中的价值。
超级会员免费看
60

被折叠的 条评论
为什么被折叠?



