Missing Values

تقدم جوليا دعمًا لتمثيل القيم المفقودة بالمعنى الإحصائي. هذا لحالات حيث لا تتوفر قيمة لمتغير في ملاحظة، ولكن توجد قيمة صالحة نظريًا. يتم تمثيل القيم المفقودة عبر الكائن missing، وهو مثيل فردي من النوع Missing. missing تعادل NULL in SQL و NA in R، وتتصرف مثلها في معظم الحالات.

Propagation of Missing Values

تتسبب القيم المفقودة في انتشار تلقائي عند تمريرها إلى العمليات الرياضية القياسية والدوال. بالنسبة لهذه الدوال، فإن عدم اليقين بشأن قيمة أحد المعاملات يؤدي إلى عدم اليقين بشأن النتيجة. في الممارسة العملية، يعني ذلك أن عملية رياضية تتضمن قيمة مفقودة تعيد عمومًا قيمة مفقودة:

julia> missing + 1
missing

julia> "a" * missing
missing

julia> abs(missing)
missing

نظرًا لأن missing هو كائن عادي في جوليا، فإن قاعدة الانتشار هذه تعمل فقط على الوظائف التي اختارت تنفيذ هذا السلوك. يمكن تحقيق ذلك من خلال:

  • إضافة طريقة محددة معينة للوسائط من نوع Missing،
  • قبول الحجج من هذا النوع، وتمريرها إلى الدوال التي تنشرها (مثل العمليات الرياضية القياسية).

يجب على الحزم أن تأخذ في الاعتبار ما إذا كان من المنطقي تمرير القيم المفقودة عند تعريف وظائف جديدة، وأن تحدد الطرق بشكل مناسب إذا كان هذا هو الحال. تمرير قيمة missing إلى دالة لا تحتوي على طريقة تقبل وسائط من نوع Missing يؤدي إلى رمي MethodError، تمامًا كما هو الحال مع أي نوع آخر.

يمكن جعل الدوال التي لا تنشر قيم missing تنشرها عن طريق لفها في دالة passmissing المقدمة من حزمة Missings.jl. على سبيل المثال، f(x) تصبح passmissing(f)(x).

Equality and Comparison Operators

تتبع مشغلات المساواة والمقارنة القياسية قاعدة الانتشار المقدمة أعلاه: إذا كان أي من المعاملات مفقودًا، فإن النتيجة تكون مفقودة. إليك بعض الأمثلة:

julia> missing == 1
missing

julia> missing == missing
missing

julia> missing < 1
missing

julia> 2 >= missing
missing

بشكل خاص، لاحظ أن missing == missing يُرجع missing، لذا لا يمكن استخدام == لاختبار ما إذا كانت القيمة مفقودة. لاختبار ما إذا كان x مفقودًا، استخدم ismissing(x).

مشغلات المقارنة الخاصة isequal و === هي استثناءات لقواعد الانتشار. ستعيد دائمًا قيمة Bool، حتى في وجود قيم missing، مع اعتبار missing متساويًا مع missing ومختلفًا عن أي قيمة أخرى. يمكن استخدامها لذلك لاختبار ما إذا كانت القيمة missing:

julia> missing === 1
false

julia> isequal(missing, 1)
false

julia> missing === missing
true

julia> isequal(missing, missing)
true

المشغل isless هو استثناء آخر: تعتبر missing أكبر من أي قيمة أخرى. يتم استخدام هذا المشغل بواسطة sort!، الذي يضع بالتالي قيم missing بعد جميع القيم الأخرى:

julia> isless(1, missing)
true

julia> isless(missing, Inf)
false

julia> isless(missing, missing)
false

Logical operators

العوامل المنطقية (أو البوليانية) |، & و xor هي حالة خاصة أخرى لأنها تنقل فقط القيم المفقودة عندما يكون ذلك مطلوبًا منطقيًا. بالنسبة لهذه العوامل، سواء كانت النتيجة غير مؤكدة أم لا، يعتمد على العملية المحددة. يتبع هذا القواعد المعروفة جيدًا لـ three-valued logic التي يتم تنفيذها بواسطة، على سبيل المثال، NULL في SQL و NA في R. تتوافق هذه التعريفات المجردة مع سلوك طبيعي نسبيًا يمكن شرحه بشكل أفضل من خلال أمثلة ملموسة.

