py4project 0.1.0__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.
py4project/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """py4project: 授業用分析・可視化ユーティリティ
2
+
3
+ pandas DataFrame から回帰分析・散布図・横バープロット・
4
+ ボックスプロットを簡単に作成するための関数群です。
5
+ """
6
+
7
+ from .core import (
8
+ regression,
9
+ regression_plot,
10
+ scatter_plot,
11
+ bar_plot,
12
+ box_plot,
13
+ )
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "regression",
19
+ "regression_plot",
20
+ "scatter_plot",
21
+ "bar_plot",
22
+ "box_plot",
23
+ ]
py4project/core.py ADDED
@@ -0,0 +1,633 @@
1
+ import japanize_matplotlib_jlite
2
+ import numpy as np
3
+ import pandas as pd
4
+ from scipy import stats
5
+ import matplotlib.pyplot as plt
6
+
7
+
8
+ # ============================================================
9
+ # エラーメッセージ用の内部ヘルパー
10
+ # 学生が実行時に出会うエラーを、原因と対処法が分かる日本語にする。
11
+ # 先頭に _ が付いた関数は「内部用」であり、学生が直接使うものではない。
12
+ # ============================================================
13
+
14
+ def _err(title, cause, hint):
15
+ """学生向けエラーメッセージの共通フォーマットを組み立てる(内部用)"""
16
+ return (f'\n【エラー】{title}\n'
17
+ f' 原因:{cause}\n'
18
+ f' 対処:{hint}\n')
19
+
20
+
21
+ def _check_dataframe(data, arg_name='data'):
22
+ """dataがDataFrameかどうかを確認する(内部用)"""
23
+ if not isinstance(data, pd.DataFrame):
24
+ raise TypeError(_err(
25
+ f'引数 {arg_name} がデータフレームではありません。',
26
+ f'{arg_name} に {type(data).__name__} 型のものが渡されました。',
27
+ '引数の順番を確認してください。'
28
+ 'この関数は「列名を先、データフレームを後」に書きます。'
29
+ '例: regression("人口", "所得", df) '
30
+ '(regression(df, "人口", "所得") ではありません)'))
31
+
32
+
33
+ def _check_column(data, col, arg_name):
34
+ """指定された列がdataに存在するかを確認する(内部用)"""
35
+ if col not in data.columns:
36
+ columns = '、'.join(str(c) for c in data.columns)
37
+ raise ValueError(_err(
38
+ f'列「{col}」がデータフレームの中に見つかりません。',
39
+ f'引数 {arg_name} に「{col}」が指定されましたが、この列は存在しません。',
40
+ f'使える列名は次の{len(data.columns)}個です → {columns}\n'
41
+ ' (スペルの間違い、全角と半角の違い、'
42
+ '余分なスペースがないか確認してください)'))
43
+
44
+
45
+ def _check_numeric(data, col, arg_name):
46
+ """指定された列が数値かどうかを確認する(内部用)"""
47
+ if not pd.api.types.is_numeric_dtype(data[col]):
48
+ example = data[col].iloc[0] if len(data) > 0 else '(データが空です)'
49
+ raise TypeError(_err(
50
+ f'列「{col}」は数値ではないため、計算やグラフに使えません。',
51
+ f'引数 {arg_name} に指定された列「{col}」には'
52
+ f'文字列などが入っています(例:{example!r})。',
53
+ '数値が入っている列を指定してください。'
54
+ '数値のはずなのにこのエラーが出る場合は、'
55
+ 'データに「-」「※」「秘匿」などの記号が混じっている可能性があります。'))
56
+
57
+
58
+ def _check_missing(data, col, arg_name):
59
+ """指定された列に欠損値がないかを確認する(内部用)"""
60
+ n_missing = data[col].isna().sum()
61
+ if n_missing > 0:
62
+ raise ValueError(_err(
63
+ f'列「{col}」に欠けている値(欠損値)が{n_missing}個あります。',
64
+ f'引数 {arg_name} に指定された列「{col}」に欠損値が含まれているため、'
65
+ '計算結果がすべて nan(数値でない)になってしまいます。',
66
+ 'df = df.dropna(subset=["' + str(col) + '"]) のように'
67
+ '欠損値のある行を除いてから、もう一度実行してください。'))
68
+
69
+
70
+ def _check_sample_size(data, n_min=3):
71
+ """回帰分析に必要な標本の大きさがあるかを確認する(内部用)"""
72
+ if len(data) < n_min:
73
+ raise ValueError(_err(
74
+ f'データの行数が{len(data)}行しかなく、回帰分析ができません。',
75
+ f'回帰分析には最低でも{n_min}行のデータが必要です。',
76
+ 'データの絞り込み条件が厳しすぎないか確認してください。'))
77
+
78
+
79
+ def _check_positive(data, col, arg_name):
80
+ """対数をとる列に0以下の値がないかを確認する(内部用)"""
81
+ n_bad = (data[col] <= 0).sum()
82
+ if n_bad > 0:
83
+ raise ValueError(_err(
84
+ f'列「{col}」に0以下の値が{n_bad}個あるため、対数をとれません。',
85
+ '常用対数(log10)は、0や負の数に対しては定義されていません。',
86
+ '対数化のオプション(xlog / ylog / xylog)をFalseに戻すか、'
87
+ '0以下の値を含む行を除いてから実行してください。'
88
+ '(このまま計算すると、エラーにならないまま -inf や nan が'
89
+ '混じった誤ったグラフになります)'))
90
+
91
+
92
+ def _label(value):
93
+ """マーカー横に表示する文字列を安全に作る(内部用)
94
+
95
+ 数値の列がtext_colに指定された場合でもエラーにせず文字列にする。"""
96
+ return str(value)[:6]
97
+
98
+
99
+ # ============================================================
100
+ # 学生が使う関数
101
+ # ============================================================
102
+
103
+ def regression(x, y, data):
104
+ """回帰分析の結果を表示する。
105
+
106
+ xを説明変数、yを被説明変数として単回帰分析をおこない、
107
+ 標本の大きさ・決定係数・傾きの95%信頼区間・回帰式を画面に表示します。
108
+
109
+ *注意* 引数は「x(説明変数)→ y(被説明変数)→ data」の順です。
110
+ 回帰式 y = a × x + b の見た目とは順番が逆になるので気をつけてください。
111
+
112
+ 引数
113
+ ----
114
+ x : 文字列
115
+ 説明変数(横軸にあたる変数)の列名。
116
+ y : 文字列
117
+ 被説明変数(縦軸にあたる変数)の列名。
118
+ data : DataFrame
119
+ xとyの列を含むデータフレーム。
120
+
121
+ 表示される内容
122
+ --------------
123
+ 標本の大きさ n : 分析に使ったデータの行数。
124
+ 決定係数 R² : xがyの変動をどれくらい説明できているかを示す0〜1の値。
125
+ 1に近いほどよく説明できている。
126
+ 信頼区間 (95%) : 傾きの値がこの範囲に入ると考えられる区間。
127
+ この区間が1をまたいでいなければ、傾きは1と異なると判断できる。
128
+ 回帰式 : 推定された直線の式。
129
+
130
+ 使用例
131
+ ------
132
+ >>> regression('全国成長効果', '実際の変化', df)
133
+ """
134
+ _check_dataframe(data)
135
+ _check_column(data, x, 'x')
136
+ _check_column(data, y, 'y')
137
+ _check_numeric(data, x, 'x')
138
+ _check_numeric(data, y, 'y')
139
+ _check_missing(data, x, 'x')
140
+ _check_missing(data, y, 'y')
141
+ _check_sample_size(data)
142
+
143
+ d = data.copy()
144
+ n = len(d)
145
+ _x = d[x]
146
+ _y = d[y]
147
+ r, pp = stats.pearsonr(_x, _y)
148
+ res = stats.linregress(_x, _y)
149
+ t = (res.slope - 1) / res.stderr
150
+ p = 2 * (1 - stats.t.cdf(abs(t), n-2))
151
+ t_crit = stats.t.ppf(0.975, n-2)
152
+ ci = (res.slope - t_crit*res.stderr, res.slope + t_crit*res.stderr)
153
+ ci_left = ci[0]
154
+ ci_right = ci[1]
155
+
156
+ print('推定式: yᵢ = b₀ + b₁·xᵢ + uᵢ, i = 1, 2, …, n')
157
+ print(f'標本の大きさ(n): {n}')
158
+ print(f'決定係数(R²) : {r**2:.3f}')
159
+ print(f'b₀ の推定値 : {res.intercept:.3f}')
160
+ print(f'b₁ の推定値 : {res.slope:.3f}')
161
+ print(f'b₁ の95%信頼区間 : [{ci_left:.3f}, {ci_right:.3f}]')
162
+
163
+
164
+ def regression_plot(x, y, data, title='', xlabel=None, ylabel=None,
165
+ line=True, text=True, text_col='産業'):
166
+ """散布図に回帰直線を重ねて表示する。
167
+
168
+ 点の散らばり方と、そこに当てはめた直線を同時に確認できます。
169
+ タイトル部分には回帰式と傾きの95%信頼区間も表示されます。
170
+
171
+ *注意* 引数は「x(説明変数)→ y(被説明変数)→ data」の順です。
172
+
173
+ 引数
174
+ ----
175
+ x : 文字列
176
+ 横軸にする変数(説明変数)の列名。
177
+ y : 文字列
178
+ 縦軸にする変数(被説明変数)の列名。
179
+ data : DataFrame
180
+ xとyの列を含むデータフレーム。
181
+ title : 文字列
182
+ グラフの上に表示するタイトル。省略すると回帰式だけが表示されます。
183
+ xlabel : 文字列
184
+ 横軸のラベル。省略するとxの列名がそのまま使われます。
185
+ ylabel : 文字列
186
+ 縦軸のラベル。省略するとyの列名がそのまま使われます。
187
+ line : True または False
188
+ Trueで回帰直線を引きます。Falseにすると点だけの散布図になります。
189
+ text : True または False
190
+ Trueで各点の横に名前を表示します。点が多くて重なる場合はFalseにしてください。
191
+ text_col : 文字列
192
+ 点の横に表示する名前が入っている列名。初期値は '産業' です。
193
+ 都道府県名を表示したい場合は text_col='都道府県' のように指定します。
194
+ 名前は先頭6文字までが表示されます。
195
+
196
+ 使用例
197
+ ------
198
+ >>> regression_plot('特化係数', '地域固有効果', df, title='大阪府')
199
+ >>> regression_plot('特化係数', '地域固有効果', df, text=False)
200
+ """
201
+ _check_dataframe(data)
202
+ _check_column(data, x, 'x')
203
+ _check_column(data, y, 'y')
204
+ _check_numeric(data, x, 'x')
205
+ _check_numeric(data, y, 'y')
206
+ _check_missing(data, x, 'x')
207
+ _check_missing(data, y, 'y')
208
+ _check_sample_size(data)
209
+ if text:
210
+ _check_column(data, text_col, 'text_col')
211
+
212
+ if xlabel == None:
213
+ xlabel = x
214
+ if ylabel == None:
215
+ ylabel = y
216
+
217
+ data = data.copy()
218
+ n = len(data)
219
+ _x = data[x]
220
+ _y = data[y]
221
+ r, p = stats.pearsonr(_x, _y)
222
+ res = stats.linregress(_x, _y)
223
+ t = (res.slope - 1) / res.stderr
224
+ p = 2 * (1 - stats.t.cdf(abs(t), n-2))
225
+ t_crit = stats.t.ppf(0.975, n-2)
226
+ ci = (res.slope - t_crit*res.stderr, res.slope + t_crit*res.stderr)
227
+ ci_left = ci[0]
228
+ ci_right = ci[1]
229
+
230
+ fig, ax = plt.subplots()
231
+ ax.scatter(_x, _y, s=60, color='steelblue', zorder=3)
232
+ if text:
233
+ for _, row in data.iterrows():
234
+ ax.annotate(_label(row[text_col]), (row[x], row[y]), fontsize=8, alpha=0.85)
235
+ if line:
236
+ xs = np.linspace(_x.min(), _x.max(), 50)
237
+ ax.plot(xs, res.slope*xs + res.intercept, color='crimson', linewidth=2,
238
+ # label=f'回帰直線 (r={r:+.3f}, p値={p:.3f})')
239
+ label=f'回帰直線')
240
+ ax.legend()
241
+ ax.set_title(f'{title}\n'+
242
+ f'推定式: {y} = {res.intercept:.2f} {res.slope:+.2f} × {x}\n'+
243
+ f'スロープ係数の信頼区間 (95%): [{ci_left:.2f}, {ci_right:.2f}]')
244
+ else:
245
+ ax.set_title(f'{title}')
246
+ if _y.min() < 0 < _y.max():
247
+ ax.axhline(0, color='gray', linewidth=0.8)
248
+ if _x.min() < 0 < _x.max():
249
+ ax.axvline(0, color='gray', linewidth=0.8)
250
+ ax.set_xlabel(f'{xlabel}')
251
+ ax.set_ylabel(f'{ylabel}')
252
+ ax.grid(alpha=0.3)
253
+ plt.tight_layout()
254
+ plt.show()
255
+
256
+
257
+ def scatter_plot(x, y, data, title='', xlabel=None, ylabel=None,
258
+ line=True, text=True, text_col='産業', xlog=False, ylog=False, xylog=False):
259
+ """散布図にトレンド線を重ねて表示する。
260
+
261
+ regression_plotとよく似ていますが、こちらは回帰式や信頼区間を表示せず、
262
+ データの傾向を目で確かめることを目的としています。
263
+ 値の大きさが極端に違う変数を扱うときは、対数化のオプションが使えます。
264
+
265
+ 引数
266
+ ----
267
+ x : 文字列
268
+ 横軸にする変数の列名。
269
+ y : 文字列
270
+ 縦軸にする変数の列名。
271
+ data : DataFrame
272
+ xとyの列を含むデータフレーム。
273
+ title : 文字列
274
+ グラフの上に表示するタイトル。
275
+ xlabel : 文字列
276
+ 横軸のラベル。省略するとxの列名がそのまま使われます。
277
+ ylabel : 文字列
278
+ 縦軸のラベル。省略するとyの列名がそのまま使われます。
279
+ line : True または False
280
+ Trueでトレンド線(当てはめた直線)を引きます。
281
+ text : True または False
282
+ Trueで各点の横に名前を表示します。点が多くて重なる場合はFalseにしてください。
283
+ text_col : 文字列
284
+ 点の横に表示する名前が入っている列名。初期値は '産業' です。
285
+ 名前は先頭6文字までが表示されます。
286
+ xlog : True または False
287
+ Trueで横軸の変数を常用対数(log10)に変換します。
288
+ ylog : True または False
289
+ Trueで縦軸の変数を常用対数(log10)に変換します。
290
+ xylog : True または False
291
+ Trueで横軸と縦軸の両方を常用対数に変換します。
292
+ xlog=True, ylog=True と書くのと同じ意味です。
293
+
294
+ 使用例
295
+ ------
296
+ >>> scatter_plot('従業者数', '売上高', df)
297
+ >>> scatter_plot('従業者数', '売上高', df, xylog=True, text=False)
298
+
299
+ 注意点
300
+ ------
301
+ 対数をとる列に0以下の値が含まれているとエラーになります。
302
+ 従業者数や売上高が0の行がある場合は、あらかじめ取り除いてください。
303
+ """
304
+ _check_dataframe(data)
305
+ _check_column(data, x, 'x')
306
+ _check_column(data, y, 'y')
307
+ _check_numeric(data, x, 'x')
308
+ _check_numeric(data, y, 'y')
309
+ _check_missing(data, x, 'x')
310
+ _check_missing(data, y, 'y')
311
+ if text:
312
+ _check_column(data, text_col, 'text_col')
313
+ if xlog or xylog:
314
+ _check_positive(data, x, 'x')
315
+ if ylog or xylog:
316
+ _check_positive(data, y, 'y')
317
+
318
+ if xlabel == None:
319
+ xlabel = x
320
+ if ylabel == None:
321
+ ylabel = y
322
+
323
+ data = data.copy()
324
+
325
+ if xlog or xylog:
326
+ data[x+'_log'] = np.log10(data[x])
327
+ _x = data[x+'_log']
328
+ else:
329
+ _x = data[x]
330
+
331
+ if ylog or xylog:
332
+ data[y+'_log'] = np.log10(data[y])
333
+ _y = data[y+'_log']
334
+ else:
335
+ _y = data[y]
336
+
337
+ fig, ax = plt.subplots()
338
+ ax.scatter(_x, _y, s=60, color='steelblue', zorder=3)
339
+
340
+ if (text and xlog and ylog) or (text and xylog):
341
+ for _, row in data.iterrows():
342
+ ax.annotate(_label(row[text_col]), (row[x+'_log'], row[y+'_log']), fontsize=8, alpha=0.85)
343
+ elif text and xlog and (not ylog):
344
+ for _, row in data.iterrows():
345
+ ax.annotate(_label(row[text_col]), (row[x+'_log'], row[y]), fontsize=8, alpha=0.85)
346
+ elif text and (not xlog) and ylog:
347
+ for _, row in data.iterrows():
348
+ ax.annotate(_label(row[text_col]), (row[x], row[y+'_log']), fontsize=8, alpha=0.85)
349
+ else:
350
+ for _, row in data.iterrows():
351
+ ax.annotate(_label(row[text_col]), (row[x], row[y]), fontsize=8, alpha=0.85)
352
+
353
+ if line:
354
+ res = stats.linregress(_x, _y)
355
+ xs = np.linspace(_x.min(), _x.max(), 50)
356
+ ax.plot(xs, res.slope*xs + res.intercept, color='crimson', linewidth=2, label=f'トレンド線')
357
+ ax.legend()
358
+
359
+ if _y.min() < 0 < _y.max():
360
+ ax.axhline(0, color='gray', linewidth=0.8)
361
+ if _x.min() < 0 < _x.max():
362
+ ax.axvline(0, color='gray', linewidth=0.8)
363
+
364
+ ax.set_title(f'{title}')
365
+ ax.set_xlabel(f'{xlabel}')
366
+ ax.set_ylabel(f'{ylabel}')
367
+ ax.grid(alpha=0.3)
368
+ plt.tight_layout()
369
+ plt.show()
370
+
371
+
372
+ # シフトシェア分析の3つの効果に対応する色。
373
+ # 授業で使うスライドや資料と同じ配色になるように固定してある。
374
+ _COLOR_MAP = {
375
+ '全国成長効果': '#5B7C99',
376
+ '産業構成効果': '#C9873A',
377
+ '地域固有効果': '#1F7A5C',
378
+ '実際の変化': '#3D4551',
379
+ }
380
+
381
+
382
+ def bar_plot(x, data, title='', xlabel=None, unit=1000,
383
+ sort_by='産業コード', tick_label='産業', ascending=None):
384
+ """横向きの棒グラフを表示する。
385
+
386
+ 産業ごとの値を横に並べた棒グラフを描きます。
387
+ 複数の列をリストで渡すと、それぞれを並べて比較できます。
388
+ シフトシェア分析の3つの効果(全国成長効果・産業構成効果・地域固有効果)は
389
+ 授業資料と同じ色で自動的に塗り分けられます。
390
+
391
+ 引数
392
+ ----
393
+ x : 文字列 または 文字列のリスト
394
+ 棒の長さにする変数の列名。
395
+ リストで複数渡すと、産業ごとに並べて表示されます。
396
+ 例: x='地域固有効果'
397
+ 例: x=['全国成長効果', '産業構成効果', '地域固有効果']
398
+ data : DataFrame
399
+ xの列を含むデータフレーム。
400
+ title : 文字列
401
+ グラフの上に表示するタイトル。
402
+ xlabel : 文字列
403
+ 横軸のラベル。省略すると列名と単位から自動でつくられます。
404
+ unit : 数値 または 辞書
405
+ 値を割る数。表示の桁を調整するために使います。
406
+ 1000 なら千単位、0.01 なら百分率(%)の表示になります。
407
+ すべての列に同じ単位を使う場合は数値をひとつ渡します(初期値は1000)。
408
+ 列ごとに変えたい場合は辞書で渡します。
409
+ 例: unit={'地域固有効果': 1000, '特化係数': 1}
410
+ sort_by : 文字列 または None
411
+ 棒を並べる順番を決める列名。初期値は '産業コード' です。
412
+ 値の大きい順に並べたいときは、その列名を指定します。
413
+ Noneを渡すと、xの最初の列の値で並べ替えます。
414
+ tick_label : 文字列
415
+ 縦軸に表示する名前が入っている列名。初期値は '産業' です。
416
+ 都道府県ごとのグラフを描く場合は tick_label='都道府県' と指定します。
417
+ ascending : True または False(省略可)
418
+ Trueで昇順(小さい順)、Falseで降順(大きい順)に並べます。
419
+ 省略した場合は次のように自動で決まります。
420
+ ・sort_by が '産業コード' のまま → 昇順(産業コード順に並ぶ)
421
+ ・sort_by を別の列に変えた場合 → 降順(値の大きい順に並ぶ)
422
+
423
+ 使用例
424
+ ------
425
+ >>> bar_plot('地域固有効果', df, title='大阪府')
426
+ >>> bar_plot(['全国成長効果', '産業構成効果', '地域固有効果'], df)
427
+ >>> bar_plot('地域固有効果', df, sort_by='地域固有効果')
428
+ """
429
+ _check_dataframe(data)
430
+
431
+ if isinstance(x, pd.DataFrame):
432
+ raise TypeError(_err(
433
+ '引数 x にデータフレームが渡されています。',
434
+ 'この関数は「列名を先、データフレームを後」に書きます。',
435
+ '例: bar_plot("地域固有効果", df) '
436
+ '(bar_plot(df, "地域固有効果") ではありません)'))
437
+
438
+ x_list = [x] if isinstance(x, str) else list(x)
439
+
440
+ for col in x_list:
441
+ _check_column(data, col, 'x')
442
+ _check_numeric(data, col, 'x')
443
+ _check_column(data, tick_label, 'tick_label')
444
+
445
+ # unit を列ごとの辞書に正規化
446
+ if isinstance(unit, dict):
447
+ unit_map = {col: unit.get(col, 1000) for col in x_list}
448
+ else:
449
+ unit_map = {col: unit for col in x_list}
450
+
451
+ for col, u in unit_map.items():
452
+ if u == 0:
453
+ raise ValueError(_err(
454
+ 'unit に 0 が指定されています。',
455
+ f'列「{col}」の unit が 0 になっており、0で割ることはできません。',
456
+ 'unit には 1(そのまま)、1000(千単位)、'
457
+ '0.01(%表示)などの0以外の数値を指定してください。'))
458
+
459
+ if xlabel is None:
460
+ distinct_units = set(unit_map.values())
461
+ if len(distinct_units) == 1:
462
+ xlabel = '・'.join(x_list) + f' (単位:{unit_map[x_list[0]]})'
463
+ else:
464
+ xlabel = '・'.join(f'{col}(単位:{unit_map[col]})' for col in x_list)
465
+
466
+ # ソート列の決定:sort_by=Noneが明示された場合のみxの最初の要素にフォールバック
467
+ effective_sort_col = sort_by if sort_by is not None else x_list[0]
468
+ if effective_sort_col not in data.columns:
469
+ columns = '、'.join(str(c) for c in data.columns)
470
+ extra = ''
471
+ if sort_by == '産業コード':
472
+ extra = ('\n (sort_by は指定しなければ "産業コード" が使われます。'
473
+ 'この列がデータにない場合は、sort_by で別の列名を指定してください)')
474
+ raise ValueError(_err(
475
+ f'並べ替えに使う列「{effective_sort_col}」が見つかりません。',
476
+ f'sort_by に「{effective_sort_col}」が指定されましたが、'
477
+ f'この列は存在しません。{extra}',
478
+ f'使える列名は次の{len(data.columns)}個です → {columns}'))
479
+
480
+ # ascending の決定
481
+ if ascending is None:
482
+ ascending = (sort_by == '産業コード')
483
+
484
+ data = data.sort_values(effective_sort_col, ascending=ascending)
485
+
486
+ fig, ax = plt.subplots(figsize=(9, 6))
487
+
488
+ n = len(x_list)
489
+ y = np.arange(len(data))
490
+ bar_height = 0.8 / n
491
+
492
+ for i, col in enumerate(x_list):
493
+ offset = (i - (n - 1) / 2) * bar_height
494
+ color = _COLOR_MAP.get(col, plt.cm.tab10(i / max(n - 1, 1)))
495
+ ax.barh(y + offset, data[col] / unit_map[col], height=bar_height,
496
+ color=color, label=col)
497
+
498
+ if ( data[x_list] < 0 ).any().sum() > 0:
499
+ ax.axvline(0, color='gray', linewidth=0.8)
500
+
501
+ ax.set_yticks(y)
502
+ ax.set_yticklabels(data[tick_label])
503
+ ax.set_xlabel(xlabel)
504
+ ax.set_title(title)
505
+ ax.invert_yaxis()
506
+ ax.grid(axis='x', alpha=0.3)
507
+ if n > 1:
508
+ ax.legend()
509
+ plt.tight_layout()
510
+ plt.show()
511
+
512
+
513
+ def box_plot(x, data, title='', text_col='産業', xlabel=None, xlog=False):
514
+ """箱ひげ図を表示する。
515
+
516
+ データの散らばり方(中央値・四分位数・外れ値)を確認するための図です。
517
+ 箱の中の線が中央値、三角の印が平均値を表します。
518
+ 外れ値には自動的に名前が表示されるので、
519
+ どの産業や都道府県が極端な値をとっているかが分かります。
520
+
521
+ 引数
522
+ ----
523
+ x : 文字列 または 文字列のリスト
524
+ 箱ひげ図にする変数の列名。
525
+ リストで複数渡すと、横に並べて比較できます。
526
+ 例: x='地域固有効果'
527
+ 例: x=['全国成長効果', '産業構成効果', '地域固有効果']
528
+ data : DataFrame
529
+ xの列を含むデータフレーム。
530
+ title : 文字列
531
+ グラフの上に表示するタイトル。
532
+ text_col : 文字列
533
+ 外れ値の横に表示する名前が入っている列名。初期値は '産業' です。
534
+ 名前は先頭6文字までが表示されます。
535
+ xlabel : 文字列のリスト
536
+ 横軸に表示するラベルのリスト。
537
+ 省略するとxの列名がそのまま使われます。
538
+ xに渡した列の数と同じ数のラベルを指定してください。
539
+ xlog : True または False
540
+ Trueでデータを常用対数(log10)に変換してから箱ひげ図を描きます。
541
+ 値の大きさが極端に違う場合に使います。
542
+
543
+ 使用例
544
+ ------
545
+ >>> box_plot('地域固有効果', df)
546
+ >>> box_plot(['全国成長効果', '産業構成効果', '地域固有効果'], df)
547
+ >>> box_plot('売上高', df, xlog=True)
548
+
549
+ 注意点
550
+ ------
551
+ xlog=True にする場合、対象の列に0以下の値が含まれているとエラーになります。
552
+ """
553
+ _check_dataframe(data)
554
+
555
+ if isinstance(x, pd.DataFrame):
556
+ raise TypeError(_err(
557
+ '引数 x にデータフレームが渡されています。',
558
+ 'この関数は「列名を先、データフレームを後」に書きます。',
559
+ '例: box_plot("地域固有効果", df) '
560
+ '(box_plot(df, "地域固有効果") ではありません)'))
561
+
562
+ if (xlabel == None) and ( not isinstance(x, list) ):
563
+ x = [x]
564
+ xlabel = x
565
+ elif (xlabel == None) and isinstance(x, list):
566
+ xlabel = x
567
+ elif not isinstance(x, list):
568
+ x = [x]
569
+
570
+ if not isinstance(xlabel, list):
571
+ xlabel = [xlabel]
572
+
573
+ for col in x:
574
+ _check_column(data, col, 'x')
575
+ _check_numeric(data, col, 'x')
576
+ _check_column(data, text_col, 'text_col')
577
+
578
+ if len(xlabel) != len(x):
579
+ raise ValueError(_err(
580
+ 'xlabel の数と x の数が合っていません。',
581
+ f'x には{len(x)}個の列名が指定されましたが、'
582
+ f'xlabel には{len(xlabel)}個のラベルが指定されています。',
583
+ 'xlabel は省略できます。指定する場合は、'
584
+ 'x と同じ数のラベルをリストで渡してください。'))
585
+
586
+ if xlog:
587
+ for col in x:
588
+ _check_positive(data, col, 'x')
589
+
590
+ data = data.copy()
591
+
592
+ fig, ax = plt.subplots()
593
+
594
+ if xlog:
595
+ x_log = [s+'_log' for s in x]
596
+ tmp = np.log10(data[x])
597
+ tmp.columns = x_log
598
+ data = pd.concat([data, tmp], axis='columns')
599
+ ax.boxplot(data[x_log], tick_labels=xlabel, showmeans=True, showfliers=False)
600
+
601
+ for i, col in enumerate(x_log, start=1):
602
+ _data = data[col]
603
+ q1, q3 = _data.quantile(0.25), _data.quantile(0.75)
604
+ iqr = q3 - q1
605
+ lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
606
+ cond = ( data[col] < lower ) | ( data[col] > upper )
607
+ outliers = data.loc[cond,:]
608
+
609
+ ax.scatter([i] * len(outliers), outliers[col], color='C0', zorder=3)
610
+ for _, row in outliers.iterrows():
611
+ ax.annotate(_label(row[text_col]), (i, row[col]),
612
+ textcoords="offset points", xytext=(6, 0),
613
+ fontsize=8, ha='left')
614
+
615
+ else:
616
+ ax.boxplot(data[x], tick_labels=xlabel, showmeans=True)
617
+
618
+ for i, col in enumerate(x, start=1):
619
+ _data = data[col]
620
+ q1, q3 = _data.quantile(0.25), _data.quantile(0.75)
621
+ iqr = q3 - q1
622
+ lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
623
+ cond = ( data[col] < lower ) | ( data[col] > upper )
624
+ outliers = data.loc[cond,:]
625
+
626
+ ax.scatter([i] * len(outliers), outliers[col], color='C0', zorder=3)
627
+ for _, row in outliers.iterrows():
628
+ ax.annotate(_label(row[text_col]), (i, row[col]),
629
+ textcoords="offset points", xytext=(6, 0),
630
+ fontsize=8, ha='left')
631
+
632
+ ax.set_title(f'{title}')
633
+ plt.show()
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.5
2
+ Name: py4project
3
+ Version: 0.1.0
4
+ Summary: 授業用回帰分析・散布図・バープロット・ボックスプロットのユーティリティ
5
+ Project-URL: Homepage, https://github.com/Py4Project/py4project
6
+ Project-URL: Repository, https://github.com/Py4Project/py4project
7
+ Project-URL: Issues, https://github.com/Py4Project/py4project/issues
8
+ Author-email: Tetsugen HARUYAMA <tetsu.yes@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: matplotlib,regional-economics,shift-share,statistics
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Natural Language :: Japanese
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: japanize-matplotlib-jlite
19
+ Requires-Dist: matplotlib>=3.9
20
+ Requires-Dist: numpy
21
+ Requires-Dist: pandas
22
+ Requires-Dist: scipy
23
+ Description-Content-Type: text/markdown
24
+
25
+ # py4project
26
+
27
+ 授業用分析・可視化ユーティリティです。pandas DataFrame から回帰分析・散布図・横バープロット・ボックスプロットを簡単に作成できます。
28
+
29
+ ## インストール
30
+
31
+ ```bash
32
+ pip install py4project
33
+ ```
34
+
35
+ ## 提供している関数
36
+
37
+ - `regression(x, y, data)` — 単回帰分析(R²、95%信頼区間、回帰式を表示)
38
+ - `regression_plot(x, y, data, ...)` — 散布図+回帰直線
39
+ - `scatter_plot(x, y, data, ...)` — 散布図+トレンド線(対数軸オプションあり)
40
+ - `bar_plot(x, data, ...)` — 横バープロット(複数系列・シフトシェア分解の配色に対応)
41
+ - `box_plot(x, data, ...)` — ボックスプロット(外れ値の自動ラベル表示、対数化オプションあり)
42
+
43
+ ## 使用例
44
+
45
+ ```python
46
+ import pandas as pd
47
+ from py4project import regression, bar_plot
48
+
49
+ df = pd.DataFrame({
50
+ "産業": ["製造業", "卸売業", "小売業"],
51
+ "全国成長効果": [120, 80, 60],
52
+ "産業構成効果": [-20, 10, 5],
53
+ "地域固有効果": [30, -5, 15],
54
+ })
55
+
56
+ bar_plot(["全国成長効果", "産業構成効果", "地域固有効果"], df, title="シフトシェア分解")
57
+ ```
58
+
59
+ ## 依存パッケージ
60
+
61
+ `numpy`, `pandas`, `scipy`, `matplotlib`, `japanize_matplotlib_jlite`(日本語フォントを自動設定します)
62
+
63
+ ## ライセンス
64
+
65
+ MIT License
@@ -0,0 +1,6 @@
1
+ py4project/__init__.py,sha256=opwura-D--aKS0VtYelF4bgDY8-zTploHhUgtjje5CQ,457
2
+ py4project/core.py,sha256=9j94NbOX5NSzSa5Wwa3j5DGKxeLCc7ObjRtkHNtd688,27027
3
+ py4project-0.1.0.dist-info/METADATA,sha256=nhZdX7Jpa6ixtVZxrR-_C7kuH27FfxLIAUZP2YjoXQc,2340
4
+ py4project-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
5
+ py4project-0.1.0.dist-info/licenses/LICENSE,sha256=enprWbUzQeXcJ8NKjqSvlsmPyUmGfhA8OYw_o2ORXCg,1074
6
+ py4project-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tetsugen HARUYAMA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.