Это вторая из четырех статей, направленных на решение проблемы выявления вспышек заболеваний путем извлечения заголовков новостей из популярных источников новостей.

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

Шаг 1. Чтение заголовков

Файл «headline_all ({date}). Txt» считывается и добавляется к массиву заголовков.

file1 = open('headline_all({date}).txt', 'r')
headline_lines = file1.readlines() 
  
headlines = []
for line in headline_lines: 
    headlines.append(line.strip())
headlines = list(set(headlines))
print(len(headlines))

Шаг 2. Уточнение заголовков

Название новостного канала удаляется из конца заголовка, чтобы данные не искажались (например, «Los Angeles Times»).

for i in range(len(headlines)):
    headline = str(headlines[i])
    size = len(headline)-2
    if (size <= 0):
        continue
    while (size != 0):
        if (headline[size-1:size+2] == " | " or headline[size-1:size+2] == ' - ' or headline[size-1:size+2] == ' – '):
            break
        size-=1
    if (size != 0):
        headlines[i] = headline[:size-1]

Шаг 3. Импорт библиотек

Каждая из импортированных библиотек вносит значительный вклад в код.

import pandas as pd
import geonamescache
import numpy as np
import re
import unidecode
from sklearn.cluster import KMeans
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from math import radians, cos, sin, sqrt, asin
import collections

Шаг 4. Получение названий страны, округа и штата

Сначала с помощью библиотеки geonamescache извлекаются названия стран. Создается словарь, в котором хранится название страны и соответствующий счетчик; вначале он инициализируется нулем, так как заголовки не анализируются. Последующий процесс был выполнен с названиями округов и штатов; для названий штатов также был сохранен штат с соответствующим кодом (например, Калифорния и Калифорния были сохранены).

gnc = geonamescache.GeonamesCache()
country_names = [k for k in gnc.get_countries_by_names()]
# Country
country_counter = {}
for index in country_names:
    country_counter[index] = 0
    
# County
county_names = {}
county_counter = {}
for index in gnc.get_us_counties():
    if (not index['name'] in county_names):
        county_names[str(index['name'])] = str(index['state'])
        county_counter[str(index['name'])] = 0
county_names_sorted = list(county_names.keys())
county_names_sorted.sort()
# State
state_names = []
state_keys = {}
state_counter = {}
state_repository = gnc.get_us_states()
for index in list(state_repository.keys()):
    state_names.append(state_repository[index]['name'])
    state_keys[state_repository[index]['code']] = state_repository[index]['name']
    state_counter[state_repository[index]['name']] = 0

Шаг 5. Проверка заголовков городов, штатов, округов и стран

Сначала инициализируется словарь, содержащий соответствующую страну, город, штат и округ для каждого заголовка, если таковой имеется. Заголовки нормализованы (акценты и специальные символы удалены), чтобы устранить любые несоответствия в разных заголовках. Сначала определяются возможные города. Заголовок разбит на массив слов. Подстроки из заголовка, которые определены как город с помощью библиотеки geonamescache, добавляются в массив Poss_cities. В качестве города, который будет добавлен в словарь, будет выбран самый большой город из возможных_cities. После долгих исследований было определено, что это даст наиболее точный город для каждого заголовка; Например, в заголовке «Коронавирус в Нью-Йорке убивает сотни людей», Нью-Йорк и Йорк являются вполне правдоподобными городами, но Нью-Йорк будет выбран как лучший город из двух.

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

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

