Вот способ создать похожий сюжет, используя стандартный matplotlib. И симптомы, и происшествия сортируются, чтобы получить макет. 3 вспомогательных участка с общими осями x и y объединяются для создания полного графика.
Код предполагает, что входные данные приведены в виде списка симптомов S для N индивидуумов. 1 представляет присутствующий симптом, в противном случае 0.
Затем для каждого индивидуума симптомы S объединяются в двоичное число (через matmul
со степенью двойки). Эти числа подсчитываются с помощью np.histogram
и сортируются от высокого к низкому.
Некоторые детали не до конца проработаны:
- xlabels и ylabels могут быть добавлены в соответствующих местах
- вероятно, отступы, размеры шрифта, относительная ширина и т.д. c. нужно настроиться на конкретную ситуацию; это сильно зависит от количества симптомов и количества совпадений, чтобы показать, как вещи остаются наиболее читабельными
- изображение в исходном посте, кажется, имеет как серые, так и черные вертикальные линии, но неясно, по какому критерию
Следующий код начинается с фрейма данных, аналогичного приведенному в обновленном вопросе. Затем он преобразуется в массив 2D numpy, пригодный для последующих вычислений и графиков.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
np.random.seed(13579)
def combined_number_to_list(cooc):
''' convert a binary number to a list of its powers of two
e.g. 5 is converted to [0, 2] because 5 == 2**0 + 2**2
'''
return [i for i in range(20) if cooc & (1 << i)]
def cooccurrences_plot(symptoms, occurrences, num_cooc=30, min_cooc_count=1, color='C1'):
''' create a plot of cooccurrences
:param symptoms: list of S symptoms
:param occurrences: NxS array of occurrences of each symptom for a list of N individuals
:param num_cooc: number of cooccurrences to show, maximum would be 2**S - 1
:param min_cooc_count: minimum count of cooccurrences needed to be shown in the plot
'''
num_symp = len(symptoms)
symp_sums = occurrences.sum(axis=0)
symp_order = symp_sums.argsort()
inv_symp_order = symp_order.argsort()
combinations = np.matmul(occurrences, (2 ** np.arange(num_symp))[inv_symp_order])
bins = np.arange(1, 2 ** num_symp + 1)
values, _ = np.histogram(combinations, bins=bins)
cooc_order = (-values).argsort()
num_cooc = np.minimum(num_cooc, len(np.where(values >= min_cooc_count)[0]))
fig, axs = plt.subplots(2, 2, sharex='col', sharey='row', figsize=(10, 5),
gridspec_kw={'width_ratios': [1, 4], 'height_ratios': [2, 3],
'wspace': 0.25, 'hspace': 0.15, 'left': 0.04, 'right': 0.96})
for ax in axs.ravel():
for dir in ['left', 'right', 'top', 'bottom']:
ax.spines[dir].set_visible(False)
axs[0, 0].axis('off')
axs[0, 1].bar(range(num_cooc), values[cooc_order][:num_cooc], ec='white', color=color)
axs[0, 1].tick_params(labelbottom=True, labelleft=True, length=0)
axs[0, 1].tick_params(axis='x', rotation=90)
axs[0, 1].grid(True, axis='y', ls='--')
axs[0, 1].yaxis.set_major_locator(MaxNLocator(6))
axs[0, 1].axhline(0, color=color)
axs[1, 0].barh(np.array(symptoms)[symp_order], symp_sums[symp_order], ec='white', color=color)
axs[1, 0].tick_params(labelbottom=True, labelleft=False, left=False, length=0)
axs[1, 0].invert_xaxis()
axs[1, 0].grid(True, axis='x', ls='--')
axs[1, 0].xaxis.set_major_locator(MaxNLocator(4))
ax = axs[1, 1]
ax.tick_params(labelbottom=False, labelleft=True, length=0)
ax.set_xticks(range(num_cooc))
ax.set_xticklabels(values[cooc_order][:num_cooc])
ax.set_xlim(-1, num_cooc - 0.4)
for i, cooc in enumerate(bins[cooc_order][:num_cooc]):
ax.plot(np.full(num_symp, i), np.arange(num_symp), 'ob-', alpha=0.15, color=color)
occ = combined_number_to_list(cooc)
ax.plot(np.full_like(occ, i), occ, 'ob-', color=color)
N = 8000
symp_probability = np.random.uniform(0.05, 0.80, 15)
data = [[i + 1587150299663, '22/04/2020'] + list(np.random.binomial(1, symp_probability).astype(bool))
for i in range(N)]
df = pd.DataFrame(columns=['id', 'date', 'Q38933', 'Q35805', 'Q767485', 'Q344873', 'Q188008', 'Q86', 'Q9690',
'Q40878', 'Q114085', 'Q474959', 'Q647099', 'Q485831', 'Q5445', 'Q1076369', 'Q3508755'],
data=data)
symptoms = df.columns[2:]
occurrences = df[symptoms].to_numpy()
cooccurrences_plot(symptoms, occurrences, num_cooc=50)
plt.show()
PS: более простой пример ввода:
N = 500
symptoms = ['symptom '+l for l in list('ABCDEF')]
symp_probability = np.random.uniform(0.05, 0.80, len(symptoms))
occurrences = np.random.binomial(1, symp_probability, size=(N, len(symptoms)))
cooccurrences_plot(symptoms, occurrences)
РЕДАКТИРОВАТЬ (алемол): я адаптировал решение с помощью моей таблицы CSV:
canonical_symptoms_name = {
'Q38933': 'fiebre',
'Q35805': 'tos',
'Q767485': 'fallo_respiratorio',
'Q344873': 'sdra',
'Q188008': 'disnea',
'Q86': 'cefalea',
'Q9690': 'cansancio',
'Q40878': 'diarrea',
'Q114085': 'congestión_nasal',
'Q474959': 'mialgia',
'Q647099': 'hemoptisis',
'Q485831': 'linfopenia',
'Q5445': 'anemia',
'Q1076369': 'tormenta_de_citocinas',
'Q3508755': 'síndrome_gripal'
}
canonical_symptoms_order = ['Q38933','Q35805', 'Q767485','Q344873', 'Q188008', 'Q86','Q9690','Q40878','Q114085','Q474959','Q647099','Q485831','Q5445','Q1076369','Q3508755']
symptom_names = [canonical_symptoms_name[code] for code in symptoms_order]
data = pd.read_csv('/data/table.csv', sep=',', parse_dates=True,
dtype={'id': np.string_, 'date':np.datetime64}.update({s: np.bool for s in symptom_names}))
df = pd.DataFrame(columns=['id', 'date']+symptom_names,
data=data)
df = df.loc[:, (df != False).any(axis=0)]
symptoms = df.columns[2:]
occurrences = df[symptoms].to_numpy()
cooccurrences_plot(symptoms, occurrences, num_cooc=50)
plt.show()
дает