Skip to contents

Creates a frequency table for a vector or variable from a data frame, with options for weighting, sorting, handling labelled data, defining custom missing values, and displaying cumulative percentages.

With output = "default", the function returns a spicy_freq_table object that auto-prints as a spicy-formatted ASCII table via print.spicy_freq_table() and spicy_print_table(); with output = "data.frame", it returns a plain data.frame containing frequencies and proportions.

Usage

freq(
  data,
  x = NULL,
  weights = NULL,
  digits = 1L,
  valid = TRUE,
  cum = FALSE,
  sort = "",
  na_val = NULL,
  labelled_levels = c("prefixed", "labels", "values"),
  factor_levels = c("observed", "all"),
  rescale = FALSE,
  decimal_mark = ".",
  output = c("default", "data.frame"),
  user_na = TRUE,
  styled
)

Arguments

data

A data.frame, vector, or factor. If a data frame is provided, specify the target variable x. If both data and x are supplied as vectors, data is ignored with a warning.

x

A variable from data (unquoted).

weights

Optional numeric vector of weights (same length as x). A logical vector is also accepted and coerced to 1/0 (include / exclude). The variable may be referenced as a bare name when it belongs to data, or as a qualified expression like other$w (evaluated in the calling environment), which always takes precedence over data lookup. Observations with NA weights are dropped from the table with a warning; see Details.

digits

Number of decimal digits to display for percentages (default: 1). Same role as digits in cross_tab(), where the NULL default resolves to the same 1 decimal whenever percentages are shown. Displayed values round ties half to even (the R / IEC 60559 convention, shared with Stata), so an exact tie like 6.25 prints as 6.2 where SPSS would print 6.3.

valid

Logical. If TRUE (default), display valid percentages (excluding missing values).

cum

Logical. If FALSE (the default), cumulative percentages are omitted. If TRUE, adds cumulative percentages.

sort

Sorting method for values:

  • "" - the order of the values (default): the levels of a factor or of a labelled variable, numbers in ascending order, text in the C locale (capitals before lower case, the same on every machine) and FALSE before TRUE, as in cross_tab() and the tables

  • "+" - increasing frequency

  • "-" - decreasing frequency

  • "name+" - alphabetical A-Z, in the C locale

  • "name-" - alphabetical Z-A, in the C locale

For labelled variables displayed with their codes (labelled_levels "prefixed" or "values"), "name+" / "name-" sort by the underlying code (so [10] follows [2], as in SPSS), not by the display string. With labelled_levels = "labels", labels sort alphabetically.

na_val

Atomic vector of numeric or character values to be treated as missing (NA).

For labelled variables (from haven or labelled), this argument must refer to the underlying coded values, not the visible labels.

Example:

x <- labelled(c(1, 2, 3, 1, 2, 3), c("Low" = 1, "Medium" = 2, "High" = 3))
freq(x, na_val = 1) # Treat all "Low" as missing

labelled_levels

For labelled variables, defines how labels and values are displayed:

  • "prefixed" or "p" - show labels as [value] label (default)

  • "labels" or "l" - show only labels

  • "values" or "v" - show only numeric codes

factor_levels

Character. Controls how factor and labelled values are displayed in the frequency table. "observed" (the default; matches Stata's tab and SPSS FREQUENCIES, which both list only values present in the data) shows only levels present in the data. "all" (code_book()'s default) keeps every declared level, including unused ones, which appear with n = 0.

rescale

Logical. If FALSE (the default), weights are used as-is. If TRUE, rescale weights so that their total equals the unweighted sample size (length(weights)). When the argument is not supplied, the default can be set globally with options(spicy.rescale = TRUE), which is read by both freq() and cross_tab(). See Details for the interaction with NA weights.

decimal_mark

Character used as the decimal mark in printed percentages. Either "." or ",". The default follows the language: options(spicy.language = "fr") gives the comma here and in cross_tab(), exactly as it does in the reporting table_*() families. An argument you type wins, so decimal_mark = "." under a French language gives French words and a decimal point. The resolved mark is frozen on the object when it is built: a table built under a French language still prints its commas when it is printed later with the language option cleared. Its words are not – freq() resolves its labels at print time – so that table prints English headings over French numbers.

output

Output format. "default" (the default) returns a spicy_freq_table object that auto-prints as a formatted spicy table; "data.frame" returns a plain data.frame with frequency values. The values match the output argument of the table_*() family; the rendered engines that family also accepts ("tinytable", "gt", "flextable", ...) are not available in freq().

