🔰ミラサポplus APIで中小企業事例データを収集してみる！

無料で使える政府系APIを使って、中小企業の事例データを収集するシステムを作ってみました。（後々分析したいと思います）
PythonとDocker、PostgreSQLを使ったアプリです。

# はじめに
プログラミングを学んでいると、「実際のデータを使って何か作りたい！」と思うことありませんか？でも、APIの利用に高額な料金がかかったり、難しい申請手続きが必要だったりすると挫折してしまいがち...

そこで今回は、中小企業庁が提供する「**ミラサポplus事例ナビAPI**」を使ってみましょう。

このAPIは無料で使えて、会員登録や利用申請も不要というお手軽さが魅力です！

:::note info
（補足）以前は内閣府の「RESAS API」という地域経済分析システムのAPIもありましたが、
こちらは現在サービス終了しています。あちらは利用申請が必要でした。
:::

# 今回作るもの
- ミラサポplus事例ナビAPIから全事例データを取得
- PostgreSQLデータベースに保存
- 将来的にデータ分析に活用する基盤を整備

これができれば、中小企業の取り組み事例のデータを様々な角度から分析できるようになります。

具体的に何ができるかというと、「地域別の成功事例」「業種別の課題解決パターン」「企業規模による取り組みの違い」などを調査できるようになります。

## 1. 事前準備とAPIの確認
ミラサポplus事例ナビAPIはSwaggerで生成されたドキュメントが公開されています。
https://app.swaggerhub.com/apis/MIRASAPOPLUS/jirei-api/1.0

試しに簡単なリクエストを投げてみましょう。「デジタル化」「生産性向上」というキーワードで事例を検索してみます。
```bash
curl -X GET "https://mirasapo-plus.go.jp/jirei-api/case_studies?keywords=デジタル化,生産性向上&limit=5" \
  -H "Accept: application/json"
```

<details>
<summary>レスポンスサンプル</summary>

