2024,江端さんの技術メモ

まず、この2つの点は、どっちが、私(江端)が追加したノードだったのかを思い出すことにします。

上のノードは、こんな風

id番号が若いから、こっちが私が作った方で、まあ、間違いないでしょう。

念の為、もう一方も確認。

こちもID番号若いけど、以前管理した番号とは違うみたいだから、こっちが既存のノードであろう、とする。

これを私が結線した時に作った道路が、これ。

結線の情報は、source,target, source_osm, target_osmで入っているので、少なくともノード間の結線であれば、ここの加工だけで何とかなるんじゃないかな、と。

で、ここの部分のエントリーを見てみたら、こんな感じでした。(tomioka_db_c_trialの方で確認中)

gid、osm_id →新規の番号を適当に付ける
source, target, source_osm, target_osm は、これから結線するノード番号を記載する。(sourceが、江端が作成したNodeになっている)
と、まあ、ここまではいいとして、tag_idってなんだろう。あと、the_geomをどうしようかなぁ。

QGIS使ってtag_id = 112 だけを表示して調べてみたけど、私が手を入れたところに(も)出てきているようなので、何も分からないまま 112 を使うことにする。

さて、次に問題は、the_geomである。これは面倒くさい。多分デタラメな値を入れても大丈夫だとは思うが、念を入れておきたい。

tomioka_db_c=# select * from ways where gid = 506;
 gid | osm_id | tag_id |        length         |      length_m      | name | source | target | source_osm | target_osm |         cost          |     reverse_cost      |       cost_s       |   reverse_cost_s   | rule | one_way | oneway  |       x1        |      y1       |       x2        |       y2       | maxspeed_forward | maxspeed_backward | priority |                                          the_geom
-----+--------+--------+-----------------------+--------------------+------+--------+--------+------------+------------+-----------------------+-----------------------+--------------------+--------------------+------+---------+---------+-----------------+---------------+-----------------+----------------+------------------+-------------------+----------+--------------------------------------------------------------------------------------------
 506 | 105780 |    112 | 7.110994016919574e-06 | 0.7650124135935403 |      |    277 |    414 |     102352 |     102501 | 7.110994016919574e-06 | 7.110994016919574e-06 | 0.0550808937787349 | 0.0550808937787349 |      |       0 | UNKNOWN | 139.61725559089 | 35.3693678707 | 139.61725862094 | 35.36936143758 |               50 |                50 |      2.5 | 0102000020E610000002000000AA04CC8EC0736140C26C467247AF414090C32695C0736140A2674F3C47AF4140
(1 row)
で、以下のように実行して、 the_geom の内容を調べてみると
tomioka_db_c=# select ST_Astext(the_geom) from ways where gid = 506;
                                st_astext
--------------------------------------------------------------------------
 LINESTRING(139.61725559089 35.3693678707,139.61725862094 35.36936143758)
(1 row)

と、2点の座標を結ぶ直線であることが分かった。

そこで、以下のプログラムを作成してみた。

/*
c:\users\ebata\tomika3b\src\others\main31.go
go run main31.go


*/
package main

import (
	"encoding/hex"
	"fmt"

	"github.com/twpayne/go-geom"
	"github.com/twpayne/go-geom/encoding/wkb"
)

func main() {

	// 2つの緯度経度ポイント
	//coordinates := [][]float64{{139.6917, 35.6895}, {-74.006, 40.7128}}
	coordinates := [][]float64{{139.61725559089, 35.3693678707}, {139.61725862094, 35.36936143758}}
	//coordinates := [][]float64{{139.61725862094, 35.36936143758}, {139.61725559089, 35.3693678707}}

	// Geometryの作成
	lineString := geom.NewLineStringFlat(geom.XY, []float64{coordinates[0][0], coordinates[0][1], coordinates[1][0], coordinates[1][1]})

	// WKB形式に変換
	wkbBytes, err := wkb.Marshal(lineString, wkb.NDR)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// WKB形式を16進数文字列に変換
	wkbHex := hex.EncodeToString(wkbBytes)

	// 出力
	fmt.Println(wkbHex)
}

さて、このプログラムからから、
0102000020E610000002000000AA04CC8EC0736140C26C467247AF414090C32695C0736140A2674F3C47AF4140
を作れるかな?

