以下の内容はhttps://smooth-pudding.hatenablog.com/entry/2025/03/23/230136より取得しました。


Python を使ったホロスコープの描き方

お久しぶりです。半年ぶりの投稿ですね。
最近とある占い師の方と交流があって、簡単なホロスコープ(西洋占星術で用いる、星の配置等を端的に表示した図式)をプログラムで作成する機会がありました。ところが、意外とプログラマ目線でホロスコープの描き方を説明している資料が少なく、情報集めに最初苦労しました。
そこで今回は、プログラマ目線で、ごく基本的なホロスコープの作成の仕方を解説したいと思います。

想定読者とこの記事で目指すこと

この記事は以下の属性を持つ人を想定しています。

  • Python の基本的な書き方は一通り押さえている
  • ホロスコープという言葉を聞いたことがない、あるいは聞いたことがあっても何なのか知らない

最終的には「ネイタルチャート」と呼ばれる最もシンプルなホロスコープの作成を目指します。ネイタルチャートといっても色々バリエーションがありますが、ここでは以下の条件で作成します。

  • ハウスシステムはプラシーダ
  • アスペクトは表示しない
  • 細かい表示にこだわらず、シンプルなものにする

このホロスコープは情報が少なすぎて実用性は乏しいですが、ざっくり何をやればよいのかの理解の助けになれば嬉しいです。

実装全体は一番最後で紹介します。

最終目標

ホロスコープとはなんぞや

西洋占星術と呼ばれる占いの分野があります。これはその名の通り西洋諸国などで発達した占いの手法で、様々な天体の位置を基に行われます。ホロスコープはこの位置の情報をシンプルに図式化したものです。西洋占星術では、ホロスコープを手がかりにして、様々な占いが展開されます。

ホロスコープには様々な種類があり、書き込まれる情報も多岐に渡ります。それらの中でも特に基本的な情報は、以下の2つです。

  • 太陽・月・太陽系の惑星それぞれの地球から見た位置(黄道上の角度)
  • 角度全体を12個の部屋に分けたもの(ハウス)

例えば、その人の性質に関わる占いをしたい場合、その人の出生の情報(生年月日・生まれた時刻・生まれた場所)から、上記の情報(といくつかの追加情報)をまとめた「ネイタルチャート」が作成されます。

「西洋占星術 ホロスコープ」と画像検索するといくつもヒットすると思うので、なんとなくイメージは掴めると思います。また、ホロスコープの詳しい意味が知りたい方は、数多くの占い関係の方が易しい説明をしているので、検索したり書籍を読んだりしてみてください。とりあえずこの記事の範囲では「ふーん、そういう世界もあるんだ」ぐらいの気持ちでOKです。

天文計算ライブラリ pyswisseph の関数たち

ホロスコープの描画のためには各天体の位置やハウスの位置を計算する必要がありますが、これを自分で実装するのは現実的ではありません。幸い、オープンソースのライブラリ pyswisseph があるので、これを使うことにします*1

具体的には以下の関数を用います。

  • utc_to_jd: 世界協定時(UTC)からユリウス暦に変換する関数
  • calc_ut: 天体の位置を計算する関数
  • houses: ハウスの開始位置(カスプ)を計算する関数

utc_to_jd(暦の変換)

天文計算ではユリウス暦と呼ばれる float 型の暦を使います。この関数は世界協定時 (UTC) からユリウス暦に変換してくれます。例えば、現在のユリウス暦での時刻(ユリウス時)を表示するには以下のように実装します。

from datetime import datetime, timezone
import swisseph as swe

now = datetime.now()
now_utc = now.astimezone(timezone.utc)
_, jd_utc = swe.utc_to_jd(
    now_utc.year, now_utc.month, now_utc.day,
    now_utc.hour, now_utc.minute, now_utc.second,
)
print(jd_utc)

calc_ut(天体の位置の計算)

ユリウス時と天体の種類を与えると、地球から見た天体の位置を計算してくれます。

import swisseph as swe

jd_utc: float = ...
(angle, *_), _ = swe.calc_ut(jd_utc, swe.SUN)
print(angle)

戻り値の最初の要素は6つの float のタプルになっていて、公式ドキュメントによるとそれぞれ以下を表しているようです*2

  • longitude:黄経
  • latitude:黄緯
  • distance:地球からの距離
  • speed in longitude:黄経方向の速度
  • speed in latitude:黄緯方向の速度
  • speed in distance:距離方向の速度

