「ロト6 当せん数字予測アプリ」
「ロト6 当せん数字予測アプリ」 (以下、本アプリ) は、ロト6 の当せん数字の抽せんに用いられる セット球 のデータをもとに、次回の当せん数字を予測するためのヒントを提供するアプリです。
...が、実用性はほとんどありません (笑)。
サイト主は、本アプリと同じ方法で、数年にわたりロト6 の当せん数字を予測していますが、これまでに 5 等(1000 円)を数回獲得したことがある程度です。
本アプリは、あくまでも Python で開発されたアプリのデモ・サンプル作品としてお試しください。
ちなみに本アプリでは、過去の ロト6 の当せん数字データを CSV ファイルに出力することもできます。

アプリについて
本アプリは、プログラミング言語 Python で開発したデスクトップアプリで、 GUI に Kivy(キヴィ)ライブラリ、 データの管理・集計に Polars(ポーラース)ライブラリを利用しています。
サイト主は、これまで Python で開発したアプリのデモ作品として、ゲームや 3D 関連のアプリを公開してきましたが、 今回は最も得意とするデータ処理を行う実用アプリです。
今回の ロト6 の当せん数字予測は、Polars によるデータ集計と Kivy によるデータ表示のデモンストレーションとしてはちょうどよい題材でした。
冒頭で申し上げたとおりアプリとしては役に立たないので、その分今回は、大雑把ながらコードの解説を行っております。
コードの解説は こちら。
本アプリには、Windows / macOS / Linux の各 OS 対応版があります。
いずれも、各 OS 上で単体で動作する形式(exe や app)にビルドされているので、Python の動作環境のインストールは不要です。
本アプリの macOS 版は Apple シリコン 搭載 Mac のみ対応しています。
Intel プロセッサ 搭載 Mac では動作しません。
確認方法は こちら。
アプリのダウンロード
以下のリンクから、本アプリとその説明書をダウンロードできます。
- アプリの説明書: LOTO6_Analyzer_Manual.pdf (3.3MB)
- Windows 版アプリ: LOTO6_Analyzer_win_v1.0.1.0.zip (102.2MB)
- macOS 版アプリ: LOTO6_Analyzer_mac_v1.0.1.0.zip (84.6MB)
- Linux (Debian) 版アプリ: LOTO6_Analyzer_deb_v1.0.1.0.zip (124.2MB)
- Linux (Fedora) 版アプリ: LOTO6_Analyzer_fed_v1.0.1.0.zip (133.4MB)
Fedora 以外の Linux ディストリビューションをご利用の場合は、Linux (Debian) 版 をダウンロードしてください。
ソースコードのダウンロード
以下のリンクから、本アプリのソースコードをダウンロードできます。
Python のプロジェクト・パッケージ管理に uv をご利用の場合は、 "uv sync" コマンドだけで環境を構築できます。
ソースコードをご利用の際は、 ライセンス の項目をお読みください。
また、ソースコードは Python 3.12、および添付の requirements_*.txt に記載された環境以外での動作確認は行っておりません。
ご利用は自己責任でお願いいたします。
更新履歴
- v1.0.1.0 [2025/06/26]
-
- datagrid を独立したパッケージに変更した。
- MIT ライセンス全文の参照先 URL を変更した。
- v1.0.0.0 [2025/06/18]
- 初版リリース。
コードの解説
Polars(ポーラース)とは
本アプリで利用している Polars は、Rust(ラスト)というプログラミング言語で書かれたデータ分析ライブラリで、
非常に高速かつメモリ効率の良いデータ処理を実現しています。
Python と Rust で利用できます。
Python のデータ分析の分野では、Pandas(パンダス)というライブラリが圧倒的なシェアを誇っていますが、 処理速度やメモリ効率の面で Polars は Pandas よりも優れているとされます。
超高速…だけじゃない!Pandasに代えてPolarsを使いたい理由
Polars は、Excel などと同じように 行 と 列 がある二次元の表データを扱うことができます。
扱い方は リレーショナル・データベース に近いですが、データベースの機能は有していないので、
データベースシステムではありません。
Polars は、列指向(column-oriented)というデータ構造を採用しており、データはすべて列単位で管理されます。
そのため、列の追加・削除はできますが、行の追加・削除を直接行う手段はありません。
行単位のループ処理も遅いです(普通はしませんが…)。
一度格納されたデータはすべて固定され、特定の要素(Excel でいうセル)の値を後から書き換えることもできません。
「それじゃ何もできないじゃないか!」
と思われるかもしれませんが、データ処理に精通していれば何かしらの代替手段は思いつくので、特に困ることはありません。
このような制約があるからこそ、非常に高速な処理が実現可能なのです。
ただやはり、初心者の方には扱いにくいと思います。
少なくとも Excel の感覚で扱うことは困難でしょう。
DataFrame(データフレーム)について
Polars では、ひとつの 表 を DataFrame(データフレーム)というオブジェクトとして扱います。
DataFrame オブジェクトは Python の変数に代入できるので、その変数に対して、あらかじめ用意されているメソッドで操作を行います。
本アプリに収録されている当せん数字データの DataFrame の各列の名前と構成は以下のとおりです。
- "times"(抽選回:整数)
- "first_num"(第 1 数字:整数)
- "second_num"(第 2 数字:整数)
- "third_num"(第 3 数字:整数)
- "fourth_num"(第 4 数字:整数)
- "fifth_num"(第 5 数字:整数)
- "sixth_num"(第 6 数字:整数)
- "bonus_num"(ボーナス数字:整数)
- "ball_set"(セット球:文字列)
- "numbers"(第 1 〜 6 数字を列記したリスト)
- "numbers_with_bonus"(第 1 〜 6 数字とボーナス数字を列記したリスト)
DataFrame に格納できるデータは、数値や文字列だけでなく、ひとつの要素(セル)に複数の値をまとめて格納できる リスト型 というものがあります。
上記の "numbers" 列や "numbers_with_bonus" 列に格納されているのが、そのリスト型のデータです。
例えば上記の DataFrame に対して、ひとつの当せん数字を検索するとき、第 1 〜 6 数字が別々の列に入ったままだと、それぞれの列に対して別々に検索を行う必要があります。
しかし、"numbers" 列や "numbers_with_bonus" 列に当せん数字がひとまとめになっていれば、この列に対してだけ検索を行えばよいわけです。
DataFrame のファイル出力・読み込み
DataFrame のデータは、そのままファイルに出力することができます。
例えば、DataFrame を CSV ファイルに出力する場合、Python では write_csv メソッドを使って、以下のように記述します。
df.write_csv("output.csv")
先頭の df は、出力する DataFrame が格納された変数です。
上記の場合、"output.csv" という名前でファイルに出力されます。
なお、リスト型の列は CSV ファイルには出力できないので、予め除外しておく必要があります。
本アプリのソースコードの main_screen.py モジュール(ファイル)の 546 行目 _write_csv メソッドを参考にしてください。
CSV ファイルを読み込む場合は、read_csv メソッドを使って、以下のように記述します。
import polars as pl
df = pl.read_csv("output.csv")
変数 df には、"output.csv" ファイルから読み込まれたデータが DataFrame として代入されます。
ところで、Polars の DataFrame のひとつひとつの列には、決まったデータ型の値しか格納することができません。
例えば、整数 を格納すべき列には、文字列 や 実数(小数を含む数値)はひとつたりとも格納することはできません。
これは、データベース等を扱ったことがあれば当然のことなのですが、 Excel とは大きく異なる点です。
上記のコードように、ファイル名だけを指定した read_csv メソッドで CSV ファイルを読み込むと、
DataFrame の各列のデータ型は自動的に決定されますが、それでは不都合が生じる場合があります。
詳しくは、DataFrame の行の追加・更新について
をご覧ください。
例えば本アプリの当せん数字データを CSV ファイルから読み込む場合は、 厳密には以下のように、各列のデータ型を指定してやる必要があります。
import polars as pl
schema_dict = {
"times": pl.UInt32,
"first_num": pl.UInt8,
"second_num": pl.UInt8,
"third_num": pl.UInt8,
"fourth_num": pl.UInt8,
"fifth_num": pl.UInt8,
"sixth_num": pl.UInt8,
"bonus_num": pl.UInt8,
"ball_set": pl.Utf8,
}
df = pl.read_csv("loto6.csv", schema=schema_dict)
変数 schema_dict は、各列の名前とデータ型のペアを格納した辞書データです。
schema_dict のような列構成を示したデータを スキーマ情報 といいます。
pl.UInt32 は 0 〜 4,294,967,295 の範囲の整数、
pl.UInt8 は 0 〜 255 の範囲の整数、pl.Utf8 は 文字列 です。
このスキーマ情報を read_csv メソッドの引数 schema に渡してやります。
CSV ファイルにはスキーマ情報を保存できないので、CSV ファイルを読み込むときは、このような書き方をしなければなりません。
なので、本アプリの当せん数字データは、スキーマ情報を含め DataFrame のデータ構造をそのまま保存できる Parquet(パーケット)というファイル形式で保存しています。
DataFrame を Parquet ファイルに出力する場合は、write_parquet メソッドを使って、以下のように記述します。
df.write_parquet("loto6.parquet", compression="zstd")
上記のように引数 compression を指定すれば、自動的にファイルを圧縮してくれるので、ファイルのサイズを小さくできます。
データ構造がそのまま保存されているので、Parquet ファイルを読み込む場合は、 read_parquet メソッドを使って、以下のように記述するだけです。
import polars as pl
df = pl.read_parquet("loto6.parquet")
DataFrame のデータ構造をそのまま入出力できるので、Parquet ファイルの読み書きは高速です。
本アプリでは、当せん数字データの Parquet ファイル "loto6.parquet" を、アプリの起動時に自動的に読み込み、終了時に自動的に保存しています。
ソースコードの main_screen モジュールの 512 行目 _read_parquet メソッドと、535 行目 _write_parquet メソッドを参考にしてください。
「各セット球の選出回数」の集計処理
では、DataFrame の集計処理はどのように行うかを見ていきます。

