pythonを使ってアンケート生データを処理してみた
対象読者
- Excel は触れるけど Python はこれから
- クロス集計を毎回手で作るのがつらい
前提
今回、業者に頼んでアンケートを取ったんですが、アンケート解析をもっとしたい!
けど、納品まで終わってしまった・・・
自分でクロス分析とかしたいけど、手作業だと時間がかかる・・・
と言うのを解決するために、作ってみました。
どのようなデータかは割愛しますが、今回のものをカスタマイズすれば、業務の自動化ができると思います。
セル0,1 準備: Colab ノートブックを5セルに分ける
# 0️⃣ Colab: 必要ライブラリ
!pip install --quiet openpyxl==3.1.2 pandas ipywidgets
# 1️⃣ Excel をアップロード(複数可)
from google.colab import files
uploaded = files.upload()
以降は
セル | 役割 |
---|---|
2 |
parse_format / build_raw 共通関数 |
3 | アップロードしたファイルに raw シートを作る |
4 |
made_cross (クロス表を付与) |
5 | ドロップダウン GUI & 連続実行 |
という構成で進めます。
セル2 ~投入されたFormatシートを読み解き、人間が読みやすいrawシートをつくる関数を設定するセル~
役割 | どのシートを参照 | このセルを終えると出来ること |
---|---|---|
1️⃣ parse_format()
|
Format | 「質問コード → 質問タイプ」を辞書で取得。選択肢範囲やタイトル行番号も同時にメモする。 |
2️⃣ build_raw()
|
data / Format | 回答データを raw シートにコピーしつつ、セルに数式を自動挿入。Excel を開くだけで可読値が表示される。 |
from openpyxl.utils import get_column_letter
def parse_format(ws):
need = {"Column","Question","Type","CtgNo","Title"}
hdr = {h.value.strip(): c # <①>
for c,h in enumerate(ws[1],1)
if (h.value or "").strip() in need}
miss = need - hdr.keys()
if miss:
raise ValueError(f"Format に欠落: {miss}")
Q,T,CNO,TTL = hdr["Question"],hdr["Type"],hdr["CtgNo"],hdr["Title"]
type_map,s_ranges,ml_titles,title_map = {},{},{},{}
cur=None
for r in range(2, ws.max_row+1): # <②>
q = ws.cell(r,Q).value
t = (ws.cell(r,T).value or "").strip()
if q: # 質問コードの行
cur=t or cur
type_map[q]=cur
title_map[q]=ws.cell(r,TTL).value
if cur in ("S","SL"): # 選択肢範囲を覚える
s=r+1
while s<=ws.max_row and ws.cell(s,CNO).value not in ("",None):
s+=1
s_ranges[q]=(r+1,s-1)
if cur in ("ML","M"): # タイトル行
ml_titles[q]=r
elif cur in ("ML","M") and ws.cell(r,Q).value: # サブ質問行
sub=ws.cell(r,Q).value
type_map[sub]=cur
title_map[sub]=ws.cell(r,TTL).value
ml_titles[sub]=r
return type_map,s_ranges,ml_titles,title_map,hdr
def build_raw(wb, tmap, srg, mlt, hdr,
data=\"data\", fmt=\"Format\", raw=\"raw\"):
ws_d, ws_f = wb[data], wb[fmt]
ws_r = wb[raw] if raw in wb.sheetnames else wb.create_sheet(raw)
ws_r.delete_rows(1, ws_r.max_row) # ← 再実行時にゴミを消す
c_cno = get_column_letter(hdr[\"CtgNo\"])
c_ttl = get_column_letter(hdr[\"Title\"])
# ① ヘッダーコピー ----------------------------------------
for c,cell in enumerate(ws_d[1],1):
ws_r.cell(1,c,cell.value)
# ② 回答行ループ -----------------------------------------
for r in range(2, ws_d.max_row+1):
for c in range(1, ws_d.max_column+1):
key = ws_r.cell(1,c).value # 列ヘッダー = Question
colL = get_column_letter(c)
dref = f\"{data}!{colL}{r}\"
typ = tmap.get(key,\"\") # 質問タイプ
# ③ タイプ別に数式を生成 ----------------------------
if typ in (\"S\",\"SL\"):
if key in srg: # VLOOKUP 範囲がある
s,e = srg[key]
rng = f\"{fmt}!${c_cno}${s}:${c_ttl}${e}\"
formula = f\"=IFERROR(VLOOKUP({dref},{rng},2,FALSE),\\\"\\\")\"
else:
formula = f\"={dref}\" # 範囲が無ければ値丸コピー
elif typ==\"N\": # 数値はそのまま
formula = f\"={dref}\"
elif typ in (\"X\",\"XL\"): # 自由入力:空白ケア
formula = f\"=IF({dref}=\\\"\\\",\\\"\\\",{dref})\"
elif typ in (\"ML\",\"M\"): # 複数 / 単一フラグ
row = mlt.get(key)
tref= f\"{fmt}!${c_ttl}${row}\" if row else \"\\\"\\\"\"
formula = f\"=IF({dref}=1,{tref},\\\"\\\")\"
else: # 不明タイプ→セル参照
formula = f\"={dref}\"
ws_r.cell(r,c,formula)
parse_format() を用いて、Formatシートを読み解く
行 | 処理 | 例え話 |
---|---|---|
<①> ヘッダー検出 |
1 行目だけを眺め、Column / Question / … が各何列目かを拾う。 |
「見出しが何列目にあるかの地図」を先につくる。 |
<②> ループ |
2 行目から最終行まで Format をスキャン。 | 「質問行」を見つけたら タイプ を覚える→次の処理で利用。 |
type_map |
{\"AQ3_1\":\"ML\", \"AQ6\":\"S\", …} |
「質問コードを渡すとタイプが返る辞書」。後で raw 生成・cross 集計の核になる。 |
s_ranges |
{ \"AQ6\": (行12, 行20) } |
S/SL 質問は選択肢を Format!E:F から VLOOKUP したい。範囲を動的に拾うための辞書。 |
ml_titles |
{ \"AQ3_1[5]\": 35 } |
ML/M は “タイトル行番号” が必要(1→○○ と変換)。 |
title_map |
{ \"AQ3_1[5]\":\"電子チケット…\" } |
サブ質問コード→ラベル文字列。GUI、cross 表の列名に再利用。 |
💡 イメージしやすいポイント
-
Format シートは「辞書生成工場」
いきなり計算せず “情報を抜き出すだけ” に徹する。 - だから 1 回呼ぶだけで 後続セルが何回でも使い回せる。
build_raw()関数で数字の“生データ”を“人間が読見やすい”形へ
ステップ | 参照シート | 説明 | 見える成果 |
---|---|---|---|
ヘッダーコピー | data |
1 行目をそのまま raw に転写 | raw の 1 行目 = 元データの列名 |
行ループ | data |
2 行目〜最終行を走査 | 行数は増えず処理量はデータ量に比例 |
列ループ | data / Format |
質問タイプを見て 数式 を組み立て | raw のセルは値ではなく数式 |
VLOOKUP | Format!E:F |
S/SL→回答番号→ラベルに変換 | Excel で開くと “1 → 男性” のように可読値 |
IF で空白処理 | — | X/XL タイプは “空文字なら空セル” | データ入力忘れでも余分な 0 が出ない |
IF(dref=1, label, "") | ML/M |
フラグ型回答をラベルに展開 | 1 行に “1” があればラベル文字列 |
ポイント
- raw シート のセルは 計算結果ではなく数式 なので、
データ修正があっても Excel 側で再計算すれば常に最新。- 生データを壊さず、新しいシートで加工する安全運転。
今回の事例における“タイプ別の数式”
タイプ | 数式テンプレート | 出力例 (Excel で見える値) |
---|---|---|
S / SL | VLOOKUP() |
1 → 覚えていない
|
N | =data!B5 |
23 (数値そのまま) |
X / XL | IF(空なら空, それ以外は値) |
空欄 → 空欄 / abc → abc
|
ML / M | IF(flag=1, ラベル, \"\") |
1 → 電子チケット発行可
|
結果
- Format シート → “辞書の山” : あらゆる変換計算の基礎データ
- data → raw : 見やすさ・参照のしやすさを両立
- 以降のセルでは raw と 辞書 を武器に「クロス分析」「GUI」などを自由に積み上げるだけ
Python 初学者は「辞書を作る=情報抽出専門の関数」「数式を書く=変換専門の関数」と分けて書くと、後から機能を足しても迷子になりません。
セル 3 ~セル2で理解したものをrawシート付きとして出力するセル〜
ゴール:アップロードした Excel をすべて
① Format を解析 → ② raw シートを追加 → ③ メモリに保持
するところまで自動化します。
このセルでやること | 参照シート | 使う関数 | 出力 |
---|---|---|---|
1. ファイルを開く | — | load_workbook() |
wb (Workbook オブジェクト) |
2. Format を辞書化 | Format |
parse_format() (セル 2で定義) |
tmap / srg / mlt / hdr |
3. raw シートを作る | data / Format |
build_raw() (セル 2) |
raw が付いた Workbook |
4. メモリに保存 | — | — | wb_objects[ファイル名] = wb |
5. _raw.xlsx を保存 |
— | wb.save() |
確認用ファイル |
from pathlib import Path
from openpyxl import load_workbook
wb_objects = {} # 後で cross を作るために保持
for fn in uploaded.keys(): # アップロードしたファイルを順に処理
wb = load_workbook(fn, data_only=False) # ❶
# ❷ Format を解析して 4つの辞書 + hdr を得る
tmap, srg, mlt, ttl, hdr = parse_format(wb["Format"])
# ❸ data → raw を生成
build_raw(wb, tmap, srg, mlt, hdr)
wb_objects[fn] = wb # ❹
# ❺ ファイル名_raw.xlsx で保存
out = Path(fn).with_stem(Path(fn).stem + "_raw.xlsx")
wb.save(out)
# files.download(out) # 必要ならコメント解除して即DL
print("raw シート生成完了")
ステップごとの超ていねい解説
load_workbook(fn, data_only=False)
❶ キーワード | 意味 |
---|---|
fn |
アップロードしたファイル名(uploaded 辞書のキー) |
data_only=False |
数式を消さずに読み込む オプション。raw シートでは 数式そのもの を書きたいので必須。 |
イメージ:Excel を「編集モード」で開く感じ。
Python からセルオブジェクトを自由に書き換えられる。
parse_format(wb["Format"])
❷ セル 2 で作った「Format 辞書工場」を呼び出し。
-
何を返す?
-
tmap
: 質問コード → タイプ (S/ML/M…
) -
srg
: S/SL 質問 → (開始行, 終了行) -
mlt
: ML/M サブ質問 → タイトル行番号 -
ttl
: 質問コード → タイトル文字列(GUI で欲しい) -
hdr
: ヘッダー名 → 列番号
-
-
なぜ必要? →
build_raw()
が “どのセルにどの数式を入れるか” を決める材料。
build_raw(wb, …)
❸ -
入力:今読み込んだ
wb
と ❷ で作った辞書類 -
処理:
- data シートから 1 行目ヘッダーを raw にコピー
- 2 行目以降をループ→列ごとにタイプを判定→数式をセルに書き込む
-
結果:
raw
シートが Workbook に追加される。
ここで Excel はまだ保存されていない → メモリ上の
wb
が更新されただけ。
wb_objects[fn] = wb
❹ - 目的:あとでセル 5(GUI)からクロス集計を作るために メモリ上の Workbook を保持。
- メリット:再度ファイルを開き直さなくて済む → 速度&メモリ節約。
_raw.xlsx
を保存
❺ -
なぜ別名?
- 元データを壊さずに「raw 付き版」を手元に残すため。
-
files.download(out)
をコメントアウトした理由- 試行錯誤中は Colab の VM に置いておくだけで十分。
- すぐダウンロードしたい場合はコメントを外せばブラウザにリンクが出る。
やったこと
-
Format シートを解析 → 辞書が出来る
└─「質問タイプ」「選択肢行範囲」が Python に取り込まれた -
data → raw 変換
└─ raw シートには 数式入りで見やすい回答テーブル が生成 -
Workbook を保持
└─ 次のセルでクロス表を作るとき「再ロード不要」で高速
💡 ポイント:セル 3 は「準備セル」。ここで raw 作成と辞書生成 を一気に終わらせ、後続セルは「クロス表ロジック」に集中できる構成になっています。
セル4 ~dataシートをクロス集計表に変換するセル~
やること | 参照するシート | 主な関数 | 生成物 |
---|---|---|---|
1. 質問メタを再取得 | Format | parse_format() |
tmap / srg / … |
2. 選んだ 2 質問の「選択肢リスト」を作る | Format / data | explode() |
行ラベル・列ラベル |
3. COUNTIFS 数式を raw から貼る |
data |
made_cross() 本体 |
件数セル |
4. 行合計・行内%・列合計を入れる | cross(作成中) | 同上 | 自動再計算セル |
5. 複数表を書く場合は 3 行空ける | cross | 同上 | 2つ目以降の表配置 |
前提: セル 3 でメモリ内に保存しておいた
wb_objects["ファイル名"]
(raw シート付き Workbook)を利用します。
“どの質問ペアでクロス集計するか” は セル 5 の GUI で選択します。
import re, pandas as pd
from openpyxl.utils import get_column_letter
from openpyxl import load_workbook
def made_cross(wb, q_row, q_col,
sheet_data="data", sheet_fmt="Format", sheet_cross="cross"):
"""raw → cross シートを生成し、件数・行%・行合計・列合計を挿入。
すでに cross シートがあれば 3 行空けて追記する。"""
# 0) シートを取得 --------------------------------------
wsF, wsD = wb[sheet_fmt], wb[sheet_data]
# 1) Format 解析 ---------------------------------------
tmap, srg, mlt, ttl, hdr = parse_format(wsF)
def explode(q):
"""質問コード q から
- mode: 'single' or 'multi'
- opts: ラベル一覧
- vals: COUNTIFS で比較する値
- cols: data シート上の列記号
を返すユーティリティ"""
typ = tmap[q]
if typ in ("S", "SL"): # ▼単一選択
s, e = srg[q]
opts, vals = [], []
for r in range(s, e+1):
opts.append(wsF.cell(r, hdr["Title"]).value)
vals.append(wsF.cell(r, hdr["CtgNo"]).value)
colL = get_column_letter(
[i+1 for i, v in enumerate(wsD[1]) if v.value == q][0]
)
return {"mode":"single", "opts":opts, "vals":vals,
"cols":[colL]*len(opts)}
elif typ in ("M", "ML"): # ▼複数選択(MA)
subs = [k for k in tmap if re.match(fr"^{re.escape(q)}\\[\\d+\\]$", k)]
opts = [ttl[s] for s in subs] # サブ質問のラベル
cols = [get_column_letter(
[i+1 for i, v in enumerate(wsD[1]) if v.value == s][0]) for s in subs]
return {"mode":"multi", "opts":opts, "vals":[1]*len(opts), "cols":cols}
else:
raise ValueError(f"{q} はクロス対象外 (type={typ})")
R, C = explode(q_row), explode(q_col) # 行 / 列のメタ
sr, er = 2, wsD.max_row # data! の行範囲
# 2) cross シートの開始行 -------------------------------
if sheet_cross in wb.sheetnames:
wsX = wb[sheet_cross]
start = wsX.max_row + 4 # 3 行空けて追記
else:
wsX = wb.create_sheet(sheet_cross)
start = 1
# 3) 見出し行 ------------------------------------------
cur = start
wsX.cell(cur, 1, f"{q_row} × {q_col}(COUNTIFS)")
for j, h in enumerate(C["opts"], 2):
wsX.cell(cur, j, h)
total_col = len(C["opts"]) + 2
wsX.cell(cur, total_col, "合計")
cur += 1
first_data_row = cur
# 4) 件数セル・行合計・行内% ----------------------------
for r_opt, r_val, r_col in zip(R["opts"], R["vals"], R["cols"]):
wsX.cell(cur, 1, r_opt)
rng1 = f"{sheet_data}!${r_col}${sr}:${r_col}${er}"
for c_idx, (c_val, c_col) in enumerate(zip(C["vals"], C["cols"]), 2):
rng2 = f"{sheet_data}!${c_col}${sr}:${c_col}${er}"
wsX.cell(cur, c_idx,
f"=COUNTIFS({rng1},{r_val},{rng2},{c_val})")
# 行合計
left = get_column_letter(2)
right = get_column_letter(total_col - 1)
wsX.cell(cur, total_col,
f"=SUM({left}{cur}:{right}{cur})")
# 行内%
pct_row = cur + 1
wsX.cell(pct_row, 1, "")
for c in range(2, total_col):
L = get_column_letter(c)
T = get_column_letter(total_col)
wsX.cell(pct_row, c,
f"=IF({T}{cur}=0,\"\",ROUND({L}{cur}/{T}{cur}*100,2)&\"%\")")
cur += 2 # (件数行+%行)
last_data_row = cur - 2 # 最後の件数行
# 5) 列合計(件数行だけ合算) ----------------------------
wsX.cell(cur, 1, "合計")
for c in range(2, total_col + 1):
L = get_column_letter(c)
refs = ",".join(f\"{L}{r}\" for r in range(first_data_row, last_data_row+1, 2))
wsX.cell(cur, c, f\"=SUM({refs})\")
return wb
4-2 パーツ別にやさしく分解
コメントタグ | 何をしている? | 参照シート | ポイント |
---|---|---|---|
0) |
wb[sheet_fmt] wb[sheet_data] でシートを変数に。 |
Format / data |
wb[\"シート名\"] で Worksheet オブジェクト取得。 |
1) |
explode() で 質問コード → 選択肢ラベル一覧 を作る。 |
Format & data | 単一選択 は 1 列固定、複数選択 はサブ列ごとに 1 or 空白。 |
2) |
cross シートの開始行を決める。 |
cross | 既存表があれば「最終行 + 4」で 3 行空ける =視認性。 |
3) | 表タイトル + 列見出しを出力。 | cross |
total_col = 列数 + 1 で右端「合計」列を予約。 |
4) | 行ループ → COUNTIFS → 行合計 → 行内%。 |
data |
COUNTIFS は「列範囲, 比較値」ペアで複数指定できる関数。 |
5) | 列合計行。奇数行(件数行)だけを SUM() 。 |
cross |
% 行 を合算に含めないため、行番号をスキップ=range(...,2)
|
explode() が返す辞書イメージ
Key | single (S/SL) | multi (M/ML) |
---|---|---|
Type |
"single" |
"multi" |
Title |
[0円〜 , 500円〜 , …] |
サブ質問のタイトル文字列 |
CtgNo |
[1, 2, 3, …] |
[1, 1, 1, …] (フラグ) |
Column |
['D', 'D', …] (全部同じ列) |
['F', 'G', …] (サブ列ごと) |
なぜ必要?
COUNTIFS(range1, val1, range2, val2)
の range と val を
「行×列のマス目ごと」に組み合わせるため。
- raw シートを 直接参照 しない
→ “最終ユーザーは data シートを書き換えるだけ” で最新化。 - 行・列合計が付くので 合計の過不足チェック がすぐ出来る。
セル4が後続セルへ渡すもの
渡すもの | 使い道 | 受け取るセル |
---|---|---|
更新済み wb
|
GUI から保存・DL | セル 5 (finish() 内) |
cross シート |
完成したクロス表 | Excel ユーザーが開いて確認 |
ポイント:セル 4 までで “計算ロジック” は完結。
セル 5 は ユーザー I/O(質問選択&保存) だけに集中できる構成です。
セル5 ~GUIで「質問ペア → クロス表」生成&最終保存できる機能~
ここまでのセルで raw シート生成(セル 3)と
クロス集計ロジック(セル 4)が完成。
セル 5 は “Python が苦手な人” でも触れる
「質問を選ぶドロップダウン+ボタン2つ」 の UI を用意します。
役割 | 触るオブジェクト | 何が起こる? |
---|---|---|
1. ドロップダウンで質問を選ぶ | ipywidgets.Dropdown |
クリック1回で “縦軸 / 横軸” をセット |
2. クロス分析を追加 ボタン | run_btn |
セル 4 の made_cross() を呼び、cross シートに表を追記 |
3. 終了してダウンロード ボタン | fin_btn |
加工済み Workbook を保存→ブラウザにダウンロードリンク |
4. ステータス表示 | widgets.Label |
進捗を「クロス分析中…☑完了」などと表示 |
コード全文
import ipywidgets as widgets
from IPython.display import display
from google.colab import files
import re
# 1️⃣ raw 付き Workbook を1つ選ぶ --------------------------
fn0 = next(iter(wb_objects)) # 1ファイル想定ならこれで十分
wb = wb_objects[fn0]
# 2️⃣ ドロップダウン用の選択肢を作る -------------------------
tmap, *_ = parse_format(wb["Format"])
valid_t = ("S", "SL", "M", "ML")
is_parent = lambda q: not re.search(r"\[\\d+\\]$", q)
q_list = sorted([q for q, typ in tmap.items()
if typ in valid_t and is_parent(q)])
row_dd = widgets.Dropdown(options=q_list, description="縦軸")
col_dd = widgets.Dropdown(options=q_list, description="横軸")
# 3️⃣ ボタンとステータス ------------------------------------
run_btn = widgets.Button(description="クロス分析を追加")
fin_btn = widgets.Button(description="終了してダウンロード",
button_style="danger")
status = widgets.Label(value="") # 進捗テキストをここに出す
# 4️⃣ ボタンイベント ----------------------------------------
def do_cross(_):
status.value = "クロス分析中..."
made_cross(wb, row_dd.value, col_dd.value) # ★ セル4 関数
status.value = f"☑ 完了: {row_dd.value} × {col_dd.value}"
def finish(_):
status.value = "保存中..."
out = "with_cross.xlsx"
wb.save(out)
files.download(out) # ブラウザにDLリンク
status.value = "✅ ダウンロードを開始しました"
run_btn.disabled = True
fin_btn.disabled = True
run_btn.on_click(do_cross)
fin_btn.on_click(finish)
# 5️⃣ 画面に並べて表示 -------------------------------------
display(row_dd, col_dd, run_btn, fin_btn, status)
ポイント解説
wb_objects
)
① Workbook 選択 (-
セル 3 で作った
wb_objects
は 辞書:
{"sample.xlsx": Workbookオブジェクト, …}
-
next(iter(wb_objects))
で 最初のキー を取り出し、対応する Workbook を取得。-
複数ファイル扱いたい場合は
Dropdown
でファイル名も選ばせると応用可。
-
複数ファイル扱いたい場合は
q_list
)
② ドロップダウンの元ネタ (-
parse_format()
で得たtmap
から- タイプが
S/SL/M/ML
の 親質問だけ 抽出 -
[n]
が付くサブ質問は GUI に出さない(ユーザーが混乱しないように)。
- タイプが
-
これで「縦軸」「横軸」をワンクリック選択可能。
on_click
)
③ ボタンのイベント (関数 | いつ呼ばれる | 何をする | 関連セル |
---|---|---|---|
do_cross() |
「クロス分析を追加」を押した瞬間 | - status を「クロス分析中…」に更新- セル 4 の made_cross() を呼び出し、cross シートに表を追記 |
セル 4 |
finish() |
「終了してダウンロード」 | - Workbook を with_cross.xlsx として保存- files.download() でブラウザにリンク表示- ボタン2つを無効化 |
— |
status.value
はただの 文字列ラベル。
ユーザーに「今処理中/完了」を見せるだけでロジックには無関係。
④ 連続クロス分析に対応
- ボタンを何度でも押せる →
made_cross()
内で 3 行空けて追記 される。 - 追加するたびに
status
が更新されるので処理が終わったのがわかる。
⑤ ダウンロードは最後に一回だけ
-
raw 生成時点ではファイルを書き出さず、ユーザーが「終了」したら保存。
- 無駄な I/O が減り Colab のストレージ節約。
- ユーザーが間違えて閉じても、途中保存せずに済む → ゴミファイル激減。
結果として
-
ドロップダウン選択
→ 変数row_dd.value
,col_dd.value
に質問コードが入る -
クロス分析を追加
→made_cross()
へ質問ペアを渡す
→ cross シート最下部に新しい表が 3 行空いて 追加 -
終了してダウンロード
→ いまメモリにある Workbook がwith_cross.xlsx
として保存
→ ブラウザにリンク → ローカル PC にダウンロード -
結果
→ 1 ファイルに 複数クロス表 が “階段状” に並び、
行合計・列合計・行内% まで自動計算済み。
初心者でも「ボタン2つでクロス表が量産できる」環境の出来上がりです。
終わりに
初めて業務に役立つpython開発を行いました。楽しかったですが、どのようにしたらpythonが理解しやすいように説明するかが肝でした。
Discussion