plot_pf_estimates.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #!/user/bin/env python
  2. # coding=utf-8
  3. """
  4. @author: yannansu
  5. @created at: 25.06.21 11:18
  6. Generate publication-quality figures of psychometric function estimates.
  7. """
  8. import matplotlib.pyplot as plt
  9. import seaborn as sns
  10. from matplotlib.ticker import MultipleLocator, FormatStrFormatter, MaxNLocator
  11. import pandas as pd
  12. import numpy as np
  13. from data_analysis.color4plot import color4plot
  14. # Figure configuration
  15. # sns.set_context("poster")
  16. plt.style.use('data_analysis/figures/plot_style.txt')
  17. # plt.style.use('data_analysis/figures/plot_style_poster.txt')
  18. alpha = {'line': 1., 'marker': 0.7}
  19. capsize = 4
  20. fist_x = 22.5
  21. # x_ticks = np.linspace(0 + fist_x, 360 + fist_x, 8, endpoint=False)
  22. x_major_ticks = np.array([0, 90, 180, 270, 360])
  23. x_minor_ticks = np.linspace(0, 360, 8, endpoint=False)
  24. legend_loc = [0.75, 0.9]
  25. gridline = {'color': [.8, .8, .8], 'alpha': 0.4, 'width': 1.5}
  26. def rep_end(estimates):
  27. """
  28. Duplicate the first and the last stimuli for visualization.
  29. """
  30. first = estimates[estimates['Hue Angle'] == estimates['Hue Angle'].min()]
  31. first_copy = first.copy()
  32. first_copy['Hue Angle'] = first_copy['Hue Angle'].apply(lambda x: x + 360)
  33. last = estimates[estimates['Hue Angle'] == estimates['Hue Angle'].max()]
  34. last_copy = last.copy()
  35. last_copy['Hue Angle'] = last_copy['Hue Angle'].apply(lambda x: x - 360)
  36. estimates_plt = pd.concat([estimates, first_copy, last_copy], ignore_index=False).sort_values('Hue Angle')
  37. return estimates_plt
  38. # JND
  39. def plot_all_JND_by_condition(estimates, save_pdf=None):
  40. cond_list = ['LL', 'HH', 'LH']
  41. title_list = ['L vs. L', 'H vs. H', 'L vs. H']
  42. # cond_list = estimates['condition'].unique()
  43. cond_num = len(cond_list)
  44. fig, axes = plt.subplots(nrows=1, ncols=cond_num, figsize=[10, 5])
  45. # plt.suptitle('Discrimination Thresholds Across Conditions')
  46. sub_list = np.sort(estimates['subject'].unique())
  47. sub_num = len(sub_list)
  48. l_gray = np.repeat(np.linspace(.2, .8, sub_num, endpoint=True), 3).reshape(-1, 3)
  49. l_color = {s: g for s, g in zip(sub_list, l_gray)}
  50. dt_color = color4plot(estimates['Hue Angle'].unique())
  51. markers = ['o', '^', 'v', 'X', 's', '+', 'd', '*']
  52. df_marker = {s: m for s, m in zip(sub_list, markers[0:sub_num])}
  53. for idx, cond in enumerate(cond_list):
  54. axes[idx].set_title(title_list[idx])
  55. for key, grp in estimates[estimates.condition == cond].groupby(['subject']):
  56. axes[idx].errorbar(x=grp['Hue Angle'], y=grp['JND'], yerr=grp['JND_err'],
  57. c=l_color[key], linestyle='dashed', capsize=capsize)
  58. axes[idx].scatter(x=grp['Hue Angle'], y=grp['JND'],
  59. c=dt_color, marker=df_marker[key], edgecolors='none', label=key)
  60. axes[idx].set_xlabel('Hue Angle (deg)')
  61. axes[idx].set_xticks(x_major_ticks)
  62. # axes[idx].set_xlim([-fist_x, 360 + fist_x])
  63. axes[idx].set_xlim([0, 360])
  64. axes[idx].set_xticklabels(x_major_ticks, rotation=45)
  65. ylim = 25
  66. axes[idx].set_ylim([0, ylim])
  67. # [plt.vlines(x, 0, ylim, colors='grey', linestyles='-', alpha=0.5) for x in [0, 360, 112.5, 112.5 + 180]]
  68. leg = axes[0].legend()
  69. [marker.set_color([.66, .66, .66]) for marker in leg.legendHandles]
  70. axes[0].set_ylabel('JND (deg)')
  71. plt.tight_layout()
  72. if save_pdf is not None:
  73. plt.savefig('data_analysis/figures/PF_estimates' + save_pdf + '.pdf')
  74. plt.show()
  75. def plot_all_PSE_lh(estimates, save_pdf=None):
  76. fig, ax = plt.subplots(figsize=(7, 5))
  77. # plt.title('Relative Bias (L vs. H)')
  78. sub_list = np.sort(estimates['subject'].unique())
  79. sub_num = len(sub_list)
  80. l_gray = np.repeat(np.linspace(.2, .8, sub_num, endpoint=True), 3).reshape(-1, 3)
  81. l_color = {s: g for s, g in zip(sub_list, l_gray)}
  82. dt_color = color4plot(estimates['Hue Angle'].unique())
  83. markers = ['o', '^', 'v', 'X', 's', '+', 'd', '*']
  84. df_marker = {s: m for s, m in zip(sub_list, markers[0:sub_num])}
  85. dt_size = 50
  86. for key, grp in estimates[estimates.condition == 'LH'].groupby(['subject']):
  87. ax.errorbar(x=grp['Hue Angle'], y=grp['PSE'], yerr=grp['PSE_err'],
  88. c=l_color[key], linestyle='dashed', capsize=capsize)
  89. ax.scatter(x=grp['Hue Angle'], y=grp['PSE'],
  90. c=dt_color, s=dt_size, marker=df_marker[key], edgecolors='none', label=key)
  91. leg = ax.legend()
  92. [marker.set_color([.66, .66, .66]) for marker in leg.legendHandles]
  93. plt.xlabel('Hue Angle (deg)')
  94. fist_x = 22.5
  95. x_ticks = np.linspace(0 + fist_x, 360 + fist_x, 8, endpoint=False)
  96. plt.xticks(x_ticks)
  97. plt.xlim([0, 360])
  98. # plt.hlines(0, 0, 360, colors='grey', alpha=0.5)
  99. plt.ylabel('PSE (deg)')
  100. ylim_abs = 12
  101. plt.ylim([-ylim_abs, ylim_abs])
  102. # [plt.vlines(x, -ylim_abs, ylim_abs, colors='grey', linestyles='-', alpha=0.5) for x in [0, 360, 112.5, 112.5 + 180]]
  103. if save_pdf is not None:
  104. plt.savefig('data_analysis/figures/PF_estimates' + save_pdf + '.pdf')
  105. plt.show()
  106. def plot_sub_estimates(estimates, save_pdf=None):
  107. labels = {'LL': 'L vs. L',
  108. 'HH': 'H vs. H',
  109. 'LH': 'L vs. H'}
  110. linecolors = {'LL': [.3, .3, .3],
  111. 'HH': [.7, .7, .7],
  112. 'LH': [.5, .5, .5]}
  113. markers = {'LL': 'o',
  114. 'HH': 's',
  115. 'LH': 'v'}
  116. marker_color = color4plot(estimates['Hue Angle'].unique())
  117. fig, axes = plt.subplots(figsize=(3.5, 10), nrows=3, ncols=1, sharex='none', sharey='none')
  118. # Plot JND, same-noise
  119. # [axes[0].vlines(x, 0, 15, alpha=gridline['alpha'], colors=gridline['color'], linewidth=gridline['width'], zorder=1)
  120. # for x in x_major_ticks]
  121. for key, grp in estimates.query("condition=='LL' or condition=='HH'").groupby('condition'):
  122. axes[0].errorbar(x=grp['Hue Angle'], y=grp['JND'], yerr=grp['JND_err'],
  123. c=linecolors[key], linestyle='dashed', capsize=capsize, zorder=2)
  124. axes[0].scatter(x=grp['Hue Angle'], y=grp['JND'],
  125. c=marker_color, marker=markers[key], edgecolors='none', alpha=alpha['marker'],
  126. label=labels[key], zorder=3)
  127. # ax[0].set_yscale('log')a
  128. # ax[0].set_ylim([10**(0), 10**1.5])
  129. # axes[0].set_ylim([0, 25])
  130. # axes[0].set_yticks([0, 5, 10, 15, 20, 25])
  131. axes[0].set_ylim([0, 15]) # For s5 and sAVG, use smaller scale
  132. axes[0].set_yticks([0, 5, 10, 15])
  133. axes[0].set_ylabel('JND (deg), same-noise')
  134. axes[0].set_xlim([0, 360])
  135. axes[0].xaxis.set_minor_locator(plt.FixedLocator(x_minor_ticks))
  136. axes[0].xaxis.set_major_locator(plt.FixedLocator(x_major_ticks))
  137. leg = axes[0].legend(loc=legend_loc)
  138. # leg.get_frame().set_linewidth(0.0)
  139. # leg.get_frame().set_facecolor('none')
  140. # [marker.set_color([.66, .66, .66]) for marker in leg.legendHandles]
  141. # Plot JND, cross-noise
  142. # [axes[1].vlines(x, 0, 15, alpha=gridline['alpha'], colors=gridline['color'], linewidth=gridline['width'], zorder=1)
  143. # for x in x_major_ticks]
  144. estimates_LH = estimates.query("condition=='LH'")
  145. axes[1].errorbar(x=estimates_LH['Hue Angle'], y=estimates_LH['JND'], yerr=estimates_LH['JND_err'],
  146. c=linecolors['LH'], linestyle='dashed', capsize=capsize, zorder=2)
  147. axes[1].scatter(x=estimates_LH['Hue Angle'], y=estimates_LH['JND'],
  148. c=marker_color, marker=markers['LH'], edgecolors='none', alpha=alpha['marker'],
  149. label='L vs. H', zorder=3)
  150. # ax[1].set_yscale('log')
  151. # ax[1].set_ylim([10**(-1), 10**1.5])
  152. axes[1].set_ylim([0, 15])
  153. axes[1].set_yticks([0, 5, 10, 15])
  154. axes[1].set_ylabel('JND (deg), cross-noise')
  155. axes[1].set_xlim([0, 360])
  156. axes[1].xaxis.set_minor_locator(plt.FixedLocator(x_minor_ticks))
  157. axes[1].xaxis.set_major_locator(plt.FixedLocator(x_major_ticks))
  158. # plt.setp(axes[1].xaxis.get_majorticklabels(), rotation=0)
  159. axes[1].set_xlabel('Hue Angle (deg)')
  160. leg = axes[1].legend(loc=legend_loc)
  161. # leg.get_frame().set_linewidth(0.0)
  162. # leg.get_frame().set_facecolor('none')
  163. # [marker.set_color([.66, .66, .66]) for marker in leg.legendHandles]
  164. # Plot PSE, cross-noise
  165. # [axes[2].vlines(x, -10, 10,
  166. # alpha=gridline['alpha'], colors=gridline['color'], linewidth=gridline['width'], zorder=1)
  167. # for x in x_major_ticks]
  168. axes[2].errorbar(x=estimates_LH['Hue Angle'], y=estimates_LH['PSE'], yerr=estimates_LH['PSE_err'],
  169. c=linecolors['LH'], linestyle='dashed', capsize=capsize, zorder=2)
  170. axes[2].scatter(x=estimates_LH['Hue Angle'], y=estimates_LH['PSE'],
  171. c=marker_color, marker=markers['LH'], edgecolors='none', alpha=alpha['marker'],
  172. label='L vs. H', zorder=3)
  173. # axes[2].hlines(0, 0, 360, alpha=gridline['alpha'], colors=gridline['color'], linewidth=gridline['width'],zorder=1)
  174. # axes[2].set_ylim([-15, 15])
  175. axes[2].set_ylim([-10, 10]) # For s5 and sAVG, use smaller scale
  176. axes[2].set_yticks([-15, -10, -5, 0, 5, 10, 15])
  177. axes[2].set_ylabel('PSE (deg), cross-noise')
  178. axes[2].set_xlim([0, 360])
  179. axes[2].xaxis.set_minor_locator(plt.FixedLocator(x_minor_ticks))
  180. axes[2].xaxis.set_major_locator(plt.FixedLocator(x_major_ticks))
  181. plt.setp(axes[2].xaxis.get_majorticklabels(), rotation=0)
  182. # axes[2].set_xticks(x_ticks)
  183. # axes[2].set_xticklabels(x_ticks, rotation=45)
  184. axes[2].set_xlabel('Hue Angle (deg)')
  185. plt.tight_layout()
  186. if save_pdf is not None:
  187. plt.savefig('data_analysis/figures/PF_estimates/' + save_pdf + '.pdf')
  188. plt.show()
  189. # ================================== Generate plots ================================
  190. """
  191. # 1. For single subject's estimates
  192. # 1.1. Load data
  193. all_estimates = pd.read_csv('data_analysis/pf_estimates/all_estimates.csv')
  194. all_estimates = all_estimates.query("subject == 's5'")
  195. # 1.2. Plot pooled data
  196. plot_all_JND_by_condition(rep_end(all_estimates), save_pdf='pooled_JND_by_cond')
  197. plot_all_PSE_lh(rep_end(all_estimates), save_pdf='pooled_PSE_lh')
  198. # 1.3. Plot estimates for each subject
  199. for key, grp in all_estimates.groupby('subject'):
  200. plot_sub_estimates(rep_end(grp), save_pdf='PF_estimates' + '_' + key)
  201. """
  202. """
  203. # 2.For average data/estimates
  204. # 2.1. Average subject: estimates from pooled data
  205. avg_estimates = pd.read_csv('data_analysis/pf_estimates/avg_estimates.csv')
  206. plot_all_PSE_lh(rep_end(avg_estimates), save_pdf='sAvg_PSE_lh')
  207. plot_sub_estimates(rep_end(avg_estimates), save_pdf='PF_estimates_sAvg')
  208. """
  209. """
  210. # 2.2 Average of estimates
  211. simple_avg_estimates = pd.read_csv('data_analysis/pf_estimates/simple_avg_estimates.csv')
  212. plot_sub_estimates(rep_end(simple_avg_estimates), save_pdf='PF_estimates_simple_avg')
  213. """