neoai-kterasawa commited on
Commit
9e16e32
·
1 Parent(s): 4e83f99
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .token
2
+ __pycache__
app.py CHANGED
@@ -1,42 +1,53 @@
1
- from typing import Any
2
 
3
- import gradio as gr
4
 
5
- from utils import get_calculated_df, get_info, text2oddslist
6
 
7
- DEFAULT_AMOUNT = 5000
8
 
9
 
10
- def calculate(_amount: Any, _text: Any) -> tuple[str, dict]:
11
- amount = int(_amount) or DEFAULT_AMOUNT
12
 
13
- df = get_calculated_df(amount, text2oddslist(str(_text)))
14
- info = get_info(df)
15
 
16
- # dfの整形
17
- display_cols = ["odds", "buy", "refound"]
18
- if df["name"].map(lambda name: name != "").any():
19
- display_cols = ["name"] + display_cols
 
20
 
21
- # markdownの整形
22
- md_string = (
23
- f"**購入額:** ¥ **{info['sum']:,}**<br>"
24
- f"**点数:** **{info['num_kind']}** 点<br>"
25
- f"**払戻:** ¥ **{info['refound_mean']:,}** (**{info['profit_mean']:+,}** (**{info['rate_min']:+.0%}**))<br>"
26
- "---"
27
- )
28
 
29
- return md_string, df[["index"] + display_cols]
 
 
 
 
 
 
 
30
 
 
 
 
 
 
 
 
 
 
31
 
32
- amount_input = gr.Number(value=DEFAULT_AMOUNT, label="amount")
33
- text_input = gr.Textbox(label="text")
34
- markdown_output = gr.Markdown()
35
- df_output = gr.Dataframe(type="pandas")
36
- iface = gr.Interface(
37
- fn=calculate,
38
- inputs=[amount_input, text_input],
39
- outputs=[markdown_output, df_output],
40
- )
 
 
41
 
42
- iface.launch()
 
 
1
+ from typing import Final
2
 
3
+ import gradio as gr # type: ignore
4
 
5
+ import src.func as func
6
 
7
+ DEFAULT_AMOUNT: Final[int] = 10_000
8
 
9
 
10
+ def clear(text: str) -> str:
11
+ return func.clear(text)
12
 
 
 
13
 
14
+ def calculate(budget: int, text: str) -> tuple[str, list[list]]:
15
+ try:
16
+ return func.calculate(budget, text)
17
+ except Exception as e:
18
+ raise gr.Error(e)
19
 
 
 
 
 
 
 
 
20
 
21
+ with gr.Blocks() as demo:
22
+ with gr.Row():
23
+ with gr.Column():
24
+ budget_input = gr.Number(value=DEFAULT_AMOUNT, label="予算")
25
+ text_input = gr.Textbox(label="netkeiba 買い目コピペ")
26
+ with gr.Row():
27
+ calculate_button = gr.Button(value="Calculate", variant="primary")
28
+ clear_button = gr.Button(value="Clear")
29
 
30
+ with gr.Column():
31
+ markdown_output = gr.Markdown()
32
+ df_output = gr.Dataframe(
33
+ value=[],
34
+ label=None,
35
+ headers=["", "", "オッズ", "賭け額", "払戻"],
36
+ datatype=["str", "str", "number", "number", "number"],
37
+ col_count=(5, "fixed"),
38
+ )
39
 
40
+ # click
41
+ calculate_button.click(
42
+ calculate,
43
+ inputs=[budget_input, text_input],
44
+ outputs=[markdown_output, df_output],
45
+ )
46
+ clear_button.click(
47
+ clear,
48
+ inputs=[text_input],
49
+ outputs=[text_input],
50
+ )
51
 
