お久しぶりです。半年ぶりの投稿ですね。
最近とある占い師の方と交流があって、簡単なホロスコープ(西洋占星術で用いる、星の配置等を端的に表示した図式)をプログラムで作成する機会がありました。ところが、意外とプログラマ目線でホロスコープの描き方を説明している資料が少なく、情報集めに最初苦労しました。
そこで今回は、プログラマ目線で、ごく基本的なホロスコープの作成の仕方を解説したいと思います。
- 想定読者とこの記事で目指すこと
- ホロスコープとはなんぞや
- 天文計算ライブラリ pyswisseph の関数たち
- プログラムの概要
- BirthInfo クラスの実装
- NatalChart クラスの実装
- 星座を使った角度の表記
- 描画の実装
- 機能を拡張するには
- 実装全体
想定読者とこの記事で目指すこと
この記事は以下の属性を持つ人を想定しています。
最終的には「ネイタルチャート」と呼ばれる最もシンプルなホロスコープの作成を目指します。ネイタルチャートといっても色々バリエーションがありますが、ここでは以下の条件で作成します。
このホロスコープは情報が少なすぎて実用性は乏しいですが、ざっくり何をやればよいのかの理解の助けになれば嬉しいです。
実装全体は一番最後で紹介します。

ホロスコープとはなんぞや
西洋占星術と呼ばれる占いの分野があります。これはその名の通り西洋諸国などで発達した占いの手法で、様々な天体の位置を基に行われます。ホロスコープはこの位置の情報をシンプルに図式化したものです。西洋占星術では、ホロスコープを手がかりにして、様々な占いが展開されます。
ホロスコープには様々な種類があり、書き込まれる情報も多岐に渡ります。それらの中でも特に基本的な情報は、以下の2つです。
- 太陽・月・太陽系の惑星それぞれの地球から見た位置(黄道上の角度)
- 角度全体を12個の部屋に分けたもの(ハウス)
例えば、その人の性質に関わる占いをしたい場合、その人の出生の情報(生年月日・生まれた時刻・生まれた場所)から、上記の情報(といくつかの追加情報)をまとめた「ネイタルチャート」が作成されます。
「西洋占星術 ホロスコープ」と画像検索するといくつもヒットすると思うので、なんとなくイメージは掴めると思います。また、ホロスコープの詳しい意味が知りたい方は、数多くの占い関係の方が易しい説明をしているので、検索したり書籍を読んだりしてみてください。とりあえずこの記事の範囲では「ふーん、そういう世界もあるんだ」ぐらいの気持ちでOKです。
天文計算ライブラリ pyswisseph の関数たち
ホロスコープの描画のためには各天体の位置やハウスの位置を計算する必要がありますが、これを自分で実装するのは現実的ではありません。幸い、オープンソースのライブラリ pyswisseph があるので、これを使うことにします*1。
具体的には以下の関数を用います。
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未満の値になっています。ハウスシステムは部屋割りの仕方の種類を指します。分からない方は「なんかプラシーダスという名前のやり方をするんだな」と思ってください。
プログラムの概要
プログラムは以下の流れになるように作成します。
- 対象者の生年月日・時刻・場所を保持するオブジェクトを作成する(BirthInfo クラス)
- 上記を元に、チャート描画のためのデータを保持したオブジェクトを作成する(NatalChart クラス)
- 上記のデータを描画する
以下のライブラリを使うので、必要に応じて 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()