跳到主要内容
Deno 2.4 已发布,带来 deno bundle、字节/文本导入、OTel 稳定版等新功能
了解更多

使用 TypeScript、Jupyter、Polars 和 Observable Plot 探索艺术

Jupyter Notebook 已成为交互式计算和数据分析的实际标准,它将代码、散文和可视化结合在一个文档中。

事实上,这篇博客文章就是在一个 Notebook 中编写的!

v1.37 起,Deno 带有内置的 Jupyter 内核,将 JavaScript 和 TypeScript 带入数据科学和机器学习领域。我曾大量使用计算型 Notebook(主要使用 Python),我发现以下原因令我对此感到兴奋

  • 简单设置 – 内核内置于 Deno CLI 中,无需额外安装。只需安装 Deno 即可。

  • 改进的依赖管理 – Notebook 的运行方式与独立脚本类似,Deno 对在代码本身中导入依赖项的支持改进了依赖管理和可重现性。

  • 交互式数据分析的统一生态系统 – Jupyter 支持丰富的 HTML/CSS/JS 输出,而 JavaScript 生态系统非常适合交互式 UI。Deno 连接了内核和前端,使 Notebook 更加强大和灵活。

在这篇文章中,我们将在 Jupyter 中使用 Deno 探索美国国家艺术馆的开放访问数据集。我们将清理和分析数据,查看公共领域作品、艺术家、来源和模式。在此过程中,我们将介绍 Deno 兼容工具,例如 npm:nodejs-polarsnpm:@observablehq/plot,最后通过添加小部件和自定义显示来获得更具互动性的体验。

数据集

美国国家艺术馆 (NGA) 开放数据项目 提供了超过 130,000 件艺术品及其创作者的访问权限,这些数据可在 GitHub 上获取。此数据集包含有价值的元数据,例如标题、日期、艺术家和分类,所有这些均在 知识共享0 (CC0) 许可下,这意味着可以自由使用和分享。

该藏品涵盖了各种各样的艺术品,从雕塑到绘画,包括玛丽·卡萨特、M.C. 埃舍尔、文森特·梵高、巴勃罗·毕加索和乔治亚·奥基夫等著名艺术家的作品。

然而,浏览此资源具有挑战性。国家艺术馆的网站用户体验不佳,使得难以理解数据集中的实际内容。几乎不可能获得任何高级别的洞察,例如绘画数量、哪些时期最具代表性,或者哪些艺术家最普遍——更不用说进行具体查询了。

幸运的是,该数据集可在 GitHub 上获取,作为相关表的集合,以 CSV 文件形式导出。为了进行分析,我们将重点关注三个关键表

  • objects.csv – 关于艺术品的元数据,包括标题、日期、材料和分类。
  • constituents.csv – 艺术家详情,例如姓名、国籍和生卒年份。
  • published_images.csv – 通过 NGA 的 IIIF API 链接到艺术品图像。

我们将清理连接这些表,以创建一个统一的数据集

但是等等!虽然这些 NGA 表格可以免费使用,但它们只包含元数据。实际的艺术品图像具有独立的许可,其中只有大约一半是 CC0 许可的。我们将单独收集这些信息,以识别可自由使用的图像。

我们的目标是统一数据,以识别公共领域的图像,探索子集,并查看可用的艺术品。也许你甚至会为你的默认操作系统壁纸找到一个升级。

加载和清理数据

开放访问数据集

首先,我们需要下载数据。如果你之前在 JavaScript 中处理过 CSV 文件,这通常看起来像一个 fetch 请求,然后进行一些解析。在这里,我们使用 jsr:@std/csv 来流式传输数据,这会给我们一个对象数组。

笔记本 [1]
import * as csv from "jsr:@std/csv@1.0.5";

let baseUrl = new URL(
  "https://github.com/NationalGalleryOfArt/opendata/raw/refs/heads/main/data/",
);
let response = await fetch(new URL("objects.csv", baseUrl));

