YouTubeデータ取得

YouTube動画一覧CSVをPythonで可視化する|月別公開本数・動画種別・再生数の見方

2026年9月8日

保存したYouTube動画一覧を、APIへ再アクセスせずに集計・可視化します。月別の公開本数、動画種別の構成、種別ごとの累積再生数上位を確認し、集計CSVとブラウザで開けるHTMLを手元へ保存する手順です。

自分のデータに合う実行経路を選ぶ

以下はWindows PowerShellでの手順です。Python 3.10以上を用意し、必要なファイルを保存したフォルダーをエクスプローラーで開き、アドレス欄に powershell と入力してEnterを押します。Get-Location で作業フォルダーを確認してください。コード全文は後ろに掲載しています。選んだ経路だけを実行し、別の経路のコードを同じファイルへつなげないでください。

API通信なしで「分析用CSV→集計CSVとHTML」へ進む段階です。まだ動画IDしかない場合、詳細APIで分析用データセットを作る手順が先に必要です。キーなしなら下の架空サンプル経路を選べます。

架空6行で試す(取得APIなし)

make_youtube_sample.pyvisualize_youtube_csv.pyaggregate_youtube_csv.pyを同じフォルダーへ保存します。取得用ファイルや.envは不要です。

python -m pip install pandas plotly
python make_youtube_sample.py
python visualize_youtube_csv.py --input outputs/youtube/sample/channel_video_analysis_dataset.csv --output outputs/youtube/sample/charts

自分の分析用CSVがある(取得APIなし)

可視化と独立集計の2ファイルだけを同じフォルダーへ保存し、pandas・Plotlyを導入します。入力CSVを outputs/youtube/channel_video_analysis_dataset.csv に置き、こちらを実行します。

python visualize_youtube_csv.py --input outputs/youtube/channel_video_analysis_dataset.csv --output outputs/youtube/charts

相対パスは作業フォルダー基準です。既存の出力先がある場合は停止するので、--outputへ未作成の別フォルダー名を指定します。最小例は 月別1枚だけを作る代替手順で、実用版の前提ではありません。実用版はaggregate_youtube_csv.pyをimportして集計するので、先に別実行する必要はありません。集計済みCSVのみを入力する再描画機能はありません。

終了時のchartsとmissing viewsを確認し、--outputに指定したフォルダーの monthly_publications.html を最初に開きます。架空6行は1月1本・2月2本・3月2本(日付不明1行だけ除外)。構成はvideo_types.csvの計6本、再生数が分かる5本(実測0を含む)はtop_videos.csvです。HTMLはそのフォルダー内で開き、CSVと照合します。これは月間再生数や実チャンネルの結果ではありません。

掲載コードと詳しい条件

実在チャンネルのデータがなくても、この記事の架空サンプル6行で最後まで試せます。掲載グラフはそのサンプルから作った見本で、実際のチャンネル分析結果ではありません。読者のローカルCSVをサイトへ送信したり、サイトの見本へ反映したりする機能はありません。

入力ファイルと準備

実データは分析用データセットを作る記事outputs/youtube/channel_video_analysis_dataset.csv を使います。まだ一覧がない場合は動画一覧取得・CSV保存から進んでください。この記事は保存後の集計担当で、取得コードは重複掲載しません。

Python 3.10以上を用意し、同じ作業フォルダーで実行します。今回の検証環境はPython 3.12、pandas 3.0.5、Plotly 7.0.0です。次のコマンドはWindowsのPowerShellなどのターミナルで実行します。

必要列は video_id(文字列)、titlepublished_at(UTCの日時または空欄)、video_typecontent_categoryview_count(0以上の整数または空欄)です。0は実測0、空欄は不明です。CSVと同名の.metadata.jsonがある場合はhashと保存件数を照合します。古いCSVでmetadataがなければ取得範囲・時点は不明として扱い、現在時刻で補いません。

APIなしで試す架空サンプルを作る

