hossam 0.3.10__py3-none-any.whl → 0.3.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hossam/__init__.py CHANGED
@@ -1,37 +1,85 @@
1
1
  from .data_loader import load_data, load_info
2
+ from .hs_stats import oneway_anova
2
3
  from matplotlib import pyplot as plt
3
4
  from matplotlib import font_manager as fm
4
5
  from importlib.resources import files, as_file
5
6
  from importlib.metadata import version
7
+ from types import SimpleNamespace
6
8
  import warnings
7
9
 
8
10
  try:
9
- __version__ = version('hossam')
11
+ __version__ = version("hossam")
10
12
  except Exception:
11
- __version__ = 'develop'
12
- __all__ = ['load_data', 'load_info']
13
+ __version__ = "develop"
14
+
15
+
16
+ hs_fig = SimpleNamespace(
17
+ dpi=200,
18
+ width=600,
19
+ height=320,
20
+ font_size=6,
21
+ font_weight="light",
22
+ frame_width=0.4,
23
+ line_width=1,
24
+ grid_alpha=0.3,
25
+ grid_width=0.4,
26
+ fill_alpha=0.3
27
+ )
28
+
29
+ __all__ = ["load_data", "load_info", "hs_classroom", "hs_gis", "hs_plot", "hs_prep", "hs_stats", "hs_timeserise", "hs_util", "hs_fig"]
13
30
 
14
- my_dpi = 200 # 이미지 선명도(100~300)
15
- default_font_size = 6
16
31
 
17
32
  def _init_korean_font():
18
- """
19
- 패키지에 포함된 한글 폰트를 기본 폰트로 설정합니다.
20
- """
21
- font_file = 'NotoSansKR-Regular.ttf'
22
- try:
23
- # 패키지 리소스에서 폰트 파일 경로 확보
24
- with as_file(files('hossam') / font_file) as font_path:
25
- fm.fontManager.addfont(str(font_path))
26
- fprop = fm.FontProperties(fname=str(font_path))
27
- fname = fprop.get_name()
28
- plt.rcParams['font.family'] = fname
29
- plt.rcParams['font.size'] = default_font_size
30
- plt.rcParams['axes.unicode_minus'] = False
31
- return
32
- except Exception as e:
33
- warnings.warn(f"한글 폰트 초기화: 패키지 폰트 사용 실패 ({e}).")
34
-
35
-
36
- # 모듈 임포트 시점에 폰트 초기화 수행
37
- _init_korean_font()
33
+ """
34
+ 패키지에 포함된 한글 폰트를 기본 폰트로 설정합니다.
35
+ """
36
+ font_file = "NotoSansKR-Regular.ttf"
37
+ try:
38
+ # 패키지 리소스에서 폰트 파일 경로 확보
39
+ with as_file(files("hossam") / font_file) as font_path:
40
+ fm.fontManager.addfont(str(font_path))
41
+ fprop = fm.FontProperties(fname=str(font_path))
42
+ fname = fprop.get_name()
43
+
44
+ plt.rcParams.update({
45
+ "font.family": fname,
46
+ "font.size": hs_fig.font_size,
47
+ "font.weight": hs_fig.font_weight,
48
+ "axes.unicode_minus": False,
49
+ "text.antialiased": True,
50
+ "lines.antialiased": True,
51
+ "patch.antialiased": True,
52
+ "figure.dpi": hs_fig.dpi,
53
+ "savefig.dpi": hs_fig.dpi * 2,
54
+ "text.hinting": "auto",
55
+ "text.hinting_factor": 8,
56
+ "pdf.fonttype": 42,
57
+ "ps.fonttype": 42,
58
+ })
59
+ print(
60
+ "\n✅ 시각화를 위한 한글 글꼴(NotoSansKR-Regular)이 자동 적용되었습니다."
61
+ )
62
+ return
63
+ except Exception as e:
64
+ warnings.warn(f"\n한글 폰트 초기화: 패키지 폰트 사용 실패 ({e}).")
65
+
66
+
67
+ def _init():
68
+
69
+ # 안내 메시지 (블릿 리스트)
70
+ messages = [
71
+ "📦 아이티윌 이광호 강사가 제작한 라이브러리를 사용중입니다.",
72
+ "📚 자세한 사용 방법은 https://py.hossam.kr 을 참고하세요.",
73
+ "📧 Email: leekh4232@gmail.com",
74
+ "🎬 Youtube: https://www.youtube.com/@hossam-codingclub",
75
+ "📝 Blog: https://blog.hossam.kr/",
76
+ f"🔖 Version: {__version__}",
77
+ ]
78
+
79
+ for msg in messages:
80
+ print(f"{msg}")
81
+
82
+ _init_korean_font()
83
+
84
+
85
+ _init()
@@ -1,21 +1,17 @@
1
1
  # -*- coding: utf-8 -*-