دعونا نوضح هذه المبدأ باستخدام عامل "أو" المنطقي |. وفقًا لقواعد المنطق البولياني، إذا كانت إحدى المعاملات true، فإن قيمة المعاملة الأخرى ليس لها تأثير على النتيجة، والتي ستكون دائمًا true:

julia> true | true
true

julia> true | false
true

julia> false | true
true

استنادًا إلى هذه الملاحظة، يمكننا أن نستنتج أنه إذا كان أحد المعاملين هو true والآخر مفقود، فإننا نعلم أن النتيجة هي true على الرغم من عدم اليقين بشأن القيمة الفعلية لأحد المعاملين. إذا كنا قد تمكنا من ملاحظة القيمة الفعلية للمعامل الثاني، فلا يمكن أن تكون إلا true أو false، وفي كلا الحالتين ستكون النتيجة true. لذلك، في هذه الحالة الخاصة، فإن عدم الوجود لا ينتشر:

julia> true | missing
true

julia> missing | true
true

على العكس، إذا كان أحد المعاملات هو false، يمكن أن تكون النتيجة إما true أو false اعتمادًا على قيمة المعامل الآخر. لذلك، إذا كان ذلك المعامل مفقودًا، يجب أن تكون النتيجة مفقودة أيضًا:

julia> false | true
true

julia> true | false
true

julia> false | false
false

julia> false | missing
missing

julia> missing | false
missing

سلوك عامل "و" المنطقي & مشابه لسلوك عامل |، مع الفرق أن عدم الوجود لا ينتشر عندما يكون أحد المعاملات false. على سبيل المثال، عندما يكون ذلك هو الحال بالنسبة للمعامل الأول:

julia> false & false
false

julia> false & true
false

julia> false & missing
false

من ناحية أخرى، تنتشر الفجوات عندما يكون أحد المعاملات true، على سبيل المثال الأول:

julia> true & true
true

julia> true & false
false

julia> true & missing
missing

أخيرًا، فإن عامل التشغيل المنطقي "أو الحصري" xor دائمًا ما ينشر القيم المفقودة، حيث أن كلا العاملين لهما تأثير دائمًا على النتيجة. كما يجب ملاحظة أن عامل النفي ! يُرجع المفقود عندما يكون العامل مفقودًا، تمامًا مثل غيره من العوامل الأحادية.

Control Flow and Short-Circuiting Operators

عوامل التحكم في التدفق بما في ذلك if، while و ternary operator x ? y : z لا تسمح بالقيم المفقودة. وذلك بسبب عدم اليقين بشأن ما إذا كانت القيمة الفعلية ستكون true أو false إذا كان بإمكاننا ملاحظتها. وهذا يعني أننا لا نعرف كيف يجب أن يتصرف البرنامج. في هذه الحالة، يتم رمي TypeError بمجرد مواجهة قيمة missing في هذا السياق:

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

للنفس السبب، وعلى عكس عوامل التشغيل المنطقية المقدمة أعلاه، فإن عوامل التشغيل البوليانية ذات التوقف القصير && و || لا تسمح بالقيم المفقودة في الحالات التي تحدد فيها قيمة المعامل ما إذا كان سيتم تقييم المعامل التالي أم لا. على سبيل المثال:

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

على العكس، لا يتم إلقاء أي خطأ عندما يمكن تحديد النتيجة دون القيم المفقودة. هذه هي الحالة عندما يتوقف الكود قبل تقييم المعامل المفقود، وعندما يكون المعامل المفقود هو الأخير:

julia> true && missing
missing

julia> false && missing
false

Arrays With Missing Values

يمكن إنشاء مصفوفات تحتوي على قيم مفقودة مثل المصفوفات الأخرى:

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

