eye is dedicated to facilitate ophthalmic research, providing convenient application programming interfaces (API) for common tasks:
Further, it provides tools for easy data summaries and calculations:
eye includes the real life data set amd
, of people who received intravitreal anti-VEGF injections for treatment naive neovascular age-related macular degeneration in Moorfields Eye Hospital. (Fasler et al. 2019)
eye includes a visual acuity conversion chart.
amd
is an anonymised real life data set from a large cohort of patients with treatment-naive neovascular age-related macular degeneration (AMD) who received intravitreal anti-VEGF therapy in Moorfields Eye Hospital, London, UK. Data was accessed on the 25th May 2020 from the source Kindly reference this data by citing the corresponding publication. (Fasler et al. 2019)
There are erroneous visual acuity entries in this data set which I noticed during the work on this package. The data set curator investigated and concluded that these were erroneous entries in the original medical health records. I decided to keep the values in the data set and wait for the final decision how to proceed from the data set curator (if they are going to replace it with missing values or not). I believe this is a great example for the challenges of real life data and a reminder to remain vigilant when doing data analysis.
Easy conversion from visual acuity notations in a single call to va()
:
va()
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, but va()
converts to logMAR by default. va()
will detect the class automatically based on specific rules detailed below.
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.
va()
basically runs three main steps:
clean_va()
which_va()
convertVA()
NA
are assigned to missing entries or strings representing such entries (“.”, "“,”{any number of spaces}“,”N/A“,”NA“,”NULL")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 logmarstep = TRUE
, the entries will be converted to logmar values or or ETDRS letters (0.02 logmar or 1 letter for each optotype). This is based on the assumption of 5 optotypes in a row. The argument should not be set TRUE
when conversion to Snellen is desired - this does not make sense. However, if you still decide to do that, you will get the values just without +/- entries.
which_va()
based on the following rulesetdrs
logmar
, but you can choose etdrs
logmar
intersection(va_chart$logMAR, va_chart$snellen_dec)
: default to logmar
, but you can choose snellen
(then snellen decimal)snellen
snellen
(fraction)quali
NA
(with a warning that values outside the range were found)Detection and conversion is on a vector as a whole by which_va()
. If a “mixed” VA notation is found, which_va_dissect()
and va_dissect()
will be called instead for each VA vector element individually.
There can be ambiguous cases for detection (detection defaults to logmar):
Snellen decimals are a particular challenge and va
may wrongly assign logMAR - this could happen if there are unusual snellen decimal values in the data which are not part of the VA conversion chart.
E.g., check the values with unique(x)
.
## automatic detection of VA notation and converting to logMAR by default
x <- c(23, 56, 74, 58) ## ETDRS letters
va(x)
#> x: from etdrs
#> [1] 1.24 0.58 0.22 0.54
va(x, to = "snellen") ## ... or convert to snellen
#> x: from etdrs
#> [1] "20/320" "20/80" "20/32" "20/70"
## A mix of notations
x <- c("NLP", "0.8", "34", "3/60", "2/200", "20/50")
va(x)
#> Mixed object (x) - converting one by one
#> [1] 3.00 0.80 1.02 1.30 2.00 0.40
## "plus/minus" entries are converted to the most probable threshold (any spaces allowed)
x <- c("20/200", "20/200 - 1", "6/6", "6/6-2", "20/50 + 3", "20/50 -2")
va(x)
#> x: from snellen
#> [1] 1.00 1.00 0.00 0.10 0.40 0.48
## or evaluating them as logmar values
va(x, logmarstep = TRUE)
#> x: from snellen
#> [1] 1.00 0.98 0.00 -0.04 0.46 0.36
## on the inbuilt data set:
head(va(amd$VA_ETDRS_Letters), 10)
#> Warning: amd$VA_ETDRS_Letters (from etdrs): NA introduced - implausible values
#> [1] 0.82 0.08 0.70 0.90 1.06 1.02 0.96 1.06 0.40 0.46
## and indeed, there are unplausible ETDRS values in this data set:
range(amd$VA_ETDRS_Letters)
#> [1] 0 105
## Any fraction is possible, and empty values
x <- c("CF", "3/60", "2/200", "", "20/40+3", ".", " ")
va(x)
#> x: from snellen
#> [1] 2.0 1.3 2.0 NA 0.3 NA NA
## but not when converting from one class to the other
x <- c("3/60", "2/200", "6/60", "20/200", "6/9")
va(x, to="snellen", type = "m")
#> x: from snellen
#> [1] NA NA "6/60" "6/60" "6/9"
Makes recoding eye variables very easy.
recodeye
recognizes integer coding 0:1 and 1:2, with right being the lower number.c("r", "re", "od", "right")
and left eyes: c("l", "le", "os", "left")
.If you have different codes, you can change the recognized strings with the eyecodes
argument, which needs to be a list. But remember to put the strings for right eyes first!
x <- c("r", "re", "od", "right", "l", "le", "os", "left")
recodeye(x)
#> [1] "r" "r" "r" "r" "l" "l" "l" "l"
## chose the resulting codes
recodeye(x, to = c("right", "left"))
#> [1] "right" "right" "right" "right" "left" "left" "left" "left"
## Or if you have weird codes for eyes
x <- c("alright", "righton", "lefty","leftover")
recodeye(x, eyecodes = list(c("alright","righton"), c("lefty","leftover")))
#> [1] "r" "r" "l" "l"
## Numeric codes 0:1/ 1:2 are recognized
x <- 1:2
recodeye(x)
#> Eyes coded 1:2. Interpreting r = 1
#> [1] "r" "l"
## chose the resulting codes
recodeye(x, to = c("right", "left"))
#> Eyes coded 1:2. Interpreting r = 1
#> [1] "right" "left"
## or, if right is coded with 2)
recodeye(x, numcode = 2:1)
#> Eyes coded 2:1 with r = 2
#> [1] "l" "r"
eyes
offers a very simple tool for counting patients and eyes.
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.
eyes
is looking for names that contain both strings “pat” and “id” (the order doesn’t matter)eyes
looks for columns called either “eye” or “eyes”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.eyes
recognizes right eyes: c("r", "re", "od", "right")
and left eyes: c("l", "le", "os", "left")
.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 …:
eyestr is a shorthand for eyes(x, report = TRUE)
. The name was chosen because it’s a contraction of “eyes” and “strings” and it’s a tiny bit easier to type than “eyetxt”.
eyestr
was designed with the use in rmarkdown in mind, most explicitly for the use inline:
We analysed
`r eyestr(amd)`
will give:
We analysed 3357 eyes of 3357 patients.
The beauty of this report is of course that the numbers will always update with new data.
eyes(amd)
#> Eyes coded 0:1. Interpreting r = 0
#> patients eyes right left
#> 3357 3357 1681 1676
## or as text for a report
eyestr(amd)
#> [1] "3357 eyes of 3357 patients"
## Complying with journal standards when at beginning of paragraph
eyestr(amd, para = TRUE)
#> [1] "A total of 3357 eyes of 3357 patients"
## Numbers smaller than or equal to 12 will be real English
eyestr(head(amd, 100))
#> [1] "Eleven eyes of eleven patients"
## But you can turn this off
eyestr(head(amd, 100), small_num = FALSE)
#> [1] "11 eyes of 11 patients"
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.
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.
myop will work reliably if you adhere to the following:
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()
basically runs three main steps:
myop_rename()
and sort_substr()
:
myopizer()
and myop_pivot()
and itself consists of three steps.
key
and value
) using tidyr::pivot_longer
.key
column will be split by position into an eye column and a variable
column.variable
and value
columns will be pivoted wide again with tidyr::pivot_wider
.## the variable has not been exactly named, (but it is probably IOP data),
## you can specify the dimension with the var argument
wide1 <- data.frame(id = letters[1:3], r = 11:13 , l = 14:16)
myop(wide1, var = "iop")
#> # A tibble: 6 x 3
#> id eye iop
#> <chr> <chr> <chr>
#> 1 a r 11
#> 2 a l 14
#> 3 b r 12
#> 4 b l 15
#> 5 c r 13
#> 6 c l 16
## If the dimension is already part of the column names, this is not necessary.
iop_wide <- data.frame(id = letters[1:3], iop_r = 11:13, iop_l = 14:16)
myop(iop_wide)
#> # A tibble: 6 x 3
#> id eye iop
#> <chr> <chr> <chr>
#> 1 a r 11
#> 2 a l 14
#> 3 b r 12
#> 4 b l 15
#> 5 c r 13
#> 6 c l 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
)
## myop deals with this in a breeze:
myop(wide_df)
#> # A tibble: 8 x 7
#> id eye surgery iop_preop iop_postop va_preop va_postop
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 a r TE 21 11 41 45
#> 2 a l TE 31 11 41 45
#> 3 b r TE 22 12 42 46
#> 4 b l TE 32 12 42 46
#> 5 c r SLT 23 13 43 47
#> 6 c l TE 33 13 43 47
#> 7 d r SLT 24 14 44 48
#> 8 d l SLT 34 14 44 48
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 x 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
#> # … with 3 more variables: l_iop_postop <chr>, l_va_preop <chr>,
#> # l_va_postop <chr>
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
fct_level = "x"
or any other arbitrary value.myop_rename()
and sort_substr()
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.
blink(wide_df)
#> va_preop: from etdrs
#> va_postop: from etdrs
#>
#> ── blink ────────────────────────────────────────────────────────────────────────────────
#> ══ Data ════════════════════════════════
#> # A tibble: 8 x 7
#> id eye surgery iop_preop iop_postop va_preop va_postop
#> <chr> <chr> <chr> <chr> <chr> <logmar> <logmar>
#> 1 a r TE 21 11 0.88 0.80
#> 2 a l TE 31 11 0.88 0.80
#> 3 b r TE 22 12 0.86 0.78
#> 4 b l TE 32 12 0.86 0.78
#> 5 c r SLT 23 13 0.84 0.76
#> 6 c l TE 33 13 0.84 0.76
#> 7 d r SLT 24 14 0.82 0.74
#> 8 d l SLT 34 14 0.82 0.74
#>
#> ══ Count of patient and eyes ═══════════
#> patients 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)
#> eye var mean sd n min max
#> 1 l va_preop 0.8 0 4 0.8 0.9
#> 2 l va_postop 0.8 0 4 0.7 0.8
#> 3 r va_preop 0.8 0 4 0.8 0.9
#> 4 r 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)
#> eye var mean sd n min max
#> 1 l iop_preop 32.5 1.3 4 31 34
#> 2 l iop_postop 12.5 1.3 4 11 14
#> 3 r iop_preop 22.5 1.3 4 21 24
#> 4 r iop_postop 12.5 1.3 4 11 14
blink(amd)
#> Warning: Data seems already myopic - no changes made. ?myop for help
#> Warning: va_etdrs_letters (from etdrs): NA introduced - implausible values
#> Eyes coded 0:1. Interpreting r = 0
#>
#> ── blink ────────────────────────────────────────────────────────────────────────────────
#> ══ Data ════════════════════════════════
#> # A tibble: 40,764 x 7
#> id eye followupdays baselineage gender va_etdrs_letters injectionnumber
#> <chr> <dbl> <dbl> <dbl> <dbl> <logmar> <dbl>
#> 1 00133… 1 0 99 1 0.82 1
#> 2 00133… 1 28 99 1 0.08 2
#> 3 00133… 1 67 99 1 0.70 3
#> 4 00133… 1 147 99 1 0.90 4
#> 5 00133… 1 259 99 1 1.06 5
#> 6 00133… 1 343 99 1 1.02 6
#> 7 00133… 1 399 99 1 0.96 7
#> 8 00133… 1 534 99 1 1.06 8
#> 9 00244… 0 0 82 0 0.40 1
#> 10 00244… 0 39 82 0 0.46 2
#> # … with 40,754 more rows
#>
#> ══ Count of patient and eyes ═══════════
#> patients eyes right left
#> 3357 3357 1681 1676
#>
#> ══ Visual acuity ═══════════════════════
#>
#> ── $VA_total (all eyes)
#> var mean sd n min max
#> 1 va_etdrs_letters 0.5 0.3 40762 -0.3 1.7
#>
#> ── $VA_eyes (right and left eyes)
#> eye var mean sd n min max
#> 1 0 va_etdrs_letters 0.5 0.3 20304 -0.3 1.7
#> 2 1 va_etdrs_letters 0.5 0.3 20458 -0.3 1.7
#>
#> ══ Intraocular pressure ════════════════
#>
#> ── $IOP_total (all eyes)
#> NULL
#>
#> ── $IOP_eyes (right and left eyes)
#> NULL
eye works smoother with tidy data, and with good names (any package does, really!)
The basic principle of tidy data is: one column for each dimension and one row for each observation.
This chapter explains how you can improve names and codes so that eye
will work like a charm.
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:
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:
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"
names(amd)
#> [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()
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.
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")
#> eye var mean sd n min max
#> 1 l iop_preop 32.5 1.3 4 31 34
#> 2 l iop_postop 12.5 1.3 4 11 14
#> 3 l va_preop 42.5 1.3 4 41 44
#> 4 l va_postop 46.5 1.3 4 45 48
#> 5 r iop_preop 22.5 1.3 4 21 24
#> 6 r iop_postop 12.5 1.3 4 11 14
#> 7 r va_preop 42.5 1.3 4 41 44
#> 8 r va_postop 46.5 1.3 4 45 48
reveal(clean_df, by = c("eye", "surgery"))
#> eye surgery var mean sd n min max
#> 1 l SLT iop_preop 34.0 NA 1 34 34
#> 2 l SLT iop_postop 14.0 NA 1 14 14
#> 3 l SLT va_preop 44.0 NA 1 44 44
#> 4 l SLT va_postop 48.0 NA 1 48 48
#> 5 r SLT iop_preop 23.5 0.7 2 23 24
#> 6 r SLT iop_postop 13.5 0.7 2 13 14
#> 7 r SLT va_preop 43.5 0.7 2 43 44
#> 8 r SLT va_postop 47.5 0.7 2 47 48
#> 9 l TE iop_preop 32.0 1.0 3 31 33
#> 10 l TE iop_postop 12.0 1.0 3 11 13
#> 11 l TE va_preop 42.0 1.0 3 41 43
#> 12 l TE va_postop 46.0 1.0 3 45 47
#> 13 r TE iop_preop 21.5 0.7 2 21 22
#> 14 r TE iop_postop 11.5 0.7 2 11 12
#> 15 r TE va_preop 41.5 0.7 2 41 42
#> 16 r TE va_postop 45.5 0.7 2 45 46
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.
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.
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 |
tidyverse
packages and the packages roxygen2
, usethis
, testthis
and devtools
, all on which eye
heavily relies.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.
Fasler, Katrin, Gabriella Moraes, Siegfried Wagner, Karsten U Kortuem, Reena Chopra, Livia Faes, Gabriella Preston, et al. 2019. “One- and Two-Year Visual Outcomes from the Moorfields Age-Related Macular Degeneration Database: A Retrospective Cohort Study and an Open Science Resource.” BMJ Open 9 (6). British Medical Journal Publishing Group. https://doi.org/10.1136/bmjopen-2018-027441.
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.