```bash
% curl -X GET "https://mirasapo-plus.go.jp/jirei-api/case_studies?keywords=デジタル化,生産性向上&limit=5" \
  -H "Accept: application/json"
{"items":[{"id":"12","number":"0000-0012","title":"設備も、採用活動も、変えてみた。","summary":"業務量の増加にともなう時間外労働の増加を解消することが課題だった。\n最新型の機械を導入したことで生産性が向上し、時間外労働の増加の問題は解決した。\n採用活動を見直したことで、新卒採用者の早期離職が減った。","year":"2017","location":{"id":"15","name":"新潟県","region_name":"関東・甲信越地方"},"used_support_refs":[],"specific_measure_categories":[],"partners":[],"background":"## 背景、きっかけ\n### 時間外労働の増加が課題\n製品の受注増加、多品種・少ロット注文の段取りや加工時間の増加によって、時間外労働が増加していた。\n### 新卒採用者の早期離職\n採用時のミスマッチを原因とする新卒社員の早期離職を減らしたかった。","challenges":"## 挑戦したこと\n### IoTシステムを導入\nIoTシステムを導入したことによって、加工実績データを一元化。無駄な時間の削減に取り組んだ。\n上記システムの導入時に補助金申請を行うなど、現実的な設備投資を実施した。\n### 採用活動の見直し\n早期に採用活動を開始し、学内企業説明会へ参加。インターンシップや会社見学も随時受け入れた。\nJOB広場やリクナビに登録し、企業PRに取り組んだ。","results":"## 取組の結果\n### 生産性が向上\n最新型機械の導入によって、一部の段取りにかかる時間や加工時間が短縮し、生産性の向上につながった。\n### 早期離職が減少\n採用活動の見直しによって、入社後のミスマッチが減少。早期離職の抑制につながった。","prospect":"ー","images":[{"title":"エヌ・エス・エス株式会社","image":{"id":"86","name":"sp_main_012.png","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/addafa04-94e0-4173-9bee-1160666226ca","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/addafa04-94e0-4173-9bee-1160666226ca_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:47Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:47Z"}}},{"title":"概要","image":{"id":"169","name":"pc_card_012_s.png","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/b6f9fd8f-4890-4fa3-80dd-02f9f1983091","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/b6f9fd8f-4890-4fa3-80dd-02f9f1983091_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:43Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:43Z"}}}],"movies":[],"related_sites":[],"organization":{"name":"エヌ・エス・エス株式会社","industry":"製造業（機械）","description":"製造業","permanent_employees_count":121,"address":"15","capital":10000000,"established_date":"1883-12-31T15:00:00Z"},"contact":{"address":"新潟県小千谷市桜町2379-1"},"industry_categories":[{"id":"5","name":"製造業","number":"E","sub_categories":[]}],"service_categories":[{"id":"5","name":"サービス","sub_categories":[{"id":"20","name":"情報提供・相談"}]}],"purpose_categories":[{"id":"5","name":"人材"}],"themes":["人手不足"],"language":"日本語","catalog":{"id":"1","name":"人手不足対応事例集100（2017年）","publisher":"中小企業庁","image":{"id":"154","name":"pc_title.png","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:47Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:47Z"}},"published":true,"update_info":{"created_at":"2020-02-26T06:58:12Z","created_by":{},"last_modified_at":"2020-06-16T05:59:27Z","last_modified_by":{}}},"published_date":"2020-02-27","update_info":{"created_by":{},"created_at":"2020-02-28T01:44:31Z","last_modified_by":{},"last_modified_at":"2020-05-13T00:54:04Z"},"published":true,"deleted":false,"mng_groups":["7DCA6F1E-0570-4A28-AD10-C0946E6A09AF"]},{"id":"27","number":"0000-0027","title":"残業が減っても生産性2倍。そのヒミツは？","summary":"工事現場と本社とが遠いことが多く、従業員には長時間の移動という負担がかかっていた。\nITツールの導入によって現場と本社との情報共有をはかったことで、生産性がアップ。\n従業員の残業時間を9割削減できた。\n資格取得の支援などによって従業員のスキルが向上。受注も増加した。","year":"2018","location":{"id":"14","name":"神奈川県","region_name":"関東・甲信越地方"},"used_support_refs":[],"specific_measure_categories":[],"partners":[],"background":"## 背景、きっかけ\n### 残業の削減と生産性アップが課題\n建設工事は、施工現場が遠方になることが多い。打ち合わせのために本社に戻るときも移動に時間がとられ、交通費もかさんでいた。\n業務内容が多様化し、従業員の育成が必要だった。しかし、休日出勤や残業が多く、育成に取り組む余裕はなかった。\n生産性の向上が経営課題だった。","challenges":"## 挑戦したこと\n### ITツールを活用して、テレワークも\nITツールの導入にあたって初期費用や運営費用をおさえるために、無料のソフトウェアを採用。\n打ち合わせ、工事進捗管理、資材管理、仕様書作成などのシステムを構築した。\n本社や現場だけでなく、従業員が自宅でシステムを利用できる仕組みを整備した。\n### 情報セキュリティ対策\n情報漏えいを防ぐために、個人所有のパソコンなどを業務では使用不可としている。その代わりに業務用の携帯端末を支給し、通信費を会社側が負担している。\n情報セキュリティ対策を万全にするためには、大きな投資も必要。5年程度の中期的なプランを作成し、改善を少しずつ進めている。\n### 導入時に苦労した点\nシステムを1から構築する苦労もあったが、新しいシステムを従業員に使ってもらう部分にも苦労があった。\nIT担当者が全従業員一人ひとりに、システムの使い方や導入理由をていねいに説明することで、理解を得た。","results":"## 取組の結果\n### システム導入の効果\n生産性が2倍にアップし、残業時間の9割を削減できた。\n従業員のワークライフバランスが改善した。\n### 資格取得者が増加\n残業が減ったことで、プライベートの時間が増えた。空いた時間で資格を取る従業員も現れた。\n資格取得者が増えたことで、公共工事における「経営事項審査」の評価点が高まり、受注が増加した。\n### 横浜市から表彰\n優良工事施工会社部門で、横浜市から表彰された。","prospect":"ー","images":[{"title":"向洋電機土木株式会社","image":{"id":"98","name":"sp_main_029.png","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/6fe085bf-1813-4ac4-93c4-8f93435b6566","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/6fe085bf-1813-4ac4-93c4-8f93435b6566_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:48Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:48Z"}}},{"title":"取組事例","image":{"id":"23","name":"company_029_1.jpg","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/370d067b-230a-41c8-9521-aa5aa0e61b69","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/370d067b-230a-41c8-9521-aa5aa0e61b69_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:40Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:40Z"}}},{"title":"取組事例","image":{"id":"24","name":"company_029_2.jpg","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/2f8ee30e-b5a9-4281-b8c6-94e53c641995","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/2f8ee30e-b5a9-4281-b8c6-94e53c641995_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:40Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:40Z"}}}],"movies":[],"related_sites":[],"organization":{"name":"向洋電機土木株式会社","industry":"建設業","description":"建設業（電気設備の設計・施工）","permanent_employees_count":27,"address":"14","capital":37000000,"established_date":"1964-12-31T15:00:00Z"},"contact":{"address":"神奈川県横浜市南区井土ヶ谷下町16-6"},"industry_categories":[{"id":"4","name":"建設業","number":"D","sub_categories":[]}],"service_categories":[{"id":"5","name":"サービス","sub_categories":[{"id":"20","name":"情報提供・相談"}]}],"purpose_categories":[{"id":"5","name":"人材"}],"themes":["人手不足"],"language":"日本語","catalog":{"id":"1","name":"人手不足対応事例集100（2017年）","publisher":"中小企業庁","image":{"id":"154","name":"pc_title.png","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:47Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:47Z"}},"published":true,"update_info":{"created_at":"2020-02-26T06:58:12Z","created_by":{},"last_modified_at":"2020-06-16T05:59:27Z","last_modified_by":{}}},"published_date":"2020-02-27","update_info":{"created_by":{},"created_at":"2020-02-28T01:44:32Z","last_modified_by":{},"last_modified_at":"2020-02-28T01:44:32Z"},"published":true,"deleted":false,"mng_groups":["7DCA6F1E-0570-4A28-AD10-C0946E6A09AF"]},{"id":"44","number":"0000-0044","title":"女性が働きやすい職場も、製造しました。","summary":"人手の確保と生産性の向上が課題。\n女性が働きやすい職場環境を整えて、女性の採用を拡大。\n一部の業務に機器を導入。工程をシンプルにでき、生産効率が高まった。","year":"2018","location":{"id":"1","name":"北海道","region_name":"北海道地方"},"used_support_refs":[],"specific_measure_categories":[],"partners":[],"background":"## 背景、きっかけ\n### 人手の確保と生産性の向上が課題\n製造業に付きまとう「3K（きつい、危険、汚い）」といったイメージのせいで人材の確保が難しかった。\n同社社長は「女性にできない分野はない」との信念を持つため、女性採用の拡大と女性が活躍できる職場づくりにも取り組むことに。\n女性が魅力を感じる募集になるよう、工夫する必要があった。\n生産効率をアップするために、工程の見直しが必要だった。","challenges":"## 挑戦したこと\n### 女性の採用を強化するために職場環境を整備\n女性用トイレの増設や更衣室の改修などで、職場環境を整備した。\n工場内を清潔に保つことで、女性が働きやすい職場づくりを心がけた。\n### 採用広報を工夫\n「製造業は工学系しか募集していない」といったイメージを払拭する採用広報を心がけた。\n### 生産工程の見直し\n定型化が可能な業務は、機器を導入することで工程をシンプルに。非熟練技術者（＝非正規雇用者）でも業務にあたれるように改善した。","results":"## 取組の結果\n### 女性社員の増加\n2015年の春、2名の女性技術職を採用。\n「女性が多い会社」と認知され始めたことで、コンスタントに女性を採用できている。\n### 生産性が向上\n定型化できる業務をエンジニア以外の社員に振り分けることで、生産効率がアップ。","prospect":"ー","images":[{"title":"シンセメック株式会社","image":{"id":"113","name":"sp_main_046.png","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/1c8cde80-03a5-400f-a71e-b240bcf8d3d2","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/1c8cde80-03a5-400f-a71e-b240bcf8d3d2_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:48Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:48Z"}}},{"title":"取組事例","image":{"id":"38","name":"company_046_2.jpg","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/a9df8dd8-8a41-4af1-bd01-04ff659dd5e0","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/a9df8dd8-8a41-4af1-bd01-04ff659dd5e0_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:41Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:41Z"}}},{"title":"取組事例","image":{"id":"39","name":"company_046_3.jpg","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/51479863-5f15-4fde-b7c6-4968f95c0649","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/51479863-5f15-4fde-b7c6-4968f95c0649_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:41Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:41Z"}}}],"movies":[],"related_sites":[],"organization":{"name":"シンセメック株式会社","industry":"製造業（機械）","description":"製造業（オーダーメイドの生産用設備の製作）","permanent_employees_count":51,"address":"1","capital":30000000,"established_date":"1949-12-31T15:00:00Z"},"contact":{"address":"北海道石狩市新港西2丁目788-7"},"industry_categories":[{"id":"5","name":"製造業","number":"E","sub_categories":[]}],"service_categories":[{"id":"5","name":"サービス","sub_categories":[{"id":"20","name":"情報提供・相談"}]}],"purpose_categories":[{"id":"5","name":"人材"}],"themes":["人手不足"],"language":"日本語","catalog":{"id":"1","name":"人手不足対応事例集100（2017年）","publisher":"中小企業庁","image":{"id":"154","name":"pc_title.png","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:47Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:47Z"}},"published":true,"update_info":{"created_at":"2020-02-26T06:58:12Z","created_by":{},"last_modified_at":"2020-06-16T05:59:27Z","last_modified_by":{}}},"published_date":"2020-02-27","update_info":{"created_by":{},"created_at":"2020-02-28T01:44:34Z","last_modified_by":{},"last_modified_at":"2020-02-28T01:44:34Z"},"published":true,"deleted":false,"mng_groups":["7DCA6F1E-0570-4A28-AD10-C0946E6A09AF"]},{"id":"57","number":"0000-0057","title":"女性社員の笑顔を招く方法。","summary":"仕事と家庭を両立できる職場環境をつくることで女性社員の定着をはかった。\n情報管理システムを新調することで生産性を高め、時間外労働を減らした。","year":"2018","location":{"id":"23","name":"愛知県","region_name":"東海・北陸地方"},"used_support_refs":[],"specific_measure_categories":[],"partners":[],"background":"## 背景、きっかけ\n### 仕事と家庭の両立と、生産性アップが課題\n社員の60％が女性だが、仕事と家庭の両立に苦労している女性社員が多かった。\n女性社員が長く働ける職場環境を整えれば、人材が定着し、人材採用の際のアピールポイントにできると考えた。","challenges":"## 挑戦したこと\n### 残業時間の削減\n緊急時でない限り、管理職を含め全社員が18時30分までに退社する決まりとした。\n20年ほど使用していた情報管理システムを見直したことで、生産性がアップ。残業時間も減った。\n### 両立支援を推進するために、各種規程を改定\n女性が安心して働ける環境を整えるために、有給休暇や子どもの看護休暇を半日単位で取得できるようにした。\n育児休業は1年6カ月まで取得できる。\n1年未満の育休期間で復職した場合、はじめの半年間は「短時間勤務でも産休前の給与を保証する制度」を新設した。\n### 管理職の意識改革\n多様な人材が活躍できる風土を醸成するために、管理職向けに人事面談を実施。キャリアプランを示して社員を導いていく重要性を説きながら、意識改革をはかった。","results":"## 取組の結果\n### 女性社員の定着率アップ\n毎年正社員として女性を採用できている。\n優れた技術を持つ女性社員が産休や育休を経て職場復帰するようになった。定着が促進するとともに技術力の蓄積も可能になった。\n### 生産性の向上\n新たな情報管理システムの導入によって業務が効率化。残業時間が大幅に減った。","prospect":"ー","images":[{"title":"株式会社中外陶園","image":{"id":"259","name":"sp_main_059.png","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/dbb322d9-e685-470b-a5a8-39255477d433","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/dbb322d9-e685-470b-a5a8-39255477d433_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:49Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:49Z"}}},{"title":"概要","image":{"id":"212","name":"pc_card_059_s.png","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/848f8194-b7e3-4e4a-807c-7c6407f27bde","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/848f8194-b7e3-4e4a-807c-7c6407f27bde_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:45Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:45Z"}}}],"movies":[],"related_sites":[],"organization":{"name":"株式会社中外陶園","industry":"製造業（その他）","description":"陶磁器製置物製造業","permanent_employees_count":44,"address":"23","capital":15000000,"established_date":"1951-12-31T15:00:00Z"},"contact":{"address":"愛知県瀬戸市薬師町50番地"},"industry_categories":[{"id":"5","name":"製造業","number":"E","sub_categories":[]}],"service_categories":[{"id":"5","name":"サービス","sub_categories":[{"id":"20","name":"情報提供・相談"}]}],"purpose_categories":[{"id":"5","name":"人材"}],"themes":["人手不足"],"language":"日本語","catalog":{"id":"1","name":"人手不足対応事例集100（2017年）","publisher":"中小企業庁","image":{"id":"154","name":"pc_title.png","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:47Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:47Z"}},"published":true,"update_info":{"created_at":"2020-02-26T06:58:12Z","created_by":{},"last_modified_at":"2020-06-16T05:59:27Z","last_modified_by":{}}},"published_date":"2020-02-27","update_info":{"created_by":{},"created_at":"2020-02-28T01:44:35Z","last_modified_by":{},"last_modified_at":"2020-02-28T01:44:35Z"},"published":true,"deleted":false,"mng_groups":["7DCA6F1E-0570-4A28-AD10-C0946E6A09AF"]},{"id":"60","number":"0000-0060","title":"取引顧客3倍増！ヒミツは若手の育成。","summary":"創業メンバーの高齢化、および若手人材の不足から、技能継承に危機感を持っていた。\n人材の採用と定着が課題。\n一人ひとりの事情を考慮して労働条件を柔軟に変えたほか、ITツールの導入によって業務改善を行い、人材の安定的な確保と生産性アップを実現。","year":"2018","location":{"id":"13","name":"東京都","region_name":"関東・甲信越地方"},"used_support_refs":[],"specific_measure_categories":[],"partners":[],"background":"## 背景、きっかけ\n### 若手の採用に苦戦\n創業メンバーの高齢化にともない、技能継承に危機感を持った。\nハローワークでの求人や、広告やネット媒体などでの求人に本格的に取り組んだ。しかし、業界的に「3K（きつい、汚い、危険）」のイメージが定着しているせいか、若手が集まらず苦戦した。\n採用した人材もすぐに離職してしまい、定着しなかった。","challenges":"## 挑戦したこと\n### 個人の事情に配慮\n定年退職後も雇用を延長。実質的に「定年の上限」をなくした。\n未経験者の積極採用をスタート。子育て中の女性も採用した。\n育児中の社員は、子どもの成長に合わせて出勤時間を変更できるようにした。また学校行事へ参加できるように、柔軟に対応している。\n### 若手への技能継承と、人材採用への取り組み\n「人材育成への協力」を定年退職後の雇用延長の条件とすることで、若手への技能継承を推進している。\n若手人材の多能工化に向けて、資格取得を手厚く支援。\nホームページをリニューアルして、働く人を中心として紹介。応募者に同社の仕事のやりがいを訴求した。\n### 職場環境の整備と、ITツールの導入\n食堂、男女ロッカー、女性専用トイレ、現場の空調などを新設およびリニューアル。職場環境を整えた。\n女性社員の発案から、納品書などを管理するためのITツールを取り入れ、業務効率の改善をはかった。","results":"## 取組の結果\n### 人材確保と定着に成功し、生産性もアップ\n同社の社会的認知度が高まるに従って、安定的な人材確保が可能になった。\n既存社員の定着率もアップし、特定の年代に偏らない幅広い年齢構成となったことで、技能継承に取り組む流れも生まれた。\nサービス品質が向上し、取引顧客数は2003年と比べて約3倍に増加。新分野関連の売上も21％に増加した。\n積極的にITツールを導入したことによって業務効率が大幅に向上。生産性も高まった。","prospect":"ー","images":[{"title":"電化皮膜工業株式会社","image":{"id":"123","name":"sp_main_062.png","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/ed863449-e22e-4ae1-b49e-b206e9ff31eb","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/ed863449-e22e-4ae1-b49e-b206e9ff31eb_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:49Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:49Z"}}},{"title":"取組事例","image":{"id":"47","name":"company_062_1.jpg","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/83ca23d8-72ec-4524-a77e-39b2a4084efa","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/83ca23d8-72ec-4524-a77e-39b2a4084efa_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:41Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:41Z"}}},{"title":"取組事例","image":{"id":"48","name":"company_062_2.jpg","mng_group":"9AF01523-F987-4D95-BB10-A50EE990B21E","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/6bd6da89-0153-4f2a-8a1a-7fa4d093521b","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/6bd6da89-0153-4f2a-8a1a-7fa4d093521b_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:41Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:41Z"}}}],"movies":[],"related_sites":[],"organization":{"name":"電化皮膜工業株式会社","industry":"製造業（その他）","description":"めっき・表面処理業","permanent_employees_count":39,"address":"13","capital":11000000,"established_date":"1946-12-31T15:00:00Z"},"contact":{"address":"東京都大田区矢口3-5-10"},"industry_categories":[{"id":"5","name":"製造業","number":"E","sub_categories":[]}],"service_categories":[{"id":"5","name":"サービス","sub_categories":[{"id":"20","name":"情報提供・相談"}]}],"purpose_categories":[{"id":"5","name":"人材"}],"themes":["人手不足"],"language":"日本語","catalog":{"id":"1","name":"人手不足対応事例集100（2017年）","publisher":"中小企業庁","image":{"id":"154","name":"pc_title.png","url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e","thumbnail_url":"https://mirasapo-prd-public.s3.ap-northeast-1.amazonaws.com/jirei/images/5c0cf33a-f66b-455b-8995-56698684dd2e_thumb","update_info":{"created_by":{},"created_at":"2020-02-27T04:21:47Z","last_modified_by":{},"last_modified_at":"2020-02-27T04:21:47Z"}},"published":true,"update_info":{"created_at":"2020-02-26T06:58:12Z","created_by":{},"last_modified_at":"2020-06-16T05:59:27Z","last_modified_by":{}}},"published_date":"2020-02-27","update_info":{"created_by":{},"created_at":"2020-02-28T01:44:35Z","last_modified_by":{},"last_modified_at":"2020-02-28T01:44:35Z"},"published":true,"deleted":false,"mng_groups":["7DCA6F1E-0570-4A28-AD10-C0946E6A09AF"]}],"total":651}
```
</details>