coordinates := [][]float64{{139.61725559089, 35.3693678707}, {139.61725862094, 35.36936143758}}
としたら、
ベース: 0102000020E610000002000000AA04CC8EC0736140C26C467247AF414090C32695C0736140A2674F3C47AF4140
作成: 01020| |0000002000000aa04cc8ec0736140c26c467247af414090c32695c0736140a2674f3c47af4140
一部欠けているが、一致している。

では、逆転させてみよう。
coordinates := [][]float64{{139.61725862094, 35.36936143758}, {139.61725559089, 35.3693678707}}
としたら、
ベース: 0102000020E610000002000000AA04CC8EC0736140C26C467247AF414090C32695C0736140A2674F3C47AF4140
作成: 000000000200000002406173c08ecc04aa4041af4772466cc2406173c09526c3904041af473c4f67a2
こっちは、不一致が多いようです。

でも geomの長さが違うと思い、ChatGPTに尋ねてみたところ、以下のように言われました。

PostGISで生成されたWKB形式のデータは、ヘッダーを含んでいるため、より長いバイト列となります。一方、Go言語の標準ライブラリを使用して生成されたWKB形式のデータは、ヘッダーを省略しているため、より短いバイト列となります。

両方のWKB形式のデータが同じ構造と座標を持っている場合、データの内容は正しく解釈されます。しかし、データの長さには違いが生じることがあります。この違いは、WKB形式が異なる実装によって生成された場合に一般的です。

WKB形式のデータは、そのバイト列を解釈する際に、ヘッダーの情報を使用してジオメトリの種類と座標の次元を正しく読み取ります。そのため、データの長さの違いは問題ありません。

ということなので、長さについては無視することにしました。まあ、ダイクストラ計算をする分には問題ないでしょう

-----

では、本命。

この2点間を結線する、をやってみます。

    //source 305(×306) {139.62232489, 35.37185132}  target 1401  {139.62233160, 35.37184490} // これが今回のターゲット

    coordinates := [][]float64{{139.62232489, 35.37185132}, {139.62233160, 35.37184490}}

で計算したところ、

0102000000020000002d41e315ea736140ed2ff5d298af41408ea8f523ea7361409a571a9d98af4140
となったので、これはこのまま利用。

今回は、この2点間を繋ぐ、wayのオブジェクトを作れば良いだけなので、基本的にはwayのエントリーを一つ追加するだけで足りるはず。

#gid =506のエントリーがこんな感じなので、これをパクります。
tomioka_db_c=# select * from ways where gid = 506;
 gid | osm_id | tag_id |        length         |      length_m      | name | source | target | source_osm | target_osm |         cost          |     reverse_cost      |       cost_s       |   reverse_cost_s   | rule | one_way | oneway  |       x1        |      y1       |       x2        |       y2       | maxspeed_forward | maxspeed_backward | priority |                                          the_geom
-----+--------+--------+-----------------------+--------------------+------+--------+--------+------------+------------+-----------------------+-----------------------+--------------------+--------------------+------+---------+---------+-----------------+---------------+-----------------+----------------+------------------+-------------------+----------+--------------------------------------------------------------------------------------------
 506 | 105780 |    112 | 7.110994016919574e-06 | 0.7650124135935403 |      |    277 |    414 |     102352 |     102501 | 7.110994016919574e-06 | 7.110994016919574e-06 | 0.0550808937787349 | 0.0550808937787349 |      |       0 | UNKNOWN | 139.61725559089 | 35.3693678707 | 139.61725862094 | 35.36936143758 |               50 |                50 |      2.5 | 0102000020E610000002000000AA04CC8EC0736140C26C467247AF414090C32695C0736140A2674F3C47AF4140

(1 row)

空き番号となっているgidは1984
空き番号となっているosm_idは、現在104088(と同じ桁であれば)、104090あたりが良さそう

