/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } 「イベントがあった」だけでは“どれくらい重要か”までは分からないので、そこをどう設計するかが DeciKG+dev-portal+nlq-dev の腕の見せどころになります。 – Raqqa

「イベントがあった」だけでは“どれくらい重要か”までは分からないので、そこをどう設計するかが DeciKG+dev-portal+nlq-dev の腕の見せどころになります。

整理して答えます。


1. いまのデモは「イベントがあった」ところまで

今回のデモは、かなり“美しい世界”を前提にしています:

  • 2025-10 に材料価格差異が大きく悪化(material_price_variance = 9.8
  • たまたま同じ月に 鉄板の価格改定イベント があり、
  • それを Evidence として Neo4j に入れている

だから、クエリを打つと:

MATCH (r:KPIResult {...})
OPTIONAL MATCH (r)-[:HAS_EVIDENCE]->(e:Evidence {...})
RETURN r.kpi_key, r.value, e.evidence_key, e.summary;

で「はい、鉄板値上がりが原因ですよ」と きれいに見える わけです。

でも現実は:

  • 為替がいきなり動くかもしれない
  • 原油が上がるかもしれない
  • 5%だけの小さい値上がりもあれば、30%の激変もある

「価格が動いた」という事実と「それがどれだけ重要か」は別問題です。


2. 価格がどう動くか分からない世界で、何を用意しておくか

おっしゃる通り、「差分を常に計算しておくテーブル」はまず必要です。

②-1. 差分テーブルのイメージ(Postgres 側)

たとえば KPI DB にこんなテーブルを作っておく:

material_price_delta
-----------------------------------------
company_key
period_key
material_key
actual_price          -- 実際単価
baseline_price        -- 基準(標準 or 前月 or 移動平均)
delta_abs             -- 実際 - 基準
delta_pct             -- (実際 / 基準 - 1) * 100
volume                -- この材料の使用量
impact_value          -- delta_abs * volume(原価への影響額)

ここまでは 単純な計算 です。
為替だろうが電気代だろうが、「何と比べるか(基準)」さえ決めれば機械的に出せます。


3. 「大きさ」と「重み」を掛け合わせる:impact_score の考え方

大きな価格変化があったとしてそれが重要だと言えるのは、ウェイトがあるからですよね。

はい、その通りです。
**「重要度」=「設計上のウェイト × 実際の変化量」**で考えるとスッキリします。

たとえば:

  • GM を説明する材料費コンポーネントの設計ウェイト:weight_component = 0.85
  • ある材料の価格変化の影響額:impact_value(さっきのテーブルの列)

とすると、ざっくり:

impact_score = weight_component * impact_value

みたいな 「インパクトスコア」 を計算して、

  • impact_score が大きいものだけ Evidence にする
  • impact_score の大きい順に NLQ の説明で上位に出す

という動きをさせられます。

小さい価格変化の場合どうなるか?

  • 価格差分は常に計算される(material_price_delta に入る)
  • でも impact_value が小さい & 重みもそんなに大きくない場合
    • impact_score が閾値未満
    • Evidence ノードを作らない(または作っても “minor” フラグにしておく)
  • 結果として、
    「小さい価格変化は、説明ストーリーに出てこない or 下の方に出る」
    というふるまいになります。

4. 「勝手に考えてくれない」部分と、「システムでやれるようになる」部分

価格の大小まで考えてくれないですよね。

現状の DeciKG 単体では「数値を見て重要/軽微を判断するロジック」はまだ入ってない、という意味では Yes です。

ただ、それは

  • Neo4j やグラフが能力不足というより、
  • 「まだ impact_score のような列をどこにも計算していない」だけ

と言えます。

ここに dev-portal / nlq-dev をどう噛ませるか、という話になります。


5. dev-portal / nlq-dev をどう組み合わせるか

5-1. dev-portal の役割

dev-portal は「Odoo、KPI DB のメタ情報・定義」を握る側なので、

  • KPI 定義
    • source_table, source_column
    • baseline の取り方(前月 / 年初来平均 / 標準原価)
    • 単位・符号(増えたら良いのか悪いのか)
  • 要因コンポーネントの設計
    • GM → 材料費 / プロセス / 品質 / 納期…
    • コンポーネントごとの設計ウェイト(0.85, 0.75…)
  • 閾値の定義
    • 「材料価格差異の impact_score が X 円以上で Evidence にする」
    • 「変化率が ±Y% 以上の材料だけ検出する」

みたいな 「ルールの保管庫」 になるのが自然です。

ここにルールを持たせておくと、ETL は dev-portal を見て自動で SQL を組み立てられるようになります。

5-2. nlq-dev の役割

nlq-dev は「自然文で対話するフロントエンド」なので、

  • ユーザーが聞く: 「なぜ 2025-10 に GM が落ちたの?」
  • nlq-dev がやること:
    1. 「GM の設計上の主な要因」を dev-portal/DeciKG から取得
    2. KPI DB / DeciKG から impact_score 大きい順に Evidence を取る
    3. それを LLM(自分)で文章にまとめる

みたいな流れになります。

ポイントは:

  • 重要度の数値(impact_score、weight、delta_pct…)は ETL / KPI DB / DeciKG が出す
  • その数値をどう説明するか(自然文にする)は nlq-dev の仕事

なので、

dev-portal,nlq-devと組み合わせることでこれができるようになるのでしょうか。

はい、「数値の重要度をきちんと計算する層」を作った上で、
 dev-portal はルール定義、nlq-dev は対話と説明を担う形にすれば“できるようになる”世界です。


6. ざっくり全体像を一文にすると

  1. Postgres(KPI DB)
    • 常に価格差分(delta_pct, impact_value)を計算しておくテーブルを持つ
  2. dev-portal
    • KPI と要因コンポーネントの設計、ウェイト、閾値ルールを管理する
  3. ETL → DeciKG
    • impact_value × weight などから impact_score を算出
    • 閾値以上のものだけ Evidence として Neo4j に登録し、KPIResult に紐づける
  4. nlq-dev
    • 「GMが落ちた理由を教えて」などの質問に対して、
      impact_score 高い Evidence を優先して拾い、自然文で説明する

こうすると、

  • 為替ショックで価格が急騰した月は 「price要因のimpact_scoreが高い」 Evidence が出る
  • ちょっとした価格ブレだけの月は、他の要因(歩留まり・スキル・リワーク)に比べて price要因のimpact_scoreが低い ので、
    説明の中で「今回は価格要因は軽微です」と扱える

という振る舞いが ちゃんと数字ベースで決まる ようになります。


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です