Missing Values

Julia proporciona soporte para representar valores faltantes en el sentido estadístico. Esto es para situaciones en las que no hay un valor disponible para una variable en una observación, pero teóricamente existe un valor válido. Los valores faltantes se representan a través del objeto missing, que es la instancia singleton del tipo Missing. missing es equivalente a NULL in SQL y NA in R, y se comporta como ellos en la mayoría de las situaciones.

Propagation of Missing Values

Los valores missing se propagan automáticamente cuando se pasan a operadores y funciones matemáticas estándar. Para estas funciones, la incertidumbre sobre el valor de uno de los operandos induce incertidumbre sobre el resultado. En la práctica, esto significa que una operación matemática que involucra un valor missing generalmente devuelve missing:

julia> missing + 1
missing

julia> "a" * missing
missing

julia> abs(missing)
missing

Dado que missing es un objeto normal de Julia, esta regla de propagación solo funciona para funciones que han optado por implementar este comportamiento. Esto se puede lograr mediante:

  • agregando un método específico definido para argumentos de tipo Missing,
  • aceptando argumentos de este tipo y pasándolos a funciones que los propagan (como los operadores matemáticos estándar).

Los paquetes deben considerar si tiene sentido propagar valores faltantes al definir nuevas funciones y definir los métodos apropiadamente si este es el caso. Pasar un valor missing a una función que no tiene un método que acepte argumentos del tipo Missing genera un MethodError, al igual que para cualquier otro tipo.

Las funciones que no propagan valores missing pueden hacerse para que lo hagan envolviéndolas en la función passmissing proporcionada por el paquete Missings.jl. Por ejemplo, f(x) se convierte en passmissing(f)(x).

Equality and Comparison Operators

Los operadores de igualdad y comparación estándar siguen la regla de propagación presentada anteriormente: si alguno de los operandos está faltante, el resultado es faltante. Aquí hay algunos ejemplos:

julia> missing == 1
missing

julia> missing == missing
missing

julia> missing < 1
missing

julia> 2 >= missing
missing

En particular, ten en cuenta que missing == missing devuelve missing, por lo que == no se puede usar para probar si un valor está ausente. Para probar si x está missing, utiliza ismissing(x).

Los operadores de comparación especiales isequal y === son excepciones a la regla de propagación. Siempre devolverán un valor Bool, incluso en presencia de valores missing, considerando missing como igual a missing y diferente de cualquier otro valor. Por lo tanto, se pueden utilizar para probar si un valor es missing:

julia> missing === 1
false

julia> isequal(missing, 1)
false

julia> missing === missing
true

julia> isequal(missing, missing)
true

El operador isless es otra excepción: missing se considera mayor que cualquier otro valor. Este operador es utilizado por sort!, que por lo tanto coloca los valores missing después de todos los demás valores:

julia> isless(1, missing)
true

julia> isless(missing, Inf)
false

julia> isless(missing, missing)
false

Logical operators

Los operadores lógicos (o booleanos) |, & y xor son otro caso especial ya que solo propagan valores missing cuando es lógicamente necesario. Para estos operadores, si el resultado es incierto o no, depende de la operación particular. Esto sigue las reglas bien establecidas de three-valued logic, que están implementadas por ejemplo en NULL en SQL y NA en R. Esta definición abstracta corresponde a un comportamiento relativamente natural que se explica mejor a través de ejemplos concretos.

Ilustremos este principio con el operador lógico "o" |. Siguiendo las reglas de la lógica booleana, si uno de los operandos es true, el valor del otro operando no tiene influencia en el resultado, que siempre será true:

julia> true | true
true

julia> true | false
true

julia> false | true
true

Basado en esta observación, podemos concluir que si uno de los operandos es true y el otro es missing, sabemos que el resultado es true a pesar de la incertidumbre sobre el valor real de uno de los operandos. Si hubiéramos podido observar el valor real del segundo operando, solo podría ser true o false, y en ambos casos el resultado sería true. Por lo tanto, en este caso particular, la falta de datos no se propaga:

julia> true | missing
true

julia> missing | true
true

Por el contrario, si uno de los operandos es false, el resultado podría ser true o false dependiendo del valor del otro operando. Por lo tanto, si ese operando está missing, el resultado también tiene que ser missing:

julia> false | true
true

julia> true | false
true

julia> false | false
false

julia> false | missing
missing

julia> missing | false
missing

El comportamiento del operador lógico "y" & es similar al del operador |, con la diferencia de que la falta de datos no se propaga cuando uno de los operandos es false. Por ejemplo, cuando ese es el caso del primer operando:

julia> false & false
false

julia> false & true
false

julia> false & missing
false

Por otro lado, la falta de datos se propaga cuando uno de los operandos es true, por ejemplo, el primero:

julia> true & true
true

julia> true & false
false

julia> true & missing
missing

Finalmente, el operador lógico "o exclusivo" xor siempre propaga valores missing, ya que ambos operandos siempre tienen un efecto en el resultado. También ten en cuenta que el operador de negación ! devuelve missing cuando el operando es missing, al igual que otros operadores unarios.

Control Flow and Short-Circuiting Operators

Los operadores de control de flujo, incluyendo if, while y el ternary operator x ? y : z, no permiten valores faltantes. Esto se debe a la incertidumbre sobre si el valor real sería true o false si pudiéramos observarlo. Esto implica que no sabemos cómo debería comportarse el programa. En este caso, se lanza un TypeError tan pronto como se encuentra un valor missing en este contexto:

julia> if missing
           println("here")
       end
ERROR: TypeError: non-boolean (Missing) used in boolean context

