Repository navigation
Expand file tree
/
Copy pathpythonCode.py
More file actions
1979 lines (1814 loc) · 89.9 KB
/
Copy pathpythonCode.py
File metadata and controls
1979 lines (1814 loc) · 89.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import numpy as np
import pandas as pd
import seaborn as sns
from scipy import stats
from sklearn.preprocessing import RobustScaler
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.impute import KNNImputer
from sklearn.preprocessing import OneHotEncoder
from scipy.spatial.distance import cdist
from sklearn.cluster import AgglomerativeClustering
from sklearn_extra.cluster import KMedoids
from sklearn.metrics import silhouette_score
import prince
from scipy.stats import kstest, shapiro, mannwhitneyu, spearmanr, kendalltau, kruskal, chi2_contingency
from statsmodels.stats.multitest import multipletests
from itertools import combinations
from pandas import ExcelWriter
filepath = '/Users/pietropante/Desktop/Python_assignment/Files4Python/dataset_project_eHealth20242025.csv'
df = pd.read_csv(filepath)
categorical_columns = ['gender', 'marital', 'education']
for col in df.columns:
if col in categorical_columns:
df[col] = df[col].astype('Int64')
df[col] = df[col].astype('object')
else:
df[col] = df[col].astype('float64')
# print(df.info())
total_missing = df.isnull().sum().sum()
rows_with_missing = df.isnull().any(axis=1).sum()
print(f"Total number of missing values in the DataFrame: {total_missing}")
print(f"# of rows containing at least one missing value: {rows_with_missing}")
categorical = df[categorical_columns]
print(categorical.shape)
numerical = df.drop(columns=categorical_columns)
print(numerical.shape)
print("Numerical columns:\n", numerical.columns.tolist())
print("Categorical columns :\n", categorical.columns.tolist())
# The use of KNN is probably the most sensible choice for several reasons. The main one is that
# KNN leverages the correlation present in the dataset, considering how other
# values (particularly those of correlated variables) influence the values to be imputed
knn_imputer = KNNImputer(
n_neighbors=5) # With 160 instances, set 5 as the value since the literature offers no clear guidance
numerical_imputed = np.round(
knn_imputer.fit_transform(
numerical)) # Round to the nearest integer for the sake of reasonableness in the imputed values
numerical = pd.DataFrame(numerical_imputed, columns=numerical.columns)
for col in categorical.columns:
mode_value = categorical[col].mode()[0]
categorical[col] = categorical[col].fillna(mode_value)
categorical[col] = categorical[col].astype(
'object') # Pandas tries to maintain the most "suitable" data type. If the column
# mostly contains strings and the mode is a string, no issue arises. However, if the column has numeric values and the
# mode is numeric, pandas may infer the best data type. In this way, we constrained this behaviour.
df = pd.concat([numerical, categorical], axis=1)
# For the data's consistency, imputations must be integers given the numerical columns in the dataset.
# Therefore, we perform this check to ensure they have been correctly rounded to the nearest integer.
numerical_columns = df.select_dtypes(include=['float64', 'int64']).columns
non_integers_info = []
for col in numerical_columns:
non_integer_values = df[col][df[col] != df[col].astype('int64')]
for idx, value in non_integer_values.items():
non_integers_info.append((col, idx, value))
if non_integers_info:
print("Non-integer values found and their positions (column, row, value):")
for col, idx, value in non_integers_info:
print(f"Column: {col}, Row: {idx}, Value: {value}")
else:
print("No non-integer values found.")
print("\nDataFrame after imputation:")
print(df.info())
total_missing = df.isnull().sum().sum()
rows_with_missing = df.isnull().any(axis=1).sum()
print(f"Total number of missing values in the DataFrame after MV filling usink KNN: {total_missing}")
print(f"Number of rows containing at least one missing value after MV filling using KNN: {rows_with_missing}")
df = df.drop_duplicates()
print("Shape after removing duplicate rows:", df.shape)
# Calculates composite scores based on specific sets of columns related to different assessments.
# It calculates scores for PHQ9, GAD7, ASQ, ASRS, and SSBA (alcohol, internet, drug, and gambling) based on predefined rules from literature.
# After calculating the scores, it drops the individual assessment columns from the dataframe to keep only the final scores.
#PHQ
df['phq9_score'] = df[['phq_1', 'phq_2', 'phq_3', 'phq_4', 'phq_5', 'phq_6', 'phq_7', 'phq_8', 'phq_9']].sum(axis=1)
df = df.drop(columns=['phq_1', 'phq_2', 'phq_3', 'phq_4', 'phq_5', 'phq_6', 'phq_7', 'phq_8', 'phq_9'])
#GAD
df['gad7_score'] = df[['gad_1', 'gad_2', 'gad_3', 'gad_4', 'gad_5', 'gad_6', 'gad_7']].sum(axis=1)
df = df.drop(columns=['gad_1', 'gad_2', 'gad_3', 'gad_4', 'gad_5', 'gad_6', 'gad_7'])
#ASQ
rule_1_columns = ['asq_1', 'asq_2', 'asq_4', 'asq_5', 'asq_6', 'asq_7', 'asq_9', 'asq_12',
'asq_13', 'asq_16', 'asq_18', 'asq_19', 'asq_20', 'asq_21', 'asq_22', 'asq_23',
'asq_26', 'asq_33', 'asq_35', 'asq_39', 'asq_41', 'asq_42', 'asq_43', 'asq_45',
'asq_46']
rule_2_columns = ['asq_3', 'asq_8', 'asq_10', 'asq_11', 'asq_14', 'asq_15', 'asq_17', 'asq_24',
'asq_25', 'asq_27', 'asq_28', 'asq_29', 'asq_30', 'asq_31', 'asq_32', 'asq_34',
'asq_36', 'asq_37', 'asq_38', 'asq_40', 'asq_44', 'asq_47', 'asq_48', 'asq_49', 'asq_50']
def calculate_score(row):
score = 0
for col in rule_1_columns:
if row[col] in [2, 3]:
score += 1
for col in rule_2_columns:
if row[col] in [0, 1]:
score += 1
return score
df['asq_score'] = df.apply(calculate_score, axis=1)
df = df.drop(columns=['asq_1', 'asq_2', 'asq_3', 'asq_4', 'asq_5', 'asq_6', 'asq_7', 'asq_8', 'asq_9', 'asq_10',
'asq_11', 'asq_12', 'asq_13', 'asq_14', 'asq_15', 'asq_16', 'asq_17', 'asq_18', 'asq_19',
'asq_20',
'asq_21', 'asq_22', 'asq_23', 'asq_24', 'asq_25', 'asq_26', 'asq_27', 'asq_28', 'asq_29',
'asq_30',
'asq_31', 'asq_32', 'asq_33', 'asq_34', 'asq_35', 'asq_36', 'asq_37', 'asq_38', 'asq_39',
'asq_40',
'asq_41', 'asq_42', 'asq_43', 'asq_44', 'asq_45', 'asq_46', 'asq_47', 'asq_48', 'asq_49',
'asq_50'])
#ASRS
rule_1_columns = ['asrs_1', 'asrs_2', 'asrs_3']
rule_2_columns = ['asrs_4', 'asrs_5', 'asrs_6']
def calculate_asrs_score(row):
score = 0
for col in rule_1_columns:
if row[col] in [2, 3, 4]:
score += 1
for col in rule_2_columns:
if row[col] in [3, 4]:
score += 1
return score
df['asrs_score'] = df.apply(calculate_asrs_score,
axis=1)
df = df.drop(columns=['asrs_1', 'asrs_2', 'asrs_3', 'asrs_4', 'asrs_5', 'asrs_6'])
#SSBA
df['ssba_alcohol_score'] = df[['ssba_alcohol_1', 'ssba_alcohol_2', 'ssba_alcohol_3', 'ssba_alcohol_4']].sum(axis=1)
df.drop(['ssba_alcohol_1', 'ssba_alcohol_2', 'ssba_alcohol_3', 'ssba_alcohol_4'], axis=1, inplace=True)
df['ssba_internet_score'] = df[['ssba_internet_1', 'ssba_internet_2', 'ssba_internet_3', 'ssba_internet_4']].sum(axis=1)
df.drop(['ssba_internet_1', 'ssba_internet_2', 'ssba_internet_3', 'ssba_internet_4'], axis=1, inplace=True)
df['ssba_drug_score'] = df[['ssba_drug_1', 'ssba_drug_2', 'ssba_drug_3', 'ssba_drug_4']].sum(axis=1)
df.drop(['ssba_drug_1', 'ssba_drug_2', 'ssba_drug_3', 'ssba_drug_4'], axis=1, inplace=True)
df['ssba_gambling_score'] = df[['ssba_gambling_1', 'ssba_gambling_2', 'ssba_gambling_3', 'ssba_gambling_4']].sum(axis=1)
df.drop(['ssba_gambling_1', 'ssba_gambling_2', 'ssba_gambling_3', 'ssba_gambling_4'], axis=1,
inplace=True)
# print(df.info())
df_of_interest = df
categorical = df_of_interest[['gender', 'marital', 'education']]
numerical = df_of_interest.drop(columns=categorical.columns)
print("Numerical columns:\n", numerical.columns.tolist())
print("Categorical columns :\n", categorical.columns.tolist())
# Checks the normality of numerical columns in the dataframe using the Shapiro-Wilk test.
# It first tests the original distribution of each column and if the p-value is greater than or equal to 0.05,
# the column is considered normally distributed. If not, the logarithmic transformation (log(x) + 1) is applied
# and the normality is tested again. It then categorizes columns into normal and non-normal distributions,
# printing the results for both the original and logarithmic versions of the variables.
normal_columns = []
non_normal_columns = []
for col in numerical.columns:
stat, p_value = stats.shapiro(
numerical[col].dropna()) # For small distributions (ours is 150 (already dropped out)), it is better to use the Shapiro-Wilk test.
if p_value >= 0.05:
normal_columns.append((col, p_value, 'original'))
else:
non_normal_columns.append((col, p_value, 'original'))
numerical_log = np.log1p(numerical[col]) # Natural logarithm (ln(x))+1 to avoid ln(0)
stat_log, p_value_log = stats.shapiro(numerical_log.dropna())
if p_value_log >= 0.05:
normal_columns.append((col, p_value_log, 'log'))
else:
non_normal_columns.append((col, p_value_log, 'log'))
if normal_columns or non_normal_columns:
print("The following variables are normally distributed:")
for col, p_value, var_type in normal_columns:
if var_type == 'original':
print(f"Column: {col} (original), p-value: {p_value:.4f}")
elif var_type == 'log':
print(f"Column: {col} (logarithmic), p-value: {p_value:.4f}")
print("\nThe following variables are not normally distributed:")
for col, p_value, var_type in non_normal_columns:
if var_type == 'original':
print(f"Column: {col} (original), p-value: {p_value:.4f}")
elif var_type == 'log':
print(f"Column: {col} (logarithmic), p-value: {p_value:.4f}")
else:
print("No numerical variables are present.")
# Plots.
for col, p_value, var_type in normal_columns + non_normal_columns:
if var_type == 'original':
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
sns.histplot(numerical[col], kde=True, bins=30, color='skyblue', stat="density", linewidth=0)
plt.title(f"Original distribution of {col} (p-value: {p_value:.4f})", fontsize=12)
plt.xlabel(col, fontsize=10)
plt.ylabel('Density', fontsize=10)
plt.subplot(1, 2, 2)
numerical_log = np.log1p(numerical[col])
sns.histplot(numerical_log, kde=True, bins=30, color='lightgreen', stat="density", linewidth=0)
plt.title(f"Distribution log({col})", fontsize=12)
plt.xlabel(f"log({col})", fontsize=10)
plt.ylabel('Density', fontsize=10)
plt.suptitle(f"Comparison of Original and Log Transformed Distribution for {col}", fontsize=14)
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
# Heatmap
plt.figure(figsize=(12, 10))
sns.heatmap(numerical.corr(), annot=True, cmap="coolwarm", fmt=".2f")
plt.title('Correlation Matrix Heatmap')
plt.show()
# Pairplots for numerical variables to visualize distributions and bivariate relationships between all numerical variables.
pairplot_fig = sns.pairplot(numerical, diag_kind="kde", plot_kws={'alpha': 0.5})
pairplot_fig.fig.suptitle('Pairplots for Numerical Variables', y=1.02, fontsize=16)
plt.show()
# Performs independence tests between pairs of numerical variables using Spearman's and Kendall's correlation tests.
# It loops through all pairs of numerical columns and computes the Spearman and Kendall statistics and p-values.
# The results are stored in 'independence_results', including the variables tested, the test used, the test statistics,
# the p-values and whether the variables are considered independent (p-value >= 0.05).
independence_results = []
for i, num_col1 in enumerate(numerical.columns):
for num_col2 in numerical.columns[i + 1:]:
# Spearman and Kendall test
spearman_stat, spearman_p = spearmanr(numerical[num_col1], numerical[num_col2])
kendall_stat, kendall_p = kendalltau(numerical[num_col1], numerical[num_col2])
independence_results.append({
"Variable 1": num_col1,
"Variable 2": num_col2,
"Test": "Spearman",
"Statistic": spearman_stat,
"P-value": spearman_p,
"Independent": spearman_p >= 0.05
})
independence_results.append({
"Variable 1": num_col1,
"Variable 2": num_col2,
"Test": "Kendall",
"Statistic": kendall_stat,
"P-value": kendall_p,
"Independent": kendall_p >= 0.05
})
# Performs independence tests between categorical and numerical variables using the Kruskal-Wallis test.
# It loops through all categorical columns and numerical columns, grouping the numerical values by the levels of the categorical variable.
# The Kruskal-Wallis test is applied to these groups to check if the numerical variable differs across the categories.
# The results, including the test statistics, p-values and whether the variables are independent (p-value >= 0.05), are stored in 'independence_results'.
for cat_col in categorical.columns:
for num_col in numerical.columns:
# Kruskal-Wallis Test
groups = [numerical[num_col][df[cat_col] == level] for level in df[cat_col].unique() if not pd.isna(level)]
kruskal_stat, kruskal_p = kruskal(*groups) if len(groups) > 1 else (np.nan, np.nan)
independence_results.append({
"Variable 1": num_col,
"Variable 2": cat_col,
"Test": "Kruskal-Wallis",
"Statistic": kruskal_stat,
"P-value": kruskal_p,
"Independent": kruskal_p >= 0.05
})
# Performs independence tests between pairs of categorical variables using the Chi-Squared test.
# It loops through all pairs of categorical columns, creating a contingency table for each pair.
# The Chi-Squared test is applied to check if the categorical variables are independent.
# The results, including the test statistics, p-values and whether the variables are independent (p-value >= 0.05),
# are stored in 'independence_results'.
for i, cat_col1 in enumerate(categorical.columns):
for cat_col2 in categorical.columns[i + 1:]:
# Chi-squared Test. (Fisher in Python can only be performed on 2x2 tables, so I use chi-squared as an approximation)
contingency_table = pd.crosstab(df[cat_col1], df[cat_col2])
chi2_stat, chi2_p, _, _ = chi2_contingency(contingency_table)
independence_results.append({
"Variable 1": cat_col1,
"Variable 2": cat_col2,
"Test": "Chi-Square",
"Statistic": chi2_stat,
"P-value": chi2_p,
"Independent": chi2_p >= 0.05
})
independence_results_df = pd.DataFrame(independence_results)
pd.set_option('display.max_columns', independence_results_df.shape[0] + 1)
pd.set_option('display.max_rows', independence_results_df.shape[0] + 1)
print(independence_results_df.drop("Statistic", axis=1))
# Scales the numerical columns using the RobustScaler to reduce the influence of outliers.
# It then generates a boxplot to visualize the distribution of the scaled numerical variables and detect potential outliers.
# Afterward, it calculates the InterQuartile Range (IQR) for each column and identifies values outside the range of 1.5 times the IQR.
# If outliers are detected, it stores the indices and columns of the outliers and prints them.
# If no outliers are found, it confirms this based on the boxplot visualization.
scaler = RobustScaler()
numerical_scaled = scaler.fit_transform(numerical)
numerical_scaled = pd.DataFrame(numerical_scaled, columns=numerical.columns)
numerical_scaled.boxplot(figsize=(12, 6))
plt.xticks(rotation=90)
plt.title("Boxplot of Normalized Numeric Columns for Outlier Detection")
plt.show()
# A riconferma:
outlier_info = []
for col in numerical_scaled.columns:
Q1 = numerical_scaled[col].quantile(0.25)
Q3 = numerical_scaled[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = numerical_scaled[(numerical_scaled[col] < lower_bound) | (numerical_scaled[col] > upper_bound)]
for index in outliers.index:
outlier_info.append((index, col))
if outlier_info:
print("Indices of outlier instances with corresponding columns:")
for index, col in outlier_info:
print(f"Index: {index}, Column: {col}")
else:
print("No outliers detected, as evinced by the boxplot.")
# Creates a new dataframe 'df_postpp'(preprocessing) that contains both the scaled numerical and categorical variables of interest for our aims.
columns_to_select = ['age', 'income', 'phq9_score', 'gad7_score']
numerical_scaled = numerical_scaled[columns_to_select]
df_postpp = pd.concat([numerical_scaled, categorical], axis=1)
# Saved as CSV
df_postpp.to_csv("/Users/pietropante/Desktop/Python_assignment/Files4Python/post_preprocessingRS_df.csv",
index=False) # 'index=False' prevents saving the index
# One-hot encoding in applied to the categorical columns using the OneHotEncoder.
# The parameter 'drop="first"' ensures that for each categorical variable with m categories, only (m-1) columns are created, avoiding multicollinearity.
# It then transforms the categorical data into a new DataFrame with the encoded variables.
# Finally, it concatenates the scaled numerical columns and the one-hot encoded categorical columns into a new DataFrame 'df_encoded'.
one_hot_encoder = OneHotEncoder(sparse_output=False,
drop='first')
categorical_encoded = one_hot_encoder.fit_transform(categorical).astype(int)
categorical_encoded_df = pd.DataFrame(categorical_encoded,
columns=one_hot_encoder.get_feature_names_out(categorical.columns))
df_encoded = pd.concat([numerical_scaled, categorical_encoded_df.reset_index(drop=True)], axis=1)
# PCA
# Principal Component Analysis (PCA) is applied to the encoded dataset to reduce its dimensionality.
# It first calculates the explained variance ratio for each principal component and the cumulative explained variance.
# Then, it identifies the number of components required to explain at least 80% of the cumulative variance.
# A new DataFrame 'df_pca_selected' is created, containing only the selected principal components that account for 80% of the variance.
pca = PCA()
df_pca = pca.fit_transform(df_encoded)
explained_variance = pca.explained_variance_ratio_
cumulative_explained_variance = np.cumsum(explained_variance)
n_components_80 = np.argmax(cumulative_explained_variance >= 0.80) + 1 # +1 because the index starts at 0
print(f"# of components to have 80% of cumulative explained variance: {n_components_80}")
print(
f"Cumulative explained variance for {n_components_80} components: {cumulative_explained_variance[n_components_80 - 1]:.4f}")
df_pca_selected = pd.DataFrame(df_pca[:, :n_components_80], columns=[f'PC{i + 1}' for i in range(n_components_80)])
plt.figure(figsize=(8, 6))
plt.plot(range(1, len(cumulative_explained_variance) + 1), cumulative_explained_variance * 100, color='b', marker='o')
plt.axhline(y=80, color='r', linestyle='--', label="80% della varianza")
plt.xlabel('# of components')
plt.ylabel('Cumulative explained variance (%)')
plt.title('PCA - Cumulative explained variance')
plt.legend()
plt.show()
# FAMD
# Factor Analysis of Mixed Data (FAMD) is applied to the dataset, reducing the dimensionality of both numerical and categorical variables.
# It calculates the eigenvalues, explained variance, and cumulative variance of the components.
# The number of components required to explain at least 80% of the variance is determined.
# A plot is created to visualize the cumulative explained variance and the 80% threshold.
# The transformed data, containing the selected components that explain at least 80% of the variance, is stored in 'df_famd_80'.
famd = prince.FAMD(n_components=df_postpp.shape[1], random_state=42)
famd = famd.fit(df_postpp)
eigenvalues = famd.eigenvalues_
explained_variance = eigenvalues / eigenvalues.sum()
cumulative_variance = explained_variance.cumsum()
num_components = (cumulative_variance <= 0.80).sum() + 1
print(f"Number of components needed to explain at least 80% of the variance: {num_components}")
plt.figure(figsize=(10, 6))
plt.plot(range(1, len(cumulative_variance) + 1), cumulative_variance, color='b', marker='o',
label='Cumulative Variance')
for i, var in enumerate(cumulative_variance):
plt.text(i + 1, var, f"{var:.3f}", ha='center', va='bottom', fontsize=9)
plt.axhline(y=0.80, color='r', linestyle='--', label='80% Explained Variance')
plt.xlabel("Number of Components")
plt.ylabel("Cumulative Explained Variance")
plt.title("FAMD - Cumulative Explained Variance")
plt.legend()
plt.show()
df_famd = famd.transform(df_postpp)
df_famd_80 = pd.DataFrame(
df_famd.iloc[:, :num_components].to_numpy(),
columns=[f"FAMD_{i + 1}" for i in range(num_components)],
index=df_postpp.index
)
print("Reduced dataset with principal components explaining at least 80% of the variance:")
print(df_famd_80.head())
# Bar Plot
plt.figure(figsize=(10, 6))
colors = ['skyblue' if i < num_components else 'gray' for i in range(len(explained_variance))]
plt.bar(range(1, len(explained_variance) + 1), explained_variance, color=colors)
for i, var in enumerate(explained_variance):
plt.text(i + 1, var, f"{var:.2f}", ha='center', va='bottom', fontsize=9)
plt.axvline(x=num_components + 0.5, color='red', linestyle='--', label=f'Components Used (n={num_components})')
plt.xlabel("Principal Component")
plt.ylabel("Explained Variance")
plt.title("Explained Variance per Principal Component")
plt.legend()
plt.show()
# FAMD from R
filepath = "/Users/pietropante/Desktop/Python_assignment/Files4Python/famd_80_fromR.csv"
df_famd_80R = pd.read_csv(filepath)
# PCAMIX from R
filepath = "/Users/pietropante/Desktop/Python_assignment/Files4Python/pcamix_fromR.csv"
df_pcamix_80R = pd.read_csv(filepath)
df_transforms = {
'PCA': df_pca_selected,
'FAMD': df_famd_80,
'FAMD_R': df_famd_80R,
'PCAMIX_R': df_pcamix_80R
}
for tag, df_scaled in df_transforms.items():
# Evaluates four different linkage methods ('ward', 'average', 'complete' and 'single') for hierarchical clustering.
# Uses the Silhouette Score to assess clustering quality and the Elbow Method to measure intra-cluster distances.
def find_optimal_clusters(X, tag):
linkage_methods = ['ward', 'average', 'complete', 'single']
fig, axs = plt.subplots(1, 2, figsize=(14, 6))
for linkage_method in linkage_methods:
silhouette_avgs = []
distortions = []
for k in range(2, 9):
clustering = AgglomerativeClustering(n_clusters=k, linkage=linkage_method)
clustering.fit(X)
labels = clustering.labels_
# Silhouette Score
silhouette_avg = silhouette_score(X, labels)
silhouette_avgs.append(silhouette_avg)
# Elbow Method
cluster_centers = np.array([X[labels == i].mean(axis=0) for i in range(k)])
dist = sum(
np.sum(cdist(X[labels == i], cluster_centers[i].reshape(1, -1), 'euclidean')) for i in range(k))
distortions.append(dist)
axs[0].plot(range(2, 9), silhouette_avgs, marker='o', label=f'{linkage_method}')
for x, y in zip(range(2, 9), silhouette_avgs):
axs[0].text(x, y, f'{y:.3f}', ha='center', va='bottom', fontsize=9)
axs[1].plot(range(2, 9), distortions, marker='o', label=f'{linkage_method}')
for x, y in zip(range(2, 9), distortions):
axs[1].text(x, y, f'{y:.0f}', ha='center', va='bottom', fontsize=9)
axs[0].set_xlabel('Number of clusters')
axs[0].set_ylabel('Silhouette Score')
axs[0].set_title(f'{tag} - Silhouette Score for Linkage Methods')
axs[1].set_xlabel('Number of clusters')
axs[1].set_ylabel('Sum of Intra-cluster Distances')
axs[1].set_title(f'{tag} - Elbow Method for Linkage Methods')
axs[0].legend(title="Linkage", loc='best')
axs[1].legend(title="Linkage", loc='best')
plt.tight_layout()
plt.show()
# This function compares the performance of Agglomerative Clustering (using the Ward linkage method)
# and K-Medoids clustering. The function evaluates the clustering quality
# using the Silhouette Score and the Elbow Method. Note: The Ward linkage method was chosen because the analysis of
# the plotted figures for the Silhouette Score and intra-cluster distances (inertia) shows that it provides
# the best trade-off between cluster cohesion and separation.
def compare_clustering(X, tag, df):
silhouette_avgs_ward = []
silhouette_avgs_kmedoids = []
distortions_ward = []
distortions_kmedoids = []
ward_labels = []
kmedoids_labels = []
for k in range(2, 9):
# Agglomerative clustering
model_ward = AgglomerativeClustering(n_clusters=k, linkage='ward')
model_ward.fit(X)
labels_ward = model_ward.labels_
silhouette_avg_ward = silhouette_score(X, labels_ward)
silhouette_avgs_ward.append(silhouette_avg_ward)
ward_labels.append(labels_ward)
# Distortion for Agglomerative
cluster_centers = np.array([X[labels_ward == i].mean(axis=0) for i in range(k)])
dist_ward = sum(
np.sum(cdist(X[labels_ward == i], cluster_centers[i].reshape(1, -1), 'euclidean')) for i in range(k))
distortions_ward.append(dist_ward)
# K-Medoids clustering
kmedoids = KMedoids(n_clusters=k, init='k-medoids++', metric='manhattan', max_iter=500, random_state=0)
kmedoids.fit(X)
labels_kmedoids = kmedoids.labels_
silhouette_avg_kmedoids = silhouette_score(X, labels_kmedoids)
silhouette_avgs_kmedoids.append(silhouette_avg_kmedoids)
kmedoids_labels.append(labels_kmedoids)
# Distortion for K-Medoids
medoid_centers = np.array([X[labels_kmedoids == i].mean(axis=0) for i in range(k)])
dist_kmedoids = sum(
np.sum(cdist(X[labels_kmedoids == i], medoid_centers[i].reshape(1, -1), 'euclidean')) for i in range(k))
distortions_kmedoids.append(dist_kmedoids)
if tag == 'PCA':
best_ward_labels = ward_labels[1]
best_kmedoids_labels = kmedoids_labels[1]
elif tag == 'FAMD':
best_ward_labels = ward_labels[3]
best_kmedoids_labels = kmedoids_labels[1]
elif tag == 'FAMD_R':
best_ward_labels = ward_labels[1]
best_kmedoids_labels = kmedoids_labels[2]
elif tag == 'PCAMIX_R':
best_ward_labels = ward_labels[1]
best_kmedoids_labels = kmedoids_labels[1]
# Add results to dataframe
df[f'{tag}_Agglomerative_Cluster'] = best_ward_labels
df[f'{tag}_KMedoids_Cluster'] = best_kmedoids_labels
# Plot results
fig, axs = plt.subplots(1, 2, figsize=(14, 6))
axs[0].plot(range(2, 9), silhouette_avgs_ward, marker='o', label='Agglomerative (Ward)', color='blue')
for x, y in zip(range(2, 9), silhouette_avgs_ward):
axs[0].text(x, y, f'{y:.3f}', ha='center', va='bottom', fontsize=9)
axs[0].plot(range(2, 9), silhouette_avgs_kmedoids, marker='o', label='K-Medoids', color='red')
for x, y in zip(range(2, 9), silhouette_avgs_kmedoids):
axs[0].text(x, y, f'{y:.3f}', ha='center', va='bottom', fontsize=9)
axs[1].plot(range(2, 9), distortions_ward, marker='o', label='Agglomerative (Ward)', color='blue')
for x, y in zip(range(2, 9), distortions_ward):
axs[1].text(x, y, f'{y:.0f}', ha='center', va='bottom', fontsize=9)
axs[1].plot(range(2, 9), distortions_kmedoids, marker='o', label='K-Medoids', color='red')
for x, y in zip(range(2, 9), distortions_kmedoids):
axs[1].text(x, y, f'{y:.0f}', ha='center', va='bottom', fontsize=9)
axs[0].set_xlabel('Number of clusters')
axs[0].set_ylabel('Silhouette Score')
axs[0].set_title(f'{tag} - Silhouette Score Comparison')
axs[0].legend()
axs[1].set_xlabel('Number of clusters')
axs[1].set_ylabel('Sum of Intra-cluster Distances')
axs[1].set_title(f'{tag} - Elbow Method Comparison')
axs[1].legend()
plt.tight_layout()
plt.show()
if tag in ['PCA', 'FAMD_R']:
return df, silhouette_avgs_ward, distortions_ward
elif tag in ['FAMD', 'PCAMIX_R']:
return df, silhouette_avgs_kmedoids, distortions_kmedoids
if tag == 'PCA':
dfPCA = df.copy()
find_optimal_clusters(df_scaled, tag)
dfPCA, silhPCA, distPCA = compare_clustering(df_scaled, tag, dfPCA)
elif tag == 'FAMD':
dfFAMD = df.copy()
find_optimal_clusters(df_scaled, tag)
dfFAMD, silhFAMD, distFAMD = compare_clustering(df_scaled, tag, dfFAMD)
elif tag == 'FAMD_R':
dfFAMD_R = df.copy()
find_optimal_clusters(df_scaled, tag)
dfFAMD_R, silhFAMD_R, distFAMD_R = compare_clustering(df_scaled, tag, dfFAMD_R)
elif tag == 'PCAMIX_R':
dfPCAMIX_R = df.copy()
find_optimal_clusters(df_scaled, tag)
dfPCAMIX_R, silhPCAMIX_R, distPCAMIX_R = compare_clustering(df_scaled, tag, dfPCAMIX_R)
# PCA
dfPCA.to_csv("/Users/pietropante/Desktop/Python_assignment/Files4Python/PCA_perFisher.csv", index=False)
dfPCA['PCA_Agglomerative_Cluster'] = dfPCA['PCA_Agglomerative_Cluster'].astype('object')
dfPCA['PCA_KMedoids_Cluster'] = dfPCA['PCA_KMedoids_Cluster'].astype('object')
num_unique_values_aggl = dfPCA['PCA_Agglomerative_Cluster'].nunique()
num_unique_values_kmed = dfPCA['PCA_KMedoids_Cluster'].nunique()
print(
f"The number of clusters in PCA_Agglomerative_Cluster is {num_unique_values_aggl}, while in PCA_KMedoids_Cluster it is {num_unique_values_kmed}.")
numeric_vars = df.select_dtypes(include=['float64', 'int64']).columns
# Function to perform normality tests, Kruskal-Wallis and Mann-Whitney U tests
def perform_statistical_analysis(df, target_col):
print(f"\nStatistical analysis for clustering '{target_col}':")
# Normality tests
normality_results = []
for var in numeric_vars:
data = df[var]
if data.nunique() > 1:
shapiro_stat, shapiro_p = shapiro(data)
ks_stat, ks_p = kstest(data, 'norm', args=(data.mean(), data.std()))
normality_results.append((var, shapiro_stat, shapiro_p, ks_stat, ks_p))
else:
print(f"Insufficient data or zero variance for Shapiro test on variable '{var}'")
# Store normality test results
normality_results_df = pd.DataFrame(normality_results,
columns=["Variable", "Shapiro_Statistic", "Shapiro_P", "KS_Statistic", "KS_P"])
# Identify variables that do not follow a normal distribution
non_normal_vars_df = normality_results_df[
(normality_results_df['Shapiro_P'] < 0.05) | (normality_results_df['KS_P'] < 0.05)]
print("\nVariables that do not follow a normal distribution:")
print(non_normal_vars_df)
if len(non_normal_vars_df) == len(numeric_vars):
print("\nAll the numerical variables are not normally distributed.")
# Kruskal-Wallis
kruskal_results = []
for var in numeric_vars:
data = df[[var, target_col]]
groups = [data[data[target_col] == group][var].values for group in data[target_col].unique()]
kruskal_stat, kruskal_p = kruskal(*groups)
kruskal_results.append((var, kruskal_stat, kruskal_p))
# Store Kruskal-Wallis results
kruskal_results_df = pd.DataFrame(
kruskal_results,
columns=["Variable", "Kruskal_Statistic", "Kruskal_P"]
)
# Identify significant variables with p-value < 0.05
significant_vars_df = kruskal_results_df[kruskal_results_df['Kruskal_P'] < 0.05]
print("\nSignificant variables after Kruskal-Wallis (p-value < 0.05):")
print(significant_vars_df)
# Mann-Whitney U Test for Pairwise Comparisons
mannwhitney_results = []
for var in significant_vars_df['Variable']:
data = df[[var, target_col]]
for (group1, group2) in combinations(data[target_col].unique(), 2):
group1_data = data[data[target_col] == group1][var].values
group2_data = data[data[target_col] == group2][var].values
mw_stat, mw_p = mannwhitneyu(group1_data, group2_data, alternative='two-sided')
mannwhitney_results.append((var, group1, group2, mw_stat, mw_p))
mannwhitney_results_df = pd.DataFrame(
mannwhitney_results,
columns=["Variable", "Group1", "Group2", "MW_Statistic", "MW_P"]
)
# Apply Bonferroni correction
mannwhitney_results_df['Bonferroni_P'] = multipletests(mannwhitney_results_df['MW_P'], method='bonferroni')[1]
significant_mw_results_df = mannwhitney_results_df[mannwhitney_results_df['Bonferroni_P'] < 0.05]
if len(significant_mw_results_df) > 0:
print("\nSignificant results of Mann-Whitney (Bonferroni corrected p-value < 0.05):")
print(significant_mw_results_df[['Variable', 'Group1', 'Group2', 'MW_Statistic', 'MW_P', 'Bonferroni_P']])
else:
print("\nNo significant Mann-Whitney results after Bonferroni correction.")
# Final Selection of Significant Variables
significant_mw_vars = significant_mw_results_df['Variable'].unique()
final_significant_vars = significant_vars_df[significant_vars_df['Variable'].isin(significant_mw_vars)]
print("\nVariabili finali significative da mantenere:")
print(final_significant_vars.iloc[:]['Variable'])
# Loop to perform analysis for both clustering methods
for target in ['PCA_Agglomerative_Cluster', 'PCA_KMedoids_Cluster']:
perform_statistical_analysis(dfPCA, target)
# FISHER ON R
# This R code performs Fisher's exact tests to compare categorical variables across different clusters.
# For each variable, pairwise comparisons between clusters are conducted, and a Bonferroni correction is applied
# to account for multiple testing. The output is a DataFrame with corrected p-values and an indication of significance.
'''> df <- read.csv("/Users/pietropante/Desktop/Python_assignment/Files4Python/PCA_perFisher.csv", stringsAsFactors = FALSE)
>
> # Modifica il data frame per adattarsi ai vari metodi di clustering
> df$gender <- as.factor(df$gender)
> df$education <- as.factor(df$education)
> df$marital <- as.factor(df$marital)
>
> # Funzione per eseguire il test di Fisher per tutte le variabili categoriali
> run_pairwise_fisher <- function(df_categorical) {
+ cluster_var <- "cluster"
+ cluster_levels <- levels(df_categorical[[cluster_var]])
+ variables <- names(df_categorical)[-which(names(df_categorical) == cluster_var)]
+ pairwise_results <- list()
+
+ # Esegui il test di Fisher per ciascuna variabile categoriale
+ for (var in variables) {
+ full_table <- table(df_categorical[[var]], df_categorical[[cluster_var]])
+ results_var <- combn(cluster_levels, 2, function(pair) {
+ subset_table <- full_table[, pair]
+ fisher_test <- fisher.test(subset_table, simulate.p.value = TRUE)
+ data.frame(
+ Variable = var,
+ Cluster_1 = pair[1],
+ Cluster_2 = pair[2],
+ Fisher_P = fisher_test$p.value
+ )
+ }, simplify = FALSE)
+ pairwise_results[[var]] <- do.call(rbind, results_var)
+ }
+
+ # Combina i risultati in un unico data frame
+ all_results <- do.call(rbind, pairwise_results)
+
+ # Applica la correzione di Bonferroni
+ m <- nrow(all_results) # numero totale di test (combinazioni di cluster)
+ alpha <- 0.05 # livello di significatività globale
+ all_results$Bonferroni_P <- p.adjust(all_results$Fisher_P, method = "bonferroni")
+
+ # Determina se i risultati sono significativi o meno
+ all_results$Significance <- ifelse(all_results$Bonferroni_P < alpha, "Significativa", "Non significativa")
+
+ return(all_results)
+ }
>
> # Esegui il test di Fisher per il primo clustering (Agglomerative)
> df$cluster <- as.factor(df$PCA_Agglomerative_Cluster)
> df_categorical <- df[, c("gender", "education", "marital", "cluster")]
> pairwise_results_agg <- run_pairwise_fisher(df_categorical)
>
> # Visualizza i risultati per Agglomerative Clustering
> cat("Agglomerative Clustering - Test di Fisher:\n")
Agglomerative Clustering - Test di Fisher:
> cat(" Variable Cluster_1 Cluster_2 Fisher_P\n")
Variable Cluster_1 Cluster_2 Fisher_P
> apply(pairwise_results_agg, 1, function(row) {
+ cat(sprintf("%10s %9s %9s %13.10f\n", row['Variable'], row['Cluster_1'], row['Cluster_2'], as.numeric(row['Fisher_P'])))
+ })
gender 0 1 0.2138930535
gender 0 2 0.6741629185
gender 1 2 0.4257871064
education 0 1 0.0004997501
education 0 2 0.0004997501
education 1 2 0.0004997501
marital 0 1 0.0004997501
marital 0 2 0.0004997501
marital 1 2 0.0004997501
NULL
> cat(" Bonferroni_P Significance\n")
Bonferroni_P Significance
> apply(pairwise_results_agg, 1, function(row) {
+ cat(sprintf("%13.9f %s\n", as.numeric(row['Bonferroni_P']), row['Significance']))
+ })
1.000000000 Non significativa
1.000000000 Non significativa
1.000000000 Non significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
NULL
>
> # Esegui il test di Fisher per il secondo clustering (KMedoids)
> df$cluster <- as.factor(df$PCA_KMedoids_Cluster)
> df_categorical <- df[, c("gender", "education", "marital", "cluster")]
> pairwise_results_kmedoids <- run_pairwise_fisher(df_categorical)
>
> # Visualizza i risultati per KMedoids Clustering
> cat("\nKMedoids Clustering - Test di Fisher:\n")
KMedoids Clustering - Test di Fisher:
> cat(" Variable Cluster_1 Cluster_2 Fisher_P\n")
Variable Cluster_1 Cluster_2 Fisher_P
> apply(pairwise_results_kmedoids, 1, function(row) {
+ cat(sprintf("%10s %9s %9s %13.10f\n", row['Variable'], row['Cluster_1'], row['Cluster_2'], as.numeric(row['Fisher_P'])))
+ })
gender 0 1 0.2663668166
gender 0 2 0.5327336332
gender 1 2 0.5692153923
education 0 1 0.0004997501
education 0 2 0.0004997501
education 1 2 0.0004997501
marital 0 1 0.0004997501
marital 0 2 0.0004997501
marital 1 2 0.0004997501
NULL
> cat(" Bonferroni_P Significance\n")
Bonferroni_P Significance
> apply(pairwise_results_kmedoids, 1, function(row) {
+ cat(sprintf("%13.9f %s\n", as.numeric(row['Bonferroni_P']), row['Significance']))
+ })
1.000000000 Non significativa
1.000000000 Non significativa
1.000000000 Non significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
0.004497751 Significativa
NULL
>
> # Funzione per eseguire il test di Fisher globale
> run_fisher_global <- function(df_categorical) {
+ cluster_var <- "cluster"
+ variables <- names(df_categorical)[-which(names(df_categorical) == cluster_var)]
+ results <- list()
+
+ # Esegui il test di Fisher per ciascuna variabile categoriale
+ for (var in variables) {
+ # Crea la tabella di contingenza completa per la variabile rispetto ai cluster
+ contingency_table <- table(df_categorical[[var]], df_categorical[[cluster_var]])
+
+ # Esegui il test di Fisher
+ fisher_test <- fisher.test(contingency_table, simulate.p.value = TRUE)
+
+ # Salva i risultati
+ results[[var]] <- data.frame(
+ Variable = var,
+ P_Value = fisher_test$p.value
+ )
+ }
+
+ # Combina i risultati in un unico data frame
+ all_results <- do.call(rbind, results)
+
+ # Determina se i risultati sono significativi o meno
+ all_results$Significance <- ifelse(all_results$P_Value < 0.05, "Significativa", "Non significativa")
+
+ return(all_results)
+ }
>
> # Esegui il test di Fisher globale per Agglomerative Clustering
> df$cluster <- as.factor(df$PCA_Agglomerative_Cluster)
> df_categorical <- df[, c("gender", "education", "marital", "cluster")]
> global_results_agg <- run_fisher_global(df_categorical)
>
> # Visualizza i risultati per Agglomerative Clustering
> cat("Agglomerative Clustering - Test di Fisher (Globale):\n")
Agglomerative Clustering - Test di Fisher (Globale):
> print(global_results_agg)
Variable P_Value Significance
gender gender 0.4502748626 Non significativa
education education 0.0004997501 Significativa
marital marital 0.0004997501 Significativa
>
> # Esegui il test di Fisher globale per KMedoids Clustering
> df$cluster <- as.factor(df$PCA_KMedoids_Cluster)
> df_categorical <- df[, c("gender", "education", "marital", "cluster")]
> global_results_kmedoids <- run_fisher_global(df_categorical)
>
> # Visualizza i risultati per KMedoids Clustering
> cat("\nKMedoids Clustering - Test di Fisher (Globale):\n")
KMedoids Clustering - Test di Fisher (Globale):
> print(global_results_kmedoids)
Variable P_Value Significance
gender gender 0.5102448776 Non significativa
education education 0.0004997501 Significativa
marital marital 0.0004997501 Significativa
'''
# Drop the 'PCA_KMedoids_Cluster' column as Agglomerative clustering is preferred
dfPCA = dfPCA.drop(columns=['PCA_KMedoids_Cluster'])
final_table = pd.DataFrame()
# Calculate the number of people in each cluster
cluster_counts = dfPCA['PCA_Agglomerative_Cluster'].value_counts()
# Add the cluster member counts to the header of the dataframe
final_table.loc['Cluster Size', cluster_counts.index] = cluster_counts.values
# Generates a summary table for the dataset grouped by clusters.
# It calculates the following statistics for each variable in the dataframe:
# - For numerical variables: median, min, and max values.
# - For categorical variables: mode and its frequency (percentage) in each cluster.
# Additionally, it includes the size of each cluster in the summary table.
# The final table displays these statistics for each cluster, making it easier to analyze the characteristics of each cluster.
for var in dfPCA.columns:
if var != 'PCA_Agglomerative_Cluster':
if dfPCA[var].dtype in ['float64', 'int64']:
stats_per_cluster = dfPCA.groupby('PCA_Agglomerative_Cluster')[var].agg(['median', 'min', 'max'])
stats_per_cluster = stats_per_cluster.round()
final_table.loc[var, stats_per_cluster.index] = [
f"{int(median)} ({int(min_val)} - {int(max_val)})"
for median, min_val, max_val in
zip(stats_per_cluster['median'], stats_per_cluster['min'], stats_per_cluster['max'])
]
else:
mode_per_cluster = dfPCA.groupby('PCA_Agglomerative_Cluster')[var].agg(lambda x: x.mode()[0])
mode_freq_per_cluster = dfPCA.groupby('PCA_Agglomerative_Cluster')[var].agg(
lambda x: (x == x.mode()[0]).sum())
total_counts = dfPCA.groupby('PCA_Agglomerative_Cluster')[var].count()
freq_per_cluster = mode_freq_per_cluster / total_counts * 100
final_table.loc[var, mode_per_cluster.index] = [
f"{mode} ({freq:.2f}%)" for mode, freq in zip(mode_per_cluster, freq_per_cluster)
]
final_table.columns = [f'Cluster {cluster}' for cluster in final_table.columns]
print(
"\nTabella con media (min-max) per variabili numeriche e moda con frequenza per variabili categoriche per ogni cluster:")
print(final_table)
# final_table.to_excel("Persona_Table.xlsx", sheet_name="PCA Cluster", index=True)
# FAMD
# Similarly to what was done previously.
dfFAMD.to_csv("/Users/pietropante/Desktop/Python_assignment/Files4Python/FAMD_perFisher.csv", index=False)
dfFAMD['FAMD_Agglomerative_Cluster'] = dfFAMD['FAMD_Agglomerative_Cluster'].astype('object')
dfFAMD['FAMD_KMedoids_Cluster'] = dfFAMD['FAMD_KMedoids_Cluster'].astype('object')
num_unique_values_aggl = dfFAMD['FAMD_Agglomerative_Cluster'].nunique()
num_unique_values_kmed = dfFAMD['FAMD_KMedoids_Cluster'].nunique()
print(
f"The number of clusters in FAMD_Agglomerative_Cluster is {num_unique_values_aggl}, while in FAMD_KMedoids_Cluster it is {num_unique_values_kmed}.")
numeric_vars = df.select_dtypes(include=['float64', 'int64']).columns
# Normality and Kruskal-Wallis tests
def perform_statistical_analysis(df, target_col):
print(f"\nAnalisi statistica per il clustering su '{target_col}':")
# Normality test
normality_results = []
for var in numeric_vars:
data = df[var]
if data.nunique() > 1:
shapiro_stat, shapiro_p = shapiro(data)
ks_stat, ks_p = kstest(data, 'norm', args=(data.mean(), data.std()))
normality_results.append((var, shapiro_stat, shapiro_p, ks_stat, ks_p))
else:
print(f"Insufficient data or zero variance for Shapiro test on variable '{var}'")
normality_results_df = pd.DataFrame(normality_results,
columns=["Variable", "Shapiro_Statistic", "Shapiro_P", "KS_Statistic", "KS_P"])
non_normal_vars_df = normality_results_df[
(normality_results_df['Shapiro_P'] < 0.05) | (normality_results_df['KS_P'] < 0.05)]
print("\nVariables that do not follow a normal distribution:")
print(non_normal_vars_df)
if len(non_normal_vars_df) == len(numeric_vars):
print("\nAll numeric variables are not distributed normally.")
# Kruskal-Wallis test
kruskal_results = []
for var in numeric_vars:
data = df[[var, target_col]]
groups = [data[data[target_col] == group][var].values for group in data[target_col].unique()]
kruskal_stat, kruskal_p = kruskal(*groups)
kruskal_results.append((var, kruskal_stat, kruskal_p))
kruskal_results_df = pd.DataFrame(
kruskal_results,
columns=["Variable", "Kruskal_Statistic", "Kruskal_P"]
)
significant_vars_df = kruskal_results_df[kruskal_results_df['Kruskal_P'] < 0.05]
print("\nVSignificant variables after Kruskal-Wallis (p-value < 0.05):")
print(significant_vars_df)
# Test di Mann-Whitney for significant variables
mannwhitney_results = []
for var in significant_vars_df['Variable']:
data = df[[var, target_col]]
for (group1, group2) in combinations(data[target_col].unique(), 2):
group1_data = data[data[target_col] == group1][var].values
group2_data = data[data[target_col] == group2][var].values
mw_stat, mw_p = mannwhitneyu(group1_data, group2_data, alternative='two-sided')
mannwhitney_results.append((var, group1, group2, mw_stat, mw_p))
mannwhitney_results_df = pd.DataFrame(
mannwhitney_results,
columns=["Variable", "Group1", "Group2", "MW_Statistic", "MW_P"]
)
# Bonferroni correction
mannwhitney_results_df['Bonferroni_P'] = multipletests(mannwhitney_results_df['MW_P'], method='bonferroni')[1]
significant_mw_results_df = mannwhitney_results_df[mannwhitney_results_df['Bonferroni_P'] < 0.05]
if len(significant_mw_results_df) > 0:
print("\nSignificant Mann-Whitney achievements (p-value Bonferroni corrected < 0.05):")
print(significant_mw_results_df[['Variable', 'Group1', 'Group2', 'MW_Statistic', 'MW_P', 'Bonferroni_P']])
else:
print("\nNo significant results from Mann-Whitney after Bonferroni's correction.")
# Select the significant variables from Kruskal-Wallis and Mann-Whitney
significant_mw_vars = significant_mw_results_df['Variable'].unique()
final_significant_vars = significant_vars_df[significant_vars_df['Variable'].isin(significant_mw_vars)]
print("\nSignificant final variables to keep:")
print(final_significant_vars.iloc[:]['Variable'])
# Loop to perform analysis for both clustering methods
for target in ['FAMD_Agglomerative_Cluster', 'FAMD_KMedoids_Cluster']:
perform_statistical_analysis(dfFAMD, target)
# FISHER IN R
'''> df <- read.csv("/Users/pietropante/Desktop/Python_assignment/Files4Python/FAMD_perFisher.csv", stringsAsFactors = FALSE)
>
> # Modifica il data frame per adattarsi ai vari metodi di clustering
> df$gender <- as.factor(df$gender)
> df$education <- as.factor(df$education)
> df$marital <- as.factor(df$marital)
>
> # Funzione per eseguire il test di Fisher per tutte le variabili categoriali
> run_pairwise_fisher <- function(df_categorical) {