Как получать и анализировать рейтинги курсов OMSCS, данные о сложности и рабочей нагрузке и компилировать их в интерактивную информационную панель

Программа Онлайн-магистр компьютерных наук (OMSCS), предлагаемая Технологическим институтом Джорджии, сегодня пользуется огромной популярностью, поскольку позволяет студентам получить степень магистра компьютерных наук онлайн в одном из лучших университетов в США, без необходимости переезжать или отказываться от своей нынешней работы. Это также относительно доступно по сравнению с другими программами.

Любой, кто рассматривает OMSCS, естественно, захочет узнать больше о предлагаемых курсах при их выборе. К счастью, полезный ресурс под названием OMSCentral предоставляет богатую базу данных отзывов учащихся об этих курсах, таких как их нагрузка, уровень сложности и качество. вообще. Веб-сайт был создан бывшим студентом OMSCS и стал популярным ресурсом для нынешних и будущих студентов OMSCS.

В этой статье мы узнаем, как:

  • очистить данные для оценок курса, сложности и нагрузки от OMSCentral
  • сделать некоторую визуализацию данных, в основном в Jupyter Notebook
  • создать HTML-страницу визуализаций
  • Кроме того, если вы заинтересованы, записную книжку также можно преобразовать в скрипт Python, который регулярно запускается с использованием действий GitHub для генерации и обновления той же HTML-страницы, что и выше, но размещенной на github.io

Прежде чем я двинусь дальше, я хотел бы указать https://www.omscentral.com/ для данных, которые мы будем собирать для анализа. Я не владею этими данными.

Страница живой панели инструментов размещена здесь на github.io (частичный снимок экрана ниже).

Репозиторий Github для всего проекта (включая скрипты для обновления HTML-страницы выше) можно найти здесь.

Давайте рассмотрим все шаг за шагом, этот пошаговый код можно найти в этой тетради.

1. Импортировать библиотеки

Во-первых, мы импортируем библиотеки, которые нам нужны для хранения данных. BeautifulSoup нужен для анализа данных из OMS Central, а requests нужен для получения данных. Pandas используется для хранения данных в DataFrames, а Plotly используется для создания визуализаций.

# libraries for webscraping, parsing and getting data
from urllib.request import urlopen, Request
from bs4 import BeautifulSoup
import json

# for plotting and data manipulation
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import plotly
import plotly.express as px

# for getting current date and time to print 'last updated' in webpage
from datetime import datetime

2. Соберите данные отзывов о курсах

Страница отзывов о курсах на OMSCentral выглядит так. Мы стремимся экспортировать все эти данные в Pandas DataFrame.

Сначала мы извлекаем исходный HTML-код страницы выше в объект html, используя следующий код, используя библиотеки request и BeautifulSoup.

url = 'https://www.omscentral.com/'

req = Request(url=url,headers={'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0'})

try:
    response = urlopen(req)   
except:
    time.sleep(10) # if there is an error and request is blocked, do it more slowly by waiting for 10 seconds before requesting again
    response = urlopen(req)  
        
# Read the contents of the file into 'html'
html = BeautifulSoup(response)

Вы также можете просмотреть исходный код, посетив https://www.omscentral.com/ в своем браузере, щелкнув правой кнопкой мыши страницу и выбрав Просмотр исходного кода.

Ближе к концу исходного кода вы увидите информацию о курсе в коде Javascript между тегами <script></script>, как показано ниже. К счастью, это уже в хорошем формате JSON, поэтому не требуется много утомительного анализа с использованием BeautifulSoup.

