7  استيراد البيانات

7.1 مقدمة

يعد العمل مع البيانات التي توفرها حزم R طريقة رائعة لتعلم أدوات علم البيانات، ولكنك سترغب في مرحلة ما في تطبيق ما تعلمته على بياناتك الخاصة. في هذا الفصل، ستتعلم أساسيات قراءة ملفات البيانات واستيرادها إلى بيئة R.

تحديداً، سينصب تركيز هذا الفصل على قراءة الملفات النصية البسيطة ذات البنية المستطيلة (Plain-text rectangular files). سنبدأ بتقديم نصائح عملية للتعامل مع خصائص البيانات مثل أسماء الأعمدة، وأنواع المتغيرات، والبيانات المفقودة (Missing data). بعد ذلك، ستتعلم كيفية قراءة البيانات من ملفات متعددة في وقت واحد، وكيفية كتابة وتصدير البيانات من R إلى ملف خارجي. أخيراً، ستتعلم كيفية إنشاء وتشكيل أطر البيانات (Data frames) يدوياً داخل R.

7.1.1 المتطلبات المسبقة

في هذا الفصل، ستتعلم كيفية تحميل الملفات المسطحة (Flat files) في R باستخدام حزمة readr، والتي تعد جزءاً أساسياً من المنظومة المركزية لـ tidyverse.

7.2 قراءة البيانات من ملف

للبدء، سنركز على النوع الأكثر شيوعاً لملفات البيانات المستطيلة: ملفات CSV، وهو اختصار لـ (Comma-separated values) أي القيم المفصولة بفاصلة. إليك كيف تبدو بنية ملف CSV بسيط. السطر الأول، والذي يُطلق عليه عادةً سطر العنوان (Header row)، يحدد أسماء الأعمدة، بينما توفر الأسطر الستة التالية البيانات الصغيرة الفعليّة. يتم الفصل بين الأعمدة بواسطة الفواصل.

Student ID,Full Name,favourite.food,mealPlan,AGE
1,Sunil Huffmann,Strawberry yoghurt,Lunch only,4
2,Barclay Lynn,French fries,Lunch only,5
3,Jayendra Lyne,N/A,Breakfast and lunch,7
4,Leon Rossini,Anchovies,Lunch only,
5,Chidiegwu Dunkel,Pizza,Breakfast and lunch,five
6,Güvenç Attila,Ice cream,Lunch only,6

يعرض الجدول 7.1 تمثيلاً لنفس هذه البيانات في شكل جدول منسق.

الجدول 7.1: بيانات من ملف students.csv على شكل جدول.
Student ID Full Name favourite.food mealPlan AGE
1 Sunil Huffmann Strawberry yoghurt Lunch only 4
2 Barclay Lynn French fries Lunch only 5
3 Jayendra Lyne N/A Breakfast and lunch 7
4 Leon Rossini Anchovies Lunch only NA
5 Chidiegwu Dunkel Pizza Breakfast and lunch five
6 Güvenç Attila Ice cream Lunch only 6

يمكننا قراءة هذا الملف واستيراده إلى R باستخدام الدالة read_csv(). الوسيط الأول هو الأهم على الإطلاق: مسار الملف (Path). يمكنك التفكير في المسار على أنه عنوان الملف؛ فالملف يُسمى students.csv ويقع داخل مجلد data.

students <- read_csv("data/students.csv")
#> Rows: 6 Columns: 5
#> ── Column specification ─────────────────────────────────────────────────────
#> Delimiter: ","
#> chr (4): Full Name, favourite.food, mealPlan, AGE
#> dbl (1): Student ID
#> 
#> ℹ Use `spec()` to retrieve the full column specification for this data.
#> ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

سيعمل الكود أعلاه بنجاح إذا كان ملف students.csv موجوداً بالفعل داخل مجلد data في مشروعك الحالي. يمكنك تحميل ملف students.csv من الرابط https://pos.it/r4ds-students-csv أو يمكنك قراءته مباشرة من ذلك الرابط عبر الإنترنت باستخدام:

students <- read_csv("https://pos.it/r4ds-students-csv")

عند تشغيل الدالة read_csv()، فإنها تطبع رسالة تخبرك بعدد صفوف وأعمدة البيانات، والمحدد (Delimiter) المستخدم، ومواصفات الأعمدة (أسماء الأعمدة مصنفة حسب نوع البيانات الذي يحتويه كل عمود). كما تطبع أيضاً بعض المعلومات حول كيفية استرداد مواصفات الأعمدة الكاملة وكيفية كتم هذه الرسالة. تعد هذه الرسالة جزءاً لا يتجزأ من آلية عمل readr، وسنعود إليها بالتفصيل في قسم 7.3.

7.2.1 نصائح عملية

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