次を make_youtube_sample.py に保存してください。実データとは別の outputs/youtube/sample/ へCSVとmetadataを作ります。日本語、改行、引用符、日付欠損、再生数0と欠損を含む6行です。既存ファイルは上書きしません。

"""Fictional sample only. Never overwrites a real dataset."""
import hashlib
import json
from pathlib import Path
import pandas as pd

output = Path("outputs/youtube/sample/channel_video_analysis_dataset.csv")
if output.exists() or output.with_suffix(".metadata.json").exists():
    raise SystemExit("サンプルは既にあります。別フォルダーを選んでください。")
rows = [
    ["fiction01", '架空の講座「CSV」', "2026-01-31T23:30:00Z", "other_video", "education", "120"],
    ["fiction02", "架空の配信\n日本語・改行の例", "2026-02-01T00:30:00Z", "live_archive", "gaming", "300"],
    ["fiction03", "架空の短編(Shorts候補)", "2026-02-15T12:00:00Z", "shorts_candidate", "music", "0"],
    ["fiction04", "架空の日時不明動画", "", "unknown", "unknown", ""],
    ["fiction05", "架空の対談", "2026-03-05T09:00:00Z", "other_video", "talk", "80"],
    ["fiction06", "架空の音楽配信", "2026-03-20T10:00:00Z", "live_archive", "music", "200"],
]
df = pd.DataFrame(rows, columns=["video_id", "title", "published_at", "video_type", "content_category", "view_count"])
output.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(output, index=False, encoding="utf-8-sig")
meta = {"csv_file": output.name, "csv_sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
        "is_fictional_sample": True, "scope": "架空の6行。実在チャンネルの取得結果ではありません。",
        "saved_rows": len(df), "reference_at": None, "fetch_started_at": None, "fetch_finished_at": None,
        "month_timezone": "UTC"}
