231 lines
No EOL
8.4 KiB
Python
231 lines
No EOL
8.4 KiB
Python
import argparse
|
||
import os
|
||
from pathlib import Path
|
||
import logging
|
||
|
||
import pandas as pd
|
||
import numpy as np
|
||
import matplotlib.pyplot as plt
|
||
import seaborn as sns
|
||
from scipy import stats
|
||
import statsmodels.api as sm
|
||
import statsmodels.formula.api as smf
|
||
|
||
|
||
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
||
|
||
|
||
def load_data(path):
|
||
df = pd.read_csv(path)
|
||
logging.info("Loaded %d rows from %s", len(df), path)
|
||
return df
|
||
|
||
|
||
def prepare_data(df):
|
||
# Ensure required columns exist
|
||
required = {'Participant_ID', 'Happiness', 'Calendar_Adherence', 'Cleanliness_Adherence', 'Punctuality_Adherence'}
|
||
missing = required - set(df.columns)
|
||
if missing:
|
||
raise KeyError(f"Missing required columns: {missing}")
|
||
|
||
if 'Group' not in df.columns:
|
||
df['Group'] = 'Intervention'
|
||
df['Group'] = df['Group'].astype(str).str.strip().str.title()
|
||
|
||
# Normalize adherence to boolean (Yes/No or True/False)
|
||
for col in ['Calendar_Adherence', 'Cleanliness_Adherence', 'Punctuality_Adherence']:
|
||
df[col] = df[col].astype(str).str.strip().str.lower().map({'yes': True, 'no': False, 'true': True, 'false': False})
|
||
|
||
# Count habits per row
|
||
df['Habits_Count'] = (
|
||
df[['Calendar_Adherence', 'Cleanliness_Adherence', 'Punctuality_Adherence']].fillna(False).astype(int).sum(axis=1)
|
||
)
|
||
|
||
# Coerce Happiness to numeric and drop rows without Happiness
|
||
df['Happiness'] = pd.to_numeric(df['Happiness'], errors='coerce')
|
||
before = len(df)
|
||
df = df.dropna(subset=['Happiness'])
|
||
logging.info('Dropped %d rows without numeric Happiness', before - len(df))
|
||
|
||
return df
|
||
|
||
|
||
def descriptive_stats(df):
|
||
print('Dataset shape:', df.shape)
|
||
print('\nOverall summary:')
|
||
print(df['Happiness'].describe())
|
||
|
||
if 'Group' in df.columns:
|
||
print('\nRows by group:')
|
||
print(df['Group'].value_counts())
|
||
|
||
print('\nAverage happiness by group:')
|
||
print(df.groupby('Group')['Happiness'].agg(['mean', 'count', 'std']).round(3))
|
||
|
||
print('\nAverage happiness by number of habits completed:')
|
||
print(df.groupby('Habits_Count')['Happiness'].agg(['mean', 'count', 'std']).round(3))
|
||
|
||
print('\nMedian happiness by habits:')
|
||
print(df.groupby('Habits_Count')['Happiness'].median())
|
||
|
||
# Correlations
|
||
print('\nPearson correlation between Habits_Count and Happiness:')
|
||
print(df[['Habits_Count', 'Happiness']].corr().round(3))
|
||
|
||
print('\nPoint-biserial correlation (each habit vs happiness, intervention group only):')
|
||
habit_df = df[df['Group'] == 'Intervention'] if 'Group' in df.columns else df
|
||
for habit in ['Calendar_Adherence', 'Cleanliness_Adherence', 'Punctuality_Adherence']:
|
||
mask = ~habit_df[habit].isna()
|
||
if mask.sum() == 0:
|
||
print(f'{habit:22} (no data)')
|
||
continue
|
||
r, p = stats.pointbiserialr(habit_df.loc[mask, habit].astype(int), habit_df.loc[mask, 'Happiness'])
|
||
print(f"{habit:22} r = {r:.3f} p = {p:.4f}")
|
||
|
||
|
||
def cohen_d(x, y):
|
||
# Cohen's d for two independent samples
|
||
nx, ny = len(x), len(y)
|
||
dof = nx + ny - 2
|
||
pooled_sd = np.sqrt(((nx - 1) * x.std(ddof=1) ** 2 + (ny - 1) * y.std(ddof=1) ** 2) / dof)
|
||
return (x.mean() - y.mean()) / pooled_sd
|
||
|
||
|
||
def run_ols(df):
|
||
if 'Group' in df.columns:
|
||
model = smf.ols('Happiness ~ Habits_Count + C(Group)', data=df).fit()
|
||
print('\nOLS regression: Happiness ~ Habits_Count + Group')
|
||
else:
|
||
X = sm.add_constant(df['Habits_Count'])
|
||
y = df['Happiness']
|
||
model = sm.OLS(y, X).fit()
|
||
print('\nSimple OLS regression: Happiness ~ Habits_Count')
|
||
print(model.summary())
|
||
return model
|
||
|
||
|
||
def run_mixedlm(df):
|
||
# Random intercept for Participant_ID
|
||
try:
|
||
md = smf.mixedlm('Happiness ~ Habits_Count', data=df, groups=df['Participant_ID'])
|
||
mdf = md.fit(reml=False)
|
||
print('\nMixed-effects model (random intercept by Participant_ID):')
|
||
print(mdf.summary())
|
||
return mdf
|
||
except Exception as e:
|
||
logging.warning('MixedLM failed: %s', e)
|
||
return None
|
||
|
||
|
||
def make_plots(df, outdir, show_plots=False):
|
||
outdir = Path(outdir)
|
||
outdir.mkdir(parents=True, exist_ok=True)
|
||
sns.set_style('whitegrid')
|
||
|
||
# Boxplot by Habits_Count
|
||
plt.figure(figsize=(9, 6))
|
||
sns.boxplot(data=df, x='Habits_Count', y='Happiness', hue='Habits_Count', palette='viridis', dodge=False)
|
||
plt.legend([], [], frameon=False)
|
||
plt.title('Daily Happiness by Number of Habits Completed')
|
||
plt.xlabel('Number of habits followed (0–3)')
|
||
plt.ylabel('Happiness (1–10)')
|
||
f1 = outdir / 'happiness_by_habits_box.png'
|
||
plt.tight_layout()
|
||
plt.savefig(f1)
|
||
if show_plots:
|
||
plt.show()
|
||
plt.close()
|
||
|
||
# Violin / jitter + regression
|
||
plt.figure(figsize=(9, 6))
|
||
sns.violinplot(data=df, x='Habits_Count', y='Happiness', hue='Habits_Count', inner=None, palette='muted', dodge=False)
|
||
plt.legend([], [], frameon=False)
|
||
sns.stripplot(x='Habits_Count', y='Happiness', data=df, color='k', alpha=0.3, jitter=0.15)
|
||
plt.title('Happiness distribution by Habits Completed')
|
||
f2 = outdir / 'happiness_by_habits_violin.png'
|
||
plt.tight_layout()
|
||
plt.savefig(f2)
|
||
if show_plots:
|
||
plt.show()
|
||
plt.close()
|
||
|
||
# Participant average bar
|
||
participant_avg = df.groupby('Participant_ID')['Happiness'].mean().sort_values()
|
||
plt.figure(figsize=(12, 5))
|
||
sns.barplot(x=range(len(participant_avg)), y=participant_avg.values, hue=range(len(participant_avg)), palette='coolwarm', dodge=False)
|
||
plt.legend([], [], frameon=False)
|
||
plt.axhline(df['Happiness'].mean(), color='black', linestyle='--', alpha=0.6)
|
||
plt.xticks(range(len(participant_avg)), participant_avg.index.astype(str), rotation=45)
|
||
plt.title('Average Happiness per Participant (sorted)')
|
||
f3 = outdir / 'participant_avg_happiness.png'
|
||
plt.tight_layout()
|
||
plt.savefig(f3)
|
||
if show_plots:
|
||
plt.show()
|
||
plt.close()
|
||
|
||
if 'Group' in df.columns:
|
||
plt.figure(figsize=(7, 5))
|
||
sns.barplot(data=df, x='Group', y='Happiness', hue='Group', estimator='mean', errorbar='sd', palette='Set2', dodge=False)
|
||
plt.legend([], [], frameon=False)
|
||
plt.title('Mean Happiness by Group')
|
||
plt.ylabel('Average happiness')
|
||
f_group = outdir / 'happiness_by_group.png'
|
||
plt.tight_layout()
|
||
plt.savefig(f_group)
|
||
if show_plots:
|
||
plt.show()
|
||
plt.close()
|
||
|
||
# Scatter with linear fit
|
||
plt.figure(figsize=(9, 6))
|
||
if 'Group' in df.columns:
|
||
sns.scatterplot(data=df, x='Habits_Count', y='Happiness', hue='Group', alpha=0.35)
|
||
else:
|
||
sns.regplot(x='Habits_Count', y='Happiness', data=df, x_jitter=0.18, scatter_kws={'alpha': 0.4})
|
||
plt.title('Happiness vs Number of Habits Completed (with linear fit)')
|
||
f4 = outdir / 'happiness_vs_habits_regression.png'
|
||
plt.tight_layout()
|
||
plt.savefig(f4)
|
||
if show_plots:
|
||
plt.show()
|
||
plt.close()
|
||
|
||
logging.info('Saved plots to %s', outdir)
|
||
|
||
|
||
def main(args):
|
||
df = load_data(args.data)
|
||
df = prepare_data(df)
|
||
|
||
descriptive_stats(df)
|
||
|
||
# Effect sizes
|
||
group0 = df[df['Habits_Count'] == 0]['Happiness']
|
||
group3 = df[df['Habits_Count'] == 3]['Happiness']
|
||
if len(group0) > 1 and len(group3) > 1:
|
||
d = cohen_d(group3, group0)
|
||
print(f"\nCohen's d (3 habits vs 0 habits) = {d:.3f}")
|
||
|
||
if 'Group' in df.columns:
|
||
control = df[df['Group'] == 'Control']['Happiness']
|
||
intervention = df[df['Group'] == 'Intervention']['Happiness']
|
||
if len(control) > 1 and len(intervention) > 1:
|
||
d_group = cohen_d(intervention, control)
|
||
print(f"Cohen's d (Intervention vs Control happiness) = {d_group:.3f}")
|
||
|
||
# Models
|
||
run_ols(df)
|
||
run_mixedlm(df)
|
||
|
||
# Plots
|
||
make_plots(df, args.outdir, show_plots=args.show)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
parser = argparse.ArgumentParser(description='Improved data analysis for organization_happiness_study_data.csv')
|
||
parser.add_argument('--data', type=str, default='organization_happiness_study_data.csv', help='CSV data path')
|
||
parser.add_argument('--outdir', type=str, default='plots', help='Directory to save plots')
|
||
parser.add_argument('--show', action='store_true', help='Show plots interactively')
|
||
args = parser.parse_args()
|
||
main(args) |