students
#> # A tibble: 6 × 5
#>   `Student ID` `Full Name`      favourite.food     mealPlan            AGE  
#>          <dbl> <chr>            <chr>              <chr>               <chr>
#> 1            1 Sunil Huffmann   Strawberry yoghurt Lunch only          4    
#> 2            2 Barclay Lynn     French fries       Lunch only          5    
#> 3            3 Jayendra Lyne    N/A                Breakfast and lunch 7    
#> 4            4 Leon Rossini     Anchovies          Lunch only          <NA> 
#> 5            5 Chidiegwu Dunkel Pizza              Breakfast and lunch five 
#> 6            6 Güvenç Attila    Ice cream          Lunch only          6

في العمود المسمى favourite.food، توجد مجموعة من الأطعمة، ثم تظهر السلسلة النصية N/A، والتي كان ينبغي أن تكون قيم مفقودة حقيقية NA حتى يتعرف عليها R كقيمة “غير متاحة” (not available). هذا أمر يمكننا معالجته وتصحيحه باستخدام الوسيط na. بشكل افتراضي، لا تتعرف الدالة read_csv() في هذه البيانات إلا على السلاسل النصية الفارغة ("") كقيم مفقودة NA، ونحن نريدها أن تتعرف أيضاً على السلسلة النصية "N/A" وتعتبرها قيمة مفقودة.

students <- read_csv("data/students.csv", na = c("N/A", ""))

students
#> # A tibble: 6 × 5
#>   `Student ID` `Full Name`      favourite.food     mealPlan            AGE  
#>          <dbl> <chr>            <chr>              <chr>               <chr>
#> 1            1 Sunil Huffmann   Strawberry yoghurt Lunch only          4    
#> 2            2 Barclay Lynn     French fries       Lunch only          5    
#> 3            3 Jayendra Lyne    <NA>               Breakfast and lunch 7    
#> 4            4 Leon Rossini     Anchovies          Lunch only          <NA> 
#> 5            5 Chidiegwu Dunkel Pizza              Breakfast and lunch five 
#> 6            6 Güvenç Attila    Ice cream          Lunch only          6