output.with_suffix(".metadata.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
print(output.resolve())

最小例:月別の公開本数をHTMLにする

次を minimal_youtube_chart.py に保存します。この最小例はサンプルCSVだけで動き、実用版の事前実行は不要です。出力先が存在する場合は別名にしてください。月の区切りはUTCです。

from pathlib import Path
import pandas as pd
import plotly.express as px

source = Path("outputs/youtube/sample/channel_video_analysis_dataset.csv")
output = source.parent / "minimal_monthly.html"
if output.exists():
    raise SystemExit("出力が既にあります。別名を指定してください。")
df = pd.read_csv(source, dtype=str, keep_default_na=False)
if "published_at" not in df:
    raise SystemExit("published_at列が必要です。サンプル生成から確認してください。")
dates = pd.to_datetime(df["published_at"], utc=True, errors="coerce", format="mixed")
counts = dates.dropna().dt.strftime("%Y-%m").value_counts().sort_index()
if counts.empty:
    raise SystemExit("集計できる公開日時がありません。取得範囲と入力を確認してください。")
table = counts.rename_axis("month").reset_index(name="video_count")
fig = px.bar(table, x="month", y="video_count", title="架空サンプル:月別公開本数(UTC)")
fig.update_xaxes(type="category")
fig.write_html(output, include_plotlyjs=True, full_html=True, config={"responsive": True})
print(output.resolve())

実用版:集計CSVと複数のグラフを保存する

まずデータセット記事の独立集計コードaggregate_youtube_csv.py として保存します。同じフォルダーへ以下を visualize_youtube_csv.py として保存してください。取得用のyoutube_dataset.pyやAPIキーは不要です。実用版は集計関数を呼ぶので、aggregate_youtube_csv.pyを先に実行する必要もありません。

"""Place aggregate_youtube_csv.py beside this file. No API access."""
import argparse
import hashlib
import html
import json
from pathlib import Path
import pandas as pd
import plotly.express as px
from aggregate_youtube_csv import aggregate

LABELS = {"other_video": "その他の動画", "live_archive": "配信アーカイブ候補",
          "shorts_candidate": "Shorts候補", "upcoming_or_live": "配信中・予定", "unknown": "不明"}


def visualize(source, output):
    # aggregate creates a new output directory, refusing accidental overwrite.
    df, tables, report = aggregate(source, output)
    if "title" not in df or "view_count" not in df:
        raise ValueError("グラフにはtitle・view_count列も必要です。データセットの出力を確認してください。")
    title_prefix = "架空サンプル — " if report["source_metadata"].get("is_fictional_sample") else "取得範囲内 — "
    charts = []
    monthly = tables["monthly_publications"]
    if not monthly.empty:
        fig = px.bar(monthly, x="month", y="video_count", title=title_prefix + "月別公開本数(UTC)",
                     labels={"month": "公開月", "video_count": "動画数"})
        fig.update_xaxes(type="category")
        charts.append(("monthly_publications", fig))
    types = tables["video_types"].copy()
    types["label"] = types["video_type"].map(LABELS).fillna(types["video_type"])
    if not types.empty:
        charts.append(("video_types", px.bar(types, x="label", y="video_count",
            title=title_prefix + "動画種別の構成(不明を含む)", labels={"label": "補助ラベル", "video_count": "動画数"})))
    # Empty count is missing, not zero; reject non-integer/negative counts.
    valid = df["view_count"].str.fullmatch(r"\d+")
    invalid = df["view_count"].ne("") & ~valid
    if invalid.any():
        raise ValueError("view_countは0以上の整数か空欄にしてください。")
    ranked = df.loc[valid].copy()
    ranked["view_count"] = ranked["view_count"].map(int)
    ranked = ranked.sort_values(["video_type", "view_count", "video_id"], ascending=[True, False, True])
    top = ranked.groupby("video_type", sort=True).head(10).copy()
    top[["video_id", "title", "video_type", "published_at", "view_count"]].to_csv(output / "top_videos.csv", index=False, encoding="utf-8-sig")
    for index, (kind, group) in enumerate(top.groupby("video_type", sort=True)):
        group = group.copy()
        group["display"] = [str(i + 1) + ": " + html.escape(str(t).replace("\n", " ")[:8]) for i, t in enumerate(group["title"])]
        group["published_label"] = group["published_utc"].dt.strftime("%Y-%m-%d").fillna("公開日不明")
        group["display"] = group["display"] + "<br>" + group["published_label"]
        fig = px.bar(group, x="view_count", y="display", orientation="h", text="view_count",
            title=title_prefix + LABELS.get(kind, kind) + "<br>累積再生数上位(公開日UTCを併記)",
            labels={"view_count": "累積再生数", "display": ""}, hover_data=["published_label", "video_id"])
        fig.update_traces(textposition="outside", cliponaxis=False)
        fig.update_xaxes(range=[0, max(group["view_count"].max(), 1) * 1.25])
        charts.append((f"top_{index}", fig))
    chart_data = {}
    for name, fig in charts:
        fig.update_layout(autosize=True, margin=dict(l=25, r=25, t=90, b=100), font=dict(size=12))
        fig.write_html(output / f"{name}.html", include_plotlyjs=True, full_html=True,
                       config={"responsive": True, "displaylogo": False}, div_id=name)
        chart_data[name] = json.loads(fig.to_json())
    (output / "chart_data.json").write_text(json.dumps(chart_data, ensure_ascii=False, indent=2), encoding="utf-8")
    report["charts"] = list(chart_data)
    report["view_count_missing_rows"] = int((~valid).sum())
    report["published_note"] = "月別は再生数でなく公開本数。取得外の月・欠損日は0件と断定しない。"
    report["artifact_hashes"] = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in output.iterdir() if p.is_file()}
    (output / "visualization.metadata.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"charts={len(charts)} / missing views={report['view_count_missing_rows']}")
    if not charts:
        print("対象データがないためHTMLは作りません。入力CSVと取得範囲を確認してください。")
    return chart_data


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", type=Path, default=Path("outputs/youtube/channel_video_analysis_dataset.csv"))
    parser.add_argument("--output", type=Path, default=Path("outputs/youtube/charts"))
    args = parser.parse_args()
    try:
        visualize(args.input, args.output)
    except (ValueError, OSError) as exc:
        raise SystemExit(str(exc)) from None

最初は架空サンプルを指定します。最小例とは別の実行で、保存済みCSVから読み直します。

実データで実行する場合はこちらを代わりに使います。入力を差し替えても分析内容をサイトへ送信しません。保存先フォルダーが存在すれば停止するので、別の出力フォルダーを指定して比較用の履歴を残します。

成果物と数値を確認する

サンプルの出力は outputs/youtube/sample/charts/ です。monthly_publications.csvは2026-01が1本、2026-02が2本、2026-03が2本。日付不明1行はここだけ除外します。video_types.csvとcontent_categories.csvは不明も含む計6本です。top_videos.csvには再生数が分かる5本が入り、0の動画も残ります。

monthly_publications.htmlとvideo_types.html、種別ごとのtop_0.htmlなどをブラウザで開いてください。topの番号と種別はHTMLの見出しで確認できます。上位は各種別最大10本で、公開日を併記します。CSV・グラフ設定・取得範囲・欠損数・集計実行時刻はmetadataとchart_data.jsonでも確認できます。

HTMLにはPlotly本体を含めているため、1ファイルが数MBになります。単体で開ける代わりに、複数HTMLでは同じライブラリも重複して保存されます。標準設定を外部CDNへ変更した場合はオフラインで開けなくなる可能性があるため、この例では単体形式を使います。

掲載コードから生成したグラフ見本(架空データ)

以下は上のコードで作った固定の見本です。日付不明1行を除いた5本の月別公開本数であり、月間再生数ではありません。

グラフから分かること・分からないこと

月別グラフは取得範囲に含まれる動画の公開本数です。1ページ分しか取得していない場合はその範囲の内訳であり、チャンネル全体ではありません。描かれていない月があることを実際の投稿0件と解釈しないでください。日付欠損の動画も種別の構成には残ります。

shorts_candidateはShorts候補、live_archiveもルールによる補助区分です。確定したShortsや公式の種別比率として提示しません。タイトルや長さを使う既存の分類には誤りがあり得ます。

再生数上位は取得時点の累積値です。公開から長い動画ほど蓄積する時間があり、動画種別・長さ・露出などの条件も異なります。視聴者数、月間再生数、最近の伸び率、人気の公平な順位とは言えません。公開日と取得時点を合わせて確認してください。APIの再生数定義変更をまたぐ比較の制約はデータセット記事の指標説明も参照してください。

0件・欠損・エラーのとき

CSVがない場合は作業フォルダーと入力パス、必要列がない場合は取得記事の出力形式を確認します。0件入力では見出しだけの集計CSVを保存し、グラフは作りません。日付が全件不明でも種別の構成は作れます。再生数が全件欠損なら上位グラフだけ省略します。metadataのhash不一致は別の取得回のファイルを組み合わせた可能性があるため、同じ取得回の組を選んでください。途中で保存エラーになった出力を完成版として扱わず、新しい出力先でやり直します。

この手順の検証範囲

掲載コードを用いて合成データと合成API応答を検証しています。CSVの再読込・集計・HTML生成とグラフ内の数値確認は実行しました。YouTube APIからのライブ取得は今回実施していません。架空の結果で実チャンネルへのアクセス成功や検索流入の増加を示すものではありません。

関連記事・公式資料

動画一覧を取得する分析用データセットと集計コード → この可視化記事。ほかの取得方法はYouTube取得ガイドへ。HTMLの保存方式はPlotly公式のHTML出力を参照してください。

-YouTubeデータ取得