x1,y1,x2,y2も比較してみたところ、
source 139.61725559  35.36936787
target 139.61725862 35.36936144
X1 139.61725559089 Y1 35.3693678707
X2 139.61725862094  Y2 35.36936143758となっていたので、とりあえずx1、x1をsource に、x2、y2をtargetにしてみたでは作ってみますか
(他のところは、現在のノード(506)をパクッても大きな問題にはならないだろう、と予測)
gid | osm_id | tag_id |        length         |      length_m      | name | source | target | source_osm | target_osm |         cost          |     reverse_cost      |       cost_s       |   reverse_cost_s   | rule | one_way | oneway  |       x1        |      y1       |       x2        |       y2       | maxspeed_forward | maxspeed_backward | priority |                                          the_geom
1984| 104090 |    112 | 7.110994016919574e-06 | 0.7650124135935403 |      |    305 |    1401 |    102381 |    4095221163 | 7.110994016919574e-06 | 7.110994016919574e-06 | 0.0550808937787349 | 0.0550808937787349 |      |       0 | UNKNOWN | 139.61725559089 | 35.3693678707 | 139.61725862094 | 35.36936143758 |               50 |                50 |      2.5 | 0102000020E610000002000000AA04CC8EC0736140C26C467247AF414090C32695C0736140A2674F3C47AF4140

INSERT INTO ways (gid, osm_id, tag_id, length, length_m, name, source, target, source_osm, target_osm, cost, reverse_cost, cost_s, reverse_cost_s, rule, one_way, oneway, x1, y1, x2, y2, maxspeed_forward, maxspeed_backward, priority, the_geom)
VALUES (1984, 104090, 112, 7.110994016919574e-06, 0.7650124135935403,NULL, 305 , 1401, 102381, 4095221163, 7.110994016919574e-06, 7.110994016919574e-06, 0.0550808937787349, 0.0550808937787349, NULL, 0, 'UNKNOWN', 139.62232489, 35.37185132, 139.62233160, 35.37184490, 50, 50, 2.5, '0102000000020000002d41e315ea736140ed2ff5d298af41408ea8f523ea7361409a571a9d98af4140');

結線されたようです。

では、ちゃんとダイクストラで繋がるのかを確認してみます。

tomioka_db_c_trial=# SELECT seq, source, target, x1, y1, x2, y2 FROM pgr_dijkstra('SELECT gid as id, source, target, cost, reverse_cost FROM ways',699, 304, directed := false) a INNER JOIN ways b ON (a.edge = b.gid) ORDER BY seq;
 seq | source | target |       x1        |       y1       |       x2        |       y2
-----+--------+--------+-----------------+----------------+-----------------+----------------
   1 |    699 |   1401 |      139.622458 |     35.3721429 |     139.6223316 |     35.3718449
   2 |    305 |   1401 |    139.62232489 |    35.37185132 |     139.6223316 |     35.3718449
   3 |    304 |    305 | 139.62216476726 | 35.37185094304 | 139.62232488969 | 35.37185132369
(3 rows)
逆方向はどうかな?
tomioka_db_c_trial=# SELECT seq, source, target, x1, y1, x2, y2 FROM pgr_dijkstra('SELECT gid as id, source, target, cost, reverse_cost FROM ways',304, 699, directed := false) a INNER JOIN ways b ON (a.edge = b.gid) ORDER BY seq;
 seq | source | target |       x1        |       y1       |       x2        |       y2
-----+--------+--------+-----------------+----------------+-----------------+----------------
   1 |    304 |    305 | 139.62216476726 | 35.37185094304 | 139.62232488969 | 35.37185132369
   2 |    305 |   1401 |    139.62232489 |    35.37185132 |     139.6223316 |     35.3718449
   3 |    699 |   1401 |      139.622458 |     35.3721429 |     139.6223316 |     35.3718449
(3 rows)
繋っているを確認できました(ホッとしました)
# 正直、ダイクストラ計算の書式が気になるけど、結線に成功しているなら、まあいいや(もう疲れた)
tomioka_db_cにも、同じエントリをして、tomioka_db_c_trialを消去しました。

2024,江端さんの技術メモ

バス時刻表を手動でCSVファイルにしてから、バスの運行テーブルに書き換えるプログラム(1行分だけだけど)

を、エクセルに貼りつけて、

csvでinput.csvという名前でセーブしてから、go run main30.go で実行すると、

てな感じで、平日、土曜、休日単位のテーブル(の1行)になる。

// バス時刻表を手動でCSVファイルにしてから、バスの運行テーブルに書き換えるプログラム
// c:\users\ebata\tomioka3B\others>go run main30.go

package main

import (
	"encoding/csv"
	"fmt"
	"os"
)

