Author: Tjebo Heeren
Date: 2025-09-20

# eye

#### Tjebo Heeren

#### 2025-09-20

## See more with eye

    library(eye)
    library(eyedata)

## Introduction

eye is dedicated to facilitate ophthalmic research, providing convenient application programming interfaces (API) for common tasks:

- Handling of visual acuity notations
- Easy count of patients and eyes
- Easy recoding of your eye variable
- Reshape your eye data - long or wide
- Quick summary of your eye data
- Get common summary statistics
- Calculate age
- Clean NA equivalent entries

eye includes a visual acuity conversion chart.

## Visual acuity

Pesky visual acuity notations are now a matter of the past. Convert between any of Snellen (meter/ feet/ decimal!), logMAR and ETDRS. The notation will be detected automatically and converted to the desired notation. For some more details see VA conversion. For entries with mixed notation, use `va_mixed` instead.

You can also decide to simply “clean” your VA vector with `cleanVA(x)`. This will remove all entries that are certainly no VA.

`va()` (and of course, its wrappers) cleans and converts visual acuity notations (classes) between Snellen (decimal, meter and feet), ETDRS, and logMAR. Each class can be converted from one to another. `va()` will detect the class automatically based on specific rules detailed below. Calling va() without specifying the “to” argument will simply clean the visual acuity entries - any notations will be accepted, no plausibility checks yet performed. This is then bascially a wrapper around `cleanVA`.

It takes an (atomic) vector with visual acuity entries as the only required argument. The user can specify the original VA notation, but va will check that and ignore the argument if implausible.

### Conversion steps

`va()` basically runs three main steps:

1. Entry cleaning with `clean_va()`
2. Notation detection with `which_va()`
3. Plausibility checks with `checkVA()`
4. Conversion with the S3 generic `convertVA()`

### Cleaning

1. `NA` are assigned to missing entries or strings representing such entries (“.”, ““,”{any number of spaces}“,”N/A”, “NA”, “NULL”, “-”)
2. Notation for qualitative entries is simplified (NPL becomes NLP, PL becomes LP).
3. “plus” and “minus” from Snellen entries are converted:
   - if entry -2 to +2 : take same Snellen value
   - if < -2 : take Snellen value one line below
   - if >+2 : Snellen value one line above

Snellen are unfortunately often entered with “+/-”, which is a violation of psychophysical methods designed to assign one (!) unambiguous value to visual acuity, with non-arbitrary thresholds based on psychometric functions. Therefore, transforming “+/-” notation to actual results is in itself problematic and the below suggestion to convert it will remain an approximation to the most likely “true” result. Even more so, as the given conditions should work for charts with 4 or 5 optotypes in a line, and visual acuity is not always tested on such charts. Yet, I believe that the approach is still better than just omitting the letters or (worse) assigning a missing value to those entries.

If the argument `smallstep = TRUE`, the entries will be converted to logmar values (0.02 logmar for each optotype). This is based on the assumption of 5 optotypes in a row. This argument can be overriden with `noplus = TRUE`, ignoring the plus minus entries entirely and simply returning the nearest Snellen values.

### Detection

- Internally done with `which_va()` based on the following rules
- if x integer and 3 < x <= 100: `etdrs`
- if x integer and 0 <= x <= 3: `logmar`, `snellendec` or `etdrs`
- if x numeric and -0.3 <= x <= 3: `logmar` or `snellendec`
- if character and format x/y: `snellen` (fraction)
- if one of “CF”, “HM”, “LP”, “PL”, “NLP”, or “NPL”: `quali`

#### Accepted VA formats / Plausibility checks [#plausibility]

- Snellen fractions (meter/ feet) need to be entered as fraction with “/”. any fraction is allowed e.g. 3/60 and 2/200 will also be recognized.
- ETDRS must be integer-equivalent between 0 and 100 (integer equivalent means, it can also be a character vector)
- logMAR must be between -0.3 and 3.0
- snellendec must be greater than 0 and smaller or equal to 2
- Qualitative must be either of PL, LP, NLP, NPL, HM, CF (any case allowed)
- Any element which is implausible / not recognized will be converted to NA

### Conversion

