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して牧歌的農家になってしまったときに老後資金を適切なポートフォリオで運用していくことができるように「ポートフォリオ最適化ツール」を作ってみました(唐突)。よわよわサーバーで運用中です。


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




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


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


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

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 の作り方を公開しています。

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 で行います。

2024年5月20日月曜日

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

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

・追記(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 を切り替えて使えるようにしておくと便利そうな気がします。

2024年5月17日金曜日

GAS で GPT-4o API を使った画像処理ができる LINE bot を作る

-----
・追記(2024-5-21)
Gemini の LINE bot も作ってみました → 「GAS で Gemini API を使った LINE bot を作る

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

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

先日発表された GPT-4o が、いろいろすごいみたいですね。

すでに API の利用ができるそうなので、以前、GAS(Google Apps Script) で作った LINE bot(「GAS で ChatGPT とやりとりできる簡単な LINE bot の作り方」)に画像処理機能を追加してみました。

スプレッドシート(サンプル)の準備等、基本的な作り方は、以前作った LINE bot と同じです。


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


GAS のコードを、以下のように書き換えます。

const spreadsheet = SpreadsheetApp.openById('スプレッドシート 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];

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

        if (messageContent.startsWith('画像)')) {
          // 画像処理の場合
          messageContent = messageContent.replace('画像)', '');
          getRequestMessage(messageContent); // リクエスト(テキスト)を成型・保存
          sendLineMessage(event.replyToken, '画像を送信してください');  // LINE で応答
        } else {
          // 画像処理以外の場合
          const requestData = getRequestMessage(messageContent); // リクエスト(テキスト)を成型・保存
          const replyMessage = generateOpenAiReply(requestData); // GPT で回答
          setAssistantMessage(replyMessage); // スプレッドシートに'role': 'assistant'で格納
          sendLineMessage(event.replyToken, replyMessage); // LINE で応答
        }
        break;
      case 'image': // リクエストが画像の場合
        const requestData = getRequestImage(event.message.id); // リクエスト(画像)を成型・保存
        const replyMessage = generateOpenAiReply(requestData); // GPT で回答
        setAssistantMessage(replyMessage); // スプレッドシートに'role': 'assistant'で格納
        sendLineMessage(event.replyToken, replyMessage); // LINE で応答
        break;
      default:
        // リクエストがサポート外のデータ形式の場合
        sendLineMessage(event.replyToken, 'サポート外のデータ形式です'); // LINE で応答
    }
  } catch {
    // エラー発生時
    sendLineMessage(event.replyToken, 'エラーが発生しました'); // LINE で応答
  }
}

// リクエストデータ(テキスト)を生成する関数
function getRequestMessage(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('(創造', ''); // 1
      tempRange.setValue(1);
    } else if (messageContent.endsWith('(厳密')) {
      messageContent = messageContent.replace('(厳密', ''); // 0
      tempRange.setValue(0);
    } else {
      tempRange.setValue(0.5);
    }
    // スプレッドシートにあらかじめ登録した system ロールの読み込み
    for (let i = 0; i < systemWatchwordArry.length; i++) {
      // 文頭に「〇〇!」でsystem ロールを指定
      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 requestData = [];
  if (systemContent) {
    requestData.push({'role': 'system', 'content': systemContent});
  }
  let values = chatRange.getValues();
  for(let i = 0; i < values.length; i++) {
    requestData.push({'role': values[i][0], 'content': values[i][1]});
  }

  return requestData;
}

// リクエストデータ(画像)を生成する関数
function getRequestImage(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 requestData = [];
  let values = chatSheet.getRange(2, 1, chatLastRow - 1, 2).getValues();
  for(let i = 0; i < values.length; i++) {
    if(values[i][0] == 'user') {
      requestData.push({'role': 'user', 'content': [{'type': 'text', 'text': values[i][1]},{'type': 'image_url', 'image_url': {'url': `data:image/png;base64,${base64Image}`}}]});
    } else {
      requestData.push({'role': values[i][0], 'content': values[i][1]});
    }
  }

  return requestData
}

// GPT から回答を得る関数
function generateOpenAiReply(requestMessage) {
  const openAiHeaders = {
    'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('OpenAI_key')}`,
    'Content-type': 'application/json',
    'X-Slack-No-Retry': 1
  };

  const openAiPayload = {
    'model': 'gpt-4o', // 使用するモデル
    'max_tokens': 1024, // 生成する文章の最大トークン数
    'temperature': tempRange.getValue(), // 生成された文章のランダムさを制御するパラメータ。値が高いほど、よりランダムな文章が生成される
    'messages': requestMessage // 生成する文章の元になるプロンプト
  };

  const openAiParams = {
    'method': 'POST',
    'headers': openAiHeaders,
    'payload': JSON.stringify(openAiPayload),
  };

  const response = UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', openAiParams);
  const resData = JSON.parse(response.getContentText());

  return resData.choices[0].message.content; // GPT の回答
}