func main() {
	// 入力ファイルと出力ファイルのパス
	inputFile := "input.csv"

	// CSVファイルを読み込む
	csvFile, err := os.Open(inputFile)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer csvFile.Close()

	reader := csv.NewReader(csvFile)
	records, err := reader.ReadAll()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// 出力するデータを格納するスライス
	var hour string

	for k := 1; k < 4; k++ {
		for _, row := range records {
			if row[0] != "" {
				hour = row[0]
			}
			if row[k] != "" {
				fmt.Printf("%02s:%02s,", hour, row[k])
			}
		}
		fmt.Println()
	}

}

バスの時刻表

2024,江端さんの忘備録

今、私は、「現実の街と同じ街を、コンピュータの中に作りこむ」をやっています。

I am currently working on "creating a city on the computer identical to an actual town.

詳しいことは割愛しますが、これを完成させて動かさないと、大学院を卒業できません。

I will spare you the details, but if I do not complete and move this, I will not graduate from college.

週末はこの作業で、全部の時間が溶けていきます。

This process melts away the entire weekend.

恐しく面倒くさい作業なのに、学術的には語るべき内容がない ―― 苦労談ならいくらでも語れるんですけどね。

It's a horrible and annoying process, but there's no academic content. Even if I could tell you all about the hard work.

面倒なのは、ゲームのように「自由に街を設計する」のではなく、「現実にある街を反映させる」ことです。

What is troublesome is not "freely designing a city" as in a game but "building in" a city that exists in reality.

言うまでもなく、"ワープ"とか"どこでもドア"のような架空の設定は許されません。

Fictitious settings such as "warps" or "doors to anywhere" are prohibited.

『一体、どこの誰が、こんな地図情報 ―― どう考えたって"内部情報"だよなぁ ―― を入手して、OpenStreetMapに展開したんだろう』

-----

会社の仕事でやっていた時は、もっと短期間で試作システムを作ることができたように思うのですが、今は、"亀"のような遅さです。

When I was working for the company, I thought I could create a prototype system in a much shorter time, but now it is as slow as a "turtle.

あらためて『なんで、こんなに時間がかかるんだろう』と考えてみて、当たり前の事実に気がつきました。

I thought again, 'Why is it taking so long?' and realized an obvious fact.

―― 辛い作業を、"他人"に押しつけてきたから

"I've been putting the hard work on "others"."

"他人"とは、例えば、会社の同僚や、ソフト外注さんです。

Others" are, for example, colleagues in the company or software subcontractors.

そういう人たちの圧倒的な支えがあったら、今と比較して、"光の速度"のような構築ができていた。

I had the overwhelming support of such people, so I would have been able to build at the "speed of light" compared to today.

しかし、私が大学でやっている研究対象のシステムの作り手は、私ひとりだけです。

However, I am the sole creator of the system, which is the subject of my research at the university.

そりゃ、"亀"のような速度になるのは、当然です。

Naturally, the speed would be like that of a tortoise.

私、ちょくちょく「一人でがんばっている」ような記述をしていましたが ―― 当たり前ですが ―― 一人でできることなんて、高が知れているんですよね。

I have often described myself as "working hard on my own," but there is only so much one person can do.

-----

バーチャルの街とは言え、これを一人で作るのは、本当に大変です。

Even though it is a virtual city, it is a real challenge to create this all by oneself.

この街、本当に完成するのか ―― そう考えると、怖くて眠れない日があります。

This city, will it be completed -- there are days when I can't sleep because I'm so scared to think about it.

これは、「一人でがんばっている」と思い上がっていた私への「報い」なのかもしれません。

This may be a "retaliation" for my presumption that I was doing my best on my own.

2024,江端さんの技術メモ

VSCode の Auto Markdownで目次や章番号が出てこなくなったら、とにかく、拡張機能の全部にチェックをつける

2024,江端さんの技術メモ

以下のGo言語プログラムで、"small2_bus_data.csv"が1行のみの
1, 93, 139.62957005198, 35.36604342344, 12:55:00
を使って、を読み込ませたのですが、その結果が
1 0 0 12:55:00 [{1 0001-01-01 00:00:00 +0000 UTC {0 0}}]
となってしまいます。

package main