كما يظهر هذا المثال، نوع عنصر هذه المصفوفات هو Union{Missing, T}، حيث T هو نوع القيم غير المفقودة. وهذا يعكس حقيقة أن إدخالات المصفوفة يمكن أن تكون إما من نوع T (هنا، Int64) أو من نوع Missing. تستخدم هذه النوعية من المصفوفات تخزينًا فعالًا في الذاكرة يعادل Array{T} الذي يحمل القيم الفعلية مع Array{UInt8} تشير إلى نوع الإدخال (أي ما إذا كان Missing أو T).

يمكن إنشاء مصفوفات تسمح بالقيم المفقودة باستخدام الصيغة القياسية. استخدم Array{Union{Missing, T}}(missing, dims) لإنشاء مصفوفات مليئة بالقيم المفقودة:

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

استخدام undef أو similar قد يعطي حاليًا مصفوفة مليئة بـ missing، لكن هذه ليست الطريقة الصحيحة للحصول على مثل هذه المصفوفة. استخدم مُنشئ missing كما هو موضح أعلاه بدلاً من ذلك.

مصفوفة بنوع عنصر يسمح بإدخالات missing (مثل Vector{Union{Missing, T}}) والتي لا تحتوي على أي إدخالات missing يمكن تحويلها إلى نوع مصفوفة لا يسمح بإدخالات missing (مثل Vector{T}) باستخدام convert. إذا كانت المصفوفة تحتوي على قيم missing، يتم إلقاء MethodError أثناء التحويل:

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

نظرًا لأن القيم المفقودة تنتشر مع العمليات الرياضية القياسية، فإن دوال الاختزال تُرجع المفقودة عند استدعائها على مصفوفات تحتوي على قيم مفقودة:

julia> sum([1, missing])
missing

في هذه الحالة، استخدم دالة skipmissing لتخطي القيم المفقودة:

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

تُعيد هذه الدالة المساعدة مُكرِّرًا يقوم بتصفية القيم المفقودة بكفاءة. يمكن استخدامها بالتالي مع أي دالة تدعم المُكرِّرات:

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

يمكن فهرسة الكائنات التي تم إنشاؤها عن طريق استدعاء skipmissing على مصفوفة باستخدام الفهارس من المصفوفة الأصلية. الفهارس التي تتوافق مع القيم المفقودة ليست صالحة لهذه الكائنات، ويتم إلقاء خطأ عند محاولة استخدامها (كما يتم تخطيها أيضًا بواسطة keys و eachindex):

julia> x[1]
3

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

هذا يسمح للدوال التي تعمل على الفهارس بالعمل بالتزامن مع skipmissing. هذه هي الحالة بشكل ملحوظ لدوال البحث والعثور. تعيد هذه الدوال الفهارس الصالحة للكائن الذي تم إرجاعه بواسطة skipmissing، وهي أيضًا الفهارس للإدخالات المطابقة في المصفوفة الأصلية:

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

julia> findfirst(!iszero, x)
1

julia> argmax(x)
1

استخدم collect لاستخراج القيم غير المفقودة وتخزينها في مصفوفة:

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

Logical Operations on Arrays

تستخدم المنطق الثلاثي الموضح أعلاه لمشغلات المنطق أيضًا بواسطة الدوال المنطقية المطبقة على المصفوفات. وبالتالي، فإن اختبارات المساواة للمصفوفات باستخدام مشغل == تعيد missing كلما لم يكن بالإمكان تحديد النتيجة دون معرفة القيمة الفعلية للمدخل missing. في الممارسة العملية، يعني هذا أنه يتم إرجاع missing إذا كانت جميع القيم غير المفقودة في المصفوفتين المقارنتين متساوية، ولكن تحتوي واحدة أو كلتا المصفوفتين على قيم مفقودة (ربما في مواضع مختلفة):

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

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

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

بالنسبة للقيم الفردية، استخدم isequal لمعالجة القيم المفقودة على أنها متساوية مع قيم المفقودة الأخرى، ولكنها مختلفة عن القيم غير المفقودة:

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

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

الدوال any و all تتبع أيضًا قواعد المنطق الثلاثي القيم. وبالتالي، تعيد missing عندما لا يمكن تحديد النتيجة:

julia> all([true, missing])
missing

julia> all([false, missing])
false

julia> any([true, missing])
true

julia> any([false, missing])
missing