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

アプリについて
本アプリは、プログラミング言語 Python で開発したデスクトップアプリで、 GUI に Kivy(キヴィ)ライブラリ、 データの管理・集計に Polars(ポーラース)ライブラリを利用しています。
サイト主は、これまで Python で開発したアプリのデモ作品として、ゲームや 3D 関連のアプリを公開してきましたが、 今回は最も得意とするデータ処理を行う実用アプリです。
今回の ロト6 の当せん数字予測は、Polars によるデータ集計と Kivy によるデータ表示のデモンストレーションとしてはちょうどよい題材でした。
冒頭で申し上げたとおりアプリとしては役に立たないので、その分今回は、大雑把ながらコードの解説を行っております。
コードの解説は こちら。
本アプリには、Windows 版と macOS 版があります。
いずれも、各 OS 上で単体で動作する形式(exe や app)にビルドされているので、Python の動作環境のインストールは不要です。
本アプリの macOS 版は Apple シリコン 搭載 Mac のみ対応しています。
Intel プロセッサ 搭載 Mac では動作しません。
確認方法は こちら。
アプリのダウンロード
以下のリンクから、本アプリとその説明書をダウンロードできます。
ソースコードのダウンロード
以下のリンクから、本アプリのソースコードをダウンロードできます。
Python のプロジェクト・パッケージ管理に uv をご利用の場合は、 "uv sync" コマンドだけで環境を構築できます。
ソースコードをご利用の際は、 ライセンス の項目をお読みください。
また、ソースコードは Python 3.13、および添付の requirements_*.txt に記載された環境以外での動作確認は行っておりません。
ご利用は自己責任でお願いいたします。
更新履歴
- v1.0.2.0 [2025/10/01]
-
- macOS 版の「ファイル出力」ダイアログボックスで、日本語のファイル名を入力する際の不具合を修正した。
- Python を Version 3.13 に更新した。
- 各 Python ライブラリを更新した。
- 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()
上記のコードように、複数のメソッドを "."(ドット)でつなげて連続的に記述する方法を メソッドチェーン といいます。
メソッドチェーンは、今どきの主要なプログラミング言語では、ごく普通の書き方です。
決して特別な書き方ではありません。
「各セット球の選出回数」の表示処理
上記の _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
なお、著作権者は、本ソフトウェアのバイナリファイル、およびソースコードに起因または関連し、 あるいはバイナリファイルおよびソースコードの使用またはその他の扱いによって生じる一切の請求、 損害、その他の義務について何らの責任も負わないものとします。