2024年8月5日月曜日

食品価格動向調査(野菜)のデータを JSON または CSV 形式で取得する Web API を作りました

食品価格動向調査(野菜)のデータを JSON または CSV 形式で取得する Web API を作りました。

いつも通り cultivationdata.net で公開しています。

キャベツ、ねぎ、レタス、ばれいしょ、たまねぎ、きゅうり、トマト、にんじん、はくさい、だいこんの10品目の小売店における価格動向を把握することができます。元データは、農林水産省の食品価格動向調査(野菜)です。

ちなみに、市況に関しては以前作成しています → こちら


以下、食品価格動向調査(野菜) Web API のエンドポイント(URI)です。

食品価格動向調査(野菜) JSON 形式(最新)
https://api.cultivationdata.net/yasai_kakaku

食品価格動向調査(野菜) JSON 形式(全件)
https://api.cultivationdata.net/yasai_kakaku?div=all

食品価格動向調査(野菜) CSV 形式(最新)
https://api.cultivationdata.net/yasai_kakaku?type=csv

食品価格動向調査(野菜) CSV 形式(全件)
https://api.cultivationdata.net/yasai_kakaku?div=all&type=csv


キーの説明はこちら → 食品価格動向調査(野菜)API説明書


CSV 形式は、Google スプレッドシートの IMPORTDATA 関数によりデータの一括取得が可能です。


たまねぎ

農家としても、値付けの参考になるかと思います。


ということで、久しぶりに Web API のラインナップが増えました。今回は、初めて元データが Excel でしたが、うまく取得して加工できました。最近、プログラミングをする際は Claude に手伝ってもらっています。ホント便利ですね~。

2024年7月29日月曜日

GitHub の Markdown ファイルを Web ページ化する

更新が頻繁なページなどで、GitHub の Markdown(MD) ファイルをそのまま Web ページに変換して公開できたら便利かなーと思いまして、簡単に作ってみました。

例えば、以下のような感じになります。


読み込みに若干のタイムラグが発生しますが、とりあえずイメージしたものを作れました。サーバーにファイルを増やさなくていいのがとても楽。


JavaScript は次の通りです。

// URLパラメータからMarkdownファイルのURLを取得する関数
function getMarkdownUrlFromParams() {
    const urlParams = new URLSearchParams(window.location.search);
    return urlParams.get('md');
}

// Markdownファイルを取得し、HTMLに変換する関数
async function convertMarkdownToHtml(url) {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const markdown = await response.text();
        return marked.parse(markdown);
    } catch (error) {
        console.error('Error:', error);
        return null;
    }
}

// HTMLからh1タグの内容を抽出する関数
function extractH1Content(html) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    const h1 = doc.querySelector('h1');
    return h1 ? h1.textContent : null;
}

// メイン処理
async function main() {
    const URL = 'https://raw.githubusercontent.com/ユーザ名/リポジトリ名/ブランチ名/'; // Markdownファイルが置かれているURL
    const mdFile = getMarkdownUrlFromParams();
    const errorElement = document.getElementById('error');
    const contentElement = document.getElementById('content');

    if (!mdFile) {
        errorElement.textContent = 'URLパラメータにMarkdownファイルが指定されていません。(?md=sample/file.md)';
        return;
    }

    const html = await convertMarkdownToHtml(`${URL}${mdFile}`);
    if (html) {
        contentElement.innerHTML = html;
        
        // h1タグの内容を抽出してタイトルに設定
        const h1Content = extractH1Content(html);
        if (h1Content) {
            document.title = `${h1Content}`;
        }
    } else {
        errorElement.textContent = '変換に失敗したか、ファイルが見つかりません。';
    }
}

// ページ読み込み時に実行
window.onload = main;


続いて、HTML ファイルです。先ほど作った JavaScript を md.js として読み込みます。

<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script src="./md.js"></script>
</head>
<body>

    <div id="error"></div>
    <div id="content"></div>

</body>
</html>

Markdown テキストからHTMLへの変換には marked.js ライブラリを利用しています。(ページ内リンクは機能しない?ようです)

GitHub 上の Markdown ファイルは Raw ファイルの URL になります。JavaScript のメイン処理の URL を書き換えてください。

(当然ですが、アクセスできる場所に置いてあれば、GitHub 上の Markdown ファイルの必要はありません)


コケモモ(甲斐犬ハーフ)

昨年に引き続き、今年も暑すぎですね。農業にもいろいろと影響が出ていますが……、何はともあれ健康第一。

皆様もどうぞ、熱中症などには気を付けてお過ごしください。

2024年7月16日火曜日

【代引き注意】Amazon マーケットプレイス商品の購入でトラブルにあった(追記あり)

久々に Amazon マーケットプレイスで購入トラブルにあったので記録を残しておきます。注意喚起とともに、同じトラブルに巻き込まれた場合の対応の参考になればと思います。