ホロスコープ上で表現されるのは黄経のみであり、ほかは使いません。黄経は0以上360未満の float で表現されます。

houses(ハウスの計算)

houses は各ハウスの開始の角度、いわゆるカスプを計算します。

import swisseph as swe

birth_jd: float = ...

cusps, _ = swe.houses(
    birth_jd,  # 生年月日・時刻のユリウス時
    35.681236,  # 北緯
    139.767125,  # 東経
    b"P",  # ハウスシステム(プラシーダス)
)
print(cusps)

cusps は12個の float からなるタプルで、それぞれが0以上360未満の値になっています。ハウスシステムは部屋割りの仕方の種類を指します。分からない方は「なんかプラシーダスという名前のやり方をするんだな」と思ってください。

プログラムの概要

プログラムは以下の流れになるように作成します。

  1. 対象者の生年月日・時刻・場所を保持するオブジェクトを作成する(BirthInfo クラス)
  2. 上記を元に、チャート描画のためのデータを保持したオブジェクトを作成する(NatalChart クラス)
  3. 上記のデータを描画する

以下のライブラリを使うので、必要に応じて pip install 等で導入しておいてください。

  • pyswisseph: 天文計算用のライブラリ
  • matplotlib: 描画用のライブラリ

いずれも pip install (ライブラリ名) でインストールできます。

BirthInfo クラスの実装

以下の情報を持てば OK です。

  • 対象者の生年月日
  • 対象者の生まれた時刻
  • 対象者の生まれた場所(緯度と経度)

これは dataclass を使って定義できます。日付と時刻は合わせて datetime 型にしておきます。

import dataclasses
from datetime import datetime

@dataclasses.dataclass
class BirthInfo:
    datetime: datetime
    latitude: float
    longitude: float

NatalChart クラスの実装

以下の情報を持てば OK です。

  • 対象者の BirthInfo
  • 各天体の角度
  • 各カスプの角度

天体を表現するために、各天体を表す列挙体を定義しておきます。

from enum import Enum
import swisseph as swe

class Planet(Enum):
    SUN = swe.SUN
    MOON = swe.MOON
    MERCURY = swe.MERCURY
    VENUS = swe.VENUS
    MARS = swe.MARS
    JUPITER = swe.JUPITER
    SATURN = swe.SATURN
    URANUS = swe.URANUS
    NEPTUNE = swe.NEPTUNE
    PLUTO = swe.PLUTO

    @property
    def swe_id(self) -> int:
        return self.value

これを使って、NatalChart クラスを dataclass として定義しておきます。実際には BirthInfo から作成できるようにしたいので、BirthInfo のみを受け取る create メソッドを足しておきます。

@dataclasses.dataclass
class NatalChart:
    birth_info: BirthInfo
    planets: dict[Planet, float]
    cusps: tuple[float, ...]

    @classmethod
    def create(cls, birth_info: BirthInfo) -> "NatalChart":
        ...

NatalChart クラスに、pyswisseph の関数たちの軽いラッパーを実装しておきます。

@staticmethod
def _datetime_to_jd(dt: datetime) -> float:
    dt_utc = dt.astimezone(timezone.utc)
    _, jd = swe.utc_to_jd(
        dt_utc.year, dt_utc.month, dt_utc.day, 
        dt_utc.hour, dt_utc.minute, dt_utc.second,
    )
    return jd

@staticmethod
def _calc_planet_pos(jd: float, planet: Planet) -> float:
    (pos, *_), _ = swe.calc_ut(jd, planet.swe_id)
    return pos

@staticmethod
def _calc_cusps(jd: float, latitude: float, longitude: float) -> tuple[float, ...]:
    cusps, _ = swe.houses(jd, latitude, longitude)
    return cusps

これらを用いれば、 create メソッドが以下のように実装できます。

@classmethod
def create(cls, birth_info: BirthInfo) -> "NatalChart":
    birth_jd = cls._datetime_to_jd(birth_info.datetime)
    cusps = cls._calc_cusps(birth_jd, birth_info.latitude, birth_info.longitude)
    planets = {
        planet: cls._calc_planet_pos(birth_jd, planet)
        for planet in Planet
    }
    return cls(birth_info, planets, cusps)