let objects = await Array.fromAsync(
  response.body.pipeThrough(new TextDecoderStream()).pipeThrough(
    new csv.CsvParseStream({ skipFirstRow: true }),
  ),
  (row) => ({
    objectid: +row.objectid,
    title: row.title,
    year: +row.beginyear,
    medium: row.medium,
    type: row.visualbrowserclassification,
  }),
);

objects.slice(0, 3);
单元格输出
[
  {
    "objectid": 0,
    "title": "Saint James Major",
    "year": 1310,
    "medium": "tempera on panel",
    "type": "painting"
  },
  {
    "objectid": 1,
    "title": "Saint Paul and a Group of Worshippers",
    "year": 1333,
    "medium": "tempera on panel",
    "type": "painting"
  },
  {
    "objectid": 4,
    "title": "Saint Bernard and Saint Catherine of Alexandria with the Virgin of the Annunciation [right panel]",
    "year": 1387,
    "medium": "tempera on poplar panel",
    "type": "painting"
  }
]

如果只是绘制数据,这样操作会很好。但对象数组对于构建我们的数据集来说并不理想。NGA 数据是关系型的,需要合并、清理和重构。DataFrame 更适合我们的用例,它提供了更高级、高效的 API,无需手动编写操作函数。

让我们看看如何使用 npm:nodejs-polars 中的 DataFrame 加载相同的数据集

笔记本 [2]
import * as pl from "npm:nodejs-polars@0.18.0";

let response = await fetch(new URL("objects.csv", baseUrl));

let objects: pl.DataFrame = pl.readCSV(await response.text(), {
  quoteChar: '"',
});

objects = objects.select(
  pl.col("objectid"),
  pl.col("title"),
  pl.col("beginyear").as("year"),
  pl.col("medium"),
  pl.col("visualbrowserclassification").as("type"),
);

objects.head();
objectid title year medium type
0 大圣雅各 1310 蛋彩画板 绘画
1 圣保罗与一群信徒 1333 蛋彩画板 绘画
4 圣伯纳德与亚历山大的圣凯瑟琳及受胎告知的圣母 1387 杨木板蛋彩画 绘画
17 马泰奥·奥利维耶里 (?) 1430 板上蛋彩画(和油画?)转移至画布 绘画
19 男子肖像 1450 蛋彩画板 绘画

在这里,我们像之前一样 fetch 数据集,但不是自己解析 CSV,而是直接使用 Polars 读取文本响应来创建 pl.DataFrame

然后我们链式调用 .select 表达式来选择和重命名我们想要的列。请注意,与在循环中对每一行进行操作不同,使用 Polars,我们直接处理。这个 API 允许我们表达复杂的、高效的操作(用 Rust 实现),而无需具体化 JavaScript 对象。

我们也把另外两个数据集加载为 pl.DataFrames

constituents.csv 表包含与艺术品相关的任何人或实体的信息,例如艺术家、策展人或收藏家。

笔记本 [3]
let response = await fetch(new URL("constituents.csv", baseUrl));
let constituents = pl.readCSV(await response.text(), { quoteChar: '"' }).select(
  "constituentid",
  pl.col("forwarddisplayname").alias("name"),
  pl.col("visualbrowsernationality").alias("nationality"),
);

constituents.head();
constituentid name nationality
9 匿名 其他
11 汉斯·巴尔东 其他
12 美国国家艺术馆 其他
13 匿名艺术家 其他
14 托马斯·M·埃文斯夫人 美国人

published_images.csv 表包含有关艺术品图像的额外信息,包括缩略图的 URL,以及将 objectid 映射到图像的 IIIF(国际图像互操作框架)

笔记本 [4]
let response = await fetch(new URL("published_images.csv", baseUrl));
let publishedImages = pl.readCSV(await response.text(), { quoteChar: '"' })
  .select(
    pl.col("depictstmsobjectid").alias("objectid"),
    pl.col("uuid"),
    pl.col("iiifthumburl").alias("thumburl"),
  );