まずはじめに、金銭的な被害を受けないための対応ですが、Amazon で注文した商品が代引きで送られてくることは絶対にないので、確実に受取拒否してください。

今回、私は事無きを得ましたが、一歩間違えると面倒なことになるようです。


一連の流れは以下の通りです。


1)Amazon で商品を購入(7/14 21:05)

購入したのは純正品のバッテリーで、商品名の下にメーカーのストア表示があるが、出荷元と販売元は別のマーケットプレイス出品者でした。(メーカー及び Amazon からの販売は確認した範囲では無し)

今思うとポイント還元も含めてかなり割安ではあったものの、プライムデーの先行セール品になっていたため怪しむこともありませんでした。

クレジットカード決済で問題なく購入手続きを完了。


2)SMS でメッセージが送られてくる(7/15 20:23)



メッセージがぶつ切れなうえ、順番がめちゃくちゃですが、入れ替えると下のようになります。

お客様注文番号:
購入者様:

この度は、Amazonで弊社の商品をご購入頂きまして、誠に有難う御座います。
現在店舗側とAmazonのシステムで一部不具合が生じており、Amazonの「決済機能」と「配送サービス」が利用出来ない状態でご面倒をおかけして、Amazonによりその為、弊社は現在在庫的な措置として、Amazonによる配送ではなく、弊社倉庫より佐川急便にて配送を行っております。 
またその際にAmazonの決済機能が使えない為、代替え措置として佐川急便の代引きにて配送を行なっております。 
佐川急便追跡番号: 
お客様にはご迷惑とご不都合をお掛けし誠に申し訳ございません。
何卒、ご理解を頂きたく宜しくお願い致します。
本件に関しまして、ご不明な点がありましたら、お知らせください。
また、Amazonで決済された金額に関しては、自動的にキャンセルされますが、万が一、返金されない場合は、大変お手数ですがその旨をメール内のメッセージにてご連絡頂きたくお願い致します。 ご注文日から5営業日以内にご確認とご連絡をお願い申し上げます。
【出品者連絡】
メールアドレス: 
電話番号: 
最後に、この度は折角ご注文を頂いたにも関わらず、ご迷惑とご不都合をお掛けし申し訳ありません。
誠意を持ってご注文された商品をお届けするよう最後まで対応する所存ですので、何卒ご理解を頂きたく宜しくお願い致します。

                   敬具
(番号等は削除しています)

初めはスパムかと思ったものの、注文番号と購入者名が実際のものだったため、出品者からのものだと気づく。

尚、メッセージへの返信は不可。



3)Amazon アカウントの状況を確認

Amazon のメッセージセンターへの連絡は無し。

「注文履歴」 > 「注文の詳細」の取引履歴で返金処理中であることを確認。

商品は日本郵便で発送済みになっているものの、追跡情報(トラッキングID)からは荷物の確認はできず。


4)佐川急便で商品が発送される

SMS で送られてきた佐川急便の追跡番号から、商品が発送されたことを確認。

(おそらく代引きで送られてくるであろう)佐川急便の荷物は Amazon の関知しないところで配送と決済が行われることになるので、当然ながら受取拒否することに。

その上で、
  • 処理中になっている返金が確実に実行されるのか
  • 返金の実行にあたって注文のキャンセルや返品などのアクションが必要なのか
  • 日本郵便で発送済みになっている荷物の行方
の3点が不明だったため Amazon カスタマーサービスに連絡することにしました。


5)Amazon カスタマーサービスに連絡(7/16 11:33)

ヘルプやチャットでは不明点の解消ができなかったため、担当者の方からの電話をリクエストしました。

判明した点は以下の通り。

まず返金について。

返金処理にもいくつかの種類があるそうです。今回は返品の有無に関わらず返金がされる処理になっており、特にアクションは必要ないとのこと。処理には数日程度要するが、確実に返金はされますとのことでした。

次に、日本郵便で発送された荷物の行方です。

こちらは、出荷元によるシステム上の操作で発送済みにできてしまうため、実際に発送されているかはわからないとのこと。追跡情報がないということは、おそらく発送はされていないのでしょう。

ただ、そもそも発送済みにしない方が購入者に対して別配送をする説得力が増すんじゃないのかと不思議に思ったのですが、よく考えたら Amazon のシステムへの対策なのかもしれません。


ということで、代引きで届く荷物さえ確実に受取拒否すれば損害を被ることはありません。一件落着です。


今回のようなケースはよくあることなのかと思ったら、担当者の方曰く、まだほとんど聞いたことのない事例だそうです。

もしも SMS をスパムだと思って特に気にせず、自身の留守中などに家族が代引きの荷物を受け取ってしまったりすると非常に厄介なことになる可能性があります。

