تصور لعدد من الانتصارات لفرق الدوري الاميركي للمحترفين باستخدام الرسوم البيانية شريط الرسوم المتحركة في R

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


أريد أن ألاحظ على الفور أن الكثير مما سأكتبه في المستقبل بالفعل في عائلة سمبسون كان على هبر في المقال ، نقوم بإنشاء رسوم بيانية متحركة باستخدام R. هذه المقالة ، بدورها ، هي ترجمة للمقالة " إنشاء رسوم بيانية متحركة متحركة باستخدام R من متوسط". لذلك ، من أجل الاختلاف بطريقة أو بأخرى عن المقالات المذكورة أعلاه ، سأحاول أن أصف بشكل كامل ما أفعله ، وكذلك تلك اللحظات التي ليست في المقالة الأصلية. على سبيل المثال ، لملء الأعمدة ، استخدمت ألوان أوامر NBA ، وليس لوحة ggplot2 القياسية ، ولكن حزمة data.table ، وليس dplyr ، في معالجة البيانات. لقد قمت بكل هذا العمل كدالة ، لذا يكفي الآن كتابة اسم الفريق والسنوات التي تحتاج إلى حساب عدد الانتصارات فيها.


معطيات


لبناء الجدول الزمني ، استخدمت بيانات عن عدد الانتصارات لكل فريق من فرق الدوري الاميركي للمحترفين الـ 30 في المواسم الخمسة عشر الماضية. تم جمعها من stats.nba.com باستخدام ملحق NBA Data Retriever ، والذي ، من خلال استخدام NBA API ، ينتج ملفات CSV بالإحصائيات اللازمة. هنا التفاصيل الكاملة من مشروعي على جيثب .


المكتبات المستخدمة


 library(data.table) library(tidyverse) library(gganimate) 

لمعالجة البيانات ، أستخدم data.table (ببساطة لأنني قابلت هذه الحزمة من قبل). يمكنني أيضًا تنزيل مجموعة من الحزم tidyverse ، وليس ggplot2 منفصلة حتى لا تقلق ، إذا ظهرت أثناء التحليل فكرة تتطلب ggplot2 إضافيًا لحزمة من هذه المجموعة. في هذه الحالة بالذات ، يمكن الاستغناء عن ggplot2 ، لا يتم ggplot2 حزم مجموعة أخرى. حسنا ، gganimate يحدد الرسومات في الحركة.


العمل مع البيانات


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


يحتوي الجدول الموجود في data.table على النموذج dt[i, j, by] ، حيث يكون "المسؤول" عن تجميع العناصر. سأقوم بالتجميع حسب عمود TeamName. وهناك عقبة. يعرض هذا العمود أسماء الفرق: ليكرز ، سيلتيكس ، هيت ، إلخ. لكن خلال الفترة قيد الاستعراض (من موسم 2004/2005) غيرت عدة فرق أسماءها: أصبحت نيو أورليانز هورنتس نيو أورليانز بيليكان ، وعادت شارلوت بوبكاتس الاسم التاريخي شارلوت هورنتس ، وسياتل سياتل سوبرسونيكس أصبح أوكلاهوما سيتي ثاندر. هذا يمكن أن يسبب الارتباك. التحويلات التالية تساعد على تجنب هذا:


 table1 <- table[TeamCity == "New Orleans" & TeamName == "Hornets", TeamName := "Pelicans"][ TeamCity == "New Orleans/Oklahoma City" & TeamName == "Hornets", TeamName := "Pelicans"][ TeamName == "Bobcats", TeamName := "Hornets"][ TeamName == "SuperSonics", TeamName := "Thunder"] 

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


للبدء ، نتخلص من المعلومات "الإضافية" ، تاركًا فقط تلك الأعمدة التي نحتاجها للعمل:


 table1 <- table1[ , .(TeamName, WINS)] 

في data.table يستبدل (.) إنشاء دالة list . خيار "كلاسيكي" أكثر لتحديد الأعمدة هو table1 <- table1[, c("TeamName", "WINS")] . بعد ذلك ، يأخذ الجدول النموذج التالي:


TeamNameWINS
شموس62
حرارة59
وخز الخيل59
بيستونز54

بالنسبة للرسوم المتحركة لكل موسم على حدة ، هذا يكفي ، ولكن لحساب إجمالي عدد الانتصارات لفترة محددة ، تحتاج إلى حساب إجمالي التراكمات من الانتصارات.


table1 <- table1[, CumWins := cumsum(WINS), by = "TeamName"]


باستخدام وظيفة cumsum نحصل على الأرقام التي نحتاجها. باستخدام: = بدلاً من = يسمح لك بإضافة عمود جديد إلى الجدول ؛ لا أقوم بالكتابة فوق نفس عمود CumWins. by = "TeamName" بتجميع البيانات حسب اسم الفريق ويتم by = "TeamName" المبلغ التراكمي لكل فريق من الفرق الثلاثين بشكل منفصل.


بعد ذلك ، أضف عمودًا في السنة التي بدأ فيها كل موسم. يمتد موسم الدوري الاميركي للمحترفين في الفترة من أكتوبر إلى مايو ، لذلك يقع في عامين تقويميين. في تسمية الموسم ، سنة بدايته ، أي الموسم: 2018 على الرسم البياني هو موسم 2018/19 في الواقع.


يحتوي الجدول الأصلي على هذه البيانات. يُظهر عمود SeasonID شكلًا في شكل 2 (السنة التي بدأ فيها الموسم) ، على سبيل المثال ، 22004. يمكنك إزالة الأولين باستخدام حزمة stringr أو وظائف R الأساسية ، لكنني ذهبت بطريقة مختلفة قليلاً. اتضح أنني أولاً استخدم هذا العمود للإشارة إلى الفصول المطلوبة ، ثم احذف وإنشاء عمود مع التواريخ مرة أخرى. إجراءات إضافية.


لقد فعلت ذلك على النحو التالي:


table1 <- table1[,year := rep(seq(2004, 2018), each = length(unique(table1$TeamName)))]


كنت محظوظًا لأنه خلال الفترة الزمنية المحددة ، لم يتغير عدد الفرق في الدوري الاميركي للمحترفين ، لذلك كررت الأرقام من 2004 إلى 2018 30 مرة. مرة أخرى ، إذا حدث في التاريخ ، فستكون هذه الطريقة غير مريحة نظرًا لأن عدد الفرق في كل موسم سيكون مختلفًا ، لذلك من الأفضل استخدام الخيار مع محو عمود SeasonID.


ثم أضف عمود cumrank.


table1 <- table1[, cumrank := frank(-CumWins, ties.method = "random"), by = "year"]


