乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

python

今回は地震データの可視化についてです
最近地震が多いので、plotlyで可視化してみました。


解説動画はこちら








データの入手先


気象庁の地震データベース

コードを動かしてみたい方は
CSVがダウンロードできるようなので
手元にダウンロードしてみてください。



データの読み込み

こちらのコードはGoogle Colabで動くようになっています。
動かしたい場合は Colabの画面左メニューから
フォルダマークをクリックすると
ファイル置き場が見えるので、そこにCSVファイルを
ドラッグなどで配置します。

ファイルを読み込みするには下記のコードです。

# 必要なライブラリのインポート
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import numpy as np
import warnings
warnings.filterwarnings('ignore')

file_path = "/content/地震リスト.csv"
df = pd.read_csv(file_path)
df.head()

df変数にデータが読み込まれると思います。



データの整形


CSVそのままのデータでは
うまく可視化が行えないため
可視化用にデータを加工する必要があります。

下記のコードを実行すると
可視化用のカラムなどが追加されます。


# データの前処理
s_order = ['震度1', '震度2', '震度3', '震度4', '震度5弱', '震度5強', '震度6弱', '震度6強', '震度7']
target = "トカラ列島近海"

def preprocess_data(df):
    # 緯度・経度の変換(度分秒から度へ)
    def convert_coordinate(coord_str):
        # 例: "29°28.2′N" -> 29.47
        if '°' in coord_str and '′' in coord_str:
            parts = coord_str.replace('N', '').replace('E', '').replace('S', '').replace('W', '')
            degree_part = parts.split('°')[0]
            minute_part = parts.split('°')[1].replace('′', '')
            return float(degree_part) + float(minute_part) / 60
        return float(coord_str)
    
    df['緯度_数値'] = df['緯度'].apply(convert_coordinate)
    df['経度_数値'] = df['経度'].apply(convert_coordinate)
    
    # 深さの数値化
    df['深さ_数値'] = df['深さ'].str.replace(' km', '').astype(float)
    
    # 震度の数値化
    s_mapping = {
        '震度1': 1, '震度2': 2, '震度3': 3, '震度4': 4, '震度5弱': 5,
        '震度5強': 5.5, '震度6弱': 6, '震度6強': 6.5, '震度7': 7
    }
    df['最大震度_数値'] = df['最大震度'].map(s_mapping)
    df['最大震度'] = pd.Categorical(df['最大震度'], categories=s_order, ordered=True)
    return df

# データの前処理を実行
df = preprocess_data(df)
df = df[df["震央地名"]==target]
df.head()



時系列の可視化


最初は時系列で
どれだけ地震が発生しているのかを
見てみましょう

日別、震度別で時系列で地震の回数を
表示してみます。

# 時系列分析(日付別)
df['日付'] = pd.to_datetime(df['地震の発生日'])
daily_shindo_counts = df.groupby(['日付', '最大震度']).size().reset_index(name='地震回数')

# 震度の順序を維持
daily_shindo_counts['最大震度'] = pd.Categorical(
    daily_shindo_counts['最大震度'], 
    categories=s_order, 
    ordered=True
)

# 震度別横並び棒グラフ
fig_time = px.bar(
    daily_shindo_counts,
    x='日付',
    y='地震回数',
    color='最大震度',
    title='日付別地震発生回数(震度別)',
    labels={'地震回数': '地震発生回数', '日付': '発生日'},
    category_orders={'最大震度': s_order},
    barmode='group'  # 横並びに表示
)

# レイアウトの調整
fig_time.update_layout(
    xaxis_tickangle=-45,
    height=600,
    width=1000,
    legend=dict(
        orientation="v",
        yanchor="top",
        y=1,
        xanchor="left",
        x=1.01
    )
)

fig_time.show()
スクリーンショット 2025-07-05 17.27.01

データは直近の1週間分しか無いようです。
7月前後で急増しているのが分かります。



箱ひげ図の可視化


次は箱ひげ図で
「トカラ列島近海」の詳細を見てみましょう。


震度ごとにマグニチュードをまとめると
このような感じになります。

# 震央地名 最大震度別での箱ひげ図