publishedImages.head();
objectid uuid thumburl
17387 00007f61-4922-417b-8f27-893ea328206c Link
19245 0000bd8c-39de-4453-b55d-5e28a9beed38 Link
23830 0001668a-dd1c-48e8-9267-b6d1697d43c8 Link
713 00032658-8a7a-44e3-8bb8-df8c172f521d Link
71457 0003d4e4-d7fd-4835-8d27-1e9e20672e39 Link

最终的表 objects_constituents.csv 表示艺术品与与之相关的人或实体之间的多对多关系。每件艺术品可能链接到多个人。

在我们的分析中,我们关注选择“主要”组成部分(即艺术家)。我们将主要艺术家定义为每个对象中 displayorder 最高的艺术家。

笔记本 [5]
let response = await fetch(new URL("objects_constituents.csv", baseUrl));
let objectToArtist = pl.readCSV(await response.text(), { quoteChar: '"' })
  .filter(pl.col("role").eq(pl.lit("artist"))).sort({ by: "displayorder" })
  .groupBy("objectid").first().select("objectid", "constituentid");

objectToArtist.head();
objectid constituentid
36594 2414
22038 7787
14300 8095
30553 8333
98206 1401

这是 Polars 的更高级用法,让我们分解一下发生了什么

  • 筛选表,只选择角色为“artist”的行。
  • displayorder 排序筛选后的数据,确保所有条目都在顶部。
  • objectid 分组行,确保每件艺术品只有一行。
  • 获取每组中的第一个(即 displayorder 最高的艺术家)。
  • 选择 objectidconstituentid 以便连接我们的表。

在 JavaScript 中手动编写此逻辑会很繁琐,但使用 Polars,它是声明式的,读起来就像一句话。这种链式操作在处理关系数据时很常见。

确定艺术品是否属于公共领域

我们已经从开放访问数据集中加载了相关表格,但它们没有表明与艺术品相关的图像是否属于公共领域。虽然 CC0 图像可以自由下载、共享和再利用,但并非所有具有 CC0 许可的艺术品都因当地版权法和特殊情况而在各地合法属于公共领域。

让我们找一种方法,将公共领域信息添加到我们的数据集中。

该信息可在 NGA 网站上获取,但只能通过搜索用户界面。手动提取它是不切实际的,因此我逆向工程了一个 API 调用,以检索具有公共领域图像的艺术品 ID。

我们只需要 ID,而不是完整数据,但查询很慢。由于这不是官方 API,我已单独保存了结果。

我们的数据集中大约有 5 万个 ID 标识公共领域的图像。

笔记本 [6]
// This is not an official API, so I’ve cached the results to avoid repeatedly
querying the server. // // let response = await fetch( //
"https://www.nga.gov/bin/ngaweb/collection-search-result/search.pageSize__100000.pageNumber__1.lastFacet__artobj_downloadable.json?artobj_downloadable=Image_download_available",
// ); // let data = await response.json(); // Deno.writeTextFileSync( //
"public-domain-ids.txt", // data.results.map((object) => object.id).join("\n"),
// );

let publicDomainIds = Deno.readTextFileSync("public-domain-ids.txt")
.split("\n") .map((d) => +d);

连接到单个统一表

现在我们已经清理并加载了所有数据,我们将执行一次大型连接,将所有这些表合并到一个统一的表中。

同样,这手动操作很具挑战性,但使用 Polars,我们可以很好地表达这些复杂的连接操作。最后,我们使用 .isIn 表达式派生一个新列,指示艺术品是否属于公共领域。

笔记本 [7]
let df = objects.join(objectToArtist, { on: "objectid" }).join(constituents, {
  on: "constituentid",
}).join(publishedImages, { on: "objectid" })
  .select(pl.exclude("constituentid"))
  .withColumns(pl.col("objectid").isIn(publicDomainIds).alias("public")).sort({
    by: "public",
  }).sort({ by: "year", descending: true, nullsLast: true });

