File size: 5,284 Bytes
26c3195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import pandas as pd
import numpy as np
import io
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side


def detect_scale(series: pd.Series) -> str:
    try:
        s = series.dropna()
        if len(s) == 0:
            return "unknown"
        if not pd.api.types.is_numeric_dtype(series):
            return "categorical"
        u = s.unique()
        if len(u) <= 2:
            return "binary"
        if s.min() >= 1 and s.max() <= 7 and len(u) <= 7:
            return "likert"
        return "continuous"
    except Exception:
        return "unknown"


def cronbach_alpha(data: pd.DataFrame) -> float:
    try:
        d = data.select_dtypes(include=[np.number]).dropna()
        k = d.shape[1]
        if k < 2:
            return np.nan
        iv = d.var(ddof=1, axis=0).sum()
        tv = d.sum(axis=1).var(ddof=1)
        if tv == 0:
            return np.nan
        return round(float((k / (k - 1)) * (1 - iv / tv)), 3)
    except Exception:
        return np.nan


def calc_ave_cr(loadings):
    try:
        lam = np.array(loadings, dtype=float)
        lam2 = lam ** 2
        ave = float(np.mean(lam2))
        sl  = float(np.sum(lam))
        cr  = sl**2 / (sl**2 + float(np.sum(1 - lam2)))
        return round(ave, 3), round(cr, 3)
    except Exception:
        return np.nan, np.nan


def sig_stars(p) -> str:
    try:
        p = float(p)
        if p < 0.001: return "***"
        if p < 0.01:  return "**"
        if p < 0.05:  return "*"
        return "n.s."
    except Exception:
        return "-"


def fmt_p(p) -> str:
    try:
        p = float(p)
        return "< .001" if p < 0.001 else f"{p:.3f}"
    except Exception:
        return "-"


# ── Excel μŠ€νƒ€μΌ ──────────────────────────────────────────────────────────────
def _hdr():
    return Font(name="Arial", bold=True, color="FFFFFF", size=11)

def _fill():
    return PatternFill("solid", fgColor="2F5496")

def _bd():
    return Border(left=Side(style="thin"), right=Side(style="thin"),
                  top=Side(style="thin"),  bottom=Side(style="thin"))

def _ctr():
    return Alignment(horizontal="center", vertical="center", wrap_text=True)

def _lft():
    return Alignment(horizontal="left", vertical="center")


def df_to_sheet(wb: Workbook, sheet_name: str, title: str,
                df_data: pd.DataFrame, note: str = "", adopt_col: str = None):
    try:
        ws = wb.create_sheet(sheet_name[:31])
        cols = list(df_data.columns)
        nc   = max(len(cols), 1)

        # 제λͺ©
        ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=nc)
        ws["A1"] = title
        ws["A1"].font = Font(name="Arial", bold=True, size=12)

        # 헀더
        for ci, col in enumerate(cols, 1):
            c = ws.cell(row=2, column=ci, value=str(col))
            c.font = _hdr(); c.fill = _fill()
            c.alignment = _ctr(); c.border = _bd()

        # adopt_col 인덱슀
        adopt_idx = (cols.index(adopt_col) + 1) if adopt_col and adopt_col in cols else None

        # 데이터
        adopt_fill_y = PatternFill("solid", fgColor="E2EFDA")
        adopt_fill_n = PatternFill("solid", fgColor="FFC7CE")
        adopt_font_y = Font(name="Arial", size=10, color="375623")
        adopt_font_n = Font(name="Arial", size=10, color="9C0006")

        for ri, row in enumerate(df_data.itertuples(index=False), 3):
            for ci, val in enumerate(row, 1):
                safe_val = "" if (isinstance(val, float) and np.isnan(val)) else val
                cell = ws.cell(row=ri, column=ci, value=safe_val)
                cell.alignment = _lft() if ci <= 2 else _ctr()
                cell.border = _bd()
                if adopt_idx and ci == adopt_idx:
                    is_y = str(val) == "채택"
                    cell.fill = adopt_fill_y if is_y else adopt_fill_n
                    cell.font = adopt_font_y if is_y else adopt_font_n

        # 주석
        if note:
            nr = len(df_data) + 4
            note_cell = ws.cell(row=nr, column=1, value=note)
            note_cell.font = Font(name="Arial", size=9, color="595959", italic=True)

        # μ—΄ λ„ˆλΉ„ μžλ™μ‘°μ •
        for col_cells in ws.columns:
            try:
                w = max((len(str(c.value)) if c.value else 0) for c in col_cells)
                ws.column_dimensions[col_cells[0].column_letter].width = min(w + 4, 45)
            except Exception:
                pass

        ws.row_dimensions[2].height = 28

    except Exception as e:
        pass  # μ‹œνŠΈ 생성 μ‹€νŒ¨ μ‹œ 쑰용히 λ„˜μ–΄κ°


def build_excel(sheets: dict) -> bytes:
    wb = Workbook()
    wb.remove(wb.active)
    for name, payload in sheets.items():
        try:
            title    = payload[0]
            df       = payload[1]
            note     = payload[2] if len(payload) > 2 else ""
            adopt_col = payload[3] if len(payload) > 3 else None
            if df is not None and len(df) > 0:
                df_to_sheet(wb, name, title, df, note, adopt_col)
        except Exception:
            pass
    buf = io.BytesIO()
    wb.save(buf)
    return buf.getvalue()