dictionary = {'headline':[], 'countries':[], 'cities':[], 'states':[], 'counties':[]}
for i in headlines:
    dictionary['headline'].append(i)
    
    # Removing Special Characters
    i = unidecode.unidecode(i)
    words = i.split()
    for x in range(len(words)):
        if (not words[x][-1].isalnum()):
            words[x] = words[x][:-1]
    
    # Checking whether the phrase extracted is the name of a city
    for j in range(len(words)):
        poss_cities = []
        string = words[j]
        if (gnc.get_cities_by_name(words[j]) != []):
            poss_cities.append(string)
        
        for k in range(j+1, len(words)):
            string += " " + words[k]
            if (gnc.get_cities_by_name(string) != []):
                poss_cities.append(string)
        
        if (len(poss_cities) != 0):
            dictionary['cities'].append(poss_cities[-1])
            break
   
    # Checking whether the phrase extracted is the name of a country (excluding Georgia and Jersey)
    for j in range(len(words)):
        poss_countries = []
        string = words[j]
        if (string in country_names):
            poss_countries.append(string)
            if (string in list(country_counter.keys())) and (string != "Georgia") and (string != "Jersey"):
                country_counter[string]+=1
        
        for k in range(j+1, len(words)):
            string += " " + words[k]
            if (string in country_names):
                poss_countries.append(string)
                if (string in list(country_counter.keys())) and (string != "Georgia") and (string != "Jersey"):
                    country_counter[string]+=1
        
        if (len(poss_countries) != 0):
            dictionary['countries'].append(poss_countries[-1])
            break
            
    # Checking whether the phrase extracted is the name of a county
    for j in range(len(words)):
        list_of_counties = list(county_names.keys())
        poss_counties = []
        string = words[j]
        if (string in list_of_counties):
            poss_counties.append(string)
            if string in list(county_counter.keys()):
                county_counter[string]+=1
        
        for k in range(j+1, len(words)):
            string += " " + words[k]
            if (string in list_of_counties):
                poss_counties.append(string)
                if string in list(county_counter.keys()):
                    county_counter[string]+=1
        
        if (len(poss_counties) != 0):
            dictionary['counties'].append(poss_counties[-1])
            dictionary['states'].append(state_keys[county_names[poss_counties[-1]]])
            break
    
    # Checking whether the phrase extracted is the name of a state
    for j in range(len(words)):
        poss_states = []
        string = words[j]
        if (string in state_names):
            poss_states.append(string)
            if string in list(state_counter.keys()):
                state_counter[string]+=1
        
        for k in range(j+1, len(words)):
            string += " " + words[k]
            if (string in state_names):
                poss_states.append(string)
                if string in list(state_counter.keys()):
                    state_counter[string]+=1
        if (len(poss_states) != 0 and len(dictionary['headline']) != len(dictionary['states'])):
            dictionary['states'].append(poss_states[-1])
            break
    
    # Making the country United States if there is a corresponding state or county
    if (len(dictionary['headline']) != len(dictionary['countries'])):
        if (len(dictionary['headline']) == len(dictionary['states'])) or (len(dictionary['headline']) == len(dictionary['counties'])):
            dictionary['countries'].append('United States')
            country_counter['United States']+=1
        else:
            dictionary['countries'].append(np.nan)
            
    # Appending NaN values if the headline doesn't have a corresponding city, country, state, or county
    if (len(dictionary['headline']) != len(dictionary['cities'])):
        dictionary['cities'].append(np.nan)
    if (len(dictionary['headline']) != len(dictionary['counties'])):
        dictionary['counties'].append(np.nan)
    if (len(dictionary['headline']) != len(dictionary['states'])):
        dictionary['states'].append(np.nan)
    while (len(dictionary['states']) > len(dictionary['headline'])):
        dictionary['states'].pop()
df = pd.DataFrame(data = dictionary)

Шаг 6. Определение наиболее пострадавших стран, штатов и округов

Словари state_counter, county_counter и country_counter преобразуются в фреймы данных и сортируются по количеству, чтобы найти наиболее пострадавшие и наименее пострадавшие страны, штаты и округа.

def convert_dict_to_df(dict1, value):
    dictionary_counter = {value: [], 'Count':[]}
    for index in list(dict1.keys()):
        dictionary_counter[value].append(index)
        dictionary_counter['Count'].append(dict1[index])
    df = pd.DataFrame(data = dictionary_counter, columns = [value, 'Count'])
    df = df.sort_values(by = ['Count'], ascending = False)
    df = df.reset_index()
    df.index += 1
    del df['index']
    return df
    
df_state = convert_dict_to_df(state_counter, 'State')
df_county = convert_dict_to_df(county_counter, 'County')
df_country = convert_dict_to_df(country_counter, 'Country')

Шаг 7. Определение широты и долготы для каждого заголовка

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