<script id = "__NEXT_DATA__" type = "application/json" > 
  {
    "props": {
        "pageProps": {
            "courses": [{
                "_createdAt": "2022-08-15T14:49:24Z",
                "_id": "26e53feb-fbd0-4233-8218-dfb7f56cc46d",
                "_rev": "959qsafI8uoLDPJeciuflB",
                "_type": "course",
                "_updatedAt": "2022-08-25T21:43:35Z",
                "codes": ["CS-8803-O13"],
                "creditHours": 3,
                "description": "The goal of this course is to provide students in CS and ECE with the fundamental background on quantum computing and equip them with the skills to write code and optimize quantum programs on real quantum computers.",
                "id": "26e53feb-fbd0-4233-8218-dfb7f56cc46d",
                "isDeprecated": false,
                "isFoundational": true,
                "name": "Special Topics: Quantum Computing",
                "officialURL": "https://omscs.gatech.edu/cs-8803-o13-quantum-computing",
                "programs": [{
                    "_key": "d21a69086f85",
                    "_ref": "b6f2bf84-c2ea-405e-8423-c348e1a94051",
                    "_type": "reference"
                }],
                "slug": "special-topics-quantum-computing",
                "syllabus": {
                    "file": {
                        "_type": "file",
                        "asset": {
                            "_ref": "file-f921a6a8952de8270a8002e7cdd20e798252a800-pdf",
                            "_type": "reference"
                        }
                    }
                },
                "tags": ["QC"],
                "textbooks": [{
                    "_key": "2f5517a7bba6eb4a7b282b9e884f13e8",
                    "name": "Quantum Computing: A Gentle Introduction",
                    "url": "https://amzn.to/3Tjt5pV"
                }],
                "reviewCount": 2,
                "rating": 3,
                "difficulty": 3,
                "workload": 15
            },

            .
            .
            .            
            

            }]
        },
        "__N_SSG": true
    },
    "page": "/",
    "query": {},
    "buildId": "RbJpKU_7gp7Gm26pP9748",
    "isFallback": false,
    "gsp": true,
    "scriptLoader": []
} 
</script>

В строке 5 приведенного выше кода под ключом course значением является список (обораченный []) курсов (с каждым курсом, обернутым {}, выше показан только первый курс).

Каждый курс содержит информацию в парах ключ-значение, например. для курса выше "name": “Special Topics: Quantum Computing” и "rating": 3.

3. Разберите данные обзоров курса в DataFrame.

Давайте извлечем код JSON выше, используя BeautifulSoup, и проанализируем полный список курсов в список словарей Python с именем parsed_list, используя библиотеку json, с кодом ниже.

# There are multiple <script> tags in the source code
# We want the data in between the final <script> tags
# Final hence index is -1
raw = html.find_all("script")[-1]

json_text = raw.text

# Load the relevant part of the data
parsed_list = json.loads(json_text)['props']['pageProps']['courses']

Не стесняйтесь смотреть на первый элемент parsed_list, чтобы увидеть словарь, который соответствует первому курсу, упомянутому выше.

Мы анализируем приведенный выше список в Pandas DataFrame с именем raw_df ниже.

raw_df = pd.DataFrame(parsed_list)
raw_df.head()

4. Очистка данных

4.1. Отделение

Глядя на столбец codes выше, мы видим, что значения представляют собой списки, а не строку, первую часть кода в каждом списке (например, CS, PUBP, ECE, что соответствует компьютерным наукам, государственной политике и электротехнике и компьютерам). Инженерное дело соответственно) на самом деле являются аббревиатурой кафедр, проводящих курс. Давайте просто извлечем эту часть в новый столбец с именем dept.

# The 'codes' column of the courses consists of lists instead of strings, extract the element in the list into a list.
raw_df['code'] = raw_df['codes'].apply(lambda x: x[0])

# The first part of the course code (before the - symbol) can be extracted as the department.
raw_df['dept'] = raw_df['code'].apply(lambda x: x.split('-')[0])

4.2. Сокращенный тег курса

Столбец tags в столбце raw_df хранит аббревиатуру названий курсов, которая позже пригодится для создания графиков визуализации. Однако это значение отсутствует для большинства строк, поэтому я также написал ниже код, чтобы генерировать собственное сокращение курса, беря первую букву каждого заглавного слова в названии курса и сохраняя их под столбец с именем tag.

# Generate own tag using first letter of each capitalized word in the name as some courses are without tags
raw_df['tag'] = raw_df['name'].apply(lambda x: ''.join([word[0] for word in x.split() if word[0].isupper()]))

# Some courses already have tags stored in the form of a list, for simplicity, extract the first element of the list to as the tag
for i, row in raw_df.iterrows():
    if isinstance(row['tags'], list):
        if len(row['tags']) > 0:
            raw_df.at[i,'tag'] = row['tags'][0]    

Результирующие столбцы курса name, tags и tag должны выглядеть так.

4.3. Фильтровать данные с минимальным количеством отзывов

Нам нужны только данные с некоторым минимальным количеством отзывов, чтобы данные отражали мнения по крайней мере нескольких человек. В данном случае я установил min_review_count на 5, но вы можете изменить его по своему усмотрению.