お金を払ってしまっているものの、送られてきた商品がちゃんとしたものである保証はありません。

そして、ちゃんとした商品でなかったとしても、Amazon のあずかり知らぬところで配送も決済も行われているため、Amzon は介入がしづらいそうです。

まず購入者が Amazon に対してトラブルが発生したということをいろいろ証拠を提出して示し、その上で Amazon が警察に相談して……という流れになるんじゃないか、というのがカスタマーサービス担当者の方の話でした。(推測を含む話だと思います)

代引きで支払ってしまった代金がどうなるのかですが、一般論として一度払ってしまうと取り返すのもしんどそうです。

例えば、上述のような手続きをしたとして Amazon が保証してくれるかですが、それはそれで悪用ができてしまいそうなので、厳しいような気がします。追記 2024-7-25:Amazon のカスタマーサービスに連絡 → 警察へ相談 → Amazonに相談内容を提出で返金対応がなされたとのレビューがありました)

ちなみに、出品者の「特定商取引法に基づく表記」にある住所から情報をたどってみたところ、登記されている会社が出てきました。

事業内容としてはバッテリーの物販とは程遠く、少なくとも表向きはちゃんとしてそうに見えるのですが、どういうことなんでしょうか。

会社のサイト上の連絡先は、SMS 及び Amazon に載っていたものと異なっていました。調べると、出品者のアカウントを乗っ取られたというパターンもあり得るようです。


-----
・追記(2024-7-17 21:53)

私が購入した出品者にレビューが付いていました。(昨日時点では無し)


全く同じ手口で被害出てしまっていますね……。

昨日私がカスタマーサービスに連絡した時点で、Amazon 側でも何か対策を取っていれば、あるいはとも思いますが……。


また、先ほど確認したところ、この出品者の商品が全てなくなっていました。割安な価格とポイント還元で短期間に注文を受け、その後、この手口で詐欺をするということであれば、やはり出品者アカウントは乗っ取りの可能性が高いように思います。

低評価のレビューを書かれて、Amazon 側に問い合わせが行くことも想定しているでしょうから、乗っ取ったアカウントを使い捨てながら繰り返すつもりなのかもしれません。


・追記(2024-7-18 7:56)

Amazon での決済の返金完了を確認。

ちなみに、返金になった決済で付いた Amazon ポイントや d ポイントは返還になります。

もしすでにポイントを使ってしまっていたらどうなるのかとカスタマーサービスで聞いたところ、その場合は返金額から差し引かれるとのことでした。


・追記(2024-7-24 15:00)

継続的にバッテリーのチェックをしていたところ、7/20にまた似たような出品者が出現。

今回も同住所に登記されている会社があり、新規出品者で2000点以上の商品が出品されていました。

確認した範囲では、どの商品も同一商品で一番安くされているため、最初に表示されるようになっているようです。

7/22夜頃には2000点以上あった商品が全てなくなり、7/23夜頃から前回の出品者と同じような内容の星1のレビューが付き始めました。

やはり、アカウントを使い捨てながらやっているんでしょうか。気をつけなければなりません。
-----


みなさま、どうぞお気をつけください。

2024年7月3日水曜日

【追記あり】ポートフォリオ最適化ツールを作りました

まずは頑張って資金をたくわえてコーストFIREへの到達を目指す。その後、日々の生活費は農業で稼いで、老後資金は資産運用でまかなう。こうした道も一つかもしれません。


天候不順、災害の頻発、市場価格の変動、急激な産業構造の変化、それらに伴うコスト増、エトセトラ、エトセトラ……。

昨今、農業経営の難易度が上がっているという話をそこかしこで聞きます。私自身、10年前とは比ぶべくもないというのが実感です。

こうなってくると、食料生産に資するという農業の本流かつ激流の土俵から下りて、よりコンパクトでコントローラブルな農業を目指そうという気持ちが芽生えてくるのも無理からぬことのように思います。

それは例えば、コーストFIRE、もしくはスローライフや牧歌的農業のイデアといった姿になるかもしれません。

資本力のない我々個人農家にとっては、利幅減少による窮余一策から規模拡大に舵を切りジリ貧になっていくというパターンこそ避けねばなりません。そこから先の人生の選択肢を狭めることになります。

大規模化は、より高度な栽培技術や経営のハンドリングが求められます。明確な勝ち筋なく、突っ込むものではないですね。

人が減り、農地が捨てられている今、産業としての農業のあり方も変えていく必要があります。大きな方向性としては、それはおそらく大規模化を伴うものであると思いますが、あえてあなたが、私が、それを担うこともないわけです。

まずは主体的に最適な規模、営農環境を整えることに注力し、生き残るために最適な土俵を見つけることが大切かと思います。


