一、说明
项目地址:https://github.com/hosseinmoein/DataFrame
使用教程:查看
DataFrame 是一个模板化的异构C++容器,专为统计、机器学习或金融应用程序的数据分析而设计。
- 一个数据帧可以有一个索引列和许多任何内置或用户定义类型的数据列
- 数据帧中的每一列最多可以与索引列一样长
- 数据帧中的列按创建顺序排列,可以按名称或索引访问它们。如果旋转列,其顺序会更改
- 若要访问任何操作的列,必须在编译时知道其名称(或索引)及其类型
- 要开始基本操作,请参阅 Hello World
- 数据帧具有同步和异步接口,后者返回C++ std::futures
- 在认真了解此库之前,请阅读下面的视图、访问者、多线程和内存对齐部分
二、数据类型
DataFrame 实例 公共类型 | 描述 |
---|---|
View | DataFrame 视图。这是对另一个相同类型的 DataFrane 的连续切片的视图。 可在此视图中读取和更改数据。 |
ConstView | 常量DataFrame 视图。这是对另一个相同类型的 DataFrane 的连续切片的视图。 一个只读视图。 |
PtrView | DataFrame 视图。这是对另一个相同类型的 DataFrane 的分离切片的视图。 可在此视图中读取和更改数据。 |
ConstPtrView | 常量DataFrame 视图。这是对另一个相同类型的 DataFrane 的分离切片的视图。 一个只读视图。 |
align_value | 一个整数值,指定DataFrame 类型的内存分配中的字节对齐边界。 |
template<typename T> AllocatorType | 用于为此类型的DataFrame 分配内存的分配器类型。 它是 std::allocator 或自定义分配器,用于在自定义字节边界上分配内存。 |
size_type | std::size_t |
IndexType | 此DataFrame 的索引列类型 |
IndexVecType | 用于索引列的向量类型。它是 std::vector 或矢量视图之一,具体取决于这是DataFrame 还是DataFrame 视图。 此外,分配器取决于align_value。 |
template<typename T> ColumnVecType | 用于数据列的向量类型。它是 std::vector 或矢量视图之一,具体取决于这是DataFrame 还是DataFrame 视图。 此外,分配器取决于align_value。 |
template<typename T> StlVecType | 具有与此DataFrame 类型兼容的分配器的 stl::vector 类型。 |
库范围类型 | 描述 |
template<typename I> StdDataFrame | 索引类型 I 的DataFrame ,它使用系统默认字节边界进行内存分配。 |
template<typename I> StdDataFrame64 | 索引类型 I 的DataFrame ,使用 64 字节边界进行内存分配。 |
template<typename I> StdDataFrame128 | 索引类型 I 的DataFrame ,使用 128 字节边界进行内存分配。 |
template<typename I> StdDataFrame256 | 索引类型 I 的DataFrame ,使用 256 字节边界进行内存分配。 |
template<typename I> StdDataFrame512 | 索引类型 I 的DataFrame ,使用 512 字节边界进行内存分配。 |
template<typename I> StdDataFrame1024 | 索引类型 I 的DataFrame ,使用 1024 字节边界进行内存分配。 |
template<typename T> struct Index2D { T begin {}; T end {}; }; | 它表示在连续内存空间内开始和结束的范围 |
DF_INDEX_COL_NAME | 一个常量字符 名称,通常指索引列。 |
DataFrame 异常 | 描述 |
struct DataFrameError{ } | 它源自 std::runtime_error。它是所有DataFrame 异常的基础。 在其他例外可能不适用的情况下,也可能引发它。 |
struct BadRange{ } | 它派生自 DataFrameError。在查询请求超出范围的数据的情况下,可能会引发此问题。 例如,当您尝试访问超出范围的数据或索引列时。 |
struct ColNotFound{ } | 它派生自 DataFrameError。在操作请求不存在的列的情况下,可能会引发它。 |
struct InconsistentData{ } | 它派生自 DataFrameError。在数据不一致的情况下,可能会引发它。 例如,当您尝试使用比索引列长的数据向量填充列时。 |
struct NotFeasible{ } | 它派生自 DataFrameError。在操作不可行的情况下,可能会抛出它。 例如,要求在字符串列中插入缺失的数据。 |
struct NotImplemented{ } | 它派生自 DataFrameError。在操作尚未实现的情况下,可能会引发它。 |
数据帧类定义为:
模板<类型名 I、类型名 H>
类数据帧;
我指定索引列类型 H 指定一个异构向量类型来包含 DataFrame 列 — 不要太纠结于此,而是使用 DataFrame 库类型
中方便的 typedef。
-
H 只能是:
-
HeteroVector<std::size_t A = 0>
:这是一个包含数据的实际异构向量。这将产生一个“标准”数据框
HeteroPtrView<std::size_t A = 0>
:这是一个异构矢量视图。转换视图为不连续的切片。
HeteroConstPtrView<std::size_t A = 0>
:HeteroPtrView 的常量版本。
HeteroView<std::size_t A = 0>
:这是一个异构矢量视图。转换视图为连续的切片。此视图比 HeteroPtrView 效率略高
HeteroConstView<std::size_t A = 0>
:HeteroView 的常量版本。
三、多线程
-
数据帧使用静态容器来实现类型异构性。默认情况下,这些静态容器不受保护。这是通过设计完成的。因此,默认情况下,没有锁定开销。如果在多线程程序中使用 DataFrame,则必须提供 ThreadGranularity.h 文件中定义的 SpinLock。数据帧将使用您的旋转锁来保护容器。
有关代码示例,请参阅上面的 set_lock()、remove_lock() 和 dataframe_tester.cc#3767。 -
此外,数据帧的实例也不是多线程安全的。换句话说,数据帧的单个实例不得在没有保护的情况下在多个线程中使用,除非它用作只读。
-
同时,DataFrame 在内部以两种不同的方式利用多线程:
- 异步接口: 某些方法有异步版本。例如,你有sort()/sort_async(),visit()/visit_async(),…更多。后面的版本返回一个可以并行执行的 std::future。
- DataFrame 在某些算法中适当地使用多个线程(内部且用户不知道)。用户可以通过调用 set_thread_level() 来控制(或关闭)多线程,设置要使用的最大线程数。默认值为 0。最佳线程数是用户硬件/软件环境的函数,通常通过跟踪和错误获得。set_thread_level() 和线程级别通常是一个静态属性,一旦设置,它就会应用于所有实例。
四、视图
视图具有有用且实用的用例。视图是对原始数据帧的引用的数据帧的切片。它看起来与数据帧完全相同,但如果修改视图中的任何数据,原始数据帧中的相应数据点也将被修改,反之亦然。有些事情不能在视图中执行。例如,您无法添加或删除列、扩展索引列、…
一般来说有两种观点
- 常规视图:可以在视图或原始数据帧中更改数据,并查看两侧的更改
- 常量视图:不能更改视图中的数据。但是,您可以更改原始数据帧中的数据,也可以通过其他视图更改数据,这些数据将在常量视图中引用
为什么要使用视图
- 在不复制数据的情况下对数据子集运行算法/切片
- 混合和比较不同的数据子集,而不复制数据
- 拥有一个事实来源,同时拥有不同的数据集而不复制数据
为了更好地理解,请进一步查看本文档和/或测试文件。
游客
访问者是实现分析(即统计、金融、机器学习)算法的主要机制。您可以轻松按照访问者的界面添加自定义算法,通过该算法扩展数据帧包。访问者还扮演着几个角色,在其他包中可能由单独的接口处理。访客扮演应用、转换器和算法的角色。例如,访问者可以转换列,也可以将列视为只读并实现算法。
有两个访客界面:
- 定期访问。此访问者是通过在数据帧实例上调用 visit() 方法来调用的。在这种情况下,数据帧将给定的索引和列数据点逐个传递给访问者函子。这对于一次可以对一个数据点进行操作的算法来说很方便。例如,相关性或方差访问者。
- 单动访问。通过在数据帧实例上调用 single_act_visit() 方法来调用此访问者。在这种情况下,给定索引和列的开始和结束迭代器将传递给访问者函子。因此,函数可以同时访问所有索引和列数据。这对于需要将整个数据放在一起的算法是必要的。例如回访者或中位数访客。
大多数访问者中都有一些通用界面。例如,以下接口在几乎所有访问者之间都是通用的:
*get_result():*它返回访问者/算法的结果。
*pre():*每次在开始将数据传递给访问者之前,数据帧都会调用它。pre() 是初始化进程
*post() 的地方:*每次将数据传递给访问者时,数据帧都会调用它。
有关更多示例和文档,请参阅此文档 DataFrameStatsVisits.h、DataFrameMLVisitors.h、DataFrameFinancialVisits.h、DataFrameTransformVisits.h 和 test/dataframe_tester[_2].cc。 我被问过很多次,为什么我选择算法的访问者模式而不是成员函数。
因为我希望算法是独立的对象。更准确地说明原因:
- 如果我将它们实现为成员函数,我将在数据帧中拥有 100 个成员函数
- 我希望用户能够在不接触DataFrame代码库的情况下合并他们的自定义算法。如果您遵循简单的界面,则可以编写自定义访问者并在数据帧中轻松使用它
- 算法有时会产生复杂的结果。有时结果是一个数字。但有时算法的结果可能是单个或多个向量。作为成员函数实现效率不高
- 我希望算法像对象一样自包含。这意味着单个对象应包含算法、输入、参数和结果
- 由于算法是自包含对象,因此可以传递给其他算法
内存对齐
数据帧使您能够在自定义对齐边界上分配内存。
您可以使用此功能来利用现代 CPU 中的 SIMD 指令。由于 DataFrame 算法都是在数据向量(列)上完成的,因此这与编译器优化结合使用时会派上用场。
有一些方便的typedef定义了分配内存的数据帧,例如,在64,128,256,…字节边界。请参阅数据帧库类型。
当您访问数据帧中的列时,您将获得对 StlVecType 的引用。StlVecType 只是一个 stl::vector,带有用于请求对齐的自定义分配器。
数字生成器
随机生成器和其他一些数字生成器被添加为一系列方便的独立函数来生成随机数(它涵盖了所有标准分布C++)。您可以无缝使用这些例程来生成随机数据帧列。
请参阅此文档和文件 RandGen.h 和 *dataframe_tester.cc。*有关 RandGenParams 的定义和默认值,请参阅此文档和文件 DataFrameTypes.h
代码结构
DataFrame 库几乎是一个仅标头库,其中包含一些样板源文件异常、HeteroVector.cc 和 HeteroView.cc 以及其他一些异常。此外,还有 DateTime.cc。
从根目录开始:
包含目录包含大部分代码。它包括.h和*.tcc*文件。后者是C++模板代码文件(它们大多位于 Internals 子目录中)。主头文件是 DataFrame.h。它包含数据帧类及其公共接口。该文件中的每个公共接口调用都有全面的注释。其余文件将向您展示香肠的制作方法。包含目录还包含主要包含内部数据帧实现的子目录。一个例外,DateTime.h 位于 Utils 子目录 src 目录中,其中包含仅限 Linux 的 make 文件和一些包含各种源代码的子目录。
测试目录包含所有测试源文件、模拟数据文件和测试输出文件。主要的测试源文件是 dataframe_tester.cc 和 dataframe_tester_2.cc。它包含数据帧所有功能的测试用例。它不是一个很有组织的结构。我计划使测试用例更有条理。
示例
std::vector<unsigned long> idx_col1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
std::vector<MyData> mydata_col (10);
std::vector<int> int_col1 = { 1, 2, -3, -4, 5, 6, 7, 8, 9, -10 };
std::vector<double> dbl_col1 = { 0.01, 0.02, 0.03, 0.03, 0.05, 0.06, 0.03, 0.08, 0.09, 0.03 };
ULDataFrame ul_df1;
// One way to load data into the DataFrame is one column at a time.
// A DataFrame column could be at most as long as its index column. So, you must load the index
// first before loading any column.
//
// Once you load a column or index, the data is moved to DataFrame. The original vectors are now empty.
// There are other ways of loading data without the move.
//
ul_df1.load_index(std::move(idx_col1));
ul_df1.load_column("dbl_col", std::move(dbl_col1));
ul_df1.load_column("my_data_col", std::move(mydata_col));
ul_df1.load_column("integers", std::move(int_col1));
std::vector<unsigned long> idx_col2 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
std::vector<std::string> str_col = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J" };
std::vector<std::string> cool_col =
{ "Azadi", "Hello", " World", "!", "Hype", "cubic spline", "Shawshank", "Silverado", "Arash", "Pardis" };
std::vector<double> dbl_col2 = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0 };
ULDataFrame ul_df2;
// Also, you can load data into a DataFrame all at once.
// In this case again the data is moved to the DataFrame.
//
ul_df2.load_data(std::move(idx_col2),
std::make_pair("string col", str_col),
std::make_pair("Cool Column", cool_col),
std::make_pair("numbers", dbl_col2));