min_review_count = 5

df = raw_df[raw_df['reviewCount'] >=  min_review_count]

4.4. Обработка «аномальных» данных

Если вы заметили, у курса «Распределенные вычисления» (с тегом «DC») более 60 часов, что почти втрое больше, чем у курса со второй по величине нагрузкой, это искажает шкалу оси при отображении рабочих нагрузок на диаграмме рассеяния, и баллы для других курсов не будут распределены равномерно. Поэтому я решил заменить рабочую нагрузку распределенных вычислений, чтобы она была равной рабочей нагрузке второго по величине курса ниже. (Не стесняйтесь придумывать любой другой способ обойти это).

# distributed computing has a ridiculously high amt of workload
# so it is replaced with the same amount of workload as the second highest one
df.loc[df["tag"] == "DC", 'workload'] = 0 
df.loc[df["tag"] == "DC", 'workload'] = df["workload"].max()

4.5. Окончательный очищенный кадр данных

Давайте выберем интересующие нас столбцы для наших окончательных очищенных данных для построения графика.

# final cleaned data ready for plotting
df_plot = df[['name', 'tag', 'dept', 'code', 'description', 'reviewCount', 'rating', 'difficulty', 'workload']]
df_plot

5. Диаграммы рассеяния

С приведенными выше очищенными данными мы, наконец, готовы сделать несколько интерактивных графиков с помощью пакета plotly.express.

5.1. Рейтинг поля и график сложности

Давайте создадим график зависимости рейтинга курса от сложности со следующими параметрами, чтобы дать хорошую общую визуализацию всех четырех свойств, которые нас интересуют:

  • ось Y: рейтинг
  • ось X: сложность
  • размер маркера: количество просмотров
  • цвет маркера: рабочая нагрузка

Давайте также добавим на график вертикальную и горизонтальную линии, представляющие среднее значение сложности и рейтинга соответственно.

fig_scatter1 = px.scatter(df_plot, x="difficulty", y="rating", 
                 hover_data=['name', 'reviewCount'], text='tag', size='reviewCount', color='workload')
fig_scatter1.update_traces(textposition='top center')
fig_scatter1.add_vline(x=df_plot["difficulty"].mean(), line_width=0.5, annotation_text = 'Mean Difficulty')
fig_scatter1.add_hline(y=df_plot["rating"].mean(), line_width=0.5, annotation_text = 'Mean Rating')
fig_scatter1.update_layout(
    title="OMSCS Course Rating and Difficulty (size = Review Count, color = Workload)",
    xaxis_title="Difficulty",
    yaxis_title="Rating",
    height=800,
    font=dict(
        size=10
    )
)
fig_scatter1.show()

Вот несколько примеров того, на что вы можете обратить внимание в этом сюжете:

  • Если вам нужны курсы, которые сложны и хорошо приняты, вы можете искать курсы в верхнем правом квадранте.
  • Курсы, отмеченные маленькими маркерами, имеют меньше отзывов, поэтому их данные могут быть не столь надежными(вы всегда можете настроить min_review_count, упомянутый в разделе 4.3, чтобы отфильтровать эти точки).
  • Чем теплее цвет маркера, тем выше рабочая нагрузка. Курсы, признанные более сложными, имеют более высокую нагрузку.
  • Помните, что этот график является интерактивным, то есть вы можете навести указатель мыши на любой маркер, и отобразятся сведения о курсе с точными номерами (см. маркер для курса машинного обучения выше). . Вы можете выбрать, какие данные показывать при наведении, в параметре hover_data в методе px.scatter выше.

5.2. График рабочей нагрузки и сложности курса

Это аналогичный график, но со следующими свойствами:

  • ось Y: рабочая нагрузка
  • ось X: сложность
  • размер маркера: количество просмотров
  • цвет маркера: рейтинг
fig_scatter2 = px.scatter(df_plot, x="difficulty", y="workload", 
                 hover_data=['name', 'reviewCount'], text='tag', size='reviewCount', color='rating')
