OSCR

Deep learning-based neuroanatomical profiling reveals population-specific brain changes in multiple sclerosis: a large-scale Middle Eastern study.

Code ↔ Paper

23 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 23 matches · 2 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Materials and methods › Statistical analysis framework › Primary outcome measures › Ventricular area analysis ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 403–466 · score 0.95 · morphological head mask, polynomial regression, skull mask area, brain atrophy, brain area, FSL
  2. [2] § Materials and methods › Statistical analysis framework › Primary outcome measures › Total lesion area analysis ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 403–466 · score 0.87 · polynomial regression, Age related lesion, skull mask, age range, mask area, WMH area
  3. [3] § Materials and methods › Deep learning architectures › Architecture selection and implementation › Trans-U-Net ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/trans_unet_model.py, the whole file · a weak match · score 0.86 · multi head attention, transformer bottleneck, Positional encoding, CNN, networks, dropout
  4. [4] § Materials and methods › Deep learning architectures › Training protocol › Hybrid loss function strategy ↔ Phase3_model_training_and_inferencing_and_evaluation/for_GM/model_training_scripts/p1_pix2pix_var5.py, lines 622–675 · score 0.85 · unified focal loss, weighted cross entropy, weighted categorical, Class weights, refine, phase
  5. [5] § Materials and methods › Deep learning architectures › Training protocol › Hybrid loss function strategy ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/p4_variant_all_net.py, lines 500–553 · score 0.85 · unified focal loss, weighted cross entropy, weighted categorical, Class weights, refine, phase
  6. [6] § Materials and methods › Statistical analysis framework › Primary outcome measures › Total lesion area analysis ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 800–879 · score 0.84 · meaningful central tendency, Shapiro Wilk, Mann Whitney, regardless, skewed, exponential
  7. [7] § Materials and methods › Deep learning architectures › Architecture selection and implementation › DeepLabV3Plus ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/dlv3_unet_model.py, lines 12–55 · score 0.83 · atrous spatial pyramid, encoder decoder, ResNet, backbone, ASPP, convolutions
  8. [8] § Results › Ventricular area analysis ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 509–554 · score 0.81 · artificially inflating brain, age related brain, skull normalized ratios, mask area, ventricular ratio, denominator
  9. [9] § Results › Total lesion area analysis ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 509–554 · score 0.76 · artificially inflating brain, age related brain, skull normalized ratios, mask area, denominator, atrophy
  10. [10] § Materials and methods › Deep learning architectures › Training protocol › Training hyperparameters ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/p4_variant_all_net.py, lines 163–252 · score 0.71 · ReduceLROnPlateau, hyperparameters, patience, optimizer, Batch, Training
  11. [11] § Materials and methods › Deep learning architectures › Architecture selection and implementation › U-Net Architecture ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/dlv3_unet_model_GN.py, lines 12–54 · score 0.66 · ReLU, encoder decoder, channel, blocks, activation, convolutions
  12. [12] § Materials and methods › Statistical analysis framework › Primary outcome measures › Total lesion area analysis ↔ Phase4_data_processing/p4_brain_mri_extractor_excel.py, lines 147–228 · score 0.63 · skull mask, brain mask, WMH area, Ventricular area, summed, pixel
  13. [13] § Results › Ventricular area analysis ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 917–973 · score 0.62 · brain tissue mask, absolute ventricular area, ventricular ratios normalized, mask area, mm2, skull
  14. [14] § Materials and methods › Study design and population ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 4111–4175 · score 0.61 · McDonald, scanner, neurological, quality, confidentiality, diagnosis
  15. [15] § Results › Deep learning model performance ↔ Phase3_model_training_and_inferencing_and_evaluation/for_GM/model_training_scripts/p1_data_loader.py, lines 1–40 · score 0.60 · fold cross validation, white matter hyperintensity, deep learning, neuroanatomical, models, segmentation
  16. [16] § Materials and methods › Statistical analysis framework › Demographic analysis ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 800–879 · score 0.60 · Chi square, Gender distribution, variables, Demographic, female, HC
  17. [17] § Materials and methods › Deep learning architectures › Performance evaluation ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/p4_inference.py, lines 232–304 · score 0.59 · positive lesion, ground truth, union, connected, overlap, predicted
  18. [18] § Materials and methods › Deep learning architectures › Architecture selection and implementation › DeepLabV3Plus ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/dlv3_unet_model_GN.py, lines 56–99 · score 0.58 · atrous spatial pyramid, ASPP
  19. [19] § Materials and methods › Deep learning architectures › Architecture selection and implementation › U-Net Architecture ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/trans_unet_model.py, the whole file · a weak match · score 0.55 · transposed, network, dropout, ReLU, encoder, blocks
  20. [20] § Materials and methods › Statistical analysis framework › Advanced statistical methods ↔ Phase5_statistical_analysis/p4_excel_analysis_developed.py, lines 2365–2423 · score 0.54 · WMH ratio, medium, biserial, Wilk, Cohen, threshold
  21. [21] § Materials and methods › MRI datasets and ground truth › Ground truth annotation process ↔ Phase4_data_processing/p4_predict_new_data.py, lines 175–225 · score 0.53 · morphological post processing, Abnormal WMH, trained, classes, ventricles
  22. [22] § Materials and methods › Implementation details ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/p4_inference.py, lines 725–800 · score 0.52 · Post processing, radius, disk, inference, kernel, morphological
  23. [23] § Materials and methods › Deep learning architectures › Training protocol › Dataset composition ↔ Phase3_model_training_and_inferencing_and_evaluation/for_WMH_Vent/model_training_scripts/p4_data_loader.py, lines 812–890 · score 0.50 · stratified splitting, training patients, MSSEG, slices, model

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