- logMAR to ETDRS: logMAR rounded to the first digit and converted with the chart.
- Snellen to logMAR: logMAR = -1 * log10(snellen_frac)
- Snellen to ETDRS: ETDRS = 85 + 50 * log10(snellen_frac) Gregori et al..
- ETDRS to logMAR: logMAR = -0.02 * etdrs + 1.7 Beck et al.
- Hand movements and counting fingers are converted following Schulze-Bonsel et al. (https://doi.org/10.1167/iovs.05-0981)
- (No) light perception are converted following the suggestions by Michael Bach
- To Snellen: Although there seems to be no good statistical reason to convert back to Snellen, it is a very natural thing to eye specialists to think in Snellen. A conversion to snellen gives a good gauge of how the visual acuity for the patients are. However, back-conversion should not be considered an exact science and any attempt to use formulas will result in very weird Snellen values that have no correspondence to common charts. Therefore, Snellen matching the nearest ETDRS and logMAR value in the VA conversion chart are used.

### Qualitative visual acuity measures

By default, `va` recognises the following values (case does not matter!)

- No light perception := c(“nlp”, “no light perception”, “no light”, “no perception of light”, “npl”),
- Light perception: = c(“lp”, “light perception”, “perception of light”, “pl”),
- Hand movements: = c(“hm”, “handmotion”, “hand movement”, “hand movements”),
- Counting fingers: = c(“cf”, “counting finger”, “counting fingers”, “finger count”, “count fingers”)

#### Custom values for qualitative entries:

To define your own values for qualitative entries, you need to pass a names list with names c(“cf”, “hm”, “nlp”, “lp”) - in that order - to quali_values. It accepts only values that can be reasonably converted into numeric values and it converts only to logMAR. If you want to convert to a different notation, you will need to call va() a second time.

### Examples

    ## automatic detection of VA notation and converting to logMAR by default
    x <- c(23, 56, 74, 58) ## ETDRS letters
    to_logmar(x) # wrapper of va(x, to = "logmar")
    #> From etdrs
    #> [1] 1.24 0.58 0.22 0.54

    ## ... or convert to snellen
    to_snellen(x) # wrapper of va(x, to = "snellen")
    #> From etdrs
    #> [1] "20/320" "20/80"  "20/32"  "20/70"

    ## eye knows metric as well
    to_snellen(x, type = "m")
    #> From etdrs
    #> [1] "6/96"  "6/24"  "6/9.6" "6/21"

    ## And the decimal snellen notation, so much loved in Germany
    to_snellen(x, type = "dec")
    #> From etdrs
    #> [1] "0.062" "0.25"  "0.625" "0.3"

    ## Remove weird entries and implausible entries depending on the VA choice
    x <- c("NLP", "0.8", "34", "3/60", "2/200", "20/50", "  ", ".", "-", "NULL")

    to_snellen(x)
    #> From snellen. Could be snellen, logmar, snellendec, etdrs
    #> 6x NA introduced for: 0.8, 34,   , ., -, NULL
    #>  [1] "20/20000" NA         NA         "20/400"   "20/2000"  "20/50"
    #>  [7] NA         NA         NA         NA
    to_snellen(x, from = "snellendec")
    #> 8x NA introduced for: 34, 3/60, 2/200, 20/50,   , ., -, NULL
    #>  [1] "20/20000" "20/25"    NA         NA         NA         NA
    #>  [7] NA         NA         NA         NA
    to_snellen(x, from = "etdrs")
    #> 8x NA introduced for: 0.8, 3/60, 2/200, 20/50,   , ., -, NULL
    #>  [1] "20/20000" NA         "20/200"   NA         NA         NA
    #>  [7] NA         NA         NA         NA
    to_snellen(x, from = "logmar")
    #> 8x NA introduced for: 34, 3/60, 2/200, 20/50,   , ., -, NULL
    #>  [1] "20/20000" "20/125"   NA         NA         NA         NA
    #>  [7] NA         NA         NA         NA

    ## "plus/minus" entries are converted to the most probable threshold (any spaces allowed)
    x <- c("20/200 - 1", "6/6-2", "20/50 + 3", "6/6-4", "20/33 + 4")
    to_logmar(x)
    #> From snellen
    #> [1] 1.0 0.0 0.3 0.1 0.1

    ## or evaluating them as logmar values (each optotype equals 0.02 logmar)
    to_logmar(x, smallstep = TRUE)
    #> From snellen
    #> [1] 1.02 0.04 0.34 0.08 0.14

    ## or you can also decide to completely ignore them (converting them to the nearest snellen value in the VA chart)
    to_snellen(x, noplus = TRUE)
    #> From snellen
    #> [1] "20/200" "20/20"  "20/50"  "20/20"  "20/32"

    ## recognises various ways to write qualitative values such as "hand motion"
    y <- c(23, "20/50", "hand motion", "hm", "count fingers", "no light perception", "nlp", "nonsense")
    va(y)
    #> 1x NA introduced for: nonsense
    #> [1] "23"    "20/50" "hm"    "hm"    "cf"    "nlp"   "nlp"   NA

    ## you can set custom strings that are recognised as values
    set_eye_strings(nlp = c("nonsense", "nlp", "no light perception"))
    va(y)
    #> [1] "23"    "20/50" "hm"    "hm"    "cf"    "nlp"   "nlp"   "nlp"

    ## reset to default with an empty call to set_eye_strings
    set_eye_strings()
    va(y)
    #> 1x NA introduced for: nonsense
    #> [1] "23"    "20/50" "hm"    "hm"    "cf"    "nlp"   "nlp"   NA

    ## use your own custom values for qualitative entries
    to_logmar(y)
    #> From snellen. Could be snellen, logmar, snellendec, etdrs
    #> 2x NA introduced for: 23, nonsense
    #> [1]  NA 0.4 2.3 2.3 2.0 3.0 3.0  NA
    to_logmar(y, quali_values = list(cf = 2, hm = 3, lp = 4, nlp = 6 ))
    #> From snellen. Could be snellen, logmar, snellendec, etdrs
    #> 2x NA introduced for: 23, nonsense
    #> [1]  NA 0.4 3.0 3.0 2.0 6.0 6.0  NA

## recodeye

Makes recoding eye variables very easy.

The following codes are recognized:

- integer coding 0:1 and 1:2, right eye being the lower number.
- right eyes: c(“r”, “re”, “od”, “right”) and
- left eyes: c(“l”, “le”, “os”, “left”) and
- both eyes: c(“b”, “both”, “ou”)

If you have different codes, you can change the recognized strings with the `eyestrings` argument, which needs to be a list. But remember to put the strings for right eyes first, or pass a named list.

You can also more globally change recognized codes with `set_eye_strings()`

    x <- c("r", "re", "od", "right", "l", "le", "os", "left", "both", "ou")
    recodeye(x)
    #>  [1] "r" "r" "r" "r" "l" "l" "l" "l" "b" "b"

    ## chose the resulting codes
    recodeye(x, to = c("od", "os", "ou"))
    #>  [1] "od" "od" "od" "od" "os" "os" "os" "os" "ou" "ou"

    ## Numeric codes 0:1/ 1:2 are recognized
    x <- 1:2
    recodeye(x)
    #> Eyes coded 1:2. Interpreting r = 1
    #> [1] "r" "l"

    ## with weird missing values
    x <- c(1:2, ".", NA, "", "    ")
    recodeye(x)
    #> Missing values and/or meaningless strings contained
    #> Eyes coded 1:2. Interpreting r = 1
    #> [1] "r" "l" NA  NA  NA  NA

    ## If you are using different strings to code for eyes, e.g., you are using a different language, you can change this either with the "eyestrings" argument
    french <- c("OD", "droit", "gauche", "OG")
    recodeye(french, eyestrings = list(r = c("droit", "od"), l = c("gauche", "og")))
    #> [1] "r" "r" "l" "l"

    ## or change it more globally with `set_eye_strings`
    set_eye_strings(right = c("droit", "od"), left = c("gauche", "og"))
    recodeye(french)
    #> [1] "r" "r" "l" "l"

    # to restore the default, call set_eye_strings empty
    set_eye_strings()

## Counting patients and eyes

`eyes` offers a very simple tool for counting patients and eyes. It will return a list object which gives you easy access to the data.

An important step in `eyes` is the guessing of the columns that identify patients and eyes. As for `myop` and of course `blink`, a specific column naming is required for a reliable automatic detection of patient and eye column(s) ( see Names and codes)

The arguments id and eye arguments overrule the name guessing for the respective columns.

### Guessing

#### patient ID columns:

- names can be in any case.
- First, `eyes` is looking for names that contain both strings “pat” and “id” (the order doesn’t matter) you can change those codes with `set_eye_strings()`
- Next, it will look for columns that are plainly called “ID”
- Last, it will search for all names that contain either “pat” or “id”

#### eye variable column:

- names can be in any case.
- `eyes` looks for columns that contain the string either “eye” or “eyes” - you can change those codes with `set_eye_strings()`
- columns will full string “eye” or “eyes” will be given precedence

### Counting

For counting eyes, eyes need to be coded in commonly used ways. You can use recodeye for very convenient recoding.

- `eyes` recognizes integer coding 0:1 and 1:2, with right being the lower number.

- Or, arguably more appropriate in R, character coding for a categorical variable:

  - right eyes: c(“r”, “re”, “od”, “right”)
  - left eyes: c(“l”, “le”, “os”, “left”)
  - both eyes: c(“b”, “both”, “ou”)

- you can change those codes with `set_eye_strings()`

### Report:

`eyes` also include a convenience function to turn the count into a text. This is intended for integration into rmarkdown reports, or for easy copy / pasting. `eyes_to_string()` parses the output of `eyes` into text under the hood. Arguments to `eyes_to_string` are passed via …:

- english Which numbers to be written in plain english: choose “small” for numbers till 12, “all” (all numbers), or “none” (or any other string!) for none
- caps First number will have capital letter

eyestr will create a string which you can paste into a report. The name was chosen because it’s a contraction of “eyes” and “strings” and it’s a tiny bit easier to type than “eyetxt”.

#### Use in rmarkdown

`eyestr` was designed with the use in rmarkdown in mind, most explicitly for the use inline. You can change the way numbers are converted to english with the `english` argument. By default, numbers smaller than or equal to 12 will be real English, all other numbers will be … numbers. You can capitalise the first number with the `caps` argument.

We analyzed ``r eyestr(amd2)`` gives: We analyzed 3357 eyes of 3357 patients We analyzed ``r eyestr(head(amd2, 100))``gives: We analyzed eleven eyes of eleven patients We analyzed ``r eyestr(amd2, english = "all")`` gives: We analyzed three thousand three hundred fifty-seven eyes of three thousand three hundred fifty-seven patients ``r eyestr(head(amd2, 100), caps = TRUE)`` were analyzed gives: Eleven eyes of eleven patients were analyzed We analyzed ``r eyestr(head(amd2, 100), english = "none")`` gives: We analyzed 11 eyes of 11 patients

## Reshape eye data

Out of convenience, data is often entered in a “wide” format: In eye research, there will be often two columns for the same variable, one column for each eye.

This may be a necessary data formal for specific questions.

However, “eye” is also variable (a dimension of your observation), and it can also be stored in a separate column. Indeed, in my experience R often needs eyes to be in a single column, with each other variable having their own dedicated column.

### myop

Reshaping many such columns can be a daunting task, and `myop()` makes this easier. It will remove duplicate rows, and pivot the eye variable to one column and generate a single column for each variable, thus shaping the data for specific types of analysis. For example, eight columns that store data of four variables for right and left eyes will be pivoted to 5 columns (one eye column and four further variable columns)). See also Examples.