本アプリの「セット球情報画面」の左の表「各セット球の選出回数」の集計は、 ball_sets_screen.py モジュールの 106 行目 _get_ball_set_count_data メソッドで行っています。
同メソッドでは、画面表示用のデータを作成して戻り値(答え)として返しているだけで、集計結果の表示は行っていません。
以下が、同メソッドのコードです(コメントは省いてあります)。
def _get_ball_set_count_data(self, df: pl.DataFrame) -> list[dict[str, int | str]]:
return (
df.group_by("ball_set")
.agg(pl.count("ball_set").alias("count"))
.select(pl.all().sort_by("ball_set"))
.with_columns((pl.col("count") / pl.sum("count")).alias("rate"))
.to_dicts()
)
3 行目の return 以下のコードは、見やすさのために改行されているだけで、実際にはひと続きのコードです。
つまり、「各セット球の選出回数」の集計は、1 行のコードだけで行っていることになります。
詳しく見ていきましょう。
変数 df は、すべての当せん数字データが格納された DataFrame です。
これを group_by メソッドにより、"ball_set" 列のデータ(セット球)別にグループ化し、新しい DataFrame を作成します。
df.group_by("ball_set")
group_by メソッドの処理により作成された、セット球別にグループ化された DataFrame に対して、 agg メソッドでグループごとに処理を行います。
.agg(pl.count("ball_set").alias("count"))
先頭の "."(ドット)が、前の処理の続きであることをあらわしています。
agg メソッドの後の括弧内のコードを エクスプレッション(Expression) といいます。
エクスプレッションは処理の流れを記述しているだけで、実際の処理は agg メソッドで実行されます。
ここでのエクスプレッションの内容(以下のコード)は、pl.count 関数でグループ内のデータ数(セット球の選出回数)をカウントし、 その選出回数を alias メソッドで新しく追加した "count" 列に出力するというものです。
pl.count("ball_set").alias("count")
次に、グループ別の "count" 列が追加された DataFrame を、sort_by メソッドで
セット球の順番(A から J の順)に並べ替えます。
sort_by メソッドもエクスプレッションの一部なので、実際には select メソッドで処理が実行され、新しい DataFrame が作成されます。
.select(pl.all().sort_by("ball_set"))
次に、各セット球の選出回数 pl.col("count") を、すべてのセット球の選出回数の合計 pl.sum("count") で割って「選出率」を算出し、 alias メソッドで新しく追加した "rate" 列にそれぞれ出力します。
このエクスプレッションが with_columns メソッドで実行され、新しい DataFrame が作成されて、集計は完了です。
.with_columns((pl.col("count") / pl.sum("count")).alias("rate"))
最後に、to_dicts メソッドで、集計済みの DataFrame を画面表示用のデータに変換しています。
.to_dicts()
上記のコードように、複数のメソッドを "."(ドット)でつなげて連続的に記述する方法を メソッドチェーン といいます。
メソッドチェーンは、今どきの主要なプログラミング言語では、ごく普通の書き方です。
決して特別な書き方ではありません。
環境によっては、メソッドチェーンを多用しすぎると、かえって可読性や保守性が低下することもあります。
しかし Polars については、処理速度の向上など大きなメリットがあるため、メソッドチェーンによる記述が前提となっているようです。
「各セット球の選出回数」の表示処理
上記の _get_ball_set_count_data メソッドで作成された表示用のデータを実際に画面に表示する処理は、 ball_sets_screen.py モジュールの 96 行目で、以下のとおり行っています。
self._datagrid_ball_sets.data = self._get_ball_set_count_data(df)
変数 self._datagrid_ball_sets が「各セット球の選出回数」の集計結果を表示するための
DataGrid ウィジェット(表示用の部品)です。
詳しくは、
DataGrid ウィジェットについて
をご覧ください。
集計結果の表示処理は、_get_ball_set_count_data メソッドで作成された表示用のデータを、
このウィジェットの data プロパティに代入する、この 1 行だけで行っています。
表のセルに、ひとつひとつループ処理で値を入れていく必要はありません。
このような集計と表示処理を、例えば Excel の VBA マクロ で書くとしたらどれほど大変なことになるかは、 容易に想像がつくと思います。
「各数字の出現状況」の集計処理
「セット球別集計画面」の左の表「各数字の出現状況」の集計処理についても見てみましょう。

