こんにちは。Anagraftの伊藤です。
データ分析を学ぼうと決意した人が最初にぶつかる壁は、統計学でも機械学習でもありません。Pythonの環境構築です。インストールしたはずなのに動かない、ネットの記事のとおりに打ったのにエラーが出る、そもそもどのツールを入れればいいのか分からない。この最初の数時間でつまずいて、学習そのものを諦めてしまう方が少なくないように思います。
本コラムは、その最初の壁を越えるところから始めて、Pythonの基礎文法、データ分析の中核ライブラリであるNumPyとPandas、可視化、実務を支えるファイル操作やスクリプト化、そしてGitとDockerによるコードと環境の管理までを、一続きの道筋で解説します。手順の丸暗記ではなく「なぜそうするのか」という仕組みの理解を軸に据えました。仕組みが分かっていれば、環境が変わっても、エラーが出ても、自力で前へ進めるからです。
本コラムは、当社のデータサイエンスシリーズの中で最も入門者向けの1冊です。ここで身につけた土台の上に、統計学編・回帰編・機械学習編・時系列編・ベイズ編・因果推論編の各コラムが積み上がります。また、グラフの描き方を用途から逆引きしたいときは、既刊の「Python実務で必ず使うデータ可視化レシピ106選」と併用できる構成にしています。
想定している読者は次のような方々です。
全12章は、環境(第1〜2章)→ Pythonの基礎(第3〜5章)→ データ分析ライブラリ(第6〜9章)→ 実務と開発環境(第10〜12章)の順に積み上げる構成です。各章は独立して参照できるようにも作っています。
目次
生成AIの登場で、Pythonのコードを書くこと自体は大きく楽になりました。やりたいことを日本語で伝えれば、動くコードが返ってきます。それでもPythonを学ぶ価値は失われていないと考えています。生成されたコードが正しいかを判断し、自社のデータと業務に合わせて直す仕事は、依然として人間の側に残っているからです。
内容を理解できないコードの実行結果だけを見て意思決定をするのは危険です。集計の対象が意図とずれていても、欠損値の扱いが誤っていても、コードは何事もなく数字を返します。その数字が妥当かどうかは、コードを読める人にしか判断できません。生成AIによってコードが量産されるほど、読んで検証できる基礎力の重要性は増していると考えています。本ガイドが提供したいのは、文法の丸暗記ではなく「なぜこう書くのか」を理解した上での、この検証する力です。
本ガイドの構成上の特徴は、多くの入門書が付録に回しがちな環境構築・Jupyter・Git・Dockerといった「コードの外側」に、全12章のうち5章を割いていることです。挫折の多くは文法ではなく環境で起きること、そして実務でコードが価値を生むには再現できる環境と管理されたコードが不可欠であることが、その理由です。データ分析の学習は、プログラミング言語の学習であると同時に、道具立ての学習でもあります。
本ガイドの一貫した方針は「仕組みを理解して、自力で前に進める人になる」ことです。エラーが出ても原因を切り分けられる、環境が変わっても対応できる、生成AIの出力を検証できる。この自立性こそが、ツールの流行り廃りを越えて残るスキルだと考えています。
Anagraftでは、AIプロジェクトの構想・課題設計から、データ分析・機械学習モデルの開発、AI人材の育成まで一貫したご支援を行っています。会社概要・ご支援内容の詳細は、以下の資料からご覧いただけます。
本ガイドの構成です。第1部から順に積み上がりますが、各章は参照用に独立して読める形にもしています。
| 部 | 章 | 扱う内容 | 越えられる壁 |
|---|---|---|---|
| 第1部 環境を整える | 第1章 環境構築の考え方 | Python本体・pip・仮想環境・ツールの選択 | 「インストールで挫折」を防ぐ |
| 第2章 Jupyterの使い方 | セル・カーネル・ショートカット・実行順の罠 | 対話的分析の作法を身につける | |
| 第2部 Pythonの基礎 | 第3章 データ型 | 数値・文字列・リスト・辞書 | すべての土台となる型の理解 |
| 第4章 制御構文 | if・for・内包表記・例外とエラーの読み方 | 「エラーで固まる」を卒業する | |
| 第5章 関数とモジュール | def・引数・import・lambda | コピペ分析から再利用へ | |
| 第3部 データ分析ライブラリ | 第6章 NumPy | ndarray・ベクトル化・ブロードキャスト | 「forを書かない」計算の発想 |
| 第7章 Pandas基礎 | 読み込み・loc/iloc・フィルタ・欠損 | 表データを自在に触る第一歩 | |
| 第8章 Pandas実践 | groupby・merge・pivot・日付処理 | 集計レポート作成の自動化 | |
| 第9章 可視化入門 | matplotlibの2流儀・Seaborn・日本語表示 | 伝わるグラフを描く土台 | |
| 第4部 実務と開発環境 | 第10章 実務ユーティリティ | pathlib・glob・tqdm・スクリプト化 | 地味だが効く自動化の道具箱 |
| 第11章 Git入門 | コミット・GitHub・.gitignore | 「analysis_v2_最新.ipynb」からの卒業 | |
| 第12章 Dockerと環境再現 | コンテナ・Dockerfile・生成AI時代の学び方 | 「自分のPCでは動くのに」を防ぐ |
データサイエンスの学習でもっとも多くの人がつまずくのは、統計学の数式でも、機械学習アルゴリズムの理屈でもありません。一番手前にある「環境構築」です。書籍やオンライン講座の手順どおりにコマンドを打っても、原因の分からないエラーが表示され、そのまま学習自体をやめてしまうことがあります。こうした離脱は、プログラミング未経験者に限らず、他の言語の経験があるビジネスパーソンにも起こりがちです。原因の多くは、本人の理解力ではなく、環境構築という工程が「何をしているのか分からないまま手順だけをなぞる」作業になっていることにあります。
本章の目的は、venvの作成コマンドを覚えていただくことではありません。Pythonという仕組みが本体とパッケージという二層構造でできていること、そしてその構造ゆえに環境が壊れやすく、壊れにくくするための工夫が仮想環境であることを、順を追って理解していただくことです。仕組みを理解しておけば、手順の一部を忘れても自分で立て直せますし、初めて見るエラーメッセージにも「たぶんこの層の話だろう」という見当がつくようになります。逆に手順の丸暗記だけでは、少し状況が変わった瞬間に対応できなくなり、それが挫折につながります。
本ガイドは全12章の構成で、第1章では環境構築の考え方を、第2章ではJupyterLabの具体的な使い方を扱います。第3章以降でPythonの文法そのものに入り、第6章のNumPy、第7章と第8章のPandasへと進んでいきます。第11章のGit、第12章のDockerまで進むと、環境そのものを再現可能な形で他者と共有する方法まで扱えるようになります。まずはこの第1章で、その土台となる考え方を固めておきましょう。
データサイエンスで使われる言語としては、Pythonのほかに統計解析に強いRや、表計算ソフトのExcelが挙げられることがよくあります。それぞれに向き不向きがありますが、Pythonが広く選ばれている最大の理由は、言語仕様そのものというより、その周辺に育った巨大なライブラリ群とコミュニティにあります。数値計算のNumPy、表形式データを扱うPandas、機械学習のscikit-learn、深層学習のPyTorchやTensorFlow。これらはすべて無料で公開されており、世界中の開発者が日々改良を続けています。分析の入口から、機械学習モデルの構築、Webアプリケーションへの組み込みまでを一本の言語で通せる汎用性が、Pythonの実務での強みです。
Rは統計学の専門的な手法を学術分野で先行して実装してきた歴史があり、統計モデリングに特化した研究では今も有力な選択肢です。Excelは特別な準備なしにすぐ使え、少人数への説明にも向いています。ただし、データの件数が数十万行を超えたあたりから処理が重くなりやすく、同じ集計作業を毎回手作業で繰り返す運用は属人化しやすいという弱点も抱えています。Pythonでコードとして処理を書いておけば、同じ集計を毎回同じ手順で、しかも人手を介さずに再現できます。どの道具にも得意分野があり、本ガイドはその中でも実務での汎用性と学習リソースの豊富さから、Pythonを最初の言語として選んでいます。
近年注目される生成AIの分野でも、Pythonの立ち位置は変わっていません。大規模言語モデルを扱うためのライブラリ群や、深層学習フレームワークであるPyTorchは、計算の中核こそC++などで書かれていますが、利用者が触る主要なAPIはPython向けに整備されています。データ分析の基礎としてPythonの読み書きに慣れておくことは、将来的に生成AIを実務で活用する場面に取り組む際の土台にもなります。統計学やPandasといった従来型のデータサイエンスの範囲にとどまらず、学習の投資対効果が長く続きやすい言語だと言えます。

環境構築でつまずく方の多くは、「Pythonをインストールしたのに、書籍のコードがエラーになる」という壁にぶつかります。これを理解するには、Pythonという仕組みが二層構造でできていることを押さえておく必要があります。
Python本体は、たとえるなら素の電卓です。四則演算や文字列の操作、繰り返し処理といった、プログラミング言語として最低限必要な機能はひととおり備えていますが、それ以上の専門的な機能は持っていません。表形式データを扱う機能も、グラフを描く機能も、Python本体には含まれていないのです。
その専門機能を追加していくのがパッケージ(ライブラリとも呼びます)です。表計算をしたければPandasというパッケージを、グラフを描きたければMatplotlibというパッケージを追加する。電卓に関数電卓用の拡張モジュールを差し込んでいくようなイメージです。この構造のおかげで、Python本体は軽く保たれたまま、必要な人が必要な機能だけを足していける柔軟性が確保されています。
パッケージを追加するための道具がpipです。pipはPython標準のパッケージ管理ツールで、コマンド一つでパッケージをダウンロードし、インストールしてくれます。そのダウンロード元になっているのが、PyPI(Python Package Index、パイピーアイと読みます)という公開リポジトリです。世界中の開発者がここに自作のパッケージを登録しており、2026年時点で公開パッケージ数は数十万件に達しています。pip install pandasというコマンド一つで、PyPIからPandasの最新版がダウンロードされ、手元のPython環境に組み込まれる。この仕組みを理解しておくと、「本体」と「パッケージ」のどちらの話をしているのかが常に区別できるようになります。
Python本体とパッケージの関係が分かると、次に理解しておきたいのが「環境はなぜ壊れるのか」です。ここを知らないまま学習を進めると、ある日突然コードが動かなくなり、原因が分からず挫折する、という展開になりがちです。典型的な壊れ方は大きく三つあります。
一つ目は、パッケージ同士のバージョン衝突です。パッケージAが「パッケージCはバージョン1系でないと動かない」と要求している一方、パッケージBは「パッケージCはバージョン2系が必要」と要求している。このとき、同じ環境に両方を共存させようとすると、片方が正常に動かなくなります。パッケージの数が増えるほど、こうした依存関係のもつれは複雑になっていきます。
二つ目は、プロジェクトごとに必要なバージョンが違う問題です。半年前に作った分析コードは、当時のPandasのバージョンを前提に書かれています。ところが、その後別のプロジェクトで最新のPandasを別途インストールすると、古いコードの一部の書き方が新しいバージョンでは廃止されていて動かなくなる、ということが起こります。同じパソコンの中で、プロジェクトAは古いバージョンのまま、プロジェクトBは最新版で、という状態を両立させたいわけですが、Pythonをパソコンに一つだけインストールし、そこにすべてのパッケージを詰め込むやり方では、この両立が構造的にできません。
三つ目として、初心者が特につまずきやすいのが、パソコンにあらかじめ入っているPythonと、自分で新たにインストールしたPythonが混在してしまうケースです。Macには従来、OSの内部処理用にシステム標準のPythonが同梱されていることがあり、ターミナルでpythonコマンドを実行したときに、自分が入れたつもりのPythonとは別のものが呼び出されて、意図しないバージョンでコードが動いてしまう、という混乱がしばしば起こります。この問題も、後述する仮想環境を使えば、システム側のPythonには一切手を加えずにプロジェクト専用の環境だけで作業を完結できるため、大きく軽減されます。
三つに共通しているのは、「一つのPython環境を、性質の異なる複数の用途で共用してしまっている」という構図です。前の二つは複数のプロジェクトの要求がぶつかる形、三つ目はOSの都合と自分の都合がぶつかる形ですが、原因はいずれも共用にあります。用途ごとに環境を分けてしまえば、これらの問題はそもそも発生しません。次に説明する仮想環境は、この発想を実現するための仕組みです。
仕組みの話が続きましたので、ここで一度、実際に手を動かすための準備を整えます。この節でやることは2つだけです。Python本体をパソコンに入れることと、コマンドを打ち込む画面を開くことです。
Python本体は、公式サイト(python.org)のダウンロードページから入手します。ここで配布されているのが、先ほど説明した「素の電卓」にあたるPython本体です。Windowsの場合、近年はPython Install Managerという管理ツールの形での配布が案内されることが増えています(従来型のインストーラも引き続き提供されています)。どちらの場合も、インストールの途中で「Pythonの置き場所をPATHに追加するか」を尋ねられます。PATHとは、コマンドを打ったときにパソコンがプログラムを探しに行く場所のリストのことで、ここに追加しておくと、どのフォルダにいてもpythonというコマンドがそのまま使えるようになります。本ガイドのコマンド例はいずれもこれを前提にしていますので、追加しておくことをおすすめします。macOSの場合も同じく公式サイトのインストーラを使います。macOSにはOSの内部処理用のPythonが最初から入っていることがありますが、公式サイトから入れたものはそれとは別物として扱われますので、そちらに影響を与える心配はありません。
次に、コマンドを打ち込む画面を開きます。この画面は一般にターミナルと呼ばれ、マウスで操作するのではなく、文字でコマンドを入力してパソコンに指示を出すための道具です。Windowsではスタートメニューで「PowerShell」と検索して起動します。macOSでは、Launchpadの「その他」か、アプリケーションフォルダの「ユーティリティ」の中にある「ターミナル」を起動します。本ガイドでpip installやgitのようなコマンドが出てきたときは、断りがない限り、すべてこの画面に打ち込むものと考えてください。ノートブックのセルに書くコードとは、打ち込む場所が別だという点をここで押さえておいてください。
ターミナルが開いたら、Pythonが正しく入ったかを確かめます。次のコマンドを打ってバージョン番号が表示されれば成功です。
python --version
ここでエラーが出る場合、PATHへの追加がされていない可能性が高いので、インストーラをもう一度実行して設定を見直します。macOSではpythonではなくpython3と打つ必要がある環境もありますので、両方を試してみてください。
最後に、これから作業するフォルダを1つ用意します。分析のコードもデータも仮想環境も、このフォルダの中にまとめておくと、後片付けや共有が楽になります。mkdirはフォルダを作るコマンド、cdはそのフォルダに移動するコマンドです。
# 作業用のフォルダを作って、その中に移動する
mkdir my_analysis
cd my_analysis
ここまでできれば、次の節の仮想環境の作成にそのまま進めます。以降のコマンドは、すべてこのmy_analysisフォルダの中にいる状態で実行するものとして読んでください。
仮想環境とは、プロジェクトごとに独立した「部屋」を作る発想です。パソコン全体で一つのPython環境を共有するのではなく、プロジェクトAには専用の部屋を、プロジェクトBには別の専用の部屋を用意し、それぞれの部屋の中に、そのプロジェクトが必要とするパッケージだけをバージョンごとインストールします。部屋の中で何をインストールしても、隣の部屋には影響しません。プロジェクトAの部屋でPandasのバージョン1系を使い、プロジェクトBの部屋でバージョン2系を使う、ということが同じパソコンの中で問題なく両立します。
Python本体には、この部屋を作るための標準機能としてvenvが用意されています。追加のソフトウェアを別途インストールしなくても、Pythonが入っていればすぐに使えるのが利点です。基本的な操作は、部屋を作る、部屋に入る(有効化する)、部屋の中で必要なパッケージを入れる、という三段階です。
実際のコマンドを見てみましょう。先ほど作成したmy_analysisフォルダの中にいる状態で、そこに仮想環境を作成します。Windows(PowerShell)とMac/Linuxでコマンドの一部が異なりますので、両方を併記します。
# Windows(PowerShell)の場合
python -m venv myenv
myenv\Scripts\Activate.ps1
# Mac/Linuxの場合
python3 -m venv myenv
source myenv/bin/activate
1行目のpython(Macでは多くの場合python3)は、Python本体を呼び出すコマンドです。-m venvは、Pythonに標準で付属しているvenvという部品(モジュールと呼びます。第5章で改めて扱います)を実行するという意味で、続けてmyenvという名前を指定すると、その名前のフォルダが作られ、そこに専用のPython環境一式が用意されます。フォルダ名は自由に決められますが、venvやenvといった分かりやすい名前を使う方が多いです。2行目のActivate.ps1やactivateが、その部屋に入る(有効化する)操作にあたります。有効化すると、ターミナルの表示に(myenv)といった文字列が付き、以降のpipやpythonのコマンドがこの専用環境に対して実行されるようになります。
なお、Windowsで初めてActivate.ps1を実行しようとすると、PowerShellの実行ポリシーの制限によってスクリプトの実行がブロックされることがあります。その場合は、PowerShellでSet-ExecutionPolicy RemoteSigned -Scope CurrentUserを一度実行しておきます。-Scope CurrentUserは自分のユーザーアカウントにだけ適用する指定なので、管理者権限は不要です(パソコン全体に適用する-Scope LocalMachineを指定する場合は管理者権限が必要になります)。ただし会社のパソコンでは組織のポリシーで制限されている場合もありますので、社内の情報システム担当者に確認しながら進めることをおすすめします。
有効化ができたら、そのプロジェクトで使うパッケージをpip installでインストールします。インストールしたパッケージの一覧は、requirements.txtというテキストファイルに書き出しておくのが定番の作法です。こうしておくと、別のパソコンで同じ環境を再現したいとき、あるいは同僚に環境を共有したいときに、ファイル一つ渡すだけで済みます。
# 必要なパッケージをインストール
pip install pandas numpy matplotlib jupyterlab
# 今の環境に入っているパッケージ一覧をファイルに書き出す
pip freeze > requirements.txt
# 別のパソコンで同じ環境を再現する場合
pip install -r requirements.txt
2つ目のコマンドにある>は、コマンドの実行結果を画面に表示する代わりに、右側に書いたファイルへ書き出すという指示です(リダイレクトと呼びます)。ここではpip freezeが出力するパッケージ一覧を、requirements.txtというファイルに保存しています。
requirements.txtの中身は、次のようにパッケージ名とバージョンが1行ずつ並んだ単純なテキストファイルです。
pandas==2.2.3
numpy==2.1.2
matplotlib==3.9.2
jupyterlab==4.2.5
上の例は、ある時点でのバージョンを書き並べた見本です。実際に書かれる番号はインストールした時期によって変わりますので、そのまま写すのではなく、ご自身の環境でpip freezeを実行した結果を使ってください。また主要パッケージのみの抜粋で、実際のpip freezeの出力には依存パッケージも多数並びます。バージョン番号まで固定しておくことで、半年後に同じファイルからインストールしたときにも、当時のパッケージ構成を再現しやすくなります。プロジェクトを始めるときはこのrequirements.txtがまだ存在しませんので、まずはpip installで必要なパッケージを入れながら開発を進め、区切りのよいタイミングでpip freezeを実行してファイル化する、という進め方が一般的です。
ここまでは公式Pythonとvenvの組み合わせで説明してきましたが、実際にはPython環境を用意する方法はいくつか存在し、初めて調べると選択肢の多さに迷う方も少なくありません。ここで代表的な三つを整理しておきます。
| 選択肢 | 特徴 | 向いている場面 |
|---|---|---|
| 公式Python+venv | Python公式サイトからPython本体を導入し、標準機能のvenvで仮想環境を作る。追加ソフトが不要でシンプル | 初めてPythonに触れる方。本ガイドが基本とする組み合わせ |
| Anaconda | Python本体に加え、NumPyやPandasなど科学計算でよく使う主要パッケージがあらかじめまとめて入った配布パッケージ。専用のパッケージ管理ツールcondaも同梱される | 個別のパッケージインストールに時間をかけたくない方。研究・教育機関で従来からよく使われている |
| uv | 近年広く使われ始めた、高速なパッケージ・プロジェクト管理ツール。パッケージのインストールや仮想環境の作成をまとめて高速に処理できるのが特徴 | コマンド操作にある程度慣れてきた方。複数プロジェクトを頻繁に切り替える方 |
結論から言うと、この三つに優劣の決定的な差はありません。どれを選んでも、データサイエンスの学習や実務を進めることはできます。大切なのは、一つに決めて使い続けることです。複数のやり方を並行して試そうとすると、かえって「どの環境で何を入れたか」が分からなくなり、混乱の元になります。本ガイドでは、追加のソフトウェアを必要とせず、仕組みが単純で見通しがよいという理由から、公式Python+venvの組み合わせを基本として説明を進めます。Anacondaやuvにご興味がある場合は、venvの考え方を理解したうえで乗り換えれば、仕組みの共通点が多いため移行しやすくなります。
データサイエンスの学習や分析作業では、コードを1行ずつ書いてはすぐに結果を確認する、という進め方をすることが多くあります。この用途に向いた開発環境がJupyterLabです。ブラウザの中でコードを実行し、その場でグラフや表を表示できるのが特徴で、データサイエンスの現場で広く使われています。使い方の詳細は第2章で扱いますので、ここでは導入と起動までを確認しておきます。
先ほど作成した仮想環境を有効化した状態で、JupyterLabを起動します。前の節でjupyterlabもまとめてインストール済みであれば、1行目は省いてjupyter labだけで構いません。
# 仮想環境を有効化した状態で実行
pip install jupyterlab
jupyter lab
jupyter labというコマンドを実行すると、自動的にブラウザが立ち上がり、ファイル一覧などを表示する画面が開きます。この画面上でNotebook(ノートブック)という種類のファイルを新規作成すると、コードを書いて実行できるセルが並んだ編集画面に切り替わります。
従来型のPythonの実行方法は、ファイル全体をまとめて一気に実行するものでした。これに対してNotebookは、コードをセルと呼ばれる小さな区切りに分けて、1セルずつ実行結果をその場で確認しながら進められる点が特徴です。データの中身を1行確認し、次にグラフを1枚描き、その結果を見てから次の処理を書く、といった試行錯誤を伴う分析作業と相性がよく、データサイエンスの現場でJupyterLabが広く使われている理由もここにあります。ここから先の具体的な操作方法は第2章で詳しく解説します。

ここまで環境構築の手順を説明してきましたが、実は「まずはコードを書いて動かす体験をしたい」という段階であれば、環境構築そのものを飛ばす選択肢もあります。その代表がGoogle Colab(正式名称はGoogle Colaboratory)です。Googleが提供するサービスで、ブラウザとGoogleアカウントさえあれば、Pythonのインストールも仮想環境の作成も不要で、すぐにコードを実行できます。主要なデータ分析用パッケージはあらかじめ用意されており、無料の範囲でも一定の計算資源を使えます。
学習を始めたばかりの段階でいきなり環境構築のエラーに直面すると、本来学びたかった中身にたどり着く前に学習を諦めてしまうことがあります。Google Colabは、その最初のハードルを省き、ひとまずPythonのコードに触れてみるための選択肢として位置づけられます。第3章以降の文法の学習は、Google Colab上で進めていただいても差し支えありません。
ただし、実務で使う段階になると制約も見えてきます。社内の顧客データや機密情報を扱う分析では、外部のクラウドサービスにデータをアップロードすること自体が、社内規程やセキュリティポリシー上許されない場合があります。また、無料の範囲では利用できる計算資源に上限があり、長時間の処理や大量のデータを扱う分析には向かないこともあります。学習の入口としてはGoogle Colabを活用しつつ、実務での本格的な分析に移る段階では、自分のパソコンに仮想環境を用意する、あるいは会社が用意した分析基盤を使う、という切り替えを想定しておくとよいでしょう。
環境構築の考え方を一通り押さえたところで、実際に手を動かして確認してみます。まずは、仮想環境を有効化した状態のターミナルで、Python本体とpipのバージョンを確認します。
python --version
pip --version
それぞれ、Python 3.14.3のような、Python本体とpipのバージョン番号が表示されれば(番号はインストールした時期によって変わります)、仮想環境の中でPython本体とpipが正しく認識されている状態です。もし想定と違うバージョンが表示された場合は、仮想環境の有効化がうまくいっていない可能性がありますので、有効化のコマンドから見直してみてください。バージョン確認ができたら、JupyterLabを起動してNotebookを開き、セルに次の1行を入力して実行してみてください。
print("Hello, データサイエンス")
この1行を実行すると、括弧の中の文字列がそのまま画面に表示されます。printは、指定した内容を画面に表示する、Pythonでもっとも基本的な命令の一つです。これから何度も使うことになりますので、最初に確認しておいて損はありません。
もう一つ、Pythonは電卓としてもそのまま使えます。試しに1 + 1のような式だけをセルに入力して実行すると、計算結果の2がそのまま表示されます。掛け算はアスタリスク(*)、割り算はスラッシュ(/)を使います。この段階では難しいことを考える必要はありません。自分のパソコンで書いたコードが、自分の意図したとおりに動く。その手応えを確認できれば、環境構築という最初の関門は突破できたことになります。
ここまでで、Python本体とパッケージの関係、環境が壊れる理由、仮想環境という解決策、そしてツールの選択肢という、環境構築の考え方の骨格を押さえました。手順そのものは、慣れないうちは何度か調べ直すことになると思いますが、それで問題ありません。仕組みが頭に入っていれば、細かい手順を忘れてもエラーメッセージから状況を推測し、自力で立て直せるようになります。
環境構築は、データサイエンスの学習における最初の関門であると同時に、一度理解してしまえば以後ほとんど意識しなくなる部分でもあります。ここで整えた仮想環境とJupyterLabは、この先の各章で共通して使い続けることになりますので、最初にひととおり動く状態にしておくことには十分な意味があります。次章では、今回インストールしたJupyterLabの具体的な使い方と、セルの実行順序やショートカットキーといった効率よくコードを書くための作法を見ていきます。第3章からはPython自体の文法に入り、数値や文字列といったデータ型の扱い方から順に積み上げていきます。
『スッキリわかるPython入門 第2版』(国本大悟・須藤秋良、インプレス):プログラミング未経験者を主な対象に、Pythonの基本文法を丁寧な解説とキャラクターを交えた説明で学べる入門書です。本章で扱った環境構築の先、文法の学習段階でつまずいたときに立ち返る一冊として役立つと思います。
前章では、データサイエンスの作業環境をどう整えるかという考え方を扱いました。この章では、その環境の中心に置かれることが多いツール、JupyterLabの使い方を扱います。Jupyterは単なるコードエディタではなく、コードと実行結果と説明文を1つの画面の中に同居させ、試行錯誤しながらデータと対話するために設計されたツールです。この章では、基本概念からショートカット、便利なコマンド、そして初心者がつまずきやすい「落とし穴」までを順番に見ていきます。特に落とし穴の部分は、Jupyterを日常的に使うようになってから遠回りをしないために、早い段階で押さえておきたい内容です。
この章で扱う内容は次の通りです。
データサイエンスの作業は、決まった手順をなぞる作業というより、仮説を立ててはデータで確かめ、うまくいかなければやり方を変える、という試行錯誤の繰り返しです。ある変数の分布を見てみる、外れ値を除いてみる、集計の単位を変えてみる、といった小さな実験を何度も積み重ねながら、少しずつ理解を深めていきます。この進め方に向いているのが、コードの断片(セル)を1つずつ実行し、その結果をすぐその場で確認できるJupyterのような対話的な実行環境です。
通常のPythonスクリプトは、ファイル全体を上から下まで実行し直さないと結果を確認できません。データの読み込みに時間がかかる分析では、変数の定義を1つ直すたびにファイル全体を再実行するのは非効率です。Jupyterでは、データの読み込みは最初の1回だけ実行しておき、その後は集計方法やグラフの描き方を変えるたびに、該当するセルだけを再実行すればよいという使い方ができます。この「一度作った状態を保持したまま、必要な部分だけをやり直せる」という性質が、データサイエンスの試行錯誤と相性がよく、Jupyterが実験ノートのような位置づけで広く使われている理由です。
加えて、Jupyterのノートブックにはコードの合間にマークダウン形式の説明文や見出しを挟み込むことができます。分析の途中経過や気づいたことをその場でメモしながら進められるため、後から見返したときに「何を考えながらこの分析をしたのか」を追いやすくなります。コード・実行結果・文章による説明が1つのファイルの中に時系列で並ぶという構造は、分析の過程そのものを記録に残す「実験ノート」としての性格を持っています。この性格を理解しておくと、この章の後半で扱う落とし穴や整理の作法についても、なぜそのような注意が必要なのかが腑に落ちやすくなります。
グラフや表を扱う場面でも、Jupyterの利点は際立ちます。データフレームの中身や折れ線グラフ、ヒストグラムといった実行結果が、コードのすぐ下にそのまま埋め込まれた形で表示されるため、コードと結果を別々のウィンドウで見比べる手間がありません。あるグラフの見た目を少し調整したいときも、該当するセルだけを書き換えて再実行すれば、その場で結果を確認しながら細かい調整を繰り返せます。通常のスクリプトを実行してグラフを画像ファイルとして保存し、それを別のビューアで開いて確認する、という進め方に比べると、Jupyterでの試行錯誤は何倍も速く回せます。この「結果をすぐ目で確認しながら少しずつ調整する」というサイクルの速さこそが、データサイエンスの学習と実務の両方でJupyterが広く使われている最大の理由です。