Por la misma razón, a diferencia de los operadores lógicos presentados anteriormente, los operadores booleanos de cortocircuito && y || no permiten valores missing en situaciones donde el valor del operando determina si el siguiente operando se evalúa o no. Por ejemplo:

julia> missing || false
ERROR: TypeError: non-boolean (Missing) used in boolean context

julia> missing && false
ERROR: TypeError: non-boolean (Missing) used in boolean context

julia> true && missing && false
ERROR: TypeError: non-boolean (Missing) used in boolean context

En contraste, no se lanza ningún error cuando el resultado se puede determinar sin los valores missing. Este es el caso cuando el código se interrumpe antes de evaluar el operando missing, y cuando el operando missing es el último:

julia> true && missing
missing

julia> false && missing
false

Arrays With Missing Values

Los arreglos que contienen valores faltantes se pueden crear como otros arreglos:

julia> [1, missing]
2-element Vector{Union{Missing, Int64}}:
 1
  missing

Como muestra este ejemplo, el tipo de elemento de tales arreglos es Union{Missing, T}, siendo T el tipo de los valores no faltantes. Esto refleja el hecho de que las entradas del arreglo pueden ser de tipo T (aquí, Int64) o de tipo Missing. Este tipo de arreglo utiliza un almacenamiento de memoria eficiente equivalente a un Array{T} que contiene los valores reales combinado con un Array{UInt8} que indica el tipo de la entrada (es decir, si es Missing o T).

Los arreglos que permiten valores faltantes se pueden construir con la sintaxis estándar. Usa Array{Union{Missing, T}}(missing, dims) para crear arreglos llenos de valores faltantes:

julia> Array{Union{Missing, String}}(missing, 2, 3)
2×3 Matrix{Union{Missing, String}}:
 missing  missing  missing
 missing  missing  missing
Note

Usar undef o similar puede actualmente dar un array lleno de missing, pero esta no es la forma correcta de obtener tal array. Usa un constructor de missing como se mostró arriba en su lugar.

Un arreglo con un tipo de elemento que permite entradas missing (por ejemplo, Vector{Union{Missing, T}}) que no contiene ninguna entrada missing se puede convertir a un tipo de arreglo que no permite entradas missing (por ejemplo, Vector{T}) utilizando convert. Si el arreglo contiene valores missing, se lanza un MethodError durante la conversión:

julia> x = Union{Missing, String}["a", "b"]
2-element Vector{Union{Missing, String}}:
 "a"
 "b"

julia> convert(Array{String}, x)
2-element Vector{String}:
 "a"
 "b"

julia> y = Union{Missing, String}[missing, "b"]
2-element Vector{Union{Missing, String}}:
 missing
 "b"

julia> convert(Array{String}, y)
ERROR: MethodError: Cannot `convert` an object of type Missing to an object of type String

Skipping Missing Values

Dado que los valores missing se propagan con los operadores matemáticos estándar, las funciones de reducción devuelven missing cuando se llaman en arreglos que contienen valores faltantes:

julia> sum([1, missing])
missing

En esta situación, utiliza la función skipmissing para omitir valores faltantes:

julia> sum(skipmissing([1, missing]))
1

Esta función de conveniencia devuelve un iterador que filtra eficientemente los valores missing. Por lo tanto, se puede utilizar con cualquier función que soporte iteradores:

julia> x = skipmissing([3, missing, 2, 1])
skipmissing(Union{Missing, Int64}[3, missing, 2, 1])

julia> maximum(x)
3

julia> sum(x)
6

julia> mapreduce(sqrt, +, x)
4.146264369941973

Los objetos creados al llamar a skipmissing en un arreglo se pueden indexar utilizando índices del arreglo padre. Los índices que corresponden a valores faltantes no son válidos para estos objetos, y se lanza un error al intentar usarlos (también se omiten en keys y eachindex):

julia> x[1]
3

julia> x[2]
ERROR: MissingException: the value at index (2,) is missing
[...]

Esto permite que las funciones que operan sobre índices funcionen en combinación con skipmissing. Este es el caso notable de las funciones de búsqueda y localización. Estas funciones devuelven índices válidos para el objeto devuelto por skipmissing, y también son los índices de las entradas coincidentes en el array padre:

julia> findall(==(1), x)
1-element Vector{Int64}:
 4

julia> findfirst(!iszero, x)
1

julia> argmax(x)
1

Utiliza collect para extraer valores no faltantes y almacenarlos en un arreglo:

julia> collect(x)
3-element Vector{Int64}:
 3
 2
 1

Logical Operations on Arrays

La lógica de tres valores descrita anteriormente para los operadores lógicos también se utiliza en funciones lógicas aplicadas a arreglos. Así, las pruebas de igualdad de arreglos utilizando el operador == devuelven missing siempre que el resultado no se pueda determinar sin conocer el valor real de la entrada missing. En la práctica, esto significa que se devuelve missing si todos los valores no faltantes de los arreglos comparados son iguales, pero uno o ambos arreglos contienen valores faltantes (posiblemente en diferentes posiciones):

julia> [1, missing] == [2, missing]
false

julia> [1, missing] == [1, missing]
missing

julia> [1, 2, missing] == [1, missing, 2]
missing

En cuanto a los valores individuales, utiliza isequal para tratar los valores missing como iguales a otros valores missing, pero diferentes de los valores no missing:

julia> isequal([1, missing], [1, missing])
true

julia> isequal([1, 2, missing], [1, missing, 2])
false

Las funciones any y all también siguen las reglas de la lógica de tres valores. Por lo tanto, devuelven missing cuando el resultado no puede ser determinado:

julia> all([true, missing])
missing

julia> all([false, missing])
false

julia> any([true, missing])
true

julia> any([false, missing])
missing