يمثل ترتيب الفرق في كل موسم بعدد الانتصارات وسيتم استخدامه كقيم للمحور X. frank تناظرية data.table أسرع من rank الأساسي ، ناقص يعني الترتيب بالترتيب التنازلي (يمكن القيام بذلك أيضًا باستخدام وسيطة decreasing = TRUE . ما ترتيب الفرق التي ties.method = "random" نفس العدد من الانتصارات ، وبالتالي ، ties.method = "random" كل من هذه ties.method = "random" . حسنًا ، تم تجميع كل هذا في عام واحد.


ويضيف تحويل الجدول الأخير عمود value_rel .


table1 <- table1[, value_rel := CumWins/CumWins[cumrank==1], by = "year"]


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


بعد كل الإضافات ، يحتوي الجدول على النموذج التالي:


TeamNameWINSCumWinsعامcumrankvalue_rel
وخز الخيل5959200430.9516129
وخز الخيل63122200511.0000000
وخز الخيل58180200620.9729730
وخز الخيل56236200711.0000000

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


 table1 <- table1[ ,.(TeamName, WINS)][ , CumWins := cumsum(WINS), by = "TeamName"][ ,year := rep(seq(2004, 2018), each = length(unique(table1$TeamName)))][ , cumrank := frank(-CumWins, ties.method = "random"), by = "year"][ , value_rel := CumWins/CumWins[cumrank==1], by = "year"] 

تغيير ملء الأعمدة من المعيار إلى ألوان الفرق.


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


للقيام بذلك ، قم بإنشاء table table_color باسم الأمر ولونه الرئيسي. الألوان مأخوذة من teamcolorcodes.com .


TeamNameTEAM_color
الصقور# E03A3E
سلتكس# 007A33
شبكات# 000000

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


  table_color <- table_color[order(TeamName)] cols <- table_color[, "TEAM_color"] 

التآمر


نحن نبني حقًا مخططًا واحدًا فقط ، والذي يحتوي على جميع مؤشرات الانتصارات البالغ عددها 450 (15 فصولًا * 30 فريقًا) ، ثم "نقسمها" على المتغير الضروري (في حالتنا ، بالسنوات) باستخدام الوظائف من الحزمة gganimate .


 gg <- ggplot(table1, aes(cumrank, group = TeamName, fill = as.factor(TeamName), color = as.factor(TeamName))) + geom_tile(aes(y = CumWins/2, height = CumWins, width = 0.7), color = NA, alpha = 0.8) 

أولاً ، نقوم بإنشاء رسم باستخدام وظيفة ggplot . في الوسيطة aes ، حدد كيفية عرض المتغيرات من الجدول على المخطط. نقوم بتجميعها حسب TeamName ، وستكون fill color مسؤولة عن لون الأعمدة.


الأعمدة الحقيقية نسميها ليست صحيحة تماما. باستخدام geom_tile نقوم "بتقسيم" البيانات الموجودة على الرسم البياني إلى مستطيلات. فيما يلي مثال لمخطط من هذا النوع:

يمكن ملاحظة كيف يتم "تقسيم" الرسم البياني إلى مربعات (يتم الحصول عليها من المستطيلات باستخدام الطبقة coord_equal() ) ، ثلاثة في كل عمود. ولكن بفضل width الوسيطة أقل من واحد ، فإن البلاط الخاص بنا يأخذ شكل الأعمدة.


  geom_text(aes(y = 0, label = paste(TeamName, " ")), vjust = 0.2, hjust = 1, size = 6) + geom_text(aes(y = CumWins, label = paste0(" ",round(CumWins))), hjust = 0, size = 7) + coord_flip(clip = "off", expand = FALSE) + scale_fill_manual(values = cols) + scale_color_manual(values = cols) + scale_y_continuous(labels = scales::comma) + scale_x_reverse() + guides(color = FALSE, fill = FALSE) + 

بعد ذلك ، أضيف توقيعين باستخدام geom_text : اسم الفريق وعدد مرات الفوز. coord_flip المحاور ، و scale_fill_manual و scale_color_manual يغير لون الأعمدة ، و scale_x_reverse "يوسع" محور X. لاحظ أننا نأخذ الألوان من متجه cols تم إنشاؤه مسبقًا.


تحدد طبقة theme خيارات لضبط عرض الرسم البياني. يشار هنا إلى كيفية عرض رؤوس وتسميات المحاور (بأي حال من الأحوال ، ما يخبرنا element_blank على الجانب الأيمن من المساواة). نقوم بإزالة خطوط الشبكة ، الخلفية ، الإطار ، الشبكة على المحور Y. plot.title الوسيطات plot.title ، plot.subtitle ، plot.caption ، قمنا بتعيين خيارات العرض الخاصة بالعنوان والعنوان الفرعي وتوقيع المخطط. لمزيد من التفاصيل حول معنى جميع المعلمات ، راجع gglot2


 theme(axis.line=element_blank(), axis.text.x=element_blank(), axis.text.y=element_blank(), axis.ticks=element_blank(), axis.title.x=element_blank(), axis.title.y=element_blank(), legend.position="none", panel.background=element_blank(), panel.border=element_blank(), panel.grid.major=element_blank(), panel.grid.minor=element_blank(), panel.grid.major.x = element_line( size=.1, color="grey" ), panel.grid.minor.x = element_line( size=.1, color="grey" ), plot.title=element_text(size=25, hjust=0.5, face="bold", colour="black", vjust=-1), plot.subtitle = element_text(size = 15), plot.caption =element_text(size=15, hjust=0.5, color="black"), plot.background=element_blank(), plot.margin = margin(2,2, 2, 4, "cm")) 

إنشاء الرسوم المتحركة


لن أتطرق إلى استخدام وظيفة transition_states ، فهذا الجزء مماثل لمنشور سابق لي حول حبري. أما بالنسبة إلى labs فإنها تخلق عنوان المخطط وعنوانه الفرعي وتوقيعه. يتيح لك استخدام {closest_state} عرض كل سنة محددة على الرسم البياني ، الذي نراه حاليًا.


  anim <- gg + transition_states(year, transition_length = 4, state_length = 1) + view_follow(fixed_x = TRUE) + labs(title = "Cumulative Wins by teams in seasons", subtitle = "Season: {closest_state}", caption = "Telegram: @NBAatlantic, Twitter: @vshufiskiy\n Data sourse: stats.nba.com") 

تعمل nba_cumulative_wins لإنشاء المخططات.


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


 _ <- function( ) { _ } 

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


 nba_cumulative_wins <- function(table, elements, first_season, last_season){ ... } 

حيث table هو اسم الجدول مع إدخال البيانات ،
elements - أسماء تلك الفرق التي يجب عرضها على المخطط
first_season - أول موسم يتم عرضه على الرسم البياني
last_season - الموسم الأخير الذي سيتم عرضه على الرسم البياني.


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


nba_cumulative_wins <- function(table, elements, first_season, last_season = 2018)


سيتم بناء الجداول حتى موسم 2018/19 ، ما لم يرد خلاف ذلك.


العمل مع elements الوسائط ، first_season ، last_season


باستخدام وسيطة elements ، يمكننا تحديد اسم الفرق التي نريد رؤيتها على المخطط. يعد هذا مناسبًا جدًا عندما يكون هناك فريقان أو 3 فرق من هذا القبيل ، ولكن إذا أردنا عرض الدوري بأكمله ، فسوف يتعين علينا كتابة elements = c() واسم جميع الفرق الثلاثين الموجودة بين قوسين.


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


  select_teams <- unique(table1$TeamName) select_div <- unique(table1$Division) select_conf <- unique(table1$Conference) select_nba <- "NBA" table1 <- if(elements %in% select_teams){ table1[TeamName %in% elements] } else if (elements %in% select_div){ table1[Division %in% elements] } else if(elements %in% select_conf){ table1[Conference %in% elements] } else if(elements == "NBA"){ table1 } else { NULL } 

تحتوي select_ الرموز الرمزية) على أسماء جميع الفرق الثلاثين ، 6 أقسام ، مؤتمرين و NBA ، وتترك الوظيفة الفريدة اسمًا فريدًا واحدًا فقط ، بدلاً من 15 (حسب عدد سنوات البيانات).