JupyterLabを使ううえで、まず押さえておきたい3つの用語があります。ノートブック、セル、カーネルです。
ノートブックとは、拡張子が「.ipynb」であるファイル1つ分の単位を指します。1つのノートブックの中には、後述するセルがいくつも並んでおり、コードと実行結果とマークダウンの説明文が、上から下へと積み重なった構成になっています。ノートブックはファイルとして保存できるため、分析の過程を丸ごと共有したり、後から見返したりすることができます。
セルとは、ノートブックを構成する1つ1つの区画のことです。セルには大きく分けて2種類あります。1つはコードセルで、Pythonのコードを書いて実行するための区画です。もう1つはマークダウンセルで、見出しや箇条書き、太字などの簡単な装飾を使いながら説明文を書くための区画です。分析の目的や、あるセルで何をしているかをマークダウンセルにメモしておくと、後からノートブックを見返したときの理解の助けになります。
カーネルとは、コードセルを実行するためのPythonの実行エンジンのことです。ノートブックを開くと、裏側でカーネルが1つ起動し、それまでに実行したコードで定義された変数や関数を、メモリ上に保持し続けます。この「それまでの実行結果を覚えている」という性質が、Jupyterの柔軟さの源であると同時に、この章の後半で扱う落とし穴の原因にもなります。カーネルは画面右上などに表示されており、処理に時間がかかっている間は「Busy」のような表示に切り替わります。動作がおかしくなったと感じたら、カーネルを再起動する(リスタートする)ことで、保持されていた状態をいったんすべて消し去り、まっさらな状態からやり直すことができます。
JupyterLabのセルには、編集モードとコマンドモードという2つの操作モードがあります。編集モードは、セルの中にカーソルがあり、文字を入力できる状態です。セルをクリックするか、コマンドモードでEnterキーを押すと編集モードに入ります。コマンドモードは、セル全体を1つの単位として選択している状態で、文字は入力できませんが、セルの追加・削除・移動といった操作をキー1つで行えます。セルの左側の余白部分をクリックするか、編集モードでEscキーを押すとコマンドモードに切り替わります。この2つのモードを意識せずに使っていると、キーボードショートカットが効かずに戸惑うことがあるため、今どちらのモードにいるかは、選択しているセルの枠の色で見分けられます。JupyterLabでは編集モードが青、コマンドモードが灰色です(旧来のJupyter Notebookでは編集モードが緑、コマンドモードが青でした)。
もっとも頻繁に使うショートカットは、セルを実行するShift+Enterです。現在のセルを実行し、次のセルに移動します(次のセルがなければ新しく作成します)。よく使う操作を一覧にまとめます。
| 操作 | キー | モード |
|---|---|---|
| セルを実行して次へ移動 | Shift + Enter | 編集/コマンド共通 |
| セルを実行してその場に留まる | Ctrl + Enter | 編集/コマンド共通 |
| 編集モードに入る | Enter | コマンドモード |
| コマンドモードに戻る | Esc | 編集モード |
| 下にセルを追加 | B | コマンドモード |
| 上にセルを追加 | A | コマンドモード |
| セルを削除 | Dを2回 | コマンドモード |
| セルをコードに変更 | Y | コマンドモード |
| セルをマークダウンに変更 | M | コマンドモード |
これらのショートカットは覚えた分だけ作業速度に直結します。特に、セルの追加・削除、コードとマークダウンの切り替えをマウス操作なしで行えるようになると、思考を止めずに手を動かせるようになります。最初からすべてを覚える必要はなく、Shift+Enterと、セルの追加・削除の3つから使い始めるだけでも十分に効果を感じられます。
Jupyterには、通常のPythonの文法にはない、便利な補助機能である「マジックコマンド」が用意されています。行の先頭に「%」を1つ付けるものを行マジック、「%%」を2つ付けるものをセルマジックと呼び、行マジックはその行だけに、セルマジックはセル全体に効果が及びます。
代表的なものの1つが%timeitです。1行のコードの実行時間を、複数回実行した平均としてかんたんに計測できます。同じような処理を複数の書き方で実装したときに、どちらが速いかを手早く比較したい場面でよく使われます。セル全体の実行時間を1回だけ計測したい場合には、セルマジックの%%timeを使います。
%timeit sum(range(1000))
%%timeはセル全体の実行時間を測るセルマジックのため、セルの先頭に単独で書きます。
%%time
total = 0
for i in range(1000):
total += i
もう1つよく使うのが、行頭に「!」を付けるとシェルコマンドをそのまま実行できるという機能です。ファイルの一覧確認などに便利です。渡されるのはお使いのOSのコマンドそのものなので、ファイル一覧であればWindowsは!dir、Mac・Linuxは!lsのように書き分けます。なおパッケージのインストールには、いま動いているカーネルへ確実にインストールできる%pipマジックを使うのが安全とされています。
%pip install pandas
!ls
このほかにも、いくつかのマジックコマンドを覚えておくと調べものの手間が減ります。代表的なものを一覧にまとめます。
| マジックコマンド | 種類 | できること |
|---|---|---|
| %timeit | 行マジック | 1行のコードの実行時間を複数回計測し平均を表示する |
| %%time | セルマジック | セル全体の実行時間を1回だけ計測する |
| %matplotlib inline | 行マジック | matplotlibのグラフをノートブック内に直接表示する |
| %whos | 行マジック | 現在カーネルが保持している変数の一覧を表示する |
| %debug | 行マジック | 直前に例外が発生した場所まで遡ってデバッグする |
| !(コマンド) | シェル実行 | OSのシェルコマンドをそのまま実行する(コマンド名はOSごとに異なります) |
すべてを暗記する必要はありませんが、「Jupyterには通常のPythonの文法にはない補助的な命令が用意されている」ということ自体を知っておくと、必要になったときに調べて使えるようになります。特に%matplotlib inlineは、グラフを描く分析を始める最初のセルに書いておく定番の1行として覚えておくとよいでしょう(近年のJupyter環境では省略しても自動的にインライン表示されることが多いですが、環境によっては明示的な指定が必要です)。
コードを書く速度と正確さを底上げしてくれるのが、補完とドキュメント参照の機能です。変数名や関数名を途中まで入力してTabキーを押すと、候補が一覧で表示され、選ぶだけで残りを入力する手間を省けます。スペルミスを防げるだけでなく、ある値(Pythonではこれをオブジェクトと呼びます)がどのようなメソッド(その値に対して呼び出せる操作)を持っているかを、思い出せなくても一覧から探せるという利点もあります。
関数やメソッドの名前の直後でShift+Tabを押すと、その関数がどのような引数を取り、何をする関数なのかを説明したドキュメントが、ポップアップとして表示されます。関数の名前だけは知っているものの、細かい引数の指定方法を忘れてしまったという場面で重宝します。同じ情報は、関数名の末尾に「?」を1つ付けて実行することでも確認できます。「??」のように2つ付けると、ドキュメントに加えて、その関数が実際にどのようなコードで実装されているかまで表示されることがあります。
import pandas as pd
pd.read_csv?
ここで出てくるpdは、第7章以降で扱うPandasというライブラリを、pdという短い名前で読み込んだものです。この章では「ライブラリの関数にも同じように?が使える」という点だけ見ていただければ十分です。
調べものをするたびにブラウザで検索するのではなく、まずTab補完やShift+Tab、「?」といったノートブック内で完結する手段を試す習慣をつけておくと、分析の流れを止めずに疑問を解消できます。特に、複数のライブラリを併用する分析では、ある関数がどのライブラリのものだったか、引数の順番はどうだったかを毎回正確に覚えておくのは容易ではありません。そうした細部を都度確認しながら進めることは、コードを理解せずに書き写すこととは違い、関数の使い方をそのつど確かめながら手を動かすという、地に足のついた学び方につながります。
JupyterLabの柔軟さは、そのまま初心者がつまずきやすい落とし穴にもつながっています。この章の中でも特に押さえておきたい内容です。
もっとも代表的な落とし穴が、セルの実行順にまつわる問題です。Jupyterのセルは、画面に並んでいる順番通りに実行されるとは限りません。カーネルは「セルが画面上で何番目にあるか」ではなく、「そのセルが何回目に実行されたか」を覚えており、各セルの左側に表示される角括弧の中の数字(実行番号)がその順序を示します。上のセルを直してから下のセルに戻って再実行する、途中のセルだけを何度も実行し直す、といった作業を続けていると、画面上の見た目の順番と、実際にカーネルが実行してきた順番がずれていきます。この状態のまま作業を続けると、あるセルの結果が、画面上は上にあるはずの別のセルの、実は古い実行結果に依存しているという、本人にも気づきにくい不整合が生まれます。
具体例で確認します。次のノートブックで、セル1、セル2の順に実行した後、セル1の内容を書き換えてセル1だけを再実行したとします。
# セル1(1回目の実行)
price = 100
# セル2
tax = price * 0.1
print(tax) # 10.0 と表示される
# セル1を書き換えて再実行(2回目の実行)
price = 200
# この時点でセル2はまだ再実行していない
この状態で画面上のセル2をもう一度見ると、表示されている「10.0」という数字は、priceが100だったときの計算結果のままです。priceはすでに200に書き換わっていますが、セル2を再実行しない限り、taxの値は古いままの表示で止まっています。ノートブックを画面の見た目だけで判断すると、taxはあたかも200を基準に計算されたかのように錯覚してしまいますが、実際にはそうではありません。分析が進んで前の方のセルを直す機会が増えるほど、この種のずれは起きやすくなります。
この落とし穴を防ぐための習慣が、「Restart&Run All」です。これは、カーネルを再起動して保持されていたすべての変数を消し去ったうえで、ノートブックの先頭から末尾まで、すべてのセルを画面に並んでいる順番通りに実行し直す操作です。分析が一区切りついたタイミングや、ノートブックを他の人と共有する前には、必ずこの「Restart&Run All」を実行し、上から順番に実行しても同じ結果が再現できるかどうかを確認する習慣を持つことが大切です。この確認を怠ったノートブックは、作った本人の環境でしか動かない、あるいは本人ですら後から再現できない状態になっている可能性があります。
ノートブックの実行番号がバラバラになっている状態は、画面の見た目の順番と実際にカーネルが実行してきた順番がずれているサインです。分析の区切りごとに「Restart&Run All」を実行し、上から順に実行し直しても同じ結果になることを確認する習慣を持つことが、Jupyterで再現性のある分析を続けるための土台になります。
実行順の問題と関連して注意したいのが、「隠れた状態」の存在です。あるセルで定義した変数を、後から削除するコードを書いたつもりでも、その変数を削除するセルを実行し忘れていたり、削除したはずのセルを実行した後に別の場所で同じ名前の変数をうっかり再定義していたりすると、変数は消えないままカーネルのメモリに残り続けます。ノートブックのコードを上から読んだだけでは存在しないはずの変数が、実際には裏側で生き続けていて、後のセルの計算に影響を与えてしまうことがあります。この隠れた状態も、実行順の問題と同じく、「Restart&Run All」によってカーネルをまっさらな状態に戻し、コードに書かれている内容だけで結果が再現できるかを確認することで防げます。

実行順の問題や隠れた状態を防ぐには、日頃からノートブックを整理された状態に保っておくことも有効です。いくつかの作法を紹介します。
これらの作法に共通しているのは、「今日の自分にとって分かりやすいノートブック」ではなく、「数か月後の自分や、初めて見る同僚にとっても分かりやすいノートブック」を意識するという視点です。ノートブックは書いた本人の頭の中を映す実験ノートであると同時に、共有され、後から読み返される記録でもあります。
整理の作法は、実行順の落とし穴とも深く関係しています。importが冒頭にまとまっていれば、「Restart&Run All」を実行したときに読み込み漏れで途中のセルが止まってしまう事態を防げます。見出しセルで構造化されていれば、どこまで実行し直したかを見失いにくくなります。1つのノートブックの目的が絞られていれば、そもそも実行するべきセルの数自体が減り、実行順が複雑に絡み合う余地も小さくなります。整理の作法は見た目を整えるためだけの習慣ではなく、この章の要である実行順の問題や隠れた状態を未然に防ぐための実務的な備えでもあります。
ブラウザで動くJupyterLabとは別に、コードエディタであるVisual Studio Code(VS Code)の中でも、拡張機能を追加することでノートブックを開いて実行できます。VS Code上でノートブックを開くと、JupyterLabとほぼ同じ見た目のセル単位の編集・実行に加えて、変数の中身を一覧で確認できるパネルや、Gitとの連携(第11章で扱います)、コード整形などのエディタとしての機能を、同じ画面の中でまとめて利用できます。
ノートブックのファイル形式そのものはJupyterLabでもVS Codeでも共通の「.ipynb」であるため、どちらのツールで作成したノートブックも、もう一方のツールで開き直すことができます。ブラウザベースの手軽さを取るか、日頃から使い慣れたエディタの中で統一して作業したいかは、好みや作業内容によって分かれるところです。普段からPythonのスクリプトや他の言語のコードもVS Codeで書いているのであれば、ノートブックも同じエディタの中で開けることに一定のメリットがあります。
近年は、コード補完や生成AIによる支援を受けながらノートブックにコードを書く機会も増えています。関数名を途中まで書くと続きの候補が提案されたり、やりたいことを日本語で説明するとコードの下書きが生成されたりする場面は、今後さらに一般的になっていくと考えられます。
こうした支援を利用すること自体は、作業を効率化するうえで有効な手段です。ただし、生成されたコードをそのまま実行するだけでは、この章で扱ったような実行順の問題や隠れた状態には気づけません。提案されたコードが何をしているのかを読んで理解し、意図した通りに動いているかを自分の目で確認し、必要であれば修正できる力は、支援ツールを使う場合でも変わらず求められます。生成AIが書いたコードであっても、ノートブックに組み込んだ以上は、そのセルを実行した人がその結果に責任を持つことになります。コードを読み解く力を土台に据えたうえで、生成AIによる支援を上手に取り入れていくという向き合い方が、これからのデータサイエンスの作業においても引き続き大切になります。
本章では、JupyterLabという道具そのものの使い方を扱いました。セルの実行順という落とし穴を押さえ、ショートカットと補完で手を速くしておけば、この先の章で試行錯誤を繰り返すときの土台になります。次章からは、そのセルに何を書くのか、つまりPythonの文法そのものに入ります。まずは、あらゆる処理の材料になるデータ型から見ていきます。
『改訂版 Pythonユーザのための Jupyter[実践]入門』(池内孝啓ほか、技術評論社):Jupyter Notebook/Labの基本操作からマジックコマンド、拡張機能、実務での活用パターンまでを幅広くカバーした解説書です。この章で扱ったショートカットやマジックコマンドの先にある、より発展的な使い方を確認したい場合の参考になります。
第1章では環境構築の考え方を、第2章ではJupyterというノートブック環境の使い方と作法を確認しました。手元にPythonを動かせる環境が整い、コードを書いて結果をその場で確認する道具も揃いました。ここからようやく、Pythonという言語そのものの中身に入っていきます。
本章のテーマはデータ型です。地味に聞こえるかもしれませんが、データ分析のコードを書くうえで最も土台になる部分だと考えています。数値なのか文字列なのか、1つの値なのか複数の値をまとめたものなのか、変更できるものなのかできないものなのか。こうした型の性質を理解していないと、後の章で扱うNumPyやPandasの挙動がなぜそうなるのか腑に落ちず、エラーが出るたびに原因を推測だけで探すことになりがちです。
本章では、変数への値の代入から始めて、数値型、文字列型、ブール型とNone、リスト、辞書、タプルと集合という順に、Pythonの基本的なデータ型を1つずつ確認します。最後に、CSVファイルの1行がなぜ辞書という型と相性がよいのかという視点から、次章以降で扱うPandasへの橋渡しをおこないます。
変数とは、値に名前を付けて、後から呼び出せるようにする仕組みです。たとえば売上の数値を扱うとき、毎回「125000」と直接書く代わりに、sales という名前を付けておけば、コードの中で sales と書くだけでその値を参照できます。
Pythonでの代入は次のように書きます。
sales = 125000
store_name = "東京店"
ここで大切なのは、代入の記号「=」が数学の「等しい」という意味ではなく、「右側の値を、左側の名前に結び付ける」という操作を表している点です。sales = 125000 は「salesは125000と等しい」ではなく、「これから125000という値をsalesという名前で呼べるようにする」と読みます。この読み替えができると、後の章で出てくる sales = sales + 1000 のような書き方(既存のsalesに1000を足し、その結果をあらためてsalesという名前に結び付け直す操作です)にも違和感がなくなります。
変数名の付け方には最低限のルールがあります。英数字とアンダースコア(_)が使え、数字から始めることはできません。また、大文字と小文字は区別されます。加えて、コードは書く瞬間よりも後から読み返す機会のほうが多いものです。sales や store_name のように、何のデータが入っているかが名前から分かるようにしておくと、数か月後の自分や、コードを引き継ぐ同僚が内容を理解しやすくなります。単語をつなげる際はアンダースコアで区切る書き方(store_name のような形式)が、Pythonのコードでは一般的です。
Pythonの数値には大きく分けて2つの型があります。整数を表すint(整数型)と、小数点を含む数値を表すfloat(浮動小数点型)です。3と書けばintに、3.0や3.5と書けばfloatになります。
足し算・引き算・掛け算はそのまま+、-、*で書けます。残りの演算子のうち、データ分析で見落とされがちなものが次の4つです。
| 演算子 | 意味 | 例 | 結果 |
|---|---|---|---|
| / | 割り算(常にfloatを返す) | 7 / 2 | 3.5 |
| // | 床除算(小さいほうの整数へ丸める) | 7 // 2 | 3 |
| % | 剰余(割り算の余り) | 7 % 2 | 1 |
| ** | 累乗 | 7 ** 2 | 49 |
//について1点だけ補足します。正の数どうしでは「小数点以下を切り捨てる」と考えて差し支えありませんが、正確には「割り算の結果より小さいほうの整数へ丸める」という動作です。そのため負の数が入ると-7 // 2は-3ではなく-4になります。売上や件数のように0以上の値だけを扱っている限りは意識せずに使えますが、差分など負になりうる値に使うときは、この違いを思い出してください。
なお、この%は割り算の余りを求める演算子で、第2章で扱ったJupyterのマジックコマンド(%timeitなど)の先頭に付く「%」とは別物です。同じ記号ですが、行の先頭に付いていればマジックコマンド、数値と数値の間にあれば剰余、と読み分けます。
// と % はセットで使う場面が多い演算子です。たとえば「合計金額が1万円札で何枚分あり、いくら端数が残るか」を計算するときや、「通し番号を7日ごとのグループに分けたい」ときのグループ番号の計算などに使えます。単純な割り算(/)だけでは表現しにくい、割り切れなさそのものを扱いたい場面で登場すると覚えておくと見通しがよくなります。
もう1つ、データ分析に携わるなら早い段階で知っておくべきなのが、floatには誤差が生じるという性質です。
print(0.1 + 0.2)
# 実行結果: 0.30000000000000004
0.1 + 0.2 の答えは当然0.3であるはずなのに、Pythonは0.30000000000000004という、わずかにずれた値を返します。これはPythonのバグではなく、コンピュータが数値を2進数(0と1だけで表す数の表現方法)で扱っていることに起因する、あらゆるプログラミング言語に共通する性質です。0.1や0.2のような10進数の小数の多くは2進数では割り切れない無限小数になり、コンピュータのメモリという有限の桁数の中では、わずかな誤差を含んだ近似値として保存されます。
floatの誤差は、データ分析の実務で気づきにくい不具合の原因になります。たとえば「売上の合計が想定と1円だけ合わない」「本来ゼロになるはずの差分がゼロにならず、条件分岐が想定どおりに動かない」といった不具合の多くは、floatの誤差が原因です。2つのfloatが「等しいかどうか」を == で直接比較するのは避け、round() で桁数を丸めてから比較する、あるいは差の絶対値が十分小さいかどうかで判定するといった対処が必要になります。この性質を知らずに数値計算のコードを書くと、原因の分からない小さなバグに悩まされることになるので、本章で最初に押さえておいてほしい点です。
誤差を目立たせずに表示したいときは、round() 関数で桁数を指定して丸めます。round(0.1 + 0.2, 2) と書けば、小数第2位までに丸めた0.3が得られます。丸めは表示や比較のための工夫であり、内部的な誤差そのものが消えるわけではない点に注意してください。
文字列とは、文字の並びを表すデータ型です。Pythonではシングルクォート(’)とダブルクォート(”)のどちらでも文字列を作れ、動作に違いはありません。文字列の中にクォート記号自体を含めたい場合(例えば東京都渋谷区の「渋谷」を”渋谷”と表記したい場合など)は、外側と内側で異なる種類のクォートを使うと簡単です。
データ分析のコードで特に頻繁に使うのが、f文字列(エフ文字列)と呼ばれる書き方です。文字列の前にfを付け、中括弧{}の中に変数や式を書くと、その値が文字列の中に埋め込まれます。
store = "東京店"
sales = 3234
report = f"{store}の売上は{sales:,}円でした。"
print(report)
# 実行結果: 東京店の売上は3,234円でした。
{sales:,}の:,の部分は書式指定と呼ばれ、数値を3桁区切りのカンマ入りで表示する指示です。分析結果を人間が読むレポートや通知文に変換する場面で、f文字列は非常によく登場します。以前は文字列の連結を “+” 記号や format() メソッドでおこなうのが主流でしたが、今はf文字列のほうが読みやすく簡潔に書けます。
文字列にはあらかじめ用意された便利な操作(メソッドと呼びます)が数多く備わっています。データを整えるときに使う頻度が特に高いのが、split(区切り文字で分割する)、strip(前後の余分な空白や改行を取り除く)、replace(指定した文字列を別の文字列に置き換える)の3つです。
加えて、文字列は1文字ずつ順番に並んだものとして扱え、位置を指定して一部を取り出せます。この位置指定をインデックス、範囲を指定して一部を切り出す操作をスライスと呼びます。Pythonでは先頭の文字が0番目として数えられる点に注意してください。
raw = " 2026-07-14,東京店,125000 "
cleaned = raw.strip()
parts = cleaned.split(",")
print(parts)
# 実行結果: ['2026-07-14', '東京店', '125000']
date_text = parts[0]
print(date_text[:4])
# 実行結果: 2026 (先頭4文字、年の部分)
print(date_text[5:7])
# 実行結果: 07 (5番目から6番目まで、月の部分)
print(cleaned.replace("東京店", "大阪店"))
# 実行結果: 2026-07-14,大阪店,125000
この例は、テキストファイルから読み込んだ1行分の生データを、分析に使える形に整える典型的な流れです。strip()で前後の余分な空白を取り除き、split(",")でカンマ区切りの値を分解してリストにする、という組み合わせは、CSV形式のデータを自作の処理で読み込むときによく使う型です(実務でCSVを読み込む際は、次章以降で扱うPandasのread_csv関数を使うのが基本ですが、その裏側でおこなわれている処理の考え方はここで見た文字列操作と同じです)。スライスのdate_text[:4]は「先頭から4文字目の手前まで」、date_text[5:7]は「5番目から7番目の手前まで」を意味し、終わりの位置は含まれない、という点がPythonのスライスの決まりごとです。

ブール型(bool)は、TrueかFalseのどちらかだけを取る型です。比較演算子(==、!=、<、>、<=、>=)を使った式の結果は、必ずこのブール型になります。
sales = 125000
print(sales > 100000)
# 実行結果: True
print(sales == 100000)
# 実行結果: False
ブール型は、後の章で扱う条件分岐(if文)や、Pandasでの行の絞り込みで中心的な役割を果たします。「売上が10万円を超える行だけを取り出す」といった処理は、内部的には各行のTrue・Falseを判定し、Trueの行だけを残す仕組みで動いています。
もう1つ、本章で必ず触れておきたいのがNoneという特別な値です。Noneは「値が存在しない」ということそのものを表す値です。0や空文字列(””)とは異なり、「何も入っていない」という状態を明示的に表現するために使われます。
customer_age = None
print(customer_age)
# 実行結果: None
print(type(customer_age))
# 実行結果: <class 'NoneType'>
アンケートの年齢欄が未回答だったり、センサーが一時的に値を取得できなかったりした場合、その項目にはNoneのような「値がない」ことを表す仕組みが必要になります。実際のデータ分析では、Noneそのものよりも、Pandasが用意するNaN(Not a Number、欠損値を表す特別な値)という形でこの概念に出会うことが多くなりますが、根底の考え方は同じです。「値がない」状態を、0や空文字列といった別の意味を持つ値と混同せずに扱う、という発想を押さえておいてください。
リストは、複数の値を順番に並べて1つにまとめたデータ型です。角括弧[]の中にカンマ区切りで値を書いて作ります。数値でも文字列でも、異なる型を混ぜて入れることもできます。
daily_sales = [98000, 125000, 110500, 132000]
print(daily_sales[0])
# 実行結果: 98000 (先頭の要素)
print(daily_sales[-1])
# 実行結果: 132000 (末尾の要素)
print(daily_sales[1:3])
# 実行結果: [125000, 110500]
daily_sales.append(145000)
print(daily_sales)
# 実行結果: [98000, 125000, 110500, 132000, 145000]
append()は、リストの末尾に新しい値を追加するメソッドです。マイナスの添字(-1など)は末尾から数える位置を表し、末尾の要素を直接取り出すときに便利です。スライスの記法は文字列のときと同じ考え方で使えます。
リストを扱ううえで、入門者がまず戸惑いやすいのが、リストがミュータブル(mutable、変更可能)であるという性質です。次のコードを見てください。
a = [10, 20, 30]
b = a # bにaを代入
b.append(40)
print(a)
# 実行結果: [10, 20, 30, 40] ← aまで変わってしまう
print(b)
# 実行結果: [10, 20, 30, 40]
c = a.copy() # 独立したコピーを作る
c.append(50)
print(a)
# 実行結果: [10, 20, 30, 40] ← aは影響を受けない
print(c)
# 実行結果: [10, 20, 30, 40, 50]
b = a という代入は、aの中身をコピーしてbという新しい箱に入れる、という意味ではありません。aという名前とbという名前が、同じ1つのリストを指すようになる、という意味です。荷札が2枚あって、どちらも同じ1つの箱に貼られているとイメージすると分かりやすいかもしれません。そのため、bに対してappend()で要素を追加すると、同じ箱を指しているaの中身も一緒に変わってしまいます。
この性質は、数値や文字列のようなイミュータブル(immutable、変更不可能)な型では意識する必要がありません。x = 10; y = x; y = y + 1 と書いても、xは10のままです。y = y + 1は「yの中身を書き換える」のではなく「11という新しい値を作って、それにyという名前を付け直す」操作だからです。一方、リストのappend()のような操作は、新しい箱を作らず今ある箱の中身そのものを書き換えます。この違いが、コピーをめぐる混乱が起きる理由です。
元のリストを変えずに複製として扱いたいときは、a.copy()、あるいはlist(a)という書き方を使います。ただし、これらは「1階層分だけ」を複製する浅いコピー(シャローコピー)である点には注意してください。リストの中にさらにリストが入っている入れ子のデータでは、内側のリストまでは複製されず、同じ箱を共有したままになります。入れ子構造まで完全に複製したい場合は、標準ライブラリのcopyモジュールが提供するdeepcopy()関数を使います。