Python · 4,176 lines · 196 KB · MIT · 9 matches

  1. """
  2. P4 Article - MS Brain MRI Statistical Analysis Framework
  3. ==========================================
  4. A comprehensive statistical analysis and visualization system for Multiple Sclerosis
  5. brain MRI studies, designed for publication in prestigious journals.
  6. This script processes real MRI analysis data and generates:
  7. - Demographic analysis and comparisons
  8. - Ventricular and lesion burden analysis
  9. - Publication-quality figures and tables
  10. - Statistical comparisons between groups
  11. Author: Research Team
  12. Developer:
  13. Mahdi Bashiri Bawil
  14. """
  15. import os
  16. import pandas as pd
  17. import numpy as np
  18. import matplotlib.pyplot as plt
  19. import seaborn as sns
  20. import scipy.stats as stats
  21. from scipy import stats
  22. from sklearn.preprocessing import StandardScaler
  23. from sklearn.linear_model import LinearRegression
  24. from sklearn.metrics import r2_score
  25. import warnings
  26. warnings.filterwarnings('ignore')
  27. # Set matplotlib parameters for publication quality
  28. plt.rcParams.update({
  29. 'font.size': 14,
  30. 'font.family': 'Arial',
  31. 'axes.linewidth': 1.2,
  32. 'axes.spines.top': False,
  33. 'axes.spines.right': False,
  34. 'figure.dpi': 300,
  35. 'savefig.dpi': 300,
  36. 'savefig.bbox': 'tight',
  37. 'savefig.transparent': False
  38. })
  39. class OutlierDetector:
  40. """
  41. Comprehensive outlier detection and filtering for MS brain MRI analysis
  42. """
  43. def __init__(self, config=None):
  44. self.config = config
  45. self.outlier_summary = {}
  46. self.cleaned_indices = None
  47. def detect_outliers_iqr(self, data, column, factor=1.5):
  48. """
  49. Detect outliers using Interquartile Range (IQR) method
  50. Parameters:
  51. - data: DataFrame
  52. - column: column name to check
  53. - factor: IQR multiplier (1.5 for mild, 3.0 for extreme outliers)
  54. """
  55. if column not in data.columns:
  56. return np.array([False] * len(data))
  57. series = data[column].dropna()
  58. Q1 = series.quantile(0.25)
  59. Q3 = series.quantile(0.75)
  60. IQR = Q3 - Q1
  61. lower_bound = Q1 - factor * IQR
  62. upper_bound = Q3 + factor * IQR
  63. outliers = (data[column] < lower_bound) | (data[column] > upper_bound)
  64. return outliers.fillna(False)
  65. def detect_outliers_zscore(self, data, column, threshold=3.0):
  66. """
  67. Detect outliers using Z-score method
  68. Parameters:
  69. - data: DataFrame
  70. - column: column name to check
  71. - threshold: Z-score threshold (typically 2.5-3.0)
  72. """
  73. if column not in data.columns:
  74. return np.array([False] * len(data))
  75. series = data[column].dropna()
  76. if len(series) == 0:
  77. return np.array([False] * len(data))
  78. z_scores = np.abs(stats.zscore(series, nan_policy='omit'))
  79. outliers = pd.Series([False] * len(data), index=data.index)
  80. outliers.loc[series.index] = z_scores > threshold
  81. return outliers.fillna(False)
  82. def detect_outliers_modified_zscore(self, data, column, threshold=3.5):
  83. """
  84. Detect outliers using Modified Z-score (more robust to outliers)
  85. Uses median absolute deviation (MAD) instead of standard deviation
  86. """
  87. if column not in data.columns:
  88. return np.array([False] * len(data))
  89. series = data[column].dropna()
  90. if len(series) == 0:
  91. return np.array([False] * len(data))
  92. median = series.median()
  93. mad = np.median(np.abs(series - median))
  94. # Avoid division by zero
  95. if mad == 0:
  96. mad = 1e-10
  97. modified_z_scores = 0.6745 * (series - median) / mad
  98. outliers = pd.Series([False] * len(data), index=data.index)
  99. outliers.loc[series.index] = np.abs(modified_z_scores) > threshold
  100. return outliers.fillna(False)
  101. def detect_multivariate_outliers(self, data, columns, contamination=0.1):
  102. """
  103. Detect multivariate outliers using Isolation Forest
  104. Parameters:
  105. - data: DataFrame
  106. - columns: list of columns to consider
  107. - contamination: expected proportion of outliers
  108. """
  109. try:
  110. from sklearn.ensemble import IsolationForest
  111. # Select only numeric columns that exist
  112. valid_columns = [col for col in columns if col in data.columns]
  113. if len(valid_columns) == 0:
  114. return np.array([False] * len(data))
  115. # Prepare data for isolation forest
  116. X = data[valid_columns].copy()
  117. # Handle missing values
  118. for col in valid_columns:
  119. X[col] = X[col].fillna(X[col].median())
  120. # Fit Isolation Forest
  121. iso_forest = IsolationForest(contamination=contamination, random_state=42)
  122. outlier_labels = iso_forest.fit_predict(X)
  123. # Convert to boolean array (True for outliers)
  124. return outlier_labels == -1
  125. except ImportError:
  126. print("Warning: sklearn not available for multivariate outlier detection")
  127. return np.array([False] * len(data))
  128. def comprehensive_outlier_detection(self, data, methods='all', age_stratified=True):
  129. """
  130. Comprehensive outlier detection using multiple methods
  131. Parameters:
  132. - data: DataFrame
  133. - methods: 'all', 'conservative', or list of specific methods
  134. - age_stratified: whether to perform outlier detection within age groups
  135. """
  136. print("\n" + "=" * 60)
  137. print("COMPREHENSIVE OUTLIER DETECTION")
  138. print("=" * 60)
  139. # Define columns to check for outliers
  140. outlier_columns = {
  141. 'continuous': [
  142. self.config.COLUMNS['age'],
  143. self.config.COLUMNS['total_skull'],
  144. self.config.COLUMNS['total_brain'],
  145. self.config.COLUMNS['total_ventricle'],
  146. self.config.COLUMNS['total_wmh'],
  147. 'VentricleRatio',
  148. 'VentricleRatio_Skull',
  149. 'WMHRatio_Skull',
  150. 'WMHRatio'
  151. ],
  152. 'wmh_subtypes': [
  153. self.config.COLUMNS.get('peri_wmh', 'TotalPeriArea'),
  154. self.config.COLUMNS.get('deep_wmh', 'TotalDeepArea'),
  155. self.config.COLUMNS.get('juxta_wmh', 'TotalJuxtArea'),
  156. 'peri_wmh_ratio',
  157. 'deep_wmh_ratio',
  158. 'juxta_wmh_ratio'
  159. ]
  160. }
  161. all_columns = outlier_columns['continuous'] + outlier_columns['wmh_subtypes']
  162. # Initialize outlier tracking
  163. outlier_flags = pd.DataFrame(index=data.index)
  164. outlier_summary = {}
  165. if age_stratified:
  166. # Detect outliers within each age group and study group
  167. print("Performing age-stratified outlier detection...")
  168. for group in ['HC', 'MS']:
  169. for age_group in data['AgeGroup'].cat.categories:
  170. if pd.isna(age_group):
  171. continue
  172. subset_mask = (data[self.config.COLUMNS['group']] == group) & \
  173. (data['AgeGroup'] == age_group)
  174. subset_data = data[subset_mask]
  175. if len(subset_data) < 5: # Skip if too few samples
  176. continue
  177. print(f" {group} - {age_group}: {len(subset_data)} patients")
  178. for column in all_columns:
  179. if column not in subset_data.columns:
  180. continue
  181. # Apply multiple detection methods
  182. col_name = f"{column}_{group}_{age_group}"
  183. # IQR method (conservative)
  184. iqr_outliers = self.detect_outliers_iqr(subset_data, column, factor=2.0)
  185. # Modified Z-score (more robust)
  186. mz_outliers = self.detect_outliers_modified_zscore(subset_data, column, threshold=3.5)
  187. # Combine methods (conservative approach)
  188. combined_outliers = iqr_outliers & mz_outliers
  189. if combined_outliers.sum() > 0:
  190. outlier_flags.loc[subset_data.index, col_name] = combined_outliers
  191. outlier_summary[col_name] = {
  192. 'count': combined_outliers.sum(),
  193. 'percentage': (combined_outliers.sum() / len(subset_data)) * 100,
  194. 'indices': subset_data.index[combined_outliers].tolist()
  195. }
  196. else:
  197. # Global outlier detection
  198. print("Performing global outlier detection...")
  199. for group in ['HC', 'MS']:
  200. group_data = data[data[self.config.COLUMNS['group']] == group]
  201. print(f" {group}: {len(group_data)} patients")
  202. for column in all_columns:
  203. if column not in group_data.columns:
  204. continue
  205. col_name = f"{column}_{group}"
  206. # Multiple detection methods
  207. iqr_outliers = self.detect_outliers_iqr(group_data, column, factor=1.5)
  208. mz_outliers = self.detect_outliers_modified_zscore(group_data, column, threshold=3.0)
  209. # Conservative combination (both methods must agree)
  210. combined_outliers = iqr_outliers & mz_outliers
  211. if combined_outliers.sum() > 0:
  212. outlier_flags.loc[group_data.index, col_name] = combined_outliers
  213. outlier_summary[col_name] = {
  214. 'count': combined_outliers.sum(),
  215. 'percentage': (combined_outliers.sum() / len(group_data)) * 100,
  216. 'indices': group_data.index[combined_outliers].tolist()
  217. }
  218. # Identify patients with multiple outlier flags
  219. outlier_flags = outlier_flags.fillna(False)
  220. outlier_counts = outlier_flags.sum(axis=1)
  221. # Define threshold for removing patients (e.g., outliers in 3+ variables)
  222. outlier_threshold = 3
  223. patients_to_remove = outlier_counts >= outlier_threshold
  224. print(f"\nOUTLIER DETECTION SUMMARY:")
  225. print(f"{'=' * 40}")
  226. print(f"Total outlier flags detected: {outlier_flags.sum().sum()}")
  227. print(f"Patients with {outlier_threshold}+ outlier flags: {patients_to_remove.sum()}")
  228. print(f"Percentage of data to be removed: {(patients_to_remove.sum() / len(data)) * 100:.2f}%")
  229. # Age group breakdown
  230. if 'AgeGroup' in data.columns:
  231. age_outlier_summary = data[patients_to_remove].groupby([self.config.COLUMNS['group'], 'AgeGroup']).size()
  232. print(f"\nOutliers by group and age:")
  233. for (group, age), count in age_outlier_summary.items():
  234. print(f" {group} - {age}: {count} patients")
  235. self.outlier_summary = {
  236. 'detailed': outlier_summary,
  237. 'patients_to_remove': data.index[patients_to_remove].tolist(),
  238. 'outlier_flags': outlier_flags,
  239. 'outlier_counts': outlier_counts
  240. }
  241. return patients_to_remove
  242. def visualize_outliers(self, data, outliers_mask, save_path=None):
  243. """
  244. Create visualizations showing detected outliers
  245. """
  246. fig, axes = plt.subplots(2, 3, figsize=(18, 12))
  247. fig.suptitle('Outlier Detection Visualization', fontsize=16, fontweight='bold')
  248. # Select key variables for visualization
  249. viz_columns = [
  250. ('VentricleRatio', 'Ventricular Ratio (%)'),
  251. ('WMHRatio', 'WMH Ratio (%)'),
  252. (self.config.COLUMNS['total_brain'], 'Total Brain Area'),
  253. (self.config.COLUMNS['total_ventricle'], 'Total Ventricle Area'),
  254. (self.config.COLUMNS['total_wmh'], 'Total WMH Area'),
  255. (self.config.COLUMNS['age'], 'Age (years)')
  256. ]
  257. for idx, (column, title) in enumerate(viz_columns):
  258. if column not in data.columns:
  259. continue
  260. row, col = idx // 3, idx % 3
  261. ax = axes[row, col]
  262. # Create boxplot with outliers highlighted
  263. for group in ['HC', 'MS']:
  264. group_data = data[data[self.config.COLUMNS['group']] == group]
  265. group_outliers = outliers_mask[group_data.index]
  266. # Normal data points
  267. normal_data = group_data.loc[~group_outliers, column].dropna()
  268. outlier_data = group_data.loc[group_outliers, column].dropna()
  269. # Plot boxplot
  270. bp = ax.boxplot([normal_data], positions=[0 if group == 'HC' else 1],
  271. widths=0.6, patch_artist=True,
  272. labels=[group])
  273. # Color boxes
  274. color = self.config.COLORS['hc'] if group == 'HC' else self.config.COLORS['ms']
  275. bp['boxes'][0].set_facecolor(color)
  276. bp['boxes'][0].set_alpha(0.7)
  277. # Highlight outliers
  278. if len(outlier_data) > 0:
  279. y_pos = 0 if group == 'HC' else 1
  280. ax.scatter([y_pos] * len(outlier_data), outlier_data,
  281. color='red', s=50, alpha=0.8, marker='x',
  282. label=f'{group} Outliers' if idx == 0 else "")
  283. ax.set_title(title, fontweight='bold')
  284. ax.set_xticklabels(['HC', 'MS'])
  285. ax.grid(True, alpha=0.3)
  286. if idx == 0:
  287. ax.legend()
  288. plt.tight_layout()
  289. if save_path:
  290. plt.savefig(save_path, dpi=300, bbox_inches='tight')
  291. return fig
  292. def generate_outlier_report(self, data, outliers_mask, save_path=None):
  293. """
  294. Generate a detailed outlier report
  295. """
  296. report_data = {
  297. 'PatientID': [],
  298. 'Group': [],
  299. 'AgeGroup': [],
  300. 'Gender': [],
  301. 'OutlierCount': [],
  302. 'OutlierVariables': []
  303. }
  304. outlier_patients = data[outliers_mask]
  305. for idx in outlier_patients.index:
  306. patient_flags = self.outlier_summary['outlier_flags'].loc[idx]
  307. outlier_vars = patient_flags[patient_flags == True].index.tolist()
  308. report_data['PatientID'].append(data.loc[idx, self.config.COLUMNS['patient_id']])
  309. report_data['Group'].append(data.loc[idx, self.config.COLUMNS['group']])
  310. report_data['AgeGroup'].append(data.loc[idx, 'AgeGroup'])
  311. report_data['Gender'].append(data.loc[idx, 'Gender'])
  312. report_data['OutlierCount'].append(len(outlier_vars))
  313. report_data['OutlierVariables'].append('; '.join(outlier_vars))
  314. report_df = pd.DataFrame(report_data)
  315. if save_path:
  316. report_df.to_csv(save_path, index=False)
  317. return report_df
  318. class MSAnalysisConfig:
  319. """Configuration class for MS analysis parameters"""
  320. # File paths
  321. # Define file paths
  322. header_dir = r"E:\MBashiri\ours_articles\Paper#Stats"
  323. header_dir = r"D:\TEMP_P4"
  324. DATA_PATH = os.path.join(header_dir, "brain_mri_analysis_results_ALL.csv")
  325. OUTPUT_DIR = os.path.join(header_dir, "csv_analysis_outputs_no_outlier_v4")
  326. os.makedirs(OUTPUT_DIR, exist_ok=True)
  327. # Age stratification
  328. AGE_BINS = [(18, 29), (30, 39), (40, 49), (50, 59), (60, 69)]
  329. AGE_LABELS = ['18-29', '30-39', '40-49', '50-59', '60+']
  330. # Color schemes for publication
  331. COLORS = {
  332. 'male': '#1f77b4', # Blue
  333. 'female': '#d62728', # Red
  334. 'hc': '#2ca02c', # Green
  335. 'ms': '#ff7f0e', # Orange
  336. 'pewmh': '#8B0000', # Dark Red
  337. 'dwmh': '#FF8C00', # Orange
  338. 'jcwmh': '#FFD700' # Gold
  339. }
  340. # Figure settings
  341. FIGURE_SIZE = (12, 8)
  342. DPI = 300
  343. # Statistical parameters
  344. ALPHA = 0.05
  345. CONFIDENCE_INTERVAL = 0.95
  346. # Column mappings
  347. COLUMNS = {
  348. 'patient_id': 'PatientID',
  349. 'age': 'PatientAge',
  350. 'sex': 'PatientSex', # 0: Male, 1: Female
  351. 'group': 'StudyGroup', # HC: Healthy Control, MS: Multiple Sclerosis
  352. 'total_brain': 'TotalBrainArea', # Brain-tissue mask area (FSL BET-derived)
  353. 'total_skull': 'TotalSkullArea', # Total head/skull mask area (morphological head mask)
  354. 'total_ventricle': 'TotalVentricleArea',
  355. 'total_wmh': 'TotalWMHArea',
  356. 'peri_wmh': 'TotalPeriArea',
  357. 'deep_wmh': 'TotalDeepArea',
  358. 'juxta_wmh': 'TotalJuxtArea'
  359. }
  360. # NOTE on denominators:
  361. # 'total_brain' = sum of BET brain binary mask pixels — represents visible brain
  362. # tissue area across slices. Primary normalisation denominator.
  363. # 'total_skull' = sum of morphological head binary mask pixels — represents the
  364. # total head cross-section (all tissues inside skull). Using this
  365. # denominator avoids inflating age-related lesion ratios via
  366. # brain atrophy, since the skull area is stable across age.
  367. # If 'TotalSkullArea' is absent from the CSV, skull-normalised columns are skipped gracefully.
  368. # Polynomial regression age limit for Figure 4 scatter plots (Reviewer 1, Comment 11).
  369. # Trend lines will be rendered only up to this age; all data points are still plotted.
  370. # Set to None to disable the limit and extend lines over the full age range.
  371. REGRESSION_AGE_LIMIT = 60 # years
  372. def add_outlier_detection_to_load_data(original_load_data_method):
  373. """
  374. Modified load_data method with integrated outlier detection
  375. """
  376. def load_data_with_outlier_detection(self, filepath=None):
  377. """Load and preprocess the MRI analysis data with outlier detection"""
  378. filepath = filepath or self.config.DATA_PATH
  379. try:
  380. # Load data using original method logic
  381. try:
  382. self.data = pd.read_csv(filepath)
  383. except:
  384. self.data = pd.read_csv(filepath, sep='\t')
  385. print(f"Data loaded successfully: {self.data.shape[0]} patients, {self.data.shape[1]} features")
  386. # Validate columns
  387. required_cols = list(self.config.COLUMNS.values())
  388. missing_cols = [col for col in required_cols if col not in self.data.columns]
  389. if missing_cols:
  390. print(f"Warning: Missing columns: {missing_cols}")
  391. # Create age groups
  392. self.data['AgeGroup'] = pd.cut(self.data[self.config.COLUMNS['age']],
  393. bins=[b[0] for b in self.config.AGE_BINS] + [self.config.AGE_BINS[-1][1]],
  394. labels=self.config.AGE_LABELS,
  395. include_lowest=True, right=False)
  396. # Create gender labels
  397. self.data['Gender'] = self.data[self.config.COLUMNS['sex']].map({0: 'Female', 1: 'Male'})
  398. # Define epsilon to prevent division by zero
  399. epsilon = 1e-10
  400. # ── DENOMINATOR 1: Brain-tissue mask area (FSL BET) ────────────────────────────────
  401. # Sum of binary brain mask pixels; used as the primary normalisation denominator
  402. # throughout the manuscript.
  403. denominator_brain = self.data[self.config.COLUMNS['total_brain']] + epsilon
  404. # denominator_brain = denominator_brain /100 # enable for more zooming
  405. # ── DENOMINATOR 2: Total skull / head mask area (Reviewer 2, Major Comments 4 & 5) ─
  406. # Sum of binary head-mask pixels (morphological background separation). Using this
  407. # as the denominator controls for age-related brain atrophy that could otherwise
  408. # artificially inflate brain-normalised ratios over time even without true lesion
  409. # accumulation. Absent column is handled gracefully below.
  410. skull_col = self.config.COLUMNS.get('total_skull', 'TotalSkullArea')
  411. _has_skull = skull_col in self.data.columns
  412. if _has_skull:
  413. denominator_skull = self.data[skull_col] + epsilon
  414. # denominator_skull = denominator_skull / 100 # enable for more zooming
  415. print(f" [Normalisation] Skull/head-mask column '{skull_col}' found — "
  416. f"skull-normalised ratios will be computed alongside brain-normalised ratios.")
  417. else:
  418. denominator_skull = None
  419. print(f" [Normalisation] Column '{skull_col}' NOT found in data — "
  420. f"skull-normalised ratios will be skipped (sensitivity analysis unavailable).")
  421. # ── Ventricle ratios ──────────────────────────────────────────────────────────────
  422. self.data['VentricleRatio'] = (self.data[self.config.COLUMNS['total_ventricle']] /
  423. denominator_brain * 100)
  424. if denominator_skull is not None:
  425. self.data['VentricleRatio_Skull'] = (self.data[self.config.COLUMNS['total_ventricle']] /
  426. denominator_skull * 100)
  427. # ── Total WMH ratios ──────────────────────────────────────────────────────────────
  428. self.data['WMHRatio'] = (self.data[self.config.COLUMNS['total_wmh']] /
  429. denominator_brain * 100)
  430. if denominator_skull is not None:
  431. self.data['WMHRatio_Skull'] = (self.data[self.config.COLUMNS['total_wmh']] /
  432. denominator_skull * 100)
  433. # ── WMH subtype proportional ratios (normalised by total WMH area) ───────────────
  434. # These express each subtype as a share of total lesion burden.
  435. denominator_tic_wmh = self.data[self.config.COLUMNS['total_wmh']] + epsilon
  436. for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']:
  437. self.data[f'{subtype}_ratio'] = (self.data[self.config.COLUMNS[subtype]] /
  438. denominator_tic_wmh * 100)
  439. # ── WMH subtype absolute ratios vs brain area (and vs skull area if available) ────
  440. for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']:
  441. self.data[f'{subtype}_ratio_brain'] = (self.data[self.config.COLUMNS[subtype]] /
  442. denominator_brain * 100)
  443. if denominator_skull is not None:
  444. self.data[f'{subtype}_ratio_skull'] = (self.data[self.config.COLUMNS[subtype]] /
  445. denominator_skull * 100)
  446. # ── Safety: replace any remaining NaN or Inf values with 0 ──────────────────────
  447. skull_ratio_cols = (
  448. ['VentricleRatio_Skull', 'WMHRatio_Skull'] +
  449. [f'{s}_ratio_skull' for s in ['peri_wmh', 'deep_wmh', 'juxta_wmh']]
  450. ) if denominator_skull is not None else []
  451. numeric_cols = (
  452. ['VentricleRatio', 'WMHRatio'] +
  453. [f'{subtype}_ratio' for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']] +
  454. [f'{subtype}_ratio_brain' for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']] +
  455. skull_ratio_cols
  456. )
  457. for col in numeric_cols:
  458. if col in self.data.columns:
  459. self.data[col] = self.data[col].replace([np.inf, -np.inf], np.nan).fillna(0)
  460. # === NEW: OUTLIER DETECTION ===
  461. print("\nPerforming outlier detection...")
  462. outlier_detector = OutlierDetector(self.config)
  463. # Detect outliers using comprehensive method
  464. outliers_mask = outlier_detector.comprehensive_outlier_detection(
  465. self.data,
  466. age_stratified=True # Detect outliers within age groups
  467. )
  468. # Create visualizations
  469. outlier_viz_path = os.path.join(self.config.OUTPUT_DIR, 'outlier_detection_visualization.png')
  470. outlier_detector.visualize_outliers(self.data, outliers_mask, outlier_viz_path)
  471. # Generate outlier report
  472. outlier_report_path = os.path.join(self.config.OUTPUT_DIR, 'outlier_report.csv')
  473. outlier_report = outlier_detector.generate_outlier_report(self.data, outliers_mask, outlier_report_path)
  474. # Store original data for reference
  475. self.data_with_outliers = self.data.copy()
  476. self.outlier_info = {
  477. 'detector': outlier_detector,
  478. 'outliers_mask': outliers_mask,
  479. 'outlier_report': outlier_report,
  480. 'removed_count': outliers_mask.sum(),
  481. 'original_count': len(self.data)
  482. }
  483. # Remove outliers from main dataset
  484. self.data = self.data[~outliers_mask].reset_index(drop=True)
  485. print(f"\nOUTLIER REMOVAL SUMMARY:")
  486. print(f"Original dataset: {self.outlier_info['original_count']} patients")
  487. print(f"Outliers removed: {self.outlier_info['removed_count']} patients")
  488. print(f"Final dataset: {len(self.data)} patients")
  489. print(f"Data retention: {(len(self.data) / self.outlier_info['original_count']) * 100:.1f}%")
  490. # Recreate age groups after outlier removal (in case categories changed)
  491. self.data['AgeGroup'] = pd.cut(self.data[self.config.COLUMNS['age']],
  492. bins=[b[0] for b in self.config.AGE_BINS] + [self.config.AGE_BINS[-1][1]],
  493. labels=self.config.AGE_LABELS,
  494. include_lowest=True, right=False)
  495. return self.data
  496. except Exception as e:
  497. print(f"Error loading data: {e}")
  498. return None
  499. return load_data_with_outlier_detection
  500. class MSStatisticalAnalysis:
  501. """Main class for MS statistical analysis and visualization"""
  502. def __init__(self, config=None):
  503. self.config = config or MSAnalysisConfig()
  504. self.data = None
  505. self.results = {}
  506. self.outlier_removal = False
  507. def load_data(self, filepath=None):
  508. """Load and preprocess the MRI analysis data"""
  509. filepath = filepath or self.config.DATA_PATH
  510. try:
  511. # Try to read as CSV first, then as tab-separated
  512. try:
  513. self.data = pd.read_csv(filepath)
  514. except:
  515. self.data = pd.read_csv(filepath, sep='\t')
  516. print(f"Data loaded successfully: {self.data.shape[0]} patients, {self.data.shape[1]} features")
  517. # Validate columns
  518. required_cols = list(self.config.COLUMNS.values())
  519. missing_cols = [col for col in required_cols if col not in self.data.columns]
  520. if missing_cols:
  521. print(f"Warning: Missing columns: {missing_cols}")
  522. # Create age groups
  523. self.data['AgeGroup'] = pd.cut(self.data[self.config.COLUMNS['age']],
  524. bins=[b[0] for b in self.config.AGE_BINS] + [self.config.AGE_BINS[-1][1]],
  525. labels=self.config.AGE_LABELS,
  526. include_lowest=True, right=False)
  527. # Create gender labels
  528. self.data['Gender'] = self.data[self.config.COLUMNS['sex']].map({0: 'Female', 1: 'Male'})
  529. # Define epsilon to prevent division by zero
  530. epsilon = 1e-10
  531. # ── DENOMINATOR 1: Brain-tissue mask area (FSL BET) ────────────────────────────────
  532. # Sum of binary brain mask pixels; used as the primary normalisation denominator
  533. # throughout the manuscript.
  534. denominator_brain = self.data[self.config.COLUMNS['total_brain']] + epsilon
  535. # denominator_brain = denominator_brain /100 # enable for more zooming
  536. # ── DENOMINATOR 2: Total skull / head mask area (Reviewer 2, Major Comments 4 & 5) ─
  537. # Sum of binary head-mask pixels (morphological background separation). Using this
  538. # as the denominator controls for age-related brain atrophy that could otherwise
  539. # artificially inflate brain-normalised ratios over time even without true lesion
  540. # accumulation. Absent column is handled gracefully below.
  541. skull_col = self.config.COLUMNS.get('total_skull', 'TotalSkullArea')
  542. _has_skull = skull_col in self.data.columns
  543. if _has_skull:
  544. denominator_skull = self.data[skull_col] + epsilon
  545. # denominator_skull = denominator_skull / 100 # enable for more zooming
  546. print(f" [Normalisation] Skull/head-mask column '{skull_col}' found — "
  547. f"skull-normalised ratios will be computed alongside brain-normalised ratios.")
  548. else:
  549. denominator_skull = None
  550. print(f" [Normalisation] Column '{skull_col}' NOT found in data — "
  551. f"skull-normalised ratios will be skipped (sensitivity analysis unavailable).")
  552. # ── Ventricle ratios ──────────────────────────────────────────────────────────────
  553. self.data['VentricleRatio'] = (self.data[self.config.COLUMNS['total_ventricle']] /
  554. denominator_brain * 100)
  555. if denominator_skull is not None:
  556. self.data['VentricleRatio_Skull'] = (self.data[self.config.COLUMNS['total_ventricle']] /
  557. denominator_skull * 100)
  558. # ── Total WMH ratios ──────────────────────────────────────────────────────────────
  559. self.data['WMHRatio'] = (self.data[self.config.COLUMNS['total_wmh']] /
  560. denominator_brain * 100)
  561. if denominator_skull is not None:
  562. self.data['WMHRatio_Skull'] = (self.data[self.config.COLUMNS['total_wmh']] /
  563. denominator_skull * 100)
  564. # ── WMH subtype proportional ratios (normalised by total WMH area) ───────────────
  565. # These express each subtype as a share of total lesion burden.
  566. denominator_tic_wmh = self.data[self.config.COLUMNS['total_wmh']] + epsilon
  567. for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']:
  568. self.data[f'{subtype}_ratio'] = (self.data[self.config.COLUMNS[subtype]] /
  569. denominator_tic_wmh * 100)
  570. # ── WMH subtype absolute ratios vs brain area (and vs skull area if available) ────
  571. for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']:
  572. self.data[f'{subtype}_ratio_brain'] = (self.data[self.config.COLUMNS[subtype]] /
  573. denominator_brain * 100)
  574. if denominator_skull is not None:
  575. self.data[f'{subtype}_ratio_skull'] = (self.data[self.config.COLUMNS[subtype]] /
  576. denominator_skull * 100)
  577. # ── Safety: replace any remaining NaN or Inf values with 0 ──────────────────────
  578. skull_ratio_cols = (
  579. ['VentricleRatio_Skull', 'WMHRatio_Skull'] +
  580. [f'{s}_ratio_skull' for s in ['peri_wmh', 'deep_wmh', 'juxta_wmh']]
  581. ) if denominator_skull is not None else []
  582. numeric_cols = (
  583. ['VentricleRatio', 'WMHRatio'] +
  584. [f'{subtype}_ratio' for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']] +
  585. [f'{subtype}_ratio_brain' for subtype in ['peri_wmh', 'deep_wmh', 'juxta_wmh']] +
  586. skull_ratio_cols
  587. )
  588. for col in numeric_cols:
  589. if col in self.data.columns:
  590. self.data[col] = self.data[col].replace([np.inf, -np.inf], np.nan).fillna(0)
  591. if self.outlier_removal:
  592. # === NEW: ADD THIS SECTION BEFORE THE FINAL RETURN ===
  593. print("\nPerforming outlier detection...")
  594. outlier_detector = OutlierDetector(self.config)
  595. # Detect outliers using comprehensive method
  596. outliers_mask = outlier_detector.comprehensive_outlier_detection(
  597. self.data,
  598. age_stratified=True # Detect outliers within age groups
  599. )
  600. # Create visualizations
  601. outlier_viz_path = os.path.join(self.config.OUTPUT_DIR, 'outlier_detection_visualization.png')
  602. outlier_detector.visualize_outliers(self.data, outliers_mask, outlier_viz_path)
  603. # Generate outlier report
  604. outlier_report_path = os.path.join(self.config.OUTPUT_DIR, 'outlier_report.csv')
  605. outlier_report = outlier_detector.generate_outlier_report(self.data, outliers_mask, outlier_report_path)
  606. # Store original data for reference
  607. self.data_with_outliers = self.data.copy()
  608. self.outlier_info = {
  609. 'detector': outlier_detector,
  610. 'outliers_mask': outliers_mask,
  611. 'outlier_report': outlier_report,
  612. 'removed_count': outliers_mask.sum(),
  613. 'original_count': len(self.data)
  614. }
  615. # Remove outliers from main dataset
  616. self.data = self.data[~outliers_mask].reset_index(drop=True)
  617. print(f"\nOUTLIER REMOVAL SUMMARY:")
  618. print(f"Original dataset: {self.outlier_info['original_count']} patients")
  619. print(f"Outliers removed: {self.outlier_info['removed_count']} patients")
  620. print(f"Final dataset: {len(self.data)} patients")
  621. print(f"Data retention: {(len(self.data) / self.outlier_info['original_count']) * 100:.1f}%")
  622. # Recreate age groups after outlier removal
  623. self.data['AgeGroup'] = pd.cut(self.data[self.config.COLUMNS['age']],
  624. bins=[b[0] for b in self.config.AGE_BINS] + [
  625. self.config.AGE_BINS[-1][1]],
  626. labels=self.config.AGE_LABELS,
  627. include_lowest=True, right=False)
  628. return self.data
  629. except Exception as e:
  630. print(f"Error loading data: {e}")
  631. return None
  632. def demographic_analysis(self):
  633. """Perform demographic analysis"""
  634. print("\n" + "=" * 60)
  635. print("DEMOGRAPHIC ANALYSIS")
  636. print("=" * 60)
  637. # Basic demographics
  638. demographic_summary = self.data.groupby([self.config.COLUMNS['group'], 'Gender']).agg({
  639. self.config.COLUMNS['age']: ['count', 'mean', 'std', 'min', 'max'],
  640. self.config.COLUMNS['patient_id']: 'count'
  641. }).round(2)
  642. print("\nDemographic Summary:")
  643. print(demographic_summary)
  644. # Age distribution by group and gender
  645. age_dist = pd.crosstab([self.data[self.config.COLUMNS['group']], self.data['AgeGroup']],
  646. self.data['Gender'])
  647. print("\nAge Distribution by Group and Gender:")
  648. print(age_dist)
  649. # Statistical tests for demographic differences
  650. hc_ages = self.data[self.data[self.config.COLUMNS['group']] == 'HC'][self.config.COLUMNS['age']]
  651. ms_ages = self.data[self.data[self.config.COLUMNS['group']] == 'MS'][self.config.COLUMNS['age']]
  652. hc_ages_male = self.data[(self.data['StudyGroup'] == 'HC') & (self.data['Gender'] == 'Male')][self.config.COLUMNS['age']]
  653. ms_ages_male = self.data[(self.data['StudyGroup'] == 'MS') & (self.data['Gender'] == 'Male')][self.config.COLUMNS['age']]
  654. hc_ages_female = self.data[(self.data['StudyGroup'] == 'HC') & (self.data['Gender'] == 'Female')][self.config.COLUMNS['age']]
  655. ms_ages_female = self.data[(self.data['StudyGroup'] == 'MS') & (self.data['Gender'] == 'Female')][self.config.COLUMNS['age']]
  656. # Age comparison between groups (Male)
  657. age_ttest = stats.ttest_ind(hc_ages_male, ms_ages_male)
  658. print(f"\nAge comparison (HC vs MS) (Male): t-statistic = {age_ttest.statistic:.3f}, p-value = {age_ttest.pvalue}")
  659. # Age comparison between groups (Male)
  660. age_ttest = stats.ttest_ind(hc_ages_female, ms_ages_female)
  661. print(f"Age comparison (HC vs MS) (Female): t-statistic = {age_ttest.statistic:.3f}, p-value = {age_ttest.pvalue}")
  662. # Age comparison between groups
  663. age_ttest = stats.ttest_ind(hc_ages, ms_ages)
  664. print(f"\nAge comparison (HC vs MS) (All): t-statistic = {age_ttest.statistic:.3f}, p-value = {age_ttest.pvalue}")
  665. # Gender distribution chi-square test
  666. gender_crosstab = pd.crosstab(self.data[self.config.COLUMNS['group']], self.data['Gender'])
  667. chi2, p_val, _, _ = stats.chi2_contingency(gender_crosstab)
  668. print(f"Gender distribution (HC vs MS): χ² = {chi2:.3f}, p-value = {p_val}")
  669. self.results['demographics'] = {
  670. 'summary': demographic_summary,
  671. 'age_distribution': age_dist,
  672. 'age_test': age_ttest,
  673. 'gender_test': (chi2, p_val)
  674. }
  675. return self.results['demographics']
  676. def _assess_normality_and_choose_test(self, data1, data2, variable_name):
  677. """
  678. Assess normality and choose appropriate statistical test.
  679. Decision logic:
  680. 1. Large sample (n >= 50 both groups) AND CV < 1 in both groups:
  681. → independent t-test. CLT applies and mean is a meaningful summary.
  682. 2. Small sample AND both groups pass Shapiro-Wilk AND |skew| < 2:
  683. → independent t-test. Genuinely normal small-n data.
  684. 3. All other cases:
  685. → Mann-Whitney U.
  686. CV >= 1 (SD >= mean) indicates an exponential-like distribution where the
  687. mean is not a meaningful central tendency, making the t-test interpretively
  688. inappropriate regardless of the CLT. This correctly selects Mann-Whitney
  689. for WMH variables and t-test for ventricular variables.
  690. """
  691. from scipy import stats
  692. import numpy as np
  693. LARGE_N_THRESHOLD = 50
  694. CV_THRESHOLD = 1.0 # SD/mean >= 1 → distribution too skewed for t-test
  695. data1_clean = data1.dropna()
  696. data2_clean = data2.dropna()
  697. if len(data1_clean) < 3 or len(data2_clean) < 3:
  698. return None, "insufficient_data", {}, None
  699. n1 = len(data1_clean)
  700. n2 = len(data2_clean)
  701. normality_info = {}
  702. shapiro_both_normal = False
  703. problematic_cv = False
  704. try:
  705. sw_data1 = data1_clean.sample(min(n1, 5000), random_state=42) if n1 > 5000 else data1_clean
  706. sw_data2 = data2_clean.sample(min(n2, 5000), random_state=42) if n2 > 5000 else data2_clean
  707. shapiro1 = stats.shapiro(sw_data1)
  708. shapiro2 = stats.shapiro(sw_data2)
  709. skew1 = stats.skew(data1_clean)
  710. skew2 = stats.skew(data2_clean)
  711. # CV: only meaningful when mean > 0
  712. mean1 = data1_clean.mean()
  713. mean2 = data2_clean.mean()
  714. std1 = data1_clean.std()
  715. std2 = data2_clean.std()
  716. cv1 = (std1 / mean1) if mean1 > 0 else float('inf')
  717. cv2 = (std2 / mean2) if mean2 > 0 else float('inf')
  718. # Distribution is problematic for t-test if CV >= 1 in either group
  719. problematic_cv = (cv1 >= CV_THRESHOLD or cv2 >= CV_THRESHOLD)
  720. shapiro_both_normal = (shapiro1.pvalue > 0.05 and shapiro2.pvalue > 0.05
  721. and abs(skew1) < 2 and abs(skew2) < 2)
  722. normality_info = {
  723. 'group1_n': n1,
  724. 'group2_n': n2,
  725. 'group1_shapiro_p': shapiro1.pvalue,
  726. 'group2_shapiro_p': shapiro2.pvalue,
  727. 'group1_skewness': skew1,
  728. 'group2_skewness': skew2,
  729. 'group1_kurtosis': stats.kurtosis(data1_clean),
  730. 'group2_kurtosis': stats.kurtosis(data2_clean),
  731. 'group1_cv': cv1,
  732. 'group2_cv': cv2,
  733. 'problematic_cv': problematic_cv,
  734. }
  735. except Exception:
  736. shapiro_both_normal = False
  737. problematic_cv = True # conservative fallback
  738. normality_info = {
  739. 'error': 'Could not assess normality',
  740. 'group1_n': n1, 'group2_n': n2
  741. }
  742. # ── Test selection ──────────────────────────────────────────────────────
  743. large_sample = (n1 >= LARGE_N_THRESHOLD and n2 >= LARGE_N_THRESHOLD)
  744. use_parametric = (large_sample and not problematic_cv) or \
  745. (not large_sample and shapiro_both_normal)
  746. if use_parametric:
  747. test_result = stats.ttest_ind(data1_clean, data2_clean)
  748. test_type = "parametric"
  749. pooled_std = np.sqrt(((n1 - 1) * data1_clean.var() +
  750. (n2 - 1) * data2_clean.var()) / (n1 + n2 - 2))
  751. effect_size = ((data2_clean.mean() - data1_clean.mean()) / pooled_std
  752. if pooled_std > 0 else 0.0)
  753. effect_size_type = "cohens_d"
  754. if large_sample:
  755. reason = (f"t-test: CLT applies (n1={n1}, n2={n2} >= {LARGE_N_THRESHOLD}) "
  756. f"and CV < {CV_THRESHOLD} in both groups "
  757. f"(CV1={cv1:.3f}, CV2={cv2:.3f}). "
  758. f"Mean is a meaningful summary; t-test is appropriate.")
  759. else:
  760. reason = "t-test: small-n, both groups pass normality criteria."
  761. else:
  762. test_result = stats.mannwhitneyu(data1_clean, data2_clean, alternative='two-sided')
  763. test_type = "non_parametric"
  764. p_clamped = max(test_result.pvalue, 1e-30)
  765. z_score = abs(stats.norm.ppf(p_clamped / 2))
  766. effect_size = z_score / np.sqrt(n1 + n2)
  767. effect_size_type = "rank_biserial_r"
  768. if large_sample and problematic_cv:
  769. reason = (f"Mann-Whitney U: large sample (n1={n1}, n2={n2}) but "
  770. f"CV >= {CV_THRESHOLD} (CV1={cv1:.3f}, CV2={cv2:.3f}). "
  771. f"SD >= mean indicates exponential-like distribution; "
  772. f"mean is not a meaningful summary regardless of CLT.")
  773. else:
  774. reason = (f"Mann-Whitney U: small sample (n1={n1}, n2={n2}) "
  775. f"with non-normal distribution.")
  776. normality_info['test_selection_reason'] = reason
  777. normality_info['large_sample_clt_applied'] = large_sample
  778. return test_result, test_type, normality_info, (effect_size, effect_size_type)
  779. def ventricular_burden_analysis(self):
  780. """Analyze ventricular burden with age and gender stratification.
  781. Produces three metrics per group:
  782. - Absolute ventricular area (mm²)
  783. - Ventricular ratio normalised by brain-tissue mask area (VentricleRatio, %)
  784. - Ventricular ratio normalised by total skull/head mask area (VentricleRatio_Skull, %)
  785. [only when TotalSkullArea column is present in the data]
  786. """
  787. print("\n" + "=" * 60)
  788. print("VENTRICULAR BURDEN ANALYSIS")
  789. print("=" * 60)
  790. # Layout: 2 groups × (2 or 3 metrics). Add third column for skull-normalised
  791. # sensitivity analysis if the column is available (Reviewer 2, Major Comments 4 & 5).
  792. _has_skull_ratio = 'VentricleRatio_Skull' in self.data.columns
  793. _n_metric_cols = 3 if _has_skull_ratio else 2
  794. fig, axes = plt.subplots(2, _n_metric_cols, figsize=(8 * _n_metric_cols, 12))
  795. if _n_metric_cols == 2:
  796. axes = np.array(axes) # ensure 2-D indexing works uniformly
  797. fig.patch.set_facecolor('white')
  798. groups = ['HC', 'MS']
  799. age_centers = [np.mean(age_range) for age_range in self.config.AGE_BINS]
  800. # Metrics to plot. A third column (skull-normalised ratio) is added if available,
  801. # providing the sensitivity analysis requested by Reviewer 2, Major Comments 4 & 5.
  802. _has_skull_ratio = 'VentricleRatio_Skull' in self.data.columns
  803. metrics = {
  804. 'area': {
  805. 'column': self.config.COLUMNS['total_ventricle'],
  806. 'ylabel': 'Ventricular Area (mm²)',
  807. 'title_suffix': 'Ventricular Area'
  808. },
  809. 'ratio': {
  810. 'column': 'VentricleRatio',
  811. 'ylabel': 'Ventricular Ratio — brain denom. (%)',
  812. 'title_suffix': 'Ventricular Ratio (brain-normalised)'
  813. }
  814. }
  815. if _has_skull_ratio:
  816. metrics['ratio_skull'] = {
  817. 'column': 'VentricleRatio_Skull',
  818. 'ylabel': 'Ventricular Ratio — skull denom. (%)',
  819. 'title_suffix': 'Ventricular Ratio (skull-normalised, sensitivity)'
  820. }
  821. else:
  822. print(" [Sensitivity] 'VentricleRatio_Skull' column absent — "
  823. "skull-normalised ventricular ratio panel will be omitted.")
  824. # Dictionary to store all table data
  825. table_data = {
  826. 'detailed_stats': {},
  827. 'plot_data': {},
  828. 'metadata': {
  829. 'age_bins': self.config.AGE_BINS,
  830. 'age_labels': self.config.AGE_LABELS,
  831. 'age_centers': age_centers,
  832. 'groups': groups,
  833. 'metrics': metrics,
  834. 'colors': self.config.COLORS
  835. }
  836. }
  837. # Process each group and metric combination
  838. for group_idx, group in enumerate(groups):
  839. group_data = self.data[self.data[self.config.COLUMNS['group']] == group]
  840. table_data['detailed_stats'][group] = {}
  841. table_data['plot_data'][group] = {}
  842. for metric_idx, (metric_name, metric_info) in enumerate(metrics.items()):
  843. ax = axes[group_idx, metric_idx]
  844. # Initialize storage for this group-metric combination
  845. table_data['detailed_stats'][group][metric_name] = {}
  846. table_data['plot_data'][group][metric_name] = {
  847. 'age_centers': age_centers.copy(),
  848. 'age_labels': self.config.AGE_LABELS.copy(),
  849. 'male_means': [],
  850. 'female_means': [],
  851. 'male_stds': [],
  852. 'female_stds': [],
  853. 'male_counts': [],
  854. 'female_counts': [],
  855. 'combined_means': [],
  856. 'male_contributions': [],
  857. 'female_contributions': []
  858. }
  859. # Process each age group
  860. for age_idx, age_label in enumerate(self.config.AGE_LABELS):
  861. age_group_data = group_data[group_data['AgeGroup'] == age_label]
  862. # Separate by gender
  863. male_data = age_group_data[age_group_data['Gender'] == 'Male'][metric_info['column']]
  864. female_data = age_group_data[age_group_data['Gender'] == 'Female'][metric_info['column']]
  865. # Calculate statistics for each gender
  866. male_stats = {
  867. 'count': len(male_data),
  868. 'mean': male_data.mean() if len(male_data) > 0 else np.nan,
  869. 'std': male_data.std() if len(male_data) > 0 else np.nan,
  870. 'min': male_data.min() if len(male_data) > 0 else np.nan,
  871. 'max': male_data.max() if len(male_data) > 0 else np.nan,
  872. 'median': male_data.median() if len(male_data) > 0 else np.nan
  873. }
  874. female_stats = {
  875. 'count': len(female_data),
  876. 'mean': female_data.mean() if len(female_data) > 0 else np.nan,
  877. 'std': female_data.std() if len(female_data) > 0 else np.nan,
  878. 'min': female_data.min() if len(female_data) > 0 else np.nan,
  879. 'max': female_data.max() if len(female_data) > 0 else np.nan,
  880. 'median': female_data.median() if len(female_data) > 0 else np.nan
  881. }
  882. # Store detailed statistics
  883. table_data['detailed_stats'][group][metric_name][age_label] = {
  884. 'Male': male_stats,
  885. 'Female': female_stats
  886. }
  887. # Calculate values for plotting (handle NaN values)
  888. male_mean = male_stats['mean'] if not np.isnan(male_stats['mean']) else 0
  889. female_mean = female_stats['mean'] if not np.isnan(female_stats['mean']) else 0
  890. male_std = male_stats['std'] if not np.isnan(male_stats['std']) else 0
  891. female_std = female_stats['std'] if not np.isnan(female_stats['std']) else 0
  892. # Calculate weighted combined mean and contributions
  893. total_male_sum = male_stats['count'] * male_mean if male_stats['count'] > 0 else 0
  894. total_female_sum = female_stats['count'] * female_mean if female_stats['count'] > 0 else 0
  895. total_subjects = male_stats['count'] + female_stats['count']
  896. if total_subjects > 0:
  897. # True weighted combined mean across both genders
  898. combined_mean = (total_male_sum + total_female_sum) / total_subjects
  899. # Calculate proportional contributions to the combined mean
  900. total_sum = total_male_sum + total_female_sum
  901. if total_sum > 0:
  902. male_contribution = (total_male_sum / total_sum) * combined_mean
  903. female_contribution = (total_female_sum / total_sum) * combined_mean
  904. else:
  905. male_contribution = 0
  906. female_contribution = 0
  907. else:
  908. combined_mean = 0
  909. male_contribution = 0
  910. female_contribution = 0
  911. # Store plot data
  912. plot_data = table_data['plot_data'][group][metric_name]
  913. plot_data['male_means'].append(male_mean)
  914. plot_data['female_means'].append(female_mean)
  915. plot_data['male_stds'].append(male_std)
  916. plot_data['female_stds'].append(female_std)
  917. plot_data['male_counts'].append(male_stats['count'])
  918. plot_data['female_counts'].append(female_stats['count'])
  919. plot_data['combined_means'].append(combined_mean)
  920. plot_data['male_contributions'].append(male_contribution)
  921. plot_data['female_contributions'].append(female_contribution)
  922. # Create the plot
  923. plot_data = table_data['plot_data'][group][metric_name]
  924. # Plot male contribution (bottom layer)
  925. ax.fill_between(age_centers, 0, plot_data['male_contributions'],
  926. color=self.config.COLORS['male'], alpha=0.7, label='Male')
  927. # Plot female contribution (top layer)
  928. ax.fill_between(age_centers, plot_data['male_contributions'],
  929. plot_data['combined_means'],
  930. color=self.config.COLORS['female'], alpha=0.7, label='Female')
  931. # Add error bars if desired (optional - uncomment if needed)
  932. # male_errors = [std/np.sqrt(count) if count > 0 else 0
  933. # for std, count in zip(plot_data['male_stds'], plot_data['male_counts'])]
  934. # female_errors = [std/np.sqrt(count) if count > 0 else 0
  935. # for std, count in zip(plot_data['female_stds'], plot_data['female_counts'])]
  936. # ax.errorbar(age_centers, plot_data['combined_means'],
  937. # yerr=combined_errors, fmt='none', color='black', alpha=0.5)
  938. # Formatting
  939. # Calculate panel letter (A, B, C, D)
  940. panel_idx = group_idx * 3 + metric_idx
  941. panel_letter = chr(65 + panel_idx) # 65 is ASCII for 'A'
  942. ax.set_title(f'{panel_letter}. {group} - {metric_info["title_suffix"]}', fontsize=20, fontweight='bold')
  943. # ax.set_title(f'{group} - {metric_info["title_suffix"]}', fontsize=14, fontweight='bold')
  944. ax.set_xlabel('Age (years)', fontsize=20)
  945. ax.set_ylabel(metric_info['ylabel'], fontsize=20)
  946. ax.legend(fontsize=20)
  947. ax.grid(True, alpha=0.3)
  948. ax.set_xticks(age_centers)
  949. ax.set_xticklabels(self.config.AGE_LABELS, fontsize=20)
  950. # Set reasonable y-axis limits
  951. max_value = max(plot_data['combined_means']) if plot_data['combined_means'] else 0
  952. if max_value > 0:
  953. ax.set_ylim(0, max_value * 1.1)
  954. plt.tight_layout()
  955. plt.savefig(os.path.join(config.OUTPUT_DIR, 'ventricular_burden_analysis.png'),
  956. dpi=self.config.DPI, bbox_inches='tight', facecolor='white')
  957. # Generate comprehensive documentation
  958. self._generate_analysis_documentation(table_data)
  959. # Generate and save tables
  960. self._generate_ventricular_tables(table_data)
  961. # Statistical analysis for both metrics
  962. print(f"\n{'=' * 50}")
  963. print("STATISTICAL COMPARISONS (HC vs MS)")
  964. print(f"{'=' * 50}")
  965. # Analysis for ventricular area
  966. hc_area = self.data[self.data[self.config.COLUMNS['group']] == 'HC'][self.config.COLUMNS['total_ventricle']]
  967. ms_area = self.data[self.data[self.config.COLUMNS['group']] == 'MS'][self.config.COLUMNS['total_ventricle']]
  968. if len(hc_area) > 0 and len(ms_area) > 0:
  969. # Use the new standardized testing approach
  970. area_test, test_type, normality_info, effect_size_info = self._assess_normality_and_choose_test(
  971. hc_area, ms_area, "Ventricular Area"
  972. )
  973. print(f"\nVentricular Area comparison:")
  974. print(f"HC: N={len(hc_area)}, mean ± SD = {hc_area.mean():.2f} ± {hc_area.std():.2f} mm²")
  975. print(f"MS: N={len(ms_area)}, mean ± SD = {ms_area.mean():.2f} ± {ms_area.std():.2f} mm²")
  976. # Print normality test results
  977. if 'group1_shapiro_p' in normality_info:
  978. print(
  979. f"Normality tests: HC p={normality_info['group1_shapiro_p']:.3f}, MS p={normality_info['group2_shapiro_p']:.3f}")
  980. if test_type == "parametric":
  981. print(f"Independent t-test: t-statistic = {area_test.statistic:.3f}, p-value = {area_test.pvalue}")
  982. print(f"Cohen's d = {effect_size_info[0]:.3f}")
  983. elif test_type == "non_parametric":
  984. print(f"Mann-Whitney U test: U-statistic = {area_test.statistic:.3f}, p-value = {area_test.pvalue}")
  985. print(f"Effect size (r) = {effect_size_info[0]:.3f}")
  986. # Also report medians for non-parametric
  987. print(
  988. f"HC: median [IQR] = {hc_area.median():.2f} [{hc_area.quantile(0.25):.2f}-{hc_area.quantile(0.75):.2f}] mm²")
  989. print(
  990. f"MS: median [IQR] = {ms_area.median():.2f} [{ms_area.quantile(0.25):.2f}-{ms_area.quantile(0.75):.2f}] mm²")
  991. else:
  992. print("Insufficient data for ventricular area comparison")
  993. area_test = None
  994. test_type = None
  995. normality_info = {}
  996. effect_size_info = (None, None)
  997. # Analysis for ventricular ratio
  998. hc_ratio = self.data[self.data[self.config.COLUMNS['group']] == 'HC']['VentricleRatio']
  999. ms_ratio = self.data[self.data[self.config.COLUMNS['group']] == 'MS']['VentricleRatio']
  1000. if len(hc_ratio) > 0 and len(ms_ratio) > 0:
  1001. # Use the new standardized testing approach
  1002. ratio_test, ratio_test_type, ratio_normality_info, ratio_effect_size_info = self._assess_normality_and_choose_test(
  1003. hc_ratio, ms_ratio, "Ventricular Ratio"
  1004. )
  1005. print(f"\nVentricular Ratio comparison:")
  1006. print(f"HC: N={len(hc_ratio)}, mean ± SD = {hc_ratio.mean():.2f} ± {hc_ratio.std():.2f}%")
  1007. print(f"MS: N={len(ms_ratio)}, mean ± SD = {ms_ratio.mean():.2f} ± {ms_ratio.std():.2f}%")
  1008. # Print normality test results
  1009. if 'group1_shapiro_p' in ratio_normality_info:
  1010. print(
  1011. f"Normality tests: HC p={ratio_normality_info['group1_shapiro_p']:.3f}, MS p={ratio_normality_info['group2_shapiro_p']:.3f}")
  1012. if ratio_test_type == "parametric":
  1013. print(
  1014. f"Independent t-test: t-statistic = {ratio_test.statistic:.3f}, p-value = {ratio_test.pvalue}")
  1015. print(f"Cohen's d = {ratio_effect_size_info[0]:.3f}")
  1016. elif ratio_test_type == "non_parametric":
  1017. print(
  1018. f"Mann-Whitney U test: U-statistic = {ratio_test.statistic:.3f}, p-value = {ratio_test.pvalue}")
  1019. print(f"Effect size (r) = {ratio_effect_size_info[0]:.3f}")
  1020. # Also report medians for non-parametric
  1021. print(
  1022. f"HC: median [IQR] = {hc_ratio.median():.2f} [{hc_ratio.quantile(0.25):.2f}-{hc_ratio.quantile(0.75):.2f}]%")
  1023. print(
  1024. f"MS: median [IQR] = {ms_ratio.median():.2f} [{ms_ratio.quantile(0.25):.2f}-{ms_ratio.quantile(0.75):.2f}]%")
  1025. else:
  1026. print("Insufficient data for ventricular ratio comparison")
  1027. ratio_test = None
  1028. ratio_test_type = None
  1029. ratio_normality_info = {}
  1030. ratio_effect_size_info = (None, None)
  1031. # Store results (update the existing results storage)
  1032. self.results['ventricular_burden'] = {
  1033. 'area_analysis': {
  1034. 'hc_stats': (hc_area.mean(), hc_area.std()) if len(hc_area) > 0 else (np.nan, np.nan),
  1035. 'ms_stats': (ms_area.mean(), ms_area.std()) if len(ms_area) > 0 else (np.nan, np.nan),
  1036. 'comparison': area_test,
  1037. 'test_type': test_type,
  1038. 'normality_info': normality_info,
  1039. 'effect_size': effect_size_info
  1040. },
  1041. 'ratio_analysis': {
  1042. 'hc_stats': (hc_ratio.mean(), hc_ratio.std()) if len(hc_ratio) > 0 else (np.nan, np.nan),
  1043. 'ms_stats': (ms_ratio.mean(), ms_ratio.std()) if len(ms_ratio) > 0 else (np.nan, np.nan),
  1044. 'comparison': ratio_test,
  1045. 'test_type': ratio_test_type,
  1046. 'normality_info': ratio_normality_info,
  1047. 'effect_size': ratio_effect_size_info
  1048. },
  1049. 'table_data': table_data
  1050. }
  1051. return self.results['ventricular_burden']
  1052. def _generate_analysis_documentation(self, table_data):
  1053. """Generate comprehensive documentation explaining the figure and analysis"""
  1054. from datetime import datetime
  1055. # Create comprehensive documentation
  1056. doc_content = f"""
  1057. VENTRICULAR BURDEN ANALYSIS - COMPREHENSIVE DOCUMENTATION
  1058. =========================================================
  1059. Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  1060. OVERVIEW
  1061. --------
  1062. This analysis examines ventricular burden in the brain comparing Healthy Controls (HC)
  1063. and Multiple Sclerosis (MS) patients. The analysis includes both absolute ventricular
  1064. area measurements and normalized ventricular ratios, stratified by age groups and gender.
  1065. FIGURE DESCRIPTION
  1066. ------------------
  1067. The figure consists of a 2x2 subplot layout:
  1068. Layout Structure:
  1069. - Top row: Healthy Controls (HC)
  1070. - Bottom row: Multiple Sclerosis (MS) patients
  1071. - Left column: Absolute Ventricular Area (mm²)
  1072. - Right column: Ventricular Ratio (%)
  1073. Subplot Details:
  1074. 1. Top-Left: HC Ventricular Area
  1075. 2. Top-Right: HC Ventricular Ratio
  1076. 3. Bottom-Left: MS Ventricular Area
  1077. 4. Bottom-Right: MS Ventricular Ratio
  1078. VISUALIZATION METHOD
  1079. --------------------
  1080. Chart Type: Stacked Area Plot with Gender Contributions
  1081. - Each subplot uses stacked area charts to show gender-stratified data across age groups
  1082. - Male contribution (bottom layer): Shows proportional contribution of male subjects to combined mean
  1083. - Female contribution (top layer): Shows proportional contribution of female subjects to combined mean
  1084. - Total height represents the true weighted combined mean across both genders
  1085. - This visualization allows comparison of both absolute values and relative gender contributions
  1086. Mathematical Approach:
  1087. - Male Contribution = (Male_Count × Male_Mean) / Total_Sum × Combined_Mean
  1088. - Female Contribution = (Female_Count × Female_Mean) / Total_Sum × Combined_Mean
  1089. - Combined Mean = (Male_Count × Male_Mean + Female_Count × Female_Mean) / (Male_Count + Female_Count)
  1090. Color Scheme:
  1091. - Male data: {table_data['metadata']['colors'].get('male', 'Blue')} (alpha=0.7)
  1092. - Female data: {table_data['metadata']['colors'].get('female', 'Red')} (alpha=0.7)
  1093. AGE STRATIFICATION
  1094. ------------------
  1095. Age Groups: {', '.join(table_data['metadata']['age_labels'])}
  1096. Age Bins: {table_data['metadata']['age_bins']}
  1097. Age Centers (for plotting): {[f'{center:.1f}' for center in table_data['metadata']['age_centers']]}
  1098. The analysis stratifies data across these age groups to examine age-related changes
  1099. in ventricular burden for both groups and genders.
  1100. METRICS ANALYZED
  1101. ----------------
  1102. 1. Ventricular Area (mm²):
  1103. - Absolute measurement of total ventricular volume
  1104. - Column: {table_data['metadata']['metrics']['area']['column']}
  1105. - Units: Square millimeters (mm²)
  1106. - Clinical significance: Larger values indicate greater ventricular enlargement
  1107. 2. Ventricular Ratio (%):
  1108. - Normalized measurement relative to total brain area/volume
  1109. - Column: VentricleRatio
  1110. - Units: Percentage (%)
  1111. - Clinical significance: Controls for individual brain size differences
  1112. ADVANCED STATISTICAL APPROACH
  1113. ------------------------------
  1114. The analysis employs a sophisticated statistical testing framework:
  1115. Normality Assessment:
  1116. - Shapiro-Wilk tests performed on both groups (HC and MS) for each metric
  1117. - Significance threshold: p < 0.05 indicates non-normal distribution
  1118. - Results inform choice between parametric and non-parametric tests
  1119. Statistical Test Selection:
  1120. 1. If both groups pass normality: Independent t-test (parametric)
  1121. 2. If one or both groups fail normality: Mann-Whitney U test (non-parametric)
  1122. Effect Size Calculations:
  1123. - Parametric tests: Cohen's d
  1124. * Small effect: d = 0.2
  1125. * Medium effect: d = 0.5
  1126. * Large effect: d = 0.8
  1127. - Non-parametric tests: Rank-biserial correlation (r ≈ Z/√N, large-sample approximation)
  1128. Note: termed "Effect size r" in earlier versions; corrected per Reviewer 2, Minor Comment 4.
  1129. * Small effect: r = 0.1
  1130. * Medium effect: r = 0.3
  1131. * Large effect: r = 0.5
  1132. For each combination of:
  1133. - Group (HC vs MS)
  1134. - Age group ({len(table_data['metadata']['age_labels'])} categories)
  1135. - Gender (Male vs Female)
  1136. - Metric (Area vs Ratio)
  1137. The following statistics are calculated:
  1138. - Sample size (N)
  1139. - Mean ± Standard Deviation
  1140. - Minimum and Maximum values
  1141. - Median values
  1142. - Interquartile ranges (for non-parametric reporting)
  1143. STATISTICAL OUTPUT INTERPRETATION
  1144. ---------------------------------
  1145. Parametric Results (t-test):
  1146. - Reports: Mean ± SD for both groups
  1147. - Test statistic: t-value and degrees of freedom
  1148. - p-value for significance testing
  1149. - Cohen's d for effect size magnitude
  1150. Non-parametric Results (Mann-Whitney U):
  1151. - Reports: Mean ± SD AND Median [IQR] for both groups
  1152. - Test statistic: U-value (or equivalent Z-score)
  1153. - p-value for significance testing
  1154. - Rank-biserial correlation (r) for effect magnitude (Mann-Whitney U; r ≈ Z/√N)
  1155. Normality Test Results:
  1156. - Shapiro-Wilk p-values reported for each group
  1157. - p < 0.05 indicates significant departure from normality
  1158. - Informs test selection rationale
  1159. INTERPRETATION GUIDELINES
  1160. -------------------------
  1161. Stacked Area Plot Interpretation:
  1162. - Total height = True combined mean (weighted by sample sizes)
  1163. - Male layer height = Proportional contribution of males to combined mean
  1164. - Female layer height = Proportional contribution of females to combined mean
  1165. - Layer proportions reflect both mean values AND sample size contributions
  1166. - Steeper slopes indicate rapid changes with age
  1167. - Wider differences between groups suggest clinical significance
  1168. Clinical Relevance:
  1169. - Ventricular enlargement is associated with brain atrophy
  1170. - MS patients typically show greater ventricular burden than healthy controls
  1171. - Age-related changes may differ between groups
  1172. - Gender differences may exist in disease progression patterns
  1173. Expected Patterns:
  1174. - MS group likely shows higher values than HC group
  1175. - Age-related increase in ventricular burden
  1176. - Potential gender differences in progression patterns
  1177. Statistical Significance Levels:
  1178. - p < 0.001: Highly significant (strong evidence)
  1179. - p < 0.01: Very significant (moderate to strong evidence)
  1180. - p < 0.05: Significant (sufficient evidence)
  1181. - p ≥ 0.05: Non-significant (insufficient evidence)
  1182. Effect Size Interpretation:
  1183. - Cohen's d or r values indicate practical significance
  1184. - Large effect sizes may be clinically meaningful even if p > 0.05
  1185. - Small p-values with small effect sizes may lack clinical relevance
  1186. DATA QUALITY NOTES
  1187. -------------------
  1188. - Zero values in plots indicate no subjects in that age/gender combination
  1189. - Small sample sizes may lead to unstable mean estimates and reduced statistical power
  1190. - Standard deviations provide insight into data variability within groups
  1191. - Missing data handled by excluding from calculations (listwise deletion)
  1192. - Normality violations automatically trigger non-parametric alternatives
  1193. STATISTICAL TESTING DETAILS
  1194. ----------------------------
  1195. Overall group comparisons (HC vs MS) performed separately for each metric:
  1196. 1. Ventricular Area Analysis:
  1197. - Automatic normality assessment using Shapiro-Wilk test
  1198. - Test selection based on normality results
  1199. - Effect size calculation appropriate to test type
  1200. - Comprehensive reporting of descriptive statistics
  1201. 2. Ventricular Ratio Analysis:
  1202. - Independent statistical analysis from area measurements
  1203. - Same rigorous normality assessment and test selection
  1204. - Separate effect size calculations
  1205. - Controls for multiple testing considerations
  1206. Test Assumptions:
  1207. - Parametric tests: Normality, independence, homogeneity of variance
  1208. - Non-parametric tests: Independence, similar distributions
  1209. - Both assume random sampling from populations of interest
  1210. OUTPUT FILES GENERATED
  1211. -----------------------
  1212. 1. Figure: ventricular_burden_analysis.png
  1213. - 2x2 subplot layout with stacked area plots showing proportional contributions
  1214. - High resolution (DPI: {getattr(self.config, 'DPI', 300)})
  1215. - White background for publication quality
  1216. 2. Enhanced Statistical Tables (CSV):
  1217. - ventricular_area_detailed_stats.csv (includes all descriptive statistics)
  1218. - ventricular_ratio_detailed_stats.csv (includes all descriptive statistics)
  1219. - ventricular_statistical_comparisons.csv (NEW: comprehensive test results)
  1220. 3. Plot Data Tables (CSV):
  1221. - ventricular_area_plot_data.csv (means and sample sizes)
  1222. - ventricular_ratio_plot_data.csv (means and sample sizes)
  1223. 4. Contribution Analysis (CSV):
  1224. - ventricular_area_contributions.csv (NEW: proportional contributions)
  1225. - ventricular_ratio_contributions.csv (NEW: proportional contributions)
  1226. 5. Statistical Results Summary (CSV):
  1227. - ventricular_normality_results.csv (NEW: normality test outcomes)
  1228. - ventricular_effect_sizes.csv (NEW: effect size calculations)
  1229. 6. This Documentation:
  1230. - ventricular_analysis_documentation.txt
  1231. TECHNICAL DETAILS
  1232. -----------------
  1233. Figure Specifications:
  1234. - Size: 16" x 12" (width x height)
  1235. - DPI: {getattr(self.config, 'DPI', 300)}
  1236. - Background: White
  1237. - Font sizes: Title=14pt (bold), Axis labels=12pt
  1238. - Grid: Enabled with 30% transparency
  1239. - Legend: Enabled for each subplot
  1240. Statistical Libraries:
  1241. - scipy.stats: Shapiro-Wilk, t-test, Mann-Whitney U
  1242. - numpy: Mathematical operations and statistical functions
  1243. - pandas: Data manipulation and summary statistics
  1244. Quality Control:
  1245. - Automatic handling of missing values
  1246. - Robust error handling for edge cases
  1247. - Comprehensive logging of statistical decisions
  1248. - Validation of statistical assumptions
  1249. ENHANCED LIMITATIONS AND CONSIDERATIONS
  1250. ---------------------------------------
  1251. 1. Sample Size Variations:
  1252. - Some age/gender combinations may have small sample sizes
  1253. - Unbalanced groups may affect statistical power
  1254. - Power analysis recommended for study design validation
  1255. 2. Multiple Comparisons:
  1256. - Two separate statistical tests performed (area and ratio)
  1257. - Consider Bonferroni correction: α = 0.05/2 = 0.025
  1258. - Family-wise error rate may be inflated without correction
  1259. 3. Age Grouping Effects:
  1260. - Discretized age groups may mask continuous age effects
  1261. - Loss of information compared to regression approaches
  1262. - Age bin boundaries are predetermined and may not reflect natural breakpoints
  1263. 4. Statistical Assumptions:
  1264. - Automatic normality testing with Shapiro-Wilk (sensitive to large samples)
  1265. - Independence assumption may be violated in related subjects
  1266. - Equal variances assumed for t-tests (consider Welch's t-test alternative)
  1267. 5. Visualization Limitations:
  1268. - Proportional contributions may be difficult to interpret intuitively
  1269. - Stacked areas emphasize combined effects over individual gender patterns
  1270. - Direct visual comparison between groups requires careful interpretation
  1271. 6. Clinical Interpretation:
  1272. - Statistical significance may not equal clinical significance
  1273. - Effect sizes should be considered alongside p-values
  1274. - Longitudinal changes not captured in cross-sectional analysis
  1275. RECOMMENDED FOLLOW-UP ANALYSES
  1276. ------------------------------
  1277. 1. Advanced Statistical Approaches:
  1278. - Age as continuous variable (linear/polynomial regression)
  1279. - Two-way ANOVA with interaction terms (group × gender × age)
  1280. - Mixed-effects models for correlated data
  1281. - Bootstrap confidence intervals for robust inference
  1282. 2. Multiple Comparisons Corrections:
  1283. - Bonferroni correction for family-wise error control
  1284. - False Discovery Rate (FDR) control for exploratory analyses
  1285. - Planned comparisons vs. post-hoc testing strategies
  1286. 3. Effect Size and Power Analysis:
  1287. - Post-hoc power calculations for observed effects
  1288. - Sample size calculations for future studies
  1289. - Confidence intervals around effect size estimates
  1290. 4. Alternative Statistical Approaches:
  1291. - Bayesian analysis for probabilistic interpretation
  1292. - Permutation tests for distribution-free inference
  1293. - Robust statistical methods for outlier resistance
  1294. 5. Clinical Validation:
  1295. - Correlation with clinical severity measures
  1296. - Longitudinal tracking of ventricular changes
  1297. - Predictive modeling for disease progression
  1298. QUALITY ASSURANCE CHECKLIST
  1299. ----------------------------
  1300. ✓ Normality testing performed automatically
  1301. ✓ Appropriate statistical test selected based on data properties
  1302. ✓ Effect sizes calculated and reported
  1303. ✓ Both parametric and non-parametric results available
  1304. ✓ Comprehensive descriptive statistics provided
  1305. ✓ Multiple output formats for different use cases
  1306. ✓ Documentation includes interpretation guidelines
  1307. ✓ Limitations and assumptions clearly stated
  1308. ✓ Recommendations for follow-up analyses provided
  1309. CONTACT AND METHODOLOGY
  1310. -----------------------
  1311. This analysis was generated using an automated pipeline for ventricular burden assessment
  1312. with enhanced statistical testing capabilities.
  1313. For questions about methodology or interpretation, refer to:
  1314. - Original research protocol and statistical analysis plan
  1315. - Relevant neuroimaging analysis guidelines
  1316. - Statistical consulting resources for complex designs
  1317. Analysis Pipeline Version: Enhanced Statistical Testing v2.0
  1318. Statistical Methods: Automatic normality assessment with adaptive test selection
  1319. Last Updated: {datetime.now().strftime('%Y-%m-%d')}
  1320. END OF DOCUMENTATION
  1321. ====================
  1322. """
  1323. # Save documentation to file
  1324. doc_filename = os.path.join(config.OUTPUT_DIR, 'ventricular_analysis_documentation.txt')
  1325. with open(doc_filename, 'w', encoding='utf-8') as f:
  1326. f.write(doc_content)
  1327. print(f"\n{'=' * 80}")
  1328. print("COMPREHENSIVE DOCUMENTATION GENERATED")
  1329. print(f"{'=' * 80}")
  1330. print(f"Documentation saved to: ventricular_analysis_documentation.txt")
  1331. print(f"File contains detailed explanation of:")
  1332. print(f"- Enhanced statistical testing methodology")
  1333. print(f"- Normality assessment and test selection")
  1334. print(f"- Effect size calculations and interpretation")
  1335. print(f"- Figure interpretation and clinical relevance")
  1336. def _generate_ventricular_tables(self, table_data):
  1337. """Generate comprehensive tables from the ventricular burden analysis with enhanced statistical reporting"""
  1338. # Table 1: Enhanced detailed statistics by group, age, and gender
  1339. print(f"\n{'=' * 80}")
  1340. print("TABLE 1: DETAILED STATISTICS BY GROUP, AGE, AND GENDER")
  1341. print(f"{'=' * 80}")
  1342. for metric_name in ['area', 'ratio', 'ratio_skull']:
  1343. unit = 'mm²' if metric_name == 'area' else '%'
  1344. if metric_name == 'area':
  1345. metric_title = 'Ventricular Area'
  1346. elif metric_name == 'ratio':
  1347. metric_title = 'Ventricular Ratio'
  1348. else:
  1349. metric_title = 'Ventricular Ratio-Skull'
  1350. print(f"\n{metric_title} ({unit}):")
  1351. print("-" * 60)
  1352. # Create DataFrame for this metric
  1353. rows = []
  1354. for group in ['HC', 'MS']:
  1355. for age_label in self.config.AGE_LABELS:
  1356. for gender in ['Male', 'Female']:
  1357. stats = table_data['detailed_stats'][group][metric_name][age_label][gender]
  1358. rows.append({
  1359. 'Group': group,
  1360. 'Age Group': age_label,
  1361. 'Gender': gender,
  1362. 'N': stats['count'],
  1363. 'Mean': f"{stats['mean']:.2f}" if not np.isnan(stats['mean']) else 'N/A',
  1364. 'SD': f"{stats['std']:.2f}" if not np.isnan(stats['std']) else 'N/A',
  1365. 'Min': f"{stats['min']:.2f}" if not np.isnan(stats['min']) else 'N/A',
  1366. 'Max': f"{stats['max']:.2f}" if not np.isnan(stats['max']) else 'N/A',
  1367. 'Median': f"{stats['median']:.2f}" if not np.isnan(stats['median']) else 'N/A',
  1368. 'IQR_25': f"{np.nan:.2f}" if np.isnan(
  1369. stats['median']) else f"{stats['median'] - stats['std'] / 2:.2f}",
  1370. 'IQR_75': f"{np.nan:.2f}" if np.isnan(
  1371. stats['median']) else f"{stats['median'] + stats['std'] / 2:.2f}"
  1372. })
  1373. df = pd.DataFrame(rows)
  1374. print(df.to_string(index=False))
  1375. # Save to CSV
  1376. filename = f'ventricular_{metric_name}_detailed_stats.csv'
  1377. df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  1378. print(f"Enhanced table saved to: {filename}")
  1379. # Table 2: Statistical Comparisons (NEW - Enhanced with normality and effect size info)
  1380. print(f"\n{'=' * 80}")
  1381. print("TABLE 2: STATISTICAL COMPARISONS (HC vs MS) - ENHANCED")
  1382. print(f"{'=' * 80}")
  1383. # Extract statistical results from the analysis
  1384. if hasattr(self, 'results') and 'ventricular_burden' in self.results:
  1385. ventricular_results = self.results['ventricular_burden']
  1386. comparison_rows = []
  1387. for metric_type in ['area_analysis', 'ratio_analysis']:
  1388. metric_name = 'Area' if metric_type == 'area_analysis' else 'Ratio'
  1389. unit = 'mm²' if metric_type == 'area_analysis' else '%'
  1390. analysis = ventricular_results[metric_type]
  1391. # Extract basic statistics
  1392. hc_mean, hc_std = analysis['hc_stats']
  1393. ms_mean, ms_std = analysis['ms_stats']
  1394. # Extract test results
  1395. comparison = analysis['comparison']
  1396. test_type = analysis['test_type']
  1397. normality_info = analysis.get('normality_info', {})
  1398. effect_size_info = analysis.get('effect_size', (None, None))
  1399. row = {
  1400. 'Metric': f'{metric_name} ({unit})',
  1401. 'HC_Mean': f"{hc_mean:.2f}" if not np.isnan(hc_mean) else 'N/A',
  1402. 'HC_SD': f"{hc_std:.2f}" if not np.isnan(hc_std) else 'N/A',
  1403. 'MS_Mean': f"{ms_mean:.2f}" if not np.isnan(ms_mean) else 'N/A',
  1404. 'MS_SD': f"{ms_std:.2f}" if not np.isnan(ms_std) else 'N/A',
  1405. 'Test_Type': test_type if test_type else 'N/A',
  1406. 'Test_Statistic': f"{comparison.statistic:.3f}" if comparison else 'N/A',
  1407. 'P_Value': f"{comparison.pvalue}" if comparison else 'N/A',
  1408. 'Effect_Size': f"{effect_size_info[0]:.3f}" if effect_size_info[0] is not None else 'N/A',
  1409. 'Effect_Size_Type': 'Cohen_d' if test_type == 'parametric' else 'r',
  1410. 'HC_Normality_p': f"{normality_info.get('group1_shapiro_p', np.nan):.3f}" if 'group1_shapiro_p' in normality_info else 'N/A',
  1411. 'MS_Normality_p': f"{normality_info.get('group2_shapiro_p', np.nan):.3f}" if 'group2_shapiro_p' in normality_info else 'N/A',
  1412. 'Normality_Passed': 'Yes' if test_type == 'parametric' else 'No' if test_type == 'non_parametric' else 'N/A'
  1413. }
  1414. comparison_rows.append(row)
  1415. comparison_df = pd.DataFrame(comparison_rows)
  1416. print("\nStatistical Comparison Results:")
  1417. print("-" * 100)
  1418. print(comparison_df.to_string(index=False))
  1419. # Save to CSV
  1420. comparison_df.to_csv(os.path.join(config.OUTPUT_DIR, 'ventricular_statistical_comparisons.csv'),
  1421. index=False)
  1422. print(f"\nStatistical comparisons saved to: ventricular_statistical_comparisons.csv")
  1423. # Table 3: Plot data (means used for visualization)
  1424. print(f"\n{'=' * 80}")
  1425. print("TABLE 3: PLOT DATA (MEANS AND CONTRIBUTIONS BY AGE GROUP)")
  1426. print(f"{'=' * 80}")
  1427. for metric_name in ['area', 'ratio', 'ratio_skull']:
  1428. unit = 'mm²' if metric_name == 'area' else '%'
  1429. if metric_name == 'area':
  1430. metric_title = 'Ventricular Area'
  1431. elif metric_name == 'ratio':
  1432. metric_title = 'Ventricular Ratio'
  1433. else:
  1434. metric_title = 'Ventricular Ratio-Skull'
  1435. print(f"\n{metric_title} - Mean Values and Contributions Used in Plot ({unit}):")
  1436. print("-" * 85)
  1437. # Create enhanced plot data table
  1438. plot_rows = []
  1439. for i, age_label in enumerate(self.config.AGE_LABELS):
  1440. age_center = table_data['plot_data']['HC'][metric_name]['age_centers'][i]
  1441. # HC data
  1442. hc_male_mean = table_data['plot_data']['HC'][metric_name]['male_means'][i]
  1443. hc_female_mean = table_data['plot_data']['HC'][metric_name]['female_means'][i]
  1444. hc_combined = table_data['plot_data']['HC'][metric_name]['combined_means'][i]
  1445. hc_male_contrib = table_data['plot_data']['HC'][metric_name]['male_contributions'][i]
  1446. hc_female_contrib = table_data['plot_data']['HC'][metric_name]['female_contributions'][i]
  1447. # MS data
  1448. ms_male_mean = table_data['plot_data']['MS'][metric_name]['male_means'][i]
  1449. ms_female_mean = table_data['plot_data']['MS'][metric_name]['female_means'][i]
  1450. ms_combined = table_data['plot_data']['MS'][metric_name]['combined_means'][i]
  1451. ms_male_contrib = table_data['plot_data']['MS'][metric_name]['male_contributions'][i]
  1452. ms_female_contrib = table_data['plot_data']['MS'][metric_name]['female_contributions'][i]
  1453. row = {
  1454. 'Age_Group': age_label,
  1455. 'Age_Center': f"{age_center:.1f}",
  1456. 'HC_Male_Mean': f"{hc_male_mean:.2f}",
  1457. 'HC_Female_Mean': f"{hc_female_mean:.2f}",
  1458. 'HC_Combined_Mean': f"{hc_combined:.2f}",
  1459. 'HC_Male_Contribution': f"{hc_male_contrib:.2f}",
  1460. 'HC_Female_Contribution': f"{hc_female_contrib:.2f}",
  1461. 'MS_Male_Mean': f"{ms_male_mean:.2f}",
  1462. 'MS_Female_Mean': f"{ms_female_mean:.2f}",
  1463. 'MS_Combined_Mean': f"{ms_combined:.2f}",
  1464. 'MS_Male_Contribution': f"{ms_male_contrib:.2f}",
  1465. 'MS_Female_Contribution': f"{ms_female_contrib:.2f}",
  1466. 'HC_Male_N': table_data['plot_data']['HC'][metric_name]['male_counts'][i],
  1467. 'HC_Female_N': table_data['plot_data']['HC'][metric_name]['female_counts'][i],
  1468. 'MS_Male_N': table_data['plot_data']['MS'][metric_name]['male_counts'][i],
  1469. 'MS_Female_N': table_data['plot_data']['MS'][metric_name]['female_counts'][i]
  1470. }
  1471. plot_rows.append(row)
  1472. plot_df = pd.DataFrame(plot_rows)
  1473. print(plot_df.to_string(index=False))
  1474. # Save to CSV
  1475. filename = f'ventricular_{metric_name}_plot_data.csv'
  1476. plot_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  1477. print(f"Enhanced plot data saved to: {filename}")
  1478. # Table 4: Contribution Analysis (NEW)
  1479. print(f"\n{'=' * 80}")
  1480. print("TABLE 4: PROPORTIONAL CONTRIBUTION ANALYSIS")
  1481. print(f"{'=' * 80}")
  1482. for metric_name in ['area', 'ratio']:
  1483. unit = 'mm²' if metric_name == 'area' else '%'
  1484. metric_title = 'Ventricular Area' if metric_name == 'area' else 'Ventricular Ratio'
  1485. print(f"\n{metric_title} - Proportional Contributions ({unit}):")
  1486. print("-" * 70)
  1487. # Create contribution analysis table
  1488. contrib_rows = []
  1489. for group in ['HC', 'MS']:
  1490. for i, age_label in enumerate(self.config.AGE_LABELS):
  1491. male_contrib = table_data['plot_data'][group][metric_name]['male_contributions'][i]
  1492. female_contrib = table_data['plot_data'][group][metric_name]['female_contributions'][i]
  1493. combined_mean = table_data['plot_data'][group][metric_name]['combined_means'][i]
  1494. male_count = table_data['plot_data'][group][metric_name]['male_counts'][i]
  1495. female_count = table_data['plot_data'][group][metric_name]['female_counts'][i]
  1496. total_count = male_count + female_count
  1497. # Calculate proportions
  1498. if combined_mean > 0:
  1499. male_prop = (male_contrib / combined_mean) * 100 if combined_mean > 0 else 0
  1500. female_prop = (female_contrib / combined_mean) * 100 if combined_mean > 0 else 0
  1501. else:
  1502. male_prop = 0
  1503. female_prop = 0
  1504. sample_male_prop = (male_count / total_count) * 100 if total_count > 0 else 0
  1505. sample_female_prop = (female_count / total_count) * 100 if total_count > 0 else 0
  1506. row = {
  1507. 'Group': group,
  1508. 'Age_Group': age_label,
  1509. 'Combined_Mean': f"{combined_mean:.2f}",
  1510. 'Male_Contribution': f"{male_contrib:.2f}",
  1511. 'Female_Contribution': f"{female_contrib:.2f}",
  1512. 'Male_Prop_of_Mean': f"{male_prop:.1f}%",
  1513. 'Female_Prop_of_Mean': f"{female_prop:.1f}%",
  1514. 'Male_Sample_Prop': f"{sample_male_prop:.1f}%",
  1515. 'Female_Sample_Prop': f"{sample_female_prop:.1f}%",
  1516. 'Total_N': total_count
  1517. }
  1518. contrib_rows.append(row)
  1519. contrib_df = pd.DataFrame(contrib_rows)
  1520. print(contrib_df.to_string(index=False))
  1521. # Save to CSV
  1522. filename = f'ventricular_{metric_name}_contributions.csv'
  1523. contrib_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  1524. print(f"Contribution analysis saved to: {filename}")
  1525. # Table 5: Effect Size Interpretation (NEW)
  1526. if hasattr(self, 'results') and 'ventricular_burden' in self.results:
  1527. print(f"\n{'=' * 80}")
  1528. print("TABLE 5: EFFECT SIZE INTERPRETATION GUIDE")
  1529. print(f"{'=' * 80}")
  1530. effect_size_rows = []
  1531. ventricular_results = self.results['ventricular_burden']
  1532. for metric_type in ['area_analysis', 'ratio_analysis']:
  1533. metric_name = 'Area' if metric_type == 'area_analysis' else 'Ratio'
  1534. analysis = ventricular_results[metric_type]
  1535. effect_size_info = analysis.get('effect_size', (None, None))
  1536. test_type = analysis['test_type']
  1537. if effect_size_info[0] is not None:
  1538. effect_size = effect_size_info[0]
  1539. if test_type == 'parametric':
  1540. # Cohen's d interpretation
  1541. if abs(effect_size) < 0.2:
  1542. magnitude = "Negligible"
  1543. elif abs(effect_size) < 0.5:
  1544. magnitude = "Small"
  1545. elif abs(effect_size) < 0.8:
  1546. magnitude = "Medium"
  1547. else:
  1548. magnitude = "Large"
  1549. else:
  1550. # Effect size r interpretation
  1551. if abs(effect_size) < 0.1:
  1552. magnitude = "Negligible"
  1553. elif abs(effect_size) < 0.3:
  1554. magnitude = "Small"
  1555. elif abs(effect_size) < 0.5:
  1556. magnitude = "Medium"
  1557. else:
  1558. magnitude = "Large"
  1559. row = {
  1560. 'Metric': metric_name,
  1561. 'Effect_Size_Value': f"{effect_size:.3f}",
  1562. 'Effect_Size_Type': "Cohen's d" if test_type == 'parametric' else "Rank-biserial correlation (r)",
  1563. 'Magnitude': magnitude,
  1564. 'Interpretation': f"{magnitude} effect size indicating {'substantial' if magnitude in ['Medium', 'Large'] else 'minimal'} practical difference"
  1565. }
  1566. effect_size_rows.append(row)
  1567. if effect_size_rows:
  1568. effect_df = pd.DataFrame(effect_size_rows)
  1569. print("\nEffect Size Interpretations:")
  1570. print("-" * 60)
  1571. print(effect_df.to_string(index=False))
  1572. # Save to CSV
  1573. effect_df.to_csv(os.path.join(config.OUTPUT_DIR, 'ventricular_effect_sizes.csv'), index=False)
  1574. print(f"\nEffect size interpretations saved to: ventricular_effect_sizes.csv")
  1575. # Table 6: Stacked area values (cumulative for visualization)
  1576. print(f"\n{'=' * 80}")
  1577. print("TABLE 6: STACKED AREA VALUES (FOR AREA CHART)")
  1578. print(f"{'=' * 80}")
  1579. for metric_name in ['area', 'ratio', 'ratio_skull']:
  1580. unit = 'mm²' if metric_name == 'area' else '%'
  1581. if metric_name == 'area':
  1582. metric_title = 'Ventricular Area'
  1583. elif metric_name == 'ratio':
  1584. metric_title = 'Ventricular Ratio'
  1585. else:
  1586. metric_title = 'Ventricular Ratio-Skull'
  1587. print(f"\n{metric_title} - Stacked Values ({unit}):")
  1588. print("-" * 70)
  1589. # Create stacked data table
  1590. stacked_rows = []
  1591. for group in ['HC', 'MS']:
  1592. for i, age_label in enumerate(self.config.AGE_LABELS):
  1593. male_mean = table_data['plot_data'][group][metric_name]['male_means'][i]
  1594. female_mean = table_data['plot_data'][group][metric_name]['female_means'][i]
  1595. row = {
  1596. 'Group': group,
  1597. 'Age Group': age_label,
  1598. 'Male Layer (0 to Male)': f"0.00 to {male_mean:.2f}",
  1599. 'Female Layer (Male to Total)': f"{male_mean:.2f} to {male_mean + female_mean:.2f}",
  1600. 'Total Height': f"{male_mean + female_mean:.2f}",
  1601. 'Male Contribution': f"{male_mean:.2f}",
  1602. 'Female Contribution': f"{female_mean:.2f}"
  1603. }
  1604. stacked_rows.append(row)
  1605. stacked_df = pd.DataFrame(stacked_rows)
  1606. print(stacked_df.to_string(index=False))
  1607. # Save to CSV
  1608. filename = f'ventricular_{metric_name}_stacked_data.csv'
  1609. stacked_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  1610. print(f"Stacked data saved to: {filename}")
  1611. print(f"\n{'=' * 80}")
  1612. print("ENHANCED STATISTICAL TABLES GENERATED!")
  1613. print(f"{'=' * 80}")
  1614. print("Generated files include:")
  1615. print("• Enhanced descriptive statistics with IQR")
  1616. print("• Comprehensive statistical comparison results")
  1617. print("• Detailed plot data with contributions")
  1618. print("• Proportional contribution analysis")
  1619. print("• Effect size interpretations")
  1620. print("• All tables saved as CSV files for further analysis")
  1621. print(f"{'=' * 80}")
  1622. def total_lesion_burden_analysis(self):
  1623. """Analyze total lesion burden with age and gender stratification - both area and ratio"""
  1624. print("\n" + "=" * 60)
  1625. print("TOTAL LESION BURDEN ANALYSIS")
  1626. print("=" * 60)
  1627. # Layout: 2 groups × (2 or 3 metrics).
  1628. _has_wmh_skull = 'WMHRatio_Skull' in self.data.columns
  1629. _n_metric_cols = 3 if _has_wmh_skull else 2
  1630. fig, axes = plt.subplots(2, _n_metric_cols, figsize=(8 * _n_metric_cols, 12))
  1631. fig.patch.set_facecolor('white')
  1632. if _n_metric_cols == 2:
  1633. axes = np.array(axes)
  1634. fig.patch.set_facecolor('white')
  1635. groups = ['HC', 'MS']
  1636. age_centers = [np.mean(age_range) for age_range in self.config.AGE_BINS]
  1637. # Metrics to plot. Skull-normalised ratio added as sensitivity analysis
  1638. # (Reviewer 2, Major Comments 4 & 5).
  1639. _has_wmh_skull = 'WMHRatio_Skull' in self.data.columns
  1640. metrics = {
  1641. 'area': {
  1642. 'column': self.config.COLUMNS['total_wmh'],
  1643. 'ylabel': 'WMH Area (mm²)',
  1644. 'title_suffix': 'WMH Area'
  1645. },
  1646. 'ratio': {
  1647. 'column': 'WMHRatio',
  1648. 'ylabel': 'WMH Ratio — brain denom. (%)',
  1649. 'title_suffix': 'WMH Ratio (brain-normalised)'
  1650. }
  1651. }
  1652. if _has_wmh_skull:
  1653. metrics['ratio_skull'] = {
  1654. 'column': 'WMHRatio_Skull',
  1655. 'ylabel': 'WMH Ratio — skull denom. (%)',
  1656. 'title_suffix': 'WMH Ratio (skull-normalised, sensitivity)'
  1657. }
  1658. else:
  1659. print(" [Sensitivity] 'WMHRatio_Skull' column absent — "
  1660. "skull-normalised WMH ratio panel will be omitted.")
  1661. # Dictionary to store all table data
  1662. table_data = {
  1663. 'detailed_stats': {},
  1664. 'plot_data': {},
  1665. 'metadata': {
  1666. 'age_bins': self.config.AGE_BINS,
  1667. 'age_labels': self.config.AGE_LABELS,
  1668. 'age_centers': age_centers,
  1669. 'groups': groups,
  1670. 'metrics': metrics,
  1671. 'colors': self.config.COLORS
  1672. }
  1673. }
  1674. # Process each group and metric combination
  1675. for group_idx, group in enumerate(groups):
  1676. group_data = self.data[self.data[self.config.COLUMNS['group']] == group]
  1677. table_data['detailed_stats'][group] = {}
  1678. table_data['plot_data'][group] = {}
  1679. for metric_idx, (metric_name, metric_info) in enumerate(metrics.items()):
  1680. ax = axes[group_idx, metric_idx]
  1681. # Initialize storage for this group-metric combination
  1682. table_data['detailed_stats'][group][metric_name] = {}
  1683. table_data['plot_data'][group][metric_name] = {
  1684. 'age_centers': age_centers.copy(),
  1685. 'age_labels': self.config.AGE_LABELS.copy(),
  1686. 'male_means': [],
  1687. 'female_means': [],
  1688. 'male_stds': [],
  1689. 'female_stds': [],
  1690. 'male_counts': [],
  1691. 'female_counts': [],
  1692. 'combined_means': [],
  1693. 'male_contributions': [],
  1694. 'female_contributions': [],
  1695. 'male_medians': [],
  1696. 'female_medians': [],
  1697. 'male_iqrs': [],
  1698. 'female_iqrs': []
  1699. }
  1700. # Process each age group
  1701. for age_idx, age_label in enumerate(self.config.AGE_LABELS):
  1702. age_group_data = group_data[group_data['AgeGroup'] == age_label]
  1703. # Separate by gender
  1704. male_data = age_group_data[age_group_data['Gender'] == 'Male'][metric_info['column']]
  1705. female_data = age_group_data[age_group_data['Gender'] == 'Female'][metric_info['column']]
  1706. # Calculate statistics for each gender
  1707. male_stats = {
  1708. 'count': len(male_data),
  1709. 'mean': male_data.mean() if len(male_data) > 0 else np.nan,
  1710. 'std': male_data.std() if len(male_data) > 0 else np.nan,
  1711. 'min': male_data.min() if len(male_data) > 0 else np.nan,
  1712. 'max': male_data.max() if len(male_data) > 0 else np.nan,
  1713. 'median': male_data.median() if len(male_data) > 0 else np.nan,
  1714. 'q25': male_data.quantile(0.25) if len(male_data) > 0 else np.nan,
  1715. 'q75': male_data.quantile(0.75) if len(male_data) > 0 else np.nan
  1716. }
  1717. female_stats = {
  1718. 'count': len(female_data),
  1719. 'mean': female_data.mean() if len(female_data) > 0 else np.nan,
  1720. 'std': female_data.std() if len(female_data) > 0 else np.nan,
  1721. 'min': female_data.min() if len(female_data) > 0 else np.nan,
  1722. 'max': female_data.max() if len(female_data) > 0 else np.nan,
  1723. 'median': female_data.median() if len(female_data) > 0 else np.nan,
  1724. 'q25': female_data.quantile(0.25) if len(female_data) > 0 else np.nan,
  1725. 'q75': female_data.quantile(0.75) if len(female_data) > 0 else np.nan
  1726. }
  1727. # Store detailed statistics
  1728. table_data['detailed_stats'][group][metric_name][age_label] = {
  1729. 'Male': male_stats,
  1730. 'Female': female_stats
  1731. }
  1732. # Calculate values for plotting (handle NaN values)
  1733. male_mean = male_stats['mean'] if not np.isnan(male_stats['mean']) else 0
  1734. female_mean = female_stats['mean'] if not np.isnan(female_stats['mean']) else 0
  1735. male_std = male_stats['std'] if not np.isnan(male_stats['std']) else 0
  1736. female_std = female_stats['std'] if not np.isnan(female_stats['std']) else 0
  1737. male_median = male_stats['median'] if not np.isnan(male_stats['median']) else 0
  1738. female_median = female_stats['median'] if not np.isnan(female_stats['median']) else 0
  1739. # Calculate IQR for plotting (if needed for error bars)
  1740. male_iqr = (male_stats['q75'] - male_stats['q25']) if (
  1741. not np.isnan(male_stats['q75']) and not np.isnan(male_stats['q25'])) else 0
  1742. female_iqr = (female_stats['q75'] - female_stats['q25']) if (
  1743. not np.isnan(female_stats['q75']) and not np.isnan(female_stats['q25'])) else 0
  1744. # Calculate weighted combined mean and contributions
  1745. total_male_sum = male_stats['count'] * male_mean if male_stats['count'] > 0 else 0
  1746. total_female_sum = female_stats['count'] * female_mean if female_stats['count'] > 0 else 0
  1747. total_subjects = male_stats['count'] + female_stats['count']
  1748. if total_subjects > 0:
  1749. # True weighted combined mean across both genders
  1750. combined_mean = (total_male_sum + total_female_sum) / total_subjects
  1751. # Calculate proportional contributions to the combined mean
  1752. total_sum = total_male_sum + total_female_sum
  1753. if total_sum > 0:
  1754. male_contribution = (total_male_sum / total_sum) * combined_mean
  1755. female_contribution = (total_female_sum / total_sum) * combined_mean
  1756. else:
  1757. male_contribution = 0
  1758. female_contribution = 0
  1759. else:
  1760. combined_mean = 0
  1761. male_contribution = 0
  1762. female_contribution = 0
  1763. # Store plot data
  1764. plot_data = table_data['plot_data'][group][metric_name]
  1765. plot_data['male_means'].append(male_mean)
  1766. plot_data['female_means'].append(female_mean)
  1767. plot_data['male_stds'].append(male_std)
  1768. plot_data['female_stds'].append(female_std)
  1769. plot_data['male_counts'].append(male_stats['count'])
  1770. plot_data['female_counts'].append(female_stats['count'])
  1771. plot_data['combined_means'].append(combined_mean)
  1772. plot_data['male_contributions'].append(male_contribution)
  1773. plot_data['female_contributions'].append(female_contribution)
  1774. plot_data['male_medians'].append(male_median)
  1775. plot_data['female_medians'].append(female_median)
  1776. plot_data['male_iqrs'].append(male_iqr)
  1777. plot_data['female_iqrs'].append(female_iqr)
  1778. # Create the plot
  1779. plot_data = table_data['plot_data'][group][metric_name]
  1780. # Plot male contribution (bottom layer)
  1781. ax.fill_between(age_centers, 0, plot_data['male_contributions'],
  1782. color=self.config.COLORS['male'], alpha=0.7, label='Male')
  1783. # Plot female contribution (top layer)
  1784. ax.fill_between(age_centers, plot_data['male_contributions'],
  1785. plot_data['combined_means'],
  1786. color=self.config.COLORS['female'], alpha=0.7, label='Female')
  1787. # Optional: Add median lines for comparison (uncomment if desired)
  1788. # ax.plot(age_centers, plot_data['male_medians'],
  1789. # color=self.config.COLORS['male'], linestyle='--', alpha=0.8, label='Male Median')
  1790. # ax.plot(age_centers, plot_data['female_medians'],
  1791. # color=self.config.COLORS['female'], linestyle='--', alpha=0.8, label='Female Median')
  1792. # Formatting
  1793. # Calculate panel letter (A, B, C, D)
  1794. panel_idx = group_idx * 3 + metric_idx
  1795. panel_letter = chr(65 + panel_idx) # 65 is ASCII for 'A'
  1796. ax.set_title(f'{panel_letter}. {group} - {metric_info["title_suffix"]}', fontsize=20, fontweight='bold')
  1797. # ax.set_title(f'{group} - {metric_info["title_suffix"]}', fontsize=14, fontweight='bold')
  1798. ax.set_xlabel('Age (years)', fontsize=20)
  1799. ax.set_ylabel(metric_info['ylabel'], fontsize=20)
  1800. ax.legend(fontsize=20)
  1801. ax.grid(True, alpha=0.3)
  1802. ax.set_xticks(age_centers)
  1803. ax.set_xticklabels(self.config.AGE_LABELS, fontsize=20)
  1804. # Set reasonable y-axis limits
  1805. max_value = max(plot_data['combined_means']) if plot_data['combined_means'] else 0
  1806. if max_value > 0:
  1807. ax.set_ylim(0, max_value * 1.1)
  1808. plt.tight_layout()
  1809. plt.savefig(os.path.join(config.OUTPUT_DIR, 'total_lesion_burden_analysis.png'),
  1810. dpi=self.config.DPI, bbox_inches='tight', facecolor='white')
  1811. # Generate comprehensive documentation
  1812. self._generate_lesion_analysis_documentation(table_data)
  1813. # Generate and save tables
  1814. self._generate_lesion_burden_tables(table_data)
  1815. # Statistical analysis for both metrics
  1816. print(f"\n{'=' * 50}")
  1817. print("STATISTICAL COMPARISONS (HC vs MS)")
  1818. print(f"{'=' * 50}")
  1819. # Analysis for WMH area (absolute values)
  1820. hc_area = self.data[self.data[self.config.COLUMNS['group']] == 'HC'][self.config.COLUMNS['total_wmh']]
  1821. ms_area = self.data[self.data[self.config.COLUMNS['group']] == 'MS'][self.config.COLUMNS['total_wmh']]
  1822. if len(hc_area) > 0 and len(ms_area) > 0:
  1823. # Use the standardized testing approach
  1824. area_test, area_test_type, area_normality_info, area_effect_size_info = self._assess_normality_and_choose_test(
  1825. hc_area, ms_area, "WMH Area"
  1826. )
  1827. print(f"\nWMH Area comparison:")
  1828. print(f"HC: N={len(hc_area)}, mean ± SD = {hc_area.mean():.2f} ± {hc_area.std():.2f} mm²")
  1829. print(f"MS: N={len(ms_area)}, mean ± SD = {ms_area.mean():.2f} ± {ms_area.std():.2f} mm²")
  1830. # Print normality test results
  1831. if 'group1_shapiro_p' in area_normality_info:
  1832. print(
  1833. f"Normality tests: HC p={area_normality_info['group1_shapiro_p']:.3f}, MS p={area_normality_info['group2_shapiro_p']:.3f}")
  1834. if area_test_type == "parametric":
  1835. print(f"Independent t-test: t-statistic = {area_test.statistic:.3f}, p-value = {area_test.pvalue}")
  1836. print(f"Cohen's d = {area_effect_size_info[0]:.3f}")
  1837. elif area_test_type == "non_parametric":
  1838. print(f"Mann-Whitney U test: U-statistic = {area_test.statistic:.3f}, p-value = {area_test.pvalue}")
  1839. print(f"Effect size (r) = {area_effect_size_info[0]:.3f}")
  1840. # Report medians for non-parametric
  1841. print(
  1842. f"HC: median [IQR] = {hc_area.median():.2f} [{hc_area.quantile(0.25):.2f}-{hc_area.quantile(0.75):.2f}] mm²")
  1843. print(
  1844. f"MS: median [IQR] = {ms_area.median():.2f} [{ms_area.quantile(0.25):.2f}-{ms_area.quantile(0.75):.2f}] mm²")
  1845. else:
  1846. print("Insufficient data for WMH area comparison")
  1847. area_test = None
  1848. area_test_type = None
  1849. area_normality_info = {}
  1850. area_effect_size_info = (None, None)
  1851. # Analysis for WMH ratio (normalized values)
  1852. hc_ratio = self.data[self.data[self.config.COLUMNS['group']] == 'HC']['WMHRatio']
  1853. ms_ratio = self.data[self.data[self.config.COLUMNS['group']] == 'MS']['WMHRatio']
  1854. if len(hc_ratio) > 0 and len(ms_ratio) > 0:
  1855. # Use the standardized testing approach
  1856. ratio_test, ratio_test_type, ratio_normality_info, ratio_effect_size_info = self._assess_normality_and_choose_test(
  1857. hc_ratio, ms_ratio, "WMH Ratio"
  1858. )
  1859. print(f"\nWMH Ratio comparison:")
  1860. print(f"HC: N={len(hc_ratio)}, mean ± SD = {hc_ratio.mean():.2f} ± {hc_ratio.std():.2f}%")
  1861. print(f"MS: N={len(ms_ratio)}, mean ± SD = {ms_ratio.mean():.2f} ± {ms_ratio.std():.2f}%")
  1862. # Print normality test results
  1863. if 'group1_shapiro_p' in ratio_normality_info:
  1864. print(
  1865. f"Normality tests: HC p={ratio_normality_info['group1_shapiro_p']:.3f}, MS p={ratio_normality_info['group2_shapiro_p']:.3f}")
  1866. if ratio_test_type == "parametric":
  1867. print(
  1868. f"Independent t-test: t-statistic = {ratio_test.statistic:.3f}, p-value = {ratio_test.pvalue}")
  1869. print(f"Cohen's d = {ratio_effect_size_info[0]:.3f}")
  1870. elif ratio_test_type == "non_parametric":
  1871. print(
  1872. f"Mann-Whitney U test: U-statistic = {ratio_test.statistic:.3f}, p-value = {ratio_test.pvalue}")
  1873. print(f"Effect size (r) = {ratio_effect_size_info[0]:.3f}")
  1874. # Report medians for non-parametric
  1875. print(
  1876. f"HC: median [IQR] = {hc_ratio.median():.2f} [{hc_ratio.quantile(0.25):.2f}-{hc_ratio.quantile(0.75):.2f}]%")
  1877. print(
  1878. f"MS: median [IQR] = {ms_ratio.median():.2f} [{ms_ratio.quantile(0.25):.2f}-{ms_ratio.quantile(0.75):.2f}]%")
  1879. else:
  1880. print("Insufficient data for WMH ratio comparison")
  1881. ratio_test = None
  1882. ratio_test_type = None
  1883. ratio_normality_info = {}
  1884. ratio_effect_size_info = (None, None)
  1885. # Store results (update the existing results storage)
  1886. self.results['lesion_burden'] = {
  1887. 'area_analysis': {
  1888. 'hc_stats': (hc_area.median(), hc_area.quantile(0.25), hc_area.quantile(0.75)) if len(
  1889. hc_area) > 0 else (np.nan, np.nan, np.nan),
  1890. 'ms_stats': (ms_area.median(), ms_area.quantile(0.25), ms_area.quantile(0.75)) if len(
  1891. ms_area) > 0 else (np.nan, np.nan, np.nan),
  1892. 'hc_mean_stats': (hc_area.mean(), hc_area.std()) if len(hc_area) > 0 else (np.nan, np.nan),
  1893. 'ms_mean_stats': (ms_area.mean(), ms_area.std()) if len(ms_area) > 0 else (np.nan, np.nan),
  1894. 'comparison': area_test,
  1895. 'test_type': area_test_type,
  1896. 'normality_info': area_normality_info,
  1897. 'effect_size': area_effect_size_info
  1898. },
  1899. 'ratio_analysis': {
  1900. 'hc_stats': (hc_ratio.median(), hc_ratio.quantile(0.25), hc_ratio.quantile(0.75)) if len(
  1901. hc_ratio) > 0 else (np.nan, np.nan, np.nan),
  1902. 'ms_stats': (ms_ratio.median(), ms_ratio.quantile(0.25), ms_ratio.quantile(0.75)) if len(
  1903. ms_ratio) > 0 else (np.nan, np.nan, np.nan),
  1904. 'hc_mean_stats': (hc_ratio.mean(), hc_ratio.std()) if len(hc_ratio) > 0 else (np.nan, np.nan),
  1905. 'ms_mean_stats': (ms_ratio.mean(), ms_ratio.std()) if len(ms_ratio) > 0 else (np.nan, np.nan),
  1906. 'comparison': ratio_test,
  1907. 'test_type': ratio_test_type,
  1908. 'normality_info': ratio_normality_info,
  1909. 'effect_size': ratio_effect_size_info
  1910. },
  1911. 'table_data': table_data
  1912. }
  1913. return self.results['lesion_burden']
  1914. def _generate_lesion_analysis_documentation(self, table_data):
  1915. """Generate comprehensive documentation explaining the lesion burden figure and analysis"""
  1916. from datetime import datetime
  1917. # Create comprehensive documentation
  1918. doc_content = f"""
  1919. TOTAL LESION BURDEN ANALYSIS - COMPREHENSIVE DOCUMENTATION
  1920. ==========================================================
  1921. Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  1922. OVERVIEW
  1923. --------
  1924. This analysis examines white matter hyperintensity (WMH) lesion burden in the brain
  1925. comparing Healthy Controls (HC) and Multiple Sclerosis (MS) patients. The analysis
  1926. includes both absolute WMH area measurements and normalized WMH ratios, stratified
  1927. by age groups and gender.
  1928. FIGURE DESCRIPTION
  1929. ------------------
  1930. The figure consists of a 2x2 subplot layout:
  1931. Layout Structure:
  1932. - Top row: Healthy Controls (HC)
  1933. - Bottom row: Multiple Sclerosis (MS) patients
  1934. - Left column: Absolute WMH Area (mm²)
  1935. - Right column: WMH Ratio (%)
  1936. Subplot Details:
  1937. 1. Top-Left: HC WMH Area
  1938. 2. Top-Right: HC WMH Ratio
  1939. 3. Bottom-Left: MS WMH Area
  1940. 4. Bottom-Right: MS WMH Ratio
  1941. VISUALIZATION METHOD
  1942. --------------------
  1943. Chart Type: Stacked Area Plot
  1944. - Each subplot uses stacked area charts to show gender-stratified data across age groups
  1945. - Male data (bottom layer): Fills from 0 to male mean value
  1946. - Female data (top layer): Fills from male mean to total (male + female) mean
  1947. - This visualization allows comparison of both absolute values and gender contributions
  1948. Color Scheme:
  1949. - Male data: {table_data['metadata']['colors'].get('male', 'Blue')} (alpha=0.7)
  1950. - Female data: {table_data['metadata']['colors'].get('female', 'Red')} (alpha=0.7)
  1951. AGE STRATIFICATION
  1952. ------------------
  1953. Age Groups: {', '.join(table_data['metadata']['age_labels'])}
  1954. Age Bins: {table_data['metadata']['age_bins']}
  1955. Age Centers (for plotting): {[f'{center:.1f}' for center in table_data['metadata']['age_centers']]}
  1956. The analysis stratifies data across these age groups to examine age-related changes
  1957. in lesion burden for both groups and genders.
  1958. METRICS ANALYZED
  1959. ----------------
  1960. 1. WMH Area (mm²):
  1961. - Absolute measurement of total white matter hyperintensity volume
  1962. - Column: {table_data['metadata']['metrics']['area']['column']}
  1963. - Units: Square millimeters (mm²)
  1964. - Clinical significance: Larger values indicate greater lesion burden
  1965. 2. WMH Ratio (%):
  1966. - Normalized measurement relative to total brain area/volume
  1967. - Column: WMHRatio
  1968. - Units: Percentage (%)
  1969. - Clinical significance: Controls for individual brain size differences
  1970. CLINICAL CONTEXT
  1971. ----------------
  1972. White Matter Hyperintensities (WMH):
  1973. - Bright signal areas on T2-weighted and FLAIR MRI sequences
  1974. - Associated with small vessel disease, aging, and neurodegeneration
  1975. - In MS: May represent demyelination, inflammation, or tissue damage
  1976. - Age-related increase is normal but accelerated in pathological conditions
  1977. Expected Patterns:
  1978. - MS patients typically show higher WMH burden than healthy controls
  1979. - Age-related increase in WMH burden in both groups
  1980. - MS may show accelerated age-related progression
  1981. - Gender differences may exist in lesion development patterns
  1982. ADVANCED STATISTICAL APPROACH
  1983. ------------------------------
  1984. The analysis employs a sophisticated statistical testing framework:
  1985. Normality Assessment:
  1986. - Shapiro-Wilk tests performed on both groups (HC and MS) for each metric
  1987. - Significance threshold: p < 0.05 indicates non-normal distribution
  1988. - Results inform choice between parametric and non-parametric tests
  1989. Statistical Test Selection:
  1990. 1. If both groups pass normality: Independent t-test (parametric)
  1991. 2. If one or both groups fail normality: Mann-Whitney U test (non-parametric)
  1992. Effect Size Calculations:
  1993. - Parametric tests: Cohen's d
  1994. * Small effect: d = 0.2
  1995. * Medium effect: d = 0.5
  1996. * Large effect: d = 0.8
  1997. - Non-parametric tests: Rank-biserial correlation (r ≈ Z/√N, large-sample approximation)
  1998. Note: termed "Effect size r" in earlier versions; corrected per Reviewer 2, Minor Comment 4.
  1999. * Small effect: r = 0.1
  2000. * Medium effect: r = 0.3
  2001. * Large effect: r = 0.5
  2002. For each combination of:
  2003. - Group (HC vs MS)
  2004. - Age group ({len(table_data['metadata']['age_labels'])} categories)
  2005. - Gender (Male vs Female)
  2006. - Metric (Area vs Ratio)
  2007. The following statistics are calculated:
  2008. - Sample size (N)
  2009. - Mean ± Standard Deviation
  2010. - Minimum and Maximum values
  2011. - Median values
  2012. - Interquartile ranges (for non-parametric reporting)
  2013. STATISTICAL OUTPUT INTERPRETATION
  2014. ---------------------------------
  2015. Parametric Results (t-test):
  2016. - Reports: Mean ± SD for both groups
  2017. - Test statistic: t-value and degrees of freedom
  2018. - p-value for significance testing
  2019. - Cohen's d for effect size magnitude
  2020. Non-parametric Results (Mann-Whitney U):
  2021. - Reports: Mean ± SD AND Median [IQR] for both groups
  2022. - Test statistic: U-value (or equivalent Z-score)
  2023. - p-value for significance testing
  2024. - Rank-biserial correlation (r) for effect magnitude (Mann-Whitney U; r ≈ Z/√N)
  2025. Normality Test Results:
  2026. - Shapiro-Wilk p-values reported for each group
  2027. - p < 0.05 indicates significant departure from normality
  2028. - Informs test selection rationale
  2029. INTERPRETATION GUIDELINES
  2030. -------------------------
  2031. Stacked Area Plot Interpretation:
  2032. - Height of bottom layer = Male mean value
  2033. - Height of top layer = Female mean value
  2034. - Total height = Combined mean (male + female means)
  2035. - Wider gaps between age points indicate larger differences
  2036. - Steeper slopes indicate rapid changes with age
  2037. Clinical Significance Thresholds:
  2038. - Minimal lesion burden: < 500 mm² (approximate)
  2039. - Mild lesion burden: 500-5000 mm²
  2040. - Moderate lesion burden: 5000-15000 mm²
  2041. - Severe lesion burden: > 15000 mm²
  2042. (Note: These are approximate guidelines and may vary by study protocol)
  2043. Expected Clinical Patterns:
  2044. - HC group: Low baseline with gradual age-related increase
  2045. - MS group: Higher baseline with potentially steeper age-related progression
  2046. - Gender differences: May reflect hormonal or lifestyle factors
  2047. - Age acceleration: MS may show earlier onset of lesion accumulation
  2048. Statistical Significance Levels:
  2049. - p < 0.001: Highly significant (strong evidence)
  2050. - p < 0.01: Very significant (moderate to strong evidence)
  2051. - p < 0.05: Significant (sufficient evidence)
  2052. - p ≥ 0.05: Non-significant (insufficient evidence)
  2053. Effect Size Interpretation:
  2054. - Cohen's d or r values indicate practical significance
  2055. - Large effect sizes may be clinically meaningful even if p > 0.05
  2056. - Small p-values with small effect sizes may lack clinical relevance
  2057. DATA QUALITY CONSIDERATIONS
  2058. ----------------------------
  2059. - Zero values in plots indicate no subjects in that age/gender combination
  2060. - Small sample sizes may lead to unstable mean estimates
  2061. - Standard deviations provide insight into data variability within groups
  2062. - WMH measurements are sensitive to MRI acquisition parameters
  2063. - Manual/automated segmentation differences may affect absolute values
  2064. - Ratios help normalize for technical and anatomical variations
  2065. - Normality violations automatically trigger non-parametric alternatives
  2066. STATISTICAL TESTING DETAILS
  2067. ----------------------------
  2068. Overall group comparisons (HC vs MS) performed separately for each metric:
  2069. 1. WMH Area Analysis:
  2070. - Automatic normality assessment using Shapiro-Wilk test
  2071. - Test selection based on normality results
  2072. - Effect size calculation appropriate to test type
  2073. - Comprehensive reporting of descriptive statistics
  2074. 2. WMH Ratio Analysis:
  2075. - Independent statistical analysis from area measurements
  2076. - Same rigorous normality assessment and test selection
  2077. - Separate effect size calculations
  2078. - Controls for multiple testing considerations
  2079. Test Assumptions:
  2080. - Parametric tests: Normality, independence, homogeneity of variance
  2081. - Non-parametric tests: Independence, similar distributions
  2082. - Both assume random sampling from populations of interest
  2083. OUTPUT FILES GENERATED
  2084. -----------------------
  2085. 1. Figure: total_lesion_burden_analysis.png
  2086. - 2x2 subplot layout with stacked area plots showing proportional contributions
  2087. - High resolution (DPI: {getattr(self.config, 'DPI', 300)})
  2088. - White background for publication quality
  2089. 2. Enhanced Statistics Tables (CSV):
  2090. - lesion_area_detailed_stats.csv (includes all descriptive statistics)
  2091. - lesion_ratio_detailed_stats.csv (includes all descriptive statistics)
  2092. - lesion_statistical_comparisons.csv (NEW: comprehensive test results)
  2093. 3. Plot Data Tables (CSV):
  2094. - lesion_area_plot_data.csv (means and sample sizes)
  2095. - lesion_ratio_plot_data.csv (means and sample sizes)
  2096. 4. Contribution Analysis (CSV):
  2097. - lesion_area_contributions.csv (NEW: proportional contributions)
  2098. - lesion_ratio_contributions.csv (NEW: proportional contributions)
  2099. 5. Statistical Results Summary (CSV):
  2100. - lesion_normality_results.csv (NEW: normality test outcomes)
  2101. - lesion_effect_sizes.csv (NEW: effect size calculations)
  2102. 6. Stacked Area Values (CSV):
  2103. - lesion_area_stacked_data.csv
  2104. - lesion_ratio_stacked_data.csv
  2105. 7. This Documentation:
  2106. - lesion_burden_analysis_documentation.txt
  2107. TECHNICAL SPECIFICATIONS
  2108. -------------------------
  2109. Figure Specifications:
  2110. - Size: 16" x 12" (width x height)
  2111. - DPI: {getattr(self.config, 'DPI', 300)}
  2112. - Background: White
  2113. - Font sizes: Title=14pt (bold), Axis labels=12pt
  2114. - Grid: Enabled with 30% transparency
  2115. - Legend: Enabled for each subplot
  2116. Data Processing:
  2117. - Missing data handled by excluding from calculations
  2118. - Zero values used when no subjects available in category
  2119. - Robust statistics (median/IQR) preferred over mean/SD
  2120. - Sample size weighting for statistical calculations
  2121. Statistical Libraries:
  2122. - scipy.stats: Shapiro-Wilk, t-test, Mann-Whitney U
  2123. - numpy: Mathematical operations and statistical functions
  2124. - pandas: Data manipulation and summary statistics
  2125. Quality Control:
  2126. - Automatic handling of missing values
  2127. - Robust error handling for edge cases
  2128. - Comprehensive logging of statistical decisions
  2129. - Validation of statistical assumptions
  2130. LIMITATIONS AND CONSIDERATIONS
  2131. ------------------------------
  2132. 1. Sample Size Variations:
  2133. - Some age/gender combinations may have small sample sizes
  2134. - Unbalanced groups may affect statistical power
  2135. - Non-parametric tests more robust to unequal sample sizes
  2136. 2. Multiple Comparisons:
  2137. - Two separate statistical tests performed (area and ratio)
  2138. - Consider Bonferroni correction: α = 0.05/2 = 0.025
  2139. - Family-wise error rate may be inflated without correction
  2140. 3. Age Grouping Effects:
  2141. - Discretized age groups may mask continuous age effects
  2142. - Age bin boundaries are predetermined and may not reflect natural breakpoints
  2143. - Consider continuous age modeling for more detailed analysis
  2144. 4. Lesion Measurement Considerations:
  2145. - WMH detection depends on MRI sequence parameters
  2146. - Segmentation methods (manual vs automated) may introduce variability
  2147. - Small lesions may be missed due to resolution limitations
  2148. - Partial volume effects at tissue boundaries
  2149. 5. Stacked Area Representation:
  2150. - Visual emphasis on gender differences may overshadow group differences
  2151. - Direct comparison between groups requires careful interpretation
  2152. - Mean values used may not reflect distribution skewness
  2153. 6. Statistical Assumptions:
  2154. - Automatic normality testing with Shapiro-Wilk (sensitive to large samples)
  2155. - Independence assumption may be violated in related subjects
  2156. - Equal variances assumed for t-tests (consider Welch's t-test alternative)
  2157. RECOMMENDED FOLLOW-UP ANALYSES
  2158. ------------------------------
  2159. 1. Advanced Statistical Approaches:
  2160. - Age as continuous variable (linear/polynomial regression)
  2161. - Two-way ANOVA with interaction terms (group × gender × age)
  2162. - Mixed-effects models for correlated data
  2163. - Bootstrap confidence intervals for robust inference
  2164. 2. Multiple Comparisons Corrections:
  2165. - Bonferroni correction for family-wise error control
  2166. - False Discovery Rate (FDR) control for exploratory analyses
  2167. - Planned comparisons vs. post-hoc testing strategies
  2168. 3. Effect Size and Power Analysis:
  2169. - Post-hoc power calculations for observed effects
  2170. - Sample size calculations for future studies
  2171. - Confidence intervals around effect size estimates
  2172. 4. Alternative Statistical Approaches:
  2173. - Bayesian analysis for probabilistic interpretation
  2174. - Permutation tests for distribution-free inference
  2175. - Robust statistical methods for outlier resistance
  2176. 5. Longitudinal Analysis:
  2177. - If temporal data available, analyze lesion progression rates
  2178. - Mixed-effects models for individual trajectories
  2179. - Survival analysis for time to lesion threshold
  2180. - Correlation with clinical severity measures
  2181. - Longitudinal tracking of lesion changes
  2182. - Predictive modeling for disease progression
  2183. QUALITY ASSURANCE CHECKLIST
  2184. ----------------------------
  2185. ✓ Normality testing performed automatically
  2186. ✓ Appropriate statistical test selected based on data properties
  2187. ✓ Effect sizes calculated and reported
  2188. ✓ Both parametric and non-parametric results available
  2189. ✓ Comprehensive descriptive statistics provided
  2190. ✓ Multiple output formats for different use cases
  2191. ✓ Documentation includes interpretation guidelines
  2192. ✓ Limitations and assumptions clearly stated
  2193. ✓ Recommendations for follow-up analyses provided
  2194. QUALITY CONTROL RECOMMENDATIONS
  2195. --------------------------------
  2196. 1. Data Validation:
  2197. - Check for implausible values (negative areas, ratios > 100%)
  2198. - Identify and investigate outliers
  2199. - Verify age group assignments
  2200. 2. Technical Validation:
  2201. - Compare manual vs automated segmentation on subset
  2202. - Inter-rater reliability for manual segmentations
  2203. - Phantom studies for scanner consistency
  2204. 3. Clinical Validation:
  2205. - Correlation with clinical disability measures
  2206. - Agreement with radiological assessment
  2207. - Validation against established biomarkers
  2208. CONTACT AND METHODOLOGY
  2209. -----------------------
  2210. This analysis was generated using an automated pipeline for lesion burden assessment
  2211. with enhanced statistical testing capabilities.
  2212. For questions about methodology or interpretation, refer to:
  2213. - Original research protocol and statistical analysis plan
  2214. - Relevant neuroimaging analysis guidelines
  2215. - Statistical consulting resources for complex designs
  2216. Analysis Pipeline Version: Enhanced Statistical Testing v2.0
  2217. Statistical Methods: Automatic normality assessment with adaptive test selection
  2218. Last Updated: {datetime.now().strftime('%Y-%m-%d')}
  2219. END OF DOCUMENTATION
  2220. ====================
  2221. """
  2222. # Save documentation to file
  2223. doc_filename = os.path.join(config.OUTPUT_DIR, 'lesion_burden_analysis_documentation.txt')
  2224. with open(doc_filename, 'w', encoding='utf-8') as f:
  2225. f.write(doc_content)
  2226. print(f"\n{'=' * 80}")
  2227. print("COMPREHENSIVE LESION BURDEN DOCUMENTATION GENERATED")
  2228. print(f"{'=' * 80}")
  2229. print(f"Documentation saved to: lesion_burden_analysis_documentation.txt")
  2230. print(f"File contains detailed explanation of figure, methods, and clinical interpretation.")
  2231. print(f"- Enhanced statistical testing methodology")
  2232. print(f"- Normality assessment and test selection")
  2233. print(f"- Effect size calculations and interpretation")
  2234. print(f"- Figure interpretation and clinical relevance")
  2235. def _generate_lesion_burden_tables(self, table_data):
  2236. """Generate comprehensive tables from the lesion burden analysis"""
  2237. # Table 1: Enhanced detailed statistics by group, age, and gender
  2238. print(f"\n{'=' * 80}")
  2239. print("TABLE 1: DETAILED STATISTICS BY GROUP, AGE, AND GENDER")
  2240. print(f"{'=' * 80}")
  2241. for metric_name in ['area', 'ratio', 'ratio_skull']:
  2242. unit = 'mm²' if metric_name == 'area' else '%'
  2243. if metric_name == 'area':
  2244. metric_title = 'WMH Area'
  2245. elif metric_name == 'ratio':
  2246. metric_title = 'WMH Ratio'
  2247. else:
  2248. metric_title = 'WMH Ratio-Skull'
  2249. print(f"\n{metric_title} ({unit}):")
  2250. print("-" * 70)
  2251. # Create DataFrame for this metric
  2252. rows = []
  2253. for group in ['HC', 'MS']:
  2254. for age_label in self.config.AGE_LABELS:
  2255. for gender in ['Male', 'Female']:
  2256. stats = table_data['detailed_stats'][group][metric_name][age_label][gender]
  2257. rows.append({
  2258. 'Group': group,
  2259. 'Age Group': age_label,
  2260. 'Gender': gender,
  2261. 'N': stats['count'],
  2262. 'Mean': f"{stats['mean']:.2f}" if not np.isnan(stats['mean']) else 'N/A',
  2263. 'SD': f"{stats['std']:.2f}" if not np.isnan(stats['std']) else 'N/A',
  2264. 'Min': f"{stats['min']:.2f}" if not np.isnan(stats['min']) else 'N/A',
  2265. 'Max': f"{stats['max']:.2f}" if not np.isnan(stats['max']) else 'N/A',
  2266. 'Median': f"{stats['median']:.2f}" if not np.isnan(stats['median']) else 'N/A',
  2267. 'IQR_25': f"{np.nan:.2f}" if np.isnan(
  2268. stats['median']) else f"{stats['median'] - stats['std'] / 2:.2f}",
  2269. 'IQR_75': f"{np.nan:.2f}" if np.isnan(
  2270. stats['median']) else f"{stats['median'] + stats['std'] / 2:.2f}"
  2271. })
  2272. df = pd.DataFrame(rows)
  2273. print(df.to_string(index=False))
  2274. # Save to CSV
  2275. filename = f'lesion_{metric_name}_detailed_stats.csv'
  2276. df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  2277. print(f"Enhanced table saved to: {filename}")
  2278. # Table 2: Statistical Comparisons (NEW - Enhanced with normality and effect size info)
  2279. print(f"\n{'=' * 80}")
  2280. print("TABLE 2: STATISTICAL COMPARISONS (HC vs MS) - ENHANCED")
  2281. print(f"{'=' * 80}")
  2282. # Extract statistical results from the analysis
  2283. if hasattr(self, 'results') and 'lesion_burden' in self.results:
  2284. lesion_results = self.results['lesion_burden']
  2285. comparison_rows = []
  2286. for metric_type in ['area_analysis', 'ratio_analysis']:
  2287. metric_name = 'Area' if metric_type == 'area_analysis' else 'Ratio'
  2288. unit = 'mm²' if metric_type == 'area_analysis' else '%'
  2289. analysis = lesion_results[metric_type]
  2290. # Extract basic statistics
  2291. hc_mean, hc_std = analysis['hc_stats']
  2292. ms_mean, ms_std = analysis['ms_stats']
  2293. # Extract test results
  2294. comparison = analysis['comparison']
  2295. test_type = analysis['test_type']
  2296. normality_info = analysis.get('normality_info', {})
  2297. effect_size_info = analysis.get('effect_size', (None, None))
  2298. row = {
  2299. 'Metric': f'{metric_name} ({unit})',
  2300. 'HC_Mean': f"{hc_mean:.2f}" if not np.isnan(hc_mean) else 'N/A',
  2301. 'HC_SD': f"{hc_std:.2f}" if not np.isnan(hc_std) else 'N/A',
  2302. 'MS_Mean': f"{ms_mean:.2f}" if not np.isnan(ms_mean) else 'N/A',
  2303. 'MS_SD': f"{ms_std:.2f}" if not np.isnan(ms_std) else 'N/A',
  2304. 'Test_Type': test_type if test_type else 'N/A',
  2305. 'Test_Statistic': f"{comparison.statistic:.3f}" if comparison else 'N/A',
  2306. 'P_Value': f"{comparison.pvalue}" if comparison else 'N/A',
  2307. 'Effect_Size': f"{effect_size_info[0]:.3f}" if effect_size_info[0] is not None else 'N/A',
  2308. 'Effect_Size_Type': 'Cohen_d' if test_type == 'parametric' else 'r',
  2309. 'HC_Normality_p': f"{normality_info.get('group1_shapiro_p', np.nan):.3f}" if 'group1_shapiro_p' in normality_info else 'N/A',
  2310. 'MS_Normality_p': f"{normality_info.get('group2_shapiro_p', np.nan):.3f}" if 'group2_shapiro_p' in normality_info else 'N/A',
  2311. 'Normality_Passed': 'Yes' if test_type == 'parametric' else 'No' if test_type == 'non_parametric' else 'N/A'
  2312. }
  2313. comparison_rows.append(row)
  2314. comparison_df = pd.DataFrame(comparison_rows)
  2315. print("\nStatistical Comparison Results:")
  2316. print("-" * 100)
  2317. print(comparison_df.to_string(index=False))
  2318. # Save to CSV
  2319. comparison_df.to_csv(os.path.join(config.OUTPUT_DIR, 'lesion_statistical_comparisons.csv'),
  2320. index=False)
  2321. print(f"\nStatistical comparisons saved to: lesion_statistical_comparisons.csv")
  2322. # Table 3: Plot data (means used for visualization)
  2323. print(f"\n{'=' * 80}")
  2324. print("TABLE 3: PLOT DATA (MEANS BY AGE GROUP)")
  2325. print(f"{'=' * 80}")
  2326. for metric_name in ['area', 'ratio', 'ratio_skull']:
  2327. unit = 'mm²' if metric_name == 'area' else '%'
  2328. if metric_name == 'area':
  2329. metric_title = 'WMH Area'
  2330. elif metric_name == 'ratio':
  2331. metric_title = 'WMH Ratio'
  2332. else:
  2333. metric_title = 'WMH Ratio-Skull'
  2334. print(f"\n{metric_title} - Mean Values and Contributions Used in Plot ({unit}):")
  2335. print("-" * 70)
  2336. # Create enhanced plot data table
  2337. plot_rows = []
  2338. for i, age_label in enumerate(self.config.AGE_LABELS):
  2339. age_center = table_data['plot_data']['HC'][metric_name]['age_centers'][i]
  2340. # HC data
  2341. hc_male_mean = table_data['plot_data']['HC'][metric_name]['male_means'][i]
  2342. hc_female_mean = table_data['plot_data']['HC'][metric_name]['female_means'][i]
  2343. hc_combined = table_data['plot_data']['HC'][metric_name]['combined_means'][i]
  2344. hc_male_contrib = table_data['plot_data']['HC'][metric_name]['male_contributions'][i]
  2345. hc_female_contrib = table_data['plot_data']['HC'][metric_name]['female_contributions'][i]
  2346. # MS data
  2347. ms_male_mean = table_data['plot_data']['MS'][metric_name]['male_means'][i]
  2348. ms_female_mean = table_data['plot_data']['MS'][metric_name]['female_means'][i]
  2349. ms_combined = table_data['plot_data']['MS'][metric_name]['combined_means'][i]
  2350. ms_male_contrib = table_data['plot_data']['MS'][metric_name]['male_contributions'][i]
  2351. ms_female_contrib = table_data['plot_data']['MS'][metric_name]['female_contributions'][i]
  2352. row = {
  2353. 'Age_Group': age_label,
  2354. 'Age_Center': f"{age_center:.1f}",
  2355. 'HC_Male_Mean': f"{hc_male_mean:.2f}",
  2356. 'HC_Female_Mean': f"{hc_female_mean:.2f}",
  2357. 'HC_Combined_Mean': f"{hc_combined:.2f}",
  2358. 'HC_Male_Contribution': f"{hc_male_contrib:.2f}",
  2359. 'HC_Female_Contribution': f"{hc_female_contrib:.2f}",
  2360. 'MS_Male_Mean': f"{ms_male_mean:.2f}",
  2361. 'MS_Female_Mean': f"{ms_female_mean:.2f}",
  2362. 'MS_Combined_Mean': f"{ms_combined:.2f}",
  2363. 'MS_Male_Contribution': f"{ms_male_contrib:.2f}",
  2364. 'MS_Female_Contribution': f"{ms_female_contrib:.2f}",
  2365. 'HC_Male_N': table_data['plot_data']['HC'][metric_name]['male_counts'][i],
  2366. 'HC_Female_N': table_data['plot_data']['HC'][metric_name]['female_counts'][i],
  2367. 'MS_Male_N': table_data['plot_data']['MS'][metric_name]['male_counts'][i],
  2368. 'MS_Female_N': table_data['plot_data']['MS'][metric_name]['female_counts'][i]
  2369. }
  2370. plot_rows.append(row)
  2371. plot_df = pd.DataFrame(plot_rows)
  2372. print(plot_df.to_string(index=False))
  2373. # Save to CSV
  2374. filename = f'lesion_{metric_name}_plot_data.csv'
  2375. plot_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  2376. print(f"Enhanced plot data saved to: {filename}")
  2377. # Table 4: Contribution Analysis (NEW)
  2378. print(f"\n{'=' * 80}")
  2379. print("TABLE 4: PROPORTIONAL CONTRIBUTION ANALYSIS")
  2380. print(f"{'=' * 80}")
  2381. for metric_name in ['area', 'ratio']:
  2382. unit = 'mm²' if metric_name == 'area' else '%'
  2383. metric_title = 'Lesion Area' if metric_name == 'area' else 'Lesion Ratio'
  2384. print(f"\n{metric_title} - Proportional Contributions ({unit}):")
  2385. print("-" * 70)
  2386. # Create contribution analysis table
  2387. contrib_rows = []
  2388. for group in ['HC', 'MS']:
  2389. for i, age_label in enumerate(self.config.AGE_LABELS):
  2390. male_contrib = table_data['plot_data'][group][metric_name]['male_contributions'][i]
  2391. female_contrib = table_data['plot_data'][group][metric_name]['female_contributions'][i]
  2392. combined_mean = table_data['plot_data'][group][metric_name]['combined_means'][i]
  2393. male_count = table_data['plot_data'][group][metric_name]['male_counts'][i]
  2394. female_count = table_data['plot_data'][group][metric_name]['female_counts'][i]
  2395. total_count = male_count + female_count
  2396. # Calculate proportions
  2397. if combined_mean > 0:
  2398. male_prop = (male_contrib / combined_mean) * 100 if combined_mean > 0 else 0
  2399. female_prop = (female_contrib / combined_mean) * 100 if combined_mean > 0 else 0
  2400. else:
  2401. male_prop = 0
  2402. female_prop = 0
  2403. sample_male_prop = (male_count / total_count) * 100 if total_count > 0 else 0
  2404. sample_female_prop = (female_count / total_count) * 100 if total_count > 0 else 0
  2405. row = {
  2406. 'Group': group,
  2407. 'Age_Group': age_label,
  2408. 'Combined_Mean': f"{combined_mean:.2f}",
  2409. 'Male_Contribution': f"{male_contrib:.2f}",
  2410. 'Female_Contribution': f"{female_contrib:.2f}",
  2411. 'Male_Prop_of_Mean': f"{male_prop:.1f}%",
  2412. 'Female_Prop_of_Mean': f"{female_prop:.1f}%",
  2413. 'Male_Sample_Prop': f"{sample_male_prop:.1f}%",
  2414. 'Female_Sample_Prop': f"{sample_female_prop:.1f}%",
  2415. 'Total_N': total_count
  2416. }
  2417. contrib_rows.append(row)
  2418. contrib_df = pd.DataFrame(contrib_rows)
  2419. print(contrib_df.to_string(index=False))
  2420. # Save to CSV
  2421. filename = f'lesion_{metric_name}_contributions.csv'
  2422. contrib_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  2423. print(f"Contribution analysis saved to: {filename}")
  2424. # Table 5: Effect Size Interpretation (NEW)
  2425. if hasattr(self, 'results') and 'lesion_burden' in self.results:
  2426. print(f"\n{'=' * 80}")
  2427. print("TABLE 5: EFFECT SIZE INTERPRETATION GUIDE")
  2428. print(f"{'=' * 80}")
  2429. effect_size_rows = []
  2430. lesion_results = self.results['lesion_burden']
  2431. for metric_type in ['area_analysis', 'ratio_analysis']:
  2432. metric_name = 'Area' if metric_type == 'area_analysis' else 'Ratio'
  2433. analysis = lesion_results[metric_type]
  2434. effect_size_info = analysis.get('effect_size', (None, None))
  2435. test_type = analysis['test_type']
  2436. if effect_size_info[0] is not None:
  2437. effect_size = effect_size_info[0]
  2438. if test_type == 'parametric':
  2439. # Cohen's d interpretation
  2440. if abs(effect_size) < 0.2:
  2441. magnitude = "Negligible"
  2442. elif abs(effect_size) < 0.5:
  2443. magnitude = "Small"
  2444. elif abs(effect_size) < 0.8:
  2445. magnitude = "Medium"
  2446. else:
  2447. magnitude = "Large"
  2448. else:
  2449. # Effect size r interpretation
  2450. if abs(effect_size) < 0.1:
  2451. magnitude = "Negligible"
  2452. elif abs(effect_size) < 0.3:
  2453. magnitude = "Small"
  2454. elif abs(effect_size) < 0.5:
  2455. magnitude = "Medium"
  2456. else:
  2457. magnitude = "Large"
  2458. row = {
  2459. 'Metric': metric_name,
  2460. 'Effect_Size_Value': f"{effect_size:.3f}",
  2461. 'Effect_Size_Type': "Cohen's d" if test_type == 'parametric' else "Rank-biserial correlation (r)",
  2462. 'Magnitude': magnitude,
  2463. 'Interpretation': f"{magnitude} effect size indicating {'substantial' if magnitude in ['Medium', 'Large'] else 'minimal'} practical difference"
  2464. }
  2465. effect_size_rows.append(row)
  2466. if effect_size_rows:
  2467. effect_df = pd.DataFrame(effect_size_rows)
  2468. print("\nEffect Size Interpretations:")
  2469. print("-" * 60)
  2470. print(effect_df.to_string(index=False))
  2471. # Save to CSV
  2472. effect_df.to_csv(os.path.join(config.OUTPUT_DIR, 'lesion_effect_sizes.csv'), index=False)
  2473. print(f"\nEffect size interpretations saved to: lesion_effect_sizes.csv")
  2474. # Table 6: Stacked area values (cumulative for visualization)
  2475. print(f"\n{'=' * 80}")
  2476. print("TABLE 6: STACKED AREA VALUES (FOR AREA CHART)")
  2477. print(f"{'=' * 80}")
  2478. for metric_name in ['area', 'ratio', 'ratio_skull']:
  2479. unit = 'mm²' if metric_name == 'area' else '%'
  2480. if metric_name == 'area':
  2481. metric_title = 'WMH Area'
  2482. elif metric_name == 'ratio':
  2483. metric_title = 'WMH Ratio'
  2484. else:
  2485. metric_title = 'WMH Ratio-Skull'
  2486. print(f"\n{metric_title} - Stacked Values ({unit}):")
  2487. print("-" * 70)
  2488. # Create stacked data table
  2489. stacked_rows = []
  2490. for group in ['HC', 'MS']:
  2491. for i, age_label in enumerate(self.config.AGE_LABELS):
  2492. male_mean = table_data['plot_data'][group][metric_name]['male_means'][i]
  2493. female_mean = table_data['plot_data'][group][metric_name]['female_means'][i]
  2494. row = {
  2495. 'Group': group,
  2496. 'Age Group': age_label,
  2497. 'Male Layer (0 to Male)': f"0.00 to {male_mean:.2f}",
  2498. 'Female Layer (Male to Total)': f"{male_mean:.2f} to {male_mean + female_mean:.2f}",
  2499. 'Total Height': f"{male_mean + female_mean:.2f}",
  2500. 'Male Contribution': f"{male_mean:.2f}",
  2501. 'Female Contribution': f"{female_mean:.2f}"
  2502. }
  2503. stacked_rows.append(row)
  2504. stacked_df = pd.DataFrame(stacked_rows)
  2505. print(stacked_df.to_string(index=False))
  2506. # Save to CSV
  2507. filename = f'lesion_{metric_name}_stacked_data.csv'
  2508. stacked_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  2509. print(f"Stacked data saved to: {filename}")
  2510. print(f"\n{'=' * 80}")
  2511. print("All lesion burden tables have been generated and saved to CSV files!")
  2512. print(f"{'=' * 80}")
  2513. print("\nGenerated Files Summary:")
  2514. print("- lesion_area_detailed_stats.csv (Complete descriptive statistics)")
  2515. print("- lesion_ratio_detailed_stats.csv (Complete descriptive statistics)")
  2516. print("- lesion_area_plot_data.csv (Data used for visualization)")
  2517. print("- lesion_ratio_plot_data.csv (Data used for visualization)")
  2518. print("- lesion_area_stacked_data.csv (Stacked area chart values)")
  2519. print("- lesion_ratio_stacked_data.csv (Stacked area chart values)")
  2520. print("- lesion_area_robust_stats.csv (Non-parametric robust measures)")
  2521. print("- lesion_ratio_robust_stats.csv (Non-parametric robust measures)")
  2522. print("- lesion_burden_analysis_documentation.txt (Comprehensive documentation)")
  2523. print("ENHANCED STATISTICAL TABLES GENERATED!")
  2524. print("- total_lesion_burden_analysis.png (Figure)")
  2525. print("• Enhanced descriptive statistics with IQR")
  2526. print("• Comprehensive statistical comparison results")
  2527. print("• Detailed plot data with contributions")
  2528. print("• Proportional contribution analysis")
  2529. print("• Effect size interpretations")
  2530. print("• All tables saved as CSV files for further analysis")
  2531. def assess_normality(self, data, variable_name="", group_name="", alpha=0.05):
  2532. """
  2533. Comprehensive normality assessment with multiple criteria
  2534. """
  2535. import scipy.stats as stats
  2536. import numpy as np
  2537. if len(data) < 3:
  2538. return False, {"reason": "Insufficient data", "n": len(data)}
  2539. # Remove NaN values
  2540. clean_data = data.dropna() if hasattr(data, 'dropna') else data[~np.isnan(data)]
  2541. if len(clean_data) < 3:
  2542. return False, {"reason": "Insufficient valid data after removing NaN", "n": len(clean_data)}
  2543. # Shapiro-Wilk test
  2544. shapiro_stat, shapiro_p = stats.shapiro(clean_data)
  2545. # Skewness and kurtosis
  2546. skewness = stats.skew(clean_data)
  2547. kurtosis_val = stats.kurtosis(clean_data)
  2548. # Multiple criteria for normality
  2549. shapiro_normal = shapiro_p > alpha
  2550. skew_normal = abs(skewness) < 2
  2551. kurtosis_normal = abs(kurtosis_val) < 7
  2552. is_normal = shapiro_normal and skew_normal and kurtosis_normal
  2553. assessment = {
  2554. 'shapiro_statistic': shapiro_stat,
  2555. 'shapiro_p': shapiro_p,
  2556. 'shapiro_normal': shapiro_normal,
  2557. 'skewness': skewness,
  2558. 'skew_normal': skew_normal,
  2559. 'kurtosis': kurtosis_val,
  2560. 'kurtosis_normal': kurtosis_normal,
  2561. 'n_samples': len(clean_data),
  2562. 'variable': variable_name,
  2563. 'group': group_name
  2564. }
  2565. return is_normal, assessment
  2566. def standardized_group_comparison(self, group1_data, group2_data, group1_name="Group1", group2_name="Group2",
  2567. variable_name=""):
  2568. """
  2569. Standardized approach for group comparisons with automatic test selection
  2570. """
  2571. import scipy.stats as stats
  2572. import numpy as np
  2573. # Clean data
  2574. g1_clean = group1_data.dropna() if hasattr(group1_data, 'dropna') else group1_data[~np.isnan(group1_data)]
  2575. g2_clean = group2_data.dropna() if hasattr(group2_data, 'dropna') else group2_data[~np.isnan(group2_data)]
  2576. if len(g1_clean) < 2 or len(g2_clean) < 2:
  2577. return None, {"error": "Insufficient data for comparison"}
  2578. # Test normality for both groups
  2579. g1_normal, g1_assessment = self.assess_normality(g1_clean, variable_name, group1_name)
  2580. g2_normal, g2_assessment = self.assess_normality(g2_clean, variable_name, group2_name)
  2581. # Choose appropriate test
  2582. if g1_normal and g2_normal:
  2583. # Use parametric test
  2584. test_stat, p_value = stats.ttest_ind(g1_clean, g2_clean)
  2585. # Calculate Cohen's d
  2586. pooled_std = np.sqrt(((len(g1_clean) - 1) * np.var(g1_clean, ddof=1) +
  2587. (len(g2_clean) - 1) * np.var(g2_clean, ddof=1)) /
  2588. (len(g1_clean) + len(g2_clean) - 2))
  2589. cohens_d = (np.mean(g1_clean) - np.mean(g2_clean)) / pooled_std
  2590. result = {
  2591. 'test_type': 'Independent t-test',
  2592. 'test_statistic': test_stat,
  2593. 'p_value': p_value,
  2594. 'effect_size_type': "Cohen's d",
  2595. 'effect_size': cohens_d,
  2596. 'parametric': True,
  2597. 'group1_stats': {
  2598. 'mean': np.mean(g1_clean),
  2599. 'std': np.std(g1_clean, ddof=1),
  2600. 'n': len(g1_clean)
  2601. },
  2602. 'group2_stats': {
  2603. 'mean': np.mean(g2_clean),
  2604. 'std': np.std(g2_clean, ddof=1),
  2605. 'n': len(g2_clean)
  2606. }
  2607. }
  2608. else:
  2609. # Use non-parametric test
  2610. test_stat, p_value = stats.mannwhitneyu(g1_clean, g2_clean, alternative='two-sided')
  2611. # Rank-biserial correlation for Mann-Whitney U (Reviewer 2, Minor Comment 4).
  2612. # r ≈ Z / √N — large-sample approximation of rank-biserial correlation.
  2613. n_total = len(g1_clean) + len(g2_clean)
  2614. z_score = stats.norm.ppf(1 - p_value / 2) if p_value > 0 else 0
  2615. effect_size_r = abs(z_score) / np.sqrt(n_total)
  2616. result = {
  2617. 'test_type': 'Mann-Whitney U test',
  2618. 'test_statistic': test_stat,
  2619. 'p_value': p_value,
  2620. 'effect_size_type': 'Rank-biserial correlation (r)',
  2621. 'effect_size': effect_size_r,
  2622. 'parametric': False,
  2623. 'group1_stats': {
  2624. 'median': np.median(g1_clean),
  2625. 'q25': np.percentile(g1_clean, 25),
  2626. 'q75': np.percentile(g1_clean, 75),
  2627. 'n': len(g1_clean)
  2628. },
  2629. 'group2_stats': {
  2630. 'median': np.median(g2_clean),
  2631. 'q25': np.percentile(g2_clean, 25),
  2632. 'q75': np.percentile(g2_clean, 75),
  2633. 'n': len(g2_clean)
  2634. }
  2635. }
  2636. # Add normality assessment details
  2637. result['normality_assessment'] = {
  2638. 'group1': g1_assessment,
  2639. 'group2': g2_assessment,
  2640. 'both_normal': g1_normal and g2_normal
  2641. }
  2642. return result, None
  2643. def ms_subgroup_analysis(self):
  2644. """Analyze MS lesion subtypes with detailed stratification - both area and ratio"""
  2645. print("\n" + "=" * 60)
  2646. print("MS SUBGROUP LESION ANALYSIS")
  2647. print("=" * 60)
  2648. # Filter MS patients only
  2649. ms_data = self.data[self.data[self.config.COLUMNS['group']] == 'MS'].copy()
  2650. # Create 3x2 subplot layout
  2651. fig, axes = plt.subplots(3, 2, figsize=(16, 18))
  2652. fig.patch.set_facecolor('white') # Ensure white background
  2653. age_centers = [np.mean(age_range) for age_range in self.config.AGE_BINS]
  2654. # Analysis for: All MS, Female MS, Male MS
  2655. subgroups = [
  2656. ('All MS Patients', ms_data),
  2657. ('Female MS Patients', ms_data[ms_data['Gender'] == 'Female']),
  2658. ('Male MS Patients', ms_data[ms_data['Gender'] == 'Male'])
  2659. ]
  2660. # Metrics to plot: absolute area (left column) and normalized ratio (right column)
  2661. metrics = {
  2662. 'area': {
  2663. 'columns': {
  2664. 'pewmh': self.config.COLUMNS.get('peri_wmh', 'peri_wmh'), # Absolute area columns
  2665. 'dwmh': self.config.COLUMNS.get('deep_wmh', 'deep_wmh'),
  2666. 'jcwmh': self.config.COLUMNS.get('juxta_wmh', 'juxta_wmh')
  2667. },
  2668. 'ylabel': 'WMH Subtype Area (mm²)',
  2669. 'title_suffix': 'WMH Subtype Areas'
  2670. },
  2671. 'ratio': {
  2672. 'columns': {
  2673. 'pewmh': 'peri_wmh_ratio', # Ratio columns
  2674. 'dwmh': 'deep_wmh_ratio',
  2675. 'jcwmh': 'juxta_wmh_ratio'
  2676. },
  2677. 'ylabel': 'WMH Subtype Ratio (%)',
  2678. 'title_suffix': 'WMH Subtype Ratios'
  2679. }
  2680. }
  2681. # Dictionary to store all table data
  2682. table_data = {
  2683. 'detailed_stats': {},
  2684. 'plot_data': {},
  2685. 'gender_comparisons': {},
  2686. 'metadata': {
  2687. 'age_bins': self.config.AGE_BINS,
  2688. 'age_labels': self.config.AGE_LABELS,
  2689. 'age_centers': age_centers,
  2690. 'subgroups': [name for name, _ in subgroups],
  2691. 'metrics': metrics,
  2692. 'colors': self.config.COLORS,
  2693. 'lesion_subtypes': ['PEWMH', 'DWMH', 'JCWMH']
  2694. }
  2695. }
  2696. for row_idx, (group_title, data) in enumerate(subgroups):
  2697. # Initialize group data in tables
  2698. table_data['detailed_stats'][group_title] = {}
  2699. table_data['plot_data'][group_title] = {}
  2700. for col_idx, (metric_type, metric_info) in enumerate(
  2701. [('area', metrics['area']), ('ratio', metrics['ratio'])]):
  2702. ax = axes[row_idx, col_idx]
  2703. # Initialize metric data in tables
  2704. table_data['detailed_stats'][group_title][metric_type] = {}
  2705. table_data['plot_data'][group_title][metric_type] = {
  2706. 'age_centers': age_centers.copy(),
  2707. 'age_labels': self.config.AGE_LABELS.copy(),
  2708. 'pewmh_means': [],
  2709. 'dwmh_means': [],
  2710. 'jcwmh_means': [],
  2711. 'pewmh_stds': [],
  2712. 'dwmh_stds': [],
  2713. 'jcwmh_stds': [],
  2714. 'pewmh_counts': [],
  2715. 'dwmh_counts': [],
  2716. 'jcwmh_counts': []
  2717. }
  2718. # Prepare data for three-layer stacked area plot
  2719. pewmh_means = []
  2720. dwmh_means = []
  2721. jcwmh_means = []
  2722. pewmh_stds = []
  2723. dwmh_stds = []
  2724. jcwmh_stds = []
  2725. pewmh_counts = []
  2726. dwmh_counts = []
  2727. jcwmh_counts = []
  2728. for age_idx, age_label in enumerate(self.config.AGE_LABELS):
  2729. age_group_data = data[data['AgeGroup'] == age_label]
  2730. # Calculate statistics for each lesion subtype
  2731. pewmh_data = age_group_data[metric_info['columns']['pewmh']].dropna()
  2732. dwmh_data = age_group_data[metric_info['columns']['dwmh']].dropna()
  2733. jcwmh_data = age_group_data[metric_info['columns']['jcwmh']].dropna()
  2734. # Means for plotting
  2735. pewmh_mean = pewmh_data.mean() if len(pewmh_data) > 0 else 0
  2736. dwmh_mean = dwmh_data.mean() if len(dwmh_data) > 0 else 0
  2737. jcwmh_mean = jcwmh_data.mean() if len(jcwmh_data) > 0 else 0
  2738. # Standard deviations
  2739. pewmh_std = pewmh_data.std() if len(pewmh_data) > 0 else 0
  2740. dwmh_std = dwmh_data.std() if len(dwmh_data) > 0 else 0
  2741. jcwmh_std = jcwmh_data.std() if len(jcwmh_data) > 0 else 0
  2742. # Sample counts
  2743. pewmh_count = len(pewmh_data)
  2744. dwmh_count = len(dwmh_data)
  2745. jcwmh_count = len(jcwmh_data)
  2746. # Store for plotting
  2747. pewmh_means.append(pewmh_mean)
  2748. dwmh_means.append(dwmh_mean)
  2749. jcwmh_means.append(jcwmh_mean)
  2750. pewmh_stds.append(pewmh_std)
  2751. dwmh_stds.append(dwmh_std)
  2752. jcwmh_stds.append(jcwmh_std)
  2753. pewmh_counts.append(pewmh_count)
  2754. dwmh_counts.append(dwmh_count)
  2755. jcwmh_counts.append(jcwmh_count)
  2756. # Store detailed statistics for tables
  2757. if age_label not in table_data['detailed_stats'][group_title][metric_type]:
  2758. table_data['detailed_stats'][group_title][metric_type][age_label] = {}
  2759. for lesion_type, lesion_data in [('PEWMH', pewmh_data), ('DWMH', dwmh_data),
  2760. ('JCWMH', jcwmh_data)]:
  2761. table_data['detailed_stats'][group_title][metric_type][age_label][lesion_type] = {
  2762. 'count': len(lesion_data),
  2763. 'mean': lesion_data.mean() if len(lesion_data) > 0 else np.nan,
  2764. 'std': lesion_data.std() if len(lesion_data) > 0 else np.nan,
  2765. 'min': lesion_data.min() if len(lesion_data) > 0 else np.nan,
  2766. 'max': lesion_data.max() if len(lesion_data) > 0 else np.nan,
  2767. 'median': lesion_data.median() if len(lesion_data) > 0 else np.nan,
  2768. 'q25': lesion_data.quantile(0.25) if len(lesion_data) > 0 else np.nan,
  2769. 'q75': lesion_data.quantile(0.75) if len(lesion_data) > 0 else np.nan
  2770. }
  2771. # Store plot data
  2772. table_data['plot_data'][group_title][metric_type]['pewmh_means'] = pewmh_means
  2773. table_data['plot_data'][group_title][metric_type]['dwmh_means'] = dwmh_means
  2774. table_data['plot_data'][group_title][metric_type]['jcwmh_means'] = jcwmh_means
  2775. table_data['plot_data'][group_title][metric_type]['pewmh_stds'] = pewmh_stds
  2776. table_data['plot_data'][group_title][metric_type]['dwmh_stds'] = dwmh_stds
  2777. table_data['plot_data'][group_title][metric_type]['jcwmh_stds'] = jcwmh_stds
  2778. table_data['plot_data'][group_title][metric_type]['pewmh_counts'] = pewmh_counts
  2779. table_data['plot_data'][group_title][metric_type]['dwmh_counts'] = dwmh_counts
  2780. table_data['plot_data'][group_title][metric_type]['jcwmh_counts'] = jcwmh_counts
  2781. # Create three-layer stacked area plot
  2782. ax.fill_between(age_centers, 0, pewmh_means,
  2783. color=self.config.COLORS['pewmh'], alpha=0.8, label='PEWMH')
  2784. ax.fill_between(age_centers, pewmh_means,
  2785. np.array(pewmh_means) + np.array(dwmh_means),
  2786. color=self.config.COLORS['dwmh'], alpha=0.8, label='DWMH')
  2787. ax.fill_between(age_centers, np.array(pewmh_means) + np.array(dwmh_means),
  2788. np.array(pewmh_means) + np.array(dwmh_means) + np.array(jcwmh_means),
  2789. color=self.config.COLORS['jcwmh'], alpha=0.8, label='JCWMH')
  2790. # Formatting
  2791. # Calculate panel letter (A through F for 3x2 layout)
  2792. panel_idx = row_idx * 2 + col_idx
  2793. panel_letter = chr(65 + panel_idx) # 65 is ASCII for 'A'
  2794. ax.set_title(f'{panel_letter}. {group_title} - {metric_info["title_suffix"]}', fontsize=18, fontweight='bold')
  2795. # ax.set_title(f'{group_title} - {metric_info["title_suffix"]}', fontsize=14, fontweight='bold')
  2796. ax.set_xlabel('Age (years)', fontsize=16)
  2797. ax.set_ylabel(metric_info['ylabel'], fontsize=16)
  2798. ax.legend(loc='upper right', fontsize=15) #, frameon=True, fancybox=True, shadow=True)
  2799. ax.grid(True, alpha=0.3)
  2800. ax.set_xticks(age_centers)
  2801. ax.set_xticklabels(self.config.AGE_LABELS, fontsize=16)
  2802. plt.tight_layout()
  2803. plt.savefig(os.path.join(config.OUTPUT_DIR, 'ms_subgroup_analysis.png'),
  2804. dpi=self.config.DPI, bbox_inches='tight', facecolor='white')
  2805. # Generate comprehensive documentation
  2806. self._generate_ms_subgroup_documentation(table_data)
  2807. # Generate and save tables
  2808. self._generate_ms_subgroup_tables(table_data)
  2809. # Statistical analysis for both area and ratio metrics
  2810. print(f"\n{'=' * 50}")
  2811. print("MS LESION SUBTYPE STATISTICS")
  2812. print(f"{'=' * 50}")
  2813. # Analysis for absolute areas
  2814. print(f"\nMS Lesion Subtype Areas (mm²):")
  2815. area_columns = [
  2816. ('PEWMH', metrics['area']['columns']['pewmh']),
  2817. ('DWMH', metrics['area']['columns']['dwmh']),
  2818. ('JCWMH', metrics['area']['columns']['jcwmh'])
  2819. ]
  2820. for subtype_name, column in area_columns:
  2821. if column in ms_data.columns:
  2822. subtype_data = ms_data[column].dropna()
  2823. if len(subtype_data) > 0:
  2824. print(
  2825. f"{subtype_name}: median [IQR] = {subtype_data.median():.2f} [{subtype_data.quantile(0.25):.2f}-{subtype_data.quantile(0.75):.2f}] mm²")
  2826. else:
  2827. print(f"{subtype_name}: No valid data available")
  2828. else:
  2829. print(f"{subtype_name}: Column '{column}' not found in data")
  2830. # Analysis for ratios
  2831. print(f"\nMS Lesion Subtype Ratios (%):")
  2832. ratio_columns = [
  2833. ('PEWMH', 'peri_wmh_ratio'),
  2834. ('DWMH', 'deep_wmh_ratio'),
  2835. ('JCWMH', 'juxta_wmh_ratio')
  2836. ]
  2837. for subtype_name, column in ratio_columns:
  2838. subtype_data = ms_data[column].dropna()
  2839. print(
  2840. f"{subtype_name}: median [IQR] = {subtype_data.median():.3f} [{subtype_data.quantile(0.25):.3f}-{subtype_data.quantile(0.75):.3f}]%")
  2841. print(f"\n{'=' * 50}")
  2842. print("GENDER COMPARISON WITH STANDARDIZED STATISTICAL APPROACH")
  2843. print(f"{'=' * 50}")
  2844. gender_comparisons = {}
  2845. # Area comparisons with standardized approach
  2846. print(f"\nArea Comparisons (mm²) - Standardized Statistical Testing:")
  2847. for subtype_name, column in area_columns:
  2848. if column in ms_data.columns:
  2849. male_data = ms_data[ms_data['Gender'] == 'Male'][column].dropna()
  2850. female_data = ms_data[ms_data['Gender'] == 'Female'][column].dropna()
  2851. if len(male_data) > 0 and len(female_data) > 0:
  2852. # Use standardized comparison
  2853. comparison_result, error = self.standardized_group_comparison(
  2854. male_data, female_data, "Male", "Female", f"{subtype_name} Area"
  2855. )
  2856. if comparison_result:
  2857. print(f"\n{subtype_name} - {comparison_result['test_type']}:")
  2858. if comparison_result['parametric']:
  2859. print(
  2860. f" Male mean ± SD: {comparison_result['group1_stats']['mean']:.2f} ± {comparison_result['group1_stats']['std']:.2f} mm² (n={comparison_result['group1_stats']['n']})")
  2861. print(
  2862. f" Female mean ± SD: {comparison_result['group2_stats']['mean']:.2f} ± {comparison_result['group2_stats']['std']:.2f} mm² (n={comparison_result['group2_stats']['n']})")
  2863. else:
  2864. print(
  2865. f" Male median [IQR]: {comparison_result['group1_stats']['median']:.2f} [{comparison_result['group1_stats']['q25']:.2f}-{comparison_result['group1_stats']['q75']:.2f}] mm² (n={comparison_result['group1_stats']['n']})")
  2866. print(
  2867. f" Female median [IQR]: {comparison_result['group2_stats']['median']:.2f} [{comparison_result['group2_stats']['q25']:.2f}-{comparison_result['group2_stats']['q75']:.2f}] mm² (n={comparison_result['group2_stats']['n']})")
  2868. print(f" Test statistic: {comparison_result['test_statistic']:.3f}")
  2869. print(f" P-value: {comparison_result['p_value']}")
  2870. print(f" {comparison_result['effect_size_type']}: {comparison_result['effect_size']:.3f}")
  2871. # Normality test results
  2872. g1_normal = comparison_result['normality_assessment']['group1']['shapiro_normal']
  2873. g2_normal = comparison_result['normality_assessment']['group2']['shapiro_normal']
  2874. print(
  2875. f" Normality: Male p={comparison_result['normality_assessment']['group1']['shapiro_p']:.3f} ({'Normal' if g1_normal else 'Non-normal'}), "
  2876. f"Female p={comparison_result['normality_assessment']['group2']['shapiro_p']:.3f} ({'Normal' if g2_normal else 'Non-normal'})")
  2877. if subtype_name not in gender_comparisons:
  2878. gender_comparisons[subtype_name] = {}
  2879. gender_comparisons[subtype_name]['area'] = comparison_result
  2880. else:
  2881. print(f"{subtype_name}: {error}")
  2882. # Ratio comparisons with standardized approach
  2883. print(f"\nRatio Comparisons (%) - Standardized Statistical Testing:")
  2884. for subtype_name, column in ratio_columns:
  2885. male_data = ms_data[ms_data['Gender'] == 'Male'][column].dropna()
  2886. female_data = ms_data[ms_data['Gender'] == 'Female'][column].dropna()
  2887. if len(male_data) > 0 and len(female_data) > 0:
  2888. # Use standardized comparison
  2889. comparison_result, error = self.standardized_group_comparison(
  2890. male_data, female_data, "Male", "Female", f"{subtype_name} Ratio"
  2891. )
  2892. if comparison_result:
  2893. print(f"\n{subtype_name} - {comparison_result['test_type']}:")
  2894. if comparison_result['parametric']:
  2895. print(
  2896. f" Male mean ± SD: {comparison_result['group1_stats']['mean']:.3f} ± {comparison_result['group1_stats']['std']:.3f}% (n={comparison_result['group1_stats']['n']})")
  2897. print(
  2898. f" Female mean ± SD: {comparison_result['group2_stats']['mean']:.3f} ± {comparison_result['group2_stats']['std']:.3f}% (n={comparison_result['group2_stats']['n']})")
  2899. else:
  2900. print(
  2901. f" Male median [IQR]: {comparison_result['group1_stats']['median']:.3f} [{comparison_result['group1_stats']['q25']:.3f}-{comparison_result['group1_stats']['q75']:.3f}]% (n={comparison_result['group1_stats']['n']})")
  2902. print(
  2903. f" Female median [IQR]: {comparison_result['group2_stats']['median']:.3f} [{comparison_result['group2_stats']['q25']:.3f}-{comparison_result['group2_stats']['q75']:.3f}]% (n={comparison_result['group2_stats']['n']})")
  2904. print(f" Test statistic: {comparison_result['test_statistic']:.3f}")
  2905. print(f" P-value: {comparison_result['p_value']}")
  2906. print(f" {comparison_result['effect_size_type']}: {comparison_result['effect_size']:.3f}")
  2907. # Normality test results
  2908. g1_normal = comparison_result['normality_assessment']['group1']['shapiro_normal']
  2909. g2_normal = comparison_result['normality_assessment']['group2']['shapiro_normal']
  2910. print(
  2911. f" Normality: Male p={comparison_result['normality_assessment']['group1']['shapiro_p']:.3f} ({'Normal' if g1_normal else 'Non-normal'}), "
  2912. f"Female p={comparison_result['normality_assessment']['group2']['shapiro_p']:.3f} ({'Normal' if g2_normal else 'Non-normal'})")
  2913. if subtype_name not in gender_comparisons:
  2914. gender_comparisons[subtype_name] = {}
  2915. gender_comparisons[subtype_name]['ratio'] = comparison_result
  2916. else:
  2917. print(f"{subtype_name}: {error}")
  2918. # Store gender comparison data in table_data
  2919. table_data['gender_comparisons'] = gender_comparisons
  2920. # Store comprehensive results
  2921. self.results['ms_subtypes'] = {
  2922. 'area_analysis': {
  2923. subtype: {
  2924. 'all_stats': (ms_data[column].dropna().median(),
  2925. ms_data[column].dropna().quantile(0.25),
  2926. ms_data[column].dropna().quantile(0.75)) if column in ms_data.columns and len(
  2927. ms_data[column].dropna()) > 0 else None
  2928. } for subtype, column in area_columns
  2929. },
  2930. 'ratio_analysis': {
  2931. subtype: {
  2932. 'all_stats': (ms_data[column].dropna().median(),
  2933. ms_data[column].dropna().quantile(0.25),
  2934. ms_data[column].dropna().quantile(0.75))
  2935. } for subtype, column in ratio_columns
  2936. },
  2937. 'gender_comparison': gender_comparisons,
  2938. 'table_data': table_data # Add table data to results
  2939. }
  2940. return self.results['ms_subtypes']
  2941. def _generate_ms_subgroup_tables(self, table_data):
  2942. """Generate comprehensive tables from the MS subgroup analysis"""
  2943. # Table 1: Detailed statistics by subgroup, age, and lesion subtype
  2944. print(f"\n{'=' * 80}")
  2945. print("TABLE 1: DETAILED STATISTICS BY SUBGROUP, AGE, AND LESION SUBTYPE")
  2946. print(f"{'=' * 80}")
  2947. for metric_name in ['area', 'ratio']:
  2948. unit = 'mm²' if metric_name == 'area' else '%'
  2949. metric_title = 'WMH Subtype Area' if metric_name == 'area' else 'WMH Subtype Ratio'
  2950. print(f"\n{metric_title} ({unit}):")
  2951. print("-" * 80)
  2952. # Create DataFrame for this metric
  2953. rows = []
  2954. for subgroup in table_data['metadata']['subgroups']:
  2955. for age_label in self.config.AGE_LABELS:
  2956. for lesion_type in ['PEWMH', 'DWMH', 'JCWMH']:
  2957. if (subgroup in table_data['detailed_stats'] and
  2958. metric_name in table_data['detailed_stats'][subgroup] and
  2959. age_label in table_data['detailed_stats'][subgroup][metric_name] and
  2960. lesion_type in table_data['detailed_stats'][subgroup][metric_name][age_label]):
  2961. stats = table_data['detailed_stats'][subgroup][metric_name][age_label][lesion_type]
  2962. rows.append({
  2963. 'Subgroup': subgroup,
  2964. 'Age Group': age_label,
  2965. 'Lesion Type': lesion_type,
  2966. 'N': stats['count'],
  2967. 'Mean': f"{stats['mean']:.2f}" if not np.isnan(stats['mean']) else 'N/A',
  2968. 'SD': f"{stats['std']:.2f}" if not np.isnan(stats['std']) else 'N/A',
  2969. 'Median': f"{stats['median']:.2f}" if not np.isnan(stats['median']) else 'N/A',
  2970. 'Q25': f"{stats['q25']:.2f}" if not np.isnan(stats['q25']) else 'N/A',
  2971. 'Q75': f"{stats['q75']:.2f}" if not np.isnan(stats['q75']) else 'N/A',
  2972. 'Min': f"{stats['min']:.2f}" if not np.isnan(stats['min']) else 'N/A',
  2973. 'Max': f"{stats['max']:.2f}" if not np.isnan(stats['max']) else 'N/A'
  2974. })
  2975. if rows:
  2976. df = pd.DataFrame(rows)
  2977. print(df.to_string(index=False))
  2978. # Save to CSV
  2979. filename = f'ms_subgroup_{metric_name}_detailed_stats.csv'
  2980. df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  2981. print(f"Table saved to: {filename}")
  2982. else:
  2983. print("No data available for detailed statistics table.")
  2984. # Table 2: Plot data (means used for visualization)
  2985. print(f"\n{'=' * 80}")
  2986. print("TABLE 2: PLOT DATA (MEANS BY AGE GROUP AND LESION SUBTYPE)")
  2987. print(f"{'=' * 80}")
  2988. for metric_name in ['area', 'ratio']:
  2989. unit = 'mm²' if metric_name == 'area' else '%'
  2990. metric_title = 'WMH Subtype Area' if metric_name == 'area' else 'WMH Subtype Ratio'
  2991. print(f"\n{metric_title} - Mean Values Used in Plot ({unit}):")
  2992. print("-" * 90)
  2993. # Create plot data table
  2994. plot_rows = []
  2995. for subgroup in table_data['metadata']['subgroups']:
  2996. if (subgroup in table_data['plot_data'] and
  2997. metric_name in table_data['plot_data'][subgroup]):
  2998. plot_data = table_data['plot_data'][subgroup][metric_name]
  2999. for i, age_label in enumerate(self.config.AGE_LABELS):
  3000. if i < len(plot_data['age_centers']):
  3001. age_center = plot_data['age_centers'][i]
  3002. row = {
  3003. 'Subgroup': subgroup,
  3004. 'Age Group': age_label,
  3005. 'Age Center': f"{age_center:.1f}",
  3006. 'PEWMH Mean': f"{plot_data['pewmh_means'][i]:.2f}",
  3007. 'DWMH Mean': f"{plot_data['dwmh_means'][i]:.2f}",
  3008. 'JCWMH Mean': f"{plot_data['jcwmh_means'][i]:.2f}",
  3009. 'PEWMH N': plot_data['pewmh_counts'][i],
  3010. 'DWMH N': plot_data['dwmh_counts'][i],
  3011. 'JCWMH N': plot_data['jcwmh_counts'][i],
  3012. 'Total Mean': f"{plot_data['pewmh_means'][i] + plot_data['dwmh_means'][i] + plot_data['jcwmh_means'][i]:.2f}"
  3013. }
  3014. plot_rows.append(row)
  3015. if plot_rows:
  3016. plot_df = pd.DataFrame(plot_rows)
  3017. print(plot_df.to_string(index=False))
  3018. # Save to CSV
  3019. filename = f'ms_subgroup_{metric_name}_plot_data.csv'
  3020. plot_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  3021. print(f"Plot data saved to: {filename}")
  3022. else:
  3023. print("No data available for plot data table.")
  3024. # Table 3: Three-layer stacked area values
  3025. print(f"\n{'=' * 80}")
  3026. print("TABLE 3: THREE-LAYER STACKED AREA VALUES")
  3027. print(f"{'=' * 80}")
  3028. for metric_name in ['area', 'ratio']:
  3029. unit = 'mm²' if metric_name == 'area' else '%'
  3030. metric_title = 'WMH Subtype Area' if metric_name == 'area' else 'WMH Subtype Ratio'
  3031. print(f"\n{metric_title} - Stacked Layer Values ({unit}):")
  3032. print("-" * 100)
  3033. # Create stacked data table
  3034. stacked_rows = []
  3035. for subgroup in table_data['metadata']['subgroups']:
  3036. if (subgroup in table_data['plot_data'] and
  3037. metric_name in table_data['plot_data'][subgroup]):
  3038. plot_data = table_data['plot_data'][subgroup][metric_name]
  3039. for i, age_label in enumerate(self.config.AGE_LABELS):
  3040. if i < len(plot_data['pewmh_means']):
  3041. pewmh_mean = plot_data['pewmh_means'][i]
  3042. dwmh_mean = plot_data['dwmh_means'][i]
  3043. jcwmh_mean = plot_data['jcwmh_means'][i]
  3044. # Calculate cumulative layer boundaries
  3045. layer1_end = pewmh_mean
  3046. layer2_end = pewmh_mean + dwmh_mean
  3047. layer3_end = pewmh_mean + dwmh_mean + jcwmh_mean
  3048. row = {
  3049. 'Subgroup': subgroup,
  3050. 'Age Group': age_label,
  3051. 'PEWMH Layer': f"0.00 to {layer1_end:.2f}",
  3052. 'DWMH Layer': f"{layer1_end:.2f} to {layer2_end:.2f}",
  3053. 'JCWMH Layer': f"{layer2_end:.2f} to {layer3_end:.2f}",
  3054. 'Total Height': f"{layer3_end:.2f}",
  3055. 'PEWMH Contribution': f"{pewmh_mean:.2f}",
  3056. 'DWMH Contribution': f"{dwmh_mean:.2f}",
  3057. 'JCWMH Contribution': f"{jcwmh_mean:.2f}",
  3058. 'PEWMH %': f"{(pewmh_mean / layer3_end * 100):.1f}%" if layer3_end > 0 else "0.0%",
  3059. 'DWMH %': f"{(dwmh_mean / layer3_end * 100):.1f}%" if layer3_end > 0 else "0.0%",
  3060. 'JCWMH %': f"{(jcwmh_mean / layer3_end * 100):.1f}%" if layer3_end > 0 else "0.0%"
  3061. }
  3062. stacked_rows.append(row)
  3063. if stacked_rows:
  3064. stacked_df = pd.DataFrame(stacked_rows)
  3065. print(stacked_df.to_string(index=False))
  3066. # Save to CSV
  3067. filename = f'ms_subgroup_{metric_name}_stacked_data.csv'
  3068. stacked_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  3069. print(f"Stacked data saved to: {filename}")
  3070. else:
  3071. print("No data available for stacked data table.")
  3072. # Table 4: Gender comparison results
  3073. print(f"\n{'=' * 80}")
  3074. print("TABLE 4: GENDER COMPARISON RESULTS")
  3075. print(f"{'=' * 80}")
  3076. if table_data['gender_comparisons']:
  3077. gender_rows = []
  3078. for lesion_type, comparisons in table_data['gender_comparisons'].items():
  3079. for metric_type, comparison_result in comparisons.items():
  3080. if isinstance(comparison_result, dict) and 'test_type' in comparison_result:
  3081. unit = 'mm²' if metric_type == 'area' else '%'
  3082. if comparison_result['parametric']:
  3083. male_stat = f"{comparison_result['group1_stats']['mean']:.3f} ± {comparison_result['group1_stats']['std']:.3f}"
  3084. female_stat = f"{comparison_result['group2_stats']['mean']:.3f} ± {comparison_result['group2_stats']['std']:.3f}"
  3085. stat_type = "Mean ± SD"
  3086. else:
  3087. male_stat = f"{comparison_result['group1_stats']['median']:.3f} [{comparison_result['group1_stats']['q25']:.3f}-{comparison_result['group1_stats']['q75']:.3f}]"
  3088. female_stat = f"{comparison_result['group2_stats']['median']:.3f} [{comparison_result['group2_stats']['q25']:.3f}-{comparison_result['group2_stats']['q75']:.3f}]"
  3089. stat_type = "Median [IQR]"
  3090. row = {
  3091. 'Lesion Type': lesion_type,
  3092. 'Metric': metric_type.upper(),
  3093. 'Unit': unit,
  3094. 'Test Used': comparison_result['test_type'],
  3095. 'Statistic Type': stat_type,
  3096. 'Male': male_stat,
  3097. 'Female': female_stat,
  3098. 'Male N': comparison_result['group1_stats']['n'],
  3099. 'Female N': comparison_result['group2_stats']['n'],
  3100. 'Test Statistic': f"{comparison_result['test_statistic']:.3f}",
  3101. 'P-value': f"{comparison_result['p_value']}",
  3102. 'Effect Size': f"{comparison_result['effect_size_type']}: {comparison_result['effect_size']:.3f}",
  3103. 'Significant (α=0.05)': 'Yes' if comparison_result['p_value'] < 0.05 else 'No',
  3104. 'Significant (Bonferroni α=0.0083)': 'Yes' if comparison_result[
  3105. 'p_value'] < 0.0083 else 'No',
  3106. 'Male Normality': 'Normal' if comparison_result['normality_assessment']['group1'][
  3107. 'shapiro_normal'] else 'Non-normal',
  3108. 'Female Normality': 'Normal' if comparison_result['normality_assessment']['group2'][
  3109. 'shapiro_normal'] else 'Non-normal'
  3110. }
  3111. gender_rows.append(row)
  3112. if gender_rows:
  3113. gender_df = pd.DataFrame(gender_rows)
  3114. print(gender_df.to_string(index=False))
  3115. # Save to CSV
  3116. filename = 'ms_subgroup_gender_comparisons.csv'
  3117. gender_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  3118. print(f"Gender comparison results saved to: {filename}")
  3119. else:
  3120. print("No gender comparison results available.")
  3121. else:
  3122. print("No gender comparison data available.")
  3123. # Table 5: Summary statistics for each subgroup and metric
  3124. print(f"\n{'=' * 80}")
  3125. print("TABLE 5: SUMMARY STATISTICS BY SUBGROUP AND METRIC")
  3126. print(f"{'=' * 80}")
  3127. for metric_name in ['area', 'ratio']:
  3128. unit = 'mm²' if metric_name == 'area' else '%'
  3129. metric_title = 'WMH Subtype Area' if metric_name == 'area' else 'WMH Subtype Ratio'
  3130. print(f"\n{metric_title} - Summary Across All Age Groups ({unit}):")
  3131. print("-" * 90)
  3132. # Create summary statistics table
  3133. summary_rows = []
  3134. for subgroup in table_data['metadata']['subgroups']:
  3135. for lesion_type in ['PEWMH', 'DWMH', 'JCWMH']:
  3136. if (subgroup in table_data['detailed_stats'] and
  3137. metric_name in table_data['detailed_stats'][subgroup]):
  3138. # Aggregate across all age groups for this subgroup/lesion type
  3139. all_means = []
  3140. all_medians = []
  3141. total_count = 0
  3142. for age_label in self.config.AGE_LABELS:
  3143. if (age_label in table_data['detailed_stats'][subgroup][metric_name] and
  3144. lesion_type in table_data['detailed_stats'][subgroup][metric_name][age_label]):
  3145. stats = table_data['detailed_stats'][subgroup][metric_name][age_label][lesion_type]
  3146. if stats['count'] > 0:
  3147. all_means.append(stats['mean'])
  3148. all_medians.append(stats['median'])
  3149. total_count += stats['count']
  3150. if all_means:
  3151. row = {
  3152. 'Subgroup': subgroup,
  3153. 'Lesion Type': lesion_type,
  3154. 'Total N': total_count,
  3155. 'Age Groups': len(all_means),
  3156. 'Mean of Means': f"{np.mean(all_means):.2f}",
  3157. 'Median of Medians': f"{np.median(all_medians):.2f}",
  3158. 'Range of Means': f"{min(all_means):.2f} - {max(all_means):.2f}",
  3159. 'Range of Medians': f"{min(all_medians):.2f} - {max(all_medians):.2f}"
  3160. }
  3161. summary_rows.append(row)
  3162. if summary_rows:
  3163. summary_df = pd.DataFrame(summary_rows)
  3164. print(summary_df.to_string(index=False))
  3165. # Save to CSV
  3166. filename = f'ms_subgroup_{metric_name}_summary_stats.csv'
  3167. summary_df.to_csv(os.path.join(config.OUTPUT_DIR, filename), index=False)
  3168. print(f"Summary statistics saved to: {filename}")
  3169. else:
  3170. print("No data available for summary statistics table.")
  3171. print(f"\n{'=' * 80}")
  3172. print("All MS subgroup tables have been generated and saved to CSV files!")
  3173. print(f"{'=' * 80}")
  3174. print("\nGenerated Files Summary:")
  3175. print("- ms_subgroup_area_detailed_stats.csv (Complete descriptive statistics)")
  3176. print("- ms_subgroup_ratio_detailed_stats.csv (Complete descriptive statistics)")
  3177. print("- ms_subgroup_area_plot_data.csv (Data used for visualization)")
  3178. print("- ms_subgroup_ratio_plot_data.csv (Data used for visualization)")
  3179. print("- ms_subgroup_area_stacked_data.csv (Three-layer stacked values)")
  3180. print("- ms_subgroup_ratio_stacked_data.csv (Three-layer stacked values)")
  3181. print("- ms_subgroup_gender_comparisons.csv (Statistical gender comparisons)")
  3182. print("- ms_subgroup_area_summary_stats.csv (Summary across age groups)")
  3183. print("- ms_subgroup_ratio_summary_stats.csv (Summary across age groups)")
  3184. print("- ms_subgroup_analysis_documentation.txt (Comprehensive documentation)")
  3185. print("- ms_subgroup_analysis.png (Figure)")
  3186. def _generate_ms_subgroup_documentation(self, table_data):
  3187. """Generate comprehensive documentation explaining the MS subgroup analysis figure"""
  3188. from datetime import datetime
  3189. # Create comprehensive documentation
  3190. doc_content = f"""
  3191. MS SUBGROUP LESION ANALYSIS - COMPREHENSIVE DOCUMENTATION
  3192. =========================================================
  3193. Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  3194. OVERVIEW
  3195. --------
  3196. This analysis examines white matter hyperintensity (WMH) lesion subtypes specifically
  3197. in Multiple Sclerosis (MS) patients. The analysis stratifies lesions by anatomical
  3198. location and provides detailed comparisons across age groups, gender, and measurement
  3199. types (absolute area vs. normalized ratios).
  3200. FIGURE DESCRIPTION
  3201. ------------------
  3202. The figure consists of a 3x2 subplot layout (16" width x 18" height):
  3203. Layout Structure:
  3204. - Row 1: All MS Patients (combined analysis)
  3205. - Row 2: Female MS Patients only
  3206. - Row 3: Male MS Patients only
  3207. - Left Column: Absolute WMH Subtype Areas (mm²)
  3208. - Right Column: WMH Subtype Ratios (%)
  3209. Subplot Details:
  3210. 1. Top-Left: All MS - WMH Subtype Areas
  3211. 2. Top-Right: All MS - WMH Subtype Ratios
  3212. 3. Middle-Left: Female MS - WMH Subtype Areas
  3213. 4. Middle-Right: Female MS - WMH Subtype Ratios
  3214. 5. Bottom-Left: Male MS - WMH Subtype Areas
  3215. 6. Bottom-Right: Male MS - WMH Subtype Ratios
  3216. VISUALIZATION METHOD
  3217. --------------------
  3218. Chart Type: Three-Layer Stacked Area Plot
  3219. - Each subplot uses three-layer stacked area charts showing lesion subtypes across age groups
  3220. - PEWMH layer (bottom): Fills from 0 to PEWMH mean value
  3221. - DWMH layer (middle): Fills from PEWMH to PEWMH + DWMH mean
  3222. - JCWMH layer (top): Fills from PEWMH + DWMH to total mean
  3223. - This visualization shows both individual subtype contributions and total lesion burden
  3224. Color Scheme:
  3225. - PEWMH: {table_data['metadata']['colors'].get('pewmh', 'Color defined in config')} (alpha=0.8)
  3226. - DWMH: {table_data['metadata']['colors'].get('dwmh', 'Color defined in config')} (alpha=0.8)
  3227. - JCWMH: {table_data['metadata']['colors'].get('jcwmh', 'Color defined in config')} (alpha=0.8)
  3228. AGE STRATIFICATION
  3229. ------------------
  3230. Age Groups: {', '.join(table_data['metadata']['age_labels'])}
  3231. Age Bins: {table_data['metadata']['age_bins']}
  3232. Age Centers (for plotting): {[f'{center:.1f}' for center in table_data['metadata']['age_centers']]}
  3233. The analysis stratifies data across these age groups to examine age-related changes
  3234. in lesion subtype distribution for MS patients.
  3235. LESION SUBTYPES ANALYZED
  3236. ------------------------
  3237. 1. PEWMH (Periventricular White Matter Hyperintensities):
  3238. - Location: Adjacent to the ventricular system
  3239. - Clinical significance: Often associated with MS pathology and severity
  3240. - Area Column: {table_data['metadata']['metrics']['area']['columns']['pewmh']}
  3241. - Ratio Column: peri_wmh_ratio
  3242. 2. DWMH (Deep White Matter Hyperintensities):
  3243. - Location: Near but not directly adjacent to ventricles
  3244. - Clinical significance: May represent different pathophysiological processes
  3245. - Area Column: {table_data['metadata']['metrics']['area']['columns']['dwmh']}
  3246. - Ratio Column: deep_wmh_ratio
  3247. 3. JCWMH (Juxtacortical White Matter Hyperintensities):
  3248. - Location: At the interface between white and gray matter
  3249. - Clinical significance: Associated with cortical involvement in MS
  3250. - Area Column: {table_data['metadata']['metrics']['area']['columns']['jcwmh']}
  3251. - Ratio Column: juxta_wmh_ratio
  3252. CLINICAL CONTEXT
  3253. ----------------
  3254. MS Lesion Distribution Patterns:
  3255. - MS lesions preferentially affect certain brain regions
  3256. - Periventricular regions are classically involved in MS
  3257. - Juxtacortical lesions may indicate disease progression
  3258. - Age-related changes may reflect disease evolution or natural aging
  3259. Expected Clinical Patterns:
  3260. - PEWMH typically most prominent in MS patients
  3261. - Age-related increase in all lesion subtypes
  3262. - Gender differences may reflect hormonal or genetic factors
  3263. - Individual variation in lesion distribution patterns
  3264. MEASUREMENT TYPES
  3265. -----------------
  3266. 1. Absolute Area (mm²):
  3267. - Direct measurement of lesion area
  3268. - Units: Square millimeters (mm²)
  3269. - Clinical significance: Reflects total lesion load per subtype
  3270. 2. Normalized Ratio (%):
  3271. - Lesion area relative to total brain area/volume
  3272. - Units: Percentage (%)
  3273. - Clinical significance: Controls for individual brain size differences
  3274. STATISTICAL APPROACH
  3275. --------------------
  3276. For each combination of:
  3277. - Subgroup (All MS, Female MS, Male MS)
  3278. - Age group ({len(table_data['metadata']['age_labels'])} categories)
  3279. - Lesion subtype (PEWMH, DWMH, JCWMH)
  3280. - Metric (Area vs Ratio)
  3281. The following statistics are calculated:
  3282. - Sample size (N)
  3283. - Mean ± Standard Deviation (for visualization)
  3284. - Median and Interquartile Range [Q25-Q75] (for robust statistics)
  3285. - Minimum and Maximum values
  3286. - 25th and 75th percentiles
  3287. Non-parametric Statistics:
  3288. - Mann-Whitney U tests for gender comparisons within each lesion subtype
  3289. - Median and IQR reported for robustness to outliers
  3290. - Appropriate for skewed lesion distribution data
  3291. INTERPRETATION GUIDELINES
  3292. -------------------------
  3293. Three-Layer Stacked Plot Interpretation:
  3294. - Bottom layer height = PEWMH mean contribution
  3295. - Middle layer height = DWMH mean contribution
  3296. - Top layer height = JCWMH mean contribution
  3297. - Total stack height = Combined lesion burden across all subtypes
  3298. - Layer thickness indicates relative contribution of each subtype
  3299. Clinical Pattern Recognition:
  3300. - Dominant lesion subtype can be identified by layer thickness
  3301. - Age-related changes visible as slope steepness
  3302. - Gender differences apparent by comparing male vs female rows
  3303. - Subtype-specific patterns may indicate different pathological processes
  3304. Expected Subtype Hierarchy:
  3305. - PEWMH often dominant in MS (thickest layer)
  3306. - DWMH and JCWMH may show age-dependent changes
  3307. - Individual variation in subtype distribution patterns
  3308. GENDER STRATIFICATION ANALYSIS
  3309. -------------------------------
  3310. The analysis includes separate visualizations for:
  3311. 1. All MS patients (combined analysis)
  3312. 2. Female MS patients only
  3313. 3. Male MS patients only
  3314. Gender Comparison Features:
  3315. - Direct visual comparison between male and female patterns
  3316. - Statistical testing for gender differences in each lesion subtype
  3317. - Separate analysis for both area and ratio measurements
  3318. - Age-stratified patterns within each gender
  3319. DATA QUALITY CONSIDERATIONS
  3320. ----------------------------
  3321. - Zero values indicate no subjects in that age/subtype combination
  3322. - Small sample sizes in gender-stratified analyses may reduce statistical power
  3323. - Lesion subtype classification depends on anatomical definition accuracy
  3324. - Manual segmentation variability may affect subtype boundaries
  3325. - Automated methods may have subtype-specific detection biases
  3326. STATISTICAL TESTING METHODOLOGY
  3327. --------------------------------
  3328. Gender Comparisons:
  3329. - Mann-Whitney U test for each lesion subtype
  3330. - Separate tests for area and ratio measurements
  3331. - Non-parametric approach suitable for skewed lesion data
  3332. - Two-sided alternative hypothesis
  3333. Multiple Testing Considerations:
  3334. - Multiple comparisons performed across lesion subtypes
  3335. - Consider Bonferroni correction: α = 0.05/6 = 0.0083 for significance
  3336. - False Discovery Rate (FDR) correction may be more appropriate
  3337. OUTPUT FILES GENERATED
  3338. -----------------------
  3339. 1. Figure: ms_subgroup_analysis.png
  3340. - 3x2 subplot layout with three-layer stacked area plots
  3341. - High resolution (DPI: {getattr(self.config, 'DPI', 300)})
  3342. - White background for publication quality
  3343. 2. Detailed Statistics Tables (CSV):
  3344. - ms_subgroup_area_detailed_stats.csv: Complete descriptive statistics for area measurements
  3345. - ms_subgroup_ratio_detailed_stats.csv: Complete descriptive statistics for ratio measurements
  3346. 3. Plot Data Tables (CSV):
  3347. - ms_subgroup_area_plot_data.csv: Mean values and counts used for area visualization
  3348. - ms_subgroup_ratio_plot_data.csv: Mean values and counts used for ratio visualization
  3349. 4. Stacked Area Values (CSV):
  3350. - ms_subgroup_area_stacked_data.csv: Layer boundaries and contributions for area plots
  3351. - ms_subgroup_ratio_stacked_data.csv: Layer boundaries and contributions for ratio plots
  3352. 5. Gender Comparison Tables (CSV):
  3353. - ms_subgroup_gender_comparisons.csv: Statistical test results comparing males vs females
  3354. 6. Summary Statistics Tables (CSV):
  3355. - ms_subgroup_area_summary_stats.csv: Aggregated statistics across age groups for areas
  3356. - ms_subgroup_ratio_summary_stats.csv: Aggregated statistics across age groups for ratios
  3357. 7. This Documentation:
  3358. - ms_subgroup_analysis_documentation.txt: Complete explanation of analysis and interpretation
  3359. TECHNICAL SPECIFICATIONS
  3360. -------------------------
  3361. Figure Specifications:
  3362. - Size: 16" x 18" (width x height) - taller for 3-row layout
  3363. - DPI: {getattr(self.config, 'DPI', 300)}
  3364. - Background: White
  3365. - Font sizes: Title=14pt (bold), Axis labels=12pt
  3366. - Grid: Enabled with 30% transparency
  3367. - Legend: Three-layer legend for each subplot
  3368. Data Processing:
  3369. - MS patients only (HC excluded from this analysis)
  3370. - Missing data handled by excluding from calculations (dropna)
  3371. - Zero values used when no subjects available in category
  3372. - Robust statistics (median/IQR) preferred for group summaries
  3373. Plotting Library: matplotlib
  3374. Statistical Library: scipy.stats (Mann-Whitney U tests)
  3375. Data Processing: pandas, numpy
  3376. TABLE DESCRIPTIONS
  3377. ------------------
  3378. Table 1 - Detailed Statistics:
  3379. Contains complete descriptive statistics (N, mean, SD, median, Q25, Q75, min, max)
  3380. for each combination of subgroup, age group, and lesion subtype.
  3381. Table 2 - Plot Data:
  3382. Contains the exact mean values and sample counts used to generate the stacked area plots,
  3383. organized by subgroup and age group.
  3384. Table 3 - Stacked Area Values:
  3385. Shows the layer boundaries and individual contributions for the three-layer stacked plots,
  3386. including percentage contributions of each lesion subtype.
  3387. Table 4 - Gender Comparisons:
  3388. Statistical test results (Mann-Whitney U) comparing male vs female patients for each
  3389. lesion subtype, with both uncorrected and Bonferroni-corrected significance levels.
  3390. Table 5 - Summary Statistics:
  3391. Aggregated statistics across all age groups for each subgroup and lesion subtype,
  3392. showing overall patterns and variability.
  3393. LIMITATIONS AND CONSIDERATIONS
  3394. ------------------------------
  3395. 1. Sample Size Limitations:
  3396. - Gender-stratified analyses have reduced sample sizes
  3397. - Some age groups may have insufficient subjects for reliable estimates
  3398. - Power analysis recommended for gender comparisons
  3399. 2. Lesion Subtype Definition:
  3400. - Anatomical boundaries between subtypes may be arbitrary
  3401. - Different segmentation protocols may yield different results
  3402. - Spatial resolution limits affecting small lesion detection
  3403. 3. Multiple Comparisons:
  3404. - Six statistical tests performed (3 subtypes × 2 metrics)
  3405. - Risk of Type I error inflation
  3406. - Consider correction for multiple testing
  3407. 4. Age Group Effects:
  3408. - Discretized age groups may mask continuous relationships
  3409. - Unequal age distributions between genders possible
  3410. - Cross-sectional design limits inferences about progression
  3411. 5. MS Disease Heterogeneity:
  3412. - MS subtypes (relapsing-remitting, progressive) not considered
  3413. - Disease duration effects not analyzed
  3414. - Treatment effects not controlled
  3415. RECOMMENDED FOLLOW-UP ANALYSES
  3416. ------------------------------
  3417. 1. Disease Subtype Stratification:
  3418. - Separate analysis for RRMS, SPMS, PPMS if sample size permits
  3419. - Include disease duration as covariate
  3420. 2. Advanced Statistical Modeling:
  3421. - Multivariate analysis of lesion subtype interdependencies
  3422. - Machine learning approaches for subtype pattern classification
  3423. - Longitudinal analysis if follow-up data available
  3424. 3. Clinical Correlation Studies:
  3425. - Correlation with disability scores (EDSS, MSFC)
  3426. - Cognitive function associations
  3427. - Treatment response predictions
  3428. 4. Spatial Analysis:
  3429. - Lesion location heat maps
  3430. - Connectivity-based lesion impact analysis
  3431. - Atlas-based regional quantification
  3432. 5. Comparative Studies:
  3433. - Comparison with other neurological conditions
  3434. - Validation in independent MS cohorts
  3435. - Cross-scanner reproducibility studies
  3436. QUALITY CONTROL RECOMMENDATIONS
  3437. --------------------------------
  3438. 1. Segmentation Validation:
  3439. - Inter-rater reliability assessment for lesion subtype classification
  3440. - Comparison of automated vs manual segmentation methods
  3441. - Test-retest reliability studies
  3442. 2. Clinical Validation:
  3443. - Correlation with established MS biomarkers
  3444. - Agreement with radiological assessment
  3445. - Validation against histopathological data if available
  3446. 3. Statistical Validation:
  3447. - Power analysis for gender comparisons
  3448. - Bootstrap confidence intervals for robust statistics
  3449. - Cross-validation of predictive models
  3450. CONTACT AND METHODOLOGY
  3451. -----------------------
  3452. This analysis was generated using an automated pipeline for MS lesion subtype assessment.
  3453. For questions about clinical interpretation, statistical methods, or lesion classification
  3454. protocols, refer to the original research protocol and neuroimaging analysis guidelines.
  3455. Analysis Pipeline Version: [Version info if available]
  3456. Last Updated: {datetime.now().strftime('%Y-%m-%d')}
  3457. REFERENCES AND FURTHER READING
  3458. -------------------------------
  3459. 1. Filippi et al. (2019). Assessment of lesions on magnetic resonance imaging
  3460. in multiple sclerosis: practical guidelines. Brain.
  3461. 2. Geurts et al. (2012). Cortical lesions in multiple sclerosis: combined
  3462. postmortem MR imaging and histopathology. AJNR Am J Neuroradiol.
  3463. 3. Brownell & Hughes (1962). The distribution of plaques in the cerebrum in
  3464. multiple sclerosis. Journal of Neurology, Neurosurgery & Psychiatry.
  3465. 4. Barkhof & Filippi (2009). MRI in multiple sclerosis. Journal of Magnetic
  3466. Resonance Imaging.
  3467. 5. Thompson et al. (2018). Diagnosis of multiple sclerosis: 2017 revisions of
  3468. the McDonald criteria. Lancet Neurology.
  3469. END OF DOCUMENTATION
  3470. ====================
  3471. """
  3472. # Save documentation to file
  3473. doc_filename = os.path.join(config.OUTPUT_DIR, 'ms_subgroup_analysis_documentation.txt')
  3474. with open(doc_filename, 'w', encoding='utf-8') as f:
  3475. f.write(doc_content)
  3476. print(f"\n{'=' * 80}")
  3477. print("COMPREHENSIVE MS SUBGROUP DOCUMENTATION GENERATED")
  3478. print(f"{'=' * 80}")
  3479. print(f"Documentation saved to: ms_subgroup_analysis_documentation.txt")
  3480. print(f"File contains detailed explanation of MS lesion subtype analysis and clinical interpretation.")
  3481. def correlation_analysis(self):
  3482. """

p4_excel_analysis_developed.py at commit 3e9edd4, under MIT · at the source

Overview

  1. Biomedical Engineering Faculty, Sahand University of Technology,Tabriz, Iran
  2. Radiology Department, Tabriz University of Medical Sciences,Tabriz, Iran
Journal: BMC medical imaging, volume 26, issue 1, article 387
Dates: received 7 December 2025; accepted 27 May 2026; published online 1 June 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1186/s12880-026-02481-2 · PMID 42226149 · PMCID PMC13449394 · OpenAlex W7163025612
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), multiple sclerosis (population), clinical / translational (subfield)
Methods: Connectivity, Statistics, Preprocessing, Machine learning
Keywords: Multiple sclerosis (MS), Deep learning, Neuroimaging, MRI, Population-specific biomarkers, FLAIR segmentation
MeSH: Brain*, Deep Learning*, Multiple Sclerosis*, Neuroimaging*, Adult, Female, Humans, Iran, Magnetic Resonance Imaging, Male, Middle Aged, Middle Eastern People, Retrospective Studies, Young Adult (* major topic)
Topic: Multiple Sclerosis Research Studies (Pathology and Forensic Medicine, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 59 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.

Repository

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

Mahdi-Bashiri/MS-DeepBrain-Study

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 3e9edd415b0beccf7ad19f036fc3dbfdcf1d8076, 22 April 2026
Languages: Python (29)
Size: 455 files, 29 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, license file, environment (requirements.txt)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (20 files), TensorFlow (15 files), Matplotlib (10 files), Keras (9 files), scikit-image (9 files), NiBabel (8 files), OpenCV (8 files), scikit-learn (8 files), SciPy (6 files), pandas (5 files), seaborn (4 files), FSL (2 files), imageio (1 file), Pillow (1 file), Plotly (1 file), pydicom (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
32 files

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:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 29 scripts, each with its path and the digest of its content;
  • 23 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/s12880-026-02481-2.

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, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 3 authors, 6 keywords, 14 MeSH terms, 58 references.

Cite

This paper

Bawil, M. B., Shamsi, M., & Bavil, A. S. (2026). Deep learning-based neuroanatomical profiling reveals population-specific brain changes in multiple sclerosis: a large-scale Middle Eastern study. BMC medical imaging, 26(1), 387. https://doi.org/10.1186/s12880-026-02481-2

BibTeX

@article{bawil2026deep,
author = {Bawil, Mahdi Bashiri and Shamsi, Mousa and Bavil, Abolhassan Shakeri},
title = {{Deep learning-based neuroanatomical profiling reveals population-specific brain changes in multiple sclerosis: a large-scale Middle Eastern study}},
journal = {BMC medical imaging},
year = {2026},
month = jun,
volume = {26},
number = {1},
pages = {387},
publisher = {BMC},
issn = {1471-2342},
doi = {10.1186/s12880-026-02481-2},
url = {https://doi.org/10.1186/s12880-026-02481-2},
pmid = {42226149},
pmcid = {PMC13449394}
}

RIS

TY - JOUR
AU - Bawil, Mahdi Bashiri
AU - Shamsi, Mousa
AU - Bavil, Abolhassan Shakeri
TI - Deep learning-based neuroanatomical profiling reveals population-specific brain changes in multiple sclerosis: a large-scale Middle Eastern study
T2 - BMC medical imaging
J2 - BMC Med Imaging
PY - 2026
DA - 2026/06/01
VL - 26
IS - 1
SP - 387
SN - 1471-2342
PB - BMC
DO - 10.1186/s12880-026-02481-2
UR - https://doi.org/10.1186/s12880-026-02481-2
LA - en
ER -

CSL-JSON

{
"id": "10.1186/s12880-026-02481-2",
"type": "article-journal",
"title": "Deep learning-based neuroanatomical profiling reveals population-specific brain changes in multiple sclerosis: a large-scale Middle Eastern study",
"container-title": "BMC medical imaging",
"author": [
{
"family": "Bawil",
"given": "Mahdi Bashiri"
},
{
"family": "Shamsi",
"given": "Mousa"
},
{
"family": "Bavil",
"given": "Abolhassan Shakeri"
}
],
"container-title-short": "BMC Med Imaging",
"volume": "26",
"issue": "1",
"page": "387",
"DOI": "10.1186/s12880-026-02481-2",
"PMID": "42226149",
"PMCID": "PMC13449394",
"ISSN": "1471-2342",
"publisher": "BMC",
"URL": "https://doi.org/10.1186/s12880-026-02481-2",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
1
]
]
}
}

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/s41597-026-07184-5 [code]
A Multiple Sclerosis MRI Dataset with Tri-Mask Annotations for Lesion Segmentation.
Journal: Scientific data
In common: pydicom, Keras, TensorFlow, 9 other tools, multiple sclerosis, structural MRI / diffusion, 6 references, 3 authors
[2] doi:10.1186/s12938-026-01555-0 [code]
Incorporating normal periventricular changes for enhanced pathological white matter hyperintensity segmentation: on multiclass deep learning approaches.
Journal: Biomedical engineering online
In common: Keras, TensorFlow, OpenCV, 7 other tools, structural MRI / diffusion, 5 references, 3 authors
[3] doi:10.1002/epi.70296 [code]
Fully automated three-dimensional deep learning-based magnetic resonance imaging segmentation of brain cavities in epilepsy surgery.
Journal: Epilepsia
In common: imageio, Keras, TensorFlow, 10 other tools, structural MRI / diffusion, clinical / translational, 1 reference
[4] doi:10.3389/fnins.2026.1870124 [code]
An end-to-end pipeline for automated fetal brain segmentation and biometry from 3D SSFP MRI.
Journal: Frontiers in neuroscience
In common: imageio, FSL, OpenCV, 8 other tools, structural MRI / diffusion, 4 references
[5] doi:10.3389/frai.2026.1771088 [code]
Few-shot deployment of pretrained MRI transformers in brain imaging tasks.
Journal: Frontiers in artificial intelligence
In common: pydicom, imageio, OpenCV, 9 other tools, structural MRI / diffusion, 1 reference
[6] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: pydicom, Keras, TensorFlow, 10 other tools
[7] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: imageio, Keras, TensorFlow, 10 other tools
[8] doi:10.1371/journal.pdig.0001316 [code]
Reliability of a convolutional neural network in segmenting multiple sclerosis lesions from MRI: Impact of data augmentation, image modality and tolerance with U-Net architecture.
Journal: PLOS digital health
In common: scikit-image, NiBabel, scikit-learn, 3 other tools, multiple sclerosis, structural MRI / diffusion, 6 references
[9] doi:10.1080/07853890.2026.2685416 [code]
Pulmonary and cerebral damage in COVID-19 survivors: is there any association?
Journal: Annals of medicine
In common: pydicom, imageio, Keras, 8 other tools, structural MRI / diffusion, clinical / translational
[10] doi:10.1371/journal.pcbi.1014571 [code]
SynAPSeg: A novel dataset and image analysis framework for deep learning-based synapse detection and quantification.
Journal: PLoS computational biology
In common: imageio, Keras, TensorFlow, 9 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.