fig_scatter2.update_traces(textposition='top center')
fig_scatter2.add_vline(x=df_plot["difficulty"].mean(), line_width=0.5, annotation_text = 'Mean Difficulty')
fig_scatter2.add_hline(y=df_plot["workload"].mean(), line_width=0.5, annotation_text = 'Mean Workload')
fig_scatter2.update_layout(
    title="OMSCS Course Workload and Difficulty (size = Review Count, color = Rating)",
    xaxis_title="Difficulty",
    yaxis_title="Workload",
    height=800,
    font=dict(
        size=10
    )
)
fig_scatter2.show()

Из очевидного линейного тренда мы видим, что рабочая нагрузка сильно коррелирует с рейтингом сложности курса. Рейтинги на самом деле не коррелируют ни с рабочей нагрузкой, ни с трудностью, поскольку повсюду на графике присутствует смесь цветов маркеров. (Мы не видим ни скоплений синего, ни скоплений желтого, сосредоточенных где-либо).

6. Графики древовидной карты

График древовидной карты похож на интерактивный иерархический график тепловой карты. Ранее я подробно объяснял этот вид графика при создании карты дерева настроений акций в статье ниже. Не стесняйтесь читать его, чтобы понять больше.



Давайте построим 2 разных древовидных карты.

6.1. Рейтинги курсов

# group data into department at the highest level, breaks it down into courses
# the 'values' parameter uses the value of the column to determine the relative size of each box in the chart
# the color of the chart follows the course rating
df_plot['label'] = df_plot['tag'] + '<br><br>' + df_plot['rating'].apply(lambda x:str(round(x, 3)))
fig_treemap1 = px.treemap(df_plot, path=[px.Constant("OMSCS Course Rating"), 'dept', 'label'], values='reviewCount',
                  color='rating', hover_data=['name', 'difficulty'],
                  color_continuous_scale=['#FF0000', "#000000", '#00FF00'])

fig_treemap1.update_traces(textposition="middle center")
fig_treemap1.update_layout(margin = dict(t=30, l=10, r=10, b=10), font_size=20)