このようなコマンドを実行すると、中小企業の事例データがJSON形式で返ってきます。この中には企業名、事業内容、取り組みの背景や成果などが含まれています。
これらのデータを体系的に収集・保存して分析できるようにするのが今回の目標です。

## 2. 環境構築
DockerとDocker Composeを使って、Pythonアプリケーションと PostgreSQLデータベースの環境を構築します。
### プロジェクト構造
まずはプロジェクトのディレクトリを作成します。
```bash
mkdir mirasapo-data-collector
cd mirasapo-data-collector
```

次に、以下のようなファイル構成を作成していきます。
```bash
mirasapo-data-collector/
├── app/
│   ├── __init__.py
│   ├── api_client.py     # API通信クライアント
│   ├── db_manager.py     # データベース操作
│   ├── main.py           # メインスクリプト
│   ├── models.py         # データモデル定義
│   └── analysis.py       # データ分析（後回し）
├── compose.yaml          # Docker Compose設定
├── Dockerfile            # Dockerイメージ定義
├── requirements.txt      # Pythonパッケージ依存関係
└── .env                  # 環境変数
```

### Docker環境の設定
まずは`compose.yaml`と`Dockerfile`を作成します。
#### compose.yamlファイル作成
```bash
touch compose.yaml
```
以下をcompose.yamlにコピペ
```yaml:docker-compose.yaml
services:
  db:
    image: postgres:17-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - .env
    environment:
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_DB=${DB_NAME}
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
    command: ["postgres", "-c", "listen_addresses=*"]

  app:
    build: .
    volumes:
      - ./app:/app
    env_file:
      - .env
    environment:
      - DB_HOST=db
      - DB_PORT=5432
    depends_on:
      db:
        condition: service_healthy
    command: python -m main

volumes:
  postgres_data:
```