// アシスタントの返答をスプレッドシートに格納する関数
function setAssistantMessage(replyMessage) {
  chatLastRow = chatSheet.getLastRow();
  chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([['assistant', 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);
}
※ コードを一部修正しました。

画像処理に関連しない部分の大きな変更点ですが、GPT-4o はより人間っぽく振る舞うことができるようになったとのことなので、英語をかませるのをやめて、日本語で直接 API とやりとりするようにしました。


改めて、画像処理モードの使い方です。


文頭に「画像)」と付けてプロンプトを書くと画像処理のモードになります。

「画像を送信してください」と返答があるので、続けて画像を送ると処理が行われます。

画像処理モードでも、これまで通り、「画像)日本語!写真の料理のレシピを教えて(創造」のような形式で、system ロールへの指示や、temperature パラメータの指定もできます(詳細は以前の投稿を参照してください)。

ただ、「続き)」を使った継続的なやりとりはできません(あまり意味ないかな?と思って)。



面倒な書き起こしもらくらく(ちょっと間違ってるけど)。


***

ということで、GPT-4o を使って画像処理ができる LINE bot を作ってみました。

GAS で作れるとサーバーのことを考えなくていいので、とても楽です。

この簡便さと能力であれば、アイデア次第で、個人でも農業に役立つものがあれこれ作れそうですね。

2024年4月25日木曜日

【定期】家計の見直し(2024年4月)

立て続けになりますが、家計の見直しです。


経営と家計が一体化した家族経営では、家計の最適化が心強い生存基盤になります。翻って、家族や自身にとって本当に価値あるものが何なのか、確認する良い機会になります。


・mineo で光回線代替

昨年、ひかり電話で使っていた固定電話を解約したのですが、今回、とうとう光回線自体を解約しました。

うちの場合、安定的な回線速度を要求するような使い方もしないので、戸建ての高価な光回線の維持しておく必要があるものだろうかと常々考えていました。

そんなわけで、3年ごとの契約更新のタイミングですっぱり解約してみました。

(ちなみに、auひかりですが、電話一本で簡単に即日解約できました)

代替回線は、mineo のマイピタ20GBとパケット放題 Plusで、1,925円です。これで、ざっくり3,400円の固定費削減になりました。

新しい mineo 回線は SIM カードを Rakuten WiFi Pocket に入れて運用することにしました。Rakuten WiFi Pocket は、以前キャンペーン時に1円で入手したもので、使いどころがなくてしまってありました。

基本は、節約ON で1.5Mbps の速度にしていますが、それほど不便なく使えています。突発的な何がなければ3日間で10GB の制限にも引っかからなそうです。

もちろん、複数人で同時に使う際には若干の遅れを感じますが、コストパフォーマンスを考えれば待てる範囲です。また、いざという時は20GB 分の高速通信も可能です。


自宅の電波が良ければ様々な選択肢があり得るんですけどね。ドコモ回線ダメ、au 回も線アップロードが貧弱という状況で、ソフトバンク回線のみまともに使えます。mineo には長くサービスを続けてほしいです。


・今年度も「do!浜通り」でガソリンを入れる

今年度も「do!浜通り」によるポイント還元をやるようです。

1回目は、4月25日(木)〜5月8日(水)で、いわき市は、10%還元の2,500ポイント上限(決済方法あたり)です。

今回から、決済方法が増えて楽天ペイも使えるので、ありがたいですね。いつものごとくガソリンを入れます。


・年金改革

年金制度の改革が以下のように検討されているそうです。

① 厚生年金の対象拡大
② 基礎年金の納付期間 40年 → 45年
③ 基礎年金の給付抑制を早期停止
④ 在職老齢年金の見直し
⑤ 保険料の基準額の上限上げ

このままでは制度が保たない、というのはわかる気がします。先日、自身(30代後半)の基礎年金のトータル納付額を確認したところ、すでに両親の納付額の1.5倍になっていました。

結局のところ、個人としては、使える制度を使いながらうまくやっていくしかないですね。


・昨今の雑感

塵も積もれば何とやらで、固定費削減の効果は抜群ですね。

今年は施設の大きな修繕等を予定しています。小規模農家が生き残っていくためには、全方位にアンテナを張って、地道にコツコツやっていくしかありません。

頑張っていきましょう!


花海棠
ちょっと前の写真ですが「花海棠」