2
- """
3
- 학생 편성 모듈
4
-
5
- 학생들을 균형잡힌 조로 나누기 위한 기능을 제공합니다.
6
- 관심사 기반 1차 군집과 점수/인원 균형 조정을 통해
7
- 동질성 있고 균형잡힌 조를 구성합니다.
8
- """
9
-
2
+ # ===================================================================
3
+ # 패키지 참조
4
+ # ===================================================================
10
5
  import math
11
6
  from pandas import DataFrame, qcut, concat, to_numeric
12
7
  from kmodes.kmodes import KModes
13
8
  from matplotlib import pyplot as plt
14
9
  import seaborn as sns
15
- from hossam import my_dpi
16
- from hossam.util import hs_load_data, hs_pretty_table
17
-
10
+ from hossam.hs_util import load_data, pretty_table
18
11
 
12
+ # ===================================================================
13
+ # 학생들을 관심사와 성적으로 균형잡힌 조로 편성한다
14
+ # ===================================================================
19
15
  def cluster_students(
20
16
  df,
21
17
  n_groups: int,
@@ -63,7 +59,7 @@ def cluster_students(
63
59
 
64
60
  # 파일 경로인 경우 데이터프레임으로 로드
65
61
  if isinstance(df, str):
66
- df = hs_load_data(df, info=False)
62
+ df = load_data(df, info=False)
67
63
 
68
64
  # 입력 검증
69
65
  if df is None or len(df) == 0:
@@ -201,6 +197,9 @@ def cluster_students(
201
197
  return result
202
198
 
203
199
 
200
+ # ===================================================================
201
+ # 조 내 인원과 성적 균형을 반복 조정하여 최적화한다
202
+ # ===================================================================
204
203
  def _balance_groups(
205
204
  df: DataFrame,
206
205
  n_groups: int,
@@ -333,6 +332,9 @@ def _balance_groups(
333
332
  return df
334
333
 
335
334
 
335
+ # ===================================================================
336
+ # 성적 데이터가 없을 때 각 조의 인원 수만 균형조정한다
337
+ # ===================================================================
336
338
  def _balance_group_sizes_only(
337
339
  df: DataFrame,
338
340
  n_groups: int,
@@ -371,6 +373,9 @@ def _balance_group_sizes_only(
371
373
  return df
372
374
 
373
375
 
376
+ # ===================================================================
377
+ # 조 편성 결과의 인원, 관심사, 점수 분포를 시각화한다
378
+ # ===================================================================
374
379
  def report_summary(df: DataFrame, figsize: tuple = (20, 4.2), dpi: int = None) -> None:
375
380
  """조 편성 결과의 요약 통계를 시각화합니다.
376
381
 
@@ -493,6 +498,9 @@ def report_summary(df: DataFrame, figsize: tuple = (20, 4.2), dpi: int = None) -
493
498
  plt.show()
494
499
 
495
500
 
501
+ # ===================================================================
502
+ # 조별 점수 분포를 커널 밀도 추정(KDE) 그래프로 시각화한다
503
+ # ===================================================================
496
504
  def report_kde(df: DataFrame, metric: str = 'average', figsize: tuple = (20, 8), dpi: int = None) -> None:
497
505
  """조별 점수 분포를 KDE(Kernel Density Estimation)로 시각화합니다.
498
506
 
@@ -604,6 +612,9 @@ def report_kde(df: DataFrame, metric: str = 'average', figsize: tuple = (20, 8),
604
612
  plt.show()
605
613
 
606
614
 
615
+ # ===================================================================
616
+ # 조별로 학생 목록과 평균 점수를 요약하여 데이터프레임으로 반환한다
617
+ # ===================================================================
607
618
  def group_summary(df: DataFrame, name_col: str = '학생번호') -> DataFrame:
608
619
  """조별로 학생 목록과 평균 점수를 요약합니다.
609
620
 
@@ -667,6 +678,9 @@ def group_summary(df: DataFrame, name_col: str = '학생번호') -> DataFrame:
667
678
  return result_df
668
679
 
669
680
 
681
+ # ===================================================================
682
+ # 학생 조 편성부터 시각화까지의 전체 분석 프로세스를 일괄 실행한다
683
+ # ===================================================================
670
684
  def analyze_classroom(
671
685
  df,
672
686
  n_groups: int,
@@ -735,7 +749,7 @@ def analyze_classroom(
735
749
  # 2. 조별 요약
736
750
  summary = group_summary(df_result, name_col=name_col)
737
751
  print("\n✓ 조별 요약:")
738
- hs_pretty_table(summary, tablefmt="pretty")
752
+ pretty_table(summary, tablefmt="pretty")
739
753
  print()
740
754
 
741
755
  # 3. 요약 시각화
@@ -1,20 +1,23 @@
1
1
  # -*- coding: utf-8 -*-
2
- # -------------------------------------------------------------
2
+ # ===================================================================
3
+ # 패키지 참조
4
+ # ===================================================================
5
+ import os
6
+ import time
7
+ import warnings
3
8
  import requests
4
9
  import concurrent.futures as futures
5
- from pandas import DataFrame
6
- import pandas as pd
10
+
11
+ from pandas import DataFrame, to_numeric
7
12
  from tqdm.auto import tqdm
8
- import time
9
- from geopandas import GeoDataFrame, read_file
10
- import geopandas as gpd
13
+ from geopandas import GeoDataFrame, read_file, points_from_xy
11
14
  from pyproj import CRS
12
- import os
13
- import warnings
14
15
 
15
- from .util import hs_pretty_table
16
+ from .hs_util import pretty_table
16
17
 
17
- # -------------------------------------------------------------
18
+ # ===================================================================
19
+ # 단일 주소를 VWorld API로 지오코딩
20
+ # ===================================================================
18
21
  def __geocode_item(session: requests.Session, index: int, addr: str, key: str) -> tuple[float, float]:
19
22
  """단일 주소를 VWorld API로 지오코딩합니다.
20
23
 
@@ -79,8 +82,11 @@ def __geocode_item(session: requests.Session, index: int, addr: str, key: str) -
79
82
  #print("%s --> (%s, %s)" % (addr, latitude, longitude))
80
83
  return result
81
84
 
82
- # -------------------------------------------------------------
83
- def hs_geocode(df: DataFrame, addr: str, key: str) -> DataFrame:
85
+
86
+ # ===================================================================
87
+ # 주소 컬럼을 일괄 지오코딩하여 위도/경도 컬럼을 추가
88
+ # ===================================================================
89
+ def geocode(df: DataFrame, addr: str, key: str) -> DataFrame:
84
90
  """주소 컬럼을 일괄 지오코딩하여 위도/경도 컬럼을 추가합니다.
85
91
 
86
92
  Args:
@@ -149,8 +155,10 @@ def hs_geocode(df: DataFrame, addr: str, key: str) -> DataFrame:
149
155
 
150
156
  return data
151
157
 
152
- # -------------------------------------------------------------
153
- def hs_load_shape(path: str, info: bool = True) -> GeoDataFrame:
158
+ # ===================================================================
159
+ # Shapefile을 읽어 `GeoDataFrame`으로 로드
160
+ # ===================================================================
161
+ def load_shape(path: str, info: bool = True) -> GeoDataFrame:
154
162
  """Shapefile을 읽어 `GeoDataFrame`으로 로드합니다.
155
163
 
156
164
  Args:
@@ -164,7 +172,7 @@ def hs_load_shape(path: str, info: bool = True) -> GeoDataFrame:
164
172
  FileNotFoundError: 파일이 존재하지 않는 경우.
165
173
 
166
174
  Examples:
167
- >>> from hossam.gis import hs_load_shape
175
+ >>> from hossam.gis import load_shape
168
176
  >>> gdf = hs_load_shape("path/to/file.shp", info=False)
169
177
  """
170
178
  if not os.path.exists(path):
@@ -174,23 +182,25 @@ def hs_load_shape(path: str, info: bool = True) -> GeoDataFrame:
174
182
 
175
183
  if info:
176
184
  print("\n✅ 테이블 정보")
177
- hs_pretty_table(data.info(), tablefmt="pretty")
185
+ pretty_table(data.info(), tablefmt="pretty")
178
186
 
179
187
  print("\n✅ 상위 5개 행")
180
- hs_pretty_table(data.head(), tablefmt="pretty")
188
+ pretty_table(data.head(), tablefmt="pretty")
181
189
 
182
190
  print("\n✅ 하위 5개 행")
183
- hs_pretty_table(data.tail(), tablefmt="pretty")
191
+ pretty_table(data.tail(), tablefmt="pretty")
184
192
 
185
193
  print("\n📊 기술통계")
186
194
  desc = data.describe().T
187
195
  desc["nan"] = data.isnull().sum()
188
- hs_pretty_table(desc, tablefmt="pretty")
196
+ pretty_table(desc, tablefmt="pretty")
189
197
 
190
198
  return data
191
199
 
192
- # -------------------------------------------------------------
193
- def hs_save_shape(
200
+ # ===================================================================
201
+ # 전처리된 데이터(GeoDataFrame 또는 DataFrame)를 Shapefile 또는 GeoPackage로 저장
202
+ # ===================================================================
203
+ def save_shape(
194
204
  gdf: GeoDataFrame | DataFrame,
195
205
  path: str,
196
206
  crs: str | None = None,
@@ -249,8 +259,8 @@ def hs_save_shape(
249
259
 
250
260
  df = gdf.copy()
251
261
  # 숫자 변환 및 결측 제거
252
- df[lat_col] = pd.to_numeric(df[lat_col], errors="coerce")
253
- df[lon_col] = pd.to_numeric(df[lon_col], errors="coerce")
262
+ df[lat_col] = to_numeric(df[lat_col], errors="coerce")
263
+ df[lon_col] = to_numeric(df[lon_col], errors="coerce")
254
264
  df = df.dropna(subset=[lat_col, lon_col])
255
265
 
256
266
  if df.empty:
@@ -258,8 +268,8 @@ def hs_save_shape(
258
268
  "⚠️[ValueError] 유효한 위경도 값이 없어 Shapefile을 생성할 수 없습니다."
259
269
  )
260
270
 
261
- geometry = gpd.points_from_xy(x=df[lon_col], y=df[lat_col])
262
- gdf = gpd.GeoDataFrame(df, geometry=geometry, crs=target_crs)
271
+ geometry = points_from_xy(x=df[lon_col], y=df[lat_col])
272
+ gdf = GeoDataFrame(df, geometry=geometry, crs=target_crs)
263
273
  else:
264
274
  # GeoDataFrame의 CRS 처리: 존재하면 유지, 없으면만 설정
265
275
  if gdf.crs is None:
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+ # ===================================================================
3
+ # 상호작용(Interaction) 항 생성 함수
4
+ # ===================================================================
5
+ import numpy as np
6
+ from pandas import DataFrame
7
+ from itertools import combinations
8
+
9
+
10
+ # ===================================================================
11
+ # 변수 간의 상호작용 항을 추가한 데이터프레임을 반환한다
12
+ # ===================================================================
13
+ def interaction(*fields: str):
14
+ """변수 간의 상호작용(interaction) 항을 생성하는 데코레이터 함수.
15
+
16
+ 사용 방법: 원본 데이터프레임에 대해 호출하면 상호작용 컬럼이 추가된 새 데이터프레임을 반환한다.
17
+
18
+ Args:
19
+ *fields (str): 상호작용할 변수들의 컬럼명. 2개 이상의 컬럼을 지정하면 모든 조합의 상호작용을 생성.
20
+ 지정하지 않으면 모든 수치형 컬럼의 모든 2-way 상호작용을 생성.
21
+
22
+ Returns:
23
+ function: 데이터프레임을 입력받아 상호작용 항이 추가된 데이터프레임을 반환하는 함수.
24
+
25
+ Examples:
26
+ >>> from hossam.hs_prep import interaction
27
+ >>> import pandas as pd
28
+ >>> df = pd.DataFrame({'x1': [1, 2, 3], 'x2': [4, 5, 6], 'x3': [7, 8, 9]})
29
+
30
+ # 특정 변수들의 상호작용만 생성
31
+ >>> result = interaction('x1', 'x2')(df)
32
+ >>> print(result.columns) # x1, x2, x3, x1*x2
33
+
34
+ # 모든 2-way 상호작용 생성
35
+ >>> result = interaction()(df)
36
+ >>> print(result.columns) # x1, x2, x3, x1*x2, x1*x3, x2*x3
37
+ """
38
+ def wrapper(data: DataFrame) -> DataFrame:
39
+ df = data.copy()
40
+
41
+ # fields가 비어있으면 모든 수치형 컬럼의 2-way 상호작용 생성
42
+ if not fields:
43
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
44
+ cols_to_interact = list(combinations(numeric_cols, 2))
45
+ else:
46
+ # fields가 지정된 경우 모든 가능한 조합 생성
47
+ field_list = [f for f in fields if f in df.columns]
48
+ if len(field_list) < 2:
49
+ return df
50
+ cols_to_interact = list(combinations(field_list, 2))
51
+
52
+ # 상호작용 항 생성
53
+ for col1, col2 in cols_to_interact:
54
+ # 두 컬럼이 모두 수치형인지 확인
55
+ if not (pd.api.types.is_numeric_dtype(df[col1]) and pd.api.types.is_numeric_dtype(df[col2])):
56
+ continue
57
+
58
+ interaction_col_name = f"{col1}*{col2}"
59
+ df[interaction_col_name] = df[col1] * df[col2]
60
+
61
+ return df
62
+
63
+ return wrapper
64
+
65
+
66
+ # ===================================================================
67
+ # 직접 상호작용 항을 추가하는 함수 (데코레이터 없이 직접 사용)
68
+ # ===================================================================
69
+ def add_interaction(data: DataFrame, *fields: str) -> DataFrame:
70
+ """데이터프레임에 상호작용 항을 추가한다.
71
+
72
+ 특정 변수 쌍 또는 모든 수치형 변수 간의 상호작용 항을 생성하여 데이터프레임에 추가한다.
73
+
74
+ Args:
75
+ data (DataFrame): 원본 데이터프레임.
76
+ *fields (str): 상호작용할 변수들의 컬럼명 목록.
77
+ 지정하지 않으면 모든 수치형 컬럼의 모든 2-way 상호작용을 생성.
78
+ 지정된 경우, 지정된 컬럼들의 모든 조합에 대해 상호작용을 생성.
79
+
80
+ Returns:
81
+ DataFrame: 상호작용 항이 추가된 새 데이터프레임.
82
+
83
+ Examples:
84
+ >>> from hossam.hs_prep import add_interaction
85
+ >>> import pandas as pd
86
+ >>> df = pd.DataFrame({'x1': [1, 2, 3], 'x2': [4, 5, 6], 'x3': [7, 8, 9]})
87
+
88
+ # 특정 변수들의 상호작용만 추가
89
+ >>> result = add_interaction(df, 'x1', 'x2')
90
+ >>> print(result.columns) # x1, x2, x3, x1*x2
91
+ >>> print(result['x1*x2'].tolist()) # [4, 10, 18]
92
+
93
+ # 모든 2-way 상호작용 추가
94
+ >>> result = add_interaction(df)
95
+ >>> print(result.columns) # x1, x2, x3, x1*x2, x1*x3, x2*x3
96
+ """
97
+ df = data.copy()
98
+
99
+ # fields가 비어있으면 모든 수치형 컬럼의 2-way 상호작용 생성
100
+ if not fields:
101
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
102
+ cols_to_interact = list(combinations(numeric_cols, 2))
103
+ else:
104
+ # fields가 지정된 경우 모든 가능한 조합 생성
105
+ field_list = [f for f in fields if f in df.columns]
106
+ if len(field_list) < 2:
107
+ return df
108
+ cols_to_interact = list(combinations(field_list, 2))
109
+
110
+ # 상호작용 항 생성
111
+ for col1, col2 in cols_to_interact:
112
+ # 두 컬럼이 모두 수치형인지 확인
113
+ if not (pd.api.types.is_numeric_dtype(df[col1]) and pd.api.types.is_numeric_dtype(df[col2])):
114
+ continue
115
+
116
+ interaction_col_name = f"{col1}*{col2}"
117
+ df[interaction_col_name] = df[col1] * df[col2]
118
+
119
+ return df