……そんなこんなで前振りが長くなりましたが、万が一コーストFIREして牧歌的農家になってしまったときに老後資金を適切なポートフォリオで運用していくことができるように「ポートフォリオ最適化ツール」を作ってみました(唐突)。よわよわサーバーで運用中です。

ポートフォリオ最適化ツール(外部サイト)
→ やはりサーバーが弱すぎて安定運用ができませんでした。代わりに Python コードを note に置きました

コード自体は9割 Claude 3.5 Sonnet に作ってもらいました。効率的フロンティアを計算し、最適なポートフォリオ配分として接点ポートフォリオと最小分散ポートフォリオを提示、結果をグラフで視覚化します。




ポートフォリオという考え方は、リスクを管理し、効率を追求するという点で、農業にも活かせるところがあるように思います。


-----
・追記(2024-7-9)

モンテカルロ・シミュレーター」「リバランス・シミュレーター」「ストレステスト・シミュレーター」も作りました。

農家の資産運用について cultivationdata.net にまとめました。


・追記(2024-8-28)

運用サーバーが弱すぎて安定運用が難しかったため、同様の Python コードを note に置きました

また農家の資産運用についてもまとめました。
-----

2024年6月29日土曜日

GAS で Claude API を使った LINE bot を作る

先日、GPTGemini を使った LINE bot をそれぞれ作りましたが、今回は Claude の API を使った LINE bot を GAS(Google Apps Script)で作ってみました。

モデルは、Claude 3.5 Sonnet を使っています。プログラミングのお供にめちゃくちゃ便利ですね。


それでは、作り方です。

LINE Messaging API チャンネルの作成とアクセストークンの取得。及び、Claude の API キーの取得は済んでる想定です。

それぞれ、GAS の「プロジェクトの設定」よりスクリプト プロパティとして追加・保存しておきます。プロパティ名は「Line_key」と「Claude_key」としています。

また、スプレッドシートを用意して、「chat」シート、「system」シート、「temperature」シートを作っておきます(サンプル)。(作り方は「GAS で ChatGPT とやりとりできる簡単な LINE bot の作り方」を参照)