52
+ if "__main__" == __name__:
53
+ demo.launch(debug=True)
archive/app.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ import gradio as gr
4
+
5
+ from archive.utils import get_calculated_df, get_info, text2oddslist
6
+
7
+ DEFAULT_AMOUNT = 5000
8
+
9
+
10
+ def calculate(_amount: Any, _text: Any) -> tuple[str, dict]:
11
+ amount = int(_amount) or DEFAULT_AMOUNT
12
+
13
+ df = get_calculated_df(amount, text2oddslist(str(_text)))
14
+ info = get_info(df)
15
+
16
+ # dfの整形
17
+ display_cols = ["odds", "buy", "refound"]
18
+ if df["name"].map(lambda name: name != "").any():
19
+ display_cols = ["name"] + display_cols
20
+
21
+ # markdownの整形
22
+ md_string = (
23
+ f"**購入額:** ¥ **{info['sum']:,}**<br>"
24
+ f"**点数:** **{info['num_kind']}** 点<br>"
25
+ f"**払戻:** ¥ **{info['refound_mean']:,}** (**{info['profit_mean']:+,}** (**{info['rate_min']:+.0%}**))<br>"
26
+ "---"
27
+ )
28
+
29
+ return md_string, df[["index"] + display_cols]
30
+
31
+
32
+ amount_input = gr.Number(value=DEFAULT_AMOUNT, label="amount")
33
+ text_input = gr.Textbox(label="text")
34
+ markdown_output = gr.Markdown()
35
+ df_output = gr.Dataframe(type="pandas")
36
+ iface = gr.Interface(
37
+ fn=calculate,
38
+ inputs=[amount_input, text_input],
39
+ outputs=[markdown_output, df_output],
40
+ )
41
+
42
+ iface.launch()
app_gradio.py → archive/app_gradio.py RENAMED
@@ -1,7 +1,9 @@
1
  import gradio as gr
2
- from utils import get_calculated_df, get_info, text2oddslist
3
 
4
  DEFAULT_AMOUNT = 5000
 
 
5
  def calculate(amount, text):
6
  amount = amount or DEFAULT_AMOUNT
7
  odds_list = text2oddslist(text)
 
1
  import gradio as gr
2
+ from archive.utils import get_calculated_df, get_info, text2oddslist
3
 
4
  DEFAULT_AMOUNT = 5000
5
+
6
+
7
  def calculate(amount, text):
8
  amount = amount or DEFAULT_AMOUNT
9
  odds_list = text2oddslist(text)
app_streamlit.py → archive/app_streamlit.py RENAMED
@@ -1,5 +1,6 @@
1
  import streamlit as st
2
- from utils import get_calculated_df, get_info, text2oddslist
 
3
 
4
  amount: int = int(st.number_input("amount", min_value=100, value=3000, step=100))
5
  text: str = st.text_area("text", "")
 
1
  import streamlit as st
2
+
3
+ from archive.utils import get_calculated_df, get_info, text2oddslist
4
 
5
  amount: int = int(st.number_input("amount", min_value=100, value=3000, step=100))
6
  text: str = st.text_area("text", "")