As with `eyes()`, `myop()` requires a specific data format. See names and codes If there is already a column called “eye” or “eyes”, myop will not make any changes - because the data is then already assumed to be in long format.

If there still are variables spread over two columns for right and left eyes, then this is an example of messy data. A solution would be to remove or simply rename the “eye” column and then let myop do the work. However, you need to be very careful in those cases if resulting data frame is plausible.

Learn about tidy data.

#### Make myop work

myop will work reliably if you adhere to the following:

1. Common codes for eyes:
   - Right eyes: “r”, “re”, “od”, “right”
   - Left eyes: “l”, “le”, “os”, “left”
2. strings for eyes need to be separated by period or underscores. (Periods will be replaced by underscores).
3. Any order of substrings is allowed:
   - Will work: “va_r”, “right_morningpressure”, “night_iop.le”, “gat_os_postop”
   - Will fail: “VAr”, “rightmorningPressure”, “night_IOPle”, “gatOSpostop”

An exception is when there is only one column for each eye. Then the column names can consist of “eye strings” only. In this case, the argument var will be used to name the resulting variable.

If there are only eye columns in your data (should actually not happen), myop will create identifiers by row position.

Please always check the result for plausibility. Depending a lot on how the data was entered, the results could become quite surprising. There is basically a nearly infinite amount of possible combinations of how to enter data, and it is likely that myop will not be able to deal with all of them.

