見出し画像

#AI自作 No.6: 民泊の実績報告CSVを自動生成 - 複数AIを比較し驚きの結果に!

こんにちは。
私の民泊ではゲストに事前に宿泊者名簿に記入をしていただいており、Googleフォームを活用しています。
実際に利用しているフォームやそのフォームの事前登録URL作成などについて記事を投稿しました。

今回この記事では、民泊制度で必要となる都道府県への実績報告に関して、宿泊者名簿データから自動生成をしてみたので、その内容について共有します。
前回に続き、AIの力を活用して自動化に取り組んでみたという記事です。
複数のAIを比較してみたのですが、予想外の結果になりました。
参考になると思いますので、ぜひお読みください。


背景

民泊では、隔月で都道府県への民泊事業の実績報告が義務づけられています。
私の場合にはまだそれほど数がないので手計算でやってしまうのが早く、省力化をする意味がある段階ではありません。
しかし、今後の展開、および、来年の繁忙期での稼働向上を意識して、今の内からここについても自動化しておきたいと考えたのがきっかけです。
合わせて、AIの力を試してみるいい機会だと考えました。
今回作成するCSVファイルは、その作成仕様が意外と複雑なこと、また、Google Apps Scriptというものを利用するのでGoogle Geminiを含め、複数のAIエンジンで試してみることにしました。
Googleスプレッドシート、GASを利用することになるので、Geminiが一番いいものを作成してくれるのかなと想像しますが、結果はどうでしょうか?

民泊の実績報告データCSVについて

民泊事業者は、都道府県知事等へ実績を定期報告する必要があります。
この定期報告は、民泊ポータルの民泊制度運営システムで実績を登録します。
やり方は、このシステムの画面から入力するやり方と作成したCSVをアップロードするやり方です。
CSVデータがあれば、アップロードするだけなので簡単です。
しかし、作成するCSVの仕様・フォーマットが分かりずらいです。
その定期実績報告データの仕様・内容についてはこちらに記載されています。

ここの記載には、CSVのヘッダーが必要なのか、国籍、宿泊日などの繰り返しデータはどう扱うのかといった点の記載がありません。
また、宿泊者数などの求め方についての留意事項として説明がありますが、ちょっと分かりにくい説明です。

https://www.mlit.go.jp/common/001247661.pdf より

最終的に登録されると画面上で以下のように表示されます。

民泊制度運営システム(事業者)

複数のAIにGASスクリプト作成を依頼

本来であればAIが理解しやすいように文章を整理してあげることにより精度・品質が高くなりますが、今回は仕様、考慮事項などがいろいろあるため整理するのが面倒です。
そのため、それぞのAIの整理する力も試してみたいという気持ちもあり、雑多な情報をそのまま渡して、GASスクリプトの作成を依頼してみました。
少し長いですが、依頼に使用したプロンプト文を以下に示します。
民泊ポータル等にある情報をそのままコピペし、不足情報を補足しました。

以下の条件、仕様のCSVデータを生成するGoogle Apps Scriptコードを書いてください。

GASコードは、元データの含まれるGoolgeスプレッドシートに含まれて実行されます。
最終的に作成されるCSVデータはスプレッドシートが保存されているフォルダに保存して。

スプレッドシートの1行目の列名が"届出番号="で始まるものがあるが、"="以降を届出番号として利用し、CSVに出力する。
処理した行は、列名が"届出番号="で始まる列の値を "更新済み"として更新してください。
また、この列が空白となっているものだけとを処理対象としてください。

入力データについて:
GASが紐づくスプレッドシートのシート名"フォームの回答 1"のデータを利用
この、シートの一行目には列名が記載されており、CSVデータ作成で利用するデータの列名は以下のとおり。
以下の列名は一部のため、入力のシートの値を参照する場合には、1行目の項目名が以下の名前で始まるものを参照すること。

チェックイン日 / Check-in date
チェックアウト日 / Check-out date
"宿泊者の人数は? How many guests is your group?"
"宿泊者氏名 / Guest name (1) 例: 山田太郎 / e.g. Andrew April"
国籍/Nationality (1)