#### Dockerfileの作成
```dockerfile:dockerfile
FROM python:3.13-slim

RUN apt-get update && apt-get install -y \
    postgresql-client \
    libpq-dev \
    gcc \
    python3-dev

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY ./app /app

ENV PYTHONPATH=/

CMD ["python", "-m", "app.main"]
```

#### 2. 環境ファイル作成
以下のコマンドを実行して`.env`ファイルを作成します。
```bash
echo "DB_NAME=mirasapo_db
DB_USER=mirasapo_user
DB_PASSWORD=mirasapo_password
DB_HOST=db
DB_PORT=5432
API_BASE_URL=https://mirasapo-plus.go.jp/jirei-api" > .env
```

補足：上記のコマンドを実行すると、以下のファイルと定義が作成されます。
```shell:.env
DB_NAME=mirasapo_db
DB_USER=mirasapo_user
DB_PASSWORD=mirasapo_password
DB_HOST=db
DB_PORT=5432
API_BASE_URL=https://mirasapo-plus.go.jp/jirei-api
```

基本的な設定ファイルが揃いました！
PostgreSQLデータベースとPythonアプリケーションの実行環境が整いました。

#### requirements.txtの作成
```shell
touch requirements.txt
```

requirements.txtに以下をコピペ
```plaintext:requirements.txt
requests==2.31.0
peewee==3.17.9
psycopg2-binary==2.9.10
python-dotenv==1.0.1
tqdm==4.66.2
ruff==0.11.3
pytest==7.4.4
pandas==2.2.3
matplotlib==3.10.1
```

