特徴量エンジニアリングツール
就活等で成果物として記録します。(githubの扱いにまだ慣れていない)pythonのFlaskを用いて開発を行いました。
主な機能は...
・CSVファイルのプレビュー
・データの基本情報(df.info)の表示
・列ごとの欠損値数、欠損率の表示
・列削除
・平均値・中央値・最頻値による欠損値補完
・勾配ブースティングによる欠損値補完
勾配ブースティングについては、ユーザーが任意の特徴量、パラメータをある程度選択できます。
requirements.txt
requirements.txt
blinker==1.9.0
click==8.1.8
colorama==0.4.6
Flask==3.1.0
Flask-SQLAlchemy==3.1.1
greenlet==3.1.1
itsdangerous==2.2.0
Jinja2==3.1.5
joblib==1.4.2
MarkupSafe==3.0.2
numpy==2.2.2
pandas==2.2.3
python-dateutil==2.9.0.post0
pytz==2025.1
scikit-learn==1.6.1
scipy==1.15.1
six==1.17.0
SQLAlchemy==2.0.38
threadpoolctl==3.5.0
typing_extensions==4.12.2
tzdata==2025.1
Werkzeug==3.1.3
xgboost==2.1.4
csv_processor.py
ファイルの読み込みから各種機能の主な部分がコーディングされています。csv_processor.py
from xgboost import XGBRegressor, XGBClassifier
from sklearn.preprocessing import LabelEncoder
import pandas as pd
import numpy as np
import io
class CSVProcessor:
def __init__(self, file_input):
if isinstance(file_input, str):
# ファイルパスを指定してデータフレームを作成
self.df = pd.read_csv(file_input)
else:
byte_stream = file_input.read() # バイトストリームを取得
decode_string = byte_stream.decode('utf-8') # バイトストリームをデコード
string_io = io.StringIO(decode_string) # デコードした文字列をStringIOオブジェクトに変換
self.df = pd.read_csv(string_io) # StringIOオブジェクトからデータフレームを作成
self.encorders = {}
def get_preview(self, rows=50):
return self.df.head(rows).to_html(classes='table table-bordered table-striped', index=False)
def get_basic_info(self):
# データフレームの情報を取得
info_buffer = io.StringIO() # StringIOオブジェクト作成
self.df.info(buf=info_buffer) # データフレームの情報を取得
return info_buffer.getvalue().replace("\n", "<br>") # 改行を<br>に変換
def get_missing_info(self):
# 欠損情報のデータフレーム作成
total_rows = len(self.df) # データフレームの行数
missing_count = self.df.isnull().sum() # 欠損数
missing_percentage = (missing_count / total_rows * 100).round(2) # 欠損率
missing_info_df = pd.DataFrame({
'列名': missing_count.index,
'欠損数': missing_count.values,
'欠損率 (%)': missing_percentage.values
})
# 余計な空白を削除
missing_info_df.columns = missing_info_df.columns.str.strip()
return missing_info_df
def highlight_missing_info(self, threshold=10):
#欠損率に基づいて欠損値情報をハイライト表示
missing_info_df = self.get_missing_info()
def gradient_highlight(val):
if isinstance(val, (int, float)):
if val < threshold:
return '' # ハイライトなし
elif threshold <= val < 30:
return 'background-color: #ffe6e6;' # 薄いピンク
elif 30 <= val < 50:
return 'background-color: #ffcccc;' # 薄い赤
elif 50 <= val < 70:
return 'background-color: #ff9999;' # 標準的な赤
else:
return 'background-color: #ff4d4d;' # 濃い赤
return ''
# ハイライト適用
styled_missing_info = missing_info_df.style.applymap(
gradient_highlight, subset=['欠損率 (%)']
).hide(axis='index').set_table_attributes('class="table table-bordered table-hover"')
return styled_missing_info.to_html()
# 特定の列削除
def remove_columns(self, columns):
# 指定した列を削除しデータフレームを更新
existing_columns = [col for col in columns if col in self.df.columns]
if not existing_columns:
raise ValueError('指定した列が見つかりません。:{columns}')
self.df.drop(columns=existing_columns, inplace=True)
return self
# 欠損値をmean, median, modeで補完
def fill_missing(self, strategy='mean', columns=None):
if columns in None:
columns = self.df.select_dtypes(include=['number']).columns.to_list() # 数値列を取得
for col in columns:
if col not in self.df.columns:
raise ValueError(f"指定された列'{col}'は存在しません。")
if strategy == 'mean':
self.df[col].fillna(self.df[col].mean(), inplace=True) # 欠損値を平均値で補完
elif strategy == 'median':
self.df[col].fillna(self.df[col].median(), inplace=True) # 欠損値を中央値で補完
elif strategy == 'mode':
self.df[col].fillna(self.df[col].mode()[0], inplace=True) # 欠損値を最頻値で補完
else:
raise ValueError(f"strategyには'mean', 'median', 'mode'のいずれかを指定してください。")
return self
def fill_missing_gradient_boosting(self, target_column, feature_columns, n_estimators=100, learning_rate=0.1, max_depth=3):
# 欠損値を勾配ブースティングで補完
# データを欠損値の有無で分割
if target_column not in self.df.columns:
raise ValueError(f"指定されたターゲット列'{target_column}'は存在しません。")
missing_features = [col for col in feature_columns if col not in self.df.columns]
if missing_features:
raise ValueError(f"指定された特徴量列'{missing_features}'は存在しません。")
train_data = self.df[self.df[target_column].notnull()] # 欠損値がないデータ
test_data = self.df[self.df[target_column].isnull()] # 欠損値があるデータ
if train_data.empty or test_data.empty:
raise ValueError("欠損値のある行または欠損値のない行が存在しません。")
# カテゴリ変数かを判定
is_categorical = self.df[target_column].dtype == 'object'
# カテゴリ変数の場合はラベルエンコーディング
if is_categorical:
le_target = LabelEncoder()
train_data[target_column] = le_target.fit_transform(train_data[target_column].astype(str)) # ターゲット列をラベルエンコーディング
self.encorders[target_column] = le_target # エンコーダを保存
# 特徴量のエンコーディング
for col in feature_columns:
if self.df[col].dtype == 'object':
le = LabelEncoder()
train_data[col] = le.fit_transform(train_data[col].astype(str))
test_data[col] = le.transform(test_data[col].astype(str))
self.encorders[col] = le
X_train = train_data[feature_columns]
y_train = train_data[target_column]
X_test = test_data[feature_columns]
# 勾配ブースティングモデルの作成
model = XGBClassifier(n_estimators=n_estimators, learning_rate=learning_rate, max_depth=max_depth, radom_state=0)\
if is_categorical else XGBRegressor(n_estimators=n_estimators, learning_rate=learning_rate, max_depth=max_depth, radom_state=0)
model.fit(X_train, y_train)
# 欠損値の補完
predicted_values = model.predict(X_test)
if is_categorical:
predicted_values = le_target.inverse_transform(predicted_values)
self.df.loc[self.df[target_column].isnull(), target_column] = predicted_values
return self
app.py
ルーティング等app.py
from flask import Flask, request, render_template, redirect, url_for, send_file
from models import db, UploadedFile
from csv_processor import CSVProcessor
import os
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///files.db'
db.init_app(app)
UPLOAD_FOLDER = "uploads"
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files['file']
if file and file.filename.endswith('.csv'):
filepath = os.path.join(UPLOAD_FOLDER, file.filename)
file.save(filepath)
# 同じファイル名が既に存在する場合は削除
existing_file = UploadedFile.query.filter_by(filename=file.filename).first()
if existing_file:
os.remove(existing_file.filepath)
db.session.delete(existing_file)
db.session.commit()
# 新しいファイルをDBに保存
uploaded_file = UploadedFile(filename=file.filename, filepath=filepath)
db.session.add(uploaded_file)
db.session.commit()
return redirect(url_for('display_file', file_id=uploaded_file.id))
files = UploadedFile.query.order_by(UploadedFile.created_at.desc()).all()
return render_template('index.html', files=files)
@app.route('/file/<int:file_id>')
def display_file(file_id):
file_record = UploadedFile.query.get_or_404(file_id)
processor = CSVProcessor(file_record.filepath)
table = processor.get_preview()
basic_info = processor.get_basic_info()
missing_info = processor.highlight_missing_info(threshold=10)
all_columns = processor.df.columns.tolist()
files = UploadedFile.query.order_by(UploadedFile.created_at.desc()).all()
return render_template('index.html', table=table, basic_info=basic_info, missing_info=missing_info,
filename=file_record.filename, all_columns=all_columns, files=files, current_file_id=file_id)
@app.route('/remove_columns/<int:file_id>', methods=['POST'])
def remove_columns(file_id):
file_record = UploadedFile.query.get_or_404(file_id)
processor = CSVProcessor(file_record.filepath)
columns_to_remove = request.form.getlist('columns_to_remove')
if not columns_to_remove:
return redirect(url_for('display_file', file_id=file_id, error="削除する列を選択してください。"))
try:
processor.remove_columns(columns_to_remove)
processor.df.to_csv(file_record.filepath, index=False)
return redirect(url_for('display_file', file_id=file_id))
except Exception as e:
return redirect(url_for('display_file', file_id=file_id, error=str(e)))
@app.route('/fill_missing/<int:file_id>', methods=['POST'])
def fill_missing(file_id):
file_record = UploadedFile.query.get_or_404(file_id)
processor = CSVProcessor(file_record.filepath)
columns_to_fill = request.form.getlist('columns_to_fill')
strategy = request.form.get('strategy')
if not columns_to_fill:
return redirect(url_for('display_file', file_id=file_id, error="補完する列を選択してください。"))
try:
processor.fill_missing(strategy=strategy, columns=columns_to_fill)
processor.df.to_csv(file_record.filepath, index=False)
return redirect(url_for('display_file', file_id=file_id))
except Exception as e:
return redirect(url_for('display_file', file_id=file_id, error=str(e)))
@app.route('/fill_missing_gb/<int:file_id>', methods=['POST'])
def fill_missing_gb(file_id):
file_record = UploadedFile.query.get_or_404(file_id)
processor = CSVProcessor(file_record.filepath)
target_column = request.form.get('target_column')
feature_columns = request.form.getlist('feature_columns')
n_estimators = int(request.form.get('n_estimators', 100))
learning_rate = float(request.form.get('learning_rate', 0.1))
max_depth = int(request.form.get('max_depth', 3))
if not target_column or not feature_columns:
return redirect(url_for('display_file', file_id=file_id, error="補完するターゲット列と特徴量を選択してください。"))
try:
processor.fill_missing_gradient_boosting(target_column, feature_columns, n_estimators, learning_rate, max_depth)
processor.df.to_csv(file_record.filepath, index=False)
return redirect(url_for('display_file', file_id=file_id))
except Exception as e:
return redirect(url_for('display_file', file_id=file_id, error=str(e)))
@app.route('/delete_file/<int:file_id>', methods=['POST'])
def delete_file(file_id):
file_record = UploadedFile.query.get_or_404(file_id)
# ファイルを削除
try:
os.remove(file_record.filepath)
except FileNotFoundError:
pass
# データベースからも削除
db.session.delete(file_record)
db.session.commit()
return redirect(url_for('upload_file'))
@app.route('/download/<int:file_id>')
def download_file(file_id):
file_record = UploadedFile.query.get_or_404(file_id)
return send_file(file_record.filepath, as_attachment=True, download_name=file_record.filename)
if __name__ == '__main__':
app.run(debug=True)
models.py
flask_sqlalchemyを用いた簡易的なデータベース。models.py
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
class UploadedFile(db.Model):
__tablename__ = 'uploaded_files'
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
filepath = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'<UploadedFile {self.filename}>'
index.html
templatesはindex.htmlのみindex.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSVファイル管理ツール</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.table-container {
overflow-x: auto;
max-height: 800px;
}
.col-preview { width: 35%; }
.col-middle { width: 30%; }
.col-info { width: 35%; }
</style>
</head>
<body>
<div class="container-fluid mt-4">
<h1 class="text-center mb-4">CSVファイル管理ツール</h1>
<!-- ファイルアップロードフォーム -->
<form method="POST" enctype="multipart/form-data" class="mb-4">
<input type="file" name="file" class="form-control mb-2" accept=".csv" required>
<button type="submit" class="btn btn-primary w-100">ファイルアップロード</button>
</form>
<!-- ファイルリスト -->
<h3>アップロード済みファイル</h3>
<ul class="list-group mb-4">
{% for file in files %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{{ url_for('display_file', file_id=file.id) }}">{{ file.filename }}</a>
<form method="POST" action="{{ url_for('delete_file', file_id=file.id) }}" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm">削除</button>
</form>
</li>
{% endfor %}
</ul>
{% if table %}
<div class="row">
<!-- 左側(プレビュー) -->
<div class="col-preview">
<h3>データプレビュー: {{ filename }}</h3>
<div class="table-container table-responsive">{{ table | safe }}</div>
</div>
<!-- 中央(列削除 & 欠損値補完) -->
<div class="col-middle">
<h3>データ前処理</h3>
<!-- 列削除フォーム -->
<form method="POST" action="{{ url_for('remove_columns', file_id=current_file_id) }}" class="mb-4">
<h5>列削除</h5>
{% for column in all_columns %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="columns_to_remove" value="{{ column }}" id="remove_{{ column }}">
<label class="form-check-label" for="remove_{{ column }}">{{ column }}</label>
</div>
{% endfor %}
<button type="submit" class="btn btn-danger w-100 mt-2">選択した列を削除</button>
</form>
<!-- 欠損値補完(平均・中央値・最頻値) -->
<form method="POST" action="{{ url_for('fill_missing', file_id=current_file_id) }}" class="mb-4">
<h5>欠損値補完(平均・中央値・最頻値)</h5>
<label class="form-label">補完する列を選択:</label>
{% for column in all_columns %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="columns_to_fill" value="{{ column }}" id="fill_{{ column }}">
<label class="form-check-label" for="fill_{{ column }}">{{ column }}</label>
</div>
{% endfor %}
<label class="form-label mt-2">補完方法:</label>
<select class="form-select" name="strategy" required>
<option value="mean">平均値補完</option>
<option value="median">中央値補完</option>
<option value="mode">最頻値補完</option>
</select>
<button type="submit" class="btn btn-info w-100 mt-2">欠損値を補完</button>
</form>
<!-- 欠損値補完(勾配ブースティング) -->
<form method="POST" action="{{ url_for('fill_missing_gb', file_id=current_file_id) }}" class="mb-4">
<h5>欠損値補完(勾配ブースティング)</h5>
<label class="form-label">ターゲット列:</label>
<select class="form-select" name="target_column" required>
<option value="" disabled selected>選択してください</option>
{% for column in all_columns %}
<option value="{{ column }}">{{ column }}</option>
{% endfor %}
</select>
<label class="form-label mt-2">特徴量を選択:</label>
{% for column in all_columns %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="feature_columns" value="{{ column }}" id="feature_{{ column }}">
<label class="form-check-label" for="feature_{{ column }}">{{ column }}</label>
</div>
{% endfor %}
<label class="form-label mt-2">パラメータ設定:</label>
<input type="number" class="form-control mb-2" name="n_estimators" value="100" min="10" required>
<input type="number" class="form-control mb-2" name="learning_rate" step="0.01" value="0.1" required>
<input type="number" class="form-control mb-2" name="max_depth" value="3" required>
<button type="submit" class="btn btn-warning w-100 mt-2">欠損値を補完</button>
</form>
<!-- データのダウンロード -->
<a href="{{ url_for('download_file', file_id=current_file_id) }}" class="btn btn-success w-100 mt-3">データをダウンロード</a>
</div>
<!-- 右側(欠損情報 & 基本情報) -->
<div class="col-info">
<h3>データ情報</h3>
<div>{{ basic_info | safe }}</div>
<h3 class="mt-4">欠損値情報</h3>
<div class="table-container table-responsive">{{ missing_info | safe }}</div>
</div>
</div>
{% endif %}
</div>
</body>
</html>
今後の展望
・行削除の追加
・ワンホットエンコーディングなどデータ加工機能の更なる追加
・勾配ブースティング以外の汎用性のある欠損値補完の追加
・データのmergeなど複数ファイルを扱えるようにする
・一部グラフ等の可視化ツール作成
・複数ファイルを同時に見えるようにする
...etc