#### myop under the hood

`myop()` basically runs three main steps:

1. Removing duplicates
2. Rename data names with `myop_rename()` and `sort_substr()`:
   - Replacing “.” by “_”
   - Re-arranging and recoding substrings in a way that strings for eyes always appear at first position. They will be recoded to “r” and “l”
3. Myopization: The actual work is done with `myopizer()` and `myop_pivot()` and itself consists of three steps.
   - All columns with an eye string at first position will be selected pivoted to two long colums (`key` and `value`) using `tidyr::pivot_longer`.
   - The `key` column will be split by position into an eye column and a `variable` column.
   - The `variable` and `value` columns will be pivoted wide again with `tidyr::pivot_wider`.

    wide1 <- data.frame(id = letters[1:3],  r = 11:13 , l = 14:16)
    iop_wide <- data.frame(id = letters[1:3], iop_r = 11:13, iop_l = 14:16)
    ## Mildly messy data frame with several variables spread over two columns:
    wide_df <- data.frame(
      id = letters[1:4],
      surgery_right = c("TE", "TE", "SLT", "SLT"),
      surgery_left = c("TE", "TE", "TE", "SLT"),
      iop_r_preop = 21:24, iop_r_postop = 11:14,
      iop_l_preop = 31:34, iop_l_postop = 11:14,
      va_r_preop = 41:44, va_r_postop = 45:48,
      va_l_preop = 41:44, va_l_postop = 45:48
    )

    ## the variable has not been exactly named, (but it is probably IOP data),
    ## you can specify the dimension with the var argument

    myop(wide1, var = "iop")
    #> # A tibble: 6 × 3
    #>   id    eye   iop
    #>   <chr> <chr> <chr>
    #> 1 a     right 11
    #> 2 a     left  14
    #> 3 b     right 12
    #> 4 b     left  15
    #> 5 c     right 13
    #> 6 c     left  16

    ## If the dimension is already part of the column names, this is not necessary.
    myop(iop_wide)
    #> # A tibble: 6 × 3
    #>   id    eye   iop
    #>   <chr> <chr> <chr>
    #> 1 a     right 11
    #> 2 a     left  14
    #> 3 b     right 12
    #> 4 b     left  15
    #> 5 c     right 13
    #> 6 c     left  16

    ## myop deals with this in a breeze:
    myop(wide_df)
    #> # A tibble: 8 × 7
    #>   id    eye   surgery iop_preop iop_postop va_preop va_postop
    #>   <chr> <chr> <chr>   <chr>     <chr>      <chr>    <chr>
    #> 1 a     right TE      21        11         41       45
    #> 2 a     left  TE      31        11         41       45
    #> 3 b     right TE      22        12         42       46
    #> 4 b     left  TE      32        12         42       46
    #> 5 c     right SLT     23        13         43       47
    #> 6 c     left  TE      33        13         43       47
    #> 7 d     right SLT     24        14         44       48
    #> 8 d     left  SLT     34        14         44       48

### hyperop

Basically the opposite of `myop()` - a slightly intelligent wrapper around `tidyr::pivot_longer()` and `tidyr::pivot_wider()`. Will find the eye column, unify the codes for the eyes (all to “r” and “l”) and pivot the columns wide, that have been specified in “cols”. Again, good names and tidy data always help!

The cols argument takes a tidyselection. Read about tidyselection

    myop_df <- myop(wide_df)
    hyperop(myop_df, cols = matches("va|iop"))
    #> # A tibble: 5 × 10
    #>   id    surgery r_iop_preop r_iop_postop r_va_preop r_va_postop l_iop_preop
    #>   <chr> <chr>   <chr>       <chr>        <chr>      <chr>       <chr>
    #> 1 a     TE      21          11           41         45          31
    #> 2 b     TE      22          12           42         46          32
    #> 3 c     SLT     23          13           43         47          <NA>
    #> 4 c     TE      <NA>        <NA>         <NA>       <NA>        33
    #> 5 d     SLT     24          14           44         48          34
    #> # ℹ 3 more variables: l_iop_postop <chr>, l_va_preop <chr>, l_va_postop <chr>