user_na

Logical. If TRUE (the default), declared missing values are treated as missing: each observed declared value becomes its own row of the Missing block (with its value label), and valid percentages exclude those observations. If FALSE, the declaration is ignored and the declared codes tabulate as valid categories. See the "Declared missing values" section.

styled

Defunct. styled = TRUE is now output = "default" (the default) and styled = FALSE is now output = "data.frame"; supplying styled is an error.

Value

With output = "data.frame", a plain data.frame with no extra attributes and columns:

  • value - unique values or factor levels

  • n - frequency count (weighted if applicable)

  • prop - proportion of total

  • valid_prop - proportion of valid responses (if valid = TRUE)

  • cum_prop, cum_valid_prop - cumulative proportions, between 0 and 1 (if cum = TRUE)

With output = "default" (the default), a spicy_freq_table object: the same data.frame carrying rendering metadata as attributes (digits, data_name, var_name, var_label, class_name, n_total, n_valid, weighted, rescaled, weight_var) used by print.spicy_freq_table(). The object is returned visibly, so a bare freq(...) call auto-prints at the console while f <- freq(...) stays silent (print f to display the table).

Details

Designed to mimic common frequency procedures from SPSS or Stata while integrating the flexibility of R's data structures. The input type (vector, factor, labelled) is auto-detected; see @param labelled_levels and @param factor_levels for the schema-vs-observed level controls, and @param na_val for optional sentinel-value recoding.

Weighting (weights): frequencies and percentages are computed proportionally to the weights. Missing values in weights cause those observations to be dropped from the table entirely (with a warning), matching the behavior of cross_tab() in spicy 0.11.0+. With rescale = TRUE, the remaining (non-NA-weighted) weights are normalized so the total weighted N equals the count of non-NA-weighted rows. With rescale = FALSE, the total weighted N is the actual sum of non-NA weights.

For schema-level inspection without computing frequencies, use varlist(); code_book() documents every variable with the counts of its categories.

Declared missing values

Survey files imported with haven often carry declared missing values: codes such as 8 = Don't know or 9 = Refused that the source file marks as missing while keeping them distinct from a plain NA. Two kinds of declaration exist: na_values / na_range metadata on haven::labelled_spss() vectors, and tagged missing values created by haven::tagged_na() (the Stata .a, .b, ... convention). haven::read_sav() keeps the na_values / na_range declaration only with user_na = TRUE: its default turns the declared codes into NA on import.

spicy honors the declaration by default (user_na = TRUE): declared missing values are excluded from every statistic exactly like NA – valid percentages, means, chi-squared tests, association measures, row-wise summaries, and group definitions – but they are not erased from display. freq() lists each observed declared value as its own row of the Missing block, with its value label; cross_tab(), table_categorical(), and table_continuous() disclose the exclusion in the table note (Declared missing values removed: x (2).); varlist() counts them as missing in N_valid / NAs / N_distinct while still listing the declared codes in Values; code_book() counts them in n_declared_missing and flags them in its values table.

Every function involved offers the same escape hatch: set user_na = FALSE to ignore the declaration and treat the declared codes as valid values (the behavior of spicy before 0.13.0). Tagged missing values are genuine NAs either way; for them, user_na = FALSE only collapses the per-tag breakdown back into the regular NA count.

See also

cross_tab() for two-way cross-tabulations; table_categorical() for multi-variable categorical summary tables; varlist() / code_book() for variable inspection; print.spicy_freq_table() for formatted printing; spicy_print_table() for the underlying ASCII rendering engine.

Examples

# Frequency table with labelled ordered factor
freq(sochealth, education)
#> Frequency table: education
#> 
#>  Category   │ Values               Freq.    Percent 
#> ────────────┼───────────────────────────────────────
#>  Valid      │ Lower secondary        261       21.8 
#>             │ Upper secondary        539       44.9 
#>             │ Tertiary               400       33.3 
#> ────────────┼───────────────────────────────────────
#>  Total      │                       1200      100.0 
#> 
#> Label: Highest education level
#> Class: ordered, factor
#> Data: sochealth
freq(sochealth, self_rated_health, sort = "-")
#> Frequency table: self_rated_health
#> 
#>  Category   │ Values         Freq.    Percent    Valid Percent 
#> ────────────┼──────────────────────────────────────────────────
#>  Valid      │ Good             558       46.5             47.3 
#>             │ Very good        295       24.6             25.0 
#>             │ Fair             266       22.2             22.5 
#>             │ Poor              61        5.1              5.2 
#>  Missing    │ NA                20        1.7                  
#> ────────────┼──────────────────────────────────────────────────
#>  Total      │                 1200      100.0            100.0 
#> 
#> Label: Self-rated health
#> Class: ordered, factor
#> Data: sochealth