### 3. データモデルの定義
次は、APIから取得するデータの構造を定義します。
PeeweeというシンプルなORMを使ってデータベーステーブルの定義をしましょう。

モデル定義用のファイルを作成します。
```bash
touch app/models.py
```

```python:app/models.py
from peewee import (
    Model, PostgresqlDatabase, CharField, TextField, IntegerField,
    DateField, ForeignKeyField, BooleanField, FloatField, DateTimeField
)
import os
from dotenv import load_dotenv

load_dotenv()

db = PostgresqlDatabase(
    os.getenv('DB_NAME'),
    user=os.getenv('DB_USER'),
    password=os.getenv('DB_PASSWORD'),
    host=os.getenv('DB_HOST'),
    port=os.getenv('DB_PORT')
)


class BaseModel(Model):
    class Meta:
        database = db


class Prefecture(BaseModel):
    id = CharField(primary_key=True)
    name = CharField()
    region_name = CharField()


class Organization(BaseModel):
    id = CharField(primary_key=True)
    name = CharField()
    industry = CharField(null=True)
    description = TextField(null=True)
    permanent_employees_count = IntegerField(null=True)
    sales_amount = IntegerField(null=True)
    address = CharField(null=True)
    capital = IntegerField(null=True)
    established_date = DateField(null=True)


class IndustryCategory(BaseModel):
    id = CharField(primary_key=True)
    name = CharField()
    number = CharField(null=True)


class PurposeCategory(BaseModel):
    id = CharField(primary_key=True)
    name = CharField()


class ServiceCategory(BaseModel):
    id = CharField(primary_key=True)
    name = CharField()


class CaseStudy(BaseModel):
    id = CharField(primary_key=True)
    number = CharField(null=True)
    title = CharField()
    summary = TextField(null=True)
    year = CharField(null=True)
    location = ForeignKeyField(Prefecture, backref='case_studies', null=True)
    organization = ForeignKeyField(Organization, backref='case_studies', null=True)
    background = TextField(null=True)
    challenges = TextField(null=True)
    results = TextField(null=True)
    prospect = TextField(null=True)
    published_date = DateField(null=True)
    published = BooleanField(default=True)
    deleted = BooleanField(default=False)


class CaseStudyIndustryCategory(BaseModel):
    case_study = ForeignKeyField(CaseStudy, backref='industry_categories_rel')
    industry_category = ForeignKeyField(IndustryCategory, backref='case_studies_rel')


class CaseStudyPurposeCategory(BaseModel):
    case_study = ForeignKeyField(CaseStudy, backref='purpose_categories_rel')
    purpose_category = ForeignKeyField(PurposeCategory, backref='case_studies_rel')


class CaseStudyServiceCategory(BaseModel):
    case_study = ForeignKeyField(CaseStudy, backref='service_categories_rel')
    service_category = ForeignKeyField(ServiceCategory, backref='case_studies_rel')


def initialize_db():
    db.connect()
    db.create_tables([
        Prefecture,
        Organization,
        IndustryCategory,
        PurposeCategory,
        ServiceCategory,
        CaseStudy,
        CaseStudyIndustryCategory,
        CaseStudyPurposeCategory,
        CaseStudyServiceCategory
    ])
    print("Database tables created successfully")
```
APIから返ってくるJSONデータの構造に合わせてモデルを設計しています。

主な特徴は
- 都道府県、組織、産業分類などの基本情報を別テーブルに
- 事例本体のテーブル（CaseStudy）
- 多対多の関係を表現する中間テーブル（CaseStudyIndustryCategory など）

### 4. APIクライアント実装
次に、APIからデータを取得するクライアントを実装します。
```bash
touch app/api_client.py
```