辞書(dict)は、キーと値の組み合わせでデータを持つ型です。波括弧{}の中に、キー:値のペアをカンマ区切りで並べて作ります。リストが「順番」で値を管理するのに対して、辞書は「名前(キー)」で値を管理する点が大きな違いです。
record = {
"date": "2026-07-14",
"store": "東京店",
"sales": 125000,
}
print(record["store"])
# 実行結果: 東京店
print(record.get("staff"))
# 実行結果: None (該当するキーがない)
print(record.get("staff", "不明"))
# 実行結果: 不明 (キーがない場合の既定値を指定)
角括弧を使って record[“store”] のようにキーを指定すると、対応する値を取り出せます。ただし、存在しないキーを角括弧で指定するとエラーが発生します。get()メソッドを使えば、キーが存在しない場合にエラーを起こさずNoneや指定した既定値を返してくれるため、キーの有無が不確かなデータを扱うときに安全です。先ほど触れたNoneが、ここで「値が見つからなかった」ことを表す形で早速登場しています。
辞書がデータ分析にとって特に重要なのは、1件分のレコード(データの1件、1行分の記録)を表現するのにちょうどよい形をしている点です。日付、店舗名、売上金額という異なる種類の情報を、キーという名前付きの棚にそれぞれ収める形で、1つの辞書にまとめられます。
records = [
{"date": "2026-07-14", "store": "東京店", "sales": 125000},
{"date": "2026-07-14", "store": "大阪店", "sales": 98000},
]
total_sales = sum(r["sales"] for r in records)
print(total_sales)
# 実行結果: 223000
for r in records:
print(f"{r['store']}: {r['sales']:,}円")
# 実行結果:
# 東京店: 125,000円
# 大阪店: 98,000円
sum(r["sales"] for r in records)の丸括弧の中は、リストの各要素から売上だけを順番に取り出して合計する、という処理を1行で書いた省略記法です(ジェネレータ式と呼ばれる書き方で、次章で扱う内包表記と近い形をしています)。今の段階では「1件ずつ取り出して合計している」と読めれば十分です。
また、f文字列の中でr['store']とシングルクォートを使っているのは、f文字列全体をダブルクォートで囲んでいるためです。中でもダブルクォートを使うと、そこで文字列が終わったと解釈されてエラーになります。外側と内側で引用符を使い分ける、と覚えておいてください。
ここでは、辞書を要素とするリストを作り、複数店舗分のレコードをまとめて扱っています。records[0]で1件目の辞書を取り出し、そこからさらに[“sales”]で売上を取り出す、という組み合わせ方は、表形式のデータをPythonの基本の型だけで表現するときの典型的な形です。次の章以降で扱う制御構文のfor文が、この「リストの中身を1件ずつ処理する」という場面で活躍します。
タプルは、リストとよく似た「複数の値を順番に並べたもの」ですが、一度作った後は中身を変更できないイミュータブルな型です。丸括弧()を使って作ります。
coordinate = (35.6812, 139.7671)
print(coordinate[0])
# 実行結果: 35.6812
緯度と経度の組、RGBの色を表す3つの数値の組など、「後から書き換わると困る、意味のある固定の組み合わせ」を表すのにタプルが向いています。関数が複数の値をまとめて返す際にも、内部ではタプルがよく使われます。リストとタプルのどちらを使うか迷ったら、「中身を後から追加・変更する必要があるか」を基準に選ぶとよいでしょう。
集合(set)は、重複のない値の集まりを表す型です。波括弧{}を使いますが、辞書と違ってキーと値のペアではなく、値だけを並べます。
visited_stores = ["東京店", "大阪店", "東京店", "名古屋店", "大阪店"]
unique_stores = set(visited_stores)
print(unique_stores)
# 実行結果: {'名古屋店', '大阪店', '東京店'} (並び順は保証されない)
print(len(unique_stores))
# 実行結果: 3
リストにset()を適用すると、重複する値が取り除かれた集合が得られます。「顧客リストに含まれるユニークな都道府県の数を数えたい」といった重複排除の場面で手早く使える型です。集合には順序という概念がないため、値を取り出す順番は保証されない点に注意してください。
type()、エラーの多くは型の不一致から起きるここまで見てきたint、float、str(文字列)、bool、list、dict、tuple、setは、いずれもPythonの基本的なデータ型です。ある値がどの型なのかを確認したいときは、type()関数を使います。
print(type(10))
# 実行結果: <class 'int'>
print(type(3.5))
# 実行結果: <class 'float'>
print(type("abc"))
# 実行結果: <class 'str'>
Pythonの入門者がつまずくエラーの多くは、実はこの型に関するものです。数値のつもりで扱っていた値が実は文字列だった、あるいはその逆、というすれ違いが原因になっているケースが少なくありません。
age_text = "42"
try:
print(age_text + 1)
except TypeError as e:
print("TypeError:", e)
# 実行結果: TypeError: can only concatenate str (not "int") to str
age_number = int(age_text)
print(age_number + 1)
# 実行結果: 43
上のコードのtryとexceptは、エラーが起きてもプログラムを止めずに、その内容を表示して先へ進むための書き方です(第4章で改めて扱います)。ここでは、エラーメッセージの中身を見ていただくために使っています。
“42”という文字列と1という整数を+でつなげようとすると、Pythonはcan only concatenate str (not “int”) to str、つまり「文字列は文字列としか連結できない」というエラーを返します。文字列としての”42″と、数値としての42は、見た目が似ていてもまったく別のデータとして扱われるためです。int()関数を使って文字列を整数に変換すれば、以降は通常の数値計算がおこなえます。同様に、str()関数を使えば数値を文字列に変換でき、float()関数を使えば文字列や整数を小数に変換できます。
Webフォームの入力値やCSVから読み込んだ値は、見た目が数値でも内部では文字列として扱われていることが頻繁にあります。エラーメッセージにTypeErrorが出たら、まずtype()で実際の型を確認する、という手順を習慣にしておくと、原因調査の時間を大きく減らせます。
本章の最後に、ここまで見てきた型が、実際のデータ分析の場面でどう対応しているのかを整理します。
表計算ソフトやデータベースからよく出力されるCSV形式のファイルを思い浮かべてください。1行分のデータ(たとえば「2026-07-14、東京店、125000円」)は、日付・店舗名・売上金額という、名前の付いた項目の組み合わせでした。これはまさに、本章で見た辞書がキーと値の組で1件分のレコードを表現する形と対応しています。一方、ある1つの列(売上金額の列だけを縦に並べたもの)は、同じ種類の値が複数個、順番に並んだ集まりであり、本章で見たリストが複数の値を順序立てて持つ形とちょうど対応しています。
| 表形式データの単位 | 対応するPythonの型 | 具体例 |
|---|---|---|
| 1件のレコード(表の1行) | 辞書(dict) | {"date": "2026-07-14", "store": "東京店", "sales": 125000} |
| 1つの列(同じ種類の値の並び) | リスト(list) | [125000, 98000, 110500, …] |
| 複数レコードの集まり(表全体) | 辞書のリスト | [{…}, {…}, {…}] |
「CSVの1行は辞書に、1つの列はリストに対応する」という見立ては、本ガイドの第7章以降で扱うPandasのDataFrame(表形式のデータをまとめて扱う仕組み)を理解する近道になります。DataFrameを扱うときも、行方向は辞書のように名前で引き、列方向はリストのように同じ種類の値の並びとして見る、という2つの見方を行き来することになります。本章で辞書とリストの性質、とくにミュータブルであることの意味を理解しておくと、DataFrameへの操作がなぜそう振る舞うのかを、丸暗記ではなく仕組みから理解できるようになります。
実務でCSVファイルを読み込む際に、辞書のリストをいちから手作りすることはほとんどなく、多くの場合は次章以降で扱うPandasのread_csv関数で読み込みから表形式への変換までを一度に済ませられます。それでも、DataFrameの内部で何が起きているのかを型のレベルで理解しておくことは、エラーに遭遇したときの原因究明や、思いどおりの形にデータを整形する作業の確かな助けになります。

次章では、本章で確認した数値・文字列・リスト・辞書といった値を実際に「動かす」ための仕組みである制御構文、すなわちif文による条件分岐と、for文・while文による繰り返し処理を取り上げます。値を入れる箱の性質が分かったところで、次はその箱をどう操るかに話を進めます。
前章では、数値・文字列・リスト・辞書・タプルといった、Pythonがデータを入れておくための「型」を見てきました。本章では視点を変え、それらのデータを使って「どういう順序で、何を、どれだけ処理するか」という、プログラムの流れそのものを組み立てる方法を扱います。プログラミング未経験の方にとっては、ここが最初の関門になりやすい部分です。if文やfor文の書き方そのものよりも、「インデント(字下げ)がブロックの範囲を決める」というPython特有のルールに慣れるまでに時間がかかることが多いためです。焦らず、実際に手元で書いて動かしながら読み進めていただければと思います。
どんなに複雑に見えるプログラムであっても、突き詰めると次の3つの部品の組み合わせでできています。
この3つを理解すれば、あとは組み合わせ方の問題です。本章ではif文(分岐)、for文とwhile文(反復)を順番に扱ったうえで、Pythonらしい書き方として知られる「内包表記」、そして初心者がつまずきやすい「エラーとの付き合い方」を扱います。データ分析の現場でどう使われるかもあわせて見ていきます。
if文は「条件式が真(True)であれば、このブロックを実行する」という分岐を書くための構文です。基本形は次のとおりです。
score = 72
if score >= 80:
print("優")
elif score >= 60:
print("良")
else:
print("要復習")
この例を実行すると「良」と表示されます。scoreが80以上であれば最初のブロック、そうでなく60以上であればelif(else ifの意味)のブロック、どちらにも当てはまらなければelseのブロックが実行されます。elifはいくつでも重ねることができ、elseは条件をすべて満たさなかった場合の「受け皿」として、省略することもできます。
条件式の中では、==(等しい)、!=(等しくない)、>・>=・<・<=(大小関係)といった比較演算子がよく使われます。特に間違えやすいのが、代入の=と等価判定の==の混同です。「もしscoreが80であれば」と書きたいときにif score = 80:と書いてしまうと、代入と判定の区別がつかずエラーになります。条件式の中では必ず==を使う、という点を意識しておくと防ぎやすいミスです。
ここで最も重要な注意点があります。if score >= 80:の行末にあるコロン(:)と、次の行の先頭にある半角スペース4つ分の字下げ(インデント)です。Pythonでは、波括弧などの記号ではなく、インデントの深さそのものが「どこからどこまでが1つのブロックか」を決めます。同じ深さでインデントされた行が、ifの条件が真だったときに実行される範囲になります。
インデントによるブロック表現はPythonの大きな特徴であり、同時に初心者が最初につまずく定番のポイントでもあります。インデントの深さが1行だけずれている、スペースとタブが混在している、といった些細な違いだけでエラーになります。多くのエディタは自動でインデントを揃えてくれますが、コピー&ペーストで貼り付けたコードは字下げが崩れやすいため、貼り付け後に見た目のずれがないかを確認する習慣をつけておくとエラーを防ぎやすくなります。
条件式の書き方にも触れておきます。複数の条件を組み合わせるときは、論理演算子であるand(かつ)、or(または)、not(否定)を使います。
| 演算子 | 意味 | 例 | 評価 |
|---|---|---|---|
| and | 両方とも真であれば真 | age >= 20 and age < 65 | 20歳以上65歳未満であればTrue |
| or | どちらかが真であれば真 | day == "土" or day == "日" | 土曜日か日曜日であればTrue |
| not | 真偽を反転させる | not is_member | is_memberがFalseであればTrue |
もう1つ、実務でよく使う判定としてinがあります。「ある値が、リストや文字列などの中に含まれているか」を調べる演算子です。
target_categories = ["食品", "日用品", "書籍"]
category = "日用品"
if category in target_categories:
print(f"{category}は対象カテゴリです")
else:
print(f"{category}は対象外です")
inは「リストの中に特定の値があるかどうか」を1行で判定できるため、if文の条件式の中で頻繁に登場します。後述するfor文と組み合わせて「リストの各要素について、別のリストに含まれているかを確認する」という使い方も定番です。
for文は「集まりの中身を1つずつ順番に取り出しながら、同じ処理をくり返す」ための構文です。Pythonのfor文は、リストや辞書、文字列など「複数の要素をまとめて持つもの」であれば、ほぼそのまま使うことができます。
sales = [120, 340, 210, 560, 90]
total = 0
for s in sales:
total += s
print(f"合計売上: {total}")
# rangeを使って回数を指定してくり返す(0, 1, 2の3回)
for i in range(3):
print(f"{i}回目の処理です")
# 辞書はデフォルトでキーが1つずつ取り出される
prices = {"りんご": 150, "みかん": 100, "バナナ": 80}
for key in prices:
print(f"{key}: {prices[key]}円")
# キーと値を同時に取り出したい場合はitems()を使う
for key, value in prices.items():
print(f"{key}は{value}円です")
リストではsのように要素そのものが1つずつ取り出され、辞書ではデフォルトでキーが取り出されます。キーと値の両方を同時に使いたい場合は、辞書のitems()メソッドを使うのが定石です。また、「決まった回数だけくり返したい」場合にはrange(3)のように、0から指定した数の手前まで整数を順に生成するrangeを使います。
データ分析の現場でよく使う書き方として、enumerateとzipがあります。enumerateは「要素を取り出しながら、同時にその番号(インデックス)も欲しい」場合に使います。zipは「複数のリストを、対応する位置どうしでまとめて同時に取り出したい」場合に使います。
files = ["sales_202601.csv", "sales_202602.csv", "sales_202603.csv"]
# enumerate: 要素と一緒に番号(0始まり)も取得する
for i, filename in enumerate(files):
print(f"{i + 1}件目のファイルを処理中: {filename}")
names = ["東京店", "大阪店", "福岡店"]
revenues = [890, 620, 410]
# zip: 複数のリストを対応する位置ごとにまとめて取り出す
for name, revenue in zip(names, revenues):
print(f"{name}の売上は{revenue}万円です")
この例のように、フォルダの中にある複数のファイルを1件ずつ順番に読み込んで集計する、といった処理はデータ分析の入り口として典型的なfor文の使いどころです。ファイルの一覧を取得する具体的な方法は第10章で扱いますが、「一覧を取得し、for文で1件ずつ処理する」という骨格そのものは本章の内容だけで理解できます。同様に、複数のCSVファイルを1つずつ読み込んで結果を1つのリストにまとめていく、複数のExcelシートを順番に開いて集計するといった作業も、骨格は同じfor文の組み合わせで書くことができます。