この集計処理は、by_ball_set_screen.py モジュールの 274 行目 _get_frequency_count_dataframe メソッドで、 以下のとおり行っています(コメントは省いてあります)。
def _get_frequency_count_dataframe(self, df: pl.DataFrame, ball_set: str) -> pl.DataFrame:
col_name: str = "numbers_with_bonus" if self._include_bonus else "numbers"
df_new: pl.DataFrame = (
df.select(["ball_set", col_name])
.filter(pl.col("ball_set") == ball_set)
.explode(col_name)
.group_by(col_name)
.agg(pl.len().alias("count"))
.with_columns(
(
pl.col("count") / df.filter(pl.col("ball_set") == ball_set).height
).alias("rate")
)
.select(pl.all().sort_by(["count", col_name], descending=True))
)
if self._include_bonus:
df_new = df_new.with_columns(pl.col(col_name).alias("numbers"))
return df_new
「セット球別集計画面」では、「集計にボーナス数字を含めない / 含める」ボタンの選択状態により集計結果が変わります。
同ボタンの状態は self._include_bonus 変数(bool 型)に代入されているので、 集計の際に当せん数字データの "numbers" 列(ボーナス数字を含まないリスト)を参照するのか、 "numbers_with_bonus" 列(ボーナス数字を含むリスト)を参照するのかを、以下のコードで判断しています。
col_name: str = "numbers_with_bonus" if self._include_bonus else "numbers"
ここでは、「集計にボーナス数字を含めない」方の
col_name = "numbers" として解説します。
次の、df_new: pl.DataFrame = の行は、一旦置いておきます。
次のコードの先頭の df は、当選番号データの DataFrame です。
ここから、select メソッドにより "ball_set"(セット球)列と "numbers" 列だけを取り出して、
新しい DataFrame を作成します。
df.select(["ball_set", col_name])
次のコードの変数 ball_set には、画面の「セット球:」ボタンで選択されたセット球の名前(A 〜 J)が代入されています。
filter メソッドで、DataFrame の "ball_set" 列の値が、
変数 ball_set と一致する行のデータだけを抽出して、新しい DataFrame を作成します。
.filter(pl.col("ball_set") == ball_set)
次の explode メソッドで、"numbers" 列に格納されている当せん数字のリストを 縦方向(行方向)に展開します。
これで "numbers" 列に、リストではなく、単独の当せん数字が格納された新しい DataFrame が作成されます。
なぜ explode メソッドによる処理が必要かといえば、当せん数字がリストに格納されたままだと、 当せん数字をグループ化したりカウントしたりすることができないからです。
.explode(col_name)
次の group_by メソッドにより、"numbers" 列の当せん数字別に DataFrame をグループ化します。
その次の agg メソッドで各グループのデータ数をカウントし、
その結果を出力した新しい count 列を追加して、新しい DataFrame を作成します。
この count 列の値が、各当せん数字の「出現回数」になります。
.group_by(col_name)
.agg(pl.len().alias("count"))
次の with_columns メソッドは、一旦置いておきます。
その次の以下のコードで、DataFrame の "ball_set" 列の値が、変数 ball_set と一致する行だけを抽出し、
height プロパティでその行数を取得しています。
これが、画面の「セット球:」ボタンで選択されているセット球の「選出回数」になります。
df.filter(pl.col("ball_set") == ball_set).height
"count" 列の各当せん数字の出現回数を、上記のセット球の選出回数で割ることにより、 各当せん数字の「出現率」を算出できるので、これを alias メソッドで新しい "rate" 列に出力します。
そして、with_columns メソッドにより、実際に "rate" 列が追加された新しい DataFrame が作成されます。
.with_columns(
(
pl.col("count") / df.filter(pl.col("ball_set") == ball_set).height
).alias("rate")
)
最後に sort_by メソッドで "count" 列の値(出現回数)が多い順に並べ替え、 select メソッドで並べ替えられた新しい DataFrame を作成します。
.select(pl.all().sort_by(["count", col_name], descending=True))
ここまで、メソッドチェーンにより事実上 1 行で処理を終えた DataFrame は、以下のコードにより、 変数 df_new に代入します。
このように、メソッドチェーンを使えば、元の DataFrame を変更することなく、処理済みの DataFrame を取得することができます。
df_new: pl.DataFrame = (
# メソッドチェーンによる処理
)
画面で「集計にボーナス数字を含める」が選択されていた場合、 変数 df_new の出現回数が格納された列の名前は "numbers_with_bonus" になっていますが、 表示用のデータは出現回数の列名が "numbers" である必要があります。
なので、「集計にボーナス数字を含める」が選択されていた場合は、 以下のコードにより新たに "numbers" 列を追加し、"numbers_with_bonus" 列のデータをそこに格納(コピー)しています。
if self._include_bonus:
df_new = df_new.with_columns(pl.col(col_name).alias("numbers"))
ちなみに、この _get_frequency_count_dataframe メソッドにより作成された DataFrame には、 この後「累積分布関数の値」の列を追加しなければならないので、 このメソッド内では to_dicts メソッドによる表示用データへの変換は行っていません。
上記の集計結果に「累積分布関数の値」の列が追加された DataFrame が代入された変数 df_by_number は、
by_ball_set_screen.py モジュールの 245 行目で、to_dicts メソッドで表示用データに変換し、
表のウィジェット self._datagrid_frequency_count の data プロパティに代入しています。
これで、集計済みのデータが画面に表示されます。
self._datagrid_frequency_count.data = df_by_number.to_dicts()
「'◯' と同時に出現している数字」の集計処理
「セット球別集計画面」の右の表「'◯' と同時に出現している数字」の集計処理についても見てみましょう。

