---
title: "Migrating to the efa_* interface"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Migrating to the efa_* interface}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

`EFAtools` exposes its functionality through a set of lowercase `efa_*` functions:
`efa_screen()`, `efa_retain()`, `efa_fit()`, and so on. Earlier releases used uppercase
names instead (`EFA()`, `N_FACTORS()`, `OMEGA()`, ...). The uppercase names have **not** been
removed — they still work — but the `efa_*` functions are now the recommended interface. This
vignette explains the change, gives the full old-to-new mapping, and walks through the
migration with the largest change to the argument list: `EFA()` to `efa_fit()`.

```{r}
library(EFAtools)
```

## Why the efa_* Interface

The `efa_*` names are the interface we recommend for new code. They read consistently,
share a common prefix that groups them in tab-completion and the documentation index, and
they are where the package's development now happens.

The uppercase names are **superseded**, not deprecated. In practice that means:

* They keep working with their **original arguments**, and calling them emits **no warning
  or message**. Existing scripts and saved analyses continue to run byte-for-byte
  unchanged.
* Their argument lists are **frozen**. New capabilities are added only to the `efa_*`
  functions, so an uppercase name will never grow a new argument.

So there is no urgency to rewrite working code. Migrate a script when you want the new
functions' additional features (or simply for consistency); until then the old calls
remain valid.

## The Old-to-New Mapping

Every uppercase function has a lowercase counterpart. For most of them the migration is a
pure rename: the arguments are identical and only the name changes.

| Old name | Recommended name |
|---|---|
| `EFA()` | `efa_fit()` |
| `N_FACTORS()` | `efa_retain()` |
| `EFA_AVERAGE()` | `efa_average()` |
| `EFA_POOLED()` | `efa_mi()` |
| `COMPARE()` | `efa_compare()` |
| `SL()` | `efa_schmid_leiman()` |
| `OMEGA()` | `efa_reliability()` |
| `FACTOR_SCORES()` | `efa_scores()` |
| `PROCRUSTES()` | `efa_procrustes()` |
| `BARTLETT()` | `efa_bartlett()` |
| `KMO()` | `efa_kmo()` |
| `PARALLEL()` | `efa_parallel()` |
| `EKC()` | `efa_ekc()` |
| `KGC()` | `efa_kgc()` |
| `HULL()` | `efa_hull()` |
| `SCREE()` | `efa_scree()` |
| `MAP()` | `efa_map()` |
| `NEST()` | `efa_nest()` |
| `SMT()` | `efa_smt()` |
| `CD()` | `efa_cd()` |

`efa_reliability()` and `efa_scores()` are broader than the `OMEGA()` and `FACTOR_SCORES()`
they replace, but they cover the same use cases and are the recommended way to obtain those
quantities going forward.

Alongside the renamed functions, the package ships tools that have **no uppercase
predecessor** — they are new, and available only under the `efa_*` interface (and the two
control constructors):

| New function | Purpose |
|---|---|
| `efa_screen()` | Data screening and factorability diagnostics in one report |
| `efa_group()` | Multigroup EFA with factor congruence |
| `efa_simulate()` | Simulate data from a factor model |
| `efa_power()` | Power analysis for EFA |
| `estimate_control()` | Bundle the estimation tuning knobs (see below) |
| `rotate_control()` | Bundle the rotation tuning knobs (see below) |

For a plain rename, the call is unchanged apart from the name. For example, the
Kaiser-Meyer-Olkin criterion:

```{r}
cor_mat <- test_models$baseline$cormat

# Recommended name -- exactly the same arguments as KMO():
efa_kmo(cor_mat)

# The old name still works and returns the same result:
identical(KMO(cor_mat)$KMO, efa_kmo(cor_mat)$KMO)
```

## Migrating from `EFA()` to `efa_fit()`

`EFA()` exposed every estimation and rotation setting as a flat argument, which made for a
long and somewhat unwieldy signature. `efa_fit()` keeps the **primary** choices — the data,
factor count, sample size, estimator, rotation, and a few others — as top-level arguments
(see `?efa_fit` for the full list), and collects the **tuning** knobs into two small control
objects built by `estimate_control()` and `rotate_control()`.

Each flat `EFA()` tuning argument now lives in one of the two controls:

| Control object | Arguments it holds |
|---|---|
| `estimate_control()` | `type`, `init_comm`, `criterion`, `criterion_type`, `max_iter`, `abs_eigen`, `start_method` |
| `rotate_control()` | `type`, `normalize`, `precision`, `order_type`, `varimax_type`, `p_type`, `k`, `random_starts` |

A few points make the translation mechanical:

* **The `type` preset feeds both controls.** `EFA()` had a single `type`
  (`"EFAtools"`, `"psych"`, `"SPSS"`, or `"none"`) that governed both estimation and
  rotation. Pass the same `type` to each constructor to reproduce it. Because the two
  controls are independent, you *can* now give them different presets, but you do not have
  to.