while文は「条件式が真である間、処理をくり返す」構文です。for文が「決まった集まりの中身を1つずつ処理する」のに対し、while文は「くり返す回数があらかじめ決まっていない」場面で使われます。
balance = 100000
years = 0
# 残高が20万円を超えるまで、年利3%で増やし続ける
while balance < 200000:
balance = balance * 1.03
years += 1
print(f"{years}年で20万円を超えました(残高: {balance:.0f}円)")
データ分析の実務では、あらかじめ件数が分かっているデータを処理することが多いため、while文の出番はfor文ほど多くありません。ただし「ある条件を満たすまで反復計算を続ける」といった処理では使われます。while文を使う際の注意点は1つだけです。条件式がいつまでも真のままだと、プログラムが終わらない「無限ループ」に陥ります。上の例でbalance = balance * 1.03を書き忘れると、balanceが100000のまま更新されないため、条件balance < 200000がいつまでも真であり続け、永久にループします。なおyears += 1は表示用の数え上げなので、これを忘れてもループ自体は終わりますが、経過年数が0年と表示されてしまいます。while文を書いたら、ループの中で条件に関わる変数が必ず更新されているかを確認する習慣をつけておくと安全です。
ここまでのfor文の書き方に加えて、Pythonにはもう1つ、「内包表記」と呼ばれる独特の書き方があります。もとになるリストなどから、条件に応じて要素を選んだり加工したりしながら新しいリストを作る、という処理を1行で書ける構文です。書籍やインターネットのサンプルコードで非常によく登場するため、自分で書けるようになるかどうかは別として、読んで意味が分かる状態にしておくことを本章の目標にします。
次の2つのコードは、まったく同じ結果(1から5までの数をそれぞれ2乗したリスト)を作ります。
# 通常のfor文で書く場合
squares = []
for n in range(1, 6):
squares.append(n ** 2)
# リスト内包表記で書く場合(結果は同じ)
squares = [n ** 2 for n in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
リスト内包表記は「[式 for 変数 in 集まり]」という形をしています。読むときのコツは、for文をそのまま横に並べ替えたものだと捉えることです。「for n in range(1, 6)」という見慣れた部分を先に見つけ、その前にある「n ** 2」が、くり返しのたびにリストへ追加される中身(式)だと理解すれば、通常のfor文と対応づけて読むことができます。内包表記は、for文に対する「糖衣構文(syntactic sugar、書き方を簡潔にするための表現)」の一種だといえます。ただし1つだけ違いがあり、内包表記の中で使った変数(上の例のn)は内包表記の外には残りません。通常のfor文ではループ変数がループの後も残るため、この点は書き分けの判断材料になります。
条件を加えることもできます。「[式 for 変数 in 集まり if 条件]」という形にすると、条件を満たす要素だけを対象にできます。
numbers = [12, 7, 25, 3, 18, 41, 9]
# 偶数だけを2乗したリストを作る(条件付き内包表記)
even_squares = [n ** 2 for n in numbers if n % 2 == 0]
print(even_squares) # [144, 324]
# 辞書内包表記:単価と個数のリストから、商品名をキーにした金額の辞書を作る
items = ["りんご", "みかん", "バナナ"]
unit_prices = [150, 100, 80]
quantities = [3, 5, 2]
price_dict = {
name: price * qty
for name, price, qty in zip(items, unit_prices, quantities)
}
print(price_dict) # {'りんご': 450, 'みかん': 500, 'バナナ': 160}
辞書内包表記は「{キー: 値 for 変数 in 集まり}」という形で、リストではなく辞書を作ります。この例ではzipを使って3つのリストを同時に回し、商品名をキー、金額を値とする辞書を1行で組み立てています。
内包表記は書き手にとって簡潔で、「Pythonらしい」書き方として好まれる一方、条件やfor文が何重にも重なってくると、かえって読みにくくなります。目安として、内包表記の中にさらにif文やfor文が2つ以上入り組むようであれば、無理に1行に詰め込まず、通常のfor文に書き直したほうが読み手にとって親切です。内包表記は「読めれば十分」という段階があってよく、実務では「まず読めることを優先し、自分で書くときは無理に凝った書き方を狙わない」というバランス感覚が有効です。
なお、リスト内包表記の角括弧[ ]を丸括弧( )に置き換えると「ジェネレータ式」と呼ばれる別の書き方になり、結果をリストとしてまとめて持つのではなく、必要になった要素だけをその都度計算して返す仕組みになります。データ件数が非常に多い場合にメモリを節約できる書き方ですが、まずは本章で扱ったリスト内包表記と辞書内包表記の2つが読めれば、実務のコードを読み解くうえでは十分です。
プログラミングを学び始めた人の多くが最初にぶつかる壁が、「エラーで処理が止まってしまい、何が起きているのか分からなくなってしまう」という状況です。しかし、Pythonのエラーメッセージ(トレースバックと呼ばれます)は、実は原因を特定するための情報を丁寧に教えてくれています。まずは慌てず、エラーメッセージを1行ずつ読む習慣をつけることが上達の近道です。
存在しない値で割り算をしてみます。
numbers = [10, 20, 0, 5]
for n in numbers:
print(100 / n)
このコードを実行すると、3件目の要素が0であるためZeroDivisionError: division by zeroというエラーで処理が止まります。トレースバックの一番下の行に、エラーの種類(この場合はZeroDivisionError)と内容(division by zero、ゼロによる除算)が書かれており、その上にはエラーが発生したファイル名と行番号が表示されます。まず見るべきなのは一番下の行で、そこに「何が」「なぜ」起きたかが要約されています。
このような、実行中に起こりうるエラーをあらかじめ想定し、エラーが起きてもプログラム全体を止めずに処理を続けたい場合に使うのがtryとexceptです。
numbers = [10, 20, 0, 5]
results = []
for n in numbers:
try:
results.append(100 / n)
except ZeroDivisionError:
print(f"{n}で割ることはできません。スキップします")
results.append(None)
print(results) # [10.0, 5.0, None, 20.0]
tryブロックの中で実際に処理を試み、指定した種類のエラーが発生した場合だけexceptブロックが実行されます。この例では0による除算が起きても処理全体は止まらず、該当する要素だけをNone(値が存在しないことを表すPythonの特別な値)に置き換えて、残りのループを続けることができています。exceptの後ろにエラーの種類を書かずexcept:とだけ書くこともできますが、これはあらゆる種類のエラーを一括で握りつぶしてしまい、本来気づくべき別の不具合まで隠してしまう危険があるため、想定しているエラーの種類を明示的に指定することが推奨されます。想定されるエラーが複数ある場合はexcept ValueError:とexcept KeyError:のようにexceptを複数並べて書くこともでき、エラーの種類ごとに異なる対処を分けて書くことができます。また、エラーが起きても起きなくても必ず実行したい後片付けの処理(ファイルを閉じるなど)があれば、finallyブロックに書いておく方法もあります。
エラーとの向き合い方について、現代的な作法にも触れておきます。エラーメッセージが表示されたら、その内容(特に一番下のエラーの種類と説明の行)を検索エンジンや生成AIにそのまま貼り付けて、原因や対処法を尋ねるという方法が広く定着しています。エラーメッセージには変数名や具体的な値が含まれることもあるため、機密情報が含まれていないかを確認したうえで使う必要はありますが、エラー文を一字一句自分だけで解読しようとするより、まずはそのまま貼り付けて手がかりを得るほうが早く解決に至ることが多くなっています。次に紹介する対処表とあわせて、エラーへの向き合い方の土台にしていただければと思います。

入門段階でとりわけ遭遇しやすいエラーを5つ取り上げ、原因と読み方を整理します。エラーの種類さえ見分けられれば、対処の見当をつけやすくなります。
| エラー名 | 典型的な原因 | 読み方のポイント |
|---|---|---|
| SyntaxError | コロンの書き忘れ、括弧の閉じ忘れ、全角文字の混入など、文法そのものの誤り | エラー行そのものよりも、その1行前までを含めて括弧やコロンの対応を確認します。全角スペースや全角括弧が紛れ込んでいないかも要チェックです |
| IndentationError | インデントの深さが揃っていない、タブとスペースが混在している | 同じブロック内の行が、すべて同じ深さのインデントになっているかを確認します。エディタの設定でタブを自動的にスペースへ変換しておくと予防できます |
| TypeError | 文字列と数値を+で連結しようとするなど、型どうしの組み合わせが不正 | 「can only concatenate str (not "int") to str」や「unsupported operand type(s) for +」といった文言とともに、関わっている型の名前が示されます。type()で実際の型を確認し、必要であればstr()やint()で変換します |
| KeyError | 辞書に存在しないキーを指定して値を取り出そうとした | エラーメッセージにキー名そのものが表示されます。dict[key]ではなくdict.get(key)を使うと、存在しない場合にエラーではなくNoneを返すよう変更できます |
| IndexError | リストの要素数を超えた位置を指定した(例:3件しかないリストの4番目を指定) | 「list index out of range」と表示されます。len()で件数を確認し、rangeやforの範囲がその件数を超えていないかを見直します |
いずれのエラーも、まずエラーメッセージの一番下の行にある「エラーの種類」を確認し、上の対処表と照らし合わせることが最短の解決経路です。エラーの種類さえ特定できれば、原因の見当をかなり絞り込むことができます。エラーメッセージをそのまま検索や生成AIへの質問に使う際も、この対処表で当たりをつけたうえで裏を取るという使い方が効率的です。
本章で扱ったfor文は、リストや辞書、ファイルの一覧などを1件ずつ処理する場面で幅広く使われる、汎用性の高い道具です。一方で、第7章・第8章で扱うPandas(表形式のデータを扱うためのライブラリ)を使い始めると、「行を1つずつfor文で処理する」という書き方は、多くの場合において避けたほうがよい書き方になります。
Pandasが内部で使っているNumPy(第6章で扱います)は、データのまとまり全体に対して計算を一括で適用する「ベクトル化」という仕組みを備えています。「単価の列に個数の列を掛けて金額の列を作る」といった処理は、for文で1行ずつ掛け算をくり返さなくても、列どうしをそのまま掛け算するだけで、全行分の計算が一度に済んでしまいます。ベクトル化された処理は、for文で1行ずつ処理するよりも大幅に高速に動作することが知られています。
本章の時点では、「for文はこういうものだ」としっかり理解しておくことを優先していただければと思います。そのうえで、表形式のデータを扱う段階になったら「for文で1行ずつ回す前に、列全体への一括処理で書けないかをまず考える」という発想の転換があることを、頭の片隅に置いておくとよさそうです。この点は第7章・第8章で具体的な書き方とともに改めて扱います。
本章では、条件分岐と繰り返しという処理の骨格に加えて、Pythonらしい書き方である内包表記と、エラーとの向き合い方を扱いました。ここまでで、値を用意し、それを条件や繰り返しで動かすところまでができるようになりました。次章では、こうして書いた処理をひとまとまりにして名前を付け、何度でも呼び出せるようにする「関数」を扱います。

『Python実践入門 言語の力を引き出し、開発効率を高める』(陶山嶺、技術評論社):if文やfor文といった基本構文の背後にある設計思想まで踏み込んで解説しており、本章で扱った内包表記や例外処理についても、なぜそのような書き方が推奨されるのかという理由から理解を深められる一冊です。
ここまでの章では、データ型(第3章)と制御構文(第4章)という、Pythonでコードを書くための最小限の部品を扱ってきました。この2つだけでも、ある程度の分析処理は書けるようになります。ただし、実際の分析ノートブックで同じような集計処理を何度も書いていると、少し困った問題にぶつかります。本章では、その問題を解決する「関数」という仕組みと、関数をファイルにまとめて使い回す「モジュール」という仕組みを扱います。
Jupyter Notebook(第2章)でデータ分析を進めていると、似たような処理を何度も書く場面が頻繁に出てきます。たとえば「単価のリストと数量のリストから合計売上を計算する」という処理を、ある月のデータに対して書いたとします。次の月のデータでも同じ計算が必要になったとき、多くの初心者がやってしまうのが、最初に書いたセルのコードをコピーして、変数名だけを書き換えて別のセルに貼り付ける、という方法です。
この「コピペ分析」は、一度きりの作業であれば問題になりません。しかし分析が進み、同じ処理を3か所、5か所とコピーしていくと、次のような問題が表面化してきます。1つ目は修正漏れです。計算のロジックに誤りが見つかったとき、コピーした箇所すべてを漏れなく直さなければなりません。10か所にコピーしていれば10か所すべてを探して直す必要があり、1か所でも直し忘れると、古い誤ったロジックと新しい正しいロジックが同じノートブックの中に混在するという、気づきにくいバグの温床になります。2つ目は可読性の低下です。似たようで少しずつ変数名の違うコードが何十行も並んだノートブックは、半年後の自分が読んでも「このセルは何をしているのか」をすぐには理解できません。処理の意図が、コードの見た目の中に埋もれてしまうためです。
関数は、この2つの問題を同時に解決します。処理のかたまりに名前を付けて1か所にまとめておけば、修正が必要なときはその1か所を直すだけで済みます。また、\( calc\_total\_sales(prices, quantities) \) のように意味の通る名前を付けておけば、呼び出す側のコードを読むだけで「合計売上を計算しているのだな」と分かります。処理の詳細を知らなくても、名前から意図が読み取れる。これが関数を使う最大の利点です。関数は、コードの重複を防ぐ道具であると同時に、処理に「見出し」を付けて読みやすくする道具でもあります。

Pythonで関数を定義するには \( def \) というキーワードを使います。基本の形は次のとおりです。関数名の後ろの丸括弧の中に書くのが引数(関数に渡す入力の値)で、関数の中で計算した結果を呼び出し元に返すのが \( return \) 文です。
def calc_total_sales(prices, quantities):
"""単価のリストと数量のリストから、合計売上を計算する。
Args:
prices (list[int]): 商品ごとの単価のリスト
quantities (list[int]): 商品ごとの販売数量のリスト
Returns:
int: 合計売上金額
"""
total = 0
for price, quantity in zip(prices, quantities):
total += price * quantity
return total
prices = [1200, 800, 3000]
quantities = [5, 10, 2]
print(calc_total_sales(prices, quantities)) # 1200*5 + 800*10 + 3000*2 = 20000
関数定義の直後に三重引用符で囲んで書いた説明文をdocstring(ドックストリング)と呼びます。docstringは、その関数が何を受け取り、何を返すのかを記録しておくための、いわば自分自身への説明書きです。半年後の自分がこの関数を再利用しようとしたとき、docstringを読むだけで使い方を思い出せるようにしておく、という習慣を早いうちから付けておくと、後々のコードの保守がずっと楽になります。docstringは省略しても関数は正しく動きますが、書いておくと関数名にカーソルを合わせたときにJupyter上でも説明が表示されるため、実務上の利点は小さくありません。
関数の中で \( return \) にたどり着くと、そこで関数の処理は終了し、指定した値が呼び出し元に返されます。\( return \) を書かない関数は、暗黙のうちに \( None \)(何もないことを表す特別な値)を返します。「画面に表示するだけで値を返す必要のない処理」であれば \( return \) がなくても問題ありませんが、「計算結果を後で使いたい」場合は必ず \( return \) で値を返すようにします。
関数の引数には、いくつかの渡し方があります。1つ目が位置引数です。先ほどの \( calc\_total\_sales(prices, quantities) \) のように、定義したときの順番どおりに値を渡す方法で、最も基本的な渡し方です。2つ目がキーワード引数です。「引数名=値」という形で、名前を明示して値を渡す方法で、順番を気にせず渡せるという利点があります。3つ目がデフォルト値です。関数を定義する側で「この引数には、呼び出し側が何も指定しなかった場合の既定値」をあらかじめ用意しておく仕組みです。
def summarize_sales(prices, quantities, tax_rate=0.1, currency="円"):
"""合計売上と税込み金額を計算して文字列で返す。"""
subtotal = sum(p * q for p, q in zip(prices, quantities))
total_with_tax = subtotal * (1 + tax_rate)
return f"税抜{subtotal}{currency}、税込{total_with_tax:.0f}{currency}"
prices = [1200, 800]
quantities = [5, 10]
# 1. 位置引数だけで呼ぶ(定義順どおりに渡す)
print(summarize_sales(prices, quantities))
# 2. キーワード引数で渡す(順番を入れ替えても意味が変わらない)
print(summarize_sales(quantities=quantities, prices=prices, tax_rate=0.08))
# 3. currencyだけ指定する(tax_rateは省略され、デフォルト値の0.1が使われる)
print(summarize_sales(prices, quantities, currency="JPY"))
ここで身につけておきたい実利は、データ分析ライブラリの関数呼び出しが読めるようになる、という点です。たとえば第7章以降で扱うpandasには \( pd.read\_csv(filepath, encoding=”utf-8″, header=0) \) のような書き方が頻出します。これは「\( filepath \) という値だけを位置引数で渡し、\( encoding \) と \( header \) はキーワード引数で明示的に指定している」という呼び出し方です。\( pd.read\_csv \) 自体は文字コードや区切り文字など数十個ものデフォルト値を持つ引数を用意していますが、利用者はそのうち必要なものだけをキーワード引数で上書きすればよく、残りはデフォルト値のまま動作します。この読み方さえ分かっていれば、公式ドキュメントに並ぶ長い引数リストを見ても、身構える必要がなくなります。
デフォルト値を使う際に、初心者がよくはまる落とし穴があります。それは、リストのようなミュータブルな値(第3章で扱った、後から中身を書き換えられる値)をデフォルト値に指定してしまうケースです。次のコードを見てください。
# 悪い例: リストをデフォルト値に指定してしまっている
def add_record_bad(name, records=[]):
records.append(name)
return records
print(add_record_bad("田中")) # ['田中'] が期待どおり返る
print(add_record_bad("鈴木")) # ['田中', '鈴木'] になってしまう(意図せず前回の結果が残る)
# 良い例: デフォルト値はNoneにしておき、関数の中で新しいリストを作る
def add_record_good(name, records=None):
if records is None:
records = []
records.append(name)
return records
print(add_record_good("田中")) # ['田中']
print(add_record_good("鈴木")) # ['鈴木'] (毎回新しいリストから始まる)
\( add\_record\_bad \) が想定と異なる結果になる理由は、Pythonの引数のデフォルト値が、関数を呼び出すたびに新しく作られるのではなく、関数が定義された時点で1回だけ作られ、それ以降の呼び出しで使い回されるためです。リストは中身を書き換えられる値なので、1回目の呼び出しで \( append \) した内容が、2回目以降の呼び出しにもそのまま残ってしまいます。この挙動はPython特有の仕様であり、直感に反するため、実務でも気づかれにくいバグの原因になりがちです。対策は単純で、リストや辞書のようなミュータブルな値をデフォルト値にしたい場合は、デフォルト値そのものは \( None \) にしておき、関数の内部で改めて空のリストや辞書を作る、という書き方を徹底することです。
デフォルト引数にリストや辞書をそのまま指定しない、という点は、Pythonの仕様に起因する数少ない「知らないと必ずはまる」落とし穴です。数値や文字列、\( None \) 以外をデフォルト値にしたくなったときは、いったん立ち止まって \( None \) 経由の書き方に置き換える癖を付けておくと安全です。
関数の中で定義した変数は、その関数の中だけで通用します。これをローカル変数と呼び、関数が実行を終えると消えてなくなります。一方、関数の外、つまりノートブックの通常のセルで定義した変数はグローバル変数と呼ばれ、ノートブック全体からアクセスできます。この「どこからどこまでその変数が見えるか」という範囲のことをスコープと呼びます。
tax_rate = 0.1 # グローバル変数
def calc_total_with_tax(subtotal):
tax_rate = 0.08 # この行で作られるのは、関数の中だけで通用するローカル変数
return subtotal * (1 + tax_rate)
print(calc_total_with_tax(1000)) # 1080.0(関数内のtax_rateである0.08が使われる)
print(tax_rate) # 0.1(関数の外のtax_rateは書き換わっていない)
この例で注目してほしいのは、関数の中で \( tax\_rate = 0.08 \) と書いても、関数の外にあるグローバル変数の \( tax\_rate \) には一切影響しない、という点です。関数の中で同じ名前の変数に値を代入すると、Pythonはそれをローカル変数として新しく扱い、グローバル変数とは別物として管理します。この仕組みのおかげで、関数の中身がどれだけ複雑であっても、関数の外の変数を誤って書き換えてしまう心配がありません。
Pythonには \( global \) というキーワードを使ってグローバル変数を関数の内側から書き換える方法も用意されていますが、実務ではできる限り避けるべき書き方とされています。関数の外の変数を関数の中からむやみに触ると、その関数を単体で読んだだけでは動作が予測できなくなり、ノートブックのどこか別の場所で予期せぬ値の変化が起きる原因になるためです。関数は「必要な値を引数として受け取り、結果を \( return \) で返す」という入出力だけに徹し、それ以外の外部の状態には手を出さない、という作法を基本にしておくと、後から読み返したときにも動作を追いやすいコードになります。
Pythonには、名前を付けずにその場限りで使う関数を作る \( lambda \) という書き方があります。\( def \) を使った通常の関数定義に比べて、1行で簡潔に書ける代わりに、複雑な処理には向きません。実務でよく見かけるのは、他の関数に「ちょっとした処理」を渡すためだけに \( lambda \) を使う場面です。
# 通常の関数として書いた場合
def is_expensive(price):
return "高額" if price >= 50000 else "通常"
# 同じ内容をlambdaで書くと1行にまとまる
is_expensive_lambda = lambda price: "高額" if price >= 50000 else "通常"
print(is_expensive(98000), is_expensive_lambda(98000)) # どちらも「高額」
# sortのkeyにlambdaを渡す例(文字列の長さの昇順に並べ替える)
tools = ["Python", "R", "SQL", "JavaScript"]
sorted_tools = sorted(tools, key=lambda s: len(s))
print(sorted_tools) # ['R', 'SQL', 'Python', 'JavaScript']
\( lambda \) の読み方は、\( lambda \) の後ろに引数を書き、コロンの後ろに戻り値の式を書く、という決まった形さえ覚えてしまえば難しくありません。\( lambda \ price: “高額” \ if \ price \geq 50000 \ else \ “通常” \) であれば、「\( price \) を受け取り、\( price \) が5万以上なら”高額”、そうでなければ”通常”を返す関数」と読み替えられます。\( sorted \) 関数の \( key \) 引数に \( lambda \ s: len(s) \) を渡した例では、「並べ替えの基準として、各要素の文字数を使ってください」という指示をその場で書いていることになります。
\( lambda \) が特によく登場するのが、第8章で扱うpandasの \( apply \) メソッドです。\( df[“価格”].apply(lambda \ x: “高額” \ if \ x \geq 50000 \ else \ “通常”) \) のように、列の値1つひとつに対して行いたい処理を \( lambda \) でその場に書き、\( apply \) に渡すという使い方が定型パターンとして頻出します。ここで \( lambda \) の読み方を身につけておくと、第8章に進んだときに戸惑わずにコードを読み進められます。逆に、\( lambda \) の中に何行にもわたる複雑な条件分岐を詰め込みたくなったときは、素直に \( def \) で名前付きの関数を定義し、その関数名を \( apply \) や \( key \) に渡す方が、可読性の面で望ましい選択です。
関数をひとまとまりにしてファイルに保存し、他のコードから読み込んで使えるようにしたものをモジュールと呼びます。Pythonの \( import \) 文には、主に3つの書き方があります。
| 書き方 | 意味 | 使用例 |
|---|---|---|
| import モジュール名 | モジュール全体を読み込み、「モジュール名.関数名」で呼び出す | import math / math.sqrt(16) |
| from モジュール名 import 関数名 | モジュールの中から特定の関数だけを読み込み、関数名だけで呼び出す | from math import sqrt / sqrt(16) |
| import モジュール名 as 別名 | モジュールに短い別名を付けて読み込む | import numpy as np / np.array([1, 2, 3]) |
Pythonにはインストール不要ですぐ使える標準ライブラリが数多く用意されています。代表的なものをいくつか押さえておきます。\( math \) は平方根や対数、円周率といった数学的な計算をまとめたモジュールです。\( datetime \) は日付や時刻を扱うためのモジュールで、「何日後」「何営業日前」といった日付計算に使います。\( random \) は乱数を生成するモジュールで、サンプルデータの作成や、行をランダムに抽出する処理などに使われます。\( os \) はファイルやフォルダ、実行環境とやり取りするためのモジュールで、フォルダの中のファイル一覧を取得したり、環境変数を読み取ったりする際に使います。
import math
import datetime
import random
import os
print(math.sqrt(16)) # 4.0(平方根)
today = datetime.date.today()
print(today + datetime.timedelta(days=30)) # 30日後の日付
random.seed(42)
print(random.sample(range(1, 50), 5)) # 1~49から重複なく5個抽選
print(os.getcwd()) # 現在の作業フォルダのパスを表示
標準ライブラリだけで完結する処理も多くありますが、データ分析ではpandasやNumPy、matplotlibといった標準ライブラリに含まれない外部のライブラリ、いわゆるサードパーティライブラリを日常的に使います。第1章で「仮想環境を作ってライブラリをインストールする」という作業の意味を説明しましたが、そこで \( pip \ install \ pandas \) のようにインストールしていたのが、まさにこのサードパーティライブラリです。標準ライブラリは \( import \) するだけですぐ使えるのに対し、サードパーティライブラリは事前に手元の環境へインストールしておく必要がある、という違いを覚えておくと、「なぜこの \( import \) 文はエラーになるのか」というつまずきの原因が特定しやすくなります。多くの場合、原因は「そのライブラリが今の仮想環境にインストールされていない」ことにあります。
関数がノートブックの中に増えてくると、複数のノートブックで同じ関数を使い回したくなる場面が出てきます。このとき有効なのが、関数を \( .py \) という拡張子のファイルに切り出し、自分だけのモジュールとして \( import \) する方法です。やり方は、標準ライブラリを読み込むときと変わりません。
# --- sales_utils.py というファイル名で、ノートブックと同じフォルダに保存しておく ---
def calc_total_sales(prices, quantities):
"""合計売上を計算する。"""
return sum(p * q for p, q in zip(prices, quantities))
def average_price(prices):
"""平均単価を計算する。"""
return sum(prices) / len(prices)
上のsales_utils.pyをノートブックと同じフォルダに保存したうえで、ノートブック側のセルでは次のように読み込んで使います。
# --- ここからはノートブック側のセル ---
import sales_utils
prices = [1200, 800, 3000]
quantities = [5, 10, 2]
print(sales_utils.calc_total_sales(prices, quantities)) # 20000
print(sales_utils.average_price(prices)) # 1666.6666666666667
この例のように、\( sales\_utils.py \) というファイルの中に関数をまとめておけば、別のノートブックからも \( import \ sales\_utils \) の1行を書くだけで同じ関数が使えるようになります。ノートブックにベタ書きしていた関数を \( .py \) ファイルへ切り出すこの流れは、分析処理を最終的に自動実行できるスクリプトへ発展させていく、第10章で扱う内容の土台にもなります。ノートブックは試行錯誤や可視化の確認に向いた場所であり、確立した処理を安定して繰り返し実行する役割は \( .py \) ファイルに任せる、という役割分担を意識しておくと、分析プロジェクト全体の見通しが良くなります。

本章の最後に、クラスという考え方に軽く触れておきます。深入りはしませんが、この先pandasやNumPyのコードを読み書きするうえで、知っておくと理解が早まる見方があります。
これまで見てきた関数は、値を受け取って値を返すだけの独立した存在でした。これに対してクラスは、「データ」と「そのデータに対して行える処理(メソッドと呼びます)」をひとまとめにする設計図です。この設計図から実際に作られたもの1つひとつをオブジェクトやインスタンスと呼びます。第7章以降で頻繁に登場するpandasのDataFrameも、実はこのクラスの仕組みで作られたオブジェクトの一種です。\( df.head() \) や \( df.describe() \) のようにピリオド(\( . \))でつないで呼び出しているものは、\( df \) というオブジェクトが内部に持っている、あらかじめ用意されたメソッド(処理)を呼び出している、という意味になります。
つまり \( df.head() \) は、「\( df \) というデータのかたまりに対して、\( head \) というあらかじめ用意された処理を実行してください」という命令だと読み替えられます。ここで大切なのは、クラスの定義方法そのものを暗記することではありません。「オブジェクトはデータと、そのデータ専用の処理(メソッド)をセットで持っている」という見方さえ押さえておけば、\( df.groupby(…) \) や \( df.plot(…) \) のように、この先何十種類も登場するピリオドの後ろの呼び出しを、いちいち身構えることなく「\( df \) 専用の処理を呼んでいるのだな」と読み進められるようになります。クラス自体を自分で設計する場面は本ガイドの範囲では扱いませんが、この見方を持っておくだけでも、この先のコードの読みやすさは大きく変わると考えています。
本章では、処理に名前を付けて再利用可能にする関数と、それをファイルに切り出して読み込むモジュールを扱いました。ここまでがPythonの文法の土台です。次章からは、この土台の上で、データ分析のために作られた専用のライブラリに入っていきます。まずは、Pandasの内側で動いている数値計算の基盤であるNumPyから見ていきます。
『独学プログラマー Python言語の基本から仕事のやり方まで』(コーリー・アルソフ、日経BP)は、プログラミング未経験の読者を想定して関数やモジュール、クラスといった概念を1つずつ丁寧に積み上げていく構成になっており、本章で駆け足に扱った内容をより広い文脈の中でじっくり学び直したい場合に適しています。関数の設計やコードの整理に関する実践的な助言も多く、本章の先にある「読みやすいコードを書く」という視点を補強してくれます。
第1章から第5章までは、環境構築の考え方、Jupyterの使い方、そしてPythonそのものの基礎、つまりデータ型・制御構文・関数とモジュールという、いわば素手でコードを書くための土台を扱ってきました。本章からは、データサイエンスの実務で欠かせないライブラリの世界に入ります。最初に取り上げるのはNumPy(ナムパイ、Numerical Pythonの略)です。NumPyは数値計算を高速かつ簡潔に行うためのライブラリで、多次元の配列(ndarray)を中心に、配列同士の演算や集計、乱数生成といった機能を提供します。
NumPyを最初に学ぶ理由は単純です。第7章以降で扱うPandasという、データサイエンスの実務で最も使用頻度の高いライブラリは、その内部でNumPyの配列を使ってデータを保持し、計算しています。Pandasの表(DataFrame)の列の多くは、NumPyの配列を土台にして作られています(データ型によっては拡張された配列形式が使われることもあります)。NumPyの配列の性質、ベクトル化演算、ブロードキャスト、axisという概念を理解しておくことは、Pandasを表面的な操作の暗記で終わらせず、挙動の理由まで含めて理解するための前提知識になります。これらの概念は、この先の章すべての土台になる基礎にあたります。
Python標準のリストは、非常に柔軟なデータ構造です。1つのリストの中に整数、文字列、別のリストなど、異なる型の要素を自由に混在させることができます。この柔軟性は便利な一方で、数値計算という用途に限って言えば、実は足かせになります。リストの各要素は、実際にはPythonのオブジェクトへの参照として個別にメモリ上のばらばらな場所に格納されており、要素ごとに型情報や参照カウントといった付随情報を持っています。そのため、リストに対して四則演算をしようとすると、Pythonは要素を1つずつ取り出し、型を確認し、演算を行うという処理をforループで繰り返す必要があります。
これに対してNumPyのndarray(エヌディアレイ、N次元配列の意味)は、あらかじめ全要素の型を1つに統一し、メモリ上に連続した1つのブロックとしてデータを並べて格納します。型が統一されているため要素ごとに型を確認する必要がなく、さらにメモリ上で連続しているためCPUのキャッシュ効率も良く、内部では型付き言語であるCで実装された処理がまとめて実行されます。この設計の違いにより、NumPyは同じ数値計算をリストよりも大幅に高速に、かつ少ないメモリで行うことができます。整理すると次のとおりです。
| 観点 | Pythonのリスト | NumPyのndarray |
|---|---|---|
| 要素の型 | 混在可能(柔軟だが遅い) | 単一の型に統一(dtype) |
| メモリ配置 | 各要素がばらばらな場所に散在 | 連続した1ブロックに整列 |
| 要素ごとの演算 | forループで1つずつ処理 | 配列全体を一括処理(ベクトル化) |
| 得意な用途 | 異なる型を持つ汎用的な集合 | 大量の数値データの計算 |
「同じ型のデータを大量に、高速に、省メモリで扱う」という設計思想こそが、NumPyがデータサイエンスの土台ライブラリと呼ばれる理由です。次章以降で扱うPandasのDataFrameも、突き詰めれば列ごとに型を揃えたNumPy配列の集まりであり、集計や絞り込みが高速に行えるのは、内部でNumPyの仕組みに乗っているからにほかなりません。
ndarrayを作る最も基本的な方法は、既存のリストをnp.array関数に渡すことです。リストのリストを渡せば、行と列を持つ2次元配列になります。また、決まった形の配列を最初から生成する関数も多数用意されており、実務ではゼロ埋めの器を用意してから値を詰めていく、連番を生成する、といった場面でよく使います。代表的な生成関数を整理すると次のとおりです。
| 関数 | 役割 | 例 |
|---|---|---|
| np.array(リスト) | 既存のリストから配列を作る | np.array([1, 2, 3]) |
np.zeros(形状) | 全要素が0の配列を作る | np.zeros((2, 3)) |
np.ones(形状) | 全要素が1の配列を作る | np.ones((3,)) |
np.arange(開始, 終了, 刻み) | 連番(等差数列)を作る | np.arange(0, 10, 2) |
np.linspace(開始, 終了, 個数) | 区間を等分割した数列を作る | np.linspace(0, 1, 5) |
arangeとlinspaceはどちらも数列を作る点で似ていますが、指定する内容が異なります。arangeは「いくつ刻みで増やすか」を指定するのに対し、linspaceは「区間を何個の点に分割するか」を指定します。グラフのx軸の値を作るときなど、区間の両端を必ず含めた等間隔の点が欲しい場合はlinspaceの方が扱いやすく、後の可視化の章でも頻繁に登場します。
作成したndarrayが持つ、shapeとdtypeという2つの属性も必ず押さえておく必要があります。shapeは配列の形、つまり各次元の要素数をタプルで表したもので、1次元配列なら(5,)、3行4列の2次元配列なら(3, 4)のように表示されます。dtypeは配列に格納されている要素の型で、int64(64ビット整数)やfloat64(64ビット浮動小数点数)といった値を取ります。計算結果が想定と違う場合、原因の多くはshapeの不一致か、dtypeが意図せず整数になっていて小数点以下が切り捨てられている、といったところにあります。エラーや不可解な結果に出会ったら、まずshapeとdtypeを確認する癖をつけておくと、原因の切り分けが早くなります。

NumPyを使ううえで最も重要な発想の転換が、ベクトル化演算です。ベクトル化とは、配列の要素1つ1つに対してforループで処理を繰り返すのではなく、配列全体に対して一括で演算を適用する書き方を指します。例えばリストの各要素を2倍にしたい場合、素のPythonではリスト内包表記やforループで要素を1つずつ処理しますが、NumPyの配列であればarr * 2と書くだけで、全要素が一括で2倍になります。ループを自分で書く必要も、要素数を意識する必要もありません。
この違いは書きやすさだけでなく、実行速度にも大きく影響します。Jupyterの%timeitコマンド(第2章で扱った、セルの実行時間を計測するマジックコマンドです)を使って、100万件の数値を2倍にする処理を、forループ(リスト内包表記)とNumPyのベクトル化演算とで比較してみます。
import numpy as np
n = 1_000_000
python_list = list(range(n))
numpy_array = np.arange(n)
# 素のPython: リスト内包表記でforループ相当の処理を行う
%timeit [x * 2 for x in python_list]
# 実行結果の例: 44.5 ms ± 1.92 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# NumPy: 配列全体に対する一括処理(ベクトル化演算)
%timeit numpy_array * 2
# 実行結果の例: 2.12 ms ± 63.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
上の例では、ベクトル化演算がリスト内包表記の約20倍の速さになりました。倍率は配列の件数や、結果を格納するメモリを新たに確保するかどうかで大きく変わるため、手元で実行した場合は数十倍から百倍以上まで幅が出ます。桁で速くなる、という理解で十分です。この差は、forループが要素1つずつをPythonのオブジェクトとして処理するのに対し、ベクトル化演算は型が統一された配列に対してCレベルでまとめて処理を行うために生まれます。データの件数が数千件程度であれば体感できる差は小さいものの、実務で扱う数十万件、数百万件規模のデータでは、この差が処理時間の数十秒、数分という違いになって現れます。
NumPyを使ったコードを書いていて、配列の要素を1つずつ取り出すためのforループを書きたくなったときは、いったん手を止めて「同じ処理を実現するNumPyの機能がすでに用意されていないか」を探す価値があります。四則演算やsqrt(平方根)、集計、条件による絞り込みといった処理の多くは、ベクトル化された関数やメソッドとして用意されています。forループを避けてベクトル化された書き方に置き換えることを意識するだけで、コードは短くなり、実行速度も向上します。
ベクトル化演算は、同じ形の配列同士だけでなく、形の異なる配列同士でも一定の規則に従って成立します。この仕組みをブロードキャストと呼びます。最も分かりやすい例が、配列とスカラー(単一の数値)の演算です。arr * 2と書いたとき、2という1つの数値が、まるで配列と同じ形に引き伸ばされたかのように、配列の全要素それぞれに適用されます。実際にはNumPyの内部でこの引き伸ばしを効率よく処理しており、メモリ上に2を配列の要素数分だけコピーしているわけではありません。
スカラーと配列の組み合わせよりも少し複雑になるのが、行列(2次元配列)と、行または列に対応するベクトル(1次元配列)との組み合わせです。この場合の規則は、2つの配列の形状(shape)を末尾の次元から順に比較し、各次元のサイズが「一致している」か「どちらかが1である」場合に、サイズ1の次元がもう一方のサイズに合わせて引き伸ばされる、というものです。例えば形状が(3, 4)の行列と、形状が(4,)のベクトルを掛け合わせると、ベクトルの4という次元は行列の列数4と一致するため、このベクトルが3行分すべてに複製されたかのように扱われ、各行に対して同じベクトルが要素ごとに掛け合わされます。
この規則は、店舗別・商品別の売上データに商品ごとの単価や税率を掛け合わせるといった、実務の集計処理そのものです。3店舗×4商品の販売数量の行列に、商品ごとの単価ベクトル(4個の値)を掛けると、単価ベクトルが3店舗分にブロードキャストされ、店舗ごと・商品ごとの売上金額の行列が一度の演算で求まります。
import numpy as np
# 3店舗 × 4商品の月間販売数(行が店舗、列が商品)
sales_units = np.array([
[120, 85, 40, 60],
[95, 110, 55, 30],
[140, 70, 65, 45],
])
# 商品ごとの単価(4商品分、1次元配列)
unit_price = np.array([1200, 800, 3000, 1500])
# 単価ベクトル(形状(4,))が販売数の行列(形状(3,4))の各行にブロードキャストされる
sales_amount = sales_units * unit_price
print(sales_amount)
# スカラー(消費税率)はさらに単純なブロードキャストで、全要素に一律で適用される
tax_rate = 1.1
sales_amount_with_tax = sales_amount * tax_rate
print(sales_amount_with_tax.round().astype(int))
最後の行のastype(int)は、値のデータ型を整数に変換するメソッドです(型の変換については第7章で改めて扱います)。ここでは、小数として計算された税込金額を丸めたうえで整数に直しています。なおNumPyのroundは、端数がちょうど0.5のときに近いほうの偶数へ丸める方式(最近接偶数への丸め)なので、一般的な四捨五入とは0.5の扱いだけが異なります。
このコードでは、単価ベクトルという1本の配列を用意するだけで、店舗×商品のすべての組み合わせの売上金額が一括で計算されています。もしブロードキャストを使わずforループで書くとすれば、店舗のループの中に商品のループをネストさせる二重ループが必要になりますが、ブロードキャストを使えば1行の掛け算で完結します。行と列という2つの向きのどちらに対してブロードキャストが働いているのかを意識することが、ブロードキャストを直感的に理解するための近道です。
ndarrayから一部の要素を取り出す方法は、基本的にはPythonのリストと同じ考え方に従います。1次元配列であればarr[0]で先頭の要素、arr[-1]で末尾の要素、arr[1:3]でインデックス1から2までの範囲(スライス)を取り出せます。2次元配列になると、行と列をカンマで区切って同時に指定できる点が、リストのリストとは異なるNumPy特有の書き方です。arr[1, 2]と書けば2行目・3列目(インデックスは0始まりです)の要素が、arr[1, :]と書けば2行目全体が、arr[:, 0]と書けば1列目全体が取り出せます。
もう1つ、NumPyならではの強力な取り出し方がブールインデックスです。これは、配列に対する比較演算(arr > 100など)を行うと、各要素が条件を満たすかどうかをTrue/Falseで表した、元の配列と同じ形のブール配列が返ってくる性質を利用したものです。このブール配列を、元の配列に対するインデックスとしてそのまま使うことで、条件を満たす要素だけを一括で抜き出すことができます。複数の条件を組み合わせる場合は、Python標準のandやorではなく、&(かつ)や|(または)を使い、各条件をカッコで囲む必要がある点に注意してください。
import numpy as np
rng = np.random.default_rng(42)
daily_sales = rng.integers(50, 300, size=30) # 30日分の日次売上(万円、ダミーデータ)
# 比較演算(daily_sales >= 200)は各要素の判定をTrue/Falseで表したブール配列を返す
is_high_sales = daily_sales >= 200
print(is_high_sales)
# ブール配列をインデックスとして使うと、条件を満たす要素だけが取り出される
high_sales_days = daily_sales[is_high_sales]
print(high_sales_days)
print(f'200万円以上だった日数: {len(high_sales_days)}日 / 全{len(daily_sales)}日')
# 複数条件はandやorではなく&・|を使い、各条件をカッコで囲む
mid_range_days = daily_sales[(daily_sales >= 100) & (daily_sales < 200)]
print(mid_range_days)
「ある条件を満たす行だけを抜き出す」という、このブールインデックスの考え方は、次章以降で扱うPandasのデータ絞り込み(df[df[‘売上’] >= 200]のような書き方)の原型そのものです。Pandasの絞り込み構文がなぜあのような書き方になっているのか、その裏側の仕組みを理解しておくと、Pandasの操作も単なる暗記ではなく仕組みの理解として身につきます。
ndarrayには、合計・平均・標準偏差・最小値・最大値といった集計を行うメソッドが標準で備わっています。sum(合計)、mean(平均)、std(標準偏差)、min(最小値)、max(最大値)はいずれも、引数を何も指定しなければ配列全体の1つの値に集計します。しかし2次元以上の配列を扱う実務では、配列全体ではなく「行ごとに集計したい」「列ごとに集計したい」という場面がほとんどです。この、どの方向に沿って集計するかを指定する引数がaxisです。
axisの指定は最初は直感に反すると感じやすいところです。axis=0を指定すると、0番目の軸(行の方向)に沿って値をつぶすように集計するため、結果は列ごとの値になります。axis=1を指定すると、1番目の軸(列の方向)に沿って値をつぶすように集計するため、結果は行ごとの値になります。「axisで指定した番号の方向がつぶれて、残った方向の分だけ結果が並ぶ」と覚えておくと、行と列のどちらの集計なのか混乱しにくくなります。
| 指定 | つぶれる方向 | 結果の意味 |
|---|---|---|
| axis指定なし | 全要素 | 配列全体で1つの値 |
| axis=0 | 行方向(縦) | 列ごとの値(列の数だけ結果が並ぶ) |
| axis=1 | 列方向(横) | 行ごとの値(行の数だけ結果が並ぶ) |
import numpy as np
# 4店舗 × 3ヶ月の売上高(行が店舗、列が月、単位は万円)
sales = np.array([
[420, 460, 510],
[380, 395, 400],
[610, 590, 630],
[275, 300, 310],
])
print('店舗ごとの3ヶ月合計(axis=1、列方向がつぶれて行ごとの値になる):')
print(sales.sum(axis=1))
print('月ごとの全店舗合計(axis=0、行方向がつぶれて列ごとの値になる):')
print(sales.sum(axis=0))
print('全店舗・全期間を通じた平均売上:', sales.mean())
print('店舗ごとの月次売上の標準偏差(axis=1):', sales.std(axis=1).round(1))
axis=1で店舗ごとの合計(4つの値)が、axis=0で月ごとの合計(3つの値)が求まる様子が確認できます。このaxisという考え方は、次章以降で扱うPandasのgroupbyや集計処理でも同じ形で登場します。行方向・列方向のどちらに沿って集計しているのかを常に意識する習慣は、NumPyだけでなくPandasを扱ううえでも必須の感覚になるため、この章でしっかり体に馴染ませておく価値があります。

ここまでのコード例で何度か使ってきたnp.random.default_rng(シード値)という書き方について、あらためて説明します。default_rngは、NumPyが現在推奨している乱数生成器の作り方で、rng = np.random.default_rng(42)のようにシード値(乱数の元になる数値)を指定してrngというオブジェクトを作り、そのrngに対してrng.integers(整数の乱数)やrng.random(0以上1未満の小数の乱数)、rng.normal(正規分布に従う乱数)といったメソッドを呼び出して使います。古いバージョンのNumPyで使われていたnp.random.seed()やnp.random.randint()といった、モジュール全体に対して直接シードを設定する書き方は、現在はレガシー扱いとされ、公式にはdefault_rngを使う書き方が推奨されています。警告は出ず今も動作しますが、新しく書くコードではdefault_rngを使うのが標準です。
シード値を固定する理由は、乱数を使った処理であっても、結果を再現できるようにするためです。シード値を指定せずに乱数を生成すると、実行するたびに異なる値が生成されますが、同じシード値を指定すれば、何度実行しても全く同じ乱数の列が得られます。本章のコード例で一貫してdefault_rng(42)のように固定のシード値を使ってきたのはこのためで、読者が同じコードを手元で実行したときに、本ガイドの実行結果と同じ数値を再現できるようにする狙いがあります。分析結果の検証や、チームメンバーとの結果の共有、バグの再現といった場面でも、乱数のシードを固定しておくことは実務上の基本作法です。
実務のデータには、欠測や未入力によって値が存在しない箇所がしばしば含まれます。NumPyでは、このような欠損した数値をnp.nan(Not a Numberの略、浮動小数点数の一種として扱われます)という特別な値で表します。np.nanが配列の中に1つでも含まれていると、その配列に対してsumやmeanといった通常の集計メソッドを呼び出した場合、計算結果全体がnanになってしまいます。これは、nanが混ざった計算の結果は信頼できない値になる、というNumPyの安全側の設計によるものです。
欠損を除外したうえで集計したい場合には、通常の集計関数の代わりに、nansum、nanmean、nanstdといった、頭にnanが付いた専用の関数を使います。これらの関数はnp.nanを無視して、残りの有効な値だけを対象に集計を行います。また、配列のどの要素がnp.nanであるかを判定するにはnp.isnanという関数を使い、これによって欠損の有無を確認したり、欠損している要素だけを取り出したりすることができます。ここで扱った欠損値の考え方は入り口にすぎず、実務のデータに含まれる欠損をどう見つけ、どう補完し、どう扱うかという本格的な議論は、次章以降のPandasの章であらためて詳しく取り上げます。
NumPyの核心はベクトル化演算です。配列の要素を1つずつforループで処理するのではなく、配列全体に対して一括で演算を適用することで、コードは短くなり、実行速度も大幅に向上します。ブロードキャストによって形の違う配列同士も一定の規則で計算でき、axisの指定によって行方向・列方向を選んで集計できます。この「ループを書かずに配列全体で考える」という発想と、axisという行・列の感覚は、次章以降で学ぶPandasにもそのまま引き継がれる、データサイエンスの基礎体力にあたる部分です。
『現場で使える!NumPyデータ処理入門 第2版』(吉田拓真・尾原颯、翔泳社):ndarrayの生成からベクトル化演算、ブロードキャスト、軸(axis)を指定した集計、線形代数や画像処理への応用まで、NumPyの機能を実務のユースケースに沿って幅広く解説している書籍です。本章で扱った内容をひととおり理解したうえで、より高度な配列操作や画像・信号データへの応用まで学びを広げたい読者に向いています。
第6章では、NumPyによる配列演算とベクトル化の考え方を扱いました。しかし、実務で日々向き合うデータの多くは、単なる数値の羅列ではなく、行と列からなる表形式です。売上明細、顧客台帳、アンケート回答、センサーのログ、どれも「行=1件のレコード」「列=項目」という構造を持っています。本章から扱うPandasは、この表形式データをPython上で自在に扱うためのライブラリです。
Pandasは内部的にNumPyの配列を利用して作られており、第6章で学んだベクトル化演算の考え方はそのままPandasにも引き継がれます。表計算ソフトのExcelで日常的に行っている「フィルタをかける」「並べ替える」「集計する」といった操作を、コードとして記述し、再現可能な形で残せるようにするのがPandasの役割だとイメージすると理解しやすくなります。手作業では数分かかる集計も、一度コードにしてしまえば同じ処理を何度でも再実行でき、途中の判断根拠もコードとして残ります。
Pandasには中心となるデータ構造が2つあります。1つ目はSeriesで、これは1列分のデータに相当します。ラベル(インデックス)付きの1次元配列だと考えてください。2つ目はDataFrameで、複数の列を横に並べた2次元の表です。DataFrameの各列は、実はそれぞれが1つのSeriesになっています。つまり、DataFrameは「同じ行インデックスを共有する複数のSeriesの集まり」だと捉えると、両者の関係がすっきり理解できます。実際、DataFrameから1列だけを取り出すと戻り値はSeries型になり、複数列を取り出すと戻り値はDataFrame型のままです。この「1列ならSeries、複数列ならDataFrame」という戻り値の違いは、以降のコードで型を意識するうえでの基本知識になります。
Excelのシートに例えるなら、シート全体がDataFrame、そのうちの1つの列がSeriesに対応します。Excelと異なるのは、行と列の両方に名前(インデックス)を持たせられる点、そして列ごとに異なるデータ型(数値、文字列、日付など)を厳密に管理できる点です。この型の管理が、後述する欠損値の扱いや集計の正確さに直結します。
実務でPandasを使う場面のほとんどは、まずCSVファイルやExcelファイルを読み込むところから始まります。CSVを読み込む関数がread_csvです。read_csvには数十種類の引数がありますが、入門段階でまず押さえておきたいのは次の3つです。
sep="\t")や、日本の一部業務システムが出力するセミコロン区切りなど、カンマ以外の区切り文字を使うファイルも存在しますこの中でも、日本語データを扱う入門者が最初につまずきやすいのが文字コードの壁です。Windows環境でExcelから「CSV形式で保存」を選ぶとshift_jis(cp932)系で保存されることが多く、これをencodingの指定なしでread_csvに渡すと文字化けやエラーの原因になります。読み込みに失敗した場合は、encoding=”cp932″やencoding=”shift_jis”、あるいはencoding=”utf-8-sig”(先頭にBOMと呼ばれる印が付いたUTF-8)を順に試して切り分けるのが実務での定石です。Excel形式のファイル(.xlsxなど)を読み込む場合はread_excelを使います。シートが複数ある場合はsheet_name引数でシート名または番号を指定します。読み込みにはopenpyxlという別ライブラリが必要になる場合があり、環境によってはpip install openpyxlが求められます。