```python:app/api_alient.py
import requests
import time
from typing import List, Dict, Any, Optional
import os
from dotenv import load_dotenv
from tqdm import tqdm

load_dotenv()

API_BASE_URL = os.getenv("API_BASE_URL", "https://mirasapo-plus.go.jp/jirei-api")


class MirasapoApiClient:
    def __init__(self, base_url: str = API_BASE_URL):
        self.base_url = base_url
        self.headers = {"Accept": "application/json"}

    def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict:
        """APIリクエストを実行し、レスポンスを返す"""
        url = f"{self.base_url}/{endpoint}"
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        return response.json()

    def get_case_study(self, case_id: str) -> Dict:
        """特定の事例IDの詳細情報を取得"""
        return self._make_request(f"case_studies/{case_id}")

    def search_case_studies(
        self, 
        keywords: Optional[List[str]] = None, 
        limit: int = 100, 
        offset: int = 0,
        **kwargs
    ) -> Dict:
        """事例を検索"""
        params = {"limit": limit, "offset": offset}
        
        if keywords:
            params["keywords"] = ",".join(keywords)
            
        # 追加のパラメータを追加
        params.update(kwargs)
        
        return self._make_request("case_studies", params)

    def get_all_case_studies(self) -> List[Dict]:
        """全ての事例を取得（ページネーション処理）"""
        all_cases = []
        offset = 0
        limit = 100
        total = None
        
        with tqdm(desc="Fetching case studies", unit="cases") as pbar:
            while True:
                response = self.search_case_studies(limit=limit, offset=offset)
                
                if total is None:
                    total = response.get("total", 0)
                    pbar.total = min(total, 1000)  # API制限で1000件以上は1001固定になるため
                
                cases = response.get("items", [])
                if not cases:
                    break
                    
                all_cases.extend(cases)
                pbar.update(len(cases))
                
                if len(all_cases) >= total or len(cases) < limit:
                    break
                    
                offset += limit
                time.sleep(0.5)  # API負荷軽減のため少し待機
        
        return all_cases

    def get_prefectures(self) -> List[Dict]:
        """都道府県一覧を取得"""
        response = self._make_request("prefectures")
        return response

    def get_industry_categories(self) -> List[Dict]:
        """標準産業分類一覧を取得"""
        response = self._make_request("categories/industries")
        return response

    def get_purpose_categories(self) -> List[Dict]:
        """お困りごと分類一覧を取得"""
        response = self._make_request("categories/purposes")
        return response

    def get_service_categories(self) -> List[Dict]:
        """行政サービス分類一覧を取得"""
        response = self._make_request("categories/services")
        return response
```
APIクライアントでは以下のポイントに注目しています
- シンプルな共通のリクエストメソッド
- ページネーション処理（一度に全件取得できないため）
- プログレスバー表示で進捗がわかるように
- API負荷軽減のための待機時間

### 5. データベース操作の実装
次に、APIから取得したデータをデータベースに保存するマネージャークラスを実装します。
``` bash
touch app/db_manager.py
```