df.head();
objectid title year medium type name nationality uuid thumburl public
227933 霍洛的拨浪鼓,雅兹 1z [1-2] + [jaatłoh4Ye’iitsoh] 2023 编织纤维 雕塑 埃里克-保罗·里格 美国人 33b66d45-07d2-44f5-82bf-59f0b16100fd thumbnail
227704 1985年的17天 2023 画布上的丙烯、织物、拼贴画和现成物品 绘画 辛尼克·史密斯 美国人 52643267-d643-4fac-a64c-3931a46f1eaa thumbnail
228278 帐篷相机图像:法国维泰伊附近的罂粟田 #2 2023 喷墨打印 照片 阿贝拉多·莫雷尔 美国人 f3747d3e-aebc-484c-8924-cc2ff3b52f45 thumbnail
228279 帐篷相机图像:法国阿尔勒附近的日落 2023 喷墨打印 照片 阿贝拉多·莫雷尔 美国人 fe2997e3-7472-42b7-94d2-d12b566fc223 thumbnail
228360 魔镜,魔镜 2023 手工纸上的 Mixografía® 版画 版画 艾莉森·萨尔 美国人 a734f694-3f8a-4681-92c9-4e5484b80cef thumbnail

我们的统一数据集终于加载完毕,接下来进行一些绘图操作。

通过绘图探索艺术品、创作者、创作时间等

NGA 门户网站允许对这些数据进行一些探索,但主要关注特定的艺术品。我们更感兴趣的是更广泛的洞察——大多数艺术品来自哪里、由谁创作、何时创作以及是否属于公共领域。

作为 JavaScript/TypeScript 运行时,Deno 使我们能够访问许多可视化库来提出这类问题。我们将使用 npm:@observablehq/plot (Observable Plot) 来创建绘图、发现模式并更好地理解藏品。

请注意,本文不是 Observable Plot 的教程,因此如果下面的某些代码不立即清晰,请不要担心。目标是演示如何在 Jupyter 中使用 Deno 和 Observable Plot 从我们的数据中提取洞察。我将在过程中强调任何 Deno 特定的细节或有用的提示。

让我们首先检查 NGA 藏品中不同“类型”艺术品的分布情况。

Notebook [8]
import * as Plot from "npm:@observablehq/plot";
import { document } from "jsr:@manzt/jupyter-helper";

// Convert our DataFrame to Array<Object>
let records = df.toRecords();

Plot.plot({
  width: 900,
  marginLeft: 50,
  color: { legend: true },
  marks: [
    Plot.barY(
      records,
      Plot.groupX({ y: "count" }, {
        x: "type",
        sort: { x: "-y" },
        fill: (d) => d.public ? "Public domain" : "Copyrighted",
      }),
    ),
  ],
  // Provide a custom `document`
  document,
});

对于上面的代码,需要记住的关键点是

  • 自定义文档 – Observable Plot 依赖于浏览器 DOM,但由于我们处于 Deno 环境中,我们从 jsr:@manzt/jupyter-helper 提供了一个自定义文档以启用渲染。
  • 将 Polars DataFrame 转换为记录 – Observable Plot 最适合处理对象数组,因此我们使用 df.toRecords() 来使数据更易于处理。

有关 Plot API 的具体信息,请参阅可用文档示例

Plot of copyright vs. public artwork by medium

我们立刻可以看到,版画、素描和照片构成了大部分藏品。每个类别中属于公共领域的图像数量各不相同——版画、素描、雕塑和绘画的图像大多属于公共领域,而照片和作品集的图像则大部分受版权保护。

使用 Observable Plot,我们可以非常容易地在绘图中获得更精细的粒度,它是可组合且富有表现力的。只需进行少量调整,您就可以完全改变数据的表示方式。例如,从上面的绘图开始,我们可以修改 marks 和一些编码字段,并生成一个完全不同的图表,从而实现快速探索和迭代——就像这种“华夫格”条形图一样。