ファイルを読み込んだら、いきなり分析に入るのではなく、まずデータの全体像を確認する習慣が重要です。次のコードは、この最初の確認作業の定番の流れを示しています。
import pandas as pd
# encoding="utf-8"を明示して読み込む(文字化けする場合はcp932等に変更する)
df = pd.read_csv("sales.csv", encoding="utf-8")
print(df.head()) # 先頭5行を確認(既定は5行、引数で行数を指定可能)
print(df.tail(3)) # 末尾3行を確認
print(df.shape) # (行数, 列数)のタプル
df.info() # 各列のデータ型・欠損の有無・メモリ使用量の概要
print(df.describe()) # 数値列の件数・平均・標準偏差・最小最大等の要約統計量
head/tailで実際の値を目で確認し、shapeで想定した件数と列数を確かめ、infoで各列のデータ型と欠損の有無を把握し、describeで数値の大まかな分布を掴む、という4点セットは、どんなデータを受け取ったときにも最初に実行する基本動作です。infoの出力でDtype(データ型)を確認する際、本来数値であるはずの列が文字列扱いになっている場合(pandas 2系ではobject、pandas 3系ではstrと表示されます)は、数値の中にカンマや単位などの余計な文字が混入している可能性が高く、後述する型変換で対処が必要になります。
行を選ぶ操作では、locとilocという2つの方法を使い分けます。この2つは、初めてPandasに触れる人が戸惑いやすい部分だと感じています。名前はよく似ていますが、行や列を指定する基準がまったく異なります。locはラベル(名前)を基準に、ilocは位置(0始まりの通し番号)を基準に指定する、という違いをまず押さえることが理解の出発点になります。
この違いが分かりにくくなるのは、行インデックスがもともと0, 1, 2, …という連番になっている場合です。この場合、loc[2]とiloc[2]はたまたま同じ行を返すため、違いに気づかないことがあります。しかし、フィルタリングや並べ替えを行った後のDataFrameでは、行インデックスの並びが崩れ、0, 3, 7, 1のように連番でなくなります。このとき、loc[2]は「インデックスの値が2という名前の行」を探すのに対し、iloc[2]は「上から3番目(0始まりで2番目)の行」を指し、両者の結果は食い違います。実務でも、並べ替えやフィルタの後にこの2つを混同する事故が起こりやすいポイントです。
import pandas as pd
df = pd.DataFrame({
"商品名": ["りんご", "バナナ", "メロン", "ぶどう"],
"価格": [120, 98, 250, 480],
"在庫数": [50, 120, 8, 30],
}, index=[10, 11, 12, 13]) # あえて連番でないインデックスを設定
print(df.loc[12]) # インデックスの「値」が12の行(名前で指定)
print(df.iloc[2]) # 上から3番目(0始まりで2番目)の行(位置で指定)
# 列も同時に指定する場合
print(df.loc[12, "価格"]) # 行=名前12、列=名前「価格」
print(df.iloc[2, 1]) # 行=位置2、列=位置1(0始まり)
# スライスの挙動にも違いがある点に注意
print(df.loc[10:12]) # locのスライスは終端(12)を「含む」
print(df.iloc[0:2]) # ilocのスライスは終端(2)を「含まない」(Python標準と同じ)
最後の2行が示すとおり、スライス表記(コロンで範囲を指定する書き方)の挙動もlocとilocで異なります。locのスライスは指定した終端のラベルを含む一方、ilocのスライスはPythonのリストと同じく終端を含みません。迷った場合は「名前で選ぶならloc、順番で選ぶならiloc」という原則に立ち返ることが、混乱を避ける近道です。
特定の条件を満たす行だけを取り出す操作は、実務での分析作業のうちもっとも頻度が高い操作の1つです。Pandasでは、条件式を書くとTrue/Falseが並んだブール型のSeriesが返り、これをDataFrameの角括弧に渡すことで、条件を満たす行だけに絞り込めます。この仕組みをブールインデックス(boolean indexing)と呼びます。複数条件を組み合わせるときはPython標準のandやorではなく、記号の&(かつ)と|(または)を使い、各条件を必ず括弧で囲みます。Pandasは行ごとに要素単位でTrue/Falseを比較する必要があり、単一の真偽値同士の比較しか想定していないandやorはSeries同士の比較には使えません。また&や|は演算子としての優先順位が比較演算子(>や==など)より高く、括弧を省略すると意図しない範囲が先に評価され、エラーや誤った結果につながります。特定のリストに含まれるかどうかで絞り込みたい場合はisinが、|を重ねるより簡潔です。条件が複雑になってきたら、文字列で条件を書けるqueryを使うとコードの見通しが良くなります。次の「列の追加と演算」の節で、これらのフィルタリングを実際のコードとともに確認します。
第6章で学んだNumPyのベクトル化演算の考え方は、Pandasの列同士の計算にもそのまま引き継がれます。列同士を四則演算するだけで、要素ごとの計算結果を持つ新しいSeriesが得られ、これをDataFrameに代入すれば新しい列として追加されます。1件ずつforループで処理する必要はありません。次のコードでは、ここまでのフィルタリングの書き方とあわせて、新しい列の追加、astypeによる型変換までを一通り確認します。
import pandas as pd
import numpy as np
df = pd.DataFrame({
"商品名": ["りんご", "バナナ", "メロン", "ぶどう", "みかん"],
"価格": [120, 98, 2500, 480, 150],
"在庫数": [50, 120, 8, 30, 200],
"産地": ["青森", "フィリピン", "静岡", "山梨", "愛媛"],
})
# 複数条件によるフィルタリング: &(かつ)、|(または)を使い、各条件は括弧で囲む
filtered = df[(df["価格"] > 100) & (df["在庫数"] < 100)]
print(filtered)
# isin: 特定のリストに含まれるかを判定する
print(df[df["産地"].isin(["青森", "静岡", "愛媛"])])
# query: 条件を文字列で書けるため、複雑な条件が読みやすくなる
print(df.query("価格 > 100 and 在庫数 < 200"))
# ベクトル化演算で新しい列を作る(forループは不要)
df["数量"] = [3, 6, 1, 2, 10]
df["売上金額"] = df["価格"] * df["数量"]
df["価格帯"] = np.where(df["価格"] >= 500, "高額", "通常")
print(df)
print(df.dtypes) # 各列のデータ型を確認
# astypeによる型変換の典型例(桁区切りのカンマが混ざった文字列を数値化する)
raw = pd.Series(["1,200", "980", "25,000"])
cleaned = raw.str.replace(",", "", regex=False).astype("float64")
print(cleaned)
astypeは列のデータ型を明示的に変換したいときに使います。CSVから読み込んだ数値列が、桁区切りのカンマや単位の文字が混じっているために文字列として読み込まれてしまうことは実務でよく起こります(表示はpandas 2系がobject、3系がstrです)。この場合、str.replaceで余分な文字を取り除いてからastypeで数値型に変換するという、上記コードの最後の3行のような処理が定番の対処法です。型が正しく数値になっていないと、集計や統計計算でエラーになったり、文字列としての意図しない結合になったりするため、info()で型を確認する習慣と合わせて覚えておくべき操作です。

実データには、欠損値(値が存在しないセル)がほぼ必ず含まれます。アンケートの未回答、センサーの通信断、入力漏れなど、原因は様々です。Pandasでは欠損値はNaN(Not a Number)として表現され、これを扱う一連のメソッドが用意されています。まず、どの列にどれだけ欠損があるかを確認するには、isna()とsum()を組み合わせます。isna()は各セルが欠損かどうかをTrue/Falseで返し、これを列ごとに合計すると欠損の件数が分かります。
import pandas as pd
import numpy as np
df = pd.DataFrame({
"商品名": ["りんご", "バナナ", None, "ぶどう"],
"価格": [120, np.nan, 250, 480],
"在庫数": [50, 120, 8, np.nan],
})
print(df.isna()) # セルごとにTrue/Falseで欠損の有無を返す
print(df.isna().sum()) # 列ごとの欠損件数を集計
# dropna: 欠損を含む行(既定)を削除する
df_dropped = df.dropna()
# fillna: 欠損を特定の値で埋める
df_filled = df.copy()
df_filled["価格"] = df_filled["価格"].fillna(df_filled["価格"].mean())
df_filled["商品名"] = df_filled["商品名"].fillna("不明")
# 時系列データなどでは、直前の値で埋めるffill、直後の値で埋めるbfillも使う
# (fillna(method="ffill")はpandas 2.1で非推奨、3.0で削除された。現行のAPIではffill()を直接呼び出す)
df_filled["在庫数"] = df_filled["在庫数"].ffill()
fillnaで欠損を埋める際、以前はfillna(method="ffill")のようにmethod引数を指定する書き方が使われていましたが、この書き方はpandas 2.1で非推奨となり、pandas 3.0で削除されました。現行のバージョンではffill()やbfill()を直接呼び出します。ffillは直前の値で埋める、bfillは直後の値で埋める処理で、株価や気温のように時間的な連続性があるデータの欠損補完でよく使われます。
欠損値をどう扱うかに唯一の正解はありません。行ごと削除するか、平均値で埋めるか、前後の値で補完するかによって、その後の分析結果や集計値は変わってしまいます。欠損の発生理由(単なる入力漏れなのか、値が0であることを意味するのか、そもそも計測対象外なのか)を確認したうえで判断するという分析上の判断が必要になる作業だという認識を持つことが重要です。
データを特定の列の値順に並べ替えるにはsort_valuesを使います。ascending=Falseを指定すると降順(大きい順)になります。複数の列を指定すれば、1つ目の列で同じ値が並んだ場合に2つ目の列で並べ替える、という多段階の並べ替えも可能です。カテゴリ列に含まれる値の種類と、それぞれの出現回数を確認したい場合はvalue_counts()が便利です。カテゴリごとの件数を素早く把握したいときによく使う操作で、groupbyを使った本格的な集計は次章で扱いますが、単一の列に対する簡易的な集計であればvalue_counts()だけで十分なことも多くあります。重複を除いた値の一覧を得たい場合はunique()、種類の数だけを知りたい場合はnunique()を使います。たとえばdf.sort_values("価格", ascending=False)で価格の高い順に並べ替え、df[“産地”].value_counts()で産地ごとの件数を、df[“産地”].nunique()で産地の種類数を確認する、といった具合です。
加工が終わったDataFrameをファイルとして書き出すには、to_csv、to_excelを使います。ここで初心者が見落としやすいのがindex引数です。Pandasの既定の挙動では、DataFrameの行インデックス(多くの場合は単なる連番)もファイルの1列として書き出されてしまいます。これを避け、意図した列だけを保存するには、df.to_csv("output.csv", index=False)のようにindex=Falseを指定するのが定番の書き方です。encodingにutf-8-sigを指定すると、Windows版のExcelで直接開いたときの文字化けを防げます。utf-8-sigはUTF-8の先頭にBOMと呼ばれる印を付けた形式で、Excelがこの印を見て文字コードを正しく判定できます。社内でExcelを使う関係者にCSVを渡す場面では覚えておくと役立つ設定です。Excel形式で保存したい場合は、df.to_excel("output.xlsx", index=False, sheet_name="集計結果")のように、sheet_name引数でシート名を指定します。
ここまで見てきた個々の操作を、実務でよくある一連の流れとしてつなげてみます。売上のCSVファイルを読み込み、型を確認し、欠損を処理し、条件で絞り込み、新しい列を作り、最後に結果を保存するという流れです。この一連の手順は、日々の売上集計やレポート作成の骨格としてそのまま応用できます。

import pandas as pd
import numpy as np
# 1. 読み込み(日本語CSVを想定し、まずutf-8で試す)
df = pd.read_csv("sales_raw.csv", encoding="utf-8")
# 2. 型を確認する
df.info()
print(df.describe())
# 3. 欠損の状況を確認し、方針を決めて処理する
print(df.isna().sum())
# 数量が欠損している行は集計に使えないため削除する
df = df.dropna(subset=["数量"])
# 単価が欠損している場合は、同じ商品の平均単価で補完する
df["単価"] = df.groupby("商品名")["単価"].transform(lambda s: s.fillna(s.mean()))
# 4. 条件で絞り込む(キャンセルされた注文を除外し、対象期間のみを残す)
df = df[df["ステータス"] != "キャンセル"]
df["注文日"] = pd.to_datetime(df["注文日"]) # 文字列のままの比較は事故のもとなので日付型に変換してから絞り込む
df = df[df["注文日"] >= "2026-01-01"]
# 5. 新しい列を作る(ベクトル化演算で売上金額を計算し、価格帯を分類する)
df["売上金額"] = df["単価"] * df["数量"]
df["価格帯"] = np.where(df["単価"] >= 3000, "高額", "通常")
# 6. 並べ替えと簡易集計で内容を確認する
print(df.sort_values("売上金額", ascending=False).head())
print(df["価格帯"].value_counts())
# 7. 保存する(index=Falseで余計な列を持ち込まない)
df.to_csv("sales_cleaned.csv", index=False, encoding="utf-8-sig")
3番目の手順で使っているtransformは、グループごとに計算した結果を、元の表と同じ行数のまま返すメソッドです。合計や平均を求める通常の集計は表が縮んでグループの数だけの行になりますが、transformは「この行が属するグループの平均値」をすべての行に配り直してくれるため、欠損の穴埋めのように元の表の形を保ったまま値を入れたい場面に向いています。
この流れの中では、groupbyとtransformを使って商品ごとの平均単価で欠損を補完するという、単純な全体平均よりも一歩踏み込んだ補完方法も紹介しました。groupbyの詳しい使い方は次章で本格的に扱いますが、欠損処理の場面でもこうした応用が可能だという点は押さえておくと役立ちます。
読み込み(read_csv)、確認(head・info・describe)、選択(loc・iloc)、絞り込み(ブールインデックス・query)、演算(ベクトル化・astype)、欠損処理(isna・dropna・fillna)、並べ替えと集計(sort_values・value_counts)、保存(to_csv)という一連の操作は、Pandasによるデータ加工の基本セットです。次章では、複数の表を結合するmerge、グループごとに集計するgroupby、縦横の形を変換するpivot_tableなど、より実践的な操作を扱います。
『Pythonによるデータ分析入門 第3版』(Wes McKinney、オライリー・ジャパン):Pandasの開発者自身による定番書であり、本章で扱ったSeriesとDataFrameの基本構造から、locとilocの使い分け、欠損値処理まで、公式の設計思想に沿って体系的に解説されています。本章の内容を一通り終えたあと、細かな挙動の理由まで理解を深めたい読者に向いています。
前章では、SeriesとDataFrameという2つの基本データ構造、条件によるフィルタリング、欠損値の扱いといった、Pandasの土台となる操作を扱いました。表を読み込み、必要な行を取り出し、欠けている値を処理する。ここまでできれば、1枚の表の中で完結する作業には困らなくなります。ただ、実務のデータ分析の多くは、1枚の表を眺めて終わりということはほとんどありません。売上明細を商品カテゴリ別・月別に集計し直す、複数のシステムから出力された表を1つに結合する、月ごとに分かれたファイルを積み上げて1年分の表にする、機械学習にかける前にデータの並びを組み替える。こうした集計・結合・整形の繰り返しが、実務のデータ分析の大半を占めていると考えています。
本章では、この3つの作業をPandasでどう行うかを、順を追って扱います。中心となるのはgroupbyによる集計です。Excelのピボットテーブルを手作業で作っていた工程を、コードとして自動化・再現可能にする操作だとイメージしていただくと、位置づけがつかみやすいと思います。そのうえで、クロス集計に特化したpivot_table、複数の表を横につなぐmerge、複数の表を縦や横に積み上げるconcat、縦持ちと横持ちを行き来するmeltとpivot、そして組み込み関数やベクトル化演算を優先しながら必要な場面でapplyを使うという考え方を扱います。最後に、売上明細と商品マスタを結合してレポートに仕上げるまでの一連の流れを、実務のミニ通し例として示します。
groupbyは、Pandasの集計操作の中核をなす機能です。その処理は「分割(split)」「適用(apply)」「結合(combine)」という3つの段階に分けて理解すると見通しがよくなります。まず、指定したキー列の値ごとに、元の表をいくつかの小さな表に分割します。次に、分割されたそれぞれの小さな表に対して、合計や平均といった何らかの処理を適用します。最後に、各グループで得られた結果を1つの表にまとめ直します。この一連の流れは、Excelのピボットテーブルで「行にカテゴリを置き、値の欄に売上の合計を指定する」という操作と本質的に同じです。groupbyは、この一連の作業をコードとして記述し、同じ処理を何度でも同じ手順で再現できるようにする仕組みだと捉えていただくとよいと思います。

最も基本的な使い方は、1つのキー列でグループ化し、1つの列に対して1つの集計関数を適用する形です。df.groupby('category')[‘sales’].sum()と書けば、category列の値ごとにsales列を合計したSeriesが得られます。キーは複数指定することもでき、df.groupby(['category', 'month'])のように書けば、カテゴリと月の組み合わせごとの集計になります。複数のキーを指定した場合、結果はMultiIndex(階層的な行ラベル)を持つ表になる点は覚えておくとよいと思います。
1つのグループに対して複数の集計を同時に行いたい場合には、aggメソッドを使います。.agg(['sum', 'mean', 'count'])のようにリストで関数名を渡せば、合計・平均・件数をまとめて計算できます。さらに実務でよく使うのが、名前付き集計(named aggregation)という書き方です。.agg(total_sales=('sales', 'sum'), avg_sales=('sales', 'mean'))のように、出力する列名と、対象列・集計関数の組を「列名, 関数名」のタプルで指定します。この書き方の利点は、集計後の列名を集計の内容に沿ったわかりやすい名前に自分で決められる点です。列ごとに異なる集計関数を適用したい場合(例えば売上は合計を、数量は平均を取りたい場合)にも、named aggregationは同じ書き方で自然に対応できます。
import pandas as pd
df = pd.DataFrame({
'category': ['家電', '家電', '食品', '食品', '食品', '衣料'],
'month': ['2026-05', '2026-06', '2026-05', '2026-05', '2026-06', '2026-06'],
'sales': [120000, 98000, 45000, 32000, 51000, 76000],
'qty': [12, 9, 300, 210, 340, 40],
})
# 単一キー・単一集計
by_category = df.groupby('category')['sales'].sum()
print(by_category)
# 複数キー・複数集計(named aggregation)
summary = df.groupby(['category', 'month']).agg(
total_sales=('sales', 'sum'),
avg_sales=('sales', 'mean'),
total_qty=('qty', 'sum'),
n_records=('sales', 'count'),
).reset_index()
print(summary)
reset_index()を最後に呼んでいるのは、groupbyの結果ではグループ化に使ったキーが行ラベル(インデックス)になっており、そのままでは通常の列として扱いにくいためです。reset_index()によってキーを通常の列に戻すことで、後続の結合や書き出しの処理がしやすくなります。実務でgroupbyの結果を使い回す際には、ほぼ習慣的にreset_index()をセットで書くことになると思います。
groupbyが「グループごとに1つの縦に長い集計表」を作るのに対し、pivot_tableは「行と列の2方向に値を展開したクロス集計表」を作る機能です。例えば、行に月、列にカテゴリを並べ、その交差するセルに売上合計を表示する、という表計算ソフトでおなじみの形は、pivot_tableでそのまま再現できます。pd.pivot_table(df, values='sales', index='month', columns='category', aggfunc='sum')のように、集計対象の値(values)、行に使う列(index)、列に使う列(columns)、集計関数(aggfunc)の4つを指定するのが基本の形です。
pivot_tableには、行・列それぞれの合計を自動で追加するmargins引数も用意されています。margins=Trueとすると、各行・各列の末尾に合計行・合計列が追加され、margins_name引数で合計行・合計列のラベル名を指定できます(既定では’All’ですが、日本語の記事や資料に載せる際は margins_name=’合計’ のように指定すると読みやすくなります)。この機能を使えば、月別・カテゴリ別の内訳と、月合計・カテゴリ合計・総合計を1つの表で同時に確認できます。
groupbyとpivot_tableのどちらを使うべきか迷う場面もあると思いますが、大まかな使い分けの目安は次のとおりです。集計結果をさらにプログラムで処理する(別の計算に使う、機械学習の入力にする等)場合は、縦に長い形のまま扱えるgroupbyのほうが後工程との相性がよく、集計結果を人が眺めるための一覧表・レポートとして仕上げたい場合は、行×列に展開されたpivot_tableのほうが直感的に読みやすい、という違いです。実際には、pivot_tableは内部的にgroupbyとunstack(縦持ちを横持ちに展開する処理)を組み合わせたものに相当しており、両者は対立する機能というより、同じ集計の異なる見せ方だと捉えていただくとよいと思います。
report = pd.pivot_table(
df,
values='sales',
index='month',
columns='category',
aggfunc='sum',
fill_value=0,
margins=True,
margins_name='合計',
)
print(report)
fill_value=0を指定しているのは、ある月にカテゴリの売上が存在しない場合、そのセルが欠損値(NaN)になってしまうためです。売上がなかったことが明確な場面では、欠損のまま残すより0で埋めたほうが、後で合計や平均を計算する際に扱いやすくなります。
実務のデータは、1枚の表だけで完結しないことがほとんどです。売上明細には商品IDだけが記録されていて、商品名やカテゴリは別の商品マスタ表に入っている、といった状態はよくあります。この2枚の表を、共通のキー列(この例では商品ID)をもとに1枚に横つなぎする操作がmergeです。
Excelでの作業に置き換えると、商品IDをキーにしてVLOOKUPやXLOOKUPで商品名を引いてくる操作にあたります。違いは、mergeなら商品名とカテゴリのような複数の列を一度に、しかも表の全行に対してまとめて引ける点です。
SQLに触れたことがない読者のために、結合の考え方を図解のイメージで説明します。内部結合(inner join)は、両方の表に共通するキーの行だけを残す結合方法です。売上明細にはあるが商品マスタには載っていない商品ID(例えば廃盤商品)があった場合、その行は結果から落ちます。一方、左結合(left join)は、左側に指定した表(通常はメインの表、ここでは売上明細)の行をすべて残し、右側の表(商品マスタ)にキーが見つからなければ、その部分をNaN(欠損値)で埋めるという結合方法です。実務でのレポート作成では、売上明細の行を1件も失いたくないケースが大半のため、left joinがデフォルトの選択肢になることが多いと思います。

merge操作で実務上もっとも注意すべき事故は、キーの重複による行の増殖です。結合キーが片方の表で重複していると、結合結果の行数は単純な合計ではなく、キーが一致する組み合わせの数だけ膨れ上がります。例えば、商品マスタの中に同じ商品IDが2行存在していた場合(データ登録の誤りなどでまれに起こります)、その商品IDを持つ売上明細の行は、結合後に2行に複製されてしまいます。これに気づかないまま合計金額などを再計算すると、実際の売上より大きい数字が出来上がってしまい、原因の特定に時間を取られることになります。
この事故を防ぐための基本的な作法が、結合前後の行数を確認することです。left joinであれば、結合後の行数は結合前の左側の表の行数と一致するはずです。もし増えていれば、右側の表のキーに重複がある証拠であり、集計や検証を進める前に必ず確認する必要があります。pandasのmergeにはvalidate引数が用意されており、’one_to_one’(1対1)、’one_to_many’(1対多)、’many_to_one’(多対1)、’many_to_many’(多対多)のいずれかを指定すると、想定した対応関係と異なる場合にエラーを送出してくれます。ただし’many_to_many’だけは指定しても検査が行われないため、行の増殖を防ぐ目的で使うなら’one_to_one’・’one_to_many’・’many_to_one’のいずれかを指定します。想定外の重複を早い段階で検知できるため、実務で使う結合には積極的に指定することをおすすめします。
import pandas as pd
sales_detail = pd.DataFrame({
'order_id': [1001, 1002, 1003, 1004],
'product_id': ['P001', 'P002', 'P003', 'P999'], # P999はマスタに存在しない想定
'sales': [12000, 8000, 15000, 3000],
})
product_master = pd.DataFrame({
'product_id': ['P001', 'P002', 'P003'],
'product_name': ['ノートPC', 'マウス', 'モニター'],
'category': ['家電', '家電', '家電'],
})
n_before = len(sales_detail)
merged = pd.merge(
sales_detail,
product_master,
on='product_id',
how='left',
validate='many_to_one', # マスタ側でキーが重複していればここでエラーになる
)
n_after = len(merged)
print(f'結合前: {n_before}行 / 結合後: {n_after}行')
assert n_before == n_after, '行数が変化しています。キーの重複を確認してください'
print(merged)
この例では、P999という商品マスタに存在しない商品IDがあるため、left joinの結果ではproduct_nameとcategoryがNaNになった行が1つ残ります。行数そのものは変化しないため、上記のassert文は通過しますが、NaNになった行を放置すると後続の集計から漏れ落ちてしまいます。マスタに登録漏れがある行をisna()などで洗い出し、原因を確認したうえで、除外するのか、暫定的な値で埋めるのかを判断する作業も、実務のmergeにはつきものです。
merge後にまず行うべきことは、結合前後の行数を突き合わせることと、キーが一致しなかった行(NaNになった行)の有無を確認することです。この2点を確認する習慣を持っておくだけで、行の増殖や登録漏れによる集計ミスの大半は未然に防げます。
mergeがキーをもとに表を横につなぐ操作であるのに対し、concatは複数の表を単純に積み上げる操作です。もっとも使用頻度が高いのは縦積み(axis=0、既定値)で、例えば1月分・2月分・3月分と月ごとに分かれて出力される売上ファイルを、pd.read_csvで個別に読み込んでから、pd.concat([df_jan, df_feb, df_mar], ignore_index=True)のようにまとめて1年分の表に結合する、といった使い方をします。ignore_index=Trueを指定すると、結合前の各表が持っていた行インデックスを振り直し、0から始まる連番のインデックスに揃え直してくれます。指定を忘れると、結合後の表に重複したインデックスが残ってしまうので、複数ファイルを縦積みする際にはほぼ必須のオプションだと考えておくとよいと思います。
もう1つの使い方が横並び(axis=1)です。行の順序(インデックス)が揃っている複数の表を、列として横に並べてつなぎ合わせます。例えば、同じ顧客リストに対して、売上に関する集計結果の表と、問い合わせ件数に関する集計結果の表を、顧客IDの並び順を揃えたうえでpd.concat([df_sales, df_inquiry], axis=1)のように結合する場面などが該当します。ただし、横並びのconcatは行の対応関係をインデックスの一致だけに頼るため、両者の並び順や行数が食い違っていると、意図しない組み合わせで結合されてしまう危険があります。キーを明示して対応関係を保証できる場面ではmergeを、単純に列を追加するだけで対応関係にリスクがない場面ではconcatの横並びを、というように使い分けるのが安全だと思います。
データの持ち方には、大きく分けて縦持ち(long format)と横持ち(wide format)という2つの形があります。横持ちは、1行が1つの分析単位(例えば1つの月)を表し、カテゴリごとの値がそれぞれ別の列に並ぶ形です。人が一覧表として眺めるにはこちらのほうが見やすく、先ほどのpivot_tableの出力はまさに横持ちの形になっています。一方の縦持ちは、1行が1つの観測値(例えば「ある月・あるカテゴリ・その売上」の組)を表し、カテゴリという情報自体が1つの列の値として格納される形です。
横持ちから縦持ちへの変換を行うのがmeltです。df.melt(id_vars=['month'], var_name='category', value_name='sales')のように書くと、id_varsに指定した列(変換せずそのまま残す列)以外の各列名がcategoryという列の値に、それぞれの値がsalesという列の値になり、表が縦に長く展開されます。逆に、縦持ちから横持ちへ戻すのがpivotです。df.pivot(index='month', columns='category', values='sales')のように書けば、meltで展開した表を元の横持ちの形に戻せます。なお、pivotは指定したindexとcolumnsの組み合わせが重複していない(1組につき値が1つしかない)ことを前提とする機能で、重複がある場合はエラーになります。重複がある、あるいは集計をしながら横持ちに変換したいという場合には、先に説明したpivot_table(集計関数aggfuncを指定できる版のpivot)を使うのが適切です。
import pandas as pd
# 横持ち(wide): 1行が1つの月、カテゴリごとの売上が別々の列に並ぶ
wide = pd.DataFrame({
"month": ["2026-05", "2026-06"],
"家電": [120000, 98000],
"食品": [45000, 51000],
"衣料": [0, 76000],
})
print(wide)
# month 家電 食品 衣料
# 0 2026-05 120000 45000 0
# 1 2026-06 98000 51000 76000
# 横 → 縦
long = wide.melt(id_vars=["month"], var_name="category", value_name="sales")
print(long)
# month category sales
# 0 2026-05 家電 120000
# 1 2026-06 家電 98000
# 2 2026-05 食品 45000
# 3 2026-06 食品 51000
# 4 2026-05 衣料 0
# 5 2026-06 衣料 76000
# 縦 → 横(元に戻る)
back = long.pivot(index="month", columns="category", values="sales")
print(back)
# category 家電 衣料 食品
# month
# 2026-05 120000 0 45000
# 2026-06 98000 76000 51000
横持ちの表は2行4列でしたが、meltを通すと6行3列になりました。月とカテゴリの組み合わせの数だけ行ができ、売上の値がsalesという1つの列に集まっています。id_varsに指定したmonthだけがそのまま列として残り、それ以外の列名(家電・食品・衣料)がcategory列の値に変わった、という対応関係を、上の3つの出力を見比べて確かめてみてください。pivotで戻したときに列の並びが元と変わっているのは、pivotが列名を並べ替えて整えるためで、中身の値は一致しています。