## blink

Although kind of nice, blink is more a nerdy extra and is not likely to be used much. Therefore I decided to stop the work on it. It will be left in the package as such.

See your data in a blink of an eye

`blink()` is more than just a wrapper around `myop()`, `eyes()`, `va()` and `reveal()`. It will look for VA and for IOP columns and provide the summary stats for the entire cohort and for right and left eyes for each VA and IOP variable.

This, again, requires a certain format of names and codes - See Names and Codes

### Work under the hood

1. Removing duplicates
2. Identification of the index of the VA and IOP columns
3. Remove certain columns from analysis:
   - Logical columns
   - Character columns if they are not coding for Snellen fractions or “qualitative visual acuity”
   - Those with unique values in the range of the vector defined by the argument “fct_level”. Those are removed because they are likely categorical codes. If you have columns with VA or IOP that contain only 0 and 1 for example (which is unlikely, but not impossible), you can just set `fct_level = "x"` or any other arbitrary value.
4. Rename data names with `myop_rename()` and `sort_substr()`
5. Getting the new names based on the indices of the columns
6. Creating a vector of “expected new names” for the summary after myopization
7. Myopizing if possible
8. Converting VA vectors to logMAR
9. Applying `reveal()` to all VA and IOP columns.

As you can imagine, a lot of those steps rely hugely on reasonable naming of your columns and this is what makes this function unfortunately a bit fragile. However, if you adhere to the naming conventions, blink (and myop) will do a great job for you.

If you are not happy with the automatic column selection, you can manually select the VA and IOP columns with the arguments `va_cols` or `iop_cols`. Both accept tidyselection. I personally find `starts_with`, `ends_with`, `contains()` or the more general `matches()` very useful.

### Examples

    blink(wide_df)
    #> The lifecycle of blink() has expired. It will no longer be
    #>   maintained, but will be kept in the package.
    #> From etdrs
    #> From etdrs
    #>
    #> ── blink ───────────────────────────────────────────────────────────────────────
    #> ══ Data ════════════════════════════════
    #> # A tibble: 8 × 7
    #>   id    eye   surgery iop_preop iop_postop va_preop va_postop
    #>   <chr> <chr> <chr>   <chr>     <chr>      <logmar> <logmar>
    #> 1 a     right TE      21        11         0.88     0.80
    #> 2 a     left  TE      31        11         0.88     0.80
    #> 3 b     right TE      22        12         0.86     0.78
    #> 4 b     left  TE      32        12         0.86     0.78
    #> 5 c     right SLT     23        13         0.84     0.76
    #> 6 c     left  TE      33        13         0.84     0.76
    #> 7 d     right SLT     24        14         0.82     0.74
    #> 8 d     left  SLT     34        14         0.82     0.74
    #>
    #> ══ Count of patient and eyes ═══════════
    #> ══ Counts ═══════════════
    #>    id  eyes right  left
    #>     4     8     4     4
    #>
    #> ══ Visual acuity ═══════════════════════
    #>
    #> ── $VA_total (all eyes)
    #>         var mean sd n min max
    #> 1  va_preop  0.8  0 8 0.8 0.9
    #> 2 va_postop  0.8  0 8 0.7 0.8
    #>
    #> ── $VA_eyes (right and left eyes)
    #>   var1 var2  var3  var4       var mean sd n min max
    #> 1 left left right right  va_preop  0.8  0 4 0.8 0.9
    #> 2 left left right right va_postop  0.8  0 4 0.7 0.8
    #> 3 left left right right  va_preop  0.8  0 4 0.8 0.9
    #> 4 left left right right va_postop  0.8  0 4 0.7 0.8
    #>
    #> ══ Intraocular pressure ════════════════
    #>
    #> ── $IOP_total (all eyes)
    #>          var mean  sd n min max
    #> 1  iop_preop 27.5 5.5 8  21  34
    #> 2 iop_postop 12.5 1.2 8  11  14
    #>
    #> ── $IOP_eyes (right and left eyes)
    #>   var1 var2  var3  var4        var mean  sd n min max
    #> 1 left left right right  iop_preop 32.5 1.3 4  31  34
    #> 2 left left right right iop_postop 12.5 1.3 4  11  14
    #> 3 left left right right  iop_preop 22.5 1.3 4  21  24
    #> 4 left left right right iop_postop 12.5 1.3 4  11  14

    blink(amd)
    #> The lifecycle of blink() has expired. It will no longer be
    #>   maintained, but will be kept in the package.
    #> Warning: Data seems already myopic - no changes made. ?myop for help
    #> From etdrs. Could be etdrs, logmar, snellendec
    #> Unclear which is the eye column. Counting id only.
    #> Specify eye column with "eye_col" argument
    #>
    #> ── blink ───────────────────────────────────────────────────────────────────────
    #> ══ Data ════════════════════════════════
    #> # A tibble: 118,255 × 11
    #>    patid sex   age   avdays_induc ethnicity loaded  time injgiven va     regimen
    #>    <chr> <chr> <fct>        <dbl> <chr>     <lgl>  <int> <lgl>    <logm> <chr>
    #>  1 id_1  m     60-69           28 se_asian  TRUE       0 TRUE     0.30   ranibi…
    #>  2 id_1  m     60-69           28 se_asian  TRUE      28 TRUE     0.30   ranibi…
    #>  3 id_1  m     60-69           28 se_asian  TRUE      56 TRUE     0.30   ranibi…
    #>  4 id_1  m     60-69           28 se_asian  TRUE      91 TRUE     0.40   ranibi…
    #>  5 id_1  m     60-69           28 se_asian  TRUE     131 TRUE     0.46   ranibi…
    #>  6 id_1  m     60-69           28 se_asian  TRUE     173 TRUE     0.40   ranibi…
    #>  7 id_1  m     60-69           28 se_asian  TRUE     236 TRUE     0.50   ranibi…
    #>  8 id_1  m     60-69           28 se_asian  TRUE     278 TRUE     0.50   ranibi…
    #>  9 id_1  m     60-69           28 se_asian  TRUE     411 TRUE     0.60   ranibi…
    #> 10 id_1  m     60-69           28 se_asian  TRUE     453 TRUE     0.54   ranibi…
    #> # ℹ 118,245 more rows
    #> # ℹ 1 more variable: pre2013 <lgl>
    #>
    #> ══ Count of patient and eyes ═══════════
    #> ══ Counts ═══════════════
    #>   id
    #> 7802
    #>
    #> ══ Visual acuity ═══════════════════════
    #>
    #> ── $VA_total (all eyes)
    #>   var mean  sd      n  min max
    #> 1  va  0.5 0.4 116530 -0.3 2.3
    #>
    #> ── $VA_eyes (right and left eyes)
    #> NULL
    #>
    #> ══ Intraocular pressure ════════════════
    #>
    #> ── $IOP_total (all eyes)
    #> NULL
    #>
    #> ── $IOP_eyes (right and left eyes)
    #> NULL