この集計処理は、by_ball_set_screen.py モジュールの 308 行目 _get_co_occurrence メソッドで、 以下のとおり行っています(コメントは省いてあります)。
def _get_co_occurrence(self, df: pl.DataFrame, df_by_number: pl.DataFrame, ball_set: str, number: int) -> pl.DataFrame:
col_name: str = "numbers_with_bonus" if self._include_bonus else "numbers"
df_new: pl.DataFrame = (
(
df.select(["ball_set", col_name])
.filter(
(pl.col("ball_set") == ball_set)
& (pl.col(col_name).list.contains(number))
)
.explode(col_name)
.group_by(col_name)
.agg(pl.len().alias("co_occurrence"))
)
.join(df_by_number, on=col_name, how="inner")
.select(pl.all().sort_by(["co_occurrence", "count"], descending=True))
).filter(pl.col(col_name) != number)
if self._include_bonus:
df_new = df_new.with_columns(pl.col(col_name).alias("numbers"))
return df_new
3 行目の "col_name: str = … if …" は、前出の 「各数字の出現状況」の集計処理 と同じで、「集計にボーナス数字を含めない / 含める」ボタンの状態によって、 集計の際に当せん数字データの DataFrame の "numbers" と "numbers_with_bonus" のどちらの列のデータを参照するかを判断します。
ここでは、col_name = "numbers"(ボーナス数字を含めない)として解説します。
col_name: str = "numbers_with_bonus" if self._include_bonus else "numbers"
変数 df はすべての当せん数字データの DataFrame で、 ここから select メソッドにより "ball_set" 列(セット球)と "numbers" 列だけを取り出して、 新しい DataFrame を作成します。
df.select(["ball_set", col_name])
変数 ball_set には画面上で選択されているセット球、変数 number には「各数字の出現状況」
の表でユーザーが選択した数字が代入されています。
filter メソッドで、"ball_set" 列の値と変数 ball_set の値が 一致し、かつ、
"numbers" 列のリストに変数 number の値が含まれる行だけを抽出して、新しい DataFrame を作成します。
.filter(
(pl.col("ball_set") == ball_set)
& (pl.col(col_name).list.contains(number))
)
次からの explode、group_by、agg メソッドの処理は、前出の 「各数字の出現状況」の集計処理 と同じです。
"numbers" 列に格納された当せん数字のリストを行方向に展開し、 当せん数字別にグループ化して、グループごとのデータ数をカウントした値(同時出現回数)を、 新しい "co_occurrence" 列に出力して、新たに DataFrame を作成します。
.explode(col_name)
.group_by(col_name)
.agg(pl.len().alias("co_occurrence"))
この DataFrame には、左の「各数字の出現状況」の表で集計した「出現回数」列を追加したいので、 次の join メソッドで、両方の表の DataFrame を 内部結合 させて、 新しい DataFrame を作成しています。
内部結合というのは、Excel でいうと、"numbers" 列の値(当せん数字)を手がかりに、 VLOOKUP 関数で「各数字の出現状況」の表の「出現回数」の値を引っぱってきて、 関数がエラーでない行だけを残すイメージです。
join メソッドの最初の引数に指定している変数 df_by_number が「各数字の出現状況」
の表の DataFrame です。
次の引数 on には、結合の手がかりになる列の名前(ここでは "numbers")を指定し、
内部結合の場合は、次の引数 how に "inner" を指定します。
.join(df_by_number, on=col_name, how="inner")
これで、DataFrame に "count" 列(出現回数)が追加されたので、次の sort_by メソッドで "co_occurrence" 列の値(同時出現回数)と "count" 列の値を大きい順に並べ替え、 select メソッドで新しい DataFrame を作成します。
.select(pl.all().sort_by(["co_occurrence", "count"], descending=True))
「'◯' と同時に出現している数字」の集計なので、当せん数字が ◯ の行は集計結果から除外しなければなりません。
そこで、最後の filter メソッドで、当せん数字が ◯(変数 number)以外の行だけを抽出して、新しい DataFrame を作成しています。
これで、当せん数字が ◯ の行が削除されたことになります。
).filter(pl.col(col_name) != number)
「Polars には、行の削除を直接行う手段がない」と先述しましたが、この方法を使えば、行を削除するのと同じことができるので問題ないわけです。
DataFrame の行の追加・更新について
本アプリの「データ管理画面」の上部には、抽選回・第 1 〜 6 数字・ボーナス数字・セット球を入力するための入力ボックスがあります。