プログラムによる処理のしやすさという観点では、縦持ちのほうが有利な場面が多くなります。理由はいくつかあります。第1に、groupbyによる集計は、縦持ちの表のほうが素直に書けます。カテゴリという情報が列名ではなく1つの列の値として入っていれば、新しいカテゴリが増えても列を追加する必要がなく、groupby('category')という同じコードがそのまま使えます。第2に、可視化ライブラリの多くは、色分けや系列の指定にhue引数のような形で「どの列を系列として使うか」を指定する設計になっており、これも縦持ちの表と相性がよい仕組みです(次章で扱います)。第3に、機械学習にデータを渡す際も、1行1レコードという縦持ちに近い構造が前提になっていることがほとんどです。横持ちの表は人が眺めるための最終的な仕上がりの形として優れていますが、分析の途中経過はできるだけ縦持ちのまま保持し、レポートとして表示する直前にpivotやpivot_tableで横持ちに変換する、という使い分けを意識しておくとよいと思います。
Pandasの列(Series)に対して何らかの処理を行いたいとき、for文で1行ずつ処理したくなる場面があるかもしれませんが、Pandasではまず、組み込みのメソッドやベクトル化された演算(NumPyの配列全体に対して一括で計算する処理)を探すことを優先してください。例えば、売上列を1.1倍したいだけであれば、df[‘sales’] * 1.1という演算だけで済み、これは列全体に対して一括で計算されるため、for文で1件ずつ計算するより大幅に高速です。文字列の処理であればstrアクセサ、日付の処理であればdtアクセサという、列全体に対して一括で処理を適用できる専用の仕組みが用意されています。
applyメソッドの出番は、組み込みメソッドやベクトル化された演算では表現しきれない、行ごとに異なる複雑な条件分岐やロジックが必要な場面に限られます。例えば、複数の列の値を組み合わせて独自の判定を行うような処理は、df.apply(lambda row: some_func(row['a'], row['b']), axis=1)のようにapplyで書けますが、これは内部的には1行ずつPythonの関数を呼び出しているため、対象の行数が多くなるほど処理時間がベクトル化された演算より大きく増えていきます。まず組み込み関数・ベクトル化を検討し、それでも実現できない場合の最終手段としてapplyを使う、という優先順位を意識しておくと、無駄に遅いコードを書かずに済みます。
strアクセサは、文字列列に対する処理をまとめて提供する仕組みです。str.contains('AI')で特定の文字列を含むかどうかを一括判定したり、str.extract(r'(\d{4})')で正規表現によって一部分(この例では4桁の数字)を取り出したり、str.split('-', expand=True)で区切り文字によって複数の列に分割したりできます。dtアクセサは日時型の列に対する処理をまとめて提供する仕組みで、dt.year、dt.month、dt.day_name()などによって、日付から年・月・曜日といった要素を一括で取り出せます(yearやmonthは属性なので括弧が要らず、day_nameはメソッドなので括弧を付けて呼び出します)。いずれも、for文で1件ずつ処理するコードを書く前に、まず該当するメソッドがないかを探す価値がある、定番の道具です。
import pandas as pd
orders = pd.DataFrame({
'order_code': ['ORD-2026-0501-A', 'ORD-2026-0512-B', 'ORD-2026-0603-A'],
'customer_memo': ['AI導入の相談', '既存プランの見直し', '解約希望あり'],
})
# strアクセサ: コードから年月部分を正規表現で抽出
orders['year_month'] = orders['order_code'].str.extract(r'ORD-(\d{4}-\d{2})')
# strアクセサ: メモに「解約」を含むかどうかを一括判定
orders['is_churn_risk'] = orders['customer_memo'].str.contains('解約')
# dtアクセサ: 日付型に変換したうえで年・月を取り出す
orders['order_date'] = pd.to_datetime(orders['year_month'] + '-01', format='%Y-%m-%d')
orders['year'] = orders['order_date'].dt.year
orders['month'] = orders['order_date'].dt.month
print(orders)
CSVなどから読み込んだ日付は、多くの場合ただの文字列として扱われており、そのままでは「年だけ取り出す」「翌月との差を計算する」といった日付らしい計算ができません。pd.to_datetimeは、文字列を日時型(datetime64)に変換する、日付を扱ううえで最初に行うべき変換です。フォーマットが決まっている場合はformat引数で明示すると変換が速く安定し、日付として解釈できない値が混じっている場合はerrors=’coerce’を指定すると、変換できない値をエラーで止めずにNaT(日付版の欠損値)に置き換えて処理を続けられます。
日時型に変換した列は、dtアクセサを通じて年・月・日・曜日・四半期など、さまざまな要素を取り出せます。月別・四半期別の集計を行う際によく使うのが、dt.to_period('M')です。年月の情報だけを持つ期間型に変換することで、日付の日にちの違いを無視して「同じ月」としてgroupbyのキーに使えるようになります。本章で扱った集計・結合・整形の技術に、日付という時間軸の要素が加わると、売上の月次推移や前年同月比較といった分析が可能になりますが、トレンドや季節性の分析、将来予測といった時系列データの本格的な扱いについては、シリーズの時系列分析編で改めて詳しく扱います。ここでは、to_datetimeとdtアクセサが日付処理の入口になるという点を押さえておいてください。
ここまでに扱った操作を組み合わせ、実務でよくあるレポート作成の一連の流れを、最初から最後まで通して確認します。想定するのは、注文明細(商品IDと日付、金額のみ)と商品マスタ(商品IDとカテゴリ)という2枚の表から、月×カテゴリで売上を集計し、レポートとして見やすい横持ちの表に整形するという作業です。
import pandas as pd
sales_detail = pd.DataFrame({
'order_date': ['2026-05-03', '2026-05-20', '2026-06-01', '2026-06-15', '2026-06-22'],
'product_id': ['P001', 'P002', 'P001', 'P003', 'P002'],
'sales': [120000, 45000, 98000, 76000, 51000],
})
product_master = pd.DataFrame({
'product_id': ['P001', 'P002', 'P003'],
'category': ['家電', '食品', '衣料'],
})
# 1. 結合(行数チェック付き)
n_before = len(sales_detail)
merged = pd.merge(sales_detail, product_master, on='product_id', how='left', validate='many_to_one')
assert len(merged) == n_before, '結合後に行数が変化しています'
# 2. 日付を日時型に変換し、年月を取り出す
merged['order_date'] = pd.to_datetime(merged['order_date'], format='%Y-%m-%d')
merged['year_month'] = merged['order_date'].dt.to_period('M').astype(str)
# 3. 年月×カテゴリで集計(named aggregation)
monthly = merged.groupby(['year_month', 'category']).agg(
total_sales=('sales', 'sum'),
).reset_index()
# 4. 横持ちのレポートに整形
report = pd.pivot_table(
monthly,
values='total_sales',
index='year_month',
columns='category',
aggfunc='sum',
fill_value=0,
margins=True,
margins_name='合計',
)
print(report)
1番目の手順にあるassertは、書いた条件が成り立っているかをその場で確かめるための構文です。条件が成り立っていればそのまま次の行へ進み、成り立たなければ、後ろに書いたメッセージとともにエラーで処理が止まります。ここでは、結合の前後で行数が変わっていないことを確かめており、もしキーの重複で行が増えていれば、その時点で気づける仕掛けになっています。
この4段階の流れは、規模の大小を問わず、多くの実務レポートに共通する骨格です。データソースが増えればmergeの回数が増え、切り口が増えればgroupbyのキーが増え、見せ方が変わればpivot_tableの行・列の指定が変わりますが、「結合して、日付を整えて、集計して、整形する」という基本の型は変わりません。この型を意識しておくと、初めて扱うデータに対しても、どこから手をつければよいかの見通しを立てやすくなると思います。

次章では、ここまで整形してきた集計結果を、matplotlibやSeabornを使ってグラフとして可視化する方法を扱います。縦持ちのまま持っておいた集計結果が、可視化のライブラリにそのまま渡しやすい形になっていることを、実際に確認していただけると思います。
『前処理大全 データ分析のためのSQL/R/Python実践テクニック』(本橋智光、技術評論社):集計・結合・整形・型変換といったデータ前処理の代表的なパターンを、SQL・R・Pythonの3言語で横並びに示した実践書です。本章で扱ったgroupbyやmergeの背後にある考え方を、SQLの発想と突き合わせながら整理し直したい読者や、Pandas以外の言語でも同種の処理を行う必要がある読者にとって、手元に置いて逆引きしやすい1冊です。
第6章から第8章までで、NumPyによる数値計算とPandasによる表形式データの操作を扱ってきました。数字を集計し、条件で絞り込み、グループごとに集計する。ここまでの技術があれば、データを表として整理することはできます。ただし、表として並んだ数字だけを眺めていても、見落とすものは少なくありません。数十行の集計表からは気づけなかった偏りや変化が、グラフに描いた瞬間に一目で分かる、という経験は分析作業では頻繁に起こります。本章では、Pythonでグラフを描くための代表的なライブラリであるmatplotlibと、その上に統計的な見せ方を重ねたSeabornを扱います。
本章の目的は、グラフの種類ごとの書き方を丸暗記していただくことではありません。matplotlibという仕組みがどのように動いているのかという骨格を理解し、見やすく壊れにくいコードの書き方を身につけていただくことです。matplotlibは歴史の長いライブラリで、ネット上のサンプルコードには書き方の異なる二つの流儀が混在しており、これが入門者を混乱させる原因になっています。本章ではこの点を最初に整理し、以降は一貫した書き方で説明を進めます。
なお、グラフの種類ごとの書き方を用途から逆引きしたい場合は、既刊の「Python実務で必ず使うデータ可視化レシピ106選」が対応しています。本章はその手前にある「なぜそう書くのか」という仕組みの理解に絞って解説します。
集計表は正確ですが、データの姿を伝える力という点では限界があります。平均や合計といった一つの数字にまとめてしまうと、その裏にあった分布の形、変数同士の関係、時間による変化といった情報が失われてしまうためです。この点をよく示す例として、統計学者フランシス・アンスコムが1973年に発表した「アンスコムの例」と呼ばれる4組のデータがあります。この4組は、平均・分散・相関係数・回帰直線の傾きといった基本統計量がすべてほぼ一致するように作られていますが、実際に散布図に描いてみると、一方はきれいな直線関係、もう一方は外れ値の影響で直線に見えているだけ、また別の一方は曲線的な関係、というようにまったく異なる形をしています。数字だけを見て「相関係数が0.8だから同じような関係だろう」と判断すると、この違いを完全に見落とすことになります。
可視化が役立つ場面は、分析の最初と最後に集中しています。分析に着手する段階では、データの分布に偏りがないか、外れ値が含まれていないか、変数同士にどのような関係がありそうかを、グラフを描きながら確認します。この工程は探索的データ分析(EDA: Exploratory Data Analysis)と呼ばれ、モデルを作り始める前の土台になります。そして分析の最後、結果を経営層や他部署に報告する段階でも、グラフは欠かせません。集計表の数字を読み解くには一定の慣れが必要ですが、適切に設計されたグラフは、専門知識のない相手にも一瞬で傾向を伝えることができます。この章では、その両方の場面で使える基礎技術を扱います。

matplotlibを学ぶうえで最初につまずきやすいのが、書き方の流儀が2種類存在するという点です。ネット上の記事や書籍のサンプルコードを見比べると、同じ折れ線グラフを描くだけなのに、まったく違う書き方が出てくることがあります。これは間違いではなく、matplotlibがもともと2つの使い方をサポートしているために起きる現象です。ここを整理せずに学習を進めると、複数の記事を参考にしたときにコードの書き方がちぐはぐになり、エラーの原因も分からなくなります。
一つ目はpyplotスタイルと呼ばれる書き方です。import matplotlib.pyplot as pltとしたうえで、plt.plot()、plt.title()、plt.xlabel()のように、plt(pyplotモジュール)の関数を次々に呼び出していきます。この書き方は、内部的には「今操作している図(Figure)と領域(Axes)」を裏側で自動的に覚えておき、それに対して命令を積み重ねていく仕組みになっています。MATLABというソフトウェアの使い方に似せて設計されており、短いコードで手早くグラフを描けるのが利点です。
二つ目はオブジェクト指向スタイル(以下、本ガイドではax方式と呼びます)です。fig, ax = plt.subplots()という1行で、図全体を表すfig(Figure)オブジェクトと、実際にグラフを描く領域を表すax(Axes)オブジェクトを、変数として明示的に取得します。以降はax.plot()、ax.set_title()、ax.set_xlabel()のように、ax変数に対してメソッドを呼び出していきます。pyplotスタイルとの違いは、「どの領域に描いているか」を裏側の自動管理に任せるのではなく、ax変数という形で自分の手元に持っておく点です。
この二つの流儀が混在する最大の弊害は、グラフを複数並べたいときに表面化します。pyplotスタイルは「今操作している領域」が一つに定まっていることを前提にした書き方です。複数のグラフを並べる場合もpyplotスタイルだけで書けはしますが、どのグラフを操作しているのかがコード上に明示されないため、グラフが増えるほど読み返したときに追いにくくなります。学習の初期段階でpyplotスタイルに慣れてしまうと、その段階で書き方を大きく変える必要に迫られ、混乱が生じます。
本ガイドでは最初からax方式、つまりfig, ax = plt.subplots()で明示的にaxを取得し、以降はax.に対してメソッドを呼び出していく書き方に統一します。1枚のグラフを描く場面でも複数枚を並べる場面でも書き方が変わらないため、途中でコードの構造を書き直す必要がありません。ネット上のサンプルコードでpyplotスタイルを見かけたときも、fig, ax = plt.subplots()を追加し、plt.のメソッドをax.に読み替えれば、ほぼそのままax方式に書き換えられます。
ax方式の基本形を押さえておきましょう。手順は共通していて、まずfig, ax = plt.subplots()でfigとaxを取得し、次にaxに対して描画メソッドを呼び、最後にタイトルや軸ラベル、凡例を設定します。代表的な描画メソッドを整理すると次のとおりです。
| グラフの種類 | axのメソッド | 主な用途 |
|---|---|---|
| 折れ線グラフ | ax.plot(x, y) | 時系列の推移など、連続的な変化を見る |
| 棒グラフ | ax.bar(x, height) | カテゴリごとの値を比較する |
| 散布図 | ax.scatter(x, y) | 2つの変数の関係を見る |
| ヒストグラム | ax.hist(data, bins=20) | 1つの変数の分布(値の散らばり方)を見る |
実際のコードを見てみます。次の例は、月ごとの売上実績と目標を折れ線グラフで重ねて描き、タイトル・軸ラベル・凡例を設定するものです。
import matplotlib.pyplot as plt
months = ["1月", "2月", "3月", "4月", "5月", "6月"]
sales = [120, 135, 128, 150, 162, 158]
target = [125, 130, 135, 140, 145, 150]
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(months, sales, marker="o", label="実績")
ax.plot(months, target, marker="s", linestyle="--", label="目標")
ax.set_title("月次売上の推移")
ax.set_xlabel("月")
ax.set_ylabel("売上(百万円)")
ax.legend()
plt.show()
ax.plot()を2回呼ぶと、同じ領域に2本の線が重ねて描かれます。label引数に指定した文字列は、最後のax.legend()を呼んだ時点でまとめて凡例として表示されます。markerは線上の点の形、linestyleは線の種類(実線・破線など)を指定する引数で、2本の線を色だけでなく形でも区別できるようにしています。ax.set_title()、ax.set_xlabel()、ax.set_ylabel()は、それぞれグラフ全体のタイトル、横軸・縦軸のラベルを設定するメソッドで、pyplotスタイルのplt.title()などに対応するax方式の書き方です。なお、このコードを実際にご自身の環境で実行すると、タイトルや軸ラベルの日本語部分が四角い記号の並びとして表示されることがあります。原因と対処法は次節で説明します。

先ほどのコードをそのまま実行すると、多くの環境で日本語部分が「□□□」のような四角い記号(俗に「豆腐」と呼ばれます)として表示されます。これはmatplotlibのバグではなく、フォント(文字の形を定義したデータ)の設定に起因する、入門者のほぼ全員が一度は通る定番の壁です。
原因は、matplotlibが標準で使用するフォントが「DejaVu Sans」という欧文フォントであり、このフォントの中にひらがな・カタカナ・漢字の字形データが含まれていないことにあります。フォント側に対応する文字の形が存在しないため、代わりに「この文字は表示できません」を意味する四角い記号が描画される、という仕組みです。英語のタイトルやラベルだけを使っている間は表面化せず、日本語を使い始めた瞬間に発覚するため、原因が分かりにくいのも混乱を招く一因です。
もっとも手軽な対処法は、matplotlib-fontjaという外部パッケージを利用することです。あらかじめ日本語フォントを内蔵しており、1行importするだけで日本語表示に必要な設定を自動的に済ませてくれます。なお、同じ用途で長く使われてきたjapanize-matplotlibというパッケージがありますが、こちらは最終更新が2020年で、Python 3.12以降では起動時にエラーになります(内部で使っているdistutilsという標準ライブラリが、Python 3.12で削除されたためです)。これから始めるならmatplotlib-fontjaを選んでください。
# 事前にインストール
pip install matplotlib-fontja
インストール後は、matplotlibをimportした直後にもう1行加えるだけです。
import matplotlib.pyplot as plt
import matplotlib_fontja
fig, ax = plt.subplots()
ax.set_title("日本語のタイトル")
この2行目を加えるだけで、以降に描くすべてのグラフで日本語が正しく表示されるようになります。もう一つの方法は、パソコンに標準で入っている日本語フォントを、matplotlibの設定として直接指定するやり方です。Windowsであればplt.rcParams["font.family"] = "Meiryo"、Macであればplt.rcParams["font.family"] = "Hiragino Sans"のように指定します。こちらは追加のパッケージを必要としない利点がありますが、実行環境ごとにフォント名を書き分ける必要があるため、複数のパソコンやクラウド環境でコードを共有する場合には、matplotlib-fontjaを使うほうが手間が少なくなります。
1枚のグラフだけでは伝えきれない情報を、複数のグラフを並べて一度に見せたい場面は頻繁にあります。ax方式では、plt.subplots()にグラフの行数と列数を指定するだけで、複数のaxをまとめて用意できます。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib_fontja
rng = np.random.default_rng(0)
x = rng.normal(loc=50, scale=10, size=200)
y = x * 0.8 + rng.normal(scale=5, size=200)
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8))
axes[0, 0].plot(np.sort(x)[:50])
axes[0, 0].set_title("折れ線")
axes[0, 1].bar(["A", "B", "C"], [30, 45, 25])
axes[0, 1].set_title("棒グラフ")
axes[1, 0].scatter(x, y, alpha=0.5)
axes[1, 0].set_title("散布図")
axes[1, 1].hist(x, bins=20)
axes[1, 1].set_title("ヒストグラム")
fig.tight_layout()
plt.show()
nrows=2, ncols=2を指定すると、2行2列、合計4つのaxが1つのfigの中に用意されます。このとき返ってくるaxesは、1つのAxesオブジェクトではなく、Axesを格納した2次元の配列になっている点に注意が必要です。axes[0, 0]は1行目1列目、axes[1, 1]は2行目2列目のaxを指しており、それぞれに対して個別にplot()やbar()を呼び出すことで、4種類のグラフを1つの画像として並べています。グラフの数が多く1次元に並べたい場合はnrows=1とすればaxesは1次元の配列になり、axes[0]、axes[1]のように添字が1つで済みます。
figsize引数は、図全体の横幅と縦幅をインチ単位のタプルで指定するものです。既定のサイズのままグラフを増やすと、1つあたりの領域が狭くなって文字やグラフが読みにくくなるため、並べる枚数に応じてfigsizeを調整する必要があります。また、複数のグラフを詰めて並べると、タイトルや軸ラベルが隣のグラフと重なって見づらくなることがあります。fig.tight_layout()を最後に呼んでおくと、各axの間隔が自動的に調整され、重なりの多くが解消されます。
第7章、第8章で扱ったPandasのDataFrameには、.plot()という専用のメソッドが用意されています。集計した結果をその場でざっと確認したいだけの場面では、matplotlibの関数を自分で呼び出すよりも、このdf.plot()のほうが手早く済みます。
| 書き方 | グラフの種類 |
|---|---|
df.plot(kind="line") | 折れ線グラフ(既定値) |
df.plot(kind="bar") | 棒グラフ |
df.plot(kind="scatter", x="列名1", y="列名2") | 散布図 |
df.plot(kind="hist") | ヒストグラム |
この手軽さの背景には、Pandasの.plot()が独自の描画エンジンを持っているのではなく、内部でmatplotlibを呼び出しているという関係があります。DataFrameの列名やインデックスを自動的に軸ラベルや凡例として使ってくれるため、自分でラベルを一つずつ指定する手間が省ける、というのが実体です。実際、df.plot()は戻り値としてmatplotlibのAxesオブジェクトを返しますので、ax = df.plot(kind="bar")のように受け取れば、その後はax.set_title()のように、ここまで説明してきたax方式のメソッドをそのまま続けて呼び出せます。また、df.plot(ax=ax)のように既存のaxを引数として渡せば、前節のsubplotsで用意した特定の領域に、Pandas側から描画することもできます。集計してすぐ確認したい段階ではdf.plot()を、タイトルや凡例、複数グラフの配置まで作り込みたい段階ではax方式を、という使い分けが実務では現実的です。
matplotlibは自由度が高い分、分布を確認するためのヒストグラムに密度曲線を重ねたり、複数のカテゴリを一度に箱ひげ図で比較したりする際には、細かい設定を自分で書き込む必要があります。Seabornは、この種の統計的な可視化をあらかじめ整った見た目で手早く描けるように設計されたライブラリで、matplotlibの上に構築されています。
Seabornの大きな特徴は、DataFrameとの相性のよさです。matplotlibのように列のデータをいちいち取り出して渡すのではなく、data=dfとしてDataFrame全体を渡し、xやyには列名を文字列で指定するだけで、グラフが描けます。代表的な関数を整理します。
| 関数 | できること |
|---|---|
sns.histplot(data=df, x="列名", kde=True) | ヒストグラムに、分布の形を滑らかな曲線で示すカーネル密度推定(KDE)を重ねて表示 |
sns.boxplot(data=df, x="カテゴリ列", y="数値列") | カテゴリごとの数値の分布を、中央値・四分位範囲・外れ値とともに箱ひげ図で比較 |
sns.heatmap(相関行列, annot=True, cmap="coolwarm") | 行列の値を色の濃淡で表現。変数間の相関係数を一覧するのによく使われる |
sns.pairplot(df) | 複数の数値列すべての組み合わせについて散布図とヒストグラムを自動生成 |
次の例は、あるDataFrameの数値列に対して、ヒストグラムとカーネル密度推定を重ねて表示するコードです。
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
sns.set_theme(style="whitegrid")
import matplotlib_fontja # set_themeの後に読み込むと日本語フォント設定が上書きされずに済む
# サンプルデータ(年齢300人分)
rng = np.random.default_rng(0)
df = pd.DataFrame({"age": rng.normal(42, 12, 300).clip(18, 80)})
fig, ax = plt.subplots(figsize=(6, 4))
sns.histplot(data=df, x="age", kde=True, ax=ax)
ax.set_title("年齢分布")
ax.set_xlabel("年齢")
plt.show()
sns.set_theme()は、以降に描くすべてのグラフの背景色や罫線の見た目をまとめて整える設定です。ここではstyle="whitegrid"を指定し、背景に薄いグリッド線が入った見やすいテーマにしています。sns.histplot()のax引数には、あらかじめ用意しておいたax方式のaxを渡しています。SeabornはグラフをmatplotlibのAxesの上に描く仕組みになっているため、渡したaxに対してax.set_title()やax.set_xlabel()のような、ここまで説明してきたax方式のメソッドをそのまま続けて呼び出せます。Seabornで手早く統計的な見た目を作り、細部の調整はmatplotlibのax方式で仕上げる、という組み合わせ方を覚えておくと、両者の強みを両立できます。

