秋叶无缘
浅析MySQL代价模型:告别盲目使用EXPLAIN,提前预知索引优化策略
背景在 MySQL 中,当我们为表创建了一个或多个索引后,通常需要在索引定义完成后,根据具体的数据情况执行 EXPLAIN 命令,才能观察到数据库实际使用哪个索引、是否使用索引。这使得我们在添加新索引之前,无法提前预知数据库是否能使用期望的索引。更为糟糕的是,有时甚至在添加新的索引后,数据库在某些查询中会使用它,而在其他查询中则不会使用,这种情况下,我们无法确定索引是否发挥了预期的作用,让人感到非常苦恼。这种情况基本上意味着 MySQL 并没有为我们选择最优的索引,而我们不得不在茫茫数据中摸索,试图找到问题的症结所在。我们可能会尝试调整索引,甚至删除索引,然后重新添加,希望 MySQL 能从中找到最优的索引选择。然而,这样的过程既耗时又费力,而且往往收效甚微。如果在添加索引之前,我们能够预知索引的使用情况,那么对于表设计将大有裨益。我们可以在设计表结构时,更加明确地知道应该选择哪些索引,如何优化索引,以提高查询效率。我们不再需要依赖盲目尝试和猜测,而是可以基于实际的数据和查询情况,做出更加明智的决策。因此,对于 MySQL 用户来说,能够预知索引走势的需求非常迫切。我们希望能有一种方法,能够让我们在添加索引之前,就清楚地了解 MySQL 将如何使用索引,以便我们能够更好地优化表结构,提高查询效率。这将极大地减轻我们的工作负担,提高我们的工作效率,让我们能够更加专注于业务逻辑的处理,而不是在索引的海洋中挣扎。为了解决这个问题,我们可以深入研究 MySQL 的索引选择机制。实际上,这个机制的核心就是代价模型,它通过一个公式来决定索引的选择策略。相对于 MySQL 其他复杂的概念,代价模型实现起来要简单得多。熟悉代价模型之后,我们可以预先了解 MySQL 在执行查询时会如何选择索引,从而更有效地进行索引优化。在接下来的文章中,我将结合近期进行索引优化的具体案例,来详细解释如何运用代价模型来优化索引。MySQL代价模型浅析MySQL数据库主要由4层组成:连接层:客户端和连接服务,主要完成一些类似于连接处理、授权管理、以及相关的安全方案。服务层:主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化以及内部函数的执行。引擎层:负责MySQL中数据的存储和提取,服务器通过AP1与存储引擎进行通信。存储层:将数据存储文件系统上,并完成与存储引擎的交互。索引策略选择在SQL优化器进行的SQL 优化器会分析所有可能的执行计划,选择成本最低的执行,这种优化器称之为:CBO(Cost-based Optimizer,基于成本的优化器)。Cost = Server Cost + Engine Cost = CPU Cost + IO Cost其中,CPU Cost 表示计算的开销,比如索引键值的比较、记录值的比较、结果集的排序 ...... 这些操作都在 Server 层完成;IO Cost 表示引擎层 IO 的开销,MySQL 可以通过区分一张表的数据是否在内存中,分别计算读取内存 IO 开销以及读取磁盘 IO 的开销。源码简读MySQL的数据源代码采用了5.7.22版本,后续的代价计算公式将基于此版本进行参考。opt_costconstants.cc【代价模型——计算所需代价计算系数】/*
在Server_cost_constants类中定义为静态常量变量的成本常量的值。如果服务器管理员没有在server_cost表中添加新值,则将使用这些默认成本常数值。
5.7版本开始可用从数据库加载常量值,该版本前使用代码中写的常量值
*/
// 计算符合条件的⾏的代价,⾏数越多,此项代价越⼤
const double Server_cost_constants::ROW_EVALUATE_COST= 0.2;
// 键⽐较的代价,例如排序
const double Server_cost_constants::KEY_COMPARE_COST= 0.1;
/*
内存临时表的创建代价
通过基准测试,创建Memory临时表的成本与向表中写入10行的成本一样高。
*/
const double Server_cost_constants::MEMORY_TEMPTABLE_CREATE_COST= 2.0;
// 内存临时表的⾏代价
const double Server_cost_constants::MEMORY_TEMPTABLE_ROW_COST= 0.2;
/*
内部myisam或innodb临时表的创建代价
创建MyISAM表的速度是创建Memory表的20倍。
*/
const double Server_cost_constants::DISK_TEMPTABLE_CREATE_COST= 40.0;
/*
内部myisam或innodb临时表的⾏代价
当行数大于1000时,按顺序生成MyISAM行比生成Memory行慢2倍。然而,没有非常大的表的基准,因此保守地将此系数设置为慢5倍(即成本为1.0)。
*/
const double Server_cost_constants::DISK_TEMPTABLE_ROW_COST= 1.0;
/*
在SE_cost_constants类中定义为静态常量变量的成本常量的值。如果服务器管理员没有在engine_cost表中添加新值,则将使用这些默认成本常数值。
*/
// 从主内存缓冲池读取块的成本
const double SE_cost_constants::MEMORY_BLOCK_READ_COST= 1.0;
// 从IO设备(磁盘)读取块的成本
const double SE_cost_constants::IO_BLOCK_READ_COST= 1.0;opt_costmodel.cc【代价模型——部分涉及方法】double Cost_model_table::page_read_cost(double pages) const
{
DBUG_ASSERT(m_initialized);
DBUG_ASSERT(pages >= 0.0);
// 估算聚集索引内存中页面数占其所有页面数的比率
const double in_mem= m_table->file->table_in_memory_estimate();
const double pages_in_mem= pages * in_mem;
const double pages_on_disk= pages - pages_in_mem;
DBUG_ASSERT(pages_on_disk >= 0.0);
const double cost= buffer_block_read_cost(pages_in_mem) +
io_block_read_cost(pages_on_disk);
return cost;
}
double Cost_model_table::page_read_cost_index(uint index, double pages) const
{
DBUG_ASSERT(m_initialized);
DBUG_ASSERT(pages >= 0.0);
double in_mem= m_table->file->index_in_memory_estimate(index);
const double pages_in_mem= pages * in_mem;
const double pages_on_disk= pages - pages_in_mem;
const double cost= buffer_block_read_cost(pages_in_mem) +
io_block_read_cost(pages_on_disk);
return cost;
}handler.cc【代价模型——部分涉及方法】// 聚集索引扫描IO代价计算公式
Cost_estimate handler::read_cost(uint index, double ranges, double rows)
{
DBUG_ASSERT(ranges >= 0.0);
DBUG_ASSERT(rows >= 0.0);
const double io_cost= read_time(index, static_cast<uint>(ranges),
static_cast<ha_rows>(rows)) *
table->cost_model()->page_read_cost(1.0);
Cost_estimate cost;
cost.add_io(io_cost);
return cost;
}
// 表全量扫描代价相关计算(IO-cost)
Cost_estimate handler::table_scan_cost()
{
const double io_cost= scan_time() * table->cost_model()->page_read_cost(1.0);
Cost_estimate cost;
cost.add_io(io_cost);
return cost;
}
// 覆盖索引扫描代价相关计算
Cost_estimate handler::index_scan_cost(uint index, double ranges, double rows)
{
DBUG_ASSERT(ranges >= 0.0);
DBUG_ASSERT(rows >= 0.0);
const double io_cost= index_only_read_time(index, rows) *
table->cost_model()->page_read_cost_index(index, 1.0);
Cost_estimate cost;
cost.add_io(io_cost);
return cost;
}
/**
估算在指定 keynr索引进行覆盖扫描(不需要回表),扫描 records条记录,需要读取的索引页面数
@param keynr Index number
@param records Estimated number of records to be retrieved
@return
Estimated cost of 'index only' scan
*/
double handler::index_only_read_time(uint keynr, double records)
{
double read_time;
uint keys_per_block= (stats.block_size/2/
(table_share->key_info[keynr].key_length + ref_length) +
1);
read_time=((double) (records + keys_per_block-1) /
(double) keys_per_block);
return read_time;
}sql_planner.cc【用于ref访问类型索引费用计算】
double tmp_fanout= 0.0;
if (table->quick_keys.is_set(key) && !table_deps && //(C1)
table->quick_key_parts[key] == cur_used_keyparts && //(C2)
table->quick_n_ranges[key] == 1+MY_TEST(ref_or_null_part)) //(C3)
{
tmp_fanout= cur_fanout= (double) table->quick_rows[key];
}
else
{
// Check if we have statistic about the distribution
if (keyinfo->has_records_per_key(cur_used_keyparts - 1))
{
cur_fanout= keyinfo->records_per_key(cur_used_keyparts - 1);
if (!table_deps && table->quick_keys.is_set(key) && // (1)
table->quick_key_parts[key] > cur_used_keyparts) // (2)
{
trace_access_idx.add("chosen", false)
.add_alnum("cause", "range_uses_more_keyparts");
is_dodgy= true;
continue;
}
tmp_fanout= cur_fanout;
}
else
{
rec_per_key_t rec_per_key;
if (keyinfo->has_records_per_key(
keyinfo->user_defined_key_parts - 1))
rec_per_key=
keyinfo->records_per_key(keyinfo->user_defined_key_parts - 1);
else
rec_per_key=
rec_per_key_t(tab->records()) / distinct_keys_est + 1;
if (tab->records() == 0)
tmp_fanout= 0.0;
else if (rec_per_key / tab->records() >= 0.01)
tmp_fanout= rec_per_key;
else
{
const double a= tab->records() * 0.01;
if (keyinfo->user_defined_key_parts > 1)
tmp_fanout=
(cur_used_keyparts * (rec_per_key - a) +
a * keyinfo->user_defined_key_parts - rec_per_key) /
(keyinfo->user_defined_key_parts - 1);
else
tmp_fanout= a;
set_if_bigger(tmp_fanout, 1.0);
}
cur_fanout= (ulong) tmp_fanout;
}
if (ref_or_null_part)
{
// We need to do two key searches to find key
tmp_fanout*= 2.0;
cur_fanout*= 2.0;
}
if (table->quick_keys.is_set(key) &&
table->quick_key_parts[key] <= cur_used_keyparts &&
const_part &
((key_part_map)1 << table->quick_key_parts[key]) &&
table->quick_n_ranges[key] == 1 + MY_TEST(ref_or_null_part &
const_part) &&
cur_fanout > (double) table->quick_rows[key])
{
tmp_fanout= cur_fanout= (double) table->quick_rows[key];
}
}
······
······
// Limit the number of matched rows
const double tmp_fanout=
min(cur_fanout, (double) thd->variables.max_seeks_for_key);
if (table->covering_keys.is_set(key)
|| (table->file->index_flags(key, 0, 0) & HA_CLUSTERED_INDEX))
{
// We can use only index tree
const Cost_estimate index_read_cost=
table->file->index_scan_cost(key, 1, tmp_fanout);
cur_read_cost= prefix_rowcount * index_read_cost.total_cost();
}
else if (key == table->s->primary_key &&
table->file->primary_key_is_clustered())
{
const Cost_estimate table_read_cost=
table->file->read_cost(key, 1, tmp_fanout);
cur_read_cost= prefix_rowcount * table_read_cost.total_cost();
}
else
cur_read_cost= prefix_rowcount *
min(table->cost_model()->page_read_cost(tmp_fanout),
tab->worst_seeks);handler.cc【用于range访问类型索引费用计算】handler::multi_range_read_info_const(uint keyno, RANGE_SEQ_IF *seq,
void *seq_init_param, uint n_ranges_arg,
uint *bufsz, uint *flags,
Cost_estimate *cost)
{
KEY_MULTI_RANGE range;
range_seq_t seq_it;
ha_rows rows, total_rows= 0;
uint n_ranges=0;
THD *thd= current_thd;
/* Default MRR implementation doesn't need buffer */
*bufsz= 0;
DBUG_EXECUTE_IF("bug13822652_2", thd->killed= THD::KILL_QUERY;);
seq_it= seq->init(seq_init_param, n_ranges, *flags);
while (!seq->next(seq_it, &range))
{
if (unlikely(thd->killed != 0))
return HA_POS_ERROR;
n_ranges++;
key_range *min_endp, *max_endp;
if (range.range_flag & GEOM_FLAG)
{
min_endp= &range.start_key;
max_endp= NULL;
}
else
{
min_endp= range.start_key.length? &range.start_key : NULL;
max_endp= range.end_key.length? &range.end_key : NULL;
}
int keyparts_used= 0;
if ((range.range_flag & UNIQUE_RANGE) && // 1)
!(range.range_flag & NULL_RANGE))
rows= 1; /* there can be at most one row */
else if ((range.range_flag & EQ_RANGE) && // 2a)
(range.range_flag & USE_INDEX_STATISTICS) && // 2b)
(keyparts_used= my_count_bits(range.start_key.keypart_map)) &&
table->
key_info[keyno].has_records_per_key(keyparts_used-1) && // 2c)
!(range.range_flag & NULL_RANGE))
{
rows= static_cast<ha_rows>(
table->key_info[keyno].records_per_key(keyparts_used - 1));
}
else
{
DBUG_EXECUTE_IF("crash_records_in_range", DBUG_SUICIDE(););
DBUG_ASSERT(min_endp || max_endp);
if (HA_POS_ERROR == (rows= this->records_in_range(keyno, min_endp,
max_endp)))
{
/* Can't scan one range => can't do MRR scan at all */
total_rows= HA_POS_ERROR;
break;
}
}
total_rows += rows;
}
if (total_rows != HA_POS_ERROR)
{
const Cost_model_table *const cost_model= table->cost_model();
/* The following calculation is the same as in multi_range_read_info(): */
*flags|= HA_MRR_USE_DEFAULT_IMPL;
*flags|= HA_MRR_SUPPORT_SORTED;
DBUG_ASSERT(cost->is_zero());
if (*flags & HA_MRR_INDEX_ONLY)
*cost= index_scan_cost(keyno, static_cast<double>(n_ranges),
static_cast<double>(total_rows));
else
*cost= read_cost(keyno, static_cast<double>(n_ranges),
static_cast<double>(total_rows));
cost->add_cpu(cost_model->row_evaluate_cost(
static_cast<double>(total_rows)) + 0.01);
}
return total_rows;
}
验证公式创建验证需要的表CREATE TABLE `store_goods_center`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`sku_id` bigint(20) NOT NULL COMMENT '商品skuid',
`station_no` varchar(20) NOT NULL COMMENT '门店编号',
`org_code` bigint(20) NOT NULL COMMENT '商家编号',
`extend_field` text COMMENT '扩展字段',
`version` int(11) DEFAULT '0' COMMENT '版本号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_pin` varchar(50) DEFAULT '' COMMENT '创建人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`update_pin` varchar(50) DEFAULT '' COMMENT '更新人',
`yn` tinyint(4) DEFAULT '0' COMMENT '删除标示 0:正常 1:删除',
`ts` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '时间戳',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_storegoods` (`station_no`, `sku_id`) USING BTREE,
KEY `idx_storegoods_org` (`org_code`, `sku_id`, `station_no`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_station_no_and_id` (`station_no`, `id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='门店商品关系表';通过存储过程初始化测试数据DELIMITER //
CREATE PROCEDURE callback()
BEGIN
DECLARE num INT;
SET num = 1;
WHILE
num <= 100000 DO
INSERT INTO store_goods_center(sku_id, station_no, org_code) VALUES (num + 10000000, floor(50+rand()*(100-50+1)), num);
SET num = num + 1;
END WHILE;
END;执行存储过程生成数据CALL callback();1.全表扫描计算代价公式计算过程:// 不同引擎计算方式有所区别
// innodb引擎实现handler.h
// 预估记录数:ha_innobase::info_low
// 页数量:ha_innobase::scan_time【数据总大小(字节) / 页大小】
// 查询全表数据大小(7880704)
SHOW TABLE STATUS LIKE 'store_goods_center';
// 查询数据库页大小(默认:16384)
SHOW VARIABLES LIKE 'innodb_page_size';
// 全表扫描计算代价
// 页数量
page = 数据总大小(字节) / 页大小 = 7880704 / 16384 = 481;
// 预估范围行数(总数据条数:10万,预估数据条数:99827,有一定误差)
records = 99827;
// 计算总代价
// 481 * 1 中的系数1 代表从主内存缓冲池读取块的成本(SE_cost_constants::IO_BLOCK_READ_COST= 1.0)
// 99827 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2)
cost = IO-cost + CPU-cost = (481 * 1) + (99827 * 0.2) = 481 + 19965.4 = 20446.4验证结果:explain format = json
select * from store_goods_center;
"cost_info": {"query_cost": "20446.40"}总结公式:全表扫描代价 = 数据总大小 / 16384 + 预估范围行数 * 0.22.覆盖索引扫描计算代价公式计算过程:// 查询全表数据大小(7880704)
SHOW TABLE STATUS LIKE 'store_goods_center';
// 查询数据库页大小(默认:16384)
SHOW VARIABLES LIKE 'innodb_page_size';
// 预估范围行数(总数据条数:1999,预估数据条数:1999,有一定误差) 1999;
records = 1999
// keys_per_block计算
// block_size是文件的block大小,mysql默认为16K;
// key_len是索引的键长度;
// ref_len是主键索引的长度;
keys_per_block = (stats.block_size / 2 / (table_share->key_info[keynr].key_length + ref_length) + 1);
// table_share->key_info[keynr].key_length 为联合索引,分别是station_no和sku_id
// station_no 为varchar(20)且为utf8mb4,长度 = 20 * 4 + 2 (可变长度需要加2) = 82
// sku_id bigint类型,长度为8
// 主键索引为bigint类型,长度为8
keys_per_block = 16384 / 2 / (82 + 8 + 8) + 1 ≈ 84
// 计算总代价
read_time = ((double) (records + keys_per_block - 1) / (double) keys_per_block);
read_time = (1999 + 84 - 1) / 84 = 24.78;
// 计算总代价
// 24.78 * 1 中的系数1 代表从主内存缓冲池读取块的成本(SE_cost_constants::IO_BLOCK_READ_COST= 1.0)
// 1999 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2)
cost = IO-cost + CPU-cost = (24.78 * 1) + (1999 * 0.2) = 24.78 + 399.8 = 424.58验证结果:explain format = json
select station_no from store_goods_center where station_no = '53';
"cost_info": {"query_cost": "424.58"}总结公式:keys_per_block = 8192 / 索引长度 + 1
覆盖索引扫描代价 = (records + keys_per_block - 1) / keys_per_block + 预估范围行数 * 0.2
公式简化(去除影响较小的复杂计算)
覆盖索引扫描代价 = (records * 涉及索引长度) / 8192 + 预估范围行数 * 0.23.ref索引扫描计算代价公式计算过程:// cardinality = 49(基数,即有多少个不同key统计。)
SHOW TABLE STATUS LIKE 'store_goods_center';
// 页数量
page = 数据总大小(字节) / 页大小 = 7880704 / 16384 = 481;
// 计算代价最低索引(sql_planner.cc 中find_best_ref函数)
// IO COST最坏不会超过全表扫描IO消耗的3倍(或者总记录数除以10)
// 其中s->found_records表示表上的记录数,s->read_time在innodb层表示page数
// s-> worst_seeks = min((double) s -> found_records / 10, (double) s -> read_time * 3);
// cur_read_cost= prefix_rowcount * min(table->cost_model() -> page_read_cost(tmp_fanout), tab -> worst_seeks);
// 预估范围行数(总数据条数:10万,预估数据条数:99827,有一定误差)
total_records = 99827;
// 预估范围行数(总数据条数:1999,预估数据条数:1999,有一定误差) 1999;
records = 1999
// 计算总代价
// 1999 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2)
// s-> worst_seeks = min((double) s -> found_records / 10, (double) s -> read_time * 3) -> min(99827 / 10, 481 * 3) = 481 * 3
// min(table->cost_model() -> page_read_cost(tmp_fanout), tab -> worst_seeks) -> min(page_read_cost(1999), 481 * 3) = 481 * 3
cost = IO-cost + CPU-cost = 481 * 3 + (1999 * 0.2) = 1443 + 399.8 = 1842.80验证结果:explain format = json
select * from store_goods_center where station_no = '53';
"cost_info": {"query_cost": "1842.80"}总结公式:下面3个公式,取值最低的
1.(数据总大小 / 16384) * 3 + 预估范围行数 * 0.2
2.总记录数 / 10 + 预估范围行数 * 0.2
3.扫描出记录数 + 预估范围行数 * 0.24.range索引扫描计算代价公式
// 预估范围行数(总数据条数:1299,预估数据条数:1299,有一定误差) 1299;
records = 1299
// 计算代价最低索引(handler.cc 中 multi_range_read_info_const 函数)
// 计算总代价
// 1299 * 0.2 计算公式:cost_model->row_evaluate_cost(static_cast<double>(total_rows))
// + 0.01 计算公式:cost->add_cpu(cost_model->row_evaluate_cost(static_cast<double>(total_rows)) + 0.01);
// 1299 + 1 中的 +1 :单个扫描区间( id > 35018 )
// 1299 + 1 计算公式:*cost= read_cost(keyno, static_cast<double>(n_ranges), static_cast<double>(total_rows));
// (1299 * 0.2 + 0.01 + 1299) * 1 中的系数1 代表从主内存缓冲池读取块的成本(SE_cost_constants::IO_BLOCK_READ_COST= 1.0)
// 1299 * 0.2 中的系数0.2 代表计算符合条件的⾏的代价(ROW_EVALUATE_COST= 0.2)
cost = IO-cost + CPU-cost = ((1299 * 0.2 + 0.01 + 1299 + 1) * 1) + (1299 * 0.2) = 1559.81 + 259.8 = 1819.61
验证结果:explain format = json
select * from store_goods_center where station_no = '53' and id > 35018;
"cost_info": {"query_cost": "1819.61"}总结公式:range扫描代价 = 预估范围行数 * 1.4 + 0.01 + 范围数
公式简化(去除影响较小的复杂计算)
range扫描代价 = 预估范围行数 * 1.4索引冲突案例门店商品系统中主要存储门店与商品的关联信息,并为B端提供根据门店ID查询关联商品的功能。由于门店关联的商品数据量较大,需要分页查询关联商品数据。为避免深分页问题,我们选择基于上次最新主键进行查询(核心思想:通过主键索引,每次定位到ID所在位置,然后往后遍历N个数据。这样,无论数据量多少,查询性能都能保持稳定。我们将所有数据根据主键ID进行排序,然后分批次取出,将当前批次的最大ID作为下次查询的筛选条件)。select 字段1,字段2 ... from store_goods_center where station_no = ‘门店id’ and id > 上次查询最大id order by id asc为了确保门店与商品组合的唯一性,我们在MySQL表中为门店ID和商品ID添加了组合唯一索引【UNIQUE KEY uniq_storegoods (station_no, sku_id) USING BTREE】。由于该索引包含门店ID并且在联合索引的第一个位置,查询会使用该索引。但是,当分页查询命中该索引后,由于排序字段无法使用索引,产生了【Using filesort】,导致门店商品系统出现了一些慢查询。为了解决这个问题,我们对慢查询进行了优化,优化思路是创建一个新的索引,使该SQL可以使用索引的排序来规避【Using filesort】的负面影响,新添加的索引为【KEY idx_station_no_and_id (station_no, id)】。添加该索引后,效果立竿见影。然而,我们发现仍然有慢查询产生,并且这些慢查询仍然使用uniq_storegoods索引,而不是idx_station_no_and_id索引。我们开始思考,为什么MySQL没有为我们的系统推荐使用最优的索引?是MySQL索引推荐有问题,还是我们创建索引有问题?如何做才能让MySQL帮我们推荐我们认为最优的索引?当然,我们也可以使用FORCE INDEX强行让MySQL走我们提前预设的索引,但是这种方式局限太大,后期索引维护成本变得很高,甚至可能使用该SQL的其他业务性能变低。为了突破整体优化的卡点状态,我们需要了解一下MySQL索引推荐底层逻辑,即MySQL代价模型。了解相应规则后,现阶段的问题将迎刃而解。案例分析及优化在回顾刚才的问题时,我们发现问题源于原始索引产生了【Using filesort】,从而导致了慢查询的出现。为了解决这个问题,我们新增了一个索引,即【KEY idx_station_no_and_id (station_no, id)】,以替代原有的索引【UNIQUE KEY uniq_storegoods (station_no, sku_id)】。然而,尽管新增索引后大部分慢查询得到了解决,但仍有部分慢查询未能消除。进一步分析发现,这些慢查询是由于SQL没有使用我们期望的索引,而是使用了老索引,从而引发了【Using filesort】问题。在通过explain进行分析后,我们暂时还没有找到合适的解决方案。问题:尽管我们新增了索引,并且大部分SQL已经能够使用新索引进行优化,但仍存在一些SQL没有使用新索引。// 通过代价模型进行分析
// 使用上面的测试数据进行分析
// 新增索引后都没有走新索引
// 老索引,扫描行数:1999,代价计算值:1842.80,ref类型索引
// 新索引,扫描行数:1999,代价计算值:1850.46,range类型索引
select 字段1,字段2 ... from store_goods_center where station_no = ‘门店id’ and id > -1 order by id asc;
// 新增索引后走新索引
// 老索引,扫描行数:1999,代价计算值:1842.80,ref类型索引
// 新索引,扫描行数:1299,代价计算值:1819.61,range类型索引
select 字段1,字段2 ... from store_goods_center where station_no = ‘门店id’ and id > 35018 order by id asc;
经过分析MySQL的代价模型,我们发现MySQL在选择使用哪个索引时,主要取决于扫描出的数据条数。具体来说,扫描出的数据条数越少,MySQL就越倾向于选择该索引(由于MySQL的索引数据访问类型各异,计算公式也会有所不同。因此,在多个索引的扫描行数相近的情况下,所选索引可能与我们期望的索引有所不同)。顺着这个思路排查,我们发现当id > -1时,无论是使用storeId + skuId还是storeId + id索引进行查询,扫描出的数据条数是相同的。这是因为这两种查询方式都是根据门店查询商品数据,且id值肯定大于1。因此,对于MySQL来说,由于这两种索引扫描出的数据条数相同,所以使用哪种索引效果相差不多。这就是为什么一部分查询走新索引,而另一部分查询走老索引的原因。然而,当查询条件为id > n时,storeId + id索引的优势便得以显现。因为它能够直接从索引中扫描并跳过id <= n的数据,而storeId + skuId索引却无法直接跳过这部分数据,因此真正扫描的数据条数storeId + skuId要大于storeId + id。因此,在查询条件为id > n时,MySQL更倾向于使用新索引。(需要注意的是,示例给出的数据索引数据访问类型不同,一个是range索引类型,一个是ref索引类型。由于算法不同,即使某个索引的检索数据率略高于另一个索引,也可能导致系统将其推荐为最优索引)问题已经分析清楚,主要原因是存在多个索引,且根据索引代价计算公式的代价相近,导致难以抉择。因此,解决这个问题的方法不应该是同时定义两个会让MySQL"纠结"的索引选择。相反,应该将两个索引融合为一个索引。具体的解决方案是根据门店查询,将原来的主键id作为上次查询的最大id替换为skuId。在算法切换完成后,删除新的门店+主键id索引。然而,这种方式可能会引发另一个问题。由于底层排序算法发生了变化(由原来的主键id改为skuId),可能导致无法直接从底层服务切换。此时,应考虑从下游使用此接口服务的应用进行切换。需要注意的是,如果下游系统是单机分页迭代查询门店数据,那么下游系统可以直接进行切换。但如果这种分页查询动作同时交给多台应用服务器执行,切换过程将变得相当复杂,他们的切换成本与底层切换成本相同。但是,这个系统的对外服务属于这种情况,下游调用系统会有多台应用服务器协作分页迭代查询数据,为这次优化带来很大影响。最终,让底层独立完成切换方式最为合适。在切换过程中,关键在于正确区分新老算法。老算法在迭代过程中不应切换至新算法。原系统对外服务提供的下次迭代用的id可用来进行区分。新算法在返回下次迭代用的id基础上增加一个常量值,例如10亿(加完后不能与原数据冲突,也可以将迭代id由整数转换成负数以区分新老算法)。因此,如果是第一次访问,直接使用新算法;如果不是第一次访问,需要根据下次迭代用的id具体规则来判断是否切换新老算法。总结与后续规划使用Explan执行计划存在无法提前预知索引选择的局限性。然而,只要熟悉MySQL底层代价模型的计算公式,我们就能预知索引的走向。借助代价模型,我们不仅可以分析索引冲突的原因,还可以在发生冲突之前进行预警。甚至在添加索引之前,我们也可以根据代价模型公式来排查潜在问题。此外,根据数据业务密度,我们还可以预估当前索引的合理性,以及是否可能出现全表扫描等情况。因此,深入研究MySQL代价模型对于优化索引管理具有关键意义。未来我们的系统应用将结合MySQL代价模型进行集成,实现自动分析数据库和表的信息,以发现当前索引存在的问题,例如索引冲突或未使用索引导致的全表扫描。此外,该工具还可以针对尚未添加索引的表,根据数据情况提供合适的索引推荐。同时,该工具还能够预测当数据达到某种密度时,可能出现全表扫描的问题,从而帮助提前做好优化准备。为了实现这些功能,我们将首先对MySQL代价模型进行深入研究,全面了解其计算公式和原理。这将有助于我们编写相应的算法,自动分析数据库和表的信息,找出潜在的索引问题。此外,我们还关注易用性和实用性,确保用户能够轻松地输入相关数据库和表的信息,并获取有关优化建议。该工具的开发将有助于提高数据库性能,减少全表扫描的发生,降低系统资源消耗。同时,它还可以为数据库管理员和开发人员提供便利,使他们能够更加专注于其他核心业务。通过结合MySQL代价模型,我们相信这个工具将在优化索引管理方面发挥重要作用,为企业带来更高的效益。参考资料github.com/mysql/mysql…
秋叶无缘
爬虫进阶实战(标价1000的单子)
前言这次这个单子是别人做不了然后我接手的。说实话,一开始看到任务我以为我运气好捡漏了,后来才发现天上不会掉馅饼。这个网站的反爬技术真的不错,这个没点水平还真的解决不了。而我恰恰是业余爱好者,就是那种没点技术水平的。还早后来我剑走偏锋,爬了两天两夜终于把这个任务交付了。这里就想把这个剑走偏锋的方法贴出来,然后供大家参考,给大家反反爬提供一个别样的思路。项目需求这次的任务是这个网站:WebNovel这个网站是美国的应该,需要一点科学手段才能访问。客户给了个csv文件,里面包含了目标小说的书名。 最终的要求就是把表格内的所有小说的章节目录爬下来。包括书名,书的ID,章节ID,章节更新时间,是否是VIP章节等信息。注意:目标不是小说内容,是目录。看起来是个非常简单的任务是吧,后面有坑的,这个网站的。一、分析目标页面我们首先随便打开一个小说的主页。关注到这样几个信息链接的规则目标章节目录的位置。很明显昂,每个页面链接的规则非常简单,网站主页地址加上book/小说名字_长数字字符ID这个目录章节本来我以为我要用selinum自动化去点击这个按钮才能显示章节目录呢,但是其实你在链接后面加个\catalog就能直接进入章节目录的界面。至于我为什么能发现,F12就能看到哈:直接通过链接进入章节目录,这种简单的解析页面就不讲了。二、信心满满的准备当时我用了半个小时左右吧,分析了下任务,甚至把所有目标小说的链接爬了下来。这里就放一个关键代码吧。headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'}
proxies ={
"http": "http://127.0.0.1:1080",
"https": "http://127.0.0.1:1080",
}
url ='https://www.webnovel.com/search?keywords='+name
# 这个相当于模拟搜索请求
# name就是书名
r = requests.get(url, headers=headers, proxies = proxies)
html = r.content.decode('utf-8', 'ignore')
my_page = BeautifulSoup(html, 'lxml')
tag=my_page.find_all('li', class_="pr pb20 mb12")[0]
title=tag.a.attrs['title']
row.append(title)
href=tag.a.attrs['href']
row.append(href)循环爬完之后的结果: 然后我这一看,不用试了,跟客户吹牛逼说,两天。两天主要考虑到两方面,一方面是1000块钱我总不能说给我俩小时。另一方面是,可能在爬的时候遇到bug让自己立于不败之地。后来果然出事了三、惨遭滑铁卢我没想到啊,真的没想到。这个小小的comming soon卡了我一天。中途我尝试过各种方法去解决这个问题,因为我也是个业余的大家看个笑话就好:1.selinum自动化,延迟几秒等他反应过来。但是把基本上现在的网站都有反爬机制,显示出这个,然后就会有很多人机验证或者直接不把数据给你给出来。这里就是第二种,你不管怎么点,都是不会把数据给你的,一直comming soon 2.尝试查询资料,解决这个识别问题。结果查阅资料,我发现了这个识别其实是selinum自己给自己的一个限制。也就是说通过一定的手段,可以解决的。通过执行一个自己创建的js文件,修改几个配置变量,就能让浏览器或者后台把他作为正常的请求。很明显,我的水平不够,没解决。以后解决了再贴出来。3.requests_html的render等待。我在查阅资料(指csdn、掘金、百度、google的病急乱投医),看到了这样的一个代码。本着试试也不吃亏的态度试了下。from requests_html import HTMLSession
def open_url(url):
headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0'}
session = HTMLSession()
resp = session.get(url, headers=headers)
resp.html.render(timeout=20)
return resp.html.html这个方法最后也没成功。而此时,因为当时有个妹子乱我道心,导致我码代码效率低下。时间只剩下一天了。四、大聪明的解决方案:返璞归真,大巧不工其实,反爬机制就是通过鉴别爬虫和正常访问的区别从而禁止爬虫的访问。而反反爬技术就是尽量把自己的爬虫伪装成正常的访问。所以这个时候我悟了,我用浏览器直接访问页面,网站终究会给我最终的章节信息。我当时直接ctrl+s确实也能通过保存下来的html文件解析出章节目录信息。所以思路很清晰了。网站不会拒绝正常的访问,而最正常的访问就是直接打开链接。所以我们需要的是一个自动打开链接的工具,和一个自动保存的工具。1.自动打开链接:这个就是自动打开链接的方法,完全正常访问,不会防备的那种。import webbrowser
webbrowser.open_new_tab(url)2.自动保存页面:这个东西,真的python处理不了,因为毕竟那个页面打开后也不想selinum那样还被python的库控制着,所以,我找了个插件Save Page WE这个就是能够自动保存界面,还支持延迟,自动关闭页面。正好完美的符合我们的需求。五、开始执行后面有完整的代码,他的逻辑很好理解昂1. 自动打开界面中间用sleep延迟了下,因为要给网站反应的时间2. Save Page WE自动保存页面3. 查看下载目录是否多了一个html文件完整代码如下:import webbrowser
import csv
import time
import os
csv_file=csv.reader(open('url.csv','r',encoding='utf-8'))
sum=0
error=[]
pre=0
#295
for line in csv_file:
if pre<1187:#444 /book/forsaken-core_17161136306825805
pre+=1
else:
print(pre,line[2],end=' ')
url ='https://www.webnovel.com'+line[2]+'/catalog'
webbrowser.open_new_tab(url)
time.sleep(8)
while 1:
path = "C:/Users/Dave-Cui/Downloads/"
fileList = os.listdir(path) # 待修改文件夹
os.chdir(path) # 将当前工作目录修改为待修改文件夹的位置
num = 0 # 名称变量
for fileName in fileList: # 遍历文件夹中所有文件
if fileName.endswith(".html"):
num += 1
if num-pre==1:
pre=num
time.sleep(5)
print(pre,'成功!')
break
当天晚上,我的电脑真的一直忽明忽暗的。然后一共是三千多个html文件,后面就是解析html文件了。这个解析和我们request.get获取到的解析是完全一致的,只是获取需要with open (filename,"r","utf-8") as f:
html=f.read()
my_page = BeautifulSoup(html, 'lxml')剩下的解析这里不多哔哔了。个人总结:这里也算是给大家提供一个爬虫的新思路供大家参考。其实算是很笨的方法,但是挺有用的。后续可能会把这一套封装好,毕竟谁也不想那个页面不停的开开关关的,在这期间严重影响电脑的使用。
秋叶无缘
理解Mysql索引原理及特性
作为开发人员,碰到了执行时间较长的sql时,基本上大家都会说”加个索引吧”。但是索引是什么东西,索引有哪些特性,下面和大家简单讨论一下。1 索引如何工作,是如何加快查询速度索引就好比书本的目录,提高数据库表数据访问速度的数据库对象。当我们的请求打过来之后,如果有目录,就会快速的定位到章节,再从章节里找到数据。如果没有目录,如大海捞针一般,难度可见一斑。这就是我们经常碰到的罪魁祸首,全表扫描。一条索引记录中包含的基本信息包括:键值(即你定义索引时指定的所有字段的值)+逻辑指针(指向数据页或者另一索引页)。通常状况下,由于索引记录仅包含索引字段值(以及4-9字节的指针),索引实体比真实的数据行要小许多,索引页相较数据页来说要密集许多。一个索引页可以存储数量更多的索引记录,这意味着在索引中查找时在I/O上占很大的优势,理解这一点有助于从本质上了解使用索引的优势,也是大部分性能优化所需要切入的点。1)没有索引的情况下访问数据:2)使用平衡二叉树结构索引的情况下访问数据:第一张图没有使用索引我们会进行顺序查找,依照数据顺序逐个进行匹配,进行了5次寻址才查询出所需数据,第二张图用了一个简单的平衡二叉树索引之后我们只用了3次,这还是数据量小的情况下,数据量大了效果更明显,所以总结来说创建索引就是为了加快数据查找速度;2 索引的组成部分和种类常见的索引的实现方式有很多种,比如hash、数组、树,下面为大家介绍下这几种模型使用上有什么区别2.1 hashhash思路简单,就是把我们插入的key通过hash函数算法(以前一般是取余数,就好比hashmap的计算方式移位异或之类的),计算出对应的value,把这个value放到一个位置,这个位置叫做哈希槽。对应磁盘位置指针放入hash槽里面。一句话总结hash索引,就是存储了索引字段的hash值和数据所在磁盘文件指针。但是不可避免的是,无论什么算法,数据量大了之后难免会出现不同的数据被放在一个hash槽里面。比如字典上的 “吴”和”武”就是同音,你查字典的时候到这里只能顺序往下去找了。索引的处理也是这样,会拉出一个链表,需要的时候顺序遍历即可。缺点:无序索引,区间查询性能低,因为区间查询会造成多次访问磁盘,多次io耗时是很难接受的。优点:insert迅速,只需往后补就行场景:等值查询, 比如memcached 。不适用大量重复数据的列,避免hash冲突总结:想成java的hashmap即可2.2 有序数组如果我们需要区间查询的时候,hash索引的性能就不尽如人意了。这个时候有序数组的优势就能体现出来了。当我们需要从一个有序数组里取A和B之间的值时,只需要通过二分法定位到A的位置,时间复杂度O(log(N)),接着从A遍历到B即可,论速度的话,基本上可以说是最快的了。但是当我们需要更新的时候,需要进行的操作就很多了。如果需要插入一条数据,你需要挪动数据之后的所有数据,浪费性能。所以总结来说,只有不怎么变化的数据适合有序数组结构的索引。缺点:insert新数据的时候,需要改变后续所有数据,成本略高。优点:查询速度很快,理论最大值。场景:归档查询,日志查询等极少变化的总结:就是顺序排的数组2.3 二叉搜索树基本原则是树的左节点都小于父节点,右节点都大于父节点\这里我们就能看出来,二叉搜索树的查询效率原则上是O(log(N)),为了保证是平衡二叉树,更新效率也是O(log(N))。但是数据很多的情况树的高度会达到很高,过多次访问磁盘,是不可取的。并且极端情况下,树退化成链表,查询的复杂度会被拉成O(n)。进化成多叉树,也就是多个子节点的时候,会大大的减少树的高度,降低访问磁盘。缺点:数据量大的时候,树会过高,导致多次访问磁盘优点:进化成多叉树,会降低树高,访问磁盘次数。场景:适用很多场景总结:左小右大的树2.4 B树在每个节点存储多个元素,在每个节点尽可能多的存储数据。每个节点可以存储1000个索引(16k/16=1000),这样就将二叉树改造成了多叉树,通过增加树的叉树,将树从高瘦变为矮胖。构建1百万条数据,树的高度只需要2层就可以(1000*1000=1百万),也就是说只需要2次磁盘IO就可以查询到数据。磁盘IO次数变少了,查询数据的效率也就提高了。这种数据结构我们称为B树,B树是一种多叉平衡查找树2.5 B+树B+树和B树最主要的区别在于非叶子节点是否存储数据的问题。B树:非叶子节点和叶子节点都会存储数据。B+树:只有叶子节点才会存储数据,非叶子节点至存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。正是因为B+树的叶子节点是通过链表连接的,所以找到下限后能很快进行区间查询,比正常的中序遍历快3 索引的维护当你insert一条数据的时候,索引需要做出必要的操作来保证数据的有序型。一般自增数据直接在后面加就行了,特殊情况下如果数据加到了中间,就需要挪动后面所有的数据,这样效率比较受影响。最糟糕的情况,如果当前的数据页(页是mysql存储的最小单位)存满了,需要申请一个新的数据页,这个过程被称为页分裂。如果造成了页分裂的话,势必会造成性能的影响。但是mysql并不是无脑的数据分裂,如果你是从中间进行数据分裂的话,对于自增主键,会导致一半的性能浪费。mysql会根据你的索引的类型,和追踪插入数据的情况决定分裂的方式,一般都存在mysql数据页的head里面,如果是零散的插入,会从中间分裂。如果是顺序插入,一般是会选择插入点开始分裂,或者插入点往后几行导致的。决定是否从中间分裂,还是从最后分裂。如果插入的是不规则的数据,没法保证后一个值比前一个大,就会触发上面说的分裂逻辑,最后达到下面的效果所以绝大多数情况下,我们都需要使用自增索引,除非需要业务自定义主键,最好能保证只有一个索引,且索引是唯一索引。这样可以避免回表,导致查询搜索两棵树。保证数据页的有序性,可以更好的使用索引。4 回表通俗的讲就是,如果索引的列在 select 所需获得的列中(因为在 mysql 中索引是根据索引列的值进行排序的,所以索引节点中存在该列中的部分值)或者根据一次索引查询就能获得记录就不需要回表,如果 select 所需获得列中有大量的非索引列,索引就需要先找到主键,再到表中找到相应的列的信息,这就叫回表。要介绍回表自然就得介绍聚集索引和非聚集索引InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:如果表定义了主键,则PK就是聚集索引;如果表没有定义主键,则第一个非空唯一索引(not NULL unique)列是聚集索引;否则,InnoDB会创建一个隐藏的row-id作为聚集索引;当我们使用普通索引查询方式,则需要先搜索普通索引树,然后得到主键 ID后,再到 ID 索引树搜索一次。因为非主键索引的叶子节点里面,实际存的是主键的ID。这个过程虽然用了索引,但实际上底层进行了两次索引查询,这个过程就称为回表。也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。或者有高频请求时,合理建立联合索引,防止回表。5 索引覆盖一句话表达的话,是只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。落实到sql上的话,只要执行计划里面的输出结果Extra字段为Using index时,能够触发索引覆盖。常见的优化手段,就是上面提到的,将查询的字段都建到索引里面,至于dba愿不愿意让你建,那就需要你们自己battle了。一般索引覆盖适用的场景包括 全表count查询优化、列查询回表、分页回表。高版本的mysql已经做了优化,当命中联合索引的其中一个字段,另外一个是id的时候,会自动优化,无需回表。因为二级索引的叶子上存了primary key,也算索引覆盖,无需额外成本。6 最左匹配原则简单来说,就是你使用 ‘xx%’的时候,符合条件的话也会使用索引。如果是联合索引的话,我举个例子,创建一个(a,b)的联合索引可以看到a的值是有顺序的,1,1,2,2,3,3,而b的值是没有顺序的1,2,1,4,1,2。但是我们又可发现a在等值的情况下,b值又是按顺序排列的,但是这种顺序是相对的。这是因为MySQL创建联合索引的规则是首先会对联合索引的最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序。所以b=2这种查询条件没有办法利用索引。举个例子,我弄一个索引,KEY idx_time_zone (time_zone,time_string) USING BTREE执行第一条sql,全表扫描执行第二条sql,可以看到使用了索引。再看两条sql,建立的索引是 KEY idx_time_zone (time_zone,time_string) USING BTREE按照正常逻辑来说,第二条sql是不符合索引字段的顺序的,应该不能使用索引才对,但是实际情况却和我们期望的不太一样,这是为啥呢?从mysql被oracle收购以后,mysql加入了很多oracle以前的技术,高版本mysql自动优化了where条件的先后顺序。简单来说就是查询优化器做了这一步操作,sql会做预处理,那一条能更好的查询就会使用那种规则。顺便提一下mysql的查询优化器能帮忙干的一些事6.1 条件转化例如where a=b and b=2,可以得到a=2,条件传递。最后的sql是 a=2 and b=2 > < = like 都可以传递6.2 无效代码的排除例如 where 1=1 and a=2, 1=1永远是正确的,所以最后会优化成 a=2在比如 where 1=0 永远是false的,这样的也会被排除掉,整sql无效或者非空字段 where a is null ,这样的也会被排除6.3 提前计算包含数学运算的部分,例如 where a= 1+2 会帮你算好,where a=36.4 存取类型当我们评估一个条件表达式,MySQL判断该表达式的存取类型。下面是一些存取类型,按照从最优到最差的顺序进行排列:system系统表,并且是常量表const 常量表eq_ref unique/primary索引,并且使用的是’=’进行存取ref 索引使用’=’进行存取ref_or_null 索引使用’=’进行存取,并且有可能为NULLrange 索引使用BETWEEN、IN、>=、LIKE等进行存取index 索引全扫描ALL 表全扫描经常看执行计划的,一眼就能看出来这是啥意思,举个例子where index_col=2 and normal_col =3 这里就会选用index_col=2 会作为驱动项。驱动项的意思是指一个sql选定他的执行计划的时候,可能有多条执行路径,一个是全表扫描,再过滤是否符合索引字段及非索引字段的值。另一种是通过索引字段,键值=2找到对应的索引树,过滤后的结果,再比较是否符合非索引字段的值。一般情况下,走索引都比全表扫描需要读取磁盘的次数少,所以称它为更好的执行路径,也就是通过索引字段,作为其驱动表达式6.5 范围存取简单来说,a in(1,2,3) 和 a=1 or a=2 or a=3 是一样的,between 1 and 2 和 a>1 and a<2也是一样的, 无需可以优化。6.6 索引存取类型避免使用相同前缀的索引,也就是一个字段不要在多个索引上有相同的前缀。比如一个字段已经建立了唯一索引,这个时候如果再给他建立一个联合索引,会导致优化器并不知道你要使用哪个索引。或者你建了前缀相同的一个单索引,一个联合索引,就算你写上了条件,也不一定能用上联合索引。当然,可以force,这就另说了。6.7 转换简单的表达式可以进行转换,比如 where -2 = a 会自动变成 where a= -2 ,但是如果牵扯到数学运算,就不能转换了 比如 where 2= -a 这时候不会自动转成 where a =-2.第二条sql就可以使用索引所以 我们在开发的过程中,需要注意sql的写法,自觉写成 where a=-26.8 and、union、order by、group by等1)andand条件后,如果都没索引,扫描全表。有一个存取类型更好,见5.4 ,会使用存储类型更好的索引,如果都一样,哪个索引先创建,用哪个。2)unionunion 每条语句单独优化这里就会分别执行两条sql,用到索引,再合并结果集3)order byorder by 会过滤无效排序,比如一个字段本来就有索引第二条sql和第一条的查询效果是一样的所以,写sql的时候,不要写无用排序,比如order by ‘xxx’ 这样没有意义。4)group by简单来说 group by 的字段,有索引会走索引,group by a order by a 这里的order by等于没写,结果集已经是排序完毕的了,参考 6.8-3 order byselect distinct col_a from table a 等价于 select col_a from a group by col_a7 索引下推主要的核心点就在于把数据筛选的过程放在了存储引擎层去处理,而不是像之前一样放到Server层去做过滤。如果在一张表上,name和age都建立索引,查询条件为 where name like ‘xx%’ and age=11,在低版本的mysql(5.6 以下)的根据索引的最左匹配原则,可以得到放弃了age,只根据name过滤数据。根据name拿到所有的id之后,再根据id回表。高版本mysql里,没有忽略age这个属性,带着age属性过滤,直接过滤掉了age为11的数据,假设不根据age过滤的数据为10条,过滤后只剩3条,就少了7次回表。减少了io会大量减少性能消耗8 小表驱动大表小表驱动大表,也是我们听惯了的话了,其含义主要是指小表的数据集驱动大表的数据集,减少连接次数。打个比方:表A有1w数据,表B有100w数据,如果A表作为驱动表,处于循环的外层,那么只需要1w次的连接即可。如果B表在外层,那么则需要循环100w次。下面我们实际测试看看,准备环境mysql 5.7+准备两张表,一张表 ib_asn_d 数据 9175, 一张表 bs_itembase_ext_attr 数据 1584115,都在商品编码字段上有索引。首先小表驱动大表多次反复测试,执行时间大概7秒。接下来看看大表驱动小表。将近300秒,不是一个量级的。接下来分别分析执行计划,执行计划里第一条就是驱动表。小表驱动大表,大表用了索引,小表全表扫描,只扫描8000多行大表驱动小表,大表全表扫描,需要扫描147w行。经过多次测试得出了结论:当使用left join时,左表是驱动表,右表是被驱动表 ;当使用right join时,右表是驱动表,左表是被驱动表 ;当使用inner join时,mysql会选择数据量比较小的表作为驱动表,大表作为被驱动表 ;驱动表索引不生效,非驱动表索引生效保证小表是驱动表很重要。9 总结覆盖索引:如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不用回表操作,直接返回结果,减少IO磁盘读写读取整行数据,所以高频字段建立联合索引是很有必要的最左前缀:联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。建立索引的时候,注意左前缀不要重复,避免查询优化器无法判定如何使用索引索引下推:name like ‘hello%’and age >10 检索,MySQL 5.6版本之前,会对匹配的数据进行回表查询。5.6版本后,会先过滤掉age<10的数据,再进行回表查询,减少回表率,提升检索速度
秋叶无缘
慢SQL治理实践及落地成果分享
一、治理背景数据库系统性能问题会对应用程序的性能和用户体验产生负面影响。慢查询可能导致应用程序响应变慢、请求堆积、系统负载增加等问题,甚至引发系统崩溃或不可用的情况。慢SQL治理是在数据库系统中针对执行缓慢的SQL查询进行优化和改进的一项重要工作。但原有的治理节奏,一般在大促备战期间,集中投入人力紧急治理,日常对慢SQL的关注度不高;即使研发团队想着手治理,实例下的SQL明细筛选繁琐,趋势不明,缺少系统化,数字化的治理方案。所以为了保证系统稳定性,预防潜在慢SQL导致应急事故,发起慢SQL常态化备战专项,下文主要描述专项的实践及落地情况。二、阶段规划1.0阶段目标:【形成常态化治理机制,关注慢SQL解决的有效率】改变慢SQL治理习惯,由原大促备战期间治理,落地按照日维度产生的慢SQL每天跟进,关注双周维度治理的有效率。关注指标:逾期率 = 工单逾期数量(创建时间在当季度的任务)/总量(创建时间在当季度的任务) 注:超过14天未处理完成的算逾期,逾期与否以第一次完成的时间来判断,如果在截止日期前未完成,算逾期;如果在截止日期前完成,但是重开后,在截止日期后完成,不算逾期,算重开;挂起的如果超过14天会统计到逾期里;重开率 =工单重开次数(创建时间在当季度的任务,如果是一个任务被重开5次,记录为5)/总量(创建时间在当季度的任务)2.0阶段目标:【彻底根治慢SQL历史债,达成阶段性内的>0.9s清零】经过1.0阶段研发团队有序进行慢SQL的逐步治理,前期已经有效解决部分慢SQL数据,但仍存在历史债,影响系统稳定性。2.0阶段要求双周阶段性清零。关注指标:P0工单推送数=大于0.9s推送时间在当周的任务总数 注:声明级别划分,P0 执行时间大于0.9秒,且达到阈值10次P1 执行时间大于0.9秒,但未达到阈值10次P2 执行时间小于0.9秒未加索引,且达到阈值10次P3 执行时间小于0.9秒未加索引,但未达到阈值10次P0工单存量数=大于0.9s推送时间在当周的任务中状态是非已解决的总数解决率=大于0.9s推送时间在当中的任务状态是已解决的/推送时间在当周的任务总数3.0阶段目标:【提高系统性能指标,阶梯型降低慢SQL阈值】存在较大隐患的0.9s阶段性清零后,对慢SQL工单逐步精细化,按照阶梯维度逐步降低慢SQL定义阈值,按照双周维度对新增慢SQL清零。关注指标:P0工单推送数=大于0.9s推送时间在当周的任务总数 注:声明级别划分,P0 执行时间大于0.9秒P1 执行时间小于等于0.9秒大于0.7秒P2 执行时间小于等于0.7秒大于0.5秒P3 执行时间小于等于0.5秒未加索引P1工单推送数=小于等于0.9s大于0.7s推送时间在当周的任务总数P2工单推送数=小于等于0.7s大于0.5s推送时间在当周的任务总数存量数=推送时间在当周的任务中状态是非已解决的总数解决率=推送时间在当中的任务状态是已解决的/推送时间在当周的任务总数4.0阶段目标:【前置预防慢SQL,落地数据库操作规范】预期目标,彻底解决历史债提升系统性能指标后,贯彻数据库操作规范预防新增慢SQL,后续持续关注新增的慢SQL,控制新增数量目标周清。关注指标:工单新增数=推送时间在当周的非现存指纹ID的任务总数存量数=推送时间在当周的任务中状态是非已解决的总数解决率=推送时间在当中的任务状态是已解决的/推送时间在当周的任务总数三、落地方案①数据准备阈值定义结合二级部门业务,每天搜集SQL的查询时间是T-1天,执行时间>0.9秒或<0.9秒但执行计划内未走索引的,剔除bi_cx和wlcx抽数后(不区分主从),聚合相同指纹慢SQL均识别为现存风险慢SQL。明确等级不同治理阶段,会针对慢SQL划分优先级,按P0-P3顺序,推动研发由高到低按照不同解决时效进行考核。同时提供辅助诊断信息,包括触发该慢SQL治理任务的数据库IP/域名/库名/执行耗时/执行计划等。归类筛选按照实例信息,数据库名,归属系统,归属产品条线,查询时间,聚合指纹等进行归类,方便归类出慢SQL的同一问题源。②工单推进工单流转按照业务条线划分,明确每个条线工单接口人,统一下派慢SQL工单给到接口人,由接口人按照系统分发组内同学,逐一解决。解决思路借鉴dba等提供的解决思路,同时总结团队内落地的解决方案,推进慢SQL快速解决。③趋势分析图表制作根据每个阶段关注指标数据,制作慢SQL解决趋势图表,实现团队内可清晰查看,每个实例下的慢SQL明细,支持多个维度筛选;同时按照时间维度支持查看解决趋势了,现存数量等。通晒复盘以专项周会的形式,同步研发团队处理节奏和进度,保障持续推进。④过程跟踪1.0阶段主要关注解决有效性2.0阶段关注>0.9s的治理,进行历史债的清理P0级SQL的解决跟进:现有历史债的清零:按月度出现慢SQL量的趋势四、落地结果【系统保障】经过慢SQL的逐步治理,截止529封板前,团队累计解决慢SQL831条,完成今年618备战期间,阶段性内的历史债**清零。**日常完成慢sql治理索引添加和代码优化,大促重点关注疑难和分析未使用索引,做到备战无遗漏。更是直接保障了系统的稳定性,近半年无因慢SQL导致的线上问题。【方案沉淀】随着慢SQL治理作为专项融入到研发的日常工作,首先团队内为避免新的慢SQL产生,落地数据库开发规范,京东集团数据库开发规范-V1.0-公示稿,同时如何分析SQL、快速定位、高效解决,团队内也输出了治理的解决方案。五、总结经过专项各个阶段的推进落地,团队内贯彻了治理目标,沉淀了解决方案,后续慢SQL治理会持续化推进,从而保障系统稳定性。
秋叶无缘
爬虫进阶实战(selinum爬取淘宝商品类目)
项目地址:gitee.com/huadeng863/…前言这个单子给我深刻的教训就是不要随便接软件开发的活。因为软件开发的话需求就太多了,给我整的心力交瘁的。它不像单纯的数据爬取任务,人甲方只要你数据准确全面即可。而且最重要的是,他么的最后他不满意,然后尾款没结。项目需求这个单子标价800。需求也比较常见,爬淘宝的SKU,要求就是输入商铺链接,爬取下所有的SKU,还要求促销价。并且要求做一个界面出来。我用PyQt5做了个大致如下的界面。功能演示如下:这个任务的要去爬出来指定淘宝或天猫链接内sku的价格和优惠价格,这个爬虫最常用的就是selinum直接爬下来。一、下载seleinum的谷歌webdriver驱动首先查看自己的谷歌浏览器的版本在chromedriver.storage.googleapis.com找对应版本的webdriver.放在你的工程目录中就行。二、selenium绕过淘宝的身份验证这里我尝试了N多方法,但是能够稳定通过的还是得靠手动扫码通过。这个绕过去的方法应该是有优化的,但是我尝试了我查到的方法全部没成功哈。可能这也是老板没结尾款的原因吧,但我真心觉得加个扫码的步骤也不是多麻烦的事,毕竟是单独对应一个链接操作一次的嘛。实现方法也比较简单,打开做个60秒的延迟判定,主要就是给你扫码留一段时间。browser = webdriver.Chrome()
browser.implicitly_wait(60)
browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument",
{"source": """Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"""})
browser.get('https://item.taobao.com/item.htm?spm=2013.1.20141001.1.6beb5273VJs5lY&id=39211103460')# 进入指定链接
browser.maximize_window()#浏览器窗口最大化
browser.find_element(By.XPATH, '//*[@id="login"]/div[1]/i').click() # 点击右上角转换扫码登录
price=browser.find_element(By.ID, "J_PromoPriceNum") # 没登录进去,找不到这个价格数据会等60秒。
三、爬取价格信息整体编程思路就是获取全部的元素列表,然后遍历。因为价格和优惠价格信息需要点击才能获取到,所以还需要在遍历时加入点击操作。def get_conceal_text(element):
js = "return arguments[0].textContent"
return browser.execute_script(js, element)
elements=browser.find_elements(By.XPATH, '//*[@id="J_isku"]/div/dl[1]/dd/ul/li')
print(elements)
with open('result.csv','w',encoding='utf-8',newline='') as f:
writer=csv.writer(f)
writer.writerow(['SKU名称','SKU价格','促销价格'])
for element in elements:
try:
row = []
name=get_conceal_text(element).replace(' ','').replace('\n','').replace('已选中','')
row.append(name)
element.click()
prices = browser.find_elements(By.CLASS_NAME, "tb-rmb-num")
for price in prices:
row.append(price.text)
writer.writerow(row)
print('\t'.join(row))
except:
print('————————————访问出错!!!——————————————————')四、GUI封装最后用PyQt5做个GUI的封装。需要的可以自取。下载好第三方库,直接运行gui.py即可。如果只想看爬虫思路的话,看tmao和taobao这两个py文件即可。附录:Selenium基础用法1.Selenium 的简介Selenium 是一个用于 Web 应用程序自动化测试的工具,最初是为网站自动化测试而开发的。Selenium 自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。它支持所有主流的浏览器(包括 IE、Firefox、Safari、Chrome、Opera 等)。可以使用它对浏览器进行各种各样的模拟操作,包括爬取一些网页内容。2.Selenium 的基本流程Selenium 爬虫是一种利用 Selenium 工具模拟浏览器操作来抓取网页数据的方法。Selenium 爬虫的基本流程如下:安装 Python 环境、Selenium 库和对应浏览器的 WebDriver。创建一个 WebDriver 对象,用于控制浏览器。打开目标网页,等待页面加载完成。定位需要抓取的元素,使用不同的定位方式,如 id、name、class name、xpath、css selector 等。对元素进行操作,如点击、输入、滚动等,模拟用户行为。提取元素的文本或属性,如链接、图片、视频等,保存到本地或数据库。关闭浏览器,释放资源。下面是一个简单的示例,使用 Selenium 爬虫抓取百度首页的搜索结果:3.Selenium 的基础用法要使用 Selenium,首先需要安装 Python 环境和 Selenium 库,以及对应浏览器的 WebDriver。WebDriver 是一个用于控制浏览器的驱动程序,需要和浏览器的版本对应,否则可能出现兼容性问题。WebDriver 的下载地址可以在 这里 查找或者自己搜索。安装好 WebDriver 后,可以将其放到 Python 的 Scripts 目录下,或者在使用时指定其路径。然后就可以通过 Python 代码来控制浏览器了。下面是一个简单的示例:好的,我会尝试为你总结 selenium 爬虫的基本流程。请看下面的内容,希望对你有帮助。4.Selenium 的基本案例下面是一些使用 Selenium 的基本案例,展示了如何进行元素定位、窗口切换、frame 切换、鼠标操作等常用功能。这个案例和上面的示例类似,只是使用了不同的元素定位方式。Selenium 提供了多种元素定位方式,包括 id、name、class name、tag name、link text、partial link text、xpath 和 css selector 等。具体可以参考 这里。from selenium import webdriver
# 创建一个 ChromeDriver 对象
browser = webdriver.Chrome()
# 打开百度首页
browser.get('http://www.baidu.com/')
# 使用 xpath 定位方式找到搜索框元素
input = browser.find_element_by_xpath('//*[@id="kw"]')
# 输入关键词
input.send_keys('selenium')
# 使用 xpath 定位方式找到搜索按钮元素
button = browser.find_element_by_xpath('//*[@id="su"]')
# 点击按钮
button.click()
# 关闭浏览器
browser.close()
# 等待页面加载完成
browser.implicitly_wait(10)
# 找到搜索结果元素,使用 xpath 定位方式
results = browser.find_elements_by_xpath('//div[@id="content_left"]/div/h3/a')
# 遍历结果,打印标题和链接
for result in results:
title = result.text
link = result.get_attribute('href')
print(title, link)
# 关闭浏览器
browser.close()
秋叶无缘
MYSQL-INNODB索引构成详解
摘要:对于MYSQL的INNODB存储引擎的索引,大家是不陌生的,都能想到是 B+树结构,可以加速SQL查询。但对于B+树索引,它到底“长”得什么样子,它具体如何由一个个字节构成的,这些的基础知识鲜有人深究。本篇文章从MYSQL行记录开始说起,层层递进,包括数据页,B+树聚簇索引,B+树二级索引,最后在文章末尾给出MYSQL索引的建议。文章涉及较多基础知识,内容较为枯燥,因此采用较多的图片补充说明,希望能对读者有帮助。A. 一条记录存储格式:COMPACT行记录结构mysql是关系型数据库,每一行记录都是表结构定义的关系的 显示表达。在脑中很直观地想到,记录存储时也可能按行存储。的确,mysql是这么存储一条行记录的。但会添加一些额外信息,来补充行记录信息。有一个概念可能大家不熟悉,是【变长字段】。mysql数据库类型中的 VARCHAR(M), VARBINARY(M), 各种TEXT,BLOB类型,这些类型的数据长度是可变的,称 数据类型为可变长类型的列 为 变长字段。另外,mysql会默认为行记录添加一些列(隐藏列)。上图补充这些隐藏列之后,完整行记录的结构如:DB_ROW_ID: 唯一标识一条记录,在表中未设置主键 或 未有不允许为NULL的UNIQUE键时,则MYSQL新增该隐藏列作为主键。 DB_TRX_ID: 事务ID。 DB_ROLL_PTR: 回滚指针。下面再详细的铺开 ,关于记录的额外信息 的具体内容。通过真实的数据库表的行数据,来强化下上面的概念。 首先新增一个表,并在表中insert两条记录。create table record_format_demo(
c1 varchar(10),
c2 varchar(10) not null,
c3 char(10),
c4 varchar(10)
) charset=ascii row_format=compact
insert into record_format_demo(c1, c2, c3, c4) values
("aaaa", "bbb", "cc", "d"),
("eeee", "fff", NULL, NULL);做一个简单的查询,验证数据正常写入。分析这两行数据的存储记录。第一行记录:第二行记录:应该会注意到,变长字段长度列表 与 NULL值列表都是逆序存放的。 原因是:这样可以使得记录中位置靠前的字段 和 它们对应的字段长度信息在内存中的距离更近,可能会提高 高速缓存的命中率。B. 盛放记录的盒子:数据页为了更清楚的理解 数据页结构 以及 下文中的索引,更换一个带主键的表。CREATE TABLE page_demo(
c1 INT,
c2 INT,
c3 VARCHAR(10000),
PRIMARY KEY (c1)
) CHARSET=ascii ROW_FORMAT=Compact;
INSERT INTO page_demo VALUES
(1, 100, 'aaaa'),
(2, 200, 'bbbb'),
(3, 300, 'cccc'),
(4, 400, 'dddd');做一个简单的查询,验证数据正常写入。根据行记录结构中的next_recrod属性的含义,多条行记录是以单向链表的形式存储。mysql为了后续更好的查询,单向链表上的记录是按照主键顺序排列的。 上述这四条记录,可以显示的画成:假如删除其中c1=2这条数据,则单向链表变更如下。 其中变化点为 c1=2 这条数据中,deleted_flag变更成0, next_record=0,但并没有从磁盘上直接清除掉,head_no也未清除。第一条记录的next_record 指向了第三条记录。当我们查询数据时,如果数据以行记录的形式一条一条从磁盘加载到内存中,那么因为IO过多的原因,整体性能肯定较为低效。 因此mysql规定,磁盘与内存交换数据的基本单位是一个页大小。这个页大小 默认是16K。 根据页中存储的数据类型不同,页也分成许多类型。对于存储行记录的页,称为索引页(Index Page),也称为数据页。那么接下来我们看看,数据页的结构如何,一条条行记录如何存放在数据页中。先上个图。从上图中,我们可以猜到,数据页总共分成7个模块,目前咱们只关心 User Records 部分,它存储的就是用户真实的行记录。 但是一个数据页的空间总共是16K,不会自动增大空间,随着User Records 模块中的行记录越来越多,那么肯定有其他模块的空间就越来越小。 这个模块是 Free Space,是页中尚未使用的空间。更新一下上面这个图,补充 Free Space的内容。随着User Records中行记录的增加,Free Space空间则越来越小。在一个数据页中,除了真实的行记录之外,还有两条固定的伪记录。 其中一条记录称为 infimum 【[ɪnˈfaɪməm] ,下确界】行记录,规定是数据页中记录的最小值。 infimum记录特别简单,仅包含了 记录头信息(5字节) + 真实记录数据(8字节),其中【69 6E 66 69 6D 75 6D 00】16进制转换成对应的单词,就是【infimum】。另外一条记录称为 supremum【 [sə'priməm] ,上确界】行记录,规定是数据页中记录的最大值。 supremum记录同样特别简单,仅包含了 记录头信息(5字节) + 真实记录数据(8字节),其中【73 75 70 72 65 6D 75 6D】16进制转换成对应的单词,就是【supremum】。再更新一版数据库页结构, 补充上infimum 与 supremum。既然规定了页面中的最小记录 与 最大记录,理所应当,上文中串联各个行记录的单向链表,则应该有一个固定的头部 与 固定的尾部。 更新一下单链表的图。注意下infimum 与 supremum中的几个关键值。infimum: n_owned=1,表示是某一个分组的最后一条记录,当前分组共1条记录;record_type=2; next_record=第一条真实行记录的真实值的相对位置。 supremum: n_owned=5,表示是某个分组的最后一条记录,当前分组共5条记录;record_type=3; next_record=0,表示记录是本数据页中最后一条记录。OK,到现在数据页中完整的单链表已经形成了。 思考一个问题,如何根据主键值,在数据页中的单链表如何查找到相应的数据。 最直接的做法是:从 infimum记录开始,沿着单链表的顺序不断的向后查找,直到supremum结束。在这期间,找到满足条件的记录就返回,找不到则不返回。一个数据页默认大小是16K,用于存储真实行记录的空间超过 98%以上。也就是说,一个数据页中在行记录填充满的情况下,行记录的数量是较多的(当然与行记录大小有关)。 如果每次查询都从单链表的infimum记录 一直到 supremum记录,肯定是无法接受的,很有必要对此现状进行优化。 由此,引出了数据页中的另一个模块,Page Directory,页目录。首先返回看下上面完整单链表中,infimum记录 与 supremum记录中的两个值,n_owned。这个值分别为 n_owned=1 与 n_owned=5。参考下n_owned的定义,它是:【页面分组之后,每个组最后一条行记录中该值等于本组内的记录数量。本组内其他记录该值都等于0。】 对于上面的单链表,其它行记录的owned值 都为零。也就是说,infimum单条记录作为一个组,剩余的四条行记录+supremum记录作为一个组。 mysql规定:•对于infimum记录所在的分组只能有1条记录,也就是它本身。•对于supremum记录所在的分组的记录数在1~8条之间。•其它的分组记录的条数范围,在4~8条之间。将每个组中 最后一条记录在页面中的地址偏移量(该记录的真实数据与数据页中第0个字节之间的距离)单独提取出来,以倒序存储到数据页中的固定模块中。 这个模块,就称为:Page Directory。Page Directory中存储的地址偏移量,也称为Slot 【[slɒt], 槽】,每个Slot两字节。【可以想想为啥是两字节?】再次更新下数据页结构图。目前的只有四条记录,两个分组,数量太少了。我们继续往表中继续增加一些记录。INSERT INTO page_demo VALUES
(5, 500, 'eeee'),
(6, 600, 'ffff'),
(7, 700, 'gggg'),
(8, 800, 'hhhh'),
(9, 900, 'iiii'),
(10, 1000, 'jjjj'),
(11, 1100, 'kkkk'),
(12, 1200, 'llll'),
(13, 1300, 'mmmm'),
(14, 1400, 'nnnn'),
(15, 1500, 'oooo'),
(16, 1600, 'pppp');
INSERT INTO page_demo VALUES
(5, 500, 'eeee'),
(6, 600, 'ffff'),
(7, 700, 'gggg'),
(8, 800, 'hhhh'),
(9, 900, 'iiii'),
(10, 1000, 'jjjj'),
(11, 1100, 'kkkk'),
(12, 1200, 'llll'),
(13, 1300, 'mmmm'),
(14, 1400, 'nnnn'),
(15, 1500, 'oooo'),
(16, 1600, 'pppp');在不断的插入新行记录时,因此不同类型分组数量的约束,所以分组也会增加。这个过程如下:•在初始情况下,一个数据页中只会有 infimum 与 supremum 这两条记录,它们分别属于两个组。此时Page Directory 也只会有两个slot,分别代表infimum的地址偏移量 与 supremum的地址偏移量。•之后每新增一条行记录,都会从Page Directory中找到对应的记录的主键值 比 待新增记录的主键值大 并且差值最小的slot【slot是一个组内最大的那条记录在页面中的地址偏移量,通过slot可以快速找到对应记录的主键值】, 然后把该slot对应的记录的 n_owned值加1,表示本组内新增了一条记录,直到该组中的记录数等于8个。•当一个组中的记录数等于8后,再插入一条记录,会将组中的记录拆分成两个组,其中一个组中是4条记录,另外一个组中是5条记录。拆分过程中,会新增一个slot,记录这个新增分组中最大的那条记录的地址偏移量。现在来看看下,目前该数据页中的行记录的分组情况。来演绎一个根据主键查询行记录的例子。假如想查询主键值 ,也就是C1=6的行记录。通过二分法查找,过程如下:1.设置low=0,high=4。计算中间slot的位置,(0 + 4) / 2 = 2, 通过slot 2 查询到对应的主键值等于8。因为8 > 6, 所以设置high = 2, low = 0不变。2.重新计算中间slot的位置,(0 + 2)/ 2 = 1, 查看 slot 1对应记录的主键值为4。又因为 4 < 6, 所以设置low = 1,high 不变。3.此时low = 1, high = 2, 所以确定主键值为6的行记录在 slot2 对应的组中。此时找到slot 2的最小一条记录【通过slot 1 的next_record找到slot 2的最小记录】,遍历slot 2中的所有记录即可。截止目前,数据页模块中,还要三个模块是未知的。回想一下,对于一条行记录,它有 记录头信息 来描述这条行记录的相关信息,那么对于一个数据页,它有对应的头部来记录数据页的相关信息吗?有的,自然是有的,而且还不少。这个模块就称为 Page Header。Page Header的结构如下:主要作用是标识 数据页中记录了多少条记录,Free Space在页面中的地址偏移量,页目录中包含多少slot等等。额外说下page_direction 与 page_n_direction的含义。page_direction: 假如新插入的一条记录的主键值比上一条记录的主键值比上一条记录大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是page_direction。page_n_direction: 假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION这个状态表示。 当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。到此为止,仅剩下两个模块了,加油啊。上文中的Page Header 是专门针对数据页记录的各种状态信息。但数据页 仅仅是多种页类型中的一种,其它的还有例如undo日志页,溢出页,存储段信息页,存储区信息页等。 因此mysql 使用了File Header 来描述各种页的通用信息。从fil_page_prev 与 fil_page_next 两个属性可以联想到,不同页之间是有关联的,而且是以双向链表的形式。最后一个模块,File Trailer 【 [ˈtreɪlə(r)],挂车】。InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一半的时候中断电了怎么处理呢?此时就需要靠 File Trailer 模块中数据起作用。展示下完整的数据页结构。盗用一下网上的一个很好的数据页图。C. 快速查询的秘籍:B+树索引上文在介绍File Header时,我们特别说明了里面的两个数据:FIL_PAGE_PREV,指向前一个页号。FIL_PAGE_NEXT, 指向后一个页号。由此可以得出,多个数据页之间的数据结构是双链表。上文使用的数据共有16条,为了演示这个双链表的效果,现在假设【仅仅是假设】每个页中存放不超过4条行记录。则上文的16条记录,形成的数据页双链表结构如下图【此处省略了许多非必要展示的字段】。从上面这个链表,可以得到以下结论:1.双向链表的页号并不保证是连续的。2.下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。【在依次写入主键数据不连续的行记录时,会发生页中数据的迁移。】从目前的双向链表结构中,想要根据主键值查找记录,也只能是从第一页开始,一页一页的依次查找。虽然在一个数据页中,可以根据 Page Directory进行快速的二分查找,但总体效率肯定不尽人意。得优化。我们注意到,【下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值】。因此,首先进行第一步改进。 维护一个目录项,目录项中记录某个页中主键的最小值以及页号,各个目录项再以单向链表的形式链接起来。这样就可以根据主键查询目录项,得到满足的条件页,再进入相应的页中查询行记录即可。到现在看看,目录项是不是也很像行记录,只是它的列值是主键与页号。那把这些目录项维护成在一个页中看看效果。毫无违和感,浑然天成。现在看到的,就是主键索引的雏形了。目前数据有些少了,继续补充一些数据,再画个图看看。INSERT INTO page_demo VALUES
(17, 1700, 'qqqq'),
(18, 1800, 'rrrr'),
(19, 1900, 'sss'),
(20, 2000, 'tttt');现在看到的,就是一个典型的INNODB的主键索引了。它包含以下特点:1.整个主键索引的数据结构是一棵树,具体是以B+树实现。2.叶子节点与非叶子节点中的行记录按照主键大小顺序排成一个单向链表,页内的记录被划分成若干组。可以利用Page Directory进行二分法快速查找到行记录。3.存放用户记录的数据页,根据页中用户记录的主键大小顺序排成一个双向链表。所有用户记录都存放在叶子节点上。4.存放目录项记录的数据页都是非叶子节点,分层不同的层级。同一层级中的页也是根据页中目录项记录的主键大小顺序,排成一个双向链表。通常也把INNODB的B+树索引称为 聚簇索引(clustered/ˈklʌstəd / index),所有的真实数据都存储在聚簇索引中。【索引即数据,数据即索引】。通常除了主键索引之外,肯定还会建一些普通索引,称为二级索引,或者辅助索引。上面的数据,我们以上文中的数据 C2列建立一个二级索引看看。现在来看看下,INNODB中 二级索引的特点。1.二级索引的数据结构 与 聚簇索引一样,是B+树结构。2.二级索引的所有目录项页存储行记录的真实数据是 索引列+页号。3.二级索引的非叶子节点存储行记录的真实数据是 索引列+主键值。因为在二级索引中查询到的是主键值,如果想要得到完整的行记录,则需要拿着主键值再到聚簇索引中再进行一次查询。这个过程称为【回表】。 【回表的过程,特别要强调下每次对于二级索引来说,每次查询到一条满足二级索引字段条件的记录时,都进行一次 回表 判断是否满足其他条件,然后将满足所有条件的一条查询结果返回给客户端。】再讲讲联合索引。现在以C2 与 C3两列作为联合索引,为了更好的展示联合索引的效果,先修改几条行记录。update page_demo set c3 = 'dddd' where c2 = 100;
update page_demo set c3 = 'cccc' where c2 = 200;
update page_demo set c3 = 'bbbb' where c2 = 300;
update page_demo set c3 = 'aaaa' where c2 = 400;
update page_demo set c2 = 300 where c1 = 4;给联合索引画个图。总结下联合索引的特点:联合索引的数据页中记录的排序,默认是按照定义联合索引的第一列排序的,在第一列值相等的情况下,再按照第二列排序。其它的方面均与单列的二级索引无差别。联合索引还有一个比较特殊的使用场景:最左前缀匹配。 例如有联合索引的列包含:C1,C2,C3 三列,而且顺序也是 C1,C2,C3。 则查询语句:select * from page_demo where c1 = x and c2 = y and c3= z, 使用到了C1,C2,C3 列排序的结果。 select * from page_demo where c1 = x and c2 = y, 使用到了C1,C2 列排序的结果。 select * from page_demo where c1 = x and c3 = z, 仅使用到了C1 列排序的结果。D. 索引的优缺点及建议优点:1.对于等值查询,可快速定位到对于的行记录。2.对于范围查询,可辅助缩小扫描区间。3.当ORDER BY的列名 与 索引的列名完全一致时,可加快排序的顺序。4.当GROUP BY的列名 与 索引的列名完全一致时,可加快分组。5.当二级索引列中 包含了 SELECT 关键字后面写明的所有列,则在查询完成二级索引之后无需进行回表操作,直接返回即可。这种情况,称为【覆盖索引】。缺点:1.建立索引占用磁盘空间。2.对表中的数据进行 增加,删除,修改 操作时,都需要修改各个索引树,特别是如果新增的行记录的主键顺序不是递增的,就会产生页分裂,页回收等操作,有较大的时间成本。3.当二级索引列的值 的 不重复值的个数较少时,通过二级索引查询找到的数据量就会比较多,相应的就会产生过多的回表操作。4.在执行查询语句的时候,首先要生成一个执行计划。通常情况下,一个SQL在执行过程中最多使用一个二级索引,在生成执行计划时需要计算使用不同索引执行查询时所需的成本,最后选择成本最低的那个索引执行查询。 因此,如果建立太多的索引,就会导致成本分析过程耗时太多,从而影响查询语句的性能。建议:1.只为用于搜索,排序,分组的列创建索引。2.索引的列需要有辨识性,尽可能地区分出不同的记录。3.索引列的类型尽量小。因为数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以存放更多的记录,磁盘I/O带来的性能损耗也就越小。4.如果需要对很长的字段进行快速查询,可考虑为列前缀建立索引。【alter table table_M add index idx_key1(column_n(10)) --> 将table_M表的 idx_key1列的前10个字符创建索引】5.覆盖索引,当二级索引列中 包含了 SELECT 关键字后面写明的所有列,则在查询完成二级索引之后无需进行回表操作,直接返回即可。因此,编写【select *】的时候,要想想是否必要。6.在查询语句中,索引列不要参与 条件值计算,也是把条件值计算完成之后,再和索引列对比。【否则MYSQL会认为 搜索条件不能形成 合适的扫描区间来减少扫描的记录数量】
秋叶无缘
浅谈SQL优化小技巧
MySQL的执行过程,帮助介绍如何进行sql优化。(1)客户端发送一条查询语句到服务器;(2)服务器先查询缓存,如果命中缓存,则立即返回存储在缓存中的数据;(3)未命中缓存后,MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树,MySQL解析器将使用MySQL语法进行验证和解析。 例如,验证是否使用了错误的关键字,或者关键字的使用是否正确;(4)预处理是根据一些MySQL规则检查解析树是否合理,比如检查表和列是否存在,还会解析名字和别名,然后预处理器会验证权限; 根据执行计划查询执行引擎,调用API接口调用存储引擎来查询数据;(5)将结果返回客户端,并进行缓存;SQL语句性能优化常用策略1、 为 WHERE 及 ORDER BY 涉及的列上建立索引对查询进行优化,应尽量避免全表扫描,首先应考虑在 WHERE 及 ORDER BY 涉及的列上建立索引。2、where中使用默认值代替null 应尽量避免在 WHERE 子句中对字段进行 NULL 值判断,创建表时 NULL 是默认值,但大多数时候应该使用 NOT NULL,或者使用一个特殊的值,如 0,-1 作为默认值。为啥建议where中使用默认值代替null,四个原因:(1)并不是说使用了is null或者 is not null就会不走索引了,这个跟mysql版本以及查询成本都有关;(2)如果mysql优化器发现,走索引比不走索引成本还要高,就会放弃索引,这些条件 !=,<>,is null,is not null经常被认为让索引失效;(3)其实是因为一般情况下,查询的成本高,优化器自动放弃索引的;(4)如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思也相对清晰一点;3、慎用 != 或 <> 操作符。MySQL 只有对以下操作符才使用索引:<,<=,=,>,>=,BETWEEN,IN,以及某些时候的 LIKE。所以:应尽量避免在 WHERE 子句中使用 != 或 <> 操作符, 会导致全表扫描。4、慎用 OR 来连接条件使用or可能会使索引失效,从而全表扫描;应尽量避免在 WHERE 子句中使用 OR 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,可以使用 UNION 合并查询:select id from t where num=10union allselect id from t where num=20一个关键的问题是否用到索引。他们的速度只同是否使用索引有关,如果查询需要用到联合索引,用 UNION all 执行的效率更高。多个 OR 的字句没有用到索引,改写成 UNION 的形式再试图与索引匹配。5、慎用 IN 和 NOT ININ 和 NOT IN 要慎用,否则会导致全表扫描。对于连续的数值,能用 BETWEEN 就不要用 IN:select id from t where num between 1 and 3。6、慎用 左模糊like ‘%…’模糊查询,程序员最喜欢的就是使用like,like很可能让索引失效。比如:select id from t where name like‘%abc%’ select id from t where name like‘%abc’ 而select id from t where name like‘abc%’才用到索引。所以:首先尽量避免模糊查询,如果必须使用,不采用全模糊查询,也应尽量采用右模糊查询, 即like ‘…%’,是会使用索引的; 左模糊like ‘%…’无法直接使用索引,但可以利用reverse + function index的形式,变化成 like ‘…%’; 全模糊查询是无法优化的,一定要使用的话建议使用搜索引擎,比如 ElasticSearch。 备注:如果一定要用左模糊like ‘%…’检索, 一般建议 ElasticSearch+Hbase架构7、WHERE条件使用参数会导致全表扫描。如下面语句将进行全表扫描:select id from t where num=@num因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推 迟到 运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。所以, 可以改为强制查询使用索引:select id from t with(index(索引名)) where num=@num8、用 EXISTS 代替 IN 是一个好的选择很多时候用exists 代替in 是一个好的选择:select num from a where num in(select num from b) 用下面的语句替换: select num from a where exists(select 1 from b where num=a.num)9、索引并不是越多越好索引固然可以提高相应的 SELECT 的效率,但同时也降低了 INSERT 及 UPDATE 的效。因为 INSERT 或 UPDATE 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。10、尽量使用数字型字段(1)因为引擎在处理查询和连接时会逐个比较字符串中每一个字符;(2)而对于数字型而言只需要比较一次就够了;(3)字符会降低查询和连接的性能,并会增加存储开销;所以:尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。11、尽可能的使用 varchar, nvarchar 代替 char, nchar(1)varchar变长字段按数据内容实际长度存储,存储空间小,可以节省存储空间;(2)char按声明大小存储,不足补空格;(3)其次对于查询来说,在一个相对较小的字段内搜索,效率更高;因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。*14、查询SQL尽量不要使用select ,而是具体字段最好不要使用返回所有:select * from t ,用具体的字段列表代替 “*”,不要返回用不到的任何字段。select *的弊端:(1)增加很多不必要的消耗,比如CPU、IO、内存、网络带宽;(2)增加了使用覆盖索引的可能性;(3)增加了回表的可能性;(4)当表结构发生变化时,前端也需要更改;(5)查询效率低;15、将需要查询的结果预先计算好将需要查询的结果预先计算好放在表中,查询的时候再Select,而不是查询的时候进行计算。16、IN后出现最频繁的值放在最前面如果一定用IN,那么:在IN后面值的列表中,将出现最频繁的值放在最前面,出现得最少的放在最后面,减少判断的次数。17、尽量使用 EXISTS 代替 select count(1) 来判断是否存在记录。count 函数只有在统计表中所有行数时使用,而且 count(1) 比 count(*) 更有效率。18、用批量插入或批量更新当有一批处理的插入或更新时,用批量插入或批量更新,绝不会一条条记录的去更新。(1)多条提交INSERT INTO user (id,username) VALUES(1,'xx'); INSERT INTO user (id,username) VALUES(2,'yy');(2)批量提交INSERT INTO user (id,username) VALUES(1,'xx'),(2,'yy'); 默认新增SQL有事务控制,导致每条都需要事务开启和事务提交,而批量处理是一次事务开启和提交,效率提升明显,达到一定量级,效果显著,平时看不出来。19、将不需要的记录在 GROUP BY 之前过滤掉提高 GROUP BY 语句的效率,可以通过将不需要的记录在 GROUP BY 之前过滤掉。下面两个查询返回相同结果,但第二个明显就快了许多。低效:SELECT JOB, AVG(SAL) FROM EMP GROUP BY JOB HAVING JOB = 'PRESIDENT' OR JOB = 'MANAGER' 高效:SELECT JOB, AVG(SAL) FROM EMP WHERE JOB = 'PRESIDENT' OR JOB = 'MANAGER' GROUP BY JOB20、避免死锁在你的存储过程和触发器中访问同一个表时总是以相同的顺序;事务应经可能地缩短,在一个事务中应尽可能减少涉及到的数据量;永远不要在事务中等待用户输入。21、索引创建规则:表的主键、外键必须有索引;数据量超过 300 的表应该有索引;经常与其他表进行连接的表,在连接字段上应该建立索引;经常出现在 WHERE 子句中的字段,特别是大表的字段,应该建立索引;索引应该建在选择性高的字段上;索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;复合索引的建立需要进行仔细分析,尽量考虑用单字段索引代替;正确选择复合索引中的主列字段,一般是选择性较好的字段;复合索引的几个字段是否经常同时以 AND 方式出现在 WHERE 子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;如果复合索引中包含的字段经常单独出现在 WHERE 子句中,则分解为多个单字段索引;如果复合索引所包含的字段超过 3 个,那么仔细考虑其必要性,考虑减少复合的字段;如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;频繁进行数据操作的表,不要建立太多的索引; 删除无用的索引,避免对执行计划造成负面影响;表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大。 尽量不要对数据库中某个含有大量重复的值的字段建立索引。22、在写 SQL 语句时,应尽量减少空格的使用查询缓冲并不自动处理空格,因此,在写 SQL 语句时,应尽量减少空格的使用,尤其是在 SQL 首和尾的空格(因为查询缓冲并不自动截取首尾空格)。23、每张表都设置一个 ID 做为其主键我们应该为数据库里的每张表都设置一个 ID 做为其主键,而且最好的是一个 INT 型的(推荐使用 UNSIGNED),并设置上自动增加的 AUTO_INCREMENT 标志。24、使用explain分析你SQL执行计划(1)typesystem:表仅有一行,基本用不到;const:表最多一行数据配合,主键查询时触发较多;eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型;ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取;range:只检索给定范围的行,使用一个索引来选择行。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符,用常量比较关键字列时,可以使用range;index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小;all:全表扫描;性能排名:system > const > eq_ref > ref > range > index > all。 实际sql优化中,最后达到ref或range级别。(2)Extra常用关键字Using index:只从索引树中获取信息,而不需要回表查询;Using where:WHERE子句用于限制哪一个行匹配下一个表或发送到客户。除非你专门从表中索取或检查所有行,如果Extra值不为Using where并且表联接类型为ALL或index,查询可能会有一些错误。需要回表查询。Using temporary:mysql常建一个临时表来容纳结果,典型情况如查询包含可以按不同情况列出列的GROUP BY和ORDER BY子句时;**25、当只要一行数据时使用 LIMIT 1 **当你查询表的有些时候,你已经知道结果只会有一条结果,但因为你可能需要去fetch游标,或是你也许会去检查返回的记录数。在这种情况下,加上 LIMIT 1 可以增加性能。这样一来,MySQL 数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据。26、将大的DELETE,UPDATE、INSERT 查询变成多个小查询能写一个几十行、几百行的SQL语句是不是显得逼格很高?然而,为了达到更好的性能以及更好的数据控制,你可以将他们变成多个小查询。27、合理分表 尽量控制单表数据量的大小,建议控制在500万以内500万并不是MySQL数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。
秋叶无缘
爬虫入门实战(标价400的单子-2)
前言在上篇文章中,我们整个爬虫任务进行了分析,同时也通过一个简单的requests.get()方法获取到了页面源码。接下来我们要做的就是通过页面源码解析获取到我们想要的数据。(在这里是每个律师的个人界面的链接)一、beatifulsoup介绍Beautiful Soup是一个可以从HTML或XML文件中提取数据的Python库,简单来说,它能将HTML的标签文件解析成树形结构,然后方便地获取到指定标签的对应属性。通过Beautiful Soup库,我们可以将指定的class或id值作为参数,来直接获取到对应标签的相关数据。简单来说,就是把html或者xml源代码进行了格式化,方便我们对其中的节点、标签、属性等进行进一步的操作.我们都知道HTML是一种标签语言,类似一种树形结构,而beatifulsoup则提供了封装好的方法从这些结构中提取出我们想要的资源。二、实战讲解库的安装就不讲了,太基础了。import requests
from bs4 import BeautifulSoup
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'}
url = "https://www.hklawsoc.org.hk/zh-HK/Serve-the-Public/The-Law-List/Members-with-Practising-Certificate?name=&jur=&sort=1&pageIndex=1#tips"
response = requests.get(url, headers=headers)
html = response.content.decode('utf-8', 'ignore')
my_page = BeautifulSoup(html, 'lxml')
print(type(my_page))# <class 'bs4.BeautifulSoup'>
my_page = BeautifulSoup(html, 'lxml')
print(type(my_page))# <class 'bs4.BeautifulSoup'>首先通过BeautifulSoup对我们之前获取到的页面源码进行解析,这样就可以通过BeautifulSoup里面的方法对my_page对象进行解构。三、寻找元素定位用我们之前讲过的定位方法(F12弹框左上角的小箭头)。在这里我们发现,包含三个律师的信息表的根元素是<table class="responsive">table下的一个<tr> ...</tr>就代表一行,所以思路很清晰了吧。通过find方法结合标签和class名称定位整个table通过find_all方法,找出所有的tr元素table=my_page.find('table', class_='responsive')
my_tr = table.find_all('tr') #my_tr是个列表
print(len(my_tr)) # 31这里是31行,注意第一行是表头,不是我们想要的元素,记得跳过。四、进一步的元素定位刚才我们找到的是一行元素(包括序号,姓名等),所以我们还要进一步的定位找出我们想要的超链接出来。超链接是挂在姓名上的,定位姓名一个tr标签里面有三个td分别代表了,序号,英文名,中文名。然后那个链接在第二个td的第二个div中,所以思路明确。tr=my_tr[1]
target_td=tr.find_all('td')[1]
target_a=target_td.find('a')
href=target_a.attrs['href']这里为什么直接用td的a元素,a元素不是两个嘛。这个自己输出下,应该就明白了。target_td=tr.find_all('td')[1]
target_a=target_td.find_all('a')
print(target_a)输出是这样的,仅有一个元素:[<a href="https://www.hklawsoc.org.hk/zh-HK/Serve-the-Public/The-Law-List/Member-Details?MemId=6726">ABATE DUNCAN ARTHUR WILLIAM</a>]问题来了,为什么我们F12看到的是两个,而输出却是一个。这个也是很好理解的。因为前端有很多界面元素都是变化的,可能我们打开页面触及到一些js函数导致界面的变化。而我们直接requests.get到的源码没有经过任何操作。 所以如果有问题,最好还是自己输出下。五、结果:因为我们不可能只保留超链接,所以我这里处理就是把所有的元素都加进去了。for tr in my_tr[1:]:
row = []
for td in tr.find_all('td'):
row.append(td.text.replace('\n', ''))
target_td=tr.find_all('td')[1]
target_a=target_td.find('a')
row.append(target_a.attrs['href'].replace('\n', ''))
print(row)输出结果如下: 找出个人链接的完整代码import requests
from bs4 import BeautifulSoup
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'}
flag=True
for i in range(1, 11339//30):
# print(i/(11339//30),end=' ')
url = "https://www.hklawsoc.org.hk/zh-HK/Serve-the-Public/The-Law-List/Members-with-Practising-Certificate?name=&jur=&sort=1&pageIndex="+str(i)+"#tips"
response = requests.get(url, headers=headers)
html = response.content.decode('utf-8', 'ignore')
my_page = BeautifulSoup(html, 'lxml')
print(type(my_page))# <class 'bs4.BeautifulSoup'>
table=my_page.find('table', class_='responsive')
my_tr = table.find_all('tr')
print(len(my_tr)) # 31
for tr in my_tr[1:]:
row = []
for td in tr.find_all('td'):
row.append(td.text.replace('\n', ''))
target_td=tr.find_all('td')[1]
target_a=target_td.find('a')
row.append(target_a.attrs['href'].replace('\n', ''))
print(row)个人总结beatifulsoup这里我只是根据案例讲了应用,相关的知识点,其实网络也有很多,所以没有详细的讲解。这里可能更加侧重爬虫任务的思路。目前这个简单的任务就告一段落了,至于之后访问这些爬下来的个人界面的链接获取详细信息,其实和这个思路相差不大,都是分析界面然后find、find_all方法逐级定位元素,通过text、attrs方法搞到具体的文本或者属性。如果有时间的话,会讲一讲个人信息界面爬取的思路的。因为这个任务是一个同行没有搞好,然后我才能接到的任务。记住那个两个元素和一个元素的区别,以后爬虫会经常遇到这样问题。甚至你想要的数据因为没有操作导致无法获取到。这也是网站的一个反爬机制,也是我们爬虫的一大难点。当然本专栏后续会更新对应的解决方法的。
秋叶无缘
SQL从提交到执行到底经历了什么
一、什么是SQLsql(Structured Query Language: 结构化查询语言)是高级的非过程化编程语言,允许用户在高层数据结构上工作, 是一种数据查询和程序设计语言, 也是(ANSI)的一项标准的计算机语言. but... 目前仍然存在着许多不同版本的sql语言,为了与ANSI标准相兼容, 它们必须以相似的方式共同地来支持一些主要的命令(比如SELECT、UPDATE、DELETE、INSERT、WHERE等等).在标准SQL中, SQL语句包含四种类型DML(Data Manipulation Language):数据操作语言,用来定义数据库记录(数据)。DCL(Data Control Language):数据控制语言,用来定义访问权限和安全级别。DQL(Data Query Language):数据查询语言,用来查询记录(数据)。DDL(Data Definition Language):数据定义语言,用来定义数据库对象(库,表,列等)二、如何执行SQL2.1 mysql以mysql为例, sql执行流程大致分为以下节点(mysql server层代码, 不包含引擎层事务/log等操作):mysqlLex: mysql自身的词法分析程序, C++语言开发, 基于输入的语句进行分词, 并解析除每个分词的意义. 分词的本质便是正则表达式的匹配过程. 源码在sql/sql_lex.ccBision: 根据mysql定义的语法规则,进行语法解析,语法解析就是生成语法树的过程. 核心是如何涉及合适的存储结构以及相关算法,去存储和遍历所有的信息语法解析中,生成语法树:mysql分析器: SQL解析, 针对关键词/非关键词进行提取、解析, 并生成解析语法树. 如果分析到语法错误,会抛出异常: ERROR: You have an error in your SQL syntax. 同时该阶段也会做一些校验, 如不存在字段会抛出异常: unknow column in field list.引申点:a. 语法树生成规则b. mysql的优化规则2.2 hive sqlHive 是基于Hadoop 构建的一套数据仓库分析系统,它提供了丰富的SQL查询方式来分析存储在Hadoop 分布式文件系统中的数据,可以将结构化的数据文件映射为一张数据库表,并提供完整的SQL查询功能,可以将SQL语句转换为MapReduce任务进行运行,通过自己的SQL 去查询分析需要的内容,这套SQL 简称Hive SQL,使不熟悉mapreduce 的用户很方便的利用SQL 语言查询,汇总,分析数据hive架构图:Driver:输入了sql字符串,对sql字符串进行解析,转化程抽象语法树,再转化成逻辑计划,然后使用优化工具对逻辑计划进行优化,最终生成物理计划(序列化反序列化,UDF函数),交给Execution执行引擎,提交到MapReduce上执行(输入和输出可以是本地的也可以是HDFS/Hbase)见下图的hive架构hiveSql的执行流程如下:sql写出来以后只是一些字符串的拼接,所以要经过一系列的解析处理,才能最终变成集群上的执行的作业(1)Parser:将sql解析为AST(抽象语法树),会进行语法校验,AST本质还是字符串(2)Analyzer:语法解析,生成QB(query block)(3)Logicl Plan:逻辑执行计划解析,生成一堆Opertator Tree(4)Logical optimizer:进行逻辑执行计划优化,生成一堆优化后的Opertator Tree(5)Phsical plan:物理执行计划解析,生成tasktree(6)Phsical Optimizer:进行物理执行计划优化,生成优化后的tasktree,该任务即是集群上的执行的作业结论:经过以上的六步,普通的字符串sql被解析映射成了集群上的执行任务,最重要的两步是 逻辑执行计划优化和物理执行计划优化(图中红线圈画)Antlr: Antrl是一种语言识别的工具, 基于java开发, 可以用来构造领域语言. 它提供了一个框架,可以通过包含java, C++, 或C#动作(action)的语法描述来构造语言识别器, 编译器和解释器.Antlr完成了hive 词法分析、语法分析、语义分析、中间代码生成的过程.AST语法树举例:引申学习:a. 从hivesql的执行机制可以看出, hive并不适合用于联机事务处理, 无法提供实时查询功能;最适合应用在基于大量不可变数据的批处理作业b. Antlr的解析流程c. hive的优化规则2.3 flink sqlFlink SQL是Flink中最高级的抽象, 可以划分为 SQL --> Table API --> DataStream/DataSetAPI --> Stateful Stream ProcessingFlink SQL包含 DML 数据操作语言、 DDL 数据语言, DQL 数据查询语言,不包含DCL语言。(1)首先,FlinkSQL 底层使用的是 apache Calcite 引擎来处理SQL语句,Calcite会使用 javaCC 做SQL解析,javaCC根据Calcite中定义的 Parser.jj 文件,生成一系列的java代码,生成的java代码会把SQL转换成AST抽象语法树(即SQLNode类型)。(2)生成的 SqlNode 抽象语法树,他是一个未经验证的抽象语法树,这时 SQL Validator 会获取 Flink Catalog 中的元数据信息来验证 sql 语法,元数据信息检查包括表名,字段名,函数名,数据类型等检查。然后生成一个校验后的SqlNode。(3)到达这步后,只是将 SQL 解析到 java 数据结构的固定节点上,并没有给出相关节点之间的关联关系以及每个节点的类型信息。所以,还需要将 SqlNode 转换为逻辑计划,也就是LogicalPlan,在转换过程中,会使用 SqlToOperationConverter 类,来将 SqlNode 转换为 Operation,Operation 会根据SQL语法来执行创建表或者删除表等操作,同时FlinkPlannerImpl.rel()方法会将SQLNode转换成RelNode树,并返回RelRoot。(4)第4步将执行 Optimize 操作,按照预定义的优化规则 RelOptRule 优化逻辑计划。Calcite 中的优化器RelOptPlanner有两种,一是基于规则优化(RBO)的HepPlanner,二是基于代价优化(CBO)的VolcanoPlanner。然后得到优化后的RelNode, 再基于Flink里面的rules将优化后的逻辑计划转换成物理计划。(5)第5步 执行 execute 操作,会通过代码生成 transformation,然后递归遍历各节点,将DataStreamRelNode 转换成DataStream,在这期间,会依次递归调用DataStreamUnion、DataStreamCalc、DataStreamScan类中重写的 translateToPlan方法。递归调用各节点的translateToPlan,实际是利用CodeGen元编成Flink的各种算子,相当于直接利用Flink的DataSet或者DataStream开发程序。(6)最后进一步编译成可执行的 JobGraph 提交运行。Flink SQL使用 Apache Calcite 作为解析器和优化器Calcite : 一种动态数据管理框架,它具备很多典型数据库管理系统的功能 如SQL 解析、 SQL 校验、 SQL 查询优化、 SQL 生成以及数据连接查询等,但是又省略了一些关键的功能,如 Calcite并不存储相关的元数据和基本数据,不完全包含相关处理数据的算法等。引申学习:a. flink sql优化规则三、常见SQL解析引擎解析引擎开发语言使用场景总结antlrjavapresto1. 包含三大主要功能: 词法分析器、语法分析器、树解析器2. 支持定义领域语言calcitejavaCCflink1. 抽象语法树2. 支持使用 FreeMarker 模版引擎扩展语法3. 能够与数据库创建查询持续补充ing...四、总结在实际工作过程中会涉及到相关的sql优化, 比如将非研发的业务老师写的复杂嵌套sql后台自动改为非嵌套执行,提高查询性能. 支持redisSQL, 以标准SQL格式解析成后台可执行的redis命令. 目前采用的开源jsqlparser框架来实现语法树的解析, 好处是操作简单, 只对sql语句进行拆分, 解析成java类的层次结构,支持visitor模式, 与数据库无关. 缺点是只支持常见的SQL语法集, 如若要扩展语法需改其源码, 对代码的侵入性与维护性造成影响.想要做好sql解析优化相关的工作, 还是要深入了解sql的执行原理, 了解各个sql引擎的特点与优劣. 站在架构的角度来思考来思考问题.工欲善其事,必先利其器.
秋叶无缘
爬虫入门实战(标价400的单子-1)
写在前面这里想做这个专栏很久了,不仅是想把自己在工作时接触到一些比较棘手的问题和解决方法记录下来,而且也想帮助初学者了解一个爬虫的完整流程。我目前的想法是更新一个简单的表格爬虫(一个基本没有反扒手段的网站)来入门。然后搞个selinunm自动化爬一下淘宝的商品类目、价格等信息来帮助大家进阶爬虫之路。最后我会分享我的终极爬虫技巧,大巧不工。其实,我的爬虫技术也是业余的,野路子。写这些文章只能帮助初学者进行爬虫一个了解,能接一些小点的单子。但是真正要应聘爬虫工程师是远远不够的。后面有时间的话我也会进行学习爬虫(如selinum伪装成正常请求,安卓模拟器app爬虫)爬虫概览其实我们或多或少都是听说过爬虫这个概念,这个东西的技术栈其实也比较成熟了。其实在我的理解中,爬虫嘛,就是给自己伪装一下,装成是正常的访问请求,然后获取到网站或者APP中的数据资源的一种技术手段。当然目前大部分爬虫都是python写的,毕竟python丰富的第三方库资源还有语言优势摆在这里。所以,我们这里也是通过python进行爬虫的编写。一、项目需求一个香港的老板应该是,他给个网址里面大概是这样的点进去那个箭头的之后是个超链接,然后,要把这个信息爬下来。需求挺明确的,而且这个网站,连一些基础的反爬手段都没有,非常适合用来入门实战。二、分析网页搞到所有律师的信息的流程清晰的分为两个步骤:1.搞到所有律师的个人介绍的超链接;2.然后再对所有链接进行访问。可以看到哈,这个记录还是有点多的,11339条,30个记录一页。要全部拿下来不是一件简单的事,1.找到所有页面的链接:这个是第一页。 这个是第二页。 让我们来看看他的链接:第一页:www.hklawsoc.org.hk/zh-HK/Serve…第二页:www.hklawsoc.org.hk/zh-HK/Serve…找到不同了嘛.第二页多出了一个字段:&pageIndex=2#tips 可以把这个数字换成1和3,试一下.&pageIndex=1#tips &pageIndex=3#tips 所以所有页面的规律就找到了,用来访问的链接如下: for i in range(1, 11339//30):
print(i/(11339//30),end=' ')
url = "https://www.hklawsoc.org.hk/zh-HK/Serve-the-Public/The-Law-List/Members-with-Practising-Certificate?name=&jur=&sort=1&pageIndex="+str(i)+"#tips"这个规律真的很简单,所以就用来爬虫的入门了.2.找到所有律师的个人页面链接:这个就是一个页面分析的任务了。还记得我们刚才是点这个链接访问的这个律师个人信息界面吧,这里面必然有着个人界面的超链接,我们需要的就是把它扒出来出来就可以了。下面我们在这个页面,按F12,查看,操作如下:点击F12,进入下面的界面 2. 点箭头指向的按钮3. 然后点击你想扒出超链接的元素点击后:这个超链接很显眼了吧。点进去,正好是我们想要的链接。三、开始爬取还记得我们开始说的嘛,爬虫是封装成正常的请求去访问页面然后下载我们想要的资源,对吧所以,这里细化为两个步骤请求页面资源和解析页面资源找到我们想要的数据**(这里我们想要的是律师个人页面的超链接)**这里先进行第一步:请求页面资源爬虫的技术获取网页的手段最基础的就是python的requests方法了。我们这里用的也是这个方法。requests的请求在这里也比较简单,就是简单的get请求,其实也有post的请求,网络上有很多资料,我这里就不赘述了。简单的requests的get请求代码如下:import requests
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'}
for i in range(1, 11339//30):
print(i/(11339//30),end=' ')
url = "https://www.hklawsoc.org.hk/zh-HK/Serve-the-Public/The-Law-List/Members-with-Practising-Certificate?name=&jur=&sort=1&pageIndex="+str(i)+"#tips"
response = requests.get(url, headers=headers)
html = response.content.decode('utf-8', 'ignore')这里response就是我们请求下来的页面资源了,经过源码解析获取到了html源码。当然,你可能会有疑问这个headers从哪来的,哪里规定的。每个浏览器都有自己的headers,因为headers要模仿你自己的浏览器向网页发送信息。如果使用Python进行爬取页面时,使用了别人的headers可能会导致爬取不到任何数据(因为代码在你自己的电脑运行,所以无法模拟别人的浏览器)当然其实用别人也可以,有的网站他可能安全做的没有那么好,就都还可以正常访问。当然,所以如何查找自己headers也很重要,具体步骤如下:随便打开一个网页,例如打开我们这个页面,右键点击‘检查’或者按F12,出现下图页面。2. 点击network3. F5刷新 4. 在name框随便点一个选项,在右侧点击‘headers’选项,在最下方找到‘User-Agent:’,粘贴到自己代码即可。如下图个人总结:这里主要是介绍一个爬虫的基本流程,能够帮助我们爬下来我们想要的页面源码。当然光爬下来源码是远远不够的,还需要各种规则(lxml、beautifulsoup以及正则表达式)的解析才能获取到从整个源码中获取我们想要的数据。
秋叶无缘
MYSQL 事务的底层原理 | 京东物流技术团队
事务的底层原理在事务的实现机制上,MySQL 采用的是 WAL:Write-ahead logging,预写式日志,机制来实现的。在使用 WAL 的系统中,所有的修改都先被写入到日志中,然后再被应用到系统中。通常包含 redo 和 undo 两部分信息。为什么需要使用 WAL,然后包含 redo 和 undo 信息呢?举个例子,如果一个系统直接将变更应用到系统状态中,那么在机器掉电重启之后系统需要知道操作是成功了,还是只有部分成功或者是失败了。如果使用了 WAL,那么在重启之后系统可以通过比较日志和系统状态来决定是继续完成操作还是撤销操作。redo log 称为重做日志,每当有操作时,在数据变更之前将操作写入 redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。undo log 称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。MySQL 中用 redo log 来在系统 Crash 重启之类的情况时修复数据,而 undo log 来保证事务的原子性。事务 id一个事务可以是一个只读事务,或者是一个读写事务:可以通过 START TRANSACTION READ ONLY 语句开启一个只读事务。在只读事务中不可以对普通的表进行增、删、改操作,但可以对用户临时表做增、删、改操作。可以通过 START TRANSACTION READ WRITE 语句开启一个读写事务,或者使用 BEGIN、START TRANSACTION 语句开启的事务默认也算是读写事务。在读写事务中可以对表执行增删改查操作。如果某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储引擎就会给它分配一个独一无二的事务 id,针对 MySQL 5.7 分配方式如下:对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务 id,否则的话是不分配事务 id 的。对于读写事务来说,只有在它第一次对某个表执行增、删、改操作时才会为这个事务分配一个事务 id,否则的话也是不分配事务 id 的。有的时候虽然开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务 id。这个事务 id 本质上就是一个数字,它的分配策略和隐藏列 row_id 的分配策略大抵相同,具体策略如下:服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务 id 时,就会把该变量的值当作事务 id 分配给该事务,并且把该变量自增 1。每当这个变量的值为 256 的倍数时,就会将该变量的值刷新到系统表空间的页号为 5 的页面中一个称之为 Max Trx ID 的属性处,这个属性占用 8 个字节的存 储空间。当系统下一次重新启动时,会将上边提到的 Max Trx ID 属性加载到内存中,将该值加上 256 之后赋值给全局变量,因为在上次关机时该全局变量的值可能大于Max Trx ID 属性值。这样就可以保证整个系统中分配的事务 id 值是一个递增的数字。先被分配 id 的事务得到的是较小的事务 id,后被分配 id 的事务得到的是较大的事务 id。mvcc全称 Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。同一行数据平时发生读写请求时,会上锁阻塞住。但 MVCC 用更好的方式去处理读写请求,做到在发生读写请求冲突时不用加锁。这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。MVCC 原理在事务并发执行遇到的问题如下:脏读:如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读;不可重复读:如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读;幻读:如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读,幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,幻读只是重点强调了读取到了之前读取没有获取到的记录。MySQL 在 REPEATABLE READ 隔离级别下,是可以很大程度避免幻读问题的发生的。版本链对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列;roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修 改前的信息;演示-- 创建表
CREATE TABLE mvcc_test (
id INT,
name VARCHAR(100),
domain varchar(100),
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
-- 添加数据
INSERT INTO mvcc_test VALUES(1, 'habit', '演示mvcc');
假设插入该记录的事务 id=50,那么该条记录的展示如图:假设之后两个事务 id 分别为 70、90 的事务对这条记录进行 UPDATE 操作。trx_id=70trx_id=90beginbeginupdate mvcc_test set name='habit_trx_id_70_01' where id=1update mvcc_test set name='habit_trx_id_70_02' where id=1commitupdate mvcc_test set name='habit_trx_id_90_01' where id=1update mvcc_test set name='habit_trx_id_90_02' where id=1commit每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性,可以将这些 undo 日志都连起来,串成一个链表。对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为:多版本并发控制,即 MVCC。ReadView对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录。对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别是从哪里来的,其实结合前面的知识,这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的。为此,InnoDB 提出了一个 ReadView 的概念,这个 ReadView 中主要包含 4 个比较重要的内容:m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的事务id 列表;min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id,也就是 m_ids 中的最小值;max_trx_id:表示在生成 ReadView 时系统中应该分配给下一个事务的 id 值,注:max_trx_id 并不是 m_ids 中的最大值,事务 id 是递增分配的。比方说现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4;creator_trx_id:表示生成该 ReadView 的事务的事务 id;有了这个 ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问;如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问;如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问;如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id之间 min_trx_id < trx_id < max_trx_id,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问;如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录;在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的一个非常大的区别就是它们生成 ReadView 的时机不同。还是以表 mvcc_test 为例,假设现在表 mvcc_test 中只有一条由事务 id 为 50 的事务插入的一条记录,接下来看一下 READ COMMITTED 和 REPEATABLE READ 所谓的生成 ReadView 的时机不同到底不同在哪里。READ COMMITTED: 每次读取数据前都生成一个 ReadView;比方说现在系统里有两个事务id 分别为 70、90 的事务在执行:-- T 70
UPDATE mvcc_test SET name = 'habit_trx_id_70_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'habit_trx_id_70_02' WHERE id = 1;
此时表 mvcc_test 中 id 为 1 的记录得到的版本链表如下所示:假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:-- 使用 READ COMMITTED 隔离级别的事务
BEGIN;
-- SELECE1:Transaction 70、90 未提交
SELECT * FROM mvcc_test WHERE id = 1;
-- 得到的列 name 的值为'habit'
这个 SELECE1 的执行过程如下:在执行 SELECT 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是[70, 90],min_trx_id 为 70,max_trx_id 为 91,creator_trx_id 为 0。然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,在 m_ids 列表内,所以不符合可见性要求第 4 条:**如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id之间 min_trx_id < trx_id < max_trx_id,那就需要判断一下trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。**根据 roll_pointer 跳到下一个版本。下一个版本的列 name 的内容是 habit_trx_id_70_01,该版本的 trx_id 值也为 70,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。下一个版本的列 name 的内容是 habit,该版本的 trx_id 值为 50,小于 ReadView 中的 min_trx_id 值,所以这个版本是符合要求的第 2 条:**如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。**最后返回的版本就是这条列 name 为 habit 的记录。之后,把事务 id 为 70 的事务提交一下,然后再到事务 id 为 90 的事务中更新一下表 mvcc_test 中 id 为 1 的记录:-- T 90
UPDATE mvcc_test SET name = 'habit_trx_id_90_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'habit_trx_id_90_02' WHERE id = 1;
此时表 mvcc 中 id 为 1 的记录的版本链就长这样:然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:-- 使用 READ COMMITTED 隔离级别的事务
BEGIN;
-- SELECE1:Transaction 70、90 均未提交
SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列 name 的值为'habit'
-- SELECE2:Transaction 70 提交,Transaction 90 未提交
SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列 name 的值为'habit_trx_id_70_02'
这个SELECE2 的执行过程如下:在执行 SELECT 语句时又会单独生成一个 ReadView,该 ReadView 的 m_ids 列表的内容就是[90],min_trx_id 为90,max_trx_id 为 91,creator_trx_id 为 0。然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_90_02,该版本的 trx_id 值为 90,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。下一个版本的列 name 的内容是 habit_trx_id_90_01,该版本的 trx_id 值为 90,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。下一个版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,小于 ReadView 中的 min_trx_id 值 90,所以这个版本是符合要求的,最后返回这个版本中列 name 为 habit_trx_id_70_02 的记录。以此类推,如果之后事务 id 为 90 的记录也提交了,再次在使用 READ COMMITTED 隔离级别的事务中查询表 mvcc_test 中 id 值为 1 的记录时,得到的结果就是 habit_trx_id_90_02 了。**总结:**使用 READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。**REPEATABLE READ:**在第一次读取数据时生成一个 ReadView;对于使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了。比方说现在系统里有两个事务id 分别为 70、90 的事务在执行:-- T 70
UPDATE mvcc_test SET name = 'habit_trx_id_70_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'habit_trx_id_70_02' WHERE id = 1;
此时表 mvcc_test 中 id 为 1 的记录得到的版本链表如下所示:假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:-- 使用 REPEATABLE READ 隔离级别的事务
BEGIN;
-- SELECE1:Transaction 70、90 未提交
SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列name 的值为'habit'
这个 SELECE1 的执行过程如下:在执行 SELECT 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是[70, 90],min_trx_id 为 70,max_trx_id 为 91,creator_trx_id 为 0。然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。下一个版本的列 name 的内容是 habit_trx_id_70_01,该版本的 trx_id 值也为 70,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。下一个版本的列 name 的内容是 habit,该版本的 trx_id 值为 50,小于 ReadView 中的 min_trx_id 值,所以这个版本是符合要求的,最后返回的就是这条列name 为 habit 的记录。之后,把事务 id 为 70 的事务提交一下,然后再到事务 id 为 90 的事务中更新一下表 mvcc_test 中 id 为 1 的记录:-- 使用 REPEATABLE READ 隔离级别的事务
BEGIN;
UPDATE mvcc_test SET name = 'habit_trx_id_90_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'habit_trx_id_90_02' WHERE id = 1;
此刻,表 mvcc_test 中 id 为 1 的记录的版本链就长这样:然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:-- 使用 REPEATABLE READ 隔离级别的事务
BEGIN;
-- SELECE1:Transaction 70、90 均未提交
SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列 name 的值为'habit'
-- SELECE2:Transaction 70 提交,Transaction 90 未提交
SELECT * FROM mvcc_test WHERE id = 1; -- 得到的列 name 的值为'habit'
这个 SELECE2 的执行过程如下:因为当前事务的隔离级别为 REPEATABLE READ,而之前在执行 SELECE1 时已经生成过 ReadView 了,所以此时直接复用之前的 ReadView,之前的 ReadView的 m_ids 列表的内容就是[70, 90],min_trx_id 为 70,max_trx_id 为 91, creator_trx_id 为 0。然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 habit_trx_id_90_02,该版本的 trx_id 值为 90,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。下一个版本的列 name 的内容是 habit_trx_id_90_01,该版本的 trx_id 值为 90,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。下一个版本的列 name 的内容是 habit_trx_id_70_02,该版本的 trx_id 值为 70,而 m_ids 列表中是包含值为 70 的事务 id 的,所以该版本也不符合要求,同理下一个列 name 的内容是 habit_trx_id_70_01 的版本也不符合要求。继续跳到下一个版本。下一个版本的列 name 的内容是 habit,该版本的 trx_id 值为 50,小于 ReadView 中的 min_trx_id 值 70,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 habit 的记录。也就是说两次 SELECT 查询得到的结果是重复的,记录的列 name 值都是 habit,这就是可重复读的含义。如果之后再把事务 id 为 90 的记录提交了,然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,得到的结果还是 habit。MVCC 下的幻读解决和幻读现象REPEATABLE READ 隔离级别下 MVCC 可以解决不可重复读问题,那么幻读呢?MVCC 是怎么解决的?幻读是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自另一个事务添加的新记录。可以想想,在 REPEATABLE READ 隔离级别下的事务 T1 先根据某个搜索条件读取到多条记录,然后事务 T2 插入一条符合相应搜索条件的记录并提交,然后事务 T1 再根据相同搜索条件执行查询。结果会是什么?按照 ReadView 中的比较规则中的第 3 条和第 4 条不管事务 T2 比事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交的。但是,在 REPEATABLE READ 隔离级别下 InnoDB 中的 MVCC 可以很大程度地避免幻读现象,而不是完全禁止幻读。怎么回事呢?来看下面的情况:首先在事务 T1 中执行:select * from mvcc_test where id = 30; 这个时候是找不到 id = 30 的记录的。在事务 T2 中,执行插入语句:insert into mvcc_test values(30,'luxi','luxi');此时回到事务 T1,执行:update mvcc_test set domain='luxi_t1' where id=30;
select * from mvcc_test where id = 30;
事务T1 很明显出现了幻读现象。在 REPEATABLE READ 隔离级别下,T1 第一次执行普通的 SELECT 语句时生成了一个 ReadView,之后 T2 向 mvcc_test 表中新插入一条记录并提交。ReadView 并不能阻止 T1 执行 UPDATE 或者 DELETE 语句来改动这个新插入的记录,由于 T2 已经提交,因此改动该记录并不会造成阻塞,但是这样一来,这条新记录的 trx_id 隐藏列的值就变成了 T1 的事务 id。之后 T1 再使用普通的 SELECT 语句去查询这条记录时就可以看到这条记录了,也就可以把这条记录返回给客户端。因为这个特殊现象的存在,可以认为 MVCC 并不能完全禁止幻读。mvcc 总结从上边的描述中可以看出来,所谓的 MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用 READ COMMITTD、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读写、写读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ 这两个隔离级别的一个很大不同就是:生成 ReadView 的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了,从而基本上可以避免幻读现象。InnoDB 的 Buffer Pool对于使用 InnoDB 作为存储引擎的表来说,不管是用于存储用户数据的索引,包括:聚簇索引和二级索引,还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是 InnoDB 对文件系统上一个或几个实际文件的抽象,也就是说数据还是存储在磁盘上的。但是磁盘的速度慢,所以 InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,即使只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。Buffer PoolInnoDB 为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,这块连续内存叫做:Buffer Pool,中文名:缓冲池。默认情况下 Buffer Pool 只有 128M 大小。查看该值:show variables like 'innodb_buffer_pool_size';可以在启动服务器的时候配置 innodb_buffer_pool_size 参数的值,它表示 Buffer Pool 的大小,配置如下:[server]
innodb_buffer_pool_size = 268435456
其中,268435456 的单位是字节,也就是指定 Buffer Pool 的大小为 256M,Buffer Pool 也不能太小,最小值为 5M,当小于该值时会自动设置成 5M。启动 MySQL 服务器的时候,需要完成对 Buffer Pool 的初始化过程,就是先向操作系统申请 Buffer Pool 的内存空间,然后把它划分成若干对控制块和缓 存页。但是此时并没有真实的磁盘页被缓存到 Buffer Pool 中,之后随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。在 Buffer Pool 中会创建多个缓存页,默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB。那么怎么知道该页在不在 Buffer Pool 中呢?在查找数据的时候,先通过哈希表中查找 key 是否在哈希表中,如果在证明 Buffer Pool 中存在该缓存也信息,如果不存在证明不存该缓存也信息,则通过读取磁盘加载该页信息放到 Buffer Pool 中,哈希表中的 key 是通过表空间号+ 页号作组成的,value 是 Buffer Pool 的缓存页。flush 链表的管理如果修改了 Buffer Pool 中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为:脏页。最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后,并不着急把修改同步到磁盘上,而是在未来的某个时间进行同步。 但是如果不立即同步到磁盘的话,那之后再同步的时候怎么知道 Buffer Pool 中哪些页是脏页,哪些页从来没被修改过呢?总不能把所有的缓存页都同步到磁盘上吧,如果 Buffer Pool 被设置的很大,那一次性同步会非常慢。所以,需要再创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush 链表。刷新脏页到磁盘后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。从 flush 链表中刷新一部分页面到磁盘,后台线程也会定时从 flush 链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为:BUF_FLUSH_LIST。redo 日志redo 日志的作用InnoDB 存储引擎是以页为单位来管理存储空间的,增删改查操作其实本质上都是在访问页面,包括:读页面、写页面、创建新页面等操作。在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访问。但是在事务的时候又强调过一个称之为持久性的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。如果只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是所不能忍受的。那么如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:刷新一个完整的数据页太浪费了;有时候仅仅修改了某个页面中的一个字节,但是在 InnoDB 中是以页为单位来进行磁盘 IO 的,也就是说在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,一个页面默认是16KB 大小,只修改一个字节就要刷新 16KB 的数据到磁盘上显然是太浪费了。随机 IO 刷起来比较慢;一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的 Buffer Pool 中的页面刷新到磁盘时,需要进行很多的随机 IO,随机 IO 比顺序 IO 要慢,尤其对于传统的机械硬盘来说。只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,比方说:某个事务将系统表空间中的第 5 号页面中偏移量为 5000 处的那个字节的值 0 改成 5 只需要记录一下:将第 5 号表空间的 5 号页面的偏移量为 5000 处的值更新为:5。这样在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。因为在系统崩溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为:重做日志,即:redo log。与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的 redo log 刷新到磁盘的好处如下:redo log 占用的空间非常小存储表空间 ID、页号、偏移量以及需要更新的值所需的存储空间是很小的;redo log 是顺序写入磁盘的在执行事务的过程中,每执行一条语句,就可能产生若干条 redo log,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序 IO;redo log 的写入过程InnoDB 为了更好的进行系统崩溃恢复,把一次原子操作生成的 redo log 都放在了大小为 512 字节的块(block)中。为了解决磁盘速度过慢的问题而引入了 Buffer Pool。同理,写入 redo log 时也不能直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,即:redo log 缓冲区,也可以简称:log buffer。这片内存空间被划分成若干个连续的 redo log block,可以通过启动参数innodb_log_buffer_size 来指定 log buffer 的大小,该启动参数的默认值为:16MB。向 log buffer 中写入 redo log 的过程是顺序的,也就是先往前边的 block 中写,当该 block 的空闲空间用完之后再往下一个 block 中写。redo log 刷盘时机log buffer 什么时候会写入到磁盘呢?log buffer 空间不足时,如果不停的往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。InnoDB 认为如果当前写入 log buffer 的 redo log 量已 经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。事务提交时,必须要把修改这些页面对应的 redo log 刷新到磁盘。后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo log 到磁盘。正常关闭服务器时等等。undo 日志事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如:情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。情况二:程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前的事务的执行。这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,需要把东西改回原先的样子,这个过程就称之为回滚,即:rollback,这样就可以造成这个事务看起来什么都没做,所以符合原子性要求。每当要对一条记录做改动时,都需要把回滚时所需的东西都给记下来。比方说:插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉。删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中。修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值。这些为了回滚而记录的这些东西称之为撤销日志,即:undo log。这里需要注意的一点是,由于查询操作并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的 undo log。undo 日志的格式为了实现事务的原子性,InnoDB 存储引擎在实际进行增、删、改一条记录时,都需要先把对应的 undo 日志记下来。一般每对一条记录做一次改动,就对应着一条 undo 日志,但在某些更新记录的操作中,也可能会对应着 2 条 undo 日志。一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的 undo 日志,这些 undo 日志会被从 0 开始编号,也就是说根据生成的顺序分别被称为第 0 号 undo 日志、第 1 号 undo 日志、...、第 n 号 undo 日志等,这个编号也被称之为 undo no。这些 undo 日志是被记录到类型为 FIL_PAGE_UNDO_LOG 的页面中。这些页面可以从系统表空间中分配,也可以从一种专门存放 undo 日志的表空间,也就是所谓的 undo tablesp中分配。
秋叶无缘
从ClickHouse通往MySQL的几条道路
一、应用背景简介ClickHouse 是 Yandex(俄罗斯最大的搜索引擎)开源的一个用于实时数据分析的基于列存储的数据库,其处理数据的速度比传统方法快 100-1000 倍。ClickHouse 的性能超过了目前市场上可比的面向列的 DBMS,每秒钟每台服务器每秒处理数亿至十亿多行和数十千兆字节的数据。它是一个用于联机分析(OLAP)的列式数据库管理系统;(OLAP是仓库型数据库,主要是读取数据,做复杂数据分析,侧重技术决策支持,提供直观简单的结果)那 ClickHouse OLAP 适用场景有:1)读多于写;2)大宽表,读大量行但是少量列,结果集较小;3)数据批量写入,且数据不更新或少更新;4)无需事务,数据一致性要求低;5)灵活多变,不适合预先建模。MySQL是一个关系型数据库管理系统,广泛用于各种应用程序和网站开发。MySQL容易上手和学习,已经被广泛应用于各种生产环境中有良好的稳定性和可靠性,MySQL支持事务处理,能够保证数据的完整性和一致性,适合需要复杂数据处理和事务控制的应用。在我们应用中的使用场景来看,简单来说通常会看中了clickhouse在处理大批量数据的写入和读取分析方面的性能,MySQL会主要负责一些基于模型进行指标二次加工的高频查询及复杂join的查询。二、实际应用中存在的问题在数据相关应用处理过程中,一般会按下图的分层进行数据处理;现有的一个实际应用场景中,会把MySQL中的数据进行全量的更替,即在新一批基于ck模型加工的数据插入MySQL库表时删除原表全量数据,来实现对于最新全量数据的刷新;该处理机制因为完全不用考虑历史数据的包袱,每批次都是全量加工和替换,是一种运行简单、有效、数据加工的准确率高的机制,在小业务量场景下可以节省开发量和时间,弊端是在大业务场景下性能端会出现与之关联的多种问题;这些告警可能包括因为读写频率高引起的CPU使用率过高、因为binlog产生量过大导致的磁盘使用量告警等;负责加工的后端应用也可能也会因数据加工量过大而引发内存使用率过高的风险。基于现有架构设计和问题背景,需要对相关的问题进行一定的调研,来探索优化的可能性;三、几种处理方法及适配的场景分析1.使用数据库触发器(Trigger):在 ClickHouse 表中创建触发器,当订单数据发生变化时,触发器可以将更新操作发送到 MySQL 表中进行更新。触发器可以监视 ClickHouse 表中的 INSERT、UPDATE 和 DELETE 操作,并将相应的操作转发到 MySQL 表中。在类似于同步数据表的场景下,触发器场景比较合适,但是在面对需要高度定制化的数据加工场景下,就显得不太合适了,也不方便调试。2.此外,也会有通过外部触发器结合消息队列的方案可以支持处理这种情况。这里边会涉及到需要监听ClickHouse的binlog记录或者CDC(Change Data Capture)流,在数据发生变化时进行解析和转发。3.也可以在应用层面,来监听ClickHouse数据库的数据变化,并在变化发生时发送相关的消息到消息队列。例如使用Debezium库来监听ClickHouse数据库的数据变化。你可以根据自己的需求来配置连接信息、监听的表等,并在监听器中编写业务逻辑处理数据变化事件。这几种处理方式相对来说对于处理的变化量来说是比较大的,即所有对于数据库的操作过程都会被监听端响应处理,所以如果数据变化量非常大的话,那么监听消费端的压力也会随之上升;4.轮询查询:Java 应用可以定期轮询查询 ClickHouse 表的变化,通过比对新的订单数据和已有的订单数据,找出有变动的数据行,并进行相应的加工处理和更新操作。这其中的关键就是采用何种方法进行比对了。比对方法可以通过逐个字段的比对来筛选该行数据是否有变化,这种方法简单有效,但是瓶颈也比较明显:不适合处理大量数据,因为性能不算好;也可以通过把数据行进行哈希算法和摘要处理,来实现更快速的字段变化的比对,这种方式相对来说会更适合处理数据量大一些的场景;当然,处理过程并不限于查询过程,有些场景是在查询阶段并不需要筛选数据,而是基于原始模型加工完的数据结果上进行字段值比对或哈希处理,用来标记处理完的数据结果是否有变化,有变化的更新无则不处理,从而减小对指标结果数据的更新范围;以上内容是对于所与到问题的处理方法的一个浅显分析,如果您还有什么指标加工方面好的经验,欢迎指正和交流。
秋叶无缘
爬虫实战指南
本专栏将带您逐步学习中文爬虫技术,从基础到高级层面。通过实战案例,包括爬取标价400的单子和1000的单子,以及使用Selenium爬取淘宝商品类目,您将学会使用Python编写爬虫程序、理解网页结构、抓取和解析数据,并应对反爬机制。无论您是初学者还是有一定经验的开发者,本专栏将为您提供实用的中文爬虫解决方案,助您掌握强大的数据采集和自动化任务技能。
秋叶无缘
数据库优化与原理解析
以及本文以MySQL数据库为研究对象,讨论数据库的优化及原理解析等内容
秋叶无缘
【R语言数据科学】:数据基础处理(mutate,filter,select)
R语言数据科学】:(三)数据基础处理(mutate、filter、select等)参考资料: Data Analysis and Prediction Algorithms with R3.使用tidyverse包进行数据处理到目前为止,我们一直在通过索引对向量进行重新排序。然而,一旦我们开始更高级的分析,数据存储的首选单元不是向量,而是数据框。接下来介绍R语言数据分析中很重要的一个库:tideverse收入导入库,显示没有该库可以使用install.packages('要安装的库名')library(tidyverse)首先我们介绍一下tidy数据,我们把每一行代表一个观测值,每一列代表这些观测的不同变量的数据称为tidy格式,murders就是一个tidy数据head(murders)stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>1AlabamaALSouth47797361352.8244242AlaskaAKWest710231192.6751863ArizonaAZWest63920172323.6295274ArkansasARSouth2915918933.1893905CaliforniaCAWest3725395612573.3741386ColoradoCOWest5029196651.292453每一行代表一个州以及其他五个变量的观察数据,每一列都表示不同变量,符合tidy数据格式的描述,我们再来看看下面这个数据#> country year fertility
#> 1 Germany 1960 2.41
#> 2 South Korea 1960 6.16
#> 3 Germany 1961 2.44
#> 4 South Korea 1961 5.99
#> 5 Germany 1962 2.47
#> 6 South Korea 1962 5.79上述数据集也是一个tidy数据,但是它是处理后的数据,原始数据如下#> country 1960 1961 1962
#> 1 Germany 2.41 2.44 2.47
#> 2 South Korea 6.16 5.99 5.79每一行包含了几个观测值,并且year变量的值放到了表头中,因此该数据不是一个tidy格式,这种作数据分析时会有许多问题,我们在后面会更详细的介绍如何将上面这个数据转换3.1 处理数据框tidyverse的dplyr包引入了一些函数,这些函数在处理数据库执行一些最常见的操作,例如,要通过添加新列来更改数据表,我们使用mutate。为了将数据表过滤到行的子集,我们使用filter。最后,为了通过选择特定列来对数据进行子集划分,我们使用select3.1.1 使用mutate添加一个列我们希望数据表中包含分析所需的所有信息。因此,第一项任务是将谋杀率添加到我们的谋杀数据框中。函数mutate将要处理的数据框作为第一个参数,将变量的名称和值作为第二个参数。因此,为了增加谋杀率列,我们使用:library(dslabs)
data('murders')
murders <- mutate(murders, rate = total/population*100000)注意:我们使用了total和population在上述函数中,但是并没有报错,这是因为dplyr包的函数会默认的将第一个参数作为研究对象,那么就会自动查找第一个参数中相应的列,也就是murders$total,这样增强了代码的可读性和便捷性# 看一下新的数据框
head(murders)stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>1AlabamaALSouth47797361352.8244242AlaskaAKWest710231192.6751863ArizonaAZWest63920172323.6295274ArkansasARSouth2915918933.1893905CaliforniaCAWest3725395612573.3741386ColoradoCOWest5029196651.292453可以看到增加了rate一列,代表谋杀率,这里我们更新了murders数据集,但是如果我们使用data(murders)重新加载数据,可以得到之前的数据集3.1.2 使用filter筛选数据集现在假设我们只想观察谋杀率小于0.7的数据集,我们使用filter()函数,和mutate一样,第一个变量是数据框,第二个变量是筛选条件filter(murders, rate<=0.7)stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>HawaiiHIWest136030170.5145920IowaIANorth Central3046355210.6893484New HampshireNHNortheast131647050.3798036North DakotaNDNorth Central67259140.5947151VermontVTNortheast62574120.31962113.1.3 使用select选择某些特定列虽然给出的案例数据集只有六列,但有时数据集有上百列,但我们并不对所有的数据集都感兴趣,因此可以使用select得到原数据集的子集(我们关注的列),这个在数据分析时是很有用的。下面假设我们只想得到state,region,rate三列,使用如下代码:new_table <- select(murders, state, region, rate)
filter(new_table, rate<=0.7)stateregionrate<chr><fct><dbl>HawaiiWest0.5145920IowaNorth Central0.6893484New HampshireNortheast0.3798036North DakotaNorth Central0.5947151VermontNortheast0.3196211这样我们得到了哪些州以及地区的谋杀率低于0.7。# 我们之前讲了rank函数,下面我们要得到rank函数
mutate(murders, rank=rank(rate)) %>% head()stateabbregionpopulationtotalraterank<chr><chr><fct><dbl><dbl><dbl><dbl>1AlabamaALSouth47797361352.824424292AlaskaAKWest710231192.675186253ArizonaAZWest63920172323.629527424ArkansasARSouth2915918933.189390355CaliforniaCAWest3725395612573.374138386ColoradoCOWest5029196651.292453143.2 pipe管道:%>% 或者 |>在r中我们可以通过管道操作符%>%执行一系列操作,例如先select再filter。最新的R版本也可以使用|> 下面我们具体介绍一下管道操作符的应用。 在上面我们先使用select得到了一个新中间对象new_table,然后在使用filter对其过滤。我们可以通过管道操作符实现在不使用中间对象的情况下也得到上述结果:murders %>% select(state, region, rate) %>% filter(rate <= 0.7)stateregionrate<chr><fct><dbl>HawaiiWest0.5145920IowaNorth Central0.6893484New HampshireNortheast0.3798036North DakotaNorth Central0.5947151VermontNortheast0.3196211上述代码相当于之前分开写的两行代码。我们来分析一下管道运算符是如何起作用的,通常,将第一个管道的结果作为第二个管道的第一个参数,下面我们再来举一些例子来更好的理解管道16 %>% sqrt()416 %>% sqrt() %>% log2()23.3 数据描述性统计探索性数据分析的一个重要部分是描述性统计。平均值和标准差是广泛使用的描述数据的两个指标。本节介绍一些dplyr函数来处理数据,group_by、summarize和pull3.3.1 summarizedplyr中的summarize函数提供了一种通过直观易读的代码计算描述性统计信息的方法。我们以height数据为例。height数据集包括学生在课堂调查中报告的身高和性别。# 导入相关库library(dplyr)
library(dslabs)
data(heights)计算女性身高的平均值和标准差s <- heights %>% filter(sex == 'Female') %>%
summarise(avg = mean(height), sd = sd(height))
savgsd<dbl><dbl>64.939423.760656可以看出和其他dplyr函数一样,使用summarize函数也会自动将第一个参数作为数据集研究,因此可以直接访问height 下面我们来看一下summarize函数的另一个例子:我们要计算美国的平均犯罪率,注意,各州犯罪率的平均值不等于美国的平均犯罪率,因为各州的人数不一样us_murder_rate <- murders %>%
summarise(rate=sum(total)/sum(population)*100000)
us_murder_raterate<dbl>3.0345553.3.2 多个描述统计量例如我们要统计某个变量的中位数,最小值,最大值。我们可以使用quantile分位数函数。heights %>% filter(sex == 'Female') %>% summarise(meidian_min_max = quantile(height, c(0.5,0,1)))meidian_min_max<dbl>64.9803151.0000079.00000但是,请注意,上面这个每个都以一行的形式返回。为了在不同的列中获得结果,我们必须定义一个返回如下数据帧的函数:median_min_max <- function(x){
qs <- quantile(x,c(0.5,0,1))
data.frame(median=qs[1],min=qs[2],max=qs[3])
}
heights %>%
filter(sex=='Female') %>%
summarise(median_min_max(height))medianminmax<dbl><dbl><dbl>64.9803151793.3.3 分组统计(group_by)数据探索中的一个常见操作是首先将数据分成若干组,然后计算每组的描述统计量。例如,我们可能想分别计算男性和女性身高的平均值和标准差。group_by函数可以帮我们做到这一点。这点和python中的groupby基本一致heights %>% group_by(sex) %>% head()sexheight<fct><dbl>Male75Male70Male68Male74Male61Female65上面数据看起来没什么区别,下面在计算统计量的时候可以发现我们可以得到分组后的统计量heights %>% group_by(sex) %>% summarise(avg = mean(height))sexavg<fct><dbl>Female64.93942Male69.31475从上图可以看出男性升高比女性升高平均高出4.33.4 pull函数上面定义的us_murder_rete对象只是一个数字。但我们将其存储在一个数据框中:class(us_murder_rate)'data.frame'对于大多数dplyr函数,返回的基本都是一个数据框。如果我们想将这个结果用于需要数值的函数,那么会产生一些问题。我们可以使用pull函数转换成数值型us_murder_rate %>% pull(rate)3.03455486317059此时返回的是一个数值型的值,等价于us_murder_rate$rate3.5 对数据框排序在检查数据集时,通按不同的列对表进行排序。我们可以使用order和sort函数,但是对于整张表的排序,dplyr的arrange函数有很大优势。例如,这里我们按人口规模对各州进行排序:# 默认是升序
murders %>% arrange(population) %>% head()stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>1WyomingWYWest56362650.88711312District of ColumbiaDCSouth6017239916.45275323VermontVTNortheast62574120.31962114North DakotaNDNorth Central67259140.59471515AlaskaAKWest710231192.67518606South DakotaSDNorth Central81418080.9825837下面对谋杀率进行降序排序murders %>% arrange(desc(rate)) %>% head()stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>1District of ColumbiaDCSouth6017239916.4527532LouisianaLASouth45333723517.7425813MissouriMONorth Central59889273215.3598924MarylandMDSouth57735522935.0748665South CarolinaSCSouth46253642074.4753236DelawareDESouth897934384.2319373.5.1 多重排序如果我们想要按多个变量来进行排序,例如先按区域排序,再按谋杀率排序murders %>% arrange(region,rate) %>% head()stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>1VermontVTNortheast62574120.31962112New HampshireNHNortheast131647050.37980363MaineMENortheast1328361110.82808814Rhode IslandRINortheast1052567161.52009335MassachusettsMANortheast65476291181.80217916New YorkNYNortheast193781025172.66795993.5.2 前n项在上面的代码中,我们使用了head()来避免页面被整个数据集填满。如果我们想看到更大的比例,我们可以使用top_n函数。此函数的第一个参数是数据框,第二个参数是要显示的行数,第三个参数是要过滤的变量。下面是如何查看前5行的示例:murders %>% top_n(5, rate)stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>District of ColumbiaDCSouth6017239916.452753LouisianaLASouth45333723517.742581MarylandMDSouth57735522935.074866MissouriMONorth Central59889273215.359892South CarolinaSCSouth46253642074.475323注意,我们并不是按rate进行排序的,只是用于筛选,默认是按最后一列进行筛选,如果要排序的话还是得使用arrange3.6 tibbles数据tibble数据可以看做成一个特殊的数据框,函数group_by和summarize总是返回这种类型的数据框。为了保持一致性,dplyr操纵函数(select、filter、mutate和arrange)保留了输入的类别:如果它们接收到常规数据框,则返回常规数据框,而如果它们接收到TIBLE,则返回TIBLE。但是tibble是tidyverse中的首选格式。事实上,可以把它们看作是数据框的现代版本。尽管如此,我们接下来将描述三个重要的区别。3.6.1 tibbles更好的展示数据tibbles的打印方法比数据帧的打印方法更具可读性。要看到这一点,比较输入murders的输出,我们通过as_tibble(murders)将其转换,可以得到tibble数据格式3.6.2 tible数据的子集还是tibble如果对数据框的列作为子集,则可能会返回非数据框的对象,例如向量或标量。例如:data(murders)class(murders[,4])'numeric'#返回的不是data.frame,但如果是tibble数据,返回的还是tibble
class(as_tibble(murders)[,4]).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'tbl_df''tbl''data.frame'这在tidyverse中很有用,因为很多函数需要数据框作为输入。对于tibbles,如果您希望访问定义列的向量,而不是返回数据框,则需要使用访问器$:class(as_tibble(murders)$population)'numeric'3.6.3 tibbles可以有更复杂的组成虽然数据帧列需要是数字、字符串或逻辑值的向量,但TIBLE可以有更复杂的对象,例如列表或函数。此外,我们还可以使用以下功能创建Tibble:tibble(id = c(1, 2, 3), func = c(mean, median, sd))idfunc<dbl><list>1function (x, ...) , UseMethod("mean")2function (x, na.rm = FALSE, ...) , UseMethod("median")3function (x, na.rm = FALSE) , sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), , na.rm = na.rm))3.7 点访问器管道操作符%>%的一个优点就是不需要产生中间对象,这是因为我们做的这些操作都是以数据框作为第一个参数,但如果我们要访问某一特定列的值,并且pull函数不好用,我们就可以使用点操作符例如我们想要知道地区在South的谋杀率的中位数,我们可以使用pull和点操作符来实现rates <- filter(murders, region == "South") %>%
mutate(rate = total / population * 10^5) %>%
.$rate
median(rates)3.398068830246043.8 purrr库在第二章中,我们学习了sapply函数,它允许我们对向量的每个元素应用相同的函数。我们构造了一个函数,并使用sapply计算几个n值的前n个整数之和,如下所示:compute_s_n <- function(n){
x <- 1:n
sum(x)
}
n <- 1:25
s_n <- sapply(n, compute_s_n)这种将相同的函数或过程应用于某一对象的中的每个元素,在数据分析中非常常见。purrr软件包包含与sapply类似的功能,但可以更好地与其他tidyverse功能交互。主要优点是我们可以更好地控制函数的输出类型。相比之下,sapply可以返回几种不同的对象类型;例如,我们可能希望从一行代码中得到一个数字结果,但sapply可能会在某些情况下将结果转换为字符。purrr函数永远不会这样做:它们将返回指定类型的对象,如果不满足会报错。 我们将学习的第一个purrr函数是map,它的工作原理与sapply非常相似。# 导入库
library(purrr)
s_n <- map(n, compute_s_n)
class(s_n)'list'如果我们想得到一个数值型向量,我们可以使用map_dbls_n <- map_dbl(n, compute_s_n)
class(s_n)'numeric'还有一个特别有用的purrr函数是map_df,它总是返回一个可编辑的数据框。 然而,被调用的函数需要返回一个带有名称的向量或列表。因此,以下代码将导致参数1必须有名称错误:compute_s_n <- function(n){
x <- 1:n
tibble(sum = sum(x))
}
s_n <- map_df(n, compute_s_n)3.9 tidyverse条件我们之前介绍了ifelse函数,下面我们介绍一些在dplyr的中的条件函数3.9.1 case_wen函数case_when函数对矢量化条件语句有用。它与ifelse类似,但可以输出任意数值,而不仅仅是TRUE或FALSE。下面是一个将数字分为负数、正数和0的示例:x <- c(-2,-1,0,1,2)
case_when(x<0 ~'Negtive',
x>0~'Positive',
TRUE ~ 'Zero').list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'Negtive''Negtive''Zero''Positive''Positive'此函数的一个常见用途是基于现有变量定义分类变量。例如,假设我们想比较四组州的谋杀率:新英格兰州、西海岸州、南部州和其他州。对于每个州,我们需要问它是否在新英格兰,如果不是,我们问它是否在西海岸,如果不是,我们问它是否在南部,如果不是,我们分配其他州。下面是我们在何时使用case_的方法:murders %>%
mutate(group = case_when(
abb %in% c("ME", "NH", "VT", "MA", "RI", "CT") ~ "New England",
abb %in% c("WA", "OR", "CA") ~ "West Coast",
region == "South" ~ "South",
TRUE ~ "Other")) %>%
group_by(group) %>%
summarize(rate = sum(total) / sum(population) * 10^5) grouprate<chr><dbl>New England1.723796Other2.708144South3.626558West Coast2.8990013.9.2 between函数数据分析中的一个常见操作是确定某个值是否在某个区间内。我们可以使用条件来检查这一点。例如,要检查向量x的元素是否在a和b之间我们可以使用:x >= a & x<=b然而,这可能会变得很麻烦,尤其是在tidyverse方法中。between函数执行相同的操作。between(x, 0, 2).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}FALSEFALSETRUETRUETRUE
秋叶无缘
【R语言数据科学】:(七)数据分布可视化
R语言数据科学本系列主要介绍R语言在数据分析领域的应用包括: R语言编程基础、R语言可视化、R语言进行数据操作、R语言建模、R语言机器学习算法实现、R语言统计理论方法实现。 本系列会完成下去,请大家多多关注点赞支持,一起学习~ 参考资料: Data Analysis and Prediction Algorithms with R数据分布可视化我们知道数值型变量一般用平均值和标准差描述,但是这样会导致一部分数据的信息缺失。我们在这一章介绍一下怎么绘制数据分布图7.1变量类型变量分为数值型和分类型两种。而分类型数据又可以分为有序和无序; 数值型数据可以分为连续和离散。例如学生的体重:连续数值型变量学生的年龄:离散型数值变量学生的性别:无序分类型变量学生的年级:有序分类型变量下面我们以heights数据集为例,进行相关数据分布的可视化实例# 导入相关库
library(tidyverse)
library(dslabs)
data(heights)7.2箱线图箱线图适合绘制分类型数据的分布情况murders %>% ggplot(aes(region)) + geom_bar()我们通常分布图以比例的形式来tab <- murders %>%
count(region) %>%
mutate(proportion = n/sum(n))
tabregionnproportion<fct><int><dbl>Northeast90.1764706South170.3333333North Central120.2352941West130.2549020tab %>% ggplot(aes(region, proportion)) + geom_bar(stat = 'identity')7.3直方图直方图往往描述数值型数据的分布,需要选择合适bins或者binwidth参数来使得我们的直方图更平滑heights %>%
filter(sex == "Female") %>%
ggplot(aes(height)) +
geom_histogram()`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.heights %>%
filter(sex == "Female") %>%
ggplot(aes(height)) +
geom_histogram(binwidth = 1)heights %>%
filter(sex == "Female") %>%
ggplot(aes(height)) +
geom_histogram(binwidth = 1, fill = "blue", col = "black") +
xlab("Female heights in inches") +
ggtitle("Histogram")7.4概率密度曲线使用geom_density来绘制概率密度曲线。概率密度曲线相当于是将直方图以平滑的形式绘制heights %>%
filter(sex == 'Female') %>%
ggplot(aes(height))+
geom_density(fill='blue',adjust=2)可以使用adjust参数来调整平滑度7.5箱线图上述图形都是绘制单个变量的分布情况,箱线图可以用于比较分类变量不同值的情况下,某一变量的分布情况, 需要指定x、y两个参数heights %>%
ggplot(aes(sex,height))+
geom_boxplot(aes(fill=sex))7.6QQ-plotqq-plot是绘制理论q分位点和样本q分位点的散点图。来判断数据是否服从标准正态分布 使用geom_qq可以绘制qqplot,我们需要使用sample参数指定我们要绘制变量的qq图heights %>% filter(sex=='Male')%>%
ggplot(aes(sample=height))+
geom_qq()默认情况下,我们是判断是否服从标准正态分布。可以使用dparams参数设置成其他正态分布。并且可以通过geom_abline 添加一条直线# 计算样本均值和标准差
params <- heights %>% filter(sex == 'Male')%>%
summarise(mean=mean(height),sd = sd(height))
heights %>% filter(sex == 'Male') %>%
ggplot(aes(sample=height))+
geom_qq(dparams = params)+
geom_abline()从qq图来看,数据近似服从正态分布,还有一种绘制qq图的方法是,我们先把数据标准化,再绘制标准qq图heights %>%
filter(sex=="Male") %>%
ggplot(aes(sample = scale(height))) +
geom_qq() +
geom_abline()可以看出结果和上面的一致7.7热力图ggplot2中绘制热力图有几种方法,geom_tile和geom_raster,热力图需要一个包含x和y坐标以及相关坐标值的数据框,# 定义数据
x <- expand.grid(x = 1:12, y = 1:10) %>%
mutate(z = 1:120) x %>% ggplot(aes(x, y, fill = z)) +
geom_raster()我们还可以使用scale_fill_gradientn来调整热力图色阶x %>% ggplot(aes(x, y, fill = z)) +
geom_raster() +
scale_fill_gradientn(colors = terrain.colors(10))
秋叶无缘
【R语言数据科学】:(五)data.table库(数据处理)
R语言数据科学参考资料: Data Analysis and Prediction Algorithms with R5.data.tabledata.table库是用于数据整理和分析的,在第三章中我们介绍了dplyr包来进行数据处理。本章介绍在data.table中如何实现相同的功能5.1 操作数据表data.table是一个单独的库。需要单独安装导入。本章介绍一些与第三章:R语言数据处理相关的方法: mutate,filter,select,group_by等首先我们使用setDT函数将数据框装换为一个data.table,否则 后面的操作可能会失效library(tidyverse)
library(data.table)
library(dslabs)murders <- copy(murders)
murders <- setDT(murders)5.1.1 selecting对数据进行选择指定列,在使用dplyr时,我们是这样写的select(murders, state, region) %>% head()stateregion<chr><fct>AlabamaSouthAlaskaWestArizonaWestArkansasSouthCaliforniaWestColoradoWest下面我们演示一下在data.table中是如何使用的murders[, c('state', 'region')] %>% head()stateregion<chr><fct>AlabamaSouthAlaskaWestArizonaWestArkansasSouthCaliforniaWestColoradoWest也可以直接使用.()来进行访问相应变量murders[,.(state, rate)] %>% head()staterate<chr><dbl>Alabama0.2824424Alaska0.2675186Arizona0.3629527Arkansas0.3189390California0.3374138Colorado0.12924535.1.2 添加一列或者改变一列我们在dplyr中使用mutate函数murders %>% mutate(murders, rate = total / population * 10^5) %>% head()stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>AlabamaALSouth47797361352.824424AlaskaAKWest710231192.675186ArizonaAZWest63920172323.629527ArkansasARSouth2915918933.189390CaliforniaCAWest3725395612573.374138ColoradoCOWest5029196651.292453在data.table中,我们使用:=来定义新的一列,这样能节约电脑内存murders[, rate := total/population * 10 ^5] %>% head()stateabbregionpopulationtotalrate<chr><chr><fct><dbl><dbl><dbl>AlabamaALSouth47797361352.824424AlaskaAKWest710231192.675186ArizonaAZWest63920172323.629527ArkansasARSouth2915918933.189390CaliforniaCAWest3725395612573.374138ColoradoCOWest5029196651.292453同样我们可以使用:=定义多个列murders[, ':='(rate=total / population * 10000, rank = rank(population))] %>% head()stateabbregionpopulationtotalraterank<chr><chr><fct><dbl><dbl><dbl><dbl>AlabamaALSouth47797361350.282442429AlaskaAKWest710231190.26751865ArizonaAZWest63920172320.362952736ArkansasARSouth2915918930.318939020CaliforniaCAWest3725395612570.337413851ColoradoCOWest5029196650.1292453305.1.3 引用和复制data.table包的设计是为了避免浪费内存。因此我们可以复制一个表x <- data.table(a=1)
y <- xy实际是x的引用,而不是一个新对象,相当于是x的另一个名字。只有当改变y的时候,才会生成一个新对象 然而在使用:=函数是,即便改变x也不会生成一个新的y对象,有时候我们不希望改变原来的对象,此时需要用copy()函数x [,a:=2]
ya<dbl>2z = copy(x)
x[,a:=3]
za<dbl>15.1.4索引在dplyr中,我们通过下述代码过滤filter(murders, rate <= 0.7) %>% head()stateabbregionpopulationtotalraterank<chr><chr><fct><dbl><dbl><dbl><dbl>AlabamaALSouth47797361350.282442429AlaskaAKWest710231190.26751865ArizonaAZWest63920172320.362952736ArkansasARSouth2915918930.318939020CaliforniaCAWest3725395612570.337413851ColoradoCOWest5029196650.129245330在data.table中,我们可以直接使用索引murders[rate<=0.7,.(state, rate)] %>% head()staterate<chr><dbl>Alabama0.2824424Alaska0.2675186Arizona0.3629527Arkansas0.3189390California0.3374138Colorado0.12924535.2 数据描述性统计和第三章一样,我们使用heights数据集为例data(heights)
# 将数据转换为data.table对象
heights <- setDT(heights)在data.table中,我们可以使用.()函数来直接访问相应的变量。因此我们可以在原来dplyr中简化代码如下s <- heights[, .(average = mean(height), standard_deviation = sd(height))]
saveragestandard_deviation<dbl><dbl>68.323014.078617下面假设我们要查询女性的平均身高和标准差s <- heights[sex == 'Female', .(avg = mean(height), standard_deviation = sd(height))]
savgstandard_deviation<dbl><dbl>64.939423.7606565.2.1 多个描述性统计指标还记得在第三章中,我们定义了如下函数median_min_max <- function(x){
qs <- quantile(x, c(0.5,0,1))
data.frame(median=qs[1], min = qs[2], max = qs[3])
}heights[,.(median_min_max(height))]medianminmax<dbl><dbl><dbl>68.55082.677175.2.2 分组统计在dplyr中我们使用group_by来进行分组,在data.table中,我们使用by进行分组heights[,.(avg = mean(height), standard_deviation=sd(height)), by = sex]sexavgstandard_deviation<fct><dbl><dbl>Male69.314753.611024Female64.939423.7606565.3 对数据排序我们可以使用与筛选相同的方法对行进行排序。以下是按谋杀率排序的州:murders[order(population)] %>% head()stateabbregionpopulationtotalraterank<chr><chr><fct><dbl><dbl><dbl><dbl>WyomingWYWest56362650.088711311District of ColumbiaDCSouth601723991.645275322VermontVTNortheast62574120.031962113North DakotaNDNorth Central67259140.059471514AlaskaAKWest710231190.267518605South DakotaSDNorth Central81418080.098258376
秋叶无缘
【R语言数据科学】:R语言基础
1. r语言基础1.1 数据类型R语言中有很多不同的类型。例如,我们需要区分数字与字符串,表格与简单的数字列表。functionclass可以帮助我们确定对象的类型:> a<-2
> class(2)
[1] "numeric"1.1.1 数据框(Data Frames)在R中存储数据集最常见的方式是在数据框中。我们可以将数据框视为一个表,其中的行表示样本观测值,列表上不同变量。我们可以将不同数据类型组合成一个数据框。 大部分数据分析都是从存储在数据框中的数据开始的。例如dslabs库中的murders数据集# 加载数据框
library(dslabs)
data(murders)
class(murders)'data.frame'1.1.2 检查数据对象使用str函数能返回更多关于数据对象的结构特征str(murders)'data.frame': 51 obs. of 5 variables:
$ state : chr "Alabama" "Alaska" "Arizona" "Arkansas" ...
$ abb : chr "AL" "AK" "AZ" "AR" ...
$ region : Factor w/ 4 levels "Northeast","South",..: 2 4 4 2 4 4 1 2 2 2 ...
$ population: num 4779736 710231 6392017 2915918 37253956 ...
$ total : num 135 19 232 93 1257 ...上述告诉了我们这个数据集有五个变量,51个观测数据包含表头。我们可以使用head()显示前六行:head(murders)stateabbregionpopulationtotal<chr><chr><fct><dbl><dbl>1AlabamaALSouth47797361352AlaskaAKWest710231193ArizonaAZWest63920172324ArkansasARSouth2915918935CaliforniaCAWest3725395612576ColoradoCOWest5029196651.1.3 访问器:$为了访问数据框的某一特定变量,我们可以使用$murders$population.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}47797367102316392017291591837253956502919635740978979346017231968765399200001360301156758212830632648380230463552853118433936745333721328361577355265476299883640530392529672975988927989415182634127005511316470879189420591791937810295354836725911153650437513513831074127023791052567462536481418063461052514556127638856257418001024672454018529945686986563626得到某一特定变量名字,使用names()names(murders).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'state''abb''region''population''total'1.1.4 向量:数字、字符串和logicalmurders$population返回一些列值就是一个向量,使用length可以得到向量长度pop <- murders$population
length(pop)
class(pop)51'numeric'上面是一个数值向量,下面我们分别看字符串和logical向量class(murders$state)'character'class(pop>50)'logical'1.1.5 因子在R语言中,使用factor存储分类型变量class(murders$region)'factor'可以看出region是factor变量,我们可以使用levels看它有哪几类levels(murders$region).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'Northeast''South''North Central''West'1.1.6 列表数据框可以看作是一个特殊的列表,使用list创建列表注意和python的区别# 方法一
record <- list(name = "John Doe",
student_id = 1234,
grades = c(95, 82, 91, 97, 93),
final_grade = "A")# 方法2
record2 <- list("John Doe", 1234)
record2'John Doe'12341.1.7 矩阵矩阵类似数据框,有行有列,但是里面必须是同样的类型,因此数据框更常用mat <- matrix(1:12, 4,3)mat# 使用[]索引具体的值
mat[1,2]51.2 向量在R中,可用于存储数据的最基本对象是向量。正如我们所看到的,复杂的数据集通常可以分解为向量。 例如,在数据框中,每列都是一个向量。1.2.1 创建向量我们使用c()来创建一个向量x <- c(1,2,3)
x.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}1231.2.2 命名有时命名向量的每一个值很有用。例如在定义国家编码时codes <- c(italy = 380, canada = 124, egypt = 818)
codes.dl-inline {width: auto; margin:0; padding: 0} .dl-inline>dt, .dl-inline>dd {float: none; width: auto; display: inline-block} .dl-inline>dt::after {content: ":\0020"; padding-right: .5ex} .dl-inline>dt:not(:first-of-type) {padding-left: .5ex}italy380canada124egypt818class(codes)'numeric'names(codes).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'italy''canada''egypt'1.2.3 序列还可以使用seq()序列函数创建向量x <- seq(1,10)
x.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}12345678910seq()函数第一个参数表示开始值,第二个参数表示结束值,最后一个参数表示步长,默认为1。同时包含开始和结束,这一点和python的range函数有一点区别,python不包含结束值。1.2.4 索引我们可以通过索引访问向量中某一特定元素x <- codes[1]
xitaly: 380codes[1:2].dl-inline {width: auto; margin:0; padding: 0} .dl-inline>dt, .dl-inline>dd {float: none; width: auto; display: inline-block} .dl-inline>dt::after {content: ":\0020"; padding-right: .5ex} .dl-inline>dt:not(:first-of-type) {padding-left: .5ex}italy 380canada 1241.3 排序例如,以murders数据集为例,我们想要以枪杀案升序排序sort(murders$total).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}2455781112121619212227323638536365678493939797991111161181201351422072192322462502862933103213513643764134575176698051257但是这样我们并不能得到是哪些州枪击案最多1.3.1 order使用order函数可以得到变量排序的索引,例如x <- c(31,4,15,92,65)
sort(x).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}415316592# 使用order
idx <- order(x)
idx.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}23154x[idx].list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}415316592可以看到结果和sort函数一样,下面我们对murders数据集使用orderind <- order(murders$total)
murders$abb[ind].list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'VT''ND''NH''WY''HI''SD''ME''ID''MT''RI''AK''IA''UT''WV''NE''OR''DE''MN''KS''CO''NM''NV''AR''WA''CT''WI''DC''OK''KY''MA''MS''AL''IN''SC''TN''AZ''NJ''VA''NC''MD''OH''MO''LA''IL''GA''MI''PA''NY''FL''TX''CA'这样可以得到哪些州的枪杀较多1.3.2 max和which.max如果我们只对最大值感兴趣,那么可以使用maxmax(murders$total)1257和order类似,which.max()返回最大值的索引i_max <- which.max(murders$total)
murders[i_max, ]min和which.min是同样的结果1.3.3 rank和order、sort,rank不是针对数据排序的,而是返回每个值的秩x <- c(31,4,15,92,64)
rank(x).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}312541.3.4 小心recycling有点时候数据长度不匹配,可能导致在向量计算的时候自动循环使用而出现问题x <- c(1,2,3)
y <- c(10,20,30,40,50,60)
x+y.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}112233415263上述两个向量长度不相等,但是发现x循环使用了两次,这有点类似python中的广播机制
秋叶无缘
【R语言数据科学】:(四)文件读取、导入和复制
R语言数据科学参考资料: Data Analysis and Prediction Algorithms with R数据导入我们之前使用的数据都是R语言内存储好的数据集,然而在实际应用时,我们需要从外部数据库、相关数据文件导入。目前,最常见的存储数据方式之一是通过电子表格。电子表格按行和列存储数据。它是数据框的文件版本。将这样的表保存到计算机文件时,需要定义列和值。 当使用文本文件创建电子表格时,就像使用简单的文本编辑器创建的电子表格一样,换行符表示新的一行,列用一些特殊的分隔符分开,例如,;空格等在定义数据表时,要注意第一行包含列名,而不是数据。我们称之为标题,当我们从电子表格中读取数据时,重要的是要知道文件是否有标题。大多数读取函数都假设数据集有一个标题。要知道文件是否有表头,在读取之前先查看文件。但是不是所有的电子表格文件都是文本形式的,例如excel,csv等。本章将如何介绍这些数据的导入# 导入相关库
library(tidyverse)4.1 路径和工作目录导入数据的第一步就是要知道数据存储的路径,也就是在哪个文件夹,这里我们看一下如何分析R内部数据的路径和工作目录包含美国谋杀案数据的电子表格是dslabs软件包的一部分。下面几行代码将该文件复制到默认情况下R查找的文件夹中。filename <- "murders.csv"
dir <- system.file("extdata", package = "dslabs")
fullpath <- file.path(dir, filename)
file.copy(fullpath, "murders.csv")FALSE上述代码复制了一个文件。'murders.csv',我们可以使用read_csv读取他dat<-read_csv(filename)Rows: 51 Columns: 5
-- Column specification ------------------------------------------------------------------------------------------------
Delimiter: ","
chr (3): state, abb, region
dbl (2): population, total
i Use `spec()` to retrieve the full column specification for this data.
i Specify the column types or set `show_col_types = FALSE` to quiet this message.4.1.1 文件系统您可以将计算机的文件系统视为一系列嵌套的文件夹,每个文件夹包含其他文件夹和文件。我们将包含所有其他文件夹的文件夹称为根目录。我们将当前所在的目录称为工作目录。因此,在文件夹中移动时,工作目录会发生变化:将其视为当前位置。4.1.2 相对路径和绝对路径文件路径是一个目录列表名,可以将其视为关于单击哪些文件夹以及按什么顺序查找文件的说明。如果这些说明是为了从根目录中查找文件,我们将其称为绝对路径。如果说明是从工作目录开始查找文件,我们将其称为相对路径。下面来看一下绝对路径system.file(package = 'dslabs')'C:/Users/DELL/Documents/R/win-library/4.0/dslabs'用斜线分隔的字符串是目录名。第一个斜杠代表根目录,我们知道这是一个绝对路径,因为它以斜杠开头。如果第一个目录名前面没有斜杠,则路径是相对的。我们可以使用函数list.files以查看相对路径的示例。dir <- system.file(package = 'dslabs')
list.files(path = dir).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'data''DESCRIPTION''extdata''help''html''INDEX''MD5''Meta''NAMESPACE''R''script'如果我们使用绝对路径,这些相对路径给出了最后的位置。例如'data'的绝对路径就是: 'C:/Users/DELL/Documents/R/win-library/4.0/dslabs/data'4.1.3 工作路径我建议大家在代码中最好使用相对路径。因为绝对路径是唯一的,并且我们希望代码是可移植的。通过使用getwd函数,可以获得工作目录的完整路径。wd <- getwd()
wd'C:/Users/DELL/r语言学习/数据科学导论:基于R'4.1.4 生成路径名另一个获得绝对路径但是不需要写出全部路径的方法是使用file.pathfilename <- 'murders.csv'
dir <- system.file('extdata',package='dslabs')
fullpath <- file.path(dir,filename)#file.path的左右是把两个文件连接在一起得到组合的路径
fullpath'C:/Users/DELL/Documents/R/win-library/4.0/dslabs/extdata/murders.csv'system.file给出了文件夹的绝对路径,其中包含了package参数。通过浏览dir中的列表文件,我们可以发现extdata中有我们想要的murders文件filename %in% list.files(file.path(dir)) # 查看filename是否在extdata中
TRUE4.1.5 复制文件file.copy()此函数接受两个参数:要复制的文件和在新目录中为其指定的名称。file.copy(fullpath, "murders.csv")FALSE4.2 reder和readxl库在本节中,我们主要讨论tidyverse数据导入功能。利用murders数据集。以dslabs包提供的csv文件为例。filename <- "murders.csv"
dir <- system.file("extdata", package = "dslabs")
fullpath <- file.path(dir, filename)
file.copy(fullpath, "murders.csv")FALSE4.2.1 readrreadr库包含将文本文件电子表格中存储的数据读取到R中的功能。readr是tidyverse软件包的一部分,也可以直接加载:library(readr)Function Format 后缀
read_table 空格分隔 txt
read_csv 逗号分隔 csv
read_csv2 分号分隔 csv
read_tsv 制表符分隔 tsv
read_delim 通用文本格式,要指定分隔符 txt虽然后缀通常告诉我们它是什么类型的文件,但不能保证它们总是匹配的。我们可以打开文件查看,也可以使用函数read_lines查看几行read_lines('murders.csv',n_max=3).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'state,abb,region,population,total''Alabama,AL,South,4779736,135''Alaska,AK,West,710231,19'同时可以看出数据有一个标题。现在我们使用read_csv读取:dat <- read_csv(filename)
Rows: 51 Columns: 5
-- Column specification ------------------------------------------------------------------------------------------------
Delimiter: ","
chr (3): state, abb, region
dbl (2): population, total
i Use `spec()` to retrieve the full column specification for this data.
i Specify the column types or set `show_col_types = FALSE` to quiet this message.head(dat)stateabbregionpopulationtotal<chr><chr><chr><dbl><dbl>AlabamaALSouth4779736135AlaskaAKWest71023119ArizonaAZWest6392017232ArkansasARSouth291591893CaliforniaCAWest372539561257ColoradoCOWest5029196654.2.2 readxllibrary(readxl)主要有以下几个函数:Function Format 后缀
read_excel auto detect the format xls, xlsx
read_xls original format xls
read_xlsx new format xlsxMicrosoft Excel格式允许在一个文件中包含多个电子表格。这些被称为sheet。上面列出的函数默认读取第一页,但我们也可以读取其他的。excel_sheets函数为我们提供excel文件中所有工作表的名称。然后,可以将这些名称传递给上述三个函数中的sheet参数,以读取除第一个以外的工作表。4.3 读取网络文件另一个常见的数据互联网数据。当这些数据存在文件中时,我们可以下载它们,然后导入它们,甚至可以直接从网络上读取它们。例如,dslab包位于GitHub上,所以我们随包下载的文件有一个url:url <- "https://raw.githubusercontent.com/rafalab/dslabs/master/inst/
extdata/murders.csv"dat <- read_csv(url)Rows: 1 Columns: 1
-- Column specification ------------------------------------------------------------------------------------------------
Delimiter: ","
chr (1): https://raw.githubusercontent.com/rafalab/dslabs/master/inst/
i Use `spec()` to retrieve the full column specification for this data.
i Specify the column types or set `show_col_types = FALSE` to quiet this message.
秋叶无缘
【R语言数据科学】:(十)数据清洗之日期型数据处理
🍀10日期型数据格式转换我们描述了三种主要类型的向量:数字、字符和逻辑。在数据科学项目中,我们经常会遇到日期变量。虽然我们可以用字符串表示日期,例如2017年11月2日,但一旦我们选择了一个被称为“历元”的参考日,就可以通过计算自历元起的天数将其转换为数字。计算机语言通常以1970年1月1日为纪元。例如,2017年1月2日是1,1969年12月31日是-1,2017年11月2日是第17204天。 在R中分析数据时,我们应该如何表示日期和时间?我们接下来看一下具体案例。💮10.1 日期数据类型library(tidyverse)
library(dslabs)polls_us_election_2016 %>% head()statestartdateenddatepollstergradesamplesizepopulationrawpoll_clintonrawpoll_trumprawpoll_johnsonrawpoll_mcmullinadjpoll_clintonadjpoll_trumpadjpoll_johnsonadjpoll_mcmullin<fct><date><date><fct><fct><int><chr><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>1U.S.2016-11-032016-11-06ABC News/Washington PostA+2220lv47.0043.004.00NA45.2016341.724304.626221NA2U.S.2016-11-012016-11-07Google Consumer SurveysB26574lv38.0335.695.46NA43.3455741.214395.175792NA3U.S.2016-11-022016-11-06IpsosA-2195lv42.0039.006.00NA42.0263838.816206.844734NA4U.S.2016-11-042016-11-07YouGovB3677lv45.0041.005.00NA45.6567640.920046.069454NA5U.S.2016-11-032016-11-06Gravis MarketingB-16639rv47.0043.003.00NA46.8408942.331843.726098NA6U.S.2016-11-032016-11-06Fox News/Anderson Robbins Research/Shaw & Company ResearchA1295lv48.0044.003.00NA49.0220843.956313.057876NAclass(polls_us_election_2016$startdate)'Date'看看当我们把它们转换成数字时会发生什么:as.numeric(polls_us_election_2016$startdate) %>% head.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}171081710617107171091710817108因为自动的将上述日期转换成纪元方式定义as.Date('1970-01-01') %>% as.numeric0绘图功能,例如ggplot中的功能,可以知道日期格式。这意味着,例如,散点图可以使用数字表示来确定点的位置,但在标签中包括字符串:polls_us_election_2016 %>% filter(pollster == "Ipsos" & state =="U.S.") %>%
ggplot(aes(startdate, rawpoll_trump)) +
geom_line()🏵️10.2 lubridate包tidyverse包括通过lubridate包处理日期的功能。library(lubridate)我们将随机抽取一个日期数据样本,演示一下如何使用lubridate包set.seed(1)
dates <- sample(polls_us_election_2016$startdate, 10) %>% sort
dates.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}2016-05-192016-08-122016-08-172016-08-292016-09-282016-10-172016-10-232016-10-252016-10-252016-10-28可以使用year,month,day分别从日期中截取年、月、日tibble(date = dates,
month = month(dates),
day = day(dates),
year = year(dates))datemonthdayyear<date><dbl><int><dbl>2016-05-1951920162016-08-1281220162016-08-1781720162016-08-2982920162016-09-2892820162016-10-17101720162016-10-23102320162016-10-25102520162016-10-25102520162016-10-2810282016month(dates, label = TRUE).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}5月8月8月8月9月10月10月10月10月10月Levels:.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'1月''2月''3月''4月''5月''6月''7月''8月''9月''10月''11月''12月'我们发现上述日期返回的都是中文的,这是因为我们安装的是中文版的R语言,我们可以使用下列代码将时间转换为英文的Sys.setlocale("LC_TIME", "English")'English_United States.1252'month(dates, label = TRUE).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}MayAugAugAugSepOctOctOctOctOctLevels:.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'Jan''Feb''Mar''Apr''May''Jun''Jul''Aug''Sep''Oct''Nov''Dec'另外一个函数是将字符串转换为日期型,使用ymd()函数,假定日期的格式是YYYY-MM-DD的格式x <- c(20090901,'2009-01-02','2009 01 03','2009-1-4','2009-1,5','created on 2009 1 6','2009 01 $$$ 07')可以看出上面我定义的x字符串的内容并不是标准的日期,但是ymd函数可以自动尽量将上述转换成下面的格式,具体如下ymd(x).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}2009-09-012009-01-022009-01-032009-01-042009-01-052009-01-062009-01-07更复杂的是,日期通常以不同的格式出现。其中年、月和日的顺序不同。首选的格式是显示年份(全部四位数字)、月份(两位数字),然后是日期,这种格式称为ISO 8601。具体来说,我们使用YYYY-MM-DD,因此如果我们转换字符串,它将按上述方法排序返回。可以看到函数ymd以这种格式返回它们。但是,如果你遇到“09/01/02”这样的日期怎么办这可能是2002年9月1日、2009年1月2日或2002年1月9日。在这些情况下,检查整个日期向量可以得到具体的日期结果。一旦我们知道了,我们可以使用不同的lubridate包的函数。例如我们可以使用mdy函数,表示第一个是月份,第二个是日期,第三个是年份x <- '09/01/02'
mdy(x)2002-09-01dmy(x)2002-01-09myd(x)2001-09-02dym(x)2001-02-09同样lubridate包还可以返回当前日期,并且设定相应的时区now()
now("GMT")# 表示零时区[1] "2022-04-23 22:14:18 CST"
[1] "2022-04-23 14:14:18 GMT"使用OlsonNames()函数查找可以选择的时区同样地,我们可以使用hour,minute,sencond分别返回小时、分钟、秒now() %>% hour()
now() %>% minute()
now() %>% second()222731.5012230873108和ymd()函数类似,在这里我们也有hms()函数,返回小时,分钟,秒x <- c("12:34:56")
hms(x)12H 34M 56S注意,这里hms的顺序不能交换,我们还可以使用mdy和hms组合得到一个具体的日期型数据x <- '12 2 2022 22:33:55'
mdy_hms(x)[1] "2022-12-02 22:33:55 UTC"最后再介绍两个十分有用的函数,make_date和round_datemake_date,可以返回一个日期型数据,例如我们想要生成2022年4月23日的日期数据make_date(2022,4,23)2022-04-23我们还可以生成21世纪20年代的时间序列make_date(2020:2029).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}2020-01-012021-01-012022-01-012023-01-012024-01-012025-01-012026-01-012027-01-012028-01-012029-01-01round_date 函数从字面意义来看,round意味四舍五入,那么在日期型数据中,它会返回某一日期最近的年份,季度,月份,周等polls_us_election_2016 %>%
mutate(week = round_date(startdate, "week")) %>%
group_by(week) %>%
summarize(margin = mean(rawpoll_clinton - rawpoll_trump)) %>%
qplot(week, margin, data = .)
秋叶无缘
【R语言数据科学】:(八)数据清洗技巧之数据格式转换(包含宽数据与长数据之间的转换)
【R语言数据科学】:(八)数据清洗技巧之数据格式转换(包含宽数据与长数据之间的转换)🌺R语言数据分析从入门到高级:(八)数据清洗技巧之数据格式转换正如我们之前介绍的,我们希望在R语言中希望数据是tidy形式的(具体大家可以看该专栏的第三篇文章)。在数据分析流程中,第一步是将数据导入,第二步往往就是对数据格式进行转换成tidy数据以便于后续分析。R语言tidyverse可以帮助我们实现这个目的。首先导入我们相关库和数据,运用了我们在该专栏第四章介绍的数据导入、复制等技巧,不理解的小伙伴可以先看之前的文章,如果报错显示没有相关库,先使用 install.packages('tidyverse') install.packages('dslabs')library(tidyverse)
library(dslabs)
path <- system.file('extdata', package='dslabs')
filename <- file.path(path, 'fertility-two-countries-example.csv')
wide_data <- read_csv(filename)head(wide_data)country196019611962196319641965196619671968...2006200720082009201020112012201320142015<chr><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>...<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>Germany2.412.442.472.492.492.482.442.372.28...1.361.361.371.381.391.401.411.421.431.44South Korea6.165.995.795.575.365.164.994.854.73...1.201.211.231.251.271.291.301.321.341.36该数据包含德国和韩国不同年份的生育率,可以看出这个数据不服从tidy数据格式,我们希望将这些不同年份作为一个变量来表示,下面我们来看看具体是怎么操作的💮8.1 pivot_longer函数tidyverse中用的最多的函数之一就是pivot_longer,可以将宽数据转换为tidy格式的数据与tidyverse其他大多数函数一样,pivot_longer第一个参数也是要转换的数据框,这里我们将 wide_data数据框输入,我们希望将其转换为一行代表一个国家某一年份的一个数据。 而在当前形式中,不同年份的数据位于不同的列中,列名称中有年份。 通过names_to和values_to参数,我们将分别指定给包含当前列名和观察值的列的列名。 如果不指定参数,默认的名字是name和value,在这里我们使用year和fertility。 通过第二个参数col指定我们想要转换的列.new_tidy_data <- wide_data %>%
pivot_longer(cols = `1960`:`2015`,names_to = "year", values_to = "fertility")head(new_tidy_data)countryyearfertility<chr><chr><dbl>Germany19602.41Germany19612.44Germany19622.47Germany19632.49Germany19642.49Germany19652.48我们可以看到,数据已转换为整齐的tidy格式,包含年份和生育率,并且每一年都有两行数据,因为我们有两个国家,因此在这里我们没有将country列进行转换,所有我们也可以使用简化使用下面的代码得到同样的结果new_tidy_data <- wide_data %>%
pivot_longer(-country, names_to = "year", values_to = "fertility")head(new_tidy_data)countryyearfertility<chr><chr><dbl>Germany19602.41Germany19612.44Germany19622.47Germany19632.49Germany19642.49Germany19652.48指的注意的一点是,pivot_longer函数假定列名是字符串的形式,我们来检查一下class(new_tidy_data$year)'character'因此,如果我们想要进行绘图,首先需要将年份这个变量值转换为数值型变量,可以使用as.intergernew_tidy_data <- wide_data %>%
pivot_longer(-country, names_to = "year", values_to = "fertility") %>%
mutate(year = as.integer(year))注意我们使用了mutate()函数,这也是我们在第三章所介绍的。现在我们可以使用该数据进行简单的绘图,例如我们要绘制年份和生育率的散点图,并以颜色进行分组new_tidy_data %>% ggplot(aes(year, fertility, color = country)) +
geom_line()🏵️8.2 poivot_wider有时将tidy数据转换为宽数据对于数据清洗非常有用。我们通常将此作为整理数据的中间步骤。pivot_Wider函数基本上与pivot_longer函数相反。第一个参数是关于数据框,参数names_from告诉哪个变量将用作列名。values_from指定使用哪些变量值填充单元格的值。默认参数值为name和value,如下图所示,我们想要展示1960-1967年两个国家的生育率数据new_wide_data <- new_tidy_data %>%
pivot_wider(names_from = year, values_from = fertility)
select(new_wide_data, country, `1960`:`1967`)country19601961196219631964196519661967<chr><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>Germany2.412.442.472.492.492.482.442.37South Korea6.165.995.795.575.365.164.994.85用法基本与poivot_longer相似,大家可以进行相互转换练习。🌹 8.3 seperate函数上面的数据清洗和实际中相比可能相对简单。接下来我们将介绍一些更复杂的情况。该数据集包含两个变量:预期寿命和生育率。如下所示,该数据不是tidy格式path <- system.file("extdata", package = "dslabs")
filename <- "life-expectancy-and-fertility-two-countries-example.csv"
filename <- file.path(path, filename)
raw_dat <- read_csv(filename)
select(raw_dat, 1:5)country1960_fertility1960_life_expectancy1961_fertility1961_life_expectancy<chr><dbl><dbl><dbl><dbl>Germany2.4169.262.4469.85South Korea6.1653.025.9953.75首先,注意这个数据也是宽数据格式,但是这个数据框包含两个变量的值,生育率和预期寿命。使用列名来表示哪个列代表哪个变量。即便我们强烈不建议在列名中对信息进行编码,但实际上这很常见。 接下来我们运用数据清洗技术来提取这些信息,并以tidy的形式保存数据。第一步,我们照样可以先试用pivot_longer函数,将数据转换为长格式。但是这里我们的名字不能命名为year,这里我们使用默认的参数dat <- raw_dat %>% pivot_longer(-country)
head(dat)countrynamevalue<chr><chr><dbl>Germany1960_fertility2.41Germany1960_life_expectancy69.26Germany1961_fertility2.44Germany1961_life_expectancy69.85Germany1962_fertility2.47Germany1962_life_expectancy70.01结果并不完全是我们所说的tidy格式,因为每个观测数据都与两行相关,而不是一行。我们希望将生育率和预期寿命这两个变量的值分为两列。实现这一点的第一步是将名称列分为年份和变量类型。请注意,在本例中是使用下划线将年份与变量名隔开。 在一个列名中编码多个变量是一个常见的问题,readr包包含一个将这些列分隔为两个或更多列的函数。除了数据之外,separate函数还有三个参数:要分隔的列的名称、用于新列的名称以及分隔变量的字符。因此,第一步代码如下:dat %>% separate(name, c("year", "name"), "_") %>% head()countryyearnamevalue<chr><chr><chr><dbl>Germany1960fertility2.41Germany1960life69.26Germany1961fertility2.44Germany1961life69.85Germany1962fertility2.47Germany1962life70.01由于因为_是separate假定的默认分隔符,所以我们不必在代码中包含它dat %>% separate(name, c('year','name')) %>% head()countryyearnamevalue<chr><chr><chr><dbl>Germany1960fertility2.41Germany1960life69.26Germany1961fertility2.44Germany1961life69.85Germany1962fertility2.47Germany1962life70.01该函数确实会分离这些值,但我们遇到了一个新问题。预期寿命变量被截断为寿命。这是因为_用于区分寿命和预期寿命,而不仅仅是年份和变量名称。我们可以添加第三列来解决这一点,并让单独的函数知道在没有第三个值的情况下,用NA填充那一列。var_names <- c("year", "first_variable", "second_variable")
dat %>% separate(name, var_names, fill = "right")%>%head()countryyearfirst_variablesecond_variablevalue<chr><chr><chr><chr><dbl>Germany1960fertilityNA2.41Germany1960lifeexpectancy69.26Germany1961fertilityNA2.44Germany1961lifeexpectancy69.85Germany1962fertilityNA2.47Germany1962lifeexpectancy70.01我们还有一种更好的方式是使用extra参数,在有额外间隔时合并最后两个变量,如下所示dat %>% separate(name, c("year", "name"), extra = "merge")%>%head()countryyearnamevalue<chr><chr><chr><dbl>Germany1960fertility2.41Germany1960life_expectancy69.26Germany1961fertility2.44Germany1961life_expectancy69.85Germany1962fertility2.47Germany1962life_expectancy70.01接下来我们可以再使用之前介绍的pivot_wider()函数,为每个变量创建一列dat %>%
separate(name, c("year", "name"), extra = "merge")%>%
pivot_wider(names_from = name, values_from = value)%>%head()countryyearfertilitylife_expectancy<chr><chr><dbl><dbl>Germany19602.4169.26Germany19612.4469.85Germany19622.4770.01Germany19632.4970.10Germany19642.4970.66Germany19652.4870.65现在数据是tidy格式,每个观测值有一行,包含四个列变量:国家、年份、生育率和预期寿命。🥀8.4 unite函数有时,将两列分开并合并为一列是很有用的。为了演示如何使用unite,假设我们不适用extra参数,我们可以使用以下代码进行合并,这些代码可能不是最佳方法,但可以作为一个示例。依然用刚刚的dat数据var_names <- c("year", "first_variable_name", "second_variable_name")
dat %>%
separate(name, var_names, fill = "right")%>%head()countryyearfirst_variable_namesecond_variable_namevalue<chr><chr><chr><chr><dbl>Germany1960fertilityNA2.41Germany1960lifeexpectancy69.26Germany1961fertilityNA2.44Germany1961lifeexpectancy69.85Germany1962fertilityNA2.47Germany1962lifeexpectancy70.01我们可以通过合并第二列和第三列,然后旋转这些列并将fertility_NA重命名为fertility,来获得相同的最终结果:dat %>%
separate(name, var_names, fill = "right") %>%
unite(name, first_variable_name, second_variable_name) %>%
pivot_wider() %>%head()
rename(fertility = fertility_NA)%>%head()countryyearfertility_NAlife_expectancy<chr><chr><dbl><dbl>Germany19602.4169.26Germany19612.4469.85Germany19622.4770.01Germany19632.4970.10Germany19642.4970.66Germany19652.4870.65
秋叶无缘
【R语言数据科学】: R语言编程基础
2.R语言编程基础R语言作为统计学界的计算机语言,在数据分析方面有着其独特的优势,我们可以通过R进行探索性数据分析,构建数据分析管道,以及数据可视化等。本章主要介绍一下R语言的一些编程基础2.1 条件表达式条件表达式属于流程控制的一部分,是编程最基础的一部分内容。最常见的是if-else语句,具体语法如下a <- 5
if (a!=0){
print(1/a)
} else{
print('No reciprocal for 0')
}[1] 0.2我们使用美国谋杀的数据,求出谋杀率murder_ratelibrary(dslabs)
data(murders)
murder_rate <- murders$total / murders$population*100000ind <- which.min(murder_rate)
ind46如果谋杀率小于0.5,则我们输出对应的州if(murder_rate[ind]<0.5){
print(murders$state[ind])
} else{
print("No state has murder rate that low")
}[1] "Vermont"现在我们改成0.25if(murder_rate[ind]<0.25){
print(murders$state[ind])
} else{
print("No state has murder rate that low")
}[1] "No state has murder rate that low"和if-else语句效果类似的就是ifelse()函数,有三个参数,第一个是判断语句,如果成立返回第二个参数,如果不成立,返回第三个值# if else
a <- 0
ifelse(a>0,1/a,NA)<NA>ifelse还适用于向量的判断。它检查逻辑向量的每个元素,如果为真,则返回第二个参数;如果条目为假,则返回第三个参数。a <- c(0,1,2,-4,5)
result <- ifelse(a>0, 1/a, NA)
result.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}<NA>10.5<NA>0.2还有一个应用场景,当数据存在缺失值时,直接运算会报错,此时我们一般选择吧空值删除或替换,下面给出了一种替换空值的方法na_example <- c(NA,1,5,NA)
no_nas <- ifelse(is.na(na_example), 0, na_example)
sum((no_nas))6针对布尔值运算,any()和all()函数,any()函数,如果有一个为真,则为真all()函数,如果有一个为假,则为假z <- c(TRUE, TRUE, FALSE)
any(z)
all(z)TRUEFALSE2.2 函数定义avg <- function(x){
s <- sum(x)
n <- length(x)
s/n
}x <- 1:100
avg(x)50.53.4 for循环假设我们要计算序列1+2+...+n的值Sn时,我们可以创建一个函数compute_s_n <- function(n){
x <- 1:n
sum(x)
}compute_s_n(5)15假设我们要得到前1,...,20个Sn,此时我们重复调用20次函数显然不合适,因此要使用循环来使我们的代码更简洁首先我们来看一下for循环的基本语法# 打印1到5
for (i in 1:5) {
print(i)
}[1] 1
[1] 2
[1] 3
[1] 4
[1] 5m <- 25
s_n <- vector(length = m) # 先生成一个空列表
for (n in 1:m){
s_n[n] <- compute_s_n(n) #接受函数值
}n <- 1:m
plot(n, s_n)可以看出图形呈现二次曲线,因为求和公式:Sn=n(n+1)/23.5 向量化和函数型编程在数据分析时,向量化有时能给我们带来许多便利x <- 1:10
sqrt(x).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}11.41421356237311.7320508075688822.236067977499792.449489742783182.645751311064592.8284271247461933.16227766016838R中一般不使用for循环,因为使用向量化操作会更简单,但是不是所有的函数都能使用向量化,这是我们需要进行处理# 例如求和
n <- 1:25
compute_s_n(n)Warning message in 1:n:
"numerical expression has 25 elements: only the first used"我们使用sapply函数,就可以对每个元素进行处理,类似python中的applyn <- 1:25
s_n <- sapply(n, compute_s_n)s_n.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}13610152128364555667891105120136153171190210231253276300325
秋叶无缘
【R语言数据科学】:(六)数据可视化之ggplot2详解
R语言数据科学本系列主要介绍R语言在数据分析领域的应用包括: R语言编程基础、R语言可视化、R语言进行数据操作、R语言建模、R语言机器学习算法实现、R语言统计理论方法实现。 本系列会完成下去,请大家多多关注点赞支持,一起学习~ 参考资料: Data Analysis and Prediction Algorithms with R💘6.ggplot2介绍可视化是R语言数据分析中的强项。R语言中的ggplot2大大的增加了数据可视化的灵活性。本章主要介绍ggplot2的基本绘图,以后打算撰写一个关于ggplot2的专栏## 导入相关库算撰写一个关于ggplot2的专栏
## 导入相关库
library(dplyr)
library(ggplot2)使用ggplot2可能需要记住一些函数和参数,说实话,很难将它们记住。因此我建议大家可以保存ggplot2-cheatsheet,在画图的时候可以参考💮6.1 图形的组成部分学习ggplot2首先知道怎么将图形分解成若干个组成部分data:通过某一数据创建ggplot2对象geometry:图形类型aesthetic mapping:增加一些图形信息🏵️6.2 ggplot对象第一步定义一个ggplot对象library(dslabs)data(murders)ggplot(data = murders)由于我们没有进行任何图形设置,返回了一个空的画板🌹6.3 图形类型在ggplot2中,我们通过增加图层来进行图形的可视化。使用+来增加不同的图层。 一般我们先增图形类型。geom_X,其中X代表某一特定的图形。例如geom_point,geom_bar,geom_histogram。常见的语法如下所示data %>% ggplot() + layer1 + layer2🥀6.4 美学映射美学映射描述了数据的属性如何与图形的特征连接。例如坐标轴、颜色、大小。使用aes()函数来定义这些属性,作为geom_X的一个参数,例如我们要画散点图murders %>% ggplot() + geom_point(aes(x=population/10^6, y=total))🌺 6.5 层级我们可以通过增加层级来给图形增加标签。使用geom_text 和 geom_label可以在图形中增加文本。 下面我们希望对每个点都希望增加标签,因此需要使用aes定义labmurders %>% ggplot() + geom_point(aes(population/10^6, total)) +
geom_text(aes(population/10^6, total, label = abb))🌻6.6 调整参数每个几何图形函数都有很多参数,例如我们希望改变图形点的大小和颜色,使用size和colmurders %>% ggplot() + geom_point(aes(population/10^6, total),size=3,col='red') +
geom_text(aes(population/10^6, total, label = abb))注意,我们发现size和col参数并不在aes中。这里给大家一个小tips:aes是使用来着样本某一特定观测数据来作为参数,例如我们的population和total,其余的不需要放在aes()中上述图形的点太大了,导致很难看清楚具体分布情况,我们可以使用geom_text中的nudge_x参数,调整文本位置murders %>% ggplot() + geom_point(aes(population/10^6, total)) +
geom_text(aes(population/10^6, total, label = abb),nudge_x = 1.5)#正数表示向右平移🌼6.7 全局和局部美学映射上述我们使用了两次aes(population/10^6, total),在每一个几何对象中都使用了。我们可以通过设置全局映射来实现代码的简化# 查看ggplot参数
args(ggplot)function (data = NULL, mapping = aes(), ..., environment = parent.frame())
NULL可以看出其中有mapping的定义,下面我们在定义ggplot对象时,先设定美学映射,还是上面那个例子,我们可以简化代码如下p <- murders %>% ggplot(aes(population/10^6, total, label = abb)) p + geom_point(size=3) +
geom_text(nudge_x = 1.5)当然,当我们需要使用新的美学映射时,可以在相应的geometry重新定义p + geom_point(size = 3)+
geom_text(x=10,y=800,label='JOJO')在第二个geom_text中,我们没有使用population和total🌷6.8 尺度变换我们可以对数据的尺度进行转换,使用scale_x_continuous()函数,其中trans参数来指定转换形式 例如进行log-转换p + geom_point(size = 3) +
# 此时取对数,需要将nudge_x 设定较小
geom_text(nudge_x = 0.05)+
scale_x_continuous(trans = 'log10')+
scale_y_continuous(trans = 'log10')在r中可以有scale_x_log10()函数直接转换标签p + geom_point(size = 3) +
# 此时取对数,需要将nudge_x 设定较小
geom_text(nudge_x = 0.05)+
scale_x_log10()+
scale_y_log10()注意:这里只是数据在图形中的位置取了对数,从而改变了原始坐标轴刻度,而不是对数据值进行变换。下面看一个简单的例子更好的理解x <- c(1,10,100,1000)
y <- c(1,10,100,1000)
df <- data.frame(x,y)
ggplot(df)+ geom_point(aes(x,y))上述是未做尺度变化前的图,下面来看看去log10变换后的结果ggplot(df)+ geom_point(aes(x,y))+scale_x_log10()+scale_y_log10()🌱6.9 坐标轴和标题同样我们可以设置坐标轴标签和图形标题p + geom_point(size = 3) +
# 此时取对数,需要将nudge_x 设定较小
geom_text(nudge_x = 0.05)+
scale_x_log10()+
scale_y_log10()+
xlab("Populations in millions (log scale)") +
ylab("Total number of murders (log scale)") +
ggtitle("US Gun Murders in 2010")和最初的图形比较,我们发现此时图形看起来美观了不少,下面我们介绍如何增加颜色🌲6.10 增加颜色p <- murders %>% ggplot(aes(population/10^6, total, label = abb)) +
geom_text(nudge_x = 0.05) +
scale_x_log10() +
scale_y_log10() +
xlab("Populations in millions (log scale)") +
ylab("Total number of murders (log scale)") +
ggtitle("US Gun Murders in 2010")p + geom_point(size = 3,col='red') 我们希望根据区域来定义颜色,注意,此时我们使用了数据的region值,因此此时需要用aes来定义p + geom_point(aes(col=region), size = 3)可以看出ggplot2会自动生成图例,可以使用show.legend=FALSE不显示图例🌳6.11 增加拟合曲线或注释下面我们通过y=rx拟合这些点,其中y是total,x是population,r=sum(total)/sum(population)*10^6.由于我们对数据取了对数变换,则log(y)=log(r)+log(x),此时可以拟合一条斜率为1,截距为log(r)的曲线# 首先计算r
r <- murders %>%
summarize(rate = sum(total) / sum(population) * 10^6) %>%
pull(rate)#将rate变为数值型数据使用geom_abline增加曲线,需要两个参数,截距默认为0,斜率默认为1p + geom_point(aes(col=region), size = 3) +
geom_abline(intercept = log10(r))🌴6.12 ggplot2附加库由于ggplot2的一些附加库,让ggplot2可视化的功能变得格外强大。主要有ggthemes和ggrepel包ggplot2的图形主题可以用theme()函数,例如使用dslabs主题的图,使用ds_theme_set()定义ds_theme_set()p + geom_point(aes(col=region), size = 3) +
geom_abline(intercept = log10(r))还有很多其他图形主题在ggthemes中,下面使用theme_economist主题library(ggthemes)
p + geom_point(aes(col=region), size = 3) +
geom_abline(intercept = log10(r)) + theme_economist()可以使用 theme_fivethirtyeight()查看有哪些主题。上图还有一个问题就是有的标签相互重叠了。可以使用ggrepel库,可以避免标签不会相互重叠。使用geom_text_repel代替geom_textlibrary(ggthemes)
library(ggrepel)
r <- murders %>%
summarize(rate = sum(total) / sum(population) * 10^6) %>%
pull(rate)
murders %>% ggplot(aes(population/10^6, total, label = abb)) +
geom_abline(intercept = log10(r), lty = 1, color = "black") +
geom_point(aes(col=region), size = 3) +
geom_text_repel() +
scale_x_log10() +
scale_y_log10() +
xlab("Populations in millions (log scale)") +
ylab("Total number of murders (log scale)") +
ggtitle("US Gun Murders in 2010") +
scale_color_discrete(name = "Region") +
theme_economist()
秋叶无缘
【R语言数据科学】:(九)数据清洗技巧之数据表连接大全
💮9.数据清洗之表连接有时候我们要分析的数据可能不是来自一个数据集,此时需要我们对不同表按照某一关键字进行连接 假设我们想探索美国各州的人口规模与选举人票之间的关系。我们的人口规模如下表所示:library(tidyverse)
library(dslabs)
head(murders)stateabbregionpopulationtotal<chr><chr><fct><dbl><dbl>1AlabamaALSouth47797361352AlaskaAKWest710231193ArizonaAZWest63920172324ArkansasARSouth2915918935CaliforniaCAWest3725395612576ColoradoCOWest502919665选票数据如下head(results_us_election_2016)stateelectoral_votesclintontrumpothers<chr><int><dbl><dbl><dbl>1California5561.731.66.72Texas3843.252.24.53Florida2947.849.03.24New York2959.036.54.55Illinois2055.838.85.46Pennsylvania2047.948.63.6直接将这两个表拼接在一起是不对的,因为他们的顺序不一致,因此我们需要考虑按照state连接两个表🏵️9.1 joins()函数left()函数是根据某个关键字列来将多个表进行合并。和sql中的语法基本类似,大家如果想了解sql语法可以看我的专栏。 一般的做法是,首先确定一个或多个用于匹配两个表的列,然后返回一个包含组合数据的新表。下面,我们使用left_join连接两个表,并且去除others的列,并将electoral_votes重命名tab <- right_join(murders, results_us_election_2016, by = "state") %>%
select(-others) %>% rename(ev=electoral_votes)
head(tab)stateabbregionpopulationtotalevclintontrump<chr><chr><fct><dbl><dbl><int><dbl><dbl>1AlabamaALSouth4779736135934.462.12AlaskaAKWest71023119336.651.33ArizonaAZWest63920172321145.148.74ArkansasARSouth291591893633.760.65CaliforniaCAWest3725395612575561.731.66ColoradoCOWest502919665948.243.3现在我们成功的将两个表连接在一起了,下面我们简单绘制一下人口和选票之间的关系tab %>% ggplot(aes(population/10^6, ev, label = abb)) +
geom_point() +
geom_text_repel() +
scale_x_continuous(trans = "log2") +
scale_y_continuous(trans = "log2") +
geom_smooth(method = "lm", se = FALSE)`geom_smooth()` using formula 'y ~ x'
Warning message:
"ggrepel: 15 unlabeled data points (too many overlaps). Consider increasing max.overlaps"从结果来看,可以看出人口越多,选票越多,平均而言每十万人有2选票,但是在人口较少的州,这个比例稍高实际上,并不总是一个表中的每一行在另一个表中都有一个匹配的行。因此,我们有不同的连接方法,为例举例说明, 下面我们先创建tab1和tab2tab_1 <- slice(murders, 1:6) %>% select(state, population)
tab_1statepopulation<chr><dbl>Alabama4779736Alaska710231Arizona6392017Arkansas2915918California37253956Colorado5029196tab_2 <- results_us_election_2016 %>%
filter(state%in%c("Alabama", "Alaska", "Arizona",
"California", "Connecticut", "Delaware")) %>%
select(state, electoral_votes) %>% rename(ev = electoral_votes)
tab_2stateev<chr><int>California55Arizona11Alabama9Connecticut7Alaska3Delaware3🌹9.1.1 左连接(left join)假设我们想要一张像tab_1这样的表格,但将选举人票添加到我们现有州。此时tab_1作为第一个参数。我们指定要使用哪个列与by参数匹配。没能匹配到的会自动返回NAleft_join(tab_1,tab_2,by = 'state') statepopulationev<chr><dbl><int>Alabama47797369Alaska7102313Arizona639201711Arkansas2915918NACalifornia3725395655Colorado5029196NA可以看出Arkansas和Colorado的ev没有匹配到,所以返回NA,下面我们将tab_2传入第一个参数left_join(tab_2,tab_1,by = 'state') stateevpopulation<chr><int><dbl>California5537253956Arizona116392017Alabama94779736Connecticut7NAAlaska3710231Delaware3NA可以看出结果返回了包含了tab_2的所有信息,其中Connecticut和Delaware的population返回NV🥀9.1.2 右连接(right join)右连接与左连接相反,返回第二个表的全部信息,匹配连接第一个表,下面我们来看一个案例tab_2 %>% right_join(tab_1, by = "state")stateevpopulation<chr><int><dbl>California5537253956Arizona116392017Alabama94779736Alaska3710231ArkansasNA2915918ColoradoNA5029196可以看出结果交换参数位置后使用右连接和直接使用左连接的结果一致。可以这样理解:左连接是将第一个表的所有信息保存下来了,同时按照某一列匹配第二个表的信息并整合起来,没有匹配到的返回NA右连接是将第二个表的所有信息保存下来了,相当于是左连接交换参数🌺9.1.3 内连接(inner join)如果我们只希望得到两个表中都包含的信息,我们可以使用内连接,没有匹配到的行将不显示inner_join(tab_1, tab_2, by = "state")statepopulationev<chr><dbl><int>Alabama47797369Alaska7102313Arizona639201711California3725395655🌻9.1.4 全连接(full join)如果我们希望保留两个表的所有行,并且用NA来填充未匹配到的值,如下所示full_join(tab_1, tab_2, by = "state")statepopulationev<chr><dbl><int>Alabama47797369Alaska7102313Arizona639201711Arkansas2915918NACalifornia3725395655Colorado5029196NAConnecticutNA7DelawareNA3🌼9.1.5 半连接(semi join)这个方法在mysql中没有涉及到,它具体工作原理是匹配两个表都有的行,然后只显示第一个表的信息。 相当于先做内连接,再把第二个表的信息去除,具体如下:semi_join(tab_1, tab_2, by = "state")statepopulation<chr><dbl>Alabama4779736Alaska710231Arizona6392017California37253956🌷9.1.6 反连接(anti join)函数anti_join与semi_join相反。它保留第一个表中没有匹配到第二个表中信息的元素,具体如下anti_join(tab_1, tab_2, by = "state")statepopulation<chr><dbl>Arkansas2915918Colorado5029196🌱9.2 binding另一个连接表的方法是使用binding,和join函数不同,binding函数不通过某个变量匹配,而是直接连接数据框,如果两个数据框纬度不同,则会报错🌲9.2.1 按列连接使用bind_cols,可以方便的连接不同列,例如bind_cols (a=1:3,b=4:6)ab<int><int>142536注意,有一个R函数cbind具有完全相同的功能。 主要区别是cbind可以创建不同类型的对象,而bind_cols总是生成一个数据框。bind_cols也可以连接不同的数据框,如下所示tab_1 <- tab[, 1:3]
tab_2 <- tab[, 4:6]
tab_3 <- tab[, 7:8]
new_tab <- bind_cols(tab_1, tab_2, tab_3)
head(new_tab)stateabbregionpopulationtotalevclintontrump<chr><chr><fct><dbl><dbl><int><dbl><dbl>1AlabamaALSouth4779736135934.462.12AlaskaAKWest71023119336.651.33ArizonaAZWest63920172321145.148.74ArkansasARSouth291591893633.760.65CaliforniaCAWest3725395612575561.731.66ColoradoCOWest502919665948.243.3🌳9.2.2 按行连接bind_rows()和bind_cols()函数类似,但是是按行连接tab_1 <- tab[1:2,]
tab_2 <- tab[3:4,]
bind_rows(tab_1, tab_2)stateabbregionpopulationtotalevclintontrump<chr><chr><fct><dbl><dbl><int><dbl><dbl>AlabamaALSouth4779736135934.462.1AlaskaAKWest71023119336.651.3ArizonaAZWest63920172321145.148.7ArkansasARSouth291591893633.760.6同样的,R语言基础函数rbin()可以实现类似的功能🌴9.3 集合运算符另一组用于连接数据集方法是集合运算符。当运用在向量上面,则实现和它们字面意思一样,例如intersect、union、setdiff和setequal。 则这些函数可以用于数据帧,而不仅仅是向量。🌵9.3.1 交集(intersect)intersect(1:10, 6:15).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}678910intersect(c("a","b","c"), c("b","c","d")).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'b''c'当我们导入了dyply包,我们可以使用intersect应用在数据框上,其功能也是求交集tab_1 <- tab[1:5,]
tab_2 <- tab[3:7,]
intersect(tab_1, tab_2)stateabbregionpopulationtotalevclintontrump<chr><chr><fct><dbl><dbl><int><dbl><dbl>ArizonaAZWest63920172321145.148.7ArkansasARSouth291591893633.760.6CaliforniaCAWest3725395612575561.731.6🌾9.3.2 并集(Union)union(1:10, 6:15).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}123456789101112131415union(c("a","b","c"), c("b","c","d")).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}'a''b''c''d'tab_1 <- tab[1:5,]
tab_2 <- tab[3:7,]
dplyr::union(tab_1, tab_2) stateabbregionpopulationtotalevclintontrump<chr><chr><fct><dbl><dbl><int><dbl><dbl>AlabamaALSouth4779736135934.462.1AlaskaAKWest71023119336.651.3ArizonaAZWest63920172321145.148.7ArkansasARSouth291591893633.760.6CaliforniaCAWest3725395612575561.731.6ColoradoCOWest502919665948.243.3ConnecticutCTNortheast357409797754.640.9🌿9.3.3 差集(setdiff)返回第一个参数对第二个参数的差集,因此和上面两个方法不同,setdiff不是对称的,具体案例如下setdiff(1:10, 6:15)
setdiff(6:15, 1:10).list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}12345.list-inline {list-style: none; margin:0; padding: 0} .list-inline>li {display: inline-block} .list-inline>li:not(:last-child)::after {content: "\00b7"; padding: 0 .5ex}1112131415tab_1 <- tab[1:5,]
tab_2 <- tab[3:7,]
dplyr::setdiff(tab_1, tab_2)stateabbregionpopulationtotalevclintontrump<chr><chr><fct><dbl><dbl><int><dbl><dbl>AlabamaALSouth4779736135934.462.1AlaskaAKWest71023119336.651.3☘️9.3.4 setequalsetequal判断两个集合是否相等,而不管顺序。例如setequal(1:5, 1:6)FALSEsetequal(1:5, 5:1)TRUEsetequal(tab_1, tab_2)FALSE
秋叶无缘
R语言数据科学系列
覆盖了R语言在数据科学领域的基础与实用知识,包括编程基础、数据处理、文件操作、数据可视化等多个方面。通过系统学习,学员将全面掌握使用R语言进行数据科学的技能
秋叶无缘
大数据闯关之MySQL基础篇(五):DCL用户管理
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。一、DCLDCL(Data Control Language)数据控制语言,用来管理数据库用户、控制数据库的访问权限。管理用户查询用户USE mysql;
SELECT * FROM user;创建用户CREATE USER '用户名'@'主机名' IDENTIFIED BY '密码'; 修改用户密码ALTER USER '用户名'@'主机名' IDENTIFIED WITH mysql_native_password BY '新密码'; 删除用户DROP USER '用户名'@'主机名';权限控制权限说明ALL所有权限SELECT查询数据INSERT插入数据UPDATE修改数据DELETE删除数据ALTER修改表DROP删除数据库/表/视图CREATE创建数据库/表查询权限· SHOW GRANTS FOR '用户名'@'主机名';授予权限· GRANTS 权限列表 ON 数据库名.表名 TO '用户名'@'主机名';撤销权限 · REVOKE 权限列表 ON 数据库名.表名 TO '用户名'@'主机名';
秋叶无缘
大数据闯关之Hadoop篇(一):大数据概述
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。之前我们过完了MySQL的基础部分和进阶部分,我们现在来到了大数据工具的Hadoop。一、大数据概述大数据:狭义上:使用分布式技术完成海量数据的处理,得到数据背后蕴含的价值广义上:数字化时代、信息化时代的基础支撑,以数据为生活赋能 海量的数据基础设施生活五个主要特征:从海量的高增长、多类别、低信息密度的数据中挖掘出高质量的结果 数据体量大:采集、存储和计算数据量大种类、来源多样化低价值密度:信息海量但是价值密度低速度快:数据增长、获取数据和数据处理速度快数据质量高二、大数据软件生态大数据核心工作主要分为以下三个部分,每个部分又拥有自己的大数据软件数据存储 Apache Hadoop - HDFS:Apache Hadoop框架内的组件HDFS是大数据体系中使用最为广泛的分布式存储技术Apache HBase:Apache HBase是大数据体系内使用非常广泛的NoSQL KV型数据库技术,HBase是基于HDFS之上构建的数据计算 Apache Hadoop - MapReduce:Apache Hadoop - MapReduce组件是最早一代的大数据分布式计算引擎Apache Hive:Apache Hive是一款以SQL为开发语言的分布式计算框架,其底层使用了Hadoop的MapReduce技术Apache SparkApache Flink数据传输 Apache Kafka:一款分布式的消息系统,可以完成海量规模的数据传输工作大数据每个部分都有自己对应的软件,以上列到的都是主流使用的软件,其中还有不少其他的大数据软件,但是本专栏将只对以上提到的软件进行学习总结整理三、Hadoop概述Hadoop是Apache软件基金会下的顶级开源项目,用以提供:分布式数据存储分布式数据计算分布式资源调度为一体的整体解决方案Apache Hadoop是典型的分布式软件框架,可以部署在1台甚至上千台服务器节点上协同工作,个人或企业可以借助Hadoop构建大规模服务器集群,完成海量数据的存储和计算通常意义上,Hadoop是一个整体,其内部还会细分为三个功能组件,分别是:HDFS组件:HDFS是Hadoop内的分布式存储组件,可以构建分布式文件系统用于数据存储MapReduce组件:MapReduce是Hadoop内分布式计算组件,提供编程接口供用户开发分布式计算程序YARN组件:YARN是Hadoop内分布式资源调度组件,可供用户整体调度大规模集群的资源使用四、分布式存储为什么需要分布式存储假设我们有一台服务器和一个100TB的文件,服务器无法存储下那么大的文件,那么就需要用多个服务器,将文件分为多个部分,将每个部分文件存储到对应的服务器。分布式不仅仅是解决了能存的问题,多台服务器协同工作带来的也是性能的横向扩展。分布式的基础架构分析去中心化模式:没有明确的中心,众多服务器之间基于特定规则进行同步协调中心化模式:以其中一台服务器为中心,该服务器可以调度其他服务器,进行统一只会,统一调度,避免混乱,这种模式也被称为一主多从模式,简称主从模式。大数据框架大多数的基础架构上,都是符合中心化模式的HDFS的基础架构HDFS是Hadoop三大组件之一,全称为Hadoop Distributed File System(Hadoop分布式文件系统),是Hadoop技术栈内提供的分布式数据存储解决方案,可以在多台服务器上构建存储集群,存储海量的数据。HDFS集群有主角色NameNode、从角色DataNode和主角色辅助角色SecondaryNameNode,其关系如下图 * NameNode:HDFS系统的主角色,是一个独立的进程,负责管理HDFS整个文件系统,负责管理DataNode
* DataNode:HDFS系统的从角色,是一个独立进程,主要负责数据的存储,即存入数据和取出数据
* SecondaryNameNode:NameNode的辅助,是一个独立进程,主要帮助NameNode完成元数据整理工作五、大数据环境这里我为了方便,就在网上找了一套搭建好所需大数据环境的三套虚拟机,分别为Node1、Node2和Node3,其中安装好了hadoop、spark、hive等大数据环境,大家有需要的可以评论一下,我私发给大家。
秋叶无缘
大数据闯关之MySQL进阶篇(一):存储引擎
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。这部分接着之前的MySQL基础篇,接下来将对MySQL进阶部分进行学习。一、MySQL体系结构我们可以看到MySQL Server是分层的,分为连接层、服务层、引擎层和存储层连接层:主要接收客户端的连接,完成连接的处理并且完成连接的授权服务层:绝大部分的功能是在服务层实现的,如sql接口、解析器等引擎层:存储引擎的提供存储层:存储数据库的相关数据二、存储引擎简介存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的,而不是基于库的,所以存储引擎也可以被称为表类型。MySQL建表的默认存储引擎为InnoDB。在创建表时,指定存储引擎CREATE TABLE 表名(
字段1 字段1类型 [COMMENT 字段1注释],
...
字段n 字段n类型 [COMMENT 字段n注释]
) ENGINE=INNODB [COMMENT 表注释];查看当前数据库支持的存储引擎SHOW ENGINES;三、存储引擎特点InnoDB:InnoDB是一种兼顾高可靠性和高性能的通用存储引擎。innoDB引擎的每张表都会对应这样一个表空间文件xxx.ibd,存储该表的表结构、数据和索引。其特点是 DML操作遵循ACID模型,支持事务行级锁,提高并发访问性能支持外键FOREIGN KEY约束,保证数据的完整性和正确性MyISAM:MyISAM是MySQL早期的默认存储引擎。其特点是: 不支持事务,不支持外键支持表锁,不支持行锁访问速度快Memory:Memory引擎的表数据是存储在内存中的,由于受到硬件问题的影响,只能将这些表作为临时表或缓存使用。其特点是: 内存存放hash索引(默认)
秋叶无缘
Python数据分析从入门到进阶:模型评估和选择(含详细代码)
引言之前我们介绍了机器学习的一些基础性工作,介绍了如何对数据进行预处理,接下来我们可以根据这些数据以及我们的研究目标建立模型。那么如何选择合适的模型呢?首先需要对这些模型的效果进行评估。本文介绍如何使用sklearn代码进行模型评估模型评估 对模型评估的基本步骤如下:首先将要将数据集分为训练集和测试集对训练集进行模型拟合确定合适的评估指标计算在测试集上的评估指标1 数据集划分在机器学习问题中,从理论上我们需要对数据集划分为训练集、验证集、测试集。训练集:拟合模型(平常的作业和测试)验证集:计算验证集误差,选择模型(模拟考)测试集:评估模型(最终考试) 但是在实际应用中,一般分为训练集和测试集两个。其中训练集:70%,测试集:30%.这个比例在深度学习中可以进行相应的调整。 我们可以使用sklearn中的train_test_split划分数据集# 导入相关库
from sklearn.model_selection import train_test_split
from sklearn import datasets
from sklearn import metrics
from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import pandas as pd# 导入数据
df = pd.read_csv(r'C:\Users\DELL\data-science-learning\seaborn-data\iris.csv')
df.shape(150, 5)# 划分数据集和测试集
train_set, test_set = train_test_split(df, test_size=0.3,
random_state=12345)train_set.shape, test_set.shape((105, 5), (45, 5))可以看出此时训练集只有105个数据,测试集有45个数据。2.交叉验证模型评估模型时,我们最常用的方法之一就是==交叉验证==,下面以一个具体案例来看如何实现,代码如下# 加载数据
digits = datasets.load_digits()# 创建特征矩阵
features = digits.data
target = digits.target# 进行标准化
stand = StandardScaler()# 创建logistic回归器
logistic = LogisticRegression()# 创建一个包含数据标准化和逻辑回归的流水线
pipline = make_pipeline(stand, logistic)# 先对数据进行标准化,再用logistic回归拟合# 创建k折交叉验证对象
kf = KFold(n_splits=10, shuffle=True, random_state=1)使用shuffle打乱数据,保证我们验证集和训练集是独立同分布的(IID)的# 进行k折交叉验证
cv_results = cross_val_score(pipline,
features,
target,
cv=kf,
scoring='accuracy',#评估的指标
n_jobs=-1)#调用所有的cpucv_results.mean()0.9693916821849783使用pipeline方法可以使得我们这个过程很方便,上述我们是直接对数据集进行了交叉验证,在实际应用中,建议先对数据集进行划分,再对训练集使用交叉验证。from sklearn.model_selection import train_test_split# 划分数据集
features_train, features_test, target_train, target_test = train_test_split(features,
target,
test_size=0.1,random_state=1)# 使用训练集来计算标准化参数
stand.fit(features_train)StandardScaler()# 然后在训练集和测试集上运用
features_train_std = stand.transform(features_train)
features_test_std = stand.transform(features_test)这里之所以这样处理是因为我们的测试集是未知数据,如果使用测试集和训练集一起训练预处理器的话,测试集的信息有一部分就会泄露,因此是不科学的。在这里我认为更general的做法是先将训练集训练模型,用验证集评估选择模型,最后再用训练集和验证集一起来训练选择好的模型,再来在测试集上进行测试。pipeline = make_pipeline(stand, logistic)cv_results = cross_val_score(pipline,
features_train_std,
target_train,
cv=kf,
scoring='accuracy',
n_jobs=-1)cv_results.mean()0.96351123380108893.回归模型评估指标评估回归模型的主要指标有以下几个 # 导入相关库
from sklearn.datasets import make_regression
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LinearRegression
from sklearn import metrics# 建立模拟数据集
features, target = make_regression(n_samples=100,
n_features=3,
n_informative=3,
n_targets=1,
noise=50,
coef=False,
random_state=1)# 创建LinerRegression回归器
ols = LinearRegression()metrics.SCORERS.keys()dict_keys(['explained_variance', 'r2', 'max_error', 'neg_median_absolute_error', 'neg_mean_absolute_error', 'neg_mean_absolute_percentage_error', 'neg_mean_squared_error', 'neg_mean_squared_log_error', 'neg_root_mean_squared_error', 'neg_mean_poisson_deviance', 'neg_mean_gamma_deviance', 'accuracy', 'top_k_accuracy', 'roc_auc', 'roc_auc_ovr', 'roc_auc_ovo', 'roc_auc_ovr_weighted', 'roc_auc_ovo_weighted', 'balanced_accuracy', 'average_precision', 'neg_log_loss', 'neg_brier_score', 'adjusted_rand_score', 'rand_score', 'homogeneity_score', 'completeness_score', 'v_measure_score', 'mutual_info_score', 'adjusted_mutual_info_score', 'normalized_mutual_info_score', 'fowlkes_mallows_score', 'precision', 'precision_macro', 'precision_micro', 'precision_samples', 'precision_weighted', 'recall', 'recall_macro', 'recall_micro', 'recall_samples', 'recall_weighted', 'f1', 'f1_macro', 'f1_micro', 'f1_samples', 'f1_weighted', 'jaccard', 'jaccard_macro', 'jaccard_micro', 'jaccard_samples', 'jaccard_weighted'])# 使用MSE对线性回归做交叉验证
cross_val_score(ols, features, target, scoring='neg_mean_squared_error', cv=5)array([-1974.65337976, -2004.54137625, -3935.19355723, -1060.04361386,
-1598.74104702])cross_val_score(ols, features, target, scoring='r2')array([0.8622399 , 0.85838075, 0.74723548, 0.91354743, 0.84469331])4.创建一个基准回归模型from sklearn.datasets import load_boston
from sklearn.dummy import DummyRegressor
from sklearn.model_selection import train_test_split# 加载数据
boston = load_boston()features, target = boston.data, boston.target# 将数据分为测试集和训练集
features_train, features_test, target_train, target_test = train_test_split(features, target,
random_state=0)# 创建dummyregression对象
dummy = DummyRegressor(strategy='mean')# 训练模型
dummy.fit(features_train, target_train)DummyRegressor()dummy.score(features_test, target_test)-0.001119359203955339# 下面我们训练自己的模型进行对比
from sklearn.linear_model import LinearRegression
ols = LinearRegression()
ols.fit(features_train, target_train)LinearRegression()ols.score(features_test, target_test)0.6354638433202129通过与基准模型的对比,我们可以发现我们线性回归模型的优势5.混淆矩阵评估分类器性能一个重要方法是查看混淆矩阵。一般的想法是计算A类实例被分类为B类的次数,以及B类被预测为A类的个数。要计算混淆矩阵,首先需要有一组预测,以便与实际目标进行比较。==混淆矩阵==如下图所示:其中:TP:正确预测正类的个数FP:错误预测正类的个数TN:正确预测负类的个数FN:错误预测负类的个数下面我们来看如何使用具体的代码得到混淆矩阵# 导入相关库
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
import pandas as pd# 加载数据
iris = load_iris()features = iris.datatarget = iris.targetclass_names = iris.target_namesfeatures_train, features_test, target_train, target_test = train_test_split(
features, target, random_state = 1)classfier = LogisticRegression()target_predicted = classfier.fit(features_train, target_train).predict(features_test)# 创建一个混淆矩阵
matrix = confusion_matrix(target_test, target_predicted)df = pd.DataFrame(matrix, index = class_names, columns=class_names)sns.heatmap(df, annot=True, cbar=None, cmap='Blues')
plt.ylabel('True Class')
plt.xlabel('Predict Class')
plt.title('Confusion matrix')Text(0.5, 1.0, 'Confusion matrix')6.分类评估指标对于分类问题的评估指标主要包含以下几个:其中,对于非均衡数据,使用F1-score比较合理。下面我们来看具体如何得到这些评估指标from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification# 创建模拟数据集
X, y = make_classification(random_state=1,
n_samples=1000,
n_features=3,
n_informative=3,
n_redundant=0,
n_classes=2)# 创建逻辑回归器
logit = LogisticRegression()# 使用准确率对模型进行交叉验证
cross_val_score(logit, X, y, scoring='accuracy')array([0.87, 0.88, 0.85, 0.93, 0.9 ])cross_val_score(logit, X, y, scoring='f1')array([0.87735849, 0.88235294, 0.85849057, 0.92708333, 0.90384615])cross_val_score(logit,X,y,scoring='precision')array([0.83035714, 0.86538462, 0.8125 , 0.9673913 , 0.86238532])其中,我们可以看出,==召回率==和==精确率==两个往往不会同时增加(增加样本量可能可以让两个指标同时增加),这里有点像我们假设检验中的第一类错误和第二类错误。因此,我们要保证这两个指标都不能太小。下面我们介绍ROC和AUC7.ROC和AUC7.1 ROC曲线RUC曲线是用于二分类器的另一个常用工具。它与精密度/召回率非常相似,但不是绘制精密度与召回率的关系,而是绘制真阳性率(召回率的另一个名称)与假阳性率(FPR)的关系。FPR是未正确归类为正的负实例的比率。通过ROC曲线来进行评估,计算出每个阈值下的真阳性率和假阳性率# 导入相关库
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_curve, roc_auc_score
from sklearn.model_selection import train_test_splitfeatures, target = make_classification(n_samples=1000,
n_features=10,
n_classes=2,
n_informative=3,
random_state=3)features_train, features_test, target_train, target_test = train_test_split(features,
target,
test_size=.1,
random_state=1)logit.fit(features_train, target_train)LogisticRegression()# 预测为1的概率
target_probabilities = logit.predict_proba(features_test)[:,1]target_testarray([0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1,
1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0,
1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0,
1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1])这里我们选取所有第二列的概率的值,也就是所有为正类的值false_positive_rate, true_positive_rate, thresholds = roc_curve(target_test,target_probabilities)我们默认是将概率大于50%的判断为正类,但当我们实际应用时,可以对阈值进行相应的调整,例如我们可以增加阈值,保证正类的准确度更高,如下所示y_predict = target_probabilities>0.6
y_predictarray([False, False, True, False, True, True, False, True, False,
False, False, True, False, False, False, True, False, False,
False, False, True, True, True, False, True, True, True,
False, True, False, True, False, True, True, False, False,
True, True, True, True, True, False, False, True, False,
True, True, False, False, False, False, True, False, False,
True, True, True, False, True, False, True, False, False,
True, True, False, True, True, True, True, True, True,
False, True, False, False, True, False, False, False, False,
True, True, False, True, False, True, False, True, False,
False, True, False, False, True, False, True, False, False,
True])# 绘制AUC曲线
plt.plot(false_positive_rate, true_positive_rate)
plt.plot([0, 1], ls='--')
plt.plot([0, 0], [1, 0], c='.7')
plt.plot([1,1], c='.7')# 我们可以通过predict_proba 查看样本的预测概率
logit.predict_proba(features_test)[2]array([0.02210395, 0.97789605])logit.classes_array([0, 1])7.2 AUC值比较分类器的一种方法是测量曲线下面积(AUC)。完美分类器的AUC等于1,而适当的随机分类器的AUC等于0.5。Sklearn提供了一个计算AUC的函数roc_auc_score计算==AUC==值 roc_auc_score(target_test,target_probabilities)0.9747899159663865可以看出该分类器的AUC值为0.97,说明该模型的效果很好。由于ROC曲线与精度/召回(PR)曲线非常相似,您可能想知道如何决定使用哪一条曲线。根据经验,当阳性类别很少,或者当你更关心假阳性而不是假阴性时,你应该更喜欢PR曲线。否则,使用ROC曲线。8.创建一个基准分类模型from sklearn.datasets import load_iris
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import train_test_splitiris = load_iris()features, target = iris.data, iris.target# 划分数据集
features_train, features_test, target_train, target_test = train_test_split(features, target,
random_state=0)dummy = DummyClassifier(strategy='uniform', random_state=1)dummy.fit(features_train, target_train)DummyClassifier(random_state=1, strategy='uniform')dummy.score(features_test, target_test)0.42105263157894735# 接下来我们创建自己的模型from sklearn.ensemble import RandomForestClassifier#随机森林分类,考虑在后面分享classfier = RandomForestClassifier()classfier.fit(features_train, target_train)RandomForestClassifier()classfier.score(features_test, target_test)0.9736842105263158可以看出,随机森林模型效果更好9.可视化训练集规模的影响我们都知道,只要给我们足够多的数据集,那我们基本能训练一个效果很好的模型,接下来我们来看看如何绘制训练集大小对模型效果的影响(learning curve)import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_digits
from sklearn.model_selection import learning_curvedigits = load_digits()features, target = digits.data, digits.target# 使用交叉验证为不同规模的训练集计算训练和测试得分
train_sizes, train_scores, test_scores = learning_curve(RandomForestClassifier(),
features,
target,
cv=10,
scoring='accuracy',
n_jobs=-1,
train_sizes=np.linspace(0.01,1,50))# 计算训练集得分的平均值和标准差
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)plt.plot(train_sizes, train_mean, '--', color='black', label='Training score')
plt.plot(train_sizes, test_mean, color='black', label='Cross-validation score')
plt.fill_between(train_sizes, train_mean-train_std,
train_mean + train_std, color='#DDDDDD')
plt.fill_between(train_sizes, test_mean-test_std,
test_mean + test_std, color='#DDDDDD')
plt.title('learning_curve')
plt.xlabel('Training Set Size')
plt.ylabel('Accuracy Score')
plt.legend(loc='best')
plt.tight_layout()
plt.show()10. 生成评估指标报告from sklearn.metrics import classification_reportiris = datasets.load_iris()features = iris.datatarget = iris.targetclass_names = iris.target_namesfeatures_train, features_test, target_train, target_test = train_test_split(
features, target, random_state = 1)classfier = LogisticRegression()model = classfier.fit(features_train, target_train)
target_predicted = model.predict(features_test)# 生成分类器的性能报告
print(classification_report(target_test,
target_predicted,
target_names=class_names)) precision recall f1-score support
setosa 1.00 1.00 1.00 13
versicolor 1.00 0.94 0.97 16
virginica 0.90 1.00 0.95 9
accuracy 0.97 38
macro avg 0.97 0.98 0.97 38
weighted avg 0.98 0.97 0.97 38
秋叶无缘
Python数据可视化大杀器之天阶技法:Seaborn(学完可实现90%数据分析绘图)
Python数据可视化python数据可视化大杀器之Seaborn详解一张好的图胜过一千个字,一个好的数据分析师必须学会用图说话。python作为数据分析最常用的工具之一,它的可视化功能也很强大,matplotlib和seaborn库使得绘图变得更加简单。本章主要介绍一下Searborn绘图。学过matplotlib的小伙伴们一定被各种参数弄得迷糊,而seaborn则避免了这些问题,废话少说,我们来看看seaborn具体是怎样使用的。Seaborn中概况起来可以分为五大类图1.关系类绘图2.分类型绘图3.分布图4.回归图5.矩阵图接下来我们一一讲解这些图形的应用,首先我们要导入一下基本的库%matplotlib inline
# 如果不添加这句,是无法直接在jupyter里看到图的
import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt如果上面报错的话需要安装相应的包pip install seaborn
pip install numpy
pip install pandas
pip install matplotlib我们可以使用set()设置一下seaborn的主题,一共有:darkgrid,whitegrid,dark,white,ticks,大家可以根据自己的喜好设置相应的主题,默认是darkgrid。我这里就设置darkgrid风格sns.set(style="darkgrid")接下来导入我们需要的数据集,seaborn和R语言ggplot2(感兴趣欢迎阅读我的R语言ggplot2专栏)一样有许多自带的样例数据集# 导入anscombe数据集
df = sns.load_dataset('anscombe')
# 观察一下数据集形式
df.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } datasetxy0I10.08.041I8.06.952I13.07.583I9.08.814I11.08.331.关系图1.1 lineplot绘制线段seaborn里的lineplot函数所传数据必须为一个pandas数组,这一点跟matplotlib里有较大区别,并且一开始使用较为复杂,sns.lineplot里有几个参数值得注意。x: plot图的x轴labely: plot图的y轴labelci: 置信区间data: 所传入的pandas数组绘制时间序列图# 导入数据集
fmri = sns.load_dataset("fmri")
# 绘制不同地区不同时间 x和y的线性关系图
sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)<AxesSubplot:xlabel='timepoint', ylabel='signal'>rs = np.random.RandomState(365)
values = rs.randn(365, 4).cumsum(axis=0)
dates = pd.date_range("1 1 2016", periods=365, freq="D")
data = pd.DataFrame(values, dates, columns=["A", "B", "C", "D"])
data = data.rolling(7).mean()
sns.lineplot(data=data, palette="tab10", linewidth=2.5)<AxesSubplot:>1.2 relplot这是一个图形级别的函数,它用散点图和线图两种常用的手段来表现统计关系。# 导入数据集
dots = sns.load_dataset("dots")
sns.relplot(x="time", y="firing_rate",
hue="coherence", size="choice", col="align",
size_order=["T1", "T2"],
height=5, aspect=.75, facet_kws=dict(sharex=False),
kind="line", legend="full", data=dots)<seaborn.axisgrid.FacetGrid at 0x1d7d3634e50>1.3 scatterplot(散点图)diamonds.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } caratcutcolorclaritydepthtablepricexyz00.23IdealESI261.555.0326.03.953.982.4310.21PremiumESI159.861.0326.03.893.842.3120.23GoodEVS156.965.0327.04.054.072.3130.29PremiumIVS262.458.0334.04.204.232.6340.31GoodJSI263.358.0335.04.344.352.75sns.set(style="whitegrid")
# Load the example iris dataset
diamonds = sns.load_dataset("diamonds")
# Draw a scatter plot while assigning point colors and sizes to different
# variables in the dataset
f, ax = plt.subplots(figsize=(6.5, 6.5))
sns.despine(f, left=True, bottom=True)
sns.scatterplot(x="depth", y="table",
data=diamonds, ax=ax)<AxesSubplot:xlabel='depth', ylabel='table'>1.4 气泡图气泡图是在散点图的基础上,指定size参数,根据size参数的大小来绘制点的大小1.4.1 普通气泡图# 导入鸢尾花数据集
planets = sns.load_dataset("planets")
cmap = sns.cubehelix_palette(rot=-.2, as_cmap=True)
ax = sns.scatterplot(x="distance", y="orbital_period",
hue="year", size="mass",
palette=cmap, sizes=(10, 200),
data=planets)1.4.2 彩色气泡图sns.set(style="white")
#加载示例mpg数据集
mpg = sns.load_dataset("mpg")
# 绘制气泡图
sns.relplot(x="horsepower", y="mpg", hue="origin", size="weight",
sizes=(40, 400), alpha=.5, palette="muted",
height=6, data=mpg)2. 分类型图表2.1 boxplot(箱线图)箱形图(Box-plot)又称为盒须图、盒式图或箱线图,是一种用作显示一组数据分散情况资料的统计图。它能显示出一组数据的最大值、最小值、中位数及上下四分位数。绘制分组箱线图# 导入数据集
tips = sns.load_dataset("tips")
# 绘制嵌套的箱线图,按日期和时间显示账单
sns.boxplot(x="day", y="total_bill",
hue="smoker", palette=["m", "g"],
data=tips)
sns.despine(offset=10, trim=True)2.2 violinplot(小提琴图)violinplot与boxplot扮演类似的角色,它显示了定量数据在一个(或多个)分类变量的多个层次上的分布,这些分布可以进行比较。不像箱形图中所有绘图组件都对应于实际数据点,小提琴绘图以基础分布的核密度估计为特征。绘制简单的小提琴图# 生成模拟数据集
rs = np.random.RandomState(0)
n, p = 40, 8
d = rs.normal(0, 2, (n, p))
d += np.log(np.arange(1, p + 1)) * -5 + 10
# 使用cubehelix获得自定义的顺序调色板
pal = sns.cubehelix_palette(p, rot=-.5, dark=.3)
# 如何使用小提琴和圆点进行每种分布
sns.violinplot(data=d, palette=pal, inner="point")<AxesSubplot:>绘制分组小提琴图tips = sns.load_dataset("tips")
# 绘制一个嵌套的小提琴图,并拆分小提琴以便于比较
sns.violinplot(x="day", y="total_bill", hue="smoker",
split=True, inner="quart",
palette={"Yes": "y", "No": "b"},
data=tips)
sns.despine(left=True)2.3 barplot(条形图)条形图表示数值变量与每个矩形高度的中心趋势的估计值,并使用误差线提供关于该估计值附近的不确定性的一些指示。绘制水平的条形图crashes = sns.load_dataset("car_crashes").sort_values("total", ascending=False)
# 初始化画布大小
f, ax = plt.subplots(figsize=(6, 15))
# 绘出总的交通事故
sns.set_color_codes("pastel")
sns.barplot(x="total", y="abbrev", data=crashes,
label="Total", color="b")
# 绘制涉及酒精的车祸
sns.set_color_codes("muted")
sns.barplot(x="alcohol", y="abbrev", data=crashes,
label="Alcohol-involved", color="b")
# 添加图例和轴标签
ax.legend(ncol=2, loc="lower right", frameon=True)
ax.set(xlim=(0, 24), ylabel="",
xlabel="Automobile collisions per billion miles")
sns.despine(left=True, bottom=True)绘制分组条形图titanic = sns.load_dataset("titanic")
# 绘制分组条形图
g = sns.barplot(x="class", y="survived", hue="sex", data=titanic,
palette="muted")2.4 pointplot(点图)点图代表散点图位置的数值变量的中心趋势估计,并使用误差线提供关于该估计的不确定性的一些指示。点图可能比条形图更有用于聚焦一个或多个分类变量的不同级别之间的比较。他们尤其善于表现交互作用:一个分类变量的层次之间的关系如何在第二个分类变量的层次之间变化。连接来自相同色调等级的每个点的线允许交互作用通过斜率的差异进行判断,这比对几组点或条的高度比较容易。sns.set(style="whitegrid")
iris = sns.load_dataset("iris")
# 将数据格式调整
iris = pd.melt(iris, "species", var_name="measurement")
# 初始化图形
f, ax = plt.subplots()
sns.despine(bottom=True, left=True)
sns.stripplot(x="value", y="measurement", hue="species",
data=iris, dodge=True, jitter=True,
alpha=.25, zorder=1)
# 显示条件平均数
sns.pointplot(x="value", y="measurement", hue="species",
data=iris, dodge=.532, join=False, palette="dark",
markers="d", scale=.75, ci=None)
# 图例设置
handles, labels = ax.get_legend_handles_labels()
ax.legend(handles[3:], labels[3:], title="species",
handletextpad=0, columnspacing=1,
loc="lower right", ncol=3, frameon=True)可以看出各种鸢尾花四个特征的分布情况,以setosa为例,发现其petal_width值集中分布在0.2左右2.5 swarmplot能够显示分布密度的分类散点图sns.set(style="whitegrid", palette="muted")
# 加载数据集
iris = sns.load_dataset("iris")
# 处理数据集
iris = pd.melt(iris, "species", var_name="measurement")
# 绘制分类散点图
sns.swarmplot(x="measurement", y="value", hue="species",
palette=["r", "c", "y"], data=iris)2.6 catplot(分类型图表的接口)可以通过指定kind参数分别绘制下列图形:stripplot() 分类散点图swarmplot() 能够显示分布密度的分类散点图boxplot() 箱图violinplot() 小提琴图boxenplot() 增强箱图pointplot() 点图barplot() 条形图countplot() 计数图3.分布图3.1 displot(单变量分布图)在seaborn中想要对单变量分布进行快速了解最方便的就是使用distplot()函数,默认情况下它将绘制一个直方图,并且可以同时画出核密度估计(KDE)图。具体用法如下:# 设置并排绘图,讲一个画布分为2*2,大小为7*7,X轴固定,通过ax参数指定绘图位置,可以看第六章具体怎么绘制多个图在一个画布中
f, axes = plt.subplots(2, 2, figsize=(7, 7), sharex=True)
sns.despine(left=True)
rs = np.random.RandomState(10)
# 生成随机数
d = rs.normal(size=100)
# 绘制简单的直方图,kde=False不绘制核密度估计图,下列其他图类似
sns.distplot(d, kde=False, color="b", ax=axes[0, 0])
# 绘制核密度估计图和地毯图
sns.distplot(d, hist=False, rug=True, color="r", ax=axes[0, 1])
# 绘制填充核密度估计图
sns.distplot(d, hist=False, color="g", kde_kws={"shade": True}, ax=axes[1, 0])
# 绘制直方图和核密度估计
sns.distplot(d, color="m", ax=axes[1, 1])
plt.setp(axes, yticks=[])
plt.tight_layout()3.2kdeplot(核密度估计图)核密度估计(kernel density estimation)是在统计学中用来估计未知分布的密度函数,属于非参数检验方法之一。通过核密度估计图可以比较直观的看出数据样本本身的分布特征。具体用法如下:简单的二维核密度估计图sns.set(style="dark")
rs = np.random.RandomState(50)
x, y = rs.randn(2, 50)
sns.kdeplot(x, y)
f.tight_layout()多个核密度估计图sns.set(style="darkgrid")
iris = sns.load_dataset("iris")
# 按物种对iris数据集进行子集划分
setosa = iris.query("species == 'setosa'")
virginica = iris.query("species == 'virginica'")
f, ax = plt.subplots(figsize=(8, 8))
ax.set_aspect("equal")
# 画两个密度图
ax = sns.kdeplot(setosa.sepal_width, setosa.sepal_length,
cmap="Reds", shade=True, shade_lowest=False)
ax = sns.kdeplot(virginica.sepal_width, virginica.sepal_length,
cmap="Blues", shade=True, shade_lowest=False)
# 将标签添加到绘图中
red = sns.color_palette("Reds")[-2]
blue = sns.color_palette("Blues")[-2]
ax.text(2.5, 8.2, "virginica", size=16, color=blue)
ax.text(3.8, 4.5, "setosa", size=16, color=red)3.3绘制山脊图rs = np.random.RandomState(1979)
x = rs.randn(500)
g = np.tile(list("ABCDEFGHIJ"), 50)
df = pd.DataFrame(dict(x=x, g=g))
m = df.g.map(ord)
df["x"] += m
# 初始化FacetGrid对象
pal = sns.cubehelix_palette(10, rot=-.25, light=.7)
g = sns.FacetGrid(df, row="g", hue="g", aspect=15, height=.5, palette=pal)
# 画出密度
g.map(sns.kdeplot, "x", clip_on=Fals
"?e, shade=True, alpha=1, lw=1.5, bw=.2)
g.map(sns.kdeplot, "x", clip_on=False, color="w", lw=2, bw=.2)
g.map(plt.axhline, y=0, lw=2, clip_on=False)
# 定义并使用一个简单的函数在坐标轴中标记绘图
def label(x, color, label):
ax = plt.gca()
ax.text(0, .2, label, fontweight="bold", color=color,
ha="left", va="center", transform=ax.transAxes)
g.map(label, "x")
# 将子地块设置为重叠
g.fig.subplots_adjust(hspace=-.25)
# 删除与重叠不协调的轴
g.set_titles("")
g.set(yticks=[])
g.despine(bottom=True, left=True)<seaborn.axisgrid.FacetGrid at 0x1d7da3567c0>3.4 joinplot(双变量关系分布图)用于绘制两个变量间分布图sns.set(style="white")
# 创建模拟数据集
rs = np.random.RandomState(5)
mean = [0, 0]
cov = [(1, .5), (.5, 1)]
x1, x2 = rs.multivariate_normal(mean, cov, 500).T
x1 = pd.Series(x1, name="$X_1$")
x2 = pd.Series(x2, name="$X_2$")
# 使用核密度估计显示联合分布
g = sns.jointplot(x1, x2, kind="kde", height=7, space=0)rs = np.random.RandomState(11)
x = rs.gamma(2, size=1000)
y = -.5 * x + rs.normal(size=1000)
sns.jointplot(x, y, kind="hex", color="#4CB391")tips = sns.load_dataset("tips")
g = sns.jointplot("total_bill", "tip", data=tips, kind="reg",
xlim=(0, 60), ylim=(0, 12), color="m", height=7)3.5 pairplot(变量关系图)变量关系组图,绘制各变量之间散点图df = sns.load_dataset("iris")
sns.pairplot(df)4. 回归图4.1 lmplotlmplot是用来绘制回归图的,通过lmplot我们可以直观地总览数据的内在关系,lmplot可以简单通过指定x,y,data绘制# 绘制整体数据的回归图
sns.lmplot(x='x',y='y',data=df)<seaborn.axisgrid.FacetGrid at 0x1d7cdfbec10># 使用分面绘图,根据dataset分面
sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None)上面显示了每一张图内画一个回归线,下面我们来看如何在一张图中画多个回归线# 加载鸢尾花数据集
iris = sns.load_dataset("iris")
g = sns.lmplot(x="sepal_length", y="sepal_width", hue="species",
truncate=True, height=5, data=iris)
# 使用truncate参数
# 设置坐标轴标签
g.set_axis_labels("Sepal length (mm)", "Sepal width (mm)")<seaborn.axisgrid.FacetGrid at 0x1d7d16cea60>可以看出setosa类型的鸢尾花主要集中在左侧,下面我们再来看一下怎么绘制logistic回归曲线# 加载 titanic dataset
df = sns.load_dataset("titanic")
# 显示不同性别年龄和是否存活的关系
g = sns.lmplot(x="age", y="survived", col="sex", hue="sex", data=df,
y_jitter=.02, logistic=True)
g.set(xlim=(0, 80), ylim=(-.05, 1.05))虽然仅仅使用一个变量来拟合logistic回归效果不好,但是为了方便演示,我们暂且这样做,从logistic回归曲线来看,男性随着年龄增长,存活率下降,而女性随着年龄上升,存活率上升4.2 residplot(残差图)线性回归残差图 绘制现象回归得到的残差回归图sns.set(style="whitegrid")
# 模拟y对x的回归数据集
rs = np.random.RandomState(7)
x = rs.normal(2, 1, 75)
y = 2 + 1.5 * x + rs.normal(0, 2, 75)
# 绘制残差数据集,并拟合曲线
sns.residplot(x, y, lowess=True, color="g")从结果来看,回归结果较好,这是因为我们的数据就是通过回归的形式生成的5.矩阵图5.1 heatmap(热力图)常见的我们使用热力图可以看数据表中多个变量间的相似度# 加载数据
flights_long = sns.load_dataset("flights")
# 绘制不同年份不同月份的乘客数量
flights = flights_long.pivot("month", "year", "passengers")
# 绘制热力图,并且在每个单元中添加一个数字
f, ax = plt.subplots(figsize=(9, 6))
sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax)绘制相关系数矩阵,绘制26个英文字母之间的相关系数矩阵from string import ascii_letters
sns.set(style="white")
# 随机数据集
rs = np.random.RandomState(33)
d = pd.DataFrame(data=rs.normal(size=(100, 26)),
columns=list(ascii_letters[26:]))
# 计算相关系数
corr = d.corr()
mask = np.zeros_like(corr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True
# 设置图形大小
f, ax = plt.subplots(figsize=(11, 9))
# 生成自定义颜色
cmap = sns.diverging_palette(220, 10, as_cmap=True)
# 绘制热力图
sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, center=0,
square=True, linewidths=.5, cbar_kws={"shrink": .5})🌐5.2 clustermap聚类图sns.set()
# 加载大脑网络示例数据集
df = sns.load_dataset("brain_networks", header=[0, 1, 2], index_col=0)
# 选择网络的子集
used_networks = [1, 5, 6, 7, 8, 12, 13, 17]
used_columns = (df.columns.get_level_values("network")
.astype(int)
.isin(used_networks))
df = df.loc[:, used_columns]
# 创建一个分类调色板来识别网络
network_pal = sns.husl_palette(8, s=.45)
network_lut = dict(zip(map(str, used_networks), network_pal))
# 将调色板转换为将在矩阵侧面绘制的向量
networks = df.columns.get_level_values("network")
network_colors = pd.Series(networks, index=df.columns).map(network_lut)
# 画出完整的聚类图
sns.clustermap(df.corr(), center=0, cmap="vlag",
row_colors=network_colors, col_colors=network_colors,
linewidths=.75, figsize=(13, 13))6.FacetGrid绘制多个图表是一个绘制多个图表(以网格形式显示)的接口。 步骤:1、实例化对象2、map,映射到具体的 seaborn 图表类型3、添加图例6.1 绘制多个直方图sns.set(style="darkgrid")
tips = sns.load_dataset("tips")
g = sns.FacetGrid(tips, row="sex", col="time", margin_titles=True)
bins = np.linspace(0, 60, 13)
g.map(plt.hist, "total_bill", color="steelblue", bins=bins)6.2 绘制多个折线图sns.set(style="ticks")
# 创建一个包含许多短随机游动的数据集
rs = np.random.RandomState(4)
pos = rs.randint(-1, 2, (20, 5)).cumsum(axis=1)
pos -= pos[:, 0, np.newaxis]
step = np.tile(range(5), 20)
walk = np.repeat(range(20), 5)
df = pd.DataFrame(np.c_[pos.flat, step, walk],
columns=["position", "step", "walk"])
# 为每一次行走初始化一个带有轴的网格
grid = sns.FacetGrid(df, col="walk", hue="walk", palette="tab20c",
col_wrap=4, height=1.5)
# 画一条水平线以显示起点
grid.map(plt.axhline, y=0, ls=":", c=".5")
# 画一个直线图来显示每个随机行走的轨迹
grid.map(plt.plot, "step", "position", marker="o")
# 调整刻度位置和标签
grid.set(xticks=np.arange(5), yticks=[-3, 3],
xlim=(-.5, 4.5), ylim=(-3.5, 3.5))
# 调整图形的布局
grid.fig.tight_layout(w_pad=1)
秋叶无缘
大数据闯关之MySQL基础篇(一):MySQL安装及基础介绍
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。一、数据库相关概念首先简单介绍一下数据库相关的概念:数据库(DataBase,DB):存储数据的仓库,数据是有组织的进行存储的。数据库管理系统(DataBase Management System,DBMS):操作和管理数据库的大型软件SQL(Structured Query Language):操作关系型数据库的编程语言,定义了一套操作关系型数据库统一标准二、MySQL安装及启动首先我们先下载MySQL,windows下载地址:dev.mysql.com/downloads/w…接下来根据提示进行安装即可,最后到了要输入MySQL的root用户的密码,这个得记住!然后即可使用MySQL三、MySQL基础3.1 数据模型MySQL客户端会给MySQL数据库服务器发送对应的SQL语句,数据库服务器会通过DBMS来维护数据库或者创建数据库,而数据库中又会存储各种表,数据又是存储在各种表中的。关系型数据库(RDBMS):建立在关系模型基础上,由多张相互连接的二维表组成的数据库。特点:使用表存储数据,格式统一,便于维护使用SQL语言操作,标准统一,使用方便3.2 通用语法及分类SQL通用语法 SQL语句可以单行或多行书写,以分号结尾SQL语句可以使用空格/缩进来增强语句的可读性MySQL数据库的SQL不区分大小写,关键字建议使用大写SQL分类DDL(Data Definition Language):数据定义语言,用来定义数据库对象(数据库,表,字段)DML(Data Manipulation Language):数据操作语言,用来对数据库表中的数据进行增删改DQL(Data Query Language):数据查询语言,用来查询数据库中表的记录DCL(Data Control Language):数据控制语言,用来创建数据库用户、控制数据库的访问权限
秋叶无缘
大数据闯关之MySQL基础篇(九):事务
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。以下为该部分的前置博客大数据闯关之MySQL基础篇(八):多表查询大数据闯关之MySQL基础篇(七):约束大数据闯关之MySQL基础篇(六):函数大数据闯关之MySQL基础篇(五):DCL用户管理大数据闯关之MySQL基础篇(四):DQL数据查询操作大数据闯关之MySQL基础篇(三):DML数据操作大数据闯关之MySQL基础篇(二):数据库操作大数据闯关之MySQL基础篇(一):MySQL安装及基础介绍一、事务简介事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。默认的,MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式地提交事务二、事务操作首先先准备数据,按以下语句创建表以及向表中插入数据create table account(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
money int comment '余额'
) comment '账户表';
insert into account(id, name, money) VALUES (null,'张三',2000),(null,'李四',2000);
首先有一个转账操作,有以下几个步骤:查询张三账户余额、将张三账户余额-1000,将李四余额+1000
select * from account where name = '张三'; -- 1. 查询张三账户余额
update account set money = money - 1000 where name = '张三'; -- 2. 将张三账户余额-1000
update account set money = money + 1000 where name = '李四'; -- 3. 将李四账户余额+1000这样就完成了张三向李四转账1000元的操作假设说向李四转账失败会发生什么,例如-- 转账操作 (张三给李四转账1000)
-- 1. 查询张三账户余额
select * from account where name = '张三';
-- 2. 将张三账户余额-1000
update account set money = money - 1000 where name = '张三';
程序异常
-- 3. 将李四账户余额+1000
update account set money = money + 1000 where name = '李四';会发现有一句: 程序异常,这句话并非sql语句或者注释,那么程序就会报错,以至于张三的钱会扣1000,而李四的钱不会变多,因此这1000块就不翼而飞了。因此我们要将这个转账操作写在一个事务里面。有以下两种方式可以控制事务事务操作方式一查看/设置事务提交方式,1为自动提交,0为手动提交SELECT @@autocommit;
SET @@autocommit-0;提交事务commit;回滚事务rollback;只要在sql语句前将事务提交方式设置为手动提交,在写完sql语句后执行commit即可事务操作方式二-- 转账操作 (张三给李四转账1000)
start transaction ; -- 开启事务
-- 1. 查询张三账户余额
select * from account where name = '张三';
-- 2. 将张三账户余额-1000
update account set money = money - 1000 where name = '张三';
程序执行报错 ...
-- 3. 将李四账户余额+1000
update account set money = money + 1000 where name = '李四';
-- 提交事务
commit;
-- 回滚事务
rollback;可以看到,这个程序一定会出错,那么我们就不需要commit了,只需要rollback即可。三、事务四大特性ACID原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败一致性(Consistency):事务完成时,必须使得所有的数据都保持一致状态隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的四、并发事务问题当A事务和B事务同时操作同一个表时会引发以下三个问题脏读:一个事务读到另外一个事务还没有提交的数据不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在五、事务隔离级别事务隔离级别的设置就是为了解决以上的并发事务问题的隔离级别脏读不可重复读幻读Read uncommitted√√√Read committed×√√Repeatable Read(默认)××√Serializable×××--查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION;
--设置事务隔离级别
SET [SESSION|GLOBAL] TAANSACTION ISOLATION LEVEL {Read uncommitted|Read committed|Repeatable Read|Serializable};基础篇到这里就结束了,下一篇章将进军Hadoop!
秋叶无缘
大数据闯关之MySQL基础篇(八):多表查询
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。以下为该部分的前置博客大数据闯关之MySQL基础篇(七):约束大数据闯关之MySQL基础篇(六):函数大数据闯关之MySQL基础篇(五):DCL用户管理大数据闯关之MySQL基础篇(四):DQL数据查询操作大数据闯关之MySQL基础篇(三):DML数据操作大数据闯关之MySQL基础篇(二):数据库操作大数据闯关之MySQL基础篇(一):MySQL安装及基础介绍一、概述在项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在各种联系,基本上分为三种一对多(多对一):在多的一方建立外键,指向一的一方的主键多对多:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键我们建立以下几张表来创建学生表、课程表以及学生表和课程表的中间表create table student(
id int auto_increment primary key comment '主键TO',
name varchar(10) comment '姓名',
no varchar(10) comment '学号'
) comment '学生表';
insert into student values (null,'黛纳丝','2000100101'),(null,'谢逊','2000100102'),(null,'殷天正','2000100103'),(null,'韦一笑','2000100104');create table course(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '课程名称'
) comment '课程表';
insert into course value (null, 'java'), (null, 'PHP'), (null, 'MySQL'), (null, 'Hadoop');
create table student_course(
id int auto_increment comment '主键' primary key,
studentid int not null comment '学生ID',
courseid int not null comment '课程ID',
constraint fk_courseid foreign key (courseid) references course (id),
constraint fk_studentid foreign key (studentid) references student (id)
)comment '学生课程中间表';
insert into student_course values (null, 1, 1), (null, 1, 2),(null, 1, 3),(null, 2, 2),(null, 2, 3),(null, 3, 4);一对一:多用于单表拆分,在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)概述:指从多张表中查询数据笛卡尔积:笛卡尔乘积是指在数学中,两个集合的所有组合情况(在多表查询时,需要消除无效的笛卡尔积)多表查询分类连接查询 内连接:相当于查询A、B交集部分数据外连接: 左外连接:查询左表所有数据,以及两张表交集部分数据右外连接:查询右表所有数据,以及两张表交集部分数据自连接:当前表与自身的连接查询,子连接必须使用表别名子查询二、内连接内连接查询的是两张表交集的部分隐式内连接SELECT 字段列表 FROM 表1,表2 WHERE 条件;显式内连接SELECT 字段列表 FROM 表1 INNER JOIN 表2 ON 条件;我们试一下查询每一个员工以及其关联部门的名称和其他信息,用显式内连接select * from emp inner join dept on emp.dept_id = dept.id;三、外连接左外连接SELECT 字段列表 FROM 表1 LEFT [OUTER] JOIN 表2 ON 条件...;相当于查询表1(左表)的所有数据,包含表1和表2交集部分的数据右外连接SELECT 字段列表 FROM 表1 RIGHT [OUTER] JOIN 表2 ON 条件...;相当于查询表2(右表)的所有数据,包含表1和表2交集部分的数据四、自连接自连接语法SELECT 字段列表 FROM 表A 别名A JOIN 表A 别名B ON 条件...;五、联合查询unionunion/union allSELECT 字段列表 FROM 表A
UNION
SELECT 字段列表 FROM 表B; 对于联合查询的多张表的列数必须保持一致,字段类型也需要保持一致六、子查询介绍:SQL语句中嵌套SELECT语句,称为嵌套查询,又称子查询SELECT * FROM t1 WHERE column1=(SELECT column1 FROM t2) ;根据子查询结果不同,分为标量子查询(子查询结果为单个值)列子查询(子查询结果为一列)行子查询(子查询结果为一行)表子查询(子查询结果为多行多列)标量子查询:子查询返回的结果是单个值(数字、字符串、日期等)常用的操作符:=、>、<、>=、<=列子查询:子查询返回的结果是一列(可以是多行),这种子查询称为列子查询常用的操作符:IN、NOT IN、 ANY、SOME、ALL行子查询:行子查询返回的结果是一行(可以是多列),这种子查询称为行子查询常用的操作符:=、<>、IN、NOT IN表子查询:表子查询返回的结果是多行多列,这种子查询称为表子查询常用的操作符:IN
秋叶无缘
大数据闯关之MySQL进阶篇(五):锁
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。这部分接着之前的MySQL基础篇,接下来将对MySQL进阶部分进行学习。以下为该部分的前置博客大数据闯关之MySQL进阶篇(一):存储引擎大数据闯关之MySQL进阶篇(二):索引大数据闯关之MySQL进阶篇(三):SQL优化大数据闯关之MySQL进阶篇(四):视图-存储过程-触发器一、全局锁锁:锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除了传统的计算资源的争用外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。按照锁的粒度分,分为以下三类全局锁:锁定数据库中的所有表表级锁:每次操作锁住整张表行级锁:每次操作锁住对应的行数据全局锁是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,以及更新操作的事务提交语句都将被阻塞语法flush tables with read lock; -- 施加全局锁
mysqldump uroot -p1234 itcast>itcast.sql -- 数据备份
unlock tables; -- 解锁特点:数据库中加全局锁,是一个比较重的操作,存在以下问题如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志,会导致主从延迟二、表级锁表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低表锁表共享读锁(read lock):只能读不能写,且不会阻塞其他客户端的读操作,但会阻塞其他客户端的写操作表独占写锁(write lock):对于当前客户端,既能读也能写,对于其他客户端,既不能读也不能写。语法: 加锁:lock tables 表名 read/write释放锁:unlock tables元数据锁(meta data lock,MDL):MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用就是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。以下标为为MDL类型及其对应SQL语句对应SQL锁类型说明lock tables xxx read/writeSHARED_READ_ONLY/SHARED_NO_READ_WRITEselect、select ... lock in share modeSHARED_READ与SHARED_READ、SHARED_READ兼容,与EXLUSIVE互斥insert、update、delete、select ... for updateSHARED_READ与SHARED_READ、SHARED_READ兼容,与EXLUSIVE互斥alter tableEXLUSIVE与其他的MDL互斥同样可以查看元数据锁· select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks;意向锁:为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。意向共享锁:由语句select ... lock in share mode添加。与表锁共享锁兼容,与表锁排他锁互斥意向排他锁:由insert、update、delete、select ... for update添加。与表锁共享锁及排他锁互斥,意向锁之间不会互斥三、行级锁行级锁:每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,对于行级锁,主要分为以下三类行锁:锁定单个行记录的锁,防止其他事务对此行进行update和delete操作共享锁:允许一个事务去读一行,组织其他事务获得相同数据集的排他锁排他锁:允许获取排他锁的事务更新数据,组织其他事务获得相同数据集的共享锁和排他锁SQL行锁类型说明insert排他锁自动加锁update排他锁自动加锁delete排他锁自动加锁select不加任何锁select ... lock in share mode共享锁需要手动在select后面加lock in share modeselect ... for update排他锁需要手动在select后面加for update间隙锁:锁定索引记录间隙,确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁临键锁:行锁和间隙锁结合,同时锁住数据,并锁住数据前面的间隙Gap
秋叶无缘
大数据闯关之Hadoop篇(二):HDFS
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。之前我们过完了MySQL的基础部分和进阶部分,我们现在来到了大数据工具的Hadoop。由于最近有点忙,好久没更新了555。这段时间尽量两到三天更新一篇。以下为该部分的前置博客大数据闯关之Hadoop篇(一):大数据概述一、Hadoop集群启动format(格式化操作):首次启动HDFS时,必须对其进行格式化操作,format本质上是初始化工作,进行HDFS清理和准备工作hdfs namenode -formatHadoop集群启停命令:shell脚本一键启停,在node1上,使用软件自带的shell脚本一键启动HDFS集群:start-dfs.sh stop-dfs.shYARN集群:start-yarn.sh stop-yarn.shHadoop集群:start-all.sh stop-all.sh我们可以启动一下Hadoop集群,并且输入jps看一下进程是否正常开启 为了保证三台机器正常启动,node1、node2和node3的进程在启动后如下 Hadoop启动日志路径:/export/server/hadoop-3.3.0/logsshell命令操作hadoop fs -mkdir /itcast:在hadoop中创建文件夹itcasthadoop fs -put zookeeper.out /itcast:上传文件hadoop fs -ls执行Hadoop官方自带的Map Reduce案例,计算圆周率Π的值cd /export/server/hadoop-3.3.0/share/hadoop/mapreduce/
hadoop jar hadoop-mapreduce-examples-3.3.0.jar pi 2 2可以看到得到了一个结果,由于给的数字太小,计算不准确,但是能够成功运行了!二、HDFS工作流程HDFS分布式文件系统基础文件系统是一种存储和组织数据的方法,实现了数据的存储、分级组织、访问和获取等操作,使得用户对文件访问和查找变得容易数据:指存储的内容本身,这些数据底层最终是存储在磁盘等存储介质上的元数据(记录数据的数据):一般指文件大小、最后修改时间、底层存储位置、属性、所属用户、权限等信息分布式存储系统核心属性分布式存储:解决数据量大、单机存储遇到瓶颈的问题元数据记录:解决文件分布在不同机器上不利于寻找的问题分块存储:解决文件过大导致单机存不下,上传下载效率低的问题副本:解决硬件故障数据丢失的问题HDFS:HDFS主要是解决大数据如何存储问题的。分布式意味着HDFS是横跨在多台计算机上的存储系统HDFS是一种能够在普通硬件上运行的分布式文件系统,它是高度容错的,适应于具有大数据的应用程序,它非常适合存储大型数据HDFS使用多台计算机存储文件,并且提供统一的访问接口。HDFS设计目标:HDFS可能有成百上千的服务器组成,每一个组件都有可能出现故障。因此故障检测和自动快速回复是HDFS的核心架构目标HDFS上的应用主要是以流式读取数据。相较于数据访问的反应时间,更注重数据访问的高吞吐量HDFS重要特性错误!未指定文件名。主从架构 HDFS集群是标准的master/slave主从架构集群一般一个HDFS集群是有一个Namenode和一定数目的Datanode组成Namenode是HDFS主节点,Datanode是HDFS从节点,两种角色各司其职,共同协调完成分布式的文件存储服务分块存储 HDFS中的文件在物理上是分块存储的,默认大小是128M副本机制 文件的所有block都有副本副本数默认是3,连同本身总共3份副本元数据记录:记录数据的数据 文件自身属性信息:文件名称、权限、修改时间等文件块位置映射信息:记录文件块和DataNode之间的映射信息抽象统一的目录树结构数据块存储 文件的各个block的具体存储管理由DataNode节点承担每个block都可以在多个DataNode上存储各角色职责介绍Namenode:HDFS分布式文件系统的核心,架构中的主角色。维护和管理文件系统元数据,包括名称空间目录树结构、文件和块的位置信息、访问权限等信息SecondaryNamenode:充当Namenode的辅助节点,主要是帮助主角色进行元数据文件的合并动作Datanode:HDFS的从角色,负责具体的数据块存储写数据流程* pipeline管道:HDFS上传文件写数据过程中采用的一种数据传输方式。客户端将数据块写入第一个数据节点,第一个数据节点保存数据之后再将块复制到第二个数据节点,再复制到第三个数据节点(如图4.2和4.3流程所示)。该方式可以充分利用每个机器的带宽,避免网络瓶颈和高延迟时的连接,最小化推送所有数据的延时
* ACK应答响应:ACK即确认字符,在数据通信中,接收方发给发送方的一种传输类控制字符,表示发来的数据已确认接收无误。在pipeline管道传输数据的过程中,传输的反方向会进行ACK校验(如图5.1和5.2所示)
* 默认3副本存储策略:
* 第一个副本:优先客户端本地,否则随机
* 第二块副本:不同于第一块副本的不同机架
* 第三块副本:第二块副本相同机架不同机器写数据整体流程HDFS客户端创建对象实例,该对象中封装了与HDFS文件系统操作的相关方法调用对象的create方法,通过RPC请求NameNode创建文件。NameNode执行各种检查判断,检查通过,NameNode就会为本次请求记下一条记录,返回输出流对象给客户端用于写数据客户端通过输出流开始写入数据客户端写入数据时,将数据分为一个个数据包,内部组件请求NameNode挑选出适合存储数据副本的一组DataNode地址。然后通过pipeline管道传输数据在传输的反方向上,通过ACK机制校验数据包传输是否成功客户端完成数据写入后,在输出流上调用close方法关闭向NameNode告知其文件写入文件。因为NameNode已经知道文件由哪些块组成,因此仅需等待最小复制块即可成功返回三、HDFS的shell操作文件系统协议HDFS Shell CLI支持操作多种文件系统,包括本地文件系统、分布式文件系统等具体操作的是什么文件系统取决于命令中文件路径URL中的前缀协议如果没有指定前缀,则会读取环境变量中的fs.defaultFS属性,以该属性值作为默认文件系统hadoop fs -ls file:/// # 操作本地文件系统
hadoop fs -ls hdfs://node1:8020/ # 操作HDFS分布式文件系统
hadoop fs -ls / # 直接根目录,没有指定协议创建文件夹hadoop fs -mkdir [-p] <path> # -p会沿着路径创建父路径查看指定目录下的内容hadoop fs -ls [-h] [-R] [<path>] # [-h] 人性化显示文件size [-R] 递归查看上传文件到HDFS指定目录下hadoop fs -put [-f] [-p] <localsrc> ... <dst>
# -f 覆盖目标文件 -p 保留访问和修改时间,所有权和权限
# localsrc 本地文件系统,dst 目标文件系统查看HDFS文件内容hadoop fs -cat <src>下载HDFS文件hadoop fs -get [-f] [-p] <src> ... <localdst>拷贝HDFS文件hadoop fs -cp [-f] <src> ... <localdst>追加数据到HDFS文件中hadoop fs -appendToFile <localsrc> ... <dst>HDFS数据移动操作hadoop fs -mv <src> ... <dst>
秋叶无缘
python数据分析从入门到进阶:分类算法实现:上(含详细代码)
《python机器学习从入门到高级》分类算法:(上)引言我们在之前的文章已经介绍了机器学习的一些基础概念,当拿到一个数据之后如何处理、如何评估一个模型、以及如何对模型调参等。接下来,我们正式开始学习如何实现机器学习的一些算法。 回归和分类是机器学习的两大最基本的问题,对于分类算法的详细理论部分。 本文主要从python代码的角度来实现分类算法。# 导入相关库
import sklearn
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt1. 数据准备下面我们以mnist数据集为例进行演示,这是一组由美国人口普查局的高中生和雇员手写的70000个数字图像。每个图像都用数字表示。也是分类问题非常经典的一个数据集# 导入mnist数据集
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
mnist.keys()dict_keys(['data', 'target', 'frame', 'categories', 'feature_names', 'target_names', 'DESCR', 'details', 'url'])其中data是我们输入的特征,target是0-9的数字X, y = mnist["data"], mnist["target"]
X.shape,y.shape((70000, 784), (70000,))可以看出一共有70000图像,其中X一共有784个特征,这是因为图像是28×28的,每个特征是0-255之间的。下面我们通过imshow()函数将其进行还原%matplotlib inline
import matplotlib as mpl
digit = X[0]
digit_image = digit.reshape((28, 28))#还原成28×28
plt.imshow(digit_image, cmap=mpl.cm.binary)
plt.axis("off")
plt.savefig("some_digit_plot")
plt.show()从我们人类角度来看,我们很容易辨别它是5,我们要做的是,当给机器一张图片时,它能辨别出正确的数字吗?我们来看看y的值y[0]'5'我们要实现的就是,给我们一张图片,不难发现这是一个==多分类任务==,下面我们正式进入模型建立,首先将数据集划分为==训练集和测试集==,这里简单的将前60000个划分为训练集,后10000个为测试集,具体代码如下y = y.astype(np.uint8)#将y转换成整数
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]2.简单二元分类实现在实现多分类任务之前,我们先从一个简单的问题考虑,现在假设我只想知道给我一张图片,它是否是7(我最喜欢的数字)。这个时候就是一个简单的二分类问题,首先我们要将我们的目标变量进行转变,具体代码如下y_train_7 = (y_train == 7)
y_test_7 = (y_test == 7)现在,我们选择一个分类器并对其进行训练。我们先使用==SGD==(随机梯度下降)分类器from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=123)#设置random_state为了结果的重复性
sgd_clf.fit(X_train, y_train_7)SGDClassifier(random_state=123)训练好模型之后我们可以进行预测,以第一张图片为例,我们预测一下它是否是7(很显然我们知道不是)sgd_clf.predict(X[0].reshape((1,-1)))array([False])可以看出判断正确了,在之前我们讨论了==模型评估==的方法,详细介绍看这篇文章:Python机器学习从入门到高级:模型评估和选择(含详细代码) 下面演示如何用代码实现各个评估指标3.模型评估我们根据分类评估指标来看看SGD分类器效果3.1 准确率from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_7, cv=3, scoring="accuracy")array([0.97565, 0.97655, 0.963 ])3.2 混淆矩阵y_train_pred = sgd_clf.predict(X_train)from sklearn.metrics import confusion_matrix
confusion_matrix(y_train_7, y_train_pred)array([[53304, 431],
[ 550, 5715]], dtype=int64)3.3 召回率和精确度from sklearn.metrics import precision_score, recall_score
print('precision:',precision_score(y_train_7, y_train_pred))
print('recall:',recall_score(y_train_7,y_train_pred))precision: 0.929873088187439
recall: 0.9122106943335994下面要用的matplotlib,想了解matplotlib可以看这篇文章:Python数据可视化大杀器之地阶技法:matplotlib(含详细代码)3.4 ROC曲线from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_train_7, y_scores)
plt.plot(fpr, tpr, linewidth=2)
plt.plot([0, 1], [0, 1], 'k--')
plt.axis([0, 1, 0, 1])
plt.xlabel('False Positive Rate (Fall-Out)', fontsize=16)
plt.ylabel('True Positive Rate (Recall)', fontsize=16)
plt.grid(True) 本章的介绍到此介绍,下一章介绍分类算法(下):如何完成多分类任务
秋叶无缘
Python数据分析入门到进阶:数据清洗(含详细代码)
在上一篇文章中,介绍了如何使用python导入数据,导入数据后的第二步往往就是数据清洗,下面我们来看看如何使用pandas进行数据清洗工作导入相关库import pandas as pd
dataframe = pd.read_csv(r'C:/Users/DELL/data-science-learning/python数据分析笔记/探索性数据分析/train.csv')
dataframe.head(5)PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarked0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNS1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85C2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNS3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123S4503Allen, Mr. William Henrymale35.0003734508.0500NaNS1.总览数据查看数据维度dataframe.shape(891, 12)描述性统计分析dataframe.describe().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassAgeSibSpParchFarecount891.000000891.000000891.000000714.000000891.000000891.000000891.000000mean446.0000000.3838382.30864229.6991180.5230080.38159432.204208std257.3538420.4865920.83607114.5264971.1027430.80605749.693429min1.0000000.0000001.0000000.4200000.0000000.0000000.00000025%223.5000000.0000002.00000020.1250000.0000000.0000007.91040050%446.0000000.0000003.00000028.0000000.0000000.00000014.45420075%668.5000001.0000003.00000038.0000001.0000000.00000031.000000max891.0000001.0000003.00000080.0000008.0000006.000000512.3292002.筛选数据过滤所有女性和年龄大于60岁的乘客dataframe[(dataframe['Sex'] == 'female') & (dataframe['Age']>=60)].dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarked27527611Andrews, Miss. Kornelia Theodosiafemale63.0101350277.9583D7S36636711Warren, Mrs. Frank Manley (Anna Sophia Atkinson)female60.01011081375.2500D37C48348413Turkula, Mrs. (Hedwig)female63.00041349.5875NaNS82983011Stone, Mrs. George Nelson (Martha Evelyn)female62.00011357280.0000B28NaN可以看出,一共有四名年龄大于60岁的女性乘客3.替换数据将female换成woman,将male换成mandataframe['Sex'].replace(['female','male'],['woman','man']).head(5)0 man
1 woman
2 woman
3 woman
4 man
Name: Sex, dtype: object4.更改列名查看所有列名dataframe.columnsIndex(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'],
dtype='object')重命名列PassengerIdSurvivedPassenger ClassNameSexAgeSibSpParchTicketFareCabinEmbarked0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNS1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85C2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNS3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123S4503Allen, Mr. William Henrymale35.0003734508.0500NaNSdataframe.rename(columns={'Pclass':'Passenger Class','Sex':'Gender'}).head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPassenger ClassNameGenderAgeSibSpParchTicketFareCabinEmbarked0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNS1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85C2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNS3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123S4503Allen, Mr. William Henrymale35.0003734508.0500NaNS5.查找唯一值在pandas中,我们可以使用unique()查找唯一值# 查找唯一值
dataframe['Sex'].unique()array(['male', 'female'], dtype=object)# 显示唯一值出现的个数
dataframe['Sex'].value_counts()male 577
female 314
Name: Sex, dtype: int64# 查找类型票的数量
dataframe['Pclass'].value_counts()3 491
1 216
2 184
Name: Pclass, dtype: int64# 查找唯一值的种类
dataframe['Pclass'].nunique()36.查找缺失值# 查找空数据dataframe[dataframe['Age'].isnull()].head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarked5603Moran, Mr. JamesmaleNaN003308778.4583NaNQ171812Williams, Mr. Charles EugenemaleNaN0024437313.0000NaNS192013Masselmani, Mrs. FatimafemaleNaN0026497.2250NaNC262703Emir, Mr. Farred ChehabmaleNaN0026317.2250NaNC282913O'Dwyer, Miss. Ellen "Nellie"femaleNaN003309597.8792NaNQpandas没有NaN 如果想要处理的话必须导入numpy包import numpy as np
dataframe['Sex'].replace('male',np.nan).head()0 NaN
1 female
2 female
3 female
4 NaN
Name: Sex, dtype: object7.删除列或行# 删除一列,采用drop方法,并传入参数axis
dataframe.drop('Age',axis=1).head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexSibSpParchTicketFareCabinEmbarked0103Braund, Mr. Owen Harrismale10A/5 211717.2500NaNS1211Cumings, Mrs. John Bradley (Florence Briggs Th...female10PC 1759971.2833C85C2313Heikkinen, Miss. Lainafemale00STON/O2. 31012827.9250NaNS3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female1011380353.1000C123S4503Allen, Mr. William Henrymale003734508.0500NaNS#删除行
dataframe.drop(1)# 删除重复行 使用subset参数指明要删除的列
dataframe.drop_duplicates(subset='Sex').head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } NamePClassAgeSexSurvivedSexCode0Allen, Miss Elisabeth Walton1st29.0female112Allison, Mr Hudson Joshua Creighton1st30.0male008. groupby分组计算男性和女性的平均值==思路一==,将所有男性和女性的条件进行选取分别计算man = dataframe[dataframe['Sex']=='male']
woman = dataframe[dataframe['Sex']=='female']
print(man.mean())
print(woman.mean())Age 31.014338
Survived 0.166863
SexCode 0.000000
dtype: float64
Age 29.396424
Survived 0.666667
SexCode 1.000000
dtype: float64==思路二==,用groupby方法简化dataframe.groupby('Sex').mean().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } AgeSurvivedSexCodeSexfemale29.3964240.6666671.0male31.0143380.1668630.0# 按行分组,计算行数
dataframe.groupby('Sex')['Name'].count()Sex
female 462
male 851
Name: Name, dtype: int64dataframe.groupby(['Sex','Survived']).mean().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdPclassAgeSibSpParchFareSexSurvivedfemale0434.8518522.85185225.0468751.2098771.03703723.0243851429.6995711.91845528.8477160.5150210.51502151.938573male0449.1217952.47649631.6180560.4401710.20726521.9609931475.7247712.01834927.2760220.3853210.35779840.8214849.按照时间段来进行分组使用resample参数来进行取样本# 创建时期范围
time_index = pd.date_range('06/06/2017', periods=100000, freq='30S') # periods表示有多少数据,freq表示步长dataframe = pd.DataFrame(index=time_index)# 创建一个随机变量
dataframe['Sale_Amout'] = np.random.randint(1, 10, 100000)# resample 参数,按周对行分组,计算每一周的总和
dataframe.resample('W').sum().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } Sale_Amout2017-06-11862922017-06-181003592017-06-251009072017-07-021008682017-07-091005222017-07-1610478# 使用resample可以按一组时间间隔来进行分组,然后计算每一个时间组的某个统计量
dataframe.resample('2W').mean().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } Sale_Amout2017-06-114.9937502017-06-254.9917162017-07-094.9947922017-07-235.037500dataframe.resample('M').count().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } Sale_Amout2017-06-30720002017-07-3128000# resample默认是以最后一个数据作 使用label参数可以进行调整
dataframe.resample('M', label='left').count().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } Sale_Amout2017-05-31720002017-06-302800010.遍历一个列的数据dataframe = pd.read_csv(url)# 以大写的形势打印前两行的名字
for name in dataframe['Name'][0:2]:
print(name.upper())ALLEN, MISS ELISABETH WALTON
ALLISON, MISS HELEN LORAINE11.对一列的所有元素应用某个函数def uppercase(x):
return x.upper()dataframe['Name'].apply(uppercase)[0:2]0 ALLEN, MISS ELISABETH WALTON
1 ALLISON, MISS HELEN LORAINE
Name: Name, dtype: object12. pandas高级函数dataframe.groupby('Sex').apply(lambda x:x.count()).dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } NamePClassAgeSexSurvivedSexCodeSexfemale462462288462462462male851851468851851851通过联合使用groupby 和apply,我们就能计算自定义的统计量 例如上面我们发现age、cabin具有大量的缺失值13. 连接多个Dataframedata_a = {'id':['1', '2', '3'],
'first': ['Alex', 'Amy', 'Allen'],
'last': ['Anderson', 'Ackerman', 'Ali']}
dataframe_a = pd.DataFrame(data_a, columns=['id','first', 'last'])data_b = {'id':['4', '5', '6'],
'first': ['Billy', 'Brian', 'Bran'],
'last': ['Bonder', 'Black', 'Balwner']}
dataframe_b = pd.DataFrame(data_b, columns=['id','first', 'last'])pd.concat([dataframe_a, dataframe_b], axis=0)#在行的方向进行.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } idfirstlast01AlexAnderson12AmyAckerman23AllenAli04BillyBonder15BrianBlack26BranBalwnerpd.concat([dataframe_a, dataframe_b], axis=1)#在列的方向进行.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } idfirstlastidfirstlast01AlexAnderson4BillyBonder12AmyAckerman5BrianBlack23AllenAli6BranBalwner# 也可以用append方法进行添加
c = pd.Series([10, 'Chris', 'Chillon'], index=['id','first','last'])dataframe.append(c, ignore_index=True)#如果c原来有名字忽略.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } idfirstlast01AlexAnderson12AmyAckerman23AllenAli310ChrisChillon
秋叶无缘
python数据分析从入门到进阶:线性回归和正则化
引言上一章中我们以minist数据集演示了如何实现一个分类算法,并进行选择。这一章中,我们将介绍广义线性模型。广义线性模型是机器学习的基础部分,也是很多其他算法的延伸。本文主要介绍一下如何实现简单的线性回归和正则化方法1.线性回归通常,线性回归通过简单计算输入特征的加权和,加上一个称为偏差项(也称为截距项)的常数来进行预测。具体公式如下:接下来我们看看具体如何实现线性回归模型import numpy as np
import matplotlib.pyplot as plt# 生成模拟数据集
X = np.random.randn(100,1)
y = 3*X+4+np.random.randn(100,1)观察一下生成的数据集plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([0, 2, 0, 15])
plt.savefig("generated_data_plot")
plt.show()可以看出数据呈现比较明显的线性趋势,下面使用LinearRegression实现对线性模型的训练from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
a = lin_reg.intercept_
b = lin_reg.coef_
a,b#回归系数和截距(array([3.86880209]), array([[3.0229374]]))下面根据训练好的线性回归模型进行预测,X_new = np.array([[0], [2]])
y_predict = lin_reg.predict(X_new)
y_predictarray([[3.86880209],
[9.91467688]])将预测结果可视化如下plt.plot(X_new, y_predict, "r-", linewidth=2, label="Predictions")
plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.legend(loc="upper left", fontsize=14)
plt.axis([0, 2, 0, 15])
plt.show()可以看出拟合结果还不错,下面我们绘制一下Learning curve:随着模型训练样本的变化观察训练集和测试集的误差from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
'''
定义learning_curves函数
'''
def plot_learning_curves(model, X, y):
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=10)#将数据分为训练集合验证集
train_errors, val_errors = [], []#定义训练误差和验证误差
for m in range(1, len(X_train) + 1):
model.fit(X_train[:m], y_train[:m])#根据训练集大小训练模型
y_train_predict = model.predict(X_train[:m])#拟合训练集
y_val_predict = model.predict(X_val)#拟合验证集
train_errors.append(mean_squared_error(y_train[:m], y_train_predict))#计算训练集MSE
val_errors.append(mean_squared_error(y_val, y_val_predict))#计算测试集MSE
plt.plot(np.sqrt(train_errors), "r-", linewidth=2, label="train")#绘制训练集RMSE
plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="val")#绘制测试集RMSE
plt.legend(loc="upper right", fontsize=14) #增加图例
plt.xlabel("Training set size", fontsize=14)
plt.ylabel("RMSE", fontsize=14) lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
plt.axis([0, 80, 0, 3])
plt.show() 从Learning Curve看出,随着样本增加val不断下降,对于该数据集而言,当训练集为80时,训练集和测试集误差基本相等。2.正则化过拟合是机器学习中一个非常重要的问题,减少过拟合有很多方法,例如正则化、增加数据或early stopping等对于线性模型而言,正则化通常通过约束模型系数来实现。接下来主要介绍岭回归、Lasso回归、early stopping2.1岭回归岭回归相对于最小二乘法的优势来源于能通过λ来权衡偏差方差。随着λ 的增加,岭回归拟合的灵活性降低,导致方差减小,但偏差增大。岭回归和最小二乘法估计非常类似,但是最小化的损失函数不同,这里是让下述损失函数最小:因此如何选择λ是一个需要思考的问题,下面我们看看如何实现岭回归#生成数据集
np.random.seed(42)#设置随机种子,保障代码重用性
m = 20
X = 3 * np.random.rand(m, 1)
y = 1 + 0.5 * X + np.random.randn(m, 1) / 1.5
X_new = np.linspace(0, 3, 100).reshape(100, 1)#训练模型
from sklearn.linear_model import Ridge
ridge_reg = Ridge(alpha=1, random_state=42)
ridge_reg.fit(X, y)
ridge_reg.get_params(){'alpha': 1,
'copy_X': True,
'fit_intercept': True,
'max_iter': None,
'normalize': False,
'random_state': 42,
'solver': 'auto',
'tol': 0.001}上述是我们去alphl=1的情况下的拟合值,但是alpha是一个超参数,我们有时候需要对进行交叉验证等方法来进行选择,下面我们来看一下alphl分别为1,10,100的拟合结果图:
for alpha, style in zip(alphas, ("b-", "g--", "r:")):
model = model_class(alpha, **model_kargs) if alpha > 0 else LinearRegression()
model.fit(X, y)
y_new_regul = model.predict(X_new)
lw = 2 if alpha > 0 else 1
plt.plot(X_new, y_new_regul, style, linewidth=lw, label=r"$\alpha = {}$".format(alpha))
plt.plot(X, y, "b.", linewidth=3)
plt.legend(loc="upper left", fontsize=15)
plt.xlabel("$x_1$", fontsize=18)
plt.axis([0, 3, 0, 4])
plt.figure(figsize=(8,4))
plot_model(Ridge, alphas=(0, 10, 100), random_state=42)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.savefig("ridge_regression_plot")
plt.show()上图是在α取不同值的拟合结果,可以发现,当α越大,回归曲线越平缓,因为对这些系数的约束越大2.2 Lasso回归岭回归确实有一个明显的缺点。岭回归将在最终模型中包含所有预测变量。惩罚项只会让所有的回归系数接近于0,但是不会等于0(除非λ=∞)。这对预测准确性来说可能没有影响,但在变量数量相当大的情况下,模型的可解释性有待考量。然后Lasso回归相对而言可以解决这个问题。与岭回归不同的是,Lasso回归可以选择最重要的几个特征拟合模型,可以让某些不重要变量的系数等于零,从而达到降纬的效果,基本公式如下:from sklearn.linear_model import Lasso
plt.figure(figsize=(8,4))
plot_model(Lasso, alphas=(0, 0.1, 1), random_state=42)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.savefig("lasso_regression_plot")
plt.show()2.3 early stopping正则化迭代学习算法(如梯度下降)的另一种非常不同的方法是,一旦验证误差达到最小值,就停止训练,被称为early stopping该方法通过批量梯度下降进行训练。随着时间的推移,算法学习,训练集上的预测误差(RMSE)和验证集上的预测误差都会下降。但过了一段时间后,验证误差又会上升(导致了过拟合)。这个时候我们可以做的是提取结束训练,当模型不会达到过拟合的程度。也被称为“免费的午餐”,下面我们来看看具体是如何实现的# 生成数据集
np.random.seed(42)
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 2 + X + 0.5 * X**2 + np.random.randn(m, 1)
X_train, X_val, y_train, y_val = train_test_split(X[:50], y[:50].ravel(), test_size=0.5, random_state=10)from copy import deepcopy
from sklearn.preprocessing import PolynomialFeatures
poly_scaler = Pipeline([
("poly_features", PolynomialFeatures(degree=90, include_bias=False)),#生成多项式回归
("std_scaler", StandardScaler())
])
X_train_poly_scaled = poly_scaler.fit_transform(X_train)#标准化处理
X_val_poly_scaled = poly_scaler.transform(X_val)#标准化
sgd_reg = SGDRegressor(max_iter=1, tol=-np.infty, warm_start=True,
penalty=None, learning_rate="constant", eta0=0.0005, random_state=42)#拟合SGD回归
minimum_val_error = float("inf")
best_epoch = None#定义最优的迭代次数
best_model = None#定义最优的模型
for epoch in range(1000):
sgd_reg.fit(X_train_poly_scaled, y_train) # 拟合模型
y_val_predict = sgd_reg.predict(X_val_poly_scaled)#预测
val_error = mean_squared_error(y_val, y_val_predict)#计算MSE
if val_error < minimum_val_error:
minimum_val_error = val_error
best_epoch = epoch
best_model = deepcopy(sgd_reg)
best_epoch,best_model(239,
SGDRegressor(eta0=0.0005, learning_rate='constant', max_iter=1, penalty=None,
random_state=42, tol=-inf, warm_start=True))可以看出第239次迭代时,拟合效果最好,下面进行可视化sgd_reg = SGDRegressor(max_iter=1, tol=-np.infty, warm_start=True,
penalty=None, learning_rate="constant", eta0=0.0005, random_state=42)
n_epochs = 500
train_errors, val_errors = [], []
for epoch in range(n_epochs):
sgd_reg.fit(X_train_poly_scaled, y_train)
y_train_predict = sgd_reg.predict(X_train_poly_scaled)
y_val_predict = sgd_reg.predict(X_val_poly_scaled)
train_errors.append(mean_squared_error(y_train, y_train_predict))
val_errors.append(mean_squared_error(y_val, y_val_predict))
best_epoch = np.argmin(val_errors)
best_val_rmse = np.sqrt(val_errors[best_epoch])
plt.annotate('Best model',
xy=(best_epoch, best_val_rmse),
xytext=(best_epoch, best_val_rmse + 1),
ha="center",
arrowprops=dict(facecolor='black', shrink=0.05),
fontsize=16,
)
plt.plot([0, n_epochs], [best_val_rmse, best_val_rmse], "k:", linewidth=2)
plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="Validation set")
plt.plot(np.sqrt(train_errors), "r--", linewidth=2, label="Training set")
plt.legend(loc="upper right", fontsize=14)
plt.xlabel("Epoch", fontsize=14)
plt.ylabel("RMSE", fontsize=14)
plt.savefig("early_stopping_plot")
plt.show()best_epoch, best_model(239,
SGDRegressor(eta0=0.0005, learning_rate='constant', max_iter=1, penalty=None,
random_state=42, tol=-inf, warm_start=True))本章的分享到此结束
秋叶无缘
利用python进行探索性数据分析(EDA):以Kaggle泰坦尼克号数据集为例
利用Python进行探索性数据分析(EDA)探索性数据分析探索性数据分析(EDA)是我们进行数据分析和很多数据挖掘比赛的第一步。当我们得到一个数据后,我们首先要分析一下这个数据的基本情况,例如哪些变量是分类型的,哪些是连续型的,它们的分布是什么情况,它们之间有什么关系,数据缺失值如何。通过探索性数据分析后,我们能大致了解数据,从而进行相关模型建立以及作为特征工程的基础。 本文通过Kaggle泰坦尼克号数据来进行探索性数据分析实战。当然也有一些简单的方法得到一个简略的探索性数据分析结果,在后续我会分享给大家。1. Titanic原始数据解读泰坦尼克号的沉没是历史上最知名的沉船事故之一。1912年4月15日,泰坦尼克号在与冰山相撞后沉没,2224名乘客和船员中有1502人死亡。这是一场世界上任何人都无法忘记的难以忘怀的灾难。建造泰坦尼克号花费了大约750万美元,但是由于这场意外,它沉到了海底。泰坦尼克号数据集对于初学者来说是一个非常好的数据集,数据包含训练集和测试集,==数据集链接==我会放在文末。欢迎大家下载学习其包含的变量如下:变量 意义 取值
survival 是否存活 0 = No, 1 = Yes
pclass 票的种类 1 = 1st, 2 = 2nd, 3 = 3rd
sex 性别
Age 年龄(岁)
sibsp 兄弟姐妹/配偶的同行人数
parch 父母/孩子的同行人数
ticket 票号
fare 票价
cabin 座舱号
embarked 登船港口 C = Cherbourg, Q = Queenstown, S = Southampton上述变量的一些说明:pclass:可以看做是社会经济地位的代表1=上层阶级2=中层阶级3=底层阶级Age:如果小于1,则年龄为分数。如果年龄是估计的,则以xx.5的形式表示sibsp:表示如下家庭关系Sibling:兄弟姐妹Souse:表示配偶(未婚和情人不算)parch: 表示如下家庭关系:Parent:父母chile:孩子好了,根据上述分析,我们对数据有哪些变量有了初步了解了,接下来我们正式进入探索性数据分析2.数据导入首先要导入相关库,如果对以下库有不了解的可以看我本专栏之前的文章导入相关库import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('fivethirtyeight')#图形主题
import warnings
warnings.filterwarnings('ignore')#忽视警告
%matplotlib inline分别导入训练集和测试集df_train = pd.read_csv('train.csv')
df_test = pd.read_csv('test.csv')3.检查数据上述我们已经导入了数据集,接下来我们来看一下该数据的一些基本情况总览一下数据df_train.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarked0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNS1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85C2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNS3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123S4503Allen, Mr. William Henrymale35.0003734508.0500NaNSdf_test.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdPclassNameSexAgeSibSpParchTicketFareCabinEmbarked08923Kelly, Mr. Jamesmale34.5003309117.8292NaNQ18933Wilkes, Mrs. James (Ellen Needs)female47.0103632727.0000NaNS28942Myles, Mr. Thomas Francismale62.0002402769.6875NaNQ38953Wirz, Mr. Albertmale27.0003151548.6625NaNS48963Hirvonen, Mrs. Alexander (Helga E Lindqvist)female22.011310129812.2875NaNS看一下数据的纬度df_train.shape,df_test.shape((891, 12), (418, 11))可以看出训练集数据包含了891行,12列数据,测试集包含418个行11列,其中Survived列是我们要进行预测的变量df_train.isnull().sum(),df_test.isnull().sum()(PassengerId 0
Survived 0
Pclass 0
Name 0
Sex 0
Age 177
SibSp 0
Parch 0
Ticket 0
Fare 0
Cabin 687
Embarked 2
dtype: int64,
PassengerId 0
Pclass 0
Name 0
Sex 0
Age 86
SibSp 0
Parch 0
Ticket 0
Fare 1
Cabin 327
Embarked 0
dtype: int64)从结果来看,训练集中age,cabin,embarked有缺失值,测试集中age,cabin,Cabin有缺失值,我们待会儿对其进行相应处理接下来我们先看一下训练集中有多少人存活了f,ax=plt.subplots(1,2,figsize=(18,8))
df_train['Survived'].value_counts().plot.pie(explode=[0,0.1],autopct='%1.1f%%',ax=ax[0],shadow=True)
ax[0].set_title('Survived')
ax[0].set_ylabel('')
sns.countplot('Survived',data=df_train,ax=ax[1])
ax[1].set_title('Survived')
plt.show()很明显,幸存率很低。在训练集的891名乘客中,只有大约350人幸存,即只有==38.4%幸存==,说明响应变量是非平衡的,我们需要进一步挖掘哪些类别的乘客存活了,这在后续的数据挖掘项目实战中会进行相关介绍。在这里我们可以将这个38.4%当做存活率的一个先验概率4. ==特征分析==除去乘客编号和姓名外一共包含十个特征变量,主要可以分为以下四种类型4.1 ==分类性变量==(名义变量)分析==分类变量==是指具有两个或多个类别的变量,该特征中的每个值都可以根据它们进行分类。例如,性别是一个分类变量,有两个类别(男性和女性)。并且我们不能对这些变量没有先后顺序。它们也被称为名义变量。例如:sex和Embarked4.1.1 性别变量分析下面我们以sex为例来分析一下首先进行按性别分组,分析一下不同性别,存活率是否有差异df_train.groupby(['Sex','Survived'])['Survived'].count()Sex Survived
female 0 81
1 233
male 0 468
1 109
Name: Survived, dtype: int64f,ax=plt.subplots(1,2,figsize=(18,8))
# 绘制不同性别下平均幸存率
df_train[['Sex','Survived']].groupby(['Sex']).mean().plot.bar(ax=ax[0])
ax[0].set_title('Survived vs Sex')
# 绘制不同性别下,存货和死亡的人数
sns.countplot('Sex',hue='Survived',data=df_train,ax=ax[1])
ax[1].set_title('Sex:Survived vs Dead')
plt.show()从结果来看发现一个很有趣的现象,船上的男人比女人多得多,不过,==女性获救的人数几乎是男性获救人数的两倍==。船上女性的存活率约为75%,而男性的存活率约为18-19%。从结果来看,性别似乎是一个很重要的特征,但它是最重要的吗?我们继续分析一下其他变量的情况4.1.2 登船港口分析f,ax=plt.subplots(2,2,figsize=(20,15))
df_train[['Embarked','Survived']].groupby(['Embarked']).mean().plot.bar(ax=ax[0,0])
ax[0,0].set_title('Survived vs Embarked')
sns.countplot('Embarked',hue='Survived',data=df_train,ax=ax[0,1])
ax[0,1].set_title('Embarked:Survived vs Dead')
sns.countplot('Embarked',hue='Survived',data=df_train,ax=ax[1,0])
ax[1,0].set_title('Embarked vs Survived')
sns.countplot('Embarked',hue='Pclass',data=df_train,ax=ax[1,1])
ax[1,1].set_title('Embarked vs Pclass')
plt.subplots_adjust(wspace=0.2,hspace=0.5)
plt.show()从S登船的旅客人数最多来自C的乘客看起来很幸运,因为他们中有很大一部分幸存了下来。原因可能是营救了所有的Pclass1和Pclass2乘客。Q港有将近95%的乘客来自Pclass3。==从不同登船港口来看,只有C港口的存活人数比死亡人数多。==sns.factorplot('Pclass','Survived',hue='Sex',col='Embarked',data=df_train)
plt.show()对于Pclass1和Pclass2的女性来说,她们的存活几率几乎为1。S港对于Pclass3乘客来说似乎非常不幸,因为男性和女性的存活率都非常低。Q港看起来对男性来说是最不幸运的,因为几乎所有人都来自Pclass3。还记得我们之前发现该变量存在缺失值,我们看到从S港登机的乘客最多时,因此在这里我们用S代替了NaN。df_train['Embarked'].fillna('S',inplace=True)4.2 ==定序变量==分析有序变量与分类变量类似,但它们之间的区别在于,我们可以对值进行相对排序。例如:如果我们有一个像高度这样的特征,其值为高、中、短,那么高度是一个顺序变量。在这里,我们可以对变量进行相对排序。 例如:PClass我们之前讲过==Pclass可以看做是社会地位的体现==pd.crosstab(df_train.Pclass,df_train.Survived,margins=True).style.background_gradient()#T_025db_row0_col0, #T_025db_row1_col1, #T_025db_row1_col2 { background-color: #fff7fb; color: #000000; } #T_025db_row0_col1 { background-color: #dddbec; color: #000000; } #T_025db_row0_col2 { background-color: #f8f1f8; color: #000000; } #T_025db_row1_col0 { background-color: #faf2f8; color: #000000; } #T_025db_row2_col0 { background-color: #3790c0; color: #f1f1f1; } #T_025db_row2_col1 { background-color: #ece7f2; color: #000000; } #T_025db_row2_col2 { background-color: #8eb3d5; color: #000000; } #T_025db_row3_col0, #T_025db_row3_col1, #T_025db_row3_col2 { background-color: #023858; color: #f1f1f1; } Survived01AllPclass 180136216297871843372119491All549342891f,ax=plt.subplots(1,2,figsize=(18,8))
df_train['Pclass'].value_counts().plot.bar(ax=ax[0])
ax[0].set_title('Number Of Passengers By Pclass')
ax[0].set_ylabel('Count')
sns.countplot('Pclass',hue='Survived',data=df_train,ax=ax[1])
ax[1].set_title('Pclass:Survived vs Dead')
plt.show()从结果可以看出一个很现实的问题,我们常说金钱买不到一切。但我们可以清楚地看到,在救援过程中,==第一类乘客被给予了非常高的优先级。尽管第三类乘客的数量要高得多,但他们的存活率仍然很低,大约在25%左右。Pclass 1,存活率约为63%,而Pclass 2,存活率约为48%。== 在这样一个物质世界,可以看出金钱和地位也很重要。 我们再将性别和票类型一起分析pd.crosstab([df_train.Sex,df_train.Survived],df_train.Pclass,margins=True).style.background_gradient(cmap='summer_r')#T_d753d_row0_col0, #T_d753d_row0_col1, #T_d753d_row0_col3, #T_d753d_row3_col2 { background-color: #ffff66; color: #000000; } #T_d753d_row0_col2, #T_d753d_row1_col2 { background-color: #f1f866; color: #000000; } #T_d753d_row1_col0 { background-color: #96cb66; color: #000000; } #T_d753d_row1_col1 { background-color: #a3d166; color: #000000; } #T_d753d_row1_col3 { background-color: #cfe766; color: #000000; } #T_d753d_row2_col0 { background-color: #a7d366; color: #000000; } #T_d753d_row2_col1, #T_d753d_row2_col3 { background-color: #85c266; color: #000000; } #T_d753d_row2_col2 { background-color: #6eb666; color: #f1f1f1; } #T_d753d_row3_col0 { background-color: #cde666; color: #000000; } #T_d753d_row3_col1 { background-color: #f0f866; color: #000000; } #T_d753d_row3_col3 { background-color: #f7fb66; color: #000000; } #T_d753d_row4_col0, #T_d753d_row4_col1, #T_d753d_row4_col2, #T_d753d_row4_col3 { background-color: #008066; color: #f1f1f1; } Pclass123AllSexSurvived female03672811917072233male077913004681451747109All216184491891sns.factorplot('Pclass','Survived',hue='Sex',data=df_train)
plt.show()在这种情况下,我们使用FactorPlot,因为它使分类值的分离变得容易。 现在我们来看看交叉表和因子图,可以看出,==Pclass1组女性的存活率约为95-96%,在94名Pclass1组女性中,只有3人死亡。同样,无论Pclass如何,女性在救援时都被放在第一位。即使是来自Pclass1的男性,存活率也很低。==4.3 连续型变量分析如果变量可以取变量列中的最小值或最大值之间的值,则称变量为连续的。 例如:age和pare4.3.1 age首先我们来看age的分布情况df_train['Age'].describe()count 714.000000
mean 29.699118
std 14.526497
min 0.420000
25% 20.125000
50% 28.000000
75% 38.000000
max 80.000000
Name: Age, dtype: float64最大80岁,最小的才0.42岁(这里是根据天数来划分成分数的),下面我们来看一下不同性别,不同票型年龄的分布f,ax=plt.subplots(1,2,figsize=(18,8))
sns.violinplot("Pclass","Age", hue="Survived", data=df_train,split=True,ax=ax[0])
ax[0].set_title('Pclass and Age vs Survived')
ax[0].set_yticks(range(0,110,10))
sns.violinplot("Sex","Age", hue="Survived", data=df_train,split=True,ax=ax[1])
ax[1].set_title('Sex and Age vs Survived')
ax[1].set_yticks(range(0,110,10))
plt.show()1) 孩子的数量随着pclass的增加而增加,但无论哪一类10岁以下的乘客(即儿童)的存活率较高。2) 来自Pclass1的20-50岁乘客的生存机会很高,甚至比女性更高。3) 对于男性来说,随着年龄的增长,存活的几率会降低。正如我们之前看到的,年龄特征有177个空值。要替换这些NaN值,我们可以为它们指定数据集的平均年龄。但问题是,有很多不同年龄的人。我们不能将一个原本是50岁的人定义为4岁的孩子。有没有办法找出乘客的年龄段?我们再来看一下我们的数据集df_train.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarked0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNS1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85C2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNS3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123S4503Allen, Mr. William Henrymale35.0003734508.0500NaNS==注意,有名字这一列,我们可以看到这些名字有一个类似Mr或Mrs的称呼,因此我们可以将Mr和Mrs的平均年龄值分配给相应的组。==首先我们通过正则表达式将这些Miss,Mr,Mrs提取出来,放在Initial列中df_train['Initial']=0
for i in df_train:
df_train['Initial']=df_train.Name.str.extract('([A-Za-z]+)\.') 我们使用了[A-Za-z]+)\ .所以它所做的是,它寻找A-Z或a-z之间的字符串,后面跟着.所以我们成功地从名字中提取了我们想要的信息df_train.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedInitial0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNSMr1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85CMrs2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNSMiss3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123SMrs4503Allen, Mr. William Henrymale35.0003734508.0500NaNSMrpd.crosstab(df_train.Initial,df_train.Sex).T.style.background_gradient(cmap='summer_r') #Checking the Initials with the Sex#T_df8c5_row0_col0, #T_df8c5_row0_col1, #T_df8c5_row0_col3, #T_df8c5_row0_col4, #T_df8c5_row0_col5, #T_df8c5_row0_col7, #T_df8c5_row0_col8, #T_df8c5_row0_col12, #T_df8c5_row0_col15, #T_df8c5_row0_col16, #T_df8c5_row1_col2, #T_df8c5_row1_col6, #T_df8c5_row1_col9, #T_df8c5_row1_col10, #T_df8c5_row1_col11, #T_df8c5_row1_col13, #T_df8c5_row1_col14 { background-color: #ffff66; color: #000000; } #T_df8c5_row0_col2, #T_df8c5_row0_col6, #T_df8c5_row0_col9, #T_df8c5_row0_col10, #T_df8c5_row0_col11, #T_df8c5_row0_col13, #T_df8c5_row0_col14, #T_df8c5_row1_col0, #T_df8c5_row1_col1, #T_df8c5_row1_col3, #T_df8c5_row1_col4, #T_df8c5_row1_col5, #T_df8c5_row1_col7, #T_df8c5_row1_col8, #T_df8c5_row1_col12, #T_df8c5_row1_col15, #T_df8c5_row1_col16 { background-color: #008066; color: #f1f1f1; } InitialCaptColCountessDonDrJonkheerLadyMajorMasterMissMlleMmeMrMrsMsRevSirSex female001010100182210125100male12016102400005170061有一些拼写错误的首字母,比如Mlle或Mme,代表Miss。我将用Miss来进行替换,具体如下所示df_train['Initial'].replace(['Mlle','Mme','Ms','Dr','Major','Lady','Countess','Jonkheer','Col','Rev','Capt','Sir','Don'],['Miss','Miss','Miss','Mr','Mr','Mrs','Mrs','Other','Other','Other','Mr','Mr','Mr'],inplace=True)此时再计算每一类的平均年龄df_train.groupby('Initial')['Age'].mean() Initial
Master 4.574167
Miss 21.860000
Mr 32.739609
Mrs 35.981818
Other 45.888889
Name: Age, dtype: float64根据这些平均年龄对每一类的缺失值填充df_train.loc[(df_train.Age.isnull())&(df_train.Initial=='Mr'),'Age']=33
df_train.loc[(df_train.Age.isnull())&(df_train.Initial=='Mrs'),'Age']=36
df_train.loc[(df_train.Age.isnull())&(df_train.Initial=='Master'),'Age']=5
df_train.loc[(df_train.Age.isnull())&(df_train.Initial=='Miss'),'Age']=22
df_train.loc[(df_train.Age.isnull())&(df_train.Initial=='Other'),'Age']=46我们再来看看年龄的缺失值情况df_train.Age.isnull().sum()0下面我们再来看一下幸存和死亡人员的年龄分布f,ax=plt.subplots(1,2,figsize=(20,10))
df_train[df_train['Survived']==0].Age.plot.hist(ax=ax[0],bins=20,edgecolor='black',color='red')
ax[0].set_title('Survived= 0')
x1=list(range(0,85,5))
ax[0].set_xticks(x1)
df_train[df_train['Survived']==1].Age.plot.hist(ax=ax[1],color='green',bins=20,edgecolor='black')
ax[1].set_title('Survived= 1')
x2=list(range(0,85,5))
ax[1].set_xticks(x2)
plt.show()儿童(年龄<5岁)被大量拯救(妇女和儿童优先策略)。最年长的乘客获救(80岁)。死亡人数最多的是30-40岁的年龄组。sns.factorplot('Pclass','Survived',col='Initial',data=df_train)
plt.show()可以看出,无论在哪一类阶级如何,妇女和儿童优先政策都是正确的。4.3.2 Fare下面对票价分析,首先看一下票价的整体分布情况df_train.Fare.describe()count 891.000000
mean 32.204208
std 49.693429
min 0.000000
25% 7.910400
50% 14.454200
75% 31.000000
max 512.329200
Name: Fare, dtype: float64发现最低的居然是0元,可能是用积分或者其他方式兑换的,下面我们看一下每种票类型下的价格分布f,ax=plt.subplots(1,3,figsize=(20,8))
sns.distplot(df_train[df_train['Pclass']==1].Fare,ax=ax[0])
ax[0].set_title('Fares in Pclass 1')
sns.distplot(df_train[df_train['Pclass']==2].Fare,ax=ax[1])
ax[1].set_title('Fares in Pclass 2')
sns.distplot(df_train[df_train['Pclass']==3].Fare,ax=ax[2])
ax[2].set_title('Fares in Pclass 3')
plt.show()4.4 离散型变量分析离散型变量是指数值型变量但是取值不是连续的,例如sipsip和parch。这两个变量表示一个人是独自一人还是与家人在一起。4.4.1 SibSp分析表示配偶和兄弟姐妹的陪同人数pd.crosstab([df_train.SibSp],df_train.Survived).style.background_gradient()#T_9cfee_row0_col0, #T_9cfee_row0_col1 { background-color: #023858; color: #f1f1f1; } #T_9cfee_row1_col0 { background-color: #d4d4e8; color: #000000; } #T_9cfee_row1_col1 { background-color: #63a2cb; color: #f1f1f1; } #T_9cfee_row2_col0, #T_9cfee_row4_col0 { background-color: #fbf4f9; color: #000000; } #T_9cfee_row2_col1 { background-color: #f6eff7; color: #000000; } #T_9cfee_row3_col0, #T_9cfee_row3_col1, #T_9cfee_row4_col1 { background-color: #fdf5fa; color: #000000; } #T_9cfee_row5_col0, #T_9cfee_row5_col1, #T_9cfee_row6_col1 { background-color: #fff7fb; color: #000000; } #T_9cfee_row6_col0 { background-color: #fef6fb; color: #000000; } Survived01SibSp 03982101971122151331244153550870df_train.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedInitial0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNSMr1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85CMrs2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNSMiss3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123SMrs4503Allen, Mr. William Henrymale35.0003734508.0500NaNSMrf,ax=plt.subplots(1,2,figsize=(12,8))
sns.barplot('SibSp','Survived',data=df_train,ax=ax[0])
ax[0].set_title('SibSp vs Survived')
sns.factorplot('SibSp','Survived',data=df_train,ax=ax[1])
ax[1].set_title('SibSp vs Survived')
plt.show() pd.crosstab(df_train.SibSp,df_train.Pclass).style.background_gradient()Pclass123SibSp 0137120351171558325815331124001850058007条形图和折线图显示,如果一名乘客独自一人在船上,没有兄弟姐妹,他有34.5%的存活率。如果兄弟姐妹的数量在1-2个,存活率会上升,但当人太多时,存活率又会下降。这是有道理的。也就是说,如果我有家人在船上,可以相互帮助,但当太多的家人在床上,我会尽力拯救他们,而不是首先拯救自己。==5-8人家庭的存活率为0%。== 原因可能是Pclass全是3。交叉表显示SibSp>3的人都在Pclass3中。Pclass3中所有大家庭(sibsp>3)的死亡迫在眉睫。4.4.2 Parchparch表示同行的孩子或者父母pd.crosstab(df_train.Parch,df_train.Pclass).style.background_gradient()Pclass123Parch 0163134381131325522116433023410350056001同样说明当同行人数太多,大部分都在Pclass3中f,ax=plt.subplots(1,2,figsize=(20,8))
sns.barplot('Parch','Survived',data=df_train,ax=ax[0])
ax[1].set_title('Parch vs Survived')
sns.factorplot('Parch','Survived',data=df_train,ax=ax[1])
ax[1].set_title('Parch vs Survived')
plt.show()这里的结果也非常相似。父母或孩子在船上的乘客生存的机会更大。然而,随着数量的增加,它会减少。 对于船上有1-3个父母或者孩子的人来说,生存的机会是好的。当船上有超过4个父母时,存活的机会会降低。🌿4.5 特征分析小结性别:与男性相比,女性的生存几率较高。船票类型:有一个明显的趋势是,成为头等舱的乘客会有更好的生存机会。Pclass3的存活率非常低。对于女性来说,Pclass1的存活几率几乎为1,而对于Pclass2的女性来说,存活几率也很高。年龄:5-10岁以下的儿童存活率很高。15岁至35岁的乘客大量死亡。上船港口:尽管大多数Pclass1乘客在S站出发,但在C站的存活几率比在S站更好。在Q站的乘客都来自Pclass3。Parch+SibSp(家人同行数):船上有1-2个兄弟姐妹、配偶或1-3个父母生存的可能性更高。5. 相关性分析sns.heatmap(df_train.corr(),annot=True,cmap='RdYlGn',linewidths=0.5)
fig=plt.gcf()
fig.set_size_inches(10,8)
plt.show()注意,我们只比较数值型变量的相关性。现在让我们假设两个特征高度或完全相关,因此一个特征的增加导致另一个特征的增加。这意味着这两个特征都包含高度相似的信息,并且信息几乎没有差异。此时称为具有多重共线性,因为两者包含几乎相同的信息。此时,我们在训练模型时,应该尽量消除多重共线性的影响。 现在,==从上面的相关系数热力图中,我们可以看到这些特征变量没有太大的相关性。SibSp和Parch之间的相关性最高,即0.41。== 所以我们可以使用所有的特征变量进行后续分析。
秋叶无缘
利用python两种方法教你一行代码实现探索性数据分析报告
两种方法教你一行代码实现探索性数据分析报告==探索性数据分析(EDA)== 是使用可视化方法总结和分析数据集主要特征的过程。EDA是数据科学家要做的第一部分,如果我们不懂得如何进行EDA,那么无法对数据进行进一步的建模。上一篇文章我以泰坦尼克号数据为例,介绍了如何使用python详细的进行探索性数据分析,但有时这是很耗费时间的,现在,我介绍两种方法实现==一行代码生成探索性数据分析报告==。分别使用以下两个包,如果没有安装的小伙伴先去安装一下。Sweetvizpandas_profiling我们照样使用==泰坦尼克号数据集==进行分析先导入数据import pandas as pd
from pandas_profiling import ProfileReport
df = pd.read_csv("train.csv")
df.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarked0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNS1211Cumings, Mrs. John Bradley (Florence Briggs Thayer)female38.010PC 1759971.2833C85C2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNS3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123S4503Allen, Mr. William Henrymale35.0003734508.0500NaNSdf.shape(891, 12)1. Sweetviz==Sweetviz== 一个python开源库,通过基本的可视化来分析数据,并生成一个html文件。这个库的主要优点是我们可以 ==比较数据集==。 首先我们创建一个名为sweet_Analysized_report的文件,显示探索性数据分析结果。在本报告中,我们可以很容易地找到不同变量的特征,如:数量、缺失值、不同值、最大值、最小值、平均值等。具体代码和结果如下图所示import sweetviz as sv
sweet_report = sv.analyze(df)
sweet_report.show_html('sweet_report.html')==相关系数热力图====Age分布情况====sibsip分布情况== ==在这个Html文件中,我们可以看到其他每个变量的分布情况,大家可以自行验证测试。==2. 比较探索性数据分析Sweetviz还支持比较不同数据集的探索性数据分析,首先,我们将数据集分成两部分,然后进行比较,然后保存此比较报告。数据集的两部分显示两种不同的颜色橙色和蓝色。具体代码和结果见下文:df1 = sv.compare(df[445:], df[:445])
df1.show_html('Compare.html')这里我把数据分为两部分,分别有445和446个数据。==survived分布情况====Pclass分布情况====sex分布情况==3. pandas_profiling==pandas_profiling==基于pandas的DataFrame数据类型,可以简单快速地进行探索性数据分析。和sweetviz类似,pandas_profiling可以返回一个html文件,包含如下内容数据整体概要:数据类型,唯一值,缺失值等各个变量的描述性统计分析各个变量的分布情况,直方图和条形图变量间的相关系数热力图等具体代码和结果如下:design_report = ProfileReport(df)
design_report.to_file(output_file='report.html')==变量分布情况====相关系数热力图====变量关系图====数据总体概要==总结用上述两种方法得到的探索性数据分析是非常简易的。如果要想详细了解数据的话,建议一步一步根据自己的需求进行分析。具体可以看下面这篇推荐的文章,不过通过上述两种方法可以让我们大致初步的了解一下数据情况,并且可以节约很多时间(毕竟探索性数据分析真的很花费时间)
秋叶无缘
大数据闯关之MySQL基础篇(七):约束
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。以下是这个系列的前置博客大数据闯关之MySQL基础篇(六):函数大数据闯关之MySQL基础篇(五):DCL用户管理大数据闯关之MySQL基础篇(四):DQL数据查询操作大数据闯关之MySQL基础篇(三):DML数据操作大数据闯关之MySQL基础篇(二):数据库操作大数据闯关之MySQL基础篇(一):MySQL安装及基础介绍一、概述约束是作用于表中字段上的规则,用于限制存储在表中的数据。目的:保证数据库中数据的正确、有效性和完整性分类:约束描述关键字非空约束限制该字段的数据不能为nullNOT NULL唯一约束保证该字段的所有数据都是唯一、不重复的UNIQUE主键约束主键是一行数据的唯一标识,要求非空且唯一PRIMARY KEY默认约束保存数据时,如果未指定该字段的值,则采用默认值DEFAULT检查约束保证字段值满足某一个条件CHECK外键约束用来让两张表的数据建立连接,保证数据的一致性和完整性FOREIGH KEY约束是作用于表中字段上的,可以在创建表/修改表的时候添加约束根据以下要求可以创建表结构字段名字段含义字段类型约束条件约束关键字idID唯一标识int主键,并且自动增长PRIMARY KEY, AUTO_INCREMENTname姓名varchar(10)不为空,并且唯一NOT NULL, UNIQUEage年龄int大于0,并且小于等于120CHECKstatus状态char(1)如果没有指定该值,默认为1DEFAULTgender性别char(1)无· · CREATE TABLE user(
· id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
· name VARCHAR(10) NOT NULL UNIQUE COMMENT '姓名',
· age INT CHECK(age > 0 && age <= 120) COMMENT '年龄',
· status CHAR(1) DEFAULT '1' COMMENT '状态',
· gender CHAR(1) COMMENT '性别'
· ) COMMENT '';二、外键约束概念:外键用来让两张表的数据之间建立连接,从而保证数据的一致性和完整性语法:添加外键 CREATE TABLE 表名(
字段名 数据类型,
...
[CONSTRAINT] [外键名称] FOREIGH KEY(外键字段名) REFERENCES 主表(主表列名)
);ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGH KEY(外键字段名) REFERENCES 主表(主表列名);外键删除/更新行为说明NO ACTION当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则不允许删除/更新RESTRICT当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则不允许删除/更新CASCADE当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则也删除/更新外键在子表中的记录SET NULL当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则设置子表中该外键值为nullSET DEFAULT父表有变更时,子表将外键列设置成一个默认的值举个CASCADE的例子ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGH KEY(外键字段名) REFERENCES 主表(主表列名) ON UPDATE CASCADE
秋叶无缘
Python数据分析从入门到进阶:快速处理文本(含代码)
1. 清洗文本对一些非结构化的文本数据进行基本的清洗stripsplitreplace# 创建文本
text_data = [' Interrobang. By Aishwarya Henriette ',
'Parking And goding. by karl fautier',
' Today is the night. by jarek prakash ']# 去除文本两端的空格
stripwhitespace = [string.strip() for string in text_data]stripwhitespace['Interrobang. By Aishwarya Henriette', 'Parking And goding. by karl fautier', 'Today is the night. by jarek prakash']# 删除句号
remove_periods = [string.replace('.','') for string in text_data]remove_periods[' Interrobang By Aishwarya Henriette ', 'Parking And goding by karl fautier', ' Today is the night by jarek prakash ']# 创建函数
def capitalizer(string):
return string.upper()[capitalizer(string) for string in remove_periods][' INTERROBANG BY AISHWARYA HENRIETTE ', 'PARKING AND GODING BY KARL FAUTIER', ' TODAY IS THE NIGHT BY JAREK PRAKASH ']# 使用正则表达式
import redef replace_letters_with_x(string):
return re.sub(r'[a-zA-Z]','x',string)[replace_letters_with_x(string) for string in remove_periods][' xxxxxxxxxxx xx xxxxxxxxx xxxxxxxxx ', 'xxxxxxx xxx xxxxxx xx xxxx xxxxxxx', ' xxxxx xx xxx xxxxx xx xxxxx xxxxxxx ']2. 解析并清洗HTML#使用beautiful soup 对html进行解析from bs4 import BeautifulSoup# 创建html代码
html = """
<div class='full_name'><span style='font-weight:bold'>
Masege Azra"
"""# 创建soup对象
soup = BeautifulSoup(html, 'lxml')soup.find('div')<div class="full_name"><span style="font-weight:bold">
Masege Azra"
</span></div>3. 移除标点import unicodedata
import systext_data = ['Hi!!!! I. love. This. Song....',
'10000% Agree!!!! #LoveIT',
'Right??!!']# 创建一个标点符号字典
punctuation = dict.fromkeys(i for i in range(sys.maxunicode) if unicodedata.category(chr(i)).startswith('P'))[string.translate(punctuation) for string in text_data]['Hi I love This Song', '10000 Agree LoveIT', 'Right']4. 文本分词这里介绍一下jieba库import jieba# 创建文本
string = 'The science of study is the technology of tomorrow'seg = jieba.lcut(string)
print(seg)['The', ' ', 'science', ' ', 'of', ' ', 'study', ' ', 'is', ' ', 'the', ' ', 'technology', ' ', 'of', ' ', 'tomorrow']当然,本文只是介绍了在数据清洗中的一些最基本的文本处理方法,后续还会介绍目前NLP的一些主流方法和代码。
秋叶无缘
Python数据分析从入门到进阶:手把手教你处理分类型数据(含详细代码)
引言在构建模型时,我们经常遇见一些分类型数据,此时需要对这些分类型数据进行相应转换。本章介绍如何使用python处理分类型数据,首先分类型数据主要包括以下两种。本身没有顺序的称为nominal,也称为==名义变量== 例如性别本身具有顺序的称为ordinal,也称为==定序变量== 例如年纪:老年、中年、青年如果我们不对分类型数据进行处理的话,那么无法将它们直接构建模型,在机器学习中,处理分类型数据最常用的方法是进行one-hot(独热编码)1. 对名义变量进行转换使用sklearn的LabelBinarizer对这些分类数据进行编码,具体代码如下# 导入相关库
import numpy as np
from sklearn.preprocessing import LabelBinarizer, MultiLabelBinarizer# 创建模拟数据
feature = np.array([['Texas'],
['California'],
['Texas'],
['Delaware'],
['Texas']])# 创建one-hot编码器 也就是将其以矩阵0 1 来表示,
one_hot = LabelBinarizer()classes = one_hot.fit_transform(feature)classesarray([[0, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 0, 1]])如上图所示,001表示Texas,010表示Delaware使用classes_查看分类one_hot.classes_array(['California', 'Delaware', 'Texas'], dtype='<U10')# 对one_hot 进行逆编码转换
one_hot.inverse_transform(classes)array(['Texas', 'California', 'Texas', 'Delaware', 'Texas'], dtype='<U10')import pandas as pd使用pandas来进行one-hot编码pd.get_dummies(feature[:,0]).dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } CaliforniaDelawareTexas00011100200130104001# sklearn 还可以处理每个观测值有多个分类的情况
multiclass_feature = [('Texas', 'Florida'),
('California', 'Alabama'),
('Texas', 'Florida'),
('Delware', 'Florida'),
('Texas', 'Alabama')]one_hot_multiclass = MultiLabelBinarizer()one_hot_multiclass.fit_transform(multiclass_feature)array([[0, 0, 0, 1, 1],
[1, 1, 0, 0, 0],
[0, 0, 0, 1, 1],
[0, 0, 1, 1, 0],
[1, 0, 0, 0, 1]])one_hot_multiclass.classes_array(['Alabama', 'California', 'Delware', 'Florida', 'Texas'],
dtype=object)2. 对ordinal分类特征编码对于定序类变量,这些变量的取值是有一定顺序的,此时,我们需要指定对应的编码dataframe = pd.DataFrame({'Score': ['Low', 'Low', 'Medium', 'Medium', 'High']})scale_mapper = {'Low':1,
'Medium':2,
'High':3}dataframe['Score'].replace(scale_mapper)0 1
1 1
2 2
3 2
4 3
Name: Score, dtype: int64其中:1-Low2-Medium3-High3. 对特征字典编码有的时候我们还会遇见一些特征字典,例如颜色的RGB值,如下所示data_dict = [{'Red':2, 'Blue':4},
{'Red':2, 'Blue':3},
{'Red':1, 'Yellow':2},
{'Red':2, 'Yellow':2}]
data_dict[{'Red': 2, 'Blue': 4}, {'Red': 2, 'Blue': 3}, {'Red': 1, 'Yellow': 2}, {'Red': 2, 'Yellow': 2}]此时的data_dict就是一个特征字典,下面我们看如何使用DictVectorizer将其进行编码from sklearn.feature_extraction import DictVectorizerdictvectorizer = DictVectorizer(sparse=False)# 默认的是会返回稀疏矩阵,此时由于矩阵比较小,我们设置强制返回稠密矩阵features = dictvectorizer.fit_transform(data_dict)featuresarray([[4., 2., 0.],
[3., 2., 0.],
[0., 1., 2.],
[0., 2., 2.]])第一列表示Blue的值,第二列表示Red的值,第三列表示Yellow的值feature_names = dictvectorizer.get_feature_names()
feature_names['Blue', 'Red', 'Yellow']pd.DataFrame(features, columns=feature_names).dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } BlueRedYellow04.02.00.013.02.00.020.01.02.030.02.02.04. 填充缺失的分类值==方法一==: 当分类特征中包含缺失值,我们可以用预测值来填充,下面演示如何使用使用KNN分类器来进行填充# 导入相关库
import numpy as np
from sklearn.neighbors import KNeighborsClassifier# 导入数据
X = np.array([[0, 2.10, 1.45],
[1, 1.18, 1.33],
[0, 1.22, 1.27],
[1, -0.21, -1.19]])# 第一列为nan
X_with_nan = np.array([[np.nan, 0.87, 1.31],
[np.nan, -0.67, -0.22]])# 训练knn分类器
clf = KNeighborsClassifier(3, weights='distance')
train_model = clf.fit(X[:, 1:], X[:,0])# 预测缺失值的分类
imputed_values = train_model.predict(X_with_nan[:,1:])# 将所预测的分类与原来的特征连接
X_with_imputed = np.hstack((imputed_values.reshape((2,1)), X_with_nan[:,1:]))X_with_imputedarray([[ 0. , 0.87, 1.31],
[ 1. , -0.67, -0.22]])np.vstack((X, X_with_imputed))array([[ 0. , 2.1 , 1.45],
[ 1. , 1.18, 1.33],
[ 0. , 1.22, 1.27],
[ 1. , -0.21, -1.19],
[ 0. , 0.87, 1.31],
[ 1. , -0.67, -0.22]])这种方法是通过将其他特征作为特征矩阵来进行预测,从而求得缺失值==方法二==:选取特征中出现最多的特征值来进行填充,使用simpleimputer# 导入相关库
from sklearn.impute import SimpleImputerX_complete = np.vstack((X,X_with_imputed))imputet = SimpleImputer(strategy='most_frequent')imputet.fit_transform(X_complete)array([[ 0. , 2.1 , 1.45],
[ 1. , 1.18, 1.33],
[ 0. , 1.22, 1.27],
[ 1. , -0.21, -1.19],
[ 0. , 0.87, 1.31],
[ 1. , -0.67, -0.22]])方法二在处理很多数据的时候可能会方便一些,方法一使用KNN预测的效果更好5. 处理不均衡分类收集更多的数据改变评估模型的衡量标准使用嵌入分类权重参数的模型使用鸢(yuan)尾花 数据集 ,默认每种类型都有五十个数据,这里我们删除山鸢尾的四十个数据# 首先导入相关数据
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier#随机森林分类器# 加载iris数据集
iris = load_iris()features = iris.datatarget = iris.target
targetarray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])# 移除前40个features
features = features[40:, :]
target = target[40:]# 转换成一个二元来观察观测值是否为0
target = np.where((target == 0), 0, 1)targetarray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])对于这种不均衡的数据,我们可以选择在训练时对其进行加权处理,我们在这里使用随机森林分类,通过weights参数来进行处理权重# 创建权重
weights = {0: .9, 1:0.1}# 创建一个带权重的随机森林分类器
RandomForestClassifier(class_weight=weights)RandomForestClassifier(class_weight={0: 0.9, 1: 0.1})还可以传入balanced参数,自动创建于分类的频数成反比的权重# 训练一个带均衡分类权重的随机森林分类器
RandomForestClassifier(class_weight='balanced')RandomForestClassifier(class_weight='balanced')6. 重采样处理不均衡分类数据的另一个思路是使用重采样方法,对占多数的使用下采样,对占少数部分的使用上采样,在下采样中,从占多数的分类中取出观测值,创建一个数量与占少数的分类相同的子集下面对鸢尾花数据进行操作# 给每个分类的观察值标签
i_class0 = np.where(target==0)[0]
i_class1 = np.where(target==1)[0]# 计算每个分类值的观察值数量
n_class0 = len(i_class0)
n_class1 = len(i_class1)# 对于每个分类为0的观察值,从分类为一的数据进行无放回的随机采样
i_class1_downsampled = np.random.choice(i_class1, size=n_class0, replace=False)np.hstack((target[i_class0], target[i_class1_downsampled]))array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])# 将分类为0和分类为1的特征矩阵连接起来
np.vstack((features[i_class0,:], features[i_class1_downsampled, :]))[0:5]array([[5. , 3.5, 1.3, 0.3],
[4.5, 2.3, 1.3, 0.3],
[4.4, 3.2, 1.3, 0.2],
[5. , 3.5, 1.6, 0.6],
[5.1, 3.8, 1.9, 0.4]])
秋叶无缘
python数据分析从入门到进阶:分类算法:下(含详细代码)
🍁1.前言在上一篇文章中,我们介绍了如何对mnist数据集建立一个二分类模型,我们当时解决的问题是给我一张图片,判断是否是数字7,但是我们不仅仅对数字7感兴趣,我们希望给我一张任意的图片,计算机能告诉我这张图片是数字几。这是一个多分类问题。一些算法(如SGD分类器、 随机森林分类器和朴素贝叶斯分类器)能处理多个类。其他(如logistic回归)是严格的二元分类器。但是我们可以通过一些策略来实现使用二分类器进行多分类OvR:一种方法是对于0-9十个类别,我们对每个类建立一个二分类器。判断是否属于该类,具体实现方法是,给我一张图片,分别使用这十个分类器预测属于该类的概率。选择概率最大的那一类作为预测结果OvO:另一种方法是对于0-9十个类别,每一次选两个类别进行比较,比较属于哪一类的概率更大。对于minist数据集,则必须在所有45个分类器进行比较,看看哪个类赢的最多。OvO的主要优点是,每个分类器只需要在训练集的一部分进行训练,即选择需要区分的两个类的数据集。然而,对于大多数二进制分类算法,OvR是首选。当我们使用二分类器来处理多分类任务时,sklearn会自动选择OvO或者OvR来处理。例如我们以支持向量机(SVM)为例🍂 2.从二元分类到多分类# 导入数据集
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1, as_frame=False)import numpy as np
X, y = mnist["data"], mnist["target"]
y = y.astype(np.uint)#更改y数据类型为整数# 将数据划分为测试集和训练集
X_train,X_test,y_train,y_test = X[:6000],X[6000:],y[:6000],y[6000:]from sklearn.svm import SVC
svm_clf = SVC(gamma="auto", random_state=123)
svm_clf.fit(X_train, y_train) # y_train
svm_clf.predict([X[0]])array([5], dtype=uint32)还记得,我们在分类算法上介绍的,第一张图片是数字5,预测正确. 其实SVC默认是采用了OvR策略,我们通过decision_function可以看到每一个样本有10个scoressome_digit_scores = svm_clf.decision_function([X[0]])
some_digit_scoresarray([[ 1.8249344 , 8.01830986, 0.81268669, 4.8465137 , 5.87200033,
9.29462954, 3.8465137 , 6.94086295, -0.21310287, 2.83645231]])可以看出,最大的是5np.argmax(some_digit_scores)5# 查看一共有几类
svm_clf.classes_array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint32)注意:训练分类器时,它会将目标类列表按值排序存储在其classes_属性中。在这种情况下,classes_数组中每个类的索引都可以方便地匹配类本身。在本例中,索引5处的类恰好是类5下面我们使用随机森林模型看看结果from sklearn.ensemble import RandomForestClassifierrf_clf = RandomForestClassifier(random_state=123)
rf_clf.fit(X_train, y_train) # y_train
rf_clf.predict([X[0]])array([5], dtype=uint32)🍃3.误差分析首先看看混淆矩阵。需要使用Cross_val_predict函数进行预测,然后调用confusion_matrix()from sklearn.metrics import confusion_matrix
from sklearn.model_selection import cross_val_predict首先这里我将X进行标准化处理from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))y_train_pred = cross_val_predict(svm_clf, X_train_scaled, y_train, cv=3)
conf_mx = confusion_matrix(y_train, y_train_pred)
conf_mxarray([[576, 0, 4, 2, 3, 2, 2, 0, 3, 0],
[ 0, 649, 9, 1, 3, 1, 0, 3, 4, 1],
[ 4, 5, 531, 7, 8, 2, 3, 9, 11, 1],
[ 0, 5, 28, 542, 2, 14, 1, 9, 5, 2],
[ 0, 2, 14, 0, 578, 1, 2, 6, 0, 20],
[ 3, 4, 9, 16, 7, 450, 10, 7, 3, 5],
[ 3, 2, 23, 0, 2, 7, 567, 2, 2, 0],
[ 2, 8, 14, 0, 7, 0, 0, 593, 0, 27],
[ 4, 7, 15, 8, 2, 15, 6, 2, 488, 4],
[ 4, 2, 9, 7, 13, 2, 0, 25, 3, 536]], dtype=int64)这是有很多类。使用Matplotlib的matshow()函数查看混淆矩阵的图像表示通常更方便:plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()这个混淆矩阵看起来不错,因为大多数图像都在主对角线上,这意味着它们被正确分类。5比其他数字略暗,这可能意味着数据集中5的图像较少,或者分类器在5上的性能不如其他数字。现在我们来比较错误率。row_sums = conf_mx.sum(axis=1, keepdims=True)#计算数量
norm_conf_mx = conf_mx / row_sums#计算错误率的混淆矩阵np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()注意,行代表正确的类,列代表预测的列,可以看出2这个数字这一列很亮,说明有很多其他类被误判为2,但是2这一行却又错判为其他类。通过分析混淆矩阵可以让我们深入了解改进分类器的方法。本例中可以先优化数字2,来减少其他数字对2的错判。例如,您可以尝试为看起来像(但不是)的数字收集更多的训练数据,以便分类器可以学习将它们与真实的2区分开来。或者你可以设计一些新的特性来帮助分类器,例如,编写一个算法来计算每个数字圆圈的数量(例如,8有两个,6有一个,5没有)。或者,你可以对图像进行预处理(例如,使用Scikit Image、Pillow或OpenCV),以使某些图案(例如闭合环)更加突出。分析单个错误也是一种很好的方法,可以了解分类器正在做什么,以及它失败的原因,但这更困难、更耗时。例如,让我们绘制数字5和3def plot_digits(instances, images_per_row=10, **options):
size = 28
images_per_row = min(len(instances), images_per_row)#每一行的数字
n_rows = (len(instances) - 1) // images_per_row + 1
n_empty = n_rows * images_per_row - len(instances)
padded_instances = np.concatenate([instances, np.zeros((n_empty, size * size))], axis=0)
image_grid = padded_instances.reshape((n_rows, images_per_row, size, size))
big_image = image_grid.transpose(0, 2, 1, 3).reshape(n_rows * size,
images_per_row * size)
plt.imshow(big_image, cmap = mpl.cm.binary, **options)
plt.axis("off")cl_a, cl_b = 3,5
X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]
X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]
X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]
X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]
plt.figure(figsize=(8,8))
plt.subplot(221); plot_digits(X_aa[:25], images_per_row=5)#每一行五个数字
plt.subplot(222); plot_digits(X_ab[:25], images_per_row=5)
plt.subplot(223); plot_digits(X_ba[:25], images_per_row=5)
plt.subplot(224); plot_digits(X_bb[:25], images_per_row=5)
plt.show()上面一行第二张图是错把3误判为5,第二行第一幅图是错把5判为3的情况🌷4. 多标签分类到目前为止,每个分类器都是分给一个类,在某些情况下,我们可能希望一个分类器输出多个类,例如一个人脸识别器;如果它能识别一个图片多个人,那么这就是一个多标签分类器。下面我们照样以mnist数据集为例, 假设此时我们的目标一个是大于7的数,另一个是偶数。下面使用KNN算法为例from sklearn.neighbors import KNeighborsClassifier
y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 0)
y_multilabel = np.c_[y_train_large, y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)KNeighborsClassifier()knn_clf.predict([X[0]])array([[False, False]])我们知道第一个数是5,它小于7并且不是偶数,因此两个返回值都是False
秋叶无缘
Python机器学习从入门到高级:导入数据(包含数据库连接)
加载数据是我们进行数据分析的第一步,本文主要介绍以下几个常用的方面导入数据集加载scikit-learn中的数据集创建模拟数据集导入csv数据集导入excel数据集连接mysql数据库1.加载sklearn包中的数据集sklearn是一个机器学习库,里面包含了许多机器学习数据集。例如:load_boston 波士顿房价的观测值 用于研究回归算法load_iris 150个花的数据,用于研究分类算法load_digits 手写数字图片的观测值,用于研究图形分类算法的优质数据集from sklearn import datasets# 手写数字数据集
digits = datasets.load_digits()# 创建特征向量
features = digits.data
# 创建目标向量
tatget = digits.targetfeatures[0]array([ 0., 0., 5., 13., 9., 1., 0., 0., 0., 0., 13., 15., 10.,
15., 5., 0., 0., 3., 15., 2., 0., 11., 8., 0., 0., 4.,
12., 0., 0., 8., 8., 0., 0., 5., 8., 0., 0., 9., 8.,
0., 0., 4., 11., 0., 1., 12., 7., 0., 0., 2., 14., 5.,
10., 12., 0., 0., 0., 0., 6., 13., 10., 0., 0., 0.])2.创建模拟数据集2.1 回归数据集下面我们通过make_regression来模拟一个回归数据集from sklearn.datasets import make_regression
features, target, coefficients = make_regression(n_samples=100,
n_features=3,
n_informative=3,
n_targets=1,
noise=0,
coef=True,
random_state=1)print('Featrue Matrix\n', features[:3])
print('Target Vector\n', target[:3])Featrue Matrix
[[ 1.29322588 -0.61736206 -0.11044703]
[-2.793085 0.36633201 1.93752881]
[ 0.80186103 -0.18656977 0.0465673 ]]
Target Vector
[-10.37865986 25.5124503 19.67705609]2.2 分类模拟数据集使用make_classification创建分类数据集from sklearn.datasets import make_classification
features, target= make_classification(n_samples=100,
n_features=3,
n_informative=3,
n_redundant=0,
n_classes=2,
weights=[.25, .75],
random_state=1)print('Featrue Matrix\n', features[:3])
print('Target Vector\n', target[:3])Featrue Matrix
[[ 1.06354768 -1.42632219 1.02163151]
[ 0.23156977 1.49535261 0.33251578]
[ 0.15972951 0.83533515 -0.40869554]]
Target Vector
[1 0 0]import matplotlib.pyplot as plt
%matplotlib inlineplt.scatter(features[:,0], features[:,1],c=target)2.3 聚类数据集使用make_blobs创建聚类数据集# 用于聚类
from sklearn.datasets import make_blobs
features, target = make_blobs(n_samples=100,
n_features=2,
centers=3,
cluster_std=0.5,
shuffle=True,
random_state=1)print('Featrue Matrix\n', features[:3])
print('Target Vector\n', target[:3])Featrue Matrix
[[ -1.22685609 3.25572052]
[ -9.57463218 -4.38310652]
[-10.71976941 -4.20558148]]
Target Vector
[0 1 1]plt.scatter(features[:,0], features[:,1],c=target)3. 加载CSV文件csv文件是我们在进行数据分析时最常用的数据格式。python中pandas库提供了非常简单的方法导入,具体如下import pandas as pd
file = r'C:\Users\DELL\Desktop\Statistic learning\ISLR\data\auto.csv'
df = pd.read_csv(file)
# 当数据没有表头时,设置header = None
df.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } mpgcylindersdisplacementhorsepowerweightaccelerationyearoriginname018.08307.0130350412.0701chevrolet chevelle malibu115.08350.0165369311.5701buick skylark 320218.08318.0150343611.0701plymouth satellite316.08304.0150343312.0701amc rebel sst417.08302.0140344910.5701ford torino4. 加载excel文件url = r'C:\Users\DELL\Desktop\我的文件\学校课程\大三上复习资料\多元统计\例题数据及程序整理\例3-1.xlsx'
df = pd.read_excel(url,header=1)
#sheetname 表数据表所在的位置,如果加入多张数据表,可以把他们放在一个列表中一起传入
f序号批发和零售业交通运输、仓储和邮政业住宿和餐饮业金融业房地产业水利、环境和公共设施管理业所属地区单位类型0153918.031444.047300.038959.047123.035375.0北京集体1261149.039936.045063.0116756.048572.047389.0上海集体2334046.047754.039653.0111004.046593.037562.0江苏集体3450269.051772.039072.0125483.056055.043525.0浙江集体4527341.043153.040554.079899.044936.042788.0广东集体56129199.090183.059309.0224305.080317.074290.0北京国有6789668.0100042.064674.0208343.088977.077464.0上海国有7869904.072784.045581.0105894.065904.059963.0江苏国有89108473.086648.051239.0163834.069972.056899.0浙江国有91063247.076359.052359.0138830.054179.047487.0广东国有101193769.080563.050984.0248919.087522.073048.0北京其他1112118433.099719.052295.0208705.082743.073241.0上海其他121363340.065300.042071.0126708.067070.050145.0江苏其他131461801.071794.041879.0125875.066284.052655.0浙江其他141562271.080955.043174.0145913.068469.052324.0广东其他5. 查询SQL数据库在实际业务分析中,很多时候数据都是存放在数据库中,因此,学会如何连接数据库是非常有必要的,之前介绍了如何使用R语言连接数据库,R语言连接mysql数据库,接下来我们看看如何使用python来连接数据库。首先需要安装pymysql包,pip install pymysql,具体使用代码如下导入相关库impcort pandas as pd
import pymysql连接mysql数据库,需要指定相关的参数dbconn=pymysql.connect(
host="localhost",
database="test",#要连接的数据库
user="root",
password="密码",#密码
port=3306,#端口号
charset='utf8'
)读取数据,通过read_sql可以实现在python中读取sql查询的结果,具体结果如下。sql = "select * from goods;"
df = pd.read_sql(sql=sql, con=dbconn)
df.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } idcategory_idcategoryNAMEpricestockupper_time011女装/女士精品T恤39.910002020-11-10121女装/女士精品连衣裙79.925002020-11-10231女装/女士精品卫衣89.915002020-11-10341女装/女士精品牛仔裤89.935002020-11-10451女装/女士精品百褶裙29.95002020-11-10561女装/女士精品呢绒外套399.912002020-11-10672户外运动自行车399.910002020-11-10782户外运动山地自行车1399.925002020-11-10892户外运动登山杖59.915002020-11-109102户外运动骑行装备399.935002020-11-1010112户外运动运动外套799.95002020-11-1011122户外运动滑板499.912002020-11-10本章的介绍到此介绍,在后续我还会考虑介绍一些如何使用python进行特征工程、数据清洗、模型构建以及一些数据挖掘实战项目。
秋叶无缘
大数据闯关之MySQL进阶篇(四):视图-存储过程-触发器
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。这部分接着之前的MySQL基础篇,接下来将对MySQL进阶部分进行学习。以下为该部分的前置博客大数据闯关之MySQL进阶篇(一):存储引擎大数据闯关之MySQL进阶篇(二):索引大数据闯关之MySQL进阶篇(三):SQL优化一、视图介绍:视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的创建-- 创建视图
create or replace view stu_v_1 as select id, name from student where id <= 10;
查询视图-- 查询视图
show create view stu_v_1;
select * from stu_v_1;
修改视图-- 修改视图
create or replace view stu_v_1 as select id, name, no from student where id <= 10;
alter view test.stu_v_1 as select id, name, no from student where id <= 10;
删除视图drop view if exists stu_v_1;
检查选项:当使用with check option子句创建视图时,MySQL会通过视图检查正在更改的每个行,以使其符合视图的定义。create or replace view stu_v_1 as select id, name from student where id <= 10 with cascaded check option;
视图的更新:要使得视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系。如果视图包含以下任何一项,则该视图不可更新:二、存储过程介绍:存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合。存储过程可以对SQL语句进行封装和复用,类似于编程语言中的函数。特点创建create procedure 存储过程名称([参数列表])
begin
-- sql语句
end;
调用call 名称([参数])
查看select * from information_schema.ROUTINES where ROUTINE_SCHEMA='itcast'
show create procedure p1;
删除drop procedure if exists p1
变量if判断create procedure p3()
begin
declare score int default 58;
declare result varchar(10);
if score >= 85 then
set result := '优秀';
elseif score >= 60 then
set result := '及格';
else
set result := '不及格';
end if;
select result;
end;
参数类型含义备注IN该类参数作为输入,也就是需要调用时传入值默认OUT该类参数作为输出,也就是该参数可以作为返回值INOUT既可以作为输入参数,也可以作为输出参数-- 根据传入的200分制的分数,进行换算,换算成百分制,然后返回
create procedure p5(inout score double)
begin
set score := score * 0.5;
end;
set @score = 198;
call p5(@score);
select @score;
case-- 根据传入的月份,判定月份所属的季节(要求采用case结构)。
-- 1-3月份,为第一季度
-- 4-6月份,为第二季度
-- 7-9月份,为第三季度
-- 10-12月份,为第四季度
create procedure p6(in month int)
begin
declare result varchar(10);
case
when month >= 1 and month <= 3 then
set result := '第一季度';
when month >= 4 and month <= 6 then
set result := '第二季度';
when month >= 7 and month <= 9 then
set result := '第三季度';
when month >= 10 and month <= 12 then
set result := '第四季度';
else
set result := '非法参数';
end case ;
select concat('您输入的月份为: ',month, ', 所属的季度为: ',result);
end;
· 循环while-- 计算从1累加到n的值,n为传入的参数值
create procedure p7(in n int)
begin
declare total int default 0;
while n>0 do
set total := total + n;
set n := n - 1;
end while;
select total;
end;
repeatcreate procedure p8(in n int)
begin
declare total int default 0;
repeat
set total := total + n;
set n := n - 1;
until n <= 0
end repeat;
select total;
end;loop:可以配合LEAVE和ITERATE语句进行使用create procedure p9(in n int)
begin
declare total int default 0;
sum:loop
if n<=0 then
leave sum;
end if;
set total := total + n;
set n := n - 1;
end loop sum;
select total;
end;· 游标cursor:用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、OPEN、FETCH和CLOSE-- 根据传入的参数uage,来查询用户表 tb_user中,所有的用户年龄小于等于uage的用户姓名(name)和专业(profession),
-- 并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。
-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name,profession from tb_user where age <= uage;
declare exit handler for SQLSTATE '02000' close u_cursor;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;· 条件处理程序handler:可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。以上的循环中由于没有定义退出循环的条件,于是在游标遍历完所有的数据后就会报错,因此就需要用到条件处理程序create procedure p12(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name,profession from tb_user where age <= uage;
declare exit handler for not found close u_cursor;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;. 三、存储函数存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的-- 从1到n的累加
create function fun1(n int)
returns int deterministic
begin
declare total int default 0;
while n>0 do
set total := total + n;
set n := n - 1;
end while;
return total;
end;
select fun1(50);
return type说明四、触发器· 介绍:触发器是与表有关的数据库对象,指在insert/update/delete之前或之后,触发并执行触发器中定义的sql语句集合。触发器这种特性可以协助应用在数据库端确保数据的完整性,日志记录,数据校验等操作· insert语法-- 需求: 通过触发器记录 user 表的数据变更日志(user_logs) , 包含增加, 修改 , 删除 ;
-- 准备工作 : 日志表 user_logs
create table user_logs(
id int(11) not null auto_increment,
operation varchar(20) not null comment '操作类型, insert/update/delete',
operate_time datetime not null comment '操作时间',
operate_id int(11) not null comment '操作的ID',
operate_params varchar(500) comment '操作参数',
primary key(`id`)
)engine=innodb default charset=utf8;
-- 插入数据触发器
create trigger tb_user_insert_trigger
after insert on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params) VALUES
(null, 'insert', now(), new.id, concat('插入的数据内容为: id=',new.id,',name=',new.name, ', phone=', NEW.phone, ', email=', NEW.email, ', profession=', NEW.profession));
end;
-- 查看
show triggers ;
-- 删除
drop trigger tb_user_insert_trigger;
-- 插入数据到tb_user
insert into tb_user(id, name, phone, email, profession, age, gender, status, createtime) VALUES (26,'三皇子','18809091212','erhuangzi@163.com','软件工程',23,'1','1',now());-- 修改数据触发器
create trigger tb_user_update_trigger
after update on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params) VALUES
(null, 'update', now(), new.id,
concat('更新之前的数据: id=',old.id,',name=',old.name, ', phone=', old.phone, ', email=', old.email, ', profession=', old.profession,
' | 更新之后的数据: id=',new.id,',name=',new.name, ', phone=', NEW.phone, ', email=', NEW.email, ', profession=', NEW.profession));
end;
show triggers ;
update tb_user set profession = '会计' where id = 23;
update tb_user set profession = '会计' where id <= 5;
delete语法-- 删除数据触发器
create trigger tb_user_delete_trigger
after delete on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params) VALUES
(null, 'delete', now(), old.id,
concat('删除之前的数据: id=',old.id,',name=',old.name, ', phone=', old.phone, ', email=', old.email, ', profession=', old.profession));
end;
show triggers ;
delete from tb_user where id = 26;
秋叶无缘
大数据闯关之MySQL进阶篇(三):SQL优化
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,所以打算开一个大专栏对大数据工具进行学习总结整理。这部分接着之前的MySQL基础篇,接下来将对MySQL进阶部分进行学习。以下为该部分的前置博客大数据闯关之MySQL进阶篇(一):存储引擎大数据闯关之MySQL进阶篇(二):索引一、SQL优化插入数据(insert)优化批量插入,避免每一次插入数据与数据库进行连接insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'jerry');手动提交事务start transaction;
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'jerry');
insert into tb_test values(4,'Tom'),(5,'Cat'),(6,'jerry');
insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'jerry');
commit;主键顺序插入大批量插入数据:如果需要一次性插入大批量数据,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令进行插入主键优化数据组织方式:在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表页分裂:页可以为空,也可以填充一半。每个页包含了2-N行数据,根据主键排列。当主键乱序插入时,一页可能按照原本顺序无法插入数据,因此可能会产生页分裂的情况来按照主键顺序排列页合并:当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记为删除并且它的空间变得允许被其他记录声明使用。一旦被标记删除后,当页中删除的记录达到MERGE_THRESHOLD,InnoDB会开始寻找最靠近的页,看看是否可以将两个页合并以优化空间使用。主键设计原则在满足业务需求的情况下,尽量降低主键的长度。插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键尽量不要使用UUID做主键或者是其他自然主键,如身份证号业务操作时,避免对主键的修改order by优化两种形式 Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫FileSort排序Using index:通过有序索引顺序扫描直接返回有序数据,这种情况即为using index,不需要额外排序,操作效率高根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则尽量使用覆盖索引多字段排序,一个升序一个降序,此时需注意联合索引在创建时的规则如果不可避免出现filesort,大数据量排序时,可以适当增大排序缓冲区大小group by优化在分组操作时,可以通过索引来提高效率分组操作时,索引的使用也是满足最左前缀法则的limit优化覆盖索引+子查询:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化
秋叶无缘
大数据闯关之MySQL基础篇(二):数据库操作
写在前面大家好,这里是立志于在有生之年看到并参与通用人工智能开发工作的Nobody,由于最近在公司要经常性地接触大数据工具,而在学校里接触的比较少,所以打算开一个大专栏对大数据工具进行学习总结整理。这一部分将介绍DDL数据库、表和字段的定义。一、DDL数据库操作查询 查询所有数据库 sql复制代码SHOW DATABASES; 查询当前数据库 sql复制代码SELECT DATABASE();创建 sql复制代码CREATE DATABASE [IF NOT EXISTS] 数据库名 [DEFAULT CHARSET 字符集];删除 sql复制代码DROP DATABASE [IF EXISTS] 数据库名;使用 sql复制代码USE 数据库名二、表操作这个操作需要先使用上述的USE语句选定所需的数据库查询 查询当前数据库所有表 sql复制代码SHOW TABLES; 查询表结构 sql复制代码DESC 表名; 查询指定表的建表语句,这个可以很清楚的看到每个字段的类型 sql复制代码SHOW CREATE TABLE 表名;创建 sql复制代码CREATE TABLE 表名( 字段1 字段1类型[COMMENT 字段1注释], 字段2 字段2类型[COMMENT 字段2注释], 字段3 字段3类型[COMMENT 字段3 注释], ... 字段n 字段n类型[COMMENT 字段n注释] )[COMMENT 表注释];修改 添加字段 sql复制代码ALTER TABLE 表名 ADD 字段名 类型(长度) [COMMENT 注释] [约束]; 修改数据类型 sql复制代码ALTER TABLE 表名 MODIFY 字段名 新数据类型(长度); 修改字段名和字段类型 sql复制代码ALTER TABLE 表名 CHANGE 旧字段名 新字段名 类型(长度) [COMMENT 注释] [约束]; 修改表名 sql复制代码ALTER TABLE 表名 RENAME TO 新表名; 删除字段名 sql复制代码ALTER TABLE 表名 DROP 字段名;删除 删除表 sql复制代码DROP TABLE [IF EXISTS] 表名; 删除指定表,并重新创建该表 sql复制代码TRUNCATE TABLE 表名;三、数据类型及案例MySQL中的数据类型有很多,主要分为三类:数值类型、字符串类型、日期时间类型数值类型 类型大小有符号(SIGNED)范围无符号(UNSIGNED)范围描述TINYINT1 byte(-128,127)(0,255)小整数值SMALLINT2 bytes(-32768,32767)(0,65535)大整数值MEDIUMINT3 bytes(-8388608,8388607)(0,16777215)大整数值INT或INTEGER4 bytes(-2147483648,2147483647)(0,4294967295)大整数值BIGINT8 bytes(-2^63,2^63-1)(0,2^64-1)极大整数值FLOAT4 bytes(-3.402823466 E+38,3.402823466351 E+38)0 和 (1.175494351 E-38,3.402823466 E+38)单精度浮点数值DOUBLE8 bytes(-1.7976931348623157 E+308,1.7976931348623157 E+308)0 和 (2.2250738585072014 E-308,1.7976931348623157 E+308)双精度浮点数值DECIMAL依赖于M(精度)和D(标度)的值依赖于M(精度)和D(标度)的值小数值(精确定点数)字符串类型 类型大小描述CHAR0-255 bytes定长字符串VARCHAR0-65535 bytes变长字符串TINYBLOB0-255 bytes不超过255个字符的二进制数据TINYTEXT0-255 bytes短文本字符串BLOB0-65 535 bytes二进制形式的长文本数据TEXT0-65 535 bytes长文本数据MEDIUMBLOB0-16 777 215 bytes二进制形式的中等长度文本数据MEDIUMTEXT0-16 777 215 bytes中等长度文本数据LONGBLOB0-4 294 967 295 bytes二进制形式的极大文本数据LONGTEXT0-4 294 967 295 bytes极大文本数据日期时间类型 类型大小范围格式描述DATE31000-01-01 至 9999-12-31YYYY-MM-DD日期值TIME3-838:59:59 至 838:59:59HH:MM:SS时间值或持续时间YEAR11901 至 2155YYYY年份值DATETIME81000-01-01 00:00:00 至 9999-12-31 23:59:59YYYY-MM-DD HH:MM:SS混合日期和时间值TIMESTAMP41970-01-01 00:00:01 至 2038-01-19 03:14:07YYYY-MM-DD HH:MM:SS混合日期和时间值,时间戳现在有一个小案例,根据需求设计一张员工信息表,要求如下:编号(纯数字)员工工号(字符串类型,长度不超过10位)员工姓名(字符串类型,长度不超过10位)性别(男/女,存储一个汉字)年龄(正常人年龄)身份证号(二代身份证号均为18位,身份证中有X这样的字符)入职时间(取值年月日即可)CREATE TABLE employees(
id INT COMMENT '编号',
worknumber VARCHAR(10) COMMENT '员工工号',
name VARCHAR(10) COMMENT '员工姓名',
gender CHAR(1) COMMENT '性别',
age TINYINT UNSIGNED COMMENT '年龄',
idNumber CHAR(18) COMMENT '身份证号',
entrytime DATE COMMENT '入职时间'
);
大家可以参考一下上面的创建表的SQL语句。
秋叶无缘
Python数据分析从入门到进阶:玩转日期型数据(含代码)
引言在我们进行一些时间序列问题时,往往要对日期型数据进行分析处理,本章介绍一下如何使用python处理日期型数据1. 将字符串转换成日期# 导入相关库;
import pandas as pd
import numpy as np# 创建字符串
date_strings = np.array(['03-04-2005 11:35 PM',
'23-05-2010 12:01 AM',
'04-09-2009 09:09 PM'])# 转换成datetime 类型的数据
[pd.to_datetime(date, format='%d-%m-%Y %I:%M %p') for date in date_strings][Timestamp('2005-04-03 23:35:00'),
Timestamp('2010-05-23 00:01:00'),
Timestamp('2009-09-04 21:09:00')]# 我们还可以增加errors参数来处理错误
# 转换成datetime类型的数据
[pd.to_datetime(date, format='%d-%m-%Y %I:%M %p', errors = 'coerce') for date in date_strings][Timestamp('2005-04-03 23:35:00'),
Timestamp('2010-05-23 00:01:00'),
Timestamp('2009-09-04 21:09:00')]当传入errors = 'coerce' 参数时,即使转换错误也不会报错,但是会将错误的值返回为Nan(缺失值)2. 处理时区一般而言,pandas的对象默认是没有时区的,不过我们也可以在创建对象时通过tz参数指定时区import pandas as pd# 创建一个dataframe
pd.Timestamp('2017-05-01 06:00:00', tz = 'Europe/London')Timestamp('2017-05-01 06:00:00+0100', tz='Europe/London')# 可以使用tz_locallize添加时区信息
data = pd.Timestamp('2017-05-01 06:00:00')# 设置时区
data_in_london = data.tz_localize('Europe/London')data_in_londonTimestamp('2017-05-01 06:00:00+0100', tz='Europe/London')# 我们还可以使用tz_convert来转换时区
data_in_london.tz_convert('Asia/Chongqing')Timestamp('2017-05-01 13:00:00+0800', tz='Asia/Chongqing')# Series对象还可以对每一个元素应用tz_localiz和tz_convert
dates = pd.Series(pd.date_range('2002-02-02', periods=3, freq='M'))# 设置时区
dates.dt.tz_localize('Asia/Chongqing')0 2002-02-28 00:00:00+08:00
1 2002-03-31 00:00:00+08:00
2 2002-04-30 00:00:00+08:00
dtype: datetime64[ns, Asia/Chongqing]3. 选择日期和时间dataframe = pd.DataFrame()dataframe['date'] = pd.date_range('2001-01-01 01:00:00', periods=100000, freq='H')删选两个日期之间的观察值, 用 & 来表示且的关系dataframe[(dataframe['date']>'2002-01-01 01:00:00') & (dataframe['date']<='2002-1-1 04:00:00')].dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } date87612002-01-01 02:00:0087622002-01-01 03:00:0087632002-01-01 04:00:00另一种方法,将date这一列设为索引,然后用loc删选dataframe = dataframe.set_index(dataframe['date'])dataframe.loc['2002-1-1 01:00:00':'2002-1-1 04:00:00'].dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } datedate2002-01-01 01:00:002002-01-01 01:00:002002-01-01 02:00:002002-01-01 02:00:002002-01-01 03:00:002002-01-01 03:00:002002-01-01 04:00:002002-01-01 04:00:004. 将数据切分成多个特征df = pd.DataFrame()
df['date'] = pd.date_range('1/1/2001', periods=150, freq='w')创建年月日时分的特征df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['hour'] = df['date'].dt.hour
df['minute'] = df['date'].dt.minutedf.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } dateyearmonthdayhourminute02001-01-072001170012001-01-1420011140022001-01-2120011210032001-01-2820011280042001-02-04200124005.计算两个日期之间的时间差import pandas as pd
dataframe = pd.DataFrame()
dataframe['Arrived'] = [pd.Timestamp('01-01-2017'), pd.Timestamp('01-04-2017')]
dataframe['left'] = [pd.Timestamp('01-01-2017'), pd.Timestamp('01-06-2017')]# 计算两个特征直接的时间间隔
dataframe['left'] - dataframe['Arrived']0 0 days
1 2 days
dtype: timedelta64[ns]
秋叶无缘
Python数据分析从入门到进阶:带你玩转特征转换(含详细代码)
引言上一章我们介绍了如何进行基本的数据清洗工作。加下来我们来看看如何进行特征转换,学统计学的小伙伴一定知道什么是标准化,这其实就是一种特征转换,在一些模型中,特征转换是有必要的。(例如某些神经网络问题,使用特征转换可以收敛更快)1.min-max缩放min-max缩放的基本思想是将所有的数据都转换到了某一固定区间,默认的是转换到0-1,其中最小的数据为0,最大的数据为1,变换公式如下:下面来看看如何使用代码实现:首先导入相关库import numpy as np
from sklearn import preprocessing #处理数据预处理包# 首先我们建立一个特征
feature = np.array([[-500.5],
[-100.1],
[0],
[100.1],
[900.9]])
featurearray([[-500.5],
[-100.1],
[ 0. ],
[ 100.1],
[ 900.9]])下面我们使用MinMaxScaler()进行特征缩放,具体代码和结果如下# 1.创建min_max缩放器
minmax_feature = preprocessing.MinMaxScaler()
# 2.对我们要装换的数据进行缩放
scaled_feature = minmax_feature.fit_transform(feature)
scaled_featurearray([[0. ],
[0.28571429],
[0.35714286],
[0.42857143],
[1. ]])拓展:MinMaxScaler()默认会返回到==0-1==之间,但是有的时候我们希望转换到-1到1之间,或者==0-2==之间,我们可以进行相关定义,具体代码如下:minmat_0_2 = preprocessing.MinMaxScaler((0,2))
scaled_feature = minmat_0_2.fit_transform(feature)
scaled_featurearray([[0. ],
[0.57142857],
[0.71428571],
[0.85714286],
[2. ]])2.标准化缩放标准化缩放是我们应用最广泛的方法之一,尤其在统计学当中,我们在建立一些统计模型时,往往先把数据标准化处理。尤其在统计推断中,根据中心极限定理,当数据足够多,我们往往对数据进行标准化之后认为其满足标准正态分布或近似满足标准正态分布,具体公式如下:使用scikit-learn的StandardScaler# 创建特征
feature = np.array([[-1000.1],
[-200.2],
[500.5],
[600.6],
[9000.9]])# 创建缩放器
scaler = preprocessing.StandardScaler()# 标准化
standard = scaler.fit_transform(feature)standardarray([[-0.76058269],
[-0.54177196],
[-0.35009716],
[-0.32271504],
[ 1.97516685]])标准化使用的比minmax更为常见,转换后认为其服从标准正态分布,下面我们来看一下标准化后数据的均值和标准差print('mean:', round(standard.mean()))
print('std:', standard.std())mean: 0
std: 1.0==拓展==:如果数据存在很严重的异常值,可能会影响特征的平均值和方差,也会对标准化早造成不好的影响,我们一般使用中位数和四分位数间距来进行缩放,默认转换规则如下:具体代码和结果如下# 创建缩放器,默认是以中位数进行缩放
robust_scaler = preprocessing.RobustScaler()robust_scaler.fit_transform(feature)array([[-1.87387612],
[-0.875 ],
[ 0. ],
[ 0.125 ],
[10.61488511]])3.归一化归一化处理是一种去量纲比较常用的方法,例如在层次分析法中,我们会使用归一化处理 主要分为L1和L2归一化,在Normalizer()中,默认是L2归一化,假设有一个m×n的矩阵,两种方法的公式如下:L1归一化基本思想是使得每一行相加等于1L2归一化基本思想是使得每一行平方相加等于1具体代码实现如下Normalizer()默认是==L2==归一化feature = np.array([[0.5, 0.5],
[1.1, 3.4],
[1.5, 20.2],
[1.63, 34.4],
[10.9, 3.3]])normalizer = preprocessing.Normalizer()normalized = normalizer.fit_transform(feature)normalizedarray([[0.70710678, 0.70710678],
[0.30782029, 0.95144452],
[0.07405353, 0.99725427],
[0.04733062, 0.99887928],
[0.95709822, 0.28976368]])# 如果以l1范数来归一化,则如下代码,他使每一行的和相加为1
normalized2 = preprocessing.Normalizer(norm='l1').transform(feature)normalized2array([[0.5 , 0.5 ],
[0.24444444, 0.75555556],
[0.06912442, 0.93087558],
[0.04524008, 0.95475992],
[0.76760563, 0.23239437]])4.生成多项式和交互特征使用degree参数选择多项式的最高阶数使用interaction_only可以选择只有交互项features = np.array([[2,3]])polynomial = preprocessing.PolynomialFeatures(degree=2)polynomial.fit_transform(features)array([[1., 2., 3., 4., 6., 9.]])# 此时把0次向也放在里面
polynomial = preprocessing.PolynomialFeatures(degree=2, include_bias=False)#此时不包括0此项
polynomial.fit_transform(features)array([[2., 3., 4., 6., 9.]])# 设置只包含交互项
interaction = preprocessing.PolynomialFeatures(degree=2,
interaction_only=True, include_bias=False)interaction.fit_transform(features)array([[2., 3., 6.]])5.使用函数进行特征转换# 使用FunctionTransform 对一组特征应用一个函数
def add_ten(x):
return x+10
ten_transformer = preprocessing.FunctionTransformer(add_ten,validate=False)ten_transformer.transform(features)array([[12, 13]])上述和pandas使用apply函数是一样的效果6.处理异常值import pandas as pd
houses = pd.DataFrame()houses['Price'] = [534433, 392333, 293222, 4322032]
houses['Bathrooms'] = [2, 3.5, 2, 116]
houses['Squre_Feet'] = [1500, 2500, 1500, 48000]==思路一==# 1.删选观察值houses[houses['Bathrooms']<20].dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PriceBathroomsSqure_Feet05344332.0150013923333.5250022932222.01500==思路二==:将异常值标记,并作为数据的一个特征# 第二种思路,将异常值标记,并作为数据的一个特征
houses['Outlier'] = np.where(houses['Bathrooms']<20, 0, 1)#小于20的即为1houses.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PriceBathroomsSqure_FeetOutlier05344332.01500013923333.52500022932222.01500034322032116.0480001==思路三==,对所有值进行转换,来降低异常值的影响# 第三种思路
houses['Log_of_Squre_Feet'] = [np.log(x) for x in houses['Squre_Feet']]houses.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } PriceBathroomsSqure_FeetOutlierLog_of_Squre_Feet05344332.0150007.31322013923333.5250007.82404622932222.0150007.31322034322032116.048000110.778956因为异常值会对均值和标准差都造成较大的影响,所以一般使用对异常值鲁棒性更高的放缩方法,例如之前介绍的RobustScaler7.将特征离散化基本思路是根据给一个阈值将特征离散化。==方法1==:使用Binarizerfrom sklearn.preprocessing import Binarizerage = np.array([[6],[12],[20],[36],[65]])binary = Binarizer(18)binary.fit_transform(age)array([[0],
[0],
[1],
[1],
[1]])==方法2==:使用numpy 将设定多个阈值来使特征离散化np.digitize(age, bins=[20, 30, 64])array([[0],
[0],
[1],
[2],
[3]], dtype=int64)阈值的设定是左闭右开 所以第一个区间不包括20。