fig_box = px.box(df, 
                 x='震央地名', 
                 y='M',
                 color='最大震度',
                 title='震央地名別 マグニチュード分布(最大震度別)',
                 labels={'M': 'マグニチュード', '震央地名': '震央地名'},
                 category_orders={'最大震度': s_order})

fig_box.update_layout(
    xaxis_tickangle=-45,
    height=600,
    width=1000
)
fig_box.show()
スクリーンショット 2025-07-05 17.29.52
同じ震度でも、マグニチュードにはバラツキがありますが
マグニチュードが上がるにつれ、震度も大きくなっています。



地図表示

今後は地図で震度を表示してみましょう
OpenStreetMapを使用して、緯度軽度を用いて
地図に震度を表示させてみます。

# マップ表示用のデータ準備
df_map = df.copy()
df_map['緯度_数値'] = pd.to_numeric(df_map['緯度_数値'], errors='coerce')
df_map['経度_数値'] = pd.to_numeric(df_map['経度_数値'], errors='coerce')
df_map['M'] = pd.to_numeric(df_map['M'], errors='coerce')
df_map = df_map.dropna(subset=['緯度_数値', '経度_数値', 'M'])
df_map['最大震度'] = pd.Categorical(df_map['最大震度'], categories=s_order, ordered=True)

# 緯度経度と最大震度・Mを用いたmap表示(OpenStreetMap使用)
fig_map2 = go.Figure()

# 震度別に色分けしてプロット
震度_colors = {
    '震度1': '#90EE90',  # 薄緑
    '震度2': '#FFD700',  # 金色
    '震度3': '#FFA500',  # オレンジ
    '震度4': '#FF6347',  # トマト色
    '震度5弱': '#FF4500',  # 赤オレンジ
    '震度5強': '#FF0000',  # 赤
    '震度6弱': '#8B0000',  # 濃い赤
    '震度6強': '#4B0082',  # インディゴ
    '震度7': '#000000'   # 黒
}