## Names and codes

eye works smoother with tidy data, and with good names (any package does, really!)

### Tidy data

The basic principle of tidy data is: one column for each dimension and one row for each observation.

Learn more about tidy data.

This chapter explains how you can improve names and codes so that `eye` will work like a charm.

### How do I rename columns in R?

When I started with R, I found it challenging to rename columns and I found the following methods very helpful:

I’ve got a data frame with unfortunate names:

    name_mess <- data.frame(name = "a", oculus = "r", eyepressure = 14, vision = 0.2)
    names(name_mess)
    #> [1] "name"        "oculus"      "eyepressure" "vision"

I can rename all names easily:

    names(name_mess) <- c("patID", "eye", "IOP", "VA")
    names(name_mess)
    #> [1] "patID" "eye"   "IOP"   "VA"

To rename only specific columns, even if you are not sure about their exact position:

    ## if you only want to rename one or a few columns:
    names(name_mess)[names(name_mess) %in% c("name", "vision")] <- c("patID", "VA")
    names(name_mess)
    #> [1] "patID"       "oculus"      "eyepressure" "VA"

For even more methods, I found those two threads on Stackoverflow very helpful:

- Rename single column
- Rename columns with named vector

### Tips and rules for naming:

1. Don’t be too creative with your names!
2. Use common coding:
   - eyes: “r”, “re”, “od”, “right” - or numeric coding r:l = 0:1 or 1:2
   - Visual acuity: “VA”, “BCVA”, “Acuity”
   - Intraocular pressure: “IOP”, “GAT”, “NCT”, “pressure”
   - Patient identifier: “pat”, “patient”, “ID” (ideally both: “patientID” or “patID”)
3. Column names:
   - No spaces!
   - Do not use numeric coding for eyes in column names
   - Separate eye and VA and IOP codes with underscores (“bcva_l_preop”, “VA_r”, “left_va”, “IOP_re”)
   - Keep names short
   - Don’t use underscores when you don’t need to: Consider each section divided by an underscore as a relevant characteristic of your variable. E.g., “preop” instead of “pre_op”, or simply “VA” instead of “VA_ETDRS_Letters”

### Name examples

Good names (`eye` will work nicely)

    ## right and left eyes have common codes
    ## information on the tested dimension is included ("iop")
    ## VA and eye strings are separated by underscores
    ## No unnecessary underscores.
    names(wide_df)
    #>  [1] "id"            "surgery_right" "surgery_left"  "iop_r_preop"
    #>  [5] "iop_r_postop"  "iop_l_preop"   "iop_l_postop"  "va_r_preop"
    #>  [9] "va_r_postop"   "va_l_preop"    "va_l_postop"

    names(iop_wide)
    #> [1] "id"    "iop_r" "iop_l"

OK names (`eye` will work)

    ## Id and Eye are common names, there are no spaces
    ## VA is separated from the rest with an underscore
    ## BUT:
    ## The names are quite long
    ## There is an unnecessary underscore (etdrs are always letters). Better just "VA"
    c("Id", "Eye", "FollowupDays", "BaselineAge", "Gender", "VA_ETDRS_Letters",
    "InjectionNumber")
    #> [1] "Id"               "Eye"              "FollowupDays"     "BaselineAge"
    #> [5] "Gender"           "VA_ETDRS_Letters" "InjectionNumber"

    ## All names are commonly used (good!)
    ## But which dimension of "r"/"l" are we exactly looking at?
    c("id", "r",  "l")
    #> [1] "id" "r"  "l"