国籍として選択できる値は以下のとおり。

日本/Japan
Korea
China
Taiwan
Hong Kong S.A.R
Thailand
Singapore
Malaysia
Indonesia
Philippines
Viet Nam
India
United Kingdom
France
Germany
Italy
Russia
Spain
USA
Canada
Australia
Others (Please enter your nationality in the next text box

作成するCSVデータについて:
CSVデータは、宿泊実績定期報告データ と呼ばれるもので以下のとおりです。
定期報告データ(CSV ファイル)の形式を以下に示します。

項目 内容
届出番号 届出番号(例 ”M011234567”)
報告対象期間 報告対象期間(例 “2023_4-5”)
宿泊日数 宿泊日数(例 “3”)
宿泊者数 宿泊者数(例 “4”)
延べ人数 延べ人数(例 “10”)
国籍 宿泊者の国籍人数(以下国籍毎の当該報告対象期間の宿泊者数) 日本、韓国、台湾、香港、中国、タイ、シンガポール、マレーシア、インドネシア、フィリピン、ベトナム、インド、英国、ドイツ、フランス、イタリア、スペイン、ロシア、米国、カナダ、オーストラリア、その他
宿泊日 当該宿泊日(例 “2023/4/6, 2023/4/7, 2023/4/8”)

CSVデータの補足説明:
「届出住宅に人を宿泊させた日数」
(例)6月 20 日 17 時にチェックインし、24 日の 10 時にチェックアウトした場合は4日
「宿泊者数」
・・・届出住宅に宿泊した実際の人数を該当期間で足し合わせた数
※同一人物が同じ届出住宅において連続して宿泊した場合は、1人とカウント
※同一人物が同じ届出住宅において連続ではなく、複数に分けて宿泊した場合はそれぞれ
1人とカウント
(例)3人が2泊3日で利用(3人)、5人が6泊7日で利用(5人)した場合は合計8人
(例)同一人物が同じ届出住宅を6月に2泊利用、7月に3泊利用した場合は合計2人
「延べ宿泊者数」
・・・各日の全宿泊者数を該当期間で足し合わせた数
(例)3人が2泊3日で利用(6人)、5人が6泊7日で利用(30 人)した場合は合計36人
「国籍別の宿泊者数内訳」
・・・日本国内に住所を有しない宿泊者の国籍の内訳

CSVデータの具体例による説明:
宿泊日の例: 6月1日-3日に日本人2名、6月5日-9日に韓国人3名、7月29日-30日に韓国人2名の場合、6月1、2、5、6、7、8日、7月29日となる
国籍別の宿泊者数内訳の例:6月1日-3日に日本人2名、6月5日-9日に韓国人3名、7月29日-30日に韓国人2名の場合、日本人に2、韓国人に5を入力
延べ人数の例:6月1日-3日に日本人2名、6月5日-9日に韓国人3名、7月29日-30日に韓国人2名の場合、2泊×2名+4泊×3名+1泊×2名=18となる。

作成されるCSVの実データの例:
届出番号,報告期間,宿泊日数,宿泊者数,延べ人数,日本,韓国,台湾,香港,中国,タイ,シンガポール,マレーシア,インドネシア,フィリピン,ベトナム,インド,英国,ドイツ,フランス,イタリア,スペイン,ロシア,米国,カナダ,オーストラリア,その他,宿泊日,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
M011234567,2023_8-9,4,6,8,4,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2024/08/08,2024/08/15,2024/08/16,2024/08/17,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

上記の例のとおり、","の数も合わせること。

このプロンプト文を私が以前記事でご紹介した複数AIに一括問合せできるサイトを活用してスクリプト作成を依頼します。

以下のとおり、回答が返ってきました。

実行結果の確認

ChatGPT, Gemini, Claudeが生成したスクリプトをGASで実行してみました。
結果は以下のとおりですで、この段階ではClaudeが一番ベターな結果となりました。

ChatGPT: 
手直しなしでエラーなく完了、CSVが生成された。
生成された内容には7か所の不具合あり。

Gemini:
エラー終了。届出番号を取得する処理の誤りで実行エラー。

Claude:
手直しなしでエラーなく完了、CSVが生成された。
生成された内容には2か所の不具合あり。

ChatGPTの不具合の内容の内容:

1.  ヘッダー行なし
2.  報告期間が宿泊日と合ってない
3.  2か月間データ集約されていない
4.5.  宿泊者、延べ人数 NaN
6. 国籍別人数なし
7. 宿泊日にチェックアウト日が含まれている

Claude の不具合の内容の内容:

1. 2か月間データ集約されていない
2. 国籍別人数 なし

出力結果だけをみてもClaudeの良さが際立っています
依頼のプロンプト文も整理しない文章をそのまま渡したのでChatGPTが期待通りでなかった部分も納得できる範囲です。
しかし、Claudeはかなり踏み込んで、理解・判断をしてくれています。
ChatGPTでは対応できず、Claudeは対応でき特に優秀だと思った点は以下のとおりです。

  • 宿泊対象期間: プロンプトで仕様を明示してなかったが、宿泊名簿データから対象期間を判断して設定してくれている。ChatGPTでは、本日を基準に2か月の設定としており、元データとの関連は無視。

  • 宿泊者数・延べ人数: プロンプト文で列挙した項目が "(ダブルクオーテーション)で間でいたものを理解して、項目をマッチングする際に考慮してくれている。 ChatGPTでは、ダブルクオーテーションがついていたためマッチせず、数が取得できなかった。

  • 項目ヘッダー行:  これも明示的に指示がなかったが、Claudeは考慮して追加してくれている。

  • 出力CSVファイル名: Claudeは、報告期間ごと別のファイル名となるようにファイル名に日付を付けてくれていた。

全体的に、Claudeが今回の件については一歩二歩先を行っている感じで、非常に優秀でした。

第2ラウンド目の勝負

エラー終了で脱落したGeminiを除き、ChatGPTとClaudeでプロンプト文を修正して再勝負させました。
プロンプトの修正は、以下の文言を追加しました。

項目名について: 
なお、宿泊者氏名、国籍には項目に"(1)"と添え字が含まれているが(1)~(8)までの繰り返し項目となっている。

作成するCSVデータについて:
なお、報告対象期間は2か月となっており、スプレッドシートの宿泊日をもとに2か月分のデータを集計し1レコードとして作成する。
また、項目名を含むヘッダー行を最初のレコードとして出力する。
宿泊日数にはチェックアウト日は含まれないので注意。

プロンプト文の修正内容

早速、結果ですが、今回も Claudeが圧勝でした。
以下、出力結果です。

Claudeは国籍別の集計で Othersのカテゴリが数値がでていない点を除けばすべて対応されています
ChatGPT版は、この段階でも2か月の報告期間での対応ができておらず、延べ人数の誤り、国籍別人数もすべてその他に分類され正しくありません。

ということで今回のGASスクリプト作成に関しては、ChatGPT v4、 Gemini v1.5、Claude Sonnet (v3.5)の比較では、Claudeが圧勝でした。
今もっともChatGPTが利用されている印象ですが、そのため一番いい結果をだすかもと思っていましたが、これまであまりなじみのなかったClaudeがこれほどいい結果を出しくれることに驚きました。

今後は、ケースバイケースでベストなものを選ぶ必要があるようですね。

なお、テストしたClaudeのスクリプトはこちらになります。
仕様について不明瞭な、かつ、整理されいない説明からよくここまで実装してくれたものと感心します。

function generateCSVReport() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName("フォームの回答 1");
  const data = sheet.getDataRange().getValues();
  const headers = data[0];

  const deliveryNumberIndex = headers.findIndex(header => header.startsWith("届出番号="));
  const checkinIndex = headers.findIndex(header => header.startsWith("チェックイン日"));
  const checkoutIndex = headers.findIndex(header => header.startsWith("チェックアウト日"));
  const guestsCountIndex = headers.findIndex(header => header.startsWith("宿泊者の人数は?"));

  const guestNameIndices = [];
  const nationalityIndices = [];
  for (let i = 1; i <= 8; i++) {
    guestNameIndices.push(headers.findIndex(header => header.startsWith(`宿泊者氏名 / Guest name (${i})`)));
    nationalityIndices.push(headers.findIndex(header => header.startsWith(`国籍/Nationality (${i})`)));
  }

  const reportData = {};
  let deliveryNumber = ""; // ループの外で定義

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (row[deliveryNumberIndex] != "") continue; // 修正: 空でない場合に処理を続行

    deliveryNumber = headers[deliveryNumberIndex].split("=")[1]; 
    const checkinDate = new Date(row[checkinIndex]);
    const checkoutDate = new Date(row[checkoutIndex]);
    const guestsCount = parseInt(row[guestsCountIndex]);

    // 偶数の月から始まる2か月間ごとに集計
    const reportPeriod = `${checkinDate.getFullYear()}_${Math.floor((checkinDate.getMonth() + 1) / 2) * 2}-${Math.floor((checkinDate.getMonth() + 1) / 2) * 2 + 1}`;

    if (!reportData[reportPeriod]) {
      reportData[reportPeriod] = {
        stayDays: 0,
        guestsCount: 0,
        totalNights: 0,
        nationalities: new Array(22).fill(0),
        stayDates: new Set()
      };
    }

    const stayDays = Math.ceil((checkoutDate - checkinDate) / (1000 * 60 * 60 * 24));
    reportData[reportPeriod].stayDays += stayDays;
    reportData[reportPeriod].guestsCount += guestsCount;
    reportData[reportPeriod].totalNights += stayDays * guestsCount;

    for (let j = 0; j < stayDays; j++) {
      const stayDate = new Date(checkinDate);
      stayDate.setDate(stayDate.getDate() + j);
      const localDate = new Date(stayDate.getTime() - stayDate.getTimezoneOffset() * 60000);
      reportData[reportPeriod].stayDates.add(localDate.toISOString().split('T')[0]);
    }

    for (let j = 0; j < guestsCount; j++) {
      const nationality = row[nationalityIndices[j]];
      const nationalityIndex = getNationalityIndex(nationality);
      if (nationalityIndex !== -1) {
        reportData[reportPeriod].nationalities[nationalityIndex]++;
      }
    }

    sheet.getRange(i + 1, deliveryNumberIndex + 1).setValue("更新済み");
  }

  let csvContent = "届出番号,報告期間,宿泊日数,宿泊者数,延べ人数,日本,韓国,台湾,香港,中国,タイ,シンガポール,マレーシア,インドネシア,フィリピン,ベトナム,インド,英国,ドイツ,フランス,イタリア,スペイン,ロシア,米国,カナダ,オーストラリア,その他," + "宿泊日,".repeat(60).slice(0, -1) + "\n";

  for (const [period, data] of Object.entries(reportData)) {
    csvContent += `${deliveryNumber},${period},${data.stayDays},${data.guestsCount},${data.totalNights},${data.nationalities.join(",")},${Array.from(data.stayDates).join(",")},${",".repeat(60 - data.stayDates.size)}\n`;
  }

  const folder = DriveApp.getFileById(ss.getId()).getParents().next();
  const fileName = `宿泊実績定期報告データ_Claude-2_${new Date().toISOString().split('T')[0]}.csv`;
  folder.createFile(fileName, csvContent, MimeType.CSV);
}

function getNationalityIndex(nationality) {
  const nationalities = [
    "日本/Japan", "Korea", "Taiwan", "Hong Kong S.A.R", "China", "Thailand", "Singapore", "Malaysia", "Indonesia", "Philippines",
    "Viet Nam", "India", "United Kingdom", "Germany", "France", "Italy", "Spain", "Russia", "USA", "Canada", "Australia", "Others"
  ];
  return nationalities.indexOf(nationality);
}

最後に

この記事はお気に入りいただけましたでしょうか?
内容お役にたちましたらうれしく思います。
また、サポートなど応援いただけましたら幸いです。


記事の内容を有効に活用できたなど、記事を気に入っていただけたようでしたらサポートしていただけますと嬉しいです。 また、こんなことを知りたい、あんなことができないかなど記事にしたいことがございましたら、サポートの有無にかかわらずお知らせくださいませ。