星座を使った角度の表記

ホロスコープでは、0°〜360°という表記をする代わりに、「牡牛座13°」というように星座+角度の表記を用います。12種類の星座(いわゆる黄道十二星座)で360°を等分して、所属する星座 + スタートからの角度というふうに表記します。

各星座に対応する黄経の範囲は以下のとおりです。例えば牡牛座13°は黄経で43°に対応します。

星座 黄経の範囲
牡羊座 0°〜30°
牡牛座 30°〜60°
双子座 60°〜90°
蟹座 90°〜120°
獅子座 120°〜150°
乙女座 150°〜180°
天秤座 180°〜210°
蠍座 210°〜240°
射手座 240°〜270°
山羊座 270°〜300°
水瓶座 300°〜330°
魚座 330°〜360°

これに対応して、以下のような列挙体を定義しておきます。

class Sign(Enum):
    ARIES = 0
    TAURUS = 30
    GEMINI = 60
    CANCER = 90
    LEO = 120
    VIRGO = 150
    LIBRA = 180
    SCORPIO = 210
    SAGITTARIUS = 240
    CAPRICORN = 270
    AQUARIUS = 300
    PISCES = 330

    @property
    def start_angle(self) -> float:
        return self.value

描画の実装

データが揃ったので、ホロスコープを描画していきましょう。ホロスコープに用いる記号を Planet に追加しておきます。

@property
def symbol(self) -> str:
    return {
        Planet.SUN: "☉",
        Planet.MOON: "☾",
        Planet.MERCURY: "☿",
        Planet.VENUS: "♀",
        Planet.MARS: "♂",
        Planet.JUPITER: "♃",
        Planet.SATURN: "♄",
        Planet.URANUS: "♅",
        Planet.NEPTUNE: "♆",
        Planet.PLUTO: "♇",
    }[self]

同様に Sign にも定義しておきます。

@property
def symbol(self) -> str:
    return {
        Sign.ARIES: "♈",
        Sign.TAURUS: "♉",
        Sign.GEMINI: "♊",
        Sign.CANCER: "♋",
        Sign.LEO: "♌",
        Sign.VIRGO: "♍",
        Sign.LIBRA: "♎",
        Sign.SCORPIO: "♏",
        Sign.SAGITTARIUS: "♐",
        Sign.CAPRICORN: "♑",
        Sign.AQUARIUS: "♒",
        Sign.PISCES: "♓",
    }[self]

記号が揃ったので、matplotlib を使って実際に配置していきます。matplotlib の極座標でプロットする機能を使っていきます。以下に注意してください。

  • cusps[0] の位置が左(西側)になるように調整する
  • 反時計回りに角度が進むようにする(通常の極座標の向きでOK)

以下はシンプルな形で実装した例です。

def plot(self) -> plt.Figure:
    import math
    
    # 極座標プロット用の設定
    fig = plt.figure(figsize=(10, 10))
    ax = fig.add_subplot(111, projection='polar')
    
    # グラフの基本設定
    ax.set_theta_zero_location('W')  # 0度を西に設定
    ax.grid(False)                   # グリッドを非表示
    ax.set_xticklabels([])           # 角度目盛りを非表示
    ax.set_rticks([])                # 半径目盛りを非表示
    ax.set_rlim(0, 1.1)                # 半径の範囲を設定
    ax.spines['polar'].set_visible(False)  # 外側の円を非表示
    plt.title('Natal Chart')

    # 半径たち
    radii = {
        "outer": 1.0,
        "sign": 0.9,
        "inner": 0.8,
        "planet": 0.5,
    }

    # 円の描画
    num_segments = 360
    angles = [math.radians(360 * i / num_segments) for i in range(num_segments + 1)]
    ax.plot(angles, [radii["outer"]] * (num_segments + 1), 'k-', linewidth=2.0)
    ax.plot(angles, [radii["inner"]] * (num_segments + 1), 'k-', linewidth=1.0)

    # 第1ハウスの位置を基準とする
    zero_angle = math.radians(self.cusps[0])

    # サインの描画
    for sign in Sign:
        start_angle = math.radians(sign.start_angle) - zero_angle
        mid_angle = math.radians(sign.start_angle + 15) - zero_angle
        ax.plot([start_angle, start_angle], [radii["inner"], radii["outer"]], 'k-', linewidth=0.5)
        ax.text(mid_angle, radii["sign"], sign.symbol, ha='center', va='center', fontsize=20)

    # ハウスカスプの描画
    for cusp in self.cusps:
        angle_rad = math.radians(cusp) - zero_angle
        ax.plot([angle_rad, angle_rad], [0, radii["inner"]], 'k-', linewidth=0.5)
    
    # 天体の描画
    for planet, angle in self.planets.items():
        angle_rad = math.radians(angle) - zero_angle
        ax.text(angle_rad, radii["planet"], planet.symbol, ha='center', va='center', fontsize=20)

    return fig