import (
	"encoding/csv"
	"fmt"
	"os"
	"strconv"
	"time"
)

// 緯度経度の型定義
type LatLng struct {
	Lat, Lng float64
}

// 時間と緯度経度の情報を持つ構造体
type BusData struct {
	NodeID   int
	Time     time.Time
	Location LatLng
}

func main() {
	// CSVファイルを開く
	file, err := os.Open("small2_bus_data.csv")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer file.Close()

	// CSVファイルの内容をパースする
	reader := csv.NewReader(file)
	records, err := reader.ReadAll()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// データを格納するためのスライス
	var busData []BusData

	// CSVの各行を処理する
	for _, record := range records {
		nodeID, _ := strconv.Atoi(record[0])
		lng, _ := strconv.ParseFloat(record[2], 64)
		lat, _ := strconv.ParseFloat(record[3], 64)
		timeStr := record[4]

		fmt.Println(nodeID, lng, lat, timeStr)

		// 時間のパース
		var parsedTime time.Time
		if timeStr != "" {
			parsedTime, _ = time.Parse("15:04:05", timeStr)
		}

		// データを構造体に格納
		data := BusData{
			NodeID: nodeID,
			Time:   parsedTime,
			Location: LatLng{
				Lat: lat,
				Lng: lng,
			},
		}
		busData = append(busData, data)
	}

	fmt.Println(busData)
}

で、かなり、すったもんだした結果、文字列に余分なスペースが含まれていたため であることが分かりました(このくらい自動で対処して欲しいが)。

strings.TrimSpace がキモだったようです。

修正後のプログラムは以下の通り。

package main

import (
	"encoding/csv"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
)

// 緯度経度の型定義
type LatLng struct {
	Lat, Lng float64
}

// 時間と緯度経度の情報を持つ構造体
type BusData struct {
	NodeID   int
	Time     time.Time
	Location LatLng
}

func main() {
	// CSVファイルを開く
	file, err := os.Open("small2_bus_data.csv")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer file.Close()

	// CSVファイルの内容をパースする
	reader := csv.NewReader(file)
	records, err := reader.ReadAll()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// データを格納するためのスライス
	var busData []BusData

	// CSVの各行を処理する
	for _, record := range records {
		nodeID, _ := strconv.Atoi(record[0])

		// スペースをトリムしてから実数に変換
		lng, err := strconv.ParseFloat(strings.TrimSpace(record[2]), 64)
		if err != nil {
			fmt.Println("Error parsing lng:", err)
			return
		}
		lat, err := strconv.ParseFloat(strings.TrimSpace(record[3]), 64)
		if err != nil {
			fmt.Println("Error parsing lat:", err)
			return
		}
		timeStr := strings.TrimSpace(record[4]) // スペースをトリム

		fmt.Println(nodeID, lng, lat, timeStr)

		// 時間のパース
		var parsedTime time.Time
		if timeStr != "" {
			parsedTime, err = time.Parse("15:04:05", timeStr)
			if err != nil {
				fmt.Println("Error parsing time:", err)
				return
			}
		}

		// データを構造体に格納
		data := BusData{
			NodeID: nodeID,
			Time:   parsedTime,
			Location: LatLng{
				Lat: lat,
				Lng: lng,
			},
		}
		busData = append(busData, data)
	}

	fmt.Println(busData)
}

出力結果は
>go run main28.go
1 139.62957005198 35.36604342344 12:55:00
[{1 0000-01-01 12:55:00 +0000 UTC {35.36604342344 139.62957005198}}]
となり、一安心です。

2024,江端さんの技術メモ

3次元を取り扱うDBSCANプログラム

このプログラムでは、同一座標は1つの座標として纏められてしまって、異なるユーザの座標として取り扱ってくれません。

これに対応するために修正したプログラムは以下の通りです。

// ~/tomioka_school/src/trip_school/dbscan_3d_2.go



package main

import (
	"fmt"
	"math"
)

// Point represents a 3D point with coordinates x, y, and t
type Point struct {
	User string
	X, Y, T float64
}

// DistanceTo calculates the Euclidean distance between two 3D points
func (p Point) DistanceTo(other Point) float64 {
	dx := p.X - other.X
	dy := p.Y - other.Y
	dt := p.T - other.T
	return math.Sqrt(dx*dx + dy*dy + dt*dt)
}

