Features module tutorial
This tutorial shows how to perform an exploratory analysis of the features data: to visualize features distribution in classes, plot the feature correlation matrix, check Mann-Whitney U-test p-values, plot univariate ROC (and calculate AUC) for each feature, perform volumetric analysis, and save all the scores. We will use PyRadiomics features as variables and binary survival label as an outcome.
Importing modules:
import os,sys
from pmtool.AnalysisBox import AnalysisBox
import pandas as pd
Required data
To run the tutorial, you will need the following files, available in the 'data/features' folder of the precision-medicine-toolbox repository:
- 'extended_clinical_df.xlsx' - this is the clinical data file provided with the Lung1 dataset (Aerts et al., 2014); we added binary variables of 1-, 1.5-, and 2-years survival, based on the presented data,
- 'extracted_features_full.xlsx' - this is a file with the PyRadiomic features values (can be extracted in the imaging module tutorial) for all the patients from Lung1 dataset.
Binary classes functionality
If you have 2 classes in your dataset, the following functionality is availabe for your analysis. We will divide Lung1 dataset by 1 year survival label, therefore having 2 classes.
Set up the parameters to get the data:
parameters = {
'feature_path': "../data/features/extracted_features_full.xlsx", # path to csv/xls file with features
'outcome_path': "../data/features/extended_clinical_df.xlsx", #path to csv/xls file with outcome
'patient_column': 'Patient', # name of column with patient ID
'patient_in_outcome_column': 'PatientID', # name of column with patient ID in clinical data file
'outcome_column': '1yearsurvival' # name of outcome column
}
Initialise the feature set (you will see a short summary):
fs = AnalysisBox(**parameters)
Number of observations: 149
Class labels: ['0' '1']
Classes balance: [0.4228187919463087, 0.5771812080536913]
Print some attributes of the feature set - first 10 patient IDs and first 10 feature names:
print ('Patient IDs: ', fs._patient_name[:10])
print ('\nFeature names: ', fs._feature_column[:10])
Patient IDs: ['LUNG1-001_20180209_CT_2_GTV-1_mask', 'LUNG1-002_000000_GTV-1_mask', 'LUNG1-002_20180526_CT_1_GTV-1_mask', 'LUNG1-003_000000_GTV-1_mask', 'LUNG1-003_20180209_CT_1_GTV-1_mask', 'LUNG1-003_20180209_CT_1_GTV-2_mask', 'LUNG1-003_20180209_CT_1_GTV-3_mask', 'LUNG1-004_000000_GTV-1_mask', 'LUNG1-006_000000_GTV-1_mask', 'LUNG1-008_000000_GTV-1_mask']
Feature names: ['original_shape_Elongation', 'original_shape_Flatness', 'original_shape_LeastAxisLength', 'original_shape_MajorAxisLength', 'original_shape_Maximum2DDiameterColumn', 'original_shape_Maximum2DDiameterRow', 'original_shape_Maximum2DDiameterSlice', 'original_shape_Maximum3DDiameter', 'original_shape_MeshVolume', 'original_shape_MinorAxisLength']
Exclude patients with the unknown outcomes, if they are represented in the dataset (feature set parameters are re-initialised and short summary is printed again):
fs.handle_nan(axis=0)
Number of observations: 149
Class labels: ['0' '1']
Classes balance: [0.4228187919463087, 0.5771812080536913]
Print the head of the composed dataframe, containing both the variables and the outcome:
fs._feature_outcome_dataframe.head(5)
| original_shape_Elongation | original_shape_Flatness | original_shape_LeastAxisLength | original_shape_MajorAxisLength | original_shape_Maximum2DDiameterColumn | original_shape_Maximum2DDiameterRow | original_shape_Maximum2DDiameterSlice | original_shape_Maximum3DDiameter | original_shape_MeshVolume | original_shape_MinorAxisLength | ... | wavelet-LLL_gldm_HighGrayLevelEmphasis | wavelet-LLL_gldm_LargeDependenceEmphasis | wavelet-LLL_gldm_LargeDependenceHighGrayLevelEmphasis | wavelet-LLL_gldm_LargeDependenceLowGrayLevelEmphasis | wavelet-LLL_gldm_LowGrayLevelEmphasis | wavelet-LLL_gldm_SmallDependenceEmphasis | wavelet-LLL_gldm_SmallDependenceHighGrayLevelEmphasis | wavelet-LLL_gldm_SmallDependenceLowGrayLevelEmphasis | ROI | 1yearsurvival | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Patient | |||||||||||||||||||||
| LUNG1-001_20180209_CT_2_GTV-1_mask | 0.732658 | 0.548834 | 46.151744 | 84.090588 | 95.336247 | 83.186537 | 95.425364 | 96.104110 | 155379.500000 | 61.609664 | ... | 14462.536758 | 33.609142 | 548084.071741 | 0.002759 | 0.000139 | 0.267761 | 2959.571494 | 0.000058 | GTV-1_mask | 1 |
| LUNG1-002_000000_GTV-1_mask | 0.878035 | 0.755488 | 70.110114 | 92.801132 | 116.931604 | 101.833197 | 104.316825 | 125.674182 | 358446.791667 | 81.482668 | ... | 13208.125493 | 55.600107 | 832176.248523 | 0.004497 | 0.000162 | 0.188931 | 1733.836805 | 0.000053 | GTV-1_mask | 0 |
| LUNG1-002_20180526_CT_1_GTV-1_mask | 0.878035 | 0.755488 | 70.110114 | 92.801132 | 116.931604 | 101.833197 | 104.316825 | 125.674182 | 358446.791667 | 81.482668 | ... | 13208.125493 | 55.600107 | 832176.248523 | 0.004497 | 0.000162 | 0.188931 | 1733.836805 | 0.000053 | GTV-1_mask | 0 |
| LUNG1-003_000000_GTV-1_mask | 0.544631 | 0.356597 | 25.559022 | 71.674815 | 56.639209 | 83.528438 | 62.265560 | 84.011904 | 34987.000000 | 39.036358 | ... | 9142.646956 | 17.909008 | 209143.444093 | 0.002673 | 0.000367 | 0.402930 | 2838.784544 | 0.000191 | GTV-1_mask | 0 |
| LUNG1-003_20180209_CT_1_GTV-1_mask | 0.544631 | 0.356597 | 25.559022 | 71.674815 | 56.639209 | 83.528438 | 62.265560 | 84.011904 | 34987.000000 | 39.036358 | ... | 9142.646956 | 17.909008 | 209143.444093 | 0.002673 | 0.000367 | 0.402930 | 2838.784544 | 0.000191 | GTV-1_mask | 0 |
5 rows × 1220 columns
Visualize feature values distribution in classes for the first 12 features (will pop up in an interactive .html report):
fs.plot_distribution(fs._feature_column[:8])
Shapiro-Wilcoxon test to get a list of normally distributed features:
features_norm_distr = fs.normality_check()
print (len(features_norm_distr))
print (features_norm_distr)
59
['original_shape_Sphericity', 'original_glcm_Idm', 'original_glcm_Id', 'original_glrlm_RunLengthNonUniformityNormalized', 'original_glszm_HighGrayLevelZoneEmphasis', 'original_glszm_SmallAreaHighGrayLevelEmphasis', 'original_gldm_SmallDependenceHighGrayLevelEmphasis', 'log-sigma-1-0-mm-3D_glcm_DifferenceEntropy', 'log-sigma-2-0-mm-3D_firstorder_MeanAbsoluteDeviation', 'log-sigma-3-0-mm-3D_firstorder_InterquartileRange', 'log-sigma-3-0-mm-3D_firstorder_RobustMeanAbsoluteDeviation', 'log-sigma-3-0-mm-3D_glcm_Id', 'log-sigma-4-0-mm-3D_firstorder_InterquartileRange', 'log-sigma-4-0-mm-3D_glcm_Idm', 'log-sigma-4-0-mm-3D_glcm_Id', 'log-sigma-4-0-mm-3D_glszm_SizeZoneNonUniformityNormalized', 'log-sigma-5-0-mm-3D_firstorder_Skewness', 'log-sigma-5-0-mm-3D_glcm_Imc1', 'log-sigma-5-0-mm-3D_glcm_Idm', 'log-sigma-5-0-mm-3D_glcm_Id', 'wavelet-LLH_firstorder_Entropy', 'wavelet-LLH_glcm_SumEntropy', 'wavelet-LHL_glcm_DifferenceAverage', 'wavelet-LHH_firstorder_10Percentile', 'wavelet-LHH_firstorder_MeanAbsoluteDeviation', 'wavelet-LHH_glcm_Correlation', 'wavelet-LHH_glcm_DifferenceAverage', 'wavelet-LHH_glcm_Idm', 'wavelet-LHH_glcm_Id', 'wavelet-LHH_glrlm_RunVariance', 'wavelet-HLL_glcm_DifferenceEntropy', 'wavelet-HLL_glcm_Imc1', 'wavelet-HLL_glcm_Idm', 'wavelet-HLL_glcm_Id', 'wavelet-HLL_glrlm_RunLengthNonUniformityNormalized', 'wavelet-HLL_glrlm_RunPercentage', 'wavelet-HLH_glcm_Correlation', 'wavelet-HLH_glcm_Idm', 'wavelet-HLH_glcm_Id', 'wavelet-HLH_glcm_InverseVariance', 'wavelet-HHL_firstorder_Entropy', 'wavelet-HHL_firstorder_MeanAbsoluteDeviation', 'wavelet-HHL_firstorder_Uniformity', 'wavelet-HHL_glcm_Correlation', 'wavelet-HHL_glcm_DifferenceEntropy', 'wavelet-HHL_glcm_JointEntropy', 'wavelet-HHL_glcm_Imc2', 'wavelet-HHL_glcm_MaximumProbability', 'wavelet-HHL_glcm_SumEntropy', 'wavelet-HHL_glrlm_ShortRunEmphasis', 'wavelet-HHH_firstorder_10Percentile', 'wavelet-HHH_firstorder_90Percentile', 'wavelet-HHH_firstorder_InterquartileRange', 'wavelet-HHH_firstorder_MeanAbsoluteDeviation', 'wavelet-HHH_firstorder_RobustMeanAbsoluteDeviation', 'wavelet-HHH_glcm_Imc2', 'wavelet-HHH_glcm_MaximumProbability', 'wavelet-LLL_glszm_HighGrayLevelZoneEmphasis', 'wavelet-LLL_gldm_SmallDependenceHighGrayLevelEmphasis']
Visualize mutual feature correlation coefficient (Spearman's) matrix for the first 12 features (in .html report):
fs.plot_correlation_matrix(fs._feature_column[:8])
Visualize Mann-Whitney (Bonferroni corrected) p-values for binary classes test (in .html report):
fs.plot_MW_p(fs._feature_column[:8])
Vizualize univariate ROC curves:
fs.plot_univariate_roc(fs._feature_column[:8], auc_threshold=0.70)
Calculate the basic statistics for each feature (save the values in data/features/extracted_features_full_basic_stats.xlsx): number of NaN, mean, std, min, max; if applicable: MW-p, univariate ROC AUC, volume correlation:
fs.calculate_basic_stats(volume_feature='original_shape_VoxelVolume')
Check the excel table:
print('Basic statistics for each feature')
pd.read_excel('../data/features/extracted_features_full_basic_stats.xlsx')
Basic statistics for each feature
| Unnamed: 0 | NaN | Mean | Std | Min | Max | p_MW_corrected | univar_auc | volume_corr | p_shapiro_test | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | original_shape_Elongation | 0 | 0.720328 | 0.161721 | 0.062127 | 0.974104 | 1.000000 | 0.517996 | 0.037515 | 1.785652e-06 |
| 1 | original_shape_Flatness | 0 | 0.559677 | 0.154895 | 0.047315 | 0.856767 | 1.000000 | 0.516611 | 0.099032 | 2.595303e-03 |
| 2 | original_shape_LeastAxisLength | 0 | 32.055804 | 15.983620 | 6.643777 | 85.495660 | 0.117975 | 0.686877 | 0.973238 | 3.124696e-04 |
| 3 | original_shape_MajorAxisLength | 0 | 61.595666 | 35.336125 | 13.611433 | 240.822486 | 0.018917 | 0.705795 | 0.842114 | 1.637431e-09 |
| 4 | original_shape_Maximum2DDiameterColumn | 0 | 63.064956 | 33.057474 | 15.620499 | 157.632484 | 0.161563 | 0.681525 | 0.950909 | 3.372626e-05 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1213 | wavelet-LLL_gldm_LargeDependenceLowGrayLevelEm... | 0 | 1.646810 | 8.820031 | 0.001036 | 79.946280 | 1.000000 | 0.524640 | 0.070593 | 3.766094e-25 |
| 1214 | wavelet-LLL_gldm_LowGrayLevelEmphasis | 0 | 0.007969 | 0.039767 | 0.000069 | 0.369746 | 0.471623 | 0.515319 | -0.805811 | 5.147920e-25 |
| 1215 | wavelet-LLL_gldm_SmallDependenceEmphasis | 0 | 0.313546 | 0.177561 | 0.009452 | 0.755977 | 1.000000 | 0.654393 | -0.634677 | 1.122224e-04 |
| 1216 | wavelet-LLL_gldm_SmallDependenceHighGrayLevelE... | 0 | 2431.019809 | 1077.764252 | 0.093515 | 5302.913608 | 1.000000 | 0.619970 | -0.274001 | 3.676662e-01 |
| 1217 | wavelet-LLL_gldm_SmallDependenceLowGrayLevelEm... | 0 | 0.000440 | 0.000872 | 0.000013 | 0.007314 | 0.077728 | 0.654854 | -0.872534 | 1.102855e-20 |
1218 rows × 10 columns
Volume analysis will show you if your features have a high correlation to volume and if volume itself is a predictive feature in separation of 2 classes. You need to have a volume feature in your dataset and send it as a function parameter (in our case it is 'original_shape_VoxelVolume').
fs.volume_analysis(volume_feature='original_shape_VoxelVolume')
Multi-class
If you have more than 2 classes in your dataset, the following functionality is availabe for your analysis. We will use a disease stage as a class label.
Set up the parameters to get the data:
parameters = {
'feature_path': "../data/features/extracted_features_full.xlsx", # path to csv/xls file with features
'outcome_path': "../data/features/extended_clinical_df.xlsx", # path to csv/xls file with outcome
'patient_column': 'Patient', # name of column with patient ID
'patient_in_outcome_column': 'PatientID', # name of column with patient ID in clinical data file
'outcome_column': 'Overall.Stage' # name of outcome column
}
Initialise the feature set:
fs = AnalysisBox(**parameters)
Number of observations: 149
Class labels: ['I' 'II' 'IIIa' 'IIIb' 'nan']
Classes balance: [0.24161073825503357, 0.09395973154362416, 0.2348993288590604, 0.4228187919463087, 0.006711409395973154]
Check the patients with the absent outcomes (because there is a NaN value among class labels in the data summary):
fs._feature_outcome_dataframe[fs._feature_outcome_dataframe['Overall.Stage'].isnull()]
| original_shape_Elongation | original_shape_Flatness | original_shape_LeastAxisLength | original_shape_MajorAxisLength | original_shape_Maximum2DDiameterColumn | original_shape_Maximum2DDiameterRow | original_shape_Maximum2DDiameterSlice | original_shape_Maximum3DDiameter | original_shape_MeshVolume | original_shape_MinorAxisLength | ... | wavelet-LLL_gldm_HighGrayLevelEmphasis | wavelet-LLL_gldm_LargeDependenceEmphasis | wavelet-LLL_gldm_LargeDependenceHighGrayLevelEmphasis | wavelet-LLL_gldm_LargeDependenceLowGrayLevelEmphasis | wavelet-LLL_gldm_LowGrayLevelEmphasis | wavelet-LLL_gldm_SmallDependenceEmphasis | wavelet-LLL_gldm_SmallDependenceHighGrayLevelEmphasis | wavelet-LLL_gldm_SmallDependenceLowGrayLevelEmphasis | ROI | Overall.Stage | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Patient | |||||||||||||||||||||
| LUNG1-272_000000_GTV-1_mask | 0.713259 | 0.487347 | 14.2573 | 29.254915 | 25.495098 | 31.400637 | 35.608988 | 37.868192 | 5713.416667 | 20.86634 | ... | 11955.686684 | 27.442472 | 349332.116971 | 0.002377 | 0.00028 | 0.233726 | 2319.671592 | 0.000215 | GTV-1_mask | NaN |
1 rows × 1220 columns
Exclude the patients with the unknown outcomes:
fs.handle_nan(axis=0)
Number of observations: 148
Class labels: ['I' 'II' 'IIIa' 'IIIb']
Classes balance: [0.24324324324324326, 0.0945945945945946, 0.23648648648648649, 0.42567567567567566]
Visualize feature values distribution in classes for the first 12 features (in .html report):
fs.plot_distribution(fs._feature_column[:8])
Visualize feature values distribution in classes for the first 12 features only for the selected classes (in .html report):
fs.plot_distribution(fs._feature_column[:8], ['I', 'IIIb'])
Visualize mutual feature correlation coefficient (Spearman's) matrix for the first 12 features (in .html report):
fs.plot_correlation_matrix(fs._feature_column[:12])
Even though we have more than 2 classes and cannot perform Mann-Whitney test for all the classes, we can perform it for the pairs of the classes (result in .html report):
fs.plot_MW_p(fs._feature_column[:100], ['I', 'IIIb'])
The same situation is with univariate analysis - we can perform it for the selected pairs of the classes:
fs.plot_univariate_roc(fs._feature_column[:100], ['I', 'IIIb'])
Calculate the basic statistics for each feature (save the values in data/features/features_basic_stats.xlsx); for multi-class analysis the following are available: number of NaN, mean, std, min, max
fs.calculate_basic_stats(volume_feature='original_shape_VoxelVolume')
Check the table:
print('Basic statistics for each feature')
pd.read_excel('../data/features/extracted_features_full_basic_stats.xlsx')
Basic statistics for each feature
| Unnamed: 0 | NaN | Mean | Std | Min | Max | volume_corr | |
|---|---|---|---|---|---|---|---|
| 0 | original_shape_Elongation | 0 | 0.720328 | 0.161721 | 0.062127 | 0.974104 | 0.037515 |
| 1 | original_shape_Flatness | 0 | 0.559677 | 0.154895 | 0.047315 | 0.856767 | 0.099032 |
| 2 | original_shape_LeastAxisLength | 0 | 32.055804 | 15.983620 | 6.643777 | 85.495660 | 0.973238 |
| 3 | original_shape_MajorAxisLength | 0 | 61.595666 | 35.336125 | 13.611433 | 240.822486 | 0.842114 |
| 4 | original_shape_Maximum2DDiameterColumn | 0 | 63.064956 | 33.057474 | 15.620499 | 157.632484 | 0.950909 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 1213 | wavelet-LLL_gldm_LargeDependenceLowGrayLevelEm... | 0 | 1.646810 | 8.820031 | 0.001036 | 79.946280 | 0.070593 |
| 1214 | wavelet-LLL_gldm_LowGrayLevelEmphasis | 0 | 0.007969 | 0.039767 | 0.000069 | 0.369746 | -0.805811 |
| 1215 | wavelet-LLL_gldm_SmallDependenceEmphasis | 0 | 0.313546 | 0.177561 | 0.009452 | 0.755977 | -0.634677 |
| 1216 | wavelet-LLL_gldm_SmallDependenceHighGrayLevelE... | 0 | 2431.019809 | 1077.764252 | 0.093515 | 5302.913608 | -0.274001 |
| 1217 | wavelet-LLL_gldm_SmallDependenceLowGrayLevelEm... | 0 | 0.000440 | 0.000872 | 0.000013 | 0.007314 | -0.872534 |
1218 rows × 7 columns
Volume analysis (result in .html report):
fs.volume_analysis(volume_feature='original_shape_VoxelVolume')