例えば以下のように実装した main を呼べば、ホロスコープが表示されます。

def main():    
    birth_info = BirthInfo(
        datetime(2001, 1, 1, 12, 0, 0),
        35.681235,
        139.767125,
    )
    chart = NatalChart.create(birth_info)
    fig = chart.plot()
    plt.show()
    # 保存したい場合
    # fig.savefig("natal_chart.png")
描画例

機能を拡張するには

以上で実装が完了しました。実用的なホロスコープを作成したい人のために、いくつかヒントを残しておきます。

アスペクトを描画したい

アスペクト (天体同士が特定の角度の位置にある関係) の線を引きたい場合は、「2つの角度の差は何度か」を計算する関数を作ればOKです。ただすこし注意が必要で、「5°と15°」の関係と「355°と5°」の関係が同じになるようにする必要があります。例えば以下のように実装すれば、360°をまたいでいるかどうかによらず、正しく角度の差を求めることができます。

def calc_angle_diff(angle1: float, angle2: float) -> float:
    return ((angle1 - angle2) + 180) % 360 - 180

これは差が -180°〜180° の範囲に収まるように調整するような処理になっています。これをうまく活用すれば、アスペクトも問題なく表現できるはずです。

トランジットチャートを描画したい

基本的には NatalChart を2つ作ってうまく組み合わせれば作成できるはずです。向きを決める zero_angle を定めるときに、内側に対応する NatalChart のものを用いれば、後はネイタルチャートを書くときとほぼ同じノリで作れると思います。

実装全体

最後に実装全体をまとめておきます。

import dataclasses
from datetime import datetime, timezone
from enum import Enum

import matplotlib.pyplot as plt
import swisseph as swe


@dataclasses.dataclass
class BirthInfo:
    datetime: datetime
    latitude: float
    longitude: float


class Planet(Enum):
    SUN = swe.SUN
    MOON = swe.MOON
    MERCURY = swe.MERCURY
    VENUS = swe.VENUS
    MARS = swe.MARS
    JUPITER = swe.JUPITER
    SATURN = swe.SATURN
    URANUS = swe.URANUS
    NEPTUNE = swe.NEPTUNE
    PLUTO = swe.PLUTO

    @property
    def swe_id(self) -> int:
        return self.value

    @property
    def symbol(self) -> str:
        return {
            Planet.SUN: "☉",
            Planet.MOON: "☾",
            Planet.MERCURY: "☿",
            Planet.VENUS: "♀",
            Planet.MARS: "♂",
            Planet.JUPITER: "♃",
            Planet.SATURN: "♄",
            Planet.URANUS: "♅",
            Planet.NEPTUNE: "♆",
            Planet.PLUTO: "♇",
        }[self]

class Sign(Enum):
    ARIES = 0
    TAURUS = 30
    GEMINI = 60
    CANCER = 90
    LEO = 120
    VIRGO = 150
    LIBRA = 180
    SCORPIO = 210
    SAGITTARIUS = 240
    CAPRICORN = 270
    AQUARIUS = 300
    PISCES = 330

    @property
    def symbol(self) -> str:
        return {
            Sign.ARIES: "♈",
            Sign.TAURUS: "♉",
            Sign.GEMINI: "♊",
            Sign.CANCER: "♋",
            Sign.LEO: "♌",
            Sign.VIRGO: "♍",
            Sign.LIBRA: "♎",
            Sign.SCORPIO: "♏",
            Sign.SAGITTARIUS: "♐",
            Sign.CAPRICORN: "♑",
            Sign.AQUARIUS: "♒",
            Sign.PISCES: "♓",
        }[self]
    
    @property
    def start_angle(self) -> float:
        return self.value


