#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のヘッダーが必要なのか、国籍、宿泊日などの繰り返しデータはどう扱うのかといった点の記載がありません。
また、宿泊者数などの求め方についての留意事項として説明がありますが、ちょっと分かりにくい説明です。
最終的に登録されると画面上で以下のように表示されます。
複数のAIにGASスクリプト作成を依頼
本来であればAIが理解しやすいように文章を整理してあげることにより精度・品質が高くなりますが、今回は仕様、考慮事項などがいろいろあるため整理するのが面倒です。
そのため、それぞのAIの整理する力も試してみたいという気持ちもあり、雑多な情報をそのまま渡して、GASスクリプトの作成を依頼してみました。
少し長いですが、依頼に使用したプロンプト文を以下に示します。
民泊ポータル等にある情報をそのままコピペし、不足情報を補足しました。
このプロンプト文を私が以前記事でご紹介した複数AIに一括問合せできるサイトを活用してスクリプト作成を依頼します。
以下のとおり、回答が返ってきました。
実行結果の確認
ChatGPT, Gemini, Claudeが生成したスクリプトをGASで実行してみました。
結果は以下のとおりですで、この段階ではClaudeが一番ベターな結果となりました。
ChatGPT:
手直しなしでエラーなく完了、CSVが生成された。
生成された内容には7か所の不具合あり。
Gemini:
エラー終了。届出番号を取得する処理の誤りで実行エラー。
Claude:
手直しなしでエラーなく完了、CSVが生成された。
生成された内容には2か所の不具合あり。
ChatGPTの不具合の内容の内容:
Claude の不具合の内容の内容:
出力結果だけをみてもClaudeの良さが際立っています。
依頼のプロンプト文も整理しない文章をそのまま渡したのでChatGPTが期待通りでなかった部分も納得できる範囲です。
しかし、Claudeはかなり踏み込んで、理解・判断をしてくれています。
ChatGPTでは対応できず、Claudeは対応でき特に優秀だと思った点は以下のとおりです。
宿泊対象期間: プロンプトで仕様を明示してなかったが、宿泊名簿データから対象期間を判断して設定してくれている。ChatGPTでは、本日を基準に2か月の設定としており、元データとの関連は無視。
宿泊者数・延べ人数: プロンプト文で列挙した項目が "(ダブルクオーテーション)で間でいたものを理解して、項目をマッチングする際に考慮してくれている。 ChatGPTでは、ダブルクオーテーションがついていたためマッチせず、数が取得できなかった。
項目ヘッダー行: これも明示的に指示がなかったが、Claudeは考慮して追加してくれている。
出力CSVファイル名: Claudeは、報告期間ごと別のファイル名となるようにファイル名に日付を付けてくれていた。
全体的に、Claudeが今回の件については一歩二歩先を行っている感じで、非常に優秀でした。
第2ラウンド目の勝負
エラー終了で脱落したGeminiを除き、ChatGPTとClaudeでプロンプト文を修正して再勝負させました。
プロンプトの修正は、以下の文言を追加しました。
早速、結果ですが、今回も 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);
}
最後に
この記事はお気に入りいただけましたでしょうか?
内容お役にたちましたらうれしく思います。
また、サポートなど応援いただけましたら幸いです。
記事の内容を有効に活用できたなど、記事を気に入っていただけたようでしたらサポートしていただけますと嬉しいです。 また、こんなことを知りたい、あんなことができないかなど記事にしたいことがございましたら、サポートの有無にかかわらずお知らせくださいませ。