library(labelled)

# Simple numeric vector
x <- c(1, 2, 2, 3, 3, 3, NA)
freq(x)
#> Frequency table: x
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent 
#> ────────────┼───────────────────────────────────────────────
#>  Valid      │ 1               1       14.3             16.7 
#>             │ 2               2       28.6             33.3 
#>             │ 3               3       42.9             50.0 
#>  Missing    │ NA              1       14.3                  
#> ────────────┼───────────────────────────────────────────────
#>  Total      │                 7      100.0            100.0 
#> 
#> Class: numeric
#> Data: x

# Plain vector with a sentinel value recoded as missing
freq(c(1, 2, 3, 99, 99), na_val = 99)
#> Frequency table: c(1, 2, 3, 99, 99)
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent 
#> ────────────┼───────────────────────────────────────────────
#>  Valid      │ 1               1       20.0             33.3 
#>             │ 2               1       20.0             33.3 
#>             │ 3               1       20.0             33.3 
#>  Missing    │ NA              2       40.0                  
#> ────────────┼───────────────────────────────────────────────
#>  Total      │                 5      100.0            100.0 
#> 
#> Class: numeric
#> Data: c(1, 2, 3, 99, 99)

# Labelled variable (haven-style)
x_lbl <- labelled(
  c(1, 2, 3, 1, 2, 3, 1, 2, NA),
  labels = c("Low" = 1, "Medium" = 2, "High" = 3)
)
var_label(x_lbl) <- "Satisfaction level"

# Treat value 1 ("Low") as missing
freq(x_lbl, na_val = 1)
#> Frequency table: x_lbl
#> 
#>  Category   │ Values          Freq.    Percent    Valid Percent 
#> ────────────┼───────────────────────────────────────────────────
#>  Valid      │ [2] Medium          3       33.3             60.0 
#>             │ [3] High            2       22.2             40.0 
#>  Missing    │ NA                  4       44.4                  
#> ────────────┼───────────────────────────────────────────────────
#>  Total      │                     9      100.0            100.0 
#> 
#> Label: Satisfaction level
#> Class: haven_labelled, vctrs_vctr, double
#> Data: x_lbl

# Display only labels, add cumulative %
freq(x_lbl, labelled_levels = "labels", cum = TRUE)
#> Frequency table: x_lbl
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent    Cum. Percent 
#> ────────────┼───────────────────────────────────────────────────────────────
#>  Valid      │ Low             3       33.3             37.5            33.3 
#>             │ Medium          3       33.3             37.5            66.7 
#>             │ High            2       22.2             25.0            88.9 
#>  Missing    │ NA              1       11.1                            100.0 
#> ────────────┼───────────────────────────────────────────────────────────────
#>  Total      │                 9      100.0            100.0           100.0 
#> 
#>  Category   │ Values      Cum. Valid Percent 
#> ────────────┼────────────────────────────────
#>  Valid      │ Low                       37.5 
#>             │ Medium                    75.0 
#>             │ High                     100.0 
#>  Missing    │ NA                             
#> ────────────┼────────────────────────────────
#>  Total      │                          100.0 
#> 
#> Label: Satisfaction level
#> Class: haven_labelled, vctrs_vctr, double
#> Data: x_lbl

# Display values only, sorted descending
freq(x_lbl, labelled_levels = "values", sort = "-")
#> Frequency table: x_lbl
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent 
#> ────────────┼───────────────────────────────────────────────
#>  Valid      │ 1               3       33.3             37.5 
#>             │ 2               3       33.3             37.5 
#>             │ 3               2       22.2             25.0 
#>  Missing    │ NA              1       11.1                  
#> ────────────┼───────────────────────────────────────────────
#>  Total      │                 9      100.0            100.0 
#> 
#> Label: Satisfaction level
#> Class: haven_labelled, vctrs_vctr, double
#> Data: x_lbl

