Чтобы получить необходимые данные, мы будем использовать twitter API, а также пакет tweepy для извлечения твитов, относящихся к интересующей нас теме, уличному бойцу 6.

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

consumer_key = "hZ4tZflgSz8MlyeZku*******"
consumer_secret = "yPWGoZnSb6EDHiIW6j0gzezclYYx8h4edBYqG2lW********"
access_token = "1495889004075622401-oNGkpwZcpGu29Goe0Zt*********"
access_token_secret = "E5PB0jSV8s2xnwHqivRswWYX9FyyfRHwz********"
# Creating handler to for authentification 
auth = tw.OAuthHandler(consumer_key, consumer_secret) 

# Setting up the access tokens
auth.set_access_token(access_token, access_token_secret)

# Creating an instance for the API 
api = tw.API(auth, wait_on_rate_limit = True)

В частности, мы извлечем 1000 твитов с мнениями и комментариями пользователей твиттера относительно раскрытия Street fighter 6, которые будут поисковым запросом для нашей цели.

topic = "street fighter 6"

# Query to extract the desired tweets
query = tw.Cursor(api.search_tweets, 
                  q = topic,lang = "en").items(1000)

# Creating a dictionary that contains the tweet and the time when it was created.
tweets = [{"Tweets":str(tweet.text.encode("utf-8")), "Timestamp":tweet.created_at} 
          for tweet in query]

Дайте нам знать, чтобы увидеть первые 5 твитов, которые были извлечены.

tweets[:5]
[{'Tweets': "b'RT @kesut4: Street Fighter 6 ! https://t.co/5T9aJXPtxA'",
  'Timestamp': datetime.datetime(2022, 2, 23, 14, 5, 10, tzinfo=datetime.timezone.utc)},
 {'Tweets': 'b"RT @MufausaThe3rd: #KOFXV 6 Player Party VS Mode is kinda fun. \\n\\nThis kind reminds of Street Fighter X Tekken\'s Pair Play mode. https://t.c\\xe2\\x80\\xa6"',
  'Timestamp': datetime.datetime(2022, 2, 23, 14, 5, tzinfo=datetime.timezone.utc)},
 {'Tweets': "b'RT @caio_thiago: Street Fighter 6 be like\\n#streetfighter6 https://t.co/GEUUNP8wsX'",
  'Timestamp': datetime.datetime(2022, 2, 23, 14, 4, 51, tzinfo=datetime.timezone.utc)},
 {'Tweets': "b'RT @caio_thiago: Street Fighter 6 be like\\n#streetfighter6 https://t.co/GEUUNP8wsX'",
  'Timestamp': datetime.datetime(2022, 2, 23, 14, 4, 43, tzinfo=datetime.timezone.utc)},
 {'Tweets': "b'We are recording the show today!!!\\n\\nWe\\xe2\\x80\\x99re talking...\\n\\n\\xf0\\x9f\\x94\\xb9 Call of Duty Delayed\\n\\xf0\\x9f\\x94\\xb9 PlayStation VR2 Headset Revealed\\n\\xf0\\x9f\\x94\\xb9 S\\xe2\\x80\\xa6 https://t.co/EQOGy25hSW'",
  'Timestamp': datetime.datetime(2022, 2, 23, 14, 3, 56, tzinfo=datetime.timezone.utc)}]

Предварительная обработка твитов

Сначала нам нужно преобразовать наши данные в более удобный и практичный формат, такой как фрейм данных, с помощью DataFrame.from_dict.

tweetsDF = pd.DataFrame.from_dict(tweets)

tweetsDF.head()

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

logo_refs = ["logo","emblem","trademark","insignia"]

# Creating function to find logo related words on a given tweet.
def find_ref(x, refs):
    flag = 0
    for ref in refs:
        if ref in x:
            flag = 1
    
    return flag

# Apply the custom function on every one of the 1000 tweets.
tweetsDF["logo"] = tweetsDF["Tweets"].apply(lambda x: find_ref(x, logo_refs))
print("",tweetsDF["logo"].sum())

tweetsDF.head()
393

Из 1000 собранных твитов, касающихся Street Fighter 6, около 393 из них, или 39,3%, так или иначе ссылаются на новый логотип игры, положительно или отрицательно.