بعد ذلك ، باستخدام if...else ، يتم التحقق من وسيطة elements المدخلة على أنها تنتمي إلى إحدى الفئات ( %in% استخدام %in% لتحديد ما إذا كان العنصر ينتمي إلى المتجه) ، ويتم تعديل جدول البيانات وفقًا لذلك. الآن ، إذا أردت أن أرى نتائج الفرق التي تلعب في دوري الجنوب الغربي بدلاً من ذلك


elements = c("Mavericks", "Spurs", "Rockets", "Grillies", "Pelicans")


فقط أدخل


elements = "Southwest" ، والتي هي أسرع بكثير وأكثر ملاءمة.


بسبب إمكانية اختيار المواسم ، يتغير العمل مع التواريخ أيضًا. في البداية ، يضاف السطر:


 table1 <- table1[SeasonID >= as.numeric(paste(2, first_season, sep = "")) & SeasonID <= as.numeric(paste(2, last_season, sep = ""))] 

لذلك أترك في الجدول فقط تلك الصفوف التي تقع في الفترة الزمنية المختارة لدينا. يتغير رمز إنشاء عمود year أيضًا. الآن يبدو مثل هذا:


 table1 <- table1[ ,year := rep(seq(first_season, last_season), each = length(unique(table1$TeamName)))] 

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


  elements1 <- if (elements == "NBA"){ c("Hawks", "Celtics", "Nets", "Hornets", "Bulls", "Cavaliers", "Mavericks", "Nuggets", "Pistons", "Warriors", "Rockets", "Pacers", "Clippers", "Lakers", "Grizzlies", "Heat", "Bucks", "Timberwolves", "Pelicans", "Knicks", "Thunder", "Magic", "76ers", "Suns", "Trail Blazers","Kings", "Spurs", "Raptors", "Jazz", "Wizards") } else if (elements == "West") { c("Mavericks","Nuggets", "Warriors", "Rockets", "Clippers", "Lakers", "Grizzlies","Timberwolves", "Pelicans", "Thunder", "Suns", "Trail Blazers","Kings", "Spurs", "Jazz") } else if (elements == "East") { c("Hawks", "Celtics", "Nets", "Hornets", "Bulls", "Cavaliers","Pistons", "Pacers", "Heat", "Bucks", "Knicks", "Magic", "76ers", "Raptors", "Wizards") } else if (elements == "Pacific") { c("Warriors", "Clippers", "Lakers", "Suns", "Kings") } else if (elements == "Southeast") { c("Magic", "Hornets", "Heat", "Hawks", "Wizards") } else if (elements == "Southwest") { c("Mavericks", "Grizzlies", "Pelicans", "Rockets", "Spurs") } else if (elements == "Central") { c("Bucks", "Pacers", "Pistons", "Bulls", "Cavaliers") } else if (elements == "Atlantic") { c("Knicks", "Nets", "Celtics", "Raptors", "76ers") } else if (elements == "Northwest") { c("Nuggets", "Trail Blazers", "Jazz", "Thunder", "Suns") } else { elements } 