utils.py → archive/utils.py RENAMED
File without changes
kculculate.code-workspace ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "folders": [
3
+ {
4
+ "name": "kculculate",
5
+ "path": "."
6
+ }
7
+ ],
8
+ "settings": {
9
+ "editor.codeActionsOnSave": {
10
+ "source.fixAll.eslint": "explicit",
11
+ "source.fixAll.stylelint": "explicit"
12
+ },
13
+ "editor.formatOnSave": true,
14
+ "editor.formatOnPaste": true,
15
+ "editor.formatOnType": true,
16
+ "json.format.keepLines": true,
17
+ "[javascript]": {
18
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
19
+ },
20
+ "[typescript]": {
21
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
22
+ },
23
+ "[typescriptreact]": {
24
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
25
+ },
26
+ "[css]": {
27
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
28
+ },
29
+ "[json]": {
30
+ "editor.defaultFormatter": "vscode.json-language-features"
31
+ },
32
+ "search.exclude": {
33
+ "**/node_modules": true,
34
+ "static": true
35
+ },
36
+ "[python]": {
37
+ "editor.defaultFormatter": "ms-python.black-formatter",
38
+ "editor.codeActionsOnSave": {
39
+ "source.organizeImports": "explicit"
40
+ }
41
+ },
42
+ "flake8.args": [
43
+ "--max-line-length=119",
44
+ "--max-complexity=15",
45
+ "--ignore=E203,E501,E704,W503",
46
+ "--exclude=.venv,.git,__pycache__,.mypy_cache,.hg"
47
+ ],
48
+ "isort.args": ["--settings-path=pyproject.toml"],
49
+ "black-formatter.args": ["--config=pyproject.toml"],
50
+ "mypy-type-checker.args": ["--config-file=pyproject.toml"],
51
+ "python.analysis.extraPaths": ["./backend"]
52
+ },
53
+ "extensions": {
54
+ "recommendations": [
55
+ "esbenp.prettier-vscode",
56
+ "dbaeumer.vscode-eslint",
57
+ "ms-python.flake8",
58
+ "ms-python.isort",
59
+ "ms-python.black-formatter",
60
+ "ms-python.mypy-type-checker"
61
+ ]
62
+ }
63
+ }
makefile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .PHONY: lint
2
+ lint: ## run tests with poetry (isort, black, pflake8, mypy)
3
+ poetry run black src
4
+ poetry run isort src
5
+ poetry run pflake8 src
6
+ poetry run mypy src --explicit-package-bases
7
+
poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "kculculate"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["neoai-kterasawa <[email protected]>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.10"
10
+ gradio = "^4.39.0"
11
+ pydantic = "^2.8.2"
12
+
13
+ [tool.poetry.group.dev.dependencies]
14
+ isort = "^5.13.2"
15
+ black = "^24.4.2"
16
+ mypy = "^1.11.0"
17
+ pyproject-flake8 = "^7.0.0"
18
+
19
+
20
+ [build-system]
21
+ requires = ["poetry-core"]
22
+ build-backend = "poetry.core.masonry.api"
requirements.txt CHANGED
@@ -1,3 +1,2 @@
1
- pandas==2.1.0
2
- streamlit==1.26.0
3
- gradio==3.44.3
 
1
+ gradio == "4.39.0"
2
+ pydantic == "2.8.2"
 
src/func.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Final
2
+
3
+ from src.model.odds import Choices
4
+
5
+ DEFAULT_AMOUNT: Final[int] = 10_000
6
+
7
+
8
+ def calculate(budget: int, raw_text: str) -> tuple[str, list[list]]:
9
+ choices = Choices.from_text(raw_text)
10
+ choices_with_bet = choices.to_choices_with_bet(budget)
11
+
12
+ summary_markdown = choices_with_bet.to_summary_markdwown()
13
+ table = choices_with_bet.to_table()
14
+
15
+ return summary_markdown, table
16
+
17
+
18
+ def clear(text: str) -> str:
19
+ return ""
src/model/odds.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from pydantic import (
4
+ BaseModel,
5
+ RootModel,
6
+ StrictFloat,
7
+ StrictInt,
8
+ StrictStr,
9
+ computed_field,
10
+ field_validator,
11
+ )
12
+
13
+
14
+ class Choice(BaseModel):
15
+ number: StrictStr
16
+ name: StrictStr
17
+ odds: StrictFloat
18
+
19
+
20
+ class ChoiceWithBet(Choice):
21
+ bet_amount: StrictInt
22
+
23
+ @computed_field # type: ignore
24
+ @property
25
+ def pay_back(self) -> int:
26
+ return int(self.bet_amount * self.odds)
27
+
28
+ @computed_field # type: ignore
29
+ @property
30
+ def profit(self) -> int:
31
+ return self.pay_back - self.bet_amount
32
+
33
+
34
+ class Choices(RootModel[list[Choice]]):
35
+ root: list[Choice]
36
+
37
+ @field_validator("root")
38
+ @classmethod
39
+ def ensure_at_least_one_choice(cls, choices: list[Choice]) -> list[Choice]:
40
+ if len(choices) == 0:
41
+ raise ValueError("買い目が1つもありません")
42
+ return choices
43
+
44
+ def to_choices_with_bet(self, budget: int) -> "ChoicesWithBet":
45
+ inverse_ratios_sum = sum(1 / choice.odds for choice in self.root)
46
+
47
+ # 重み計算
48
+ choice_with_bet_list: list[ChoiceWithBet] = []
49
+ for chice in self.root:
50
+ weight = (1 / chice.odds) / inverse_ratios_sum
51
+ # 逆比
52
+ choice_with_bet_list.append(
53
+ ChoiceWithBet(
54
+ number=chice.number,
55
+ name=chice.name,
56
+ odds=chice.odds,
57
+ bet_amount=max(int(weight * budget / 100) * 100, 100),
58
+ )
59
+ )
60
+
61
+ return ChoicesWithBet(root=choice_with_bet_list)
62
+
63
+ @classmethod
64
+ def from_text(cls, text: str) -> "Choices":
65
+ lines = text.split("\n")
66
+ for strat_key in ("選択", "オッズ", "組み合わせ", "オッズを見て個別選択"):
67
+ if strat_key in lines:
68
+ lines = lines[lines.index(strat_key) + 1 :]
69
+ text = "\n".join(lines)
70
+ # pcの時
71
+ is_pc = re.match(r"[\d\n]+", text)
72
+ if is_pc:
73
+ return cls._from_text_from_pc(text)
74
+ # moduleの時
75
+ return cls._from_text_from_mobile(text)
76
+
77
+ @classmethod
78
+ def _from_text_from_pc(cls, text: str) -> "Choices":
79
+ choices_list: list[Choice] = []
80
+ tmp_list: list[str] = []
81
+ lines: list[str] = text.split("\n")
82
+ for line in lines:
83
+ is_odds = "." in line
84
+ if is_odds:
85
+ choices_list.append(
86
+ Choice(
87
+ number="-".join(tmp_list),
88
+ name="",
89
+ odds=float(line),
90
+ )
91
+ )
92
+ tmp_list = []
93
+ else:
94
+ if line.strip():
95
+ tmp_list.append(line.strip())
96
+ return cls(root=choices_list)
97
+
98
+ @classmethod
99
+ def _from_text_from_mobile(cls, text: str) -> "Choices":
100
+ choice_list: list[Choice] = []
101
+ lines: list[str] = text.split("\n")
102
+ for strat_key in ("選択", "オッズ", "組み合わせ", "オッズを見て個別選択"):
103
+ if strat_key in lines:
104
+ lines = lines[lines.index(strat_key) + 1 :]
105
+ if "すべてを選択" in lines:
106
+ lines = lines[: lines.index("すべてを選択")]
107
+ for i in range(len(lines) // 3):
108
+ # oddsが1つ目にある
109
+ if "." in lines[3 * i + 1]:
110
+ idx_odds = 1
111
+ idx_name = 2
112
+ else:
113
+ idx_odds = 2
114
+ idx_name = 1
115
+
116
+ choice_list.append(
117
+ Choice(
118
+ number="-".join(lines[3 * i].split()),
119
+ name="-".join(lines[3 * i + idx_name].split()),
120
+ odds=float(lines[3 * i + idx_odds]),
121
+ )
122
+ )
123
+ return cls(root=choice_list)
124
+
125
+
126
+ class ChoicesWithBet(RootModel[list[ChoiceWithBet]]):
127
+ root: list[ChoiceWithBet]
128
+
129
+ def to_summary_markdwown(self) -> str:
130
+ return (
131
+ f"**点数**: {len(self.root)} 点<br>"
132
+ f"**購入額**: ¥ {self.total_bet_amount:,}<br>"
133
+ f"**回収率**: {self.average_pay_back / self.total_bet_amount:.0%}"
134
+ )
135
+
136
+ def to_table(self) -> list[list]:
137
+ return [
138
+ [
139
+ choice.number,
140
+ choice.name,
141
+ choice.odds,
142
+ choice.bet_amount,
143
+ choice.pay_back,
144
+ ]
145
+ for choice in self.root
146
+ ]
147
+
148
+ @computed_field # type: ignore
149
+ @property
150
+ def total_bet_amount(self) -> int:
151
+ return sum(choice.bet_amount for choice in self.root)
152
+
153
+ @computed_field # type: ignore
154
+ @property
155
+ def average_pay_back(self) -> float:
156
+ return sum(choice.pay_back for choice in self.root) / len(self.root)
157
+
158
+ @computed_field # type: ignore # type: ignore
159
+ @property
160
+ def average_profit(self) -> float:
161
+ return sum(choice.profit for choice in self.root) / len(self.root)