CSVを受け取るたびに、欠損値がどこにいくつあるか、同じ行が二重に入っていないか、列の型が想定どおりかを確かめる作業が発生します。毎回pandasのコードを書くのは面倒なので、Streamlitで「アップロードして確認して直して落とす」までを1画面にまとめました。
実際に8行のCSVを読み込ませ、欠損値3件と重複行2件を画面で確認し、クリーニング後に行数が8から6へ変わる比較結果とcleaned.csvのダウンロード準備までたどり着いています。実装は1ファイルで完結する構成です。
Streamlitの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。Streamlitとpandasを使ってCSVクリーニングアプリを作るカリキュラムを始めます。概要紹介。
Streamlitとpandasの役割と使い方を学ぶCSVクリーニングアプリの要件を整理する 完成コードと実行結果を確認する 最後に完成した画面を実際に操作して確かめる 具体的にやること。
アップローダーからCSVファイルを受け取るUTF-8で読めないCSVをcp932で読み直す 読み込み直後の行数・列数・欠損値の合計・重複行数を表示する 読み込んだ表の先頭10行を表示する 列ごとのデータ型・欠損値数・欠損率を一覧する 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、streamlit、pandas Streamlitとpandasとは。
Streamlit:Streamlitは、Pythonのスクリプトを書くだけでブラウザに表示できる画面を作れるWebアプリ用ライブラリpandas:pandasは、CSVのような表形式のデータを読み込んで集計や変換を行うデータ分析ライブラリ Streamlitで作るCSVクリーニングアプリの要点。
型をそろえるconvert_dtypes重複行を消すdrop_duplicate_rows欠損値を扱うfill_missing CSVクリーニングアプリの要件定義。
8行のCSVで欠損値3件と重複行2件が表示される 列ごとの欠損値数と欠損率が一覧に並ぶ 重複行を削除する設定で行数が8から6へ変わる 比較表に行数・列数・欠損値の合計・重複行数が並ぶ クリーニング後の欠損値の合計が0になるcleaned.csvのダウンロード準備完了が表示される INTRO: Monaco EditorでCSVクリーニングアプリを実装。
コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。LINE 001: モジュール全体の説明文。このアプリ全体の役割を説明するドキュメント文字列です。
StreamlitとpandasでCSVの欠損値・重複行・データ型を整えるアプリであることを示しています。LINE 003: ioモジュールの読み込み。アップロードされたバイト列を扱うためにioモジュールを読み込んでいます。
後でBytesIOを使ってCSVデータをメモリ上で扱うために必要です。LINE 005: pandasの読み込み。表形式データを扱うためのpandasライブラリをpdという名前で読み込んでいます。
CSVの読み込みやデータ加工の中心的な機能を提供します。LINE 006: streamlitの読み込み。Webアプリの画面を作るためのstreamlitライブラリをstという名前で読み込んでいます。
ボタンや表の表示など画面部品を作るのに使います。LINE 009: 確認対象の画面パス定義。動作確認したい画面のパスをリストとして定義しています。
ここではトップページのみを対象としています。LINE 012: データ型選択肢の定義開始。データ型変換の選択肢を格納するリストの定義を始めています。
この後に続く各項目がセレクトボックスの選択肢になります。LINE 013: 変更しない選択肢。データ型を変更しない場合の選択肢文字列です。
ユーザーが列の型をそのまま維持したいときに選びます。LINE 014: 文字列型への変換選択肢。列を文字列型(string)に変換する選択肢です。
数値や日付を含む列でも文字として扱いたいときに選びます。LINE 015: 整数型への変換選択肢。列を整数型(Int64)に変換する選択肢です。
小数点を含む数値でも四捨五入して整数として扱いたいときに選びます。LINE 016: 小数型への変換選択肢。列を小数型(float)に変換する選択肢です。
数値データを小数として扱いたいときに選びます。LINE 017: 日時型への変換選択肢。列を日時型(datetime)に変換する選択肢です。
日付や時刻を表す文字列を日時データとして扱いたいときに選びます。LINE 018: データ型選択肢リストの終了。DTYPE_OPTIONSリストの定義を閉じています。
これで5種類のデータ型選択肢がまとまりました。LINE 019: 欠損値処理選択肢の定義開始。欠損値の処理方法を格納するリストの定義を始めています。
この後に続く各項目がセレクトボックスの選択肢になります。LINE 020: 平均値・最頻値で埋める選択肢。数値列は平均値、それ以外の列は最頻値で欠損値を埋める選択肢です。
統計的にバランスの良い補完方法として使われます。LINE 021: 欠損行削除の選択肢。欠損値を含む行そのものを削除する選択肢です。
欠損が多いデータを完全に除去したいときに選びます。LINE 022: 前方補完の選択肢。直前の行の値で欠損値を埋める前方補完の選択肢です。
時系列データなどで直前の値を引き継ぎたいときに使います。LINE 023: 指定値で埋める選択肢。ユーザーが指定した値で欠損値を埋める選択肢です。
この選択肢を選ぶと後で入力欄が表示されます。LINE 024: 何もしない選択肢。欠損値をそのまま残す選択肢です。
欠損値処理を行いたくない場合に選びます。LINE 025: 欠損値処理選択肢リストの終了。MISSING_OPTIONSリストの定義を閉じています。
これで5種類の欠損値処理方法がまとまりました。RUN 1/7: 選択肢の定義まで入力して中身を確かめる。データ型と欠損値処理の選択肢をリストとして定義した状態です。
画面のセレクトボックスに並ぶ文言はこの2つのリストが元になるため、件数と先頭の値を表示して並び順を確認します。CHECK 1/7: 途中実行に成功。
データ型の選択肢5件 ['変更しない', '文字列(string)', '整数(Int64)', '小数(float)', '日時(datetime)'] 欠損値処理の選択肢5件 数値列は平均値・その他の列は最頻値で埋める RETURN 01: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 029: CSV読み込み関数のキャッシュ設定。同じデータで関数を何度も呼んでも再計算しないように、Streamlitのキャッシュ機能を関数に付けています。読み込み処理の負荷を軽減する役割があります。
LINE 030: CSV読み込み関数の定義。アップロードされたバイト列を受け取り、DataFrameを返す関数load_csvを定義しています。この関数がCSVの取り込み処理の入口になります。
LINE 031: CSV読み込み関数の説明文。この関数がアップロードされたCSVのバイト列をDataFrameへ読み込む処理であることを説明するドキュメント文字列です。LINE 032: 読み込み処理の例外対策開始。
文字コードのエラーに備えて、読み込み処理をtry文で囲んでいます。この後の処理でエラーが起きた場合の代替処理につなげます。LINE 033: バイト列からCSVを読み込む。
バイト列をio.BytesIOでメモリ上のファイルとして扱い、pandasのread_csvでDataFrameに変換しています。標準的な文字コードでの読み込みを試みています。LINE 034: 文字コードエラーの捕捉。
UTF-8での読み込みに失敗したときにUnicodeDecodeErrorを捕捉しています。日本語のCSVでよくある文字化けに対応するための処理です。LINE 035: cp932での再読み込み。
文字コードエラーが起きた場合に、Windowsでよく使われるcp932(Shift-JIS系)で読み込み直しています。これにより日本語CSVの文字化けを防いでいます。RUN 2/7: CSVを読み込む関数を試す。
アップロードされたバイト列をDataFrameへ変換する関数が書けました。小さなCSVを渡して、行数と列名、欠損値の合計が取れるかを確かめます。CHECK 2/7: 途中実行に成功。
行数2 列名 ['name', 'age'] 欠損値の合計1 RETURN 02: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 039: 列プロファイル関数の定義。
DataFrameの各列についてデータ型や欠損状況をまとめる関数profile_columnsを定義しています。この関数の戻り値は画面上の一覧表示に使われます。LINE 040: 列プロファイル関数の説明文。
この関数が列ごとのデータ型・欠損値数・欠損率をまとめた表を返す処理であることを説明するドキュメント文字列です。LINE 041: 集計結果のDataFrame生成開始。集計結果をまとめた新しいDataFrameを作成する処理を開始しています。
この後の辞書がそのまま表の列になります。LINE 042: 辞書データの開始。DataFrameの元になる辞書データの定義を開始しています。
各キーが列名として表に表示されます。LINE 043: 列名一覧の取得。元のDataFrameの列名一覧をリストとして取得し、「列名」という列にしています。
どの列についての情報かを示すための項目です。LINE 044: データ型一覧の取得。各列のデータ型を文字列に変換してリスト化し、「データ型」という列にしています。
列ごとの現在の型を一覧で確認できるようにしています。LINE 045: 欠損値数の集計。各列の欠損値の数を数えてリスト化し、「欠損値数」という列にしています。
isnaで欠損を判定しsumで合計しています。LINE 046: 欠損率の計算開始。各列の欠損率をパーセントで計算するリスト内包表記を開始しています。
次の行で実際の計算処理が続きます。LINE 047: 欠損率の算出処理。欠損値の割合を計算し100倍してパーセント表示にし、小数第1位で丸めています。
列ごとの欠損の程度を分かりやすい数値にしています。LINE 048: 辞書データの終了。DataFrameの元になる辞書データの定義を閉じています。
これで列名・データ型・欠損値数・欠損率の4項目がそろいました。LINE 049: DataFrame生成のかっこ閉じ。pd.DataFrameの呼び出しを閉じています。
ここまでの辞書データがまとめて表形式に変換されます。LINE 050: profile_columns関数の終了。pd.DataFrame呼び出し全体のかっこを閉じ、集計結果の表を関数の戻り値として返しています。
RUN 3/7: 列ごとの欠損値数をまとめる関数を試す。列名・データ型・欠損値数・欠損率を1つの表にまとめる関数ができました。欠損値を含む小さな表を渡し、4つの列がそろって返るかを確認します。
CHECK 3/7: 途中実行に成功。列名 データ型 欠損値数 欠損率(%) name object 1 50.0 age float64 1 50.0 RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 054: 表の指標算出関数の定義。DataFrame全体の行数や欠損値数などをまとめる関数frame_statsを定義しています。クリーニング前後の比較に使われます。
LINE 055: 表の指標算出関数の説明文。この関数が行数・列数・欠損値の合計・重複行数をまとめて返す処理であることを説明するドキュメント文字列です。LINE 056: 指標をまとめた辞書の返却開始。
各種指標をまとめた辞書を返す処理を開始しています。この後の各行が辞書の項目になります。LINE 057: 行数の取得。
len関数でDataFrameの行数を取得し、「行数」という項目にしています。データ全体の件数を表す指標です。LINE 058: 列数の取得。
DataFrameのshape属性から列数を取得し、「列数」という項目にしています。データにいくつの項目があるかを表す指標です。LINE 059: 欠損値合計の集計。
全セルの欠損値を判定して合計し、「欠損値の合計」という項目にしています。データ全体でどれだけ欠損があるかを示す指標です。LINE 060: 重複行数の集計。
完全に一致する重複行の数を数えて、「重複行数」という項目にしています。データの重複具合を確認するための指標です。LINE 061: frame_stats関数の終了。
指標をまとめた辞書の定義を閉じ、関数の戻り値として返しています。これで4つの指標がひとまとめになります。RUN 4/7: 表全体の指標を返す関数を試す。
行数・列数・欠損値の合計・重複行数をまとめて返す関数です。この4項目がクリーニング前後の比較表に並ぶので、値が正しく数えられるかを見ておきます。CHECK 4/7: 途中実行に成功。
{'行数': 3, '列数': 2, '欠損値の合計': 1, '重複行数': 1} RETURN 04: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 065: データ型変換関数の定義。
列ごとに選ばれたデータ型へ変換する関数convert_dtypesを定義しています。DataFrameと変換方法の辞書を受け取ります。LINE 066: データ型変換関数の説明文。
この関数が列ごとに選ばれたデータ型へ変換し、変換できない値は欠損値にする処理であることを説明するドキュメント文字列です。LINE 067: 元データのコピー作成。元のDataFrameを変更しないようにコピーを作成しています。
以降の変換処理はこのコピーに対して行われます。LINE 068: 列ごとの変換処理の開始。choicesに含まれる列名と選択されたデータ型の組を1つずつ取り出す繰り返し処理を開始しています。
この後の条件分岐で実際の変換が行われます。LINE 069: 文字列型への変換判定。選択されたデータ型が文字列(string)かどうかを判定しています。
一致した場合に次の行で変換処理が行われます。LINE 070: 文字列型への変換実行。対象の列をastypeで文字列型(string)に変換しています。
数値や日付を含む列でも文字として統一したいときに使われます。LINE 071: 整数型への変換判定。選択されたデータ型が整数(Int64)かどうかを判定しています。
一致した場合に次の行で変換処理が行われます。LINE 072: 整数型への変換実行。数値に変換したうえで四捨五入し、欠損に対応した整数型Int64に変換しています。
変換できない値は自動的に欠損値になります。LINE 073: 小数型への変換判定。選択されたデータ型が小数(float)かどうかを判定しています。
一致した場合に次の行で変換処理が行われます。LINE 074: 小数型への変換実行。対象の列を数値に変換し、小数型float64に変換しています。
変換できない値は欠損値として扱われます。LINE 075: 日時型への変換判定。選択されたデータ型が日時(datetime)かどうかを判定しています。
一致した場合に次の行で変換処理が行われます。LINE 076: 日時型への変換実行。対象の列を日時型に変換しています。
日付として解釈できない値は欠損値になります。LINE 077: 変換結果の返却。すべての列の変換処理が終わったコピーを関数の戻り値として返しています。
これで型変換後のDataFrameが得られます。RUN 5/7: データ型を変換する関数を試す。選ばれたデータ型へ列を変換する関数ができました。
数字と文字が混ざった列を整数へ変換し、変換できない値が欠損値になることを確認します。CHECK 5/7: 途中実行に成功。変換後のデータ型Int64 変換後の値 [np.int64(28), np.int64(35), <NA>] RETURN 05: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 081: 重複行削除関数の定義。重複行を削除するかどうかを制御する関数drop_duplicate_rowsを定義しています。
DataFrameと有効・無効のフラグを受け取ります。LINE 082: 重複行削除関数の説明文。この関数が完全に重複する行を最初の1行だけ残して削除する処理であることを説明するドキュメント文字列です。
LINE 083: 重複削除の無効判定。重複行削除が無効に設定されているかどうかを判定しています。無効の場合は次の行で元データをそのまま返します。
LINE 084: 重複削除しない場合の返却。重複行削除が無効な場合に、元のDataFrameのコピーをそのまま返しています。データを変更せずに保持したい場合に使われます。
LINE 085: 重複行の削除実行。drop_duplicatesで完全に重複する行を除去し、reset_indexで行番号を振り直しています。これにより重複のないきれいなデータが得られます。
RUN 6/7: 重複行を削除する関数を試す。完全に重複する行を最初の1行だけ残して削除する関数です。同じ行を含む表を渡し、削除前後の行数が変わるかを確かめます。
CHECK 6/7: 途中実行に成功。削除前3重複1 削除後2 RETURN 06: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 089: 欠損値処理関数の定義。データフレームと処理方法、埋める値を受け取り、欠損値を処理した結果を返す関数を定義しています。この関数が画面5番の処理の中心になります。
LINE 090: 関数の説明文。この関数が選択された方法に応じて欠損値を処理することを説明するドキュメント文字列です。処理内容を後から読み返すときの目印になります。
LINE 091: 元データのコピー作成。元のデータフレームを書き換えないように、コピーを作成して以降の処理に使用します。こうすることで元データを安全に保ったまま加工できます。
LINE 092: 行削除方法かどうかの判定。処理方法が「欠損値を含む行を削除する」かどうかを確認しています。条件に一致した場合は次の行で行削除処理が行われます。
LINE 093: 欠損行を削除して返す。欠損値を含む行をすべて削除し、インデックスを振り直した結果をすぐに返しています。ここで処理は終了し、以降のコードは実行されません。
LINE 094: 前方補完方法かどうかの判定。処理方法が「直前の行の値で埋める(前方補完)」かどうかを確認しています。条件に一致した場合は次の行で前方補完が行われます。
LINE 095: 前方補完して返す。直前の行の値で欠損値を埋める前方補完を行い、その結果をすぐに返しています。ここで処理は終了し、以降のコードは実行されません。
LINE 096: 列ごとの処理ループ開始。ここから先の処理は列ごとに欠損値を埋める方法のために、データフレームの各列を順番に取り出して処理します。LINE 097: 欠損値の有無を確認。
対象の列に欠損値が1つも無いかどうかを確認しています。欠損値が無ければ処理する必要がないため、次の行でスキップされます。LINE 098: 欠損値が無い列はスキップ。
欠損値が存在しない列については処理をせず、次の列の処理へ進みます。無駄な処理を避けるための工夫です。LINE 099: 指定値埋め方法かどうかの判定。
処理方法が「指定した値で埋める」かどうかを確認しています。条件に一致した場合はユーザーが入力した値で欠損値を埋める処理に進みます。LINE 100: 入力値を数値に変換。
ユーザーが入力した埋める値を数値に変換しようと試み、変換できない場合は欠損値として扱います。この結果は次の行の判定に使われます。LINE 101: 数値列かどうかの判定。
対象の列が数値型であり、かつ入力値が数値に変換できたかどうかを確認しています。両方の条件を満たす場合は数値として欠損値を埋めます。LINE 102: 数値で欠損値を埋める。
変換に成功した数値を使って、対象列の欠損値を埋めています。数値列に対して数値の入力値を正しく反映するための処理です。LINE 103: 数値でない場合の分岐。
対象列が数値型でない場合や入力値が数値に変換できない場合の処理に分岐しています。次の行では文字列として値を埋めます。LINE 104: 文字列で欠損値を埋める。
対象列を文字列型に変換したうえで、入力された文字列の値で欠損値を埋めています。数値以外の列にも対応できるようにしています。LINE 105: 平均値・最頻値埋め方法の判定。
処理方法が「数値列は平均値・その他の列は最頻値で埋める」かどうかを確認しています。条件に一致した場合は列の種類に応じた自動補完が行われます。LINE 106: 数値列かどうかの判定。
対象の列が数値型かどうかを確認しています。数値列であれば次の行で平均値を使った補完が行われます。LINE 107: 平均値で欠損値を埋める。
対象列の平均値を計算し、その値で欠損値を埋めています。数値データの傾向を保ったまま欠損値を補うための処理です。LINE 108: 数値以外の列の分岐。
対象列が数値型でない場合の処理に分岐しています。次の行以降では最頻値を使った補完の準備が行われます。LINE 109: 最頻値の計算。
対象列の中で最も多く出現する値(最頻値)を計算しています。欠損値以外のデータから求めることで、代表的な値を取得しています。LINE 110: 最頻値が存在するかの確認。
最頻値が計算できたかどうかを確認しています。列がすべて欠損値の場合など最頻値が求められないケースを避けるための確認です。LINE 111: 最頻値で欠損値を埋める。
求めた最頻値を使って、対象列の欠損値を埋めています。文字列やカテゴリ的な列に適した補完方法です。LINE 112: 処理結果の返却。
すべての列に対する欠損値の処理が終わったデータフレームを呼び出し元に返しています。この関数の最終的な出力になります。RUN 7/7: 欠損値を処理する関数を試す。
選ばれた方法で欠損値を扱う関数まで書けました。数値列と文字列列を含む表を渡し、平均値と最頻値による補完で欠損値が減るかを確認します。CHECK 7/7: 途中実行に成功。
処理前の欠損値2 処理後の欠損値0 score team 80.0 A 70.0 A 60.0 A RETURN 07: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 116: ページの基本設定。
ページタイトルとレイアウトの幅を設定し、ブラウザのタブ表示や画面全体の見た目を整えています。アプリの一番最初に実行される設定です。LINE 117: アプリのタイトル表示。
画面上部にアプリのタイトルを大きく表示しています。ユーザーが最初に目にする見出しとなります。LINE 118: アプリの説明文表示。
アプリの使い方や機能の概要を説明する補足文を表示しています。タイトルの下に小さく表示される案内文です。LINE 121: 手順1の見出し表示。
CSVファイルをアップロードする手順であることを示す見出しを表示しています。画面を上から順に読み進めるための区切りです。LINE 122: CSVファイルのアップロード欄。
CSVファイルを選択してアップロードするための部品を画面に表示しています。ここでユーザーが選んだファイルが後続の処理に使われます。LINE 123: 未アップロード状態の判定。
まだファイルがアップロードされていないかどうかを確認しています。アップロードされていない場合は次の行以降で案内が表示されます。LINE 124: アップロード前の案内表示。
ファイルがアップロードされていないことを利用者に伝える案内メッセージを表示しています。次に何をすればよいかを示しています。LINE 125: 処理の停止。
ファイルが未アップロードのため、それ以降の処理を実行せずにここで停止しています。データが無い状態での不要な処理を防いでいます。LINE 127: アップロードデータの取得。
アップロードされたファイルの中身をバイト列として取得しています。この後の読み込み処理で使用するデータです。LINE 128: CSVデータの読み込み。
取得したバイト列をもとに、CSVをデータフレームとして読み込んでいます。ここで読み込んだ内容がクリーニング前の元データになります。LINE 129: 読み込み前の指標計算。
読み込んだ元データの行数・列数・欠損値数・重複行数をまとめて計算しています。クリーニング前後の比較に使う基準値となります。LINE 130: 行数の取り出し。
先ほど計算した指標の中から行数だけを取り出して変数に保存しています。後で画面表示に使うための準備です。LINE 131: 列数の取り出し。
先ほど計算した指標の中から列数だけを取り出して変数に保存しています。後で画面表示に使うための準備です。LINE 132: 欠損値合計の取り出し。
先ほど計算した指標の中から欠損値の合計数だけを取り出して変数に保存しています。後で画面表示に使うための準備です。LINE 133: 重複行数の取り出し。
先ほど計算した指標の中から重複行数だけを取り出して変数に保存しています。後で画面表示に使うための準備です。LINE 136: ファイル識別キーの作成。
ファイル名とデータサイズを組み合わせて、アップロードされたファイルを識別するための文字列を作成しています。同じファイルかどうかを判定するために使います。LINE 137: アップロードファイルの変更判定。
前回保存した識別キーと今回のキーが異なるかどうかを確認しています。異なる場合は新しいファイルがアップロードされたと判断します。LINE 138: 識別キーの更新。
新しいファイルの識別キーをセッションに保存し、次回の比較に使えるようにしています。ファイルが切り替わったことを記録する処理です。LINE 139: 以前のクリーニング結果を破棄。
新しいファイルがアップロードされたため、以前保存していたクリーニング結果をセッションから削除しています。古い結果が誤って表示されないようにするための処理です。LINE 141: 読み込み結果の表示開始。
読み込みが完了したことを伝えるメッセージを画面に表示するための処理を開始しています。次の数行で表示内容を組み立てています。LINE 142: 行数と列数の表示文作成。
読み込んだ元データの行数と列数を埋め込んだ表示用の文字列を作成しています。専用のIDを付けて後から参照しやすくしています。LINE 143: 欠損値と重複行数の表示文作成。
元データの欠損値件数と重複行数を埋め込んだ文字列を続けて作成しています。前の行の文字列とつながって1つのメッセージになります。LINE 144: HTMLとしての表示指定。
作成した文字列をHTMLとしてそのまま画面に表示するよう指定しています。この指定によりタグが正しく反映されます。LINE 145: st.markdown呼び出しの終了。
読み込み完了メッセージを表示する一連のss.markdown呼び出しの終端です。これでデータ読み込み完了の通知が画面に反映されます。LINE 146: 元データの先頭10行表示。
読み込んだ元データの先頭10行を表として画面に表示しています。データの内容をざっと確認できるようにしています。LINE 149: 手順2の見出し表示。
列ごとの欠損値数とデータ型を確認する手順であることを示す見出しを表示しています。画面の区切りとなる部分です。LINE 150: 列プロファイル表の表示。
各列のデータ型・欠損値数・欠損率をまとめた表を画面に表示しています。データの状態を列単位で把握するための表です。LINE 153: 手順3の見出し表示。
重複行を確認して削除する手順であることを示す見出しを表示しています。画面の区切りとなる部分です。LINE 154: 重複行数の表示。
完全に重複している行数を目立つ数値として画面に表示しています。ユーザーが重複の有無をひと目で把握できるようにしています。LINE 155: 重複削除の選択チェックボックス。
重複行を削除するかどうかをユーザーが選択できるチェックボックスを表示しています。初期状態ではオンになっており、選択結果は後続の処理で使われます。LINE 156: 重複行が存在するかの判定。
重複行数が1件以上あるかどうかを確認しています。重複行がある場合は次の行以降で具体的な重複行が表示されます。LINE 157: 重複行表示の見出し。
これから表示する内容が重複している行の先頭部分であることを説明するキャプションを表示しています。LINE 158: 重複行の一覧表示。重複しているすべての行を抽出し、その先頭10行を表として画面に表示しています。
どの行が重複しているかを具体的に確認できます。LINE 161: 手順4の見出し表示。各列のデータ型を確認して変換する手順であることを示す見出しを表示しています。
画面の区切りとなる部分です。LINE 162: データ型選択結果の受け皿作成。各列に対して選ばれたデータ型の選択結果を格納するための空の辞書を用意しています。
この後のループで中身が追加されていきます。LINE 163: 2列レイアウトの作成。画面を横に2つの列に分けるレイアウトを作成しています。
この後、列ごとの選択項目を左右に並べて配置します。LINE 164: 列ごとの繰り返し処理開始。元データの各列名を順番に取り出しながら、インデックス番号とあわせて処理するループを開始しています。
列ごとの選択項目を作るための準備です。LINE 165: 配置先レイアウトの選択。インデックス番号が偶数か奇数かによって、先ほど作成した2つの列のどちらに表示するかを決めています。
項目を左右交互に並べるための処理です。LINE 166: データ型選択ボックスの表示。列名と現在のデータ型を表示したうえで、変換後のデータ型を選ぶプルダウンを表示しています。
選ばれた内容は後続のデータ型変換処理に使われます。LINE 167: 選択肢の渡し値。プルダウンに表示するデータ型の選択肢一覧を渡しています。
この一覧の中からユーザーが変換先の型を選べるようになっています。LINE 168: データ型選択肢の指定。selectboxに表示する選択肢としてDTYPE_OPTIONSを渡しています。
「変更しない」「文字列」「整数」「小数」「日時」の5つから選べるようになります。LINE 169: 初期選択の指定。index=0を指定することで、選択肢の先頭にある「変更しない」が最初に選ばれた状態で表示されます。
LINE 170: 列ごとの一意なキー設定。key=f"dtype_{column}"により、列ごとに異なるキーを付けて選択状態を区別しています。これにより複数の列の選択が互いに干渉しません。
LINE 171: 選択結果の格納。selectboxの戻り値をdtype_choices[column]に代入し、各列に対して選ばれたデータ型の文字列を保存しています。LINE 174: 手順5の見出し表示。
「5.欠損値の処理方法を選択して適用する」という見出しを画面に表示し、次の操作エリアであることを示しています。LINE 175: 欠損値処理方法の選択。MISSING_OPTIONSの中から欠損値の処理方法を選ぶselectboxを表示し、選ばれた方法をmissing_method変数に保存しています。
LINE 176: 埋める値の初期化。fill_valueという変数を空文字で初期化しています。後で「指定した値で埋める」が選ばれたときだけ実際の値が入力されます。
LINE 177: 指定値入力の条件判定。missing_methodが「指定した値で埋める」であるかどうかを判定し、その場合だけ次の行で入力欄を表示します。LINE 178: 埋める値のテキスト入力。
text_inputを使って欠損値を埋めるための値をユーザーに入力してもらい、その値をfill_valueに保存しています。初期値は「未入力」です。LINE 179: 実行ボタンの条件判定。
「クリーニングを実行」ボタンが押されたかどうかを判定し、押された場合だけ以下のクリーニング処理を実行します。LINE 180: データ型変換の実行。convert_dtypes関数を呼び出し、df_rawの各列を選択されたデータ型に変換した結果をtypedに保存しています。
LINE 181: 重複行削除の実行。drop_duplicate_rows関数を呼び出し、typedからdrop_dupの設定に応じて重複行を削除した結果をdedupedに保存しています。LINE 182: 欠損値処理結果の保存。
fill_missing関数でdedupedの欠損値を処理し、その結果をセッション状態のcleaned_dfに保存することで、次回以降の表示でも結果が保持されます。LINE 185: 手順6の見出し表示。「6.クリーニング前後を比較してCSVをダウンロードする」という見出しを表示し、最終確認とダウンロードの段階であることを示しています。
LINE 186: クリーニング結果の取得。セッション状態からcleaned_dfを取得してdf_cleanに代入しています。まだクリーニングが実行されていない場合はNoneになります。
LINE 187: 未実行時の判定。df_cleanがNoneかどうかを判定し、まだクリーニングが実行されていない場合に案内メッセージを表示する分岐に入ります。LINE 188: 未実行メッセージ表示開始。
st.markdownを呼び出し、クリーニング未実行時に表示する案内文の作成を開始しています。LINE 189: 未実行案内文の内容。「クリーニングを実行」ボタンを押すよう促すメッセージを、id付きのdiv要素として組み立てています。
LINE 190: HTML表示の許可。unsafe_allow_html=Trueを指定することで、divタグを含むHTMLをそのまま画面に表示できるようにしています。LINE 191: markdown呼び出しの終了。
st.markdownの引数指定を閉じ、未実行時の案内メッセージの表示処理を完了しています。LINE 192: 処理の停止。st.stop()を呼び出すことで、クリーニング未実行の状態ではこれ以降の比較やダウンロードの処理を行わずに止めています。
LINE 194: クリーニング後の指標算出。frame_stats関数でdf_cleanの行数・列数・欠損値合計・重複行数をまとめて計算し、afterに保存しています。LINE 195: クリーニング後の行数取得。
afterの辞書から「行数」を取り出し、a_rowsという変数に保存しています。LINE 196: クリーニング後の列数取得。afterの辞書から「列数」を取り出し、a_colsという変数に保存しています。
LINE 197: クリーニング後の欠損値数取得。afterの辞書から「欠損値の合計」を取り出し、a_naという変数に保存しています。LINE 198: クリーニング後の重複行数取得。
afterの辞書から「重複行数」を取り出し、a_dupという変数に保存しています。LINE 199: 完了メッセージ表示開始。st.markdownを呼び出し、クリーニング完了を知らせるメッセージの作成を開始しています。
LINE 200: 行数変化の表示内容。クリーニング前の行数n_rowsからクリーニング後のa_rowsへの変化を、id付きのdivとして文字列に組み込んでいます。LINE 201: 欠損値・重複行変化の表示内容。
欠損値の合計と重複行数について、クリーニング前後の変化を続けて文字列に組み込んでいます。LINE 202: HTML表示の許可。unsafe_allow_html=Trueを指定し、直前で組み立てたdivタグ付きのメッセージをそのままHTMLとして表示できるようにしています。
LINE 203: markdown呼び出しの終了。st.markdownの引数指定を閉じ、クリーニング完了メッセージの表示処理を完了しています。LINE 206: 比較表の作成開始。
pd.DataFrameを呼び出し、クリーニング前後の指標を並べた比較表comparisonの作成を開始しています。LINE 207: 比較表の列定義開始。比較表に含める列を辞書形式で定義し始めています。
LINE 208: 項目列の設定。beforeの辞書のキー一覧を「項目」列として設定し、行数・列数・欠損値の合計・重複行数の名前を並べています。LINE 209: クリーニング前の値の設定。
beforeの各項目の値を取り出し、「クリーニング前」列として並べています。LINE 210: クリーニング後の値の設定。beforeと同じキーの順番でafterの値を取り出し、「クリーニング後」列として並べることで前後の項目が対応するようにしています。
LINE 211: 列定義の終了。比較表に含める列の辞書定義を閉じています。LINE 212: 比較表作成の終了。
pd.DataFrameの呼び出しを閉じ、比較表comparisonの作成を完了しています。LINE 213: 比較表の表示。作成したcomparisonをst.dataframeで画面に表示し、クリーニング前後の指標を一目で比較できるようにしています。
LINE 215: 左右2列レイアウトの作成。st.columns(2)で画面を左右2つの領域に分け、それぞれをleftとrightという変数に保存しています。LINE 216: 左側領域の指定。
withブロックにより、以降の表示内容を左側の領域(left)に配置するよう指定しています。LINE 217: 左側の見出し表示。「クリーニング前(先頭10行)」という太字の見出しを左側領域に表示しています。
LINE 218: クリーニング前データの表示。df_rawの先頭10行を左側領域に表示し、クリーニング前の状態を確認できるようにしています。LINE 219: 右側領域の指定。
withブロックにより、以降の表示内容を右側の領域(right)に配置するよう指定しています。LINE 220: 右側の見出し表示。「クリーニング後(先頭10行)」という太字の見出しを右側領域に表示しています。
LINE 221: クリーニング後データの表示。df_cleanの先頭10行を右側領域に表示し、クリーニング後の状態を左側と並べて比較できるようにしています。LINE 223: 欠損値・データ型見出しの表示。
「クリーニング後の列ごとの欠損値数とデータ型」という太字の見出しを表示し、次に表示する表の内容を示しています。LINE 224: クリーニング後の列情報表示。profile_columns関数でdf_cleanの各列のデータ型・欠損値数・欠損率をまとめた表を作成し、画面に表示しています。
LINE 227: CSVバイト列への変換。df_cleanをCSV形式の文字列に変換したうえでutf-8-sigでエンコードし、ダウンロード用のバイト列csv_bytesを作成しています。LINE 228: ダウンロードボタンの呼び出し開始。
st.download_buttonを呼び出し、クリーニング済みCSVをダウンロードするためのボタン表示を開始しています。LINE 229: ボタンラベルの指定。「クリーニング済みCSVをダウンロード」というボタンに表示する文字列を指定しています。
LINE 230: ダウンロードデータの指定。data引数にcsv_bytesを渡し、ボタンが押されたときにダウンロードされる中身のデータを指定しています。LINE 231: ファイル名の指定。
file_name引数で、ダウンロード時のファイル名を「cleaned.csv」に指定しています。LINE 232: MIMEタイプの指定。mime引数で「text/csv」を指定し、ダウンロードされるファイルがCSV形式であることをブラウザに伝えています。
LINE 233: ボタンのキー指定。key="download_csv"を指定し、このダウンロードボタンを画面上の他の要素と区別できるようにしています。LINE 234: ダウンロードボタン呼び出しの終了。
st.download_buttonの引数指定を閉じ、ダウンロードボタンの表示処理を完了しています。LINE 235: ダウンロード案内表示開始。st.markdownを呼び出し、ダウンロード準備が整ったことを知らせるメッセージの作成を開始しています。
LINE 236: ダウンロード内容の表示。ファイル名cleaned.csvと、クリーニング後の行数a_rows・列数a_colsをid付きのdivとして組み込んだメッセージを作成しています。LINE 237: HTML表示の許可。
unsafe_allow_html=Trueを指定し、直前で組み立てたdivタグ付きのメッセージをそのままHTMLとして表示できるようにしています。LINE 238: markdown呼び出しの終了。st.markdownの引数指定を閉じ、ダウンロード準備完了メッセージの表示処理を完了しています。
PREVIEW: 完成したCSVクリーニングアプリを操作。入力・送信・結果表示を実際のブラウザ画面で確認します。UPLOAD: 実際の入力データでアプリを操作。
CSVをアップロードして欠損値3件と重複行2件を確認し、クリーニング実行後に行数8→6の比較結果とcleaned.csvのダウンロード準備を確認できた 確認1/5: サンプルCSVを読み込んで全体の状態を確認する。8行4列の検証用CSVを読み込ませ、画面上部に出る読み込み結果と同じ内容を表示します。欠損値と重複行の件数がその場で分かります。
確認2/5: 列ごとの欠損値数とデータ型を一覧する。同じサンプルCSVで列単位の集計を表示します。どの列に欠損値が何件あり、現在のデータ型が何かを一覧で確認できます。
確認3/5: 重複行を確認して削除する。重複している行を抜き出して表示したあと、削除後の行数を表示します。画面の指標と同じ件数になることを確認できます。
確認4/5: 欠損値を埋めて前後を比較する。重複行を削除したうえで平均値と最頻値による補完を適用し、クリーニング前後の指標を比較表として表示します。確認5/5: クリーニング済みCSVのダウンロード準備を確認する。
補完まで終えた表をCSVへ書き出し、ダウンロード案内と同じ件数を表示します。クリーニング後の列ごとの状態もあわせて確認できます。学習内容のまとめ。
節の見出しを作るst.subheader CSVを読み込むpd.read_csv型をそろえるconvert_dtypes 1ファイルで完結する構成 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。
Streamlitとpandasとは
今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。
Streamlitが1画面のアプリを組み立てる仕組み
Streamlitは、Pythonのスクリプトを書くだけでブラウザに表示できる画面を作れるWebアプリ用ライブラリです。HTMLやJavaScriptを別に用意しなくても、関数を上から並べた順序がそのまま画面の並びになる点が特徴です。入力部品が操作されるとスクリプトが先頭から読み直され、表示が最新の状態へ差し替わる仕組みになっています。
このアプリでは、ファイルの受け取りから表の表示、ダウンロードボタンまでをStreamlitの関数だけで組み立てました。
画面づくりで使っているStreamlitの関数と、その担当箇所を対応させて挙げます
- 節の見出しを作るst.subheader
- 表を描画するst.dataframe
- 重複行数を強調するst.metric
- CSVを受け取るst.file_uploader
- 処理を打ち切るst.stop
- 再実行をまたいで値を残すst.session_state
- 結果を配布するst.download_button
pandasでCSVの状態を集計する役割
pandasは、CSVのような表形式のデータを読み込んで集計や変換を行うデータ分析ライブラリです。読み込んだ表はDataFrameという単位で扱い、列名を指定して1列だけを取り出したり、欠損値の件数を列ごとに数えたりできます。重複行の判定やデータ型の変換にも専用の関数があるため、CSVを1行ずつ自分で走査する必要はありません。
画面に並ぶ数値はすべてpandasの集計結果から作られています。
CSVの状態を数えるためにpandasから呼び出している機能を挙げます
- CSVを読み込むpd.read_csv
- 欠損値を判定するisna
- 重複行を数えるduplicated
- 重複行を残さず消すdrop_duplicates
- 数値へ変換するpd.to_numeric
- 日時へ変換するpd.to_datetime
- 結果をCSV文字列にするto_csv
Python・Streamlitで開発する場合の環境構築
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install streamlit pandas
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install streamlit pandas
- 起動は .\.venv\Scripts\python.exe -m streamlit run csv_cleaner_app.pyで、初回起動時にメールアドレスの入力を求められたら空のままEnterでスキップできます。
- Shift_JIS(cp932)のCSVも扱えるよう、UTF-8で読めなかった場合にcp932で読み直す実装にしています。
- ダウンロードするCSVはExcelで文字化けしないようutf-8-sigでエンコードしています。
CSVクリーニングアプリの要件定義
目的は、Streamlitとpandasで、アップロードしたCSVの欠損値数・重複行数・データ型を画面で確認し、クリーニング後の行数の変化とcleaned.csvのダウンロードまでを1画面で扱えるようにすることです。
対象者として、Pythonの基本文法とpandasの初歩を学び、毎回書いていたCSVの点検コードをブラウザから操作できる画面としてまとめる方法を学びたい人を想定しています。
完成物は、StreamlitとpandasでCSVを読み込み、欠損値・重複行・データ型を確認してクリーニング結果をcleaned.csvとしてダウンロードできるCSVクリーニングアプリです。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- アップローダーからCSVファイルを受け取る
- UTF-8で読めないCSVをcp932で読み直す
- 読み込み直後の行数・列数・欠損値の合計・重複行数を表示する
- 読み込んだ表の先頭10行を表示する
- 列ごとのデータ型・欠損値数・欠損率を一覧する
- 完全に重複している行数を指標として示す
- 重複している行の先頭10行を表示する
- 列ごとに変換先のデータ型を選択する
- 欠損値の処理方法を5つの選択肢から選ぶ
- 指定した値で埋める場合だけ埋める値の入力欄を出す
- 実行ボタンで型変換・重複削除・欠損値処理を順に適用する
- クリーニング前後の4指標を比較表に並べる
- クリーニング前後の先頭10行を左右に並べる
- クリーニング後の列ごとの欠損値数とデータ型を再表示する
- クリーニング結果をcleaned.csvとして書き出す
非機能要件
- 1ファイルで完結する構成
- st.cache_dataによる読み込み結果の再利用
- ファイル未選択時はst.stopで以降を止める
- クリーニング未実行時は比較表の手前で止める
- アップロードファイルが変わるとcleaned_dfを破棄する
- クリーニング結果をsession_stateへ保持する
- 変換できない値は欠損値として扱う
- 重複行は最初の1行だけを残す
- ダウンロードするCSVはutf-8-sigで書き出す
- 画面レイアウトはwideで表示する
- 選択肢を定数リストで一元管理する
実装方針
今回はStreamlitとpandasの基本動作を追いやすくするため、CSVクリーニングアプリ本体を1つのPythonファイルへまとめます。
入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。
CSVクリーニングアプリを安全に組み立てるための実装方針は次のとおりです。
- 1ファイルで完結する構成
- st.cache_dataによる読み込み結果の再利用
- ファイル未選択時はst.stopで以降を止める
- クリーニング未実行時は比較表の手前で止める
- アップロードファイルが変わるとcleaned_dfを破棄する
- クリーニング結果をsession_stateへ保持する
- 変換できない値は欠損値として扱う
- 重複行は最初の1行だけを残す
- ダウンロードするCSVはutf-8-sigで書き出す
- 画面レイアウトはwideで表示する
- 選択肢を定数リストで一元管理する
完成と判断する条件
- 8行のCSVで欠損値3件と重複行2件が表示される
- 列ごとの欠損値数と欠損率が一覧に並ぶ
- 重複行を削除する設定で行数が8から6へ変わる
- 比較表に行数・列数・欠損値の合計・重複行数が並ぶ
- クリーニング後の欠損値の合計が0になる
- cleaned.csvのダウンロード準備完了が表示される
- CSV未選択時は案内文だけが表示される
CSVクリーニングアプリを作る際の重要ポイント
このアプリの中心は、型変換・重複行の削除・欠損値の処理という3段階を順番に適用する流れです。段階ごとに関数を分けているため、どこで行数や欠損値が変わったのかを前後の指標で追えます。結果はsession_stateへ保存し、ボタンを押し直さなくても比較表とダウンロードボタンが残る作りにしました。
最後にcleaned.csvとして書き出す準備が整い、行数と列数を添えて表示します。
クリーニング処理を組み立てている関数と、結果の保存先を順番に挙げます
- 型をそろえるconvert_dtypes
- 重複行を消すdrop_duplicate_rows
- 欠損値を扱うfill_missing
- 前後の指標を並べるframe_stats
- 列単位の状態を出すprofile_columns
- 結果を持ち回るsession_stateのcleaned_df
クリーニングを3つの段階に分ける理由
ボタンが押されたあとの処理は、型変換・重複行の削除・欠損値の処理という順番で進みます。先に型をそろえると数値列として平均値を計算でき、重複行を消してから埋めることで同じ値を二重に数える心配がありません。段階ごとに関数を分けておけば、どの段で結果が変わったのかも追いやすくなります。
実行ボタンを押したときに呼ばれる関数と、渡す入力を順に並べます
- convert_dtypesへ列ごとの選択内容を渡す
- drop_duplicate_rowsへチェックボックスの真偽値を渡す
- fill_missingへ処理方法と埋める値を渡す
- 3段の結果をsession_stateのcleaned_dfへ保存
欠損値の処理方法を選び分ける考え方
欠損値の扱いは、集計を続けたいのか、その行を落としたいのかで答えが変わります。そこで5つの方法をセレクトボックスから選べるようにし、選ばれた文字列で処理を切り替えました。指定した値で埋める場合だけ入力欄が増え、数値列に数字を入れたときは数値のまま埋められます。
用意した5つの処理方法が、どの場面に向いているかを示します
- 平均値と最頻値で埋める方法は集計を続けたい場合
- 行を削除する方法は欠損のある行を残したくない場合
- 前方補完は直前の行の値を引き継げる場合
- 指定した値で埋める方法は未入力と明示したい場合
- 何もしない方法は型変換と重複削除だけ試す場合
アップロードし直したときに結果を破棄する仕組み
ファイル名とバイト数をつないだ文字列をキーとして覚えておき、前回と違えば保存済みの結果を捨てます。この判定がないと、別のCSVを読み込んだ直後に前回のクリーニング結果が残り、比較表の数値が食い違ってしまいます。読み込み状態を1つのキーで管理する小さな工夫です。
保存済みの結果を残す場合と捨てる場合の分かれ目を挙げます
- 同じファイルを操作し続けたときはcleaned_dfを保持
- 別のファイルへ差し替えたときはcleaned_dfを削除
- クリーニング未実行のときは案内文を出して停止
- 実行後は再操作しても比較表が残る
クリーニング前後を並べて確認する画面
前後の違いは、行数・列数・欠損値の合計・重複行数の4項目を1つの表へ並べて示します。さらに先頭10行を左右に置き、列ごとの欠損値数とデータ型も再表示するため、狙った列だけが変わったかを目で追うことができます。最後に書き出し用のCSVを用意し、ファイル名と件数を添えて案内する流れです。
比較画面で読み取る指標と、書き出し時の設定を挙げます
- 行数の変化で削除された件数を読み取る
- 欠損値の合計の変化で処理の効果を読み取る
- 重複行数の変化で削除結果を読み取る
- utf-8-sigで表計算ソフトでも開ける文字コードにする
- ファイル名はcleaned.csvで固定
PythonでCSVクリーニングアプリの完成コード
コードは大きく3層に分かれます。先頭で選択肢の定数を定義し、次にpandasの処理を担う関数群を置き、最後にUIを組み立てる流れです。
関数はどれも受け取ったDataFrameを直接書き換えず、df.copy()した複製を返します。元データが残るので、設定を変えて何度でも試せますし、前後の比較表もそのまま作れました。
読み込みだけはst.cache_dataを付けています。Streamlitは操作のたびにスクリプトを上から実行し直す仕組みなので、同じCSVを毎回パースしないためのキャッシュが効きます。
このセクションの用語
- pandas
- 表形式のデータを読み込んだり集計したりするためのPythonライブラリです。CSVの操作では定番の選択肢になります。
- DataFrame
-
pandasが扱う行と列からなる表のオブジェクトです。Excelのシート1枚をPythonで持っているような感覚で使えます。 - dtype
- 列ごとに決まっているデータ型のことです。数値・文字列・日時などの区別を持ち、
df.dtypesで一覧できます。 - st.cache_data
- 関数の結果を覚えておき、同じ入力なら計算を省く
Streamlitのデコレータです。読み込みや集計の重複実行を減らせます。 - errors="coerce"
-
pandasの変換関数に渡す指定で、変換に失敗した値を例外にせず欠損値へ置き換える仕組みです。処理を止めたくないときに使います。
"""StreamlitとpandasでCSVの欠損値・重複行・データ型を整えるWebアプリ。"""
import io
import pandas as pd
import streamlit as st
# 動作確認したい画面
ROUTES = ["/"]
# 選択肢の定義
DTYPE_OPTIONS = [
"変更しない",
"文字列(string)",
"整数(Int64)",
"小数(float)",
"日時(datetime)",
]
MISSING_OPTIONS = [
"数値列は平均値・その他の列は最頻値で埋める",
"欠損値を含む行を削除する",
"直前の行の値で埋める(前方補完)",
"指定した値で埋める",
"何もしない",
]
# CSVの読み込み
@st.cache_data(show_spinner=False)
def load_csv(data: bytes) -> pd.DataFrame:
"""アップロードされたCSVのバイト列をDataFrameへ読み込む。"""
try:
return pd.read_csv(io.BytesIO(data))
except UnicodeDecodeError:
return pd.read_csv(io.BytesIO(data), encoding="cp932")
# 列ごとの欠損値数を確認する(データ型もあわせて集計する)
def profile_columns(df: pd.DataFrame) -> pd.DataFrame:
"""列ごとのデータ型・欠損値数・欠損率をまとめた表を返す。"""
return pd.DataFrame(
{
"列名": list(df.columns),
"データ型": [str(dtype) for dtype in df.dtypes],
"欠損値数": [int(df[col].isna().sum()) for col in df.columns],
"欠損率(%)": [
round(float(df[col].isna().mean()) * 100, 1) for col in df.columns
],
}
)
# 表全体の指標(クリーニング前後の比較に使う)
def frame_stats(df: pd.DataFrame) -> dict:
"""行数・列数・欠損値の合計・重複行数をまとめて返す。"""
return {
"行数": int(len(df)),
"列数": int(df.shape[1]),
"欠損値の合計": int(df.isna().sum().sum()),
"重複行数": int(df.duplicated().sum()),
}
# 各列のデータ型を確認して変換する
def convert_dtypes(df: pd.DataFrame, choices: dict) -> pd.DataFrame:
"""列ごとに選ばれたデータ型へ変換する(変換できない値は欠損値にする)。"""
out = df.copy()
for col, choice in choices.items():
if choice == "文字列(string)":
out[col] = out[col].astype("string")
elif choice == "整数(Int64)":
out[col] = pd.to_numeric(out[col], errors="coerce").round().astype("Int64")
elif choice == "小数(float)":
out[col] = pd.to_numeric(out[col], errors="coerce").astype("float64")
elif choice == "日時(datetime)":
out[col] = pd.to_datetime(out[col], errors="coerce")
return out
# 重複行の削除
def drop_duplicate_rows(df: pd.DataFrame, enabled: bool) -> pd.DataFrame:
"""完全に重複する行を、最初の1行だけ残して削除する。"""
if not enabled:
return df.copy()
return df.drop_duplicates().reset_index(drop=True)
# 欠損値の処理
def fill_missing(df: pd.DataFrame, method: str, fill_value: str) -> pd.DataFrame:
"""選択された方法で欠損値を処理する。"""
out = df.copy()
if method == "欠損値を含む行を削除する":
return out.dropna().reset_index(drop=True)
if method == "直前の行の値で埋める(前方補完)":
return out.ffill()
for col in out.columns:
if not out[col].isna().any():
continue
if method == "指定した値で埋める":
number = pd.to_numeric(fill_value, errors="coerce")
if pd.api.types.is_numeric_dtype(out[col]) and pd.notna(number):
out[col] = out[col].fillna(number)
else:
out[col] = out[col].astype("string").fillna(fill_value)
elif method == "数値列は平均値・その他の列は最頻値で埋める":
if pd.api.types.is_numeric_dtype(out[col]):
out[col] = out[col].fillna(out[col].mean())
else:
mode = out[col].mode(dropna=True)
if not mode.empty:
out[col] = out[col].fillna(mode.iloc[0])
return out
# 画面の基本設定
st.set_page_config(page_title="CSVクリーニングアプリ", layout="wide")
st.title("CSVクリーニングアプリ")
st.caption("CSVをアップロードし、欠損値・重複行・データ型を確認してクリーニング結果をダウンロードします。")
# 1. CSVファイルをアップロードして読み込む
st.subheader("1. CSVファイルをアップロードして読み込む")
uploaded = st.file_uploader("CSVファイルを選択", type=["csv"], key="csv_file")
if uploaded is None:
st.info("CSVファイルをアップロードすると、欠損値・重複行・データ型の確認が始まります。")
st.stop()
raw_bytes = uploaded.getvalue()
df_raw = load_csv(raw_bytes)
before = frame_stats(df_raw)
n_rows = before["行数"]
n_cols = before["列数"]
n_na = before["欠損値の合計"]
n_dup = before["重複行数"]
# アップロードし直したときは前回のクリーニング結果を破棄する
source_key = uploaded.name + ":" + str(len(raw_bytes))
if st.session_state.get("source_key") != source_key:
st.session_state["source_key"] = source_key
st.session_state.pop("cleaned_df", None)
st.markdown(
f'<div id="profile-summary">読み込み完了: {n_rows}行 / {n_cols}列'
f" / 欠損値 {n_na}件 / 重複行 {n_dup}件</div>",
unsafe_allow_html=True,
)
st.dataframe(df_raw.head(10))
# 2. 列ごとの欠損値数を確認する(データ型もあわせて表示する)
st.subheader("2. 列ごとの欠損値数を確認する(データ型もあわせて表示)")
st.dataframe(profile_columns(df_raw))
# 3. 重複行数を確認して削除する
st.subheader("3. 重複行数を確認して削除する")
st.metric("完全に重複している行数", f"{n_dup}行")
drop_dup = st.checkbox("重複行を削除する(最初の1行を残す)", value=True, key="drop_duplicates")
if n_dup:
st.caption("重複している行(先頭10行)")
st.dataframe(df_raw[df_raw.duplicated(keep=False)].head(10))
# 4. 各列のデータ型を確認して変換する
st.subheader("4. 各列のデータ型を確認して変換する")
dtype_choices = {}
grid = st.columns(2)
for index, column in enumerate(df_raw.columns):
with grid[index % 2]:
dtype_choices[column] = st.selectbox(
f"{column}(現在: {df_raw[column].dtype})",
DTYPE_OPTIONS,
index=0,
key=f"dtype_{column}",
)
# 5. 欠損値の処理方法を選択して適用する
st.subheader("5. 欠損値の処理方法を選択して適用する")
missing_method = st.selectbox("処理方法", MISSING_OPTIONS, index=0, key="missing_method")
fill_value = ""
if missing_method == "指定した値で埋める":
fill_value = st.text_input("埋める値", value="未入力", key="fill_value")
if st.button("クリーニングを実行", type="primary", key="run_clean"):
typed = convert_dtypes(df_raw, dtype_choices)
deduped = drop_duplicate_rows(typed, drop_dup)
st.session_state["cleaned_df"] = fill_missing(deduped, missing_method, fill_value)
# 6. クリーニング前後を比較してCSVをダウンロードする
st.subheader("6. クリーニング前後を比較してCSVをダウンロードする")
df_clean = st.session_state.get("cleaned_df")
if df_clean is None:
st.markdown(
'<div id="clean-summary">「クリーニングを実行」を押すと、前後の比較結果が表示されます。</div>',
unsafe_allow_html=True,
)
st.stop()
after = frame_stats(df_clean)
a_rows = after["行数"]
a_cols = after["列数"]
a_na = after["欠損値の合計"]
a_dup = after["重複行数"]
st.markdown(
f'<div id="clean-summary">クリーニング完了: 行数 {n_rows} → {a_rows}'
f" / 欠損値 {n_na} → {a_na} / 重複行 {n_dup} → {a_dup}</div>",
unsafe_allow_html=True,
)
# クリーニング前後を比較する(行数・列数・欠損値の合計・重複行数を並べる)
comparison = pd.DataFrame(
{
"項目": list(before.keys()),
"クリーニング前": [before[key] for key in before],
"クリーニング後": [after[key] for key in before],
}
)
st.dataframe(comparison)
left, right = st.columns(2)
with left:
st.markdown("**クリーニング前(先頭10行)**")
st.dataframe(df_raw.head(10))
with right:
st.markdown("**クリーニング後(先頭10行)**")
st.dataframe(df_clean.head(10))
st.markdown("**クリーニング後の列ごとの欠損値数とデータ型**")
st.dataframe(profile_columns(df_clean))
# クリーニング済みCSVをダウンロードする
csv_bytes = df_clean.to_csv(index=False).encode("utf-8-sig")
st.download_button(
"クリーニング済みCSVをダウンロード",
data=csv_bytes,
file_name="cleaned.csv",
mime="text/csv",
key="download_csv",
)
st.markdown(
f'<div id="download-note">ダウンロード準備完了: cleaned.csv({a_rows}行 / {a_cols}列)</div>',
unsafe_allow_html=True,
)
コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
load_csvでCSVのバイト列を読み込む
@st.cache_data(show_spinner=False)
def load_csv(data: bytes) -> pd.DataFrame:
"""アップロードされたCSVのバイト列をDataFrameへ読み込む。"""
try:
return pd.read_csv(io.BytesIO(data))
except UnicodeDecodeError:
return pd.read_csv(io.BytesIO(data), encoding="cp932")アップロード部品から届くのはバイト列なので、io.BytesIOで包んでpd.read_csvへ渡します。utf-8で失敗したときだけcp932で読み直す作りにしたので、表計算ソフトから出した日本語CSVでも止まりません。st.cache_dataが付いているため、同じファイルなら2回目以降の読み込みを省けます。
profile_columnsで欠損値数と欠損率を出す
"データ型": [str(dtype) for dtype in df.dtypes],
"欠損値数": [int(df[col].isna().sum()) for col in df.columns],
"欠損率(%)": [
round(float(df[col].isna().mean()) * 100, 1) for col in df.columns
],df[col].isna()は各セルが欠損値かどうかの真偽値になり、sum()で件数、mean()で割合が求まります。割合を100倍して小数第1位で丸めると、欠損率が読みやすい数字になりました。df.dtypesを文字列にして並べることで、型と欠損の状況を1枚の表で追えます。
frame_statsで表全体の指標をまとめる
return {
"行数": int(len(df)),
"列数": int(df.shape[1]),
"欠損値の合計": int(df.isna().sum().sum()),
"重複行数": int(df.duplicated().sum()),
}行数・列数・欠損値の合計・重複行数という4つの数字を辞書で返します。df.isna().sum().sum()は列ごとの合計をもう一度足して、表全体の欠損数にする書き方です。同じ関数をクリーニング前と後で呼ぶだけで、比較表の材料がそろいました。
convert_dtypesで列のデータ型を変換する
elif choice == "整数(Int64)":
out[col] = pd.to_numeric(out[col], errors="coerce").round().astype("Int64")
elif choice == "小数(float)":
out[col] = pd.to_numeric(out[col], errors="coerce").astype("float64")
elif choice == "日時(datetime)":
out[col] = pd.to_datetime(out[col], errors="coerce")選択肢に応じてpd.to_numericとpd.to_datetimeを使い分けています。errors="coerce"を付けると、変換できない値は例外ではなく欠損値になるので、1件の異常値で処理全体が止まりません。整数はround()してからInt64にすることで、欠損値を残したまま整数型にできます。
drop_duplicate_rowsで重複行を1行に絞る
if not enabled:
return df.copy()
return df.drop_duplicates().reset_index(drop=True)チェックが外れているときはdf.copy()を返すだけで、元のDataFrameには触りません。drop_duplicates()は全列が一致する行のうち最初の1行だけを残します。削除すると行番号が飛ぶので、reset_index(drop=True)で番号を振り直しています。
fill_missingで欠損値の扱いを切り替える
if method == "欠損値を含む行を削除する":
return out.dropna().reset_index(drop=True)
if method == "直前の行の値で埋める(前方補完)":
return out.ffill()画面で選ばれた文字列で分岐し、行削除ならdropna()、前方補完ならffill()の結果を返します。どちらもout = df.copy()した複製への操作なので、読み込んだデータはそのまま残ります。平均値や最頻値で埋める方法は、この後の列ごとのループで型を見ながら適用する流れです。
参考:
©Streamlit公式ドキュメントStreamlit runs your script from top to bottom at every user interaction or code change.
st.session_stateで前後比較とCSV出力
Streamlitは入力を変えるたびにスクリプト全体を実行し直します。そのままではクリーニング結果が毎回消えてしまうため、実行ボタンを押したタイミングでst.session_stateへ保存する形にしました。
比較にはframe_statsの戻り値を前後で並べた表を使います。行数・列数・欠損値の合計・重複行数が横並びになるので、どの数字が減ったのかを一目で追えます。
画面下部では前後の先頭10行を並べ、st.download_buttonからcleaned.csvを出力する形にしている構成です。ファイル名を固定したのは、受け取る側が中身を推測しやすいからです。
この画面を動かすうえで押さえておきたい、再実行と保存まわりのポイントを次にまとめます。
このセクションの用語
- st.session_state
- 同じブラウザのセッション内で値を保ち続ける
Streamlitの保存場所です。再実行をまたいで結果を残したいときに使います。 - 再実行(rerun)
- ボタンや選択の操作をきっかけにスクリプトが先頭から動き直す動作です。通常の変数は毎回初期化されます。
- st.download_button
- バイト列や文字列をファイルとして保存させるボタンを作る関数です。ファイル名とデータを渡すだけで使えます。
比較表と先頭10行の並べ方
前後比較の表示で決めたことを挙げます。
- 指標は
frame_statsが返す4項目に絞り、クリーニング前と後を並べて置く - 全行ではなく先頭10行だけを表示し、画面が縦に伸びすぎないようにする
- 前後を隣に置くことで、同じ行がどう変わったかを目で追えるようにする
download_buttonでcleaned.csvを渡す
ダウンロード周りで気を付けた点を挙げます。
- 出力名は
cleaned.csvという固定名にして共有時の混乱を避ける -
st.session_stateに保存したDataFrameから出力し、何度押しても同じ結果になるようにする - 設定を変えたいときは選択肢を直してボタンを押し直せば、保存内容が上書きされる
再実行:操作ごとに全体が走る
結果保存:session_stateに置く
キャッシュ:同じCSVは再読込なし
出力名:cleaned.csvで固定
PythonでCSVクリーニングアプリのエラー対処
動かし始めの段階では、CSVの文字コードと実行環境まわりのエラーが起きやすいです。どれも原因がはっきりしているので、メッセージの1行目を読んで切り分けます。
次の表は一般に遭遇しやすいものを整理したもので、今回の実行で必ず出たという意味ではありません。手元で再現したときに当たりを付ける材料として使ってください。
このセクションの用語
- UnicodeDecodeError
- 文字コードの想定が合わずに文字列へ変換できないときに出るPythonの例外です。日本語CSVでよく遭遇します。
- cp932
- Windows環境で使われるShift_JIS系の文字コードです。表計算ソフトから保存したCSVがこの形式になることがあります。
- StreamlitAPIException
-
StreamlitのAPIの使い方が想定と違うときに投げられる例外です。画面に赤い枠でメッセージが出ます。
| エラー例 | 原因 | 対処 |
|---|---|---|
| UnicodeDecodeError: 'utf-8' codec can't decode byte | CSVがcp932などのShift_JIS系で保存されている |
load_csvのようにencoding="cp932"で読み直すフォールバックを用意する |
| ModuleNotFoundError: No module named 'streamlit' | アプリを起動している環境にstreamlitが入っていない |
起動に使っている環境と、パッケージを入れた環境が同じか確認する |
| AttributeError: module 'streamlit' has no attribute 'cache_data' |
st.cache_dataが無い古いバージョンを使っている |
インストール済みのstreamlitのバージョンを確認して新しいものに合わせる |
| StreamlitAPIException: st.session_state has no key ... | 保存前のキーを参照している |
if "key" not in st.session_stateのように初期値を入れてから読む |
| 表の表示時にArrowTypeErrorが出る | 1つの列に数値と文字列が混ざっている |
astype("string")で文字列に寄せてから表示する |
CSVクリーニングアプリで注意したい点
型変換で一番戸惑うのは、失敗した値が例外ではなく欠損値に変わる挙動です。errors="coerce"は処理を止めない代わりに、気付かないうちに欠損値を増やすことがあります。変換後に欠損値数をもう一度見る習慣が効きます。
Int64とint64の違いも混同しやすい部分です。大文字で始まるInt64は欠損値を保持できる型で、小文字のint64には欠損値を入れられません。
重複判定はdf.duplicated()が全列一致の行だけを重複として数えます。ID列だけが違うデータは重複と見なされないので、必要なら比較対象の列を絞る発想が求められました。
ポイントとしては、型変換と重複判定で引っかかりやすい箇所を短くまとめます。
coerce:失敗値は欠損値化
Int64:欠損値を保持できる型
重複判定:全列一致のみ対象
適用順:型変換→重複→欠損
Pythonアプリで欠損値3件と重複行2件を確認
8行のサンプルCSVを用意し、ブラウザから実際に読み込ませました。列ごとの表には欠損値が合計3件、重複行は2件と表示され、想定と一致しています。
その状態でクリーニングを実行すると、比較表の行数が8から6へ変わりました。数字の変化として結果が反映されたことを確認できています。
最後にcleaned.csvのダウンロードボタンが押せる状態になるところまで見届けました。実ブラウザでの操作でも同じ手順をたどり、画面のキャプチャを残しています。
このセクションの用語
- st.file_uploader
- ブラウザからファイルを選ばせる
Streamlitの部品です。選ばれたファイルの中身はバイト列として受け取れます。
画面上で確認できた内容を挙げます。
- 列ごとのデータ型・欠損値数・欠損率がまとまった表が出ること
- 表全体の指標として重複行数2件が読み取れること
- クリーニング前後の比較表で行数が8から6へ変わること
- 前後の先頭10行が並んで表示されること
-
cleaned.csvのダウンロードボタンが押せる状態になること