```python:app/db_manager.py
from typing import Dict, List, Any
from models import (
    db, Prefecture, Organization, IndustryCategory, PurposeCategory,
    ServiceCategory, CaseStudy, CaseStudyIndustryCategory,
    CaseStudyPurposeCategory, CaseStudyServiceCategory
)


class DBManager:
    @staticmethod
    def save_prefectures(prefectures: List[Dict[str, Any]]) -> None:
        """都道府県データを保存"""
        with db.atomic():
            for prefecture in prefectures:
                Prefecture.insert(
                    id=prefecture["id"],
                    name=prefecture["name"],
                    region_name=prefecture["region_name"]
                ).on_conflict(
                    conflict_target=[Prefecture.id],
                    update={
                        Prefecture.name: prefecture["name"],
                        Prefecture.region_name: prefecture["region_name"]
                    }
                ).execute()

    @staticmethod
    def save_industry_categories(categories: List[Dict[str, Any]]) -> None:
        """標準産業分類データを保存"""
        with db.atomic():
            for category in categories:
                IndustryCategory.insert(
                    id=category["id"],
                    name=category["name"],
                    number=category.get("number")
                ).on_conflict(
                    conflict_target=[IndustryCategory.id],
                    update={
                        IndustryCategory.name: category["name"],
                        IndustryCategory.number: category.get("number")
                    }
                ).execute()

    @staticmethod
    def save_purpose_categories(categories: List[Dict[str, Any]]) -> None:
        """お困りごと分類データを保存"""
        with db.atomic():
            for category in categories:
                PurposeCategory.insert(
                    id=category["id"],
                    name=category["name"]
                ).on_conflict(
                    conflict_target=[PurposeCategory.id],
                    update={PurposeCategory.name: category["name"]}
                ).execute()

    @staticmethod
    def save_service_categories(categories: List[Dict[str, Any]]) -> None:
        """行政サービス分類データを保存"""
        with db.atomic():
            for category in categories:
                ServiceCategory.insert(
                    id=category["id"],
                    name=category["name"]
                ).on_conflict(
                    conflict_target=[ServiceCategory.id],
                    update={ServiceCategory.name: category["name"]}
                ).execute()

    @staticmethod
    def save_case_studies(case_studies: List[Dict[str, Any]]) -> None:
        """事例データを保存"""
        for case in case_studies:
            with db.atomic():
                # Organizationを保存
                if "organization" in case and case["organization"]:
                    org_data = case["organization"]
                    org_id = case["id"]  # 事例IDを組織IDとしても使用
                    
                    Organization.insert(
                        id=org_id,
                        name=org_data.get("name", "不明"),
                        industry=org_data.get("industry"),
                        description=org_data.get("description"),
                        permanent_employees_count=org_data.get("permanent_employees_count"),
                        sales_amount=org_data.get("sales_amount"),
                        address=org_data.get("address"),
                        capital=org_data.get("capital"),
                        established_date=org_data.get("established_date")
                    ).on_conflict(
                        conflict_target=[Organization.id],
                        update={
                            Organization.name: org_data.get("name", "不明"),
                            Organization.industry: org_data.get("industry"),
                            Organization.description: org_data.get("description"),
                            Organization.permanent_employees_count: org_data.get("permanent_employees_count"),
                            Organization.sales_amount: org_data.get("sales_amount"),
                            Organization.address: org_data.get("address"),
                            Organization.capital: org_data.get("capital"),
                            Organization.established_date: org_data.get("established_date")
                        }
                    ).execute()
                
                # CaseStudyを保存
                CaseStudy.insert(
                    id=case["id"],
                    number=case.get("number"),
                    title=case["title"],
                    summary=case.get("summary"),
                    year=case.get("year"),
                    location=case.get("location", {}).get("id") if case.get("location") else None,
                    organization=case["id"] if "organization" in case and case["organization"] else None,
                    background=case.get("background"),
                    challenges=case.get("challenges"),
                    results=case.get("results"),
                    prospect=case.get("prospect"),
                    published_date=case.get("published_date"),
                    published=case.get("published", True),
                    deleted=case.get("deleted", False)
                ).on_conflict(
                    conflict_target=[CaseStudy.id],
                    update={
                        CaseStudy.number: case.get("number"),
                        CaseStudy.title: case["title"],
                        CaseStudy.summary: case.get("summary"),
                        CaseStudy.year: case.get("year"),
                        CaseStudy.location: case.get("location", {}).get("id") if case.get("location") else None,
                        CaseStudy.organization: case["id"] if "organization" in case and case["organization"] else None,
                        CaseStudy.background: case.get("background"),
                        CaseStudy.challenges: case.get("challenges"),
                        CaseStudy.results: case.get("results"),
                        CaseStudy.prospect: case.get("prospect"),
                        CaseStudy.published_date: case.get("published_date"),
                        CaseStudy.published: case.get("published", True),
                        CaseStudy.deleted: case.get("deleted", False)
                    }
                ).execute()
                
                # 既存の関連付けを削除
                CaseStudyIndustryCategory.delete().where(
                    CaseStudyIndustryCategory.case_study == case["id"]
                ).execute()
                
                CaseStudyPurposeCategory.delete().where(
                    CaseStudyPurposeCategory.case_study == case["id"]
                ).execute()
                
                CaseStudyServiceCategory.delete().where(
                    CaseStudyServiceCategory.case_study == case["id"]
                ).execute()
                
                # 産業分類の関連付け
                if "industry_categories" in case and case["industry_categories"]:
                    for industry in case["industry_categories"]:
                        CaseStudyIndustryCategory.create(
                            case_study=case["id"],
                            industry_category=industry["id"]
                        )
                
                # お困りごと分類の関連付け
                if "purpose_categories" in case and case["purpose_categories"]:
                    for purpose in case["purpose_categories"]:
                        CaseStudyPurposeCategory.create(
                            case_study=case["id"],
                            purpose_category=purpose["id"]
                        )
                
                # 行政サービス分類の関連付け
                if "service_categories" in case and case["service_categories"]:
                    for service in case["service_categories"]:
                        CaseStudyServiceCategory.create(
                            case_study=case["id"],
                            service_category=service["id"]
                        )
```
ここでのポイントは
- トランザクション処理（db.atomic()）
- UPSERT処理（on_conflict）で既存データの更新
- 関連テーブルの更新

### 6. メインプログラムの実装
最後に、これまでのコンポーネントを組み合わせてメインプログラムを実装します。
```bash
touch app/main.py
```

```python:app/main.py
import sys
import time
from models import initialize_db
from api_client import MirasapoApiClient
from db_manager import DBManager


def main():
    print("Starting Mirasapo Data Collector")
    
    # データベース初期化
    initialize_db()
    
    # APIクライアント初期化
    api_client = MirasapoApiClient()
    
    try:
        # 基本データの取得と保存
        print("Fetching and saving prefectures...")
        prefectures = api_client.get_prefectures()
        DBManager.save_prefectures(prefectures)
        
        print("Fetching and saving industry categories...")
        industry_categories = api_client.get_industry_categories()
        DBManager.save_industry_categories(industry_categories)
        
        print("Fetching and saving purpose categories...")
        purpose_categories = api_client.get_purpose_categories()
        DBManager.save_purpose_categories(purpose_categories)
        
        print("Fetching and saving service categories...")
        service_categories = api_client.get_service_categories()
        DBManager.save_service_categories(service_categories)
        
        # 全事例の取得と保存
        print("Fetching all case studies...")
        case_studies = api_client.get_all_case_studies()
        print(f"Total case studies fetched: {len(case_studies)}")
        
        print("Saving case studies to database...")
        DBManager.save_case_studies(case_studies)
        
        print("Data collection completed successfully")
    
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1
    
    return 0


if __name__ == "__main__":
    sys.exit(main())
```
メイン処理の流れはシンプルです。
- データベースの初期化
- マスターデータ（都道府県、産業分類など）の取得と保存
- 事例データの取得と保存

### 7. 実行と動作確認
これで基本的な実装は完了しました！Docker Composeを使って起動しましょう。
```bash
docker compose up --build -d
```

アプリケーションが起動して、データ収集が始まります。ログを確認してみましょう
```
docker compose logs -f app
```

データベースに接続して、データが正しく保存されているか確認してみましょう。
```
docker compose exec db psql -U mirasapo_user -d mirasapo_db
```

PostgreSQLのプロンプトが表示されたら、テーブルやデータを確認できます
```sql
-- テーブル一覧の確認
\dt

-- 事例データの確認
SELECT id, title FROM case_study LIMIT 5;

-- 産業別の事例数を集計
SELECT ic.name, COUNT(csic.case_study_id) AS case_count 
FROM case_study_industry_category csic
JOIN industry_category ic ON csic.industry_category_id = ic.id
GROUP BY ic.name
ORDER BY case_count DESC;
```