* **Three arguments were renamed.** The estimator is selected with `estimator` instead of
  `method` — passing `method` to `efa_fit()` is an error that points to the new name, while
  `EFA()` keeps its `method` argument. `P_type` is now `p_type`, and `randomStarts` is now
  `random_starts`; `EFA()` still accepts those old spellings silently.
* **Rotation-engine extras** — the criterion-specific arguments a rotation may take, such
  as `maxit`, `gam` (oblimin), or `delta` (geomin) — are passed through `rotate_control()`'s
  `...` (or, equivalently, `efa_fit()`'s own `...`). Both validate the names: an extra the
  selected rotation's engine cannot consume (for example `gamma`, a misspelling of oblimin's
  `gam`) is rejected with an error, where `EFA()` silently ignores it.

The side-by-side below reproduces an SPSS-style analysis. The old flat call and the new
control-based call give identical results:

```{r}
# Old flat interface
efa_old <- EFA(cor_mat, n_factors = 3, N = 500, type = "SPSS")

# New interface: the SPSS preset travels through the two control objects
efa_new <- efa_fit(cor_mat, n_factors = 3, N = 500,
                   estimate_control = estimate_control(type = "SPSS"),
                   rotate_control = rotate_control(type = "SPSS"))

# Identical numerical result
all.equal(efa_old$rot_loadings, efa_new$rot_loadings)
```

An individual knob is set on the control it belongs to. A flat
`EFA(..., type = "SPSS", max_iter = 500, k = 3)` becomes:

```{r, eval = FALSE}
efa_fit(cor_mat, n_factors = 3, N = 500, rotation = "promax",
        estimate_control = estimate_control(type = "SPSS", max_iter = 500),
        rotate_control   = rotate_control(type = "SPSS", k = 3))
```

One behavioural difference is worth knowing about. `efa_fit()` **rejects** a tuning knob
passed directly, rather than silently ignoring it. This turns a common and previously
invisible mistake into an immediate, informative error that names the constructor the knob
belongs to:

```{r, eval = FALSE}
efa_fit(cor_mat, n_factors = 3, N = 500, max_iter = 500)
#> Error: `max_iter` cannot be passed to `efa_fit()` directly.
#> i The estimation and rotation tuning knobs live in `estimate_control()` and `rotate_control()`.
#> i For example: `efa_fit(x, ..., estimate_control = estimate_control(max_iter = 500))`.
```

The same move — collecting flat estimation knobs into `estimate_control()` — applies to the
other functions that fit a model internally: `efa_retain()`, `efa_schmid_leiman()`, and the
retention criteria that fit a model or use EFA-based eigenvalues — `efa_parallel()`,
`efa_kgc()`, `efa_scree()`, `efa_hull()`, `efa_nest()`, and `efa_smt()`. `efa_average()` is the
exception: it keeps its flat estimation and rotation arguments, and only renames `P_type` to
`p_type`.

## What the `type` Presets Replicate

The `type` presets are more than shorthand for a bundle of defaults. `"SPSS"` and `"psych"`
follow how SPSS's FACTOR procedure and `psych::fa()` implement principal axis factoring and
promax rotation, and `"EFAtools"`, the default, combines the settings that performed best in
a systematic comparison of the three
([Grieder and Steiner, 2022](https://doi.org/10.3758/s13428-021-01581-x)). There is one place a
preset alone is not enough: because every preset keeps EFAtools' Kaiser normalization, `"psych"`
also needs `normalize = FALSE` to reproduce `psych::fa()`'s promax — and even then a small
residual difference remains, from ordinary convergence slack between the two implementations.

## Returned Objects Keep Their Legacy Classes

Migrating a call does not break code that inspects or dispatches on the result. The renamed
functions attach the **legacy class alongside the new one**, so `inherits()` checks and S3
methods written against the old classes keep working — including on objects saved from
earlier sessions.

```{r}
class(efa_new)
inherits(efa_new, "EFA")
```

The same holds for the other direct renames: `efa_retain()` objects still inherit
`"N_FACTORS"`, `efa_bartlett()` still inherits `"BARTLETT"`, and likewise for
`efa_kmo()`, `efa_schmid_leiman()`, `efa_compare()`, `efa_average()`, and `efa_mi()`. The
retention criteria (`efa_parallel()`, `efa_ekc()`, and the rest) all share the single
`efa_retention` class they have returned since EFAtools 0.8.0. And the uppercase functions
themselves keep working unchanged: an `EFA()` call returns the same result as before, now
additionally classed `efa` so that it picks up the shared methods — `inherits()` checks
against `"EFA"` are unaffected.

## Where to Next

That is the whole migration: rename the function, and — for `EFA()` only — move the tuning
knobs into `estimate_control()` and `rotate_control()`. For an overview of the full analysis
workflow under the `efa_*` interface, see the [EFAtools](EFAtools.html) vignette; the
individual help pages document each function's arguments in full.