// Cluster represents a cluster of points
type Cluster struct {
	Points []Point
}

// DBSCAN performs density-based clustering of 3D points
func DBSCAN(points []Point, epsilon float64, minPts int) []Cluster {
	var clusters []Cluster
	var visited = make(map[string]bool)

	for _, point := range points {
		pointKey := fmt.Sprintf("%s,%f,%f,%f", point.User, point.X, point.Y, point.T)
		if visited[pointKey] {
			continue
		}
		visited[pointKey] = true

		neighbours := getNeighbours(points, point, epsilon)
		if len(neighbours) < minPts {
			continue
		}

		var clusterPoints []Point
		expandCluster(&clusterPoints, points, visited, point, neighbours, epsilon, minPts)
		clusters = append(clusters, Cluster{Points: clusterPoints})
	}

	return clusters
}

// getNeighbours returns all points within distance epsilon of the given point
func getNeighbours(points []Point, point Point, epsilon float64) []Point {
	var neighbours []Point
	for _, other := range points {
		if point.DistanceTo(other) <= epsilon {
			neighbours = append(neighbours, other)
		}
	}
	return neighbours
}

// expandCluster expands the cluster from the given point
func expandCluster(cluster *[]Point, points []Point, visited map[string]bool, point Point, neighbours []Point, epsilon float64, minPts int) {
	*cluster = append(*cluster, point)
	for _, neighbour := range neighbours {
		neighbourKey := fmt.Sprintf("%s,%f,%f,%f", neighbour.User, neighbour.X, neighbour.Y, neighbour.T)
		if !visited[neighbourKey] {
			visited[neighbourKey] = true
			neighbourNeighbours := getNeighbours(points, neighbour, epsilon)
			if len(neighbourNeighbours) >= minPts {
				expandCluster(cluster, points, visited, neighbour, neighbourNeighbours, epsilon, minPts)
			}
		}
	}
}

func main() {
	points := []Point{
		{"A", 1, 2, 0},
		{"A", 1.5, 1.8, 1},
		{"A", 5, 8, 2},
		{"A", 8, 8, 3},
		{"A", 1, 0.6, 4},
		{"A", 9, 11, 5},
		{"A", 8, 2, 6},
		{"A", 10, 2, 7},
		{"A", 9, 3, 8},
		{"B", 1, 2, 0},
		{"B", 1.5, 1.8, 1},
		{"B", 5, 8, 2},
		{"B", 8, 8, 3},
		{"B", 1, 0.6, 4},
		{"B", 9, 11, 5},
		{"B", 8, 2, 6},
		{"B", 10, 2, 7},
		{"B", 9, 3, 8},
		{"C", 1, 2, 0},
		{"C", 1.5, 1.8, 1},
		{"C", 5, 8, 2},
		{"C", 8, 8, 3},
		{"C", 1, 0.6, 4},
		{"C", 9, 11, 5},
		{"C", 8, 2, 6},
		{"C", 10, 2, 7},
		{"C", 9, 3, 8},
	}

	// epsilon := 3.0
	// minPts := 5

	epsilon := 2.5
	minPts := 5


	clusters := DBSCAN(points, epsilon, minPts)
	fmt.Println("Combined Clusters:")
	for i, cluster := range clusters {
		fmt.Printf("Cluster %d:\n", i+1)
		for _, point := range cluster.Points {
			fmt.Printf("  (%s, %.2f, %.2f, %.2f)\n", point.User, point.X, point.Y, point.T)
		}
	}
}

出力結果は以下の通りです。
C:\Users\ebata\tomioka_school\src\trip_school>go run dbscan_3d_2.go
Combined Clusters:
Cluster 1:
(A, 1.00, 2.00, 0.00)
(A, 1.50, 1.80, 1.00)
(B, 1.00, 2.00, 0.00)
(B, 1.50, 1.80, 1.00)
(C, 1.00, 2.00, 0.00)
(C, 1.50, 1.80, 1.00)
Cluster 2:
(A, 8.00, 2.00, 6.00)
(A, 10.00, 2.00, 7.00)
(A, 9.00, 3.00, 8.00)
(B, 8.00, 2.00, 6.00)
(B, 10.00, 2.00, 7.00)
(B, 9.00, 3.00, 8.00)
(C, 8.00, 2.00, 6.00)
(C, 10.00, 2.00, 7.00)
(C, 9.00, 3.00, 8.00)