作成したグラフを報告資料やスライドに貼り付けるには、画像ファイルとして保存する必要があります。ax方式で作業している場合、保存にはfig.savefig()を使います。
import matplotlib.pyplot as plt
import matplotlib_fontja
fig, ax = plt.subplots(figsize=(6, 4))
ax.bar(["A", "B", "C"], [30, 45, 25])
ax.set_title("カテゴリ別件数")
fig.savefig("category_count.png", dpi=300, bbox_inches="tight")
dpi(dots per inch)は画像の解像度を指定する引数で、値が大きいほど画像は精細になりますが、ファイルサイズも大きくなります。画面で確認する程度であれば100前後でも十分ですが、資料に印刷して使う場合や、拡大して細部まで見せたい場合は、300程度を指定しておくと文字や線がにじみにくくなります。bbox_inches="tight"は、軸ラベルやタイトルが画像の端で切れてしまうのを防ぐための指定で、グラフの余白を保存時に自動調整してくれます。これを指定しないと、特に日本語の長いラベルを使ったときに、文字の一部が画像の外にはみ出して保存されることがあります。なお、保存したい形式はファイル名の拡張子で切り替えられます。.pngは画面表示や資料への貼り付けに、.svgや.pdfは拡大しても粗くならないベクター形式で、印刷物への掲載に向いています。
グラフは正しく描いても、見せ方次第で受け取る側の印象を誤らせることがあります。ここでは、実務でグラフを作る際に押さえておきたい3つの基本作法を挙げます。
これらはいずれも、グラフの作り手が意図せずとも、読み手に誤った印象を与えてしまう典型的な落とし穴です。既刊「データから正しく結論を導く統計学のまとめ」の第12章では、この種のグラフによる印象操作を、シンプソンのパラドックスや選択バイアスなどと並ぶ「数字にだまされる落とし穴」の一つとして扱っています。グラフを描く技術と、その見せ方が読み手の判断をどう左右するかという視点は、本来一体のものとして押さえておくべきです。
グラフは、作り手の意図を分かりやすく伝えられる一方で、作り手が意図しなくても読み手の判断を歪めてしまうことがあります。技術的に正しく描くことと、誠実に伝わる形で描くことは、必ずしも同じではないと考えています。
本章では、ax方式という一貫した書き方を軸に、matplotlibとSeabornの基本的な仕組みを説明してきました。実務では、この仕組みの理解を土台にしつつ、「積み上げ棒グラフを描きたい」「地図上に分布を表示したい」といった具体的な要望から、対応するコードをその場で探したい場面が多くなります。そうした用途からの逆引きには、既刊の「Python実務で必ず使うデータ可視化レシピ106選」が対応しています。本章の内容と合わせてお使いいただくと、仕組みを理解したうえで、必要なコードにも素早くたどり着けるようになります。
ここまでで、なぜ可視化が必要かという出発点から、matplotlibの2つの流儀、基本のグラフ、日本語表示への対処、複数グラフの並べ方、Pandasからの手軽なプロット、Seabornによる統計的な可視化、保存の方法、そして伝わるグラフの作法までを扱いました。次章では、ここまで学んだPythonの基礎を実務でそのまま使うための、日付処理やファイル入出力といったユーティリティを見ていきます。
これまでの章では、NumPyやPandasを使ってデータそのものを扱う方法、そしてMatplotlibなどで結果を可視化する方法を見てきました。しかし、実際の分析業務では、データを分析している時間よりも、その前後にある周辺作業に費やす時間のほうが長くなりがちです。フォルダの中に散らばった大量のファイルを1つずつ読み込む、処理の途中経過が分からず何十分も画面を眺め続ける、ファイル名の付け方が統一されておらず後から探せなくなる、といった作業です。こうした周辺作業を効率よくこなせるかどうかは、分析全体にかかる時間を大きく左右します。この章では、そのための小さな道具をひととおり揃えていきます。
扱う内容は次の通りです。
いずれも高度な理論を必要とする内容ではありませんが、知っているかどうかで日々の作業時間が大きく変わる項目ばかりです。この章を通じて、分析の本質的な部分に時間を使うための足回りを整えていきます。
ファイルやフォルダの場所を表す文字列を「パス」と呼びます。古いPythonのコードでは、パスを単純な文字列として扱い、os.pathモジュールの関数でつなぎ合わせる書き方が主流でした。しかし、この書き方にはOSごとの違いに悩まされるという弱点があります。Windowsではフォルダの区切りに円マーク(バックスラッシュ)を使う一方、macOSやLinuxではスラッシュを使うため、文字列を直接連結するコードはOSをまたぐと動かなくなることがあります。
この問題を解決してくれるのが、Python標準ライブラリのpathlibモジュールです。パスを単なる文字列ではなく、Pathという専用のオブジェクトとして扱い、パスの結合や存在確認、拡張子の取得といった操作を、OSの違いを意識せずに書けるようにしてくれます。
from pathlib import Path
# パスの結合には「/」演算子を使う(OSのスラッシュ問題を吸収してくれる)
data_dir = Path("data") / "raw" / "2026"
file_path = data_dir / "sales.csv"
print(file_path)
# 書き方はどのOSでも「/」でよく、表示だけがOSに合わせて変わる
# Windowsでは data\raw\2026\sales.csv
# macOS/Linuxでは data/raw/2026/sales.csv
# 存在確認
print(file_path.exists()) # ファイルが存在するかどうか
print(data_dir.is_dir()) # フォルダかどうか
# 各種の情報を取り出す
print(file_path.name) # "sales.csv"
print(file_path.stem) # "sales"(拡張子を除いた部分)
print(file_path.suffix) # ".csv"(拡張子)
print(file_path.parent) # 1つ上の階層のフォルダ
# フォルダが存在しなければ作成する
data_dir.mkdir(parents=True, exist_ok=True)
ポイントは、「/」という記号をパスの結合に使える点です。文字列連結ではなくPathオブジェクト同士の演算として結合されるため、Windowsで実行してもmacOSで実行しても、内部的には正しい区切り文字に変換されます。拡張子や親フォルダの取得もstemやparentといった分かりやすい名前の属性で行えるため、文字列を自分でスライスして加工するよりも読みやすいコードになります。mkdirにparents=Trueを指定すると、中間のフォルダが存在しない場合でもまとめて作成してくれる点も実務でよく使う機能です。
とはいえ、社内に残っている古いスクリプトの中には、今でもos.pathを使ったコードが数多く存在します。os.path.join関数でパスを結合したり、os.path.exists関数で存在確認をしたりする書き方です。pathlibが登場する以前はこちらが標準的な書き方だったため、既存のコードを保守する際には、os.pathの書き方も見分けられるようにしておく必要があります。新しくコードを書く場面では、読みやすさと安全性の両面からpathlibを使うことを基本にしてよいと考えられます。
実務のデータは、1つのファイルにきれいにまとまっているとは限りません。月ごとに分かれたCSVファイル、店舗ごとに分かれたExcelファイル、日次のログファイルなど、同じ形式のファイルが大量にフォルダの中に並んでいる状況は非常によくあります。こうした「フォルダ内の同じパターンのファイルをまとめて読み込む」という作業は、分析実務の中でも特に頻出するパターンであり、この章でも重点的に扱います。
この処理を簡潔に書けるのが、pathlibのglobメソッドです。globは、「*」や「?」といった記号(ワイルドカードと呼びます)を使ってファイル名のパターンを指定し、条件に一致するファイルを一覧として取得する仕組みです。「*.csv」と指定すれば、拡張子がcsvであるすべてのファイルが対象になります。
from pathlib import Path
import pandas as pd
data_dir = Path("data/raw")
# フォルダ直下の全CSVファイルのパスを取得する
csv_files = sorted(data_dir.glob("*.csv"))
print(csv_files)
# Windowsでは [WindowsPath('data/raw/2026-01.csv'), WindowsPath('data/raw/2026-02.csv'), ...]
# macOS/Linuxでは [PosixPath('data/raw/2026-01.csv'), PosixPath('data/raw/2026-02.csv'), ...]
# 1つずつ読み込んでリストに集め、最後にまとめて結合する
df_list = []
for path in csv_files:
df = pd.read_csv(path)
df["source_file"] = path.name # どのファイル由来かを列として残しておく
df_list.append(df)
all_df = pd.concat(df_list, ignore_index=True)
print(all_df.shape)
globの結果は保存されている順番とは限らないため、ファイル名に日付が入っている場合などは、sorted関数で並べ替えておくと結果が安定します。また、どのファイルに由来するデータかを後から追えるように、読み込み時にsource_fileのような列を追加しておく習慣も実務では重宝します。集計結果に想定外の値が混じっていたときに、元のファイルまで遡って原因を調べやすくなるためです。
サブフォルダの中まで含めて再帰的に検索したい場合は、globのパターンに「**」を使います。「data_dir.glob("**/*.csv")」と書けば、data_dirの直下だけでなく、さらに下の階層にあるCSVファイルまで探索できます。フォルダ構成が年ごと・月ごとに階層化されているケースでは、この再帰検索が役立ちます。

大量のファイルを1つずつ読み込んだり、行数の多いデータに対してループ処理を回したりすると、処理の完了まで数分から数十分かかることがあります。この間、画面には何も表示されず、処理がきちんと進んでいるのか、それとも固まってしまったのかが分からない状態が続きます。長時間の処理では、こうした状態が続くこと自体が作業の負担になりますし、途中で異常が起きていても気づけないまま待ち続けることになります。
この不安を解消してくれるのが、tqdmというライブラリです。ループの対象をtqdmで包むだけで、残り件数や経過時間、処理速度を示すプログレスバーが自動的に表示されるようになります。
from tqdm import tqdm
import time
# 通常のforループをtqdmで包むだけでプログレスバーが表示される
for path in tqdm(csv_files, desc="CSV読み込み中"):
df = pd.read_csv(path)
time.sleep(0.1) # 処理に時間がかかる想定のダミー処理
# Jupyter Notebook/Lab上ではtqdm.notebookを使うと表示が見やすくなる
from tqdm.notebook import tqdm as tqdm_notebook
for path in tqdm_notebook(csv_files):
df = pd.read_csv(path)
tqdmの引数descには、プログレスバーの横に表示する説明文を指定でき、複数の処理を並べて実行するようなスクリプトでは、今どの処理が動いているのかを見分ける手がかりになります。Jupyter環境ではtqdm.notebookを使うと、ノートブックの表示形式に合わせた見た目のよいプログレスバーが表示されます。処理そのものを速くする機能ではありませんが、残り時間の見当がつくことで、完了まで待つべきか、いったん中断して原因を調べるべきかを判断しやすくなります。時間のかかる処理を扱う実務では、進捗が見えること自体に実用的な意味があります。
Pandasのread_csvやto_csvは便利な反面、CSV以外の形式のテキストファイルを扱ったり、ファイルの内容を1行ずつ細かく処理したりする場面には向いていません。こうした低レベルなファイル操作を行う際に使うのが、Python標準の組み込み関数openと、with構文の組み合わせです。
# with構文を使うとファイルを閉じ忘れる心配がない
with open("memo.txt", mode="w", encoding="utf-8") as f:
f.write("分析結果のメモ\n")
f.write("欠損値の割合は3.2%でした\n")
with open("memo.txt", mode="r", encoding="utf-8") as f:
for line in f:
print(line.strip()) # strip()で末尾の改行を取り除く
# CSVを1行ずつ細かく処理したい場合はcsvモジュールを使う
import csv
with open("data/raw/2026-01.csv", mode="r", encoding="utf-8", newline="") as f:
reader = csv.reader(f)
header = next(reader) # 1行目(ヘッダー)だけを先に取り出す
for row in reader:
pass # 1行ごとの処理をここに書く
with構文を使う最大の利点は、処理が正常に終わったときはもちろん、途中でエラーが発生した場合でも、ブロックを抜ける際に自動的にファイルが閉じられる点です。open関数を単独で呼び出してclose関数を書き忘れると、ファイルが開いたままの状態になり、他のプログラムからそのファイルを編集できなくなるといった不具合につながることがあります。with構文はこうした閉じ忘れを防ぐための定番の書き方であり、ファイル操作をするときは基本的にwith構文とセットで使うと覚えておくとよいでしょう。
もう1つ注意したいのがencoding(文字エンコーディング)の指定です。日本語を含むテキストファイルは、UTF-8やShift-JIS(CP932)といった複数の方式で文字が保存されており、読み込み側が想定と異なる方式で開こうとすると、文字化けやエラーの原因になります。特にWindows環境でExcelから書き出したCSVファイルは、Shift-JIS系のエンコーディングになっていることが多く、encoding=”utf-8″のまま読み込むとエラーになる場合があります。ファイルが正しく読み込めないときは、まずencodingの指定を疑い、encoding=”cp932″などに変更して試してみるのが定石です。
実務のデータには、注文日時やログの記録時刻など、日付や時刻に関する情報が数多く登場します。これらを扱うための標準ライブラリが、datetimeモジュールです。
from datetime import datetime
# 現在の日時を取得する
now = datetime.now()
print(now) # 2026-07-14 10:30:00.123456 のような形式
# datetimeを好きな文字列形式に変換する(strftime)
print(now.strftime("%Y-%m-%d")) # "2026-07-14"
print(now.strftime("%Y%m%d_%H%M%S")) # "20260714_103000"
# 文字列をdatetimeに変換する(strptime)
date_str = "2026-07-14"
parsed = datetime.strptime(date_str, "%Y-%m-%d")
print(parsed.year, parsed.month, parsed.day) # 2026 7 14
# 日付の差分を計算する
from datetime import timedelta
one_week_later = now + timedelta(days=7)
print(one_week_later.strftime("%Y-%m-%d"))
strftimeは、datetimeオブジェクトを人間が読みやすい文字列に変換するメソッドで、「%Y」が4桁の年、「%m」が2桁の月、「%d」が2桁の日を表すというように、書式を組み合わせて好きな形式の文字列を作れます。反対に、文字列として保存された日付をdatetimeオブジェクトに変換したいときに使うのがstrptimeで、こちらも同じ書式指定を使います。strftimeとstrptimeは名前が似ていて混同しやすいため、「フォーマットへ変換するのがstrftime、フォーマットから変換するのがstrptime」と覚えておくと区別しやすくなります。
実務でよく使われるパターンの1つが、ファイル名に日付を付けて、いつ作成した出力なのかを一目で分かるようにしておくという方法です。分析結果を出力するたびに同じファイル名で上書きしてしまうと、過去の結果を見返せなくなってしまいますが、ファイル名に実行日時を含めておけば、実行するたびに新しいファイルとして保存され、履歴を追跡できます。
from pathlib import Path
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = Path("output") / f"report_{timestamp}.csv"
print(output_path)
# Windowsでは output\report_20260714_103000.csv
# macOS/Linuxでは output/report_20260714_103000.csv
# all_dfはこれまでの例で作成したDataFrameを想定
output_path.parent.mkdir(parents=True, exist_ok=True)
all_df.to_csv(output_path, index=False, encoding="utf-8")
ファイル名に「report_20260714_103000.csv」のような日時を付ける習慣は、同じファイル名での上書きを防ぎ、実行のたびの結果を履歴として残せるようにする、地味ながら効果の大きい工夫です。strftimeで日時を文字列化し、f文字列(第3章で扱った書式)でファイル名に組み込む、という組み合わせを覚えておくと、さまざまな出力処理に応用できます。
データの件数が増えてくると、処理の実行時間が気になり始めます。ここで大切なのが、高速化に取り組む順序です。多くの場合、最初に検討すべきはPythonのforループをNumPyやPandasのベクトル化された演算に置き換えることです。第6章で扱った通り、NumPyの配列演算は内部でC言語による高速な実装が使われており、同じ処理をforループで1件ずつ行うよりも、はるかに高速に処理できます。実務で処理が遅いと感じたとき、その原因の多くは、ベクトル化できる処理をforループでそのまま書いてしまっていることにあります。
ベクトル化を検討し尽くしてもなお処理に時間がかかる場合に、次の選択肢として検討するのが並列処理です。並列処理とは、1つの処理を複数のCPUコアに分担させて同時に進めることで、全体の処理時間を短縮する方法です。Pythonには古くからmultiprocessingモジュールが用意されていますが、書き方がやや複雑であるため、より簡単に並列処理を扱えるconcurrent.futuresモジュールが現代的な入口としてよく使われます。
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
import pandas as pd
def load_csv(path):
"""1つのCSVファイルを読み込んで返す関数"""
return pd.read_csv(path)
if __name__ == "__main__":
csv_files = sorted(Path("data/raw").glob("*.csv"))
# 複数のプロセスに処理を分担させて並列に読み込む
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(load_csv, csv_files))
all_df = pd.concat(results, ignore_index=True)
print(all_df.shape)
このコードに出てくるif __name__ == "__main__":は、このファイルが直接実行されたときだけ以下を動かす、という意味の書き方です(次の節で改めて扱います)。並列処理では、この書き方で囲っておかないと処理が正しく始まらないため、この例はノートブックのセルに貼り付けるのではなく、.pyファイルとして保存し、ターミナルから実行してください。
ProcessPoolExecutorは、指定した数のプロセスを立ち上げ、executor.mapに渡した関数を、対象の一覧に対して並列に実行してくれる仕組みです。max_workersには同時に使うプロセス数を指定し、一般的にはお使いのマシンのCPUコア数を目安に設定します。ファイルの読み込みのように、1件ごとの処理が互いに独立していて依存関係がない処理は、並列処理と相性が良い典型例です。反対に、ある行の計算結果を次の行の計算に使うような、順序に依存する処理は並列化が難しく、無理に並列化しようとするとコードが複雑になるだけで効果が得られないこともあります。並列処理は便利な選択肢ですが、まずベクトル化を尽くしたうえで、それでも足りない場合の追加の手段として位置づけておくのが実務での順序です。
第2章で触れた通り、分析の初期段階ではJupyterのノートブック上で試行錯誤するのが効率的ですが、同じ処理を毎日実行したい、他のメンバーに使ってもらいたい、他のプログラムから呼び出したいといった段階に進むと、ノートブックのままでは扱いにくくなってきます。処理が固まり、定期的な実行・共有・再利用が必要になったタイミングが、コードを拡張子「.py」のPythonスクリプトファイルに切り出す1つの目安です。
スクリプト化する際に必ずと言ってよいほど登場するのが、if __name__ == "__main__":という書き方です。これは、そのファイルが直接実行されたときだけ、以下のコードを実行するという意味を持つ書き方です。Pythonのファイルは、直接実行される場合と、他のファイルからimportされて部品として使われる場合の両方があり得ます。この書き方をしておくと、他のファイルから関数だけをimportして使いたいときに、意図しない処理まで実行されてしまうことを防げます。
import argparse
from pathlib import Path
import pandas as pd
def process_files(input_dir: Path, output_path: Path) -> None:
"""指定フォルダ内の全CSVを結合し、1つのファイルとして出力する"""
csv_files = sorted(input_dir.glob("*.csv"))
df_list = [pd.read_csv(path) for path in csv_files]
all_df = pd.concat(df_list, ignore_index=True)
output_path.parent.mkdir(parents=True, exist_ok=True) # 出力先フォルダが無ければ作る
all_df.to_csv(output_path, index=False, encoding="utf-8")
print(f"{len(csv_files)}件のファイルを結合し、{output_path}に保存しました")
def main():
parser = argparse.ArgumentParser(description="CSVファイルを一括結合するスクリプト")
parser.add_argument("--input", type=str, required=True, help="入力フォルダのパス")
parser.add_argument("--output", type=str, default="output/merged.csv", help="出力ファイルのパス")
args = parser.parse_args()
process_files(Path(args.input), Path(args.output))
if __name__ == "__main__":
main()
このスクリプトをmerge_csv.pyのようなファイル名で保存しておけば、コマンドライン(第1章で開いたのと同じ、Windowsなら PowerShell、macOSならターミナルの画面です)から「python merge_csv.py –input data/raw –output output/merged.csv」のように実行できます。argparseは、こうしたコマンドラインからの引数(オプション)を受け取るための標準ライブラリで、add_argumentメソッドで受け付けたい引数の名前や型、必須かどうかを指定しておくと、実行時に渡された値を自動的に解釈してくれます。requiredをTrueにした引数は指定を省略できず、defaultを指定した引数は省略時にその既定値が使われます。処理内容をprocess_filesのような関数として切り出し、main関数から呼び出す形にしておくと、後から別のスクリプトやテストコードから同じ処理を再利用しやすくなります。ノートブックの中だけで完結していたコードが、こうした形に整理されることで、定期実行や他メンバーとの共有に耐えられる資産へと育っていきます。
スクリプト化まで済ませたコードは、人が手動で実行する以外に、決まった時刻に自動的に実行させることもできます。Windows環境ではタスクスケジューラ、macOSやLinux環境ではcronと呼ばれる仕組みが標準で用意されており、いずれも「このコマンドを、この時刻・この間隔で自動実行する」という設定をOSに登録できる機能です。毎朝決まった時刻に前日分のデータを集計してファイルに出力しておく、といった定型的な処理を人手を介さずに回し続けたい場合に活用されます。それぞれの詳しい設定方法は本ガイドの範囲を超えるため、ここでは「スクリプト化まで進めた処理は、タスクスケジューラやcronのような仕組みに接続することで定期実行できる」という位置づけに触れるにとどめます。
『退屈なことはPythonにやらせよう 第3版 ノンプログラマーにもできる自動化処理プログラミング』(Al Sweigart、オライリー・ジャパン):ファイル操作や定型作業の自動化をテーマにした入門書で、この章で扱ったパス操作やファイルの読み書きを、より幅広い具体例とともに学べます。プログラミング未経験者にも配慮した説明が特徴で、実務の地味な作業を1つずつ自動化していく発想を身につけたい場合の参考になります。
分析用のフォルダに、似たような名前のノートブックがいくつも並んでしまうことがあります。「analysis.ipynb」「analysis_v2.ipynb」「analysis_v2_最新.ipynb」「analysis_v2_最新_修正済み.ipynb」といった具合です。どれが本当に最新なのか、どこをどう直したのかは、ファイル名だけでは判断できません。かといって全部のファイルを開いて中身を見比べ、差分を目視で探すのは時間ばかりかかる作業です。こうしたファイルコピーによる手動のバージョン管理は、プロジェクトが小さいうちは何とか回っていても、分析が複雑になり、共同作業者が増えるほど、確実に破綻していきます。「先週の結果を出したコードはどのファイルだったか」が分からなくなり、報告資料の数値を再現できずに困る、という事態も起こりやすくなります。
この問題に対して、ソフトウェア開発の現場で標準的に使われている解決策がGitという仕組みです。Gitはファイルの変更履歴を記録し、必要なときに過去の状態へ戻したり、複数人の変更を安全に統合したりするための道具で、バージョン管理システムと呼ばれる分野の代表格にあたります。分析用のコードやノートブックを書く以上、Gitと無縁ではいられません。この章では、Gitの基本的な考え方から、最小限のコマンド操作、そしてデータ分析者にとって特に重要な注意点までを順番に見ていきます。
この章で扱う内容は次の通りです。
バージョン管理とは、ファイルの変更履歴を時間の流れに沿って記録し、必要なときにいつでも過去の状態に戻れるようにしておく仕組みのことです。ファイルコピーによる手動管理との決定的な違いは、記録される情報の質にあります。「analysis_v2_最新_修正済み.ipynb」というファイル名からは、せいぜい「これが新しいらしい」という程度の情報しか読み取れません。一方でバージョン管理システムを使うと、変更のたびに、いつ変更されたのか、誰が変更したのか、具体的に何がどう変わったのか、そしてなぜその変更をしたのかという4つの情報が、記録として残ります。
この4つ目の「なぜ」が特に重要です。半年前に書いたコードを見返して、なぜこの書き方にしたのか思い出せない、ということは起こりがちです。変更履歴に理由が残っていれば、当時の判断をそのまま読み返すことができます。バージョン管理を使うと、過去のどの時点の状態にも戻れるうえ、その時点で何を意図していたかまで一緒に確認できます。たとえば、あるコミットの時点でのファイルの状態を丸ごと取り出したり、2つの時点の間で何がどう変わったかを差分として並べて見たりする操作が、いずれもコマンド1つで行えます。過去の状態と当時の意図の両方をいつでも取り出せるというこの性質が、分析コードを長期的に安心して育てていくための土台になります。
Gitを理解するうえでまず押さえておきたいのが、リポジトリ、コミット、ステージングエリアという3つの用語です。リポジトリとは、あるプロジェクトの変更履歴をすべて保管する箱のようなものです。フォルダの中にGitを導入すると、そのフォルダ全体が1つのリポジトリになり、以降の変更履歴がすべてそこに蓄積されていきます。
コミットとは、ある時点でのファイルの状態を、履歴として記録する操作、およびその記録そのものを指す言葉です。コミットを1つ作るたびに、その時点のファイル一式のスナップショットが、メッセージや作成者、日時とともに履歴に刻まれます。重要なのは、コミットは「意味のある単位」で作るという考え方です。ファイルを1文字直すたびにコミットする必要はありませんし、逆に1週間分の変更をまとめて1つのコミットにしてしまうと、後から履歴をたどっても何がどう変わったのか把握しづらくなります。「欠損値の補完方法を変更した」「グラフの軸ラベルを修正した」といった、1つのまとまった作業単位でコミットを作るのが基本です。コミットは、ゲームで言えばセーブポイントに近い感覚で捉えると分かりやすくなります。意味のある区切りごとにセーブしておけば、後から「あの時点まで戻りたい」となったときに、迷わず戻れます。
ステージングエリアは、Gitに特徴的な仕組みで、変更したファイルと、コミットとして記録するファイルの間に置かれた、一時的な準備エリアです。Gitが扱う場所は大きく3つに分かれています。実際にファイルを編集する作業ディレクトリ、次のコミットに含める変更を仮に置いておくステージングエリア、そして確定した履歴が積み上がっていくリポジトリです。作業ディレクトリで編集したファイルは、git addというコマンドでステージングエリアに移され、git commitというコマンドでステージングエリアの内容がリポジトリに履歴として記録されます。この2段階構成のおかげで、複数のファイルを編集していても、そのうち意味のあるまとまりだけを選んでコミットに含める、という柔軟な運用ができます。

コミットとは、意味のある単位で区切られたセーブポイントです。ファイルを保存するたびではなく、1つの作業がまとまったタイミングで刻んでいくものと捉えておくと、後から履歴をたどるときに迷いにくくなります。
Gitは、Pythonとは別のソフトウェアです。第1章でPythonを入れただけではgitコマンドは使えませんので、先に導入しておきます。公式サイト(git-scm.com)のダウンロードページから、お使いのOS向けのものを入手します。Windows向けの公式配布はGit for Windowsという名前で、インストーラを実行して既定の設定のまま進めれば問題ありません。macOSでも同じサイトからインストーラを入手できます。
導入できたかどうかは、第1章で開いたのと同じターミナル(Windowsは PowerShell、macOSはターミナル)で次のコマンドを打って確かめます。バージョン番号が表示されれば使える状態です。
git --version
あわせて、コミットに記録される名前とメールアドレスを最初に1回だけ設定しておきます。この情報は「誰がいつ何を変えたか」の「誰が」にあたる部分で、設定していないと最初のコミットでエラーになります。
git config --global user.name "自分の名前"
git config --global user.email "自分のメールアドレス"
Gitの操作は数多くありますが、最初に覚えるべきものは4つに絞り込めます。init、add、commit、logです。この4つの流れを最小サイクルとして体に染み込ませておくと、あとの応用はその上に積み上げていくだけで済みます。
まずgit initで、フォルダをリポジトリとして初期化します。この操作は、そのプロジェクトの中で最初に1回だけ実行します。なお、Gitのバージョンや設定によっては、初期化時に作られるブランチの名前がmasterになります。本ガイドはこの後の説明をmainブランチで進めるため、先にgit config –global init.defaultBranch mainを1回だけ実行して、既定の名前をmainに揃えておきます。続けてファイルを編集したら、git addでステージングエリアに変更を移し、git commitでその内容を履歴として記録します。記録された履歴は、git logで一覧表示して確認できます。
# 新しく作るリポジトリの既定ブランチ名をmainにする(最初に1回だけ、パソコン単位の設定)
git config --global init.defaultBranch main
# プロジェクトフォルダをリポジトリとして初期化(最初に1回だけ)
git init
# 編集したファイルをステージングエリアに追加
git add analysis.py
# ステージングエリアの内容をコミットとして記録
git commit -m "前処理: 欠損値を平均値で補完する処理を追加"
# これまでのコミット履歴を確認
git log
git addには、ファイルを個別に指定する方法のほかに、git add .のようにピリオドを指定して、変更されたファイルをまとめてステージングエリアに追加する方法もあります。ただし、後述する.gitignoreの設定が不十分な段階でgit add .を使うと、意図しないファイルまで追加してしまう危険があります。慣れないうちは、ファイル名を個別に指定するか、次に説明するgit statusで対象を確認してからaddする習慣をつけておくと安全です。git logを実行すると、コミットごとに割り当てられる固有のID(ハッシュ値と呼ばれる文字列)、作成者、日時、コミットメッセージが新しい順に表示されます。このハッシュ値を使えば、特定のコミット時点の状態に戻ることもできます。
Gitを使ううえで、addやcommitと並んで頻繁に使うのが、git statusとgit diffです。この2つは、今どういう状態にあるのかを確認するためのコマンドで、直接履歴を変更するわけではありません。だからこそ、いつでも気軽に実行してよいコマンドで、操作に迷ったときはまずこの2つを打つ、という習慣をつけておくと安心です。
git statusは、作業ディレクトリとステージングエリアの現在の状態を表示します。どのファイルが変更されていてまだステージングされていないか、どのファイルがステージング済みでコミット待ちの状態にあるか、Gitの管理下にまだ入っていない新しいファイルは何か、といった情報がまとめて分かります。git diffは、実際の変更内容を行単位で表示します。どの行が削除され、どの行が追加されたのかが、記号付きで一目で分かる形式で出力されます。
# 現在の変更状況を確認
git status
# 具体的な変更内容(差分)を確認
git diff
# addした後、コミット前の差分を確認したい場合
git diff --staged
addする前にgit statusで対象ファイルを確かめ、commitする前にgit diffやgit diff –stagedで中身を見直す。この2段階の確認を挟むだけで、意図しないファイルを間違ってコミットしてしまう事故や、コミットメッセージと実際の変更内容がずれてしまう事故を、かなりの割合で防げます。
コミットメッセージは、単なる作業ログではなく、将来の自分や、後からコードを引き継ぐ人に向けた伝言です。よいコミットメッセージは「何を」だけでなく「なぜ」まで書かれています。「修正」「更新」「変更」といった一言だけのメッセージは、後から履歴を見返したときにほとんど手がかりになりません。
たとえば「欠損値の補完方法を変更」というメッセージだけでは、何をどう変えたのかは分かっても、その判断の背景までは残りません。「前処理: 欠損値の補完を平均値からKNN法に変更(外れ値の影響を抑えるため)」のように、変更内容と理由をセットで書いておくと、数か月後に同じコードを見返したときの理解のスピードがまったく違います。1行目に変更内容を簡潔にまとめ、必要であれば空行を挟んで詳細を追記する、という書き方も広く使われています。表記のルールに正解はありませんが、日本語で構わないので、変更の内容と理由の両方を書くことを基本にしておくとよいでしょう。
ソフトウェア開発の現場では、コミットメッセージの冒頭に「fix:」(不具合の修正)や「feat:」(機能の追加)といった種類を表す接頭辞を付ける、Conventional Commitsと呼ばれる書き方の慣習も広まっています。分析コードのコミットにこうした接頭辞を必須にする必要はありませんが、「前処理:」「集計:」「可視化:」のように、変更の対象が一目で分かる言葉を頭に添えておくだけでも、後から履歴を検索するときの手がかりになります。
ここまで説明してきたリポジトリは、自分のパソコンの中だけに存在するローカルリポジトリです。これに対して、インターネット上のサーバーに置かれたリポジトリのコピーを、リモートリポジトリと呼びます。GitHubは、このリモートリポジトリを預かるホスティングサービスの代表格で、GitLabやBitbucketといった同種のサービスもあります。
ローカルリポジトリとリモートリポジトリは、git clone、git push、git pullという3つの操作でやり取りをします。git cloneは、リモートリポジトリの内容を丸ごと自分のパソコンにコピーしてくる操作です。git pushは、ローカルで作ったコミットをリモートリポジトリに送る操作、git pullは、リモートリポジトリの最新の状態をローカルに反映させる操作です。
# すでにあるリモートリポジトリを手元にコピーする
git clone https://github.com/ユーザー名/リポジトリ名.git
# ローカルのコミットをリモートリポジトリに送る(初回はブランチの追跡設定も行う)
git push -u origin main
# リモートリポジトリの最新状態を手元に取り込む
git pull origin main
手元でgit initから始めたリポジトリを、初めてGitHubへ上げる場合は、もう一手間が必要です。まずGitHubのサイトでアカウントを作り、画面上で空のリポジトリを1つ作成します。このとき、READMEなどを一緒に作る設定は外しておくと、手元の内容とぶつからずに済みます。次に、作成したリポジトリのURLを、手元のリポジトリに「送り先」として登録します。この送り先に付ける既定の名前がoriginで、上のgit push -u origin mainのoriginはこの登録名を指しています。git cloneで始めた場合はこの登録が自動で行われるため、この手順は不要です。
# 手元のリポジトリに、送り先(リモート)をoriginという名前で登録する
git remote add origin https://github.com/ユーザー名/リポジトリ名.git
# 登録できたか確認する
git remote -v
リモートリポジトリを使うことには、大きく2つの価値があります。1つはバックアップとしての価値です。ローカルリポジトリしかない状態で、パソコンが故障したり誤って削除したりすると、それまでの変更履歴ごとすべて失われてしまいます。リモートリポジトリに定期的にpushしておけば、手元の環境とは独立した場所に履歴の複製が残るため、この種の事故に対する保険になります。もう1つは共有としての価値です。同じリモートリポジトリに対して複数人がpushとpullを繰り返すことで、チームで1つの分析コードを育てていくことができます。

