OSCR

Validation of human kallikrein 6 in the cerebrospinal fluid of patients with progressive and non-progressive alzheimer's disease: correlation with other biomarkers.

Code ↔ Paper

2 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 2 matches
  1. [1] § Results › Correlation of CSF hK6 concentration and APOE status ↔ KLK6_ELISA_data_analysis.ipynb, lines 189–199 · score 0.72 · e2e3, e2e4, e3e3, e3e4, status, APOE
  2. [2] § Methods › Statistical analysis ↔ KLK6_ELISA_data_analysis.ipynb, lines 2178–2218 · score 0.64 · post hoc, Mann Whitney, Kruskal Wallis, Holm

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 2,452 lines · 98 KB · MIT · 2 matches

  1. # %% [markdown]
  2. # # Analysis of Kallikrein-6 protein (hK6) concentration in patients with Alzheimer's disease and controls
  3. # %% [markdown]
  4. # Important notes:
  5. # Please read the README.md file prior to running the code to ensure reproducibility.
  6. #
  7. # The analysis herein is published in the following paper:
  8. # Chatanaka MK, Soosaipillai A, Morato X, Diamandis E. Human Kallikrein 6 in the cerebrospinal fluid of patients with progressive and non-progressive Alzheimer’s disease. 2025. In preparation.
  9. #
  10. # %%
  11. #important libraries to load
  12. import numpy as np
  13. import matplotlib.pyplot as plt
  14. import pandas as pd
  15. import scipy.stats as stats # for stats.probplot, check QQ plot for normally distributed data
  16. import seaborn as sns
  17. from scipy.stats import f_oneway # one-way ANOVA
  18. import statsmodels.api as sm
  19. import statsmodels.formula.api as smf
  20. import pingouin as pg #for for prtial correlation to adjust for sex
  21. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  22. from statsmodels.stats.multitest import multipletests #for post-hoc Holm correction
  23. from dateutil.relativedelta import relativedelta #use this to accurately account for month and day differences when calculating age
  24. # %%
  25. #Read files based on location (SHOULD BE CHANGED BY USER)
  26. file_path = "C:\\Users\\miyoh\\Git_Hub_Work\\Python_KLK6_data_analysis\\KLK6_ELISA_data_correct_for_dilution.xlsx" #Edit accordingly to where the file is
  27. file_path2= "C:\\Users\\miyoh\\Git_Hub_Work\\Python_KLK6_data_analysis\\20240321_400CSF_selection_for_Toronto_reduced_FC_Corrected.xlsx" #clinical information of patients from collaborator
  28. data= pd.read_excel(file_path, "Sheet2")
  29. clinical_info = pd.read_excel(file_path2, "Sheet 1")
  30. # %% [markdown]
  31. # ## Preliminary checks
  32. # %% [markdown]
  33. # ### Prepare raw df (not clinical information dataframe)
  34. # %%
  35. data #check that it was read fine
  36. #Result: it has 394 rows and 4 columns, but we should have 393 samples.
  37. # %%
  38. #the columns from df need to be renamed
  39. data.columns = data.iloc[0] #the new column names are taken from row 1
  40. data = data.drop(data.index[0]) #drop the first row, since we have now saved is as column names
  41. # %%
  42. data #check that it worked, new column names should be Value, ID, Group, Original_ID_ACE
  43. #Results: 393 rows and 4 columns, Column headers are correct
  44. # %%
  45. #get info on data types and missing values
  46. data.info()
  47. #Results: no NAs, all types are object types
  48. # %%
  49. #make Value column a numeric value for proper subsequent analyses
  50. # with this code, the other three columns remain intact, without being removed
  51. data = data[["ID", "Group", "Value", "Original_ID_ACE"]].apply(pd.to_numeric, errors = "ignore")
  52. # %%
  53. #Define order of groups for later graphs
  54. group_order = ['SCD', 'MCI non Progressor', 'MCI Progressor', 'AD']
  55. # %% [markdown]
  56. # ### Prepare clinical information
  57. # %%
  58. clinical_info
  59. #correctly has 393 samples, 46 columns, but not all are needed
  60. # %%
  61. #Access column names, so that a selection can be made
  62. clinical_info.columns
  63. # %%
  64. # Create another dataframe with picked columns
  65. clinical_info_chosen = clinical_info[["Aliq", "MMSE_PL", "APOE", "Sex",
  66. "F.Nacimiento", "FechaLCR", "Abeta_42_LCR",
  67. "P_tau_LCR", "T_tau_LCR"]]
  68. # %%
  69. #check clinical_info_chosen
  70. #this will show the top 5 and bottom 5 rows
  71. clinical_info_chosen
  72. #393 rows x9 columns
  73. # %%
  74. #get info on data types and missing values
  75. clinical_info_chosen.info()
  76. #Results: There is some missing data that will be dealt with later on
  77. # %%
  78. #turn F.Nacimiento (date of birth) into datetime()
  79. clinical_info_chosen["F.Nacimiento"] = pd.to_datetime(clinical_info_chosen["F.Nacimiento"])
  80. # %%
  81. #Create a new column for Age = FechaLCR- F.Nacimiento
  82. clinical_info_chosen['Age'] = [relativedelta(i,j).years for i,j in zip(clinical_info_chosen["FechaLCR"], clinical_info_chosen["F.Nacimiento"])]
  83. # %%
  84. #Rename Masculino = Male, Femenino = Female
  85. clinical_info_chosen["Sex"] = clinical_info_chosen["Sex"].replace("Masculino", "Male")
  86. clinical_info_chosen["Sex"] = clinical_info_chosen["Sex"].replace("Femenino", "Female")
  87. # %%
  88. #check results
  89. print("The clinical data look like this:")
  90. clinical_info_chosen
  91. #Results: in column Sex, we should see Male, Female; in column Age, we should see the result of FechaLCR - F.Nacimiento as integer numbers.
  92. # %% [markdown]
  93. # ### Combine df with clinical_info_chosen
  94. # %%
  95. #Merge the dataframes with different column names
  96. df = pd.merge(data, clinical_info_chosen, left_on= "Original_ID_ACE", right_on= "Aliq", how= "left")
  97. print("\nMerged DataFrame (Left Join with different column names):")
  98. print(df) #check result
  99. #Result: 393 rows x 14 columns
  100. # %% [markdown]
  101. # ### Table 1
  102. # %%
  103. #Count percentage of female and male per Group
  104. # Count and percentage by group
  105. gender_percentage = df.groupby('Group')['Sex'].value_counts(normalize=True).mul(100).round(2) #round to 2 decimals
  106. print("Percentage of each sex by group:")
  107. print(gender_percentage)
  108. # %%
  109. #Count median of MMSE score per group
  110. mmse_counts = df.groupby('Group')['MMSE_PL'].median().round(2) #round to 2 decimals
  111. print("Median of the MMSE Score by group:")
  112. print(mmse_counts)
  113. # %%
  114. #Count median of CSF Abeta 42 per group
  115. Abeta_counts = df.groupby('Group')['Abeta_42_LCR'].median().round(2) #round to 2 decimals
  116. print("Median of the Amyloid beta 1-42 by group:")
  117. print(Abeta_counts)
  118. # %%
  119. #Count median of CSF p-tau per group
  120. ptau_counts = df.groupby('Group')['P_tau_LCR'].median().round(2) #round to 2 decimals
  121. print("Median of the p-tau by group:")
  122. print(ptau_counts)
  123. # %%
  124. #Count median of CSF t-tau per group
  125. ttau_counts = df.groupby('Group')['T_tau_LCR'].median().round(2) #round to 2 decimals
  126. print("Median of the t-tau by group:")
  127. print(ttau_counts)
  128. # %%
  129. #Count median of CSF hK6
  130. hk6_counts = df.groupby("Group")['Value'].median().round(2)
  131. print("hK6 median in each group:")
  132. print(hk6_counts)
  133. # %% [markdown]
  134. # ### Modifications to df
  135. # %%
  136. # Change APOE status to correct versions with Greek lettering as per allele naming
  137. df.loc[df['APOE'] == 'e2e3', 'APOE'] = 'ε2ε3'
  138. df.loc[df['APOE'] == 'e2e4', 'APOE'] = 'ε2ε4'
  139. df.loc[df['APOE'] == 'e3e3', 'APOE'] = 'ε3ε3'
  140. df.loc[df['APOE'] == 'e3e4', 'APOE'] = 'ε3ε4'
  141. df.loc[df['APOE'] == 'e4e4', 'APOE'] = 'ε4ε4'
  142. # %% [markdown]
  143. # ### Kruskal Wallis for all
  144. # %%
  145. #Perform Kruskal Wallis and Holm's correction
  146. variables = ['Age', 'MMSE_PL', 'Abeta_42_LCR', 'P_tau_LCR', 'T_tau_LCR', 'Value'] # Variables to perform Kruskal Wallis on
  147. # Create a results list to store all statistics
  148. results = []
  149. p_values = [] # Store p-values for later correction
  150. print("Kruskal-Wallis Test Results for Multiple Variables")
  151. print("=" * 60)
  152. for var in variables:
  153. print(f"\nVariable: {var}")
  154. print("-" * 30)
  155. # Extract data for each group
  156. group_data = []
  157. for group in group_order:
  158. group_values = df[df['Group'] == group][var].dropna()
  159. group_data.append(group_values)
  160. print(f"Group {group}: n={len(group_values)}, median={group_values.median():.2f}")
  161. # Perform Kruskal-Wallis test
  162. h_stat, p_value = stats.kruskal(*group_data)
  163. print(f"Kruskal-Wallis H-statistic: {h_stat:.3f}, p-value: {p_value:.4f}")
  164. # Store results
  165. result_dict = {
  166. 'Variable': var,
  167. 'H_statistic': h_stat,
  168. 'p_value': p_value,
  169. 'significant': p_value < 0.05
  170. }
  171. # Add group medians and sample sizes
  172. for i, group in enumerate(group_order):
  173. result_dict[f'Group_{group}_median'] = group_data[i].median()
  174. result_dict[f'Group_{group}_n'] = len(group_data[i])
  175. results.append(result_dict)
  176. p_values.append(p_value) # Collect p-value for correction
  177. # Apply Holm correction to all p-values AFTER collecting them all
  178. rejected, holm_p_values, _, _ = multipletests(p_values, alpha=0.05, method='holm')
  179. # Update results with Holm-corrected p-values
  180. for i, result in enumerate(results):
  181. result['Adjusted_p_value'] = holm_p_values[i]
  182. result['Significant_after_correction'] = holm_p_values[i] < 0.05
  183. # Convert to DataFrame for easier viewing
  184. results_df = pd.DataFrame(results)
  185. # Print summary of results
  186. print("\n\nSummary with Holm-Bonferroni Correction:")
  187. print("=" * 60)
  188. for i, row in results_df.iterrows():
  189. sig_symbol = "*" if row['Significant_after_correction'] else ""
  190. print(f"{row['Variable']:15} p={row['p_value']:.4f} -> adj_p={row['Adjusted_p_value']:.4f} {sig_symbol}")
  191. # %% [markdown]
  192. # ## Check normality, variance (homoscedasticity) of hK6 data
  193. # %% [markdown]
  194. # To see if the data are normally distributed, it is possible to run two statistical tests:
  195. # - Shapiro-Wilk Test, which is suitable for smaller sample sizes (typically <50), where the null hypothesis is that the data are normally distributed.
  196. # - D'Agostino's K-squared test (normaltest), which is based on skewness and kurtosis and is generally suitable for larger sample sizes. Similar to Shapiro-Wilk, a low p-value suggests non-normalilty.
  197. # %%
  198. #Perform D'Agostino K^2 test
  199. groups = df['Group'].unique() #group by Group column
  200. for group_name in groups:
  201. group_data = df[df["Group"] == group_name]["Value"] #group the values based on the Groups
  202. statistic, p_value = stats.normaltest(group_data) #if we wanted to do Shapiro-Wilk, we would run a shapiro()
  203. print(f"Normality test for Group {group_name}:")
  204. print(f" D'Agostino K^2 Statistic: {statistic:.4f}")
  205. print(f" P-value: {p_value:.4f}")
  206. alpha = 0.05 #significance level
  207. if p_value > alpha:
  208. print(f"Result: Data for Group {group_name} appears to be normally distributed (fail to reject H0)")
  209. else:
  210. print(f"Result: Data for group {group_name} does not appear to be normally distributed (reject H0)")
  211. print("-" * 30)
  212. # %%
  213. #Alternatively, check normal distribution using QQ plot
  214. #It helps to visualize the distribution
  215. fix, axes= plt.subplots(2,2, figsize=(8,6), dpi=600)
  216. axes=axes.flatten() #Flatten the 2x2 array of axes for easy indexing
  217. for i, group_name in enumerate(groups):
  218. if i <len(axes): #this ensures that we don't exceed the number of subplots
  219. group_data = df[df['Group'] == group_name]['Value'].dropna()
  220. stats.probplot(group_data, dist= 'norm', plot=axes[i])
  221. axes[i].set_title(f"Q-Q Plot for {group_name} Group \n (n= {len(group_data)})")
  222. plt.tight_layout()
  223. plt.suptitle("Q-Q Plots for Normality Assessment", y= 1.02, fontsize= 14)
  224. plt.show()
  225. # %%
  226. #Since our data is not normally distributed, we log2 transform our values
  227. #in a new column, which will be used from now on for any parametric tests chosen
  228. df["log2_Value"] = np.log2(df["Value"])
  229. # %%
  230. #Check for normality again using the log2 values
  231. for group_name in groups:
  232. group_data = df[df["Group"] == group_name]["log2_Value"]
  233. statistic, p_value = stats.normaltest(group_data)
  234. print(f"Normality test for Group {group_name}:")
  235. print(f" D'Agostino Statistic: {statistic:.4f}")
  236. print(f" P-value: {p_value:.4f}")
  237. alpha = 0.05 #significance level
  238. if p_value > alpha:
  239. print(f"Result: Data for Group {group_name} appears to be normally distributed (fail to reject H0)")
  240. else:
  241. print(f"Result: Data for group {group_name} does not appear to be normally distributed (reject H0)")
  242. print("-" * 30)
  243. #Result: Our hK6 data is now normally distributed
  244. # %%
  245. # I will also check normality for age, because we will have to adjust for it
  246. for group_name in groups:
  247. group_data = df[df['Group'] == group_name]['Age']
  248. statistic, p_value = stats.normaltest(group_data)
  249. print(f'Normality test for group {group_name}:')
  250. print(f"D'Agostino Statistic: {statistic:.4f}")
  251. print(f"P-value: {p_value}")
  252. alpha=0.05
  253. if p_value > alpha:
  254. print(f"Result: Data for Group {group_name} appears to be normally distributed (fail to reject H0)")
  255. else:
  256. print(f"Result: Data for group {group_name} does not appear to be normally distributed (reject H0)")
  257. print("-" * 30)
  258. # %% [markdown]
  259. # Note: ANOVA is more powerful than Kruskal-Wallis, since Kruskal-Wallis compares the ranks, and some information may be lost. When we have ordinal data (such as MMSE Score)
  260. # we have to use Kruskal-Wallis.
  261. # %% [markdown]
  262. # Prior to ANOVA, we have to check for variances, since they have to be equal (we have to assume that the variability within each group being compared is roughly the same).
  263. # %% [markdown]
  264. # To do this, we can do the Bartlett's test, or Levene's test.
  265. # We have used Levene's test which checks for homogeneity of variances but is less sensitive to departures from normality than Bartlett's test, making it more robust.
  266. # %%
  267. #Create 4 groups for stat analysis based on Group column
  268. group_nonprog = df[df["Group"] == "MCI non Progressor"][['Group','log2_Value',"Value", "MMSE_PL", "APOE", "Sex","Age","Abeta_42_LCR", "P_tau_LCR", "T_tau_LCR"]]
  269. group_prog = df[df["Group"] == "MCI Progressor"][['Group','log2_Value',"Value", "MMSE_PL", "APOE", "Sex","Age","Abeta_42_LCR", "P_tau_LCR", "T_tau_LCR"]]
  270. group_SCD= df[df["Group"] == "SCD"][['Group', 'log2_Value',"Value", "MMSE_PL", "APOE", "Sex","Age","Abeta_42_LCR", "P_tau_LCR", "T_tau_LCR"]]
  271. group_AD = df[df["Group"] == "AD"][['Group', 'log2_Value',"Value", "MMSE_PL", "APOE", "Sex","Age","Abeta_42_LCR", "P_tau_LCR", "T_tau_LCR"]]
  272. # %%
  273. #Check for NAs for the Levene's test
  274. print(f"nonprog: {group_nonprog['log2_Value'].isna().sum()}")
  275. print(f"prog: {group_prog['log2_Value'].isna().sum()}")
  276. print(f"SCD: {group_SCD['log2_Value'].isna().sum()}")
  277. print(f"AD: {group_AD['log2_Value'].isna().sum()}")
  278. # %%
  279. # Check variances
  280. print("Group variances:")
  281. print(f"nonprog: {group_nonprog['log2_Value'].var()}")
  282. print(f"prog: {group_prog['log2_Value'].var()}")
  283. print(f"SCD: {group_SCD['log2_Value'].var()}")
  284. print(f"AD: {group_AD['log2_Value'].var()}")
  285. # Check if any group has zero variance (all values identical)
  286. print("All values same in groups?")
  287. print(f"nonprog: {group_nonprog['log2_Value'].nunique() == 1}")
  288. print(f"prog: {group_prog['log2_Value'].nunique() == 1}")
  289. print(f"SCD: {group_SCD['log2_Value'].nunique() == 1}")
  290. print(f"AD: {group_AD['log2_Value'].nunique() == 1}")
  291. # %%
  292. #perform Levene's test
  293. l_statistic, p_value_l = stats.levene(group_nonprog['log2_Value'], group_prog['log2_Value'], group_SCD['log2_Value'], group_AD['log2_Value'])
  294. print(f"Levene's Test Statistic: {l_statistic}")
  295. print(f"Levene's Test p-value: {p_value_l}")
  296. # Interpretation: If p_value < significance level (e.g., 0.05), reject the null hypothesis
  297. # and conclude that variances are not equal.
  298. print("-"*30) #divider
  299. alpha = 0.05 #significance level
  300. if p_value_l > alpha:
  301. print("The variances are equal (fail to reject the H0)")
  302. else:
  303. print("The variances are not equal (reject the H0)")
  304. #Result: The variances are equal
  305. # %% [markdown]
  306. # ## Performing ANCOVA for the hK6 data
  307. # %% [markdown]
  308. # We have to perform the statistics, in this case a rank-based ANCOVA test (pnon-arametric), using the log2 transformed data
  309. # %%
  310. # 2. Check distribution before/after transformation
  311. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
  312. sns.histplot(df['Value'], ax=ax1, kde=True)
  313. ax1.set_title('Raw hK6 Distribution')
  314. sns.histplot(df['log2_Value'], ax=ax2, kde=True)
  315. ax2.set_title('Log2-Transformed hK6 Distribution')
  316. plt.show()
  317. # 3. Perform ANCOVA on transformed data
  318. model = smf.ols('log2_Value ~ Age + C(Group)', data=df).fit()
  319. ancova_table = sm.stats.anova_lm(model, typ=2)
  320. print("ANCOVA Results:")
  321. print(ancova_table)
  322. # 4. Check model assumptions
  323. residuals = model.resid
  324. fig, axes = plt.subplots(1, 2, figsize=(12, 5))
  325. stats.probplot(residuals, dist="norm", plot=axes[0])
  326. axes[0].set_title('Q-Q Plot of Residuals')
  327. axes[1].scatter(model.fittedvalues, residuals)
  328. axes[1].axhline(y=0, color='r', linestyle='--')
  329. axes[1].set_xlabel('Fitted values')
  330. axes[1].set_ylabel('Residuals')
  331. axes[1].set_title('Residuals vs Fitted')
  332. plt.show()
  333. df['hK6_rank'] = df['log2_Value'].rank()
  334. # ANCOVA model: Ranked_KLK6 ~ Age + Diagnosis
  335. model_rank = smf.ols('hK6_rank ~ Age + C(Group)', data=df).fit()
  336. print("ANCOVA Results:")
  337. print(sm.stats.anova_lm(model_rank, typ=2))
  338. # %% [markdown]
  339. # Based on the above results, we should stick to the parametric ANCOVA.
  340. # %% [markdown]
  341. # In order to see which groups are significantly different from each other, a post-hoc test is used. Tukey's is a common choice, which we have used here.
  342. # %%
  343. tukey_result = pairwise_tukeyhsd(endog=df['log2_Value'], groups = df['Group'], alpha = 0.05)
  344. print(tukey_result)
  345. p_values_tukey = tukey_result.pvalues
  346. # %%
  347. p_values_tukey #check to see what the different p_values are to use later
  348. # %% [markdown]
  349. # ### Function for hK6 plot asterisks
  350. # %%
  351. # Function to add significance bars with custom symbols, this will be used in the creation of the graphs for hK6 later on.
  352. '''
  353. Parameters
  354. ax= name of plot that has already been defined
  355. group1, group2= the two groups that we are comparing (we don't need to mention the dataframe, since it has been typed in ax)
  356. p_value= the p_value from the Tukey test, include location in [], e.g. p_values_tukey[0] for AD vs MCI_conver_toDem
  357. x1, x2= location of boxplot for each group compared, e.g. MCI_NonProgressor is in x1= 0, while MCI_conver_toDem is in x1= 1.
  358. y= height of line based on the graph.
  359. '''
  360. def add_sig_bars(ax, group1, group2, p_value, x1, x2, y):
  361. """Add significance bars with custom symbols and sizes based on p-value."""
  362. y_offset = 0.2 # Offset for the bar
  363. ax.plot([x1, x1, x2, x2], [y, y + y_offset, y + y_offset, y], color='black')
  364. # Annotate with different symbols based on p-value
  365. if p_value < 0.001:
  366. ax.text((x1 + x2) / 2, y + y_offset + 0.1, '***', fontsize=12, ha='center', color='red')
  367. elif p_value < 0.01:
  368. ax.text((x1 + x2) / 2, y + y_offset + 0.1, '**', fontsize=12, ha='center', color='orange')
  369. elif p_value < 0.05:
  370. ax.text((x1 + x2) / 2, y + y_offset + 0.1, '*', fontsize=12, ha='center', color='blue')
  371. # %% [markdown]
  372. # ## hK6 plots
  373. # NOTE: MCI_conver_toDem corresponds to MCI Progressor in the later analysis
  374. # %% [markdown]
  375. # ### Create plots with raw data
  376. # %%
  377. #Scatterplot with boxplot
  378. plt.figure(figsize=(10,6))
  379. # Set the style to include a grid
  380. sns.set_style("whitegrid")
  381. ax1 = sns.boxplot(y = "Value", x= 'Group',palette= "RdBu", data= df,order= group_order)
  382. sns.stripplot(y = "Value", x="Group", ax= ax1, data= df, color = "black")
  383. plt.title("Comparison of hK6 in the AD continuum", fontsize = 17)
  384. # %%
  385. #Boxplot without the scatterplot
  386. plt.figure(figsize=(10,6))
  387. ax = sns.boxplot(y = df["Value"], x= df["Group"], palette= "RdBu", order= group_order)
  388. # Set the style to include a grid
  389. sns.set_style("whitegrid")
  390. plt.title("Comparison of hK6 in the AD continuum", fontsize = 17)
  391. # %% [markdown]
  392. # ### Create plots with log2 transformed data
  393. # %%
  394. #Scatterplot with boxplot
  395. plt.figure(figsize=(10,6))
  396. # Set the style to include a grid
  397. sns.set_style("whitegrid")
  398. ax1 = sns.boxplot(y = df["log2_Value"], x= df["Group"], hue= "Group",palette= "RdBu", data= df,
  399. hue_order= group_order, order = group_order)
  400. sns.stripplot(y = df["log2_Value"], x= df["Group"], ax= ax1, data= df, color = ".3")
  401. #manually check the sig p values
  402. add_sig_bars(ax1, "MCI non Progessor", "MCI Progressor",p_values_tukey[3],0 ,1, 9.5)
  403. add_sig_bars(ax1, "AD", "MCI non Progessor",p_values_tukey[1],0 ,3, 10)
  404. add_sig_bars(ax1, "AD", "SCD",p_values_tukey[2],2 ,3, 10.4)
  405. add_sig_bars(ax1, "SCD", "MCI Progressor",p_values_tukey[4],1 ,2, 9.4)
  406. plt.title("Comparison of hK6 in the AD continuum", fontsize = 17)
  407. plt.ylim(top=11.0)
  408. # %%
  409. #Boxplot without scatterplot
  410. plt.figure(figsize=(10,6))
  411. ax = sns.boxplot(y = df["log2_Value"], x= df["Group"], palette= "RdBu",
  412. hue_order= group_order, order = group_order)
  413. # Set the style to include a grid
  414. sns.set_style("whitegrid")
  415. #manually check the sig p values
  416. add_sig_bars(ax, "MCI non Progessor", "MCI Progressor",p_values_tukey[3],0 ,1, 9.5)
  417. add_sig_bars(ax, "AD", "MCI non Progessor",p_values_tukey[1],0 ,3, 10)
  418. add_sig_bars(ax, "AD", "SCD",p_values_tukey[2],2 ,3, 10.4)
  419. add_sig_bars(ax, "SCD", "MCI Progressor",p_values_tukey[4],1 ,2, 9.4)
  420. plt.title("Comparison of hK6 in the AD continuum", fontsize = 17)
  421. plt.ylim(top=11.0)
  422. # %% [markdown]
  423. # ## Function for correlation analysis
  424. # %%
  425. #CREATE A FUNCTION FOR SPEARMAN CORRELATION FIGURES AND RANK-TRANSFORMED FIGURES
  426. '''
  427. Parameters
  428. x,y= variable names or arrays
  429. data= DataFrame that contains the variables (optional)
  430. title= Plot title (Optional)
  431. xlabel, ylabel= Axis labels (optional)
  432. group= Grouping variable for color coding (optional)
  433. group_order= Order of groups for coloring (optional)
  434. '''
  435. def plot_spearman_correlation(x, y, data=None, title=None, xlabel=None, ylabel=None, group=None, group_order= None):
  436. if data is not None:
  437. x_data = data[x] #the x value from the dataframe
  438. y_data = data[y] #the y data from the dataframe
  439. x_col_name= x #the x column name
  440. y_col_name= y #the y column name
  441. if group is not None:
  442. group_data = data[group] #if group data is provided, then use it
  443. else:
  444. x_data = x
  445. y_data = y
  446. x_col_name= "X Variable"
  447. y_col_name= "Y Variable"
  448. group_data= group
  449. # use provided labels or use the default ones as above
  450. xlabel = xlabel if xlabel is not None else x_col_name
  451. ylabel = ylabel if ylabel is not None else y_col_name
  452. # Calculate spearman correlation
  453. corr, p_value = stats.spearmanr(x_data, y_data)
  454. # Create figure
  455. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6), dpi=600)
  456. #color coding based on the groups
  457. if group is not None:
  458. #Get unique groups and then colors
  459. if group_order is not None:
  460. unique_groups= group_order
  461. else:
  462. unique_groups = group_data.unique()
  463. n_groups= len(unique_groups) #number of groups
  464. colors= sns.color_palette("RdBu", n_groups) #choose the colour for each of the groups
  465. #Plot each group with a different color
  466. for i, grp in enumerate(unique_groups): #grp=group
  467. mask = group_data == grp
  468. ax1.scatter(x_data[mask], y_data[mask], alpha= 0.6, s=20,
  469. color= colors[i], label= str(grp))
  470. ax2.scatter(x_data.rank()[mask], y_data.rank()[mask], alpha=0.6,
  471. s=20, color= colors[i], label= str(grp))
  472. #add legend code here if needed
  473. else:
  474. #code without groups
  475. ax1.scatter(x_data, y_data, alpha=0.6, s=20)
  476. ax2.scatter(x_data.rank(), y_data.rank(), alpha=0.6, s=20)
  477. # Add regression lines to both plots
  478. # For original data
  479. slope_orig, intercept_orig, r_value_orig, p_value_orig, std_err_orig = stats.linregress(x_data, y_data)
  480. x_range_orig = np.linspace(x_data.min(), x_data.max(), 100)
  481. y_pred_orig = slope_orig * x_range_orig + intercept_orig
  482. ax1.plot(x_range_orig, y_pred_orig, '-',color='black', linewidth=2, alpha=0.8,
  483. label=f'Slope: {slope_orig:.2f}')
  484. # For rank-transformed data
  485. x_rank = x_data.rank()
  486. y_rank = y_data.rank()
  487. slope_rank, intercept_rank, r_value_rank, p_value_rank, std_err_rank = stats.linregress(x_rank, y_rank)
  488. x_range_rank = np.linspace(x_rank.min(), x_rank.max(), 100)
  489. y_pred_rank = slope_rank * x_range_rank + intercept_rank
  490. ax2.plot(x_range_rank, y_pred_rank, '-',color='black', linewidth=2, alpha=0.8,
  491. label='Slope')
  492. # Original data scatter plot
  493. ax1.set_xlabel(xlabel)
  494. ax1.set_ylabel(ylabel)
  495. ax1.set_title('Original Data')
  496. ax1.grid(True, alpha=0.3)
  497. # Rank-transformed data
  498. x_rank = x_data.rank()
  499. y_rank = y_data.rank()
  500. ax2.set_xlabel(f'{xlabel} Rank')
  501. ax2.set_ylabel(f'{ylabel} Rank')
  502. ax2.set_title('Rank-Transformed Data')
  503. ax2.grid(True, alpha=0.3)
  504. # Add perfect correlation line to rank plot (1:1 line) depending on whether it is positive or negative
  505. if slope_rank >0:
  506. min_rank = min(x_rank.min(), y_rank.min())
  507. max_rank = max(x_rank.max(), y_rank.max())
  508. ax2.plot([min_rank, max_rank], [min_rank, max_rank], '--',color= 'purple', alpha=0.7,
  509. label= 'Perfect pos slope: 1')
  510. ax2.legend(bbox_to_anchor=(-0.1, 1)) #add legend
  511. else:
  512. min_rank = min(x_rank.min(), y_rank.min())
  513. max_rank = max(x_rank.max(), y_rank.max())
  514. ax2.plot([min_rank, max_rank], [max_rank, min_rank], '--',color= 'purple', alpha=0.7,
  515. label= 'Perfect neg slope: -1')
  516. ax2.legend(bbox_to_anchor=(-0.1, 1)) #add legend
  517. # Add correlation info to both plots
  518. for ax in [ax1, ax2]:
  519. ax.text(0.05, 0.95, f'ρ = {corr:.2f}\nraw p = {p_value:.3f}',
  520. transform=ax.transAxes,
  521. bbox=dict(boxstyle="round,pad=0.3", fc="white", alpha=0.9))
  522. plt.suptitle(f'{title} (Spearman ρ = {corr:.2f}, p= {p_value:.3f})', fontsize=14)
  523. plt.tight_layout()
  524. plt.savefig(f'{title}.jpg',
  525. dpi=600,
  526. bbox_inches='tight', # removes extra white space
  527. pad_inches=0.1, # small padding around figure
  528. facecolor='white', # background color
  529. edgecolor='none', # no border
  530. transparent=False) # not transparent
  531. plt.show()
  532. # %% [markdown]
  533. # ## Correlation of hK6 with Age, Sex, MMSE score, APOE status, Abeta, P tau and T tau (combined data)
  534. # %% [markdown]
  535. # - hK6 protein concentration is a continuous variable, Age is a continous variable, Sex is categorical (Binary), MMSE score is continous, and APOE status is categorical (Ordinal/Nominal).
  536. # - When comparing two continous variables, we performed a spearman correlation.
  537. # - When comparing continous to categorical (Binary) we performed the Point-Biserial correlation.
  538. # - When comparing continous to categorical (Ordinal/Nominal) we performed an Kruskal-Wallis.
  539. # %%
  540. df #check how it looks
  541. #Result: 393 rows x 15 columns
  542. # %%
  543. #Check descriptive stats
  544. print(df.describe(include='all'))
  545. # %%
  546. #Check specificically for categorical variables
  547. print("\nValue counts for categorical variables:")
  548. print("Sex:\n", df['Sex'].value_counts())
  549. print("\nAPOE_Status:\n", df['APOE'].value_counts())
  550. # %%
  551. ### Check correlations of all variables with age
  552. corr_matrix = df[['Age', 'Value', 'MMSE_PL', 'Abeta_42_LCR', 'P_tau_LCR', 'T_tau_LCR']].corr(method = 'spearman')
  553. print("correlation matrix with Age: ")
  554. print(corr_matrix)
  555. # %% [markdown]
  556. # There is a positive correlation with p-tau and t-tau and hK6 concentration, and a negative correlation with MMSE score and Abeta 42.
  557. # %% [markdown]
  558. # Thus, we should adjust for age through a partial correlation.
  559. # %%
  560. # Create a binary variable for sex (0/1)
  561. df['Sex_numeric'] = df['Sex'].map({'Male': 0, 'Female': 1}) # or vice versa
  562. # %% [markdown]
  563. # ### Correlation with Age
  564. # %%
  565. #Correlation with age
  566. print("=" *50)
  567. print("CORRELATION WITH AGE")
  568. print("="*50)
  569. # Check normality using QQ plot
  570. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
  571. stats.probplot(df['Value'].dropna(), dist="norm", plot=ax1)
  572. ax1.set_title('QQ Plot - hK6 Levels') #all diagnoses combined
  573. stats.probplot(df['Age'].dropna(), dist="norm", plot=ax2)
  574. ax2.set_title('QQ Plot - Age')
  575. plt.tight_layout()
  576. plt.show()
  577. # %%
  578. ##Correlation with Age
  579. print('='*50)
  580. print('CORRELATION WITH AGE')
  581. print('='*50)
  582. #Fix dropna issues (the x and y variables are not the same length)
  583. age_temp_df = df[["log2_Value","Value", 'Age', 'Sex_numeric', 'Group']].dropna()
  584. #Spearman correlation because Abeta 42 is a continous variable
  585. spearman_corr_age, spearman_p_age = stats.spearmanr(age_temp_df['Value'], age_temp_df['Age'])
  586. print(f"Spearman Correlation: ρ = {spearman_corr_age:.3f}, p = {spearman_p_age:.4f}")
  587. print(f'The sample size is: {len(age_temp_df)}')
  588. #Adjust for sex
  589. print('='*50)
  590. print("CORRELATION WITH AGE ( ADJUSTED FOR SEX)")
  591. print("="*50)
  592. partial_spearman_age = pg.partial_corr(data= age_temp_df, x= 'Age', y='Value', covar= 'Sex_numeric', method= 'spearman')
  593. print('Partial correlation (controlling for sex):')
  594. print(f"ρ = {partial_spearman_age['r'].iloc[0]:.3f}, p = {partial_spearman_age['p-val'].iloc[0]:.4f}")
  595. print('*'*50)
  596. # Compare unadjusted vs adjusted results
  597. print(f"\nComparison:")
  598. print(f"Unadjusted p-value (Mann-Whitney): {spearman_p_age:.4f}")
  599. print(f"Age-adjusted p-value (ANCOVA): {partial_spearman_age['p-val'].iloc[0]:.4f}")
  600. if abs(spearman_p_age - partial_spearman_age['p-val'].iloc[0]) > 0.05:
  601. print("Note: Adjustment for age meaningfully changed the results")
  602. else:
  603. print("Note: Results were robust to age adjustment")
  604. #Create a scatterplot
  605. plt.figure(figsize=(8,6), dpi= 600)
  606. sns.scatterplot(data= age_temp_df, x= "Age", y= "Value",
  607. hue= 'Group', palette= 'RdBu', hue_order= group_order)
  608. plt.title(f"hK6 vs. Age (Spearman ρ = {spearman_corr_age:.3f}, p = {spearman_p_age:.4f})")
  609. plt.xlabel("Age (years)")
  610. plt.ylabel("hK6 concentration (ng/mL)")
  611. plt.savefig('comb_hK6vsAge.jpg',
  612. dpi=600,
  613. bbox_inches='tight', # removes extra white space
  614. pad_inches=0.1, # small padding around figure
  615. facecolor='white', # background color
  616. edgecolor='none', # no border
  617. transparent=False) # not transparent
  618. plt.show()
  619. # %%
  620. #Spearman correlation when combining all data
  621. plot_spearman_correlation("Age", "Value", data= df, title= "hK6 vs. Age (Spearman Correlation)",
  622. xlabel= "Age (years)",ylabel= "hK6 concentration (ng/mL)", group= 'Group', group_order= group_order)
  623. # %% [markdown]
  624. # ### Correlation with Sex
  625. # %%
  626. #Correlation with Sex
  627. print('='*50)
  628. print('CORRELATION WITH SEX')
  629. print('='*50)
  630. # Check normality within each group using Shapiro-Wilk
  631. for sex_group in df['Sex'].unique():
  632. group_data = df[df['Sex'] == sex_group]['Value'].dropna()
  633. stat, p = stats.shapiro(group_data)
  634. print(f"Shapiro-Wilk for {sex_group}: p = {p:.4f}")
  635. #Use Mann-Whitney test, non-normal data
  636. male_data = df[df['Sex']== 'Male']['Value'].dropna()
  637. female_data = df[df['Sex']== 'Female']['Value'].dropna()
  638. stat_sex, p_value_sex = stats.mannwhitneyu(male_data, female_data, alternative= 'two-sided')
  639. print(f"\nMann-Whitney U test: U = {stat_sex}, p = {p_value_sex:.4f}")
  640. print('*'*50)
  641. print('='*50)
  642. print('CORRELATION WITH SEX (ADJUSTED FOR AGE)')
  643. print('='*50)
  644. print("\n2. Partial Correlation Approach:")
  645. partial_corr_sex = pg.partial_corr(data=df, x='Sex_numeric', y='Value', covar='Age', method='spearman')
  646. print(f"Partial correlation (Sex vs Value, controlling for Age):")
  647. print(f"ρ = {partial_corr_sex['r'].iloc[0]:.3f}, p = {partial_corr_sex['p-val'].iloc[0]:.4f}")
  648. # Compare unadjusted vs adjusted results
  649. print(f"\nComparison:")
  650. print(f"Unadjusted p-value (Mann-Whitney): {p_value_sex:.4f}")
  651. print(f"Age-adjusted p-value (ANCOVA): {partial_corr_sex['p-val'].iloc[0]:.4f}")
  652. if abs(p_value_sex - partial_corr_sex['p-val'].iloc[0]) > 0.05:
  653. print("Note: Adjustment for age and meaningfully changed the results")
  654. else:
  655. print("Note: Results were robust to age adjustment")
  656. # %%
  657. # Create a box plot
  658. plt.figure(figsize=(8, 6), dpi=600)
  659. sns.boxplot(data=df, x='Sex', y='Value',boxprops= dict(alpha=0.6), hue= "Sex", palette= "RdBu")
  660. sns.stripplot(data=df, x='Sex', y='Value', color='black', alpha=0.5, size=4)
  661. plt.xlabel("Sex")
  662. plt.ylabel("hK6 concentration (ng/mL)")
  663. plt.title(f'hK6 Levels by Sex (Mann-Whitney p = {p_value_sex:.3f})')
  664. plt.savefig('comb_hK6vsSex.jpg',
  665. dpi=600,
  666. bbox_inches='tight', # removes extra white space
  667. pad_inches=0.1, # small padding around figure
  668. facecolor='white', # background color
  669. edgecolor='none', # no border
  670. transparent=False) # not transparent
  671. plt.show()
  672. # %% [markdown]
  673. # ### Correlation with MMSE score
  674. # %%
  675. ##Correlation with MMSE Score
  676. print('='*50)
  677. print('CORRELATION WITH MMSE Score')
  678. print('='*50)
  679. #Fix dropna issues (the x and y variables are not the same length)
  680. mmse_temp_df = df[["log2_Value","Value", 'MMSE_PL', 'Sex_numeric', 'Age', 'Group']].dropna()
  681. #Spearman correlation because Abeta 42 is a continous variable
  682. spearman_corr_mmse, spearman_p_mmse = stats.spearmanr(mmse_temp_df['Value'], mmse_temp_df['MMSE_PL'])
  683. print(f"Spearman Correlation: ρ = {spearman_corr_mmse:.3f}, p = {spearman_p_mmse:.4f}")
  684. print(f'The sample size is: {len(mmse_temp_df)}')
  685. #Adjust for sex
  686. print('='*50)
  687. print("CORRELATION WITH MMSE Score ( ADJUSTED FOR SEX AND AGE)")
  688. print("="*50)
  689. partial_spearman_mmse = pg.partial_corr(data= mmse_temp_df, x= 'MMSE_PL', y='Value',
  690. covar= ['Sex_numeric', 'Age'], method= 'spearman')
  691. print('Partial correlation (controlling for sex and age):')
  692. print(f"ρ = {partial_spearman_mmse['r'].iloc[0]:.3f}, p = {partial_spearman_mmse['p-val'].iloc[0]:.4f}")
  693. print('*'*50)
  694. # Compare unadjusted vs adjusted results
  695. print(f"\nComparison:")
  696. print(f"Unadjusted p-value (Mann-Whitney): {spearman_p_mmse:.4f}")
  697. print(f"Age-adjusted p-value (ANCOVA): {partial_spearman_mmse['p-val'].iloc[0]:.4f}")
  698. if abs(spearman_p_mmse - partial_spearman_mmse['p-val'].iloc[0]) > 0.05:
  699. print("Note: Adjustment for age and sex meaningfully changed the results")
  700. else:
  701. print("Note: Results were robust to age and sex adjustment")
  702. #Create a scatterplot
  703. plt.figure(figsize=(8,6), dpi= 600)
  704. sns.scatterplot(data= mmse_temp_df, x= "MMSE_PL", y= "Value",
  705. hue= 'Group', palette= 'RdBu', hue_order= group_order)
  706. plt.title(f"hK6 vs. MMSE Score (Spearman ρ = {spearman_corr_mmse:.3f}, p = {spearman_p_mmse:.4f})")
  707. plt.xlabel("MMSE Score (1-30 scale)")
  708. plt.ylabel("hK6 concentration (ng/mL)")
  709. plt.savefig('comb_hK6vsMMSE.jpg',
  710. dpi=600,
  711. bbox_inches='tight', # removes extra white space
  712. pad_inches=0.1, # small padding around figure
  713. facecolor='white', # background color
  714. edgecolor='none', # no border
  715. transparent=False) # not transparent
  716. plt.show()
  717. # %%
  718. ## Repeat analysis by checking the covariates separately
  719. # Correlation with MMSE Score
  720. print('='*50)
  721. print('CORRELATION WITH MMSE Score')
  722. print('='*50)
  723. # Fix dropna issues
  724. mmse_temp_df = df[["log2_Value","Value", 'MMSE_PL', 'Sex_numeric', 'Age', 'Group']].dropna()
  725. # Spearman correlation
  726. spearman_corr_mmse, spearman_p_mmse = stats.spearmanr(mmse_temp_df['Value'], mmse_temp_df['MMSE_PL'])
  727. print(f"Spearman Correlation: ρ = {spearman_corr_mmse:.3f}, p = {spearman_p_mmse:.4f}")
  728. print(f'The sample size is: {len(mmse_temp_df)}')
  729. # ===========================================================================
  730. # STEP 1: TEST EACH COVARIATE SEPARATELY
  731. # ===========================================================================
  732. print('='*50)
  733. print("IDENTIFYING WHICH COVARIATE CAUSES SIGNIFICANCE LOSS")
  734. print("="*50)
  735. # 1. Adjust for SEX only
  736. partial_sex_only = pg.partial_corr(data=mmse_temp_df, x='MMSE_PL', y='Value',
  737. covar=['Sex_numeric'], method='spearman')
  738. # 2. Adjust for AGE only
  739. partial_age_only = pg.partial_corr(data=mmse_temp_df, x='MMSE_PL', y='Value',
  740. covar=['Age'], method='spearman')
  741. # 3. Adjust for BOTH (your original)
  742. partial_both = pg.partial_corr(data=mmse_temp_df, x='MMSE_PL', y='Value',
  743. covar=['Sex_numeric', 'Age'], method='spearman')
  744. print(f"\nUnadjusted: ρ = {spearman_corr_mmse:.3f}, p = {spearman_p_mmse:.4f}")
  745. print(f"Adjusted for SEX only: ρ = {partial_sex_only['r'].iloc[0]:.3f}, p = {partial_sex_only['p-val'].iloc[0]:.4f}")
  746. print(f"Adjusted for AGE only: ρ = {partial_age_only['r'].iloc[0]:.3f}, p = {partial_age_only['p-val'].iloc[0]:.4f}")
  747. print(f"Adjusted for BOTH: ρ = {partial_both['r'].iloc[0]:.3f}, p = {partial_both['p-val'].iloc[0]:.4f}")
  748. # ===========================================================================
  749. # STEP 2: DETERMINE WHICH COVARIATE IS RESPONSIBLE
  750. # ===========================================================================
  751. print('\n' + '*'*50)
  752. print("ANALYSIS OF SIGNIFICANCE CHANGE")
  753. print('*'*50)
  754. # Check if significance was lost
  755. if spearman_p_mmse < 0.05 and partial_both['p-val'].iloc[0] >= 0.05:
  756. print("✓ Significance was LOST after adjustment")
  757. # Check which adjustment caused the change
  758. sex_change = abs(spearman_p_mmse - partial_sex_only['p-val'].iloc[0])
  759. age_change = abs(spearman_p_mmse - partial_age_only['p-val'].iloc[0])
  760. print(f"Change due to SEX adjustment: Δp = {sex_change:.4f}")
  761. print(f"Change due to AGE adjustment: Δp = {age_change:.4f}")
  762. if sex_change > age_change and partial_sex_only['p-val'].iloc[0] >= 0.05:
  763. print("→ PRIMARY CULPRIT: SEX (adjusting for sex alone removes significance)")
  764. elif age_change > sex_change and partial_age_only['p-val'].iloc[0] >= 0.05:
  765. print("→ PRIMARY CULPRIT: AGE (adjusting for age alone removes significance)")
  766. elif partial_sex_only['p-val'].iloc[0] < 0.05 and partial_age_only['p-val'].iloc[0] >= 0.05:
  767. print("→ PRIMARY CULPRIT: AGE (sex adjustment preserves significance)")
  768. elif partial_age_only['p-val'].iloc[0] < 0.05 and partial_sex_only['p-val'].iloc[0] >= 0.05:
  769. print("→ PRIMARY CULPRIT: SEX (age adjustment preserves significance)")
  770. else:
  771. print("→ COMBINED EFFECT: Both age and sex contribute to significance loss")
  772. elif spearman_p_mmse >= 0.05 and partial_both['p-val'].iloc[0] < 0.05:
  773. print("✓ Significance was GAINED after adjustment")
  774. # Similar logic for gained significance...
  775. else:
  776. print("✓ No major change in significance after adjustment")
  777. # ===========================================================================
  778. # STEP 3: CHECK FOR CONFOUNDING RELATIONSHIPS
  779. # ===========================================================================
  780. print('\n' + '='*50)
  781. print("CONFOUNDING ANALYSIS")
  782. print("="*50)
  783. # Check correlations between covariates and main variables
  784. print("Correlations to identify potential confounding:")
  785. print(f"MMSE vs Age: ρ = {stats.spearmanr(mmse_temp_df['MMSE_PL'], mmse_temp_df['Age'])[0]:.3f}")
  786. print(f"MMSE vs Sex: ρ = {stats.spearmanr(mmse_temp_df['MMSE_PL'], mmse_temp_df['Sex_numeric'])[0]:.3f}")
  787. print(f"Value vs Age: ρ = {stats.spearmanr(mmse_temp_df['Value'], mmse_temp_df['Age'])[0]:.3f}")
  788. print(f"Value vs Sex: ρ = {stats.spearmanr(mmse_temp_df['Value'], mmse_temp_df['Sex_numeric'])[0]:.3f}")
  789. # ===========================================================================
  790. # STEP 4: VISUALIZE THE RELATIONSHIPS
  791. # ===========================================================================
  792. # Create faceted plots to see the relationships
  793. fig, axes = plt.subplots(2, 2, figsize=(12, 10), dpi=600)
  794. # Plot 1: Original relationship
  795. sns.scatterplot(data=mmse_temp_df, x="MMSE_PL", y="Value", hue='Group',
  796. palette='RdBu', ax=axes[0,0])
  797. axes[0,0].set_title(f"Original: ρ = {spearman_corr_mmse:.3f}, p = {spearman_p_mmse:.4f}")
  798. # Plot 2: Stratified by sex
  799. sns.scatterplot(data=mmse_temp_df, x="MMSE_PL", y="Value", hue='Sex_numeric',
  800. palette='viridis', ax=axes[0,1])
  801. axes[0,1].set_title("Colored by Sex")
  802. # Plot 3: Colored by age (categorized)
  803. mmse_temp_df['Age_group'] = pd.cut(mmse_temp_df['Age'], bins=3)
  804. sns.scatterplot(data=mmse_temp_df, x="MMSE_PL", y="Value", hue='Age_group',
  805. palette='plasma', ax=axes[1,0])
  806. axes[1,0].set_title("Colored by Age Group")
  807. # Plot 4: Residuals after adjusting for age and sex (optional)
  808. from sklearn.linear_model import LinearRegression
  809. X_adjust = mmse_temp_df[['Age', 'Sex_numeric']]
  810. model = LinearRegression().fit(X_adjust, mmse_temp_df['Value'])
  811. residuals = mmse_temp_df['Value'] - model.predict(X_adjust)
  812. axes[1,1].scatter(mmse_temp_df['MMSE_PL'], residuals, c=mmse_temp_df['Age'], cmap='plasma')
  813. axes[1,1].set_xlabel("MMSE Score")
  814. axes[1,1].set_ylabel("Value (adjusted for Age/Sex)")
  815. axes[1,1].set_title("Relationship with MMSE after adjusting for Age/Sex")
  816. plt.tight_layout()
  817. plt.savefig('MMSE_confounding_analysis.jpg', dpi=600, bbox_inches='tight')
  818. plt.show()
  819. print('\n' + '='*50)
  820. print("FINAL CONCLUSION")
  821. print("="*50)
  822. print("Check the individual adjustments above to see which covariate")
  823. print("caused the greatest change in p-value and effect size.")
  824. # %%
  825. #Correlation with MMSE score
  826. plot_spearman_correlation("MMSE_PL", "Value", data= mmse_temp_df, xlabel= "MMSE Score", ylabel =
  827. "hK6 concentration (ng/mL)",
  828. title= "hK6 vs. MMSE Score (Spearman correlation)",
  829. group= 'Group',
  830. group_order=group_order)
  831. # %% [markdown]
  832. # ### Correlation with APOE status
  833. # %%
  834. #Correlation with APOE status, performing ANOVA on the log2 hK6 values
  835. print('='*50)
  836. print("CORRELATION WITH APOE STATUS")
  837. print('='*50)
  838. #remove NAs like in the MMSE analysis
  839. apoe_temp_df = df[["log2_Value","Value", 'APOE','Age', 'Sex_numeric', 'Group']].dropna()
  840. # Prepare data for ANOVA - create a list of arrays for each group
  841. apoe_groups = []
  842. for status in apoe_temp_df['APOE'].unique():
  843. group_data = apoe_temp_df[apoe_temp_df['APOE'] == status]['log2_Value']
  844. apoe_groups.append(group_data)
  845. print(f"APOE {status}: n = {len(group_data)}, mean = {np.mean(group_data):.2f} ± {np.std(group_data):.2f}")
  846. # Perform one-way ANOVA
  847. f_stat_apoe, p_value_apoe = f_oneway(*apoe_groups)
  848. print(f"\nOne-way ANOVA: F = {f_stat_apoe:.3f}, p = {p_value_apoe:.4f}")
  849. # If ANOVA is significant (p < 0.05), perform Tukey's HSD post-hoc test
  850. if p_value_apoe < 0.05:
  851. print("\nPerforming Tukey's HSD post-hoc test:")
  852. # Prepare data for Tukey test
  853. tukey_data = apoe_temp_df[['log2_Value', 'APOE']].dropna()
  854. tukey_results = pairwise_tukeyhsd(tukey_data['log2_Value'], tukey_data['APOE'])
  855. print(tukey_results)
  856. # Plot the results
  857. tukey_results.plot_simultaneous()
  858. plt.title("Tukey HSD Post-hoc Test - Confidence Intervals")
  859. plt.show()
  860. else:
  861. print("ANOVA not significant - no post-hoc tests needed.")
  862. # ADJUSTED ANALYSIS - ANCOVA approach
  863. print('='*50)
  864. print('APOE ANALYSIS (ADJUSTED FOR AGE AND SEX)')
  865. print('='*50)
  866. # First, describe the groups
  867. apoe_groups = []
  868. for status in sorted(apoe_temp_df['APOE'].unique()):
  869. group_data = apoe_temp_df[apoe_temp_df['APOE'] == status]['log2_Value'].dropna()
  870. apoe_groups.append(group_data)
  871. print(f"APOE {status}: n = {len(group_data)}, mean = {np.mean(group_data):.2f} ± {np.std(group_data):.2f}")
  872. # Perform ANCOVA (correct approach for categorical variables)
  873. ancova_result = pg.ancova(data=apoe_temp_df, dv='log2_Value', between='APOE', covar=['Age', 'Sex_numeric'])
  874. print('\nANCOVA Results (adjusted for Age and Sex):')
  875. print(ancova_result)
  876. # Post-hoc tests if ANCOVA is significant
  877. if ancova_result['p-unc'].iloc[0] < 0.05:
  878. print('\nPost-hoc pairwise comparisons:')
  879. posthoc = pg.pairwise_ancova(data=apoe_temp_df, dv='log2_Value', between='APOE', covar=['Age', 'Sex_numeric'])
  880. print(posthoc)
  881. # %%
  882. # Create a box plot, plot raw values instead of log2_values
  883. plt.figure(figsize=(10, 6), dpi=600)
  884. sns.boxplot(data=apoe_temp_df, x='APOE', y='Value', hue = "APOE",
  885. palette= "viridis", boxprops = dict(alpha= 0.6))
  886. sns.stripplot(data=apoe_temp_df, x='APOE', y='Value', color='black', alpha=0.5, size=4)
  887. plt.title(f'hK6 Levels by APOE Status (ANOVA p = {p_value_apoe:.3f})')
  888. plt.xticks(rotation=45)
  889. plt.xlabel("APOE Status")
  890. plt.ylabel("hK6 concentration (ng/mL)")
  891. plt.tight_layout()
  892. plt.savefig('comb_hK6vsAPOE.jpg',
  893. dpi=600,
  894. bbox_inches='tight', # removes extra white space
  895. pad_inches=0.1, # small padding around figure
  896. facecolor='white', # background color
  897. edgecolor='none', # no border
  898. transparent=False) # not transparent
  899. plt.show()
  900. # %% [markdown]
  901. # ### Correlationn with Abeta 42
  902. # %%
  903. #Correlation with Abeta 42
  904. print('='*50)
  905. print('CORRELATION WITH ABETA 42')
  906. print('='*50)
  907. #Fix dropna issues (the x and y variables are not the same length)
  908. abeta_temp_df = df[["log2_Value","Value", 'Abeta_42_LCR','Sex_numeric', 'Age', 'Group']].dropna()
  909. #Spearman correlation because Abeta 42 is a continous variable
  910. spearman_corr_abeta, spearman_p_abeta = stats.spearmanr(abeta_temp_df['Value'], abeta_temp_df['Abeta_42_LCR'])
  911. print(f"Spearman Correlation: ρ = {spearman_corr_abeta:.3f}, p = {spearman_p_abeta:.4f}")
  912. print(f'The sample size is: {len(abeta_temp_df)}')
  913. partial_spearman_abeta = pg.partial_corr(data=abeta_temp_df, x='Value', y='Abeta_42_LCR',
  914. covar='Age', method='spearman')
  915. print("\nSpearman partial correlation:")
  916. print(partial_spearman_abeta)
  917. #Adjust for sex and age
  918. print('='*50)
  919. print('CORRELATION WITH ABETA 42 (ADJUSTED FOR SEX AND AGE)')
  920. print('='*50)
  921. partial_corr_abeta= pg.partial_corr(data= abeta_temp_df, x= 'Abeta_42_LCR', y= 'Value', covar=['Age', 'Sex_numeric'], method= 'spearman')
  922. print(f"ρ: {partial_corr_abeta['p-val'].iloc[0]:.4f}, p = {partial_corr_abeta['p-val'].iloc[0]:.4f}")
  923. print('*'*50)
  924. # Compare unadjusted vs adjusted results
  925. print(f"\nComparison:")
  926. print(f"Unadjusted p-value : {spearman_p_abeta:.4f}")
  927. print(f"Age-adjusted p-value (ANCOVA): {partial_spearman_abeta['p-val'].iloc[0]:.4f}")
  928. if abs(spearman_p_abeta - partial_spearman_abeta['p-val'].iloc[0]) > 0.05:
  929. print("Note: Adjustment for age and sex meaningfully changed the results")
  930. else:
  931. print("Note: Results were robust to age and sex adjustment")
  932. # %%
  933. #Create a scatterplot
  934. plt.figure(figsize=(8,6), dpi= 600)
  935. sns.scatterplot(data= abeta_temp_df, x= "Abeta_42_LCR", y= "Value",
  936. hue= 'Group', palette= 'RdBu', hue_order= group_order)
  937. plt.title(f"hK6 vs. Aβ1-42 (Spearman ρ = {spearman_corr_abeta:.3f}, p = {spearman_p_abeta:.4f})")
  938. plt.xlabel("Aβ1-42 values (pg/mL)")
  939. plt.ylabel("hK6 concentration (ng/mL)")
  940. plt.savefig('comb_hK6vsAbeta42.jpg',
  941. dpi=600,
  942. bbox_inches='tight', # removes extra white space
  943. pad_inches=0.1, # small padding around figure
  944. facecolor='white', # background color
  945. edgecolor='none', # no border
  946. transparent=False) # not transparent
  947. plt.show()
  948. # %% [markdown]
  949. # ### Correlation with pTau
  950. # %%
  951. #Correlation with p-Tau
  952. print('='*50)
  953. print('CORRELATION WITH P-TAU')
  954. print('='*50)
  955. #Fix dropna issues (the x and y variables are not the same length)
  956. ptau_temp_df = df[["log2_Value","Value", 'P_tau_LCR','Age', 'Sex_numeric', 'Group']].dropna()
  957. #Spearman correlation because Abeta 42 is a continous variable
  958. spearman_corr_ptau, spearman_p_ptau = stats.spearmanr(ptau_temp_df['Value'], ptau_temp_df['P_tau_LCR'])
  959. print(f"Spearman Correlation: ρ = {spearman_corr_ptau:.3f}, p = {spearman_p_ptau:.4f}")
  960. print(f'The sample size is: {len(ptau_temp_df)}')
  961. #Adjust for sex and age
  962. print('='*50)
  963. print('CORRELATION WITH ABETA 42 (ADJUSTED FOR SEX AND AGE)')
  964. print('='*50)
  965. partial_corr_ptau= pg.partial_corr(data= ptau_temp_df, x= 'P_tau_LCR', y= 'Value', covar=['Age', 'Sex_numeric'], method= 'spearman')
  966. print(f"ρ: {partial_corr_ptau['p-val'].iloc[0]:.4f}, p = {partial_corr_ptau['p-val'].iloc[0]:.4f}")
  967. print('*'*50)
  968. # Compare unadjusted vs adjusted results
  969. print(f"\nComparison:")
  970. print(f"Unadjusted p-value : {spearman_p_ptau:.4f}")
  971. print(f"Age-adjusted p-value (ANCOVA): {partial_corr_ptau['p-val'].iloc[0]:.4f}")
  972. if abs(spearman_p_ptau - partial_corr_ptau['p-val'].iloc[0]) > 0.05:
  973. print("Note: Adjustment for age and sex meaningfully changed the results")
  974. else:
  975. print("Note: Results were robust to age and sex adjustment")
  976. print('='*50)
  977. #Create a scatterplot
  978. plt.figure(figsize=(8,6), dpi=600)
  979. sns.scatterplot(data= ptau_temp_df, x= "P_tau_LCR", y= "Value", hue= 'Group',
  980. palette= 'RdBu', hue_order= group_order)
  981. plt.title(f"hK6 vs. p-Tau (Spearman ρ = {spearman_corr_ptau:.3f}, p = {spearman_p_ptau:.4f})")
  982. plt.xlabel("P-tau values (pg/mL)")
  983. plt.ylabel("hK6 concentration (ng/mL)")
  984. plt.savefig('comb_hK6vspTau.jpg',
  985. dpi=600,
  986. bbox_inches='tight', # removes extra white space
  987. pad_inches=0.1, # small padding around figure
  988. facecolor='white', # background color
  989. edgecolor='none', # no border
  990. transparent=False) # not transparent
  991. plt.show()
  992. # %% [markdown]
  993. # ### Correlation with tTau
  994. # %%
  995. #Correlation with t-Tau
  996. print('='*50)
  997. print('CORRELATION WITH T-TAU')
  998. print('='*50)
  999. #Fix dropna issues (the x and y variables are not the same length)
  1000. ttau_temp_df = df[["log2_Value","Value", 'T_tau_LCR','Age', 'Sex_numeric', 'Group']].dropna()
  1001. #Spearman correlation because Abeta 42 is a continous variable
  1002. spearman_corr_ttau, spearman_p_ttau = stats.spearmanr(ttau_temp_df['Value'], ttau_temp_df['T_tau_LCR'])
  1003. print(f"Spearman Correlation: ρ = {spearman_corr_ttau:.3f}, p = {spearman_p_ttau:.4f}")
  1004. print(f'The sample size was: {len(ttau_temp_df)}')
  1005. partial_spearman_ttau = pg.partial_corr(data=df, x='Value', y='T_tau_LCR',
  1006. covar='Age', method='spearman')
  1007. print("\nSpearman partial correlation:")
  1008. print(partial_spearman_ttau)
  1009. #Adjust for sex and age
  1010. print('='*50)
  1011. print('CORRELATION WITH ABETA 42 (ADJUSTED FOR SEX AND AGE)')
  1012. print('='*50)
  1013. partial_corr_ttau= pg.partial_corr(data= ttau_temp_df, x= 'T_tau_LCR', y= 'Value', covar=['Age', 'Sex_numeric'], method= 'spearman')
  1014. print(f"ρ: {partial_corr_ttau['p-val'].iloc[0]:.4f}, p = {partial_corr_ttau['p-val'].iloc[0]:.4f}")
  1015. print('*'*50)
  1016. # Compare unadjusted vs adjusted results
  1017. print(f"\nComparison:")
  1018. print(f"Unadjusted p-value : {spearman_p_ttau:.4f}")
  1019. print(f"Age-adjusted p-value (ANCOVA): {partial_corr_ttau['p-val'].iloc[0]:.4f}")
  1020. if abs(spearman_p_ttau - partial_corr_ttau['p-val'].iloc[0]) > 0.05:
  1021. print("Note: Adjustment for age and sex meaningfully changed the results")
  1022. else:
  1023. print("Note: Results were robust to age and sex adjustment")
  1024. print('='*50)
  1025. #Create a scatterplot
  1026. plt.figure(figsize=(8,6), dpi= 600)
  1027. sns.scatterplot(data= ttau_temp_df, x= "T_tau_LCR", y= "Value",
  1028. hue= 'Group', palette= 'RdBu', hue_order= group_order)
  1029. plt.title(f"hK6 vs. t-Tau (Spearman ρ = {spearman_corr_ttau:.3f}, p = {spearman_p_ttau:.4f})")
  1030. plt.xlabel("T-tau values (pg/mL)")
  1031. plt.ylabel("hK6 concentration (ng/mL)")
  1032. plt.savefig('comb_hK6vstTau.jpg',
  1033. dpi=600,
  1034. bbox_inches='tight', # removes extra white space
  1035. pad_inches=0.1, # small padding around figure
  1036. facecolor='white', # background color
  1037. edgecolor='none', # no border
  1038. transparent=False) # not transparent
  1039. plt.show()
  1040. # %% [markdown]
  1041. # ### Correction for multiple testing
  1042. # %%
  1043. # IMPORTANT
  1044. # Correct for multiple testing
  1045. print('='*50)
  1046. print('MULTIPLE TESTING CORRECTION')
  1047. print('='*50)
  1048. # Collect all raw p-values
  1049. raw_p_values = [
  1050. spearman_p_age, partial_spearman_age['p-val'].iloc[0],
  1051. p_value_sex, partial_corr_sex['p-val'].iloc[0],
  1052. spearman_p_mmse, partial_spearman_mmse['p-val'].iloc[0],
  1053. p_value_apoe, ancova_result['p-unc'].iloc[0], # APOE main effect from ANCOVA
  1054. spearman_p_abeta, partial_spearman_abeta['p-val'].iloc[0],
  1055. spearman_p_ptau, partial_corr_ptau['p-val'].iloc[0],
  1056. spearman_p_ttau, partial_corr_ttau['p-val'].iloc[0]
  1057. ]
  1058. # Create a summary DataFrame (FIXED syntax errors)
  1059. variables = ['Age', 'Age (adj)',
  1060. 'Sex', 'Sex (adj)',
  1061. 'MMSE', 'MMSE (adj)',
  1062. 'APOE_Status', 'APOE_Status (adj)',
  1063. "Aβ1-42", "Aβ1-42 (adj)",
  1064. 'pTau', 'pTau (adj)',
  1065. 'tTau', 'tTau (adj)']
  1066. tests = ['Spearman', 'Partial Spearman',
  1067. 'Mann-Whitney U', 'Partial Spearman',
  1068. 'Spearman', 'Partial Spearman',
  1069. 'ANOVA/Kruskal-Wallis', 'ANCOVA',
  1070. 'Spearman', 'Partial Spearman',
  1071. 'Spearman', 'Partial Spearman',
  1072. 'Spearman', 'Partial Spearman']
  1073. # Apply Holm correction
  1074. rejected_Holm, holm_p, _, _ = multipletests(raw_p_values, alpha=0.05, method='holm')
  1075. results_summary = pd.DataFrame({
  1076. 'Variable': variables,
  1077. 'Test': tests,
  1078. 'Raw_p_value': raw_p_values,
  1079. 'Holm_Adjusted_p': holm_p,
  1080. 'Significant_Holm': rejected_Holm
  1081. })
  1082. print("Summary of Results with Multiple Testing Correction:")
  1083. print(results_summary.round(4))
  1084. # Highlight significant results after correction
  1085. print("\nSignificant results after Holm correction (p < 0.05):")
  1086. significant_results = results_summary[results_summary['Holm_Adjusted_p'] < 0.05]
  1087. if len(significant_results) > 0:
  1088. print(significant_results[['Variable', 'Test', 'Raw_p_value', 'Holm_Adjusted_p']].round(4))
  1089. else:
  1090. print("No significant results after multiple testing correction.")
  1091. # %% [markdown]
  1092. # ## Correlation of hK6 concentration with Age, Sex, MMSE score, APOE status, Abeta, P tau and T tau (non-combined data)
  1093. #
  1094. # - We used the raw data with Shapiro-Wlilk test, because we are breaking the groups into smaller subgroups, unlike in the earlier analysis. This applies to all analyses herein.
  1095. # - MCI_Progressor = MCI_conver_toDem
  1096. # %% [markdown]
  1097. # ### Correlation with Age
  1098. # %%
  1099. #Correlation with AGE for MCI non progressors
  1100. plot_spearman_correlation('Age', 'Value', data=group_nonprog, xlabel= 'Age (years)',
  1101. ylabel= 'hK6 concentration (ng/mL)',
  1102. title= "hK6 vs Age Spearman Correlation for MCI-nonProg")
  1103. # %%
  1104. #Correlation with AGE for MCI progressors
  1105. plot_spearman_correlation('Age', 'Value', data=group_prog, xlabel= 'Age (years)',
  1106. ylabel= 'hK6 concentration (ng/mL)', title= "hK6 vs Age Spearman Correlation for MCI-Prog")
  1107. # %%
  1108. #Correlation with AGE for SCD
  1109. plot_spearman_correlation('Age', 'Value', data=group_SCD, xlabel= 'Age (years)',
  1110. ylabel= 'hK6 concentration (ng/mL)', title= "hK6 vs Age Spearman Correlation for SCD")
  1111. # %%
  1112. #Correlation with AGE for AD
  1113. plot_spearman_correlation('Age', 'Value', data=group_AD, xlabel= 'Age (years)',
  1114. ylabel= 'hK6 concentration (ng/mL)', title= "hK6 vs Age Spearman Correlation for AD")
  1115. # %% [markdown]
  1116. # ### Correlation with Sex
  1117. # %%
  1118. #Correlation with Sex MCI-nonProg
  1119. print('='*50)
  1120. print('CORRELATION WITH SEX FOR MCI-NON PROGRESSORS')
  1121. print('='*50)
  1122. # Check normality within each group using Shapiro-Wilk
  1123. for sex_group in group_nonprog['Sex'].unique():
  1124. group_data = group_nonprog[group_nonprog['Sex'] == sex_group]['Value'].dropna()
  1125. stat_nonprog, p_nonprog = stats.shapiro(group_data)
  1126. print(f"Shapiro-Wilk for {sex_group}: p = {p_nonprog:.4f}")
  1127. print(" ")
  1128. # %%
  1129. print("="*50)
  1130. print("The Shapiro-Wilk test rejected the H0, the data deviates from normality")
  1131. #Use Mann-Whitney U test, non-normal data
  1132. male_data_nonprog = group_nonprog[group_nonprog['Sex']== 'Male']['Value'].dropna()
  1133. female_data_nonprog = group_nonprog[group_nonprog['Sex']== 'Female']['Value'].dropna()
  1134. stat_sex_nonprog, p_value_sex_nonprog = stats.mannwhitneyu(male_data_nonprog, female_data_nonprog, alternative= 'two-sided')
  1135. print(f"\nMann-Whitney U Test: U = {stat_sex_nonprog}, p = {p_value_sex_nonprog:.4f}")
  1136. # Create a box plot
  1137. plt.figure(figsize=(8, 6), dpi=600)
  1138. sns.boxplot(data=group_nonprog, x='Sex', y='Value', hue= "Sex", palette= "RdBu")
  1139. sns.stripplot(data=group_nonprog, x='Sex', y='Value', color='black', alpha=0.5, size=4)
  1140. plt.xlabel("Sex")
  1141. plt.ylabel("hK6 concentration (ng/mL)")
  1142. plt.title(f'hK6 Levels by Sex in MCI-nonProg (Mann-Whitney p = {p_value_sex_nonprog:.3f})')
  1143. plt.savefig('hK6vsSex_MCI_nonProg.jpg',
  1144. dpi=600,
  1145. bbox_inches='tight', # removes extra white space
  1146. pad_inches=0.1, # small padding around figure
  1147. facecolor='white', # background color
  1148. edgecolor='none', # no border
  1149. transparent=False) # not transparent
  1150. plt.show()
  1151. # %%
  1152. #Correlation with Sex MCI-Prog
  1153. print('='*50)
  1154. print('CORRELATION WITH SEX FOR MCI-PROGRESSORS')
  1155. print('='*50)
  1156. # Check normality within each group using Shapiro-Wilk
  1157. for sex_group in group_prog['Sex'].unique():
  1158. group_data = group_prog[group_prog['Sex'] == sex_group]['Value'].dropna()
  1159. stat_prog, p_prog = stats.shapiro(group_data)
  1160. print(f"Shapiro-Wilk for {sex_group}: p = {p_prog:.4f}")
  1161. # %%
  1162. print("The Shapiro-Wilk test rejected the H0, the data deviates from normality")
  1163. #Use Mann-Whitney U test, non-normal data
  1164. male_data_prog = group_prog[group_prog['Sex']== 'Male']['Value'].dropna()
  1165. female_data_prog = group_prog[group_prog['Sex']== 'Female']['Value'].dropna()
  1166. stat_sex_prog, p_value_sex_prog = stats.mannwhitneyu(male_data_prog, female_data_prog, alternative= 'two-sided')
  1167. print(f"\nMann-Whitney U Test: U = {stat_sex_prog}, p = {p_value_sex_prog:.4f}")
  1168. # Create a box plot
  1169. plt.figure(figsize=(8, 6), dpi=600)
  1170. sns.boxplot(data=group_prog, x='Sex', y='Value', hue= "Sex", palette= "RdBu")
  1171. sns.stripplot(data=group_prog, x='Sex', y='Value', color='black', alpha=0.5, size=4)
  1172. plt.xlabel("Sex")
  1173. plt.ylabel("hK6 concentration (ng/mL)")
  1174. plt.title(f'hK6 Levels by Sex in MCI-Prog (Mann-Whitney p = {p_value_sex_prog:.3f})')
  1175. plt.savefig('hK6vsSex_MCI_Prog.jpg',
  1176. dpi=600,
  1177. bbox_inches='tight', # removes extra white space
  1178. pad_inches=0.1, # small padding around figure
  1179. facecolor='white', # background color
  1180. edgecolor='none', # no border
  1181. transparent=False) # not transparent
  1182. plt.show()
  1183. # %%
  1184. #Correlation with Sex SCD
  1185. print('='*50)
  1186. print('CORRELATION WITH SEX FOR SCD')
  1187. print('='*50)
  1188. # Check normality within each group using Shapiro-Wilk
  1189. for sex_group in group_SCD['Sex'].unique():
  1190. group_data = group_SCD[group_SCD['Sex'] == sex_group]['Value'].dropna()
  1191. stat_SCD, p_SCD = stats.shapiro(group_data)
  1192. print(f"Shapiro-Wilk for {sex_group}: p = {p_SCD:.4f}")
  1193. # %%
  1194. #Use Mann-Whitney U test, non-normal data
  1195. male_data_SCD = group_SCD[group_SCD['Sex']== 'Male']['Value'].dropna()
  1196. female_data_SCD = group_SCD[group_SCD['Sex']== 'Female']['Value'].dropna()
  1197. stat_sex_SCD, p_value_sex_SCD = stats.mannwhitneyu(male_data_SCD, female_data_SCD, alternative= 'two-sided')
  1198. print(f"\nMann-Whitney U Test: U = {stat_sex_SCD}, p = {p_value_sex_SCD:.4f}")
  1199. # Create a box plot
  1200. plt.figure(figsize=(8, 6), dpi=600)
  1201. sns.boxplot(data=group_SCD, x='Sex', y='Value', hue= "Sex", palette= "RdBu")
  1202. sns.stripplot(data=group_SCD, x='Sex', y='Value', color='black', alpha=0.5, size=4)
  1203. plt.xlabel("Sex")
  1204. plt.ylabel("hK6 concentration (ng/mL)")
  1205. plt.title(f'hK6 Levels by Sex in SCD (Mann-Whitney p = {p_value_sex_SCD:.3f})')
  1206. plt.savefig('hK6vsSex_SCD.jpg',
  1207. dpi=600,
  1208. bbox_inches='tight', # removes extra white space
  1209. pad_inches=0.1, # small padding around figure
  1210. facecolor='white', # background color
  1211. edgecolor='none', # no border
  1212. transparent=False) # not transparent
  1213. plt.show()
  1214. # %%
  1215. #Correlation with Sex AD
  1216. print('='*50)
  1217. print('CORRELATION WITH SEX FOR AD')
  1218. print('='*50)
  1219. # Check normality within each group using Shapiro-Wilk
  1220. for sex_group in group_AD['Sex'].unique():
  1221. group_data = group_AD[group_AD['Sex'] == sex_group]['Value'].dropna()
  1222. stat_AD, p_AD = stats.shapiro(group_data)
  1223. print(f"Shapiro-Wilk for {sex_group}: p = {p_AD:.4f}")
  1224. # %%
  1225. #Use Mann-Whitney U test, non-normal data
  1226. male_data_AD = group_AD[group_AD['Sex']== 'Male']['Value'].dropna()
  1227. female_data_AD = group_AD[group_AD['Sex']== 'Female']['Value'].dropna()
  1228. stat_sex_AD, p_value_sex_AD = stats.mannwhitneyu(male_data_AD, female_data_AD, alternative= 'two-sided')
  1229. print(f"\nMann-Whitney U Test: U = {stat_sex_AD}, p = {p_value_sex_AD:.4f}")
  1230. # Create a box plot
  1231. plt.figure(figsize=(8, 6), dpi=600)
  1232. sns.boxplot(data=group_AD, x='Sex', y='Value', hue= "Sex", palette= "RdBu")
  1233. sns.stripplot(data=group_AD, x='Sex', y='Value', color='black', alpha=0.5, size=4)
  1234. plt.xlabel("Sex")
  1235. plt.ylabel("hK6 concentration (ng/mL)")
  1236. plt.title(f'hK6 Levels by Sex in AD (Mann-Whitney p = {p_value_sex_AD:.3f})')
  1237. plt.savefig('hK6vsSex_AD.jpg',
  1238. dpi=600,
  1239. bbox_inches='tight', # removes extra white space
  1240. pad_inches=0.1, # small padding around figure
  1241. facecolor='white', # background color
  1242. edgecolor='none', # no border
  1243. transparent=False) # not transparent
  1244. plt.show()
  1245. # %% [markdown]
  1246. # ### Correlation with MMSE score
  1247. # %%
  1248. #MCI-nonProgressors
  1249. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1250. mmse_temp_df_nonprog = group_nonprog[["Value", 'MMSE_PL', 'Group']].dropna()
  1251. print(f"Original sample size: {len(df)}")
  1252. print(f"Sample size after removing missing values: {len(mmse_temp_df_nonprog)}")
  1253. plot_spearman_correlation('MMSE_PL', 'Value', data=mmse_temp_df_nonprog,
  1254. xlabel= 'MMSE Score (1-30)', ylabel= 'hK6 concentration (ng/mL)',
  1255. title= "hK6 vs MMSE Score Spearman Correlation for MCI-nonProg")
  1256. # %%
  1257. #MCI-progressors
  1258. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1259. mmse_temp_df_prog = group_prog[["Value", 'MMSE_PL']].dropna()
  1260. print(f"Original sample size: {len(df)}")
  1261. print(f"Sample size after removing missing values: {len(mmse_temp_df_prog)}")
  1262. plot_spearman_correlation('MMSE_PL', 'Value', data=mmse_temp_df_prog, xlabel= 'MMSE Score (1-30)',
  1263. ylabel= 'hK6 concentration (ng/mL)', title= "hK6 vs MMSE Score Spearman Correlation for MCI-Prog")
  1264. # %%
  1265. #SCD
  1266. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1267. mmse_temp_df_SCD = group_SCD[["Value", 'MMSE_PL']].dropna()
  1268. print(f"Original sample size: {len(df)}")
  1269. print(f"Sample size after removing missing values: {len(mmse_temp_df_SCD)}")
  1270. plot_spearman_correlation('MMSE_PL', 'Value', data=mmse_temp_df_SCD, xlabel= 'MMSE Score (1-30)',
  1271. ylabel= 'hK6 concentration (ng/mL)', title= "hK6 vs MMSE Score Spearman Correlation for SCD")
  1272. # %%
  1273. #AD
  1274. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1275. mmse_temp_df_AD = group_AD[["Value", 'MMSE_PL']].dropna()
  1276. print(f"Original sample size: {len(df)}")
  1277. print(f"Sample size after removing missing values: {len(mmse_temp_df_AD)}")
  1278. plot_spearman_correlation('MMSE_PL', 'Value', data=mmse_temp_df_AD, xlabel= 'MMSE Score (1-30)',
  1279. ylabel= 'hK6 concentration (ng/mL)', title= "hK6 vs MMSE Score Spearman Correlation for AD")
  1280. # %% [markdown]
  1281. # ### Correlation with APOE status
  1282. # %%
  1283. #Correlation with APOE status for MCI-nonProg
  1284. print('='*50)
  1285. print("CORRELATION WITH APOE STATUS FOR MCI NON-PROGRESSORS")
  1286. print('='*50)
  1287. #remove NAs like in the MMSE analysis
  1288. apoe_temp_df_nonprog = group_nonprog[["log2_Value","Value", 'APOE', 'Group']].dropna()
  1289. # Prepare data for ANOVA - create a list of arrays for each group
  1290. apoe_groups = []
  1291. for status in apoe_temp_df_nonprog['APOE'].unique():
  1292. group_data = apoe_temp_df_nonprog[apoe_temp_df_nonprog['APOE'] == status]['log2_Value']
  1293. apoe_groups.append(group_data)
  1294. print(f"APOE {status}: n = {len(group_data)}, mean = {np.mean(group_data):.2f} ± {np.std(group_data):.2f}")
  1295. # Perform one-way ANOVA
  1296. f_stat_apoe_nonprog, p_value_apoe_nonprog = f_oneway(*apoe_groups)
  1297. print(f"\nOne-way ANOVA: F = {f_stat_apoe_nonprog:.3f}, p = {p_value_apoe_nonprog:.4f}")
  1298. # If ANOVA is significant (p < 0.05), perform Tukey's HSD post-hoc test
  1299. if p_value_apoe_nonprog < 0.05:
  1300. print("\nPerforming Tukey's HSD post-hoc test:")
  1301. # Prepare data for Tukey test
  1302. tukey_data = apoe_temp_df_nonprog[['log2_Value', 'APOE']].dropna()
  1303. tukey_results = pairwise_tukeyhsd(tukey_data['log2_Value'], tukey_data['APOE'])
  1304. print(tukey_results)
  1305. # Plot the results
  1306. tukey_results.plot_simultaneous()
  1307. plt.title("Tukey HSD Post-hoc Test - Confidence Intervals")
  1308. plt.show()
  1309. else:
  1310. print("ANOVA not significant - no post-hoc tests needed.")
  1311. # Create a box plot
  1312. plt.figure(figsize=(10, 6), dpi= 600)
  1313. sns.boxplot(data=apoe_temp_df_nonprog, x='APOE', y='Value', hue = "APOE",palette= 'viridis', boxprops = dict(alpha= 0.6))
  1314. sns.stripplot(data=apoe_temp_df_nonprog, x='APOE', y='Value', color='black', alpha=0.5, size=4)
  1315. plt.title(f'hK6 Levels by APOE Status in MCI-nonProg (log2 values ANOVA p = {p_value_apoe_nonprog:.3f})')
  1316. plt.xticks(rotation=45)
  1317. plt.xlabel("APOE Status")
  1318. plt.ylabel("hK6 concentration (ng/mL)")
  1319. plt.tight_layout()
  1320. plt.savefig('hK6vsAPOE_MCI_noprog.jpg',
  1321. dpi=600,
  1322. bbox_inches='tight', # removes extra white space
  1323. pad_inches=0.1, # small padding around figure
  1324. facecolor='white', # background color
  1325. edgecolor='none', # no border
  1326. transparent=False) # not transparent
  1327. plt.show()
  1328. # %%
  1329. #Correlation with APOE status for MCI-Prog
  1330. print('='*50)
  1331. print("CORRELATION WITH APOE STATUS FOR MCI PROGRESSORS")
  1332. print('='*50)
  1333. #remove NAs like in the MMSE analysis
  1334. apoe_temp_df_prog = group_prog[["log2_Value","Value", 'APOE', 'Group']].dropna()
  1335. # Prepare data for ANOVA - create a list of arrays for each group
  1336. apoe_groups = []
  1337. for status in apoe_temp_df_prog['APOE'].unique():
  1338. group_data = apoe_temp_df_prog[apoe_temp_df_prog['APOE'] == status]['log2_Value']
  1339. apoe_groups.append(group_data)
  1340. print(f"APOE {status}: n = {len(group_data)}, mean = {np.mean(group_data):.2f} ± {np.std(group_data):.2f}")
  1341. # Perform one-way ANOVA
  1342. f_stat_apoe_prog, p_value_apoe_prog = f_oneway(*apoe_groups)
  1343. print(f"\nOne-way ANOVA: F = {f_stat_apoe_prog:.3f}, p = {p_value_apoe_prog:.4f}")
  1344. # If ANOVA is significant (p < 0.05), perform Tukey's HSD post-hoc test
  1345. if p_value_apoe_prog < 0.05:
  1346. print("\nPerforming Tukey's HSD post-hoc test:")
  1347. # Prepare data for Tukey test
  1348. tukey_data = apoe_temp_df_prog[['log2_Value', 'APOE']].dropna()
  1349. tukey_results = pairwise_tukeyhsd(tukey_data['log2_Value'], tukey_data['APOE'])
  1350. print(tukey_results)
  1351. # Plot the results
  1352. tukey_results.plot_simultaneous()
  1353. plt.title("Tukey HSD Post-hoc Test - Confidence Intervals")
  1354. plt.show()
  1355. else:
  1356. print("ANOVA not significant - no post-hoc tests needed.")
  1357. # Create a box plot
  1358. plt.figure(figsize=(10, 6), dpi=600)
  1359. sns.boxplot(data=apoe_temp_df_prog, x='APOE', y='Value', hue= "APOE", palette = "viridis", boxprops= dict(alpha=0.6))
  1360. sns.stripplot(data=apoe_temp_df_prog, x='APOE', y='Value', color='black', alpha=0.5, size=4)
  1361. plt.title(f'hK6 Levels by APOE Status in MCI-Prog (log2 values ANOVA p = {p_value_apoe_prog:.3f})')
  1362. plt.xticks(rotation=45)
  1363. plt.xlabel("APOE Status")
  1364. plt.ylabel("hK6 concentration (ng/mL)")
  1365. plt.tight_layout()
  1366. plt.savefig('hK6vsAPOE_MCI_prog.jpg',
  1367. dpi=600,
  1368. bbox_inches='tight', # removes extra white space
  1369. pad_inches=0.1, # small padding around figure
  1370. facecolor='white', # background color
  1371. edgecolor='none', # no border
  1372. transparent=False) # not transparent
  1373. plt.show()
  1374. # %%
  1375. #Correlation with APOE status for SCD
  1376. print('='*50)
  1377. print("CORRELATION WITH APOE STATUS FOR SCD")
  1378. print('='*50)
  1379. #remove NAs like in the MMSE analysis
  1380. apoe_temp_df_SCD = group_SCD[["log2_Value","Value", 'APOE', 'Group']].dropna()
  1381. # Prepare data for ANOVA - create a list of arrays for each group
  1382. apoe_groups = []
  1383. for status in apoe_temp_df_SCD['APOE'].unique():
  1384. group_data = apoe_temp_df_SCD[apoe_temp_df_SCD['APOE'] == status]['log2_Value']
  1385. apoe_groups.append(group_data)
  1386. print(f"APOE {status}: n = {len(group_data)}, mean = {np.mean(group_data):.2f} ± {np.std(group_data):.2f}")
  1387. # Perform one-way ANOVA
  1388. f_stat_apoe_SCD, p_value_apoe_SCD = f_oneway(*apoe_groups)
  1389. print(f"\nOne-way ANOVA: F = {f_stat_apoe_SCD:.3f}, p = {p_value_apoe_SCD:.4f}")
  1390. # If ANOVA is significant (p < 0.05), perform Tukey's HSD post-hoc test
  1391. if p_value_apoe_SCD < 0.05:
  1392. print("\nPerforming Tukey's HSD post-hoc test:")
  1393. # Prepare data for Tukey test
  1394. tukey_data = apoe_temp_df_SCD[['log2_Value', 'APOE']].dropna()
  1395. tukey_results = pairwise_tukeyhsd(tukey_data['log2_Value'], tukey_data['APOE'])
  1396. print(tukey_results)
  1397. # Plot the results
  1398. tukey_results.plot_simultaneous()
  1399. plt.title("Tukey HSD Post-hoc Test - Confidence Intervals")
  1400. plt.show()
  1401. else:
  1402. print("ANOVA not significant - no post-hoc tests needed.")
  1403. # Create a box plot
  1404. plt.figure(figsize=(10, 6), dpi=600)
  1405. sns.boxplot(data=apoe_temp_df_SCD, x='APOE', y='Value', hue = "APOE",palette= 'viridis', boxprops = dict(alpha= 0.6))
  1406. sns.stripplot(data=apoe_temp_df_SCD, x='APOE', y='Value', color='black', alpha=0.5, size=4)
  1407. plt.title(f'hK6 Levels by APOE Status in SCD (log2 values ANOVA p = {p_value_apoe_SCD:.3f})')
  1408. plt.xticks(rotation=45)
  1409. plt.xlabel("APOE Status")
  1410. plt.ylabel("hK6 concentration (ng/mL)")
  1411. plt.tight_layout()
  1412. plt.savefig('hK6vsAPOE_SCD.jpg',
  1413. dpi=600,
  1414. bbox_inches='tight', # removes extra white space
  1415. pad_inches=0.1, # small padding around figure
  1416. facecolor='white', # background color
  1417. edgecolor='none', # no border
  1418. transparent=False) # not transparent
  1419. plt.show()
  1420. # %%
  1421. #Correlation with APOE status for AD
  1422. print('='*50)
  1423. print("CORRELATION WITH APOE STATUS FOR AD")
  1424. print('='*50)
  1425. #remove NAs like in the MMSE analysis
  1426. apoe_temp_df_AD = group_AD[["log2_Value","Value", 'APOE', 'Group']].dropna()
  1427. # Prepare data for ANOVA - create a list of arrays for each group
  1428. apoe_groups = []
  1429. for status in apoe_temp_df_AD['APOE'].unique():
  1430. group_data = apoe_temp_df_AD[apoe_temp_df_AD['APOE'] == status]['log2_Value']
  1431. apoe_groups.append(group_data)
  1432. print(f"APOE {status}: n = {len(group_data)}, mean = {np.mean(group_data):.2f} ± {np.std(group_data):.2f}")
  1433. # Perform one-way ANOVA
  1434. f_stat_apoe_AD, p_value_apoe_AD = f_oneway(*apoe_groups)
  1435. print(f"\nOne-way ANOVA: F = {f_stat_apoe_AD:.3f}, p = {p_value_apoe_AD:.4f}")
  1436. # If ANOVA is significant (p < 0.05), perform Tukey's HSD post-hoc test
  1437. if p_value_apoe_AD < 0.05:
  1438. print("\nPerforming Tukey's HSD post-hoc test:")
  1439. # Prepare data for Tukey test
  1440. tukey_data = apoe_temp_df_AD[['log2_Value', 'APOE']].dropna()
  1441. tukey_results = pairwise_tukeyhsd(tukey_data['log2_Value'], tukey_data['APOE'])
  1442. print(tukey_results)
  1443. # Plot the results
  1444. tukey_results.plot_simultaneous()
  1445. plt.title("Tukey HSD Post-hoc Test - Confidence Intervals")
  1446. plt.show()
  1447. else:
  1448. print("ANOVA not significant - no post-hoc tests needed.")
  1449. # Create a box plot
  1450. plt.figure(figsize=(10, 6), dpi= 600)
  1451. sns.boxplot(data=apoe_temp_df_AD, x='APOE', y='Value', hue = "APOE",palette= 'viridis', boxprops = dict(alpha= 0.6))
  1452. sns.stripplot(data=apoe_temp_df_AD, x='APOE', y='Value', color='black', alpha=0.5, size=4)
  1453. plt.title(f'hK6 Levels by APOE Status in AD (log2 values ANOVA p = {p_value_apoe_AD:.3f})')
  1454. plt.xticks(rotation=45)
  1455. plt.xlabel("APOE Status")
  1456. plt.ylabel("hK6 concentration (ng/mL)")
  1457. plt.tight_layout()
  1458. plt.savefig('hK6vsAPOE_AD.jpg',
  1459. dpi=600,
  1460. bbox_inches='tight', # removes extra white space
  1461. pad_inches=0.1, # small padding around figure
  1462. facecolor='white', # background color
  1463. edgecolor='none', # no border
  1464. transparent=False) # not transparent
  1465. plt.show()
  1466. # %% [markdown]
  1467. # ### Correlation with Abeta 42
  1468. # %%
  1469. #MCI-nonProgressors
  1470. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1471. abeta_temp_df_nonprog = group_nonprog[["Value", 'Abeta_42_LCR', 'Group']].dropna()
  1472. print(f"Original sample size: {len(df)}")
  1473. print(f"Sample size after removing missing values: {len(abeta_temp_df_nonprog)}")
  1474. plot_spearman_correlation('Abeta_42_LCR', 'Value', data=abeta_temp_df_nonprog, xlabel= 'Aβ1-42 (pg/mL)',
  1475. ylabel= 'hK6 concentration (ng/mL)', title=
  1476. "hK6 vs Aβ1-42 Spearman Correlation for MCI-nonProg")
  1477. # %%
  1478. #MCI-progressors
  1479. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1480. abeta_temp_df_prog = group_prog[["Value", 'Abeta_42_LCR', 'Group']].dropna()
  1481. print(f"Original sample size: {len(df)}")
  1482. print(f"Sample size after removing missing values: {len(abeta_temp_df_prog)}")
  1483. plot_spearman_correlation('Abeta_42_LCR', 'Value', data=abeta_temp_df_prog, xlabel= 'Aβ1-42 (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1484. title= "hK6 vs Aβ1-42 Spearman Correlation for MCI-Prog")
  1485. # %%
  1486. #SCD
  1487. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1488. abeta_temp_df_SCD = group_SCD[["Value", 'Abeta_42_LCR', 'Group']].dropna()
  1489. print(f"Original sample size: {len(df)}")
  1490. print(f"Sample size after removing missing values: {len(abeta_temp_df_SCD)}")
  1491. plot_spearman_correlation('Abeta_42_LCR', 'Value', data=abeta_temp_df_SCD, xlabel= 'Aβ1-42 (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1492. title= "hK6 vs Aβ1-42 Spearman Correlation for SCD")
  1493. # %%
  1494. #AD
  1495. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1496. abeta_temp_df_AD = group_AD[["Value", 'Abeta_42_LCR', 'Group']].dropna()
  1497. print(f"Original sample size: {len(df)}")
  1498. print(f"Sample size after removing missing values: {len(abeta_temp_df_AD)}")
  1499. plot_spearman_correlation('Abeta_42_LCR', 'Value', data=abeta_temp_df_AD, xlabel= 'Aβ1-42 (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1500. title= "hK6 vs Aβ1-42 Spearman Correlation for AD")
  1501. # %% [markdown]
  1502. # ### Correlation with p-tau
  1503. # %%
  1504. #MCI-nonprogressors
  1505. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1506. ptau_temp_df_nonprog = group_nonprog[["Value", 'P_tau_LCR', 'Group']].dropna()
  1507. print(f"Original sample size: {len(df)}")
  1508. print(f"Sample size after removing missing values: {len(ptau_temp_df_nonprog)}")
  1509. plot_spearman_correlation('P_tau_LCR', 'Value', data=ptau_temp_df_nonprog, xlabel= 'p-Tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)', title=
  1510. "hK6 vs p-Tau Spearman Correlation for MCI-nonProg")
  1511. # %%
  1512. #MCI-progressors
  1513. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1514. ptau_temp_df_prog = group_prog[["Value", 'P_tau_LCR', 'Group']].dropna()
  1515. print(f"Original sample size: {len(df)}")
  1516. print(f"Sample size after removing missing values: {len(ptau_temp_df_prog)}")
  1517. plot_spearman_correlation('P_tau_LCR', 'Value', data=ptau_temp_df_prog, xlabel= 'p-Tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1518. title= "hK6 vs p-Tau Spearman Correlation for MCI-Prog")
  1519. # %%
  1520. #SCD
  1521. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1522. ptau_temp_df_SCD = group_SCD[["Value", 'P_tau_LCR', 'Group']].dropna()
  1523. print(f"Original sample size: {len(df)}")
  1524. print(f"Sample size after removing missing values: {len(ptau_temp_df_SCD)}")
  1525. plot_spearman_correlation('P_tau_LCR', 'Value', data=ptau_temp_df_SCD, xlabel= 'p-Tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1526. title= "hK6 vs p-Tau Spearman Correlation for SCD")
  1527. # %%
  1528. #AD
  1529. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1530. ptau_temp_df_AD = group_AD[["Value", 'P_tau_LCR', 'Group']].dropna()
  1531. print(f"Original sample size: {len(df)}")
  1532. print(f"Sample size after removing missing values: {len(ptau_temp_df_AD)}")
  1533. plot_spearman_correlation('P_tau_LCR', 'Value', data=ptau_temp_df_AD, xlabel= 'p-Tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1534. title= "hK6 vs p-Tau Spearman Correlation for AD")
  1535. # %% [markdown]
  1536. # ### Correlation with t-tau
  1537. # %%
  1538. #MCI-nonprogressors
  1539. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1540. ttau_temp_df_nonprog = group_nonprog[["Value", 'T_tau_LCR', 'Group']].dropna()
  1541. print(f"Original sample size: {len(df)}")
  1542. print(f"Sample size after removing missing values: {len(ttau_temp_df_nonprog)}")
  1543. plot_spearman_correlation('T_tau_LCR', 'Value', data=ttau_temp_df_nonprog, xlabel= 'T-tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)', title=
  1544. "hK6 vs T-tau Spearman Correlation for MCI-nonProg")
  1545. # %%
  1546. #MCI-progressors
  1547. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1548. ttau_temp_df_prog = group_prog[["Value", 'T_tau_LCR', 'Group']].dropna()
  1549. print(f"Original sample size: {len(df)}")
  1550. print(f"Sample size after removing missing values: {len(ttau_temp_df_prog)}")
  1551. plot_spearman_correlation('T_tau_LCR', 'Value', data=ttau_temp_df_prog, xlabel= 'T-tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1552. title= "hK6 vs T-tau Spearman Correlation for MCI-Prog")
  1553. # %%
  1554. #SCD
  1555. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1556. ttau_temp_df_SCD = group_SCD[["Value", 'T_tau_LCR', 'Group']].dropna()
  1557. print(f"Original sample size: {len(df)}")
  1558. print(f"Sample size after removing missing values: {len(ttau_temp_df_SCD)}")
  1559. plot_spearman_correlation('T_tau_LCR', 'Value', data=ttau_temp_df_SCD, xlabel= 'T-tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1560. title= "hK6 vs T-tau Spearman Correlation for SCD")
  1561. # %%
  1562. #AD
  1563. #Because we know there are NAs here, we create a temporary dataframe to avoid errors
  1564. ttau_temp_df_AD = group_AD[["Value", 'T_tau_LCR', 'Group']].dropna()
  1565. print(f"Original sample size: {len(df)}")
  1566. print(f"Sample size after removing missing values: {len(ttau_temp_df_AD)}")
  1567. plot_spearman_correlation('T_tau_LCR', 'Value', data=ttau_temp_df_AD, xlabel= 'T-tau (pg/mL)', ylabel= 'hK6 concentration (ng/mL)',
  1568. title= "hK6 vs T-tau Spearman Correlation for AD")
  1569. # %% [markdown]
  1570. # ### Multiple testing correction
  1571. # %%
  1572. #Make sex binary in the four dataframes
  1573. group_prog['Sex_numeric'] = group_prog['Sex'].map({'Male': 0, 'Female': 1}) # or vice versa
  1574. group_nonprog['Sex_numeric'] = group_nonprog['Sex'].map({'Male': 0, 'Female': 1}) # or vice versa
  1575. group_SCD['Sex_numeric'] = group_SCD['Sex'].map({'Male': 0, 'Female': 1}) # or vice versa
  1576. group_AD['Sex_numeric'] = group_AD['Sex'].map({'Male': 0, 'Female': 1}) # or vice versa
  1577. # %%
  1578. #1. For Age- Spearman correlations
  1579. # Perform Spearman correlation (non-parametric, more robust)
  1580. spearman_corr_age_nonprog, spearman_p_age_nonprog = stats.spearmanr(group_nonprog['Value'].dropna(), group_nonprog['Age'].dropna())
  1581. partial_corr_age_nonprog = pg.partial_corr(data=group_nonprog, x='Age', y='Value', covar=['Sex_numeric'], method='spearman')
  1582. spearman_corr_age_prog, spearman_p_age_prog = stats.spearmanr(group_prog['Value'].dropna(), group_prog['Age'].dropna())
  1583. partial_corr_age_prog = pg.partial_corr(data=group_prog, x='Age', y='Value', covar=['Sex_numeric'], method='spearman')
  1584. spearman_corr_age_SCD, spearman_p_age_SCD = stats.spearmanr(group_SCD['Value'].dropna(), group_SCD['Age'].dropna())
  1585. partial_corr_age_SCD = pg.partial_corr(data=group_SCD, x='Age', y='Value', covar=['Sex_numeric'], method='spearman')
  1586. spearman_corr_age_AD, spearman_p_age_AD = stats.spearmanr(group_AD["Value"].dropna(), group_AD['Age'].dropna())
  1587. partial_corr_age_AD = pg.partial_corr(data=group_AD, x='Age', y='Value', covar=['Sex_numeric'], method='spearman')
  1588. # %%
  1589. #2. For Sex- Mann-Whitney U tests, ALREADY HAVE IT
  1590. # %%
  1591. #3. For MMSE score- spearman correlations
  1592. group_nonprog_mmse = group_nonprog.dropna(subset= ['MMSE_PL'])
  1593. group_prog_mmse = group_prog.dropna(subset= ['MMSE_PL'])
  1594. group_SCD_mmse = group_SCD.dropna(subset= ['MMSE_PL'])
  1595. group_AD_mmse = group_AD.dropna(subset= ['MMSE_PL'])
  1596. spearman_corr_MMSE_nonprog, spearman_p_MMSE_nonprog = stats.spearmanr(group_nonprog_mmse['Value'].dropna(), group_nonprog_mmse['MMSE_PL'].dropna())
  1597. partial_corr_MMSE_nonprog = pg.partial_corr(data=group_nonprog_mmse, x='MMSE_PL', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1598. spearman_corr_MMSE_prog, spearman_p_MMSE_prog = stats.spearmanr(group_nonprog_mmse['Value'].dropna(), group_nonprog_mmse['MMSE_PL'].dropna())
  1599. partial_corr_MMSE_prog = pg.partial_corr(data=group_prog_mmse, x='MMSE_PL', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1600. spearman_corr_MMSE_SCD, spearman_p_MMSE_SCD = stats.spearmanr(group_nonprog_mmse['Value'].dropna(), group_nonprog_mmse['MMSE_PL'].dropna())
  1601. partial_corr_MMSE_SCD = pg.partial_corr(data=group_SCD_mmse, x='MMSE_PL', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1602. spearman_corr_MMSE_AD, spearman_p_MMSE_AD = stats.spearmanr(group_nonprog_mmse["Value"].dropna(), group_nonprog_mmse['MMSE_PL'].dropna())
  1603. partial_corr_MMSE_AD = pg.partial_corr(data=group_AD_mmse, x='MMSE_PL', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1604. # %%
  1605. #4. For APOE status- ANOVA, ALREADY HAVE IT
  1606. # %%
  1607. #5. For Abeta 42- Spearman correlations
  1608. group_nonprog_abeta = group_nonprog.dropna(subset= ['Abeta_42_LCR'])
  1609. group_prog_abeta = group_prog.dropna(subset= ['Abeta_42_LCR'])
  1610. group_SCD_abeta = group_SCD.dropna(subset= ['Abeta_42_LCR'])
  1611. group_AD_abeta = group_AD.dropna(subset= ['Abeta_42_LCR'])
  1612. spearman_corr_Abeta_nonprog, spearman_p_Abeta_nonprog = stats.spearmanr(group_nonprog_abeta['Value'].dropna(), group_nonprog_abeta['Abeta_42_LCR'].dropna())
  1613. partial_corr_abeta_nonprog = pg.partial_corr(data=group_nonprog_abeta, x='Abeta_42_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1614. spearman_corr_Abeta_prog, spearman_p_Abeta_prog = stats.spearmanr(group_prog_abeta['Value'].dropna(), group_prog_abeta['Abeta_42_LCR'].dropna())
  1615. partial_corr_abeta_prog = pg.partial_corr(data=group_prog_abeta, x='Abeta_42_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1616. spearman_corr_Abeta_SCD, spearman_p_Abeta_SCD = stats.spearmanr(group_SCD_abeta['Value'].dropna(), group_SCD_abeta['Abeta_42_LCR'].dropna())
  1617. partial_corr_abeta_SCD = pg.partial_corr(data=group_SCD_abeta, x='Abeta_42_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1618. spearman_corr_Abeta_AD, spearman_p_Abeta_AD = stats.spearmanr(group_AD_abeta["Value"].dropna(), group_AD_abeta['Abeta_42_LCR'].dropna())
  1619. partial_corr_abeta_AD = pg.partial_corr(data=group_AD_abeta, x='Abeta_42_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1620. # %%
  1621. #6. For p-tau- Spearman correlations
  1622. group_nonprog_ptau = group_nonprog.dropna(subset= ['P_tau_LCR'])
  1623. group_prog_ptau = group_prog.dropna(subset= ['P_tau_LCR'])
  1624. group_SCD_ptau = group_SCD.dropna(subset= ['P_tau_LCR'])
  1625. group_AD_ptau = group_AD.dropna(subset= ['P_tau_LCR'])
  1626. spearman_corr_ptau_nonprog, spearman_p_ptau_nonprog = stats.spearmanr(group_nonprog_ptau['Value'].dropna(), group_nonprog_ptau['P_tau_LCR'].dropna())
  1627. partial_corr_ptau_nonprog = pg.partial_corr(data=group_nonprog_ptau, x='P_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1628. spearman_corr_ptau_prog, spearman_p_ptau_prog = stats.spearmanr(group_prog_ptau['Value'].dropna(), group_prog_ptau['P_tau_LCR'].dropna())
  1629. partial_corr_ptau_prog = pg.partial_corr(data=group_prog_ptau, x='P_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1630. spearman_corr_ptau_SCD, spearman_p_ptau_SCD = stats.spearmanr(group_SCD_ptau['Value'].dropna(), group_SCD_ptau['P_tau_LCR'].dropna())
  1631. partial_corr_ptau_SCD = pg.partial_corr(data=group_SCD_ptau, x='P_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1632. spearman_corr_ptau_AD, spearman_p_ptau_AD = stats.spearmanr(group_AD_ptau["Value"].dropna(), group_AD_ptau['P_tau_LCR'].dropna())
  1633. partial_corr_ptau_AD = pg.partial_corr(data=group_AD_ptau, x='P_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1634. # %%
  1635. #7. For t-tau- Spearman correlations
  1636. group_nonprog_ttau = group_nonprog.dropna(subset= ['T_tau_LCR'])
  1637. group_prog_ttau = group_prog.dropna(subset= ['T_tau_LCR'])
  1638. group_SCD_ttau = group_SCD.dropna(subset= ['T_tau_LCR'])
  1639. group_AD_ttau = group_AD.dropna(subset= ['T_tau_LCR'])
  1640. spearman_corr_ttau_nonprog, spearman_p_ttau_nonprog = stats.spearmanr(group_nonprog_ttau['Value'].dropna(), group_nonprog_ttau['T_tau_LCR'].dropna())
  1641. partial_corr_ttau_nonprog = pg.partial_corr(data=group_nonprog_ttau, x='T_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1642. spearman_corr_ttau_prog, spearman_p_ttau_prog = stats.spearmanr(group_prog_ttau['Value'].dropna(), group_prog_ttau['T_tau_LCR'].dropna())
  1643. partial_corr_ttau_prog = pg.partial_corr(data=group_prog_ttau, x='T_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1644. spearman_corr_ttau_SCD, spearman_p_ttau_SCD = stats.spearmanr(group_SCD_ttau['Value'].dropna(), group_SCD_ttau['T_tau_LCR'].dropna())
  1645. partial_corr_ttau_SCD = pg.partial_corr(data=group_SCD_ttau, x='T_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1646. spearman_corr_ttau_AD, spearman_p_ttau_AD = stats.spearmanr(group_AD_ttau["Value"].dropna(), group_AD_ttau['T_tau_LCR'].dropna())
  1647. partial_corr_ttau_AD = pg.partial_corr(data=group_AD_ttau, x='T_tau_LCR', y='Value', covar=['Sex_numeric', 'Age'], method='spearman')
  1648. # %%
  1649. print('='*50)
  1650. print('MULTIPLE TESTING CORRECTION FOR NON-COMBINED RESULTS')
  1651. print('='*50)
  1652. raw_p_values_noncomb = [spearman_p_age_nonprog, spearman_p_age_prog, spearman_p_age_SCD, spearman_p_age_AD,
  1653. partial_corr_age_nonprog['p-val'].iloc[0], partial_corr_age_prog['p-val'].iloc[0], partial_corr_age_SCD['p-val'].iloc[0], partial_corr_age_AD['p-val'].iloc[0],
  1654. p_value_sex_nonprog, p_value_sex_prog, p_value_sex_SCD, p_value_sex_AD,
  1655. spearman_p_MMSE_nonprog, spearman_p_MMSE_prog, spearman_p_MMSE_SCD, spearman_p_MMSE_AD,
  1656. partial_corr_MMSE_nonprog['p-val'].iloc[0], partial_corr_MMSE_prog['p-val'].iloc[0], partial_corr_MMSE_SCD['p-val'].iloc[0], partial_corr_MMSE_AD['p-val'].iloc[0],
  1657. p_value_apoe_nonprog, p_value_apoe_prog, p_value_apoe_SCD, p_value_apoe_AD,
  1658. spearman_p_Abeta_nonprog, spearman_p_Abeta_prog, spearman_p_Abeta_SCD, spearman_p_Abeta_AD,
  1659. partial_corr_abeta_nonprog['p-val'].iloc[0], partial_corr_abeta_prog['p-val'].iloc[0], partial_corr_abeta_SCD['p-val'].iloc[0], partial_corr_abeta_AD['p-val'].iloc[0],
  1660. spearman_p_ptau_nonprog, spearman_p_ptau_prog, spearman_p_ptau_SCD, spearman_p_ptau_AD,
  1661. spearman_p_ttau_nonprog, spearman_p_ttau_prog, spearman_p_ttau_SCD, spearman_p_ttau_AD ] # [Age, Sex, MMSE, APOE, Abeta, pTau, tTau]
  1662. #Apply Holm correction
  1663. rejected, holm_p_noncomb,_,_ = multipletests(raw_p_values_noncomb, alpha= 0.05, method= 'holm')
  1664. # Create a summary DataFrame
  1665. variables_noncomb = ['Age','Age', 'Age', 'Age'
  1666. ,'Age (adj)','Age (adj)', 'Age (adj)', 'Age (adj)'
  1667. ,'Sex','Sex', 'Sex', 'Sex'
  1668. ,'MMSE','MMSE', 'MMSE', 'MMSE'
  1669. ,'MMSE (adj)','MMSE (adj)', 'MMSE (adj)', 'MMSE (adj)'
  1670. ,'APOE_Status', 'APOE_Status', 'APOE_Status', 'APOE_Status'
  1671. ,"Aβ1-42", "Aβ1-42", "Aβ1-42", "Aβ1-42"
  1672. ,"Aβ1-42 (adj)", "Aβ1-42 (adj)", "Aβ1-42 (adj)", "Aβ1-42 (adj)"
  1673. ,'pTau', 'pTau', 'pTau', 'pTau'
  1674. ,'tTau', 'tTau', 'tTau', 'tTau']
  1675. tests_noncomb = ['Spearman','Spearman','Spearman','Spearman',
  1676. 'Adjusted Spearman','Adjusted Spearman','Adjusted Spearman','Adjusted Spearman',
  1677. 'Mann-Whitney U','Mann-Whitney U','Mann-Whitney U','Mann-Whitney U',
  1678. 'Spearman','Spearman','Spearman','Spearman',
  1679. 'Adjusted Spearman','Adjusted Spearman','Adjusted Spearman','Adjusted Spearman',
  1680. 'ANOVA','ANOVA','ANOVA','ANOVA',
  1681. 'Spearman','Spearman','Spearman','Spearman',
  1682. 'Adjusted Spearman','Adjusted Spearman','Adjusted Spearman','Adjusted Spearman',
  1683. 'Spearman','Spearman','Spearman','Spearman',
  1684. 'Spearman','Spearman','Spearman','Spearman']
  1685. groups_noncomb = ['MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1686. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1687. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1688. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1689. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1690. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1691. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1692. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1693. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD',
  1694. 'MCI non Progressor', 'MCI Progressor', 'SCD', 'AD']
  1695. results_summary_noncomb = pd.DataFrame({
  1696. 'Variable': variables_noncomb,
  1697. 'Test': tests_noncomb,
  1698. 'Group': groups_noncomb,
  1699. 'Raw_p_value': raw_p_values_noncomb,
  1700. 'Holm_p': holm_p_noncomb
  1701. })
  1702. print("Summary of Results with Multiple Testing Correction:")
  1703. print(results_summary_noncomb.round(4))
  1704. # Highlight significant results after correction
  1705. print("\nSignificant results after Holm correction (p < 0.05):")
  1706. significant_results_noncomb = results_summary_noncomb[results_summary_noncomb['Holm_p'] < 0.05]
  1707. if len(significant_results_noncomb) > 0:
  1708. print(significant_results_noncomb[['Group','Variable', 'Holm_p']])
  1709. else:
  1710. print("No significant results after multiple testing correction.")
  1711. # %% [markdown]
  1712. # ## Kruskal Wallis for all variables
  1713. # - Since we do not have to test for equal variances/normality, it is easier to perform Kruskal-Wallis on these variables.
  1714. # - MMSE Score variables has to be analyzed with Kruskal-Wallis test, due to it being an ordinal variable.
  1715. # %%
  1716. # Rename column 'Value' to 'hK6' in-place
  1717. df.rename(columns={'Value': 'hK6'}, inplace=True)
  1718. # %%
  1719. variables = ['Age', 'MMSE_PL', 'Abeta_42_LCR', 'P_tau_LCR', 'T_tau_LCR', 'hK6'] #Variables to perform Kruskal Wallis to
  1720. # Create a results dataframe to store all statistics
  1721. results = []
  1722. print("Kruskal-Wallis Test Results for Multiple Variables")
  1723. print("=" * 60)
  1724. for var in variables:
  1725. print(f"\nVariable: {var}")
  1726. print("-" * 30)
  1727. # Extract data for each group
  1728. group_data = []
  1729. for group in group_order:
  1730. group_values = df[df['Group'] == group][var].dropna()
  1731. group_data.append(group_values)
  1732. print(f"Group {group}: n={len(group_values)}, median={group_values.median():.2f}")
  1733. # Perform Kruskal-Wallis test
  1734. h_stat, p_value = stats.kruskal(*group_data)
  1735. print(f"Kruskal-Wallis H-statistic: {h_stat:.3f}, p-value: {p_value:.4f}")
  1736. # Store results
  1737. result_dict = {
  1738. 'Variable': var,
  1739. 'H_statistic': h_stat,
  1740. 'p_value': p_value,
  1741. 'significant': p_value < 0.05
  1742. }
  1743. # Add group medians and sample sizes
  1744. for i, group in enumerate(group_order):
  1745. result_dict[f'Group_{group}_median'] = group_data[i].median()
  1746. result_dict[f'Group_{group}_n'] = len(group_data[i])
  1747. results.append(result_dict)
  1748. # Perform Mann-Whitney U post-hoc tests with Holm correction if significant
  1749. if p_value < 0.05:
  1750. print("Significant difference found! Performing Mann-Whitney U post-hoc tests:")
  1751. # Calculate number of comparisons for correction
  1752. n_groups = len(group_order)
  1753. n_comparisons = n_groups * (n_groups - 1) // 2
  1754. print(f"Performing {n_comparisons} pairwise comparisons (Holm correction)")
  1755. print("\nPairwise comparisons (Mann-Whitney U):")
  1756. print("-" * 50)
  1757. posthoc_results = []
  1758. raw_p_values_for_Holm = [] # store raw p-values for Holm correction
  1759. # First pass: collect all raw p-values
  1760. for i in range(n_groups):
  1761. for j in range(i + 1, n_groups):
  1762. group1, group2 = group_order[i], group_order[j]
  1763. data1 = group_data[i]
  1764. data2 = group_data[j]
  1765. # Perform Mann-Whitney U test
  1766. stat, p_raw = stats.mannwhitneyu(data1, data2, alternative='two-sided')
  1767. # Calculate effect size (rank-biserial correlation)
  1768. n1, n2 = len(data1), len(data2)
  1769. effect_size = 1 - (2 * stat) / (n1 * n2) # simple effect size measure
  1770. result = {
  1771. 'Comparison': f'{group1} vs {group2}',
  1772. 'U_statistic': stat,
  1773. 'Raw_p_value': p_raw,
  1774. 'Effect_size': effect_size
  1775. }
  1776. posthoc_results.append(result)
  1777. raw_p_values_for_Holm.append(p_raw)
  1778. # Apply Holm correction to all p-values (AFTER collecting them all)
  1779. rejected, holm_p_values, _, _ = multipletests(raw_p_values_for_Holm, alpha=0.05, method='holm')
  1780. # Update results with Holm-corrected p-values
  1781. for k, result in enumerate(posthoc_results):
  1782. result['Adjusted_p_value'] = holm_p_values[k]
  1783. result['Significant'] = holm_p_values[k] < 0.05
  1784. result['Comparison_index'] = k
  1785. # Calculate significance marker first to avoid f-string issues
  1786. sig_marker = '*' if result['Significant'] else ''
  1787. # Use consistent quotes and correct variable names
  1788. print(f"{result['Comparison']}: U = {result['U_statistic']:.1f}, "
  1789. f"raw p = {result['Raw_p_value']:.4f}, "
  1790. f"adj p = {result['Adjusted_p_value']:.4f} {sig_marker}")
  1791. # Add posthoc results to the main results
  1792. result_dict['posthoc_results'] = pd.DataFrame(posthoc_results)
  1793. # Convert results to dataframe
  1794. results_df = pd.DataFrame(results)
  1795. # Display summary table
  1796. print("\n" + "=" * 80)
  1797. print("SUMMARY TABLE")
  1798. print("=" * 80)
  1799. summary_cols = ['Variable', 'H_statistic', 'p_value', 'significant']
  1800. for group in group_order:
  1801. summary_cols.extend([f'Group_{group}_median', f'Group_{group}_n'])
  1802. print(results_df[summary_cols].round(4))
  1803. # Create visualizations
  1804. # use correct group order
  1805. n_vars = len(variables)
  1806. n_cols = min(3, n_vars)
  1807. n_rows = (n_vars + n_cols - 1) // n_cols # ceiling division
  1808. fig, axes = plt.subplots(n_rows, n_cols, figsize=(5*n_cols, 5*n_rows), dpi=600)
  1809. if n_vars > 1:
  1810. axes = axes.flatten()
  1811. else:
  1812. axes = [axes]
  1813. for i, var in enumerate(variables):
  1814. if i < len(axes):
  1815. # Create boxplot (for the subplots)
  1816. sns.boxplot(data=df, x='Group', y=var, ax=axes[i], hue='Group',
  1817. palette='RdBu', legend=False, hue_order=group_order, order=group_order)
  1818. sns.stripplot(data=df, x='Group', y=var, ax=axes[i], legend=False,
  1819. alpha=0.6, color='black', size=3, order=group_order)
  1820. p_val = results_df[results_df['Variable'] == var]['p_value'].values[0]
  1821. sig_stars = '***' if p_val < 0.001 else '**' if p_val < 0.01 else '*' if p_val < 0.05 else 'ns'
  1822. axes[i].set_title(f'{var}\np = {p_val:.4f} {sig_stars}', size=13)
  1823. axes[i].set_xlabel('Groups', size=12)
  1824. axes[i].set_ylabel(var)
  1825. axes[i].set_xticks(range(len(df['Group'].unique())))
  1826. axes[i].set_xticklabels(group_order, ha="right", rotation_mode="anchor")
  1827. axes[i].tick_params(axis="x", rotation=45)
  1828. # Hide any empty subplots
  1829. for j in range(len(variables), len(axes)):
  1830. axes[j].set_visible(False)
  1831. plt.tight_layout()
  1832. plt.suptitle('Distribution of Variables Across the Four Diagnostic Groups', fontsize=16, y=1.02)
  1833. plt.savefig('kruskal_wallis_results_7_figures.jpg', dpi=600, bbox_inches='tight')
  1834. plt.show()
  1835. # Save results to CSV
  1836. results_df.to_csv('kruskal_wallis_results.csv', index=False)
  1837. print("\nResults saved to 'kruskal_wallis_results.csv'")
  1838. # Print significant findings
  1839. print("\n" + "=" * 80)
  1840. print("SIGNIFICANT FINDINGS")
  1841. print("=" * 80)
  1842. for _, row in results_df.iterrows():
  1843. if row['significant']:
  1844. print(f"\n{row['Variable']} shows significant differences across groups (p = {row['p_value']:.4f})")
  1845. # FIXED: Check if posthoc_results exists and is not empty
  1846. if 'posthoc_results' in row and hasattr(row['posthoc_results'], 'empty'):
  1847. posthoc_df = row['posthoc_results']
  1848. if not posthoc_df.empty:
  1849. # Make sure the column name matches what you stored
  1850. sig_column = 'Significant' if 'Significant' in posthoc_df.columns else 'significant'
  1851. sig_comparisons = posthoc_df[posthoc_df[sig_column] == True]
  1852. if not sig_comparisons.empty:
  1853. print("Significant pairwise differences (Holm-corrected):")
  1854. for _, comp in sig_comparisons.iterrows():
  1855. print(f" {comp['Comparison']}: adj p = {comp['Adjusted_p_value']:.4f}")
  1856. else:
  1857. print(" No significant pairwise differences found after Holm correction")
  1858. else:
  1859. print(" No posthoc results available (empty DataFrame)")
  1860. else:
  1861. print(" No posthoc results available")
  1862. # 1. ANCOVA for continuous variables (age-adjusted group comparisons)
  1863. age_sensitive_vars = ['MMSE_PL', 'Abeta_42_LCR', 'P_tau_LCR', 'T_tau_LCR', 'hK6']
  1864. print("AGE-ADJUSTED COMPARISONS (ANCOVA)")
  1865. print("=" * 50)
  1866. for var in age_sensitive_vars:
  1867. print(f"\nVariable: {var} (adjusted for age)")
  1868. print("-" * 30)
  1869. # Perform ANCOVA with age as covariate
  1870. ancova_result = pg.ancova(data=df, dv=var, between='Group', covar='Age')
  1871. print(ancova_result)
  1872. # Compare with Kruskal-Wallis result
  1873. kw_p = results_df[results_df['Variable'] == var]['p_value'].values[0]
  1874. ancova_p = ancova_result['p-unc'].iloc[0] # Group effect p-value
  1875. print(f"Kruskal-Wallis p: {kw_p:.4f}")
  1876. print(f"ANCOVA (age-adjusted) p: {ancova_p:.4f}")
  1877. if abs(kw_p - ancova_p) > 0.05:
  1878. print("→ Age appears to be an important confounder")
  1879. else:
  1880. print("→ Results are robust to age adjustment")
  1881. # 2. Alternatively, use partial correlations for continuous relationships
  1882. print("\n" + "=" * 50)
  1883. print("AGE-ADJUSTED CORRELATIONS WITH GROUP (Partial Correlation)")
  1884. print("=" * 50)
  1885. # Create a numeric group variable for correlation
  1886. df['Group_numeric'] = df['Group'].map({group: i for i, group in enumerate(group_order)})
  1887. for var in age_sensitive_vars:
  1888. partial_corr = pg.partial_corr(data=df, x='Group_numeric', y=var,
  1889. covar=['Age'], method='spearman')
  1890. print(f"{var}: ρ = {partial_corr['r'].iloc[0]:.3f}, p = {partial_corr['p-val'].iloc[0]:.4f}")
  1891. # %% [markdown]
  1892. # ## Extra section: creation of tables for patient statistics, not necessary
  1893. # %%
  1894. #define for InterQuartile Range
  1895. def calculate_iqr(dataframe):
  1896. q1 = dataframe.quantile(0.25)
  1897. q3 = dataframe.quantile(0.75)
  1898. return q3 - q1
  1899. def q1(dataframe):
  1900. return dataframe.quantile(0.25)
  1901. def q3(dataframe):
  1902. return dataframe.quantile(0.75)
  1903. # %%
  1904. #Interquartile range
  1905. iqr = df.groupby("Group")["Value"].apply(calculate_iqr)
  1906. print(iqr)
  1907. print("-"*30)
  1908. iqr_log2 = df.groupby("Group")["log2_Value"].apply(calculate_iqr)
  1909. print(iqr_log2)
  1910. # %%
  1911. #Find the Q1 and Q3 by applying the functions
  1912. q_25 = df.groupby("Group")["Value"].apply(q1) #25th quantile
  1913. print(q_25)
  1914. print("-"*30)
  1915. q_75 = df.groupby("Group")['Value'].apply(q3) #75th quantile
  1916. print(q_75)
  1917. print("-"*30)
  1918. q_25_log = df.groupby("Group")["log2_Value"].apply(q1)
  1919. print(q_25_log)
  1920. print("-"*30)
  1921. q_75_log = df.groupby("Group")['log2_Value'].apply(q3)
  1922. print(q_75_log)
  1923. # %%
  1924. # Group by 'Group' and apply multiple aggregation functions to 'Value' and 'log2_Value
  1925. # Save the tables to an Excel file
  1926. table1_values = df.groupby('Group').agg({'Value': ['mean', 'median', 'std', 'min', 'max']})
  1927. Table1 = pd.concat([table1_values,q_25, q_75], join= "inner", axis=1) #concat to add quartiles
  1928. Table1.columns = ["Mean", "Median", "std", "Min", "Max", "Q1", "Q3"]#add headers to q_25 and q_75
  1929. print(Table1)
  1930. Table1.to_excel("my_table_values.xlsx")
  1931. table1_values_log2 = df.groupby('Group').agg({'log2_Value': ['mean', 'median', 'std', 'min', 'max']})
  1932. Table1_log2 = pd.concat([table1_values_log2,q_25_log, q_75_log], join= "inner", axis=1) #concat
  1933. Table1_log2.columns = ["Mean", "Median", "std", "Min", "Max", "Q1", "Q3"]#add headers to q_25 and q_75
  1934. print(Table1_log2)
  1935. Table1_log2.to_excel("my_table_log2_values.xlsx")

KLK6_ELISA_data_analysis.ipynb at commit eb12641, under MIT · at the source

Overview

Authors: Miyo K Chatanaka1, Antoninus Soosaipillai2, Amanda Cano3,4, Adelina Orellana3,4, Mercè Boada3,4, Ioannis Prassas1,5, Xavier Morató3,4, Eleftherios P Diamandis2
  1. Department of Laboratory Medicine and Pathobiology, University of Toronto, Toronto, ON Canada
  2. Lunenfeld-Tanenbaum Research Institute, Mount Sinai Hospital, Room L6-201, 60 Murray St, Toronto, ON Canada
  3. Ace Alzheimer Center Barcelona, International University of Catalunya (UIC), Barcelona, Spain
  4. Networking Research Center on Neurodegenerative Diseases (CIBERNED), Instituto de Salud Carlos III, Madrid, Spain
  5. Laboratory Medicine Program, University Health Network, Toronto, ON Canada
Journal: Clinical proteomics, volume 23, issue 1, article 23
Dates: received 26 September 2025; accepted 3 December 2025; published online 15 March 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1186/s12014-025-09577-x · PMID 41834054 · PMCID PMC13104293 · OpenAlex W7136088233
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), Alzheimer's / dementia (population), clinical / translational (subfield)
Methods: Statistics, Connectivity
Keywords: Human kallikrein 6, Alzheimer’s disease, Dementia biomarkers, Amyloid beta 1–42, Total tau, Phosphorylated tau
Topic: Coagulation, Bradykinin, Polyphosphates, and Angioedema (Genetics, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 56 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repositories

Its files are read in the Code ↔ Paper reader above, with 2 matches between paragraphs and lines of code.

miyohtnk/PhD_KLK6_data_analysis

License: MIT
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: eb12641861466c13c8629529eb7f29084b7fbbe8, 26 September 2025
Languages: Jupyter (1)
Size: 5 files, 1 script
Software Heritage: not archived
Found in: the text, “Statistical analysis”
Holds: README, license file, 1 notebook
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), NumPy (1 file), pandas (1 file), Pingouin (1 file), scikit-learn (1 file), SciPy (1 file), seaborn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
1 file

miyohtnk/phd

License: none: the authors keep all their rights
State: the link is dead, verified on 30 September 2026
Evidence: found in the paper
Software Heritage: not archived
Found in: “Data availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 30 September 2026: the link is dead
  • 30 September 2026: the link is dead

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 1 script, each with its path and the digest of its content;
  • 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1186/s12014-025-09577-x.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 6 keywords, 50 references.

Cite

This paper

Chatanaka, M. K., Soosaipillai, A., Cano, A., Orellana, A., Boada, M., Prassas, I., Morató, X., & Diamandis, E. P. (2026). Validation of human kallikrein 6 in the cerebrospinal fluid of patients with progressive and non-progressive alzheimer's disease: correlation with other biomarkers. Clinical proteomics, 23(1), 23. https://doi.org/10.1186/s12014-025-09577-x

BibTeX

@article{chatanaka2026validation,
author = {Chatanaka, Miyo K and Soosaipillai, Antoninus and Cano, Amanda and Orellana, Adelina and Boada, Mercè and Prassas, Ioannis and Morató, Xavier and Diamandis, Eleftherios P},
title = {{Validation of human kallikrein 6 in the cerebrospinal fluid of patients with progressive and non-progressive alzheimer's disease: correlation with other biomarkers}},
journal = {Clinical proteomics},
year = {2026},
month = mar,
volume = {23},
number = {1},
pages = {23},
publisher = {BMC},
issn = {1542-6416},
doi = {10.1186/s12014-025-09577-x},
url = {https://doi.org/10.1186/s12014-025-09577-x},
pmid = {41834054},
pmcid = {PMC13104293}
}

RIS

TY - JOUR
AU - Chatanaka, Miyo K
AU - Soosaipillai, Antoninus
AU - Cano, Amanda
AU - Orellana, Adelina
AU - Boada, Mercè
AU - Prassas, Ioannis
AU - Morató, Xavier
AU - Diamandis, Eleftherios P
TI - Validation of human kallikrein 6 in the cerebrospinal fluid of patients with progressive and non-progressive alzheimer's disease: correlation with other biomarkers
T2 - Clinical proteomics
J2 - Clin Proteomics
PY - 2026
DA - 2026/03/15
VL - 23
IS - 1
SP - 23
SN - 1542-6416
PB - BMC
DO - 10.1186/s12014-025-09577-x
UR - https://doi.org/10.1186/s12014-025-09577-x
LA - en
ER -

CSL-JSON

{
"id": "10.1186/s12014-025-09577-x",
"type": "article-journal",
"title": "Validation of human kallikrein 6 in the cerebrospinal fluid of patients with progressive and non-progressive alzheimer's disease: correlation with other biomarkers",
"container-title": "Clinical proteomics",
"author": [
{
"family": "Chatanaka",
"given": "Miyo K"
},
{
"family": "Soosaipillai",
"given": "Antoninus"
},
{
"family": "Cano",
"given": "Amanda"
},
{
"family": "Orellana",
"given": "Adelina"
},
{
"family": "Boada",
"given": "Mercè"
},
{
"family": "Prassas",
"given": "Ioannis"
},
{
"family": "Morató",
"given": "Xavier"
},
{
"family": "Diamandis",
"given": "Eleftherios P"
}
],
"container-title-short": "Clin Proteomics",
"volume": "23",
"issue": "1",
"page": "23",
"DOI": "10.1186/s12014-025-09577-x",
"PMID": "41834054",
"PMCID": "PMC13104293",
"ISSN": "1542-6416",
"publisher": "BMC",
"URL": "https://doi.org/10.1186/s12014-025-09577-x",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
15
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41398-026-04081-8 [code]
Functional system-specific brain aging across the Alzheimer's disease continuum.
Journal: Translational psychiatry
In common: Pingouin, statsmodels, seaborn, 5 other tools, Alzheimer's / dementia, clinical / translational, 1 reference
[2] doi:10.1371/journal.pone.0343722 [code]
Comprehensive methodology for sample enrichment in EEG biomarker studies for Alzheimer's risk classification.
Journal: PloS one
In common: Pingouin, statsmodels, seaborn, 5 other tools, Alzheimer's / dementia, clinical / translational
[3] doi:10.64898/2026.05.06.26352540 [code]
Generating synthetic tau-PET scans in Alzheimer’s disease from MRI, blood biomarkers and demographics with deep learning
Journal: medRxiv (preprint)
In common: seaborn, scikit-learn, pandas, 3 other tools, Alzheimer's / dementia, clinical / translational, 2 references
[4] doi:10.1002/alz.71649 [code]
Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: Pingouin, statsmodels, seaborn, 5 other tools, Alzheimer's / dementia
[5] doi:10.1002/alz.71365 [code]
Benchmarking speech biomarkers of Alzheimer's against cognitive and neural measures.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: Pingouin, statsmodels, seaborn, 5 other tools, Alzheimer's / dementia
[6] doi:10.34133/csbj.0042 [code]
Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &lt;i&gt;CRB1&lt;/i&gt;: Implications for Clinical Trials.
Journal: Computational and structural biotechnology journal
In common: Pingouin, statsmodels, seaborn, 5 other tools, clinical / translational
[7] doi:10.1038/s43587-026-01096-0 [code]
Neuronal APOE4-induced early hippocampal network hyperexcitability in Alzheimer's disease pathogenesis.
Journal: Nature aging
In common: Pingouin, statsmodels, seaborn, 5 other tools, Alzheimer's / dementia
[8] doi:10.1162/imag.a.1259 [code]
Neuronal avalanches as a predictive biomarker for guiding tailored BCI training programs.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Pingouin, statsmodels, seaborn, 5 other tools, clinical / translational
[9] doi:10.1038/s41531-026-01380-1 [code]
Identifying maximal beta power from directional subthalamic local field potentials in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: Pingouin, statsmodels, seaborn, 5 other tools, clinical / translational
[10] doi:10.7554/elife.108673 [code]
Adaptive behavior is guided by integrated representations of controlled and non-controlled information.
Journal: eLife
In common: Pingouin, statsmodels, seaborn, 5 other tools

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.