Notebook [9]
Plot.plot({
  width: 900,
  marginLeft: 50,
  color: { legend: true },
  marks: [
    Plot.waffleY(
      /* changed, Plot.barY */
      records,
      Plot.groupZ(
        /* changed, Plot.groupX */
        { y: "count" },
        {
          fx: "type",
          fill: (d) => d.public ? "Public Domain" : "Copyrighted",
          sort: { fx: "-y" }, /* changed, sort: { x: "-y"  } */
          unit: 300, /* new */
        },
      ),
    ),
    Plot.ruleY([0]),
  ],
  document,
});

Waffle chart plot of copyright vs. public artwork by medium

结合 Polars 和 Plot,我们可以提出更具体的数据问题,例如“哪些艺术家的作品在藏品中最多?”。在这里,我们首先使用 Polars 按艺术家(name)和艺术品是否属于公共领域(public)对数据进行分组,并计算每种情况下的作品数量。然后,我们使用 Plot 可视化排名前 25 位的艺术家。

Notebook [10]
const artworkTotals = df
  .groupBy("name", "public")
  .len()
  .sort("name_count", true)
  .head(25)
  .toRecords();

Plot.plot({
  marginLeft: 200,
  color: { legend: true },
  marks: [
    Plot.barX(artworkTotals, {
      x: "name_count",
      y: "name",
      sort: { y: "-x" },
      fill: (d) => d.public ? "Public domain" : "Copyrighted",
    }),
  ],
  document,
});

Copyright vs. public domain artwork by artist

同样,我们可以从这张图中得出一些高级别的结论:摄影师 罗伯特·弗兰克 的作品数量几乎是下一位最高艺术家的两倍。某些条目,例如“美国20世纪”和“德国15世纪”,代表的是群体而非个体。此外,艺术家的作品通常要么完全属于公共领域,要么完全不属于——这可能是因为公共领域状态是在藏品级别确定的,机构会一次性清理由整批作品,而不是单独评估每件作品。

有了我们整洁干净的数据集,我们可以在 Deno 中快速迭代,提出广泛的问题(“这个藏品里到底有什么?”)和非常具体的问题(“谁在特定年份创作了这件艺术品?”)。

通过交互性进行更深入的探索

到目前为止,我们已经使用静态绘图来探索数据,但 Jupyter Notebook 支持使用 HTML、CSS 和 JavaScript 生成丰富的交互式输出。

借助 Deno,我们可以通过使用现成的交互式 anywidgets 或快速构建自定义组件来可视化预构建工具不支持的子集,从而为数据探索增加更多交互性。Deno 提供了对整个 Web 生态系统的直接访问,使自定义可视化无缝衔接。

在本节中,我们将探讨

  • 用于数据探索的预构建交互式表格 anywidgets
  • 创建自定义 <Gallery /> 组件以可视化数据集子集

使用 DataFrame 查看器 anywidgets 探索数据

在 Jupyter 中,“小部件”通过交互式视图和对内核(即后端)中对象的控制来扩展 Notebook 输出。与标准输出不同,它们由内核端和前端代码组成,通过自定义消息直接通信。虽然 Jupyter 小部件生态系统主要以 Python 为中心,但 anywidget 项目 提供了一种与内核无关的方式来创建和共享这些组件。

使用 anywidget,小部件可以发布到 JSR 并在 Deno Jupyter 内核中使用。例如,jsr:@manzt/jupyter-helper 提供了一些现成的 anywidgets,用于交互式查看 Polars 数据帧。

Notebook [11]
import { agGrid } from "jsr:@manzt/jupyter-helper";

agGrid(df.head(200)); // just look at the first 200 items

agGrid 导出使用流行的 AG Grid 库渲染 pl.DataFrame,支持交互式排序、过滤和分页。此默认视图虽然简约,但可通过 AG Grid 的许多功能轻松扩展。

Notebook [12]
import { quak } from "jsr:@manzt/jupyter-helper";

const paintings = df.filter(pl.col("type").eq(pl.lit("painting")));
quak(paintings);