gnc = geonamescache.GeonamesCache()
states_lat_long = {
    'Alabama': [32.806671, -86.791130],
    'Alaska': [61.370716, -152.404419],
    'Arizona': [33.729759, -111.431221],
    'Arkansas': [34.969704, -92.373123],
    'California': [36.116203, -119.618564],
    'Colorado': [39.059811, -105.311104], 
    'Connecticut': [41.597782, -72.755371],
    'Delaware': [39.318523, -75.507141],
    'District of Columbia': [38.897438, -77.026817],
    'Florida': [27.766279, -81.686783],
    'Georgia': [33.040619, -83.643074],
    'Hawaii': [21.094318, -157.498337],
    'Idaho': [44.240459, -114.478828],
    'Illinois': [40.349457, -88.986137],
    'Indiana': [39.849426, -86.258278],
    'Iowa': [42.011539, -93.210526],
    'Kansas': [38.526600, -96.726486],
    'Kentucky': [37.668140, -84.670067],
    'Louisiana': [31.169546, -91.867805],
    'Maine': [44.693947, -69.381927],
    'Maryland': [39.063946, -76.802101],
    'Massachusetts': [42.230171, -71.530106],
    'Michigan': [43.326618, -84.536095],
    'Minnesota': [45.694454, -93.900192],
    'Mississippi': [32.741646, -89.678696],
    'Missouri': [38.456085, -92.288368],
    'Montana': [46.921925, -110.454353],
    'Nebraska': [41.125370, -98.268082],
    'Nevada': [38.313515, -117.055374],
    'New Hampshire': [43.452492, -71.563896],
    'New Jersey': [40.298904, -74.521011],
    'New Mexico': [34.840515, -106.248482],
    'New York': [42.165726, -74.948051],
    'North Carolina': [35.630066, -79.806419],
    'North Dakota': [47.528912, -99.784012],
    'Ohio': [40.388783, -82.764915],
    'Oklahoma': [35.565342, -96.928917],
    'Oregon': [44.572021, -122.070938],
    'Pennsylvania': [40.590752, -77.209755],
    'Rhode Island': [41.680893, -71.511780],
    'South Carolina': [33.856892, -80.945007],
    'South Dakota': [44.299782, -99.438828],
    'Tennessee': [35.747845, -86.692345],
    'Texas': [31.054487, -97.563461],
    'Utah': [40.150032, -111.862434],
    'Vermont': [44.045876, -72.710686],
    'Virginia': [37.769337, -78.169968],
    'Washington': [47.400902, -121.490494],
    'West Virginia': [38.491226, -80.954453],
    'Wisconsin': [44.268543, -89.616508],
    'Wyoming': [42.755966, -107.302490]
}
file2 = open('county_lat_long.txt', 'r') 
counties = file2.readlines()
county_data = {} 
for i in range(50, len(counties)):
    county_values = counties[i].strip().split('\t')
    county_data[county_values[3]] = [county_values[-2], county_values[-1]]
    
file2 = open('country_lat_long.txt', 'r') 
countries = file2.readlines()
country_data = {}
for i in range(len(countries)):
    country_values = countries[i].strip().split('\t')
    country_data[country_values[-1]] = [country_values[-3], country_values[-2]]

Шаг 8: Удаление городов с низкой плотностью населения

Во-первых, заботятся о значениях «NaN». Широта и долгота сначала проверялись для города, затем округа, затем штата, а затем страны. Поскольку есть несколько мест с одним и тем же названием, был добавлен город с наибольшим населением. Кроме того, были исключены города с населением менее 50 000 человек, поскольку маловероятно, что эти города будут упомянуты авторитетным источником новостей. Широта, долгота и соответствующий код страны добавляются в фрейм данных.

