داده های مرتب (Tidy Data)، پایتون، پانداس

یکی از مفاهیم Data science که خیلی ها با آن آشنا نیستند مفهوم داده های مرتب یا Tidy Data هست.. مثلا من چند وقت پیش درگیر یک پروژه تحت نظر یکی از شرکت های بزرگ هلندی که بودجه چند ده میلیون دلاری و یک تیم اختصاصی Data Science داشت بودم و این شرکت یک دیتاست برای تحلیل به من داد. وقتی من دیتاست را دیدم متوجه شدم که این دیتاست از استاندارد داده های مرتب پیروی نمی کند و طبیعتا تمامی تحلیل های قبلی آن شرکت دارای عیب و ایراد بود. جالب تر این بود که کسی در خصوص این مفهوم و این استاندارد نمی دانست و وقتی در خصوص این مساله به آن ها گفتم خیلی حال کردند و خوشحال شدند! برای هممین تصمیم گرفتم که پستی در این خصوص بنویسم چون احتمالا خیلی از ماها هم از وجود این استاندارد خبر نداریم.ما انسان ها تمایل داریم که برای نحوه اجرای انواع کارهای مختلفی که انجام می دهیم استاندارد تعریف کنیم. به عنوان مثال، فرآیند ذخیره سازی داده ها را در نظر بگیرید. برای ذخیره سازی داده ها از دیتابیس ها استفاده می کنیم. داده ها هم در دیتابیس ها بر اساس استاندارهای خاصی ذخیره می شوند. دیتابیس ها هم از استانداردهای خاصی برای ذخیره سازی داده ها استفاده می کنند (مثلا در دیتابیس های رابطه ای باید یک ستون ID داشته باشیم.) اما حالا که برای ذخیره سازی داده ها، این داده ها باید از استاندارد تبعیت کنند این سوال پیش می آید که آیا استانداردی هم داریم که مشخص کند که داده ها برای عملیات های رایج دیتاساینس مثل پاکسازی، مصورسازی و مدل سازی چگونه باشند تا این کارها را راحت تر کنند؟به عبارت دیگر می خواهیم یک استاندارد وجود داشته باشد که قبل از این که شروع به کار و تحلیل داده ها کنیم، داده ها را بر اساس آن استاندارد آماده کنیم و آن ها را به صورت یک ساختار مناسب برای آن تحلیل در بیاوریم. خوشبختانه برای حل این مسئله، استانداردی در سال 2014 توسط یکی از توسعه دهنده های معروف زبان برنامه نویسی R و نویسنده کتابخانه هایی مانند ggplot2 ارائه شد. آقای Hadley Wickham در یک مقاله به نام «Tidy Data» معرفی شد. به همین خاطر، جای شگفتی هم نیست که اسم این استاندارد اصطلاحا Tidy Data یا «داده های مرتب» گذاشته شد. طبق مقاله آقای Wickham اگر داده های ما بر اساس این استاندارد به صورت tidy در بیایند بقیه عملیات های دیتاساینسی ما مثل پاکسازی داده ها، مصورسازی، مدل سازی و ... راحت تر و با معناتر می شوند.به علاوه از آن جا که Hadley Wickham یک توسعه دهنده زبان برنامه نویسی R بود، برای این که فرآیند تبدیل داده های نامرتب (messy) به tidy را در زبان برنامه نویسی R را راحت تر کند کتابخانه های reshape و reshape2 را هم نوشت که با آن ها می شود این کار را انجام. این مفهوم داده های مرتب و این کتابخانه ها بعدها به قدری مهم و تاثیر گذار شدند که مجموعه ای از کتابخانه های نوشته شده در زبان R موسوم به Tidyverse به وجود آمده است که بر اساس داده های ورودی که به صورت مرتب هستند انواع تحلیل ها را انجام می دهند که بعدا اگر فرصتی شد به صورت مفصل تری در خصوص آن ها صحبت می کند.حالا منظور از داده های tidy چیست؟ بر اساس این استاندارد هادلی ویکهام یک دیتاست را دیتاست tidy تعریف می کند که ساختار زیر را داشته باشد.هر متغیر به صورت یک ستون باشد و در آن مقادیر ذخیره شده باشند.هر مشاهده به صورت یک سطر در ذخیره باشدهر واحد مشاهده ای به صورت یک جدول جداگانه باشد.بر اساس این تعریف از داده های مرتب می توانیم مواردی که داده های ما به صورت مرتب نیستند را به صورت زیر دسته بندی کنیم:عنوان ستون های دیتاست ما به جای این که نام متغییر باشند در حقیقت مقدار یک متغیر دیگر است.اطلاعات چند متغیر به جای این که به صورت ستون های مجزا در دیتاست ذخیره شده باشند همگی به صورت همزمان در یک ستون ذخیره شده اند.متغیرها هم در ستون ها و هم در سطرها ذخیره شده باشند.چندین نوع از مشاهدات در یک جدول مشابه ذخیره شده اند.یک نوع مشاهده در چندین جدول ذخیره شده است.از آن جا که در اکثر موارد تنها با یکی از سه مورد ابتدایی بالا سر و کار داریم، در ادامه به تفکیک به بررسی یک مثال از هر کدام از این موارد می پردازیم و تحوه حل آن در پایتون و کتابخانه پانداس بررسی می کنیم. ( تمامی کدها و داده های این پست در این لینک قابل مشاهده هستند.)عنوان ستون های دیتاست به جای این که نام متغییر باشند در حقیقت مقدار یک متغیر دیگر هستند.برای شروع با اولین نشانه داده های نامرتب یعنی « عنوان ستون های دیتاست ما به جای این که نام متغییر باشند در حقیقت مقدار یک متغیر دیگر است.» و توضیح مثالی که خود آقای wickhom در مقاله اش برای تبین این مشکل بررسی کرده بود شروع می کنم. دیتاست زیر را در نظر بگیرید:این دیتاست حاوی اطلاعاتی در خصوص یک نظرسنجی در مورد مذهب افراد و میزان درآمد آن ها در ایالات متحده آمریکا است. همانطور که می بینیم سه متغیر در این دیتاست وجود دارد: نوع مذهب، میزان درآمد و فراوانی (تعداد اقراد). اما همانطور که در تصویر بالا مشاهده می کنیم مقادیر مختلف متغیر میزان درآمد هر کدام به صورت یک ستون جدا درآمده اند و تعداد افرادی که متناظر با هر کدام از این درآمدها هستند به صورت مقادیر این ستون ها ذخیره شده اند. این نوع ذخیره سازی داده ها از جهاتی می تواند منطقی باشد. به عنوان مثال، یکی از مزایای این نوع ذخیره سازی داده کم تر شدن حجم اشغال شده می باشد.طبق استاندارد آقای Wickham برای مرتب کردن این نوع دیتاست ها اصطلاحا باید آن ها را ذوب (melt) کنیم، به عبارت دیگر باید ستون های دارای این مشکل را تبدیل به سطر کنیم. به این کار اصطلاحا تبدیل یک دیتافریم عریض (wide) به یک دیتافریم طویل (long) می گوییم. به صورت دقیق تر، در مثال بالا، یک ستون جدید به نام income ایجاد می کنیم و مقادیر ستون های متناظر با درآمد را تبدیل به سطر های جدید و مقادیر این ستون می کنیم.با این که ابتدا این مفاهیم ابتدا در R پیاده سازی شده اند ولی مرتب کردن دیتاست های نامرتب با استفاده از پایتون و به طور خاص کتابخانه پانداس که اصلی ترین کتابخانه در پایتون برای کار با داده ها است هم به راحتی امکان پذیر است. برای این کار پانداس یک تابع دارد که این کار را برای ما انجام می دهد. برای نشان دادن این قابلیت پانداس ابتدا مثال زیر که یک دیتاست متفاوت است را در نظر بگیرید:import pandas as pd data = pd.read_csv('538.tsv',sep ='\t') data = data.drop(['TOTAL'],axis = 1) data.head()این دیتاست حاوی آمار و اطلاعاتی در خصوص یک بازی نقش آفرینی معروف و قدیمی به نام «Dungeons & Dragons» است. در این بازی هر بازیکن قبل از شروع بازی باید یک نژاد (Race) و یک کلاس (Class) برای شخصیت خودش انتخاب کند. دیتاست فوق هم مربوط به تعداد بازیکن هایی هستند که ترکیب های مختلفی از نژاد و کلاس را در نسخه 5 این بازی انتخاب کرده اند. همانطور که می بینیم مقادیر متغیر کلاس بازیکن در این دیتاست به صورت مجموعه ای از ستون ها ذخیره شده اند که این با اصل داده های مرتب در تضاد است.حالا فرض کنید که بخواهید یک مدل یادگیری ماشین بسازید که بر اساس این داده ها پیشبینی کند که در نسخه 6 این بازی تعداد بازیکن ها برای هر کدام از ترکیب های نژاد و کلاس چقدر خواهد بود. اگر سعی کنید که یک مدل را بر اساس ساختار فعلی این دیتاست آموزش بدهید می بینید که این کار غیر ممکن است! به صورت واضح دلیل آن این است که این دیتاست به صورت مرتب ذخیره نشده است و باید آن را مرتب کنیم. کتابخانه پانداس هم یک تابع به نام ()pd.melt دارد که می تواند یک یک دیتاست نامرتب به یک دیتاست مرتب یا همان یک دیتافریم عریض را به یک دیتافریم طویل تبدیل کند (ذوب کند).برای این که از این تابع استفاده کنیم باید چند چیز را مشخص کنیم:frame : همان دیتافریمی (دیتاستی) است که می خواهیم تبدیل را بر روی آن انجام بدهیم و آن را مرتب کنیم.با id_vars لیستی از ستون هایی که نمی خواهیم آن ها را تغییر دهیم (ذوب کنیم) را مشخص می کنیم.با val_vars لیستی از ستون هایی که می خواهیم آن ها را تبدیل به سطر کنیم (ذوب کنیم) یا همان ستون هایی که مرتب نیستند را مشخص می نماییم. به صورت پیشفرض، هر ستونی را که در id_vars در نظر نگیریم به صورت خودکار به عنوان یک ستون ورودی value_vars در نظر گرفته می شود یعنی می توانیم یا ستون هایی که نمی خواهیم تغییر کنند را به عنوان ورودی به id_vars مشخص کنیم یا ستون هایی که می خواهیم تغییر کنند را به عنوان ورودی به val_vars بدهیم و لازم نیست حتما هر دو را مشخص کنیم.وقتی که یک دیتاست را ذوب می کنیم دو ستون جدید ایجاد می شوند: یک ستون که حاوی اسم ستون های ذوب شده و ستون دیگر حاوی مقادیر ذخیره شده بازای هر کدام از این ستون ها در دیتاست قبلی است. به صورت پیشفرض پانداس برای ستون های جدید ایجاد شده نام های key و value را قرار می دهد ولی این اسم ها خیلی جالب و مفید نیستند و برای همین بهتر است که برای هر کدام از این ستون ها یک نام با معنا انتخاب کنیم. برای این که نام هر کدام از این ستون ها را تغییر دهیم می توانیم پارامتر ورودی var_name و value_name را در تابع ()melt مشخص کنیمبرای این دیتاست به خصوص ما می خواهیم همه ستون ها به غیر از ستون Race را ذوب کنیم و آن ها را به صورت یک ستون جدید به اسم Class در بیاوریم. برای همین، ستون حاوی تعداد بازیکن های هر نژاد و کلاس را هم دریک ستون جدید به نام 'Number of Players' ذخیره می کنیم.: data_tidy = pd.melt(data,id_vars=['Race'], var_name = 'Class', value_name='Number of Players') data_tidy.head()همانطور که در شکل بالا می بینیم حالا دیتاست ما مرتب شده و به طور خاص برای مدل سازی مناسب تر است. به طور مثال، الان می توانیم ستون تعداد بازیکن ها را به عنوان متغیر هدف و ستون های کلاس و نژاد را به عنوان ویژگی ها در یک مدل یادگیری ماشین استفاده کنیم. یا حالا فرض کنید که می خواهیم تعداد بازیکن ها را به ازای هر ترکیب خاص از نژاد و کلاس رسم کنیم. با دیتاست مرتب شده این کار با یک خط کد به راحتی قابل انجام است ولی انجام همین کار با دیتاست نامرتب مصیبت خواهد بود!plt.figure(figsize = (20,10)) sns.barplot(x = 'Race', y = 'Number of Players', hue = 'Class', data = data_tidy, palette = 'Paired') اطلاعات چند متغیر به جای این که به صورت ستون های مجزا در دیتاست ذخیره شده باشند همگی به صورت همزمان در یک ستون ذخیره شده اند.این مشکل هم همانند مشکل قبلی است با این تفاوت که به جای این که ما چند ستون مجزا را برای تعدادی از متغیرها تخصیص بدهیم همگی آن ها را در یک ستون ذخیره کرده ایم. بگذارید این مورد را با یک مثال به صورت مشخص تر توضیح بدهم. به عنوان دیتاست زیر را در نظر بگیرید که اطلاعات جمع آوری شده مربوط به شیوع بیماری ابولا در آفریقا را در بر دارد. این اطلاعات شامل متغیر های تاریخ (Date)، روز (Day)، کشور مورد بررسی، نوع گزارش (تعداد بیماری های گزارش شده (Case) و تعداد فوتی های (Death) ناشی از ابولا) و فراوانی است.ebola = pd.read_csv('ebola.csv') ebola.head() همانطور که می بینیم این دیتاست به صورت مرتب ذخیره نشده است و اکثر ستون های این دیتاست آمار فراوانی مشاهده شده برای ترکیب های مختلف دو متغیر کشور و نوع گزارش را به صورت همزمان در ستون های مختلف ذخیره کرده اند. در حالیکه ما باید به جای این همه ستون ، فقط سه ستون برای اسم کشور، نوع گزارش و فراوانی داشته باشیم.حل این مشکل نیز مشابه با قبل است و اینجا هم از تابع ()pd.melt استفاده می کنیم تا این دیتاست عریض را به یک دیتاست طویل تبدیل کنیم. از آن جا که تعداد ستون های نامرتب خیلی زیاد است، منطقی است که فقط در این جا ستون هایی که نمی خواهیم تغییر پیدا کنند یعنی Date و Day را به عنوان ورودی به آرگومان id_vars بدهیم و نیازی نیست که value_vars را مشخص کنیم.ebola_tidy = ebola.melt( id_vars=['Date', 'Day'], var_name = 'Variable', value_name= 'Frequency') ebola_tidy.head()بعد از ذوب کردن این دیتافریم می بینیم که کماکان با یک مشکل موجه هستیم. ما ستون Frequency را ساخته ایم ولی هنوز ستون مجزایی برای کشور و نوع گزارش نداریم و اطلاعات این دو ستون به صورت همزمان در ستون Variable ذخیره شده است. این مورد حالت دقیق تر مشکل « اطلاعات چند متغیر به جای این که به صورت ستون های مجزا در دیتاست ذخیره شده باشند همگی به صورت همزمان در یک ستون ذخیره شده اند.» است. برای حل این مشکل باید ستون Variable را به دو ستون Report_Type و Country تقسیم کنیم. چون که این اطلاعات این دو متغیر در ستون Variable با یک _ از همدیگر جدا شده اند انجام این کار راحت است. از آن جا که مقادیر این ستون به صورت رشته های پایتون (string) ذخیره شده اند، می توانیم با استفاده از مشخصه str به انواع متدهای کار با رشته های پایتون دسترسی داشته باشیم. در این جا هم از متد ()split استفاده می کنیم تا مقادیر این ستون را بر اساس کاراکتر _ از هم جدا کنیم. اگر expand = True مقادیر جدا شده به صورت ستون های جداگانه به دیتافریم اضافه می شوند که ما هم این دو ستون را به صورت 'Report_Type','Country' به دیتاست اضافه می کنیم. به علاوه، دیگر هم نیازی به ستون 'Variable' هم نداریم و آن را هم حذف می کنیم.ebola_tidy[['Report_Type','Country']] = ebola_tidy['Variable'].str.split('_', expand = True) ebola_tidy.drop(['Variable'],axis = 1, inplace = True) ebola_tidy.head()حالا یک دیتاست مرتب شده داریم و می توانیم انواع مدل سازی ها و تحلیل ها را بر روی انجام بدهیم.متغیرها هم در ستون ها و هم در سطرها ذخیره شده باشندطبق مقاله آقای Wickham این مسئله پیچیده ترین نوع داده های غیرمرتب است. برای نشان دادن این نوع از نامرتب بودن داده ها بگذارید به دیتاستی که در خود مقاله Tidy Data به عنوان مثال ذکر شده است نگاهی بیاندازیم. دیتاستی که در این مثال استفاده شده (شکل پائین) مربوط به اطلاعات جمع آوری شده روزانه حداقل و حداکثر دمای ایستگاه های مختلف سنجش وضعیت آب و هوا در مکزیک و در طول 5 ماه است.در این دیتاست ما با دو مشکل مواجه هستیم:اول از همه مثل دیتاست های قبلی که دیدیم بازای روزهای مختلف هر ماه یک ستون جداگانه داریم که یکی از نشانه های دیتاست نامرتب است. به طور خاص، در این جا چون بعضی از ماه ها 31 روز ندارند مقدار قرار گرفته برای مشاهدات در آن ماه ها در ستون مربوط به روز 31 ام به صورت NaN است ( در عکس بالا به صورت – نمایش داده شده اند!) که این نحوه نمایش غیرمنطقی به نظر می رسد.به همین دلیل برای مرتب کردن این دیتاست باید ابتدا همانند قبل ستون های مربوط به روزهای مختلف را ذوب کنیم و همه مقادیر آن ها را در یک ستون جدید به نام day ذخیره کنیم.weather_molten = pd.melt(weather, id_vars=['id','year','month','element'], var_name='day', value_name='temperature') weather_molten.head() حالا دیتاست ما به وضعیت بهتری درآمده است اما کماکان یک مشکل دیگر داریم. مشکل دوم این دیتاست این است که در حقیقت ستون element در این دیتاست نشان دهنده یک متغیر نیست بلکه به صورت بر عکس حاوی متغیرهای دیگری به نام tmax (حداکثر دمای ثبت شده) و tmin (حداقل دمای ثبت شده) است. برای همین در اینجا باید عملیات برعکس ذوب کردن را بر روی ستون element انجام بدهیم یعنی یک ستون جدید به نام tmax و یک ستون جدید دیگر به نام tmin بسازیم.به این عملیات مخالف ذوب کردن اصطلاحا چرخاندن (pivoting) گفته می شود. در ذوب کردن به دنبال این هستیم که ستون ها را به سطر تبدیل کنیم به صورت برعکس زمانی که می خواهیم عملیات چرخاندن را انجام بدهیم، به دنبال این هستیم که مقادیر یکتای موجود در یک ستون را به مجموعه ای از ستون های مجزا تبدیل کنیم (یعنی به ازای هر مقدار یکتا در یک ستون یک ستون مجزا ساخته می شود).انجام این عملیات در پانداس همانند ذوب کردن به راحتی انجام می پذیرد چون که هر دیتافریم در پانداس یک متد ()pivot_table دارد که با آن می توانیم این عملیات را هم به راحتی انجام بدهیم. برای استفاده از این متد باید 3 آرگومان ورودی اصلی زیر را مشخص کنیم:مشابه با آرگومان id_vars در تابع ()melt این متد یک آرگومان به نام index دارد که با آن می توانیم ستون هایی که نمی خواهیم چرخانده شوند را مشخص کنیم. در این مثال می خواهیم ستون های id، year، month و day دست نخورده بمانند.آرگومان columns یک آرگومان دیگر این متد برای مشخص کردن ستون هایی است که می خواهیم چرخانده شوند. در این مثال ما می خواهیم ستون element را بچرخانیم.در نهایت پارامتر values مشخص می کند که مقادیر کدام ستون زمانی که یک ستون را می چرخانیم و چند ستون جدید تبدیل می کنیم برای پر کردن آن ستون ها استفاده شوند. در این مثال ما می خواهیم مقادیر موجود در ستون temperature در ستون های جدید ساخته شده بعد از چرخاندن استفاده شود.حالا بگذارید به صورت عملی نحوه پیاده سازی این تابع را بر روی این دیتاست ببینیم.weather_tidy = weather_molten.pivot_table( index = ['id','year','month','day'], columns = 'element', values = 'temperature') weather_tidy.head() هر چند که دیتاست ما مرتب شده است و هر متغیر به صورت یک ستون ذخیره و هر مشاهده تشکیل یک سطر در دیتاست را می دهد، ولی این دیتاست هنوز به فرمت نهایی و مورد پسند در نیامده است و کمی باید آن را تغییر بدهیم. همانطور که می بینیم تمامی ستون هایی که به عنوان ورودی به پارامتر index متد ()pivot_table داده ایم، به index های جدید دیتاست ساخته شده تبدیل شده اند و برای همین ظاهر این دیتاست فعلی کمی عجیب و غریب شده است. به همین دلیل از متد ()reset_index استفاده می کنیم تا این مشکل را بر طرف کنیم.weather_tidy.reset_index(inplace = True) weather_tidy.head()حالا یک دیتاست با ظاهر بهتر داریم. البته می توانیم باز هم تغییراتی در این دیتاست انجام بدهیم. به طور مثال می توانیم که ستون element را حذف کنیم و یا یک ستون جدید برای تاریخ بسازیم که ترکیب ستون های سال، ماه و روز باشد. اما انجام این کار ها را به عهده خود شما می گذارم.