産業連関表を活用した為替変動による波及効果の分析, 金融経済データとの相関分析, また3Dグラフによるサプライチェーン構造の可視化を行う.
| フォルダ名 | 用途 | ファイル名 | ファイルの場所 |
| kawasehendou | flaskを用いたシステムの記述 | 7to12_Yfinance.ipynb | kawasehendou |
| kabuka | 用途 | 7to12_JPX401.ipynb | kabuka |
| sankakuka_2 | javascriptや画像のファイルが入っている | sankakuka_2020.py | sankakuka_2 |
| 3Dgraph | 3Dグラフを作成するときのjavascriptのファイル | 3Dgraph_kihon.ipynb | 3Dgraph |
| 3Dgraph | javascriptで読み込む用のjsonファイル | app_0114.py | 3Dgraph |
| soukan | グラフのボタンを作成する用の画像 | soukan_7to12_map_yfinance.ipynb | soukan |
バージョンについては上で示した通りである. seleniumではクリックなどのユーザーアクションを模倣することで深いところにあるデータも収集することができる. スクレイピングをする部分のコードはこれ↓
#ref(): File not found: "scraping.py" at page "戸田さん卒論_backup"
#ref(): File not found: "スクレイピング流れ.PNG.jpg" at page "戸田さん卒論_backup"
今回は日本銀行時系列統計データ検索サイトを例に説明する. 日本銀行時系列統計データ検索サイトではクリックアクションを繰り返すことで金利や物価指数など金融時系列データをcsvファイル形式で取得することができる.
ダウンロードしたデータはcsv形式でダウンロードされる.
そのため,ダウンロードされたcsvファイルを一つのフォルダにまとめて保存しする必要がある.
また,ダウンロードした時点のファイル名もダウンロードされた時間,回数によって勝手に決められてしまうものもあるため,ファイル名を変更する必要がある.
そのためにまず,ファイルのダウンロード先を指定する必要がある
dldir_path = Path('保存先のフォルダのパス')
dldir_path.mkdir(exist_ok=True)
download_dir = str(dldir_path.resolve())
options.add_experimental_option('prefs', {'download.default_directory': download_dir})
また,ファイル名を変更するコードとして以下を追加する.
filename_list=["変更後のファイル名(1)","変更後のファイル名(2)".....]
list_of_files = glob.glob('保存先のフォルダのパス/*')
latest_file = max(list_of_files, key=os.path.getctime)
file_name = str(latest_file.split('\\')[1])
print(latest_file)
print()
os.remove("csv/"+filename_list[i-1])
os.rename(latest_file,"csv/"+filename_list[i-1])
産業連関表は以下の総務省のリンク先からダウンロードする
https://www.soumu.go.jp/toukei_toukatsu/data/io/2020/io20_00001.html
#ここからスタート #日本経済新聞のページからティッカーのみを取得する. from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager from bs4 import BeautifulSoup import time
# Chromeドライバーのセットアップ
service = Service("C:/Users/ta_na/OneDrive/デスクトップ/chromedriver-win64 (1)/chromedriver-win64/chromedriver.exe")
driver = webdriver.Chrome(service=service)
# URLを指定 url = "https://www.nikkei.com/markets/kabu/nidxprice/?StockIndex=N500" driver.get(url)
# ページが完全に読み込まれるのを待つ time.sleep(3)
# 繰り返し回数を指定 repeat_count = 10 # 例として3回繰り返す
# ティッカー情報を格納するリスト tickers = []
# 繰り返し処理
for i in range(repeat_count):
print(f"Page {i+1} のティッカーを取得中...")
# BeautifulSoupを使ってページのHTMLを解析
soup = BeautifulSoup(driver.page_source, 'html.parser')
# ティッカー情報を格納
for row in soup.find_all('tr'): # テーブル行を取得
columns = row.find_all('td') # 行内の列を取得
if len(columns) > 1: # 必要な列が存在する場合のみ処理
ticker = columns[0].text.strip() # ティッカー情報を取得
tickers.append(ticker)
# 次のページに移動するためのクリックアクション
try:
# XPathを使って次ページボタンをクリック
next_button = driver.find_element(By.XPATH, "//*[@id='CONTENTS_MARROW']/div[2]/div/div[2]/div/ul/li[7]/a") # ボタンのXPathを修正
next_button.click() # クリックアクション
print(f"ページ {i+1} の次のページへ移動")
except Exception as e:
print(f"クリックアクションでエラーが発生しました: {e}")
break # 次のページに移動できない場合、ループを終了
# 次のページの読み込みを待機
time.sleep(3)
# 結果を出力
print("取得したティッカー一覧:")
print(tickers)
# 必要に応じてファイルに保存
with open("nikkei500_tickers_all.txt", "w") as file:
for ticker in tickers:
file.write(f"{ticker}\n")
# ブラウザを閉じる driver.quit()
Pythonのライブラリであるyfinanceを用いてデータの収集を行う
import yfinance as yf import pandas as pd import os
output_dir = 'C:/Users/ta_na/OneDrive/デスクトップ/kabuka/kabuka/csv/out_data'
# 業種ごとのデータを格納する辞書
industry_stock_data = {}
industry_averages = {}
# 1業種ずつ処理
for col, tickers in df_t1.items(): # df_tickers1 は辞書なので items() でキーと値を取得
tickers = [ticker for ticker in tickers if ticker is not None] # Noneを除いたティッカーのリスト
ticker_close_prices = [] # その業種のティッカーに対応するClose価格を一時的に格納するリスト
for ticker in tickers:
if isinstance(ticker, str): # tickerが文字列である場合
ticker_symbol = ticker + ".T" # ティッカー番号に .T を追加して日本株式形式にする
print(f"取得するティッカー: {ticker_symbol}")
try:
# yfinanceを使って指定期間の日足データを取得
data = yf.download(ticker_symbol, interval="1d", start="2024-07-01", end="2024-12-01", progress=False)
# データが取得できた場合、Closeの値だけを格納
if not data.empty:
ticker_close_prices.append(data['Close'])
print(f"{ticker_symbol} の日足株価データのCloseのみ取得しました。")
else:
print(f"{ticker_symbol} のデータが見つかりませんでした。")
except Exception as e:
print(f"{ticker_symbol} の取得中にエラーが発生しました: {e}")
else:
print(f"無効なティッカー: {ticker}(スキップされました)")
# データを統合して業種ごとのデータフレームを作成
if ticker_close_prices:
industry_df = pd.concat(ticker_close_prices, axis=1)
# 列数とティッカー数が一致しない場合、自動調整
if len(industry_df.columns) != len(tickers):
print(f"警告: 列数の不一致。業種: {col} のティッカー数 {len(tickers)} と列数 {len(industry_df.columns)} が一致しません。")
# 一致しない場合、列数に合わせてティッカーリストを調整
tickers = tickers[:len(industry_df.columns)]
# 列名にティッカー番号を設定
industry_df.columns = tickers
industry_df['Date'] = industry_df.index # 日付を追加
industry_df.set_index('Date', inplace=True) # 'Date' をインデックスとして設定
# NaNを削除(各ティッカーに対してNaNがある場合)
industry_df = industry_df.dropna(axis=1, how='any') # NaNを含む列を削除
# 業種ごとの一日ごとの平均株価を計算
industry_df['Average'] = industry_df.mean(axis=1)
# 業種ごとの平均株価データを industry_averages に格納
industry_averages[col] = industry_df['Average']
# 必要に応じてCSVファイルとして保存
file_path = os.path.join(output_dir, f"{col}_stock_data_with_average.csv")
industry_df.to_csv(file_path)
print(f"{col} のデータを '{file_path}' に保存しました。")
else:
print(f"{col} 業種のデータが存在しませんでした。")
# 最終的な業種別平均株価データフレームを作成 df_heikin = pd.DataFrame(industry_averages)
# 結果の確認
print("業種別平均株価データフレーム:")
print(df_heikin)
# 必要に応じてCSVとして保存 df_heikin.to_csv(os.path.join(output_dir, "industry_average_stock_prices.csv"), encoding="utf-8-sig")
import yfinance as yf import pandas as pd
為替のデータを取得する
# USD/JPYの為替データを取得
usd_jpy = yf.download("JPY=X", start="2024-07-01", end="2024-12-01", progress=False)
# 終値を取得 usd_jpy_close = usd_jpy.iloc[:, 3]
usd_jpy_close = usd_jpy.reset_index()
分析するためにはデータの前処理を行わなければ正しい結果は得られない. 今回は前処理の手法として正規化を行う. 正規化の手法として,VAR-LiNGAMではMin-Max法によるデータの正規化,RF5に用いるデータの正規化手法としては説明変数にMax法による正規化,目的変数にはZ scoreによる正規化を行う.
VAR-LiNGAMの分析ではPythonのモジュール「LiNGAM」のVAR-LiNGAMを用いる. これはhttps://lingam.readthedocs.io/en/latest/tutorial/var.html を参考にすればよい
path = "用いるデータのパス" df=pd.read_csv(path,parse_dates=True,index_col="Day",encoding="cp932") scaler = MinMaxScaler() normalized_df = pd.DataFrame(scaler.fit_transform(df), columns=df.columns) model = lingam.VARLiNGAM(lags=2, prune=True) model.fit(normalized_df)
上のコードではデータの読み取り,正規化を行い,それについてVAR-LiNGAMを実行している.
labels = [f'{col}(t)' for i, col in enumerate(normalized_df.columns)]+[f'{col}(t-1)' for i,col in enumerate(df.columns)]
make_dot(np.hstack(model.adjacency_matrices_),
lower_limit=0.05,
ignore_shape=True,
labels=labels
)
上のコードで2次元グラフを作成している.このコードは結果を出すうえで特に必要ないため,実行しなくても良いが必要になった場合にはgraphvizのインストールが必要になる. graphvizのインストールについては以下のサイトを参考にした
https://www.kkaneko.jp/tools/win/graphviz.html
model = lingam.VARLiNGAM() result = model.bootstrap(normalized_df,n_sampling=100)
これでLiNGAMのVAR-LiNGAMをモデルとして設定し,ブートストラップ法による分析を行っている.n_samplingの数でサンプリング回数を設定し,今回は100回になっているが,用いるデータのサンプル数を考慮し設定する必要がある. ブートストラップ法では一般的に用いるデータのサンプル数よりも大きいものを設定することが望ましい.
p_values = model.get_error_independence_p_values()
これで誤差変数間の独立性を分析する.
cdc = result.get_causal_direction_counts(n_directions=8, min_causal_effect=0.3, split_by_causal_effect_sign=True)
このコードで因果関係の方向性を取得する.n_directionは上位いくつの因果方向を限定するかを設定するものであり,min_causal_effectでは係数が何以上の因果方向に限定するかを指定できる.
print_causal_directions(cdc, 100, labels=labels)
でその結果を描画できる.
dagc = result.get_directed_acyclic_graph_counts(n_dags=3, min_causal_effect=0.2, split_by_causal_effect_sign=True)
これではDAGのランキングを取得できる.また,
causal_effects = result.get_total_causal_effects(min_causal_effect=0.01) df = pd.DataFrame(causal_effects)
df['from'] = df['from'].apply(lambda x : labels[x]) df['to'] = df['to'].apply(lambda x : labels[x]) df
これで合計因果関係を取得することができ,この結果を用いて3Dグラフの作成を行うため,pandasのto_csvを用いることでcsvファイルに保存しておく.
#ref(): File not found: "ingakekka.PNG" at page "戸田さん卒論_backup"
得られた因果性は上のような形式で保存される. fromには影響を与えている要素,toには影響を与えられている要素,valueには因果性の大きさが格納されている. 一行ずつ因果の向きと大きさが格納されている
数法則発見法の1つであるRF5を用いて経済変数間のモデル化を行う. RF5のプログラムは以下に貼っておく.
RF5では何度も反復を行い学習することでモデルを作成するため,反復回数を設定する必要がある. 反復回数を設定する際にはwhile s < 500:の500の部分を反復したい回数に設定する. この反復回数は従来研究では10000回や20000回が望ましいと述べられていたのでそれ以上の値に設定した方が良いだろう.
実行して得られたBICが低ければ低いほど良い結果をとりやすいのでBICが最も小さくなったものの結果を見て考察を行えばよい.
RFを実行する際はDriveのRF_real_experiment100.pyを実行すればよい. 実行する際は実際に用いるデータを置き換える必要がある. その際には169行目のpd.read_csv('データのパス(csv形式)')に置き換える. データの形式の例を以下に示す.
#ref(): File not found: "data.png.jpg" at page "戸田さん卒論_backup"
データの内容としては右端の列に目的変数,それ以外の列に説明変数のデータを入れる. 結果はtheta.csvに出力され,それをRF5の定式化に当てはめることで構築されたモデルを復元することができる. 以下にtheta.csvの中身を示す.
#ref(): File not found: "theta.jpg" at page "戸田さん卒論_backup"
RF5の定式化は以下に示す.
#ref(): File not found: "RF_teishikika.png" at page "戸田さん卒論_backup"
θは(w_0,w_j,w_jk)かなっている. このθの中身がtheta.csvには格納され,これを用いて復元を行う.
産業連関表の三角化を行う.
import numpy as np import pandas as pd from pulp import LpProblem, LpVariable, LpBinary, lpSum, LpMaximize, value import networkx as nx import matplotlib.pyplot as plt
# 入力データの読み込み
def read_input_data(file_path):
# CSVファイルを読み込む
df_yunyu = pd.read_csv(file_path)
# セクター数を取得
#NSec = int(df.iloc[0, 1])
NSec = 37
# A行列を取得(2行目以降を数値行列として読み込む)
#A = df.iloc[2:, 2:].replace({',': ''}, regex=True).values.astype(float)
A = df_yunyu.replace({',': ''}, regex=True).values.astype(float)
return NSec, A
# 最適化問題の定義と解決
def solve_triangulation(NSec, A):
problem = LpProblem("Triangulation", LpMaximize)
# セクターの範囲
SSec = range(NSec)
# 決定変数
X = [[LpVariable(f"X_{i}_{j}", cat=LpBinary) for j in SSec] for i in SSec]
# 制約条件
for i in SSec:
for j in SSec:
if i < j:
for k in SSec:
if j < k:
problem += X[i][j] + X[j][k] - X[i][k] >= 0,
f"Transitivity1_{i}_{j}_{k}"
problem += X[i][j] + X[j][k] - X[i][k] <= 1,
f"Transitivity2_{i}_{j}_{k}"
# 目的関数
LowSum = lpSum(
(A[i, j] - A[j, i]) * X[i][j] + A[j, i]
for i in SSec for j in SSec if i < j
)
problem += LowSum
# 問題を解く
problem.solve()
# 結果を取り出す
Xo = np.zeros((NSec, NSec))
Rank = np.zeros(NSec)
for i in SSec:
for j in SSec:
if i < j:
Xo[i, j] = value(X[i][j])
elif i > j:
Xo[i, j] = 1 - value(X[j][i])
Xo[i, i] = 1
Rank[i] = sum(Xo[i, :])
return Xo, Rank
# 結果(rank)をファイルに出力
def write_output_data(file_path, Rank):
df = pd.DataFrame({"Sector": np.arange(1, len(Rank) + 1), "Rank": Rank})
# インデックスをリセットして1からの連番にする
df.reset_index(drop=True, inplace=True) # 現在のインデックスをリセット
df.index = df.index + 1 # インデックスを1から始める
# 産業名リスト
industry_names = [
"農林漁業", "鉱業", "飲食料品", "繊維製品", "パルプ・紙・木製品", "化学製品", "石油・石炭製品",
"プラスチック・ゴム製品", "窯業・土石製品", "鉄鋼", "非鉄金属", "金属製品", "はん用機械",
"生産用機械", "業務用機械", "電子部品", "電気機械", "情報通信機器", "輸送機械", "その他の製造工業製品",
"建設", "電力・ガス・熱供給", "水道", "廃棄物処理", "商業", "金融・保険", "不動産", "運輸・郵便",
"情報通信", "公務", "教育・研究", "医療・福祉", "他に分類されない会員制団体", "対事業所サービス",
"対個人サービス", "事務用品", "分類不明"
]
# インデックスを産業名に置き換える
df.index = industry_names
df.to_csv(file_path, encoding="utf-8-sig")
# Rank 列を昇順でソート
df_rank_sorted = df.sort_values(by="Rank", ascending=True)
df_rank_sorted.to_csv("C:/Users/ta_na/OneDrive/デスクトップ/s ankakuka_2/sankakuka_2/csv/out_data/Japan2020_37_rank_sort_kihon_tonyukeisu.csv", encoding="utf-8-sig") # index=False でインデックスを保存しない
# 結果(行列X)をファイルに出力
def write_output_matrix(file_path, matrix):
# 行列をデータフレームに変換して保存
df = pd.DataFrame(matrix)
df.to_csv(file_path, index=False, header=False)
# メイン処理
if __name__ == "__main__":
# 入力ファイルと出力ファイル
IN_FILE = "C:/Users/ta_na/OneDrive/デスクトップ/kawasehendou/kawasehendou/csv/coefficient_matrix_kihon.csv"
OUT_FILE_RANK = "C:/Users/ta_na/OneDrive/デスクトップ/sankakuka_2/sankakuka_2/csv/out_data/Japan2020_37_rank_kihon_tonyukeisu.csv"
OUT_FILE_X = "C:/Users/ta_na/OneDrive/デスクトップ/sankakuka_2/sankakuka_2/csv/out_data/Japan2020_37_X_kihon_tonyukeisu.csv"
# データの読み込み
NSec, A = read_input_data(IN_FILE)
# 最適化の実行
Xo, Rank = solve_triangulation(NSec, A)
# 結果を保存
write_output_data(OUT_FILE_RANK, Rank)
write_output_matrix(OUT_FILE_X, Xo)
print(f"Results saved to {OUT_FILE_RANK}")
print(f"Results saved to {OUT_FILE_X}")
3D Force-Directed Graphに産業部門間の関係の情報を送るためにjsonファイルを作成する.
産業部門間の関係が格納されたcsvファイルをjsonファイルに変換する
import numpy as np import pandas as pd
X = pd.read_csv("C:/Users/ta_na/OneDrive/デスクトップ/sankakuka_2/sankakuka_2/csv/out_data/Japan2020_37_X_kihon_tonyukeisu.csv", header=None)
X
import numpy as np import pandas as pd
coefficient_matrix_kihon = pd.read_csv("C:/Users/ta_na/OneDrive/デスクトップ/kawasehendou/kawasehendou/csv/coefficient_matrix_kihon.csv")
coefficient_matrix_kihon
# X の 1 の部分に対応する coefficient_matrix_kihon の値を取り出し、 # X の 0 の部分は 0 のままにする coefficient_matrix_kihon_2 = X * coefficient_matrix_kihon
# 出力結果
print(coefficient_matrix_kihon_2)
# 新しいデータフレームを作成
rows = []
for i, row_label in enumerate(coefficient_matrix_kihon_2.index):
for j, col_label in enumerate(coefficient_matrix_kihon_2.columns):
if i != j and coefficient_matrix_kihon_2.iloc[i, j] != 0: # 対角要素をスキップ
rows.append({'from': row_label, 'to': col_label, 'effect': coefficient_matrix_kihon_2.iloc[i, j]})
# 新しいデータフレームを作成 result_df = pd.DataFrame(rows)
print(result_df)
# CSVファイルに保存
result_df.to_csv('csv/out_data/from_to_kihon.csv', index=False, encoding='utf-8-sig')
# 'effect'列の値を100倍 result_df['effect'] = result_df['effect'] * 100
#全部 import pandas as pd import csv import json
# effectを削除 df_effect=result_df.drop(["effect"],axis=1)
#fromとtoの中の要素を一つのリストにする count_id = df_effect.stack().drop_duplicates().tolist()
# links の形式にデータを変換する
links = []
for index, row in result_df.iterrows():
links.append({
"source": row['from'],
"target": row['to'],
"value": row['effect']
})
# データを指定された形式に変換
nodes = [{"id": item, "group": 1} for item in count_id]
# JSON形式に変換
result = {"nodes": nodes, "links": links}
# JSONファイルに保存
with open("static/json/output_toda_kihon_zenbu.json", "w") as f:
json.dump(result, f, indent=2)
作成するjsonファイルの形式(output_toda_kihon_zenbu.json)
{
"nodes": [
{
"id": "\u8fb2\u6797\u6f01\u696d",
"group": 1
},
{
"id": "\u98f2\u98df\u6599\u54c1",
"group": 1
},
~~~~~~~~~~省略~~~~~~~~~~
{
"id": "\u60c5\u5831\u901a\u4fe1",
"group": 1
},
{
"id": "\u5bfe\u4e8b\u696d\u6240\u30b5\u30fc\u30d3\u30b9",
"group": 1
}
],
"links": [
{
"source": "\u8fb2\u6797\u6f01\u696d",
"target": "\u98f2\u98df\u6599\u54c1",
"value": 19.05628066765477
},
{
"source": "\u8fb2\u6797\u6f01\u696d",
"target": "\u6559\u80b2\u30fb\u7814\u7a76",
"value": 0.13824726106617
},
~~~~~~~~~~省略~~~~~~~~~~
{
"source": "\u5206\u985e\u4e0d\u660e",
"target": "\u4ed6\u306b\u5206\u985e\u3055\u308c\u306a\u3044\u4f1a\u54e1\u5236\u56e3\u4f53",
"value": 1.2702003066174
},
{
"source": "\u5206\u985e\u4e0d\u660e",
"target": "\u5bfe\u500b\u4eba\u30b5\u30fc\u30d3\u30b9",
"value": 0.43188001994264
}
]
}
3Dグラフの描画にはThree.jsのモジュール”3D Force-Directed Graph”を使う.
参考にしたサイト👉https://vasturiano.github.io/3d-force-graph/
javascriptの買い方はサイトを参考にすれば様々な変更が可能.
⚠モジュールのインポート方法はサイトのものでは行えなかったため独自で行った.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>3D Graph with Rankings</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
}
#container {
display: flex;
flex: 1;
width: 100%;
}
#three {
flex: 1;
background-color: aliceblue;
height: 100%;
}
#info {
position: fixed; /* fixedに変更して、スクロールしても位置が固定されるようにする */
top: 10px;
right: 10px;
background-color: #fff;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
width: 300px;
max-height: 900px;
overflow-y: auto;
z-index: 10;
}
#rankTable {
position: fixed; /* 左上に固定表示 */
top: 10px;
left: 10px;
background-color: #fff;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
width: 300px;
max-height: 900px;
overflow-y: auto;
z-index: 10;
font-size: 12px;
}
/* レスポンシブ対応 */
@media screen and (max-width: 768px) {
#container {
flex-direction: column;
}
#rankTable {
width: 100%;
}
#three {
width: 100%;
height: 400px;
}
}
</style>
</head>
<body>
<!-- 左上にランク表 --> <div id="rankTable"> <table> <thead> <tr> <th>産業部門名</th> <th>順列</th> </tr> </thead> <tbody> <tr><td>生産用機械</td><td>1</td></tr> <tr><td>医療・福祉</td><td>2</td></tr> <tr><td>教育・研究</td><td>3</td></tr> <tr><td>対個人サービス</td><td>4</td></tr> <tr><td>飲食料品</td><td>5</td></tr> <tr><td>農林漁業</td><td>6</td></tr> <tr><td>水道</td><td>7</td></tr> <tr><td>他に分類されない会員制団体</td><td>8</td></tr> <tr><td>分類不明</td><td>9</td></tr> <tr><td>公務</td><td>10</td></tr> <tr><td>輸送機械</td><td>11</td></tr> <tr><td>情報通信機器</td><td>12</td></tr> <tr><td>事務用品</td><td>13</td></tr> <tr><td>業務用機械</td><td>14</td></tr> <tr><td>建設</td><td>15</td></tr> <tr><td>はん用機械</td><td>16</td></tr> <tr><td>電気機械</td><td>17</td></tr> <tr><td>電子部品</td><td>18</td></tr> <tr><td>窯業・土石製品</td><td>19</td></tr> <tr><td>金属製品</td><td>20</td></tr> <tr><td>鉄鋼</td><td>21</td></tr> <tr><td>非鉄金属</td><td>22</td></tr> <tr><td>廃棄物処理</td><td>23</td></tr> <tr><td>繊維製品</td><td>24</td></tr> <tr><td>その他の製造工業製品</td><td>25</td></tr> <tr><td>パルプ・紙・木製品</td><td>26</td></tr> <tr><td>プラスチック・ゴム製品</td><td>27</td></tr> <tr><td>化学製品</td><td>28</td></tr> <tr><td>電力・ガス・熱供給</td><td>29</td></tr> <tr><td>石油・石炭製品</td><td>30</td></tr> <tr><td>鉱業</td><td>31</td></tr> <tr><td>商業</td><td>32</td></tr> <tr><td>運輸・郵便</td><td>33</td></tr> <tr><td>不動産</td><td>34</td></tr> <tr><td>金融・保険</td><td>35</td></tr> <tr><td>情報通信</td><td>36</td></tr> <tr><td>対事業所サービス</td><td>37</td></tr> </tbody> </table> </div>
<!-- 真ん中に3Dグラフ --> <div id="three"></div>
<!-- 右上にノード情報 --> <div id="info"> <!-- ノードの情報がここに表示されます --> </div>
<!-- JavaScript ライブラリの読み込み --> <script type="module" src="https://unpkg.com/three@0.158.0/build/three.js" defer></script> <script type="module" src="https://unpkg.com/3d-force-graph@1.73.0/dist/3d-force-graph.min.js" defer></script> <script type="module" src="https://unpkg.com/three-spritetext@1.8.1/dist/three-spritetext.min.js" defer></script> <script src="./static/main2_0114.js" charset="utf-8" defer></script> </body> </html>
const highlightLinks = new Set(); const highlightNodes = new Set(); let hoverNode = null;
const Graph = ForceGraph3D()
(document.getElementById("three"))
.jsonUrl('http://127.0.0.1:5000/static/json/output_toda_kihon_zenbu.json')
.nodeColor('#ffffff')
.nodeLabel('id')
.nodeRelSize(20)
.nodeThreeObject(node => {
const sprite = new SpriteText(node.id);
sprite.material.depthWrite = false; // make sprite background transparent
sprite.color = node.color;
// // 特定のノード名の色を変更
// if (node.id === '農林漁業') {
// sprite.color = 'red'; // 特定ノードのテキスト色を赤に変更
// } else {
// sprite.color = node.color; // 他のノードは元の色を使用
// }
sprite.textHeight = 7.5;
return sprite;
})
.linkThreeObject(link => {
// extend link with text sprite
const sprite = new SpriteText(link.value.toFixed(2)); // 小数点1桁にフォーマット
sprite.color = 'blue';
sprite.textHeight = 4.0;
return sprite;
})
.linkPositionUpdate((sprite, { start, end }) => {
const middlePos = Object.assign(...['x', 'y', 'z'].map(c => ({
[c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
})));
// Position sprite
Object.assign(sprite.position, middlePos);
})
.onNodeClick(node => {
// ノードクリック時に入出力リンクを表示
const outgoingLinks = Graph.graphData().links.filter(link => link.source.id === node.id);
const incomingLinks = Graph.graphData().links.filter(link => link.target.id === node.id);
// 左上に情報を表示
const displayArea = document.getElementById("info");
displayArea.innerHTML = `<h3>ノード: ${node.id}</h3>`;
// 出ているリンク
//displayArea.innerHTML += `<h4>Outgoing Links</h4>`;
displayArea.innerHTML += `<h4>販売先</h4>`;
outgoingLinks.forEach(link => {
displayArea.innerHTML += `<p>${link.target.id} (Value: ${link.value.toFixed(2)})</p>`;
});
// 向かってくるリンク
//displayArea.innerHTML += `<h4>Incoming Links</h4>`;
displayArea.innerHTML += `<h4>購入先</h4>`;
incomingLinks.forEach(link => {
displayArea.innerHTML += `<p>${link.source.id} (Value: ${link.value.toFixed(2)})</p>`;
});
})
.onNodeHover(node => {
// ノードにホバーした時の処理
hoverNode = node ? node.id : null;
updateHoverText(); // テキストを更新する関数を呼び出す
})
.linkOpacity(0.25)
.linkDirectionalArrowLength(3)
.linkDirectionalArrowRelPos(1)
.linkCurvature(0.01)
//.linkDirectionalParticleWidth(2)
//.linkDirectionalParticles("value")
//.linkDirectionalParticleSpeed(d => d.value * 0.01)
.linkThreeObjectExtend(true)
.linkColor(() => 'red') // リンクの色を黒に設定
.linkWidth(1)
.backgroundColor("#f8f8f8");
// ノード間の力学的レイアウトを調整
Graph
.d3Force("link").distance(300) // リンクの長さを100に設定(デフォルトは約30-50)
.d3Force("charge").strength(300); // ノード間の反発力を調整(値を小さくすると密集する)
function updateHoverText() {
// ホバー時のノードのテキストサイズを変更
Graph.nodeThreeObject(node => {
const sprite = new SpriteText(node.id);
sprite.material.depthWrite = false;
sprite.color = node.color;
sprite.textHeight = hoverNode === node.id ? 15 : 7.5; // ホバー時にテキストサイズを変更
return sprite;
});
}
.jsonUrl('http://127.0.0.1:5000/static/json/output_toda_kihon_zenbu.json').nodeThreeObject
.onNodeClick
.linkOpacity(0.25)
.linkDirectionalArrowLength(3)
.linkDirectionalArrowRelPos(1)
.linkCurvature(0.01)
.linkDirectionalParticleWidth(2)
.linkDirectionalParticles("value")
.linkDirectionalParticleSpeed(d => d.value * 0.01)
.linkThreeObjectExtend(true)
.linkColor(() => '#708090')
.linkWidth(1)
.backgroundColor("#f8f8f8")
実際の出力結果
#ref(): File not found: "3d.jpg" at page "戸田さん卒論_backup"
スクレイピングなどの処理を行っているとき画面がそのままだと,ちゃんとプログラムが動いているのかわからないのでロード画面を作った.
<div id="loading">
<p>作成中...</p>
</div>
<script>
window.addEventListener('DOMContentLoaded', function(){
// ページの表示状態が変わったときに実行
document.addEventListener("visibilitychange", function() {
console.log(document.visibilityState);
document.getElementById('loading').style.display = 'none';
});
});
function performSearch() {
var loadingScreen = document.getElementById('loading');
// ロード画面を表示
loadingScreen.style.display = 'flex';
}
</script>
ブラウザバックをしたときにフォームなどのユーザからの入力が保持されていないとエラーが起こるため,それを回避している.
@app.route('/post', methods=['POST'])
def post():
session["class"] = request.form.get("class_select")
return redirect(url_for('select'))
@app.route("/select", methods=["GET", "POST"])
def select():
if 'class' not in session:
return redirect(url_for('select'))
class_set = session["class"]
~~~~~~~~~~省略~~~~~~~~~~改善版としてグラフをよりアトラクティブにするシステムを作成した. 改善前ではノードを引っ張ることしかできていなかったが,クリックしドラッグを行うことで様々な角度からネットワークを確認できるようにした.
その際にはthree.jsによる可視化を行い,アプリケーションの作成を行った.
three.jsについては以下のサイトを参考にした. https://github.com/vasturiano/3d-force-graph 以下に改善版のプログラムを示す.
const highlightLinks = new Set(); const highlightNodes = new Set(); let hoverNode = null;
const Graph = ForceGraph3D()
(document.getElementById("three"))
.jsonUrl('http://127.0.0.1:5000/static/output2.json')
.nodeColor('#ffffff')
.nodeLabel('id')
.nodeRelSize(20)
.nodeThreeObject(node => {
const sprite = new SpriteText(node.id);
sprite.material.depthWrite = false; // make sprite background transparent
sprite.color = node.color;
sprite.textHeight = 7.5;
return sprite;
})
.linkThreeObject(link => {
// extend link with text sprite
const sprite = new SpriteText(`${link.value}`);
sprite.color = 'blue';
sprite.textHeight = 1.5;
return sprite;
})
.linkPositionUpdate((sprite, { start, end }) => {
const middlePos = Object.assign(...['x', 'y', 'z'].map(c => ({
[c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
})));
// Position sprite
Object.assign(sprite.position, middlePos);
})
.onNodeClick(node => {
var baseUrl = "page/" + node.id;
window.location.href = baseUrl;
})
.onNodeHover(node => {
// ノードにホバーした時の処理
hoverNode = node ? node.id : null;
updateHoverText(); // テキストを更新する関数を呼び出す
})
.linkOpacity(0.25)
.linkDirectionalArrowLength(3)
.linkDirectionalArrowRelPos(1)
.linkCurvature(0.01)
.linkDirectionalParticleWidth(2)
.linkDirectionalParticles("value")
.linkDirectionalParticleSpeed(d => d.value * 0.01)
.linkThreeObjectExtend(true)
.linkColor(link=>{
if (link.value > 0){
return 'red'
} else {
return 'blue'
}
})
.linkWidth(1)
.backgroundColor("#f8f8f8");
function updateHoverText() {
// ホバー時のノードのテキストサイズを変更
Graph.nodeThreeObject(node => {
const sprite = new SpriteText(node.id);
sprite.material.depthWrite = false;
sprite.color = node.color;
sprite.textHeight = hoverNode === node.id ? 15 : 7.5; // ホバー時にテキストサイズを変更
return sprite;
});
} これを実行する際は改善前と同じくflaskフォルダ内のapp.pyを実行すればよい. 以下に実行結果を示す.
#ref(): File not found: "2.png" at page "戸田さん卒論_backup"