بعد ذلك ، قم بإنشاء جدول بأسماء الأوامر التي نحتاجها ، table_color بتوصيل هذا الجدول بـ table_color باستخدام دالة inner_join من حزمة dplyr . يتضمن inner_join فقط الحالات التي تتطابق في كلا الجدولين.


  table_elements1 <- data.table(TeamName = elements1) table_color <- table_color[order(TeamName)] inner_table_color <- inner_join(table_color, table_elements1) cols <- inner_table_color[, "TEAM_color"] 

تغير الوظيفة هجاء العنوان والعنوان الفرعي. يأخذون هذه النظرة:


 anim <- gg + transition_states(year, transition_length = 4, state_length = 1) + view_follow(fixed_x = TRUE) + labs(title = paste("Cumulative Wins by teams in seasons", first_season, "-", last_season, sep = " "), subtitle = paste(if (elements %in% select_div ){ paste(elements, "Division", sep = " ") } else if (elements %in% select_conf ){ paste("Conference", elements, sep = " ") }, "Season: {closest_state}", sep = " "), caption = "Telegram: @NBAatlantic, Twitter: @vshufiskiy\nData sourse: stats.nba.com") 

أداء


علاوة على ذلك ، كل هذا مرئي.


 animate(anim, nframes = (last_season - first_season + 1) * (length(unique(table1$TeamName)) + 20), fps = 20, width = 1200, height = 1000, renderer = gifski_renderer(paste(elements[1], "cumwins.gif", sep = "_"))) 

لقد اخترت الرقم في nframes تجريبي ، بحيث تبعًا لعدد الأوامر المحددة ، تزيد / تنقص السرعة.


جدول المواعيد



آمل أن يكون منصبي مثيرًا للاهتمام. كود مشروع جيثب .


إذا كنت مهتمًا بالمكون الرياضي لهذه التصورات ، يمكنك زيارة مدونتي على sports.ru "على جانبي المحيط الأطلسي"

Source: https://habr.com/ru/post/ar458904/


All Articles