ある程度Gitに慣れてくると、次に出てくるのがブランチという概念です。ブランチは、既存のコードに影響を与えずに試行錯誤するための、いわば平行世界のようなものだとイメージすると理解しやすくなります。普段作業しているmainという名前のブランチ(かつてはmasterという名前が使われていましたが、現在はGitHub上の新規リポジトリなどでもmainが標準的な名称になっています)から、新しい試みのためのブランチを分岐させて作業し、うまくいったら本流であるmainに合流させる。これがブランチの基本的な使い方です。
本ガイドでは、ブランチの詳細な運用や、複数人の変更がぶつかったときの解消方法(コンフリクトの解決)までは深入りしません。まずは、mainブランチと、試したいことがあるときだけ作る作業ブランチという、最小限の2種類の使い分けから始めれば十分です。
# 新しい作業ブランチを作って切り替える
git switch -c feature/new-preprocessing
# (このブランチの上でファイルを編集し、addとcommitを行う)
# mainブランチに戻る
git switch main
# 作業ブランチの変更をmainに取り込む
git merge feature/new-preprocessing
試している前処理の方法がうまくいかなかった場合は、そのブランチをmainに合流させずに放置しておいても、mainブランチ自体には何の影響も及びません。この「失敗してもmainは無傷のまま」という安心感が、ブランチを使う最大の利点です。
ここまでの内容はGitの基本操作でしたが、ここから説明する.gitignoreは、実務でもっとも注意を払うべき項目です。Gitはフォルダの中のファイルをそのまま履歴として記録していく仕組みであるため、何も対策をしないと、顧客データを含むCSVファイルや、外部サービスに接続するためのAPIキー・パスワードといった認証情報まで、うっかりコミットしてしまう危険があります。特にリモートリポジトリとして公開設定のGitHubを使っている場合、こうした情報がインターネット上に公開され、外部から自由に閲覧できる状態になってしまいます。認証情報の流出は、悪意のある第三者による不正利用や、機械的にリポジトリを監視するボットによる短時間での不正使用につながるおそれがあり、実務では絶対に避けなければならない事故です。
この事故を防ぐための仕組みが.gitignoreです。プロジェクトの直下に.gitignoreという名前のテキストファイルを置き、その中にGitの管理対象から外したいファイルやフォルダのパターンを列挙しておくと、該当するファイルはgit statusやgit add .の対象から自動的に除外されます。ただしこれが効くのは、まだ一度もコミットしていないファイルに対してだけです。すでにコミットしてしまったファイルは、.gitignoreに書き足しても追跡され続けるため、git rm --cached ファイル名で追跡対象から外す操作が別途必要になります。認証情報を一度でもコミットしてしまった場合は、履歴からも消えていない点に注意してください。
# .gitignore の記述例
data/
*.csv
.env
__pycache__/
*.pyc
.ipynb_checkpoints/
venv/
data/フォルダやCSVファイルのようにサイズが大きく更新頻度の高いデータファイル、.envファイルのように認証情報を記載するファイル、__pycache__やvenvのように環境依存で再生成できるファイルなどを、あらかじめ除外対象にしておくのが基本の考え方です。プロジェクトを開始する最初の段階で.gitignoreを用意しておく習慣をつけておくと、後から慌てて対応する必要がなくなります。なお、すでに認証情報を含むファイルをコミットしてしまった後で.gitignoreに追加しても、過去の履歴には残ったままになりますので、その場合は該当する認証情報自体を無効化し、再発行するという対応が必要になります。この履歴からの削除作業は難易度が高いため、そもそもコミットする前に防ぐことが何よりも重要です。
データファイルと認証情報をコミットしないことは、Gitを使ううえでもっとも重要な注意点です。.gitignoreをプロジェクトの最初の段階で用意しておくことが、情報漏えい事故を防ぐ確実な備えになります。
データ分析の現場でGitを使ううえで、避けて通れないのがノートブックとの相性の問題です。拡張子が.ipynbのJupyterノートブックファイルは、見た目こそコードとグラフが並ぶ画面ですが、実体はJSON形式のテキストファイルです。ここに、コード本体だけでなく、セルを実行した際の出力(表示されたグラフの画像データや、実行結果のテキスト)まで、まとめて書き込まれる構造になっています。
この構造がGitとの相性を悪くします。コードを1行変更しただけのつもりでも、セルを再実行すると出力データも書き換わるため、git diffで差分を確認しようとすると、実際のコードの変更点が、大量の出力データの変化に埋もれてしまいます。グラフの画像データはbase64という形式でテキスト化された長い文字列としてファイルに含まれるため、diffの画面が意味の読み取れない文字の羅列で埋め尽くされることも珍しくありません。
この問題への対処として広く行われているのが、コミットする前にセルの出力をクリアしておくという運用です。JupyterLabのメニューから出力をすべて削除する操作を行ってから保存し、コードだけがすっきりした状態でコミットすることで、diffの見通しが大きく改善します。この作業を手作業でこまめに行うのが煩わしい場合は、コミットのタイミングで自動的に出力をクリアしてくれるnbstripoutというツールもあります。また、ノートブックをコードと文書に分離してPythonファイルやマークダウンファイルとして扱えるようにするjupytextというツールも存在し、diffの読みやすさを重視するチームで使われることがあります。本ガイドではこれらのツールの詳細な導入手順までは扱いませんが、ノートブックとGitの間にこうした相性の問題があることと、それに対応する選択肢が存在することは、知っておいて損はありません。
この章の冒頭で触れた「analysis_v2_最新_修正済み.ipynb」のような状態は、Gitを導入することで根本的に解消されます。ファイルを何個もコピーして残しておく必要はなくなり、1つのファイルの変更履歴として、いつでも過去のどの時点にも戻れるようになります。「先月の報告資料で使った数値は、どのコードから出したものだったか」という状況に直面したとき、コミット履歴をたどれば、その時点で実際に動いていたコードそのものを正確に取り出せます。これは「あの時の結果が再現できない」という、分析業務で起こりがちな困りごとに対する、もっとも直接的な備えです。
また、Gitの利用は、コードのレビュー文化への入口でもあります。ソフトウェア開発の現場では、コードの変更をリモートリポジトリに送る際、他のメンバーがその変更内容を確認してからmainブランチに取り込む、というレビューの手続きが広く定着しています。分析コードについても、変更内容を他者の目でチェックしてもらう文化が根づいているチームほど、前処理の誤りや集計ロジックの見落としが早い段階で発見されやすくなります。Gitによって変更履歴が可視化されることは、こうしたレビューの土台を作ることにもつながります。バージョン管理は、単に過去に戻るための機能にとどまらず、チームで分析コードを育てていくための共通言語としての役割も担っています。
『改訂2版 わかばちゃんと学ぶ Git使い方入門』(湊川あい著・DQNEO監修、シーアンドアール研究所):イラストを多用した対話形式でGitの基本操作を解説しており、コマンドライン操作に不慣れな入門者でも挫折しにくい構成になっています。本章で扱ったinit、add、commitの最小サイクルやリモートリポジトリとの連携をさらに手を動かしながら定着させたい場合の参考になります。
第1章では、Pythonの環境が本体とパッケージの二層構造でできていること、そしてプロジェクトごとに独立した部屋を用意する仮想環境(venv)の考え方を扱いました。venvは同じパソコンの中でプロジェクトごとにパッケージのバージョンを分ける仕組みとしては十分に機能します。しかし、OSそのものが違うパソコン同士や、自分の手元と本番のサーバーといった、パソコンをまたいだ環境の差異までは埋めてくれません。この最終章では、venvのさらに一段階外側にある「コンテナ」という発想と、その代表格であるDockerを扱います。あわせて、本ガイド全体の締めくくりとして、生成AIがコードを書く作業を加速させる時代に、学習者が何を自分の力として身につけておくべきかを整理します。
この章で扱う内容は次の通りです。
第1章のvenvによって、同じパソコンの中でプロジェクトごとにパッケージのバージョンを分けるという問題は解決しました。ところが、実務やチームでの分析を進めていくと、それとは別種の「動かない」に遭遇することがあります。自分のパソコンでは問題なく動いていたコードを、同僚のパソコンで実行してもらったところエラーが出る、あるいは自分のパソコンで作った分析用のプログラムを本番のサーバーに載せたところ、まったく同じコードのはずなのに結果が変わってしまう、といった事態です。
原因はいくつも重なり合っています。1つ目はOSの違いです。自分はWindowsで作業していても、同僚はMacを使っている、本番サーバーはLinuxで動いている、というのは珍しいことではありません。OSが変わると、内部で使われている数値計算のライブラリの挙動がわずかに異なったり、ファイルパスの区切り文字が違ったりすることがあります。2つ目はPython本体やパッケージのバージョンの違いです。requirements.txtでパッケージのバージョンを固定していたとしても、Python本体のバージョンが異なれば、パッケージの内部的な挙動が変わる場合があります。3つ目は、パッケージをインストールした順番の違いです。パッケージ同士が依存し合っている場合、インストールする順番によって、最終的に環境に入るバージョンの組み合わせがわずかに変わってしまうことがあります。
これらはいずれも、venvだけでは解決できない種類の問題です。venvが分離してくれるのは、あくまで同じOS・同じPython本体の上に乗っているパッケージ群だけです。OS自体の違いや、Python本体のインストール方法の違いまでは、venvの管轄の外にあります。分析用のコードを1人で書いて1人で使い続けるだけであれば、この問題が表面化する機会は多くありません。しかし、複数人で1つのプロジェクトを進めたり、自分のパソコンで作ったモデルを本番のシステムに組み込んだりする段階になると、「自分のPCでは動くのに」という状況は決して珍しい話ではなくなります。この章で扱うDockerは、こうしたOSレベルの差異まで含めて環境をまるごと閉じ込め、どこで実行しても同じ結果が再現される状態を作るための道具です。
Dockerが採用しているコンテナという考え方を理解するには、貨物輸送で使われる海上コンテナを思い浮かべると分かりやすいです。中身が家具であっても機械部品であっても、規格化された箱に詰めてしまえば、船でもトラックでも同じ手順で運搬できます。ソフトウェアのコンテナも同じ発想で、アプリケーションの実行に必要なファイル一式やライブラリ、設定を丸ごと1つの箱に詰め込み、その箱さえあれば、どのパソコンやサーバーの上でも中身が変わらないまま動かせるようにする仕組みです。
似た言葉に仮想マシンがあります。仮想マシンは、1台のパソコンの中に、OSごと丸ごと独立した「もう1台のパソコン」を作り出す技術です。仮想マシンを起動するたびに、独立したOSが1つ立ち上がるため、起動には数十秒から数分かかり、ディスク容量も数ギガバイト単位で消費します。これに対してコンテナは、OSの中核部分(カーネルと呼ばれる基盤部分)を複数のコンテナで共有し、アプリケーションの実行に必要な部分だけを個別に用意します。厳密には、WindowsやmacOSでDocker Desktopを使う場合、共有されるのはパソコンのOSそのものではなく、その上で1つだけ動くLinux(WindowsではWSL 2、macOSでは軽量な仮想マシン)のカーネルです。それでも、コンテナを増やすたびにOSが増えるわけではないという利点は変わりません。OSをまるごと1つ余分に立ち上げる必要がないため、起動は数秒程度で済み、ディスク容量も仮想マシンに比べてはるかに小さく抑えられます。この軽さが、コンテナが仮想マシンに代わって広く使われるようになった最大の理由です。
Dockerは、このコンテナという仕組みを手軽に扱えるようにしたソフトウェアです。専門的な知識がなくても、後述するいくつかのコマンドを覚えるだけで、コンテナを作ったり動かしたりできるようになっています。

Dockerを理解するうえでまず押さえておきたいのが、イメージとコンテナという2つの用語です。この2つの関係は、料理におけるレシピと、そのレシピをもとに実際に作られた料理の関係に近いものです。
イメージとは、OSの基本部分、Pythonの本体、必要なパッケージ、設定ファイルといった、環境を構成する要素一式をまとめて記録した設計図のようなものです。イメージ自体は静止した状態のファイルの集まりであり、それ単体では何かの処理を実行しているわけではありません。このイメージをもとに実際に起動し、処理を実行している状態のものがコンテナです。同じ1つのイメージから、コンテナはいくつでも作り出すことができます。1つのレシピ(イメージ)から、何皿分の料理(コンテナ)でも作れるのと同じ関係です。
イメージを一から自分で作ることもできますが、多くの場合はすでに公開されている既製のイメージを土台にして、そこに必要なものを付け足していきます。その既製イメージが多数公開されている場所がDocker Hubです。Docker Hubは、世界中の開発者や企業が作成したDockerイメージを検索し、無料でダウンロードできる公開のリポジトリ(保管場所)です。Pythonの公式イメージ、後述するJupyter用のイメージ、データベースのイメージなど、幅広い種類のイメージが公開されており、docker pullというコマンド1つで手元にダウンロードできます。ゼロから環境を組み立てるのではなく、目的に合った既製イメージを土台に選び、そこに自分のプロジェクト固有の設定を足していくというのが、Dockerを使ううえでの基本的な進め方です。
Dockerを手元で使うには、Docker Desktopというアプリケーションを入れます。公式サイト(docker.com)からお使いのOS向けのインストーラを入手し、指示に従って導入したうえで、アプリケーションを起動しておきます。以降のdockerコマンドは、Docker Desktopが起動している状態でないと動きません。この点はGitと違うところで、コマンドを打つ前にアプリが立ち上がっているかを確認する習慣をつけておくと、原因の分かりにくいエラーを避けられます。
Windowsで導入する際には、いくつか前提条件があります。対応しているのはWindows 10とWindows 11の64ビット版のうち、Pro・Enterprise・Educationの各エディションです。また、コンテナを動かす土台としてWSL 2(Windows Subsystem for Linux 2)という機能が既定で使われるため、これが有効になっている必要があります。加えて、パソコンのBIOSまたはUEFIの設定で仮想化支援機能が有効になっていること、メモリが8GB以上あることも求められます。会社から貸与されたパソコンでは、これらの設定変更に管理者権限が必要な場合がありますので、情報システム担当者に確認しながら進めてください。
導入できたかどうかは、第1章で開いたのと同じターミナルで次のコマンドを打って確かめます。バージョン番号が表示されれば使える状態です。
docker --version
Dockerの操作に慣れる最初の一歩として、公式に提供されているJupyter入りのイメージを、実際に手元で動かしてみます。Jupyter Docker Stacksと呼ばれるプロジェクトが、データ分析でよく使うパッケージをあらかじめ組み込んだイメージ群を公開しており、pandasやnumpy、matplotlibなどを含むscipy-notebookというイメージを例に使います(このイメージ群は以前はDocker Hub上のjupyter名義で配布されていましたが、2023年からquay.ioという別のレジストリ(イメージの配布場所)に移管されています)。
docker run -p 8888:8888 -v "${PWD}:/home/jovyan/work" quay.io/jupyter/scipy-notebook
コマンドの中のjovyanは、このJupyter用イメージにあらかじめ用意されている利用者の名前で、コンテナの中での作業用フォルダがその人のホームディレクトリの下に置かれている、というだけの意味です。自分で決める名前ではないので、そのまま書き写してください。
このコマンドを実行すると、イメージが手元になければ自動的にダウンロードされたうえでコンテナが起動し、ターミナルにJupyterへアクセスするためのURL(トークンと呼ばれる認証用の文字列が付いたもの)が表示されます。そのURLをブラウザで開けば、いつも通りのJupyterの画面が立ち上がります。ここで自分のパソコンにPythonや各種パッケージを一切インストールしていなくても、Jupyterによる分析環境がすぐに手に入るという点が、Dockerの手軽さを実感しやすい部分です。
このコマンドの中の-pと-vという2つのオプションが、Docker入門者にとって最初の壁になりやすい部分ですので、丁寧に見ておきます。
-p(ポートフォワード)は、コンテナの中で動いているJupyterに、コンテナの外である自分のパソコンのブラウザからアクセスできるようにするための橋渡しです。コンテナは、いわば外の世界から隔離された箱であり、箱の中でJupyterが8888番という窓口(ポート番号)で待ち構えていても、そのままでは箱の外からその窓口を見ることができません。-p 8888:8888という指定は、「自分のパソコン側の8888番の窓口と、コンテナの中の8888番の窓口をつなぐ」という意味です。コロンの左側が自分のパソコン側の番号、右側がコンテナの中の番号にあたり、たとえば-p 18888:8888のように左側の番号だけを変えれば、自分のパソコンのhttp://localhost:18888でアクセスできるようになります。
-v(ボリュームマウント)は、コンテナの中のフォルダと、自分のパソコンの中の特定のフォルダを結びつけ、両者が同じ中身を共有するようにする指定です。コンテナは基本的に、停止して削除してしまうと、その中で作成・変更したファイルはすべて消えてしまいます。分析の成果であるノートブックのファイルがコンテナの中にしか存在しない状態では、コンテナを削除した瞬間に作業内容が失われてしまいます。-v “${PWD}:/home/jovyan/work”という指定は、「自分のパソコンの現在のフォルダ(${PWD})を、コンテナの中の/home/jovyan/workというフォルダに重ねて見せる」という意味であり、こうしておけば、コンテナの中のJupyterで保存したノートブックは、実際には自分のパソコンのフォルダにそのまま保存されます。コンテナを削除しても、自分のパソコン側に残ったファイルは消えません。-pがコンテナの中への「入口」を開く指定であるのに対し、-vはコンテナの中と外で「ファイルの中身」を共有するための指定である、という役割の違いを意識しておくと理解しやすくなります。
-p(ポートフォワード)はコンテナの中の窓口を外から使えるようにする指定、-v(ボリュームマウント)はコンテナの中と外でファイルを共有する指定です。この2つを指定し忘れると、「ブラウザからJupyterに繋がらない」「コンテナを消したらノートブックまで消えた」という初心者がつまずきやすい事態につながります。

docker runで既製のイメージを動かすだけでも便利ですが、実際のプロジェクトでは、そのプロジェクト独自のパッケージ構成を組み込んだ、自分専用のイメージを作りたい場面が出てきます。そのための設計図を書くファイルがDockerfileです。Dockerfileは、テキストエディタで書ける単純なファイルで、拡張子は付けずに「Dockerfile」という名前でプロジェクトのフォルダに置きます。
最小構成のDockerfileを見てみます。
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "analyze.py"]
1行ずつ意味を確認します。FROMは、このイメージの土台として、どの既製イメージを使うかを指定します。ここではPython公式が配布している軽量版(slim)のPython 3.14イメージを土台にしています。WORKDIRは、これ以降のコマンドを実行する作業フォルダをコンテナの中に指定するもので、ここでは/appという名前のフォルダを作業場所にしています。COPYは、自分のパソコン側にあるファイルを、コンテナの中にコピーする命令です。1つ目のCOPYでrequirements.txtだけを先にコピーし、RUNでpip installを実行してパッケージ一式をインストールしたうえで、2つ目のCOPYでプロジェクトの残りのファイルをコピーしています。この順番には理由があり、requirements.txtに変更がない限り、Dockerはパッケージのインストール結果を再利用(キャッシュ)してくれるため、コードだけを直したときの再ビルドが速くなります。最後のCMDは、docker runでこのイメージからコンテナを起動したときに、最初に実行されるコマンドを指定します。ここではanalyze.pyというPythonスクリプトを実行する設定にしています。
このDockerfileをもとにイメージを作る(ビルドする)コマンドと、そのイメージからコンテナを起動するコマンドは次の通りです。
# Dockerfileがあるフォルダでイメージをビルドする(myanalysisは自分で付ける名前)
docker build -t myanalysis .
# ビルドしたイメージからコンテナを起動する
docker run myanalysis
Dockerfileが持つ本質的な価値は、単にコマンドを短縮できることではありません。「このプロジェクトの環境を作るには、このOSを土台に、このパッケージを、この順番でインストールする」という環境構築の手順を、口頭の説明や手作業の記録としてではなく、実行可能なコードとして書き残せるという点にあります。この考え方は、インフラ(サーバーや実行環境)の構成をコードとして管理するInfrastructure as Code(略してIaC)と呼ばれる大きな潮流の入口にあたります。Dockerfileという1つのテキストファイルさえ手元にあれば、誰が実行してもほぼ同じ環境をその場で再現できます(土台イメージのタグの中身は更新されうるため、厳密に固定したい場合は、バージョン番号や、イメージの中身そのものを一意に指す識別子(ダイジェストと呼ばれます)を明示して指定します)。手順書を人間が読んで手作業でなぞる必要がなく、環境構築という作業そのものが自動化され、再現性が保証されるという点が、Dockerfileの本質的な価値です。
第1章では、pip freezeでrequirements.txtを書き出しておくことで、パッケージのバージョンまで固定した環境を別のパソコンで再現できるという話をしました。Dockerfileは、このrequirements.txtの効果をさらに一段階徹底させる仕組みだと捉えると理解しやすくなります。
requirements.txt単体でも、パッケージのバージョンは固定できます。しかし、requirements.txtに書かれているのはあくまでパッケージの情報だけであり、Python本体のバージョンや、OSの種類そのものまでは固定してくれません。同じrequirements.txtを使っても、実行するパソコンのPython本体のバージョンが異なれば、微妙に異なる結果になる可能性は残ります。Dockerfileでは、FROM python:3.14-slimのようにPython本体のバージョンとOSの土台まで含めて指定したうえで、その中でrequirements.txtを使ってパッケージをインストールします。つまり、「OS」「Python本体のバージョン」「パッケージとそのバージョン」という3層をまとめて指定できるため、環境の再現性を大きく高められます。
第1章で扱ったrequirements.txtが積み残していた「同じOS・同じPython本体である」という前提を、Dockerfileが引き取って解消する、という関係だと考えると、本ガイドの第1章と第12章がひとつながりの話としてつながります。venvとrequirements.txtがパッケージレベルの再現性を担い、Dockerがその外側にあるOSレベルの再現性まで担う、という役割分担です。
ここまででDockerの基本的な使い方は一通り押さえましたが、実際にどのような場面でDockerが活躍するのかを整理しておきます。
| 場面 | Dockerが解決すること |
|---|---|
| チームでの環境統一 | メンバーそれぞれのパソコンのOSやPythonのバージョンが違っても、同じDockerfileから作った同じコンテナの中で作業すれば、全員が同一の環境で分析を進められる |
| 本番システムへの受け渡し | 手元で作った分析コードや機械学習モデルを、本番のサーバーに載せる際、開発環境と本番環境の差異による「動かない」を防ぐ。この受け渡しの仕組みを体系化した領域がMLOpsと呼ばれ、Dockerはその土台の1つになっている |
| クラウドでの実行 | クラウド上の計算資源でコードを実行するサービスの多くが、Dockerのコンテナ形式を実行の単位として受け付けており、コンテナ化しておくことでクラウドへの展開がスムーズになる |
一方で、現実的な位置づけも押さえておきます。1人で自分のパソコンの中だけで学習や分析を完結させている段階では、Dockerは必須の道具ではありません。venvと第1章で扱った仮想環境の考え方だけでも、日々の学習や個人での分析作業は十分に成立します。Dockerが真価を発揮するのは、複数人での共同作業や、開発環境と本番環境をまたいだ受け渡しが発生する場面です。学習の初期段階から無理にDockerの習得を急ぐ必要はなく、チームでの分析や、モデルを本番に載せる段階に差しかかったタイミングで、あらためてこの章を読み返していただく、という使い方でも十分です。
本ガイドの締めくくりとして、生成AIが広く使われるようになった現在、プログラミングの学習をどう位置づけるかについて触れておきます。
コードを書くという作業そのものは、生成AIによって大きく加速されています。やりたいことを日本語で説明すれば、関数の骨組みやよく使うパターンのコードがすぐに提案される場面は、この先さらに一般的になっていくと考えられます。この変化自体は、作業効率を高めるものであり、否定する理由はありません。
ただし、生成AIが提案したコードを読んで、それが本当に意図した通りに動くかを検証する力、そして、そもそも何を計算させるべきか、どのデータをどう扱うべきかという課題そのものを設計する力は、生成AIに代わってもらうことが難しい部分です。提案されたコードの中に、本ガイドで扱ってきたような、実行順の落とし穴や、データ型の扱い方の誤り、集計単位のずれといった問題が紛れ込んでいないかを見抜くには、コードを読み解く基礎的な力が土台として必要になります。生成AIが書いたコードであっても、それを実行し、その結果を報告する人間が、最終的な内容に責任を持つことに変わりはありません。
学習の進め方としては、既存のコードをそのまま書き写す「写経」から始め、次にその一部を自分の目的に合わせて書き換える「改変」、そして最終的には白紙の状態から自分で設計して書く「ゼロからの実装」へと段階を踏んでいく進め方が、昔から効果的だとされてきました。この段階を踏む中で欠かせないのが、エラーメッセージと自力で向き合う経験です。エラーが出るたびにすぐ答えを求めるのではなく、エラーメッセージ自体を読み、どの層で何が起きているのかを考え、原因を切り分けてみることが大切です。この経験の積み重ねは、遠回りに見えて、生成AIをうまく使いこなすための土台にもなります。生成AIに指示を出す際、「なぜ動かないのか」「何が足りないのか」を的確に言語化できるかどうかは、自分自身がエラーの構造をどれだけ理解しているかに大きく左右されるためです。曖昧な指示からは曖昧な提案しか返ってきませんが、問題の所在を具体的に説明できれば、生成AIからの提案の精度も上がります。
本ガイドは、第1章の環境構築から始まり、Jupyterの使い方、Python自体の基礎文法、NumPyやPandasによるデータの扱い方、可視化、Gitによるバージョン管理、そしてこの章のDockerまでを扱ってきました。これらはいずれも、生成AIがどれだけ発展しても、自分の中に置いておく価値が薄れない基礎知識です。生成AIという強力な支援を得ながらも、自分の頭で考え、自分の目でコードを確かめる力を土台に据えて、データサイエンスの学習と実務を積み重ねていっていただければと思います。
コードを書く作業は生成AIによって加速しますが、提案されたコードを読んで検証する力、データと課題を自ら設計する力は、使う人自身の中に持っておく必要があります。写経から改変、ゼロからの実装へと段階を踏み、エラーと自力で向き合った経験を積み重ねることが、生成AIへの的確な指示力にもつながります。
『仕組みと使い方がわかる Docker&Kubernetesのきほんのきほん』(小笠原種高、マイナビ出版):イラストを多用しながらDockerとKubernetesの仕組みをやさしく解説した入門書です。この章で扱ったイメージとコンテナの関係、docker runの基本操作をさらに広げ、複数のコンテナを組み合わせて運用するKubernetesの世界まで見通したい場合の橋渡しになります。
本ガイドでは、環境構築の仕組みとJupyterの作法から始めて、Pythonの基礎文法(データ型・制御構文・関数)、データ分析の中核であるNumPyとPandas、可視化、実務を支えるユーティリティ、そしてGitとDockerによるコードと環境の管理までを一続きに見てきました。
全編を通して繰り返してきたのは「仕組みを理解すれば、自力で前へ進める」ということでした。仮想環境の仕組みが分かれば、パッケージの衝突が起きても原因を切り分けられます。エラーメッセージの読み方が分かれば、表示された内容から次に何を確認すべきかを判断できます。ベクトル化の発想が分かれば、Pandasのコードは暗記ではなく、必要な処理を組み立てて書けるようになります。GitとDockerの考え方が分かれば、「あのときの分析が再現できない」という実務のトラブルを仕組みで防げます。
本ガイドを終えた方の次の一歩として、当社のデータサイエンスシリーズは次のように接続しています。
学習の合言葉は「写経→改変→ゼロから」です。動くコードをそのまま打って動かし、次に自分のデータに合わせて改変し、最後に何も見ずに組み立てる。この3段階を回した経験の量が、生成AIに的確な指示を出す力にも、生成されたコードを見抜く力にも直結します。
Anagraftでは、AIプロジェクトの構想・課題設計から、データ分析・機械学習モデルの開発、AI人材の育成まで一貫したご支援を行っています。会社概要・ご支援内容の詳細は、以下の資料からご覧いただけます。
各章末でご紹介した参考書籍の一覧です。学習の段階に合わせてお選びください。