2024,江端さんの技術メモ

// NormalizeCoordinates 京急富岡駅を基準として正規化された緯度と経度を返す関数
// C:\Users\ebata\tomioka_school\src\trip_school\NormalizeCoordinates.go

package main

import (
	"fmt"
	"math"
)


func NormalizeCoordinates(referenceLat, referenceLng, lat, lng float64) (float64, float64) {
	// 1度あたりの緯度経度のメートル換算
	metersPerDegreeLat := 111319.9 // 緯度1度あたりのメートル数
	metersPerDegreeLng := 111319.9 * math.Cos(referenceLat*(math.Pi/180.0)) // 経度1度あたりのメートル数

	// 緯度と経度の差を計算
	deltaLat := lat - referenceLat
	deltaLng := lng - referenceLng

	// 正規化された緯度と経度を計算
	normalizedLat := deltaLat * metersPerDegreeLat / 100.0
	normalizedLng := deltaLng * metersPerDegreeLng / 100.0

	return normalizedLat, normalizedLng
}

func main() {
	// 京急富岡駅の緯度経度

	keikyuTomiokaLat := 35.367131726654705
	keikyuTomiokaLng := 139.62988318023088

	// 確認用の緯度経度(富岡駅のバス停)

	lat := 35.36605614545459
	lng := 139.6295094178281

	// 緯度経度の正規化
	normalizedLat, normalizedLng := NormalizeCoordinates(keikyuTomiokaLat, keikyuTomiokaLng, lat, lng)

	fmt.Printf("正規化された緯度: %.6f\n", normalizedLat)
	fmt.Printf("正規化された経度: %.6f\n", normalizedLng)
}

2024,江端さんの技術メモ

// NormalizeTime 2つの日時文字列の時間差を秒単位で計算し、600秒を1として正規化する関数
// C:\Users\ebata\tomioka_school\src\trip_school\NormalizeTime.go

package main

import (
	"fmt"
	"time"
)

func NormalizeTime(timeStr1, timeStr2 string) float64 {
	layout := "2006-01-02 15:04:05"
	t1, err := time.Parse(layout, timeStr1)
	if err != nil {
		fmt.Println("Error parsing time string 1:", err)
		return 0
	}

	t2, err := time.Parse(layout, timeStr2)
	if err != nil {
		fmt.Println("Error parsing time string 2:", err)
		return 0
	}

	duration := t2.Sub(t1).Seconds()
	normalizedDuration := duration / 600.0

	return normalizedDuration
}

func main() {
	timeStr1 := "2024-01-01 00:00:00"
	timeStr2 := "2024-02-11 23:45:00"

	normalized := NormalizeTime(timeStr1, timeStr2)
	fmt.Println("正規化された時間差:", normalized)
}

2024,江端さんの技術メモ

/*
	c:\users\ebata\dscan\dscan3.go

	DBSCANアルゴリズムを実装し、3次元空間のデータ(x、y、t)をクラスタリングするシンプルなプログラムです。
	このプログラムでは、各データポイントは3次元座標(x、y、t)で表され、距離の計算にはユークリッド距離を使用します。

	DBSCANをk-meansの違いで説明します。

	DBSCAN(Density-Based Spatial Clustering of Applications with Noise)とk-meansは、クラスタリングアルゴリズムですが、そのアプローチや動作原理にはいくつかの違いがあります。

	(1)クラスタリング方法:

	- DBSCAN: 密度ベースのクラスタリングアルゴリズムであり、データポイントの密度に基づいてクラスタを形成します。各点は、一定の距離(ε、epsilon)内に最小限の近傍点数(minPts)が存在する場合、その点を中心としたクラスタが形成されます。
	- k-means: 距離ベースのクラスタリングアルゴリズムであり、データポイントの距離に基づいてクラスタを形成します。クラスタの数(k)を事前に指定し、各点を最も近いセントロイド(クラスタの中心)に割り当てます。

	(2)クラスタの形状:

	- DBSCAN: クラスタの形状は任意であり、密度の高い領域に基づいて形成されます。したがって、DBSCANは非凸形状のクラスタを処理できます。
	- k-means: クラスタの形状は円形(球形)であり、各クラスタのセントロイドからの距離に基づいて決定されます。したがって、k-meansは凸形状のクラスタを前提としています。

	(3)ハイパーパラメータ:

	- DBSCAN: ε(epsilon)とminPtsの2つのハイパーパラメータを必要とします。εは近傍点の距離の閾値を定義し、minPtsはクラスタと見なすための最小の近傍点数を指定します
	- k-means: クラスタの数(k)を指定する必要があります。

	(4)ノイズの処理:

	- DBSCAN: ノイズポイントを自動的に検出し、外れ値として扱います。密度が低い領域に存在するポイントは、任意のクラスタに割り当てられず、ノイズとして扱われます。
	-k-means: 外れ値やノイズの処理を明示的に行いません。各点は必ずどれかのクラスタに割り当てられます。

	 要するに、DBSCANは密度ベースのアルゴリズムであり、任意の形状のクラスタを検出し、ノイズを処理する能力があります。一方、k-meansは距離ベースのアルゴリズムであり、クラスタの形状が円形であることを前提としています。


*/