Далее мы хотим еще немного отформатировать строки, которые у нас есть для данных. Например, мы заинтересованы в удалении пунктуации, URL-адресов, имен пользователей и стоп-слов (обычных слов без реальной информации о настроении), а также в выполнении лемматизации каждого слова для каждого твита, чтобы самая основная форма слова оставалась. (например: бег, бег и бег все становятся бегом).

import nltk 
from nltk.corpus import stopwords 

import re
import string

from textblob import Word, TextBlob
custom_stopwords = ["b'RT",'b"RT',"b'",'b"',"RT"]
# Creating function for the preprocessing of the tweets
def preprocess_tweets(tweet, custom_stopwords):
    pre_tweets = tweet
    
    pre_tweets = re.sub('@[\w]+','',pre_tweets) #Remove the usernames
    
    pre_tweets = " ".join(word for word in pre_tweets.split() if word not in 
                         stop_words) #Remove stop words
    
    pre_tweets = " ".join(word for word in pre_tweets.split() if word not in
                         custom_stopwords) #Remove custom stopwords ("b'RT",'b"RT')
    
    pre_tweets = " ".join(word for word in pre_tweets.split() if word not in
                         string.punctuation) #Remove punctuation
    
    pre_tweets = " ".join(Word(word).lemmatize() for word in pre_tweets.split()) #Take every word to root 
    
    pre_tweets = re.sub(r'http\S+', '', pre_tweets) #Remove URL's
    
    return pre_tweets 
                       
tweetsDF["preprocessed_tweet"] = tweetsDF["Tweets"].apply(lambda x: preprocess_tweets(x,custom_stopwords))
tweetsDF.head()

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

Рассчитать настроение

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

tweetsDF["Polarity"] = tweetsDF["preprocessed_tweet"].apply(lambda x: TextBlob(x).sentiment[0])

tweetsDF["Subjectivity"] = tweetsDF["preprocessed_tweet"].apply(lambda x: TextBlob(x).sentiment[1])
tweetsDF.head()

Анализ настроения твитов

Давайте теперь посмотрим, каковы общие настроения людей в твиттере по поводу выхода первого тизер-трейлера Street Fighters 6.

plt.figure(figsize = (12,6))

plt.subplot(1,2,1)
sns.histplot(x = "Polarity", data = tweetsDF, kde = True, 
             bins = 15, color = "green")
plt.title("Polarity distribution")

plt.subplot(1,2,2)
sns.histplot(x = "Subjectivity", data = tweetsDF, kde = True, 
             bins = 15,color = "purple")
plt.title("Subjectivity distribution")
Text(0.5, 1.0, 'Subjectivity distribution')

Во-первых, что касается полярности твитов, очень заметно, что подавляющее большинство людей, кажется, занимают очень нейтральную позицию, когда речь идет о новейшей части серии уличных бойцов. Тем не менее, мы также можем увидеть приличное количество положительных твитов, от 0,5 до 1; с другой стороны, также заметно несколько негативных твитов, хотя и намного меньше, чем нейтральных и позитивных.

Теперь, что касается субъективности, интересно посмотреть, как модель textblob классифицирует большинство твитов как полностью объективные, даже несмотря на то, что мы говорим о мнении людей в очень чувствительной для некоторых людей теме, например, новый выпуск одного из самые легендарные франшизы в игровой истории.

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

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

plt.figure(figsize = (12,6))

plt.suptitle("Polarity distribution")
plt.subplot(1,2,1)
sns.histplot(x = "Polarity", data = tweetsDF[tweetsDF["logo"]==0], kde = True,
            bins = 15, color = "yellow")
plt.title("Polarity distribution when logo is not mentioned")
plt.text(0.25,200,
         "Polarity mean: " + str(round(tweetsDF[tweetsDF["logo"]==0]["Polarity"].mean(),4)))

plt.subplot(1,2,2)
sns.histplot(x = "Polarity", data = tweetsDF[tweetsDF["logo"]==1], kde = True,
            bins = 15, color = "blue")
plt.title("Polarity distribution when logo is mentioned")
plt.text(0.25,250,
        "Polarity mean: " + str(round(tweetsDF[tweetsDF["logo"]==1]["Polarity"].mean(),4)))