「Polars には、行の追加を直接行う手段がない」と先述しましたが、本アプリでは、 各入力ボックスに入力したデータを「追加・更新」ボタンで一覧表に追加することができます。
また、「Polars は、特定の要素の値を後から書き換えることができない」と先述しましたが、本アプリでは、 一覧表から特定の行を選択し、入力ボックスに表示されたデータを書き換えて「追加・更新」ボタンをクリックすれば、 一覧表のデータを上書きすることもできます。
これらの処理はどのように実現しているのか、詳しく見てみましょう。
今から説明する方法は、手作業で入力されたデータを既存の DataFrame に 1 回ずつ追加するといった
「処理速度が重要でないケース」に限られます。
既存の DataFrame に自動的に繰り返し新しい行を追加するような「処理速度が重要なケース」には向かないことを、
先に申し上げておきます。
データの追加・更新処理については、main_screen.py モジュールの 376 行目 on_modify_button_click メソッドで行っています。
def on_modify_button_click(self) -> None:
"""「追加・更新」ボタンがクリックされたとき。"""
if not self._validate_values():
return
record: dict[str, int | str] = {
"times": int(self._text_times.text),
"first_num": int(self._text_1st_num.text),
"second_num": int(self._text_2nd_num.text),
"third_num": int(self._text_3rd_num.text),
"fourth_num": int(self._text_4th_num.text),
"fifth_num": int(self._text_5th_num.text),
"sixth_num": int(self._text_6th_num.text),
"bonus_num": int(self._text_bonus_num.text),
"ball_set": self._text_ball_set.text.upper(),
}
df: pl.DataFrame = self._shared_vars.df
if df.filter(pl.col("times") == record["times"]).is_empty():
self._append_record(record)
self._display_status(f"'第 {record['times']} 回' のデータを追加しました。")
else:
times: int = int(record["times"])
self._shared_vars.df = self._shared_vars.df.filter(pl.col("times") != times)
self._append_record(record)
self._display_status(f"'第 {record['times']} 回' のデータを更新しました。")
self._selected_times = -1
self._clear_input_text()
まず、各入力ボックスに入力された値がすべて正しいか、以下のコードでチェックしています。
入力された値がひとつでも正しくなければ、このメソッドはここで終了します。
if not self._validate_values():
return
次のコードでは、データ名(DataFrame の列名)と、 入力ボックスに入力されたデータのペアを格納した辞書データを作成して、変数 record に代入しています。
record: dict[str, int | str] = {
"times": int(self._text_times.text),
"first_num": int(self._text_1st_num.text),
"second_num": int(self._text_2nd_num.text),
"third_num": int(self._text_3rd_num.text),
"fourth_num": int(self._text_4th_num.text),
"fifth_num": int(self._text_5th_num.text),
"sixth_num": int(self._text_6th_num.text),
"bonus_num": int(self._text_bonus_num.text),
"ball_set": self._text_ball_set.text.upper(),
}
元となっている当せん数字のデータは、変数 self._shared_vars に格納されたオブジェクトの変数
df として、アプリ全体で共有しています。
ただ、そのまま扱うには名前が長すぎるので、以下のコードで、ローカル変数 df に代入しています。
df: pl.DataFrame = self._shared_vars.df
以下の if 文では、元となる当せん数字データの DataFrame の "times" 列(抽選回)に、 入力ボックスに入力された抽選回が存在するかを判断しています。
まず、filter メソッドで、当せん数字データから対象となる抽選回と一致する行だけを抽出した
DataFrame を取得します。
この DataFrame が 空 であれば「該当する抽選回は存在しない」ということで、
入力されたデータを追加する処理を行います。
is_empty メソッドは、DataFrame が空であれば True を返します。
「該当する抽選回がすでに存在する」場合は、入力されたデータを上書きする処理を行います。
if df.filter(pl.col("times") == record["times"]).is_empty():
# データを追加する処理
else:
# データを上書きする処理
以下のコードは、データを追加する処理です。
入力データが代入された変数 record を、元となる当せん数字データに _append_record メソッドで追加しています。
_append_record メソッドについては後述します。
if df.filter(pl.col("times") == record["times"]).is_empty():
self._append_record(record)
self._display_status(f"'第 {record['times']} 回' のデータを追加しました。")
以下のコードは、データを上書きする処理です。
変数 times は、上書き予定のデータの抽選回です。
次の filter メソッドで、元となる当せん数字データ self._shared_vars.df
から上書き予定の抽選回以外の行を抽出して、元となる当せん数字データに代入し直しています。
これで、上書き予定の抽選回のデータが、元の当せん数字データから削除されたことになります。
この当せん数字データに、新たに上書き予定のデータを追加すれば、結果的にデータを上書きしたことになります。
データを追加する処理と同じく、次の _append_record メソッドで、データを追加しています。
else:
times: int = int(record["times"])
self._shared_vars.df = self._shared_vars.df.filter(pl.col("times") != times)
self._append_record(record)
self._display_status(f"'第 {record['times']} 回' のデータを更新しました。")
上記のデータを追加する _append_record メソッドは、main_screen.py モジュールの 644 行目にあります。
def _append_record(self, record: dict[str, int | str]) -> None:
schema: dict[str, pl.datatypes.DataType] = {
"times": pl.UInt32,
"first_num": pl.UInt8,
"second_num": pl.UInt8,
"third_num": pl.UInt8,
"fourth_num": pl.UInt8,
"fifth_num": pl.UInt8,
"sixth_num": pl.UInt8,
"bonus_num": pl.UInt8,
"ball_set": pl.Utf8,
}
df_new: pl.DataFrame = pl.DataFrame(record, schema=schema)
df_new = self._append_numbers_column(df_new)
self._shared_vars.df = pl.concat([self._shared_vars.df, df_new]).select(
pl.all().sort_by("times")
)
self._display_data()
このメソッドの引数 record は、当せん数字データの DataFrame の列名と、 入力ボックスに入力されたデータのペアの辞書データです。
次の変数 schema に代入されているのは、 DataFrame のファイル出力・読み込み で、CSV ファイルを読み込む際に必要と説明していた スキーマ情報 (当せん数字データの DataFrame の各列の名前とデータ型のペアの辞書データ)です。
schema: dict[str, pl.datatypes.DataType] = {
"times": pl.UInt32,
"first_num": pl.UInt8,
"second_num": pl.UInt8,
"third_num": pl.UInt8,
"fourth_num": pl.UInt8,
"fifth_num": pl.UInt8,
"sixth_num": pl.UInt8,
"bonus_num": pl.UInt8,
"ball_set": pl.Utf8,
}
pl.DataFrame コンストラクタの引数に、上記の record と schema を渡してやることで、 入力ボックスに入力されたデータが格納された新しい DataFrame を作成することができます。
df_new: pl.DataFrame = pl.DataFrame(record, schema=schema)
次の _append_numbers_column メソッドで、上記の新しい DataFrame に、
第 1 〜 6 数字をまとめたリスト型の "numbers" 列と、
それにボーナス数字を加えたリスト型の "numbers_with_bonus" 列を追加しています。
これで、新しい DataFrame と、元の当せん数字データの列構成が同じになります。
df_new = self._append_numbers_column(df_new)
「Polars には、行の追加を直接行う手段はない」と先述しましたが、pl.concat 関数により 「複数の DataFrame を縦方向に結合する」ことはできます。
以下のコードのとおり、pl.concat 関数の引数に、結合したい DataFrame のリストを渡してやることで、 元の当せん数字データの DataFrame に新しい DataFrame を結合して、新しい DataFrame を作成することができます。
その DataFrame を、次の sort_by メソッドで、抽選回の順番に並べ替えています。
self._shared_vars.df = pl.concat([self._shared_vars.df, df_new]).select(
pl.all().sort_by("times")
)
上記の方法により、DataFrame に新しい行を追加するのと同じことはできるのですが、 この「DataFrame の縦結合」には注意が必要です。
まず、結合するすべての DataFrame の 列の名前 と データ型 が、完全に一致している必要があります。
DataFrame のファイル出力・読み込み
で、「CSV ファイルを読み込む read_csv
メソッドの引数にスキーマ情報を指定しないと、DataFrame の各列のデータ型は自動的に決定される」
と説明しましたが、DataFrame の列のデータ型が自動的に決定されると、
「結合する DataFrame 同士の列のデータ型が一致しない」という不具合が発生する場合があります。
これについては、cast メソッドによって列のデータ型を後から変更することもできるので、 必ずしも CSV ファイルを読み込む際にスキーマ情報を指定する必要はありませんが、 列のデータ型には注意しておく必要があります。
また、列指向の Polars にとって、DataFrame の行数を増やすという処理は非常に負担が大きく、処理に時間がかかります。
DataFrame の縦結合を頻繁に繰り返すようなコードは、書くべきではありません。
DataGrid ウィジェットについて
「各セット球の選出回数」の表示処理 で説明したとおり、本アプリでは、集計結果の DataFrame を to_dicts メソッドで表示用のデータに変換して、 それを表示用のウィジェットの data プロパティに代入するだけで、集計結果を表示することができます。
self._datagrid_ball_sets.data = self._get_ball_set_count_data(df)
本アプリで利用している、その表示用のウィジェットが DataGrid(データグリッド)です。
DataGrid は、サイト主が自作した "datagrid" パッケージに含まれており、"datagrid" フォルダに収録しています。
DataGrid の正体は、Kivy の RecycleView ウィジェットを配置した BoxLayout です。
RecycleView は、Kivy で「スクロール可能な二次元形式の表」を表示するウィジェットですが、
デフォルトでは ヘッダー(列見出し)を表示する機能がありません。
RecycleView にヘッダーを表示する方法はいくつかありますが、サイト主は、BoxLayout を使って RecycleView
とヘッダーを別々に配置する方法を採っていました。
ただ、毎回そのコードを書くのが面倒だったので、今回パッケージ化した次第です。
パッケージ化するついでに、表に表示されたデータをクリックすると、その行を選択状態にして、
その行のデータを取得できる機能を実装しています。
DataGrid は、BoxLayout を継承したクラスなので、main_screen.py モジュールの 217 行目のように、 KV コード により単独で配置できます。
DataGrid:
id: datagrid
viewclass: 'DataGridMainRow'
size_hint: 1.0, 0.9
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
上記の viewclass プロパティで指定している "DataGridMainRow" というのは、
datagrid_main_row.py モジュールで定義しているクラスの名前です。
この DataGridMainRow クラスは、datagrid パッケージに含まれる RowItem クラスを継承しています。
RowItem を継承したクラスでは、表のレコード行 1 行分の列のレイアウトを定義します。
例えば、DataGridMainRow の場合は、datagrid_main_row.py モジュールの 19 行目以下のように、 datagrid パッケージに含まれる DataLabel ウィジェットを、列の数だけ並べて定義しています。
<DataGridMainRow>:
selectable: True
DataLabel:
text: str(root.times)
size_hint_x: 0.12
text_size: self.size
halign: 'right'
DataLabel:
text: f"{root.first_num :0>2}"
size_hint_x: 0.11
text_size: self.size
halign: 'center'
...
そして、datagrid_main_row.py モジュールの 117 行目以下のように、クラスの定義で、 当せん数字データの DataFrame の列名と同じ名前のプロパティを記述しておけば、to_dicts メソッドで変換した DataFrame のデータを data プロパティに代入するだけでデータを表示することができます。
class DataGridMainRow(RowItem):
"""メイン画面のデータグリッドのレコード行を定義したクラス"""
times: int = NumericProperty(0)
"""抽選回"""
first_num: int = NumericProperty(0)
"""第 1 数字"""
second_num: int = NumericProperty(0)
"""第 2 数字"""
...
ちなみに、ヘッダー行のレイアウトは、datagrid_main_row.py モジュールの 68 行目 "<DataGridMainHeader>"
以下で定義しています。
ここでは、datagrid パッケージに含まれる HeaderLabel ウィジェットを、列の数だけ配置しています。
<DataGridMainHeader>:
HeaderLabel:
text: "抽選回"
size_hint_x: 0.12
text_size: self.size
halign: 'center'
HeaderLabel:
text: "第 1"
size_hint_x: 0.11
text_size: self.size
halign: 'center'
...
例えば、この HeaderLabel を Button や Spinner ウィジェットに置き換えれば、 「表の列見出しをクリックしてデータの並べ替えやフィルタリングを行う機能」を、 datagrid パッケージに手を加えることなく実装できます。
なお、上記で定義された DataGridMainHeader ウィジェットは、main_screen.py モジュールの 254 行目で、 Python コードにより DataGrid ウィジェットの header_item プロパティに代入しています。
self._datagrid.header_item = DataGridMainHeader()
この header_item プロパティへの DataGridMainHeader ウィジェットの代入を KV コード で行う場合は、DataGridMainHeader クラスの import 文も KV コードで記述する必要があるので、ご注意ください。
ライセンス
「ロト6 当せん数字予測アプリ」(以下 本ソフトウェア)の著作権は、 開発者である 筒井敏文 が保有します。
本ソフトウェアのバイナリファイル、およびソースコードは
MIT ライセンス
の下で配布します。
本ソフトウェアのバイナリファイル、およびソースコードの改変や再配布は自由に行うことができます。
ただし再配布の際には必ず、添付の "LICENSE.TXT" ファイルを配布物にも添付するか、
または配布物のわかりやすい場所に以下の 3 行を記載してください。
Copyright (c) 2025 toshifumi tsutsui
Released under the MIT license
https://wpandora8.net/the_mit_license.html
なお、著作権者は、本ソフトウェアのバイナリファイル、およびソースコードに起因または関連し、 あるいはバイナリファイルおよびソースコードの使用またはその他の扱いによって生じる一切の請求、 損害、その他の義務について何らの責任も負わないものとします。