package main

import (
	"fmt"
	"math"
)

// Point represents a 3D point with coordinates x, y, and t
type Point struct {
	X, Y, T float64
}

// DistanceTo calculates the Euclidean distance between two 3D points
func (p Point) DistanceTo(other Point) float64 {
	dx := p.X - other.X
	dy := p.Y - other.Y
	dt := p.T - other.T
	return math.Sqrt(dx*dx + dy*dy + dt*dt)
}

// Cluster represents a cluster of points
type Cluster struct {
	Points []Point
}

// DBSCAN performs density-based clustering of 3D points
func DBSCAN(points []Point, epsilon float64, minPts int) []Cluster {
	var clusters []Cluster
	var visited = make(map[Point]bool)

	for _, point := range points {
		if visited[point] {
			continue
		}
		visited[point] = true

		neighbours := getNeighbours(points, point, epsilon)
		if len(neighbours) < minPts {
			continue
		}

		var clusterPoints []Point
		expandCluster(&clusterPoints, points, visited, point, neighbours, epsilon, minPts)
		clusters = append(clusters, Cluster{Points: clusterPoints})
	}

	return clusters
}

// getNeighbours returns all points within distance epsilon of the given point
func getNeighbours(points []Point, point Point, epsilon float64) []Point {
	var neighbours []Point
	for _, other := range points {
		if point.DistanceTo(other) <= epsilon {
			neighbours = append(neighbours, other)
		}
	}
	return neighbours
}

// expandCluster expands the cluster from the given point
func expandCluster(cluster *[]Point, points []Point, visited map[Point]bool, point Point, neighbours []Point, epsilon float64, minPts int) {
	*cluster = append(*cluster, point)
	for _, neighbour := range neighbours {
		if !visited[neighbour] {
			visited[neighbour] = true
			neighbourNeighbours := getNeighbours(points, neighbour, epsilon)
			if len(neighbourNeighbours) >= minPts {
				expandCluster(cluster, points, visited, neighbour, neighbourNeighbours, epsilon, minPts)
			}
		}
		// Add neighbour to cluster if not already in another cluster
		var isInCluster bool
		for _, c := range *cluster {
			if c == neighbour {
				isInCluster = true
				break
			}
		}
		if !isInCluster {
			*cluster = append(*cluster, neighbour)
		}
	}
}

func main() {
	// Example usage
	points := []Point{
		{X: 1, Y: 2, T: 0},
		{X: 1.5, Y: 1.8, T: 1},
		{X: 5, Y: 8, T: 2},
		{X: 8, Y: 8, T: 3},
		{X: 1, Y: 0.6, T: 4},
		{X: 9, Y: 11, T: 5},
		{X: 8, Y: 2, T: 6},
		{X: 10, Y: 2, T: 7},
		{X: 9, Y: 3, T: 8},
	}

	epsilon := 3.0
	minPts := 2

	clusters := DBSCAN(points, epsilon, minPts)
	for i, cluster := range clusters {
		fmt.Printf("Cluster %d:\n", i+1)
		for _, point := range cluster.Points {
			fmt.Printf("  (%.2f, %.2f, %.2f)\n", point.X, point.Y, point.T)
		}
	}
}