quak 导出使用 quak 数据表查看器渲染 pl.DataFrame。与 agGrid 类似,它支持排序,但还在每列上方提供了摘要可视化,显示一维分布。这些可视化是交互式的,允许进行交叉过滤。例如,选择特定年份创作的公共领域绘画。

Deno 提供了对 Web 生态系统的直接访问,使得构建小型、实用的 UI 以探索现成工具不支持的数据方式变得容易。

例如,在“画廊视图”中实际查看一组艺术品将有助于直观了解该子集中的内容。由于数据集包含图像 URL,我们可以做到这一点。

让我们创建一个自定义 JSX 组件,它使用 jsr:@manzt/jupyter-helper 中的 render 导出,为任何数据集子集进行服务器端渲染 (SSR) 画廊视图。此辅助函数仅将 JSX 转换为 HTML 字符串,然后我们使用 Deno.jupyter.Displayable 接口来显示它。

Notebook [13]
import * as React from "npm:react";
import { render } from "jsr:@manzt/jupyter-helper";

function Gallery({ objects, size = 100 }) {
  return (
    <div
      style={{
        display: "grid",
        gridTemplateColumns: `repeat(auto-fill, minmax(${size}px, 1fr))`,
        gap: "4px",
      }}
    >
      {objects.select("objectid", "thumburl", "title", "public")
        .map(([objectid, thumburl, title, publicDomain]) => (
          <div
            key={objectid}
            style={{ position: "relative", textAlign: "center" }}
          >
            <a
              href={`https://www.nga.gov/collection/art-object-page.${objectid}.html`}
              style={{
                display: "block",
                width: `${size}px`,
                height: `${size}px`,
                position: "relative",
              }}
            >
              <img
                src={thumburl}
                alt={title}
                style={{
                  width: "100%",
                  height: "100%",
                  objectFit: "cover",
                  borderRadius: "5px",
                }}
              />{" "}
              {publicDomain && (
                <img
                  src="https://mirrors.creativecommons.org/presskit/icons/zero.svg"
                  alt="Public Domain"
                  style={{
                    position: "absolute",
                    bottom: "3px",
                    right: "3px",
                    width: "20px",
                    height: "20px",
                    opacity: 0.60,
                  }}
                />
              )}
            </a>
          </div>
        ))}
    </div>
  );
}

Gallery 是一个自定义组件,用于显示给定数据集中艺术品缩略图的网格。每张图片都链接到其 NGA 收藏页面,公共领域作品则标有 CC0 图标。

我们可以使用我们的数据对 Gallery 进行 SSR,以更好地理解一个较小的子集,从而更容易地可视化探索特定艺术家、时间段或类别。例如,从所有藏品中随机抽取样本

Notebook [14]
render(<Gallery objects={df.sample(20)} />);

Render of art in a table

或仅绘画样本

Notebook [15]
render(<Gallery objects={paintings.sample(20)} />);

Render of painting in a table

值得花一点时间来欣赏,我们仅用几行代码就构建了一个领域特定的实用工具,使我们的数据探索更加直观。这种灵活性是 Deno 强大且独有的,让我们能够快速制作提供更深入洞察的自定义实用工具。

有了我们的工具包,让我们更仔细地研究 NGA 数据集

深入探究艺术品的创作时间

到目前为止,我们的数据探索主要集中在使用静态绘图对 NGA 藏品按艺术家和艺术品类型进行高级别汇总。现在,让我们使用我们完整的工具包深入研究一些可能更有趣的东西:这些作品的创作时间。

我们将从堆叠直方图开始,查看艺术品类型随时间的总体分布,然后按公共领域状态进行分面,以揭示任何模式。

Notebook [16]
Plot.plot({
  y: { grid: true },
  color: { legend: true },
  marks: [
    Plot.rectY(
      records.filter((r) => r.year > 1401), // 15th century or later
      Plot.binX({ y: "count" }, {
        x: (d) => new Date(d.year, 0, 1),
        fill: "type",
        fy: (d) => d.public ? "Public Domain" : "Copyrighted",
      }),
    ),
    Plot.ruleY([0]),
  ],
  marginLeft: 100,
  marginRight: 100,
  width: 1000,
  height: 400,
  document,
});