Bad names (`eye` will fail)

    ## VA/IOP not separated with underscore
    ## `eye` won't be able to recognize IOP and VA columns
    c("id", "iopr", "iopl", "VAr", "VAl")
    #> [1] "id"   "iopr" "iopl" "VAr"  "VAl"

    ## A human may think this is clear
    ## But `eye` will fail to understand those variable names
    c("person", "goldmann", "vision")
    #> [1] "person"   "goldmann" "vision"

    ## Not even clear to humans
    c("var1", "var2", "var3")
    #> [1] "var1" "var2" "var3"

## Reveal common statistics

`reveal()` offers a simple API to show common summary statistics for all numeric columns of your data frame. `reveal()` is basically a slightly complicated wrapper around `mean()`, `sd()`, `length()`, `min()` and `max()` (with na.rm = TRUE and `length()` counting only non-NA values).

It is not really intended to replace other awesome data exploration packages / functions such as `skimr::skim`, and it will likely remain focussed on summarizing numerical data only.

It uses an S3 generic under the hood with methods for atomic vectors, data frames, and lists of either atomic vectors or data frames. Character vectors will be omitted (and it should give a warning that it has done so).

`reveal()` takes the grouping argument `by` and it returns vector for atomic vectors or a data frame for lists.

### Examples

    clean_df <- myop(wide_df)
    reveal(clean_df)
    #>          var mean  sd n min max
    #> 1  iop_preop 27.5 5.5 8  21  34
    #> 2 iop_postop 12.5 1.2 8  11  14
    #> 3   va_preop 42.5 1.2 8  41  44
    #> 4  va_postop 46.5 1.2 8  45  48

    reveal(clean_df, by = "eye")
    #>   var1 var2 var3 var4  var5  var6  var7  var8        var mean  sd n min max
    #> 1 left left left left right right right right  iop_preop 32.5 1.3 4  31  34
    #> 2 left left left left right right right right iop_postop 12.5 1.3 4  11  14
    #> 3 left left left left right right right right   va_preop 42.5 1.3 4  41  44
    #> 4 left left left left right right right right  va_postop 46.5 1.3 4  45  48
    #> 5 left left left left right right right right  iop_preop 22.5 1.3 4  21  24
    #> 6 left left left left right right right right iop_postop 12.5 1.3 4  11  14
    #> 7 left left left left right right right right   va_preop 42.5 1.3 4  41  44
    #> 8 left left left left right right right right  va_postop 46.5 1.3 4  45  48

    reveal(clean_df, by = c("eye", "surgery"))
    #>    var1 var2 var3 var4  var5  var6  var7  var8 var9 var10 var11 var12 var13
    #> 1  left left left left right right right right left  left  left  left right
    #> 2  left left left left right right right right left  left  left  left right
    #> 3  left left left left right right right right left  left  left  left right
    #> 4  left left left left right right right right left  left  left  left right
    #> 5  left left left left right right right right left  left  left  left right
    #> 6  left left left left right right right right left  left  left  left right
    #> 7  left left left left right right right right left  left  left  left right
    #> 8  left left left left right right right right left  left  left  left right
    #> 9  left left left left right right right right left  left  left  left right
    #> 10 left left left left right right right right left  left  left  left right
    #> 11 left left left left right right right right left  left  left  left right
    #> 12 left left left left right right right right left  left  left  left right
    #> 13 left left left left right right right right left  left  left  left right
    #> 14 left left left left right right right right left  left  left  left right
    #> 15 left left left left right right right right left  left  left  left right
    #> 16 left left left left right right right right left  left  left  left right
    #>    var14 var15 var16 var17 var18 var19 var20 var21 var22 var23 var24 var25
    #> 1  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 2  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 3  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 4  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 5  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 6  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 7  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 8  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 9  right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 10 right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 11 right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 12 right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 13 right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 14 right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 15 right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #> 16 right right right   SLT   SLT   SLT   SLT   SLT   SLT   SLT   SLT    TE
    #>    var26 var27 var28 var29 var30 var31 var32        var mean  sd n min max
    #> 1     TE    TE    TE    TE    TE    TE    TE  iop_preop 34.0  NA 1  34  34
    #> 2     TE    TE    TE    TE    TE    TE    TE iop_postop 14.0  NA 1  14  14
    #> 3     TE    TE    TE    TE    TE    TE    TE   va_preop 44.0  NA 1  44  44
    #> 4     TE    TE    TE    TE    TE    TE    TE  va_postop 48.0  NA 1  48  48
    #> 5     TE    TE    TE    TE    TE    TE    TE  iop_preop 23.5 0.7 2  23  24
    #> 6     TE    TE    TE    TE    TE    TE    TE iop_postop 13.5 0.7 2  13  14
    #> 7     TE    TE    TE    TE    TE    TE    TE   va_preop 43.5 0.7 2  43  44
    #> 8     TE    TE    TE    TE    TE    TE    TE  va_postop 47.5 0.7 2  47  48
    #> 9     TE    TE    TE    TE    TE    TE    TE  iop_preop 32.0 1.0 3  31  33
    #> 10    TE    TE    TE    TE    TE    TE    TE iop_postop 12.0 1.0 3  11  13
    #> 11    TE    TE    TE    TE    TE    TE    TE   va_preop 42.0 1.0 3  41  43
    #> 12    TE    TE    TE    TE    TE    TE    TE  va_postop 46.0 1.0 3  45  47
    #> 13    TE    TE    TE    TE    TE    TE    TE  iop_preop 21.5 0.7 2  21  22
    #> 14    TE    TE    TE    TE    TE    TE    TE iop_postop 11.5 0.7 2  11  12
    #> 15    TE    TE    TE    TE    TE    TE    TE   va_preop 41.5 0.7 2  41  42
    #> 16    TE    TE    TE    TE    TE    TE    TE  va_postop 45.5 0.7 2  45  46