### 8. データ分析の準備（後回し部分）
データ収集・保存の仕組みができたので、次はデータ分析に進みたいところですが、これは別の機会に詳しく解説します。
今回は簡単な分析関数の例だけ紹介しておきます。
```bash
touch app/analysis.py
```

```python:app/analysis.py
"""
事例データの分析モジュール
後回しとされていますが、基本的な分析関数を用意しています。
"""

import pandas as pd
import matplotlib.pyplot as plt
from models import (
    CaseStudy, Prefecture, Organization, IndustryCategory,
    CaseStudyIndustryCategory, CaseStudyPurposeCategory, CaseStudyServiceCategory
)


def analyze_case_studies_by_region():
    """地域別の事例数を分析"""
    query = (
        CaseStudy
        .select(Prefecture.region_name, Prefecture.name, CaseStudy.id)
        .join(Prefecture, on=(CaseStudy.location == Prefecture.id))
        .where(CaseStudy.published == True)
        .order_by(Prefecture.region_name, Prefecture.name)
    )
    
    results = []
    for case in query:
        results.append({
            'region_name': case.location.region_name,
            'prefecture_name': case.location.name,
            'case_id': case.id
        })
    
    df = pd.DataFrame(results)
    
    # 地方別の集計
    region_counts = df.groupby('region_name').size().sort_values(ascending=False)
    
    # 都道府県別の集計
    prefecture_counts = df.groupby('prefecture_name').size().sort_values(ascending=False)
    
    # 可視化
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 8))
    
    region_counts.plot(kind='bar', ax=ax1)
    ax1.set_title('地方別事例数')
    ax1.set_ylabel('事例数')
    
    prefecture_counts.head(10).plot(kind='bar', ax=ax2)
    ax2.set_title('都道府県別事例数 (上位10)')
    ax2.set_ylabel('事例数')
    
    plt.tight_layout()
    plt.savefig('regional_analysis.png')
    
    return {
        'region_counts': region_counts.to_dict(),
        'prefecture_counts': prefecture_counts.to_dict()
    }


def analyze_case_studies_by_industry():
    """産業別の事例数を分析"""
    query = (
        CaseStudyIndustryCategory
        .select(IndustryCategory.name, CaseStudyIndustryCategory.case_study)
        .join(IndustryCategory)
        .join(CaseStudy)
        .where(CaseStudy.published == True)
    )
    
    results = []
    for rel in query:
        results.append({
            'industry_name': rel.industry_category.name,
            'case_id': rel.case_study.id
        })
    
    df = pd.DataFrame(results)
    
    # 産業別の集計
    industry_counts = df.groupby('industry_name').size().sort_values(ascending=False)
    
    # 可視化
    plt.figure(figsize=(12, 8))
    industry_counts.plot(kind='bar')
    plt.title('産業別事例数')
    plt.ylabel('事例数')
    plt.tight_layout()
    plt.savefig('industry_analysis.png')
    
    return {
        'industry_counts': industry_counts.to_dict()
    }


def analyze_case_studies_by_employee_count():
    """従業員規模別の事例数を分析"""
    query = (
        CaseStudy
        .select(CaseStudy.id, Organization.permanent_employees_count)
        .join(Organization)
        .where(
            (CaseStudy.published == True) & 
            (Organization.permanent_employees_count.is_null(False))
        )
    )
    
    results = []
    for case in query:
        results.append({
            'case_id': case.id,
            'employees': case.organization.permanent_employees_count
        })
    
    df = pd.DataFrame(results)
    
    # 従業員規模のカテゴリ化
    bins = [0, 10, 30, 50, 100, 300, float('inf')]
    labels = ['1-9', '10-29', '30-49', '50-99', '100-299', '300+']
    df['employee_category'] = pd.cut(df['employees'], bins=bins, labels=labels)
    
    # 従業員規模別の集計
    employee_counts = df.groupby('employee_category').size()
    
    # 可視化
    plt.figure(figsize=(10, 6))
    employee_counts.plot(kind='bar')
    plt.title('従業員規模別事例数')
    plt.ylabel('事例数')
    plt.tight_layout()
    plt.savefig('employee_analysis.png')
    
    return {
        'employee_counts': employee_counts.to_dict()
    }
```

# まとめ
今回は、無料で使える中小企業庁の「ミラサポplus事例ナビAPI」を使って、中小企業の事例データを収集・保存するシステムを作りました。

やったことは
- 公共APIの活用方法
- Docker + PostgreSQL環境の構築
- Peewee ORMを使ったデータモデリング
- APIクライアントの実装
- データベース操作の基本
- 大量データの効率的な取得・保存方法

このプロジェクトは以下のような拡張が可能です
- より高度なデータ分析の実装
- ダッシュボードの作成（DashやStreamlitなど）
- 定期的なデータ更新の自動化
- 類似事例の検索や推薦機能

皆さんも是非、公共APIを活用した面白いプロジェクトに挑戦してみてください！

# 参考資料
- [Docker Compose V2で変わったdocker-compose.ymlの書き方](https://zenn.dev/miroha/articles/whats-docker-compose-v2)
- [【docker compose】警告`version` is obsoleteの解決法](https://zenn.dev/harpseal/articles/8dc2602ffe3365)

# 備考
その他、開発に必要な設定の追加分です。
### Ruff設定ファイル作成
```bash
touch .ruff.toml
```

```toml:.ruff.toml
# Ruff設定
line-length = 100
target-version = "py311"

[lint]
select = ["E", "F", "W", "I", "N", "B", "A", "C4", "PT", "RUF"]
ignore = []

[format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
docstring-code-format = true
```

### 開発補助ツールの導入
#### 1. Lintツールのインストール
```bash
pip install ruff black mypy
```

#### 2. pre-commitの設定（オプション）
```bash
pip install pre-commit

cat > .pre-commit-config.yaml << EOL
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.3.4
  hooks:
    - id: ruff
      args: [--fix]
    - id: ruff-format

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.5.0
  hooks:
    - id: trailing-whitespace
    - id: end-of-file-fixer
    - id: check-yaml
    - id: check-added-large-files
EOL

pre-commit install
```