| Signature | Description |
|---|---|
enum class crosstab_norm_policy : unsigned char { none = 1, // Return raw integer counts. // Divide every cell by the grand total so every cell is a fraction in // [0, 1] and all cells sum to 1. // all = 2, // Divide each cell by its row total. Each row therefore sums to 1. // row = 3, // Divide each cell by its column total. Each column therefore sums to 1. // column = 4, }; |
Controls whether and how crosstab() normalises the count matrix. |
| Signature | Description | Parameters |
|---|---|---|
template<hashable_equal ROW_T, hashable_equal COL_T> DataFrame<ROW_T> crosstab(const char *row_col_name, const char *col_col_name, bool margins = false, crosstab_norm_policy norm_p = crosstab_norm_policy::none) const; |
Compute a cross-tabulation (contingency table) of two categorical columns (or one column and the index). For every unique value in the row-axis column, a row is added to the result DataFrame. For every unique value in the column-axis column, a column is added to the result DataFrame. Each cell contains the number of times that the (row-value, col-value) pair appears together in the input. The result DataFrame is indexed by the sorted unique values of the row column (type ROW_T). Each data column is named after the corresponding unique value from the column-axis column (via std::to_string() or, for string-like types, using the value directly). Either row_col_name or col_col_name may be set to DF_INDEX_COL_NAME ("INDEX") to use the DataFrame index as that axis, following the same convention used by fl_valid_index() and the groupby family. NOTE: NaN values in either axis column are silently skipped (the row is excluded from all counts), matching the behaviour of value_counts(). |
ROW_T: Type of the row-axis column. Must be hashable and equality-comparable (same constraint as value_counts). When row_col_name == DF_INDEX_COL_NAME, ROW_T must match IndexType. COL_T: Type of the column-axis column. Same constraints as ROW_T. Column names in the result are formed by converting COL_T values to strings via std::to_string(); for std::string / const char * the value is used directly. row_col_name: Name of the row-axis data column, or DF_INDEX_COL_NAME. col_col_name: Name of the column-axis data column, or DF_INDEX_COL_NAME. margins: If true, append "All" row and "All" column marginal totals They are computed before any normalisation.. normalize: Normalisation mode (see crosstab_norm_policy). |
static void test_crosstab() { std::cout << "\nTesting crosstab( ) ..." << std::endl; using MyDataFrame = StdDataFrame<unsigned long>; MyDataFrame df; df.load_index(std::vector<unsigned long>{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); df.load_column<std::string>("dept", std::vector<std::string>{ "A","A","A","B","B","B","C","C","C","C" }); df.load_column<std::string>("grade", std::vector<std::string>{ "X","Y","X","Y","X","Y","X","X","Y","Y" }); // Raw counts // { const auto result { df.crosstab<std::string, std::string>("dept", "grade") }; // result.write<std::ostream, unsigned long>(std::cout, io_format::pretty_prt); const auto &idx { result.get_index() }; assert(idx.size() == 3); assert(idx[0] == "A" && idx[1] == "B" && idx[2] == "C"); // Column names must be sorted unique grade values: X, Y // assert(result.has_column("X")); assert(result.has_column("Y")); const auto &X { result.get_column<std::size_t>("X") }; const auto &Y { result.get_column<std::size_t>("Y") }; assert(X[0] == 2 && X[1] == 1 && X[2] == 2); // dept A,B,C vs X assert(Y[0] == 1 && Y[1] == 2 && Y[2] == 2); // dept A,B,C vs Y } // Margins // { const auto result { df.crosstab<std::string, std::string>("dept", "grade", /*margins=*/true) }; // result.write<std::ostream, unsigned long>(std::cout, io_format::pretty_prt); // 4 rows: A, B, C, "" // const auto &idx { result.get_index() }; assert(idx.size() == 4); assert(idx[0] == "A" && idx[1] == "B" && idx[2] == "C" && idx[3].empty()); // 3 columns: X, Y, All // assert(result.has_column("X")); assert(result.has_column("Y")); assert(result.has_column("All")); const auto &X { result.get_column<std::size_t>("X") }; const auto &Y { result.get_column<std::size_t>("Y") }; const auto &All { result.get_column<std::size_t>("All") }; // Row totals (All column) // assert(All[0] == 3); // A: 2+1 assert(All[1] == 3); // B: 1+2 assert(All[2] == 4); // C: 2+2 assert(All[3] == 10); // grand total // Column totals (All row, index 3) // assert(X[3] == 5); // 2+1+2 assert(Y[3] == 5); // 1+2+2 assert(All[3] == 10); // grand total } // Normalize all // { const auto result { df.crosstab<std::string, std::string>("dept", "grade", /*margins=*/false, crosstab_norm_policy::all) }; // result.write<std::ostream, double>(std::cout, io_format::pretty_prt, { .precision = 3 }); const auto &idx { result.get_index() }; assert(idx.size() == 3); assert(idx[0] == "A" && idx[1] == "B" && idx[2] == "C"); const auto &X { result.get_column<double>("X") }; const auto &Y { result.get_column<double>("Y") }; // Grand total = 10 // assert(std::abs(X[0] - 0.2) < 1e-9); // A/X = 2/10 assert(std::abs(Y[0] - 0.1) < 1e-9); // A/Y = 1/10 assert(std::abs(X[1] - 0.1) < 1e-9); // B/X = 1/10 assert(std::abs(Y[1] - 0.2) < 1e-9); // B/Y = 2/10 assert(std::abs(X[2] - 0.2) < 1e-9); // C/X = 2/10 assert(std::abs(Y[2] - 0.2) < 1e-9); // C/Y = 2/10 // All cells must sum to 1.0 // double total { 0.0 }; for (std::size_t i { 0 }; i < 3; ++i) total += X[i] + Y[i]; assert(std::abs(total - 1.0) < 1e-9); } // Normalize row // { const auto result { df.crosstab<std::string, std::string>("dept", "grade", /*margins=*/false, crosstab_norm_policy::row) }; // result.write<std::ostream, double>(std::cout, io_format::pretty_prt, { .precision = 3 }); const auto &idx { result.get_index() }; assert(idx.size() == 3); assert(idx[0] == "A" && idx[1] == "B" && idx[2] == "C"); const auto &X { result.get_column<double>("X") }; const auto &Y { result.get_column<double>("Y") }; // Row totals: A=3, B=3, C=4 // assert(X.size() == 3); assert(Y.size() == 3); assert(std::abs(X[0] - 2.0/3.0) < 1e-9); assert(std::abs(Y[0] - 1.0/3.0) < 1e-9); assert(std::abs(X[1] - 1.0/3.0) < 1e-9); assert(std::abs(Y[1] - 2.0/3.0) < 1e-9); assert(std::abs(X[2] - 0.5) < 1e-9); assert(std::abs(Y[2] - 0.5) < 1e-9); // Each row must sum to 1.0 // for (std::size_t i { 0 }; i < 3; ++i) assert(std::abs(X[i] + Y[i] - 1.0) < 1e-9); } // Normalize column // { const auto result { df.crosstab<std::string, std::string>("dept", "grade", /*margins=*/false, crosstab_norm_policy::column) }; // result.write<std::ostream, double>(std::cout, io_format::pretty_prt, { .precision = 3 }); const auto &idx { result.get_index() }; assert(idx.size() == 3); assert(idx[0] == "A" && idx[1] == "B" && idx[2] == "C"); const auto &X { result.get_column<double>("X") }; const auto &Y { result.get_column<double>("Y") }; // Column totals: X=5, Y=5 // assert(std::abs(X[0] - 2.0/5.0) < 1e-9); assert(std::abs(Y[0] - 1.0/5.0) < 1e-9); assert(std::abs(X[1] - 1.0/5.0) < 1e-9); assert(std::abs(Y[1] - 2.0/5.0) < 1e-9); assert(std::abs(X[2] - 2.0/5.0) < 1e-9); assert(std::abs(Y[2] - 2.0/5.0) < 1e-9); // Each column must sum to 1.0 // double sum_X { 0.0 }, sum_Y { 0.0 }; for (std::size_t i { 0 }; i < 3; ++i) { sum_X += X[i]; sum_Y += Y[i]; } assert(std::abs(sum_X - 1.0) < 1e-9); assert(std::abs(sum_Y - 1.0) < 1e-9); } // Row from index column // { MyDataFrame df; df.load_index(std::vector<unsigned long>{ 10, 10, 20, 20, 30, 30 }); df.load_column<std::string>("grade", std::vector<std::string>{ "X","Y","X","Y","X","Y" }); const auto result { df.crosstab<unsigned long, std::string>(DF_INDEX_COL_NAME, "grade") }; // result.write<std::ostream, unsigned long>(std::cout, io_format::pretty_prt); const auto &idx = result.get_index(); assert(idx.size() == 3); assert(idx[0] == 10 && idx[1] == 20 && idx[2] == 30); const auto &X { result.get_column<unsigned long>("X") }; const auto &Y { result.get_column<unsigned long>("Y") }; for (std::size_t i { 0 }; i < 3; ++i) { assert(X[i] == 1); assert(Y[i] == 1); } } // Column from index column // { MyDataFrame df; df.load_index(std::vector<unsigned long>{ 1, 2, 1, 2 }); df.load_column<std::string>("dept", std::vector<std::string>{ "A","A","B","B" }); const auto result { df.crosstab<std::string, unsigned long>("dept", DF_INDEX_COL_NAME) }; // result.write<std::ostream, unsigned long>(std::cout, io_format::pretty_prt); const auto &idx { result.get_index() }; assert(idx.size() == 2); assert(idx[0] == "A" && idx[1] == "B"); // Column names are the stringified index values "1" and "2" // assert(result.has_column("1")); assert(result.has_column("2")); const auto &c1 { result.get_column<unsigned long>("1") }; const auto &c2 { result.get_column<unsigned long>("2") }; assert(c1[0] == 1 && c1[1] == 1); assert(c2[0] == 1 && c2[1] == 1); } // Integer types // { MyDataFrame df; df.load_index(std::vector<unsigned long>{ 0,1,2,3,4,5 }); df.load_column<int>("row_key", std::vector<int>{ 1,1,2,2,3,3 }); df.load_column<int>("col_key", std::vector<int>{ 10,20,10,20,10,20 }); const auto result { df.crosstab<int, int>("row_key", "col_key") }; // result.write<std::ostream, unsigned long>(std::cout, io_format::pretty_prt); // Index: 1, 2, 3 // const auto &idx { result.get_index() }; assert(idx.size() == 3); assert(idx[0] == 1 && idx[1] == 2 && idx[2] == 3); // Column names: "10", "20" // assert(result.has_column("10")); assert(result.has_column("20")); const auto &c10 { result.get_column<unsigned long>("10") }; const auto &c20 { result.get_column<unsigned long>("20") }; for (std::size_t i { 0 }; i < 3; ++i) { assert(c10[i] == 1); assert(c20[i] == 1); } } // Normalize all margins // { const auto result { df.crosstab<std::string, std::string>("dept", "grade", /*margins=*/true, crosstab_norm_policy::all) }; // result.write<std::ostream, double>(std::cout, io_format::pretty_prt, { .precision = 3 }); // 4 rows: A, B, C, "" // const auto &idx { result.get_index() }; assert(idx.size() == 4); assert(idx[0] == "A" && idx[1] == "B" && idx[2] == "C" && idx[3].empty()); // Grand total = 10; the "All/All" cell should be 1.0 // const auto &All { result.get_column<double>("All") }; assert(All.size() == 4); assert(std::abs(All[0] - 0.3) < 1e-9); assert(std::abs(All[2] - 0.4) < 1e-9); assert(std::abs(All.back() - 1.0) < 1e-9); // All row: X_total/grand = 5/10 = 0.5; Y_total/grand = 0.5 // const auto &X { result.get_column<double>("X") }; const auto &Y { result.get_column<double>("Y") }; assert(X.size() == 4); assert(Y.size() == 4); assert(std::abs(X[0] - 0.2) < 1e-9); assert(std::abs(X[2] - 0.2) < 1e-9); assert(std::abs(X.back() - 0.5) < 1e-9); assert(std::abs(Y[0] - 0.1) < 1e-9); assert(std::abs(Y[2] - 0.2) < 1e-9); assert(std::abs(Y.back() - 0.5) < 1e-9); } }