قد تلاحظ أيضاً أن العمودين Student ID و Full Name محاطان بعلامات اقتباس مائلة خلفية (Backticks). السبب في ذلك هو أنهما يحتويان على مسافات فاصلة، مما يكسر قواعد R المعتادة لتسمية المتغيرات؛ وهي ما نسميها بـ الأسماء غير المتوافقة مع قواعد الصياغة (Non-syntactic names). وللإشارة إلى هذه المتغيرات واستدعائها، ستحتاج إلى إحاطتها بعلامات الاقتباس المائلة الخلفية هكذا `:

students |> 
  rename(
    student_id = `Student ID`,
    full_name = `Full Name`
  )
#> # A tibble: 6 × 5
#>   student_id full_name        favourite.food     mealPlan            AGE  
#>        <dbl> <chr>            <chr>              <chr>               <chr>
#> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only          4    
#> 2          2 Barclay Lynn     French fries       Lunch only          5    
#> 3          3 Jayendra Lyne    <NA>               Breakfast and lunch 7    
#> 4          4 Leon Rossini     Anchovies          Lunch only          <NA> 
#> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch five 
#> 6          6 Güvenç Attila    Ice cream          Lunch only          6

هناك أسلوب بديل يتمثل في استخدام الدالة janitor::clean_names() والتي تعتمد على خوارزميات تقريبية لتحويل جميع الأسماء إلى نمط snake case دفعة واحدة1.

students |> janitor::clean_names()
#> # A tibble: 6 × 5
#>   student_id full_name        favourite_food     meal_plan           age  
#>        <dbl> <chr>            <chr>              <chr>               <chr>
#> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only          4    
#> 2          2 Barclay Lynn     French fries       Lunch only          5    
#> 3          3 Jayendra Lyne    <NA>               Breakfast and lunch 7    
#> 4          4 Leon Rossini     Anchovies          Lunch only          <NA> 
#> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch five 
#> 6          6 Güvenç Attila    Ice cream          Lunch only          6

من المهام الشائعة الأخرى بعد استيراد البيانات هي مراجعة أنواع المتغيرات وتدقيقها. على سبيل المثال، يعد المتغير meal_plan متغيراً فئوياً (Categorical variable) يحتوي على مجموعة محددة ومعروفة من القيم الممكنة، وبالتالي يفضل تمثيله في بيئة R كعامل (Factor):

students |>
  janitor::clean_names() |>
  mutate(meal_plan = factor(meal_plan))
#> # A tibble: 6 × 5
#>   student_id full_name        favourite_food     meal_plan           age  
#>        <dbl> <chr>            <chr>              <fct>               <chr>
#> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only          4    
#> 2          2 Barclay Lynn     French fries       Lunch only          5    
#> 3          3 Jayendra Lyne    <NA>               Breakfast and lunch 7    
#> 4          4 Leon Rossini     Anchovies          Lunch only          <NA> 
#> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch five 
#> 6          6 Güvenç Attila    Ice cream          Lunch only          6

لاحظ أن القيم الفعلية في المتغير meal_plan ظلت كما هي دون تغيير، ولكن نوع المتغير المكتوب أسفل اسم العمود قد تغير من سلسلة نصية (<chr>) إلى عامل فئوي (<fct>). ستتعلم المزيد عن العوامل الفئوية في الفصل 16.

قبل أن تقوم بتحليل هذه البيانات، من المرجح أنك سترغب في إصلاح العمود age. حالياً، يُعامل العمر كمتغير نصي لأن إحدى المشاهدات قد كُتبت نصياً ككلمة five بدلاً من الرقم 5. سناقش تفاصيل حل هذه المشكلة الإدخالية في الفصل 20.

students <- students |>
  janitor::clean_names() |>
  mutate(
    meal_plan = factor(meal_plan),
    age = parse_number(if_else(age == "five", "5", age))
  )

students
#> # A tibble: 6 × 5
#>   student_id full_name        favourite_food     meal_plan             age
#>        <dbl> <chr>            <chr>              <fct>               <dbl>
#> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only              4
#> 2          2 Barclay Lynn     French fries       Lunch only              5
#> 3          3 Jayendra Lyne    <NA>               Breakfast and lunch     7
#> 4          4 Leon Rossini     Anchovies          Lunch only             NA
#> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch     5
#> 6          6 Güvenç Attila    Ice cream          Lunch only              6

الدالة الجديدة هنا هي if_else()، والتي تأخذ ثلاث وسيطات أساسية. الوسيط الأول test يجب أن يكون متجهاً منطقياً (Logical vector). وستحتوي النتيجة على قيمة الوسيط الثاني yes عندما يكون الشرط test هو TRUE، وعلى قيمة الوسيط الثالث no عندما يكون الشرط FALSE. هنا نقوم بالصياغة التالية: إذا كان العمر age يساوي السلسلة النصية "five" فاجعله "5"، وإذا لم يكن كذلك فاتركه كما هو دون تغيير. ستتعلم المزيد عن الدالة if_else() والمتجهات المنطقية في الفصل 12.

7.2.2 وسيطات أخرى (Other arguments)

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

read_csv(
  "a,b,c
  1,2,3
  4,5,6"
)
#> Warning: The `file` argument of `read_csv()` should use `I()` for literal data as of
#> readr 2.2.0.
#>   
#>   # Bad (for example):
#>   read_csv("x,y\n1,2")
#>   
#>   # Good:
#>   read_csv(I("x,y\n1,2"))
#> # A tibble: 2 × 3
#>       a     b     c
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3
#> 2     4     5     6

في العادة، تستخدم الدالة read_csv() السطر الأول من البيانات كـ أسماء للأعمدة، وهو العرف الأكثر شيوعاً في بناء الملفات. ولكن ليس من الغريب أن تتضمن الملفات بضعة أسطر من البيانات الوصفية (Metadata) في أعلى الملف. يمكنك استخدام الأمر skip = n لتخطي أول n من الأسطر، أو استخدام الأمر comment = "#" لإسقاط وحذف جميع الأسطر التي تبدأ برمز الهاشتاج (على سبيل المثال) #:

read_csv(
  "The first line of metadata
  The second line of metadata
  x,y,z
  1,2,3",
  skip = 2
)
#> # A tibble: 1 × 3
#>       x     y     z
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3

read_csv(
  "# A comment I want to skip
  x,y,z
  1,2,3",
  comment = "#"
)
#> # A tibble: 1 × 3
#>       x     y     z
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3

في حالات أخرى، قد لا تحتوي البيانات على أسماء للأعمدة في السطر الأول من الأساس. يمكنك استخدام الأمر col_names = FALSE لإخبار الدالة read_csv() بعدم معاملة السطر الأول كعناوين، وبدلاً من ذلك سيتم تسميتها تتابعاً من X1 إلى Xn:

read_csv(
  "1,2,3
  4,5,6",
  col_names = FALSE
)
#> # A tibble: 2 × 3
#>      X1    X2    X3
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3
#> 2     4     5     6

كبديل لذلك، يمكنك تمرير متجه نصي (Character vector) إلى col_names ليتم استخدامه مباشرة كـ أسماء رسمية للأعمدة الحالية:

read_csv(
  "1,2,3
  4,5,6",
  col_names = c("x", "y", "z")
)
#> # A tibble: 2 × 3
#>       x     y     z
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3
#> 2     4     5     6

هذه الوسيطات هي كل ما تحتاج إلى معرفته لقراءة الغالبية العظمى من ملفات CSV التي ستصادفها في ممارستك العملية وتحليلاتك البحثية. (أما بالنسبة للحالات الاستثنائية الأخرى، فستحتاج إلى فحص ملف .csv الخاص بك بعناية وقراءة وثائق المساعدة والدعم المخصصة لدالة read_csv() للاطلاع على وسيطاتها العديدة الأخرى).

7.2.3 أنواع الملفات الأخرى

بمجرد إتقانك لاستخدام الدالة read_csv()، يصبح استخدام دوال حزمة readr الأخرى أمراً غاية في السهولة والمباشرة؛ فالمسألة لا تتعدى معرفة الدالة المناسبة التي يتعين عليك الاستعانة بها:

  • read_csv2() تقرأ الملفات التي تفصل بين حقولها الفاصلة المنقوطة. تستخدم هذه الملفات العلامة ; بدلاً من الفاصلة , للفصل بين الحقول، وهي شائعة الاستخدام في الدول التي تعتمد الفاصلة , كعلامة عشرية.

  • read_tsv() تقرأ الملفات المفصولة بمسافات جدولة (tab-delimited).

  • read_delim() تقرأ الملفات بأي محدد فاصِل (Delimiter)، وتحاول تخمين المحدد تلقائياً إذا لم تقم بتحديده.

  • read_fwf() تقرأ ملفات العرض الثابت (Fixed-width). ويمكنك تحديد الحقول إما عن طريق عرضها باستخدام fwf_widths() أو عن طريق مواضعها باستخدام fwf_positions().

  • read_table() تقرأ صيغة شائعة من ملفات العرض الثابت حيث يتم الفصل بين الأعمدة بواسطة مسافات بيضاء (White space).

  • read_log() تقرأ ملفات سجلات النظام المصممة بنمط (Apache-style log).

7.2.4 تمارين

  1. ما هي الدالة التي ستستخدمها لقراءة ملف يتم الفصل بين حقوله باستخدام الرمز “|”؟

  2. بالإضافة إلى الوسيطات file و skip و comment، ما هي الوسيطات الأخرى المشتركة بين الدالتين read_csv() و read_tsv()؟

  3. ما هي الوسيطات الأكثر أهمية بالنسبة للدالة read_fwf()؟

  4. في بعض الأحيان، تحتوي السلاسل النصية داخل ملف CSV على فواصل. ولتجنب تسببها في حدوث مشكلات في القراءة، يجب إحاطتها بعلامة اقتباس، مثل " أو '. بشكل افتراضي، تفترض الدالة read_csv() أن علامة الاقتباس ستكون ". لقراءة النص التالي وتحويله إلى إطار بيانات (Data frame)، ما هو الوسيط الذي تحتاج إلى تحديده في الدالة read_csv()؟

    "x,y\n1,'a,b'"
  5. حدد الخلل أو الخطأ في كل من ملفات CSV المضمنة التالية. وماذا يحدث عند تشغيل الكود؟

    read_csv("a,b\n1,2,3\n4,5,6")
    read_csv("a,b,c\n1,2\n1,2,3,4")
    read_csv("a,b\n\"1")
    read_csv("a,b\n1,2\na,b")
    read_csv("a;b\n1;3")
  6. تدرب على الإشارة إلى الأسماء غير المتوافقة مع قواعد الصياغة (Non-syntactic names) في إطار البيانات التالي عن طريق:

    أ. استخراج المتغير المسمى 1.
    ب. رسم مخطط التشتت (Scatterplot) يمثل المتغير 1 مقابل المتغير 2.
    ج. إنشاء عمود جديد يسمى 3، وهو ناتج قسمة المتغير 2 على المتغير 1.
    د. إعادة تسمية الأعمدة إلى one و two و three.

    annoying <- tibble(
      `1` = 1:10,
      `2` = `1` * 2 + rnorm(length(`1`))
    )

7.3 التحكم في أنواع الأعمدة

لا يحتوي ملف CSV على أي معلومات حول نوع كل متغير (أي ما إذا كان منطقياً، أو رقمياً، أو نصياً، إلخ)، ولذلك ستحاول حزمة readr تخمين النوع تلقائياً. يصف هذا القسم كيفية عمل عملية التخمين، وكيفية حل بعض المشكلات الشائعة التي تؤدي إلى فشلها، وكيفية تحديد أنواع الأعمدة بنفسك إذا دعت الحاجة. أخيراً، سنذكر بضع استراتيجيات عامة ومفيدة إذا فشلت readr فشلاً ذريعاً وكنت بحاجة إلى الحصول على نظرة أعمق لبنية ملفك وتكوينه.

7.3.1 تخمين الأنواع

تستخدم readr آلية تقريبية (Heuristic) لتحديد أنواع الأعمدة. لكل عمود، تقوم الحزمة بسحب قيم 1000 صف2 متباعدة بشكل متساوٍ من الصف الأول إلى الصف الأخير، مع تجاهل القيم المفقودة. ثم تبدأ في فحص الأسئلة التالية تتابعاً:

  • هل يحتوي العمود فقط على F أو T أو FALSE أو TRUE (مع تجاهل حالة الأحرف)؟ إذا كان الأمر كذلك، فهو متغير منطقي (Logical).
  • هل يحتوي فقط على أرقام (مثل 1 أو -4.5 أو 5e6 أو Inf)؟ إذا كان الأمر كذلك، فهو متغير عددي (Number).
  • هل يتطابق مع معيار ISO8601 الدولي؟ إذا كان الأمر كذلك، فهو تاريخ (Date) أو تاريخ ووقت (Date-time). (سنعود إلى تفاصيل التواريخ والأوقات بمزيد من التفصيل في قسم 17.2).
  • خلاف ذلك، لا بد أن يكون المتغير سلسلة نصية (String).

يمكنك رؤية هذا السلوك عملياً في هذا المثال البسيط:

read_csv("
  logical,numeric,date,string
  TRUE,1,2021-01-15,abc
  false,4.5,2021-02-15,def
  T,Inf,2021-02-16,ghi
")
#> # A tibble: 3 × 4
#>   logical numeric date       string
#>   <lgl>     <dbl> <date>     <chr> 
#> 1 TRUE        1   2021-01-15 abc   
#> 2 FALSE       4.5 2021-02-15 def   
#> 3 TRUE      Inf   2021-02-16 ghi

تعمل هذه الآلية التقريبية بشكل ممتاز إذا كان لديك مجموعة بيانات نظيفة، ولكن في الحياة الواقعية والعملية، ستواجه باقة متنوعة من حالات الفشل الغريبة والمثيرة.

7.3.2 القيم المفقودة، أنواع الأعمدة، والمشكلات

الطريقة الأكثر شيوعاً لفشل الكشف التلقائي عن نوع العمود هي احتواء العمود على قيم غير متوقعة، مما يترتب عليه ظهور عمود نصي (character) بدلاً من نوع أكثر تحديداً وملائمة لطبيعة المتغير. وأحد أكثر الأسباب شيوعاً لحدوث ذلك هو وجود قيمة مفقودة تم تسجيلها باستخدام رمز آخر غير الرمز NA الذي تتوقعه حزمة readr عادةً.

خذ ملف CSV البسيط المكون من عمود واحد كمثال:

simple_csv <- "
  x
  10
  .
  20
  30"

إذا قمنا بقراءته دون أي وسيطات إضافية، يتحول x إلى عمود نصي:

read_csv(simple_csv)
#> # A tibble: 4 × 1
#>   x    
#>   <chr>
#> 1 10   
#> 2 .    
#> 3 20   
#> 4 30

في هذه الحالة الصغيرة جداً، يمكنك بسهولة رؤية القيمة المفقودة الممثلة بالنقطة .. ولكن ماذا يحدث إذا كان لديك آلاف الصفوف مع وجود بضع قيم مفقودة فقط ممثلة بنقاط . منثورة بينها؟ أحد الأساليب للتعامل مع هذا هو إخبار readr بأن x هو عمود عددي، ثم تتبع الموضع الذي يفشل فيه الكود لمعرفة السبب. يمكنك القيام بذلك باستخدام الوسيط col_types، والذي يأخذ قائمة مسماة (Named list) تتطابق فيها الأسماء مع أسماء الأعمدة في ملف CSV:

df <- read_csv(
  simple_csv, 
  col_types = list(x = col_double())
)
#> Warning: One or more parsing issues, call `problems()` on your data frame for
#> details, e.g.:
#>   dat <- vroom(...)
#>   problems(dat)

الآن تفيد الدالة read_csv() بوجود مشكلة (Problem)، وتخبرنا أنه يمكننا معرفة المزيد من التفاصيل باستخدام الدالة problems():

problems(df)
#> # A tibble: 1 × 5
#>     row   col expected actual file                                          
#>   <int> <int> <chr>    <chr>  <chr>                                         
#> 1     3     1 a double .      /tmp/RtmpsFZbxE/vroom-chr-to-file-20932fb2db15

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

read_csv(simple_csv, na = ".")
#> # A tibble: 4 × 1
#>       x
#>   <dbl>
#> 1    10
#> 2    NA
#> 3    20
#> 4    30

7.3.3 أنواع الأعمدة

توفر حزمة readr ما مجموعه تسعة أنواع من الأعمدة لتختار من بينها واستخدامها:

  • col_logical() و col_double() لقراءة القيم المنطقية والأعداد الحقيقية. ونادراً ما تحتاج إليها (إلا في الحالات المذكورة أعلاه)، نظرًا لأن readr ستخمنها بالنيابة عنك في العادة.
  • col_integer() لقراءة الأعداد الصحيحة. ونحن نادراً ما نفرق بين الأعداد الصحيحة (Integers) والأعداد العشرية الحقيقية (Doubles) في هذا الكتاب لأنها متكافئة وظيفياً، ولكن قراءة الأعداد الصحيحة صراحةً قد تكون مفيدة أحياناً لأنها تشغل نصف المساحة التي تستهلكها الأعداد العشرية في الذاكرة العشوائية.
  • col_character() لقراءة السلاسل النصية. وقد يكون من المفيد تحديد ذلك صراحةً عندما يكون لديك عمود يمثل معرفاً رقمياً (Numeric identifier)، أي سلسلة طويلة من الأرقام التي تحدد هوية كائن ما ولكن لا معنى لتطبيق العمليات الحسابية عليها؛ ومن الأمثلة على ذلك: أرقام الهواتف، وأرقام البطاقات الشخصية، وأرقام بطاقات الائتمان، إلخ.
  • col_factor() و col_date() و col_datetime() لإنشاء العوامل الفئوية، والتواريخ، والتواريخ الملحقة بالوقت على التوالي؛ وستتعلم المزيد عنها عندما نصل إلى فصول أنواع البيانات تلك في الفصل 16 و الفصل 17.
  • col_number() هو محلل عددي مرن يتجاهل المكونات غير الرقمية، وهو مفيد بشكل خاص لقراءة العملات (Currencies). وستتعلم المزيد عنه في الفصل 13.
  • col_skip() يتخطى عموداً معيناً فلا يتم تضمينه في النتيجة، وهو أمر مفيد لتسريع قراءة البيانات إذا كان لديك ملف CSV ضخم جداً وتريد استخدام بعض الأعمدة فقط دون غيرها.

من الممكن أيضاً تجاوز الآلية التقريبية الافتراضية التي تخمن الأنواع، وذلك بالانتقال من استخدام الدالة list() إلى الدالة cols() وتحديد الخيار الافتراضي للمتغيرات عبر .default:

another_csv <- "
x,y,z
1,2,3"

read_csv(
  another_csv, 
  col_types = cols(.default = col_character())
)
#> # A tibble: 1 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     2     3

هناك دالة مساعدة أخرى مفيدة للغاية وهي cols_only() والتي تقرأ فقط الأعمدة التي تحددها لها صراحة وتتجاهل الباقي:

read_csv(
  another_csv,
  col_types = cols_only(x = col_character())
)
#> # A tibble: 1 × 1
#>   x    
#>   <chr>
#> 1 1

7.4 قراءة البيانات من ملفات متعددة

في بعض الأحيان تكون بياناتك مجزأة وموزعة عبر ملفات متعددة بدلاً من إدراجها في ملف واحد. على سبيل المثال، قد تكون لديك بيانات مبيعات لعدة أشهر، بحيث تكون بيانات كل شهر في ملف منفصل: 01-sales.csv لشهر يناير، و 02-sales.csv لشهر فبراير، و 03-sales.csv لشهر مارس. باستخدام الدالة read_csv()، يمكنك قراءة هذه البيانات دفعة واحدة ورصها (دمجها عمودياً) فوق بعضها البعض في إطار بيانات واحد.

sales_files <- c("data/01-sales.csv", "data/02-sales.csv", "data/03-sales.csv")
read_csv(sales_files, id = "file")
#> # A tibble: 19 × 6
#>   file              month    year brand  item     n
#>   <chr>             <chr>   <dbl> <dbl> <dbl> <dbl>
#> 1 data/01-sales.csv January  2019     1  1234     3
#> 2 data/01-sales.csv January  2019     1  8721     9
#> 3 data/01-sales.csv January  2019     1  1822     2
#> 4 data/01-sales.csv January  2019     2  3333     1
#> 5 data/01-sales.csv January  2019     2  2156     9
#> 6 data/01-sales.csv January  2019     2  3987     6
#> # ℹ 13 more rows

مرة أخرى، سيعمل الكود أعلاه بنجاح إذا كانت ملفات CSV هذه موجودة داخل مجلد data في مشروعك الحالي. يمكنك تحميل هذه الملفات من الروابط https://pos.it/r4ds-01-sales و https://pos.it/r4ds-02-sales و https://pos.it/r4ds-03-sales أو يمكنك قراءتها مباشرة من الويب عبر الإنترنت باستخدام الكود التالي:

sales_files <- c(
  "https://pos.it/r4ds-01-sales",
  "https://pos.it/r4ds-02-sales",
  "https://pos.it/r4ds-03-sales"
)
read_csv(sales_files, id = "file")

يضيف الوسيط id عموداً جديداً يسمى file إلى إطار البيانات الناتج، وهو يحدد الملف الذي جاءت منه تلك البيانات. يعد هذا مفيداً بشكل خاص في الحالات التي لا تحتوي فيها الملفات المستوردة على عمود تعريفي يمكن أن يساعدك في تتبع المشاهدات والرجوع بها إلى مصادرها الأصلية.

وإذا كان لديك ملفات كثيرة جداً تريد قراءتها، فقد يصبح من المرهق والمزعج كتابة أسمائها كلها في قائمة. بدلاً من ذلك, يمكنك استخدام الدالة الأساسية list.files() للبحث عن الملفات تلقائياً عن طريق مطابقة نمط معين (Pattern) في أسماء الملفات. وستتعلم المزيد عن هذه الأنماط والتعبيرات النمطية في الفصل 15.

sales_files <- list.files("data", pattern = "sales\\.csv$", full.names = TRUE)
sales_files
#> [1] "data/01-sales.csv" "data/02-sales.csv" "data/03-sales.csv"

7.5 الكتابة والتصدير إلى ملف

تأتي حزمة readr أيضاً مع دالتين مفيدتين للغاية لكتابة البيانات وحفظها على القرص الصلب: write_csv() و write_tsv(). الوسيطات الأكثر أهمية لهذه الدوال هي x (إطار البيانات المراد حفظه) و file (الموقع والمسار المخصص لحفظه). ويمكنك أيضاً تحديد كيفية كتابة وحفظ القيم المفقودة باستخدام الوسيط na، وما إذا كنت تريد إلحاق البيانات بملف موجود بالفعل عبر الوسيط append.

write_csv(students, "students.csv")

لنحاول الآن قراءة ملف CSV هذا مجدداً إلى بيئة البرنامج. لاحظ أن معلومات نوع المتغيرات التي قمت بتهيئتها وتعديلها للتو قد فُقدت تماماً عند الحفظ بصيغة CSV، لأنك تبدأ من جديد بقراءة الملف كملف نصي بسيط:

students
#> # A tibble: 6 × 5
#>   student_id full_name        favourite_food     meal_plan             age
#>        <dbl> <chr>            <chr>              <fct>               <dbl>
#> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only              4
#> 2          2 Barclay Lynn     French fries       Lunch only              5
#> 3          3 Jayendra Lyne    <NA>               Breakfast and lunch     7
#> 4          4 Leon Rossini     Anchovies          Lunch only             NA
#> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch     5
#> 6          6 Güvenç Attila    Ice cream          Lunch only              6
write_csv(students, "students-2.csv")
read_csv("students-2.csv")
#> # A tibble: 6 × 5
#>   student_id full_name        favourite_food     meal_plan             age
#>        <dbl> <chr>            <chr>              <chr>               <dbl>
#> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only              4
#> 2          2 Barclay Lynn     French fries       Lunch only              5
#> 3          3 Jayendra Lyne    <NA>               Breakfast and lunch     7
#> 4          4 Leon Rossini     Anchovies          Lunch only             NA
#> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch     5
#> 6          6 Güvenç Attila    Ice cream          Lunch only              6

هذا يجعل ملفات CSV غير موثوقة تماماً لتخزين وحفظ النتائج المؤقتة—حيث ستحتاج إلى إعادة إنشاء مواصفات الأعمدة وتعديل الأنواع في كل مرة تقوم فيها بتحميل البيانات. وهناك بديلان رئيسيان لتجاوز هذه المشكلة:

  1. الدالتان write_rds() و read_rds() هما غلافان موحدان (Uniform wrappers) مبنيان حول الدوال الأساسية saveRDS() و readRDS(). وتقوم هذه الدوال بتخزين البيانات بالصيغة الثنائية الخاصة بلغة R والتي تُسمى RDS. هذا يعني أنه عندما تعيد تحميل الكائن، فإنك تقوم بتحميل نفس كائن R تماماً وبنفس خصائصه وأنواعه التي قمت بحفظها وتخزينها.

    write_rds(students, "students.rds")
    read_rds("students.rds")
    #> # A tibble: 6 × 5
    #>   student_id full_name        favourite_food     meal_plan             age
    #>        <dbl> <chr>            <chr>              <fct>               <dbl>
    #> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only              4
    #> 2          2 Barclay Lynn     French fries       Lunch only              5
    #> 3          3 Jayendra Lyne    <NA>               Breakfast and lunch     7
    #> 4          4 Leon Rossini     Anchovies          Lunch only             NA
    #> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch     5
    #> 6          6 Güvenç Attila    Ice cream          Lunch only              6
  2. تتيح لك حزمة arrow قراءة وكتابة ملفات parquet، وهي صيغة ملفات ثنائية سريعة جداً يمكن مشاركتها واستخدامها عبر لغات برمجة مختلفة (مثل بايثون). وسنعود إلى حزمة arrow بمزيد من التفصيل والعمق في الفصل 22.

    library(arrow)
    write_parquet(students, "students.parquet")
    read_parquet("students.parquet")
    #> # A tibble: 6 × 5
    #>   student_id full_name        favourite_food   meal_plan           age
    #>        <dbl> <chr>            <chr>            <fct>             <dbl>
    #> 1          1 Sunil Huffmann   Strawberry yoghurt Lunch only            4
    #> 2          2 Barclay Lynn     French fries       Lunch only            5
    #> 3          3 Jayendra Lyne    NA                 Breakfast and lunch   7
    #> 4          4 Leon Rossini     Anchovies          Lunch only           NA
    #> 5          5 Chidiegwu Dunkel Pizza              Breakfast and lunch   5
    #> 6          6 Güvenç Attila    Ice cream          Lunch only            6

تتميز ملفات Parquet بأنها أسرع بكثير من ملفات RDS ويمكن استخدامها خارج بيئة R، ولكنها تتطلب تثبيت واستدعاء حزمة arrow.

7.6 إدخال البيانات يدوياً (Data entry)

في بعض الأحيان، ستحتاج إلى بناء وتجميع جدول بيانات (tibble) “يدوياً” عن طريق إدخال كمية صغيرة من البيانات مباشرة داخل سكربت R الخاص بك. هناك دالتان مفيدتان لمساعدتك في القيام بذلك، وتختلفان في طريقة ترتيب وتنسيق الجدول؛ فإما أن ترتبه بناءً على الأعمدة أو بناءً على الصفوف. تعمل الدالة tibble() عن طريق ترتيب البيانات في شكل أعمدة (By column):

tibble(
  x = c(1, 2, 5), 
  y = c("h", "m", "g"),
  z = c(0.08, 0.83, 0.60)
)
#> # A tibble: 3 × 3
#>       x y         z
#>   <dbl> <chr> <dbl>
#> 1     1 h      0.08
#> 2     2 m      0.83
#> 3     5 g      0.6

إن ترتيب وتنسيق البيانات بواسطة الأعمدة قد يجعل من الصعب رؤية كيفية ارتباط الصفوف وتوافقها مع بعضها البعض، ولذلك فإن البديل الأفضل هو استخدام الدالة tribble()، وهي اختصار لـ (transposed tibble) أي الجدول المنقول، والتي تتيح لك ترتيب وتنسيق بياناتك صفاً بصف (Row by row). تم تصميم الدالة tribble() خصيصاً لتسهيل إدخال البيانات يدوياً داخل الكود: حيث تبدأ عناوين الأعمدة بعلامة المدة ~ ويتم الفصل بين المدخلات أو البيانات باستخدام الفواصل commas. هذا يجعل من الممكن صياغة وعرض كميات صغيرة من البيانات في شكل منسق للغاية وسهل القراءة البصرية:

tribble(
  ~x, ~y, ~z,
  1, "h", 0.08,
  2, "m", 0.83,
  5, "g", 0.60
)
#> # A tibble: 3 × 3
#>       x y         z
#>   <dbl> <chr> <dbl>
#> 1     1 h      0.08
#> 2     2 m      0.83
#> 3     5 g      0.6

7.7 ملخص

في هذا الفصل، تعلمت كيفية تحميل ملفات CSV واستيرادها باستخدام الدالة read_csv()، وكيفية إدخال بياناتك الخاصة يدوياً باستخدام الدالتين tibble() و tribble(). وقد تعلمت أيضاً كيفية عمل ملفات csv، وبعض المشكلات الشائعة التي قد تواجهك، وكيفية التغلب عليها وتصحيحها. سوف نعود إلى موضوع استيراد البيانات عدة مرات في هذا الكتاب: حيث سيعلمك الفصل 20 كيفية استيراد البيانات من ملفات Excel وجداول بيانات Google Sheets، وسيوضح لك الفصل 21 كيفية تحميل البيانات واستدعائها من قواعد البيانات مباشرة، و الفصل 22 من ملفات parquet، و الفصل 23 من ملفات JSON، و الفصل 24 من مواقع الويب المختلفة.

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


  1. إن حزمة janitor ليست جزءاً من منظومة tidyverse، ولكنها توفر العديد من الدوال المفيدة جداً لتنظيف البيانات وتعمل بكفاءة عالية داخل أنابيب تمرير البيانات التي تستخدم الرمز |>.↩︎

  2. يمكنك تجاوز القيمة الافتراضية البالغة 1000 صف باستخدام الوسيط guess_max.↩︎