# 是时候来学一下切片了 [[是时候来学一下切片了]]

<CourseFloatingBanner chapter={5}
  classNames="absolute z-10 right-0 top-0"
  notebooks={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/zh-CN/chapter5/section3.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/zh-CN/chapter5/section3.ipynb"},
]} />

大多数情况下，您使用的数据都需根据模型所要求的输入进行清洗。在本节中，我们将探索 🤗 Datasets 提供的用于数据集清洗的各种功能。

<Youtube id="tqfSFcPMgOI"/>

## 切片与切分我们的数据 [[切片与切分我们的数据]]

与 Pandas 类似，🤗 Datasets 提供了几个函数来操作 **Dataset** 和 **DatasetDict** 对象。我们在[第三章](/course/chapter3)已经遇到了 **Dataset.map()** 方法，在本节中，我们将探索我们可以使用的其他功能。

对于这个例子，我们将使用托管在[加州大学欧文分校机器学习存储库](https://archive.ics.uci.edu/ml/index.php)的[药物审查数据集](https://archive.ics.uci.edu/ml/datasets/Drug+Review+Dataset+%28Drugs.com%29)，其中包含患者对各种药物的评论，以及正在治疗的病情和患者满意度的 10 星评级。

首先我们需要下载并提取数据，这可以通过 **wget** 和 **unzip** 命令：

```py
!wget "https://archive.ics.uci.edu/ml/machine-learning-databases/00462/drugsCom_raw.zip"
!unzip drugsCom_raw.zip
```

由于 TSV 只是使用制表符而不是逗号作为分隔符的 CSV 变体，我们可以使用加载**csv**文件的**load_dataset()**函数并指定分隔符 示例如下：

```py
from datasets import load_dataset

data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
# \t is the tab character in Python
drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")
```

在进行任何类型的数据分析时，一个好的做法是抽取一个小的随机样本，以快速了解您正在处理的数据类型。在🤗数据集中，我们可以通过链接 **Dataset.shuffle()** 和 **Dataset.select()** 共同来完成抽取：

```py
drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000))
# Peek at the first few examples
drug_sample[:3]
```

```python out
{'Unnamed: 0': [87571, 178045, 80482],
 'drugName': ['Naproxen', 'Duloxetine', 'Mobic'],
 'condition': ['Gout, Acute', 'ibromyalgia', 'Inflammatory Conditions'],
 'review': ['"like the previous person mention, I&#039;m a strong believer of aleve, it works faster for my gout than the prescription meds I take. No more going to the doctor for refills.....Aleve works!"',
  '"I have taken Cymbalta for about a year and a half for fibromyalgia pain. It is great\r\nas a pain reducer and an anti-depressant, however, the side effects outweighed \r\nany benefit I got from it. I had trouble with restlessness, being tired constantly,\r\ndizziness, dry mouth, numbness and tingling in my feet, and horrible sweating. I am\r\nbeing weaned off of it now. Went from 60 mg to 30mg and now to 15 mg. I will be\r\noff completely in about a week. The fibro pain is coming back, but I would rather deal with it than the side effects."',
  '"I have been taking Mobic for over a year with no side effects other than an elevated blood pressure.  I had severe knee and ankle pain which completely went away after taking Mobic.  I attempted to stop the medication however pain returned after a few days."'],
 'rating': [9.0, 3.0, 10.0],
 'date': ['September 2, 2015', 'November 7, 2011', 'June 5, 2013'],
 'usefulCount': [36, 13, 128]}
```

请注意，出于可以复现的目的，我们已将在**Dataset.shuffle()**选取了固定的随机数种子。 **Dataset.select()** 需要一个可迭代的索引，所以我们已经通过了 **range(1000)** 从随机打乱的数据集中选取前 1,000 个示例。从抽取的数据中，我们已经可以看到我们数据集的一些特点：

* **Unnamed: 0**这列看起来很像每个患者的匿名 ID。
* **condition** 这列包含有描述健康状况的标签。
* 评论长短不一，混合有 Python 行分隔符 (**\r\n**) 以及 HTML 字符代码，如** &\#039;**。
  
让我们看看我们如何使用 🤗 Datasets 来处理这些问题。为了验证**Unnamed: 0** 列存储的是患者 ID的猜想，我们可以使用 **Dataset.unique()** 函数来验证匿名ID 的数量是否与拆分后每部分中的行数匹配：

```py
for split in drug_dataset.keys():
    assert len(drug_dataset[split]) == len(drug_dataset[split].unique("Unnamed: 0"))
```

这似乎证实了我们的假设，所以让我们把 **Unnamed: 0** 列重命名为患者的id。我们可以使用 **DatasetDict.rename_column()**函数一次性重命名DatasetDict中共有的列：

```py
drug_dataset = drug_dataset.rename_column(
    original_column_name="Unnamed: 0", new_column_name="patient_id"
)
drug_dataset
```

```python out
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
        num_rows: 161297
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
        num_rows: 53766
    })
})
```

<Tip>

✏️ **试试看！** 使用 `Dataset.unique()` 函数查找训练和测试集中满足某个条件的药物经过去重之后的数量。

</Tip>

接下来，让我们使用 **Dataset.map()**标准化所有 **condition** 标签 .正如我们在[第三章](/course/chapter3)中所做的那样，我们可以定义一个简单的函数，可以将该函数应用于**drug_dataset** 拆分后每部分的所有行：

```py
def lowercase_condition(example):
    return {"condition": example["condition"].lower()}


drug_dataset.map(lowercase_condition)
```

```python out
AttributeError: 'NoneType' object has no attribute 'lower'
```

哦不，我们的map功能遇到了问题！从错误中我们可以推断出 **condition** 列存在 **None** , 不能转换为小写，因为它们不是字符串。让我们使用 **Dataset.filter()** 删除这些行 ，其工作方式类似于 **Dataset.map()** 。例如：

```py
def filter_nones(x):
    return x["condition"] is not None
```

然后运行 **drug_dataset.filter(filter_nones)** ，我们可以在一行中使用lambda 函数.在 Python 中，lambda 函数是您无需明确命名即可使用的微函数（匿名函数）。它们一般采用如下形式：

```
lambda <arguments> : <expression>
```

其中**lambda** 是 Python 的特殊[关键字](https://docs.python.org/3/reference/lexical_analysis.html#keywords), **arguments** 是以逗号进行分隔的函数输入的列表/集合， **expression** 代表您希望执行的操作。例如，我们可以定义一个简单的 lambda 函数来对一个数字进行平方，如下所示：

```
lambda x : x * x
```

我们需要将要输入给这个函数值括在括号中：

```py
(lambda x: x * x)(3)
```

```python out
9
```

类似地，我们可以通过用逗号分隔多个参数来定义 lambda 函数。例如，我们可以按如下方式计算三角形的面积：

```py
(lambda base, height: 0.5 * base * height)(4, 8)
```

```python out
16.0
```

当您想定义小型、一次性使用的函数时，Lambda 函数非常方便（有关它们的更多信息，我们建议阅读安德烈·布尔高写的[真正的Python教程](https://realpython.com/python-lambda/)）。在🤗 Datasets 中，我们可以使用 lambda 函数来定义简单的映射和过滤操作，所以让我们使用这个技巧来消除我们数据集中的 **None** 条目：

```py
drug_dataset = drug_dataset.filter(lambda x: x["condition"] is not None)
```

当 **None** 条目已删除，我们可以标准化我们的 **condition** 列：

```py
drug_dataset = drug_dataset.map(lowercase_condition)
# Check that lowercasing worked
drug_dataset["train"]["condition"][:3]
```

```python out
['left ventricular dysfunction', 'adhd', 'birth control']
```

有用！现在我们已经清理了标签，让我们来看看清洗后的评论文本。

## 创建新的数据列 [[创建新的数据列]]

每当您处理客户评论时，一个好的做法是检查每个评论中的字数。评论可能只是一个词，比如“太棒了！”或包含数千字的完整文章，根据实际的情况，您需要以不同的方式处理这些极端情况。为了计算每条评论中的单词数，我们将使用基于空格分割每个文本的粗略方法。

让我们定义一个简单的函数来计算每条评论中的单词数：

```py
def compute_review_length(example):
    return {"review_length": len(example["review"].split())}
```

与我们的 `lowercase_condition()` 函数不同，`compute_review_length()` 返回一个字典，其键与数据集中的列名之一不对应。 在这种情况下，当 `compute_review_length()` 传递给 `Dataset.map()` 时，它将应用于数据集中的所有行以创建新的 `review_length` 列：

```py
drug_dataset = drug_dataset.map(compute_review_length)
# Inspect the first training example
drug_dataset["train"][0]
```

```python out
{'patient_id': 206461,
 'drugName': 'Valsartan',
 'condition': 'left ventricular dysfunction',
 'review': '"It has no side effect, I take it in combination of Bystolic 5 Mg and Fish Oil"',
 'rating': 9.0,
 'date': 'May 20, 2012',
 'usefulCount': 27,
 'review_length': 17}
```

正如预期的那样，我们可以看到一个 **review_length** 列已添加到我们的训练集中。我们可以使用 **Dataset.sort()**对这个新列进行排序，然后查看极端长度的评论的样子：

```py
drug_dataset["train"].sort("review_length")[:3]
```

```python out
{'patient_id': [103488, 23627, 20558],
 'drugName': ['Loestrin 21 1 / 20', 'Chlorzoxazone', 'Nucynta'],
 'condition': ['birth control', 'muscle spasm', 'pain'],
 'review': ['"Excellent."', '"useless"', '"ok"'],
 'rating': [10.0, 1.0, 6.0],
 'date': ['November 4, 2008', 'March 24, 2017', 'August 20, 2016'],
 'usefulCount': [5, 2, 10],
 'review_length': [1, 1, 1]}
```

正如我们所猜想的那样，一些评论只包含一个词，虽然这对于情感分析来说可能没问题，但如果我们想要预测病情，这些评论可能并不适合。

<Tip>

🙋向数据集添加新列的另一种方法是使用函数Dataset.add_column() 。这允许您输入Python 列表或 NumPy，在不适合使用Dataset.map()情况下可以很方便。

</Tip>

让我们使用 **Dataset.filter()** 功能来删除包含少于 30 个单词的评论。与我们对 **condition** 列的处理相似，我们可以通过选取评论的长度高于此阈值来过滤掉非常短的评论：

```py
drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30)
print(drug_dataset.num_rows)
```

```python out
{'train': 138514, 'test': 46108}
```

如您所见，这已经从我们的原始训练和测试集中删除了大约 15% 的评论。

<Tip>

✏️ 试试看！使用 Dataset.sort() 函数查看单词数最多的评论。请参阅文档以了解您需要使用哪个参数按长度降序对评论进行排序。

</Tip>

我们需要处理的最后一件事是评论中是否存在 HTML 字符代码。我们可以使用 Python 的**html**模块取消这些字符的转义，如下所示：

```py
import html

text = "I&#039;m a transformer called BERT"
html.unescape(text)
```

```python out
"I'm a transformer called BERT"
```

我们将使用 **Dataset.map()** 对我们语料库中的所有 HTML 字符进行转义：

```python
drug_dataset = drug_dataset.map(lambda x: {"review": html.unescape(x["review"])})
```

如您所见， **Dataset.map()** 方法对于处理数据非常有用——在示例中仅仅是浅尝辄止就有很大的收获！

## map() 方法的超级加速 [[map() 方法的超级加速]]

**Dataset.map()** 方法有一个 **batched** 参数，如果设置为 **True** , map 函数将会分批执行所需要进行的操作（批量大小是可配置的，但默认为 1,000）。例如，之前对所有 HTML 进行转义的 map 函数运行需要一些时间（您可以从进度条中读取所用时间）。我们可以通过使用列表推导同时处理多个元素来加快速度。

当您在使用 **Dataset.map()**函数时指定 **batched=True**。该函数会接收一个包含数据集字段的字典，每个值都是一个列表，而不仅仅是单个值。**Dataset.map()** 的返回值应该是相同的：一个包含我们想要更新或添加到数据集中的字段的字典，字典的键是要添加的字段，字典的值是结果的列表。例如，这是使用 **batched=True**对所有 HTML 字符进行转义的方法 ：

```python
new_drug_dataset = drug_dataset.map(
    lambda x: {"review": [html.unescape(o) for o in x["review"]]}, batched=True
)
```

如果您在笔记本中运行此代码，您会看到此命令的执行速度比前一个命令快得多。这不是因为我们的评论已经是处理过的——如果你重新执行上一节的指令（没有 **batched=True** )，它将花费与以前相同的时间。这是因为列表推导式通常比在同一代码中用 **for** 循环执行相同的代码更快，并且我们还通过同时访问多个元素而不是一个一个来处理来提高处理的速度。

在[第六章](/course/chapter6)我们将遇到的“快速”标记器，它可以快速标记大文本列表。使用 **Dataset.map()** 和 **batched=True** 是加速的关键。例如，要使用快速标记器标记所有药物评论，我们可以使用这样的函数：

```python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")


def tokenize_function(examples):
    return tokenizer(examples["review"], truncation=True)
```

正如你在[第三章](/course/chapter3)所看到的，我们原本就可以将一个或多个示例传递给分词器，因此在**batched=True**是一个非必须的选项.让我们借此机会比较不同选项的性能。在笔记本中，您可以在您要测量的代码行之前添加 **%time**来测试改行运行所消耗的时间：

```python no-format
%time tokenized_dataset = drug_dataset.map(tokenize_function, batched=True)
```

您还可以通过将整个单元格计时 **%%time** 在单元格的开头。在我们执行此操作的硬件上，该指令显示 10.8 秒（这是写在“Wall time”之后的数字）。

<Tip>

✏️ **试试看！** 使用和不使用 `batched=True` 执行相同的指令，然后使用慢速标记器尝试（在 `AutoTokenizer.from_pretrained()` 方法中添加 `use_fast=False`），这样你就可以看看在你的电脑上它需要多长的时间。

</Tip>

以下是我们在使用和不使用批处理时使用快速和慢速分词器获得的结果：

Options         | Fast tokenizer | Slow tokenizer
:--------------:|:--------------:|:-------------:
`batched=True`  | 10.8s          | 4min41s
`batched=False` | 59.2s          | 5min3s

这意味着使用带有 **batched=True** 选项比没有批处理的慢选项快 30 倍——这真是太棒了！这就是为什么**AutoTokenizer** 的默认设置是**use_fast=True**的主要原因 （以及为什么它们被称为“快速”）。他们能够实现这样的加速，因为在底层的标记化代码是在 Rust 中执行的，Rust 是一种可以轻松并行化执行的语言。

并行化也是快速标记器通过批处理实现近 6 倍加速的原因：单个标记化操作是不能并行的，但是当您想同时标记大量文本时，您可以将执行拆分为多个进程，每个进程都对自己的文本负责。

**Dataset.map()** 也有一些自己的并行化能力。由于它们不受 Rust 的支持，因此慢速分词器的速度赶不上快速分词器，但它们仍然会更快一些（尤其是当您使用没有快速版本的分词器时）。要启用多处理，请在**Dataset.map()**时使用 **num_proc** 参数并指定要在调用中使用的进程数 ：

```py
slow_tokenizer = AutoTokenizer.from_pretrained("bert-base-cased", use_fast=False)


def slow_tokenize_function(examples):
    return slow_tokenizer(examples["review"], truncation=True)


tokenized_dataset = drug_dataset.map(slow_tokenize_function, batched=True, num_proc=8)
```

您可以对处理的时间进行一些试验，以确定要使用的最佳进程数；在我们的例子中，8 似乎产生了最好的速度增益。以下是我们在使用和不使用多处理时所需要的时间：

Options         | Fast tokenizer | Slow tokenizer
:--------------:|:--------------:|:-------------:
`batched=True`  | 10.8s          | 4min41s
`batched=False` | 59.2s          | 5min3s
`batched=True`, `num_proc=8`  | 6.52s          | 41.3s
`batched=False`, `num_proc=8` | 9.49s          | 45.2s

对于慢速分词器来说，这些结果要合理得多，但快速分词器的性能也得到了显着提高。但是请注意，情况并非总是如此——除了 **num_proc=8**，我们的测试表明，使用**batched=True**而不带有**num_proc**参数的选项处理起来更快。通常，我们不建议将 Python 多线程处理用于具有**batched=True**功能的快速标记器  .

<Tip>

使用num_proc以加快处理速度通常是一个好主意，只要您使用的函数还没有自己带有的进行某种多进程处理的方法。

</Tip>

将所有这些功能浓缩到一个方法中已经非常了不起，但还有更多！使用 **Dataset.map()** 和 **batched=True** 您可以更改数据集中的元素数量。当你想从一个例子中创建几个训练特征时，这是非常有用的。我们将在[第七章](/course/chapter7).中进行的几个NLP任务的预处理中使用到这个功能，它非常便利。

<Tip>

💡在机器学习中，一个例子通常可以为我们的模型提供一组特征。在某些情况下，这些特征会储存在数据集的几个列，但在其他情况下（例如此处的例子和用于问答的数据），可以从单个示例的一列中提取多个特征

</Tip>

让我们来看看它是如何工作的！在这里，我们将标记化我们的示例并将最大截断长度设置128，但我们将要求标记器返回全部文本块，而不仅仅是第一个。这可以用 **return_overflowing_tokens=True** ：

```py
def tokenize_and_split(examples):
    return tokenizer(
        examples["review"],
        truncation=True,
        max_length=128,
        return_overflowing_tokens=True,
    )
```

在使用**Dataset.map()** 正式在整个数据集上开始处理之前让我们先在一个例子上测试一下：

```py
result = tokenize_and_split(drug_dataset["train"][0])
[len(inp) for inp in result["input_ids"]]
```

```python out
[128, 49]
```

瞧！我们在训练集中的第一个示例变成了两个特征，因为它被标记为超过我们指定的最大截断长度，因此结果被截成了两段：第一段长度为 128 ，第二段长度为 49 。现在让我们对所有元素执行此操作数据集！

```py
tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
```

```python out
ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000
```

不好了！它没有起作用！为什么呢？查看错误消息会给我们一个线索：列的长度不匹配，一列长度为 1,463，另一列长度为 1,000。1,000行的"review"给出了 1,463 行的新特征，导致和原本的1000行数据不匹配。

问题出在我们试图混合两个不同大小的不同数据集： **drug_dataset** 列将有一定数量的元素（我们错误中的 1,000），但是我们正在构建**tokenized_dataset** 将有更多的元素（错误消息中的 1,463）。这不适用于 **Dataset** ，因此我们需要从旧数据集中删除列或使它们的大小与新数据集中的大小相同。我们可以用 **remove_columns** 参数：

```py
tokenized_dataset = drug_dataset.map(
    tokenize_and_split, batched=True, remove_columns=drug_dataset["train"].column_names
)
```

现在这个过程没有错误。我们可以通过比较长度来检查新数据集的元素是否比原始数据集多得多：

```py
len(tokenized_dataset["train"]), len(drug_dataset["train"])
```

```python out
(206772, 138514)
```

我们提到我们还可以通过使旧列与新列的大小相同来处理长度不匹配的问题。为此，我们可以使用 **overflow_to_sample_mapping** 字段，当我们设置**return_overflowing_tokens=True** .它为我们提供了特征到它所产生的样本的映射。使用这个，我们可以将原始数据集中的每个键关联到一个合适大小的值列表中，通过遍历所有的数据来生成新特性:

```py
def tokenize_and_split(examples):
    result = tokenizer(
        examples["review"],
        truncation=True,
        max_length=128,
        return_overflowing_tokens=True,
    )
    # Extract mapping between new and old indices
    sample_map = result.pop("overflow_to_sample_mapping")
    for key, values in examples.items():
        result[key] = [values[i] for i in sample_map]
    return result
```

我们可以使用**Dataset.map()**来进行批处理，这样无需我们删除旧列：

```py
tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
tokenized_dataset
```

```python out
DatasetDict({
    train: Dataset({
        features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
        num_rows: 206772
    })
    test: Dataset({
        features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
        num_rows: 68876
    })
})
```

我们获得了与以前相同数量的训练特征，但在这里我们保留了所有旧字段。如果您在使用模型计算之后需要它们进行一些后处理，您可能需要使用这种方法。

您现在已经了解了 🤗 Datasets如何以各种方式用于预处理数据集。虽然🤗 Datasets 的处理功能会覆盖你大部分的模型训练需求，有时您可能需要切换到 Pandas 以使用更强大的功能，例如 **DataFrame.groupby()** 或用于可视化的高级 API。幸运的是，🤗 Datasets旨在与 Pandas、NumPy、PyTorch、TensorFlow 和 JAX 等库可以相互转换。让我们来看看这是如何工作的。

## `🤗 Datasets 和 DataFrames 的相互转换 [[`🤗 Datasets 和 DataFrames 的相互转换]]

<Youtube id="tfcY1067A5Q"/>

为了实现各种第三方库之间的转换，🤗 Datasets 提供了一个 **Dataset.set_format()** 功能。此功能可以通过仅更改输出格式的，轻松切换到另一种格式，而不会影响底层数据格式，即 Apache Arrow。格式化会在数据本身上进行。为了演示，让我们将数据集转换为 Pandas：

```py
drug_dataset.set_format("pandas")
```

现在，当我们访问数据集的元素时，我们会得到一个 **pandas.DataFrame** 而不是字典：

```py
drug_dataset["train"][:3]
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>patient_id</th>
      <th>drugName</th>
      <th>condition</th>
      <th>review</th>
      <th>rating</th>
      <th>date</th>
      <th>usefulCount</th>
      <th>review_length</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>95260</td>
      <td>Guanfacine</td>
      <td>adhd</td>
      <td>"My son is halfway through his fourth week of Intuniv..."</td>
      <td>8.0</td>
      <td>April 27, 2010</td>
      <td>192</td>
      <td>141</td>
    </tr>
    <tr>
      <th>1</th>
      <td>92703</td>
      <td>Lybrel</td>
      <td>birth control</td>
      <td>"I used to take another oral contraceptive, which had 21 pill cycle, and was very happy- very light periods, max 5 days, no other side effects..."</td>
      <td>5.0</td>
      <td>December 14, 2009</td>
      <td>17</td>
      <td>134</td>
    </tr>
    <tr>
      <th>2</th>
      <td>138000</td>
      <td>Ortho Evra</td>
      <td>birth control</td>
      <td>"This is my first time using any form of birth control..."</td>
      <td>8.0</td>
      <td>November 3, 2015</td>
      <td>10</td>
      <td>89</td>
    </tr>
  </tbody>
</table>

让我们创建一个 **pandas.DataFrame** 来选择 **drug_dataset[train]** 的所有元素：

```py
train_df = drug_dataset["train"][:]
```

<Tip>

🚨 在底层，`Dataset.set_format()` 改变了数据集的 `__getitem__()` dunder 方法的返回格式。 这意味着当我们想从 `"pandas"` 格式的 `Dataset` 中创建像 `train_df` 这样的新对象时，我们需要对整个数据集进行切片以获得 `pandas.DataFrame`。 无论输出格式如何，您都可以自己验证 `drug_dataset["train"]` 的类型依然还是 `Dataset`。

</Tip>


从这里我们可以使用我们想要的所有 Pandas 功能。例如，我们可以通过花式链接来计算 **condition**类之间的分布 ：

```py
frequencies = (
    train_df["condition"]
    .value_counts()
    .to_frame()
    .reset_index()
    .rename(columns={"index": "condition", "condition": "frequency"})
)
frequencies.head()
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>condition</th>
      <th>frequency</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>birth control</td>
      <td>27655</td>
    </tr>
    <tr>
      <th>1</th>
      <td>depression</td>
      <td>8023</td>
    </tr>
    <tr>
      <th>2</th>
      <td>acne</td>
      <td>5209</td>
    </tr>
    <tr>
      <th>3</th>
      <td>anxiety</td>
      <td>4991</td>
    </tr>
    <tr>
      <th>4</th>
      <td>pain</td>
      <td>4744</td>
    </tr>
  </tbody>
</table>


一旦我们完成了 Pandas 分析，我们总是通过使用对象 **Dataset.from_pandas()**方法可以创建一个新的 **Dataset** 如下：


```py
from datasets import Dataset

freq_dataset = Dataset.from_pandas(frequencies)
freq_dataset
```

```python out
Dataset({
    features: ['condition', 'frequency'],
    num_rows: 819
})
```

<Tip>

✏️ **试试看！** 计算每种药物的平均评级并将结果存储在一个新的Dataset.

</Tip>

我们对 🤗 Datasets中可用的各种预处理技术的介绍到此结束。在最后一部分，让我们创建一个验证集来准备用于训练分类器的数据集。在此之前，我们将输出格式 **drug_dataset** 从 **pandas**重置到 **arrow** ：

```python
drug_dataset.reset_format()
```

## 创建验证集 [[创建验证集]]

尽管我们有一个可以用于评估的测试集，但在开发过程中保持测试集不变并创建一个单独的验证集是一个很好的做法。一旦您对模型在测试集上的表现感到满意，您就可以对验证集进行最终的检查。此过程有助于降低您过拟合测试集并部署在现实世界数据上失败的模型的风险。

🤗 Datasets提供了一个基于**scikit-learn**的经典方法**Dataset.train_test_split()** .让我们用它把我们的训练集分成 **train** 和 **validation** （为了可以复现，我们将设置**seed**的值为一个常量）：

```py
drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
# Rename the default "test" split to "validation"
drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
# Add the "test" set to our `DatasetDict`
drug_dataset_clean["test"] = drug_dataset["test"]
drug_dataset_clean
```

```python out
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 110811
    })
    validation: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 27703
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 46108
    })
})
```

太好了，我们现在已经准备好了一个数据集，可以用来训练一些模型了！在[第五节]](/course/chapter5/5)我们将向您展示如何将数据集上传到 Hugging Face Hub，但现在让我们查看在本地计算机上保存数据集的几种方法。

## 保存数据集 [[保存数据集]]

<Youtube id="blF9uxYcKHo"/>

虽然 🤗 Datasets 会缓存每个下载的数据集和对它执行的操作，但有时你会想要将数据集保存到磁盘（例如，以防缓存被删除）。如下表所示，🤗 Datasets 提供了三个主要功能来以不同的格式保存您的数据集：

| 数据格式    |        对应的方法        |
| :---------: | :--------------------: |
|    Arrow    | `Dataset.save_to_disk()` |
|     CSV     |    `Dataset.to_csv()`    |
|    JSON     |   `Dataset.to_json()`    |

例如，让我们以 Arrow 格式保存我们清洗过的数据集：

```py
drug_dataset_clean.save_to_disk("drug-reviews")
```

这将创建一个具有以下结构的目录：

```
drug-reviews/
├── dataset_dict.json
├── test
│   ├── dataset.arrow
│   ├── dataset_info.json
│   └── state.json
├── train
│   ├── dataset.arrow
│   ├── dataset_info.json
│   ├── indices.arrow
│   └── state.json
└── validation
    ├── dataset.arrow
    ├── dataset_info.json
    ├── indices.arrow
    └── state.json
```

在那里我们可以看到每个部分.arrow表，以及一些元数据数据集信息.json和状态文件保存在一起.您可以将 Arrow 格式视为一个精美的列和行的表格，它针对构建处理和传输大型数据集的高性能应用程序进行了优化。

保存数据集后，我们可以使用 **load_from_disk()** 功能从磁盘读取数据如下：

```py
from datasets import load_from_disk

drug_dataset_reloaded = load_from_disk("drug-reviews")
drug_dataset_reloaded
```

```python out
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 110811
    })
    validation: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 27703
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 46108
    })
})
```

对于 CSV 和 JSON 格式，我们必须将每个部分存储为单独的文件。一种方法是迭代**DatasetDict**中的键和值 ：

```py
for split, dataset in drug_dataset_clean.items():
    dataset.to_json(f"drug-reviews-{split}.jsonl")
```

这将保存每个拆分都是[JSON的标准格式](https://jsonlines.org)，其中数据集中的每一行都存储为一行 JSON。这是第一个示例：

```py
!head -n 1 drug-reviews-train.jsonl
```

```python out
{"patient_id":141780,"drugName":"Escitalopram","condition":"depression","review":"\"I seemed to experience the regular side effects of LEXAPRO, insomnia, low sex drive, sleepiness during the day. I am taking it at night because my doctor said if it made me tired to take it at night. I assumed it would and started out taking it at night. Strange dreams, some pleasant. I was diagnosed with fibromyalgia. Seems to be helping with the pain. Have had anxiety and depression in my family, and have tried quite a few other medications that haven't worked. Only have been on it for two weeks but feel more positive in my mind, want to accomplish more in my life. Hopefully the side effects will dwindle away, worth it to stick with it from hearing others responses. Great medication.\"","rating":9.0,"date":"May 29, 2011","usefulCount":10,"review_length":125}
```

然后我们可以使用[第二节](/course/chapter5/2)学过的技术加载 JSON 文件如下：

```py
data_files = {
    "train": "drug-reviews-train.jsonl",
    "validation": "drug-reviews-validation.jsonl",
    "test": "drug-reviews-test.jsonl",
}
drug_dataset_reloaded = load_dataset("json", data_files=data_files)
```

这就是我们探索 🤗 Datasets 的旅程！现在我们有了一个清洗过的数据集，以下是您可以尝试的一些想法：

1. 使用[第3章](/course/chapter3)的技术来训练一个分类器，它可以根据药物评论预测病人的情况。
2. 使用 [Chapter 1](/course/chapter1) 中的“summarization”管道生成评论摘要。

接下来，我们将看看 🤗 Datasets如何使您能够在不撑爆笔记本电脑内存的情况下处理庞大的数据集！