# Show all declared factor levels, including unused ones (n = 0).
# The default "observed" mirrors Stata's `tab` and SPSS FREQUENCIES,
# which both drop unused levels.
f <- factor(c("Yes", "No", "Yes"), levels = c("Yes", "No", "Maybe"))
freq(f, factor_levels = "all")
#> Frequency table: f
#> 
#>  Category   │ Values      Freq.    Percent 
#> ────────────┼──────────────────────────────
#>  Valid      │ Yes             2       66.7 
#>             │ No              1       33.3 
#>             │ Maybe           0        0.0 
#> ────────────┼──────────────────────────────
#>  Total      │                 3      100.0 
#> 
#> Class: factor
#> Data: f

# With weighting
df <- data.frame(
  sex = factor(c("Male", "Female", "Female", "Male", NA, "Female")),
  weight = c(12, 8, 10, 15, 7, 9)
)

# Weighted frequencies (raw weighted counts, the default)
freq(df, sex, weights = weight)
#> Frequency table: sex
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent 
#> ────────────┼───────────────────────────────────────────────
#>  Valid      │ Female         27       44.3             50.0 
#>             │ Male           27       44.3             50.0 
#>  Missing    │ NA              7       11.5                  
#> ────────────┼───────────────────────────────────────────────
#>  Total      │                61      100.0            100.0 
#> 
#> Class: factor
#> Data: df
#> Weight: weight

# Weighted frequencies rescaled so the total matches the sample size
freq(df, sex, weights = weight, rescale = TRUE)
#> Frequency table: sex
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent 
#> ────────────┼───────────────────────────────────────────────
#>  Valid      │ Female          3       44.3             50.0 
#>             │ Male            3       44.3             50.0 
#>  Missing    │ NA              1       11.5                  
#> ────────────┼───────────────────────────────────────────────
#>  Total      │                 6      100.0            100.0 
#> 
#> Class: factor
#> Data: df
#> Weight: weight (rescaled)

# Base R style, with weights and cumulative percentages
freq(df$sex, weights = df$weight, cum = TRUE)
#> Frequency table: sex
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent    Cum. Percent 
#> ────────────┼───────────────────────────────────────────────────────────────
#>  Valid      │ Female         27       44.3             50.0            44.3 
#>             │ Male           27       44.3             50.0            88.5 
#>  Missing    │ NA              7       11.5                            100.0 
#> ────────────┼───────────────────────────────────────────────────────────────
#>  Total      │                61      100.0            100.0           100.0 
#> 
#>  Category   │ Values      Cum. Valid Percent 
#> ────────────┼────────────────────────────────
#>  Valid      │ Female                    50.0 
#>             │ Male                     100.0 
#>  Missing    │ NA                             
#> ────────────┼────────────────────────────────
#>  Total      │                          100.0 
#> 
#> Class: factor
#> Data: df
#> Weight: df$weight

# Piped version (tidy syntax) and sort alphabetically descending ("name-")
df |> freq(sex, sort = "name-")
#> Frequency table: sex
#> 
#>  Category   │ Values      Freq.    Percent    Valid Percent 
#> ────────────┼───────────────────────────────────────────────
#>  Valid      │ Male            2       33.3             40.0 
#>             │ Female          3       50.0             60.0 
#>  Missing    │ NA              1       16.7                  
#> ────────────┼───────────────────────────────────────────────
#>  Total      │                 6      100.0            100.0 
#> 
#> Class: factor
#> Data: df

# European decimal mark (matches `cross_tab()` and the `table_*()` family)
freq(sochealth, education, decimal_mark = ",")
#> Frequency table: education
#> 
#>  Category   │ Values               Freq.    Percent 
#> ────────────┼───────────────────────────────────────
#>  Valid      │ Lower secondary        261       21,8 
#>             │ Upper secondary        539       44,9 
#>             │ Tertiary               400       33,3 
#> ────────────┼───────────────────────────────────────
#>  Total      │                       1200      100,0 
#> 
#> Label: Highest education level
#> Class: ordered, factor
#> Data: sochealth

# Plain data.frame return (for programmatic use)
f <- freq(df, sex, output = "data.frame")
head(f)
#>    value n      prop valid_prop
#> 1 Female 3 0.5000000        0.6
#> 2   Male 2 0.3333333        0.4
#> 3   <NA> 1 0.1666667         NA