From 600944351a2bf3b98ba0e0b660ed5abd4449bc18 Mon Sep 17 00:00:00 2001 From: Stephanie Reinders Date: Wed, 6 Nov 2024 17:36:07 -0600 Subject: [PATCH] Changed writer and doc indices to optional in `get_clusters_batch()` ## Changes to `get_clusters_batch()` If the user supplies writer and doc indices, the output data frame contains columns 'docname', 'writer', 'doc', and columns for cluster assignment and graph measurements. If the user does not supply writer and doc indices, the output data frame has the 'docname' column but not 'writer' or 'doc' columns. The cluster assignment and graph measurement columns are the same. ## Changes to `get_cluster_fill_counts()` `get_cluster_fill_counts()` now checks whether the 'writer' and 'doc' columns are present in the input data frame. If they are present, the function groups by them, along with 'docname' and 'cluster'. If they are not present, the function groups by 'docname' and 'cluster' alone. ## Tests I created tests for `get_clusters_batch()` and `get_cluster_fill_counts()` when writer and doc indices are not used. I also renamed the test files to match the corresponding R files to make finding things easier. --- R/cluster_assignment.R | 45 +++++++++++++----- R/cluster_format.R | 34 ++++++++----- man/get_clusters_batch.Rd | 12 ++--- .../fixtures/processHandwriting/clusters.rds | Bin 0 -> 4268 bytes .../clusters_wo_indices.rds | Bin 0 -> 4213 bytes .../fixtures/processHandwriting/counts.rds | Bin 0 -> 324 bytes .../processHandwriting/counts_wo_indices.rds | Bin 0 -> 302 bytes .../processHandwriting/make_fixtures.R | 20 ++++++++ ...processBatch.R => test-batch-processing.R} | 0 ...sisfunctions.R => test-cluster-analysis.R} | 0 ...assignment.R => test-cluster-assignment.R} | 24 ++++++++++ ...ceFunctions.R => test-cluster-distances.R} | 0 ..._datafunctions.R => test-cluster-format.R} | 24 ++++++++++ ..._modelfunctions.R => test-cluster-model.R} | 0 ...t_ThinText.R => test-process-read-image.R} | 0 ...ssHandwriting.R => test-process-writing.R} | 21 ++++++++ tests/testthat/test-processDocument.R | 20 -------- 17 files changed, 151 insertions(+), 49 deletions(-) create mode 100644 tests/testthat/fixtures/processHandwriting/clusters.rds create mode 100644 tests/testthat/fixtures/processHandwriting/clusters_wo_indices.rds create mode 100644 tests/testthat/fixtures/processHandwriting/counts.rds create mode 100644 tests/testthat/fixtures/processHandwriting/counts_wo_indices.rds rename tests/testthat/{test-processBatch.R => test-batch-processing.R} (100%) rename tests/testthat/{test-ClusterModeling_analysisfunctions.R => test-cluster-analysis.R} (100%) rename tests/testthat/{test-cluster_assignment.R => test-cluster-assignment.R} (72%) rename tests/testthat/{test_ClusterDistanceFunctions.R => test-cluster-distances.R} (100%) rename tests/testthat/{test-ClusterModeling_datafunctions.R => test-cluster-format.R} (83%) rename tests/testthat/{test-ClusterModeling_modelfunctions.R => test-cluster-model.R} (100%) rename tests/testthat/{test_ThinText.R => test-process-read-image.R} (100%) rename tests/testthat/{test-processHandwriting.R => test-process-writing.R} (62%) delete mode 100644 tests/testthat/test-processDocument.R diff --git a/R/cluster_assignment.R b/R/cluster_assignment.R index 20d0cf13..e19971a4 100644 --- a/R/cluster_assignment.R +++ b/R/cluster_assignment.R @@ -21,10 +21,10 @@ #' @param input_dir A directory containing graphs created with #' [`process_batch_dir`] #' @param output_dir Output directory for cluster assignments -#' @param writer_indices Vector of start and end indices for the writer id in -#' the graph file names -#' @param doc_indices Vector of start and end indices for the document id in the -#' graph file names +#' @param writer_indices Optional. A Vector of start and end indices for the writer id in +#' the graph file names. +#' @param doc_indices Optional. Vector of start and end indices for the document id in the +#' graph file names. #' @param num_cores Integer number of cores to use for parallel processing #' @param save_master_file TRUE or FALSE. If TRUE, a master file named #' 'all_clusters.rds' containing the cluster assignments for all documents in @@ -46,7 +46,13 @@ #' #' @export #' @md -get_clusters_batch <- function(template, input_dir, output_dir, writer_indices, doc_indices, num_cores = 1, save_master_file = FALSE) { +get_clusters_batch <- function(template, + input_dir, + output_dir, + writer_indices = NULL, + doc_indices = NULL, + num_cores = 1, + save_master_file = FALSE) { # bind global variables to fix check() note i <- outliercut <- docname <- NULL @@ -120,7 +126,10 @@ get_clusters_batch <- function(template, input_dir, output_dir, writer_indices, # get cluster assignments cluster_assign <- sapply(imagesList, makeassignment, templateCenterList = template$centers, outliercut = outliercut) - df <- make_clusters_df(cluster_assign, doc, writer_indices, doc_indices) + df <- make_clusters_df(cluster_assign = cluster_assign, + doc = doc, + writer_indices = writer_indices, + doc_indices = doc_indices) saveRDS(df, file = outfile) @@ -168,7 +177,10 @@ get_clusters_batch <- function(template, input_dir, output_dir, writer_indices, message(paste("Getting cluster assignments for", doc$docname)) cluster_assign <- sapply(imagesList, makeassignment, templateCenterList = template$centers, outliercut = outliercut) - df <- make_clusters_df(cluster_assign, doc, writer_indices, doc_indices) + df <- make_clusters_df(cluster_assign = cluster_assign, + doc = doc, + writer_indices = writer_indices, + doc_indices = doc_indices) saveRDS(df, file = outfile) message(paste("Saving cluster assignments for ", doc$docname, "\n")) @@ -411,7 +423,7 @@ MakeLetterListLetterSpecific = function(letterList, dims) return(letterList) } -make_clusters_df <- function(cluster_assign, doc, writer_indices, doc_indices) { +make_clusters_df <- function(cluster_assign, doc, writer_indices = NULL, doc_indices = NULL) { # calculate pc rotation angle and wrapped pc rotation angle # NOTE: foreach can't find get_pc_rotation unless it is nested in make_clusters_df get_pc_rotation <- function(x) { @@ -426,8 +438,12 @@ make_clusters_df <- function(cluster_assign, doc, writer_indices, doc_indices) { # add docname, writer, doc, slope, xvar, yvar, and covar df$docname <- doc$docname - df$writer <- sapply(df$docname, function(x) substr(x, start = writer_indices[1], stop = writer_indices[2])) - df$doc <- sapply(df$docname, function(x) substr(x, start = doc_indices[1], stop = doc_indices[2]), USE.NAMES = FALSE) + + if (!is.null(writer_indices) && !is.null(doc_indices)){ + df$writer <- sapply(df$docname, function(x) substr(x, start = writer_indices[1], stop = writer_indices[2])) + df$doc <- sapply(df$docname, function(x) substr(x, start = doc_indices[1], stop = doc_indices[2]), USE.NAMES = FALSE) + } + df$slope <- sapply(doc$process$letterList, function(x) x$characterFeatures$slope) df$xvar <- sapply(doc$process$letterList, function(x) x$characterFeatures$xvar) df$yvar <- sapply(doc$process$letterList, function(x) x$characterFeatures$yvar) @@ -436,7 +452,11 @@ make_clusters_df <- function(cluster_assign, doc, writer_indices, doc_indices) { df$pc_wrapped <- 2 * df$pc_rotation # sort columns - df <- df[, c("docname", "writer", "doc", "cluster", "slope", "xvar", "yvar", "covar", "pc_rotation", "pc_wrapped")] + if (!is.null(writer_indices) && !is.null(doc_indices)){ + df <- df[, c("docname", "writer", "doc", "cluster", "slope", "xvar", "yvar", "covar", "pc_rotation", "pc_wrapped")] + } else { + df <- df[, c("docname", "cluster", "slope", "xvar", "yvar", "covar", "pc_rotation", "pc_wrapped")] + } return(df) } @@ -468,7 +488,8 @@ delete_graphs <- function(doc, max_edges = 30){ #' get_clusterassignment #' #' An internal function for getting cluster assignments for model or questioned -#' documents. This function runs 'get_clusters_batch'. +#' documents. This function runs 'get_clusters_batch'. Notice that this function +#' requires writer and doc indices even those 'get_clusters_batch' does not. #' #' @param main_dir Directory containing a cluster template created with #' `make_clustering_template` diff --git a/R/cluster_format.R b/R/cluster_format.R index d2d459b8..a3cf6e64 100644 --- a/R/cluster_format.R +++ b/R/cluster_format.R @@ -88,17 +88,29 @@ format_template_data <- function(template) { get_cluster_fill_counts <- function(df) { docname <- writer <- doc <- cluster <- n <- NULL - # count number of graphs in each cluster for each writer - cluster_fill_counts <- df %>% - dplyr::group_by(docname, writer, doc, cluster) %>% - dplyr::summarise(n = dplyr::n()) %>% - dplyr::mutate(n = as.integer(n)) %>% - tidyr::pivot_wider(names_from = cluster, values_from = n, values_fill = 0) - - # sort columns - cols <- c(colnames(cluster_fill_counts[, c(1, 2, 3)]), sort(as.numeric(colnames(cluster_fill_counts[, -c(1, 2, 3)])))) - cluster_fill_counts <- cluster_fill_counts[, cols] - + if (('writer' %in% colnames(df)) && ('doc' %in% colnames(df))) { + # count number of graphs in each cluster for each writer + cluster_fill_counts <- df %>% + dplyr::group_by(docname, writer, doc, cluster) %>% + dplyr::summarise(n = dplyr::n()) %>% + dplyr::mutate(n = as.integer(n)) %>% + tidyr::pivot_wider(names_from = cluster, values_from = n, values_fill = 0) + + # sort columns + cols <- c(colnames(cluster_fill_counts[, c(1, 2, 3)]), sort(as.numeric(colnames(cluster_fill_counts[, -c(1, 2, 3)])))) + cluster_fill_counts <- cluster_fill_counts[, cols] + } else { + cluster_fill_counts <- df %>% + dplyr::group_by(docname, cluster) %>% + dplyr::summarise(n = dplyr::n()) %>% + dplyr::mutate(n = as.integer(n)) %>% + tidyr::pivot_wider(names_from = cluster, values_from = n, values_fill = 0) + + # sort columns + cols <- c(colnames(cluster_fill_counts[, c(1)]), sort(as.numeric(colnames(cluster_fill_counts[, -c(1)])))) + cluster_fill_counts <- cluster_fill_counts[, cols] + } + return(cluster_fill_counts) } diff --git a/man/get_clusters_batch.Rd b/man/get_clusters_batch.Rd index 8891b360..d7382920 100644 --- a/man/get_clusters_batch.Rd +++ b/man/get_clusters_batch.Rd @@ -8,8 +8,8 @@ get_clusters_batch( template, input_dir, output_dir, - writer_indices, - doc_indices, + writer_indices = NULL, + doc_indices = NULL, num_cores = 1, save_master_file = FALSE ) @@ -22,11 +22,11 @@ get_clusters_batch( \item{output_dir}{Output directory for cluster assignments} -\item{writer_indices}{Vector of start and end indices for the writer id in -the graph file names} +\item{writer_indices}{Optional. A Vector of start and end indices for the writer id in +the graph file names.} -\item{doc_indices}{Vector of start and end indices for the document id in the -graph file names} +\item{doc_indices}{Optional. Vector of start and end indices for the document id in the +graph file names.} \item{num_cores}{Integer number of cores to use for parallel processing} diff --git a/tests/testthat/fixtures/processHandwriting/clusters.rds b/tests/testthat/fixtures/processHandwriting/clusters.rds new file mode 100644 index 0000000000000000000000000000000000000000..f9964c11e8aa50e5d46b324f9c322ae7a788a446 GIT binary patch literal 4268 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 OU;hp^D%AuYA^-ram6mh> literal 0 HcmV?d00001 diff --git a/tests/testthat/fixtures/processHandwriting/clusters_wo_indices.rds b/tests/testthat/fixtures/processHandwriting/clusters_wo_indices.rds new file mode 100644 index 0000000000000000000000000000000000000000..427f909803e4edb415ed6fcd8903d5728f5134fe GIT binary patch literal 4213 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