@dataclasses.dataclass
class NatalChart:
    birth_info: BirthInfo
    planets: dict[Planet, float]  # 各惑星の角度 (0-360)
    cusps: tuple[float, ...]  # ハウスカスプの角度 (0-360)

    @classmethod
    def create(cls, birth_info: BirthInfo) -> "NatalChart":
        birth_jd = cls._datetime_to_jd(birth_info.datetime)
        cusps = cls._calc_cusps(birth_jd, birth_info.latitude, birth_info.longitude)
        planets = {
            planet: cls._calc_planet_pos(birth_jd, planet)
            for planet in Planet
        }
        return cls(birth_info, planets, cusps)

    @staticmethod
    def _datetime_to_jd(dt: datetime) -> float:
        dt_utc = dt.astimezone(timezone.utc)
        _, jd = swe.utc_to_jd(
            dt_utc.year, dt_utc.month, dt_utc.day,
            dt_utc.hour, dt_utc.minute, dt_utc.second,
        )
        return jd

    @staticmethod
    def _calc_planet_pos(jd: float, planet: Planet) -> float:
        (pos, *_), _ = swe.calc_ut(jd, planet.swe_id)
        return pos

    @staticmethod
    def _calc_cusps(jd: float, latitude: float, longitude: float) -> tuple[float, ...]:
        cusps, _ = swe.houses(jd, latitude, longitude)
        return cusps
    
    def plot(self) -> plt.Figure:
        import math
        
        # 極座標プロット用の設定
        fig = plt.figure(figsize=(10, 10))
        ax = fig.add_subplot(111, projection='polar')
        
        # グラフの基本設定
        ax.set_theta_zero_location('W')  # 0度を西に設定
        ax.grid(False)                   # グリッドを非表示
        ax.set_xticklabels([])           # 角度目盛りを非表示
        ax.set_rticks([])                # 半径目盛りを非表示
        ax.set_rlim(0, 1.1)                # 半径の範囲を設定
        ax.spines['polar'].set_visible(False)  # 外側の円を非表示
        plt.title('Natal Chart')

        # 半径たち
        radii = {
            "outer": 1.0,
            "sign": 0.9,
            "inner": 0.8,
            "planet": 0.5,
        }

        # 円の描画
        num_segments = 360
        angles = [math.radians(360 * i / num_segments) for i in range(num_segments + 1)]
        ax.plot(angles, [radii["outer"]] * (num_segments + 1), 'k-', linewidth=2.0)
        ax.plot(angles, [radii["inner"]] * (num_segments + 1), 'k-', linewidth=1.0)

        # 第1ハウスの位置を基準とする
        zero_angle = math.radians(self.cusps[0])

        # サインの描画
        for sign in Sign:
            start_angle = math.radians(sign.start_angle) - zero_angle
            mid_angle = math.radians(sign.start_angle + 15) - zero_angle
            ax.plot([start_angle, start_angle], [radii["inner"], radii["outer"]], 'k-', linewidth=0.5)
            ax.text(mid_angle, radii["sign"], sign.symbol, ha='center', va='center', fontsize=20)

        # ハウスカスプの描画
        for cusp in self.cusps:
            angle_rad = math.radians(cusp) - zero_angle
            ax.plot([angle_rad, angle_rad], [0, radii["inner"]], 'k-', linewidth=0.5)
        
        # 天体の描画
        for planet, angle in self.planets.items():
            angle_rad = math.radians(angle) - zero_angle
            ax.text(angle_rad, radii["planet"], planet.symbol, ha='center', va='center', fontsize=20)

        return fig


def main():    
    birth_info = BirthInfo(
        datetime(2001, 1, 1, 12, 0, 0),
        35.681235,
        139.767125,
    )
    chart = NatalChart.create(birth_info)
    fig = chart.plot()
    plt.show()
    # 保存したい場合
    # fig.savefig("natal_chart.png")

if __name__ == "__main__":
    main()

*1:pyswisseph は無償利用の場合は GPL ライセンスであり、ソースコードの公開が必要です。詳しくは Swiss Ephemeris の公式の説明を確認してください。

*2:黄経・黄緯は黄道座標と呼ばれるものです。




以上の内容はhttps://smooth-pudding.hatenablog.com/entry/2025/03/23/230136より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14