Copyright vs. public art overtime by medium

受版权保护和公共领域的艺术品图像分布明显不同。几乎所有受版权保护的作品都出现在 1850 年之后,而公共领域的艺术品则随时间分布更均匀——除了 1940 年代公共领域素描出现急剧飙升,新增了超过 1.5 万件艺术品。

这是一个引人注目的异常。它们都来自同一位艺术家吗?为什么只有素描?让我们仔细看看。

我们将公共领域数据过滤到这个时间范围。

Notebook [17]
let notablePeriod = df.filter("public") // just public domain
  .filter(pl.col("year").gt(pl.lit(1925))) // between 1925 - 1955
  .filter(pl.col("year").lt(pl.lit(1955))).sort({ by: "year" });

Plot.plot({
  y: { grid: true },
  color: { legend: true },
  marks: [Plot.rectY(
    notablePeriod.toRecords(),
    Plot.binX({ y: "count" }, {
      x: (d) => new Date(d.year, 0, 1),
      fill: "type",
    }),
  )],
  document,
});

Public artwork by time

将视角放大到 1925-1955 年的范围,我们看到几乎所有数据都落在更窄的时间窗口内。有趣的是,高峰期高度集中在 1935 年到 1942 年之间,这段时期之外的艺术品非常少。

再缩小范围,我们可以看到在这个时间窗口内有大约 1.8 万件独立素描作品

Notebook [18]
const totalNumberOfDrawings = notablePeriod
  .filter(pl.col("year").gt(pl.lit(1934)))
  .filter(pl.col("year").lt(pl.lit(1943)))
  .shape
  .height;

totalNumberOfDrawings;

由超过 1000 名个体创作

单元格输出
18096
const numberOfArtists = notablePeriod
  .filter(pl.col("year").gt(pl.lit(1934)))
  .filter(pl.col("year").lt(pl.lit(1943)))
  .groupBy("name")
  .len()
  .shape
  .height;

numberOfArtists;
单元格输出
1034

这变得更有趣了!这些艺术品并非只来自一个特定来源——在 1935 年至 1942 年间,公共作品出现了一股热潮。

让我们使用自定义的 <Gallery /> 视图来探索这些作品的一个子集——也许看到它们会给我们一些启发。

Notebook [19]
render(<Gallery objects={notablePeriod.sample(100)} />);

100 samples of public paintings

因此,所有这些艺术品,尽管来自不同的个体,却具有非常相似的风格和媒介。这值得深入研究——什么可以解释这种模式?

感谢我们自定义的 <Gallery /> 组件,我们可以点击艺术品并查看它们的元数据。随机抽取几幅,我们发现它们都属于 “美国设计索引” 系列藏品。

“美国设计索引”旨在记录和保存美国民间艺术和装饰艺术,包含 18,257 幅水彩画,这些作品是 1935 年至 1942 年间作为联邦艺术项目 (FAP) 救济计划的一部分而创作的。大约 400 名艺术家精心再现了来自美国各地的纺织品、木雕、风向标和其他物品,旨在建立独特的美国视觉传承。该项目最终收藏于美国国家艺术馆,成为一个广泛展出的视觉档案。

就是这样!我们看到的是大萧条期间由联邦项目资助的公共作品集合——这在数据和历史背景中都清晰可见。

总结

利用 Deno 的 Jupyter 内核,我们探索了 NGA 开放访问数据集,结合静态绘图、交互式小部件和自定义 JSX 组件来揭示数据中的模式。这使我们获得了一个历史性的洞察——20世纪30年代的公共艺术项目“美国设计索引”。

借助 Deno、JSX 和 anywidget,我们无缝地将数据分析与基于网络的视觉化连接起来,从而轻松构建用于深入探索的领域专用工具。