CSVクリーニングアプリの活用例
このアプリは特定の列名や業務に依存していないので、CSVを扱う場面ならそのまま持ち込めます。前処理の前に状態を見る道具として置いておくと便利です。
特に効くのは、Pythonを書かない人と一緒にデータを見たいときです。画面を共有すれば、欠損の多い列や重複の有無をその場で確認しながら話を進められます。
このセクションの用語
- 前処理
- 集計や学習の前にデータを整える作業のことです。欠損値の補完や重複削除、型そろえなどが含まれます。
- スキーマ
- 各列の名前や型、必須かどうかといったデータの構造の決まりです。投入先のシステムと形を合わせる基準になります。
| 使える場面 | 具体的な使い方 |
|---|---|
| アンケート結果の集計前 | 空欄の多い設問を欠損率で洗い出し、二重送信の重複行を削除してから集計に回す |
| 売上・経費CSVの取り込み前 | 金額列を小数(float)、日付列を日時(datetime)に変換し、変換で欠損値が増えた列から入力ミスを探す |
| データベースやBIへの投入前 |
profile_columnsの欠損率で必須列の欠けを確認し、型が想定どおりかをスキーマと突き合わせる |
| 非エンジニアへの前処理の依頼 | 画面のURLを共有し、ブラウザ操作だけでcleaned.csvを作って戻してもらう |
| 機械学習の前処理の下地作り | 欠損値の埋め方を切り替えて行数と欠損合計の変化を見比べ、採用する方法を決める |
PythonとStreamlitでCSV点検を作った振り返り
Streamlitとpandasの組み合わせは、CSVの状態確認をそのまま画面にできる相性の良さがありました。列ごとの集計をprofile_columns、表全体をframe_statsに分けたことで、前後比較まで素直につながっています。
実際に8行のCSUを試し、欠損値3件と重複行2件の表示から、行数8→6の比較結果とcleaned.csvのダウンロード準備まで確認できました。手元のファイルで同じ流れをたどれば挙動はすぐ掴めます。
次に足すなら、重複判定の対象列を選べるようにしたり、外れ値の確認を加える方向が考えられます。1ファイル構成なので、関数を1つ増やしてUIに選択肢を足すだけで拡張できそうです。
作ってみて手応えを感じた点を挙げます。
- 関数がDataFrameを書き換えないので、設定を変えて何度でも試し直せる
- 処理の適用順が固定されているため、結果の変化を追いやすい
- 結果を
st.session_stateに置くことで、再実行後もダウンロードを続けられる
参考にした一次情報
- ^ st.file_uploader - Streamlit Docs. https://docs.streamlit.io/develop/api-reference/widgets/st.file_uploader, (参照26-08-15).
- ^ st.download_button - Streamlit Docs. https://docs.streamlit.io/develop/api-reference/widgets/st.download_button, (参照26-08-15).
- ^ Working with missing data - pandas User Guide. https://pandas.pydata.org/docs/user_guide/missing_data.html, (参照26-08-15).
- ^ pandas.DataFrame.drop_duplicates - pandas API reference. https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.drop_duplicates.html, (参照26-08-15).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。