Text(0.25, 250, 'Polarity mean: 0.0695')

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

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

Мы уже упоминали, что, несмотря на первоначальные разногласия, не так много негативных комментариев, касающихся тизер-трейлера и нового логотипа. Однако по-прежнему важно понимать поведение тех немногих негативных комментариев, как мы увидим далее.

# Create function that identifies negative comments (polarity less than 0)
def find_negative(x):
    negative = 0
    if x < 0:
        negative = 1
    return negative

tweetsDF["negative"] = tweetsDF["Polarity"].apply(find_negative)
# Create a new dataframe that contains only the negative comments
neg_tweets = tweetsDF[tweetsDF["negative"] == 1]

Во-первых, мы хотим узнать, сколько негативных твитов связано с мнениями о новом логотипе игры.

plt.figure(figsize = (12,8))

sns.countplot(x = "logo",data = neg_tweets, palette = "Set1")
plt.title("Number of negative tweets")
Text(0.5, 1.0, 'Number of negative tweets')

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

Теперь давайте посмотрим на распределение полярности негативных комментариев как для твитов, говорящих о логотипе, так и для тех, кто говорит на другие темы.

plt.figure(figsize = (12,6))

plt.suptitle("Polarity distribution (only negative comments)")
plt.subplot(1,2,1)
sns.histplot(x = "Polarity", data = neg_tweets[neg_tweets["logo"]==0], kde = True,
            bins = 15, color = "yellow")
plt.title("Polarity distribution when logo is not mentioned")
plt.text(-0.6,8,
          "Polarity mean: " + str(round(neg_tweets[neg_tweets["logo"]==0]["Polarity"].mean(),4)))

plt.subplot(1,2,2)
sns.histplot(x = "Polarity", data = neg_tweets[neg_tweets["logo"]==1], kde = True,
            bins = 15, color = "blue")
plt.title("Polarity distribution when logo is mentioned")
plt.text(-0.6,7,
          "Polarity mean: " + str(round(neg_tweets[neg_tweets["logo"]==1]["Polarity"].mean(),4)))
Text(-0.6, 7, 'Polarity mean: -0.2198')

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

Наконец, мы хотим взглянуть на временной ряд средней полярности. Таким образом, мы можем увидеть, изменилось ли общее настроение фанатов с течением времени или оно осталось неизменным.

# Converting timestamp data into datetime
times = pd.to_datetime(tweetsDF["Timestamp"])

# Estimating the mean polarity by hour and minute
polar_date = pd.DataFrame(tweetsDF.groupby([times.dt.hour, times.dt.minute]).Polarity.mean())

# Calculating the number of tweets by hour and minute
date_number = pd.DataFrame(tweetsDF.groupby([times.dt.hour, times.dt.minute]).Tweets.count())

# Estimating the variance for the polarity by hour and minute
polar_var = pd.DataFrame(tweetsDF.groupby([times.dt.hour, times.dt.minute]).Polarity.var())
# Merging the three dataframes that we created 
polar_time = pd.merge(polar_date, date_number, left_index = True, right_index = True)
polar_time = pd.merge(polar_time, polar_var, left_index = True, right_index = True)
# Calculating the upper bound for each estimation of the mean at a 95% confidence level
pol_upper = polar_time["Polarity_x"]+1.96*((polar_time["Polarity_y"]/polar_time["Tweets"])**(0.5))

# Calculating the lower bound for eac estimation of the mean at a 95% confidence level
pol_lower = polar_time["Polarity_x"]-1.96*((polar_time["Polarity_y"]/polar_time["Tweets"])**(0.5))
# Converting the time periods (hour, minute) into strings
time_periods = [str(i) for i in polar_date.index]

# Selecting only a few time periods
xlab_times = [time_periods[i] for i in [0,66,133,200,267,334,401]]
fig, ax = plt.subplots()

ax.plot(time_periods, polar_date)
ax.fill_between(time_periods, pol_lower, pol_upper, alpha = 0.3)
ax.set_xticks(xlab_times)
plt.title("Mean polarity over time")
plt.show()

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

Выводы

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