★ 機能一覧
  • 文頭に「続き)」を付けることで会話を継続。(例:続き)もっと詳細に教えて
  • 文頭に「昔話!」のように付けることで、スプレッドシートの「system」シートにあらかじめ登録したシステム指示を実行。(例:昔話!きのこ
  • 文頭に「画像)」で画像の処理モード。(例:画像)何ですか? → 画像を送信)
  • 文末に「(厳密」「(創造」で temperature を指定。通常 0.5、厳密 0、創造 1。(例:3行で小噺を作って(創造

コードは以下の通りです。

const spreadsheet = SpreadsheetApp.openById('スプレッドシート ID); // スプレッドシート ID を書き換え

const chatSheet = spreadsheet.getSheetByName('chat');
let chatLastRow = chatSheet.getLastRow();
let chatRange = chatSheet.getRange(2, 1, chatLastRow, 2);

const systemSheet = spreadsheet.getSheetByName('system');
const systemLastRow = systemSheet.getLastRow();
const systemWatchwordRange = systemSheet.getRange(2, 1, systemLastRow - 1, 1);
const systemContentRange = systemSheet.getRange(2, 2, systemLastRow - 1, 1);

const tempSheet = spreadsheet.getSheetByName('temperature');
const tempRange = tempSheet.getRange(1, 1);

function doPost(e) {
  try {
    const event = JSON.parse(e.postData.contents).events[0];
    let prompt = [];
    let replyMessage = '';

    switch(event.message.type) {
      case 'text': // リクエストがテキストの場合
        let messageContent = event.message.text;

        if (messageContent.startsWith('画像)')) {
          // 「画像)」に続いて指示で画像処理
          // 画像処理の場合
          messageContent = messageContent.replace('画像)', '');
          claudeRequestText(messageContent);
          sendLineMessage(event.replyToken, '画像を送信してください'); 
        } else {
          // 画像処理以外の場合
          prompt = claudeRequestText(messageContent);
          replyMessage = getClaudeReply(prompt); // Claude で回答
          setAiMessage('assistant', replyMessage); // AI の返答をスプレッドシートに'role': 'assistant'で格納
          sendLineMessage(event.replyToken, replyMessage);
        }
        break;
      case 'image': // リクエストが画像の場合
        prompt = claudeRequestImage(event.message.id);
        replyMessage = getClaudeReply(prompt); // Claude で回答
        sendLineMessage(event.replyToken, replyMessage);
        break;
      default:
        // リクエストがサポート外のデータ形式の場合
        sendLineMessage(event.replyToken, 'サポート外のデータ形式です');
    }
  } catch {
    // エラー発生時
    sendLineMessage(event.replyToken, '不明なエラーが発生しました');
  }
}

// Claude でリクエストデータ(テキスト)を生成する関数
function claudeRequestText(messageContent) {
  // system ロールの配列を生成
  const systemWatchwordArry = systemWatchwordRange.getValues().flat();
  const systemContentArry = systemContentRange.getValues().flat();
  let systemWatchword = '';
  let systemContent = '';

  // リクエスト内容の処理
  if (messageContent.startsWith('続き)')) {
    // 文頭に「続き)」で会話を継続
    messageContent = messageContent.replace('続き)', '');
    chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([['user', messageContent]]);
  } else {
    // temperature を取得してスプレッドシートにセット
    // 文末に「(創造」「(厳密」で指定
    if (messageContent.endsWith('(創造')) {
      messageContent = messageContent.replace('(創造', '');
      tempRange.setValue(1);
    } else if (messageContent.endsWith('(厳密')) {
      messageContent = messageContent.replace('(厳密', '');
      tempRange.setValue(0);
    } else {
      tempRange.setValue(0.5);
    }
    // スプレッドシートにあらかじめ登録した system ロールの読み込み
    // 文頭に「〇〇!」でsystem ロールを指定
    for (let i = 0; i < systemWatchwordArry.length; i++) {
      if (messageContent.startsWith(systemWatchwordArry[i] + '!')) {
        systemWatchword = systemWatchwordArry[i] + '!';
        systemContent = systemContentArry[i];
        break;
      }
    }
    messageContent = messageContent.replace(systemWatchword, '');

    chatRange.clear(); // 以前のチャット内容を削除

    // リクエスト内容をスプレッドシートに格納
    if (systemContent) {
      chatSheet.getRange(2, 1, 2, 2).setValues([['system', systemContent], ['user', messageContent]]);
      chatRange = chatSheet.getRange(2, 1, 2, 2);
    } else {
      chatSheet.getRange(2, 1, 1, 2).setValues([['user', messageContent]]);
      chatRange = chatSheet.getRange(2, 1, 1, 2);
    }
  }

  // リクエストデータを生成
  let messages = [];
  let values = chatRange.getValues();
  for(let i = 0; i < values.length; i++) {
    if(values[i][0] == 'system') {
      continue;
    }
    messages.push({'role': values[i][0], 'content': values[i][1]});
  }

  return messages;
}

// Claude でリクエストデータ(画像)を生成する関数
function claudeRequestImage(id) {
  // 画像を取得
  const response = UrlFetchApp.fetch('https://api-data.line.me/v2/bot/message/' + id + '/content',{
    'headers': {
      'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('Line_key')}`,
    },
    'method': 'get'
  });
  const imageBlob = response.getBlob().getAs('image/png');

  // 画像を Base64 にエンコード
  const base64Image = Utilities.base64Encode(imageBlob.getBytes());

  // リクエストデータを生成
  let messages = [];
  let values = chatSheet.getRange(2, 1, chatLastRow - 1, 2).getValues();
  for(let i = 0; i < values.length; i++) {
    if(values[i][0] == 'user') {
      messages.push({'role': 'user','content': [{'type': 'image','source': {'type': 'base64','media_type': 'image/png','data': base64Image}},{'type': 'text','text': values[i][1]}]});
    } else {
      messages.push({'role': values[i][0], 'content': values[i][1]});
    }
  }

  return messages;
}

// Claude から回答を得る関数
function getClaudeReply(prompt) {
  let system = "";
  if(chatSheet.getRange(2, 1).getValue() == 'system') {
    system = chatSheet.getRange(2, 2).getValue();
  }

  const payload = {
    'model': 'claude-3-5-sonnet-20240620', // 使用するモデル,
    'max_tokens': 1024, // 生成する文章の最大トークン数
    'temperature': tempRange.getValue(),
    'system': system, // システム指示
    'messages': prompt
  };
  
  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': PropertiesService.getScriptProperties().getProperty('Claude_key'),
      'anthropic-version': '2023-06-01',
    },
    payload: JSON.stringify(payload),
  };

  try {
    const response = JSON.parse(UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', options).getContentText());
    return response.content[0].text; // Claude の回答
  } catch (error) {
    // エラー発生時
    console.error('Error:', error);
    return 'エラー: Claude の回答が得られませんでした';
  }
}

// AI の返答をスプレッドシートに格納する関数
function setAiMessage(role, replyMessage) {
  chatLastRow = chatSheet.getLastRow();
  chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([[role, replyMessage]]);
}

// LINE メッセージを送信する関数
function sendLineMessage(replyToken, replyMessage) {
  const linePayload = {
    'method': 'post',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('Line_key')}`,
    },
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': replyMessage,
      }],
    }),
  };

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', linePayload);
}

1行目の「スプレッドシート ID」は書き換えてください。

コード作成後にウェブアプリとして公開、URL を LINE Messaging API チャンネルで Webhook として設定して完成です。





***

生成AI、本当に日進月歩で性能が上がっていきますね。自身にとってのより良い活かし方を、日々探っていきたいですね。


cultivationdata.net では、GPT、Gemini、Claude を搭載した LINE bot の作り方を公開しています。 → note での公開に変更しました(2024-8-17)

2024年5月28日火曜日

スプレッドシートのデータ分析を AI(GPT)に手伝ってもらう GAS ライブラリを作成しました


スプレッドシート上のデータの分析を AI(GPT)に手伝ってもらう GAS ライブラリを作りました。Google Apps Script 上で直接分析、または分析の提案を受けることができます(どういった分析になるかはデータによります)。

GAS ライブラリは、欠損値・重複値の探索を行う GAS ライブラリ以来、久しぶりに作りました。ライブラリ化しておくことで、いつでもお手軽に使えます。

GPT を使用するため、OpenAI の API キーが必要となります。


スクリプト ID

1kwJbmRU1hKumO3I_HeO2leId8SiTojKsdZOx5DsYTUWrqkrenj9OEq3W


ライブラリの追加方法:

  1. ライブラリの追加から上記スクリプト ID を入力して「検索」を押下
  2. バージョンを選択、任意の ID を指定して「追加」を押下

※ バージョンは最新のもの、もしくは「HEAD(開発モード)」を指定してください。

「HEAD(開発モード)」であれば常に最新の状態が維持されますが、バージョンに反映前の変更も加わりますので、予期せぬ不具合が発生する可能性があります(まぁ、最新バージョンでもありますが)。


使い方:

スクリプトの実行方法は、以下の通りです。

ライブラリの追加の際に指定した任意の ID.aiAnalysis(スプレッドシート ID, シート名, データ範囲, OpenAI の API キー, [システムプロンプト, ユーザープロンプト, 使用するモデル(default: gpt-4o), 生成する文章の最大トークン数(default: 4096), 生成された文章のランダムさ(default: 0.5)]*任意)

戻り値として GPT による回答を返します。

リファレンスはこちら


例えば test という ID でライブラリで読み込んで使う場合、以下のようなコードになります。

function myFunction() {
  const spId = "スプレッドシート ID";
  const shName = "シート名";
  const dataRange = "A1:C3";
  const key = "OpenAI API キー"

  Logger.log(test.aiAnalysis(spId, shName, dataRange, key));

  // 任意の設定
  /**
  const system = ""; // システムプロンプト
  const user = ""; // ユーザープロンプト
  const model = ""; // デフォルトは gpt-4o
  const max_tokens = 1024; // デフォルトは 4096
  const temperature = 0; // デフォルトは 0.5

  const specified = [system, user, model, max_tokens, temperature]

  Logger.log(test.aiAnalysis(spId, shName, dataRange, key, specified));
   */
}


以下は、試しに2020-2021年の原木椎茸の栽培データを投げてみた結果です。

データを分析するために、以下のステップを踏みます:

### 1. データの概要を把握
まず、データの全体的な構造と各列の意味を把握します。データには以下の列があります:
- 発生ID
- 発生開始日
- 品種
- 使用回数
- 棚番号
- 収穫日
- 積算温度(℃時)
- 平均温度(℃)
- 分散(温度)
- 標準偏差(温度)
- 平均湿度(%)
- 分散(湿度)
- 標準偏差(湿度)
- タイミング
- 品質
- 収穫までの日数
- 参考値

### 2. データのクリーニング
データに欠損値や異常値がないか確認し、必要に応じて処理します。

### 3. 基本統計量の計算
各数値列の基本統計量(平均、標準偏差、最小値、最大値など)を計算します。

### 4. データの可視化
データの分布や相関関係を可視化するために、ヒストグラム、散布図、箱ひげ図などを作成します。

### 5. 品質に影響を与える要因の特定
品質(良、優、可、不可)に影響を与える要因を特定するために、各要因と品質の関係を分析します。

### 6. モデルの構築
品質を予測するためのモデルを構築します。例えば、ロジスティック回帰や決定木などを使用します。

### 7. 結果の解釈と提言
分析結果を解釈し、品質向上のための提言を行います。

それでは、ステップごとに詳細に進めていきます。

### 1. データの概要を把握
データの全体的な構造を確認します。

```python
import pandas as pd

# データの読み込み
data = pd.read_csv('data.csv')

# データの概要を表示
print(data.info())
print(data.head())
```

### 2. データのクリーニング
欠損値や異常値がないか確認します。

```python
# 欠損値の確認
print(data.isnull().sum())

# 異常値の確認(数値列の基本統計量を表示)
print(data.describe())
```

必要に応じて欠損値の補完や異常値の処理を行います。

### 3. 基本統計量の計算
各数値列の基本統計量を計算します。

```python
# 基本統計量の計算
print(data.describe())
```

### 4. データの可視化
データの分布や相関関係を可視化します。

```python
import matplotlib.pyplot as plt
import seaborn as sns

# ヒストグラムの作成
data.hist(bins=30, figsize=(20, 15))
plt.show()

# 散布図行列の作成
sns.pairplot(data)
plt.show()
```

### 5. 品質に影響を与える要因の特定
品質(良、優、可、不可)に影響を与える要因を特定します。

```python
# 品質と各要因の関係を分析
sns.boxplot(x='品質', y='平均温度(℃)', data=data)
plt.show()

sns.boxplot(x='品質', y='平均湿度(%)', data=data)
plt.show()

# 相関行列の作成
corr_matrix = data.corr()
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm')
plt.show()
```

### 6. モデルの構築
品質を予測するためのモデルを構築します。

```python
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report

# 特徴量とターゲットの分割
X = data[['平均温度(℃)', '平均湿度(%)', '積算温度(℃時)', '収穫までの日数']]
y = data['品質']

# データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# ロジスティック回帰モデルの構築
model = LogisticRegression()
model.fit(X_train, y_train)

# 予測と評価
y_pred = model.predict(X_test)
print(accuracy_score(y_test, y_pred))
print(classification_report(y_test, y_pred))
```

### 7. 結果の解釈と提言
分析結果を解釈し、品質向上のための提言を行います。

```markdown
# 結果の解釈

- 平均温度、平均湿度、積算温度、収穫までの日数が品質に影響を与えていることが分かりました。
- ロジスティック回帰モデルにより、品質の予測精度は約XX%でした。

# 提言

- 平均温度をXX℃に保つことで、品質が向上する可能性があります。
- 平均湿度をXX%に保つことで、品質が向上する可能性があります。
- 収穫までの日数をXX日に調整することで、品質が向上する可能性があります。
```

以上がデータ分析のステップです。具体的なデータを用いた分析の実行は、Pythonのコードを実際に実行する必要があります。


Python を用いた分析手順を解説してくれていますね。

デフォルトでは、ざっくりしたプロンプトで指示しているのですが、具体的な指示を与えれば、より詳細な分析もしてくれると思います。


本ライブラリに関する今後の更新は cultivationdata.net で行います。 note に引っ越しました。

2024年5月20日月曜日

GAS で Gemini API を使った LINE bot を作る

-----
・追記(2024-6-24)
cultivationdata.net で、GPT、Gemini、Claude を搭載した LINE bot の作り方を公開しました。 → note での公開に変更しました(2024-8-17)

・追記(2024-6-29)
Claude の LINE bot を作りました → 「GAS で Claude API を使った LINE bot を作る
-----

先日、GPT-4o を使った LINE bot を作りましたが、今回は Google の生成 AI である Gemini の API を使った LINE bot を GAS(Google Apps Script)で作ってみました。

とりあえず、継続的なやりとりや temperature の指定、画像処理もできるようにしました。

LINE Messaging API チャンネルの作成とアクセストークンの取得。及び、Gemini の API キーの取得は済んでる想定です。

それぞれ、GAS の「プロジェクトの設定」よりスクリプト プロパティとして追加・保存しておきます。プロパティ名は「Line_key」と「Gemini_key」としています。

また、スプレッドシート(サンプル)を用意して、「chat」シートと「temperature」シートを作っておきます。(作り方は「GAS で ChatGPT とやりとりできる簡単な LINE bot の作り方」を参照)


★ 機能一覧
  • 文頭に「続き)」を付けることで会話を継続。(例:「続き)もっと詳細に教えて」)
  • 文頭に「画像)」で画像の処理。(例:「画像)何ですか?」 → 画像を送信)
  • 文末に「(厳密」「(創造」で temperature を指定。通常 0.5、厳密 0、創造 1。(例:「3行で小噺を作って(創造」)


コードは以下の通りです。

const spreadsheet = SpreadsheetApp.openById('スプレッドシート ID');

const chatSheet = spreadsheet.getSheetByName('chat');
let chatLastRow = chatSheet.getLastRow();
let chatRange = chatSheet.getRange(2, 1, chatLastRow, 2);

const tempSheet = spreadsheet.getSheetByName('temperature');
const tempRange = tempSheet.getRange(1, 1);

function doPost(e) {
  try {
    const event = JSON.parse(e.postData.contents).events[0];
    let prompt = [];
    let replyMessage = '';

    switch(event.message.type) {
      case 'text': // リクエストがテキストの場合
        let messageContent = event.message.text;

        if (messageContent.startsWith('画像)')) {
          // 画像処理の場合
          messageContent = messageContent.replace('画像)', '');
          geminiRequestText(messageContent); // リクエスト(テキスト)を成型・保存
          sendLineMessage(event.replyToken, '画像を送信してください'); 
        } else {
          // 画像処理以外の場合
          prompt = geminiRequestText(messageContent);
          replyMessage = getGeminiReply(prompt); // Gemini で回答
          setAiMessage('model', replyMessage); // AI の返答をスプレッドシートに'role': 'model'で格納
          sendLineMessage(event.replyToken, replyMessage);
        }
        break;
      case 'image': // リクエストが画像の場合
        prompt = geminiRequestImage(event.message.id);
        replyMessage = getGeminiReply(prompt); // Gemini で回答
        sendLineMessage(event.replyToken, replyMessage);
        break;
      default:
        // リクエストがサポート外のデータ形式の場合
        sendLineMessage(event.replyToken, 'サポート外のデータ形式です');
    }
  } catch {
    // エラー発生時
    sendLineMessage(event.replyToken, '不明なエラーが発生しました');
  }
}

// リクエストデータ(テキスト)を生成する関数
function geminiRequestText(messageContent) {
  // リクエスト内容の処理
  if (messageContent.startsWith('続き)')) {
    // 文頭に「続き)」で会話を継続
    messageContent = messageContent.replace('続き)', '');
    chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([['user', messageContent]]);
  } else {
    // temperature を取得してスプレッドシートにセット
    // 文末に「(創造」「(厳密」で指定
    if (messageContent.endsWith('(創造')) {
      messageContent = messageContent.replace('(創造', ''); // 1
      tempRange.setValue(1);
    } else if (messageContent.endsWith('(厳密')) {
      messageContent = messageContent.replace('(厳密', ''); // 0
      tempRange.setValue(0);
    } else {
      tempRange.setValue(0.5);
    }

    chatRange.clear(); // 以前のチャット内容を削除

    // リクエスト内容をスプレッドシートに格納
    chatSheet.getRange(2, 1, 1, 2).setValues([['user', messageContent]]);
    chatRange = chatSheet.getRange(2, 1, 1, 2);
  }

  // リクエストデータを生成
  let contents = [];
  let values = chatRange.getValues();
  for(let i = 0; i < values.length; i++) {
      contents.push({'role': values[i][0], 'parts': {'text': values[i][1]}});
  }

  return contents;
}

// Gemini でリクエストデータ(画像)を生成する関数
function geminiRequestImage(id) {
  // 画像を取得
  const response = UrlFetchApp.fetch('https://api-data.line.me/v2/bot/message/' + id + '/content',{
    'headers': {
      'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('Line_key')}`,
    },
    'method': 'get'
  });
  const imageBlob = response.getBlob().getAs('image/jpeg');

  // 画像を Base64 にエンコード
  const base64Image = Utilities.base64Encode(imageBlob.getBytes());

  // リクエストデータを生成
  let contents = [{"parts": [{"text": chatSheet.getRange(2, 2).getValue()}, {"inlineData": {"mimeType": "image/jpeg", "data": base64Image}}]}];

  return contents;
}

// Gemini から回答を得る関数
function getGeminiReply(prompt) {
  let url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${PropertiesService.getScriptProperties().getProperty('Gemini_key')}`;

  const payload = {
    'contents': prompt,
    'generationConfig': {
      'maxOutputTokens': 1024, // 生成する文章の最大トークン数
      'temperature': tempRange.getValue() // 生成された文章のランダムさを制御するパラメータ。値が高いほど、よりランダムな文章が生成される
    }
  };

  const options = {
    'payload': JSON.stringify(payload),
    'method' : 'POST',
    'muteHttpExceptions': true,
    'contentType':'application/json'
  };

  try {
    const response = JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
    return response.candidates[0].content.parts[0].text; // Gemini の回答
  } catch {
    // エラー発生時
    return 'エラー: Gemini の回答が得られませんでした';
  }
}

// AI の返答をスプレッドシートに格納する関数
function setAiMessage(role, replyMessage) {
  chatLastRow = chatSheet.getLastRow();
  chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([[role, replyMessage]]);
}

// LINE メッセージを送信する関数
function sendLineMessage(replyToken, replyMessage) {
  const linePayload = {
    'method': 'post',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('Line_key')}`,
    },
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': replyMessage,
      }],
    }),
  };

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', linePayload);
}
※ 「gemini-1.5-flash-latest」モデルを使ったコードに変更しました(2024-6-1)

1行目の「スプレッドシート ID」は書き換えてください。

コード作成後にウェブアプリとして公開、URL を LINE Messaging API チャンネルで Webhook として設定して完成です。


使い方は、GPT のものと同じです。(ただ、システム指示はできません)



***

今回 Gemini をはじめて触りました。GPT だけでなく、こちらもしっかり追っていければと思います。

LINE bot に関しては、GPT とGemini を切り替えて使えるようにしておくと便利そうな気がします。