latitude = []
longitude = []
country_code = []
for index_val in df1.index:
    city = df1['cities'][index_val]
    state = df1['states'][index_val]
    country = df1['countries'][index_val]
    county = df1['counties'][index_val]
    val = gnc.get_cities_by_name(city)
    
    # Listing the order of priority: city, county, state, then country
    if (city == np.nan or val == []):
        if (county in list(county_counter.keys())):
            latitude.append(county_data[county][0])
            longitude.append(county_data[county][1])
        else:
            if (state in state_names):
                latitude.append(states_lat_long[state][0])
                longitude.append(states_lat_long[state][1])
            else:
                if (country in list(country_data.keys())):
                    latitude.append(float(country_data[country][0]))
                    longitude.append(float(country_data[country][1]))
                else:
                    latitude.append(np.nan)
                    longitude.append(np.nan)
        country_code.append(np.nan)
    else:
        # Extracting places with a population of more than 50,000 (excluding Latina and York)
        maxpop = 0
        index = 0
        for j in range(len(val)):
            keys = [e for e in val[j]]
            population = val[j][keys[0]]['population']
            if (population > maxpop):
                maxpop = population
                index = j
        keys = [e for e in val[index]]
        if (maxpop <= 50000) or (city == "York") or (city == "Latina" and country != "Spain"):
            latitude.append(np.nan)
            longitude.append(np.nan)
            country_code.append(np.nan)
            df1.loc[df1.index == index_val, 'cities']=np.nan
        else:
            latitude.append(val[index][keys[0]]['latitude'])
            longitude.append(val[index][keys[0]]['longitude'])
            country_code.append(val[index][keys[0]]['countrycode'])
        #print(index)
        if (str(city) == str(state)):
            df1.loc[df1.index == index_val, 'cities']=np.nan
        if (str(country) == str(city)):
            df1.loc[df1.index == index_val, 'cities']=np.nan
            
latitude = [float(i) for i in latitude]
longitude = [float(i) for i in longitude]
df1['latitude'] = latitude
df1['longitude'] = longitude
df1['countrycode'] = country_code
df = df1.dropna(subset = ['latitude', 'longitude'])
df = df.reset_index()
del df['index']

Шаг 9. Разделение фрейма данных

Во-первых, фрейм данных делится на четыре фрейма данных: df_us (фрейм данных, содержащий заголовки, относящиеся к США), df_no_us (фрейм данных, содержащий заголовки, относящиеся к США, с только «широтой» и «долготой» в качестве столбцов), df_world (исходный фрейм данных) и df_no_world (исходный фрейм данных с только «широтой» и «долготой» в качестве столбцов).

df_us = {'headline':[], 'cities':[], 'latitude':[], 'counties': [], 'states': [], 'countries': [], 'longitude':[], 'countrycode':[]}
df_world = df
df_no_us = {'latitude':[], 'longitude':[]}
df_no_world = {'latitude':[], 'longitude':[]}
for index in df.index:
    if (df['countries'][index] == "United States"):
        for column in list(df.columns):
            df_us[column].append(df[column][index])
        for column in ['latitude', 'longitude']:
            df_no_us[column].append(df[column][index])
    for column in ['latitude', 'longitude']:
        df_no_world[column].append(df[column][index])
            f_us = pd.DataFrame(data = df_us)
df_no_us = pd.DataFrame(data = df_no_us)
df_no_world = pd.DataFrame(data = df_no_world)
df_us

Шаг 9. Построение кривой изгиба для определения количества кластеров

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

def elbow_curve(df1):
    clusters = range(1, 50)
    kmeans_elbow = [KMeans(n_clusters=i) for i in clusters]
    score = [kmeans_elbow[i].fit(df1).score(df1) for i in range(len(kmeans_elbow))]
    plt.plot(clusters, score)
    plt.xlabel('Number of Clusters')
    plt.ylabel('Score')
    plt.title('Elbow Curve')
    plt.show()
    
elbow_curve(df_no_us)
elbow_curve(df_no_world)

Шаг 10. Запуск алгоритма K-средних

Используя библиотеку sklearn, была создана функция, которая реализует алгоритм кластеризации k-средних по столбцам широты и долготы фрейма данных. Кроме того, для облегчения работы было выбрано заранее определенное количество кластеров США и мира. Функция запускалась с фреймами данных США и мира.

def run_k_means(df1, num_cluster, printGraph):
    #Adding to Dataframe
    kmeans_elbow = KMeans(n_clusters=num_cluster-1)
    df1["cluster_label"] = kmeans_elbow.fit(df1).labels_
if (printGraph):
        kmeans = KMeans(n_clusters=num_cluster).fit(df1)
        centroids = kmeans.cluster_centers_
plt.scatter(df1['latitude'], df1['longitude'], c= kmeans.labels_.astype(float), s=50, alpha=0.5)
        plt.scatter(centroids[:, 0], centroids[:, 1], c='red', s=50)
        plt.show()
        
us_clusters = 30
run_k_means(df_no_us, us_clusters, True)
world_clusters = 30
run_k_means(df_no_world, world_clusters, True)

Щелкните эту ссылку для доступа к репозиторию Github с подробным объяснением кода: Github.