## Calculate age

This is a simple function and should not require much explanation. However, it may be noteworthy to mention the subtle distinction of periods and durations, which are an idiosyncrasy of time measurements and well explained in this thread.

### Examples

    dob <- c("1984-10-16", "2000-01-01")

    ## If no second date given, the age today
    getage(dob)
    #> [1] 40.9 25.7

    ## If the second argument is specified, the age until then
    getage(dob, "2000-01-01")
    #> [1] 15.2  0.0

## Important notes

I do not assume responsability for your data or analysis. Please always keep a critical mind when working with data - if you do get results that seem implausible, there may be a chance that the data is in an unfortunate shape for which `eye` may not be suitable.

### VA conversion chart

This chart is included in the package as `va_chart`

Snellen feet | Snellen meter | Snellen decimal | logMAR | ETDRS | Categories
--- | --- | --- | --- | --- | ---
20/20000 | 6/6000 | 0.001 | 3 | 0 | NLP
20/10000 | 6/3000 | 0.002 | 2.7 | 0 | LP
20/4000 | 6/1200 | 0.005 | 2.3 | 0 | HM
20/2000 | 6/600 | 0.01 | 1.9 | 2 | CF
20/800 | 6/240 | 0.025 | 1.6 | 5 | NA
20/630 | 6/190 | 0.032 | 1.5 | 10 | NA
20/500 | 6/150 | 0.04 | 1.4 | 15 | NA
20/400 | 6/120 | 0.05 | 1.3 | 20 | NA
20/320 | 6/96 | 0.062 | 1.2 | 25 | NA
20/300 | 6/90 | 0.067 | 1.18 | 26 | NA
20/250 | 6/75 | 0.08 | 1.1 | 30 | NA
20/200 | 6/60 | 0.1 | 1.0 | 35 | NA
20/160 | 6/48 | 0.125 | 0.9 | 40 | NA
20/125 | 6/38 | 0.16 | 0.8 | 45 | NA
20/120 | 6/36 | 0.167 | 0.78 | 46 | NA
20/100 | 6/30 | 0.2 | 0.7 | 50 | NA
20/80 | 6/24 | 0.25 | 0.6 | 55 | NA
20/70 | 6/21 | 0.29 | 0.54 | 58 | NA
20/63 | 6/19 | 0.32 | 0.5 | 60 | NA
20/60 | 6/18 | 0.33 | 0.48 | 61 | NA
20/50 | 6/15 | 0.4 | 0.4 | 65 | NA
20/40 | 6/12 | 0.5 | 0.3 | 70 | NA
20/32 | 6/9.6 | 0.625 | 0.2 | 75 | NA
20/30 | 6/9 | 0.66 | 0.18 | 76 | NA
20/25 | 6/7.5 | 0.8 | 0.1 | 80 | NA
20/20 | 6/6 | 1.0 | 0.0 | 85 | NA
20/16 | 6/5 | 1.25 | -0.1 | 90 | NA
20/15 | 6/4.5 | 1.33 | -0.12 | 91 | NA
20/13 | 6/4 | 1.5 | -0.2 | 95 | NA
20/10 | 6/3 | 2.0 | -0.3 | 100 | NA

## Acknowledgements

- Thanks to Alasdair Warwick, Aaron Lee, Tim Yap, Siegfried Wagner and Abraham Olvera for great suggestions, testing and code review.
- Pearse Keane, Dun Jack Fu, Katrin Fasler and Christoph Kern for their contribution of open source data
- Thanks to Antoine Fabri for his contribution to `getage()`
- Thanks to Hadley Wickham and all developers of the `tidyverse` packages and the packages `roxygen2`, `usethis`, `testthis` and `devtools`, all on which `eye` heavily relies.

## Resources

- Michael Bach’s homepage
- Michael Bach on NLP and LP

## References

Beck, Roy W, Pamela S Moke, Andrew H Turpin, Frederick L Ferris, John Paul SanGiovanni, Chris A Johnson, Eileen E Birch, et al. 2003. “A Computerized Method of Visual Acuity Testing.” American Journal of Ophthalmology 135 (2). Elsevier BV: 194–205. https://doi.org/10.1016/s0002-9394(02)01825-1.

Gregori, Ninel Z, William Feuer, and Philip J Rosenfeld. 2010. “Novel Method for Analyzing Snellen Visual Acuity Measurements.” Retina 30 (7). Ovid Technologies (Wolters Kluwer Health): 1046–50. https://doi.org/10.1097/iae.0b013e3181d87e04.

Holladay, Jack T. 2004. “Visual Acuity Measurements.” Journal of Cataract and Refractive Surgery 30 (2): 287–90. https://doi.org/10.1016/j.jcrs.2004.01.014.

Schulze-Bonsel, Kilian, Nicolas Feltgen, Hermann Burau, Lutz Hansen, and Michael Bach. 2006. “Visual Acuities ‘Hand Motion’ and ‘Counting Fingers’ Can Be Quantified with the Freiburg Visual Acuity Test.” Investigative Ophthalmology & Visual Science 47 (3): 1236–40. https://doi.org/10.1167/iovs.05-0981.