fig_treemap1.show()
  • В методе px.treemap под параметром path мы передаем следующий список [px.Constant('OMSCS Course Rating'), 'dept', 'label'] (порядок списка важен). Это означает, что сначала мы называем общий верхний уровень графика постоянной строкой "Рейтинг курса OSCCS". Это то, что представляет собой весь график, и он будет напечатан в верхней части графика. Затем мы группируем курс по отделам, которые его предлагают, а затем разбиваем его на метки курса на самом низком уровне.
  • Столбец label создается в первой строке приведенного выше кода, каждая метка состоит из тега курса с двумя разрывами строки HTML <br><br>, за которыми следует его соответствующий рейтинг под ним. Это хороший способ обобщить название и рейтинг курса, как показано на скриншоте древовидной карты ниже.
  • Мы также передаем rating в параметр color, чтобы цвет представлял рейтинг.
  • В параметре color_continuous_scale мы передаем нашу цветовую шкалу. Я выбрал цветовой код #FF0000 (red) для нижней границы, #000000 (black) для средней точки и #0000FF (green) для верхней границы.
  • Затем мы передаем информацию, которую хотим просмотреть, при наведении указателя мыши на каждое поле в древовидной карте через параметр hover_data. Здесь я предполагаю, что пользователь захочет увидеть следующее более подробно [‘name', 'difficulty'], то есть полное название курса, а также рейтинг сложности при наведении курсора.

Конечный результат показан ниже! С первого взгляда вы сможете сказать, что такие курсы, как CN (компьютерные сети), IIS (введение в информационную безопасность) и ML4T ( Машинное обучение для трейдинга) имеют наибольшее количество отзывов (что может означать, что на них записывается много людей), и что GIOS (Выпускник Введение в операционные системы) является одним из Курсы с самым высоким рейтингом.

Помните, что эта карта интерактивна, то есть вы можете щелкнуть по каждому отделу, чтобы увеличить карту только этого отдела. Например, позвольте мне подробно рассмотреть курсы ISYE (Промышленная и системная инженерия) ниже.

6.2. Трудности курса

# group data into department at the highest level, breaks it down into courses
# the 'values' parameter uses the value of the column to determine the relative size of each box in the chart
# the color of the chart follows the course difficulty
df_plot['label'] = df_plot['tag'] + '<br><br>' + df_plot['difficulty'].apply(lambda x:str(round(x, 3)))
fig_treemap2 = px.treemap(df_plot, path=[px.Constant("OMSCS Course Difficulty"), 'dept', 'label'], values='reviewCount',
                  color='difficulty', hover_data=['name', 'rating'],
                  color_continuous_scale=['#FF0000', "#000000", '#00FF00'])

fig_treemap2.update_traces(textposition="middle center")
fig_treemap2.update_layout(margin = dict(t=30, l=10, r=10, b=10), font_size=20)

fig_treemap2.show()

Это похоже на предыдущее, за исключением того, что вместо этого отображается сложность трассы.

7. Графики гистограмм

Это довольно просто, поэтому я не буду подробно останавливаться на этом. Давайте быстро взглянем на распределение нагрузки, рейтинги и трудности для курсов OMSCS ниже.

7.1. Распределение нагрузки

fig_hist1 = px.histogram(df_plot, x='workload', nbins=30, title='Workload Distribution')
fig_hist1.update_layout(
    width=800
)
fig_hist1.show()

7.2. Распределение рейтинга

fig_hist2 = px.histogram(df_plot, x='rating', nbins=30, title='Rating Distribution')
fig_hist2.update_layout(
    width=800
)
fig_hist2.show()

7.3. Распределение сложности

fig_hist3 = px.histogram(df_plot, x='difficulty', nbins=30, title='Difficulty Distribution')
fig_hist3.update_layout(
    width=800
)
fig_hist3.show()

8. Тепловая карта корреляции

Итак, насколько сильно коррелируют рейтинг курса, сложность и нагрузка? Давайте представим это в матрице ниже.

fig_corr = px.imshow(df[['rating', 'difficulty', 'workload']].corr(), text_auto = True, title = 'Correlation')
fig_corr.show()

Как и ожидалось, существует сильная корреляция 0,887 между сложностью и рабочей нагрузкой, как показано ранее в разделе 5.2.

Также корреляция между трудностью и нагрузкой с рейтингом довольно слабая (0,222 и 0,285), хотя и положительная. Судя по нашим диаграммам рассеяния выше, действительно есть курсы, которые сложны, имеют большую нагрузку и низкие рейтинги. (Думаю, мы должны дважды подумать, прежде чем идти на эти курсы!)

9. Создайте HTML-страницу

Теперь пришло время, наконец, сгенерировать HTML-страницу. Мы назовем эту страницу omscs_courses_rating_difficulty.html.

9.1. Напишите заголовок и описание для HTML-страницы.

Сначала мы извлекаем код для получения текущего времени. Мы будем использовать это для предоставления времени последнего обновления страницы.

# datetime object containing current date and time
now = datetime.now()
dt_string = now.strftime("%m/%d/%Y %I:%M:%S %p")
timezone_string = datetime.now().astimezone().tzname()
print(dt_string, timezone_string)

Затем мы открываем HTML-файл и пишем описание в HTML-коде, которое мы хотим отобразить в виде информации на странице. Помимо поля «Последнее обновление», я добавил код для подтверждения OMSCentral, а также добавил ссылку на исходный код GitHub и эту пояснительную статью, а также рекламировал свою собственную страницу портфолио GitHub. хD

with open('omscs_courses_rating_difficulty.html', 'a') as f:
    f.truncate(0) # clear file if something is already written on it
    title = "<h1>Georgia Tech OMSCS</h1><h2>Summary of Course Difficulty and Rating</h2>"
    updated = "<h3>Last updated: <span id='timestring'></span>"       
    # GitHub Actions server timezone may not be at the same timezone of person opening the page on browser
    # hence Javascript code is written below to convert to client timezone before printing it on
    current_time = "<script>var date = new Date('" + dt_string + " " + timezone_string + "'); document.getElementById('timestring').innerHTML += date.toString()</script>"
    description = "<br><br>The data is pulled from <a href='https://www.omscentral.com/'>OMSCentral</a> daily via a GitHub Actions script to update the summary information in this page.<br><br>"
    credits = "Credits to <a href='https://www.omscentral.com/'>OMSCentral</a> for the information, review and rating of the courses. I do not own any of this data."
    subtitle = "<h3>Explanation and Source Code</h3>"
    code = """<a href="https://medium.com/datadriveninvestor/use-github-actions-to-create-a-live-stock-sentiment-dashboard-online-580a08457650">Explanatory Article</a> | <a href="https://github.com/damianboh/gatech_omscs_live_rating_reviews_plot">Source Code</a>"""
    author = """ | Created by Damian Boh, check out my <a href="https://damianboh.github.io/">GitHub Page</a>"""
    
    f.write(title + updated + current_time + description + credits + subtitle + code + author)

Конечным результатом является ряд текста над информационной панелью на HTML-странице, показанной ниже.

9.2. Поместите графики на HTML-страницу

Напомним, что ранее мы давали графикам уникальные имена fig_scatter1, fig_scatter2, fig_treemap1 и т. д., поэтому мы можем ссылаться на них в коде генерации HTML-страницы ниже. Я решил исключить графики histogram ниже, не стесняйтесь раскомментировать соответствующие строки, чтобы включить графики гистограммы в HTML-страницу, если хотите.

    f.write(fig_scatter1.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file
    f.write(fig_scatter2.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file
    f.write(fig_treemap1.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file
    f.write(fig_treemap2.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file
    
    # uncomment below to include the histograms also
    # f.write(fig_hist1.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file
    # f.write(fig_hist2.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file
    # f.write(fig_hist3.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file
    
    f.write(fig_corr.to_html(full_html=False, include_plotlyjs='cdn')) # write the fig created above into the html file

Опять же, приведенный выше пошаговый код можно найти в этой записной книжке.

10. (Дополнительно) Разместите HTML-страницу на Github.io и регулярно обновляйте ее с помощью GitHub Actions.

Поскольку обзоры курсов на OMSCentral будут время от времени обновляться, было бы неплохо создать рабочий процесс для регулярного создания HTML-страницы выше со всеми соответствующими графиками и разместить эту страницу на github.io.

Мы назовем его Панель просмотра отзывов о курсах OMSCS Live Georgia Tech: https://damianboh.github.io/omscs_courses_rating_difficulty.html.

Вот схема шагов, необходимых для достижения этой цели:

  1. Преобразуйте весь приведенный выше код в блокноте Jupyter в скрипт update_page.py, который можно запустить в любое время для создания HTML-страницы.
  2. Создайте файл requirements.txt, чтобы перечислить все необходимые библиотеки Python для запуска приведенного выше кода.
  3. Создайте репозиторий GitHub, содержащий вышеупомянутый скрипт update_page.py и файлы requirements.txt.
  4. Создайте отдельный репозиторий страниц GitHub для размещения ваших страниц в домене your_github_username.github.io. ПРИМЕЧАНИЕ. Эта страница предназначена для размещения страниц на github.io и представляет собой репозиторий, отличный от репозитория на шаге 3.
  5. Настройте файл GitHub Actions .github/workflows.yml в репозитории, созданном на шаге 4, чтобы он выполнял следующие действия: Выполнять каждые полчаса (или каждый день или даже неделю, если хотите) и установите список необходимых библиотек, запустите .py скрипт, чтобы сгенерировать HTML-страницу и отправить HTML-страницу. в репозиторий страниц GitHub на шаге 3, чтобы страница обновилась на github.io.

Ранее я писал статью об аналогичном проекте по переносу графика карты настроений акций на HTML-страницу, размещенную на github.io, с использованием очень похожего сценария .github/workflows.yml в действиях GitHub. Не стесняйтесь ссылаться на эту статью для этого проекта. Я оставляю это читателю в качестве упражнения, чтобы он прочитал приведенную ниже статью и соответствующим образом адаптировал ее для реализации на странице рейтингов курсов OMSCS.



Если вам нужна подсказка, не стесняйтесь обращаться к моему полному репозиторию Github за полным проектом со сценарием update_page.py, файлом действий GitHub .github/workflows.yml и файлом requirements.txt.

Я надеюсь, что вы нашли этот проект полезным. Если вам понравилась эта статья, не стесняйтесь проверить другие подобные статьи, которые я написал.







Если вам нравятся подобные статьи и вы хотите поддержать таких писателей, как я, подумайте о том, чтобы зарегистрироваться, чтобы стать участником Medium. За 5 долларов в месяц вы получите неограниченный доступ к любой статье на Medium. Если вы зарегистрируетесь по моей ссылке, я получу небольшую комиссию без каких-либо дополнительных затрат для вас.



Подпишитесь на DDIntel Здесь.

Посетите наш сайт здесь: https://www.datadriveninvestor.com

Присоединяйтесь к нашей сети здесь: https://datadriveninvestor.com/collaborate