for 震度 in s_order:
    if 震度 in df_map['最大震度'].values:
        subset = df_map[df_map['最大震度'] == 震度]
        fig_map2.add_trace(go.Scattermapbox(
            lat=subset['緯度_数値'],
            lon=subset['経度_数値'],
            mode='markers',
            marker=dict(
                size=subset['最大震度_数値'] ** 2 ,  # サイズ調整
                color=震度_colors.get(震度, '#000000'),
                sizemode='diameter'
            ),
            text=subset['震央地名'],
            hovertemplate='%{text}
' + '緯度: %{lat:.2f}
' + '経度: %{lon:.2f}
' + 'マグニチュード: %{customdata[0]}
' + '最大震度: %{customdata[1]}
' + '', customdata=list(zip(subset['M'], subset['最大震度'])), name=震度 )) fig_map2.update_layout( title='地震発生位置(最大震度・マグニチュード)- OpenStreetMap', mapbox=dict( style="open-street-map", center=dict(lat=df_map['緯度_数値'].mean(), lon=df_map['経度_数値'].mean()), zoom=8 ), height=700, width=1000 ) fig_map2.show()
スクリーンショット 2025-07-05 17.34.15

今回の地震は、かなり局所的に起きていることが分かります。
特に震度5以上が発生した箇所がかなり近く
この近辺は注意が必要な地域に見えます。


散布図表示


最後に
マグニチュード、震度、深さを
散布図で見てみましょう

# 深さとマグニチュードの関係
fig_scatter = px.scatter(
    df,
    x='深さ_数値',
    y='M',
    color='最大震度',
    size='最大震度_数値',
    hover_name='震央地名',
    hover_data={
        '地震の発生日': True,
        '地震の発生時刻': True
    },
    title='深さとマグニチュードの関係(最大震度別)',
    labels={'深さ_数値': '深さ (km)', 'M': 'マグニチュード'},
    category_orders={'最大震度': s_order}
)

fig_scatter.show()
スクリーンショット 2025-07-05 17.36.27


どの深さでも、満遍なく起きているように見えますね
地震自体は、いつ何時、どこでも起きてしまいます。



まとめ

緯度経度が記載されているデータは
plotlyなどを用いれば地図表示が比較的
簡単に行うことができます。

可視化の表現の幅が広がるので
覚えておくと良いかもしれませんね


今日はここまでです
それでは




今回は金融庁の
NISA積立シミュレーターがやばいので
その解説です。


解説動画はこちら


 

金融庁の積立シミュレーター

これがそのシミュレーターです。

サイト

ここで
毎月の積立1万円
年利12%
40年間

で試算してみます。
スクリーンショット 2025-06-14 17.12.47


こんな感じになりました。

年利が高すぎますが
計算がわかりやすいようにして
計算してみた結果です。

40年後の資産は
11765万円になりました



Pythonでのシミュレーションコード


先程のシミュレーターと同じ計算を
Pythonでも計算してみましょう。

シミュレーションコード

monthly_rate = 0.12 / 12       # 月利(年利12%)
months = 40 * 12               # 総月数(480ヶ月)
monthly_payment = 10000       # 毎月の積立額(マイナス表記にする)

total = 0
for _ in range(months):
    total = total * (1 + monthly_rate) + monthly_payment

print(f"将来の資産総額: {total:,.0f} 円")
将来の資産総額: 117,647,725 円


金融庁のシミュレーターの結果は
11765万円 なので
ほぼ同じ結果になりました。


落とし穴

実はここに落とし穴があります!!!!

年利計算での正しいシミュレーション結果は
こうなります。
(年末に積立するパターン)

year_rate = 0.12            # 年利12%
years = 40                  # 総年数(40年)
year_payment = 120000    # 年の積立額

total = 0
for _ in range(years):
    total = total * (1 + year_rate) + year_payment

print(f"将来の資産総額: {total:,.0f} 円")
将来の資産総額: 92,050,970 円

先程の計算結果 : 117,647,725 円
とは大きく乖離してきます。


からくり

金融庁のサイトのシミュレーションは年利ではなく
年利を12ヶ月で割った月利の複利(年利 / 12)で計算しているようです。

そのため年利と月利では、計算結果が大きく異なってきます。

もし月利複利計算を正しくするとしたら
12乗して年利になるような値を月利にしないといけません。

月利 = (1 + 0.12) ** (1/12) - 1
= 0.009488792934583046 = 0.948%

年利から月利に直して計算するコードはこちら
monthly_rate = (1 + 0.12) ** (1/12) - 1  # 月利(年利12%)
months = 40 * 12                          # 総月数(480ヶ月)
monthly_payment = 10000                 # 毎月の積立額(マイナス表記にする)

total = 0
for _ in range(months):
    total = total * (1 + monthly_rate) + monthly_payment

print(f"将来の資産総額: {total:,.0f} 円")
将来の資産総額: 97,010,200 円

月利計算だとこのくらいの金額になります。




資産推移のシミュレーター

年利を月利換算しても正しく計算するようにしたものは
こんな感じになります。
import pandas as pd
import plotly.express as px

def plot_investment(rate_percent, years, payment_man):
    annual_rate = rate_percent / 100
    monthly_rate = (1 + annual_rate) ** (1/12) - 1
    months = years * 12
    monthly_payment = payment_man * 10000

    results, ci_total, am_total = [], 0, 0
    for month in range(1, months + 1):
        ci_total = ci_total * (1 + monthly_rate) + monthly_payment
        am_total += monthly_payment
        results.append({'月': month, '資産総額': ci_total, '積立金額': am_total})

    df = pd.DataFrame(results)
    fig = px.line(df, x='月', y=['資産総額', '積立金額'],
                  labels={'value': '金額(円)', 'variable': '項目'},
                  title=f'毎月{payment_man}万円積立(年利{rate_percent:.1f}%・{years}年)の資産推移')
    
    fig.update_layout(
        yaxis_tickformat=',',
        height=400
    )
    fig.show()

積立シミュレーション設定

#@title 積立シミュレーション設定
rate_percent = 5.1 # @param {type:"slider", min:1.0, max:20.0, step:0.1}
years = 30 # @param {type:"slider", min:10, max:50, step:1}
payment_man = 20 # @param {type:"slider", min:1, max:30, step:1}

plot_investment(rate_percent, years, payment_man)

colabで実行するとこのような画面が出てきます
スクリーンショット 2025-06-14 17.34.43

これを設定して実行すると
スクリーンショット 2025-06-14 17.34.53
積立のシミュレーション結果が反映されます。

colabで試せるので
コピペして試してみてください。



まとめ

金融庁のサイトのシミュレーターは
年利と月利を齟齬しているので
数十年後の金額が大きく乖離してくるようです。

単利と複利、月利と年利
この辺りの関係を正しく計算できるように
日々プログラムでシミュレーションすると安全です。

資産形成に取り組みたい方は
この機会にプログラミングも併せて
学んでみてはいかがでしょうか?


それでは。


今回は画像に隠しメッセージを仕込める技術
ステガノグラフィーのご紹介です。


解説動画はこちら



ステガノグラフィーとは


ステガノグラフィー(Steganography)
画像、音声、動画などのデジタルデータに
別の情報を隠す技術のことです

これにより、情報を目に見えない形で
秘匿することができます。

早速情報を仕込んだ画像を作成してみましょう



ステガノグラフィーの仕組み

画像への埋め込み

最下位ビット(LSB)を利用して
画像の色をわずかに変更しメッセージを隠します。

最下位ビット(Least Significant Bit):
二進数の右端に位置する値

バイナリ変換:
メッセージをバイナリ形式に変換し終了ビットを追加

ピクセルの変更:
各ピクセルのRGB成分の最下位ビットを変更して
メッセージを埋め込み



ビット演算

変更対象のRGB値の各成分は0から255の範囲の整数
~1 は、ビット反転演算子

1 のビットを反転すると、全てのビットが反転し
...11111110 というビット列になる

RGB値 が 5の場合(バイナリで 00000101)
~1 は ...11111110 となり
5 & ~1 は 00000101 & 11111110 となり
結果は 00000100(つまり 4)になる

00000101 &
11111110



00000100


ピクセルの変更

各ピクセルのRGB成分から最下位ビットを取得し
バイナリメッセージの値に変更します


メッセージの復元

バイナリメッセージを8ビットごとに分割し、文字に変換
終了ビット(11111111)が見つかるまでメッセージを復元する



音声ファイルや動画ファイルにも同様の手法が使われ
音声の波形や動画のフレームに情報を埋め込むことが可能です。


埋め込む画像


元画像はこんな感じです。
sample





画像にメッセージを埋め込むコードサンプル

from PIL import Image

def encode_image(image_path, secret_message, output_path):
    # 画像を開く
    image = Image.open(image_path)
    encoded_image = image.copy()

    # メッセージをバイナリに変換
    binary_message = ''.join(format(ord(char), '08b') for char in secret_message) + '11111111'  # 終了ビット
    data_index = 0

    # 画像のピクセルを走査
    for y in range(encoded_image.height):
        for x in range(encoded_image.width):
            pixel = list(encoded_image.getpixel((x, y)))
            for i in range(3):  # RGBの各成分
                if data_index < len(binary_message):
                    pixel[i] = (pixel[i] & ~1) | int(binary_message[data_index])  # 最下位ビットを変更
                    data_index += 1
            encoded_image.putpixel((x, y), tuple(pixel))
            if data_index >= len(binary_message):
                break
        if data_index >= len(binary_message):
            break

    encoded_image.save(output_path)

# 使用例
input_img_path = "画像のパス"
input_message = "埋め込むメッセージ"
output_img_path = "出力後のパス"
encode_image(input_img_path, input_message, output_img_path)

実行するとメッセージを埋め込んだ画像が生成されます。

元の画像と比較すると

download


差分はほとんどわからないですね!!


画像からメッセージを取り出すコードサンプル

from PIL import Image

# バイナリメッセージを8ビットごとに分割して文字に変換
def make_message(binary_message):
    message = ''
    for i in range(0, len(binary_message), 8):
        byte = binary_message[i:i+8]
        if byte == '11111111':  # 終了ビットを検出
            break
        message += chr(int(byte, 2))
    return message

def decode_image(image_path):
    image = Image.open(image_path)
    binary_message = ''

    # 画像のピクセルを走査
    for y in range(image.height):
        for x in range(image.width):
            pixel = image.getpixel((x, y))
            for i in range(3):  # RGBの各成分
                binary_message += str(pixel[i] & 1)  # 最下位ビットを取得

    return make_message(binary_message)

# 使用例
decoded_message = decode_image('output_image.png')
print("Decoded message:", decoded_message)
Decoded message: Hello Otupy

メッセージが復元されました。




最後に

こちらの画像にはメッセージを埋め込んでいるので
ダウンロードして解読してみてくださいね

bbbb




今回はステガノグラフィーをご紹介しました
画像などに隠しメッセージを残せるので

暗号のように秘匿したいやりとりで
使うこともできたりして
なかなか面白い技術です。

いろいろ遊んでみてみてくださいね
それでは



今回は重力レンズエフェクトで
画像を歪ませで遊んでみました。


解説動画はこちら


 

重力レンズとは

光が天体の重力によって曲げられ
天体があたかもレンズとして働く効果のことです

光の曲がり具合はレンズとなる天体の質量が大きいほど強く
背後の銀河から来る光が強く曲げられて像が大きく歪んで見えます


今回はレンズをブラックホールで想定して
擬似的な効果の計算で画像を作成するコードのご紹介です。


基本の計算式

レンズ中心からの距離が小さくなるほど
(レンズに近づくほど)歪みが強くなるような計算です

元々の重力レンズの計算は少し難解かなと思いましたので
簡易な計算でエフェクトをかけます。

スクリーンショット 2024-11-23 17.08.32




処理の流れ

入力画像をNumPy配列に変換し、全ピクセルの座標を計算
重力レンズ効果を適用する領域を特定

レンズ中心からの距離に基づいて歪みを計算し、新しいピクセル座標を算出
新しい座標に基づいてピクセルを再配置し、ブラックホール領域を黒で塗りつぶし
最終的な画像として出力


ipywidgets でUIを作って操作できます。
スライダーの値を変えると、画像が変化します。

Google colabなどで試す事ができます。


コードはこちら
from PIL import Image, ImageDraw
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, IntSlider, FloatSlider

cached_image = None

# 重力レンズエフェクトをかけた画像を生成する
def apply_gravitational_lens(image, lens_radius, black_hole_radius, distortion_factor):
    
    # 画像をNumPy配列に変換
    input_pixels = np.array(image)
    width, height = image.size
    cx, cy = width // 2, height // 2
    
    # 座標グリッドの生成と中心からの相対座標,距離の計算
    y, x = np.meshgrid(np.arange(height), np.arange(width), indexing="ij")
    dx, dy = x - cx, y - cy
    sx, sy = x.copy(),y.copy()
    distance = np.sqrt(dx**2 + dy**2)

    # 歪みを適用 { distortion = 1+distortion_factor * ((lens_radius - distance[lens_mask]) / lens_radius)**2 }
    lens_mask = (black_hole_radius < distance) & (distance < lens_radius)
    distortion = 1 + distortion_factor * ((lens_radius - distance[lens_mask]) / lens_radius)**2
    sx[lens_mask] = cx + dx[lens_mask] * distortion
    sy[lens_mask] = cy + dy[lens_mask] * distortion

    # 座標のクリッピングと出力画像の生成
    sx = np.clip(sx, 0, width - 1).astype(int)
    sy = np.clip(sy, 0, height - 1).astype(int)
    output_pixels = input_pixels[sy, sx]

    # ブラックホール領域の適用
    black_hole_mask = distance <= black_hole_radius
    output_pixels[black_hole_mask] = [0, 0, 0]
    return Image.fromarray(output_pixels)

# インタラクティブUI部
def interactive_gravitational_lens(image_path):

    global cached_image
    if cached_image is None:
        cached_image = Image.open(image_path).convert("RGB") 

    def update(lens_radius, black_hole_radius, distortion_factor):
        output_image = apply_gravitational_lens(
            cached_image,
            lens_radius=lens_radius,
            black_hole_radius=black_hole_radius,
            distortion_factor=distortion_factor,
        )
        plt.figure(figsize=(8, 8))
        plt.imshow(output_image)
        plt.axis('off')
        plt.title(f"Lens Radius: {lens_radius:03}, Black Hole Radius: {black_hole_radius:03}, Distortion: {distortion_factor:.02}")
        plt.show()

    interact(
        update,
        lens_radius=IntSlider(min=50, max=990, step=10, value=50, description="Lens Radius"),
        black_hole_radius=IntSlider(min=10, max=200, step=5, value=10, description="Black Hole Radius"),
        distortion_factor=FloatSlider(min=0.1, max=10.0, step=0.1, value=0.1, description="Distortion Factor"),
    )

interactive_gravitational_lens("画像パス")

download-1

レンズの半径と
歪み具合を調整して
面白画像を作って遊んでみてください。

動画では色々な画像で試しているので
ぜひみてみてくださいね

今回はこれまでです
それでは。



今回はまじめに
最近のPythonの文法をまとめてみました。

解説動画はこちら


 
さて最近のPythonの文法について
しばらく触れていなかったので
少し解説していきたいと思います。

今回はGoogle Colabを使用しているので
そのバージョンである3.10までの内容です。




文字列のフォーマット指定

f"文字列{変数}"
で文字列にデータを差し込みすることができます。
# 昔のフォーマット
name = "フリーザ"
hourly = 530000
text = "{0} : 私の時給は{1}円です。".format(name, hourly)
print(text)

# Fフォーマット
name = "フリーザ"
hourly = 530000
text = f"{name} : 私の時給は{hourly}円です。"
print(text)
フリーザ : 私の時給は530000円です。

フォーマットの指定もできます

{変数:指定文字列}
{変数:,} : 3桁区切り
{変数:.2f} : 小数点以下の指定(この場合は2桁)

# Fフォーマット
name = "フリーザ"
name2 = "ザーボン"
hourly = 530000
hourly2 = 2980.2983
text = f"{name} : 私の時給は{hourly:,}円です。"
print(text)
text2 = f"{name2} : 私の時給は{hourly2:.3f}円です。"
print(text2)


defaultdict

存在しないキーのデフォルト値を指定できます

通常の辞書型では最初にキーが存在しないと値を操作できません
# キーを初期化代入する必要がある
d = {}
for key in "aabbbc":
    if key not in d:
        d[key] = 0
    d[key] += 1

print(d)

defaultdictの場合は初期値を決められるので
コードを省略できます。

# defaultdictの場合
from collections import defaultdict
d = defaultdict(int)

for key in "aabbbc":
    d[key] += 1

#print(d)
print(d["a"])
2



関数の結果をキャッシュ

functools.lru_cacheを用いると
同じ引数の関数の結果をキャッシュでき
再帰の計算などが早く行えるようになります。

# キャッシュ無し
def fibonacci_old(n):
    if n < 2:
        return n
    return fibonacci_old(n-1) + fibonacci_old(n-2)
print(fibonacci_old(30))
msレベル

# キャッシュあり
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci_cache(n):
    if n < 2:
        return n
    return fibonacci_cache(n-1) + fibonacci_cache(n-2)
print(fibonacci_cache(100))
usレベル

圧倒的に早くなります。


アンパック演算子

*を用いて変数への複数代入を行う

a, *b, c = [1,2,3,4,5,6]
print(a, b, c)

a, b, *c = [1,2,3,4,5,6]
print(a, b, c)
1 , [2,3,4,5] , 6
1 , 2 , [3,4,5,6]



ウォルラス演算子

:= で変数への代入と評価を同時にできるようになります

(a:=2) >= 2
True


WITH構文で複数ファイルのコンテキスト

with open(ファイル名) as 変数, open(ファイル名) as 変数 ... :

# ファイルを作っておく
import random
import string

for name in ["a","b","c"]:
  with open(name+".txt","w") as _w:
    text = ''.join([random.choice(string.ascii_lowercase) for i in range(10)])
    _w.write(text)

# with構文
with open("a.txt") as _a, open("b.txt") as _b, open("c.txt") as _c:
  print(_a.read())
  print(_b.read())
  print(_c.read())


match文

3.10でmatch文が導入された
他の言語でいうcase文ですね

match 条件:
  case パターン:
    return 戻り値
  case パターン:
    return 戻り値


# match文
def http_error(status):
    match status:
        case 400:
            return "ダメっすねー"
        case 404:
            return "見つからねっす"
        case 418:
            return "なんでしょね"
        case _:
            return "何言ってるか分かんねっす"

http_error(404)
見つからねっす


いつの間にか便利な機能が追加されていて
業務でも捗っている感じになっていました。

文法の知識は定期的にアップデートしていかないと
もったいないですね

今回は最近のPython文法についてでした
それでは。


このページのトップヘ